STM32H7 的 D-Cache 与 DMA 数据一致性:SCB_CleanDCache_by_Addr 的正确用法与踩坑记录

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

STM32H7 搭载 Cortex-M7 内核,主频高达 480MHz,为了匹配 CPU 的高速运算,芯片内部集成了 D-Cache(数据缓存)。CPU 访问数据时,会优先从 D-Cache 中读取,写数据时也先写入 D-Cache,再由硬件在适当的时候同步到主存(SRAM)。

而 DMA 控制器是“老实人”,它直接访问主存,完全不知道 D-Cache 的存在。这就导致了经典的一致性问题:

  • CPU 写,DMA 读:CPU 将数据写入 D-Cache,但尚未回写到 SRAM,此时启动 DMA 发送,DMA 从 SRAM 读到的是旧数据。
  • DMA 写,CPU 读:DMA 将新数据写入 SRAM,但 CPU 读取时命中 D-Cache 中的旧数据,导致读到“脏”数据。

解决这一问题的核心就是软件维护 Cache 一致性,即使用 SCB_CleanDCache_by_Addr、SCB_InvalidateDCache_by_Addr 等函数。

2. 关键函数:Clean、Invalidate 与 Clean&Invalidate

CMSIS 提供了三个核心函数:

  • SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将 Cache 中指定地址范围的数据写回到 SRAM。用于 CPU 写 → DMA 读 的场景。
  • SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):将 Cache 中指定地址范围的数据无效化。CPU 下次读取时会从 SRAM 重新加载。用于 DMA 写 → CPU 读 的场景。
  • SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):先 Clean 再 Invalidate,用于双向传输或不确定的场景。

注意:这些函数的参数 addr 必须是 32 字节对齐的地址,dsize 必须是 32 字节的整数倍。否则函数内部会触发断言或行为异常。

3. 正确使用步骤与代码示例

3.1 发送数据(CPU → DMA)

#include "core_cm7.h"

#define BUFFER_SIZE  1024
#define CACHE_LINE   32

// 确保缓冲区按 32 字节对齐
__attribute__((aligned(CACHE_LINE))) uint8_t tx_buffer[BUFFER_SIZE];

void send_data_dma(void)
{
    // 1. 填充数据到 tx_buffer
    for (int i = 0; i < BUFFER_SIZE; i++) {
        tx_buffer[i] = i & 0xFF;
    }

    // 2. 清理 D-Cache,将数据写回 SRAM
    // 注意:地址和长度必须按 32 字节对齐
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, BUFFER_SIZE);

    // 3. 启动 DMA 传输
    HAL_DMA_Start(&hdma_usart1_tx, (uint32_t)tx_buffer, (uint32_t)&USART1->TDR, BUFFER_SIZE);
}

3.2 接收数据(DMA → CPU)

__attribute__((aligned(CACHE_LINE))) uint8_t rx_buffer[BUFFER_SIZE];

void receive_data_dma(void)
{
    // 1. 启动 DMA 接收
    HAL_DMA_Start(&hdma_usart1_rx, (uint32_t)&USART1->RDR, (uint32_t)rx_buffer, BUFFER_SIZE);

    // 2. 等待 DMA 传输完成(实际项目中建议用中断或轮询标志)
    while (HAL_DMA_PollForTransfer(&hdma_usart1_rx, HAL_DMA_FULL_TRANSFER, 1000) != HAL_OK);

    // 3. 无效化 D-Cache,确保 CPU 读取时从 SRAM 获取新数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, BUFFER_SIZE);

    // 4. 处理数据
    process_data(rx_buffer, BUFFER_SIZE);
}

3.3 双向传输(如 SPI 全双工)

__attribute__((aligned(CACHE_LINE))) uint8_t spi_tx[BUFFER_SIZE];
__attribute__((aligned(CACHE_LINE))) uint8_t spi_rx[BUFFER_SIZE];

void spi_transfer_dma(void)
{
    // 准备发送数据
    fill_tx_buffer(spi_tx, BUFFER_SIZE);

    // 清理发送缓冲区
    SCB_CleanDCache_by_Addr((uint32_t *)spi_tx, BUFFER_SIZE);

    // 启动 SPI DMA 收发
    HAL_SPI_TransmitReceive_DMA(&hspi1, spi_tx, spi_rx, BUFFER_SIZE);

    // 等待完成...

    // 无效化接收缓冲区
    SCB_InvalidateDCache_by_Addr((uint32_t *)spi_rx, BUFFER_SIZE);

    // 处理接收数据
    process_data(spi_rx, BUFFER_SIZE);
}

4. 踩坑记录与注意事项

4.1 地址与长度未对齐

坑:直接传入任意地址和长度,例如 SCB_CleanDCache_by_Addr((uint32_t*)&buf[1], 100)。

现象:函数内部断言失败,或清理不完整,导致数据错乱。

解决:始终使用 __attribute__((aligned(32))) 定义缓冲区,并确保操作长度是 32 的倍数。若无法对齐,可手动扩展范围到 32 字节边界。

4.2 在 DMA 传输过程中操作 Cache

坑:在 DMA 还在搬运数据时,就调用 Invalidate 或 Clean。

现象:Cache 与 DMA 竞争访问,数据损坏。

解决:必须在 DMA 传输完全停止后,再进行 Cache 维护操作。对于循环模式 DMA,需特别小心,通常建议使用双缓冲或暂停 DMA。

4.3 忘记配置 MPU 导致 Cache 策略错误

坑:默认情况下,STM32H7 的 SRAM 区域可能被配置为 Write-Back Cache,但某些区域(如 DMA 缓冲区)更适合 Write-Through 或 Non-Cacheable。

解决:使用 MPU 将 DMA 缓冲区所在区域配置为 Non-Cacheable 或 Write-Through,可从根本上避免一致性问题。例如:

MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000; // 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);

4.4 使用 SCB_InvalidateDCache 导致未提交数据丢失

坑:在 CPU 刚写完数据但未 Clean 的情况下,直接 Invalidate,会丢弃 Cache 中的新数据。

解决:若不确定 Cache 状态,使用 SCB_CleanInvalidateDCache_by_Addr,但要注意性能开销。

4.5 中断与 Cache 维护的原子性

坑:在 Cache 维护过程中被中断打断,中断服务程序又访问了同一缓冲区。

解决:在维护 Cache 前后关闭中断,或使用互斥锁保护。

5. 总结

STM32H7 的 D-Cache 是一把双刃剑,用好了性能飙升,用错了数据错乱。核心原则:

  • CPU 写、DMA 读 → 先 Clean。
  • DMA 写、CPU 读 → 先 Invalidate。
  • 地址长度 32 字节对齐。
  • DMA 停止后再维护 Cache。
  • 善用 MPU 将 DMA 缓冲区设为 Non-Cacheable,一劳永逸。

掌握这些要点,你就能在 STM32H7 上驾驭 DMA 与 D-Cache,构建稳定高效的系统。