寄存器模型与编程模型:通用寄存器组与特殊功能寄存器
寄存器,说白了就是 CPU 内部的小型存储单元。Cortex-M 内核的寄存器模型,我做了这么多年嵌入式,觉得它是理解整个芯片行为的钥匙。你想想看,CPU 执行指令、处理中断、切换任务,本质上都是在操作这些寄存器。
今天我们就来把 R0-R15、PSP/MSP、PRIMASK/FAULTMASK/BASEPRI 这些家伙彻底搞清楚。
通用寄存器组:R0-R15
Cortex-M 有 16 个通用寄存器,编号从 R0 到 R15。它们不是完全平等的——有些有特殊用途,有些就是纯粹的数据容器。
| 寄存器 | 别名 | 用途说明 |
|---|---|---|
| R0-R7 | 低组寄存器 | 所有指令都能访问,16 位 Thumb 指令的默认操作对象 |
| R8-R12 | 高组寄存器 | 32 位 Thumb-2 指令才能访问,常用于保存临时变量 |
| R13 | SP(堆栈指针) | 指向当前堆栈顶部,分 MSP 和 PSP |
| R14 | LR(链接寄存器) | 保存函数返回地址 |
| R15 | PC(程序计数器) | 指向当前正在执行的指令地址 |
关键点:R0-R12 是通用数据寄存器,R13-R15 有固定用途。写代码时别把 LR 当普通变量用,除非你很清楚自己在做什么。
我在项目中遇到过一个问题:一个同事把 R8 当循环计数器,结果在中断服务函数里改了 R8 的值,主循环直接跑飞了。嗯,这就是没搞清楚寄存器保存规则的后果。
堆栈指针:MSP 与 PSP
Cortex-M 有两个堆栈指针,这是个很巧妙的设计。MSP(主堆栈指针)和 PSP(进程堆栈指针),它们共用 R13 这个寄存器编号,但物理上是两个独立的寄存器。
为什么会这样?说白了,就是为了隔离系统代码和用户代码的堆栈空间。
- MSP:复位后默认使用,Handler Mode 下强制使用。用于中断服务函数、异常处理、内核代码。
- PSP:Thread Mode 下可选使用。用于任务代码、用户应用程序。
我的习惯:在 RTOS 里,我把 MSP 留给中断和内核,PSP 给每个任务分配独立的堆栈。这样即使某个任务栈溢出,也不会立刻把系统搞崩——至少中断还能跑。
切换当前使用的是哪个 SP,靠的是 CONTROL 寄存器的 bit 1。0 表示 MSP,1 表示 PSP。我记得第一次写 RTOS 移植时,忘了在任务切换时切换 PSP,结果中断返回后 PC 直接飞到了火星上……
特殊功能寄存器:PRIMASK / FAULTMASK / BASEPRI
这三个家伙是中断控制的"阀门"。它们控制着哪些中断可以被响应,哪些必须憋着。
| 寄存器 | 位宽 | 作用 |
|---|---|---|
| PRIMASK | 1 bit | 置 1 时屏蔽所有可屏蔽中断(除了 NMI 和 HardFault) |
| FAULTMASK | 1 bit | 置 1 时屏蔽所有异常,包括 HardFault(除了 NMI) |
| BASEPRI | 至少 3 bit | 设置中断优先级阈值,低于该优先级的中断被屏蔽 |
警告:FAULTMASK 是个危险的东西。我曾经在调试时用它屏蔽了所有中断,结果 HardFault 也进不去,芯片直接死锁。复位按钮按了三次才反应过来——嗯,是我自己写的代码。
这三个寄存器的使用场景很明确:
- PRIMASK:临界区保护,比如操作共享变量时关中断。
- FAULTMASK:系统即将进入不可恢复状态时使用,比如掉电前。
- BASEPRI:只屏蔽低优先级中断,高优先级中断还能响应。这个在实时系统中特别有用。
工作模式:Thread Mode 与 Handler Mode
Cortex-M 只有两种工作模式,简单得让人舒服。
- Thread Mode:普通程序运行的模式。可以特权级也可以非特权级。
- Handler Mode:处理异常或中断时的模式。永远是特权级。
这两种模式的区别,我打个比方:Thread Mode 是普通员工干活,Handler Mode 是老板亲自处理紧急事务。老板(Handler Mode)有更高的权限,能访问所有资源,而且必须用 MSP。
模式切换规则:
- Thread Mode → Handler Mode:发生异常/中断时自动切换
- Handler Mode → Thread Mode:异常返回时自动切换
- Thread Mode 内部不能直接切换到 Handler Mode(只能通过触发异常)
你想想看,为什么要有这两种模式?说白了,就是为了安全。用户程序跑在 Thread Mode,即使崩溃了,中断还能响应。系统还能通过 HardFault 做最后的挣扎。
寄存器模型与工作模式的关系图
下面这张图把寄存器、堆栈指针和工作模式的关系画清楚了。
CONTROL 寄存器:模式与栈指针的"开关"
CONTROL 寄存器虽然只有 3 个有效位,但它的作用很关键。
| 位 | 名称 | 功能 |
|---|---|---|
| bit 0 | nPRIV | 0 = 特权级 Thread Mode,1 = 非特权级 Thread Mode |
| bit 1 | SPSEL | 0 = 使用 MSP,1 = 使用 PSP(仅在 Thread Mode 有效) |
| bit 2 | FPCA | 浮点上下文活动标志(仅 Cortex-M4F/M7 等有 FPU 的内核) |
避坑指南:修改 CONTROL 寄存器后,必须执行 ISB 指令(指令同步屏障),否则后续指令可能读到旧值。我曾经因为这个 bug 调试了整整一个下午。
异常返回时的寄存器自动保存
Cortex-M 在进入异常时,硬件会自动把一部分寄存器压栈。这个机制很贴心,但也容易让人忽略细节。
自动压栈的寄存器顺序:
- R0
- R1
- R2
- R3
- R12
- LR(返回地址)
- PC(异常返回地址)
- xPSR(程序状态寄存器)
一共 8 个寄存器,32 字节。嗯,这就是为什么每个中断至少需要 32 字节的栈空间。如果用了浮点单元,还要额外压栈 17 个寄存器(S0-S15、FPSCR 等),那就是 68 字节。
注意:R4-R11 不会被自动保存。如果你的中断函数里用了这些寄存器,编译器会在函数入口处手动压栈。这也是为什么中断函数要尽量少用局部变量——栈开销会变大。
我的工程经验总结
做了这么多年 Cortex-M 开发,我总结了几条寄存器使用的铁律:
- MSP 和 PSP 别搞混:在 RTOS 里,中断服务函数里操作的是 MSP,任务代码里操作的是 PSP。切换时一定要先保存当前 SP。
- PRIMASK 别长时间置 1:关中断时间过长,会影响系统实时性。我一般控制在 10 微秒以内。
- BASEPRI 比 PRIMASK 更优雅:只屏蔽低优先级中断,高优先级中断还能响应。这样既保护了临界区,又不耽误紧急事件处理。
- Handler Mode 里别改 PSP:Handler Mode 强制使用 MSP,改 SPSEL 位不会生效。别问我怎么知道的……
寄存器模型这东西,光看书是不够的。我建议你打开调试器,单步跑一段简单的代码,观察每个寄存器的变化。看懂了 R0-R15 的流转,你就看懂了 Cortex-M 的一半。