使用过ucosii的朋友应该都会知道,单片机+嵌入式实时操作系统能够做到尽可能最大化的利用cpu资源,通过加入实时操作系统能够做出更加强大的产品和应用。
不知道使用过ucosii的朋友有没有去了解过它进行任务调度的原理和实现方式呢?
我个人结合ucosii的源码和自己的理解,分享一些有关ucosii的任务管理和调度的实现。
1、ucos-ii 任务创建与任务调度
1.1、任务的创建
当你调用 OSTaskCreate( ) 进行任务的创建的时候,会初始化任务的堆栈、保存cpu的寄存器、创建任务的控制块(OS_TCB)等的操作;
if (OSTCBPrioTbl[prio] == (OS_TCB *)0) { /* Make sure task doesn't already exist at this priority */
OSTCBPrioTbl[prio] = OS_TCB_RESERVED;/* Reserve the priority to prevent others from doing ... */
/* ... the same thing until task is created. */
OS_EXIT_CRITICAL();
psp = OSTaskStkInit(task, p_arg, ptos, 0u); /* Initialize the task's stack */
err = OS_TCBInit(prio, psp, (OS_STK *)0, 0u, 0u, (void *)0, 0u);
if (err == OS_ERR_NONE) {
if (OSRunning == OS_TRUE) { /* Find highest priority task if multitasking has started */
OS_Sched();
}
} else {
OS_ENTER_CRITICAL();
OSTCBPrioTbl[prio] = (OS_TCB *)0;/* Make this priority available to others */
OS_EXIT_CRITICAL();
}
return (err);
}
注意:ucosii不支持两个及以上相同的任务优先级的任务,ucosiii支持时间片轮转。
ucosii 的任务控制块是任务中很重要,它记录了任务的信息,包括优先级、延时时间、状态等信息。控制块定义如下:
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;
1.2、任务调度实现
1.2.1、将任务优先级进行分组
因为ucosii最大优先级数量为64个,所以可以分成8组,每组8个优先级。
当一个任务被创建成功之后,它的组号由优先级的高三位决定(bit5 bit4 bit3),它在组内的编号由优先级的低三位决定(bit2 bit1 bit0),如下:
#if OS_LOWEST_PRIO <= 63u /* Pre-compute X, Y */
ptcb->OSTCBY = (INT8U)(prio >> 3u); // 组
ptcb->OSTCBX = (INT8U)(prio & 0x07u); // 组内编号
#else
1.2.2、任务就绪表
ucosii对任务优先级的调度管理是通过查询任务就绪表进行的。任务就绪表里面保存着当前所有任务的就绪状态,如下:
OSRdyTbl[8]
说明:
1)它是uint8的数据类型。它的长度是8,每一个元素代表一个组,
比如 OSRdyTbl[0]代表第0组, OSRdyTbl[1]代表第1组,OSRdyTbl[2]代表第2组……以此类推。
2)每一个元素中的每一个位(bit)代表组内的任务的就绪状态(1为就绪,0为未就绪)。
打个比方:
1)当优先级为12 的任务就绪时,那么对应的OSRdyTbl[1]的第4位bit,绝对等于1;
当整个系统中,当只有优先级为12的任务就绪,其他所有任务都没有就绪时,那么OSRdyTbl[1] 绝对等于0x10。
2)当优先级为0和1的任务就绪时,那么对应的OSRdyTbl[0]的第0位bit以及第1位bit,都绝对等于1;
当整个系统中,当只有优先级为0和1的任务就绪,其他所有任务都没有就绪时,那么OSRdyTbl[0] 绝对等于0x03。
1.2.3、任务释放CPU使用权
当任务中调用 OSTimeDly( ) 时,会让任务进入休眠的状态,交出CPU的执行权给到其他就绪任务去执行,这个过程就发生了任务的切换。
简单而言就是会把任务就绪表 OSRdyTbl 中对应的任务优先级在组内的编号状态改变,从而使任务自身进入休眠状态。代码如下:
if (ticks > 0u) { /* 0 means no delay! */
OS_ENTER_CRITICAL();
y = OSTCBCur->OSTCBY; /* Delay current task */
OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX;
if (OSRdyTbl[y] == 0u) {
OSRdyGrp &= (OS_PRIO)~OSTCBCur->OSTCBBitY;
}
OSTCBCur->OSTCBDly = ticks; /* Load ticks in TCB */
OS_EXIT_CRITICAL();
OS_Sched(); /* Find next task to run! */
}
在上面的代码中发现了一个东西:OSRdyGrp。这个有什么用呢?
OSRdyGrp:管理任务就绪组的
OSRdyGrp是INT8U类型的,它每一个bit代表一个组,只要这个组内有任何一个任务就绪了,那对应的这个bit就会被设置为1,表示这个组内目前有就绪的任务。否者对应的位为0。
举个例子,如下:
1)系统中只有任务0就绪了,那么OSRdyGrp 便等于 0x01(二进制00000001)。
2)系统中有任务0和任务63都就绪了,那么OSRdyGrp 便等于 0x81(二进制10000001)。
1.2.4、任务实现调度切换操作
发生一次任务调度是通过 OS_Sched() 进行的。源码如下:
void OS_Sched (void)
{
#if OS_CRITICAL_METHOD == 3u /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0u;
#endif
OS_ENTER_CRITICAL();
if (OSIntNesting == 0u) { /* Schedule only if all ISRs done and ... */
if (OSLockNesting == 0u) { /* ... scheduler is not locked */
OS_SchedNew();
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */
#if OS_TASK_PROFILE_EN > 0u
OSTCBHighRdy->OSTCBCtxSwCtr++; /* Inc. # of context switches to this task */
#endif
OSCtxSwCtr++; /* Increment context switch counter */
OS_TASK_SW(); /* Perform a context switch */
}
}
}
OS_EXIT_CRITICAL();
}
这里的过程如下:
(1) 先通过 OS_SchedNew() 找到当前处于就绪状态的最高优先级的任务,如下:
y = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((y << 3u) + OSUnMapTbl[OSRdyTbl[y]]);
(2) 然后通过 OS_TASK_SW() 进行任务切换,它的过程如下:
1)OS_TASK_SW 只是一个宏,它实际替换的是 OSCtxSw()
#define OS_TASK_SW() OSCtxSw()
2)OSCtxSw()是由汇编实现的
OSCtxSw
PUSH {R4, R5}
LDR R4, =NVIC_INT_CTRL ;触发PendSV异常 (causes context switch)
LDR R5, =NVIC_PENDSVSET
STR R5, [R4]
POP {R4, R5}
BX LR
就这样,上下文就完成了一次切换。