1、系统是通过把待运行程序的地址赋予程序计数器PC来实现程序的切换的。
处理器通过两个指针寄存器(PC和SP)来与任务代码和任务堆栈建立联系并运行它
多任务时,任务与处理器之间的关系的处理,任务的切换是任务运行环境的切换
存储的主要信息
另外再用一个数据结构保存任务堆栈指针(SP),这个数据结构叫做任务控制块(TCB),它除了保存任务堆栈指针之外还要负责保存任务其他信息。
任务控制块
typedef struct os_tcb {
OS_STK *OSTCBStkPtr; //指向任务堆栈栈顶的指针
……
INT8U OSTCBStat; //任务的当前状态标志
INT8U OSTCBPrio; //任务的优先级别
……
} OS_TCB;
实质上系统是通过SP的切换来实现程序的切换的。
其实,程序切换的关键把程序的私有堆栈指针赋予处理器的堆栈指针SP
程序代码、私有堆栈、任务控制块是任务的三要件。任务控制块提供了运行环境的存储位置
一个完整的任务应该有如下三部分:
任务状态:
等待状态:
正在运行的任务,需要等待一段时间或需要等待一个事件发生再运行时,该任务就会把CPU的使用权让给别的任务而使任务进入等待状态。
睡眠状态
任务在没有被配备任务控制块或被剥夺了任务控制块时的状态叫做任务的睡眠状态
就绪状态(任务的CPU使用权被剥夺时)
系统为任务配备了任务控制块且在任务就绪表中进行了就绪登记,这时任务的状态叫做就绪状态。
运行状态
处于就绪状态的任务如果经调度器判断获得了CPU的使用权,则任务就进入运行状态
终端服务状态
一个正在运行的任务一旦响应中断申请就会中止运行而去执行中断服务程序,这时任务的状态叫做中断服务状态
任务控制块:主要作用就是保存该任务的虚拟处理器的堆栈指针寄存器SP。
为了更好地识别每一个任务控制块,μC/OS-II是用任务的优先级来作为任务的标识的,所以,任务控制块还要来保存该任务的优先级别。
同时,一个任务在不同的时刻还处于不同的状态,显然,记录了任务状态的数据也应该保存到任务控制块中
综上所述,一个任务中的任务控制块保存了虚拟处理器的SP,优先级别以及任务状态等。
typedef struct os_tcb {
OS_STK *OSTCBStkPtr; //指向任务堆栈栈顶的指针
……
INT8U OSTCBStat; //任务的当前状态标志
INT8U OSTCBPrio; //任务的优先级别
……
} OS_TCB;
空闲任务只是做了一个计数工作,空闲任务中并没有调用任务延时函数
void OS_TaskIdle(void* pdata)
{
# if OS_CRITICAL_METHOD = = 3
OS_CPU_SR cpu_sr;
#endif
pdata = pdata; //防止某些编译器报错
for(;;)
{
OS_ENTER_CRITICAL( );//关闭中断
OSIdleCtr++; //计数
OS_EXIT_CRITICAL( ); //开放中断
}
}
μC/OS-II规定,一个用户应用程序必须使用这个空闲任务,而且这个任务是不能用软件来删除的
这个统计任务每秒计算一次CPU在单位时间内被使用的时间,并把计算结果以百分比的形式存放在变量OSCPUsage中,以便应用程序通过访问它来了解CPU的利用率,所以这个系统任务OSTaskStat( )叫做统计任务
μC/OS_II 把任务的优先权分为64个优先级别,每一个级别都用一个数字来表示。数字0表示任务的优先级别最高,数字越大则表示任务的优先级别越低
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个
在应用程序中定义任务堆栈的栈区非常简单,即定义一个OS_STK类型的一个数组并在创建一个任务时把这个数组的地址赋给该任务就可以了。
//定义堆栈的长度
#define TASK_STK_SIZE 512
//定义一个数组来作为任务堆栈
OS_STK MyTaskStk[TASK_STK_SIZE];
在创建用户任务时,要传递任务的堆栈指针和任务优先级别
void main(void)
{
……
OSTaskCreate(
MyTask, //任务的指针
&MyTaskAgu, //传递给任务的参数
&MyTaskStk[TASK_STK_SIZE-1], //任务堆栈栈顶地址
20 //任务的优先级别
);
……
}
使用函数OSTaskCreate()创建任务时,要注意所使用的处理器对堆栈增长方向是支持向上还是向下的
应用程序在创建一个新任务的时候,必须把在系统启动这个任务时CPU各寄存器所需要的初始数据(任务指针、任务堆栈指针、程序状态字等等),事先存放在任务的堆栈中
μC/OS-II在创建任务函数OSTaskCreate( )中通过调用任务堆栈初始化函数OSTaskStkInit( )来完成任务堆栈初始化工作的
OS_STK *OSTaskStkInit(
void (*task)(void *pd),
void *pdato,
OS_STK *ptos,
INT16U opt
);
其实,任务堆栈的初始化就是对该任务虚拟处理器的初始化
μC/OS-II用来记录任务的堆栈指针、任务的当前状态、任务的优先级别等一些与任务管理有关的属性的表就叫做任务控制块,没有任务控制块就不能被系统 承认与管理,
任务控制块结构的主要成员
typedef struct os_tcb {
OS_STK *OSTCBStkPtr; //指向任务堆栈栈顶的指针
……
struct os_tcb *OSTCBNext;//指向后一个任务控制块的指针
struct os_tcb *OSTCBPrev; //指向前一个任务控制块的指针
……
INT16U OSTCBDly; //任务等待的时限(节拍数)
INT8U OSTCBStat; //任务的当前状态标志
INT8U OSTCBPrio; //任务的优先级别
……
} OS_TCB;
当进行系统初始化时,初始化函数会按用户提供的任务数为系统创建具有相应数量的任务控制块并把它们链接为一个链表。 由于这些任务控制块还没有对应的任务,故这个链表叫做空任务块链表。
下图白色部分指空任务控制表链表,而黑色部分指任务控制块链表,每一个任务控制块都有优先级
当应用程序调用函数OSTaskCreate( )创建一个任务时,这个函数会调用系统函数OSTCBInit ( )来为任务控制块进行初始化。这个函数首先为被创建任务从空任务控制块链表获取一个任务控制块,然后用任务的属性对任务控制块各个成员进行赋值,最后再把这个任务控制块链入到任务控制块链表的头部
多任务操作系统的核心工作就是任务调度。
所谓调度,就是通过一个算法在多个任务中确定该运行的任务,做这项工作的函数就叫做调度器。
μC/OS_II进行任务调度的思想是 “近似地每时每刻总是让优先级最高的就绪任务处于运行状态” 。为了保证这一点,它在系统或用户任务调用系统函数及执行中断服务程序结束时总是调用调度器,来确定应该运行的任务并运行它 。
μC/OS_II进行任务调度的依据就是任务就绪表
为了能够使系统清楚地知道,系统中哪些任务已经就绪,哪些还没有就绪,μC/OS_II在RAM中设立了一个记录表,系统中的每个任务都在这个表中占据一个位置,并用这个位置的状态(1或者0)来表示任务是否处于就绪状态,这个表就叫做任务就绪状态表,简称叫任务就绪表
任务就绪表是一个二维数组 OSRdyTbl[]
为加了加快访问任务就绪表的速度,系统定义了一个变量 OSRdyGrp
来表明就绪表种每行是否存在就绪任务
任务优先级别与就虚表中 x y
的关系
用代码将prio为29的任务自动置为就绪状态
OSRdyTbl[prio>>3] | = 0x01<<(prio&0x07);
OSRdyGrp | =0x01<<(prio>>3);
//用下面这个更好
OSRdyTbl[prio>>3] | = OSMapTbl[prio&0x07];
OSRdyGrp | =OSMapTbl[prio>>3];
//以上两种方式表达是等价的
上面这个不对,因为我们无法确定在 OSRdGrp[3]
中没有其他的就绪任务,因此我们需要用如下代码,来进行出表操作:
//OSMapTbl[]用来方便生成第几行,第几列的一个转换而已。
if((OSRdyTbl[prio>>3] &= ~OSMapTbl[prio&0x07]) == 0)
OSRdyGrp&=~OSMapTbl[prio>>3];
人工查找最高优先级
在就绪表中计算机查找最高优先级别的代码:
//用来得出最高优先级的数组,每个下标都对应与之相等的INT8U数的最高优先位。
y = OSUnMapTal[OSRdyGrp];
x = OSUnMapTal[OSRdyTbl[y]];
prio = (y<<3) +x;
任务切换宏 OS_TASK_SW()
不要企图用PUSH和POP指令来使程序计数器PC压栈和出栈,因为没有这样的指令。中断动作和过程调用指令可以使PC压栈;中断返回指令可以使PC出栈。
因此任务切换 OS_TASK_SW()
必定是一个中断服务程序。
需要由宏 OS_TASK_SW()
来引发一次中断或者一次调用来使 OS_TASK_SW()
执行任务切换工作
用函数OSTaskCreate()
函数是创建一个任务,OSTaskCreate( )函数的原型如下:
INT8U OSTaskCreate (
void (*task)(void *pd),//指向任务的指针
void *pdata, //传递给任务的参数
OS_STK *ptos, //指向任务堆栈栈顶的指针
INT8U prio //任务的优先级
)
一般来说,任务可以在调用函数OSStart( )
启动任务调度之前来创建,也可以在任务中来创建。但是,μC/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的代码
}
}
在使用μC/OS-II的所有服务之前,必须要调用μC/OS-II的初始化函数OSInit( )对μC/OS-II自身的运行环境进行初始化。
函数OSInit( )
将对μC/OS-II的所有的全局变量和数据结构进行初始化,同时创建空闲任务OSTaskIdle
,并赋之以最低的优先级别和永远的就绪状态。如果用户应用程序还要使用统计任务的话(常数OS_TASK_STAT_EN=1
),则OSInit( )
还要以优先级别为OS_LOWEST_PRIO-1
来创建统计任务
初始化函数OSInit( )
对数据结构进行初始化时,主要要创建包括空任务控制块链表在内的5个空数据缓冲区。同时,为了可以快速地查询任务控制块链表中的各个元素,初始化函数OSInit( )
还要创建一个数组OSTCBPrioTbl[OS_LOWEST_PRIO + 1]
,在这个数组中,按任务的优先级别的顺序把任务控制块的指针存放在了对应的元素中
初始化成功之后
//OSInit()
void OSInit (void)
{
OSInitHookBegin(); /* Call port specific initialization code */
OS_InitMisc(); /* Initialize miscellaneous variables */
OS_InitRdyList(); /* Initialize the Ready List */
OS_InitTCBList(); /* Initialize the free list of OS_TCBs */
OS_InitEventList(); /* Initialize the free list of OS_EVENTs */
OS_FlagInit(); /* Initialize the event flag structures */
OS_MemInit(); /* Initialize the memory manager */
OS_QInit(); /* Initialize the message queue structures */
OS_InitTaskIdle(); /* Create the Idle Task */
OS_InitTaskStat(); /* Create the Statistic Task */
OSInitHookEnd(); /* Call port specific init. code */
OSDebugInit();
}
μC/OS-II进行任务的管理是从调用启动函数OSStart( )
开始的,当然其前提条件是在调用该函数之前至少创建了一个用户任务 (除了统计任务与空闲任务之外)
启动后:
void OSStart (void)
{
INT8U y;
INT8U x;
if (OSRunning == FALSE) {
y = OSUnMapTbl[OSRdyGrp]; /* Find highest priority's task priority number */
x = OSUnMapTbl[OSRdyTbl[y]];
OSPrioHighRdy = (INT8U)((y << 3) + x);
OSPrioCur = OSPrioHighRdy;
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; /* Point to highest priority task ready to run */
OSTCBCur = OSTCBHighRdy;
OSStartHighRdy(); /* Execute target specific code to start task */
}
}
系统接收到中断请求后,这时如果CPU处于中断允许状态(即中断是开放的),系统就会中止正在运行的当前任务,而按照中断向量的指向转而去运行中断服务子程序;当中断服务子程序的运行结束后,系统将会根据情况返回到被中止的任务继续运行或者转向运行另一个具有更高优先级别的就绪任务。
**注意!**中断服务子程序运行结束之后,系统将会根据情况进行一次任务调度去运行优先级别最高的就绪任务,而并不是一定要接续运行被中断的任务的。
中断响应过
通常在进入中断时需要使用OSIntEnter()
;退出中断前使用OSIntExit()
;
void OSIntEnter (void)
{
if (OSRunning == TRUE)
{
if (OSIntNesting < 255)
{
OSIntNesting++; //中断嵌套层数计数器加一
}
}
}
void OSIntExit (void)
{
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cpu_sr;
#endif
if (OSRunning == TRUE)
{
OS_ENTER_CRITICAL( );
if (OSIntNesting > 0)
{
OSIntNesting--; //中断嵌套层数计数器减一
}
if ((OSIntNesting == 0) && (OSLockNesting == 0))
{
OSIntExitY = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((OSIntExitY << 3)
+ OSUnMapTbl[OSRdyTbl[OSIntExitY]]);
if (OSPrioHighRdy != OSPrioCur)
{
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
OSCtxSwCtr++;
OSIntCtxSw( );
}
}
OS_EXIT_CRITICAL( );
}
}
在中断服务程序中调用的负责任务切换工作的函数OSIntCtxSw( )
叫做中断级任务切换函数
OSIntCtxSw( )
{
OSTCBCur = OSTCBHighRdy; //任务控制块的切换
OSPrioCur=OSPrioHighRdy;
SP = OSTCBHighRdy->OSTCBStkPtr; //SP指向待运行任务堆栈用出栈指令把R1,R2,……弹入CPU的通用寄存器;
RETI; //中断返回,使PC指向待运行任务
}
μC/OS-II与大多数计算机系统一样,用硬件定时器产生一个周期为ms级的周期性中断来实现系统时钟,最小的时钟单位就是两次中断之间相间隔的时间,这个最小时钟单位叫做时钟节拍(Time Tick)。
硬件定时器以时钟节拍为周期定时地产生中断,该中断的中断服务程序叫做OSTickISR()
。中断服务程序通过调用函数OSTimeTick( )
来完成系统在每个时钟节拍时需要做的工作。
void OSTickISR(void) //系统时钟中断服务程序
{
保存CPU寄存器;
调用OSIntEnter( ); //记录中断嵌套层数
if (OSIntNesting = = 1;
{
OSTCBCur->OSTCBStkPtr = SP; //保存堆栈指针
}
调用OSTimeTick( ); //节拍处理
清除中断;
开中断;
调用OSIntExit( ); //中断嵌套层数减一
恢复CPU寄存器;
中断返回;
}
void OSTimeTick (void) //时钟节拍服务函数
{
……
OSTimeTickHook( );
……
OSTime++; //记录节拍数
……
if (OSRunning = = TRUE) {
ptcb = OSTCBList;
while (ptcb->OSTCBPrio != OS_IDLE_PRIO)
{
OS_ENTER_CRITICAL( );
if (ptcb->OSTCBDly != 0)
{
if (--ptcb->OSTCBDly = = 0) //任务的延时时间减一
{
if ((ptcb->OSTCBStat & OS_STAT_SUSPEND)
= = OS_STAT_RDY)
{
OSRdyGrp |= ptcb->OSTCBBitY;
OSRdyTbl[ptcb->OSTCBY]
|= ptcb->OSTCBBitX;
} else {
ptcb->OSTCBDly = 1;
}
}
}
ptcb = ptcb->OSTCBNext;
OS_EXIT_CRITICAL( );
}
}
函数OSTimeTick( )
的任务,就是在每个时钟节拍了解每个任务的延时状态,使其中已经到了延时时限的非挂起任务进入就绪状态。
由于嵌入式系统的任务是一个无限循环,并且μC/OS-II还是一个抢占式内核,所以为了使高优先级别的任务不至于独占CPU,可以给其他任务优先级别较低的任务获得CPU使用权的机会,μC/OS-II规定:除了空闲任务之外的所有任务必须在任务中合适的位置调用系统提供的函数OSTimeDly( ),
使当前任务的运行延时(暂停)一段时间并进行一次任务调度,以让出CPU的使用权。
void OSTimeDly (INT16U ticks) //系统提供的任务延时函数
{
#if OS_CRITICAL_METHOD = = 3
OS_CPU_SR cpu_sr;
#endif
if (ticks > 0)
{
OS_ENTER_CRITICAL( );
if ((OSRdyTbl[OSTCBCur->OSTCBY]
&= ~OSTCBCur->OSTCBBitX) = = 0)
{
OSRdyGrp &= ~OSTCBCur->OSTCBBitY; //取消当前任务的就绪状态
}
OSTCBCur->OSTCBDly = ticks;//延时节拍数存入任务控制块
OS_EXIT_CRITICAL( );
OS_Sched( ); //调用调度函数
}
}
#取消任务延时函数
INT8U OSTimeDlyResume( INT8U prio);
#获得系统时间函数
INT32U OSTimeGet( void );
#设置系统时间函数
void OSTimeSet( INT32U ticks );
事件:任务间的同步依赖于任务间的通信。在μC/OS-II中,是使用信号量、邮箱(消息邮箱)和消息队列这些被称作事件的中间环节来实现任务之间的通信的。
为了把描述事件的数据结构统一起来,μC/OS-II使用叫做事件控制块ECB(Event Control Block)的数据结构来描述诸如信号量、消息邮箱 和 消息队列这些事件。事件控制块中包含包括等待任务表在内的所有有关事件的数据 。
typedef struct
{
INT8U OSEventType; //事件的类型
INT16U OSEventCnt; //信号量计数器
void * OSEventPtr; //消息或消息队列的指针
INT8U OSEventGrp; //等待事件的任务组
INT8U OSEventTbl[OS_EVENT_TBL_SIZE];//任务等待表
} OS_EVENT;
事件控制块(ECB)结构
把一个任务置于等待状态要调用 OS_EventTaskWait()
函数,该函数的原型是:
void OS_EventTaskWait(OS_EVENT*pevent)
//OS_EVENT*pevent 是事件控制块的指针
//该函数通常在任务调用函数OSXXXPend()请求一个事件时,被OSXXXPend所调用
如果一个正在等待的任务具备了可以运行的条件,那么就要使它进入就绪状态,这时要调用OS_EventTaskRdy( )
函数。该函数的作用就是把调用这个函数的任务在任务等待表中的位置清0(解除等待状态)后,再把任务在任务就绪表中对应的位置1,然后引发一次任务调度。
它的原型为:
INT8U OS_EventTaskRdy (
OS_EVENT *pevent, //事件控制块的指针
void *msg, //未使用
INT8U msk //清除TCB状态标志掩码
);
//该函数通常在任务调用函数OS×××Post ( ) 发送一个事件时,被函数OS×××Post ( )所调用。
如果一个正在等待事件的任务已经超过了等待的时间,却仍因为没有获取事件等原因而未具备可以运行的条件,却又要使它进入就绪状态,这时要调用OS_EventTO( )
函数。
void OS_EventTO (OS_EVENT * pevent );
//OS_EVENT * pevent //事件控制块的指针
//函数OS_EventTO ( )将在任务调用OS×××Pend( ) 请求一个事件时,被函数OS×××Pend( )所调用。
在μC/OS-II初始化时,系统会在初始化函数OSInit( )
中按应用程序使用事件的总数创建OS_MAX_EVENTS
个空事件控制块并使用成员OSEventPtr
作为链接指针,把这些空事件控制块链接成一个单向链表。由于链表中的所有控制块尚未与具体事件相关联,故该链表叫做空事件控制块链表。(空任务控制块链表名称为:OSEventFreeList
)以后,每当应用程序创建一个事件时,系统就会从链表中取出一个空事件控制块,并对它进行初始化以描述该事件。
在使用信号量之前,应用程序必须调用函数OSSemCreate( )
来创建一个信号量
创建信号量的函数原型为:
OS_EVENT *OSSemCreate (INT16U cnt);
//其中:INT16U cnt 为信号量计数器初值
//函数的返回值为已创建的信号量的指针。
OSSemCreate():创建一个信号量并对信号量的初始计数值赋值,该初始值为0到65535之间的一个数。
OS_EVENT *OSSemCreate(INT16U cnt);
//cnt:信号量的初始值。
执行步骤:
任务通过调用函数OSSemPend( )
请求信号量,函数OSSemPend( )
的原型如下:
void OSSemPend ( OS_EVENT *pevent, INT16U timeout, INT8U *err);
//1)参数pevent是被请求信号量的指针;
//2)INT16U timeout,
//等待时限,为防止任务因得不到信号量而处于长期的等待状态,函数OSSemPend允许用参数timeout设置一个等待时间的限制,当任务等待的时间超过timeout时可以结束等待状态而进入就绪状态。如果参数timeout被设置为0,则表明任务的等待时间为无限长;
//3)INT8U *err //错误信息
OSSemPend()
:等待一个信号量,即操作系统中的P操作,将信号量的值减1
OSSemPend (OS_EVENT *pevent,INT16U timeout, INT8U *err);
执行步骤:
OSSched()
函数,调度下一个最高优先级的任务运行。任务获得信号量,并在访问共享资源结束以后,必须要释放信号量,释放信号量也叫做发送信号量,发送信号量需调用函数OSSemPost ( )
。OSSemPost ( )
函数在对信号量的计数器操作之前,1)首先要检查是否还有等待该信号量的任务。2)如果没有,就把信号量计数器OSEventCnt加一;3)如果有,则调用调度器OS_Sched( )去运行等待任务中优先级别最高的任务。
函数OSSemPost ( )的原型为:
INT8U OSSemPost (OS_EVENT *pevent );
//OS_EVENT *pevent //待释放信号量的指针
//调用函数成功后,函数返回值为OS_ON_ERR,否则会根据具体错误返回OS_ERR_EVENT_TYPE、OS_SEM_OVF。
应用程序如果不需要某个信号量了,那么可以调用函数OSSemDel( )
来删除该信号量,这个函数的原型为:
OS_EVENT *OSSemDel (OS_EVENT *pevent, INT8U opt, INT8U *err);
//1)OS_EVENT *pevent, //信号量的指针
//2)INT8U opt, //删除条件选项
//3)INT8U *err //错误信息
在可剥夺型内核中,当任务以独占方式使用共享资源时,会出现低优先级任务先于高优先级任务而被运行的现象,这种现象叫做任务优先级反转。在一般情况下是不允许出现这种任务优先级反转现象的,下面就对优先级的反转现象做一个详细的分析,以期找出原因及解决方法。
通过例子可以发现,使用信号量的任务是否能够运行是受任务的优先级别和是否占用信号量两个条件约束的,而信号量的约束高于优先级别的约束。
于是当出现低优先级别的任务与高优先级别的任务使用同一个信号量,而系统中还存有别的中等优先级别的任务时,如果低优先级别的任务先获得了信号量,就会使高级别的任务处于等待状态,而那些不使用该信号量的中等级别的任务却可以剥夺低优先级别的任务的CPU使用权而先于高优先级别的任务而运行了。
解决问题的办法之一:
使获得信号量任务的优先级别在使用共享资源期间暂时提升到所有任务最高优先级的高一个级别上,以使该任务不被其他的任务所打断,从而能尽快地使用完共享资源并释放信号量,然后在释放了信号量之后再恢复该任务原来的优先级别。
创建互斥信号量需要调用函数 OSMutexCreate
,函数原型如下:
OS_EVENT *OSMutexCreate (
INT8U prio, //优先级别
INT8U *err //错误信息
);
函数OSMutexCreate( )
从空事件控制块链表获取一个事件控制块,把成员OSEventType
赋以常数OS_EVENT_TYPE_MUTEX
以表明这是一个互斥型信号量,然后再把成员OSEventCnt
的高8位赋以prio
(欲提升的优先级别),低8位赋以常数OS_MUTEX_AVAILABLE
(该常数值为0xFFFF)的低8位(0xFF)以表明信号量尚未被任何任务所占用,处于有效状态。
当任务需要访问一个独占式共享资源时,就要调用函数OSMutexPend( )
来请求管理这个资源的互斥型信号量,如果信号量有信号(OSEventCnt
的低8位为0xFF),则意味着目前尚无任务占用资源,于是任务可以继续运行并对该资源进行访问,否则就进入等待状态,直至占用这个资源的其他任务释放了该信号量。
函数OSMutexPend( )的原型为:
void OSMutexPend (
OS_EVENT *pevent, //互斥型信号量指针
INT16U timeout, //等待时限
INT8U *err //错误信息
);
任务可以通过调用函数OSMutexPost()
发送一个互斥型信号量,这个函数的原型为:
INT8U OSMutexPost (
OS_EVENT *pevent //互斥型信号量指针
);
如果把数据缓冲区的指针赋给一个事件控制块的成员OSEventPrt
,同时使事件控制块的成员OSEventType
为常数OS_EVENT_TYPE_MBOX,则该事件控制块就叫做消息邮箱,消息邮箱是在两个需要通信的任务之间通过传递数据缓冲区指针的方法来通信的。
邮箱的系统服务
OSMboxCreate
OSMboxPost
OSMboxPend
OSMboxAccept
OSMboxQuery
创建邮箱需要调用函数OSMboxCreate ( )
函数的原型为:
OS_EVENT *OSMboxCreate (void * msg );
//其中:1)函数中的参数msg为消息的指针;
//2)函数的返回值为消息邮箱的指针。
调用函数OSMboxCreate ( )
需先定义msg的初始值。在一般的情况下,这个初始值为NULL;但也可以事先定义一个邮箱,然后把这个邮箱的指针作为参数传递到函数OSMboxCreate ( )
中,使之一开始就指向一个邮箱。
总结 msg:既可以定义为NULL,也可以定义为邮箱
OS_EVENT *CommMbox;
void main(void){
...
OSInit();
...
CommMbox =OSMboxCreate((void*)0);
OSStart();
}
任务可以通过调用函数OSMboxPost ( )
向消息邮箱发送消息,这个函数的原型为:
INT8U OSMboxPost (
OS_EVENT *pevent, //消息邮箱指针
void *msg //消息指针
);
OS_EVENT *CommMbox;
INT8U CommRxBuf[100];
void CommTaskRx(void *pdata)
{
INT8U err;
...
for (;;)
{
...
err = OSMboxPost(CommMbox, (void*)&CommRxbuf[0]);
...
}
}
当一个任务请求邮箱时需要调用函数OSMboxPend( )
,这个函数的主要作用就是查看邮箱指针OSEventPtr是否为NULL,如果不是NULL就把邮箱中的消息指针返回给调用函数的任务,同时用OS_NO_ERR通过函数的参数err通知任务获取消息成功;如果邮箱指针OSEventPtr是NULL,则使任务进入等待状态,并引发一次任务调度。
函数OSMboxPend( )的原型为:
void *OSMboxPend (
OS_EVENT *pevent, //请求消息邮箱指针
INT16U timeout, //等待时限
INT8U *err //错误信息
);
void CommTask(void *pdata)
{
INT8U err;
void *msg;
pdata = pdata;
for (;;) { ...
msg=OSMboxPend(CommMbox,10,&err);
if(err == OS_NO_ERR) {
/* 收到消息时的代码 */
}
else {
/* 未收到消息时的代码*/
}
}
使用消息队列可以在任务之间传递多条消息。消息队列由三个部分组成:事件控制块、队列控制块和消息指针。
当把事件控制块成员OSEventType的值置为OS_EVENT_TYPE_Q时,该事件控制块描述的就是一个消息队列。
队列控制块数据结构
typedef struct os_q {
struct os_q *OSQPtr;//空闲队列控制块指针
void **OSQStart; //指向消息队列的起始地址
void **OSQEnd; //指向消息队列的结束地址
void **OSQIn; //指向消息队列中下一个插入消息的位置
void **OSQOut;//指向消息队列中下一个取出消息的位置
INT16U OSQSize; //消息队列中总的单元数
INT16U OSQEntries; //消息队列中当前的消息数量
} OS_Q;
创建一个消息队列首先需要定义一指针数组,然后把各个消息数据缓冲区的首地址存入这个数组中,然后再调用函数OSQCreate( )来创建消息队列。
创建消息队列函数OSQCreate( )的原型为:
OS_EVENT OSQCreate(void** start, INT16U size );
//其中:
//1)void** start, //指针数组的地址
//2)INT16U size //指针数组的长度
请求消息队列的目的是为了从消息队列中获取消息。任务请求消息队列需要调用函数OSQPend( )
,该函数的原型为:
void*OSQPend(
OS_EVENT*pevent, //所请求的消息队列的指针
INT16U timeout, //等待时限
INT8U*err //错误信息
);
任务需要通过调用函数OSQPost( )
或OSQPostFront( )
来向消息队列发送消息。函数OSQPost( )
以FIFO(先进先出)的方式组织消息队列,函数OSQPostFront
( )以LIFO(后进先出)的方式组织消息队列。这两个函数的原型分别为:
INT8U OSQPost(
OS_EVENT*pevent, //消息队列的指针
void*msg //消息指针
);
INT8U OSQPostFront(
OS_EVENT*pevent, //消息队列的指针
void*msg //消息指针
);
//函数中的参数msg为待发消息的指针。
在实际应用中,任务常常需要与多个事件同步,即要根据多个信号量组合作用的结果来决定任务的运行方式。μC/OS-II为了实现多个信号量组合的功能定义了一种特殊的数据结构——信号量集。
信号量集所能管理的信号量都是一些二值信号,所有信号量集实质上是一种可以对多个输入的逻辑信号进行基本逻辑运算的组合逻辑,其示意图如图5-1所示
信号集的标志组
不同于信号量、消息邮箱、消息队列等事件,μC/OS-II不使用事件控制块来描述信号量集,而使用了一个叫做标志组的结构OS_FLAG_GRP。
OS_FLAG_GRP结构如下:
typedef truct{
INT8U OSFlagType; //识别是否为信号量集的标志
void *OSFlagWaitList;//指向等待任务链表的指针
OS_FLAGS OSFlagFlags; //所有信号列表
}OS_FLAG_GRP;
成员OSFlagWaitList
是一个指针,当一个信号量集被创建后,这个指针指向了这个信号量集的等待任务链表。
与其他前面介绍过的事件不同,信号量集用一个双向链表来组织等待任务,每一个等待任务都是该链表中的一个节点(Node)。标志组OS_FLAG_GRP的成员OSFlagWaitList
就指向了信号量集的这个等待任务链表。
等待任务链表节点OS_FLAG_NODE的结构如下:
typedef struct {
void *OSFlagNodeNext; //指向下一个节点的指针
void *OSFlagNodePrev; //指向前一个节点的指针
void *OSFlagNodeTCB; //指向对应任务控制块的指针
void *OSFlagNodeFlagGrp; //反向指向信号量集的指针
OS_FLAGS OSFlagNodeFlags; //信号过滤器
INT8U OSFlagNodeWaitType;//定义逻辑运算关系的数据
} OS_FLAG_NODE;
给等待任务链表添加节点的函数为OS_FlagBlock( )
,这个函数的原型为:
static void OS_FlagBlock (
OS_FLAG_GRP *pgrp, //信号量集指针
OS_FLAG_NODE *pnode, //待添加的等待任务节点指针
OS_FLAGS flags, //指定等待信号的数据
INT8U wait_type, //信号与等待任务之间的逻辑
INT16U timeout //等待时限
);
这个函数将在请求信号量集函数OSFlagPend ( )
中被调用。
从等待任务链表中删除一个节点的函数为OS_FlagUnlink( )
,这个函数的原型为:
void OS_FlagUnlink (OS_FLAG_NODE *pnode);
这个函数将在发送信号量集函数OSFlagPost( )
中被调用。
任务可以通过调用函数OSFlagCreate ()
来创建一个信号量集。OSFlagCreate ( )的函数原型为:
OS_FLAG_GRP *OSFlagCreate (
OS_FLAGS flags, //信号的初始值
INT8U *err //错误信息
);
任务可以通过调用函数OSFlagPend( )
请求一个信号量集,OSFlagPend( )函数的原型为:
OS_FLAGS OSFlagPend (
OS_FLAG_GRP *pgrp, //所请求的信号量集指针
OS_FLAGS flags, //滤波器
INT8U wait_type, //逻辑运算类型
INT16U timeout, //等待时限
INT8U *err //错误信息
);
任务可以通过调用函数OSFlagPost ( )向信号量集发信号,OSFlagPost ( )函数的原型为:
OS_FLAGS OSFlagPost (
OS_FLAG_GRP *pgrp, //信号量集指针
OS_FLAGS flags, //选择所要发送的信号
INT8U opt, //信号有效的选项
INT8U *err //错误信息
);
所谓任务向信号量集发信号,就是对信号量集标志组中的信号进行置“1”(置位)或置“0”(复位)的操作。至于对信号量集中的哪些信号进行操作,用函数中的参数flags来指定;对指定的信号是置“1”还是置“0”,用函数中的参数opt来指定(opt = OS_FLAG_SET为置“1”操作;opt = OS_FLAG_CLR为置“0”操作)。
应用程序在运行中为了某种特殊需要,经常需要临时获得一些内存空间,因此作为一个比较完善的操作系统必须具有动态分配内存的能力。
能否合理、有效地对内存储器进行分配和管理,是衡量一个操作系统品质的指标之一。特别地对于实时操作系统来说,还应该保证系统在动态分配内存时,它的执行时间必须是可确定的。μC/OS-II改进了ANSI C
用来动态分配和释放内存的malloc( )和free( )函数,使它们可以对大小固定的内存块进行操作,从而使 malloc( )和free( )函数的执行时间成为可确定的,满足了实时操作系统的要求。
μC/OS-II对内存进行两级管理,即把一个大片连续的内存空间分成了若干个分区,每个分区又分成了若干个大小相等的内存块来进行管理。操作系统以分区为单位来管理动态内存,而任务以内存块为单位来获得和释放动态内存。内存分区及内存块的使用情况则由表——内存控制块来记录。
本节首先介绍内存分区和分区中的内存块,然后再介绍内存控制块。
应用程序如果要使用动态内存的话,则要首先在内存中划分出可以进行动态分配的区域,这个划分出来区域叫做内存分区,每个分区要包含若干个内存块。μC/OS-II要求同一个分区中的内存块的字节数必须相等,而且每个分区与该分区的内存块的数据类型必须相同。
在内存中划分一个内存分区与内存块的方法非常简单,只要定义一个二维数组就可以了,其中的每个一维数组就是一个内存块。例如,定义一个用来存储INT16U类型数据,有10个内存块,每个内存块长度为10的内存分区的代码如下:
INT16U IntMemBuf[10][10]
;
需要注意的是,上面这个定义只是在内存中划分出了分区及内存块的区域,还不是一个真正的可以动态分配的内存区,如图6-1(a)所示。只有当把内存控制块与分区关联起来之后,系统才能对其进行相应的管理和控制,它才能是一个真正的动态内存区
为了使系统能够感知和有效地管理内存分区,μC/OS-II给每个内存分区定义了一个叫做内存控制块(OS_MEM)的数据结构。系统就用这个内存控制块来记录和跟踪每一个内存分区的状态。内存控制块的结构如下:
typedef struct {
void *OSMemAddr; //内存分区的指针
void *OSMemFreeList; //内存控制块链表的指针
INT32U OSMemBlkSize; //内存块的长度
INT32U OSMemNBlks; //分区内内存块的数目
INT32U OSMemNFree; //分区内当前可分配的内存块的数目
} OS_MEM;
当应用程序调用函数OSMemCreate( )
建立了一个内存分区之后,内存控制块与内存分区和内存块之间的关系如图
划分了欲使用的分区和内存块之后,应用程序可以通过调用函数OSMemCreate( )来建立一个内存分区, OSMemCreate( ) 函数的原型为:
OS_MEM *OSMemCreate(
void *addr, //内存分区的起始地址
INT32U nblks, //分区中内存块的数目
INT32U blksize, //每个内存块的字节数
INT8U *err //错误信息
);
在应用程序需要一个内存块时,应用程序可以通过调用函数OSMemGet( )
向某内存分区请求获得一个内存块,OSMemGet( )函数的原型为:
void *OSMemGet (
OS_MEM *pmem, //内存分区的指针
INT8U *err //错误信息
);
当应用程序不再使用一个内存块时,必须要及时地将它释放。应用程序通过调用函数OSMemPut( )
来释放一个内存块,OSMemPut( )函数的原型为:
INT8U OSMemPut (
OS_MEM *pmem, //内存块所属内存分区的指针
void *pblk //待释放内存块的指针
);
应用程序可以通过调用函数OSMemQuery( )
来查询一个分区目前的状态信息,函数函数OSMemQuery( )的原型为:
INT8U OSMemQuery (
OS_MEM *pmem, //待查询的内存控制块的指针
OS_MEM_DATA *pdata//存放分区状态信息的结构的指针
)
其中参数pdata是一个OS_MEM_DATA类型的结构,该结构的定义如下:
typedef struct {
void *OSAddr; //内存分区的指针
void *OSFreeList; //分区内内存块链表的头指针
INT32U OSBlkSize; //内存块的长度
INT32U OSNBlks; //分区内内存块的数目
INT32U OSNFree; //分区内空闲内存块的数目
INT32U OSNUsed; //已被分配的内存块数目
} OS_MEM_DATA;
睡眠态
就绪态
运行态
OSStart()
可以启动多任务。该函数只能在启动时调用一次等待状态
正在运行的任务可以通过下面的调用进入等待状态。延迟时间到,立即强制执行任务切换,让下一个优先级最高、并进入就绪态的任务执行。
等待时间过去后,系统服务(内部)函数OSTimeTick()使延迟了的任务进入就绪态
用户无需在应用程序代码中调用这个函数
n正在运行的任务可能需要通过调用函数等待某一事件发生。如果该事件并未发生,任务就进入等待状态
n当事件发生或等待超时,被挂起的任务就进入就绪态
中断服务态
正在执行的任务是可以被中断的,除非该任务将中断关闭,或系统将中断关闭。被中断的任务便进入了中断服务态
响应中断后,正在运行的任务被挂起,中断服务子程序控制了CPU的使用权
中断服务子程序可能会报告一个或多个事件的发生,而使一个或多个任务进入就绪态
上述情况下,从中断服务子程序返回之前,mC/OS –II 要判定
当所有的任务都在等待时间发生或等待延迟时间结束时,μC/OS-II 执行被称为(还有统计任务OSTaskStat()
)**空闲任务(Idle Task)**的内部函数,即:OSTaskIdle()
任务调度就是:查找准备就绪的最高优先级的任务并进行上下文切换。
OSTCBHighRdy
待运行任务控制块(确定最高优先级的任务控制块)OSTCBCur
正在运行的运行任务控制块任务调度的主要函数不就是在进行查表操作么!
void OSSched(void){
IN8U y;
OS_ENTER_CRITICAL();
if((OSLockNesting = 0)&&(OSIntNesting = 0)){
y = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (IN8U)(y<<3)+OSUnMapTbl[OSRdyTbl[y]]);
if (OSPrioHighRdy != OSPrioCur) {
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
OSCtxSwCtr++; //统计任务次数,跟踪任务切换次数
OS_TASK_SW(); //任务切换
}
}
OS_EXIT_CRITICAL();(开放中断的宏)
}
主要任务为:将被挂起的任务寄存器入栈,将较高优先级的任务寄存器出栈
关于OSIntEnter(void)
void OSIntEnter(void){
OS_ENTER_CRITICAL();
OSIntNesting++;
OS_EXTI_CRITICAL();
}
void OSIntExit(void){
OS_ENTER_CRITICAL();
if((--OSIntNesting | OSLockNesting)==0{
OSIntExity = OSUnMapTbl(OSRdyGrp);
OSPrioHighRdy = (IN8U)((OSIntExitY<<3)+OSUnMapTbl[OSRdyTbl[OSIntExitY]];
if(OSPrioHighRdy != OSPrioCur){
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
OSCtxSwCtr++;
OSIntCtxSw();
}
}
OS_EXTI_CRITICAL();
}
void OSTimeTick (void){
OS_TCB *ptcb;
OSTimeTickHook();
ptcb = OSTCBList;
while (ptcb->OSTCBPrio != OS_IDLE_PRIO){
OS_ENTER_CRITICAL();
if (ptcb->OSTCBDly != 0) {
if (--ptcb->OSTCBDly == 0) {
if (!(ptcb->OSTCBStat & OS_STAT_SUSPEND)) {
OSRdyGrp |= ptcb->OSTCBBitY;
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX;
}
}
}
其他任务:
OSTaskStkChk
()OSTaskDel
()OSTaskDelReq
()OSTaskChangePrio
()OSTaskSuspend
()OSTaskResume
()时间管理相关的函数
μC/OS中,采用多种方法保护任务之间的共享数据和提供任务之间的通信。
OSSchedLock
( )禁止调度保护任务级的共享资源。所有的通信信号都被看成是事件(event),一个称为事件控制块的数据结构来表征每一个具体事件
事件控制块TCB的操作:
OS_EventWaitListInit
();OS_EventTaskRdy
()OS_EventTaskWait
()OS_EventTO()
信号量在多任务系统中用于:控制共享资源的使用权、标志事件的发生、使两个任务的行为同步。
μC/OS中信号量由两部分组成:信号量的计数值和等待该信号任务的等待任务表。 的计数值可以为二进制, 也可以是其他整数。
系统通过OSSemPend
( )和OSSemPost
( )来支持信号量的两种原子操作P()和V()。P()操作减少信号量的值,如果新的信号量的值不大于0,则操作阻塞;V()操作增加信号量的值。
信号量操作:
OSSemCreate():建立一个信号量
OSSemDel(): 删除一个信号量
OSSemPend():等待一个信号量
OSSemPost():发出一个信号量
OSSemAccept():无等待地请求一个信号量
OSSemQuery()函数:查询一个信号量的当前状态
由于部分图片来源于ppt,想要深入学习的可私信我