使用ucosii也有一段时间了,把学习到的总结一下。这篇文章不是对ucosii如何使用的讲解,而是主要看看ucosii内核实现的原理,或者说讲一些RTOS种通用的知识。对于RTOS基础知识的讲解,暂时就不表述了。
RTOS中一个任务的三要素:程序代码、堆栈、任务控制块。
程序代码就是任务的执行函数。这没啥好说的
每个任务都有自己的堆栈,来保存自身的信息。这是每个任务自己的资源。是每个人自己的一亩三分地。
任务控制块则是对任务的描述,例如任务名称、任务优先级、堆栈地址等等信息。
任务如何切换呢?
首先,理解一句话叫做,处理器就是个傻瓜,PC让它干啥,它就干啥,PC指向哪儿,它就去哪儿。这句话就是任务切换的本质,你想让处理器执行任务A,好,让PC指向任务A,你想切换到任务B,没问题,让PC指向任务B就搞定了。说的正式一点,系统是通过把带运行程序的地址赋予程序计数器PC来实现任务的切换的。
任务运行时处理器与内存之间的关系
处理器通过两个指针寄存器PC和SP来与任务代码和任务堆栈建立联系,PC指向的是执行的任务代码,SP则是指向任务的堆栈。
有的学习材料将程序运行环境分为两个部分,一部分是处理器中运行环境,另一部分是内存中的运行环境,内存中的运行环境被称为虚拟运行环境。个人觉得这种说法可能会带来误解。这里也解释解释,只有将处理器的PC值和SP赋值(以及其他一些通用和状态寄存器),任务才能真正运行起来。而在内存中呢,会把各个任务的PC和SP值(以及其他一些通用和状态寄存器)保存(或者叫备份)一份,当需要运行这个任务的时候呢,就把这些值从内存中拷贝到处理器的对应寄存器中,这个任务呢就跑起来了。
有了上面这段话,任务如何切换就应该有一个大致的印象了。
左侧是处理器,中间的是内存中的空间,内存有每一个任务的信息备份。当运行某个任务的时候,就把该任务的虚拟环境从内存中放到真实的处理器环境当中。
我们还要考虑到一点,当切换任务时,当前正在执行的任务怎么办呢?很简单,将当前任务信息从处理器备份到内存中就可以了,下一次用的时候再从内存中复制到处理器中就OK了。
内存中要备份任务的什么信息呢
上面的任务切换就是备份当前任务的信息到内存中,赋值下一个任务信息到处理器中。那么问题是,需要保存那些任务信息呢?
用通俗的话说一说要保存的内容,
1、PC 。亲们,中断发生时需要做什么呢?现场保护,其实任务切换和中断的工作也有很多共同之处。假如当前正在执行i++,此时我们要进行任务切换,我们需要把PC值保存,等到下一次执行这个任务时,我们还能从i++开始执行。
2、任务堆栈指针SP ,前面我们一直在说这个东东,只有知道了sp我们才能找到对应的任务堆栈,才能找到堆栈保存任务信息。所以只要找到了任务栈指针SP,任务的私有信息就找到了。
3、PSW 程序状态字寄存器,例如一些进位、补位信息。这些不能丢,否则,在此切换到该任务时,计算不就错误了。
4、通用寄存器内容,这没啥说的,可能会保存一些变量的值,我们当然得保存起来了。
ARM在中断会自动将R0-R3 R12 LR PC xPSR保存起来,所以我们要保存是通过用寄存器中剩下的R4-R11 和SP。SP指向的任务栈,只有知道了SP,才能存储其他信息。这里还有知道一点,除了SP,其他要保存的信息都是保存在各自的任务栈中。但是SP是保存在主栈当中的。或者这样说,SP是保存在我们前面提到的任务块当中,任务块并不是存在任务栈当中的,而是存在于主栈当中的。
我们来看看任务块中都有什么
typedef struct os_tcb {
OS_STK *OSTCBStkPtr; /* Pointer to current top of stack */
#if OS_TASK_CREATE_EXT_EN > 0u
void *OSTCBExtPtr; /* Pointer to user definable data for TCB extension */
OS_STK *OSTCBStkBottom; /* Pointer to bottom of stack */
INT32U OSTCBStkSize; /* Size of task stack (in number of stack elements) */
INT16U OSTCBOpt; /* Task options as passed by OSTaskCreateExt() */
INT16U OSTCBId; /* Task ID (0..65535) */
#endif
struct os_tcb *OSTCBNext; /* Pointer to next TCB in the TCB list */
struct os_tcb *OSTCBPrev; /* Pointer to previous TCB in the TCB list */
#if (OS_EVENT_EN)
OS_EVENT *OSTCBEventPtr; /* Pointer to event control block */
#endif
#if (OS_EVENT_EN) && (OS_EVENT_MULTI_EN > 0u)
OS_EVENT **OSTCBEventMultiPtr; /* Pointer to multiple event control blocks */
#endif
#if ((OS_Q_EN > 0u) && (OS_MAX_QS > 0u)) || (OS_MBOX_EN > 0u)
void *OSTCBMsg; /* Message received from OSMboxPost() or OSQPost() */
#endif
#if (OS_FLAG_EN > 0u) && (OS_MAX_FLAGS > 0u)
#if OS_TASK_DEL_EN > 0u
OS_FLAG_NODE *OSTCBFlagNode; /* Pointer to event flag node */
#endif
OS_FLAGS OSTCBFlagsRdy; /* Event flags that made task ready to run */
#endif
INT32U OSTCBDly; /* Nbr ticks to delay task or, timeout waiting for event */
INT8U OSTCBStat; /* Task status */
INT8U OSTCBStatPend; /* Task PEND status */
INT8U OSTCBPrio; /* Task priority (0 == highest) */
INT8U OSTCBX; /* Bit position in group corresponding to task priority */
INT8U OSTCBY; /* Index into ready table corresponding to task priority */
OS_PRIO OSTCBBitX; /* Bit mask to access bit position in ready table */
OS_PRIO OSTCBBitY; /* Bit mask to access bit position in ready group */
#if OS_TASK_DEL_EN > 0u
INT8U OSTCBDelReq; /* Indicates whether a task needs to delete itself */
#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;
这是任务块的原型,是不是太复杂了,我们来简化一下
typedef struct os_tcb {
OS_STK *OSTCBStkPtr; /* Pointer to current top of stack */
struct os_tcb *OSTCBNext; /* Pointer to next TCB in the TCB list */
struct os_tcb *OSTCBPrev; /* Pointer to previous TCB in the TCB list */
INT32U OSTCBDly; /* Nbr ticks to delay task or, timeout waiting for event */
INT8U OSTCBStat; /* Task status */
INT8U OSTCBPrio; /* Task priority (0 == highest) */
} OS_TCB;
简单来看,任务控制块就可以简化成上面的样子
OSTCBStkPtr:任务栈指针,放在任务块的开始,方便使用OSTCBNext:指向下一个任务块的指针
OSTCBPrev:指向上一个任务块的指针
从这里也能看出,系统的所有任务是一个双链表结构。
OSTCBDly:延迟拍数,使用delay函数会用到这个变量
OSTCBStat:任务的状态
OSTCBPrio:任务的优先级,ucosii中不允许想同优先级的任务,所以可以通过优先级唯一的区分任务
在系统初始化的时候,会根据设定的最大任务数创建一个空任务控制块链表OSTCBFreeList
void OSInit (void)
{
OSInitHookBegin(); /* Call port specific initialization code */
OS_InitMisc(); /* Initialize miscellaneous variables */
OS_InitRdyList(); /* Initialize the Ready List */
OS_InitTCBList(); /* 初始化一个空任务控制块链表 */
OS_InitEventList(); /* Initialize the free list of OS_EVENTs */
#if (OS_FLAG_EN > 0u) && (OS_MAX_FLAGS > 0u)
OS_FlagInit(); /* Initialize the event flag structures */
#endif
#if (OS_MEM_EN > 0u) && (OS_MAX_MEM_PART > 0u)
OS_MemInit(); /* Initialize the memory manager */
#endif
#if (OS_Q_EN > 0u) && (OS_MAX_QS > 0u)
OS_QInit(); /* Initialize the message queue structures */
#endif
OS_InitTaskIdle(); /* Create the Idle Task */
#if OS_TASK_STAT_EN > 0u
OS_InitTaskStat(); /* Create the Statistic Task */
#endif
#if OS_TMR_EN > 0u
OSTmr_Init(); /* Initialize the Timer Manager */
#endif
OSInitHookEnd(); /* Call port specific init. code */
#if OS_DEBUG_EN > 0u
OSDebugInit();
#endif
}
static void OS_InitTCBList (void)
{
INT8U ix;
INT8U ix_next;
OS_TCB *ptcb1;
OS_TCB *ptcb2;
OS_MemClr((INT8U *)&OSTCBTbl[0], sizeof(OSTCBTbl)); /* Clear all the TCBs */
OS_MemClr((INT8U *)&OSTCBPrioTbl[0], sizeof(OSTCBPrioTbl)); /* Clear the priority table */
for (ix = 0u; ix < (OS_MAX_TASKS + OS_N_SYS_TASKS - 1u); ix++) { /* Init. list of free TCBs 根据最大任务数创建空任务链表 */
ix_next = ix + 1u;
ptcb1 = &OSTCBTbl[ix];
ptcb2 = &OSTCBTbl[ix_next];
ptcb1->OSTCBNext = ptcb2;
#if OS_TASK_NAME_EN > 0u
ptcb1->OSTCBTaskName = (INT8U *)(void *)"?"; /* Unknown name */
#endif
}
ptcb1 = &OSTCBTbl[ix];
ptcb1->OSTCBNext = (OS_TCB *)0; /* Last OS_TCB */
#if OS_TASK_NAME_EN > 0u
ptcb1->OSTCBTaskName = (INT8U *)(void *)"?"; /* Unknown name */
#endif
OSTCBList = (OS_TCB *)0; /* TCB lists initializations */
OSTCBFreeList = &OSTCBTbl[0];
}
创建完毕如下图,形成一个空任务链表,在创建任务时,就从空任务链表中拿一个任务控制块放到任务控制链表OSTCBList中
如何对任务进行索引呢?ucosii把所有的任务块指针按照优先级放到一个指针数组OSTCBPrioTbl[OS_LOWEST_PRIO + 1u]中,通过这个指针数组就能找到每一个任务块的地址,创建任务完成后,形成如下图所示的数据结构图
当应用程序调用OSTaskCreate()函数时创建一个任务时,这个函数会调用调用系统函数OSTcbInit()来为任务控制块进行初始化。这个函数首先为被创建的任务控制块从空任务控制链表中获取一个任务控制块,然后对任务的属性对任务控制块各个成员进行赋值,最后再把这个任务控制块插入任务控制链表OSTCBList的头部,使用链表头插的方法。至此,任务就创建完成了。
/*
*********************************************************************************************************
* INITIALIZE TCB
*
* Description: This function is internal to uC/OS-II and is used to initialize a Task Control Block when
* a task is created (see OSTaskCreate() and OSTaskCreateExt()).
*
* Arguments : prio is the priority of the task being created
*
* ptos is a pointer to the task's top-of-stack assuming that the CPU registers
* have been placed on the stack. Note that the top-of-stack corresponds to a
* 'high' memory location is OS_STK_GROWTH is set to 1 and a 'low' memory
* location if OS_STK_GROWTH is set to 0. Note that stack growth is CPU
* specific.
*
* pbos is a pointer to the bottom of stack. A NULL pointer is passed if called by
* 'OSTaskCreate()'.
*
* id is the task's ID (0..65535)
*
* stk_size is the size of the stack (in 'stack units'). If the stack units are INT8Us
* then, 'stk_size' contains the number of bytes for the stack. If the stack
* units are INT32Us then, the stack contains '4 * stk_size' bytes. The stack
* units are established by the #define constant OS_STK which is CPU
* specific. 'stk_size' is 0 if called by 'OSTaskCreate()'.
*
* pext is a pointer to a user supplied memory area that is used to extend the task
* control block. This allows you to store the contents of floating-point
* registers, MMU registers or anything else you could find useful during a
* context switch. You can even assign a name to each task and store this name
* in this TCB extension. A NULL pointer is passed if called by OSTaskCreate().
*
* opt options as passed to 'OSTaskCreateExt()' or,
* 0 if called from 'OSTaskCreate()'.
*
* Returns : OS_ERR_NONE if the call was successful
* OS_ERR_TASK_NO_MORE_TCB if there are no more free TCBs to be allocated and thus, the task cannot
* be created.
*
* Note : This function is INTERNAL to uC/OS-II and your application should not call it.
*********************************************************************************************************
*/
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 /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0u;
#endif
#if OS_TASK_REG_TBL_SIZE > 0u
INT8U i;
#endif
OS_ENTER_CRITICAL();
ptcb = OSTCBFreeList; /* Get a free TCB from the free TCB list 从空任务链表中获取一个任务控制块 */
if (ptcb != (OS_TCB *)0) {
OSTCBFreeList = ptcb->OSTCBNext; /* Update pointer to free TCB list */
OS_EXIT_CRITICAL(); /*根据参数对任务块各个成员赋值*/
ptcb->OSTCBStkPtr = ptos; /* Load Stack pointer in TCB */
ptcb->OSTCBPrio = prio; /* Load task priority into TCB */
ptcb->OSTCBStat = OS_STAT_RDY; /* Task is ready to run */
ptcb->OSTCBStatPend = OS_STAT_PEND_OK; /* Clear pend status */
ptcb->OSTCBDly = 0u; /* Task is not delayed */
............
OSTCBInitHook(ptcb);
OSTaskCreateHook(ptcb); /* Call user defined hook */
OS_ENTER_CRITICAL();
OSTCBPrioTbl[prio] = ptcb; /*将这个任务控制块地址放到OSTCBPrioTbl数组中*/
ptcb->OSTCBNext = OSTCBList; /* Link into TCB chain */
ptcb->OSTCBPrev = (OS_TCB *)0; /*将这个任务控制块插到任务控制块链表的头部*/
if (OSTCBList != (OS_TCB *)0) {
OSTCBList->OSTCBPrev = ptcb;
}
OSTCBList = ptcb;
OSRdyGrp |= ptcb->OSTCBBitY; /* Make task ready to run */
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX;
OSTaskCtr++; /* Increment the #tasks counter */
OS_EXIT_CRITICAL();
return (OS_ERR_NONE);
}
OS_EXIT_CRITICAL();
return (OS_ERR_TASK_NO_MORE_TCB);
}