简介:这份资源面向STM32F4嵌入式开发者与学习者,聚焦多通道ADC与DMA协同采集这一实战主题,帮助解决多路模拟信号高速采样时CPU占用高、中断频繁、实时性不足的问题。压缩包共103个文件,约846KB,以47个.h头文件与46个.c源文件为主体,涵盖ADC、DMA、RCC、TIM、USART等外设驱动与工程逻辑,另含uvprojx与uvoptx工程文件、hex烧录文件、s启动文件及少量png、bat、txt辅助内容,可直接导入Keil环境编译调试。资源围绕ADC初始化、DMA通道配置、ADC与DMA请求连接、连续转换启动及DMA传输完成中断处理等关键环节展开,读者可据此掌握多通道采样序列设置、内存到外设的数据搬运机制与结果读取流程,理解如何用DMA释放CPU资源、提升采集效率与实时性。目前已有1514人学习下载,适合需要快速搭建多通道数据采集原型的开发者参考与二次开发。
1. 多通道 DMA ADC 到底解决了什么痛点
如果你用 STM32F4 做过模拟量采集,大概率经历过这样的场景:主循环里轮询 ADC 的 EOC 标志,采一路电压还行,一旦要同时盯四路、六路甚至八路传感器,CPU 就被拖死在等待转换完成上,采样率上不去,波形还抖得厉害。STM32F4 多通道 DMA ADC 这套组合拳,解决的正是「多路模拟量高速采集时不占用 CPU」这个核心诉求。它让 ADC 按规则序列自动轮转多个通道,每转换完一个通道就把结果通过 DMA 搬进内存数组,全程不需要 CPU 插手,你只管在缓冲区里读数据。这套方案适合做电机电流采样、多路温度巡检、音频前端、电池组电压监测这类需要连续、等间隔、多通道同步采集的嵌入式场景。标题里的 zip 通常意味着一个可直接编译的工程包,但真正值钱的是里面 ADC 与 DMA 的配置逻辑,下面把它拆开讲透。
2. 规则序列与 DMA 请求:先把数据通路讲清楚
2.1 为什么多通道必须走 DMA 而不是中断
STM32F4 的 ADC 有规则通道和注入通道两组。规则通道最多 16 个,按你写入 SQR1/SQR2/SQR3 的顺序依次转换,转换完一个产生一次 EOC。如果只用中断,每个通道转换完都进一次中断服务函数去读 DR 寄存器,通道一多、采样率一高,中断频率就爆炸。假设 8 个通道、每通道 10 kHz 采样,中断频率就是 80 kHz,CPU 光进进出出中断上下文就吃掉大量周期,主循环基本没得跑。
DMA 的思路是把「读 DR」这个动作交给硬件。ADC 每次转换完成发出一个 DMA 请求,DMA 控制器直接把 ADC->DR 的内容搬到你指定的内存地址,搬完一个自动把地址偏移到下一个。CPU 只在半满/全满中断里处理一批数据,负担从「每通道一次」降到「每缓冲区一次」。这就是多通道采集能跑起来的根本原因。
2.2 规则序列长度与扫描模式的配合
要让 ADC 自动轮转多个通道,必须打开扫描模式(SCAN),否则它只会反复转换 SQR 里的第一个通道。规则序列长度由 SQR1 的 L[3:0] 位决定,写入的是「通道数减一」。比如采 4 个通道,L 写 3。每个通道号按顺序填进 SQR3、SQR2、SQR1 的低位部分,顺序就是你想要的采样顺序。
这里有个容易翻车的点:采样顺序不是按通道号大小排的,而是按你写入 SQR 的顺序。很多人以为填了通道 0、1、2、3 就一定是这个顺序,其实只要写入次序变了,DMA 缓冲区里数据的排列也跟着变。所以缓冲区下标和物理通道的对应关系,必须拿 SQR 的写入顺序去核对,不能想当然。
2.3 DMA 循环模式与数据宽度的匹配
ADC 的 DR 寄存器是 16 位有效(F4 上是 12 位精度,存在低 12 位或对齐后的位置),DMA 外设数据宽度要设成半字(Half Word,16 位),内存数据宽度也设成半字,两边一致才不会出现高低字节错位。DMA 模式选循环模式(Circular),这样缓冲区填满后自动回到起点继续填,适合连续采集;如果选普通模式,采完一轮就停了,得手动重启。
DMA 的优先级、FIFO 这些在纯 ADC 搬运场景里一般用默认值就够,但要注意 DMA 流和通道的对应关系。STM32F4 的 DMA1/DMA2 各有 8 个流,每个流有 8 个通道可选,ADC1 通常挂在 DMA2 的某个流上,具体是哪个流、哪个通道,必须查对应型号的参考手册,不能凭记忆写。选错了流,DMA 请求根本不会触发,现象就是缓冲区一直是初值,ADC 却在正常转换。
3. 从零配通多通道 DMA ADC 的完整步骤
3.1 时钟、GPIO 与 ADC 基础配置
先把时钟树理清。ADC 时钟来自 APB2 经分频器,F4 的 ADC 时钟不能超过 36 MHz,常用 PCLK2 分频到 21 MHz 或 30 MHz 左右。GPIO 要配成模拟输入模式(GPIO_Mode_AN),注意模拟模式下不需要上下拉,配了反而影响精度。
// 使能 GPIO、ADC、DMA 时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); // PA0~PA3 配为模拟输入,对应 ADC1_IN0~IN3 GPIO_InitTypeDef gpio; gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; gpio.GPIO_Mode = GPIO_Mode_AN; // 模拟输入 gpio.GPIO_PuPd = GPIO_PuPd_NOPULL; // 模拟模式不加上下拉 GPIO_Init(GPIOA, &gpio); // ADC 通用配置:独立模式、预分频 ADC_CommonInitTypeDef common; common.ADC_Mode = ADC_Mode_Independent; common.ADC_Prescaler = ADC_Prescaler_Div4; // PCLK2=84M 时约 21MHz common.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled; common.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles; ADC_CommonInit(&common);这段代码里ADC_Prescaler_Div4是关键参数,它决定 ADC 时钟。F4 的 ADC 最大时钟 36 MHz,分频系数要保证结果不超限。ADC_DMAAccessMode_Disabled在单 ADC 多通道场景下用不到多重 ADC 的 DMA 模式,保持关闭即可。
3.2 规则序列、采样时间与 DMA 请求使能
接下来配置 ADC1 本体,重点是扫描模式、连续转换、外部触发和 DMA 使能。
ADC_InitTypeDef adc; adc.ADC_Resolution = ADC_Resolution_12b; adc.ADC_ScanConvMode = ENABLE; // 多通道必须开扫描 adc.ADC_ContinuousConvMode = ENABLE; // 连续转换,配合循环 DMA adc.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None; // 软件触发 adc.ADC_DataAlign = ADC_DataAlign_Right; adc.ADC_NbrOfConversion = 4; // 规则序列长度 = 4 ADC_Init(ADC1, &adc); // 配置 4 个规则通道的采样顺序和采样时间 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_84Cycles); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_84Cycles); ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_84Cycles); ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_84Cycles); ADC_DMARequestAfterLastTransferCmd(ADC1, ENABLE); ADC_DMACmd(ADC1, ENABLE); // 打开 ADC 的 DMA 请求 ADC_Cmd(ADC1, ENABLE);ADC_NbrOfConversion = 4和后面四次ADC_RegularChannelConfig的 rank 参数一一对应,rank 1 到 4 就是转换顺序。采样时间ADC_SampleTime_84Cycles适合源阻抗较高的信号,如果信号源阻抗低、要追高采样率,可以降到 15 或 28 周期。ADC_DMARequestAfterLastTransferCmd保证最后一轮转换后 DMA 请求仍然有效,循环模式下必须开,否则缓冲区最后一格可能填不上。
3.3 DMA 流配置与缓冲区读取
DMA 配置要和 ADC 的请求对齐,外设基址是 ADC->DR,内存基址是你的数组。
#define BUF_LEN 64 uint16_t adc_buf[BUF_LEN]; // 4 通道 x 16 组 DMA_InitTypeDef dma; dma.DMA_Channel = DMA_Channel_0; // 查手册确认 ADC1 对应通道 dma.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; dma.DMA_Memory0BaseAddr = (uint32_t)adc_buf; dma.DMA_DIR = DMA_DIR_PeripheralToMemory; dma.DMA_BufferSize = BUF_LEN; dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址固定 dma.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增 dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; dma.DMA_Mode = DMA_Mode_Circular; // 循环模式 dma.DMA_Priority = DMA_Priority_High; dma.DMA_FIFOMode = DMA_FIFOMode_Disable; DMA_Init(DMA2_Stream0, &dma); DMA_Cmd(DMA2_Stream0, ENABLE); ADC_SoftwareStartConv(ADC1); // 软件触发启动DMA_BufferSize是搬运次数,不是字节数。4 通道各采 16 组就是 64 次搬运,所以填 64。DMA_MemoryInc_Enable让内存地址每次搬完自动加一,这样 64 个半字依次填满数组。读取时按「组」来解析:adc_buf[i*4 + ch]就是第 i 组第 ch 个通道的值,这个下标关系直接由 SQR 写入顺序决定。
4. 采样精度与吞吐率的参数调优
4.1 采样时间、分辨率与总转换时间的换算
总转换时间 = 采样时间 + 逐次逼近时间。F4 的 12 位分辨率下,逐次逼近固定 12 个 ADC 时钟周期,采样时间由 SMPR 寄存器设置,可选 3、15、28、56、84、112、144、480 周期。以 84 周期采样、ADC 时钟 21 MHz 为例,单通道转换时间 = (84+12)/21M ≈ 4.57 微秒,4 通道一轮约 18.3 微秒,理论吞吐率约 54.6 kHz。要提速就降采样时间或提高 ADC 时钟,但采样时间太短会让高阻抗信号源充不满采样电容,读数偏低且跳。
| 采样周期 | 单通道转换时间(21MHz) | 适用场景 |
|---|---|---|
| 3 | 0.71 us | 低阻抗、高速 |
| 28 | 1.90 us | 一般传感器 |
| 84 | 4.57 us | 高阻抗信号源 |
| 480 | 23.4 us | 极慢变化、高精度 |
4.2 用定时器触发替代软件触发
软件触发启动后 ADC 连续跑,采样间隔由转换时间决定,不够精确。要等间隔采样,常见做法是用 TIM 的 TRGO 事件触发 ADC。把ADC_ExternalTrigConvEdge设成上升沿,ADC_ExternalTrigConv选对应的定时器触发源,定时器按你想要的采样率配置更新事件。这样每个触发点启动一轮规则序列转换,间隔由定时器精确控制,做频谱分析或闭环控制时比软件触发靠谱得多。
4.3 电压换算与参考电压的坑
原始值是 12 位,换算电压用V = raw * Vref / 4095。Vref 一般是 3.3V,但如果你板子上 VDDA 和 VREF+ 走线不好,或者用了内部参考,实际值会有偏差。要求高的场合建议外接精密基准,或者用内部 VREFINT 通道反推实际 VDDA。VREFINT 的出厂校准值存在特定地址,读出来和当前采样值一比,就能算出真实参考电压,这是提升绝对精度的实用技巧。
5. 多通道 DMA ADC 避坑与排查清单
5.1 缓冲区数据全为 0 或不变
现象:ADC 配置看起来没问题,但adc_buf里始终是初值。原因通常是 DMA 流或通道选错,ADC 的 DMA 请求没接到正确的 DMA 流上。解决:查对应型号参考手册的 DMA 请求映射表,确认 ADC1 挂在哪个 DMA 的哪个流、哪个通道,逐一核对DMA_Channel和DMA2_StreamX。另一个可能是忘了ADC_DMACmd(ADC1, ENABLE),ADC 侧没开请求,DMA 自然不动。
5.2 通道数据错位、顺序对不上
现象:明明采的是通道 0 到 3,读出来通道 1 的值跑到通道 0 的位置。原因:SQR 写入顺序和缓冲区解析下标不一致,或者ADC_NbrOfConversion和实际配置的通道数不匹配。解决:把ADC_RegularChannelConfig的 rank 参数和解析时的i*N + ch严格对齐,N 等于规则序列长度。改通道顺序时两边一起改,别只改一处。
5.3 采样值跳动大、精度差
现象:读数噪声大,同一路电压反复跳几十个 LSB。原因:采样时间太短、模拟地和数字地没处理好、VDDA 去耦不足,或者 GPIO 配成了带上下拉的模式。解决:先加长采样时间到 84 或 144 周期,检查模拟输入引脚是否误配了上拉/下拉,再在 VDDA 和 VSSA 附近补 100nF 加 1uF 去耦。软件上可以做多次平均或中值滤波,但根子通常在硬件和采样时间。
5.4 DMA 半满/全满中断里读数据读到撕裂值
现象:在 DMA 中断里读缓冲区,偶尔读到明显异常的值。原因:DMA 正在写某个半字时 CPU 同时读它,出现读写竞争。解决:用双缓冲(DMA 的 M0/M1 交替)或者乒乓缓冲,在中断里只处理已经搬完的那一半,另一半留给 DMA 继续写。STM32F4 的 DMA 支持双缓冲模式,配置DMA_MemoryBurst和双缓冲基址即可,这是做连续高速采集的标准做法。
5.5 高采样率下丢数据或溢出
现象:采样率一提,数据就乱或者 DMA 报溢出。原因:DMA 带宽被其他外设抢占,或者缓冲区太小、中断处理太慢。解决:提高 DMA 流优先级,把 ADC 的 DMA 流设成 Very High;加大缓冲区长度,降低中断频率;检查是否有其他高带宽 DMA 在抢总线。必要时把不相关的外设 DMA 关掉,给 ADC 让路。
6. 把采集做成可复用的驱动模块
6.1 用结构体封装通道配置
工程里如果到处散落ADC_RegularChannelConfig,改一个通道要翻好几个文件。我一般把通道配置收进一个结构体数组,初始化时循环写入,解析时也按同一份配置走。
typedef struct { uint8_t channel; // ADC_Channel_x uint8_t rank; // 规则序列位置 uint8_t sample; // 采样时间 } AdcChCfg; static const AdcChCfg ch_cfg[] = { {ADC_Channel_0, 1, ADC_SampleTime_84Cycles}, {ADC_Channel_1, 2, ADC_SampleTime_84Cycles}, {ADC_Channel_2, 3, ADC_SampleTime_84Cycles}, {ADC_Channel_3, 4, ADC_SampleTime_84Cycles}, }; #define CH_NUM (sizeof(ch_cfg)/sizeof(ch_cfg[0])) void adc_channels_init(ADC_TypeDef *adc) { for (uint8_t i = 0; i < CH_NUM; i++) { ADC_RegularChannelConfig(adc, ch_cfg[i].channel, ch_cfg[i].rank, ch_cfg[i].sample); } }这样通道数、顺序、采样时间集中在一处,CH_NUM同时用于 DMA 缓冲区长度计算和解析步长,改配置不会漏改。解析函数里用adc_buf[group * CH_NUM + i]取第 i 个通道,和ch_cfg[i]一一对应,可读性和可维护性都上来了。
6.2 双缓冲加 DMA 中断的落地写法
连续采集最稳的结构是双缓冲。DMA 配成双缓冲模式,半满中断处理前半段,全满中断处理后半段,处理时另一段正在被 DMA 写,互不干扰。
#define HALF_LEN 32 uint16_t buf_a[HALF_LEN], buf_b[HALF_LEN]; // DMA 双缓冲配置片段 dma.DMA_Memory0BaseAddr = (uint32_t)buf_a; dma.DMA_Memory1BaseAddr = (uint32_t)buf_b; dma.DMA_BufferSize = HALF_LEN; dma.DMA_Mode = DMA_Mode_Circular; // 使能双缓冲需要开 DMA_DoubleBufferMode DMA_DoubleBufferModeConfig(DMA2_Stream0, (uint32_t)buf_b, DMA_Memory_0); DMA_DoubleBufferModeCmd(DMA2_Stream0, ENABLE); // 中断里判断当前 DMA 目标,处理另一块 void DMA2_Stream0_IRQHandler(void) { if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TCIF0)) { DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TCIF0); process(buf_a); // 当前目标切到 buf_b,处理 buf_a } if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_HTIF0)) { DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_HTIF0); process(buf_b); } }DMA_DoubleBufferModeConfig的第三个参数指定当前 DMA 正在写哪块,中断里处理的是另一块。process函数里做电压换算、滤波、入队,注意别在里面做耗时操作,否则会拖慢下一轮。缓冲区长度HALF_LEN要按「组数 × 通道数」算,别只填组数。
6.3 验证采集是否正确的三个手段
第一,接一个已知电压(比如分压后的 1.65V),看换算结果是否接近,偏差在几个 LSB 内算正常。第二,用信号发生器给一路正弦波,把缓冲区数据导出来画波形,看频率和幅值对不对,顺便验证采样率。第三,故意把某一路接地和接 VDDA,看读数是否分别接近 0 和 4095,快速判断通道映射有没有错位。这三步做完,基本能确认采集链路是通的。
我自己的习惯是,每换一块板子或改一次 ADC 配置,先把这三步跑一遍再往下做业务逻辑,省得后面调算法时怀疑是采集的锅。多通道 DMA ADC 这东西,配置对了很稳,配置错了现象又特别像「玄学」,把数据通路和下标关系吃透,比反复试参数管用得多。希望帮到你。
本文还有配套的精品资源,点击获取