uCOS-III笔记之任务调度
宇宙是依靠相互之间的万有引力保持着万物的运行状态,人是依靠心脏的跳动泵血维持着鲜活的生命,汽车是依靠发送机不停地爆发出能量保证运行。那么我们的uCOS又是怎么被有启动又是怎么被有无止境的运行起来的呢。
一、任务调度的动力
1、下面我们就从一个重要的中断开始:系统滴答 SysTick_Handler开始。
void OS_CPU_SysTickHandler (void)
{
CPU_SR_ALLOC();
CPU_CRITICAL_ENTER();
OSIntNestingCtr++; /* Tell uC/OS-III that we are starting an ISR */
CPU_CRITICAL_EXIT();
OSTimeTick(); /* Call uC/OS-III's OSTimeTick() */
OSIntExit(); /* Tell uC/OS-III that we are leaving the ISR */
}
这个就是我们ucos能够维持运行进行任务调度的原动力。中断首先是进行一个时间戳的累加OSIntNestingCtr++,我们可以根据这个时间戳来做时间标识。这里我们重点看一下OSIntCtxSw()这个函数,它在哪呢让我们找到他。
NVIC_INT_CTRL EQU 0xE000ED04 ; Interrupt control state register.
NVIC_SYSPRI14 EQU 0xE000ED22 ; System priority register (priority 14).
NVIC_PENDSV_PRI EQU 0xFF ; PendSV priority value (lowest).
NVIC_PENDSVSET EQU 0x10000000 ; Value to trigger PendSV exception.
;********************************************************************************************************
; PERFORM A CONTEXT SWITCH (From interrupt level) - OSIntCtxSw()
;
; Note(s) : 1) OSIntCtxSw() is called by OSIntExit() when it determines a context switch is needed as
; the result of an interrupt. This function simply triggers a PendSV exception which will
; be handled when there are no more interrupts active and interrupts are enabled.
;********************************************************************************************************
OSIntCtxSw
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
2、PendSV正式切换任务
这其实是一个汇编指令,看说明我们可以知道他就是启动一个PendSV中断,看代码STR R1,[R0] ,就是将NVIC_PENDSVSET的值写到NVIC_INT_CTRL寄存器中。
那么再来看看PendSV这个重要的中断,它主要是完成上下文切换,然后就是完成就绪任务的切换。这里有一个比较主要的钩子函数 OSTaskSwHook
;********************************************************************************************************
; HANDLE PendSV EXCEPTION
; void OS_CPU_PendSVHandler(void)
;
; Note(s) : 1) PendSV is used to cause a context switch. This is a recommended method for performing
; context switches with Cortex-M3. This is because the Cortex-M3 auto-saves half of the
; processor context on any exception, and restores same on return from exception. So only
; saving of R4-R11 is required and fixing up the stack pointers. Using the PendSV exception
; this way means that context saving and restoring is identical whether it is initiated from
; a thread or occurs due to an interrupt or exception.
;
; 2) Pseudo-code is:
; a) Get the process SP, if 0 then skip (goto d) the saving part (first context switch);
; b) Save remaining regs r4-r11 on process stack;
; c) Save the process SP in its TCB, OSTCBCurPtr->OSTCBStkPtr = SP;
; d) Call OSTaskSwHook();
; e) Get current high priority, OSPrioCur = OSPrioHighRdy;
; f) Get current ready thread TCB, OSTCBCurPtr = OSTCBHighRdyPtr;
; g) Get new process SP from TCB, SP = OSTCBHighRdyPtr->OSTCBStkPtr;
; h) Restore R4-R11 from new process stack;
; i) Perform exception return which will restore remaining context.
;
; 3) On entry into PendSV handler:
; a) The following have been saved on the process stack (by processor):
; xPSR, PC, LR, R12, R0-R3
; b) Processor mode is switched to Handler mode (from Thread mode)
; c) Stack is Main stack (switched from Process stack)
; d) OSTCBCurPtr points to the OS_TCB of the task to suspend
; OSTCBHighRdyPtr points to the OS_TCB of the task to resume
;
; 4) Since PendSV is set to lowest priority in the system (by OSStartHighRdy() above), we
; know that it will only be run when no other exception or interrupt is active, and
; therefore safe to assume that context being switched out was using the process stack (PSP).
;********************************************************************************************************
OS_CPU_PendSVHandler
CPSID I ; Prevent interruption during context switch
MRS R0, PSP ; PSP is process stack pointer
CBZ R0, OS_CPU_PendSVHandler_nosave ; Skip register save the first time
;if enable the FPU
SUBS R0, R0, #0X40
VSTM R0, {S16-S31}
SUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stack
STM R0, {R4-R11}
LDR R1, =OSTCBCurPtr ; OSTCBCurPtr->OSTCBStkPtr = SP;
LDR R1, [R1]
STR R0, [R1] ; R0 is SP of process being switched out
; At this point, entire context of process has been saved
OS_CPU_PendSVHandler_nosave
PUSH {R14} ; Save LR exc_return value
LDR R0, =OSTaskSwHook ; OSTaskSwHook();
BLX R0
POP {R14}
LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy;
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
LDR R0, =OSTCBCurPtr ; OSTCBCurPtr = OSTCBHighRdyPtr;
LDR R1, =OSTCBHighRdyPtr
LDR R2, [R1]
STR R2, [R0]
LDR R0, [R2] ; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr;
LDM R0, {R4-R11} ; Restore r4-11 from new process stack
ADDS R0, R0, #0x20
;if enable FPU
VLDM R0, {S16-S31}
ADDS R0, R0, #0X40
MSR PSP, R0 ; Load PSP with new process SP
ORR LR, LR, #0x04 ; Ensure exception return uses process stack
CPSIE I
BX LR ; Exception return will restore remaining context
END
在这个Hook中主要有两个很重要的变量那就是 OSTCBCurPtr 和 OSTCBHighRdyPtr。两个都是OS_TCB 任务控制块,
其中有三个时钟变量:CyclesDelta(当前运行过的时间)、CyclesStart(任务运行时当前时钟)、CyclesTotal(任务运行的时长),
结果就是:经过这里将时钟设置好再通过上面的中断就可以将就绪的最高优先级的任务控制块OSTCBHighRdyPtr放入当前的任务控制块OSTCBCurPtr 中。那么该切换的任务就这么切换了。
void OSTaskSwHook (void)
{
#if OS_CFG_TASK_PROFILE_EN > 0u
CPU_TS ts;
#endif
#ifdef CPU_CFG_INT_DIS_MEAS_EN
CPU_TS int_dis_time;
#endif
#if OS_CFG_APP_HOOKS_EN > 0u
if (OS_AppTaskSwHookPtr != (OS_APP_HOOK_VOID)0) {
(*OS_AppTaskSwHookPtr)();
}
#endif
#if OS_CFG_TASK_PROFILE_EN > 0u
ts = OS_TS_GET();
if (OSTCBCurPtr != OSTCBHighRdyPtr) {
OSTCBCurPtr->CyclesDelta = ts - OSTCBCurPtr->CyclesStart;
OSTCBCurPtr->CyclesTotal += (OS_CYCLES)OSTCBCurPtr->CyclesDelta;
}
OSTCBHighRdyPtr->CyclesStart = ts;
#endif
#ifdef CPU_CFG_INT_DIS_MEAS_EN
int_dis_time = CPU_IntDisMeasMaxCurReset(); /* Keep track of per-task interrupt disable time */
if (OSTCBCurPtr->IntDisTimeMax < int_dis_time) {
OSTCBCurPtr->IntDisTimeMax = int_dis_time;
}
#endif
#if OS_CFG_SCHED_LOCK_TIME_MEAS_EN > 0u
/* Keep track of per-task scheduler lock time */
if (OSTCBCurPtr->SchedLockTimeMax < OSSchedLockTimeMaxCur) {
OSTCBCurPtr->SchedLockTimeMax = OSSchedLockTimeMaxCur;
}
OSSchedLockTimeMaxCur = (CPU_TS)0; /* Reset the per-task value */
#endif
}
二、就绪任务
就绪任务的来源我们知道了调度就是把当前就绪任务切换为运行状态,那么问题来的这个就绪任务是如何来的呢。下面就来一步步的分析到底在什么情况下这个OSTCBHighRdyPtr会发生改变。
那就要看这个任务调度的函数,我们可以看到一个获取最高优先级的函数OS_PrioGetHighest,通过这个函数我们在OSRdyList这个就绪列表中得到需要运行的任务控制块。
struct os_rdy_list {
OS_TCB *HeadPtr; /* Pointer to task that will run at selected priority */
OS_TCB *TailPtr; /* Pointer to last task at selected priority */
OS_OBJ_QTY NbrEntries; /* Number of entries at selected priority */
};
如何获取最高优先级的就绪任务呢,看一下这个函数。其中有一个CPU_CntLeadZeros的函数被调用了。这个其实有两种方法的一种就是处理器支持指令集的计算前导零,就是用汇编质量即可,另一种则要通过c代码实现,下面是以32位写的代码。
OS_PRIO OS_PrioGetHighest (void)
{
CPU_DATA *p_tbl;
OS_PRIO prio;
prio = (OS_PRIO)0;
p_tbl = &OSPrioTbl[0];
while (*p_tbl == (CPU_DATA)0) { /* Search the bitmap table for the highest priority */
prio += DEF_INT_CPU_NBR_BITS; /* Compute the step of each CPU_DATA entry */
p_tbl++;
}
prio += (OS_PRIO)CPU_CntLeadZeros(*p_tbl); /* Find the position of the first bit set at the entry */
return (prio);
}
汇编方法:
CPU_CntLeadZeros
CLZ R0, R0 ; Count leading zeros
BX LR
C方法:
CPU_DATA CPU_CntLeadZeros32 (CPU_INT32U val)
{
#if (!((defined(CPU_CFG_LEAD_ZEROS_ASM_PRESENT)) && \
(CPU_CFG_DATA_SIZE >= CPU_WORD_SIZE_32)))
CPU_DATA ix;
#endif
CPU_DATA nbr_lead_zeros;
/* ---------- ASM-OPTIMIZED ----------- */
#if ((defined(CPU_CFG_LEAD_ZEROS_ASM_PRESENT)) && \
(CPU_CFG_DATA_SIZE >= CPU_WORD_SIZE_32))
nbr_lead_zeros = CPU_CntLeadZeros((CPU_DATA)val);
nbr_lead_zeros -= (CPU_CFG_DATA_SIZE - CPU_WORD_SIZE_32) * DEF_OCTET_NBR_BITS;
#else /* ----------- C-OPTIMIZED ------------ */
if (val > 0x0000FFFFu) {
if (val > 0x00FFFFFFu) { /* Chk bits [31:24] : */
/* .. Nbr lead zeros = .. */
ix = (CPU_DATA)(val >> 24u); /* .. lookup tbl ix = 'val' >> 24 bits */
nbr_lead_zeros = (CPU_DATA)(CPU_CntLeadZerosTbl[ix] + 0u); /* .. plus nbr msb lead zeros = 0 bits.*/
} else { /* Chk bits [23:16] : */
/* .. Nbr lead zeros = .. */
ix = (CPU_DATA)(val >> 16u); /* .. lookup tbl ix = 'val' >> 16 bits */
nbr_lead_zeros = (CPU_DATA)(CPU_CntLeadZerosTbl[ix] + 8u); /* .. plus nbr msb lead zeros = 8 bits.*/
}
} else {
if (val > 0x000000FFu) { /* Chk bits [15:08] : */
/* .. Nbr lead zeros = .. */
ix = (CPU_DATA)(val >> 8u); /* .. lookup tbl ix = 'val' >> 8 bits */
nbr_lead_zeros = (CPU_DATA)(CPU_CntLeadZerosTbl[ix] + 16u); /* .. plus nbr msb lead zeros = 16 bits.*/
} else { /* Chk bits [07:00] : */
/* .. Nbr lead zeros = .. */
ix = (CPU_DATA)(val >> 0u); /* .. lookup tbl ix = 'val' >> 0 bits */
nbr_lead_zeros = (CPU_DATA)(CPU_CntLeadZerosTbl[ix] + 24u); /* .. plus nbr msb lead zeros = 24 bits.*/
}
}
#endif
return (nbr_lead_zeros);
}
三、就绪任务的管理
1、回归到void OSInit (OS_ERR *p_err) 来看一下,在初始化中已经创建了几个任务,其中有一个是OS_TickTaskInit() 创建Tick任务,这是一个怎么的任务呢。
void OSInit (OS_ERR *p_err)
{
...
#if OS_CFG_ISR_POST_DEFERRED_EN > 0u
OS_IntQTaskInit(p_err); /* Initialize the Interrupt Queue Handler Task */
if (*p_err != OS_ERR_NONE) {
return;
}
#endif
OS_IdleTaskInit(p_err); /* Initialize the Idle Task */
if (*p_err != OS_ERR_NONE) {
return;
}
OS_TickTaskInit(p_err); /* Initialize the Tick Task */
if (*p_err != OS_ERR_NONE) {
return;
}
#if OS_CFG_STAT_TASK_EN > 0u /* Initialize the Statistic Task */
OS_StatTaskInit(p_err);
if (*p_err != OS_ERR_NONE) {
return;
}
#endif
...
}
OS_TickTaskInit:进入一看发现是创建了一个优先级为OSCfg_TickTaskPrio的OS_TickTask任务,这个优先级竟然是1,看到这里我们就有点眉目了,这个Tick任务Pend一个信号量,然后调用一个看名字就能知道什么用的函数OS_TickListUpdate();就是用来更新就绪任务表的函数。现在我们就去找在哪里给这个任务发了一个信号量。
void OS_TickTask (void *p_arg)
{
OS_ERR err;
CPU_TS ts;
p_arg = p_arg; /* Prevent compiler warning */
while (DEF_ON) {
(void)OSTaskSemPend((OS_TICK )0,
(OS_OPT )OS_OPT_PEND_BLOCKING,
(CPU_TS *)&ts,
(OS_ERR *)&err); /* Wait for signal from tick interrupt */
if (err == OS_ERR_NONE) {
if (OSRunning == OS_STATE_OS_RUNNING) {
OS_TickListUpdate(); /* Update all tasks waiting for time */
}
}
}
}
void OS_TickListUpdate (void)
{
CPU_BOOLEAN done;
OS_TICK_SPOKE *p_spoke;
OS_TCB *p_tcb;
OS_TCB *p_tcb_next;
OS_TICK_SPOKE_IX spoke;
CPU_TS ts_start;
CPU_TS ts_end;
CPU_SR_ALLOC();
OS_CRITICAL_ENTER();
ts_start = OS_TS_GET();
OSTickCtr++; /* Keep track of the number of ticks */
spoke = (OS_TICK_SPOKE_IX)(OSTickCtr % OSCfg_TickWheelSize);
p_spoke = &OSCfg_TickWheel[spoke];
p_tcb = p_spoke->FirstPtr;
done = DEF_FALSE;
while (done == DEF_FALSE) {
if (p_tcb != (OS_TCB *)0) {
p_tcb_next = p_tcb->TickNextPtr; /* Point to next TCB to update */
switch (p_tcb->TaskState) {
case OS_TASK_STATE_RDY:
case OS_TASK_STATE_PEND:
case OS_TASK_STATE_SUSPENDED:
case OS_TASK_STATE_PEND_SUSPENDED:
break;
case OS_TASK_STATE_DLY:
p_tcb->TickRemain = p_tcb->TickCtrMatch /* Compute time remaining of current TCB */
- OSTickCtr;
if (OSTickCtr == p_tcb->TickCtrMatch) { /* Process each TCB that expires */
p_tcb->TaskState = OS_TASK_STATE_RDY;
OS_TaskRdy(p_tcb); /* Make task ready to run */
} else {
done = DEF_TRUE; /* Don't find a match, we're done! */
}
break;
case OS_TASK_STATE_PEND_TIMEOUT:
p_tcb->TickRemain = p_tcb->TickCtrMatch /* Compute time remaining of current TCB */
- OSTickCtr;
if (OSTickCtr == p_tcb->TickCtrMatch) { /* Process each TCB that expires */
#if (OS_MSG_EN > 0u)
p_tcb->MsgPtr = (void *)0;
p_tcb->MsgSize = (OS_MSG_SIZE)0u;
#endif
p_tcb->TS = OS_TS_GET();
OS_PendListRemove(p_tcb); /* Remove from wait list */
OS_TaskRdy(p_tcb);
p_tcb->TaskState = OS_TASK_STATE_RDY;
p_tcb->PendStatus = OS_STATUS_PEND_TIMEOUT; /* Indicate pend timed out */
p_tcb->PendOn = OS_TASK_PEND_ON_NOTHING; /* Indicate no longer pending */
} else {
done = DEF_TRUE; /* Don't find a match, we're done! */
}
break;
case OS_TASK_STATE_DLY_SUSPENDED:
p_tcb->TickRemain = p_tcb->TickCtrMatch /* Compute time remaining of current TCB */
- OSTickCtr;
if (OSTickCtr == p_tcb->TickCtrMatch) { /* Process each TCB that expires */
p_tcb->TaskState = OS_TASK_STATE_SUSPENDED;
OS_TickListRemove(p_tcb); /* Remove from current wheel spoke */
} else {
done = DEF_TRUE; /* Don't find a match, we're done! */
}
break;
case OS_TASK_STATE_PEND_TIMEOUT_SUSPENDED:
p_tcb->TickRemain = p_tcb->TickCtrMatch /* Compute time remaining of current TCB */
- OSTickCtr;
if (OSTickCtr == p_tcb->TickCtrMatch) { /* Process each TCB that expires */
#if (OS_MSG_EN > 0u)
p_tcb->MsgPtr = (void *)0;
p_tcb->MsgSize = (OS_MSG_SIZE)0u;
#endif
p_tcb->TS = OS_TS_GET();
OS_PendListRemove(p_tcb); /* Remove from wait list */
OS_TickListRemove(p_tcb); /* Remove from current wheel spoke */
p_tcb->TaskState = OS_TASK_STATE_SUSPENDED;
p_tcb->PendStatus = OS_STATUS_PEND_TIMEOUT; /* Indicate pend timed out */
p_tcb->PendOn = OS_TASK_PEND_ON_NOTHING; /* Indicate no longer pending */
} else {
done = DEF_TRUE; /* Don't find a match, we're done! */
}
break;
default:
break;
}
p_tcb = p_tcb_next;
} else {
done = DEF_TRUE;
}
}
ts_end = OS_TS_GET() - ts_start; /* Measure execution time of tick task */
if (OSTickTaskTimeMax < ts_end) {
OSTickTaskTimeMax = ts_end;
}
OS_CRITICAL_EXIT();
}
2、OSSched()
这个调度函数,只要涉及到应用层接口修改到优先级的函数中都会调用它,来完成任务的切换。
void OSSched (void)
{
CPU_SR_ALLOC();
if (OSIntNestingCtr > (OS_NESTING_CTR)0) { /* ISRs still nested? */
return; /* Yes ... only schedule when no nested ISRs */
}
if (OSSchedLockNestingCtr > (OS_NESTING_CTR)0) { /* Scheduler locked? */
return; /* Yes */
}
CPU_INT_DIS();
OSPrioHighRdy = OS_PrioGetHighest(); /* Find the highest priority ready */
OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;
if (OSTCBHighRdyPtr == OSTCBCurPtr) { /* Current task is still highest priority task? */
CPU_INT_EN(); /* Yes ... no need to context switch */
return;
}
#if OS_CFG_TASK_PROFILE_EN > 0u
OSTCBHighRdyPtr->CtxSwCtr++; /* Inc. # of context switches to this task */
#endif
OSTaskCtxSwCtr++; /* Increment context switch counter */
#if defined(OS_CFG_TLS_TBL_SIZE) && (OS_CFG_TLS_TBL_SIZE > 0u)
OS_TLS_TaskSw();
#endif
OS_TASK_SW(); /* Perform a task level context switch */
CPU_INT_EN();
}
四、更新任务就绪链表的导火索
有没有见过这个函数,没错这个函数就是前面看到的void OS_CPU_SysTickHandler (void)系统时钟里面的一个函数,到这里基本就可以连起来了。
void OSTimeTick (void)
{
OS_ERR err;
#if OS_CFG_ISR_POST_DEFERRED_EN > 0u
CPU_TS ts;
#endif
OSTimeTickHook(); /* Call user definable hook */
#if OS_CFG_ISR_POST_DEFERRED_EN > 0u
ts = OS_TS_GET(); /* Get timestamp */
OS_IntQPost((OS_OBJ_TYPE) OS_OBJ_TYPE_TICK, /* Post to ISR queue */
(void *)&OSRdyList[OSPrioCur],
(void *) 0,
(OS_MSG_SIZE) 0u,
(OS_FLAGS ) 0u,
(OS_OPT ) 0u,
(CPU_TS ) ts,
(OS_ERR *)&err);
#else
(void)OSTaskSemPost((OS_TCB *)&OSTickTaskTCB, /* Signal tick task */
(OS_OPT ) OS_OPT_POST_NONE,
(OS_ERR *)&err);
#if OS_CFG_SCHED_ROUND_ROBIN_EN > 0u
OS_SchedRoundRobin(&OSRdyList[OSPrioCur]);
#endif
#if OS_CFG_TMR_EN > 0u
OSTmrUpdateCtr--;
if (OSTmrUpdateCtr == (OS_CTR)0u) {
OSTmrUpdateCtr = OSTmrUpdateCnt;
OSTaskSemPost((OS_TCB *)&OSTmrTaskTCB, /* Signal timer task */
(OS_OPT ) OS_OPT_POST_NONE,
(OS_ERR *)&err);
}
#endif
#endif
}
五、总结:
1、系统主动调度:系统时钟到来调用 OSTimeTick() -------> 发出一个OSTaskSemPost(OSTickTaskTCB...)信号量 ----->触发OS_TickTask更新就绪任务链表 -------> 然后调用OSIntExit()进行相关的上下文保护以及任务切换
2:用户调度:主要是通过这个函数OSSched()这个函数遍布整个ucos系统,不管是在任务的创建、删除、挂起、就绪..;还是在信号量的创建、删除、发送、就绪....;还是在消息的各种操作函数。。。都会用到。都可能会有任务的切换可能。