ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

ADC与DMA协同实现高效电压采样:原理、配置与实战

ADC与DMA协同实现高效电压采样:原理、配置与实战 1. 为什么电压采样非要把 ADC 和 DMA“绑在一起”干过单片机开发的朋友都有这个体会单纯用 ADC 读个电压并不难难的是在“读得准”和“不拖累主程序”之间找到一个平衡点。很多项目一开始都是查询方式读 ADC也就是 CPU 不停去查转换完成标志或者干脆延时等结果。这套打法在采样频率低、通道数少、系统逻辑简单的场合确实能用但一旦通道多起来、采样率提上去、主循环里还要处理通信或显示任务查询方式的缺陷就藏不住了——CPU 被死死卡在等 ADC 转换这件事上其他活全干不了。能打破这种僵局的思路很直接既然 CPU 等着 ADC 浪费时间那就让数据自己“跑到”内存里等攒够了再通知 CPU 来处理。这就是 ADC-DMA 协同工作的核心价值。DMA 的全称是 Direct Memory Access翻译过来是直接存储器访问。它本质上是单片机内部一个专门负责数据搬运的硬件模块可以在不需要 CPU 干预的情况下把外设数据寄存器里的内容搬到内存。ADC 每完成一次转换DMA 就自动读走一次结果这样一来CPU 只要在初始化时把 DMA 配置好剩下的大量采样过程可以完全不参与。这套方案解决的核心问题有三个一是降低 CPU 占用率MCU 可以把宝贵的算力留给业务逻辑二是采样数据的搬运延迟可控DMA 的响应是硬件级的不会因为中断优先级配置不当或主程序忙不过来而导致丢数据三是配合硬件 FIFO 或循环缓冲区可以实现“边采集边处理”的流水线效果尤其适合需要连续监控电压波形或做平均值滤波的场景。从我接触过的项目经验看ADC-DMA 组合并不是什么高不可攀的黑科技但它能解决的问题非常普遍。比如做电源管理、电池电压检测、传感器采集、电机相电流采样甚至简易示波器逻辑分析仪这类项目核心都是先把电压量采对、采稳、采得不影响主流程。这篇文章就把整个实现链路拆开讲清楚从原理设计、参数计算、寄存器配置、代码实现到硬件布线和数据后处理每一步都配上我实际踩坑后的经验希望能帮你少走几段弯路。2. 先把原理吃透ADC 转换流程与 DMA 搬运机制2.1 ADC 打个喷嚏的时间有多长采样周期怎么算很多新手第一次配置 ADC 时看到“采样时间”“采样周期”“转换时间”这些词会懵。简单说一次完整的 ADC 转换 采样阶段 转换阶段。采样阶段是给内部采样电容充电充电时间决定了对信号源内阻的容忍能力转换阶段是逐次逼近比较的过程。以常见的 12 位逐次逼近型 ADCSAR ADC为例它的总转换时间可以用这个公式估算总转换时间 (采样周期数 固定转换周期数) × ADC 时钟周期不同厂家的固定转换周期数不一样常见值是 12 到 16 个周期。比如某 MCU 的 ADC 配置为 12 位分辨率、采样时间为 10 个周期ADC 时钟为 14 MHz那么固定转换周期 12 个周期总周期数 10 12 22 个周期总转换时间 22 / 14 MHz ≈ 1.57 µs这意味着在理论满速下这个通道的采样率大约能到 636 kSPS。但切记这是单通道、无额外开销的极限值实际项目中如果启用扫描模式多通道连续采样还要把各个通道的采样时间叠加起来算总周期。采样时间并不是越长越好。采样周期短ADC 采样的“窗口期”就短如果信号源内阻高、PCB 走线寄生电容大采样电容还没来得及充满转换结果就会偏小这种现象在低速传感器输出上尤其明显。反之采样周期过长虽然有助于降低源阻抗带来的误差但会牺牲采样率而且在信号快速变化时引入较大的平均效应。经验做法是针对常规分压电路和运放输出这类低阻抗源采样周期设置在 10 到 20 个周期比较稳妥对于直接接高内阻传感器、又没有缓冲器的情况采样周期至少要给到 30 个周期以上。2.2 DMA 是什么为什么它适合给 ADC 当搬运工DMA 控制器的本质是一个硬件状态机它维护着三组关键信息源地址、目的地址、传输长度。配置完成后它会按设定的方向一块一块地搬数据每次搬运完自动把地址指针加一、长度计数器减一直到传输计数归零触发完成中断。这跟 ADC 的配合逻辑特别契合。ADC 在规则通道组转换完成后结果存放在数据寄存器里。如果不启用 DMACPU 要在每次转换结束中断里读走数据然后清标志位。中断频繁触发时CPU 大部分时间都在进出中断上下文切换开销巨大。开启 DMA 后ADC 每转换完一个通道硬件会自动触发一次 DMA 请求DMA 把数据寄存器的内容搬到内存数组全程 CPU 无感知。用生活化类比来说ADC 就像一台不断生产零件的机器CPU 是质检员DMA 是传送带。质检员如果每出一个零件就跑过去捡一次再回工位他什么都干不了装上传送带后零件自动送到仓库质检员只需要每隔一段时间到仓库清点一次即可。这就是 ADC-DMA 协同的直观价值。2.3 协同工作的三种典型模式单次搬运、循环搬运、双缓冲搬运单次搬运模式最简单配置好 DMA 传输长度后启动一次数据搬完就停。适合一次性读取几个通道电压然后进入低功耗状态的场景比如电池电量检测。循环搬运模式最关键的是设置 DMA 的循环模式Circular Mode。这种模式下 DMA 搬完设定的长度后会自动把地址指针重置回起始位置继续搬运不需要 CPU 再次触发。ADC 也设置为连续采样模式这样就形成了一个全自动的“无限采样流水线”。数据回绕写入缓冲区时最老的数据会被最新的覆盖。这种模式配合 DMA 半传输中断很经典缓冲区一半填满时触发中断CPU 处理前半段数据DMA 继续填后半段等后半段填满再次触发时前半段已经处理完了。两半交替使用实时性和连续性都在线。双缓冲模式是循环模式的进阶版DMA 在两个独立内存块之间交替传输。它避免了对同一个缓冲区做读写访问可能带来的数据冲突问题。在一些高端 MCU 上DMA 控制器直接支持这种硬件双缓冲切换。如果 MCU 不支持硬件双缓冲也可以用循环模式配合空闲中断或者手动切换 DMA 目标地址的方式模拟出类似效果。三种模式的选型逻辑并不复杂采样频率低、数据量小就选中止式单次搬运要长期监控就选循环搬运加半传输中断数据吞吐率大、不允许任何数据竞争就优先考虑双缓冲。实际项目中我用的最多的组合其实是“循环搬运 半传输中断 空闲中断”这套组合能覆盖绝大多数电压采样场景。3. 动手配置寄存器级与 HAL 库两条路线的完整实现3.1 从寄存器角度看清 DMA 地址增长逻辑理解地址增长方向是配置 DMA 最容易出错的点之一。DMA 的源地址和目的地址都可以独立配置为固定或递增模式。对于 ADC 多通道采样来说源地址固定ADC 的数据寄存器只有一个固定地址每次转换完的数据都“放在同一个位置”目的地址递增内存数组是连续的数据依次存放如果源地址配成了递增DMA 会从 ADC 数据寄存器的地址往下读读出一堆寄存器里的其他内容数据自然是乱的。GD32E230 上 ADC-DMA 数据紊乱的常见原因之一就是源地址模式配置错误。同理如果目的地址配置成固定模式那么每次新数据都会覆盖数组的第一个元素数组后面全是 0看起来就是“采了一个值就不再更新”。寄存器配置的核心逻辑大致如下以常见 ARM Cortex-M 内核 MCU 为例使能 ADC 和 DMA 控制器时钟配置 DMA 通道源地址指向 ADC 数据寄存器目的地址指向自定义数组传输方向为外设到内存设置传输数据宽度为半字16 位因为 12 位 ADC 结果存在 16 位寄存器中设置地址增长外设地址固定内存地址递增配置循环模式或者正常模式根据实际需求选择使能 DMA 通道然后启动 ADC 转换采样完成后读取内存数组的数据即可这个过程里有几个容易忽略的配置项数据宽度一定要 ADC 数据寄存器宽度一致一般是半字DMA 的优先级要根据系统里其他 DMA 通道的优先级统筹分配启用循环模式时注意缓冲区大小必须是 DMA 传输长度的整数倍否则回绕后数据衔接会错位。3.2 用 STM32CubeMX 图形化配置多通道 DMA 采集流程对于使用 HAL 库的开发者尤其是 STM32 平台使用 CubeMX 做图形化配置是效率最高的路径。先把流程快速过一遍第一步打开 ADC 外设配置。使能需要的通道比如注入电压采样、温度传感器、电池电压检测共三个通道。在 Parameter Settings 里设置扫描模式为 Enabled连续转换模式根据实际需要选择如果要连续监测就启用如果只是周期性读取就关闭改用定时器触发。第二步在 ADC 的 DMA Settings 选项卡里添加 DMA Request选择通道并设置为 Circular 模式。此时要确认内存地址增量是 Enabled外设地址增量是 Disabled。数据宽度两端都选 Half Word。第三步配置定时器触发。需要精确定时采样的场景比如电机控制中的三相电压采样启用定时器更新事件作为 ADC 触发源。选好定时器后调整预分频和自动重载值来得到目标采样频率。比如系统时钟 72 MHz预分频 72自动重载 999那么触发频率就是 72 MHz ÷ 72 ÷ 1000 1 kHz也就是每秒触发 1000 次采样。第四步在初始化代码中确认 MX_ADC1_Init、MX_DMA_Init 和 HAL_ADC_Start_DMA(hadc1, buffer, length) 的调用顺序。HAL_ADC_Start_DMA 启动后DMA 会自动工作数据持续写入 buffer。用 HAL_ADC_ConvCpltCallback 回调函数判断一轮采样结束再配合 HAL_ADC_ConvHalfCpltCallback 实现半缓冲区处理。这里补充一个我踩过的坑CubeMX 生成的 DMA 初始化代码里默认中断优先级可能偏低。如果系统里同时有通信中断和采样 DMA 中断务必在 NVIC 设置里把 DMA 中断优先级调到合适的位置否则高负载下 DMA 完成中断响应不及时可能导致缓冲区的数据被覆盖了才处理造成波形失真或漏数据。3.3 多通道规则扫描 vs 注入通道什么时候用哪个多通道采样时很容易纠结一个问题规则通道组和注入通道组到底用哪个规则通道组是 ADC 转换的“常规通道”数量多适合批量采集。但规则组有个特点如果把多个通道放到规则组里转换结果是依次存放在同一个数据寄存器的每转换完一个就得赶紧读走不然下一个通道的结果就会覆盖上一个。这正是规则组必须配 DMA 的原因——规则组天然适合 DMA 成批搬运。注入通道组则像“优先级更高的插队通道”。当规则组正在扫描时如果发生了注入通道的触发事件ADC 会暂停当前规则转换先执行注入通道转换结束后再回到规则组。注入通道组的数据寄存器是每个通道独立的所以没有覆盖竞争问题。在需要严格同步采样或紧急捕获的场景注入通道很有用。实际项目选型经验是温度、电压、电流这类周期慢变量全部放规则组用 DMA 批量搬运如果有一路信号需要触发瞬间立刻采样比如过流保护触发后第一时间记录电压值就放到注入组。两者可以在同一个 ADC 实例上共存灵活度非常高。针对标题中提到的“ADC-DMA 协同工作”我的建议是优先把规则组和 DMA 用好这是最常见也最稳定的组合。注入通道不是必须的反而会在配置时增加复杂度仅在真正需要打断采样流程时才引入。3.4 硬件滤波不能全靠软件RC 前端与参考电源设计ADC 采集精度出问题很多时候不是软件算得不好而是硬件上给 ADC 输入的信号本身就“脏”。尤其对于逐次逼近型 ADC它内部采样电容的充电动作会在输入端产生电流毛刺。如果前级是一个高阻抗电压源毛刺会导致采样电容充电电压产生偏差进而表现为采样值跳动或漂移。解决思路有三种。第一种是在 ADC 输入引脚与信号源之间串一个电阻限制毛刺电流然后再并联一个对地电容形成一个低通滤波器。这个 RC 滤波器的截止频率要远高于信号频率同时远低于 ADC 采样频率的奈奎斯特频率否则会衰减有用信号。RC 截止频率的计算公式是f_c 1 / (2π × R × C)比如 R 1 kΩC 10 nF那么 f_c 1 / (2π × 1000 × 10⁻⁸) ≈ 15.9 kHz。如果信号本身是 100 Hz 的慢变电压那这个滤波效果非常好如果采样对象是几十 kHz 的开关纹波这个滤波器反而会把纹波信息削弱要根据实际需求权衡。第二种思路是加一个低输出阻抗的缓冲器比如运放跟随器。运放输出阻抗很低可以干脆利落地驱动 ADC 的采样电容消除信号源内阻带来的采样误差。缺点是成本高一点多一个器件功耗也多一些。第三种思路也是很多工程师容易忽略的那就是参考电压源的稳定性。ADC 的转换结果是输入电压与参考电压的比值参考电压只要有一点点波动就会直接按比例映射到采样的数字值里。如果 MCU 集成 ADC 的供电和参考电压共用同一个引脚而这个供电本身带纹波采样值必然会跟着纹波动。最好的做法是用独立的高精度基准源给 VREF 供电或者在 VREF 引脚就近放一个大容量的低 ESR 去耦电容外加一个小磁珠隔离主电源的噪声。3.5 规避时钟抖动与电源噪声3 个 PCB 布局要点“热词”里提到的“adc/dac 电路设计:规避时钟抖动与电源噪声的3个pcb布局要点”这个点对于 ADC 采样项目的最终精度影响极其关键我觉得非常值得拆开细讲**第一点ADC 引脚的去耦电容要近近到不能再近。**VREF 引脚的 0.1 µF 和 1 µF 去耦电容必须放在距离引脚 3 mm 以内而且走线要先过电容再到芯片引脚。我看到过太多反面案例去耦电容放在 PCB 板边缘中间还穿了几根过孔结果去耦效果大打折扣采样值在低噪声环境下依然抖动。**第二点模拟地要独立铺铜避免数字噪声通过地平面耦合。**ADC 的模拟信号参考平面和 MCU 的数字部分最好有清晰的物理隔离。如果不能做完整的分割至少要保证 ADC 采样引脚、去耦电容、RC 滤波器这些模拟元件的“地”是完整连续的不要让数字信号的回流路径穿越模拟区域。**第三点时钟源远离模拟输入走线。**晶振或时钟芯片的输出信号往往有大幅度的快速翻转谐波丰富。如果这些信号走线紧贴着 ADC 输入走线容性耦合会把时钟边沿噪声注入到模拟信号里最终变现为采样值的周期性扰动。实测下来这类干扰在时域波形上表现为采样结果的边缘抖动非常难靠软件滤波消除。布线时把时钟区域和模拟采样区域分开并在中间铺地隔离是最简单有效的办法。4. 数据后处理从原始 ADC 值到真实电压值4.1 ADC 采样值到电压值的换算公式拿到 DMA 缓冲区里的原始数据后第一步就是把它换算成实际电压。这个换算关系非常直接V_real (ADC_Value / 2^N) × V_REF其中 N 是 ADC 分辨率V_REF 是参考电压。比如 12 位 ADC参考电压 3.3 V采样值为 2048那么实际电压为V_real (2048 / 4096) × 3.3 1.65 V有些人会问“如果单片机 ADC 输入口电压为 1 V采样得到的值是多少”答案就套这个公式反推ADC_Value (1 / 3.3) × 4096 ≈ 1241请注意这个换算只适用于 ADC 输入范围在 0 到 V_REF 之间的情况。如果输入带有负压或超过参考电压需要先用运放或分压电阻把信号搬到可采范围内否则会削顶或损坏引脚。4.2 数据漂移的锅硬件和软件都要背ADC 数据漂移是个让人头疼的经典问题。表现为开机一段时间后采样值缓慢变化或者在不同温度下回读同一电压结果不一致。硬件层面漂移通常来自参考电压的温度系数。普通 LDO 输出电压的温漂指标并不高用做 ADC 基准会在温度变化时产生明显误差。解决方法是换一个低温漂的基准源芯片或者至少选择温漂系数被明确标注为低值的线性稳压器。软件层面ADC 的偏移误差和增益误差是可以通过校准来修正的。很多 MCU 的 ADC 内部支持偏移校准和线性校准初始化时先跑一次校准流程把校准结果写回校准寄存器。GD32H7 这类相对高端的器件还会提供硬件滤波功能可以在 ADC 内部直接做一些简单的数据平滑减少 CPU 的滤波负担。经验做法是每次设备上电后先读取一个已知的参考电压比如标准 1.5 V 基准对比实际采样结果和历史标定值计算出一个修正系数之后在软件里对采样数据做实时修正。这种“软校准”方法在实际项目中屡试不爽能把采样精度从“大致能用”提升到“稳定可靠”。4.3 软件滤波平均值、中位值与滑动滤波怎么选原始 ADC 数据即使经过硬件滤波仍然可能带有随机噪声。软件滤波是最后一道防线。常见的几种滤波算法各有适用场景算术平均值滤波最简单的做法把 N 次采样结果相加再除以 N。对高斯白噪声抑制效果最好噪声幅度可以降到原来的 1/√N 倍。适合缓慢变化的直流电压测量比如电池电压、电源轨监测。缺点是响应慢如果需要捕捉快速变化就不好用。滑动平均滤波移动平均用一个先进先出的窗口每采一个新样本窗口抛弃最老样本计算窗口内平均值。它比算数平均更平滑实时性更好适合周期性采样的场景。窗口大小的选择很关键窗口太大滞后明显窗口太小滤波效果差。我常用的窗口长度在 8 到 32 之间。中位值滤波把采样结果排序后取中间值。它对脉冲噪声、毛刺干扰有奇效能完全滤除掉孤立的异常值。缺点是计算量稍大因为要做排序不适合高频采样。实际项目中可以把“中位值 平均值”组合使用先取中位值剔除野值再做平均平滑。写 C 语言滤波函数时最需要注意一个细节用整形计算要防止溢出。多通道累加时如果每通道数据是 16 位累加 32 次就可能超过 16 位范围必须用 32 位变量承接累加结果最后再除以采样次数。这个低级错误我见过很多次写代码时务必要提高警惕。5. 优化技巧与常见问题排查5.1 DMA 数据“首采异常”问题ADC 刚启动时DMA 搬进缓冲区的第一批数据往往是不可靠的。原因在于 ADC 上电后需要一段稳定时间前几次转换可能还没有进入真正的稳定工作状态或者外部信号源还没稳定建立。一个简单有效的处理方式DMA 缓冲区开头多留几个“废弃数据”位置启动后忽略前几个样本正式数据从后面的位置开始解析。还有一个相关的问题是 DMA 循环模式下缓冲区被覆盖。如果 CPU 处理数据的速度跟不上 DMA 写入速度缓冲区还没读完就被新数据覆盖数据就乱了。排查方法很简单DMA 缓冲区头部留一个状态字段或时间戳字段每次 CPU 读数据前比较状态值如果发现已经被 DMA 写入覆盖就说明处理速度不够需要把工作转移到中断里或者直接降低采样率。5.2 串口 DMA 发送不连续与 CAN 中断选择的边界条件“热词”里的“stm32串口hal库使用dma发送数据不能连续发送”也是个典型问题这类问题本质上不止出现在串口而是所有 DMA 外设都可能遇到。根本原因通常是上一次 DMA 传输还没结束又调用了下一次启动函数导致新数据被丢弃或传输位置错乱。解决办法是在启动新的发送之前检查 DMA 传输状态必要时先调用HAL_UART_DMAStop或等待传输完成回调。另外也可以利用 DMA 传输完成中断逐个发送形成一个“队列”机制保证数据一句接一句地连续发。再延伸一下“CAN总线是中断接收还是DMA接收”的取舍问题。CAN 的报文长度和数据帧结构本身就自带同步机制每帧数据量不大中断方式的开销完全可以接受。而 ADC 采样是高频、大量、数据流式的DMA 优势远大于中断方式。反过来如果需要用 SPI 外接高速 ADC 或 DAC数据量大且连续同样建议用 DMA。判断标准很简单数据是突发小包还是高频流式前者中断够用后者必须 DMA。5.3 实测中典型的“数据紊乱”案例复盘有个实测案例值得复盘在某款 MCU 上配置 ADC 三通道 DMA 采集结果缓冲区里的第二、三个通道的数据经常出现大跳变。仔细排查后发现问题是 DMA 的传输长度配置成了缓冲区整个长度而 ADC 的注入通道组也在同时工作注入通道转换结果也会触发 DMA 请求导致送入 DMA 的数据顺序发生变化。解决之法是把注入通道独立处理不参与规则组的 DMA 传输同时调整了 DMA 请求的优先级策略。另外一个更容易忽视的案例初始化代码中先启动了 ADC 连续转换后使能 DMA结果前几次 DMA 请求未被响应数据丢失。正确顺序应该是先配置并启动 DMA再启动 ADC 转换。故意把顺序反过来的话就会看到缓冲区前几个数据时好时坏之后才恢复正常看起来像“随机故障”一样。6. 扩展思考ADC-DMA 方案还能怎么延伸ADC-DMA 协同工作模式不只是“采集电压数据”这么简单它可以作为一个高效的数据采集底座向上对接各种复杂算法。比如做简易电能分析仪时用这套方案采集三相电压和电流配合软件计算功率因数、谐波含量做电机控制器时用定时器三角波触发 ADC 采样让采样点始终保持在 PWM 周期的中心点再通过 DMA 把电流采样数据搬运出来实现所谓的“中心对齐采样”从而获取更准确的平均电流信息。另外一个常见扩展方向是“DMA 加空闲中断”的组合。这在串口接收中特别常用利用 DMA 循环接收配合串口空闲中断既能保证高频数据的连续接收又能在一次完整报文接收结束时立刻通知 CPU 处理。类比到 ADC 场景就是 DMA 持续搬运采样数据然后根据采样缓冲区中的某种特征比如超过阈值触发中断唤醒 CPU 做进一步分析。这种事件驱动的架构让 MCU 大部分时间处于低功耗状态只有在特定事件发生时才会被唤醒非常适合电池供电的物联网设备。对于需要更高数据吞吐率的场景还可以把 ADC 数据 DMA 到内存后再通过 DMA 搬运到 SPI 外设发送出去实现“ADC 采样到外发”的全链路 DMA 化。这条链路上 CPU 完全不参与数据移动唯一要做的是在两个 DMA 之间设置好同步机制。这个方案我并没有在实际产品中使用过但它确实是扩展 ADC-DMA 应用的一个重要方向值得调研。回到主题最核心的部分ADC-DMA 协同工作的本质是让 ADC 转换速率和 DMA 搬运带宽匹配起来同时用正确的硬件布局和软件逻辑保证数据质量。从我个人的角度看这套方案真正难的地方不在初始化配置本身而在于理解数据流的方向和节奏。配置好一次后后面换芯片平台或加采样子系统思路都是通用的。最后分享一个小技巧调试 ADC-DMA 采样系统时强烈建议先在 DMA 缓冲区的固定位置写一个特殊标志值比如 0xAAAA 或 0x55AA。正常运行后周期性检查这个标志是否被 DMA 覆盖。如果标志还在说明 DMA 没有正常工作如果标志被覆盖说明 DMA 已经在搬运数据了。这个“标志探测法”排查问题比仿真器单步执行快速得多尤其配合实时数据显示时几乎一眼就能定位有没有启动成功。这套技术组合值得你花一个下午仔细调试一遍一旦跑通后面做采样类项目会顺手很多。
RELATED READING

延伸阅读

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