STM32H7 双缓冲搬运的时序陷阱:MDMA 与 LPDMA 协同作战的深度解析

在 STM32H7 系列中,MDMA(Master DMA)和 LPDMA(Low-Power DMA)是两员猛将:MDMA 擅长内存到内存的大块搬运,LPDMA 则负责外设与内存间的低功耗传输。当它们协同完成双缓冲任务时,若忽视时序细节,轻则数据错乱,重则系统死锁。本文将带你拆解这些陷阱,并给出实战规避方案。

一、为什么需要 MDMA 与 LPDMA 协同?

双缓冲(Ping-Pong Buffer)是嵌入式系统中常用的数据流优化手段:一块缓冲区在接收数据时,另一块被处理或搬运,从而隐藏延迟。在 STM32H7 中,典型场景是:

  • LPDMA 将外设(如 ADC、UART)数据持续写入缓冲区 A/B。
  • MDMA 将已满的缓冲区搬运到内存中的大容量存储区(如 SDRAM)。

两者并行工作,理论上能实现零拷贝流水线。但实际中,由于 MDMA 和 LPDMA 共享系统总线,且各自有独立的传输完成中断,时序协调成为关键。

二、时序陷阱:三个隐藏的雷区

陷阱 1:缓冲区切换的竞态条件

当 LPDMA 正在写缓冲区 A 时,MDMA 可能已开始搬运 A。若 MDMA 读取速度慢于 LPDMA 写入速度,会发生读半写半的数据错乱。反之,若 MDMA 提前完成,LPDMA 可能尚未填满,导致搬运不完整数据。

核心问题:两个 DMA 的完成事件是异步的,没有硬件同步机制。

陷阱 2:MDMA 的突发传输与总线占用

MDMA 默认使用突发传输(Burst),一次可搬运 16 或 32 字节。若 LPDMA 正在写入同一内存区域,MDMA 的突发读取可能打断 LPDMA 的写入,造成数据撕裂。

陷阱 3:缓存一致性问题

STM32H7 的 D-Cache 默认开启,MDMA 和 LPDMA 访问内存时绕过 Cache。若 CPU 或 DMA 写入的数据仍留在 Cache 中,另一方读取时将得到旧数据。

三、规避策略:硬件信号量 + 双缓冲状态机

1. 使用硬件信号量(HSEM)实现互斥

STM32H7 内置硬件信号量(HSEM),支持多主设备间原子操作。我们可以为每个缓冲区分配一个信号量,约定:

  • LPDMA 写入前获取信号量,写完后释放。
  • MDMA 搬运前获取信号量,搬完后释放。

这样确保同一时刻只有一个 DMA 操作缓冲区。

2. 双缓冲状态机设计

定义三个状态:

  • READY:缓冲区可被写入。
  • FULL:缓冲区已填满,等待搬运。
  • EMPTY:缓冲区已搬空,可再次写入。

通过中断回调更新状态,并利用 HSEM 保护状态变量。

3. 缓存维护

在 DMA 操作前后,必须调用 SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 确保数据一致性。

四、代码示例:基于 HAL 库的实现

以下示例展示 LPDMA 接收 UART 数据,MDMA 搬运到 SDRAM 的核心逻辑。

/* 缓冲区定义 */
#define BUF_SIZE 1024
uint8_t bufA[BUF_SIZE] __attribute__((aligned(32)));
uint8_t bufB[BUF_SIZE] __attribute__((aligned(32)));
uint8_t *currentBuf = bufA;

/* 信号量句柄 */
HAL_HSEM_HandleTypeDef hsem;

/* 状态枚举 */
typedef enum { READY, FULL, EMPTY } buf_state_t;
buf_state_t stateA = READY, stateB = READY;

/* LPDMA 传输完成回调 */
void HAL_LPDMA_TransferComplete(DMA_HandleTypeDef *hdma) {
    if (hdma->Instance == LPDMA1_Channel0) {
        // 获取信号量保护状态
        HAL_HSEM_Get(&hsem, 0);
        if (currentBuf == bufA) {
            stateA = FULL;
        } else {
            stateB = FULL;
        }
        HAL_HSEM_Release(&hsem, 0);
        
        // 切换缓冲区
        currentBuf = (currentBuf == bufA) ? bufB : bufA;
        
        // 启动下一次 LPDMA 接收
        HAL_UART_Receive_DMA(&huart1, currentBuf, BUF_SIZE);
        
        // 触发 MDMA 搬运已满缓冲区
        if (stateA == FULL) {
            MDMA_StartTransfer(bufA, sdram_addr, BUF_SIZE);
        } else if (stateB == FULL) {
            MDMA_StartTransfer(bufB, sdram_addr, BUF_SIZE);
        }
    }
}

/* MDMA 传输完成回调 */
void HAL_MDMA_TransferComplete(MDMA_HandleTypeDef *hmdma) {
    HAL_HSEM_Get(&hsem, 0);
    if (hmdma->Init.SourceAddress == (uint32_t)bufA) {
        stateA = EMPTY;
    } else {
        stateB = EMPTY;
    }
    HAL_HSEM_Release(&hsem, 0);
}

/* 初始化函数 */
void DMA_Init(void) {
    // 初始化 HSEM
    hsem.Instance = HSEM;
    HAL_HSEM_Init(&hsem);
    
    // 配置 LPDMA 接收 UART 数据
    // 配置 MDMA 搬运,注意设置突发传输为 16 字节,并开启 FIFO
    // 配置完成后启动第一次接收
    HAL_UART_Receive_DMA(&huart1, bufA, BUF_SIZE);
}

/* MDMA 启动函数 */
void MDMA_StartTransfer(uint8_t *src, uint32_t dst, uint32_t size) {
    // 清理源地址的 D-Cache,确保数据可见
    SCB_CleanDCache_by_Addr((uint32_t*)src, size);
    
    // 配置 MDMA 传输(此处省略具体配置,使用 HAL_MDMA_Start_IT)
    HAL_MDMA_Start_IT(&hmdma, (uint32_t)src, dst, size);
}

五、注意事项与调试技巧

  • 信号量超时:HSEM 获取时需设置超时,避免死锁。建议使用 HAL_HSEM_Get 的阻塞模式,并加入超时计数。
  • 中断优先级:LPDMA 和 MDMA 中断优先级应设置为不同级别,避免嵌套导致状态错乱。通常 LPDMA 优先级高于 MDMA。
  • FIFO 与突发配置:MDMA 建议启用 FIFO,并将突发大小设为 16 字节,以减少总线冲突。
  • 调试方法:使用逻辑分析仪抓取 DMA 的请求信号,或通过断点观察状态变量,确认时序是否符合预期。
  • 缓存一致性:除了 DMA 前后维护 Cache,还需确保缓冲区地址 32 字节对齐,否则 SCB_CleanDCache_by_Addr 可能报错。

六、总结

MDMA 与 LPDMA 的协同并非简单拼接,而是需要精心设计同步机制。通过硬件信号量保护状态、合理配置突发传输、维护缓存一致性,可以彻底规避时序陷阱。希望本文能帮助你构建稳定高效的 STM32H7 数据搬运系统。