2. Cortex-M内核编程模型:寄存器组与操作模式

寄存器,说白了就是CPU内部的高速暂存器。你写的C代码,最终都要靠它们来干活。Cortex-M内核的寄存器设计得相当规整,我个人觉得比老式ARM7好理解多了。

咱们先看一张总览图,把整个编程模型的骨架搭起来。

Cortex-M 编程模型总览 通用寄存器组 R0-R15 R0 - R7 : 低组寄存器(Thumb指令直接访问) R8 - R12 : 高组寄存器(部分指令需前缀) R13 (SP): 堆栈指针(MSP / PSP) R14 (LR): 链接寄存器 R15 (PC): 程序计数器 特殊寄存器组 PRIMASK : 全局中断屏蔽(第0位) FAULTMASK: 故障屏蔽(比PRIMASK更强) BASEPRI : 优先级阈值屏蔽 CONTROL : 线程模式/栈指针选择 xPSR : 程序状态寄存器(组合体) 操作模式 Thread Mode(线程模式) ↔ Handler Mode(处理模式) 普通代码运行 | 异常/中断服务程序 核心要点 • 所有寄存器都是32位宽,与Cortex-M总线宽度一致 • 异常入口自动压栈R0-R3、R12、LR、PC、xPSR,硬件完成

2.1 通用寄存器组:R0-R15

这16个寄存器是干活的主力。我习惯把它们分成三组来看:

2.1.1 R0-R7:低组寄存器

所有Thumb指令都能直接访问它们。说白了,这是最常用的8个寄存器,编译器会把局部变量、临时结果往这里塞。你在调试器里看汇编代码,频繁出现的R0、R1就是它们。

2.1.2 R8-R12:高组寄存器

部分Thumb指令不能直接操作它们,需要先MOV到低组。嗯,这里要注意,写内联汇编时容易踩坑——我见过有人直接在R8上做加法,结果编译器报错。

2.1.3 R13 (SP):堆栈指针

这个寄存器有点特殊。物理上存在两个SP:

指针 全称 默认使用场景
MSP 主堆栈指针 复位后默认使用,Handler Mode强制使用
PSP 进程堆栈指针 Thread Mode下可选,常用于RTOS任务栈
我的经验:做裸机开发时,我一直用MSP。但上了RTOS后,PSP就派上大用场了——每个任务有自己的栈,通过PSP切换,互不干扰。我曾经调试过一个任务栈溢出问题,就是靠观察PSP的值变化定位的。

2.1.4 R14 (LR):链接寄存器

函数调用时,硬件自动把返回地址存到LR里。但要注意,异常发生时LR会被赋予一个特殊值——EXC_RETURN,用来指示返回后的模式、栈选择等信息。

避坑指南:我曾经在中断服务函数里调用了另一个函数,结果LR被覆盖,返回时直接跑飞。解决办法:要么在中断里避免嵌套调用,要么手动保存LR。

2.1.5 R15 (PC):程序计数器

指向当前正在执行的指令地址。Cortex-M的PC有个特点:读PC时返回的值是当前指令地址+4(流水线效应),写PC则触发跳转。调试时看PC值,能快速定位程序卡在哪。

2.2 特殊寄存器

这些寄存器不直接参与数据运算,但控制着内核的行为。我刚开始学的时候觉得它们很抽象,后来调试中断相关问题时,才发现它们有多重要。

2.2.1 PRIMASK

只有第0位有效。置1时,屏蔽所有可屏蔽中断(NMI和硬fault除外)。

// 关闭所有中断(除了NMI和硬fault)
__disable_irq();   // 等价于设置PRIMASK=1
// 临界区代码...
__enable_irq();    // 清除PRIMASK

2.2.2 FAULTMASK

比PRIMASK更狠。置1时,连硬fault都被屏蔽了(除了NMI)。

警告:我建议非必要别碰FAULTMASK。曾经有个同事在调试时用它屏蔽了所有异常,结果系统死锁了连调试器都连不上——因为硬fault也被关了,内核彻底哑巴。

2.2.3 BASEPRI

这个寄存器很实用。它设置一个优先级阈值,只有优先级高于这个值的中断才能响应。

// 屏蔽优先级低于等于2的中断
__set_BASEPRI(2 << 4);  // Cortex-M用高4位表示优先级

为什么说它实用?你想想看,有时候你只想关掉低优先级中断,但不想动高优先级的实时响应。PRIMASK一刀切太粗暴,BASEPRI就优雅多了。

2.2.4 CONTROL

这个寄存器控制两个关键选择:

  • 第0位:Thread Mode下选择栈指针(0=MSP,1=PSP)
  • 第1位:选择特权级别(0=特权级,1=用户级)

2.2.5 xPSR:程序状态寄存器

它其实是三个寄存器的组合体:

组成部分 位域 作用
APSR [31:27] 算术标志:N(负)、Z(零)、C(进位)、V(溢出)、Q(饱和)
IPSR [8:0] 当前异常编号(0=无异常,非0=正在处理的异常号)
EPSR [26:25],[15:10] 执行状态:T位(Thumb状态)、ICI/IT位(中断连续/IF-THEN)
调试小技巧:看IPSR的值就能知道当前是不是在中断里,以及是哪个中断。我调试一个ADC采集丢数据的问题时,就是靠IPSR发现中断嵌套层级不对,导致采样时序乱了。

2.3 操作模式:Thread Mode 与 Handler Mode

Cortex-M只有两种模式,简单明了:

  • Thread Mode(线程模式):跑普通代码的地方。复位后默认进入此模式。
  • Handler Mode(处理模式):处理异常或中断时自动进入。退出异常时自动返回Thread Mode。

这两种模式的区别,我总结成一张表:

特性 Thread Mode Handler Mode
触发方式 复位或从异常返回 发生异常/中断
栈指针选择 MSP 或 PSP(由CONTROL决定) 强制使用MSP
特权级别 特权级或用户级 始终特权级
典型场景 main()循环、任务代码 中断服务函数、fault处理
我的习惯:做裸机开发时,我全程用Thread Mode + MSP,简单直接。上RTOS后,我会把CONTROL的第0位置1,让任务跑在PSP上,中断处理用MSP——这样任务栈溢出不会污染中断栈,安全性高很多。

为什么会设计两种模式?你想想看,中断处理需要高可靠性,如果中断里还能被其他代码干扰栈指针,那系统就乱套了。Handler Mode强制使用MSP,就是为了保证异常处理时栈的确定性。

嗯,到这里编程模型的核心内容就讲完了。记住一句话:寄存器是CPU的双手,模式是CPU的规则。搞懂了这些,后面看中断响应、任务切换这些机制,就会觉得顺理成章。