STM32H7 双缓冲搬运的时序陷阱:MDMA 与 LPDMA 协同作战的深度解析
👁 3 阅读 · 2026-08-27 · 嵌入式
在 STM32H7 高性能系列中,MDMA 与 LPDMA 的协同工作能实现高效的双缓冲数据搬运,但两者之间微妙的时序关系常导致数据错位或丢失。本文深入剖析 MDMA 与 LPDMA 在双缓冲场景下的时序陷阱,提供基于硬件信号量的同步策略、代码示例及关键注意事项,助你规避隐性 Bug,释放 H7 的极致带宽。
# 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 数据搬运系统。