一、为什么 DMA 和 D-Cache 会打架?
STM32H7 主频高达 480MHz,为了匹配 CPU 速度,Cortex-M7 内核配备了 D-Cache(数据缓存)。CPU 访问内存时,数据可能只停留在 Cache 中,并未写回 SRAM。而 DMA 是“绕过 CPU”直接访问 SRAM 的搬运工,它看不到 Cache 里的新数据。
典型场景:
- CPU 准备好发送缓冲区 → 数据还在 D-Cache → 启动 DMA 发送 → DMA 从 SRAM 读到旧数据。
- DMA 接收数据写入 SRAM → CPU 读缓冲区 → CPU 从 D-Cache 读到旧数据。
这就是Cache 一致性(Coherency)问题。
二、三种解决思路对比
| 方案 | 原理 | 优点 | 缺点 | |------|------|------|------| | Cache 维护 | 手动 Clean/Invalidate | 灵活,适合小缓冲区 | 易漏、性能损耗 | | MPU 配置 Write-Through | 写操作同时更新 SRAM | 无需手动 Clean | 仍有读缓存问题 | | MPU 配置 Non-Cacheable | 该区域完全绕过 Cache | 一劳永逸 | 降低 CPU 访问速度 |
实际工程中常组合使用:DMA 缓冲区用 Non-Cacheable,其他区域保留 Cache。
三、Cache 维护操作:Clean 与 Invalidate
CMSIS 提供以下函数(需包含 core_cm7.h):
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
- Clean:把 Cache 中已修改的数据写回 SRAM。用于 CPU 写 → DMA 读 之前。
- Invalidate:丢弃 Cache 内容,强制下次从 SRAM 重新加载。用于 DMA 写 → CPU 读 之前。
- CleanInvalidate:两者都做,用于双向传输。
注意:地址必须 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];
// 发送前:清理 D-Cache
SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, DMA_BUF_SIZE);
HAL_DMA_Start(&hdma_memtomem, (uint32_t)dma_tx_buf, (uint32_t)dest, DMA_BUF_SIZE);
// 接收后:无效化 D-Cache
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, DMA_BUF_SIZE);
process_data(dma_rx_buf);
四、MPU 配置:从根源上隔离 DMA 缓冲区
更优雅的方式是用 MPU 将 DMA 缓冲区设为 Non-Cacheable 或 Write-Through。
4.1 配置步骤
- 使能 MPU:
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk; - 关闭 MPU:
MPU->CTRL = 0; - 配置 Region:基地址、大小、属性。
- 使能 Region 和 MPU。
4.2 完整代码示例
#include "stm32h7xx.h"
// 假设 DMA 缓冲区放在 D2 SRAM,地址 0x30000000,大小 32KB
#define DMA_BUFFER_ADDR 0x30000000
#define DMA_BUFFER_SIZE 0x8000 // 32KB
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// Region 0: DMA 缓冲区设为 Non-Cacheable, Non-Shared
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = DMA_BUFFER_ADDR;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
// Region 1: 其他 SRAM 保持 Write-Back, Write-Allocate
MPU_InitStruct.Number = MPU_REGION_NUMBER1;
MPU_InitStruct.BaseAddress = 0x24000000; // AXI SRAM
MPU_InitStruct.Size = MPU_REGION_SIZE_512KB;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
4.3 链接脚本调整
确保 DMA 缓冲区真的落在 Non-Cacheable 区域:
MEMORY
{
DTCMRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
AXI_SRAM (xrw) : ORIGIN = 0x24000000, LENGTH = 512K
D2_SRAM (xrw) : ORIGIN = 0x30000000, LENGTH = 288K
}
SECTIONS
{
.dma_buffer (NOLOAD) :
{
. = ALIGN(32);
*(.dma_buffer)
. = ALIGN(32);
} > D2_SRAM
}
在代码中指定段:
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_rx_buf[4096];
五、排查路径与注意事项
5.1 快速排查清单
- 确认 DMA 缓冲区地址是否在 Non-Cacheable 区域。
- 若未配 MPU,检查是否在 DMA 启动前后正确调用了 Clean/Invalidate。
- 检查缓冲区是否 32 字节对齐。
- 检查
SCB_EnableDCache()是否已调用(HAL 初始化中默认开启)。 - 使用
__DSB()和__ISB()确保内存操作完成。
5.2 常见坑点
- Invalidate 误伤:如果 CPU 刚写过缓冲区,直接 Invalidate 会丢弃未写回的数据。应先 Clean 再 Invalidate。
- 多缓冲区共享 Cache Line:32 字节 Cache Line 内混有 DMA 和非 DMA 数据,Invalidate 会误丢其他变量。务必对齐并隔离。
- DMA 描述符:链表式 DMA 的描述符本身也需 Non-Cacheable 或手动 Clean。
- 中断中操作 Cache:Clean/Invalidate 耗时,避免在高速中断中频繁调用。
5.3 推荐实践
- 所有 DMA 缓冲区统一放在 MPU 配置的 Non-Cacheable 区域。
- 若必须用 Cache,封装
dma_send_prepare()和dma_recv_complete()统一处理。 - 使用
SCB_InvalidateDCache_by_Addr时,长度参数用size + 31 & ~31向上取整。
六、总结
STM32H7 的 D-Cache 是把双刃剑。理解 Clean/Invalidate 的语义,结合 MPU 将 DMA 缓冲区设为 Non-Cacheable,是解决一致性问题的标准路径。建议在项目初期就规划好内存布局,避免后期调试时被“随机数据错乱”折磨。记住:DMA 不认 Cache,只认 SRAM。