1. 项目概述:为什么需要关注MCU的内部温度?
在嵌入式开发中,我们常常需要监控系统的运行状态,而温度是一个极其关键的物理量。无论是为了确保芯片在安全范围内工作,防止过热降频甚至损坏,还是为了实现一些与环境温度相关的功能(比如温度补偿),获取温度信息都至关重要。你可能会想到外接一个DS18B20或者DHT11这类数字温度传感器,但对于很多成本敏感、空间受限或者对精度要求不高的应用来说,这无疑增加了BOM成本和PCB面积。
其实,很多现代微控制器(MCU),包括GD32系列,都在芯片内部集成了一个温度传感器。这个传感器就像给MCU装了一个“内置体温计”,它感知的是芯片内核(Die)附近的结温。对于开发者而言,这意味着我们无需任何外部元件,仅通过软件配置ADC(模数转换器)读取一个特定的内部通道,就能获取到芯片的大致温度。这简直是“免费的午餐”,不用白不用。
我在多个工业控制和消费电子项目中都使用过这个功能,主要用它来做两件事:一是系统健康监控,当检测到芯片温度异常升高时,触发报警或采取降频措施;二是在一些对精度要求不高的场合,直接用它替代外部传感器,比如室内环境温度的大致监测、设备内部散热评估等。当然,它的绝对精度通常不如专业的外部传感器(典型误差在±2°C左右),并且测量的是芯片结温而非环境温度,但胜在方便、零成本。本章,我们就来彻底搞懂GD32内部温度传感器的原理、校准方法以及如何写出稳定可靠的读取代码。
2. 内部温度传感器的工作原理与硬件基础
2.1 传感器核心:PN结的电压温度特性
GD32内部温度传感器的本质,是一个基于半导体PN结电压温度特性的传感单元。几乎所有硅基半导体都有一个特性:当流过PN结的电流恒定时,其正向压降(Vf)与温度(T)呈近似线性的负相关关系。也就是说,温度升高,这个压降会减小。内部温度传感器就是利用了这个物理原理。
具体到GD32,这个传感器被设计成一个输出随温度变化的电压源(VTS)。在GD32的参考手册中,你可以找到这样一条关键信息:温度传感器输出电压与温度的关系大致是线性的,其斜率(也称为温度系数)通常是一个负的固定值,例如-4.3 mV/°C(具体值需查对应型号的数据手册)。这意味着温度每升高1°C,传感器输出的电压大约会降低4.3毫伏。
注意:这个斜率是典型值,由于半导体制造工艺的偏差,每一颗芯片的实际斜率会有微小差异。因此,若要获得相对准确的温度,仅靠这个典型值是不够的,我们还需要借助芯片出厂时存储在系统存储器(System Memory)中的校准值。这是提高精度的关键,后文会详细讲解。
2.2 与ADC的接口:专用内部通道
这个传感器产生的模拟电压信号(VTS)并不会直接连接到芯片的外部引脚,而是通过一个内部模拟开关,连接到ADC模块的一个特定输入通道上。在GD32中,这个通道通常是ADC的通道16(对于ADC0)或类似编号(具体请查阅对应型号的数据手册,例如GD32F10x系列是ADC0的通道16)。
我们的任务,就是配置ADC去采样这个通道16上的电压值。ADC将这个模拟电压转换成一个数字量(ADC采样值)。然后,我们通过一个公式,将这个数字量换算成温度值。整个数据通路完全在芯片内部完成,与外部电路无关,这也是其“内部”二字的由来。
2.3 硬件连接与电源影响
虽然传感器是内部的,但其测量精度仍然受到一些外部因素的影响,最主要的就是ADC的参考电压(VREF+)。内部温度传感器输出的电压VTS,是与芯片的VDDA(模拟电源)相关的。ADC在采样VTS时,是以VREF+(通常与VDDA相连)作为参考基准进行量化的。
因此,VDDA/VREF+的电压稳定性直接决定了ADC采样值的稳定性,进而影响温度计算的准确性。如果VDDA波动很大,即使温度没变,ADC读出的数值也会漂移。所以,在要求稍高的应用中,确保模拟电源干净、稳定是首要任务。通常建议使用LDO为VDDA供电,并搭配适当的滤波电容。
3. 开发前的关键准备:数据手册与校准值解读
动手写代码之前,我们必须翻出对应型号的GD32数据手册(Datasheet)和参考手册(Reference Manual)。这是避免盲目操作、写出正确代码的前提。
3.1 查找关键参数
你需要从数据手册中确认以下几个核心参数,它们将直接用于我们的计算公式:
- 内部温度传感器通道号:例如,
ADC0_CHANNEL_16。 - 温度传感器输出电压与温度的关系公式:手册中通常会给出一个近似公式,例如:
VTS = V25 + (T - 25) * SlopeV25:芯片在25°C(常温)时,温度传感器的典型输出电压值(单位:mV或V)。Slope:温度传感器的平均斜率(单位:mV/°C),通常为负值。T:当前温度(°C)。VTS:在温度T时,传感器输出的电压。
- ADC参考电压(VREF+):通常是3.3V。需要确认你的电路设计。
例如,GD32F103系列的数据手册可能给出:V25 = 1.43V,Slope = -4.3 mV/°C。
3.2 理解并获取出厂校准值
如前所述,仅用典型参数计算误差较大。GD32芯片在出厂前,会在特定温度(通常是30°C和110°C)下对内部温度传感器和ADC进行测试,并将实测的ADC采样值(原始数据)写入芯片的系统存储器(只读)中。这些就是出厂校准值。
对于温度传感器,通常有两个校准值:
TS_CAL1:在温度T1(如30°C)下,ADC采样内部温度传感器通道得到的原始值(ADC Data)。TS_CAL2:在温度T2(如110°C)下,ADC采样内部温度传感器通道得到的原始值(ADC Data)。
T1和T2的具体数值(如30和110)也需要在数据手册中确认。这些校准值位于固定的Flash地址。在GD32的标准外设库(如GD32F10x_Firmware_Library)或HAL库中,通常已经以宏定义的形式提供了这些地址和值。
为什么校准值如此重要?因为这两个点(T1, TS_CAL1)和(T2, TS_CAL2)是芯片在真实测试中得到的精确数据。我们可以利用这两点,为当前这颗具体的芯片建立一条独一无二的“温度-ADC值”转换直线,从而最大程度地消除工艺偏差和ADC误差带来的影响。计算过程本质上是一个两点确定一条直线的线性插值。
4. 实战代码:从ADC采样到温度计算
理论铺垫完成,现在进入实战环节。我们将分步骤实现内部温度传感器的读取。
4.1 ADC初始化配置
首先,我们需要初始化ADC,并配置其采样内部温度传感器通道。
#include "gd32f10x.h" void adc_config(void) { /* 1. 使能外设时钟 */ rcu_periph_clock_enable(RCU_GPIOA); // ADC通道可能复用某些GPIO时钟,先开启 rcu_periph_clock_enable(RCU_ADC0); /* 设置ADC时钟分频,确保ADC时钟频率不超过手册规定的最大值(如14MHz) */ rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV6); // 假设APB2时钟72MHz,72/6=12MHz /* 2. 配置ADC工作模式 */ adc_mode_config(ADC_MODE_FREE); // 独立模式 adc_special_function_config(ADC0, ADC_SCAN_MODE, ENABLE); // 扫描模式(如果多通道) adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, DISABLE); // 单次转换 adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); // 数据右对齐 /* 3. 配置通道 */ // 对于内部温度传感器通道,无需配置GPIO,它是内部连接的。 adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 1); // 规则组序列长度为1 // 将内部温度传感器通道(ADC_CHANNEL_16)配置为规则组序列的第一个(序号0) adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_16, ADC_SAMPLETIME_55POINT5); /* 4. 使能内部温度传感器 */ // 这是一个关键步骤!必须使能温度传感器,它才会输出电压。 adc_tempsensor_vrefint_enable(); /* 5. 使能ADC并校准 */ adc_enable(ADC0); delay_ms(1); // 短暂延时,等待ADC稳定 adc_calibration_enable(ADC0); // 执行ADC自校准 }实操心得:
adc_tempsensor_vrefint_enable()这个函数非常重要,它同时使能了温度传感器和内部参考电压模块。内部温度传感器和内部参考电压(VREFINT)通常共享一个使能位。如果不调用此函数,ADC采样到的将是无效数据。另外,使能后最好等待一段时间(几个ms),让传感器稳定工作,再进行采样。
4.2 温度读取与计算函数
接下来,我们编写一个函数来执行一次ADC转换,并根据校准值计算温度。
// 假设我们从库文件或数据手册中已知以下信息(以GD32F103为例): #define TS_CAL1_ADDR ((uint16_t*)0x1FFFF7B8) // 30°C时的校准值地址 #define TS_CAL2_ADDR ((uint16_t*)0x1FFFF7C2) // 110°C时的校准值地址 #define TS_CAL1_TEMP 30 // 校准点1温度 #define TS_CAL2_TEMP 110 // 校准点2温度 float read_internal_temperature(void) { uint16_t adc_raw_value = 0; float temperature = 0.0f; /* 1. 触发ADC转换并读取结果 */ adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL); while(SET != adc_flag_get(ADC0, ADC_FLAG_EOC)); // 等待转换结束 adc_flag_clear(ADC0, ADC_FLAG_EOC); adc_raw_value = adc_regular_data_read(ADC0); /* 2. 从系统存储器读取出厂校准值 */ uint16_t ts_cal1 = *TS_CAL1_ADDR; uint16_t ts_cal2 = *TS_CAL2_ADDR; /* 3. 使用两点校准法计算温度(线性插值) */ // 公式推导:温度与ADC值在有效范围内近似线性。 // 已知两点 (ADC1, T1) 和 (ADC2, T2),求 (ADCx, Tx) // 斜率 k = (T2 - T1) / (ADC2 - ADC1) // Tx = T1 + k * (ADCx - ADC1) // 注意:由于温度传感器电压随温度升高而降低,ADC值也随温度升高而降低。 // 因此,ts_cal2(对应110°C)的ADC值通常小于ts_cal1(对应30°C)。 if (ts_cal2 != ts_cal1) { // 避免除零错误 temperature = (float)TS_CAL1_TEMP + ((float)(adc_raw_value - ts_cal1) * (float)(TS_CAL2_TEMP - TS_CAL1_TEMP)) / (float)(ts_cal2 - ts_cal1); } else { // 校准值异常,使用典型参数公式(精度较差) // 假设VREF=3.3V,ADC 12位分辨率,V25=1.43V, Slope=-0.0043V/°C float vts = (float)adc_raw_value / 4095.0f * 3.3f; // 计算电压值 temperature = 25.0f + (vts - 1.43f) / (-0.0043f); } return temperature; }代码逻辑解析:
- 触发与读取:软件触发一次规则组转换,并等待转换完成标志位(EOC),然后读取ADC数据寄存器中的原始值
adc_raw_value。 - 获取校准值:通过预定义的地址指针,直接读取Flash中存储的两个校准点ADC值。
- 核心计算:采用线性插值法。我们将
(ts_cal1, TS_CAL1_TEMP)和(ts_cal2, TS_CAL2_TEMP)视为一条直线上的两个已知点,当前读取的adc_raw_value对应横坐标,求其纵坐标(温度)。这个公式直接建立了ADC原始值与温度的关系,巧妙地绕开了需要精确知道VREF+电压和传感器斜率的需求,因为校准值已经包含了当前芯片的所有特性。 - 异常处理:如果两个校准值意外相等(概率极低),则回退到使用典型参数(
V25,Slope)和已知VREF+电压的计算方法作为保底。这种方法误差较大,但保证了函数的健壮性。
4.3 主程序逻辑与滤波处理
在实际应用中,单次ADC采样容易受到噪声干扰。为了得到更稳定的温度读数,通常需要进行软件滤波。
int main(void) { float temp_sum = 0; float temperature = 0; uint8_t sample_count = 10; // 系统时钟、延时等初始化 // ... adc_config(); while(1) { temp_sum = 0; // 采样10次取平均 for(int i=0; i<sample_count; i++) { temp_sum += read_internal_temperature(); delay_ms(5); // 每次采样间隔5ms } temperature = temp_sum / sample_count; printf("Chip Temperature: %.2f °C\r\n", temperature); // 根据温度做逻辑判断 if(temperature > 85.0f) { // 温度过高警告,可以触发LED、风扇或降频 // ... } delay_ms(1000); // 每秒更新一次 } }注意事项:
adc_tempsensor_vrefint_enable()会使能一个内部模块,这会增加芯片的功耗。在低功耗应用中,如果不需要频繁读取温度,应在每次读取前使能,读取后禁用(调用adc_tempsensor_vrefint_disable())。但要注意,使能和禁用后,传感器和ADC需要一段稳定时间才能进行准确采样。
5. 精度提升与进阶应用技巧
掌握了基础读取方法后,我们再来探讨如何提升测量精度和可靠性。
5.1 使用内部参考电压(VREFINT)进行补偿
前面提到,ADC的转换依赖于参考电压VREF+的精度。即使我们使用了出厂校准值,如果实际供电的VDDA(作为VREF+)与芯片测试校准时的电压有偏差,或者在工作过程中发生波动,仍然会引入误差。
GD32提供了一个“终极武器”——内部参考电压(VREFINT)。这是一个出厂时经过校准的、非常稳定的内部电压基准(例如1.2V)。它的电压值在芯片测试时也被精确测量,并像温度传感器校准值一样,存储在系统存储器的固定地址(VREFINT_CAL)。
我们可以通过以下步骤进行补偿:
- 读取VREFINT通道(通常是ADC通道17)的ADC原始值
adc_vrefint_raw。 - 从系统存储器读取VREFINT的校准值
vrefint_cal,这个值是在VREF+为典型值(如3.3V)时,测量VREFINT(1.2V)得到的ADC理论值。 - 计算当前实际的VREF+电压:
Vref_actual = (VREFINT_TYPICAL_VOLTAGE * vrefint_cal) / adc_vrefint_raw。其中VREFINT_TYPICAL_VOLTAGE是1.2V。 - 在计算温度(或其他任何ADC通道的值)时,使用这个计算出的
Vref_actual来代替理想值(3.3V),或者更直接地,用比例系数来修正ADC值。
修正后的温度读取函数思路:
uint16_t read_adc_with_vref_compensation(adc_channel) { // 1. 读取目标通道ADC值 adc_target_raw // 2. 读取VREFINT通道ADC值 adc_vrefint_raw // 3. 获取VREFINT校准值 vrefint_cal // 4. 计算修正后的ADC值: corrected_adc = adc_target_raw * vrefint_cal / adc_vrefint_raw // 5. 使用 corrected_adc 参与后续温度计算(两点校准法) // 这样,无论VREF+实际是多少,我们都能将其“归一化”到校准时的基准。 }这种方法几乎可以消除电源电压波动带来的所有ADC测量误差,是追求高精度测量的必备技巧。
5.2 测量的是结温,不是环境温度
务必牢记,内部温度传感器测量的是芯片硅核的结温(Junction Temperature),它通常比环境温度(Ambient Temperature)或芯片表面温度要高。其差值取决于芯片的功耗(运行频率、外设活动情况、IO负载等)和散热条件。
- 芯片空载、低速运行时,结温可能只比环境温度高几度。
- 芯片全速运行、外设全开、驱动大电流IO时,结温可能比环境温度高出20-30°C甚至更多。
因此,如果你想用内部传感器估算环境温度,必须在芯片处于低功耗休眠或空闲状态下进行测量,并且需要根据芯片的封装热阻(ΘJA)和功耗进行粗略估算,这非常不精确。内部温度传感器的正确用途是监控芯片自身的工作状态,防止过热。
5.3 低功耗模式下的使用策略
在电池供电的设备中,需要精细管理功耗。内部温度传感器和ADC模块都消耗电流。建议的策略是:
- 间歇性测量:根据应用需求,设定一个较长的测量间隔(如每10秒或每分钟测量一次),而不是持续测量。
- 按需使能:在每次测量序列开始时,依次使能ADC时钟、温度传感器、ADC模块,执行校准、采样、计算。测量序列结束后,立即禁用温度传感器和ADC模块,甚至可以关闭其时钟。
- 利用唤醒源:可以将定时器RTC或低功耗定时器(LPTIM)配置为唤醒源,从Stop等低功耗模式唤醒,完成一次温度测量后,判断是否需要处理,然后再进入休眠。
6. 常见问题与调试排查实录
在实际开发中,你可能会遇到以下问题:
问题1:读出的温度值固定不变或是一个明显错误的常数值(如0, 4095, 或某个中间值)。
- 排查思路:
- 检查传感器使能:确认是否调用了
adc_tempsensor_vrefint_enable()。这是最容易被忽略的一步。 - 检查ADC配置:确认ADC时钟配置是否正确(未超频),通道号是否配置为正确的内部传感器通道(如
ADC_CHANNEL_16)。 - 检查转换触发与标志位:单次转换模式下,是否在每次读取前都触发了转换?是否在等待EOC标志位后才读取数据?读取后是否清除了标志位?
- 检查参考电压:测量VDDA引脚的实际电压,是否与代码中计算用的参考电压值(如3.3)相符?电压是否稳定?
- 使用调试器查看寄存器:在调试模式下,查看ADC的
STAT寄存器,确认EOC标志是否置位;查看RDATA寄存器,看ADC转换结果是否在变化。
- 检查传感器使能:确认是否调用了
问题2:读出的温度值跳动很大,噪声明显。
- 排查思路:
- 硬件滤波:检查VDDA和VSSA的电源滤波是否良好。靠近芯片的管脚处,应并联一个10uF的钽电容和一个0.1uF的陶瓷电容。
- 软件滤波:如示例所示,采用多次采样取平均的方法。可以尝试均值滤波、中值滤波或一阶低通数字滤波。
- 采样周期:适当增加ADC的采样周期(
ADC_SAMPLETIME_55POINT5可以改为更长的ADC_SAMPLETIME_239POINT5),给内部的采样保持电容更长的充电时间,对高阻抗源(内部传感器可视为高阻抗源)尤其有效。 - 环境干扰:确保MCU远离电机、继电器、开关电源等强噪声源。
问题3:读出的温度值与实际环境温度偏差很大(例如,室温25°C时读到40°C)。
- 排查思路:
- 理解“结温”:首先确认你测量的是芯片结温。用手触摸芯片,如果感觉温热,说明芯片自身在发热。尝试将芯片置于休眠模式,仅保留ADC和温度传感器工作,再测量,此时读数会更接近环境温度。
- 校准值使用错误:确认从系统存储器读取校准值的地址是否正确。确认用于计算的
TS_CAL1_TEMP和TS_CAL2_TEMP常量是否与数据手册标注的校准温度点一致。 - 计算溢出:检查温度计算函数中的数据类型和运算顺序。确保使用了浮点数(
float)或进行定点数运算时处理了精度问题。(adc_raw_value - ts_cal1)这类减法可能产生负数,要确保后续乘法除法能正确处理。
问题4:在低功耗模式下唤醒后,第一次温度读数不准。
- 排查思路:
- 稳定时间:从低功耗模式唤醒ADC和温度传感器后,需要足够的稳定时间。在使能传感器和ADC校准之后,增加一个显著的延时(例如10-50ms)再进行第一次采样。
- 重新校准:某些GD32型号的ADC在退出低功耗模式后,可能需要重新执行校准(
adc_calibration_enable)。查阅参考手册中关于ADC低功耗行为的描述。
调试技巧:
- 分段验证:先写一个简单的ADC代码,去读取一个已知的外部电压(如通过电阻分压得到的1.65V),验证整个ADC配置和读取流程是否正确。然后再切换到内部温度传感器通道。
- 打印原始ADC值:在计算温度之前,先将原始的
adc_raw_value、ts_cal1、ts_cal2通过串口打印出来。观察原始ADC值是否在一个合理的范围内(通常介于ts_cal2和ts_cal1之间)。这能帮你快速定位问题是出在ADC采样阶段,还是温度计算阶段。 - 查阅勘误手册:对于特定的GD32型号,去官网查找最新的芯片勘误手册(Errata Sheet)。偶尔会有关于内部温度传感器或ADC在特定模式下的已知问题及解决方案。
掌握了内部温度传感器的原理和这些实战技巧,你就能在GD32项目中游刃有余地实现可靠的芯片温度监控功能。这个看似简单的功能,背后涉及了模拟电路知识、ADC应用、校准思想和低功耗设计,是锻炼嵌入式开发者综合能力的一个很好的切入点。