uC/OS-II简介
uC/OS是一种公开源代码、结构小巧、具有可剥夺实时内核的实时操作系统。用户只要有标准的ANSI的C交叉编译器,有汇编器、连接器等软件工具,就可以将uC/OS嵌入到开发的产品中。uC/OS-II具有执行效率高、占用空间小、实时性能优良和可扩展性强等优点,最小内核可编译至2KB。uC/OS-II已移植到几乎所有知名的CPU上。
严格地说,uC/OS-II只是一个实时操作系统内核,它仅仅包含了任务调度,任务管理,时间管理,内存管理和任务间的通信和同步等基本功能。没有提供输入输出管理,文件系统,网络等额外的服务。但由于uC/OS-II良好的可扩展性和源码开放,这些非必须的功能完全可以由用户根据自己需要分别实现
uC/OS-II工作原理
uC/OS-II是一种基于优先级的可抢先的硬实时内核。
要实现多任务机制,那么目标CPU必须具备一种在运行期更改PC的途径,否则无法做到切换。不幸的是,直接设置PC指针,目前还没有哪个CPU支持这样的指令。但是一般CPU都允许通过类似JMP,CALL这样的指令来间接的修改PC。我们的多任务机制的实现也正是基于这个出发点。事实上,我们使用CALL指令或者软中断指令来修改PC,主要是软中断。但在一些CPU上,并不存在软中断这样的概念,所以,我们在那些CPU上,使用几条PUSH指令加上一条CALL指令来模拟一次软中断的发生。
在uC/OS-II里,每个任务都有一个任务控制块(Task Control Block),这是一个比较复杂的数据结构。在任务控制快的偏移为0的地方,存储着一个指针,它记录了所属任务的专用堆栈地址。事实上,再uC/OS-II内,每个任务都有自己的专用堆栈,彼此之间不能侵犯。这点要求程序员再他们的程序中保证。一般的做法是把他们申明成静态数组。而且要申明成OS_STK类型。当任务有了自己的堆栈,那么就可以将每一个任务堆栈再那里记录到前面谈到的任务控制快偏移为0的地方。以后每当发生任务切换,系统必然会先进入一个中断,这一般是通过软中断或者时钟中断实现。然后系统会先把当前任务的堆栈地址保存起来,仅接着恢复要切换的任务的堆栈地址。由于哪个任务的堆栈里一定也存的是地址(还记得我们前面说过的,每当发生任务切换,系统必然会先进入一个中断,而一旦中断CPU就会把地址压入堆栈),这样,就达到了修改PC为下一个任务的地址的目的。
uC/OS-II的任务管理
1、单道程序设计和多道程序设计:嵌入式操作系统可以分为单道程序设计和多道程序设计两种类型。采用单道程序设计的操作系统在任何时候只能有一个程序在运行。采用多道程序设计的操作系统允许多个程序同时存在并运行,采用多道程序设计技术可以有效提高系统资源利用率。
2、进程、线程和任务
进程:进程是在描述多道系统中并发活动过程引入的一个概念。进程和程序是两个既有联系又有区别的概念,两者不能混为一谈。程序是静态的,而进程是动态的、变化的。进程和程序之间并不是一一对应的。一个进程在运行的时候可以启动一个或多个程序,同一个程序也可能由多个进程同时执行。程序可以以文件的形式存放在硬盘或光盘上,作为一种软件资源长期保存。而进程只是一次执行过程,它是暂时的,是动态地产生和终止的。
一个进程通常包含以下几个方面的内容:
相应的程序:进程是一个正在运行的程序,有相应程序的代码和数据。
CPU 上下文:程序在运行时,CPU中含有PC、PSW、通用寄存器、段寄存器、栈指针寄存器等各种寄存器的当前值内容,例如:在PC中记录的将要取出的指令的地址,在PSW中用于记录处理器的运行状态信息,通用寄存器存放的数据或地址;段寄存器存放的程序中各个段的地址;栈指针寄存器记录的栈顶的当前位置。
一组系统资源:包括操作系统用来管理进程的数据结构、进程的内存地址空间、进程正在使用的文件等。
线程:线程是一个比进程更小的能独立运行的基本单位。所谓的线程,就是进程当中的一条执行流程。
从资源组合的角度来看,进程把一组相关的资源组合起来,构成了一个资源平台,其中包括运行上下文、内存地址空间、打开的文件等。从程序运行的角度来看,进程就是一个正在运行的程序。可以把进程看成是程序代码在这个资源平台上的一条执行流程(线程),也就是可以认为进程等于线程加上资源平台。
在一个进程当中,或者说在一个资源平台上,可以同时存在多个线程。可以用线程来作为CPU的基本调度单位,使得各个线程之间可以并发执行。对于同一个进程当中的各个线程来说,运行在相同的资源平台上,可以共享该进程的大部分资源(如内存地址空间、代码、数据、文件等),但也有一小部分资源是不能共享的,每个线程都必须拥有各自独立的一份,如CPU运行上下文和栈。
任务:在一些嵌入式系统中,把能够独立运行的实体称为“任务”,并没有使用“进程”或“线程”这两个概念。任务到底是进程还是线程,在研究一个具体的嵌入式操作系统的时候,要加以区分。
在任务的创建过程需要定义的主要参数有任务的优先级、栈空间的大小和函数名。任务具有独立的优先级和栈空间,CPU上下文一般也是存放在栈空间中。对于不同的任务,它们也能够访问相同的全局变量,在这些任务之间,可以很方便地、直接地去使用共享的内存,而不需要经过系统内核来进行通信。
通常认为,在嵌入式操作系统中“任务”就是线程,如在 linux、uC/OS等嵌入式操作系统中。
3、任务的状态及其转换
睡眠状态:人物在没有被配备任务控制块或被剥夺了任务控制块时的状态。任务驻留在内存中,但还没交给内核管理。通过调用任务创建函数把任务交给内核。就绪状态:系统为任务配备了任务控制块且在任务就绪表中进行了就绪登记,这时任务的状态。任务已经准备好,且可运行,但由于优先级比正在运行的任务的优先级低,所以还暂时不能运行。
运行状态:处于就绪状态的任务如果经调度器判断获得了CPU 的使用权,则任务就进入运行状态。得到了CPU 的控制权正在运行之中的任务状态。中断服务状态:一个正在运行的任务一旦响应中断申请就会中止运行而去执行中断服务程序,这时任务的状态。发生中断时CPU 提供相应的中断服务,原来正在运行的任务暂时停止运行,进入了被中断状态。
等待状态:正在运行的任务,需要等待一段时间或需要等待一事件发生在运行时,该任务就会把CPU 的使用权让给别的任务而使任务进入等待状态。正在运行的任务由于调用延时函数OSTimeDly(),或等待事件信号量而将自身挂起。
4、任务控制块
任务控制块——任务在系统中的身份证(系统中的所有资源都应该有身份证),一个任务的任务控制块的主要作用就是保存该任务的虚拟处理器的堆栈指针寄存器SP。其实,随着任务管理工作的复杂性的提高,它还应该保存一些其他信息。由于系统存在着多个任务,于是系统如何识别并管理一个任务就是一个需要解决的问题。识
别一个任务的最直接的办法是为每一个任务起一个名称。由于 uC/OS-II 中的任务都有一个唯一的优先级别,因此uC/OS-II 是用任务的优先级来作为任务的标识。所以,任务控制块还要来保存该任务的优先级别。另外,前面也谈到,一个任务在不同的时刻还处于不同的状态,显然,记录了任务状态的数据也应该保存到任务控制块中。
基于上述原因,系统必须为每个任务创建一个保存与该任务有关的相关信息的数据结构,这个数据结构就叫做该任务的任务控制块(TCB)。
任务控制块完整的定义:
typedef struct os_tcb {
OS_STK *OSTCBStkPtr;
#if OS_TASK_CREATE_EXT_EN
void *OSTCBExtPtr;
O_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
INT16U OSTCBDly;
INT8U OSTCBStat;
INT8U OSTCBPrio;
INT8U OSTCBX;
INT8U OSTCBY;
INT8U OSTCBBitX;
INT8U OSTCBBitY;
#if OS_TASK_DEL_EN
BOOLEAN OSTCBDelReq;
#endif
} OS_TCB;
任务在内存中的结构:
多个任务靠任务控制块组成一个任务链表:
系统提供的空闲任务:
在多任务系统运行时,系统经常会在某个时间内无用户任务可运行而处于所谓的空闲状态,为了使CPU 在没有用户任务可执行的时候有事可做,uC/OS-II 提供了一个叫做空闲任务
OSTaskldle()的系统任务。
void OSTaskldle(void* pdata)
{
#if OS_CRITICAL_METHOD==0;
OS_CPU_SR cpu_sr;
#endif
pdata=pdata;//防止某些编译器报错
for(;;)
{
OS_ENTER_CRITICAL();//关闭中断
OSdleCtr++;
OS_EXIT_CRITICAL();//开放中断
}
}
uC/OS-II 规定,一个用户应用程序必须使用这个空闲任务,而且这个任务是不能用软件来删除的。
系统提供的另一个任务——统计任务
uC/OS-II 提供的另一个系统任务是统计任务OSTaskStat()。这个统计任务每秒计算一次CPU在单位时间内的时间,并把计算结果以百分比的形式存放在变量OSCPUsage 中,以便应用程序通过访问它来了解CPU 的利用率,所以这个系统任务OSTaskStat()叫做统计任务。
任务的优先权及优先级别
uC/OS-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,共OS_LOWEST_PRIO-1。
任务堆栈
保存 CPU 寄存器中的内容及存储任务私有数据的需要,每个任务都应该配有自己的堆栈,任务堆栈是任务的重要的组成部分。在应用程序定义任务堆栈的栈区非常简单,即定义一个 OS_STK 类型的一个数组并在创建一个任务时把这个数组的地址赋给该任务就可以了。
例如:
//定义堆栈的长度
#define TASK_STK_SIZE 512
//定义一个数组来作为任务堆栈
OS_STK TaskStk[TASK_STK_SIZE];/系统定义的一个数据类型OS_STK
void main(void)
{
……
OSTaskCreate(
MyTask,
&MyTaskAgu,
&MyTaskStk[MyTaskStkN-1],
20
);
……
}
在创建任务时,要传递任务的堆栈指针和任务优先级别。就是函数OSTaskCreate()的形参。使用 OSTaskCreate()创建任务时,一定要注意所使用的处理器对堆栈增长方向的支持是向上还是向下的。
任务堆栈的初始化
应用程序创建一个新任务的时候,必须把在系统启动这个任务时 CPU 各寄存器所需要的初始数据(任务指针、任务堆栈指针、程序状态字等等),事先存放在任务的堆栈中。uC/OS-II 在创建任务函数OSTaskCreate()中通过调用任务堆栈初始化函数OSTaskStklnit()来完成任务堆栈初始化工作的。
OS_STK *OSTaskStklnit(
void(*task)(void *pd),
void *pdata,
OS_STK *ptos,
INT16U opt
);
由于各种处理器的寄存器及对堆栈的操作方式不尽相同,因此该函数需要用户在进行uC/OS-II 的移植时,安所使用的处理器由用户来编写。
其实,任务堆栈的初始化就是对该任务的虚拟处理器的初始化(复位)。
任务控制块(OS_TCB)及任务控制块链表
uC/OS-II 用来记录任务的堆栈指针、任务的当前状态、任务的优先级别等一些与任务管理有关的属性的表就叫做任务控制块。任务控制块就相当于是一个任务的身份证,没有任务控制块的任务是不能被系统承认和管理
的。
任务控制块结构的主要成员
空任务控制块链表
当进行系统初始化时,初始化函数会按用户提供的任务数为系统创建具体相应数量的任务控制块并把它们链接为一个链表。由于这些任务控制块还没有对应的任务,故这个链表叫做空任务块链表,即相当于是一些空
白的身份证。
任务控制块链表和空任务控制链表
当应用程序调用函数 OSTaskCreate()创建一个任务时,这个函数会调用系统函数OSTCBlnit()来为任务控制块进行初始化。这个函数首先为被创建任务从空任务控制块链表获得一个任务控制块,然后用任务的属性对任务控制块各个成员进行赋值,最后再把这个任务控制块链入任务控制块链表的头部。
加速任务位置计算
OSTCBX,OSTCBY,OSTCBBitX,OSTCBBitY 四个变量用于加速任务进入就绪状态的过程或进入等待事件发生状态的过程。
根据任务的优先级 OSTCBPrio 计算得出,计算公式如下
OSTCBY = OSTCBPrio >> 3;
OSTCBBitY = OSMapTbl[OSTCBY];
OSTCBX = OSTCBPrio & 0x07;
OSTCBBitX = OSMapTbl[OSTCBX];
任务就绪表及任务调度
多任务操作系统的核心工作就是任务调度。
所谓调度,就是通过一个算法在多个任务确定该运行的任务,做这项工作的函数就叫做调度器。uC/OS-II 进行任务调度的思想是“近似地每时每刻总是让优先级最高的就绪任务处于运行状态”。为了保证这一点,它在系统或用户任务调用系统函数及执行中断服务程序结束时总是调用调度器,来确定应该运行的任务并运行它。uC/OS-II 进行任务调度的依据就是任务就绪表。为了能够使系统清楚地知道,系统中那些任务已经就绪,那些还没有就绪,uC/OS-II 在RAM中设立了一个记录表,系统中的每个任务都在这个表中占据一个位置,并用这个位置的状态(0 或者1)来表示任务是否处于就绪状态,这个表就叫做任务就绪状态表,简称任务就绪表。
任务就绪表
变量 OSRdyGrp 的格式及含义
任务就绪表的示意图
在程序中,可以用类似下面的代码把优先级别为 prio 的任务置为就绪状态:
OSRdyGrp | =OSMapTbl[prio>>3];
OSRdyTbl[prio>>3] | = OSMapTbl[prio&0x07];
如果要使一个优先级别为 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]]);
小结:
系统通过查找任务就绪表来获取待运行任务的优先级。
任务切换过程
如何获得待运行任务的任务控制块?
根据就绪表获取待运行任务的任务控制块指针。
任务切换宏 OS_TASK_SW()
任务切换就是中止正在运行的任务(当前任务),转而去运行另外一个任务的操作,当然这个
任务应该是就绪任务中优先级别最高的那个任务。
任务切换断点保护
调度器进行任务切换的动作
不要企图用PUSH 和POP 指令来使程序计数器PC 压栈和出栈,因为没有这样的指令。
中断动作和过程调用指令可以使 PC 压栈;
中断返回指令可以使 PC 出栈。
因此任务切换 OSCtxSw()必定是一个中断服务程序。
需要由宏 OS_TASK_SW()来引发一次中断或者一次调度来使OSCtxSw()执行任务切换工作。
对于实时系统来说,应该尽可能地实现即时调度。
用函数 OSTaskCreate()创建任务
应用程序通过调用 OSTaskCreate( ) 函数来创建一个任务,OSTaskCreate( )函数的原型如下:
INT8U OSTaskCreate (
void (*task)(void *pd),//指向任务的指针
void *pdata, //传递给任务的参数
OS_STK *ptos, //指向任务堆栈栈顶的指针
INT8U prio //任务的优先级
)
创建任务的一般方法:
一般来说,任务可以在调用函数OSStart()启动任务调度之前来创建,也可以在任务中来创建。但是,uC/OS-II 有一个规定:在调用启动任务函数OSStart()之前,必须已经创建了至少一个任务。因此,人们习惯上在调用函数OSStart()之前先创建一个任务,并赋予它最高的优先级别,从而使它成为起始任务。然后在这个起始任务中,在创建其他个任务。如果要使用系统提供的统计任务,则统计任务的初始化函数也必须在这个起始任务中来调用。
void main(void)
{ ……
OSInit( ); //对μC/OS-II 进行初始化
……
OSTaskCreate (TaskStart,……);//创建任务TaskStart
OSStart( ); //开始多任务调度
}
void TaskStart(void*pdata)
{
……//在这个位置安装并启动μC/OS-II 的时钟
OSStatInit( ); //初始化统计任务
……//在这个位置创建其他任务
for(;;)
{
起始任务 TaskStart 的代码
}
}
ARM7 中的应用例子LPC2124,创建两个任务,LED1、LED2。实现代码和仿真电路如下:
1. 对应的Proteus 仿真图:
2.实现代码
#include "config.h"
#include "stdlib.h"
#define LED1 (1<<18)
#define LED2 (1<<19)
#define TaskLED1StkSize 128
#define TaskLED2StkSize 128
#define Task0StkLengh 64
OS_STK Task0Stk [Task0StkLengh];
OS_STK TaskLED1Stk[TaskLED1StkSize];
OS_STK TaskLED2Stk[TaskLED2StkSize];
void Task0(void *pdata);
void TaskLED1(void *pdata);
void TaskLED2(void *pdata);
int main (void)
{
OSInit ();
OSTaskCreate (Task0,(void *)0, &Task0Stk[Task0StkLengh - 1], 2);
OSStart ();
return 0;
}
void Task0 (void *pdata)
{
pdata = pdata;
TargetInit ();
OSTaskCreate(TaskLED1,(void*)0,&TaskLED1Stk[TaskLED1StkSize-1],3);
OSTaskCreate(TaskLED2,(void*)0,&TaskLED2Stk[TaskLED2StkSize-1],4);
while(1)
{
OSTimeDly(10);
}
}
void TaskLED1(void *pdata)
{
pdata=pdata;
PINSEL2=PINSEL2&(~0x04);
IO1DIR=LED1;
IO1SET=LED1;
for(;;)
{
IO1CLR=LED1;
OSTimeDly(OS_TICKS_PER_SEC/100);
IO1SET=LED1;
OSTimeDly(OS_TICKS_PER_SEC/100);
}
}
void TaskLED2(void *pdata)
{
pdata=pdata;
PINSEL2=PINSEL2&(~0x08);
IO1DIR|=LED2;
IO1SET=LED2;
for(;;)
{
IO1CLR=LED2;
OSTimeDly(OS_TICKS_PER_SEC/1);
IO1SET=LED2;
OSTimeDly(OS_TICKS_PER_SEC/1);
}
}
uC/OS-II 初始化
在使用 uC/OS-II 的所有服务之前,必须要调用uC/OS-II 的初始化函数OSInit()对uC/OS-II
自身的运行环境进行初始化。
函数 OSInit()将对uC/OS-II 的所有全局变量和数据结构进行初始化,同时创建空闲任务OSTaskldle,并赋之以最低优先级别和永远的就绪状态。如果用户应用程序还要使用统计任务的话(常数OS_TASK_STAT_EN=1),则OSInit()还要以优先级别为OS_LOWEST_PRIO-1;来创建统计任务。
初始化函数 OSInit()对数据结构进行初始化时,主要要创建包括空任务控制块链表在内的5个空数据缓冲区。同时,为了可以快速地查询任务控制块链表中的各个元素,初始化函数OSInit()还要创建一个数组OSTCBPrioTbl[OS_LOWEST_PRIO+1],在这里数组中,按任务的优先级别的顺序把任务控制块的指针存放在了对应的元素中。
uC/OS-II 初始化后的数据结构
uC/OS-II 的启动:
uC/OS-II 进行任务的管理是从调用启动函数OSStart()开始的,当然其前提条件是在调用该函数之前至少创建了一个用户任务。
分析一下任务的代码:
以上就是对嵌入式实时操作系统uC/OS-II任务管理的一些学习总结。