内存模型基础:ARMv7-M/ARMv8-M内存模型

做多核调试,绕不开内存模型。说白了,就是CPU怎么看待你写的地址、怎么安排读写顺序。我见过不少工程师,缓存一致性调了几个月没搞定,最后发现是内存类型配错了。嗯,咱们今天就把这个根基打牢。

ARMv7-M vs ARMv8-M:核心差异

先看两张架构的对比。ARMv7-M是Cortex-M3/M4/M7用的,ARMv8-M是Cortex-M23/M33/M55用的。我刚开始接触M55时,以为内存模型差不多,结果踩了个坑——M55多了个EL(异常等级)的概念,内存属性跟异常等级绑定了。

特性 ARMv7-M ARMv8-M
异常模型 Handler/Thread模式 EL0 ~ EL3 四级异常等级
内存属性 MPU区域属性 MPU + 系统寄存器联合控制
原子操作 LDREX/STREX LDREX/STREX + 新增原子指令
缓存支持 可选(M7有L1) 可选(M55有L1)

我个人习惯,先看芯片是哪个架构,再决定内存模型怎么配。M7和M55虽然都有缓存,但M55多了TrustZone,安全区和非安全区的内存属性可能不一样。

三种内存类型:Normal, Device, Strongly-Ordered

ARM把内存分成三大类。你想想看,CPU访问内存时,能不能合并、能不能重排、要不要立即生效——全看这个分类。

1. Normal Memory(普通内存)

最常见的内存类型。SRAM、Flash、DDR都属于这一类。CPU可以对Normal内存做合并写、推测读、乱序执行。说白了,性能最好,但一致性最麻烦。

关键属性:

  • 可缓存(Cacheable)或不可缓存(Non-cacheable)
  • 可合并写(Write-Combine)
  • 允许推测访问(Speculative Access)

我在项目中遇到过,两个核共享一个Normal内存的变量,一个写一个读,结果读到的全是旧值。为什么?因为写操作被缓存在L1里,还没刷出去。这就是缓存一致性问题。

2. Device Memory(设备内存)

外设寄存器、控制寄存器用这个类型。CPU不能对Device内存做推测访问,也不能合并写。每次读写都必须“真的”访问总线。

子类型 行为 典型场景
Device-nGRE 不合并、不重排、不推测 普通外设寄存器
Device-nRE 不合并、允许重排读、不推测 FIFO类外设
Device-nWE 不合并、允许重排写、不推测 写缓冲类外设

避坑指南:我曾经把UART的数据寄存器配成Normal类型,结果CPU把两次写操作合并成一次32位写,UART只收到了高16位。从那以后,外设寄存器我全部用Device类型。

3. Strongly-Ordered Memory(强序内存)

最严格的内存类型。所有访问严格按程序顺序执行,不允许任何重排、合并、推测。性能最差,但行为最可预测。

什么时候用?我一般只在多核同步、电源管理、中断控制器这些地方用。比如两个核要同步状态,用Strongly-Ordered保证写操作立即生效,不需要加DMB指令。

内存属性与访问顺序

光知道类型还不够,还得理解内存属性怎么影响访问顺序。ARMv7-M和ARMv8-M都支持MPU(内存保护单元),每个区域可以独立配置属性。

核心属性位

  • TEX[2:0]:类型扩展,决定缓存策略
  • C:Cacheable位,是否允许缓存
  • B:Bufferable位,是否允许写缓冲
  • S:Shareable位,是否共享
  • XN:Execute Never,禁止执行

举个例子,M7的MPU配置一个区域为Normal、可缓存、可缓冲、共享:

// MPU区域配置示例:Normal, Cacheable, Bufferable, Shareable
MPU->RNR  = 0;                    // 区域0
MPU->RBAR = 0x20000000;           // 基地址
MPU->RASR = (0x03 << MPU_RASR_TEX_Pos) |  // TEX=011
            (1    << MPU_RASR_C_Pos)   |  // C=1
            (1    << MPU_RASR_B_Pos)   |  // B=1
            (1    << MPU_RASR_S_Pos)   |  // S=1
            (0x1F << MPU_RASR_SIZE_Pos)|  // 大小
            (1    << MPU_RASR_ENABLE_Pos); // 使能

访问顺序规则

ARM内存模型定义了观察者(Observer)的概念。一个写操作什么时候能被另一个观察者看到?规则如下:

  1. 同类型内存:Normal对Normal,顺序由内存类型决定
  2. 不同类型:Strongly-Ordered > Device > Normal(优先级从高到低)
  3. 屏障指令:DMB、DSB、ISB可以强制排序

我的经验:调试多核时,如果发现数据不一致,先检查内存类型配对了没有。我遇到过最诡异的一次,两个核共享一个标志位,一个写一个读,读到的值总是延迟一拍。最后发现是标志位所在的区域配成了Normal Non-cacheable,但写缓冲没关。改成Device类型就解决了。

知识体系图:内存模型核心逻辑

下面这张图把内存类型、属性、访问顺序的关系串起来了。你调试时遇到问题,可以按这个逻辑排查。

ARMv7-M/ARMv8-M 内存模型核心逻辑 Normal Memory Device Memory Strongly-Ordered 属性:C/B/S/TEX 可缓存 | 可缓冲 | 共享 允许推测/合并 属性:nGRE/nRE/nWE 不合并 | 不推测 部分允许重排 属性:严格顺序 不合并 | 不推测 不允许任何重排 访问顺序规则 同类型内存:按内存类型自身规则排序 不同类型:Strongly-Ordered > Device > Normal(优先级从高到低) 屏障指令:DMB / DSB / ISB 可强制排序 多核调试要点 1. 检查MPU区域内存类型配置 2. 确认共享属性(Shareable)是否开启 3. 必要时添加DMB/DSB屏障指令

实际调试中的坑

讲几个我踩过的坑,你遇到了可以少走弯路。

坑1:Normal内存的推测读

我曾经在M7上调试一个双核共享缓冲区,一个核写数据,另一个核读。读核总是读到一些“不可能”的值。后来发现,CPU对Normal内存做了推测读,提前把还没写好的数据读进了缓存。解决方案:把共享缓冲区配成Device类型,或者加DMB指令。

坑2:写合并导致外设异常

一个工程师找我帮忙,说他的SPI DMA传输总是不对。我一看代码,SPI的TX寄存器地址配成了Normal内存。CPU把两次8位写合并成一次16位写,SPI只认低8位。改成Device-nGRE就好了。

我的建议:刚开始做多核项目时,先把所有共享内存配成Device类型。等系统跑稳了,再根据性能需求逐步改成Normal。这样能避免90%的一致性bug。

小结

内存模型是缓存一致性的地基。Normal、Device、Strongly-Ordered三种类型,决定了CPU怎么访问、怎么排序。配错了,轻则性能下降,重则数据错乱。我每次做新项目,第一件事就是打开芯片手册,确认每个外设和内存区域该用什么类型。嗯,这个习惯帮我省了不少调试时间。