为什么 H7 的 Cache 会成为 DMA 的“猪队友”?
STM32H7 的 Cortex-M7 内核包含 L1 数据 Cache(D-Cache) 和 指令 Cache(I-Cache),通常 D-Cache 大小为 16KB 或 32KB。Cache 的存在让 CPU 访问频繁使用的数据时无需每次都读 AXI SRAM,从而大幅提升性能。
但 DMA 是“独立于 CPU 的总线主设备”,它不经过 Cache,直接读写物理内存。这就导致两个经典问题:
- DMA 写内存,CPU 读旧值:DMA 把新数据写入 SRAM,但 CPU 之前已把该地址的旧数据缓存到 D-Cache,CPU 读到的仍是旧值。
- CPU 写内存,DMA 读旧值:CPU 写数据到 D-Cache(写回策略下未立即写入 SRAM),DMA 启动后从 SRAM 读到的是旧数据。
解决思路只有两条:要么让 DMA 缓冲区不经过 Cache(MPU 配置为 Write-Through/Non-Cacheable),要么在 DMA 传输前后手动维护 Cache 一致性(Clean/Invalidate)。
方法一:MPU 配置 DMA 缓冲区为 Non-Cacheable
最省心的做法是把 DMA 使用的内存区域通过 MPU 配置为 Non-Cacheable 或 Write-Through,这样 CPU 和 DMA 看到的内存始终一致。
配置步骤
- 在链接脚本中划分一块专用 RAM 区域(如
0x30000000开始的 32KB),或直接使用 AXI SRAM 的某段地址。 - 在
main()初始化阶段调用 MPU 配置函数。 - 使能 MPU 和 D-Cache。
完整代码示例
#include "stm32h7xx.h"
/* 假设 DMA 缓冲区放在 0x30000000,大小 32KB */
#define DMA_BUFFER_ADDR 0x30000000UL
#define DMA_BUFFER_SIZE (32UL * 1024UL)
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {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_DISABLE;
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);
/* 使能 MPU */
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config(); /* 先配 MPU */
SCB_EnableICache(); /* 再开 I-Cache */
SCB_EnableDCache(); /* 最后开 D-Cache */
/* 此后 DMA_BUFFER_ADDR 区域读写均不经过 Cache */
while (1) { }
}
注意:MPU 配置必须在使能 D-Cache 之前完成,否则可能因 Cache 已缓存旧数据而出错。
方法二:手动 Clean / Invalidate 维护一致性
如果 DMA 缓冲区必须放在可 Cache 区域(如普通 AXI SRAM),则需要在 DMA 传输前后手动操作 Cache。
核心 API
-
SCB_CleanDCache_by_Addr(addr, size):将 D-Cache 中已修改的数据写回 SRAM。用于 CPU 写、DMA 读之前。 -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 D-Cache 中对应地址的缓存行,强制下次从 SRAM 读取。用于 DMA 写、CPU 读之前。 -
SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再无效化,用于双向传输。
典型场景代码
#define BUF_SIZE 1024
__attribute__((aligned(32))) uint8_t dma_buf[BUF_SIZE];
/* 场景1:CPU 填充数据,DMA 发送 */
void DMA_Send(void)
{
for (int i = 0; i < BUF_SIZE; i++) {
dma_buf[i] = i & 0xFF;
}
/* 写回 D-Cache,确保 SRAM 中数据最新 */
SCB_CleanDCache_by_Addr((uint32_t *)dma_buf, BUF_SIZE);
HAL_DMA_Start(&hdma, (uint32_t)dma_buf, (uint32_t)&UART->TDR, BUF_SIZE);
}
/* 场景2:DMA 接收数据,CPU 读取 */
void DMA_Receive(void)
{
HAL_DMA_Start(&hdma, (uint32_t)&UART->RDR, (uint32_t)dma_buf, BUF_SIZE);
/* 等待 DMA 完成(中断或轮询) */
while (HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, 1000) != HAL_OK);
/* 无效化 D-Cache,丢弃旧缓存,强制从 SRAM 读新数据 */
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buf, BUF_SIZE);
for (int i = 0; i < BUF_SIZE; i++) {
process(dma_buf[i]);
}
}
避坑要点与注意事项
-
地址与大小必须 32 字节对齐:Cortex-M7 的 Cache 行大小为 32 字节,
SCB_*DCache_by_Addr的地址和长度都必须是 32 的倍数,否则会误伤相邻数据。建议缓冲区用__attribute__((aligned(32)))修饰。 -
Invalidate 前先 Clean:如果缓冲区中既有 CPU 写入的数据,又有 DMA 要写入的区域,直接 Invalidate 会丢失 CPU 未写回的数据。应使用
SCB_CleanInvalidateDCache_by_Addr。 - 不要对栈上变量做 DMA:栈变量地址不固定且可能与其他数据共享 Cache 行,极易出错。DMA 缓冲区应使用全局数组或静态分配。
- 中断中避免 Cache 操作:Cache 维护操作耗时且可能被中断打断,建议在 DMA 完成中断中仅置标志,在主循环中执行 Invalidate。
- MPU 配置与链接脚本一致:如果使用自定义段,确保链接脚本中的地址与 MPU 配置的基地址一致,否则 MPU 不生效。
- 调试时关闭 Cache:在排查诡异数据错误时,可先关闭 D-Cache 验证是否为一致性问题,再逐步开启。
总结
STM32H7 的 Cache 与 DMA 一致性问题是嵌入式开发中的经典“深坑”。优先推荐 MPU 将 DMA 缓冲区配置为 Non-Cacheable,简单可靠;若必须使用 Cache 区域,则严格遵循“CPU 写后 Clean、DMA 写后 Invalidate”的原则,并注意 32 字节对齐和 Clean/Invalidate 顺序。掌握这两招,你就能在 H7 上放心地同时享受高主频与 DMA 高效传输。