一、为什么 D-Cache 会成为 DMA 的“猪队友”?
STM32H7 的 Cortex-M7 内核包含 16KB 的 D-Cache(数据缓存)。CPU 访问数据时,若命中缓存则直接读写缓存,不会立即同步到 SRAM。而 DMA 是独立于 CPU 的总线主设备,它直接读写物理内存(SRAM)。
当两者同时操作同一块缓冲区时,就会出现:
- CPU 写数据到缓存,但未写回 SRAM → DMA 从 SRAM 读到旧数据。
- DMA 写入新数据到 SRAM,但 CPU 缓存中仍是旧数据 → CPU 读到旧值。
这就是缓存一致性问题。STM32H7 提供了 SCB_CleanDCache_by_Addr() 和 SCB_InvalidateDCache_by_Addr() 等函数来手动维护一致性,但误用会导致更隐蔽的偶发错误。
二、典型误用场景与现象
2.1 只 Clean 不 Invalidate(发送方向)
DMA 发送(内存→外设)前,必须将 CPU 写入缓存的数据 Clean(写回 SRAM)。若忘记 Clean,DMA 可能发送旧数据。
2.2 只 Invalidate 不 Clean(接收方向)
DMA 接收(外设→内存)前,若缓冲区已被 CPU 写过,必须先 Clean 再 Invalidate。否则 Invalidate 会丢弃未写回的脏数据,导致数据丢失。
2.3 在 DMA 传输过程中 Invalidate
DMA 正在写 SRAM 时,CPU 执行 Invalidate 会丢弃部分已写入的数据,造成数据错乱。
2.4 对非缓存对齐地址操作
STM32H7 的 Cache 行大小为 32 字节。若操作地址未按 32 字节对齐,Clean/Invalidate 会影响相邻数据,引发随机错误。
三、系统化排查路径
遇到偶发数据错乱时,按以下步骤排查:
-
确认是否启用 D-Cache:检查
SCB_EnableDCache()是否被调用。 - 定位共享缓冲区:找出 CPU 与 DMA 共同访问的内存区域。
-
检查操作顺序:
- 发送前:是否执行了 Clean?
- 接收前:是否执行了 Clean + Invalidate?
- 是否在 DMA 传输中误操作 Cache?
- 检查地址对齐:缓冲区起始地址和长度是否 32 字节对齐?
- 使用 MPU 配置:将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable,可从根本上避免一致性问题。
-
调试手段:
- 暂时关闭 D-Cache,若问题消失,则确认为缓存一致性导致。
- 在 Clean/Invalidate 前后加内存屏障
__DSB()。 - 使用 ITM 或 GPIO 翻转测量时序。
四、正确配置步骤与完整代码
4.1 启用 D-Cache 与 MPU 配置
#include "stm32h7xx.h"
// 定义 DMA 缓冲区,强制 32 字节对齐
__attribute__((aligned(32))) uint8_t dma_rx_buf[256];
__attribute__((aligned(32))) uint8_t dma_tx_buf[256];
void Cache_Init(void)
{
// 启用 D-Cache
SCB_EnableDCache();
SCB_EnableICache();
// 配置 MPU:将 DMA 缓冲区区域设为 Non-Cacheable
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = (uint32_t)dma_rx_buf & 0xFFFFFFE0; // 32字节对齐
MPU_InitStruct.Size = MPU_REGION_SIZE_256B;
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_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
4.2 DMA 发送(内存→外设)
void DMA_SendData(uint8_t *src, uint16_t len)
{
// 确保数据已写入 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)src, len);
__DSB(); // 数据同步屏障
HAL_DMA_Start(&hdma_memtomem, (uint32_t)src, (uint32_t)&UART->TDR, len);
}
4.3 DMA 接收(外设→内存)
void DMA_ReceiveData(uint8_t *dst, uint16_t len)
{
// 先 Clean 避免丢弃脏数据,再 Invalidate 确保读取新数据
SCB_CleanDCache_by_Addr((uint32_t *)dst, len);
SCB_InvalidateDCache_by_Addr((uint32_t *)dst, len);
__DSB();
HAL_DMA_Start(&hdma_uart_rx, (uint32_t)&UART->RDR, (uint32_t)dst, len);
}
// DMA 传输完成中断中,读取数据前再次 Invalidate
void DMA_Complete_Callback(void)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, sizeof(dma_rx_buf));
__DSB();
// 此时可安全读取 dma_rx_buf
}
五、注意事项与最佳实践
- 优先使用 MPU:将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through,可一劳永逸,避免手动维护。
- 地址对齐:所有 Clean/Invalidate 操作的地址和长度必须按 32 字节对齐,否则会破坏相邻数据。
- 顺序不能错:接收方向必须先 Clean 再 Invalidate;发送方向只需 Clean。
-
加内存屏障:在 Cache 操作后、启动 DMA 前插入
__DSB(),确保操作完成。 - 避免传输中操作:DMA 传输期间严禁对缓冲区执行 Invalidate。
- 调试技巧:若问题偶发且与数据量相关,优先怀疑 Cache 一致性;可暂时关闭 D-Cache 验证。
-
使用
SCB_InvalidateDCache_by_Addr而非全局 Invalidate:全局操作影响范围大,可能误伤其他数据。
掌握以上原理与排查方法,STM32H7 的 D-Cache 与 DMA 一致性问题将不再神秘。正确配置 MPU 并遵循 Clean/Invalidate 规范,可显著提升系统稳定性。