UCOSIII的任务简介
在UCOSIII中任务是以何种面貌存在的呢?
在UCOSIII中任务就是程序实体,UCOSIII能够管理和调度这些小任务(程序)。UCOSIII中的任务由三部分组成:任务堆栈、任务控制块和任务函数。
- 任务堆栈:上下文切换的时候用来保存任务的工作环境,就是STM32的内部寄存器值;
- 任务控制块:任务控制块用来记录任务的各个属性;
- 任务函数:由用户编写的任务处理代码,是实实在在干活的,一般写法如下:
void XXX_task(void *p_arg)
{
while(1)
{
... //任务处理过程
}
}
可以看出用任务函数通常是一个无限循环,当然了,也可以是一个只执行一次的任务。任务的参数是一个void类型的,这么做的目的是可以可以传递不同类型的数据甚至是函数。
可以看出任务函数其实就是一个C语言的函数,但是在使用UCOIII的情况下这个函数不能有用户自行调用,任务函数何时执行执行,何时停止完全有操作系统来控制。
UCOSIII的系统任务
UCOSIII默认有5个系统任务:
- 空闲任务:UCOSIII创建的第一个任务,UCOSIII必须创建的任务,此任务有UCOSIII自动创建,不需要用户手动创建;
- 时钟节拍任务:此任务也是必须创建的任务;
- 统计任务:可选任务,用来统计CPU使用率和各个任务的堆栈使用量。此任务是可选任务,由宏OS_CFG_STAT_TASK_EN控制是否使用此任务;
- 定时任务:用来向用户提供定时服务,也是可选任务,由宏OS_CFG_TMR_EN控制是否使用此任务;
- 中断服务管理任务:可选任务,由宏OS_CFG_ISR_POST_DEFERRED_EN控制是否使用此任务。
前两个系统任务时必须创建的任务,而后三者不是。控制后三者任务的宏都是在文件OS_CFG.h中。
UCOSIII的任务状态
从用户的角度看,UCOSIII的任务一共有5种状态:
- 休眠态:任务已经在CPU的flash中了,但是还不受UCOSIII管理;
- 就绪态:系统为任务分配了任务控制块,并且任务已经在就绪表中登记,这时这个任务就具有了运行的条件,此时任务的状态就是就绪态;
- 运行态:任务获得CPU的使用权,正在运行;
- 等待态:正在运行的任务需要等待一段时间,或者等待某个事件,这个任务就进入了等待态,此时系统就会把CPU使用权转交给别的任务;
- 中断服务态:当发送中断,当前正在运行的任务会被挂起,CPU转而去执行中断服务函数,此时任务的任务状态叫做中断服务态。
这5种状态之间相互转化的关系如下图:
UCOSIII的任务详解
上文讲到:UCOSIII中的任务由三部分组成:任务堆栈、任务控制块和任务函数。下面就对这三个部分进行分析:
任务堆栈
任务堆栈是任务的重要部分,堆栈是在RAM中按照“先进先出(FIFO)”的原则组织的一块连续的存储空间。为了满足任务切换和响应中断时保存CPU寄存器中的内容及任务调用其它函数时的需要,每个任务都应该有自己的堆栈。
任务堆栈创建
#define START_STK_SIZE 512 //堆栈大小
CPU_STK START_TASK_STK[START_STK_SIZE]; //定义一个数组来作为任务堆栈
任务堆栈的大小是多少呢?
CPU_STK为CPU_INT32U类型,也就是unsigned int类型,为4字节的,那么任务堆栈START_TASK_STK的大小就为:512 X 4=2048字节!
任务堆栈初始化
任务如何才能切换回上一个任务并且还能接着从上次被中断的地方开始运行?
恢复现场即可,现场就是CPU的内部各个寄存器。因此在创建一个新任务时,必须把系统启动这个任务时所需的CPU各个寄存器初始值事先存放在任务堆栈中。这样当任务获得CPU使用权时,就把任务堆栈的内容复制到CPU的各个寄存器,从而可以任务顺利地启动并运行。
把任务初始数据存放到任务堆栈的工作就叫做任务堆栈的初始化,UCOSIII提供了完成堆栈初始化的函数:OSTaskStkInit()。
CPU_STK *OSTaskStkInit (OS_TASK_PTR p_task,
void *p_arg,
CPU_STK *p_stk_base,
CPU_STK *p_stk_limit,
CPU_STK_SIZE stk_size,
OS_OPT opt)
{
... //函数内容
return (p_stk);
}
当然,用户一般不会直接操作堆栈初始化函数,任务堆栈初始化函数由任务创建函数OSTaskCreate()调用。不同的CPU对于的寄存器和对堆栈的操作方式不同,因此在移植UCOSIII的时候需要用户根据各自所选的CPU来编写任务堆栈初始化函数。
void OSTaskCreate (OS_TCB *p_tcb, //任务控制块
CPU_CHAR *p_name, //任务名字
OS_TASK_PTR p_task, //任务函数
void *p_arg, //传递给任务函数的参数
OS_PRIO prio, //任务优先级
CPU_STK *p_stk_base, //------任务堆栈基地址
CPU_STK_SIZE stk_limit, //------任务堆栈深度限位
CPU_STK_SIZE stk_size, //------任务堆栈大小
OS_MSG_QTY q_size,
OS_TICK time_quanta,
void *p_ext, //用户补充的存储区
OS_OPT opt,
OS_ERR *p_err) //存放该函数错误时的返回值
{
... //函数内容
}
函数OSTaskCreate()中的参数p_stk_base(任务堆栈基地址)如何确定?
根据堆栈的增长方式,堆栈有两种增长方式:
- 向上增长:堆栈的增长方向从低地址向高地址增长;
- 向下增长:堆栈的增长方向从高地址向低地址增长。
函数OSTaskCreate()中的参数p_stk_base是任务堆栈基地址,那么如果CPU的堆栈是向上增长的话那么基地址就&START_TASK_STK[0];如果CPU堆栈是向下增长的话基地址就是&START_TASK_STK[START_STK_SIZE-1]。STM32的堆栈是向下增长的!
任务控制块
任务控制块是用来记录与任务相关的信息的数据结构,每个任务都要有自己的任务控制块。我们使用OSTaskCreate()函数来创建任务的时候就会给任务分配一个任务控制块。任务控制块由用户自行创建,如下代码为创建一个任务控制块:
OS_TCB StartTaskTCB; //创建一个任务控制块
OS_TCB为一个结构体,描述了任务控制块,任务控制块中的成员变量用户不能直接访问,更不可能改变他们。
OS_TCB为一个结构体,其中有些成员采用了条件编译的方式来确定。
struct os_tcb {
CPU_STK *StkPtr; /* 指向当前任务堆栈的栈顶 */
void *ExtPtr; /* 指向用户可定义的数据区 */
CPU_STK *StkLimitPtr; /* 可指向任务堆栈中的某个位置 */
OS_TCB *NextPtr; /* Pointer to next TCB in the TCB list */
OS_TCB *PrevPtr; /* Pointer to previous TCB in the TCB list */
OS_TCB *TickNextPtr;
OS_TCB *TickPrevPtr;
OS_TICK_SPOKE *TickSpokePtr; /* Pointer to tick spoke if task is in the tick list */
CPU_CHAR *NamePtr; /* Pointer to task name */
CPU_STK *StkBasePtr; /* Pointer to base address of stack */
#if defined(OS_CFG_TLS_TBL_SIZE) && (OS_CFG_TLS_TBL_SIZE > 0u)
OS_TLS TLS_Tbl[OS_CFG_TLS_TBL_SIZE];
#endif
OS_TASK_PTR TaskEntryAddr; /* Pointer to task entry point address */
void *TaskEntryArg; /* Argument passed to task when it was created */
OS_PEND_DATA *PendDataTblPtr; /* Pointer to list containing objects pended on */
OS_STATE PendOn; /* Indicates what task is pending on */
OS_STATUS PendStatus; /* Pend status */
OS_STATE TaskState; /* See OS_TASK_STATE_xxx */
OS_PRIO Prio; /* Task priority (0 == highest) */
CPU_STK_SIZE StkSize; /* Size of task stack (in number of stack elements) */
OS_OPT Opt; /* Task options as passed by OSTaskCreate()*/
OS_OBJ_QTY PendDataTblEntries; /* Size of array of objects to pend on */
CPU_TS TS; /* Timestamp */
OS_SEM_CTR SemCtr; /* Task specific semaphore counter */
/* DELAY / TIMEOUT */
OS_TICK TickCtrPrev; /* Previous time when task was ready */
OS_TICK TickCtrMatch; /* Absolute time when task is going to be ready */
OS_TICK TickRemain; /* Number of ticks remaining for a match */
/* ... run-time by OS_StatTask() */
OS_TICK TimeQuanta;
OS_TICK TimeQuantaCtr;
#if OS_MSG_EN > 0u
void *MsgPtr; /* Message received */
OS_MSG_SIZE MsgSize;
#endif
#if OS_CFG_TASK_Q_EN > 0u
OS_MSG_Q MsgQ; /* Message queue associated with task */
#if OS_CFG_TASK_PROFILE_EN > 0u
CPU_TS MsgQPendTime; /* Time it took for signal to be received */
CPU_TS MsgQPendTimeMax; /* Max amount of time it took for signal to be received */
#endif
#endif
#if OS_CFG_TASK_REG_TBL_SIZE > 0u
OS_REG RegTbl[OS_CFG_TASK_REG_TBL_SIZE]; /* Task specific registers */
#endif
#if OS_CFG_FLAG_EN > 0u
OS_FLAGS FlagsPend; /* Event flag(s) to wait on */
OS_FLAGS FlagsRdy; /* Event flags that made task ready to run */
OS_OPT FlagsOpt; /* Options (See OS_OPT_FLAG_xxx) */
#endif
#if OS_CFG_TASK_SUSPEND_EN > 0u
OS_NESTING_CTR SuspendCtr; /* Nesting counter for OSTaskSuspend() */
#endif
#if OS_CFG_TASK_PROFILE_EN > 0u
OS_CPU_USAGE CPUUsage; /* CPU Usage of task (0.00-100.00%) */
OS_CPU_USAGE CPUUsageMax; /* CPU Usage of task (0.00-100.00%) - Peak */
OS_CTX_SW_CTR CtxSwCtr; /* Number of time the task was switched in */
CPU_TS CyclesDelta; /* value of OS_TS_GET() - .CyclesStart */
CPU_TS CyclesStart; /* Snapshot of cycle counter at start of task resumption */
OS_CYCLES CyclesTotal; /* Total number of # of cycles the task has been running */
OS_CYCLES CyclesTotalPrev; /* Snapshot of previous # of cycles */
CPU_TS SemPendTime; /* Time it took for signal to be received */
CPU_TS SemPendTimeMax; /* Max amount of time it took for signal to be received */
#endif
#if OS_CFG_STAT_TASK_STK_CHK_EN > 0u
CPU_STK_SIZE StkUsed; /* Number of stack elements used from the stack */
CPU_STK_SIZE StkFree; /* Number of stack elements free on the stack */
#endif
#ifdef CPU_CFG_INT_DIS_MEAS_EN
CPU_TS IntDisTimeMax; /* Maximum interrupt disable time */
#endif
#if OS_CFG_SCHED_LOCK_TIME_MEAS_EN > 0u
CPU_TS SchedLockTimeMax; /* Maximum scheduler lock time */
#endif
#if OS_CFG_DBG_EN > 0u
OS_TCB *DbgPrevPtr;
OS_TCB *DbgNextPtr;
CPU_CHAR *DbgNamePtr;
#endif
};
任务控制块初始化
USOCIII提供了用于任务控制块初始化的函数:OS_TaskInitTCB()。但是,用户不需要自行初始化任务控制块。因为和任务堆栈初始化函数一样,函数OSTaskCreate()在创建任务的时候会对任务的任务控制块进行初始化。
UCOSIII的任务就绪表
UCOSIII中任务优先级数由宏OS_CFG_PRIO_MAX来配置,UCOSIII中数值越小,优先级越高,最低可用优先级就是OS_CFG_PRIO_MAX-1。默认OS_CFG_PRIO_MAX的值为64。
UCOSIII中就绪表由2部分组成:
- 优先级位映射表OSPrioTbl[]:用来记录哪个优先级下有任务就绪;
- 就绪任务列表OSRdyList[]:用来记录每一个优先级下所有就绪的任务。
优先级位映射表OSPrioTbl[]
OSPrioTbl[]在os_prio.c中有定义:
CPU_DATA OSPrioTbl[OS_PRIO_TBL_SIZE];
在STM32中CPU_DATA为unsigned int,有4个字节,32位。因此表OSPrioTbl每个参数有32位,其中每个位对应一个优先级下是否有任务就绪。
OS_PRIO_TBL_SIZE = ((OS_CFG_PRIO_MAX - 1u) / DEF_INT_CPU_NBR_BITS)+ 1)
DEF_INT_CPU_NBR_BITS = CPU_CFG_DATA_SIZE * DEF_OCTET_NBR_BITS
OS_CFG_PRIO_MAX由用户自行定义,默认为64。而CPU_CFG_DATA_SIZE=CPU_WORD_SIZE_32=4,DEF_OCTET_NBR_BITS=8。
所以,当系统有64个优先级的时候:OS_PRIO_TBL_SIZE=((64-1)/(4*8)+1)=2。
对比图就很明确了,由于由64个优先级,所以用两个32位的数来存储。每一位相对应于一个优先级,该位为1表示该优先级有任务就绪,该位为0表示该优先级没有任务就绪。
如何找到已经就绪了的最高优先级的任务?
函数OS_PrioGetHighest()用于找到就绪了的最高优先级的任务。
OS_PRIO OS_PrioGetHighest (void)
{
CPU_DATA *p_tbl;
OS_PRIO prio;
prio = (OS_PRIO)0;
p_tbl = &OSPrioTbl[0]; //指向优先级位映射表的第一个32bit的数
while (*p_tbl == (CPU_DATA)0) { /* 判断该32位的数为0 */
prio += DEF_INT_CPU_NBR_BITS; /* 将32位的数拿出来 */
p_tbl++;
}
prio += (OS_PRIO)CPU_CntLeadZeros(*p_tbl); /* Find the position of the first bit set at the entry */
return (prio);
}
这段程序的思路:先判断每一个32位的数是否为0,不为0的话,再通过函数CPU_CntLeadZeros()来计算前导零数量。这个函数用汇编来写的:
CPU_CntLeadZeros
CLZ R0, R0 ; Count leading zeros
BX LR
就绪任务列表OSRdyList[]
通过上一步我们已经知道了哪个优先级的任务已经就绪了,但是UCOSIII支持时间片轮转调度,同一个优先级下可以有多个任务,因此我们还需要在确定是优先级下的哪个任务就绪了。
先看一下os_rdy_list[]的定义:
struct os_rdy_list {
OS_TCB *HeadPtr //用于创建链表,指向链表头
OS_TCB *TailPtr; //用于创建链表,指向链表尾
OS_OBJ_QTY NbrEntries; //此优先级下的任务数量
};
UCOSIII支持时间片轮转调度,因此在一个优先级下会有多个任务,那么我们就要对这些任务做一个管理,这里使用OSRdyList[]数组管理这些任务。OSRdyList[]数组中的每个元素对应一个优先级,比如OSRdyList[0]就用来管理优先级0下的所有任务。OSRdyList[0]为OS_RDY_LIST类型,从上面OS_RDY_LIST结构体可以看到成员变量:HeadPtr和TailPtr分别指向OS_TCB,我们知道OS_TCB是可以用来构造链表的,因此同一个优先级下的所有任务是通过链表来管理的,HeadPtr和TailPtr分别指向这个链表的头和尾,NbrEntries用来记录此优先级下的任务数量,图5.5.2表示了优先级4现在有3个任务时候的就绪任务列表。
这里记住一句话:同一优先级下如果有多个任务的话最先运行的永远是HeadPtr所指向的任务!