
简介这份资源是面向嵌入式初学者与STM32开发者的NTC热敏电阻温度采集程序基于STM32F1038T6微控制器实现解决从模拟电压采集到温度换算的完整链路问题适合用于工业控制、物联网设备等场景下的温度监测学习与实践。压缩包共4个文件包含2个C源文件与2个头文件分别承担ADC配置、NTC参数配置及主逻辑实现整体约5KB结构精简便于快速移植到同类工程。目前已有6573人学习下载热度较高。程序覆盖ADC初始化、通道与采样参数设置、轮询或中断方式读取转换结果、依据B值系数与电阻-温度关系完成温度换算并预留显示或串口输出接口同时涉及温度补偿、滑动平均滤波与任务调度优化等提升精度与稳定性的思路。读者可借此掌握STM32的ADC操作、中断处理与NTC传感技术是理解硬件接口到软件算法完整方案的实用案例。1. NTC 温度采集从 ADC 原始值到可信温度的完整链路NTC 温度采集程序在 STM32 项目里出现的频率极高但真正把它做稳的人不多。多数人第一次写都是 ADC 读一个值、套个公式、串口打印看起来能跑放到实际环境里就开始飘同一杯水今天 25.3℃明天 24.1℃换个板子差两度线拉长一点读数直接跳变。问题不在 NTC 本身而在于从 ADC 原始码到最终温度之间有一条被大多数人忽略的链路——参考电压、分压电阻、B 值公式、自热效应、滤波策略每一环都能把精度吃掉。这篇笔记面向的是正在用 STM32 做温度采集的嵌入式工程师不管你是做鱼缸温控、智能台灯、物联网网关还是毕业设计只要涉及 NTC这套链路都通用。我会按「硬件怎么接 → ADC 怎么配 → 公式怎么算 → 代码怎么写 → 坑怎么避」的顺序把每一步的参数选择和失败现象讲清楚。读完你应该能独立搭出一套误差可控在 ±0.5℃ 以内的采集方案并且知道误差变大时该往哪里查。2. 硬件链路与 ADC 配置先把信号源搞干净2.1 NTC 分压电路的两个关键电阻NTC 采集的本质是把电阻变化转成电压变化STM32 的 ADC 只能测电压所以分压电路是第一步。典型接法是 VCC — 上拉电阻 R1 — ADC 采样点 — NTC — GND采样点电压为V_adc VCC × R_ntc / (R1 R_ntc)这里 R1 的选型直接决定灵敏度分布。NTC 的阻值随温度指数下降如果 R1 选得离 NTC 在目标温区的阻值太远ADC 码值会挤在一端分辨率浪费。常见做法是让 R1 等于 NTC 在目标温区中点温度下的阻值。比如 10K B3950 的 NTC25℃ 时是 10K如果你主要测 20~40℃R1 取 10K 就能让采样点电压落在 VCC/2 附近ADC 利用最充分。VCC 的选择也有讲究。很多人直接拿 STM32 的 3.3V 供电当 VCC但 3.3V 同时给 MCU 和分压电路供电时MCU 电流波动会让 3.3V 有几十毫伏的抖动直接反映到 ADC 读数上。血泪经验是分压电路的 VCC 要么单独用一颗 LDO要么至少加 100nF10uF 去耦并且走线远离电机、继电器这类负载。2.2 STM32 ADC 采样时间与参考电压设置以 STM32F103 为例ADC 时钟最高 14MHz采样时间可选 1.5/7.5/13.5/28.5/41.5/55.5/71.5/239.5 个周期。NTC 分压点的等效输出阻抗等于 R1 与 R_ntc 的并联值10K 对 10K 并联是 5K这个阻抗不算低。采样时间太短采样保持电容充不满读数会偏低且不稳定。我一般会这样配ADC 时钟 12MHz采样时间选 55.5 周期总转换时间约 (55.512.5)/12MHz ≈ 5.7μs。这个速度对温度采集绰绰有余同时给采样电容足够的充电时间。如果你用的是 STM32F4 或 H7采样时间选项不同但原则一样——源阻抗越大采样时间越长。参考电压方面STM32F103 的 VREF 通常直接接 VDDA3.3V但 3.3V 本身有误差和温漂。如果对精度要求高可以外接一颗 REF3033 之类的基准源给 VREF这样 ADC 的满量程就稳定了。没有外部基准时至少要在代码里用实测的 VDDA 值参与计算而不是硬编码 3.3。// STM32F103 ADC1 初始化通道0PA0采样时间55.5周期 void NTC_ADC_Init(void) { RCC-APB2ENR | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_ADC1EN; RCC-CFGR ~RCC_CFGR_ADCPRE; RCC-CFGR | RCC_CFGR_ADCPRE_DIV6; // PCLK272MHz, ADC时钟12MHz GPIOA-CRL ~GPIO_CRL_MODE0; GPIOA-CRL ~GPIO_CRL_CNF0; // PA0 模拟输入 ADC1-SQR1 0; // 规则序列长度1 ADC1-SQR3 0; // 第一个转换通道 通道0 ADC1-SMPR2 | ADC_SMPR2_SMP0_2 | ADC_SMPR2_SMP0_1 | ADC_SMPR2_SMP0_0; // SMP0[2:0]111 - 239.5周期源阻抗高时更稳 ADC1-CR2 | ADC_CR2_ADON; // 开启ADC for(volatile int i0;i10000;i); // 等待稳定 ADC1-CR2 | ADC_CR2_CAL; // 启动校准 while(ADC1-CR2 ADC_CR2_CAL); }这段代码里SMP0我实际给了 239.5 周期比前面说的 55.5 更保守。原因是很多板子的分压电阻精度一般源阻抗可能比理论值大采样时间拉长能明显改善跳变。代价是转换变慢但温度采集不需要高速。ADC_CR2_CAL校准位必须等它自动清零否则第一次转换结果不可信这个坑后面还会提。2.3 多通道采集时的切换顺序如果你同时采 NTC 和其他模拟量比如电池电压规则序列里通道顺序会影响结果。STM32 的 ADC 只有一个采样保持电路切换通道后第一个转换值往往受上一通道残留影响。常见做法是每个通道连续转换两次丢弃第一次取第二次。或者把 NTC 这种高阻抗通道放在序列最后前面放低阻抗通道让采样电容有更多时间稳定。// 双通道通道0(NTC) 通道1(电池)各转两次取第二次 u16 NTC_ReadFiltered(void) { u16 v1, v2; ADC1-SQR3 1; // 先转通道1 ADC1-CR2 | ADC_CR2_ADON; while(!(ADC1-SR ADC_SR_EOC)); (void)ADC1-DR; // 丢弃 ADC1-SQR3 0; // 再转通道0 ADC1-CR2 | ADC_CR2_ADON; while(!(ADC1-SR ADC_SR_EOC)); v1 ADC1-DR; ADC1-CR2 | ADC_CR2_ADON; // 通道0再转一次 while(!(ADC1-SR ADC_SR_EOC)); v2 ADC1-DR; return v2; // 取第二次 }逻辑说明先转一次通道1把采样电容“预热”到相近电压再转通道0第一次丢弃、第二次保留。参数上SQR3的低 5 位是通道号改通道就改这里。如果你用 HAL 库对应的是HAL_ADC_ConfigChannel和HAL_ADC_PollForConversion思路一样。3. B 值公式与查表法把 ADC 码变成温度3.1 B 值公式的推导与参数含义NTC 的阻值和温度关系用 B 值公式描述1/T 1/T0 (1/B) × ln(R/R0)其中 T 是开尔文温度T0 是标称温度通常 298.15K即 25℃R0 是标称温度下的阻值10KB 是材料常数如 3950。从 ADC 码反推温度要先算 R_ntcR_ntc R1 × ADC_code / (ADC_fullscale - ADC_code)再代入 B 值公式求 T。这个公式在 0~50℃ 范围内误差可以做到 ±0.2℃但前提是 B 值准确。市面上 10K B3950 的 B 值公差常见 ±1%对应约 ±0.5℃ 误差。如果买的是 B 值公差 ±5% 的便宜货算出来差两三度很正常这时候要么换料要么逐颗标定。#include math.h #define R1_VALUE 10000.0f // 上拉电阻 10K #define R0_VALUE 10000.0f // NTC 标称阻值 10K #define B_VALUE 3950.0f // B 常数 #define T0_KELVIN 298.15f // 25℃ 开尔文 #define ADC_FULLSCALE 4095.0f // 12位ADC满量程 float NTC_CodeToTemp(u16 adc_code) { float r_ntc, temp_k; if (adc_code 0 || adc_code 4095) return -100.0f; // 异常值 r_ntc R1_VALUE * (float)adc_code / (ADC_FULLSCALE - (float)adc_code); temp_k 1.0f / (1.0f/T0_KELVIN (1.0f/B_VALUE) * logf(r_ntc / R0_VALUE)); return temp_k - 273.15f; }参数说明R1_VALUE必须和实际焊接的电阻一致用万用表量一下再填别信丝印。B_VALUE和R0_VALUE来自 NTC 规格书不同厂家同型号可能不同。ADC_FULLSCALE是 4095 而不是 4096因为 12 位 ADC 输出范围是 0~4095。logf是单精度对数STM32F1 没有硬件浮点这个函数会调用库里的软件浮点一次约几十微秒1Hz 采集完全够用。3.2 查表法省掉浮点运算的实用方案如果 MCU 资源紧张或者不想在中断里跑浮点查表法是更工程化的选择。做法是预先算好一张 ADC 码到温度的对应表运行时用二分查找或线性插值。表可以按 1℃ 间隔生成覆盖 -20~100℃共 121 个点每个点存 ADC 码u16占 242 字节很省。// 由 B 值公式预生成的 ADC 码表-20℃ 到 100℃步进1℃ const u16 ntc_adc_table[121] { 4020, 4008, 3994, /* ... 中间省略 ... */ 120, 108, 97 }; float NTC_TableLookup(u16 adc_code) { int low 0, high 120, mid; if (adc_code ntc_adc_table[0]) return -20.0f; if (adc_code ntc_adc_table[120]) return 100.0f; while (high - low 1) { mid (low high) / 2; if (adc_code ntc_adc_table[mid]) low mid; else high mid; } // 线性插值 float ratio (float)(ntc_adc_table[low] - adc_code) / (float)(ntc_adc_table[low] - ntc_adc_table[high]); return -20.0f (float)low ratio; }逻辑说明表按温度升序排列ADC 码是降序温度越高阻值越小采样点电压越低。二分查找定位区间后做线性插值精度取决于表密度1℃ 间隔插值误差小于 0.1℃。生成表的方法是用 B 值公式反算每个温度对应的 R_ntc再算 ADC 码用 Python 或 Excel 批量生成即可。注意表要放在const段STM32F1 的 Flash 够放。3.3 两种方法的选型对比对比项B值公式法查表法代码体积小但需链接 math 库表占 242 字节无 math 依赖运算耗时约 50~100μs软件浮点约 5~10μs精度取决于 B 值公差取决于表生成参数可逐颗标定灵活性改参数即改结果换 NTC 需重新生成表适用场景采集频率低、调试阶段高频采集、量产、无浮点单元我一般调试阶段用 B 值公式方便改参数看效果量产固件切查表法省资源也省心。如果 MCU 带 FPU如 STM32F4公式法的耗时可以忽略直接用公式更省事。4. 避坑与排查NTC 采集翻车的五个现场4.1 读数跳变超过 1℃但万用表测电压稳定现象串口打印的温度每隔几秒跳 1~2℃用万用表量分压点电压却很稳。原因ADC 采样时间不够采样保持电容没充满或者 ADC 时钟配置错误实际时钟超过规格导致转换异常。解决把采样时间从 7.5 周期加到 55.5 或 239.5 周期检查RCC_CFGR_ADCPRE分频确保 ADC 时钟不超过 14MHz。改完还跳在采样点对地加 100nF 电容做硬件滤波。4.2 第一次上电读数总是偏高或偏低现象每次复位后第一次读的温度不对第二次开始正常。原因ADC 校准没做或者校准后没等稳定就开始转换。解决初始化里必须执行ADC_CR2_CAL校准并等待其自动清零校准后延时一小段时间再启动转换。用 HAL 库的话HAL_ADCEx_Calibration_Start之后加个几毫秒延时。4.3 换一块板子温度差两度现象同一颗 NTC同一份固件换板子后读数差 1~2℃。原因分压电阻 R1 的实际阻值偏差或者 VDDA 实际不是 3.3V。解决用万用表实测每块板的 R1 和 VDDA把实测值写进代码或做成标定参数。量产时可以在板上留一个精密电阻通道上电自校准。4.4 温度高时误差变大现象低温段挺准超过 60℃ 后误差越来越大。原因B 值公式在宽温区是近似高温段偏差增大或者 NTC 自热效应明显测量电流太大。解决如果只测窄温区用该温区的 B 值重新拟合降低测量电流分压电阻加大到 47K 或 100K减少自热。自热功率 P V²/R10K 分压时约 0.27mW一般没事但小封装 NTC 要注意。4.5 电机一启动温度就乱跳现象板子上有电机或继电器一动作温度读数就飞。原因电源波动和电磁干扰耦合到 ADC 采样点。解决分压电路 VCC 单独 LDO 供电采样点加 RC 滤波1K100nF软件上做中值滤波连续采 5 次去掉最大最小取平均ADC 采样避开电机动作时刻。5. 进阶让 NTC 采集从能用变成可信5.1 两点标定法把误差压到 ±0.3℃B 值公式的误差主要来自 B 值和 R0 的公差。如果愿意花十分钟标定精度可以大幅提升。做法是准备两个温度点冰水混合物0℃和沸水100℃注意海拔修正把 NTC 放进去记录 ADC 码反算出该颗 NTC 的实际 B 值和 R0替换代码里的宏定义。// 两点标定已知 T10℃ 对应 adc1T2100℃ 对应 adc2 // 反算 R0 和 B float r1 R1_VALUE * adc1 / (ADC_FULLSCALE - adc1); float r2 R1_VALUE * adc2 / (ADC_FULLSCALE - adc2); float t1_k 273.15f, t2_k 373.15f; float b_cal (logf(r2) - logf(r1)) / (1.0f/t2_k - 1.0f/t1_k); float r0_cal r1 * expf(b_cal * (1.0f/t1_k - 1.0f/298.15f));标定后把b_cal和r0_cal写回公式同一颗 NTC 在 0~100℃ 内误差通常能到 ±0.3℃。注意沸水温度随气压变化高原地区要按当地沸点修正否则标定本身就有偏差。5.2 用 DMA 加定时器触发做无感采集如果 NTC 采集只是系统里的一小部分不想让它占用 CPU可以用定时器触发 ADC DMA 搬运。配置 TIM3 每 100ms 触发一次 ADCDMA 把结果搬到内存数组主循环直接读数组。这样采集完全在后台跑CPU 该干嘛干嘛。// TIM3 触发 ADC1DMA 搬运到 adc_buffer void NTC_DMA_Init(void) { RCC-AHBENR | RCC_AHBENR_DMA1EN; DMA1_Channel1-CPAR (u32)ADC1-DR; DMA1_Channel1-CMAR (u32)adc_buffer; DMA1_Channel1-CNDTR 8; // 8个采样点 DMA1_Channel1-CCR DMA_CCR_CIRC | DMA_CCR_MINC | DMA_CCR_PSIZE_16BIT; DMA1_Channel1-CCR | DMA_CCR_EN; ADC1-CR2 | ADC_CR2_DMA | ADC_CR2_EXTTRIG; ADC1-CR2 | ADC_CR2_EXTSEL_2 | ADC_CR2_EXTSEL_1; // TIM3 TRGO // TIM3 配置为 100ms 更新TRGO 选更新事件 }逻辑说明DMA 循环模式自动覆盖adc_buffer主循环读数组做平均即可。CNDTR8表示缓冲 8 个点配合 100ms 触发就是 800ms 的数据窗口。参数上EXTSEL要查参考手册确认 TIM3 TRGO 对应的编码不同系列不一样。这个方案的好处是采集节拍由硬件保证不受主循环任务影响温度曲线会平滑很多。5.3 一个我常犯的错误早期做 NTC 采集我总想着一次就把温度算准公式参数反复调结果忽略了硬件本身的一致性。后来才明白NTC 采集的精度上限在硬件——分压电阻精度、基准电压稳定性、PCB 布局这些不解决软件再优化也是白搭。现在我的习惯是先拿万用表把分压点电压和理论值对一遍确认硬件没问题再动代码。这个顺序反过来能省掉大量玄学调试时间。希望帮到你。本文还有配套的精品资源点击获取