多任务操作系统的核心工作就是任务调度。所谓调度,就是通过一个算法在多个任务中确定该运行的任务,做这项工作的函数就叫做调度器。μC/OS-II进行任务调度的思想是 “近似地每时每刻总是让优先级最高的就绪任务处于运行状态” 。为了保证这一点,μC/OS-II在系统或用户任务调用系统函数及执行中断服务程序结束时总是调用调度器,来确定应该运行的任务并运行它 。
μC/OS-Ⅱ的调度器主要有两个功能:一是确定进入就绪态的任务中哪个优先级最高;二是进行任务切换。调度有两种方式:任务级的调度是由OSSched()
函数完成的;中断级的调度是由OSIntExt()
函数完成的。
OSSched()
(基于ARM Cortex-M3)。void OS_Sched (void)
{
#if OS_CRITICAL_METHOD == 3 /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0;
#endif
OS_ENTER_CRITICAL();
if (OSIntNesting == 0) { /* Schedule only if all ISRs done and ... */
if (OSLockNesting == 0) { /* ... scheduler is not locked */
OS_SchedNew();
if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
#if OS_TASK_PROFILE_EN > 0
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) 采用第三种方法开关中断。OSSched()
函数的所有代码都属临界区代码。为了防止在处理的过程中有中断打入而将某些任务转入就绪状态,中断必须关闭。
(2) 若调度是禁止的(即调度上锁),或当前处于中断服务子程序中,任务的调度是不允许的,任务调度函数OSSched()
将退出。调度锁定嵌套计数器OSLockNesting
用于跟踪调度上锁次数,当OSLockNesting>0
时,表明调度是禁止的;只有OSLockNesting=0
时,调度才是允许的,也称为调度开锁。中断嵌套计数器OSIntNesting
用于跟踪中断嵌套层数,只要一进入中断服务,就有OSIntNesting>0
;只有所有中断都退出时,才有OSIntNesting = 0
。
(3) 若调度是开放的且程序不处于中断服务之中,则调用OS_SchedNew()
函数找出准备就绪且优先级最高的任务。
(4) 若准备就绪的最高优先级任务不是当前正在运行的任务,就要进行任务切换。为实现任务切换,必须切换任务控制块,将OSTCBHighRdy
指向优先级最高的那个任务控制块OS_TCB。并且计数器OSCtxSwCtr
加1,以跟踪任务切换次数。
(5)任务切换是通过一个宏调用OS_TASK_SW()
来完成的。任务切换很简单,实际上就是人为地模拟一次中断过程。它分两步完成,首先将被挂起的任务的CPU寄存器推入堆栈,然后将准备就绪的最高优先级任务的寄存器值从任务栈中恢复到寄存器中。在μC/OS-Ⅱ中,任务的栈结构就是模拟中断栈结构,所有CPU寄存器都保存在栈中,切换任务就像中断返回,不同的是:中断返回弹出的是被中断任务的CPU寄存器值,而任务切换弹出的是准备就绪的最高优先级任务的CPU寄存器值,这样准备就绪的高优先级任务就可以运行了。
以下这里重点分析一下OS_TASK_SW()
函数在ARM Cortex-M3处理器上的实现,OS_TASK_SW()
是汇编函数OSCtxSw()
的宏定义
#define OS_TASK_SW() OSCtxSw()
如下是OSCtxSw()
的汇编源码,该段代码的作用是触发一次PendSV异。任务切换实际上是在PendSV的异常服务程序中完成的。
OSCtxSw
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
PendSV的异常服务程序汇编源码如下
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
SUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stack
STM R0, {R4-R11}
LDR R1, =OSTCBCur ; OSTCBCur->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, =OSTCBCur ; OSTCBCur = OSTCBHighRdy;
LDR R1, =OSTCBHighRdy
LDR R2, [R1]
STR R2, [R0]
LDR R0, [R2] ; R0 is new process SP; SP = OSTCBHighRdy->OSTCBStkPtr;
LDM R0, {R4-R11} ; Restore r4-11 from new process stack
ADDS R0, R0, #0x20
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
uC/OS-II采用软中断指令触发PendSV异常来实现任务的切换。
1. 获取当前任务的堆栈指针PSP
。如果PSP=0
,判断为第一次任务切换,跳至OS_CPU_PendSVHandler_nosave
;如果PSP!=0
,继续执行步骤2。如果任务第一次运行,则PSP=0这里写代码片
;如果不是第一运行,则PSP=ptos-0x20
。因为CM3在进入PendSV异常服务函数时会自动将xPSR、PC、LR、R12、R3--R0
依次保存金PSP=ptos
所指向的堆栈。上述寄存器入栈后PSP=ptos-#0x20
。
2. 将R11--R4
压入当前任务堆栈。PSP=ptos-#0x40
。
3. 将PSP
保存到当前任务控制块中OSTCBCur->OSTCBStkPtr = PSP
。
以下为OS_CPU_PendSVHandler_nosave
4. 调用钩子函数OSTaskSwHook();
。
5. 获得待运行任务的任务控制块OSTCBCur = OSTCBHighRdy;
。
6. PSP
指向待运行任务中保存的堆栈地址PSP=STCBHighRdy->OSTCBStkPtr;
执行后PSP=ptos-0x40;
。
7. 将R4--R11
依次出栈。PSP=ptos-#0x20
。
8. 异常返回,返回时将R0--R3、R12、LR、PC、xPSR
依次出栈。
======= 任务级任务调度就分析这么多吧!!!========
下面分析程序如何开始运行第一个任务,其中包括PendSV异常优先级设定的汇编代码,PendSV优先级设置为最低0xFF。
uC/OS-II一般在OSStart()
前先创建一个启动任务,但是这个任务并没有运行,因为OSRunning == OS_FALSE
。执行OSStart()
触发第一次任务调度让系统有任务跑起来。
void OSStart (void)
{
if (OSRunning == OS_FALSE) {
OS_SchedNew(); /* Find highest priority's task priority number */
OSPrioCur = OSPrioHighRdy;
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; /* Point to highest priority task ready to run */
OSTCBCur = OSTCBHighRdy;
OSStartHighRdy(); /* Execute target specific code to start task */
}
}
执行OSStartHighRdy();
相当于一次任务调度。
1. 设置PendSV异常优先级为最低0xFF。
2. 初始化PSP=0;
,标志第一次执行任务调度。
3. 设置OSRunning = TRUE
。
4. 触发一次PendSV异常,执行PendSV异常服务程序OS_CPU_PendSVHandler
OSStartHighRdy
LDR R0, =NVIC_SYSPRI14 ; Set the PendSV exception priority
LDR R1, =NVIC_PENDSV_PRI
STRB R1, [R0]
MOVS R0, #0 ; Set the PSP to 0 for initial context switch call
MSR PSP, R0
LDR R0, =OSRunning ; OSRunning = TRUE
MOVS R1, #1
STRB R1, [R0]
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
CPSIE I ; Enable interrupts at processor level
OSStartHang
B OSStartHang ; Should never get here