ARM是RISC架构
ARM属于精简指令集计算机(RISC),其核心理念是:内存只做读写,运算全部在CPU内部完成。
这与CISC(如x86)不同——x86的一条指令可以直接对内存中的值进行运算(如内存值+5),而ARM必须先把数据从内存加载到寄存器,运算后再写回内存。
三个关键寄存器
R13、R14、R15是三个特殊寄存器,理解它们对掌握RTOS任务切换至关重要:
| 寄存器 | 别名 | 作用 | RTOS意义 |
|---|---|---|---|
| R13 | SP(Stack Pointer) | 指向当前栈顶地址 | 任务切换时必须保存/恢复 |
| R14 | LR(Link Register) | 保存函数返回地址 | 函数调用链追踪 |
| R15 | PC(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] |
寻址方式
| |
以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:任务切换时用栈保存/恢复整个任务现场(所有寄存器)
任务栈大小估算
估算任务栈大小需要考虑两部分开销:
- 基础开销:任务切换时需要保存所有寄存器,约64字节(16个寄存器 × 4字节)
- 调用深度开销:每层函数调用消耗一个栈帧(LR + 保存的寄存器 + 局部变量),调用深度越深,栈消耗越大
例如,调用深度为5层时:总栈大小 ≈ 64 + 5 × 平均栈帧大小。