ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32 ADC温度采集实战:从NTC热敏电阻到软件滤波的完整实现

STM32 ADC温度采集实战:从NTC热敏电阻到软件滤波的完整实现 1. 项目缘起从模拟信号到数字温度的必经之路在嵌入式开发尤其是基于STM32这类MCU的项目里采集温度是一个再常见不过的需求。无论是环境监测、设备状态感知还是过程控制温度都是一个关键参数。很多新手甚至一些有经验的开发者在拿到一个温度传感器比如经典的DS18B20、DHT11或者更简单的热敏电阻、热电偶后第一个想法往往是去找对应的数字接口或专用协议库。这当然没错但对于那些输出模拟电压的温度传感器如NTC热敏电阻、PT100配合变送器或者MCU内部自带的温度传感器我们绕不开一个核心环节模数转换也就是ADC。我见过不少项目代码里ADC值读取得飞起但最后显示的温度却飘忽不定或者压根不对。问题往往不是出在ADC配置本身而是从原始ADC数值到实际物理量这里是温度的转换链路没有打通。这个链路里包含了参考电压的认知、传感器特性的理解、数学公式的应用以及软件滤波的处理。今天我就以STM32平台和STM32CubeMX工具为例把“使用AD转换获取温度值”这个看似简单的任务从原理到实操从配置到代码彻底拆解清楚。无论你用的是F1、F4还是H7系列这套思路都是相通的。我们不止要“跑起来”更要明白每一个参数背后的意义以及如何让结果更可靠。2. 核心原理拆解ADC读数如何变成温度值在动手配置CubeMX和写代码之前我们必须先搞清楚我们要做什么。这个过程不是一个黑盒而是一个清晰的、可推导的物理到数字的映射链条。2.1 模拟温度传感器的工作机制我们以最常用的NTC负温度系数热敏电阻为例。它的电阻值会随着温度升高而降低。我们通常不会直接测量电阻而是将它接入一个电路将电阻变化转化为电压变化。最常见的是串联一个固定电阻称为上拉电阻或分压电阻到参考电压Vref热敏电阻另一端接地。这样热敏电阻两端的电压即ADC的输入电压Vin就由它的电阻值决定。根据分压公式Vin Vref * (Rntc / (Rntc R_fixed))。其中Rntc是热敏电阻在当前温度下的阻值。可以看到温度变化 → Rntc变化 → Vin变化。这样我们就把温度这个物理量转换成了MCU可以处理的模拟电压信号。2.2 ADC的量化过程从电压到数字STM32内部的ADC模块其核心任务就是将输入引脚上的模拟电压Vin转换成一个数字量我们常说的ADC值或原始数据。这个过程的关键在于ADC的分辨率和参考电压。分辨率比如12位ADC意味着它可以将参考电压范围等分为 2^12 4096 个等级。每个等级对应一个数字码从0到4095或0到4096取决于对齐方式。参考电压Vref这是ADC转换的基准。理想情况下当输入电压 Vin Vref 时ADC输出满量程数字如4095。当 Vin 0V通常连接到Vref-默认为地时ADC输出0。所以ADC转换的基本公式是ADC_Value (Vin / Vref) * (2^分辨率 - 1)。对于12位ADC就是ADC_Value (Vin / Vref) * 4095。注意很多STM32芯片的Vref引脚在内部连接到VDDA模拟电源。这意味着你的VDDA电压的稳定性和精度直接决定了ADC转换的准确度。务必为VDDA提供干净、稳定的电源这是高精度采集的前提。2.3 构建完整的转换公式现在我们把两条链路连接起来温度 - (通过传感器电路) - 电压 Vin。电压 Vin - (通过ADC) - 数字量 ADC_Value。我们的目标是从已知的ADC_Value反推出温度。所以需要逆推首先从ADC值反推电压Vin (ADC_Value / 4095) * Vref。接着从电压Vin反推热敏电阻阻值Rntc。根据上面的分压公式变形Rntc R_fixed * (Vin / (Vref - Vin))。最后从阻值Rntc反推温度T。对于NTC热敏电阻阻值和温度的关系是非线性的通常用Steinhart-Hart方程描述1/T A B*ln(R) C*[ln(R)]^3其中A, B, C是传感器常数。在实际项目中为了简化我们常用查表法在固定温度点测量阻值做成表格中间值进行线性插值或者使用简化公式配合查表进行补偿。对于STM32内部温度传感器过程类似但更直接。芯片内部有一个温度传感二极管其输出电压Vsense与温度成线性关系。STM32的参考手册会给出一个公式通常是Temperature (℃) ((Vsense - V25) / Avg_Slope) 25。其中V25是传感器在25℃时的输出电压Avg_Slope是平均斜率单位mV/℃。而Vsense可以通过ADC测量特定内部通道获得。所以最终公式为Temperature (((ADC_Value * Vref/4095) - V25) / Avg_Slope) 25。V25和Avg_Slope在芯片数据手册的电气特性章节可以查到注意这个值通常是一个典型值有偏差适合监测芯片结温变化趋势而非绝对精确测量。理解了这个数学和物理模型接下来的配置和编程就是按图索骥了。3. STM32CubeMX工程配置详解假设我们使用一个NTC热敏电阻接在PA0引脚和STM32F407VET6。打开CubeMX开始配置。3.1 时钟树Clock Configuration配置ADC的时钟ADCCLK至关重要。它来源于APB2总线时钟PCLK2。STM32F4的ADC最大时钟频率为36MHz。在CubeMX的时钟树配置中你需要确保系统时钟HCLK配置正确。APB2预分频器APB2 Prescaler设置合理使得PCLK2不超过84MHzF407的最大值。ADC预分频器ADC Prescaler需要单独设置确保最终的ADCCLK ≤ 36MHz。通常选择分频系数为4、6或8对应PCLK2为144MHz、216MHz或288MHz时ADCCLK都能满足要求。稳妥起见可以先设为/8。实操心得ADC时钟并非越快越好。较高的时钟可以提高转换速率但可能引入更多的噪声影响精度。对于低速的温度采样比如每秒几次将ADCCLK设置在10-20MHz之间是一个平衡性能和精度的不错选择。3.2 ADC参数配置Parameter Settings在Analog-ADC1下进行配置。Mode选择Independent mode独立模式。温度采样通常不需要双ADC的增强模式。Data Alignment选择Right alignment右对齐。这样读取到的ADCx-DR寄存器值就是一个直接的12位数字0-4095处理起来最直观。Scan Conversion Mode选择Disabled。我们只扫描一个通道温度。Continuous Conversion Mode选择Disabled。我们采用单次转换由软件或定时器触发。如果需要连续采样可以启用但要注意功耗和数据处理。Discontinuous Conversion ModeDisabled。DMA Continuous Requests如果启用了DMA且为连续模式这里才需要使能。我们初期先不用DMA。End Of Conversion Selection选择EOC flag at the end of single conversion单次转换后置位EOC标志。方便我们查询转换完成。在Regular Conversion Mode规则转换模式下点击Add选择你要使用的ADC通道例如IN0对应PA0。Rank秩设为1。Sampling Time采样时间这是非常关键的一个参数它决定了ADC内部采样保持电容对输入信号充电的时间。对于像热敏电阻这样源阻抗较高的信号源需要更长的采样时间以确保电容电压能充分接近外部实际电压。采样时间过短会导致转换结果偏低且不稳定。STM32F4的ADC采样时间可以非常长最高达480个ADC时钟周期。对于温度采样建议设置为480 Cycles以确保充分采样。换算成时间采样时间 采样周期数 / ADCCLK频率。例如ADCCLK21MHz480个周期就是约22.8us足够稳定。3.3 引脚分配与模拟输入在Pinout Configuration的引脚图中找到你使用的引脚如PA0其模式会自动被设置为ADC1_IN0。确保该引脚没有被复用到其他冲突功能上。3.4 生成工程代码在Project Manager选项卡设置好项目名称、路径、IDE如MDK-ARM V5或STM32CubeIDE然后点击GENERATE CODE。第一次生成会询问是否初始化外设选择“是”。4. 关键代码实现与传感器数据处理CubeMX生成的代码搭建了硬件抽象层HAL我们需要在合适的地方添加应用逻辑。4.1 ADC初始化和单次转换在main.c的用户代码区/* USER CODE BEGIN 2 */和/* USER CODE END 2 */之间启动ADC。/* USER CODE BEGIN 2 */ // 启动ADC if (HAL_ADC_Start(hadc1) ! HAL_OK) { Error_Handler(); } /* USER CODE END 2 */然后在需要采样温度的地方比如主循环中或者在一个定时器中断里执行一次转换并读取值。/* USER CODE BEGIN WHILE */ while (1) { uint32_t adc_value 0; float temperature_c 0.0f; // 启动一次转换并等待转换完成超时设为10ms if (HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) { // 读取转换结果 adc_value HAL_ADC_GetValue(hadc1); // 将ADC值转换为温度 temperature_c Convert_ADC_To_Temperature(adc_value); // 这里可以打印、显示或使用温度值 printf(ADC: %lu, Temp: %.2f C\r\n, adc_value, temperature_c); } else { // 处理转换超时或错误 printf(ADC Conversion Error!\r\n); } HAL_Delay(1000); // 每秒采样一次 /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */4.2 核心转换函数实现Convert_ADC_To_Temperature是这个项目的灵魂。我们以NTC热敏电阻10KB值3950与10K固定电阻分压为例演示查表法的实现。首先你需要根据热敏电阻的数据手册建立一个温度-阻值对应表。// 假设我们有一个从-10℃到80℃的稀疏表间隔10℃ typedef struct { int16_t temp; // 温度单位0.1℃扩大10倍存储避免浮点数 uint32_t res; // 对应阻值单位欧姆 } NTC_Table_TypeDef; static const NTC_Table_TypeDef ntc_table[] { {-100, 98520}, // -10.0℃ { 0, 32650}, // 0.0℃ {100, 22380}, // 10.0℃ {200, 10000}, // 20.0℃ (通常25℃是标称值这里是20℃) {250, 6536}, // 25.0℃ {300, 4370}, // 30.0℃ {400, 2670}, // 40.0℃ {500, 1689}, // 50.0℃ {600, 1108}, // 60.0℃ {700, 748}, // 70.0℃ {800, 518}, // 80.0℃ }; #define NTC_TABLE_SIZE (sizeof(ntc_table)/sizeof(ntc_table[0])) // 假设参数 #define VREF 3.3f // ADC参考电压假设VDDA3.3V #define R_FIXED 10000.0f // 固定分压电阻10K #define ADC_MAX 4095.0f // 12位ADC最大值 float Convert_ADC_To_Temperature(uint32_t adc_raw) { float vin, r_ntc; uint16_t i; float temp_result; // 1. ADC值 - 电压 vin (adc_raw / ADC_MAX) * VREF; // 2. 电压 - 热敏电阻阻值 (注意防止除零) if (vin VREF) return -273.15f; // 超量程返回绝对零度表示错误 if (vin 0.0f) return 273.15f; // 电压为0电阻为0温度极高返回一个极大值表示错误 r_ntc R_FIXED * (vin / (VREF - vin)); // 3. 阻值 - 温度 (查表线性插值) // 查找阻值所在的区间 if (r_ntc ntc_table[0].res) { // 阻值高于表格最高值按第一个区间外推温度更低 i 0; } else if (r_ntc ntc_table[NTC_TABLE_SIZE-1].res) { // 阻值低于表格最低值按最后一个区间外推温度更高 i NTC_TABLE_SIZE - 2; } else { // 在表格范围内查找所在区间 for (i 0; i NTC_TABLE_SIZE - 1; i) { if (r_ntc ntc_table[i].res r_ntc ntc_table[i1].res) { break; } } } // 线性插值公式: T T1 (R - R1) * (T2 - T1) / (R2 - R1) // 注意我们的表格是温度递增、阻值递减的 float t1 ntc_table[i].temp / 10.0f; // 转换为浮点摄氏度 float t2 ntc_table[i1].temp / 10.0f; float r1 (float)ntc_table[i].res; float r2 (float)ntc_table[i1].res; temp_result t1 (r_ntc - r1) * (t2 - t1) / (r2 - r1); return temp_result; }这个函数清晰地展示了三步转换过程并使用了查表加线性插值在保证精度的同时避免了复杂的指数对数运算非常适合在资源有限的MCU上运行。避坑指南VREF的取值是精度关键。如果你的板子VDDA不是精确的3.3V或者你想提高精度有两种方法1) 使用STM32内部参考电压VREFINT通道来校准。先读取VREFINT通道的ADC值其对应的电压是已知的芯片出厂校准值存放在系统存储区可以反推出实际的VDDA电压。2) 使用外部高精度基准电压源芯片。对于温度测量方法1通常足够。5. 软件滤波与数据稳定性处理直接从ADC读取的值是包含噪声的尤其是当采样时间设置不足、电源纹波较大或电路板布局不佳时。直接使用单次采样值计算温度会导致显示值跳动。因此软件滤波是必不可少的。5.1 常用滤波算法对比与选择算术平均滤波连续采样N次取平均值。最简单能有效抑制随机噪声但会引入滞后且对脉冲干扰的抑制能力差。#define SAMPLE_TIMES 10 uint32_t adc_sum 0; for(int i0; iSAMPLE_TIMES; i) { HAL_ADC_PollForConversion(hadc1, 1); adc_sum HAL_ADC_GetValue(hadc1); } uint32_t adc_avg adc_sum / SAMPLE_TIMES;中位值平均滤波防脉冲干扰平均滤波法连续采样N次去掉一个最大值和一个最小值然后对剩下的N-2个值求平均。结合了中值滤波和平均滤波的优点对偶然的脉冲干扰有很好的抑制作用。非常推荐用于温度采集。#define SAMPLE_TIMES 12 // 采样12次去掉最大最小剩10个平均 uint32_t adc_buf[SAMPLE_TIMES]; uint32_t adc_sum 0, adc_max 0, adc_min 0xFFFFFFFF; // 采样并找出最大最小值 for(int i0; iSAMPLE_TIMES; i) { HAL_ADC_PollForConversion(hadc1, 1); adc_buf[i] HAL_ADC_GetValue(hadc1); adc_sum adc_buf[i]; if(adc_buf[i] adc_max) adc_max adc_buf[i]; if(adc_buf[i] adc_min) adc_min adc_buf[i]; } // 减去最大最小值后求平均 uint32_t adc_filtered (adc_sum - adc_max - adc_min) / (SAMPLE_TIMES - 2);一阶滞后滤波低通滤波Y(n) α * X(n) (1-α) * Y(n-1)。α是滤波系数0α1。这种方法对周期性干扰有良好抑制作用平滑度高但相位滞后灵敏度低。适合变化缓慢的参数如温度。float alpha 0.2f; // 滤波系数越小越平滑滞后越大 static float filtered_adc 2048.0f; // 初始值 uint32_t adc_raw Get_ADC_Value(); // 获取单次ADC值 filtered_adc alpha * (float)adc_raw (1 - alpha) * filtered_adc; uint32_t adc_to_use (uint32_t)(filtered_adc 0.5f); // 四舍五入我的经验对于温度测量我通常采用“中位值平均滤波”作为硬件采样后的第一道处理它能很好地滤除偶然的毛刺。如果温度值用于控制算法如PID可以再对计算出的温度值进行一次“一阶滞后滤波”让控制量更平滑。5.2 采样策略优化定时触发与DMA在主循环中轮询采样HAL_Delay会阻塞CPU且定时不精确。更好的方式是使用定时器触发ADC配置一个基本定时器如TIM2使其以固定频率如10Hz产生更新事件并将这个事件连接到ADC的触发源。在CubeMX的ADC配置中将External Trigger Conversion Source设置为Timer 2 Trigger Out event。这样ADC就能被硬件定时自动触发无需软件干预精度极高。使用DMA传输数据使能ADC的DMA请求并配置DMA将ADC转换完成的数据自动搬运到内存中的一个数组循环模式或单次模式。结合定时器触发你可以实现一个“采样流水线”定时器定时触发ADCADC转换完成触发DMADMA将数据搬到缓冲区。主程序只需要在需要时比如缓冲区半满或全满时去处理这批数据即可效率极高且几乎没有CPU开销。在CubeMX中配置DMA在ADC1的DMA Settings点击Add选择ADC1模式选择Circular循环模式数据宽度都选Word对应32位但ADC数据是12位实际占用半字。然后在代码中启动带DMA的ADCHAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, BUFFER_SIZE);。6. 精度提升实践与常见问题排查即使按照上述步骤操作你可能还是会发现测量结果有偏差。我们来系统性地排查和提升精度。6.1 系统性误差分析与校准参考电压误差这是最大的误差源。万用表测量VDDA引脚的实际电压替换代码中的VREF宏定义。或者实现VREFINT校准。// 获取内部参考电压通道的ADC值 (通道17 for F4) HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); uint32_t vrefint_raw HAL_ADC_GetValue(hadc1); // 从芯片特定地址读取出厂校准值 (参考数据手册) // uint16_t *vrefint_calib (uint16_t*)0x1FFF7A2A; // F407地址示例 float vdda_actual 3.3f * (*vrefint_calib) / (float)vrefint_raw; // 后续计算使用vdda_actual代替固定的VREF分压电阻误差固定电阻R_fixed也有精度通常是1%或5%。可以用高精度万用表测量其实际阻值更新代码中的R_FIXED。传感器误差NTC热敏电阻本身的B值误差和阻值误差。高精度应用需要单个校准或者在关键温度点如冰水混合物0℃沸水100℃进行两点校准修正查表数据。ADC自身误差包括偏移误差、增益误差、积分非线性等。STM32的ADC支持自校准。在ADC初始化完成后HAL_ADC_Init之后调用HAL_ADCEx_Calibration_Start(hadc1, ADC_SINGLE_ENDED);可以大幅减少这些误差。6.2 典型问题与解决方案问题ADC读数跳动大即使滤波后也不稳定。检查1采样时间。这是最常见的原因。将采样时间调到最大如480 Cycles试试。检查2电源和地。确保模拟部分VDDA、VSSA的电源干净最好通过磁珠或电感与数字电源隔离并搭配足够的去耦电容10uF钽电容0.1uF陶瓷电容靠近引脚。检查3PCB布局。模拟信号线ADC输入线应远离高频数字信号线如时钟、PWM、数据总线。最好在模拟信号线周围铺地屏蔽。检查4信号源阻抗。热敏电阻电路输出阻抗较高ADC输入引脚不能有大的漏电流。避免在ADC输入引脚上接太大的对地电容10pF可能会影响采样。问题测量值整体偏高或偏低呈系统性偏差。检查1参考电压VREF。用万用表实测VDDA。检查2分压计算。双重检查分压公式和代码中的计算顺序特别是浮点数运算的精度。尝试将公式中的所有浮点常数明确加上.f后缀如3.3f。检查3传感器特性表。确认你使用的NTC的B值和标称阻值如25℃下的10K与代码中的查表数据匹配。问题使用内部温度传感器测量芯片温度但数值完全不对或变化很小。检查1通道选择。内部温度传感器连接到特定的ADC通道如STM32F4是ADC1/ADC2/ADC3的通道16。在CubeMX中必须选择Temperature Sensor Channel而不是普通的GPIO通道。检查2采样时间。内部温度传感器需要更长的采样时间数据手册会注明最小采样时间对于F4至少需要10us。确保你的ADC时钟和采样周期设置满足要求。检查3公式参数。V25和Avg_Slope这两个参数必须从你所用的具体型号的数据手册中查找不同系列、甚至不同批次的芯片可能有细微差别。使用错误的参数会导致计算错误。注意内部温度传感器测量的是芯片的结温与环境温度有差异尤其当MCU自身功耗大时结温会显著升高。它主要用于监测芯片是否过热而非精确测量环境温度。通过以上从原理到配置从代码到滤波再到精度分析和问题排查的完整流程你应该能够建立一个稳定、可靠的STM32温度采集系统。记住嵌入式开发中模拟信号处理是“细节决定成败”的典型领域耐心调试和深入理解每一个环节是获得满意结果的关键。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进