
STM32F4 多路ADC七路ADC采集的“双重ADC DMA”实战拆解有人问过我一个问题F4的ADC不是有十几个通道吗为什么非要用双重ADC一个ADC轮流扫七路不行吗能扫但性能和灵活性上差点意思。尤其是当你需要同时采集多个传感器信号、数据又要连续不断进内存的时候单ADC扫描模式的瓶颈就出来了。本文用一套“ADC1四路 ADC2四路、共七路有效数据 DMA自动搬运”的方案把双重ADC多通道的配置逻辑、对齐陷阱、代码实现一次讲透。这套思路在做电机驱动电流采样、电池管理系统电压采集、多路环境监测时都很通用。1. 为什么是“双重ADC 多通道”先搞懂单ADC的瓶颈1.1 单ADC多通道扫描慢在哪STM32F4的ADC虽然引脚不少但内核只有一个采样保持电路和一个转换器。所谓多通道扫描本质上还是“排队转换”通道0转换完切到通道1再切到通道2一路排下去。这里有两个看不见的损耗。一个是通道切换开销。规则通道的扫描模式下每次通道切换后采样保持电容需要重新充电充电时间就是采样时间。如果你用的是84个时钟周期的采样时间ADC时钟21MHz那么一个通道的转换周期是12 84 96个周期一个通道耗时约4.57微秒。七路全跑完就是32微秒左右。听起来好像也不慢但如果你要的是“持续、无间隙、且任意一路都不能被延迟”的数据流单ADC就吃力了。另一个是时基不同步。单ADC扫描七路时第0路和第6路的采样时刻差了一整轮对于需要“同一时刻观察多个信号”的场景三相电流采样、多路电压同步监测这个相位差会直接带来计算误差。1.2 双重ADC能带来什么STM32F4内部有两个独立ADC内核它们可以独立工作也可以组合成双重模式。组合模式下两个ADC在同一个触发源下同时转换各自配置的通道然后把结果拼在一起放同一个32位寄存器里。这样做有两个实际好处第一吞吐量翻倍。如果两个ADC各扫四路一轮下来只需要四拍就完成八路数据的采集而单ADC扫八路需要八拍。对高速控制系统来说这意味着同样的时间内能拿到更多有效样本或者同样的样本率下留出了更多CPU时间。第二通道间可配置不同的采样时间。ADC1可以用短采样时间采快速变化的电压信号ADC2可以用长采样时间采高阻抗传感器出来的信号两个ADC互不拖累。这在单ADC里很难优雅实现还得绕路写复杂的时序控制。双重模式本身也分成好几种同步规则模式RegSimult、同步注入模式InjSimult、交替模式AltTrig等。我们做多路独立信号采集用的是同步规则模式 独立扫描序列下面整套配置都围绕这个路子展开。2. 硬件准备与通道分配方案2.1 引脚怎么选七路通道如何落到两个ADC上F4的ADC1和ADC2有很多引脚是复用的比如PA0既可以作为ADC1_IN0也可以作为ADC2_IN0所以在分配时要注意避免冲突。下面是常见的引脚分布情况引脚ADC1通道ADC2通道PA0IN0IN0PA1IN1IN1PA2IN2IN2PA3IN3IN3PB0IN8IN8PB1IN9IN9PC4IN14IN14PC5IN15IN15我建议这么分配七路ADC1扫四个通道IN0(PA0)、IN1(PA1)、IN2(PA2)、IN3(PA3)ADC2也扫四个通道IN8(PB0)、IN9(PB1)、IN14(PC4)、IN15(PC5)注意这里是3 4 7路有效但不是让ADC2只扫三路。两个ADC的扫描序列长度必须一样这是双重ADC多通道最容易踩的坑后面专门讲。所以ADC2也配四个通道第四路要么用来做扩展要么就重复采一路当校验总之序列长度要拉齐。2.2 硬件接线和采样前的准备信号进入ADC前建议做两件小事。一是给每个通道加一个RC低通滤波几十到几百欧的电阻加100nF左右的电容。别小看这个传感器信号里那些高频毛刺会通过采样电容混叠到结果里出来的数据毛刺大到你怀疑人生。二是确保VREF引脚附近的去耦电容足够。参考电压不稳ADC的LSB会被噪声淹没。至少要用一个10uF电解电容并联一个100nF陶瓷电容放在离VREF引脚尽量近的位置。另外要注意信号源输出阻抗。如果信号源阻抗超过10kΩ建议加一级运放跟随器。F4的采样电容大约是几pF采样时间不足时根本充不饱读出来的数值总是偏低且抖动。这个问题在接高阻电位器、高阻分压电阻时最明显。3. 核心代码拆解HAL库初始化与DMA搬运3.1 时钟配置与双重模式初始化最容易被忽略的一步是ADC时钟分频。F4的ADC最高时钟是36MHzAPB2默认是84MHz所以分频系数至少要选4。ADC_HandleTypeDef hadc1; ADC_HandleTypeDef hadc2; DMA_HandleTypeDef hdma_adc1; ADC_MultiModeTypeDef multimode {0}; ADC_ChannelConfTypeDef sConfig {0}; __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_ADC2_CLK_ENABLE(); __HAL_RCC_DMA2_CLK_ENABLE(); hadc1.Instance ADC1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; // 84 / 4 21MHz hadc1.Init.Resolution ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode ENABLE; hadc1.Init.ContinuousConvMode ENABLE; hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConvEdge ADC_EXTERNALTRIGCONVEDGE_NONE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 4; HAL_ADC_Init(hadc1);时钟分频这里我实际踩过一次默认配置如果不显式指定CubeMX经常按DIV2生成在84MHz的APB2下直接超过36MHz上限ADC结果会出现可复现的偏差。保守的做法就是无脑上DIV421MHz的ADCCLK对绝大多数应用都够用转换时间算下来大概0.71微秒一个通道七路一轮也就不到3微秒完全不含糊。两个ADC的独立配置基本一样区别只是NbrOfConversion保持两边一致就行。3.2 双重模式配置关键是这个ADC_MultiModeTypeDef结构体multimode.Mode ADC_DUALMODE_REGSIMULT; multimode.DMAAccessMode ADC_DMAACCESSMODE_MULTI; multimode.TwoSamplingDelay ADC_TWOSAMPLINGDELAY_5CYCLES; HAL_ADCEx_MultiModeConfigChannel(hadc1, multimode);ADC_DUALMODE_REGSIMULT表示两个ADC的规则通道组同步转换。此时读数寄存器合并为一个32位寄存器低16位是ADC1的结果高16位是ADC2的结果。搞清楚这个布局后面的DMA才敢动手。这里有个很多人一开始搞不懂的地方双重模式下的DMA请求不需要ADC2单独配置DMA通道挂在ADC1上就行。每次转换完成硬件自动把两个ADC的数据打包成32位然后用ADC1的DMA请求触发搬运。如果你去给ADC2也开DMA反而会出问题因为F4上ADC2的独立DMA能力在双重模式下是不起作用的。3.3 DMA配置与缓冲区设计DMA直接用DMA2的Stream0通道选0这是ADC1的固定映射。hdma_adc1.Instance DMA2_Stream0; hdma_adc1.Init.Channel DMA_CHANNEL_0; hdma_adma1.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_adma1.Init.PeriphInc DMA_PINC_DISABLE; hdma_adma1.Init.MemInc DMA_MINC_ENABLE; hdma_adma1.Init.PeriphDataAlignment DMA_PDATAALIGN_WORD; hdma_adma1.Init.MemDataAlignment DMA_MDATAALIGN_WORD; hdma_adma1.Init.Mode DMA_CIRCULAR; hdma_adma1.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_adma1); __HAL_LINKDMA(hadc1, DMA_Handle, hdma_adma1);数据宽度必须设成WORD32位。这是双重ADC和单ADC在DMA配置上最大的区别。单ADC模式下转换结果是16位的你用半字搬运没问题双重模式下一个32位寄存器里装着两个ADC的结果如果还按16位搬运高16位的ADC2数据你就永远拿不到了。缓冲区用uint32_t数组__attribute__((aligned(4))) uint32_t adc_buf[4] {0};数组长度是4。为什么是4不是因为每路一个元素而是因为每次DMA传输对应一次双ADC联动转换也就是同时采了ADC1的一路和ADC2的一路。ADC1扫4个通道ADC2扫4个通道所以一轮完整数据正好是4个32位元素。3.4 启动转换与数据拆分启动用HAL库的扩展多模式启动函数HAL_ADCEx_MultiModeStart_DMA(hadc1, (uint32_t *)adc_buf, 4);启动之后DMA就一直循环搬运。主循环里不需要管ADC只做拆数for (int i 0; i 4; i) { uint16_t adc1_val adc_buf[i] 0xFFFF; uint16_t adc2_val (adc_buf[i] 16) 0xFFFF; ... }拆出来之后ADC1的四个值对应PA0、PA1、PA2、PA3上的信号ADC2的四个值对应PB0、PB1、PC4、PC5上的信号。七路有效数据外加一路扩展备用整整齐齐。4. 七路ADC数据对齐的隐形陷阱4.1 序列长度不一致会怎样这是我在双重ADC上栽过最久的一个跟头。一开始我图省事ADC1配了四个通道ADC2只配了三个通道心想反正DMA每传一次就是两路数据传三次就能拿到3对剩下ADC1的第四路单独传一次不就得了结果数据出来全乱了ADC2的值跑到了ADC1的位置上而且越往后错得越离谱。原因要从同步规则的机制说起。双重ADC同步模式下两个ADC要“同时”转换。转换的节奏由两个ADC各自的扫描序列共同决定。如果ADC1有四个通道要转ADC2只有三个那么前三拍一切正常第四拍时ADC2已经没活干了但ADC1还在转第四个通道。此时32位寄存器里低16位是ADC1第四路的有效值高16位是上一次残留的旧值。最麻烦的是这颗“旧值”不是你肉眼一眼就能看出来的只要信号变化不大它看起来就像一组正常数据甚至能骗过你以为的简单校验逻辑只有当某个通道突变时才露出马脚。4.2 序列长度拉齐一种可靠的对齐方案最稳的做法就是把两个ADC的扫描序列配得一样长。我现在的标准写法是ADC1扫IN0、IN1、IN2、IN3四路全部有效ADC2扫IN8、IN9、IN14、IN15前三级有效第四路要么以后扩展用要么重复采一遍IN8用来事后校验这样两边都是四路DMA每次传回一个32位字里面永远同时包含ADC1当前通道和ADC2当前通道的有效数据拆出来以后通道顺序天然对应根本不需要做重排。如果你真的需要ADC2只有三路那就把ADC2的NbrOfConversion也设成4第四个通道随便指向一个空引脚或者重复通道在应用层只读前三个值就行。这样牺牲一点点转换时间换来的对齐逻辑简单可靠这一点都不亏。4.3 数据可信度校验的实操做法数据到手后别急着直接拿去用。建议加一层轻量的合理性校验特别是做工业控制时。我常用的思路是在ADC2的第四路重复采样ADC2的第三路也就是PC4和PC5采同一个信号源然后判断两个值是否在合理的误差范围内。如果偏差超过比如2%说明采样链路可能有问题要么是输入阻抗太高采样时间不足要么是参考电压在波动要么干脆是某个引脚虚焊了。这个重复通道就是你的“哨兵”平时正常工作时不参与运算出问题时它第一个报警。另一个校验方向是检查连续性。正常的物理信号不会瞬间跳变如果某一路ADC的相邻采样值突然差了超过设定阈值大概率是DMA数据错位或者外部干扰打进了引脚。你可以把上一轮的结果存一份在主循环里做简单的差分判断超阈值就置一个标志位方便后面调故障。5. 常见问题速查与踩坑记录现象可能原因排查方向ADC2的数据全为0DMA数据宽度设成了半字只搬了低16位把PeriphDataAlignment和MemDataAlignment改成WORD只有前两路有数据后面全固定NbrOfConversion没配够扫描序列被截断检查hadc1和hadc2的NbrOfConversion是否对应通道数数据顺序错位越往后越乱两个ADC的扫描序列长度不一致把两边通道数配成相同不用的位置重复分配ADC抖动大读数不稳定采样时间过短信号高阻提高采样时间到84周期或更长检查输入阻抗启动DMA后不触发回调DMA中断没使能、回调函数名写成了别的名字检查HAL_ADC_ConvCpltCallback名字拼错直接不调用双重模式下启动卡死没有正确执行HAL_ADCEx_MultiModeConfigChannel配置确保Mode、DMAAccessMode都已设置再启动DMA读数整体偏高或偏低没有做ADC校准上电后调用ADC校准函数还有一个高频低错值得单独提没有使能DMA的传输完成中断。很多人配好了回调函数却发现一直进不去查半天代码没问题最后发现CubeMX里没勾选DMA中断。HAL库虽然帮你做了很多封装但中断使能这一步它不会替你做。务必在启动DMA之前把对应的NVIC中断打开HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);这样一整套方案跑下来七路ADC数据就能稳定地出现在内存里了。F4的双ADC其实还有很多可玩的变体比如交替模式下两个ADC轮流采同一个信号等效翻倍采样率再比如用ADC的注入组配合规则组做紧急优先级采样都是在这个基础上做文章。如果后续你想把这套七路采集扩展到更多路思路也简单ADC2的第四路补上真实传感器就是八路还不够的话F4系列部分型号还有第三个ADC可用把ADC3独立出来再做一路DMA就能到十几路。记住一个核心原则双重ADC的数据正确性很大程度上取决于两路扫描序列的对称性任何时候破坏这个对称性都要做好数据对齐的准备。