为什么 D-Cache 会让 DMA 数据“变味”?

STM32H7 主频高达 480MHz,为了匹配 CPU 与存储器速度,Cortex-M7 内核配备了 L1 D-Cache(通常 16KB 或 32KB)。当 CPU 访问 SRAM 或外部 SDRAM 时,数据会被缓存到 D-Cache 中。

问题来了:DMA 是独立于 CPU 的总线主设备,它直接读写物理内存,不经过 D-Cache。于是出现两种典型错误:

  • DMA 写入内存,CPU 读到的却是旧缓存:CPU 之前读过的数据还在 Cache 里,DMA 更新了物理内存,但 CPU 仍从 Cache 取旧值。
  • CPU 写入数据,DMA 发送的却是旧值:CPU 写数据只更新了 Cache(写回策略),物理内存尚未更新,DMA 却从物理内存取数据发送。

解决思路只有两条:要么让 DMA 缓冲区不经过 Cache(MPU 配置为 Non-Cacheable),要么在恰当时机手动维护 Cache 一致性(Clean/Invalidate)。

地址对齐:32 字节是硬性要求

Cortex-M7 的 Cache 行(Cache Line)大小为 32 字节。Clean 和 Invalidate 操作的最小单位就是一行。

  • 如果 DMA 缓冲区起始地址或长度不是 32 字节对齐,维护 Cache 时可能误伤相邻数据,导致其他变量被意外丢弃或写回。
  • 强烈建议:所有 DMA 缓冲区使用 __attribute__((aligned(32))) 强制 32 字节对齐,且长度向上取整到 32 的倍数。
// 定义 32 字节对齐的 DMA 缓冲区
#define BUF_SIZE  256
__attribute__((aligned(32))) uint8_t dma_tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[BUF_SIZE];

MPU 配置:让 DMA 区域“绕过”Cache

最省心的方案是用 MPU 将 DMA 缓冲区所在内存区域配置为 Non-Cacheable, Non-Shareable。这样 CPU 和 DMA 都直接访问物理内存,无需任何 Clean/Invalidate 操作。

以 STM32H7 的 D2 SRAM(0x30000000)为例,配置 MPU 区域:

#include "stm32h7xx.h"

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置 D2 SRAM 区域为 Non-Cacheable
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x30000000;
    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_NOT_SHAREABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL1;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:MPU 配置必须在使能 D-Cache 之前完成,且区域不能与内核默认属性冲突。

Clean 与 Invalidate 的实战顺序

如果因性能原因必须使用 Cacheable 内存,就必须手动维护一致性。核心原则:

  • CPU 写 → DMA 读(发送方向):先 Clean(将 Cache 写回内存),再启动 DMA。
  • DMA 写 → CPU 读(接收方向):先 Invalidate(丢弃 Cache 旧数据),再启动 DMA;DMA 完成后,再次 Invalidate 才能安全读取。

发送方向(TX)

// 1. 填充数据到 dma_tx_buf
memcpy(dma_tx_buf, src, len);

// 2. Clean D-Cache,确保数据写入物理内存
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, BUF_SIZE);

// 3. 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, BUF_SIZE);

接收方向(RX)

// 1. 启动 DMA 前,Invalidate 缓冲区,丢弃旧缓存
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, BUF_SIZE);

// 2. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, BUF_SIZE);

// 3. DMA 完成中断中,再次 Invalidate 才能读取最新数据
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, BUF_SIZE);
    // 现在可以安全处理 dma_rx_buf 中的数据
}

完整示例:UART DMA 收发 + Cache 维护

#include "stm32h7xx.h"
#include "stm32h7xx_hal.h"

#define BUF_SIZE  256
__attribute__((aligned(32))) uint8_t dma_tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[BUF_SIZE];

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;

void DMA_Config(void)
{
    // 使能 DMA 时钟(略)
    // 配置 DMA 流,方向、外设地址、内存地址、数据长度等
    // 注意:内存地址必须 32 字节对齐
}

void UART_Init(void)
{
    // UART 初始化(略)
}

void Send_Data(uint8_t *src, uint16_t len)
{
    if (len > BUF_SIZE) len = BUF_SIZE;
    memcpy(dma_tx_buf, src, len);

    // 清理 D-Cache,确保 DMA 读到最新数据
    SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, BUF_SIZE);

    HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len);
}

void Start_Receive(void)
{
    // 无效化 D-Cache,避免 CPU 读到旧数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, BUF_SIZE);
    HAL_UART_Receive_DMA(&huart1, dma_rx_buf, BUF_SIZE);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 再次无效化,确保读到 DMA 写入的最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, BUF_SIZE);

    // 处理数据...
    // 重新启动接收
    Start_Receive();
}

int main(void)
{
    HAL_Init();
    MPU_Config();          // 先配置 MPU
    SCB_EnableICache();    // 使能 I-Cache
    SCB_EnableDCache();    // 使能 D-Cache
    UART_Init();
    DMA_Config();

    Start_Receive();

    uint8_t msg[] = "Hello STM32H7 DMA + Cache\r\n";
    Send_Data(msg, sizeof(msg) - 1);

    while (1) { }
}

注意事项与避坑指南

  • 不要对同一缓冲区同时 Clean 和 Invalidate:除非你明确知道数据流向,否则可能丢失未写回的数据。
  • DMA 完成中断中必须再次 Invalidate:否则 CPU 可能读到 Cache 中的旧数据。
  • 避免在 DMA 传输过程中操作 Cache:可能引发数据竞争。
  • 使用 SCB_* 函数时地址必须 32 字节对齐:否则行为未定义。
  • 如果使用外部 SDRAM,务必配置 MPU 为 Write-Through 或 Non-Cacheable:否则性能与一致性都难以保证。
  • 调试时若发现数据“偶尔”错误,优先检查 Cache 一致性:这是 H7 上最常见的隐蔽 bug。

掌握以上三点——对齐、MPU、Clean/Invalidate 顺序——你就能在 STM32H7 上放心地同时享受 D-Cache 的高性能和 DMA 的高效率。