STM32H7 的 D-Cache 与 DMA 一致性:地址对齐、Cache Line 与 Clean/Invalidate 的实战配置

STM32H7 系列搭载 Cortex-M7 内核,主频高达 480MHz,并配备了 L1 数据缓存(D-Cache)。D-Cache 能大幅提升 CPU 访问外部存储器的效率,但同时也引入了与 DMA 控制器之间的缓存一致性问题。若处理不当,DMA 传输的数据可能不是最新的,或者 CPU 读取到的是缓存中的旧数据。本文将从原理出发,结合实战配置,帮你彻底搞懂 D-Cache 与 DMA 的协同工作。

一、为什么会有缓存一致性问题?

Cortex-M7 的 D-Cache 是写回(Write-Back) 且写分配(Write-Allocate) 的。当 CPU 写数据时,数据先写入 Cache,并不立即写回主存(SRAM/SDRAM)。当 CPU 读数据时,若 Cache 未命中,则从主存加载到 Cache。

DMA 则直接访问主存,不经过 Cache。因此:

  • CPU 写 → DMA 读:CPU 写入的数据可能还在 Cache 中,未同步到主存,DMA 读到旧数据。
  • DMA 写 → CPU 读:DMA 将新数据写入主存,但 CPU 可能从 Cache 中读到旧数据。

解决方法是使用 Clean(将 Cache 内容写回主存)和 Invalidate(将 Cache 内容标记为无效,强制下次读取时从主存加载)操作。

二、Cache Line 与地址对齐

Cortex-M7 的 D-Cache Line 大小为 32 字节。Clean 和 Invalidate 操作都是以 Cache Line 为单位的。这意味着:

  • 如果缓冲区起始地址或长度未按 32 字节对齐,Clean/Invalidate 可能会影响相邻数据。
  • 例如,一个 4 字节的变量位于某个 Cache Line 中,Invalidate 该行会丢弃整个 32 字节的数据,若相邻数据尚未写回,将导致数据丢失。

黄金法则:用于 DMA 的缓冲区必须按 32 字节对齐,且大小最好是 32 的整数倍。可以使用 __attribute__((aligned(32))) 或 ALIGN_32BYTES 宏。

三、Clean 与 Invalidate 的使用场景

  • CPU 写,DMA 读(发送):在启动 DMA 发送前,对缓冲区执行 Clean 操作,确保数据写回主存。
  • DMA 写,CPU 读(接收):在 DMA 接收完成后,对缓冲区执行 Invalidate 操作,丢弃 Cache 中的旧数据。
  • 双向传输:先 Clean 再 Invalidate,或使用 SCB_CleanInvalidateDCache_by_Addr。

注意:Invalidate 操作必须确保 CPU 没有未写回的数据,否则会丢失。因此,在 Invalidate 前,最好先 Clean 或确保该区域未被修改。

四、实战配置步骤

1. 启用 D-Cache

在 main() 初始化时启用 D-Cache 和 I-Cache:

SCB_EnableICache();
SCB_EnableDCache();

2. 定义对齐的 DMA 缓冲区

#include "stm32h7xx.h"

#define BUFFER_SIZE 256
ALIGN_32BYTES(uint8_t dma_tx_buffer[BUFFER_SIZE]);
ALIGN_32BYTES(uint8_t dma_rx_buffer[BUFFER_SIZE]);

ALIGN_32BYTES 是 CMSIS 提供的宏,定义在 cmsis_compiler.h 中。

3. 发送数据前 Clean

void DMA_SendData(void) {
    // 填充数据
    for (int i = 0; i < BUFFER_SIZE; i++) {
        dma_tx_buffer[i] = i;
    }
    // Clean D-Cache,确保数据写回主存
    SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buffer, BUFFER_SIZE);
    // 启动 DMA 传输
    HAL_DMA_Start(&hdma_usart1_tx, (uint32_t)dma_tx_buffer, (uint32_t)&USART1->TDR, BUFFER_SIZE);
}

4. 接收数据后 Invalidate

void DMA_ReceiveData(void) {
    // 启动 DMA 接收
    HAL_DMA_Start(&hdma_usart1_rx, (uint32_t)&USART1->RDR, (uint32_t)dma_rx_buffer, BUFFER_SIZE);
    // 等待传输完成(中断或轮询)
    while (!dma_rx_complete);
    // Invalidate D-Cache,丢弃旧数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buffer, BUFFER_SIZE);
    // 现在可以安全读取 dma_rx_buffer
}

5. 使用 MPU 配置内存属性(可选但推荐)

对于频繁 DMA 的缓冲区,可以将其配置为 Non-Cacheable,从而避免 Clean/Invalidate 操作。但会降低 CPU 访问速度。

void MPU_Config(void) {
    HAL_MPU_Disable();
    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x30000000; // D2 SRAM 地址
    MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

五、注意事项与常见坑

  • 地址对齐:DMA 缓冲区必须 32 字节对齐,否则 Clean/Invalidate 可能破坏相邻数据。
  • 长度对齐:若缓冲区大小不是 32 的倍数,Clean/Invalidate 会覆盖到缓冲区外的数据。建议将大小向上取整到 32 的倍数。
  • Invalidate 前先 Clean:如果 CPU 可能修改了缓冲区,Invalidate 会丢弃未写回的数据。安全做法是先 Clean 再 Invalidate。
  • 中断与 DMA 竞争:在 DMA 传输过程中,CPU 不应访问缓冲区,否则可能引发一致性问题。
  • 使用 CMSIS 函数:SCB_CleanDCache_by_Addr 等函数已处理地址对齐,但传入的地址和长度仍需按 32 字节对齐。
  • 性能权衡:频繁的 Clean/Invalidate 会降低性能。对于大数据块,可考虑使用 Non-Cacheable 内存或 DMA 双缓冲。

六、总结

STM32H7 的 D-Cache 是一把双刃剑。正确使用 Clean 和 Invalidate 操作,并保证 DMA 缓冲区的地址与长度对齐,是确保数据一致性的关键。对于简单应用,可以在每次 DMA 传输前后调用 CMSIS 函数;对于高性能场景,建议通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable。理解原理,结合实际需求选择方案,才能发挥 H7 的最大效能。