
前阵子给一个锂电池包做温度监测板子随手买了10颗10K NTC热敏电阻焊好之后跑了一遍读取代码25℃的空调房里读出来29.3℃。这不是个例很多朋友做NTC温度测量都是这样——能跑通能出数但谈不上“精准”。后来我花了一下午把分压电路、采样方式、校准流程全部捋了一遍误差压到了±0.2℃以内代码也重写成了查表加插值的稳定版本。这篇文章就把10K NTC从选型、原理、电路、代码到校准的完整链路说清楚。末尾附完整的C语言实现直接用就行不用再翻各种零散资料。适合单片机入门玩家、DIY温控项目、以及正在做量产产品预研的硬件工程师作为参考。1. 为什么是10K NTC先搞清楚方案选型很多新手上来就问“NTC和DS18B20哪个准”这是个好问题但答案取决于你的使用场景。10K NTC不一定是精度最高的但它确实是性价比和灵活性平衡得最好的测温方案之一市面上大量家电、电动车BMS、3D打印机热床都在用。1.1 常见温度传感器的一线对比我整理了一个简单的选型对照表覆盖了日常项目里最常碰到的几类传感器传感器精度测温范围接口复杂度成本适合场景10K NTC±1%~±0.1℃取决于校准-40~125℃常见电阻分压ADC几毛到几块低成本、高灵敏度、紧凑测温DS18B20±0.5℃典型-55~125℃单总线需协议时序3~8元线缆较长、多点测温、工程机PT100±0.1℃~±0.3℃-200~600℃恒流源/电桥放大几十块起工业级、宽温、高精度热电偶±0.5℃~±2℃-200~1300℃冷端补偿高精度ADC几块到几十块超高温、宽温域如果你只是测板子温度、环境温度、电池温度、散热器温度这些常见对象NTC的响应速度其实非常快热敏电阻本身的热容小配上合适的封装贴片0603封装的NTC在水流或风道里能明显感受到变化。而且它不需要额外的协议芯片一个ADC通道再加一个普通电阻就搞定这对资源紧张的单片机来说太友好了。1.2 10K NTC的真实精度边界这里要泼一盆冷水标称“1%精度”的NTC在实际不校准的使用中误差可能到±2℃甚至更多。原因有三标称精度通常指25℃和50℃等特定温度点的阻抗偏差不代表全温域都准B值后面详细讲本身有离散性不同批次可能差1%分压电阻、ADC参考电压的误差会叠加进去。所以“精准测温”这件事只靠买好元件是不够的你需要的是“传感器自身标定 电路设计 软件补偿”三管齐下。这也是这篇文章想重点解决的。2. NTC测温的数学与电路基础不吃透原理就调不好精度在很多DIY教程里NTC测温的代码就一行 temperature 1/(log(Rntc/10000)/3950 1/298.15) - 273.15。这行代码本身没错但很多人抄了之后发现温度偏高或偏低就是因为不理解这条公式是怎么来的以及里面的3950、10000、298.15这几个常数到底代表什么。2.1 B值公式与Steinhart-Hart方程NTC热敏电阻的特点是温度升高阻值下降。这个关系可以用B值简化公式描述1/T 1/T0 (1/B) × ln(R/R0)其中T是目标温度开尔文T0是参考温度通常取298.15K也就是25℃R0是T0温度下的阻值10K NTC就是10000ΩR是当前实测阻值B是材料常数10K NTC常见有3380、3435、3950、4100等这个公式的好处是只需要两个参数就能算适合快速实现。但它的缺点是B值本身在宽温域里不是严格常数温度离25℃越远误差越大。更精确的描述是Steinhart-Hart方程它用三个系数拟合出更接近真实响应的曲线1/T A B×ln(R) C×(ln(R))³A、B、C可以由厂家给出也可以通过三点标定自行计算。对于需要高精度的场景强烈建议用这个公式配合查表法做软件补偿效果非常明显。2.2 分压电路推导与ADC转换关系单片机没法直接测电阻所以要把NTC的阻值变化转换成电压变化。最常见的电路就是一个分压器VCC —— 平衡电阻R_bal —— 采样点Vout —— NTC —— GND采样点接到单片机的ADC引脚电压关系是Vout VCC × R_NTC / (R_bal R_NTC)反过来得到Vout后可以算R_NTCR_NTC R_bal × Vout / (VCC - Vout) R_bal × ADC / (ADC_MAX - ADC)当VCC与ADC参考电压相等时Vout/VCC可以直接替换为ADC/ADC_MAX这就是代码里最常见的那个表达式。如12位ADCADC_MAX就是4095。这里注意一点如果单片机的ADC参考电压不是VCC而是内部基准比如2.5V那就必须用实际的电压值去算不能直接套ADC比例否则误差会非常明显。2.3 为什么分压电阻选10K而不是其他平衡电阻的取值直接决定测量灵敏度。直观理解如果说温度在-20℃到100℃变化时NTC阻值从几百千欧变到几百欧那平衡电阻选太大或太小都会让采样电压集中在某个区间另一端的灵敏度就被压缩了。选10K的理由其实很简单25℃时NTC是10K分压电阻10K时Vout正好等于VCC的一半。这个点附近是ADC最线性的区域而且Vout/ADC的中值附近灵敏度最高可以让25℃附近的测温精度最大化。对于大多数“室温附近”的应用10K是标准答案。如果你的测温区间偏向低温比如-40~0℃平衡电阻可以适当加大到47K偏向高温50~150℃可以减小到4.7K或3.3K。改平衡电阻的同时记得重新算一遍校准表。3. 硬件电路设计容易忽略的四个细节很多人以为NTC电路就一个电阻分压随便搭搭就行。但实际上这四个细节做不好后面的软件再怎么优化都救不回来。3.1 自热效应与采样电流控制NTC在分压电路里会流过电流电流会在电阻上产生焦耳热导致NTC自身温度高于环境温度这就是自热效应。别小看这一点我实测过3.3V供电平衡电阻10KNTC在25℃时静态功耗约为0.27mW某些小封装NTC的自热系数在空气中可能到0.5℃/mW算下来就是0.13℃的偏差如果环境散热差误差更大。降低自热的办法很直接提高平衡电阻阻值或者采用间歇式采样——平时把分压电路断电只在采样的那几百微秒里接通。用GPIO供电是个非常实用的技巧能显著降低自热对测量结果的影响。3.2 滤波电容与ADC输入阻抗在采样点对地加一个0.1μF的陶瓷电容可以滤掉大部分高频干扰这是很多开发板都有的标准做法。但电容不能加得太大因为ADC采样瞬间需要从电容取电荷大电容会导致采样值“拉低”最后稳定不下来。我试过1μFSTM32常规采样模式下读数明显偏低来回跳。另外一个容易踩的坑是单片机的ADC输入阻抗不是无穷大GPIO内部的采样开关有寄生电容和导通电阻。如果分压电路的总阻抗很高比如平衡电阻100KADC采样的建立时间不足读出来的数字会偏低。解决办法适当延长ADC采样时间或者用运放做缓冲。3.3 引线电阻、焊盘与PCB布局短距离PCB走线时引线电阻的影响基本可以忽略。但如果NTC通过线缆探头方式引出线缆本身有阻值这个阻值会直接叠加到R_NTC上。例如普通10cm杜邦线的电阻大约0.1Ω影响微乎其微但如果用几十米的屏蔽线阻值可能就有几欧甚至几十欧100℃时的NTC阻值可能只有几百欧这时候误差就很明显了。处理思路用三线制或四线制测量或者用已知长度的线缆做一次总阻值补偿。我量产过一个小型温度探头就是在一端做了“短接探头”的出厂自检测出线阻后写进Flash再在软件里减掉。PCB布局方面NTC不要靠近大功率器件和热风面如果目的是测空气温度悬空一点更好不要贴着铜皮大面积敷地。3.4 基准电压的选择如果MCU的VCC本身是LDO输出纹波很小且稳定那直接用VCC做ADC参考是可以接受的。如果板上有电机、继电器这类大电流设备VCC波动明显ADC读数会跟着抖这种情况下建议用内部基准电压或者独立基准芯片。另外一个细节12位ADC的量化步进对10K NTC测温来说大概对应0.05℃的分辨率这已经够用了。真正限制精度的是参考电压的漂移而不是ADC本身。4. 核心代码实现从ADC原始值到温度值这章直接给干货。我的代码风格是“平台无关的C语言”ADC读取函数接口保留你只需要在自己工程里实现uint16_t NTC_ReadADC(void)这一行剩下的逻辑全部通用。4.1 软件滤波滑动平均与限幅的配合NTC本身是慢变信号但电路中的电磁干扰会造成偶发的尖刺。单纯用平均值滤波一个尖刺就会把结果拉偏单纯用中值滤波又难消除周期性噪声。我常用的是“限幅滑动平均”组合先判断本次采样值与上次合法值的差是否超过阈值比如2℃超过视为异常值丢弃连续采样8~16次去掉最大值和最小值再对剩余值取平均。这段滤波代码量很小但测温稳定性提升非常明显尤其是在带电机、继电器的板子上。4.2 公式法用B值公式一行算出温度适合不想维护查找表、对温度精度要求一般的场景。核心计算如下/** * brief 根据ADC值计算NTC阻值 * param adc: ADC采样值0~4095 * param ref_resistor: 平衡电阻阻值单位Ω * retval NTC当前阻值单位Ω */ float NTC_GetResistance(uint16_t adc, uint32_t ref_resistor) { if (adc 4095) { adc 4095; // 防止除零 } if (adc 0) { adc 1; } return (float)ref_resistor * adc / (4095.0f - adc); } /** * brief 根据NTC阻值计算温度B值公式版本 * param resistance: NTC阻值单位Ω * param r25: 25℃标称阻值10K NTC传入10000.0f * param b_value: B值例如3950.0f * retval 温度单位℃ */ float NTC_GetTempByFormula(float resistance, float r25, float b_value) { float t_kelvin; t_kelvin 1.0f / (1.0f / 298.15f (1.0f / b_value) * logf(resistance / r25)); return t_kelvin - 273.15f; }用法示例uint16_t adc NTC_ReadADC(); float r NTC_GetResistance(adc, 10000.0f); float temp NTC_GetTempByFormula(r, 10000.0f, 3950.0f);这版本实现简单但我必须提醒一下B值公式在25℃中心点附近精度还可以到了-20℃或80℃这种区间误差可能到±1.5℃以上。所以如果你的项目跨越的温域比较宽请看下面的查表法。4.3 查表法二分查找加线性插值速度快且无浮点压力查表法的思路是提前算好“ADC值→温度”的映射表运行时二分查找相邻两个点再做线性插值。好处是速度快、不依赖浮点库、完全不受B值公式外推误差影响精度反而更高。生成查找表可以用脚本算我常用的Python生成脚本如下import math # 参数 B 3950.0 R25 10000.0 R_BAL 10000.0 VCC 3.3 ADC_MAX 4095 # 从-20℃到100℃每1℃打一个点 temps range(-20, 101) print(static const int16_t ntc_temp_lut[] {) for t in temps: tk t 273.15 r R25 * math.exp(B * (1.0 / tk - 1.0 / 298.15)) adc int(round((r / (R_BAL r)) * ADC_MAX)) if adc 0: adc 0 if adc ADC_MAX: adc ADC_MAX print(f {adc}, // {t}℃) print(};)C端的查询函数#define LUT_START_TEMP (-20) #define LUT_END_TEMP (100) #define LUT_STEP_TEMP (1) /** * brief 根据ADC值查询温度查表线性插值 * param adc: ADC采样值 * param lut: 查找表 * retval 温度℃放大了10倍返回比如25.3℃返回253 */ int16_t NTC_GetTempByLUT(uint16_t adc, const int16_t lut[]) { if (adc lut[0]) { return LUT_START_TEMP * 10; } if (adc lut[LUT_END_TEMP - LUT_START_TEMP]) { return LUT_END_TEMP * 10; } int idx 0; int left 0, right (LUT_END_TEMP - LUT_START_TEMP); while (left right) { int mid (left right) / 2; if (lut[mid] adc) { idx mid; left mid 1; } else { right mid - 1; } } // 线性插值 int16_t adc0 lut[idx]; int16_t adc1 lut[idx 1]; int16_t temp0 (LUT_START_TEMP idx) * 10; if (adc1 adc0) { return temp0; } int16_t temp_res temp0 (int16_t)((adc - adc0) * 10 / (adc1 - adc0)); return temp_res; }这里返回温度放大了10倍避免浮点运算这是MCU上很实用的做法。如果你要更高的温度分辨率可以在查找表里做成0.5℃一个点甚至0.25℃一个点但要注意表体积会变大。4.4 完整代码整合附源码把上面的模块拼起来再加上限幅滤波就是一个可以直接跑的函数#include math.h #define NTC_R25 (10000.0f) #define NTC_BVAL (3950.0f) #define REF_RESISTOR (10000.0f) #define ADC_MAX_VALUE (4095.0f) #define FILTER_SAMPLES (16) #define TEMP_LIMIT (200) // 限幅阈值单位0.1℃即20℃ // 外部读取一次ADC原始值 extern uint16_t NTC_ReadADC(void); static int16_t last_temp_x10 0; /** * brief 限幅滑动平均滤波返回稳定后的温度放大了10倍 */ int16_t NTC_ReadTemp_x10(void) { uint32_t sum 0; uint16_t min_v 4095, max_v 0; uint16_t samples[FILTER_SAMPLES]; // 采样并同时找极值 for (int i 0; i FILTER_SAMPLES; i) { uint16_t v NTC_ReadADC(); samples[i] v; if (v min_v) min_v v; if (v max_v) max_v v; } // 去掉极值后求和 int cnt 0; for (int i 0; i FILTER_SAMPLES; i) { if (samples[i] ! min_v samples[i] ! max_v) { sum samples[i]; cnt; } } if (cnt 0) { sum samples[0]; cnt 1; } uint16_t adc_avg (uint16_t)(sum / cnt); // 查表得到温度放大10倍 int16_t temp_x10 NTC_GetTempByLUT(adc_avg, ntc_temp_lut); // 限幅防止单次跳变过大 if (last_temp_x10 ! 0 abs(temp_x10 - last_temp_x10) TEMP_LIMIT) { return last_temp_x10; } last_temp_x10 temp_x10; return temp_x10; }说明一下这段代码里的ntc_temp_lut就是4.3节Python脚本生成的数组。直接把两个文件放一起编译即可不需要额外依赖。5. 校准才是“精准”的关键两点法与三点法实操代码写完了但误差可能还在±1℃以上。真正把系统精度拉下来的是校准。这章讲的是我实测有效的校准流程。5.1 误差来源拆解为什么标称1%的器件实际测得不准先看一个典型系统里的误差分配误差来源典型偏差量影响权重NTC阻值初始偏差25℃±0.5%~±1%高B值偏差±0.5%~±1.5%高分压电阻精度1%±0.3℃中ADC参考电压偏差±0.2℃~±0.5℃中自热误差0.1℃~0.5℃低连接器/线缆接触电阻0.1Ω~几Ω看场景这些误差有些是固定偏差可以通过校准消除有些是偶发或漂移量只能靠电路设计和滤波抑制。如果你发现温度整体偏高或偏低一截那基本是初始阻值和B值偏差导致的固定误差完全可以通过校准修正。5.2 两点校准冰水浴与恒温槽最简单的有效校准是做两个点0℃用冰水混合物100℃用沸水。如果条件不够也可以用恒温槽或者精密温度计做参考。操作流程把NTC探头放进冰水混合物碎冰水稳定后0℃读取代码输出的ADC值或温度值把NTC探头放进沸水中保证当地大气压下沸点稳定可查当地沸点表再读一次根据这两个点的实测偏差算出温度修正的斜率与偏移量在软件里做线性补偿。对应代码里的实现static int8_t cal_offset 0; // 偏移量单位℃ static float cal_slope 1.0f; // 斜率修正系数 float NTC_GetTempWithCalibration(void) { int16_t raw_x10 NTC_ReadTemp_x10(); float temp raw_x10 / 10.0f; temp temp * cal_slope cal_offset; return temp; }用冰水浴和沸水校准后整套系统的误差可以从±2℃压缩到±0.3℃以内。如果你只做单点校准比如只在25℃或你的工作温度附近校一次那最简单的方式就是记一个固定偏移量offset 基准温度 - 实测温度然后加到结果上。单点校准对“平台漂移”类型的误差很有效但解决不了斜率问题。5.3 三点校准与Steinhart-Hart系数拟合两点校准适合50℃以内的窄温域。如果你的测试范围很宽比如-20℃到100℃两点校准还不够因为NTC的非线性误差并不是一条直线。这时候用三个温度点做Steinhart-Hart系数拟合效果更好。三个点一般取低温、中温、高温例如0℃、25℃、80℃。对应的就是三个温度T1、T2、T3和三个阻值R1、R2、R3。把三组(T, R)代入方程组1/T1 A B·ln(R1) C·(ln(R1))³1/T2 A B·ln(R2) C·(ln(R2))³1/T3 A B·ln(R3) C·(ln(R3))³解出A、B、C三个系数然后用Steinhart-Hart公式去计算温度会比B值公式在全温域内准得多。这套计算我建议在PC端完成把得到的A、B、C直接写进代码不要在单片机上现场解方程。下面是一个用Python三参数拟合的示例import numpy as np T [273.15, 298.15, 353.15] # 0℃, 25℃, 80℃ R [32550.0, 10000.0, 1350.0] # 实测电阻值 # 构造矩阵求解 M [] Y [] for t, r in zip(T, R): lr np.log(r) M.append([1, lr, lr**3]) Y.append(1.0 / t) A, B, C np.linalg.solve(np.array(M), np.array(Y)) print(A, B, C)把A、B、C代入你的C代码float NTC_GetTempBySH(float r) { float lr logf(r); float inv_t A B * lr C * lr * lr * lr; return (1.0f / inv_t) - 273.15f; }实测下来三点拟合配合查表之后的残余误差可以控制在±0.1℃左右前提是三个标定温度点本身要够准。有条件就用恒温槽没条件就用精密铂电阻温度计当参考。5.4 校准结果的验证与回读校准不是做完就结束一定要做回读测试。建议在至少5个温度点比如0、25、40、60、80℃上回测记录修正前的读数和修正后的读数计算每个点的残余误差。如果某个点误差还是大说明你的校准模型或者测温点选取有问题需要重新选点或者检查硬件。我自己常用的回读记录表是这样的参考温度℃修正前读数℃修正后读数℃残余误差℃0.02.10.10.125.027.324.9-0.140.042.840.20.260.063.559.9-0.180.083.180.10.1回读测试可以暴露很多问题。我之前有一次校准后在某一个点还是偏了0.4℃排查半天才发现是恒温槽的温度均匀性不够探头放的位置比参考探头高了一点。6. 实测数据与踩坑复盘把精度从±2℃压到±0.2℃最后这章分享一组我自己的实测数据以及过程中踩过的几个比较隐蔽的坑。6.1 一组实测对比数据测试条件STM32F10312位ADC10K NTCB39501%精度平衡电阻10K1%室温25℃环境下对同一个恒温槽进行连续测量参考用PT100。校准用的是0℃冰水浴和100℃沸水两点线性修正。恒温槽设定温度未校准读数校准后读数偏差01.80.10.11012.09.9-0.12527.125.10.14042.640.10.15557.955.00.07073.569.9-0.1未校准的初始偏差最大到了1.8℃主要来自NTC的初始阻值和B值离散。两点校准之后0~70℃范围内残余误差稳定在±0.1℃左右。这说明对于大多数日常应用两点校准已经足够。6.2 我踩过的具体坑与解决办法第一个坑ADC采样时间设太短。我最初用STM32默认的1.5周期采样读出来的值明显偏低温度整体偏高大概0.5℃。原因是分压电路阻抗高ADC内部采样电容没充足。把采样时间改到7.5周期或更长时间后读数就稳了。第二个坑分压电阻用了电位器调参。调试的时候顺手用了3296电位器当平衡电阻方便调阻值。结果温度读数跳来跳去怎么滤波都压不住。最后测量发现电位器本身在振动和温度变化下阻值有波动。换成固定精度电阻后问题消失。所以量产或正式测试时平衡电阻务必用固定电阻最好是温漂低的金属膜电阻。第三个坑NTC贴片封装紧挨着才跑满的MCU。我之前测环境温度把NTC放在STM32旁边MCU跑起来表面温度45℃左右NTC被烤得读数比实际环境温度高了将近5℃。后来把NTC拉远用飞线引到外壳开口位置读数才正常。传感器测的是什么不取决于它自己而取决于它被放在哪里。第四个坑用了内部基准电压但没做软件校准。STM32的内部基准VREFINT可以测实际VCC我干脆直接用了VCC作为ADC参考结果每次换电池或者USB供电不稳读数就在漂。后来改成固定采样、固定供电的LDO代码里再配合VREFINT做动态参考校准读数才稳定下来。6.3 后续可以扩展的方向当前这套方案已经能稳定跑到±0.2℃以内足够覆盖锂电池监测、暖通控制、智能家居、工业仪表等一大批应用。如果你想继续深挖有几个方向可以尝试NTC多点分布式测温用多路ADC或者模拟开关切换一次性读取多个位置温度再结合查表统一校准与无线模组结合LoRa或BLE做低功耗无线温度节点NTC分压电路可以做到微安级功耗温度数据日志把校准后的温度数据每隔固定时间写入Flash或SD卡方便做长期趋势分析动态自热补偿依据不同散热条件建立热模型对自热误差做二次修正。根据我个人的实际经验10K NTC从来不是“不准”的代名词它只是一个需要被认真对待的传感器——电路上做好分压和滤波软件上用好合适模型再配上校准流程完全可以在低成本前提下达到让人满意的精度。如果你正在做相关项目不妨把这套流程直接套用应该能少走不少弯路。