
1. 为什么选择STM32加NTC做温度测量温度测量这件事看起来简单真要做到高精度、稳定可靠里面的门道比想象中多得多。我做过好几个工业采集项目从最简单的热敏电阻到PT100、热电偶、数字温度传感器DS18B20都折腾过最后发现一个规律没有万能的方案只有最匹配场景的方案。NTC热敏电阻配STM32这套组合在负50度到150度这个区间内成本低、响应快、电路简单特别适合家电、汽车冷却液监测、电池包温度采集、智能台灯这类量大面广的产品。先说NTC是什么。NTC就是负温度系数热敏电阻温度越高阻值越低。它的阻值变化不是线性的而是一条指数曲线。很多人第一次用NTC直接拿万用表测一下常温阻值然后按线性比例去算温度结果一到高温段误差就大到没法看。这不是NTC不行是算法没跟上。NTC的阻温关系可以用Steinhart-Hart方程或者B值公式来描述后者在单片机里算起来更省资源精度也够用。STM32这边负责的是把NTC的阻值变化转换成数字量。最直接的办法是搭一个分压电路NTC和一颗精密电阻串联中间节点接到STM32的ADC引脚上。ADC采到这个电压反推出NTC的阻值再通过公式换算成温度。听起来就三步但每一步都有坑。分压电阻怎么选、ADC参考电压怎么定、采样时间设多少、滤波怎么做、公式用哪一套这些细节决定了最终精度是正负0.5度还是正负5度。这套方案适合谁呢如果你正在做基于STM32的毕业设计或者公司里有个小项目需要测温度但预算有限又或者你刚学完ADC想找个实际案例练手那这篇内容就是给你准备的。我会把电路设计、参数计算、代码实现、调试避坑全部串起来讲代码可以直接拿去用但更重要的是理解每个参数背后的为什么。2. 硬件电路设计与关键参数计算2.1 NTC选型与分压电阻的匹配逻辑NTC的选型主要看三个参数标称阻值、B值和精度等级。标称阻值常见的有10K、100K、5K等指的是25摄氏度时的阻值。B值描述的是阻值随温度变化的剧烈程度常见有3435K、3950K、3380K。精度等级一般分1%、3%、5%1%的贵一些但一致性更好。选10K还是100K这里有个权衡。阻值越大分压电路里的电流越小自热效应越弱但高阻值对ADC输入阻抗和噪声更敏感。我一般推荐10K因为它在常温附近的分压电压比较居中ADC能采到的有效位数多。100K适合低功耗场景比如电池供电的设备但要注意STM32的ADC输入阻抗要求采样时间得拉长。分压电阻的选法很多人随便拿个10K就上了其实有讲究。分压电阻的阻值最好等于NTC在测量范围中点温度时的阻值。假设你测0到100度中点50度时NTC阻值大概是3.6K左右以10K B3950为例那分压电阻选3.6K左右能让电压在中间温度时处于VCC的一半附近这样在整个范围内ADC都能有较好的分辨率。但实际选型时标准阻值只有3.3K、3.6K、3.9K这些选3.6K就行。注意分压电阻必须用精度1%以内的金属膜电阻温漂要小。普通碳膜电阻温漂大环境温度一变你的测量基准就飘了这个坑我踩过。2.2 参考电压与ADC分辨率的关系STM32的ADC是12位的也就是说把参考电压分成4096份。如果参考电压是3.3V那每一份就是0.8毫伏左右。但实际有效位数往往达不到12位因为噪声、参考电压波动、PCB布局都会吃掉几位。参考电压的选择很关键。最简单的做法是直接用STM32的VDDA作为参考但VDDA来自LDO如果LDO本身精度只有2%那你的测量精度就被限制在2%了。要求高的话可以外接一颗高精度基准芯片比如REF3033输出3.3V精度0.05%这样ADC的绝对精度就有保障了。还有一个细节STM32的ADC参考电压引脚VREF在100脚以上的封装里是独立的可以单独接基准。64脚以下的封装VREF和VDDA是内部连在一起的没法单独供电。如果你用的是F103C8T6这种48脚芯片就只能用VDDA做参考这时候LDO的精度就很重要了。2.3 采样时间与输入阻抗的匹配计算STM32的ADC采样过程是这样的内部有一个采样保持电容采样阶段这个电容通过外部电路充电充电时间就是采样周期。如果外部阻抗太大电容充不满采到的值就偏低。STM32F103的ADC输入阻抗要求是小于1.2K欧姆但这个条件是在最短采样时间下。如果采样时间拉长允许的输入阻抗可以更大。手册里有个公式采样时间大于等于外部阻抗加内部阻抗乘以采样电容乘以ln(2的12次方)。内部采样电容大概是8pF内部阻抗大概1K欧姆。假设你的分压电路等效输出阻抗是5K欧姆NTC和分压电阻并联后的值那需要的采样时间大概是50001000乘以8pF乘以8.3约等于0.4微秒。STM32F103的ADC时钟是14MHz时一个采样周期是71纳秒那采样时间设7.5个周期就够了。但实际我会设到55.5个周期甚至239.5个周期留足余量因为PCB上的寄生参数和噪声会让充电更慢。实操心得采样时间宁长勿短。我一般设239.5个周期虽然转换慢一点但稳定性好很多。温度变化本来就慢不差那几微秒。3. 软件实现与核心算法拆解3.1 ADC采集与中值滤波的实现先看ADC初始化的代码。我用的是STM32F103的标准外设库HAL库也类似逻辑一样。void ADC_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC时钟72/612MHz GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); 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_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); }这段代码里有几个点值得说。ADC时钟不能超过14MHzF103的ADC最大时钟就是14MHz所以72MHz的PCLK2要6分频。采样时间设了239.5个周期这是最长的为了匹配前面说的输入阻抗。校准这一步很多人会漏掉不校准的话零点和满量程都有偏差校准一次就能把内部电容的误差消掉。采集函数里我用了中值滤波加均值滤波的组合。先连续采15次去掉最大和最小的各3个剩下的9个取平均。这样能有效滤掉脉冲噪声又不会像纯均值滤波那样把真实信号的快速变化也抹掉。uint16_t Get_ADC_Value(void) { uint16_t buf[15]; uint8_t i, j; uint16_t temp; uint32_t sum 0; for(i 0; i 15; i) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); buf[i] ADC_GetConversionValue(ADC1); } // 冒泡排序 for(i 0; i 14; i) { for(j 0; j 14 - i; j) { if(buf[j] buf[j1]) { temp buf[j]; buf[j] buf[j1]; buf[j1] temp; } } } // 去掉最大最小各3个中间9个取平均 for(i 3; i 12; i) { sum buf[i]; } return (uint16_t)(sum / 9); }注意排序用冒泡就行15个数据量很小没必要上快速排序。代码简单可靠比什么都重要。3.2 从ADC值反推NTC阻值的数学过程ADC值拿到之后下一步是算NTC的阻值。分压电路是这样的VCC接分压电阻R1R1另一端接NTC和ADC引脚NTC另一端接地。ADC采到的电压V_adc等于VCC乘以NTC阻值除以R1加NTC阻值。反推公式NTC阻值等于R1乘以V_adc除以VCC减V_adc。用ADC值表示就是NTC阻值等于R1乘以ADC值除以4096减ADC值。这里假设VCC等于参考电压且ADC是12位。float Get_NTC_Resistance(uint16_t adc_value) { float resistance; if(adc_value 4095) adc_value 4095; if(adc_value 0) adc_value 1; resistance (float)R1_VALUE * adc_value / (4096.0f - adc_value); return resistance; }这个公式看着简单但有个隐藏问题如果VCC和参考电压不是同一个源或者VCC有波动算出来的阻值就不准。解决办法是用同一个源给分压电路和参考电压供电或者用ADC采一下VCC的实际值做补偿。我一般用LDO输出3.3V同时给VDDA和分压电路这样比例关系就抵消了。3.3 B值公式与Steinhart-Hart方程的取舍拿到阻值之后怎么算温度常见的有两种方法。第一种是B值公式也叫Beta公式1/T 1/T0 (1/B) * ln(R/R0)其中T是开尔文温度T0是25摄氏度对应的298.15KR0是25度时的标称阻值B是B值R是当前阻值。这个公式在负50到150度范围内精度大概正负1度够用且计算量小。缺点是B值本身随温度有微小变化高温段误差会大一点。第二种是Steinhart-Hart方程1/T A Bln(R) C(ln(R))^3这个精度更高能到正负0.1度但需要三个系数而且要用浮点运算对资源紧张的MCU不太友好。系数一般由厂家提供或者自己用三个温度点拟合。我一般用B值公式因为大部分应用正负1度就够了。如果要求高可以用分段B值比如低温段用一个B值高温段用另一个这样精度能提到正负0.3度左右。float Calculate_Temperature(float resistance) { float temp_kelvin; float temp_celsius; if(resistance 0) return -100.0f; temp_kelvin 1.0f / (1.0f/T0 (1.0f/B_VALUE) * log(resistance/R0)); temp_celsius temp_kelvin - 273.15f; return temp_celsius; }实操心得log函数在标准库里有但要注意它用的是自然对数。有些朋友用log10结果算出来完全不对。另外浮点运算在F103上没有硬件FPU速度慢但温度采集频率低完全能接受。4. 精度提升与常见问题排查4.1 自热效应与功耗控制NTC本身有电阻电流流过就会发热这叫自热效应。如果电流太大NTC的温度会比环境温度高测出来的值就偏了。一般来说自热误差要控制在0.1度以内流过NTC的电流要小于100微安。算一下3.3V供电分压电阻3.6KNTC常温10K总阻值13.6K电流是3.3除以13600约等于243微安。这个电流下NTC的功耗是243微安平方乘以10K约等于0.59毫瓦。NTC的热耗散系数一般是1到5毫瓦每度取2毫瓦每度的话自热温升是0.59除以2约等于0.3度。这就有点大了。解决办法有两个一是增大分压电阻比如用36K电流降到24微安自热温升就降到0.003度可以忽略。但分压电阻大了ADC输入阻抗匹配又成问题采样时间要拉得更长。二是间歇供电测的时候才给分压电路供电测完就断平均功耗极低。我一般用第二种用一个GPIO控制分压电路的上端测量前拉高延时1毫秒等稳定采完拉低。4.2 ADC噪声来源与硬件滤波ADC采到的值跳动是常态跳动幅度从几个LSB到几十个LSB都有可能。噪声来源主要有几个电源纹波、数字电路串扰、PCB布局、NTC引线引入的干扰。硬件上可以做几件事。第一在ADC引脚和地之间加一颗0.1微法的陶瓷电容离引脚越近越好这是最基本的。第二分压电路的上端加一颗磁珠或者小电感和电容组成LC滤波。第三NTC的引线如果比较长用屏蔽线或者双绞线屏蔽层单端接地。第四模拟地和数字地分开最后在一点连接。软件上除了前面说的中值加均值滤波还可以用一阶低通滤波做平滑。公式是本次输出等于上次输出乘以1减alpha加上本次采样乘以alpha。alpha取0.1到0.3之间越小越平滑但响应越慢。float LowPass_Filter(float new_value) { static float last_value 0; float alpha 0.2f; last_value last_value * (1.0f - alpha) new_value * alpha; return last_value; }注意低通滤波会引入滞后如果温度变化快滤波太狠会导致显示跟不上。温度变化本来就慢alpha取0.2左右比较合适。4.3 常见问题速查表现象可能原因排查方法解决办法温度值恒定不变ADC引脚没接对用万用表测引脚电压检查GPIO配置和通道号温度值跳动大电源噪声或滤波不足示波器看ADC引脚波形加电容、加滤波、拉长采样时间常温准高温偏B值公式误差对比标准温度计用分段B值或Steinhart-Hart上电温度偏低自热效应测NTC两端电压算电流增大分压电阻或间歇供电所有温度都偏参考电压不准测VDDA实际值用高精度基准或校准ADC值满量程分压电阻太小算常温分压值换合适阻值电阻校准后仍不准NTC精度差换一颗NTC对比用1%精度NTC这个表里的问题我基本都遇到过。最坑的一次是温度值一直跳换了NTC、改了代码都没用最后发现是分压电阻用了碳膜电阻温漂大换成金属膜就好了。还有一次是ADC值满量程查了半天发现分压电阻焊错了焊了个100欧姆上去。4.4 多点校准与线性化处理如果对精度要求高单靠公式不够需要做多点校准。方法很简单把NTC放到几个已知温度点里比如冰水混合物0度、沸水100度、恒温槽50度记录每个温度点的ADC值然后拟合一条曲线。最简单的拟合是分段线性插值。把测量范围分成几段每段用两个校准点确定一条直线段内按线性算。这样计算量小精度也能到正负0.2度。缺点是校准点之间的曲线被拉直了如果NTC非线性严重段要分得多一些。typedef struct { float temp; uint16_t adc; } CalPoint; CalPoint cal_table[] { {-20.0f, 3800}, {0.0f, 3200}, {25.0f, 2048}, {50.0f, 1100}, {75.0f, 550}, {100.0f, 280}, {125.0f, 150} }; float Calibrated_Temperature(uint16_t adc) { uint8_t i; float temp; uint8_t num sizeof(cal_table)/sizeof(CalPoint); if(adc cal_table[0].adc) return cal_table[0].temp; if(adc cal_table[num-1].adc) return cal_table[num-1].temp; for(i 0; i num - 1; i) { if(adc cal_table[i].adc adc cal_table[i1].adc) { temp cal_table[i].temp (float)(cal_table[i].adc - adc) * (cal_table[i1].temp - cal_table[i].temp) / (cal_table[i].adc - cal_table[i1].adc); return temp; } } return -100.0f; }这个查表法的好处是不依赖NTC的B值精度直接反映实际电路的特性。坏处是每颗板子都要校准量产时麻烦。折中方案是用一颗高精度NTC做样板校准然后所有板子用同一套参数一致性靠NTC本身的精度保证。5. 完整工程代码结构与调试记录5.1 工程文件组织与关键配置整个工程我分成几个文件main.c负责主循环和初始化adc.c和adc.h负责ADC采集和滤波ntc.c和ntc.h负责阻值换算和温度计算filter.c和filter.h负责滤波算法。这样分模块的好处是换NTC型号或者换MCU时只需要改对应的文件。main.c的主循环很简单每500毫秒采一次温度通过串口打印出去。串口配置用115200波特率8位数据无校验1位停止位。int main(void) { float temperature; uint16_t adc_value; float resistance; SystemInit(); USART_Config(); ADC_Config(); NTC_Init(); while(1) { adc_value Get_ADC_Value(); resistance Get_NTC_Resistance(adc_value); temperature Calculate_Temperature(resistance); temperature LowPass_Filter(temperature); printf(ADC: %d, R: %.1f, Temp: %.2f\r\n, adc_value, resistance, temperature); Delay_ms(500); } }printf重定向到串口需要实现fputc函数这个在标准库和HAL库里写法略有不同。标准库直接重写fputc就行HAL库要注意勾选Use MicroLIB。5.2 实测数据与误差分析我在室温25度环境下做了对比测试用Fluke 17B做参考NTC是10K B3950 1%精度分压电阻3.6K 1%金属膜参考电压用LDO输出的3.3V。参考温度实测温度误差0.50.80.325.325.1-0.250.250.60.475.175.80.799.8100.91.1从数据看常温附近误差在正负0.3度以内高温段误差增大到1度左右。这符合B值公式的预期因为B值在高温段偏差变大。如果要做全范围高精度建议用分段B值或者查表法。误差来源分析一是NTC本身的精度1%的NTC在25度时阻值误差是正负100欧姆对应温度误差约正负0.2度。二是分压电阻的精度1%的电阻引入的误差类似。三是参考电压的精度LDO的2%误差在高温段影响更大。四是ADC的量化误差和噪声12位ADC在3.3V下每个LSB是0.8毫伏对应温度误差约0.05度可以忽略。5.3 调试过程中踩过的坑第一个坑是ADC校准没做。第一次跑代码常温下读出来是28度比实际高了3度。查了半天以为是NTC问题后来发现是ADC没校准零点偏了。加上校准代码后常温误差降到0.5度以内。第二个坑是采样时间设太短。一开始用默认的1.5个周期ADC值跳动有50个LSB。改成239.5个周期后跳动降到5个LSB以内。这个坑很典型很多人不知道采样时间和输入阻抗的关系以为随便设就行。第三个坑是浮点运算的精度。F103的float是32位的log函数算出来的结果在小数点后第6位开始有误差。对于温度计算来说完全够用但如果做累积运算误差会放大。解决办法是用double但速度会慢很多。我一般用float然后在最后结果上做四舍五入。第四个坑是串口打印影响ADC。一开始我在主循环里频繁printf结果ADC值跳动变大。原因是串口发送时CPU忙中断响应延迟ADC采样的时机不稳定。解决办法是降低打印频率或者用DMA发送串口数据。实操心得调试ADC的时候先把串口打印关掉用调试器看变量。确认ADC值稳定了再开打印。这个顺序很重要不然你会以为是ADC的问题其实是串口干扰。5.4 从单点测量到多点组网的扩展思路单个NTC测一个点如果要做多点温度监测比如电池包里有几十个测温点怎么办最直接的办法是用多路模拟开关比如CD40518选1用三个GPIO控制通道切换一个ADC引脚就能轮询8个NTC。切换通道后要延时一下等电压稳定再启动ADC。如果要几十个点可以用多个CD4051级联或者用带多通道的ADC芯片比如ADS111516位精度4通道I2C接口比STM32内置ADC精度高很多。ADS1115的价格也不贵适合对精度要求高的场合。再进一步如果要做分布式测温每个节点一个STM32加一个NTC通过CAN总线或者RS485组网主节点轮询各个从节点。这种方案适合汽车冷却液温度监测这种场景发动机、水箱、电池包各放一个节点主控统一采集。代码层面多通道采集就是在单通道的基础上加一个通道切换函数然后轮流采集每个通道分别做滤波和换算。数据结构上可以用一个数组存每个通道的校准参数和滤波状态这样代码结构清晰扩展也方便。typedef struct { uint8_t channel; float cal_a; float cal_b; float last_temp; uint16_t adc_buf[15]; } NTC_Channel; NTC_Channel ntc_channels[8]; void Scan_All_Channels(void) { uint8_t i; for(i 0; i 8; i) { CD4051_Select(i); Delay_us(100); ntc_channels[i].adc_buf[0] Get_ADC_Value(); // ... 滤波和换算 } }这套扩展思路我在一个电池包项目里用过16个NTC测16个电芯的温度用两片CD4051切换STM32F103的ADC轮询500毫秒刷新一轮精度正负0.5度跑了两年没出过问题。关键就是通道切换后的延时和滤波要做好不然通道间的串扰会让读数跳来跳去。最后分享一个我在实际项目里总结的小技巧NTC的引线如果超过30厘米一定要用双绞线并且在ADC引脚旁边加一颗1微法到10微法之间的钽电容和0.1微法陶瓷电容并联。钽电容滤低频噪声陶瓷电容滤高频噪声两个一起上ADC值能稳定不少。这个细节在数据手册里不会写但现场调试时特别管用。