本文章适合有MCU和FreeRTOS的使用基础,进一步学习FreeRTOS内核与单片机底层

参考视频与文章:

01_任务的引入_ARM架构_汇编简明教程_哔哩哔哩_bilibili

ARM架构汇编基础-CSDN博客

一、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/LDRSTRB/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...)

任务切换简化流程如下:

  1. 触发 PendSV 异常(专用于上下文切换的低优先级异常,在NVIC中的优先级最低
  2. 在 PendSV_Handler 中:
    • 判断是否要切换任务
    • 手动保存当前任务的完整寄存器到其自己的 TCB(Task Control Block)的栈中
    • 从新任务的 TCB 栈中恢复所有寄存器
  3. 异常返回时,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成员中,作为恢复上下文的 “钥匙”。
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐