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.1 任务调度机制

💡
  • 内核的核心功能:调度
  • 内核调度的对象是任务
  • 一个策略:调度策略
  • 四个机制:基本调度机制、任务协调机制、内存管理机制、事件触发机制(中断)、时间触发机制(时间管理)

2.1.1 什么是任务

  • 任务是一个可执行的软件实体,它负责执行一些特定的操作。它包括一个指令序列、相关的数据以及计算机资源
  • 任务是一个无限循环的函数。它就像一个普通函数一样需要接收参数,但是它永远不会返回。任务的返回值类型永远是void。
    • 函数VS任务:
      • 函数:是静态代码块,由任务调用,不能独立运行,依赖于任务上下文。不参与 OS 的调度。
      • 任务:是动态运行的实体,通过 OSTaskCreate() 等 API 创建,由 OS 管理,具备独立上下文和栈空间。
 

2.1.2 任务的特点

  • Concurrence(Simultaneousness)(并发性)
    • 由于系统中多个任务并发执行,这些任务宏观上是同时运行的,微观上仍然是串行的。
  • Independence & Asynchrony (dependence & Synchrony)(异步独立性)
    • 任务间相互独立,不存在前驱和后继的关系。按照各自的运行速度运行。
    • 反之,任务间相互依赖,则任务具有同步性。
  • Dynamic(动态性)
    • 就跟操作系统课上讲的是一样的,就是进程是一个动态实体,而程序是一个静态实体。
    • 程序(Program)是静态实体:它是存储在磁盘或内存中的一组指令或代码,没有执行过程,仅仅是一段静态的数据。进程(Process)或任务(Task)是动态实体:它是程序在执行时的实例,有动态的运行状态,包含了执行中的指令、数据、栈、寄存器、资源等。
      • notion image
  • 任务具有动态性、异步独立性、并发性
 
 

2.1.3 任务、线程、进程

  • 进程(Process)资源分配的基本单位。独立运行的程序实例,具有独立的地址空间、独立资源、独立生命周期,包含多个线程。
  • 线程(Thread)操作系统调度的基本单位。运行在进程的地址空间内,是进程中的可独立调度执行单元,共享进程资源。
  • 任务(Task)RTOS 中的可独立调度的执行单元。在嵌入式 RTOS 中,任务 ≈ 线程,表示 OS 中可独立调度的运行实体。(每个任务/线程具有独立堆栈、上下文和运行状态。任务/线程的切换由内核进行调度,任务/线程的调度策略由操作系统负责管理。任务/线程都支持并发执行,并通过调度机制进行管理。)
 
进程和线程的区别:
只需要把任务当成一个线程就好了。而线程是没有内存隔离的,而进程是有内存隔离的。RTOS只实现了多线程。线程之间是共享地址的,能够访问相同的全局变量。当前线程的地址对于其他线程是可见的,如果修改了当前线程的数据,其他线程是可以知道并且能够访问的。进程之间不能相互访问对方的变量,数据变化不会影响其他进程,一般通过地址保护和虚拟地址来实现。
notion image
💡
上图中创建了一个进程,那么这个时候就会存在内存隔离,那么在进程test中的变量i就是主进程中i的一份拷贝,所以在主进程中执行i++并不会影响进程test中的i,所以在进程i中数据i的值应该为1。
notion image
💡
上图中创建了一个任务,而任务相当于是一个线程,他的地址空间是与其他在同一进程中的线程共享的,所以在主线程中执行i++将影响test线程地址空间中的i变量,所以test任务将输出2。
 

2.1.4 任务的执行体

相当于是任务的三要素中的指令序列了。并且在这里其实也涉及到了数据,因为程序和数据一般是相互依存的。需要注意的是,任务是一个无限循环,并且返回值永远为void
一个简单任务的执行体如下:
notion image
 
 
 

2.2 OS_TCB

2.2.1 TCB的成员

这里主要了解的是任务TCB结构体中一些重要的成员,并没有涉及到创建任务啥的。
  • OSTCBStkPtr:任务栈指针指向任务栈,是TCB的第一个成员,存放在地址最低处
    • 在进程中的每个线程都有自己独立的堆栈,该变量在任务的栈顶
    • 静态申请了大小为OSTASKSTATSTKSIZE的一片内存空间,调用 OSTaskCreateO创建一个任务。该任务用来统计任务的运行信息。其中第三个参数是传递栈顶的指针,OS_STKGROWTH表示堆栈增长的方向,若OS_STK_GROWTH为1,表明方向为从高到低,否则从低到高。如果堆栈增长方向为从高到低,那么栈顶指针就为&OSTaskStatStk[OS_TASK_STAT_STKSIZE-1]:反之,则&OSTaskStatStk[0]
    • 模拟栈需要事先被定义。
  • OSTCBNextOSTCBPrev:用于存已经初始化的TCB队列(即OSTCBList)中的下一个和前一个TCB(已初始化队列是一个双向链表,是为了便于进行快速的链表操作。这样只要找到一个TCB就能马上获取他的前后TCB并进行操作)
    • 代码分析在后面
    • notion image
  • 这里在额外说一下这些各种奇怪的组织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数
    • 也就是任务需要等待多少个时钟周期后才能继续执行。
    • 主动调用阻塞函数(如 OSTimeDly())让出 CPU,触发调度器切换任务。
  • OSTCBStat:代表任务的状态
  • 任务的状态转换图如下(这个可跟操作系统课上讲的进程五态图不一样):
    • notion image
    • 在这个图中需要记一些会引起状态转换的典型事件,也就是上图中标红的部分(还需要额外记一个任务的删除。并且注意任务删除之后任务只是进入冬眠状态
    • Dormant(睡眠):任务只以代码的形式存在,来交给操作系统管理,即没有分配任务 控制块和任务堆栈。
    • Ready(就绪):任务已经被创建,并被挂载到就绪队列中。
    • Running(运行):任务获得CPU的执行权。。
    • Waiting(等待): 任务因等待某个事件(如消息、信号量、时间延迟等)而暂停。需要等待一个事件的发生再运行,CPU使用权被剥夺。
    • ISR(中断服务状态):正在运行的任务一旦受到其他中断的干扰就会终止运行,转而执行 中断服务程序,这时处于中断服务状态。
  • OSTCBPrio:代表任务的优先级(在uC中任务优先级唯一,就相当于是任务的ID号了
    • μC/OSI1支持64个优先级,分别对应优先级0~63,其中0为最高优先级,63为最低优先 级,最低的两个优先级分配给空闲(idle)任务和统计(stat)任务。系统保留了4个最高优先级任务和4个最低优先级任务,用户可以使用的任务数有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的都是掩码就好了)
  • 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] 指示的优先级表的相应优先级。
    • notion image
下面结合一个实际例子来说明上面介绍的变量
notion image
如上图,这个时候创建了一个优先级为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。这个时候OSTCBBitYOSTCBBitX 的作用就展现出来了.因为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。
                • 生成掩码:
              • 精准清除
                💡
                |=:精准控制哪一位设置为1
                • 保留原值:任何位与0与运算。置1:任何位与1与运算
                • 掩码作用:标记需要操作的位
                  • 保留位:掩码中对应位为0,表示保留原值。
                  • 置1位: 掩码中对应位为1,表示精准置1。
                  • 生成掩码:
                • 精准清除
            • 找到最高优先级任务
              • notion image
            • 上图是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];
             
             
             

            2.3 任务创建

            创建任务时,首先须为其分配一个任务控制块 OS_TCB,然后对 OS_TCB 的各成员进行初始化。μC/OS II 提供了两种创建任务的方式,分别为 OSTaskCreate() OSTaskCreateExt()
            函数指针
            返回类型 (*指针名称)(参数类型列表);
             
             

            OSTaskCreate()

            函数原型
            • task:一个函数指针,指向任务所开始的函数,当任务第一次被调度运行时,就会从函数开始处运行。接收一个void *类型的参数。
            • p_arg:传递给任务函数的参数指针
            • ptos:任务堆栈的栈顶指针。
            • prio:任务的优先级。
             
             
            下面是部分代码详细分析:

            2.3.1 临界区代码保护

            • 进入临界区,检查是否在中断中创建任务,如果在则退出临界区,返回错误
              • 中断:是计算机系统中的一种异步事件,它允许外部设备或内部事件在正常程序运行时打断当前任务的执行,转而执行一个预先定义好的函数(即中断服务程序,ISR)。一旦中断处理完成,控制权会返回给原本正在执行的任务。
                • 基本流程:触发中断→保存上下文→执行中断服务例程(ISR)→恢复上下文
                • 嵌套中断:如果系统允许多个中断嵌套,即当前处理中断后,又有新的中断发生,可以打断当前中断并处理中断。
              • 临界段代码:是指一段代码,在执行时需要独占对共享资源(如全局变量、硬件设备等)的访问。
                • 多个任务可能会同时访问这些共享资源,如果没有同步机制,可能导致数据不一致、竞态条件或其他意外错误。因此,临界区需要加锁保护,以防止中断或其他任务同时访问。
                • 进出临界区:
                  • 进入临界区:通过禁用中断来防止中断发生,保证当前任务的操作不被打断,从而确保临界区内的操作能够原子性地完成(即不受干扰地一次性完成)。
                  • 退出临界区:当临界区操作完成后,恢复中断,使得系统能够响应新的中断请求。
              • OSIntNesting > 0OSIntNesting 是一个全局变量,表示当前中断的嵌套层数。操作系统会追踪中断的嵌套层级。初始时,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++);
                  • 压栈结果如下图:
                    • notion image
                      这个TaskStartAStk应该就是为任务开出来的栈空间,是一个数组,大小为256个单元,每个单元是OS_STK类型。由于之前这个任务从来没有使用过栈,所以栈顶指针应该是&TaskStartAStk[255](也就是栈空间中最后一个单元的地址)。在前面声明堆栈增长方式的时候有说,如果堆栈增长方向为从高到低,那么栈顶指针应为&OSTaskStatStk[OS_TASK_STAT_STK_SIZE-1]。
                      notion image
                      (0x20001000-15*4)
                    • 此时为模拟任务切换时,当前任务的运行环境时如何保存在堆栈中的。实际发生的任务切换,需要根据16个寄存器的值依次保存在stk中。向前见2.4.3任务切换。
                 
                 

                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),则需要设置 OS_TASK_STAT_EN为1,此时系统任务有2个(空闲任务IDLE和统计任务STAT)。否则,系统任务只有1个(空闲任务)。
                • 定义了最多可建立的任务数为 20。
                • OS_MemClr 用于清除 TCB 表和优先级表,确保所有条目初始化为零。
                  • 内存清零函数,传入清除项目的首地址和大小。
                • ptcb1ptcb2 是指向 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为最低优先级。
                notion image
                上图为空闲OS_TCB链表OSTCBFreeList,这个时候应该回去看一下那段最初申请一个空闲的TCB块的代码了,就明白了。
                notion image
                这个是初始化后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]
                          • notion image
                          • 返回的是传入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()具体代码实现:
                         
                         

                        2.4.3 任务上下文切换

                        实际上就是将原任务的CPU现场保护在原任务的栈中,然后将新任务的CPU执行现场从新任务的栈中恢复出来(这里就可以讨论一下创建任务时进行的模拟压栈的作用了,实际上就是为了能够在任务上下文切换的时候进行统一处理)
                        💡
                        上下文切换的概念:当内核决定要执行一个不同的任务时,操作系统就会把当前CPU的执行现场(CPU寄存器)保存在当前任务的栈上。然后新任务的上下文将从新任务的栈中恢复然后继续执行新任务,这个过程被称为上下文切换。
                        • 如果内核刚刚启动(即启动的时候没有正在执行的任务)。调用OSStartHighRdy()即可切换到优先级最高的任务。
                        • 否则进行上下文切换(context switching)
                        • 两种切换:
                          • 任务主动切换:OSCtxSw()
                          • 中断导致的任务被动切换:OSIntCtxSw()
                        • 在os_cpu.h中 #define OS_TASK_SW OSCtxSw()

                        OSCtxSw()

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

                          (1)保存当前任务的上下文

                          • 需要按照模拟压栈(2.3.2初始化任务栈)的顺序保存,也就是先保存PC,然后保存剩余的除了SP寄存器的通用寄存器,然后保存状态寄存器。
                          notion image
                          • 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中)
                          notion image
                          • 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)保存当前任务的SP到当前任务的TCB中

                          • 需要注意的点是
                            • =OSTCBCur是一种伪指令,它会将OSTCBCur变量的地址加载到寄存器R0中。
                              • =(变量名):用来将一个变量的地址放入寄存器中,而不是直接加载数据。
                            • OSTCBCur 一个全局变量,存放是当前正在执行的任务的任务控制块(TCB)的地址。
                            • notion image
                              notion image
                            • 理一下有点混乱:第一步将0x9000存入R0.第二步,将0x9000的内容,0x7000存入R0。第三步:将SP的值存入R0存的地址(0x7000)中去。即存入OSTCBStkPtr字段。
                              • notion image
                          钩子函数,供开发人员扩展,如增加代码以通奸任务的切换次数。
                           
                           

                          (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)
                           
                          3.3将高优先级任务的堆栈地址传给处理器的SP(R13)
                          • OSTCBHighRdy即当前任务了。
                          • 就绪队列中高优先级任务的TCB中堆栈指针OSTCBStkPtr送入处理器SPSP=OSTCBHighRdy→OSTCBStkPtr此时,SP 指向高优先级任务堆栈中CPSR所在的内存地址。
                          • ARM 规定,SP始终指向堆栈顶位置
                            • STM指令把寄存器列表中索引最小的寄存器存储在最低地址,因此当任务现场信息保存完时,SP是指向CPSR的。
                            • 不同任务有各自的堆栈顶指针,每当任务切换时,被切换的任务的栈顶指针都保存在其OS_TCB的OSTCBStkPtr成员中。
                           

                          (4)恢复新任务的执行现场到CPU中

                          notion image
                          LDMFD:递减满栈的POP。弹出一个值到R0并更新SP。这里弹出的是T2的CPSR。
                          MSR SPSR_cxsf, R0需要先将状态寄存器恢复到备份寄存器中。
                          • 只需要注意这里需要先将状态寄存器恢复到备份寄存器中(因为状态寄存器在保存现场时是最后一个入栈的),并且在恢复其他的寄存器时需要在LDMFD指令后面加上^以便在现场恢复的时候顺便将SPSR寄存器中的值拷贝到CPSR中。
                          • POP顺序:CPSR,R0,R1…,LR,PC
                           
                           

                          为什么要保存两次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不相等。
                          notion image
                          造成这种情况的原因是ARM有7种工作模式:用户(user)模式、快速中断(FIQ)模式、外部中断(IRQ)模式、管理(supervisor)模式、数据访问中止(abort)模式、系统(system)模式、未定义(undefined)模式。具体而言,中断发生前,处理器工作在系统/用户模式,中断发生后,处理器自动切换到外部中断模式,而不同模式下寄存器的分配和使用是不同的,如上图所示。
                           
                           

                          调度点

                          notion image
                          这里就直接把调度点都列举出来(实际上就是操作系统课上讲的调度点,对于这样可抢占的操作系统,就是当就绪队列发生变化或者当前任务不再占据CPU的时候就需要进行调度)
                          • 操作系统启动
                          • 中断退出可能改变了就绪队列。这里的中断尤其是时钟中断)
                          • 新任务创建(改变了就绪队列)
                          • 当前任务进入等待状态(当前任务不再占据CPU)
                          • 当前任务进入冬眠状态(当前任务不再占据CPU)
                          • 当前任务执行结束(当前任务不再占据CPU)
                           

                          OSIntCtxSw()

                          在OSCtxSw()中:
                          1. 保护现场:保护寄存器:R0-R12 、 LR 、PC(这里主动切换的话和LR相同)还有CPSR
                          2.将保护好现场,压好栈的最新sp放入OSTCBCur的OSTCBstkPtr字段(也就是第一个)
                          在OSIntCtxSw()中:第一个步骤以及第二个步骤是由统一的中断服务处理程序完成的,如下图:
                          notion image
                          补充叙述一下这里的保存上下文的思路:
                          准则是:我们需要保存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里面,完成上下文保存。
                          所有任务的上下文在栈的布局如下:
                          notion image
                          统一的中断处理程序里面压栈后,执行第二个步骤将OSTCBCur→OSTCBStkPtr=sp的操作和非中断的OSCtxSw有不同,在于这里判定了是否中断嵌套,如果中断嵌套了的话,只是仍然将被中断的上下文保存到原始的任务的堆栈里面,但是这个上下文实际上并不是任务的上下文,所以不用更新到OSTCBCur→OSTCBstkPtr里面
                          ps:这里可以注意一个恢复现场的细节:
                          LDMFD SP!,{R0}
                          MSR SPSR_cxsf,R0
                          在恢复cpsr的时候,是把内容填充如spsr_cxsf里面,由指令
                          LDMFD SP!,{R0-R12,LR,PC}^
                          这个指令包含PC并且有^,所以会恢复spsr内容进入cpsr,这样同时实现寄存器和模式的切换
                          我结合OSIntExit()来说:(ps:OSIntExit的实现代码很大部分和sched加上sched_new一样,原因是它的大部分功能是执行任务切换(少部分是更新OSIntNesting),但它并没有直接调用sched,因为它需要调用OSIntCtxSw而不是OSCtxSw
                          先判定是否OS跑起来,然后修改OSIntNesting(-1)(注意enter critical需要),然后判定是否有中断嵌套或者锁嵌套,之后操作和sched_new()一样,查找到最高优先级的就绪任务:OSTCBHighRdy,不过需要像sched() 一样,判定OSTCBHighRdy是否和OSTCBCur一样,不一样的话需要调用中断的上下文切换(OSIntCtxSw
                          notion image
                          这就有必要开启我们下一节:中断!
                           
                           

                          中断

                          主要涉及到事件触发的机制以及时间触发的机制
                          中断被定义为导致程序正常运行流程发生改变的事件

                          中断的分类

                          中断被分为外部中断(硬中断)、自陷、异常
                          • 硬中断(外部中断)是由于CPU外部原因而改变程序运行流程的过程
                          • 自陷(内部中断)表示通过处理器软件指令(程序指令、系统调用),可预期地使CPU正在执行的任务流程发生改变。
                            • 调试使C语言一步一步执行:每段代码之后编译器会插入一个软中断SWI
                          • 异常:CPU自动产生的自陷,以处理特定的异常事件。无法预见。
                           

                          2440裸板中断

                          异常向量表(2440)

                          下图是ARM9 2440的一级中断向量表
                          notion image
                          如图所示是异常向量表在内存中的分布。异常向量表全是汇编语言的跳转指令,从内存的零地址开始连续存储在内存中。代码实现如下图:
                          notion image
                          当发生对应的异常的时候,PC将通过硬件机制跳转到相应的异常在异常向量表中的地址开始执行,再通过跳转指令跳到一个异常处理代码的起始地址。
                          • 标识符Reset/Undef/SWI/PreAbort/DataAbort/IFQ和FIQ都代表了一个地址。
                          • eg:PC正常执行(eg0x3080)。当中断发生,PC跳转到0x18处执行b IRQ.IRQ代表的地址0X3000为中断服务程序的地址。
                           

                          Q1 Where to return

                          中断返回

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

                            Q2 How to distinguish different INTs?

                            中断注册

                            STM32中只有一个层级的中断,而2440中有两层中断——第一层就是上面的RESET....,第二层却存储在RAM中。
                            • 下图是第一层中断,也称为公共入口;对于IRQ,经过第一层中断后就要被导向到第二层中断来distinguish different INTs
                              • 第一层中断向量表是所有中断和异常的入口,他由数条LDR指令组成。不同的终端、异常触发时会执行到不同的LDR指令。
                              • notion image
                            • 触发IRQ中断时,便会进入第一层中断向量表,随后跳转到IRQ中断服务程序。在IRQ服务程序中,再跳转到第二层中断向量表。下图位2440第一层IRQ展开的第二层中断向量表。(在公共入口中将中断分发)
                              • notion image
                            • 在三星2440中,有一个类似于STM32中NVIC的终端管理器,它能集中管理外部中断源,并通过INTOFFSET寄存器来告诉CPU外边出发了哪个中断。于是,我们在IRQ中断服务程序中书写下面的代码来distinguish different INTsSolution:
                            • 中断分发代码如下:
                              • notion image
                              • 存在一个rINTOFFSET寄存器。发生中断时,该寄存器会为中断源分配一个整数,这个整数与中断源唯一对应。在发生中断的时候读取该寄存器的值,根据不同的值区分不同的中断,从而执行不同的中断服务程序。
                                • notion image
                              • HandleEINT0代表的是一个内存地址,其内容是对应中断的中断服务函数入口。如上图所示。每种中断服务函数入口的地址为32位,占据了4个字节,所以我们在计算每一种中断源对应的服务函数地址的时候会把INTOFFSET左移两位(乘以4)后再与起始地址HandleEINT0相加得到具体的中断服务函数入口地址。
                                • 💡
                                  总结:通过查找INTOFFSET寄存器和HANDLEINT地址,通过计算:INTOFFSET*4+HANDLEINT得到对应的中断服务程序地址存放的位置
                            • 硬件自动根据中断源,确定该执行哪个中断(32中是由NVIC完成)

                            Q3 How to save the information of interrupted programs?

                            保存T1现场
                            • 压栈R0~R12
                            • 压栈R14(LR)
                            • 压栈CPSR
                              • 通过SPSR&R0

                            裸板中断的主要流程

                            • 硬件跳转至公共入口
                              • 硬件将PC寄存器保存在LR寄存器中,并且跳转至一级中断服务程序处(对于除一般中断以外的中断,就是ISR;对一般中断而言,就是一个公共入口,下面就以一般中断为例)
                            • 保存中断现场
                              • 在中断服务程序入口中进行中断现场保存(不保存LR)。保存中断现场(是保存在中断栈中的,保存的内容跟任务切换时保存的内容差不多,但是并没有完整保存所有,因为这里的LR是不需要保存的)
                            • 获取中断服务程序的入口地址,然后跳转至中断服务程序
                            • 在中断服务程序中需要清除中断标志位(源挂起、目的挂起)
                            • 返回公共入口程序中恢复现场
                            • 中断返回
                            notion image
                            这张图的细细分析如下:
                            • F1中PC为0x6000,遇到中断…
                            notion image

                            2440里面中断进入的时候硬件会进行的操作

                            • 保存被中断程序当前的pc到LR_irq中(也就是中断返回后应该执行的下一条指令位置,所以中断里面对他-4操作再保存到任务的栈的pc位置)
                            • 将程序当前状态寄存器CPSR的值放入相应模式的SPSR(也即是irq的spsr)(用于中断返回时的恢复)
                            • 切换处理器模式为irq模式,也就是将CPSR的模式位设为相应的中断模式,并禁用相应模式的中断。如果是快中断模式,则禁用所有中断。
                            • 通过异常向量表找到irq应该进入的处理程序地址,放入pc中实现跳转
                             
                             
                             
                             

                            Q4 [ OS ] How to process the IRQ if μc/OS II is running ?

                            notion image
                            总的流程: 保存IRQ模式下的特殊寄存器-退出中断模式-保存现场-返回中断模式中断-区分中断源-中断服务程序-退出中断模式-恢复现场
                            1. 保存R0-R2寄存器的值压入到当前的IRQ堆栈,然后将此sp放入R0
                            1. irq的sp复原,将r1保存pc(irq_lr-4),r2保存中断前cpsr(irq的spsr)
                            1. 退出中断模式,改为任务的svc模式,转到任务的堆栈
                            1. 保存现场,pc(R1)入栈,然后是LR(R14) R12-R0
                            1. 然后从r0指向的irq堆栈中出栈到R3-R5(对应保存的R0-R2),然后连着R2(保存的cpsr)入栈(svc任务栈)
                            1. 更新中断嵌套OSIntNesting计数,然后判断是否在中断嵌套里面,如果没有嵌套则:
                            1. 将sp保存入当前任务的TCB记录:OSTCBCur→OSTCBStkPtr里面
                            1. 恢复cpsr为irq模式,也就是进入irq模式,取得INTOFFSET值来区分中断源
                            1. 计算得出IRQ入口,保存到PC,执行ISR。
                            1. ISR完成后,切换到svc模式,先调用OSIntExit进行可能的任务重调度以及更新OSIntNesting
                            1. 最后是恢复现场(完成任务的恢复执行)
                             

                            和裸板中断对比

                            裸板没有操作系统,没有任务一说,栈就在统一的区域,进入中断,同样要保护现场,就把所有寄存器: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 的调度函数
                            notion image
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                             
                            Notion使用小指南喵~嵌入式操作系统
                            Loading...
                            🐟🐟
                            🐟🐟
                            在坚冰还盖着北海的时候,我看到了怒放的梅花
                            最新发布
                            四轴飞行器
                            2025-4-16
                            Notion使用小指南喵~
                            2025-4-16
                            Hi3861 & 服创
                            2025-4-16
                            Git
                            2025-4-16
                            嵌入式操作系统
                            2025-3-28
                            C2驾驶证考试
                            2025-3-13
                            公告
                            🎉NotionNext 3.15已上线🎉
                            -- 感谢您的支持 ---
                            👏欢迎更新体验👏