one. 在用户任务函数中,必须包含至少一次对操作系统服务函数的调用,否则比其优先级低的任务将无法得到运行机会,这是用户任务函数与普通函数的明显区别。任务函数的结构按
任务的执行方式可以分为三类:单次执行类、周期执行类和事件触发类
1.单次执行任务函数
void MyTask (void *pdata) //单次执行的任务函数
{
进行准备工作的代码;
任务实体代码;
调用任务删除函数; // 调用OSTaskDel(OS_PRIO_SELF)
}
单次执行的任务采用“创建任务函数”来启动,当该任务被另外一个任务(或主函数)创建时,就进入就绪状态,等到比它优先级高的任务都被挂起来时便获得运行权,进入运行
状态,任务完成后再自行删除,“启动任务”就是一个例子。例如:
void main (void) // 主函数
{
OSInit (); //初始化操作系统
OSTaskCreate(TaskStart,(void *)0,&TaskStartStk[TASK_STK_SIZE-1],1);// 创建启动任务
OSStart (); //启动操作系统,开始对任务进行调度管理
}
void TaskStart(void *pdata) // 启动任务
{
pdata = pdata;
系统硬件初始化; // 时钟系统、中断系统、外设等等
创建各个任务; //如键盘任务、显示任务、采样任务、数据处理任务、打印任务等等
创建各种通信工具; //如信号量、消息邮箱、消息队列等等
OSTaskDel (OS_PRIO_SELF); //删除自己
}
2, 周期性任务函数的结构
void MyTask (void *pdata) //周期性执行的任务函数
{
进行准备工作的代码;
for (;;) //无限循环,也可用 while (1)
{
任务实体代码;
调用系统延时函数; // 调用OSTimeDly( ) 或OSTimeDlyHMSM( )
}
}
通过合理设置调用OSTimeDly( ) 或OSTimeDlyHMSM( ) 时的参数值可以调整任务的执行周期,当任务执行周期远大于系统时钟节拍时,任务执行周期的相对误差比较小;当任务
执行周期只有几个时钟节拍时,相邻两次执行的间隔时间抖动不能忽视,任务的执行周期的相对误差比较大,只适用于对周期稳定性要求不高的任务(如键盘任务);当任务执行周期只有一个时钟节拍时,可将该任务的功能放到OSTimeTickHook( )(时钟节拍函数中的钩子函数)中去执行;当任务执行周期小于一个时钟节拍或者不是时钟节拍的整数倍时,将无法使用延时函数对其进行周期控制,只能采用独立于操作系统的定时中断来触发。采用独立定时器触发的任务具有很高的周期稳定性。 周期性执行的任务函数编程比较单纯,只要创建一次,就能周期运行。在实际应用中,很多任务都具有周期性,它们的任务函数都使用这种结构,如键盘扫描任务、显示刷新任务、模拟信号采样任务等等.
3.事件触发执行的任务
此类任务在创建后,虽然很快可以获得运行权,但任务实体代码的执行需要等待某种事件的发生,在相关事件发生之前,则被操作系统挂起。相关事件发生一次,该任务实体代码
就执行一次,故该类型任务称为事件触发执行的任务,其任务函数的结构如下:
void MyTask (void *pdata) //事件触发执行的任务函数
{
进行准备工作的代码;
for (;;) //无限循环,也可用 while (1)
{
调用获取事件的函数; // 如:等待信号量、等待邮箱中的消息等等。
任务实体代码;
}
}
事件触发执行的任务函数也由三部分组成:第一部分“进行准备工作的代码”和第三部分“任务实体代码”的含义与前面两种任务的含义相同,第二部分是“调用获取事件的函数”,
使用了操作系统提供的某种通信机制,等待另外一个任务(或ISR )发出的信息(如信号量或邮箱中的消息),在取得这个信息之前处于等待状态(挂起状态),当另外一个任务(或ISR )发出相关信息时(调用了操作系统提供的通信函数),操作系统就使该任务进入就绪状态,通过任务调度,任务的实体代码获得运行权,完成该任务的实际功能。
如用一个“发送”按钮启动串行口通信任务,将数据发送到上位机。在键盘任务中,按下“发送”按钮后就发出信号量。在串行口任务中,只要得到信号量就将数据发给上位机
OS_EVENT *Sem; //信号量指针
void TaskKey (void *pdata) //键盘任务函数(示意)
{
INT8U key;
for (;;) //无限循环,也可用 while (1)
{
key=keyin(); //读入按键操作信息
switch (key)
{
case KEY_SUART: //“发送”按钮
OSSemPost(Sem); //向串行口发送任务发出信号量
break;
case KEY_$$$: //其它按钮的处理代码
.
.
.
}
OSTimeDly(2); //延时
}
}
void TaskUart(void *pdata) // 串行口发送任务(示意)
{
pdata = pdata;
INT8U err;
for (;;) //无限循环
{
OSSemPend(Sem, 0, &err); //等待键盘任务发出的信号量
串行口初始化;
组织发送帧;
数据指针初始化;
发送数据;
}
}
如果在触发任务时还需要传送参数,可以采用发送信息的方法,程序如下:
- 8 -
程序清单L4-9 用消息触发任务
OS_EVENT *Mybox; // 消息邮箱
void TaskKey (void *pdata) //键盘任务函数(示意)
{
INT8U key;
INT16U baud; //波特率,由用户通过键盘选定
for (;;) //无限循环,也可用 while (1)
{
key=keyin(); //读入按键操作信息
switch (key)
{
case KEY_SUART: //“发送”按钮
OSMboxPost(Mybox,&baud) //发送消息(波特率)
break;
case KEY_$$$: //其它按钮的处理代码
.
.
.
}
OSTimeDly(2); //延时
}
}
void TaskUart(void *pdata) // 串行口发送任务(示意)
{
INT16U baud; //波特率
INT8U err;
for (;;) //无限循环
{
pdata=OSMboxPend(Mybox, 0, &err); //等待键盘任务发出的消息
baud=(INT16U)*pdata; //获取波特率
串行口初始化; // 用获取的波特率初始化串行口
组织发送帧;
数据指针初始化;
发送数据;
}
}
two.优先级的设计安排
任务的优先级资源由操作系统提供,以μ C/OS-II为例,共有64个优先级,优先级的高低按编号从0(最高)到 63(最低)排序。由于用户实际使用到的优先级总个数通常远小于
64,为节约系统资源,可以通过定义系统常量 OS_LOWEST_PRIO 的值来限制优先级编号的范围,当最低优先级为定为18(共 19个不同的优先级)时,定义如下:
#define OS_LOWEST_PRIO 18
μ C/OS-II实时操作系统总是将最低优先级OS_LOWEST_PRIO 分配给“空闲任务”,将次低优先级OS_LOWEST_PRIO-1 分配给“统计任务”。在此例中,最低优先级为定为 18,则“空闲任务”的优先级为 18,“统计任务”的优先级为 17,用户实际可使用的优先级资源为0 到16,共 17 个。
μ C/OS-II实时操作系统还保留对最高的四个优先级(0 、1 、2 、3)和 OS_LOWEST_PRIO-3 与 OS_LOWEST_PRIO-2 的使用权,以备将来操作系统升级时使用。如果用户的应用程序希望在将来升级后的操作系统下仍然可以不加修改地使用,则用户任务可以放心使用的优先级个数为OS_LOWEST_PRIO-7 。在本例中,软件优先级资源为 18-7 =11 个,即可使用的优先级为4 、5 、6 、7 、8 、9 、10 、11 、12 、13 、14 。
例如一个应用系统中安排有键盘任务、显示任务、模拟信号采集任务、数据处理任务、串行口接收任务、串行口发送任务。在这些任务中,模拟信号采集任务、串行口接收任务和
串行口发送任务均与ISR 关联,实时性要求比较高。其中,串行口接收任务是关键任务和紧迫任务,遗漏接收内容是不允许的;模拟信号采集任务是紧迫任务,但不是关键任务,遗漏一个数据还不至于发生重大问题;在串行口发送任务中,CPU 是主动方,慢一些也可以,只要将数据发出去就可以。键盘任务和显示任务是人机接口任务,实时性要求很低。数据处理任务根据其运算量来决定,运算量很大时,优先级安排最低,运算量不大时,优先级可安排得比键盘任务高一些。
根据以上分析,最低优先级OS_LOWEST_PRIO 定为18,各个任务的优先级安排如下:串行口接收任务(优先级 2 ),模拟信号采集任务(优先级 4),串行口发送任务(优先级 6 ),数据处理任务(优先级9),显示任务(优先级 12),键盘任务(优先级13)。当优先级的安排比较宽松时,以后增加新任务就比较方便,在不改变现有任务优先级的情况下,很容易根据需要找到一个合适的空闲优先级。
three.与操作系统有关的数据结构
一个任务要想在操作系统的管理下工作,必须首先被创建。在 μ C/OS-II中,任务的创建函数原形如下:
INT8U OSTaskCreate (void (*task)(void *pd), void *pdata, OS_STK *ptos, INT8U prio);
从任务的创建函数的形参表可以看出,除了任务函数代码外,还必须准备三样东西:任务参数指针、任务堆栈指针和任务优先级,这三样东西实际上与任务的三个数据结构有关:
任务参数表、任务堆栈和任务控制块。
z 任务参数表:由用户定义的参数表,可用来向任务传输原始参数(即任务函数代码中的参数void *pdata )。通常设为空表,即(void *)0 。
z 任务堆栈:其容量由用户设置,必须保证足够大。
z 任务控制块:由操作系统设置。
操作系统还控制其它数据结构,这些数据结构与一个以上的任务有关,如信号量、消息邮箱、消息队列、内存块、事件控制块等等。
操作系统控制的数据结构均为全局数据结构,用户可以对这些与操作系统有关的数据结构进行剪裁,
与操作系统无关的数据结构
每个任务都有其特定的功能,需要处理某些特定的信息,为此需要定义对应的数据结构来保存这些信息,常用的数据结构有变量、数组、结构体、字符串等。
每个信息都有其生产者(对数据结构进行写操作)和消费者(对数据结构进行读操作),一个信息至少有一个生产者和一个消费者,且都可以不止一个。
当某个信息的生产者和消费者都是同一个任务(与其它任务无关)时,保存这个信息的数据结构应该在该任务函数内部定义,成为它的私有信息,如局部变量。
当某个信息的生产者和消费者不是同一个任务(包括ISR )时,保存这个信息的数据结构应该在任务函数的外部定义,使它成为共享资源,如全局变量。对这部分数据结构的访问
需要特别小心,必须保证访问的互斥性