LiteOS-M 切换汇编逐行图解(5):HalPendSV 任务切换的现场搬运(五阶段逐行)

本文是切换汇编系列第五篇 。前四篇讲完了语法打底(第 1、2 篇)、首任务启动(第 3 篇)、临界区与触发(第 4 篇);本篇讲最后也是最重要的一段------HalPendSV 的现场搬运,也就是"任务切换"这四个字真正发生时,CPU 和那几十行汇编在干什么。

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

文章目录

    • 〇、先看地图:本篇三站路线图(整体)
    • 一、第①站:开场------舞台换演员(存旧取新总逻辑)
      • [1.1 整体:舞台换演员](#1.1 整体:舞台换演员)
      • [1.2 局部:进场前提------硬件已经做了什么](#1.2 局部:进场前提——硬件已经做了什么)
    • 二、第②站:五阶段逐行精讲
      • [2.1 整体:五阶段总览](#2.1 整体:五阶段总览)
      • [2.2 局部:阶段① 序章------保存中断状态,关中断](#2.2 局部:阶段① 序章——保存中断状态,关中断)
      • [2.3 局部:阶段② 问调度器------C 算法与汇编的交接面](#2.3 局部:阶段② 问调度器——C 算法与汇编的交接面)
        • [为什么 push r12?](#为什么 push r12?)
        • [为什么 push lr?](#为什么 push lr?)
        • [`cmp` 在 `mov` 之前的排序技巧](#cmpmov 之前的排序技巧)
      • [2.4 局部:阶段③ 存旧任务------把 R4-R12 和 d8~d15 收进旧任务自己的栈](#2.4 局部:阶段③ 存旧任务——把 R4-R12 和 d8~d15 收进旧任务自己的栈)
        • [`stmfd r0!, {r4-r12}`:寄存器号小的放低地址](#stmfd r0!, {r4-r12}:寄存器号小的放低地址)
        • [`str r0, r6`:旧任务的全部家当=一个栈指针](#str r0, [r6]:旧任务的全部家当=一个栈指针)
      • [2.5 局部:阶段④ 换任务------改两个指针](#2.5 局部:阶段④ 换任务——改两个指针)
      • [2.6 局部:阶段⑤ 恢复新任务------按存旧的镜像流程反向搬运](#2.6 局部:阶段⑤ 恢复新任务——按存旧的镜像流程反向搬运)
      • [2.7 局部:对称性一览------存旧 ↔ 取新逐位镜像](#2.7 局部:对称性一览——存旧 ↔ 取新逐位镜像)
    • [三、第③站:栈帧几何------204 字节与 `TaskContext` 逐槽对齐](#三、第③站:栈帧几何——204 字节与 TaskContext 逐槽对齐)
      • [3.1 整体:204 字节怎么摞](#3.1 整体:204 字节怎么摞)
      • [3.2 局部:204 字节逐槽账单表](#3.2 局部:204 字节逐槽账单表)
    • 四、源码小毛刺(诚实档)
    • 五、小结:本篇你该带走的三句话

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

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

  1. HalPendSV 整体在搬运什么? ------ 答:把旧任务没来得及自己收的 callee-saved 收进旧任务栈,再从新任务栈取出摆好。
  2. 这几十行汇编能分成几个阶段? ------ 答:五阶段------存旧 → 过户 → 取新 → 恢复 PSP → 异常返回。
  3. 204 字节的栈帧是怎么和 TaskContext 结构体逐槽对齐的? ------ 答:软件存低地址、硬件弹高地址,合计 204 字节(FPU)或 68 字节(非 FPU)。

本篇先看整体,再逐站拆:

  • 第①站 开场HalPendSV 像舞台总监,帮旧演员把道具收进柜子、从新演员柜子取出。
  • 第②站 五阶段逐行:序章(存中断状态)→ 问调度器 → 存旧 → 过户 → 取新 + 异常返回。
  • 第③站 栈帧几何 :204 字节的来龙去脉,与 TaskContext 逐槽对齐(铁律③具象化)。

一句话记住:HalPendSV 五阶段------存旧 → 过户 → 取新 → 恢复 PSP → 异常返回。


一、第①站:开场------舞台换演员(存旧取新总逻辑)

1.1 整体:舞台换演员

正常任务切换(本篇的 HalPendSV)像舞台换演员

  • 旧演员正在演,被导演(PendSV 异常)打断。
  • 旧演员下场前把戏服道具 (R4-R12、d8~d15 这些 callee-saved 寄存器)收进自己的柜子(旧任务自己的栈)。
  • 柜子钥匙(栈指针)记在 TCB 的 stackPointer 字段里。
  • 新演员上场前从自己的柜子取出来穿戴好(恢复 R4-R12、d8~d15)。
  • 硬件自动把"上场台词本"(R0~R3、R12、LR、PC、xPSR 这 8/26 字硬件帧)弹回去------新演员从上次被打断的台词继续演。

这就是 HalPendSV 的全部逻辑:存旧(把 callee-saved 收进旧任务栈)→ 取新(从新任务栈恢复 callee-saved)。硬件帧由 CPU 自动压弹,软件只补存 callee-saved 那批。

【类比·舞台换演员】HalPendSV 是舞台总监------旧演员被叫下场(PendSV 进场),先帮他收戏服道具进自己柜子(存旧 R4-R12、d8~d15),柜子钥匙交给 TCB 保管;然后从新演员的柜子取出他的戏服道具(取新),让他穿戴好上场。硬件负责自动弹"台词本"(PC 槽指向断点),新演员从被打断的台词接着演。

1.2 局部:进场前提------硬件已经做了什么

PendSV 异常触发后硬件自动进场,此时:

  • Handler 模式(异常态)。
  • 用 MSP(异常态用主栈)。
  • 硬件已把 8/26 字硬件帧 压进旧任务的 PSP 栈(FPU 活跃时是 26 字扩展帧)。
  • LR = EXC_RETURN (FPU 口径下是 0xFFFFFFED)。

剩下的 callee-saved 寄存器(R4-R12、d8~d15),就是这段汇编要搬运的货。


二、第②站:五阶段逐行精讲

2.1 整体:五阶段总览

先把五阶段摊开看一眼,后面每一段代码都会回到这张图:

阶段 干什么 关键指令
① 序章 存中断状态 + 关中断 mrs r12, PRIMASK / cpsid I
② 问调度器 调 C 算法选新任务 push {r12,lr} / blx OsSchedTaskSwitch
③ 存旧 收进旧任务自己的栈 stmfd r0!, {r4-r12} / str r0, [r6]
④ 过户 改两个指针 str r0, [r5](runTask = newTask)
⑤ 取新 恢复新任务 + 异常返回 ldmfd r1!, {r4-r12} / msr psp, r1 / bx lr

源码按五阶段拆解(每段都是原文照抄,注解是我加的)。每阶段先说"做什么",再说"代码怎么做"。

2.2 局部:阶段① 序章------保存中断状态,关中断

asm 复制代码
HalPendSV:
    mrs   r12, PRIMASK             @ r12 = 进异常前的中断屏蔽状态
    cpsid  I                      @ 关中断:搬运现场的过程不能被打断

做什么:记下中断屏蔽状态(等下恢复用),然后关中断。

为什么拿 r12 当仓库?r0~r3、lr 马上都有大用(要传参、存 EXC_RETURN),r12 是 caller-saved 的临时寄存器,此刻闲着------下文会看到它一路被"借"得恰到好处。

【类比·r12 当仓库】r12 像进门时手里拎的塑料袋------临时装一下钥匙(PRIMASK 快照),等会出门(恢复时)再倒出来。r0~r3、lr 这些"正经工位"接下来都有正经活,r12 这种"临时杂役"此刻空着,正好借用。

2.3 局部:阶段② 问调度器------C 算法与汇编的交接面

注:下面出现的 HalTaskSwitch:HalPendSV 函数内部的一个局部标号,不是独立函数,仅用于阶段③/⑤ 的跳转。

asm 复制代码
HalTaskSwitch:
    push  {r12, lr}                @ r12(PRIMASK快照)和lr(EXC_RETURN)要过C函数这一关,先保护
    blx   OsSchedTaskSwitch        @ 调C函数:调度算法选出新任务,返回值放r0(0=不用切)
    pop   {r12, lr}                @ 取回两件行李
    cmp   r0, #0                   @ 判断返回值,更新标志位
    mov   r0, lr                   @ 顺手把EXC_RETURN暂存进r0(mov不改标志位!)
    bne   TaskContextSwitch        @ 返回值非0 → 要切任务
    msr   PRIMASK, r12             @ 不用切:恢复中断状态
    bx    lr                       @ 异常返回,回到被打断的原任务,一切如旧

做什么 :调 C 函数 OsSchedTaskSwitch 让调度算法选新任务;返回 0 不用切(直接异常返回回原任务),返回非 0 要切(跳到阶段③)。

三个细节全是真功夫

为什么 push r12?

AAPCS 里 r12 是 caller-saved------C 函数有权随意用掉它。r12 里装着 PRIMASK 快照,过 blx 这关必须先 push 后 pop,否则 C 函数可能把它弄丢。

【类比·push r12】像你要进 C 函数这个"可能有污染的车间",手里的贵重物品(PRIMASK 快照)得先存进门口储物柜(push {r12, lr}),出来再取回(pop)。

为什么 push lr?

blx 会把返回地址写进 lr,覆盖掉 EXC_RETURN 。丢了它,最后 bx lr 就不是异常返回而是跳飞。

cmpmov 之前的排序技巧

mov 不改标志位,所以 bne 判断的仍是 cmp r0, #0 的结果。要是先 mov r0, lrcmp,比较的对象就错了------r0 已经被覆盖成 EXC_RETURN 了。

【类比·排序技巧】像考试时先判分(cmp 更新标志位)再誊抄答案(mov 不改标志位)------顺序不能反,反了就误判。bne 看的是 cmp 那一瞬间的判分结果,mov 后誊抄不动判分。

2.4 局部:阶段③ 存旧任务------把 R4-R12 和 d8~d15 收进旧任务自己的栈

asm 复制代码
TaskContextSwitch:
    mov   lr, r0                   @ EXC_RETURN从r0放回lr(r0接下来要用)
    mrs   r0, psp                  @ r0 = 旧任务的PSP(硬件帧基址)
    stmfd r0!, {r4-r12}            @ ★R4~R12压入旧任务栈,r0随之下移36字节
                                   @  注意:r12此刻=PRIMASK快照,正好落进帧的uwPriMask槽!
    ldr.w r3, =OS_FPU_CPACR        @ 运行时再判一次FPU
    ldr   r3, [r3]
    and   r3, r3, #OS_FPU_CPACR_ENABLE
    cmp   r3, #OS_FPU_CPACR_ENABLE
    bne   __DisabledFPU1
    vstmdb r0!, {d8-d15}           @ FPU路径:d8~d15(=S16~S31)再压64字节

__DisabledFPU1:
    ldr   r5, =g_losTask
    ldr   r6, [r5]                 @ r6 = 旧任务TCB
    str   r0, [r6]                 @ ★TCB->stackPointer = r0:旧任务的全部家当=一个栈指针

★ 号两行是全文最核心的两笔

stmfd r0!, {r4-r12}:寄存器号小的放低地址

9 个寄存器入栈,从低到高正好是 R4, R5, ..., R11, R12------与 TaskContextuwR4...uwR11, uwPriMask 槽位严丝合缝

妙在 r12 恰好装着 PRIMASK,顺势落进 uwPriMask 槽------每个任务的中断屏蔽状态就这样跟着自己的栈走,恢复时各回各家。

【类比·r12 落 uwPriMask 槽】像收行李时箱子按编号从低到高摞(R4 在最底,R12 在最上),而 r12 里恰好装着 PRIMASK 快照------它自然落进最上面那个标着"中断状态"的格子。这是巧合也是设计:stmfd {r4-r12} 把 r12 当最后一个压,正好对上 TaskContextuwPriMask 放在 R4~R11 之后。

str r0, [r6]:旧任务的全部家当=一个栈指针

旧任务的"全部状态"(R4-R12、d8~d15 都在栈上了)最终浓缩成一个栈指针 存进 TCB 的 stackPointer 字段。第 3 篇讲过"TCB 只记一个栈指针,寄存器快照全在任务栈上"------这就是落笔处。

【类比·浓缩成一个栈指针】旧演员下台前,戏服道具全收进自己柜子,柜子钥匙交给 TCB 保管。TCB 不需要记每件道具在哪------记一把钥匙(栈指针)就够,下次取时开柜子全在里面。

2.5 局部:阶段④ 换任务------改两个指针

asm 复制代码
    ldr   r0, [r5, #4]             @ r0 = g_losTask.newTask(调度器在阶段②选好的)
    str   r0, [r5]                 @ g_losTask.runTask = newTask:官册过户
    ldr   r1, [r0]                 @ r1 = 新任务的栈指针(TCB第一个成员)

做什么 :把"调度器选好的新任务"正式登记为"当前运行任务"------runTask = newTask。然后取新任务的栈指针 r1。

所谓"切换任务",在数据结构层面就一句 runTask = newTask

2.6 局部:阶段⑤ 恢复新任务------按存旧的镜像流程反向搬运

(承接阶段④,r1 已装好新任务栈指针。)

asm 复制代码
    and   r3, r3, #OS_FPU_CPACR_ENABLE   @ ←源码原样:这行其实是无效指令,见第四节"小毛刺"
    cmp   r3, #OS_FPU_CPACR_ENABLE
    bne   __DisabledFPU2
    vldmia r1!, {d8-d15}           @ FPU路径:先弹新任务的d8~d15,r1上移64字节

__DisabledFPU2:
    ldmfd r1!, {r4-r12}            @ 弹R4~R12(含新任务自己的PRIMASK→r12),r1上移36字节
    msr   psp, r1                  @ PSP指向新任务的硬件帧基址
    msr   PRIMASK, r12             @ ★恢复的是【新任务】自己的中断屏蔽状态
    bx    lr                       @ ★异常返回:硬件弹硬件帧,PC=新任务断点,切换完成

做什么 :按存旧的镜像流程反向搬运------先弹 FPU 寄存器(如果有),再弹 R4~R12(含新任务自己的 PRIMASK),把 PSP 推到新任务的硬件帧基址,恢复新任务的中断状态,最后 bx lr 异常返回。

2.7 局部:对称性一览------存旧 ↔ 取新逐位镜像

动作 存旧任务 恢复新任务
FPU 寄存器 vstmdb r0!, {d8-d15} 向下压 vldmia r1!, {d8-d15} 向上弹
通用寄存器 stmfd r0!, {r4-r12} ldmfd r1!, {r4-r12}
栈指针落账 str r0, [r6] 存进旧 TCB msr psp, r1 送进 PSP
中断状态 r12→uwPriMask 槽随栈保存 uwPriMask 槽→r12→msr PRIMASK

恢复时用的是新任务自己的 PRIMASK ------阶段⑤的 msr PRIMASK, r12 里的 r12,此时是从新任务栈的 uwPriMask 槽弹出来的,不是旧任务的。每个任务的中断屏蔽状态各回各家。


三、第③站:栈帧几何------204 字节与 TaskContext 逐槽对齐

3.1 整体:204 字节怎么摞

【看不懂图没关系】下面这张 ASCII 图信息密,看不懂别慌------记住一句话:"软件存低地址、硬件弹高地址,合计 204 字节与 TaskContext 对齐"。图是给动手验证时对照用的,不是要背的。

FPU 口径,新任务栈从低到高:

复制代码
低地址  ┌──────────────────────────┐ ← TCB->stackPointer(r1 起点)
        │ d8~d15(S16~S31) 64字节   │ ← vldmia 先弹这 64B
        ├──────────────────────────┤
        │ R4~R11 + PriMask  36字节  │ ← ldmfd 再弹这 36B
        ├──────────────────────────┤ ← msr psp 后 PSP 在这
        │ S0~S15+FPSCR+保留 72字节  │ ┐
        ├──────────────────────────┤ ├ 26字扩展硬件帧
        │ R0,R1,R2,R3,R12,LR,PC,xPSR│ ┘ ← bx lr 硬件自动弹这 104B
高地址  └──────────────────────────┘
        合计 64+36+72+32 = 204 字节 = sizeof(TaskContext) ✓

204 字节 = 64 + 36 + 72 + 32 ,每一处都必须和 TaskContext 结构体对应。任何一处的编译期大小与运行期偏移对不上,就是 68/204 错位 HardFault。

3.2 局部:204 字节逐槽账单表

区域 槽位 字节数 由谁压入 由谁弹出 对应 TaskContext 字段
软件帧(低地址) d8d15(S16S31) 64 vstmdb r0!, {d8-d15} vldmia r1!, {d8-d15} uwS16~uwS31
软件帧 R4~R11 + PriMask 36 stmfd r0!, {r4-r12} ldmfd r1!, {r4-r12} uwR4~uwR11, uwPriMask
扩展硬件帧 S0~S15 + FPSCR + 保留 72 异常进入时硬件自动 异常返回时硬件自动 uwS0~uwS15, uwFPSCR, uwReserved
核心硬件帧 R0,R1,R2,R3,R12,LR,PC,xPSR 32 异常进入时硬件自动 异常返回时硬件自动 uwR0~uwR12, uwLR, uwPC, uwxPSR
合计 204

非 FPU 工程 :去掉 d8~d15(64B)和 S0~S15+FPSCR+保留(72B),剩下 36+32 = 68 字节。这就是为什么同一套代码会出现 68/204 两种口径。

对照 los_arch_context.hTaskContext 结构体:S16~S31 在前、R4~PriMask 居中、R0~xPSR 在后、S0~FPSCR 垫底------结构体、压栈顺序、弹出顺序三方完全一致 。非 FPU 工程少掉两段浮点区,68 字节,同样三方一致。这正是铁律③的具象化------编译期结构体大小与运行期汇编偏移严格对齐。


四、源码小毛刺(诚实档)

【先说清】这不是 bug,是历史遗留,不影响运行。提它只是教你怎么读源码------问每条指令"它改变了什么"。

阶段⑤第二行的 and r3, r3, #OS_FPU_CPACR_ENABLE无效指令 ------r3 自阶段③做掩码后未被任何指令改过,与同一个立即数再做一次位与,结果不变,等价于 NOP;真正起作用的是后面的 cmp/bne

这是官方源码里的冗余(也许是早期版本中途动过 r3 的残留)。读内核要敢于下这个判断:每条指令都问一句"它改变了什么",答不上来的就是摆设。当然,改不改它不归我们管------arch 层一行不动是移植纪律。

【类比·冗余指令】像菜谱里写"先把鸡蛋打碗里,再把鸡蛋打碗里"------第二步是空操作,不影响结果。读源码遇到这种"貌似多余"的指令,要敢于判它 NOP,别硬找它的用途。


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

  1. 存旧取新是镜像对称stmfd↔ldmfdvstmdb↔vldmia,软件只搬运 callee-saved(R4-R12、d8d15),硬件帧(R0R3、R12、LR、PC、xPSR)由 CPU 自动压弹------AAPCS 的分工决定了硬件帧和软件帧的分界。
  2. r12 一举两得 :序章借它存 PRIMASK 快照,阶段③恰好顺势落进 uwPriMask 槽,于是"每个任务带着自己的中断屏蔽状态上车下车",恢复时各回各家。
  3. 切换 = 一个栈指针的过户 :数据结构层面就一句 runTask = newTask;旧任务的全部家当浓缩成 TCB 里一个 stackPointer,新任务的现场从它自己的栈里原样取出。

下篇预告:《LiteOS-M 切换汇编逐行图解(6):一次完整调度接力与调试实战》------用一次 LOS_TaskDelay(200) 把四函数串成接力,给出完整调度时序图,并给 objdump / GDB 实战命令,亲手把本篇的栈帧几何对一遍账。全系列收官。


相关推荐
2601_958352901 小时前
不改PCB也能调拾音方向?真相在这。
前端·嵌入式硬件·回声消除·语音模块·降噪处理
M78佐菲1 小时前
ARM学习笔记(四)
linux·arm开发·笔记·嵌入式硬件·学习
马剑威(威哥爱编程)1 小时前
【共创稿事节】HarmonyOS 7 数字身份 DID 实战:TEE 颁发、本人同意、最小化出示
华为·harmonyos
贾伟康1 小时前
【HarmonyOS 7新能力|039】冷启网络预建链工程封装:把接入逻辑放进可维护的分层结构
性能优化·harmonyos·arkts·软件架构·网络优化
半导体憨包1 小时前
光伏组件级MLPE功率优化器DCDC:25年寿命+IP67+雷击,怎么设计才不“翻车“?
嵌入式硬件·硬件工程·dc-dc·半导体芯片
LCG元1 小时前
STM32F103 双ADC交错采样实战:DUALMOD寄存器拆解、DMA位宽陷阱与1.71MSPS有效采样率实测
stm32·单片机·嵌入式硬件
Industio_触觉智能2 小时前
瑞芯微RK3576开发板部署模型|端侧Agent工具调用实现拆解
嵌入式硬件·agent·智能硬件·开发板·瑞芯微·端侧部署·rk3576
星栖与芯11 小时前
LiteOS-M 切换汇编逐行图解(4):中断三件套与 HalTaskSchedule 触发
汇编·stm32·单片机·嵌入式硬件·harmonyos
李游Leo13 小时前
定位功能“偶尔失效“怎么查:HarmonyOS Location Kit 权限、订阅与地理围栏实践
harmonyos