本章给出μC/OS-Ⅱ的主要结构概貌:
- μC/OS-Ⅱ是怎样处理临界段代码的;
- 什么是任务,怎样把用户的任务交给μC/OS-Ⅱ;
- 任务是怎样调度的;
- 应用程序CPU的利用率是多少,μC/OS-Ⅱ是怎样知道的;
- 怎样写中断服务子程序;
- 什么是时钟节拍,μC/OS-Ⅱ是怎样处理时钟节拍的;
- μC/OS-Ⅱ是怎样初始化的,以及
- 怎样启动多任务;
和其它内核一样,μC/OS-Ⅱ为了处理临界段代码需要关中断,处理完毕后再开中断。
这使得μC/OS-Ⅱ能够避免同时有其它任务或中断服务进入临界段代码。
*关中断的时间是实时内核开发商应提供的最重要的指标之一,因为这个指标影响用户系统对实时事件的响应性。
μC/OS-Ⅱ努力使关中断时间降至最短,但就使用μC/OS-Ⅱ而言,关中断的时间很大程度上取决于微处理器的架构以及编译器所生成的代码质量。*
微处理器一般都有关中断/开中断指令,用户使用的C语言编译器必须有某种机制能够在C中直接实现关中断/开中断地操作。某些C编译器允许在用户的C源代码中插入汇编语言的语句。这使得插入微处理器指令来关中断/开中断很容易实现。而有的编译器把从C语言中关中断/开中断放在语言的扩展部分。
μC/OS-Ⅱ定义两个宏(macros)来关中断和开中断,以便避开不同C编译器厂商选择不同的方法来处理关中断和开中断。μC/OS-Ⅱ中的这两个宏调用分别是:OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()。因为这两个宏的定义取决于所用的微处理器,故在文件OS_CPU.H中可以找到相应宏定义。每种微处理器都有自己的OS_CPU.H文件。
一个任务通常是一个无限的循环。
void YourTask (void *pdata) (1)
{
for (;;) { (2)
/* 用户代码 */
调用uC/OS-II的某种系统服务:使任务交出cpu控制权
OSMboxPend();
OSQPend();
OSSemPend();
OSTaskDel(OS_PRIO_SELF);
OSTaskSuspend(OS_PRIO_SELF);
OSTimeDly();
OSTimeDlyHMSM();
/* 用户代码 */
}
}
这也是ucos任务架构。
当任务完成以后,任务可以自我删除。
注意任务代码并非真的删除了,μC/OS-Ⅱ只是简单地不再理会这个任务了,这个任务的代码也不会再运行。
任务的形式参数变量是由用户代码在第一次执行的时候带入的。请注意,该变量的类型是一个指向void的指针。这是为了允许用户应用程序传递任何类型的数据给任务,巧用可以起到很好的效果。
μC/OS-Ⅱ可以管理多达64个任务,但目前版本的μC/OS-Ⅱ有两个任务已经被系统占用了。作者保留了优先级为0、1、2、3、OS_LOWEST_PRIO-3、OS_LOWEST_PRI0-2,OS_LOWEST_PRI0-1以及OS_LOWEST_PRI0这8个任务以被将来使用。OS_LOWEST_PRI0是作为定义的常数在OS_CFG.H文件中用定义常数语句#define constant定义的。因此用户可以有多达56个应用任务。必须给每个任务赋以不同的优先级,优先级可以从0到OS_LOWEST_PR10-2。优先级号越低,任务的优先级越高。μC/OS-Ⅱ总是运行进入就绪态的优先级最高的任务。目前版本的μC/OS-Ⅱ中,任务的优先级号就是任务编号(ID)。优先级号(或任务的ID号)也被一些内核服务函数调用,如改变优先级函数OSTaskChangePrio(),以及任务删除函数OSTaskDel()。
在任一给定的时刻,任务的状态一定是在这五种状态之一。
一旦任务建立了,任务控制块OS_TCB将被赋值。
任务控制块是一个数据结构,
当任务的CPU使用权被剥夺时,μC/OS-Ⅱ用它来保存该任务的状态。
当任务重新得到CPU使用权时,任务控制块能确保任务从当时被中断的那一点丝毫不差地继续执行。
OS_TCB全部驻留在RAM中。
读者将会注意到笔者在组织这个数据结构时,考虑到了各成员的逻辑分组。任务建立的时候,OS_TCB就被初始化了。
详情见:
再读 ucosII源码(邵贝贝):内核结构–任务控制块(Task Control Blocks, OS_TCB)
每个任务的就绪态标志都放入就绪表中的,就绪表中有两个变量OSRdyGrp和OSRdyTbl[]。在OSRdyGrp中,任务按优先级分组,8个任务为一组。OSRdyGrp中的每一位表示8组任务中每一组中是否有进入就绪态的任务。任务进入就绪态时,就绪表OSRdyTbl[]中的相应元素的相应位也置位。就绪表OSRdyTbl[]数组的大小取决于OS_LOWEST_PRIO(见文件OS_CFG.H)。当用户的应用程序中任务数目比较少时,减少OS_LOWEST_PRIO的值可以降低μC/OS-Ⅱ对RAM(数据空间)的需求量。
为确定下次该哪个优先级的任务运行了,内核调度器总是将OS_LOWEST_PRIO在就绪表中相应字节的相应位置1。
详情参见:
uCOS 就绪表实现方法解析
μC/OS-Ⅱ总是运行进入就绪态任务中优先级最高的那一个。确定哪个任务优先级最高,下面该哪个任务运行了的工作是由调度器(Scheduler)完成的。任务级的调度是由函数OS_Sched()完成的。中断级的调度是由另一个函数OSIntExit()完成的,这个函数将在以后描述。
/*$PAGE*/
/*
*********************************************************************************************************
* SCHEDULER
*
* Description: This function is called by other uC/OS-II services to determine whether a new, high
* priority task has been made ready to run. This function is invoked by TASK level code
* and is not used to reschedule tasks from ISRs (see OSIntExit() for ISR rescheduling).
*
* Arguments : none
*
* Returns : none
*
* Notes : 1) This function is INTERNAL to uC/OS-II and your application should not call it.
* 2) Rescheduling is prevented when the scheduler is locked (see OS_SchedLock())
*********************************************************************************************************
*/
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();/*找到当前就绪表中的最高优先级*/
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];/*最高优先级对应的任务控制块*/
if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */
#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();
}
#define OS_TASK_SW() OSCtxSw()
;********************************************************************************************************
; PERFORM A CONTEXT SWITCH (From task level)
; void OSCtxSw(void)
;
; Note(s) : 1) OSCtxSw() is called when OS wants to perform a task context switch. This function
; triggers the PendSV exception which is where the real work is done.
;********************************************************************************************************
OSCtxSw
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
μC/OS-Ⅱ任务调度所花的时间是常数,与应用程序中建立的任务数无关。
第一条 条件语句的条件不满足,任务调度函数OS_Sched()将退出,不做任务调度。这个条件是:如果在中断服务子程序中调用OS_Sched(),此时中断嵌套层数OSIntNesting>0,或者由于用户至少调用了一次给任务调度上锁函数OS_SchedLock(),使OSLockNesting>0。
如果不是在中断服务子程序调用OS_Sched(),并且任务调度是允许的,即没有上锁,则任务调度函数将找出那个进入就绪态且优先级最高的任务,进入就绪态的任务在就绪任务表中有相应的位置位。一旦找到那个优先级最高的任务,OS_Sched()检验这个优先级最高的任务是不是当前正在运行的任务,以此来避免不必要的任务调度。
注意,在μC/OS中曾经是先得到OSTCBHighRdy然后和OSTCBCur做比较。因为这个比较是两个指针型变量的比较,在8位和一些16位微处理器中这种比较相对较慢。而在μC/OS-Ⅱ中是两个整数的比较。并且,除非用户实际需要做任务切换,在查任务控制块优先级表OSTCBPrioTbl[]时,不需要用指针变量来查OSTCBHighRdy。综合这两项改进,即用整数比较代替指针的比较和当需要任务切换时再查表,使得μC/OS-Ⅱ比μC/OS在8位和一些16位微处理器上要更快一些。
为实现任务切换,OSTCBHighRdy必须指向优先级最高的那个任务控制块OS_TCB,这是通过将以OSPrioHighRdy为下标的OSTCBPrioTbl[]数组中的那个元素赋给OSTCBHighRdy来实现的。接着,统计计数器OSCtxSwCtr加1,以跟踪任务切换次数。最后宏调用OS_TASK_SW()来完成实际上的任务切换。
任务切换很简单,由以下两步完成:
将被挂起任务的微处理器寄存器推入堆栈,
然后将较高优先级的任务的寄存器值从栈中恢复到寄存器中。
在μC/OS-Ⅱ中,就绪任务的栈结构总是看起来跟刚刚发生过中断一样,所有微处理器的寄存器都保存在栈中。换句话说,μC/OS-Ⅱ运行就绪态的任务所要做的一切,只是恢复所有的CPU寄存器并运行中断返回指令。为了做任务切换,运行OS_TASK_SW(),人为模仿了一次中断。多数微处理器有软中断指令或者陷阱指令TRAP来实现上述操作。中断服务子程序或陷阱处理(Trap hardler),也称作事故处理(exception handler),必须提供中断向量给汇编语言函数OSCtxSw()。OSCtxSw()除了需要OS_TCBHighRdy指向即将被挂起的任务,还需要让当前任务控制块OSTCBCur指向即将被挂起的任务,参见第8章,移植μC/OS-Ⅱ,有关于OSCtxSw()的更详尽的解释。
OS_Sched()的所有代码都属临界段代码。在寻找进入就绪态的优先级最高的任务过程中,为防止中断服务子程序把一个或几个任务的就绪位置位,中断是被关掉的。为缩短切换时间,OS_Sched()全部代码都可以用汇编语言写。为增加可读性,可移植性和将汇编语言代码最少化,OS_Sched()是用C写的。
给调度器上锁函数OSSchedlock()用于禁止任务调度,直到任务完成后调用给调度器开锁函数OSSchedUnlock()为止,
然而,此时中断是可以被识别的,中断服务也能得到(假设中断是开着的)。
OSSchedlock()和OSSchedUnlock()必须成对使用。
μC/OS-Ⅱ允许嵌套深度达255层。当OSLockNesting等于零时,调度重新得到允许。
函数OSSchedLock()和OSSchedUnlock()的使用要非常谨慎,因为它们影响μC/OS-Ⅱ对任务的正常管理。
调用OSSchedLock()以后,用户的应用程序不得使用任何能将现行任务挂起的系统调用。也就是说,用户程序不得调用OSMboxPend()、OSQPend()、OSSemPend()、OSTaskSuspend(OS_PR1O_SELF)、OSTimeDly()或OSTimeDlyHMSM(),直到OSLockNesting回零为止。因为调度器上了锁,用户就锁住了系统,任何其它任务都不能运行。
当低优先级的任务要发消息给多任务的邮箱、消息队列、信号量时(见第6章 任务间通讯和同步),用户不希望高优先级的任务在邮箱、队列和信号量没有得到消息之前就取得了CPU的控制权,此时,用户可以使用禁止调度器函数。
μC/OS-Ⅱ总是建立一个空闲任务,这个任务在没有其它任务进入就绪态时投入运行。这个空闲任务[OSTaskIdle()]永远设为最低优先级,即OS_LOWEST_PRI0。
空闲任务OSTaskIdle()什么也不做,只是在不停地给一个32位的名叫OSIdleCtr的计数器加1,
统计任务使用这个计数器以确定现行应用软件实际消耗的CPU时间。
空闲任务不可能被应用软件删除。
如果用户应用程序打算使用统计任务,用户必须在初始化时建立一个唯一的任务,在这个任务中调用OSStatInit()(见文件OS_CORE.C)。换句话说,在调用系统启动函数OSStart()之前,用户初始代码必须先建立一个任务,在这个任务中调用系统统计初始化函数OSStatInit(),然后再建立应用程序中的其它任务。程序清单L3.12是统计任务的示意性代码。
void main (void)
{
OSInit(); /* 初始化uC/OS-II (1)*/
/* 安装uC/OS-II的任务切换向量 */
/* 创建用户起始任务(为了方便讨论,这里以TaskStart()作为起始任务) (2)*/
OSStart(); /* 开始多任务调度 (3)*/
}
static void App_TaskStart (void *p_arg)
{
(void)p_arg;
BSP_Init(); /* Initialize BSP functions. */
OS_CPU_SysTickInit(); /* Initialize the SysTick. */
#if (OS_TASK_STAT_EN > 0)
OSStatInit(); /* Determine CPU capacity. */
#endif
App_EventCreate(); /* Create application events. */
App_TaskCreate(); /* Create application tasks. */
while(1){
OSTimeDly(100);
}
}
OSStatInit()缘何要处于这样的位置?书上说的很清楚,
参见:
ucos 统计任务 OS_TaskStat()
μC/OS中,中断服务子程序要用汇编语言来写。然而,如果用户使用的C语言编译器支持在线汇编语言的话,用户可以直接将中断服务子程序代码放在C语言的程序文件中。中断服务子程序的示意码如下:
用户中断服务子程序:
保存全部CPU寄存器; (1)
调用OSIntEnter或OSIntNesting直接加1; (2)
执行用户代码做中断服务; (3)
调用OSIntExit(); (4)
恢复所有CPU寄存器; (5)
执行中断返回指令; (6)
常用的处理器,中断发生时CPU寄存器是自动入栈的。
void OSIntEnter (void)
{
if (OSRunning == OS_TRUE) {
if (OSIntNesting < 255u) {
OSIntNesting++; /* Increment ISR nesting level */
}
}
}
μC/OS需要用户提供周期性信号源,用于实现时间延时和确认超时。节拍率应在每秒10次到100次之间,或者说10到100Hz。时钟节拍率越高,系统的额外负荷就越重。
时钟节拍的实际频率取决于用户应用程序的精度。时钟节拍源可以是专门的硬件定时器,也可以是来自50/60Hz交流电源的信号。
cortex-M3中,一般设置为1000Hz。
用户必须在多任务系统启动以后再开启时钟节拍器,也就是在调用OSStart()之后。换句话说,在调用OSStart()之后做的第一件事是初始化定时器中断。
void OS_CPU_SysTickHandler (void)
{
OS_CPU_SR cpu_sr;
OS_ENTER_CRITICAL(); /* Tell uC/OS-II that we are starting an ISR */
OSIntNesting++;
OS_EXIT_CRITICAL();
OSTimeTick(); /* Call uC/OS-II's OSTimeTick() */
OSIntExit(); /* Tell uC/OS-II that we are leaving the ISR */
}
OSTimtick()中量大的工作是给每个用户任务控制块OS_TCB中的时间延时项OSTCBDly减1(如果该项不为零的话)。
OSTimTick()从OSTCBList开始,沿着OS_TCB链表做,一直做到空闲任务[L3.21(3)]。当某任务的任务控制块中的时间延时项OSTCBDly减到了零,这个任务就进入了就绪态[L3.21(5)]。而确切被任务挂起的函数OSTaskSuspend()挂起的任务则不会进入就绪态[L3.21(4)]。OSTimTick()的执行时间直接与应用程序中建立了多少个任务成正比。
程序清单 L3.21 时钟节拍函数 OSTimtick() 的一个节拍服务
void OSTimeTick (void)
{
OS_TCB *ptcb;
OSTimeTickHook(); (1)
ptcb = OSTCBList; (2)
while (ptcb->OSTCBPrio != OS_IDLE_PRIO) { (3)
OS_ENTER_CRITICAL();
if (ptcb->OSTCBDly != 0) {
if (--ptcb->OSTCBDly == 0) {
if (!(ptcb->OSTCBStat & OS_STAT_SUSPEND)) { (4)
OSRdyGrp |= ptcb->OSTCBBitY; (5)
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX;
} else {
ptcb->OSTCBDly = 1;
}
}
}
ptcb = ptcb->OSTCBNext;
OS_EXIT_CRITICAL();
}
OS_ENTER_CRITICAL(); (6)
OSTime++; (7)
OS_EXIT_CRITICAL();
}
断服务子程序似乎就得写这么长,如果用户不喜欢将中断服务程序写这么长,可以从任务级调用OSTimeTick(),如程序清单L3.22所示。要想这么做,得建立一个高于应用程序中所有其它任务优先级的任务。时钟节拍中断服务子程序利用信号量或邮箱发信号给这个高优先级的任务。
程序清单 L3.22 时钟节拍任务 TickTask() 作时钟节拍服务.
void TickTask (void *pdata)
{
pdata = pdata;
for (;;) {
OSMboxPend(...); /* 等待从时钟节拍中断服务程序发来的信号 */
OSTimeTick();
}
}
在调用μC/OS-Ⅱ的任何其它服务之前,μC/OS-Ⅱ要求用户首先调用系统初始化函数OSIint()。OSIint()初始化μC/OS-Ⅱ所有的变量和数据结构(见OS_CORE.C)。
OSInit()建立空闲任务idle task,这个任务总是处于就绪态的。空闲任务OSTaskIdle()的优先级总是设成最低,即OS_LOWEST_PRIO。如果统计任务允许OS_TASK_STAT_EN和任务建立扩展允许都设为1,则OSInit()还得建立统计任务OSTaskStat()并且让其进入就绪态。OSTaskStat的优先级总是设为OS_LOWEST_PRIO-1。
图F3.7表示调用OSInit()之后,一些μC/OS-Ⅱ变量和数据结构之间的关系。其解释是基于以下假设的:
多任务的启动是用户通过调用OSStart()实现的。然而,启动μC/OS-Ⅱ之前,用户至少要建立一个应用任务,
OSStart()的代码如程序清单L3.25所示。当调用OSStart()时,OSStart()从任务就绪表中找出那个用户建立的优先级最高任务的任务控制块[L3.25(1)]。然后,OSStart()调用高优先级就绪任务启动函数OSStartHighRdy()[L3,25(2)],(见汇编语言文件OS_CPU_A.ASM),这个文件与选择的微处理器有关。实质上,函数OSStartHighRdy()是将任务栈中保存的值弹回到CPU寄存器中,然后执行一条中断返回指令,中断返回指令强制执行该任务代码。见9.04.01节,高优先级就绪任务启动函数OSStartHighRdy()。那一节详细介绍对于80x86微处理器是怎么做的。注意,OSStartHighRdy()将永远不返回到OSStart()。
程序清单 L3.25 启动多任务:
void OSStart (void)
{
INT8U y;
INT8U x;
if (OSRunning == FALSE) {
y = OSUnMapTbl[OSRdyGrp];
x = OSUnMapTbl[OSRdyTbl[y]];
OSPrioHighRdy = (INT8U)((y << 3) + x);
OSPrioCur = OSPrioHighRdy;
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; (1)
OSTCBCur = OSTCBHighRdy;
OSStartHighRdy(); (2)
}
}
设置PendSV 系统异常为最低优先级0xff=255,==实现中断的实时响应。
NVIC_PENDSV_PRI EQU 0xFF ; PendSV priority value (lowest).
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
多任务启动以后变量与数据结构中的内容如图F3.9所示。这里笔者假设用户建立的任务优先级为6,注意,OSTaskCtr指出已经建立了3个任务。OSRunning已设为“真”,指出多任务已经开始,OSPrioCur和OSPrioHighRdy存放的是用户应用任务的优先级,OSTCBCur和OSTCBHighRdy二者都指向用户任务的任务控制块。
略过
读者或许注意到有4个OS_CORE.C中的函数没有在本章中提到。这4个函数是OSEventWaitListInit(),OSEventTaskRdy(),OSEventTaskWait(),OSEventTO()。这几个函数是放在文件OS_CORE.C中的,而对如何使用这个函数的解释见第6章,任务间的通讯与同步。。。。。。。。。