特点:
开源、可移植、可固化、可裁剪?
占先式、多任务、可确定性、任务栈、系统服务、中断管理、稳定性可靠性
提供的系统服务:
信号量、带互斥机制的信号量(减少优先级倒置问题)、事件标志、消息信箱、信息队列、内存管理、时钟管理、任务管理
INT8U OSTaskCreate(
Void(*task)(void*pd).//任务代码指针
Void *pdata,//任务参数指针
OS_STK*ptos,//任务栈的栈顶指针
INT8U prio//任务优先级
);
OSTaskCreateExt()
Ucos-II可以管理64个任务,用户可以使用56个任务
内核总是创建一个空闲任务OSTaskIdle();其优先级为最低优先级,当所有其他任务未在执行时,空闲任务开始执行。并且应用程序不能删除该任务,空闲任务的工作是将32位的计数器OSIdleCtr加1,该计数器被系统任务使用。
统计任务OSTaskStat(),提供运行时间统计,每秒钟运行一次,计算当前CPU利用率
任务控制块存放任务的核心数据包括:任务堆栈指针、任务的状态、优先级、任务链表指针等
任务被建立时,任务控制块OS_TCB将被赋值。
图 1任务控制块结构体
OSTCBSTKPtr:指向当前任务栈顶的指针,每个任务可以有自己的栈,栈容量可以是任意的。
OSTCBStkBottom:执行任务栈底的指针
OSTCBStkSize:栈容量,用可容纳的指针数目而不是字节数(Byte)来表示
图 2栈
所有的任务控制块分属于两条不同的链表,单向的空闲链表(头指针为:OSTCBFreeList)和双向的使用链表(头指针为OSTCBList)
OSTCBNext、OSTCBPrev:用于将任务控制 块插入到空闲链表或使用链表中。每个任 务的任务控制块在任务创建的时候被链接 到使用链表中,在任务删除的时候从链表 中被删除。双向连接的链表使得任一成员 都能快速插入或删除
任务存在于内存空间但是内核不可见
可以通过OSTaskCreate()或OSTaskCreateExt()变成就绪态
可以通过OSTaskDel返回睡眠态
所有就绪的任务中,具有最高优先级的任务被选中运行
如果任务在运行时被抢占了CPU,则进入又回到就绪态
任务已经在CPU上运行
当一个任务在运行时,如果没有关闭中断,则有可能被中断打断
当一个任务在运行时,可能因为一些原因进入阻塞态
OSMBoxPend(),OSQPend(),OSSemPend(),OSTaskSuapend(),OSTimeDly()
该任务原来在CPU上运行,后来被中断所打断,有中断服务程序ISR接管CPU
当中断服务程序运行完毕后,内核要判断是否有新的、更高优先级的任务就绪,如果有,则原来的任务被抢占;如果没有,则原有的任务重新运行。
任务由于正在等待某个事件(信号量、邮箱或者队列)被挂起
当任务等待的事件发生时,就回到就绪状态。OSMBoxpost(),OSQPost(),
OSSemPost(),OSTaskResume(),OSTimeDlyResume(),OSTimeTick()
图 3任务状态转换
每个任务的就绪标志放入在就绪表中,就绪表中有两个变量OSRdyGrp和OSRdyTbl[]
在OSRdyGrp中,任务按优先级分组,8个任务一组,OSRdyGrp中的每一位表示8组任务中是否有进入就绪状态的任务。任务进入就绪状态,就绪表OSRdyTbl[]中的相应元素的相应位置也置位。
图 4任务就绪表
UCOS是可抢占实时多任务内核,总是运行在就绪任务中优先级最高的那一个
UCOS不支持时间片轮转,每个任务的优先级要求不一样且是唯一,所以任务调度的工作就是:查找准备就绪的最高优先级的任务并进行上下文切换。
UCOS任务调度所花的时间为常数,与应用程序中建立的任务数无关
确定哪个任务的优先级最高,应该选择哪个任务去运行是由调度器(Scheduler)来完成。
任务级的调度由函数OSSched()完成
中断级的调度由函数OSIntExt()完成
将优先级分解成高三位和低三位
高优先级有小的优先级号
图 5优先级计算
将被挂起任务的寄存器内容入栈
将较高优先级任务的寄存器内容出栈,恢复到硬件寄存器中。
通过sc系统调用指令完成
保护当前任务现场
恢复新任务的现场
执行中断返回指令
开始执行新的任务
图 6保护现场
图 7恢复现场
OSSchedlock:给调度器上锁,用于禁止任务调度,保持对CPU的控制权(即使有高优先级任务进入就绪态)
OSSchedUnlock:给调度器开锁,当任务完成后调用此函数,调度器重新得到允许
什么时候会上锁??
当低优先级的任务要发消息给多任务的邮箱、 消息队列、信号量时,它不希望高优先级的任 务在邮箱、队列和信号量还没有得到消息之前 就取得了CPU的控制权,此时,可以使用调度器上锁函数。
OSTaskCreate();
OSTaskCreateExt();
任务可以在多任务调度开始(OSStart())之前创建,也可以在其他任务的执行过程中被创建。但是在OSStart()被调用之前,用户必须至少创建一个任务。
不能再中断服务程序(ISR)中创建新任务。
OSTaskDel();
删除一个任务,其TCB会从所有可能的系统数据结构中移除,任务将返回并处于休眠状态(任务的代码还在)
如果任务正处于就绪状态,把它从就绪表中移除,这样以后就不会再调度执行
如果任务正处于邮箱、消息队列或信号量的等待队列中,也会被移除
将任务的OS_TCB从OSTCBList链表当中移动到OSTCBFreeList
OSTaskChangePrio():在程序运行期间,用户可以通过调用本函数来改变某个任务的优先级
OSTaskSuspend():挂起一个任务
如果任务处于就绪态,把它从就绪表中移除
在任务的TCB中设置OS_STAT_SUSPEND标志,表明该任务正在被挂起
OSTaskResume():回复一个任务
回复被OSTaskSuspend()挂起的任务
清除TCB中OSTCBStat字段的OS_STAT_SUSPEND位
OSTaskQuery():获得一个任务的有关信息
中断:由于某种事件的发生导致流程的改变。产生中断的事件称为中断源。
CPU响应中断的条件:
至少有一个中断源向CPU发出中断信号;
系统允许中断,且对此中断信号未屏蔽
中断一旦被识别,CPU会保持部分(或全部)运行上下文(context,即寄存器的值),然后跳转到专门的子程序去处理此事件,称为中断服务子程序(ISR)。
UCOS-II中,中断服务子程序要用汇编编写,如果用户使用的C语言编译器支持在线汇编的话,可以直接将中断服务子程序放在C语言的程序文件中。
(1)保存全部CPU寄存器的值;
(2)调用OSIntEnter(),或直接把全局变量OSIntNesting(中断嵌套层次)加1;
调用OSIntEnter()之前必须关闭中断
(3)执行用户代码做中断服务;
(4)调用OSIntExit();装入高优先级任务
OSIntExit()函数中任务切换,为什么使用OSIntCtxSw()而不用OS_TASK_SW()?
(5)恢复所有CPU寄存器
(6)执行中断返回指令
时钟节拍是一种特殊的中断,相当于操作系统的心脏起搏器
UCOS需要用户提供周期性信号源,用于实现时间延时和确认超时。节拍率应在10到100Hz之间,时钟节拍率越高,系统的额外负荷就越重。
时钟节拍的实际频率取决于用户程序的精度,时钟节拍源可以是硬件定时器,或者来自50/60Hz交流电源信号
Void OSTickISR(void)
{
}
与时间管理相关的系统服务:
任务延时函数,申请该服务的任务可以延时一段时间
调用OSTimeDLY后,任务进入等待状态
使用方法:
Void OSTimeDly(INT16U ticks);
Ticks:表示需要延时的时间长度,用时钟节拍的个数来表示
OSTimeDly()的另一个版本,即按时分秒时延函数
使用方法:
INT8U OSTimeDlyHMSM(
INT8U hours, // 小时
INT8U minutes, // 分钟
INT8U seconds, // 秒
INT16U milli // 毫秒 );
让处于延时时期的任务提前结束延时,进入就绪状态
使用方法:
INT8U OSTimeDlyResume (INT8U prio);
?prio表示需要提前结束延时的任务的优先级 /任务ID。
每隔一个时钟节拍,发生一个时钟中断,将一个32位的计数器OSTime加1
该计数器在用户调用OSStart()初始化多任务和4,294,967,295个节拍执行完一遍的时候从0开始计数。若时钟节拍频率等于100Hz,该计数器每隔497天就重新开始计数。
OSTimeGet():获得该计数器的当前值
OSTimeSet():设置该计数器的值
如果在OSStart之前启动定时器,则系统可能无法正确执行完OSStartHighRdy
OSStart函数直接调用OSStartHighRdy去执行最高优先级的任务,OSStart不返回
系统定时器应该在系统的最高优先级中启动
使用OSRunning变量来控制系统的运行
用户必须在多任务系统启动以后在开启时钟节拍,也就是在OSStart()之后
在调用OSStart()之后的第一件事就是初始化定时器中断
在调用uC/OS-II的任何其它服务之前,用户必须首先调用系统初始化函数OSInit()来初始化 mC/OS的所有变量和数据结构;
OSInit()建立空闲任务OSTaskIdle(),该任务总是处于就绪状态,其优先级一般被设成最低,即 OS_LOWEST_PRIO;如果需要,OSInit()还建立统 计任务OSTaskStat(),并让其进入就绪状态;
OSInit()还初始化了4个空数据结构缓冲区:空闲TCB链表OSTCBFreeList、空闲事件链表 OSEventFreeList、空闲队列链表OSQFreeList和 空闲存储链表OSMemFreeList。
多任务的启动是用户通过调用OSStart()实现的,然而,启动ucos-ii之前,用户至少要建立一个应用任务。
图 8ucos创建第一个任务
所有的通信信号都被看做是事件,usoc通过事件控制块(ECB)来管理每一个具体事件
图 9 事件控制块结构体
一个任务或ISR可以通过事件控制块(信号量、邮箱、消息队列)向另外的任务发信号
一个任务还可以等待另一个任务或者中断服务子程序给它发送信号,可以指定最长等待时间
多个任务可以同时等待同一个事件的发生,当该事件发生后,所有等待该事件的任务中优先级最高的任务得到该事件并进入就绪态,准备执行。
每个正在等待某个事件的任务被加入到该事件的ECB的等待任务列表中,该列表包含两个变量OEEventGrp和OSEventTbl[]
在OSEventGrp中,任务按优先级分组,8个任务一组,共8组,分别对应OSEventGrp当中的8位,当某组中有任务处于等待该事件的状态,对应的位就被置位,同时,OSEventTbl[]中的相应位也被置位。
图 10 任务等待列表
ECB的总数由用户所需要的信号量、邮箱和消息队列的总数决定,由OS_CFG.h中的#define OS_MAX_EVENTS定义
在调用OSInit()初始化系统时,所有的ECB被链接成一个单向链表---空闲事件控制块链表
每当建立一个信号量、邮箱或者消息队列时,就从该链表中取出一个空闲事件控制块,并对它初始化。
OSEventWaitListInit()
:初始化一个事件控制块。当创建一个信号量、邮箱或消 息队列时,相应的创建函数会调用本函数对ECB的内容进 行初始化,将OSEventGrp和OSEventTbl[]数组清零;
OSEventWaitListInit (OS_EVENT *pevent);
prevent:指向需要初始化的事件控制块的指针。
OSEventTaskRdy()
:使一个任务进入就绪态。当一个事件发生时,需要将其等待任务列表中的最高优先级任务置位就绪态
OSEventTaskRdy (OS_EVENT *pevent, void *msg, INT8U msk);
msg:指向消息的指针;msk:用于设置TCB的状态。
OSEventTaskWait()
:使一个任务进入等待状态。当某个任务要等待一个事件的发生时,需要调用本函数将该任务从就绪任务表中删 除,并放到相应事件的等待任务表中;
?OSEventTaskWait (OS_EVENT *pevent);
OSEventT0()
:由于等待超时而将任务置为就绪态。如果一个任务等待的事件在预先指定的时间内没有发生,需要调用本函数 将该任务从等待列表中删除,并把它置为就绪状态;
OSEventTO (OS_EVENT *pevent);
为了实现资源共享,操作系统必须提供临界区操作的功能
Ucos采用关闭/打开中断的方式来处理临界区代码,从而避免竞争条件,实现任务间的互斥。
Ucos定义两个宏来开关中断,OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()
这两个宏的定义取决于所用的微处理器,每种微处理器有自己的OS_CPU.H文件
图 11 任务1和任务2共享临界资源
当代码需要处理临界段代码时,需要关中断,处理完毕后再开中断;
关中断时间是实时内核最重要的指标之一;
在实际应用中,关中断的时间很大程度取决于微处理器的结构和编译器生成的代码质量;
三种方法:
OS_CRITICAL_METHOD==1
用处理器指令开关中断,开中断:OS_ENTER_CRITICAL(),关中断:OS_EXIT_CRITICAL()
OS_CRITICAL_METHOD==2
实现OS_ENTER_CRITICAL()时,先在堆栈中保存中断的开关状态,然后再关中断。
实现OS_EXIT_CRITICAL()时,从堆栈中弹出原来中断的开关状态
OS_CRITICAL_METHOD==3
把当前处理器的状态字保存在局部变量中,关中断时保存,开中断时恢复。
项目代码中使用的是OS_CRITICAL_METHOD==3
图 12 项目代码中的临界区保护
信号量在多任务系统中的功能
实现对共享资源的互斥访问(包括单个共享资源或者多个相同的资源)
实现任务间的行为同步
Ucos支持信号量需要在OS_CFG.H中将OS_SEM_EN开关置1
Ucos中的信号量由两部分组成:信号量的计数值(16位无符号整数)和等待该信号量的任务所组成的等待任务表
图 13 信号量
OSSEMCreate():
创建一个信号量,并对信号量的初始计数值赋值,该初始值为0到65535之间的一个数
OS_EVENT*OSSemCreate(INT16U cnt);cnt:信号量初始值
OSSemPend():
等待一个信号量,及操作系统中的P操作,将信号量减1;
OSSemPend (OS_EVENT *pevent, INT16U timeout, INT8U *err);
OSSemPost():
发送一个信号量,即操作系统的V操作,将信号量的值加1;
OSSemPost(OS_EVENT*parent);
OSSemAccept():
当一个任务请求一个信号量时,如果该信号量暂时无效,则让该任务简单地返回,而不是进入等待状态
OSSemQuery():
查询一个信号量的当前状态
低级通信:
只能传递状态和整数值等控制信息,传递的信息量小(信号量)
高级通信:
能够传递任意数量的数据(共享内存、邮箱、消息队列)
Ucos中的如何实现共享内存?
内存地址空间只有一个,为所有的任务所共享
为了避免竞争,需要使用信号量来互斥访问。
邮箱:一个任务或者ISR可以通过邮箱想另一个任务发送一个指针型变量,该指针包含特定“消息”的数据结构
需要在OS_CFG.H中将OS_MBOX_EN开关置为1,才能支持邮箱。
邮箱可能处于两种状态:
满状态、空状态
图 14 任务、邮箱、ISR
邮箱的系统服务:
OSMboxCreate()
OSMboxPost()
OSMboxPend()
OSMboxAccept()
OSMboxQuery()
消息队列可以使一个任务或ISR向另一个任务发送多个以指针方式定义的变量
在OS_CFG.H中将OS_Q_EN开关置1,使能消息队列,OS_MAX_QS定义系统支持的最多消息队列
一个消息队列可以容纳多个不同的消息,因此可把它看作是由多个邮箱组成的数组,只是它们共用一个等待任务列表
图 15 消息队列
图 16 消息队列数据结构
消息队列的系统服务:
OSQCreate():创建消息队列
OSQPend():等待消息队列中的消息
OSQAccept() :无等待地请求消息队列中的消息
OSQPost():以FIFO方式向消息队列发送一个消息
OSQPostFront() :以LIFO方式向消息队列发送一个消息
OSQFlush():清空一个消息队列
OSQQuery():查询一个消息队列的状态
Ucos不划分内核空间和用户空间,整个系统只有一个地址空间,即物理内存空间,应用程序和内 核程序都能直接对所有的内存单元进行访问;
系统中的“任务”,实际上都是线程–––只有运行 上下文和栈是独享的,其他资源都是共享的。
内存布局:代码段(text)、数据段(data)、bss段、堆 空间、栈空间;
内存管理是对堆管理
在ANSI C中可以用malloc()和free()两个函数动态地分配 内存和释放内存。在嵌入式实时操作系统中,容易产生碎片。
由于内存管理算法的原因,malloc()和free()函数执行时 间是不确定的。μC/OS-II 对malloc()和free()函数进行了 改进,使得它们可以分配和释放固定大小的内存块。这样 一来,malloc()和free()函数的执行时间也是固定的了
uC/OS采用的是固定分区的存储管理方法
μC/OS把连续的大块内存按分区来管理,每个分区包含有整数个大小相同的块;
??????? 在一个系统中可以有多个内存分区,这样,用户的应用程序就可以从不同的内存分区中得到不同大小的内存块。但是,特定的内存块在释 放时必须重新放回它以前所属于的内存分区;
图 17 内存分区
Ucos中使用内存控制块MCB(memeory control block)来跟踪每一个内存分区,系统中的每个内存分区都有自己的MCB。
图 18 内存控制块结构体
在OS_CFG.H中将开关量OS_MEM_EN置1,使能内存管理。
这样在系统初始化OSInit()时就会调用OSMemInit(),对内存管理器进行初始化,建立空闲的内存控制块链表。
OSMemCreate():创建一个内存分区
OSMemGet():分配一个内存块
从已经建立的内存分区中申请一个内 存块。该函数的唯一参数是指向特定内存分 区的指针。如果没有空闲的内存块可用,返 回NULL指针。
OSMenPut():释放一个内存块