1. 为什么NTC热敏电阻配STM32是个好组合
温度测量这件事,看起来简单,做起来坑不少。我最早接触温度采集是在一个工业控制项目里,当时用DS18B20,一线总线,精度还行,但响应速度慢,而且多点组网时时序要求苛刻,线一长就容易出问题。后来换到NTC热敏电阻加STM32的方案,才发现这个组合在成本和灵活性上优势明显——一颗NTC几毛钱,STM32的ADC又是片内自带,整体BOM成本极低,而且采样速度可以做到毫秒级,适合需要快速响应的场景。
NTC是Negative Temperature Coefficient的缩写,意思是负温度系数热敏电阻,温度越高阻值越低。它的阻值变化不是线性的,而是一条指数曲线,这也是为什么很多人觉得NTC“不好用”——直接读ADC值换算温度,误差能大到离谱。但只要你掌握了正确的换算方法,NTC的精度可以做到±0.1℃甚至更高,完全能满足大多数工业和消费类场景的需求。
STM32这边,几乎全系列都带12位ADC,像F103、F407、G0、G4、H7这些常用型号,ADC采样率从1Msps到5Msps不等,分辨率也有16位可选。用STM32的ADC去采集NTC分压后的电压,再通过算法换算成温度,整个链路非常清晰。我实测下来,用STM32F103加一颗10kΩ B值3950的NTC,在0到100℃范围内,经过校准后精度可以稳定在±0.3℃以内,如果加一点滤波和线性化处理,还能更好。
这篇文章适合谁看?如果你正在做基于STM32的温度采集项目,或者毕业设计里需要测温模块,又或者你想搞清楚ADC采样和NTC换算的完整链路,那这篇内容就是为你准备的。我会从硬件电路设计讲到代码实现,再到校准和排错,把我在实际项目中踩过的坑和总结的技巧都摊开来讲。代码部分我会给出完整的C语言实现,基于HAL库,可以直接移植到你的工程里。
2. 硬件电路设计与关键参数计算
2.1 NTC分压电路的基本结构
NTC测温最常用的电路就是分压法:一个NTC和一个固定电阻串联,中间节点接到STM32的ADC引脚。NTC接在VCC和ADC引脚之间,固定电阻接在ADC引脚和GND之间,这样温度升高时NTC阻值减小,ADC引脚电压升高。反过来接也可以,但逻辑会反过来,我习惯用NTC上拉的方式,因为这样温度越高ADC读数越大,符合直觉。
这里有个关键点:固定电阻的阻值选择。很多人随便拿一个10kΩ就往上焊,结果发现低温段分辨率很差。固定电阻的阻值应该选在你要测量的温度范围中点对应的NTC阻值附近。比如你测0到100℃,中点50℃时NTC阻值大概是3.6kΩ(以10kΩ B3950为例),那固定电阻选3.3kΩ或3.6kΩ会比10kΩ好很多。为什么?因为分压电路在固定电阻和NTC阻值相等时,电压对阻值的变化率最大,也就是灵敏度最高。
我一般会先确定测量范围,然后查NTC的R-T表,找到中点温度对应的阻值,再选最接近的标准电阻值。如果你手头只有10kΩ的电阻,那测量范围最好设在20到80℃之间,这样中点50℃时NTC阻值3.6kΩ,和10kΩ差距不算太大,勉强能用,但低温段精度会明显下降。
2.2 参考电压的选择与噪声抑制
STM32的ADC参考电压通常用VDDA,也就是3.3V。但3.3V这个电源往往同时给MCU和其他外设供电,纹波和噪声不小。如果你对精度要求高,建议用独立的基准电压芯片,比如REF3033,输出3.0V或3.3V,噪声能低一个数量级。我试过在F103上用VDDA直接做参考,ADC读数跳动大概在±3个LSB,换成外部基准后跳动降到±1个LSB以内。
另外,ADC引脚前面一定要加RC低通滤波。NTC的响应时间通常在几秒到几十秒,所以滤波截止频率可以设得很低,比如10Hz甚至1Hz。我一般用1kΩ电阻加1μF电容,截止频率约160Hz,能滤掉大部分高频噪声,又不会影响NTC本身的响应速度。如果你测的是快速变化的温度,比如水流温度,那电容可以减小到0.1μF。
还有一点容易被忽略:NTC的引线。如果NTC离STM32比较远,比如超过10厘米,引线上的分布电容和电感会引入干扰。这时候建议用屏蔽线,或者把NTC的模拟部分和数字部分在PCB上分开布局,模拟地单点接地。我在一个电机控制项目里,NTC引线只有5厘米,但电机一启动ADC读数就乱跳,后来在NTC两端并了一个0.1μF电容,问题就解决了。
2.3 自热效应与激励电流
NTC是有自热效应的——电流流过NTC会产生热量,导致测量值偏高。这个误差在测量精度要求高的时候必须考虑。NTC的功耗应该控制在0.1mW以下,最好在0.01mW量级。怎么算?假设NTC阻值10kΩ,流过的电流是I,功耗P=I²R。如果P要小于0.01mW,那I要小于约31.6μA。分压电路里,电流由VCC和总阻值决定,3.3V除以(10k+10k)=165μA,功耗约0.27mW,偏大了。
解决办法有两个:一是增大固定电阻和NTC的阻值,比如都用100kΩ,电流降到16.5μA,功耗就下来了;二是用间歇供电,只在采样时给NTC通电,采样完就断电。我一般用第一种方法,因为简单可靠。但阻值大了之后,ADC输入阻抗的影响就不可忽略。STM32的ADC输入阻抗在采样时间足够长的情况下可以认为是无穷大,但采样时间短的时候等效输入阻抗会下降。所以用100kΩ级别时,ADC采样时间要设长一些,比如239.5个周期,确保采样电容充分充电。
3. NTC温度换算的数学原理与代码实现
3.1 从ADC值到电阻值的换算
STM32的ADC是12位的,读数范围0到4095,对应电压0到VDDA。假设NTC上拉,固定电阻下拉,ADC读数记为adc_val,则ADC引脚电压V_adc = adc_val / 4095 * VDDA。根据分压公式,V_adc = VDDA * R_fixed / (R_ntc + R_fixed),整理得R_ntc = R_fixed * (VDDA / V_adc - 1) = R_fixed * (4095 / adc_val - 1)。
这个公式很简单,但有个坑:adc_val不能为0,否则除零。实际电路中,如果NTC短路或ADC引脚接地,adc_val会是0。所以代码里要加保护,adc_val小于某个阈值时直接返回错误或上限温度。我一般设adc_val最小为10,对应R_ntc约409倍R_fixed,已经远超正常范围了。
另外,VDDA的实际值可能不是精确的3.3V,比如3.28V或3.32V。如果你用VDDA做参考,那VDDA的误差会直接传递到结果里。所以要么用外部基准,要么在代码里用实测的VDDA值。我通常会在板子上留一个测试点,用万用表量一下VDDA,然后把实际值写进代码。
3.2 Steinhart-Hart方程与B值公式
NTC的阻值和温度关系最准确的描述是Steinhart-Hart方程:1/T = A + Bln(R) + Cln(R)³,其中T是开尔文温度,R是阻值,A、B、C是常数。这个方程精度最高,但需要三个系数,而且计算量大,在单片机上跑有点吃力。
更常用的是B值公式:1/T = 1/T0 + (1/B) * ln(R/R0),其中T0是参考温度(通常25℃=298.15K),R0是T0时的阻值(比如10kΩ),B是B值(比如3950)。这个公式只需要两个参数,计算量小,在0到100℃范围内精度足够。我实测过,B值公式和Steinhart-Hart方程在0到100℃内的最大偏差约0.2℃,对大多数应用足够了。
B值公式的代码实现要注意:ln函数在math.h里,但STM32的HAL库默认可能没开浮点运算。F103没有FPU,浮点运算是软件模拟,速度慢但能用。如果你用的是F4或G4,有硬件FPU,那浮点运算就很快。我一般会把B值公式里的常数预先算好,减少运行时计算量。比如1/T0和1/B都是常数,可以提前算出来。
3.3 完整代码实现与注释
下面是我在实际项目中用的代码,基于STM32 HAL库,ADC用DMA采样,多次平均后换算温度。代码里包含了ADC读取、电阻计算、温度换算和简单的滤波。
#include "main.h" #include <math.h> #define R_FIXED 10000.0f // 固定电阻阻值,单位欧姆 #define R0 10000.0f // NTC在25℃时的阻值 #define B_VALUE 3950.0f // NTC的B值 #define T0 298.15f // 25℃对应的开尔文温度 #define ADC_MAX 4095.0f // 12位ADC最大值 #define VDDA 3.3f // 参考电压 extern ADC_HandleTypeDef hadc1; // 读取ADC并换算成NTC阻值 float Get_NTC_Resistance(void) { uint32_t adc_sum = 0; uint16_t adc_val = 0; float v_adc, r_ntc; // 采样16次取平均,降低噪声 for (int i = 0; i < 16; i++) { HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); adc_sum += HAL_ADC_GetValue(&hadc1); } adc_val = adc_sum / 16; // 防止除零和异常值 if (adc_val < 10) adc_val = 10; if (adc_val > 4085) adc_val = 4085; // 计算NTC阻值 r_ntc = R_FIXED * (ADC_MAX / (float)adc_val - 1.0f); return r_ntc; } // 根据NTC阻值计算温度,返回摄氏度 float Get_Temperature(void) { float r_ntc = Get_NTC_Resistance(); float ln_r = logf(r_ntc / R0); float inv_t = 1.0f / T0 + ln_r / B_VALUE; float temp_k = 1.0f / inv_t; float temp_c = temp_k - 273.15f; return temp_c; }这段代码里,采样16次取平均是最简单的滤波,能有效抑制随机噪声。如果你需要更平滑的曲线,可以用一阶低通滤波:new_val = alpha * old_val + (1-alpha) * new_val,alpha取0.1到0.3之间。我一般用0.2,响应速度和平滑度比较平衡。
还有一点:logf函数在math.h里,链接时需要加-lm。在Keil里,需要在工程设置里勾选Use MicroLIB,或者手动添加math库。如果你用的是GCC,在Makefile里加-lm就行。
4. 精度提升与校准实战
4.1 两点校准法的具体操作
NTC的标称B值和R0都有公差,通常±1%到±5%,直接套公式会有几度的误差。要提升精度,必须校准。最实用的方法是两点校准:在冰水混合物(0℃)和沸水(100℃,注意气压修正)中各测一次,记录ADC读数,然后反推实际的R0和B值。
具体操作:先把NTC放进冰水混合物里,等读数稳定(大概2到3分钟),记录ADC值adc_0和对应的阻值R_0_actual。再放进沸水里,记录adc_100和R_100_actual。然后根据B值公式反推:B_actual = ln(R_0_actual / R_100_actual) / (1/T_0 - 1/T_100),其中T_0=273.15K,T_100=373.15K。R0_actual就是R_0_actual。把这两个值写回代码,精度能提升到±0.1℃以内。
我实测过,一颗标称10kΩ B3950的NTC,未校准前在50℃时误差约1.5℃,两点校准后误差降到0.2℃。这个提升非常明显,而且操作不复杂,值得花时间做。
4.2 分段线性插值法
如果你觉得浮点运算太慢,或者MCU没有FPU,可以用分段线性插值。把NTC的R-T表按温度分成若干段,每段用线性近似。比如每10℃一段,在0到100℃内分10段,每段存两个端点阻值和温度,运行时先查表找到所在段,再线性插值。这样计算量小,精度也能做到±0.5℃以内。
分段线性插值的代码实现:定义一个结构体数组,每个元素包含阻值下限、阻值上限、温度下限、温度上限。运行时遍历数组,找到R_ntc所在的区间,然后temp = temp_low + (R_ntc - R_low) * (temp_high - temp_low) / (R_high - R_low)。这个方法在8位单片机上都能跑,STM32更是绰绰有余。
4.3 常见误差来源与对策
NTC测温的误差来源主要有几个:NTC本身的公差、固定电阻的公差、VDDA的误差、ADC的量化误差、自热效应、引线电阻。NTC和固定电阻建议用1%精度的,VDDA用外部基准,ADC量化误差可以通过过采样和平均降低,自热效应通过增大阻值或间歇供电解决,引线电阻在引线短的时候可以忽略。
还有一个容易被忽略的:ADC的偏移误差和增益误差。STM32的ADC有出厂校准值,可以在初始化时调用HAL_ADCEx_Calibration_Start()进行校准。我试过,校准前后ADC读数在零点附近能差2到3个LSB,对低温段测量影响不小。所以每次上电后先做一次ADC校准,是个好习惯。
5. 常见问题排查与实操避坑指南
5.1 ADC读数跳动大怎么办
ADC读数跳动是最常见的问题。先检查硬件:VDDA和VSSA之间有没有加去耦电容?我一般加100nF和10μF并联,靠近MCU引脚。ADC引脚前面有没有RC滤波?没有的话加1kΩ加1μF。NTC引线有没有屏蔽?没有的话尽量缩短,或者用双绞线。
软件上,增加采样次数取平均是最直接的办法。我一般采16次,如果还跳,就采64次。但采样次数太多会影响响应速度,所以要权衡。另外,ADC的采样时间要设够,特别是用高阻值NTC时。STM32F103的ADC采样时间可选1.5到239.5个周期,我一般用71.5或239.5,确保采样电容充满。
还有一个坑:如果ADC引脚同时被其他外设复用,比如用作GPIO输出,那读数肯定乱。检查一下GPIO初始化,确保ADC引脚配置为模拟输入模式,不要上下拉。
5.2 温度值跳变或明显错误
温度值跳变通常是因为ADC值在临界点附近波动,导致换算出的阻值跳变。比如adc_val在2047和2048之间跳,阻值就会跳几十欧姆,温度跳零点几度。解决办法是加迟滞,或者用滑动平均滤波。我一般用滑动窗口,窗口大小8,每次新值进来就替换最旧的值,然后取平均。这样曲线很平滑,响应也够快。
如果温度值明显错误,比如室温显示100℃,先检查分压电路有没有焊反。NTC和固定电阻的位置搞反了,温度就会反着来。再检查B值和R0有没有写错,10kΩ的NTC写成100kΩ,温度会差很多。还有,logf的参数不能为负或零,如果R_ntc算出来是负数,说明ADC值有问题,检查adc_val是否超范围。
5.3 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| ADC读数始终为0 | NTC短路或ADC引脚接地 | 万用表测ADC引脚电压 | 检查NTC和焊接 |
| ADC读数始终为4095 | NTC断路或ADC引脚接VCC | 万用表测ADC引脚电压 | 检查NTC和焊接 |
| 读数跳动±10以上 | 电源噪声或滤波不足 | 示波器看VDDA纹波 | 加RC滤波和去耦电容 |
| 温度偏高几度 | 自热效应或VDDA偏高 | 测NTC电流和VDDA | 增大阻值或间歇供电 |
| 温度偏低几度 | 引线电阻或VDDA偏低 | 测引线电阻和VDDA | 用三线制或校准 |
| 低温段精度差 | 固定电阻选大了 | 查R-T表 | 换小阻值固定电阻 |
| 高温段精度差 | 固定电阻选小了 | 查R-T表 | 换大阻值固定电阻 |
5.4 实操心得与避坑技巧
第一个心得:NTC的焊接温度不能太高,烙铁停留时间不能太长。NTC的环氧封装耐温有限,长时间高温焊接会导致阻值漂移。我一般用300℃烙铁,每个焊点不超过3秒,焊完等冷却再测阻值。
第二个心得:如果测量范围跨零度以下,B值公式的精度会下降,因为NTC在低温段的非线性更强。这时候建议用Steinhart-Hart方程,或者分段校准。我在一个冷链项目里测-20到0℃,用B值公式误差到了1℃,换成Steinhart-Hart后降到0.3℃。
第三个心得:代码里加一个温度合理性检查。如果算出来的温度超出NTC的物理范围,比如-50到150℃之外,直接返回错误值,避免后续逻辑出错。我在一个加热控制项目里,NTC脱落导致ADC读数满量程,温度算出来500℃,加热器一直全功率运行,差点出事。后来加了范围检查,超过150℃就报故障并关闭加热。
第四个心得:如果多个NTC共用一路ADC,用模拟开关切换时,切换后要等一段时间再采样,让RC滤波电容充电稳定。我一般等1ms,采样时间设239.5周期,这样切换通道后的第一组数据才可靠。
6. 从单点测温到多点组网的扩展思路
单点测温跑通之后,很容易扩展到多点。STM32的ADC有多通道,配合DMA可以自动扫描多个通道,把每个NTC的ADC值存到数组里,然后逐个换算温度。我用F103的ADC1扫描4个通道,DMA循环模式,采样率1kHz,CPU几乎不参与,效率很高。
如果NTC数量超过ADC通道数,可以用模拟开关,比如CD4051,8选1。STM32用3个GPIO控制通道选择,ADC只采一路。切换通道后延时1ms再采样,避免通道间串扰。我做过16路NTC采集,用两片CD4051,扫描周期100ms,温度刷新率10Hz,完全够用。
再往上,如果要做分布式测温,可以用RS485总线,每个节点一个STM32加一个NTC,主机轮询。RS485抗干扰能力强,传输距离可达1200米,适合工业现场。每个节点的代码和单点测温一样,只是加了Modbus协议。我做过一个粮仓测温项目,32个节点,每个节点4路NTC,总共128个测温点,用RS485组网,运行两年多没出过问题。
代码层面,多点采集的关键是DMA缓冲区和通道顺序的对应关系。STM32的ADC扫描模式按通道号从小到大依次转换,DMA按顺序搬运。所以你要确保DMA缓冲区的索引和通道号对应。我一般定义一个结构体数组,每个元素包含通道号、NTC参数、温度值,初始化时按通道号排序,这样DMA搬完直接对应。
最后分享一个调试技巧:如果多点采集时某个通道读数异常,先单独测那个通道,排除硬件问题。如果硬件没问题,检查DMA缓冲区有没有越界,或者通道顺序有没有搞错。我遇到过一次,DMA缓冲区大小设成了4,但实际扫描了5个通道,结果最后一个通道的数据覆盖了第一个通道,查了半天才发现是数组大小写错了。这种低级错误在实际项目中很常见,写代码时多留个心眼。