引言

STM32H7 系列(如 STM32H743/750)搭载 Cortex-M7,主频高达 480MHz,内置 L1-Cache(I-Cache 和 D-Cache)。Cache 能极大提升性能,但若处理不当,DMA 与外设交互时会产生数据不一致,轻则数据错误,重则 HardFault。本文聚焦五个隐蔽坑,每个坑都附原理分析和规避代码。

坑 1:DMA 与 CPU 共享缓冲区未做 Cache 维护

原理

DMA 直接访问 RAM,绕过 CPU 的 D-Cache。当 CPU 写入缓冲区后,数据可能仍留在 Cache 中,尚未写回 RAM。此时 DMA 从 RAM 读取,得到的是旧数据。反之,DMA 写入 RAM 后,CPU 读取时可能命中 Cache 中的旧数据。

规避方案

  • 使用 SCB_CleanDCache() 在 DMA 启动前将 CPU 数据写回 RAM。
  • 使用 SCB_InvalidateDCache() 在 DMA 完成后使 Cache 失效,强制 CPU 从 RAM 重新加载。
  • 推荐使用 CMSIS 提供的函数,并确保在临界区(如关中断)内操作。
// 示例:DMA 发送前 Clean,接收后 Invalidate
uint8_t tx_buf[256] __attribute__((aligned(32)));
uint8_t rx_buf[256] __attribute__((aligned(32)));

void DMA_Send(void) {
    SCB_CleanDCache(); // 或 SCB_CleanDCache_by_Addr((uint32_t)tx_buf, sizeof(tx_buf));
    HAL_UART_Transmit_DMA(&huart1, tx_buf, sizeof(tx_buf));
}

void DMA_ReceiveComplete(void) {
    SCB_InvalidateDCache(); // 或 SCB_InvalidateDCache_by_Addr((uint32_t)rx_buf, sizeof(rx_buf));
    // 现在 rx_buf 中的数据是 RAM 中的最新值
}

坑 2:MPU 配置不当导致 Cache 行为异常

原理

Cortex-M7 的 Cache 行为受 MPU(Memory Protection Unit)控制。默认情况下,STM32H7 的 SRAM 区域可能被配置为 Write-back(回写)模式,而某些外设区域(如 FMC 控制的 SDRAM)可能被配置为 Write-through(直写)或不可缓存。若 MPU 未正确配置,DMA 访问这些区域时可能绕过 Cache 或产生不可预测行为。

规避方案

  • 明确配置 MPU,为每个内存区域设置合适的 Cache 策略。
  • 对于 DMA 缓冲区,建议使用 Write-through 或 Non-cacheable 区域,避免一致性维护。
  • 使用 HAL_MPU_ConfigRegion() 或直接操作 MPU 寄存器。
// 配置 SRAM 区域为 Write-back,但 DMA 缓冲区单独设为 Non-cacheable
void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    HAL_MPU_Disable();

    // 配置整个 SRAM 为 Write-back
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x20000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_512KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 配置 DMA 专用缓冲区(假设在 0x20040000)为 Non-cacheable
    MPU_InitStruct.BaseAddress = 0x20040000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_CONTROL_HRNDM_ENABLE);
}

坑 3:多核或中断上下文中的 Cache 操作竞争

原理

STM32H7 部分型号(如 H745/H747)是双核(Cortex-M7 + Cortex-M4),共享内存区域。若两个核同时访问共享数据,且各自有独立的 Cache,则一致性维护必须同步。此外,中断服务程序(ISR)中执行 Cache 操作时,若与主循环冲突,可能导致数据损坏。

规避方案

  • 使用硬件信号量(如 HSEM)或软件锁保护共享区域。
  • 在 ISR 中避免使用阻塞式 Cache 操作,或确保操作原子性。
  • 对于双核,使用 SCB_CleanDCacheSCB_InvalidateDCache 时,需确保两个核都执行相同操作,或使用共享内存的 Non-cacheable 属性。
// 使用 HSEM 保护共享缓冲区
void SharedData_Write(uint8_t *data, uint32_t len) {
    HAL_HSEM_FastTake(0); // 获取信号量
    SCB_CleanDCache_by_Addr((uint32_t)shared_buf, len);
    memcpy(shared_buf, data, len);
    SCB_CleanDCache_by_Addr((uint32_t)shared_buf, len); // 再次 Clean 确保写回
    HAL_HSEM_Release(0);
}

void SharedData_Read(uint8_t *data, uint32_t len) {
    HAL_HSEM_FastTake(0);
    SCB_InvalidateDCache_by_Addr((uint32_t)shared_buf, len);
    memcpy(data, shared_buf, len);
    HAL_HSEM_Release(0);
}

坑 4:使用外部存储器(SDRAM/PSRAM)时未处理 Cache 行对齐

原理

Cache 操作的最小单位是 Cache Line(STM32H7 为 32 字节)。若缓冲区地址或长度不是 32 字节对齐,SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 会操作整个 Cache Line,可能覆盖相邻数据,导致数据丢失或错误。

规避方案

  • 确保缓冲区地址和大小均按 32 字节对齐。
  • 使用 __ALIGNED(32)__attribute__((aligned(32))) 声明。
  • 若无法对齐,则手动处理边界部分。
// 正确示例:对齐的缓冲区
uint8_t sdram_buf[1024] __attribute__((aligned(32)));

// 错误示例:未对齐,可能导致 Cache 操作越界
uint8_t bad_buf[100]; // 长度不是 32 的倍数

// 手动处理非对齐情况(不推荐,尽量对齐)
void Safe_Invalidate(uint32_t addr, uint32_t size) {
    uint32_t start = addr & ~0x1F;
    uint32_t end = (addr + size + 0x1F) & ~0x1F;
    SCB_InvalidateDCache_by_Addr(start, end - start);
}

坑 5:DMA 描述符或链表数据未维护 Cache

原理

使用 DMA 链表模式(如 MDMA 或 DMAMUX)时,描述符(Descriptor)存储在内存中。CPU 修改描述符后,若未 Clean Cache,DMA 可能读取到旧描述符,导致传输错误。同样,DMA 更新状态后,CPU 需 Invalidate 才能读取最新状态。

规避方案

  • 将描述符放入 Non-cacheable 区域(如特定 MPU 配置)。
  • 或在每次修改描述符后执行 Clean,在读取状态前执行 Invalidate。
  • 使用 HAL_MDMA_Start_IT 等函数时,注意其内部是否已处理。
// 示例:MDMA 描述符维护
MDMA_HandleTypeDef hmdma;
MDMA_LinkNodeTypeDef node __attribute__((aligned(32)));

void MDMA_Config(void) {
    // 配置描述符
    node.BM0.NDTR = len;
    node.BM0.SAR = (uint32_t)src;
    node.BM0.DAR = (uint32_t)dst;
    // 关键:Clean 描述符
    SCB_CleanDCache_by_Addr((uint32_t)&node, sizeof(node));
    HAL_MDMA_Start_IT(&hmdma, src, dst, len);
}

void MDMA_IRQHandler(void) {
    HAL_MDMA_IRQHandler(&hmdma);
    // 读取状态前 Invalidate
    SCB_InvalidateDCache_by_Addr((uint32_t)&node, sizeof(node));
    if (node.BM0.BRCR & MDMA_BRCR_TC) {
        // 传输完成
    }
}

总结与最佳实践

  • 统一规划:在项目初期就确定哪些内存区域需要 Cache,哪些不需要,并配置 MPU。
  • 封装函数:将 Cache 操作封装为通用函数,避免散落各处。
  • 使用对齐:所有 DMA 缓冲区、描述符一律 32 字节对齐。
  • 测试驱动:在压力测试(高频 DMA + 中断)下验证一致性。
  • 参考手册:仔细阅读 STM32H7 参考手册的 Cache 章节和勘误表。

Cache 一致性不是洪水猛兽,只要理解原理,遵循规范,就能避开这些坑。希望本文能帮你节省数天调试时间。欢迎留言交流你的踩坑经历!