ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32F103C8T6 ADC实战:从噪声滤波到多通道DMA采样

STM32F103C8T6 ADC实战:从噪声滤波到多通道DMA采样 1. 从“芯”认识STM32F103C8T6的ADC如果你手头正好有一块“蓝桥杯”开发板或者市面上最常见的STM32F103C8T6最小系统板那么你大概率已经和它的ADC模数转换器打过照面了。这颗经典的“蓝色药丸”芯片以其极高的性价比和丰富的片上资源成为了无数嵌入式开发者的入门首选。而它的ADC模块则是连接数字世界与模拟世界的桥梁无论是读取电位器的电压、检测电池电量还是采集温度、光照等传感器信号都离不开它。STM32F103C8T6内部集成了两个12位的逐次逼近型SARADC我们通常称之为ADC1和ADC2。这里的“12位”是它的分辨率意味着它可以将0到参考电压VREF之间的模拟电压量化为0到40952^12 - 1之间的一个数字值。这个数字值就是我们常说的ADC值或原始采样值。对于大多数应用我们使用芯片的供电电压通常是3.3V作为参考电压那么ADC值4095就对应着3.3V的输入电压。但事情往往没有看起来那么简单。很多新手在第一次使用这个ADC时会惊讶地发现为什么我接了一个稳定的1.65V电压读出来的ADC值却在某个范围内上下跳动而不是一个固定的2048这就是我们常说的“ADC数值抖动”或噪声。这种抖动可能来源于电源纹波、PCB板上的数字信号干扰、ADC内部的热噪声甚至是你的代码采样时机不当。理解并处理好这种抖动是让ADC从“能用”到“好用”的关键一步。所以这篇内容我们不打算只罗列寄存器配置而是想从一个实际使用者的角度拆解STM32F103C8T6的ADC。我们会从最基础的原理和配置入手深入到实际工程中必然会遇到的噪声滤波、多通道采样、DMA传输等核心问题并结合常见的“最小系统板温度计”、“按键检测”等场景分享一些从数据手册里找不到的实操心得和避坑指南。2. ADC基础原理、配置与第一个采样程序在开始写代码之前我们必须先搞清楚ADC是怎么工作的以及STM32F103C8T6的ADC有哪些关键特性。这能帮助我们在后续遇到问题时知道该从哪里入手排查。2.1 逐次逼近型ADC的工作原理与关键参数STM32F103C8T6采用的是逐次逼近型ADC。你可以把它想象成一个非常聪明的“天平”。假设我们有一个0-3.3V的输入电压未知重量和一个精确的12位数字码已知砝码从2048、1024、512...一直到1对应电压值。转换开始时ADC先拿出最大的“砝码”1.65V数字码2048和输入电压比较。如果输入电压更大就保留这个砝码并记录数字码最高位为1反之则丢弃记录为0。接着它再拿出下一个“砝码”上次结果基础上加或减1024对应的电压进行比较如此反复经过12次比较后就能确定一个最接近输入电压的数字码。这个过程决定了SAR ADC的几个重要特性转换时间一次完整的12位转换需要若干个ADC时钟周期。STM32F103C8T6的ADC时钟ADCCLK最高为14MHz一次转换最少需要12.5个周期采样时间转换时间。如果你把采样时间设置得很长总转换时间就会增加。采样保持在比较开始前ADC内部有一个采样保持电路会像拍照一样“捕捉”住此刻输入引脚上的电压并在整个转换期间保持它不变。如果输入信号在转换期间变化太快就会导致采样错误。参考电压VREF这是ADC的“满量程”基准。所有测量都是相对于这个电压的。在STM32F103C8T6上VREF引脚在芯片内部默认连接到了VDDA模拟电源通常也是3.3V。这是一个非常关键的细节如果你的VDDA电源有噪声或纹波那么所有ADC测量值都会跟着一起“抖动”。因此为VDDA和VSSA模拟地提供干净、稳定的电源并在引脚附近放置去耦电容是提高ADC精度的第一步。2.2 使用STM32CubeMX进行基础ADC配置对于新手和快速开发我强烈推荐使用STM32CubeMX进行初始化配置。它能直观地帮你设置时钟树、引脚模式并生成基础代码框架。引脚配置在CubeMX的引脚图中找到你想用作ADC输入的GPIO比如PA0ADC1的通道0。将其功能设置为ADC1_IN0。CubeMX会自动将该引脚配置为模拟输入模式这是必须的否则数字电路会干扰模拟信号。ADC参数配置Resolution选择12Bits。Scan Conversion Mode单通道采样时选择Disable多通道采样时必须启用Enable。Continuous Conversion Mode连续转换模式。如果选择EnableADC会不停地自动开始下一次转换选择Disable则需要代码或触发来启动每次转换。初学者可以先选Disable。Discontinuous Conversion Mode通常Disable。DMA Continuous Requests如果使用DMA这里要Enable。End Of Conversion Selection通常选EOC flag at the end of single conversion。Clock Prescaler在Clock Configuration标签页确保ADCCLK不超过14MHz。通常系统时钟72MHz分频选择PCLK2 divided by 6得到12MHz的ADCCLK是安全且常用的。Sampling Time采样时间。这个值越大ADC内部的采样电容充电越充分对抗信号源高输出阻抗的能力越强但转换速度越慢。对于大多数传感器如电位器、温度传感器239.5 Cycles是一个比较稳妥的起点。如果你的信号变化很快可以适当减小。注意CubeMX生成的代码会调用HAL_ADC_MspInit来初始化GPIO和时钟并在main函数中调用MX_ADC1_Init。你需要在自己的代码中调用HAL_ADC_Start和HAL_ADC_PollForConversion来启动和等待转换。2.3 第一个单次采样程序与原始数据处理生成了代码后我们来写一个最简单的单次采样函数uint32_t read_adc_value(ADC_HandleTypeDef* hadc) { uint32_t adc_raw 0; // 启动ADC转换 if (HAL_ADC_Start(hadc) ! HAL_OK) { Error_Handler(); } // 等待转换完成超时时间设为10ms if (HAL_ADC_PollForConversion(hadc, 10) HAL_OK) { // 读取转换结果 adc_raw HAL_ADC_GetValue(hadc); } // 停止ADC对于单次模式转换完成即停止 HAL_ADC_Stop(hadc); return adc_raw; }拿到adc_raw0-4095后我们需要把它转换成实际的电压值float convert_to_voltage(uint32_t adc_raw, float vref) { // vref 是你的参考电压通常是3.3V return ((float)adc_raw / 4095.0f) * vref; }现在在main函数的循环里调用read_adc_value并把结果通过串口打印出来。接上一个可调电位器中间脚接ADC输入两边分别接3.3V和GND旋转电位器你应该能在串口助手上看到变化的电压值。恭喜你的ADC已经跑起来了但很快你就会发现即使电位器不动打印出来的值也在小范围波动。这就是我们开头提到的“抖动”。要获得稳定的读数我们必须引入滤波算法。3. 对抗噪声ADC值滤波的实战策略与函数实现ADC值抖动是不可避免的我们的目标不是消除它而是通过软件算法来平滑它得到一个能代表真实信号趋势的稳定值。下面介绍几种在嵌入式系统中最常用且有效的滤波方法。3.1 均值滤波简单粗暴的入门之选思路很简单连续采样N次求和后取平均值。#define SAMPLE_TIMES 10 uint32_t adc_average_filter(ADC_HandleTypeDef* hadc) { uint32_t sum 0; for(int i 0; i SAMPLE_TIMES; i) { sum read_adc_value(hadc); // 可以加一个小延时避免采样过快信号源来不及响应 HAL_Delay(1); } return sum / SAMPLE_TIMES; }优点实现简单对周期性干扰有较好的抑制。缺点速度慢SAMPLE_TIMES越大得到一次有效数据的时间越长。对脉冲性干扰偶尔出现的尖峰的抑制能力差一个坏点会污染整个平均值。占用内存存储多个样本虽然这里没存但更高级的滑动平均需要。实操心得SAMPLE_TIMES取8~32是比较常见的范围。对于变化缓慢的信号如温度可以取大一些对于需要快速响应的信号则要取小。HAL_Delay(1)的加入是为了让信号源尤其是那些输出阻抗较高的传感器有足够的时间稳定但会进一步降低速度。在实际项目中我通常会用定时器来触发ADC采样而不是在循环里用Delay。3.2 中值滤波对抗“毛刺”的利器思路连续采样N次N为奇数将这N个值排序取中间的那个值作为输出。#define MEDIAN_FILTER_SIZE 5 // 必须是奇数 int cmp(const void *a, const void *b) { return (*(uint32_t*)a - *(uint32_t*)b); } uint32_t adc_median_filter(ADC_HandleTypeDef* hadc) { uint32_t samples[MEDIAN_FILTER_SIZE]; for(int i 0; i MEDIAN_FILTER_SIZE; i) { samples[i] read_adc_value(hadc); HAL_Delay(1); } // 使用标准库的qsort进行排序注意会占用较多栈空间和CPU时间 qsort(samples, MEDIAN_FILTER_SIZE, sizeof(uint32_t), cmp); // 返回中值 return samples[MEDIAN_FILTER_SIZE / 2]; }优点能有效滤除偶然出现的脉冲干扰比如开关继电器造成的瞬间毛刺。缺点需要排序计算量比均值滤波大对缓变信号中的小幅度波动滤波效果不如均值滤波。实操心得在资源紧张的C8T6上如果采样频率不高比如每秒几次用qsort问题不大。但如果需要高频滤波自己写一个针对固定长度比如5个元素的排序网络会更高效。中值滤波非常适合用在有电机、继电器等大功率设备启停的系统中作为ADC数据的“第一道防线”。3.3 一阶滞后滤波低通滤波平衡响应与平滑的经典方法这是一种递归滤波本次输出是上次输出和本次输入的加权平均。它模拟了RC低通滤波器的效果。#define ALPHA 0.2f // 滤波系数范围(0,1)。越小滤波越强响应越慢。 uint32_t adc_lowpass_filter(ADC_HandleTypeDef* hadc, uint32_t prev_output) { uint32_t new_sample read_adc_value(hadc); // 公式Y(n) α * X(n) (1-α) * Y(n-1) float result_f ALPHA * (float)new_sample (1.0f - ALPHA) * (float)prev_output; return (uint32_t)(result_f 0.5f); // 四舍五入 }优点计算量极小只涉及一次乘法和一次加法。不需要存储历史数据队列节省RAM。通过调整ALPHA可以在“平滑度”和“响应速度”之间灵活权衡。ALPHA接近1响应快但平滑效果差接近0平滑效果好但响应迟钝。缺点存在相位滞后。对于快速变化的信号滤波后的输出会“跟不上”真实信号的变化。实操心得这是我在大多数需要平滑ADC读数的项目中的首选。ALPHA的取值需要根据信号变化频率和采样频率来调试。一个经验是ALPHA 采样周期 / (滤波时间常数 采样周期)。例如你每秒采样100次周期10ms希望滤除频率高于5Hz的噪声时间常数约200ms那么ALPHA可以取 10 / (200 10) ≈ 0.048。在实际中我通常从0.1开始试观察波形再微调。3.4 复合滤波策略组合拳应对复杂场景在实际项目中单一的滤波算法往往不够用。我常用的一个组合是中值滤波 一阶滞后滤波。第一级硬件/软件防脉冲用中值滤波窗口大小为3或5滤除偶尔出现的极端毛刺。这能保护后续的滤波算法不被坏点污染。第二级平滑对中值滤波后的结果再进行一阶滞后滤波平滑掉高频的随机噪声。这种组合既能抵抗突发干扰又能提供平滑的输出计算复杂度也可接受。代码实现就是先调用中值滤波函数再把它的返回值作为一阶滞后滤波的输入。重要提示所有滤波算法都会引入延迟。在设计系统时尤其是闭环控制系统比如PID温控必须考虑这个延迟对系统稳定性的影响。滤波不是越强越好而是在“噪声抑制”和“响应速度”之间找到适合你应用场景的最佳平衡点。4. 进阶应用多通道、DMA与定时器触发采样当你的项目需要采集多个传感器信号或者需要以固定频率高速采样时轮询单次采样的方式就显得力不从心了。这时必须用到STM32 ADC更强大的功能。4.1 多通道扫描与DMA传输假设你需要循环采集PA0通道0、PA1通道1、PA2通道2三个点的电压。CubeMX配置关键点在ADC1的Parameter Settings中将Number Of Conversion设置为3。在Rank列表中分别设置Channel 0,Channel 1,Channel 2并为每个通道设置合适的Sampling Time。将Scan Conversion Mode设置为Enabled。将Continuous Conversion Mode设置为Enabled让ADC自动循环转换。在DMA Settings标签页点击Add选择ADC1模式选择Circular循环模式。这样DMA会周而复始地把ADC数据搬运到指定内存。代码实现要点#define ADC_BUFFER_SIZE 3 uint32_t adc_buffer[ADC_BUFFER_SIZE]; // DMA目标缓冲区 int main(void) { // ... CubeMX生成的初始化代码 ... HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE); // 现在adc_buffer[0], [1], [2] 会自动被更新为通道0,1,2的转换结果 while (1) { // 你的主循环不需要再主动采样直接使用adc_buffer中的数据即可 float vol0 convert_to_voltage(adc_buffer[0], 3.3f); float vol1 convert_to_voltage(adc_buffer[1], 3.3f); float vol2 convert_to_voltage(adc_buffer[2], 3.3f); // ... 处理数据 ... HAL_Delay(100); } }优势CPU被完全解放只需要在需要时读取adc_buffer即可。采样速率由ADC本身的转换时间决定可以达到很高的效率。避坑指南缓冲区对齐确保adc_buffer数组在内存中的地址是4字节对齐的通常全局数组会自动对齐否则DMA传输可能会出错。数据覆盖在Circular模式下DMA会不停地用新数据覆盖旧数据。如果你的数据处理速度跟不上采样速度可能会读到“半新半旧”的一组数据。对于严格要求数据同步的应用可以考虑使用Normal模式并在DMA传输完成中断中处理数据并重新启动DMA。采样时间多通道时每个通道的采样时间是独立的。如果某个通道的信号源输出阻抗很高比如某些热敏电阻电路需要给这个通道单独设置更长的采样时间。4.2 定时器触发采样实现精确的采样率有些应用要求ADC必须以固定的、精确的时间间隔进行采样例如音频采集、振动分析。这时就需要用定时器来触发ADC。配置流程配置一个定时器比如TIM2。将其设置为Update Event触发。计算定时器频率使其溢出频率等于你想要的采样率。例如系统时钟72MHz预分频PSC71自动重载值ARR999则定时器频率为 72MHz / (711) / (9991) 1000Hz即每秒触发1000次。配置ADC在ADC1的Parameter Settings中找到External Trigger Conversion Source选择Timer 2 Trigger Out event。将Continuous Conversion Mode设为Disabled因为我们将由外部定时器来触发每次转换。配置DMA和之前一样配置DMA为Circular模式。代码实现// 启动定时器它会周期性地产生触发信号 HAL_TIM_Base_Start(htim2); // 启动ADC并等待外部触发 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE);这样ADC就会严格按照TIM2设定的节奏进行采样并由DMA搬运数据。采样率完全由定时器决定非常精确。一个常见的误解有人以为用HAL_Delay在循环里采样就能控制采样率。实际上HAL_Delay的精度很差且受中断影响大根本无法保证精确的间隔。对于任何对时序有要求的采样都必须使用定时器触发。5. 工程实践从最小系统板温度计看完整设计流程让我们用一个具体的、常见的项目——“基于STM32F103C8T6最小系统板和NTC热敏电阻的数字温度计”——来串联前面讲到的所有知识点。这个项目要求能测量环境温度并通过串口输出最好还能将历史数据存储到EEPROM或Flash中对应热词中的“能存储数据”。5.1 硬件电路设计与注意事项我们使用一个10K的NTC热敏电阻负温度系数温度升高电阻减小和一个10K的精密电阻串联接在3.3V和GND之间。NTC和10K电阻的连接点接到PA0ADC1_IN0。这是一个经典的分压电路。计算与选型ADC测的是中间点的电压V_adc 3.3V * (R_fixed / (R_ntc R_fixed))。其中R_fixed是10K精密电阻R_ntc是热敏电阻的阻值。需要查阅你购买的NTC热敏电阻的数据手册找到它的B值例如B3950和25°C时的标称阻值例如R2510K。有了这些参数才能将ADC值换算成温度。PCB布局与走线要点模拟部分隔离尽量让分压电路靠近MCU的ADC引脚。走线要短避免穿过数字信号区域如时钟线、数据总线。电源去耦在VDDA和VSSA引脚附近一定要放置一个10uF的钽电容和一个100nF的陶瓷电容用于滤除低频和高频噪声。这是改善ADC精度的最重要的硬件措施。信号滤波在ADC输入引脚到分压电路之间可以串联一个100欧姆的电阻并在地之间接一个100nF的电容形成一个简单的RC低通滤波器可以滤除一部分高频干扰。5.2 软件架构与代码实现// 1. 定义数据结构 typedef struct { float temperature; // 滤波后的温度值 uint32_t raw_adc; // 原始ADC值 uint32_t adc_buffer[10]; // 用于滤波的缓冲区 uint8_t buf_index; } temp_sensor_t; temp_sensor_t sensor; // 2. ADC初始化与DMA配置使用CubeMX生成如前所述 // 假设我们使用ADC1通道0DMA循环模式。 // 3. 温度计算函数基于Steinhart-Hart方程简化版 float calculate_temperature(uint32_t adc_value, float vref, float r_fixed, float r25, float b_value) { float v_adc ((float)adc_value / 4095.0f) * vref; float r_ntc r_fixed * (vref / v_adc - 1.0f); // 计算NTC当前阻值 // 使用B值公式: 1/T 1/T0 (1/B) * ln(R/R0) // T0 25 273.15 298.15K, R0 r25 float t_kelvin 1.0f / (1.0f/298.15f (1.0f/b_value) * logf(r_ntc / r25)); float t_celsius t_kelvin - 273.15f; return t_celsius; } // 4. 复合滤波函数中值一阶滞后 uint32_t filter_adc_value(uint32_t new_sample) { // 简单移动中值滤波窗口为3 sensor.adc_buffer[sensor.buf_index] new_sample; sensor.buf_index (sensor.buf_index 1) % 3; uint32_t sorted[3]; memcpy(sorted, sensor.adc_buffer, sizeof(sorted)); // 冒泡排序找中值数据量小效率可接受 if(sorted[0] sorted[1]) {swap(sorted[0], sorted[1]);} if(sorted[1] sorted[2]) {swap(sorted[1], sorted[2]);} if(sorted[0] sorted[1]) {swap(sorted[0], sorted[1]);} uint32_t median sorted[1]; // 一阶滞后滤波 #define ALPHA 0.1f sensor.raw_adc (uint32_t)(ALPHA * (float)median (1.0f - ALPHA) * (float)sensor.raw_adc 0.5f); return sensor.raw_adc; } // 5. 主循环中的处理 int main(void) { // ... 初始化 ... HAL_ADC_Start_DMA(hadc1, (uint32_t*)dma_adc_raw, 1); // DMA只搬运一个通道数据到变量dma_adc_raw while (1) { // 每隔500ms处理一次 HAL_Delay(500); // 获取经过DMA更新的原始值并滤波 uint32_t filtered_adc filter_adc_value(dma_adc_raw); // 计算温度 sensor.temperature calculate_temperature(filtered_adc, 3.3f, 10000.0f, 10000.0f, 3950.0f); // 通过串口输出 printf(ADC: %lu, Temp: %.2f C\r\n, filtered_adc, sensor.temperature); // 可以添加条件比如每分钟存储一次数据到Flash // store_data_to_flash(sensor.temperature); } }5.3 数据存储与校准思考关于热词中提到的“能存储数据”你可以将定期采集的温度值加上时间戳存储到芯片内部的Flash模拟EEPROM中。STM32F103C8T6的Flash容量是64KB注意避开程序存储区域并妥善处理擦写寿命问题通常Flash每个扇区可擦写1万次。校准这个温度计的精度取决于多个因素3.3V参考电压的精度、10K固定电阻的精度、NTC的B值精度以及ADC本身的精度。对于要求不高的场合可以和一个可靠的温度计对比计算出一个软件校准的偏移量。对于精度要求高的场合需要在硬件上使用更精密的基准电压源如TL431和电阻并在软件中采用多点查表法或更复杂的拟合公式。6. 深度排错当ADC值不稳定时你的检查清单即使按照上面的步骤操作你可能还是会遇到ADC读数不稳、不准的问题。别慌按照下面的清单系统性排查99%的问题都能找到根源。6.1 硬件层面排查电源质量是第一位用示波器测量VDDA引脚或3.3V电源的波形。如果看到有明显的纹波大于几十毫伏那ADC抖动的主要元凶就是它。检查电源电路确保LDO或DCDC的输出电容容量和ESR符合要求在MCU的电源入口增加一个磁珠和多个不同容值的去耦电容如10uF 0.1uF 0.01uF。模拟地与数字地确保VSSA模拟地和VSS数字地在芯片附近通过一个0欧姆电阻或磁珠单点连接。PCB布局上模拟部分的地回路要尽量小不要被数字大电流的地回路包围。信号源阻抗ADC输入引脚内部有采样电容在采样期间需要对其充电。如果信号源比如你的分压电路输出阻抗太高就会导致充电缓慢采样电压不准确。NTC热敏电阻的阻值本身就在变化在低温时可能高达几十K欧姆。解决方案一是增加ADC的采样时间Sampling Time给电容足够的充电时间二是在ADC输入端并联一个100pF~1nF的小电容到地作为电荷池但注意这会影响信号带宽。外部干扰检查ADC走线是否靠近晶振、开关电源、电机驱动线等噪声源。如果无法远离可以考虑用屏蔽线或在地平面进行隔离。6.2 软件与配置层面排查采样时间是否足够这是最常见的原因之一。对于高阻抗信号源将采样时间设置为最大值239.5 Cycles试试。用公式估算充电时间常数 τ R_source * C_sample。STM32的采样电容C_sample大约为几pF。为了达到12位精度通常需要充电时间达到9-10个τ。如果R_source是10KC_sample是5pFτ50ns。那么至少需要450-500ns的采样时间。在12MHz的ADCCLK下一个周期是83ns239.5 Cycles就是近20us远远足够。ADC时钟是否超频确认ADCCLK没有超过14MHz对于STM32F103。过高的时钟会导致转换错误。是否开启了其他高优先级中断如果在ADC转换期间发生了高优先级中断并执行了较长时间可能会干扰ADC的时序导致转换错误。可以尝试暂时禁用所有中断进行测试。滤波算法是否得当确认你使用的滤波算法和参数是否适合你的信号特性。用串口将原始ADC值打印出来在电脑上用绘图工具查看波形能直观地判断噪声类型和滤波效果。DMA或中断冲突在多通道DMA传输中确保缓冲区大小和Number Of Conversion设置匹配并且没有其他DMA通道或中断函数错误地修改了相关内存或寄存器。6.3 一个容易被忽略的细节未使用的ADC引脚处理STM32F103C8T6有多个ADC通道如果你只用了其中几个那些未使用的、被配置为模拟输入的引脚最好在硬件上接地或接一个固定的电压如通过电阻上拉到VDD或下拉到GND。让它们悬空很容易拾取到噪声这些噪声可能会通过芯片内部耦合到正在使用的ADC通道上影响测量结果。排查ADC问题是一个需要耐心和系统性的过程。从电源和地开始用示波器观察再用软件调整参数逐步缩小范围。当你最终获得一条平滑稳定的ADC曲线时那种成就感是无可替代的。STM32F103C8T6的ADC模块虽然不算顶级但只要理解其原理并妥善处理完全能满足绝大多数工业和消费电子项目的需求。
返回列表