# 引言 STM32F4 系列内置 Cortex-M4 内核,支持 D-Cache(数据缓存)和 I-Cache。开启 D-Cache 后,CPU 访问内存数据会先经过缓存,而 DMA 外设直接访问物理内存(SRAM),两者之间没有硬件自动同步机制。当 DMA 描述符(如 DMA2D、以太网 DMA 等)位于可缓存区域时,CPU 更新描述符后可能仍停留在 Cache 中,而 DMA 从物理内存读取旧数据;反之,DMA 写入描述符后,CPU 可能读到 Cache 中的陈旧数据。这种缓存一致性问题轻则导致数据传输错误,重则引发系统死机。 # 问题现象与定位 某项目中,使用 STM32F429 的 DMA2D 进行图像搬运,开启 D-Cache 后,图像出现随机花屏,且 DMA 传输完成中断偶尔丢失。通过调试器查看 DMA2D 描述符(位于 SRAM1),发现其内容与预期不符,部分字段被错误修改。初步怀疑是 DMA 与 CPU 访问冲突,但关闭 D-Cache 后问题消失,确认是缓存一致性问题。 # 原理剖析 Cortex-M4 的 D-Cache 采用 VIPT(Virtually Indexed Physically Tagged)策略,以 32 字节为一行(Line)。当 CPU 写内存时,若命中 Cache,则只更新 Cache 行(写回策略),物理内存暂不更新;当 DMA 读内存时,直接访问物理内存,因此读不到最新数据。反之,DMA 写内存后,物理内存已更新,但 CPU 若之前缓存了该地址,则后续读操作会命中 Cache,读到旧值。 DMA 描述符通常由 CPU 初始化并更新,DMA 控制器读取描述符来获取传输参数。若描述符所在内存区域被标记为 Cacheable,则上述问题必然发生。 # 解决方案 ## 方案一:将描述符放在非缓存区域 STM32F4 的 SRAM 分为多个区域,可通过 MPU(Memory Protection Unit)配置某段内存为 Non-cacheable。例如,将 SRAM1 的起始 4KB 配置为 Non-cacheable,专门存放 DMA 描述符。 **配置步骤:** 1. 使能 MPU 时钟(RCC->AHB1ENR |= RCC_AHB1ENR_MPUEN)。 2. 设置 MPU 区域寄存器(MPU_RNR、MPU_RBAR、MPU_RASR)。 3. 使能 MPU(MPU_CTRL |= MPU_CTRL_ENABLE)。 **示例代码:** ```c void MPU_Config_NonCacheable(void) { // 禁用 MPU MPU->CTRL = 0; // 配置区域 0:SRAM1 起始 4KB,Non-cacheable MPU->RNR = 0; MPU->RBAR = (uint32_t)0x20010000; // SRAM1 基地址 MPU->RASR = (0x02 << MPU_RASR_AP_Pos) | // 全权限 (0x00 << MPU_RASR_TEX_Pos) | // TEX=0 (0x01 << MPU_RASR_C_Pos) | // C=1, B=0 -> Non-cacheable (0x00 << MPU_RASR_B_Pos) | (0x01 << MPU_RASR_S_Pos) | // Shareable (0x0B << MPU_RASR_SIZE_Pos) | // 4KB (2^12) (0x01 << MPU_RASR_ENABLE_Pos); // 使能 MPU MPU->CTRL = MPU_CTRL_ENABLE; } ``` 注意:MPU 配置需在启动早期完成,且需确保描述符地址对齐到 32 字节。 ## 方案二:使用 Cache 维护操作 若不想配置 MPU,可在每次 CPU 更新描述符后,执行 Clean(清 Cache)操作,确保数据写回物理内存;在 DMA 完成中断中,执行 Invalidate(无效化)操作,使 CPU 重新从内存读取。 **关键函数(CMSIS 提供):** - `SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize)` - `SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize)` **示例代码:** ```c // 更新描述符 DMA2D_Desc->CTRL = ...; DMA2D_Desc->FG_COLOR = ...; // 确保描述符写回内存 SCB_CleanDCache_by_Addr((uint32_t*)DMA2D_Desc, sizeof(DMA2D_Desc_TypeDef)); // 启动 DMA DMA2D->CR |= DMA2D_CR_START; // 在 DMA 完成中断中 SCB_InvalidateDCache_by_Addr((uint32_t*)DMA2D_Desc, sizeof(DMA2D_Desc_TypeDef)); // 然后读取描述符状态 ``` 注意:Clean 和 Invalidate 的地址需 32 字节对齐,大小需为 32 的倍数。若描述符结构体未对齐,可定义联合体或使用 `__attribute__((aligned(32)))`。 ## 方案三:使用非缓存属性(推荐) 若使用 STM32CubeMX 生成代码,可在链接脚本中定义非缓存段,并将描述符放置在该段。例如,在 GCC 中: ```c __attribute__((section(".noncacheable"))) DMA2D_Desc_TypeDef dma2d_desc; ``` 并在链接脚本中,将该段映射到 MPU 配置的非缓存区域。 # 完整代码示例(基于 HAL 库) 以下示例演示使用 MPU 配置非缓存区域,并正确管理 DMA2D 描述符。 ```c // 描述符结构体(对齐到 32 字节) typedef struct __attribute__((aligned(32))) { uint32_t CTRL; uint32_t FG_COLOR; uint32_t BG_COLOR; uint32_t P_OFFSET; uint32_t P_ADDR; uint32_t P_STRIDE; uint32_t N_P_OFFSET; uint32_t N_ADDR; uint32_t N_STRIDE; uint32_t O_P_OFFSET; uint32_t O_ADDR; uint32_t O_STRIDE; } DMA2D_Desc_TypeDef; // 全局描述符(位于非缓存区域,需在链接脚本中指定) DMA2D_Desc_TypeDef dma2d_desc __attribute__((section(".noncacheable"))); void MPU_Config(void) { // 配置 MPU 区域 0:SRAM1 起始 4KB 为非缓存 MPU->CTRL = 0; MPU->RNR = 0; MPU->RBAR = (uint32_t)&dma2d_desc & ~0x1F; // 对齐到 32 字节 MPU->RASR = (0x02 << MPU_RASR_AP_Pos) | (0x00 << MPU_RASR_TEX_Pos) | (0x01 << MPU_RASR_C_Pos) | (0x00 << MPU_RASR_B_Pos) | (0x01 << MPU_RASR_S_Pos) | (0x0B << MPU_RASR_SIZE_Pos) | // 4KB (0x01 << MPU_RASR_ENABLE_Pos); MPU->CTRL = MPU_CTRL_ENABLE; } void DMA2D_Transfer(void) { // 初始化描述符 dma2d_desc.CTRL = DMA2D_M2M | DMA2D_ARGB8888; dma2d_desc.FG_COLOR = 0xFFFFFFFF; dma2d_desc.P_ADDR = (uint32_t)src_buffer; dma2d_desc.P_STRIDE = width; dma2d_desc.O_ADDR = (uint32_t)dst_buffer; dma2d_desc.O_STRIDE = width; // 无需 Clean,因为描述符在非缓存区域 // 启动 DMA2D HAL_DMA2D_Start(&hdma2d, (uint32_t)&dma2d_desc, 0, 0); } void DMA2D_IRQHandler(void) { if (DMA2D->ISR & DMA2D_FLAG_TC) { DMA2D->IFCR = DMA2D_FLAG_TC; // 无需 Invalidate,因为描述符在非缓存区域 // 处理传输完成 } } ``` # 注意事项 - **地址对齐**:Cache 操作要求地址 32 字节对齐,大小 32 的倍数。若结构体未对齐,可手动调整或使用 `__attribute__((aligned(32)))`。 - **MPU 配置时机**:MPU 必须在系统启动早期配置,且需在使能 D-Cache 之前完成,否则可能无效。 - **多核或中断上下文**:若在中断中修改描述符,需确保操作原子性,并考虑 Cache 维护的延迟。 - **调试技巧**:关闭 D-Cache 对比测试,可快速确认是否缓存问题;使用逻辑分析仪或断点检查物理内存值。 - **性能权衡**:非缓存区域访问速度较慢,但仅用于描述符,影响可忽略。 # 总结 STM32F4 开启 D-Cache 后,DMA 描述符的缓存一致性是常见陷阱。通过 MPU 配置非缓存区域或手动维护 Cache,可彻底解决。推荐使用 MPU 方案,代码简洁且性能影响小。希望本文能帮助开发者快速定位并修复此类问题,避免在项目中踩坑。