最近在调一套多通道模拟量采集的板子,需求是七路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位精度,单次转换就是84+12=96个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 为什么还要加DMA
ADC产生的数据如果不经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位" 全是这个残留值,造成通道错位。
所以我的做法是"4+4,用8个位置装7个信号"。具体分配:
| 信号编号 | ADC归属 | 外部引脚 | ADC通道 | 序列Rank |
|---|---|---|---|---|
| 第1路 | ADC1 | PA0 | IN0 | Rank1 |
| 第2路 | ADC1 | PA1 | IN1 | Rank2 |
| 第3路 | ADC1 | PA2 | IN2 | Rank3 |
| 第4路 | ADC1 | PA3 | IN3 | Rank4 |
| 第5路 | ADC2 | PA4 | IN4 | Rank1 |
| 第6路 | ADC2 | PA5 | IN5 | Rank2 |
| 第7路 | ADC2 | PA6 | IN6 | Rank3 |
| 占位重复 | ADC2 | PA6 | IN6 | Rank4 |
最后一路故意让ADC2在第4个Rank再采一次IN6,两个ADC的序列长度都是4,DMA每次拿到4个32位字。解析时只需要前7个有效值,第4个字的"高16位"是重复的IN6,直接丢掉。这个技巧既保证了序列等长,又不需要额外占用一个引脚去接地做假通道。
2.3 时钟、采样时间和总耗时计算
F4的ADC时钟来自APB2,通过ADC_CCR里的ADCPRE分频得到,上限是36MHz。如果系统主频168MHz,APB2通常跑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。常用的外部通道如下:
| 引脚 | 通道 | 可用ADC |
|---|---|---|
| PA0 | IN0 | ADC1/2/3 |
| PA1 | IN1 | ADC1/2/3 |
| PA2 | IN2 | ADC1/2/3 |
| PA3 | IN3 | ADC1/2/3 |
| PA4 | IN4 | ADC1/2 |
| PA5 | IN5 | ADC1/2 |
| PA6 | IN6 | ADC1/2 |
| PA7 | IN7 | ADC1/2 |
| PB0 | IN8 | ADC1/2 |
| PB1 | IN9 | ADC1/2 |
| PC0 | IN10 | ADC1/2/3 |
选择引脚时除了看功能映射,还要留意和板上其他外设的冲突,比如PA0可能接了按键或者唤醒引脚,PA2/PA3可能被USART2占用。布局阶段先把引脚功能表拉出来过一遍,不然画完板子才发现引脚被占,返工成本很高。
3.2 推荐引脚分配与电路处理
我这次用的分配已经在第二节列出:ADC1用PA0~PA3,ADC2用PA4~PA6,全部集中在GPIOA的低位引脚,好处是初始化GPIO特别方便,一个GPIOA时钟打开,把所有引脚配成模拟输入就行。
硬件上几个关键点:
- 每个ADC输入引脚对地接一个100nF电容,滤高频噪声。如果被测信号变化速度不快,这个电容还能兼任抗混叠职责。
- 信号源内阻大的话,在引脚前加一级运放跟随器,否则采样电容充电不足会导致读数偏低且不稳定。运放选用通用单电源轨到轨型号即可,不需要多高端。
- VREF+直接连VDDA的话,参考电压就是电源电压,电源纹波会直接进ADC结果。要求高的场合用外部基准芯片,把VREF+接2.5V或3.0V精密基准。
- VDDA和VSSA必须单独走线并加去耦,常见的做法是1μF+100nF并联,模拟电源和数字电源之间用磁珠或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会使用DMA,ADC2的DMA请求在同步模式下不会单独开启。如果把两个都打开反而可能冲突。
4.3 双重ADC模式配置
在ADC1上调用多模式配置函数,把同步模式和DMA访问模式一起写进ADC_CCR:
ADC_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_CCR:MULTI[4:0]置为00001选择正则同步,DMA[1:0]置为10选择单DMA通道32位访问,DELAY[3:0]设为采样间延迟。TwoSamplingDelay这个参数在交织采样模式下才真正起效,同步模式下给个默认值就行。
这里有一个值得理解的点:为什么是HAL_ADCEx_而不是HAL_ADC_?因为双ADC属于扩展功能,配置的是两个ADC的公共寄存器,必须挂在主ADC(ADC1)上执行。如果把参数传给hadc2,函数内部会断言失败。
4.4 通道序列配置
通道配置要分别对两个ADC做。ADC1的四个Rank:
ADC_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访问模式。如果设成HALFWORD,DMA会按16位搬运,读回来的数据完全错乱。这是配置里最容易出错的地方之一,很多朋友卡在这一步半天,最后发现就是对齐方式写错。
中断方面,如果要每轮转换完成都得到通知,把DMA2_Stream0的中断打开:
HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);然后在中断服务函数里转发给HAL:
void 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%。看似差得不多,但如果你后面要做标定或者闭环计算,这点固定偏差会在整个链路里累积。
如果信号经过了运放或者分压网络,还要把分压比还原。比如100k+100k分压测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路 | 2482 | 2.0008 | +0.0008 |
| 第2路 | 2480 | 1.9992 | -0.0008 |
| 第3路 | 2483 | 2.0016 | +0.0016 |
| 第4路 | 2481 | 2.0000 | 0 |
| 第5路 | 2481 | 2.0000 | 0 |
| 第6路 | 2480 | 1.9992 | -0.0008 |
| 第7路 | 2482 | 2.0008 | +0.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 问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 所有通道读数恒为0 | GPIO未配模拟输入,或ADC未使能 | 检查GPIO配置、ADC时钟;用调试器看ADC_DR |
| 所有通道读数满量程4095 | 引脚悬空、输入短路到VREF或采到噪声 | 检查接线,用万用表测引脚电压 |
| 个别通道读数偏小且稳定 | 信号源内阻过大、采样时间不足 | 增大采样时间,或加运放跟随器 |
| 数据顺序错乱 | DMA字节/半字对齐配置错误 | 确认PeriphDataAlignment和MemDataAlignment都是WORD |
| 前几轮数据像垃圾值 | ADC刚上电未稳定 | 启动后丢弃前2~3轮数据 |
| 回调不触发 | 多模式回调函数名不对 | 检查HAL版本,HAL_ADC_ConvCpltCallback和MultiMode回调都挂上 |
| 第4个字高16位和预期不符 | 序列不等长或Rank4误配 | 确认ADC2的Rank4是重复IN6 |
| DMA启动后系统死机 | 缓冲区大小或对齐问题 | 确认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中心对齐触发电流采集),我会再写一篇专门说触发源选择和时序对齐的做法。