4. Cache基础原理
在深入ARM Cortex-M7的Cache优化之前,必须先建立扎实的Cache基础理论。Cache的本质是位于CPU与主存储器(Flash/SRAM)之间的高速小容量存储阵列,它利用程序的时间局部性与空间局部性,将近期可能被访问的数据提前搬运到离CPU更近的位置。Cortex-M7的L1 Cache分为I-Cache(指令)与D-Cache(数据),其行为直接决定了实时控制与信号处理代码的最终执行效率。本节将围绕映射方式、行大小、命中/未命中判定以及写策略四个核心维度展开,为后续TCM配置与内存带宽优化奠定概念基础。
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,最近最少使用)淘汰一行。
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控制器执行以下步骤:
- 地址拆分:提取Index、Tag和Offset字段。
- 组选择:使用Index定位到Cache中的唯一一组。
- Tag比较:将该组内所有路(4路)的Tag与地址Tag同时比较。
- 命中判定:若任一Tag匹配且有效位为1,则命中,根据Offset取出对应字节/字返回CPU。
- 未命中处理:若所有Tag均不匹配,则产生一次Cache Miss,硬件从主存加载整个32字节行到Cache中,同时将请求的数据返回CPU(Critical Word First优化)。
当组内4路全部被占用且发生未命中时,需要选择一路进行替换。Cortex-M7采用伪LRU(Pseudo-LRU)算法,为每组维护一个状态位来近似追踪最近最少使用的行。相比真LRU,伪LRU硬件开销小,性能接近。对于实时系统,替换策略的确定性比最优更重要,伪LRU提供了可预测的行为。
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传输前后调用缓存维护函数,增加代码复杂度。
__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配置)和数据结构布局。