快速回顾
在之前的文章中通过多个模型来建立对多任务的概念,本文将会从源码角度解析模型的具体实现。μC/OS-Ⅱ中的多任务https://blog.youkuaiyun.com/sherlock_cp/article/details/130077923
任务控制块
在μC/OS-Ⅱ中,任务控制块类型为OS_TCB,包含了大量的成员:
//ucos_ii.h
typedef struct os_tcb {
OS_STK *OSTCBStkPtr; //任务堆栈当前位置指针
#if OS_TASK_CREATE_EXT_EN > 0u
void *OSTCBExtPtr; //任务扩展块指针
OS_STK *OSTCBStkBottom; //任务堆栈栈底
INT32U OSTCBStkSize; //任务堆栈长度
INT16U OSTCBOpt; //选项字节
INT16U OSTCBId; //任务(0..65535)
#endif
struct os_tcb *OSTCBNext; //指向下一个任务控制块,用于形成双向链表结构
struct os_tcb *OSTCBPrev; //指向上一个任务控制块,用于形成双向链表结构
#if (OS_EVENT_EN)
OS_EVENT *OSTCBEventPtr; /* 任务等待事件指针,如发生等待,会指向对应事件 */
#endif
#if (OS_EVENT_EN) && (OS_EVENT_MULTI_EN > 0u)
OS_EVENT **OSTCBEventMultiPtr; /* 任务等待多重事件的指针 */
#endif
#if ((OS_Q_EN > 0u) && (OS_MAX_QS > 0u)) || (OS_MBOX_EN > 0u)
/* 通常发送的消息或邮件会存放到对应的消息队列或邮箱中,但若遇到堵塞,则这些信息会到这里 */
void *OSTCBMsg;
#endif
#if (OS_FLAG_EN > 0u) && (OS_MAX_FLAGS > 0u)
#if OS_TASK_DEL_EN > 0u
OS_FLAG_NODE *OSTCBFlagNode; /* 事件标志节点 */
#endif
OS_FLAGS OSTCBFlagsRdy; /* 当前存在的事件标志位 */
#endif
INT32U OSTCBDly; //任务等待执行延时
INT8U OSTCBStat; //任务状态(就绪、pending)
INT8U OSTCBStatPend; //任务pending状态
INT8U OSTCBPrio; //任务优先级
INT8U OSTCBX; /* 任务的组内优先级 */
INT8U OSTCBY; /* 任务的优先级组 */
OS_PRIO OSTCBBitX; /* 任务的组内优先级掩码,用于快速映射和寻找 */
OS_PRIO OSTCBBitY; /* 任务优先级组掩码,用于快速映射和寻找 */
#if OS_TASK_DEL_EN > 0u
INT8U OSTCBDelReq; /* 任务删除请求标志 */
#endif
#if OS_TASK_PROFILE_EN > 0u
INT32U OSTCBCtxSwCtr; /* Number of time the task was switched in */
INT32U OSTCBCyclesTot; /* Total number of clock cycles the task has been running */
INT32U OSTCBCyclesStart; /* Snapshot of cycle counter at start of task resumption */
OS_STK *OSTCBStkBase; /* Pointer to the beginning of the task stack */
INT32U OSTCBStkUsed; /* Number of bytes used from the stack */
#endif
#if OS_TASK_NAME_EN > 0u
INT8U *OSTCBTaskName; //任务名称
#endif
#if OS_TASK_REG_TBL_SIZE > 0u //任务寄存器,可以用于存储任务执行过程中的数据
INT32U OSTCBRegTbl[OS_TASK_REG_TBL_SIZE];
#endif
} OS_TCB;
多任务系统模型
在μC/OS-Ⅱ系统创建之初,下图所展示的多任务模型就已经建立(具体逻辑回顾前文),只不过其中的任务控制块都是空白的。OSTCBFreeList链表可以理解为系统剩余的空白任务控制块,而OSTCBList链表会在每次创建任务时,从OSTCBFreeList截取一个任务控制块并填充信息装载到自身。
这里涉及到的多个重要的全局变量,都定义在ucos_ii.h文件中:
//ucos_ii.h
OS_EXT OS_TCB *OSTCBCur; //当前正在运行任务的任务控制块指针
OS_EXT OS_TCB *OSTCBFreeList; //空白任务控制块链表
OS_EXT OS_TCB *OSTCBHighRdy; //就绪的最高优先级任务控制块指针
OS_EXT OS_TCB *OSTCBList; //已注册任务的任务控制块链表
OS_EXT OS_TCB *OSTCBPrioTbl[OS_LOWEST_PRIO + 1u]; //任务优先级数组,可以快速定位对应任务控制块,+1是因为优先级0也是一个任务
OS_EXT OS_TCB OSTCBTbl[OS_MAX_TASKS + OS_N_SYS_TASKS]; //所有任务(空白+已注册)的任务控制块数组,包含的是任务控制块实体而非指针
包括五个指针和一个数组,其中数组OSTCBTbl[]是真正包含所有任务控制块的数据实体,其它指针都是对这些实体的引用。
其中涉及的配置宏定义:
OS_N_SYS_TASKS:系统任务的数量(最多2个,空闲和统计任务),其中空闲任务必须存在,统计任务按需开启;
OS_LOWEST_PRIO:任务的最低优先级,也即空闲任务的优先级;
OS_MAX_TASKS:最大任务数量,注意该数量不包括系统任务数;
初始化多任务系统
在OSInit()函数运行时,里面有一个OS_InitTCBList()函数专门用来初始化这些任务控制块指针和数组:
//os_core.c
static void OS_InitTCBList (void)
{
INT8U ix;
INT8U ix_next;
OS_TCB *ptcb1;
OS_TCB *ptcb2;
OS_MemClr((INT8U *)&OSTCBTbl[0], sizeof(OSTCBTbl)); //清空所有任务控制块内容
OS_MemClr((INT8U *)&OSTCBPrioTbl[0], sizeof(OSTCBPrioTbl)); //清空任务优先级数组内容
for (ix = 0u; ix < (OS_MAX_TASKS + OS_N_SYS_TASKS - 1u); ix++) {
ix_next = ix + 1u;
ptcb1 = &OSTCBTbl[ix];
ptcb2 = &OSTCBTbl[ix_next];
ptcb1->OSTCBNext = ptcb2; //下一个任务控制块指针指向数组下一个元素,即&OSTCBTbl[ix_next]
#if OS_TASK_NAME_EN > 0u //是否开启任务名称,如开启,则初始化为"?"
ptcb1->OSTCBTaskName = (INT8U *)(void *)"?";
#endif
}
ptcb1 = &OSTCBTbl[ix];
ptcb1->OSTCBNext = (OS_TCB *)0; //数组最后一个任务控制块的下一个指针指向空(0)
#if OS_TASK_NAME_EN > 0u
ptcb1->OSTCBTaskName = (INT8U *)(void *)"?";
#endif
OSTCBList = (OS_TCB *)0; //将任务控制块链表头指向空(0)
OSTCBFreeList = &OSTCBTbl[0]; //将剩余空白任务控制块链表头指向数组首元素,即前文创建好的链表头
}
在该函数里,会将OSTCBTbl[]里的所有空白任务控制块按顺序用成员内部指针OSTCBNext串联起来形成一张链表,最后将链表头,也即&OSTCBTbl[0]赋予OSTCBFreeList,形成一张空白链表。也就是说,OSTCBFreeList空白链表的成员实际上在内存空间就是连续的数组。任务控制块链表OSTCBList由于系统创建之初并无任务,只能初始化为0。
除此之外,OSInit()函数还对空闲任务(必须存在)和统计任务(如有需要)进行了创建和初始化。具体函数为OS_InitTaskIdle()和OS_InitTaskStat()(和常规任务创建一样,此处不展示)。
创建任务
在初始化完毕后,用户就可以根据需求创建任务了。任务创建函数有两个版本,老版本的OSTaskCreate()函数仅支持传入四个参数,基本上被弃用了。另一个更新版本的OSTaskCreateExt()函数支持9个传入参数,具有更精细的控制粒度("Ext"即"extend":扩展版本)。下面着重介绍一下后者:
//os_task.c
INT8U OSTaskCreateExt (void (*task)(void *p_arg), //任务函数代码
void *p_arg, //任务初次运行时传入的参数,是一个指针
OS_STK *ptos, //任务堆栈栈顶,任务堆栈方向与OS_STK_GROWTH相关
INT8U prio, //任务优先级,数字越小优先级越高
INT16U id, //任务ID(0-65535),通常没用,但要确保唯一,可以取优先级
OS_STK *pbos, //任务堆栈栈底
INT32U stk_size, //任务堆栈大小
void *pext, //TCB扩展块,通常不用
INT16U opt) //选项字(16bit),系统规定的有三种
{
OS_STK *psp;
INT8U err;
#if OS_CRITICAL_METHOD == 3u //临界区的进入方法等于3时,需要用cpu_sr保存局部变量
OS_CPU_SR cpu_sr = 0u;
#endif
#if OS_ARG_CHK_EN > 0u //使能参数检查
if (prio > OS_LOWEST_PRIO) { //确保优先级大于配置的最低优先级
return (OS_ERR_PRIO_INVALID);
}
#endif
OS_ENTER_CRITICAL(); //进入临界区,禁止任务切换
if (OSIntNesting > 0u) { //查看中断嵌套,检查是否在中断内,禁止中断内创建任务
OS_EXIT_CRITICAL();
return (OS_ERR_TASK_CREATE_ISR);
}
if (OSTCBPrioTbl[prio] == (OS_TCB *)0) { //检查传入的优先级对应的任务控制块是否为空(0)
OSTCBPrioTbl[prio] = OS_TCB_RESERVED; //先占用掉该优先级,然后退出临界区防止待在临界区太久,后续再填装任务控制块
OS_EXIT_CRITICAL();
#if (OS_TASK_STAT_STK_CHK_EN > 0u) //检查统计任务是否开启
OS_TaskStkClr(pbos, stk_size, opt); //如开启了统计任务,则清空任务堆栈
#endif
psp = OSTaskStkInit(task, p_arg, ptos, opt); //用传入参数初始化任务堆栈,从栈顶开始填装,并返回填装后所处的位置指针
err = OS_TCBInit(prio, psp, pbos, id, stk_size, pext, opt);
if (err == OS_ERR_NONE) {
if (OSRunning == OS_TRUE) { /* Find HPT if multitasking has started */
OS_Sched(); //任务切换
}
} else {
//创建出错,要进入临界区,将对应优先级的任务控制块清空,再退出临界区
OS_ENTER_CRITICAL();
OSTCBPrioTbl[prio] = (OS_TCB *)0;
OS_EXIT_CRITICAL();
}
return (err);
}
OS_EXIT_CRITICAL();
return (OS_ERR_PRIO_EXIST);
}
其中任务堆栈的初始化函数OSTaskStkInit(),任务堆栈的填装方式与CPU寄存器相关,同时还填装了用户定义的任务函数入口,以及初次运行传入的参数。
//os_cpu_c.c
OS_STK *OSTaskStkInit (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT16U opt)
{
OS_STK *stk; //局部堆栈变量,OS_STK<=>INT32U,这个长度和单片机地址位数有关
(void)opt; //在这里选项字节并未用到,是为了防止编译警告
stk = ptos; //将传入的任务堆栈栈顶指针赋值到局部变量
#if (__FPU_PRESENT==1)&&(__FPU_USED==1) //检查是否有FPU,该配置会影响任务堆栈的结构
*(--stk) = (INT32U)0x00000000L; //No Name Register
*(--stk) = (INT32U)0x00001000L; //FPSCR
*(--stk) = (INT32U)0x00000015L; //s15
*(--stk) = (INT32U)0x00000014L; //s14
*(--stk) = (INT32U)0x00000013L; //s13
*(--stk) = (INT32U)0x00000012L; //s12
*(--stk) = (INT32U)0x00000011L; //s11
*(--stk) = (INT32U)0x00000010L; //s10
*(--stk) = (INT32U)0x00000009L; //s9
*(--stk) = (INT32U)0x00000008L; //s8
*(--stk) = (INT32U)0x00000007L; //s7
*(--stk) = (INT32U)0x00000006L; //s6
*(--stk) = (INT32U)0x00000005L; //s5
*(--stk) = (INT32U)0x00000004L; //s4
*(--stk) = (INT32U)0x00000003L; //s3
*(--stk) = (INT32U)0x00000002L; //s2
*(--stk) = (INT32U)0x00000001L; //s1
*(--stk) = (INT32U)0x00000000L; //s0
#endif
/* Registers stacked as if auto-saved on exception */
*(stk) = (INT32U)0x01000000L; /* xPSR */
*(--stk) = (INT32U)task; //任务函数入口(R15 PC)
*(--stk) = (INT32U)OS_TaskReturn; /*意外退出时,走这个函数,仅系统调用,用户不可调用(R14 LR) */
*(--stk) = (INT32U)0x12121212L; /* R12 */
*(--stk) = (INT32U)0x03030303L; /* R3 */
*(--stk) = (INT32U)0x02020202L; /* R2 */
*(--stk) = (INT32U)0x01010101L; /* R1 */
*(--stk) = (INT32U)p_arg; /* R0 : argument */
#if (__FPU_PRESENT==1)&&(__FPU_USED==1)
*(--stk) = (INT32U)0x00000031L; //s31
*(--stk) = (INT32U)0x00000030L; //s30
*(--stk) = (INT32U)0x00000029L; //s29
*(--stk) = (INT32U)0x00000028L; //s28
*(--stk) = (INT32U)0x00000027L; //s27
*(--stk) = (INT32U)0x00000026L; //s26
*(--stk) = (INT32U)0x00000025L; //s25
*(--stk) = (INT32U)0x00000024L; //s24
*(--stk) = (INT32U)0x00000023L; //s23
*(--stk) = (INT32U)0x00000022L; //s22
*(--stk) = (INT32U)0x00000021L; //s21
*(--stk) = (INT32U)0x00000020L; //s20
*(--stk) = (INT32U)0x00000019L; //s19
*(--stk) = (INT32U)0x00000018L; //s18
*(--stk) = (INT32U)0x00000017L; //s17
*(--stk) = (INT32U)0x00000016L; //s16
#endif
/* Remaining registers saved on process stack */
*(--stk) = (INT32U)0x11111111L; /* R11 */
*(--stk) = (INT32U)0x10101010L; /* R10 */
*(--stk) = (INT32U)0x09090909L; /* R9 */
*(--stk) = (INT32U)0x08080808L; /* R8 */
*(--stk) = (INT32U)0x07070707L; /* R7 */
*(--stk) = (INT32U)0x06060606L; /* R6 */
*(--stk) = (INT32U)0x05050505L; /* R5 */
*(--stk) = (INT32U)0x04040404L; /* R4 */
return (stk); //返回任务堆栈初始完毕后的指针位置,一定要栈底以上
}
另一个就是任务控制块的初始化函数OS_TCBInit(),该函数较长,具体看注释。
//os_core.c
INT8U OS_TCBInit (INT8U prio,
OS_STK *ptos,
OS_STK *pbos,
INT16U id,
INT32U stk_size,
void *pext,
INT16U opt)
{
OS_TCB *ptcb;
#if OS_CRITICAL_METHOD == 3u //临界区的进入方法等于3时,需要用cpu_sr保存局部变量
OS_CPU_SR cpu_sr = 0u;
#endif
#if OS_TASK_REG_TBL_SIZE > 0u
INT8U i;
#endif
OS_ENTER_CRITICAL();
ptcb = OSTCBFreeList; //从OSTCBFreeList取一个TCB
if (ptcb != (OS_TCB *)0) { //确保该TCB不是空白的,否则代表任务数量已满,无法创建
OSTCBFreeList = ptcb->OSTCBNext; //OSTCBFreeList指向下一个TCB,该步骤会卸载头一个空白TCB(用来创建新任务了)
OS_EXIT_CRITICAL();
ptcb->OSTCBStkPtr = ptos; //将任务堆栈指针指向要创建的任务堆栈栈顶
ptcb->OSTCBPrio = prio; //任务优先级
ptcb->OSTCBStat = OS_STAT_RDY; //任务状态:就绪
ptcb->OSTCBStatPend = OS_STAT_PEND_OK; //pending状态:完成
ptcb->OSTCBDly = 0u; //任务等待执行延时:0
#if OS_TASK_CREATE_EXT_EN > 0u //是否使能OSTaskCreateExt(),并将这些额外的参数填装
ptcb->OSTCBExtPtr = pext;
ptcb->OSTCBStkSize = stk_size;
ptcb->OSTCBStkBottom = pbos;
ptcb->OSTCBOpt = opt;
ptcb->OSTCBId = id;
#else
pext = pext; /* Prevent compiler warning if not used */
stk_size = stk_size;
pbos = pbos;
opt = opt;
id = id;
#endif
#if OS_TASK_DEL_EN > 0u //是否使能OSTaskDel()
ptcb->OSTCBDelReq = OS_ERR_NONE; //非0表示要删除任务了
#endif
//计算优先级组(Y)和组内下标(X)
#if OS_LOWEST_PRIO <= 63u //最低优先级小于63,则用六位来表示,分别为3位优先级组和3位组内优先级数
ptcb->OSTCBY = (INT8U)(prio >> 3u);
ptcb->OSTCBX = (INT8U)(prio & 0x07u);
#else //最低优先级大于63,则用八位来表示,分别位4位优先级组和4位组内优先级数
ptcb->OSTCBY = (INT8U)((INT8U)(prio >> 4u) & 0xFFu);
ptcb->OSTCBX = (INT8U) (prio & 0x0Fu);
#endif
//计算掩码,是为了快速定位就绪任务。如果最低优先级小于64,OS_PRIO为8位,否则为16位
ptcb->OSTCBBitY = (OS_PRIO)(1uL << ptcb->OSTCBY);
ptcb->OSTCBBitX = (OS_PRIO)(1uL << ptcb->OSTCBX);
#if (OS_EVENT_EN)
ptcb->OSTCBEventPtr = (OS_EVENT *)0; /* Task is not pending on an event */
#if (OS_EVENT_MULTI_EN > 0u)
ptcb->OSTCBEventMultiPtr = (OS_EVENT **)0; /* Task is not pending on any events */
#endif
#endif
#if (OS_FLAG_EN > 0u) && (OS_MAX_FLAGS > 0u) && (OS_TASK_DEL_EN > 0u)
ptcb->OSTCBFlagNode = (OS_FLAG_NODE *)0; /* Task is not pending on an event flag */
#endif
#if (OS_MBOX_EN > 0u) || ((OS_Q_EN > 0u) && (OS_MAX_QS > 0u))
ptcb->OSTCBMsg = (void *)0; /* No message received */
#endif
#if OS_TASK_PROFILE_EN > 0u
ptcb->OSTCBCtxSwCtr = 0uL; /* Initialize profiling variables */
ptcb->OSTCBCyclesStart = 0uL;
ptcb->OSTCBCyclesTot = 0uL;
ptcb->OSTCBStkBase = (OS_STK *)0;
ptcb->OSTCBStkUsed = 0uL;
#endif
#if OS_TASK_NAME_EN > 0u
ptcb->OSTCBTaskName = (INT8U *)(void *)"?";
#endif
#if OS_TASK_REG_TBL_SIZE > 0u /* Initialize the task variables */
for (i = 0u; i < OS_TASK_REG_TBL_SIZE; i++) {
ptcb->OSTCBRegTbl[i] = 0u;
}
#endif
OSTCBInitHook(ptcb); //任务控制块初始化Hook(用户回调函数)
OSTaskCreateHook(ptcb); //任务创建Hook(用户回调函数)
OS_ENTER_CRITICAL();
OSTCBPrioTbl[prio] = ptcb; //将填装好的TCB指针填入优先级数组的对应位置
ptcb->OSTCBNext = OSTCBList; //将填装好的TCB挂载到OSTCBList上
ptcb->OSTCBPrev = (OS_TCB *)0; //该TCB的前一个TCB指针指向0
if (OSTCBList != (OS_TCB *)0) { //如果当前OSTCBList已经有任务挂载,则将链表头的TCB指向当前创建的新TCB,完成双向链接
OSTCBList->OSTCBPrev = ptcb;
}
OSTCBList = ptcb; //OSTCBList链表头指向该TCB,完成新任务的挂载
OSRdyGrp |= ptcb->OSTCBBitY; //将任务优先级组的掩码并入就绪优先级组
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX; //将任务组内优先级的掩码并入就绪列表
OSTaskCtr++; //创建的任务数+1
OS_EXIT_CRITICAL();
return (OS_ERR_NONE);
}
OS_EXIT_CRITICAL();
return (OS_ERR_TASK_NO_MORE_TCB); //没有空白TCB了,即任务数量已满
}
任务优先级
μC/OSⅡ最多支持255个任务,任务的优先级有两种表达方式:
①直接使用一个8位二进制数表示0-254个优先级数,这是最常用的,广泛用于逻辑判断操作;
②分成优先级组和组内优先级,可以理解为一个N*N的矩阵空间,在任务切换时会频繁使用,能迅速定位高优先级的就绪任务,如下图:
具体类型为OS_PRIO,它和任务最低优先级OS_LOWEST_PRIO有关:
#if OS_LOWEST_PRIO <= 63u
typedef INT8U OS_PRIO;
#else
typedef INT16U OS_PRIO;
#endif
看起来很容易理解,系统所需要的优先级数量比较少时,使用8位优先级,能表示8*8=64种优先级;较多时则使用16位优先级,能表示16*16=256种优先级。上图结构对应的全局变量为:
//ucos_ii.h
OS_EXT OS_PRIO OSRdyGrp; /* 优先级组,哪一组有就绪任务就会在对应位置1 */
OS_EXT OS_PRIO OSRdyTbl[OS_RDY_TBL_SIZE]; /* 组内优先级,组内哪一个优先级有就绪任务就会在对应位置1 */
其中:OS_RDY_TBL_SIZE会根据设定的任务数量动态定义,防止浪费。
//ucos_ii.h
#define OS_RDY_TBL_SIZE ((OS_LOWEST_PRIO) / 8u + 1u) /* 组内优先级数组 */
如上所述,μC/OSⅡ系统同时使用了两种方式表示优先级,那么两种方式如何进行转换呢?答案就在本文创建和切换的代码中。
另外,OS_LOWEST_PRIO的定义范围为0-254,为什么没有255呢(刚好用满8位)?这是因为255即0xFF是一个特殊优先级OS_PRIO_SELF,用于在任务操作时传入表示任务自身(类似对线程进行批量操作时可以用到,因为是动态的,难以知道当时运行任务的优先级)。
任务切换
在μC/OSⅡ中,任务切换也可以称为上下文切换(context switch),进行上下文切换的底层函数有两个:常态切换OSCtxSw()和中断切换OSIntCtxSw(),对于用户来说,这种切换是无感的,用户也不用手动去调用这些接口,系统会在合适的时机自动进行切换,比如创建任务后、等待信号量或事件等等。
在系统执行上下文切换之前,都要先进行一系列的准备,其中最重要的就是获取下一个任务。OS_Sched()就是做这个事情的,简单来说,就是获取就绪任务的最高优先级,并在完成一切准备工作后,调用OSCtxSw()执行上下文切换(前提是有切换的必要):
//os_core.c
void OS_Sched (void)
{
#if OS_CRITICAL_METHOD == 3u /* 进入临界区的准备 */
OS_CPU_SR cpu_sr = 0u;
#endif
OS_ENTER_CRITICAL();
if (OSIntNesting == 0u) { /* 该方法不允许在中断中使用 */
if (OSLockNesting == 0u) { /* 调度器不能上锁 */
OS_SchedNew(); //找出当前就绪的最高优先级任务
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; //获取就绪的最高优先级任务
if (OSPrioHighRdy != OSPrioCur) { /* 就绪的最高优先级任务不是当前运行的任务,才需要切换,否则继续运行 */
#if OS_TASK_PROFILE_EN > 0u
OSTCBHighRdy->OSTCBCtxSwCtr++; /* Inc. # of context switches to this task */
#endif
OSCtxSwCtr++; /* 切换次数加1,这个变量好像没什么用 */
OS_TASK_SW(); /* 上下文切换,底层调用OSCtxSw(),由汇编指令实现 */
}
}
}
OS_EXIT_CRITICAL();
}
这里面的OS_SchedNew()用于获取当前就绪任务中的最高优先级OSPrioHighRdy:
//os_core.c
static void OS_SchedNew (void)
{
#if OS_LOWEST_PRIO <= 63u /* 小于64个任务,使用8位优先级 */
INT8U y;
//相当于对组号和组内编号进行解码,还原成原始的优先级序号
y = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((y << 3u) + OSUnMapTbl[OSRdyTbl[y]]);
#else /* 大于等于64个任务,使用16位优先级 */
INT8U y;
OS_PRIO *ptbl;
if ((OSRdyGrp & 0xFFu) != 0u) {
y = OSUnMapTbl[OSRdyGrp & 0xFFu];
} else {
y = OSUnMapTbl[(OS_PRIO)(OSRdyGrp >> 8u) & 0xFFu] + 8u;
}
ptbl = &OSRdyTbl[y];
if ((*ptbl & 0xFFu) != 0u) {
OSPrioHighRdy = (INT8U)((y << 4u) + OSUnMapTbl[(*ptbl & 0xFFu)]);
} else {
OSPrioHighRdy = (INT8U)((y << 4u) + OSUnMapTbl[(OS_PRIO)(*ptbl >> 8u) & 0xFFu] + 8u);
}
#endif
}
里面还涉及一个const映射数组OSUnMapTbl[],用于将优先级的组机制还原成原始的优先级序号(快速映射的算法,这里不多介绍)。
除此之外,我们通常会在中断中成对调用OSIntEnter()和OSIntExit(),其中OSIntExit()就会执行中断切换,其内容和上面的常态任务切换OS_Sched()基本一致,只是对中断嵌套做了处理,上下文切换函数有所区别(OSCtxSw()和OSIntCtxSw()):
//os_core.c
void OSIntExit (void)
{
#if OS_CRITICAL_METHOD == 3u /* 进入临界区的准备 */
OS_CPU_SR cpu_sr = 0u;
#endif
if (OSRunning == OS_TRUE) {
OS_ENTER_CRITICAL();
if (OSIntNesting > 0u) { /* 将中断嵌套-1 */
OSIntNesting--;
}
if (OSIntNesting == 0u) { /* 如果还有中断嵌套(不止一层),就退出 */
if (OSLockNesting == 0u) { /* 检查调度器是否上锁,上锁后无法切换 */
OS_SchedNew(); //获取就绪的最高优先级
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
if (OSPrioHighRdy != OSPrioCur) { /* 就绪的最高优先级任务不是当前运行的任务,才进行切换 */
#if OS_TASK_PROFILE_EN > 0u
OSTCBHighRdy->OSTCBCtxSwCtr++; /* Inc. # of context switches to this task */
#endif
OSCtxSwCtr++; /* 切换次数加1,这个变量好像没什么用 */
OSIntCtxSw(); /* 进行上下文切换 */
}
}
}
OS_EXIT_CRITICAL();
}
}
获取了下一个要执行任务的优先级后,就可以进行上下文切换了,具体的切换代码是使用汇编指令编写的,它们都会主动触发PendSV中断,并在中断处理函数内进行任务切换:
os_cpu_a.asm
PendSV_Handler
CPSID I ; 上下文切换过程中禁止中断
MRS R0, PSP ; PSP is process stack pointer 如果在用PSP堆栈,则可以忽略保存寄存器,参考CM3权威中的双堆栈
CBZ R0, PendSV_Handler_Nosave ; Skip register save the first time
;Is the task using the FPU context? If so, push high vfp registers.
TST R14, #0x10
IT EQ
VSTMDBEQ R0!, {S16-S31}
SUBS R0, R0, #0x20 ; 保存寄存器值到当前任务的堆栈中
STM R0, {R4-R11}
LDR R1, =OSTCBCur ; OSTCBCur首地址即待切换任务的堆栈指针
LDR R1, [R1]
STR R0, [R1] ; R0 is SP of process being switched out
在这里不进一步探究,在上一篇文章中有对该过程进行图形化的解析。