⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 chapter3.txt

📁 ucos的PDF书籍、2.00源码以及我的学习心得
💻 TXT
字号:
3.01 任务
~~uC/OS中任务的书写有几个特点:
1、每个任务都是一个死循环;
2、uC/OS中所有任务的返回类型都是void;
3、在任务中必须包含延时等待语句,否则,那些优先级较该任务低的任务将无法执行!

框架:
void SampleTask(void *pdata)
{
    //...
    for(;;)
    {
        //...
        OSTimeDlyHMSM();
       //...
    }
}


3.03 Task Control Blocks (OS_TCBs)
~~uC/OS的TCB结构:
typedef struct os_tcb {
	OS_STK *OSTCBStkPtr;
#if OS_TASK_CREATE_EXT_EN
	void *OSTCBExtPtr;
	OS_STK *OSTCBStkBottom;
	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;	
其中,各成员变量的意义见书中83-84页的解释。

~~uC/OS中对TCB的组织
对于一个基于uC/OS内核创建的操作系统,它所能够支持的最多的任务个数,可以由系统的创建者在系统创建之初根据系统运行环境的情况(主要是RAM的情况),通过在文件OS_CFG.h中对
宏变量OS_MAX_TASKS进行定义来具体说明。
在系统运行时,uC/OS内核会根据OS_MAX_TASKS的值为系统固定分配(OS_MAX_TASKS + OS_N_SYS_TASKS) * sizeof(OS_TCB) bytes的
RAM空间(OSTCBTbl[])用于存放这些TCB(其中,OS_N_SYS_TASKS为系统任务的个数。在OS_TASK_STAT_EN设置为1时,OS_N_SYS_TASKS被定义为2,即存在两项系统任务-系统空闲进程OsTaskIdel和用于计算系统性能指标的进程OSTaskStat。系统任务在OSInit中被创建)。
同时,使用一个名为OSTCBFreeList的指针来记录TCB的使用情况。该指针相当于是由空闲的TCB所构成的链表的头指针。当有新任务创建,需要使用TCB时,将链表的第一个TCB从中删除,提供新任务使用;而当有任务被删除时,将此任务所使用的TCB加入链表的末尾,相当于对TCB的回收。

3.04 Ready List
~~uC/OS的最低优先级(OS_LOWEST_PRIO)通常分配给系统空闲进程(idel task),这样才能保证只有在系统中没有其他进程运行时才来运行系统空闲进程。

~~uC/OS对就绪进程的管理
由于uC/OS中最多可以同时运行64项任务,因此便于管理,作者将这64项任务分为8组每组包含8项任务,通过两个变量进行统一管理。
变量1:OSRdyGrp
这是一INT8U类型的数,变量中每1位(bit)表示一组任务的就绪情况。只要该组中有1项任务就绪,就将相应的位置1;只有当该组中不存在就绪任务时,相应的位才为0。
变量2:OSRdyTbl[]
这是一个INT8U类型数组的头指针,该数组共有8个成员变量,每一项成员变量都与OSRdyGrp中的相应位对应,而每一项成员变量中的每1位(bit)又同时表示了该组中相应任务的就绪情况。比如:OSRdyTbl[0]与OSRdyGrp的第0位相对应,表示第0组的就绪情况,而OSRdyTbl[0]的第0位又标示了该组中第0项任务的就绪情况。当OSRdyGrp的第0位为0时,OSRdyTbl[0]的值必定为0(表示没有任务就绪);而只要OSRdyTbl[0]的值不为0,那么OSRdyGrp的第0位一定为1(表示有任务就绪)。
对就绪进程这样进行组织有以下几项好处:
1、节省了使用空间,这个不言而喻;
2、便于对就绪进程的查找。由于就绪进程以上方式进行组织,在查找就绪进程时就不需要一一查找比较了,而是先查找OSRdyGrp的各位寻找不为0的位,以确定包含有就绪任务的组;然后再根据第1步中获得的组信息,对OSRdyTbl中相应的成员变量的各位进行查找,以确定具体就绪的任务。这相当于对就绪任务建立了两级索引,因此极大的加快了查找速度。
3、在第2点的基础上,经过一定的改进,使得对最高优先级就绪任务的确定可以在常数时间内完成,而与就绪任务的多少无关。这一点后边再说。

~~uC/OS对就绪进程的分组原则
uC/OS是通过优先级对64个任务进行分组的。对于uC/OS中的一项特定任务,其优先级是0 ~ 63中的一个数,且这个数是唯一的,即在系统中不存在其他任务的优先级与该任务的相同。由于优先级的这一特点,可以将优先级看作任务的代号,与特定的任务一一对应。在uC/OS中,通过将优先级的高3位作为组号,而低3位作为组中任务号的方式,将64项任务分成了8组,每组包含8项任务。对于优先级为prio的任务,它所属于的组的组号为prio >> 3,而在该组中它的任务号为prio & 0x07,所以在OSRdyGrp中它所在的组的就绪情况由该变量的第prio >> 3位来反映,而它具体的就绪情况由OSRdyTbl[prio >> 3]的第prio & 0x07位反映。

~~uC/OS中对最高优先级就绪进程的查找
根据上边对uC/OS中就绪进程分组原则的讨论,可以知道,组号越低,任务号越低的进程,其优先级越高。
如果采用一般的查找方式,对最高优先级就绪进程的查找应该遵循以下的步骤:
1、从OSRdyGrp的第0位开始,依次向左查找各位。第1个不为0的位所对应的组号,就是所要寻找的进程所在的组号;(组号越低原则)
比如:若OSRdyGrp的值为01000100,那么所要寻找的进程在第2组(组号从0开始)。
2、根据第1步中得到的组号信息,对OSRdyTbl的相应成员变量,从其第0位开始,依次向左查找各位。第1个不为0的位所对应的任务号,就是所要寻找的进程的任务号;(任务号越低原则)
3、由组号和任务号就可以确定最高优先级就绪进程的优先级值了,再根据该值得到相应的TCB。
以上所述方法的最长查找时间与就绪任务的个数有关,就绪任务越多,最长查找时间也就越大。为此,作者在一般查找方式的基础上,进行了一定程度的改进,使得查找可以在常数时间内完成。
改进后的查找方法:
观察一般查找方法的前两步可以发现,这两步有一个共同点:它们都是通过寻找相应INT8U类型变量中自右向左的第1个值为1的位来确定应该返回的组号或任务号的。比如:如果OSRdyGrp的值为01000001,那么第1步应该返回的组号就是0。可以看出如果OSRdyGrp的值定了,其返回值也就相应的定了,OSRdyTbl一样。
基于以上特点,作者引入了OSUnMapTbl。这是一个有256项数组,相当于一个哈西表,用来映射OSRdyGrp(或OSRdyTbl的成员)为特定值时的返回值。比如:当OSRdyGrp为00000110(6)时,OSUnMap[6]的值为1,表示应该返回的组号为1。
由于OSUnMapTbl的引入,对最高优先级就绪进程的查找将遵循以下步骤进行:
1、y = OSUnMapTbl[OSRdyGrp];	y为目标进程所在组的组号
2、x = OSUnMapTbl[OSTdyTbl[y]];	x为目标进程的任务号
3、prio = y << 3 + x;			prio为目标进程的优先级号
4、由优先级号prio在OSTCBPrioTbl中找到对应的TCB
可见,寻找最高优先级就绪进程的工作在常数时间内完成了!!!

3.05 Task Scheduling
~~uC/OS的进程调度有两种任务级的进程调度和中断级的进程调度。由于两种调度方式有些许的不同,因此分别用两个不同的函数完成。对任务级的进程调度使用OSSched函数完成,而对中断级的进程调度使用OSIntExit函数完成。

~~uC/OS中任务级进程调度的原理:
在阐述该原理之前,需要明确以下几个知识点:
(1)在进行函数调用时,CPU会自动将函数的返回地址(即在调用时刻,保存于程序计数器PC中的值)压入堆栈保存;
(2)当进行中断响应时,CPU会自动将函数的返回地址(即在响应时刻,保存于程序计数器PC中的值)以及CPU当前的状态字 (即在响应时刻,保存于状态寄存器SR中的值)压入堆栈保存;
(3)IRET指令(在80X86指令集中的中断返回指令)主要完成的操作为:
按照先PC后SR的顺序,将两个寄存器的值从堆栈中恢复出来。采用此顺序是因为在CPU响应中断时,PC和SR中的值是按照先SR后PC的顺序进行入栈保存的,根据堆栈先进后出的特点,出栈应该按照与入栈相反的顺序进行。(注意:完成相同功能的msp430指令为reti,该指令执行时PC和SR的出入栈顺序正好与上边的相反)

在明确以上知识点的基础上,现在开始讨论任务级进程调度具体的步骤。(这里基于80X86指令集,对于其他指令集如msp430指令集,主要区别在于堆栈结构中PC和SR位置的不同)
作者在这里将任务级的以软中断的形式实现。当在OSSched函数中使用宏OS_TASK_SW()时,实际上是执行了汇编指令int 0x80,而在中断向量0x80处保存的正是处理函数OSCtxSw的地址
(尽管目前我还没有搞清楚具体是怎样将0x80与OSCtxSw映射起来的,但OSCtxSw是中断0x80的处理函数这点应该是对的)。因此,根据知识点(2)当使用OSCtxSw进行进程调度时,CPU会按照处理中断的程序将当前CPU的SR及PC值依次存入当前待调出任务的堆栈之中。之后,在OSCtxSw中,会将当前CPU其他寄存器值存入待调出任务的堆栈之中。故在任务调出前,其堆栈结构应该是这样的:
CPU其他寄存器n的值		<- SP指针(栈顶)
...
CPU其他寄存器1的值
PC的值
SR的值
当任务被调出时,CPU需要做的工作就是将此时的SP的值保存到调出任务TCB的OSTCBStkPtr中,以便在以后重新调入此任务时使用。完成任务调出的工作之后,CPU将待调入任务TCB中OSTCBStkPtr的值赋给SP同时将堆栈中前n项值弹出覆盖CPU的其他寄存器。最后,使用IRET指令将PC和SR值用新任务的PC和SR值覆盖。这样,当OSCtxSw返回时,CPU将从调入任务PC所指示的地址处开始执行,因此也就完成了一次任务调度。

3.07 Idel Task
~~系统空闲进程的主要任务就是在系统没有其他进程运行的时候,由该进程占用CPU运行。它的主要操作就是循环的给一个32位的变量OSIdelCtr加1。由于uC/OS总是将系统的最低优先级赋给该进程,因此在此进程的函数体中,不需要添加任何的延时等待操作,因为只要系统中存在其他进程,其优先级一定比该进程高,必然会抢占CPU运行。另外,在此进程的函数体中可以发现,对OSIdelCtr的增量操作是在关闭中断的情况下完成的,这是因为在8位或16位的处理器中,对一个32位数的增量操作将不只需要一条汇编指令,为了防止在该操作执行过程中被其他高优先级的进程打断,因此需要关闭中断进行。

3.09 Interrupts under μC/OS-II
~~中断级的进程调度一般是在OSIntExit函数中由函数OSIntCtxSw完成,之所以使用OSIntCtxSw而非OSCtxSw主要有以下两个原因:
1、在OSCtxSw函数中,首先需要将当前CPU其他寄存器的值保存入当前任务堆栈中。这一步在OSIntCtxSw是不需要的,因为根据要求在用户书写响应特定中断的中断处理程序时,首先要将CPU其他寄存器的值压入堆栈保存,OSIntExit是在用户中断处理程序中调用的,所以在它调用时,CPU其他寄存器的值已然保存入当前任务堆栈之中了。
2、在OSIntCtxSw进行任务调出的工作之前,其堆栈结构与OSCtxSw的不同。这是因为OSIntCtxSw是在OSIntExit函数调用的,而OSIntExit函数又是在用户中断处理程序中通过函数形式调用的。故根据知识点(1)和(2),当调用OSIntExit时,堆栈的结构是这样的:
OSIntExit的返回地址			<-SP指针(栈顶)
CPU其他寄存器1的值
...
CPU其他寄存器n的值
PC的值
SR的值
另外,OSIntExit函数的源码是这样的结构的:
void OSIntExit(void)
{
	OS_ENTER_CRITICAL()
	...
	OS_EXIT_CRITICAL()
}
如果开关中断采用第二种方法,那么OS_ENTER_CRITICAL()是这样定义的{asm PUSHF;CTL},注意这里有一项将CPU当前状态字入栈的操作,所以在执行完OS_ENTER_CRITICAL()后,堆栈的结构为:
调用OSIntExit时SR的值		<-SP指针(栈顶)
OSIntExit的返回地址
CPU其他寄存器1的值
...
CPU其他寄存器n的值
PC的值
响应中断时SR的值
最后,当在OSIntExit中调用OSIntCtxSw时,由于是函数调用,根据知识点(1)需要将OSIntCtxSw的返回地址压入堆栈,所以最终当开始进行任务调度时,堆栈结构是这样的:
OSIntCtxSw的返回地址		<-SP指针(栈顶)
调用OSIntExit时SR的值
OSIntExit的返回地址
CPU其他寄存器1的值		<-SP*
...
CPU其他寄存器n的值
PC的值
响应中断时SR的值
为了能够在任务调出后,能够正常被重新调入。在保存当前任务PCB中OSTCBStkPtr的值时,应该采用SP*而非SP。这是OSIntCtxSw与OSCtxSw又一项不同的地方。

3.05 ~3.09
~~进程级的任务调度与中断级任务的调度哪种调度更常用呢?
一开始我觉得应该是进程级的任务调度要更常用一些,现在想想其实不然。
uC/OS中的任务一般都是具有这种结构的:
void task(void *pdata)
{
    /*application data*/
    for(;;)
    {
        /*application data*/
        OSTimeDly(...);		//or OSTimeDlyHMSM(...)
    }
}
注意在每个任务的死循环中都需要一个延时函数用于为其他低优先级的任务提供执行的机会。那么,当高优先级任务的延时时间结束时是什么使得其重新获得CPU的呢?答案是时间中断处理函数。该函数主要用于调用OSTimeTick函数将那些被延时函数(不止是OSTimeDly和OSTimeDlyHMSM)挂起的任务在其延时结束时重新置入就绪队列。而又由于在退出中断时,处理函数要执行OSIntExit函数判断是否存在高优先级的就绪任务以决定是否进行任务调度(当然,这里是中断级的任务调度)。因此,保证了那些被延时函数挂起的高优先级任务在其延时结束时可以迅速抢占CPU。在uC/OS系统中,由于延时原因而导致的任务切换时占多数的,因此中断级的任务调度也就较任务级的任务更常用。

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -