# 引言 STM32F4 系列(如 STM32F407/429)内置了 D-Cache(数据缓存),用于缓解 CPU 与慢速存储器(如 SDRAM)之间的速度差异。然而,当 CPU 和 DMA 同时访问 SDRAM 时,D-Cache 的写回(write-back)策略会导致缓存中的数据与物理内存不一致,进而引发数据错乱、系统崩溃等严重问题。本文将聚焦这一痛点,提供三种经过验证的工程化解法。 # 问题根源:D-Cache 的写回机制 D-Cache 采用写回策略:CPU 写入数据时,数据先被标记为脏(dirty)并暂存于 Cache 行(通常 32 字节),直到缓存行被替换或显式清理时才写回 SDRAM。若 DMA 直接读取 SDRAM,而 CPU 刚写入的数据仍停留在 Cache 中,DMA 将读到旧数据;反之,若 DMA 写入 SDRAM,而 Cache 中已有该地址的副本,CPU 后续读取会命中缓存,得到过期数据。 # 解法一:软件维护 Cache(清理与失效) ## 原理 通过调用 CMSIS 提供的函数,在关键操作前后手动同步 Cache 与 SDRAM。 - `SCB_CleanDCache()`:将脏缓存行写回 SDRAM。 - `SCB_InvalidateDCache()`:使缓存行失效,强制 CPU 从 SDRAM 重新读取。 ## 配置步骤 1. 启用 D-Cache:在系统初始化时调用 `SCB_EnableDCache()`。 2. 在 CPU 写数据后、启动 DMA 前,执行 Clean 操作。 3. 在 DMA 完成传输后、CPU 读数据前,执行 Invalidate 操作。 ## 代码示例 ```c // 假设 SDRAM 缓冲区地址为 sdram_buf,长度为 len void cpu_write_to_sdram(uint32_t *data, uint32_t len) { memcpy(sdram_buf, data, len); SCB_CleanDCache(); // 确保数据写回 SDRAM // 然后启动 DMA 读取 sdram_buf } void dma_write_to_sdram(uint32_t *data, uint32_t len) { // DMA 写入完成后触发中断 SCB_InvalidateDCache(); // 使缓存失效,避免读到旧数据 memcpy(data, sdram_buf, len); } ``` ## 注意事项 - 此方法简单直接,但频繁调用会降低性能,适合数据量小或低频场景。 - 必须保证操作地址按 32 字节对齐,否则可能影响相邻缓存行。 # 解法二:MPU 配置非缓存区域 ## 原理 利用内存保护单元(MPU)将 SDRAM 区域设置为“非缓存”(Non-cacheable)属性,使 CPU 访问该区域时绕过 D-Cache,直接读写 SDRAM。这样从根本上避免一致性问题,但牺牲了缓存加速效果。 ## 配置步骤 1. 初始化 MPU 并设置区域: - 基地址:SDRAM 起始地址(如 0xC0000000) - 大小:SDRAM 容量(如 8MB) - 属性:Normal memory, Non-cacheable 2. 启用 MPU 和 D-Cache(注意顺序)。 ## 代码示例 ```c void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct; // 禁用 MPU 进行配置 HAL_MPU_Disable(); MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0xC0000000; MPU_InitStruct.Size = MPU_REGION_SIZE_8MB; 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); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); // 启用 D-Cache(需在 MPU 之后) SCB_EnableDCache(); } ``` ## 注意事项 - 非缓存区域访问速度较慢,若 SDRAM 用于帧缓冲或大块数据,可能影响性能。 - 适用于对实时性要求高、数据一致性敏感的场合,如 DMA 与 CPU 频繁交互。 # 解法三:DMA 双缓冲机制 ## 原理 采用双缓冲(Ping-Pong)策略,将 SDRAM 划分为两个缓冲区。CPU 写入一个缓冲区时,DMA 从另一个缓冲区读取;反之亦然。通过交替使用,避免同一地址同时被 CPU 和 DMA 访问,从而无需频繁维护 Cache。 ## 配置步骤 1. 在 SDRAM 中定义两个缓冲区(大小相同)。 2. 使用 DMA 的循环模式或半传输中断,切换缓冲区。 3. 在切换时,仅需对当前 CPU 使用的缓冲区执行 Cache 清理(写)或失效(读)。 ## 代码示例 ```c #define BUF_SIZE 1024 uint32_t sdram_buf[2][BUF_SIZE]; volatile uint8_t active_buf = 0; void DMA_Transfer_Complete(DMA_HandleTypeDef *hdma) { // 切换缓冲区 active_buf ^= 1; // 对即将使用的缓冲区执行 Cache 操作 if (active_buf == 1) { SCB_InvalidateDCache_by_Addr((uint32_t*)&sdram_buf[1], BUF_SIZE*4); } else { SCB_CleanDCache_by_Addr((uint32_t*)&sdram_buf[0], BUF_SIZE*4); } } void CPU_Process(void) { // CPU 处理当前非活动缓冲区(即 DMA 刚完成写入的) uint32_t *data = sdram_buf[active_buf ^ 1]; // 处理数据... } ``` ## 注意事项 - 需要额外的 SDRAM 空间,且 DMA 配置较复杂。 - 适合高吞吐、连续传输的场景,如音频流、图像采集。 - 需确保缓冲区地址按 32 字节对齐,以便使用 `SCB_CleanDCache_by_Addr` 等函数。 # 总结与选型建议 | 方法 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 软件维护 Cache | 简单、无需额外硬件 | 性能开销大,易出错 | 低频小数据量 | | MPU 非缓存 | 彻底解决一致性问题 | 牺牲缓存加速 | 实时性要求高,数据量适中 | | DMA 双缓冲 | 高性能,无阻塞 | 占用内存,复杂度高 | 高吞吐连续传输 | 在实际项目中,建议优先评估 MPU 方案,因其配置简单且稳定;若性能不足,再考虑双缓冲。无论哪种方案,务必在初始化时正确配置 D-Cache 和 MPU,并在关键路径上添加必要的屏障指令(如 `__DSB()`)以确保操作顺序。希望本文能帮助你规避 STM32F4 开发中的经典陷阱,提升系统可靠性。