一、为什么 D-Cache 会让 DMA 数据错位?

STM32H7 的 Cortex-M7 内核带有 16KB D-Cache 和 I-Cache。D-Cache 默认关闭,一旦开启,CPU 访问内存时会将数据缓存到 Cache 中,后续读写直接命中 Cache,不再访问实际 SRAM。

而 DMA 是独立于 CPU 的总线主设备,它直接读写 SRAM,不经过 D-Cache。这就导致:

  • CPU 写数据到 Cache,但未回写到 SRAM,DMA 从 SRAM 读到的仍是旧数据;
  • DMA 将新数据写入 SRAM,但 CPU 读到的仍是 Cache 中的旧数据。

这就是典型的缓存一致性问题,表现为串口收发错位、ADC 采样值跳变、SD 卡读写失败等。

二、解决思路:MPU 配置回写区间

Cortex-M7 提供 MPU(内存保护单元),可将特定内存区域配置为不同的 Cache 策略:

  • Write-Through:CPU 写操作同时更新 Cache 和 SRAM,读操作可缓存;
  • Write-Back:CPU 写操作只更新 Cache,延迟回写,性能最高;
  • Non-Cacheable:完全不使用 Cache,性能最低但一致性最好。

对于 DMA 缓冲区,推荐使用 Write-Back + Read-Allocate 并配合手动清理/无效化操作,或者直接配置为 Non-Cacheable 简化设计。本文重点介绍 按 MPU 配置回写区间 的方法,兼顾性能与一致性。

三、配置步骤

3.1 确定 DMA 缓冲区地址

将 DMA 缓冲区放在一个独立的、按 32 字节对齐的 SRAM 区域,例如 0x30000000 开始的 64KB(D2 SRAM)。

3.2 配置 MPU 区域

使用 HAL 库配置 MPU:

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置 DMA 缓冲区区域:0x30000000,大小 64KB
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x30000000;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_64KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_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_PRIVILEGED_DEFAULT);
}

此配置将 0x30000000 区域设为 Write-Back、Write-Allocate 的 Cache 属性。

3.3 使能 D-Cache

SCB_EnableICache();
SCB_EnableDCache();

3.4 DMA 传输前后的 Cache 维护

对于 Write-Back 区域,DMA 发送前需 Clean(回写)Cache,DMA 接收后需 Invalidate(无效化)Cache。

// DMA 发送前:将 CPU 写入的数据从 Cache 回写到 SRAM
SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer));

// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buffer, sizeof(tx_buffer));

// DMA 接收完成后:无效化 Cache,确保 CPU 读取到 SRAM 中的新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));

注意:SCB_InvalidateDCache_by_Addr 的地址必须 32 字节对齐,长度建议为 32 的倍数。

四、完整示例:UART DMA 收发

#include "stm32h7xx_hal.h"

#define BUFFER_SIZE 64

// 缓冲区必须 32 字节对齐,放在 MPU 配置的区域
__attribute__((aligned(32))) uint8_t tx_buffer[BUFFER_SIZE] __attribute__((section(".dma_buffer")));
__attribute__((aligned(32))) uint8_t rx_buffer[BUFFER_SIZE] __attribute__((section(".dma_buffer")));

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;

void UART_DMA_Init(void)
{
    // 初始化代码略,重点在 Cache 维护
}

void UART_SendData(uint8_t *data, uint16_t len)
{
    memcpy(tx_buffer, data, len);
    // 回写 Cache,确保 DMA 读到最新数据
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, len);
    HAL_UART_Transmit_DMA(&huart1, tx_buffer, len);
}

void UART_ReceiveData(uint8_t *data, uint16_t len)
{
    // 无效化 Cache,确保 CPU 读到 DMA 写入的新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, len);
    memcpy(data, rx_buffer, len);
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, BUFFER_SIZE);
        // 处理数据...
    }
}

五、注意事项

  • 地址对齐:Cache 维护函数要求地址 32 字节对齐,长度建议为 32 的倍数,否则可能误伤相邻数据。
  • 避免频繁维护:每次 DMA 传输都 Clean/Invalidate 会降低性能,可考虑将小缓冲区配置为 Non-Cacheable。
  • 多缓冲区:若使用多个 DMA 缓冲区,确保它们位于同一 MPU 区域,或分别配置。
  • 中断安全:在中断中调用 Cache 维护函数时,注意不要与主循环中的 DMA 操作冲突。
  • 调试技巧:若数据错位,可先关闭 D-Cache 验证问题是否由 Cache 引起。
  • 链接脚本:使用 __attribute__((section(".dma_buffer"))) 需在链接脚本中定义该段并分配到正确地址。

六、总结

STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的常见坑点。通过 MPU 将 DMA 缓冲区配置为 Write-Back 回写区间,并在传输前后正确调用 SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr,即可在保证性能的同时避免数据错位。掌握这一机制,能让你的 H7 项目稳定运行在 480MHz 的高性能模式下。