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寄存器
#endif

OS_CPU_SR_Save
    MRS     R0, PRIMASK     ;将状态寄存器的值送到R0通用寄存器,相当于读取该值
    CPSID   I               ;关闭IRQ中断(在PRIMASK=1的情况下),I代表IRQ(F对应FIQ)
    BX      LR              ;跳转到LR寄存器中的地址开始执行
 
OS_CPU_SR_Restore
    MSR     PRIMASK, R0     ;将R0通用寄存器该值送回状态寄存器,读取之后什么也没做就返回 
    BX      LR              ;跳转到LR寄存器中的地址开始执行

调度切换

void SysTick_Handler(void)
{
  OSIntEnter();//进中断
  OSTimeTick();//节拍+1
  OSIntExit();//退中断

  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();
    }
}

OSIntCtxSw
    LDR     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中断



;********************************************************************************************************
;                                         启动多任务系统
;********************************************************************************************************

OSStartHighRdy
    LDR     R0, =NVIC_SYSPRI14                                  ; Set the PendSV exception priority
    LDR     R1, =NVIC_PENDSV_PRI
    STRB    R1, [R0]

    MOVS    R0, #0                                              ; Set the PSP to 0 for initial context switch call
    MSR     PSP, R0

    LDR     R0, =OS_CPU_ExceptStkBase                           ; Initialize the MSP to the OS_CPU_ExceptStkBase
    LDR     R1, [R0]
    MSR     MSP, R1    

    LDR     R0, =OSRunning                                      ; OSRunning = TRUE
    MOVS    R1, #1
    STRB    R1, [R0]

    LDR     R0, =NVIC_INT_CTRL                                  ; Trigger the PendSV exception (causes context switch)
    LDR     R1, =NVIC_PENDSVSET
    STR     R1, [R0]

    CPSIE   I                                                   ; Enable interrupts at processor level

OSStartHang
    B       OSStartHang                                         ; Should never get here


;********************************************************************************************************
;                                       PendSV_Handler中断处理函数
; OS先请求上下文切换 --即悬起PendSV异常
; 异常悬起后(缓期执行即等待其他中断结束后才能进入),执行切换
;********************************************************************************************************

PendSV_Handler
    CPSID   I                                                   ; Prevent interruption during context switch
    MRS     R0, PSP                                             ; PSP is process stack pointer
    CBZ     R0, OS_CPU_PendSVHandler_nosave                     ; Skip register save the first time

    SUBS    R0, R0, #0x20                                       ; Save remaining regs r4-11 on process stack
    STM     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_nosave
    PUSH    {R14}                                               ; Save LR exc_return value
    LDR     R0, =OSTaskSwHook                                   ; OSTaskSwHook();
    BLX     R0
    POP     {R14}

    LDR     R0, =OSPrioCur                                      ; OSPrioCur = OSPrioHighRdy;
    LDR     R1, =OSPrioHighRdy
    LDRB    R2, [R1]
    STRB    R2, [R0]

    LDR     R0, =OSTCBCur                                       ; OSTCBCur  = OSTCBHighRdy;
    LDR     R1, =OSTCBHighRdy
    LDR     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 stack
    ADDS    R0, R0, #0x20
    MSR     PSP, R0                                             ; Load PSP with new process SP
    ORR     LR, LR, #0xF4                                       ; Ensure exception return uses process stack
    CPSIE   I
    BX      LR                                                  ; Exception return will restore remaining context

    END

软件定时器

软件定时器的意义:

适合对实时性要求不高的功能,比如控制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总线命令交互处理任务,一条命令执行一个功能;

那么,当收到一条命令时,将命令序列化为一个结构体指针,根据优先级或到期时间将指针压入链表,检查任务空闲状态,若空闲则提取第一条命令推送到处理任务进行执行。

  • 3
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猪熊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值