STM32H7 的 D-Cache 与 DMA 一致性:地址对齐、Clean/Invalidate 时序与实测踩坑

STM32H7 系列凭借 Cortex-M7 内核和高达 480MHz 的主频,成为高性能嵌入式应用的首选。然而,启用 D-Cache 后,DMA 传输的数据往往与 CPU 看到的不一致,导致“数据错乱”、“DMA 不工作”等诡异问题。本文将从原理出发,结合地址对齐、Clean/Invalidate 时序,给出完整解决方案和实测经验。

一、为什么 DMA 与 D-Cache 会冲突?

Cortex-M7 的 D-Cache 是写回(Write-Back) 策略:CPU 写数据时只更新 Cache,不立即写回内存;读数据时若 Cache 命中则直接返回,不访问内存。而 DMA 直接访问物理内存,不经过 Cache。因此:

  • CPU 写 → DMA 读:数据可能还在 Cache 中,DMA 读到旧内存值。
  • DMA 写 → CPU 读:DMA 更新了内存,但 CPU 读到的是 Cache 中的旧数据。

解决思路:在 DMA 传输前后,手动维护 Cache 一致性。

二、地址对齐:32 字节边界是硬性要求

D-Cache 的 Cache Line 为 32 字节。Clean/Invalidate 操作以 Cache Line 为单位,若缓冲区地址或长度未对齐,会误伤相邻数据。

规则:

  • DMA 缓冲区起始地址必须 32 字节对齐。
  • 缓冲区长度最好是 32 的整数倍。
  • 若无法对齐,需扩展操作范围至整个 Cache Line,并确保相邻数据不被意外修改。
// 推荐:使用对齐属性定义缓冲区
__attribute__((aligned(32))) uint8_t dma_buffer[1024];

// 或使用 C11 标准
#include <stdalign.h>
alignas(32) uint8_t dma_buffer[1024];

三、Clean 与 Invalidate 的时序

  • Clean:将 Cache 中已修改的数据写回内存。用于 CPU 写 → DMA 读 场景。
  • Invalidate:丢弃 Cache 中的数据,强制下次读取从内存获取。用于 DMA 写 → CPU 读 场景。
  • Clean+Invalidate:先写回再丢弃,用于双向传输或不确定场景。

关键时序:

  1. CPU 准备数据 → Clean → 启动 DMA 发送。
  2. 启动 DMA 接收 → DMA 完成 → Invalidate → CPU 读取数据。

注意:Invalidate 必须在 DMA 完成后进行,否则可能丢弃 DMA 尚未写入的数据。

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

#include "stm32h7xx_hal.h"

#define DMA_BUF_SIZE 1024
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];

// 发送前:Clean D-Cache
void dma_send_prepare(void) {
    SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, DMA_BUF_SIZE);
}

// 接收后:Invalidate D-Cache
void dma_receive_complete(void) {
    SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, DMA_BUF_SIZE);
}

// 示例:UART DMA 发送
void uart_dma_send(UART_HandleTypeDef *huart) {
    // 填充数据
    for (int i = 0; i < DMA_BUF_SIZE; i++) dma_tx_buf[i] = i & 0xFF;
    dma_send_prepare();
    HAL_UART_Transmit_DMA(huart, dma_tx_buf, DMA_BUF_SIZE);
}

// 示例:UART DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    dma_receive_complete();
    // 此时 dma_rx_buf 中数据有效
    process_data(dma_rx_buf, DMA_BUF_SIZE);
}

五、实测踩坑与注意事项

  1. 忘记 Clean 导致发送数据错乱:CPU 写入缓冲区后立即启动 DMA,DMA 读到旧数据。务必在启动 DMA 前调用 SCB_CleanDCache_by_Addr。
  2. Invalidate 时机错误:在 DMA 传输过程中 Invalidate,可能丢弃部分已接收数据。应在 DMA 完成中断中执行。
  3. 地址未对齐引发硬件异常:SCB_InvalidateDCache_by_Addr 要求地址 32 字节对齐,否则触发 HardFault。使用 __attribute__((aligned(32))) 强制对齐。
  4. 多缓冲区共享 Cache Line:若两个缓冲区位于同一 Cache Line,Clean/Invalidate 会相互影响。确保每个缓冲区独立对齐。
  5. MPU 配置:可将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable,但会降低 CPU 性能。推荐手动维护一致性。
  6. 中断优先级:DMA 完成中断优先级应低于或等于调用 Clean/Invalidate 的上下文,避免竞态。

六、总结

STM32H7 的 D-Cache 与 DMA 一致性并非无解难题,核心在于:对齐地址、正确选择 Clean/Invalidate、严格时序。遵循本文的代码模板和注意事项,可避免绝大多数数据错乱问题。建议在项目初期就规划好 DMA 缓冲区的对齐和 Cache 维护策略,后期调试将事半功倍。