源代码请在https://github.com/ifreecoding/MbedRtos.git下载
第2节 任意任务间的切换
上一节我们使用2个固定的任务验证了操作系统任务切换的功能,但这些代码并不具有通用性,如果要扩充其它任务,就必须修改操作系统函数,这显然是不可接受的。操作系统作为独立于用户代码的部分,它的内部细节应该是不被用户所见的,是一个黑盒,需要做到用户只需要修改操作系统提供的接口文件里面的参数,调用接口函数就可以完全满足程序开发的要求。因此,在本节我们将对上节的代码做些改动,使其可以支持任意多个任务之间的互相切换,而又不需要修改Wanlix目录下的操作系统代码,仅仅是编写srccode目录下的用户代码,调用操作系统的接口函数即可,这样才真正实现了操作系统的独立性。
首先我们来看看任务切换函数——WLX_TaskSwitch。上节中,这个函数固定在两个任务之间切换,因此要实现可以切换到任何一个函数的功能就必须修改此函数,需要为这个函数增加一个入口参数,用这个入口参数来指明需要切换到的任务。WLX_TaskSwitch函数的主要功能是做好任务切换前的准备,将当前运行任务的栈指针和将要运行任务的栈指针存入到对应的全局变量中,上节中,为每个任务分别指定了guiTask21CurSp和guiTask2CurSp全局变量保存它们的当前栈指针,每个全局变量绑定到了任务,因此,这个入口参数还必须能够关联到任务的栈指针。
为此,我们引入TCB的概念,在操作系统里这是一个非常重要的概念。TCB是Task Control Block的缩写,意为任务控制块,与任务控制相关的重要信息都放被到TCB里。TCB是一个结构体,每个任务都拥有一个TCB ,可以把每个任务的与任务控制相关的结构都放入到它的TCB中,因此我们可以将任务当前的栈指针保存到它的TCB中,到目前为止,TCB格式如下:
typedef struct w_tcb
{
U32 uiTaskCurSp;
}W_TCB;
TCB结构不只是这么简单,只是到目前为止就是这么简单,随着操作系统功能的不断完善,TCB也会不断的增加它的结构。
我们可以考虑将TCB放到任务的栈中。当任务创建时在栈的开始处保留一块内存作为TCB的存放空间,TCB之后的栈空间才作为真正的栈使用,这样,任务的TCB也就与任务绑定到了一起,每个TCB就可以代表一个任务。由于ARM芯片是线性地址空间,也就是说每个内存地址都是唯一的,因此每个任务堆栈的开始地址也就是唯一的,因此每个TCB的地址也就是唯一的了,这样我们就可以使用TCB的地址来代表各个不同的任务。
有了TCB,下面我们来修改WLX_TaskSwitch函数。修改很简单,只是将TCB指针作为入口参数,在函数里替换掉原来与任务相关的全局变量,修改后的函数如下:
00107 void WLX_TaskSwitch(W_TCB* pstrTcb)
00108 {
00109
00111 gpuiCurTaskSpAddr = &gpstrCurTcb->uiTaskCurSp;
00112
00113
00114 guiNextTaskSp = pstrTcb->uiTaskCurSp;
00115
00116
00117 gpstrCurTcb = pstrTcb;
00118
00119 WLX_ContextSwitch();
00120 }
00107行,入口参数pstrTcb是将要运行任务的TCB指针,准备切换到该任务运行。
00111行,将当前运行任务的TCB中保存当前栈指针变量的地址存入全局变量gpuiCurTaskSpAddr中。
00114行,将将要运行任务的TCB中保存当前栈指针的变量,也就是当前的栈指针,存入全局变量guiNextTaskSp中。
00117行,将全局变量gpstrCurTcb 更新为将要运行任务的TCB,为下次任务切换做准备。
00119行,调用汇编函数WLX_ContextSwitch执行具体的寄存器备份、恢复操作。
同理,WLX_TaskStart函数也需要做类似的变量替换,不再介绍,读者自行分析。
00127 void WLX_TaskStart(W_TCB* pstrTcb)
00128 {
00129
00130 guiNextTaskSp = pstrTcb->uiTaskCurSp;
00131
00132
00133 gpstrCurTcb = pstrTcb;
00134
00135 WLX_SwitchToTask();
00136 }
由于增加了TCB,因此必须修改任务初始化函数。从本节开始,所有的任务将采用WLX_TaskCreate函数创建,在WLX_TaskCreate函数内分别对TCB和栈进行初始化。
00018 W_TCB* WLX_TaskCreate(VFUNC vfFuncPointer, U8* pucTaskStack, U32 uiStackSize)
00019 {
00020 W_TCB* pstrTcb;
00021
00022
00023 if(NULL == vfFuncPointer)
00024 {
00025
00026 return (W_TCB*)NULL;
00027 }
00028
00029
00030 if((NULL == pucTaskStack) || (0 == uiStackSize))
00031 {
00032
00033 return (W_TCB*)NULL;
00034 }
00035
00036
00037 pstrTcb = WLX_TaskTcbInit(pucTaskStack, uiStackSize);
00038
00039
00040 WLX_TaskStackInit(pstrTcb, vfFuncPointer);
00041
00042 return pstrTcb;
00043 }
00018行,函数返回值是被创建任务的TCB指针;入口参数vfFuncPointer是创建任务所使用的函数;入口参数pucTaskStack是创建任务所使用的栈地址,是栈的低地址;入口参数uiStackSize是栈的大小。
00023行,对入口参数判断,如果函数指针为空,则返回NULL空指针代表创建任务失败。在C语言里,指针为NULL(也就是0)代表无效指针,因为0地址的内存一般都是中断向量表的复位向量,正常使用指针时是不会指向这里的。
00030行,对入口参数判断,如果栈指针为NULL或者栈大小为0,则返回失败。
00037行,调用WLX_TaskTcbInit函数初始化任务的TCB,并得到当前任务的TCB指针。
00040行,调用WLX_TaskStackInit函数初始化当前的任务栈。
00042行,任务创建成功,返回当前任务的TCB。以后就可以使用这个返回值来代表这个任务了。
目前的TCB比较简单,只有一个保存栈地址的变量,在WLX_TaskTcbInit函数里就是来初始化这个变量的,并返回TCB指针。
00051 W_TCB* WLX_TaskTcbInit(U8* pucTaskStack, U32 uiStackSize)
00052 {
00053 W_TCB* pstrTcb;
00054 U8* pucStackBy4;
00055
00056
00057 pucStackBy4 = (U8*)(((U32)pucTaskStack + uiStackSize) & 0xFFFFFFFC);
00058
00059
00060 pstrTcb = (W_TCB*)(((U32)pucStackBy4 - sizeof(W_TCB)) & 0xFFFFFFFC);
00061
00062
00063 pstrTcb->uiTaskCurSp = (U32)pstrTcb;
00064
00065 return pstrTcb;
00066 }
00051行,函数返回值是被创建任务的TCB指针,创建任务后这个TCB就代表该任务;pucTaskStack是任务的栈地址,是栈的低地址;uiStackSize是栈的大小。
00057行,确定栈顶地址。“(U32)pucTaskStack + uiStackSize”是栈顶地址,由于栈必须是4字节对齐,因此再通过“& 0xFFFFFFFC”操作,从栈顶向下寻找4字节对齐的地址作为栈顶地址。
00060行,确定TCB地址。“(U32)ucStackBy4 - sizeof(W_TCB)”操作,从栈中减去存放TCB的空间,再通过“& 0xFFFFFFFC”操作,向下寻找4字节对齐的地址作为存放TCB的起始地址,这个也是任务调度时使用的栈顶地址。
00063行,初始化TCB中的变量,保存任务的栈指针。
00065行,返回任务的TCB指针。
TCB的引入也需要对WLX_TaskStackInit函数做简单的修改,由于该函数只是简单使用TCB替换了原来专用的全局变量,没有大的改动,就不做过多介绍了,读者可以对比上节的函数自己分析。
00074 void WLX_TaskStackInit(W_TCB* pstrTcb, VFUNC vfFuncPointer)
00075 {
00076 U32* puiSp;
00077
00078
00079 puiSp = (U32*)pstrTcb->uiTaskCurSp;
00080
00081 *(--puiSp) = (U32)vfFuncPointer;
00082 *(--puiSp) = pstrTcb->uiTaskCurSp;
00083 *(--puiSp) = 0;
00084 *(--puiSp) = 0;
00085 *(--puiSp) = 0;
00086 *(--puiSp) = 0;
00087 *(--puiSp) = 0;
00088 *(--puiSp) = 0;
00089 *(--puiSp) = 0;
00090 *(--puiSp) = 0;
00091 *(--puiSp) = 0;
00092 *(--puiSp) = 0;
00093 *(--puiSp) = 0;
00094 *(--puiSp) = 0;
00095 *(--puiSp) = 0;
00096 *(--puiSp) = MODE_USR;
00097
00098
00099 pstrTcb->uiTaskCurSp = (U32)puiSp;
00100 }
经过上述修改操作系统就具有通用性了,无论建立多少个任务都无需修改操作系统的代码,只要为任务分配一个栈空间,使用WLX_TaskCreate函数就可以创建任务,并可以使用这个任务的TCB指针作为入口参数,调用WLX_TaskSwitch函数就可以切换到这个任务。
另外说一点,创建任务时,需要用户先用全局变量为所创建的任务申请一个任务栈空间,将它的起始地址和大小作为参数传递给WLX_TaskCreate函数来创建任务。如果能将申请任务栈的操作封装到WLX_TaskCreate函数里面就会更方便一些,但我在GNU环境下没有找到配置堆(heap)的方法(谁知道请在论坛上反馈一下,谢谢!),因此无法在WLX_TaskCreate函数里使用C函数库里的malloc函数从堆中申请任务栈。如果使用自己编写的堆函数则不如C库函数的方便,兼容性也不好,因此这里需要用户自己申请任务的栈空间。后面在Cortex内核芯片上,我们将换一个编译器,到那时候再完善这个功能。
在看最终效果前,我们再对任务切换过程中寄存器备份、恢复操作做最后一点优化。上节我们使用WLX_ContextSwitch函数完成任务寄存器入栈、出栈及最后跳转的操作,这样做存在2个问题:
1. 每次执行任务切换时都需要多执行2条汇编指令,来判断是否是从非操作系统状态切换到操作系统状态,请参考上节WLX_ContextSwitch函数的00024行和00025行。
2. 在程序从非操作系统状态切换为操作系统状态时,没有必要将芯片寄存器保存到系统栈中。
为此,我们将原有的WLX_ContextSwitch函数拆分成2个函数,WLX_ContextSwitch函数和WLX_SwitchToTask函数。WLX_ContextSwitch函数仍被WLX_TaskSwitch函数调用,用于每次任务切换,WLX_SwitchToTask函数被WLX_TaskStart函数调用,用于第一次任务切换。
这两个汇编函数没有实质性的改动,不再详细介绍,请读者自行分析。
00012 .func WLX_ContextSwitch
00013 WLX_ContextSwitch:
00014
00015 @保存当前任务的堆栈信息
00016 STMDB R13, {R0-R14}
00017 SUB R13, R13, #0x3C
00018 MRS R0, CPSR
00019 STMDB R13!, {R0}
00020
00021 @保存当前任务的指针值
00022 LDR R0, =gpuiCurTaskSpAddr
00023 LDR R1, [R0]
00024 STR R13, [R1]
00025
00026 @获取将要运行任务的指针
00027 LDR R0, =guiNextTaskSp
00028 LDR R13, [R0]
00029
00030 @获取将要运行任务的堆栈信息并运行新任务
00031 LDMIA R13!, {R0}
00032 MSR CPSR, R0
00033 LDMIA R13, {R0-R14}
00034 BX R14
00035
00036 .endfunc
00043 .func WLX_SwitchToTask
00044 WLX_SwitchToTask:
00045
00046 @获取将要运行任务的指针
00047 LDR R0, =guiNextTaskSp
00048 LDR R13, [R0]
00049
00050 @获取将要运行任务的堆栈信息并运行新任务
00051 LDMIA R13!, {R0}
00052 MSR CPSR, R0
00053 LDMIA R13, {R0-R14}
00054 BX R14
00055
00056 .endfunc
至此就完成了本节代码的修改。在测试代码里我们建立3个任务,TEST_TestTask1、TEST_TestTask2和TEST_TestTask3,并将它们的TCB保存到全局变量gpstrTask1Tcb、gpstrTask1Tcb 和gpstrTask1Tcb 中,供任务切换时使用。
00020 S32 main(void)
00021 {
00022
00023 DEV_HardwareInit();
00024
00025
00026 gpstrTask1Tcb = WLX_TaskCreate((VFUNC)TEST_TestTask1, gaucTask1Stack,
00027 TASKSTACK);
00028 gpstrTask2Tcb = WLX_TaskCreate((VFUNC)TEST_TestTask2, gaucTask2Stack,
00029 TASKSTACK);
00030 gpstrTask3Tcb = WLX_TaskCreate((VFUNC)TEST_TestTask3, gaucTask3Stack,
00031 TASKSTACK);
00032
00033
00034 WLX_TaskStart(gpstrTask1Tcb);
00035
00036 return 0;
00037 }
00044 void TEST_TestTask1(void)
00045 {
00046 while(1)
00047 {
00048 DEV_PutString((U8*)"\r\nTask1 is running!");
00049
00050 DEV_DelayMs(1000);
00051
00052 WLX_TaskSwitch(gpstrTask3Tcb);
00053 }
00054 }
00061 void TEST_TestTask2(void)
00062 {
00063 while(1)
00064 {
00065 DEV_PutString((U8*)"\r\nTask2 is running!");
00066
00067 DEV_DelayMs(2000);
00068
00069 WLX_TaskSwitch(gpstrTask1Tcb);
00070 }
00071 }
00078 void TEST_TestTask3(void)
00079 {
00080 while(1)
00081 {
00082 DEV_PutString((U8*)"\r\nTask3 is running!");
00083
00084 DEV_DelayMs(3000);
00085
00086 WLX_TaskSwitch(gpstrTask2Tcb);
00087 }
00088 }
这3个任务在循环运行,向串口打印数据,每间隔一段时间切换到另外一个任务继续运行。TEST_TestTask1任务运行时向串口打印“Task1 is running!”代表TEST_TestTask1任务开始运行,1秒后主动切换到TEST_TestTask3任务,TEST_TestTask3任务运行时向串口打印“Task3 is running!”代表TEST_TestTask3任务开始运行,3秒后主动切换到TEST_TestTask2任务,TEST_TestTask2任务运行时向串口打印“Task2 is running!”代表TEST_TestTask2任务开始运行,2秒后主动切换到TEST_TestTask1任务,如此反复循环。
编译本节代码,串口打印如下图:
读者也可通过本节的视频观察这3个任务的动态执行过程,读者也可以自行增加任务在单板上运行一下,体验本节的收获!