STM32H7 D-Cache与DMA一致性实战:用MPU精准配置回写区间,告别全片Clean/Invalidate

1. 为什么D-Cache会让DMA“失灵”?

STM32H7主频高达480MHz,为了匹配CPU速度,芯片内置了D-Cache。CPU访问数据时,会先将数据从SRAM加载到Cache,后续读写都直接操作Cache,而不是SRAM。

DMA则不同,它直接访问SRAM,不经过Cache。这就导致了经典的一致性问题:

  • CPU写,DMA读:CPU写入的数据可能还在Cache中(Write-Back模式),SRAM里还是旧数据。DMA从SRAM读,拿到的是旧数据。
  • DMA写,CPU读:DMA将新数据写入SRAM,但CPU的Cache中可能还保留着该地址的旧数据。CPU读时直接命中Cache,读到旧数据。

解决思路有两种:

  1. 全片Clean/Invalidate:简单粗暴,但每次DMA传输都操作整个Cache,耗时且可能影响其他任务的数据。
  2. MPU配置特定区域为Write-Through或Non-Cacheable:精准控制,只对DMA缓冲区生效,效率高。

本文重点介绍第二种方法,并给出可落地的代码。

2. MPU配置:将DMA缓冲区设为回写区间

MPU(Memory Protection Unit)可以定义内存区域的属性。对于DMA缓冲区,我们通常有两种选择:

  • Non-Cacheable:CPU访问不经过Cache,彻底避免一致性问题,但会降低CPU访问速度。
  • Write-Through:CPU写操作同时更新Cache和SRAM,DMA读能拿到最新数据;但DMA写后,CPU读仍可能命中旧Cache,需要Invalidate。

更推荐的做法是:将DMA缓冲区配置为Write-Back,但在DMA传输前后手动执行Clean/Invalidate,且只针对该缓冲区。这样兼顾性能与安全。

下面以STM32H7为例,使用HAL库配置MPU。

2.1 定义MPU区域

假设我们使用0x30000000开始的SRAM区域(D2域SRAM1~3)作为DMA缓冲区,大小32KB。

#include "stm32h7xx_hal.h"

#define DMA_BUFFER_ADDR   0x30000000
#define DMA_BUFFER_SIZE   0x8000  // 32KB

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置DMA缓冲区区域:Write-Back, Read/Write allocate
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = DMA_BUFFER_ADDR;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_32KB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_BUFFERABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 其他区域保持默认(通常为Write-Back)
    // ...

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

关键点:

  • IsCacheable = MPU_ACCESS_CACHEABLEIsBufferable = MPU_ACCESS_BUFFERABLE 组合为Write-Back。
  • TypeExtField = MPU_TEX_LEVEL0 表示普通内存属性。
  • 区域大小根据实际缓冲区调整,务必对齐。

2.2 使能Cache

main()中初始化后使能D-Cache:

SCB_EnableICache();
SCB_EnableDCache();

3. DMA传输前后的精准Cache维护

配置好MPU后,DMA缓冲区仍可能被缓存。因此,在DMA传输前后必须执行Clean/Invalidate,但只针对该缓冲区。

3.1 CPU写 → DMA读(发送)

CPU填充缓冲区后,需要将Cache中的数据写回SRAM,即Clean操作。

void DMA_SendData(uint8_t *src, uint32_t len)
{
    // 确保数据在SRAM中
    SCB_CleanDCache_by_Addr((uint32_t *)src, len);

    // 启动DMA传输
    HAL_DMA_Start(&hdma_memtomem, (uint32_t)src, (uint32_t)dest, len);
}

3.2 DMA写 → CPU读(接收)

DMA传输完成后,CPU要读取新数据,必须先Invalidate对应Cache行,否则可能读到旧数据。

void DMA_ReceiveData(uint8_t *dst, uint32_t len)
{
    // 启动DMA接收
    HAL_DMA_Start(&hdma_memtomem, (uint32_t)src, (uint32_t)dst, len);

    // 等待传输完成(中断或轮询)
    // ...

    // 无效化Cache,确保CPU读取的是SRAM中的新数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)dst, len);
}

注意:SCB_InvalidateDCache_by_Addr的地址必须32字节对齐,长度建议为32的倍数。如果缓冲区未对齐,需先对齐处理。

4. 完整示例:DMA双缓冲串口接收

以UART空闲中断+DMA接收为例,展示如何安全使用Cache。

#define RX_BUFFER_SIZE 256
__attribute__((aligned(32))) uint8_t rxBuffer[RX_BUFFER_SIZE];

void UART_DMA_Init(void)
{
    // 配置DMA为循环模式,接收串口数据
    HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_BUFFER_SIZE);
}

// 空闲中断回调
void HAL_UART_IdleCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 停止DMA获取已接收长度
        HAL_UART_DMAStop(huart);
        uint16_t len = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx);

        // 无效化Cache,确保读取到最新数据
        SCB_InvalidateDCache_by_Addr((uint32_t *)rxBuffer, len);

        // 处理数据
        ProcessData(rxBuffer, len);

        // 重新启动DMA
        HAL_UART_Receive_DMA(huart, rxBuffer, RX_BUFFER_SIZE);
    }
}

5. 注意事项与避坑指南

  • 地址对齐:Cache操作要求地址32字节对齐,长度也应为32的倍数。使用__attribute__((aligned(32)))定义缓冲区。
  • 避免全片操作SCB_CleanDCache()SCB_InvalidateDCache()会影响整个Cache,可能导致其他任务数据丢失,务必使用_by_Addr版本。
  • DMA传输期间禁止CPU访问:如果CPU在DMA传输过程中访问了缓冲区,可能引发Cache与SRAM不一致。建议使用双缓冲或信号量保护。
  • MPU配置顺序:先配置MPU,再使能Cache。修改MPU后需要重新使能Cache。
  • 性能权衡:Non-Cacheable区域CPU访问慢,但无需维护;Write-Back性能高,但需手动维护。根据数据量选择。
  • 调试技巧:如果发现数据错乱,可临时将缓冲区设为Non-Cacheable验证是否为一致性问题。

6. 总结

STM32H7的D-Cache与DMA一致性并非无解。通过MPU将DMA缓冲区配置为Write-Back,并在传输前后精准执行Clean/Invalidate,既能发挥Cache的性能优势,又能保证数据正确。记住三个关键:对齐、按地址操作、避免全片。掌握这些,你就能在H7上放心使用DMA了。