一、D-Cache 的硬件原理与一致性问题根源

STM32H7 的 Cortex-M7 内核集成了 16KB 的 D-Cache(数据缓存),用于加速 CPU 对 SRAM 的访问。当 CPU 执行读写操作时,数据会先被加载到 Cache 中,后续访问直接命中 Cache,避免每次访问低速的 AXI SRAM。

然而,这种机制在 DMA 或外设(如 LTDC、SDMMC)直接访问内存时,会引发一致性问题:

  • DMA 写入,CPU 读取:DMA 将数据写入 SRAM,但 CPU 的 Cache 中可能仍保留旧数据(Cache 命中),导致 CPU 读到过期数据。
  • CPU 写入,DMA 读取:CPU 修改数据后,数据仅更新在 Cache 中,尚未写回 SRAM,DMA 读取时得到的是旧数据。

此外,Cortex-M7 的 D-Cache 是写回(Write-back) 策略,即 CPU 写操作只更新 Cache,不会立即同步到 SRAM,直到缓存行被替换或显式 Clean 操作。这进一步加剧了不一致风险。

二、volatile 的局限与正确理解

很多开发者习惯用 volatile 修饰共享变量,期望解决一致性问题。但 volatile 只告诉编译器“该变量可能被外部修改,每次访问必须从内存地址读取”,它并不涉及硬件 Cache 的刷新或失效

volatile uint32_t flag;
// 编译器不会优化对 flag 的访问,但 CPU 仍可能命中 Cache 中的旧值

因此,在 STM32H7 中,volatile 只能防止编译器优化,无法解决 D-Cache 一致性问题。真正需要的是硬件层面的 Cache 维护操作

三、硬件修复:Cache 维护操作与 __DSB()

Cortex-M7 提供了 CP15 指令(通过 CMSIS 封装)来操作 D-Cache:

  • SCB_CleanDCache():将 Cache 中脏数据写回 SRAM(Clean)。
  • SCB_InvalidateDCache():使 Cache 行失效,下次访问强制从 SRAM 重新加载(Invalidate)。
  • SCB_CleanInvalidateDCache():先 Clean 再 Invalidate。

但仅调用这些函数还不够,因为 Cache 操作是异步的,需要数据同步屏障指令 __DSB() 来确保操作完成。__DSB() 会阻塞 CPU 直到所有之前的存储操作(包括 Cache 维护)完成。

典型流程:

// 在 DMA 启动前,确保 CPU 写入的数据已同步到 SRAM
SCB_CleanDCache();
__DSB();

// 启动 DMA 传输...

// 在 DMA 完成后,使 Cache 失效,让 CPU 重新从 SRAM 读取
SCB_InvalidateDCache();
__DSB();

四、实战案例:DMA 接收数据不一致

假设使用 UART DMA 接收不定长数据,数据存入 rx_buffer,并设置 rx_complete 标志。

#define BUF_SIZE 256
uint8_t rx_buffer[BUF_SIZE];
volatile uint8_t rx_complete = 0;

void DMA_IRQHandler(void) {
    if (DMA_GetITStatus(DMA1_Stream0, DMA_IT_TC)) {
        DMA_ClearITPendingBit(DMA1_Stream0, DMA_IT_TC);
        // 关键:使 Cache 失效,确保 CPU 读取到 DMA 写入的新数据
        SCB_InvalidateDCache();
        __DSB();
        rx_complete = 1;  // volatile 保证标志可见
    }
}

int main(void) {
    // 初始化 DMA...
    while (1) {
        if (rx_complete) {
            rx_complete = 0;
            // 此时 rx_buffer 中的数据是有效的
            process_data(rx_buffer);
        }
    }
}

注意:SCB_InvalidateDCache() 会使整个 D-Cache 失效,效率较低。对于大缓冲区,建议使用区域操作:

SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, BUF_SIZE);
__DSB();

五、注意事项与最佳实践

  • 区分 volatile 与 Cache 维护volatile 用于编译器层面,Cache 维护用于硬件层面,两者互补,不可替代。
  • 使用区域操作:尽量使用 _by_Addr 函数,避免全 Cache 刷新带来的性能损失。
  • 对齐与大小:Cache 操作要求地址 32 字节对齐,大小需为 32 的倍数,否则可能操作不完整。
  • DMA 描述符:如果 DMA 使用描述符(如 MDMA),描述符本身也可能被缓存,需同样处理。
  • RTOS 环境:在多任务系统中,Cache 维护操作需放在临界区或使用互斥锁,防止任务切换导致数据错乱。
  • 调试建议:遇到诡异数据问题时,可暂时关闭 D-Cache(SCB_DisableDCache())验证是否由一致性引起。

六、总结

STM32H7 的 D-Cache 是一把双刃剑,性能提升的同时也带来了数据一致性挑战。理解硬件缓存原理,正确使用 volatile__DSB() 配合 Cache 维护函数,是解决此类问题的关键。记住:volatile 防编译器,__DSB() 防硬件乱序,Cache 维护保数据一致。掌握这套组合拳,你就能在 H7 平台上写出既高效又可靠的代码。