引言

STM32H7系列搭载Cortex-M7内核,内置L1 Cache(I-Cache和D-Cache),性能强劲,但这也引入了新的挑战:当DMA(直接内存访问)与CPU共享数据缓冲区时,Cache的一致性(Coherency)问题可能导致数据错乱。很多开发者初次使用H7时,发现DMA接收的数据“不更新”或发送的数据“乱码”,根源往往在此。本文将从原理到实践,彻底解决这一陷阱。

问题根源:Cache与DMA的“各自为政”

1. Cache的工作原理

Cortex-M7的D-Cache是写回(Write-back)策略,即CPU写数据时,先写入Cache,标记为脏(Dirty),之后才异步写回主存(SRAM)。CPU读数据时,若Cache命中,直接读Cache,不再访问主存。

2. DMA的“直来直去”

DMA控制器直接访问主存(SRAM),不经过Cache。它读主存数据时,看不到Cache中的最新值;它写主存数据时,也不会更新Cache。

3. 冲突场景

  • CPU写,DMA读:CPU更新缓冲区(数据在Cache中),DMA读取主存,得到的是旧数据(因为Cache未写回)。
  • DMA写,CPU读:DMA将新数据写入主存,CPU读时,Cache命中旧数据,得到的是陈旧数据。

这就是“一致性陷阱”。

解决方案概览

针对上述问题,有三种主流方案,各有适用场景:

  1. 关闭D-Cache:简单粗暴,但牺牲性能。
  2. 配置MPU区域为不可缓存:针对特定内存区域,关闭Cache,保持其他区域性能。
  3. 软件维护Cache:在关键操作前后,手动执行Clean(写回)和Invalidate(失效)操作。

下面逐一详解。

方案一:关闭D-Cache(不推荐)

原理

直接禁用D-Cache,所有读写都直接访问主存,DMA与CPU自然一致。

配置步骤

在系统初始化代码中,注释或删除使能Cache的代码。例如,在main.c中:

// 默认HAL库生成的代码会调用以下函数
// SCB_EnableDCache();  // 注释掉这行

注意事项

  • 性能损失明显,尤其对内存密集型任务。
  • 仅适合调试或对性能无要求的场景。

方案二:MPU配置不可缓存区域(推荐)

原理

通过MPU(内存保护单元)将DMA使用的缓冲区所在内存区域配置为“不可缓存”或“写透”(Write-through),这样CPU读写该区域时直接访问主存,避免不一致。

配置步骤

  1. 定义缓冲区:放在特定内存段,例如__attribute__((section(".ARM.__at_0x24000000")))(AXI SRAM)。
  2. 初始化MPU:在系统初始化时,配置MPU区域。
// 示例:配置0x24000000开始的64KB为不可缓存
void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

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

    // 配置区域0
    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_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;  // 关键:不可缓存
    MPU_InitStruct.IsShareable = MPU_ACCESS_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);
}
  1. 在main中调用MPU_Config(); 放在HAL_Init()之后。

注意事项

  • 缓冲区地址和大小必须与MPU区域匹配(对齐和大小需为2的幂)。
  • 不可缓存区域访问速度稍慢,但仅限该区域。

方案三:软件维护Cache(灵活通用)

原理

在DMA操作前后,手动执行Cache维护指令:

  • DMA发送前:Clean(写回)CPU写入的数据,确保主存最新。
  • DMA接收后:Invalidate(失效)Cache,强制CPU从主存重新读取。

配置步骤

  1. 确保缓冲区地址对齐:建议32字节对齐(Cache line大小)。
__attribute__((aligned(32))) uint8_t dma_buffer[1024];
  1. DMA发送前
// 假设dma_buffer中已有数据,准备发送
SCB_CleanDCache_by_Addr((uint32_t*)dma_buffer, sizeof(dma_buffer));
// 然后启动DMA发送
HAL_UART_Transmit_DMA(&huart, dma_buffer, sizeof(dma_buffer));
  1. DMA接收完成后(在DMA中断回调中):
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1)
    {
        // 使Cache失效,确保读取最新数据
        SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buffer, sizeof(dma_buffer));
        // 现在可以安全处理dma_buffer中的数据
        ProcessData(dma_buffer);
    }
}

注意事项

  • 地址必须32字节对齐,大小最好是32的倍数,否则可能影响相邻数据。
  • 对于连续DMA传输,每次传输后都要Invalidate,否则可能读到旧数据。
  • 如果缓冲区很小,也可以使用SCB_CleanDCache()SCB_InvalidateDCache()(全缓存操作),但效率低。

完整示例:UART DMA接收+发送

以下是一个使用方案三的完整示例,演示UART DMA收发。

#include "main.h"

// 定义缓冲区,32字节对齐
__attribute__((aligned(32))) uint8_t rx_buffer[256];
__attribute__((aligned(32))) uint8_t tx_buffer[] = "Hello from STM32H7!\r\n";

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);

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

    // 启动DMA接收,接收256字节
    HAL_UART_Receive_DMA(&huart1, rx_buffer, 256);

    // 发送数据前,Clean Cache
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer));
    HAL_UART_Transmit_DMA(&huart1, tx_buffer, sizeof(tx_buffer));

    while (1)
    {
        // 主循环
    }
}

// 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1)
    {
        // Invalidate Cache,确保读取最新数据
        SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
        // 处理数据...
        // 重新启动接收
        HAL_UART_Receive_DMA(&huart1, rx_buffer, 256);
    }
}

总结与最佳实践

  • 优先使用MPU配置:对于固定DMA缓冲区,MPU方案最省心,无需每次操作Cache。
  • 软件维护Cache:适用于动态缓冲区或无法对齐的情况,但需注意对齐和操作时机。
  • 避免关闭Cache:除非性能无关紧要。
  • 调试技巧:如果怀疑Cache问题,可临时关闭D-Cache测试,若问题消失,则确认是Cache一致性。

掌握这些方法,你就能在STM32H7上自如驾驭DMA与Cache,充分发挥高性能优势。