文章目录
一.任务的基本概念
对于一个大的任务可以分解成多个小任务去完成,并且让这些小任务同时处理,极大地提高了解决问题的效率。任务就是程序实体,UCOSII就是一个能够对这些小任务进行管理和调度,从而可以并发方式执行这些小任务的多任务操作系统。
UCOSII的任务由三部分组成:
任务控制块:用来记录任务的各个属性。
任务堆栈:上下文切换的时候用来存储任务的工作环境,也就是MCU的内部寄存器值。
任务函数:由用户编写的任务处理代码,是任务实实在在干活的部分。
具体创建一个任务的示例:
//START 任务
//设置任务优先级
#define START_TASK_PRIO 10
//设置任务堆栈大小
#define START_STK_SIZE 64
//任务堆栈
OS_STK START_TASK_STK[START_STK_SIZE];
//任务函数
void start_task(void *pdata);
OSTaskCreate(start_task,(void *)0,(OS_STK *)&START_TASK_STK[START_STK_SIZE-1],START_TASK_PRIO );//3.创建起始任务
//开始任务
void start_task(void *pdata)
{
..........................
}
二.任务的管理
UCOSII的任务有两种:用户任务和系统任务。
为了对系统中的多个任务进行管理,采用链表的形式吧所有任务控制块连接起来。这个链表也叫任务注册表,把一个任务控制块加入这个链表也叫做任务的注册。
系统任务:空闲任务和统计任务
空闲任务:系统在某段时间内无用户任务可运行从而处在所谓的空闲状态。为了使CPU有事情可做,UCOSii提供了一个叫做空闲任务OSTaskIdle()系统任务。
统计任务:OSTaskStat(),该任务每秒计算一次CPU在单位时间内被使用的时间,从而计算出CPU的利用率。如果使用统计任务需要:在OS_CFG.H中将OS_TASK_STAT_EN设置为1,并在程序中调用OSStatInit()对统计任务进行初始化。
三.任务的状态
睡眠状态:任务在没有被配备任务控制块或剥夺了任务控制块时的状态叫做任务的睡眠状态。
就绪状态:系统为任务配备了任务控制块且在任务就绪表中进行了就绪登记,任务处于就绪状态。
运行状态:任务经过调度器获得了CPU的使用权,进入运行状态,任一时刻只能有一个任务处在运行状态。
等待状态:正在运行的任务需要等待一段时间或等待一个时间后再运行(例如请求一个信号量在无限期等待)该任务把CPU使用权让给其他任务从而进入等待状态。
中断服务状态:一个正在运行的任务一旦响应中断申请就会去执行中断服务程序从而处于中断服务状态。
四.任务控制块及其链表
UCOSII用来记录任务的堆栈指针、任务的当前状态、任务的优先级等一些与任务滚哪里相关的属性的表就叫做任务控制块。任务控制块相当于任务的身份证,系统就是通过任务控制块来感知和管理任务的,没有任务控制块的任务不能被系统承认和管理。
//任务控制块初始化函数
INT8U OS_TCBInit (INT8U prio,
OS_STK *ptos,
OS_STK *pbos,
INT16U id,
INT32U stk_size,
void *pext,
INT16U opt)
{
...............................
}
//在任务创建函数中对任务控制块进行初始化
INT8U OSTaskCreate (void (*task)(void *p_arg),
void *p_arg,
OS_STK *ptos,
INT8U prio)
{
..................................
err = OS_TCBInit(prio, psp, (OS_STK *)0, 0u, 0u, (void *)0, 0u);//对任务控制块进行初始化
..................................
}
任务控制块链表:
UCOSII在任务控制块的管理上需要两条链表,一条空任务块链表(其中所有任务控制块还未分配给任务)和一条任务块链表(其中所有任务控制块已分配给任务)。具体做法是:系统在调用函数OSInit()对UCOSII系统进行初始化时,就先在RAM中建立一个OS_TCB结构类型的数组OSTCBTb1[],然后把各个元素链接成一个如下图所示的链接表,从而形成一个空任务块链表。
UCOSII允许用户使用函数OSTaskDel()来删除一个任务。删除一个任务实质上就是把该任务从任务控制链表(TCB)中删掉,并把它归还给空任务控制块链表。
五.任务就绪表及任务调度
为就绪任务分配CPU是多任务操作系统的核心工作。需要做两件事情:
1.判断哪些任务处于就绪状态
2.进行任务调度
任务调度的思想是:近似地每时每刻让优先级最高的就绪任务处于运行状态。具体做法是,它在系统或用户任务调用系统函数及执行中断服务程序结束时调用调度器,以确定应该运行的任务并运行它。任务调度器的主要工作有两项:一是在任务就绪表中查找最高优先级的任务;二是实现任务的切换,调度器又把任务切换的工作分为两个步骤:1.获得待运行任务的TCB指针;2.进行断点数据的切换
//系统任务调度函数,在其中查找处于就绪状态优先级最高的任务!!!
void OS_Sched (void)
{
#if OS_CRITICAL_METHOD == 3u /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0u;
#endif
OS_ENTER_CRITICAL();
if (OSIntNesting == 0u) { /* Schedule only if all ISRs done and ... */
if (OSLockNesting == 0u) { /* ... scheduler is not locked */
OS_SchedNew();
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];/*查得最高优先级别就绪任务的优先级别*/
if (OSPrioHighRdy != OSPrioCur) { /* 确认这个就绪任务不是当前正在运行的任务 */
#if OS_TASK_PROFILE_EN > 0u
OSTCBHighRdy->OSTCBCtxSwCtr++; /* Inc. # of context switches to this task */
#endif
OSCtxSwCtr++; /* 统计任务切换次数的计数器加1 */
OS_TASK_SW(); /* 进行一次任务切换 */
}
}
}
OS_EXIT_CRITICAL();
}
调度器OS_Sched ()在确认未被上锁并且不是中断服务程序调用调度器的情况下,首先从任务就绪表查得最高优先级别就绪任务的优先级别OSPrioHighRdy;然后在确认这个就绪任务不是当前正在运行的任务(OSPrioCur是存放正在运行任务的优先级别的变量)的条件下,用OSTCBHighRdy作为下标去访问数组OSTCBPrioTbl[]…接下来根据OSTCBHighRdy和OSTCBCur这两个指针分别指向待运行任务控制块和当前任务控制块,从而在宏OS_TASK_SW()中实现任务切换。
六.任务的创建与删除,挂起与恢复
1.任务的创建:
INT8U OSTaskCreate (void (*task)(void *p_arg),
void *p_arg,
OS_STK *ptos,
INT8U prio)
{
OS_STK *psp;
INT8U err;
#if OS_CRITICAL_METHOD == 3u /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0u;
#endif
#ifdef OS_SAFETY_CRITICAL_IEC61508
if (OSSafetyCriticalStartFlag == OS_TRUE) {
OS_SAFETY_CRITICAL_EXCEPTION();
}
#endif
#if OS_ARG_CHK_EN > 0u
if (prio > OS_LOWEST_PRIO) { /* Make sure priority is within allowable range */
return (OS_ERR_PRIO_INVALID);
}
#endif
OS_ENTER_CRITICAL();
if (OSIntNesting > 0u) { /* Make sure we don't create the task from within an ISR */
OS_EXIT_CRITICAL();
return (OS_ERR_TASK_CREATE_ISR);
}
if (OSTCBPrioTbl[prio] == (OS_TCB *)0) { /* Make sure task doesn't already exist at this priority */
OSTCBPrioTbl[prio] = OS_TCB_RESERVED;/* Reserve the priority to prevent others from doing ... */
/* ... the same thing until task is created. */
OS_EXIT_CRITICAL();
psp = OSTaskStkInit(task, p_arg, ptos, 0u); /* Initialize the task's stack */
err = OS_TCBInit(prio, psp, (OS_STK *)0, 0u, 0u, (void *)0, 0u);//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
if (err == OS_ERR_NONE) {
if (OSRunning == OS_TRUE) { /* Find highest priority task if multitasking has started */
OS_Sched();
}
} else {
OS_ENTER_CRITICAL();
OSTCBPrioTbl[prio] = (OS_TCB *)0;/* Make this priority available to others */
OS_EXIT_CRITICAL();
}
return (err);
}
OS_EXIT_CRITICAL();
return (OS_ERR_PRIO_EXIST);
}
从函数OSTaskCreate ()的源码中可以看到,函数对待创建任务的优先级别进行了一系列判断,确认该优先级合法且未被使用之后,随即调用函数OSTaskStkInit()和OSTCBInit()对任务堆栈和任务控制块进行初始化。初始化成功后,除了把任务计数器加1外,还要进一步判断UCOSII的核是否在运行状态(即OSRunning的值是否为1),如果OSRunning的值为1,则调用OS_Sched ()进行任务调度。
一般我们创建任务是先创建一个start任务,然后在start任务中创建其他任务,如下所示:
int main(void)
{
OSInit(); //进行操作系统初始化
OSTaskCreate(start_task,(void *)0,(OS_STK *)&START_TASK_STK[START_STK_SIZE-1],START_TASK_PRIO );//3.创建起始任务
OSStart(); //任务开始执行
}
//开始任务
void start_task(void *pdata)
{
OS_CPU_SR cpu_sr=0;
pdata = pdata;
OS_ENTER_CRITICAL(); //进入临界区(无法被中断打断)
OSTaskCreate(led0_task,(void *)0,(OS_STK*)&LED0_TASK_STK[LED0_STK_SIZE-1],LED0_TASK_PRIO);//在开始任务中创建LED0任务
OSTaskCreate(led1_task,(void *)0,(OS_STK*)&LED1_TASK_STK[LED1_STK_SIZE-1],LED1_TASK_PRIO); //在开始任务中创建LED1任务
OSTaskCreate(uart1_task,(void *)0,(OS_STK*)&UART1_TASK_STK[UART1_STK_SIZE-1],UART1_TASK_PRIO); //在开始任务中创建UART0任务
OSTaskSuspend(START_TASK_PRIO); //挂起起始任务.
OS_EXIT_CRITICAL(); //退出临界区(可以被中断打断)
}
2.任务的删除
所谓的任务删除就是把任务的任务控制块从任务控制块链表中删除,并归还给空任务控制块列表,然后在任务就绪表中把该任务的就绪状态位设置为0,于是该任务就不能被调度器所调用了。简单说就是把身份证注销了!
有时,任务会占用一些动态内存分配的内存或信号量之类的资源。为了避免贸然删除某个任务后可能导致发生不确定的错误,因此,在删除一个任务时一定要谨慎。具体的办法是,提出删除任务请求的任务只负责提出请求,而删除任务则有被删除任务自己完成。
删除任务请求函数原型如下:
INT8U OSTaskDelReq(
INT8U prio //待删除任务的优先级别
);
被删除任务方一定要用OS_PRIO_SELF作为参数来调用OSTaskDelReq()函数。如果调用的返回值为OS_TASK_DEL_ERQ,意味着有其他任务发出了删除任务的请求,那么被删除任务就会在适当的时候调用函数OSTaskDel(OS_PRIO_SELF)来删除自己。
典型代码如下:
if(OSTaskDelReq(OS_PRIO_SELF)==OS_ERR_TASK_DEL_REQ) //判断是否有删除请求
{
//释放资源和动态内存的代码
OSTaskDel(OS_PRIO_SELF); //删除任务本身Task
}
3.任务的挂起和恢复
任务挂起函数原型:
INT8U OSTaskSuspend(INT8U prio);
任务恢复函数原型:
INT8U OSTaskResume(INT8U prio);
4.任务优先级的修改
函数原型:
INT8U OSTaskChangePrio(
INT8U oldprio, //任务现在的优先级别
INT8U newprio, //要修改的优先级别
)
5.查询任务的信息
有时,在应用程序运行中需要了解一个任务的指针、堆栈等信息,这时可以通过函数OSTaskQuery()来获取选定的任务的信息。
原型如下:
INT8U OSTaskQuery(
INT8U prio, //待查询任务的优先级别
OS_TCB *pdata //存储任务信息的结构
)
七.UCOSII的初始化和启动
1.UCOSII的初始化
在UCOSII的所有服务之前,必须调用OSInit(),对UCOSII自身的运行环境进行初始化。
函数OSInit()将对UCOSII的所有全局变量和数据结构进行初始化,同时创建空闲任务OSTaskIdle,并赋之以最低的优先级别和永远的就绪状态。如果用户程序还要使用统计任务(常数OS_TASK_STAT_EN=1),则OSInit()还要以优先级为OS_LOWEST_PRIO-1来创建统计任务。
初始化函数OSInit()对数据结构进行初始化时,主要是创建包括空任务控制块链表在内的5个空数据缓存区。同时为了可以快速地查询任务控制块链表中的各个元素,OSInit()还要创建一个数组OSTCBTaskTb1[OS_LOWEST_PRIO+1]。在这个数组中,按任务的优先级别顺序把任务控制块的指针存放在对应的元素中。经过初始化后,系统中数据结构如下所示:
2.UCOSII的启动
UCOSII进入任务的管理是从调用启动函数OSStart()开始的。当然,其前提条件是在调用该函数之前至少创建了一个任务。
OSStart()的源码如下:
void OSStart (void)
{
if (OSRunning == OS_FALSE) {
OS_SchedNew(); /* Find highest priority's task priority number */
OSPrioCur = OSPrioHighRdy;
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; /* Point to highest priority task ready to run */
OSTCBCur = OSTCBHighRdy;
OSStartHighRdy(); /* Execute target specific code to start task */
}
}
假如在OSStart()之前已经创建了任务MyTask,如下代码所示:
#define MY_TASK_STK_SIZE 512 //定义堆栈容量
void main(void)
{
OSInit(); //初始化UCOSII
..............
OSTaskCreate(
MyTask, //创建任务MyTask
(void*)0,
&MyTaskStk[MY_TASK_STK_SIZE-1],
6 //任务优先级为6
);
OSStart(); //启动UCOSII
}
那么,在执行上述代码之后,UCOSII立即进入多任务管理阶段,这时的数据结构如下所示:
OSStartHighRdy()在多任务系统启动函数OSStart()中调用。完成的功能是:设置系统运行标志位OSRunning=TRUE,将就绪表中最高优先级任务的栈指针Load到SP中,并强制中断返回。这样就绪的最高优先级任务就如同从中断返回到运行状态一样,使得整个系统得以运转。
八.总结
1.任务由任务控制块、任务堆栈、任务代码三部分组成。系统通过任务控制块来感知和控制任务;任务堆栈注意用来保护断点和恢复断点;任务代码是一个超循环结构,它描述了任务的执行过程。在创建一个任务时,函数OSSTaskCreate()负责给任务分配任务控制块和任务堆栈,并对他们进行初始化,然后把任务控制块、任务堆栈、任务代码三者关联起来形成一个完整的任务。
2.系统是按任务就绪表和任务优先级别来调度任务的。执行任务调度工作的是调度器OS_Sched (),它负责查找具有最高优先级的就绪任务并执行它,同时把这个任务TCB的指针存放在OSTABCur中。通常在调用API函数和运行中断服务程序之后都要调用OS_Sched ()来进行一次任务调度。
3.任务切换的核心工作是任务堆栈指针的切换。
4.任务调度器OS_Sched ()代码的设计,使得它的运行时间与系统中的任务数无关,从而使它满足了实时系统的要求。
5.任务的优先级别也是任务的唯一标识。
6.应用程序首先应该调用OSInit()函数对全局变量和数据结构进行初始化。
7.应用程序是通过调用函数OSStart()开始进入多任务管理的,但在调用函数OSStart()之前必须至少建立了一个任务。
8.在中断程序运行结束后会进行一次任务调度,来选择当前就绪任务的最高优先级并执行,因此,中断执行结束返回不一定执行之前那个被打断的任务,而是看有没有更高优先级的任务就绪。若有,则执行新任务,若没有,则执行旧任务。
9.在当前任务执行完毕进入延时函数会执行一次任务调度OS_Sched ()(可以看看延时函数的代码,其中进行了OS_Sched ()),这样才能让低优先级任务获得执行机会。
10.由于是可剥夺的系统,因此高优先级任务具有绝对的话语权,只要高优先级任务就绪,可以打断低优先级任务立即执行。
11.任务切换函数OSCtxSw()
它是任务切换函数,被UCOSII任务调度器OSSched()在切换宏OS_TASK_SW()中调用,其工作就是保存被中止运行任务的断点和恢复待运行任务的断点并启动它。OSCtxSw()实现是靠中断返回指令IRET来实现的,因此OSCtxSw()应该是一个中断服务程序。