STM32H7 480MHz 主频下 Cache 一致性维护的三种实用策略

为什么 Cache 一致性是 H7 的“隐形杀手”?

STM32H7 采用 Cortex-M7 内核,主频高达 480MHz,内置 16KB I-Cache 和 16KB D-Cache。Cache 大幅提升 CPU 访问速度,但引入了一个经典问题:CPU 与 DMA 外设访问同一内存区域时,数据可能不一致。例如,DMA 将 ADC 数据写入 SRAM,CPU 从 Cache 读取时可能拿到旧数据;反之,CPU 写数据后未及时回写,DMA 可能搬运脏数据。

解决思路有三:

  • 硬件层面:利用 MPU 将共享内存配置为不可缓存(或写透)。
  • 软件层面:手动执行 Clean(回写)和 Invalidate(失效)操作。
  • 架构层面:使用双缓冲机制,避免 CPU 和 DMA 同时访问同一缓冲区。

下面逐一展开,每种策略均包含原理、配置和代码。

策略一:MPU 配置不可缓存区域(最简单粗暴)

原理

通过 MPU 将特定 SRAM 区域设置为“非缓存”或“写透”,使 CPU 每次读写都直接访问物理内存,彻底避免 Cache 一致性问题。适合低频访问的共享数据(如控制标志、小数据包)。

配置步骤

  1. 使能 MPU 并配置区域属性。
  2. 设置区域基地址、大小和访问权限。
  3. 启用 MPU。

代码示例(基于 HAL 库)

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

    __HAL_RCC_MPU_CONFIG_CLK_ENABLE();
    HAL_MPU_Disable();

    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x30000000; // SRAM1 起始地址
    MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
    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.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意事项

  • 不可缓存区域会降低 CPU 访问速度,仅用于小数据量共享。
  • 配置后需确保链接脚本中该区域不被其他用途覆盖。

策略二:软件 Clean/Invalidate 操作(灵活精准)

原理

保持 Cache 开启,在 DMA 传输前后手动执行 Cache 维护指令。CPU 写数据后调用 SCB_CleanDCache() 将脏数据回写;DMA 写入后调用 SCB_InvalidateDCache() 使 Cache 行失效,强制 CPU 重新从内存读取。

配置步骤

  1. 开启 D-Cache(默认开启)。
  2. 在 DMA 读取前 Clean,DMA 写入后 Invalidate。
  3. 注意地址对齐(32 字节对齐)和长度。

代码示例(以 DMA 接收 UART 数据为例)

#define BUF_SIZE 256
uint8_t rx_buffer[BUF_SIZE] __attribute__((aligned(32)));

void DMA_RX_Start(void)
{
    // 清空接收缓冲区(可选)
    memset(rx_buffer, 0, BUF_SIZE);

    // 启动 DMA 接收(假设已配置 DMA 通道)
    HAL_UART_Receive_DMA(&huart1, rx_buffer, BUF_SIZE);
}

void DMA_RX_Complete_Callback(void)
{
    // DMA 写入完成后,使 Cache 失效,确保 CPU 读到最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, BUF_SIZE);

    // 处理数据...
}

void DMA_TX_Start(uint8_t *data, uint16_t len)
{
    // CPU 写数据后,回写 Cache 到内存
    SCB_CleanDCache_by_Addr((uint32_t*)data, len);

    // 启动 DMA 发送
    HAL_UART_Transmit_DMA(&huart1, data, len);
}

注意事项

  • 地址必须 32 字节对齐,长度最好为 32 的倍数,否则需处理边界。
  • 频繁调用会影响性能,建议批量操作。
  • 使用 __attribute__((aligned(32))) 确保缓冲区对齐。

策略三:双缓冲机制(架构级优化)

原理

分配两个缓冲区,CPU 和 DMA 交替使用。当 DMA 写入缓冲区 A 时,CPU 处理缓冲区 B;完成后交换角色。这样 CPU 和 DMA 不会同时访问同一内存,从根源上避免冲突。

配置步骤

  1. 定义两个缓冲区,大小相同。
  2. 使用 DMA 传输完成中断切换当前活动缓冲区。
  3. 在切换时执行一次 Clean/Invalidate(可选,但推荐)。

代码示例(基于双缓冲的 ADC 采样)

#define BUF_SIZE 1024
uint8_t buf_a[BUF_SIZE] __attribute__((aligned(32)));
uint8_t buf_b[BUF_SIZE] __attribute__((aligned(32)));
volatile uint8_t active_buf = 0;

void DMA_ADC_Start(void)
{
    // 启动第一次传输到 buf_a
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)buf_a, BUF_SIZE);
    active_buf = 0;
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    uint8_t *processed_buf;

    // 当前完成的缓冲区是 active_buf,切换下一次到另一个
    if (active_buf == 0) {
        processed_buf = buf_a;
        active_buf = 1;
        HAL_ADC_Start_DMA(&hadc1, (uint32_t*)buf_b, BUF_SIZE);
    } else {
        processed_buf = buf_b;
        active_buf = 0;
        HAL_ADC_Start_DMA(&hadc1, (uint32_t*)buf_a, BUF_SIZE);
    }

    // 对 processed_buf 执行 Invalidate(因为 DMA 刚写入)
    SCB_InvalidateDCache_by_Addr((uint32_t*)processed_buf, BUF_SIZE);

    // 处理 processed_buf 数据...
}

注意事项

  • 缓冲区大小需匹配 DMA 传输长度,避免溢出。
  • 双缓冲可提高吞吐量,但内存占用翻倍。
  • 切换逻辑需在中断中快速完成,避免丢数据。

总结与选型建议

| 策略 | 适用场景 | 优点 | 缺点 | |------|----------|------|------| | MPU 不可缓存 | 小数据、低频共享 | 简单、无软件开销 | 性能损失、不灵活 | | 软件 Clean/Invalidate | 中等数据量、灵活控制 | 性能较好、精确 | 需注意对齐、有代码开销 | | 双缓冲 | 高速连续数据流 | 性能最优、无冲突 | 内存翻倍、实现复杂 |

实际项目中,常组合使用:例如用 MPU 保护关键控制结构,用双缓冲处理大数据流,并在切换时执行必要的 Cache 操作。掌握这三种策略,你就能在 STM32H7 上游刃有余地处理 Cache 一致性问题,释放 480MHz 的全部潜力。