内存模型基础: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)的概念。一个写操作什么时候能被另一个观察者看到?规则如下:
- 同类型内存:Normal对Normal,顺序由内存类型决定
- 不同类型:Strongly-Ordered > Device > Normal(优先级从高到低)
- 屏障指令:DMB、DSB、ISB可以强制排序
我的经验:调试多核时,如果发现数据不一致,先检查内存类型配对了没有。我遇到过最诡异的一次,两个核共享一个标志位,一个写一个读,读到的值总是延迟一拍。最后发现是标志位所在的区域配成了Normal Non-cacheable,但写缓冲没关。改成Device类型就解决了。
知识体系图:内存模型核心逻辑
下面这张图把内存类型、属性、访问顺序的关系串起来了。你调试时遇到问题,可以按这个逻辑排查。
实际调试中的坑
讲几个我踩过的坑,你遇到了可以少走弯路。
坑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怎么访问、怎么排序。配错了,轻则性能下降,重则数据错乱。我每次做新项目,第一件事就是打开芯片手册,确认每个外设和内存区域该用什么类型。嗯,这个习惯帮我省了不少调试时间。