本文是切换汇编系列第四篇 。前三篇讲完了语法打底(第 1、2 篇)、首任务启动(第 3 篇);本篇讲两件小事------但小事很关键:临界区怎么保护 (中断三件套)和任务怎么主动让出 CPU (
HalTaskSchedule)。工程限定 :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 局部:三段源码)
- [1.3 局部:逐段讲清](#1.3 局部:逐段讲清)
- [1.4 局部:临界区典型用法](#1.4 局部:临界区典型用法)
- [1.5 局部:为什么"存旧恢复旧"而不是"出来直接开中断"](#1.5 局部:为什么“存旧恢复旧”而不是“出来直接开中断”)
- [1.6 局部:为什么进函数先 `mrs r0, PRIMASK` 再关?](#1.6 局部:为什么进函数先
mrs r0, PRIMASK再关?)
- [二、第②站:`HalTaskSchedule` = 按门铃(触发切换)](#二、第②站:
HalTaskSchedule= 按门铃(触发切换)) -
- [2.1 整体:三行触发一次切换](#2.1 整体:三行触发一次切换)
- [2.2 局部:源码逐行](#2.2 局部:源码逐行)
- [2.3 局部:两个值得较真的点](#2.3 局部:两个值得较真的点)
-
- [点 1:`bx lr` 后面的 `dsb/isb` 执行得到吗?](#点 1:
bx lr后面的dsb/isb执行得到吗?) - [点 2:抢占点在哪,恢复就精确到哪](#点 2:抢占点在哪,恢复就精确到哪)
- [点 1:`bx lr` 后面的 `dsb/isb` 执行得到吗?](#点 1:
- [2.4 重点·一步一步看:从任务让出 CPU 到 PendSV 进场](#2.4 重点·一步一步看:从任务让出 CPU 到 PendSV 进场)
- [三、第③站:调用链(从 `LOS_TaskDelay` 到 `HalTaskSchedule`)](#三、第③站:调用链(从
LOS_TaskDelay到HalTaskSchedule)) -
- [3.1 整体:完整链路](#3.1 整体:完整链路)
- [3.2 局部:一张表分清三件套 vs `HalTaskSchedule`](#3.2 局部:一张表分清三件套 vs
HalTaskSchedule)
- 四、小结:本篇你该带走的三句话
〇、先看地图:本篇三站路线图(整体)
读完本篇,你应该能回答三个问题:
- 临界区是什么?中断三件套(Lock/UnLock/Restore)为什么只有 3 条指令,却撑起了 RTOS 的共享数据保护?
- 任务主动让出 CPU 时,
HalTaskSchedule只写了 3 行汇编,它到底做了什么?为什么说它只是"按门铃"? - 从
LOS_TaskDelay(200)到真正切换,中间经过哪些函数?

本篇讲的两段汇编都很短------三件套每段 3 条指令,HalTaskSchedule 只有 6 行。但它们定义了 RTOS 的两个基本动作:先给整体,再一站一站拆。
- 第①站 临界区 = 会议室门禁 :三件套(
HalIntLock/HalIntUnLock/HalIntRestore)是可嵌套的门禁,核心是"存旧恢复旧"。 - 第②站
HalTaskSchedule= 按门铃:只往 ICSR 写 bit28 挂起 PendSV,不立刻切换------PendSV 最低优先级,等 CPU 空才进场。 - 第③站 调用链 :从
LOS_TaskDelay到HalTaskSchedule的完整路径。

一、第①站:临界区 = 会议室门禁(中断三件套)
1.1 整体:什么是临界区?
临界区(Critical Section)就是读写共享数据的代码段。共享数据是多任务都能碰的变量或结构体------比如任务就绪队列、定时器链表、全局计数器。
为什么进临界区要关中断?因为中断服务程序也可能读写这些共享数据。如果任务 A 正在改链表,中途被中断 B 打断,B 也去改同一条链表,指针就可能指向一片混乱------这就是竞态条件(Race Condition)。
三件套的作用不是"永远禁止中断",而是"在执行共享数据操作的那一小段时间里,保证不被打断";操作做完立刻恢复原来的中断状态。
【类比·会议室】临界区是会议室,共享数据是桌上的机密文件。中断是门外要进来办事的人。关中断就是暂时关门,等会议(共享数据操作)结束再恢复门禁。

这张图顺便解释了一件事:中断进来时,硬件会自动把 caller-saved 那批"便签级"寄存器压栈;切换汇编只需要补存 callee-saved 这些"大件工作台"。 critical section 本身只需要关中断,不用手动存寄存器------因为真正切换时,HalPendSV 会处理寄存器保存。
1.2 局部:三段源码
asm
HalIntLock: @ UINT32 HalIntLock(void)
mrs r0, PRIMASK @ r0 = 关之前的中断状态(这就是 C 侧拿到的返回值)
cpsid i @ 关中断
bx lr
HalIntUnLock: @ VOID HalIntUnLock(void)
mrs r0, PRIMASK @ 返回旧状态(接口对称,一般不用)
cpsie i @ 开中断
bx lr
HalIntRestore: @ VOID HalIntRestore(UINT32 val)
msr PRIMASK, r0 @ 恢复成调用者传来的状态
bx lr
三段各 3 条指令,是全文件最短的函数。但定义了 RTOS 临界区的标准姿势。
1.3 局部:逐段讲清
HalIntLock(关中断,返回旧状态):
mrs r0, PRIMASK:读当前的 PRIMASK(中断是不是已经关了)放进 r0。cpsid i:关中断(PRIMASK=1)。bx lr:返回,C 侧从 r0 拿到"关之前的状态"。
HalIntUnLock(开中断):
- 接口对称也返回旧状态,但一般不用------
HalIntRestore更安全。 cpsie i是开中断的专用指令,比msr primask顺手。
HalIntRestore(恢复成指定状态):
- 调用者(C 侧)把要恢复的状态通过 r0 传进来。
msr PRIMASK, r0:写回 PRIMASK。
1.4 局部:临界区典型用法
C 侧的标准姿势:
c
UINT32 intSave = LOS_IntLock(); /* 进临界区:关中断,旧状态存局部变量 */
/* ... 摸共享数据 ... */
LOS_IntRestore(intSave); /* 出临界区:恢复成"进来之前"的样子 */
1.5 局部:为什么"存旧恢复旧"而不是"出来直接开中断"
因为临界区可能嵌套:
外层:LOS_IntLock() → 关中断,intSave1 = 1(外面已经关了)
内层:LOS_IntLock() → 关中断,intSave2 = 1(还是关着的)
内层:LOS_IntRestore(intSave2=1) → 恢复成关着的,对!
外层:LOS_IntRestore(intSave1=1) → 恢复成关着的,对!
如果内层不管三七二十一开中断(cpsie i),外层的保护就被捅破了------别的中断可能进来踩共享数据。"存旧恢复旧"让嵌套天然正确,不管嵌几层,最后恢复的都是最外层进之前的状态。
【类比·会议室嵌套】会议室里正在开会(外层临界区,门已关),又有个人进来借东西(内层临界区)。内层办完要出门------如果无脑开门,门外排队的人(中断)一拥而入,把外层会议搅了。正确做法是内层出门时把门"恢复成进来前的样子"(还是关着),等外层自己办完再开门。
1.6 局部:为什么进函数先 mrs r0, PRIMASK 再关?
因为 AAPCS 规定 r0 是返回值寄存器 ------(AAPCS 就是 ARM 函数调用的约定,规定返回值放 r0)------bx lr 返回到 C 时,C 代码从 r0 取返回值,所以旧 PRIMASK 必须放进 r0。这是 AAPCS 与中断控制的接口约定。
【一句话】三件套每段只有 3 条指令,核心是"存旧恢复旧";r0 既是返回值寄存器,又是 PRIMASK 快照的载体------AAPCS 和异常控制在此汇合。
二、第②站:HalTaskSchedule = 按门铃(触发切换)
2.1 整体:三行触发一次切换
任务主动让出 CPU(比如调了 LOS_TaskDelay(200))时,最终会调到 HalTaskSchedule。它干的事很简单------按一下 PendSV 的门铃(往 ICSR 写 bit28)。
按了门铃不一定立刻有人应门------PendSV 是最低优先级异常(第 3 篇讲过),要等 CPU 空下来(没有更高优先级中断在跑、中断没被屏蔽)才进场。这个"排队等场"是铁律④的设计意图:切换永远让路给外设中断。
【类比·按门铃】
HalTaskSchedule像按 PendSV 的门铃------按了(写 ICSR bit28)就挂起请求,但 PendSV 是最低优先级,要等所有"客户"(外设中断)办完事才轮到它进场搬运。这保证搬运现场的过程不会被外设中断打断。
2.2 局部:源码逐行
asm
HalTaskSchedule:
ldr r0, =OS_NVIC_INT_CTRL @ r0 = 0xE000ED04(ICSR)
ldr r1, =OS_NVIC_PENDSVSET @ r1 = 0x10000000(bit28)
str r1, [r0] @ 写 1 挂起 PendSV------"我要切任务"
bx lr
dsb
isb
ldr r0, =OS_NVIC_INT_CTRL:字面量池读 ICSR 地址(0xE000ED04)。ldr r1, =OS_NVIC_PENDSVSET:字面量池读"挂起位"(0x10000000,bit28)。str r1, [r0]:往 ICSR 写这个值------bit28=1 就是把 PendSV 挂起(pending)。
写完并不立刻切------PendSV 作为最低优先级异常,要等 CPU 空下来(当前没有更高优先级异常在跑、中断没被屏蔽)才进场。这个"排队等场"正是铁律④的设计意图。
2.3 局部:两个值得较真的点
点 1:bx lr 后面的 dsb/isb 执行得到吗?
通常执行不到------本函数跑在 Thread 模式、中断一般开着,str 写完的下一个指令边界 PendSV 就可能抢占进来,bx lr/dsb/isb 成了摆设。
这是防御性尾巴:万一某天有人在关中断环境调它(PendSV 排队等开中断),返回后紧跟屏障能确保挂起请求对内核可见后才继续。删了大概率没事,留着肯定无害------读内核源码,这种"貌似多余"的指令先别急着判死刑。
【类比·防御性尾巴】像出门随身带伞------平时用不上(晴天 PendSV 抢着进场),但万一哪天暴雨(关中断环境调它),有伞不至于淋湿。删了大概率没事,留着肯定无害。
点 2:抢占点在哪,恢复就精确到哪
无论 PendSV 是在 str 后抢进来、还是等 bx lr 之后才进场,被切走的任务快照都由硬件 + HalPendSV 完整保存;等它下次被调回,从当初被打断的那条指令继续------调度器对"切没切、何时切"的细节无需关心。
这是 RTOS 的一个重要特性:任务切换对被切走的任务是透明的------它不知道自己被切走过,下次恢复从断点继续,像什么都没发生。
2.4 重点·一步一步看:从任务让出 CPU 到 PendSV 进场

把这张图的结论变成三句话:
- 任务主动让出 CPU(如
LOS_TaskDelay(200))。 LOS_Schedule()检查调度器状态后调HalTaskSchedule()。HalTaskSchedule往 ICSR 写 bit28 挂起 PendSV,但不立刻切换;PendSV 等 CPU 空了才进场------这就是"按门铃 ≠ 立刻换人"。
【一句话】
HalTaskSchedule只是按门铃:往 ICSR 写 bit28 挂起 PendSV,真正的搬运在下一篇HalPendSV里完成。
三、第③站:调用链(从 LOS_TaskDelay 到 HalTaskSchedule)
3.1 整体:完整链路
讲个完整链路,帮你看清 HalTaskSchedule 在哪一步被调到。
任务里调 LOS_TaskDelay(200):
c
/* los_sched.c */
VOID LOS_Schedule(VOID)
{
if (g_taskScheduled && LOS_CHECK_SCHEDULE) {
HalTaskSchedule(); /* ← 就是这里调到汇编 */
}
}
LOS_TaskDelay→ 把自己挂进定时器链表(200ms 后唤醒)→ 调LOS_Schedule。LOS_Schedule检查"调度器已启动 && 没被锁"→ 调HalTaskSchedule。HalTaskSchedule往 ICSR 写 bit28,挂起 PendSV。- PendSV 等场进场(第 5 篇的
HalPendSV)→ 搬运现场。
整条链路的"请求开关"就是 HalTaskSchedule 这三行------它不搬运现场,只负责"按门铃",真正的搬运在第 5 篇。
3.2 局部:一张表分清三件套 vs HalTaskSchedule
| 中断三件套 | HalTaskSchedule | |
|---|---|---|
| 什么时候用 | 马上要摸共享数据 | 任务主动让出 CPU |
| 做了什么 | 关/开/恢复中断 | 往 ICSR bit28 写 1,挂起 PendSV |
| 是否立即切换 | 不切换任务 | 不立即切换(只是请求) |
| 关键原则 | 存旧恢复旧,可嵌套 | PendSV 最低优先级,等 CPU 空才进场 |
| 类比 | 会议室门禁 | 按门铃 |
【一句话】三件套保护"当前任务里的共享数据操作"不被打断;
HalTaskSchedule请求"换个任务来跑"。两件事经常连着出现(先保护共享数据,再决定让出 CPU),但职责完全不同。
四、小结:本篇你该带走的三句话
- 中断三件套的核心是"存旧恢复旧":进临界区前先读 PRIMASK 存进 r0(返回值),出来时恢复成"进来前的样子"------这让嵌套天然正确,不会捅破外层的保护。
HalTaskSchedule只是按门铃 :往 ICSR 写 bit28 挂起 PendSV,不立刻切------PendSV 最低优先级,要等 CPU 空下来才进场,这保证搬运现场不被外设中断打断(铁律④)。- 三件套每段都只有 3 条指令 :
mrs读状态 +cpsid/cpsie开关 +bx lr返回------这是 RTOS 临界区的最小实现,简单到不能再简。
下篇预告 :《LiteOS-M 切换汇编逐行图解(5):HalPendSV 任务切换的现场搬运(五阶段逐行)》------逐行讲透
HalPendSV的存旧取新(r12 顺势落进 uwPriMask 槽、204 字节栈帧几何与 TaskContext 逐槽对齐、源码里那行冗余指令为何等价于 NOP)。再下一篇《(6):一次完整调度接力与调试实战》用一次LOS_TaskDelay(200)的完整旅程把四函数串成接力,并给出 objdump/GDB 实战验证,全系列收官。