4. ARMv7 内存模型:内存类型、属性与序模型

内存模型这东西,听起来很抽象。但说白了,它决定了你的 CPU 怎么看待外面的世界——也就是内存和外设。我早年做第一块 Cortex-A8 板子时,就因为搞混了 Device 和 Strongly Ordered,导致 DMA 传数据死活不对。嗯,从那以后我再也不敢小看这一章了。

ARMv7 的内存模型,核心就三件事:内存类型、内存属性、内存序模型。你把这三点理清了,MMU 配置就成功了一半。

4.1 三种内存类型

ARMv7 把整个地址空间分成三大类。每种类型的行为完全不同,尤其是对缓存和访问顺序的处理。

内存类型 典型用途 缓存支持 访问序保证
Normal DDR、Flash、普通 RAM 可 Cacheable + Bufferable 弱序,可重排
Device 内存映射外设、FIFO、控制寄存器 不可 Cacheable,可 Bufferable 强序,但允许合并写
Strongly Ordered 中断控制器、系统控制协处理器 不可 Cacheable,不可 Bufferable 严格序,完全保序

4.1.1 Normal 内存

Normal 内存,就是咱们最常见的 DDR 和 Flash。它的特点是:CPU 可以随便缓存、随便乱序执行、甚至可以预取。你想想看,如果每次读内存都要等几百个周期,那性能就完蛋了。所以 Normal 内存给了 CPU 最大的优化空间。

我个人习惯把 Normal 内存再细分成两种场景:

  • Normal 可缓存:代码段、数据段、堆栈。性能敏感,必须开缓存。
  • Normal 不可缓存:某些调试缓冲区、共享内存。避免缓存一致性问题。
经验之谈: 我在项目中遇到过,两个核共享一段 Normal 内存做通信,结果一个核写了数据,另一个核死活读不到新值。原因就是写缓存没刷出去。解决办法:要么用 Non-cacheable 映射,要么手动执行 clean 和 invalidate 操作。

4.1.2 Device 内存

Device 内存,说白了就是外设的寄存器空间。比如 UART、GPIO、I2C 的控制寄存器。这类内存有个特点:读或写一次,可能就改变了硬件状态。你不能让 CPU 随便缓存它,否则写一次寄存器,结果被缓存了没发出去,外设就罢工了。

Device 内存的访问规则:

  • 不可 Cacheable(绝对不能缓存)
  • 可以 Bufferable(允许写缓冲合并)
  • 访问序:同一地址的访问保持顺序,不同地址之间可能重排
注意: Device 内存允许写合并。也就是说,如果你连续写两个 32 位寄存器,硬件可能把它们合并成一个 64 位写操作。这对某些外设是致命的。我曾经调试一个网卡驱动,就是因为写描述符时被合并了,导致控制字不对。解决办法:用 Strongly Ordered 或者加内存屏障。

4.1.3 Strongly Ordered 内存

Strongly Ordered 是最严格的内存类型。它既不能缓存,也不能缓冲。每次访问都直接打到总线上,而且严格保序。说白了,你写 A 再写 B,硬件保证 A 一定在 B 之前完成。

什么时候用?

  • 中断控制器(GIC)的寄存器
  • 系统控制协处理器(CP15)
  • 跨核同步用的标志位
  • 任何对时序极其敏感的外设

代价呢?性能极差。每次访问都要等总线事务完成,CPU 被 stall 住。所以别滥用,只在必要的地方用。

4.2 内存属性:Cacheable 与 Bufferable

这两个属性,是 MMU 页表描述符里的两个比特位。但它们决定了整个系统的行为。

属性 含义 影响
Cacheable 允许数据进入 CPU 缓存 读命中快,但存在一致性问题
Bufferable 允许写操作在写缓冲中合并 写性能提升,但可能延迟到达外设

组合起来有四种情况:

  • Cacheable + Bufferable:Normal 内存的标配。性能最好。
  • Cacheable + Non-bufferable:很少用。缓存了但写不缓冲,每次写都直接发出去。
  • Non-cacheable + Bufferable:Device 内存的典型配置。不缓存,但允许写合并。
  • Non-cacheable + Non-bufferable:Strongly Ordered 的配置。最慢,但最安全。
核心原则: 对于 Normal 内存,尽量开 Cacheable 和 Bufferable。对于外设,除非你非常清楚自己在做什么,否则不要开 Cacheable。Bufferable 可以酌情开,但要小心写合并。

4.3 内存序模型

内存序,就是 CPU 访问内存的顺序。ARMv7 是弱序模型。什么意思?就是 CPU 和编译器可以自由重排内存访问,只要不影响单线程的正确性。这对性能是好事,但对多核编程和外设驱动是噩梦。

三种序模型对比:

模型 架构 重排自由度 编程难度
强序 x86 低 低
弱序 ARM、PowerPC 高 高
严格序 Strongly Ordered 区域 无 低

为什么会这样?ARM 的设计哲学是:把重排的自由留给硬件,把同步的责任交给软件。你想想看,如果每次内存访问都严格保序,那 CPU 的乱序执行、写缓冲、预取就全废了。性能损失至少 30%。

那怎么保证顺序?用内存屏障指令:

  • DMB(数据内存屏障):保证屏障前的所有内存访问在屏障后的访问之前完成。
  • DSB(数据同步屏障):比 DMB 更严格,等待所有缓存、写缓冲、TLB 操作都完成。
  • ISB(指令同步屏障):刷新流水线,保证之后的指令都从内存或缓存重新取。
我的习惯: 写外设驱动时,在每次配置寄存器序列之后,加一条 DSB。虽然性能有点损失,但能避免很多诡异的 bug。尤其是 DMA 描述符的更新,一定要用 DSB 保证描述符真的写到了内存,再通知 DMA 引擎。

4.4 知识结构总览

下面这张图,把内存类型、属性和序模型的关系串起来了。你可以把它当作配置 MMU 时的速查表。

ARMv7 内存模型知识结构 ARMv7 内存模型核心结构 内存类型 内存属性 内存序模型 Normal Device Strongly Ordered Cacheable Bufferable 典型组合与使用场景 • Normal + Cacheable + Bufferable → DDR、Flash(高性能) • Device + Non-cacheable + Bufferable → 外设寄存器(允许写合并) • Strongly Ordered + Non-cacheable + Non-bufferable → 中断控制器、系统控制 内存序:Normal 弱序(可重排) → Device 强序(保序但可合并) → Strongly Ordered 严格序(完全保序) 提示:配置 MMU 时,先确定地址空间用途,再选类型,最后定属性。序模型由类型自动决定。

4.5 避坑指南

最后,分享几个我踩过的坑:

  • 别把外设配成 Normal 内存:我见过有人把 UART 寄存器配成 Normal + Cacheable,结果写一个字符,缓存没刷,串口半天没反应。
  • 共享内存慎用 Bufferable:两个核共享一个标志位,如果开了 Bufferable,写操作可能被缓冲,另一个核读到的永远是旧值。
  • 中断服务程序里用 DSB:在 ISR 里访问外设寄存器前,加一条 DSB,保证之前的所有内存操作都已完成。否则可能读到脏数据。
  • DMA 描述符用 Non-cacheable:或者用 cache 但手动 clean。否则 CPU 更新了描述符,DMA 看到的还是缓存里的旧数据。
血的教训: 我曾经在一个项目里,把 LCD 的帧缓冲配成了 Normal + Cacheable,结果屏幕显示出现随机条纹。查了三天,最后发现是 CPU 写缓存后没刷,DMA 控制器读到了过时的数据。解决方案:帧缓冲用 Non-cacheable,或者每次更新后执行 clean 操作。