# 引言 在 STM32F4 系列高性能 MCU 中,D-Cache(数据缓存)能显著提升 CPU 访问内存的效率,但同时也引入了缓存一致性问题。当 DMA 外设直接访问内存时,若未正确配置 MPU(内存保护单元)或未执行缓存维护操作,CPU 与 DMA 看到的数据可能不一致,导致数据损坏或功能异常。本文以实际项目为例,讲解问题原理、排查步骤及解决方案。 # 问题现象与原理 ## 典型症状 - 以太网接收数据包偶尔出现 CRC 错误或内容错乱。 - USB 批量传输数据丢失或重复。 - ADC 采集的数据在 DMA 搬运后,CPU 读取的值与预期不符。 ## 根本原因 STM32F4 的 D-Cache 是写回(write-back)模式,CPU 写入数据时先更新缓存,仅当缓存行被替换或显式清理时才写回内存。DMA 外设直接访问物理内存,不经过缓存。若 DMA 读取的内存区域仍驻留在缓存中且未写回,则 DMA 读到的是旧数据;若 DMA 写入内存,而 CPU 缓存中已有该区域的副本,则 CPU 可能读到过期的缓存数据。 # 排查方法 ## 1. 确认是否启用 D-Cache 检查启动代码或初始化中是否调用了 `SCB_EnableDCache()`。若未启用,则问题可能源于其他原因。 ## 2. 定位 DMA 缓冲区地址 查看 DMA 描述符或配置中的缓冲区地址,确认其是否位于可缓存区域(如 SRAM1/SRAM2)。 ## 3. 检查 MPU 配置 若未配置 MPU,默认情况下整个 SRAM 区域是可缓存的。此时,任何 DMA 与 CPU 共享的数据都可能引发一致性问题。 ## 4. 临时禁用 D-Cache 验证 在初始化时注释掉 `SCB_EnableDCache()`,观察问题是否消失。若消失,则基本确定是缓存一致性问题。 # 解决方案:MPU 配置 + 缓存维护 ## 方案一:配置 MPU 将 DMA 缓冲区设为非缓存 将 DMA 使用的内存区域配置为“非缓存、非缓冲”属性,这样 CPU 访问该区域时直接操作内存,避免缓存干扰。 ### 配置步骤 1. 确定 DMA 缓冲区地址和大小(需对齐到 32 字节)。 2. 使用 `MPU_Region_InitTypeDef` 配置 MPU 区域。 3. 使能 MPU 并设置优先级。 ### 代码示例 ```c void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct; // 禁用 MPU 进行配置 HAL_MPU_Disable(); // 配置 DMA 缓冲区区域(假设地址 0x20001000,大小 4KB) MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x20001000; MPU_InitStruct.Size = MPU_REGION_SIZE_4KB; MPU_InitStruct.SubRegionDisable = 0; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; // 不使用 TEX MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 关键:非缓存 MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); // 使能 MPU HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); } ``` 调用 `MPU_Config()` 在系统初始化时执行。 ## 方案二:使用缓存维护函数 若不想配置 MPU,也可在 DMA 操作前后手动执行缓存清理或无效化操作。 - 当 CPU 写入数据并准备 DMA 读取时,调用 `SCB_CleanDCache()` 或 `SCB_CleanDCache_by_Addr()` 将缓存写回内存。 - 当 DMA 写入数据并准备 CPU 读取时,调用 `SCB_InvalidateDCache()` 或 `SCB_InvalidateDCache_by_Addr()` 使缓存行失效。 ### 代码示例 ```c // 假设缓冲区地址为 buf,长度为 len // CPU 写入数据后,清理缓存 SCB_CleanDCache_by_Addr((uint32_t*)buf, (int32_t)len); // 启动 DMA 读取... // DMA 传输完成后,无效化缓存 SCB_InvalidateDCache_by_Addr((uint32_t*)buf, (int32_t)len); ``` 注意:地址需 32 字节对齐,长度需为 32 的倍数,否则需手动调整。 # 完整示例:以太网 DMA 接收 以下展示在 STM32F429 上使用 DMA 接收以太网数据帧时的正确配置。 ```c // 定义 DMA 缓冲区(建议使用 __attribute__((aligned(32)))) __attribute__((aligned(32))) uint8_t rx_buffer[1522]; void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth) { // 无效化缓存,确保 CPU 读取到 DMA 写入的最新数据 SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer)); // 处理数据... } // 在初始化中配置 MPU(可选,但推荐) void MPU_Config(void) { // 如上所示,将 rx_buffer 所在区域设为非缓存 } int main(void) { HAL_Init(); SystemClock_Config(); MPU_Config(); // 先配置 MPU SCB_EnableDCache(); // 再使能 D-Cache // 初始化以太网等... } ``` # 注意事项 - **对齐与大小**:MPU 区域大小必须是 2 的幂次,且基地址需对齐到区域大小。若缓冲区不满足,可定义更大的区域或使用缓存维护函数。 - **性能权衡**:将 DMA 缓冲区设为非缓存会降低 CPU 访问该区域的性能,但通常影响不大。 - **多区域配置**:若多个 DMA 缓冲区分散在不同地址,可配置多个 MPU 区域,但注意 MPU 区域数量有限(STM32F4 通常 8 个)。 - **缓存维护开销**:频繁调用缓存维护函数可能影响实时性,建议优先使用 MPU 配置。 - **调试技巧**:使用调试器查看内存内容,对比 DMA 写入前后的数据,可快速确认一致性错误。 # 总结 D-Cache 与 DMA 的交互是 STM32F4 开发中的常见陷阱。通过合理配置 MPU 将 DMA 缓冲区设为非缓存,或在使用时执行缓存清理/无效化操作,可有效避免数据不一致问题。建议在项目初期就规划好内存区域属性,以减少后期调试成本。