STM32H7 使用 MDMA 与双缓冲实现 ADC 连续采样零 CPU 干预的配置陷阱

引言

在高速数据采集系统中,ADC 连续采样往往要求 CPU 零干预,以释放算力给核心算法。STM32H7 系列凭借其强大的 MDMA(Master DMA)和双缓冲(Double Buffer)机制,成为实现这一目标的理想平台。然而,MDMA 的灵活性与复杂性并存,配置不当极易引发数据错乱、传输中断甚至系统死锁。本文将深入剖析配置中的关键陷阱,并提供一套经过验证的解决方案。

原理基础

MDMA 与 DMA 的区别

STM32H7 拥有两种 DMA:传统 DMA(DMA1/DMA2)和 MDMA。MDMA 支持 128 位带宽、4KB 突发传输和链表模式,可访问所有存储器(包括 AXI SRAM),并能与 DMA 级联。在 ADC 连续采样场景中,我们通常使用 DMA1 将 ADC 数据搬运至 SRAM,再由 MDMA 将数据块搬移至最终缓冲区,实现两级流水线。

双缓冲机制

双缓冲(Double Buffer)通过两个缓冲区交替使用,避免数据覆盖。在 MDMA 中,可通过配置 MDMA_CTR 寄存器的 DBUF 位启用双缓冲,并设置两个目标地址。当 MDMA 完成一次传输后,自动切换至另一缓冲区,同时触发中断通知 CPU 处理已满缓冲区。

配置陷阱剖析

陷阱一:MDMA 通道选择错误

MDMA 有 16 个通道,但并非所有通道都支持双缓冲。查阅参考手册,只有通道 0-7 支持 DBUF 功能。若使用通道 8-15,双缓冲配置将被忽略,导致数据覆盖。

解决方案:务必选择通道 0-7 之一,并在初始化时检查 MDMA_CCRDBUF 位是否置位。

陷阱二:缓冲区地址对齐要求

MDMA 要求源和目的地址必须按传输宽度对齐。若使用 32 位数据,地址需 4 字节对齐;若使用 128 位突发,需 16 字节对齐。未对齐时,MDMA 可能产生总线错误或数据错位。

解决方案:使用 __attribute__((aligned(32))) 声明缓冲区,或通过 memalign 动态分配。

陷阱三:传输结束回调的触发条件

MDMA 的传输完成中断(TCIF)在每次块传输完成后触发,但若未正确配置 TCIETEIE,中断可能不产生。此外,双缓冲模式下,中断标志需在回调中手动清除,否则会持续触发。

解决方案:在中断服务函数中读取 MDMA_CISR 清除标志,并确保 MDMA_CCRTCIE 置位。

陷阱四:与 DMA 级联时的同步问题

当 DMA1 搬运 ADC 数据至中间缓冲区,MDMA 再搬运至最终缓冲区时,需确保 DMA1 完成一帧后 MDMA 才启动。若两者独立运行,可能产生数据竞争。

解决方案:使用 DMA1 的传输完成中断来触发 MDMA 启动,或利用 MDMA 的硬件请求信号(如 MDMA_REQ)与 DMA1 联动。

配置步骤

  1. 初始化 ADC:配置 ADC 为连续扫描模式,使能 DMA 请求。
  2. 配置 DMA1:选择 ADC 的 DMA 通道,设置数据宽度为半字(16 位),循环模式,并指定中间缓冲区。
  3. 配置 MDMA:选择支持双缓冲的通道(如通道 0),设置源地址为中间缓冲区,目的地址为双缓冲区的两个地址,启用 DBUF,传输宽度为 32 位,突发大小为 4。
  4. 使能中断:在 MDMA 中断服务函数中处理数据并清除标志。
  5. 启动流程:先使能 MDMA,再使能 DMA1,最后启动 ADC。

完整代码示例

以下代码基于 STM32H743,使用 HAL 库,实现 16 位 ADC 连续采样,双缓冲大小为 1024 字节。

#include "stm32h7xx_hal.h"

// 缓冲区定义,32 字节对齐
__attribute__((aligned(32))) uint32_t dma_buf[256];      // 中间缓冲区(DMA1 写入)
__attribute__((aligned(32))) uint32_t mdma_buf[2][256];  // 双缓冲区(MDMA 写入)

volatile uint8_t buf_index = 0;  // 当前使用的缓冲区索引

// ADC 句柄和 DMA 句柄
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
MDMA_HandleTypeDef hmdma_mdma;

void MDMA_Init(void) {
    __HAL_RCC_MDMA_CLK_ENABLE();

    hmdma_mdma.Instance = MDMA_Channel0;
    hmdma_mdma.Init.Request = MDMA_REQUEST_DMA1_ADC1;  // 由 DMA1 触发
    hmdma_mdma.Init.TransferTriggerMode = MDMA_BLOCK_TRANSFER;  // 块传输
    hmdma_mdma.Init.Priority = MDMA_PRIORITY_HIGH;
    hmdma_mdma.Init.Endianness = MDMA_LITTLE_ENDIAN;
    hmdma_mdma.Init.SourceInc = MDMA_SRC_INC_WORD;   // 源地址递增,字
    hmdma_mdma.Init.DestinationInc = MDMA_DEST_INC_WORD;  // 目的地址递增
    hmdma_mdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_WORD;  // 32 位
    hmdma_mdma.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD;
    hmdma_mdma.Init.DataAlignment = MDMA_DATAALIGN_PACK;
    hmdma_mdma.Init.BufferTransferLength = 256;  // 每次传输 256 字
    hmdma_mdma.Init.SourceBurst = MDMA_SOURCE_BURST_4BEATS;
    hmdma_mdma.Init.DestBurst = MDMA_DEST_BURST_4BEATS;
    hmdma_mdma.Init.SourceBlockAddressOffset = 0;
    hmdma_mdma.Init.DestBlockAddressOffset = 0;
    hmdma_mdma.Init.DoubleBufferMode = MDMA_DOUBLEBUFFER_MODE_ENABLE;  // 启用双缓冲
    hmdma_mdma.Init.DoubleBufferBaseAddress = (uint32_t)mdma_buf;  // 双缓冲基地址
    hmdma_mdma.Init.DoubleBufferCurrentBuffer = 0;  // 初始使用缓冲区 0

    HAL_MDMA_Init(&hmdma_mdma);

    // 配置中断
    HAL_NVIC_SetPriority(MDMA_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(MDMA_IRQn);
}

void MDMA_Start(void) {
    // 源地址为 DMA1 的中间缓冲区,目的地址为双缓冲基地址
    HAL_MDMA_Start(&hmdma_mdma, (uint32_t)dma_buf, (uint32_t)mdma_buf, 256, 1);
}

void MDMA_IRQHandler(void) {
    HAL_MDMA_IRQHandler(&hmdma_mdma);
}

void HAL_MDMA_TCIncCallback(MDMA_HandleTypeDef *hmdma) {
    // 传输完成,处理当前缓冲区数据
    buf_index = hmdma->Init.DoubleBufferCurrentBuffer ^ 1;  // 获取已满的缓冲区索引
    // 此处可处理 mdma_buf[buf_index] 中的数据
    // 注意:需在回调中清除中断标志,HAL 库已自动清除,但若使用非 HAL 需手动
}

// 主函数中初始化并启动
int main(void) {
    HAL_Init();
    // 配置时钟、ADC、DMA1 等(略)
    MDMA_Init();
    MDMA_Start();
    // 启动 ADC 和 DMA1(略)
    while (1) {
        // 主循环无需干预数据搬运
    }
}

注意事项

  • 缓冲区大小:MDMA 的 BufferTransferLength 必须与 DMA1 的传输长度匹配,否则可能丢失数据或产生溢出。
  • 中断优先级:MDMA 中断优先级应高于其他非关键中断,但低于实时性要求更高的中断(如系统节拍)。
  • 调试技巧:若数据错乱,先检查地址对齐和 DBUF 位;若中断不触发,检查 TCIETEIE 是否使能。
  • 功耗考虑:零 CPU 干预虽好,但 MDMA 和 DMA 的持续运行会增加功耗,低功耗场景需权衡。

结语

STM32H7 的 MDMA 与双缓冲组合为 ADC 连续采样提供了强大的零 CPU 干预方案,但配置陷阱众多。通过理解原理、遵循正确的配置步骤,并注意地址对齐、通道选择等细节,即可稳定实现高速数据采集。希望本文能助你在嵌入式开发中少走弯路。