第3章第2节 任意任务间的切换

来源:互联网 发布:top域名有什么用 编辑:程序博客网 时间:2024/05/23 13:15


目前更新到5.3节,请在
http://dl.dbank.com/c02ackpwp6下载5.3节的全部文档

 

本节源代码请在http://dl.dbank.com/c04m68jx5a下载

 

第2节 任意任务间的切换

上一节我们使用2个固定的任务验证了操作系统任务切换的功能,但这些代码并不具有通用性,如果要扩充其它任务,就必须修改操作系统函数,这显然是不可接受的。操作系统作为独立于用户代码的部分,它的内部细节应该是不被用户所见的,是一个黑盒,需要做到用户只需要修改操作系统提供的接口文件里面的参数,调用接口函数就可以完全满足程序开发的要求。因此,在本节我们将对上节的代码做些改动,使其可以支持任意多个任务之间的互相切换,而又不需要修改Wanlix目录下的操作系统代码,仅仅是编写srccode目录下的用户代码,调用操作系统的接口函数即可,这样才真正实现了操作系统的独立性。

 

首先我们来看看任务切换函数——WLX_TaskSwitch。上节中,这个函数固定在两个任务之间切换,因此要实现可以切换到任何一个函数的功能就必须修改此函数,需要为这个函数增加一个入口参数,用这个入口参数来指明需要切换到的任务。WLX_TaskSwitch函数的主要功能是做好任务切换前的准备,将当前运行任务的栈指针和将要运行任务的栈指针存入到对应的全局变量中,上节中,为每个任务分别指定了guiTask21CurSpguiTask2CurSp全局变量保存它们的当前栈指针,每个全局变量绑定到了任务,因此,这个入口参数还必须能够关联到任务的栈指针。

为此,我们引入TCB的概念,在操作系统里这是一个非常重要的概念。TCBTask 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的地址来代表各个不同的任务。

第3章第2节 <wbr>任意任务间的切换

图 17  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_TestTask1TEST_TestTask2TEST_TestTask3,并将它们的TCB保存到全局变量gpstrTask1TcbgpstrTask1Tcb 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章第2节 <wbr>任意任务间的切换

图 18  可创建任意多个任务的运行结果

   读者也可通过本节的视频观察这3个任务的动态执行过程,读者也可以自行增加任务在单板上运行一下,体验本节的收获!

阅读全文
0 0
原创粉丝点击