理解RTOS任务切换:ARM寄存器、栈帧与上下文

ARM是RISC架构

ARM属于精简指令集计算机(RISC),其核心理念是:内存只做读写,运算全部在CPU内部完成

这与CISC(如x86)不同——x86的一条指令可以直接对内存中的值进行运算(如内存值+5),而ARM必须先把数据从内存加载到寄存器,运算后再写回内存。

三个关键寄存器

R13、R14、R15是三个特殊寄存器,理解它们对掌握RTOS任务切换至关重要:

寄存器别名作用RTOS意义
R13SP(Stack Pointer)指向当前栈顶地址任务切换时必须保存/恢复
R14LR(Link Register)保存函数返回地址函数调用链追踪
R15PC(Program Counter)存储下一条指令地址修改PC即跳转,任务切换的核心

核心汇编指令

数据搬运指令

指令作用示例
LDR从内存读取4字节到寄存器LDR R0, [R1] → 读R1地址处的值到R0
LDRB从内存读取1字节LDRB R0, [R1]
LDRH从内存读取2字节LDRH R0, [R1]
STR将寄存器值写入内存4字节STR R0, [R1] → R0的值写入R1地址
STRB写入1字节STRB R0, [R1]
STRH写入2字节STRH R0, [R1]

寻址方式

1
2
3
LDR R0, [R1]        ; 直接寻址:读取R1地址处的值
LDR R0, [R1, #4]    ; 带偏移:读取R1+4地址处的值
LDR R0, [R1, R2]    ; 寄存器偏移:读取R1+R2地址处的值

LDR R0, [R1, R2]为例,假设R1 = 0x20000000,R2 = 0x04,则实际访问地址 = 0x20000004

偏移量的单位永远是字节,指令后缀决定读写大小:

  • LDR R0, [R1, #4]:偏移4字节,从目标地址读取4字节数据
  • LDRB R0, [R1, #4]:偏移同样是4字节,但只读取1字节数据

运算指令

指令作用示例
ADD加法ADD R0, R0, R1 → R0 = R0 + R1
SUB减法SUB R0, R0, #1 → R0 = R0 - 1

比较与跳转指令

指令作用示例
CMP比较两个值,更新状态寄存器CMP R0, R1 → 不改变R0和R1的值
B无条件跳转B label → 跳转到label处
BL带链接跳转(函数调用)BL func → 保存返回地址到LR,跳转到func

重点理解BL指令:假设执行BL func时PC = 0x100,执行后:

  • LR = 0x104(下一条指令的地址,即返回地址)
  • PC = func的入口地址

效果:跳转到目标函数,同时保存了返回地址。

嵌套调用如何保护LR

如果函数A调用函数B,BL会将A的返回地址覆盖到LR中。那A原来的返回地址怎么办?

答案是用栈保存:函数入口处先PUSH {LR}将当前LR压栈,函数返回时POP {PC}从栈中恢复返回地址并直接跳转回去。嵌套多层调用时,每层的返回地址都保存在各自的栈帧中,逐层恢复即可。

栈帧

栈帧是函数调用时在栈上分配的一块空间,函数返回时自动回收。每个栈帧包含:

  • 返回地址(LR)
  • 保存的寄存器值
  • 局部变量(编译器可能优化到寄存器中,除非寄存器不够用或使用了volatile
  • 超过4个的函数参数

栈帧的生命周期

                        栈空间
                    ┌─────────────┐
调用 funcA 前        │   空闲      │  ← SP在这里
                    └─────────────┘
                         ↓ PUSH
调用 funcA 时        ┌─────────────┐
                    │  funcA栈帧   │  ← SP向下移动
                    └─────────────┘
                         ↓ POP
funcA 返回后         ┌─────────────┐
                    │   空闲      │  ← SP回到原位,栈帧"消失"
                    └─────────────┘
                    (内存数据还在,但标记为可复用)

嵌套调用时的栈帧变化

栈空间(从高地址向低地址增长):

调用 a_func 时:
┌────────────────────┐ 高地址
│  main 的栈帧       │
├────────────────────┤
│  a_func 栈帧       │ ← SP(包含局部变量、保存的LR等)
└────────────────────┘ 低地址

a_func 中调用 b_func 时:
┌────────────────────┐ 高地址
│  main 的栈帧       │
├────────────────────┤
│  a_func 栈帧       │  (仍然占用,等待恢复)
├────────────────────┤
│  b_func 栈帧       │ ← SP 继续向下移动
└────────────────────┘ 低地址

b_func 返回后:
┌────────────────────┐ 高地址
│  main 的栈帧       │
├────────────────────┤
│  a_func 栈帧       │ ← SP 回到这里
├────────────────────┤
│  (空闲,可复用)   │
└────────────────────┘ 低地址

与RTOS任务切换的关系

理解了上述概念,就能理解RTOS任务切换的本质:

  • SP:每个任务拥有独立的栈和SP,切换任务时保存当前SP、恢复目标任务的SP
  • LR:函数调用链保存在栈中,恢复SP后调用链自然恢复
  • PC:任务切换的核心就是修改PC,让CPU跳转到目标任务的代码继续执行
  • PUSH/POP:任务切换时用栈保存/恢复整个任务现场(所有寄存器)

任务栈大小估算

估算任务栈大小需要考虑两部分开销:

  1. 基础开销:任务切换时需要保存所有寄存器,约64字节(16个寄存器 × 4字节)
  2. 调用深度开销:每层函数调用消耗一个栈帧(LR + 保存的寄存器 + 局部变量),调用深度越深,栈消耗越大

例如,调用深度为5层时:总栈大小 ≈ 64 + 5 × 平均栈帧大小。