# 引言 在 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 驱动文档