简介:面向嵌入式开发者的STM32 NTC温度测试完整工程,基于STM32F103实现负温度系数热敏电阻的实时温度采集与换算,涵盖ADC采样、分压电路、B值公式计算及USMART调试等关键环节,适合学习传感器采集与单片机外设驱动的开发者参考。资源为zip压缩包,共90个文件,以C源码(.c/.h)为主,同时包含Keil工程配置(.uvprojx/.uvoptx)、编译生成的hex/axf和辅助调试的map/crf等,整体仅1.3MB,便于直接打开、编译和烧录。已有3762人学习使用,除核心温度测量程序外,还整理了LED、OLED、LCD、ADC、EXTI、DMA、定时器等外设驱动模块及readme说明,可帮助读者从电压采集到温度显示、异常判断完成完整温控系统搭建。 做温控相关项目时,很多人第一次接触stm32的ADC都会选NTC热敏电阻。我之前做一个小型的培养箱温度监测模块,原理图非常简单——一个10k NTC加一个10k采样电阻,分压后送给stm32的ADC引脚。结果板子贴好、固件烧进去,室温25℃居然读出来17℃。当时第一反应是代码写错,翻来覆去查了一整晚,最后才发现问题根本不在算法,而是从硬件分压、采样时间到标定方式,整条链路都有坑。这篇就把NTC温度测试的完整链路过一遍,从电路设计、ADC配置,到温度算法选型和实测标定,尽量把我在项目里踩过的坑都说清楚。
这套内容适合刚接触stm32 ADC采集的人,也适合那些用NTC做产品但又觉得“读数总飘、精度上不去”的人。文章偏实战,原理部分我会用比较容易理解的方式讲透,代码和参数都给出可直接参考的配置。
1. 硬件方案先行:NTC分压电路与STM32采样通道设计
1.1 为什么是NTC而不是其他温度传感器
NTC的全称是负温度系数热敏电阻,温度升高时阻值下降。它的优点是便宜、响应快、电路结构简单,常温精度做到±0.5℃左右完全可行,测温范围在-40℃到+150℃之间,覆盖了绝大多数消费级和工业级场景。相比之下,DS18B20虽然接口简单、不用算电阻,但它的封装规格相对固定,测量延迟也更大;热电偶能测很高温度,却需要冷端补偿和放大电路;PT100精度高,但成本高、调理电路复杂,用在几十块钱成本的板子上并不划算。
因此,在大多数中低温、小体积、低成本场景下,NTC是综合性价比最高的选择。STM32做NTC采集也不需要外部专用ADC芯片,芯片自带的12位ADC配合好电路设计,已经能满足0.1℃级别的分辨率需求。这个小项目里选用的参数是:25℃标称阻值10kΩ、B值3950,这是市面上最常见也是资料最全的型号。
1.2 分压电路怎么接、采样电阻选多大
NTC本身是电阻,它不能直接进ADC,必须通过分压电路把电阻变化转换成电压变化。工程上最常用的接法是让NTC放在下臂,采样电阻R0接VCC,中间的分压点接STM32的ADC引脚:
VCC —— R0 —— (ADC采样点) —— NTC —— GND
此时ADC引脚电压:
V_adc = VCC * R_ntc / (R0 + R_ntc)
R0的取值直接影响测量分辨率和量程分布,选型原则是让R0接近测温范围中间点对应的NTC阻值。以10k NTC、测温范围-20℃到80℃为例,25℃时NTC阻值是10k,那么R0取10k,25℃的分压点正好落在VCC的一半,ADC读数在中间,两边都有充足的动态范围。
我按实测参数算一组数据。VCC=3.3V,12位ADC满量程4095,R0=10k,25℃时R_ntc=10k,V_adc=1.65V,ADC读数≈2048。温度到30℃时,按B=3950计算R_ntc≈8.04k,V_adc≈1.47V,ADC读数≈1823。也就是说5℃温差产生225个LSB的变化,换算下来大概0.022℃/LSB,这个分辨率对于大多数应用场景都够用了。
如果只测低温区间,R0可以适当取大一些,让低温段的分压变化更“舒展”;如果侧重高温段,R0取小一些。但不要为了让某个区间的分辨率极端化而牺牲整体量程,R0和NTC标称值同数量级是工程上最通用的做法。
1.3 长引线和滤波电容带来的ADC读数误差
NTC探头如果通过长线连接主板,线缆电阻会叠加在NTC阻值上。比如普通的杜邦线内阻大约0.1Ω左右,影响不大;但如果用几十米的信号线,线缆电阻可能达到几欧姆甚至十几欧姆,这时候测温偏差就不可忽略了。这种情况我建议改用三线制接法,或者干脆把信号调理电路移到探头端再传模拟电压。
很多人在ADC引脚上加一个100nF的滤波电容,这对于抑制高频干扰确实有帮助,但有一个副作用必须清楚:NTC分压点的输出阻抗比较高,常温下大约是R0与R_ntc的并联值,也就是5kΩ左右。100nF电容和5kΩ阻抗组成的时间常数是0.5ms,而STM32的ADC默认采样时间只有1.5-2.4μs左右,采样电容根本来不及充满,ADC读到的电压会明显偏低。初次调试时如果发现ADC读数远低于理论值,并且换采样时间后读数变化很大,多半就是这个原因。基础项目里滤波电容建议控制在10nF以内,同时配合下文说到的长时间采样配置。
2. ADC转换不是想当然:采样时间、参考电压与源阻抗的影响
2.1 真正决定ADC读数稳定性的参数
STM32的12位ADC在配置上有几个容易被忽略却又至关重要的参数:参考电压、采样时间、ADC时钟分频和转换分辨率。参考电压VREF通常是直接接VDDA的,如果VDDA受负载波动影响,那么ADC的满量程也会跟着波动,导致同一个温度读出来的ADC值不一致。对精度要求高的项目,建议给VDDA单独加LDO,或者用高精度基准源,同时把模拟电源和数字电源做好磁珠隔离。
ADC采样时间是另一个关键点。STM32内部的采样开关闭合后,采样电容需要从外部信号源汲取电荷,外部等效阻抗越高,需要的充电时间就越长。这个参数可以在CubeMX里直接配置,HAL库对应的代码片段是:
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.SamplingTime = ADC_SAMPLETIME_480CYCLES;480个ADC时钟周期的采样时间,在APB2时钟72MHz、分频4即18MHz ADC时钟下,大约26.7μs。实测下来,这个配置配合5kΩ级别的NTC分压源阻抗,读数非常稳定。如果你用的是标准外设库,对应修改ADC_SMPR2寄存器里的SMP位为0b111即可,效果一样。
2.2 源阻抗陷阱:一个很容易忽略的坑
很多人在做NTC采集时,为了让信号“更干净”,在ADC引脚前串联一个几kΩ的电阻,再加一个大电容滤波。这个做法在低频慢变信号里听起来合理,但对于ADC采样实际上是灾难。我调试时就遇到过:ADC读数总是比万用表实测电压低几十个mV,后来我用示波器量了采样瞬间的引脚波形,发现每次采样时引脚电压都会被拉低一块,采样结束后再慢慢回升,典型的采样电容充电不足现象。
判断源阻抗是否过高的办法很简单:把采样时间从默认值逐步调大,比如从14周期调到480周期,如果ADC读数有明显上升且最终趋于稳定,那就是源阻抗导致充电不充分。解决办法有三种:第一,降低串联电阻、减小滤波电容;第二,增加采样时间;第三,在ADC前端加一个低输出阻抗的运放缓冲器,让运放去驱动ADC采样电容。前两种适合大多数简单项目,第三种适合信号源阻抗实在降不下来的场景。
2.3 多次采样取平均依然是小成本大收益
NTC本身是慢变信号,温度变化的时间常数通常以秒计,所以ADC采回来的数据做软件滤波几乎不需要担心滞后问题。最基础的做法是连续采样多次取平均,比如每次采集64次,去掉最大值和最小值,然后对剩余值平均。这样做的意义在于:单次ADC采样存在量化噪声和系统噪声,多次平均可以显著降低随机噪声的影响。
我实际统计过,单次采集的ADC读数波动幅度大概在±3个LSB,64次平均后波动降到±1个LSB以内,换算成温度大约是0.02℃,完全够用。滤波代码也很简单,用一个32位的累加器,采样完除以次数即可。注意不要用uint16_t累加,64次12位ADC值累加会超过65535,直接溢出导致错误结果,这个细节我见过不少人踩过。
3. 从ADC原始值到温度:查表、公式、拟合三种路线的选型与实现
3.1 第一步:把ADC原始值换算成NTC电阻值
ADC原始值只是电压的数字量,要换算成温度,中间必须先得到NTC当前的阻值。用上文的接法,NTC在下臂、采样电阻接VCC,且VREF≈VCC时,存在一个非常方便的换算关系:
R_ntc = R0 * adc / (4095 - adc)
注意这里VCC和VREF被约掉了,这意味着只要分压电源和ADC参考电压用同一路供电,参考电压的绝对精度几乎不影响最终电阻换算,ADC天然具备比例测量的优势。这个公式推导起来很简单:V_adc = adc * VREF / 4095,同时V_adc = VCC * R_ntc / (R0 + R_ntc),令VREF≈VCC,两边约分就得到上面的式子。
实际编码时有一个常见错误:先用整数运算算adc / (4095 - adc),得到0就直接丢精度了。先转成float再计算,或者直接用浮点表达式,否则高温区数据会严重失真。
3.2 查表法:工程上最稳的土办法
查表法是最直观也最容易控制的方案。先从NTC手册里拿到R-T对照表,或者用仿真工具生成一张-20℃到100℃、每1℃一个阻值的表,存成const数组。每次采样后先算R_ntc,然后在表中二分查找相邻两个温度点,用线性插值得到小数部分温度。
查表法最大的优点是把NTC的非线性完全“吃”进了表格里,只要表数据可靠,精度就能保证。缺点是Flash占用会多一些,比如-20℃到100℃每1℃一个点,一共121个float值,约500字节,对STM32来说完全不值一提。如果你的NTC手册给的表精度不够,还可以用自己的标定数据填充表格,后面第4章会讲。
实现上,推荐用R值建表而不是ADC值建表。用R值建表的好处是电路参数变化时表不用改,换R0或者换供电电压都不影响算法的正确性。
const float temp_table[] = { -20.0f, -19.0f, -18.0f, ... }; const float r_table[] = { 97000.0f, 92000.0f, ... }; float R_to_temp(float R) { // 二分查找R_table中的区间 // 对temp_table做线性插值 }二分查找加插值在STM32上执行只需要几微秒,性能完全不是问题。
3.3 Steinhart-Hart公式:精度上限更高,但别硬套默认系数
Steinhart-Hart方程是目前精度最高的NTC温度换算数学表达式:
1/T = A + B * ln(R) + C * (ln(R))^3
其中T是开尔文温度,R是当前NTC阻值,A、B、C是三个待定系数。三个系数最好用三个已知的温度-电阻标定点来求解。比如用0℃、25℃、70℃三组实测数据,通过解三元一次方程组就能得到。用Python求解非常方便:
import numpy as np R1, T1 = 32650.0, 273.15 + 0.0 R2, T2 = 10000.0, 273.15 + 25.0 R3, T3 = 1751.0, 273.15 + 70.0 M = np.array([ [1, np.log(R1), np.log(R1)**3], [1, np.log(R2), np.log(R2)**3], [1, np.log(R3), np.log(R3)**3] ]) b = np.array([1/T1, 1/T2, 1/T3]) A, B, C = np.linalg.solve(M, b) print(A, B, C)然后把求出来的A、B、C写进C代码:
float calc_temp_sh(float r_ntc) { float lnR = logf(r_ntc); float t_k_inv = A + B * lnR + C * lnR * lnR * lnR; return 1.0f / t_k_inv - 273.15f; }需要注意的是,网上很多文章直接给一组所谓“通用”的A、B、C系数,但NTC的阻值和B值离散度通常有±1%到±3%,套用通用系数在不同批次的NTC上精度可能差得很远。最稳妥的做法是拿自己实际使用的NTC型号做标定,求系数,不要偷懒。
如果只做粗略测量,可以退一步用简化β公式:
T = 1 / (1/T0 + (1/B) * ln(R/R0)) - 273.15
β公式只需要三个参数:T0=298.15K,R0=25℃标称阻值,B值。优点是计算量小,但在远离25℃的温度区间误差会变大,因为B值本身随温度是缓慢变化的。实际项目中,如果测温范围窄(比如20℃到40℃),β公式够了;如果范围宽,S-H公式或查表更可靠。
3.4 最小二乘拟合和查表到底怎么选
搜相关资料时,“NTC曲线拟合精度高”这个说法很常见。所谓拟合,通常是用实测标定数据构造一条温度-电阻曲线,比如四阶多项式或者分段插值,然后用这条曲线去换算温度。
最小二乘多项式拟合的思路是:测量8到10个标定点的温度与阻值,用最小二乘拟合出一个三阶或四阶多项式,之后用多项式直接计算。优点是计算速度极快,几乎不消耗Flash,缺点是外推能力弱,超出拟合范围后误差剧增。我自己的使用建议是:
- 测温范围窄且固定:用三阶多项式拟合,省Flash且精度高。
- 测温范围宽或希望通用性更好:用S-H公式加标定,或者直接查表加线性插值。
- 对精度要求不高、想最快跑通功能:β公式就够。
这三条路线我都在实际项目里用过。印象最深的是有个项目为了省Flash,用四阶多项式拟合,拟合范围内精度做到了±0.3℃,但换了个探头批次后误差立刻扩大到±1.5℃,说明拟合参数对器件个体很敏感,最终我还是改回了标定+查表方案。
4. 实测标定与误差修正:把采集精度从“能用”拉到“可信”
4.1 标定的具体操作流程
不管选哪种算法,只要NTC来自常规生产批次,不做标定就声称“精度±0.1℃”基本不现实。标定是提高精度的关键一环。
我的做法是准备一个保温容器装水,配合高精度的参考温度计和水银温度计或已校准的数字温度计,将NTC探头和参考探头绑在一起放入水中。每取一个温度点,等水温稳定后用万用表记录NTC阻值,同时记录参考温度计读数。稳定判据是:1分钟内NTC阻值变化不超过1Ω。
标定点建议从低温到高温至少取5个点,例如0℃、15℃、25℃、40℃、60℃。0℃可以用冰水混合物,其它温度通过掺热水或冷水调节。每个点都等稳定后记录,然后整理成一张表:
| 标定点 | 参考温度℃ | NTC阻值kΩ |
|---|---|---|
| 1 | 0.2 | 32.6 |
| 2 | 15.1 | 18.9 |
| 3 | 25.0 | 10.0 |
| 4 | 40.3 | 5.8 |
| 5 | 60.1 | 2.9 |
有了这组数据,你可以做S-H三系数标定,也可以直接填充查表法所需的R-T表,或者用最小二乘拟合多项式。
4.2 误差来源分析:为什么算出来总差一点
标定前,先把可能导致误差的因素排查一遍。这张表是我反复整理过的:
| 误差源 | 典型量级 | 对策 |
|---|---|---|
| NTC个体离散(25℃阻值和B值偏差) | ±1%~±3%阻值,约±0.5℃~±2℃ | 实测标定 |
| 自热效应 | 0.1℃~1℃甚至更高 | 降低工作电流、间歇采集 |
| ADC参考电压漂移 | 按比例折算为温度偏差 | 外部基准或使用比例测量 |
| 采样时间不足 | 读数偏低,温度偏高 | 增大采样时间到480周期 |
| 长线引线电阻 | 每欧姆约0.03℃~0.1℃ | 短线、三线制 |
| 滤波电容导致的建立时间 | 同上 | 缩小电容或加运放 |
其中自热效应非常多见但经常被忽略。NTC在工作时自身有电流通过,发热会导致阻值偏低,测出的温度比实际高。以3.3V供电、R0=10k、NTC=10k为例,NTC上的功率约为0.27mW,常见贴片NTC的耗散系数约2mW/℃,自热温升约0.1℃,基本可以忽略。但如果把R0换成了1k,电流增大后NTC功耗约2.7mW,温升可能到1℃以上,这个误差在恒温箱验证时会非常刺眼。解决办法是尽量选阻值较大的NTC如100k,或者采用间歇供电方式:只在采集前的几十毫秒打开分压电源,采集完立刻关闭,把自热影响降到最低。
4.3 标定后的静态误差修正
即使做了标定,计算出的温度与参考温度之间仍可能存在系统偏差。如果偏差呈近似线性趋势,可以用一阶修正:
T_corrected = k * T_measured + b
用两个标定点的实测温度和参考温度解出k和b即可。比如0℃时实测为-0.5℃,60℃时实测为61.2℃,那么k = (60 - 0) / (61.2 - (-0.5)) ≈ 0.973,b = 0 - 0.973 * (-0.5) ≈ 0.486。实际使用效果是这条直线把整体偏移拉回来了。
如果误差随温度呈弯曲趋势,用一阶修正不够,建议做分段线性修正:把标定点之间的误差做成一个与温度相关的修正表,按当前计算温度查表得到修正量。这个方法实现简单,也是我在产品里用得最多的方法。
修正完成后,一定要做一次独立验证:重新配置水浴温度,在标定点之间选几个温度进行复测,统计最大误差和平均误差。只有复测数据能稳定在目标精度范围内,才能说这块板子达到了设计指标。
4.4 一个快速排查问题的亲测小技巧
最后分享一个我在调试多路NTC采集时常用的检验方法:取两个同型号NTC,插到同一个板子上的两个ADC通道,让它们悬空处于同一环境温度,看两个通道读数是否一致。如果两只探头读数相差超过0.3℃,那问题多半不是NTC本身,而是两个ADC通道之间或两路分压电路之间存在差异,比如某一路的采样时间没配够、某一颗采样电阻精度不够。这个方法不需要恒温设备,十几分钟就能把硬件问题基本定位清楚。
NTC温度测试看起来是一件“搭个分压电路、读个ADC值”的小事,实际上从电路设计到算法选型再到标定修正,每个环节都会直接影响最终精度。很多时候精度上不去,问题并不在算法,而在更早的硬件和配置环节。先把硬件基础打好、把采样时间配好,再谈软件算法,你会发现自己项目的温控精度比预期高不少。
本文还有配套的精品资源,点击获取