4. Cache基础原理

在深入ARM Cortex-M7的Cache优化之前,必须先建立扎实的Cache基础理论。Cache的本质是位于CPU与主存储器(Flash/SRAM)之间的高速小容量存储阵列,它利用程序的时间局部性与空间局部性,将近期可能被访问的数据提前搬运到离CPU更近的位置。Cortex-M7的L1 Cache分为I-Cache(指令)与D-Cache(数据),其行为直接决定了实时控制与信号处理代码的最终执行效率。本节将围绕映射方式、行大小、命中/未命中判定以及写策略四个核心维度展开,为后续TCM配置与内存带宽优化奠定概念基础。

Cache 核心概念关系图 CPU 核心 Cortex-M7 发出地址/数据请求 Cache 存储器阵列 行大小: 32 字节 (典型) 映射方式: 直接/组相联/全相联 Tag + Index + Offset 命中判定 Tag 比较 命中 → 直接返回数据 未命中 → 访问主存 主存储器 Flash / SRAM 访问延迟高 地址/数据 Tag/Index 未命中 写策略 写回 (Write-Back) 脏位标记,延迟写主存 写通 (Write-Through) 立即写主存 行大小影响: 空间局部性利用效率 32B行 → 适合顺序访问; 过大行 → 浪费带宽, 过小行 → 命中率低

4.1 Cache映射方式:直接映射、组相联与全相联

映射方式决定了主存地址如何对应到Cache中的存储位置。Cortex-M7的D-Cache采用4路组相联(4-way Set-Associative)结构,I-Cache同样为4路组相联,这是性能与硬件复杂度之间的折中。理解映射方式需要先掌握地址的拆分:一个32位地址被分为Tag(标签)、Index(索引)和Offset(块内偏移)三个字段。Index用于选择Cache中的组(Set),Offset用于定位行内字节,Tag则用于在组内比较是否命中。

映射方式 组内路数 硬件复杂度 冲突未命中概率 典型应用
直接映射 1路 低 高(多个地址映射同一行) 早期MCU,简单DSP
组相联(4路) 4路 中 中(4个候选位置) Cortex-M7 L1 Cache
全相联 全部行 高 低(任意位置存放) TLB,小型缓存

以Cortex-M7的D-Cache为例,假设Cache总容量为32KB,行大小为32字节,4路组相联。则总行数为1024行,每组包含4行,因此共有256组。地址中的Index字段需要8位(2^8=256),Offset字段需要5位(2^5=32),剩余19位为Tag。当CPU访问某个地址时,硬件先提取Index定位到具体的组,然后并行比较组内4个Tag是否与地址中的Tag匹配。若匹配,则命中;否则未命中,需要从主存加载数据到该组中某个空闲行,若组内已满则按替换策略(通常是LRU,最近最少使用)淘汰一行。

工程启示:在编写Cortex-M7代码时,应尽量避免多个热点数据地址的Index字段相同但Tag不同,否则会造成冲突未命中。例如,两个大小为8KB的数组若在内存中间隔2KB对齐,它们可能映射到相同的Cache组,导致频繁替换。通过调整数组的起始地址偏移(如增加padding)可以缓解这一问题。

4.2 行大小与空间局部性

Cache行(Line)是数据在Cache与主存之间传输的最小单位。Cortex-M7的Cache行大小为32字节,这意味着即使CPU只访问一个字节,Cache也会一次性加载32字节到行内。行大小的选择直接影响两个关键指标:空间局部性利用效率和带宽浪费率。

  • 顺序访问场景:当代码顺序遍历数组时,32字节行能一次加载8个32位整数,后续7次访问全部命中,效率极高。
  • 随机访问场景:若频繁跳转访问不同地址,每个32字节行可能只用到其中4字节,导致Cache有效利用率仅12.5%,同时浪费了总线带宽。
  • 伪共享问题:在多任务或中断与主循环共享数据时,若两个变量位于同一Cache行且被不同执行上下文修改,会产生行颠簸(Ping-Pong),即使它们逻辑上无关。

在Cortex-M7中,行大小是硬件固定的,无法软件配置。但开发者可以通过数据结构布局优化来适配行大小:将频繁一起访问的字段放入同一结构体并保证其起始地址按32字节对齐;将只读数据与读写数据分离到不同内存区域,避免写操作导致整个行被标记为脏。

4.3 命中/未命中判定与替换策略

Cache命中的判定过程是硬件自动完成的,但理解其细节有助于性能调优。当CPU发起一次内存访问时,Cache控制器执行以下步骤:

  1. 地址拆分:提取Index、Tag和Offset字段。
  2. 组选择:使用Index定位到Cache中的唯一一组。
  3. Tag比较:将该组内所有路(4路)的Tag与地址Tag同时比较。
  4. 命中判定:若任一Tag匹配且有效位为1,则命中,根据Offset取出对应字节/字返回CPU。
  5. 未命中处理:若所有Tag均不匹配,则产生一次Cache Miss,硬件从主存加载整个32字节行到Cache中,同时将请求的数据返回CPU(Critical Word First优化)。

当组内4路全部被占用且发生未命中时,需要选择一路进行替换。Cortex-M7采用伪LRU(Pseudo-LRU)算法,为每组维护一个状态位来近似追踪最近最少使用的行。相比真LRU,伪LRU硬件开销小,性能接近。对于实时系统,替换策略的确定性比最优更重要,伪LRU提供了可预测的行为。

关键风险:在中断服务程序(ISR)中访问大量数据时,若ISR的数据集大于Cache容量,会导致反复的未命中与替换,产生抖动(Thrashing)。此时应考虑将ISR关键数据放入TCM(紧耦合内存),或使用CMSIS-DSP库中的缓存操作函数进行预取。

4.4 写策略:写回与写通

写策略决定了CPU执行写操作时数据如何同步到主存。Cortex-M7的D-Cache支持两种策略,可通过MPU(内存保护单元)的Region属性按内存区域配置:Write-Back(写回)和Write-Through(写通)。

特性 Write-Back(写回) Write-Through(写通)
写操作行为 仅更新Cache行,标记脏位 同时更新Cache和主存
主存更新时机 行被替换或显式Clean时 每次写操作立即更新
写带宽消耗 低(合并多次写) 高(每次写都占用总线)
数据一致性风险 高(需软件维护) 低(主存始终最新)
典型应用 大块数据缓冲区、DMA区域 外设寄存器、共享标志位

在Cortex-M7中,默认的D-Cache策略为Write-Back。对于普通SRAM数据,Write-Back能显著减少总线访问次数。例如,一个算法反复更新一个256字节的数组,Write-Back模式下,所有写操作先进入Cache,仅当数组所在行被替换时才写回主存,可能只需写回8次(256/32)。而Write-Through模式下,每次写操作都会触发一次主存写,共256次,总线压力增加32倍。

然而,Write-Back引入了数据一致性问题。当DMA外设直接读写内存时,Cache中的脏数据尚未写回主存,DMA可能读到旧数据;反之,DMA写入主存后,Cache中可能仍保留旧值。解决方法是使用CMSIS提供的缓存维护函数:SCB_CleanDCache()(写回脏数据)、SCB_InvalidateDCache()(丢弃Cache行,强制下次从主存加载)、SCB_CleanAndInvalidateDCache()(先写回再失效)。

4.5 案例拆解:音频采集缓冲区的Cache配置

假设一个Cortex-M7音频系统,I2S外设通过DMA将麦克风采样数据(每帧128字节)写入内存缓冲区,CPU需要对该缓冲区进行FIR滤波处理。初始配置将所有内存区域设为Write-Back,导致以下问题:

  • 现象:音频出现间歇性爆音,DMA读取到的数据有时为全零或旧数据。
  • 根因分析:DMA写入主存后,CPU的D-Cache中仍保留旧数据。CPU读取时命中Cache,得到过时采样值。同时,CPU写回滤波结果时,DMA可能尚未完成读取,造成数据覆盖。

解决方案:将音频缓冲区所在内存区域配置为Write-Through,并禁止Cache分配(通过MPU设置TEX=0, C=0, B=1)。这样DMA写入主存后,CPU读取时直接访问主存(或Cache未命中后加载新数据),保证一致性。对于滤波系数等只读数据,保留Write-Back以利用Cache加速。

// MPU配置示例:将0x20000000区域设为Write-Through
MPU->RBAR = (0x20000000 & 0xFFFFFFE0) | MPU_RBAR_VALID | 0;  // Region 0
MPU->RASR = (0 << 0)    |  // Enable
            (0 << 1)    |  // Disable execution
            (0 << 3)    |  // Full access
            (1 << 5)    |  // Shareable
            (0 << 8)    |  // TEX=0
            (1 << 10)   |  // C=1 (Write-Through)
            (0 << 11)   |  // B=0
            (24 << 12)  |  // 16MB size
            (1 << 16);     // Enable region

结果对比:修改后爆音消失,CPU读取DMA数据始终为最新值。代价是写操作带宽增加,但音频缓冲区写入频率较低(每帧一次),影响可忽略。若缓冲区较大且写入频繁,可改用Write-Back + 手动Clean/Invalidate,但需在DMA传输前后调用缓存维护函数,增加代码复杂度。

经验总结:在Cortex-M7上,外设与CPU共享的数据区域优先使用Write-Through或Non-Cacheable;纯CPU内部计算数据使用Write-Back;DMA描述符建议放在Non-Cacheable区域或使用内存屏障。每次修改MPU配置后,需执行__DSB()和__ISB()确保配置生效。

4.6 常见误区与复盘

在实际项目中,工程师常对Cache产生以下误解,导致性能不升反降:

  • 误区一:Cache越大越好。Cortex-M7的Cache容量固定(通常32KB I-Cache + 32KB D-Cache),但若数据访问模式为随机且数据集远超Cache容量,命中率会急剧下降。此时应使用TCM或DMA双缓冲,而非依赖Cache。
  • 误区二:Write-Through一定比Write-Back慢。对于写频繁且数据量小的场景,Write-Through避免了脏行替换时的批量写回延迟,反而更可预测。实时系统更看重确定性,而非平均性能。
  • 误区三:忽略Cache行对齐。若一个关键结构体跨越两个Cache行,访问它可能需要两次行加载。将结构体按__ALIGNED(32)对齐可确保单行访问。
  • 误区四:在中断中使用Cache维护函数。SCB_CleanDCache()会阻塞CPU直到操作完成,在ISR中调用会破坏实时性。应设计为在后台任务或主循环中处理缓存同步。

复盘清单:每次优化Cache前,先回答四个问题:① 数据是只读还是读写?② 访问模式是顺序还是随机?③ 数据是否与外设共享?④ 实时性要求是否允许Cache Miss的不可预测延迟?通过这四个问题的答案,才能正确选择映射方式(硬件固定)、写策略(MPU配置)和数据结构布局。