引言:480MHz 的代价——Cache 一致性问题

STM32H7 系列(如 STM32H743、H750)最高运行在 480MHz,配备 32KB 的 I-Cache 和 32KB 的 D-Cache,性能直逼入门级应用处理器。然而,高主频带来的 Cache 机制却成为嵌入式开发者的噩梦:当 DMA 外设与 CPU 通过共享内存交互时,Cache 中的数据可能与外存(SRAM)不一致,导致数据错乱、程序异常。本文将从原理到实战,彻底讲透这个问题。

一、Cortex-M7 的 Cache 架构与一致性原理

1.1 为什么需要 Cache?

CPU 主频高达 480MHz,而 SRAM 的访问延迟通常在 5-10ns 量级,若每次访问都直接走总线,CPU 将被迫等待,性能大打折扣。Cache 作为 CPU 与 SRAM 之间的高速缓冲,存储最近访问的数据副本,使得 CPU 能以接近 1 个时钟周期访问热点数据。

1.2 写策略与一致性风险

Cortex-M7 的 D-Cache 默认采用 写回(Write-back) 策略:CPU 写数据时先写入 Cache,标记为脏(Dirty),直到缓存行被替换时才写回 SRAM。这种策略提高了写性能,但带来了致命问题:

  • DMA 读 SRAM 时:若 CPU 已修改数据但仍在 Cache 中,DMA 读到的将是旧数据(Stale Data)。
  • DMA 写 SRAM 时:若 Cache 中已有该地址的副本,CPU 后续读取会命中 Cache,得到旧值,而 DMA 写入的新值被忽略。

1.3 缓存行(Cache Line)与对齐

D-Cache 的缓存行大小通常为 32 字节(STM32H7 可配置为 32 或 64 字节)。Cache 操作以缓存行为单位,因此数据对齐和操作粒度至关重要。

二、实战陷阱:典型故障场景

2.1 场景一:UART DMA 接收数据错乱

// 错误示例:DMA 接收缓冲区,未做 Cache 维护
uint8_t rx_buffer[256];

void UART_DMA_Start(void) {
    HAL_UART_Receive_DMA(&huart1, rx_buffer, 256);
}

// 中断回调中处理数据
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    process_data(rx_buffer);  // 可能读到旧数据!
}

现象:接收到的数据偶尔出现乱码,尤其在连续接收时。

原因:DMA 将数据写入 SRAM,但 rx_buffer 可能已在 Cache 中,CPU 读取时命中 Cache 旧值。

2.2 场景二:ADC 采样数据被 DMA 覆盖后 CPU 仍读旧值

uint32_t adc_values[64];

// ADC 持续 DMA 传输到 adc_values
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_values, 64);

while (1) {
    // 主循环读取,可能读到旧值
    process_adc(adc_values);
}

现象:采样值不更新,或偶尔更新。

三、修复方案:Clean 与 Invalidate 的正确姿势

3.1 核心 API 介绍

CMSIS 提供了标准的 Cache 操作函数,位于 core_cm7.h

  • SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将指定地址的脏缓存行写回 SRAM(Clean)。
  • SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):使指定地址的缓存行失效,下次读取时从 SRAM 重新加载(Invalidate)。
  • SCB_CleanInvalidateDCache_by_Addr:先 Clean 再 Invalidate,用于双向同步。

注意:地址必须按缓存行大小对齐,长度按缓存行大小向上取整。

3.2 修复场景一:DMA 接收数据

正确流程:在启动 DMA 前,Invalidate 接收缓冲区(确保 Cache 中无残留旧数据);在 DMA 完成后,再次 Invalidate(使 CPU 从 SRAM 重新加载)。

#define CACHE_LINE_SIZE  32

uint8_t rx_buffer[256] __attribute__((aligned(32)));  // 对齐到 32 字节

void UART_DMA_Start(void) {
    // 启动前 Invalidate,丢弃 Cache 中的旧副本
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
    HAL_UART_Receive_DMA(&huart1, rx_buffer, 256);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    // 完成后 Invalidate,确保读到 DMA 写入的新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
    process_data(rx_buffer);
}

3.3 修复场景二:DMA 发送数据

正确流程:在启动 DMA 发送前,Clean 发送缓冲区(将 Cache 中的脏数据写回 SRAM)。

uint8_t tx_buffer[128] __attribute__((aligned(32)));

void UART_DMA_Send(uint8_t *data, uint16_t len) {
    memcpy(tx_buffer, data, len);
    // 发送前 Clean,确保 DMA 能读到最新数据
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, len);
    HAL_UART_Transmit_DMA(&huart1, tx_buffer, len);
}

3.4 修复场景三:ADC 连续采样

uint32_t adc_values[64] __attribute__((aligned(32)));

void ADC_DMA_Start(void) {
    // 启动前 Invalidate
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_values, sizeof(adc_values));
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_values, 64);
}

// 在 DMA 半传输或完成中断中处理
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    // 处理前 Invalidate
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_values, sizeof(adc_values));
    process_adc(adc_values);
}

四、进阶技巧与注意事项

4.1 使用 MPU 配置非 Cacheable 区域

对于频繁被 DMA 访问的缓冲区,可以配置 MPU 将特定区域设置为 非 Cacheable(Non-cacheable),彻底避免一致性问题。但需注意:非 Cacheable 区域访问速度较慢,适用于小缓冲区。

// 示例:配置 0x20000000 起始的 4KB 为非 Cacheable
void MPU_Config_NonCacheable(void) {
    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    HAL_MPU_Disable();
    
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x20000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_4KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;  // 关键
    MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

4.2 注意编译优化与 volatile

即使做了 Cache 维护,编译器也可能将变量优化到寄存器中。对于共享变量,应使用 volatile 修饰,防止编译器过度优化。

volatile uint8_t rx_buffer[256] __attribute__((aligned(32)));

4.3 缓存行对齐的重要性

如果缓冲区未对齐,SCB_InvalidateDCache_by_Addr 可能会操作到相邻缓存行,导致意外数据失效。建议使用 __attribute__((aligned(32)))ALIGN_32BYTES 宏。

4.4 性能权衡

频繁的 Clean/Invalidate 操作会降低性能(每次操作约 10-20 个周期)。对于高频数据流,建议使用双缓冲 + 非 Cacheable 区域,或使用硬件 FIFO 减少同步频率。

五、总结:三条黄金法则

  1. DMA 写内存后,CPU 读取前必须 Invalidate(丢弃 Cache 旧副本)。
  2. CPU 写内存后,DMA 读取前必须 Clean(将脏数据写回 SRAM)。
  3. 缓冲区必须按缓存行对齐,且操作长度向上取整

遵循这三条法则,即可在 STM32H7 上安全使用 DMA 与 Cache,充分发挥 480MHz 主频的性能优势。

结语

Cache 一致性是高性能 MCU 开发的必修课。本文从原理到实战,给出了完整的解决方案。建议开发者在项目初期就建立 Cache 维护的规范,避免后期调试的深坑。如果你在实际项目中遇到类似问题,欢迎留言交流。