STM32学习系列写到第24篇,今天换个轻松但也容易踩坑的话题:内部温度传感器。很多初学者一听“温度传感器”,第一反应是外接一个DS18B20或者NTC热敏电阻,其实STM32芯片内部本身就集成了一颗温度传感器,它不占用额外的GPIO,不需要买任何器件,只要把ADC配置好,就能直接读出芯片附近的温度。这个功能在监测芯片发热、评估负载、做简单过温保护时非常实用,适合正在学ADC外设、想搞清楚数据手册公式、以及准备做小型温控方案的开发者参考。
不过开始之前,我得先泼一盆冷水:内部温度传感器测的是“芯片内部结温”,不是室温。芯片运行起来之后,结温一般会比环境温度高几度到十几度,负载越重温差越大。搞不清这个概念,实验时很容易被“怎么比室温高这么多”的问题带偏。下面我把原理、公式、代码、校准方法和踩过的坑一次性讲清楚。
1. 内部温度传感器的真面目:原理与设计思路
1.1 一个藏在ADC里的温度计:硬件结构
STM32内部温度传感器并不是一个独立外设,它挂在ADC模块内部,你可以把它理解成“ADC多了一个隐藏通道”。以我手头的STM32F103为例,温度传感器对应ADC1的通道16,另一个内部参考电压VREFINT对应通道17。芯片内部利用了一只工作在线性区的PN结,温度变化时PN结正向压降会近似线性变化,传感器把这种变化调理成模拟电压信号,最终由ADC量化成数字量。
整个链路可以写成:温度变化 → PN结压降变化 → 模拟电压变化 → ADC数值变化。
这个设计思路最大的优势是零成本零布线。你想监测芯片是不是过热,不需要在板子上额外焊传感器,直接在代码里读ADC就能拿到温度数据。对硬件工程师来说,这是在产品设计阶段就能留好的“后门”,PCBA打样回来先不接热敏电阻,也能快速验证芯片发热问题。
内部温度传感器的绝对精度并不高,大概在±2℃到±4℃这个量级,但用于趋势监测和过温保护完全够用。你要是指望它达到专用数字温度传感器的精度,那得先做校准,这个后面我会单独讲。
1.2 电压转温度:那个必须吃透的公式
数据手册里给的换算公式是这样的:
T(℃) = [(V25 - VSENSE) / Avg_Slope] + 25
其中:
- V25:传感器在25℃时对应的输出电压,典型值1.43V
- Avg_Slope:传感器输出电压随温度变化的平均斜率,典型值4.3mV/℃
- VSENSE:实际从ADC读到的电压值
我习惯用一个具体数字来理解公式。假设ADC是12位分辨率,参考电压VREF取3.3V,读到的ADC原始值是1550,那么:
VSENSE = 1550 × 3.3 / 4096 ≈ 1.248V
代入公式:
T = (1.43 - 1.248) / 0.0043 + 25 ≈ 42.3℃
也就是说,当前芯片内部温度大约42.3℃。这个计算过程很简单,但实际项目里翻车的人非常多,我至少见过三种搞法:
第一,电压单位混用。V25是伏特,Avg_Slope是mV/℃,有人直接把1.43V当成1430mV带入公式,结果温度差出几百倍。第二,忘记减25再除斜率,而是先除再减,顺序反了之后,温度曲线整体平移,结果对不上。第三,VREF写死3.3V,但实际板子供电是3.28V,系统性偏差就这么来的。
所以做这类型换算的时候,我总会在代码注释里把单位标得清清楚楚,避免过几天回头看代码时又把自己绕进去。
1.3 为什么数据手册只给“典型值”
我第一次用这个传感器,拿典型值直接算,结果和用万用表实测的板级温度差了将近5℃,当时很怀疑是不是买到了有问题的芯片。后来我仔细翻芯片手册的电气特性章节,才看到一个容易被忽略的细节:V25的范围是1.41V到1.47V,Avg_Slope的范围是3.8mV/℃到4.7mV/℃。
换句话说,芯片手册给的1.43V和4.3mV/℃只是所有芯片统计出来的中间值,不是你手上这颗芯片的真实值。每颗芯片出厂时PN结特性不完全一致,这和很多人手冲咖啡时不同批次豆子的风味差异差不多,原理相同、个体有别。
这就带来一个非常现实的问题:直接用典型值算出的温度,绝对误差可能有好几度。如果应用场景只是判断温度有没有超过80℃这种粗略阈值,问题不大;但如果你在做一个需要温度显示给用户看的产品,就必须做校准。
校准思路其实不复杂:已知真实温度,反推个性参数。把芯片放在已知温度环境里,读取一次VSENSE,就能算出一组“实际V25”,再结合另一个温度点或已知斜率,把偏移和斜率同时修正。后面我会详细说操作步骤。
2. 实验前的准备与关键参数
2.1 硬件与开发环境准备
这次实验我用了一块常见的STM32F103C8T6核心板,配套USB转TTL串口模块进行数据输出。硬件清单如下:
- STM32F103C8T6核心板一块
- USB转TTL串口模块一个
- 杜邦线若干
- 面包板(可选,方便固定线缆)
- 电脑端安装好Keil MDK和串口调试助手
接线只需要三根线:核心板的TX接串口模块的RX,核心板的RX接串口模块的TX,GND接GND。很多新手第一次接串口时会把TX和RX接反,导致串口助手完全没输出,检查半天才发现是交叉线没交叉好。注意这里不是同名直连,是交叉连接。
开发环境上,我用的是标准外设库。如果你用的是HAL库,实现思路完全一样,只是函数名和初始化结构体名称不同。这个实验不需要接任何外部温度传感器硬件,这一点一定要记住,内部温度传感器是藏在ADC里面的,外部找不到对应引脚。
2.2 ADC与内部温度传感器的初始化要点
内部温度传感器初始化的核心是两件事:第一,正常初始化ADC模块;第二,使能温度传感器通道。要注意的是,内部温度传感器在默认状态下是关闭的,不使能的话,即使配置了通道16,读取出来的值也是无效的。
我用的标准外设库配置代码如下:
void TEMP_ADC_Init(void) { ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_TempSensorVrefintCmd(ENABLE); ADC_Cmd(ADC1, ENABLE); }这段代码里,ADC_TempSensorVrefintCmd(ENABLE) 是同时使能内部温度传感器和内部参考电压VREFINT的。如果你用寄存器操作,需要把ADC_CR2寄存器的TSVREFE位置1。我自己用过一段时间寄存器版本之后,才对内部结构理解得更透彻,建议有时间的话也去翻一翻寄存器描述。
初始化时我没有设置外部触发,而是用软件触发模式。这样每次启动转换的时机完全可控,方便调试时一步一步看结果。
2.3 时钟与采样时间怎么定
ADC的时钟频率直接决定转换速度和稳定性。STM32F103的ADC最大时钟是14MHz,我选择RCC_PCLK2_Div6,APB2时钟72MHz除以6得到12MHz,留了一些余量,同时保证采样精度。
采样时间的配置更关键。内部温度传感器的输出阻抗相对较高,如果采样时间太短,采样电容来不及充满,得到的ADC值就会偏低,而且噪声会明显变大。我直接在配置函数里用ADC_SampleTime_239Cycles5,也就是最长的239.5个周期,优先保证稳定性。
这里插一句经验:温度测量不是高速采样场景,完全不需要追求极短的采样时间。我用1.5个周期采样时,温度读数能来回跳好几度,改成239.5个周期之后,波动立刻降到0.2℃以内。这个对比效果非常直观,建议你自己动手试一次,感受会比看我描述深刻得多。
还有一个容易忽略的点:ADC刚上电时需要一个短暂的稳定时间,不要刚完成初始化就立刻开始转换。我一般会在初始化函数后面加100微秒到1毫秒级别的延时,或者通过标志位等待ADC准备好,这样读出来的第一个值才可靠。
3. 实验代码与实测过程
3.1 通道选择与单次转换实现
读取内部温度,本质上就是读取ADC通道16的单次转换结果。我没有选择连续转换模式,因为温度变化很慢,连续转换除了增加功耗之外并没有实际意义。每次需要数据时,软件触发一次转换就够了。
uint16_t TEMP_GetAdcValue(void) { ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 1, ADC_SampleTime_239Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); return ADC_GetConversionValue(ADC1); }这段代码的逻辑非常直接:重新配置通道16为规则通道,然后软件启动转换,等待EOC标志位置位,最后返回ADC数据寄存器里的转换结果。
从调试角度说,单次转换模式比连续模式好用得多。每一步都是确定性的,一旦结果异常,可以很快判断是配置错误、通道配置错误还是换算错误。连续转换虽然省了触发时间,但遇到问题时装了一大堆逻辑去排查,反而浪费时间。
3.2 温度换算与平均处理
拿到ADC原始值之后,换算成温度之前,我会做一次数字滤波,也就是多次采样取平均。温度信号本身是缓变的,随机噪声叠加在上面,靠一次采样很难得到稳定读数。
float TEMP_Calculate(uint16_t adcValue) { float vsense = (float)adcValue * 3.3f / 4096.0f; float temperature = (1.43f - vsense) / 0.0043f + 25.0f; return temperature; } float TEMP_GetAverage(uint8_t times) { uint32_t sum = 0; uint8_t i; for (i = 0; i < times; i++) { sum += TEMP_GetAdcValue(); } return TEMP_Calculate((uint16_t)(sum / times)); }平均次数我一般取10到20次。太少的话噪声压不下去,太多的话主循环会被阻塞,影响其他任务调度。温度一秒读一次就够了,每次读10次采样取平均,完全能满足监测需求。
这里有个细节要注意:累加变量务必用uint32_t,不要用uint16_t。ADC原始值是12位的,最大4095,20次累加最大81900,已经超过uint16_t的65535上限了。我第一次写的时候没注意,累加和溢出导致温度算出来完全对不上,输出结果跳来跳去,还以为是传感器有问题,后来加打印才发现是累加变量的问题。
3.3 串口输出与现象记录
实验主程序里,我每秒通过串口打印一次ADC原始值、模拟电压和温度,方便直接观察数据变化:
int main(void) { uint16_t adc; float temp; USART1_Init(115200); TEMP_ADC_Init(); while (1) { adc = TEMP_GetAdcValue(); temp = TEMP_Calculate(adc); printf("ADC=%d, VSENSE=%.3fV, TEMP=%.2fC\r\n", adc, temp); Delay_ms(1000); } }下面是某次实测记录,芯片空载运行,室温大约26℃:
| 时间点 | ADC原始值 | 计算电压(V) | 计算温度(℃) |
|---|---|---|---|
| 上电后立即 | 1650 | 1.329 | 23.5 |
| 运行5分钟 | 1602 | 1.291 | 32.3 |
| 运行15分钟 | 1578 | 1.272 | 36.7 |
| 忙碌计算10分钟 | 1546 | 1.246 | 42.8 |
| 空闲恢复10分钟 | 1589 | 1.281 | 34.6 |
可以看到,随着芯片持续运行,温度逐步上升;进入繁忙计算后温度进一步攀升,停止高负载后温度又缓慢回落。这个趋势完全符合物理直觉,说明传感器工作正常。
如果你用手摸一下芯片表面,会看到温度明显爬升,松手后又会慢慢下降,这种直观的反馈很适合给初学者做演示。脚本里加个延时观察温度变化,也能很直观地感受到芯片自身发热对结温的影响。
3.4 进阶校准:用VREFINT修正参考电压
前面的代码里,我是把参考电压写死成3.3V来计算的。但实际上很多核心板的3.3V并不精确,USB供电或者某些LDO输出的3.3V可能有几十毫伏的偏差。VREF不准,算出来的VSENSE也就不准,最终温度偏差就会变大。
STM32内部其实提供了一个解决方法:VREFINT。这是一路不随供电电压变化而变化的内部基准电压,典型值约1.2V。通过读取VREFINT通道的ADC值,可以反推出当前实际的VREF:
VREF = VREFINT典型值 × 4096 / VREFINT原始ADC值
具体做法是:先读取通道17的ADC值,算出实际VREF,再用这个实际VREF替换写死的3.3V,代入温度转换公式重新计算VSENSE。
uint16_t adcRef = 0; float vrefActual = 0.0f; float vsense = 0.0f; float temp = 0.0f; adcRef = GetVrefintValue(); vrefActual = 1.2f * 4096.0f / (float)adcRef; vsense = (float)GetTempAdcValue() * vrefActual / 4096.0f; temp = (1.43f - vsense) / 0.0043f + 25.0f;用VREFINT做修正之后,温度读数的稳定性和可重复性会好一截。尤其是供电电压不稳定的场景,比如锂电池供电或者带电噪声的设备里,这招特别管用。
要提醒的是,读取VREFINT时也要配置足够的采样时间,不能因为它是基准电压就草率处理。VREFINT和温度传感器一样需要稳定时间,读取前等待一下效果更好。
4. 常见问题与排查技巧实录
4.1 读出的温度异常偏高或偏低
这是实验里最容易出现的问题。温度高得离谱,比如七八十度起步,多半是计算时混用了单位,把1.43V当成1430mV代入公式,或者公式里少除了斜率后面的1000倍。温度低到0℃以下,多半是ADC读数太低,最常见原因是内部温度传感器没有使能,通道16读到的值是随机噪声或者上次转换的残留值。
我用了一个固定排查顺序,基本能解决绝大多数异常读数:
- 确认是否调用了ADC_TempSensorVrefintCmd(ENABLE)或者设置了TSVREFE位
- 确认ADC_RegularChannelConfig配置的通道确实是ADC_Channel_16
- 确认换算函数里电压单位统一,V25、Avg_Slope、VSENSE的单位没有混用
- 确认参考电压VREF代入值和实际供电电压一致
按这个顺序走一遍,一般两分钟内就能定位问题。我自己调试的时候,有一半以上的时间是花在这个排查过程中的,而不是纠结于复杂的算法。
4.2 温度读数波动很大
先检查采样时间。我在1.5个周期和239.5个周期下分别测过,前者温度波动能到好几度,后者稳定在0.2℃以内,差距非常大。建议直接把采样时间配到最长,稳定性优先,尤其做温度这种缓变信号。
其次要多次采样取平均,这是简单有效的数字滤波方式。但要注意采样频率不能太高,如果每隔几毫秒就连续采样,芯片本身会因为ADC频繁工作而额外发热,导致温度读数比真实结温高出一两度。我实测过高频连续采样几分钟后,温度读数会缓慢爬升,这不是传感器漂移,是芯片自热在叠加。
还有一点容易被忽略:复位后第一次转换的结果可能偏大,大概率是ADC在复位后没有完全稳定。我习惯在初始化之后加入一个等待延时,比如20毫秒,让ADC和内部传感器都稳定了再开始读数据,能明显减少首次采样的异常。
4.3 测的“室温”为什么比环境温度高
这是每个初玩内部温度传感器的人都会有的困惑。必须再强调一次:内部温度传感器测的是芯片结温,不是环境温度。芯片工作时的功耗会转化为热量,芯片外壳和内部PN结都会因此高于环境温度,负载越高温差越大。
所以千万不要拿内部温度传感器当电子温度计去测室温,它的设计目的是监测芯片自身状态,不是取代外部温度传感器。如果非要用它来估计环境温度,可以在芯片完全空闲、没有大电流外设运行、且ADC采样频率很低的情况下读取,读到的数值会比高负载时接近环境温度,但依然不能等同。
明白这一点之后,调试心态会好很多。不然你会在“读数为什么偏高”这件事上纠结很久,实际上元器件没有坏,代码也没有问题,只是物理规律如此。
4.4 关于校准的个人心得
做学习验证时,直接用典型值换算就行。但做实际产品,我会做两点校准,方法如下:
把板子放在可调温的恒温环境里,选择两个温度点,比如25℃和50℃,分别记录ADC值,然后反推这颗芯片实际的V25和斜率。两个温度点之间跨度越大,拟合出的斜率和偏移就越可靠。校准结果写死在程序里,后续代码统一用校准参数。
根据我的经验,在校准过程中需要注意几点:
- 温度环境必须均匀稳定,不要放在风口或者阳光直射的地方
- 每个温度点的稳定等待时间要足够长,至少十分钟
- 校准读取时采样次数要更多,最好做50次以上取平均,减小瞬时噪声
- 校准期间的芯片工作状态要和实际使用保持一致,不要在空载校准之后拿去做满载高温监控
如果你手头有恒温箱那当然最好,没有的话可以借助几个相对恒温的介质做简易校准,但精确度会差一些。总之,内部温度传感器校准是“一分付出率一分收获”的事,校准做得好,精度能接近专用温度传感器的水平。
结尾就不说什么大道理了,分享一个我个人很喜欢的小技巧:拿内部温度传感器去观察代码的执行效率。比如你写一段算法,跑之前记录一下温度,跑完之后再记录温度,温度上升的幅度基本能反映这段代码的“暴力程度”。结合功耗计或者示波器电流钳,你就能快速找出代码里最耗电、发热最猛的部分。这比单纯读温度有意思多了,也是内部温度传感器一个很容易被低估的应用场景。