学习目的:
- 熟悉uCOS-III任务间切换实现原理
在使用单片机做一些复杂的产品开发时,单纯的裸机系统通常不能很完美的解决问题,为了降低编程的难度,开发中我们一般会引入RTOS进行多任务管理。在引入RTOS的后,编程思想和裸机系统程序设计有所不同,我们会根据产品所要实现的功能,将整个系统分割成一个个独立的且无法返回的函数,这些函数也就是我们通常所讲的任务。不同的任务在RTOS内核的管理下不停运行,宏观上,不同的任务之间仿佛是在同时运行的。但实际上对于单片机而言,一般情况下它只有一个CPU,每个时间点只能运行一个任务,其实宏观上的并行不是真正的并行,而是RTOS在不同任务间进行切换,分别让不同任务获取CPU资源运行。由于CPU的运行速度很快,从而让人感觉到任务间好像是同时运行的
这里我们来分析在Cortex-M3架构下,uCOS-III内核中任务间切换是如何实现的。值得注意的是,任务间切换是和CPU相关的,不同内核的CPU在切换细节上可能有所不同,但思路上应该是一致的。本文讲述的只是uCOS-III任务切换的底层实现细节,对于任务间何时进行切换没有进行详细描述
一、触发任务切换
所谓任务切换的触发就是当OS内核判断满足任务调度条件时,告诉CPU去执行任务切换的代码。uCOS-III内核设计时,将任务间切换定在了在PendSV异常的服务函数中完成,因此任务切换的触发也就是去让CPU进入PendSV异常
对于ARM Cortex-M3架构的单片机,可通过设置SCB寄存器(异常和中断控制寄存器)中包含的ICSR寄存器(中断控制及状态寄存器)的第28位来触发PendSV异常,ICSR寄存器的地址是0xE000ED04。当向ICSR寄存器的第28位写1,PendSV异常被挂起,当其他高优先级的中断被执行后,被挂起的PendSV异常被执行。此时,CPU跳转到PendSV异常入口,执行PendSV异常服务程序
由此可以知道,运行在Cortex-M3内核单片机上的uCOS-III系统,如果想进行任务切换,需要向0xE000ED04寄存器中写入0x10000000来触发PendSV异常
为了通用性,uCOS-III中将触发PendSV异常的操作封装成了一些宏,这样uCOS-III内核上层的程序不需要关心不同硬件触发PendSV异常的具体细节,只需要调用这个宏即可。这些宏在os_cpu.h文件中定义的,具体实现需要由移植人员根据所使用的单片机架构去完成,如在Cortex-M3内核单片机上,这些宏的实现如下:
#ifndef NVIC_INT_CTRL #define NVIC_INT_CTRL *((CPU_REG32 *)0xE000ED04) #endif #ifndef NVIC_PENDSVSET #define NVIC_PENDSVSET 0x10000000 #endif #define OS_TASK_SW() NVIC_INT_CTRL = NVIC_PENDSVSET #define OSIntCtxSw() NVIC_INT_CTRL = NVIC_PENDSVSET
不过,实际上,uCOS-III在进行任务切换时并没有直接去调用OS_TASK_SW(),内核设计者将OS_TASK_SW()放在OSSched函数中,OSSched函数才是上层任务切换直接调用到的函数。OSSched函数中先找到当前任务就绪表中优先级最高的就绪任务,然后调用OS_TASK_SW()触发PendSV异常,等待CPU处理PendSV异常,在异常中完成任务间的切换
void OSSched (void) { ... OSPrioHighRdy = OS_PrioGetHighest(); /* Find the highest priority ready */ OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr; ... OS_TASK_SW(); /* Perform a task level context switch */ ... }
二、任务切换实现
uCOS-III任务的切换是在PendSV异常中进行的,当内核想进行任务切换时,调用OSSched函数。该函数最终会触发PendSV异常,紧接着CPU进入PendSV异常的入口,调用PendSV异常处理函数,任务切换的实现也就是在PendSV异常处理函数中完成的。下面我们将会具体分析下任务切换的实现机制
在介绍任务切换实现的具体内容前,我们先了解一个uCOS-III中一个核心的数据结构——TCB(任务控制块)。任务控制块,本质上就是一个结构体,在软件上代表的就是对一个任务的抽象,相当于任务的身份证,里面存有任务的所有信息,比如任务的堆栈,任务名称,任务参数等。有了这个任务控制块之后,以后系统对任务的全部操作都可以通过这个TCB来实现。任务控制块中有很多内容,下面只介绍了我们今天所要使用到的几个成员
struct os_tcb { CPU_STK *StkPtr; /* Pointer to current top of stack */ ... CPU_STK *StkBasePtr; /* Pointer to base address of stack */ ... OS_TASK_PTR TaskEntryAddr; /* Pointer to task entry point address */ ... OS_PRIO Prio; /* Task priority (0 == highest) */ CPU_STK_SIZE StkSize; /* Size of task stack (in number of stack elements) */ ... };
- StkPtr:指向当前任务栈顶指针
- StkBasePtr:指向当前任务栈的基地址指针
- TaskEntryAddr:指向任务的入口函数指针
- Prio:任务优先级
- StkSize:任务分配栈大小
uCOS-III中,每创建的一个任务,需要实例化一个os_tcb对象来描述这个任务,同时,需要给每个创建的任务分配一个栈空间和优先级,os_tcb中记录了当前任务的所有信息。每个os_tcb对象里的信息,在调用OSTaskCreate时被初始化
好了,了解了任务控制块的一些信息后,我们就开始来分析任务切换的具体实现。任务切换代码是用汇编来编写的,存放在os_cpu_a.s文件中,它依赖于CPU,CPU不同实现上也有所不同
OS_CPU_PendSVHandler CPSID I ; 关中断 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, =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 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
① 将PSP指针的值加载到R0寄存器中,PSP指针指向当前任务的堆栈栈顶
Cortex-M3处理器内核共有两个堆栈指针,也就是支持两个堆栈。当引用R13(或写作SP)时,引用到的是当前正在使用的那一个,另一个必须使用特殊的指令来访问(MSR,MRS指令)。这两个堆栈指针分别是:
- 主堆栈指针(MSP),这是缺省的堆栈指针,它由OS内核、异常服务例程以及所有需要特殊访问的应用程序代码来使用
- 进程堆栈指针(PSP),用于常规的应用程序代码(不处于异常服务例程中)
此时,CPU进入PendSV异常,使用的是MSP指针,此时的PSP指针保存的是入栈前的任务的当前堆栈地址
② 判断R0是否为0, 如果为0跳到OS_CPU_PendSVHandler_nosave位置运行。此时R0寄存器表示的是PSP寄存器内容,在第一次进入任务调度时,PSP指针会被设置成0,对于第一次进行任务调度这种情况不需要执行下面的将被切换的任务的寄存器内容入栈操作
③ 手动将R4-R11寄存器内容存放到被切换出的任务的堆栈空间中,寄存器入栈,用于后续该任务重新调度时的现场恢复
④ 修改被切换出的任务的任务控制块中记录当前堆栈的栈顶的位置信息,即更新任务控制块中StkPtr指针内容
①~④步骤,完成了将被切换出的任务的寄存器信息写入到该任务的栈中,并修改任务控制块中栈顶指针的位置。执行过程可如下图内容所示:
⑤ 调用任务切换的钩子函数OSTaskSwHook
⑥ 修改当前运行任务优先级变量,OSPrioCur = OSPrioHighRdy
⑦ 修改当前运行任务指针,OSTCBCurPtr = OSTCBHighRdyPtr,当前运行任务为最高优先级任务
⑧ 找到当前要运行任务的堆栈指针,将栈中保存的R4~R11寄存器信息重新加载到R4~R11寄存器中
⑨ 修改PSP堆栈指针指向新任务堆栈
⑩ 异常返回,这个时候任务堆栈中的剩下内容将会自动加载到xPSR,PC,R14,R12,R3,R2,R1,R0
异常返回时,通过修改LR的位2为1,实现模式切换,确保从MSP指针切换到PSP指针
⑥~⑩步骤,从即将要运行的任务的任务控制块中找到该任务栈顶信息,完成新任务的恢复现场操作。执行细节可如下图内容所示:
三、小结
uCOS-III中任务间的切换是通过触发PendSV异常来实现的,在异常处理函数中,将当前任务现场信息存放到该任务的堆栈中,以便于后续该任务的恢复。然后找到下一个要运行的任务的堆栈地址,修改PSP指针指向该堆栈地址,执行恢复现场操作,这样就完成了两个任务间的切换工作