一、问题背景与核心挑战
在工业控制、音频处理等场景,STM32F4 的 ADC 支持多通道扫描采样,配合 DMA 可无 CPU 干预地将数据连续搬运至内存。但 168MHz 主频下,系统复杂度和时序要求显著提升,主要面临两大难题:
- 数据错位:多通道扫描时,若 DMA 传输与 ADC 转换完成事件不同步,或缓冲区边界处理不当,会导致通道数据错位(如 CH1 数据写入 CH2 的位置)。
- 缓存一致性:STM32F4 部分型号(如 F407)无 D-Cache,但若使用带 Cache 的 F4 系列(如 F429),DMA 直接访问内存时,CPU 缓存中的脏数据可能未被写回,导致 DMA 读到旧数据或 CPU 读到 DMA 写入但未失效的数据。
二、原理剖析:ADC 扫描与 DMA 协同机制
1. ADC 扫描模式与数据序列
STM32F4 的 ADC 支持规则组扫描模式,可配置多个通道按顺序转换。每次转换完成后,数据存入 ADC_DR 寄存器,并触发 DMA 请求。DMA 将 ADC_DR 的值搬运至内存缓冲区。
关键点:每个通道转换完成都会触发一次 DMA 请求,因此 DMA 传输次数必须与通道数严格匹配。若配置不当(如 DMA 缓冲区大小不是通道数的整数倍),会导致后续数据错位。
2. DMA 循环模式与双缓冲
- 循环模式:DMA 传输完一轮后自动重载地址,适合连续采样。但需注意,循环模式下 DMA 会持续覆盖缓冲区,若 CPU 读取速度跟不上,可能读到半新半旧的数据。
- 双缓冲模式:使用两个缓冲区,DMA 在写一个缓冲区时,CPU 可读另一个,通过中断切换,避免数据覆盖冲突。这是解决数据错位的经典方案。
3. 缓存一致性(以 F429 为例)
带 D-Cache 的 F4 系列,CPU 和 DMA 访问内存时存在缓存一致性问题。DMA 写入内存时,若对应地址在 D-Cache 中已被缓存,CPU 读取时可能拿到旧值;反之,CPU 修改数据后,DMA 可能读到未写回的数据。
解决方案:
- 使用
SCB_CleanDCache()在 DMA 启动前将 CPU 数据写回内存。 - 使用
SCB_InvalidateDCache()在 DMA 完成后使缓存失效,强制 CPU 从内存重新读取。
三、配置步骤(基于 CubeMX + HAL 库)
1. ADC 配置
- 开启 ADC1,选择扫描模式(Scan Mode),连续转换(Continuous Mode)。
- 添加多个通道(如 CH0~CH3),设置采样时间(如 84MHz 下 15 周期)。
- 配置 DMA 请求为循环模式,数据宽度为半字(16 位,ADC 精度 12 位)。
2. DMA 配置
- 选择 DMA2 Stream0(ADC1 专用),方向为外设到内存。
- 模式设为 Circular,外设地址固定(&hadc1.Instance->DR),内存地址递增。
- 数据长度设置为通道数(如 4)。
3. 双缓冲实现(手动切换)
HAL 库支持 DMA 双缓冲,但需在回调中切换缓冲区。示例:
#define ADC_CH_NUM 4
uint16_t adc_buf[2][ADC_CH_NUM];
volatile uint8_t buf_index = 0;
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
if (hadc->Instance == ADC1) {
buf_index ^= 1; // 切换缓冲区
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf[buf_index], ADC_CH_NUM);
}
}
注意:首次启动时,需先启动 DMA 到缓冲区 0,然后在回调中切换。
四、完整代码示例(基于 STM32F407)
#include "main.h"
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
#define ADC_CH_NUM 4
uint16_t adc_buf[2][ADC_CH_NUM];
volatile uint8_t buf_index = 0;
void ADC_Init(void) {
__HAL_RCC_ADC1_CLK_ENABLE();
__HAL_RCC_DMA2_CLK_ENABLE();
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK2_DIV4; // 84MHz/4=21MHz
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = ENABLE;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.NbrOfConversion = ADC_CH_NUM;
HAL_ADC_Init(&hadc1);
// 配置通道
ADC_ChannelConfTypeDef sConfig = {0};
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_15CYCLES;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
// 重复配置 CH1~CH3...
// DMA 配置
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);
// 启动 DMA 到缓冲区 0
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf[0], ADC_CH_NUM);
}
// 在中断回调中切换缓冲区
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
if (hadc->Instance == ADC1) {
buf_index ^= 1;
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf[buf_index], ADC_CH_NUM);
}
}
// 主循环中读取当前缓冲区(注意临界区保护)
void ProcessADCData(void) {
uint8_t idx = buf_index ^ 1; // 读取非活动缓冲区
for (int i = 0; i < ADC_CH_NUM; i++) {
printf("CH%d: %d\n", i, adc_buf[idx][i]);
}
}
五、避免数据错位的关键策略
- 缓冲区大小对齐:确保 DMA 缓冲区大小是通道数的整数倍,且每次传输长度等于通道数。
- 使用双缓冲:避免 CPU 和 DMA 同时访问同一缓冲区,通过回调切换,保证数据完整性。
- 临界区保护:在读取缓冲区时,暂时屏蔽 ADC DMA 中断,防止切换导致数据不一致。
- 校验通道顺序:在调试时,可给各通道施加不同电压,验证数据对应关系。
六、缓存一致性处理(针对带 D-Cache 的 F4)
若使用 STM32F429 等带 D-Cache 的型号,需在 DMA 操作前后维护缓存:
// 启动 DMA 前,将 CPU 数据写回内存(若缓冲区有 CPU 写入的数据)
SCB_CleanDCache();
// DMA 完成后,使缓存失效,强制 CPU 从内存读取新数据
SCB_InvalidateDCache();
注意:在双缓冲切换时,也要对即将使用的缓冲区执行相应操作。
七、注意事项与常见陷阱
- DMA 中断优先级:确保 DMA 中断优先级高于可能抢占它的其他中断,避免数据丢失。
- ADC 采样时间:过短的采样时间可能导致通道间串扰,建议根据信号源阻抗调整。
- 主频与 ADC 时钟:168MHz 主频下,ADC 时钟最高 36MHz(APB2 分频),需合理配置预分频。
- 调试时禁用优化:优化可能导致变量缓存,影响数据一致性,调试时建议 -O0。
- 使用 volatile 修饰共享变量:防止编译器优化导致读取旧值。
八、总结
在 STM32F4 168MHz 下实现多路 ADC DMA 连续采样,核心在于理解扫描模式与 DMA 的同步机制,并采用双缓冲策略避免数据错位。对于带 D-Cache 的型号,缓存维护不可忽视。通过合理配置和代码实践,可构建高效稳定的数据采集系统。希望本文能助你避开常见陷阱,提升开发效率。