STM32H7实战:AXI SRAM与TCM间DMA传输的缓存一致性处理

一、为什么需要关注缓存一致性?

STM32H7系列(如H743、H750)内置了强大的存储系统:

  • TCM(Tightly Coupled Memory):ITCM和DTCM,零等待访问,但不支持DMA
  • AXI SRAM:主存储区,支持DMA,但通过AXI总线连接,默认启用L1-Cache
  • L1-Cache:CPU核心的缓存,分为I-Cache和D-Cache,用于加速对AXI SRAM的访问。

当CPU通过D-Cache写入AXI SRAM,而DMA外设直接读写AXI SRAM时,双方看到的数据可能不一致。例如:

  • CPU写数据到AXI SRAM(数据暂存在Cache中),DMA读取时可能读到旧数据。
  • DMA写入新数据到AXI SRAM,CPU读取时可能命中Cache中的旧数据。

这种不一致会导致通信错误、图像花屏、控制失灵等严重问题。

二、硬件架构与问题根源

STM32H7的存储映射简化图如下:

+----------------+     +----------------+
|     CPU        |     |   DMA控制器    |
|  +----------+  |     |                |
|  | L1-Cache|  |     |                |
|  +----+-----+  |     |                |
+-------|--------+     +--------|-------+
        |                       |
        v                       v
   +----+----+             +----+----+
   | AXI SRAM|             | AXI SRAM|
   +---------+             +---------+
  • CPU路径:CPU访问AXI SRAM时,先经过L1-Cache,Cache命中则直接返回,不访问物理SRAM。
  • DMA路径:DMA是总线主设备,直接访问AXI SRAM,不经过CPU的Cache。

因此,当CPU和DMA并发访问同一内存区域时,必须手动维护Cache的一致性。

三、解决方案实战

方案1:Cache清理与失效(最直接)

在DMA传输前,CPU需将Cache中的数据写回SRAM(Clean);传输完成后,使Cache失效,强制从SRAM重新加载。

配置步骤:

  1. 使能D-Cache(默认在SystemInit中已使能)。
  2. 使用CMSIS提供的函数:SCB_CleanDCache()SCB_InvalidateDCache()
  3. 确保缓冲区地址按32字节对齐(Cache line大小)。

代码示例:

// 缓冲区定义(注意对齐)
__ALIGNED(32) uint8_t tx_buffer[1024];
__ALIGNED(32) uint8_t rx_buffer[1024];

void DMA_Transfer(void)
{
    // 填充tx_buffer
    memcpy(tx_buffer, "Hello DMA", 10);

    // 1. 清理Cache,确保数据写入SRAM
    SCB_CleanDCache();

    // 2. 启动DMA传输(假设使用DMA2D或DMA1)
    HAL_DMA_Start(&hdma, (uint32_t)tx_buffer, (uint32_t)rx_buffer, 1024);
    HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, 1000);

    // 3. 使Cache失效,让CPU读取SRAM中的最新数据
    SCB_InvalidateDCache();

    // 现在rx_buffer中的数据是有效的
}

注意: 如果缓冲区较大,建议使用区域操作函数提高效率:

  • SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize)
  • SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize)

方案2:MPU配置(将区域设为非缓存)

通过MPU将AXI SRAM的特定区域配置为非缓存(Non-cacheable),这样CPU访问该区域时直接操作SRAM,无需手动维护。

配置步骤:

  1. 初始化MPU,设置区域属性。
  2. 使能MPU。

代码示例:

void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 禁用MPU进行配置
    HAL_MPU_Disable();

    // 配置区域:例如,将0x24000000开始的64KB设为非缓存
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x24000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_REGION_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_REGION_NOT_CACHEABLE;  // 关键:非缓存
    MPU_InitStruct.IsShareable = MPU_REGION_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);

    // 使能MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

优点: 无需每次传输都手动清理Cache,适合高频DMA场景。 缺点: 牺牲了该区域的缓存性能,适用于对实时性要求高但数据量不大的场景。

方案3:使用非缓存DMA缓冲区(推荐)

在链接脚本中定义独立的非缓存段,将DMA缓冲区放置其中,彻底避免一致性问题。

配置步骤:

  1. 修改链接脚本(.ld文件),添加非缓存段。
  2. 在代码中声明缓冲区属性。

链接脚本示例(GCC):

/* 在SECTIONS段中添加 */
.nocache (NOLOAD) :
{
    . = ALIGN(32);
    *(.nocache)
    . = ALIGN(32);
} >RAM_D1

代码声明:

__attribute__((section(".nocache"))) uint8_t dma_buffer[1024];

优点: 无需MPU配置,也无需手动Cache操作,性能最佳。 缺点: 需要修改链接脚本,对新手有一定门槛。

四、完整实战示例(结合UART DMA)

以下示例演示使用方案1(Cache清理/失效)实现UART DMA接收不定长数据。

#include "stm32h7xx_hal.h"

#define RX_BUF_SIZE 256
__ALIGNED(32) uint8_t rx_buffer[RX_BUF_SIZE];
volatile uint8_t rx_complete = 0;

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if (huart->Instance == USART1) {
        // DMA接收完成,使Cache失效,确保读取最新数据
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, Size);
        rx_complete = 1;
    }
}

void UART_DMA_Init(void)
{
    // ... UART和DMA初始化代码(略)

    // 启动DMA接收(空闲中断模式)
    HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, RX_BUF_SIZE);
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_DMA_Init();
    MX_USART1_UART_Init();

    // 使能D-Cache(如果未在SystemInit中使能)
    SCB_EnableDCache();

    UART_DMA_Init();

    while (1) {
        if (rx_complete) {
            // 处理rx_buffer中的数据
            rx_complete = 0;
        }
    }
}

五、注意事项与最佳实践

  • 缓冲区对齐:DMA缓冲区必须32字节对齐,否则Cache操作可能失败或效率低下。
  • 区域操作:尽量使用SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr,避免全缓存操作影响性能。
  • MPU配置:如果使用MPU,确保区域大小和基地址正确,且不与其他配置冲突。
  • 双核场景:在H745/H755等双核芯片中,两个CPU共享AXI SRAM,还需考虑核间通信的缓存一致性,建议使用非缓存共享内存。
  • 调试技巧:使用硬件调试器观察Cache和SRAM内容,确认一致性操作是否生效。

六、总结

缓存一致性是STM32H7高性能开发的必修课。通过本文的三种方案,你可以根据项目需求选择最合适的方法:

  • 方案1:简单直接,适合低频传输。
  • 方案2:MPU配置,适合需要平衡性能与一致性的场景。
  • 方案3:非缓存段,适合高频、大数据量传输,是工业级推荐做法。

掌握这些技巧,你的嵌入式系统将更加健壮,远离数据错乱的噩梦。