引言:一次诡异的 DMA 传输失败

某工业控制项目中,我们使用 STM32F429 驱动外部 SDRAM 作为显示缓冲区,并通过 DMA 将图像数据从 SDRAM 搬运到 LCD 控制器。代码运行一段时间后,屏幕出现随机花屏,调试发现 DMA 读取的数据与 CPU 写入的数据不一致,甚至出现“旧数据”残留。排查良久,最终定位到罪魁祸首——D-Cache 未正确维护。

原理剖析:D-Cache 与 SDRAM 的一致性鸿沟

为什么 D-Cache 会带来问题?

STM32F4 系列(如 F429、F407)内置 4KB 或 8KB 的 D-Cache,用于加速 CPU 对内存的访问。当 CPU 写入 SDRAM 时,数据可能暂存在 Cache 中,并未立即写回 SDRAM;同样,DMA 外设直接访问 SDRAM 时,绕过 Cache,导致 CPU 和 DMA 看到的数据视图不一致。

  • 写操作:CPU 写 Cache,SDRAM 中仍是旧数据(Write-back 策略)。
  • 读操作:CPU 读 Cache,可能命中已失效的旧数据(Stale data)。

关键概念:Clean 与 Invalidate

  • Clean (Cache Clean):将 Cache 中已修改的数据写回 SDRAM,确保外设可见。
  • Invalidate (Cache Invalidate):将 Cache 中的行标记为无效,后续读取强制从 SDRAM 加载最新数据。

配置步骤:开启 D-Cache 与 SDRAM 初始化

1. 使能 D-Cache

在系统初始化代码中,调用 SCB_EnableDCache() 使能 D-Cache。注意:必须在所有外设(如 DMA、LTDC)初始化之前使能,否则可能出现不可预知行为。

void SystemInit_Cache(void) {
    SCB_EnableDCache();
    // 可选:配置 Cache 策略,如 Write-back, Write-allocate
    // SCB->CCR |= SCB_CCR_DWTC_Msk; // 使能 Write-Through 模式(谨慎使用)
}

2. SDRAM 初始化与 MPU 配置

SDRAM 通常映射在外部存储器区域(如 Bank1 0xC0000000)。为了正确管理 Cache,建议为 SDRAM 区域配置 MPU(Memory Protection Unit),设置其属性为“Normal, Write-back, Write-allocate”,以便使用标准 Cache 维护指令。

void MPU_Config_SDRAM(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; // 根据 SDRAM 大小调整
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; // Normal memory, Write-back, Write-allocate
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
    
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_CONTROL_HRD_MEM_MAP);
}

数据一致性维护策略:Clean & Invalidate 实战

场景一:CPU 写 SDRAM,DMA 读取(如显示缓冲区)

在 CPU 完成写入后,必须执行 Cache Clean,确保数据写回 SDRAM,再启动 DMA 传输。

// 假设 buffer_addr 是 SDRAM 中的地址,buffer_size 是字节数
void CPU_Write_To_SDRAM(uint32_t buffer_addr, uint8_t *data, uint32_t size) {
    // 1. CPU 写入数据到 SDRAM(通过普通指针)
    memcpy((void*)buffer_addr, data, size);
    
    // 2. Clean D-Cache,将写入的数据写回 SDRAM
    SCB_CleanDCache_by_Addr((uint32_t*)buffer_addr, size);
    
    // 3. 现在可以安全启动 DMA 读取
    Start_DMA_Read(buffer_addr, size);
}

场景二:DMA 写 SDRAM,CPU 读取(如摄像头采集)

DMA 将数据写入 SDRAM 后,CPU 读取前必须 Invalidate 对应 Cache 行,避免读到旧数据。

void DMA_Write_To_SDRAM(uint32_t buffer_addr, uint32_t size) {
    // 1. 启动 DMA 写入 SDRAM
    Start_DMA_Write(buffer_addr, size);
    // 等待 DMA 完成(如中断或轮询)
    Wait_DMA_Complete();
    
    // 2. Invalidate D-Cache,使 CPU 强制从 SDRAM 重新加载
    SCB_InvalidateDCache_by_Addr((uint32_t*)buffer_addr, size);
    
    // 3. 现在 CPU 可以安全读取数据
    Process_Data((uint8_t*)buffer_addr, size);
}

场景三:双缓冲交替使用

在双缓冲场景中,每个缓冲区在切换前都需要执行相应的 Clean 或 Invalidate 操作,确保数据同步。

#define BUFFER_SIZE 1024
uint32_t buffer_A = 0xC0000000;
uint32_t buffer_B = 0xC0001000;

void Switch_Buffer(uint8_t use_A) {
    uint32_t active_buf = use_A ? buffer_A : buffer_B;
    uint32_t inactive_buf = use_A ? buffer_B : buffer_A;
    
    // 先 Clean 即将被 DMA 读取的缓冲区(假设 CPU 已写入数据)
    SCB_CleanDCache_by_Addr((uint32_t*)active_buf, BUFFER_SIZE);
    
    // 启动 DMA 从 active_buf 读取
    Start_DMA_Read(active_buf, BUFFER_SIZE);
    
    // 同时,对 inactive_buf 进行 Invalidate,准备接收新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)inactive_buf, BUFFER_SIZE);
}

注意事项与常见陷阱

  • 地址对齐:Clean/Invalidate 操作要求地址按 32 字节对齐(Cache line 大小)。若缓冲区未对齐,需手动调整或使用更大的对齐区域。
  • 大小参数SCB_CleanDCache_by_Addr 的 size 参数必须是 32 字节的倍数,否则可能遗漏部分行。
  • 频繁操作开销:每次 Clean/Invalidate 都有性能开销,应尽量减少调用次数,例如在批量操作后统一维护。
  • DMA 与 Cache 并发:确保 DMA 传输期间不要对同一区域执行 Cache 操作,否则可能导致数据错乱。建议使用信号量或关中断保护。
  • MPU 配置错误:若 SDRAM 区域未配置为 Cacheable,则 Clean/Invalidate 无效,但性能下降;若配置为 Write-Through,则无需 Clean,但性能可能受损。
  • 使用 HAL 库注意:HAL 库提供 HAL_DCACHE_Clean() 等函数,但它们是全局操作,效率较低,建议使用 CMSIS 提供的地址级函数。

总结

D-Cache 与 SDRAM 的数据一致性是 STM32F4 开发中不可忽视的细节。通过理解 Cache 工作原理,合理配置 MPU,并严格遵循“写后 Clean,读前 Invalidate”的原则,即可避免大多数缓存一致性问题。记住:在嵌入式世界里,缓存不是免费的午餐,它需要你用正确的策略来“买单”。