# 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_Addr` 和 `SCB_InvalidateDCache_by_Addr` 确保数据一致性。 ## 四、代码示例:基于 HAL 库的实现 以下示例展示 LPDMA 接收 UART 数据,MDMA 搬运到 SDRAM 的核心逻辑。 ```c /* 缓冲区定义 */ #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 数据搬运系统。