STM32H7 的 D-Cache 与 DMA 数据一致性:地址对齐、Clean/Invalidate 时机与实战踩坑

STM32H7 搭载 Cortex-M7,主频高达 480MHz,配有 16KB D-Cache 和 16KB I-Cache。D-Cache 能显著提升数据访问速度,但一旦与 DMA 配合,稍不注意就会遇到“数据错乱”“DMA 传输不完整”等诡异问题。本文从原理到实战,帮你彻底搞懂 D-Cache 与 DMA 的数据一致性。

一、为什么 D-Cache 会让 DMA 出错?

Cortex-M7 的 D-Cache 是**写回(Write-Back)+ 写分配(Write-Allocate)**策略。CPU 写数据时,若命中 Cache,只更新 Cache 行,不立即写回 SRAM;CPU 读数据时,若命中则直接读 Cache,不访问 SRAM。

而 DMA 是直接访问 SRAM的,它看不到 Cache 里的内容。于是出现两种典型错误:

  • CPU 写 → DMA 读:CPU 写了新数据到 Cache,但未写回 SRAM,DMA 从 SRAM 读到的是旧数据。
  • DMA 写 → CPU 读:DMA 把新数据写入 SRAM,但 CPU 读的是 Cache 中的旧数据(Cache 未失效)。

解决思路很简单:在 DMA 传输前后,对相关内存区域执行 Clean(写回)或 Invalidate(失效)操作。

二、地址对齐:必须遵守的硬性规则

Cortex-M7 的 Cache 行大小为 32 字节。Clean/Invalidate 操作以 Cache 行为单位,因此 DMA 缓冲区地址和长度必须 32 字节对齐,否则会误伤相邻数据。

  • 缓冲区起始地址:__attribute__((aligned(32))) 或 ALIGN_32BYTES。
  • 缓冲区长度:建议为 32 的整数倍,若不是,需手动补齐或使用 SCB_InvalidateDCache_by_Addr 时注意边界。
  • 禁止跨 Cache 行共享:不要将 DMA 缓冲区与其他变量放在同一 32 字节区域内。

示例:

// 正确:32 字节对齐
__attribute__((aligned(32))) uint8_t dma_rx_buf[256];

// 错误:可能未对齐,导致 Clean/Invalidate 越界
uint8_t dma_tx_buf[100];

三、Clean 与 Invalidate 的时机

记住一个口诀:“CPU 写、DMA 读 → Clean;DMA 写、CPU 读 → Invalidate”。

1. CPU 写数据,DMA 发送(如 UART TX、SPI TX)

  • 步骤:CPU 填充缓冲区 → Clean(将 Cache 写回 SRAM)→ 启动 DMA。
  • 注意:Clean 后不要立即修改缓冲区,否则又会产生脏数据。

2. DMA 接收数据,CPU 读(如 UART RX、ADC)

  • 步骤:启动 DMA 前 Invalidate(丢弃 Cache 旧数据)→ DMA 传输完成 → 再次 Invalidate(确保 CPU 读到最新 SRAM 数据)。
  • 注意:Invalidate 会丢弃未写回的数据,若缓冲区在 DMA 期间被 CPU 写过,必须先 Clean。

3. 双向传输(如 SPI 全双工)

  • 发送缓冲区:Clean;接收缓冲区:Invalidate。
  • 顺序:先 Invalidate 接收区,再 Clean 发送区,最后启动 DMA。

四、完整代码示例(基于 HAL 库)

以下以 UART DMA 接收为例,展示正确流程。

#include "stm32h7xx_hal.h"

#define RX_BUF_SIZE  256
__attribute__((aligned(32))) uint8_t rx_buf[RX_BUF_SIZE];

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;

void uart_dma_init(void)
{
    // 初始化 UART 和 DMA(略)
    // 启动 DMA 接收前,先 Invalidate 接收缓冲区
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_BUF_SIZE);
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 传输完成后再次 Invalidate,确保 CPU 读到最新数据
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_BUF_SIZE);
        // 此时可以安全处理 rx_buf 中的数据
        process_data(rx_buf, RX_BUF_SIZE);
    }
}

// 发送示例
__attribute__((aligned(32))) uint8_t tx_buf[128];

void uart_send_dma(uint8_t *data, uint16_t len)
{
    memcpy(tx_buf, data, len);
    // 发送前 Clean,将 Cache 写回 SRAM
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
    HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}

五、实战踩坑与注意事项

  • 坑 1:忘记对齐。缓冲区未 32 字节对齐,Clean/Invalidate 会破坏相邻变量,导致程序跑飞。务必使用 aligned(32)。
  • 坑 2:Invalidate 前未 Clean。若 CPU 在 DMA 期间修改了缓冲区,Invalidate 会丢弃这些修改。正确做法:先 Clean 再 Invalidate,或避免 CPU 与 DMA 同时访问同一区域。
  • 坑 3:DMA 传输中访问缓冲区。DMA 工作时,CPU 不应读写该缓冲区,否则数据竞争。若必须访问,请使用双缓冲或暂停 DMA。
  • 坑 4:MPU 配置不当。若将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable,可省去 Clean/Invalidate,但会降低 CPU 性能。推荐对 DMA 区域使用 Non-Cacheable 或 Write-Through 策略,简化软件设计。
  • 坑 5:中断中调用 Clean/Invalidate。这些操作可能耗时,建议在 DMA 完成中断中只做必要处理,或使用 SCB_InvalidateDCache_by_Addr 的异步版本(若支持)。
  • 坑 6:多缓冲区共享 Cache 行。两个 DMA 缓冲区若位于同一 32 字节行,Clean 一个会影响另一个。确保每个缓冲区独占 Cache 行。

六、总结

D-Cache 与 DMA 的数据一致性是 STM32H7 开发的必修课。核心原则:对齐、Clean/Invalidate 时机正确、避免竞争。理解 Cache 行和写回机制后,你就能写出既高效又稳定的代码。若项目对性能要求不高,也可将 DMA 区域配置为 Non-Cacheable,一劳永逸。希望本文能帮你少走弯路,顺利驾驭 H7 的强大性能。