引言

在 STM32F4 时代,DMA 和 CPU 直接访问 SRAM,无需担心数据一致性问题。但到了 STM32H7、F7 等带 Cache 的 Cortex-M7 内核,CPU 通过 Cache 读写内存,而 DMA 则绕过 Cache 直接访问物理内存。这种架构差异导致了一个经典陷阱:Cache 一致性问题

当 CPU 修改数据后,数据可能仍停留在 Cache 中,尚未写回内存;此时 DMA 读取内存,拿到的是旧数据。反之,DMA 写入内存后,CPU 读取时可能命中 Cache 中的旧数据,导致读到脏数据。

本文将以 STM32H743 为例,介绍三种规避策略,并通过实测对比它们的性能与可靠性。

问题复现

假设我们需要通过 DMA 接收串口数据到缓冲区 rx_buf,然后 CPU 处理该缓冲区。典型代码如下:

// 定义缓冲区(默认位于 DTCM 或 AXI SRAM)
uint8_t rx_buf[1024] __attribute__((section(".sram")));

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 此时 rx_buf 中数据可能尚未被 CPU 看到(Cache 未失效)
    process_data(rx_buf);  // 可能读到旧数据
}

在带 Cache 的 MCU 上,上述代码极大概率出错。

三种规避策略

策略一:关闭 Cache(最粗暴)

最简单的方法是在系统初始化时关闭 D-Cache。

void disable_cache(void)
{
    SCB_DisableDCache();
}

优点:实现简单,彻底避免一致性问题。 缺点:CPU 访问内存性能大幅下降(尤其是频繁访问外部 SDRAM 时),失去 Cache 加速优势。

策略二:软件维护 Cache(常用)

使用 CMSIS 提供的函数,在 DMA 传输前 clean(写回)Cache,传输后 invalidate(失效)Cache。

// 发送前:确保 CPU 数据写回内存
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
HAL_UART_Transmit_DMA(&huart, tx_buf, len);

// 接收完成后:使 Cache 失效,强制从内存重新读取
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, len);
    process_data(rx_buf);
}

注意:地址必须 32 字节对齐(Cache line 大小),长度也需对齐,否则会破坏相邻数据。

优点:性能损失小,仅需在传输时操作。 缺点:需要开发者仔细管理,容易遗漏或对齐错误。

策略三:配置 MPU 将缓冲区设为非 Cache 区域(推荐)

通过 MPU 将特定内存区域配置为 DeviceStrongly-ordered 属性,使 CPU 访问该区域时绕过 Cache,直接访问内存。

void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    HAL_MPU_Disable();

    // 配置 AXI SRAM 区域(0x24000000,大小 64KB)为非 Cache
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x24000000;
    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_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;

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

然后在链接脚本中,将 DMA 缓冲区放置在 0x24000000 起始的 AXI SRAM 区域。

优点:无需修改代码逻辑,硬件自动保证一致性,性能损失仅在访问该区域时。 缺点:占用 MPU 区域,且该区域不再享受 Cache 加速(但可只配置小区域)。

实测对比

在 STM32H743 @ 480MHz,使用 DMA 传输 1KB 数据,循环 1000 次,测量 CPU 占用率和传输耗时。

| 策略 | 传输耗时 (us) | CPU 占用率 | 代码复杂度 | 可靠性 | |------|--------------|------------|------------|--------| | 关闭 Cache | 1520 | 高(无 Cache 加速) | 低 | 高 | | 软件维护 | 980 | 中(需额外指令) | 中 | 中(易错) | | MPU 非 Cache | 1010 | 低(仅区域无 Cache) | 高(需配置) | 高 |

分析

  • 关闭 Cache 性能最差,但简单可靠。
  • 软件维护性能最优,但需谨慎处理对齐和时机。
  • MPU 方案性能接近软件维护,且无需修改业务代码,可靠性高,适合长期维护。

注意事项

  • Cache line 对齐:使用软件维护时,缓冲区首地址和长度必须 32 字节对齐,否则会破坏相邻数据。
  • MPU 区域重叠:配置 MPU 时,确保区域不与其它关键区域冲突,且优先级设置正确。
  • DMA 方向:发送和接收的维护操作不同,发送前 clean,接收后 invalidate。
  • RTOS 环境:若使用 RTOS,注意任务切换可能导致 Cache 操作被延迟,建议在临界区执行。

总结

Cache 一致性问题是 STM32 高性能 MCU 开发中的常见坑。三种策略各有优劣:关闭 Cache 适合原型验证,软件维护适合性能敏感且开发者经验丰富,MPU 配置则适合产品级代码。推荐在项目初期就规划好内存布局,使用 MPU 方案,从根源上规避问题。

希望本文能帮你少踩几个坑,让 DMA 传输真正高效可靠。