STM32F4 系列 D-Cache 与 DMA 描述符缓存一致性维护的三种实用策略
👁 1 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列(如 STM32F407)中,启用 D-Cache 可显著提升 CPU 访问外部存储器的性能,但同时也引入了与 DMA 操作之间的缓存一致性问题,尤其是 DMA 描述符(如链表节点)被 CPU 修改后,DMA 可能读取到陈旧数据。本文深入剖析问题根源,并给出三种实用策略:Cache 清理/无效化、使用非缓存内存区域、以及 DMA 描述符对齐与双缓冲设计,每种策略均附有原理讲解、配置步骤和代码示例,帮助开发者根据应用场景选择最优方案,确保数据一致性。
# 引言
在 STM32F4 系列(如 STM32F407)高性能 MCU 中,D-Cache 的引入极大提升了 CPU 访问外部存储器(如 SDRAM)的速度,但同时也带来了与 DMA 协同工作时的缓存一致性问题。当 DMA 描述符(如 DMA2D、以太网 DMA 描述符)由 CPU 在内存中创建或修改后,DMA 外设可能读取到 D-Cache 中的陈旧数据,导致数据传输错误。本文将深入分析问题根源,并给出三种实用策略,帮助开发者确保数据一致性。
# 问题根源
STM32F4 的 D-Cache 是写回(write-back)模式,CPU 写操作先更新 Cache,而不会立即写回主存。当 DMA 外设(如 DMA2D、以太网 MAC)通过 AHB 总线直接访问内存时,它绕过 Cache,直接读取主存。如果描述符数据仍停留在 Cache 中,DMA 将读到旧值。同理,DMA 写入内存的数据也可能被 CPU 从 Cache 中读到旧值。
# 策略一:Cache 清理与无效化(最直接)
## 原理
在 CPU 修改描述符后、启动 DMA 前,调用 `SCB_CleanDCache()` 将 Cache 中对应地址的数据写回主存。在 DMA 完成传输后、CPU 读取描述符前,调用 `SCB_InvalidateDCache()` 使 Cache 中对应行失效,强制从主存重新加载。
## 配置步骤
1. 确保在 `system_stm32f4xx.c` 中启用了 D-Cache(`SCB_EnableDCache()`)。
2. 在修改描述符后,调用清理函数。
3. 在 DMA 传输完成后,调用无效化函数。
## 代码示例
```c
// 假设描述符结构体
__attribute__((aligned(32))) DMA_Descriptor_t desc;
// CPU 修改描述符
desc.ctrl = 0x1234;
desc.addr = (uint32_t)buffer;
// 清理 D-Cache,确保写回主存
SCB_CleanDCache_by_Addr((uint32_t*)&desc, sizeof(desc));
// 启动 DMA(以 DMA2D 为例)
DMA2D->FGMAR = (uint32_t)&desc;
DMA2D->CR |= DMA2D_CR_START;
// 等待传输完成
while (!(DMA2D->ISR & DMA2D_ISR_TCIF)) ;
// 无效化 D-Cache,确保读取最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)&desc, sizeof(desc));
// 现在可以安全读取 desc 中的状态
```
## 注意事项
- 清理/无效化操作有性能开销,频繁调用会影响效率。
- 地址必须 32 字节对齐(Cache line 大小),否则可能无效。
- 建议使用 `by_Addr` 变体,避免全 Cache 操作。
# 策略二:使用非缓存内存区域(推荐)
## 原理
将 DMA 描述符放置在 MPU 配置为非缓存(Non-cacheable)的内存区域中。这样 CPU 访问该区域时直接读写主存,无需 Cache 维护,从根源上避免一致性问题。
## 配置步骤
1. 在链接脚本中定义一段独立内存区域,如 `DMA_DESC`。
2. 使用 MPU 将该区域配置为 `Device` 或 `Strongly-ordered` 属性(非缓存)。
3. 将描述符变量放置在该区域。
## 代码示例
```c
// 链接脚本(.ld)中添加:
// .dma_desc (NOLOAD) : { *(.dma_desc) } > RAM_DMA
// 定义描述符变量,并指定段属性
__attribute__((section(".dma_desc"), aligned(32))) DMA_Descriptor_t desc;
// MPU 配置(初始化时调用)
void MPU_Config(void) {
MPU_Region_InitTypeDef MPU_InitStruct;
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x20000000; // 假设 RAM_DMA 起始地址
MPU_InitStruct.Size = MPU_REGION_SIZE_4KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL_0; // 非缓存
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_Init(&MPU_InitStruct);
MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
// 使用描述符时无需任何 Cache 操作
```
## 注意事项
- 需要合理规划内存布局,确保区域大小足够。
- 非缓存区域访问速度较慢,但描述符访问频率低,影响可忽略。
- 确保 MPU 配置在启用 D-Cache 之前完成。
# 策略三:描述符对齐与双缓冲设计(高级)
## 原理
利用 Cache line 对齐特性,将描述符设计为 32 字节对齐,并采用双缓冲机制。CPU 在修改描述符 A 时,DMA 正在使用描述符 B,通过交替使用避免同时访问同一 Cache line。同时,在切换时进行必要的 Cache 操作,但频率降低。
## 配置步骤
1. 定义两个描述符,均 32 字节对齐。
2. 使用一个标志位指示当前活跃描述符。
3. 在切换前清理旧描述符的 Cache,切换后无效化新描述符的 Cache。
## 代码示例
```c
#define DESC_NUM 2
__attribute__((aligned(32))) DMA_Descriptor_t descs[DESC_NUM];
volatile uint8_t active_desc = 0;
void DMA_Start_Transfer(void) {
uint8_t next = active_desc ^ 1;
// 修改下一个描述符
descs[next].ctrl = ...;
descs[next].addr = ...;
// 清理下一个描述符的 Cache(因为 DMA 将读取)
SCB_CleanDCache_by_Addr((uint32_t*)&descs[next], sizeof(DMA_Descriptor_t));
// 启动 DMA 使用 next 描述符
DMA2D->FGMAR = (uint32_t)&descs[next];
DMA2D->CR |= DMA2D_CR_START;
// 等待完成,然后无效化当前描述符(因为 CPU 可能读取状态)
while (!(DMA2D->ISR & DMA2D_ISR_TCIF)) ;
SCB_InvalidateDCache_by_Addr((uint32_t*)&descs[active_desc], sizeof(DMA_Descriptor_t));
// 切换活跃描述符
active_desc = next;
}
```
## 注意事项
- 描述符必须独立位于不同 Cache line,避免伪共享。
- 切换逻辑需保证 DMA 不会同时访问两个描述符。
- 适用于高速传输场景,可减少 Cache 操作次数。
# 总结
三种策略各有优劣:策略一简单但开销大;策略二推荐用于新设计,彻底避免问题;策略三适合性能敏感场景。实际开发中,应根据系统对实时性和复杂度的要求选择。无论哪种方案,理解 D-Cache 工作原理是前提。建议在项目初期就规划好内存布局,避免后期调试的困扰。