任务和任务管理是 RTOS 的核心,且大多数项目使用 RTOS 的目的就是为了使用 RTOS 的多任务管理能力。 µC/OS-III作为经典的RTOS,了解并学习其任务管理机制,是非常有必要的。
单任务系统就是在裸机上编程,一般都是在 main()函数初始化完成后使用一个while大循环,在循环中顺序地调用相应地函数以处理相应的事务,在while(1)循环中插入各种中断服务函数,整个逻辑顺序依次而行,所以我们也一般称为“流水账”编程方式。如下图所示:
可以看出,整个代码在while大循环中依次执行,其中部分地方插入点中断,这种框架的优势就是代码较易读,基本上函数调用的顺序就是整个软件各个模块的执行顺序;但是这种框架短板也非常明显,那就是对整个软件各个模块时效控制是很低的,没有轻重缓急之分,比如函数1是控制灯闪烁的函数,需要循环一圈才会调用一次,而且因为不知道函数2和函数2以及中断的执行时间,所以你也没法确定你的闪烁周期,如果删除或增加一个函数模块,你这个周期还会变。
多任务系统在处理事务的实时性上比单任务系统要好得多,从宏观上来看,多任务系统的多个任务是可以“并行”的,紧急的事务就可以提高优先级被CPU 优先处理。要注意的是,µC/OS-III 作为单核CPU上的操作系统,其实同一时刻CPU只能处理一个任务,所说的多任务并行其实是通过高效的任务调度算法合理分配各个任务占用CPU的时间,使得各个任务都会根据配置高效运行,所以看起来各个任务像是“并行”一样。如下图所示:
从上图可以看出,相较于单任务系统而言,多任务系统的任务是具有优先级的,高优先级的任务可以像中断的抢占机制一样,抢占低优先级任务的运行,从而获得 CPU 的使用权;各个任务时效也是可以根据配置参数进行配置的,整个系统的实时性和可控性大大提高。
µC/OS-III 中任务存在五种状态,分别为休眠态、就绪态、运行态、挂起态和中断态,某一时刻一个任务一定是处于这五种状态中的一种, 整个µC/OS-III 中的五种任务状态之间的转换图如:
这是经典的µC/OS-III任务状态转换图,标准了五种不同任务状态的转化方式
■. 休眠态:通过函数 OSTaskDel()删除的任务就会回到休眠态,这里所说的删除,并非将任务从代码空间中删除,而仅仅是将任务切换到休眠态,让 µC/OS-III 内核不再管理这个任务,需要通过调用函数 OSTaskCreate()对一个处于休眠态的任务进行重新创建。
■. 就绪态:准备好运行但还未运行时的任务就处于就绪态。此时任务处于µC/OS-III 就绪列表OSRdylist[X] ,X表示任务优先级数值0-31(32位变量,由高位到低位依次排列)。
■. 运行态:对于单核 CPU 的 MCU 而言,就是是当前正在运行的且唯一的任务所处状态,称为运行态。
■. 挂起态:运行态任务因延时或等待某事件被挂起,例如等待信号量、消息队列或事件标志等,这时运行态任务就会让出 CPU 的使用权,切换到挂起态等待事件发生。 当等待的事件发生后,处于挂起态的任务就会切换到就绪态,等待任务调度器调度运行。
■. 中断态:处于运行态任务被打断,CPU跳转去执行中断服务函数,原本属于运行态的任务会切换到中断态,直到中断结束,再切换回来继续运行
总结:
A. 创建的任务默认是就绪态
B. 被删除的任务,转为休眠态
C. 仅就绪态和中断态可转变成运行态
D. 其他状态的任务要运行,必须转成就绪态
在µC/OS-III中,优先级是决定任务调度器如何分配 CPU 使用权的因素之一。因此µC/OS-III中每一个任务都被分配一个0~(OS_CFG_PRIO_MAX-1)的任务优先级,宏 OS_CFG_PRIO_MAX 表示最大优先级数, 并且 µC/OS-III 中一个任务只能有一个优先级,一个优先级下可以有多个任务。
在 cpu_cfg.h文件中有宏CPU_CFG_LEAD_ZEROS_ASM_PRESENT,该宏用于配置 µC/OSIII 使用硬件指令的方法或是软件算法的方法计算前导零数量, µC/OS-III 使用位图的方式记录当前系统中存在的所有任务优先级, 在 µC/OS-III 系统中存在的最高任务优先级时,就会使用到前导零计数。前导置零指指令CPU_CntLeadZeros():简单理解就是计算一个32位数头部有多少个0,OS_PrioGetHighest()函数通过前导置0指令获得最高优先级。
µC/OS-III 的任务优先级高低与其对应的任务优先级数值是成反比的,也就是说,任务优先级数值为 0 的任务是最高优先级的任务,任务优先级数值为(OS_CFG_PRIO_MAX-1)的任务是优先级最低的任务。
任务调度:就是任务调度器是使用相关算法来决定当前需要执行的哪个任务,在µC/OS-III 中有两种任务调度规则:
首先已经知道在µC/OS-III 每一个任务都会根据其重要性被分配一个任务优先级,抢占式调度的意思任务优先级高的任务就能够抢占任务优先级低的任务,从而获得 CPU的使用权。抢占式调度特点如下:
A:高优先级优先执行
B:高优先级任务不停止,低优先级任务无法执行
C:被抢占的任务会进入就绪态
D:被挂起的高优先级的任务所等待的事件在中断中发生,中断服务函数退出后不会返回任务优先级低的任务运行,会直接返回任务优先级高的任务去运行
µC/OS-III 每一个优先级下可能不止一个任务,那么这些任务的优先级都相同,此时又该怎么调度这些任务呢?
当这些具有相同任务优先级的任务就绪时,任务调度器会根据用户设置的任务时间片轮流地运行这些任务,当然这些任务的运行依然会被任务优先级更高的任务抢占。时间片调度特点如下:
A:同等优先级任务,轮流执行,根据时间片流转依次
B:一个时间片大小取决于滴答定时器中断频率
C:每个任务可以定义自己的时间片长度
D:没有使用的时间片,下次执行不会累加,依旧是初始的时间长度
时间片是依一次系统时钟节拍为单位的,uCOS默认设置的任务时间片为100,则当前任务运行100次时钟节拍之后就切换到相同任务优先级的任务中。
将任务操作函数之前,先说一个重要概念一一任务堆栈
µC/OS-III 中每个任务都有自己独立的堆栈空间,这个栈空间大小是在创建任务时就确定好的,任务中函数中的局部变量、函数调用时的现场保护和现场恢复等都是要使用到该栈空间的。创建任务函数如下:
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, /*任务选项共5个*/
OS_ERR *p_err) /*指向接收错误代码变量指针*/
其中参数 p_stk_base 就是任务栈空间的首地址,参数 stk_size 就是任务栈空间的大小,对于 STM32 所使用的 ARM Cortex-M 对应的 CPU_STK 定义如下所示:
typedef unsigned int CPU_INT32U;
typedef CPU_INT32U CPU_STK
因此 stk_size 表示的任务栈大小的单位为字,因此实际栈大小为 :stk_size*4(1 字为 4 字节)字节。
1.创建任务函数OSTaskCreat():任务创建后会进入就绪态,(如果是多个任务会根据创建的先后顺序以此执行,然后才会根据优先级独立运行,所以要根据运行顺序调整创建顺序或优先级,或者创建任务时使用临界区,然后创建完成释放临界区,任务会根据优先级执行。
2.任务创建流程:
A. 定义函数入口参数(任务控制块、堆栈大小、优先级、任务名称…….
B. 调用创建任务函数
C. 实现函数功能
D. 创建后任务就立即进入就绪态,
注意事项:
1.在调用任何关于μCOS3函数之前必须先初始化μCOS3,仅需要初始化一次即可OSInit(&err);
2.任务创建后是不会直接运行的,需要开启任务调度器,任务才会运行,仅需要调用一次即可OSStart(&err);
1.删除任务函数OSTaskDel (OS_TCB *p_tcb,OS_ERR *p_err):不需要该任务时可以删除任务,但是不会释放该任务堆栈和删除码,
注意事项:不能删除空闲任务
注意:中断中不能创建和删除任务,只能在任务中执行,任务可以删除自己
挂起任务:OSTaskSuspend (OS_TCB *p_tcb,OS_ERR *p_err):可以无条件(无论该任务处于什么状态(被删除的除外))的挂起任务,被挂起的任务不会参与任务调度,如果被挂起的任务是当前真正执行的,那么会发生任务调度,将CPU使用权交给另一个任务,无论优先级如何,被挂起之后某人任务不再执行直到任务恢复。当出入的任务控制块指针是0,表示挂起任务自身。
注意事项:
挂起任务本质就是将就绪列表的任务移除(不是删除),不参与运行
OSTaskResume (OS_TCB *p_tcb,OS_ERR *p_err):用于恢复被OSTaskSuspend()函数挂起的任务,可多次调用OSTaskSuspend挂起同一个任务,解挂需要调用相同次数的OSTaskResume()才能解挂;恢复任务的目的是将任务插回就绪列表。