
简介面向STM32F4系列嵌入式开发者的ADC多通道采集工程源码采用DMA方式实现多路模拟信号的高效数据搬运避免CPU等待转换结果显著提升数据吞吐率与实时性适合传感器监测、设备控制等场景。压缩包内共128个文件以60个C源码和60个头文件为核心配套hex可烧录固件、Keil工程配置文件及辅助脚本整体大小仅572KB可快速加载到常见STM32F4开发板实验。代码覆盖ADC时钟、通道初始化、多通道转换序列排布、采样时间调节、DMA参数配置、传输完成中断服务及数据缓冲区处理等完整流程同时涉及外部触发启动与异常处理等实用技巧开发者可以对照标准外设库理解底层寄存器设置和库函数调用逻辑。已有1372人学习浏览无论用于多路传感器数据采集、监测系统还是电机控制都能借助这套源码快速搭建稳定的采集链路有效降低开发门槛与调试成本。资源目录结构紧凑便于随时查阅各模块配置代码。 前两天群里有人发了一段ADC采集的代码四路模拟量DMA方式但读出来只有第一路是准的后面三路要么是0要么跟着第一路的值一起变。这种问题我在STM32 ADC多通道采集的帖子里见过不下十次。很多人以为只要把CubeMX里的通道配齐、打开DMA数据就会按顺序乖乖躺进数组里实际上一旦涉及扫描模式、连续转换、DMA循环这三个开关的组合任何一个没配对出来的数据就是乱七八糟的。这篇就把ADC多通道DMA采集的完整配置、数据排序规则和几个高频翻车现场复盘一遍适合正在做多通道采集、被数据错位和卡死问题折磨的人参考。1. 多通道采集为什么推荐DMA三种采集方式的差别ADC采集本身不难难的是“多通道”和“实时性”两个词一起出现。同样是读四路电压轮询、中断、DMA三种方式写出来的代码难度和运行效果差很远。1.1 轮询模式代码最简单代价是CPU空转轮询模式是最直观的写法启动一次转换死等转换完成标志位然后读数据寄存器。单通道这么玩没问题但换成四通道一次完整采集就需要依次等待四次转换结束。每次等待期间CPU什么事都干不了只能在那空转。如果ADC时钟是12MHz一个通道转换周期是14个ADC时钟采样周期用默认值1.5周期加上12.5周期转换时间单次转换大约1.17微秒四通道就是不到5微秒。听起来不长但如果你要做200Hz的实时控制CPU每5毫秒就要空转5微秒占比虽然只有0.1%可一旦系统里还有PID运算、通信协议、显示刷新这种空转就会被无限放大。更麻烦的是轮询方式读到的数据在时间轴上天然是错开的四个通道的数据其实相差了数微秒某些同步采集场景是不能接受的。1.2 中断模式适合低频高频下是灾难中断方式比轮询先进一些每次转换完成后进入中断在中断里读取数据并启动下一次转换。它的优点是不需要CPU死等但代价是中断频率成倍上升——四通道每采集一轮就要进四次中断。每次中断进入、压栈、读取、出栈实际开销远大于轮询里的那个空while。当采集频率不高、通道数不多时中断方式完全够用。但当你把采样频率拉到几十千赫或者通道数扩展到八个以上CPU的大部分时间都会耗费在中断切换上主循环反而被饿死。我见过一个项目用中断方式做八通道20kHz采样结果中断占用时间超过30%稍微加一点逻辑处理程序就崩了最后不得不改成DMA。1.3 DMA方式数据搬运不再占用CPUDMA方式最直观的理解是ADC转换完一个通道的数据后DMA控制器自动把结果从ADC的数据寄存器搬到内存数组里整个过程不需要CPU参与。你要做的只有两件事第一在启动时调用一次HAL_ADC_Start_DMA第二等整轮转换全部完成后在DMA传输完成回调里拿到数据。这意味着CPU在ADC转换期间完全解放可以去做别的事情。四通道的转换顺序、数据排序、搬运地址全部由硬件和DMA配置决定不依赖中断响应速度。对于需要高频率、多通道、数据需要同步处理的场景DMA基本是唯一合理的选择。用一句话概括轮询是让CPU亲自盯中断是让CPU频繁跑腿DMA是派一个专职快递员数据直接送货上门。2. CubeMX配置里最容易搞错的三个开关扫描、连续转换与循环模式不管你是用CubeMX自动生成代码还是纯寄存器开发多通道DMA采集的本质就是组合几个模式开关。这三个开关各自独立又相互影响但网上很多教程只讲了其中一个导致大家配完之后出现各种奇怪现象。2.1 ADC参数面板里必须逐项确认的配置先以CubeMX配ADC1为例Parameter Settings里这几个参数每一个都和最终行为直接相关配置项推荐值说明ScanConvModeEnabled多通道必须开启否则只转第一个RankContinuousConvModeEnabled连续转换转换完一轮紧接着下一轮DMA Continuous RequestsEnabledDMA持续搬运和连续转换配合使用End of Conversion SelectionEOC flag at end of sequence整轮转换结束才产生事件适合多通道Number of Conversion具体通道数比如四通道就填4Rank依序选择对应通道Rank1是第一次转换Rank2是第二次依此类推ScanConvMode这个开关我单独强调一下。很多人以为只要把多个Rank配置上了ADC就会自动扫描所有通道实际上扫描模式不打开ADC只认Rank1对应的那一个通道后面配置的Rank2、Rank3、Rank4全部被忽略。这是“只采到第一路”最常见的根因。End of Conversion Selection也容易被忽略。默认情况下EOC在每个通道转换结束后都会置位如果此时恰好DMA配置有偏差很容易出现数据还没搬完就被下一次转换覆盖。多通道场景建议直接选“End of sequence conversion”确保整轮都转完了才产生一次事件数据完整性更有保障。2.2 DMA配置里的两个关键参数DMA部分在CubeMX里选ADC1的请求然后重点看两个参数。第一个是Mode必须选Circular循环模式。选Normal的话DMA搬完一轮数据就停了ADC还在继续转换但再也没有人帮它搬运数据数组里的值永远是第一轮的旧数据。第二个是Data WidthADC的数据寄存器是16位有效DMA的数据宽度建议Memory和Peripheral都设成Half Word也就是16位跟缓冲区类型保持一致。这里如果配成Word缓冲区声明成uint16_tDMA每次搬运4字节数组会被写穿后果就是数据错乱甚至意外覆盖其它变量。这三个开关配好之后CubeMX会自动生成MX_ADC1_Init和MX_DMA_Init两个函数。我的建议是生成完代码后不要急着下载人肉检查一遍初始化顺序。DMA的初始化必须在ADC初始化之前否则外设请求和对应DMA通道之间可能还没建立好关系。CubeMX生成的顺序一般是对的但如果你手改过代码这一点很容易被破坏。3. DMA缓冲区里数据是怎么摆放的通道排序与缓冲区大小计算多通道DMA采集里另一个高频疑惑是数据到底按什么顺序放进数组的数组该开多大搞清楚这两件事代码基本就成功了一半。3.1 缓冲区长度公式与对应关系假设Rank1配置成通道0Rank2配置成通道1Rank3配置成通道2Rank4配置成通道3那么ADC转换启动后第一次转换结果写到adc_buf[0]第二次写到adc_buf[1]第三次写到adc_buf[2]第四次写到adc_buf[3]。也就是说DMA缓冲区里数据的顺序和你配置的Rank顺序严格一一对应而不是按照通道号的数值大小排序。缓冲区长度公式很简单缓冲区长度 通道数 × 每个通道需要采集的次数。比如四个通道每通道只采一组数据数组就是uint16_t adc_buf[4]执行HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buf, 4)第三次参数填4表示一次性搬运4个数据。如果每通道要采16次用于软件滤波那数组就是uint16_t adc_buf[4 * 16]启动时第三次参数填64。搬运完成的标志会在整批64个数据都搬完后产生一次回调这种批处理方式特别适合和均值滤波结合。3.2 HAL库启动与读取代码示例启动代码和单通道没有太大区别核心就一行uint16_t adc_buf[4]; HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, 4);启动之后不要再用HAL_ADC_GetValue(hadc1)去读数据DMA模式下真正的数据在adc_buf数组里。读取逻辑是// 第一路电压假设参考电压3.3V且没有分压 float voltage_ch0 adc_buf[0] * 3.3f / 4095.0f;如果你想一次性把四路都读出来按索引直接取就行uint16_t ch0 adc_buf[0]; uint16_t ch1 adc_buf[1]; uint16_t ch2 adc_buf[2]; uint16_t ch3 adc_buf[3];不要在转换过程中频繁去读这个数组DMA每转完一个通道就会往数组里写一次数据你在这个间隙去读读到的可能是新旧混合的中间状态。要么等转换完成回调触发后再统一读取要么在主循环里用一个标志位判断当前是否处于稳定状态。3.3 进阶一次DMA循环采集多组数据很多实时性要求高的项目不是简单采一次就用而是连续采几十次做均值。这时候DMA的环形缓冲区优势就体现出来了。比如四个通道各采16次数组大小64回调触发时数组里的数据是最近64次转换结果。均值处理的索引计算是重点直接看代码#define CH_NUM 4 #define SAMPLE_NUM 16 uint16_t adc_buf[CH_NUM * SAMPLE_NUM]; uint16_t filtered[CH_NUM]; uint32_t sum; void ProcessADCData(void) { for (uint8_t ch 0; ch CH_NUM; ch) { sum 0; for (uint8_t i 0; i SAMPLE_NUM; i) { sum adc_buf[i * CH_NUM ch]; } filtered[ch] (uint16_t)(sum / SAMPLE_NUM); } }关键在adc_buf[i * CH_NUM ch]这个索引DMA按转换次序连续写第一轮四个通道存在索引0、1、2、3第二轮存在4、5、6、7所以第i轮第ch个通道的位置就是i * CH_NUM ch。这样处理后filtered数组里就是四路均值结果拿来滤波或者上报都很方便。4. 踩坑实录多通道ADCDMA最常见的五个翻车现场这一节写的都是我在实际项目里遇到或者帮别人排查过的真实故障。每个症状都有对应的排查链路建议收藏起来遇到问题按顺序查。4.1 只采到第一路扫描模式没开症状非常典型adc_buf[0]随输入电压变化adc_buf[1]到最后全是0或者复制的第一路的值。排查链路先看CubeMX里ScanConvMode是不是Enabled。如果用的是寄存器开发检查ADC_CR1寄存器的SCAN位。扫描模式不打开ADC根本不会按Rank表走到第二个通道后面的转换请求全部被忽略。配置正确后再用调试器全速运行在回调里打断点看数组内容确认是否四路都有变化。这个坑之所以反复出现是因为CubeMX在默认新建工程时ScanConvMode初始是Disabled你添加了多个Rank但忘了把扫描模式打开代码生成后也不会报错只有运行时才暴露问题。4.2 采了一轮就停DMA循环模式没开症状串口打印第一轮四路数据是正确的但第二轮开始数据不变永远是第一次采集的值。排查链路检查DMA配置里Mode是不是Normal。Normal模式的含义是搬运完指定数量数据后DMA通道自动关闭。ADC如果配置成连续转换它还在后台不断转换但DMA已经不工作了数组自然没人更新。解决办法有两种一是把DMA的Mode改成Circular让它搬完一批自动接着搬下一批二是不改DMA配置在DMA传输完成回调里重新调用一次HAL_ADC_Start_DMA。第二种方式多一次重复启动逻辑不如第一种干净。4.3 数据错位与覆盖DMA数据宽度和缓冲区类型不匹配症状改变第二路输入电压数组里对应位置没变反而另一个索引的值跟着变或者整个数组的数据看起来像是被某种规律打乱了。排查链路优先检查DMA的Data Width设置。ADC数据寄存器是16位有效但如果你把外设和内存数据宽度都设成Word32位同时缓冲区声明为uint16_t数组那么DMA每写一次数据实际上占用了两个数组元素的位置相当于把相邻通道的数据覆盖掉了。反过来如果缓冲区声明成uint32_tDMA宽度设Half Word数组里会出现高低位错位的情况。正确做法很统一DMA宽度用Half Word缓冲区类型用uint16_t两者严格对齐。4.4 回调里做重活系统卡死症状程序跑着跑着就死机或者主循环里的其他任务明显变慢。把回调函数里的处理代码注释掉程序就恢复正常。排查链路DMA传输完成回调是在中断上下文执行的。多通道DMA的转换周期可能只有几十微秒如果你在HAL_ADC_ConvCpltCallback里做了浮点运算、库函数打印、OLED刷新这类耗时操作下一轮DMA中断已经在中断队列里排队了长期积累导致栈溢出或者中断响应异常。正确姿势是回调里只做最快的事情。比如置一个全局标志位或者用memcpy把数据快速拷贝到另一个缓冲区真正的滤波和业务处理放到主循环里。volatile uint8_t adc_ready 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { adc_ready 1; } }主循环里检测adc_ready再处理数据处理完清掉标志。这个模式在DMA采集里比什么都稳。4.5 数值整体偏移采样周期和阻抗不匹配症状用万用表量引脚明明是2.5VADC读回来却稳定在2.3V左右而且这个偏差随信号源内阻变化。排查链路ADC内部采样电容需要在采样阶段将电压充到与外部信号源一致采样时间太短、信号源阻抗太大、外部没有接滤波电容都会导致采样电压建立不充分。解决方法最直接的是把采样周期调大。CubeMX里每个通道的SamplingTime都可以单独配比如从1.5周期改成239.5周期。ADC时钟在12MHz时239.5周期的采样时间大约20微秒大部分几十千欧以内阻抗的场景都够用了。另一个有效方案是在ADC输入引脚和地之间并联一个0.1uF电容能显著降低信号源阻抗对采样电容的影响。4.6 容易忽略的GPIO模拟输入配置还有一个看似低级但实际高频的问题ADC引脚没有配置成模拟输入模式。直接表现是采集值有变化但噪声很大或者数值表现不稳定。STM32的GPIO引脚在作为ADC输入时必须配置为Analog模式。如果保持默认的推挽输出或者复用模式引脚内部结构会引入额外的导通电阻和寄生电容直接影响采样精度。用CubeMX时在Pinout界面把对应引脚选为ADC通道它会自动设为Analog。如果是手写寄存器配置记得把GPIO的MODER寄存器对应位设成Analog模式。这个细节不处理后面所有精度优化都是白费。5. 精度再提高一步采样周期设定与软件滤波的实操建议数据能稳定采出来了接下来的重点就是精度。ADC的精度受硬件布局、参考电压、采样周期和软件算法四方面影响其中采样周期和软件滤波是开发者可以控制的。5.1 采样周期的计算公式与选择策略ADC一次完整的转换时间由两部分组成采样时间和转换时间。12位分辨率下转换时间是固定的12.5个ADC时钟周期采样时间则由SamplingTime寄存器位决定可选值从1.5周期到239.5周期不等。总转换时间公式总转换时间 (采样周期 12.5) / ADC时钟频率在ADC时钟12MHz下如果采样周期选1.5周期总转换时间约1.17微秒选239.5周期则是21微秒。频率和精度是矛盾的信号源阻抗越高需要的采样时间越长。我的个人经验是信号源阻抗低几百欧以内、线短、要求高速采集时用1.5周期或较少周期配合高质量参考电压信号源阻抗中等几十千欧或经过分压电阻网络采集时默认直接用239.5周期省去排查建立时间的麻烦采集电池电压、温度传感器这类变化缓慢的信号时采样周期拉满转换频率低一点完全无所谓。如果多个通道信号源特性差别很大CubeMX里可以给不同Rank配不同采样时间不用为了某个慢速通道把整个ADC拖慢。5.2 三种常用滤波方式怎么选软件滤波本质上是牺牲时间换精度DMA批量采集为滤波提供了天然便利。中值滤波适合滤除尖峰毛刺比如电机启动瞬间的电磁干扰方法是连续采N个值取中间值。均值滤波适合处理随机噪声直接求算术平均是ADC采集中使用频率最高的方法。滑动平均是均值滤波的改进版每次新数据进来只计算最近N个数据的平均值适合实时性要求高的闭环控制。三种方式没有绝对好坏看场景。如果是采集电池电压均值滤波就可以如果是采集存在脉冲干扰的电流信号中值滤波更稳。5.3 与DMA缓冲区结合的均值滤波实现借助DMA的批量搬运能力均值滤波可以写得很优雅。配置上把DMA的目标缓冲区开成通道数 × 采样次数的大小启动时一次填满然后等一轮DMA传输完成在主循环里做均值计算。前面3.3节里的代码就是完整实现这里不再重复。一个补充建议均值采样次数选择2的幂次比如8、16、32。因为除以2的幂可以改成右移运算编译器会直接优化成移位指令处理几十万次采样时性能和除法指令有明显差距。代码上只需要把除法改成filtered[ch] (uint16_t)(sum 4); // 除以16这个细节在低主频MCU上体现更明显也算是一个低成本优化。多通道DMA采集虽然只是ADC应用里的一小块但它把嵌入式开发里的中断管理、外设协同、缓冲区设计都串起来了。我在实际项目里习惯把DMA循环模式配合固定缓冲区回调里只翻转标志位主循环统一做滤波和协议上报这样采集频率再高也不会出现数据覆盖或中断卡死。最后再分享一个小技巧调试阶段不要只看平均值把原始数组通过串口打印出来用上位机画波形数据抖动、错位、建立时间不足这类问题一眼就能看出来比盯着调试器的Watch窗口效率高得多。本文还有配套的精品资源点击获取