UCOS学习笔记
-
五、 任务管理
5.1. 任务创建OSTaskCreate
函数原型:
void OSTaskCreate (OS_TCB *p_tcb, CPU_CHAR *p_name, OS_TASK_PTR p_task, void *p_arg, OS_PRIO prio, CPU_STK *p_stk_base, CPU_STK_SIZE stk_limit, CPU_STK_SIZE stk_size, OS_MSG_QTY q_size, OS_TICK time_quanta, void *p_ext, OS_OPT opt, OS_ERR *p_err)
- *p_tcb: 指向任务的任务控制块OS_TCB。
- *p_name: 指向任务的名字,我们可以给每个任务取一个名字
- p_task: 执行任务代码,也就是任务函数名字
- *p_arg: 传递给任务的参数
- prio: 任务优先级,数值越低优先级越高,用户不能使用系统任务使用的那些优先 级!
- *p_stk_base: 指向任务堆栈的基地址
- stk_limit: 任务堆栈的堆栈深度,用来检测和确保堆栈不溢出
- stk_size: 任务堆栈大小
- q_size: UCOSIII 中每个任务都有一个可选的内部消息队列,我们要定义宏 OS_CFG_TASK_Q_EN>0,这是才会使用这个内部消息队列。
- time_quanta: 在使能时间片轮转调度时用来设置任务的时间片长度,默认值为时钟节拍除以10
- *p_ext: 指向用户补充的存储区
- opt: 包含任务的特定选项,有如下选项可以设置
- OS_OPT_TASK_NONE: 表示没有任何选项
- OS_OPT_TASK_STK_CHK: 指定是否允许检测该任务的堆栈
- OS_OPT_TASK_STK_CLR: 指定是否清除该任务的堆栈
- OS_OPT_TASK_SAVE_FP: 指定是否存储浮点寄存器,CPU 需要有浮点
- *p_err: 用来保存调用该函数后返回的错误码
led任务创建实例
OSTaskCreate((OS_TCB * )&Task_LED1_TaskTCB, (CPU_CHAR * )"task_led1 task", (OS_TASK_PTR )task_led1_task, (void * )0, (OS_PRIO )TASK_LED1_TASK_PRIO, (CPU_STK * )&TASK_LED1_TASK_STK[0], (CPU_STK_SIZE)TASK_LED1_STK_SIZE/10, (CPU_STK_SIZE)TASK_LED1_STK_SIZE, (OS_MSG_QTY )0, (OS_TICK )1, (void * )0, (OS_OPT )OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR, (OS_ERR * )&err);
5.2. 任务删除OSTaskDel
当一个任务不需要运行的话,我们就可以将其删除掉,删除任务不是说删除任务代码,而是UCOSIII 不再管理这个任务,在有些应用中我们只需要某个任务只运行一次,运行完成后就将其删除掉,比如外设初始化任务start_task
函数原型:
void OSTaskDel (OS_TCB *p_tcb, OS_ERR *p_err)
-
*p_tcb: 指向要删除的任务 TCB,也可以传递一个 NULL 指针来删除调用 OSTaskDel()函数的任务自身
-
*p_err: 指向一个变量用来保存调用 OSTaskDel()函数后返回的错误码
虽然 UCOSIII 允许用户在系统运行的时候来删除任务,但是应该尽量的避免这样的操作,如果多个任务使用同一个共享资源,这个时候任务A 正在使用这个共享资源,如果删除了任务A,这个资源并没有得到释放,那么其他任务就得不到这个共享资源的使用权,会出现各种奇怪的结果。
我们调用OSTaskDel()删除一个任务后,这个任务的任务堆栈、OS_TCB 所占用的内存并没有释放掉,因此我们可以利用他们用于其他的任务,当然我们也可以使用内存管理的方法给任务堆栈和OS_TCB 分配内存,这样当我们删除掉某个任务后我们就可以使用内存释放函数将这个任务的任务堆栈和 OS_TCB 所占用的内存空间释放掉。
OSTaskDel((OS_TCB*)0,&err); //删除start_task任务自身
5.3. 任务挂起OSTaskSuspend
有时候有些任务因为某些原因需要暂停运行,但是以后还要运行,因此我们就不能删除掉任务,这里我们可以使用 OSTaskSuspend()函数挂起这个任务,以后再恢复运行
函数原型:
void OSTaskSuspend (OS_TCB *p_tcb, OS_ERR *p_err)
- *p_tcb: 指向需要挂起的任务的OS_TCB,可以通过指向一个NULL 指针将调用该函数的任务挂起
- *p_err: 指向一个变量,用来保存该函数的错误码
5.4. 任务解挂OSTaskResume
OSTaskResume()函数用来恢复被OSTaskSuspend()函数挂起的任务,OSTaskResume()函数是唯一能恢复被挂起任务的函数。如果被挂起的任务还在等待别的内核对象,比如事件标志组、信号量、互斥信号量、消息队列等,即使使用OSTaskResume()函数恢复了被挂起的任务,该任务也不一定能立即运行,该任务还是要等相应的内核对象,只有等到内核对象后才可以继续运行
函数原型:
void OSTaskResume (OS_TCB *p_tcb, OS_ERR *p_err)
- *p_tcb: 指向需要解挂的任务的OS_TCB,指向一个NULL 指针是无效的,因为该任务正在运行,不需要解挂
- *p_err: 指向一个变量,用来保存该函数的错误码
5.5. 开启时间片轮转调度
OSSchedRoundRobinCfg()函数用来使能或失能 UCOSIII 的时间片轮转调度功能,如果我们要使用时间片轮转调度功能的话不仅要将宏 OS_CFG_SCHED_ROUND_ROBIN_EN 定义为 1,还需要调用OSSchedRoundRobinCfg()函数来使能UCOSIII
函数原型:
void OSSchedRoundRobinCfg (CPU_BOOLEAN en, OS_TICK dflt_time_quanta, OS_ERR *p_err)
- en: 用于设置打开或关闭时间片轮转调度机制,如果为 DEF_ENABLED 表
示打开时间片轮转调度,为 DEF_DISABLED 表示关闭时间片轮转调度。 - dflt_time_quanta: 设置默认的时间片长度,就是系统时钟节拍个数,比如我们设置系统时钟频率 OSCfg_TickRate_Hz 为 200Hz,那么每个时钟节拍就是 5ms。当我们设置 dflt_time_quanta 为 n 时,时间片长度就是(5*n)ms 长,如果我们设置dflt_time_quanta 为0 的话,UCOSIII 就会使用系统默认的时间片长度:OSCfg_TickRate_Hz / 10,比如如果 OSCfg_TickRate_Hz 为 200,那么时间片长度为:200/10*5=100ms
- *p_err: 保存调用此函数后返回的错误码
在任务初始化函数中写入宏编译可以不用每次都要重新编写这个函数
#if OS_CFG_SCHED_ROUND_ROBIN_EN //当使用时间片轮转的时候 //使能时间片轮转调度功能,时间片长度为1个系统时钟节拍,既1*5=5ms OSSchedRoundRobinCfg(DEF_ENABLED,1,&err); #endif
5.6. 放弃本轮时间片
当一个任务想放弃本次时间片,把CPU 的使用权让给同优先级下的另外一个任务就可以使用OSSchedRoundRobinYield()函数
函数原型:
void OSSchedRoundRobinYield (OS_ERR *p_err)
- *p_err: 来保存函数调用后返回的错误码
- OS_ERR_NONE: 调用成功
- OS_ERR_ROUND_ROBIN_1: 当前优先级下没有其他就绪任务
- OS_ERR_ROUND_ROBIN_DISABLED: 未使能时间片轮转调度功能
- OS_ERR_YIELD_ISR: 在中断调用了本函数
-
六、 UCOSIII系统内部任务
6.1. 空闲任务
空闲任务:OS_IdleTask(),在 os_core.c 文件中定义。任务OS_IdleTask()是必须创建的,不过不需要手动创建,在调用OS_Init()初始化UCOS 的时候就会被创建。
空闲任务优先级为OS_CFG_PRIO_MAX – 1,即最大任务数-1,表示其优先级最低当没有任务就绪时,系统会进入空闲任务。
6.2. 时钟节拍任务
时钟节拍任务:OS_Ticktask(),它也是必须创建的任务且不需要手动创建,时钟节拍任务的作用是跟踪正在延时的任务,以及在指定时间内等待某个内核对象的任务
6.3. 统计任务
在UCOSIII 中统计任务可用来统计CPU 的使用率、各个任务的CPU 使用率和各任务的堆栈使用情况,默认情况下统计任务是不会创建的,如果要使能统计任务的话需要将宏OS_CFG_STAT_TASK_EN 置1。统 计 任 务 的 优 先 级 通 过 宏OS_CFG_STAT_TASK_PRIO 设置,ALIENTEK 将 统 计 任 务 的 优 先 级 设 置 为OS_CFG_PRIO_MAX-2,也就是倒数第二
启动统计任务将OS_CFG_STAT_TASK_EN置一,CPU 的总的使用率会保存在变量OSStatTaskCPUUsage 中,我们可以通过读取这个值来获取 CPU 的使用率。从 V3.03.00 版本起,CPU 的使用率用一个0~10000 之间的整数表示(对应0.00~100.00%)
如果将宏OS_CFG_STAT_TASK_STK_CHK_EN 置1 的话表示检查任务堆栈使用情况,那么统计任务就会调用 OSTaskStkChk()函数来计算所有已创建任务的堆栈使用量,并将检测结果写入到每个任务的OS_TCB 中的StkFree 和StkUsed 中
6.4. 定时任务
UCOSIII 提供软件定时器功能,定时任务是可选的,将宏 OS_CFG_TMR_EN 设置为 1 就会使能定时任务,在OSInit()中将会调用函数OS_TmrInit()来创建定时任务。定时任务的优先级通过宏OS_CFG_TMR_TASK_PRIO 定义,ALIENTEK 默认将定时器任务优先级设置为 2。
6.5. 中断服务管理任务
当把 os_cfg.h 文件中的宏 OS_CFG_ISR_POST_DEFERRED_EN 置 1 就会使能中断服务管理任务,UCOSIII 会创建一个名为OS_IntQTask()的任务,该任务负责“延迟”在 ISR 中调用的系统post 服务函数的行为。中断服务管理任务的任务优先级永远是最高的,为 0
在 UCOS 中可以通过关闭中断和任务调度器上锁两种方式来管理临界段代码(有关临界段代码保护下一章会详细讲解),如果采用后一种,即调度器上锁的方式来管理临界段代码的话,那么在中断服务函数中调用的“post”类函数就不允许操作诸如任务就绪表、等待表等系统内部数据结构。
当 ISR(中断服务函数)调用UCOSIII 提供的“post”函数时,要发送的数据和发送的目的地都会存入一个特别的缓冲队列中,当所有嵌套的 ISR 都执行完成以后 UCOSIII 会做任务切换,运行中断服务管理任务,该任务会把缓存队列中存放的信息重发给相应的任务。这样做的好处就是可以减少中断关闭的时间,否则,在 ISR 中还需要把任务从等待列表中删除,并把任务放入就绪表,以及做一些其他的耗时操作。
6.6. 空闲任务钩子函数
空闲任务钩子函数在空闲任务中调用,可以让用户在空闲任务中做一些处理,将OS_CFG_APP_HOOKS_EN置一,这样每次在空闲任务中就会调用函数App_OS_SetAllHooks(),编写其内容即可实现用户想要的功能。
注意!在空闲任务的钩子函数中不能调用任何可以是空闲进入等待态的代码,原因很简单, CPU 总是在不停的运行,需要一直工作,不能让 CPU 停下来,哪怕是执行一些对应用没有任何用的代码,比如简单的将一个变量加一。在 UCOS 中为了让 CPU 一直工作,在所有应用任务都进入等待态的时候 CPU 会执行空闲任务,我们可以从空闲任务的任务函数 OS_IdleTask()看出,在OS_IdleTask()中没有任何可以让空闲任务进入等待态的代码。如果在 OS_IdleTask()中有可以让空闲任务进入等待态的代码的话有可能会在同一时刻所有任务(应用任务和空闲任务)同时进入等待态,此时CPU 就会无所事事了,所以在空闲任务的钩子函数OSIdleTaskHook()中不能出现可以让空闲任务进入等待态的代码!这点很重要,一定要谨记!
-
七、 中断管理
在 STM32 中是支持中断的,中断是一个硬件机制,主要用来向 CPU 通知一个异步事件发生了,这时CPU 就会将当前CPU 寄存器值入栈,然后转而执行中断服务程序,在CPU 执行中断服务程序的时候有可能有更高优先级的任务就绪,那么当退出中断服务程序的时候,CPU 就会直接执行这个高优先级的任务。
UCOS环境中编写中断服务函数
void XXX_Handler(void) { OSIntEnter(); //进入中断 //用户自行编写的中断服务函数 OSIntExit(); //触发任务切换软中断 }
在中断服务函数中发布消息或信号有两种处理模式:直接发布和延迟发布
我们可以通过宏 OS_CFG_ISR_POST_DEFERRED_EN 来选择使用直接发布还是延迟发布。宏 OS_CFG_ISR_POST_DEFERRED_EN 在 os_cfg.h 文件中有定义,当定义为 0 时使用直接发布模式,定义为 1 的时候使用延迟发布模式。不管使用那种方式,我们的应用程序不需要做出任何的修改,编译器会根据不同的设置编译相应的代码。
7.1. 直接发布
- 外设产生中断请求
- 中断服务程序开始运行,该中断服务程序中可能会包含有发送信号量、消息、事件标志组等事件。那么等待这个事件的任务的优先级要么比当前被中断的任务高,要么比其低。
- 如果中断对应的事件使得某个比被中断的任务优先级低的任务进入就绪态,则中断退出后仍恢复运行被中断的任务。
- 如果中断对应的事件使得某个比被中断的任务优先级更高的任务进入就绪态,则UCOSIII 将进行任务切换,中断服务程序推出后就执行更高优先级的任务。
- 如果使用直接发布模式的话,则UCOSIII 必须关中断以保护临界段代码,防止中断处理程序访问这些临界段代码。
使用直接发布模式的话,UCOSIII 会对临界段代码采用关闭中断的保护措施,这样就会延长中断的响应时间。虽然UCOSIII 已经采用了所有可能的措施来降低中断关闭时间,但仍然有一些复杂的功能会使得中断关闭相对较长的时间。
7.2. 延迟发布
当设置宏 OS_CFG_ISR_POST_DEFERRED_EN 为 1 的时候,UCOSIII 不是通过关中断,而是通过给任务调度器上锁的方法来保护临界段代码,在延迟发布模式下基本不存在关闭中断的情况
- 外设产生中断请求
- 断服务程序开始运行,该中断服务程序中可能会包含有发送信号量、消息、事件标志组等事件。那么等待这个事件的任务的优先级要么比当前被中断的任务高,要么比其低
- 中断服务程序通过调用系统的发布服务函数向任务发布消息或信号,在延迟发布模式下,这个过程不是直接进行发布操作,而是将这个发布函数调用和相应的参数写入到专用队列中,该队列称为中断队列。然后使中断队列处理任务进入就绪态,这个任务是UCOSIII 的内部任务,并且具有最高优先级(0)
- 中断服务程序处理结束时,UCOSIII 切换执行中断队列处理任务,该任务从中断队列中提取出发布函数调用信息,此时仍需要关闭中断以防止中断服务程序同时对中断队列进行访问。中断队列处理任务提取出发布函数调用的信息后重新开中断,锁定任务调度器,然后进行发布函数调用,相当于发布函数调用一直是在任务级代码中进行的,这样本来应该在临界段中处理的代码就被放到了任务级完成
- 断队列处理任务将中断队列处理完,将自身挂起,并重新启动任务调度来运行处于最高优先级的就绪任务。如果原先被中断的任务仍然是最高优先级的就绪任务,则 UCOSIII恢复运行这个任务
- 于中断队列处理任务的发布操作使得更重要的任务进入就绪态,内核将切换到更高优先级的任务运行
- 在使用延迟发布模式额外增加的操作都是为了避免使用关中断来保护临界段代码。这些额外增加的操作仅包括将发布调用及其参数复制到中断队列中、从中断队列提取发布调用和相关参数以及一次额外的任务切换
7.3. 两者对比
直接发布模式下,UCOSIII 通过关闭中断来保护临界段代码。延迟发布模式下,UCOSIII通过锁定任务调度来保护临界段代码。
延迟发布模式下,UCOSIII 在访问中断队列时,仍然需要关闭中断,但这个时间是非常短的。
如果应用中存在非常快速的中断请求源,则当 UCOSIII 在直接发布模式下的中断关闭时间不能满足要求的时候,可以使用延迟发布模式来降低中断关闭时间。
-
八、 临界段代码保护
有一些代码我们需要保证其完成运行,不能被打断,这些不能被打断的代码就是临界段代码,也叫临界区。
我们在进入临界段代码的时候使用宏
OS_CRITICAL_ENTER();
退出临界区的时候使用宏
OS_CRITICAL_EXIT(); //或 OS_CRITICAL_EXIT_NO_SCHED();
当宏 OS_CFG_ISR_POST_DEFERRED_EN 定义为 0 的时候,进入临界区的时候 UCOSIII会使用关中断的方式,退出临界区以后重新打开中断。当OS_CFG_ISR_POST_DEFERRED_EN定义为 1 的时候进入临界区前是给调度器上锁,并在退出临界区的时候给调度器解锁。
-
九、 时间管理
9.1. OSTimeDly()函数
当我们需要对一个任务进行延时操作的时候就可以使用这个函数
函数原型:
void OSTimeDly (OS_TICK dly, OS_OPT opt, OS_ERR *p_err)
- dly: 指定延时的时间长度,这里单位为时间节拍数
- opt: 指定延迟使用的选项,有四种选项
- OS_OPT_TIME_DLY : 相对模式
- OS_OPT_TIME_TIMEOUT: 和OS_OPT_TIME_DLY 一样
- OS_OPT_TIME_MATCH: 绝对模式
- OS_OPT_TIME_PERIODIC: 周期模式
- p_err: 指向调用该函数后返回的错误码
“相对模式”在系统负荷较重时有可能延时会少一个节拍,甚至偶尔差多个节拍,在周期模式下,任务仍然可能会被推迟执行,但它总会和预期的“匹配值”同步。因此,推荐使用“周期模式”来实现长时间运行的周期性延时。
“绝对模式”可以用来在上电后指定的时间执行具体的动作,比如可以规定,上电N 秒后关闭某个外设。9.2. OSTimeDlyHMSM()函数
我 们 也 可 调 用 OSTimeDlyHMSM() 函数来更加直观的来对某个任务延时
函数原型:
void OSTimeDlyHMSM (CPU_INT16U hours, CPU_INT16U minutes, CPU_INT16U seconds, CPU_INT32U milli, OS_OPT opt, OS_ERR *p_err)
- hours
- minutes
- seconds
- milli: 前面这四个参数用来设置需要延时的时间,使用的是:小时、分钟、秒和毫 秒这种格式,这个就比较直观了,这个延时最小单位和我们设置的时钟节拍频率有关,比如我们设置时钟节拍频率OS_CFG_TICK_RATE_HZ 为 200 的话,那么最小延时单位就是 5ms
- opt: 比OSTimeDly()函数多了两个选项 OS_OPT_TIME_HMSM_STRICT 和
OS_OPT_TIME_HMSM_NON_STRICT,其他四个选项都一样的。
使用OS_OPT_TIME_HMSM_NON_STRICT 选项的话将会检查延时参数,
hours 的范围应该是 0~99,minutes 的范围应该是 0~59,seconds 的范围为0~59, milli 的范围为 0~999。
使用OS_OPT_TIME_HMSM_NON_STRICT 选项的话,hours 的范围为0~999, minutes 的范围为 0~9999,seconds 的范围为 0~65535,mili 的范围为 0~4294967259。 - *p_err: 调用此函数后返回的错误码
9.3. OSTimeDlyResume()函数
一个任务可以通过调用这个函数来“解救”那些因为调用了 OSTimeDly() 或者OSTimeDlyHMSM()函数而进入等待态的任务
函数原型:
void OSTimeDlyResume (OS_TCB *p_tcb,OS_ERR *p_err)
- *p_tcb: 需要恢复的任务的任务控制块
- *p_err: 指向调用这个函数后返回的错误码
9.4. OSTimeGet()函数
OSTimeGet()函数用来获取当前时钟节拍计数器的值
9.5. OSTimeSet()函数
OSTimeSet()函数可以设置当前时钟节拍计数器的值,这个函数谨慎使用
-
十、 软件定时器
10.1. 定时器工作模式
在学习单片机的时候会使用定时器来做很多定时任务,这个定时器是单片机自带的,也就是硬件定时器,在UCOSIII 中提供了软件定时器,我们可以使用这些软件定时器完成一些功能。
定时器其实就是一个递减计数器,当计数器递减到 0 的时候就会触发一个动作,这个动作就是回调函数,当定时器计时完成时就会自动的调用这个回调函数。因此我们可以使用这个回调函数来完成一些设计。比如,定时 10 秒后打开某个外设等等,在回调函数中应避免任何可以阻塞或者删除定时任务的函数。
定时器的分辨率由我们定义的系统节拍频率 OS_CFG_TICK_RATE_HZ 决定,比如我们定义为200,系统时钟周期就是5ms,定时器的最小分辨率肯定就是5ms。但是定时器的实际分辨率是通过宏 OS_CFG_TMR_TASK_RATE_HZ 定 义 的 , 这 个 值 绝 对 不 能 大 于OS_CFG_TICK_RATE_HZ。比如我们定义 OS_CFG_TMR_TASK_RATE_HZ 为 100,则定时器的时间分辨率为10ms。有关UCOSIII 定时器的函数都在 os_tmr.c 文件中。
10.1.1. 单次定时器
触发单次定时器的时序图
重复触发单词定时器的时序图
10.1.2. 周期定时器
- 无初始化延迟,使用OSTmrCreate()函数创建定时器的时候,参数dly 为0 的话,就没有初始化延迟
- 有初始化延迟,初始化延时就是OSTmrCreate()函数中的参数dly 就是初始化延迟,定时器的第一个周期就是 dly。当第一个周期完成后就是用参数period 作为周期值
10.2. 创建软件定时器
创建一个软件定时器的步骤
- 宏定义使能定时器
#define OS_CFG_TMR_EN 1u #define OS_CFG_TMR_DEL_EN 1u
- 在my_app_timer.c全局定义一个定时器
static OS_TMR mode_change_timer;
3. 使用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: 调用此函数以后返回的错误码
如
//创建模式切换定时器定时器 OSTmrCreate((OS_TMR *)&mode_change_timer, //定时器1 (CPU_CHAR *)"mode_change_timer", //定时器名字 (OS_TICK )2, //定时第一个周期的时间, 2*10=20ms (OS_TICK )1, //第二周期及以后的周期时间, 1*10=10ms (OS_OPT )OS_OPT_TMR_PERIODIC, //周期模式 (OS_TMR_CALLBACK_PTR)mode_change_timer_callback, //定时器1回调函数 (void *)0, //传递给回调函数的参数为0 (OS_ERR *)&err); //返回的错误码
- 编写在上一步中填入的回调函数,即用户自定义内容
static void mode_change_timer_callback(void *p_tmr, void *p_arg) { if (auto_ctrl_cnt) auto_ctrl_cnt--; }
- 开启定时器OSTmrStart
函数原型:
CPU_BOOLEAN OSTmrStart (OS_TMR *p_tmr, OS_ERR *p_err)
- *p_tmr: 定时器名称
- *p_err: 保存错误类型
- 关闭定时器OSTmrStop
函数原型:
CPU_BOOLEAN OSTmrStop (OS_TMR *p_tmr, OS_OPT opt, void *p_callback_arg, OS_ERR *p_err)
- *p_tmr: 定时器名称
- opt: 选项
- OS_OPT_TMR_NONE: 不带选项
- *p_callback_arg: 传递给回调函数的参数
- *p_err: 保存错误类型
-
十一、 信号量
信号量像是一种上锁机制,代码必须获得对应的钥匙才能继续执行,一旦获得了钥匙,也就意味着该任务具有进入被锁部分代码的权限。一旦执行至被锁代码段,则任务一直等待,直到对应被锁部分代码的钥匙被再次释放才能继续执行。
信号量分为二进制信号量和技术型信号量
使用信号量步骤
- 在my_os_msg.c文件中全局定义一个信号量
OS_SEM Flash_write_sem; /* 定义一个信号量 */
-
创建信号量OSSemCreate
函数原型:
void OSSemCreate (OS_SEM *p_sem, CPU_CHAR *p_name, OS_SEM_CTR cnt, OS_ERR *p_err)
-
*p_sem: 指向信号量控制块
-
*p_name: 指向信号量的名字
-
cnt: 设置信号量的初始值,如果此值为 1,代表此信号量为二进制信号量,如果大于1的话就代表此信号量为计数型信号量
-
*p_err: 保存错误码
如
OSSemCreate ( (OS_SEM *)&Flash_write_sem, (CPU_CHAR *)"Flash_write_sem", (OS_SEM_CTR)0, //初始状态有0个信号量 (OS_ERR *)&err);
-
请求信号量OSSemPend
函数原型
OS_SEM_CTR OSSemPend (OS_SEM *p_sem, OS_TICK timeout, OS_OPT opt, CPU_TS *p_ts, OS_ERR *p_err)
- *p_sem: 指向一个信号量的指针
- timeout: 指定等待信号量的超时时间(时钟节拍数),如果在指定时间内没有等到信号量则允许任务恢复执行。如果指定时间为 0 的话任务就会一直等待下去,直到等到信号量
- opt: 用于设置是否使用阻塞模式,有下面两个选项
- OS_OPT_PEND_BLOCKING: 指定信号量无效时,任务挂起以等待信号量
- OS_OPT_PEND_NON_BLOCKING: 信号量无效时,任务直接返回
- *p_ts: 指向一个时间戳,用来记录接收到信号量的时刻,如果给这个参数赋值 NULL,则说明用户没有要求时间戳
- *p_err: 保存错误码
如
OSSemPend(&Flash_write_sem, 0, OS_OPT_PEND_BLOCKING, 0, &err); //请求信号量
- 发送信号量OSSemPost
OS_SEM_CTR OSSemPost (OS_SEM *p_sem, OS_OPT opt, OS_ERR *p_err)
- *p_sem: 指向一个信号量的指针
- opt: 用来选择信号量发送的方式
- OS_OPT_POST_1: 仅向等待该信号量的优先级最高的任务发送信号量
- OS_OPT_POST_ALL: 向等待该信号量的所有任务发送信号量
- OS_OPT_POST_NO_SCHED: 该选项禁止在本函数内执行任务调度操作。即使该函数使得更高优先级的任务结束挂起进入就绪状态,也不会执行任务调度,而是会在其他后续函数中完成任务调度
- *p_err: 保存错误码
-
十二、 互斥信号量
12.1. 优先级反转
优先级反转在可剥夺内核中是非常常见的,在实时系统中不允许出现这种现象,这样会破坏任务的预期顺序,可能会导致严重的后果,如下图
- 任务H 和任务 M 处于挂起状态,等待某一事件的发生,任务L 正在运行
- 某一时刻任务 L 想要访问共享资源,在此之前它必须先获得对应该资源的信号量
- 任务 L 获得信号量并开始使用该共享资源
- 由于任务H 优先级高,它等待的事件发生后便剥夺了任务L 的 CPU 使用权
- 任务H 开始运行
- 任务H 运行过程中也要使用任务 L 正在使用着的资源,由于该资源的信号量还被任务L 占用着,任务H 只能进入挂起状态,等待任务 L 释放该信号量
- 任务 L 继续运行
- 由于任务M 的优先级高于任务L,当任务 M 等待的事件发生后,任务 M 剥夺了任务L 的CPU 使用权
- 任务M 处理该处理的事
- 任务M 执行完毕后,将 CPU 使用权归还给任务L
- 任务L 继续运行
- 最终任务L 完成所有的工作并释放了信号量,到此为止,由于实时内核知道有个高优先级的任务在等待这个信号量,故内核做任务切换
- 任务H 得到该信号量并接着运行
在这种情况下,任务 H 的优先级实际上降到了任务L 的优先级水平。因为任务H 要一直待直到任务L 释放其占用的那个共享资源。由于任务 M 剥夺了任务L 的CPU 使用权,使得任务H 的情况更加恶化,这样就相当于任务 M 的优先级高于任务H,导致优先级反转
12.2. 互斥信号量
为了解决使用普通信号量会出现的优先级反转问题,提出互斥信号量的概念
- 任务H 与任务 M 处于挂起状态,等待某一事件的发生,任务L 正在运行中
- 某一时刻任务 L 想要访问共享资源,在此之前它必须先获得对应资源的互斥型信号量
- 任务 L 获得互斥型信号量并开始使用该共享资源
- 由于任务H 优先级高,它等待的事件发生后便剥夺了任务L 的 CPU 使用权
- 任务H 开始运行
- 任务 H 运行过程中也要使用任务 L 在使用的资源,考虑到任务 L 正在占用着资源UCOSIII 会将任务 L 的优先级升至同任务H 一样,使得任务 L 能继续执行而不被其他中等优先级的任务打断
- 任务 L 以任务H 的优先级继续运行,注意此时任务H 并没有运行,因为任务H 在等待任务L 释放掉互斥信号量
- 任务 L 完成所有的任务,并释放掉互斥型信号量,UCOSIII 会自动将任务 L 的优先级恢复到提升之前的值,然后 UCOSIII 会将互斥型信号量给正在等待着的任务H
- 任务H 获得互斥信号量开始执行
- 任务H 不再需要访问共享资源,于是释放掉互斥型信号量
- 由于没有更高优先级的任务需要执行,所以任务H 继续执行
- 任务 H 完成所有工作,并等待某一事件发生,此时 UCOSIII 开始运行在任务 H 或者任务L 运行过程中已经就绪的任务 M
- 任务M 继续执行
12.3. 互斥信号量使用步骤
- 在my_os_msg.c中全局定义一个互斥信号量
OS_MUTEX FILE_CONTRL; /* 定义一个互斥信号量 */
-
创建互斥信号量OSMutexCreate
函数原型
void OSMutexCreate (OS_MUTEX *p_mutex, CPU_CHAR *p_name, OS_ERR *p_err)
- *p_mutex: 指向互斥型信号量控制块
- *p_name: 互斥信号量的名字
- *p_err: 保存错误码
如
/*** 创建一个互斥信号量, 用来给文件读写加锁 ***/ OSMutexCreate((OS_MUTEX *)&FILE_CONTRL, (CPU_CHAR *)"FILE_CONTRL", (OS_ERR *)&err);
-
请求互斥信号量OSMutexPend
函数原型
void OSMutexPend (OS_MUTEX *p_mutex, OS_TICK timeout, OS_OPT opt, CPU_TS *p_ts, OS_ERR *p_err)
- *p_mutex: 指向互斥信号量
- timeout: 指定等待互斥信号量的超时时间(时钟节拍数),如果在指定的时间内互斥信号量没有释放,则允许任务恢复执行。该值设置为 0 的话,表示任务将会一直等待下去,直到信号量被释放掉
- opt: 用于选择是否使用阻塞模式
- OS_OPT_PEND_BLOCKING: 指定互斥信号量被占用时,任务挂起等待该互斥信号量
- OS_OPT_PEND_NON_BLOCKING: 指定当互斥信号量被占用时,直接返回任务,注意!当设置为 OS_OPT_PEND_NON_BLOCKING,是 timeout 参数就没有意义了,应该设置为 0
- *p_ts: 指向一个时间戳,记录发送、终止或删除互斥信号量的时刻
- *p_err: 保存错误码
如
OSMutexPend (&FILE_CONTRL, 0, OS_OPT_PEND_BLOCKING, 0, &err); //请求互斥信号量
-
发送互斥信号量OSMutexPost
函数原型:
void OSMutexPost (OS_MUTEX *p_mutex, OS_OPT opt, OS_ERR *p_err)
- *p_mutex: 指向互斥信号量
- opt: 用来指定是否进行任务调度操作
- OS_OPT_POST_NONE: 不指定特定的选项
- OS_OPT_POST_NO_SCHED: 禁止在本函数内执行任务调度操作
- *p_err: 保存错误码
如:
OSMutexPost(&FILE_CONTRL, OS_OPT_POST_NONE, &err); //释放互斥信号量
-
十四、 消息队列
可以理解为UCOS自带的队列库函数,用于通过队列向别的任务传递消息
14.1. 消息队列使用步骤
- 定义一个消息队列
OS_Q Msg_Que
-
创建消息队列OSQCreate
函数原型:
void OSQCreate (OS_Q *p_q, CPU_CHAR *p_name, OS_MSG_QTY max_qty, OS_ERR *p_err)
- *p_q: 指向一个消息队列
- *p_name: 消息队列的名字
- max_qty: 指定消息队列的长度,必须大于 0。当然,如果 OS_MSGs 缓冲池中没有足够多的 OS_MSGs 可用,那么发送消息将会失败,并且返回相应的错误码,指明当前没有可用的 OS_MSGs
- *p_err: 保存错误码
-
等待消息队列OSQPend
函数原型:
void *OSQPend (OS_Q *p_q, OS_TICK timeout, OS_OPT opt, OS_MSG_SIZE *p_msg_size, CPU_TS *p_ts, OS_ERR *p_err)
- *p_q: 指向一个消息队列
- timeout: 等待消息的超时时间,如果在指定的时间没有接收到消息的话,任务就会被唤醒接着运行。这个参数也可以设置为 0,表示任务将一直等待下去,直到接收到消息
- opt: 用来选择是否使用阻塞模式,有两个选项可以选择
- OS_OPT_PEND_BLOCKING: 如果没有任何消息存在的话就阻塞任务,一直等待,直到接收到消息
- OS_OPT_PEND_NON_BLOCKING: 如果消息队列没有任何消息的话任务就直接返回
- *p_msg_size: 指向一个变量用来表示接收到的消息长度(字节数)
- *p_ts: 指向一个时间戳,表明什么时候接收到消息。如果这个指针被赋值为NULL的话,说明用户没有要求时间戳
- *p_err: 保存错误码
-
向消息队列发送消息OSQPost
函数原型:
void OSQPost (OS_Q *p_q, void *p_void, OS_MSG_SIZE msg_size, OS_OPT opt, OS_ERR *p_err)
-
*p_q: 指向一个消息队列
-
*p_void: 指向实际发送的内容,p_void 是一个执行 void 类型的指针,其具体含义由用户程序的决定
-
msg_size: 设定消息的大小,单位为字节数
-
opt: 用来选择消息发送操作的类型,基本的类型可以有下面四种
- OS_OPT_POST_ALL: 将消息发送给所有等待该消息队列的任务,需要和选项OS_OPT_POST_FIFO 或者 OS_OPT_POST_LIFO 配合使用
- OS_OPT_POST_FIFO: 待发送消息保存在消息队列的末尾
- OS_OPT_POST_LIFO: 待发送的消息保存在消息队列的开头
- OS_OPT_POST_NO_SCHED: 禁止在本函数内执行任务调度
我们可以使用上面四种基本类型来组合出其他几种类型,如下:
OS_OPT_POST_FIFO + OS_OPT_POST_ALL
OS_OPT_POST_LIFO + OS_OPT_POST_ALL
OS_OPT_POST_FIFO + OS_OPT_POST_NO_SCHED
OS_OPT_POST_LIFO + OS_OPT_POST_NO_SCHED
OS_OPT_POST_FIFO + OS_OPT_POST_ALL + OS_OPT_POST_NO_SCHED
OS_OPT_POST_LIFO + OS_OPT_POST_ALL + OS_OPT_POST_NO_SCHED
-
*p_err: 保存错误码
14.2. 任务内建消息队列
和任务信号量一样,UCOSIII 中每个任务也都有其内建消息队列,这样的话用户就不需要使用外部的消息队列就可直接向任务发布消息,这个特性不仅简化了代码,而且比使用外部消息队列更加有效
使用步骤
-
开启宏定义OS_CFG_TASK_Q_EN
-
等待任务内建消息OSTaskQPend
函数原型
void *OSTaskQPend (OS_TICK timeout, OS_OPT opt, OS_MSG_SIZE *p_msg_size, CPU_TS *p_ts, OS_ERR *p_err)
-
发送任务内建消息OSTaskQPost
函数原型
void OSTaskQPost (OS_TCB *p_tcb, void *p_void, OS_MSG_SIZE msg_size, OS_OPT opt, OS_ERR *p_err)
- *p_tcb: 指向接收消息的任务的 TCB,可以通过指定一个 NULL 指针或该函数调用者的TCB 地址来向该函数的调用者自己发送一条消息
-
十五、 事件标志组
有时候一个任务可能需要和多个事件同步,这个时候就需要使用事件标志组
使用步骤
- 在my_os_msg.c文件中全局定义一个事件标志组
OS_FLAG_GRP ctrl_data_event;
- 在my_os_msg.h头文件中定义事件和事件标志组初始值
#define VISUAL_DATA_FLAG 0x01 //视觉信息到来事件 #define FORCE_DBUS_DATA_FLAG 0x02 //强制遥控事件 #define CTRL_DATA_FLAG_VALUE 0x00 //事件标志组初始值
事件由比特序列表示,事件1为b'0000 0001',即16进制1;事件2为b'0000 0010',即16进制2;事件3为b'0000 0100',即16进制4,依次类推
-
等待事件OSFlagPend
函数原型:
OS_FLAGS OSFlagPend (OS_FLAG_GRP *p_grp, OS_FLAGS flags, OS_TICK timeout, OS_OPT opt, CPU_TS *p_ts, OS_ERR *p_err)
-
*p_grp: 指向事件标志组
-
flags: bit 序列,任务需要等待事件标志组的哪个位就把这个序列对应的位置 1,根据设置这个序列可以是 8bit、16bit 或者32 比特,比如任务需要等待时间标志组的bit0和bit1 时(无论是等待置位还是清零),flag 是的值就为0X03
-
timeout: 指定等待事件标志组的超时时间(时钟节拍数),如果在指定的超时时间内所等待的一个或多个事件没有发生,那么任务恢复运行。如果此值设置为 0,则任务就将一直等待下去,直到一个或多个事件发生
-
opt: 决定任务等待的条件是所有标志置位、所有标志清零、任意一个标志置位还是任意一个标志清零,具体的定义如下
-
OS_OPT_PEND_FLAG_CLR_ALL: 等待事件标志组所有的位清零
-
OS_OPT_PEND_FLAG_CLR_ANY: 等待事件标志组中任意一个标志清零
-
OS_OPT_PEND_FLAG_SET_ALL: 等待事件标志组中所有的位置位
-
OS_OPT_PEND_FLAG_SET_ANY: 等待事件标志组中任意一个标志置位
调用上面四个选项的时候还可以搭配下面三个选项
-
OS_OPT_PEND_FLAG_CONSUME: 用来设置是否继续保留该事件标志的状态
-
OS_OPT_PEND_NON_BLOCKING: 标志组不满足条件时不挂起任务
-
OS_OPT_PEND_BLOCKING: 标志组不满足条件时挂起任务
这里应该注意选项 OS_OPT_PEND_FLAG_CONSUME 的使用方法,如果我们希望任务等待事件标志组的任意一个标志置位,并在满足条件后将对应的标志清零那么就可以搭配使用选项 OS_OPT_PEND_FLAG_CONSUME
-
-
*p_ts: 指向一个时间戳,记录了发送、终止和删除事件标志组的时刻,如果为这个指针赋值NULL,则函数的调用者将不会收到时间戳
-
*p_err: 保存错误码
-
返回值为返回等待到的事件比特序列,如果等待多个事件时用于判断等到的是什么事件
如
OS_FLAGS os_flags_num; os_flags_num = OSFlagPend((OS_FLAG_GRP*)&ctrl_data_event, (OS_FLAGS )VISUAL_DATA_FLAG+FORCE_DBUS_DATA_FLAG, (OS_TICK )10, //等待10个时间片,即10ms (OS_OPT )OS_OPT_PEND_FLAG_SET_ANY+OS_OPT_PEND_FLAG_CONSUME, (CPU_TS *)0, (OS_ERR *)&err); switch (os_flags_num) { case 0: //等待超时,未等到任何事件 visual_no_use_flag++; if (visual_no_use_flag > 20) { //200ms内未传来视觉数据 visual_no_use_flag = 30; rc_ctrl_mode(); } else { visual_ctrl_mode(); } break; case VISUAL_DATA_FLAG: //视觉信息到来事件 visual_no_use_flag = 0; visual_ctrl_mode(); break; case FORCE_DBUS_DATA_FLAG: //强制进入遥控器控制模式事件 default: break; //退出至下一个循环,进入到上档强制遥控器模式 }
-
向事件标志组发布标志OSFlagPost
函数原型:
OS_FLAGS OSFlagPost (OS_FLAG_GRP *p_grp, OS_FLAGS flags, OS_OPT opt, OS_ERR *p_err)
- *p_grp: 指向事件标志组
- flags: 决定对哪些位清零和置位,当 opt 参数为OS_OPT_POST_FLAG_SET 的时,参数flags 中置位的位就会在事件标志组中对应的位也将被置位。当 opt 为 OS_OPT_POST_FLAG_CLR 的时候参数 flags 中置位的位在事件标志组中对应的位将被清零
- opt: 决定对标志位的操作,有两种选项
- OS_OPT_POST_FLAG_SET: 对标志位进行置位操作
- OS_OPT_POST_FLAG_CLR: 对标志位进行清零操作
- *p_err: 保存错误码
- 返回值为返回发布标志后的事件比特序列
如
OSFlagPost( (OS_FLAG_GRP*)&ctrl_data_event, (OS_FLAGS )VISUAL_DATA_FLAG, (OS_OPT )OS_OPT_POST_FLAG_SET, (OS_ERR *)&err);
-
十三、 任务内嵌信号量
前面我们使用信号量时都需要先创建一个信号量,不过在UCOSIII 中每个任务都有自己的内嵌的信号量,这种功能不仅能够简化代码,而且比使用独立的信号量更有效。任务信号量是直接内嵌在UCOSIII 中的
-
等待任务信号量OSTaskSemPend
OStaskSemPend()允许一个任务等待由其他任务或者 ISR 直接发送的信号,使用过程基本和独立的信号量相同
函数原型:
OS_SEM_CTR OSTaskSemPend (OS_TICK timeout, OS_OPT opt, CPU_TS *p_ts, OS_ERR *p_err)
-
发布任务信号量OSTaskSemPost
函数原型:
OS_SEM_CTR OSTaskSemPost (OS_TCB *p_tcb, OS_OPT opt, OS_ERR *p_err)
- *p_tcb: 指向要用信号通知的任务的 TCB,当设置为NULL 的时候可以向自己发送信量
-
-
总结得挺好的,内容丰富
-
不愧是熊神,这也太硬核了吧!
-
要不要来个RT-thread?期待ing