ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32 ADC从原理到实战:多通道、DMA与滤波算法全解析

2026/8/29 11:31:19 拓冰建站 浏览量
STM32 ADC从原理到实战:多通道、DMA与滤波算法全解析 1. 项目概述为什么ADC是嵌入式开发的“感官”核心玩过STM32的朋友都知道光会点个灯、调个串口那只是入门。想让你的单片机真正“感知”世界ADC模数转换器是绕不开的一道坎。无论是测量电池电压、读取温度传感器还是处理麦克风的声音信号都需要把外部连续变化的模拟量转换成单片机内部能处理的数字量。这个“翻译官”的角色就是ADC来担任的。我刚开始接触ADC时觉得它不就是个配置寄存器、读个数据嘛。但真上手做项目才发现坑一个接一个采样值跳得跟心电图似的、转换时间算不准、多通道切换手忙脚乱……这些问题不解决你的智能台灯可能亮度忽明忽暗你的两轮小车可能因为电池电压检测不准而突然“趴窝”。这篇笔记我就结合自己踩过的那些坑把STM32的ADC从原理到实战掰开揉碎了讲清楚。目标很明确让你看完就能用起来并且知道为什么这么用遇到问题也知道去哪儿找答案。无论你是刚学完江协科技或江科大STM32教程的新手还是正在做智能小车、环境监测等项目的朋友这篇关于ADC使用的深度解析应该都能给你带来些实实在在的帮助。2. ADC核心原理与STM32中的实现机制2.1 模数转换的基本流程从连续到离散要玩转ADC不能只当个调库侠得先明白它肚子里那点“墨水”。ADC的工作可以想象成给一条连续波动的曲线拍照。这条曲线就是模拟信号比如一个从0V到3.3V变化的电压。拍照的过程就是采样。但照片是离散的由一个个像素点组成ADC输出的数字量也是一样是一个个离散的数值。这个过程主要分三步采样、保持、量化编码。采样在一个极短的瞬间ADC内部的采样开关闭合捕获此刻输入引脚上的电压值。这就好比相机快门的“咔嚓”一声。保持采样开关断开捕获到的电压值被存入一个叫“保持电容”的小元件里在接下来的转换时间内保持不变为后续电路提供一个稳定的输入。这是为了防止在转换过程中输入电压又变化了导致“照片”拍糊了。量化与编码这是核心。ADC有一个参考电压Vref通常是3.3V它定义了输入电压的最大范围。ADC的位数比如12位决定了它能把Vref分成多少份。对于12位ADC总份数是 2^12 4096份。每一份对应的电压值就是Vref / 4096。然后ADC将保持电容上的电压与这一份份的电压进行比较找到它属于哪一份最后将这个份数的序号0到4095以二进制形式输出这就是我们读到的ADC值。注意这里有个关键点ADC的测量是相对于它的参考电压的。如果你的Vref接的是3.3V那么ADC值4095就代表3.3V。但如果你的Vref不稳定或者被噪声干扰那么所有测量结果都会失准。所以在精度要求高的场合一定要给Vref引脚接一个干净、稳定的基准电压源而不是直接连到MCU的供电电源上。2.2 STM32 ADC的架构特色与关键参数STM32的ADC模块功能相当强大不同系列F1/F4/H7等有细微差别但核心思想相通。我们以最常见的STM32F1和F4系列为例。1. 分辨率与量程最常见的是12位分辨率输出0-4095。这意味着理论上的电压分辨率为 3.3V / 4096 ≈ 0.8mV。但注意这是理想值实际有效位数ENOB会受到噪声影响而降低。有些型号支持6/8/10位分辨率可调降低分辨率可以换取更快的转换速度。2. 转换时间与时钟转换一次需要时间公式是总转换时间 采样时间 12.5个ADC时钟周期。采样时间你可以配置比如STM32F1可以是1.5到239.5个周期时间越长ADC对输入信号“看”得越清楚采样越准确抗噪声能力越强但速度越慢。12.5个周期是固定用于量化和编码的时间。 所以ADC的输入时钟ADCCLK频率直接决定了转换速度。ADCCLK由APB2时钟分频而来切记不能超过芯片手册规定的最大值通常F1是14MHzF4是36MHz。如果你需要高速采样比如音频信号就需要在速度和精度间做权衡选用更高的ADCCLK和更短的采样时间。3. 转换模式单次转换触发一次转换一个通道然后停止。适合不频繁的测量。连续转换开启后ADC会不停地对一个通道进行转换转换完立刻开始下一次。扫描模式这是ADC的“多任务”模式。你可以配置一个通道序列比如通道1, 2, 3, 5ADC会按顺序自动转换这个序列里的所有通道。非常适合需要同时监控多个传感器的情况。4. 触发源ADC不会自己无缘无故开始工作需要被“触发”。触发源可以是软件触发在代码里写一句HAL_ADC_Start(hadc1)。定时器触发这是实现精准、周期性采样的关键你可以配置一个定时器如TIM2产生更新事件UEV用这个事件去触发ADC。这样采样间隔就由定时器的周期精确控制不受主程序循环快慢的影响。在做数字信号处理如音频采集、振动分析时这是必须掌握的技能。5. DMA传输当你在扫描模式下转换多个通道或者用连续转换模式高速采样时如果每个数据都产生中断让CPU来读CPU会被频繁打断效率极低。这时就需要DMA直接存储器访问出场了。你可以配置DMA让ADC转换完成的数据自动地、安静地搬运到你指定的内存数组里。CPU可以等数组满了比如采了1024个点再去处理这大大解放了CPU是实现高效数据采集的标配。3. 从零开始使用CubeMX配置ADC的完整流程理论懂了我们上手实战。现在STM32开发HAL库配合CubeMX图形化配置是主流能省去大量查寄存器的时间。这里我以STM32F407VET6和ADC1的通道1PA1引脚进行单次转换为例演示完整配置并解释每一个选项的意义。3.1 基础单通道配置软件触发引脚与时钟配置在CubeMX的Pinout Configuration标签页找到ADC1。在IN1下拉框选择PA1根据你的硬件连接选择对应通道和引脚。在左侧System Core-RCC中确保HSE外部高速时钟已开启这是系统高精度运行的基础。在Clock Configuration标签页配置系统时钟树。确保APB2总线时钟PCLK2ADC的时钟源被正确设置比如设为84MHzF4的常见值。然后回到ADC配置页设置ADC Prescaler分频器让ADC Clock不超过36MHz。例如PCLK284MHz分频选择/4得到ADCCLK21MHz这是安全的。ADC参数配置在Parameter Settings标签下Resolution选择12位。这是精度和速度的平衡点。Scan Conversion Mode选择Disable。因为我们只用一个通道不需要扫描。Continuous Conversion Mode选择Disable。我们需要时手动启动单次转换。DMA Continuous Requests暂时Disable。单次不用DMA。End Of Conversion Selection选择EOC flag after each conversion每次转换后产生EOC标志。在Rank规则序列设置里点击Add选择Channel 1。Sampling Time这是关键参数它决定了ADC对输入电压的“观察”时间。时间越长采样电容充电越充分结果越稳定尤其当信号源内阻较大时。对于大多数传感器如电位器、温度芯片Cycles 480是一个比较稳妥的起点在21MHz时钟下约合22.8us。如果你追求速度可以尝试Cycles 84或更小但需要测试稳定性。生成代码与基础读取函数配置好工程名、路径、IDEKeil或IAR等生成代码。在用户代码区编写一个简单的读取函数uint16_t ADC_GetValue(ADC_HandleTypeDef* hadc) { uint16_t adc_value 0; HAL_ADC_Start(hadc); // 启动转换 if (HAL_ADC_PollForConversion(hadc, 10) HAL_OK) // 等待转换完成超时10ms { adc_value HAL_ADC_GetValue(hadc); // 读取转换值 } HAL_ADC_Stop(hadc); // 停止转换 return adc_value; }在主循环中调用voltage ADC_GetValue(hadc1) * 3.3f / 4096.0f;即可得到电压值。实操心得HAL_ADC_PollForConversion是轮询方式会阻塞程序。在简单的单次测量中没问题但在复杂系统里更推荐使用中断方式HAL_ADC_Start_IT在转换完成回调函数HAL_ADC_ConvCpltCallback中读取数据这样不会影响其他任务的执行。3.2 进阶配置多通道扫描与DMA传输当需要读取多个传感器时扫描模式DMA是黄金搭档。假设我们要循环采集通道1PA1和通道2PA2的数据。CubeMX配置升级Scan Conversion Mode必须Enable。Continuous Conversion Mode也Enable这样ADC在DMA帮助下会永不停止地循环转换序列。在Rank里添加两个Rank分别设置Channel 1和Channel 2并设置各自的采样时间。切换到DMA Settings标签页点击Add选择ADC1模式选择Circular循环模式这样DMA会在传输完数组后自动回到开头配合ADC的连续转换实现无缝的数据流。数据宽度选择Word32位因为ADC数据寄存器是32位的但实际有效数据是低16位。也可以选Half Word但要和内存数组类型匹配。代码实现在main.c的全局变量区定义缓存数组uint32_t adc_dma_buf[2]; // 存放两个通道的数据在main函数的初始化部分/* USER CODE BEGIN 2 */启动DMA传输HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_dma_buf, 2);此后adc_dma_buf[0]和adc_dma_buf[1]就会自动被DMA更新为通道1和通道2的最新值。你的主程序可以随时去读取这个数组而无需担心启动、等待转换等操作。踩坑记录这里有个巨坑在扫描模式下ADC的数据寄存器DR里存放的永远是当前规则序列中最后一个转换通道的数据。如果你不用DMA而是在中断里直接读HAL_ADC_GetValue(hadc1)你永远只能读到通道2的值通道1的数据被覆盖了。所以扫描模式必须配合DMA使用DMA会在每个通道转换完成后立即把数据搬走确保每个通道的数据都被保存下来。3.3 高精度定时器触发配置对于需要固定采样率的应用如音频44.1kHz必须用定时器触发。CubeMX配置配置一个定时器比如TIM3。在Parameter Settings中设置Prescaler和Counter Period使得更新事件UEV的频率等于你想要的采样率。例如定时器时钟84MHz预分频器设为839周期设为1999则更新频率 84MHz / (840 * 2000) 50Hz。在ADC1的配置中找到External Trigger Conversion Source选择Timer 3 Trigger Out event。Continuous Conversion Mode此时应Disable。因为我们需要定时器来触发每一次转换。如果你是多通道扫描还需要使能扫描模式和DMA。代码实现初始化完成后你只需要启动定时器和ADC的DMA传输如果是多通道。HAL_TIM_Base_Start(htim3); // 启动定时器 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_dma_buf, BUFFER_SIZE); // 启动ADC DMA定时器会周期性地发出触发信号ADC收到信号后自动完成一次或一个序列的转换并通过DMA存入数组。整个过程完全由硬件同步精度极高软件无需干预。4. 提升ADC性能的实战技巧与滤波算法硬件配置好了读出来的数据却跳来跳去这是新手必经之路。ADC的测量值包含各种噪声必须经过软件处理才能用。4.1 硬件层面的“降噪”基础在写代码滤波之前硬件设计是根本电源去耦在MCU的VDD和VSSGND引脚附近务必放置一个0.1uF和一个10uF的电容且尽量靠近引脚。这是为ADC的模拟部分提供干净、低阻抗的本地电源。独立的模拟地如果板子有模拟部分传感器、运放最好使用独立的模拟地AGND并通过磁珠或0欧电阻在一点与数字地DGND连接避免数字电路的开关噪声通过地线串扰到模拟信号。信号调理对于高内阻或微弱的信号先用一个电压跟随器运放进行缓冲降低输出阻抗再送入ADC。对于高频噪声可以在ADC输入引脚加一个RC低通滤波器如1kΩ 100nF截止频率约1.6kHz能滤除大部分高频干扰。4.2 软件滤波算法大全与C语言实现硬件是基础软件滤波是最后一道防线。下面介绍几种最常用、最有效的算法。1. 均值滤波最简单粗暴连续采样N次取算术平均值。#define SAMPLE_COUNT 10 uint16_t ADC_AverageFilter(ADC_HandleTypeDef* hadc) { uint32_t sum 0; for(int i0; iSAMPLE_COUNT; i) { sum ADC_GetValue(hadc); HAL_Delay(1); // 适当延时避免采样点过于集中 } return (uint16_t)(sum / SAMPLE_COUNT); }优点简单能有效抑制随机噪声。缺点速度慢对突发性干扰尖峰敏感会拉偏平均值。SAMPLE_COUNT越大越平滑但实时性越差。2. 中值滤波取N次采样值排序取中间值。#define MEDIAN_FILTER_SIZE 5 // 最好取奇数 uint16_t ADC_MedianFilter(ADC_HandleTypeDef* hadc) { uint16_t buffer[MEDIAN_FILTER_SIZE]; // 1. 采集数据 for(int i0; iMEDIAN_FILTER_SIZE; i) { buffer[i] ADC_GetValue(hadc); HAL_Delay(1); } // 2. 简单冒泡排序数据量小效率可接受 for(int i0; iMEDIAN_FILTER_SIZE-1; i) { for(int j0; jMEDIAN_FILTER_SIZE-1-i; j) { if(buffer[j] buffer[j1]) { uint16_t temp buffer[j]; buffer[j] buffer[j1]; buffer[j1] temp; } } } // 3. 返回中值 return buffer[MEDIAN_FILTER_SIZE/2]; }优点能完美滤除偶然的、幅度很大的脉冲干扰比如开关继电器引起的毛刺。缺点排序消耗CPU资源实时性较差。适合变化缓慢的信号如温度。3. 一阶滞后滤波低通滤波这是一种递归滤波本次输出结果 α * 本次采样值 (1-α) * 上次输出结果。其中α是滤波系数0α1。float g_adc_filtered_value 0.0f; // 全局变量保存上次滤波结果 #define ALPHA 0.2f // 滤波系数越小越平滑但滞后越严重 float ADC_LowPassFilter(ADC_HandleTypeDef* hadc) { uint16_t raw_value ADC_GetValue(hadc); float voltage raw_value * 3.3f / 4096.0f; // 转换为电压 g_adc_filtered_value ALPHA * voltage (1 - ALPHA) * g_adc_filtered_value; return g_adc_filtered_value; }优点计算量极小只有一个乘法和加法非常适合在中断服务函数或高速循环中运行。能有效平滑信号。缺点会产生相位滞后输出变化比输入慢α越小平滑效果越好但滞后越明显。不适合对实时性要求极高的控制回路。4. 滑动平均滤波队列法这是均值滤波的升级版维护一个固定长度的队列。新数据入队最老数据出队始终对队列内所有数据求平均。#define MOVING_AVG_SIZE 8 uint16_t adc_buffer[MOVING_AVG_SIZE] {0}; uint8_t buffer_index 0; uint32_t buffer_sum 0; uint16_t ADC_MovingAverageFilter(ADC_HandleTypeDef* hadc) { uint16_t new_value ADC_GetValue(hadc); // 减去即将被覆盖的旧值加上新值 buffer_sum buffer_sum - adc_buffer[buffer_index] new_value; adc_buffer[buffer_index] new_value; // 新值入队 buffer_index (buffer_index 1) % MOVING_AVG_SIZE; // 索引循环 return (uint16_t)(buffer_sum / MOVING_AVG_SIZE); }优点实时性好每次计算只需要一次减法、一次加法和一次除法避免了均值滤波的批量求和延时。输出响应速度快。缺点对突发尖峰干扰仍然有一定敏感性但比普通均值滤波好。需要固定内存空间。如何选择追求极简和速度信号本身较干净一阶滞后滤波。要消除偶然的、大幅度的尖峰干扰中值滤波。需要平衡平滑度、实时性和抗干扰能力滑动平均滤波我最常用。对实时性要求不高只需简单平滑均值滤波。终极方案可以组合使用例如“中值滤波”“滑动平均滤波”先用中值去掉毛刺再用滑动平均进一步平滑。5. 调试与问题排查实录ADC调不通或者数据不准别急着怀疑人生按照以下清单一步步排查99%的问题都能解决。5.1 常见问题速查表现象可能原因排查步骤与解决方案读取的ADC值始终为01. ADC或对应GPIO时钟未开启。2. 引脚模式配置错误。3. 转换未启动或未等待完成就读取。4. 硬件连接问题线断了。1. 检查CubeMX中RCC和对应GPIO的时钟使能。2. 确认引脚模式为Analog模拟模式而不是Input或Output。3. 单次转换确保调用了HAL_ADC_Start并成功等待HAL_ADC_PollForConversion。4. 用万用表测量ADC输入引脚电压是否正常。ADC值固定为4095或最大值1. 输入电压超过参考电压Vref。2. 输入引脚浮空未接任何信号内部噪声导致饱和。3. 采样时间太短对于高内阻信号源未充分充电。1. 确保输入信号电压在0-Vref之间。2. 给未使用的ADC引脚接地或接一个固定电压不要悬空。3. 增加Sampling Time采样周期数。ADC值跳动剧烈噪声大1. 电源噪声大去耦电容不足或摆放不当。2. 模拟地与数字地未分开或单点连接不当。3. 信号线受到干扰如靠近电机、时钟线。4. 软件未滤波。1. 检查电源纹波确保去耦电容0.1uF和10uF紧靠MCU电源引脚。2. 检查PCB布局确保地平面完整模拟部分独立。3. 远离干扰源使用屏蔽线或双绞线。4. 应用本章第4节的软件滤波算法。多通道扫描模式只能读到最后一个通道的值未使用DMA而在中断或主循环中直接读取ADC-DR寄存器。必须启用DMA让DMA在每次通道转换完成后自动搬运数据。扫描模式与直接读取寄存器不兼容。定时器触发采样采样率不对1. 定时器时钟或分频计算错误。2. ADC的时钟ADCCLK配置过高或过低导致转换跟不上触发速率。3. DMA传输速度跟不上。1. 重新计算定时器ARR和PSC值确认更新频率。2. 确保总转换时间 x 通道数 触发周期。例如单通道转换需10us则触发周期必须大于10us即采样率低于100kHz。3. 检查DMA优先级确保不被其他高优先级中断长时间阻塞。使用DMA时数组数据不更新1. DMA或ADC未启动。2. DMA缓存数组长度或数据类型配置错误。3. DMA传输完成中断中错误地停止了DMA或ADC。1. 确认正确调用了HAL_ADC_Start_DMA。2. 检查CubeMX中DMA的数据宽度Word/HalfWord与数组类型uint32_t/uint16_t是否匹配。3. 在循环模式下不要在DMA传输完成中断回调函数里做停止操作。5.2 利用调试器与ADC内置功能辅助诊断1. 查看寄存器 在调试模式下Keil/IAR可以查看ADCx-SR状态寄存器和ADCx-DR数据寄存器的值确认转换是否完成EOC位、数据是否更新。2. 使用ADC的看门狗 STM32的ADC内置了模拟看门狗AWD。你可以设置一个电压高低阈值当转换结果超出这个窗口时会产生中断。这在监测电池电压是否过低或过高时非常有用无需软件不断轮询比较。3. 注入通道 除了规则通道序列ADC还有一组“注入通道”。你可以把它理解为高优先级的VIP通道。当注入通道被触发时它会打断当前的规则序列转换先转换注入通道然后再回到规则序列。这在需要紧急采样某个关键信号时如过流保护非常有用。4. 双重/三重交替模式仅限支持多ADC的型号 对于F4/H7等系列有多个ADC内核如ADC1, ADC2, ADC3。可以配置它们以交替模式工作对一个通道进行“接力”采样从而理论上将采样率翻倍或翻三倍这是实现超高速采样的高级玩法。