简介:这份资源面向STM32F4嵌入式开发者与学习者,聚焦多通道ADC与DMA协同采集这一实战主题,帮助解决多路模拟信号高速采样时CPU占用高、中断频繁的问题。压缩包共103个文件,以47个.h头文件与46个.c源文件为主体,涵盖ADC、DMA、RCC、TIM、USART等标准外设驱动,另含uvprojx与uvoptx工程文件、s启动文件、hex固件、png截图及bat脚本,整体约846KB,可直接导入Keil环境编译调试。目前已有1515人学习下载。代码完整实现了ADC初始化、DMA通道配置、ADC与DMA请求连接、连续转换启动及传输完成中断处理等关键环节,读者可据此掌握多通道扫描、采样时间与分辨率设置、内存到外设的数据搬运逻辑,并理解如何用DMA替代中断降低CPU负担,适合需要实时监控多路模拟量的采集类项目参考与二次开发。
1. 从一次采样抖动说起:STM32F4 多通道 DMA ADC 到底解决了什么
如果你用 STM32F4 做过 ADC 采集,大概率遇到过这种场景:单通道轮询读值挺稳,一旦切到多通道,数据就开始串位,通道 0 的值跑到通道 3 上,或者干脆整组数据错位。更头疼的是,采样率一高,CPU 全耗在等 EOC 标志上,主循环里其他任务全被拖慢。这份STM32F4 多通道DMAADC.zip就是冲着这类问题来的——它把多通道 ADC 配合 DMA 搬运的完整工程打包好了,核心思路是让 ADC 按规则组顺序扫描多个通道,每转换完一个通道就触发一次 DMA 请求,把数据直接搬进内存数组,CPU 全程不插手。
它适合两类人:一类是刚上手 F4 系列、被多通道采样顺序和 DMA 配置绕晕的新手,照着工程改引脚和通道就能跑;另一类是做电机控制、音频采集、传感器阵列的熟手,需要一份能直接对照的 DMA 双缓冲或循环模式配置参考。下面我按「资源里有什么 → 怎么配 → 坑在哪 → 怎么验证」的顺序拆一遍,中间会给出关键寄存器和 HAL 库两种写法,方便你对着自己的板子改。
2. 规则组扫描 + DMA 循环:多通道采集的底层逻辑与工程结构
2.1 为什么多通道必须靠 DMA,而不是中断
STM32F4 的 ADC 有三个独立单元,每个单元支持最多 16 个外部通道加若干内部通道。规则组(Regular Group)允许你把多个通道排成一个序列,ADC 按顺序逐个转换。问题在于:如果你用中断方式,每转换完一个通道就进一次中断,假设规则组里有 6 个通道,采样率 100kHz,那 CPU 每秒要进 60 万次中断,光进出栈就吃掉大半算力,主循环基本别想干别的。
DMA 的思路是把「搬运」这件事从 CPU 手里拿走。ADC 每次转换完成,硬件自动发一个 DMA 请求,DMA 控制器把 ADC_DR 寄存器里的值搬到你指定的内存地址,搬完一个通道地址自增,指向数组下一个位置。整个过程 CPU 只在 DMA 传输完成一半或全部完成时被通知一次,中断频率从「每通道一次」降到「每半缓冲一次」。
常见做法是配成循环模式(Circular),DMA 搬完整个数组后自动回到开头继续搬,配合半传输中断和传输完成中断,就能实现「一边采一边处理」的双缓冲效果。这份工程里用的就是循环模式 + 半传输/完成中断的组合,数组长度设成通道数的整数倍,保证每个通道的数据落在固定位置。
2.2 工程目录里值得先看的几个文件
解压后先别急着编译,按这个顺序翻:
| 文件/目录 | 作用 | 优先看什么 |
|---|---|---|
Core/Src/main.c | 主流程 | ADC 校准、DMA 启动、主循环处理 |
Core/Src/adc.c | ADC 初始化 | 规则组通道数、采样时间、扫描模式 |
Core/Src/dma.c | DMA 初始化 | 外设地址、内存地址、数据宽度、循环模式 |
Core/Inc/main.h | 宏定义 | 通道对应的 GPIO 引脚、数组长度 |
MDK-ARM/或STM32CubeIDE/ | 工程文件 | 根据你的 IDE 选对应目录打开 |
我一般会先确认adc.c里ADC_ChannelConfTypeDef的Rank顺序,这个顺序决定了 DMA 数组里第几个元素对应哪个物理通道。很多人数据串位就是这里 Rank 和实际接线对不上。
2.3 规则组扫描模式的关键参数
在 CubeMX 里配 ADC 时,有几个参数直接决定 DMA 能不能正常工作:
- Scan Conversion Mode:必须 Enable,否则只转第一个通道。
- Continuous Conversion Mode:如果要用 DMA 循环搬运,建议 Enable,让 ADC 不停转换;如果只想采一轮就停,就 Disable,靠软件触发。
- DMA Continuous Requests:Enable,保证每次转换都发 DMA 请求。
- Number Of Conversions:填规则组里的通道总数,比如 4 个通道就填 4。
- External Trigger:一般选软件触发或定时器触发,用定时器触发能精确控制采样率。
采样时间(Sampling Time)别设太短,F4 的 ADC 输入阻抗和采样电容决定了最短采样时间,设太短读数会偏小。常见做法是先用 84 或 144 个周期试,稳定后再往下压。
3. 从 CubeMX 到代码:多通道 DMA ADC 的完整配置步骤
3.1 用 CubeMX 生成骨架,重点改这三处
如果你从零开始,先在 CubeMX 里选好芯片型号,然后按下面走:
- 在
Analog里找到 ADC1,勾选你要用的通道,比如 IN0、IN1、IN4、IN5。 - 在
Parameter Settings里把Scan Conversion Mode设为 Enabled,Continuous Conversion Mode设为 Enabled,Number Of Conversions填 4。 - 在
DMA Settings里点 Add,选 ADC1,模式选 Circular,数据宽度都选 Half Word(ADC 是 12 位,右对齐放 16 位里)。 - 在
NVIC Settings里勾上 DMA 的传输完成中断和半传输中断。
生成代码后,CubeMX 会自动填好MX_DMA_Init()和MX_ADC1_Init(),但数组定义和启动逻辑要自己加。
3.2 定义缓冲区并启动 DMA
在main.c的/* USER CODE BEGIN PV */区域定义数组:
/* USER CODE BEGIN PV */ #define ADC_CH_NUM 4 #define ADC_BUF_LEN (ADC_CH_NUM * 2) // 双缓冲,每个通道存两次 uint16_t adc_dma_buf[ADC_BUF_LEN]; // DMA 目标数组 volatile uint8_t adc_half_ready = 0; // 半传输标志 volatile uint8_t adc_full_ready = 0; // 全传输标志 /* USER CODE END PV */这里ADC_BUF_LEN设成通道数的 2 倍,是为了配合半传输中断做双缓冲。DMA 搬完前一半时触发半传输中断,你可以处理前一半数据,同时 DMA 继续搬后一半,互不冲突。
启动顺序很重要,先校准 ADC,再启动 DMA,最后启动 ADC:
/* USER CODE BEGIN 2 */ HAL_ADCEx_Calibration_Start(&hadc1); // F4 必须校准 HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buf, ADC_BUF_LEN); /* USER CODE END 2 */HAL_ADC_Start_DMA的第三个参数是「传输次数」,单位是「数据项个数」,不是字节数。这里填ADC_BUF_LEN,DMA 就会搬 8 个半字。如果你填成字节数 16,DMA 会多搬一倍,数组直接越界,这是血泪经验里最常见的翻车点。
3.3 中断回调里处理数据
半传输和全传输中断共用同一个回调,靠HAL_ADC_ConvHalfCpltCallback和HAL_ADC_ConvCpltCallback区分:
/* USER CODE BEGIN 4 */ void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc->Instance == ADC1) { adc_half_ready = 1; // 前一半数据就绪 } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc->Instance == ADC1) { adc_full_ready = 1; // 后一半数据就绪 } } /* USER CODE END 4 */主循环里轮询这两个标志,谁置位就处理对应半区。处理时注意:DMA 还在后台搬另一半,你只能读已经置位的那一半,不能碰另一半,否则读到的是正在被覆盖的脏数据。
3.4 通道顺序与数组下标的对应关系
假设规则组顺序是 IN0 → IN1 → IN4 → IN5,Rank 分别设为 1、2、3、4,那么 DMA 数组里的排列是:
| 数组下标 | 对应通道 | 物理引脚示例 |
|---|---|---|
| buf[0] | IN0 | PA0 |
| buf[1] | IN1 | PA1 |
| buf[2] | IN4 | PA4 |
| buf[3] | IN5 | PA5 |
| buf[4] | IN0 | 第二轮 |
| buf[5] | IN1 | 第二轮 |
| ... | ... | ... |
如果你发现 buf[0] 读到的值跟 PA0 对不上,先查 Rank 顺序,再查 GPIO 有没有配成模拟模式。F4 的 ADC 引脚必须把 GPIO_Mode 设成GPIO_MODE_ANALOG,配成输入或复用都会导致读数异常。
4. 避坑与排查:多通道 DMA ADC 最容易翻车的五个点
4.1 数据串位:Rank 顺序和数组下标错位
现象:buf[0] 的值明显是另一个通道的,或者四个通道的值看起来像被循环移位了。
原因:CubeMX 里通道的 Rank 顺序和你代码里读数组的顺序不一致。比如你在 CubeMX 里先加了 IN4 再加 IN0,Rank1 是 IN4,但代码里按 IN0 去读 buf[0]。
解决:在adc.c里找到sConfig.Rank的赋值,按 Rank 从小到大列出通道,然后在代码里用宏定义把下标和通道名绑定,比如#define IDX_IN0 0,别硬编码数字。
4.2 DMA 只搬一次就停
现象:数组里只有第一组数据,之后不再更新。
原因:DMA 模式配成了 Normal 而不是 Circular。Normal 模式下搬完指定次数就停,循环模式才会自动重载。
解决:在dma.c里确认hdma_adc1.Init.Mode = DMA_CIRCULAR。如果是用 CubeMX 生成的,检查 DMA Settings 里 Mode 是不是选成了 Normal。
4.3 读数偏小或跳动大
现象:采样值比万用表测的低一截,或者相邻两次读差异很大。
原因:采样时间太短,ADC 内部采样电容没充到输入电压。F4 的 ADC 输入阻抗和采样时间要匹配,信号源内阻越大,需要的采样周期越多。
解决:把sConfig.SamplingTime从ADC_SAMPLETIME_3CYCLES往上调,试ADC_SAMPLETIME_84CYCLES或ADC_SAMPLETIME_144CYCLES。另外在引脚旁边加 0.1uF 到地的小电容,能明显改善跳动。
4.4 半传输中断不触发
现象:只有全传输回调进,半传输回调从来不进。
原因:NVIC 里没使能 DMA 的半传输中断,或者HAL_ADC_Start_DMA之前 DMA 中断被关了。
解决:在 CubeMX 的 NVIC Settings 里确认DMA2_Stream0(具体流号看芯片)的Half Transfer中断勾上了。生成的代码里HAL_NVIC_EnableIRQ要包含对应的 IRQ。
4.5 数组越界导致 HardFault
现象:程序跑一会儿就进 HardFault,或者数组旁边的变量被莫名改写。
原因:HAL_ADC_Start_DMA的第三个参数填成了字节数,DMA 搬的数据量是数组容量的两倍,直接踩到相邻内存。
解决:记住第三个参数是「数据项个数」,uint16_t数组长度是 8 就填 8,不是 16。如果不确定,用sizeof(adc_dma_buf)/sizeof(adc_dma_buf[0])算。
5. 进阶验证:用定时器触发 + 双缓冲把采样率钉死
5.1 为什么软件触发不够稳
前面配的是软件触发,HAL_ADC_Start_DMA一调用 ADC 就连续转,采样率取决于 ADC 时钟和采样周期,但启动时刻有抖动。做音频或振动分析时,采样率不稳会导致频谱泄漏,FFT 出来的峰值对不上。常见做法是改用定时器触发,让 TIM 的更新事件去触发 ADC 转换,这样每个采样点之间的间隔由定时器硬件保证,精度到时钟周期级别。
5.2 定时器触发的配置要点
以 TIM2 为例,假设系统时钟 84MHz,要 10kHz 采样率,预分频和重装载值这样算:
/* 定时器触发 ADC,10kHz 采样率 */ htim2.Instance = TIM2; htim2.Init.Prescaler = 84 - 1; // 84MHz / 84 = 1MHz htim2.Init.Period = 100 - 1; // 1MHz / 100 = 10kHz htim2.Init.CounterMode = TIM_COUNTERMODE_UP; HAL_TIM_Base_Init(&htim2); /* 配置 TIM2 更新事件作为 ADC 外部触发 */ TIM_MasterConfigTypeDef sMasterConfig = {0}; sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig);然后在 ADC 配置里把ExternalTrigConv改成ADC_EXTERNALTRIGCONV_T2_TRGO,ExternalTrigConvEdge设为ADC_EXTERNALTRIGCONVEDGE_RISING。这样每次 TIM2 溢出,ADC 就启动一轮规则组转换,DMA 跟着搬。
5.3 验证采样率是否准确
最直接的办法是让 ADC 采一个已知频率的方波,比如用信号发生器给 1kHz 方波,采 1024 个点做 FFT,看峰值是不是落在 1kHz。如果峰值偏移,检查定时器时钟树有没有配错,或者 ADC 的采样时间是否拖慢了实际转换速率。
另一个办法是翻转一个 GPIO,在 DMA 完成回调里翻转,用示波器看翻转频率。如果设的是 10kHz 采样、4 通道、双缓冲,那全传输中断频率应该是 10kHz / 4 / 2 = 1.25kHz,示波器上看到 1.25kHz 方波就说明配置对了。
5.4 双缓冲处理时的一个习惯
我处理双缓冲时习惯给每个半区加一个序号,主循环处理完把序号清掉,中断里置位。这样即使主循环偶尔被高优先级中断打断,也不会重复处理同一批数据。具体做法是在回调里把adc_half_ready和adc_full_ready当成「待处理请求」,主循环处理完手动清零,而不是在回调里清。从那以后我每次配 DMA 双缓冲都强制走一遍「半传输标志 + 全传输标志 + 主循环清零」的流程,再没出现过数据覆盖导致的玄学跳变。
希望这份工程和上面的配置思路能帮你把多通道采集跑稳,少走几个我当年踩过的弯路。
本文还有配套的精品资源,点击获取