I
第 3章 内核结构 ................................................................................................................... 1
3.0 临界段(Critical Sections).......................................................................................... 1
3.1 任务 ........................................................................................................................... 1
3.2 任务状态 ................................................................................................................... 3
3.3 任务控制块(Task Control Blocks, OS_TCBs) .................................................... 4
3.4 就绪表(Ready List) ............................................................................................. 7
3.5 任务调度(Task Scheduling) .............................................................................. 10
3.6 给调度器上锁和开锁(Locking and UnLocking the Scheduler) ............................ 11
3.7 空闲任务(Idle Task) ............................................................................................... 12
3.8 统计任务 ................................................................................................................. 13
3.9 μC/OS 中的中断处理 ............................................................................................. 16
3.10 时钟节拍 ................................................................................................................. 20
3.11 μC/OS-Ⅱ初始化 .................................................................................................... 23
3.12 μC/OS-Ⅱ的启动 .................................................................................................... 24
3.13 获取当前μC/OS-Ⅱ的版本号 ............................................................................... 27
3.14 OSEvent???()函数 .................................................................................................. 28
3-1
第3章 内核结构
本章给出μC/OS-Ⅱ的主要结构概貌。读者将学习以下一些内容;
μC/OS-Ⅱ是怎样处理临界段代码的;
什么是任务,怎样把用户的任务交给μC/OS-Ⅱ;
任务是怎样调度的;
应用程序 CPU 的利用率是多少,μC/OS-Ⅱ是怎样知道的;
怎样写中断服务子程序;
什么是时钟节拍,μC/OS-Ⅱ是怎样处理时钟节拍的;
μC/OS-Ⅱ是怎样初始化的,以及
怎样启动多任务;
本章还描述以下函数,这些服务于应用程序:
OS_ENTER_CRITICAL() 和 OS_EXIT_CRITICAL(),
OSInit(),
OSStart(),
OSIntEnter() 和 OSIntExit(),
OSSchedLock() 和 OSSchedUnlock(), 以及
OSVersion().
3.0 临界段(Critical Sections)
和其它内核一样,μC/OS-Ⅱ为了处理临界段代码需要关中断,处理完毕后再开中断。
这使得μC/OS-Ⅱ能够避免同时有其它任务或中断服务进入临界段代码。关中断的时间是实
时内核开发商应提供的最重要的指标之一,因为这个指标影响用户系统对实时事件的响应
性。μC/OS-Ⅱ努力使关中断时间降至最短,但就使用μC/OS-Ⅱ而言,关中断的时间很大程
度上取决于微处理器的架构以及编译器所生成的代码质量。
微处理器一般都有关中断/开中断指令,用户使用的 C 语言编译器必须有某种机制能够
在 C 中直接实现关中断/开中断地操作。某些 C 编译器允许在用户的 C 源代码中插入汇编语
言的语句。这使得插入微处理器指令来关中断/开中断很容易实现。而有的编译器把从 C 语
言中关中断/开中断放在语言的扩展部分。μC/OS-Ⅱ定义两个宏(macros)来关中断和开中
断,以便避开不同 C 编译器厂商选择不同的方法来处理关中断和开中断。μC/OS-Ⅱ中的这
两个宏调用分别是:OS_ENTER_CRITICAL()和 OS_EXIT_CRITICAL()。因为这两个宏的定义取
决于所用的微处理器,故在文件 OS_CPU.H 中可以找到相应宏定义。每种微处理器都有自己
的 OS_CPU.H 文件。
3.1 任务
一个任务通常是一个无限的循环[L3.1(2)],如程序清单 3.1 所示。一个任务看起来像
其它 C 的函数一样,有函数返回类型,有形式参数变量,但是任务是绝不会返回的。故返回
参数必须定义成 void[L3.1(1)]。
3-2
程序清单 L3.1 任务是一个无限循环
void YourTask (void *pdata) (1)
{
for (;;) { (2)
/* 用户代码 */
调用uC/OS-II的某种系统服务:
OSMboxPend();
OSQPend();
OSSemPend();
OSTaskDel(OS_PRIO_SELF);
OSTaskSuspend(OS_PRIO_SELF);
OSTimeDly();
OSTimeDlyHMSM();
/* 用户代码 */
}
}
不同的是,当任务完成以后,任务可以自我删除,如清单 L3.2 所示。注意任务代码并
非真的删除了,μC/OS-Ⅱ只是简单地不再理会这个任务了,这个任务的代码也不会再运行,
如果任务调用了 OSTaskDel(),这个任务绝不会返回什么。
程序清单 L 3.2 . 任务完成后自我删除
void YourTask (void *pdata)
{
/* 用户代码 */
OSTaskDel(OS_PRIO_SELF);
}
形式参数变量[L3.1(1)]是由用户代码在第一次执行的时候带入的。请注意,该变量的
类型是一个指向 void 的指针。这是为了允许用户应用程序传递任何类型的数据给任务。这
个指针好比一辆万能的车子,如果需要的话,可以运载一个变量的地址,或一个结构,甚至
是一个函数的地址。也可以建立许多相同的任务,所有任务都使用同一个函数(或者说是同
一个任务代码程序), 见第一章的例 1。例如,用户可以将四个串行口安排成每个串行口都
是一个单独的任务,而每个任务的代码实际上是相同的。并不需要将代码复制四次,用户可
以建立一个任务,向这个任务传入一个指向某数据结构的指针变量,这个数据结构定义串行
口的参数(波特率、I/O 口地址、中断向量号等)。
μC/OS-Ⅱ可以管理多达 64 个任务,但目前版本的μC/OS-Ⅱ有两个任务已经被系统占
用了。作者保留了优先级为 0、1、2、3、OS_LOWEST_PRIO-3、OS_LOWEST_PRI0-2,
OS_LOWEST_PRI0-1 以及 OS_LOWEST_PRI0 这 8 个任务以被将来使用。OS_LOWEST_PRI0 是作为
定义的常数在 OS_CFG.H 文件中用定义常数语句#define constant 定义的。因此用户可以有
多达 56 个应用任务。必须给每个任务赋以不同的优先级,优先级可以从 0 到
OS_LOWEST_PR10-2。优先级号越低,任务的优先级越高。μC/OS-Ⅱ总是运行进入就绪态的
3-3
优先级最高的任务。目前版本的μC/OS-Ⅱ中,任务的优先级号就是任务编号(ID)。优先级
号(或任务的 ID 号)也被一些内核服务函数调用,如改变优先级函数 OSTaskChangePrio(),
以及任务删除函数 OSTaskDel()。
为了使μC/OS-Ⅱ能管理用户任务,用户必须在建立一个任务的时候,将任务的起始地
址与其它参数一起传给下面两个函数中的一个:OSTastCreat 或 OSTaskCreatExt()。
OSTaskCreateExt()是 OSTaskCreate()的扩展,扩展了一些附加的功能。,这两个函数的解
释见第四章,任务管理。
3.2 任务状态
图 3.1 是μC/OS-Ⅱ控制下的任务状态转换图。在任一给定的时刻,任务的状态一定是
在这五种状态之一。
睡眠态(DORMANT)指任务驻留在程序空间之中,还没有交给μC/OS-Ⅱ管理,(见程序
清单 L3.1 或 L3.2)。把任务交给μC/OS-Ⅱ是通过调用下述两个函数之一:OSTaskCreate()
或 OSTaskCreateExt()。当任务一旦建立,这个任务就进入就绪态准备运行。任务的建立可
以是在多任务运行开始之前,也可以是动态地被一个运行着的任务建立。如果一个任务是被
另一个任务建立的,而这个任务的优先级高于建立它的那个任务,则这个刚刚建立的任务将
立即得到 CPU 的控制权。一个任务可以通过调用 OSTaskDel()返回到睡眠态,或通过调用该
函数让另一个任务进入睡眠态。
调用 OSStart()可以启动多任务。OSStart()函数运行进入就绪态的优先级最高的任务。
就绪的任务只有当所有优先级高于这个任务的任务转为等待状态,或者是被删除了,才能进
入运行态。
图 3.1 任务的状态
正在运行的任务可以通过调用两个函数之一将自身延迟一段时间,这两个函数是
OSTimeDly()或 OSTimeDlyHMSM()。这个任务于是进入等待状态,等待这段时间过去,下一
个优先级最高的、并进入了就绪态的任务立刻被赋予了 CPU 的控制权。等待的时间过去以后,
系统服务函数 OSTimeTick()使延迟了的任务进入就绪态(见 3.10节,时钟节拍)。
正在运行的任务期待某一事件的发生时也要等待,手段是调用以下 3 个函数之一:
3-4
OSSemPend(),OSMboxPend(),或 OSQPend()。调用后任务进入了等待状态(WAITING)。当
任务因等待事件被挂起(Pend),下一个优先级最高的任务立即得到了 CPU 的控制权。当事
件发生了,被挂起的任务进入就绪态。事件发生的
报告
软件系统测试报告下载sgs报告如何下载关于路面塌陷情况报告535n,sgs报告怎么下载竣工报告下载
可能来自另一个任务,也可能来自中
断服务子程序。
正在运行的任务是可以被中断的,除非该任务将中断关了,或者μC/OS-Ⅱ将中断关了。
被中断了的任务就进入了中断服务态(ISR)。响应中断时,正在执行的任务被挂起,中断服
务子程序控制了 CPU的使用权。中断服务子程序可能会报告一个或多个事件的发生,而使一
个或多个任务进入就绪态。在这种情况下,从中断服务子程序返回之前,μC/OS-Ⅱ要判定,
被中断的任务是否还是就绪态任务中优先级最高的。如果中断服务子程序使一个优先级更高
的任务进入了就绪态,则新进入就绪态的这个优先级更高的任务将得以运行,否则原来被中
断了的任务才能继续运行。
当所有的任务都在等待事件发生或等待延迟时间结束,μC/OS-Ⅱ执行空闲任务(idle
task),执行 OSTaskIdle()函数。
3.3 任务控制块(Task Control Blocks, OS_TCBs)
一旦任务建立了,任务控制块 OS_TCBs 将被赋值(程序清单 3.3)。任务控制块是一个
数据结构,当任务的 CPU 使用权被剥夺时,μC/OS-Ⅱ用它来保存该任务的状态。当任务重
新得到 CPU使用权时,任务控制块能确保任务从当时被中断的那一点丝毫不差地继续执行。
OS_TCBs 全部驻留在 RAM 中。读者将会注意到笔者在组织这个数据结构时,考虑到了各成员
的逻辑分组。任务建立的时候,OS_TCBs 就被初始化了(见第四章 任务管理)。
程序清单 L 3.3 µC/OS-II任务控制块.
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
3-5
INT16U OSTCBDly;
INT8U OSTCBStat;
INT8U OSTCBPrio;
INT8U OSTCBX;
INT8U OSTCBY;
INT8U OSTCBBitX;
INT8U OSTCBBitY;
#if OS_TASK_DEL_EN
BOOLEAN OSTCBDelReq;
#endif
} OS_TCB;
.OSTCBStkPtr 是指向当前任务栈顶的指针。μC/OS-Ⅱ允许每个任务有自己的栈,尤为重要
的是,每个任务的栈的容量可以是任意的。有些商业内核要求所有任务栈的容量都一样,除
非用户写一个复杂的接口函数来改变之。这种限制浪费了 RAM,当各任务需要的栈空间不同
时,也得按任务中预期栈容量需求最多的来分配栈空间。OSTCBStkPtr 是 OS_TCB 数据结构
中唯一的一个能用汇编语言来处置的变量(在任务切换段的代码 Context-switching code
之中,)把 OSTCBStkPtr 放在数据结构的最前面,使得从汇编语言中处理这个变量时较为容
易。
.OSTCBExtPtr 指向用户定义的任务控制块扩展。用户可以扩展任务控制块而不必修改μ
C/OS-Ⅱ的源代码。.OSTCBExtPtr 只在函数 OstaskCreateExt()中使用,故使用时要将
OS_TASK_CREAT_EN 设为 1,以允许建立任务函数的扩展。例如用户可以建立一个数据结构,
这个数据结构包含每个任务的名字,或跟踪某个任务的执行时间,或者跟踪切换到某个任务
的次数(见例 3)。注意,笔者将这个扩展指针变量放在紧跟着堆栈指针的位置,为的是当
用户需要在汇编语言中处理这个变量时,从数据结构的头上算偏移量比较方便。
.OSTCBStkBottom 是指向任务栈底的指针。如果微处理器的栈指针是递减的,即栈存储器从
高地址向低地址方向分配,则 OSTCBStkBottom 指向任务使用的栈空间的最低地址。类似地,
如果微处理器的栈是从低地址向高地址递增型的,则 OSTCBStkBottom 指向任务可以使用的
栈空间的最高地址。函数 OSTaskStkChk()要用到变量 OSTCBStkBottom,在运行中检验栈空
间的使用情况。用户可以用它来确定任务实际需要的栈空间。这个功能只有当用户在任务建
立 时 允 许 使 用 OSTaskCreateExt() 函 数 时 才 能 实 现 。 这 就 要 求 用 户 将
OS_TASK_CREATE_EXT_EN 设为 1,以便允许该功能。
.OSTCBStkSize 存有栈中可容纳的指针元数目而不是用字节(Byte)表示的栈容量总数。也
就是说,如果栈中可以保存 1,000 个入口地址,每个地址宽度是 32 位的,则实际栈容量是
4,000 字节。同样是 1,000 个入口地址,如果每个地址宽度是 16 位的,则总栈容量只有 2,000
字节。在函数 OSStakChk()中要调用 OSTCBStkSize。同理,若使用该函数的话,要将
OS_TASK_CREAT_EXT_EN 设为 1。
.OSTCBOpt 把“选择项”传给 OSTaskCreateExt(),只有在用户将 OS_TASK_CREATE_EXT_EN
设为 1 时,这个变量才有效。μC/OS-Ⅱ目前只支持 3 个选择项(见 uCOS_II.H):
OS_TASK_OTP_STK_CHK, OS_TASK_OPT_STK_CLR 和 OS_TASK_OPT_SAVE_FP 。
OS_TASK_OTP_STK_CHK 用于告知 TaskCreateExt(),在任务建立的时候任务栈检验功能得
3-6
到了允许。OS_TASK_OPT_STK_CLR 表示任务建立的时候任务栈要清零。只有在用户需要有栈
检验功能时,才需要将栈清零。如果不定义 OS_TASK_OPT_STK_CLR,而后又建立、删除了任
务,栈检验功能报告的栈使用情况将是错误的。如果任务一旦建立就决不会被删除,而用户
初始化时,已将 RAM 清过零,则 OS_TASK_OPT_STK_CLR 不需要再定义,这可以节约程序执行
时间。传递了 OS_TASK_OPT_STK_CLR 将增加 TaskCreateExt()函数的执行时间,因为要将栈
空间清零。栈容量越大,清零花的时间越长。最后一个选择项 OS_TASK_OPT_SAVE_FP 通知
TaskCreateExt(),任务要做浮点运算。如果微处理器有硬件的浮点协处理器,则所建立的
任务在做任务调度切换时,浮点寄存器的内容要保存。
.OSTCBId 用于存储任务的识别码。这个变量现在没有使用,留给将来扩展用。
.OSTCBNext 和.OSTCBPrev 用于任务控制块 OS_TCBs 的双重链接,该链表在时钟节拍函数
OSTimeTick()中使用,用于刷新各个任务的任务延迟变量.OSTCBDly,每个任务的任务控制
块 OS_TCB 在任务建立的时候被链接到链表中,在任务删除的时候从链表中被删除。双重连
接的链表使得任一成员都能被快速插入或删除。
.OSTCBEventPtr 是指向事件控制块的指针,后面的章节中会有所描述(见第 6章 任务间通
讯与同步)。
.OSTCBMsg 是指向传给任务的消息的指针。用法将在后面的章节中提到(见第 6 章任务间通
讯与同步)。
.OSTCBDly 当需要把任务延时若干时钟节拍时要用到这个变量,或者需要把任务挂起一段时
间以等待某事件的发生,这种等待是有超时限制的。在这种情况下,这个变量保存的是任务
允许等待事件发生的最多时钟节拍数。如果这个变量为 0,表示任务不延时,或者表示等待
事件发生的时间没有限制。
.OSTCBStat 是任务的状态字。当.OSTCBStat 为 0,任务进入就绪态。可以给.OSTCBStat 赋
其它的值,在文件 uCOS_II.H 中有关于这个值的描述。
.OSTCBPrio 是任务优先级。高优先级任务的.OSTCBPrio 值小。也就是说,这个值越小,任
务的优先级越高。
.OSTCBX, .OSTCBY, .OSTCBBitX 和 .OSTCBBitY 用于加速任务进入就绪态的过程或进入等待
事件发生状态的过程(避免在运行中去计算这些值)。这些值是在任务建立时算好的,或者
是在改变任务优先级时算出的。这些值的算法见程序清单 L3.4。
程序清单 L 3.4 任务控制块OS_TCB中几个成员的算法
OSTCBY = priority >> 3;
OSTCBBitY = OSMapTbl[priority >> 3];
OSTCBX = priority & 0x07;
OSTCBBitX = OSMapTbl[priority & 0x07];
.OSTCBDelReq 是一个布尔量,用于表示该任务是否需要删除,用法将在后面的章节中描述
(见第 4 章 任务管理)
应用程序中可以有的最多任务数(OS_MAX_TASKS)是在文件 OS_CFG.H 中定义的。这个
最多任务数也是μC/OS-Ⅱ分配给用户程序的最多任务控制块 OS_TCBs 的数目。将
OS_MAX_TASKS 的数目设置为用户应用程序实际需要的任务数可以减小 RAM 的需求量。所有
的任务控制块 OS_TCBs 都是放在任务控制块列表数组 OSTCBTbl[]中的。请注意,μC/OS-Ⅱ
分配给系统任务 OS_N_SYS_TASKS 若干个任务控制块,见文件μC/OS-Ⅱ.H,供其内部使用。
3-7
目前,一个用于空闲任务,另一个用于任务统计(如果 OS_TASK_STAT_EN 是设为 1 的)。在
μC/OS-Ⅱ初始化的时候,如图 3.2 所示,所有任务控制块 OS_TCBs 被链接成单向空任务链
表。当任务一旦建立,空任务控制块指针 OSTCBFreeList 指向的任务控制块便赋给了该任务,
然后 OSTCBFreeList 的值调整为指向下链表中下一个空的任务控制块。一旦任务被删除,任
务控制块就还给空任务链表。
图 3.2 空任务列表
3.4 就绪表(Ready List)
每个任务被赋予不同的优先级等级,从 0 级到最低优先级 OS_LOWEST_PR1O,包括 0 和
OS_LOWEST_PR1O 在内(见文件 OS_CFG.H)。当μC/OS-Ⅱ初始化的时候,最低优先级
OS_LOWEST_PR1O 总是被赋给空闲任务 idle task。注意,最多任务数目 OS_MAX_TASKS 和最
低优先级数是没有关系的。用户应用程序可以只有 10个任务,而仍然可以有 32 个优先级的
级别(如果用户将最低优先级数设为 31的话)。
每个任务的就绪态标志都放入就绪表中的,就绪表中有两个变量 OSRedyGrp 和
OSRdyTbl[]。在 OSRdyGrp 中,任务按优先级分组,8 个任务为一组。OSRdyGrp 中的每一位
表示 8 组任务中每一组中是否有进入就绪态的任务。任务进入就绪态时,就绪表 OSRdyTbl[]
中的相应元素的相应位也置位。就绪表 OSRdyTbl[]数组的大小取决于 OS_LOWEST_PR1O(见文
件 OS_CFG.H)。当用户的应用程序中任务数目比较少时,减少 OS_LOWEST_PR1O 的值可以降
低μC/OS-Ⅱ对 RAM(数据空间)的需求量。
为确定下次该哪个优先级的任务运行了,内核调度器总是将 OS_LOWEST_PR1O 在就绪表
中相应字节的相应位置 1。OSRdyGrp 和 OSRdyTbl[]之间的关系见图 3.3,是按以下规则给出
的:
当 OSRdyTbl[0]中的任何一位是 1 时,OSRdyGrp 的第 0 位置 1,
当 OSRdyTbl[1]中的任何一位是 1时,OSRdyGrp 的第 1 位置 1,
当 OSRdyTbl[2]中的任何一位是 1 时,OSRdyGrp 的第 2 位置 1,
当 OSRdyTbl[3]中的任何一位是 1 时,OSRdyGrp 的第 3 位置 1,
当 OSRdyTbl[4]中的任何一位是 1 时,OSRdyGrp 的第 4 位置 1,
当 OSRdyTbl[5]中的任何一位是 1时,OSRdyGrp 的第 5 位置 1,
当 OSRdyTbl[6]中的任何一位是 1 时,OSRdyGrp 的第 6 位置 1,
当 OSRdyTbl[7]中的任何一位是 1时,OSRdyGrp 的第 7 位置 1,
程序清单 3.5 中的代码用于将任务放入就绪表。Prio是任务的优先级。
3-8
程序清单 L3.5 使任务进入就绪态
OSRdyGrp |= OSMapTbl[prio >> 3];
OSRdyTbl[prio >> 3] |= OSMapTbl[prio & 0x07];
表 T3.1 OSMapTbl[]的值
Index Bit Mask (Binary)
0 00000001
1 00000010
2 00000100
3 00001000
4 00010000
5 00100000
6 01000000
7 10000000
读者可以看出,任务优先级的低三位用于确定任务在总就绪表 OSRdyTbl[]中的所在位。
接下去的三位用于确定是在 OSRdyTbl[]数组的第几个元素。OSMapTbl[]是在 ROM 中的(见
文件 OS_CORE.C)屏蔽字,用于限制 OSRdyTbl[]数组的元素下标在 0 到 7之间,见表 3.1
3-9
图 3.3μC/OS-Ⅱ就绪表
如果一个任务被删除了,则用程序清单 3.6 中的代码做求反处理。
程序清单 L3.6 从就绪表中删除一个任务
if ((OSRdyTbl[prio >> 3] &= ~OSMapTbl[prio & 0x07]) == 0)
OSRdyGrp &= ~OSMapTbl[prio >> 3];
以上代码将就绪任务表数组 OSRdyTbl[]中相应元素的相应位清零,而对于 OSRdyGrp,
只有当被删除任务所在任务组中全组任务一个都没有进入就绪态时,才将相应位清零。也就
是说 OSRdyTbl[prio>>3]所有的位都是零时,OSRdyGrp 的相应位才清零。为了找到那个进入
就绪态的优先级最高的任务,并不需要从 OSRdyTbl[0]开始扫描整个就绪任务表,只需要查
另外一张表,即优先级判定表 OSUnMapTbl([256])(见文件 OS_CORE.C)。OSRdyTbl[]中每个
字节的 8 位代表这一组的 8 个任务哪些进入就绪态了,低位的优先级高于高位。利用这个字
节为下标来查 OSUnMapTbl 这张表,返回的字节就是该组任务中就绪态任务中优先级最高的
那个任务所在的位置。这个返回值在 0 到 7 之间。确定进入就绪态的优先级最高的任务是用
以下代码完成的,如程序清单 L3.7 所示。
程序清单 L3.7 找出进入就绪态的优先级最高的任务
y = OSUnMapTbl[OSRdyGrp];
x = OSUnMapTbl[OSRdyTbl[y]];
3-10
prio = (y << 3) + x;
例如,如果 OSRdyGrp 的值为二进制 01101000,查 OSUnMapTbl[OSRdyGrp]得到的值是 3,
它相应于 OSRdyGrp 中的第 3位 bit3,这里假设最右边的一位是第 0 位 bit0。类似地,如果
OSRdyTbl[3]的值是二进制 11100100,则 OSUnMapTbl[OSRdyTbc[3]]的值是 2,即第 2 位。于
是任务的优先级 Prio 就等于 26(3*8+2)。利用这个优先级的值。查任务控制块优先级表
OSTCBPrioTbl[],得到指向相应任务的任务控制块 OS_TCB 的工作就完成了。
3.5 任务调度(Task Scheduling)
μC/OS-Ⅱ总是运行进入就绪态任务中优先级最高的那一个。确定哪个任务优先级最高,
下面该哪个任务运行了的工作是由调度器(Scheduler)完成的。任务级的调度是由函数
OSSched()完成的。中断级的调度是由另一个函数 OSIntExt()完成的,这个函数将在以后描
述。OSSched()的代码如程序清单 L3.8所示。
程序清单 L3.8 任务调度器(the Task Scheduler)
void OSSched (void)
{
INT8U y;
OS_ENTER_CRITICAL();
if ((OSLockNesting | OSIntNesting) == 0) { (1)
y = OSUnMapTbl[OSRdyGrp]; (2)
OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]); (2)
if (OSPrioHighRdy != OSPrioCur) { (3)
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; (4)
OSCtxSwCtr++; (5)
OS_TASK_SW(); (6)
}
}
OS_EXIT_CRITICAL();
}
μC/OS-Ⅱ任务调度所花的时间是常数,与应用程序中建立的任务数无关。如程序清单
中[L3.8(1)]条件语句的条件不满足,任务调度函数 OSSched()将退出,不做任务调度。这
个条件是:如果在中断服务子程序中调用 OSSched(),此时中断嵌套层数 OSIntNesting>0,
或者由于用户至少调用了一次给任务调度上锁函数 OSSchedLock(),使 OSLockNesting>0。
如果不是在中断服务子程序调用 OSSched(),并且任务调度是允许的,即没有上锁,则任务
调度函数将找出那个进入就绪态且优先级最高的任务[L3.8(2)],进入就绪态的任务在就绪
任务表中有相应的位置位。一旦找到那个优先级最高的任务,OSSched()检验这个优先级最
高的任务是不是当前正在运行的任务,以此来避免不必要的任务调度[L3.8(3)]。注意,在
μC/OS 中曾经是先得到 OSTCBHighRdy 然后和 OSTCBCur 做比较。因为这个比较是两个指针
型变量的比较,在 8 位和一些 16 位微处理器中这种比较相对较慢。而在μC/OS-Ⅱ中是两个
整数的比较。并且,除非用户实际需要做任务切换,在查任务控制块优先级表 OSTCBPrioTbl[]
3-11
时,不需要用指针变量来查 OSTCBHighRdy。综合这两项改进,即用整数比较代替指针的比
较和当需要任务切换时再查表,使得μC/OS-Ⅱ比μC/OS 在 8 位和一些 16 位微处理器上要
更快一些。
为实现任务切换,OSTCBHighRdy 必须指向优先级最高的那个任务控制块 OS_TCB,这是
通过将以OSPrioHighRdy为下标的OSTCBPrioTbl[]数组中的那个元素赋给OSTCBHighRdy来
实现的[L3.8(4)]。接着,统计计数器 OSCtxSwCtr 加 1,以跟踪任务切换次数[L3.8(5)]。
最后宏调用 OS_TASK_SW()来完成实际上的任务切换[L3.8(6)]。
任务切换很简单,由以下两步完成,将被挂起任务的微处理器寄存器推入堆栈,然后将
较高优先级的任务的寄存器值从栈中恢复到寄存器中。在μC/OS-Ⅱ中,就绪任务的栈结构
总是看起来跟刚刚发生过中断一样,所有微处理器的寄存器都保存在栈中。换句话说,μ
C/OS-Ⅱ运行就绪态的任务所要做的一切,只是恢复所有的 CPU 寄存器并运行中断返回指令。
为了做任务切换,运行 OS_TASK_SW(),人为模仿了一次中断。多数微处理器有软中断指令或
者陷阱指令 TRAP 来实现上述操作。中断服务子程序或陷阱处理(Trap hardler),也称作事
故处理(exception handler),必须提供中断向量给汇编语言函数 OSCtxSw()。OSCtxSw()
除了需要 OS_TCBHighRdy 指向即将被挂起的任务,还需要让当前任务控制块 OSTCBCur 指向
即将被挂起的任务,参见第 8章,移植μC/OS-Ⅱ,有关于 OSCtxSw()的更详尽的解释。
OSSched()的所有代码都属临界段代码。在寻找进入就绪态的优先级最高的任务过程中,
为防止中断服务子程序把一个或几个任务的就绪位置位,中断是被关掉的。为缩短切换时间,
OSSched()全部代码都可以用汇编语言写。为增加可读性,可移植性和将汇编语言代码最少
化,OSSched()是用 C写的。
3.6 给调度器上锁和开锁(Locking and UnLocking the Scheduler)
给调度器上锁函数 OSSchedlock()(程序清单 L3.9)用于禁止任务调度,直到任务完成
后调用给调度器开锁函数 OSSchedUnlock()为止,(程序清单 L3.10)。调用 OSSchedlock()
的任务保持对 CPU 的控制权,尽管有个优先级更高的任务进入了就绪态。然而,此时中断是
可以被识别的,中断服务也能得到(假设中断是开着的)。OSSchedlock()和 OSSchedUnlock()
必须成对使用。变量 OSLockNesting 跟踪 OSSchedLock()函数被调用的次数,以允许嵌套的
函数包含临界段代码,这段代码其它任务不得干预。μC/OS-Ⅱ允许嵌套深度达 255 层。当
OSLockNesting 等于零时,调度重新得到允许。函数 OSSchedLock()和 OSSchedUnlock()的
使用要非常谨慎,因为它们影响μC/OS-Ⅱ对任务的正常管理。
当 OSLockNesting 减到零的时候,OSSchedUnlock()调用 OSSched[L3.10(2)]。
OSSchedUnlock()是被某任务调用的,在调度器上锁的期间,可能有什么事件发生了并使一
个更高优先级的任务进入就绪态。
调用 OSSchedLock()以后,用户的应用程序不得使用任何能将现行任务挂起的系统调
用 。 也就是说 ,用户程序不得调用 OSMboxPend() 、 OSQPend() 、 OSSemPend() 、
OSTaskSuspend(OS_PR1O_SELF)、OSTimeDly()或 OSTimeDlyHMSM(),直到 OSLockNesting 回
零为止。因为调度器上了锁,用户就锁住了系统,任何其它任务都不能运行。
当低优先级的任务要发消息给多任务的邮箱、消息队列、信号量时(见第 6 章 任务间通
讯和同步),用户不希望高优先级的任务在邮箱、队列和信号量没有得到消息之前就取得了
CPU 的控制权,此时,用户可以使用禁止调度器函数。
程序清单 L3.9 给调度器上锁
void OSSchedLock (void)
3-12
{
if (OSRunning == TRUE) {
OS_ENTER_CRITICAL();
OSLockNesting++;
OS_EXIT_CRITICAL();
}
}
程序清单 L3.10 给调度器开锁.
void OSSchedUnlock (void)
{
if (OSRunning == TRUE) {
OS_ENTER_CRITICAL();
if (OSLockNesting > 0) {
OSLockNesting--;
if ((OSLockNesting | OSIntNesting) == 0) { (1)
OS_EXIT_CRITICAL();
OSSched(); (2)
} else {
OS_EXIT_CRITICAL();
}
} else {
OS_EXIT_CRITICAL();
}
}
}
3.7 空闲任务(Idle Task)
μC/OS-Ⅱ总是建立一个空闲任务,这个任务在没有其它任务进入就绪态时投入运行。
这个空闲任务[OSTaskIdle()]永远设为最低优先级,即 OS_LOWEST_PRI0。空闲任务
OSTaskIdle()什么也不做,只是在不停地给一个 32 位的名叫 OSIdleCtr 的计数器加 1,统
计任务(见 3.08节,统计任务)使用这个计数器以确定现行应用软件实际消耗的 CPU 时间。
程序清单 L3.11 是空闲任务的代码。在计数器加 1 前后,中断是先关掉再开启的,因为 8
位以及大多数 16 位微处理器的 32 位加 1 需要多条指令,要防止高优先级的任务或中断服务
子程序从中打入。空闲任务不可能被应用软件删除。
程序清单 L3.11 μC/OS-Ⅱ的空闲任务.
void OSTaskIdle (void *pdata)
{
pdata = pdata;
for (;;) {
OS_ENTER_CRITICAL();
3-13
OSIdleCtr++;
OS_EXIT_CRITICAL();
}
}
3.8 统计任务
μC/OS-Ⅱ有一个提供运行时间统计的任务。这个任务叫做 OSTaskStat(),如果用户将
系统定义常数 OS_TASK_STAT_EN(见文件 OS_CFG.H)设为 1,这个任务就会建立。一旦得到
了允许,OSTaskStat()每秒钟运行一次(见文件 OS_CORE.C),计算当前的 CPU 利用率。换
句话说,OSTaskStat()告诉用户应用程序使用了多少 CPU 时间,用百分比表示,这个值放在
一个有符号 8 位整数 OSCPUsage 中,精读度是 1个百分点。
如果用户应用程序打算使用统计任务,用户必须在初始化时建立一个唯一的任务,在这
个任务中调用OSStatInit()(见文件OS_CORE.C)。换句话说,在调用系统启动函数OSStart()
之前,用户初始代码必须先建立一个任务,在这个任务中调用系统统计初始化函数
OSStatInit(),然后再建立应用程序中的其它任务。程序清单 L3.12 是统计任务的示意性代
码。
程序清单 L3.12 初始化统计任务.
void main (void)
{
OSInit(); /* 初始化uC/OS-II (1)*/
/* 安装uC/OS-II的任务切换向量 */
/* 创建用户起始任务(为了方便讨论,这里以TaskStart()作为起始任务) (2)*/
OSStart(); /* 开始多任务调度 (3)*/
}
void TaskStart (void *pdata)
{
/* 安装并启动uC/OS-II的时钟节拍 (4)*/
OSStatInit(); /* 初始化统计任务 (5)*/
/* 创建用户应用程序任务 */
for (;;) {
/* 这里是TaskStart()的代码! */
}
}
因为用户的应用程序必须先建立一个起始任务[TaskStart()],当主程序 main()调用
系统启动函数 OSStcnt()的时候,μC/OS-Ⅱ只有 3 个要管理的任务:TaskStart()、
OSTaskIdle()和 OSTaskStat()。请注意,任务 TaskStart()的名称是无所谓的,叫什么名字
都可以。因为μC/OS-Ⅱ已经将空闲任务的优先级设为最低,即 OS_LOWEST_PR10,统计任务
3-14
的优先级设为次低,OS_LOWEST_PR10-1。启动任务 TaskStart()总是优先级最高的任务。
图 F3.4 解释初始化统计任务时的流程。用户必须首先调用的是μC/OS-Ⅱ中的系统初始
化函数 OSInit(),该函数初始化μC/OS-Ⅱ[图 F3.4(2)]。有的处理器(例如 Motorola 的
MC68HC11),不需要“设置”中断向量,中断向量已经在 ROM 中有了。用户必须调用
OSTaskCreat()或者 OSTaskCreatExt()以建立 TaskStart()[图 F3.4(3)]。进入多任务的条
件准备好了以后,调用系统启动函数 OSStart()。这个函数将使 TaskStart()开始执行,因
为 TaskStart()是优先级最高的任务[图 F3.4(4)]]。
图 F3.4 统计任务的初始化
TaskStart()负责初始化和启动时钟节拍[图 F3.4(5)]。在这里启动时钟节拍是必要的,因
为用户不会希望在多任务还没有开始时就接收到时钟节拍中断。接下去 TaskStart()调用统
计初始化函数 OSStatInit()[图 F3.4(6)]。统计初始化函数 OSStatInit()决定在没有其
它应用任务运行时,空闲计数器(OSIdleCtr)的计数有多快。奔腾 II 微处理器以 333MHz
运行时,加 1 操作可以使该计数器的值达到每秒 15,000,000 次。OSIdleCtr 的值离 32 位计
数器的溢出极限值 4,294,967,296 还差得远。微处理器越来越快,用户要注意这里可能会是
将来的一个潜在问
题
快递公司问题件快递公司问题件货款处理关于圆的周长面积重点题型关于解方程组的题及答案关于南海问题
。
系统统计初始化任务函数OSStatInit()调用延迟函数OSTimeDly()将自身延时2个时钟
节拍以停止自身的运行[图 F3.4(7)]。这是为了使 OSStatInit()与时钟节拍同步。μC/OS-
Ⅱ然后选下一个优先级最高的进入就绪态的任务运行,这恰好是统计任务 OSTaskStat()。
读者会在后面读到 OSTaskStat()的代码,但粗看一下,OSTaskStat()所要做的第一件事就
是查看统计任务就绪标志是否为“假”,如果是的话,也要延时两个时钟节拍[图 F3.4(8)]。
一定会是这样,因为标志 OSStatRdy 已被 OSInit()函数初始化为“假”,所以实际上
DSTaskStat 也将自己推入休眠态(Sleep)两个时钟节拍[图 F3.4(9)]。于是任务切换到空
3-15
闲任务,OSTaskIdle()开始运行,这是唯一一个就绪态任务了。CPU 处在空闲任务 OSTaskIdle
中,直到 TaskStart()的延迟两个时钟节拍完成[图 3.4(10)]。两个时钟节拍之后,
TaskStart()恢复运行[图 F3.4(11)]。 在执行 OSStartInit()时,空闲计数器 OSIdleCtr
被清零[图 F3.4(12)]。然后,OSStatInit()将自身延时整整一秒[图 F3.4(13)]。因为没有
其它进入就绪态的任务,OSTaskIdle()又获得了 CPU的控制权[图 F3.4(14)]。一秒钟以后,
TaskStart()继续运行,还是在 OSStatInit()中,空闲计数器将 1 秒钟内计数的值存入空闲
计数器最大值 OSIdleCtrMax 中[图 F3.4(15)]。
OSStarInit()将统计任务就绪标志 OSStatRdy 设为“真”[图 F3.4(16)],以此来允许两
个时钟节拍以后 OSTaskStat()开始计算 CPU 的利用率。
统计任务的初始化函数 OSStatInit()的代码如程序清单 L3.13