摘抄于大学期间记录在QQ空间的一篇自学笔记,当前清理空间,本来想直接删除掉的,但是感觉有些舍不得,因此先搬移过来。
一、UC/OS_II体系结构
二、UC/OS_II中的任务
1 、任务的基本概念
在UCOS-II中,通常把一个大型任务分解成多个小任务,小任务对应的程序实体就叫做"任务"(实际上是一个线程),UCOS-II就是一个能对这些小任务的运行进行管理和调度的多任务操作系统。
从任务的存储结构来看,UCOS-II由三个组成部分:任务程序代码(任务的执行部分)、任务堆栈(用来保存任务工作环境)、任务控制块(用来保存任务属性)。
UCOS-II的任务分为用户任务(由应用程序设计者为了解决应用问题而编写)和系统任务(由系统提供为应用程序来提供某种服务)。为了管理上的方便,UCOS把每一个任务都作为一个节点,然后把它们连接成一个任务链表。目前UCOS-II最多可以对64个任务进行管理。
2 、任务的状态
嵌入式系统中只有一个CPU,因此一个具体时刻只能允许一个任务占用CPU,根据任务是否占用CPU,任务在UCOS-II中可能处于下列五种状态之一,且可以在不同状态之间的转换。
3 、用户任务代码结构
备注:main函数中首先初始化ucos-ii,在创建3个任务,每一个任务为一个函数,其格式如上:可以被中断的用户代码、关闭中断、不可中断的代码、开启中断、可以被中断的代码。
4 、两个系统任务
Ucos-ii预定义了两个为应用程序服务的系统任务:空闲任务和统计任务,其中空闲任务是每个应用程序必须使用,而统计任务是应用程序可以根据实际需要来选择使用。
1)空闲任务
系统经常会在某个时间内无用户任务可运行而处于空闲状态,为了使cpu没有用户任务执行时有事可做,特提供了一个空闲任务OSTaskIdle()的系统任务:
系统任务只有一行代码pdata=pdata;因此什么都没有做,但是软件不可栓出这个任务。
2)统计任务
Ucos-ii提供了统计任务OSTaskStart(),此任务每秒计算一次cpu在单位时间内被使用的时间,并把计算结果以百分比的形式存放在变量OSCPUUsage中,以便应用程序通过访问它来了解cpu的利用率。
如果用户程序要使用这个统计任务,者必须把定义在系统头文件OS_CFG .H中的系统配置常数OS_TASK_STAT_EN设置为1,并且必须在创建统计任务之前调用函数OSStatInit()对统计任务进行初始化。
5 、任务的优先级
Ucos-ii的每个任务都必须具有一个唯一的优先级别,把任务的优先权分为64个优先级别,每一个优先级别用一个数字来表示,数字0表示任务的优先级别最高,数字越大表示任务的优先级越低。
用户程序可以在文件OS_CFG .H中通过给表示最低优先级别的常数OS_LOWEST_PRIO赋值的方法来说明应用程序优先级的数目,该常数一旦被定义,意味着系统可以供使用的优先级为0、1、2直到OS_LOWEST_PRIO,共OS_LOWEST_PRIO+1个任务,同时系统把最低优先级OS_LOWEST_PRIO自动赋值给空闲任务,如果应用程序还使用了统计任务,者系统会把OS_LOWEST_PRIO-1自动赋值给统计任务,因此用户任务可以使用的优先级别为0、1、2到OS_LOWEST_PRIO-2。
6 、任务堆栈
为了满足任务切换和响应中断时保存cpu寄存器中的内容及存储任务私用数据的需要每个任务都应该配有自己的堆栈。
1)任务堆栈的定义
为了定义任务堆栈,在文件OS_CPU .H中专门定义了一个数据类型OS_STK:
cpp
Typedef unsigned int OS_STK;
这样在应用程序中定义任务堆栈的栈区即定义一个OS_STK类型的数组即可如下:
cpp
#define TASK_STK_SIZE 512 //定义堆栈的长度(1024字节)
#OS_STK TaskStk[TASK_STK_SIZE]; //定义数组作为任务堆栈
调用OSTaskCreate()来创建一个任务,把数组指针作为此函数参数,完成任务和任务堆栈的关联,如下:
cpp
#define MyTaskStkN 64
OS_STK MyTaskStk[MyTaskStkN];
Void main(void) {
//...............
OSTaskCreate(
yTask, //指向任务的指针
void ( * task) (void * pd)
&MyTaskAgu,//传递任务的参数void *pdata,
&MyTaskStk[MyTaskStkN-1],//任务堆栈栈顶OS_STK *ptos,
20 //指定任务优先级INT8U prio
);
//................
}
注意:堆栈增长方向随系统使用处理器不同而不同,有的处理器是增长方向向上,有的处理器增长方向向下,如图:
增长方向向下:
OSTaskCreate(MyTask,&MyTaskAgu,&MyTaskStk[MyTaskStkN-1],20);
增长方向向上:
OSTaskCreate(MyTask,&MyTaskAgu,&MyTaskStk[0],20);
2) 任务堆栈的初始化:
任务堆栈的初始化由UC/OS-II在创建任务函数OSTaskCreate()中通过调用任务堆栈初始化函数OSTaskStkInit()来完成任务堆栈初始化工作,其原型如下:
OS_STK *OSTaskStkInit( //前三个参数跟创建任务函数参数一样
void (* task)(void *pd),
void *pdato,
OS_STK *ptos,
INT16U opt
);
7 、任务控制块:
任务控制块负责把任务代码和任务堆栈进行关联,使任务控制块、任务代码、任务堆栈成为一个整体,并系统要通过这个任务控制块来感知和管理一个任务,UCOS-II把系统所有任务的控制块链接为两条链表,通过对这两条链表管理各任务控制块。
1) 任务控制块的结构:
任务控制块是一个结构类型数据,当用户调用OSTaskCreate()创建一个用户任务时,此函数会对任务控制块中所有成员赋予与该任务相关的数据,并驻留在RAM中。
任务控制块结构定义如下:
/*其中成员OSCBStat用来存放任务的当前状态,该成员变量可能值:
OS_STAT_RDY 表示任务处于就绪状态
OS_STAT_SEM 表示任务处于等待信号量状态
OS_STAT_MBOX 表示任务处于等待消息邮箱状态
OS_STAT_Q 表示任务处于等待消息队列状态
OS_STAT_SUSPEND 表示任务处于被挂起状态
OS_STAT_MUTEX 表示任务处于等待互斥信号量状态
*/
2) 任务控制块链表:
UCOS-II用两条链表来管理任务控制块,一条是空任务块链表OSTCBFreeList(其中所有任务控制块还没有分配给任务,应用程序调用OSInit()系统初始化时创建),另一条是任务块链表OSTCBList(其中所有任务控制块已经分配给任务,应用程序调用OSTaskCreate()创建任务时创建)。
系统调用函数OSInit()对系统进行初始化时,先在RAM中建立一个OS_TCB结构类型的数组OSTCBTbl[],这样每个数组元素就是一个任务控制块,然后把这些控制块连接成如下一个链表:
UC/OS-II初始化时建立的空任务链表的元素个数:
OS_MAX_TASKS(用户任务的最大数目)+OS_N_SYS_TASKS(系统任务数目,其值为1或者2,为2时表示一个空闲任务一个统计任务)
应用程序调用系统函数OSTaskCreate()或OSTaskCreateExt()创建一个任务时,系统就会将空任务控制块链表头指针OSTCBFreeList指向任务控制块分配给该任务,在给任务控制块中各成员赋值后,就按任务控制块链表的头指针OSTCBList将其加入到任务控制块链表中。如下图,创建了两个用户任务并使用了两个系统任务:
3) 任务控制块初始化:
当用户程序调用函数OSTaskCreate()创建一个任务时,这个函数会调用系统函数OSTCBInit()来为任务控制块进行初始化。这个函数首先为被创建任务从任务控制块链表获取一个任务控制块,然后用任务的属性对任务控制块各成员赋值,最后把这个任务控制块链入到任务控制块链表的头部。
初始化任务控制块函数OSTCBInit()原型:
INT8U OSTCBInit(
INT8U prio, //任务优先级,保存在OSTCBPrio中
OS_STK *ptos, //任务堆栈栈顶指针,保存在OSTCBStkPtr中
OS_STK *pbos, //任务堆栈栈底指针,保存在OSTCBStkBottom中
INT16U id, //任务的标识符,保存在OSTCBId中
INT16U stk_size, //任务堆栈的长度,保存在OSTCBStkSize中
void *pext, //任务控制块的扩展指针,保存在OSTCBExtPtr中
INT16U opt //任务控制块的选择项,保存在OSTCBOpt中
)
4) 任务控制块的删除
UC/OS-II允许用函数OSTaskDel()删除一个任务(实质上把该任务从任务控制块链表中删掉,并把它归于这个空任务控制块链表)。
Uc/os-ii还定义了一个OS_TCB * OSTCBCur,专门存放当前正在运行的任务的任务控制块指针。
8 、任务就绪表:
多任务操作系统的核心就是任务调度(调度就是通过一个算法在多个任务中来确定哪个任务来运行),这项工作的函数就叫在调度器。UCOS-II任务调度器思想是每时每刻总让优先级最高的就绪任务处于运行状态,为了保证这一点,它在系统或用户任务调用昔日函数及执行中断服务程序结束时总是调用调度器来确定应该运行的任务并运行它。
1) 任务就绪表的结构:
系统总是从处于就绪状态的任务中来选择一个任务运行,为了使系统直到任务就绪情况,UC/OS-II在RAM中设立了一个记录表,系统中的每个任务都在这个表中占据一个位置,并用这个位置的状态(1或者0)来表示任务是否处于就绪状态。
UC/OS-II用数组INT8U OSRdyTbl[]来充当这个任务就绪表,在这个任务就绪表中以任务优先级别高低顺序,为每个任务安排了一个二进制位,并规定该位值为1表示对应的任务处于就绪状态,否则非就绪状态。
由于每个任务的就绪状态只占一位,因此OSRdyTbl[]数组的一个元素可表达8个任务的就绪状态,即一个数组元素描述了8个任务的就绪状态,把这个8个任务看出一个任务组,如果OSRdyTbl[]数组有8个元素者可表示64个任务状态,UC/OS-II又定义变量INT8U OSRdyCrp使该变量的每一个位都对应OSRdyTbl[]的一个任务组,并规定如果某任务组中有任务就绪,者变量OSRdyCrp把该任务组对应的位置为1,否则为0。
例如; OSRdyGrp=11100101,那么OSRdyTbl[0]、OSRdyTbl[2]、OSRdyTbl[5]、OSRdyTbl[6]、OSRdyTbl[7]任务组中有任务就绪。
任务就绪表如下INT8U OSRdyTbl[]:
任务组如下INTU8 OSRdyGrp:
2) 任务就绪表的操作:
任务优先级、任务组和任务就绪表的关系如下图:
把优先级为prio的任务置为就绪状态:
OSRdyGrp | =OSMapTbl[prio>>3]; //提取三四五位得到是哪一个任务组
OSRdyTbl[prio>>3] | =OSMapTbl[prio&0x07];//提取任务在数组元素中对应位置
其中OSMapTbl[]是UC/OS-II为加快运算速度定义的一个数组,它各元素值为:
OSMapTbl[0]=00000001B OSMapTbl[4]=00010000B
OSMapTbl[1]=00000010B OSMapTbl[5]=00100000B
OSMapTbl[2]=00000100B OSMapTbl[6]=01000000B
OSMapTbl[3]=00001000B OSMapTbl[7]=10000000B
如果要使一个优先级为prio的任务脱离就绪状态:
if((OSRdyTbl[prio>>3]&=-OSMapTbl[prio&0x07])==0)
OSRdyGrp&=-OSMapTbl[prio>>3];
从任务就绪表中获取优先级别最高的就绪任务:
y=OSUnMapTal[OSRdyGrp]; //获取优先级别的D5、D4、D3位
x=OSUnMapTal[OSRdyTbl[y]]; //获取优先级别的D2、D1、D0位
prio=(y<<3)+x; //获得就绪任务的优先级别
或
y=OSUnMapTbl[OSRdyGrp];
prio=(INT8U)((y<<3)+OSUnMapTbl[OSRdyTbl[y]]);
其中OSUnMapTbl[]是用来提高查找速度的数组。
9、任务的调度:
任务调度器的主要工作有两项:一是在任务就绪表中查找具有最高优先级的就绪任务,二是实现任务的切换。UC/OS-II有两种调度器:一种是任务级的调度器(由OSSched()函数实现调度),另一种是中断级的调度器(由OSIntExt()来实现调度)。任务调度器把任务切换的工作分为两个步骤:第一步获得待运行任务的TCB指针,第二步是进行断点数据的切换。
1)获得待运行就绪任务控制块的指针:
因为被中止任务的任务控制块指针存放在全局变量OSTCBCur中,所以调度器这部分的工作主要是获得待运行任务的任务控制块指针。
任务调度器OSSched()源代码如下:
调度器OSSched()源码解析:
UC/OS-II允许应用程序通过调用函数OSSchedLock()和OSSchedUnlock()给调度器上锁和解锁。调度器每被上锁一次,变量OSLockNesting就加1,调度器每被解锁一次,变量OSLockNesting,就减1。调度器OSSched()在确认未被上锁并且不是中断服务程序调用调度器的情况下,首先任务就绪表中查得的最高优先级别就绪任务的优先级别OSPrioHighRdy,然后确认这个就绪任务不是当前正在运行的任务(OSPrioCur是存放正在运行任务的优先级变量)的条件下,用数组OSTCBPrioTble[OSPrioHighRdy]的值(即待运行就绪任务的任务控制块指针)赋给指针变量OSTCBHighRdy。
于是下面可以根据OSTCBHighRdy和OSTCBCur这两个指针分别指向待运行任务控制块和当前任务控制块的指针在宏OS_TASK_SW()中实施任务切换。
2)任务切换宏OS_TASK_SW():
10、任务的创建:
1)OSTaskCreate()创建任务:
OSTaskCreate()函数对待创建任务的优先级进行一系列判断,确认该优先级合法未被使用之后,调用函数OSTaskStkInit()和OSTCBInit()对任务堆栈和任务控制块进行初始化。初始化成功后把任务计数器加1,判断UC/OS-II的核是否在运行状态,如果OSRunning的值为1,者调用OSSched()进行任务调度。
2)OSTaskCreateExt()创建任务其原型:
1) 创建任务一般方法:
Void main(void)
{
......
OSInit(); //对UC/OS-II进行初始化
.......
OSTaskCreate(TaskStart,....); //创建起始任务TaskStart
OSStart(); //开启多任务调度
}
Void TaskStart(void *pdata)
{
....... //此处安装并启动UC/OS-II时钟
OSStatInit(); //初始化统计任务
........ //此处创建其他任务
for(;;)
{
起始任务TaskStart代码
}
}
注意:UC/OS-II不允许在中断服务程序中创建任务。
11、任务管理函数:
所谓挂起一个任务就是停止这个任务的运行。用户任务可通过调用系统提供的函数OSTaskSuspend()来挂起自身或者除空闲任务之外的其他任务,且只能在其他任务中通过调用恢复函数OSTaskReume()使其恢复为就绪状态。
1) 挂起任务:
INT8U OSTaskSuspend(INT8U prio);
参数prio:为带挂起任务的优先级别,若要挂起自身,参数为OS_PRIO_SELF
返回值:成功返回OS_NO_ERR,否则返回错误具体情况
功能描述:一系列判断待挂起的任务是这个函数的任务本身,如果是本身必须删除该任务在任务就绪表中的就绪标识,并在任务控制块成员OSTCBStart中做了挂起记录之后引发一次任务调度,如果带挂起任务不是调用函数的任务本身,那么只需要删除任务就绪表中被挂起任务的就绪标志,并在任务控制块成员OSTCBStart做了挂起记录即可。
2) 恢复任务:
INT8U OSTaskResume(INT8U prio);
参数prio:为带挂起任务的优先级别
返回值:成功返回OS_NO_ERR,否则返回错误具体情况
功能描述:判断任务确实是已存在的挂起任务,同时又不是一个等待任务(任务控制块成员OSTCBDly=0)时,就清除任务控制块成员OSTCBStat中的挂起记录并使任务就绪,最后调用调度器OSSched()进行任务调度。
3) 任务优先级别的修改:
INT8U OSTaskChangePrio(
INT8U oldprio, //任务现在的优先级别
INT8U newprio //需要修改的优先级别
);
返回值:成功返回OS_NO_ERR,否则返回错误具体情况
4) 任务的删除
所谓删除一个任务就是把该任务置于睡眠状态,即先把任务控制块从任务控制块链表中删除,并归还给空任务控制块链表,然后再任务就绪表中把该任务的就绪状态置0.
INT8U OSTaskDel(INT8U prio);
参数:要删除任务的优先级别,若要删除自身,参数为OS_PRIO_SELF
5) 请求删除任务函数:
通常为了防止删除占用资源的任务,UC/OS-II提供了一个信号OSTCBDelReq:请求删除任务函数OSTaskDelReq(),这样提出删除任务请求的任务和被删除任务的双方都使用该函数来访问OSTCBDelReq这个信号,从而根据这个信号的状态来决定各自的行为。
INT8U OSTaskDelReq(INT8U prio);
参数:提出删除任务请求的任务在调用此函数时参数为待删除任务的优先级,被删除任务在调用此函数时参数为OS_PRIO_SELF
功能描述:删除任务请求方调用此函数目的是查看被删除的任务控制块是否还在,如果不在认为被删除任务已经被删除,否则令被删除任务的任务控制块成员OSTCBDelReq的值为OS_TASK_DEL_REQ表示该任务在合适的时候删除自己。
例如:
被删除任务调用此函数时,判断参数为OS_PRIO_SELF时,就会返回自己任务控制块成员OSTCBDelReq的值,若该值为OS_TASK_DEL_REQ,意味有其他任务发来栓出自己的请求,那么就应该在适当的时候调用OSTaskDel(OS_PRIO_SELF)来删除自己。
例如:
6) 查询任务信息函数:
INT8U OSTaskQuery(
INT8U prio, //待查询任务的优先级别
OS_TCB *pdata //存储任务信息的结构
);
返回值:成功将返回OS_NO_ERR并把查询得到的任务信息存放在结构OS_TCB类型变量中。
12、UC/OS-II的初始化和任务启动:
1)UC/OS-II的初始化:
在使用uc/os-ii的所有服务之前,必须调用OSInit()函数对自身的运行环境进行初始化。
OSInit()对UC/OS-II的所有全局变量和数据结构进行初始化,同时创建空闲任务OSTaskIdlc或者统计任务,其对数据结构进行初始化,主要是创建包括空任务控制块链表在内的5个空数据缓冲区,同时为了快速的查询任务控制块链表中的各个元素,OSInit()还得创建数组OSTCBPrioTbl[OS_LOWEST_PRIO+1]。经过初始化后,系统数据结构如下:
2)UC/OS-II的启动:
UC/OS-II进行任务的管理是从调用启动函数OSStart()开始的,当然前提条件是调用之前至少创建了一个用户。
void OSStart (void)
{
if (O SRunning == OS_FALSE) //若OS是未处于运行状态
{
OS_SchedNew(); //查找最高优先级
OSPrioCur = OSPrioHighRdy;
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
/* Point to highest priority task ready to run */
OSTCBCur = OSTCBHighRdy;
OSStartHighRdy();
/* Execute target specific code to start task */
}
}
OSStartHighRdy()在多任务系统启动函数OSStart()中调用,实现的功能是,设置系统运行标位OSRunning=TRUE,将就绪表中最高优先级任务的栈指针加载到SP中,并强制中断返回,这样就绪的最高优先级任务就如同从中断里返回到运行态一样,使得整个系统得以运作。
三、UC/OS_II的中断和时钟:
1、UC/OS-II的中断:
UC/OS-II系统响应中断的过程是:系统接收到中断请求后,如果此时CPU处于中断允许状态,系统就会中止当前任务,按照中断向量转而运行中断服务子程序,当中断服务子程序运行结束后,系统就会根据情况返回到被中止的任务继续运行或者进行一次调度运行到另一个更高优先级的就绪任务。中断可嵌套,UCOS-II定义了一个全局变量OSIntNesting来记录中断嵌套的层数。
在编写中断服务程序时要用到两个重要的函数OSIntEnter()和OSIntExit()。一个中断服务子程序的流程图如下:
1) 进入中断服务函数OSIntEnter:
void OSIntEnter (void)
{
if (OSRunning == OS_TRUE) {
if (OSIntNesting < 255u) {
OSIntNesting++; //中断嵌套层数计数器加1
}}}
说明:此函数经常在中断服务程序被中断任务的断点数据之后,运行用户中断服务代码之前调用,统计嵌套层数,因此叫做进入中断服务函数。
2) 退出中断服务函数OSIntExit:
说明:此函数判断中断嵌套层数为0,调度器未被锁定且从任务就绪表中查找到得最高优先级就绪任务又不是被中断的任务的条件下调用OSIntCtxSw进行任务切换,否则返回被中断的服务子程序。
3) 中断级任务切换函数OSIntCtxSw:
中断级任务切换函数OSIntCtxSw()与任务级切换函数OSCtxSw()的一样,通常由汇编代码编写,如下:
OSCtxSw ;任务级切换函数
LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0] ;设置ICSR寄存器,产生pendSV异常
BX LR
OSIntCtxSw ;中断级任务调用
LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
4) 临界段:
CPU只有在中断开放期间才能响应中断请求,而在其他时间不能响应中断请求。为增加代码移植性,UCOS-II用OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()两个宏来实现中断的开放和关闭,而把与系统硬件相关的关中断和开中断指令分别封装在这两个宏中。
2、UC/OS-II的时钟:
用硬件定时器产生一个周期为ms级的周期性中断来实现系统时钟,最小的时钟单位就是两次中断之间的时间间隔,这个最小时钟单位叫做时钟节拍(Time Tick)。硬件定时器以时钟节拍为周期定时的产生中断,该中断服务程序叫做OSTickISR()如下:
Void OSTickISR(void)
{
.........; //保存CPU寄存器;
OSIntEnter(); //进入中断服务函数
If(OSIntNesting==1)
{
OSTCBCur->OSTCBStkPtr=SP; //保存堆栈指针
}
OSTimeTick(); //调用节拍处理函数
.........; //此处清除中断
.........; //此处开中断
OSIntExit(); //退出中断服务函数
.........; //恢复CPU寄存器,中断返回
}
1) 时钟节拍服务函数OSTimeTick:
时钟中断服务程序中调用了OSTimeTick(),此函数做了两件事情:一是给计数器OSTime加1,二是遍历任务控制块链表中的所有任务控制块,把各个任务控制块中用来存放任务延时时限成员变量OSTCBDly减1,并使该项为0同时又不被挂起的任务进入就绪状态(即在每个时钟节拍了解每个任务的延时状态,使其中已经到了延时时限的非挂起任务进入就绪状态)。其源码如下:
2) 时钟节拍钩子函数OSTimeTickHook:
OSTimeTick()是系统调用的函数,为了方便应用程序设计人员在系统调用的函数中插入一些自己的工作,UC/OS-II提供了时钟节拍服务函数的钩子函数OSTimeTickHook().
3) 获取和设置系统时钟:
系统定义了一个INT32U类型的全局变量OSTime来记录系统发生的时钟节拍数,OSTime在应用程序调用OSStart()时被初始化为0,以后每发生1个时钟节拍其值就被加1。应用程序调用OSTimeGet()可获取OSTime的值,调用OSTimeSet可设置OSTime
Void OSTimeGet(void); //返回值为OSTime
Void OSTimeSet(INT32U ticks); //参数为OSTime的设置值
4) 任务的延时:
为使高优先级的任务不至于独占CPU,UC/OS-II规定除了空闲任务之外的所有任务必须在任务中合适的位置调用系统提供的函数OSTimeDly(),使当前任务的运行延时一段时间并进行一次任务调度,以让出CPU使用权。
调用了函数OSTimeDly()或OSTimeDlyHMSM()的任务,当规定的延时时间期满,或有其他任务通过调用函数OSTimeDlyResume()取消延时是,它会立即进入就绪状态。