一、问题背景与核心挑战

在工业控制、音频处理等场景,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 的型号,缓存维护不可忽视。通过合理配置和代码实践,可构建高效稳定的数据采集系统。希望本文能助你避开常见陷阱,提升开发效率。