STM32H7 的 D-Cache 与 DMA 数据一致性:Clean/Invalidate 的误用场景与实测踩坑记录

STM32H7 系列凭借 Cortex-M7 内核和高达 480MHz 的主频,成为高性能嵌入式应用的首选。然而,启用 D-Cache 后,DMA 传输的数据一致性成为许多开发者的噩梦。本文将深入探讨 D-Cache 与 DMA 的协作机制,分析 Clean/Invalidate 的误用场景,并提供完整的解决方案和实测经验。

1. 原理:为什么 D-Cache 会与 DMA 冲突?

Cortex-M7 的 D-Cache 是写回(Write-Back)、写分配(Write-Allocate)的。CPU 访问内存时,数据会被缓存到 D-Cache 中,而 DMA 控制器直接访问物理内存(SRAM/SDRAM),不经过 Cache。因此:

  • CPU 写数据到内存:数据可能只停留在 Cache 中,未写入物理内存。此时启动 DMA 发送,DMA 读到的是旧数据。
  • DMA 写数据到内存:DMA 将新数据写入物理内存,但 CPU 可能仍从 Cache 中读取旧数据。

解决方法是使用 Cache 维护操作:

  • Clean(清理):将 Cache 中的脏数据写回物理内存。
  • Invalidate(无效化):将 Cache 中的对应行标记为无效,强制 CPU 下次读取时从物理内存加载。

2. 误用场景与正确做法

2.1 场景一:CPU 写 → DMA 读(发送)

错误做法:写完数据后直接启动 DMA,未执行 Clean。

正确做法:在启动 DMA 前,对数据缓冲区执行 Clean 操作。

// 假设 buffer 是 CPU 写入的数据,准备通过 DMA 发送
SCB_CleanDCache_by_Addr((uint32_t *)buffer, sizeof(buffer));
// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, buffer, sizeof(buffer));

2.2 场景二:DMA 写 → CPU 读(接收)

错误做法:DMA 接收完成后直接读取数据,未执行 Invalidate。

正确做法:在 DMA 接收完成中断中,先 Invalidate 对应缓冲区,再读取数据。

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 无效化 Cache,确保 CPU 读取到 DMA 写入的新数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, sizeof(rx_buffer));
    // 处理数据
    process_data(rx_buffer);
}

2.3 场景三:双向传输(如 SPI 全双工)

错误做法:只做 Clean 或只做 Invalidate。

正确做法:发送前 Clean TX 缓冲区,接收后 Invalidate RX 缓冲区。

SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, sizeof(tx_buffer));
HAL_SPI_TransmitReceive_DMA(&hspi1, tx_buffer, rx_buffer, size);
// 在完成回调中
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, sizeof(rx_buffer));

3. 配置步骤与完整代码示例

3.1 启用 D-Cache

在 main() 初始化阶段启用 D-Cache:

void Enable_DCache(void)
{
    SCB_EnableICache();  // 启用指令 Cache
    SCB_EnableDCache();  // 启用数据 Cache
}

3.2 配置 MPU 保护 DMA 缓冲区(可选但推荐)

为避免手动维护 Cache,可将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable。

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);
}

3.3 完整 DMA 发送示例(UART)

#define BUFFER_SIZE 128
uint8_t tx_buffer[BUFFER_SIZE] __attribute__((aligned(32)));  // 32字节对齐

void Send_Data_DMA(void)
{
    // 填充数据
    for (int i = 0; i < BUFFER_SIZE; i++) {
        tx_buffer[i] = i;
    }
    // Clean D-Cache
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, BUFFER_SIZE);
    // 启动 DMA 发送
    HAL_UART_Transmit_DMA(&huart1, tx_buffer, BUFFER_SIZE);
}

3.4 完整 DMA 接收示例(UART)

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

void Start_Receive_DMA(void)
{
    // 启动 DMA 接收
    HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // Invalidate D-Cache
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, BUFFER_SIZE);
    // 处理数据
    Process_Data(rx_buffer);
    // 重新启动接收
    HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
}

4. 注意事项与踩坑记录

  • 地址对齐:SCB_CleanDCache_by_Addr 和 SCB_InvalidateDCache_by_Addr 要求地址 32 字节对齐,长度也建议为 32 的倍数。否则可能误操作相邻数据。
  • Invalidate 的风险:如果缓冲区中有 CPU 尚未写回的数据,Invalidate 会丢弃这些数据。因此,对于 DMA 接收缓冲区,应确保在 Invalidate 前 CPU 没有写入操作。
  • DMA 描述符:如果使用 DMA 双缓冲或链表模式,每个缓冲区都需要单独维护 Cache。
  • 性能影响:频繁的 Clean/Invalidate 会降低性能,建议将 DMA 缓冲区放在 Non-Cacheable 区域(通过 MPU 配置)。
  • 实测踩坑:在 STM32H743 上,曾遇到 UART DMA 发送数据错乱,原因是 tx_buffer 未对齐,Clean 操作影响了相邻变量。对齐后问题解决。
  • 调试技巧:使用 SCB_InvalidateDCache() 全局无效化可快速验证是否为 Cache 问题,但会严重影响性能,仅用于调试。

5. 总结

D-Cache 与 DMA 的数据一致性是 STM32H7 开发中的关键点。正确使用 Clean 和 Invalidate,或通过 MPU 配置 Non-Cacheable 区域,可以确保数据传输可靠。牢记:CPU 写后 DMA 读要 Clean,DMA 写后 CPU 读要 Invalidate。希望本文的踩坑记录能帮助你少走弯路。