简介:这份资源面向STM32F4嵌入式开发者与学习者,聚焦多通道ADC配合DMA进行高速数据采集这一典型场景,帮助解决多路模拟信号采样时CPU占用高、实时性不足的问题。压缩包共103个文件,以47个.h头文件与46个.c源文件为主体,涵盖ADC、DMA、RCC、TIM等外设驱动与初始化代码,另含uvprojx与uvoptx工程文件、s启动文件、hex固件及png示意图,整体约846KB,可直接导入Keil环境编译调试。内容围绕ADC时钟与采样时间配置、连续转换模式、DMA通道与传输方向设置、ADC与DMA请求关联、传输完成中断处理等环节展开,并给出数据处理与主循环控制逻辑,便于读者理解外设协同工作机制。目前已有1515人学习下载,适合希望掌握多通道采集与DMA搬运技巧、提升系统响应效率的开发者参考实践。
1. 多通道采集卡在 10kHz 就掉数据,问题往往不在 ADC 本身
有个做电机控制的朋友找我,说他的板子用 STM32F4 采三相电流,单通道跑 50kHz 稳如老狗,一开三通道就丢点,串口打出来的波形像被狗啃过。他怀疑是 ADC 精度不够,准备换外置芯片。我让他把工程发过来,看了一眼——三个通道轮流HAL_ADC_Start再HAL_ADC_PollForConversion,每次转换完还要等 CPU 把值搬走。这不是 ADC 的问题,是采集架构的问题。
这份STM32F4 多通道DMAADC.zip解决的就是这类场景:用 DMA 把 ADC 的多通道转换结果自动搬到内存,CPU 只负责在缓冲区满的时候处理数据,中间不参与搬运。适合做电机电流采样、音频预处理、多路传感器巡检、电源监控这类需要连续高速采集的嵌入式开发者。如果你正在用 F4 系列,被多通道采集的时序和丢数问题卡住,这份工程值得拆开看。
2. 工程结构拆解:从 CubeMX 配置到 DMA 搬运的完整链路
2.1 目录组织与文件职责
拿到压缩包解压后,目录结构是典型的 STM32CubeMX 生成工程加手工补全的驱动层。核心文件分布如下:
STM32F4_MultiADC_DMA/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── adc.h // ADC 初始化与通道配置 │ │ ├── dma.h // DMA 流配置 │ │ └── stm32f4xx_hal_conf.h │ └── Src/ │ ├── main.c // 主循环与数据处理入口 │ ├── adc.c // ADC 校准、启动、DMA 关联 │ ├── dma.c // DMA 中断优先级与传输宽度 │ └── stm32f4xx_it.c // DMA 传输完成中断服务函数 ├── Drivers/ │ ├── STM32F4xx_HAL_Driver/ │ └── CMSIS/ └── MDK-ARM/ // Keil 工程文件adc.c负责把 ADC1 的三个规则通道(IN0、IN1、IN2)配成扫描模式,dma.c把 DMA2 Stream0 的源地址指向ADC1->DR,目标地址指向一个uint16_t数组。main.c里只做两件事:启动采集、在 DMA 完成回调里处理数据。这种分层的好处是,换通道只改adc.c,换缓冲区大小只改main.c的宏定义,互不干扰。
2.2 CubeMX 里的关键配置项
很多人用 CubeMX 配 ADC+DMA 时,被一堆选项绕晕。这个工程里几个必须锁死的配置如下:
| 配置项 | 设置值 | 原因 |
|---|---|---|
| Scan Conversion Mode | Enabled | 多通道必须开扫描 |
| Continuous Conversion Mode | Enabled | 连续采集不停止 |
| DMA Continuous Requests | Enabled | 每次转换完自动触发 DMA |
| Number Of Conversions | 3 | 对应三个规则通道 |
| DMA Mode | Circular | 缓冲区循环覆盖,适合流式采集 |
| Peripheral Data Width | Half Word | ADC 是 12 位,右对齐占 16 位 |
| Memory Data Width | Half Word | 与外设宽度一致,避免打包错位 |
这里有个容易翻车的点:DMA Continuous Requests如果不使能,DMA 只搬一次就停了,现象是串口只出一帧数据然后死寂。另一个是Continuous Conversion Mode,关掉的话 ADC 转换完一轮就停,必须靠软件反复触发,高速场景下根本来不及。
2.3 时钟树与采样时间计算
F4 的 ADC 时钟来自 APB2 经过分频,最高不能超过 36MHz。工程里 APB2 跑 84MHz,ADC 预分频设为 4,得到 21MHz 的 ADC 时钟。采样时间设为 84 个周期,转换时间就是(84 + 12) / 21MHz ≈ 4.57us,三个通道一轮约 13.7us,理论最高轮询率约 73kHz。实际跑下来,DMA 搬运不占 CPU,10kHz 采集时 CPU 占用不到 5%。
注意:采样时间不是越长越好。84 周期适合源阻抗较高的信号,如果信号源内阻低于 1kΩ,可以降到 15 周期,把轮询率提上去。
3. 核心代码逐段拆解:ADC 校准、DMA 关联与中断处理
3.1 ADC 多通道初始化
adc.c里的初始化函数是整份工程的地基。它做了三件事:配 GPIO 为模拟输入、配 ADC 规则通道序列、把 DMA 流和 ADC 绑在一起。
// adc.c - ADC1 三通道扫描 + DMA 初始化 void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; // 21MHz hadc1.Init.Resolution = ADC_RESOLUTION_12B; // 12位精度 hadc1.Init.ScanConvMode = ENABLE; // 扫描模式 hadc1.Init.ContinuousConvMode = ENABLE; // 连续转换 hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.NbrOfConversion = 3; // 3个规则通道 hadc1.Init.DMAContinuousRequests = ENABLE; // DMA循环请求 hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV; // 序列结束才产生EOC HAL_ADC_Init(&hadc1); // 通道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); // 通道2,序列位置3 sConfig.Channel = ADC_CHANNEL_2; sConfig.Rank = 3; HAL_ADC_ConfigChannel(&hadc1, &sConfig); }EOCSelection设成ADC_EOC_SEQ_CONV是关键。如果设成单通道转换结束就触发,DMA 会在每个通道转换完都搬一次,但目标缓冲区地址递增,三个通道的数据会错位。设成序列结束,DMA 每搬一轮就是完整的三通道数据,缓冲区里[0]=CH0, [1]=CH1, [2]=CH2依次排列。
3.2 DMA 流配置与缓冲区定义
DMA 配置在dma.c里,核心是源地址、目标地址、传输长度和循环模式。
// dma.c - DMA2 Stream0 配置,用于 ADC1 void MX_DMA_Init(void) { __HAL_RCC_DMA2_CLK_ENABLE(); 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_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 的数据寄存器地址固定是0x4001204C,递增就跑到别的寄存器去了。MemInc必须打开,否则三个通道的数据全挤在缓冲区第一个位置,后面两个永远是 0。Mode选DMA_CIRCULAR是为了让 DMA 搬完一轮自动回到缓冲区头部,不用软件重新启动。
缓冲区定义在main.c里:
// main.c - 采集缓冲区定义 #define ADC_CHANNELS 3 #define ADC_SAMPLES 256 #define ADC_BUF_SIZE (ADC_CHANNELS * ADC_SAMPLES) uint16_t adc_dma_buffer[ADC_BUF_SIZE]; // DMA目标缓冲区 volatile uint8_t adc_half_ready = 0; // 半满标志 volatile uint8_t adc_full_ready = 0; // 全满标志ADC_BUF_SIZE是 768 个半字,DMA 搬满 768 次触发一次全满中断,搬到 384 次触发半满中断。半满中断的意义在于:当 DMA 在写后半段时,CPU 可以处理前半段的数据,实现乒乓缓冲,避免数据被覆盖。
3.3 中断回调与数据处理
DMA 传输完成中断在stm32f4xx_it.c里触发,但实际处理逻辑放在main.c的回调函数中。
// main.c - DMA转换完成回调 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { adc_full_ready = 1; // 标记全满,主循环处理 } } void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { adc_half_ready = 1; // 标记半满 } } // main.c - 主循环中的数据处理 while (1) { if (adc_half_ready) { adc_half_ready = 0; // 处理前半段:adc_dma_buffer[0] 到 [383] process_adc_data(&adc_dma_buffer[0], ADC_SAMPLES / 2); } if (adc_full_ready) { adc_full_ready = 0; // 处理后半段:adc_dma_buffer[384] 到 [767] process_adc_data(&adc_dma_buffer[ADC_CHANNELS * ADC_SAMPLES / 2], ADC_SAMPLES / 2); } }process_adc_data里按三通道解包:buffer[i*3]是 CH0,buffer[i*3+1]是 CH1,buffer[i*3+2]是 CH2。如果要做 RMS 计算或滤波,在这里做,不要在中断里做,中断里只置标志位。
提示:
adc_half_ready和adc_full_ready用volatile修饰,防止编译器优化掉主循环里的读取。如果主循环处理时间超过半缓冲的采集时间,会出现数据覆盖,这时候要么加大缓冲区,要么降低采样率。
3.4 启动采集的完整调用链
初始化完成后,启动采集只需要两行:
// main.c - 启动ADC校准和DMA采集 HAL_ADCEx_Calibration_Start(&hadc1); // F4必须校准 HAL_ADC_Start_DMA(&hadc1, // 启动ADC (uint32_t*)adc_dma_buffer, // DMA目标地址 ADC_BUF_SIZE); // 传输次数HAL_ADCEx_Calibration_Start在 F4 上不能省。不校准的话,零漂可能到几十个 LSB,采小信号时误差肉眼可见。校准只需要在初始化后做一次,之后不用重复。
HAL_ADC_Start_DMA的第三个参数是传输次数,不是字节数。因为数据宽度是半字,768 次传输对应 1536 字节。如果这里填成 1536,DMA 会多搬一倍,缓冲区直接溢出。
4. 避坑与排查:多通道 DMA 采集最常见的五个翻车点
4.1 现象:串口只出一帧数据,之后全是 0
原因:DMAContinuousRequests没使能,或者 DMA 模式设成了DMA_NORMAL而不是DMA_CIRCULAR。DMA 搬完一轮就停了,ADC 还在转,但数据没人搬。
解决:检查hadc1.Init.DMAContinuousRequests = ENABLE,检查hdma_adc1.Init.Mode = DMA_CIRCULAR。两个都对了,DMA 才会循环搬运。
4.2 现象:三个通道数据错位,CH0 的值跑到 CH1 的位置
原因:EOCSelection设成了ADC_EOC_SINGLE_CONV,每个通道转换完都触发 DMA,但 DMA 的目标地址一直在递增,导致数据排列变成[CH0, CH1, CH2, CH0, CH1, CH2...]被拉成[CH0, CH1, CH2, CH0...]但通道对应关系乱了。
解决:把EOCSelection改成ADC_EOC_SEQ_CONV,让 DMA 在三个通道全部转换完才搬一次。
4.3 现象:采集值跳动很大,噪声明显
原因:采样时间太短,采样保持电容没充到信号电压。或者 GPIO 没配成模拟输入,还带着上拉或下拉。
解决:把SamplingTime从 15 周期提到 84 或 144 周期。检查 GPIO 初始化,模式必须是GPIO_MODE_ANALOG,不能是GPIO_MODE_INPUT。
4.4 现象:DMA 传输完成中断进不去
原因:DMA 中断优先级没配,或者HAL_NVIC_EnableIRQ没调用。还有一种情况是中断服务函数里没调HAL_DMA_IRQHandler。
解决:在dma.c里加上HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0)和HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn)。在stm32f4xx_it.c的DMA2_Stream0_IRQHandler里调HAL_DMA_IRQHandler(&hdma_adc1)。
4.5 现象:半满和全满标志同时置位,数据处理不过来
原因:主循环处理速度跟不上采集速度,半满还没处理完,全满已经来了。缓冲区被 DMA 覆盖,数据前后不一致。
解决:加大ADC_SAMPLES,或者降低采样率,或者把数据处理放到更高优先级的任务里。如果用了 RTOS,把处理任务优先级提到 DMA 中断之上,但注意不要在中断里做耗时操作。
5. 二次开发改造:换通道、改采样率、加滤波的实操路径
拿到这份工程后,最常见的改造需求有三个:换采集通道、改采样率、加数字滤波。下面按改动量从小到大说。
换通道最简单。比如要把 CH0/CH1/CH2 换成 CH4/CH5/CH6,只需要改adc.c里三个sConfig.Channel的值,同时把对应 GPIO 配成模拟输入。注意 F4 的 ADC1 通道 4/5/6 对应 PA4/PA5/PA6,别配错引脚。改完通道后,main.c里解包逻辑不用动,因为 DMA 搬运顺序还是按 Rank 来的。
改采样率要动两个地方。一是SamplingTime,从 84 周期降到 15 周期,单通道转换时间从 4.57us 降到 1.29us,三通道一轮从 13.7us 降到 3.87us,理论轮询率从 73kHz 提到 258kHz。二是ADC_SAMPLES,如果采样率提高但缓冲区不变,半满中断来得更频繁,CPU 负载上升。我一般会把ADC_SAMPLES从 256 提到 512 或 1024,让每次处理的数据块更大,减少中断次数。
加滤波的话,推荐在process_adc_data里做滑动平均或 IIR。下面是一个三通道滑动平均的示例:
// main.c - 三通道滑动平均滤波 #define FILTER_WINDOW 8 static uint16_t filter_buf[ADC_CHANNELS][FILTER_WINDOW] = {0}; static uint8_t filter_idx = 0; void process_adc_data(uint16_t *buf, uint16_t samples) { for (uint16_t i = 0; i < samples; i++) { for (uint8_t ch = 0; ch < ADC_CHANNELS; ch++) { // 存入滑动窗口 filter_buf[ch][filter_idx] = buf[i * ADC_CHANNELS + ch]; } filter_idx = (filter_idx + 1) % FILTER_WINDOW; // 计算三通道平均值 for (uint8_t ch = 0; ch < ADC_CHANNELS; ch++) { uint32_t sum = 0; for (uint8_t j = 0; j < FILTER_WINDOW; j++) { sum += filter_buf[ch][j]; } uint16_t filtered = sum / FILTER_WINDOW; // filtered 就是滤波后的通道值,送显示或控制 (void)filtered; } } }FILTER_WINDOW越大,滤波效果越好,但响应越慢。电机电流环里一般用 4 到 8,太大相位滞后会影响控制带宽。如果要做 RMS,把平方和累加放在这里,开方放到主循环,别在中断里开方。
还有一个改造方向是双 ADC 同步采集。F4 支持 ADC1 和 ADC2 在双重模式下交替或同步采样,DMA 搬运的数据量翻倍,但采样率也能翻倍。配置上要把 ADC2 也初始化,然后调HAL_ADCEx_MultiModeStart_DMA代替HAL_ADC_Start_DMA。这个改动量稍大,但原理和单 ADC 一样,缓冲区里前一半是 ADC1 的数据,后一半是 ADC2 的。
从那以后我每次配多通道 DMA 采集,都强制走一遍检查清单:DMAContinuousRequests使能了吗、EOCSelection是序列结束吗、DMA 模式是循环吗、GPIO 是模拟输入吗、校准做了吗。这五条过一遍,基本不会翻车。希望帮到你。
本文还有配套的精品资源,点击获取