LK 博客
RTOS设计与开发(1):从栈开始理解 Cortex-M3 异常模型
嵌入式
约 1 分钟阅读 1 赞 0 条评论 鸿蒙黑体

RTOS设计与开发(1):从栈开始理解 Cortex-M3 异常模型

Yukikaze
Yukikaze @Yukikaze
累计点赞 1 登录后每个账号只能点一次
内容长度 0 正文词元数
正文
目录会跟随阅读位置移动。
阅读进度

一、“从水下的第一个生命的萌芽开始......”

如果想从 0 设计一个运行在 Cortex-M3 上的 RTOS,那么有一部分内容几乎绕不过去:

  • 栈到底是什么
  • MSP / PSP 是什么
  • Thread mode / Handler mode 是什么
  • 异常发生时 CPU 自动保存了什么
  • 任务切换时为什么还要手动保存 R4-R11
  • EXC_RETURN 到底在干什么

一开始看这些名词会非常乱,但它们本质上都在回答同一个问题:

Cortex-M3 遇到异常时,CPU 如何把当前任务停下来,又如何在以后恢复回来。

而 RTOS 的任务切换,本质上就是对这个机制的利用。

二、什么是栈

1. 栈的本质

栈(Stack)本质上是RAM 中的一块按特殊规则使用的内存区域。

这块区域主要用来保存“临时数据”和“执行现场”,例如:

  • 局部变量
  • 函数调用时的返回信息
  • 临时保存的寄存器内容
  • 异常/中断发生时的现场信息

栈可以理解成一个“后进先出”的结构,也就是:

  • 后放进去的内容
  • 先拿出来

这和一摞盘子很像:最后放上去的盘子,最先被拿走。

2. 什么叫压栈、出栈

  • 压栈(push):把数据放进栈里
  • 出栈(pop):把数据从栈里拿出来

在 ARM 中,常能看到这样的汇编:

PUSH {r4-r7}
POP  {r4-r7}

含义分别是:

  • PUSH:把寄存器内容保存到栈中
  • POP:从栈中恢复寄存器内容

3. 栈指针 SP

栈需要一个寄存器来记录“当前栈顶位置”,这个寄存器叫:

SP = Stack Pointer = 栈指针

要注意:

  • 是 RAM 中的一块区域
  • SP是一个寄存器,用来指向当前栈顶地址

在 Cortex-M3 中,栈通常是向低地址增长的,也就是:

  • 压栈时,SP 变小
  • 出栈时,SP 变大

三、寄存器是什么,和栈有什么关系

CPU 内部有一组寄存器,它们是 CPU 最快、最直接使用的数据存储位置。

常见寄存器包括:

  • R0-R12:通用寄存器
  • SP:栈指针
  • LR:链接寄存器
  • PC:程序计数器
  • xPSR:程序状态寄存器

可以这样理解:

  • 寄存器:CPU 手边的小盒子,速度快但数量少
  • RAM:容量大,但速度比寄存器慢
  • :RAM 中专门用于临时保存现场的区域

很多时候,CPU 会先用寄存器计算;当寄存器不够用,或者需要保存现场时,就把数据压进栈中。

四、几个关键寄存器的作用

1. PC

PC = Program Counter = 程序计数器

它保存的是:

下一条要执行的指令地址

程序能继续往下跑,核心就靠 PC。

2. LR

LR = Link Register = 链接寄存器

普通函数调用时,可以先把它理解成:

返回地址线索

函数调用结束后,CPU 需要知道该回到哪里继续执行,LR 在这里起重要作用。

3. xPSR

xPSR = Program Status Register

它保存程序当前状态,例如:

  • 条件标志位
  • Thumb 状态
  • 当前异常相关信息

所以程序恢复时,不仅要知道“回到哪里”,还要知道“以什么状态回去”。

五、为什么 RTOS 特别在乎栈

RTOS 的任务切换,本质上就是:

把当前任务的执行现场完整保存,再恢复另一个任务的执行现场。

任务的执行现场包括:

  • 当前寄存器内容
  • 执行到哪条指令
  • 当前函数调用层次
  • 局部变量和中间结果

这些信息很多都保存在栈里。

所以 RTOS 必须保证:

每个任务都有自己的栈。

这样任务 A 被切走时,现场留在任务 A 自己的栈中;任务 B 运行时,使用任务 B 自己的栈;以后再切回任务 A 时,就能从它自己的栈中继续恢复执行。

六、Thread mode 和 Handler mode

Cortex-M3 运行时有两种主要模式:

1. Thread mode

这是“正常工作模式”,通常用来执行:

main() 普通函数 RTOS 中的任务函数

也就是说,大多数应用代码都运行Thread mode

2. Handler mode

这是“异常处理模式”,用于执行:

中断服务函数 系统异常处理 PendSV、SysTick、HardFault 等异常处理代码

也就是说:

普通代码跑在 Thread mode,异常/中断处理代码跑在 Handler mode。

七、MSP 和 PSP 是什么

Cortex-M3 有两个栈指针寄存器:

  • MSP = Main Stack Pointer = 主栈指针
  • PSP = Process Stack Pointer = 进程栈指针 / 任务栈指针

注意这里很容易混淆:

  • CPU 里只有一个 MSP 寄存器和一个 PSP 寄存器
  • 任务可以有很多个栈空间
  • 当前运行哪个任务时,PSP 就指向哪个任务的栈顶

所以更准确地说:

每个任务有自己的栈空间;CPU 用 PSP 指向当前任务的栈。

1. MSP 的用途

MSP 一般用于:

  • 系统启动初期
  • main() 早期运行
  • 异常/中断处理
  • 内核相关代码

它很像“系统栈”。

2. PSP 的用途

PSP 一般用于:

  • RTOS 中各个普通任务的运行

它很像“当前任务栈”。

3. RTOS 中的常见分工

在很多 Cortex-M RTOS 中:

  • Thread mode 通常使用 PSP
  • Handler mode 一定使用 MSP

这样可以把:

  • 任务栈
  • 系统/异常栈

分开管理,互不混淆。

八、异常发生时,为什么要保存现场

假设任务 A 正在运行,此时突然发生中断或异常。

CPU 需要跳去执行异常处理代码,但异常处理结束后,任务 A 还得继续跑。 所以在跳走之前,必须先保存任务 A 的现场。

Cortex-M3 在异常进入时,会由硬件自动压栈一部分关键寄存器。

这部分寄存器是:

  • R0-R3
  • R12
  • LR
  • PC
  • xPSR

九、自动压栈寄存器

1. 自动压栈的内容

异常进入时,硬件自动保存:

  • R0
  • R1
  • R2
  • R3
  • R12
  • LR
  • PC
  • xPSR

总共 8 个寄存器。

  • 由于 Cortex-M3 是 32 位架构:

一个寄存器 = 32 位 = 4 字节

所以总大小是:

8 × 4 = 32 字节

2. 为什么自动压这几个

因为这几项构成了最小可恢复现场。

尤其是:

  • PC:决定恢复后从哪条指令继续执行
  • xPSR:决定恢复后的状态
  • LR:保留原任务被打断时的返回链路信息
  • R0-R3, R12:保存常用临时寄存器

所以:

异常进入时,硬件自动压栈是为了保存最基本的返回现场。

十、为什么 R4-R11 需要手动保存

自动压栈只保存了最基本的一部分,但对于任务切换来说,还不够。

因为R4-R11中也可能保存着任务还要继续使用的重要中间结果。

例如:

  • 任务 A 运行时把某些关键中间值放在R4、R5
  • 切换到任务 B 后,B 也会使用这些寄存器
  • 如果不提前保存任务 A 的R4-R11
  • 那么切回 A 时,这些值就会被破坏

于是任务 A 就无法像没被打断过一样继续运行。

所以在 RTOS 的上下文切换中,通常要由软件手动保存:

  • R4-R11

手动保存的本质

可以把它理解成:

  • 自动压栈:保证“能返回”
  • 手动保存R4-R11:保证“返回后内容没乱”

这就是为什么完整任务切换必须同时处理两类寄存器。

十一、异常入口时,自动压栈压到哪里

这个问题非常关键。

假设:

  • 任务 A 正在 Thread mode 下运行
  • 它使用的是PSP

此时发生异常,自动压栈的那 8 个寄存器会先压到:

任务 A 自己的栈,也就是 PSP 指向的任务栈中

为什么?

因为保存的是“被打断任务的现场”, 那么现场当然必须留在它自己的栈里,这样以后才能从它自己的栈恢复回来。

随后 CPU 才会进入 Handler mode,并开始使用 MSP。

所以可以记成一句:

异常入口先按原栈保存现场,再进入 Handler mode 使用 MSP。

十二、EXC_RETURN 是什么

EXC_RETURN 是 Cortex-M3 异常机制中的一个非常关键的概念。

它不是某个小标志位,而是:

写入 LR 的一个特殊返回码

1. 普通函数调用时的 LR

在普通代码中,LR 通常保存普通函数返回地址相关信息。

2. 异常处理时的 LR

当进入异常处理时,当前寄存器中的 LR 会被设置成一个特殊值,也就是:

EXC_RETURN

这个值的作用是告诉 CPU:

  • 这不是普通函数返回
  • 这是一次异常返回
  • 返回时该按异常返回规则处理
  • 该使用哪种栈指针恢复
  • 应该回到哪种模式

所以:

EXC_RETURN 负责“按什么方式返回”

而不是决定“回到哪条指令”。

3. 真正决定回到哪条指令的是谁

真正决定恢复后从哪条指令继续执行的是:

栈中恢复出来的 PC

所以要明确分工:

  • LR 中的 EXC_RETURN:决定“怎么返回”
  • 栈中保存的 PC:决定“返回到哪条指令”

十三、为什么要区分“栈里的 LR”和“当前 LR”

这也是非常容易混的地方。

当异常发生时:

  • 硬件会把“旧任务当时的 LR”压入栈中
  • 当前寄存器中的 LR 被设置为 EXC_RETURN

所以要区分:

1. 栈里的 LR

它属于“旧任务现场的一部分”。

2. 当前寄存器里的 LR

它属于“当前异常处理上下文”,用于控制异常返回。

简而言之:

栈里的 LR 是被打断任务的历史现场;当前 LR 是异常返回的控制信息。

十四、PendSV 为什么能做任务切换

在 Cortex-M RTOS 中,任务切换常用 PendSV 异常来实现。

原因在于:

  • 它是专门适合做延迟调度与上下文切换的异常
  • 可以在异常上下文中安全地切换任务现场

但这里最重要的一点是:

PendSV 只是在 Handler mode 中完成“切换动作”,并不是让新任务在 Handler mode 下运行。

也就是说:

  • 任务 A 正常运行:Thread mode
  • 触发 PendSV:进入 Handler mode
  • 在 PendSV 中保存 A、恢复 B
  • 异常返回
  • 任务 B 开始运行:仍然是 Thread mode

所以:

切换过程在 Handler mode,任务运行在 Thread mode。

十五、一次任务切换的完整过程

下面以“任务 A 切换到任务 B”为例,梳理一遍全过程。

场景开始:任务 A 运行

  • 当前模式:Thread mode
  • 当前栈指针:PSP_A
  • 任务 A 正在执行自己的普通代码

第一步:触发 PendSV 异常

CPU 检测到 PendSV 需要处理,开始异常入口操作。

硬件自动压栈

将任务 A 当前的:

  • R0-R3
  • R12
  • LR
  • PC
  • xPSR

压入任务 A 自己的栈(也就是 PSP_A 指向的栈)。

这一步保存的是任务 A 的最小恢复现场。

第二步:进入 Handler mode

自动压栈完成后:

  • CPU 进入Handler mode
  • 当前异常处理代码开始运行
  • 异常处理使用MSP

同时,当前寄存器中的 LR 被赋予EXC_RETURN的身份。

第三步:PendSV_Handler 中执行上下文切换

PendSV_Handler中,RTOS 会做这些事情:

  • 手动保存任务 A 的R4-R11
  • 保存当前任务 A 的PSP_A
  • 调度器选出要运行的下一个任务 B
  • 读取任务 B 上次保存的PSP_B
  • 恢复任务 B 的R4-R11

此时仍然处于Handler mode

第四步:异常返回

PendSV_Handler结束时:

  • CPU 看到当前LR = EXC_RETURN
  • 知道这不是普通函数返回,而是异常返回

于是 CPU 按异常返回规则:

  • 从任务 B 的栈中恢复自动压栈那部分内容 恢复出:
    • R0-R3
    • R12
    • LR
    • PC
    • xPSR

其中:

恢复出来的 PC 决定任务 B 从哪条指令继续执行。

第五步:任务 B 运行

异常返回完成后:

  • CPU 退出Handler mode
  • 回到Thread mode
  • 当前使用PSP_B 任务 B 从自己的 PC 所指位置继续执行

此时任务 B 不是在 Handler mode 中运行,而是作为普通任务,在 Thread mode 下继续执行。

十六、一张最小流程图总结

任务A运行(Thread mode, PSP_A)
        ↓
发生PendSV异常
        ↓
硬件自动压栈到任务A自己的栈(R0-R3, R12, LR, PC, xPSR)
        ↓
进入Handler mode,开始使用MSP
        ↓
PendSV_Handler中手动保存R4-R11
        ↓
保存任务A的PSP_A
        ↓
选择任务B
        ↓
恢复任务B的PSP_B
        ↓
恢复任务B的R4-R11
        ↓
LR = EXC_RETURN,执行异常返回
        ↓
CPU从任务B栈中恢复自动压栈内容
        ↓
恢复出PC
        ↓
任务B在Thread mode下继续运行

十七、最终总结

学习 Cortex-M3 异常模型时,最重要的是把下面这些关系彻底捋顺:

1. 栈与栈指针

栈是 RAM 中的一块区域 SP 是指向当前栈顶的寄存器

2. 两种模式

Thread mode:任务/普通代码运行 Handler mode:异常/中断处理运行

3. 两种栈指针

MSP:主栈指针,异常/系统使用 PSP:任务栈指针,当前任务使用

4. 自动压栈

异常进入时,硬件自动保存:

  • R0-R3
  • R12
  • LR
  • PC
  • xPSR

作用是保存“最小可恢复现场”。

5. 手动保存

RTOS 任务切换时,软件还必须手动保存:

  • R4-R11

作用是保证任务恢复后,重要中间结果不丢失。

6. EXC_RETURN

  • EXC_RETURN 是写入 LR 的特殊返回码
  • 它告诉 CPU:这是一次异常返回,应按异常规则恢复
  • 真正决定恢复后从哪条指令继续执行的,是栈中恢复出来的 PC

作者名片

Yukikaze
Yukikaze
@Yukikaze

私にできることなら何でもするから

评论区
文章作者和管理员都可以管理这里的评论。
0 条评论
登录后即可参与评论。 去登录
还没有评论,欢迎留下第一条交流内容。