引言:性能背后的代价

STM32H7 搭载 Cortex-M7,主频高达 480MHz,内置 32KB I-Cache 和 32KB D-Cache。Cache 让 CPU 访问内存速度接近零等待,但代价是——CPU 与外设(如 DMA、以太网 MAC、LTDC)看到的物理内存可能不一致。这种不一致轻则导致数据错乱,重则系统 HardFault。很多开发者遇到“偶发故障”却百思不得其解,其实根源就在 Cache 一致性。

本文将基于 STM32H743 实际调试经验,总结 5 种隐蔽陷阱,并给出可直接落地的修复方案。


陷阱一:DMA 写入内存后,CPU 读到旧数据

原理

当 DMA 将外设数据(如 ADC 采样、UART 接收)搬运到 SRAM 时,如果该内存区域已被 CPU 读取过并缓存到 D-Cache,那么 CPU 再次读取时直接命中 Cache,返回的是陈旧数据,而 DMA 写入的是物理 RAM。

典型场景

// 错误示例:DMA 接收完成中断中直接处理数据
uint8_t rx_buffer[1024] __attribute__((aligned(32)));

void DMA_RX_Complete_IRQ(void) {
    // 此时 rx_buffer 可能还在 D-Cache 中,CPU 读到的是旧数据
    process_data(rx_buffer);  // 随机错误!
}

修复方案

在 DMA 写入完成后,必须使 D-Cache 失效(Invalidate),丢弃 Cache 中的旧副本。

void DMA_RX_Complete_IRQ(void) {
    // 使 D-Cache 失效,强制从物理 RAM 重新读取
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
    process_data(rx_buffer);
}

注意SCB_InvalidateDCache_by_Addr 要求地址 32 字节对齐,且长度需为 32 的倍数,否则会触发断言或未定义行为。


陷阱二:CPU 写数据后,DMA 读出旧数据

原理

CPU 向内存写入数据,数据先进入 D-Cache(写回策略),尚未写回物理 RAM。此时如果 DMA 去读取该内存,DMA 直接访问物理 RAM,得到的是未更新的旧值

典型场景

// 错误示例:CPU 填充发送缓冲区,然后启动 DMA 发送
uint8_t tx_buffer[512] __attribute__((aligned(32)));

void send_via_dma(void) {
    fill_buffer(tx_buffer);  // CPU 写入,可能只存在于 Cache
    HAL_UART_Transmit_DMA(&huart, tx_buffer, 512);  // DMA 读物理 RAM,数据错误
}

修复方案

在启动 DMA 前,必须将 D-Cache 中的脏数据**写回(Clean)**到物理 RAM。

void send_via_dma(void) {
    fill_buffer(tx_buffer);
    // 写回 D-Cache,确保物理 RAM 与 Cache 一致
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer));
    HAL_UART_Transmit_DMA(&huart, tx_buffer, 512);
}

陷阱三:DMA 双向操作(如双缓冲)时,Clean 和 Invalidate 顺序错误

原理

有些外设(如以太网 DMA、SDMMC)使用双缓冲或环形缓冲区,CPU 和 DMA 同时读写同一块内存。如果只做 Clean 或只做 Invalidate,会导致数据丢失或旧数据覆盖。

典型场景

以太网接收描述符和缓冲区,DMA 写入新数据,CPU 处理后又写回状态。

// 错误示例:先 Invalidate 再 Clean,导致新数据被旧数据覆盖
void eth_rx_process(void) {
    SCB_InvalidateDCache_by_Addr(rx_buf, len);  // 先使失效
    process_packet(rx_buf);                     // CPU 修改数据
    SCB_CleanDCache_by_Addr(rx_buf, len);       // 写回,但可能覆盖 DMA 新写入?
}

修复方案

正确顺序是:先 Clean(将 CPU 修改写回),再 Invalidate(丢弃 DMA 可能更新的旧 Cache 行)。但更稳妥的是使用双缓冲 + 交替使用,避免同一时刻共享。

// 正确做法:使用双缓冲,CPU 处理 buffer A 时 DMA 写入 buffer B
void eth_rx_process(void) {
    // 处理 buffer A(DMA 已完成写入)
    SCB_InvalidateDCache_by_Addr(rx_buf_A, len);  // 使失效,读取最新 DMA 数据
    process_packet(rx_buf_A);
    // 此时不 Clean,因为 CPU 不修改 rx_buf_A,只读
    // 切换 DMA 到 buffer A,CPU 下次处理 buffer B
}

陷阱四:MPU 配置不当导致 Cache 策略失效

原理

STM32H7 默认所有内存区域都是 Write-Back(回写)Cache 策略,但某些外设(如 FMC 控制的 SDRAM、QSPI Flash)需要 Write-Through(写通)或 Non-Cacheable 才能保证一致性。如果 MPU 未正确配置,即使你调用了 Clean/Invalidate 函数,也可能因为 Cache 策略不匹配而无效。

典型场景

使用 FMC 外接 SDRAM 作为帧缓冲,LTDC 直接读取,CPU 写入。如果 SDRAM 配置为 Write-Back,LTDC 可能读到未写回的数据。

修复方案

通过 MPU 将 SDRAM 区域配置为 Write-Through 或 Non-Cacheable。

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

    HAL_MPU_Disable();

    // 配置 SDRAM 区域为 Write-Through
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0xC0000000;  // SDRAM 地址
    MPU_InitStruct.Size = MPU_REGION_SIZE_8MB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL_0;  // 配合 C=1, B=0 得到 Write-Through
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;

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

注意:修改 MPU 配置后,必须执行 __DSB()__ISB() 确保生效。


陷阱五:中断上下文中的 Cache 操作导致死锁

原理

在中断服务函数(ISR)中调用 SCB_CleanDCacheSCB_InvalidateDCache 时,如果中断优先级高于某个正在执行 Cache 维护操作的任务,可能导致嵌套操作,而 Cortex-M7 的 Cache 维护指令不支持重入,可能造成总线死锁。

典型场景

一个高优先级定时器中断中调用 SCB_InvalidateDCache_by_Addr,而主循环中正在执行 SCB_CleanDCache_by_Addr,两者冲突。

修复方案

避免在 ISR 中做 Cache 维护,改为在任务级处理。如果必须,则使用临界区保护。

// 错误:ISR 中直接操作
void TIM_IRQHandler(void) {
    SCB_InvalidateDCache_by_Addr(buf, len);  // 危险!
}

// 正确:置标志,在任务中处理
void TIM_IRQHandler(void) {
    g_cache_op_pending = 1;
}

void main_loop(void) {
    if (g_cache_op_pending) {
        __disable_irq();  // 关中断,防止嵌套
        SCB_InvalidateDCache_by_Addr(buf, len);
        __enable_irq();
        g_cache_op_pending = 0;
    }
}

总结与最佳实践

  • 内存对齐:所有共享缓冲区必须 32 字节对齐,长度 32 的倍数,否则 Cache 操作会越界。
  • 使用 CMSIS 函数SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_AddrSCB_CleanInvalidateDCache_by_Addr,不要自己写汇编。
  • 双缓冲优先:对于高频 DMA(如以太网),使用双缓冲并交替切换,可减少 Cache 操作次数。
  • MPU 规划:在项目初始化时,明确哪些区域需要 Cacheable,哪些需要 Non-Cacheable,避免后期混乱。
  • 调试技巧:如果出现随机故障,先尝试关闭 D-Cache(SCB_DisableDCache())看是否复现,若消失则基本确定是 Cache 一致性问题。

Cache 一致性不是洪水猛兽,只要理解原理并遵循上述规则,STM32H7 的 480MHz 性能就能稳稳发挥。希望这篇文章能帮你少踩几个坑。