引言

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 操作。

代码示例

// 假设 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(注意顺序)。

代码示例

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 清理(写)或失效(读)。

代码示例

#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 开发中的经典陷阱,提升系统可靠性。