STM32F4多通道ADC+DMA采集实战:配置、避坑与定时器触发
简介这份资源面向STM32F4嵌入式开发者与学习者聚焦多通道ADC与DMA协同采集这一实战主题帮助解决多路模拟信号高速采样时CPU占用高、实时性不足的问题。压缩包共103个文件以47个.h头文件与46个.c源文件为主体涵盖ADC、DMA、RCC、TIM、USART等标准外设驱动另含uvprojx与uvoptx工程文件、hex烧录文件、s启动文件及少量png说明图整体约846KB可直接导入Keil打开编译。内容围绕ADC初始化、DMA通道配置、ADC与DMA请求连接、连续转换启动及DMA传输完成中断处理等环节展开读者可据此掌握多通道采样序列设置、内存到外设的数据搬运机制与中断回调逻辑理解如何用DMA替代频繁中断以释放CPU资源。目前已有1514人学习下载适合需要搭建多路模拟量采集框架、提升系统实时响应能力的开发者参考与二次开发。1. 多通道 ADC DMA 这套组合到底解决了什么工程痛点如果你用 STM32F4 做过模拟量采集大概率经历过这样的场景主循环里轮询 ADC 的 EOC 标志采一路电压还行一旦要同时盯 4 路、8 路传感器CPU 就被拖死在等待转换结果上稍微加点串口打印或者屏幕刷新采样节拍立刻乱掉。STM32F4 多通道 ADC DMA 这套方案核心就是用 DMA 把 ADC 的转换结果自动搬运到内存数组CPU 只在半满/全满中断里处理数据彻底把轮询的活卸下来。它适合做电机电流采样、多路温度巡检、电池组电压监测这类需要连续、等间隔、多通道轮转采集的场合。这份资源包给的就是一套能直接编译、能跑通、能改通道数的工程骨架不是零散代码片段。2. 拆开资源包工程结构、时钟树与 ADC 规则组配置拿到一个 STM32F4 的 ADCDMA 工程别急着烧录先花十分钟把结构看清楚后面改通道、改采样率才不会翻车。这个资源包是典型的 HAL 库工程目录结构遵循 CubeMX 生成的习惯但代码里做了手动整理方便直接读。2.1 工程目录与关键文件定位解压后你会看到类似这样的布局STM32F4_MultiADC_DMA/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── adc.h │ │ └── dma.h │ └── Src/ │ ├── main.c │ ├── adc.c │ ├── dma.c │ └── stm32f4xx_it.c ├── Drivers/ │ ├── STM32F4xx_HAL_Driver/ │ └── CMSIS/ ├── MDK-ARM/ (Keil 工程文件) └── STM32F4_MultiADC_DMA.ioc真正要动的是adc.c、dma.c和main.c三个文件。.ioc文件是 CubeMX 的配置存档如果你习惯用 CubeMX 重新生成改完.ioc再生成代码即可如果不想装 CubeMX直接手改adc.c里的初始化结构体也能达到同样效果。我一般建议保留.ioc因为 ADC 的规则组序列、DMA 请求映射这些配置项在图形界面里改比翻寄存器手册快得多。2.2 时钟树与 ADC 时钟分频的硬约束STM32F4 的 ADC 时钟来自 APB2经过一个可配置的分频器2/4/6/8。这里有个硬约束ADC 时钟不能超过 36MHz否则转换结果会飘甚至直接锁死。假设你的系统主频跑 168MHzAPB2 通常是 84MHz那么分频系数至少选 4得到 21MHz 的 ADC 时钟。这个值直接决定单次转换时间T_conv (采样周期 12) / ADC_CLK12 是 12 位分辨率下的固定转换周期数。如果采样周期设为 84 个 ADC 时钟周期那么单通道转换时间就是 (8412)/21MHz ≈ 4.57μs。多通道轮转时总扫描时间 通道数 × 单通道时间。这个公式你在改采样率时必须心里有数不然会出现“明明配了 8 路怎么总采样率只有几十 kHz”的困惑。2.3 规则组序列与 DMA 请求的绑定关系多通道采集的关键在于 ADC 的规则组序列Regular Sequence。STM32F4 的 ADC1 最多支持 16 个规则通道每个序列位置可以填任意通道号。配置时要注意两点第一序列长度必须和你要采的通道数一致第二DMA 请求要在 ADC 启动前使能否则第一次转换结果会丢。下面这段是adc.c里规则组配置的核心代码我加了注释说明每个参数的含义// ADC1 规则组配置4 个通道扫描模式连续转换 ADC_ChannelConfTypeDef sConfig {0}; // 规则组总长度 4即每次扫描采 4 个通道 hadc1.Init.ScanConvMode ENABLE; // 扫描模式必须开否则只采第一个通道 hadc1.Init.ContinuousConvMode ENABLE; // 连续转换配合 DMA 做循环搬运 hadc1.Init.DiscontinuousConvMode DISABLE; // 不用间断模式保持连续流 hadc1.Init.NbrOfConversion 4; // 规则组序列长度 hadc1.Init.DMAContinuousRequests ENABLE; // DMA 循环请求缓冲区满了自动从头覆盖 hadc1.Init.EOCSelection ADC_EOC_SEQ_CONV;// 序列转换完成才置 EOC减少中断频率 // 通道 0采样周期 84 个时钟序列位置 1 sConfig.Channel ADC_CHANNEL_0; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_84CYCLES; HAL_ADC_ConfigChannel(hadc1, sConfig); // 通道 1序列位置 2 sConfig.Channel ADC_CHANNEL_1; sConfig.Rank 2; HAL_ADC_ConfigChannel(hadc1, sConfig); // 通道 4序列位置 3 sConfig.Channel ADC_CHANNEL_4; sConfig.Rank 3; HAL_ADC_ConfigChannel(hadc1, sConfig); // 通道 5序列位置 4 sConfig.Channel ADC_CHANNEL_5; sConfig.Rank 4; HAL_ADC_ConfigChannel(hadc1, sConfig);逻辑说明ScanConvMode打开后ADC 会按照 Rank 顺序依次转换ContinuousConvMode让 ADC 在一轮扫描结束后立刻开始下一轮不需要软件再次触发DMAContinuousRequests是关键它让 DMA 在缓冲区满后自动回到起始地址继续搬形成循环缓冲。参数怎么改如果你要采 8 路把NbrOfConversion改成 8然后依次添加 Rank 5 到 Rank 8 的通道配置即可。采样周期根据信号源阻抗调整高阻抗信号源用 144 或 480 个周期低阻抗的可以用 15 或 28 个周期换速度。2.4 DMA 流配置循环模式与数据宽度的匹配DMA 这边最容易出问题的是数据宽度和外设地址对齐。ADC 的 DR 寄存器是 32 位但有效数据只有低 12 位所以 DMA 的外设数据宽度设成半字16 位就够了内存数据宽度也设半字这样uint16_t adc_buffer[4]正好对应 4 个通道。// DMA2 Stream0 配置对应 ADC1 的 DMA 请求 hdma_adc1.Instance DMA2_Stream0; hdma_adc1.Init.Channel DMA_CHANNEL_0; // ADC1 固定映射到通道 0 hdma_adc1.Init.Direction DMA_PERIPH_TO_MEMORY; // 外设到内存 hdma_adc1.Init.PeriphInc DMA_PINC_DISABLE; // 外设地址不递增始终读 DR 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); // 把 DMA 句柄绑定到 ADC 句柄 __HAL_LINKDMA(hadc1, DMA_Handle, hdma_adc1);这里DMA_CIRCULAR是灵魂参数。如果设成DMA_NORMALDMA 搬完指定长度就停了你得在中断里手动重启采样就会出现间隙。循环模式下DMA 自己回卷CPU 只需要在半满和全满中断里取数据。优先级建议给 High因为 ADC 数据流对时序敏感被其他 DMA 请求打断可能导致溢出。3. 从零跑通初始化顺序、校准与中断取数配置写对只是第一步启动顺序错了照样采不到数。STM32F4 的 ADC 上电后必须做校准否则零点和增益误差会让你的电压读数偏出几十毫伏。这一章把启动流程拆成可抄的步骤顺带把中断取数的框架搭起来。3.1 上电初始化与 ADC 校准的固定套路HAL 库把校准封装成了HAL_ADCEx_Calibration_Start但调用时机有讲究必须在 ADC 使能之前调用而且校准期间不能启动转换。我一般把初始化顺序固定成下面这样基本不会出玄学问题// 1. 初始化 DMA必须先于 ADC因为 ADC 初始化里会链接 DMA 句柄 MX_DMA_Init(); // 2. 初始化 ADC 外设GPIO、时钟、参数 MX_ADC1_Init(); // 3. 执行 ADC 校准注意此时 ADC 还没使能 if (HAL_ADCEx_Calibration_Start(hadc1) ! HAL_OK) { Error_Handler(); // 校准失败通常是时钟没配好或供电不稳 } // 4. 启动 DMA 传输指定缓冲区地址和长度 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, 4); // 5. 此时 ADC 已经开始连续转换DMA 自动搬运逻辑说明HAL_ADC_Start_DMA内部会依次做三件事——使能 DMA 流、使能 ADC、触发软件启动转换。所以你不需要再手动调HAL_ADC_Start。参数4是转换次数对应 4 个通道DMA 每搬完 4 个半字就会触发一次全满中断如果开了的话。常见错误是先把 ADC 使能了再校准这时候校准会返回错误但很多人不看返回值结果读数一直偏。3.2 半满/全满中断里怎么取数循环模式下DMA 会在缓冲区搬到一半时触发半满中断搬完时触发全满中断。利用这两个中断可以做“乒乓缓冲”半满时处理前半段全满时处理后半段这样 CPU 处理数据和 DMA 搬运数据互不打架。// 在 main.c 里定义缓冲区4 个通道各一个半字 uint16_t adc_buffer[4]; // DMA 全满回调HAL 库的弱函数需要用户重写 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc-Instance ADC1) { // 此时 adc_buffer[0..3] 分别对应 Rank1..Rank4 的通道值 uint16_t ch0 adc_buffer[0]; // 通道 0 uint16_t ch1 adc_buffer[1]; // 通道 1 uint16_t ch4 adc_buffer[4-2]; // 通道 4注意索引 uint16_t ch5 adc_buffer[3]; // 通道 5 // 在这里做滤波、换算或打包发送 } } // 半满回调同理处理前半段数据 void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc-Instance ADC1) { // 前半段数据就绪可以做另一组处理 } }逻辑说明回调函数里不要做耗时操作比如浮点运算或串口打印。我一般只做数据拷贝到另一个缓冲区或者置一个标志位让主循环处理。参数adc_buffer的索引和 Rank 顺序严格对应Rank1 的数据在[0]Rank2 在[1]以此类推。如果你改了 Rank 顺序索引也要跟着改这是最容易翻车的地方。3.3 电压换算与参考电压的实测修正ADC 读出来是 0 到 4095 的整数换算成电压需要参考电压。STM32F4 的 VREF 通常接 3.3V但实际值可能在 3.28 到 3.32 之间波动。如果你做电池电压监测这个偏差会直接反映在结果里。我的习惯是用万用表量一下 VREF 的实际电压然后在代码里用实测值// 实测 VREF 3.296V比标称 3.3V 低 4mV #define VREF_ACTUAL 3.296f #define ADC_RES 4096.0f float adc_to_voltage(uint16_t raw) { return (raw / ADC_RES) * VREF_ACTUAL; }对于 12 位 ADC4096 是满量程。如果你开了过采样或者用了 10 位分辨率这个分母要相应改。另外如果信号经过分压电阻换算时还要乘回分压比。这些细节看着小但决定了你的读数能不能直接拿去用。4. 避坑与排查多通道 DMA 采集的五个血泪经验这套方案原理不复杂但实际调试时踩的坑一个比一个隐蔽。下面五条是我和身边同事反复遇到的每条按“现象 → 原因 → 解决”写清楚你遇到类似情况可以直接对号入座。4.1 只有第一个通道有数据后面全是零现象adc_buffer[0]有值[1]、[2]、[3]始终是 0 或者不变。原因ScanConvMode没开或者NbrOfConversion设成了 1。ADC 只转换了 Rank1 就停了DMA 搬来搬去都是同一个值。解决检查hadc1.Init.ScanConvMode是否为ENABLENbrOfConversion是否等于实际通道数。改完记得重新生成代码或手动改初始化结构体。4.2 数据跳变严重像是有随机噪声现象同一路稳定电压读出来在几十个 LSB 范围内乱跳。原因采样周期太短ADC 内部采样电容没充到信号电压。尤其是信号源内阻大的时候比如 NTC 热敏电阻分压内阻可能到几十 kΩ。解决把SamplingTime从ADC_SAMPLETIME_15CYCLES改成ADC_SAMPLETIME_144CYCLES或480CYCLES。代价是单通道转换时间变长总采样率下降但数据稳定性会明显改善。4.3 DMA 传输一半就停了不再更新现象程序跑几秒后adc_buffer不再变化调试器看 DMA 流已经 Disable。原因DMA 模式设成了DMA_NORMAL而不是DMA_CIRCULAR搬完指定长度后自动停止。或者发生了 DMA 传输错误比如内存地址越界错误标志置位后流被硬件关闭。解决确认hdma_adc1.Init.Mode DMA_CIRCULAR。如果已经是循环模式检查adc_buffer的数组长度是否和HAL_ADC_Start_DMA的第三个参数一致长度不匹配会导致 DMA 越界。4.4 中断进得太频繁主循环被拖死现象系统响应变慢串口输出卡顿甚至看门狗复位。原因EOCSelection设成了ADC_EOC_SINGLE_CONV每个通道转换完都触发中断4 个通道就是 4 倍中断频率。加上 DMA 半满/全满中断CPU 大部分时间在进中断。解决把EOCSelection改成ADC_EOC_SEQ_CONV只在整组序列转换完成后才置 EOC。同时评估 DMA 中断频率如果采样率很高可以只开全满中断半满中断关掉用更大的缓冲区换更低的 CPU 占用。4.5 校准通过但读数整体偏大或偏小现象所有通道读数都比实际值高或低一个固定比例。原因VREF 实际电压和代码里用的 3.3V 不一致或者 ADC 校准没做、做的时候 ADC 已经使能了。解决先用万用表量 VREF 引脚的实际电压更新换算公式里的VREF_ACTUAL。然后确认校准调用在HAL_ADC_Start_DMA之前且校准函数返回HAL_OK。如果校准返回错误检查 ADC 时钟是否超过 36MHz。5. 进阶技巧用定时器触发 ADC 实现精确采样率前面讲的连续转换模式采样率由 ADC 时钟和采样周期决定但实际值受中断响应、DMA 仲裁影响不够精确。如果你要做 10kHz 等间隔采样做 FFT 分析或者要跟 PWM 同步采电机电流就得用定时器触发 ADC。这个技巧在资源包里没有直接体现但工程骨架支持你加上去。5.1 定时器 TRGO 触发 ADC 的配置链路思路是用一个定时器比如 TIM2产生固定频率的 TRGO 信号ADC 配置成外部触发模式触发源选 TIM2_TRGO。这样每次定时器溢出ADC 自动启动一轮扫描DMA 照常搬运。采样率完全由定时器决定精度取决于晶振。// TIM2 配置84MHz / (84 * 1000) 1kHz 触发频率 htim2.Instance TIM2; htim2.Init.Prescaler 84 - 1; // 预分频到 1MHz htim2.Init.Period 1000 - 1; // 1MHz / 1000 1kHz htim2.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_Base_Init(htim2); // 配置 TRGO 输出更新事件触发 TIM_MasterConfigTypeDef sMasterConfig {0}; sMasterConfig.MasterOutputTrigger TIM_TRGO_UPDATE; sMasterConfig.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(htim2, sMasterConfig); // ADC 外部触发配置 hadc1.Init.ContinuousConvMode DISABLE; // 关掉连续转换改由触发驱动 hadc1.Init.ExternalTrigConv ADC_EXTERNALTRIGCONV_T2_TRGO; hadc1.Init.ExternalTrigConvEdge ADC_EXTERNALTRIGCONVEDGE_RISING;逻辑说明ContinuousConvMode必须关掉否则定时器触发和连续转换会打架。ExternalTrigConvEdge选上升沿对应 TRGO 的更新事件。启动时先HAL_TIM_Base_Start(htim2)再HAL_ADC_Start_DMA顺序反了会丢第一批数据。5.2 采样率与缓冲区大小的匹配计算定时器触发模式下采样率 定时器频率 / 规则组通道数。比如 1kHz 触发、4 个通道那么每个通道的实际采样率是 250Hz。如果你要每通道 1kHz触发频率就得设成 4kHz。缓冲区大小决定了你多久处理一次数据。假设触发频率 4kHz4 个通道DMA 缓冲区设 64 个半字那么每 64/4 16 次触发就填满一次对应 16/4kHz 4ms。也就是说你每 4ms 要处理一批数据这个时间窗口要留够。参数含义典型值调整影响定时器预分频降低计数时钟84-1改大则触发频率降低定时器周期决定溢出频率1000-1改小则触发频率升高规则组通道数每次触发转换的通道数4增加则单通道采样率下降DMA 缓冲区长度一次搬运的数据量64增加则中断频率降低延迟增大采样周期ADC 采样电容充电时间84 cycles增加则转换时间变长需匹配触发间隔这张表建议在改参数时对照看。触发间隔必须大于单通道转换时间乘以通道数否则 ADC 还没转换完下一次触发就来了会导致触发丢失或数据错位。比如 84 周期采样、21MHz ADC 时钟单通道 4.57μs4 通道就是 18.3μs触发间隔至少 20μs对应最高 50kHz 触发频率。超过这个值数据就会乱。5.3 验证采样率是否准确的实操方法配好定时器触发后怎么确认采样率真的对我的习惯是找一个已知频率的信号源比如 1kHz 方波采回来做过零检测数 1 秒内的周期数。如果没有信号发生器可以用 DAC 输出一个正弦波回接到 ADCDAC 的更新率是已知的对比一下就能算出实际采样率。另一个办法是翻转一个 GPIO在 DMA 全满回调里翻转用示波器量翻转频率乘以缓冲区长度就是实际采样率。这个方法最直接也最能暴露中断延迟的问题。如果实测值比理论值低检查是不是中断里做了太多事或者 DMA 优先级被其他流抢了。从那以后我每次配多通道 ADCDMA都会先把定时器触发和 GPIO 翻转验证跑一遍确认采样节拍稳了再往上叠业务逻辑。希望帮到你。本文还有配套的精品资源点击获取