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 写回物理内存,并失效所有行。此方法不依赖任何硬件配置,代码量最小。

配置步骤

  1. 在启动文件或 system_stm32f4xx.c 中使能 D-Cache:
    SCB_EnableDCache();
    
  2. 在 DMA 传输前,清理 Cache 确保描述符和数据已写回:
    SCB_CleanDCache();
    
  3. 在 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,从根本上避免一致性问题。

配置步骤

  1. 在系统初始化时,配置 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);
    }
    
  2. main 函数初始化中调用 MPU_Config_NonCacheable()
  3. 将 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() 按地址范围操作,粒度更细,开销更小。

配置步骤

  1. 使能 D-Cache。
  2. 在 DMA 传输前,清理描述符区域(确保描述符写回):
    SCB_CleanDCache_by_Addr((uint32_t*)&dma2d_desc, sizeof(dma2d_desc));
    
  3. 在 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 协同工作,避免那些令人头疼的“随机性”数据错误。