为什么 H7 上 DMA 会“丢数据”?

STM32H7 的 Cortex-M7 内核带有 L1 数据 Cache(D-Cache),默认写回(Write-Back)策略。当 CPU 写数据到 SRAM 时,数据可能只停留在 Cache 中,并未真正写入物理内存;当 CPU 读数据时,也可能直接从 Cache 拿到旧值,而忽略了 DMA 刚刚搬运到内存的新数据。

DMA 是“绕过 Cache”直接访问物理内存的。因此,CPU 与 DMA 看到的内存视图可能不一致,导致:

  • CPU 写的数据 DMA 读不到(还在 Cache 里)
  • DMA 写的数据 CPU 读不到(Cache 里是旧值)

解决手段就是两条指令:

  • SCB_CleanDCache_by_Addr():把 Cache 中的脏数据写回物理内存
  • SCB_InvalidateDCache_by_Addr():丢弃 Cache 中的旧数据,强制下次从内存重新加载

核心原则:谁写谁 Clean,谁读谁 Invalidate

记住一句话:Clean 用于“我写完了,交给别人读”;Invalidate 用于“别人写完了,我要读”。

具体到 DMA 场景:

  • CPU 发送数据给外设(TX):CPU 先写缓冲区 → Clean → 启动 DMA
  • CPU 接收外设数据(RX):启动 DMA 前 Invalidate(防止 Cache 有旧值)→ DMA 完成后再次 Invalidate → CPU 读数据

注意:Invalidate 会丢弃未写回的数据,所以绝不能对 CPU 刚写过但未 Clean 的缓冲区做 Invalidate,否则数据直接丢失。

配置步骤与完整代码

1. 使能 Cache 与 MPU 配置

H7 默认开启 D-Cache。建议用 MPU 将 DMA 缓冲区所在区域配置为 Write-Through 或 Non-Cacheable,可大幅简化一致性处理。

void MPU_Config(void)
{
    HAL_MPU_Disable();
    MPU_Region_InitTypeDef MPU_Init = {0};

    MPU_Init.Enable           = MPU_REGION_ENABLE;
    MPU_Init.Number           = MPU_REGION_NUMBER0;
    MPU_Init.BaseAddress      = 0x30000000;      // D2 SRAM
    MPU_Init.Size             = MPU_REGION_SIZE_64KB;
    MPU_Init.SubRegionDisable = 0x00;
    MPU_Init.TypeExtField     = MPU_TEX_LEVEL1;
    MPU_Init.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_Init.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_Init.IsShareable      = MPU_ACCESS_SHAREABLE;
    MPU_Init.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE; // 关键:非缓存
    MPU_Init.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;
    HAL_MPU_ConfigRegion(&MPU_Init);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

2. 发送(TX)流程:Clean 后启动 DMA

#define TX_LEN  256
alignas(32) uint8_t tx_buf[TX_LEN];  // 必须 32 字节对齐

void UART_DMA_Send(void)
{
    for (int i = 0; i < TX_LEN; i++) tx_buf[i] = i;

    // 1. 将 Cache 中的新数据写回物理内存
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, TX_LEN);

    // 2. 启动 DMA
    HAL_UART_Transmit_DMA(&huart1, tx_buf, TX_LEN);
}

3. 接收(RX)流程:Invalidate 两次

#define RX_LEN  256
alignas(32) uint8_t rx_buf[RX_LEN];

void UART_DMA_StartRecv(void)
{
    // 启动前 Invalidate,清除 Cache 中可能存在的旧数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_LEN);
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_LEN);
}

// DMA 完成回调中再次 Invalidate
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_LEN);
    // 此时 CPU 读取 rx_buf 才是 DMA 写入的最新数据
    ProcessData(rx_buf, RX_LEN);
}

常见坑点与注意事项

  • 地址与长度必须 32 字节对齐:SCB_*Cache_by_Addr 要求地址对齐到 32 字节,长度建议向上取整到 32 的倍数,否则可能误伤相邻数据。
  • Invalidate 前必须确保没有未 Clean 的写操作:否则会丢弃 CPU 刚写的数据。RX 缓冲区在启动 DMA 前若被 CPU 写过,应先 Clean 再 Invalidate。
  • DMA 描述符/链表也要注意:如果使用带描述符的 DMA,描述符本身也需放在非缓存区或手动 Clean。
  • 多缓冲区(Double Buffer):每个缓冲区独立 Clean/Invalidate,不要图省事对整个大区域操作。
  • 性能权衡:频繁 Clean/Invalidate 有开销。对高吞吐场景,优先用 MPU 配置 Non-Cacheable 区域,从根源避免一致性问题。
  • 调试陷阱:在调试器下查看内存可能触发 Cache 行为异常,建议用 __DSB() 确保操作完成后再观察。

小结

STM32H7 的 Cache 与 DMA 一致性并不复杂,关键是牢记 “CPU 写→Clean,CPU 读→Invalidate”,并保证地址对齐。若项目对性能要求不极致,直接用 MPU 把 DMA 缓冲区设为 Non-Cacheable 是最省心的方案。掌握这两招,H7 的 DMA 就能稳定跑起来。