一、问题现象与背景

某工业控制项目基于 STM32F429 平台,使用 SDMMC 接口读写 eMMC,并启用 D-Cache 以加速图像数据处理。系统运行一段时间后,随机出现 DMA 传输完成中断丢失、数据校验错误,甚至 HardFault。通过调试发现,问题集中在 DMA 描述符链表的更新上:CPU 修改描述符后,DMA 读取到的是旧数据,导致传输状态异常。

二、缓存一致性原理

STM32F4 系列(如 F429、F407)的 Cortex-M4 内核带有可选的 D-Cache(数据缓存)和 I-Cache(指令缓存)。D-Cache 位于 CPU 和总线之间,用于缓存外部存储器(如 SDRAM、SRAM)的数据。当 CPU 访问外部存储器时,如果命中缓存,则直接操作缓存行(Cache Line,通常为 32 字节),而不会立即写入物理内存。

DMA 控制器则直接通过总线访问物理内存,不经过 D-Cache。这就导致两个问题:

  • DMA 读取旧数据:CPU 修改了内存数据(但数据仍留在缓存中),DMA 直接读取物理内存,得到的是修改前的旧值。
  • DMA 写入被覆盖:DMA 将新数据写入物理内存,但 CPU 缓存中仍保留着旧数据,之后 CPU 再次读取该地址时,会命中缓存,得到旧值,导致数据丢失。

对于 DMA 描述符,通常由 CPU 初始化并更新,然后由 DMA 硬件读取。如果描述符所在内存区域被缓存,且 CPU 写入后未及时刷新(Clean)到物理内存,DMA 就会读到过时的描述符。

三、问题定位与排查步骤

1. 确认 D-Cache 是否启用

检查代码中是否调用了 SCB_EnableDCache(),或在 CubeMX 中启用了 D-Cache。若未启用,则不存在此问题。

2. 检查描述符内存区域属性

查看 DMA 描述符所在内存区是否被配置为 Cacheable(可缓存)。默认情况下,STM32F4 的 SRAM 区域(0x20000000 起始)是 Write-Back 可缓存的,而外部 SDRAM 区域(如 0xC0000000)也可配置为可缓存。

3. 复现与观察

在 DMA 传输完成中断中,打印描述符的状态字段。若发现状态字段与预期不符,或 DMA 始终未更新状态,则高度怀疑缓存一致性问题。

4. 使用调试器查看物理内存

在 CPU 修改描述符后,立即通过调试器读取描述符地址的物理内存值。若与 CPU 写入的值不同,则确认数据仍滞留在缓存中。

四、修复方案

方案一:禁用 D-Cache(不推荐)

直接调用 SCB_DisableDCache() 禁用 D-Cache,但会牺牲性能,违背启用缓存的初衷。仅适用于调试阶段。

方案二:将描述符放入非缓存区域(推荐)

利用 MPU(内存保护单元)将描述符所在内存区域配置为 Device 或 Strongly-ordered 属性,禁止缓存。这种方法无需修改每次访问的代码,但需要合理划分内存区域。

方案三:软件维护缓存(最常用)

在 CPU 修改描述符后,调用 SCB_CleanDCache_by_Addr() 将缓存行写回物理内存;在 DMA 写入描述符后(如传输完成中断),调用 SCB_InvalidateDCache_by_Addr() 使缓存行失效,强制从物理内存重新读取。

五、完整代码示例(基于 HAL 库)

以下以 SDMMC 为例,展示如何正确管理 DMA 描述符缓存。

// 定义描述符结构体(示例)
typedef struct {
    uint32_t control;   // 控制字
    uint32_t buffer;    // 缓冲区地址
    uint32_t status;    // 状态字(由 DMA 更新)
} SDMMC_DMA_Desc;

// 描述符数组(需对齐到 32 字节)
__attribute__((aligned(32))) SDMMC_DMA_Desc desc[2];

// 初始化描述符
void Desc_Init(void) {
    for (int i = 0; i < 2; i++) {
        desc[i].control = 0;
        desc[i].buffer = (uint32_t)buffer[i];
        desc[i].status = 0;
    }
    // 将描述符写回物理内存(Clean)
    SCB_CleanDCache_by_Addr((uint32_t*)desc, sizeof(desc));
}

// 更新描述符(例如设置控制字)
void Desc_Update(int idx, uint32_t control) {
    desc[idx].control = control;
    // 只刷新该描述符所在的缓存行(32字节对齐)
    SCB_CleanDCache_by_Addr((uint32_t*)&desc[idx], sizeof(SDMMC_DMA_Desc));
}

// DMA 传输完成中断回调
void HAL_SDMMC_DMA_RxCpltCallback(SDMMC_HandleTypeDef *hsd) {
    // 使描述符缓存失效,确保读取到 DMA 写入的最新状态
    SCB_InvalidateDCache_by_Addr((uint32_t*)desc, sizeof(desc));
    // 检查 desc[0].status 等字段
    if (desc[0].status & ERROR_FLAG) {
        // 处理错误
    }
}

// 主函数中启用 D-Cache
int main(void) {
    HAL_Init();
    SystemClock_Config();
    // 启用 D-Cache
    SCB_EnableDCache();
    // 初始化外设...
    Desc_Init();
    // 启动 DMA 传输...
}

六、注意事项

  • 缓存行对齐SCB_CleanDCache_by_Addr()SCB_InvalidateDCache_by_Addr() 要求地址按 32 字节对齐,且长度最好为 32 的整数倍。若描述符数组未对齐,需手动调整。
  • 频繁刷新开销:每次修改描述符都刷新缓存会带来性能损失,但通常描述符操作频率不高,影响可忽略。
  • 多描述符批量刷新:若一次更新多个描述符,可合并为一次刷新,减少开销。
  • 中断上下文:在中断中调用缓存维护函数是安全的,但需注意中断优先级和嵌套,避免长时间阻塞。
  • MPU 配置:若使用 MPU 将描述符区域设为非缓存,需确保 MPU 配置在启用 D-Cache 之前完成,且区域大小和属性正确。
  • 其他外设:以太网(ETH)、USB、DCMI 等外设的 DMA 描述符同样适用此方法。

七、总结

D-Cache 与 DMA 的缓存一致性是 STM32F4 开发中的经典陷阱。通过理解缓存工作原理,合理选择非缓存区域或软件维护缓存,可以彻底解决描述符不一致问题。建议在项目初期就规划好内存布局,将 DMA 描述符和缓冲区放置于独立区域,并统一管理缓存操作,以避免后期调试的困扰。