资讯动态

STM32F4双重ADC同步模式实现七路高速采集的设计与实现

发布时间:2026/10/15 23:07:12 来源:尧图企业网站定制
最近在调一套多通道模拟量采集的板子需求是七路ADC数据同时进系统三相电流、两路母线电压、两路温度采样率还不能太低。单颗ADC扫七路也能做但每一路都要排队等转换总体刷新率上不去而且电流这种信号最好能成对同步采扫完A相再去扫B相时间上差了几微秒算角度和闭环的时候就别扭了。最后定下用STM32F4的双重ADC同步模式 一路DMA搬运七路信号分给ADC1和ADC2并行转换数据在DMA里自动拼好CPU基本不参与搬运。这篇就把这套方案的硬件分配、寄存器原理、HAL库配置和实测踩坑全部写清楚给同样要做多路采集的朋友一个可以直接抄的参考。1. 方案动机七路整成双重ADC到底图什么1.1 单ADC扫多通道的根本瓶颈很多人第一反应是七路而已单ADC加扫描模式不就行了。表面看确实行F4的ADC1最多有16个外部通道扫描模式里把序列排长一点DMA一次次把结果搬出来代码也挺简单。但这里有两个硬伤。第一个是时间。ADC的转换是串行的每个通道必须等上一个通道转换结束才能轮到下一个。假设采样时间84周期、12位精度单次转换就是841296个ADC时钟周期。ADC时钟用21MHz的话一路要4.57微秒扫完七路就是32微秒整一轮每一路的更新率只有约31kS/s。对传感器慢速采集无所谓但对电流环或者振动信号这个速度就有点紧了。第二个是同步性。扫描模式本质上是顺序快照七路信号在时间轴上被拉开了。同一时刻的七路电压是多少不存在的。对需要计算矢量或者对比相位关系的场景差几个微秒就足以让角度计算产生可见误差。我用一个粗俗但直观的类比单ADC扫描像只有一个收银员七个人排队结账每个人结账耗时固定队伍越长队尾的人等得越久双重ADC同步模式等于开了两个窗口两个人同时结账整体效率直接翻倍。1.2 双重ADC能带来什么本质变化F4系列里的ADC1和ADC2是一对双胞胎它们共享同一个ADC_CCR公共控制寄存器可以工作在同步模式。所谓同步就是主从两个ADC由同一个触发信号启动在同一个时钟节拍下各转各的通道。关键点在于ADC1和ADC2的转换序列可以各自独立配置比如ADC1排4个通道、ADC2排4个通道两者同时开始、同时推进。每过一个转换周期实际上同时完成了两路采集一轮下来8个通道只花了4个通道串行的时间。七路信号用8个采样位去装其中一路做重复采样当作占位实际效果就是七路等效刷新率近乎翻倍。更重要的是同步性。同步模式下ADC1的第n个通道和ADC2的第n个通道是在同一个节拍采样的时间差可以忽略不计。这就是为什么电机电流采样这类应用特别吃双重ADC——相电流需要精确同时采样才能算出正确的电流矢量。1.3 为什么还要加DMAADC产生的数据如果不经DMA就得靠中断或者轮询去读数据寄存器。七路也好八路也好每完成一路转换就进一次中断系统里只要有别的实时任务中断频繁进出就非常难受。更麻烦的是如果在轮询里读寄存器稍有延迟就可能读到旧数据或者覆盖新数据。DMA的定位就是专职搬运工。ADC每完成一路转换自动发出一个DMA请求DMA把数据从外设寄存器搬到内存数组全程不需要CPU干预。CPU只在一个完整的转换轮结束后收到一次回调通知或者干脆连通知都不等直接去数组里取最新数据。这样七路采集对CPU的占用几乎可以忽略。2. 双重ADC同步模式与关键参数设计2.1 正则同步模式的工作机制F4的双重ADC不止一种玩法有正则同步、注入同步、交织采样等。这里用的是正则同步模式寄存器里对应MULTI[4:0]的00001组合。这个模式的行为是ADC1作为主ADC2作为从两者共享同一个触发信号。软件触发时执行一次SWSTART两个ADC同时启动各自的扫描序列。每个转换节拍ADC1转SQR1里的当前通道ADC2同时转自己SQR1里的当前通道互不干扰。转换结束后ADC1的数据在ADC1_DR里ADC2的数据在ADC2_DR里。此时如果DMA访问模式配置成32位读一次ADC1的DR地址就能同时拿到两个数据低16位是ADC1的结果高16位是ADC2的结果。这个读一次拿两个的机制正是方案简洁的关键。DMA的目标缓冲区直接用uint32_t数组一个元素装一对数据七路ADC最终就变成一个8元素4对的数组索引关系非常清晰。2.2 七路通道的分配策略关键问题来了7路怎么分给两个ADC直觉是ADC1分4路、ADC2分3路。但这里有个隐藏约束同步模式下两个ADC的序列长度最好一致。因为DMA在32位模式下是按对搬运的如果一个ADC先转完自己的序列它那边的数据寄存器就会停在最后一个值后续DMA读到的 高16位 全是这个残留值造成通道错位。所以我的做法是44用8个位置装7个信号。具体分配信号编号ADC归属外部引脚ADC通道序列Rank第1路ADC1PA0IN0Rank1第2路ADC1PA1IN1Rank2第3路ADC1PA2IN2Rank3第4路ADC1PA3IN3Rank4第5路ADC2PA4IN4Rank1第6路ADC2PA5IN5Rank2第7路ADC2PA6IN6Rank3占位重复ADC2PA6IN6Rank4最后一路故意让ADC2在第4个Rank再采一次IN6两个ADC的序列长度都是4DMA每次拿到4个32位字。解析时只需要前7个有效值第4个字的高16位是重复的IN6直接丢掉。这个技巧既保证了序列等长又不需要额外占用一个引脚去接地做假通道。2.3 时钟、采样时间和总耗时计算F4的ADC时钟来自APB2通过ADC_CCR里的ADCPRE分频得到上限是36MHz。如果系统主频168MHzAPB2通常跑84MHz那么ADCPRE必须至少4分频得到21MHz——2分频的42MHz超限了这是很多新手初始化时忽略的约束。采样时间的选择直接影响精度和速度的平衡。采样时间越长内部采样电容充电越充分对高内阻信号源越友好但总转换时间也越长。12位分辨率下单次转换总时间 采样时间 12个ADC时钟周期。以常规的84周期采样时间为例单次转换84 12 96个周期在21MHz下96 / 21MHz ≈ 4.57微秒一轮4对通道4 × 4.57 ≈ 18.3微秒每路等效更新率约54.7kS/s如果信号源内阻很低比如运放输出可以激进一点用28周期采样时间单次40个周期约1.9微秒一轮约7.6微秒每路等效刷新率接近130kS/s。我一般的经验是分压电阻直接进ADC的用84周期以上运放缓冲过的信号28到56周期随便选。3. 引脚规划与硬件注意事项3.1 F4的ADC引脚资源盘点STM32F4系列以常见的F407为例的ADC1和ADC2引脚是共享的也就是说同一个引脚可以映射到ADC1也能映射到ADC2。常用的外部通道如下引脚通道可用ADCPA0IN0ADC1/2/3PA1IN1ADC1/2/3PA2IN2ADC1/2/3PA3IN3ADC1/2/3PA4IN4ADC1/2PA5IN5ADC1/2PA6IN6ADC1/2PA7IN7ADC1/2PB0IN8ADC1/2PB1IN9ADC1/2PC0IN10ADC1/2/3选择引脚时除了看功能映射还要留意和板上其他外设的冲突比如PA0可能接了按键或者唤醒引脚PA2/PA3可能被USART2占用。布局阶段先把引脚功能表拉出来过一遍不然画完板子才发现引脚被占返工成本很高。3.2 推荐引脚分配与电路处理我这次用的分配已经在第二节列出ADC1用PA0~PA3ADC2用PA4~PA6全部集中在GPIOA的低位引脚好处是初始化GPIO特别方便一个GPIOA时钟打开把所有引脚配成模拟输入就行。硬件上几个关键点每个ADC输入引脚对地接一个100nF电容滤高频噪声。如果被测信号变化速度不快这个电容还能兼任抗混叠职责。信号源内阻大的话在引脚前加一级运放跟随器否则采样电容充电不足会导致读数偏低且不稳定。运放选用通用单电源轨到轨型号即可不需要多高端。VREF直接连VDDA的话参考电压就是电源电压电源纹波会直接进ADC结果。要求高的场合用外部基准芯片把VREF接2.5V或3.0V精密基准。VDDA和VSSA必须单独走线并加去耦常见的做法是1μF100nF并联模拟电源和数字电源之间用磁珠或0欧电阻隔离。3.3 输入阻抗带来的精度坑这块最容易踩。F4内部采样电容大概几皮法采样开关闭合后信号源必须在采样时间内把电容充到和输入电压一致。如果信号源内阻是几十千欧采样时间又短电容根本充不满测出来的电压就会偏低。有个粗略的经验公式假设采样时间84周期、21MHz约4微秒允许最大信号源内阻大致在十几千欧级别。超过这个范围要么加大采样时间到480周期要么加运放缓冲。我就是因为一开始直接用一个100k电位器分压去测读数比万用表低了将近一截后来加跟随器才恢复正常。这个坑在低速传感器上尤其隐蔽因为读数看起来稳定实际上稳定地偏小。4. 软件实现HAL库全流程下面用HAL库走一遍完整配置。代码以F4系列HAL为准个别结构体字段在不同版本库里略有差异以你本机头文件为准。4.1 GPIO和时钟配置七路输入全部在GPIOA上配置成模拟输入__HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_ADC2_CLK_ENABLE(); __HAL_RCC_DMA2_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_ANALOG; GPIO_InitStruct.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, GPIO_InitStruct);一个很容易忽略的细节模拟输入模式下不需要配置速度推挽、开漏、上下拉全部不适用。有些人从GPIO输出配置复制过来把Pull设成上拉会导致被测电压被内部电阻抬偏测不准。4.2 ADC1和ADC2的独立初始化两个ADC除了实例不同初始化参数基本一致ADC_HandleTypeDef hadc1; ADC_HandleTypeDef hadc2; hadc1.Instance ADC1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode ADC_SCAN_ENABLE; hadc1.Init.ContinuousConvMode ENABLE; hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConvEdge ADC_EXTERNAL_TRIGCONVEDGE_NONE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 4; hadc1.Init.DMAContinuousRequests ENABLE; hadc1.Init.EOCSelection ADC_EOC_SEQ_CONV; HAL_ADC_Init(hadc1); hadc2.Instance ADC2; hadc2.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; hadc2.Init.Resolution ADC_RESOLUTION_12B; hadc2.Init.ScanConvMode ADC_SCAN_ENABLE; hadc2.Init.ContinuousConvMode ENABLE; hadc2.Init.DiscontinuousConvMode DISABLE; hadc2.Init.ExternalTrigConvEdge ADC_EXTERNAL_TRIGCONVEDGE_NONE; hadc2.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc2.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc2.Init.NbrOfConversion 4; hadc2.Init.DMAContinuousRequests DISABLE; hadc2.Init.EOCSelection ADC_EOC_SEQ_CONV; HAL_ADC_Init(hadc2);注意ADC2的DMAContinuousRequests设成了DISABLE因为32位模式下只有主ADC1会使用DMAADC2的DMA请求在同步模式下不会单独开启。如果把两个都打开反而可能冲突。4.3 双重ADC模式配置在ADC1上调用多模式配置函数把同步模式和DMA访问模式一起写进ADC_CCRADC_MultiModeTypeDef multimode {0}; multimode.Mode ADC_DUALMODE_REGSIMULT; multimode.DMAAccessMode ADC_DMAACCESSMODE_32_BITS; multimode.TwoSamplingDelay ADC_TWOSAMPLINGDELAY_5CYCLES; HAL_ADCEx_MultiModeConfigChannel(hadc1, multimode);从寄存器视角看这个函数最终改的是ADC_CCRMULTI[4:0]置为00001选择正则同步DMA[1:0]置为10选择单DMA通道32位访问DELAY[3:0]设为采样间延迟。TwoSamplingDelay这个参数在交织采样模式下才真正起效同步模式下给个默认值就行。这里有一个值得理解的点为什么是HAL_ADCEx_而不是HAL_ADC_因为双ADC属于扩展功能配置的是两个ADC的公共寄存器必须挂在主ADCADC1上执行。如果把参数传给hadc2函数内部会断言失败。4.4 通道序列配置通道配置要分别对两个ADC做。ADC1的四个RankADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel ADC_CHANNEL_0; // PA0 sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_84CYCLES; HAL_ADC_ConfigChannel(hadc1, sConfig); sConfig.Channel ADC_CHANNEL_1; // PA1 sConfig.Rank 2; HAL_ADC_ConfigChannel(hadc1, sConfig); sConfig.Channel ADC_CHANNEL_2; // PA2 sConfig.Rank 3; HAL_ADC_ConfigChannel(hadc1, sConfig); sConfig.Channel ADC_CHANNEL_3; // PA3 sConfig.Rank 4; HAL_ADC_ConfigChannel(hadc1, sConfig);ADC2的序列重点看Rank3和Rank4是同一个通道sConfig.Channel ADC_CHANNEL_4; // PA4 sConfig.Rank 1; HAL_ADC_ConfigChannel(hadc2, sConfig); sConfig.Channel ADC_CHANNEL_5; // PA5 sConfig.Rank 2; HAL_ADC_ConfigChannel(hadc2, sConfig); sConfig.Channel ADC_CHANNEL_6; // PA6 sConfig.Rank 3; HAL_ADC_ConfigChannel(hadc2, sConfig); sConfig.Channel ADC_CHANNEL_6; // PA6 重复占位用 sConfig.Rank 4; HAL_ADC_ConfigChannel(hadc2, sConfig);序列里重复配置同一个通道在F4里是合法的硬件会把同一个采样开关按Rank顺序再闭合一次多出来的这个值就是前一个值的近时刻重复。这样既保证了两边序列等长又不会浪费额外的外部引脚。4.5 DMA配置与链路ADC1在F4上的DMA映射是DMA2的Stream0、Channel0。由于数据寄存器是32位组合访问外设端和数据端都必须按字对齐DMA_HandleTypeDef hdma_adc1; hdma_adc1.Instance DMA2_Stream0; hdma_adc1.Init.Channel DMA_CHANNEL_0; hdma_adc1.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc DMA_PINC_DISABLE; hdma_adc1.Init.MemInc DMA_MINC_ENABLE; hdma_adc1.Init.PeriphDataAlignment DMA_PDATAALIGN_WORD; hdma_adc1.Init.MemDataAlignment DMA_MDATAALIGN_WORD; hdma_adc1.Init.Mode DMA_CIRCULAR; hdma_adc1.Init.Priority DMA_PRIORITY_HIGH; hdma_adc1.Init.FIFOMode DMA_FIFOMODE_DISABLE; HAL_DMA_Init(hdma_adc1); __HAL_LINKDMA(hadc1, DMA_Handle, hdma_adc1);PeriphDataAlignment和MemDataAlignment必须是WORD这直接对应32位DMA访问模式。如果设成HALFWORDDMA会按16位搬运读回来的数据完全错乱。这是配置里最容易出错的地方之一很多朋友卡在这一步半天最后发现就是对齐方式写错。中断方面如果要每轮转换完成都得到通知把DMA2_Stream0的中断打开HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);然后在中断服务函数里转发给HALvoid DMA2_Stream0_IRQHandler(void) { HAL_DMA_IRQHandler(hdma_adc1); }4.6 启动采集与数据解析缓冲区定义和启动#define ADC_ROUND_WORDS 4 uint32_t adc_buf[ADC_ROUND_WORDS]; volatile uint8_t adc_frame_ready 0; uint16_t raw_ch[8]; HAL_ADCEx_MultiModeStart_DMA(hadc1, (uint32_t *)adc_buf, ADC_ROUND_WORDS);启动后ADC1和ADC2同时进入连续转换每完成4对通道DMA完成一轮搬运并触发一次回调。回调里把32位字拆成两个16位值void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { for (int i 0; i ADC_ROUND_WORDS; i) { raw_ch[i] (uint16_t)(adc_buf[i] 0xFFFFu); raw_ch[i 4] (uint16_t)(adc_buf[i] 16); } adc_frame_ready 1; } }注意我提到的是HAL_ADC_ConvCpltCallback有些版本的HAL在多模式DMA下会优先走HAL_ADCEx_MultiModeConvCpltCallback看你库里回调表是怎么注册的。稳妥的做法是两个回调都写一个里面做同样的处理反正只会在其中一个触发。主循环里消费数据while (1) { if (adc_frame_ready) { adc_frame_ready 0; /* raw_ch[0]~raw_ch[6] 对应7路有效通道raw_ch[7]为重复占位丢弃 */ for (int i 0; i 7; i) { float voltage (float)raw_ch[i] * 3.3f / 4095.0f; /* 按信号类型继续处理或发送出去 */ } } }4.7 转换成电压的刻度细节12位ADC满量程是4095对应参考电压。如果VREF直连VDDA且VDDA是3.3V那么电压 原始值 * 3.3 / 4095不要用除以4096的写法。12位ADC的满量程值是4095不是4096用4096会把满量程算高约0.024%。看似差得不多但如果你后面要做标定或者闭环计算这点固定偏差会在整个链路里累积。如果信号经过了运放或者分压网络还要把分压比还原。比如100k100k分压测0~50V母线电压引脚上的实际电压是总电压的一半那么真实电压 raw * 3.3 / 4095 * 2建议把分压比定义成宏别散落在代码各处后面调整分压电阻时只改一处。5. 实测验证与数据分析5.1 测试方法代码烧进去之后我先用精密稳压源分别给七路输入灌固定电压从0V开始每次加0.5V一直测到3.3V记录每个通道的转换值和换算电压。测试过程中在电脑上通过串口把raw_ch[0]到raw_ch[6]打出来对比每一路的线性度。固定电压法是最快的验证手段比直接接传感器靠谱得多——传感器信号本身就在波动你根本无法判断误差是ADC的还是传感器的。5.2 数据结果分析以一路典型数据为例输入2.000V用四位半万用表校准过稳压源12位转换值理论上是期望值 2.000 / 3.3 * 4095 ≈ 2481实际采集到的原始值通道实测原始值换算电压(V)与理论值误差第1路24822.00080.0008第2路24801.9992-0.0008第3路24832.00160.0016第4路24812.00000第5路24812.00000第6路24801.9992-0.0008第7路24822.00080.0008七路的平坦度在±2个LSB以内换算电压误差小于2mV。这个水平对于常规采集已经足够。如果还嫌差可以做软件标定每路采集零点和满量程点算增益和偏置逐通道修正。F4每个ADC还有模拟看门狗和偏移校正这类硬件辅助但效果有限多数情况下软件标定更实用。5.3 噪声与波动观察我把一个稳定的1.5V直流信号接到其中一路连续跑了几万次转换观察原始值分布。正常情况下值在615附近浮动1.5/3.3×4095≈1861这里假设测的是1.5V则约1861波动范围±3个LSB。如果波动明显变大优先检查输入引脚对地的100nF电容是否漏焊VDDA去耦是否到位采样时间是否太短导致充电不足板上是否有开关电源靠近模拟走线这里有个经验不要一看到波动就怀疑ADC先拿示波器看引脚上的实际波形。很多时候是参考电压在跳或者输入信号本身就有纹波ADC只是忠实反映了被测信号。6. 常见问题与排查技巧6.1 问题速查表现象可能原因排查方向所有通道读数恒为0GPIO未配模拟输入或ADC未使能检查GPIO配置、ADC时钟用调试器看ADC_DR所有通道读数满量程4095引脚悬空、输入短路到VREF或采到噪声检查接线用万用表测引脚电压个别通道读数偏小且稳定信号源内阻过大、采样时间不足增大采样时间或加运放跟随器数据顺序错乱DMA字节/半字对齐配置错误确认PeriphDataAlignment和MemDataAlignment都是WORD前几轮数据像垃圾值ADC刚上电未稳定启动后丢弃前2~3轮数据回调不触发多模式回调函数名不对检查HAL版本HAL_ADC_ConvCpltCallback和MultiMode回调都挂上第4个字高16位和预期不符序列不等长或Rank4误配确认ADC2的Rank4是重复IN6DMA启动后系统死机缓冲区大小或对齐问题确认adc_buf是uint32_t数组长度匹配DMA传输数量6.2 最容易被忽视的三个坑第一个坑是DMA缓冲区对齐。HAL要求DMA目标缓冲区按32位对齐虽然大部分编译器默认就把uint32_t数组对齐了但如果用了指针强转或者结构体打包就可能出问题。稳妥做法是在数组定义前加对齐修饰__ALIGN_BEGIN uint32_t adc_buf[ADC_ROUND_WORDS] __ALIGN_END;第二个坑是首次转换数据。每次都一样启动ADC后第一轮数据基本不能要。因为采样电容初始状态不确定而且同步模式刚建立时主从节奏可能还没完全对齐。我在启动函数后面加一个简单的丢弃逻辑等第二个回调之后才置frame_ready标志实测下来数据就干净了。第三个坑是EOCSelection对DMA的影响。ADC_EOC_SEQ_CONV表示序列转换完成后置EOC标志逐通道转换时则用ADC_EOC_SINGLE_CONV。在DMA搬运的场景下如果EOCSelection配置不当DMA请求的产生时机可能和预期不一致。我习惯统一用ADC_EOC_SEQ_CONV配合DMA这个组合在F4上最稳。6.3 从单DMA切到双DMA的扩展思路这套方案用一路DMA解决8个采样位已经覆盖了七路需求。如果你的通道数继续增加比如要12路甚至16路可以考虑两种扩展路径。第一种是把占位通道全部变成真实通道ADC1和ADC2各扩展序列长度保持同步模式不变DMA缓冲区跟着加大就行。第二种是改用双DMA模式配置成ADC_DMAACCESSMODE_12_10_BITS让ADC1和ADC2各自挂一路DMA、各读各的数据寄存器。这样两边序列长度可以不一致灵活性更高但代价是DMA通道、中断和回调都多了一倍代码复杂度上升不少。我目前的建议是通道在10路以内优先用单DMA 32位模式简单可靠超过10路或者两个ADC的数据率差异很大再考虑双DMA。最后补一句这套双重ADC同步DMA我第一次调通用了大半天大部分时间都耗在DMA对齐和序列长度这两个细节上理清楚之后其实就是一个固定的套路。实际做产品时我会在正式采集前做一次完整标定把每一路的增益误差记进校准表毕竟温漂和分压电阻公差这些硬件层面的偏差光靠ADC本身是抹不平的。后面如果项目用到外部触发同步采样比如PWM中心对齐触发电流采集我会再写一篇专门说触发源选择和时序对齐的做法。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑