ARM Cache 架构详解

全面解析 ARM 缓存的底层架构演进、核心运行策略,以及在系统启动与多核 DMA 场景下的硬件维护操作。

1. 为什么需要缓存?

ARM 架构诞生之初,处理器时钟与内存访问速度大致相当。但几十年来 CPU 频率飙升了数个数量级,外部总线和 DRAM 却没有跟上——在 ARM 系统中,访问外部内存可能花费数十甚至上百个核心周期

缓存是一块小而快的片上 SRAM,位于核心与主存之间,保存主存数据的临时副本。它之所以有效,是因为程序的访存模式具有两个特征:

  • 时间局部性:代码中的循环和反复调用的函数,短时间内同一地址会被多次访问
  • 空间局部性:栈帧、数组等数据结构,相邻地址大概率被连续访问

手册定义:缓存查找时地址匹配且 Valid Bit 置位即为 命中(Hit),否则为 缺失(Miss)。首次访问触发 cache linefill 将数据从主存搬入缓存,后续命中则避开慢速外部总线——缓存本身不加速首次访问,但大幅提升后续访问速度。

写缓冲器是另一种隐藏延迟的机制。核心执行 store 指令时,将地址、数据和控制信息推入一组硬件缓冲器中即可继续执行下一条指令,无需等待慢速主存完成实际写入。这种”fire and forget”模式让核心不必因每次 store 而停顿。某些写缓冲器还支持写合并——将连续的字节写入合并为单次突发传输,进一步减少总线流量。

1.1 内存层级与缓存的代价

引入缓存并不是没有代价的。原书 §9.2 明确指出了缓存带来的三大挑战:

  1. 执行时间不可预测:Hit 和 Miss 的访问时间天差地别。这对于硬实时(Hard Real-time)系统是毁灭性的,这也是某些控制系统更倾向于使用 TCM(紧耦合内存)而非缓存的原因。
  2. 功耗增加:Tag 比较和缓存行填充需要消耗额外能量。
  3. 一致性噩梦:一旦存在多核或 DMA,就需要一套复杂的机制来保证大家看到的数据是一致的。

为了平衡性能和代价,现代 ARM 处理器采用了多级缓存架构哈佛架构(Harvard architecture)

  • L1 缓存:紧贴核心,指令和数据独立(哈佛架构),避免取指和访存的结构性寻址冲突,追求极致命中速度。
  • L2 缓存:通常为多个核心共享的统一缓存(指令数据混合,冯·诺依曼架构),容量更大,用于捕获 L1 溢出的数据,最大程度减少对外部慢速总线的访问。

1

理解了缓存的宏观代价与分级,接下来剖析它内部是怎么组织的。


2. 缓存的基础架构

理解了”为什么需要缓存”之后,下一个问题是”缓存内部怎么工作”。本节从 Cache Line、Tag、Index 三个核心概念出发,对比直接映射和组相联两种组织方式,解释为什么前者会被 ARM 主缓存弃用。

2

2.1 核心概念

缓存存储三样东西:地址标签(Tag)、数据(Data)、状态信息。32 位地址被划分为三个字段:

1
2
Address:  [         Tag         |  Index  | Offset ]
31 N N-1 0
术语 含义 存储位置
Tag 地址的高位部分,标识缓存行对应主存中的哪个区域 存储在缓存 Tag RAM 中
Index 地址中段位,选择缓存内的哪一行(用作缓存 RAM 的地址) 不额外存储
Offset 地址低位,在缓存行内部选择具体字节或字(通常分为 Word Offset 和 Byte Offset) 不额外存储
Cache Line 缓存的最小加载单元(通常 8 或 16 个字) 连续数据块
Valid Bit 标记此行数据是否有效 每行一个
Dirty Bit 标记此行是否被写入过(与主存内容不一致) 仅在数据缓存中出现
Way 组相联缓存中的并列副本路径 每条 way 有独立的 Tag RAM
Set 同一 Index 值在所有 Way 中对应的缓存行集合

实战陷阱:伪共享(False Sharing)
Cache Line 是缓存加载的最小单位。即使代码只读写一个 32-bit 的变量,硬件也会把包含它在内的整条 64B Cache Line 全拉进缓存。在多核场景下,如果两个毫无关联的并发变量刚好被编译器塞在同一个 Cache Line 里,当两个核心分别高频修改这两个变量时,会导致这条 Cache Line 在两个核之间疯狂地来回踢出和同步。这类性能黑洞被称为”伪共享”。系统级解法通常是对关键结构体加上 __attribute__((aligned(64))) 强制每个变量独占一行。

缓存大小仅计算数据 RAM 的容量——Tag RAM 占用的物理面积不包括在内,但实际上 Tag 存储是不可忽略的硅开销。

2.2 直接映射缓存:极简但危险

直接映射是最简单的缓存实现——每个主存地址固定映射到缓存中的一个特定行。缺点显而易见:不同地址若具有相同的 Index 字段,便会争夺同一行。

3

4

实例:thrashing 陷阱

考虑一个小型直接映射缓存:每行 4 字(Offset 需要 2 位,bit[3:2]),共 4 行(Index 需要 2 位,bit[5:4])。地址 0x000x400x80 的 bit[5:4] 全为 0x00——它们映射到同一行:

1
2
3
4
void add_array(int *data1, int *data2, int *result, int size) {
for (int i = 0; i < size; i++)
result[i] = data1[i] + data2[i];
}

result=0x00, data1=0x40, data2=0x80,每次迭代中三个指针交替访问同一缓存行,导致每个访问都互相踢出对方的数据——无一次命中,程序在”加载-踢出-加载-踢出”的恶性循环中性能崩塌。

手册指出直接映射缓存因此不在 ARM 核心主缓存中使用(仅在分支目标地址缓存等特殊场景中出现)。

2.3 组相联缓存:用硅面积换确定性

组相联缓存将缓存划分为多个Way(典型 L1 为 2 或 4 way)。一个地址的 Index 字段在每条 Way 中选取一行,这些对应行构成一个 Set。查找时硬件并行比较 Set 内所有 Way 的 Tag——只要任一 Way 的 Tag 匹配且 Valid,即为命中。

回到上面的 thrashing 实例:在 2-way 组相联缓存中,0x000x400x80 可以同时存在于 Set 0 的两个 Way 中(第三个地址才需要替换)。thrashing 概率从必然降到了接近零。

5

手册指出:对于 L1 缓存,超过 4-way 组相联后性能提升微乎其微;对于更大的 L2 缓存(如 L2C-310),8-way 或 16-way 关联度更有价值。全相联缓存(任意地址可映射到任意行)在理论上最优,但因硬件复杂度过高,只在实际中用于 MMU TLB 等极小容量场景。

理论讲清后,下面把参数代入 ARM 的具体实现。


3. Cortex-A 处理器的缓存实现

抽象的缓存原理讲完了,本节将其代入 ARM 的具体实现:以 Cortex-A7/A9 的 32KB 4-way 数据缓存为例,逐字段分解地址划分;同时追踪 ARM 缓存从 VIVT 到 PIPT 的标签机制演进,并给出全系列 Cortex-A 处理器的缓存参数对照。

6

3.1 实标缓存结构解析

手册给出了一个真实的例子:Cortex-A7 或 Cortex-A9 的 32KB 4-way 组相联数据缓存,每行 8 字(32 字节)。

地址划分过程:

1
2
3
32KB ÷ 4 way ÷ 32 byte/line = 256 line/way  →  Index 需要 8 bit (bit[12:5])
每行 8 字 → Offset 需要 3 bit (bit[4:2])
剩余高位 → Tag 占用 bit[31:13]
1
2
Address: [    Tag 19-bit   | Index 8-bit | Offset 3-bit | Byte 2-bit ]
31 13 12 5 4 2 1 0

如果在每条 Way 中有 256 行、每个 Tag 是 19 位,加上 Valid 和 Dirty 位,Tag RAM 的开销相当可观——但这是实现高效缓存命中检测的必要代价。

3.2 缓存控制器的命中流程

缓存控制器收到核心的访存请求后执行以下流程:

  1. 用地址的 Index 位选择 Set 内所有 Way 的缓存行
  2. 并行读出所有 Way 的 Tag 值
  3. 与请求地址的 Tag 位并行比较
  4. 任一匹配且 Valid → Hit:从对应行的 Offset 位置取出数据
  5. 全部不匹配 → Miss:触发 cache linefill,将主存对应行读入

linefill 期间核心不需要等到整行填充完毕——控制器优先获取包含请求数据的关键字(critical word),先送入核心流水线,其余数据后台填充。

3.3 VIVT → VIPT → PIPT:用虚拟还是物理地址?

缓存查找就像查字典,分两步:先翻到对应的那一页(Index 找行),再看字条对不对得上(Tag 身份核对)。在引入 MMU(虚拟内存)后,找行和核对身份是用 CPU 看到的“虚拟地址(V)”,还是真实内存的“物理地址(P)”?这就有了三种演进方案:

方案 Index(选行) Tag(核对) 适用场景 / 代表处理器 核心优缺点
VIVT Virtual (虚拟) Virtual (虚拟) 早期处理器(ARM9) 速度最快,但切换进程时必须清空全部缓存
VIPT Virtual (虚拟) Physical (物理) Cortex-A 指令缓存 速度快,但存在页染色(Page Coloring)陷阱
PIPT Physical (物理) Physical (物理) Cortex-A 数据缓存、L2 硬件最复杂,但最安全,没有别名隐患

1. VIVT:早期设计的痛点
全部用虚拟地址操作。好处是快,完全不需要等 MMU 工作。但这有一个致命缺陷:虚拟地址是进程私有的。进程 A 和进程 B 的 0x1000 虚拟地址,指向的其实是两块完全不同的物理内存。一旦 OS 切换了进程,缓存里存的旧数据对新进程来说全变成“假数据”了。因此,每次切换进程都必须强行清空整个缓存,导致系统严重卡顿。

2. VIPT:用时间差变魔术(兼顾速度与正确性)
为了不频繁清空缓存,工程师发明了 VIPT:

  • 第一步(Virtual Index 快寻址):CPU 一发出请求,直接用虚拟地址去找缓存行。不用等 MMU 翻译,速度极快。
  • 第二步(Physical Tag 验真身):在找行的同时,MMU 在旁边加班把虚拟地址翻译成物理地址。等行刚好找出来时,MMU 也翻译完了,最后用全局唯一的物理地址(Tag)做比对。这既隐藏了翻译延迟,又解决了跨进程重名的问题。
  • 隐患(页染色与别名):通常情况下,在 4KB 分页中,虚拟/物理地址的低 12 位是一模一样的。只要单路 Cache(Way)容量不超 4KB,Index 就都在这 12 位的“安全区”里。但是,如果单路 Cache 大于 4KB(例如 32KB/4路=8KB),Index 就会出圈借用 bit[12]。由于一份数据在不同进程的虚拟地址 bit[12] 和真实的物理地址 bit[12] 可能不同,会导致同一份物理数据却被缓存在了两个不同的组里(产生“别名”)。为此,OS 内存系统必须强制规避,让分配的虚拟和物理地址这一位必须相同,这就叫页染色(Page Coloring)

3. PIPT:现代处理器的终极方案
为了彻底让软件无忧,Cortex-A 的数据缓存直接采用 PIPT。老老实实等 MMU 翻译出物理地址后,再用物理地址去选行和比对。虽然这需要更强的硬件 TLB 加速,但这从根本上拔除了数据一致性和重名别名的各种隐患。

3.4 Cortex-A 系列缓存参数对照

参数 A5 A7 A8 A9 A12 A15
L1 ICache 大小 4–64KB 8–64KB 16/32KB 16/32/64KB 32/64KB 32KB
L1 DCache 大小 4–64KB 8–64KB 16/32KB 16/32/64KB 32KB 32KB
L1 ICache 结构 2-way 2-way 4-way 4-way 4-way 2-way
L1 DCache 结构 4-way 4-way 4-way 4-way 4-way 2-way
Cache Line (ICache) 32B 64B 64B 32B 64B 64B
DCache 实现方式 PIPT PIPT PIPT PIPT PIPT PIPT
ICache 实现方式 VIPT VIPT VIPT VIPT VIPT PIPT
L2 集成 外部 内部 内部 外部 内部 内部
L2 大小 128KB–1MB 0–1MB 256KB–8MB 512KB–4MB
L2 结构 8-way 8-way 16-way 16-way
错误保护 L2 ECC L2 ECC L1/L2 可选

缓存的组织参数各不相同,但所有缓存都要回答三个运行时问题:什么时候分配行?哪一行被替换?写入时如何处理一致性?


4. 缓存的三大策略

缓存的组织结构是静态的——策略是动态的:来了新数据要不要分配行?哪一行被踢出去?写入是只写缓存还是同时写内存?这三个决策分别对应分配策略、替换策略和写策略,是缓存行为的核心配置维度。

4.1 分配策略

决定缓存缺失时是否执行 linefill:

策略 读缺失 写缺失
Read Allocate 执行 linefill 直接写入下一级(不分配缓存行)
Write Allocate 执行 linefill 执行 linefill(通常配合读缺失分配,统称为 Read-Write Allocate)

当前 ARM 核心通常配合 Write-back 写策略使用 Write Allocate——写缺失时也将数据拉入缓存,后续对同一区域的写操作就能命中缓存。

4.2 替换策略

当 Set 内所有 Way 都被占用时,选择牺牲者(victim):

策略 机制 特点
Round-robin 递增计数器轮转 可预测,但某些场景性能差
Pseudo-random 伪随机递增 多数场景优于 round-robin,ARM 推荐
LRU 替换最近最少使用的行 A15 支持,理论上最优但硬件代价高

大多数 ARM 处理器同时支持 Round-robin 和 Pseudo-random。如果 victim 是 Dirty 的,必须在写入新数据前将其逐出(evict)到主存。

4.3 写策略

策略 写入目标 Dirty Bit 性能 一致性
Write-through 缓存+主存同时写入 不需要 写频繁时慢(每次写都走总线) 天然一致
Write-back 仅写入缓存 标记 Dirty 大幅减少总线写流量 需手动维护一致

Write-back 的核心机制:写入仅更新缓存行并置 Dirty 位。该行被逐出时,若 Dirty 位有效,才将整行写回主存。这对反复操作同一块内存的场景(如栈帧、循环缓冲区)效果极佳。

手册明确警告:Write-back 场景下直接 invalidate 包含 Dirty 数据的缓存行是危险的——被修改但未写回的数据会永久丢失。正确做法是 Clean(将 Dirty 行写回主存并清 Dirty 位)后再 Invalidate(清 Valid 位)

4.4 命中率与性能优化规则

手册 §9.7 给出了缓存性能的几个实用定义和优化规则。命中率定义为缓存命中次数除以总访存请求次数(通常以百分比表示)。命中率没有”典型值”——它强烈依赖于代码和数据的关键部分的大小、空间局部性以及缓存本身的大小

手册给出了几条经实践检验的性能规则:

  • 启用所有缓存和写缓冲器——这是性能的”二进制开关”:Cortex-A 系列在有缓存和无缓存之间的性能差可达 10–100 倍
  • 将频繁访问的数据放在一起——一个数组的基址如果对齐到缓存行起始位置,单次 linefill 就能覆盖多个元素
  • 顺序访问受益于缓存行为,非顺序访问(如链表)则相反——顺序扫描数组时,每次 linefill 预取的相邻元素在下几次访问中命中;遍历链表时每个节点可能位于不同的缓存行,且地址不连续导致 linefill 利用率极低
  • Thumb 代码密度悖论:手册给出了一个反直觉的例子——一段 C 代码编译为 Thumb 时可能完全装入缓存,而编译为 ARM(或”最大性能”优化)时因代码更大而无法完全装入。结果是 Thumb 版本反而跑得更快,尽管每条指令的原始性能不如 ARM。这提醒性能优化不能只看指令级——缓存行为往往决定全局结果

策略是自动执行的——但软件在特定时刻必须显式干预缓存状态。下一节正是这份”干预手册”。


5. 缓存维护操作

缓存自动管理绝大多数场景,但也存在需要软件介入的时刻——boot 初始化、DMA 数据同步、自修改代码更新。本节覆盖 Clean/Invalidate 的区别、CP15 操作指令、PoC/PoU 两个一致性概念点,以及 Set/Way 级别的遍历清除流程。

5.1 Clean 与 Invalidate

操作 作用 适用场景
Invalidate 清 Valid 位,行内数据作废 指令缓存(无 Dirty)、DMA 写入后核心读取前
Clean 将 Dirty 行写回主存,清 Dirty 位 核心写入 DMA 缓冲区后、共享数据写出
Clean + Invalidate 先 Clean 再 Invalidate 数据缓存中未知状态的行的安全清除

手册特别强调 ARM 用词约定:只说 Clean 和 Invalidate,不叫 Flush(Flush 在不同架构中文档中有歧义)。

所有操作通过 CP15 完成,仅特权代码可执行(User 模式不可用)。操作范围可以是整缓存、按 Set/Way、或按虚拟地址指定特定行。

5.2 Boot 代码中的缓存初始化

手册给出的完整初始化序列展示了标准的缓存启动流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; Step 1: 禁用 MMU 和缓存(确保已知状态)
MRC p15, 0, r1, c1, c0, 0 ; 读 SCTLR
BIC r1, r1, #1 ; 清 M bit(关 MMU)
BIC r1, r1, #(1 << 12) ; 清 I bit(关指令缓存)
BIC r1, r1, #(1 << 2) ; 清 C bit(关数据缓存及 L2)
MCR p15, 0, r1, c1, c0, 0 ; 写回 SCTLR

; Step 2: 无效化所有缓存和 TLB
MOV r0, #0
MCR p15, 0, r0, c7, c5, 0 ; ICIALLU — 无效化指令缓存
MCR p15, 0, r0, c7, c5, 6 ; BPIALL — 无效化分支预测器
MCR p15, 0, r0, c8, c7, 0 ; TLBIALL — 无效化整个统一 TLB
ISB ; 指令同步屏障

; Step 3: 启用指令缓存和分支预测
MRC p15, 0, r0, c1, c0, 0 ; 读 SCTLR
ORR r0, r0, #(1 << 12) ; 置 I bit
ORR r0, r0, #(1 << 11) ; 置 Z bit(分支预测)
MCR p15, 0, r0, c1, c0, 0 ; 写回 SCTLR

Linux 内核封装了这些操作为 __clear_cache()(定义在 arch/arm/mm/cache-v7.S),等价函数在 Android 中名为 cacheflush()

5.3 DMA 一致性场景

DMA 是缓存维护的典型使用场景:

  • 核心→DMA(核心写、DMA 读):核心写入 DMA 缓冲区后,需 Clean 数据缓存(如调用内核 API v7_dma_clean_range),将 Dirty 数据推入主存,DMA 引擎才能读到正确内容
  • DMA→核心(DMA 写、核心读):DMA 写入完成后,需 Invalidate 缓存中被 DMA 更新过的地址范围(如调用 v7_dma_inv_range),核心读时才不会拿到过时缓存

5.4 PoC 与 PoU

手册定义了 ARM 架构中两个关键一致性概念点:

概念 全称 含义 实例
PoC Point of Coherency 所有访存主体(核心、DSP、DMA)看到同一份数据的位置 通常是外部主存
PoU Point of Unification 某核心的指令缓存和数据缓存看到同一份数据的位置 通常是统一 L2 缓存

这两个概念直接决定了自修改代码的同步流程:自修改代码后,应先将修改后的代码从 D-Cache Clean 到 PoU,使新指令对取指路径可见;再 Invalidate I-Cache 中对应的缓存项,迫使 CPU 下次取指时从 PoU 重新加载最新指令,而不是继续执行缓存中的旧指令。

5.5 Set/Way 级别维护操作

手册 §9.9.1 给出了按 Set/Way 遍历清除全数据缓存的完整汇编代码。核心思路是三重嵌套循环:遍历缓存级别(读取 CLIDR 获取实现了多少级缓存)→ 遍历 Way(读 CCSIDR 获取该级最大 Way 数)→ 遍历 Set(读 CCSIDR 获取该级最大 Set 数),并对每个 (Level, Way, Set) 组合执行 DCCSW(Data Cache Clean by Set/Way)。

关键指令:

1
MCR  p15, 0, R11, c7, c10, 2    ; DCCSW — 按 Set/Way 清洗一行

此处的 R11 是通过位拼接构造的参数:
R11 = Cache_Level | (Way_Number << Way_Shift) | (Set_Index << Line_Shift)

完整汇编实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
; CLIDR : 
; 31 0
; +-------+-----+-----+-----+-----+-----+-----+---------+
; | LoC | RAZ | C7 | C6 | C5 | C4 | C3 | C2 C1 |
; +-------+-----+-----+-----+-----+-----+-----+---------+
; 30 27 26 23 ... 0
;
; Cache Type Fields (每级3 bit)


; CSSELR
; 31 4 3 2 1 0
; +--------------------------------------+---+-+-+
; | RES0 |Level|I|
; +--------------------------------------+---+-+-+
;
; 告诉 CPU:"我要查询哪一级 Cache。"

; CCSIDR
; 31 13 12 3 2 0
; +---------------------------------+--------------+------+
; | Number of Sets | Associativity| Line |
; +---------------------------------+--------------+------+
; set数-1 way数-1 line size

MRC p15, 1, R0, c0, c0, 1 ; 读 CLIDR,获取缓存级别信息
ANDS R3, R0, #0x07000000 ; 取 LoC (Level of Coherency) 字段
MOV R3, R3, LSR #23 ; 右移 23 位 → 缓存级别数,最终 R3 表示 Cache 一共有几级
BEQ Finished ; 无缓存 → 跳过
MOV R10, #0 ; R10 = 当前缓存级别 (Level),0 表示 L1, 2 表示 L2...
Loop1:
ADD R2, R10, R10, LSR #1 ; R2 = (R10 * 3) / 2 = CacheType字段位偏移(= Level × 3)
MOV R1, R0, LSR R2 ; 右移 R2 位,取该 Level 的 Cache Type 字段
AND R1, R1, #7 ; 取低 3 位 → Cache Type
CMP R1, #2 ; <2 = 无数据缓存或仅指令缓存, 等效于 if (没有Data Cache) continue;
BLT Skip
MCR p15, 2, R10, c0, c0, 0 ; 写 CSSELR → 选择当前缓存级别
ISB ; 同步 CSSELR 更改
MRC p15, 1, R1, c0, c0, 0 ; 读 CCSIDR → 获取该级缓存的拓扑
AND R2, R1, #7 ; 取 Line Length 字段 (bits [2:0])
ADD R2, R2, #4 ; +4 = log2(行字节数) 偏移
LDR R4, =0x3FF
ANDS R4, R4, R1, LSR #3 ; 取 Way 数 - 1 (右对齐)
CLZ R5, R4 ; 前导零计数 → Way 字段的位偏移
MOV R9, R4 ; R9 = Way 计数器 (递减)
Loop2:
LDR R7, =0x00007FFF
ANDS R7, R7, R1, LSR #13 ; 取 Set 数 - 1 (右对齐)
Loop3:
ORR R11, R10, R9, LSL R5 ; 拼接 Level + Way 号
ORR R11, R11, R7, LSL R2 ; 拼接 Set 号 → 完整 Set/Way 操作数
MCR p15, 0, R11, c7, c10, 2 ; DCCSW — 按 Set/Way 清洗一行
SUBS R7, R7, #1 ; 下一个 Set
BGE Loop3
SUBS R9, R9, #1 ; 下一个 Way
BGE Loop2
Skip:
ADD R10, R10, #2 ; 下一级缓存 (Level += 2)
CMP R3, R10
BGT Loop1
DSB
Finished:

注意: 在多核集群中,如果多个核心共享一个 Point of Coherency 之前的缓存,每个核都独立执行此序列会导致共享缓存被重复清空。虽然不会出错,但会徒增总线负载和功耗。

这段代码是 ARM 体系中最精妙的缓存管理序列之一。CLIDR、CSSELR、CCSIDR 三个层次的信息被逐级解析——从”系统中有几级缓存”到”当前级别的 Way 数和 Set 数”再到”逐行 DCCSW”——任何 ARMv7 实现无论缓存拓扑如何,同一段代码都能不加修改地运行。

等效的 C 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int levels = get_cache_levels();      // CLIDR

for (int level = 0; level < levels; level++) {

select_cache(level); // CSSELR

CacheInfo info = get_cache_info();// CCSIDR

for (int way = info.ways - 1; way >= 0; way--) {
for (int set = info.sets - 1; set >= 0; set--) {

clean_cache_line(level, way, set); // DCCSW
}
}
}

至此我们覆盖了缓存维护的核心操作。本章剩余三个补充主题——写缓冲的合并机制、L2 缓存控制器的外部管理接口、以及软错误的硬件防护——为缓存体系画卷补上最后几笔。


6. 写缓冲、L2 缓存控制器与 ECC

缓存维护操作之后,本章剩余内容覆盖缓存体系的三个补充维度:写缓冲器如何通过合并写入减少总线流量、L2C-310 外部缓存控制器的结构与管理接口、以及软错误对 SRAM 缓存的威胁与 ECC/Parity 的防护方案。

6.1 写缓冲器

写缓冲器接受地址、数据和控制信息,让核心无需等待写入完成就能继续执行。性能增益来自”藏起”写入延迟——只要缓冲器未满,核心就看不到内存的慢速度。但访问外设时这种行为可能不是期望的:有时你需要核心停下来等写入真正完成再进入下一步。ARM 内存排序模型通过内存类型属性来控制缓存和写缓冲如何对待特定地址区域。
与内存映射编程

Cortex-A5 和 A9 没有集成 L2 缓存,依赖外部控制器如 ARM L2C-310。L2C-310 支持最大 8MB 缓存、4–16 way 组相联,可在多个核心间共享,甚至支持按 master/way 锁定数据以管理多组件对缓存的分享。

需要特别注意:L2C-310 只是一个外部外设设备,它并不受 CP15 协处理器指令的控制!

  • L1 缓存的 Clean/Invalidate 是通过 MCR p15... 特权指令执行的。
  • 外部 L2 缓存控制器完全是内存映射(Memory-mapped)的。对其执行维护操作意味着要向特定的物理基地址偏移寄存器读写(例如对 L2X0_CLEAN_INV_WAY 写入位掩码)来实现。操作系统内核的维护代码通过轮询控制器里的状态位,或者响应控制器的中断来获知清洗动作完工。这就导致了维护内外缓存的方式在驱动层面截然不同

6.2 L2 缓存控制器

Cortex-A5 和 A9 没有集成 L2 缓存,依赖外部控制器如 ARM L2C-310。L2C-310 支持最大 8MB 缓存、4–16 way 组相联,可在多个核心间共享,甚至支持按 master/way 锁定数据以管理多组件对缓存的分享。L2C-310 仅操作物理地址,维护操作通过写其内存映射寄存器完成,核心通过轮询状态寄存器判断操作何时结束。提供了 cache sync 操作强制等待所有待处理操作完工。

6.3 ECC 与 Parity

随着晶体管缩小和电压降低,宇宙射线和 alpha 粒子引发的软错误成为现实威胁。占硅面积比例最大的 SRAM 缓存尤其脆弱——某些系统中如无保护,平均故障间隔可能以秒计。

方案 能力 典型应用
Parity 检测单 bit 错误(不能修复) Tag + Data RAM 均可实现;不匹配触发 Prefetch/Data Abort
ECC 检测多 bit 错误 + 修复单 bit 错误(修复需多周期) L2 缓存最常见;Cortex-A15 更在 L1 内实现

Parity 在每次读写时检查。Cortex-A12 和 A15 在 L2 提供 ECC,Cortex-A15 是唯一在 L1 即可选配 ECC/Parity 的处理器。

以上覆盖了 ARM 缓存体系的完整范围——从为什么需要缓存到如何手动维护缓存。以下七条是本章的核心结论。


7. 学习要点总结

从速度鸿沟到缓存组织,从策略选择到维护操作——ARM 缓存的完整知识链条可以用以下七条核心结论串联。

  1. 缓存的本质是用小容量高速 SRAM 来遮掩大容量低速 DRAM 的访问延迟。时间局部性和空间局部性是缓存有效的物理前提——随机访问模式会使任何缓存失效。

  2. 直接映射只在 TLB 和 BTAC 等极小缓存中使用。ARM 主缓存的底线是组相联——L1 典型 2/4-way,L2 可达 16-way。

  3. VIPT/PIPT 的选择是一场速度与正确性的权衡:VIPT 让 Tag 比较可以在 MMU 翻译前开始(速度快),但引入了 page coloring 约束;PIPT 没有别名问题(正确性强),但增加了硬件复杂度。Cortex-A 数据缓存全系 PIPT,指令缓存采用 VIPT。

  4. Write-back + Write-allocate 是 ARM 核心的默认组合:写缺失时分配缓存行,写入仅更新缓存标记 Dirty,逐出时才写回——这极大减少总线写流量,但带来了多核和 DMA 的一致性挑战。

  5. Clean ≠ Invalidate:Invalidate 仅清 Valid 位,若行是 Dirty 的则修改过的数据永久丢失;Clean 将 Dirty 数据写回并清 Dirty。标准安全操作是先 Clean 再 Invalidate。

  6. Boot 代码的缓存初始化三步走:禁用 MMU+缓存(回到已知起点)→ 无效化全部缓存和 TLB(清除随机上电态)→ 先开 I$ 和分支预测,后逐步开启 D$ 和 MMU。

  7. PoC 和 PoU 是理解多核和自修改代码一致性的核心概念:Clean to PoC 让所有观察者看到同一份数据;Clean to PoU + Invalidate I$ 让自修改代码的指令更新安全生效。DMA 场景下两者缺一不可。