1. 任务控制块TCB(Task Control Block)
TCB结构体定义如下:
typedef struct os_tcb {
OS_STK *OSTCBStkPtr; /* 任务堆栈栈顶地址-将其放于第一位是应为UCOS对栈的操作是使用汇编指令,这样有利于汇编指令的书写 */
#if OS_TASK_CREATE_EXT_EN > 0 //os_cfg.h中开启任务创建扩展功能
void *OSTCBExtPtr; /* Pointer to user definable data for TCB extension 用户自定义TCB扩展数据指针 */
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) 任务ID号 */
#endif
struct os_tcb *OSTCBNext; /* Pointer to next TCB in the TCB list TCB后驱节点地址(可能位于空闲链表或者就绪链表) */
struct os_tcb *OSTCBPrev; /* Pointer to previous TCB in the TCB list TCB前驱结点地址 */
//消息、队列、信号量等都用到事件控制块,所以均定义有事件控制块的指针变量OS_EVENT *
#if (OS_EVENT_EN) || (OS_FLAG_EN > 0)
OS_EVENT *OSTCBEventPtr; /* Pointer to event control block */ //任务在一个时刻只能被一种
消息、队列、信号量其中一种时间所阻塞,这里记录阻塞任务的ECB地址,用于任务修改优先级时需要修改相应ECB等待表,或者删除任务时,判断任务是否被阻塞
#endif
#if (OS_EVENT_EN) && (OS_EVENT_MULTI_EN > 0)
OS_EVENT **OSTCBEventMultiPtr; /* Pointer to multiple event control blocks */
#endif
#if ((OS_Q_EN > 0) && (OS_MAX_QS > 0)) || (OS_MBOX_EN > 0)
void *OSTCBMsg; /* Message received from OSMboxPost() or OSQPost() */
#endif
//消息、队列、信号量等都用到事件控制块,所以均定义有事件控制块的指针变量OS_EVENT *
#if (OS_FLAG_EN > 0) && (OS_MAX_FLAGS > 0)
#if OS_TASK_DEL_EN > 0
OS_FLAG_NODE *OSTCBFlagNode; /* Pointer to event flag node */
#endif
OS_FLAGS OSTCBFlagsRdy; /* Event flags that made task ready to run */
#endif
INT16U 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) */ //OS_LOWEST_PRIO
//OSTCBX、OSTCBY、OSTCBBitX、OSTCBBitY用于快速查找任务就绪表的状态,判断任务是否就绪(后序介绍如何快速查找任务就绪状态和就绪任务的最高优先级)
//UCOS-II最大支持64个就绪任务,提出空闲任务(Idle task)和统计任务(
statistics task)
INT8U OSTCBX; /* Bit position in group corresponding to task priority OSRdyGrp就绪组分组的位置,即OSRdyGrp变量位域 */
INT8U OSTCBY; /* Index into ready table corresponding to task priority OSRdyTbl就绪数组位置, 即OSRdyTbl[OSRdyGrp]变量位域 */
#if OS_LOWEST_PRIO <= 63 //os_cfg.h定义系统支持的最大
INT8U OSTCBBitX; /* Bit mask to access bit position in ready table */
INT8U OSTCBBitY; /* Bit mask to access bit position in ready group */
#else
INT16U OSTCBBitX; /* Bit mask to access bit position in ready table */
INT16U OSTCBBitY; /* Bit mask to access bit position in ready group */
#endif
//os_core.c中的部分代码
#if OS_LOWEST_PRIO <= 63
ptcb->OSTCBY = (INT8U)(prio >> 3); /* Pre-compute X, Y, BitX and BitY */
ptcb->OSTCBX = (INT8U)(prio & 0x07);
ptcb->OSTCBBitY = (INT8U)(1 << ptcb->OSTCBY);
ptcb->OSTCBBitX = (INT8U)(1 << ptcb->OSTCBX);
#else
ptcb->OSTCBY = (INT8U)((prio >> 4) & 0xFF); /* Pre-compute X, Y, BitX and BitY */
ptcb->OSTCBX = (INT8U) (prio & 0x0F);
ptcb->OSTCBBitY = (INT16U)(1 << ptcb->OSTCBY);
ptcb->OSTCBBitX = (INT16U)(1 << ptcb->OSTCBX);
#endif
#if OS_TASK_DEL_EN > 0
INT8U OSTCBDelReq; /* Indicates whether a task needs to delete itself */
#endif
//任务运行信息
#if OS_TASK_PROFILE_EN > 0
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_SIZE > 1
INT8U OSTCBTaskName[OS_TASK_NAME_SIZE];
#endif
} OS_TCB;
这里所说的任务,可以理解成一个线程。
2. 控制块链表:空闲链表和就绪链表
任务控制块定义在ucos_ii.h中,OS_TCB OSTCBTbl[OS_MAX_TASKS + OS_N_SYS_TASKS];其大小为用户定义的最大任务数+空闲任务+<可选的统计任务>
任务控制块空闲链表和就绪链表在调用OSInit函数时建立,初始时,空闲链表表头指向OSTCBTbl[0],就绪链表指向NULL;
当用户创建一个任务时,系统从空闲链表中取出表头结点加入就绪链表,就绪链表表头指向新加入结点,该结点后驱结点指向旧就绪链表,空闲链表表头指向下一个空闲结点。
OS_TCB *OSTCBFreeList, *OSTCBList;
当新任务创建时,行为大致如下(注意空闲链表和就绪链表均为双向链表)
temp=OSTCBFreeList;
OSTCBFreeList=temp->OSTCBNext;
temp->OSTCBNext->OSTCBPrev = NULL;
temp->OSTCBNext = OSTCBList;
temp->OSTCBPrev = NULL;
OSTCBList = temp;
3. 任务就绪表和就绪组
内核在任务调度时,需要尽快计算出当前就绪的最高优先级任务,转而切换到该任务继续执行(注意:
UCOS中任务优先级值越小表示优先等级越高)。
UCOS设计出一个固定时间内获取就绪最高优先级任务的方法:
- 由于UCOS最大支持64个就绪任务,而且不同任务优先级必须不同,所以使用64位表示优先级0-63的就绪状态(设置最低优先级最好不要超过63),当然超过63后,OSRdyGrp和OSRdyTbl会使用INT16U表示;
- 假设优先级为0~63共用64bits可以完全表示,每一位对应一个优先级的任务是否就绪(1:就绪,0:未就绪),将此64bits数据分成8各组(用OSRdyGrp表示),每个组用8bits即INT8U表示(用OSRdyTbl[8]表示),因此,prio/8表示优先级为prio的任务在OSRdyGrp的位置,prio%8表示优先级为prio的任务在OsRdyTbl[prio/8]中的位置,而由于prio/8相当于prio>>3,所以就可以大大简化查看指定优先级任务的就绪状态;
- 假设OSRdyTbl[]中,OSRdyTbl[0]从MSB->LSB表示优先级为7-0的任务就绪状态,OSRdyTbl[1]从MSB->LSB表示优先级为15-8的任务就绪状态,以此类推,而OSRdyGrp从MSB->LSB表示OSRdyTbl[7]-OSRdyTbl[0]就绪组中是否存在已经就绪的任务。那么当OSRdyGrp为2'b11010010,那么最高优先级的任务必定位于OSRdyTbl[1]中,如果在OSRdyTbl[1]为2'b01001011,那么最高优先级任务则确定为优先级8。
- 要想快速从一个8位数据中得出最高优先级位置--即获取一个8微数据中为1的最低位,UCOS的做法是使用查找表(LUT)的方式来加速,即对应0x00-0xFF每一种情况均给出为1最低位的位域。这个查找表则存储位于os_core.c文件的INT8U const OSUnMapTbl[256]中。
因此,UCOS在计算就绪最高优先级任务时,所使用的时间是固定长度。
以下是UCOS的核心代码:
#if OS_LOWEST_PRIO <= 63 /* See if we support up to 64 tasks */
INT8U y;
y = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]);
#else /* We support up to 256 tasks */
INT8U y;
INT16U *ptbl;
if ((OSRdyGrp & 0xFF) != 0) {
y = OSUnMapTbl[OSRdyGrp & 0xFF];
} else {
y = OSUnMapTbl[(OSRdyGrp >> 8) & 0xFF] + 8;
}
ptbl = &OSRdyTbl[y];
if ((*ptbl & 0xFF) != 0) {
OSPrioHighRdy = (INT8U)((y << 4) + OSUnMapTbl[(*ptbl & 0xFF)]);
} else {
OSPrioHighRdy = (INT8U)((y << 4) + OSUnMapTbl[(*ptbl >> 8) & 0xFF] + 8);
}
#endif
可见,UCOS支持256个任务,巧妙利用OSUnMapTbl计算最高优先级任务。此时OSRdyGrp使用INT16U表示,OSRdyTbl[]也为INT16U。
4. 任务堆栈和任务切换
每个任务的堆栈是相互独立,大小可以有用户自由定义,类型为OS_STK,此类型长度与CPU有关,所以定义在os_cpu.h中,例如定义为typedef unsigned int OS_STK;
在os_cpu.h中还需要根据实际cpu定义堆栈的生长方向:#define OS_STK_GROWTH 1u //1:表示从高地址向低地址生长,0:表示从低地址向高地址生长
根据情况不同,对堆栈的操作不同,所以对上下文切换操作也是不同的。一般分为任务级的任务切换和中断服务中的任务切换。
引用《Cortex-M3权威指南》原话:
“响应异常的第一个行动,就是自动保存现场的必要部分:依次把xPSR, PC, LR, R12以及R3-R0由硬件自动压入适当的堆栈中:如果当响应异常时,当前的代码正在使用PSP,则压入PSP,也就是使用进程堆栈;否则就压入MSP,使用主堆栈。一旦进入了服务例程,就将一直使用主堆栈。”
这时,由于响应异常/中断,CPU自动保存了部分寄存器,所以相应的,我们只需要手动保存{R4-R11}到任务堆栈,即可实现保存上文。
而对于系统的一般任务调度,内核主动切换较高优先级时,则需要将CPU所有进程寄存器保存起来,再进行任务切换。
关于任务切换的代码,需要使用汇编语言实现,实现源码放在os_cpu_a.asm中,阅读一下例程的写法:
OSCtxSw ; 日常任务切换
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
OSIntCtxSw ; 中断任务切换
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
其中,NVIC_INT_CTRL为中断控制及状态寄存器ICSR地址0xE000ED04,NVIC_PENDSVSET值为0x10000000,对应于ICSR的PENDSVSET位,当该位被置位时,产生PendSV中断,继而转入中断服务中去。
借助PendSV中断服务,顺水推舟,将当前任务的部分寄存器存到任务的栈中,我们只需要将{R4-R11}手动存入当前任务栈中即可。当然,为了安全起见,在任务切换起见必须禁止其他中断。然后从系统变量中获得当前已就绪的最高优先级及其任务控制块地址,更新系统变量后,将栈指针更换为新任务的栈指针,并从中弹出{R4-R11},剩余的寄存器将在PendSV中断服务结束后自动弹出覆盖相应寄存器,在结束PendSV中断服务前,记得打开中断开关,通过BX LR指令退出PendSV,此时其他寄存器值也被弹出了。
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 8*4bytes=32bytes=0x20bytes
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(); here do nothing
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
为什么用PendSV而不用SVC呢?我们观察下两者的区别:
同样引用《Cortex-M3权威指南》原话:
“对于SVC异常来说,若因优先级不比当前正处理的高,或是其它原因使之无法立即响应,将上访成硬fault”
UCOS同时要求任务切换是可屏蔽的,特别是临界区代码执行期间,必须禁止任务调度,所以使用SVC就不太合适。
为什么用PendSV而不用SWI呢?原因如下:
- 首先是异常与中断时存在区别的。PendSV是异常,SWI是软件中断。《Cortex-M3权威指南》原话:“编号为1-15的对应系统异常,大于等于16的则全是外部中断。”“中断与异常的区别在于,那240个中断对CM3核来说都是“意外突发事件”——也就是说,该请求信号来自CM3内核的外面,来自各种片上外设和外扩的外设,对CM3来说是“异步”的;而异常则是因CM3内核的活动产生的——在执行指令或访问存储器时产生,因此对CM3来说是“同步”的。”也就是说PendSV可以得到及时的响应。
- 《Cortex-M3权威指南》第8章:“软件中断,包括手工产生的普通中断,能以多种方式产生。最简单的就是使用相应的SETPEND寄存器;而更专业更快捷的作法,则是通过使用软件触发中断寄存器STIR。”“系统异常(NMI,faults,PendSV等),不能用此法悬起。而且缺省时根本不允许用户程序改动NVIC寄存器的值。如果确实需要,必须先在NVIC的配置和控制寄存器(0xE000_ED14)中,把比特1(USERSETMPEND)置位,才能允许用户级下访问NVIC的STIR。”这样可以避免故意地通过软件中断方式切换运行恶意代码。
- 《Cortex-M3权威指南》第11章:“软件中断没有SVC专业:比如,它们是不精确的,也就是说,抢占行为不一定会立即发生,即使当时它没有被掩蔽,也没有被其它ISR阻塞,也不能保证马上响应。这也是写缓冲造成的,会影响到与操作NVIC STIR相临的后一条指令:如果它需要根据中断服务的结果来决定如何工作(如条件跳转),则该指令可能会误动作——这也可以算是紊乱危象的一种表现形式。为解决这个问题,必须使用一条DSB指令,如下例所示:MOV R0, #SOFTWARE_INTERRUPT_NUMBER LDR R1,=0xE000EF00 ; 加载NVIC软件触发中断寄存器的地址 STR R0, [R1] ; 触发软件中断 DSB ……”“还有另一个隐患:如果欲触发的软件中断被除能了,或者执行软件中断的程序自己也是个异常服务程序,软件中断就有可能无法响应。因此,必须在使用前检查这个中断已经在响应中了。为达到此目的,可以让软件中断服务程序在入口处设置一个标志。”
- Cortex-M3处理器支持两种处理器操作模式和两级特权操作。
|
特权级 |
用户级 |
异常handler代码 |
handle模式 |
错误的用法 |
主应用程序的代码 |
线程模式 |
线程模式 |
在 CM3 运行主应用程序时(线程模式),既可以使用特权级,也可以使用用户级;但是异常服务例程必须在特权级下执行。复位后,处理器默认进入线程模式,特权极访问。在特权级下,程序可以访问所有范围的存储器(如果有 MPU,还要在MPU规定的禁地之外),并且可以执行所有指令。
一旦通过修改CONTROL寄存器进入用户级时,要想回到特权级,则必须调用指令SVC触发SVC异常,进入handle服务中修改CONTROL寄存器,重回特权级。事实上,从用户级到特权级的唯一途径就是异常。如果异常服务,处理器会先切换入特权级,服务中不做处理的话,在服务退出时,将返回原先的状态。
5. PendSV堆栈使用
“CM3的堆栈是分为两个:主堆栈和进程堆栈。”
“在进入异常服务时,CM3会自动把一些寄存器压栈,这里使用的是发生本异常的瞬间正在使用的SP指针(MSP或者是PSP)。离开异常服务后,只要没有更改过CONTROL[1],就依然使用发生本次异常的瞬间正在使用的堆栈指针来执行出栈操作。”
“当CONTROL[1]=0时,只使用MSP,此时用户程序和异常handler共享同一个堆栈。这也是复位后的缺省使用方式。当CONTROL[1]=1时,线程模式将不再使用MSP,而改用PSP(handler模式永远使用MSP)。这样做的好处在哪里?原来,在使用OS的环境下,只要OS内核仅在handler模式下执行,用户应用程序仅在用户模式下执行,这种双堆栈机制派上了用场——防止用户程序的堆栈错误破坏OS使用的堆栈。”
也就是说,当CONTROL[1]=1时,当内核触发异常,并进入异常服务时,在异常服务中系统所有动作均使用MSP,而进入异常前会使用PSP压栈,退出异常时也会使用PSP出栈。这样内核和线程就不会因使用同一个堆栈指针破坏内核数据。(当CONTROL[1]=1时,在异常服务中的变量以及中断嵌套就会使用MSP了)
在CPU复位后,将进入特权级,并默认使用MSP,在没有操作系统下,编写一般的C语言代码,就基本会一直使用MSP;当引入操作系统后,要视乎系统初始化时,是否修改CONTROL寄存器。
《Cortex-M3权威指南》第十二章:
“使用PendSV(一个优先级最低的异常)来执行上下文切换,从而消灭了在中断服务例程中出现上下文切换的可能。”
当使用双堆栈时,系统调用PendSV异常进行上下文切换时,当前任务使用PSP压栈保存环境,在PendSV服务过程中遇到SysTick异常服务时,会使用MSP对PendSV服务环境进行压栈,同样,其他中断嵌套触发也会使用MSP,PendSV退出前,将{R4-R11}覆盖到已就绪的最高优先级任务中,并更新新的PSP地址,当PendSV退出时(由于PendSV优先级最低,已经没有其他中断挂起的情况)会使用PSP进行出栈。双堆栈使用可以免去中断开关带来的中断无法及时响应的问题。
附上CM3的CONTROL寄存器:
CONTROL[1]的使用问题:
在Cortex-M3的handler模式中,CONTROL[1]总是0。在线程模式中则可以为0或1。
因此,仅当处于特权级的线程模式下,此位才可写,其它场合下禁止写此位。改变处理器的模式也有其它的方式:在异常返回时,通过修改LR的位2,也能实现模式切换。这是LR在异常返回时的特殊用法,颠覆了对LR的传统使用方式。
那么就可以解释PendSVHandler中任务切换的末尾几个语句
ORR LR, LR,
#0x04 ; Ensure exception return uses process stack
CPSIE I
BX LR
这里实现了特权级到用户级的转换。
6. 任务优先级指针表
UCOS为了提高索引指定优先级的任务,因此使用了简单的哈希数组来存储每个不同优先级的任务的TCB块地址。
OS_TCB *OSTCBPrioTbl[OS_LOWEST_PRIO + 1];
当通过任务就绪组确立最高优先级且已就绪的任务的优先级后,即可通过任务优先级指针表,找到该任务的TCB块地址。