一、为什么STM32H7需要关注Cache一致性?

STM32H7搭载Cortex-M7内核,内置L1-Cache(I-Cache和D-Cache),用于缓解CPU与低速存储器(如Flash、SRAM)之间的速度鸿沟。然而,Cache的引入带来了一个经典问题:DMA直接访问物理内存,而CPU可能只操作Cache中的副本,导致双方看到的数据不一致。

  • 场景1:CPU写数据到SRAM(数据留在Cache中),DMA从SRAM读取时可能读到旧数据。
  • 场景2:DMA从外设接收数据到SRAM,CPU读取时可能命中Cache中的旧数据。

若不加处理,轻则数据错误,重则系统崩溃。因此,掌握一致性维护是STM32H7开发的必备技能。

二、STM32H7的Cache与DMA架构解析

2.1 Cache工作原理

STM32H7的D-Cache采用**写回(Write-back)**策略,即CPU写操作先更新Cache,标记为脏(Dirty),仅在缓存行被替换或显式Clean时写回内存。读操作则优先命中Cache,未命中时从内存加载整行(32字节)。

2.2 DMA与Cache的冲突点

DMA控制器直接访问总线矩阵,不经过Cache。因此,当CPU和DMA共享同一块内存区域时,必须通过软件维护一致性。

2.3 一致性维护操作

Cortex-M7提供了两条关键指令(通过CMSIS函数封装):

  • SCB_CleanDCache_by_Addr:将指定地址的脏缓存行写回内存(用于CPU写后,DMA读前)。
  • SCB_InvalidateDCache_by_Addr:使指定地址的缓存行失效(用于DMA写后,CPU读前)。

注意:操作地址必须32字节对齐,且长度应为32的倍数,否则会破坏相邻数据。

三、实战:双缓冲ADC采样与DMA传输

3.1 需求描述

使用ADC1以DMA双缓冲模式采集数据,缓冲数组位于SRAM,CPU在后台处理已满的缓冲区。要求启用D-Cache,保证数据正确。

3.2 硬件配置

  • MCU:STM32H743ZI
  • 外设:ADC1,采样率1MHz,DMA1 Stream0
  • 缓冲区:两个uint32_t数组,每个大小1024(4KB),需32字节对齐

3.3 配置步骤

  1. 使能Cache:在main函数开头调用SCB_EnableDCache()
  2. 定义对齐缓冲区:使用__attribute__((aligned(32)))确保对齐。
  3. 配置DMA:设置双缓冲模式,内存地址为缓冲区地址。
  4. 维护一致性:在DMA传输完成中断中,对已满缓冲区执行Invalidate操作。

3.4 完整代码示例

#include "stm32h7xx_hal.h"

// 32字节对齐的缓冲区(4KB)
__attribute__((aligned(32))) uint32_t buf0[1024];
__attribute__((aligned(32))) uint32_t buf1[1024];

// DMA句柄
extern DMA_HandleTypeDef hdma_adc1;

// 当前活动缓冲区索引
volatile uint8_t active_buf = 0;

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    uint32_t* filled_buf;
    uint32_t size = sizeof(buf0);

    // 确定哪个缓冲区已满(双缓冲模式下,当前目标地址是另一个)
    if (active_buf == 0) {
        filled_buf = buf1;
        active_buf = 1;
    } else {
        filled_buf = buf0;
        active_buf = 0;
    }

    // 使填充缓冲区的缓存行失效,确保CPU读取到DMA写入的最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)filled_buf, size);

    // 处理数据(例如计算平均值)
    uint32_t sum = 0;
    for (int i = 0; i < 1024; i++) {
        sum += filled_buf[i];
    }
    // 注意:处理过程中,CPU可能再次缓存这些数据,但下次DMA写入前需Clean
}

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

    // 使能D-Cache(必须在初始化外设前)
    SCB_EnableDCache();

    // 初始化ADC和DMA(略)
    MX_ADC1_Init();
    MX_DMA_Init();

    // 启动ADC DMA双缓冲传输
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)buf0, 1024);
    HAL_ADCEx_MultiModeStart_DMA(&hadc1, (uint32_t*)buf1, 1024);

    while (1) {
        // 主循环可处理其他任务
    }
}

3.5 代码说明

  • 在回调中,我们仅对已满的缓冲区执行Invalidate,因为DMA写入后,该缓冲区在Cache中可能残留旧数据。
  • 若CPU在回调中修改了缓冲区(本例未修改),则下次DMA写入前需执行Clean操作,否则DMA可能读到脏数据。
  • 使用SCB_InvalidateDCache_by_Addr时,地址和长度必须对齐32字节,否则会触发断言(在调试模式下)。

四、注意事项与最佳实践

  • 对齐与长度:所有共享缓冲区必须32字节对齐,且长度建议为32的倍数。可使用__attribute__((aligned(32)))memalign
  • 操作顺序:CPU写后DMA读前,必须Clean;DMA写后CPU读前,必须Invalidate。顺序颠倒会导致数据错误。
  • 避免频繁操作:Cache操作有开销,对于高频DMA传输,可考虑将缓冲区配置为非Cacheable(通过MPU),但会牺牲性能。
  • 使用MPU隔离:对于关键缓冲区,可通过MPU将区域设置为DeviceStrongly-Ordered,禁用Cache,但需权衡。
  • 调试技巧:若出现数据随机错误,先检查对齐和操作顺序;使用逻辑分析仪或断点观察DMA写入前后内存值。

五、总结

STM32H7的Cache一致性维护是高性能开发的关键环节。通过理解Cache工作原理,正确使用Clean和Invalidate操作,并遵循对齐与顺序原则,即可有效避免DMA与CPU的数据冲突。本文的实战案例可直接应用于ADC、UART、SPI等外设的DMA传输场景。掌握这一技能,你将能充分发挥H7的性能优势,构建稳定可靠的嵌入式系统。