ucOSII知识整理
目录
实时操作系统的概念
软实时|硬实时
前后台系统
多任务
任务状态
可剥夺型内核与不可剥夺型内核
任务间通讯、同步与临界段
如何实现优先级切换?
OSTCBY和OSTCBX
OSTCBBitX和OSTCBBitY
OSRdyGrp和OSRdyTbl[ ]
查询映射表获取最高优先级任务
系统启动与任务切换(汇编)
临界段
调度切换
如何实现任务切换
SysTick_Handler的优先级
软件定时器
任务间通讯机制
1、信号量
2、消息队列
3、消息邮箱
实时操作系统的概念
软实时|硬实时
实时这个概念很原始,就是我让你做什么你就做什么,不要让我看到一点拖延。就像火箭点火,不管你中间经过了多么复杂的过程,我想要的就是立刻点上火,不断地降低中间的延时,从1分钟到1秒,甚至到1ms以内。
软实时和硬实时的概念区分,其实就是应用原来越复杂了。如果只做一两件事,那么硬实时无疑是高效准确的,但是事情多了之后如果仍然硬实时而缺乏调度,总是会丢失对低优先级事件的响应。
前后台系统
RTOS的上一个阶段,是前后台系统。
main函数是一个while(1)无限循环,中断触发的服务程序被称为前台,while(1)循环中调用相应的函数完成处理称为后台。
这种系统在复杂应用中的响应时间是难以确定的,往往程序修改了,循环的整个时序都要受到影响。而且应用越复杂,总体响应时间就越长。
但在逻辑简单的产品中反而有效,因为系统响应时间可以确定为一个常数,如微波炉、电饭锅等产品。
多任务
RTOS相比前后台系统的优点,最明显的就是RTOS可以有多个后台,即多任务。
每个任务都设置触发条件或检测条件,条件不允许时任务闲置,多任务运行时由内核调度。
这样就可以做到,设计应用程序时就可以把问题分割成多个任务;每个任务作为一个单独的模块,实现一个输入输出的功能。
多任务间,既有相互完全独立的,也有相互协作的。
比如一个车辆监控设备,要实现一个CAN和GPS数据监控功能与车身状态监控功能。
它就可以拆分出来如下几个任务:
1、数据上传任务,负责对系统监控的周期性数据进行周期性上传,以及实时反馈异常数据;
2、CAN数据采集任务,GPS采集任务;
3、数据处理任务,负责对采集到的数据进行加工处理,并传递到上传任务;
4、系统监控任务,负责监控车辆异常状态,并做警报和UI显示;
任务状态
每个任务都是一个while(1)无限循环。
每个任务都处于5种状态之中,即休眠态、就绪态、运行态、挂起态和被中断态。
休眠态,顾名思义就是任务休眠之后就不被内核调度了;
就绪意味着任务运行条件已经满足,但还有高优先级任务正在执行;
运行态即当前任务正在执行;
挂起态又叫等待事件态,任务执行时需要等待特定事件的发生时进入此态;
被中断态,发生中断时所有任务进入被中断态。
可剥夺型内核与不可剥夺型内核
内核调度:可剥夺型内核与时间片轮转
大多数RTOS的内核都采用可剥夺型内核,即最高优先级任务一旦就绪,总能立即取得CPU的控制权,被中断的任务进入就绪态;
不可剥夺型内核,则是任务被中断打断后,CPU仍返回原来被中断的任务,接着执行,直到该任务完成,任务完成后自己调用内核服务函数释放CPU控制权。
时间片轮转调度中,允许任务的优先级相同,内核根据任务确定好的时间(额度)运行一段时间,然后切换给另一个任务。
ucOSII不支持时间片轮转调度算法,freeRTOS是支持的。
任务间通讯、同步与临界段
任务间的通讯本质上只有两种方式,全局变量和中断服务程序,信号量或消息队列都只是这种全程变量的不同表现形式。
那么为了保证“全程变量”的可靠性,就需要加一个保护(关闭中断),保证变量的修改不被打断,这个概念被称为“临界段”。
实时内核最重要的指标就是关闭中断用了多长时间(中断延迟),保存和回复CPU寄存器需要多长时间(中断响应时间和中断恢复时间)。
任务间除了通讯还有同步,任务间同步则依赖于时钟节拍(如同心脏脉搏),它是一种特定的周期性中断,它使得内核可以将任务延时划分成若干个时钟节拍,以及事件发生时的超时依据。时钟节拍频率越快,系统开销就越大。
如何实现优先级切换?
INT8U const OSUnMapTbl[256] = {0u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u,4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u,5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u,4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u,6u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u,4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u,5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u,4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u,7u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u,4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u,5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u,4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u,6u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u,4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u,5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u,4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u };
ucOS-ii内核最核心的就是上面的映射表 - OSUnMapTbl[256] ,该数组表示0~255中每个数的二进制值中从右边开始第一个出现1的位。
那么,ucOSII是如何实现任务调度的呢?又是如何用到这个映射表的?
首先,ucOSII的任务的最大个数固定为64(或128),同时任务的TCB表使用全局变量。
其次,ucOSII按照优先级进行调度,数值越低优先级越高。
//内核部分变量的定义:
//1、TCB成员变量
INT8U OSTCBPrio;
INT8U OSTCBX;
INT8U OSTCBY;
OS_PRIO OSTCBBitX;
OS_PRIO OSTCBBitY;//2、全局变量
OS_PRIO OSRdyGrp;
OS_PRIO OSRdyTbl[OS_RDY_TBL_SIZE];
INT8U OSPrioHighRdy;
其中,OSTCBPrio表示系统优先级,其最大数值为255,但ucOSII中只用了6个bit,即最大值为63(111 111)。
其它数值与OSTCBPrio有如下转换关系
OSTCBY和OSTCBX
64个任务可以分成8组,每组8个。
将优先级值得高三位转成OSTCBY(表示所在组),低三位转成OSTCBX(表示组内第几个)。
则就有3个关系式:
1:OSTCBPrio = OSTCBY << 3 + OSTCBX;
2:OSTCBY = OSTCBPrio >> 3;
3:OSTCBX = OSTCBPrio & 0x07;
根据关系式可知,64个任务分成8组后,同一组任务的OSTCBY相同,任务OSTCBPrio数值相差为8的倍数的OSTCBX相同。
OSTCBBitX和OSTCBBitY
4:OSTCBBitX = 1 << OSTCBX;
5:OSTCBBitY = 1 << OSTCBY;
这样就得到了一个属于每个任务都有的位标志量,标志着变更就绪状态时该置哪一位。
1)优先级数值最大为63时,OSTCBPrio用6个bit,OSTCBY和OSTCBX用3个bit,3个bit最大为7,所以OSTCBBitY和OSTCBBitX需要8个bit,即1个字节;
1)优先级数值最大为127时,OSTCBPrio用7个bit,OSTCBY和OSTCBX用4个bit,4个bit最大为15,所以OSTCBBitY和OSTCBBitX需要16个bit,即2个字节;
OSRdyGrp和OSRdyTbl[ ]
OSRdyGrp和OSRdyTbl[8]是系统调度中的就绪组和就绪表,每当有任务就绪或等待时,就只需要将对应位置1或清0。
6:任务就绪时将对应位置1
OSRdyGrp |= ptcb->OSTCBBitY;
OSRdyTbl[y] |= ptcb->OSTCBBitX;
7:任务等待时将对应位清0
OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX;
OSRdyGrp &= (OS_PRIO)~OSTCBCur->OSTCBBitY;
查询映射表获取最高优先级任务
8:每当有就绪状态变更时,自动计算出最高优先级
INT8U y = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((y << 3u) + OSUnMapTbl[OSRdyTbl[y]]);
由以上公式可以分析:
1)y值-OSUnMapTbl[OSRdyGrp]即最高优先级任务对应的OSTCBY值;
2)OSRdyTbl[y]即是所在组的8个任务的就绪状态,OSUnMapTbl[OSRdyTbl[y]])则是其中最高优先级任务的OSTCBX值;
由于OSUnMapTbl[256]表示0~255中二进制数值从右边开始第一个出现1的位,,那么无论是“就绪组OSRdyGrp”,还是“OSRdyTbl[y]”,只要代入(uin8_t的数值)进去就能得出最高优先级任务所在的Y或X值。
公式1~5,在任务初始化时就已经生效,即任务初始化时,先设置优先级,然后转化成另外四个变量常值。
公式6~8,在任务就绪状态变更时生效,且位操作效率高,特别是巧妙地通过一张静态表去查询,中间产生的这种执行时间,是可以根据芯片的指令周期去计算的,即确定为一个常数。
系统启动与任务切换(汇编)
临界段
#if OS_CRITICAL_METHOD == 3u
#define OS_ENTER_CRITICAL() {cpu_sr = OS_CPU_SR_Save();} //进入临界段-保存CPU寄存器
#define OS_EXIT_CRITICAL() {OS_CPU_SR_Restore(cpu_sr);} //退出临界段-恢复CPU寄存器
#endifOS_CPU_SR_SaveMRS R0, PRIMASK ;将状态寄存器的值送到R0通用寄存器,相当于读取该值CPSID I ;关闭IRQ中断(在PRIMASK=1的情况下),I代表IRQ(F对应FIQ)BX LR ;跳转到LR寄存器中的地址开始执行OS_CPU_SR_RestoreMSR PRIMASK, R0 ;将R0通用寄存器该值送回状态寄存器,读取之后什么也没做就返回 BX LR ;跳转到LR寄存器中的地址开始执行
调度切换
void SysTick_Handler(void)
{OSIntEnter();//进中断OSTimeTick();//节拍+1OSIntExit();//退中断HAL_IncTick();
}
void OSIntExit (void)
{ OS_CPU_SR cpu_sr = 0u;if (OSRunning == OS_TRUE) {OS_ENTER_CRITICAL();if (OSIntNesting > 0u) { OSIntNesting--;//退一次中断-1}if (OSIntNesting == 0u) { if (OSLockNesting == 0u) {//检查:存在中断和调度锁时不执行切换 OS_SchedNew();//上述公式8,根据索引表计算出最高优先级任务OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];//获取最高优先级任务对应的TCB指针if (OSPrioHighRdy != OSPrioCur) { OSCtxSwCtr++;//如果当前任务不是最高优先级任务,则执行切换,并统计上下文切换次数 OSIntCtxSw();}}}OS_EXIT_CRITICAL();}
}OSIntCtxSwLDR R0, =NVIC_INT_CTRL ;直接对寄存器赋值,写入中断控制寄存器地址 LDR R1, =NVIC_PENDSVSET ;NVIC_PENDSVSET-第28位置1,意即悬起PendSV中断 STR R1, [R0] ;将R1的值复制到以R0的值作为地址的内存中去BX LR ;跳转到R14寄存器所指向的地址
如何实现任务切换
ucoSII的实现方法是PendSV(可悬起系统调用),它有一个非常重要的特质:它可以缓期执行,即等待其它高优先级任务或中断完成之后才会执行。
在如下OS_CPU_A.ASM(OS的汇编代码的OS启动部分),可以看到,在OS启动之后直接将PENDSV的优先级设置为0XFF即最低。
这样就保证了:
请求任务切换之后(写1悬起PendSV异常),处理器直到所有其他异常或中断完成之后,才会进入PendSV中断处理函数。
(这也是为什么移植ucOSII时,要在stm32f4xx_it.c文件中,注释掉PendSV_Handler函数的原因,因为这个函数被定义到了这个汇编文件中)
(也是在PendSV_Handler这个函数中,真正的实现了寄存器入栈和出栈(保存与恢复)的操作,从而实现任务切换)
SysTick_Handler的优先级
与PendSV异常相关联的,就是SysTick_Handler的优先级设置,能注意到这个问题才证明你认真思考了。
SysTick_Handler的优先级一般两种做法,要么和PendSV一样设置为最低,要么最高。
最低则能保证优先处理中断产生的消息或数据,但是中断过于频繁时会阻塞RTOS的运行调度。
最高则能保证RTOS的实时切换与调度,但毕竟是1秒中断一次,可能会影响高频率下的其他中断处理。
一般的做法就是将二者都设置为最低,参考stm32cubxMX。
;********************************************************************************************************
; NVIC内核寄存器变量定义,用于悬起PendSV异常
;********************************************************************************************************NVIC_INT_CTRL EQU 0xE000ED04 ; 中断控制和状态寄存器地址
NVIC_SYSPRI14 EQU 0xE000ED22 ; 设置PendSV优先级的寄存器地址
NVIC_PENDSV_PRI EQU 0xFF ; 255,即将PendSV设置为最低优先级
NVIC_PENDSVSET EQU 0x10000000 ; 写1则悬起PendSV中断;********************************************************************************************************
; 启动多任务系统
;********************************************************************************************************OSStartHighRdyLDR R0, =NVIC_SYSPRI14 ; Set the PendSV exception priorityLDR R1, =NVIC_PENDSV_PRISTRB R1, [R0]MOVS R0, #0 ; Set the PSP to 0 for initial context switch callMSR PSP, R0LDR R0, =OS_CPU_ExceptStkBase ; Initialize the MSP to the OS_CPU_ExceptStkBaseLDR R1, [R0]MSR MSP, R1 LDR R0, =OSRunning ; OSRunning = TRUEMOVS R1, #1STRB R1, [R0]LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)LDR R1, =NVIC_PENDSVSETSTR R1, [R0]CPSIE I ; Enable interrupts at processor levelOSStartHangB OSStartHang ; Should never get here;********************************************************************************************************
; PendSV_Handler中断处理函数
; OS先请求上下文切换 --即悬起PendSV异常
; 异常悬起后(缓期执行即等待其他中断结束后才能进入),执行切换
;********************************************************************************************************PendSV_HandlerCPSID I ; Prevent interruption during context switchMRS R0, PSP ; PSP is process stack pointerCBZ R0, OS_CPU_PendSVHandler_nosave ; Skip register save the first timeSUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stackSTM R0, {R4-R11}LDR R1, =OSTCBCur ; OSTCBCur->OSTCBStkPtr = SP;LDR R1, [R1]STR R0, [R1] ; R0 is SP of process being switched out; At this point, entire context of process has been saved
OS_CPU_PendSVHandler_nosavePUSH {R14} ; Save LR exc_return valueLDR R0, =OSTaskSwHook ; OSTaskSwHook();BLX R0POP {R14}LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy;LDR R1, =OSPrioHighRdyLDRB R2, [R1]STRB R2, [R0]LDR R0, =OSTCBCur ; OSTCBCur = OSTCBHighRdy;LDR R1, =OSTCBHighRdyLDR R2, [R1]STR R2, [R0]LDR R0, [R2] ; R0 is new process SP; SP = OSTCBHighRdy->OSTCBStkPtr;LDM R0, {R4-R11} ; Restore r4-11 from new process stackADDS R0, R0, #0x20MSR PSP, R0 ; Load PSP with new process SPORR LR, LR, #0xF4 ; Ensure exception return uses process stackCPSIE IBX LR ; Exception return will restore remaining contextEND
软件定时器
软件定时器的意义:
适合对实时性要求不高的功能,比如控制led、蜂鸣器,任务检查或tick等等(要求严格的肯定是要上定时器的)。
软件定时器的如何触发的,参考OS_CPU_C.C文件中的OSTimeTickHook钩子函数:在硬件中断中调用ucOSII系统节拍处理函数,在这个函数有1个钩子函数,用于检测并推送定时器的信号量。
软件定时器的实现算法:
用到了一个时间轮的概念,它将定时器按照到期值的余数进行分组(组数OS_TMR_CFG_WHEEL_SIZE由用户定义,工程难度不大的用默认值就好)(形成一个链表),tick触发时先求余,求余相等的再去分组中检查tick值是否相等,这样大大缩短了处理时间。
任务间通讯机制
1、信号量
此信号量非彼信号量。
ucOS的信号量,本质上是一个单字节类型的用于计数的全局变量。
使用逻辑:任务阻塞性等待计数值,计数值大于1时OS会执行切换,执行或处理多少次由计数值的大小变化决定。
而其它地方,比如freeRTOS,一般是用来做资源管理的(当然ucOS也可以这样用),“必需”要设置信号量的上限值,即某任务使用共享资源时+1,另外一个任务访问时若>=上限值则不能访问,任务使用完毕时-1,才能去访问。
2、消息队列
消息队列的实现机制和方法在不同OS之间都大同小异。
本质上都是一个指针数组,存放的是指向一块内存区域的消息指针,一个队列结构。post时,将指针写入数组的尾部(也可以扩展实现插到队头),pend读取时取出队头的指针消息。
消息队列适用于任务间内部消息传递,以及数据安全性要求较低的外部消息传递。
比如,1个发送任务的发送消息队列,由内部推送;再比如,中断接收外部的JSON消息,可以用消息队列进行传递。
但是,当数据传递安全性较高时,(建议)可以使用信号量+数据队列的形式处理数据。比如中断源收到数据-先缓存起来,然后信号量通知线程去读取数据,防止出现粘包、断包。
3、消息邮箱
邮箱本质上是数组长度为1的消息队列。传递的是一个指针。
(邮箱+链表或队列)非常适合用来传递内部数据处理任务,比如一个CAN总线命令交互处理任务,一条命令执行一个功能;
那么,当收到一条命令时,将命令序列化为一个结构体指针,根据优先级或到期时间将指针压入链表,检查任务空闲状态,若空闲则提取第一条命令推送到处理任务进行执行。
ucOSII知识整理相关推荐
- python常用变量名_python基础知识整理
Python Python开发 Python语言 python基础知识整理 序言:本文简单介绍python基础知识的一些重要知识点,用于总结复习,每个知识点的具体用法会在后面的博客中一一补充程序: 一 ...
- Spring AOP 知识整理
为什么80%的码农都做不了架构师?>>> AOP知识整理 面向切面编程(AOP)通过提供另外一种思考程序结构的途经来弥补面向对象编程(OOP)的不足.在OOP中模块化的关键单元 ...
- Linux系统基础知识整理
一.说明 本篇文章,我将结合自己的实践以及简介,来对linux系统做一个直观清晰的介绍,使得哪些刚接触Linux的小伙伴可以快速入门,也方便自己以后进行复习查阅. 二.基本知识整理 1.Linux文件 ...
- 计算机二级c语基础知识,计算机二级C语基础知识整理.doc
计算机二级C语基础知识整理 1.1 算法 算法:是一组有穷指令集,是解题方案的准确而完整的描述.通俗地说,算法就是计算机解题的过程.算法不等于程序,也不等于计算方法,程序的编制不可能优于算法的设计. ...
- js事件(Event)知识整理
鼠标事件 鼠标移动到目标元素上的那一刻,首先触发mouseover 之后如果光标继续在元素上移动,则不断触发mousemove 如果按下鼠标上的设备(左键,右键,滚轮--),则触发mousedow ...
- Spring学习篇:IoC知识整理(一)
现在正通过spring的官方文档学习spring,将自己学习时的点点滴滴记录下来. Ioc知识整理(一): IoC (Inversion of Control) 控制反转. 1.bean的别名 我们每 ...
- 使用Aspose.Cells的基础知识整理
使用Aspose.Cells的基础知识整理 转自 http://www.cnblogs.com/kenblove/archive/2009/01/07/1371104.html 这两天用Aspose. ...
- 前端基础知识整理汇总(中)
前端基础知识整理汇总(中) Call, bind, apply实现 // call Function.prototype.myCall = function (context) {context = ...
- 前端基础知识整理汇总(上)
前端基础知识整理汇总(上) HTML页面的生命周期 HTML页面的生命周期有以下三个重要事件: 1.DOMContentLoaded -- 浏览器已经完全加载了 HTML,DOM 树已经构建完毕,但是 ...
最新文章
- 在线作图|2分钟在线绘制RDA图
- jQuery学习笔记——jQuery选择器详解种类与方法
- Autodesk布道GIS新理念
- 商淘多b2b2c商城系统怎么在个人电脑上安装_b2b2c商城系统免费模板怎么用?
- PHP 单一入口程序
- git删除远程分支文件,不改变本地文件
- 08 在C#程序中使用注释测试分析 1214
- 安卓开发之如何利用Intent对象,实现Activity和另一个Activity之间的跳转
- 旋转校正原理_【干货】全站仪水准器的检校原理及方法,值得学习!
- 橙子减肥法:好吃快速成为瘦美人 - 健康程序员,至尚生活!
- Grasshopper: Architectural Prototyping Grasshopper建筑原型 Lynda课程中文字幕
- 存储过程与函数的区别
- c语言基础课程包括啥,推荐收藏!C语言入门基础知识大全
- 好架构师都是写代码写出来的
- 复旦教授疯狂打车800次,发现大数据杀熟的秘密!附完整调研报告
- 基于HAL库的STM32F704的电阻式触摸屏的学习
- 《搜索和推荐中的深度匹配》——1.5 近期进展
- 多功能纺织品易燃性测试仪市场现状及未来发展趋势
- 数据分析真题日刷 | 网易2018校园招聘数据分析工程师笔试卷
- 筋斗云教程(一): 运行筋斗云应用