第2章:内核架构基础——冯诺依曼与哈佛架构、三级流水线、寄存器组详解

各位同学,咱们今天聊聊Cortex-M内核的“骨架”。说白了,就是它怎么取指令、怎么处理数据、以及那些寄存器到底干嘛用的。这些东西是基础中的基础,但也是很多坑的源头。我当年刚接触ARM时,就在流水线问题上吃过亏,后面会跟大家细说。

2.1 冯诺依曼 vs 哈佛架构:不只是概念

先问大家一个问题:CPU取指令和读写数据,走的是同一条路,还是分开走?

这就是冯诺依曼架构和哈佛架构的核心区别。

  • 冯诺依曼架构:指令和数据共用一条总线。优点是设计简单,缺点是容易“堵车”——取指令时不能读写数据,反之亦然。这就是所谓的“冯诺依曼瓶颈”。
  • 哈佛架构:指令总线和数据总线是分开的。可以同时取指令和读写数据,效率更高。但硬件复杂度也上去了。

那么Cortex-M系列用哪种?答案是:改良的哈佛架构

为什么说“改良”?因为Cortex-M内部确实有独立的指令总线和数据总线,可以并行操作。但对外(比如访问外部存储器时),它又可能复用总线。所以它不是纯粹的哈佛架构,而是取了两种架构的优点。

关键点:Cortex-M3/M4/M7等内核,内部取指和访存是并行的。这意味着CPU在执行当前指令的同时,已经在预取下一条指令了。这对性能提升非常明显。

我个人习惯把这种架构理解成“双车道高速公路”——指令走快车道,数据走慢车道,互不干扰。但到了收费站(外部总线),可能又得合并成一个车道。嗯,这个比喻虽然不完美,但初学者容易理解。

2.2 三级流水线:取指、译码、执行

Cortex-M3/M4采用三级流水线。哪三级?

  1. 取指(Fetch):从存储器中取出指令。
  2. 译码(Decode):解析指令,生成控制信号。
  3. 执行(Execute):真正干活,比如算术运算、访存等。

你想想看,如果没有流水线,CPU得等一条指令完全执行完,才能开始下一条。有了流水线,每个时钟周期都能完成一条指令(理想情况下)。

但这里有个坑——流水线冒险

我曾经在一个项目中,用Cortex-M3做电机控制。代码里有个条件跳转指令,结果发现执行时间比预期多了好几个周期。查了半天,原来是流水线被“冲刷”了——跳转指令导致预取的指令全部作废,流水线得重新填充。

避坑指南:我曾经在写中断服务程序时,忘记考虑流水线延迟。结果中断返回后,下一条指令的执行时机比我预想的晚了几个周期。在实时性要求高的场合,这个细节会要命。

所以,写代码时尽量少用条件跳转,或者用“条件执行”指令来避免流水线冲刷。Cortex-M支持条件执行,这是ARM架构的老传统了。

2.3 寄存器组详解:R0-R15、PSR、PRIMASK

寄存器是CPU的“工作台”。Cortex-M有16个通用寄存器(R0-R15),外加几个特殊寄存器。咱们一个一个说。

2.3.1 通用寄存器 R0-R12

R0到R12是真正的通用寄存器。你可以随便用,编译器会帮你分配。但要注意:

  • R0-R3:通常用于传递参数和返回值。函数调用时,前4个参数放这里。
  • R4-R11:被调用者保存。也就是说,如果你的函数要用这些寄存器,得先保存再恢复。
  • R12:又叫IP(Intra-Procedure-call scratch register),用于过程调用时的临时数据。

我个人习惯在写汇编时,把频繁使用的变量放在R4-R7里。因为R0-R3太容易被函数调用冲掉了。

2.3.2 R13:堆栈指针 SP

R13就是堆栈指针。但Cortex-M有两个堆栈指针:

  • MSP(主堆栈指针):复位后默认使用。中断处理也用它。
  • PSP(进程堆栈指针):用于任务级代码,常见于RTOS。

你当前用的是哪个,取决于CONTROL寄存器的设置。嗯,这里要注意:中断发生时,硬件会自动压栈,用的是当前SP。如果你在RTOS里切换了PSP,中断一来,硬件会切换到MSP。这个机制很巧妙,但初学时容易搞混。

2.3.3 R14:链接寄存器 LR

LR保存函数返回地址。执行BL指令时,硬件自动把下一条指令的地址存入LR。函数结束时,用BX LR就能返回。

但有个特殊情况——中断。进入中断时,LR会被设置成一个特殊值(EXC_RETURN),指示中断返回后的模式。我曾经在调试时,看到LR的值是0xFFFFFFF9,愣了半天才反应过来这是中断返回标记。

2.3.4 R15:程序计数器 PC

PC指向当前正在执行的指令地址。注意,Cortex-M的PC最低位必须是1(表示Thumb状态)。如果你不小心把PC设成了偶数,会触发异常。

小技巧:调试时,查看PC值可以快速定位程序跑飞的位置。我习惯在HardFault_Handler里打印PC和LR,基本能还原出错的上下文。

2.3.5 特殊寄存器:PSR、PRIMASK等

除了R0-R15,还有几个特殊寄存器,它们不直接参与运算,但控制着CPU的行为。

寄存器 全称 作用
PSR 程序状态寄存器 包含条件标志(N/Z/C/V)、中断号、Thumb状态等
PRIMASK 优先级屏蔽寄存器 置1后,屏蔽所有可屏蔽中断(除了NMI和HardFault)
FAULTMASK 错误屏蔽寄存器 置1后,连HardFault都屏蔽(慎用!)
BASEPRI 基础优先级寄存器 屏蔽低于指定优先级的中断
CONTROL 控制寄存器 选择堆栈指针(MSP/PSP)、选择特权级别

PSR里有个叫“ICI/IT”的位域,用于支持“如果-则-否则”指令块。这个在Cortex-M3/M4里很实用,可以避免分支预测失败。我写状态机时特别喜欢用IT指令,能让代码更紧凑。

PRIMASK是我用得最多的一个。在临界区保护时,直接写:

__disable_irq();   // 设置PRIMASK=1
// 临界区代码
__enable_irq();    // 清除PRIMASK=0

但要注意:PRIMASK屏蔽的是所有可屏蔽中断。如果你的系统里有关键中断(比如系统滴答定时器),用BASEPRI会更精细——只屏蔽优先级低于某个值的中断。

总结一下:寄存器组是Cortex-M编程的“基本功”。R0-R15是干活的主力,PSR告诉你当前状态,PRIMASK/BASEPRI是中断控制的“开关”。把这些搞清楚了,写底层代码心里就有底了。

好了,这一章就到这里。下一章咱们聊聊异常和中断——这可是Cortex-M的看家本领。到时候我会分享一个我调试中断优先级时遇到的“灵异事件”,保证让你印象深刻。