STM32F4多通道DMA ADC采集实战:从配置到避坑 简介这份资源面向STM32F4嵌入式开发者与学习者聚焦多通道ADC配合DMA进行高速数据采集这一典型场景帮助解决多路模拟信号采样时CPU占用高、实时性不足的问题。压缩包共103个文件以47个.h头文件与46个.c源文件为主体涵盖ADC、DMA、RCC、TIM、USART等标准外设驱动另含uvprojx与uvoptx工程文件、s启动文件、hex固件、png截图及bat脚本整体约846KB可直接导入Keil打开编译。内容围绕ADC初始化、DMA通道配置、ADC与DMA请求连接、连续转换启动及DMA传输完成中断处理等环节展开读者可据此掌握多通道采样序列设置、采样时间与分辨率选择、内存到外设的数据搬运机制并理解如何用DMA替代频繁中断以释放CPU资源。目前已有1515人学习下载适合需要快速搭建多路模拟量采集框架、提升系统实时响应能力的开发者参考借鉴。1. 从一次采样抖动说起STM32F4 多通道 DMA ADC 到底解决什么问题如果你用 STM32F4 做过 ADC 采集大概率遇到过这种场景单通道轮询读值挺稳一旦切到多通道、还要连续采样数据就开始飘通道之间还会串。问题往往不在 ADC 本身而在于采样节奏和搬运方式没设计好。这份资源给的就是一套 STM32F4 多通道 ADC DMA 的完整工程核心目标是把多个模拟通道的采样结果在不占用 CPU 的情况下自动搬进内存缓冲区让主循环只负责处理数据而不是守着寄存器等转换完成。它适合两类人一类是刚上手 STM32F4、想搞明白 ADC 多通道到底怎么配的开发者另一类是已经在做数据采集、电机控制、传感器阵列需要一套能直接跑、能改通道数和采样率的工程模板的人。下面我按“这工程怎么搭起来 → 怎么跑通 → 怎么改”的顺序把关键代码和踩坑点拆开讲。2. 工程结构与技术选型为什么是 ADC DMA 定时器触发2.1 目录结构与模块划分拿到这份工程第一件事是看目录。STM32F4 的工程结构通常比较固定标准库或 HAL 库的布局略有差异但核心文件跑不出这几类Project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── adc.h // ADC 初始化与通道配置 │ │ ├── dma.h // DMA 流配置 │ │ └── tim.h // 定时器触发源配置 │ └── Src/ │ ├── main.c // 主循环与数据处理 │ ├── adc.c │ ├── dma.c │ └── tim.c ├── Drivers/ │ ├── STM32F4xx_HAL_Driver/ │ └── CMSIS/ └── MDK-ARM/ 或 STM32CubeIDE/ └── 工程文件adc.c负责规则组通道顺序和采样时间dma.c负责把 ADC 数据寄存器搬到内存数组tim.c提供稳定的触发源。这三个文件是整套采集链路的核心main.c只做启动和结果处理。常见做法是把通道数、缓冲区大小做成宏定义放在头文件里改的时候只动一处。2.2 为什么不用轮询和中断轮询的问题很直接CPU 被绑死在 ADC 上多通道时还得手动切换通道、等 EOC 标志采样间隔完全取决于代码执行速度抖动大。中断方式好一点但每个通道转换完都进一次中断通道一多、采样率一高中断开销就上来了主循环被切得稀碎。DMA 的思路是让 ADC 转换完成后自动把数据写进内存CPU 完全不参与搬运。配合定时器触发采样时刻由硬件决定间隔精确、抖动小。这套组合在 STM32F4 上是标准打法F4 的 ADC 支持规则组多通道扫描DMA 支持循环模式两者一配合就是一套连续采集引擎。2.3 关键参数先定下来在写代码前有几个参数必须先想清楚它们决定了后面配置怎么写参数含义典型取值影响通道数规则组扫描的通道数量4决定 DMA 缓冲区长度和扫描序列采样时间每个通道的 ADC 采样周期84 或 144 cycles越大越稳但总转换时间变长触发源启动转换的信号TIM2 TRGO决定采样率是否精确DMA 模式循环或单次Circular循环模式适合连续采集数据宽度ADC 数据对齐12 位右对齐决定内存里怎么解析采样时间这块有个血泪经验信号源内阻大的时候采样时间设短了读数会明显偏低很多人以为是代码问题其实是采样电容没充够。F4 的 ADC 时钟一般配到 21MHz 或 30MHz采样时间选 84 周期以上比较稳妥。3. 核心配置拆解ADC 规则组、DMA 流和定时器触发怎么串起来3.1 ADC 多通道规则组配置先看 ADC 初始化。多通道的关键是规则组的扫描序列每个通道按顺序转换结果依次进数据寄存器DMA 再按顺序搬走。// adc.c void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig {0}; hadc1.Instance ADC1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; // ADC 时钟 PCLK2/4 hadc1.Init.Resolution ADC_RESOLUTION_12B; // 12 位分辨率 hadc1.Init.ScanConvMode ENABLE; // 多通道必须开扫描 hadc1.Init.ContinuousConvMode DISABLE; // 由定时器触发不连续 hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConvEdge ADC_EXTERNALTRIGCONVEDGE_RISING; hadc1.Init.ExternalTrigConv ADC_EXTERNALTRIGCONV_T2_TRGO; // TIM2 触发 hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 4; // 规则组 4 个通道 hadc1.Init.DMAContinuousRequests ENABLE; // 允许 DMA 持续请求 hadc1.Init.EOCSelection ADC_EOC_SEQ_CONV; // 序列转换完才置 EOC HAL_ADC_Init(hadc1); // 通道 0采样时间 84 周期 sConfig.Channel ADC_CHANNEL_0; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_84CYCLES; HAL_ADC_ConfigChannel(hadc1, sConfig); // 通道 1 sConfig.Channel ADC_CHANNEL_1; sConfig.Rank 2; HAL_ADC_ConfigChannel(hadc1, sConfig); // 通道 4 sConfig.Channel ADC_CHANNEL_4; sConfig.Rank 3; HAL_ADC_ConfigChannel(hadc1, sConfig); // 通道 5 sConfig.Channel ADC_CHANNEL_5; sConfig.Rank 4; HAL_ADC_ConfigChannel(hadc1, sConfig); }这里几个点值得说清楚。ScanConvMode必须开否则规则组只会转第一个通道。ContinuousConvMode关掉是因为我们要用定时器触发如果开了连续转换ADC 会自己一轮接一轮转定时器就失去意义了。NbrOfConversion要和实际配置的通道数一致写 4 就得配 4 个通道少配一个 DMA 缓冲区就会多搬一个无效值。EOCSelection选序列结束这样 DMA 请求是在整组转换完后统一触发配合 DMA 的循环模式正好一轮搬 4 个数据。3.2 DMA 流配置与循环模式DMA 这边要把外设地址、内存地址、数据长度、循环模式都设对。// dma.c void MX_DMA_Init(void) { __HAL_RCC_DMA2_CLK_ENABLE(); hdma_adc1.Instance DMA2_Stream0; // ADC1 常用 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_HALFWORD; hdma_adc1.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode DMA_CIRCULAR; // 循环模式连续采集 hdma_adc1.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_adc1); __HAL_LINKDMA(hadc1, DMA_Handle, hdma_adc1); }PeriphInc必须关因为 ADC 数据寄存器地址是固定的DMA 每次从同一个地址取数。MemInc必须开否则 4 个通道的数据会全堆在缓冲区第一个位置。数据宽度选半字因为 ADC 是 12 位用uint16_t存正好。循环模式是这套方案能连续跑的关键DMA 搬完一轮自动回到起点不需要软件重新启动。3.3 定时器触发与采样率计算定时器在这里的角色是节拍器它的 TRGO 输出接到 ADC 的外部触发输入。// tim.c void MX_TIM2_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig {0}; TIM_MasterConfigTypeDef sMasterConfig {0}; htim2.Instance TIM2; htim2.Init.Prescaler 8400 - 1; // 84MHz / 8400 10kHz 计数 htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 1000 - 1; // 10kHz / 1000 10Hz 触发 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(htim2); sClockSourceConfig.ClockSource TIM_CLOCKSOURCE_INTERNAL; HAL_TIM_ConfigClockSource(htim2, sClockSourceConfig); // 把更新事件映射到 TRGO作为 ADC 触发源 sMasterConfig.MasterOutputTrigger TIM_TRGO_UPDATE; sMasterConfig.MasterSlaveMode TIM_SLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(htim2, sMasterConfig); }采样率的算法是定时器时钟除以预分频得到计数频率再除以周期得到触发频率。上面配置里 84MHz 经 8400 分频得 10kHz周期 1000 得 10Hz也就是每秒触发 10 次每次采 4 个通道。如果要改采样率动Period就行比如改成 100-1 就是 100Hz。注意触发频率不能超过 ADC 完成一轮 4 通道转换所需的时间否则会丢触发。一轮转换时间大约是 4 个通道各 84 周期采样加 12 周期转换按 21MHz ADC 时钟算大概 20 微秒左右所以触发间隔要远大于这个值才稳。3.4 启动顺序与主循环处理配置都好了启动顺序不能乱先初始化 DMA再初始化 ADC最后启动定时器。// main.c #define ADC_CH_NUM 4 uint16_t adc_buf[ADC_CH_NUM]; // DMA 目标缓冲区 int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); // DMA 必须先于 ADC MX_ADC1_Init(); MX_TIM2_Init(); HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, ADC_CH_NUM); HAL_TIM_Base_Start(htim2); // 启动触发源 while (1) { // adc_buf 里的数据由 DMA 自动更新 // 这里直接读就行注意读取时可能正好在更新 uint16_t ch0 adc_buf[0]; uint16_t ch1 adc_buf[1]; // ... 处理数据 HAL_Delay(100); } }HAL_ADC_Start_DMA的第三个参数是搬运次数要和通道数一致。主循环里直接读adc_buf就行DMA 在后台默默更新。这里有个隐患如果主循环读的时候 DMA 正好在写可能读到半新半旧的数据。要求高的场景可以开 DMA 传输完成中断在中断里做双缓冲切换或者用__HAL_DMA_GET_COUNTER判断当前搬到哪了。4. 避坑与排查多通道 DMA ADC 最容易翻车的几个点4.1 通道之间数据串位现象是通道 0 的值跑到通道 1 的位置或者某个通道读数明显不对。原因通常是规则组里通道的 Rank 配错了或者NbrOfConversion和实际通道数不一致。DMA 是按转换顺序搬的Rank 顺序就是内存里的存放顺序。解决方法是把 Rank 从 1 开始连续编号NbrOfConversion等于通道总数改完重新核对一遍。4.2 只有第一个通道有数据现象是缓冲区里只有第一个值在变后面全是 0 或固定值。这基本是ScanConvMode没开ADC 只转规则组第一个通道。另一个可能是 DMA 的MemInc没开4 个数据全写到了缓冲区首地址。检查adc.c里ScanConvMode ENABLEdma.c里MemInc DMA_MINC_ENABLE。4.3 采样值偏小或不稳现象是读数比实际电压低或者跳动大。常见原因是采样时间太短信号源内阻大时采样电容充不满。把SamplingTime从 84 周期加到 144 甚至 480 周期试试。另外检查 ADC 时钟有没有超规格F4 的 ADC 时钟上限是 36MHz超了读数会乱。还有 VDDA 和 VREF 的滤波电容有没有接这个硬件问题经常被忽略。4.4 触发频率过高导致丢数据现象是数据更新速度对不上或者某些轮次的数据没变。原因是定时器触发频率超过了 ADC 一轮转换的时间ADC 还没转完新的触发就来了。算一下一轮转换时间把触发周期设成它的 2 倍以上。用示波器看 ADC 的 EOC 或者 DMA 请求信号最直观。4.5 DMA 传输完成中断进不去现象是开了中断但回调不执行。检查 DMA 流的中断有没有在 NVIC 里使能HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn)有没有调。另外循环模式下传输完成中断每轮都会触发如果中断里处理太慢会拖累采集建议中断里只做标志位切换数据处理放主循环。5. 二次开发改造从 4 通道到任意通道从固定采样到动态调整这套工程跑通之后真正有价值的是按自己需求改。我一般会从三个方向动手通道数、采样率、数据处理方式。改通道数最直接在adc.c里增删HAL_ADC_ConfigChannel调用同步改NbrOfConversion和main.c里的ADC_CH_NUM宏DMA 那边不用动因为搬运次数是启动时传进去的。注意 F4 的 ADC1 规则组最多 16 个通道超过就得用注入组或者多 ADC 协同。改完通道数记得重新算一轮转换时间触发频率要跟着调。采样率调整就是改tim.c里的Period值。我习惯把采样率做成一个宏比如#define SAMPLE_RATE_HZ 100然后用公式反推 Period这样改起来不容易算错。如果要做变速采样可以在运行时用__HAL_TIM_SET_AUTORELOAD(htim2, new_period)动态改周期但要注意改的瞬间可能产生一次异常触发最好先停定时器再改再启动。数据处理这块原始工程是主循环直接读缓冲区。实际项目里我建议改成双缓冲加 DMA 传输完成中断定义两个缓冲区DMA 填一个的时候处理另一个中断里切换。这样既不会读到半新半旧的数据也不会因为处理耗时影响采集。下面是一个双缓冲的骨架#define BUF_SIZE 64 uint16_t buf_a[BUF_SIZE]; uint16_t buf_b[BUF_SIZE]; volatile uint8_t active_buf 0; // DMA 传输完成回调 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (active_buf 0) { // buf_a 满了处理 buf_aDMA 继续填 buf_b process_data(buf_a, BUF_SIZE); HAL_ADC_Start_DMA(hadc, (uint32_t *)buf_b, BUF_SIZE); active_buf 1; } else { process_data(buf_b, BUF_SIZE); HAL_ADC_Start_DMA(hadc, (uint32_t *)buf_a, BUF_SIZE); active_buf 0; } }这个骨架里BUF_SIZE要是通道数的整数倍否则通道和缓冲区会对不齐。process_data里做实际的滤波、换算、上报。注意回调里重新启动 DMA 会有短暂间隙要求连续无间断的话得用 DMA 的双缓冲模式F4 的 DMA 支持双缓冲通过HAL_DMAEx_MultiBufferStart配置那个配置稍复杂但能做到真正无缝切换。还有一个实用技巧把 ADC 值换算成电压的公式封装成内联函数voltage raw * 3.3f / 4095.0f用浮点还是定点看 MCU 有没有 FPU。F4 有单精度 FPU直接用浮点没问题但如果在中断里频繁算还是定点快。我一般会在初始化时算好一个系数运行时只做乘法。从那以后我每次配多通道 ADC都强制先画一遍通道顺序和缓冲区布局的对应关系再动手写代码省得后面调半天发现是 Rank 配反了。希望这套工程和上面的拆解能帮你少走点弯路拿到手先跑通再按自己的通道数和采样率改一遍基本就能用到实际项目里了。本文还有配套的精品资源点击获取