STM32F4 使用 D-Cache 时 DMA 描述符缓存一致性维护的三种实用策略
一、问题根源:D-Cache 与 DMA 的“视线盲区”
STM32F4 系列(如 STM32F407、STM32F429)内置了 4KB 或 8KB 的 D-Cache,用于加速 CPU 对 SRAM 的访问。然而,DMA 控制器(如 DMA2D、SDIO、以太网 MAC)在传输数据时直接访问物理内存,不经过 D-Cache。这就导致两个经典问题:
- CPU 写数据到 SRAM 后,数据可能仍留在 Cache 中,尚未写回物理内存。DMA 读取时拿到的是旧数据。
- DMA 将新数据写入 SRAM 后,物理内存已更新,但 Cache 中仍保留旧副本。CPU 读取时拿到的是过期数据。
对于 DMA 描述符(如 DMA2D 的控制结构体、SDIO 的 CID/DATA 寄存器映射),这种不一致会直接导致传输状态判断错误或数据错乱。
二、策略一:全缓存刷新(简单粗暴,适合低频传输)
原理
在每次 DMA 操作前后,调用 SCB_CleanDCache() 或 SCB_CleanInvalidateDCache() 强制将整个 D-Cache 写回物理内存,并失效所有行。此方法不依赖任何硬件配置,代码量最小。
配置步骤
- 在启动文件或
system_stm32f4xx.c中使能 D-Cache:SCB_EnableDCache(); - 在 DMA 传输前,清理 Cache 确保描述符和数据已写回:
SCB_CleanDCache(); - 在 DMA 传输完成后,失效 Cache 使 CPU 重新从物理内存读取:
SCB_InvalidateDCache();
完整代码示例(以 DMA2D 为例)
// 假设 DMA2D 描述符位于 SRAM1,地址 0x20000000
DMA2D_HandleTypeDef hdma2d;
void DMA2D_TransferWithFullCacheFlush(void) {
// 1. 清理整个 D-Cache,确保描述符和源数据写回 SRAM
SCB_CleanDCache();
// 2. 启动 DMA2D 传输
HAL_DMA2D_Start(&hdma2d, srcAddr, dstAddr, width, height);
// 3. 等待传输完成(轮询或中断)
HAL_DMA2D_PollForTransfer(&hdma2d, HAL_MAX_DELAY);
// 4. 失效整个 D-Cache,使 CPU 看到 DMA 写入的新数据
SCB_InvalidateDCache();
// 此时读取目标缓冲区数据是安全的
}
注意事项
- 性能开销大:每次传输都刷新整个 Cache,对于高频 DMA(如摄像头采集)会严重拖慢系统。
- 实时性影响:在中断中执行全缓存刷新可能导致中断延迟增加。
- 适用场景:低频、大数据块传输,或系统对性能不敏感。
三、策略二:MPU 配置描述符区域为非缓存(推荐)
原理
利用 Cortex-M4 的 MPU(内存保护单元),将包含 DMA 描述符的 SRAM 区域配置为 非缓存(Non-cacheable) 属性。这样 CPU 访问该区域时直接读写物理内存,绕过 D-Cache,从根本上避免一致性问题。
配置步骤
- 在系统初始化时,配置 MPU 区域:
void MPU_Config_NonCacheable(void) { MPU_Region_InitTypeDef MPU_InitStruct; // 禁用 MPU 进行配置 HAL_MPU_Disable(); // 配置区域:基地址 0x20000000,大小 4KB(覆盖描述符区域) MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x20000000; MPU_InitStruct.Size = MPU_REGION_SIZE_4KB; 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); // 使能 MPU HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); } - 在
main函数初始化中调用MPU_Config_NonCacheable()。 - 将 DMA 描述符定义在该区域(例如通过链接脚本或直接指定地址)。
完整代码示例(使用属性指定地址)
// 将描述符放在非缓存区域(假设 0x20000000 起始的 4KB)
__attribute__((section(".non_cacheable"))) DMA2D_DescTypeDef dma2d_desc;
// 在链接脚本中定义 .non_cacheable 段,并映射到 0x20000000
void DMA2D_TransferWithMPU(void) {
// 无需手动刷新 Cache,直接操作描述符
dma2d_desc.control = ...;
dma2d_desc.srcAddr = ...;
HAL_DMA2D_Start(&hdma2d, ...);
HAL_DMA2D_PollForTransfer(&hdma2d, HAL_MAX_DELAY);
// 读取状态,数据一致
if (dma2d_desc.status == DONE) { ... }
}
注意事项
- 区域大小需对齐:MPU 区域大小必须是 2 的幂次,且基地址对齐。
- 性能影响:非缓存区域访问速度较慢,但仅影响描述符,不影响大数据缓冲区。
- 适用场景:描述符频繁访问,且对实时性要求高,如网络协议栈、USB 等。
四、策略三:DMA 中断中精准维护(精细控制)
原理
在 DMA 传输完成中断中,仅对描述符和关键数据缓冲区执行 Clean 和 Invalidate 操作,而不是全缓存刷新。使用 SCB_CleanDCache_by_Addr() 和 SCB_InvalidateDCache_by_Addr() 按地址范围操作,粒度更细,开销更小。
配置步骤
- 使能 D-Cache。
- 在 DMA 传输前,清理描述符区域(确保描述符写回):
SCB_CleanDCache_by_Addr((uint32_t*)&dma2d_desc, sizeof(dma2d_desc)); - 在 DMA 传输完成中断中,失效描述符和数据缓冲区:
void DMA2D_IRQHandler(void) { // 检查中断标志 if (__HAL_DMA2D_GET_FLAG(&hdma2d, DMA2D_FLAG_TC)) { // 失效描述符区域 SCB_InvalidateDCache_by_Addr((uint32_t*)&dma2d_desc, sizeof(dma2d_desc)); // 失效数据缓冲区(假设长度为 len) SCB_InvalidateDCache_by_Addr((uint32_t*)dataBuffer, len); // 清除标志 __HAL_DMA2D_CLEAR_FLAG(&hdma2d, DMA2D_FLAG_TC); // 通知任务处理 } }
完整代码示例(结合 HAL 回调)
// 在 HAL 库中,DMA2D 传输完成回调
void HAL_DMA2D_TransferComplete_Callback(DMA2D_HandleTypeDef *hdma2d) {
// 精准失效描述符和数据缓冲区
SCB_InvalidateDCache_by_Addr((uint32_t*)&dma2d_desc, sizeof(dma2d_desc));
SCB_InvalidateDCache_by_Addr((uint32_t*)dataBuffer, len);
// 处理数据...
}
void StartTransfer(void) {
// 清理描述符区域,确保 DMA 看到最新配置
SCB_CleanDCache_by_Addr((uint32_t*)&dma2d_desc, sizeof(dma2d_desc));
HAL_DMA2D_Start_IT(&hdma2d, src, dst, w, h);
}
注意事项
-
地址对齐:
SCB_CleanDCache_by_Addr要求地址按 32 字节对齐,否则可能无效。 - 长度处理:长度最好为 32 的倍数,否则需手动向上取整。
- 适用场景:高频 DMA 传输,且描述符和数据缓冲区大小固定,适合网络、音频等流式处理。
五、总结与选型建议
| 策略 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 全缓存刷新 | 实现简单,无需额外配置 | 性能开销大,影响实时性 | 低频、大块传输,调试阶段 | | MPU 非缓存区域 | 无一致性风险,性能稳定 | 占用 MPU 区域,访问速度稍慢 | 描述符频繁访问,高实时性系统 | | 中断精准维护 | 开销最小,灵活 | 需注意对齐和长度,代码稍复杂 | 高频传输,流式数据处理 |
在实际项目中,建议优先考虑 MPU 策略,因为它从硬件层面隔离了问题,代码逻辑最清晰。若 MPU 区域不足或需要动态管理,则采用中断精准维护。全缓存刷新仅作为临时方案或调试辅助。
掌握这三种策略,你就能在 STM32F4 上放心使用 D-Cache 和 DMA 协同工作,避免那些令人头疼的“随机性”数据错误。