ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32 ADC-DMA多通道采样实战:原理、配置与避坑指南

STM32 ADC-DMA多通道采样实战:原理、配置与避坑指南 1. 为什么我坚持用 ADC-DMA 协作采样而不是中断逐次读取做嵌入式电源控制或者数据采集这些年但凡涉及多路电压采样我最怕看到的就是在主循环里用 ADC 查询模式一个通道一个通道去读。不是说不能用而是你一旦把采样这件事和 CPU 的执行节奏绑死后面所有的时序分析、数据一致性、系统实时性都会跟着难受。ADC-DMA 协同工作的核心思路其实特别简单让 ADC 按照固定节拍去转换转换完的结果由 DMA 自动搬运到内存缓冲区CPU 完全不参与中间过程。ADC 只负责把模拟量变成数字量DMA 只负责把数字量搬走CPU 从头到尾只关心数据什么时候准备好了这一个问题。在这个协作模型里ADC 是数据生产者DMA 是搬运工内存缓冲区是仓库CPU 是仓库管理员管理员只需要在货到的时候清点入库不需要亲自去码头搬货。这套方案解决的核心痛点有三个第一CPU 零等待采样过程不阻塞主流程第二多通道数据能够严格按顺序归档不会出现通道间数据错位第三采样频率可以做得很稳定配合定时器触发尤其明显。不管你用的是 STM32、GD32 还是其它 Cortex-M 内核的 MCU这套思路基本通用。适合谁看呢做电机控制、电源管理、电池管理系统、工业采集模块的朋友只要你的系统里有需要连续监测多路模拟量的需求这篇文章都能给你一个可以直接落地的参考方案。2. 从转换时序到搬运机制的完整链路拆解2.1 ADC 采样周期和转换时间的对应关系到底是什么很多刚接触 ADC 的同学会把采样周期和转换时间混为一谈实际上这两个概念是两段不同的时间开销。ADC 的一次完整转换通常分为两个阶段采样阶段和转换阶段。采样阶段是采样保持电容去追踪输入电压的过程这段时间俗称采样周期用 ADC 时钟周期数来衡量比如 1.5 周期、7.5 周期、28.5 周期等。转换阶段是把采样得到的电荷逐次逼近成数字码的过程对 12 位 ADC 来说转换阶段固定是 12.5 个 ADC 时钟周期。总转换时间 采样周期 12.5 个 ADC 时钟周期。举个例子STM32F103 的 ADC 时钟最高 14 MHz如果你配置采样周期是 1.5那么一次转换的总时间就是 (1.5 12.5) / 14 MHz ≈ 1 μs也就是理论上能做到 1 Msps。这里需要特别提醒的是采样周期并不是越小越好。采样保持电容需要时间充电如果输入源阻抗很大而采样时间又很短电容还没充到目标电压就开始转换了那结果必然偏小。我一般建议在输入源阻抗不确定的情况下优先选择 7.5 或 13.5 周期的采样时间稳定性优先于速度。如果你用的是 GD32H7 系列它内置了硬件滤波器硬件过采样可以把多次转换结果做平均后输出从而换取更高的有效位数。这种情况下采样周期依旧影响单次转换速度一定要先算清楚。2.2 DMA 循环模式与 Continuous Requests 的关系DMA 在 ADC 采样场景里最常用的就是循环模式Circular Mode。循环模式的意思是DMA 把 ADC 转换结果按顺序搬运到指定的内存缓冲区当缓冲区写满后自动把地址指针重置回缓冲区起始位置继续搬运新一轮数据。这样一来只要 ADC 持续转换DMA 就会持续搬运CPU 随时可以从缓冲区里读取到最新的一轮数据。在这个基础上STM32 HAL 库的 ADC 配置里有一个选项叫 Continuous Requests。这个选项很多人没搞明白它和 Continuous Conversion 是不一样的。Continuous Conversion 控制 ADC 是否连续启动转换而 Continuous Requests 控制的是 DMA 是否在每次转换结束后持续发起新的传输请求。在实际使用中如果你用 DMA 循环模式搬运 ADC 数据建议把 Continuous Requests 开启否则可能出现数据只搬运了一轮就停住的现象。我用一个生活化的类比来解释整个链路ADC 是一个自动化的量杯每隔固定时间倒出一杯水DMA 是一个传送带把倒出来的每一杯水都运到仓库里。Continuous Requests 就好比传送带始终处于待命状态只要量杯一倒出来就立刻接住。如果你没用 DMA 而是用中断方式那就等于量杯每倒一杯水就要按一次门铃喊你去拿一次两次还行连续高频采集时 CPU 光跑门铃就得累死。3. 硬件电路与 PCB 布局3 个直接决定采样精度的细节3.1 电压采样电路的基本结构和参数计算先讲最常见的电阻分压采样电路。假设你要采集的电压范围是 0~24V而 MCU 的 ADC 输入范围是 0~3.3V那就必须通过电阻分压把电压缩放到 ADC 可接受的范围内。分压电阻的选择有两个关键点分压比和阻值大小。分压比容易理解比如 R1 100kΩR2 15kΩ那么分压比就是 15 / (100 15) ≈ 0.130424V 对应 3.13V留了一点余量给 ADC 参考电压的误差。阻值大小的选择没那么直观但要考虑两点一是功耗阻值越小分压回路的静态电流越大24V 下如果两个电阻都是 1kΩ光分压回路电流就是 12mA这在电池供电的系统里根本不可接受二是输入阻抗匹配阻值太大会导致 ADC 采样时采样电容充电时间不够动态误差变大。所以我一般建议总阻值选在 100kΩ~1MΩ 之间同时尽量在电阻分压节点后加一级电压跟随器。这样采样电路对 ADC 呈现的是低阻抗源采样时间可以选得比较短速度优势能发挥出来。当然如果你对成本敏感不想加运放那就需要把 ADC 采样周期调大比如 28.5 周期以上来弥补高阻抗源带来的充电时间不足问题。3.2 前端 RC 滤波的设计思路ADC 前端加 RC 低通滤波几乎是每个成熟项目都会做的事它既能滤除高频噪声又能起到抗混叠的作用。这里我直接给出一个常用的经验公式截止频率 f_c 1 / (2π × R × C)关键点是 R 的取值要从采样源阻抗里一并考虑。假设你用了电压跟随器输出阻抗很低那 R 取 100ΩC 取 1nF截止频率大约是 1.6 MHz基本不影响正常的信号带宽又能滤掉一部分高频毛刺。如果你没有跟随器采样源阻抗可能达到 10kΩ 以上那 R 就要谨慎选因为 R 和 ADC 内部采样电容还会构成另一个 RC 网络影响采样精度。∑-Δ ADC 的前端 RC 设计更讲究。这类 ADC 内部采样电容会周期性反冲导致前级电路出现纹波如果 RC 参数选得不对会出现明显的线性误差。我实测过一款 16 位 ∑-Δ ADC前端 RC 用 1kΩ 100nF 和用 100Ω 10nF结果是后者反而更差因为 RC 组合的阻抗特性影响了内部调制器的建立时间。这里我的建议是严格参考数据手册里给出的推荐 RC 区间不要自己盲目改大电容大电容不一定带来更好的滤波效果反而可能引入建立时间问题。3.3 规避时钟抖动与电源噪声的 PCB 布局要点PCB 布局是最容易被轻视但后患无穷的环节。ADC 的模拟精度再高前端参考电压有一丝噪声到最后都是白搭。第一个要点ADC 的参考电压VREF去耦电容要尽量靠近 MCU 的 REF 引脚走线要短而粗并且用独立的过孔回到地平面。参考电压是 ADC 转换的尺子尺子本身不稳测出来的东西自然不准。去耦电容我建议用 1μF 100nF 的组合形成宽频段的低阻抗路径。第二个要点模拟地与数字地要单点连接通常是建议在 MCU 下方的地平面处汇接。有些新手把模拟地和数字地彻底分开再用一根细线连接结果地阻抗加大噪声反而更大。我自己的习惯是在 PCB 布局时就规划好ADC 相关的模拟电路和数字电路分区放置地平面不做物理分割而是依靠布局分区来控制回流路径。第三个要点时钟抖动问题。ADC 转换的时钟如果抖动过大会直接表现为采样结果的噪声增加。这里说的时钟抖动包含外部晶振的抖动如果 ADC 时钟来自外部晶振和内部 PLL 引入的抖动。布局上要确保晶振尽量靠近 MCU晶振下方不要走任何数字信号线晶振地线独立打过孔到地平面。另外开关电源的布局也要远离模拟采样区域至少保持 1cm 以上的间距否则开关噪声会通过空间耦合进采样前端。4. 基于 STM32CubeMX 的多通道 ADC-DMA 采集实现4.1 从零开始的 CubeMX 配置流程现在的开发效率已经很高了我一般直接用 STM32CubeMX或者 GD32 的对应图形化工具初始化工程。针对 ADC 多通道 DMA 采集关键配置项我列一下我常用的方案ADC ModeIndependent Mode多通道开启 Scan Conversion ModeNumber Of Conversion根据实际通道数填写比如 4 个通道Conversion Trigger定时器触发推荐或软件触发简单场景Sampling Time7.5 Cycles 或根据前端阻抗调整DMA Continuous RequestsEnabledDMA ModeCircular这里要注意的是CubeMX 生成代码后DMA 配置里有一个 Data Width 的设置。如果你的 ADC 分辨率是 12 位转换结果存在 16 位寄存器里那 DMA 搬运的数据宽度应该选 Half Word。很多问题就出在这里比如 GD32E230 出现 ADC DMA 数据紊乱一排查往往就是数据宽度或者内存地址对齐没配置对。4.2 代码层面的核心逻辑初始化完成后真正的逻辑代码其实很少#define ADC_CHANNEL_NUM 4 uint16_t adc_buf[ADC_CHANNEL_NUM] {0}; HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, ADC_CHANNEL_NUM);就这一行ADC 就会持续转换并搬运数据。HAL_ADC_Start_DMA 这个函数会完成 ADC 启动和 DMA 启动两步操作之后不需要你再干预。读取数据时你只需要访问 adc_buf 数组float voltage0 (float)adc_buf[0] * 3.3f / 4095.0f;每个通道对应的物理电压就是转换值乘以参考电压再除以满量程。有一点要重点提醒DMA 循环模式下CPU 读取 adc_buf 时DMA 可能正在更新同一个位置的数据。如果恰好你读了一半DMA 写入了新数据那这个通道的数据就是前后两帧的混合体。严谨的做法是使用 DMA 传输完成中断在中断回调里拷贝一份数据快照或者使用双缓冲区DMAMUX 支持的部分型号有 Ping-Pong 模式来实现无缝切换。4.3 定时器触发采样的进阶玩法如果你的采样频率需要和 PWM 同步比如电机控制里的电流采样那就强烈推荐你用定时器触发 ADC。具体做法是配置一个定时器的更新事件或者比较事件作为 ADC 的触发源通过 CubeMX 在 ADC Trigger 里选择 Timer X Trigger Out event。这种方式有一个极大的好处采样时刻和 PWM 开关时刻严格同步。比如你在 PWM 中心对齐模式下配置 ADC 采样点在计数周期的中点那采到的电流就是平均电流而不是 PWM 开或者关瞬间的尖峰电流。如果你用软件触发采样时刻会随着主循环的负载波动而抖动最后控制算法的性能就会受影响。5. 我踩过的 AFE 滤波、数据漂移与 DMA 数据紊乱的坑5.1 GD32E230 ADC DMA 数据紊乱问题实录有一段时间我在 GD32E230 上做多通道采样遇到一个非常诡异的现象ADC 数据偶尔出现通道间错位比如本该是通道 1 的数据跑到通道 3 里去了。排查过程我走了不少弯路最后锁定在三个因素第一个是 DMA 的 Memory Address Increment 没有正确配置。ADC 多通道转换后DMA 需要按顺序递增地址写入缓冲区如果地址增量没开那所有通道的数据都会写到同一个位置。第二个是 ADC 的转换顺序和 DMA 缓冲区通道位号的对应关系。规则组通道的转换顺序如果调整了而你的代码还是按固定顺序解析缓冲区数据自然会错位。这个问题最常见的场景是后期维护时在 CubeMX 里新增了一个通道编译烧录后数据全乱了因为缓冲区对应关系变了。第三个是 DMA 请求优先级问题。在多重 DMA 请求并存的系统里如果 ADC-DMA 请求优先级设置过低高优先级的外设可能会抢占导致某些批次的数据延迟更新。解决方案是给 ADC 对应的 DMA 通道分配较高优先级同时打开 DMA 的 FIFO 来吸收突发请求。5.2 C 语言实现 ADC 值滤波函数的实用套路硬件调试得差不多了软件滤波还是得做一层。我这里分享一个实测效果非常好的组合滤波方案中位值平均滤波。原理很简单连续采集 N 个值N 取奇数比如 7 或 11去掉最大值和最小值剩下的取平均。这样既抵抗了随机脉冲干扰又保留了平均值滤波的平滑效果。代码也很短uint16_t median_average_filter(uint16_t *buf, uint8_t len) { uint16_t max 0, min 0xFFFF; uint32_t sum 0; uint8_t i; for (i 0; i len; i) { sum buf[i]; if (buf[i] max) max buf[i]; if (buf[i] min) min buf[i]; } sum - max min; return (uint16_t)(sum / (len - 2)); }需要注意的是这个函数需要传入一个连续的缓冲区所以在 DMA 循环模式下你最好维护一个环形队列每次 DMA 中断来时把新值写进去然后调用滤波函数。5.3 ADC 数据漂移的处理思路数据漂移是模拟采样里最让人头大的问题之一。表现是同样的输入电压早上测是 1.498V下午变成 1.512V或者开机时和运行一段时间后的读数对不上。漂移的原因通常有三个方向。第一个是参考电压温漂这是拿到手先怀疑的对象。解决方法是换用低温漂的参考芯片或者对参考电压做软件校准。第二个是采样电路本身的热噪声电阻的温漂会改变分压比导致同样输入电压下采样值发生变化。这时候需要把分压电阻换成低温漂型号精度等级选 0.1% 或更高的。第三个是 MCU 内部的增益误差随温度变化这种情况可以通过标定曲线的方式修正在出厂前做两点标定或三点标定。这里有个很实用的排查技巧把 ADC 输入端直接短接到参考电压的地看读数是否稳定。如果接地读数都有漂移那问题一定在 MCU 内部控制逻辑或参考源而不是前端电路。6. HAL 库 DMA 发送连续性的处理细节除了 ADC 采集DMA 的另一个高频应用是串口或 SPI 的数据发送。如果你用过 STM32 HAL 库的 HAL_UART_Transmit_DMA一定会遇到一个问题连续两次调用 DMA 发送时第二次调用会失败或者数据发不出去。这个问题的根因是 HAL 库的 DMA 发送状态机没有在上一次传输完成后自动复位。解决思路有两种。第一种是在发送完成回调里手动清除发送状态void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { __HAL_UART_CLEAR_FLAG(huart, UART_FLAG_TC); } }第二种是使用DMA 循环模式 内存缓冲区的方案把要发送的数据提前放入缓冲区DMA 自动循环发送CPU 只需要在需要更新数据时改写缓冲区内容。这种方式特别适合需要周期性发送固定长度数据的场景比如周期性上报采集结果。用这种思路做 ADC-DMA 协同采样时也是一样让采集、搬运、发送尽量各自独立运转CPU 只在关键节点做轻量级处理系统的实时性能会有明显的提升。7. 最后分享一个实用小技巧我自己做多通道电压采样时有一个习惯给每个通道分配一个带符号标记的名义满量程值在调试阶段通过串口周期打印各通道原始值。如果某个通道的数据始终是 0 或者满量程先不要怀疑算法去量一下对应的 MCU 引脚电压。很多时候是杜邦线接触不良或者 PCB 焊点虚焊软件调半天根本是在调空气。另外DMA 缓冲区如果定义成 uint16_t 类型要注意 C 语言里的对齐属性。部分编译器对 ARM 内核要求在访问 16 位数据时地址必须偶对齐如果你的缓冲区定义在结构体里且前面有 uint8_t 成员可能会出现对齐问题。稳妥的做法是直接用__ALIGNED(2)或者单独定义一个全局数组别放在结构体中间。ADC 和 DMA 这套协作机制刚开始接触时会觉得有点绕一旦用熟了你会爱上这种你干你的、我干我的的工作方式。每次看到数据稳定地依次写入缓冲区主循环还能腾出手来处理通信、显示、控制逻辑我就觉得当初花在原理理解上的功夫完全值回票价。
返回列表