为什么 H7 上 DMA 一用就出错?
STM32H7 的 Cortex-M7 内核带 16KB 的 D-Cache(数据缓存)。CPU 读写内存时,数据可能只停留在 Cache 里,并没有真正写回 SRAM。而 DMA 是直接访问物理内存的“搬运工”,它看不见 Cache。
于是出现两类经典问题:
- 发送方向(内存 → 外设):CPU 刚填好的缓冲区还在 Cache 里,DMA 却从 SRAM 读到了旧数据。
- 接收方向(外设 → 内存):DMA 把新数据写进 SRAM,但 CPU 读到的却是 Cache 里的旧副本。
解决手段就是两个操作:Clean(清写回) 和 Invalidate(无效化)。
Clean 与 Invalidate 到底做了什么
- Clean(CleanDCache):把 Cache 中“脏”的数据写回 SRAM,保证内存里是最新的。Cache 内容保留。
- Invalidate(InvalidateDCache):把 Cache 行标记为无效,下次 CPU 读取时会重新从 SRAM 加载。注意:如果 Cache 里有脏数据,直接 Invalidate 会丢数据。
一句话记忆:
- 数据要给 DMA 读(发送)→ 先 Clean。
- 数据要被 DMA 写(接收)→ 先 Invalidate(且必须保证 Cache 无脏数据)。
关键陷阱:Cache 行对齐
Cortex-M7 的 D-Cache 行大小是 32 字节。Clean/Invalidate 是按行操作的,不是按字节。
如果缓冲区没有 32 字节对齐,或长度不是 32 的整数倍,操作会波及相邻数据,可能:
- 把不该清的数据清掉;
- 把相邻变量所在的 Cache 行无效化,导致数据丢失。
最佳实践:DMA 缓冲区用 __attribute__((aligned(32))) 强制 32 字节对齐,长度也按 32 对齐。
#define DMA_BUF_SIZE 256
__attribute__((aligned(32)))
uint8_t dma_tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32)))
uint8_t dma_rx_buf[DMA_BUF_SIZE];
正确时机:发送与接收分别怎么放
发送(内存 → 外设)
顺序:CPU 填数据 → Clean → 启动 DMA。
// 1. CPU 填充发送缓冲区
for (int i = 0; i < DMA_BUF_SIZE; i++) {
dma_tx_buf[i] = i & 0xFF;
}
// 2. 把 Cache 写回 SRAM,确保 DMA 读到最新数据
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, DMA_BUF_SIZE);
// 3. 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, DMA_BUF_SIZE);
接收(外设 → 内存)
顺序:Invalidate → 启动 DMA → 等待完成 → CPU 读数据。
// 1. 无效化 Cache,防止 CPU 读到旧副本
// 注意:此时缓冲区不能有未写回的脏数据
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, DMA_BUF_SIZE);
// 2. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, DMA_BUF_SIZE);
// 3. 等待 DMA 完成(中断或轮询)
while (HAL_UART_GetState(&huart1) != HAL_UART_STATE_READY) { }
// 4. 再次无效化,确保 CPU 读到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, DMA_BUF_SIZE);
// 5. 此时读取才是安全的
process_data(dma_rx_buf, DMA_BUF_SIZE);
接收方向为什么启动前也要 Invalidate?因为缓冲区可能残留上次的 Cache 行,若 DMA 只写了部分数据,CPU 可能读到“新旧混合”的内容。启动前无效化可避免这种脏读。
更省心的方案:MPU 配置为 Non-Cacheable
如果某块内存只用于 DMA,最省事的办法是用 MPU 把它配成 Non-Cacheable,从此不用手动 Clean/Invalidate。
void MPU_Config_DMA_Region(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000; // D2 SRAM 示例
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
把 DMA 缓冲区放到这块 Non-Cacheable 区域,代码里就再也不用关心 Clean/Invalidate 了。代价是 CPU 访问这块内存会慢一些,适合“DMA 频繁、CPU 少读”的场景。
常见坑与避坑清单
- 只 Clean 不 Invalidate,或反过来:发送只 Clean,接收只 Invalidate,别搞混。
-
缓冲区未 32 字节对齐:会导致相邻数据被误伤,务必
aligned(32)。 - 对含脏数据的区域 Invalidate:会丢失尚未写回的数据,接收前先确保无脏数据。
- 在 DMA 传输中途操作 Cache:可能造成数据错乱,务必在启动前或完成后操作。
-
忘记
SCB_EnableDCache():H7 默认可能未开 Cache,先确认状态。 -
用
SCB_CleanInvalidateDCache()图省事:它会同时清和无效化,接收场景可能丢数据,慎用。 - 中断里频繁 Clean/Invalidate:开销不小,能配 Non-Cacheable 就配。
小结
H7 的 D-Cache 与 DMA 一致性,核心就三句话:
- 发送前 Clean,让 DMA 读到最新数据。
- 接收前后 Invalidate,让 CPU 读到 DMA 的新数据。
- 缓冲区 32 字节对齐,或直接用 MPU 配成 Non-Cacheable。
把时机放对,H7 的高性能 Cache 和 DMA 就能安心一起用,不再“玄学丢数据”。