STM32H7 的 D-Cache 与 DMA 数据一致性:Clean/Invalidate 的边界条件与实测陷阱

1. 为什么 D-Cache 会成为 DMA 的“猪队友”?

Cortex-M7 的 D-Cache 是一把双刃剑。它通过缓存最近访问的数据来加速 CPU 读写,但 DMA 控制器直接访问物理内存,不经过 Cache。这就导致:

  • CPU 写数据后启动 DMA 发送:数据可能还在 Cache 中(Write-Back 模式),DMA 读到的是内存中的旧数据。
  • DMA 接收数据后 CPU 读取:Cache 中可能缓存了该内存区域的旧内容,CPU 读到的是旧数据而非 DMA 刚写入的新数据。

解决思路是操作 Cache 维护函数:SCB_CleanDCache_by_Addr()(将 Cache 内容写回内存)和 SCB_InvalidateDCache_by_Addr()(丢弃 Cache 内容,强制从内存重读)。但边界条件远非调用函数那么简单。

2. 核心原理:Clean 与 Invalidate 的语义

  • Clean:把指定地址范围的 Cache 行写回内存。用于 CPU 写 → DMA 读 场景(发送)。
  • Invalidate:把指定地址范围的 Cache 行标记为无效。下次 CPU 访问将重新从内存加载。用于 DMA 写 → CPU 读 场景(接收)。
  • Clean+Invalidate:先写回再无效。用于双向传输或不确定场景。

关键点:Cache 操作以 Cache Line(32 字节) 为单位。地址必须按 32 字节对齐,长度必须是 32 的倍数,否则会误伤相邻数据。

3. 配置步骤与完整代码示例

3.1 内存属性配置

DMA 缓冲区必须位于 Non-Cacheable 或 Write-Through 内存区域,或者使用手动维护。推荐将 DMA 缓冲区放在 0x30000000(D2 SRAM)并配置 MPU 为 Non-Cacheable,可省去手动维护。但若使用 Cacheable 内存,必须手动维护。

// 假设使用 D1 SRAM (0x24000000) 且 MPU 未配置为 Non-Cacheable
// 定义 32 字节对齐的 DMA 缓冲区
aligned(32) uint8_t dma_tx_buf[256];
aligned(32) uint8_t dma_rx_buf[256];

// 发送前:Clean 缓冲区,确保数据写入内存
void dma_send_prepare(void *buf, uint32_t len) {
    uint32_t addr = (uint32_t)buf;
    uint32_t size = len;
    // 地址对齐处理:向下对齐到 32 字节边界
    uint32_t aligned_addr = addr & ~0x1F;
    uint32_t offset = addr - aligned_addr;
    // 长度向上对齐到 32 字节倍数
    uint32_t aligned_size = (offset + size + 31) & ~0x1F;
    SCB_CleanDCache_by_Addr((uint32_t*)aligned_addr, aligned_size);
}

// 接收后:Invalidate 缓冲区,丢弃旧 Cache
void dma_recv_complete(void *buf, uint32_t len) {
    uint32_t addr = (uint32_t)buf;
    uint32_t size = len;
    uint32_t aligned_addr = addr & ~0x1F;
    uint32_t offset = addr - aligned_addr;
    uint32_t aligned_size = (offset + size + 31) & ~0x1F;
    SCB_InvalidateDCache_by_Addr((uint32_t*)aligned_addr, aligned_size);
}

3.2 完整 DMA 收发流程

// 发送流程
void uart_dma_send(uint8_t *data, uint16_t len) {
    memcpy(dma_tx_buf, data, len);          // 1. 拷贝到 DMA 缓冲区
    dma_send_prepare(dma_tx_buf, len);      // 2. Clean D-Cache
    HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len); // 3. 启动 DMA
}

// 接收流程(在 DMA 完成中断中调用)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    dma_recv_complete(dma_rx_buf, RX_LEN);  // 1. Invalidate D-Cache
    process_data(dma_rx_buf, RX_LEN);       // 2. 安全读取数据
    HAL_UART_Receive_DMA(&huart1, dma_rx_buf, RX_LEN); // 3. 重启接收
}

4. 实测陷阱与边界条件

陷阱 1:地址未对齐导致数据损坏

若缓冲区地址不是 32 字节对齐,SCB_CleanDCache_by_Addr 会操作包含该地址的整个 Cache Line。如果该 Cache Line 中还有其他变量,Clean 可能把无关数据写回,Invalidate 可能丢弃未保存的数据。务必使用 __attribute__((aligned(32))) 或 ALIGN_32BYTES 宏。

陷阱 2:长度未对齐导致越界

传入长度不是 32 的倍数时,函数内部会向上取整到 Cache Line 边界。若缓冲区末尾恰好是其他关键数据,Invalidate 会将其丢弃。建议缓冲区大小设为 32 的倍数,并传入实际长度,函数内部自动对齐。

陷阱 3:Invalidate 前未 Clean 导致数据丢失

如果 CPU 在 DMA 接收前曾写过该缓冲区(例如初始化),这些写操作可能还在 Cache 中。直接 Invalidate 会丢弃它们,导致内存中仍是旧数据。接收前应先 Clean 再 Invalidate,或确保缓冲区未被 CPU 写过。

陷阱 4:DMA 传输中 CPU 访问缓冲区

DMA 传输期间,CPU 若访问同一缓冲区,可能触发 Cache 与 DMA 的竞争。传输期间应避免 CPU 访问,或使用双缓冲区(Ping-Pong)机制。

陷阱 5:中断优先级与 Cache 维护顺序

在 DMA 完成中断中,必须先 Invalidate 再读取数据。若中断优先级配置不当,可能导致 Cache 维护操作被更高优先级中断打断,引发一致性问题。建议将 DMA 中断优先级设为较高,并确保 Cache 维护操作原子性。

5. 最佳实践总结

  • 优先使用 Non-Cacheable 内存:通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,可彻底避免一致性问题,性能损失可接受。
  • 若必须使用 Cacheable 内存:严格遵循“发送前 Clean,接收后 Invalidate”原则,并确保地址和长度 32 字节对齐。
  • 使用 SCB_InvalidateDCache_by_Addr 前先 Clean:防止丢弃未写回的数据。
  • 避免在 DMA 传输中访问缓冲区:使用双缓冲区或信号量同步。
  • 实测验证:用逻辑分析仪或调试器观察内存数据,确认 Cache 维护操作生效。

6. 结语

STM32H7 的 D-Cache 与 DMA 协同工作并非不可逾越的鸿沟,但需要开发者对 Cache 行、对齐、操作顺序有清晰的认识。掌握 Clean/Invalidate 的边界条件,结合 MPU 配置与双缓冲策略,你就能在嵌入式项目中充分发挥 H7 的性能,同时保证数据一致性。