LiteOS-M 切换汇编逐行图解(2):指令·异常机制·全景表

本文是切换汇编系列的第二篇 。上一篇《(1)汇编是什么·寄存器与栈》搭好了三块地基------为什么切换必须写汇编栈是什么16 寄存器地图与 AAPCS 分工 。本篇接着搭另外三块地基:本文件用到的指令异常进出栈机制 、和 los_dispatch.S 全景表。

本篇读者画像 :已经知道 R0~R15、MSP/PSP、AAPCS 基本概念,但看到 mrsstmfdbx lr 还会愣一下。看完本篇,这些符号会变成"老朋友"。

工程限定 :OpenHarmony LiteOS-M v3.0-LTS + STM32MP157 M4 独立启动(Cortex-M4 + 硬件 FPU,GCC + Makefile)。讲解对象是内核 arch 层的 los_dispatch.S,本工程一行未改

文章目录

    • 〇、先看地图:本篇三站路线图(整体)
    • [一、第①站:本文件用到的指令(只精讲 4 组核心)](#一、第①站:本文件用到的指令(只精讲 4 组核心))
      • [1.1 整体:4 组指令干什么](#1.1 整体:4 组指令干什么)
      • [1.2 局部:数据搬运------mov / ldr / str](#1.2 局部:数据搬运——mov / ldr / str)
      • [1.3 局部:多寄存器存取------stmfd / ldmfd(切换汇编的主角)](#1.3 局部:多寄存器存取——stmfd / ldmfd(切换汇编的主角))
      • [1.4 局部:特殊寄存器读写------mrs / msr / cpsid / cpsie](#1.4 局部:特殊寄存器读写——mrs / msr / cpsid / cpsie)
      • [1.5 局部:跳转------b / bx / blx](#1.5 局部:跳转——b / bx / blx)
      • [1.6 局部:其他指令(按需查表)](#1.6 局部:其他指令(按需查表))
    • [二、第②站:异常进出栈与 EXC_RETURN](#二、第②站:异常进出栈与 EXC_RETURN)
      • [2.1 整体:中断进来,硬件自动干什么](#2.1 整体:中断进来,硬件自动干什么)
      • [2.2 局部:EXC_RETURN------只需要记两个值](#2.2 局部:EXC_RETURN——只需要记两个值)
      • [2.3 局部:异常返回------同一条 bx lr,两种身份](#2.3 局部:异常返回——同一条 bx lr,两种身份)
      • [2.4 重点·一步一步看:异常进出栈全过程](#2.4 重点·一步一步看:异常进出栈全过程)
    • [三、第③站:`los_dispatch.S` 全景表(7 常量 + 4 函数 + 3 数据结构)](#三、第③站:los_dispatch.S 全景表(7 常量 + 4 函数 + 3 数据结构))
      • [3.1 整体:文件骨架长什么样](#3.1 整体:文件骨架长什么样)
      • [3.2 局部:4 个函数是整条流水线的 4 个岗位](#3.2 局部:4 个函数是整条流水线的 4 个岗位)
      • [3.3 局部:7 个常量(只需要记两个关键的)](#3.3 局部:7 个常量(只需要记两个关键的))
      • [3.4 局部:3 个数据结构(后面四篇反复用到)](#3.4 局部:3 个数据结构(后面四篇反复用到))
    • 四、小结:本篇你该带走的三句话

〇、先看地图:本篇三站路线图(整体)

读完本篇,你应该能回答三个问题:

  1. los_dispatch.S 里那些汇编指令(mov/ldr/str/stmfd/ldmfd/mrs/msr/bx ...)每条干什么?
  2. 中断进来和出去时,硬件自动干了什么?那个叫 EXC_RETURN 的魔术值是干嘛的?
  3. los_dispatch.S 全文的骨架是什么?7 个常量、4 个函数、3 个数据结构各对应什么?

本篇只搭这三块地基,不讲具体函数怎么干活 ------那是第 3~6 篇的事。本篇看完,你看到任何一条汇编都能知道"它在动哪个寄存器、指令字面啥意思",看到 bx lr 能知道"它在普通函数里还是异常里"。

  • 第①站 指令los_dispatch.S 用到的指令不到 20 条,只精讲 4 组核心(数据搬运、多寄存器存取、特殊寄存器读写、跳转),其他按需查表。
  • 第②站 异常进出栈与 EXC_RETURN :异常进入时硬件自动压 8/26 字、把魔术值塞进 LR;异常返回时硬件识别魔术值自动弹栈------同一条 bx lr 两种身份。
  • 第③站 los_dispatch.S 全景表:7 个常量 + 4 个函数 + 3 个数据结构,是后面四篇逐行精讲的地图。

本系列主线先剧透一句:任务切换本质上像工人交接班。上一篇讲了"交接班需要什么柜子(栈)和哪些工具(寄存器)",本篇讲"交接班的规矩(指令与异常机制)"和"整条流水线由哪些岗位组成(四函数)"。下一篇开始,一个岗位一个岗位地拆。


一、第①站:本文件用到的指令(只精讲 4 组核心)

1.1 整体:4 组指令干什么

los_dispatch.S 用到的指令不到 20 条。这里只精讲看懂切换汇编必用的 4 组,其他几组(运算/屏障)放在最后按需查表。

干什么 本文件的典型用法
数据搬运 在寄存器之间、寄存器与内存之间搬数字 mov r0, #2ldr r0, [r1, #4]str r5, [r4]
多寄存器存取 一次性搬一批寄存器进出栈 stmfd r0!, {r4-r12}ldmfd r12!, {r0-r7}
特殊寄存器读写 碰 PSP/CONTROL/PRIMASK 这些 C 够不着的 mrs r12, PRIMASKmsr psp, r12cpsid i
跳转 改 PC,包括条件跳、直跳、调函数 bnebx r6bx lrblx OsSchedTaskSwitch

下面逐组讲。

1.2 局部:数据搬运------mov / ldr / str

asm 复制代码
mov   r0, #2            @ 立即数 → 寄存器:r0 = 2
mov   lr, r5            @ 寄存器 → 寄存器:lr = r5
ldr   r0, [r1, #4]      @ 读内存:r0 = *(r1 + 4)
str   r5, [r4]          @ 写内存:*(r4) = r5
  • mov 像"把一张纸条上的数字抄到另一张"------抄的是立即数或寄存器。
  • ldr/str 像"从抽屉取/往抽屉放"------ldr 读内存,str 写内存,[r1, #4] 表示"r1 地址 + 4 偏移处"。

【一句话】mov 是寄存器之间或立即数→寄存器的搬运;ldr/str 是寄存器和内存之间的搬运,方括号 [r1, #4] 表示"从 r1+4 这个地址"。

1.3 局部:多寄存器存取------stmfd / ldmfd(切换汇编的主角)

asm 复制代码
stmfd r0!, {r4-r12}     @ 压栈:先减地址再存
ldmfd r12!, {R0-R7}     @ 出栈:先取再增

两个名字一套指令stmfd = stmdb(先减后存),ldmfd = ldmia(先取后增)。读 FreeRTOS 看到 stmdb,读 LiteOS 看到 stmfd,是同一条指令------这是 UAL(统一书写规范)允许的同义词。

fd = full descending(满递减栈),是 AAPCS 规定的栈型------栈向低地址长,SP 指向最后压入的数据。压栈出栈固定用这俩。

【类比·搬箱子】stmfd r0!, {r4-r12} 像"把 9 个箱子(r4~r12)依次摞到 r0 指向的位置下方"------地址先减,箱子从小编号到大编号依次摆低到高。! 表示搬完把最新地址写回 r0。

铁规则:寄存器号小的放低地址stmfd r0!, {r4-r12} 存完后,内存从低到高是 r4, r5, ..., r11, r12------这条规则决定了栈上数据的摆放顺序,第 5 篇对账 TaskContext 结构体时全靠它。

【一句话】stmfd/ldmfd 是一次搬一批寄存器进出栈,寄存器号小的放低地址------这条规则决定了栈上数据布局,对账结构体时全靠它。

1.4 局部:特殊寄存器读写------mrs / msr / cpsid / cpsie

asm 复制代码
mrs r12, PRIMASK        @ 特殊寄存器 → 通用寄存器(读)
msr CONTROL, r0         @ 通用寄存器 → 特殊寄存器(写)
msr psp, r12            @ 写栈指针
cpsid i                 @ 关中断(PRIMASK=1)
cpsie i                 @ 开中断(PRIMASK=0)

mrs/msr唯二能碰特殊寄存器的指令------这就是第 1 篇说的"CPU 母语里专门碰特殊寄存器的两句话"。

cpsid/cpsie 是开关中断的专用指令------不用先搬个寄存器存值,一条搞定,比 msr primask 顺手。

【一句话】mrs/msr 是读写 PSP/CONTROL/PRIMASK 这些特殊寄存器的唯一通道;cpsid/cpsie 是开关中断的专用快捷指令。

1.5 局部:跳转------b / bx / blx

asm 复制代码
bne  __DisabledFPU      @ 条件跳转:不相等(Z=0)就跳
bx   r6                 @ 跳到 r6 里的地址(bit0 是 Thumb 标志)
bx   lr                 @ 返回;异常处理函数里触发"异常返回"
blx  OsSchedTaskSwitch  @ 调 C 函数:返回地址存入 lr 再跳

条件后缀速查(接在 b 后面用,最常用就 ne/eq 两个):

后缀 含义
eq 相等(Z=1)
ne 不等(Z=0)
其他(cs/cc/ge/lt...) 用到再查 ARM 手册

bxblx 的区别:blx 会把返回地址存进 LR 再跳------就是"调用函数";bx 不存------是"直接跳过去不回来了"。

【一句话】blx = 调函数(存返回地址),bx = 直跳(不存),bne/eq = 条件跳。

1.6 局部:其他指令(按需查表)

这几条用得少或简单,列在这儿供你读到时回查:

指令 干什么 何时遇到
add r12, r12, #100 加法 第3篇 第四步 FPU 偏移对账
and r1, r1, #0x00F00000 位与(掩码提取) 第3篇 第四步 判 CPACR
cmp r1, #0x00F00000 比较(只更新标志位,不跳转) 第3篇 第四步 判 CPACR
vstmdb/vldmia FPU 版多寄存器存取 第5篇 存/取 d8~d15
vpush/vpop {s0} 单 FPU 寄存器压/弹 第3篇 第五步 激活 FPCA
dsb/isb 数据/指令同步屏障 第4篇 HalTaskSchedule 尾巴

【读源码方法论】读到不熟的指令别慌------回来查这张表就行。本系列涉及的所有指令都在这张表里。


二、第②站:异常进出栈与 EXC_RETURN

2.1 整体:中断进来,硬件自动干什么

异常(中断)进入时,硬件自动做三件事,软件完全不用管:

  1. 把 8 个字压入当前栈 (任务态就是 PSP):
    • R0、R1、R2、R3、R12、LR、PC、xPSR------低地址到高地址正好按这个顺序,恰好就是第 1 篇讲的"调用方保管"那批。
    • FPU 活跃(FPCA=1)时再加压 18 个字(S0~S15、FPSCR、保留),称"扩展帧"。
    • 8 字 = 基本帧,26 字 = 扩展帧
  2. LR 被硬件填成 EXC_RETURN 魔术值 ------它不是返回地址,是一张"返回方式说明书"。
  3. IPSR 更新为异常号,取向量、跳异常处理函数(Handler 模式、用 MSP)。

【一句话】中断进来时硬件自动压 8/26 字进当前栈,并把"返回方式说明书"(EXC_RETURN)塞进 LR------这部分软件完全不用管。

2.2 局部:EXC_RETURN------只需要记两个值

EXC_RETURN 是个魔术值------它不是地址,是硬件识别"怎么返回"的说明书。低 4 位编码返回方式:

返回到 用哪个栈 帧类型
0xFFFFFFF1 Handler 模式 MSP 基本
0xFFFFFFF9 Thread 模式 MSP 基本
0xFFFFFFFD Thread 模式 PSP 基本
0xFFFFFFED Thread 模式 PSP 扩展(含 FPU)

零基础只需要记两个

  • 0xFFFFFFFD:返回任务态、用 PSP、基本帧(无 FPU)。
  • 0xFFFFFFED:返回任务态、用 PSP、扩展帧(有 FPU)。

另两个(0xFFFFFFF1/9)本文用不到------那是返回 Handler 模式或用 MSP 的场景。

【一句话】记住两个魔术值:0xFFFFFFFD(任务态+PSP+基本帧)、0xFFFFFFED(任务态+PSP+扩展帧)。LR 里装这俩值时,bx lr 就是异常返回。

2.3 局部:异常返回------同一条 bx lr,两种身份

异常返回 :只要把 EXC_RETURN 送进 PC(bx lr 或出栈到 PC),硬件识别出魔术值,自动把栈上那 8/26 个字弹回寄存器------PC 一恢复,被打断的代码从断点继续,像什么都没发生过。

所以同一条 bx lr两种身份

  • 普通函数里是"返回调用处"。
  • 异常处理函数里是"触发异常返回"。

区别只在 LR 里装的是普通地址还是魔术值。

【类比·bx lr 两种身份】bx lr 像回家开门------普通函数里 LR 是家门口地址,开门就回家;异常里 LR 是一把特殊钥匙(EXC_RETURN 魔术值),开门的瞬间硬件识别出"哦你是从异常里出来的",自动帮你恢复中断现场。

2.4 重点·一步一步看:异常进出栈全过程

上面讲的是结论,下面把"进入 → 处理 → 返回"走一遍,每一步都不跳过:

把 5 步串成一句话 :导演喊停(中断发生)→ 助理收便签(硬件压 8/26 字)→ 给一把特殊钥匙(LR=EXC_RETURN)→ 你在会议室办完事(异常处理)→ 把钥匙交前台开门(bx lr)→ 助理按钥匙说明还原便签、你回断点继续。

切换汇编存在的意义,就是补齐"助理不收的那部分"------R4R11、d8d15 这些 callee-saved 大件,得靠 HalPendSV 自己搬。


三、第③站:los_dispatch.S 全景表(7 常量 + 4 函数 + 3 数据结构)

3.1 整体:文件骨架长什么样

los_dispatch.S 的整体结构是:

复制代码
文件头(.syntax/.arch/.fpu/.thumb 四件套自我介绍)
    ↓
7 个常量定义(.equ)
    ↓
4 个函数实现(HalStartToRun / HalIntLock/UnLock/Restore / HalTaskSchedule / HalPendSV)

这四行开头的"自我介绍"在第 1 篇已经讲过,本篇不再重复。重点是:这个文件去掉版权头不到 170 行,真正的指令不到 100 条,却撑起了 LiteOS-M 在 Cortex-M4 上的全部切换动作。

【一句话】los_dispatch.S 的骨架 = 四件套头 + 7 常量 + 4 函数;不到 100 条指令撑起启动/临界区/触发/搬运四件事。

3.2 局部:4 个函数是整条流水线的 4 个岗位

第 1 篇见过这个表,这里再补一列"岗位称呼",方便和第 5 篇的"职场角色"图对上:

函数 岗位 干什么 谁调它
HalStartToRun 启动员 启动第一个任务(切 PSP + 直跳任务入口) LOS_Start() 间接调
HalIntLock / HalIntUnLock / HalIntRestore 保安 关/开/恢复中断(临界区三件套) LOS_IntLock() 等 C 接口
HalTaskSchedule 调度员 触发一次 PendSV,请求切换 LOS_Schedule() 等调度入口
HalPendSV 搬运工 真正的现场搬运:存旧任务、取新任务 PendSV 异常(硬件调它)

3.3 局部:7 个常量(只需要记两个关键的)

符号 干什么
OS_FPU_CPACR 0xE000ED88 FPU 协处理器访问控制寄存器
OS_FPU_CPACR_ENABLE 0x00F00000 FPU 使能位掩码
OS_NVIC_INT_CTRL 0xE000ED04 中断控制状态寄存器 ICSR;写 bit28 挂起 PendSV(第 4 篇按门铃用)
OS_NVIC_SYSPRI2 0xE000ED20 系统异常优先级 SHPR3;设 PendSV/SysTick 最低优先级(第 3 篇 第三步 设最低优先级用)
OS_NVIC_PENDSV_PRI 0xF0F00000 一次写两个字段:PRI_14=0xF0、PRI_15=0xF0
OS_NVIC_PENDSVSET 0x10000000 ICSR bit28 PENDSVSET,往 ICSR 写它 = 挂起 PendSV
OS_TASK_STATUS_RUNNING 0x0010 任务运行态标志。本文件定义了但没用------历史遗留

零基础只需要记两个

  • OS_NVIC_INT_CTRL(ICSR)------第 4 篇"按门铃"用。
  • OS_NVIC_SYSPRI2(SHPR3)------第 3 篇 第三步"设最低优先级"用。

其他几个用到再回来查。OS_TASK_STATUS_RUNNING 是死代码,v3.0-LTS 里这个标志在 C 侧设置,汇编里定义了没用上。

【读内核方法论】读内核别背寄存器名字------背地址。同一个寄存器不同代码叫法不同(LiteOS 叫 SYSPRI2,FreeRTOS 叫 portNVIC_SYSPRI2,ARM 手册叫 SHPR3),但地址 0xE000ED20 是固定的。

3.4 局部:3 个数据结构(后面四篇反复用到)

c 复制代码
/* los_task.h */
typedef struct {
    LosTaskCB   *runTask;   /* 偏移 0:当前运行任务 TCB */
    LosTaskCB   *newTask;   /* 偏移 4:调度器选好的下一个任务 TCB */
} LosTask;                  /* 全局变量 g_losTask */

typedef struct TagTaskCB {
    VOID *stackPointer;     /* 偏移 0:TCB 第一个成员就是任务栈指针! */
    ...
} LosTaskCB;

/* los_arch_context.h */
typedef struct {
    /* ... 具体字段顺序在第5篇栈帧几何里详讲 ... */
} TaskContext;              /* 任务上下文,大小 204(FPU)或 68(非FPU)字节 */

记住三个偏移关系:

  • ldr r0, [r1, #4] 读的是 g_losTask.newTask(偏移 4)。
  • ldr r0, [r0] 读的是 TCB->stackPointer(偏移 0,第一个成员)。
  • TaskContext 的大小(204/68)必须与运行期汇编偏移严格一致------这就是铁律③。

核心结论TCB 只记一个栈指针,寄存器快照全在任务栈上------这是"任务上下文存在哪"的答案。一个任务的所有状态,最终浓缩成一个栈指针。

【一句话】g_losTask 是 {当前任务, 下一任务} 两个指针;TCB 第一个成员是栈指针;TaskContext 大小必须与汇编偏移对齐------三个数据结构撑起整个切换逻辑。


四、小结:本篇你该带走的三句话

  1. 指令只分 4 组核心:数据搬运(mov/ldr/str)、多寄存器存取(stmfd/ldmfd,寄存器号小的放低地址)、特殊寄存器读写(mrs/msr/cpsid/cpsie)、跳转(bx/blx/bne)------其他指令按需查表。
  2. 异常进出栈是硬件自动的 :中断进来硬件压 8/26 字进当前栈、把 EXC_RETURN 塞进 LR;返回时硬件识别魔术值自动弹栈------bx lr 在异常里就是"触发异常返回",与普通函数返回是同一指令两种身份。
  3. los_dispatch.S 的骨架就三部分 :7 个常量 + 4 个岗位函数 + 3 个数据结构;其中 g_losTask / TCB->stackPointer / TaskContext 是后面所有切换动作的数据根基。

地基搭完。从下一篇开始讲具体函数怎么干活。

下篇预告 :《LiteOS-M 切换汇编逐行图解(3):HalStartToRun 首任务五步直跳》------从"演员第一次上场"的类比开始,先讲清这个函数整体要干什么(开机后让第一个任务跑起来),再逐行讲透五步:设 PendSV/SysTick 最低优先级、切 PSP、取首任务栈指针、FPU/非 FPU 双路径恢复现场、vpush/vpop 激活 FPCA + bx r6 直跳。


相关推荐
OH_TPC1 小时前
【鸿蒙优选三方库】@react-native-ohos/react-native-svg:SVG 矢量图形渲染组件
华为·harmonyos·鸿蒙
Brilliantwxx3 小时前
【STM32】 I2C阻塞通信实战与HAL源码(教程篇)
stm32
从零开始的嵌入式之旅3 小时前
day44
arm开发·经验分享·笔记·嵌入式硬件
李游Leo4 小时前
HarmonyOS 7 实战开发 01:搭建内容工作台基础框架
harmonyos
硬禾科技4 小时前
连接世界的工程哲学
嵌入式硬件·测试工具·自动化·硬件工程
深圳市青牛科技实业有限公司 小芋圆4 小时前
GC1808:高性能、低成本的立体声音频ADC芯片
人工智能·嵌入式硬件·无人机·太阳能逆变器
fhq_fn_jgd4 小时前
51单片机(串口通信)
单片机·嵌入式硬件
深圳市元恩微科技开发有限公司4 小时前
兆讯恒达(北京兆讯)MH2457 系列 国产屏驱 MCU
单片机
沸速存储5 小时前
AI 机器人靠哪些内存与存储驱动整机运行?
服务器·科技·嵌入式硬件·电脑