type
slug
status
summary
icon
category
date
tags
password
总览
调度是RTOS的核心功能,用于确定多任务环境下任务执行的顺序和在获得CPU资源后执行时间的长度。
RTOS提供的机制:
- 基本调度机制:【实时调度策略的具体实施】
- 创建任务、删除任务、挂起任务、改变任务优先级、任务堆栈检查、获取任务信息……
- 任务协调机制:【多任务协同并发运行】
- 任务件通信、同步、互斥访问共享资源
- 内存管理机制:【确保内存有效使用】
- 为任务和数据分配内存空间
- 事务处理机制:【对应中断和时间管理】
- 【事件触发event-triggered机制】:对应中断。
- 中断用于接受和处理系统外部/内部事件。
- 【时间触发time-triggered机制】:对应时间管理。
- 维护系统时钟
- 记录多任务时间参数(周期、截止时间)
Chapter 2 Kernel of ERTOS(ERTOS内核)
- Task management:任务管理;Scheduling Policy:调度策略
- Synchronization:同步;Communication:通信;Mutex:互斥锁;Coordinate mechanism:协调机制
- Memory management:内存管理;Inventory management mechanism:库存管理机制
- Interruption & Time management:中断和时间管理
- Event dealing mechanisms: Event-triggered mechanism Time-triggered mechanism(事件处理机制:事件触发机制、时间触发机制)
2.0 任务调度机制
- 内核的核心功能:调度
- 内核调度的对象是任务
- 一个策略:调度策略
- 四个机制:基本调度机制、任务协调机制、内存管理机制、事件触发机制(中断)、时间触发机制(时间管理)
Q1 How to describe a task?
TASK:可执行函数,栈,TCB
创建任务的本质就是在任务的可执行函数、TCB 与栈之间建立联系。创建好后,它才能进入就绪队列。
2.1.1 什么是任务
- 任务是一个可执行的软件实体,它负责执行一些特定的操作。它包括一个指令序列、相关的数据以及计算机资源
- 任务是一个无限循环的函数。它就像一个普通函数一样需要接收参数,但是它永远不会返回。任务的返回值类型永远是void。
- 函数VS任务:
- 函数:是静态代码块,由任务调用,不能独立运行,依赖于任务上下文。不参与 OS 的调度。
- 任务:是动态运行的实体,通过 OSTaskCreate() 等 API 创建,由 OS 管理,具备独立上下文和栈空间。
2.1.2 任务的特点
- Concurrence(Simultaneousness)(并发性)
- 由于系统中多个任务并发执行,这些任务宏观上是同时运行的,微观上仍然是串行的。
- Independence & Asynchrony (dependence & Synchrony)(异步独立性)
- 任务间相互独立,不存在前驱和后继的关系(独立)。按照各自的运行速度运行(异步)。
- 反之,任务间相互依赖,则任务具有同步性。
- Dynamic(动态性)
- 就跟操作系统课上讲的是一样的,就是进程是一个动态实体,而程序是一个静态实体。
- 程序(Program)是静态实体:它是存储在磁盘或内存中的一组指令或代码,没有执行过程,仅仅是一段静态的数据。进程(Process)或任务(Task)是动态实体:它是程序在执行时的实例,有动态的运行状态,包含了执行中的指令、数据、栈、寄存器、资源等。

- 任务具有动态性、异步独立性、并发性
2.1.3 任务、线程、进程
- 进程(Process):资源分配的基本单位。独立运行的程序实例,具有独立的地址空间、独立资源、独立生命周期,包含多个线程。
- 线程(Thread):操作系统调度的基本单位。运行在进程的地址空间内,是进程中的可独立调度执行单元,共享进程资源。
- 任务(Task):RTOS 中的可独立调度的执行单元。在嵌入式 RTOS 中,任务 ≈ 线程,表示 OS 中可独立调度的运行实体。(每个任务/线程具有独立堆栈、上下文和运行状态。任务/线程的切换由内核进行调度,任务/线程的调度策略由操作系统负责管理。任务/线程都支持并发执行,并通过调度机制进行管理。)
进程和线程的区别:

只需要把任务当成一个线程就好了。而线程是没有内存隔离的,而进程是有内存隔离的。RTOS只实现了多线程。线程之间是共享地址的,能够访问相同的全局变量。当前线程的地址对于其他线程是可见的,如果修改了当前线程的数据,其他线程是可以知道并且能够访问的。进程之间不能相互访问对方的变量,数据变化不会影响其他进程,一般通过地址保护和虚拟地址来实现。

上图中创建了一个进程,那么这个时候就会存在内存隔离,那么在进程test中的变量i就是全局变量i的一份拷贝,所以在主进程中执行i++并不会影响进程test中的i,所以在进程i中数据i的值应该为1。

上图中创建了一个任务,而任务相当于是一个线程,它的地址空间是与其他在同一进程中的线程共享的,所以在主线程中执行i++将影响test线程地址空间中的i变量,所以test任务将输出2。
总结一下:
- 进程是资源管理的基本单位,线程是操作系统调度的基本单位
- 进程之间内存空间彼此隔离独立,无法互相访问,线程之间内存空间可互相访问
- 进程之间的内存会由MPU/MMU硬件单元管理,使得他们之间互相隔离
- 一个进程崩溃了,其他进程不受影响;一个线程崩溃,所属同一个进程的其他线程将受到影响
2.1.4 任务的执行体
相当于是任务的三要素(一个指令序列、相关的数据以及计算机资源)中的指令序列了。并且在这里其实也涉及到了数据,因为程序和数据一般是相互依存的。需要注意的是,任务是一个无限循环,并且返回值永远为void。
一个简单任务的执行体如下:

- 一个简单的点灯任务(基于MSP430 P90)

这里把延时设计为OSTimeDly()与之前裸机程序使用的延时函数有什么区别?
- OSTimeDly()在执行时,CPU资源从一个任务转交给其它任务执行
- 裸机的延时函数在执行时,CPU资源全部分配给延时函数,无法执行其它函数。

- 引入两个任务之后,操作系统的调度逻辑

- IDLE任务在系统空闲时运行,执行节能等操作。
- ISR_WDT作为系统的时钟节拍来源,每个Tick到来时都执行,以确保系统的定时功能和任务调度的准确性。在 ISR_WDT 中,会触发 uC/OS-II 的时间管理机制,包括任务延时、就绪判断等。使得调度器正常运行。
- CPU使用情况:
- 当没有其他任务处于就绪状态时,CPU执行的是 IDLE 任务。这个任务是 uC/OS-II 系统启动时自动创建的最低优先级任务,用于“打发”CPU时间,防止系统停滞。IDLE 任务一般是个死循环,会执行一些低功耗操作,比如
__WFI()
(等待中断)进入低功耗模式。 - CPU 正在执行 IDLE 任务,此时 WDT 中断触发。CPU 暂停 IDLE 任务,进入中断服务程序
ISR_WDT()
在中断服务程序中会调用OSTimeTick()
,这是 uC/OS-II 的时间管理核心函数,用于: - 更新任务延时计数器(比如
OSTimeDly()
中的任务)。 - 检查是否有任务从挂起变为就绪(如延时结束、超时等待等)。
- ISR_WDT 执行完毕后,中断退出前会进行一次调度
OSIntExit()
,判断是否需要切换任务。若仍然没有其他更高优先级任务就绪,则继续返回执行 IDLE 任务。 若有任务变为就绪(例如某个任务延时结束),则调度器将中断退出后切换到该就绪任务。
- 任务就绪队列使用情况
- 当系统空闲时:就绪表中仅有 IDLE 任务(最低优先级的任务)
- Tick 中断期间:
OSTimeTick()
会遍历任务控制块(TCB),更新计时器;- 有任务满足条件时,会被加入到就绪队列;
- 否则,仍然只有 IDLE 任务保持就绪状态。

- 从图中看,IDLE 任务似乎是连续的,但这并不意味着
ISR_WDT
不占用时间,而是因为ISR_WDT
占据时间很短,但确实会打断 IDLE 执行。(相较于 IDLE 运行时间可以忽略不计,所以图上显示成“几乎连续”。) - 举个具体例子:系统时钟 72MHz、
OSTimeTick()
在 ISR 中执行完大概需要3微秒、Tick周期为10毫秒。所以约3μs执行ISR_WDT
,剩下的9.997ms都在执行IDLE。
- 什么时候创建任务?


2.2 OS_TCB
2.2.1 TCB的成员
这里主要了解的是任务TCB结构体中一些重要的成员,并没有涉及到创建任务啥的。
OSTCBStkPtr
:任务栈指针指向任务栈,是TCB的第一个成员,存放在地址最低处- 在进程中的每个线程都有自己独立的堆栈
- 模拟栈需要事先被定义。这个栈并不是芯片启动时在 startup.s 中定义的栈(那是给 OS 自己用的)。这个栈的大小由程序员评估确定(芯片寄存器、函数局部变量、函数调用关系)
- 类似于
int a[128]
一样,在μc/OS-ii中也是通过一个 typedef 出来的OS_STK
初始化一个数组来实现:OS_STK Task1_stk[128]
- 静态申请了大小为
OSTASKSTATSTKSIZE
的一片内存空间,调用OSTaskCreate()
创建一个任务。该任务用来统计任务的运行信息。其中第三个参数是传递栈顶的指针,OS_STKGROWTH表示堆栈增长的方向,若OS_STK_GROWTH为1,表明方向为从高到低,否则从低到高。如果堆栈增长方向为从高到低,那么栈顶指针就为&OSTaskStatStk[OS_TASK_STAT_STKSIZE-1]
:反之,则&OSTaskStatStk[0]
。
- 栈的地址?
- 假设堆栈数组的基地址是
0x2000
,每个元素占 4 字节(uint32_t
类型)。数组大小为1024/4=256,OSTaskStatStk[0]
的地址是0x2000
,OSTaskStatStk[1]
的地址是0x2004
,OSTaskStatStk[2]
的地址是0x2008
……最后一个为OSTaskStatStk[255]
的地址为(0x2000+255*4)或者(0x2000+1024-4)
=0x23FC
- 堆栈的栈顶指针(
SP
)会根据堆栈的增长方向来确定。ARM架构为堆栈向下生长,即&OSTaskStatStk[OS_TASK_STAT_STKSIZE-1]即sp = &OSTaskStack[255]
;

在嵌入式操作系统(如 uC/OS-II 或 FreeRTOS)中,堆栈空间通常在系统启动时就会分配好,数组的每个元素的位置在编译时就已经固定了。栈的增长方向和栈顶指针的位置是由宏
OS_STK_GROWTH
来控制的。OSTCBNext
与OSTCBPrev
:用于存已经初始化的TCB队列(即OSTCBList)中的下一个和前一个TCB(已初始化队列是一个双向链表,是为了便于进行快速的链表操作。这样只要找到一个TCB就能马上获取他的前后TCB并进行操作)- 代码分析在后面

- 这里在额外说一下这些各种奇怪的组织TCB的结构。
OSTCBTbl
:用于存放所有的TCB块,是一个数组。- 为的是空间的确定性(初始化的数量是根据用户的需求确定的,也就是用户需要的最低优先级)
OSTCBFreeList
:用于记录空闲的TCB的链表。- 空闲的任务控制块表示未被分配给任何任务的控制块。
- 初始化的时候执指向的是
OSTCBTbl
的头部,默认所有的TCB都是空闲的,直至它们被分配给给实际任务 - 当创建一个新任务时,系统会从
OSTCBFreeList
中取出一个空闲的 TCB 并将其分配给新任务。一旦任务结束或销毁,其对应的 TCB 会被归还到OSTCBFreeList
中,成为新的空闲控制块。 OSTCBList
:用于记录已经被分配并正在使用中的TCB的链表(初始化的时候为空)- 当任务处于运行状态时,它的 TCB 就会被添加到
OSTCBList
中。 - 任务结束或被销毁时,TCB 会从
OSTCBList
中移除,并归还到OSTCBFreeList
中。 OSTCBPrioTbl
:是一个表(通常是一个数组),用于存放已经初始化了的TCB的指针。OSTCBPrioTbl
是一个指向任务控制块(TCB)的指针数组。每个任务在创建时,根据其优先级都会在OSTCBPrioTbl
中的相应位置存储指向该任务控制块的指针。- 表是根据任务的优先级来排序的,使得操作系统能够根据任务的优先级快速查找任务的 TCB。系统调度时,优先级较高的任务会被从
OSTCBPrioTbl
中优先取出执行。
OSTCBDly
:代表任务的延时Tick数,也就是任务需要等待多少个时钟周期后才能继续执行。- 这个就是Task1 点灯延迟 10 秒时,操作系统对这个任务进行计时的地方。它从一个初始值不断递减,到一定值后该任务将从等待队列进入就绪队列。
- 这个值是由位于 ISR_WDT 中断服务程序中的
OSTimeTick()
来进行管理。
OSTCBStat
:代表任务的状态- 任务的状态转换图如下(这个可跟操作系统课上讲的进程五态图不一样):
- 在这个图中需要记一些会引起状态转换的典型事件,也就是上图中标红的部分(还需要额外记一个任务的删除。并且注意任务删除之后任务只是进入冬眠状态)
- Dormant(睡眠):任务只以代码的形式存在,来交给操作系统管理,即没有分配任务 控制块和任务堆栈。
- Ready(就绪):任务已经被创建,并被挂载到就绪队列中。
- Running(运行):任务获得CPU的执行权。。
- Waiting(等待): 任务因等待某个事件(如消息、信号量、时间延迟等)而暂停。需要等待一个事件的发生再运行,CPU使用权被剥夺。
- ISR(中断服务状态):正在运行的任务一旦受到其他中断的干扰就会终止运行,转而执行 中断服务程序,这时处于中断服务状态。

(P137 RTOS将不停地维护这个状态机)
OSTCBPrio
:代表任务的优先级- 在uC中任务优先级唯一,因此,可以仅仅依靠优先级来确认任务,TCB 中不用再设置任务ID,优先级就相当于是任务的ID号了
- μC/OSI1支持64个优先级,分别对应优先级0~63,其中0为最高优先级,63为最低优先 级,最低的两个优先级:63分配给空闲(idle)任务和62统计(stat)任务。系统保留了4个最高优先级任务和4个最低优先级任务(0~3和60~63给系统),用户可以使用的任务数有56个。
- uC/OSII的优先级通过os_cfg.h文件(代码2.7)和ucosII.h文件(代码2.8)定义。
OS_LOWEST_PRIO
一旦被定义,意味着可供系统使用的优先级一共有OS_LOWEST_PRIO+1
个,它们分别是0、1、2、·、OS_LOWEST_PRIO
。数字0表示任务的 优先级最高,数字越大则表示任务的优先级越低,最低优先级为OSLOWEST_PRIO
。- 分配统计任务(STAT)的优先级和空闲任务(IDLE)的优先级
OSTCBX、OSTCBY、OSTCBBitX、OSTCBBitY
:用于优先级位图法,牺牲空间赢得时间,以实现时间的确定性(关于优先级位图后面会详细介绍。只需要知道这这里带Bit的都是掩码就好了)- 为了支撑实时性,需要一种 O(1) 的任务调度算法,使得查找任耗时与任务数量无关。这种算法用到了一个数据结构:Priority Bit Map
OSTCBEventPtr
:用于指向任务使用的事件(如信号量、互斥量、邮箱等)。这个定义主要是在任务退出时使用的。如果任务退出后还占用着事件,且无法释放这些事件,就会导致其他任务永远得不到这些事件,因此在退出时必须释放事件。- 事件:通常指的是某些状态的发生或者是某些条件的满足,可以用来控制任务的执行。事件通常由信号量、互斥量、条件变量或消息队列等机制来实现。
- 事件的作用:事件可以用来在任务间进行同步或通信。例如,任务 A 可能等待一个事件(如一个信号量的释放、另一个任务的完成等),而任务 B 可能在某个条件满足时触发该事件,从而唤醒任务 A 继续执行。
(***)优先级位图法(略)
在 uC/OS-II 中,任务的就绪状态并不是直接使用传统的双向链表,而是通过位图(bitmap)来管理。查找效率高:可以在 O(1) 时间内找到优先级最高的任务。占用空间小:相比于链表,位图占用的空间更加紧凑。
- 优先级位图法使用的数据结构
- 这里分为全局变量和TCB成员变量两部分进行介绍
- 全局变量
- OSMapTbl数组:掩码映射表,将一个0-7取值的数映射为对应的掩码(如传入1,则返回00000010)
- OSUnMapTbl数组:掩码取消映射表,获取一个8位数据中1所在的最低位(如传入10100100,则返回2)
- 上面这两个数组更像是一个定死的工具
- OSRdyGrp变量:记录优先级组的情况,即记录优先级位图中的某一行是否有任务在就绪队列中,如果有任务,则对应位置1
- OSRdyTbl数组:优先级位图的本体,在64优先级的情况下被初始化为8个8位数据组成的数组。若某个优先级当前有任务在就绪队列中,则该位为1
- TCB成员变量
- OSTCBX:当前任务优先级在优先级位图中的列号
- OSTCBY:当前任务优先级在优先级位图中的行号
- OSTCBBitX:OSTCBX对应的掩码
- OSTCBBitY:OSTCBY对应的掩码
(***)优先级位图法(详)
在 uC/OS-II 中,任务的就绪状态并不是直接使用传统的双向链表,而是通过位图(bitmap)来管理。查找效率高:可以在 O(1) 时间内找到优先级最高的任务。占用空间小:相比于链表,位图占用的空间更加紧凑。
两个重要全局变量:
OSRdyGrp、OSRdyTbl[]
(#define OS_EXT extern
)- OSRdyGrp变量:一个八位二进制数,每一位代表一个优先级组。记录优先级组的情况,即记录优先级位图中的某一行是否有任务在就绪队列中,如果有任务,则对应位置1。
- OSRdyTbl数组:优先级位图的本体,在64优先级的情况下被初始化为8个8位数据组成的数组。每个数组的成员即一个8位二进制数,代表一个组。若某个优先级当前有任务在就绪队列中,则该位为1。OSRdyTbl[x]表示x组的八个优先级,x与OSRdyGrp的x对应。
- eg:当OSRdyTbl[0]的八位数其中有一位为1,那么OSRdyGrp的第0位置1。
- 行掩码:OSTCBBitY 按位与 |= 设置对应位为1,保护其他位。
- 代表Y坐标,即第几行。
- 列掩码:OSTCBBitX
- 代表X坐标。
- TCB成员变量
- OSTCBY:当前任务优先级在优先级位图中的行号
prio>>3
将优先级右移三位,相当于/8,得到行号,确定优先级所在的组。目的为了获取prio的高三位的值。高三位代表优先表中的行号。- OSTCBX:当前任务优先级在优先级位图中的列号
prio & 0x07
按位与操作常用于提取特定位的值。掩码0x07(00000111),目的为了获取prio的低三位的值。低三位代表优先表中的列号。- OSTCBBitX:OSTCBX对应的掩码
- OSTCBBitY:OSTCBY对应的掩码
- 全局变量
- OSMapTbl:数组,掩码映射表(优先级映射表),用于将位位置(0-7取值)的数转换为对应的掩码。如传入1,则返回00000010
- 当
index=0
时,OSMapTbl[0] = 0x01
(二进制:00000001)。 - 当
index=1
时,OSMapTbl[1] = 0x02
(二进制:00000010)。 - 依此类推,直到
index=7
时,OSMapTbl[7] = 0x80
(二进制:10000000)。 - OSUnMapTbl:数组,掩码取消映射表,获取一个8位数据中1所在的最低位(如传入10100100,则返回2)
- 遍历可能的8位二进制掩码(0~255),0—>00000000(0),1→00000001(0),2→00000010(1),3—>00000011(0),直到255→11111111(0),共256个数,存在一个OSUnMapTbl[256]中。
- 上面这两个数组更像是一个定死的工具
- 因为优先级为0~63(64个优先级),如果一个优先级转化为8位INT8U型的变量表示,那么第7位和第6位一定是0,其他位可能为1,也可能为0。
- 高 3 位的值对应 OSRdyGrp 指示的优先级表的组数 Y
- 低 3 位的值对应 OSRdyTbl[Y] 指示的优先级表的相应优先级。

下面结合一个实际例子来说明上面介绍的变量

如上图,这个时候创建了一个优先级为35的任务(或者说是TCB)。首先先将优先级转换为二进制数:0010 0011(由于只有64个优先级,所以这里最高两位恒为0)
由二进制数实际上就能确定优先级在位图中的行号(row)和列号(column)了:二进制数右移3位得到的一个3位数据实际上就是行号,而二进制数低三位就是列号(因为行号*8+列号=优先级)(Y*4+X)(注意数组/图标下标从0开始!)
- 行号:(00)100011右移三位,高三位为100,即行号为4。
- 列号:(00)100011低三位011,即列号为3。
- 而OSTCBBitY与OSTCBBitX就是OSTCBY与OSTCBX的掩码,可以直接通过掩码表得到,所以有赋值语句:
至此TCB的成员变量已经初始化完成。
但是在TCB初始化的过程中还需要对
OSRdyGrp
变量以及OSRdyTbl
数组进行修改。- 对于
OSRdyGrp
变量,由于需要置入就绪队列的任务优先级为35,位于优先级分组4中,所以需要将OSRdyGrp
的第四位置1。这个时候OSTCBBitY
与OSTCBBitX
的作用就展现出来了.因为OSTCBBitY
就是00010000.所以有赋值语句:OSRdyGrp |= ptcb->OSTCBBitY
- 对于
OSRdyTbl
变量,由于需要置入就绪队列的任务优先级为35,应该位于优先级分组4中的第3个,所以需要将该为置1。 - 首先需要先取出改组的优先级情况,即为
OSRdyTbl[ptcb->OSTCBY]
(OSTCBY就是行号) - 然后需要将第三位置1,同样的OSTCBBitX的值就是00001000,所以有语句
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX
至此所有有关就绪队列优先级位图的变量都介绍完毕了。
三个使用优先级位图法的地方
- 任务进入就绪队列
- 任务退出就绪队列
- 退出就绪队列同样需要提供任务的优先级。实际上就只需要修改OSRdyTbl数组与OSRdyGrp变量即可。
- 唯一需要注意的点是,如果
OSRdyTbl
中该行(该行指的是需要被移出就绪队列的任务所在的行)中对应当前任务的位被置0后该行没有任务了(即OSRdyTbl[Y]中8位全为0),这个时候才需要将OSRdyGrp的对应位置0。 - 保留原值:任何位与1运算,清除原值:任何位与0运算
- 掩码作用:标记需要操作的位
- 保留位:掩码中对应位为1,表示保留原值。
- 清除位: 掩码中对应位为0,表示强制清0。
- 生成掩码:
- 精准清除
- 保留原值:任何位与0与运算。置1:任何位与1与运算
- 掩码作用:标记需要操作的位
- 保留位:掩码中对应位为0,表示保留原值。
- 置1位: 掩码中对应位为1,表示精准置1。
- 生成掩码:
- 精准清除
&=:精准清除某一位。
|=:精准控制哪一位设置为1
- 找到最高优先级任务

- 上图是
INT8U const OSUnMapTbl[256]
- 这里需要详细介绍一下。就以64位位图为例。需要知道的是,寻找最高优先级的任务是与特定的TCB无关的,所以并不会涉及到TCB成员变量的操作。
- 首先需要寻找最高优先级任务所在的优先级组,这个时候就需要使用
OSUnMapTbl
数组了,它返回的是传入8位数据中的1所在的最低位置。(如传入10100100,则返回2) - 故有语句:
y = OSUnMapTbl[OSRdyGrp];
此时y就是最高优先级任务的所在的行号了(就相当于是TCB的成员变量OSTCBY) - 获取到行号之后就需要查找最高优先级任务在当前分组中的列号了,同样需要使用OSUnMapTbl。但是此时传入的参数就不再是OSRdyGrp,而应该是优先级位图.
- 所以最高优先级任务的列号应该为:
x = OSUnMapTbl[OSRdyTbl[y]]);
即第y组的那个8位优先级数中1所在的最低位置。(eg:01001000,返回值为3) - 知道行号和列号之后就可以计算出最高优先级:
OSPrioHighRdy= (INT8U)((y << 3) +OSUnMapTbl[OSRdyTbl[y]]);
- 实际上就是行号*8+列号。
- 而如果查找到了最高优先级,就能通过OSTCBPrioTbl数组找到最高优先级任务的TCB
OS_TCB *ptcb = OSTCBPrioTbl[OSPrioHighRdy];
Q2 How to create a task in μc/OS II
.png?table=block&id=1ddad35f-a930-8064-b7dc-e4b8d7cb809c&t=1ddad35f-a930-8064-b7dc-e4b8d7cb809c)
.png?table=block&id=1ddad35f-a930-8094-ad8b-eb8354ad6338&t=1ddad35f-a930-8094-ad8b-eb8354ad6338)
.png?table=block&id=1ddad35f-a930-8063-8191-c5ce1f6c24e6&t=1ddad35f-a930-8063-8191-c5ce1f6c24e6)
.png?table=block&id=1ddad35f-a930-80a5-a492-f2df9651245d&t=1ddad35f-a930-80a5-a492-f2df9651245d)
任务创建的时候,栈的初始化是不是可有可无的?❌
- 在任务创建后就会进入就绪队列
- 可是就绪队列里不只是有刚创建好的任务,还有其他已经执行过的任务
- 他们的区别就是栈中内容
- 可操作系统把就绪队列中任务提出进入执行态,需要执行一系列操作,其中就要把栈中的一部分数据压入到 CPU 中(由汇编代码实现)
- 压入到CPU中的操作是统一的。如果新建的任务不对栈进行一定初始化,PC 等关键值就会出错,程序会跑飞。
指针数组:OSTCBPrioTbl[] 的作用和意义是什么 ⚠️
- OSTCBPrioTbl[] 是一个指针数组。对于 OSTCBPrioTbl[63] 而言,其值的意义是:一个指向优先级为 63的任务的TCB 的指针。
- 设计这个数组的作用是为了在 的时间复杂度内找出某优先级任务对应的 TCB,是一种牺牲空间、换得时间的,在优先级与任务建立联系的方式,OSschdu() 函数的返回值是任务的优先级。这时就可以通过数组的取下标操作迅速找到这个任务对应的。TCB,随后立即定位到该任务的栈,随即开始恢复该任务的运行环境
OS_Sched()
.png?table=block&id=1ddad35f-a930-80fe-b935-dcd8d04ccdaa&t=1ddad35f-a930-80fe-b935-dcd8d04ccdaa)
.png?table=block&id=1ddad35f-a930-80a5-a008-ef189ddadf12&t=1ddad35f-a930-80a5-a008-ef189ddadf12)
2.3 任务创建
创建任务时,首先须为其分配一个任务控制块 OS_TCB,然后对 OS_TCB 的各成员进行初始化。μC/OS II 提供了两种创建任务的方式,分别为
OSTaskCreate()
和 OSTaskCreateExt()
函数指针
返回类型 (*指针名称)(参数类型列表);
OSTaskCreate()
函数原型
task
:一个函数指针,指向任务所开始的函数,当任务第一次被调度运行时,就会从函数开始处运行。接收一个void *类型的参数。
p_arg
:传递给任务函数的参数指针
ptos
:任务堆栈的栈顶指针。
prio
:任务的优先级。
下面是部分代码详细分析:
2.3.1 临界区代码保护
- 进入临界区,检查是否在中断中创建任务,如果在则退出临界区,返回错误
- 中断:是计算机系统中的一种异步事件,它允许外部设备或内部事件在正常程序运行时打断当前任务的执行,转而执行一个预先定义好的函数(即中断服务程序,ISR)。一旦中断处理完成,控制权会返回给原本正在执行的任务。
- 基本流程:触发中断→保存上下文→执行中断服务例程(ISR)→恢复上下文
- 嵌套中断:如果系统允许多个中断嵌套,即当前处理中断后,又有新的中断发生,可以打断当前中断并处理中断。
- 临界段代码:是指一段代码,在执行时需要独占对共享资源(如全局变量、硬件设备等)的访问。
- 多个任务可能会同时访问这些共享资源,如果没有同步机制,可能导致数据不一致、竞态条件或其他意外错误。因此,临界区需要加锁保护,以防止中断或其他任务同时访问。
- 进出临界区:
- 进入临界区:通过禁用中断来防止中断发生,保证当前任务的操作不被打断,从而确保临界区内的操作能够原子性地完成(即不受干扰地一次性完成)。
- 退出临界区:当临界区操作完成后,恢复中断,使得系统能够响应新的中断请求。
OSIntNesting > 0
:OSIntNesting
是一个全局变量,表示当前中断的嵌套层数。操作系统会追踪中断的嵌套层级。初始时,OSIntNesting
为 0,表示系统没有处理中断;如果发生中断,OSIntNesting
会增加,表示进入了中断。- 为什么不能在中断中创建任务?:中断和任务创建涉及的资源竞争,可能会同时访问和修改共享的系统资源(如任务队列、优先级表等)。同时中断需要快速响应,任务创建也可能影响中断响应性。
- 检查任务优先级是否已被占用,在临界区内进行对OSTCBPrioTbl的修改。
OSTCBPrioTbl
是一个指向任务控制块(TCB)的指针数组。每个任务在创建时,都会在OSTCBPrioTbl
中的相应位置存储指向该任务控制块的指针。- 一旦确认该优先级位置为空(未占用),操作系统会通过
OSTCBPrioTbl[prio] = (OS_TCB *)1;
来标记该优先级为已占用。 - 这里的
(OS_TCB *)1
只是一个占位符,表示该位置已经被任务占用。实际上并不指向有效的任务控制块(TCB),而是仅仅表示该优先级位置已经被分配。之后,系统会继续为该任务分配实际的 TCB,并在任务创建过程中更新OSTCBPrioTbl
。 OSTCBPrioTbl
是一个共享资源,不同任务可能会修改该表,因此我们需要保证在修改OSTCBPrioTbl[prio]
时,不会有其他任务或中断同时访问该表,导致数据竞争。通过使用OS_ENTER_CRITICAL()
,操作系统确保进入这段代码后,其他任务和中断不能打断当前的操作。- 退出临界区:任务堆栈和任务控制块的初始化不再涉及对共享资源的修改,因此可以退出临界区,允许其他任务和中断正常执行。避免了长时间占用临界区导致的系统响应迟缓。这种设计确保了任务创建的原子性和系统的实时性。
2.3.2 初始化任务栈
- 任务栈初始化:也就是进行模拟压栈(需要按照ARM要求的寄存器顺序压栈,编号高的寄存器需要压在高地址处)
函数原型:
- task:任务执行函数的入口地址/p_arg:任务参数的地址/ptos:栈顶指针/opt:预留参数保留/返回:调整之后的堆栈指针(前三个参数和返回值都是顺序放在寄存器中的)
- 在堆栈初始化过程中,需要保存任务的起始地址、处理器状态字(在 ARM 中叫作程序当前状态)、中断返回地址和寄存器等信息。
OSTaskStkInit()
是一个与硬件相关的函数,因为不同处理器有不同的寄存器结构。
- 下面以ARM9 S3C2440为例说明:(ARM是先存高号寄存器)
- ARM9 S3C2440的任务环境由寄存器R0-R15及CPSR体现(17个寄存器),每个寄存器都是32bit(4B)。
- R0-R7:通用寄存器/R8-R12:影子寄存器。
- R13:堆栈指针(SP)
- R14:链接寄存器(LR)。
- R15:程序计数器(PC)。
- CPSR:当前程序状态寄存器。
- 当低优先级任务被高优先级任务抢占而发生上下文切换时,需要保持上述16个寄存器的值(R13除外,R13(SP)的值将保存在当前任务的TCB-OSTCBStkPtr中)
- 因此在堆栈初始化时就要压入这些寄存器来模拟(因为任务刚创建时尚未开始运行,其运行环境只能模拟)任务环境入栈。
- 下面是模拟压栈的代码:
- stk:指向当前堆栈的栈顶(即存放栈顶的地址)
- task:任务入口函数的地址。转化成OS_STK类型,赋予给stk。即将入口函数的地址存于堆栈栈顶。(在TCB中不需要再存储任务入口函数的地址)
- 当任务第一次被调度时,系统会从堆栈中恢复这个地址,并将PC设置为这个地址,从而开始执行任务的入口函数。所以说这里最开始为函数入口的地址,后面为该函数的PC的值。
- 需要注意的是传入的栈顶指针是可以使用的,所以需要先向stk写值将栈变成一个满递减栈(stk = ptos)。然后就只需要将寄存器按顺序压入即可(PC寄存器的位置压入任务执行体入口指针(*stk)=(OS_TCB)task,SP寄存器不压入,R0压入任务参数,最下面一个单元压入任务状态)。最后返回当前栈顶的地址。
- 另外需要有一个C语言的小知识,对指针做减法减去的是指针所指向的数据类型的大小,而不是简单的1。eg:OS_STK为INT32U(即4个字节),
*(--stk)
会先将stk减少4 字节,然后将0存储在新的堆栈位置。 - 前缀操作:指在操作数前面放置操作符,如
++i
或--i
。先对变量进行增减操作,然后再返回变量的新值。 - 后缀操作:指操作符放在操作数后面,如
i++
或i--
。先返回变量的原值,然后对变量进行增减操作。 - 压栈操作为先移动指针,再压入数据,即
*(—stk)=(INT32U)0;
弹栈操作则为先读出数据,再移动指针INT32U value = *(stk++);
- 压栈结果如下图:
- 此时为模拟任务切换时,当前任务的运行环境时如何保存在堆栈中的。实际发生的任务切换,需要根据16个寄存器的值依次保存在stk中。向前见2.4.3任务切换。
下面是部分重点代码分析:

这个TaskStartAStk应该就是为任务开出来的栈空间,是一个数组,大小为256个单元,每个单元是OS_STK类型。由于之前这个任务从来没有使用过栈,所以栈顶指针应该是&TaskStartAStk[255](也就是栈空间中最后一个单元的地址)。在前面声明堆栈增长方式的时候有说,如果堆栈增长方向为从高到低,那么栈顶指针应为&OSTaskStatStk[OS_TASK_STAT_STK_SIZE-1]。

(0x20001000-15*4)
2.3.3 TCB初始化
函数原型
- prio: 任务的优先级。/ptos: 指向任务栈顶的指针。
- pbos: 指向任务栈底的指针。/id: 任务的标识符。/stk_size: 任务栈的大小。/pext: 指向任务的扩展信息。/opt: TCB 的选项参数。(扩展创建任务时需要使用的参数)
下面是OS_TCBInit的具体实现的部分重点代码:
下面是重点代码分析:
- 申请一个指向OS_TCB的指针
- 申请一个空闲的TCB块并对各成员赋值
理解该行代码的知识点如下:
uCOS在调用
OSInit()
进行系统初始化的时候,根据用户的配置信息如,通过InitTCBList()
申请OS_MAX_TASKS+OS_N_SYS_TASKS
个空闲TCB。将它们通过链表的形式链接,链表头指针为OSTCBFreeList
。- 如果为系统配置了统计任务,则需要设置
OS_TASK_STAT_EN
为1,此时系统任务有2个(空闲任务IDLE和统计任务STAT)。否则,系统任务只有1个(空闲任务)。
- 定义了最多可建立的任务数为 20。
OS_MemClr
用于清除 TCB 表和优先级表,确保所有条目初始化为零。- 内存清零函数,传入清除项目的首地址和大小。
ptcb1
和ptcb2
是指向 TCB 表中相邻条目的指针。使用for
循环将每个 TCB 的OSTCBNext
指向下一个 TCB,形成一个链表。- 如果启用了任务名称功能,每个 TCB 的任务名称初始化为
'?'
,表示未命名任务。一共OS_MAX_TASKS+OS_N_SYS_TASKS
个空闲TCB,对应号码为0~OS_MAX_TASKS+OS_N_SYS_TASKS-1
- 循环条件时环条件是
i < (OS_MAX_TASKS + OS_N_SYS_TASKS - 1)
。这意味着循环会运行OS_MAX_TASKS + OS_N_SYS_TASKS - 1
次。n个TCB,需要连接n-1次。即结束循环时,所有TCB都已链接。ptcb1指向最后一个TCB,它的OSTCBNext设置为NULL(即(OS_TCB*)0)。 - 循环外再次给最后一个TCB的OSTCBTaskName数组命名,
OS_ASCII_NUL
即ASCII空字符’\0’。
OSTCBTbl
是一个数组,用于存储所有任务的TCB。OSTCBList
:用于记录已经被分配并正在使用中的TCB的链表(初始化的时候为空OSTCBFreeList
:用于记录空闲的TCB的链表。OSTCBList = (OS_TCB *)0; /*没有任务处于就绪队列*/ OSTCBFreeList = &OSTCBTbl[0]; /*所有TCB都在空闲状态,遂指向第一个TCB*/
OSTCBTbl[]
定义如下:OS_TCB OSTCBTbl[OS_MAX_TASKS + OS_N_SYS_TASKS];
(通过TASKS总数来定)OS_TCB OSTCBTbl[OS_LOWEST_PRIO + 1];
- uCOS支持64个优先级,为0~63.最低两个优先级分给IDLE和STAT任务。系统保留了4各最高优先级任务和4个优先级。用户可以使用的任务数有56个。
#define OS_LOWEST_PRIO 30
(os_cfg.h中定义任务能被分配的最低优先级)- 一旦被确定,可供系统使用的优先级为OS_LOWEST_PRIO+1个,分别为0,1,…OS_LOWEST_PRIO。0为最高优先级,OS_LOWEST_PRIO为最低优先级。

上图为空闲OS_TCB链表OSTCBFreeList,这个时候应该回去看一下那段最初申请一个空闲的TCB块的代码了,就明白了。

这个是初始化后OS_TCB链表0STCBList
- OSTaskCreatHook()函数。Hook函数都是空代码,用户可以自行添加。可以在调试的时候供用户添加输出信息。
- 更新优先级占位
OSTCBPrioTbl
是一个数组,保存了每个优先级对应的任务控制块(TCB)的指针。此时ptcb指向优先级为prio的任务块。所以将其存入OSTCBPrioTbl,代表优先级prio的任务TCB已被分配。- 头插法进入双向链表
- 代码思路:该TCB的Next指向当前第一个TCB块(OSTCBList也指向当前第一个任务块),Pre为NULL(这样代表这个任务为任务链表中的第一个任务。)OSTCBList再指向该任务块。
- 一些自己代码思路的小误区:Pre是NULL而不是指向OSTCBList。判断当前任务链表是不是空链表。如果是空链表,那么Next也应该为NULL。此时的OSTCBList=(*OSTCB)0;
- 使任务进入就绪队列(优先级位图法)
- 获取空闲任务块失败(即ptcb=OSTCBFreeList;OSTCBFreeList=(*OS_TCB)0),则也需要退出临界区。同时返回OS_NO_MORE_TCB错误,表示没有更多TCB分配。
总结:
- 任务TCB初始化:初始化当前任务的TCB。实际上就是把上面介绍的TCB成员都初始化一下(如:栈指针,Dly,状态,优先级,优先级位图变量)。另外再修改一些全局变量。需要修改的全局变量如下:
- OSTCBFreeList指针:因为TCB块需要从OSTCBFreeList中取出
- OSTCBPrioTbl数组:因为需要支持根据优先级快速查找任务
- OSTCBList指针:因为TCB初始化之后就应该从OSTCBFreeList链表中取出并放在OSTCBList链表中
- 变量OSRdyGrp:为了支持优先级位图
- OSRdyTbl数组:为了支持优先级位图
- TCB初始化如果没有问题的话,就进行任务调度
总结一下任务创建的全过程:
- 判断中断嵌套层数
- 判断当前优先级是否被某个任务占用
- 初始化任务栈(模拟压栈)
- 初始化任务TCB
- 没有问题就开始调度任务
2.3.4 将新创建的任务挂载到就绪队列
任务堆栈初始化、OS_TCB初始化后需要将刚刚创建的任务挂载到就绪队列,以供OS_Sched()调度。使用优先级位图法。见前面。
2.3.5 调用OS_Sched()
创建任务的最后异步时调用内核调度程序OS_Sched(),由内核根据调度策略安排任务执行。详情见2.4调度任务
2.3.6创建任务扩展OSTaskCreateExt()
函数原型
以任务0为例说明各参数含义
- task:表示指向任务函数
my_task_0_t_
的指针。
- p_arg:传递给任务函数的参数。任务开始执行时,这个参数将被传递给任务函数。设置为
NULL
,表示任务不需要额外参数。
- ptos栈顶地址:
&my_task_0[MY_TASK_SIZE_0 - 1u]
,栈顶指针,指向任务栈的顶部。在 μC/OS-II 中,栈是向下增长的,所以这个指针实际上是栈的最高地址。
- prio:
12
,表示任务优先级较低(数字越小,优先级越高)。
- id:
1
,任务的唯一标识符,通常用于调试和跟踪。
- pbos栈底地址:
my_task_0
,栈数组的起始地址,栈底指针,指向任务栈的底部,存放栈数组的起始地址。
- stk_size:
sizeof(my_task_0)
,栈的大小。
- pext扩展数据地址:
NULL
,无扩展数据。该参数表示指向任务扩展数据的指针。任务扩展数据用于存储任务特定的信息,如事件标志组、互斥信号量等。
- opt:判断是否需要对堆栈进行清0操作。
- name任务名称:
"LED_OFF"
,表示任务的功能是关闭 LED。
2.3.7 编写任务函数
- 传递函数指针的时候,直接写函数名。
- 创建任务的参数为:执行任务的函数地址(函数名),函数参数(void*)0,任务的堆栈空间&TwstTaskStk[99],优先级10
- 任务的执行函数为Test_Task(),无限循环,1000ms打印一次”in test“
2.4 任务调度
uC/OSII是通过内核调度函数 OS_Sched()来实现任务调度的。
- 主动调度:任务主动调用调度函数,根据调度算法选择下一个将要执行的任务.
- 如果被调度的任务就是当前任务,则不切换;否则就切换。例如,任务执行过程中调用函数 OSTaskSuspend()主动挂起自己。
- 被动调度:往往是由事件触发的。
- 例如,ticks 时钟中断产生而触发任务新的周期到达,或者有高优先级任务的等待时间结束,就需要调用调度函数来切换任务。
- 对于 RTOS 而言,调度策略通常是基于优先级的抢占式调度,高优先级任务可以抢占低优先级任务的执行,而调度的本质就是从就绪队列中找到优先级最高的任务来执行。
调度思路:
首先判断是否能调度,如果能,则找到优先级最高的任务,然后判断该优先级任务是否为当前任务,若不是,则进行任务切换;否则继续执行任务。
OS_Sched()
任务调度OS_Sched的主要步骤如下
- 判断中断嵌套层数以及锁(判断是否能够进行调度)
- 要求中断嵌套层数为0和未给调度上锁
- 找到就绪队列最高优先级最高的任务
- 这个是通过函数
OS_SchedNew()
根据优先级位图法对全局变量OSPrioHighRdy
赋值实现的。OSPrioHighRdy
是表示 OS_SchedNew() 找到的就绪队列中的最高优先级,全局变量OSPrioCur
表示当前运行任务的优先级.
- 判断就绪队列中最高优先级任务是否是当前任务,如果不是当前任务就进行任务的上下文切换
- 如果就绪队列中最高优先级任务不是当前任务(
OSPrioHighRdy != OSPrioCur
) OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
找到最高优先级任务的TCB指针,保存在OSTCBHighRdy(全局变量)中- 再通过
OS_TASK_SW()
进行任务的上下文切换。
2.4.1 判断中断嵌套层数和锁
- 两种直接返回的情况:
- 调用中断服务程序而尚未开始调度时。(是否在中断中标志
OSIntNesting
) OSIntNesting
用于追踪中断的嵌套深度。ISR执行时,OSIntNesting
的值会大于 0,表示系统当前处于中断状态。如果OSIntNesting
的值为 0,则表示不在中断中,可以进行任务调度。- 在中断服务程序中是不能进行任务切换的,因为中断服务程序结束之后有很多收尾工作要做,必须执行完所有中断服务程序才能进行任务切换。
- 任务调度上锁函数 OSSchedLock()对调度上锁(任务调度上锁标志
OSLockNesting
) OSLockNesting
是用来标记调度是否被锁定的标志。调度锁定通常是为了保护临界区,暂时禁止任务切换,确保在特定代码段(如访问共享资源、操作全局变量等)期间,任务不会被抢占。防止任务切换影响到关键的操作或数据的一致性。- 暂停抢占,保护临界区的两种方式:
- 关中断:(优点)操作系统能够防止任何中断的发生。确保任务在执行过程中不被中断打断,从而避免任务切换。(缺点)但关中断会影响其他中断的响应,会影响系统的实时性。
- 调度锁机制:禁止任务调度而不影响中断的响应。(优点)当调度锁定时,任务上下文切换被禁止,但系统仍然允许中断处理,保证实时性。(缺点:只适用于任务上下文不可重入的情况)
- 任务上下文的“可重入性”是指任务的执行可以在没有问题的情况下中断并恢复,或者任务在执行过程中可以安全地被其他任务中断和恢复。
- 不可重入任务:在执行过程中不能被中断、暂停或切换到其他任务的任务。典型的例子是当任务执行期间依赖一些共享资源(如临界区保护的共享变量、硬件寄存器等)时。
- 综合来看,调度锁机制可以提供更精细的控制,而不会牺牲中断的响应性,是现代 RTOS 中普遍采用的方式。
2.4.2 找到就绪队列最高优先级任务
OS_SchedNew()
- 优先级位图法:保证了查找时间的确定性(从就绪队列中找到最高优先级任务所花的时间与队列长度无关)
- 一个新的数据结构:优先级判定表
OSUnMapTbl[256]
- 返回的是传入8位数据中的1所在的最低位置。(如传入10100100,则返回2)
- 枚举法实现

- 查找最高优先级任务:见优先级位图法使用的三个地方(详)
- 首先根据OSRdyGrp找到最高优先级所在的组并赋值给y:
y= OSUnMapTbl[OSRdyGrp];
,然后根据OSRdyTbl[y]找到最高优先级的任务。所以最高优先级任务的列号应该为:x = OSUnMapTbl[OSRdyTbl[y]]);
即第y组的那个8位优先级数中1所在的最低位置。(eg:01001000,返回值为3) - 知道行号和列号之后就可以计算出最高优先级:
OSPrioHighRdy= (INT8U)((y << 3) +OSUnMapTbl[OSRdyTbl[y]]);
- 实际上就是行号*8+列号。
- 而如果查找到了最高优先级,就能通过
OSTCBPrioTbl
数组找到最高优先级任务的TCB
OS_TCB *ptcb = OSTCBPrioTbl[OSPrioHighRdy];
OS_SchedNew()
具体代码实现:Q3 How to Schedule tasks in Ready Queue?
- 旧任务出栈、新任务入栈
.png?table=block&id=1ddad35f-a930-8009-adc4-cb5efc1643da&t=1ddad35f-a930-8009-adc4-cb5efc1643da)
2.4.3 任务上下文切换
实际上就是将原任务的CPU现场保护在原任务的栈中,然后将新任务的CPU执行现场从新任务的栈中恢复出来(这里就可以讨论一下创建任务时进行的模拟压栈的作用了,实际上就是为了能够在任务上下文切换的时候进行统一处理)
上下文切换的概念:当内核决定要执行一个不同的任务时,操作系统就会把当前CPU的执行现场(CPU寄存器)保存在当前任务的栈上。然后新任务的上下文将从新任务的栈中恢复然后继续执行新任务,这个过程被称为上下文切换。
- 如果内核刚刚启动(即启动的时候没有正在执行的任务)。调用OSStartHighRdy()即可切换到优先级最高的任务。
- 否则进行上下文切换(context switching)
- 两种切换:
- 任务主动切换:OSCtxSw()
- 中断导致的任务被动切换:OSIntCtxSw()
- 在os_cpu.h中
#define OS_TASK_SW OSCtxSw()
OSCtxSw()
上下文切换的核心步骤如下:

- 保存当前任务的执行现场(需要按照模拟压栈的顺序保存,也就是先保存PC,然后保存剩余的除了SP寄存器的通用寄存器,然后保存状态寄存器)
- 保存压好栈的最新的SP到该任务的TCB中(也就是存储在TCB的首个字单元OSTCBstkPtr中)
具体代码分析如下:
2.4.3.1 保存当前任务的上下文
- 需要按照模拟压栈(2.3.2初始化任务栈)的顺序保存,也就是先保存PC,然后保存剩余的除了SP寄存器的通用寄存器,然后保存状态寄存器。

- FD:递减满堆栈。弹出指令(POP):LDMFD。压入指令(PUSH):STMFD
- LDMFD:Load Multiple Full Descending,表示从高地址到低地址加载多个寄存器
- STMFD:Store Multiple Full Descending,表示从高地址到低地址存储多个寄存器
SP!
:带有"!"表示自减存储,先将SP递减再存储数据。(--SP
)
STMFD SP!, {LR}
- 在不考虑流水线的情况下,PC指向下一条将要执行的语句。如图在
OS_Sched()
中调用OS_TASK_SW()
(地址:0x8000,PC为0x8004。即将发生跳转(即PC值将发生突变到CtxSW的首地址0x9000)需要存放返回地址0x8004,编译器自动存放于LR中。此时LR=PC=0x8004).模拟压栈PC位置压入的是任务的入口函数地址,这里保存LR实际上是保存了返回地址等价于保存PC)(使用BL指令时,当前PC的值会自动存储到LR中)

STMFD SP!, {R0-R12, LR}
- 第二步还要再压入一次LR寄存器,是为了占位,保证上下文的完整性(LR,R12~R0)。这个时候(还没有进入CtxSw时)压栈的时候PC与LR寄存器的值就是相同的了,但是作用是不一样的。
- 第一个LR(PC):保存的是任务切换函数返回地址,即跳回原任务的位置。
- 第二个LR:保存的是当前任务执行时的返回地址,属于任务自身上下文的一部分。用于任务恢复时跳回任务继续执行的正确位置/在任务本身执行过程中变化,保存的是任务调用函数时的返回地址。在这里是一样的。
- (在叽里呱啦什么?)但是实际上就算在中断的情况下LR寄存器中的值大部分时候也是没有意义的,因为LR寄存器会在一个函数进入的时候被保存到栈空间中,所以在中断上下文切换的时候虽然LR和PC的值不同,但是LR寄存器的值还是没啥用。
- MRS:读状态寄存器指令。状态寄存器(CPSR或SPSR)的内容传送到目标寄存器中。
- 将当前状态寄存器(CPSR)的值传送到通用寄存器(R0)。并将R0(CPSR)压入栈。
- MSR:写状态寄存器指令。直接设置状态寄存器CPSR或SPSR。
- p173(讲到的时候再说)
2.4.3.2 保存当前任务的SP到当前任务的TCB中
- 需要注意的点是
=OSTCBCur
是一种伪指令,它会将OSTCBCur
变量的地址加载到寄存器R0
中。- =(变量名):用来将一个变量的地址放入寄存器中,而不是直接加载数据。
OSTCBCur
一个全局变量,存放是当前正在执行的任务的任务控制块(TCB)的地址。- 理一下有点混乱:第一步将0x9000存入R0.第二步,将0x9000的内容,0x7000存入R0。第三步:将SP的值存入R0存的地址(0x7000)中去。即存入OSTCBStkPtr字段。



钩子函数,供开发人员扩展,如增加代码以通奸任务的切换次数。
2.4.3.3 切换当前任务为最高优先级任务(准备恢复现场)
前情提要:
OS_Sched()
中OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
找到就绪队列中最高优先级任务的TCB指针,保存在OSTCBHighRdy(全局变量)中3.1 切换当前任务块指针
目的:切换当前任务控制块(OSTCBCur)为就绪队列中最高优先级任务(OSTCBHighRdy) 的任务控制块。
将OSTCBHighRdy的地址存入R0,将OSTCBCur的地址存入R1。[R0]即OSTCBHighRdy的内容,即就绪队列中最高优先级任务的TCB的地址,存入R0。将最高优先级任务的TCB的地址存入R1所存放的地址(OSTCBCur的地址)即OSTCBCur指向最高优先级的TCB。
3.2切换当前任务优先级
同样的操作也会切换 当前任务的优先级(OSPrioCur),更新为 最高优先级任务的优先级(OSPrioHighRdy)。
优先级为8位(B代表按字节取值)
3.3将高优先级任务的堆栈地址传给处理器的SP(R13)
- OSTCBHighRdy即当前任务了。
- 就绪队列中高优先级任务的TCB中堆栈指针OSTCBStkPtr送入处理器SP。
SP=OSTCBHighRdy→OSTCBStkPtr
。此时,SP 指向高优先级任务堆栈中CPSR所在的内存地址。
- ARM 规定,SP始终指向堆栈顶位置
- STM指令把寄存器列表中索引最小的寄存器存储在最低地址,因此当任务现场信息保存完时,SP是指向CPSR的。
- 不同任务有各自的堆栈顶指针,每当任务切换时,被切换的任务的栈顶指针都保存在其OS_TCB的OSTCBStkPtr成员中。
2.4.3.4 恢复新任务的执行现场到CPU中

LDMFD:递减满栈的POP。弹出一个值到R0并更新SP。这里弹出的是T2的CPSR。
MSR SPSR_cxsf, R0
:需要先将状态寄存器恢复到备份寄存器中。- 只需要注意这里需要先将状态寄存器恢复到备份寄存器中(因为状态寄存器在保存现场时是最后一个入栈的),并且在恢复其他的寄存器时需要在LDMFD指令后面加上^以便在现场恢复的时候顺便将SPSR寄存器中的值拷贝到CPSR中。
- POP顺序:CPSR,R0,R1…,LR,PC
Q:为什么要保存两次LR
第一次保存的LR是调用 OS_TASK_SW()之前的程序指针 PC,
第二次保存的LR则是任务环境中的链接寄存器LR。
任务切换的原因:
1. 任务主动发起任务切换
如果任务主动发起任务切换,例如,某一任务执行过程中需要主动挂起自己(OSTaskSuspend()),这将触发 OS_Sched()重调度,此时两个LR的值是相等的,都是调用 OS_TASK_SW() 之前的程序指针,因为在切换前后,ARM 处理器一直都工作在系统/用户模式(system/user)。
2. 中断触发任务切换
如果是中断引发任务切换,例如,一个低优先级的任务正在运行,用户通过中断创建一个新任务,而新任务的优先级高于当前运行任务的优先级,显然,低优先级任务将被高优先级任务抢占,第一个保存的LR是中断模式下的程序指针PC,第二个LR保存的是旧任务的LR,故两个LR不相等。

造成这种情况的原因是ARM有7种工作模式:用户(user)模式、快速中断(FIQ)模式、外部中断(IRQ)模式、管理(supervisor)模式、数据访问中止(abort)模式、系统(system)模式、未定义(undefined)模式。具体而言,中断发生前,处理器工作在系统/用户模式,中断发生后,处理器自动切换到外部中断模式,而不同模式下寄存器的分配和使用是不同的,如上图所示。
Q4 Which events will trigger the context switch?
2.4.4 调度点

- OS_Sched()函数分布于操作系统内核各处,下图中的横线上的函数、事件中便会调用OS_Sched(),触发任务切换:
- 操作系统启动
- 中断启动/退出(可能改变了就绪队列。这里的中断尤其是时钟中断)
- 新任务创建(改变了就绪队列)
- 两个延时
这里就直接把调度点都列举出来(实际上就是操作系统课上讲的调度点,对于这样可抢占的操作系统,就是当就绪队列发生变化或者当前任务不再占据CPU的时候就需要进行调度)
Q5 Critical section accessing during sched?
2.4.6 临界区处理(三种关中断(进入CRITICAL的方式))

- 第一种,简单的开关中断。
- 简单高效,但是无法处理嵌套的情况
- 当有两层临界区保护的时候,在退出内层临界区的时候就会开中断,使外层的临界区也失去保护

- 第二种,引入栈来保存CPSR,判别嵌套中断
- 在OS_ENTER_CRITICAL时将中断状态入栈,再关中断
- 但是传参时易出现错误,不符合函数调用标准

- 第三种,通过局部变量的方法保存程序当前状态寄存器,再通过局部变量恢复程序当前状态寄存器。(最优,没有危险的改变sp指针,利用了参数传递,以及由c编译器来管理栈的布局)

2.5 Interruption 中断
- 实时响应:涉及到事件触发的机制以及时间触发的机制
- 中断被定义为导致程序正常运行流程发生改变的事件(程序分支除外)
- 中断是一种硬件机制,他是由硬件实现的,因此并不是任何芯片都可以使用中断。只有那些实现了中断机制的芯片才能使用中断。
- 对于软件开发人员来说,主要是在中断的硬件机制上实现中断服务程序。
- 再进一步,就是就是设计出中断响应服务的抽象层,并通过一定的封装,将中断与通断服务程序结合起来 (中断向量号),完成中断服务的注册、执行、返回等。
- 三种方式处理外界中断请求:
- 中断作为任务切换
- 触发中断,就触发切换任务,切到一个所关联的任务来处理它
- 一旦中断发生,与中断关联的任务立即被激活,或者中断释放一个信号量通知同步的任务运行。
- 这种方式的缺陷是:中断服务程序需要完成任务切换,而不仅仅只是保存被中断任务的上下文,因此中断的开销比较大。
- 中断作为系统调用
- 触发中断,中断就立马让RTOS内核去处理它
- 中断作为系统调用也称突发系统调用,就像前后台系统中的突发事务处理一样。系统调用由RTOS内核完成,运行于核心态(用户任务运行于非核心态)中断发生时,如果需要使用系统调用,中断服务程序就向内核发出系统调用请求,由系统调用完成外部中断事务。
- RTOS必须要区分开来系统调用由中断发出还是由任务发出。因为一个任务可以抢占其他任务,但不能抢占中断服务程序。
- 中断作为前台任务
- 最后一种处理中断的方式是将其视为一种特殊的前台任务。这种情况,每个任务都有独立的工作空间,而且内核运行时可以被中断的。
- 中断发生时,中断服务程序首先保存被中断任务的上下文,然后判断中断请求的类型。
- 如果请求能由中断服务程序完成,那么中断服务程序直接进行处理
- 如果不能,那么就创建或者激活某个任务来执行,中断服务程序将控制权交给内核调度器,由它决定触发哪个任务来处理中断请求,待该任务完成后,再由中断服务程序恢复中断现场
- 最终端的处理由中断服务程序和任务共同完成(操作系统将为中断服务程序提供一些系统调用,以便中断服务程序能够与任务进行通信)
2.5.1 中断的分类
中断被分为外部中断(硬中断)(Interruption)、自陷(Trap)、异常(Exception)
- 硬中断(外部中断):由于CPU外部原因而改变程序运行流程的过程
- 自陷(内部中断):通过处理器软件指令(程序指令、系统调用),可预期地使CPU正在执行的任务流程发生改变。
- 调试使C语言一步一步执行:每段代码之后编译器会插入一个软中断SWI
- 异常:CPU自动产生的自陷,以处理特定的异常事件。无法预见。
- 除0、访存失败……
2.5.2 裸板下的ARM9 2440中断流程
2.5.2.1 ARM9 2440的一级中断向量表
.png?table=block&id=1ddad35f-a930-8016-8e05-e1843d686206&t=1ddad35f-a930-8016-8e05-e1843d686206)
在以上的7种运行模式种,有两个模式对应中断:中断模式(IRQ)和快中断模式(FIQ),快中断的优先级比一般中断高。当放生中断时,PC会跳转到指定的地址开始执行。

如上图所示是异常向量表在内存中的分布。异常向量表全是汇编语言的跳转指令,从内存的零地址开始连续存储在内存中。在产生异常时,所有跳转的地址在CPU生产时已经确定且无法更改(也有一些可变,是改变异常向量表的起始地址)

- 当发生对应的异常的时候,PC将通过硬件机制跳转到相应的异常在异常向量表中的地址开始执行,再通过跳转指令跳到一个异常处理代码的起始地址。
- 这些跳转指令都是独立的跳转指令,即无法在原地实现中断服务。
- 标识符Reset/Undef/SWI/PreAbort/DataAbort/IFQ和FIQ都代表了一个地址。这个地址就是跳转指令要跳转的地址。
- eg:PC正常执行(eg0x3080)。当中断发生,PC跳转到0x18处执行b IRQ.IRQ代表的地址0X30000000为中断服务程序的地址。(IRQ是这段程序的标签,标志着这段代码的入口,其他代码可以通过该标签获得这段代码的入口地址0x30000000),b.原地循环
- 省略的代码是:这段代码所执行的功能是在中断服务程序执行完后,将引发这个中断的中断源清零,以免在中断服务程序退出后,CPU以为又有对应的中断发生,从而不断地执行同一段中断服务程序。
- 出现三个问题:
- P1.中断返回:中断服务程序执行完加法后一直在原地循环,没有返回被中断的程序的任何具体操作。
- P2.中断注册:程序没有区分中断,对于所有的中断,中断服务程序总是执行一个简单的加法,不同中断源产生的中断如何区分,各个中断服务程序如何注册呢?
- P3.状态保存和现场恢复:代码5.2并没有涉及中断的状态保存和现场恢复。

Q1 Where to return?Q1 Where to return?
2.5.2.2 中断返回
触发中断时,硬件自动将PC置为对应的中断向量表。但执行完中断服务程序后,返回的地址在哪里,怎么返回?
- 转到中断/执行跳转时,LR自动得到PC的备份。
- 但为什么返回的时候LR要减去4才能返回给PC?
- 我们希望返回到 Current 的下一条(Current+4)
- 但是,在执行Current时,PC已经变成Current+8啦!所以LR也是Current+8


- 这是因为CPU的流水线作业:
- 中断跳转的时候PC指向的是跳转指令的下两条指令(所以跳转的时候拷贝的LR也是下两条指令的地址),而返回的时候需要返回当前执行指令的下一条指令的地址。所以返回的时候需要将LR-4。
- 错开各个指令的执行,让W、F、E、D随时随刻都在工作



- Solution:在最后一行加上
Q2 How to distinguish different INTs?
2.5.2.3 中断注册
STM32中只有一个层级的中断,而2440中有两层中断——第一层就是上面的RESET....,第二层却存储在RAM中。
- 下图是第一层中断,也称为公共入口;对于IRQ,经过第一层中断后就要被导向到第二层中断来distinguish different INTs
- 第一层中断向量表是所有中断和异常的入口,他由数条LDR指令组成。不同的中断、异常触发时会执行到不同的LDR指令。

- 触发IRQ中断时,便会进入第一层中断向量表,随后跳转到IRQ中断服务程序。在IRQ服务程序中,再跳转到第二层中断向量表。下图为2440第一层”IRQ”展开的第二层中断向量表。(在公共入口中将中断分发)

- 在三星2440中,有一个类似于STM32中NVIC的中断管理器,它能集中管理外部中断源,并通过INTOFFSET寄存器来告诉CPU外边出发了哪个中断。于是,我们在IRQ中断服务程序中书写下面的代码来distinguish different INTs
- Solution:
- 中断分发代码如下:
- 存在一个rINTOFFSET寄存器。发生中断时,该寄存器会为中断源分配一个整数,这个整数与中断源唯一对应。在发生中断的时候读取该寄存器的值,根据不同的值区分不同的中断,从而执行不同的中断服务程序。
HandleEINT0
代表的是一个内存地址,其内容是对应中断的中断服务函数入口。如上图所示。每种中断服务函数入口的地址为32位,占据了4个字节,所以我们在计算每一种中断源对应的服务函数地址的时候会把INTOFFSET左移两位(乘以4)后再与起始地址HandleEINT0相加得到具体的中断服务函数入口地址。- eg:想执行HandlerINT_TIMERR0,这是第10号中断,所有INTOFFSET=1010(2)=10(10),根据地址计算可得101000(2)=40(10)=0x00000028,所以加上基地址0x33FFFF20,则为中断服务程序的地址存放的地址0X33FFFF48
- 这个IRQ程序里面并没有之前人为返回的指令,是因为通过关键字_irq交给编译器处理了,编译器种也会有一个处理关键字_irq的程序。


总结:通过查找
INTOFFSET
寄存器和HANDLEINT
地址,通过计算:INTOFFSET
*4+HANDLEINT
得到对应的中断服务程序地址存放的位置- 硬件自动根据中断源,确定该执行哪个中断(32中是由NVIC完成)
Q3 How to save the information of interrupted programs?
2.5.2.4 裸板的状态保存和现场恢复
保存T1现场
- 压栈R0~R12
- 压栈R14(LR)
- 压栈CPSR
- 通过SPSR & R0
下面是裸机情况下完整的中断服务程序公共入口:
- 需要注意的是这里并不是在末尾来根据流水线调整返回地址的,而是在公共入口的开始处先将LR寄存器-4了。
- (1)~(4)完成了现场的保护,保护了r0~r12,被中断的返回地址和被中断的程序当前状态寄存器(保存在SPSR中)(5)~(11)判断响应的中断源然后跳转到响应的中断服务程序的入口地址。(12)~(15)恢复被中断的程序寄存器,然后返回被中断的程序。
2.5.3 OSIntCtxSw()和裸板下的IRQ
2.5.3.1 ARM的7种工作模式
- ARM有7种工作模式:用户(user)模式、快速中断(FIQ)模式、外部中断(IRQ)模式、管理(supervisor)模式、数据访问中止(abort)模式、系统(system)模式、未定义(undefined)模式。
- 中断发生前,处理器工作在系统/用户模式,中断发生后,处理器自动切换到外部中断模式,而不同模式下寄存器的分配和使用是不同的。

2.5.3.2 裸板下的IRQ

- ①PC指向0x6000的时候发生中断INT,引起模式切换。硬件自动完成:LR_IRQ=PC_流水线预取指令=0x6008,SPSR_IRQ=CPSR(Function)……以及一些CPSR的设置。②同时硬件自动跳转PC到0x18,③再通过标签跳转到IRQ一级中断服务程序

- 此时的保存现场是保存在中断栈里面的,注意原来的OSCtSw()是在这个函数中保存现场,而中断引起的任务切换是在IRQ一级中断服务程序(又叫中断服务程序的公共入口)里面保存T1现场。
- ④此时第一个LR保存的是中断模式下的PC,第二个LR保存的是旧任务的LR(这个LR可能是出这个F1之后需要回到main中的地址),所以两个LR不相等。
- ⑤保存T1现场,⑥区分中断源:以OSTickISR为例。
- ⑦:此时PC到0x30000024,正是OSTickISR需要返回的地址。所以将返回地址0x30000024存入LR(
MOV LR,PC
这个时候LR为LR_IRQ),所以并不需要PC-4.然后跳转到OSTickISR(即将首地址存入PC,MOV PC,R1
)

- ⑧这个时候进入IRQ二级中断服务程序,即真正的执行所要求功能的中断服务程序,如OSTickISR。此时PC指向OSTickISR的入口地址0x40000000,
- 注意:第一步已经将LR_IRQ保存到R5,因为后续
BL OSTimeTick
(在进行这个函数调用的时候CPU硬件保留了LR,这个时候LR的值是PC-4,即因为流水线实际上为这紧邻的下一条。在追寻本质,那就是编译器会把函数返回处理为BX LR
)⑨处理完毕整个OSTickISR后会返回LDMFD这一句执行。
- 代码分析:
- S3C2440 中断控制器核心寄存器解释
SRCPND
(Source Pending Register)——源挂起- 每个位对应一个中断源。只要外设产生了中断请求,对应位就被置为1。这个表示“有中断发生了”
- 作用:识别“谁请求了中断”。
INTPND
(Interrupt Pending Register)——目的挂起- 只有当
SRCPND
有中断发生,并且该中断的优先级是当前最高的,才会在INTPND
中被设置。表示“CPU 正在处理谁的中断”。 - 清除顺序建议
- 进入中断后先清
SRCPND
(表明“我已经处理过这个源了”) - 再清
INTPND
(说明“这次中断处理完毕”) - 汇编指令详解:
ORR R1, R1, R2
- 按位或(OR)操作。
ORR <目标寄存器>, <操作数1>, <操作数2>
。 - R1 = (R1 原来的清除目标位) | (R2 原来的挂起状态)
- 想要清除的位是R1,同时也不想影响其他的中断源(保留原本的挂起状态)R2
STR R1, [R0]
- 把刚刚合并好的
R1
值写回SRCPND
寄存器的地址(R0
里面是SRCPND
的地址) - S3C2440 的规则是:SRCPND 写 1 来清除对应位
- 这样一次就清除了多个位……如果只想清除那一位,应该是只用写STR这一句而不是要写ORR运算。
- 但是总而言之,只需要知道在真正中断程序中需要清除一下源挂起和目的挂起寄存器即可。
回顾:2440里面中断进入的时候硬件会进行的操作
- 保存被中断程序当前的pc到LR_irq中(也就是中断返回后应该执行的下一条指令位置,所以一级中断里面对他-4操作再保存到任务的栈的pc位置)
- 将程序当前状态寄存器CPSR的值放入相应模式的SPSR(也即是irq的spsr)(用于中断返回时的恢复)
- 切换处理器模式为irq模式,也就是将CPSR的模式位设为相应的中断模式,并禁用相应模式的中断。如果是快中断模式,则禁用所有中断。
- 通过异常向量表找到irq应该进入的处理程序地址,放入pc中实现跳转(即来到一级中断程序中,即中断程序的公共入口处)

- 栈中弹出一个值到
r0
,也就是中断发生前保存的CPSR
。
- 将
r0
中保存的CPSR
的值恢复到SPSR_irq
中 - 为什么不是恢复
CPSR
? - 因为 中断模式下不能直接写
CPSR
,只能写SPSR
,并通过最后的movs pc, lr
自动把SPSR_irq
的值恢复回CPSR
。
- 从栈中弹出之前保存的通用寄存器
r0~r12
以及lr
的值。
- 跳转回中断发生前的程序(即
lr_irq
中保存的地址) - 关键点在于
movs
:这个带s
后缀的指令不仅将lr
的值赋给pc
,而且还自动把SPSR_irq
的值恢复到CPSR
中 - 所以这条指令完成了:“返回”和“状态恢复”两个关键动作。PC=0X6004(中断发生时硬件存的LR_IRQ=PC_流水线=0x6008,但是在IRQ一级中断程序的第一句
SUB LR,LR,#4
,已经将LR_IRQ变成了0x6004,即发生中断的那条指令的下一条指令) - 这里的两个LR是不一样的,一个是系统/用户模式下的LR,一个是LR_IRQ
- 一些汇编的知识点:
- 注意前面所写的模式切换的寄存器使用,前13个寄存器(r0~r12)在系统/用户模式是通用的,这个LR代表的是LR_IRQ,所以在这里已经存入中断栈里面。在接下来的MOV LR,PC这个时候LR_IRQ的值被覆盖也没有关系。
- 从高地址到低地址存入SP所指向的栈,这里的SP为SP_IRQ,所指向的栈为中断栈。
- 注意:STMFD 为 先
SP = SP - 4
,再存(从高地址到低地址) - 注意STMFD是反着寄存器列表的顺序压栈的,即先存LR,再存R12,R11,……,R0(
共需空间 =
14 * 4 = 56 (0x38)
字节,所以栈顶向下移动:0x1000 - 0x38 = 0x0FC8
) - LDMFD弹栈方向,按寄存器列表顺序出栈,R0,R1,……,LR
- LDMFD:先取,后
SP = SP + 4
(从低地址到高地址) - 取出最后一个LR_IRQ(即0x0FFC的内容后),SP +=4,SP回到0x1000.
- STMFD和STMDB作用一样,就是先递减再存,为
--sp
,前者在表示上更容易阅读,是在栈内递减
- 整个流程如下图:


Q4 [ OS ] How to process the IRQ if μc/OS II is running ?
2.5.3.3 uc/OS II 运行下的IRQ(中断的发生及相应)
- 由于μC/OSⅡ是一个可抢占的内核,中断处理完成之后,内核调度程序 OS_Sched()会从就绪队列中找到优先级最高的任务运行(优先级最高的任务有可能不是被中断的那个任务),因此在发生中断、进入用户的中断服务程序前,首先需要保存被中断任务的上下文:在完成用户的中断服务程序之后,需要进行任务重调度以确定恢复到哪一个任务的上下文。
- 此外,uC/OSI允许中断嵌套,所以在中断服务程序中还需要增加中断嵌套计数器,然后根据中断嵌套的数量来决定是否重新调度。
- 有操作系统的中断服务程序公共入口的代码如下:

流程详细分析如下:

- 前3步和裸板中断一样,硬件自动完成四件事:①LR_IRQ=PC_流水线=0x6008,②将CPSR存入SPSR_IRQ中③模式切换SVC模式到IRQ模式(即修改CPSR的模式位)④PC自动跳转到一级中断服务程序(即中断服务程序的公共入口)
- ARM9没有中断向量表,取而代之的是异常向量表。所有的中断IRQ都会进入同一个入口,因此可以在这个入口中完成如保存上下文、增加中断嵌套计数器等共有操作,然后再通过INTOFFSET 和自定义的中断向量表进入对应的中断服务程序。
- 在完成中断服务程序后,调用μC/OSⅡ的调度函数进行重新调度。

核心是:保存原任务的上下文到它自己的任务栈中(此前无操作系统时是保存到IRQ的栈中)
- CPU只能访问当前模式下的LR、SPSR、SP。我们需要把原任务上下文保存到SVC模式的栈中,而触发IRQ后,原任务上文的LR、SPSR、SP信息处于IRQ模式中!
- 我们怎么在SVC模式下访问IRQ模式的寄存器呢?
- 很显然,我们只有把他们备份到所有模式共享的通用寄存器。
- 但是通用寄存器的内容就会被覆盖!
- 因此,我们先备份通用寄存器
- 再备份SP_IRQ、LR_IRQ和SPSR_IRQ到通用寄存器。

4.保存R0-R2寄存器的值压入到当前的IRQ栈
- eg:SP最初是0x500C,将R2存入0x5008,将R1存入0x5004,将R0存入0x5000.将此SP存入R0,即在R0(0X5000处存入SP=0x5000),然后SP+=12,即复原SP,SP=0x500C
5.然后将此SP放入R0,IRQ的SP复原,将R1保存LR_IRQ-4,R2保存SPSR_IRQ(中断前cpsr)
- R1保存的是LR_IRQ-4=0x6008-4=0x6004(即返回地址),R2保存的是SPSR_IRQ(在IRQ模式下只能访问SPSR,而这个时候的SPSR_IRQ是硬件自动保存的原来任务的CPSR)


6.切换CPU模式:从IRQ到SVC并禁止中断(IRQ和FIQ),转到任务的堆栈
msr cpsr_cxsf, #...
是什么?msr
(Move to Status Register):将一个立即数或寄存器的值写入程序状态寄存器(CPSR),写入的是cxsf字段
#SVCMODE | NOINT
是什么?msr cpsr_cxsf, #0x13 | 0xC0 → msr cpsr_cxsf, #0xD3
(*****)7.保存原来任务的上下文到SVC模式的栈中

- 注意:由于在SVC和IRQ的R0~R3是通用的,所以这里的R1就是前面所提到的LR_IRQ-4,也就是用作恢复执行原任务的PC。R3~R12也是通用的,即保存的是原任务的上下文。这里的LR是SVC模式下原任务的LR_SVC,与LR_IRQ不一样
- 入栈顺序是R1(PC),LR(R14),R12,R11……R3

- LDMIA:Load Multiple(一次加载多个寄存器)Increment After(地址自增,从当前地址开始,之后地址加4)
r0!
= 加了!
表示 更新 r0 的值为最终地址{r3-r5}
= 表示将r0
所存地址开始的连续内存内容,加载到r3, r4, r5
- 注意这里的R0存放的是前面的SP,即0x5000(即通过R0指向IRQ栈),将0x5000的内容(原R0)存入R3,0x5004的内容(原R1)存入R4,将0x5008的内容(原R2)存入R5
- 但并不会改变SVC栈里面的内容

8.中断嵌套计数

- 前言已经写到:此外,uC/OSI允许中断嵌套,所以在中断服务程序中还需要增加中断嵌套计数器,然后根据中断嵌套的数量来决定是否重新调度。
- 这个时候仍然在SVC模式
- 这个时候已经发生中断,那么就需要将中断嵌套数+1;
strb r1, [r0]
:将更新后的值 存储回OSIntNesting
变量。strb为存储一个字节,所以这里更新的是OSIntNesting的低8位。如果是32位的数据,应该使用str。下面存储SP的时候可以看到。
- 如果当前的嵌套层数不等于1(即在这个中断之前已经存在中断了),跳转至编号1
teq r1, #1
:测试相等(Test for Equality)指令,类似于CMP
,但不保存标志位bne %F1
:bne
是Branch if Not Equal(如果不等则跳转)

8.1(⑨)如果当前嵌套层数=1,即当前中断是第一个中断。
- 前两句很经典的套路:关于TCB成员内容的提取
OSTCBCur
是指向 当前任务控制块(TCB) 的指针- 先存储成员的地址:
ldr r0, =OSTCBCur
将OSTCBCur
的地址加载到r0
- 提取该地址上的内容(即提取该成员),此时寄存器里面的值位该成员的内容:
ldr r0, [r0]
就是加载当前任务的 TCB 地址到r0
- 然后
str sp, [r0]
将当前SVC栈指针保存到[r0]。即保存到TCB的第一个成员中(OSStkPtr指向任务栈)
8.2 如果当前嵌套层数≠1,即存在中断嵌套,那么就跳转到序号1


- ⑩因为上下文已经保存到SVC任务栈中,所以不用继续停留在SVC模式。修改CPSR_c切换回IRQ模式
- ⑪接下来和裸板的中断处理类似,从INTOFFSET(0x4a000014)读出中断偏移量,然后找到(二级)中断向量表的首地址(HandlerEINT0),之后进入对应的中断服务程序OSTickISR。⑫(INTOFFSET*4+HandlerEINT0)=(
ldr pc,[r1,r0,lsl#2]
)

- ⑫注意执行保存返回地址的指令
mov lr,pc
由于流水线的存在,PC指向当前指令的下两条指令(即当前mov指令地址+8),即msr模式切换指令


- ⑬结束完中断处理程序之后,完成中断现场的恢复。这个时候虽然嵌套层数不唯一,即需要恢复的是上一层中断的现场,但这是一个有操作系统的IRQ返回,所以上层中断的现场依然保存在SVC模式的栈中,所以切换到SVC模式下恢复现场并且关闭中断。
- ⑭进行中断返回(
BL OSIntExit
) - 在uc/OS完成中断处理之后,需要调用中断退出函数OSIntExit(),进行中断嵌套以及是否允许调度的判断,并选择最高优先级程序。(所有的中断服务程序执行结束后都会进入此函数,无论是否是嵌套的中断)
- 注意这就是和裸板中断的区别,有操作系统后处理完中断后会进行任务的调度。

- 先判断系统运行,后进行中断嵌套处理(因为刚刚执行完一个中断服务程序,所有嵌套数-1)进行中断嵌套的和调度的判断,记得进入临界区。
- 当中断嵌套数归0且没有锁定,才会进行下面的调度操作。
OSPrioHighRdy=(INT8U)((OSIntExitY<<3)+ OSUnMapTbl[OSRdyTbl[OSIntExitY]]);
- 如果最高优先级的任务不是当前任务,就进行任务切换。
OSCtxSwCtr
是一个计数器,用来记录上下文切换的次数。每次发生上下文切换时,计数器加1。- 中断中的上下文切换

2.5.3.4 OSIntCtxSw()

- 在OSCtxSw()中:
1.保护现场:保护寄存器:R0-R12 、 LR 、PC(这里主动切换的话和LR相同)还有CPSR
2.将保护好现场,压好栈的最新sp放入OSTCBCur的OSTCBstkPtr字段(也就是第一个)
- 在OSIntCtxSw()中:第一个步骤以及第二个步骤是由统一的中断服务处理程序完成的,如下图(
OSIntCtxSw()
就是OSCtxSw()去掉了上下文保存后的部分,因为在IRQ一级中断服务程序中已经保存了任务的上下文):

补充叙述一下这里的保存上下文的思路:
- 准则是:我们需要保存pc(返回任务执行的点)、LR、R12-R0、CPSR这16个寄存器到任务的栈sp里面,并且将存好的sp放入对应任务的
OSTCBStkPtr
里面。
- 而进入中断的时候是在irq模式,任务的sp是在svc模式,这里就需要转换模式,但是一些信息已经跑到irq模式了(比如被中断的任务的pc和当时的cpsr信息都在irq模式,需要用统一的寄存器传递出来,而寄存器本身也需要保存,所以就用irq的栈来保存寄存器原来的值)
- 所以代码里面:先将R0-R2入栈,R0保存irq下的sp指针(用于在svc模式下访问),这个时候就可以恢复irq的sp了我们只需要存入堆栈,不用让sp继续跟踪,用R0跟踪位置即可,R1用于保存LR-4对应的任务返回PC值,R2用于保存任务被中断时的cpsr(从irq模式下的spsr获得,因为中断的时候将其保存到spsr然后再修改了cpsr)
- 而后切换到svc模式进行入栈操作,先R1入栈保存返回的PC,然后是LR R12-R3入栈,(R0-R2的内容在irq堆栈里面),然后R0作为irq的sp索引,出栈到R3-R5(对应R0-R2),然后入栈R2-R5(R2存储的是中断前的cpsr),从而完成任务栈的保存,最后将任务sp更新入
OSTCBCur→OSTCBStkPtr
里面,完成上下文保存。
- 所有任务的上下文在栈的布局如下:

统一的中断处理程序里面压栈后,执行第二个步骤将
OSTCBCur→OSTCBStkPtr
=sp的操作和非中断的OSCtxSw
有不同,在于这里判定了是否中断嵌套,如果中断嵌套了的话,只是仍然将被中断的上下文(即该层中断的上一层中断的上下文)保存到原始的任务的堆栈里面,但是这个上下文实际上并不是任务的上下文,所以不用更新到OSTCBCur→OSTCBstkPtr
里面。ps:这里可以注意一个恢复现场的细节:
- 在恢复cpsr的时候,是把内容填充如spsr_cxsf里面,由指令
LDMFD SP!,{R0-R12,LR,PC}^
- 这个指令包含PC并且有^,所以会恢复spsr内容进入cpsr,这样同时实现寄存器和模式的切换
- 这个restore的过程和模拟压栈的顺序息息相关,也是我们安排每个上下文存储应该有的顺序。
我结合
OSIntExit()
来说:(ps:OSIntExit的实现代码很大部分和sched
加上sched_new
一样,原因是它的大部分功能是执行任务切换(少部分是更新OSIntNesting
),但它并没有直接调用sched,因为它需要调用OSIntCtxSw
而不是OSCtxSw
)先判定是否OS跑起来,然后修改OSIntNesting(-1)(注意enter critical需要),然后判定是否有中断嵌套或者锁嵌套,之后操作和
sched_new()
一样,查找到最高优先级的就绪任务:OSTCBHighRdy
,不过需要像sched()
一样,判定OSTCBHighRdy
是否和OSTCBCur
一样,不一样的话需要调用中断的上下文切换(OSIntCtxSw
)

OSIntExit()
函数在 SVC 模式下执行,然后又调用OSIntCtxSw()
注意当前都在SVC模式下,恢复CPSR的思路是先将CPSR的值存入SPSR,再通过^更新CPSR。
OSIntCtxSw()
执行完后返回上级函数OSIntExit()
,然后退出临界区,返回上级即PC⑮,此时还是在SVC模式下。从栈中加载一个四字节数据到r0,并且将sp+4,由我们上面所画的图可以看出,STMFD为先减再存,此时指向SVC栈内最后一个成员CPSR。
- [sp]:sp指针所指向的内存地址。
ldr r0, [sp], #4
,加载SP所指向的内存地址的数据到R0,然后更新SP,SP+=4;那么r0里存放的是SVC栈内任务的CPSR。然后将r0的值写入SPSR中。LDMIA弹栈恢复任务现场,先加载数据再自动递增SP。^
符号表示恢复返回时会自动恢复CPSR
。

总的流程: 保存IRQ模式下的特殊寄存器-退出中断模式-保存现场-返回中断模式中断-区分中断源-中断服务程序-退出中断模式-恢复现场
- 保存R0-R2寄存器的值压入到当前的IRQ堆栈,然后将此SP放入R0
- IRQ的SP复原,将R1保存PC(LR_IRQ-4),R2保存中断前CPSR(IRQ模式下只能使用SPSR,中断时硬件自动保存CPSR到SPSR中)
- 退出中断模式,改为任务的SVC模式,转到任务的堆栈
- 保存现场,PC(R1)入栈,然后是LR(R14) R12-R0
- 然后从R0(SP)指向的IRQ堆栈中出栈到R3-R5(对应保存的R0-R2),然后连着R2(保存的CPSR)入栈(SVC任务栈)
- 更新中断嵌套OSIntNesting计数,然后判断是否在中断嵌套里面,如果没有嵌套则:
将SP保存入当前任务的TCB记录:
OSTCBCur→OSTCBStkPtr
里面- 如果存在中断嵌套,恢复cpsr为irq模式,也就是进入irq模式,取得INTOFFSET值来区分中断源
- 计算得出IRQ入口,保存到PC,执行ISR。
- ISR完成后,切换到svc模式,先调用OSIntExit进行可能的任务重调度以及更新OSIntNesting
- 最后是恢复现场(完成任务的恢复执行)
和裸板中断对比
裸板没有操作系统,没有任务一说,栈就在统一的区域,进入中断,同样要保护现场,就把所有寄存器:R0-R12 SP(R13) LR(R14)-4(对应返回的pc) CPSR保存入栈,特别强调,这里其实是并没有保存sp的!(因为没有任务的概念,sp的区分只在于异常模式和svc模式),然后处理完中断后,会相应恢复现场,从栈里面弹出这些寄存器的值,这个期间,是支持中断嵌套的,整个过程都是在IRQ模式下进行。那没有保存sp有影响吗?没有的,因为sp的改变仅仅是用来存放了中断时的上下文,而恢复现场后,sp的值也自然的由于出栈就恢复到中断之前的值,所以不需要单独保存。
而UCOSII上,每个任务都有一个任务栈,而进入中断模式,也有irq对应的R13_irq,专门的异常处理堆栈,这个时候的现场保护,需要进入svc模式压栈(压入对应任务栈),需要保存跳入的(LR-4)(对应返回的pc,为R14_irq),R0-R12,在svc模式下的LR。这里虽然也没有保存sp在栈里面但是把它保存到了任务TCB对应的字段
OSTCBStkPtr
,这是因为需要恢复任务的时候知道任务的栈在哪里,并且还多保存了在svc里面的lr,这是不同。然后进入IRQ模式,进入对应中断服务程序,并且服务程序返回后,需要调用OSIntExit()
来出中断(涉及中断嵌套计数以及可能的任务重调度)。再返回(由以后的任务重调度返回或者没有发送任务重调度),执行恢复现场操作。中断服务程序运行在IRQ模式下,而uc/OS II 运行在SVC模式下,由于不同模式会用到不同的堆栈寄存器,因此uc/OS II任务的上下文会保存在SVC模式下的SP指针指向的堆栈中
有没有操作系统的区别是什么呢?
——中断结束后的返回点不同了!
注意一点,裸板下的IRQ中断服务程序,执行完后,回到某条指令执行,它整个代码可以看做是只有一个任务(线程),不管怎么返回也都是在这一个任务中执行。带有OS时,执行完IRQ后有可能就调度到其他任务去了,不会返回原中断点执行。但是OS就不一样了,它必须保存LR_IRQ到任务栈中,才能保证该任务能从被终端点处回复执行。
- 没有操作系统时,所有代码均为一个线程。不管怎么弄跳转都在一个线程里面跳
- 有操作系统时,跳转就牵扯到任务(线程)的切换了!被中断的线程不一定会在中断结束后立即恢复执行。这时候要保证被中断的线程能再度从被中断处继续执行,因此多了很多操作。
Q5 [ OS ] How to process context switch triggered by the IRQ?
见
OSIntCtxSW
μC/OS II 的中断管理机制
μC/OS II是一个非常精简的RTOS,其内核没有专门针对对中断的管理机制,但对于中断服务程序的编写有一定要求。
- μC/OS II是一个可抢占的内核,中断处理完成之后,内核调度程序OS_Sched()会从就绪队列中找到优先级最高的任务运行(优先级最高的任务并不一定是被中断的那个任务),因此在发生中断、进入用户的中断服务程序前,首先需要保存被中断任务的上下文。
- 此外,μC/OS II允许中断嵌套,所以在中断服务程序中还需要增加中断嵌套计数器,然后根据嵌套数量来决定是否重新调度。
这些就是μC/OS II中的中断与裸板中断的区别。
- 所有的IRQ中断都会进入同一个公共入口,于是乎我们可以效仿上面的做法。把IRQ中断的公共操作都放在这里面,比如:
- 保存原任务的上下文到它的栈中
- 增加中断嵌套计数器
- 确定对应的中断向量号
- 完成中断服务后,调用 μc/OS II 的调度函数

OS(左)与裸板(右)的中断流程
Q6 Is it an address in each vector?
很显然,它不是地址,而是LDR指令

2.6 时间管理(TIME MANAGEMENT)
- 通过时钟可以实现:
- 延时任务
- 周期性触发任务执行
- 任务有限等待的计时
- 软定时器的定时管理
- 确认超时
- 时间相关的调度操作
大多数嵌入式系统有两种时间源:
- 实时时钟 (RTC, Real-Time Clock)
- 依靠电池供电,即使系统断电,也可以维持新日期和时间
- 独立于操作系统,因此也称为硬件时钟,它为整个系统提供一个时间标准
- 定时器/计数器 System Timer
- 嵌入式处理器集成了多个定时器或者计数器
- 实时内核需要一个定时器作为系统时钟,并由内核控制系统时钟工作
- 系统时钟的最小粒度是由应用和操作系统的特点决定的
- Size of System Timer:Tick
一般而言,实时时钟是系统时钟的基准,实时内核通过读取实时时钟来初始化系统时钟,此后两者保持同步运行,共同维持系统时间。

有μc/OS ii 的IRQ服务程序(左)OSTimeDly()函数 (右)

- 根据硬件的不同,嵌入式系统的时钟源可以是专门的硬件定时器,也可以是来自交流电(AC)的 50/60 Hz信号频率。
- 定时器一般由晶体振荡器(crystal osillator)提供周期信号源,并通过程序对其计数寄存器进行设置,使其产生固定周期的脉冲,而每次脉冲的产生都将触发一个时钟中断。
- 时钟中断的频率既是系统的心跳,也叫时基或tick,tick的大小决定了整个系统的时间粒度。
- 上图解:晶体振荡器提供周期信号源(时钟源),它通过总线(bus)连接到 CPU 核(CPUCore)上,开发人员可编程设定计数寄存器(图中为counter)的初始值,随后,每一个晶体振荡器输入信号都会导致该值增加,当计数寄存器溢出时,就产生一个输出脉冲(pulse),输出脉冲可以用来触发 CPU核上的一个中断。输出脉冲是RTOS时钟的硬件基础,它将被送到中断控制器上,产生中断信号,触发时钟中断,由时中断服务程序维持系统时钟的正常工作。
实时内核的时间管理以系统时钟为基础,通过tick处理程序来实现。
- 以系统时钟为基础:
- 定时器周期性产生中断脉冲驱动,RTOS将响应并执行其中断服务程序,并在中断服务程序中调用tick处理函数
- Tick处理程序
- 实时内核的一部分,与具体的定时器/计数器无关,由系统时钟中断服务程序调用,使内核具有对不同定时器/计数器的适应性。
- Tick处理程序与中断的关系
- 触发流程:
- 硬件中断:定时器周期性输出脉冲,向CPU发送时钟中断。
- 中断处理:
- 硬件抽象层(HAL)完成底层中断处理。
- 内核层响应中断,找到内核层对应的时钟中断号
- 调用该中断号对应的服务程序(即Tick处理程序)。
- Tick处理程序的核心功能
- 内核时钟管理
- 线程延迟操作超时检测与处理。
- 调度策略相关操作(如时间片轮转调度)。
- 系统时间基准维护(如全局时钟计数累加)。
- 关键特点:与硬件定时器无关,仅依赖系统时钟中断触发。
- 时间片轮转调度
- 触发条件:Tick触发,每次时钟中断执行Tick处理程序。
- 在Tick处理程序中对当前正在运行的任务的已执行时间进行“加1”操作(或者是剩余时间进行-1操作)。执行完该操作后,如果任务的已执行时间与任务时间片相等,则表示任务使用完一个时间片的执行时间,需要进入调度点,触发重调度。
- 线程延迟操作流程
- 触发条件:线程调用延迟函数(
OSTimeDly()
)主动挂起自身。 - 状态切换:
- 运行 → 挂起:当前线程从 运行状态(Running) 切换为 挂起状态(Suspend)
- 加入等待队列:线程被挂载到 时间等待链(Delay Queue),记录剩余等待时间(单位为Tick)。
- 时间等待链(Delay Queue)机制
- 等待队列也称为时间等待链,用来存放需要延迟处理的任务。
- Tick处理程序的关键操作
- 遍历等待链:每次时钟中断触发Tick处理程序时,遍历时间等待链中的所有线程。
- 剩余时间递减:对每个线程的剩余等待时间执行 “减1”操作。
- 线程唤醒:
- 某个线程的剩余等待时间被减到0,则将该线程从等待队列中移出
- 挂载到就绪队列(标记为可调度状态)
- 触发调度点:在Tick中断退出前调用调度器,触发重调度。
2.6.1 OSTimeDly()
先是我的远古记忆感受:
就是TCB里面有个delay的设置,记录了要等多少tick,tick是systick的计数,systick在我们f401里面是挂载在AHB上(84Mhz),一次systick中断就会让tick-1,当达到0时,会判定任务是否被挂起或者等待着某个事件,如果没有的话就直接恢复放到就绪队列里面就行了,调用OSTimeDly的时候就是把任务从就绪队列里面移除,设置它的
OSTCBCur→OSTCBDly
字段为传入的参数值,最后进行重调度。下面是OSTimeDly()的具体代码实现:
- 代码分析:
ticks > 0u
:0个tick就表明不需要延时任务,函数会立马回到调用处- OS_PRIO:8位无符号数,最高两位一定为0.高3位代表组索引,低3位代表列索引,优先级=y*8+x(可有可无的类型转化)
OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX
:取反位掩码则生成清除掩码,那么与运算,则清除任务在OSRdyTbl对应位,同时使用掩码,可以保护其他位2- 如果当前任务优先级组(这一行所有位都为0)没有其他就绪任务,
OSRdyGrp &= (OS_PRIO)~OSTCBCur->OSTCBBitY; /* 清除就绪组中对应的组标记 */
OSTCBDly
:TCB中记录任务剩余延迟时间的字段。OSTCBCur->OSTCBDly = ticks; /* 设置任务的延迟时间(Tick数) */
- 注意:退出临界区
OS_Sched()
:触发重调度任务切换,执行下一个就绪队列中优先级最高的就绪态任务
- 代码里面看起来就是先判定是否中断嵌套,是否锁嵌套,然后如果设置了有效的延迟(ticks大于0),然后就是从就绪队列里面移除任务的操作,主要是先把
OSRdyTbl
这个位图里面对应任务那个位置零,再看是否需要更新伴随的OSRdyGrp
(按照行来分组,也就是从0号任务开始,每8个任务为一组)是否需要清零表明这一组(行)的任务都不在就绪状态。最后设置任务的OSTCBDly
字段为需要等待的ticks,这个ticks是每次在systick
中断(在stm32f401的移植里面)处理程序里面的OSTimeTick
函数里面会更新的,减到0以后会主动调用重调度切换任务执行(还是寻找就绪队列中优先级最高的任务执行)。

2.6.2 OSTimeTick()
2.6.2.1 OSTickISR的核心代码
ARM Mini2440的0号中断对应的是时钟中断,如果产生了时钟中断,就需要相应的中断服务程序来进行处理。μC/OSI在ARMMini2440上的中断服务程序需要完成的工作由OSTickISR定义。
- ARM Mini2440中断控制器基础
- 中断触发:Timer0周期性产生中断,CPU跳转到
OSTickISR
。 - SRCPND(Source Pending Register):记录所有中断源的挂起状态,每位对应一个中断源(如Timer0对应位10)。
- INTPND(Interrupt Pending Register):中断挂起寄存器,记录当前系统中挂起的中断请求,需手动清除以响应下一次中断。
- 中断清除机制:向SRCPND和INTPND的对应位写1可清除中断标志,确保中断能再次触发。
- 首先得到Timer0在中断源寄存器SRCPEND中的bit位置(对应位10,100 0000 0000)(0x400);然后将中断源寄存器SRCPND中的值与0x400进行或操作,并将值写回,达到清中断(pending)的目的。
- 同理,将INTPND寄存器(只有一个待响应的中断处于挂起状态)的值读出再写回,清除挂起状态。str将
r1
寄存器中的值写入到r0
寄存器所指向的地址(即INTPND
寄存器)。
- 最后跳转到OSTimeTick()Tick处理函数。递减所有任务的延迟时间(
OSTCBDly
)。检查时间片耗尽的任务,触发调度。维护系统时钟计数器(OSTime
)。
mov pc, r5
从中断返回,恢复被中断的任务。
OSTimeTick
是在systick/OSTickISR里面被调用,也就是每次系统时钟中断的时候会调用这个函数进行核心的中断服务处理,它(应该是在systick
里面而不是OSTimeTick
)会记录中断次数到一个全局变量Time,并且遍历OSTcbList
(也就是使用的TCB链表),对于每个TCB去判定是否OSTimeDly
非0,非0说明被阻塞延时了,OSTimeDly
表示仍然需要等待的tick数,这个时候需要更新它(-1操作),然后进一步判断此时是否OSTimeDly
为0,为0说明等待时间到达,如果达到并且任务没有被suspend
或者没有等待某个事件,那么就把该任务放回就绪队列,最后在整个中断服务程序(systick
/OSTickISR
)返回之前,会调用UC的OSIntExit()
函数更新OSIntNesting
以及进行可能的任务重调度(OSIntCtxSw
)。
2.6.2.2 OSTimeTick的核心代码


- 临界区保护
- 内核运行状态检查以及遍历TCB链表
- 取一个指针指向链表头TCB
OSTCBList
是一个全局双向链表,按优先级顺序链接所有任务的TCB。- 空闲任务(IDLE) 是优先级最低的任务(通常为
OS_LOWEST_PRIO
),位于链表末尾。 - 遍历终止条件:当
ptcb->OSTCBPrio == OS_IDLE_PRIO
时,停止遍历,避免处理空闲任务。
- 进入临界区和延时处理
- 检查任务剩余等待时间是否为0,非零就递减该值
--ptcb->OSTCBDly
- 如果递减后剩余时间为0,则需要唤醒
- 唤醒条件判断
OSTCBStat
表示任务状态(如挂起、等待事件等)。按位标志,检查那一位是否为1- 状态掩码的使用
- 状态掩码通常通过位运算(如按位与
&
)来检查任务的当前状态,或者通过按位或|
和按位异或^
运算来修改任务的状态。例如,要检查一个任务是否被挂起,可以使用如下代码:就是对应位置1 - 加入就绪队列
状态宏 | 含义 | 值(十六进制) |
OS_STAT_RDY | 就绪状态 | 0x00 =0000 0000 |
OS_STAT_SUSPEND | 被挂起 | 0x08 =0000 0100 |
OS_STAT_WAIT | 等待某事件 | 0x10 =0000 0001 |
OS_STAT_WAIT_TIMEOUT | 等待超时 | 0x20 =0000 0010 |
注意:这些状态常常是可以同时存在的,例如一个任务可以是
WAIT + SUSPEND
状态。- 如果为挂起状态(虽然Tick变为0,延时结束,但是还为挂起态不能就绪,所以需要重新设置
ptcb->OSTCBDly = 1
,让下一次Tick再来判断一次) - 为什么不让
OSTCBDly
维持为0
? - 如果你不设置它为1,而让它维持为0,系统就会不断重复地判断它是不是可以恢复,每次进
OSTimeTick()
都发现它dly==0
,这就造成了无意义的重复处理。 - 而设置为1的效果是:系统会每隔一个Tick再次检查,既节省资源,又不耽误恢复。
- 最后顺着链表取出下一个TCB,然后退出临界区
代码执行流程
这个是书上给的OSTimeTick核心代码:
不同代码分析
- OSTimeTick()以调用由用户定义的时钟节拍hook函数 OSTimeTickHook()开始(L(1))。这个hook函数可以将时钟节拍函数 OSTimeTick()扩展。由于 OSTimeTick()中的其他代码工作量较大(在任务多的情况下),hook函数使用户可以定义对时间要求苛刻的工作(如读取传感器、更新PWM 寄存器等)。L(2)计算自系统上电以来的时钟节拍数,每次加1,这是个 unsigned int32 类型的变量,名为 OSTime。
- L(3)、L(4)是对等待时间链的操作,具体而言,需要从全局 OSTCBList的第一个TCB开始,遍历OS TCB链表,对每个任务TCB的 OSTCBDly成员进行“减1”操作。
- 若某个任务的时间延时项OSTCBDly减为0,首先看ptcb指向的任务是否是在等待一个事件的发生,如果是,说明等待超时,我们需要把它从等待队列中移出来,并说明是超时:
- 清除等待标志:①ptcb->OSTCBStat &= (INT8U) ~(INT8U)OS_STAT_PEND_ANY;
- 标记超时结果:②ptcb->OSTCBStatPend = OS_STAT_PEND_TO
- 否则标记正常完成:若任务未处于事件等待状态(单纯延时结束)ptcb->OSTCBStatPend = OS_STAT_PEND_OK;
状态宏定义 | 值 | 含义 |
OS_STAT_RDY | 0x00 | 任务就绪 |
OS_STAT_PEND_OK | 0x01 | 事件等待成功返回 |
OS_STAT_PEND_TO | 0x02 | 事件等待超时返回 |
OS_STAT_PEND_ANY | 0x06 (例如) | 掩码值,表示所有可能的事件等待状态(如等待信号量,消息队列等) |
- 如果该任务没有被挂起,说明延时时间到,将通过优先级位图法把它放回就绪队列中(L(8)、L(9))。
- OS_TRACE_TASK_READY(ptcb);
- 这是一条 跟踪(trace)宏,用于记录调试信息、系统事件、或者供可视化工具分析任务行为。

2.6.3 差分时间链
为了确保RTOS各项功能执行时间的确定性,需要对内核的各项数据结构进行优化。比如采用差分时间链、双向链表、优先级位图法等。
实时系统在运行过程中,需要进行各种与时间相关的操作。例如,μc/OS II通过OSTimeDly()或OSTimeDlyHMSM()实现对任务的延迟操作。若对多个任务进行延迟操作,就构成了一个延迟队列,内核将延迟任务的 TCB 依次挂载到延迟队列中。例如,用 OSTimeDly()将任务 A~任务D分别延迟3、5、10和 14个ticks:

每当时钟中断产生一个tick,则需要对时间等待链中的每一个结点进行“减1”操作:若时间等待链中的结点数量较多,则时钟中断服务程序的计算开销就比较大。

- 不同的时间链
- 绝对时间:第一条时间链是绝对时间,执行tick需要遍历链表
- 差分时间
- 记录与上一个任务相差的时间(也就是需要比上一个任务多延时几个tick)
- 为了减小计算开销,可采用差分时间等待链来描述延迟队列,如上图所示,队列中某个结点的值是相对于前一个结点的时间差。例如,任务 B需要延迟5个ticks,其前一个结点任务A 需要延迟3个ticks,意味着任务B在任务A延迟结束后,还需要延迟2个ticks,因此任务B所在结点的值为2,该值是相对于任务A的时间差。
- 该任务的绝对等待时间为,包括该结点值在内和前面结点的所有的值的和。所以每次-1,都相当于每个结点-1.
- 采用差分时间等待链后,每当时钟中断产生一个tick,只需对队列头部结点进行“减1”操作,当减到O时,就将其从等待链中取出,后续结点将成为新的头部结点并且被激活。在该过程中,等待链中其他结点的值保持不变,无须对每一个结点进行“减1”操作,这样可减小计算开销。
Q:如何在差分时间链中插入一个新的事件?如有新任务要进行延迟操作,需要在差分时间等待链中插入新的结点。例如,任务E,要延迟7个ticks,
A:如图所示,
①判断插入位置:这样只需在任务B和任务C之间插入任务E (3+2<7<3+2+5)
②修改该结点的值: 7-3-2=2
②修改后一个结点的值:(3+2+5)-7=3

- 看门狗定时器(Watchdog Timer)
- 硬件定时器,用于防止系统在长时间无响应或发生死锁时继续运行。
sysClkRateGet()
:获取系统时钟频率的函数调用,用于确定看门狗定时器的时间单位。DEADLINE_TIME
:看门狗定时器的超时时间,以系统时钟频率的单位表示。(FUNCTION)deadlineHandler
:一个函数指针,指向当看门狗定时器超时时调用的处理函数。这个函数通常用于执行恢复操作或系统重置。

- 系统时钟中断
- 这里主要介绍的就是OSTimeTick函数(Tick本质上也是一个中断,而OSTimeTick函数就是一个中断服务程序)。同样的,介绍一下这个函数的主要流程
- 这个函数主要任务就是遍历所有的已有任务并修改Dly的值
- 将符合唤醒条件的任务加入就绪队列(涉及到就绪队列优先级位图相关的操作),所以这个时候也会进行任务调度(需要注意的是这里并不是在Tick函数中进行调度的,还是在中断公共入口中进行调度的)

- 作者:🐟🐟
- 链接:https://www.imyuyu.top//article/EOS/Chapter2
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。