LiteOS-M 切换汇编逐行图解(4):中断三件套与 HalTaskSchedule 触发

本文是切换汇编系列第四篇 。前三篇讲完了语法打底(第 1、2 篇)、首任务启动(第 3 篇);本篇讲两件小事------但小事很关键:临界区怎么保护 (中断三件套)和任务怎么主动让出 CPUHalTaskSchedule)。

工程限定 :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:抢占点在哪,恢复就精确到哪)
      • [2.4 重点·一步一步看:从任务让出 CPU 到 PendSV 进场](#2.4 重点·一步一步看:从任务让出 CPU 到 PendSV 进场)
    • [三、第③站:调用链(从 `LOS_TaskDelay` 到 `HalTaskSchedule`)](#三、第③站:调用链(从 LOS_TaskDelayHalTaskSchedule))
      • [3.1 整体:完整链路](#3.1 整体:完整链路)
      • [3.2 局部:一张表分清三件套 vs `HalTaskSchedule`](#3.2 局部:一张表分清三件套 vs HalTaskSchedule)
    • 四、小结:本篇你该带走的三句话

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

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

  1. 临界区是什么?中断三件套(Lock/UnLock/Restore)为什么只有 3 条指令,却撑起了 RTOS 的共享数据保护?
  2. 任务主动让出 CPU 时,HalTaskSchedule 只写了 3 行汇编,它到底做了什么?为什么说它只是"按门铃"?
  3. LOS_TaskDelay(200) 到真正切换,中间经过哪些函数?

本篇讲的两段汇编都很短------三件套每段 3 条指令,HalTaskSchedule 只有 6 行。但它们定义了 RTOS 的两个基本动作:先给整体,再一站一站拆。

  • 第①站 临界区 = 会议室门禁 :三件套(HalIntLock / HalIntUnLock / HalIntRestore)是可嵌套的门禁,核心是"存旧恢复旧"。
  • 第②站 HalTaskSchedule = 按门铃:只往 ICSR 写 bit28 挂起 PendSV,不立刻切换------PendSV 最低优先级,等 CPU 空才进场。
  • 第③站 调用链 :从 LOS_TaskDelayHalTaskSchedule 的完整路径。

一、第①站:临界区 = 会议室门禁(中断三件套)

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 进场

把这张图的结论变成三句话

  1. 任务主动让出 CPU(如 LOS_TaskDelay(200))。
  2. LOS_Schedule() 检查调度器状态后调 HalTaskSchedule()
  3. HalTaskSchedule 往 ICSR 写 bit28 挂起 PendSV,但不立刻切换;PendSV 等 CPU 空了才进场------这就是"按门铃 ≠ 立刻换人"。

【一句话】HalTaskSchedule 只是按门铃:往 ICSR 写 bit28 挂起 PendSV,真正的搬运在下一篇 HalPendSV 里完成。


三、第③站:调用链(从 LOS_TaskDelayHalTaskSchedule

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),但职责完全不同。


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

  1. 中断三件套的核心是"存旧恢复旧":进临界区前先读 PRIMASK 存进 r0(返回值),出来时恢复成"进来前的样子"------这让嵌套天然正确,不会捅破外层的保护。
  2. HalTaskSchedule 只是按门铃 :往 ICSR 写 bit28 挂起 PendSV,不立刻切------PendSV 最低优先级,要等 CPU 空下来才进场,这保证搬运现场不被外设中断打断(铁律④)。
  3. 三件套每段都只有 3 条指令mrs 读状态 + cpsid/cpsie 开关 + bx lr 返回------这是 RTOS 临界区的最小实现,简单到不能再简。

下篇预告 :《LiteOS-M 切换汇编逐行图解(5):HalPendSV 任务切换的现场搬运(五阶段逐行)》------逐行讲透 HalPendSV 的存旧取新(r12 顺势落进 uwPriMask 槽、204 字节栈帧几何与 TaskContext 逐槽对齐、源码里那行冗余指令为何等价于 NOP)。再下一篇《(6):一次完整调度接力与调试实战》用一次 LOS_TaskDelay(200) 的完整旅程把四函数串成接力,并给出 objdump/GDB 实战验证,全系列收官。


相关推荐
李游Leo3 小时前
定位功能“偶尔失效“怎么查:HarmonyOS Location Kit 权限、订阅与地理围栏实践
harmonyos
庆登登登7 小时前
nvm 鸿蒙 PC 适配全记录:从 Shell Function 到 HNP 原生交付
华为·harmonyos
G***技7 小时前
告别物联网碎片化:杰和LH707+LM2-100-V0联合方案给出标准答案
人工智能·嵌入式硬件
不羁的木木7 小时前
给鸿蒙 App 增加用系统应用打开文件的能力 —— open_app_file 的鸿蒙使用指南
flutter·harmonyos
SKH.8 小时前
ARM汇编
嵌入式硬件·arm
李游Leo8 小时前
HarmonyOS 7 系统能力深度实战 02:用模块化对象开放应用内部能力
harmonyos
深圳市恒锐丰科技杨生9 小时前
EG2134 300V 三相独立半桥栅极驱动芯片|屹晶 EGmicro
嵌入式硬件·硬件工程
朝发如雪9 小时前
快速掌握单片机基础(1)(嵌入式基本概念)
单片机
szxinmai主板定制专家9 小时前
【嵌入式实战】RK3588+GMSL多路相机采集方案|SerDes长距离低延时视频流落地教程
人工智能·单片机·机器人·边缘计算·zynq