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 外设搬数据到内存。流程是这样的:
- DMA 把音频数据写入内存缓冲区。
- M7 处理前,先 Invalidate 缓冲区地址,确保读到的是最新数据。
- M7 处理完,把结果写入另一个缓冲区。
- M7 处理完后,Clean 这个缓冲区,确保 DMA 能读到正确的结果。
你看,每一步都很清晰。只要记住“谁写谁 Clean,谁读谁 Invalidate”,基本不会出错。
3.5 知识结构总览
下面这张图,把本章的核心逻辑串起来了。你可以把它当成一个速查表。
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。