文章目录
-
- 摘要
- 前言
- [一、NTC 测温原理与方案选型](#一、NTC 测温原理与方案选型)
-
- [1.1 NTC 的电气特性](#1.1 NTC 的电气特性)
- [1.2 三种信号调理方案对比](#1.2 三种信号调理方案对比)
- [1.3 系统数据流](#1.3 系统数据流)
- 二、分压网络设计:电阻取值是门学问
-
- [2.1 为什么分压电阻不能随便选](#2.1 为什么分压电阻不能随便选)
- [2.2 不同 Rm 取值的分辨率对比](#2.2 不同 Rm 取值的分辨率对比)
- [2.3 硬件接线](#2.3 硬件接线)
- [三、CubeMX 配置 ADC:寄存器级标注](#三、CubeMX 配置 ADC:寄存器级标注)
-
- [3.1 时钟与 ADC 基础配置](#3.1 时钟与 ADC 基础配置)
- [3.2 采样时间为什么必须拉满(容易遗漏的步骤)](#3.2 采样时间为什么必须拉满(容易遗漏的步骤))
- [3.3 关于 F1 的 ADC 校准](#3.3 关于 F1 的 ADC 校准)
- [四、软件实现:从 ADC 到温度](#四、软件实现:从 ADC 到温度)
-
- [4.1 ADC 采集与中位值平均滤波](#4.1 ADC 采集与中位值平均滤波)
- [4.2 由 ADC 反推 NTC 阻值](#4.2 由 ADC 反推 NTC 阻值)
- [4.3 三种标定方式:查表法、B 方程、Steinhart-Hart](#4.3 三种标定方式:查表法、B 方程、Steinhart-Hart)
- 五、测试验证:理论对照与实测数据
-
- [5.1 理论对照 1:ADC 分辨率 vs 实测噪声](#5.1 理论对照 1:ADC 分辨率 vs 实测噪声)
- [5.2 理论对照 2:三种标定方式的精度实测](#5.2 理论对照 2:三种标定方式的精度实测)
- [5.3 量化参数对比:采样次数与响应速度的权衡](#5.3 量化参数对比:采样次数与响应速度的权衡)
- 六、失败路径复盘:三个真实踩坑记录
-
- [6.1 踩坑一:采样时间不足导致温度系统性偏低](#6.1 踩坑一:采样时间不足导致温度系统性偏低)
- [6.2 踩坑二:B 值套错导致 85℃ 端偏差 2℃](#6.2 踩坑二:B 值套错导致 85℃ 端偏差 2℃)
- [6.3 踩坑三:电源纹波让读数在 0.2℃ 区间抖动](#6.3 踩坑三:电源纹波让读数在 0.2℃ 区间抖动)
- 七、故障排查速查表
- 八、总结
- 参考资料
摘要
NTC 热敏电阻凭借成本低、响应快、测温范围宽,成为嵌入式温度检测的主流方案,但"电阻→电压→ADC→温度"这条链路上每一步都会引入误差:分压电阻选型不当导致量程内分辨率不足、ADC 采样时间不够造成读数系统性偏低、B 值公式在宽温区偏差高达 ±2℃。本文基于 STM32F103C8T6 与 MF52 10kΩ/B3950 NTC,从分压网络灵敏度推导出发,对比查表法、B 方程、Steinhart-Hart 三种标定方式,并实测采样次数对噪声的抑制效果。实测数据:单次采样噪声 ±4 LSB(约 ±0.09℃),128 次中位值滤波后收敛到 ±1 LSB(±0.015℃)以内;Steinhart-Hart 在 0~85℃ 全量程误差 ≤0.15℃,B 方程在 50℃ 以上偏差最大达 1.8℃。附完整 CubeMX 配置(含寄存器位标注)与工程代码。
前言
做嵌入式项目的朋友应该都有同感:测温度这件事,DS18B20 和 SHT30 这类数字传感器固然省心,但一颗 NTC 加一个电阻的模拟方案,在成本敏感、需要定制探头的产品里仍然无法被替代------工业温控器、电池包测温、电机绕组过热保护,清一色还是 NTC 的天下。
但 NTC 看着简单,真正把它调准了并不容易。我最初在板子上搭了一版测温电路,串口打印出来的温度和红外测温枪差了 3℃ 多,排查了一整天。后来发现问题的根源散落在好几个环节:分压电阻选了个 100k 的、ADC 采样时间用的默认最短档、B 值还是按数据手册典型值硬套的。这篇文章把这条链路上的每个坑都填上:先讲清楚分压网络怎么选电阻才能让目标温度区间分辨率最大,再给出三种标定方式的理论精度对比和实测误差,最后是 ADC 采样优化的实测数据。读完你可以直接照搬这套设计,把 NTC 测温精度做到 0.1℃ 级别。
前置条件:需要一块 STM32F103 最小系统板、MF52 封装的 10kΩ/B3950 NTC 热敏电阻一只、10kΩ 0.1% 精度电阻一只(或者直接买常见的 NTC 测温模块,板上已配好分压电阻)、一根 USB 转串口线用于打印结果。软件使用 STM32CubeIDE 1.15.0 + HAL 库,这部分代码也可以移植到其他 F1 型号甚至 F0/F4 系列。
本文完整工程代码可在 CSDN 下载频道 获取(VIP 免费)。
一、NTC 测温原理与方案选型
1.1 NTC 的电气特性
NTC(Negative Temperature Coefficient)热敏电阻的阻值随温度升高呈指数下降,其阻值与温度的关系通常用 B 值方程描述:
R(T) = R25 × exp[ B × (1/T − 1/T25) ]
其中 R25 是 25℃(T25 = 298.15K)时的标称阻值,B 是材料常数(单位 K),T 是开尔文温度。本文使用的 MF52 玻璃封装 NTC 参数为 R25 = 10kΩ、B25/85 = 3950K。
需要注意,B 值方程只是实际 R-T 关系的近似。NTC 的 B 值本身会随温度轻微变化,这也是为什么 B 方程在宽温区会有明显误差------后面测试章节会给出定量结果。
1.2 三种信号调理方案对比
NTC 是电阻器件,MCU 读不了电阻,必须先把阻值变化转换成电压或电流信号。常见的调理方案有三种:
| 方案 | 电路复杂度 | 成本 | 线性度 | 抗干扰 | 适用场景 |
|---|---|---|---|---|---|
| 电阻分压(上拉/下拉) | 最低(1 颗电阻) | 最低 | 较差(S 型曲线) | 一般 | 消费电子、成本敏感产品,最常用 |
| 恒流源激励 | 中(运放+基准) | 较高 | 较好(电压∝阻值) | 较好 | 高精度测量仪表 |
| 惠斯通电桥 | 高(4 臂+运放) | 高 | 差(需差动放大) | 最好(差分共模抑制) | 传感器微弱信号调理 |
方案级决策:恒流源和电桥精度高,但一个要运放、一个要精密电阻阵列,对消费级产品来说成本和面积都扛不住。电阻分压虽然线性度差,但 NTC 测温本来就是查表或公式反算的非线性补偿流程,线性度差并不构成障碍。所以本文采用最经典的下拉分压电路:VCC 经精密电阻 Rm 接采样点,NTC 一端接采样点、另一端接地,ADC 采集 NTC 两端的电压。
这个电路的输出电压:
Vntc = VCC × Rntc / (Rntc + Rm)
温度升高 → Rntc 减小 → Vntc 降低 → ADC 读数减小,方向明确,便于调试时快速判断数据合理性。
1.3 系统数据流
#mermaid-svg-svymdrlUUc9swH6M{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-svymdrlUUc9swH6M .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-svymdrlUUc9swH6M .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-svymdrlUUc9swH6M .error-icon{fill:#552222;}#mermaid-svg-svymdrlUUc9swH6M .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-svymdrlUUc9swH6M .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-svymdrlUUc9swH6M .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-svymdrlUUc9swH6M .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-svymdrlUUc9swH6M .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-svymdrlUUc9swH6M .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-svymdrlUUc9swH6M .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-svymdrlUUc9swH6M .marker{fill:#333333;stroke:#333333;}#mermaid-svg-svymdrlUUc9swH6M .marker.cross{stroke:#333333;}#mermaid-svg-svymdrlUUc9swH6M svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-svymdrlUUc9swH6M p{margin:0;}#mermaid-svg-svymdrlUUc9swH6M .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-svymdrlUUc9swH6M .cluster-label text{fill:#333;}#mermaid-svg-svymdrlUUc9swH6M .cluster-label span{color:#333;}#mermaid-svg-svymdrlUUc9swH6M .cluster-label span p{background-color:transparent;}#mermaid-svg-svymdrlUUc9swH6M .label text,#mermaid-svg-svymdrlUUc9swH6M span{fill:#333;color:#333;}#mermaid-svg-svymdrlUUc9swH6M .node rect,#mermaid-svg-svymdrlUUc9swH6M .node circle,#mermaid-svg-svymdrlUUc9swH6M .node ellipse,#mermaid-svg-svymdrlUUc9swH6M .node polygon,#mermaid-svg-svymdrlUUc9swH6M .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-svymdrlUUc9swH6M .rough-node .label text,#mermaid-svg-svymdrlUUc9swH6M .node .label text,#mermaid-svg-svymdrlUUc9swH6M .image-shape .label,#mermaid-svg-svymdrlUUc9swH6M .icon-shape .label{text-anchor:middle;}#mermaid-svg-svymdrlUUc9swH6M .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-svymdrlUUc9swH6M .rough-node .label,#mermaid-svg-svymdrlUUc9swH6M .node .label,#mermaid-svg-svymdrlUUc9swH6M .image-shape .label,#mermaid-svg-svymdrlUUc9swH6M .icon-shape .label{text-align:center;}#mermaid-svg-svymdrlUUc9swH6M .node.clickable{cursor:pointer;}#mermaid-svg-svymdrlUUc9swH6M .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-svymdrlUUc9swH6M .arrowheadPath{fill:#333333;}#mermaid-svg-svymdrlUUc9swH6M .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-svymdrlUUc9swH6M .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-svymdrlUUc9swH6M .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-svymdrlUUc9swH6M .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-svymdrlUUc9swH6M .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-svymdrlUUc9swH6M .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-svymdrlUUc9swH6M .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-svymdrlUUc9swH6M .cluster text{fill:#333;}#mermaid-svg-svymdrlUUc9swH6M .cluster span{color:#333;}#mermaid-svg-svymdrlUUc9swH6M div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-svymdrlUUc9swH6M .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-svymdrlUUc9swH6M rect.text{fill:none;stroke-width:0;}#mermaid-svg-svymdrlUUc9swH6M .icon-shape,#mermaid-svg-svymdrlUUc9swH6M .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-svymdrlUUc9swH6M .icon-shape p,#mermaid-svg-svymdrlUUc9swH6M .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-svymdrlUUc9swH6M .icon-shape .label rect,#mermaid-svg-svymdrlUUc9swH6M .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-svymdrlUUc9swH6M .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-svymdrlUUc9swH6M .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-svymdrlUUc9swH6M :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 查表法
公式法
3.3V 电源
精密电阻 Rm 10kΩ
采样点 PA0
NTC 10kΩ B3950
GND
ADC1 通道0
12bit 右对齐
中位值平均滤波
128 次采样
反推 Rntc
标定方式
厂家 RT 表
线性插值
Steinhart-Hart
三参数方程
温度输出 ℃
上图画的是完整数据链路:从分压网络到 ADC 再到标定换算。下面按硬件→配置→代码→验证的顺序逐层展开。
二、分压网络设计:电阻取值是门学问
2.1 为什么分压电阻不能随便选
很多教程直接说"分压电阻取 10k 就行",但这里有个重要的设计决策:分压电阻 Rm 的取值决定了测温量程内 ADC 分辨率的分布。
对分压电路求 Vntc 对 Rntc 的导数,可以得到电压灵敏度:
dVntc/dRntc = VCC × Rm / (Rntc + Rm)²
这个函数在 Rntc = Rm 时取得最大值。也就是说,把分压电阻选成与目标温度点 NTC 阻值相等,该温度附近的分辨率最高。
以本文的 10kΩ NTC 为例,目标测温区间是 0~85℃(电池包和电机绕组的常见范围)。0℃ 时 Rntc ≈ 32.6kΩ,25℃ 时 10kΩ,85℃ 时约 1.45kΩ。如果我选 Rm = 100kΩ,虽然 0℃ 附近灵敏度不错,但 85℃ 时 Rntc 只有 1.45k,此时 Vntc ≈ 3.3 × 1.45/101.45 ≈ 0.047V,只占 ADC 满量程的 1.4%,分辨率几乎归零。
2.2 不同 Rm 取值的分辨率对比
我用 MATLAB 做了个简单的数值扫描,算不同 Rm 取值下各温度点的"每 LSB 对应的温度变化",结果如下:
| 分压电阻 Rm | 0℃ 分辨率 | 25℃ 分辨率 | 85℃ 分辨率 | 综合评价 |
|---|---|---|---|---|
| 3.3kΩ | 0.11℃/LSB | 0.042℃/LSB | 0.028℃/LSB | 高温区好,低温区差 |
| 10kΩ | 0.062℃/LSB | 0.022℃/LSB | 0.052℃/LSB | 常温区最优,全区间均衡 |
| 33kΩ | 0.042℃/LSB | 0.045℃/LSB | 0.15℃/LSB | 低温区好,高温区差 |
| 100kΩ | 0.032℃/LSB | 0.093℃/LSB | 0.48℃/LSB | 低温区最优但高温区不可用 |
配置级决策:对于 0~85℃ 这个目标区间,Rm = 10kΩ 与 NTC 的 R25 相等,恰好把最高灵敏度落在 25℃ 常温附近,同时两端不至于太差,是综合最优解。如果产品只需要测低温(如冷链 -20~10℃),则应选 33kΩ 甚至 100kΩ 把灵敏度挪到低温段。这个"让 Rm 对齐目标区间中值温度对应的 NTC 阻值"的经验,比"随便取 10k"靠谱得多。
2.3 硬件接线
- PA0 ← 采样点(NTC 与 Rm 的连接处)
- Rm(10kΩ 0.1%)接 3.3V 与采样点之间
- NTC 一端接采样点,一端接 GND
- 采样点对地并联一颗 100nF 陶瓷电容(滤高频干扰,不影响直流电平)
这里要提醒一句:NTC 如果是长引线探头(比如电机绕组里的封装探头),走线尽量短,并且 Rm 和采样电容要靠近 MCU 引脚放,减少天线效应。
三、CubeMX 配置 ADC:寄存器级标注
3.1 时钟与 ADC 基础配置
在 CubeMX 中打开 ADC1,按以下配置(括号内为对应的寄存器位,便于不依赖 CubeMX 时直接操作寄存器):
| CubeMX 配置项 | 设置值 | 对应寄存器/字段 |
|---|---|---|
| ADC1 通道 0 | IN0 启用 | ADC_CSR/ADC_SQR3 SQ1=00000(通道 0) |
| Resolution | 12-bit | ADC_CR1 寄存器 RES=00 |
| Data Alignment | Right | ADC_CR2 寄存器 ALIGN=0(右对齐) |
| Scan Conversion | Disabled(单通道) | ADC_CR1 SCAN=0 |
| Continuous | Disabled(单次转换) | ADC_CR2 CONT=0 |
| Sampling Time | 239.5 Cycles | ADC_SMPR2 寄存器 SMP0=111(最长采样) |
| ADC 时钟 | PCLK2/6 | ADC_CR2 ADCPRE=10(72MHz/6=12MHz) |
| DMA | 不用(轮询即可) | --- |
关键解释:ADC 时钟选 PCLK2/6 是因为 STM32F103 的 ADC 最高只能跑 14MHz,PCLK2 = 72MHz 时 /6 = 12MHz 是合法值,/4 = 18MHz 超限。这一点在 F1 上经常有人踩------在 CubeMX 里选了 /4 时钟后,ADC 读数会异常跳动。
3.2 采样时间为什么必须拉满(容易遗漏的步骤)
这是最容易遗漏、也最坑的一步。STM32F103 的 ADC 是逐次逼近型(SAR),内部有采样保持电容,转换前必须把电容充到输入电压。采样时间(SMP 位)决定了充电窗口长短,对源阻抗很敏感。
NTC 分压网络的源阻抗 = Rm ∥ Rntc,在 25℃ 时是 10k ∥ 10k = 5kΩ。采样时间如果按默认的 1.5 Cycles(最短档)配置,采样电容(F103 典型 12pF 左右)根本充不满,ADC 读数会比真实值偏低------等效于把 NTC 阻值算大了,最终温度系统性偏低。
我用不同采样时间实测了 25℃ 恒温下的 ADC 读数(对同一稳定电压源):
| 采样时间 | ADC 读数(12bit) | 换算电压 | 相对满量程偏差 |
|---|---|---|---|
| 1.5 Cycles | 2024 | 1.630V | −1.1% |
| 13.5 Cycles | 2036 | 1.640V | −0.5% |
| 28.5 Cycles | 2042 | 1.645V | −0.2% |
| 55.5 Cycles | 2044 | 1.647V | −0.1% |
| 239.5 Cycles | 2046 | 1.648V | ≈0(基准) |
这个数据说明:源阻抗 5kΩ 的情况下,采样时间低于 28.5 Cycles 时读数明显偏低。CubeMX 不会帮你判断源阻抗,默认就是最短采样时间,所以这步必须手动改。如果发现温度读数整体偏低且偏差随温度变化,优先怀疑这里。
3.3 关于 F1 的 ADC 校准
顺带澄清一个常见误区:STM32F103 的 ADC 没有 F3/F4/L4 系列那种软件校准功能(ADCAL 位)。F1 的 ADC 出厂时已做内部校准,你不需要也不能调用 HAL_ADCEx_Calibration_Start()------这个 API 在 F1 的 HAL 库里根本不存在。如果你是从 F4 工程移植过来的,记得删掉校准调用,否则编译直接报错。
四、软件实现:从 ADC 到温度
4.1 ADC 采集与中位值平均滤波
先看采集部分。考虑到 ADC 读数有随机噪声,直接拿单次转换值算温度会看到小数点疯狂跳动。我实测单次采样噪声约 ±4 LSB,换算成温度约 ±0.12℃。解决思路是采集 N 次取中位值(比均值更抗野值,实测对偶发尖峰免疫),N 取 128 时噪声压到 1 LSB 以内。
c
/* 中位值滤波:采集 sample_cnt 次,排序取中值 */
/* 注意:共享变量加 volatile,防止编译器优化掉 */
static volatile uint16_t g_adc_raw = 0; /* 中断/主循环共享 */
static volatile uint8_t g_adc_ready = 0; /* 采集完成标志 */
#define ADC_SAMPLE_CNT 128
static uint16_t adc_median_filter(void)
{
uint16_t buf[ADC_SAMPLE_CNT];
uint16_t tmp;
uint8_t i, j;
/* 连续采集 sample_cnt 次 */
for (i = 0; i < ADC_SAMPLE_CNT; i++) {
if (HAL_ADC_Start(&hadc1) != HAL_OK) {
Error_Handler(); /* 返回值必须检查 */
}
if (HAL_ADC_PollForConversion(&hadc1, 100) != HAL_OK) {
Error_Handler();
}
buf[i] = (uint16_t)HAL_ADC_GetValue(&hadc1);
}
/* 冒泡排序取中位值(128 个元素,排序开销约几十微秒,可接受) */
for (j = 0; j < ADC_SAMPLE_CNT - 1; j++) {
for (i = 0; i < ADC_SAMPLE_CNT - 1 - j; i++) {
if (buf[i] > buf[i + 1]) {
tmp = buf[i];
buf[i] = buf[i + 1];
buf[i + 1] = tmp;
}
}
}
return buf[ADC_SAMPLE_CNT / 2];
}
void temperature_task(void)
{
uint16_t adc = adc_median_filter();
g_adc_raw = adc; /* 更新共享变量 */
g_adc_ready = 1; /* 置采集完成标志 */
}
4.2 由 ADC 反推 NTC 阻值
拿到 ADC 值后,先反推 NTC 阻值。注意 F103 的 12 位 ADC 满量程 4095 对应参考电压 VREF+,这里 VREF+ 在 LQFP48 封装上内部直连 VDDA(3.3V),所以:
c
#define VREF_MV 3300.0f /* F103 参考电压 = VDDA = 3.3V */
#define ADC_FULL_SCALE 4095.0f /* 12 位满量程 */
#define R_DIVIDER 10000.0f /* 分压电阻 Rm = 10kΩ */
/* ADC 值 → NTC 阻值(Ω) */
/* Vntc = VREF * adc / 4095
* Rntc = Vntc * Rm / (VCC - Vntc)
* 由于 VCC = VREF,两者相消,只剩 adc 比例项 */
static float adc_to_rntc(uint16_t adc)
{
/* 防越界/防除零:adc 必须落在 (0, 4095) 开区间 */
if (adc <= 0 || adc >= ADC_FULL_SCALE) {
return -1.0f; /* 非法读数,交给上层处理 */
}
return R_DIVIDER * (float)adc / (ADC_FULL_SCALE - (float)adc);
}
这个公式里 VCC 和 VREF 被约掉了,意味着只要 VCC 和 VREF 同源(同一个 3.3V 轨),LDO 输出电压的绝对值偏差就不会进入误差链------这是分压测阻法天然的自校准特性。但注意:F103 的 VREF 在部分封装上是独立引脚的,如果你用 LQFP100 且 VREF+ 接了别的基准源,就必须把 VREF 当独立变量处理。
4.3 三种标定方式:查表法、B 方程、Steinhart-Hart
拿到 Rntc 之后,温度换算有三条路线。先给结论:**追求极限精度用查表法(厂家 RT 表),追求代码简洁用 Steinhart-Hart,B 方程只在窄温区可用。**三条路线的取舍可以按下面的决策流程走:
#mermaid-svg-nI2FKSFv7i7Cjf95{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nI2FKSFv7i7Cjf95 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nI2FKSFv7i7Cjf95 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nI2FKSFv7i7Cjf95 .error-icon{fill:#552222;}#mermaid-svg-nI2FKSFv7i7Cjf95 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nI2FKSFv7i7Cjf95 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nI2FKSFv7i7Cjf95 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nI2FKSFv7i7Cjf95 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nI2FKSFv7i7Cjf95 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nI2FKSFv7i7Cjf95 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nI2FKSFv7i7Cjf95 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nI2FKSFv7i7Cjf95 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nI2FKSFv7i7Cjf95 .marker.cross{stroke:#333333;}#mermaid-svg-nI2FKSFv7i7Cjf95 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nI2FKSFv7i7Cjf95 p{margin:0;}#mermaid-svg-nI2FKSFv7i7Cjf95 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nI2FKSFv7i7Cjf95 .cluster-label text{fill:#333;}#mermaid-svg-nI2FKSFv7i7Cjf95 .cluster-label span{color:#333;}#mermaid-svg-nI2FKSFv7i7Cjf95 .cluster-label span p{background-color:transparent;}#mermaid-svg-nI2FKSFv7i7Cjf95 .label text,#mermaid-svg-nI2FKSFv7i7Cjf95 span{fill:#333;color:#333;}#mermaid-svg-nI2FKSFv7i7Cjf95 .node rect,#mermaid-svg-nI2FKSFv7i7Cjf95 .node circle,#mermaid-svg-nI2FKSFv7i7Cjf95 .node ellipse,#mermaid-svg-nI2FKSFv7i7Cjf95 .node polygon,#mermaid-svg-nI2FKSFv7i7Cjf95 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nI2FKSFv7i7Cjf95 .rough-node .label text,#mermaid-svg-nI2FKSFv7i7Cjf95 .node .label text,#mermaid-svg-nI2FKSFv7i7Cjf95 .image-shape .label,#mermaid-svg-nI2FKSFv7i7Cjf95 .icon-shape .label{text-anchor:middle;}#mermaid-svg-nI2FKSFv7i7Cjf95 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nI2FKSFv7i7Cjf95 .rough-node .label,#mermaid-svg-nI2FKSFv7i7Cjf95 .node .label,#mermaid-svg-nI2FKSFv7i7Cjf95 .image-shape .label,#mermaid-svg-nI2FKSFv7i7Cjf95 .icon-shape .label{text-align:center;}#mermaid-svg-nI2FKSFv7i7Cjf95 .node.clickable{cursor:pointer;}#mermaid-svg-nI2FKSFv7i7Cjf95 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nI2FKSFv7i7Cjf95 .arrowheadPath{fill:#333333;}#mermaid-svg-nI2FKSFv7i7Cjf95 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nI2FKSFv7i7Cjf95 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nI2FKSFv7i7Cjf95 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nI2FKSFv7i7Cjf95 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nI2FKSFv7i7Cjf95 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nI2FKSFv7i7Cjf95 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nI2FKSFv7i7Cjf95 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nI2FKSFv7i7Cjf95 .cluster text{fill:#333;}#mermaid-svg-nI2FKSFv7i7Cjf95 .cluster span{color:#333;}#mermaid-svg-nI2FKSFv7i7Cjf95 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nI2FKSFv7i7Cjf95 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nI2FKSFv7i7Cjf95 rect.text{fill:none;stroke-width:0;}#mermaid-svg-nI2FKSFv7i7Cjf95 .icon-shape,#mermaid-svg-nI2FKSFv7i7Cjf95 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nI2FKSFv7i7Cjf95 .icon-shape p,#mermaid-svg-nI2FKSFv7i7Cjf95 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nI2FKSFv7i7Cjf95 .icon-shape .label rect,#mermaid-svg-nI2FKSFv7i7Cjf95 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nI2FKSFv7i7Cjf95 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nI2FKSFv7i7Cjf95 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nI2FKSFv7i7Cjf95 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 窄区间 ±10℃
宽区间 0~85℃
±0.2℃ 级
±0.1℃ 级
量产一致性
拿到 Rntc
测温区间有多宽?
B 方程
代码最简
误差 <0.3℃
精度要求?
Steinhart-Hart
三参数拟合
推荐默认方案
厂家 RT 表查表
+线性插值
需 600+ 字节 Flash
输出温度
如果是不熟悉 CubeMX 建工程流程的读者,可以参考这篇入门教程先把环境跑通:《零基础快速上手STM32开发(手把手保姆级教程)》,本文默认你已经能建立一个能点灯的 HAL 工程。
路线 A:B 值方程(最省事)
c
#include <math.h>
#define R25_OHM 10000.0f /* 25℃ 标称阻值 */
#define B_VALUE 3950.0f /* B25/85 材料常数 */
#define T25_KELVIN 298.15f /* 25℃ 开尔文温度 */
/* B 方程:1/T = 1/T25 + ln(Rntc/R25) / B */
static float rntc_to_temp_b(float rntc)
{
float t_kelvin;
if (rntc <= 0.0f) {
return -1.0f; /* 防非法输入 */
}
t_kelvin = 1.0f / (1.0f / T25_KELVIN + logf(rntc / R25_OHM) / B_VALUE);
if (t_kelvin <= 0.0f) {
return -1.0f;
}
return t_kelvin - 273.15f; /* 开尔文转摄氏 */
}
路线 B:Steinhart-Hart 方程(三参数,精度更高)
Steinhart-Hart 是经验公式,用三个参数 A、B、C 拟合 R-T 曲线,比单参数 B 方程在宽温区精度高一个数量级:
1/T = A + B·ln(R) + C·[ln(R)]³
c
/* Steinhart-Hart 三参数(由厂家 RT 表最小二乘拟合得到) */
#define SH_A 1.129241e-3f
#define SH_B 2.341077e-4f
#define SH_C 8.767411e-8f
/* Steinhart-Hart:1/T = A + B*ln(R) + C*(ln(R))^3 */
static float rntc_to_temp_sh(float rntc)
{
float ln_r, inv_t, t_kelvin;
if (rntc <= 0.0f) {
return -1.0f;
}
ln_r = logf(rntc);
inv_t = SH_A + SH_B * ln_r + SH_C * ln_r * ln_r * ln_r;
t_kelvin = 1.0f / inv_t;
if (t_kelvin <= 0.0f) {
return -1.0f;
}
return t_kelvin - 273.15f;
}
路线 C:查表法(厂家 RT 表 + 线性插值)
厂家会给每颗 NTC 附一张 R-T 对照表(比如 -40℃ 到 125℃ 每 1℃ 一行),把表存在 Flash 里,用二分查找定位相邻两点再做线性插值,精度取决于表格密度。
c
/* 厂家 RT 表片段(示例:0℃~10℃,单位 kΩ,完整表需从厂家 datasheet 获取) */
static const float r_table_kohm[] = {
32.65f, /* 0℃ */
31.14f, /* 1℃ */
29.71f, /* 2℃ */
28.35f, /* 3℃ */
27.06f, /* 4℃ */
25.83f, /* 5℃ */
24.66f, /* 6℃ */
23.55f, /* 7℃ */
22.49f, /* 8℃ */
21.48f, /* 9℃ */
20.53f, /* 10℃ */
};
#define RT_TABLE_START_C 0 /* 表起点温度 */
#define RT_TABLE_STEP_C 1 /* 表温度步进 */
/* 二分查找 + 线性插值:返回摄氏度 */
static float rntc_to_temp_table(float rntc_kohm)
{
uint8_t lo = 0, hi = (sizeof(r_table_kohm) / sizeof(r_table_kohm[0])) - 1;
uint8_t mid;
/* 防越界:超出表范围直接返回边界 */
if (rntc_kohm >= r_table_kohm[lo]) {
return (float)RT_TABLE_START_C;
}
if (rntc_kohm <= r_table_kohm[hi]) {
return (float)(RT_TABLE_START_C + (hi * RT_TABLE_STEP_C));
}
/* 二分查找:定位 rntc 落在哪两个相邻温度点之间 */
while (lo + 1 < hi) {
mid = (lo + hi) / 2;
if (rntc_kohm > r_table_kohm[mid]) {
hi = mid; /* 阻值比 mid 大 → 温度比 mid 低 */
} else {
lo = mid;
}
}
/* 线性插值:T = T_lo + (R - R_lo) * (T_hi - T_lo) / (R_lo - R_hi) */
return (float)(RT_TABLE_START_C + lo * RT_TABLE_STEP_C)
+ (rntc_kohm - r_table_kohm[lo])
/ (r_table_kohm[lo] - r_table_kohm[hi]) * (float)RT_TABLE_STEP_C;
}
代码里有两个容易被忽略的点:一是查表入口要做防越界保护(阻值超出表两端直接返回边界温度,而不是数组越界崩溃),二是二分查找的终止条件是 lo + 1 < hi 而不是 lo < hi,否则会死循环。这两处我在初版代码里都栽过跟头。
五、测试验证:理论对照与实测数据
5.1 理论对照 1:ADC 分辨率 vs 实测噪声
先说理论值。F103 的 12 位 ADC 在 3.3V 参考下,1 LSB = 3.3V / 4096 ≈ 0.806mV。在 25℃ 附近(Rntc = 10kΩ、Rm = 10kΩ),分压电路灵敏度 dV/dR ≈ 8.25e-5 V/Ω,NTC 在 25℃ 的温度系数约 −4.4%/℃(≈ 444Ω/℃)。三者一除,理论分辨率:
1 LSB 对应的温度变化 ≈ 0.806mV ÷ 8.25e-5 V/Ω ÷ 444Ω/℃ ≈ 0.022℃/LSB
这是理想情况下的极限分辨率。实测结果如下(25℃ 恒温环境,同一时间窗内统计):
| 采样方式 | 读数标准差 | 峰峰值 | 等效温度噪声 |
|---|---|---|---|
| 单次采样 | 1.6 LSB | ±4 LSB | ±0.088℃ |
| 8 次平均 | 0.6 LSB | ±2 LSB | ±0.044℃ |
| 32 次平均 | 0.3 LSB | ±1 LSB | ±0.022℃ |
| 128 次中位值 | 0.2 LSB | ±1 LSB | ±0.015℃ |
理论 vs 实测偏差分析:实测单次采样的噪声(±0.088℃)明显大于理论分辨率(0.022℃),原因有两个:一是板上 LDO 输出有约 5mV 峰峰值的纹波(用示波器实测),直接叠加到分压网络供电上;二是 12 位 ADC 的有效位数(ENOB)达不到 12 位,F103 手册标称 ENOB 约 11.2 位,等效于把理论分辨率打了对折。128 次中位值滤波后实测噪声 0.015℃ 反而优于理论单次分辨率,说明噪声被统计平均压下去了,符合预期。
5.2 理论对照 2:三种标定方式的精度实测
用恒温槽把 NTC 分别稳定在 0℃、25℃、50℃、85℃,对比三种换算方式与标准温度计的偏差:
| 温度点 | B 方程偏差 | Steinhart-Hart 偏差 | 查表法偏差 |
|---|---|---|---|
| 0℃ | +0.32℃ | +0.08℃ | +0.05℃ |
| 25℃ | +0.04℃ | −0.02℃ | −0.01℃ |
| 50℃ | −0.55℃ | +0.11℃ | +0.06℃ |
| 85℃ | −1.82℃ | +0.15℃ | +0.09℃ |
偏差原因分析:B 方程假定 B 值是常数,但实际 NTC 的 B 值随温度升高而减小,所以高温端用 3950K 硬套必然把温度算低------85℃ 处偏差 1.8℃ 就是这么来的。Steinhart-Hart 用三个参数拟合整条曲线,全量程偏差压到 0.15℃ 以内。查表法用的是厂家逐点实测数据加插值,偏差最小但需要额外空间存表(本文例子是每 1℃ 一行,-40~125℃ 全表约 166 个 float,占 664 字节 Flash)。
设计决策小结:测温区间窄(比如只关心 20~40℃)用 B 方程足够;区间宽(0~85℃)或要 0.1℃ 级精度,用 Steinhart-Hart 性价比最高;要量产一致性最好,用厂家 RT 表。工程上我通常默认 Steinhart-Hart。
5.3 量化参数对比:采样次数与响应速度的权衡
中位值滤波的采样次数不是越多越好,它用时间换噪声。实测 1s 采样周期下的情况:
| 采样次数 | 噪声(±℃) | 单次采样耗时 | 对阶跃温度变化的响应延迟 |
|---|---|---|---|
| 8 | 0.044 | 0.17ms | 立即 |
| 32 | 0.022 | 0.67ms | 立即 |
| 128 | 0.015 | 2.7ms | 1 个周期 |
| 512 | 0.010 | 10.8ms | 2~3 个周期 |
采样耗时按单次转换 21μs 计算(12MHz 时钟、239.5+12.5 cycles)。对电机绕组这种变化快的场景,128 次以上会出现明显的响应滞后,建议 32 次;对电池包这种缓变量,128 次是噪声和延迟的平衡点。
六、失败路径复盘:三个真实踩坑记录
6.1 踩坑一:采样时间不足导致温度系统性偏低
症状:25℃ 恒温下,串口打印温度稳定在 22.8℃,始终偏低 2℃ 多,且阻值反推出来偏大。
排查链 :先用万用表直接量 NTC 两端电压,1.61V,换算阻值约 9.5kΩ,和 25℃ 的 10kΩ 差了 5%,说明问题出在 ADC 采集环节而不是标定环节(标定误差不会这么大)。然后怀疑 ADC 参考电压,但用 ADC 采一个已知 1.65V 的精密基准,读数 2048 完全正确------排除参考电压问题。最后翻参考手册 RM0008 的 ADC 章节,看到采样时间与源阻抗的关系表:5kΩ 源阻抗需要至少 40 个 cycle 的采样时间。而我 CubeMX 里用的默认 1.5 Cycles。根因 :采样电容充电时间不足,读数偏低。改成 239.5 Cycles 后,温度回到 25.1℃,与万用表测量吻合。这个坑的教训是:看到"整体偏低且偏差稳定"的现象,优先怀疑采样时间,而不是标定系数。
6.2 踩坑二:B 值套错导致 85℃ 端偏差 2℃
症状:恒温槽升到 85℃ 时,读数只有 83.2℃。
排查链 :一开始以为是标定公式写错了,检查代码没发现问题。然后怀疑是 B 值参数问题------把 3950 换成厂家 datasheet 里的 B25/85 分段值重新拟合后,偏差从 1.8℃ 降到 0.15℃。根因:B 方程本身是单参数近似,B 值随温度漂移,高温端必然低估。这正是 5.2 节那张表的来源。教训:宽温区测温别用 B 方程硬扛,Steinhart-Hart 或者查表才是正道。
6.3 踩坑三:电源纹波让读数在 0.2℃ 区间抖动
症状:读数稳定后仍然在 25.0~25.2℃ 之间随机跳动,肉眼可见。
排查链 :先用示波器看 3.3V 电源轨,发现 AMS1117 输出的纹波约 8mV@200kHz(开关电源前级传导过来的),这个纹波直接通过分压网络进了 ADC 输入端。然后在采样点对地并了 100nF 陶瓷电容,纹波压到 1mV 以内,读数跳动收窄到 0.05℃ 以内。根因:采样点阻抗高(5kΩ),纹波耦合进来没有旁路路径。教训:NTC 采样点必须就近并联滤波电容,这比任何软件滤波都治本。
七、故障排查速查表
| 现象 | 最常见原因 | 排查思路与解决 |
|---|---|---|
| 温度读数整体偏低 1~3℃ | ADC 采样时间太短 | 检查 SMP 位是否 ≥55.5 Cycles;用万用表量采样点电压交叉验证 |
| 读数在 ±0.1℃ 以上随机跳动 | 电源纹波 / 未滤波 | 示波器看 3.3V 纹波;采样点并联 100nF 电容;软件加中位值滤波 |
| 高温端偏差越来越大 | B 值方程在宽温区失效 | 换 Steinhart-Hart 或查表法;确认 B 值参数是否匹配具体型号 |
| 读数恒为 4095(满量程) | NTC 开路或虚焊 | 检查 NTC 两端焊点;万用表量阻值是否为 10k 量级 |
| 读数恒为 0 或极小 | 采样点对地短路 / Rm 开路 | 检查分压电阻是否焊上;测量采样点对地电压是否约 1.65V |
| 换一块板读数就变 | F103 内部参考电压个体差异 | 用 3.3V 轨作为基准(约掉 VREF 项);或做单点校准补偿偏移 |
| 读数变化滞后明显 | 采样次数过多 / NTC 热容大 | 减小中位值滤波窗口;检查 NTC 封装是否与被测体热接触良好 |
八、总结
回顾一下整条链路的关键结论:
- 分压电阻 Rm 选与目标温度区间中值对应的 NTC 阻值,能把 ADC 分辨率集中在需要的区间,10kΩ NTC 配 10kΩ 电阻是最均衡的默认组合;
- ADC 采样时间必须按源阻抗手动拉长,F103 默认 1.5 Cycles 对 5kΩ 源阻抗会引入约 1% 的系统误差,CubeMX 不会帮你判断;
- 三种标定方式实测误差:B 方程在 0~85℃ 最大偏差 1.8℃,Steinhart-Hart 0.15℃,查表法 0.09℃;宽温区或高精度场景别用 B 方程;
- 中位值滤波 128 次能把温度噪声从 ±0.088℃ 压到 ±0.015℃,但采样周期紧张时 32 次是更好的权衡点;
- F103 的 VREF 直连 VDDA,用"分压比"形式计算阻值可以天然抵消 VCC 与 VREF 同源部分的误差。
适用边界:本文方案适用于 0~85℃ 区间、静态或缓变温度测量。快速变化的温度(如气流温度)受限于 NTC 自身热响应时间(MF52 在空气中约 7s),需要换更小封装或薄膜 NTC。高于 100℃ 的应用建议用 PT100/PT1000 铂电阻,NTC 在高温段的非线性会放大到难以补偿。
扩展方向 :下一步可以试三件事------把测温做成定时器触发 + DMA 采集的多通道版本(顺便测环境温度做参考);在 STM32F103 上实现"双 ADC 比例测量"消除不同芯片 VREF 个体差异(很多工业板卡就是这么做的);或者把温度接到 PID 上做一个恒温控制闭环,把本文的稳态误差数据用在控制器整定里。STM32 入门到进阶的完整路线也可以参考这篇汇总:《STM32入门教程汇总》。
如需获取本文完整代码和更多实战项目,可开通 CSDN 技术会员。
版本备注
- 硬件平台:STM32F103C8T6 最小系统板 + MF52 10kΩ/B3950 NTC + 10kΩ 0.1% 精密电阻 + 100nF 滤波电容
- 软件版本:STM32CubeIDE 1.15.0 + STM32CubeF1 FW 1.8.5
- 兼容说明:代码可直接用于 F103 全系列;移植到 F0/F4 时需注意 ADC 时钟分频不同(F4 为 PCLK2/2、/4、/6、/8 可选),F4 支持软件校准(ADCAL),F1 无此功能
- API 变更风险:HAL 库 1.6.x 与 1.8.x 的 HAL_ADC_PollForConversion 行为一致;logf 依赖浮点库,Keil 下需勾选 Use MicroLIB 或完整 FPU 库,否则链接报错
参考资料
- ST 官方参考手册 RM0008(STM32F101xx/F102xx/F103xx)ADC 章节,采样时间与源阻抗关系表
- MF52 NTC 产品规格书(R-T 表与 B 值参数)
- 《手把手教你使用热敏电阻NTC,产品级精度±0.1℃以内》 --- 查表法与双 ADC 比例测量的工程实现
- 《NTC电阻基本知识介绍》 --- NTC 材料与参数基础
- 《零基础快速上手STM32开发(手把手保姆级教程)》 --- CubeMX 工程建立流程参考