STM32F4 系列使用 D-Cache 时 DMA 描述符缓存一致性维护的三种实用策略
👁 1 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列(如 STM32F407/429)中,当启用 D-Cache 并与 DMA 协同工作时,缓存一致性问题常导致数据错乱,尤其在 DMA 描述符(如以太网 DMA、SDIO 描述符)场景下。本文深入剖析问题根源,并给出三种实用策略:直接禁用 D-Cache、使用 Cache 清理/无效化操作、以及利用 MPU 配置非缓存区域。每种策略均附原理讲解、配置步骤和完整代码示例,帮助开发者根据实时性与性能需求做出最优选择。
# STM32F4 使用 D-Cache 时 DMA 描述符缓存一致性维护的三种实用策略
## 问题背景:D-Cache 与 DMA 的冲突
STM32F4 系列(Cortex-M4F)内置了 D-Cache(数据缓存),用于加速 CPU 对内存的访问。然而,DMA 控制器直接访问物理内存,不经过 Cache。当 CPU 修改 DMA 描述符(如描述符中的缓冲区地址、长度、状态标志)后,这些数据可能仍停留在 Cache 中,尚未写回物理内存;反之,DMA 更新描述符后,CPU 读取时可能命中过期的 Cache 行。这种不一致性会导致 DMA 传输错误、数据丢失甚至系统崩溃。
典型场景:使用 STM32F4 的以太网 MAC(DMA 描述符)或 SDIO(SD 卡)时,若启用了 D-Cache,必须处理缓存一致性。
## 策略一:直接禁用 D-Cache(简单粗暴)
### 原理
禁用 D-Cache 后,CPU 所有访问都直接操作物理内存,DMA 与 CPU 天然一致。代价是 CPU 访问内存速度下降(尤其对频繁访问的数据),但系统功能正确。适合对性能要求不高或调试阶段。
### 配置步骤
1. 在系统初始化时,不使能 D-Cache(默认关闭)。
2. 若已使能,则调用 `SCB_DisableDCache()` 关闭。
### 代码示例
```c
#include "stm32f4xx.h"
void SystemInit_CacheConfig(void) {
// 禁用 D-Cache(默认关闭,此处显式调用)
SCB_DisableDCache();
// 注意:禁用后无需清理,因为未使用
}
// 以太网 DMA 描述符定义(无 Cache 时直接使用)
ETH_DMADescTypeDef DMATxDescTab[ETH_TXBUFNB] __attribute__((aligned(4)));
int main(void) {
SystemInit_CacheConfig();
// 初始化以太网...
HAL_ETH_Init(&heth);
// 正常操作,无需额外维护
}
```
### 注意事项
- 禁用 D-Cache 会降低整体性能,尤其在大量内存拷贝或计算场景。
- 若系统其他部分依赖 D-Cache(如图形加速),需谨慎评估。
## 策略二:使用 Cache 清理/无效化操作(精准控制)
### 原理
在 CPU 修改描述符后,调用 `SCB_CleanDCache()` 将 Cache 内容写回内存;在 DMA 更新描述符后,调用 `SCB_InvalidateDCache()` 使 Cache 行失效,强制从内存重新读取。此策略保持 D-Cache 开启,性能较好,但需在关键点手动操作。
### 配置步骤
1. 使能 D-Cache:`SCB_EnableDCache()`。
2. 在每次 CPU 写描述符后,清理相关地址的 Cache。
3. 在每次 DMA 完成中断后,无效化相关地址的 Cache。
### 代码示例
```c
#include "stm32f4xx.h"
// 描述符缓冲区(需对齐到 Cache 行大小,通常 32 字节)
#define CACHE_LINE_SIZE 32
ETH_DMADescTypeDef DMATxDescTab[ETH_TXBUFNB] __attribute__((aligned(CACHE_LINE_SIZE)));
// CPU 准备描述符(例如设置缓冲区地址和长度)
void PrepareTxDescriptor(ETH_DMADescTypeDef *desc, uint8_t *buf, uint16_t len) {
desc->Buffer1Addr = (uint32_t)buf;
desc->ControlBufferSize = len;
desc->Status = ETH_DMATXDESC_VALID;
// 清理该描述符对应的 Cache 行,确保写回内存
SCB_CleanDCache_by_Addr((uint32_t*)desc, sizeof(*desc));
}
// DMA 发送完成中断回调
void HAL_ETH_TxCpltCallback(ETH_HandleTypeDef *heth) {
// 无效化描述符,读取 DMA 更新的状态
SCB_InvalidateDCache_by_Addr((uint32_t*)DMATxDescTab, sizeof(DMATxDescTab));
// 检查状态...
}
int main(void) {
SCB_EnableDCache();
// 初始化以太网...
HAL_ETH_Init(&heth);
// 发送数据
PrepareTxDescriptor(&DMATxDescTab[0], tx_buffer, 100);
HAL_ETH_Transmit_IT(&heth, &DMATxDescTab[0]);
while(1) { /* 主循环 */ }
}
```
### 注意事项
- 必须确保描述符地址按 Cache 行对齐(STM32F4 的 D-Cache 行大小为 32 字节),否则清理/无效化可能影响相邻数据。
- 清理和无效化操作有开销,频繁调用会降低性能,建议批量处理(如整个描述符表)。
- 在 DMA 传输过程中,CPU 不应访问描述符,否则需额外同步。
## 策略三:使用 MPU 配置非缓存区域(硬件隔离)
### 原理
通过 MPU(内存保护单元)将 DMA 描述符所在的内存区域配置为“非缓存”属性(即 `Device` 或 `Strongly-ordered`),这样 CPU 访问该区域时直接绕过 D-Cache,与 DMA 保持一致。其他内存区域仍可启用 Cache,兼顾性能与正确性。
### 配置步骤
1. 使能 MPU 并配置区域:设置基地址、大小、属性(`MPU_REGION_NO_CACHE`)。
2. 将描述符数组放置在该区域(通过链接脚本或属性指定)。
3. 初始化 MPU 后,正常使用描述符,无需手动清理。
### 代码示例
```c
#include "stm32f4xx.h"
// 描述符区域基地址(例如 SRAM3 的某段)
#define DESC_REGION_BASE 0x20004000 // 示例地址,需根据实际内存布局
#define DESC_REGION_SIZE (4 * 1024) // 4KB
// 将描述符数组放置到该区域(使用 __attribute__)
ETH_DMADescTypeDef DMATxDescTab[ETH_TXBUFNB] __attribute__((section(".dma_desc")));
void MPU_Config(void) {
MPU_Region_InitTypeDef MPU_InitStruct;
// 禁用 MPU 进行配置
HAL_MPU_Disable();
// 配置区域 0:描述符区域,非缓存
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = DESC_REGION_BASE;
MPU_InitStruct.Size = MPU_REGION_SIZE_4KB;
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_NOT_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);
// 使能 MPU
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
// 链接脚本中需将 .dma_desc 段放置到 DESC_REGION_BASE 地址
int main(void) {
HAL_Init();
MPU_Config();
SCB_EnableDCache(); // 其他区域仍可使用 Cache
// 初始化以太网...
// 直接操作描述符,无需清理/无效化
}
```
### 注意事项
- MPU 配置需在系统早期完成,且注意区域大小和地址对齐(通常 32 字节倍数)。
- 非缓存区域访问速度较慢,但仅影响描述符,对整体性能影响小。
- 需修改链接脚本(如 `.ld` 文件)将描述符段定位到指定地址,确保与 MPU 区域匹配。
## 总结与选型建议
| 策略 | 优点 | 缺点 | 适用场景 |
|------|------|------|----------|
| 禁用 D-Cache | 简单,无额外代码 | 性能下降明显 | 调试阶段或性能要求低 |
| 手动清理/无效化 | 性能较好,灵活 | 需精确控制,易出错 | 描述符数量少,实时性要求高 |
| MPU 非缓存区域 | 硬件保证,性能影响小 | 配置复杂,需链接脚本配合 | 生产环境,描述符频繁使用 |
对于大多数 STM32F4 项目,推荐使用策略三(MPU 配置),它平衡了性能与正确性。若项目简单或处于开发初期,策略一可快速验证功能;策略二适合对 Cache 操作熟悉且描述符访问点明确的场景。无论选择哪种,务必在初始化阶段确保一致性,并在文档中记录维护点,以便后续维护。