FreeRTOS内核(一)ARM架构基础与栈
本文章适合有MCU和FreeRTOS的使用基础,进一步学习FreeRTOS内核与单片机底层
参考视频与文章:
01_任务的引入_ARM架构_汇编简明教程_哔哩哔哩_bilibili
一、CPU
ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:
① 对内存只有读、写指令
② 对于数据的运算是在CPU内部实现
③ 使用RISC指令的CPU复杂度小一点,易于设计
二、寄存器与核心汇编
1、核心寄存器的作用:
| 寄存器 | 类型 | 作用 |
|---|---|---|
| R0–R3 | 通用 | 参数传递、临时变量(调用者保存) |
| R4–R11 | 通用 | 局部变量(被调用者保存) |
| R12 | 通用 | 临时变量(调用者保存) |
| R13 (SP) | 栈指针 | 指向当前栈顶 |
| R14 (LR) | 链接寄存器 | 保存返回地址 |
| R15 (PC) | 程序计数器 | 指向当前指令(Cortex-M 中,有 MSP/PSP之分) |
| xPSR | 状态寄存器 | 包含条件标志、中断状态等 |
物理上存在两个独立的栈指针寄存器:
-
MSP(主栈指针,Main Stack Pointer):复位后默认使用,通常用于内核代码和中断服务程序。
-
PSP(进程栈指针,Process Stack Pointer):用于用户任务/线程(在 RTOS 中,每个任务有自己的栈,但 PSP 一次只能指向一个任务栈)。
在任何时刻,当前使用的栈指针是 MSP 还是 PSP,由CONTROL寄存器的 bit1 决定。当处于线程模式(用户任务)时,可以选择使用 PSP;当处于 handler 模式(中断/异常)时,强制使用 MSP。

2、主要汇编
2.1、数据读写指令:实现内存与寄存器之间的数据传输
① LDR:加载,将内存中的数据读取到寄存器(例:LDR R0, [R1],将 R1 指向的内存地址数据加载到 R0);
② STR:存储,将寄存器中的数据写入内存(例:STR R0, [R1],将 R0 数据写入 R1 指向的内存地址)。
LDRB/LDRH/LDR和STRB/STRH/STR,分别对应 1 字节 / 2 字节 / 4 字节的读写,解决不同数据类型(char/short/int)的内存访问需求
2.2、运算指令:
③ ADD,实现寄存器间的加法运算(例:ADD R0, R1, R2,R1+R2 的结果存入 R0),核心说明所有运算均在寄存器中完成。
2.3、栈操作指令:任务上下文保存 / 恢复的核心,也是任务切换的关键
④ PUSH:将寄存器数据压入栈,同时更新栈指针 SP(例:PUSH {R3, LR},保存 R3 和 LR 到栈,用于函数调用前的上下文保护);
⑤ POP:从栈中弹出数据到寄存器,更新 SP(例:POP {R3, PC},从栈恢复数据到 R3 和 PC,实现函数返回)。
指令关键说明:函数调用时 R0~R3 无需手动保存(硬件自动处理),但任务切换时需要保存所有寄存器,因为任务无法预知后续会使用哪些寄存器,这是任务上下文保存的核心原则。
3、索引方式
前索引:LDR R0, [R1, #4] → 先计算R1+4的地址,再读取数据,不修改 R1;
后索引:LDR R0, [R1], #4 → 先从 R1 地址读取数据,再计算R1+4,修改 R1;
自动索引:LDR R0, [R1, #4]! → 先计算R1+4,读取数据后把新地址写回 R1。
三、一条代码的执行

下面给出C语言与对应的汇编,理解一下寄存器操作,特别是SP LR PC的操作
// 简单加法函数
int add(int a, int b)
{
return a + b; // 核心逻辑:a+b,返回结果
}
int main(void)
{
int x = 10;
int y = 20;
int z = add(x, y); // 调用add函数,z=30
while(1); // 死循环
}
; add函数的汇编实现
add:
ADD R0, R0, R1 ; R0 = R0+R1(a+b,ARM中函数参数存在R0/R1)
BX LR ; 跳回LR保存的地址(main函数中调用add的下一行)
; main函数的汇编实现
main:
; 1. 初始化栈(编译器自动生成,这里简化)
LDR SP, =0x20000400 ; 设置主栈指针MSP
; 此时的栈
; 地址 内容
; 0x20000400 ——(SP指向这里,空)
; 0x200003FC ——
; 0x200003F8 ——
; 0x200003F4 ——
; 2. 执行int x=10; int y=20;
MOV R0, #10 ; R0=10,#10表示“常量10”,不是地址/寄存器
STR R0, [SP, #-4]! ; 压栈保存x(SP=SP-4,x存在SP指向的地址)
MOV R1, #20 ; R1=20, #-4是“偏移量常量”,表示SP减4
STR R1, [SP, #-4]! ; 压栈保存y(SP=SP-4,y存在SP指向的地址)
; '!'把计算后的新地址写回基址寄存器
; 地址 内容
; 0x20000400 ——
; 0x200003FC 10(x)
; 0x200003F8 20(y,SP指向这里)
; 0x200003F4 ——
; 3. 执行int z = add(x, y);
LDR R0, [SP, #4] ; 从栈中取出x到R0(函数第一个参数)
LDR R1, [SP] ; 从栈中取出y到R1(函数第二个参数)
BL add ; 调用add函数:
; - 第一步:把当前PC+4(下一条指令地址)写入LR
; - 第二步:跳转到add函数执行
; 4. add函数返回后,执行这行
STR R0, [SP, #-4]! ; R0是add返回值(30),压栈保存为z
; 5. 执行while(1);
loop:
B loop ; 死循环
BL 指令:带 “记忆功能” 的跳转,自动把返回地址存到 LR,再跳转到目标函数;
BX 指令:“按记忆返回” 的跳转,把 LR 中保存的返回地址写入 PC,实现函数返回;
LR 的作用:全程自动保存返回地址,是 BL 和 BX 之间的 “桥梁”,没有 LR 就无法实现函数的调用和返回。
四、任务现场保存
运行现场(Execution Context / CPU Context):指程序在某一时刻被中断或切换时,CPU 所有寄存器的当前值。包括:
通用寄存器(R0–R12)
特殊寄存器:LR(Link Register,返回地址)、PC(Program Counter)、PSR(Program Status Register)
1.任务切换场景大全
| 场景 | 谁打断谁? | 保存哪些寄存器? | 谁负责保存? | 是否需要完整上下文? |
|---|---|---|---|---|
| 函数调用 | 主调函数 → 子函数 | 按 AAPCS:callee 保存 R4–R11;caller 自行保护 R0–R3 | 编译器自动生成 | ❌ 否(按约定即可) |
| 硬件中断 | 外设 → 当前代码 | 硬件自动保存 R0–R3,R12,LR,PC,xPSR;软件补充 R4–R11(若用到) | 硬件 + ISR 编写者 | ⚠️ 部分(看 ISR 是否调用函数) |
| 任务切换 | 调度器 → 当前任务 | 全部寄存器(R0–R12, LR, PC, PSR, SP) | RTOS 内核(如 FreeRTOS 的 PendSV) | ✅ 是(完整上下文) |
2.硬件中断切换场景
ARM AAPCS(ARM Architecture Procedure Call Standard)约定:
- R0–R3:用于传递前 4 个参数(整型/指针)
- 返回值:放在 R0
- LR(R14):保存返回地址
- R4–R11:属于“被调用者保存寄存器”(callee-saved),若子函数要用,必须先压栈保存,返回前恢复
- R0–R3、R12:属于“调用者保存寄存器”(caller-saved),调用完就可能被破坏,主调函数若需保留,自己保存
| 步骤 | 谁执行 | 动作 | 使用的栈 | 补充说明 |
|---|---|---|---|---|
| 中断前 | 任务 A | 运行,使用 PSP,指向任务栈 | 任务栈 | FreeRTOS 中,任务运行时用 PSP(进程栈指针),MSP 仅作为备用 |
| 中断响应 | 硬件 | 自动切换到 MSP(内核强制) | 系统主栈 | ARMv7-M/ARMv8-M 内核规定:所有异常(中断)处理必须使用 MSP |
| - | 硬件 | 压栈 R0–R3, R12, LR, PC, xPSR 到系统主栈 | 系统主栈 | 这 8 个寄存器是 “硬件自动压栈”,占 32 字节(每个寄存器 4 字节),是固定开销 |
| - | 硬件 | 从向量表加载 ISR 地址,更新 PC 到 ISR 入口;LR 自动设为EXC_RETURN值 | - | LR 不再是任务返回地址,而是EXC_RETURN(特殊值,用于中断返回时判断栈 / 模式) |
| ISR 执行 | 软件(编译器) | 如果 ISR 中使用了 R4–R11,编译器会自动压栈这些寄存器 | 系统主栈 | 这 8 个寄存器是 “软件手动压栈”(编译器生成的序言代码),非硬件自动,占 32 字节(可选) |
| - | 软件 | 执行用户编写的 ISR 代码(如读中断标志、缓存数据) | 系统主栈 | 用户代码的局部变量、函数调用都会占用 MSP 栈 |
| ISR 返回前 | 软件(编译器) | 如果之前压了 R4–R11,编译器会自动弹出这些寄存器 | 系统主栈 | 编译器生成的收尾代码,与压栈对应 |
| 中断返回 | 硬件 | 弹出之前硬件压栈的 8 个寄存器(R0–R3, R12, LR, PC, xPSR) | 系统主栈 | 栈指针 MSP 自动回退,恢复中断前的 CPU 状态 |
| - | 硬件 | 根据 LR(EXC_RETURN)的值,切换回 PSP(若中断前是任务运行) | - | EXC_RETURN 的 bit2 决定返回后用 MSP 还是 PSP:0=MSP,1=PSP(任务场景下是 1) |
| 返回后 | 任务 A | 继续执行,使用 PSP 指向任务栈 | 任务栈 | 完全恢复中断前的任务执行状态 |
3.硬件中断前后SP指针如何恢复:MSP和PSP?
-
中断前,任务 A 使用的是 PSP,硬件响应中断时自动切换为 MSP,所以 PSP 的值没有被改变,只是不再使用。
-
中断返回时,硬件根据
EXC_RETURN切回 PSP,PSP 的值就是任务 A 原来的栈顶,所以任务栈立即可用。 -
MSP 在中断过程中被使用,硬件压栈/出栈会自动调整 MSP,返回后 MSP 也恢复到压栈前的值。
因此,SP 的恢复是通过栈指针的切换 + 硬件自动调整实现的,无需保存 SP 的具体数值。
4. FreeRTOS 任务切换场景(详细的请到我主页看下一章)
着重讲任务切换(FreeRTOS 核心机制)
这是最复杂的情况!
- 任务 A 正在跑,调度器决定切到任务 B。
- A 和 B 完全无关,B 不会帮 A 保存任何东西。
- 必须保存 A 的完整现场:所有寄存器(R0–R12, LR, PC, PSR, SP...)
任务切换简化流程如下:
- 触发 PendSV 异常(专用于上下文切换的低优先级异常,在NVIC中的优先级最低)
- 在 PendSV_Handler 中:
- 判断是否要切换任务
- 手动保存当前任务的完整寄存器到其自己的 TCB(Task Control Block)的栈中
- 从新任务的 TCB 栈中恢复所有寄存器
- 异常返回时,CPU 自动从新任务的栈弹出寄存器 → 实现切换!
5.硬件中断和任务切换对比
| 维度 | 硬件中断现场保存 | FreeRTOS 任务切换现场保存 |
|---|---|---|
| 触发方式 | 硬件异步触发(如定时器、串口中断) | 软件主动触发(如taskYIELD()、滴答定时器中断) |
| 执行主体 | 纯硬件(CPU 内核自动完成) | 软件(FreeRTOS 的 PendSV 中断服务函数) |
| PC 的保存逻辑 | 硬件自动压栈(中断响应时强制保存) | 软件手动压栈(PendSV 中主动保存) |
| 保存的寄存器范围 | 硬件默认压栈:R0-R3、R12、LR、PC、xPSR软件按需压栈:R4-R11(编译器决定) | 软件全量压栈:R0-R12、LR、PC、xPSR(所有寄存器)(因为任务切换需要完整保存上下文) |
| 使用的栈 | 固定使用 MSP(系统主栈) | 切换前用当前任务的 PSP(任务栈),压栈到该任务的私有栈中 |
| 保存目的 | 中断返回后,恢复同一个任务的执行 | 切换后,恢复另一个任务的执行 |
| 灵活性 | 固定流程,无软件干预空间 | 可定制(如 FreeRTOS 可配置栈大小、保存策略) |
6.为何切换任务时没把SP压栈?
- FreeRTOS 任务切换时不把 SP(PSP)压栈保存,因为 SP 是栈的 “指针”,而非栈内的 “数据”;
- SP(PSP)的数值会被单独保存到任务控制块(TCB)的
pxTopOfStack成员中,作为恢复上下文的 “钥匙”。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)