3. Cache 一致性管理:M7 内核的“双刃剑”

好,咱们进入第三章。这一章,我打算跟你好好聊聊 M7 内核的 L1 Cache。

说实话,Cache 这东西,用好了是神器,用不好就是噩梦。我在做第一块双核板子的时候,就吃过它的亏。当时数据莫名其妙地乱跳,查了整整两天,最后发现是 Cache 没刷干净。嗯,从那以后,我再也不敢小看它了。

3.1 M7 内核 L1 Cache 工作原理

先说说 M7 的 Cache 是怎么工作的。说白了,它就是一块离 CPU 极近的高速缓存。

M7 有独立的指令 Cache(I-Cache)和数据 Cache(D-Cache),各 16KB。CPU 要读数据时,先看 Cache 里有没有。有,就是“命中”,直接拿,一个周期搞定。没有,就是“未命中”,得去主存搬,几十个周期就出去了。

你想想看,这速度差了多少倍?所以 Cache 对性能的提升是巨大的。

核心概念:Cache Line

M7 的 Cache 以“行”为单位管理,每行 32 字节。也就是说,哪怕你只读一个字节,Cache 也会把相邻的 32 字节一起搬进来。这叫“空间局部性”。

我个人习惯把 Cache 想象成一个“小本子”。CPU 先把常用数据记在小本子上,查起来快。但问题来了——如果别的“人”(比如 DMA、另一个核)改了主存里的数据,小本子上的记录就过时了。这就是 Cache 污染。

3.2 Cache 污染问题分析

Cache 污染,说白了就是数据不一致。M7 自己读写没问题,但一旦涉及外设、DMA、或者另一个 M4 核,麻烦就来了。

我遇到过最典型的一个场景:M7 往一个缓冲区写数据,然后通知 DMA 把这个缓冲区的内容发送出去。结果 DMA 发出去的全是乱码。为什么?因为 M7 写的是 Cache,还没写回主存。DMA 读的是主存,读到的全是旧数据。

这就是“写污染”。反过来,如果 DMA 先往主存写了一批数据,M7 再去读,读到的可能是 Cache 里的旧数据,这叫“读污染”。

污染类型 场景 后果
写污染 CPU 写 Cache,未写回主存 DMA/外设读到旧数据
读污染 DMA 更新主存,CPU 读 Cache CPU 拿到过时数据

这两种情况,在双核系统里尤其常见。M7 和 M4 共享内存时,如果不做 Cache 维护,数据乱飞是必然的。

避坑指南

我曾经在一个项目中,M7 和 M4 通过共享内存通信。M7 写了一个标志位,M4 死活检测不到。查到最后,发现 M7 的标志位还在 Cache 里,根本没写回主存。从那以后,我定了个规矩:所有共享内存区域,要么用非 Cache 属性,要么每次访问前后都做 Clean/Invalidate。

3.3 使用 SCB 指令维护 Cache 一致性

好了,问题清楚了,怎么解决?ARM 提供了 SCB(System Control Block)指令,专门用来维护 Cache 一致性。常用的就三个操作:Clean、Invalidate、Clean & Invalidate。

  • Clean:把 Cache 里脏的数据写回主存。只写回,不清除 Cache。
  • Invalidate:把 Cache 里的数据标记为无效。下次读的时候,必须从主存重新加载。
  • Clean & Invalidate:先写回,再标记无效。最彻底,也最常用。

M7 的 SCB 指令可以操作整个 Cache,也可以只操作某一段地址范围。我个人建议,能用范围操作就别用全局操作,效率高很多。

下面是我常用的几个函数,直接抄过去就能用:

/* 清理一段地址范围的 D-Cache(写回主存) */
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize)
{
    int32_t line_size = 32;  /* M7 Cache Line 大小 */
    uint32_t start_addr = (uint32_t)addr;
    uint32_t end_addr = start_addr + dsize;
    
    /* 对齐到 Cache Line 边界 */
    start_addr &= ~(line_size - 1);
    
    for (uint32_t cur = start_addr; cur < end_addr; cur += line_size)
    {
        SCB->DCCMVAC = cur;  /* 写回并标记无效 */
    }
    __DSB();  /* 数据同步屏障,确保操作完成 */
}

/* 无效化一段地址范围的 D-Cache(丢弃 Cache 内容) */
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize)
{
    int32_t line_size = 32;
    uint32_t start_addr = (uint32_t)addr;
    uint32_t end_addr = start_addr + dsize;
    
    start_addr &= ~(line_size - 1);
    
    for (uint32_t cur = start_addr; cur < end_addr; cur += line_size)
    {
        SCB->DCIMVAC = cur;  /* 仅标记无效 */
    }
    __DSB();
}

/* 清理并无效化(最常用) */
void SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize)
{
    int32_t line_size = 32;
    uint32_t start_addr = (uint32_t)addr;
    uint32_t end_addr = start_addr + dsize;
    
    start_addr &= ~(line_size - 1);
    
    for (uint32_t cur = start_addr; cur < end_addr; cur += line_size)
    {
        SCB->DCCIMVAC = cur;  /* 先写回再标记无效 */
    }
    __DSB();
}

使用技巧

注意看,我每次操作完都加了一个 __DSB()。这个很重要!DSB 保证前面的 Cache 操作全部完成之后,CPU 才会继续往下执行。不加 DSB,你可能会在 Cache 还没刷完的时候就去操作外设,那等于白刷。

3.4 什么时候该用哪个?

很多初学者会问:我到底该用 Clean 还是 Invalidate?

我总结了一个简单的规则:

  • CPU 写数据给 DMA/外设读:用 Clean。把 CPU 写的 Cache 数据刷回主存,让 DMA 能读到最新数据。
  • DMA/外设写数据给 CPU 读:用 Invalidate。把 CPU 里旧的 Cache 数据丢掉,下次读的时候从主存拿新的。
  • 不确定谁先谁后,或者双向通信:用 Clean & Invalidate。最保险,但性能开销也最大。

举个例子。我在做音频采集时,M7 负责处理音频数据,DMA 负责从 I2S 外设搬数据到内存。流程是这样的:

  1. DMA 把音频数据写入内存缓冲区。
  2. M7 处理前,先 Invalidate 缓冲区地址,确保读到的是最新数据。
  3. M7 处理完,把结果写入另一个缓冲区。
  4. M7 处理完后,Clean 这个缓冲区,确保 DMA 能读到正确的结果。

你看,每一步都很清晰。只要记住“谁写谁 Clean,谁读谁 Invalidate”,基本不会出错。

3.5 知识结构总览

下面这张图,把本章的核心逻辑串起来了。你可以把它当成一个速查表。

Cache 一致性管理知识结构 M7 L1 Cache I-Cache (16KB) D-Cache (16KB) Cache 污染问题 写污染(CPU→DMA) 读污染(DMA→CPU) SCB 指令维护一致性 Clean(写回主存) Invalidate(丢弃) Clean & Invalidate 核心原则:谁写谁 Clean,谁读谁 Invalidate

3.6 一个完整的实战例子

最后,我给你一个完整的例子。假设 M7 要跟一个 SPI 外设通信,SPI 用 DMA 方式收发数据。这是典型的 Cache 一致性场景。

/* SPI DMA 发送缓冲区 */
uint8_t spi_tx_buf[1024] __attribute__((aligned(32)));

/* SPI DMA 接收缓冲区 */
uint8_t spi_rx_buf[1024] __attribute__((aligned(32)));

void spi_dma_transfer(void)
{
    /* 准备发送数据 */
    prepare_tx_data(spi_tx_buf);
    
    /* 发送前:Clean D-Cache,确保 DMA 能读到最新数据 */
    SCB_CleanDCache_by_Addr((uint32_t *)spi_tx_buf, sizeof(spi_tx_buf));
    
    /* 启动 SPI DMA 传输 */
    HAL_SPI_TransmitReceive_DMA(&hspi1, spi_tx_buf, spi_rx_buf, sizeof(spi_tx_buf));
    
    /* 等待传输完成 */
    while (spi_dma_busy);
    
    /* 接收后:Invalidate D-Cache,确保 CPU 读到的是 DMA 写入的最新数据 */
    SCB_InvalidateDCache_by_Addr((uint32_t *)spi_rx_buf, sizeof(spi_rx_buf));
    
    /* 现在可以安全地处理接收数据了 */
    process_rx_data(spi_rx_buf);
}

你看,代码很简单。关键就是两步:发送前 Clean,接收后 Invalidate。记住这个模式,你在绝大多数场景下都不会出问题。

本章要点回顾

  • M7 的 L1 Cache 分 I-Cache 和 D-Cache,每行 32 字节。
  • Cache 污染分写污染和读污染,根源是 Cache 与主存数据不一致。
  • SCB 指令提供 Clean、Invalidate、Clean & Invalidate 三种操作。
  • 操作后必须加 __DSB() 确保完成。
  • 核心原则:谁写谁 Clean,谁读谁 Invalidate。