目录
1.学习环境
基于正点原子的STM32F429以及相关例程与视频
stm32f429阿波罗开发板 — 正点原子资料下载中心 1.0.0 文档
第5讲 UCOSIII在STM32F407上的移植_哔哩哔哩_bilibili
2.滴答定时器
硬件定时器来产生“滴答”中断来作为系统时基,也就是系统时钟节拍。
配置SysTick->LOAD时,要与宏OS_CFG_TICK_RATE_HZ以及系统时钟产生关联,以此来计算出重装载寄存器的值,从而确定系统时钟节拍的周期。
另:SysTick在stm官方没有讲解,在arm的cortex M3 M4资料《Cortex M3权威指南(中文)》中有详细解释。
3.任务
任务在操作系统中,就是循环运行的单独功能的一个函数,类似于裸机中的main函数。
但是操作系统的功能就是在有多个main函数,也是task任务的时候,对其进行调度,分配运行时间,使cpu的利用率提高。
UCOSIII中的任务由三部分组成:任务堆栈、任务控制块和任务函数。
任务堆栈:上下文切换的时候用来保存任务的工作环境,就是STM32的内部寄存器值。
任务控制块:任务控制块用来记录任务的各个属性。
任务函数:由用户编写的任务处理代码,是实实在在干活的,一般写法如下:
void XXX_task(void *p_arg)
{
while(1) //一般任务需要循环运行,当然只想运行1次,也是可以操作的
{
。。。。。//任务处理过程
}
}
3.1 UCOSIII系统任务
UCOSIII的任务优先级数量由OS_CFG_PRIO_MAX定义。理论上优先级数量不存在限制。
UCOSIII默认有5个系统任务:
1、空闲任务:UCOSIII创建的第一个任务,UCOSIII必须创建的任务,此任务有UCOSIII自动创建,不需要用户手动创建,任务优先级为最低,即OS_CFG_PRIO_MAX-1。
2、时钟节拍任务:此任务也是必须创建的任务。需要相对较高的优先级,任务优先级为OS_CFG_TICK_TASK_PRIO。
3、统计任务:可选任务,用来统计CPU使用率和各个任务的堆栈使用量。此任务是可选任务,由宏OS_CFG_STAT_TASK_EN控制是否使用此任务,任务优先级为OS_CFG_STAT_TASK_PRIO。
4、定时任务:用来向用户提供定时服务,也是可选任务,由宏OS_CFG_TMR_EN控制是否使用此任务。任务优先级为OS_CFG_TMR_TASK_PRIO。
5、中断服务管理任务:可选任务,由宏OS_CFG_ISR_POST_DEFERRED_EN控制是否使用此任务。任务优先级最高,为0,且不可更改。
空闲任务不允许用户将其状态改变成等待态。这点很容易
理解,操作系统不允许空闲,即便是空闲任务也不是真正的空闲,也是有空闲任务在的,如果空闲任务进入等待,在切换任务时没有任务就绪也会引起问题。
这同时在如下函数中有所体现。
3.2 UCOSIII任务状态
从用户的角度看,UCOSIII的任务一共有5种状态:
1、休眠态:任务已经在CPU的flash中了,但是还不受UCOSIII管理。即为编写了这部分代码,但并未创建,或者已经被删除。
2、就绪态:系统为任务分配了任务控制块,并且任务已经在就绪表中登记,这时这个任务就具有了运行的条件,此时任务的状态就是就绪态。处于就绪态的任务,满足调度条件,获得CPU使用权时,即可运行。
3、运行态:任务获得CPU的使用权,正在运行。
4、等待态:正在运行的任务需要等待一段时间,或者等待某个事件,这个任务就进入了等待态,此时系统就会把CPU使用权转交给别的任务。
5、中断服务态:当发送中断,当前正在运行的任务会被挂起,CPU转而去执行中断服务函数,此时任务的任务状态叫做中断服务态。
3.3 UCOSIII任务调度
UcosIII的任务调度分为两种方式:
可剥夺型任务调度:即抢占式任务调度 或者说 按照优先级调度
时间片轮转调度:在同一优先级下的任务按照时间片分配运行时间
可剥夺型任务调度,在运行时总会优先运行所有就绪任务中优先级最高的那个任务。当更高优先级任务就绪时,在任务调度发生时,任务将会切换至最高已就绪的最高优先级的任务。
时间片轮转调度:当轮到当前优先级的任务运行时,如果此优先级下有多个同优先级的任务就绪,则按照预先设置好的时间片大小来切换运行任务。
任务调度的触发可以分为主动发生与被动发生。主动发生为用户编写的代码运行时主动调用了会引起任务调度功能的函数。被动发生可以理解为滴答时钟发生中断时,周期性进行的任务调度。
注意:所谓的抢占式任务调度,剥夺型任务调度,也是按照时钟节拍来运行的,并不是只要有高优先级的任务就绪就会立刻切换,是需要有调度器去安排任务的切换的,而调度器即任务的调度,也是需要被触发的,例如系统时间节拍。
任务切换分为两种:任务级切换和中断级切换。
任务级切换函数为:OSCtxSw()。
中断级切换函数为:OSIntCtxSw()。
任务调度函数一般不由用户直接调用,用户在使用相关API函数时,自然会调用到。
3.4 任务相关的API函数
函数的参数的设置和含义不多做赘述,可参考正点原子的《STM32F429 UCOS开发手册_V1.0》
任务创建和删除:顾名思义,为任务的创建与删除功能
OSTaskCreate()创建任务
OSTaskDel()删除任务 //另,当参数1使用NULL指针时,可以删除调用此函数的任务本身。
任务挂起和恢复:当我们想暂停某个任务,但是又不想删除掉这个任务的时候就可以使用这两个函数进行相关操作。
OSTaskSuspend()任务的挂起
OSTaskResume()任务的恢复
另,我们可以多次挂起同一个任务,但是需要恢复时,需要进行同等次数的恢复操作。
时间片轮转调度
OSSchedRoundRobinCfg(): //时间片时长为参数n*时钟节拍时长
如果我们想要使用UCOSIII的时间片轮转调度的话不仅要将宏OS_CFG_SCHED_ROUND_ROBIN_EN置1,还需要调用函数OSSchedRoundRobinCfg()
OSSchedRoundRobinYield ()放弃本次时间片函数
3.5 钩子函数
钩子函数一般主要是用来扩展其他函数(任务)功能的,钩子函数有如下几个:
1、OSIdleTaskHook(),空闲任务调用这个函数,可以用来让CPU进入低功耗模式
2、OSInitHook(),系统初始化函数OSInit()调用此函数。
3、OSStatTaskHook(),统计任务每秒中都会调用这个函数,此函数允许你向统计任务中添加自己的应用函数。
4、OSTaskCreateHook(),任务创建的钩子函数。
5、OSTaskDelHook(),任务删除的钩子函数。
6、OSTaskReturnHook(),任务意外返回时调用的钩子函数,比如删除某个任务
7、OSTaskSwHook(),任务切换时候调用的钩子函数。
8、OSTimeTickHook(),滴答定时器调用的钩子函数。
想要使用钩子函数,需要将宏OS_CFG_APP_HOOKS_EN置1。
以上钩子函数,是在相对应的函数中被调用的,函数主体在os_app_hooks.c中由用户自己填写。以上钩子函数同时也为App_OS_SetAllHooks()中被赋值给对应函数的指针。
所谓钩子函数,个人通俗的理解为,钩挂在某个函数中,附加进行的代码,可以达到不修改标准代码的同时,统一,规范的添加自己想要的代码的目的。
4.UCOSIII的中断
UCOSIII的中断函数与裸机的中断函数在写法上的差别仅在于,UCOSIII的中断函数在进入和退出时,需要使用OSIntEnter()与OSIntExit()函数,在标记中断的嵌套次数。
UCOSIII最多支持250级的中断嵌套。
5.UCOSIII的临界段代码保护
临界段代码也叫做临界区,是指那些必须完整连续运行,不可被打断的代码段,例如外设的初始化过程。当访问这些临界段代码的时候需要对这些临界段代码进行保护。
当宏OS_CFG_ISR_POST_DEFERRED_EN为0时,UCOSIII使用关中断的方式来保护临界段代码,当设置为1的时候就会采用给调度器上锁的方式来保护临界段代码。
UCOSIII定义了一个进入临界段代码的宏:OS_CRITICAL_ENTER(),定义了两个退出临界段代码的宏:OS_CRITICAL_EXIT()和OS_CRITICAL_EXIT_NO_SCHED()。
OS_CRITICAL_EXIT会伴随任务调度,反之使用OS_CRITICAL_EXIT_NO_SCHED时不会进行任务调度。
6.UCOSIII的延时函数
UCOSIII的延时函数有两种,OSTimeDly()和OSTimeDlyHMSM()。
OSTimeDly是以时钟节拍数为单位进行的延时,需要换算一下延时时间。
OSTimeDlyHMSM是以小时 分钟 秒 毫秒,为参数进行的延时,在使用时更加直观。
OSTimeDly有3种工作模式:相对模式、周期模式和绝对模式
OSTimeDlyHMSM仅在相对模式下工作。
延时函数可以通过在其他函数中调用OSTimeDlyResume()来取消使用延时函数任务的延时,从而使该进入就绪状态,并且在OSTimeDlyResume的最后会引发一次任务调度。
OSTimeSet()允许用户改变当前时钟节拍计数器的值。需要谨慎使用,可能引起bug。
OSTimeGet()用来获取当前时钟节拍计数器的值。
7.UCOSIII软件定时器
UCOSIII的软件定时器其实就是一个递减计数器,当计数器递减到0的时候,可以触发某个特定的动作,这个动作就是回调函数。
UCOSIII中定时器的时间分辨率由一个宏OS_CFG_TMR_TASK_RATE_HZ,单位为HZ。但是这个值绝对不能大于我们定义的系统节拍频率OS_CFG_TICK_RATE_HZ。
需要使用软件定时器的话需要将宏OS_CFG_TMR_DEL_EN定义为1。
7.1 软件定时器API函数
OSTmrStart()可以在任意时刻使用,使用后定时器技术从重装载值处开始重新倒计时。
软件定时器的模式分为:单次模式和周期模式
软件定时器的可以分别设置第一次运行时的初始值和周期重装载的值。
8.信号量和互斥型号量
8.1 信号量
信号量像是一种上锁机制,代码必须获得对应的钥匙才能继续执行,一旦获得了钥匙,也就意味着该任务具有进入被锁部分代码的权限。一旦执行至被锁代码段,则任务一直等待,直到对应被锁部分代码的钥匙被再次释放才能继续执行。
信号量用于控制对共享资源的保护,但是现在基本用来做任务同步用。
信号量分为两种:二进制信号量与计数型信号量。
二进制信号量一次只能有一个任务使用共享资源。
计数型信号量可以同时有多个任务访问共享资源。
8.1.1 信号量API函数
OSSemPend()与OSSemPost()还会对信号量进行加减操作,通过信号量的计数来表现出信号量的使用状态。
8.2 优先级反转
例如当前有3个任务,优先级为高中低,当高优先级任务在等待信号量时,且该信号量需要低优先级任务释放。此时高优先级挂起,等待低优先级任务释放信号量,但是在低优先级任务释放信号量之前,中优先级任务就绪,剥夺低优先级任务对CPU的使用权,在中优先级任务完成后,再运行低优先级任务,低优先级任务释放信号量,再运行高优先级任务。
此时中优先级任务的优先级达到了高于高优先级任务的优先级的效果。
如图:
参考STM32F429 UCOS开发手册_V1.0 P143页。
8.3 互斥信号量
为了避免优先级反转这个问题,UCOSIII支持一种特殊的二进制信号量:互斥信号量,用它可以解决优先级反转问题。
高优先级任务等待互斥信号量时,会临时将正在使用互斥信号量的低优先级任务的优先级提高到和自身同等的优先级,等其释放互斥型号量之后再将其的优先级还原,从而避免了中优先级任务的插入。
8.3.1 互斥信号量API函数
8.4 任务内嵌信号量
在UCOSIII中每个任务都有自己的内嵌的信号量,这种功能不仅能够简化代码,而且比使用独立的信号量更有效。任务信号量是直接内嵌在UCOSIII中的。
与信号量不同的是,不需要主动去创建,且任务内嵌信号量的等待只能在该任务内等待,通过别的任务以任务控制块为参数发布此任务的信号量。
8.4.1 任务内嵌信号量API函数
9.UCOSIII的消息传递
一个任务或者中断服务程序有时候需要和另一个任务交流信息,这个就是消息。
传递的过程就叫做任务间通信,任务间的消息传递可以通过2种途径:一是通过全局变量,二是通过发布消息。
消息包含一下几个部分:指向数据的指针,数据的长度和记录消息发布时刻的时间戳,指针指向的可以是一块数据区域或者甚至是一个函数。
9.1 消息队列
UCOSIII的消息内容被封装在结构体OS_MSG中,而消息队列就是封装了相关参数,信息和消息的队列。
9.1.1 消息队列API函数
使用消息队列时,需要为了消息的数据,进行对内存的申请与释放操作。
9.2 任务内建消息队列
与任务内嵌信号量相似,我们所创建的任务还拥有自己的任务内建消息队列,与内嵌信号量不同的是,要使用任务内建消息队列功能的时候需要将宏OS_CFG_TASK_Q_EN置1来使能相关的代码。
同样的,任务内建的消息队列不需要用户额外去创建,消息的等待函数也只能在本函数内使用,通过别的任务以任务控制块为参数发布此任务的消息。
9.2.1 任务内建消息队列API函数
10.事件标志组
有时候一个任务需要与多个事件同步,这个时候就需要使用事件标志组。事件标志组与任务之间有两种同步机制:“或”同步和“与”同步。
需要使用事件标志组的时候需要将宏OS_CFG_FLAG_EN置1。
事件标志组中有一个OS_FLAGS类型Flags变量用于标志各个事件的状态。OS_FLAGS可以是8/16/32位的变量类型。其每一位都可以用来表示一个事件的状态。
10.1 事件标志组API函数
事件发生时通过OSFlagPost对事件标志组属于他的那一位进行置位操作;再由需要等待事件标志组的函数使用OSFlagPend,OSFlagPend会对事件标志组的Flags进行判断。
11.同时等待多个内核对象
在UCOSIII中允许任务同时等待多个信号量和多个消息队列,也就是说,UCOSIII不支持同时等待多个事件标志组或互斥信号量。
一个任务可以等待任意数量的信号量和消息队列,第一个信号量或消息队列的发布会导致该任务进入就绪态。
(事件标志组可以选择多个事件之间的与或关系,但同时等待多个内核对象没有这个选择,不过可以理解为或关系)
不需要创建什么特殊的对象,使用OSPendMulti()函数将需要等待的信号量与信号队列作为变量加入需要等待的对象数组中即可。
OSPendMulti()的返回值为请求到的内核对象数量。
12.UCOSIII存储管理
通常应用程序可以调用ANSI C编译器的malloc()和free()函数来动态的分配和释放内存,但是在嵌入式事实操作系统中最好不要这么做,多次这样的操作会把原来很大的一块连续存储区域逐渐地分割成许多非常小并且彼此不相邻的存储区域,这就是存储碎片。
UCOSIII中提供了一种替代malloc()和free()函数的方法,UCOSIII中将存储空间分成区和块,每个存储区有数量不等大小相同的存储块,在一个系统中可以有多个存储区。