又到了来废话的时候了,今天这一节,我们把第三章结束,所讲述的内容如标题所示,废话不多讲,切正题。
任务堆栈:
所谓堆栈,就是在存储器中按数据“后进先出”原则组织的连续存储空间。为了满足任务切换和响应中断时保存CPU寄存器中的内容及任务调用其他函数时的需要,每个任务都有自己的堆栈。所有uCOS-II任务的任务控制块都有一个指向该任务堆栈的指针。
任务堆栈的创建:
为了方便定义堆栈,在系统文件OS_CPU.h中定义一个数据类型:
typedef unsigned int OS_TASK;
这样在定义任务堆栈栈区时,只要定义一个OS_TASK类型的数组即可:
#define TASK_STK_SIZE 512 //定义堆栈长度1024字节
OS_TASK TaskStk[TASK_STK_SIZE];
当调用系统函数OSTaskCreate()来创建一个任务时,把数组的指针传递给函数OSTaskCreate()中的堆栈栈顶参数ptos,就可以把该数组与任务关联起来而成为该任务的任务堆栈。
下面介绍一下创建任务函数OSTaskCreate()的原型:
INT8U OSTaskCreate( void (*task) (void * pd), //指向任务的指针
void * pdata, //传递给任务的参数
OS_STK *ptos, //任务堆栈栈顶的指针
INT8U prio //指定任务优先级级别的参数
);
这里注意使用的处理器所支持的堆栈增长方向,有些处理器增长方向从低地址指向高地址,有些则相反。所以导致我们任务堆栈的栈顶指针有两个选择,&MyTaskStk[0],&MyTaskSt[TASK_STK_SIZE-1];这就是我们常说的大小端吧,个人理解。
任务堆栈的初始化:
应用程序在创建一个新的任务时,必须把在系统启动这个任务时所需要的CPU个寄存器初始数据(任务指针,任务堆栈指针及程序状态字等)事先存放在任务堆栈,这样当获得CPU使用权时,把堆栈的内容复制到CPU的各寄存器,是任务顺利运行。
任务堆栈初始化函数OSTaskStkInit(),就是干这个的,其原型如下:
OS_STK *OSTaskStkInit( void (*task)(void *pd),
void *pdata,
OS_STK *ptos,
INT16U opt
);
通常用户不会接触这个函数,该函数是由系统任务创建函数OSTaskCreate()调用。
任务控制块及其链表:
任务控制块是一个结构类型数据,当用户应用程序调用OSTaskCreate()函数创建一个用户任务时,该函数就回去任务控制块中的所有成员赋予与该任务相关的数据,并驻留在RAM中,任务控制块结构定义如下:
typedef struct os_tcb{
OS_STK *OSTCBStkPtr; //指向任务堆栈栈顶的指针
#if OS_TASK_CREATE_EXT_EN
void *OSTCBExtPtr; //指向任务控制块扩展的指针
OS_STK *OSTCBStkBootom; //指向任务堆栈栈底的指针
INT32U OSTCBStkSize; //任务堆栈的长度
INT16U OSTCBOpt; //创建任务时的选择项
INT16U OSTCBId; //目前该未使用
#endif
struct os_tcb * OSTCBNext; //指向后一个任务控制块的指针
struct os_tcb * OSTCBPrev; //指向前一个任务控制块的指针
#if(OS_Q_EN && (OS_MAX_QS >= 2))|| OS_MBOX_EN || OS_Sem_EN
OS_EVENT * OSTCBEventPtr; //指向时间控制块的指针
#endif
#if(OS_Q_EN && (OS_MAX_QS >= 2))|| OS_MBOX_EN
void *OSTCBMsg; //指向传递给任务消息的指针
#endif
INT16U OSTCBDly; //任务等待的时限
INt8U OSTCBStat; //任务当前状态标志
INT8U OSTCBPrio; //任务的优先级别
INT8U OSTCBX; //用于快速访问就绪表的数据
INT8U OSTCBY; //用于快速访问就绪表的数据
INT8U OSTCBBitX; //用于快速访问就绪表的数据
INT8U OSTCBBitY; //用于快速访问就绪表的数据
#if OS_TASK_DEL_EN
BOOLEAN OSTCBDelReq; //用于请求删除任务时用到的标志
#endif
}OS_TCB;
任务控制块链表:
uCOS-II在初始化时也要按照配置文件所设定的任务数是先定义一批空白任务控制块,这样当创建一个任务时,只需要哪一个空白任务填上任务属性即可。
也就是说uCOS-II需要两条链表:一条空任务块链表,一条任务块链表。具体做法是:系统在调用函数OSInit()对系统进行初始化时,就在RAM中建立一个OS_TCB结构类型的数组OSTCBTbl[],然后把各个元素链接成一个链表,从而形成一个空白任务块链表。
uCOS-II初始化时简历的空任务链表的元素一共是OS_MAX_TASKS+OS_N_SYS_TASKS个,其中定义在文件OS_CFG.h中的常数OS_MAX_TASKS指明了用户任务最大数,而定义在文件UCOS_II.h中的常数OS_N_SYS_TASKS指明了系统任务数目(空闲任务,统计任务)。
当应用程序调用系统函数OSTaskCreate()或OSTaskCreateExt()创建任务时,系统会将空任务控制链表头指针OSTCBFreeList指向的任务控制块分配给该任务,在给任务控制块中的 各成员赋值后,系统就按任务控制块链表的头指针OSTCBFreeList将其加入任务控制块链表。
之前,我们提到过,为了加快访问速度,我们还创建一个指针数组,其元素指向任务控制块链表中的元素,uCOS-II在uCOS_II.h文件中定义了一个OS_TCB*数据类型的数组OSprioTbl[]。该数组以任务优先级为顺序在各元素存放了指向了各个任务控制块的指针。C语言中把常用的变量放在寄存器中,加快访问,同样uCOS中把访问频率最高的控制块用OSTCBCur来存放这个任务控制块的指针。
任务控制块的初始化:
当用户应用程序调用函数OSTaskCreate()创建任务时,这个函数会调用系统函数OSTCNInit()来为任务控制块进行初始化,其原型如下:
INT8U OSTCBInit( INT8U prio, //任务的优先级别,保存在OSTCBPrio
OS_STK * ptos, //任务堆栈栈顶指针 保存在OSTCBStkPtr
OS_STK* pbos, //任务堆栈栈底指针,保存在OSTCBBottom
INT16U id, //任务的标识符 保存在OSTCBId
INT16U stk_size, //任务堆栈长度
void * pext, //任务控制块的扩展指针,保存在OSTCBExtPtr
INT16U opt //任务控制块的选项
);
该函数的主要任务如下:
1.为被创建任务从空任务控制块链表获取一个任务控制块,
2.用任务的属性对任务控制块各个成员进行赋值
3.把这个任务控制块链入到任务控制块链表
任务就绪表及任务调度:
为系统中处于就绪状态的任务分配CPU是多任务操作系统的核心工作,这就涉及到:一判断哪些任务处于就绪状态,二进行任务调度。,从之前的讲解中我们知道,系统总是把处于就绪状态的任务中选择一个来运行,系统如何知道这个任务有没有处于就绪状态,这时候任务就绪表,它登记了系统中所有处于就绪状态的任务。说白了任务就绪表就是一个位图,每个任务在这个就绪表中占有一个二进制位,0或1表示是否处于就绪状态。
简单的说任务就绪表是一个INT8U类型的OSRduTbl[]数组,每个任务的就绪状态占据一位,一个OSRdyTbl[]元素可表达8个任务的状态,我们将这8个元素看作一个任务组,为了方便查找,uCOS-II定义一个INT8U类型的OSRdyGrp变量,其每个位对应一个任务组,由于OSRdyTbl有8个二进制位,每位对应一个任务组,每个任务组包含8个任务,所以uCOS-II最多可管理8*8=64个任务。
如何根据任务的优先级来找到任务所在就绪表具体位置呢,我们知道优先级是个单字节数,最大值不会超过63,即二进制的00111111,因此就可以用高三位来指明变量OSRdyTbl的具体数据位,找到该任务所在哪个任务组,用低三位来表示所在任务组的具体数据位。明白这一点非常重要。
系统对于就绪表主要有三个操作:登记、注销、从就绪表中得知就绪任务的最高优先级任务的标识。
//登记
OSRdyGrp |= OSMapTbl[prio>>3]; //确定哪个任务组
OSRdyTbl[prio>>3] = OSMapTbl[prio&0x07]; //确定这个任务组的具体数据位
OSMaoTbl[0] = 00000001B;
OSMaoTbl[1] = 00000010B;
OSMaoTbl[2] = 00000100B;
OSMaoTbl[3] = 00001000B;
OSMaoTbl[4] = 00010000B;
OSMaoTbl[5] = 00100000B;
OSMaoTbl[6] = 01000000B;
OSMaoTbl[7] = 10000000B;
//注销
if((OSRdyTbl[prio>>3] &= -OSMapTbl[prio&0x07] )== 0;
OSRdyGrp &= -OSMapTbl[prio>>3];
//最高优先级的就绪任务查找方法1
y = OSUnMapTbl[OSRdyGrp]; //获得优先级的D3 D4 D5位
x = OSUnMapTbl[OSRdyTbl[y]]; //获得优先级的D0 D1 D2位
prio = (y<<3)+x;
//最高优先级的就绪任务查找方法2
y = OSUnMapTbl[OSRdyGrp];
prio = (INT8U)(y<<3)+OSUnMapTal[OSRdyTbl[y]];
//OSUnMapTbl[]也是uCOS-II为提高快速查找定义的一个数组,共256个元素
一开始很多读者跟我一样想,****,一个查找怎么复杂,没办法为了追求实时性,所以使用这种方法,多见几次就好了。
uCOS-II经常使用类似于就绪表形式的表来记录任务的某种状态,因此务必熟悉表的结构和基本操作。
任务的调度:
uCOS-II的任务调度思想是:近似地让CPU一直处于工作状态。具体做法就是:在系统或者用户任务调用系统函数及执行中断服务程序结束时调用调度器,确定应该运行的任务并运行。
任务调度器的主要工作有两种:在就绪表中查找优先级别最高的就绪任务,二是实现任务的切换。uCOS-II有两种调度器:一是任务级的调度器OSSched(),二是中断级的调度器OSIntExt();
这里主要针对任务级的调度器,前面已经就获取就绪表最高优先级的任务已经说了很多,这是主要实现如何进行任务切换,调度器把任务切换分为两个步骤:一获得待运行任务的TCB指针,二进行断点数据的切换。
因为操作系统是通过任务的任务控制块TCB来管理任务的,因此调度器真正实施任务的切换的工作就是获得待运行任务的任务控制块指针和当前任务的任务控制块指针,而当前任务的TCB指针就存放在全局变量OSTCBCur中,所以只需要获得待运行任务TCB指针就好了。OSSched的源代码如下:
void OSSched(void)
{
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cpu_sr;
#endif
INT8U y;
OS_ENTER_CRITCAL();
if((OSLockNesting|OSIntNesting) == 0)
{
y = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy //获得最高优先级任务
= (INT8U)(y<<3)+UnMapTbl[OSRdytbl[y]];
if(OSPrioHighRdy != OSPrioCur)
{
OSTCBHighRdy //得到任务控制块指针
= OSTCBPrioTbl[OSPrioHighRdy];
OSCtSwCtr++; //统计任务切换次数的计数器+1
OS_TASK_SW();
}
}
OS_EXIT_CRITICAL();
}
uCOS-II允许应用程序通过调用函数OSSchedLock()和OSSchedUnlock()给调度器上锁和解锁,为了记录调度器上锁解锁情况,系统定义了一个变量OSLockNesting,上锁+1,解锁-1,来来了解调度器上锁的嵌套次数,上面已经获得了待运行任务的TCB指针OSTCBHighRdy,真正完成任务的切换是在OSSched()代码中OS_TASK_SW()任务切换宏去完成的。
任务的切换就是断点数据的切换,断点数据的切换也就是CPU堆栈指针的切换,被中止运行任务的任务堆栈指针要保护到该任务的任务控制块中,待运行任务的任务堆栈指针要有该任务控制块转存到CPU的SP中,未完成上述操作,OSCtxSw()要依次完成以下7项工作:
1.把被中止任务的断点指针保存到任务堆栈中
2.把CPU通用寄存器的内容保存到任务堆栈中
3.把被中止任务的任务堆栈指针当前值保存到该任务的任务控制块的OSTCBStkPtr中,
4.获得待运行任务的任务控制块
5.使CPU通过任务控制块获得待运行任务的任务堆栈指针
6.把待运行任务堆栈中通用寄存器内容恢复到CPU的通用寄存器中
7.使CPU获得待运行任务的断点指针(该指针是待运行任务在上一次被调度器中止运行时保存在任务堆栈中)
根据前面的了解,我们基本上完成了2到6项,下面完成1和7,众所周知(我就不知道),CPU是按其中一个特殊功能寄存器----程序指针PC(程序计数器)的指向来运行程序,或者说只有使PC寄存器获得新任务的地址,才会使CPU运行新的程序。既然如此,对于被中止任务,应该把任务的断点指针(在PC寄存器中)压入任务堆栈,对于待运行任务,把任务堆栈中上一次被中断时待运行任务的中断指针推入到PC寄存器中,但是目前处理器没有针对PC的出栈进栈指令,所以要另辟蹊径。想办法引发一次中断,并让中断向量指向OSCtxSw()(其实这个函数就是中断函数),利用系统在跳转到中断服务程序会自动把断点指针压入到堆栈的功能,把断点指针存入到堆栈,而利用中断返回指令IRET能把断点指针推入到CPU的PC寄存器功能,恢复待运行任务的断点,这样就实现断点数据的保存和恢复了。OSCtxSw()都是用汇编编写因为涉及寄存器操作,这里只给出示意型代码:
void OSCtxSw(void)
{
用压栈指令把CPU通用寄存器R1,R2.。。。。压入堆栈
OSTCBCur -> OSTCBStkPtr = SP; //在中止任务控制块保存SP
OSTCBCur = OSTCBHighRdy; //任务控制块的切换
OSPrioCur = OSPrioHighRdy;
SP = OSTCBHighRdy -> OSTCBStkPtr; //使SP指向待运行任务堆栈
用出栈指令把R1,R2.。。。。弹入到CPU的通用寄存器
IRET; //中断返回,使PC指向待运行任务
}
那么如何引发中断? 就是宏OS_TASK_SW()的作用了,如果使用的微处理器具有软中断指令,在这个宏中封装一个软中断指令,如果没有,可以在这个宏封装其他可以使PC等相关寄存器压栈的指令(如调用指令)。
看来这第三章一次还说不完,还得再开一节,不过概念和基本点已经说完,后面的主要是应用了。不写了,手腕疼。