UCOSIII 系统(STM32任务管理)学习笔记


作者:瓴
时间:2021.07.28


UCOSIII 系统(STM32任务管理)学习笔记

UCOSIII 系统学习笔记

————————————————
说明:本文为个人学习UCOSIII过程中时所做的笔记,主要对UCOSIII中关键内容进行记述,所以较建议为了快速了解UCOSIII系统内容的用户阅读。对于其中的函数,仅选取了方便理解系统整体结构内容的函数进行了记录,如果需要详细使用和学习理解,建议配合正点原子的《 STM32F4 UCOS开发手册_V3.0 》进行学习,此文档可以从正点原子官网http://www.openedv.com/posts/list/38349.htm下载。

个人对UCOSIII的学习理解为,UCOSIII系统主要功能是基于链表对任务进行链接,优先级对任务进行决策和定时器、信号量、消息队列、标志组等对任务进行管理。
举个例子,比如一个机器人比赛规则为机器人需移动到不同5个位置分别射箭,如果不使用UCOSIII,那么为了保证移动过程中的精度和稳定,我们得尽量让驱动电机和读取码盘数据这两个动作时间短且来回切换,在程序中除了编写这两个动作程序,还要编写大量来回切换的程序。如果使用UCOSIII,我们仅需要两个动作的程序,然后用时间片轮转调度进行管理即可,且代码量不会随两个动作切换次数而增加。这仅是移动过程,加上射箭,取箭和其他各种辅助传感器的等动作交织在一起,动作越多越复杂,那么UCOSIII的优势将会越明显。

————————————————

一、UCOSIII 任务管理

1、任务状态

UCOSIII 支持的是单核 CPU,不支持多核 CPU,某一时刻只有一个任务会获得 CPU 使用权进入运行态,其他的任务就会进入其他状态,UCOSIII 中的任务有多个状态,其中包括休眠态,就绪态,运行态,等待态,中断服务态。

任务状态描述
休眠态休眠态就是任务只是以任务函数的方式存在,只是储存区中的一段段代码,并未用 OSTaskCreate()函数创建这个任务。
就绪态任务在就绪表中已经登记,等待获取 CPU 使用权。
运行态正在运行的任务处于运行态。
等待态正在运行的任务需要等待某一个事件,比如信号量、消息、事件标志组等,就会暂时让出 CPU 使用权,进入等待事件状态。
中断服务态一个正在执行的任务被中断打断,CPU 转而执行中断服务程序,这时这个任务就会挂起,进入中断服务态。

2、任务控制块 OS_TCB

任务控制块 OS_TCB 用来保存任务的信息,我们使用 OSTaskCreate()函数来创建任务的时候就会给任务分配一个任务控制模块。任务控制块是一个结构体。

3、任务堆栈 TASK_STK[]

任务堆栈用来切换任务和调用其他函数的时候保存现场的,每个任务都有自己的堆栈。我们使用 OSTaskCreat()函数创建任务的时候就可以把创建的堆栈传递给任务。

在 UCOSIII 中用 CPU_STK 数据类型来定义任务堆栈,其实 CPU_STK 就是CPU_INT32U,可以看出一个 CPU_STK 变量为 4 字节,因此任务的实际堆栈大小应该为我们定义的 4 倍。例如 CPU_STK TASK_STK[64]; //定义一个 TASK_STK 任务堆栈。堆栈大小为 64*4=256 字节。

EQU 和 C 语言中的宏定义#define 一样,用于定义一个宏。例如 NVIC_INT_CTRL EQU 0XE000ED04。

4、任务就绪表

UCOSIII 将已经就绪的任务放在就绪表里,包括优先级位映表 OSPrioTbl[]、就绪任务列表 OSRdyList[]。

(1)、优先级位映表 OSPrioTbl[]

优先级位映表 OSPrioTbl[]用来标记哪些任务就绪了。

当某一任务就绪后会将优先级位映射表 OSPrioTbl[]中相应的位置置 1。依次查询数组的元素,当某个元素为 0 时,就继续扫描下一元素,不为 0 就继续执行该元素对应的任务。优先级位映射表中从左到右优先级逐渐降低,同时每个 OSPrioTbl[]数组的元素最高优先级位在左边,最低优先级在右边。这样做主要是为了支持使用一条特殊的指令“计算前导零(CLZ)”,从而可以快速的找到最高优先级任务。
在这里插入图片描述

(2)就绪任务列表 OSRdyList[]

就绪任务列表 OSRdyList[]主要用来标记哪些任务就绪了。结构体定义如下

struct os_rdy_list {
OS_TCB *HeadPtr; //用于创建链表,指向链表头
OS_TCB *TailPtr; //用于创建链表,指向链表尾
OS_OBJ_QTY NbrEntries; //此优先级下的任务数量
}

UCOSIII 支持时间片轮转调度,因此同一个优先级下会有多个任务,所以 UCOSIII 的就绪任务列表 OSRdyList[]数组中每一个元素对应管理一个优先级下的所有任务,比如OSRdyList[3]就用来管理优先级 3 下的所有任务。

注:有些优先级下只能有一个任务,比如 UCOSIII 自带的 5 个系统任务:
空闲任务 OS_IdleTask()
时钟节拍任务OS_TickTask()
统计任务 OS_StatTask()
定时任务 OS_TmrTask()
中断服务管理任务 OS_IntQTask()。

同一个优先级下的所有任务是通过链表来管理的,OS_TCB 是用来构造链表的,HeadPtr 和 TailPtr 分别指向这个链表的头和尾,NbrEntries 用来记录此优先级下的任务数量,如下表示优先级 4 下 3 个任务的就绪任务列表。
在这里插入图片描述

5、任务调度和切换

任务调度和切换就是让就绪表中优先级最高的任务获得 CPU 使用权。UCOSIII 是可剥夺型、抢占式的,高优先级任务可以抢了低优先级任务的 CPU 使用权。

(1)、可剥夺型调度

任务调度是由任务调度器完成的,任务调度器有两种:任务级调度器 OSSched()和中断级调度器 OSIntExit()。

a、任务级调度器
任务级调度器函数为 OSSched(),在 OSSched()中调用 OSCtxSw()实现任务切换,所以真正执行任务切换的是宏 OS_TASK_SW(),如下:

#define OS_TASK_SW() OSCtxSw()//调用该宏和函数的满足条件就会发生任务切换
OSCtxSW()要做的就是将当前任务的 CPU 寄存器的值保存在任务堆栈中,也就是保存现场,保存完当前任务的现场后将新任务的 OS_TCB 中保存的任务堆栈指针的值加载到CPU 的堆栈指针寄存器中,最后还要从新任务的堆栈中恢复 CPU 寄存器的值。

b、中断级调度器
任务级调度器函数为 OSIntExit(),在中断级调度器中真正完成任务切换的就是中断级任务切换函数 OSIntCtxSW(),与任务级切换函数 OSCtxSW()不同的是,由于进入中断的时候现场已经保存过了,所以 OSIntCtxSW()不需要像 OSCtxSW()一样先保存当前任务现场,只需要做 OSCtxSW()的后半部分工作,也就是从将要执行的任务堆栈中恢复 CPU 寄存器的值。

(2)时间片轮转调度

UCOSIII 支持多个任务同时拥有一个优先级,要使用这个功能我们需要定义OS_CFG_SCHED_ROUND_ROBIN_EN 为 1,在 UCOSIII 中允许一个任务运行一段时间(时间片)后让出 CPU 的使用权,让拥有相同优先级的下一个任务运行,这种调度方法称为时间片轮转调度。
在这里插入图片描述
1)任务 3 运行完相应时间片,任务 3 还没运行完。
2)UCOSIII 切换到任务 1 运行完其相应时间片,任务 1 还没运行完。
3)切换到任务 2 运行完相应时间片,任务 2 还没运行完。
4)切换到任务 3 运行完相应时间片,任务 3 还没运行完。
5)切换到任务 1 运行,任务 1 在其时间片内任务运行结束,于是切换到任务 2。
6)任务 2 运行完相应时间片,切换到任务 3 运行。

当某一优先级下有多个任务时,每次任务切换后运行的都是处于就绪任务列表OSRdyList[]链表头的任务,当这个任务的时间片用完后这个任务就会被放到链表尾,然后再运行新的链表头的任务。这些任务是这样被调度和运行的。

6、系统时钟函数 OSTimeTick()

系统时钟,是处理器运行时间基准(每一条机器指令一个时钟周期),时钟是单片机运行的基础,时钟信号推动单片机内各个部分执行相应的指令。

UCOSIII 需要一个系统时钟来对任务进行整个节拍的延迟,并为等待事件的任务提供超时判断。我们通过调用 OSTimeTick()函数来为系统提供时钟。

7、临界代码保护

为了保护一些代码完成运行,不被打断。这些不能被打断的代码称为临界段代码,也叫临界区。

使用宏 OS_CRITICAL_ENTER()进入临界区代码;使用宏 OS_CRITICAL_EXIT()和
OS_CRITICAL_EXIT_NO_SCHED()退出临界区代码。

在 UCOSIII 中可以通过关闭中断和任务调度器上锁两种方式来保护临界段代码。

OS_CFG_ISR_POST_DEFERRED_EN 为 0,使用关中断的方式进入临界区;
OS_CFG_ISR_POST_DEFERRED_EN 为 1,使用锁定调度器方式进入临界区。

OS_CFG_ISR_POST_DEFERRED_EN 在 os_cfg.h 文件中定义。

二、任务相关 API 函数的使用

1、任务创建和删除实验

(1)、任务创建函数 OSTaskCreate()

任务创建函数 OSTaskCreate()就是将任务控制块 0S_TCB、任务堆栈 TASK_STK[]、和我们自己编写的任务代码联系在一起,并且初始化任务控制块的相应字段。刚创建的任务就会进入就绪态,因此不能在中断服务程序中调用 OSTaskCreate()函数创建任务。创建任
务设置优先级时,不能设置为和 UCOSIII 自带系统任务相同的优先级!

(2)、任务删除函数 OSTaskDel()

任务删除函数 OSTaskDel()用于删除不需要运行的任务,删除任务只是 UCOSIII 不再管理该任务,并不是删除该任务代码。

尽量避免任务运行时删除任务,如果多个任务使用同一共享资源,任务 A 正在使用这个资源,如果此时删除了任务 A,这个共享资源就不会被释放,那么其他任务将得不到这个共享资源的使用权。

我们调用 OSTaskDel()删除一个任务后,这个任务的任务堆栈、OS_TCB 所占用的内存并没有释放掉,因此我们可以利用它们用于其他的任务,当然我们也可以使用内存管理的方法给任务堆栈和 OS_TCB 分配内存,这样当我们删除掉某个任务后我们就可以使用内存释放函数将这个任务的任务堆栈和 OS_TCB 所占用的内存空间释放掉。

2、任务的挂起与恢复

(1)任务挂起函数 OSTaskSuspend()

有时有些任务因为某些原因需要暂停运行,但是后面还要运行,因此我们就不能删除掉任务。于是我们可以使用 OSTaskSuspend()函数暂停挂起某任务,以便以后恢复再运行。

我们可以多次调用 OSTaskSuspend ()函数来挂起一个任务,同时我们也需要调用同样次数的 OSTaskResume()函数才可以恢复被挂起的任务。

(2)、任务恢复函数 OSTaskResume()

任务恢复函数 OSTaskResume()用于恢复被挂起的任务,OSTaskResume()是唯一能恢复被挂起任务的函数。

如果被挂起的任务还在等待别的内核对象,比如事件标志组、信号量、互斥信号量、消息队列等,即使使用 OSTaskResume()函数恢复了被挂起的任务,该任务也不一定能立即运行,该任务还是要等相应的内核对象,只有等到内核对象后才可以继续运行。

3、时间片轮转调度函数

UCOSIII 支持多个任务拥有相同优先级,这些任务采用时间片轮转调度方法进行任务调度 。 要 想 采 用 时 间 片 轮 转 调 度 , 就 需 将 os_cgf.h 文 件 中 的 宏OS_CFG_SCHED_ROUND_ROBIN_EN 定义为 1。这样时间片轮转调度的代码才会被编译。否则不能使用时间片轮转调度。

(1)OSSchedRoundRobinCfg()函数

OSSchedRoundRobinCfg()函数用来使能或失能 UCOSIII 的时间片轮转调度功能。

(2)OSSchedRoundRobinYield()函数

当一个任务想放弃本次时间片,OSSchedRoundRobinYield()函数用来把 CPU 使用权让给同优先级的另一任务。
调用该函数后遇到最多的错误是OS_ERR_ROUND_ROBIN_1,即当前优先级下没有就绪任务。

三、USCOSIII 系统内部任务

UCOSIII 中系统内部任务有 5 个,分别为:空闲任务 OS_IdleTask()、时钟节拍任务OS_Ticktask()、统计任务OS_StatTaskInit()、定时任务 OS_TmrInit()、终端服务管理任务 OS_IntQTask()。

1、空闲任务 OS_IdleTask()

空闲任务 OS_IdleTask()是必须创建的,不过在调用 OS_Init()初始化 UCOS 的时候就会被创建,不需要手动创建。同时空闲任务优先级是最低的。

空闲任务的作用:在所有应用任务都进入等待态的时候,CPU 可以执行空闲任务,从而一直工作。

(1)钩子函数 OSIdleTaskHook()

空闲任务中还调用了钩子函数 OSIdleTaskHook(),钩子函数主要用来对某些任务进行扩展的。要使用钩子函数需要将宏 OS_CFG_APP_HOOKS_EN 置 1。

如果我们想要在钩子函数中执行一些功能,我们可以将代码写在 App_OS_IdleTaskHook
()函数中。注:在空闲任务的钩子函数中不能调用任何可以使空闲进入等待态的代码。

原因:CPU 总是在不停的运行,需要一直工作,CPU 不能停下来,哪怕是执行一些对应用没有任何用的代码,比如简单的将一个变量加一。在 UCOS 中为了让 CPU 一直工作,在所有应用任务都进入等待态的时候 CPU 会执行空闲任务。

我们可以从空闲任务的任务函数OS_IdleTask()看出,在 OS_IdleTask()中没有任何可以让空闲任务进入等待态的代码。如果在 OS_IdleTask()中有可以让空闲任务进入等待态的代码的话,有可能会在同一时刻使得所有任务(应用任务和空闲任务)同时进入等待态,此时CPU 就会无事可做了。

UCOSIII 一共拥有 8 个钩子函数,包括空闲任务的钩子函数。

函数名功能
OSIdleTaskHook()空闲任务调用这个函数,可以用来让 CPU 进入低功耗模式。
OSInitHook()系统初始化函数 OSInit()调用此函数。
OSStatTaskHook()统计任务每秒中都会调用这个函数,此函数允许你向统计任务中添加自己的应用函数。
OSTaskCreateHook()任务创建的钩子函数。
OSTaskDelHook()任务删除的钩子函数。
OSTaskReturnHook()任务意外返回时调用的钩子函数,比如删除某个任务
OSTaskSwHook()任务切换时候调用的钩子函数。
OSTimeTickHook()滴答定时器调用的钩子函数。

2、时钟节拍任务 OS_Ticktask()

时钟节拍任务 OS_Ticktask()也是必须创建的,同样不需要手动创建,调用 OSInit()初始化函数时就会被创建。时钟节拍任务默认优先级为 1。

时钟节拍任务的作用:跟踪正在延时的任务、指定时间内等待某个内核对象的任务。

3、统计任务 OS_StatTaskInit()

统计任务 OS_StatTaskInit()默认情况下不会创建,如果要使能统计任务需要将OS_CFG_STAT_TASK_EN 置 1。OS_StatTaskInit()函数用来创建统计任务。统计任务优先级倒数第二。

统计任务的作用:用来统计 CPU 使用率、各任务的 CPU 使用率和堆栈使用情况。

如果要使用统计任务就需要在 main()函数创建的第一个也是唯一一个应用函数(如start_task()任务)中调用OSStatTaskCPUUsageInit()函数。

4、定时任务 OS_TmrInit()

UCOSIII提供软件定时器功能,如果要使能定时任务需要将宏OS_CFG_TMR_EN 置1。定时任务可选择。同样调用 OSInit()初始化函数时,OSInit()中会调用 OS_TmrInit()函数创建定时任务。定时任务默认优先级为 2。

5、中断服务管理任务 OS_IntQTask()

如果要使能中断服务管理任务需要将 OS_CFG_ISR_POST_DEFERRED_EN 置 1。OSInit()中会调用 OS_TmrInit()函数创建中断服务管理任务。中断服务管理任务的优先级为 0,是最高的。

中断服务管理任务 OS_IntQTask()的作用:该任务负责“延迟”在中断服务函数 ISR 中调用的系统 post 服务函数的行为。

调度器上锁的方式来管理临界段代码时,在中断服务函数中调用的“post”类函数就不允许操作诸如任务就绪表、等待表等系统内部数据结构。

当中断服务函数调用 post 函数时,会将要发送的数据和发送的目的地存入一个特别缓冲队列。当所有中断服务函数都执行完之后,UCOSIII 会做任务切换,运行中断服务管理任务会把刚才存入缓冲队列的信息重新发给相应任务。这样就可以减少关闭中断的时间,否则还需要把任务从等待列表中删除,然后把任务放入就绪表中等耗时操作。

四、UCOSIII 中断的和时间管理

1、中断管理

(1)、UCOSIII 中断处理过程

STM32 中是支持中断的,中断是一个硬件机制,主要用来向 CPU 通知一个异步事件
发生了。当一个中断发生时,CPU 会将当前 CPU 寄存器值存入栈中,然后去执行中断服务程序,在此期间如果有更高优先级就绪,退出中断后去执行这个更高优先级的任务。

UCOSIII 支持中断嵌套,最大支持 250 级的中断嵌套。UCOSIII 中使用 OSIntNestingCtr
记录嵌套次数,每进入一次中断加 1,退出一次减 1。

编写 UCOSIII 中断服务函数时,需要使用 OSIntEnter()和 OSIntExit()函数,这两个函数对 OSIntNestingCtr 进行相应增减处理,从而记录中断嵌套次数。中断函数如下:

void XXX_Handler(void) 
{ 
OSIntEnter(); //进入中断
/*用户自行编写的中断服务程序;*/
OSIntExit(); //触发任务切换软中断
}

XXX_Handler(void)为不同中断源的中断函数名字,可打开 startup_stm32f10x_hd.s 文件查询需要使用的中断名。

(2)、直接发布和间接发布

UCOSIII 对中断发布的信息或者信号的处理有两种模式:直接发布和延迟发布两种方式。
通过宏OS_CFG_ISR_POST_DEFERRED_EN 来选择模式,为 0 时直接发布模式,为 1 时延迟发布模式。

直接发布模式下,UCOSIII 通过关闭中断来保护临界段代码;
延迟发布模式下,UCOSIII 通过锁定任务调度来保护临界段代码。在延迟发布模式下,UCOSIII 在访问中断队列时,仍然需要关闭中断,但这个时间是非常短的。

如果应用中存在非常快速的中断源,当 UCOSIII 直接模式下中断模式关闭时间不满足要求,可以用延迟发布来降低中断关闭时间。

(3)、时钟节拍中断

UCOSIII 需要一个系统时钟节拍作为系统心跳,这个时钟我们一般都使用 MCU 的硬件定时器。Cortex-M 内核提供了一个定时器用于产生系统钟节拍 ,这个定时器就是 Systick。UCOSIII 通过时钟节拍来对任务进行整个的延迟,并为等待事件通过时钟节拍来对任务进行整个节拍的延迟,并为等待事件提供超时判断。

时钟节拍中断必须调用 OSTimeTick()函数,我们使用 Systick 来为系统提供时钟,因此在 Systick 的中断服务程序就必须调用 OSTimeTick() 。

时钟节拍中断服务程序首先会调用钩子函数 OSTimeTickHook() ,这个函数中用户可以放置一些代码。

2、时间管理

(1)、OSTimeDly()函数

OSTimeDly()函数用来对一个任务进行延时操作。

延时时间的单位为时间节拍数,如我们设置系统时间频率 OSCfg_TickRate_Hz 为 200Hz,则每个时钟节拍就是 5ms 的时间长度。

(2)、OSTimeDlyHMSM()函数

OSTimeDlyHMSM()函数来更加直观的来对某个任务延时。

延时时间单位为小时、分钟、秒、毫秒的格式。但这个延时最小单位和我们设置的时钟
节拍频率有关,比如我们设置时钟节拍频率 OSCfg_TickRate_Hz 为 200Hz 的话,那么最小延时单位就是 5ms。

补:上述延时函数执行时,会把当前任务挂起,从而任务进入中断服务态。延时一段时间之后再将任务转为就绪态。若任务 1 使用上述函数延时 1s,延时期间发生任务调度,但调度后的任务 2 执行到 1s 时还未执行完成,此时会先将任务 1 转为就绪态,但任务 2 占有CPU 使用权继续运行。任务 1 继续等待至任务 2 运行完成后,获得 CPU 使用权后继续运行。

delay_xus()延时函数是通过 us 延时函数实现,不会发生调度;ms 延时函数里面延时如果超过 OS 的最少时间周期,使用的是系统的延时函数实现,会引发调度;延时低于 OS 的最少时间周期,仍是使用 us 延时函数实现,不引起调度。delay_ms 延时程序如下。

void delay_ms(u16 nms)
{
if(delay_osrunning&&delay_osintnesting==0) //如果 OS 已经在跑了,并且不是在中
断里面(中断里面不能任务调度) 
{
if(nms>=fac_ms) //延时的时间大于 OS 的最少时间周期
{ 
 delay_ostimedly(nms/fac_ms); //OS 延时
}
nms%=fac_ms; //OS 已经无法提供这么小的延时了,采用普通方式延时
}
delay_us((u32)(nms*1000)); //普通方式延时
} 
(3)、其他有关时间的函数

a、OSTimeDlyResume()函数

一个任务可以通过调用 OSTimeDlyResume()函数来“解救”那些因为调用了 OSTimeDly()或者 OSTimeDlyHMSM()函数而进入等待态的任务。

b、OSTimeGet()和 OSTimeSet()函数

OSTimeGet()函数用来获取当前时钟节拍计数器的值。OSTimeSet()函数可以设置当前时钟节拍计数器的值,这个函数谨慎使用。

五、UCOSIII 软件定时器

UCOSIII 中提供了软件定时器,定时器的本质是计数器减到零时,可以触发某种动作的执行,这个动作通过回调函数来实现。

1、定时器工作模式

当定时器计时完成时就会自动调用回调函数。如果我们要使用定时器需要将宏OS_CFG_TMR_DEL_EN 定义为 1 。 UCOSIII 中 定 时 器 的 时 间 分 辨 率 由 一 个 宏OS_CFG_TMR_TASK_RATE_HZ 定义,默认为 100Hz。如我们定义为 200Hz,系统时钟周期为 5ms。

2、定时器相关 API 函数

(1)创建定时器函数 OSTmrCreate(),函数原型如下。
void OSTmrCreate (OS_TMR *p_tmr,
 CPU_CHAR *p_name,
 OS_TICK dly,
 OS_TICK period,
 OS_OPT opt,
 OS_TMR_CALLBACK_PTR p_callback,
void *p_callback_arg,
 OS_ERR *p_err)
/*p_tmr: 指向定时器的指针,宏 OS_TMR 是一个结构体。
p_name: 定时器名称。
dly: 初始化定时器的延迟值。
period: 重复周期。
opt: 定时器运行选项,这里有两个模式可以选择。
OS_OPT_TMR_ONE_SHOT 单次定时器
OS_OPT_TMR_PERIODIC 周期定时器
p_callback: 指向回调函数的名字。
p_callback_arg: 回调函数的参数。
p_err: 调用此函数以后返回的错误码。*/

可见定时器有单次定时器和周期定时器两种模式。单次定时器在调用 OSTmrStart()函数后开始倒计数,将 dly 减为 0 后调用回调函数,单次定时器只执行一次就停止运行。我们可以调用 OSTmrStop()函数来删除这个运行完成的定时器。其实我们也可以重新调用OSTmrStart()函数来重新触发单次定时器。

周期定时器在调用 OSTmrStart()函数后开始倒计数,将 dly 减为 0 后调用回调函数,并重置计数器重新开始计时,一直循环下去。若OSTmrCreate()创建周期定时器时的参数 dly为 0,则无初始延迟,那么定时器每个周期为 period。若参数 dly 不为 0,则有初始延迟,第一个周期就是 dly,之后的周期才为 period。
在这里插入图片描述
在这里插入图片描述

(2)、其他定时器函数
函数描述
OSTmrCreate()创建定时器。
OSTmrStart()函数启动定时器计数,无论定时器计时完成或正在运行,调用该函数都会重新触发定时器。
OSTmrStop()停止计数器倒计时。
OSTmrDel()删除定时器。
OSTmrStateGet()获取当前定时器状态。
OSTmrRemainGet()获取定时器的剩余时间。

六、UCOSIII 信号量和互斥信号量

UCOSIII 中可能会有多个任务访问共享资源,同时对这个共享资源操作时会出错,因此信号量最早是用来控制任务存取共享资源。现在信号量被用来实现任务之间的同步和 ISR 之
间的同步。

在可剥夺的内核中,当任务独占式使用共享资源时就会出现低优先级任务先于高优先任务运行的现象,这个现象称之为优先级反转现象。为了解决这个问题,引出了互斥信号量的概念。

1、信号量

信号量像是一种上锁机制,一旦执行至被锁代码段,则任务一直等待,直到对应被锁部分代码的钥匙被再次释放,代码获得对应的钥匙继续执行。一旦获得了钥匙,也就意味着该任务具有进入被锁部分代码的权限。
信号量分为两种:二进制信号量与计数型信号量。

在共享资源中只有任何可以使用信号量,中断服务程序则不能使用。

(1)二进制信号量

二进制信号量只能取 0 和 1 两个值。某一资源对应的信号量为 1 的时候,那么就可以使用这一资源,如果对应资源的信号量为 0,那么等待该信号量的任务就会被放进等待信号量的任务表中。同时等待信号量的任务也可设置为超时,如果超过设定的时间任务没有等到信号量的话那么该任务就会进入就绪态。

(2)计数型信号量

计数型信号量可以取不止 2 个值。我们可以同时设置多个信号量,允许有多个任务访问共享资源。比如某一个信号量初始化值为 10,那么只有前 10 个请求该信号量的任务可以使用共享资源,以后的任务需要等待前 10 个任务释放掉信号量。每当有任务请求信号量的时候,信号量的值就会减 1,直到减为 0。当有任务释放掉信号量的时候,信号量的值就会加1。

函数描述
OSSemCreate()创建一个信号量
OSSemDel()删除一个信号量
OSSemPend()等待一个信号量
OSSemPendAbort()取消等待
OSSemPost()释放或发送一个信号量
OSSemSet()强制设置一个信号量的值

2、互斥信号量

为了避免优先级反转现象,UCOSIII 支持一种特殊的二进制信号量,叫互斥信号量。

二值信号量主要用于进行共享资源的独占式访问,但是二值信号量容易产生优先级反转。
互斥信号量一般用于解决优先级反转。

原理:若某当前任务 3 拥有互斥信号量,系统会将该任务的优先级暂时提高到与最高优先级 1 任务相同的优先级,从而避免优先级反转发生。等待任务 3 完成释放互斥信号量后,又将任务 3 的优先级恢复。

注意!只有任务才能使用互斥信号量,中断服务程序则不可以。UCOSIII 允许用户嵌套使 用互斥型信号量,一旦一个任务获得了一个互斥型信号量,则该任务最多可以对该互斥型信号 量嵌套使用 250 次,当然该任务只有释放相同的次数才能真正释放这个互斥型信号量。

函数描述
OSMutexCreate()创建一个互斥信号量
OSMutexDel()删除一个互斥型信号量
OSMutexPend()等待一个互斥型信号量
OSMutexPendAbort()取消等待
OSMutexPost()释放一个互斥型信号量

3、任务内嵌信号量

UCOSIII 中每个任务都有自己的 内嵌的信号量,这种功能不仅能够简化代码,而且比
使用独立的信号量更有效。

函数描述
OSTaskSemPend()等待任务信号量
OSTaskSemPendAbort()取消等待任务信号量
OSTaskSemPost()发布任务信号量
OSTaskSemSet()强行设置任务信号量计数

七、UCOSIII 消息传递

有时候一个任务要和另外一个或者几个任务进行消息的传递,也称之为任务间通信,在UCOSIII 中消息可以通过消息队列作为中介发布给任务,也可以直接发布给任务。

1、消息队列

消息一般包含:指向数据的指针、表明数据长度的变量和记录消息发布时刻的时间戳。指针指向的可以是一块数据区或者甚至是一个函数。消息的内容必须一直保持可见性,因为发布的数据本身不产生数据拷贝,发布数据采用的引用传递是指针传递而不是值传递。

在 UCOSII 中有消息邮箱和消息队列,但是在 UCOSIII 中只有消息队列。消息队列是由用 户创建的内核对象,数量不限制。

在 UCOSIII 中对于消息队列的读取可以采用先进先出(FIFO)和后进先出(LIFO)的方式。

2、消息队列相关函数

函数描述
OSQCreate()创建一个消息队列
OSQDel()删除一个消息队列
OSQFlush()清空一个消息队列
OSQPend()等待消息队列
OSQPendAbort()取消等待消息队列
OSQPost()向消息队列发送一条消息
常用的消息队列函数有三个,创建消息队列函数 OSQCreate(),向消息队 列发送消息函数 OSQPost()和等待消息队列函数OSQPend()。

3、任务内嵌消息队列

和任务信号量一样,UCOSIII 中每个任务也都有其内建消息队列,这样的话用户就不需要使用外部的消息队列就可直接向任务发布消息,这个特性不仅简化了代码,而且比使用外部消息队列更加有效,如果要使用任务内建消息队列的话宏 OS_CFG_TASK_Q_EN 必须置1。

4、任务内嵌消息队列相关函数

函数名描述
OSTaskQPend()等待消息
OSTaskQPendAbort()取消等待消息
OSTaskQPost()向任务发送一条消息
OSTaskQFlush()清空任务的消息队列

八、事件标志组

除了使用任务信号量来完成任务同步,还有另一种任务同步的方法就是事件标志组,事件标志组用来解决一个任务和多个事件之间的同步。

1、事件标志组

有时候一个任务可能需要和多个事件同步,这个时候就需要使用事件标志组。事件标志组与任务之间有两种同步机制:“或”同步和“与”同步,当任何一个事件发生,任务都被同步的同步机制是“或”同步;需要所有的事件都发生任务才会被同步的同步机制是“与”同步。

在 UCOSIII 中事件标志组是OS_FLAG_GRP,事件标志组中也包含了一串任务,这些任务都在等待着事件标志组中的部分(或全部)事件标志被置 1 或被清零,在使用之前,必须创建事件标志组。

任务和 ISR(中断服务程序)都可以发布事件标志,但是,只有任务可以创建、删除事件标志组以及取消其他任务对事件标志组的等待。

任务可以通过调用函数 OSFlagPend()等待事件标志组中的任意个事件标志,调用函数OSFlagPend()的时候可以设置一个超时时间,如果过了超时时间请求的事件还没有被发布,那么任务就会重新进入就绪态。

我们可以设置同步机制为“或”同步还是“与”同步。

2、事件标志组相关函数

函数描述
OSFlagCreate()创建事件标志组
OSFlagDel()删除事件标志组
OSFlagPend()等待事件标志组
OSFlagPendAbort()取消等待事件标志组
OSFlagPendGetFlagsRdy()获取使任务就绪的事件标志
OSFlagPost()向事件标志组发布标志

九、同时等待多个内核对象

前面讲解的信号量、互斥信号量、消息队列和时间标志组都是任务如何等待单个对象,本部分将介绍任务如何等待多个内核对象。UCOSIII 中只支持同时等待多个信号量和消息队列,不支持持同时等待多个事件标志组和互斥信号量。

1、同时等待多个内核对象

UCOSIII 中一个任务可以同时等待任意数量的信号量或者消息队列,当只要等到其中的任意一个的时候就会导致该任务进入就绪态。

函数 OSPendMulti()用来等待多个内核对象,调用 OSPendMulti()时,如果这些对象中有多个可用,则所有可用的信号量和消息都将返回给调用者,如果没有任何对象可用,则OSPendMulti()将挂起当前任务,直到以下任一情况发生:

(1)、对象变为可用。
(2)、到达设定的超时时间。
(3)、一个或多个任务被删除或被终止。
(4)、一个或多个对象被删除。

如果一个对象变为可用,并且有多个任务在等待这个对象,则 UCOSIII 将恢复优先级最高的那个任务。

十、存储管理

作为一个操作系统,内存管理是其必备的功能,在 UCOSIII 中也有内存管理模块,使用内存管理模块可以动态的分配和释放内存,这样可以高效的使用“昂贵”的内存资源。

1、内存管理简介

内存管理是一个操作系统必备的系统模块,我们在用 VC++或者 Visual Studio 学习 C 语言的时候会使用 malloc()和 free()这两个函数来申请和释放内存。我们在使用 Keil MDK 编写 STM32 程序的时候就可以使用 malloc()和 free(),但是不建议这么用,这样的操作将原来大块内存逐渐的分割成很多个小块内存,产生大量的内存碎片,最终导致应用不能申
请到大小合适的连续内存。

UCOSIII 提供了自己的动态内存方案,UCOIII 将存储空间分成区和块,一个存储区有数个固定大小的库组成。
在这里插入图片描述
一般存储区是固定的,在程序中可以用数组来表示一个存储区,比如 u8 buffer[20][10]就表示一个有 20 个存储块,每个存储块 10 字节的存储区。如果我们定义的存储区在程序运行期间都不会被删除掉,一直有效,那么存储区内存也可以使用 malloc()来分配。在创建存储区以后应用程序就可以获得固定大小的存储块。

在实际使用中我们可以根据应用程序对内存需求的不同建立多个存储区,每个存储区中有不同大小、不同数量的存储块,应用程序可以根据所需内存不同从不同的存储区中申请内存使用,使用完以后在释放到相应的存储区中。

2、存储区创建

在使用内存管理之前首先要创建存储区,创建存储区之前我们需要了解一个重要的结构体,存储区控制块:OS_MEM。创建存储区使用函数 OSMemCreate()。
在这里插入图片描述

3、存储块的使用

调用函数 OSMemCreate()创建好存储区以后我们就可以使用创建好的存储块了。

(1)、内存申请

使用函数 OSMemGet()来获取存储块,从指定的存储区中获取存储块供给应用使用。

UCOSIII 自带的内存管理函数的局限性,每次申请内存的时候用户要先估计所申请的内存是否会超过存储区中存储块的大小。比如我们创建了一个有 10 个存储块,每个存储块大小为 100 字节的存储区 buffer。这时我们应用程序需要申请一个 10 字节的内存,那么就可以使用函数 OSMemGet()从存储区 buffer 中申请一个存储块。但是每个存储块有 100 个字节,但是应用程序只使用其中的 10 个字节,剩余的 90 个字节就浪费掉了,为了减少浪费我们可以创建一个每个存储块为 10 字节的存储区,这样就不会有内费了。但是,问题又来了,假设我们在程序的其他地方需要申请一个 150 字节的内存,但是存储区 buffer 的每个存储块只有 100 字节,显然存储区 buffer 不能满足程序的需求。有读者就会问可不可以在存储区中连续申请两个 100 字节的存储块,这样就有 200 字节的内存供应用程序使用了?想法是好想法,但是通过阅读函数 OSMemGet()发现并没有提供这样的功能,OSMemGet()函数在申请内存的时候每次只取指定存储区的一个存储块!如果想申请 150 字节的内存就必须再新建一个每个存储块至少有 150 字节的存储区。

可以看出 UCOSIII 的内存管理很粗糙,不灵活,并不能申请指定大小的内存块。使用过 ALIENTEK 的 STM32 开发板的用户就会知道, ALIENTEK 实现了内存的动态使用,可以申请任意大小的内存空间,使用起来十分方便。

(2)、内存释放

在 UCOSIII 中内存释放可以使用函数OSMemPut()来完成,将申请到的存储块还给指定的存储区。

  • 8
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值