目录
1. 简介
2. 栈 - 代码运行的"便签纸"
3. 寄存器一览”
4. 通用寄存器
4.1 R0 ~ R3
4.2 R4 ~ R11
4.3 R12
4.4 汇编示例
5. 堆栈指针 R13
5.1 Banked
5.2 栈指针 和 栈
5.3 栈帧
5.4 栈溢出与保护
5.5 裸机与 RTOS 的栈使用差异
5.6 栈操作示意图
6. 链接寄存器 LR
6.1 基本工作机制
6.2 函数嵌套调用中的保护
6.3 中断/异常发生时,LR变身“特殊通行证”
6.4 调试时用LR找Bug
7. 程序计数器 PC
7.1 基本特性与流水线效应
7.2 PC的对齐要求与LSB规则
7.3 PC不可直接修改
8. 程序状态寄存器组 PSRs
8.1 xPSR的组合与拆分
8.2 APSR:运算标志的"晴雨表"
8.3 EPSR:执行状态的"监管者"
8.4 IPSR:当前中断的"身份牌"
9. 中断屏蔽寄存器组(PRIMASK、FAULTMASK、BASEPRI)
9.1 功能概述
9.2 BASEPRI 的典型应用场景:
10. 控制寄存器 CONTROL
10.1 Handler 模式是什么
10.2 位0 nPRIV:特权级与用户级的"身份切换"
10.3 位1 SPSEL:双堆栈指针的"选择开关"
10.4 复位后的默认状态与系统启动流程
1. 简介
本文主要讲解 ARM Cortex-M3 内核的寄存器系统,主要包含三类寄存器:通用寄存器(R0-R15)、程序状态寄存器组的三个子寄存器(APSR、EPSR、IPSR)的功能,以及中断屏蔽寄存器组(PRIMASK、FAULTMASK、BASEPRI)和控制寄存器(CONTROL)。
想读懂还是需要有点基础的,建议阅读前要有 裸机开发 和 RTOS操作系统开发 经验。
2. 栈 - 代码运行的"便签纸"
在学习寄存器之前,我们先要稍微了解一下 ----栈(stack)。
在代码运作中,会使用寄存器保存临时数据,但是 Cortex‐M3 内核的寄存器又是有限的。当同时活跃的数据变多,函数嵌套变深或者遇到中断时,寄存器吃不下这么多数据该怎么办?
所以 CPU 需要临时把这些数据搬到内存里缓存起来,到用的时候再搬回寄存器。这块专门用来临时存放寄存器数据的内存,就是 ----栈 (Stack)。
栈 是 后进先出(LIFO) 的结构,完美符合函数调用和中断处理。举例:A 调用 B,B 再调用 C,C 会先退出,也就是最后调用的最先返回。
3.寄存器一览”
Cortex‐M3 处理器拥有R0~R15的寄存器组,和若干"特殊功能寄存器"。
R0~R7 是低组通用寄存器(Low Registers),R8~R12 是高组通用寄存器(High Registers)
R13 是栈指针(SP)。 SP指向的是栈顶,它有两个,一个是主堆栈指针(MSP,Main Stack Pointer),另一个是进程堆栈指针(PSP,Process Stack Pointer)。
R14 是链接寄存器(LR)。"函数A" 调用 "函数B" 时,LR会记下 “A的下一条指令地址”,B执行完之后,就能找到回来的路。
R15 是程序计数器(PC)。始终指向下一个即将执行的指令的地址。
"特殊功能寄存器"分别是:
- 程序状态寄存器组(PSRs)
- 中断屏蔽寄存器组(PRIMASK、FAULTMASK、BASEPRI)
- 控制寄存器(CONTROL)
表中 xPSR 实际就是 PSRs 的一个“别名”,也就是有两个名字。
特殊功能寄存器有预定义的功能,必须通过专用的指令 MSR/MRS 来访问,而且它们也没有与之相关联的访问地址。
| MRS(Move to Register From Special Register) | 加载特殊功能寄存器的值到通用寄存器 |
| MSR(Move to Special Register From Register) | 存储通用寄存器的值到特殊功能寄存器 |
4. 通用寄存器
R0‐R12 都是 32 位通用寄存器,用于数据操作。R0-R7与R8-R12的主要区别在于:绝大多数 16 位的 Thumb 指令只能访问低寄存器。
但是实际功能分组时,不是按照低组或高组来区分的。
4.1 R0 ~ R3
R0~R3用于函数参数传入、返回值传出、临时值。
| 参数传入 | 调用函数时,R0~R3 按顺序存放 前四个 整数/指针参数 |
| 返回值传出 | 函数返回时,通过 R0(32位及以下) 或 R0+R1(64位) 返回结果 |
| 临时值 | 函数内部的局部变量、中间计算结果也可暂存在 R0~R3 中 |
当参数有很多,寄存器不够用,剩余的参数传递会使用栈空间。因此想让代码更高效,在函数设计时要尽量控制参数不超过 4 个,以利用寄存器传递,而不是占用速度更慢的栈空间。
当调用函数时,R0~R3原本已经有数据了(上层函数使用),为避免这些数据被 "被调函数" 覆盖,"上层函数" 需要先将R0~R3中 需要保留的值 保存在栈中(压栈),"被调函数" 返回后 再恢复寄存器的值。所以R0~R3属于"调用者保存寄存器"。
4.2 R4 ~ R11
R4~R11用于保存函数的局部变量和中间结果。
在 "函数A" 调用 "函数B",如果 "函数B" 要使用R4~R11寄存器,不管寄存器上面的值是否有效,都必须先将寄存器原始值压栈保存,直到到函数返回前再出栈恢复。由此可见R4~R11属于"被调用者保存寄存器"。
R4~R11的压栈规则如上,所以即使出现再多层函数嵌套调用,调用前后寄存器的值也不会改变。
(PS:在很多编译器的标准调用约定中,R11被用作栈帧指针(FP,Frame Pointer,也可以叫做栈底指针),它指向 当前函数栈的起始位置。配合 SP 可以方便地访问 栈 上的参数和局部变量,并支持调试时的栈回溯。如果函数不使用 栈帧指针,比如在高优化等级下,R11 也可作为普通的通用寄存器使用)
4.3 R12
R12 不太重要,实际可以不用关注。
R12 在标准调用约定中也被称为IP (Intra-Procedure-call scratch register,过程内调用暂存寄存器),主要用于动态链接、代码跳转以及函数入口/出口的临时存储。
在函数调用过程中,有时需要一小段“辅助代码”来完成跳转。例如:当你要调用的函数不在当前程序里,而在一个动态库(.dll 或 .so)中,运行时才知道它的真实地址;或者你的代码要从 Thumb 指令集切换到 ARM 指令集(或者反过来),中间需要几条额外的指令做状态切换。这段“辅助代码”很短,它需要一个临时的地方存放某个地址或中间值。这个临时地方就是R12。
4.4 汇编示例
首先稍微了解一下表格的汇编指令:
| 示例 | 等价执行结果 |
MOVS/MOVr3, #0x04 MOVS/MOVr4, r0 | r3 = 0x04 r4 = r0 |
| PUSH{r4-r5, lr} | 将 r4、r5、lr 压栈 |
ADDS/ADDr0, r1 ADDS/ADDr0, r1, r2 ADDS/ADDr0, #5 | r0 = r0 + r1 r0 = r1 + r2 r0 = r0 + 5 |
| POP{r4-r5, pc} | 将 r4、r5、pc 出栈 |
(PS:MOVS 和 MOV、ADDS 和 ADD,基本格式是一样的,表面执行效果也是一样的,但是它们不是一个东西,感兴趣可以去查查)
如图,结合 左边的C代码 和 右边的汇编代码,只关注通用寄存器:
| 输入的参数 (1,2,3,4) 存入了R0~R3(参数传入) |
| R4、R5用于存放中间结果,所以在进入函数时将R4、R5压栈,然后使用R4、R5作为中间值 |
| 最终结果存放与R0(返回值传出) |
| 函数退出时又将R4、R5出栈 |
5. 堆栈指针 R13
SP 有两个,一个是主堆栈指针(MSP,Main Stack Pointer)另一个是进程堆栈指针(PSP,Process Stack Pointer)。
| MSP | Cortex-M3 复位后默认使用的堆栈指针,也是特权级下默认的堆栈。 所有的异常处理例程(包括中断服务函数、HardFault、系统服务调用等)必须且只能使用 MSP。操作系统内核代码(如任务调度器)通常也使用 MSP。 |
| PSP | 专门为用户应用程序准备的堆栈指针。 通常用于运行 RTOS 中的各个用户任务,每个任务拥有自己独立的栈,通过 PSP 来访问。 所以裸机一般不会使用 PSP。 |
MSP 和 PSP 实际指向不同的栈区,所以切换栈指针就是切换当前使用的栈内存区域。
5.1 Banked
虽然 Cortex-M3 拥有两个堆栈指针,但是它们是"Banked"。
| Banked | 在同一个寄存器名称(R13/SP)之下,物理上存在两个独立的寄存器(MSP和PSP)。 它们拥有各自独立的物理存储单元,但软件层面统一通过 |
"Banked"机制的核心是“上下文依赖的选择器”。内核通过CONTROL 寄存器和 当前的工作模式来决定把哪一个物理寄存器映射给SP这个名字。CONTROL 后面还会讲。
5.2 栈指针 和 栈
栈由一块连续的内存和栈顶指针SP组成。在 Cortex-M3 中,栈是"向下生长"(由"高地址"向"低地址"增长)的,压栈时 SP 的值减小,出栈时 SP 的值增大。
在 Cortex-M3 中,栈操作主要通过两条指令完成:
| PUSH | 将寄存器内容压入栈中。 执行时, |
| POP | 从栈中弹出数据到寄存器。 执行时,先读取 |
由于栈向下增长,初始化时SP通常被设置为栈内存区域的最高地址 + 1(即栈顶的初始位置)。例如,若栈区为0x2000 1000~0x2000 1FFF,则初始SP应设为0x2000 2000。
5.3 栈帧
在Cortex-M3中,“栈帧”是一个广义概念,它指代保存在当前堆栈中的一组连续数据。但需要特别注意的是,中断/异常产生的栈帧与函数嵌套调用产生的栈帧,在形成机制和用途上有本质区别。
| 中断栈帧 | 当发生异常或中断时,为了确保中断服务程序执行完毕后能精确还原被打断的现场,Cortex-M3的硬件逻辑会自动执行压栈操作,这一过程无需软件指令干预,且具有原子性。硬件会自动将R0 ~ R3、R12、LR(R14)、PC(R15)、xPSR寄存器按固定顺序压入当前使用的堆栈(MSP 或 PSP)。 |
| 函数调用栈帧 | 当发生普通的函数嵌套调用,例如 函数调用栈帧主要用于保存:局部变量、被调用者需保存的寄存器、返回地址。 |
中断栈帧是硬件为处理异步事件而强制创建的“快照”;
函数调用栈帧是编译器为管理同步执行流(局部变量、函数嵌套)而手动构建的数据结构。
5.4 栈溢出与保护
栈的大小是有限的,若 函数嵌套过深或局部变量太多,压栈操作导致栈指针低于栈的允许范围(向低地址端生长过头),就会发生栈溢出(Stack Overflow)。
栈溢出可能导致:
破坏相邻内存区域的数据(如全局变量、堆数据)
程序运行异常、HardFault
难以调试的随机性故障
5.5 裸机与 RTOS 的栈使用差异
| 裸机 | 全程仅使用 MSP,初始化时由启动文件设置 SP 初始值。所有函数调用、中断都共用这一块主栈,因此栈大小需覆盖“主循环最大嵌套深度 + 最大中断嵌套深度”的场景。 |
| RTOS 程序 | MSP 仅用于异常处理和调度器核心代码,PSP 用于各用户任务。每个任务拥有独立的栈空间,任务栈大小需根据该任务的调用深度和局部变量量单独配置。这样做的好处是任务间栈隔离,一个任务的栈溢出不会立即污染其他任务(但仍可能破坏全局数据)。 |
5.6 栈操作示意图
假设栈区从0x2000 1000到0x2000 1FFF(按字对齐,每次压栈4字节),初始 SP 指向栈顶边界0x20002000(即最高地址 + 1)。
高地址 0x20002000 ┌─────────────┐ ← 初始 SP(栈顶边界,不存放数据) │ │ │ (空闲) │ │ │ │ │ │ │ │ │ 低地址 0x20001000 └─────────────┘ ← 栈底(警戒区)连续压入 R0、R1、R2(每次4字节):
高地址 0x20002000 ┌─────────────┐ ← 初始 SP(栈顶边界,不存放数据) 0x20001FFF ─ ─ ─ ─ ─ ─ ─┤ ← 栈区最高有效地址(边界线) │ R0 │ ← 最先压入,地址最高 0x20001FFC ├─────────────┤ ← 第1次PUSH后 SP(R0) │ R1 │ 0x20001FF8 ├─────────────┤ ← 第2次PUSH后 SP(R1) │ R2 │ ← 最后压入,地址最低(当前栈顶) 0x20001FF4 ├─────────────┤ ← 第3次PUSH后 SP(R2) │ │ │ .... │ 低地址 0x20001000 └─────────────┘ ← 栈底(向下方向已被占用,继续PUSH会溢出)| 第1次 PUSH {R0} | SP = SP - 4 →0x20001FFC,将 R0 写入该地址。 |
| 第2次 PUSH {R1} | SP = SP - 4 →0x20001FF8,将 R1 写入该地址。 |
| 第3次 PUSH {R2} | SP = SP - 4 →0x20001FF4,将 R2 写入该地址。 |
可见:先压入的数据在高地址,后压入的数据在低地址,SP 始终指向当前栈顶(即最后一个压入的数据)。
弹出到 R2、R1、R0(后进先出):
高地址 0x20002000 ┌─────────────┐ ← 第3次POP后 SP(回到初始位置) 0x20001FFF ─ ─ ─ ─ ─ ─ ─┤ │ R0 │ ← 第3次POP:读取R0,SP += 4 0x20001FFC ├─────────────┤ ← 第2次POP后 SP(指向R0) │ R1 │ ← 第2次POP:读取R1,SP += 4 0x20001FF8 ├─────────────┤ ← 第1次POP后 SP(指向R1) │ R2 │ ← 第1次POP:读取R2,SP += 4 0x20001FF4 ├─────────────┤ ← 第1次POP前 SP(指向R2,起始状态) │ │ │ .... │ 低地址 0x20001000 └─────────────┘ ← 栈底| 第1次 POP {R2} | 从当前 SP (0x20001FF4) 读取数据到 R2,然后 SP = SP + 4 →0x20001FF8。 |
| 第2次 POP {R1} | 从当前 SP (0x20001FF8) 读取数据到 R1,然后 SP = SP + 4 →0x20001FFC。 |
| 第3次 POP {R0} | 从当前 SP (0x20001FFC) 读取数据到 R0,然后 SP = SP + 4 →0x20002000。 |
可以看出:栈是由"高地址"向"低地址"增长,压栈时 SP 的值减小,出栈时 SP 的值增大。
6. 链接寄存器 LR
在Cortex-M3的寄存器生态中,R14 扮演着一个看似简单却至关重要的角色 —— 它被专门设计为链接寄存器(Link Register),用于保存中断函数处理完成或函数调用完成后的返回地址。如果没有LR,处理器在跳转到子程序后就“迷失”了归途。
6.1 基本工作机制
当你在C代码里调用一个函数时,CPU实际会执行一条特殊的汇编指令(叫BL或BLX)。这条指令会自动做两件事:
| 1. 将当前程序计数器(PC)的值加上一个偏移量,计算出下一条即将执行的指令地址。 |
| 2. 将该地址自动存入LR。 |
随后,处理器跳转到目标子程序执行。当子程序执行完毕时,只需执行一条BX LR指令,处理器就会将LR中保存的地址重新加载到PC,从而实现“从哪里来,回哪里去”的返回。
6.2 函数嵌套调用中的保护
LR 虽然方便,但是它只有一个。
这就引发了一个问题:"函数A" 调用了 "函数B",调用B之前,LR里存的是“A执行完后回主程序的地址”。但当你调用B时,CPU会自动把“B执行完后回A的地址”覆盖进LR,这样A的返回地址就丢了。
为了解决这个问题,在调用B之前,编译器会帮我们把LR里原来的地址压栈,等B执行完了再出栈。再回看 4.4 章节的示例,可以看到进入函数后会将 LR 与其他寄存器一起压栈,防止内部再调用别的函数覆盖原本的 LR。
6.3 中断/异常发生时,LR变身“特殊通行证”
这是LR最“分裂”的地方 ——在正常函数里它是地址,但在中断服务程序里它变成了一个“魔数”(神秘数字)。
因为当外部中断或硬件异常发生时,CPU会:
| 1.硬件自动把当前的运行状态(包括返回地址)压入堆栈保存。 |
2. 然后把一个特殊的魔数(比如0xFFFFFFF9)写入LR,而不是写普通的返回地址。 |
这个魔数其实是告诉CPU:“我不是地址,我是异常返回指令”。
当你的中断处理程序执行完,执行BX LR时,CPU 看到 LR 里存的不是普通地址,而是这类魔数,就会触发硬件自动恢复机制,把之前压入堆栈的寄存器全部弹出来,回到中断前的状态。
几个常见的魔数含义:
0xFFFFFFF1 | 退出中断后,回到“线程模式”,用主堆栈指针(MSP) |
0xFFFFFFF9 | 退出中断后,回到“线程模式”,用进程堆栈指针(PSP)—— RTOS任务切换常用这个 |
0xFFFFFFFD | 退出中断后,回到“Handler模式”(用于中断嵌套) |
6.4 调试时用LR找Bug
当程序跑飞进HardFault了,你可以在调试器里查看LR的值,看它是从哪个函数跳过来的,配合堆栈里的信息,就能倒推出“死机前最后执行了哪一步”。
7. 程序计数器 PC
在Cortex-M3的所有寄存器中,R15 —— 程序计数器(Program Counter),是最特殊的一个。它既是CPU执行流程的"导航仪",也是整个内核运行状态最直观的窗口。PC总是指向当前正在执行的指令地址,其值的变化直接决定了程序的走向。
7.1 基本特性与流水线效应
Cortex-M3内核采用三级流水线架构:
取指(Fetch)→ 译码(Decode)→ 执行(Execute)
这三个阶段并行工作,使得指令吞吐率显著提升:
| 时刻 | 取指阶段 | 译码阶段 | 执行阶段 |
|---|---|---|---|
| 第1个时钟 | 取指令N | (空闲) | (空闲) |
| 第2个时钟 | 取指令N+1 | 译码指令N | (空闲) |
| 第3个时钟 | 取指令N+2 | 译码指令N+1 | 执行指令N |
当CPU在执行某条指令时,取指阶段已经提前取到了 "当前指令地址 + 8" 处的指令。所以这种流水线设计给PC带来一个特性:
在Cortex-M3中,当读取PC的值时,得到的并非当前正在执行的指令地址,而是"当前指令地址 + 4"。
| 为什么是 +4 而不是 +8? | 在Cortex-M3中,读取PC时,硬件返回的并不是真正的取指地址,而是当前执行地址 + 4(ARMv7-M架构规定)。这是一个架构规定的返回值,而不是物理取指地址的直接映射。 |
(PS:PC的读取值 ≠ 实际的取指地址。流水线中的取指地址可能更靠前,这取决于流水线状态和指令对齐,但架构层面规定PC读取返回 +4,为了软件提供一致的相对偏移基准。)
7.2 PC的对齐要求与LSB规则
Cortex-M3 要求 PC 值必须半字对齐,即地址最低位(LSB)在物理上始终为0(地址为偶数),以保证指令按 16 位或 32 位边界正确取指。
但在 Thumb 指令集体系中,函数地址和返回地址(LR)的最低位被用作 “指令集状态标记”。由于 Cortex-M3 仅支持 Thumb-2 指令集,该标记必须固定为1,表示目标为 Thumb 代码。
因此,编译器在生成函数地址时,会将真实入口地址的 LSB强制置 1。例如,一个真实地址为0x080001CC的函数,在代码获取地址时,会显示为0x080001CD。当执行BX或BLX跳转时,处理器会自动忽略该 LSB(将其清 0)后再送入 PC,从而正确访问对齐的指令地址。
7.3 PC不可直接修改
PC作为R15,在寄存器编号上与其他通用寄存器连续排列,因此理论上可以通过MOV PC, Rn或LDR PC, [addr]直接修改PC的值实现代码跳转。
但是在C语言层面,永远不要用指针直接修改 PC,做这种非标操作很容易造成问题。实际上即使在汇编代码中,也不应该直接赋值 PC。
8. 程序状态寄存器组 PSRs
PSRs 寄存器直接反映了处理器的当前运算状态、执行模式以及中断嵌套层级,是理解 Cortex-M3 内核运行机理的核心窗口。
在Cortex-M3的寄存器体系中,程序状态寄存器组(Program Status Registers,PSRs)是唯一 一组"名不副实"的寄存器。它看似一个独立的32位寄存器,实则是三个功能各异的寄存器的逻辑组合。
8.1 xPSR的组合与拆分
PSR是一个32位的 "虚拟寄存器",实际由 三个物理寄存器 通过逻辑组合构成:
| 子寄存器 | 全称 | 位段位置 | 职责 |
|---|---|---|---|
| APSR | 应用程序状态寄存器 | 位[31:27] | 存储ALU运算产生的条件标志 |
| EPSR | 执行状态寄存器 | 位[26:25] + 位[24] + 位[15:10] | 控制指令执行状态与中断可切状态 |
| IPSR | 中断状态寄存器 | 位[8:0] | 指示当前正在服务的异常编号 |
这三个子寄存器在物理上是独立的,但在逻辑上被映射到同一个32位地址空间。开发者可以使用以下方式访问:
| 整体访问 | 使用MRS Rn, xPSR读取完整的32位xPSR,或MSR xPSR, Rn整体写入(通常只有特权级代码才能写入) |
| 单独访问 | 使用MRS Rn, APSR、MRS Rn, EPSR或MRS Rn, IPSR分别读取各个子寄存器。 |
8.2 APSR:运算标志的"晴雨表"
APSR(应用程序状态寄存器)存放着算术逻辑单元(ALU)执行运算后产生的条件标志位,是条件分支指令(如
BEQ、BNE、BGT等)的判断依据。
| 位 | 标志 | 名称 | 置位条件 |
|---|---|---|---|
| 31 | N | 负标志 | 运算结果为负数时置1(最高位为1) |
| 30 | Z | 零标志 | 运算结果为0时置1 |
| 29 | C | 进位/借位标志 | 无符号加法溢出时置1;无符号减法产生借位时清0 |
| 28 | V | 溢出标志 | 有符号运算结果溢出时置1 |
| 27 | Q | 饱和标志 | 发生饱和运算(如SSAT/USAT指令)时置1,需要手动清零 |
8.3 EPSR:执行状态的"监管者"
EPSR(执行状态寄存器)负责管理处理器在执行指令时的内部状态。EPSR不是给程序员看的,是给硬件自己在中断“插队”时看的备忘录。EPSR不可写。
| T位 | 告诉硬件 “我现在是Thumb模式” —— Cortex-M3永远为1,不用管。 |
| IT位 | 告诉硬件 “我正处在一个条件执行块里,做到第几条了”。 |
| ICI位 | 告诉硬件 “我正搬一车寄存器,搬到第几个了”。 |
8.4 IPSR:当前中断的"身份牌"
IPSR(中断状态寄存器)是一个9位寄存器(位[8:0]),用于标识当前正在服务的异常或中断编号。它的值直接反映了处理器的异常嵌套层级。IPSR是只读的,任何写入操作均无效。
| IPSR值 | 含义 |
|---|---|
| 0 | 线程模式(Thread Mode),未处理任何异常 |
| 1 | 复位(Reset) |
| 2 | 不可屏蔽中断(NMI) |
| 3 | 硬件错误(HardFault) |
| 4-10 | 其他系统异常(MemManage、BusFault、UsageFault、SVCall、DebugMonitor、PendSV、SysTick) |
| 16-255 | 外部中断(IRQ0-IRQ239,具体数量因芯片型号而异) |
我们可以在共享的中断处理函数中,通过读取 IPSR 判断当前是哪个中断触发了该函数(适用于多个中断共用一个入口的场景)。
9. 中断屏蔽寄存器组(PRIMASK、FAULTMASK、BASEPRI)
中断屏蔽寄存器常用于控制异常或中断的使能和除能。
每个异常都有优先级,数值越小优先级越高,这些特殊寄存器可基于优先级屏蔽异常,只有在特权访问模式才可以对它们操作。
在用户模式下,对中断屏蔽寄存器写操作会被忽略,读操作会返回0。
9.1 功能概述
PEIMASK和FAULTMASK都是简单粗暴的中断屏蔽工具,BASEPRI寄存器相较来说更加细腻。
9.2 BASEPRI 的典型应用场景
| RTOS临界区 | RTOS的任务切换通常由 RTOS 进入临界区时,将BASEPRI寄存器设置为特定阈值,该阈值一般会比 PendSV 大,屏蔽 PendSV 中断就无法进行任务调度。 |
| 中断服务函数中的临界段 | 在中断处理过程中,如果需要执行一段不被低优先级中断打断的代码,但又希望允许更高优先级的中断(如紧急错误中断)嵌套,BASEPRI是理想选择。 RTOS 临界区的思路也是这样。 |
| 驱动层的资源保护 | 当多个中断共享同一个硬件资源(如UART发送缓冲区)时,设置BASEPRI屏蔽那些也使用该资源的中断,但保留更紧急的系统中断 |
10. 控制寄存器 CONTROL
在Cortex-M3的寄存器家族中,CONTROL寄存器是唯一 一个不直接参与数据运算、不保存状态标志、也不控制中断屏蔽,却深刻影响着整个系统运行模式的寄存器。
CONTROL 寄存器只有两个有效位,却决定了处理器在特权级与用户级之间的身份切换,以及主堆栈与进程堆栈之间的选择 —— 这两项决策是操作系统实现 任务隔离 和 系统
安全 的基础。
10.1 Handler 模式是什么
Handler模式已经出现过很多次了,一直没讲解,现在放在这里讲。
Handler模式在响应任何异常或中断时自动进入。用专业的话来说就是:处理器正在执行异常处理程序(包括所有中断服务函数和系统异常) 时的运行状态。
与Handler模式相对的是线程模式(Thread Mode),即处理器在执行普通应用程序代码时的状态。
10.2 位0 nPRIV:特权级与用户级的"身份切换"
Cortex-M3支持特权级(Privileged Level) 和 用户级(User Level)两种执行模式的特权级别划分:
| 特权级 |
|
| 用户级 |
|
nPRIV位的实际作用:nPRIV = 0时,线程模式享有全部权限;nPRIV = 1时,线程模式被降级为用户级,权限受限。
Handler模式(中断/异常处理)始终运行在特权级,不受nPRIV位影响。
10.3 位1 SPSEL:双堆栈指针的"选择开关"
Cortex-M3拥有两个物理堆栈指针(MSP 和 PSP),但 R13 在任意时刻只能指向其中之一。SPSEL位决定了当前SP寄存器对应的是哪一个。
将内核堆栈(MSP)与 应用程序堆栈(PSP)分离,是一种安全隔离的硬件设计。当用户程序发生堆栈溢出时,破坏的是PSP指向的用户堆栈区域,而MSP指向的内核堆栈不受影响,系统仍有机会通过 HardFault 或 MemManage 异常进行恢复或上报,而不至于完全崩溃。
10.4 复位后的默认状态与系统启动流程
Cortex-M3复位后,CONTROL寄存器的默认值为0x00000000,nPRIV = 0:线程模式运行在特权级。SPSEL = 0:当前使用MSP作为堆栈指针。
这意味着在系统启动的最初阶段,所有代码(包括启动代码、系统初始化、main函数)都运行在特权级并使用MSP。
直到操作系统初始化完成,创建了第一个用户任务并准备好 PSP 堆栈后,才会通过修改CONTROL 切换到用户级 + PSP的组合。
在RTOS的任务切换中,CONTROL寄存器的管理是上下文切换的核心环节。