STM32H7 的 D-Cache 与 DMA 一致性维护:按 cache line 对齐的收发缓冲区设计

STM32H7 系列凭借 Cortex-M7 内核、480MHz 主频和 16KB D-Cache,成为高性能嵌入式应用的首选。然而,开启 D-Cache 后,DMA 与 CPU 之间的数据一致性问题会频繁导致“数据错乱”“接收丢包”等诡异现象。本文从原理到实践,带你彻底解决这一痛点。

一、为什么 DMA 与 D-Cache 会“打架”?

Cortex-M7 的 D-Cache 是写回(write-back)+ 写分配(write-allocate) 策略。CPU 访问内存时,数据先被加载到 Cache,修改后不会立即写回主存。而 DMA 直接访问物理内存(SRAM),不经过 Cache。

  • 发送方向:CPU 写好缓冲区 → 数据可能还在 Cache 中 → DMA 从 SRAM 读到旧数据。
  • 接收方向:DMA 把新数据写入 SRAM → CPU 读到的却是 Cache 中的旧数据。

解决思路只有两条:要么让 DMA 也走 Cache(不可行),要么在 DMA 传输前后手动维护 Cache 一致性。

二、Cache Line 对齐:一切维护的前提

Cortex-M7 的 D-Cache 行大小为 32 字节。SCB_CleanDCache_by_Addr 和 SCB_InvalidateDCache_by_Addr 等函数按地址范围操作,但硬件实际以 cache line 为单位。

如果缓冲区起始地址或长度不是 32 字节对齐,维护操作可能“误伤”相邻数据,导致其他变量被清空或写回。因此,DMA 缓冲区必须按 32 字节对齐,且长度建议为 32 的整数倍。

// 推荐:使用编译器属性强制对齐
#define DMA_BUF_SIZE  256
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];

若使用动态分配,可用 memalign(32, size) 或 aligned_alloc(32, size)。

三、发送与接收的 Cache 维护流程

发送(CPU → DMA)

  1. CPU 填充缓冲区。
  2. Clean(写回)缓冲区,确保数据到达 SRAM。
  3. 启动 DMA 发送。
  4. 等待传输完成。

接收(DMA → CPU)

  1. Invalidate(无效化)缓冲区,丢弃 Cache 中的旧数据。
  2. 启动 DMA 接收。
  3. 等待传输完成。
  4. 再次 Invalidate,确保 CPU 读取到 DMA 写入的新数据。

注意:Invalidate 操作会丢弃未写回的数据,因此接收缓冲区在 Invalidate 前不应有 CPU 未写回的内容。

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

以下代码以 UART DMA 收发为例,展示完整的维护流程。

#include "stm32h7xx_hal.h"

#define DMA_BUF_SIZE  256

__attribute__((aligned(32))) uint8_t tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[DMA_BUF_SIZE];

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;

// 发送函数
void uart_dma_send(uint8_t *data, uint16_t len) {
    // 1. 拷贝数据到对齐缓冲区(若 data 未对齐)
    memcpy(tx_buf, data, len);

    // 2. Clean D-Cache,确保数据写回 SRAM
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);

    // 3. 启动 DMA 发送
    HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}

// 接收启动函数
void uart_dma_start_receive(uint16_t len) {
    // 1. Invalidate D-Cache,丢弃旧数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, len);

    // 2. 启动 DMA 接收
    HAL_UART_Receive_DMA(&huart1, rx_buf, len);
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        // 再次 Invalidate,确保 CPU 读到新数据
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, DMA_BUF_SIZE);

        // 处理数据...
        process_data(rx_buf, DMA_BUF_SIZE);

        // 重新启动接收
        uart_dma_start_receive(DMA_BUF_SIZE);
    }
}

五、关键注意事项与常见陷阱

  • 对齐是硬性要求:缓冲区地址和长度都应为 32 字节的整数倍。若长度不是 32 的倍数,维护操作会覆盖相邻内存。
  • Clean 与 Invalidate 的顺序:发送前 Clean,接收前 Invalidate,接收后再次 Invalidate。顺序错误会导致数据丢失或读到旧值。
  • 避免在中断中频繁维护:Cache 维护操作有一定开销,高频率中断中应谨慎使用。
  • MPU 配置:可将 DMA 缓冲区所在内存区域配置为 Write-Through 或 Non-Cacheable,从根本上避免一致性问题,但会牺牲部分性能。
  • 调试技巧:若怀疑 Cache 问题,可临时关闭 D-Cache 验证;使用 SCB_InvalidateDCache() 全局无效化(慎用)辅助定位。
  • 多缓冲区场景:若使用多个缓冲区,每个缓冲区都应独立对齐,并分别维护。

六、总结

STM32H7 的 D-Cache 与 DMA 一致性维护并不复杂,核心就是 32 字节对齐 + 正确的 Clean/Invalidate 时序。按照本文的缓冲区设计模式,你可以安全地在 H7 上发挥 DMA 与 Cache 的全部性能。记住:对齐是基础,时序是关键,测试是保障。

延伸阅读:STM32H7 参考手册中关于 Cortex-M7 Cache 的章节,以及 ARM 官方 SCB_CleanDCache_by_Addr 等 CMSIS 函数说明。