引言

在嵌入式开发中,STM32F4 系列凭借强大的 Cortex-M4 内核和丰富的外设,常被用于图像处理、音频采集等大数据量场景。为了提升性能,我们通常会将 SDRAM 作为外部存储器,并启用 D-Cache 加速 CPU 访问。然而,D-Cache 的引入带来了一个隐蔽的陷阱:数据一致性问题。当 DMA 外设直接访问 SDRAM 时,CPU 可能从 Cache 中读到陈旧数据,或者 DMA 写入的数据被 Cache 覆盖,导致系统行为异常。本文将通过一个真实案例,带你从踩坑到正确配置,彻底掌握 D-Cache 与 SDRAM 的协同工作。

1. 原理剖析:D-Cache 与 SDRAM 的交互

1.1 D-Cache 的工作机制

D-Cache 是 CPU 内部的高速缓存,用于缓存最近访问的内存数据。当 CPU 读取内存时,首先检查 Cache 是否命中;若命中,则直接返回 Cache 中的数据,避免访问慢速的 SDRAM。写入时,Cache 采用 写回(Write-back) 策略,数据先写入 Cache,标记为脏(Dirty),直到被替换或显式清理时才写回 SDRAM。

1.2 数据一致性问题的根源

  • DMA 读取场景:CPU 将数据写入 SDRAM(可能残留在 Cache 中),随后 DMA 外设直接读取 SDRAM。由于 Cache 中的数据尚未写回,DMA 读到的是旧数据。
  • DMA 写入场景:DMA 将数据写入 SDRAM,而 CPU 的 Cache 中可能缓存了同一地址的旧数据。当 CPU 再次读取时,命中 Cache,得到旧值,而非 DMA 写入的新数据。

1.3 解决策略

  • Clean(清理):将 Cache 中的脏数据写回 SDRAM,确保 DMA 读取前数据一致。
  • Invalidate(失效):使 Cache 中的对应行失效,强制 CPU 下次从 SDRAM 重新加载,确保 DMA 写入后 CPU 能读到新数据。

2. 实战踩坑:一个音频采集案例

2.1 项目背景

某音频采集系统使用 STM32F429,通过 I2S 外设接收麦克风数据,DMA 将数据存入外部 SDRAM,CPU 对数据进行 FFT 处理。初期未启用 D-Cache,一切正常;启用 D-Cache 后,FFT 结果偶尔出现异常,且无规律。

2.2 调试过程

  • 检查 DMA 配置:确认 DMA 传输完成中断正常触发。
  • 打印 SDRAM 数据:发现 DMA 写入后,CPU 读取的数据部分正确、部分错误,且错误地址与 Cache 行大小(32 字节)对齐相关。
  • 最终定位:DMA 写入后,Cache 中保留了旧数据,CPU 读取时命中 Cache,导致数据不一致。

3. 正确配置步骤

3.1 启用 D-Cache

system_stm32f4xx.c 中,确保 SCB_EnableDCache() 被调用(通常在 SystemInit() 后)。

void SystemInit(void)
{
    // ... 其他初始化
    SCB_EnableDCache();
}

3.2 配置 MPU(内存保护单元)

为了确保 SDRAM 区域不被 Cache 错误缓存,需要配置 MPU 将 SDRAM 区域设置为 非缓存(Non-cacheable)写回(Write-back) 属性。推荐使用写回模式,并配合软件维护。

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_CACHEABLE; // 允许缓存
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; // 写回模式
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU
    HAL_MPU_Enable(MPU_CONTROL_PRIVILEGED_DEFAULT);
}

3.3 数据一致性维护函数

在 DMA 传输前后,调用以下函数确保一致性:

// 清理 Cache:将脏数据写回 SDRAM(DMA 读取前调用)
void Cache_Clean(uint32_t addr, uint32_t size)
{
    SCB_CleanDCache_by_Addr((uint32_t*)addr, (int32_t)size);
}

// 失效 Cache:使缓存行失效(DMA 写入后调用)
void Cache_Invalidate(uint32_t addr, uint32_t size)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)addr, (int32_t)size);
}

// 清理并失效(用于双向操作)
void Cache_CleanInvalidate(uint32_t addr, uint32_t size)
{
    SCB_CleanInvalidateDCache_by_Addr((uint32_t*)addr, (int32_t)size);
}

注意:地址和大小需按 32 字节对齐,否则可能影响相邻数据。

3.4 完整代码示例

以下是一个 DMA 接收数据的典型流程:

#define SDRAM_BUF_ADDR  0xC0001000  // SDRAM 缓冲区地址
#define BUF_SIZE        1024        // 缓冲区大小(字节)

// 启动 DMA 接收(假设 DMA 已配置)
void Start_DMA_Receive(void)
{
    // 清理 Cache,确保 DMA 读取到最新数据(如果之前有写入)
    Cache_Clean(SDRAM_BUF_ADDR, BUF_SIZE);

    // 启动 DMA 传输(外设到内存)
    HAL_DMA_Start_IT(&hdma, (uint32_t)&MIC_DATA, SDRAM_BUF_ADDR, BUF_SIZE/4);
}

// DMA 传输完成回调
void DMA_Complete_Callback(void)
{
    // 失效 Cache,使 CPU 从 SDRAM 重新加载数据
    Cache_Invalidate(SDRAM_BUF_ADDR, BUF_SIZE);

    // 现在可以安全地处理数据
    Process_FFT((uint32_t*)SDRAM_BUF_ADDR, BUF_SIZE/4);
}

4. 注意事项

  • 对齐要求SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 要求地址和大小按 32 字节对齐。若不对齐,需手动调整范围。
  • 性能权衡:频繁清理/失效 Cache 会降低性能,建议在 DMA 传输前后一次性操作,避免逐字节操作。
  • 多缓冲机制:若使用双缓冲,确保每个缓冲区独立维护一致性。
  • MPU 配置:若未配置 MPU,默认情况下 SDRAM 可能被设置为不可缓存,此时 D-Cache 失效,但性能下降。建议明确配置。
  • 中断安全:在中断中调用 Cache 操作时,注意优先级和嵌套,避免死锁。

5. 总结

D-Cache 是提升 STM32F4 性能的利器,但必须与 SDRAM 和 DMA 协同工作。通过理解 Cache 的写回机制,合理配置 MPU,并在关键点执行 Clean/Invalidate 操作,可以彻底解决数据一致性问题。希望本文的实战经验能帮助你少走弯路,让系统稳定运行。