资讯动态

STM32F411CEU6多通道ADC采集:扫描模式+DMA实现详解

发布时间:2026/8/30 0:03:33 来源:尧图企业网站定制
1. 多通道 ADC 的用武之地把“Multichannel ADC”和“STM32F411CEU6”这两个关键字放在一起其实就是嵌入式开发里最常遇到的一类需求用一块不算贵的 MCU同时采集多路模拟信号。STM32F411CEU6 是 48 引脚的 Cortex-M4F 主控主频能到 100MHz内部带了 3 个 12 位逐次逼近型 ADC外加上 DMA 支持和经典的扫描采样机制做多通道数据采集非常顺手。这篇文章不打算只贴代码我会把从硬件架构、CubeMX 配置、HAL 底层逻辑到实际调试中踩过的坑完整拆开讲一遍。适应什么场景比如四路 NTC 温度采集、电机驱动里的相电流和母线电压同时监测、电池管理系统里多节电芯电压巡检或者一个简单的手持仪表需要同时看三到四路传感器输出。这类需求共同点是采样通道多、要求数据连续、不希望 CPU 在每次转换完成后都被打断。那为什么偏偏用 STM32F411CEU6因为它在 LQFP48 封装下仍然能引出 16 个 ADC 外部输入通道2.4Msps 的理论采样率对绝大多数传感器足够功耗也低而且 HAL 库和 CubeMX 对这套外设支持很成熟网上随便一搜就是大量现成案例。真正把多通道 ADC 玩明白关键不是会点几个按钮而是搞清楚扫描模式、DMA 循环缓冲、采样时间、触发方式这些概念之间的关系。下面我从硬件底层开始讲。2. ADC 硬件资源与设计前置知识2.1 F411CEU6 的 ADC 基本规格STM32F411CEU6 内部有 3 个 ADC 外设编号 ADC1、ADC2、ADC3都是 12 位逐次逼近型SAR结构。它们的参考电压都来自 VDDA/VREF48 引脚封装里 VREF 和 VDDA 在内部是相连的所以实际参考电压就是模拟电源电压。每个 ADC 支持多达 16 个外部通道外加内部温度传感器通道和 VREFINT 参考电压通道。需要注意虽然芯片内部有 3 个 ADC但外部引脚并不充裕。F411CEU6 上实际可用的 ADC 输入通道如下表通道号引脚通道号引脚ADC_IN0PA0ADC_IN8PB0ADC_IN1PA1ADC_IN9PB1ADC_IN2PA2ADC_IN10PC0ADC_IN3PA3ADC_IN11PC1ADC_IN4PA4ADC_IN12PC2ADC_IN5PA5ADC_IN13PC3ADC_IN6PA6ADC_IN14PC4ADC_IN7PA7ADC_IN15PC5这几个通道对于多路传感器采集是相当充裕的。实际项目里最常用的组合是 PA0 到 PA3 接 4 路模拟量PB0、PB1 再接两路六路同时采样一点问题没有。但如果引脚同时被 SPI、I2C、USART 占用就需要在布局阶段提前规划。我的经验是先把模拟通道固定下来再做数字外设的引脚分配。2.2 SAR ADC 的转换周期是怎么算的要理解采样率就得先拆转换周期。SAR ADC 每次转换分两个阶段采样阶段和逐次逼近阶段。F411 的 ADC 时钟由 APB2 分频得到可配置为 2、4、6、8 分频。F411CEU6 在 100MHz 主频下APB2 默认是 50MHz所以 2 分频后就是 25MHz 的 ADC 时钟。每次转换需要“采样时间 12 个 ADC 时钟周期”。采样时间可以单独配置有 3、15、28、56、84、112、144、480 个周期可选。如果采样时间选 3 周期一整次转换就是 15 个 ADC 时钟周期那么单通道最大转换率 25MHz / 15 ≈ 1.67Msps如果是 4 个通道的扫描模式每轮扫描完成 4 次转换那么每个通道的实际采样率就降到1.67Msps / 4 ≈ 417ksps这个数据对绝大多数温度、压力、电压检测都足够了。但如果你要做音频或振动分析每个通道想要 100ksps 以上就得留意采样时间是否选得太长。选采样时间不能只看采样率还要看信号源的内阻和 PCB 走线阻抗后面精度部分我会展开讲。2.3 为什么要用扫描模式而不是逐个切换很多新手会写一个循环每次切换 ADC 通道、启动转换、等标志位、读数据。这种方式在单通道或极低速场景下能跑但有两个明显问题每次切换通道都要重新配置寄存器CPU 一直忙等 EOC 标志浪费大量算力。扫描模式下你只需要把要采的通道和顺序配置好触发一次后 ADC 会自动按顺序采完所有通道数据由 DMA 搬运到内存CPU 全程不用参与。这里的核心概念是“规则通道序列”。ADC 里有一套规则通道寄存器你把通道 0 放在 Rank1、通道 1 放在 Rank2、通道 2 放在 Rank3、通道 3 放在 Rank4转换时硬件就按这个顺序依次执行。配合 DMA结果会按照同样的顺序连续写入内存缓冲区数据排列天然整齐。3. 多通道采样整体方案设计3.1 为什么必须用 DMA我在评论区见过不少新手问“不用 DMA 直接读可以吗”可以但不推荐。ADC 转换完成后如果没有用 DMA每个通道转换完都要进一次中断或者 CPU 轮询 EOC 标志。假设每通道 20ksps、共 4 路那 CPU 每秒钟要处理 8 万次中断或查询。虽然也能跑但中断处理函数里有压栈、出栈、标志清理这些开销对系统实时性影响很大。而 DMA 是硬件搬运数据从 ADC 数据寄存器搬到内存不占用 CPU 时间。F411 的 ADC1 可以映射到 DMA2 的 Stream0 或 Stream4Channel 固定为 0。CubeMX 会自动帮你选择 Stream0配成外设到内存方向、循环模式、半字长度这样一个 DMA 通道就能同时服务多路 ADC 数据。3.2 连续转换与 DMA 循环模式的配合多通道 ADC 的经典配置是“扫描模式开启 连续转换开启 DMA 循环模式”。扫描模式负责一轮内按顺序采完所有通道连续转换负责采完一轮马上开始下一轮DMA 循环模式负责把数据源源不断搬进内存缓冲区并且缓冲区满了自动从头开始覆盖。这三者配合最终在内存里呈现的是一个连续的数据流顺序大概是通道0、通道1、通道2、通道3、通道0、通道1、通道2、通道3……这给数据解析提供了很大方便。你只要知道一轮有多少个通道就能从缓冲区里按固定步长把每个通道的数据抽出来。3.3 缓冲区设计思路缓冲区大小的选择有讲究。最简单的做法是设一个长度为“通道数”的小缓冲区DMA 每搬完一轮就产生一次传输完成中断在中断里取走数据。这种方案的问题是中断频率太高而且 DMA 回绕速度和中断执行速度之间容易出竞争。更好的做法是使用一个较大的环形缓冲区利用 DMA 的半传输和传输完成中断实现类似乒乓缓冲的效果。比如 4 通道、每轮 4 个数据设缓冲区长度为 256 个半字64 轮完整数据DMA 每搬满 128 个半字产生一次半传输中断再搬满另外 128 个半字产生一次传输完成中断。两个中断交替处理缓冲区的前半段和后半段这样在处理数据的同时DMA 还在往另一段缓冲区写入互不冲突。4. STM32CubeMX 配置过程4.1 时钟树配置打开 CubeMX芯片选 STM32F411CEU6。第一步先把时钟配好。如果板子上有 8MHz 晶振RCC 里选 HSE 外部晶振然后在 Clock Configuration 页面把 SYSCLK 配到 100MHzAPB2 自然就是 50MHz。ADC 预分频器选 /2得到 25MHz 的 ADC 时钟。如果板子上没有外部晶振那就只能用 HSI 16MHz通过 PLL 倍频到 96MHz 左右也能用只是精度会差一些。做多通道 ADC 采集外部晶振还是建议装一个尤其当你后续想用定时器做精确采样触发时时钟源精度直接影响采样间隔。4.2 ADC 参数配置在 Analog 选项卡里打开 ADC1勾选你需要的通道。假设我要采 4 路分别是 PA0、PA1、PA2、PA3那就分别勾选 IN0、IN1、IN2、IN3。参数区按下面设置ModeIndependent ModeScan Conversion ModeEnabledContinuous Conversion ModeEnabledDiscontinuous Conversion ModeDisabledDMA Continuous RequestsEnabledEnd of Conversion SelectionSequencerNumber Of Conversion4External Trigger SourceSoftware TriggerExternal Trigger EdgeNone通道顺序是在每个通道的子项里设置的。Rank 1 对应通道 0Rank 2 对应通道 1以此类推。每个通道还有独立的采样时间如果没有特殊要求先全部选 3 Cycles后面发现采样值不稳再逐个加大。这里有一个容易混淆的点Continuous Conversion Mode 和 DMA Continuous Requests 是两回事。前者是 ADC 自己连续启动扫描后者是 DMA 工作在循环模式。在 CubeMX 里如果只开连续转换、不开 DMA 连续请求DMA 传输一轮后就会停止需要手动重新启动这是个很容易踩的坑。4.3 DMA 配置直接在 CubeMX 左侧列表里点 DMA Settings添加 DMA Request 为 ADC1。方向选 Peripheral To MemoryMode 选 Circular外设数据宽度和内存数据宽度都选 Half Word。优先级建议选 High因为 ADC 数据流不允许丢失优先级太低可能被其他 DMA 请求抢占。在 NVIC 选项卡里要确保 DMA2 Stream0 的全局中断是 Enabled。没有这个中断后续你在代码里写的回调函数就永远不执行。很多新手配完 DMA 发现回调不触发八成是这里没勾。4.4 生成工程的代码结构CubeMX 生成的代码里adc.c 会包含 ADC 初始化函数和每个通道的配置dma.c 会包含 DMA 初始化函数。函数调用关系在 main.c 里是MX_DMA_Init(); MX_ADC1_Init();这两个初始化本身不需要你改但默认并不会开启 ADC 采样。你要在 main 函数里手动加上校准和启动 DMA 的代码下面第五节我会给完整示例。5. HAL 库代码与数据处理实现5.1 启动校准和 DMA 采样F4 系列带有 ADC 出厂校准功能启动转换前建议先校准一次。代码很简单HAL_ADCEx_Calibration_Start(hadc1); HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_dma_buf, ADC_BUF_LEN);注意HAL_ADC_Start_DMA的第二个参数是 uint32_t 指针但实际数据是 16 位的所以强制转换一下即可。第三个参数是 DMA 传输的数据单元个数不是字节数。缓冲区定义为 uint16_t 数组长度就是数组元素个数。校准的目的是消除 ADC 内部的失调误差。如果你发现采集到的信号在零输入时读数不为零先检查是不是漏了校准。5.2 缓冲区与处理函数实现这里我给出一个可直接用的多通道数据处理框架。假设通道数是 4缓冲区总长度 256每个 DMA 中断处理 128 个半字#define ADC_CH_NUM 4U #define ADC_GROUP_LEN (ADC_CH_NUM * 32U) /* 128个半字即32轮完整扫描 */ static uint16_t adc_dma_buf[ADC_GROUP_LEN * 2U]; static uint32_t adc_acc[ADC_CH_NUM]; static uint32_t adc_group_cnt; static void ProcessAdcSamples(const uint16_t *buf, uint32_t len) { uint32_t offset 0; uint32_t total_groups len / ADC_CH_NUM; while (offset ADC_CH_NUM len) { for (uint32_t ch 0; ch ADC_CH_NUM; ch) { adc_acc[ch] buf[offset ch]; } offset ADC_CH_NUM; } adc_group_cnt total_groups; } void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { ProcessAdcSamples(adc_dma_buf[0], ADC_GROUP_LEN); } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { ProcessAdcSamples(adc_dma_buf[ADC_GROUP_LEN], ADC_GROUP_LEN); } }这段代码的逻辑是DMA 先搬满前半段缓冲区触发半传输中断我处理前半段DMA 继续搬后半段触发传输完成中断我处理后半段。因为两段缓冲区不重叠所以 DMA 往后半段写的时候前半段是稳定的反之亦然。5.3 主循环取平均结果在主循环里读取累加结果并换算成电压while (1) { uint32_t acc_copy[ADC_CH_NUM]; uint32_t cnt_copy; float voltage[ADC_CH_NUM]; __disable_irq(); for (uint32_t ch 0; ch ADC_CH_NUM; ch) { acc_copy[ch] adc_acc[ch]; } cnt_copy adc_group_cnt; __enable_irq(); if (cnt_copy 0) { for (uint32_t ch 0; ch ADC_CH_NUM; ch) { float avg_raw (float)acc_copy[ch] / (float)cnt_copy; voltage[ch] avg_raw * 3.3f / 4095.0f; } } HAL_Delay(100); }为什么要在中断关闭状态下拷贝数据因为中断回调里还在累加主循环直接读取 adc_acc 可能会读到一半写一半的数据。拷贝到局部变量后即使中断继续累加也不影响本次计算的一致性。这是多线程共享变量最常见的数据竞争问题嵌入式里同样适用。5.4 一个容易忽略的细节回调函数里处理的样本数与中断触发时机要严格匹配。如果你在 CubeMX 里把 DMA 缓冲区长度设成了 128但代码里回调又按 256 去处理就会越界访问。我的习惯是缓冲区长度全部用宏定义CubeMX 配置、代码声明、回调处理三处都引用同一个宏避免手改不一致。还有一个细节是 DMA 计数寄存器NDTR。调试时如果怀疑 DMA 搬运数量不对可以直接读__HAL_DMA_GET_COUNTER(hdma_adc1)看还有多少数据没搬完。这是在 debug 模式排查问题非常好用的手段。6. 精度、噪声与校准经验6.1 采样时间怎么选很多人把采样时间固定设为 3 Cycles然后抱怨 ADC 读数跳动厉害。原因很简单信号源内阻过大采样电容还没充满就开始转换了。SAR ADC 的采样过程相当于通过一个内部电阻给采样电容充电外部信号源内阻越大充电时间常数越大。如果采样窗口太短电容电压还没追平输入电压转换出来的结果自然偏低且不稳定。我的经验是从运放输出直接驱动 ADC 的3 Cycles 就够接分压电阻网络的建议至少 15 Cycles如果是高阻传感器直接接在引脚上比如光敏电阻、热敏电阻用 84 甚至 480 Cycles 更稳妥。采样时间加长对采样率的损失用上一节提到的均值滤波完全可以补偿回来。6.2 均值滤波和过采样在 12 位 ADC 上追求更高有效位数常见做法是过采样。但要注意直接用 DMA 累加再求平均只能降低随机噪声并不能真正提高分辨率。做 4 倍过采样平均有效分辨率大概提升 1 位做 16 倍过采样平均大概提升 2 位。实际项目中我建议不要盲目追求位数的名义提升关键是先去掉明显噪声源。比如开关电源给模拟部分供电时ADC 的参考电压会跟着纹波跳动这时候采样结果就像在水面上漂着的船无论怎么平均都无法消除参考电压本身抖动带来的误差。解决办法是给 VDDA 引脚加磁珠和去耦电容模拟部分单独用低噪声 LDO 供电模拟地与数字地单点连接。6.3 校准的意义和局限F411 自带的校准可以消除内部失调误差但校准温度变化后会漂移所以对精度要求高的系统最好在每次上电或环境温度明显变化后重新校准。执行校准前要确保 ADC 已经上电并且时钟使能否则可能死等在校准函数里。还有一个指标叫有效位数ENOB。12 位 ADC 的 ENOB 通常也就 10 到 11 位信噪比越高ENOB 越接近 12。实际设计时与其看数据手册里的理想信噪比不如用一块已知精度的信号源直接测量比如用高精度万用表同时读输入电压和 ADC 输出画一条实际传递曲线看线性度和增益误差。这个方法在量产前做一次能发现很多 datasheet 上看不出来的问题。6.4 参考电压不稳怎么办F411CEU6 在这个封装里没有独立 VREF 引脚参考电压和 VDDA 是同一个引脚。这意味着如果你用 3.3V 给 MCU 供电ADC 参考就是 3.3V而这个 3.3V 还会给其他数字电路供电。数字电路高速翻转时电源上会产生微小的压降和毛刺直接反映在 ADC 结果里。解决思路有两个方向。第一个方向是让电源更干净给 VDDA 加滤波数字部分和模拟部分供电分开。第二个方向是软件补偿用一个已知基准电压通道同时采样然后用这个通道的读数反推实际参考电压。F411 内部有 VREFINT可以采样这个内部基准来校准参考电压漂移。这个技巧在电池供电、电压随电量下降的场景下特别有用。7. 定时器触发采样和多 ADC 同步扩展7.1 为什么需要定时器触发连续转换模式适合低速、实时性要求不高的数据采集。但有些场景要求采样间隔严格等间隔比如电机 FOC 电流环、音频采样、振动分析。这时候连续转换模式就有问题因为 ADC 转换完成后立刻开始下一次转换每次转换之间的间隔完全依赖转换时间而转换时间又涉及时钟抖动和中断延迟无法保证周期绝对均匀。解决办法是用定时器的 TRGO 事件作为 ADC 的外部触发源。在 CubeMX 里ADC 的 External Trigger Source 选择某个定时器的 Trigger Out event同时把定时器配置成你想要采样频率的 PWM 输出模式。定时器到达更新事件时自动触发 ADC 启动一次转换或一轮扫描采样间隔完全由定时器硬件决定和 CPU 负载无关。7.2 CubeMX 配置方法假设用 TIM2 做触发源ADC 触发选择 Timer 2 Trigger Out event边沿选 Rising。TIM2 的时钟源选 Internal ClockPrescaler 和 Counter Period 按目标采样率计算。例如 APB1 定时器时钟 50MHz想要 10kHz 采样那么分频系数 50MHz / 10000 5000如果 Precaler 0Counter Period 就设 4999输出 10kHz 的更新事件。这样每 100 微秒触发一次 ADC。DMA 缓冲区的使用方式和连续模式一样定时器触发模式下启动后DMA 依然以循环模式搬运数据到内存。7.3 双 ADC 同步采集如果应用中需要同时采样两路信号且要求两路采样时刻完全一致比如三相电流检测那么可以尝试双 ADC 同步模式。F411 支持 ADC1 和 ADC2 在同步模式下工作两个 ADC 同时触发、同时转换转换结果可以选择交错存储在同一个 DMA 缓冲区里。CubeMX 里把 ADC1 的 Mode 选为 Dual regular simultaneous modeADC2 会自动同步配置。这种模式的优点是硬件时间戳完全相同避免了在同一 ADC 上分时采样带来的相位差。但代码复杂度也明显上升数据排列是通道间交错还是按规则序列排列需要仔细看参考手册。我的建议是先跑通单 ADC 多通道方案再考虑双 ADC 扩展否则问题叠加很难排查。8. 常见问题与调试经验8.1 问题速查表现象可能原因排查方向采样值全部为 0DMA 未启动或采样时间不足检查是否调用 HAL_ADC_Start_DMA用调试器看 adc_dma_buf 内容只有第一个通道有数据扫描模式未开启或 Number Of Conversion 没设够确认 ScanConvMode 为 ENABLENbrOfConversion 等于通道数数据顺序和通道顺序对不上Rank 顺序配置与预期不一致在 CubeMX 里逐个核对每个通道的 RankDMA 回调不执行NVIC 中 DMA 中断未使能检查 DMA2_Stream0 全局中断是否勾选采样值跳变明显信号源阻抗过高、电源噪声加长采样时间加强 VDDA 滤波半传输回调处理数据错乱缓冲区长度与回调处理长度不一致统一宏定义检查 len 参数是否被修改连续模式下只采一轮就停止DMA Continuous Requests 未开启配置里打开 DMA Continuous Requests校准函数卡住ADC 时钟未使能或 ADC 被停止先确认 RCC 里 ADC 时钟分频配置再确认校准前没有调用 Stop8.2 我踩过的几个坑第一个坑是 DMA 中断和 ADC 中断混用。F4 系列的 ADC 本身也有中断比如转换结束中断、模拟看门狗中断。有些新手在 CubeMX 里把 ADC 全局中断和 DMA 中断都开了然后在 HAL_ADC_IRQHandler 和 DMA IRQHandler 里重复处理结果一个数据被统计两次。正确的做法是DMA 搬运模式下主要靠 DMA 中断回调ADC 的转换结束中断不打开让它安安静静地把数据交给 DMA。第二个坑是半传输回调和传输完成回调的先后顺序。DMA 循环模式下先触发半传输回调再触发传输完成回调。如果你把两个回调里处理的数据段搞反了数据处理顺序就乱。调试时可以在回调里加一个临时断点或者翻转 GPIO 电平用示波器看两个回调的执行节奏。第三个坑和 DMA 缓冲区大小有关。DMA 传输的半字数量和缓冲区的字节长度经常被混淆。设缓冲区为 uint16_t 数组长度 256那么 DMA 配置里的 Data Length 是 256而不是 512。如果误设成 512DMA 会越界写入程序大概率跑飞而且这种错误很难通过看代码发现。8.3 调试工具和手段多通道 ADC 这类问题最有效的调试工具其实就是三段第一段用调试器在回调函数里设置断点确认回调频率和实际采样频率匹配第二段把采集到的原始值通过串口或虚拟示波器工具发到 PC 端画出来看波形第三段用信号发生器输入已知幅值的正弦波对比 ADC 还原出来的波形幅值和相位。如果你手头没有示波器也可以先在内存里填一段已知数据模拟一个假信号源比如正弦波查表然后走同样的 DMA 路径确认数据处理链路是通的。这个方法可以帮你把“ADC 硬件问题”和“数据处理代码问题”分开排查非常节省时间。9. 关于多通道 ADC 的一点个人体会做了不少多通道 ADC 的板子我最深的体会是通道数量本身从来不是难点难的是每个通道都有不同的信号特性。有的通道接的是低阻电压源采样时间可以设得很短有的通道接的是高阻传感器采样时间必须拉长还有的通道信号本身就带高频噪声需要在软件里做滤波。把整条链路当成一个系统去看比单独调某个参数重要得多。另一个体会是CubeMX 生成代码后一定要自己读一遍 adc.c 和 dma.c 里的初始化逻辑不要直接跳到写业务代码。HAL 库的初始化顺序、DMA 请求映射、中断优先级这些信息全都在生成代码里写着。我见过不少项目工作得好好的但在 CubeMX 里重新生成一次代码后突然 DMA 不工作了原因就是新的配置把某个中断优先级或 DMA 流悄悄改掉了。这套“扫描模式 DMA 循环 半传输回调”的方案是我做 F411 多通道采集时会优先推荐的基础模板。它不挑具体应用温度采集能用电流采样能用电池电压巡检也能用。你只需要根据实际信号特征调整采样时间、缓冲长度和均值次数剩下交给 DMA 去跑。

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

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

免费获取报价