Ucos实现多任务的基础包括几个方面:任务控制块,任务堆栈,中断,任务优先级,一一说起
首先,任务控制块的结构如下
//系统在运行一个任务的时候,按照任务的优先级获取任务控制块,再在任务堆栈中获得任务代码指针
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; /*目前该域未使用 */
#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; /*任务当前的状态标志 */
//任务状态有以下列表
/******************************************
OS_STAT_RDY 状态就绪
OS_STAT_SEM 等待信号量
OS_STAT_MBOX 等待消息邮箱
OS_STAT_Q 等待消息队列
OS_STAT_SUSPEND 任务挂起
OS_STAT_MUTEX 等待互斥信号量
OS_STAT_FLAG 等待标志
OS_STAT_MULTI 等待多那个啥,还不知道
************************************************/
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; /*任务呗切换到的次数 */
INT32U OSTCBCyclesTot; /*任务一共运行的节拍数 */
INT32U OSTCBCyclesStart; /* 任务开始的时候的时钟周期的快*/
OS_STK *OSTCBStkBase; /*任务堆栈的开始位置 */
INT32U OSTCBStkUsed; /* 已经使用的堆栈数量 */
#endif
#if OS_TASK_NAME_EN > 0u
INT8U *OSTCBTaskName;//任务tcb的名称字符串指针
#endif
#if OS_TASK_REG_TBL_SIZE > 0u
INT32U OSTCBRegTbl[OS_TASK_REG_TBL_SIZE];//这里应该是快速保存任务寄存器的数据
#endif
} OS_TCB;
该结构体中有比较多的靠宏定义打开的变量,暂时不讨论,主要有这几个比较重要
OS_STK *OSTCBStkPtr; /*指向任务堆栈栈顶的指针*/
struct os_tcb *OSTCBNext; /*指向后一个任务控制块的指针 */
struct os_tcb *OSTCBPrev; /*指向前一个任务控制块的指针 */
INT8U OSTCBPrio; /*任务的优先级别 */
为什么没有任务代码的指针,这是因为任务代码被操作系统存放在了堆栈区中,我们知道,对于任务来说,任务代码指针实际上指的是执行这段代码的时候,处理器的PC指针,当系统中断到来的时候,这个指针自动保存,执行完中断自动恢复,执行原来的流程,ucos采用的原理就是设计一个系统级别的中断,定时的发生该中断,将执行完中断并自动恢复的过程修改成为任务切换过程,这个切换过程将我们自己的任务堆栈恢复到cpu实际的堆栈中,自动就能切换到新任务了从而实现多任务.可见,任务代码指正是应该要放在堆栈中的,后面在针对代码说.
另外,操作系统的任务控制块并不是动态申请的,而是编译的时候就已经确定有多少个的了,如下
OS_EXT OS_TCB OSTCBTbl[OS_MAX_TASKS + OS_N_SYS_TASKS]; /* Table of TCBs */
OS_MAX_TASKS和OS_N_SYS_TASKS是靠os_cfg文件来定义的,前一个指的是系统最大任务数量,后一个是系统保留任务数量,系统最多只有这么些任务控制块,所以最多最多这么多任务了,同时还有一个相关联的全局变量
OS_EXT OS_TCB *OSTCBPrioTbl[OS_LOWEST_PRIO + 1u];
这个变量会保存系统中所有已经设置了的任务控制块的指针(也就是create了的任务),他的大小是系统最大优先级决定,ucos不允许优先级重复的原因就在这里,他会将系统的任务控制块按照优先级的形式存放在这个数组中,这样,当切换任务的时候就不需要轮询任务控制块列表,而是获取任务优先级之后立刻可以在OSTCBPrioTbl这个表中获取任务控制块,效率快很多(不仅仅是快很多的问题,实时系统要求代码运行效率是可以预估的,而轮询链表的时间是不能确定的,可能第一次就找到了,也可能到最后也找不到).
另外还有几个必备的变量
OS_EXT OS_TCB *OSTCBCur;
OS_EXT OS_TCB *OSTCBFreeList;
OS_EXT OS_TCB *OSTCBHighRdy;
OS_EXT OS_TCB *OSTCBList;
OSTCBCur标识当前着正在运行的tcb块, OSTCBFreeList系统全部tcb中空闲的tcb块链表头指针, OSTCBHighRdy当前已经准备好的最高优先级的tcb块,下一次切换的目标, OSTCBList系统有效tcb块的链表头指针
之前我们看到,在tcb变量结构中有一个next的指针和一个prev的指针,这就是用来构造链表的,在系统初始化的时候会开始构造,但是首先我们需要明白一个事情,不管链表形成了一个神马结构,实际的数据元素依然是在OSTCBTbl数组中存放的,只是程序使用的时候组织了一个链表而已
系统初始化的时候,程序调用OS_Init函数(外部编程调用),在OS_Init中调用OS_InitTCBList函数,在OS_InitTCBList中实现空链表的初始化,如下
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;
#if OS_TASK_NAME_EN > 0u
ptcb1->OSTCBTaskName = (INT8U *)(void *)"?";
#endif
}
ptcb1 = &OSTCBTbl[ix];
ptcb1->OSTCBNext = (OS_TCB *)0;
#if OS_TASK_NAME_EN > 0u
ptcb1->OSTCBTaskName = (INT8U *)(void *)"?";
#endif
OSTCBList = (OS_TCB *)0; OSTCBFreeList = &OSTCBTbl[0]; }
可以看到,进入这个函数,通过一次循环,将OSTCBTbl中的全部元素连接成了一个大的链表,链表的头为OSTCBFreeList,这样就完成了空任务块链表的初始化,之后OSTCBList赋值为null,等待之后的任务创建
创建任务的时候调用的函数为OSTaskCreate,我们分析其部分结构
首先,他会经过一番判定,第一,不能在中断中创建任务 第二,任务优先级不能重复
if (OSTCBPrioTbl[prio] == (OS_TCB *)0)
前面我们说过,任务创建之后,系统会根据任务的优先级将其tcb控制块放到OSTCBPrioTbl中对应的位置,那么该位置的数据就会不为0,此时要是检测到新任务创建的优先级对应的tcb已经有数据了,说明优先级重复,不能创建
之后会调用两个函数
OSTaskStkInit(task, p_arg, ptos, 0u);
OS_TCBInit(prio, psp, (OS_STK *)0, 0u, 0u, (void *)0, 0u);
第一个函数是我们在os_cpu_c.c中需要移植的函数,他和处理器架构相关,具体查看移植指南,有一个重点是在OSTaskStkInit中的这一句
*(stk) = (INT32U)0x01000000L; /* xPSRxPSR T 位(第 24 位)置 1 ,否则第一次执行任务时Fault*/
*(--stk) = (INT32U)task; /* Entry PointPC 肯定得指向任务入口*/
将任务的指针存放在了堆栈中,和之前说的吻合
堆栈区设置好了之后就要初始化tcb控制块,这一段代码比较长,说说几个细节
首先,系统应该从空闲控制块中取出一个控制块并且加入有效tcb控制块中,如下
ptcb = OSTCBFreeList
if (ptcb != (OS_TCB *)0)
{
OSTCBFreeList = ptcb->OSTCBNext;
可见,空链表的表头被换成了第二个节点,表头用来进行后面的操作,也就是用来当成当前任务的任务控制块
ptcb->OSTCBStkPtr = ptos;
ptcb->OSTCBPrio = prio;
ptcb->OSTCBStat = OS_STAT_RDY;
ptcb->OSTCBStatPend = OS_STAT_PEND_OK;
ptcb->OSTCBDly = 0u;
保存了传入的堆栈的栈顶指针,任务优先级,栈中有任务代码指针,齐活,在完成之后一些关于信号量等等的操作之后(后面说),来到了这一段
OSTCBPrioTbl[prio] = ptcb;
ptcb->OSTCBNext = OSTCBList;
ptcb->OSTCBPrev = (OS_TCB *)0;
if (OSTCBList != (OS_TCB *)0)
{
OSTCBList->OSTCBPrev = ptcb;
}
OSTCBList = ptcb;
将当前创建的任务的控制块指针按照优先级放到了OSTCBPrioTbl中,后面直接可以根据优先级取出来,新的任务控制块连接到了系统已经存在的任务控制块的头部,从而完成了一个空闲控制块到有效控制块的转移,并创建了任务,等待后期调用
另外,ucos定义了一个空闲任务,用于在没有任务的情况下执行该任务,所以这个任务必须要是最低优先级,否则他会抢占别的优先级的任务,任务函数名
OS_TaskIdle
该任务什么事情都不干,在那类似空转运行,被OS_InitTaskIdle调用,而OS_InitTaskIdle被os_init调用,从而在初始化系统的时候该任务自动创建
除了空闲任务之外,还定义了一个统计任务,专用于统计系统执行情况,如cpu使用率这些,要监控系统的话,可以再统计任务钩子函数中做操作将系统运行情况输出,与之相关的变量为OSCPUUsage