单片机与嵌入式
51单片机的寻址方式
51单片机的汇编指令系统
51汇编语言的伪指令
51汇编语言程序结构与程序示例
Keil C51程序设计
Keil C51的库函数
PIC单片机的基本结构
PIC单片机的汇编语言指令
PIC单片机的C语言编程
ATmega16单片机基本结构
ATmega16单片机的汇编语言
ATmega128单片机的结构
STM32单片机基础
使用STM32CubeMX开发STM32单片机
uC/OS-II嵌入式操作系统
uC/OS-II在STM32F10xx上的移植
FreeRTOS系统介绍
Linux系统介绍
Linux系统编程
嵌入式Linux编程
一、uC/OS操作系统概述:
uC/OS是Jean J. Labrosse于1992年推出的一种规模很小的微内核,后来在此基础上经过修改和扩充后在1999年又推出第二版,称为uC/OS-II。为了提高其可移植性,除了那些与硬件关系极为紧密的软件模块使用汇编语言编写外,绝大多数代码都是用C语言编写的。
1.体系结构:
uC/OS-II是一个微内核,它只对CPU和硬件时钟进行了抽象和封装,而没有提供其他硬件抽象层。在移植uC/OS-II时,主要就是根据具体硬件换一个或者添加一个硬件抽象层。
uC/OS-II目前除了内核外还有商业化文件系统uC/FS、图形系统uC/GUI以及任务调试工具uC KA和uC View。
2. 可剥夺性实时内核:
uC/OS-II是基于优先级的可剥夺型内核,系统中的所有任务都有一个唯一的优先级别,适合应用在实时性要求较强的场合。
如果当前进程并未执行完毕,又不是自愿放弃处理器使用权,而被强制中止执行被迫将处理器使用权转移给其他进程,那么这种内核就是可剥夺型的。其实,真正实施处理器使用权转移工作的是调度器,调度器能以强硬的方式获得处理器使用权并分配给另外的进程。作为实时系统中的实时进程,各自所有对应的实时任务的紧急程度总是有区别的,为了确保紧急进程能比较快地被执行,提高系统的实时性,所以希望操作系统的调度器一旦发现有紧急进程在等待处理器,就能强硬地从当前进程中把处理器抢夺过来,并分配给正在等待的这个紧急进程。因此,实时操作系统内核应是可剥夺型的。可剥夺型内核的重要特点是,系统中的每个进程都有一个表示其紧急程度的优先级,调度器可根据等待进程的优先级来决定是否要剥夺当前进程的处理器使用权。
3.不区分用户空间和系统空间:
uC/OS-II不区分用户空间和系统空间,很适合应用在比较简单的处理器上。当然,系统和用户共用一个空间,会由于用户应用程序与系统服务程序模块之间联系过于紧密而使系统的安全性变差。但是,嵌入式应用的封闭性,使其不会成为一个很严重的问题,而且有时候还会给用户带来某种方便。
二、uC/OS-II文件结构:
1.与CPU硬件相关的文件:
1)OS_CPU.H文件:
OS_CPU.H是一个与CPU硬件相关的文件,其中有些内容在系统移植时要根据工程项目实际使用的CPU进行修改,有些则无需修改。
无需修改的部分为数据类型的定义部分,内容如下:
typedef unsigned char BOOLEAN;
typedef unsigned char INT8U;
typedef signed char INT8S;
typedef unsigned int INT16U;
typedef signed int INT16S;
typedef unsigned long INT32U;
typedef signed long INT32S;
typedef float FP32;
typedef double FP64;
typedef unsigned int OS_STK;
typedef unsigned short OS_CPU_SR;
#define BYTE INT8S
#define UBYTE INT8U
#define WORD INT16S
#define UBYTE INT16U
#define LONG INT32S
#define ULONG INT32U
为了系统移植方便,uC/OS-II对一些已有的数据类型做了重新定义。此外,在这个文件中,还定义了一些与CPU相关的常数和宏。在应用于x86处理器上时的部分定义为:
#define OS_CRITICAL_METHOD 2
#if OS_CRITICAL_METHOD= =1
#define OS_ENTER_CRITICAL() asm CLI
#define OS_EXIT_CRITICAL() asm STI
#endif
#if OS_CRITICAL_METHOD= =2
#define OS_ENTER_CRITICAL() asm {PUSHF; CLI}
#define OS_EXIT_CRITICAL() asm POPF
#endif
#if OS_CRITICAL_METHOD= =3
#define OS_ENTER_CRITICAL() asm (cpu_sr=OSCPUSavesr())
#define OS_EXIT_CRITICAL() asm (OSCPURestoreSR(cpu_sr))
#endif
2)OS_CPU_A.ASM文件:
在OS_CPU_A.ASM文件中,集中了所有与处理器相关的汇编语言代码模块,是项目开发者要根据实际使用的CPU必须进行移植的文件。
3)OS_CPU_C.C文件:
在OS_CPU_C.C文件中,集中了所有与CPU相关的C语言代码模块,也是项目开发者要根据实际使用的CPU必须进行移植的文件。
2.与应用程序相关的文件:
1)INCLUDES.H文件:
INCLUDES.H文件是uC/OS-II的主头文件,在每个C文件中都要包含这个文件,即在C文件的头部都应有语句:
#include “includes.h”。
INCLUDES.H文件的内容为:
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <conio.h>
#include <dos.h>
#include <setjmp.h>
#include “\software\ucos-ii\ix86\bc31\os_cpu.h”
#include “os_cfg.h”
#include “\software\ucos-ii\source\ucos_ii.h”
#include “\software\blocks\pc\bc31\pc.h”
可以看到,这个文件把工程项目中应包含的头文件都集中到了一起,使得项目开发者无需再去考虑项目中的每个文件究竟应该需要或者不需要哪些头文件了。
2)OS_CFG.H文件:
OS_CFG.H文件是配置文件。uC/OS-II是依靠编译时的条件编译来实现软件系统的裁剪性的,即把用户可裁剪的代码段写在#if和#endif预编译指令之间,在编译时根据预编译指令后面的常数值来确定是否该代码段进行编译。例如:
#if OS_TASK_CREATE_EXT_EN
...... //可裁剪的代码段
#endif
如果工程需要这个代码段,则需要在配置文件中把常数OS_TASK_CREATE_EXT_EN的值置为1,否则置为0。
此外,配置文件OS_CFG.H还包括与项目有关的其他一些常数的设置。在这个文件中,对所有的配置常数事先都预置了一些默认值,用户可根据需要对这些配置值进行修改。
配置文件OS_CFG.H的部分内容如下:
#define OS_MAX_EVENTS 2 //系统中事件的总数
#define OS_MAX_FLAGS 2 //系统中信号量集的总数
#define OS_MAX_MEM_PART 2 //系统中内存块的总数
#define OS_MAX_QS 2 //系统中消息队列的总数
#define OS_MAX_TASKS 2 //系统中任务的总数
#define OS_LOWEST_PRIO 2 //系统中用户任务的总数
#define OS_TASK_IDLE_STK_SIZE 2 //空闲任务的堆栈长度
......
#define OS_TASK_CHANGE_PRIO_EN 2 //是否使用修改优先级功能
#define OS_TASK_CREATE_EN 2 //是否使用任务创建功能
......
3.系统内核的各种服务文件:
uC/OS-II内核是以C函数的形式提供各种服务,这些功能模块都是与处理器硬件无关的,即在不同的处理器之间移植时是无须修改的。这些文件的说明如下:
OS_CORE.C //核心服务模块文件
OS_FLAG.C //信号量服务模块文件
OS_MBOX.C //消息邮箱服务模块文件
OS_MEM.C //存储管理服务模块文件
OS_MUTEX.C //互斥型信号量服务模块文件
OS_Q.C //消息队列服务模块文件
OS_SEM.C //信号量服务模块文件
OS_TASK.C //任务管理服务模块文件
OS_TIME.C //时间管理服务模块文件
uCOS_II.C //
uCOS_II.H //与应用相关的配置文件
4.可剪裁性的实现:
uC/OS-II的可剪裁性是通过条件编译来实现的,配置文件OS_CFG.H中提供了一些用户可定义的配置常数,用户可通过为这些常数赋值的方法来选用或删除uC/OS-II的一些程序段。
系统设计时,应用程序设计人员应仔细对配置文件中的各配置常数进行设置,才能保证系统的精简及适用。
三、uC/OS-II的任务概述:
通常认为,嵌入式系统中只有一个进程,而把这个进程进行分解之后的那些小程序模块,由于没有自己的内存空间,实质上就是线程。uC/OS-II中,习惯上把这样的线程叫做任务,或者实时任务。
1.实时任务的代码形式:
实时系统中,任务的执行大多数是由外部事件触发的,实时操作系统的主要工作就是响应并处理各种外部事件。
外部事件的形式,有异步事件、周期事件,或二者的组合。作为实时系统,对于每个事件对应有一个处理这个事件的程序模块,这些程序模块就是任务。uC/OS-II中,任务就应是如下形式的一个C函数:
void uCOSTask(void *pdata)
{
for(;;)
{任务代码;}
}
由于uC/OS-II是一种可剥夺型内核,所以只要没有更高优先级的任务来抢夺当前任务的处理器使用权,那么当前任务就会一直占用处理器。为此,任务的代码应是一个由for(;;)语句实现的无限循环。正在占用处理器的任务,也可以在适当的时机主动放弃处理器的使用权。
2.任务的存储结构:
从存储结构来看,任务由任务程序代码、任务堆栈和任务控制块组成。其中,任务控制块用来保存任务属性,任务堆栈用来保存任务工作环境,任务程序代码是任务的执行部分。
任务控制块存放了指向任务执行代码的指针和存放任务私有数据的存储区--任务堆栈的指针,所以是系统管理任务的依据。为了便于管理,任务控制块中还有前一个任务控制块指针和后一个任务控制块指针,当系统中存在多个任务时,系统通过这两个指针把任务控制块链接成一个链表。
3.任务的状态:
通常情况下,嵌入式系统中只有一个处理器,所以在一个具体时刻只能允许一个任务占用处理器。任务在uC/OS-II中具有5种状态:
1)睡眠状态:任务只是一代码的形式驻留在程序空间(ROM或RAM),还未交给操作系统管理时的情况。或者说,任务在未被配备任务控制块或被剥夺了任务控制块时的状态,叫做任务的睡眠状态。
2)就绪状态:如果系统为任务配备了任务控制块,且在任务就绪表中进行了就绪登记,则任务就具备了运行的充分条件,这时任务的状态叫做就绪状态。
3)运行状态:处于就绪状态的任务,如果经调度器判断获得了处理器的使用权,则任务就进入运行状态。任何时刻,只能有一个任务处于运行状态,就绪的任务只有当所有优先级高于本任务的任务都转入等待状态时,才能进入运行状态。
4)等待状态:正在运行的任务,需要等待一段时间或等待一个事件发生再运行,该任务就会把处理器的使用权让给其他任务而使任务进入等待状态。
5)中断服务状态:一个正在运行的任务,一旦响应中断申请就会中止运行而去执行中断服务程序,这时任务的状态叫做中断服务状态。
任务在系统和应用程序的控制下,会根据具体情况改变自己的状态,这个改变叫做任务状态的转换。
系统为了能有效地对系统中的任务进行协调管理,必须随时了解任务的当前状态。所以,在所有任务的任务控制块中都有一个专门用来存储任务当前状态的状态信息字段,以供系统和用户应用程序来查询。
4.任务的优先级别:
uC/OS-II中的每一个任务都必须具有一个唯一的优先级,用来表示该任务在抢夺处理器时所具有的权力。uC/OS-II把任务的优先权分为64个优先级,每一个级别都用一个数字来表示。数字0表示任务的优先级最高,数字越大则表示任务的优先级越低。
uC/OS-II规定,一个应用程序的任务数最多可以有64个。实际应用中,为了节省内存,用户可根据应用程序的需要,在配置文件OS_CFG.H中通过对常数OS_LOWEST_PRIO赋值的方法来说明应用程序中任务优先级的数目。该常数一旦被定义,就意味着系统中可供使用的优先级一共有OS_LOWEST_PRIO+1个,也意味着系统中任务的总数不能超过此值。
其中,系统总是把最低优先级OS_LOWEST_PRIO自动赋给由系统创建的一个任务--空闲任务。如果应用程序中还使用了统计任务,则系统会把优先级OS_LOWEST_PRIO-1自动赋给统计任务,因此用户任务可使用的优先级共OS_LOWEST_PRIO-1个。
由于每个任务都具有唯一的优先级,因此uC/OS-II通常也用任务的优先级来作为这个任务的标识。
5.任务控制块:
uC/OS-II用来记录任务的堆栈指针、任务的当前状态、任务的优先级等一些与任务管理有关的属性表就叫做任务控制块。任务控制块相当于一个任务在系统中的档案。
在uC/OS-II中,当用户应用程序调用系统函数OSTaskCreate()创建一个用户任务时,这个函数就会把该任务的所有相关数据赋予任务控制块中的对应成员,并驻留在RAM中。任务控制块结构的定义如下:
typedef struct os_tcb{
OS_STK *OSTCBStkPtr; //指向任务堆栈顶的指针
#if OS_TASK_CREAT_EXT_EN
void *OSTCBExtPtr; //指向任务控制块扩展的指针
void *OSTCBStkBottom; //指向任务堆栈栈底的指针
void OSTCBStkSize; //任务堆栈的长度
void OSTCBOpt; //创建任务时的选择项
void 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;
其中成员OSTCBStat用来存放任务的当前状态,该成员变量可能的值见表:
OS_STAT_RDY 就绪状态 OS_STAT_SEM 等待信号量状态
OS_STAT_MBOX 等待消息邮箱状态 OS_STAT_Q 等待消息队列状态
OS_STAT_SUSPEND 被挂起状态 OS_STAT_SEM 等待互斥型信号量状态
6.任务控制块链表:
uC/OS-II用两条链表来管理任务控制块:一条是空任务块链表(其中所有任务控制块还没有分配给任务);另一条是任务块链表(其中所有任务控制块已经分配给任务了)。空任务块链表是在应用程序调用函数OSInit()对uC/OS-II系统进行初始化时建立并初始化,而任务控制块则是在系统调用系统函数OSTackCreate()创建任务时建立的。
函数OSInit()创建空任务控制块链表的步骤为:先在RAM中建立一个OS_TCB结构类型的数组OSTCBTbl[],使数组的每个元素都是一个任务控制块,然后利用OS_TCB结构中的两个指针OSTCBNext和OSTCBPrev把这些控制块链接成一个链表。由于链表中的这些控制块还没有与具体任务相关联,或者说还未把它们分配给任务,因此这个链表就叫做空任务块链表。
uC/OS-II初始化时建立的空任务链表一共有OS_MAX_TASKS+OS_N_SYS_TASKS个元素。其中,OS_N_SYS_TASKS是配置文件OS_CFG.H中用来指明系统任务总数的常数。这两个常数都需要在进行系统配置时由用户根据需要来指定。
每当应用程序创建一个任务,即在调用系统函数OSTackCreate()或OSTackCreateExt()时,系统就会从链表的头部(指针OSTCBFreeList指向的任务控制块)摘取一个空任务控制块分配给该任务,并把它加入到任务控制块链表中。当然,在加入之前要给任务控制块中的各成员赋值。
在这种链表中查找一个元素有时是费时的,而且查找时间还与目标元素在链表中的位置有关,有不确定性。为了加快对任务控制块的访问速度,uC/OS-II在uCOS_II.H文件中还定义了一个数据类型为OS_TCB*的数组OSTCBTbl[],专门用来存放指向各任务控制块的指针,并按任务的优先级把这些指针存放数组的各个元素里。这样,在访问某个任务的任务控制块时,就可以按照任务的优先级直接从数组OSTCBTbl[]的对应元素中获得该任务控制块的指针,并通过它直接找到该任务控制块了。
在所有任务控制块中,系统访问最频繁的一定是当前正在运行任务的控制块。为了能更快地访问正在运行任务的任务控制块,uC/OS-II还定义了一个OS_TCB*类型的全局变量OSTCBCur,专门存放当前正在运行的任务的任务控制块指针。
所以,当系统一旦选定一个要运行的任务时,就先按任务的优先级把数组OSTCBTbl的对应元素中的值传入OSTCBCur,于是系统再对这个任务的任务控制块进行访问时就快捷多了。
uC/OS-II允许用函数OSTaskDel()删除一个任务。删除一个任务,实质上就是把该任务的任务控制块从任务控制块链表中删除,并把它归还给空任务控制块链表。因为这个任务对应的任务控制块已经被“吊销”,系统就不再理会,当作不存在了,其实任务的代码还仍然在内存中并没有删除。如果需要,该任务还可以重新被创建。
7.任务堆栈:
堆栈,就是在存储器中按数据“后进先出”原则组织的连续存储空间。为了满足任务切换和响应中断时保存处理器寄存器中的内容及存储任务私有数据的需要,每个任务都应配有自己的堆栈,这个堆栈就叫做任务堆栈,它是任务的重要组成部分。
为方便定义任务堆栈,在文件OS_CPU.H中专门定义了一个数据类型OS_STK:
typedef unsigned int OS_STK; //该类型长度为16位
这样,在内存中划分一个任务堆栈的栈区就非常简单--在应用程序中定义一个OS_STK类型的一个数组即可。例如:
#define TASK_STK_SIZE 512 //定义堆栈的长度(1024字节)
OS_STK TaskStk[TASK_STK_SIZE]; //定义一个数组作为任务堆栈
当调用任务创建函数OSTaskCreate()创建一个任务时,把数组的指针传递给函数OSTask Create()中的堆栈栈顶参数ptos,在函数内部会把这个参数传入任务控制块的指针变量OSTCBStkPtr,这样就可以把该数组与任务关联起来成为该任务所占用的数据区。当这个任务获得处理器而运行前,系统会从任务控制块中取出这个指针并把它赋给处理器的堆栈指针寄存器,于是这个数据区就成为这个任务的任务堆栈。
创建任务函数OSTaskCreate()的原型如下:
INT8U OSTaskCreate (
void (*task)(void *pd), //指向任务的指针
void *pdata, //传递给任务的参数
OS_STK *ptos, //任务堆栈栈顶的指针
INT8U prio //指定任务优先级的参数
);
创建一个任务,堆栈长度为128字节,优先级为20,任务参数pdata的实参为MyTaskAgu,需要的代码如下:
#define MyTaskStkN 64 //定义堆栈的长度(1024字节)
OS_STK MyTaskStk[MyTaskStkN ]; //定义一个数组作为任务堆栈
void main(void)
{
......
OSTaskCreate (MyTask,&MyTaskAgu,&MyTaskStk[MyTaskStkN-1],20);
......
}
需要注意的是,堆栈的增长方向是随系统使用的处理器的不同而不同,有些处理器要求堆栈的增长方向向上,而另一些处理器要求堆栈的增长方向向下。因此,在使用函数OSTask Create()创建任务时,一定要注意使用的处理器堆栈增长方向的支持是向上还是向下的。
上面的示例是在假设使用了支持堆栈向下增长方式的处理器的条件下设置的函数参数ptos。如果使用的处理器支持堆栈的增长方向是向上的,则应写成:
OSTaskCreate (MyTask,&MyTaskAgu,&MyTaskStk[0],20);
为了提高应用系统的可移植性,在编写程序时也可把两种代码都编写出来,利用OS_CFG.H文件中的常数OS_STK_GROWTH作为选择开关,使用户可通过定义该常数的值来选择相应的代码段,以适应不同的堆栈增长方式的需要。示例:
#define MyTaskStkN 64 //定义堆栈的长度(1024字节)
OS_STK MyTaskStk[MyTaskStkN ]; //定义一个数组作为任务堆栈
void main(void)
{
......
#if OS_STK_GROWTH==1
OSTaskCreate (MyTask,&MyTaskAgu,&MyTaskStk[MyTaskStkN-1],20);
#else
OSTaskCreate (MyTask,&MyTaskAgu,&MyTaskStk[0],20);
#endif
......
}
当处理器启动运行一个任务时,处理器的各寄存器总是需要预置一些与待运行任务相关的初始数据,例如指向任务的指针、程序状态字PSW等。系统启动任务时,最方便的方法就是让处理器从这个任务的任务堆栈里获得这些数据。为此,应用程序在创建一个新任务时,就必须把在系统启动这个任务时处理器各寄存器所需的初始数据(任务指针、任务堆栈指针、程序状态字等)事先存放在这个任务的堆栈中。这样,当任务获得处理器使用权时,就能把堆栈中的初始数据复制到处理器的各寄存器里,从而可使任务顺利地启动并运行。
由于任务堆栈中的内容与任务相关,因此这个工作是在创建任务函数OSTaskCreate() 中通过调用任务堆栈初始化函数OSTaskStkInit()来完成的。函数OSTaskStkInit()的原型如下:
OS_STK *OSTaskStkInit (
void (*task)(void *pd), //指向任务的指针
void *pdata, //传递给任务的参数
OS_STK *ptos, //任务堆栈栈顶的指针
INT16U opt //
);
目前,由于各种处理器的寄存器及对堆栈的操作方式不尽相同,因此该函数需要用户在进行uC/OS-II的移植时按所使用的处理器由用户来编写。
8.系统任务:
uC/OS-II预定义了两个为应用程序服务的系统任务:空闲任务和统计任务。其中,空闲任务是每个应用程序必须使用的,而统计任务则是应用程序可根据实际需要来选择使用的。
1)空闲任务:
在多任务系统运行时,系统经常会在某个时间内无用户任务可运行而处于所谓空闲状态,为了使处理器在没有用户任务可执行时有事可做,uC/OS-II提供了一个OSTaskIdle()的系统任务,叫做空闲任务。空闲任务的代码如下:
void OSTaskIdle(void *pdata)
{
#if OS_CRITICAL_METHOD= =3
OS_CPU_SR cpu_sr;
#endif
pdata=pdata; //防止某些编译器报错
for(;;)
{
OS_ENTER_CRITICAL(); //关闭中断
OSdleCtr++; //计数
OS_EXIT_CRITICAL(); //开放中断
}
}
这里有一行代码“pdata=pdata;”,因为在这个任务中没有用到参数pdata,对某些C编译器在对代码进行编译时会对这种情况报错,有了这行代码就不会报错了。
从代码可以看到,这个空闲任务只是对系统定义的一个空闲任务运行计数器OSdleCtr进行加1操作。这个空闲任务是系统中所有任务中优先级最低的一个,这样就保证处理器一旦出现其他任务就绪时,即可马上中止空闲任务而去运行那个就绪的任务。
uC/OS-II规定,一个用户应用程序必须使用这个空闲任务,而且这个任务是不能用软件来删除的。
2)统计任务:
uC/OS-II提供的另一个系统任务就是统计任务OSTaskStat()。这个统计任务每秒计算一次处理器在单位时间内被使用的时间,并把计算结果以百分比的形式存放在变量OS处理器sage中,以便应用程序通过访问它来了解处理器的利用率,所以被叫做统计任务。
用户应用程序是否使用统计任务,可根据应用程序的实际需要来进行选择。如果用户应用程序要使用这个统计任务,则必须把定义在系统头文件OS_CFG.H中的系统配置常数OS_TASK_STAT_EN的值设置为1,并且必须在创建统计任务之前调用函数OSStatInit()对统计任务进行初始化。这个统计任务的级别仅比空闲任务高一级。
9.临界区:
uC/OS-II中的临界区,指的是一个特殊代码段。如果在程序中有一段代码在执行期间不允许中断,那么它就是具有原子性的代码段,这种代码段在uC/OS-II中叫做临界区。为了防止临界段代码在执行期间被中断,在它之前要关闭中断,而在它之后要打开中断。
在uC/OS-II中,用宏OS_ENTER_CRITICAL()关闭中断,而用OS_EXIT_CRITICAL()打开中断。因此,从形式上看,一个临界区的代码形式如下:
OS_ENTER_CRITICAL(); //关闭中断
...... ; //临界区
OS_EXIT_CRITICAL(); //开放中断
四、uC/OS-II的初始化和启动:
为了完成自身的工作,uC/OS-II定义了大量的全局数据结构,在启动时必须为这些全局数据结构进行初始化。初始化之后,uC/OS-II还必须启动内核以进入正常工作状态。uC/OS-II的这两项工作都是由用户在main()函数中分别调用系统函数OSInit()和OSStart()来完成的。
1.uC/OS-II需要初始化的数据结构及全局变量:
在使用uC/OS-II的所有服务之前,必须调用初始化函数OSInit()对uC/OS-II自身的运行环境进行初始化。函数OSInit()主要负责建立任务控制块链表、就绪任务表等一些数据结构,并对系统所使用的全局变量进行初始化。
uC/OS-II定义并需要进行初始化的全局变量如下:
变量 |
类型 |
说明 |
初始化值 |
OSPrioCur |
INT8U |
正在运行任务的优先级 |
0 |
OSPrioHighRdy |
INT8U |
具有最高优先级的就绪任务的优先级 |
0 |
OSTCBCur |
OS_TCB* |
指向正在运行任务控制块的指针 |
NULL |
OSTCBHighRdy |
OS_TCB* |
指向最高优先级就绪任务控制块的指针 |
NULL |
OSTime |
INT32U |
系统当前时间(节拍数) |
0L |
OSIntNesting |
INT8U |
存放中断嵌套的层数(0~255) |
0 |
OSLockNesting |
INT8U |
调用了OSSchededLock的嵌套层数 |
0 |
OSCtxSwCtr |
INT32U |
上下文切换的次数 |
0 |
OSTaskCtr |
INT8U |
已经建立了的任务数 |
2 |
OSRunning |
BOOLEAN |
uCOS-II核是否正在运行的标志 |
FALSE |
OSCPUUsage |
INT8S |
存放CPU的利用率(%)的变量 |
0 |
OSIdleCtrMax |
INT32U |
表示每秒空闲任务计数的最大值 |
0L |
OSIdleCtrRun |
INT32U |
表示空闲任务计数器每秒的计数值 |
0L |
OSIdleCtr |
INT32U |
空闲任务的计数器 |
0L |
OSStatRdy |
BOOLEAN |
统计任务是否就绪的标志 |
FALSE |
OSIntExity |
INT8U |
用于函数OSInitExt() |
0 |
2.uC/OS-II的初始化:
调用系统函数OSInit()可对uC/OS-II进行初始化,其代码如下:
void OSInit(void)
{
#if OS_VERSION> =204
OSInitHookBegin();
#endif
OS_InitMisc(); //初始化OSIntNesting等全局变量
OS_InitRdyList(); //初始化就绪任务表及其相关变量
OS_InitTCBList(); //初始化空任务控制块链表
OS_InitEventList(); //初始化事件控制块链表
#if (OS_VERSION> =251)&&(OS_FLAG_EN>0)&&(OS_MAX_FLAGS>0)
OS_FlagInit(); //初始化信号量集
#endif
#if (OS_MEM_EN>0)&&(OS_MAX_MEM_PART>0)
OS_MemInit(); //存储管理初始化
#endif
#if (OS_Q_EN>0)&&(OS_MAX_Q>0)
OS_QInit(); //初始化
#endif
OS_InitTaskIdle(); //空闲任务初始化
#if OS_TASK_STAT_EN>0
OS_InitTaskStat(); //统计任务初始化
#endif
#if OS_VERSION> =204
OSInitHookEnd();
#endif
}
函数OSInit()将对uC/OS-II的所有全局变量和数据结构进行初始化,同时创建空闲任务OSTaskIdle,并赋之以最低的优先级和永远的就绪状态。如果用户应用程序还要使用统计任务(OS_TASK_STAT_EN=1),则OSInit()还要以优先级为OS_LOWEST_PRIO-1来创建统计任务。
初始化函数OSInit()对数据结构进行初始化时,主要创建包括空任务控制块链表在内的5个空数据缓冲区。同时,为了可以快速地查询任务控制块链表中的各个元素,初始化函数OSInit()还要创建一个数组OSTCBPrioTbl[OS_LOWEST_PRIO+1],在这个数组中,按任务的优先级顺序把任务控制块的指针存放在对应的元素中。
初始化之后各全局变量的情况如上表。
3.uC/OS-II的启动:
main()函数中,在对系统初始化并创建了至少一个用户任务之后,调用启动函数OSStart(),则uC/OS-II会立即开始对任务的管理工作。
函数OSStart()的源代码如下:
void OSStart(void)
{
INT8U y;
INT8U x;
If(OSRunning= =FALSE)
{
y=OSUnMapTbl[OSRdyGrp];
x=OSUnMapTbl[OSRdyTbl[y]];
OSPrioHighRdy=(INT8U)((y<<3)+x);
OSPrioCur=OSPrioHighRdy;
OSTCBHighRdy=OSTCBPrioTbl[OSPrioHighRdy];
OSTCBCur=OSTCBHighRdy;
OSStartHighRdy(); //运行最高优先级的就绪任务
}
}
在函数OSStart()中,用到了表示内核是否处于运行状态的变量OSRunning。若该变量值为FALSE,则意味着内核处于未运行状态;若该变量值为TRUE,则意味着内核处于运行状态。由于在调用函数OSInit()进行初始化之后,已经把该值初始化为FALSE,所以在启动函数OSStart()中首先对OSRunning进行判断。如果该值为FALSE,则查找最高优先级的就绪任务开始运行,并在函数OSStartHighRdy()中把OSRunning置为TRUE;否则,意味着内核已经处在运行状态,函数OSInit()什么工作也不做地返回。
假如有代码如下:
#include “includes.h”
#define MY_TASK_STK_SIZE 512 //定义堆栈容量
void main(void)
{
OSInit();
......
OSTaskCreate(MyTask,(void *)0,&MyTaskStk[MY_TASK_STK_SIZE-1],6);
OSStart();
}
那么,主函数main()调用了函数OSStrat()后,uC/OS-II立即进入了多任务管理阶段,一些变量也会产生变化。
OSStartHighRdy()在多任务系统启动函数OSStart()中调用,完成的功能是:设置系统运行标志位OSRunning=TRUE,将就绪表中最高优先级任务的栈指针装载到处理器的SP中,并强制中断返回。这样,就绪的最高优先级任务就如同从中断里返回到运行态一样,使得整个系统得以运转。
五、uC/OS-II任务的创建:
uC/OS-II是通过任务控制块来管理任务的,因此创建任务的工作实质上是给任务的代码分配一个任务控制块,并通过任务控制块把任务代码和任务堆栈关联起来形成一个完整的任务。当然,还要使刚创建的任务进入就绪状态,并借着引发一次任务调度。
uC/OS-II有两个用来创建任务的函数:OSTaskCreate()和OSTaskCreateExt()。其中,OSTaskCreateExt()是函数OSTaskCreate()的扩展,并提供了一些附加功能。
1.用函数OSTaskCreate()创建任务:
应用程序通过调用函数OSTaskCreate()来创建任务。函数OSTaskCreate()的源代码如下:
INT8U OSTaskCreate (
void (*task)(void *pd), //指向任务的指针
void *pdata, //传递给任务的参数
OS_STK *ptos, //指向任务堆栈栈顶的指针
INT8U prio //任务的优先级
);
{
#if OS_CRITICAL_METHOD= =3
OS_CPU_SR cpu_sr;
#endif
void *psp;
INT8U err;
if (prio>OS_LOWEST_PRIO) //检测任务的优先级是否合法
{
return (OS_PRIO_INVALID);
}
OS_ENTER_CRITICAL();
if (OSTCBPrioTbl[prio]= =(OS_TCB*)0) //确认优先级未被使用
{
OSTCBPrioTbl[prio]=(OS_TCB*)1; //确保优先级
}
OS_ENTER_CRITICAL();
OS_EXIT_CRITICAL();
psp=(void *)OSTaskStkInit(task,pdata,ptos,0); //初始化任务堆栈
err=OSTCBInit(prio,psp,(void *)0,0,0,(void *)0,0); //获得并初始化任务控制块
if (err= =OS_NO_ERR)
{
OS_ENTER_CRITICAL();
OSTaskCtr++; //任务计数器加1
OS_EXIT_CRITICAL();
if (OSRunning)
{
OSSched(); //任务调度
}
}
else
{
OS_ENTER_CRITICAL();
OSTCBPrioTbl[prio]=(OS_TCB)0; //放弃任务
OS_EXIT_CRITICAL();
}
return(err);
}
else
{
OS_EXIT_CRITACAL();
return(OS_PRIO_EXIST);
}
}
从函数OSTaskCreate()的源代码可以看到,该函数主要完成三项任务:任务堆栈的初始化、任务控制块的初始化和任务调度。
函数OSTaskCreate()首先对由参数prio传递进来的优先级进行一系列判断,确认该优先级合法且未被使用之后,随即调用函数OSTaskStkInit()和OSTCBInit()对任务堆栈和任务控制块进行初始化。
在对任务堆栈进行初始化时,要把传递进来的任务代码指针task及任务的其他环境数据存入任务堆栈,并返回新的栈顶指针psp。而在任务控制块初始化时,首先自空任务控制块链表摘取一个控制块分配给任务,并把这个任务控制块加入系统的任务控制块链表;然后就把栈顶指针psp传入任务控制块的指针OSTCBStkPtr,同时还要把任务的优先级prio传入任务控制块的成员变量OSTCBPrio;最后再在就绪任务表中给本任务进行登记。至此,任务创建工作结束,且任务已处在就绪状态。
其后,除了把任务计数器加1之外,还要进一步判断uC/OS-II是否处在运行状态(即全局变量OSRunning的值是否为1)。如果是,则因有新的就绪任务加入而需要调用调度器OSSched()进行一次任务调度。
如果这时这个新就绪任务的优先级高于当前任务,那么调度器就会根据这个新就绪任务的任务控制块中的堆栈指针传送到处理器的SP寄存器,然后从任务堆栈中存放的任务环境的各参数弹入处理器的各寄存器,其中就把任务堆栈中的任务代码指针弹入处理器的程序指针PC中,进而处理器就会开始运行这个新任务了。当然,如果这个新建任务的优先级低于当前运行的任务,那么这个新建任务只好等待。
在创建任务函数的代码中看到,任务代码的指针并不是存放在任务控制块中的,而是存放在任务堆栈里面。
函数OSTaskCreate()调用成功后,将返回OS_NO_ERR;否则,将根据具体情况返回OS_PRIO_INVALID、OS+PRIO_EXIST及在函数内调用任务控制块初始化函数失败时返回的消息。
2.用函数OSTaskCreateExt()创建任务:
在任务及应用程序中也可通过调用函数OSTaskCreateExt()来创建一个任务。用OSTaskCreateExt()来创建任务将更加灵活,但也会增加一些额外的开销。OSTaskCreateExt()函数的原型如下:
INT8U OSTaskCreateExt (
void (*task)(void *pd), //指向任务的指针
void *pdata, //传递给任务的参数
OS_STK *ptos, //指向任务堆栈栈顶的指针
INT8U prio, //任务的优先级
INT16U id, //任务的标识
OS_STK *pbos, //任务堆栈栈底的指针
INT32U stk_size, //任务堆栈的容量
void *pext, //指向附加数据域的指针
INT16U opt //用于设定操作选项
);
3.创建任务的一般方法:
一般来说,任务可在调用函数OSStart()启动任务调度之前来创建,也可在任务中来创建。但是uC/OS-II有一个规定:在调用启动任务函数OSStart()之前,必须已经创建了至少一个任务。因此,习惯上在调用函数OSStart()之前先创建一个任务,并赋予它最高的优先级,从而使它成为起始任务;然后在这个起始任务中,再创建其他各任务。
如果要使用系统提供的统计任务,则统计任务的初始化函数也必须在这个起始任务中来调用。
下面是创建任务的示意性代码:
#include “includes.h”
......
/*主函数*/
void main(void)
{
......
OSInit(); //对uC/OS-II进行初始化
......
OSTaskCreate(TaskStart,......); //创建起始任务TaskStart
OSStart(); //开始多任务调度
}
/*起始任务*/
void TaskStart(void *pdata)
{
...... //在这个位置安装并启动uC/OS-II的时钟
OSStatInit(); //初始化统计任务
...... //在这个位置创建其他任务
for(;;)
{
...... //起始任务TaskStart的代码
}
}
需要注意的是,uC/OS-II不允许在中断服务程序中创建任务。
4.示例:只有一个任务的应用程序,每秒钟在显示器上显示一个字符M。
#include “includes.h”
#define TASK_STK_SIZE 512 //任务堆栈长度
OS_STK MyTaskStk[TASK_STK_SIZE]; //定义任务堆栈区
INT16S key; //用于退出uC/OS-II的键
INT8U x=0,y=0; //字符显示位置
void MyTask(void *data); //声明一个任务
/*主函数*/
void main(void)
{
char * s_M=”M”; //定义要显示的字符
OSInit(); //初始化uC/OS-II环境
PC_DOSSaveReturn(); //保存DOS环境
PC_VectSet(uCOS,OSCtxSw); //安装uC/OS-II任务切换中断向量
OSTaskCreate (
MyTask, //创建任务TaskStart
s_M, //给任务传递参数
&MyTaskStk[TASK_STK_SIZE-1], //设置任务堆栈栈顶指针
0 //优先级为0
);
OSStart(); //启动多任务调度
}
/*起始任务*/
void MyTask (void *pdata)
{
#if OS_CRITICAL_METHOD= =3
OS_CPU_SR cpu_sr;
#endif
pdata=pdata;
OS_ENTER_CRITICAL();
PC_VectSet(0x08, OSTickISR); //安装uC/OS-II时钟中断向量
PC_SetTickRate(OS_TICKS_PER_SEC); //设置uC/OS-II时钟频率
OS_EXIT_CRITICAL();
OSStatInit(); //初始化统计任务
for(;;)
{
if (x>10)
{
x=0;
y+=2;
}
PC_DispChar (
x, y, //字符的显示位置
*(char *)pdata, //被显示的字符
DISP_BGND_BLACK+DISP_FGND_WRITE
);
x+=1;
if (PC_GetKey(&key)= =TRUE) //如果按下ESC键则退出uC/OS-II
{
if (key= =0x1B)
{
PC_DOSReturn(); //返回DOS
}
}
OSTimeDlyHMSM(0,0,1,0); //等待1s
}
}
六、任务的管理和调度:
1.就绪任务表:
uC/OS-II总是在已就绪任务中选择一个任务来运行。为了了解系统中的任务哪些是就绪任务,uC/OS-II在系统初始化时建立了一个供任务登记的表,系统中的每个任务都在这个表中占据一个位置,并用这个位置的状态(1或0)来表示任务是否处于就绪状态,这个表就叫做任务就绪状态表,简称就绪任务表。
uC/OS-II的就绪任务表,实质上是一个类型为INT8U的数组OSRdyTbl[]。在这个就绪任务表中,以任务的优先级(也为任务的标识)高低为顺序,为每个任务安排了一个二进制位,并规定该位值为1表示对应的任务处于就绪状态,而该位值为0表示对应的任务处于非就绪状态。
因此,一个被分配了任务控制块和任务堆栈的任务,要想取得抢占处理器的资格,必须先在就绪任务表中属于自己的位置上进行登记,否则就永远不会被处理器运行。
由于每个任务在数组中只占一位,因此OSRdyTbl[]数组的一个元素可表达8个任务的就绪状态,每个数组元素描述的8个任务就是一个任务组。为了便于对就绪表的查找,uC/OS-II又定义了一个数据类型为INT8U的变量OSRdyGrp,并使该变量的每一个位都对应OSRdyTbl[]的一个任务组(即数组的一个元素)。如果某任务组中有就绪任务,则在变量OSRdyGrp里把该任务组所对应的位置1,否则置0。
由于变量OSRdyGrp有8个二进制数,每位对应OSRdyTbl[]数组的一个元素,每个元素又可记录8个任务的就绪状态,因此uC/OS-II最多可管理8x8=64个任务。
由于优先级是一个单字节的数字,而且最大值不会超过63,即二进制形式的00111111,因此可把优先级看成是一个6位的二进制数。这样,就可用高3位(D5 D4 D3)来指明变量OSRdyGrp的具体数据位,并用来确定就绪表数组元素的下标;用低3位(D2 D1 D0)来指明该数组元素具体数据位。于是,根据任务的优先级就可以确定该任务在就绪表中的确切位置了。
uC/OS-II在初始化时创建就绪任务表,并把其中所有数据位都置0,即在表中没有就绪任务。
2.就绪任务表的操作:
对就绪任务表的操作有两项:一是把应就绪的任务在就绪表中进行登记;二是在需要时把任务从就绪表中删除。
1)在就绪表中登记就绪任务:
在程序中,可用类型下面的代码把优先级为prio的任务置为就绪状态:
OSRdyGrp|=OSMapTbl[prio>>3];
OSRdyTbl[prio>>3]|=OSMapTbl[prio&0x07];
其中,OSMapTbl[]是uC/OS-II为加快运算速度定义的一个数组,其各元素的值为:
OSMapTbl[0]=00000001B OSMapTbl[1]=00000010B
OSMapTbl[2]=00000100B OSMapTbl[3]=00001000B
OSMapTbl[4]=00010000B OSMapTbl[5]=00100000B
OSMapTbl[6]=01000000B OSMapTbl[7]=10000000B
2)从就绪表中删除:
如果要使一个优先级为prio的任务脱离就绪状态,则可使用如下代码:
if ((OSRdyTbl[prio>>3]&=-OSMapTbl[prio&0x07])= =0)
OSRdyGrp&=-OSMapTbl[prio>>3];
3)从就绪表中获取优先级最高的就绪任务:
uC/OS-II的调度器在进行任务调度时,要在就绪表中查找最高优先级的就绪任务,所以也就是要获得就绪任务中优先级最高任务的优先级别。
从就绪任务表中获取优先级最高的就绪任务可用如下代码:
y=OSUnMapTbl[OSRdyGrp]; //获得优先级的D5 D4 D3位
x=OSUnMapTbl[OSRdyTbl[y]]; //获得优先级的D2 D1 D0位
prio=(y<<3)+x; //获得就绪任务的优先级
或
y=OSUnMapTbl[OSRdyGrp]; //获得优先级的D5 D4 D3位
prio=(INT8U)((y<<3)+OSUnMapTbl[OSRdyTbl[y]]) //获得就绪任务的优先级
该代码执行后得到的是最高优先级就绪任务的优先级(即任务的标识),其中OSUnMapTbl[]同样是uC/OS-II为提高查找速度定义的一个数组,它共有256个元素,其定义如下:
INT8U const OSUnMapTbl[]= {
0,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
6,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
7,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
6,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0
};
由于在使用数组OSUnMapTbl[]时是以OSRdyGrp为下标的,因此这个数组一共有256个元素。也就是说,无论OSRdyGrp的值是多少,在数组OSUnMapTbl[]中总能找到对应的元素值,而且这个元素值就是最高级就绪任务优先级的y。因为数组OSUnMapTbl[]各元素的值是基于这样一个思想来设置的:表示任务组的变量OSRdyGrp是一个8位二进制数,从这个数的最低位向高位查找,碰到的第一个为1的位所对应的就绪任务组一定是最高优先级就绪任务所在的组,所以它的组号一定是最高优先级就绪任务的级别的高3位。例如,变量OSRdyGrp中的第一个为1的位是D3,那么最高优先级就绪任务优先级的高3位一定是011(十进制的3),于是在数组OSUnMapTbl[]的256个元素中,凡是下标值的D3为1的元素值均定义为3。
所以,有了这样一个数组,在查找最高级就绪任务时,只要以变量OSRdyGrp为下标,就可直接在数组OSUnMapTbl[]得到就绪任务的y值了。否则,就要编写一个循环程序在就绪表中查找,不但耗时,而且运算时间不可预测。
同样,这个数组也用来查找最高级就绪任务的x值,这时是以OSRdyTbl[y]为下标来进行查找的。
3.任务的挂起和恢复:
所谓挂起一个任务,就是因某种原因由任务自己或其他任务停止这个任务的运行。uC/OS-II中,用户任务可通过调用系统提供的函数OSTaskSuspend()来挂起自身或者除空闲任务之外的其他任务。用函数OSTaskSuspend()挂起的任务,只能在其他任务中通过调用恢复函数OSTaskResume()使其恢复为就绪状态。
1)挂起任务:
挂起任务函数OSTaskSuspend()的原型如下:
INT8U OSTaskSuspend(INT8U prio);
函数的参数prio为待挂起任务的优先级。若调用函数OSTaskSuspend()的任务要挂起自身,则参数必须为常数OS_PRIO_SELF(该常数在文件uCOS_II.H中被定义为0xFF)。
当函数调用成功时,返回信息OS_NO_ERR;否则,根据出错的具体情况返回OS_TASK_ SUSPEND_IDLE、OS_PRIO_INVALID和OS_TASK_SUSPEND_PRIO等。
函数OSTaskSuspend()流程图中有一系列的判断,主要是判断待挂起的任务是否调用这个函数的任务本身。若是任务自身,则必须在删除任务的就绪状态表中的就绪标志,并在任务控制块成员OSTCBStat中做了挂起记录之后引发一次任务调度,以使CPU去运行就绪的其他任务。若待挂起的任务不是调用函数的任务本身,而是其他任务,则只要删除就绪任务表中被挂起任务的就绪标志,并在任务控制块成员OSTCBStat中做了挂起记录即可。
2)恢复任务:
恢复任务函数OSTaskResume()的原型如下:
INT8U OSTaskResume(INT8U prio);
函数的参数为待恢复任务的优先级。若函数调用成功,则返回信息OS_NO_ERR;否则,根据出错的具体情况返回OS_PRIO_INVALID、OS_TASK_RESUME_PRIO和OS_TASK_NOT _SUSPEND等。
在函数OSTaskSuspend()流程图中,函数在判断任务确实是一个已存在的挂起任务,同时又不是一个等待任务(任务控制块成员OSTCBDly=0)时,就清除任务控制块成员OSTCBStat中的挂起记录并使任务就绪,最后调用调度器OSSched()进行任务调度,并返回函数调用成功的信息记录OS_NO_ERR。
4.任务优先级的修改:
每一个任务都必须有一个优先级,但这个优先级并不是一成不变的。在程序的运行过程中,任务可根据需要通过调用函数OSTaskChangePrio()来改变任务的优先级。
函数OSTaskChangePrio()的原型如下:
INT8U OSTaskChangePrio (
INT8U oldprio, //任务现在的优先级
INT8U newprio, //要修改的优先级
);
若调用函数OSTaskChangePrio()成功,则函数返回OS_NO_ERR。
5.任务的删除:
所谓删除一个任务,就是把该任务置于睡眠状态。具体的做法是,把当初分配给被删除任务的任务控制块从任务控制块链表中删除,并归还给空任务控制块链表,然后在就绪任务表中把该任务的就绪状态位置0,于是该任务就不能再被调度器调度了。
在任务中,可通过调用函数OSTaskDel()来删除任务自身或者除了空闲任务以外的其他任务。
删除任务函数OSTaskDel()的原型如下:
#if OS_TASK_DEL_EN
INT8U OSTaskDel(INT8U prio); //prio为要删除任务的优先级
如果一个任务调用这个函数是为了删除任务自己,则应在调用函数时令函数的参数prio为OS_PRIO_SELF。
有时,任务会占用一些动态分配的内存或信号量之类资源。这时,如果有其他任务把这个任务删除了,那么被删除任务所占用的一些资源就会因为没有释放而丢失,这是任何系统都无法接受的。因此,在删除一个占用资源的任务时一定要谨慎。具体的办法是,提出删除任务请求的任务只负责提出删除任务请求,而删除工作则由被删除任务自己来完成。这样,被删除任务就可以根据自身的具体情况来决定何时删除自身,从而使其有机会在删除自身之前把占用的资源释放掉。
显然,如果想使提出删除任务请求的任务和被删除任务之间,能够像上述方式来执行删除工作,则它们双方必须有某种通信方法。uC/OS-II在所有任务控制块中都设置了一个成员OSTCBDelReq来作为这个联络信号。当任务A要删除任务B时,任务A就将任务B的任务控制块中OSTCBDelReq的值设为OS_TACK_DEL_REQ,意思是“你应该删除你自己了”。这样,当任务B发现自己的任务控制块中成员OSTCBDelReq的值为OS_TACK_DEL_REQ时,就会在合适的时候删除自身了。
为了完成上述任务,uC/OS-II提供了一个双方都能调用的函数--请求删除任务函数OSTaskDelReq()。这样,提出删除任务请求的任务和被删除任务的双方就都使用这个函数来访问OSTCBDelReq这个信号,从而可根据这个信号的状态来决定各自的行为。
函数OSTaskDelReq()的原型如下:
INT8U OSTaskDelReq(INT8U prio); //prio为待删除任务的优先级
提出删除任务请求的任务在调用这个函数时,函数的参数应为被删除任务的优先级prio;被删除任务在调用这个函数时,函数的参数应为OS_PRIO_SELF。
函数OSTaskDelReq()的流程中也有一系列判断,先要查看被删除的任务控制块是否还在,如果在则令被删除任务的任务块成员OSTCBDelReq的值为OS_TACK_DEL_REQ,且通知该任务;如果不在则认为被删除任务已经被删除。
while(OSTaskDelReq(44)!=OS_TASK_NOT_EXIST)
{
OSTimeDly(1); //延时一个时钟节拍
}
即通过不断地调用函数OSTaskDelReq()来查询优先级为44的任务是否还存在。只要还存在,就调用延时函数OSTimeDly()等待,直到发现被删除任务不存在了才继续运行。
被删除任务方一定要用OS_PRIO_SELF作为参数来调用OSTaskDelReq()函数,函数判断出参数是OS_PRIO_SELF时,将会返回任务TCB的域OSTCBDelReq的值,如果该值为OS_TACK_DEL_REQ,就意味着有其他任务发出了删除任务请求,那么被删除任务就应在适当的时候调用函数OSTaskDel(OS_PRIO_SELF)来删除自己。
被删除任务方调用函数OSTaskDelReq()的典型代码段如下:
if (OSTaskDelReq(OS_PRIO_SELF)= =OS_TACK_DEL_REQ)
{
...... //释放资源和动态内存的代码
OSTaskDel(OS_PRIO_SELF); //确保优先级
}
else
{
...... //其他应用代码
}
6.查询任务的信息:
有时,在应用程序运行中需要了解一个任务的指针、堆栈等信息,这时就可通过调用函数OSTaskQuery()来获取选定的任务的信息。函数OSTaskQuery()的原型如下:
INT8U OSTaskQuery (
INT8U prio, //待查询任务的优先级
OS_TCB *pdata //存储任务信息的结构
);
若调用函数OSTaskQuery()查询成功,则函数将返回OS_NO_ERR,并把查询得到的任务信息存放在结构OS_TCB类型的变量中。
七、uC/OS-II的任务调度:
在多任务系统中,按某种规则选择待运行任务的工作叫做调度,而令处理器中止当前正在运行的任务转而去运行另外的一个任务的工作叫做任务切换。一般情况下,系统常常是把任务调度和任务切换作为一个系统服务来提供的,用来完成这个任务的系统程序模块或系统函数就叫做调度器。
uC/OS-II有两种调度器:任务级的调度器和中断级的调度器。任务级的调度器由函数OSSched()来实现,中断级的调度器由函数OSIntExt()来实现。
1.调度器OSSched()的任务调度部分:
调度器OSSched()的前半部分叫调度部分,职责就是寻找优先级最高的就绪任务作为待运行任务。也就是说,要通过就绪任务表获得待运行任务的标识(优先级),进而获得控制该任务的依据--任务控制块。
由于uC/OS-II中所有任务都各自具有一个唯一的由用户确定的优先级,并且所有就绪任务都按这个优先级顺序登记在就绪任务表OSRdyGrp和OSRdyTbl[]中,加上uC/OS-II又是可剥夺型内核,所以调度器OSSched()的调度算法及其简单。
y=OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy=(INT8U)((y<<3)+OSUnMapTbl[OSRdyTbl[y]])
从就绪任务表中获得最高级就绪任务的优先级,并存放在变量OSPrioHighRdy中。然后,用这个优先级OSPrioHighRdy从存放任务控制块指针的数组OSTCBPrioTbl[]中获得该任务的任务控制块指针,并存放在指针变量OSTCBHighRdy中。完成这个工作的代码如下:
OSTCBHighRdy=OSTCBPrioTbl[OSPrioHighRdy];
只要获得了最高级就绪任务的任务控制块指针,再加上存放在指针变量OSTCBCur中的当前运行任务的任务控制块,系统就掌握了当前任务和待运行任务的控制块,然后就可以进行任务切换了。
任务级调度器OSSched的源代码如下:
INT8U OSSched ( void )
{
#if OS_CRITICAL_METHOD= =3
OS_CPU_SR cpu_sr;
#endif
INT8U y;
OS_ENTER_CRITICAL();
if ((OSLockNesting|OSIntNesting)= =0) //
{
y=OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy=(INT8U)((y<<3)+OSUnMapTbl[OSRdyTbl[y]])
if (OSPrioHighRdy!=OSPrioCur) //
{
OSTCBHighRdy=OSTCBPrioTbl[OSPrioHighRdy]; //得到任务控制块指针
OSCtxSwCtr++; //统计任务切换次数的计数器加1
OS_TASK_SW();
}
}
OS_EXIT_CRITICAL();
}
从代码中可以看到,一个高于当前运行任务优先级的就绪任务,只有当调度器进行调度时才有机会抢占处理器。因此,调度器是否存在调度禁区(调度死区)以及这个禁区有多大,是直接影响内核实时性的一个重要因素。
在调度器OSSched()中,uC/OS-II的调度禁区是用以下代码来实现的:
if ((OSLockNesting|OSIntNesting)= =0) //
即调度器在进行调度之前要对变量OSIntNesting进行判断,如果该变量不为0,那么就不会进行调度,当然也就不会有任务切换。uC/OS-II规定,在中断服务程序中不允许进行任务调度,所以每当进入中断服务程序就要把变量OSIntNesting加1,而当中断返回前则要把变量OSIntNesting减1,这样调度器就不会在中断服务程序中进行调度工作了。
为了用户可对调度器的工作进行控制,uC/OS-II还提供了两个系统函数OSSchedLock()和OSSchedUnlock():前者的作用是为调度器调度器上锁,后者的作用是为调度器解锁。为了记录调度器被锁和解锁的情况,uC/OS-II还定义了一个全局变量OSLockNesting。当应用程序每调用一次函数OSSchedLock()为调度器上锁时,变量OSLockNesting就加1;反之,应用程序每调用一次函数OSSchedUnlock()为调度器解锁时,变量OSLockNesting就减1.所以,调度器在判断是否要进行调度时,还要查看变量OSLockNesting的当前值。如果OSLockNesting非0,则禁止进行调度;反之,则进行调度。
综上所述,只要调度器未被上锁或未处在中断服务程序中,调度器就可以进行调度。在调度禁区这个方面,uC/OS-II是明显优于一般操作系统的。因为在一般操作系统中,是禁止在系统调用中进行调度的,而uC/OS-II则无此限制。因而,与其他操作系统相比,uC/OS-II的调度禁区就显得更小,可剥夺性也就显得更为强硬。
一个小的调度禁区,再加上时间可预测且极为简单快速的调度算法,就为uC/OS-II成为一个性能优良的实时内核提供了保证。
2.调度器OSSched()的任务切换部分:
调度器获得了最高级就绪任务的任务控制块指针后,任务切换的工作是由宏OSCtxSw()来执行的。所谓任务切换,就是中止正在运行的任务(当前任务),转而去运行另外一个任务的工作。
1)任务断点的保存:
如果把任务被中止运行时的位置叫做断点,而把当时处理器的PC、PSW等各寄存器中数据的集合叫做断点数据,那么当任务再次运行时,必须在断点处以断点数据作为初始数据接着运行才能实现“无缝”的接续运行。于是,任务要实现“无缝”接续运行,就必须在任务被中止时,就把该任务的断点数据保存起来,以备将来被重新运行时能在处理器的各寄存器中恢复这些断点数据。
断点数据保存的最好办法是保存在任务自己的堆栈中,所以创建任务时都要为每个任务设置一个私有的任务堆栈。
为了将来能恢复断点数据,在保护断点数据之后,还要把任务堆栈当前的指针(SP)保存在任务控制块的成员变量OSTCBStkPtr中。
2)任务的切换:
一个被中止的任务能否正确地在断点处恢复运行,其关键在于是否能正确地在处理器各寄存器中恢复断点数据;而能正确地恢复断点数据的关键是处理器的堆栈指针SP是否有正确的指向。在系统中存在多个任务时,如果在恢复断点数据时用另一个任务的任务指针(存放在控制块成员OSTCBStkPtr中)来改变处理器的堆栈指针SP,那么处理器运行的就不是刚才被中止运行的任务,而是另一个任务了,也就是实现了任务切换。
任务切换的实质是断点数据的切换,也就是处理器堆栈指针的切换,被中止运行任务的任务堆栈指针要保护到该任务的任务控制块中,待运行任务的任务堆栈指针要由该任务控制块转存到处理器的SP中。保证完成上述任务的前提是要获得被中止任务和待运行任务的任务控制块。
uC/OS-II定义了一个函数OSCtxSw()来完成任务切换,函数依次要完成如下任务:
①把被中止任务的断点指针保存到任务堆栈中;
②把处理器调用寄存器的内容保存到任务堆栈中;
③把被中止任务的任务堆栈指针当前值保存到该任务的任务控制块的OSTCBStkPtr中;
④获得待运行任务的任务控制块;
⑤使处理器通过任务控制块获得待运行任务的任务堆栈指针;
⑥把待运行任务堆栈中通用寄存器的内容恢复到处理器的调用寄存器中;
⑦使处理器获得待运行任务的断点指针(该指针是待运行任务在上次被调度器中止运行时保留在任务堆栈中的)。
由于uC/OS-II总是把当前正在运行任务的控制块指针存放在指针变量OSTCBCur中,并且在调度器的调度过程中已经得到了待运行任务的控制块指针OSTCBHighRdy,所以完成上述②~⑥的各项工作很容易。示意性代码如下:
......; //用压栈指令把处理器通用寄存器R1 R2...压入堆栈
OSTCBCur->OSTCBStkPtr=SP; //把SP保存在中止任务控制块中
OSTCBCur=OSTCBHighRdy; //使系统获得待运行任务控制块
SP=OSTCBHighRdy->OSTCBStkPtr; //把待运行任务堆栈指针赋予SP
......; //用出栈指令把R1 R2...弹入处理器通用寄存器
但完成①和⑦就有一些麻烦。处理器是按特殊功能寄存器PC(程序指针)的指向来运行程序的。但目前的处理器一般没有对PC的出栈和入栈指令,就只能用可以改变PC值的指令(例如CALL、INT或IRET等)来变通了,即想办法引起一次中断或调用,并让中断向量指向OSCtxSw()(其实这个函数就是中断服务程序),利用系统在跳转到中断服务程序时会自动把断点指针压入堆栈的功能把断点指针存入堆栈,而利用中断返回指令IRET(或有相同功能的指令)能把中断指针推入处理器的PC寄存器的功能恢复待运行任务的断点,这样就可以实现断点的保存和恢复了。
由于任务切换时需要对处理器的寄存器进行操作,一般情况下OSCtxSw()都要用汇编语言来写。OSCtxSw()的示意性代码如下:
void OSCtxSw(void)
{
......; //用压栈指令把处理器通用寄存器R1 R2...压入堆栈
OSTCBCur->OSTCBStkPtr=SP; //在中止任务控制块中保存SP
OSTCBCur=OSTCBHighRdy; //任务控制块的切换
OSPrioCur=OSPrioHighRdy;
SP=OSTCBHighRdy->OSTCBStkPtr; //使SP指向待运行任务堆栈
......; //用出栈指令把R1 R2...弹入处理器通用寄存器
IRET; //中断返回,使PC指向待运行任务
}
引发中断,是宏OS_TASK_SW()的作用。如果使用的处理器具有软中断指令,则可在这个宏中封装一个软中断指令;如果使用的处理器没有提供软中断指令,则可以试试在宏中封装其他可使PC等相关寄存器压栈的指令(例如调用指令)。
3)调度的时机:
对于uC/OS-II这种严格按照任务优先级进行调度的系统来说,只有就绪任务表的内容发生变化时才需要调度。在uC/OS-II中,只有如下情况才能使就绪任务表内容发生变化:
①有新任务被创建,并在就绪任务表中进行了登记;
②有任务被删除;
③有处于等待状态的任务被唤醒;
④由于异步事件的发生,在中断服务程序中激活了一个或几个任务;
⑤正在运行的任务需要等待某个事件而进入等待状态;
⑥正在运行的任务调用延时函数而自愿进入等待状态。
综上所述,uC/OS-II应在所有系统调用函数的末尾及中断服务程序结束之前调用调度器OSSched()。
八、uC/OS-II的中断:
中断,是计算机系统处理异步事件的重要机制。当异步事件发生时,事件向处理器发出中断请求,在一般情况下处理器会响应这个请求,暂停正在运行的程序,而去运行一个叫做中断服务程序的程序来处理该事件。
任务在运行过程中,应内部或外部异步事件的请求,中止当前任务,而去处理异步事件所要求的任务的过程叫做中断;应中断请求而运行的程序叫做中断服务子程序(Interrupt Service Routines,ISR)。中断服务子程序的入口地址叫做中断向量。
1.uC/OS-II的中断过程:
uC/OS-II系统响应中断的过程是:系统接收到中断请求后,如果处理器处于中断允许状态(即中断是开放的),系统会中止正在运行的当前任务,而按照中断向量的指向转而去运行中断服务子程序;当中断服务子程序运行结束后,系统将会根据情况返回到被中止的任务继续运行,或者转到另外一个具有更高优先级的就绪任务上运行。
对于可剥夺型的uC/OS-II内核来说,中断服务子程序运行结束前,需要进行一次任务调度。正是因为有了这次调度,在中断结束时,系统才有可能去运行另外一个任务,而不是一定要返回被中断的任务。当然,这个被运行的任务一定是优先级最高的就绪任务,其目的是提高系统的实时性。
uC/OS-II系统允许中断嵌套,即在中断服务程序的运行过程中,处理器可响应高优先级中断源的中断请求。为记录中断嵌套的层数,uC/OS-II定义了一个全局变量OSIntNesting。同时,变量OSIntNesting也作为调度器是否可进行调度的标志,以保证调度器不会在中断服务程序中进行任务调度。所以,全局变量OSIntNesting有两个用途:一是记录中断嵌套层数;二是为调度器加锁或解锁。uC/OS-II用了两个函数OSIntEnter()和OSIntExit()来处理变量OSIntNesting。
OSIntEnter()函数比较简单,其作用就是把全局变量OSIntNesting加1,从而用它来记录中断嵌套的层数并为调度器加锁。嵌套的层数并为调度器加锁。OSIntEnter()函数的代码如下:
void OSIntEnter(void)
{
if (OSRunning= =TRUE)
{
if (OSIntNesting<255) //
{
OSIntNesting++;
}
}
}
函数OSIntEnter()经常在中断服务程序保护被中断任务的断点数据之后,运行用户中断服务代码之前来调用,所以通常把它叫做进入中断服务程序函数。
退出中断服务函数OSIntExit()的源代码如下:
void OSIntExit(void)
{
#if OS_CRITICAL_METHOD= =3
OS_CPU_SR cpu_sr;
#endif
if (OSRunning= =TRUE)
{
OS_ENTER_CRITICAL();
if (OSIntNesting>0) //
{
OSIntNesting--;
}
if ( (OSIntNesting= =0) && (OSLockNesting= =0) )
{
OSIntExitY=OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy=(INT8U)((OSIntExitY<<3)+OSUnMapTbl[OSIntExitY]]);
if (OSPrioHighRdy!=OSPrioCur)
{
OSTCBHighRdy=OSTCBPrioTbl[OSPrioHighRdy];
OSCtxSwCtr++;
OSIntCtxSw();
}
}
OS_EXIT_CRITICAL();
}
}
从中可以看到,在中断嵌套层数计数器为0、调度器未被锁定且从任务就绪表中查找到的最高级就绪任务又不是被中断任务的条件下,将要进行任务切换;否则,就返回被中断的服务子程序。
OSIntEnter()用来通知内核:现在已进入中断服务程序,禁止调度;OSIntExit()用来通知内核:中断服务程序已结束,可以进行调度。因此,在这两个函数之间就是调度禁区,因此要成对使用。
uC/OS-II中,通常用一个任务来完成异步事件的处理工作,而在中断服务程序中只是通过向任务发送消息的方法去激活这个任务。
2.中断级任务切换函数:
uC/OS-II在运行完中断服务程序之后,并不一定返回到被中断的任务上去,而是通过一次任务调度来决定是返回被中断的任务,还是运行一个具有更高优先级的就绪任务,因此系统需要一个在中断服务程序中使用的调度器--中断级任务调度器。
在上面的OSIntExit()的源代码中,在中断嵌套层数计数器为0、调度器未被锁定且从任务就绪表中查找到的最高级就绪任务又不是被中断任务的条件下,将要调用函数OSIntCtxSw(),该函数就是用来完成任务切换工作的。
中断级任务切换函数OSIntCtxSw()通常是用汇编语言来写的,其示意性代码如下:
void OSIntCtxSw(void)
{
OSTCBCur=OSTCBHighRdy; //任务控制块的切换
OSPrioCur=OSPrioHighRdy;
SP=OSTCBHighRdy->OSTCBStkPtr; //使SP指向待运行任务堆栈
......; //用出栈指令把R1 R2...弹入处理器通用寄存器
IRET; //中断返回,使PC指向待运行任务
}
对照任务级任务切换函数OSCtxSw()的代码可以发现,中断级任务切换函数的代码与任务级的后半段完全相同,因为被中断任务的断点保护工作已经在中断服务程序中完成了。
需要注意,记录中断嵌套层数的变量OSIntNesting是一个计数器,所以只有当其值为0时才允许调度。因此,尽管嵌套中断中每次中断服务程序都调用了中断退出函数OSIntExit(),但并非每个嵌套中断结束前都会发生调度,而只有当变量OSIntNesting为0时才会发生调度。
3.临界段的处理:
当有异步事件发生时会引起中断,但处理器不是在任何情况下都会相应这个请求的,处理器只有在中断开放期间才能响应中断请求,而在其他时间是不能响应中断请求的。因此,所有处理器指令系统中都有开中断和关中断指令,以使应用程序可通过这些指令来控制处理器何时中断开放,何时中断关闭。
之所以这样,是因为在应用程序中经常有一些代码段必须不受任何干扰地持续运行,这样的代码段叫做临界段。因此,为了使临界段在运行时不受中断所打断,在临界段代码前必须关中断屏蔽中断请求,而在临界段代码后开中断以解除屏蔽,使得处理器可以响应中断请求。
由于各厂商生产的处理器和C编译器的开关中断的方法和指令不尽相同,为增强uC/OS-II的可移植性,uC/OS-II用OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()这两个宏来实现中断的开放和关闭,而把与系统硬件相关的开关中断指令分别封装在这两个宏中。
uC/OS-II提示,不要在临界段中调用uC/OS-II提供的功能函数,以免系统崩溃。这是因为,在功能函数中通常也会用到这两个宏,而处理器的开中断和关中断指令是不能嵌套使用的。
uC/OS-II提示,不要在临界段中调用uC/OS-II提供的功能函数,以免系统崩溃。这是因为,在功能函数中通常也会用到这两个宏,而处理器的开中断和关中断指令是不能嵌套使用的。
宏OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()可以有三种不同的实现方法,用户可通过定义移植文件OS_CPU.H中常数OS_CRITICAL_METHOD来选择实现方法。
①OS_CRITICAL_METHOD=1:直接使用处理器的开中断和关中断指令来实现宏。
其示意性代码如下:
#define OS_ENTER_CRITICAL() asm(“DI”) //关中断
#define OS_EXIT_CRITICAL() asm(“EI”) //关中断
②OS_CRITICAL_METHOD=2:加上把处理器的允许中断标志压栈及弹出。
其示意性代码如下:
#define OS_ENTER_CRITICAL() \
asm(“PUSH PSW”) asm(“DI”) //关中断
#define OS_EXIT_CRITICAL() \
asm(“POP PSW”) //关中断
③OS_CRITICAL_METHOD=3:将程序状态字的值保存在C语言函数的局部变量中
其示意性代码如下:
#define OS_ENTER_CRITICAL() \
cpu_sr=get_processor_psw(); \ //把程序状态字保存cpu_sr中
disable_interrupts(); //关中断
#define OS_EXIT_CRITICAL() \
set_processor_psw(cpu_sr); //从cpu_sr中恢复程序状态字
使用的前提是:用户使用的C编译器具有扩展功能,用户可获得程序状态字的值。由于不知道用户的C编译器所提供的函数名,因此上面的宏中使用的函数名称只是示意而已。
九、uC/OS-II的时钟:
任何一个计算机系统都应该有一个系统时钟,操作系统也都要提供一个周期性的信号源,以提供系统处理诸如任务的等待、延时等与时间有关的事件,这个周期性的信号源叫做时钟。uC/OS-II用硬件定时器产生一个周期为ms级的周期性中断来实现系统时钟,最小时钟单位就是两次中断之间的间隔时间,这个最小时钟单位叫做时钟节拍(Time Tick)。
1. 时钟节拍的中断服务程序:
硬件定时器以时钟节拍为周期定时地产生中断,该中断的中断服务程序叫做OSTickISR()。中断服务程序通过调用函数OSTimeTick()来完成系统在每个时钟节拍时需要完成的工作。
因为使用C语言不便于对处理器的寄存器进行处理,所以时钟节拍的中断服务程序OSTickISR()是用汇编语言来编写的。OSTickISR()的示意性代码如下:
void OSTickISR(void)
{
保存处理器寄存器;
调用OSIntEnter(); //记录中断嵌套层数
if (OSIntNesting= =1)
{
OSTCBCur->OSTCBStkPtr=SP; //在任务TCB中保存堆栈指针
}
调用OSTimeTick(); //节拍处理
清除中断;
开中断;
调用OSIntExit(); //中断嵌套层数减1
恢复处理器寄存器;
中断返回;
}
在时钟中断服务程序中调用的OSTimeTick()叫做时钟节拍服务程序。其源代码如下:
void OSTickISR(void)
{
#if OS_CRITICAL_METHOD= =3
OS_CPU_SR cpu_sr;
#endif
OS_TCB *ptcb;
OSTimeTickHook();
#if OS_TIME_GET_SET_EN>0
OS_ENTER_CRITICAL();
OSTime++;
OS_EXIT_CRITICAL();
#endif
if (OSRunning= =TRUE)
{
ptcb=OSTCBList;
while (ptcb->OSTCBPrio!=OS_IDLE_PRIO)
{
OS_ENTER_CRITICAL();
if (ptcb->OSTCBDly!=0)
{
if (--ptcb->OSTCBDly= =0)
{
if ((ptcb->OSTCBStat&OS_STAT_SUSPEND)= =OS_STAT_RDY)
{
OSRdyGrp|=ptcb->OSTCBBitY;
OSRdyTbl[ptcb->OSTCBY]|=ptcb->OSTCBBitX;
}
else
{
ptcb->OSTCBDly=1;
}
}
}
ptcb=ptcb->OSTCBNext;
OS_EXIT_CRITICAL();
}
}
从上面代码可知,uC/OS-II在每次响应定时中断时调用OSTickISR()做了两件事情:一是用来记录时间进程的计数器OSTime加1;二是遍历任务控制块链表中的所有任务控制块,把各个任务控制块中用来存放任务延时时限的OSTCBDly变量减1,并使该项为0并且又不是被挂起的任务(即延时时间已到的任务)进入就绪状态。
简单的说,函数OSTimeTick()的任务就是在每个时钟节拍了解每个任务的延时状态,使其中已经到了延时时限的非挂起任务进入就绪状态。
OSTimeTick()是系统调用的函数,为了使应用程序设计人员能在系统调用的函数中插入一些自己的工作,uC/OS-II提供了时钟节拍服务函数OSTimeTickHook()钩子函数。
此外,uC/OS-II还提供了OSStkInitHook()、OSInitHookBegin()、OSInitHookEnd()、OSTask CreateHook()、OSTaskDelHook()、OSTaskSwHook()、OSTaskStatHook()、OSTCBInitHook()、OSTaskIdleHook()和OSTimeTickHook()共10个钩子函数,以供用户在系统调用函数中书写自己的代码。
2.uC/OS-II的时间管理:
uC/OS-II在时间管理上主要是在任务的延时、取消延时、设置和获取系统时间等方面提供任务。
1)任务的延时:
在应用程序的设计中,经常会碰到因某种原因需要程序暂停一段时间后再继续运行的情况。另外,由于嵌入式系统的任务是一个无限循环的结构,并且uC/OS-II又是一个可剥夺型内核,所以为了使高优先级的任务不至于独占处理器,可以给其他优先级较低的任务获得处理器使用权的机会,uC/OS-II规定:除了空闲任务之外的所有任务,必须在任务中的合适位置暂停运行一段时间,已给其他任务提供获取处理器的机会。也就是说,高优先级的任务要以延时运行的手段,自愿地出让处理器使用权。
为此,系统提供了任务延时函数OSTimeDly(),其代码如下:
void OSTimeDly(INT16U ticks)
{
#if OS_CRITICAL_METHOD= =3
OS_CPU_SR cpu_sr;
#endif
if (ticks>0)
{
OS_ENTER_CRITICAL();
if ((OSRdyTbl[OSTCBCur->OSTCBY]
&=~OSTCBCur->OSTCBBitX)= =0)
{
OSRdyGrp&=~OSTCBCur->OSTCBBitY; //取消当前任务的就绪状态
}
OSTCBCur->OSTCBDly=ticks; //延时节拍数存入任务控制块
OS_EXIT_CRITICAL();
OS_Sched();
}
}
函数的参数ticks是以节拍数为单位的延时时间。
为了能使用更为习惯的方法来使用任务延时函数,uC/OS-II还提供了一个以时、分、秒、毫秒为参数的任务延时函数OSTimeDlyHMSM(),该函数的原型如下:
INT8U OSTimeDlyHMSM (
INT8U hours, //时
INT8U minutes, //分
INT8U seconds, //秒
INT16U milli, //毫秒
);
该函数与函数OSTimeDly()一样,在结束前也会引发一次调度。
调用了函数OSTimeDly()或OSTimeDlyHMSM()的任务,当规定的延时时间期满,或有其他任务通过调用函数OSTimeDlyResume()取消了延时时,它会立即进入就绪状态。
2)取消任务的延时:
延时的任务,可通过在其他任务中调用OSTimeDlyResume()取消延时而进入就绪状态。如果任务比正在运行的任务优先级高,则立即会引发一次任务调度。
OSTimeDlyResume()函数的原型如下:
INT8U OSTimeDlyResume (INT8U prio);
参数prio为被取消延时任务的优先级。
OSTimeDlyResume()函数的原代码如下:
INT8U OSTimeDlyResume (INT8U prio);
{
#if OS_CRITICAL_METHOD= =3
OS_CPU_SR cpu_sr;
#endif
OS_TCB *ptcb;
if (prio>=OS_LOWEST_PRIO)
{
return(OS_PRIO_INVALID);
}
OS_ENTER_CRITICAL();
ptcb=(OS_TCB *)OSTCBPrioTbl[prio];
if (ptcb!=(OS_TCB *)0)
{
if (ptcb->OSTCBDly!=0)
{
ptcb->OSTCBDly=0;
if ((ptcb->OSTCBStat&OS_STAT_SUSPEND)= =OS_STAT_RDY)
{
OSRdyGrp|=ptcb->OSTCBBitY;
OSRdyTbl[ptcb->OSTCBY]|=ptcb->OSTCBBitX;
OS_EXIT_CRITICAL();
OS_Sched();
}
else
{
OS_EXIT_CRITICAL();
}
return(OS_NO_ERR);
}
else
{
OS_EXIT_CRITICAL();
return(OS_TIME_NOT_DLY);
}
}
OS_EXIT_CRITICAL();
return(OS_TASK_NOT_EXIST);
}
3)获取和设置系统时间:
为了方便,系统定义了一个INT32U类型的全局变量OSTime来记录系统发生的时钟节拍数。OSTime在应用程序调用OSStart()时被初始化为0,以后每发生一次时钟节拍,OSTime的值就被加1。
在应用程序调用函数OSTimeGet()可获取OSTime的值。该函数的原型如下:
INT32U OSTimeGet (void);
函数的返回值即为OSTime的值。
如果在应用程序调用函数OSTimeSet(),则可设置OSTime的值。函数的原型为:
void OSTimeSet (INT32U ticks);
函数的参数ticks为OSTime的设置值(节拍数)。
十、任务间的同步:
一般情况下,嵌入式系统中的所有任务都是一个大任务的子任务,它们之间有着各种各样的关联,所以需要通过彼此之间的有效合作来完成一项大规模的工作。呀合作就要有沟通,也就是通信,并通过通信来形成任务之间的默契和协调,从而使各任务可以无冲突、流畅地同步运行。
1.任务间的同步:
为了实现各任务之间的合作和无冲突的运行,在有关联的任务之间,必须建立一些制约关系。这些制约关系主要有两种:直接制约关系和间接制约关系。
在多任务合作工作的过程中,操作系统应解决两个问题:一是各任务间应具有一种互斥关系,即对于某个共享资源,如果一个任务正在使用,则其他任务只能等待,等到该任务释放该资源后,等待的任务之一才能使用它;二是相关的任务在执行上要有先后次序,一个任务要等其伙伴发来通知,或建立了某个条件后才能继续执行,否则只能等待。
任务之间的这种制约性的合作运行机制叫做任务间的同步。
uC/OS-II中,使用信号量、消息邮箱和消息队列这些数据结构来作为中间媒介进行通信。由于这些数据结构将要影响任务的程序流程,所以它们也被称做事件。
1)信号量:
信号量是一类用来进行任务间通信的最基本事件。使用信号量的最初目的,是为了共享资源设立一个表示该共享资源被占用情况的标志。有了这个标志,任务在访问该共享资源之前,就可通过查询这个标志,了解资源被占用的情况,然后再决定下步行为,而不至于在不知情的情况下与其他任务发生冲突。
信号量有互斥型信号量和计数式信号量。互斥型信号量是一个二值信号,可以实现共享资源的独占式占用。
在严格按优先级进行调度的可剥夺型内核中,在使用了信号量进行同步的任务中,制约任务能否运行的条件有两个:一个是它的优先级,另一个是它是否获得了它正在等待的信号量。这样,也就产生优先级反转问题,需要加以解决。
2)消息邮箱:
在多任务操作系统中,常常需要通过传递一个数据(消息)的方式来进行任务之间的通信。为了达到这种目的,可以在内存中创建一个存储空间作为该数据的缓冲区。如果把这个缓冲区叫做消息缓冲区,那么在任务间传递数据(消息)的一个最简单的方法就是传递消息缓冲区的指针。于是,用来传递消息缓冲区指针的数据结构(事件)就叫做消息邮箱。
3)消息队列:
消息邮箱不仅可用来传递一个消息,而且也可定义一个指针数据。让数组的每个元素都存放一个消息缓冲区指针,那么任务就可通过传递这个指针数组指针的方法来传递多个消息。这种可以传递多个消息的数据结构叫做消息队列。
2. 事件及事件控制:
uC/OS-II把信号量、消息邮箱和消息队列这类用于任务同步和通信的数据结构叫做事件。
1)等待任务表:
uC/OS-II采用了与任务就绪表类似的方法对等待事件进行记录和排序,使用了一个INT8U类型的数组OSEventTbl[]作为记录等待事件任务的记录表,这个表叫做等待任务表。在这个等待任务表中仍然是以任务的优先级为顺序,令系统中的每个任务都在表中占据一位,并用该位为1来表示这一位对应的任务为事件的等待任务,否则不是等待任务。同样,为了加快对该表的访问速度,也定义了一个INT8U类型的变量OSEventGrp来表示等待任务表中的任务组。
至于等待任务的等待时限,则记录在等待任务的任务控制块TCB的成员OSTCBDly中,并在每个时钟节拍中断服务程序中对该数据进行维护。每当有任务的等待时限已到时,则将该任务从事件等待任务表中删除,并使它进入就绪状态。
2)事件控制块:
为了把描述事件的数据结构统一起来,uC/OS-II把事件等待任务表与事件相关的其他信息组合起来定义了一个叫做事件控制块ECB的数据结构。这样,在uC/OS-II中统一用ECB来描述诸如信号量、消息邮箱和消息队列这些事件。
在文件uCOS_II.H中,事件控制块的定义如下:
typedef struct {
INT8U OSEventType; //事件的类型
INT16U OSEventCnt; //信号量计数器
void *OSEventPtr; //消息或消息队列的指针
INT8U OSEventGrp; //等待事件的任务组
INT8U OSEventTbl[OS_EVENT_TBL_SIZE]; //任务等待表
}OS_EVENT;
应用程序中的任务通过指针pevent来访问事件控制块。成员OSEventTbl[OS_EVENT_TBL_ SIZE]是一个数组。这个数组的格式与前面任务就绪表的格式一样。应用程序中的所有任务按照优先级各自在表中占据一个二进制位,并用该位的值是1还是0来表示该位对应的任务是否为正在等待事件的任务,这个表被叫做任务等待表。
与任务就绪表类似,结构成员OSEventGrp表示任务等待表中的各任务组是否存在等待任务。
事件控制块ECB结构中的成员OSEventType用来指明事件的类型,取值的可能如下:
OS_EVENT_TYPE_SEM 事件是信号量 OS_EVENT_TYPE_MUTEX 事件是互斥型信号量
OS_EVENT_TYPE_MBOX 事件是消息邮箱 OS_EVENT_TYPE_Q 事件是消息队列
OS_EVENT_TYPE_UNUSED 空事件控制块(未被使用的事件控制块)
3)事件控制块的基本操作函数:
uC/OS-II有4个对事件控制块进行基本操作的函数(定义在文件OS_CORE.C中),以供操作信号量、消息邮箱、消息队列等事件的函数来调用。
①事件控制块的初始化函数:
调用OS_EventWaitListInit()函数可对事件控制块进行初始化,函数的原型如下:
void OS_EventWaitListInit (OS_EVENT *pevent); //参数为事件控制块的指针
这个函数的作用就是把变量OSEventGrp及任务等待表中的每一位都清0,即令事件的任务等待表中不含有任何等待任务。
初始化事件控制块的函数OS_EventWaitListInit(),将在任务调用OSXXXCreate()函数创建事件时,被OSXXXCreate()函数所调用。其中XXX的含义为:Sem--对信号量进行操作的函数;Mutex--对互斥型信号量进行操作的函数;Mbox--对消息邮箱进行操作的函数;Q--对消息队列进行操作的函数。
②使一个任务进入等待状态的函数:
当一个任务在请求一个事件而不能获得时,应把这个任务登记在事件的等待任务列表中,并把任务控制块中的任务状态置为等待状态和把任务置为非就绪任务。
把一个任务置于等待状态要调用OS_EventTaskWait()函数,函数原型如下:
void OS_EventTaskWait (OS_EVENT *pevent); //参数为事件控制块的指针
函数OS_EventTaskWait()将在任务调用函数OSXXXPend()请求一个事件时,被OSXXXPend()所调用。
③使一个正在等待任务进入就绪状态的函数:
如果一个正在等待的任务具备了可以运行的条件,那么就要使它进入就绪状态。这时要调用OS_EventTaskRdy()函数,该函数的作用就是把调用这个函数的任务在任务等待表中的位置清0(解除等待状态)后,再把任务在任务就绪表中的对应位置置1,然后引发一次任务调度。
函数OS_EventTaskRdy()的原型如下:
void OS_EventTaskRdy (
OS_EVENT *pevent, //事件控制块的指针
void *msg, //未使用
INT8U msk //清除TCB状态标志掩码
);
函数OS_EventTaskRdy()将在任务调用函数OSXXXPost()发送一个事件时,被函数OSXXX Post()所调用。
④使一个等待超时的任务进入就绪状态的函数:
如果一个正在等待事件的任务已经超过了等待的时间,却仍由于没有获取事件等原因而未具备可以运行的条件,却又要使它进入就绪状态,这时要调用函数OS_EventTO()。
函数OS_EventTO()的原型如下:
void OS_EventTO (OS_EVENT *pevent); //参数为事件控制块的指针
函数OS_EventTO()将在任务调用OSXXXPend()请求一个事件时,被OSXXXPend()所调用。
4)空事件控制块链表:
与管理任务控制块的方法类似,uC/OS-II把事件控制块也组织为两个链表来管理。
在uC/OS-II初始化时,系统会在初始化函数OSInit()中按应用程序使用事件的总数OS_MAX _EVENTS(在文件OS_CFG.H中定义),创建OS_MAX_EVENTS个空事件控制块并借用成员OSEventPtr作为链接指针,把这些空事件控制块链接成一个单向链表。由于链表中的所有控制块尚未与具体事件相关联,因此该链表叫做空事件控制块链表。以后,每当应用程序创建一个事件,系统就会从链表中取出一个空事件控制块,并对它进行初始化以描述该事件。而当应用程序删除一个事件时,就会将该事件的控制块归还给空事件控制块链表。
2.信号量及其操作:
当事件控制块成员OSEventType的值被设置为OS_EVENT_TYPE_SEM时,这个事件控制块描述的就是一个信号量。信号量由信号量计数器和任务等待表两部分组成。
信号量使用事件控制块的成员OSEventCnt作为计数器,而用OSEventTbl[]数组来充当等待任务表。
每当有任务申请信号量时,如果信号量计数器OSEventCnt的值大于0,则把OSEventCnt减1并使任务继续运行;如果OSEventCnt的值为0,则会将任务列入等待表OSEventTbl[],而使任务处于等待状态。如果有正在使用信号量的任务释放了该信号量,则会在任务等待表中找出优先级最高的等待任务,并在使它就绪后调用调度器引发一次调度;如果任务等待表中已经没有等待任务,则信号量计数器就只简单地加1。信号量不使用事件控制块的成员OSEventPtr。
1)创建信号量:
在使用信号量之前,应用程序必须调用函数OSSemCreate()来创建一个信号量。函数的原型如下:
OS_EVENT *OSSemCreate(INT16U cnt); //参数为信号计数器初值
函数的返回值为已创建的信号值的指针。OSSemCreate()的源代码如下:
OS_EVENT *OSSemCreate(INT16U cnt)
{
#if OS_CRITICAL_METHOD= =3
OS_CPU_SR cpu_sr;
#endif
OS_EVENT *pevent;
if (OSIntNesting>0)
{
return ((OS_EVENT *)0);
}
OS_ENTER_CRITICAL();
pevent=OSEventFreeList;
if (OSEventFreeList!=(OS_EVENT *)0)
{
OSEventFreeList=(OS_EVENT *)OSEventFreeList->OSEventPtr;
}
OS_EXIT_CRITICAL();
if (pevent!=(OS_EVENT *)0)
{
pevent->OSEventType=OS_EVENT_TYPE_SEM;
pevent->OSEventCnt=cnt;
pevent->OSEventPtr=(void *)0;
OS_EventWaitListInit (pevent);
}
return(pevent);
}
2)请求信号量:
任务通过调用函数OSSemPend()请求信号量,其原型如下:
void OSSemPend (
OS_EVENT *pevent, //信号量的指针
INT16U timeout, //等待时限
INT8U *err //错误信息
);
参数pevent是被请求信号量的指针。
为防止任务因得不到信号量而处于长期的等待状态,函数OSSemPend()允许用参数timeout设置一个等待时间的限制。当任务等待的时间超过timeout时,可以结束等待状态而进入就绪状态。如果参数timeout被设置为0,则表明任务的等待时间为无限长。
函数调用成功后,err的值为OS_NO_ERR。如果函数调用失败,则函数会根据在函数中出现的具体错误,令err的值分别为OS_ERR_PEND_ISR、OS_ERR_PEVENT_NULL、OS_ERR_ EVENT_TYPE和OS_TIMEOUT。
当任务需要访问一个共享资源时,先要请求管理该资源的信号量,这样就可以根据信号量当前是否有效(即信号量的计数器OSEventCnt的值是否大于0)来决定该任务是否可以继续运行。
如果信号量有效(即信号量的计数器OSEventCnt的值大于0),则把信号量计数器减1,然后继续运行任务。如果信号量无效(即信号量的计数器OSEventCnt的值等于0),则会在等待任务表中把该任务对应的位置1而让任务处于等待状态,并把等待时限timeout保存在任务控制块TCB的成员OSTCBDly中。
当一个任务请求信号量时,如果希望在信号量无效时准许任务不进入等待状态而继续运行,则不调用函数OSSemPend(),而是调用函数OSSemAccept()来请求信号量。该函数的原型如下:
INT16U OSSemAccept(OS_EVENT * pevent); //参数为信号量的指针
调用函数成功后,函数返回值为OS_NO_ERR。
3)发送信号量:
任务获得信号量,并在访问共享资源结束以后,必须释放信号量。释放信号量也叫做发送信号量,发送信号量须调用函数OSSemPost()。函数OSSemPost()在对信号量的计数器操作之前,首先要检查是否还有等待该信号量的任务,如果没有就把信号量的计数器OSEventCnt加1,如果有则调用调度器OS_Sched()去运行等待任务中优先级最高的任务。
函数OSSemPost()的原型如下:
INT8U OSSemPost(OS_EVENT * pevent); //参数为信号量的指针
若函数调用成功,返回值为OS_NO_ERR,否则会根据具体错误返回OS_ERR_EVENT_ TYPE、OS_SEM_OVF。
4)删除信号量:
如果应用程序不需要某个信号量,那么可以调用函数OSSemDel()来删除信号量。函数的原型如下:
OS_EVENT OSSemDel(
OS_EVENT *pevent, //信号量的指针
INT8U opt, //删除条件选项
INT8U *err //错误信息
);
函数中的参数opt用来指明信号量的删除条件。该参数有两个参数值可以选择:如果选择常数OS_DEL_NO_PEND,则当等待任务表中已没有等待任务时才删除信号量;如果选择常数OS_DEL_ALLWAYS,则表明在等待任务表中无论是否有等待任务都立即删除信号量。
函数调用成功后,err的值为OS_NO_ERR。
需要注意的是,只能在任务中删除信号量,而不能在中断服务程序中删除。
5)查询信号量的状态:
任务可以调用函数OSSemQuery()随时查询信号量的当前状态,函数的原型如下:
INT8U OSSemQuery(
OS_EVENT *pevent, //信号量的指针
OS_SEM_DATA *pdata //存储信号量状态的结构
);
该函数的第二个参数pdata一个OS_SEM_DATA结构的指针。OS_SEM_DATA结构如下:
typedef struct {
INT16U OSCnt;
INT8U OSEventTbl[OS_EVENT_TBL_SIZE];
INT8U OSEventGrp;
}OS_SEM_DATA;
任务调用函数OSSemQuery()对信号量查询后,会把信号量中的相关信息存储到OS_SEM_DAT类型的变量中,因此在调用函数OSSemQuery()之前,须定义一个OS_SEM_DATA结构类型的变量。
函数调用成功后,返回值为OS_NO_ERR。
3.任务优先级反转和互斥信号量:
互斥型信号量是一个二值信号,它可使任务以独占方式使用共享资源。互斥型信号量会出现任务优先级反转问题。
1)任务优先级的反转:
在可剥夺型内核中,当任务以独占方式使用共享资源时,会出现低优先级任务先于高优先级任务而被运行的现象,被称为优先级反转。在一般情况下是不允许出现这种任务优先级反转现象的。
之所以出现优先级反转现象,是因为一个优先级较低的任务在获得了信号量使用共享资源期间被具有较高优先级的任务所打断而不能释放信号量,从而使正在等待这个信号量的更高优先级的任务因得不到信号量而被迫处于等待状态。在这个等待期间,就让优先级低于它而高于占据信号量的任务的任务先运行了。
解决问题的办法之一是:使获得信号量任务的优先级在使用共享资源期间暂时提升到所有任务最高优先级的高一个级别之上,以使该任务不被其他任务所打断,从而能尽快地使用完共享资源并释放信号量;在释放了信号量之后,再恢复该任务原来的优先级。
2)互斥型信号量:
互斥型信号量是一个二值信号量,任务可用互斥型信号量来实现对共享资源的独占式处理。为了解决任务在使用独占式资源出现的优先级反转问题,互斥型信号量除了具有普通信号量的机制外,还有其他一些特性。uC/OS-II仍然用事件控制块来描述一个互斥型信号量。
在描述互斥型信号量的事件控制块中,除了成员OSEventType要赋以常数OS_EVENT_ TYPE_MUTEX以表明这是一个互斥型信号量和仍然没有使用成员OSEventPtr之外,成员OSEventCnt被分成了低8位和高8位两部分:低8位用来存放信号值(该值为0xFF时信号有效,否则无效),高8位用来存放为了避免出现优先级反转现象而要提升的优先级prio。
①创建互斥型信号量:
创建互斥型信号量需要调用函数OSMutexCreate(),函数的原型如下:
OS_EVENT *OSMutexCreate(
INT8U prio, //优先级
INT8U *err //错误信息
);
函数OSMutexCreate()从空事件控制块链表获取一个事件控制块,把成员OSEventType赋以常数OS_EVENT_TYPE_MUTEX,以表明这是一个互斥型信号量,然后再把成员OSEventCnt的高8位赋以prio(要提升的优先级),低8位赋以常数OS_MUTEX_AVAILABLE(该常数值为0xFFFF)的低8位(0xFF),以表明信号量尚未被任何任务所占用,处于有效状态。
②请求互斥型信号量:
当任务需要访问一个独占式共享资源时,就要调用函数OSMutexPend()来请求管理这个资源的互斥型信号量。如果信号量有信号(OSEventCnt的低8位为0xFF),则意味着目前尚无任务占用资源,于是任务可以继续运行并对该资源进行访问;否则就进入等待状态,直至占用这个资源的其他任务释放了该信号量。
为防止任务因得不到信号量而处于长期等待状态,函数OSMutexPend()允许用参数timeout设置一个等待时间限制。当任务等待时间超出该时间限制值时,可以结束等待状态。
函数OSMutexPend()的原型如下:
void OSMutexPend (
OS_EVENT *pevent, //互斥型信号量的指针
INT16U timeout, //等待时限
INT8U *err //错误信息
);
任务也可通过调用函数OSMutexAccept()无等待地请求一个互斥型信号量,该函数的原型为:
INT8U OSMutexAccept (
OS_EVENT *pevent, //互斥型信号量的指针
INT8U *err //错误信息
);
③发送互斥型信号量:
任务可通过调用函数OSMutexPost()发送一个互斥型信号量,函数的原型如下:
INT8U OSMutexPost(OS_EVENT * pevent); //参数为互斥型信号量的指针
④获取互斥型信号量的当前状态:
任务可通过调用函数OSMutexQuery()获取互斥型信号量的当前状态,函数的原型如下:
INT8U OSMutexQuery(
OS_EVENT *pevent, //互斥型信号量的指针
OS_MUTEX_DATA *pdata //存放互斥型信号量状态的结构
);
函数的参数pdata是OS_MUTEX_DATA结构类型的指针。函数被调用后,在pdata指向的结构中存放了互斥型信号量的相关信息。OS_MUTEX_DATA结构定义如下:
typedef struct {
INT8U OSEventTbl[OS_EVENT_TBL_SIZE];
INT8U OSEventGrp;
INT8U OSValue;
INT8U OSOwnerPrio;
INT8U OSMutexPIP;
}OS_MUTEX_DATA;
⑤删除互斥型信号量:
任务调用函数OSMutexDel()可删除一个互斥型信号量。函数的原型如下:
OS_EVENT *OSMutexDel(
OS_EVENT *pevent, //互斥型信号量的指针
INT8U opt, //删除方式选项
INT8U *err //错误信息
);
4.消息邮箱及其操作:
如果任务与任务之间要传递一个数据,那么为了适应不同数据的需要,最好在存储器中建立一个数据缓冲区,把要传递的数据放在该缓冲区中,即可实现任务间的数据通信。
如果把数据缓冲区的指针赋给一个事件控制块成员OSEventPrt,同时使事件控制块的成员OSEventType为常数OS_EVENT_TYPE_MBOX,则该事件控制块就叫做消息邮箱。消息邮箱是在两个需要通信的任务之间通过传递数据缓冲区指针的方法来通信的。
1)创建消息邮箱:
创建消息邮箱需要调用OSMboxCreate(),函数的原型如下:
OS_EVENT *OSMboxCreate(void *msg); //参数为消息指针
函数中的参数msg为消息指针,函数的返回值为消息邮箱的指针。
调用函数OSMboxCreate()须先定义msg的初始值。在一般情况下,该初始值为NULL。也可以事先定义一个邮箱,然后把这个邮箱的指针作为参数传递到函数OSMboxCreate()中,使其一开始就指向一个邮箱。
函数OSMboxCreate()的源代码如下:
OS_EVENT *OSMboxCreate(void *msg)
{
#if OS_CRITICAL_METHOD= =3
OS_CPU_SR cpu_sr;
#endif
OS_EVENT *pevent;
if (OSIntNesting>0)
{
return ((OS_EVENT *)0);
}
OS_ENTER_CRITICAL();
pevent=OSEventFreeList;
if (OSEventFreeList!=(OS_EVENT *)0)
{
OSEventFreeList=(OS_EVENT *)OSEventFreeList->OSEventPtr;
}
OS_EXIT_CRITICAL();
if (pevent!=(OS_EVENT *)0)
{
pevent->OSEventType=OS_EVENT_TYPE_MBOX;
pevent->OSEventCnt=0;
pevent->OSEventPtr=msg;
OS_EventWaitListInit (pevent);
}
return(pevent);
}
2)向消息邮箱发送消息:
任务可通过调用函数OSMboxPost()向消息邮箱发送消息,函数的原型如下:
INT8U OSMboxPost (
OS_EVENT *pevent, //消息邮箱的指针
void *msg //消息指针
);
函数中的第二个参数msg为消息缓冲区的指针,函数的返回值为错误号。
uC/OS-II又增加了一个向消息邮箱发送消息的函数OSMboxPostOpt(),这个函数可以广播的形式向事件等待任务表中的所有任务发送消息。该函数的原型如下:
INT8U OSMboxPostOpt (
OS_EVENT *pevent, //消息邮箱的指针
void *msg //消息指针
INT8U opt //广播选项
);
函数中的第三个参数opt用来说明是否把消息向所有等待任务广播。如果该值为OS_POST_ OPT_BROADCAST,则意味着把消息向所有等待任务广播;如果为OS_POST_OPT_NONE,则把消息只向优先级最高的等待任务发送。
3)请求消息邮箱:
当一个任务请求邮箱时需要调用函数OSMboxPend(),该函数的主要作用就是查看邮箱指针OSEventPtr是否为NULL。如果邮箱指针OSEventPtr不是NULL,则把邮箱中的消息指针返回给调用函数的任务,同时用OS_NO_ERR通过函数的参数err通知任务获取消息成功;如果邮箱指针OSEventPtr是NULL,则使任务进入等待状态,并引发一次任务调度。
函数OSMboxPend()的原型如下:
void *OSMboxPend (
OS_EVENT *pevent, //消息邮箱的指针
INT16U timeout, //等待时限
INT8U *err //错误信息
);
任务在请求邮箱失败时也可以不进行等待而继续运行,如果要以这种方式来请求邮箱,则任务需要调用函数OSMboxAccept(),该函数的原型为:
INT8U *OSMboxAccept (OS_EVENT *pevent); //参数为消息邮箱的指针
该函数的返回值为消息的指针。
4)查询消息邮箱的当前状态:
任务可通过调用函数OSMboxQuery()查询消息邮箱的当前状态,并把相关信息存放在一个结构OS_MBOX_DATA中。函数的原型如下:
INT8U OSMboxQuery(
OS_EVENT *pevent, //消息邮箱的指针
OS_MBOX_DATA *pdata //存放消息邮箱信息的结构
);
OS_MBOX_DATA结构定义如下:
typedef struct {
void *OSMsg;
INT8U OSEventTbl[OS_EVENT_TBL_SIZE];
INT8U OSEventGrp;
}OS_MBOX_DATA;
5)删除消息邮箱:
任务调用函数OSMboxDel()可删除一个消息邮箱。函数的原型如下:
OS_EVENT *OSMboxDel(
OS_EVENT *pevent, //消息邮箱的指针
INT8U opt, //删除方式选项
INT8U *err //错误信息
);
5.消息队列及其操作:
使用消息队列可在任务之间传递多条消息。消息队列由三部分组成:事件控制块、消息队列和消息。当把事件控制块成员OSEventType的值置为OS_EVENT_TYPE_Q时,该事件控制块描述的就是一个消息队列。
1)消息指针数组:
消息队列的数据结构相当于一个共用一个任务等待列表的消息邮箱数组,事件控制块成员OSEventPtr指向了一个叫做队列控制块(OS_Q)的结构,该结构管理一个数组MsgTbl[],该数组中的元素都是一些指向消息的指针。
消息队列的核心是消息指针数组,其中各参数的含义如下:
OSQSize 数组的长度
OSQEntries 已存放消息指针的元素数目
OSQStart 指针,指向消息指针数组的起始地址
OSQEnd 指针,指向消息指针数组结束单元的下一个单元。它使得数组构成了一个循环的缓冲区
OSQIn 指针,指向插入一条消息的位置。当它移动到与OSQEnd相等时,被调整到指向数组的起始单元
OSQOut 指针,指向被取出消息的位置。当它移动到与OSQEnd相等时,被调整到指向数组的起始单元
其中,可移动的指针为OSQIn和OSQOut,而指针OSQStart和OSQEnd只是一个标志(常指针)。当可移动的指针OSQIn和OSQOut移动到数组末尾,也就是说与OSQEnd相等时,可移动的指针将会被调整到数组的起始位置OSQStart。也就是说,从效果上来看,指针OSQStart和OSQEnd等值。于是,这个由消息指针构成的数组就头尾衔接起来,形成了一个循环队列。
向指针数组中插入消息指针的方式有两种:先进先出(FIFO)和后进先出(LIFO)方式。当采用FIFO方式时,消息队列将在指针OSQIn指向的位置插入消息指针,而把指针OSQOut指向的消息指针作为输出;当采用LIFO方式时,则只使用指针OSQOut。
2)队列控制块:
typedef struct os_q{
struct os_q *OSQPtr;
void * *OSQStart;
void * *OSQEnd;
void * *OSQIn;
void * *OSQOut;
INT16U OSQSize;
INT16U OSQEntries;
}OS_Q;
在uC/OS-II初始化时,系统将按文件OS_CFG.H中的配置常数OS_MAX_QS定义OS_MAX_ QS个队列控制块,并用队列控制块中的指针OSQPtr将所有队列控制块链接为链表。由于这时还没有使用它们,因此这个链表叫做空队列控制块链表。
每当任务创建一个消息队列时,就会在空队列控制块链表中摘取一个控制块供消息队列来使用,并令该消息队列事件控制块中的指针OSEventPtr指向这个队列控制块;而当任务释放一个消息队列时,就会将该消息队列使用的队列控制块归还空队列控制块链表。
3)消息队列的操作:
①创建消息队列:
创建一个消息队列首先需要定义一指针数组,然后把各消息数据缓冲区的首地址存入这个数组中,然后再调用函数OSQCreate()来创建消息队列。该函数的原型如下:
OS_EVENT OSQCreate(
void **start, //优先级
INT16U size //错误信息
);
函数中的参数start为存放消息缓冲区指针数组的地址;参数size为该数组的大小。函数的返回值为消息队列的指针。
函数OSQCreate()首先从空闲队列控制块链表摘取一个控制块并按参数start和size填写诸项,然后把消息队列初始化为空(即其中不包含任何消息)。
②请求消息队列:
请求消息队列的目的是为了从消息队列中获取消息。任务请求消息队列需要调用函数OSQPend()。该函数的原型如下:
void *OSQPend (
OS_EVENT *pevent, //所请求的消息队列的指针
INT16U timeout, //等待时限
INT8U *err //错误信息
);
函数的参数pevent是要访问的消息队列事件控制块的指针;参数timeout是任务等待的时限。函数的返回值为消息指针。
函数要通过访问事件控制块的成员OSEventPtr指向的队列控制块OS_Q的成员OSQEntries来判断是否有消息可用。如果有消息可用,则返回OS_Q成员OSQOut指向的消息,同时调整指针OSQOut,使之指向下一条消息并把有效消息数的变量OSQEntries减1;如果无消息可用(即OSQEntries=0),则使用调用函数OSQPend()的任务挂起,使之处于等待状态并引发一次任务调度。
如果希望任务无等待地请求一个消息队列,则需要调用函数OSQAccept(),函数的原型为:
void OSQAccept (OS_EVENT *pevent); //所请求的消息队列的指针
③向消息队列发送消息:
任务需要通过调用函数OSQPost()或OSQPostFront()来向消息队列发送消息。函数OSQPost()以FIFO的方式组织消息队列;函数OSQPostFront()以LIFO的方式组织消息队列。这两个函数的原型如下:
INT8U OSQPost (
OS_EVENT *pevent, //消息队列的指针
void *msg //消息指针
);
INT8U OSQPostFront(
OS_EVENT *pevent, //消息队列的指针
void *msg //消息指针
);
函数中的参数msg为待发送消息的指针。
如果任务希望以广播的方式通过消息队列发送信息,则需要调用函数OSQPostOpt()。该函数的原型如下:
INT8U OSQPostOpt (
OS_EVENT *pevent, //消息队列的指针
void *msg //消息指针
INT8U opt //广播选项
);
调用这个函数发送消息时,如果参数opt的值为OS_POST_OPT_BROADCAST,则凡是等待该消息队列的所有任务都会收到消息。
④清空消息队列:
任务可通过调用函数OSQFlush()来清空消息队列,函数的原型如下:
INT8U OSQFlush(OS_EVENT *pevent ); //参数为消息队列的指针
⑤删除消息队列:
任务可通过调用函数OSQDel()来删除一个已存在的消息队列。函数的原型如下:
OS_EVENT *OSQDel(OS_EVENT *pevent ); //参数为消息队列的指针
⑥查询消息队列:
任务可通过调用函数OSQQuery()来查询一个消息队列的状态,该函数的原型如下:
INT8U OSQQuery(
OS_EVENT *pevent, //消息队列的指针
OS_Q_DATA *pdata //存放状态信息的结构
);
函数中的参数pdata是OS_Q_DATA类型的指针。函数被调用后,OS_Q_DATA的结构定义如下:
typedef struct {
void *OSMsg;
INT16U OSNMsgs;
INT16U OSQSize;
INT8U OSEventTbl[OS_EVENT_TBL_SIZE];
INT8U OSEventGrp;
}OS_Q_DATA;
函数OSQQuery()的查询结果就放在以OS_Q_DATA为类型的变量中。
十一、信号量集:
在实际应用中,任务常常需要与多个事件同步,即要根据多个信号量组合作用的结果来决定任务的运行方式。uC/OS-II为了实现多个信号量组合的功能定义了一种特殊的数据结构--信号量集。
信号量集所能管理的信号量都是一些二值信号。信号量集实质上是一种可以对多个输入的逻辑信号进行基本逻辑运算的组合逻辑。
uC/OS-II中,请求信号量集的任务得以继续运行的条件与所请求的信号量之间有两种逻辑关系:与(AND或ALL)关系、或(OR或ANY)关系。在与关系下,只有当任务所请求的信号量都有效时,任务才能继续运行;而在或关系下,只要在任务所请求信号量中有信号有效,任务就会继续运行。
1.信号量集的结构:
uC/OS-II的信号量集由两部分组成:第一部分叫做标志组,其中存放了信号量集中的所有信号;第二部分叫做等待任务链表,链表中的每一个节点都对应一个正在等待信号量集的等待任务,信号量集根据这个链表来管理等待任务。
1)信号量集的标志组:
不同于信号量、消息邮箱、消息队列等事件,uC/OS-II不使用事件控制块来描述信号量集,而使用了一个叫做标志组的结构OS_FLAG_GRP。OS_FLAG_GRP结构如下:
typedef struct {
INT8U OSFlagType; //识别是否为信号量集的标志
void * OSFlagWaitList; //指向等待任务链表的指针
OS_FLAG OSFlagFlags; //所有信号列表
}OS_FLAG_GRP;
结构中,成员OSFlagType是信号量集的标识,该成员变量的值应固定为OS_EVENT_TYPE _FLAG。成员OSFlagFlags是OS_FLAGS类型的变量,该变量用来存放信号量集所有信号的状态,每一个信号占据一个二进制位。信号量集中可以存放多少个信号,取决于OSFlagFlags的长度。这个长度可根据应用程序需要信号的书目定义为8位、16位或32位(在文件OS_CFG.H中来定义,目前系统定义的是16位)。把OS_FLAGS定义为INT8U类型时,信号量集最多可描述8个事件的状态。成员OSFlagWaitList是一个指针,当一个信号量集被创建后,这个指针指向了这个信号量集的等待任务链表。
在uC/OS-II初始化时,系统会根据在文件OS_CFG.H中定义的常数OS_MAX_FLAGS来创建OS_MAX_FLAGS个标志组,并借用成员OSFlagWaitList作为指针把这些标志组链接成一个单向链表。由于这个链表中的各标志组还未被真正创建,因此这个链表叫做空标志组链表。
空标志组链表的头指针存放在系统全局变量OSFlagList中,每当应用程序创建一个信号量集时,就从这个链表中取一个标志组,并移动头指针OSFlagList,使之指向下一个空标志组。
2)等待任务链表:
信号量集用一个双向链表来组织等待任务,每一个等待任务都是该链表中的一个节点(Node)。标志组OS_FLAG_GRP的成员OSFlagWaitList就指向了信号量集的等待任务链表。
等待任务链表节点OS_FLAG_NODE的结构如下:
typedef struct {
void * OSFlagNodeNext; //指向下一个节点的指针
void * OSFlagNodePrev; //指向前一个节点的指针
void * OSFlagNodeTCB; //指向对应任务的任务控制块的指针
void * OSFlagNodeFlagGrp; //反向指向信号量集的指针
OS_FLAGS OSFlagNodeFlags; //信号过滤器
INT8U OSFlagNodeWaitType; //定义逻辑运算关系的数据
}OS_FLAG_NODE;
其中成员OSFlagNodeFlagGrp是一个反向指向信号量集标志组的指针,是在等待任务链表中删除一个节点或添加一个节点时用到的指针。节点成员OSFlagNodeTCB是指向等待任务TCB(任务控制块)的指针,信号量集的等待任务链表通过这个指针把链表节点与等待任务关联了起来。
成员OSFlagNodeFlags相当于一个过滤器,利用它可在标志组成员OSFlagNode Flags的信号中只把请求任务需要的信号筛选出来,而把其余信号屏蔽掉。也就是说,一个请求信号量集的任务可以需要信号量集的所有信号,也可以只需要其中的部分信号,要通过在成员OSFlagNodeFlags中与所等待信号对应的二进制位进行置1来指定,而任务不需要信号的位置0.简单地说,OSFlagNodeFlags就是一个屏蔽字,通过它可获得信号量集信号的子集。
等待的任务,只有在所需的信号有效且满足指定的逻辑关系时,才能由等待状态进入就绪状态,这个逻辑关系可通过给结构OS_FLAG_NODE中的成员OSFlagNodeWaitType赋值的方法来指定,这个值有表中所列举的4个常数之一。
常数 |
信号有效状态 |
等待任务的就绪条件 |
OS_FLAG_WAIT_CLR_ALL或 |
0 |
信号全部有效(全0) |
OS_FLAG_WAIT_CLR_ANY或 |
0 |
信号有一个或一个以上的有效(有0) |
OS_FLAG_WAIT_SET_ALL或 |
1 |
信号全部有效(全1) |
OS_FLAG_WAIT_SET_ANY或 |
1 |
信号有一个或一个以上有效(有1) |
信号量集用OSFlagFlags来记录信号,用OSFlagNodeFlags来筛选信号,用OSFlagNode WaitType来控制信号的有效状态和和信号量集有效之间的逻辑关系。
把等待任务链表的节点连接起来就形成了等待任务链表。在等待任务链表的基础上,再加上标志组和各节点对应的任务控制块就形成了整个信号量集了。
3)对等待任务链表的操作:
uC/OS-II定义了两个对等待任务链表的基本操作:添加节点和删除节点,以供对信号量集操作的函数调用。
①添加节点:
给等待任务链表添加节点的函数为OS_FlagBlock(),该函数的原型如下:
static void OS_FlagBlock (
OS_FLAG_GRP *pgrp, //信号量集指针
OS_FLAG_NODE *pnode //待添加的等待任务节点指针
OS_FLAGS flags, //指定等待信号的数据
INT8U wait_type, //信号与等待任务之间的逻辑
INT16U timeout //等待时限
);
这个函数将在请求信号量集函数OSFlagPend()中被调用。
②删除节点:
从等待任务链表中删除一个节点的函数为OS_FlagUnlink(),该函数的原型如下:
void OS_FlagUnlink (OS_FLAG_NODE *pnode);
这个函数将在发送信号量集函数OSFlagPost()中被调用。
2.信号量集的操作:
1)创建信号量集:
任务可通过调用OSFlagCreate()来创建一个信号量集,该函数的原型如下:
OS_FLAG_GRP *OSFlagCreate(
OS_FLAGS flags, //信号的初始值
INT8U *err //错误信息
);
函数OSFlagCreate()的源代码如下:
OS_FLAG_GRP *OSFlagCreate(OS_FLAGS flags, INT8U *err)
{
#if OS_CRITICAL_METHOD= =3
OS_CPU_SR cpu_sr;
#endif
OS_FLAG_GRP *pgrp;
if (OSIntNesting>0)
{
*err=OS_ERR_CREATE_ISR;
return ((OS_FLAG_GRP *)0);
}
OS_ENTER_CRITICAL();
pgrp=OSFlagFreeList;
if (pgrp!=(OS_FLAG_GRP *)0
{
OSFlagFreeList=(OS_FLAG_GRP *)OSFlagFreeList->OSFlagWaitList;
pgrp->OSFlagType=OS_EVENT_TYPE_FLAG;
pgrp->OSFlagFlags=flags;
pgrp->OSFlagWaitList=(void *)0;
OS_EXIT_CRITICAL();
*err=OS_NO_ERR;
}
else
{
OS_EXIT_CRITICAL();
*err=OS_FLAG_GRP_DEPLETED;
}
return(pgrp);
}
从函数的源代码中可知,创建信号量集的函数主要做了两项工作:一是从空标志组链表中取下一个标志组,并同时给成员OSFlagType和OSFlagFlags赋初值;二是令指向等待任务链表的指针OSFlagWaitList为空指针。在实际应用中,OSFlagFlags可以根据需要赋值。
创建一个信号量集分为两个步骤:首先要定义一个全局的OS_FLAG_CRP类型的指针,然后在应用程序需要创建信号量集的位置调用函数OSFlagCreate()。例如
OS_FLAG_GRP *FlagPtr;
INT8U err;
void main(void)
{
... ...
OSInit(); //初始化uC/OS-II环境
... ...
FlagPtr=OSFlagCreate((OS_FLAGS)0, &err); //创建信号量集,所有信号的初值为0
... ...
}
调用创建信号量集OSFlagCreate()成功后,该函数返回的是这个信号量集的标志组的指针,应用程序可用这个指针对信号量集进行相应的操作。
2)请求信号量集:
任务可通过调用函数OSFlagPend()请求一个信号量集,该函数的原型如下:
OS_FLAGS OSFlagPend(
OS_FLAG_GRP *pgrp, //所请求的信号量集指针
OS_FLAGS flags, //滤波器
INT8U wait_type, //逻辑运算类型
INT16U timeout, //等待时限
INT8U *err //错误信息
);
函数参数flags是用来给等待任务链表节点成员OSFlagNodeFlags赋值的;参数wait_type是前面表中列举的4个OSFlagNodeWaitType常数之一。函数调用成功后,将返回标志组成员OSFlagFlags的值。
任务也可通过调用函数OSFlagAccept()无等待地请求一个信号量集,该函数的原型如下:
OS_FLAGS OSFlagAccept(
OS_FLAG_GRP *pgrp, //所请求的信号量集指针
OS_FLAGS flags, //滤波器
INT8U wait_type, //逻辑运算类型
INT8U *err //错误信息
);
函数OSFlagAccept()的参数除了少了一个等待时限timeout之外,其余与OSFlagPend()函数的参数相同,其返回值也是标志组的成员OSFlagFlags。
3)向信号量集发送信号:
任务可通过调用函数OSFlagPost()向信号量集发送信号,该函数的原型如下:
OS_FLAGS OSFlagPost(
OS_FLAG_GRP *pgrp, //信号量集指针
OS_FLAGS flags, //选择所要发送的信号
INT8U opt, //信号有效的选项
INT8U *err //错误信息
);
所谓任务向信号量集发送信号,就是对信号量集标志组中的信号进行置1(置位)或清0(复位)的操作。至于对信号量集中的哪些信号进行操作,由函数中的参数flags来指定;对指定的信号是置1还是清0,由函数中的参数opt来指定(opt=OS_FLAG_SET为置1操作,opt=OS_FLAG_CLR为清0操作)。
例如,要对信号量集FlagPtr发送信号,待发送的信号为OSFlagFlags中的第0位和第3位并且要把它们置1,则调用时的代码如下:
OS_FLAGS OSFlagPost(
FlagPtr, //信号量集指针
(OS_FLAGS)9, //选择所要发送的信号
OS_FLAG_SET, //信号有效的选项
&err //错误信息
);
4)查询信号量集的状态:
调用函数OSFlagQuery()可查询一个信号量集的状态,该函数的原型如下:
OS_FLAGS OSFlagQuery(
OS_FLAG_GRP *pgrp, //待查询的信号量集指针
INT8U *err //错误信息
);
函数的返回值为被查询信号量集标志组的成员OSFlagFlags,应用程序可用它来完成一些更为复杂的控制。
5)删除信号量集:
通过调用函数OSFlagDel()可删除一个信号量集,该函数的原型如下:
OS_FLAG_GRP *OSFlagDel(
OS_FLAG_GRP *pgrp, //待删除的信号量集指针
INT8U *err //错误信息
);
十二、uC/OS-II的内存管理:
应用程序在运行中为了某种特殊需要,经常需要临时获得一些内存空间,因此作为一个比较完善的操作系统,必须具有动态分配内存的能力。
能否合理、有效地对内存储器进行分配和管理,是衡量一个操作系统品质的指标之一。特别是对于实时操作系统来说,还应保证系统在动态分配内存时,它的执行时间必须是可确定的。
1.内存控制块:
uC/OS-II对内存进行两级管理:把一片连续的内存空间分成了若干个分区;每个分区又分成了若干个大小相等的内存块。分区是操作系统的管理单位,而内存块是分配单位。内存分区及内存块的使用情况则由一个叫做内存控制块的表来记录。
1)内存的划分:
如果应用程序要使用动态内存,则首先要在内存中划分出可进行动态分配的区域,这个划分出来的区域叫做内存分区,每个分区又要分为若干个叫做内存块的小区。uC/OS-II规定,同处一个分区的内存块的大小必须相等,而且同一分区内存块的数据类型必须相同。
划分一个内存分区与内存块的方法非常简单,只要定义一个二维数组即可,其中每个一维数组就是一个内存块。例如,定义一个用来存储INT16U类型的数据,有10个内存块,每个内存块长度为10的内存分区的代码如下:
INT16U IntMemBuf[10][10];
上面这个定义只是在内存中划分出了分区及内存块的区域,还不是一个真正的可以动态分配的内存区。只有当把内存控制块与分区关联起来之后,系统才能对其进行相应的管理和控制,它才能是一个真正的动态内存区。
2)内存控制块OS_MEM的结构:
为了使系统能够感知和有效地管理内存分区,uC/OS-II给每个内存分区定义了一个叫做内存控制块OS_MEM的数据结构。系统就用这个内存控制块来记录和跟踪每个内存分区的状态。内存控制块的结构如下:
typedef struct {
void * OSMemAddr; //内存分区的指针
void * OSMemFreeList; //内存控制块链表的指针
INT32U OSMemBlkSize; //内存块的长度
INT32U OSMemNBlks; //分区内内存块的数目
INT32U OSMemNFree; //分区内当前可分配的内存块的数目
}OS_MEM;
当应用程序调用函数OSMemCreate()建立了一个内存分区之后,内存控制块的内存分区指针OSMemAddr指向了内存分区,内存分区中的各个内存块又组成了一个单向链表,内存控制块的链表指针OSMemFreeList就指向了这个单向链表的头。
内存控制块的其他三个变量分别记录了分区分区中内存块的长度、总数目以及现在还未被分配的内存块数目。
3)空内存控制块链表:
与uC/OS-II中的其他控制块一样,在uC/OS-II初始化时,会调用内存控制块的初始化函数OS_MemInit()定义并初始化一个空内存控制块链表。
在这个空内存控制块链表中,一共有OS_MAX_MEM_PART(在文件OS_CFG.H中定义的常数)个空内存控制块。这时内存控制块的成员OSMemFreeList暂时作为指向下一个内存控制块的指针。
由于这时链表中的所有控制块还没有对应的内存分区,所有这个链表就叫做空内存控制块链表。每当应用程序需要创建一个内存分区时,系统就会空内存控制块链表中摘取一个控制块,而把链表的头指针OSMemFreeList指向下一个空内存控制块;而当应用程序释放一个内存分区时,则会把该分区对应的内存控制块归还给空内存控制块链表。
2.内存的管理:
uC/OS-II用于动态内存管理的函数有:创建动态内存分区函数OSMemCreate()、请求获得内存块函数OSMemGet()、释放内存块函数OSMemPut()和查询动态内存分区状态函数OSMem Query()四个函数。
1)创建内存分区:
划分了要使用的分区和内存块之后,应用程序可通过调用函数OSMemCreate()来建立一个内存分区。该函数的原型如下:
OS_MEM *OSMemCreate (
void *addr, //内存分区的起始地址
INT32U nblks, //分区中内存块的数目
INT32U blksize, //每个内存块的字节数
INT8U *err //错误信息
);
函数首先对创建一个内存分区的基本条件做一系列的判断,然后才定义内存分区。如果其中有一个条件不满足,就意味着函数调用的失败,于是函数就返回一个NULL指针,并把相应的错误信息传递到err中。
创建一个分区,必须满足两个条件:一是每个分区至少有两块内存块;二是内存块的空间至少能存放一个指针,这是因为要在内存块中建立一个用于把分区内的内存块链接为一个链表指针。
接下来,函数主要做了三项工作:首先自空内存控制块链表取一个控制块;然后把分区内的内存块链接成链表建立内存分区;最后再把刚建立的内存分区的相关信息添入内存控制块,并返回与这个刚建立的内存分区相关联的内存控制块的指针。
2)请求获得一个内存块:
在应用程序需要一个内存块时,应用程序可通过调用函数OSMemGet()向某内存分区请求获得一个内存块。该函数的原型如下:
void *OSMemGet (
OS_MEM *pmem, //内存分区的指针
INT8U *err //错误信息
);
函数OSMemGet()中的参数pmem是应用程序希望获得的内存块所在内存分区对应的内存控制块的指针。函数调用成功后,其返回值为所请求的内存块的指针。
函数OSMemGet()在判断了应用程序传递来的内存控制块的指针为非NULL及内存分区尚存在未被分配的内存块后,就将内存块链表的第一个块的指针OSMemFreeList赋给了指针pblk;然后就重新调整内存块链表,并使指针OSMemFreeList指向新的链表头;最后返回指针pblk。
当函数调用失败时,函数返回空指针NULL。
需要注意,应用程序在调用函数OSMemGet()时,事先应知道该分区中内存块的大小,并且在使用该内存时不能超其长度,否则会引起灾难性的后果。
3)释放一个内存块:
当应用程序不再使用一个内存块时,必须及时地将它释放。应用程序通过调用函数OSMemPut()来释放一个内存块。该函数的原型如下:
INT8U OSMemPut (
OS_MEM *pmem, //内存块所属内存分区的指针
void *pblk //待释放内存块的指针
);
如果函数调用成功,将返回信息OS_NO_ERR;否则将根据具体错误返回OS_MEM_ INVALID_PMEM(控制块指针为空指针)、OS_INVALID_PBLK(释放内存块指针为空指针)和OS_MEM_FULL(分区已满)。
需要注意,在调用函数OSMemPut()的一个内存块时,一定要确保把该内存块释放到它原来所属的内存分区中,否则会引起灾难性的后果。
4)查询一个内存分区的状态:
应用程序可通过调用函数OSMemQuery()来查询一个分区目前的状态信息。该函数的原型如下:
INT8U OSMemQuery(
OS_MEM *pmem, //待查询的内存控制块的指针
OS_MEM_DATA *pdata //存放分区状态信息的结构的指针
);
其中参数pdata是一个OS_MEM_DATA类型的结构。该结构的定义如下:
typedef struct {
void * OSAddr; //内存分区的指针
void * OSFreeList; //分区内内存控制块链表的头指针
INT32U OSBlkSize; //内存块的长度
INT32U OSNBlks; //分区内内存块的数目
INT32U OSNFree; //分区内当前可分配的内存块的数目
INT32U OSNUsed; //分区内当前可分配的内存块的数目
}OS_MEM_DATA;
调用函数OSMemQuery()后,内存分区的状态信息就存放在这个结构中。调用函数成功后,返回常数OS_NO_ERR。