嵌入式实时操作系统内核分析.docx
- 文档编号:15644112
- 上传时间:2023-07-06
- 格式:DOCX
- 页数:23
- 大小:212.75KB
嵌入式实时操作系统内核分析.docx
《嵌入式实时操作系统内核分析.docx》由会员分享,可在线阅读,更多相关《嵌入式实时操作系统内核分析.docx(23页珍藏版)》请在冰点文库上搜索。
嵌入式实时操作系统内核分析
关于对μC/OS-Ⅱ嵌入式实时操作系统内核分析
一、μC/OS-Ⅱ概述
1、μC/OS-Ⅱ的基本概念
μC/OS是源码公开的实时嵌入式操作系统,具有源代码开放、内核小、实时性好的突出优点,能够被移植到各种微处理器和微控制器上。
我们今天所分析的μC/OS-Ⅱ实时操作系统内核正是建立在μC/OS的基础之上的。
μC/OS-Ⅱ是典型的微内核实时操作系统。
它提供了任务调度、任务管理、时间管理和任务间的通信等基本功能。
与一般通用的操作系统相比,嵌入式实时操作系统既具有通用操作系统的基本特点,又具有系统实时性、硬件的相关依赖性、软件固态化以及应用的专用性等特点。
嵌入式实时操作系统通常包括与硬件相关的底层驱动软件、系统内核、设备驱动接口、通信协议、图形界面、标准化浏览器Browser等。
评价嵌入式操作系统的重要指标有实时性、可裁剪性、可扩展性。
2、μC/OS-Ⅱ的基本性能特点
公开源代码、可移植性好、可固化、可裁剪、抢占式实时内核、多任务管理、函数调用和系统服务的执行时间是确定的、每个任务有自己单独的栈、提供多系统服务和中断请求可使正执行任务暂时挂起等多个优点。
二、任务管理
1、任务的工作状态
任务是μC/OS-Ⅱ中最重要的概念之一。
它也被称作为一个线程为一个简单的程序,该程序可以认为CPU完全只属于该程序。
每个任务都被赋予一定的优先级,有着自己的一套CPU寄存器和栈空间。
如下图:
一个任务通常是一个无限的循环。
μC/OS-Ⅱ可以管理多达64个任务,但由于其两个任务被系统占用,并保留了优先级为0、1、2、3、OS_LOWEST_PRIO-3、OS_LOWEST_PRIO-2、OS_LOWEST_PRIO-1以及OS_LOWEST_PRIO这八个任务供将来使用。
因此用户可以使用的有56个应用任务。
每个任务都有不同的优先级,一般来说,任务的优先级号越低,任务的优先级越高。
μC/OS-Ⅱ总是先运行进入就绪态的优先级最高的任务。
下图所示为任务所处的可能的5种状态。
在任一时刻,任务的状态一定是这5种状态之一。
如下图:
收到消息
等待消息
挂起
挂起时间到
任务调度
创建任务
中断
任务被占先
删除任务
中断结束
2、任务调度
任务调度是实时内核最重要的工作之一,μC/OS-Ⅱ是抢占式实时多任务内核,采用基于优先级的任务调度。
μC/OS-Ⅱ的任务调度包括任务级的任务调度和中断级的任务调度,所采用的调度算法是相同的。
任务级的调度是函数OSSched()完成的,中断级的任务调度则由函数OSIntExt()完成。
其中函数OSSched()的内容如下:
VoidOSSched(void)
{
INT8Uy;
OS_ENTER_CRITICAL();
if((OSLockNesting|OSIntNesting)==0)
{
y=OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy=(INT8U)((y<<3)+OSUnMapTbl[OSRdyTbl[y]];
if(OSPrioHighRdy!
=OSPrioCur)
{
OSTCBHighRdy=OSTCBPrioTbl[OSPrioHighRdy];
OSCtxSwCtr++;
OS_TASK_SW();
}
}
OS_EXET_CRITICAL();
}
从上面的程序中可以了解到为了避免在调度过程中被中断,在OSSched()开始时,首先调用了OS_ENTER_CRITICAL()关中断,然后判断任务调度器是否上锁或调用是否来自中断服务子程序,如果不是,则开始任务调度。
如下图所示:
为一个任务就绪表。
下图便是一个相关例子,把prio为29的任务设置在任务就绪状态:
从任务就绪表中获取优先级别最高的就绪任务可用如下类似的代码:
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]]);
3、μC/OS-Ⅱ的初始化
μC/OS-Ⅱ要求用户首先调用系统初始化函数OSIint(),对μC/OS-Ⅱ所有的变量和数据结构进行初始化,然后调用函数OSTaskCreate()或OSTaskCreateExt()建立用户任务,最后通过调用OSStart()函数启动多任务。
Voidmain(void)
{
OSInit();
.
.
通过调用函数OSTaskCreate()或OSTaskCreateExt()创建至少一个任务;
OSStart();/*开始调用调度!
OSStart()永远不会返回*/
}
当调用OSStart()函数时,OSStart()函数从任务就绪表中找出用户建立的优先级最高任务的控制块。
然后,OSStart()函数调用高优先级就绪任务启动函数OSStartHighRdy()。
这个函数的任务是把任务栈中的任务状态参数值恢复到CPU寄存器中,然后执行一条中断返回指令,中断返回指令强制执行该任务代码。
其代码如下:
voidOSStart(void)
{
INT8Uy;
INT8Ux;
if(OSRunning==FALSE)
{
y=OSUnMapTbl[OSRdyGrp];
x=OSUnMapTbl[OSRdyTbl[y]];
OSPrioHighRdy=(INT8U)((y<<3)+x);
OSPrioCur=OSPrioHighRdy;
OSTCBHighRdy=OSTCBPrioTbl[OSPrioHighRdy];
OSTCBCur=OSTCBHighRdy;
OSStartHighRdy();
}
}
4、任务的创建
应用程序通过调用OSTaskCreate()函数来创建一个任务,OSTaskCreate()函数的原型如下:
INT8UOSTaskCreate
{
void(*task)(void*pd),//指向任务的指针
void*pdata,//传递给任务的参数
OS_STK*ptos,//指向任务堆栈栈顶的指针
INT8Uprio//任务的优先级
}
INT8UOSTaskCreate(void(*task)(void*pd),void*pdata,OS_STK*ptos,INT8Uprio)
{
OS_STK*psp;//初始化任务堆栈指针变量,返回新的栈顶指针
INT8Uerr;//定义(获得并定义初始化任务控制块)是否成功
#ifOS_ARG_CHK_EN>0//所有参数必须在指定的参数内
if(prio>OS_LOWEST_PRIO)
{//检查任务优先级是否合法
return(OS_PRIO_INVALID);
}//参数指定的优先级大于OS_LOWEST_PRIO
#endif
OS_ENTER_CRITICAL();//关闭中断
if(OSTCBPrioTbl[prio]==(OS_TCB*)0)//确认优先级未被使用,即就绪态为0
{
OSTCBPrioTbl[prio]=(OS_TCB*)1;//保留这个优先级,将就绪态设为1
OS_EXIT_CRITICAL();//打开中断
psp=(OS_STK*)OSTaskStkInit(task,pdata,ptos,0);//初始化任务堆栈
err=OS_TCBInit(prio,psp,(OS_STK*)0,0,0,(void*)0,0);//获得并初始化任务控制块
if(err==OS_NO_ERR)//任务控制初始化成功
{
OS_ENTER_CRITICAL();//关闭中断
OSTaskCtr++;//任务计数器加1
OS_EXIT_CRITICAL();//打开中断
if(OSRunning==TRUE)//检查是否有(某个)任务在运行
{
OS_Sched();//任务调度,最高任务优先级运行
}
}
else//否则,任务初始化失败
{
OS_ENTER_CRITICAL();//关闭中断
OSTCBPrioTbl[prio]=(OS_TCB*)0;//放弃任务,设此任务就绪态为0
OS_EXIT_CRITICAL();//打开中断
}
return(err);//返回(获得并定义初始化任务控制块是否成功)
}
OS_EXIT_CRITICAL();//打开中断
return(OS_PRIO_EXIST);}//返回(具有该优先级的任务已经存在)
三、中断和时间管理
1、中断处理
μC/OS-II系统响应中断的过程为:
系统接收到中断请求后,这时如果CPU处于中断允许状态(即中断是开放的),系统就会中止正在运行的当前任务,而按照中断向量的指向转而去运行中断服务子程序,当中断服务子程序的运行结束后,系统将会根据情况返回到被中止的任务继续运行或者转向运行另一个具有更高优先级别的就绪任务。
中断服务子程序运行结束之后,系统将会根据情况进行一次任务调度去运行优先级别最高的就绪任务,而并不是一定要接续运行被中断的任务的。
中断嵌套层数计数器和锁定嵌套计数器OSLockNesting二者都必须是零,OSRdyTbl[]所需的检索值Y是保存在全程变量OSIntExitY中检查具有最高优先级别的就绪任务的优先级是否是正在运行的任务的优先级,将任务控制块优先级表保存到指向最高级优先级就绪任务控制块的指针,上下文切换的次数统计任务计数器。
做中断任务切换:
voidOSIntEnter(void)
{
if(OSRunning==TRUE)
{
if(OSIntNesting<255)
{
OSIntNesting++;//中断嵌套层数计数器加一
}
}
}
2、时间管理
μC/OS-II需要用户提供周期性信号源,用于实现时间延时和确认超时。
节拍率应在每秒10-100次之间。
时钟节拍率越高,系统的额外负荷就越重。
时钟节拍是一种特殊的中断,μC/OS-II中的时钟节拍服务是在通过在中断服务子程序中调用OSTimeTick()函数实现的。
时钟节拍的中断服务子程序如下所示:
VoidOSTickISR(void)
{
保存处理器寄存器的值;
调用OSIntEnter()或将OSIntNesting加1;
调用OSTimeTick();/*检查每个任务的时间延时*/
调用OSIntExit();
恢复处理器寄存器的值;
执行中断返回指令;
}
其中时钟节拍函数OSTimeTick()的主要工作是给每个任务控制块OS_TCB中的时间延时项OSTCBDly减1。
当某任务的任务模块中的延时项OSTCBDly减为0,则该任务将进入就绪任务表。
因此,OSTimeTick()的执行时间与应用程序中建立了多少任务有关。
四、任务之间的通信与同步
1、任务互斥和同步
系统中的多个任务在运行时,经常需要互相无冲突地访问同一个共享资源,或者需要互相支持和依赖,甚至有时还要互相加以必要的限制和制约,才保证任务的顺利运行。
因此,操作系统必须具有对任务的运行进行协调的能力,从而使任务之间可以无冲突、流畅地同步运行,而不致导致灾难性的后果。
与人们依靠通信来互相沟通,从而使人际关系和谐、工作顺利的做法一样,计算机系统是依靠任务之间的良好通信来保证任务与任务的同步的。
有以下3种方法可以解决任务的互斥。
(1)关闭中断法
当一个任务进入临界区后,首先关中断,然后就可以去访问共享资源。
当它从临界区推出后,再把中断打开。
关闭中断法虽然简单有效,但也有明显的缺点。
首先,中断关闭后,如果后面由于种种原因不能再及时打开,那么整个系统就有可能崩溃。
其次,关闭中断后,所有的任务将被阻止,无法获得运行的机会。
因此,关闭中断法不能作为一种普遍使用的互斥实现方法。
(2)繁忙等待法
当一个任务进入临界区时,首先检查一下是否允许进入,若允许,就直接进入;若不允许,就在那里循环地等待。
繁忙等待法的缺点是要不断地执行测试指令,会浪费大量的CPU时间。
(3)信号量法
信号量是一种约定机制,在多任务内核中将信号量用于:
控制共享资源的使用权,标志某事件的发生,使两个任务的行为同步。
信号量是由操作系统维护,任务不能直接修改它的值,只能通过初始化和两个标准原语来对它进行访问。
2、任务间的通信
任务间的同步依赖于任务间的通信。
在μC/OS-II中,是使用信号量、邮箱(消息邮箱)和消息队列这些被称作事件的中间环节来实现任务之间的通信的。
把一个任务置于等待状态要调用OS_EventTaskWait()函数。
该函数的原型为:
voidOS_EventTaskWait(
OS_EVENT*pevent//事件控制块的指针
);
函数OS_EventTaskWait(),将在任务调用函数OS×××Pend()请求一个事件时,被OS×××Pend()所调用。
如果一个正在等待的任务具备了可以运行的条件,那么就要使它进入就绪状态。
这时要调用OS_EventTaskRdy()函数。
该函数的作用就是把调用这个函数的任务在任务等待表中的位置清0(解除等待状态)后,再把任务在任务就绪表中对应的位置1,然后引发一次任务调度。
OS_EventTaskRdy()函数的原型为:
INT8UOS_EventTaskRdy(
OS_EVENT*pevent,//事件控制块的指针
void*msg,//未使用
INT8Umsk//清除TCB状态标志掩码
);
函数OS_EventTaskRdy()将在任务调用函数OS×××Post()发送一个事件时,被函数OS×××Post()所调用。
如果一个正在等待事件的任务已经超过了等待的时间,却仍因为没有获取事件等原因而未具备可以运行的条件,却又要使它进入就绪状态,这时要调用OS_EventTO()函数。
OS_EventTO()函数的原型为:
voidOS_EventTO(
OS_EVENT*pevent//事件控制块的指针
);
函数OS_EventTO()将在任务调用OS×××Pend()请求一个事件时,被函数OS×××Pend()所调用。
3、信号量实例
如下为一个信号量法控制任务之间调用的例子:
在使用信号量之前,应用程序必须调用函数OSSemCreate()来创建一个信号量,OSSemCreate()的原型为:
OS_EVENT*OSSemCreate(
INT16Ucnt//信号量计数器初值
);
函数的返回值为已创建的信号量的指针。
任务通过调用函数OSSemPend()请求信号量,函数OSSemPend()的原型如下:
voidOSSemPend(OS_EVENT*pevent,//信号量的指针
INT16Utimeout,//等待时限
INT8U*err);//错误信息
参数pevent是被请求信号量的指针。
为防止任务因得不到信号量而处于长期的等待状态,函数OSSemPend允许用参数timeout设置一个等待时间的限制,当任务等待的时间超过timeout时可以结束等待状态而进入就绪状态。
如果参数timeout被设置为0,则表明任务的等待时间为无限长。
任务获得信号量,并在访问共享资源结束以后,必须要释放信号量,释放信号量也叫做发送信号量,发送信号量需调用函数OSSemPost()。
OSSemPost()函数在对信号量的计数器操作之前,首先要检查是否还有等待该信号量的任务。
如果没有,就把信号量计数器OSEventCnt加一;如果有,则调用调度器OS_Sched()去运行等待任务中优先级别最高的任务。
函数OSSemPost()的原型为:
INT8UOSSemPost(
OS_EVENT*pevent//信号量的指针
);
调用函数成功后,函数返回值为OS_ON_ERR,否则会根据具体错误返回OS_ERR_EVENT_TYPE、OS_SEM_OVF。
应用程序如果不需要某个信号量了,那么可以调用函数OSSemDel()来删除该信号量,这个函数的原型为:
OS_EVENT*OSSemDel(
OS_EVENT*pevent,//信号量的指针
INT8Uopt,//删除条件选项
INT8U*err//错误信息
);
通过例子可以发现,使用信号量的任务是否能够运行是受任务的优先级别和是否占用信号量两个条件约束的,而信号量的约束高于优先级别的约束。
于是当出现低优先级别的任务与高优先级别的任务使用同一个信号量,而系统中还存有别的中等优先级别的任务时,如果低优先级别的任务先获得了信号量,就会使高级别的任务处于等待状态,而那些不使用该信号量的中等级别的任务却可以剥夺低优先级别的任务的CPU使用权而先于高优先级别的任务而运行了。
我们可以使获得信号量任务的优先级别在使用共享资源期间暂时提升到所有任务最高优先级的高一个级别上,以使该任务不被其他的任务所打断,从而能尽快地使用完共享资源并释放信号量,然后在释放了信号量之后再恢复该任务原来的优先级别。
4、邮箱
创建邮箱需要调用函数OSMboxCreate(),这个函数的原型为:
OS_EVENT*OSMboxCreate(
void*msg//消息指针
);
函数中的参数msg为消息的指针,函数的返回值为消息邮箱的指针。
调用函数OSMboxCreate()需先定义msg的初始值。
在一般的情况下,这个初始值为NULL;但也可以事先定义一个邮箱,然后把这个邮箱的指针作为参数传递到函数OSMboxCreate()中,使之一开始就指向一个邮箱。
任务可以通过调用函数OSMboxPost()向消息邮箱发送消息,这个函数的原型为:
INT8UOSMboxPost(
OS_EVENT*pevent,//消息邮箱指针
void*msg//消息指针
);
当一个任务请求邮箱时需要调用函数OSMboxPend(),这个函数的主要作用就是查看邮箱指针OSEventPtr是否为NULL,如果不是NULL就把邮箱中的消息指针返回给调用函数的任务,同时用OS_NO_ERR通过函数的参数err通知任务获取消息成功;如果邮箱指针OSEventPtr是NULL,则使任务进入等待状态,并引发一次任务调度。
函数OSMboxPend()的原型为:
void*OSMboxPend(
OS_EVENT*pevent,//请求消息邮箱指针
INT16Utimeout,//等待时限
INT8U*err//错误信息
);
5、消息队列
使用消息队列可以在任务之间传递多条消息。
消息队列由三个部分组成:
事件控制块、消息队列和消息。
当把事件控制块成员OSEventType的值置为OS_EVENT_TYPE_Q时,该事件控制块描述的就是一个消息队列。
消息队列相当于一个共用一个任务等待列表的消息邮箱数组,事件控制块成员OSEventPtr指向了一个叫做队列控制块(OS_Q)的结构,该结构管理了一个数组MsgTbl[],该数组中的元素都是一些指向消息的指针。
创建一个消息队列首先需要定义一指针数组,然后把各个消息数据缓冲区的首地址存入这个数组中,然后再调用函数OSQCreate()来创建消息队列。
创建消息队列函数OSQCreate()的原型为:
OS_EVENTOSQCreate(
void**start,//指针数组的地址
INT16Usize//数组长度
);
请求消息队列的目的是为了从消息队列中获取消息。
任务请求消息队列需要调用函数OSQPend(),该函数的原型为:
void*OSQPend(
OS_EVENT*pevent,//所请求的消息队列的指针
INT16Utimeout,//等待时限
INT8U*err//错误信息
);
任务需要通过调用函数OSQPost()或OSQPostFront()来向消息队列发送消息。
函数OSQPost()以FIFO(先进先出)的方式组织消息队列,函数OSQPostFront()以LIFO(后进先出)的方式组织消息队列。
这两个函数的原型分别为:
INT8UOSQPost(
OS_EVENT*pevent,//消息队列的指针
void*msg//消息指针
);
和
INT8UOSQPost(
OS_EVENT*pevent,//消息队列的指针
void*msg//消息指针
);
函数中的参数msg为待发消息的指针。
五、μC/OS-Ⅱ的移植分析
1、移植
所谓移植,是指一个操作系统可以在某个微处理器或者微控制器上运行。
虽然μC/OS-Ⅱ的大部分源代码是用C语言写成的,仍需要用C语言和汇编语言完成一些与处理器相关的代码。
2、产生可重入代码
可重入代码指的是一段代码可以被多个任务同时调用,而不必担心会破坏数据。
也就是说,可重入型函数在任何时刻都可以被中断执行,过一段时间以后又可以继续运行,而不会因为在函数中断时被其它的任务重新调用,影响函数中的数据。
如下为一个可重入型函数
Voidswap(int*x,int*y)
{
Inttemp;
temp=*x;
*x=*y;
*y=temp;
}
运用局部变量temp作为变量。
C编译器把局部变量分配在栈中。
多次调用同一个函数,可以保证每次的temp互不影响。
μC/OS-Ⅱ中进行任务调度时,会把当前任务的CPU寄存器存放到此任务的堆栈中,然后,再从另一个任务的堆栈中恢复原来的工作寄存器,继续运行另一个任务。
所以寄存器的入栈和出栈是μC/OS-Ⅱ中多任务调度的基础。
3、设置与处理器和编译器相关的代码
μC/OS-Ⅱ定义了两个宏来禁止和允许中断:
OS_ENTER_CRITICAL()和OS_EXT_CRITICAL()。
μC/OS-Ⅱ需要先禁止中断,在访问代码的临界区,并在访问完毕后重新允许中断。
如下:
INTS_OFF
mrsr0,cpsr;
movr1,r0;
orrr1,r1,#0xc0;
msrCPSR,r1;
andr0,r0,#0x80;
movpc,lr;
INTS_ON
mrsr0,cpsr;
bicr0,r0,#0xc0;
msrCPSR,r1;
movpc,lr;
绝大多数的微处理器和微控制器的栈堆是从上往下增长的。
4、与操作系统相关的函数
任务堆栈初始化函数OSTaskStklnit,用户传递任务的地址、
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 嵌入式 实时 操作系统 内核 分析