一、问题现象与背景

在 STM32F4 系列(如 STM32F429/439)开发中,为了提升性能,我们常启用 D-Cache(数据缓存),并使用外部 SDRAM 作为帧缓冲或大容量数据区。然而,当 DMA 控制器或 LCD 控制器(LTDC)直接访问 SDRAM 时,CPU 通过 D-Cache 读写的数据可能尚未写回 SDRAM,或 SDRAM 中的数据已被外设更新但 Cache 仍保留旧值,从而引发数据不一致。典型现象包括:

  • LCD 显示花屏、撕裂;
  • 以太网或 USB 传输的数据包内容错误;
  • 文件系统读写 SDRAM 缓存时出现随机损坏。

二、D-Cache 与 SDRAM 数据一致性原理

2.1 D-Cache 的工作机制

D-Cache 是 CPU 与主存(SDRAM)之间的高速缓存,以缓存行(通常为 32 字节)为单位。当 CPU 读取数据时,若命中 Cache 则直接返回,否则从 SDRAM 加载整个缓存行;写入时,若采用写回(Write-back)策略,数据先写入 Cache,并标记为脏(Dirty),只有在缓存行被替换或显式清理时才会写回 SDRAM。

2.2 不一致性根源

  • DMA 写入 SDRAM,CPU 读取:DMA 将数据写入 SDRAM,但 D-Cache 中仍保留旧数据,CPU 读取时命中 Cache,得到过期数据。
  • CPU 写入 SDRAM,DMA 读取:CPU 写入的数据暂存于 Cache,尚未写回 SDRAM,DMA 直接读取 SDRAM 得到旧数据。

三、问题定位方法

3.1 复现与隔离

  • 尝试关闭 D-Cache(SCB_DisableDCache()),若问题消失,则基本确认与 Cache 相关。
  • 检查 SDRAM 内存区域是否被配置为可缓存(Cacheable)。在 STM32F4 中,MPU(内存保护单元)默认将外部 RAM 区域配置为 Write-back,这容易引发问题。

3.2 使用调试工具

  • 利用硬件调试器观察 Cache 状态寄存器(如 D-Cache 的清洁/无效化操作)。
  • 在关键点插入断点,对比 SDRAM 实际数据与 CPU 读取数据。

四、解决方案

4.1 硬件设计层面

  • 若条件允许,使用支持 Cache 一致性的总线桥(如 AXI 的 Coherency 扩展),但 STM32F4 不具备此功能,因此主要依赖软件。
  • 优化 PCB 布局,减少 SDRAM 访问延迟,但这不解决一致性问题。

4.2 内存属性配置(MPU)

将 SDRAM 区域配置为 非缓存(Non-cacheable)写通(Write-through),以牺牲部分性能换取一致性。推荐使用 MPU 设置。

// 配置 MPU 将 SDRAM 区域设为 Write-through
void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct;

    // 禁用 MPU
    HAL_MPU_Disable();

    // 配置 SDRAM 区域(基址 0xC0000000,大小 8MB)
    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_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_ENABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

4.3 软件同步操作

当无法避免使用 Cacheable 区域时,必须在 DMA 操作前后执行 Cache 维护操作。

4.3.1 清理(Clean)与无效化(Invalidate)

  • DMA 写入前:清理 Cache,确保 CPU 数据写回 SDRAM。
  • DMA 读取后:无效化 Cache,使 CPU 重新从 SDRAM 加载。
// 清理 D-Cache(将脏数据写回 SDRAM)
SCB_CleanDCache_by_Addr((uint32_t*)buffer, size);

// 无效化 D-Cache(丢弃缓存数据,下次从 SDRAM 读取)
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, size);

// 清理并无效化(用于 DMA 双向传输)
SCB_CleanInvalidateDCache_by_Addr((uint32_t*)buffer, size);

注意:地址需按 32 字节对齐,大小应为 32 的倍数,否则需手动处理边界。

4.3.2 完整示例:DMA 接收数据

#define BUFFER_SIZE 1024
uint8_t rx_buffer[BUFFER_SIZE] __attribute__((aligned(32)));

void DMA_Receive(void)
{
    // 无效化 Cache,避免读取到陈旧数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, BUFFER_SIZE);

    // 启动 DMA 接收(假设已配置好)
    HAL_UART_Receive_DMA(&huart, rx_buffer, BUFFER_SIZE);

    // 等待 DMA 完成(中断或轮询)
    while (dma_busy);

    // 此时 rx_buffer 中的数据已从 SDRAM 加载到 Cache,可直接使用
}

4.3.3 完整示例:DMA 发送数据

void DMA_Send(uint8_t* data, uint32_t size)
{
    // 清理 Cache,确保数据写回 SDRAM
    SCB_CleanDCache_by_Addr((uint32_t*)data, size);

    // 启动 DMA 发送
    HAL_UART_Transmit_DMA(&huart, data, size);
}

五、注意事项

  • 对齐与大小:Cache 操作要求地址 32 字节对齐,大小 32 的倍数。若缓冲区不对齐,需额外处理首尾部分。
  • 性能权衡:频繁的 Clean/Invalidate 会降低性能,建议对关键缓冲区采用非缓存配置,或使用 DMA 双缓冲并交替操作。
  • 多任务并发:在 RTOS 环境中,需确保 Cache 操作与 DMA 操作的原子性,必要时使用临界区保护。
  • 调试建议:在开发初期,可暂时关闭 D-Cache 以简化调试,待功能稳定后再优化性能。

六、总结

STM32F4 的 D-Cache 与 SDRAM 数据一致性问题是嵌入式开发中的经典难点。通过理解 Cache 工作原理,合理配置 MPU 内存属性,并在 DMA 操作前后执行正确的 Cache 维护,可以彻底解决此类问题。希望本文的定位方法和解决方案能帮助你快速排查并修复相关 Bug,提升系统稳定性。