一、为什么 DMA 和 D-Cache 会打架?
STM32H7 搭载 Cortex-M7 内核,主频可达 480MHz,为了弥补 CPU 与内存之间的速度鸿沟,芯片内部集成了 D-Cache(数据缓存)。CPU 访问数据时,会先将内存中的数据加载到 Cache 中,后续读写直接操作 Cache,从而大幅提升性能。
但 DMA(直接存储器访问)是独立于 CPU 的硬件单元,它直接读写物理内存,不经过 Cache。这就导致了经典的一致性问题:
- CPU 写,DMA 读:CPU 将数据写入 Cache,但尚未写回内存(Write-Back 模式),此时启动 DMA 发送,DMA 读到的是内存中的旧数据。
- DMA 写,CPU 读:DMA 将新数据写入内存,但 CPU 读取时命中 Cache 中的旧数据,导致读到错误值。
简单说:Cache 让 CPU 和 DMA 看到了两个不同的“内存视图”。
二、解决思路:MPU 配置 + Cache 维护
针对上述问题,STM32H7 提供两种主要手段:
- MPU(内存保护单元)配置:将 DMA 缓冲区所在的内存区域配置为 Non-Cacheable 或 Write-Through 模式,从根源上避免 Cache 与 DMA 的不一致。
-
手动 Clean/Invalidate:在 DMA 传输前后,调用
SCB_CleanDCache_by_Addr()或SCB_InvalidateDCache_by_Addr()维护 Cache 一致性。
实际项目中,两者常结合使用:对频繁 DMA 的大缓冲区用 MPU 设为 Non-Cacheable;对临时小块数据用 Clean/Invalidate 更灵活。
三、MPU 配置实战(以 DMA 接收缓冲区为例)
假设我们在 0x30000000(D1 SRAM)定义了一个 1024 字节的 DMA 接收缓冲区,希望该区域不经过 D-Cache。
3.1 配置步骤
- 使能 MPU:
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;(可选,用于调试) - 禁用 MPU:
MPU->CTRL = 0; - 配置 Region 0:基地址
0x30000000,大小 1024 字节,属性为 Normal Non-Cacheable - 使能 MPU 和 D-Cache
3.2 完整代码
#include "stm32h7xx.h"
#define DMA_BUFFER_ADDR 0x30000000
#define DMA_BUFFER_SIZE 1024
void MPU_Config(void)
{
// 关闭 MPU
MPU->CTRL = 0;
// 配置 Region 0
MPU->RNR = 0; // 选择 Region 0
MPU->RBAR = DMA_BUFFER_ADDR & MPU_RBAR_ADDR_Msk;
MPU->RASR = (0x09 << MPU_RASR_SIZE_Pos) | // 大小 2^(9+1)=1024 字节
(0x00 << MPU_RASR_AP_Pos) | // 特权/用户可读写
(0x01 << MPU_RASR_TEX_Pos) | // TEX=001, C=0, B=0 -> Normal Non-Cacheable
MPU_RASR_ENABLE_Msk;
// 使能 MPU(背景区域默认属性)
MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
// 使能 D-Cache 和 I-Cache
SCB_EnableICache();
SCB_EnableDCache();
}
注意:MPU 配置必须在使能 Cache 之前完成,且 Region 地址和大小必须对齐。
四、Clean/Invalidate 实战
如果不想将整个缓冲区设为 Non-Cacheable,可以在 DMA 传输前后手动维护 Cache。
4.1 关键函数
-
SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将 Cache 中已修改的数据写回内存。 -
SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):丢弃 Cache 中的旧数据,强制从内存重新加载。
4.2 典型场景代码
场景 1:CPU 准备数据,DMA 发送
uint8_t tx_buf[256] __attribute__((aligned(32)));
void DMA_Send(void)
{
// 填充数据
for (int i = 0; i < 256; i++) tx_buf[i] = i;
// 清理 D-Cache,确保数据写回内存
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, 256);
// 启动 DMA 发送
HAL_DMA_Start(&hdma_memtomem, (uint32_t)tx_buf, (uint32_t)dest, 256);
}
场景 2:DMA 接收数据,CPU 读取
uint8_t rx_buf[256] __attribute__((aligned(32)));
void DMA_Receive_Complete_Callback(void)
{
// 无效化 D-Cache,丢弃旧数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, 256);
// 现在 CPU 读取的是 DMA 写入的新数据
process_data(rx_buf, 256);
}
五、注意事项与避坑指南
-
地址对齐:Clean/Invalidate 操作的地址和大小必须是 32 字节对齐,否则可能误伤相邻数据。建议缓冲区使用
__attribute__((aligned(32)))。 - Invalidate 的风险:如果缓冲区中部分数据在 Cache 中被修改但未写回,直接 Invalidate 会丢失这些修改。因此,Invalidate 前应确保没有待写回的数据,或先 Clean 再 Invalidate。
- DMA 传输中的 Cache 操作:不要在 DMA 传输过程中操作 Cache,否则可能引发不可预知错误。
- 多缓冲区管理:如果使用多个 DMA 缓冲区,建议统一放在 Non-Cacheable 区域,简化维护。
- 性能权衡:Non-Cacheable 区域会降低 CPU 访问速度,对性能敏感的数据可考虑 Write-Through 模式或手动维护。
- 调试技巧:若发现数据错乱,可先关闭 D-Cache 验证是否为一致性问题。
六、总结
STM32H7 的 D-Cache 与 DMA 一致性是嵌入式开发中的经典难题。核心原则是:让 CPU 和 DMA 看到相同的内存视图。通过 MPU 将 DMA 缓冲区设为 Non-Cacheable 是最稳妥的方案;若需兼顾性能,则务必在 DMA 传输前后正确调用 Clean/Invalidate,并注意对齐与操作顺序。掌握这些技巧,你就能在 H7 平台上既享受 Cache 带来的性能飞跃,又保证数据传输的绝对可靠。