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.43VAvg_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.47VAvg_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接串口模块的TXGND接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_Div6APB2时钟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位的最大409520次累加最大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)计算温度(℃)上电后立即16501.32923.5运行5分钟16021.29132.3运行15分钟15781.27236.7忙碌计算10分钟15461.24642.8空闲恢复10分钟15891.28134.6可以看到随着芯片持续运行温度逐步上升进入繁忙计算后温度进一步攀升停止高负载后温度又缓慢回落。这个趋势完全符合物理直觉说明传感器工作正常。如果你用手摸一下芯片表面会看到温度明显爬升松手后又会慢慢下降这种直观的反馈很适合给初学者做演示。脚本里加个延时观察温度变化也能很直观地感受到芯片自身发热对结温的影响。3.4 进阶校准用VREFINT修正参考电压前面的代码里我是把参考电压写死成3.3V来计算的。但实际上很多核心板的3.3V并不精确USB供电或者某些LDO输出的3.3V可能有几十毫伏的偏差。VREF不准算出来的VSENSE也就不准最终温度偏差就会变大。STM32内部其实提供了一个解决方法VREFINT。这是一路不随供电电压变化而变化的内部基准电压典型值约1.2V。通过读取VREFINT通道的ADC值可以反推出当前实际的VREFVREF 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次以上取平均减小瞬时噪声校准期间的芯片工作状态要和实际使用保持一致不要在空载校准之后拿去做满载高温监控如果你手头有恒温箱那当然最好没有的话可以借助几个相对恒温的介质做简易校准但精确度会差一些。总之内部温度传感器校准是“一分付出率一分收获”的事校准做得好精度能接近专用温度传感器的水平。结尾就不说什么大道理了分享一个我个人很喜欢的小技巧拿内部温度传感器去观察代码的执行效率。比如你写一段算法跑之前记录一下温度跑完之后再记录温度温度上升的幅度基本能反映这段代码的“暴力程度”。结合功耗计或者示波器电流钳你就能快速找出代码里最耗电、发热最猛的部分。这比单纯读温度有意思多了也是内部温度传感器一个很容易被低估的应用场景。