STM32F4 D-Cache 与 DMA 描述符缓存一致性:三种实用维护策略
👁 2 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列高性能 MCU 上,启用 D-Cache 可显著提升 CPU 访问外部存储器的效率,但同时也引入了 DMA 与缓存之间的数据一致性问题,尤其是 DMA 描述符(如以太网 DMA 描述符)被 CPU 和 DMA 同时访问时,极易出现数据错乱。本文深入剖析缓存一致性的根源,并给出三种实用维护策略:全缓存刷新、描述符区域配置为非缓存(MPU)以及手动 Clean/Invalidate 操作。每种策略均附有原理讲解、配置步骤和完整代码示例,帮助开发者根据应用场景选择最佳方案。
# 引言
在 STM32F4 系列(如 STM32F407、STM32F429)中,D-Cache 的引入大幅提升了 CPU 访问外部 SDRAM 或 Flash 的速度,但同时也带来了新的挑战:当 DMA 控制器直接访问内存时,如果 CPU 已经将数据缓存(Dirty Cache),DMA 读到的可能是过时数据;反之,DMA 写入内存后,CPU 可能从缓存中读到旧值。这种缓存一致性问题在 DMA 描述符(如以太网 DMA 描述符、SDIO 描述符)上尤为突出,因为描述符由 CPU 和 DMA 频繁交替读写。
本文将介绍三种实用的维护策略,帮助你在 STM32F4 项目中正确处理 D-Cache 与 DMA 描述符的一致性。
# 问题根源
STM32F4 的 D-Cache 是写回(Write-back)模式,即 CPU 写操作先更新缓存,仅当缓存行被替换或显式 Clean 时才写回内存。DMA 访问内存时绕过缓存,直接读写物理地址。因此,当 CPU 修改描述符后,若未及时写回,DMA 会读取到旧值;当 DMA 更新描述符后,若 CPU 缓存中仍保留旧行,CPU 会忽略 DMA 的修改。
# 策略一:全缓存刷新(简单粗暴)
## 原理
在每次 DMA 操作前后,调用 SCB_CleanDCache() 和 SCB_InvalidateDCache() 将整个 D-Cache 写回或失效。此方法实现简单,但性能开销大,仅适用于描述符操作频率低或对性能不敏感的场景。
## 配置步骤
1. 在系统初始化时启用 D-Cache:
```c
SCB_EnableDCache();
```
2. 在 DMA 描述符修改前,调用 CleanDCache;在 DMA 操作完成后,调用 InvalidateDCache。
## 代码示例
```c
// 以太网发送描述符更新
void ETH_DMA_TxDesc_Write(ETH_DMADescTypeDef *desc, uint32_t data) {
// 修改描述符字段
desc->DESC0 = data;
// 写回整个缓存,确保描述符写入内存
SCB_CleanDCache();
// 启动 DMA 传输...
}
// 以太网接收描述符处理
void ETH_DMA_RxDesc_Read(ETH_DMADescTypeDef *desc) {
// 使缓存失效,确保读取到 DMA 写入的最新数据
SCB_InvalidateDCache();
uint32_t status = desc->DESC0;
// 处理数据...
}
```
## 注意事项
- 全缓存刷新会清空所有缓存行,导致后续 CPU 访问性能下降。
- 在多任务或中断环境中,频繁刷新可能引入不确定性。
# 策略二:描述符区域配置为非缓存(MPU)
## 原理
利用 MPU(Memory Protection Unit)将 DMA 描述符所在的 RAM 区域设置为非缓存(Non-cacheable),这样 CPU 和 DMA 都直接访问物理内存,彻底避免一致性问题。此方法性能最佳,但需要合理划分内存区域。
## 配置步骤
1. 在链接脚本中为描述符分配独立内存段(例如 .dma_desc)。
2. 初始化 MPU,配置该区域为 Normal memory, Non-cacheable。
3. 确保描述符地址对齐到 32 字节(MPU 区域大小要求)。
## 代码示例
```c
// 定义描述符数组,放在独立段
__attribute__((section(".dma_desc"))) ETH_DMADescTypeDef tx_desc[ETH_TX_DESC_CNT];
// MPU 配置函数
void MPU_Config(void) {
MPU_Region_InitTypeDef MPU_InitStruct;
HAL_MPU_Disable();
// 配置描述符区域(假设地址 0x20000000,大小 1KB)
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x20000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_1KB;
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;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_CONTROL_HRNDM);
}
// 主函数中调用
int main(void) {
HAL_Init();
MPU_Config();
SCB_EnableDCache();
// 之后对描述符的读写无需任何缓存维护操作
}
```
## 注意事项
- MPU 区域大小必须是 2 的幂次方,且起始地址对齐。
- 非缓存区域访问速度稍慢,但描述符访问频率低,影响可忽略。
- 需要确保描述符区域不与普通数据混用,否则可能导致性能下降。
# 策略三:手动 Clean/Invalidate 描述符行
## 原理
针对单个描述符或描述符所在的缓存行(通常 32 字节)执行 Clean 或 Invalidate 操作,只维护必要的数据,避免全缓存刷新。此方法在性能和复杂度之间取得平衡,适用于描述符较多但操作不频繁的场景。
## 配置步骤
1. 启用 D-Cache。
2. 在修改描述符前,调用 SCB_CleanDCache_by_Addr() 将描述符地址写回。
3. 在读取 DMA 更新的描述符前,调用 SCB_InvalidateDCache_by_Addr() 使对应缓存行失效。
## 代码示例
```c
// 描述符地址对齐到 32 字节
#define CACHE_LINE_SIZE 32
// 写描述符前 Clean
void Desc_PrepareWrite(ETH_DMADescTypeDef *desc) {
SCB_CleanDCache_by_Addr((uint32_t*)desc, sizeof(ETH_DMADescTypeDef));
}
// 读描述符前 Invalidate
void Desc_PrepareRead(ETH_DMADescTypeDef *desc) {
SCB_InvalidateDCache_by_Addr((uint32_t*)desc, sizeof(ETH_DMADescTypeDef));
}
// 使用示例
void UpdateTxDesc(ETH_DMADescTypeDef *desc, uint32_t buf_addr) {
desc->DESC2 = buf_addr;
Desc_PrepareWrite(desc); // 确保写入内存
// 启动 DMA...
}
void ProcessRxDesc(ETH_DMADescTypeDef *desc) {
Desc_PrepareRead(desc); // 获取 DMA 写入的最新值
uint32_t len = desc->DESC1;
// 处理数据...
}
```
## 注意事项
- 地址必须 32 字节对齐,否则操作可能影响相邻数据。
- 若描述符大小超过一个缓存行,需确保整个描述符都被覆盖。
- 在多核或中断嵌套场景,需考虑操作顺序,避免竞态。
# 总结与选型建议
- **策略一(全缓存刷新)**:代码简单,适合原型验证或低频操作,但性能损失大。
- **策略二(MPU 非缓存)**:性能最佳,适合对实时性要求高的应用,但需要精心规划内存布局。
- **策略三(手动行维护)**:灵活高效,适合大多数生产项目,但需注意对齐和操作顺序。
在实际项目中,建议优先考虑策略二,将描述符区域隔离为非缓存,同时保留其他数据区域的缓存加速。若无法使用 MPU,则采用策略三,并确保所有描述符操作都遵循 Clean/Invalidate 规范。
# 参考资料
- STM32F4xx Reference Manual (RM0090)
- ARM Cortex-M4 Programming Guide
- STM32CubeF4 HAL 驱动文档