【FreeRTOS】第五课:任务管理
目录
一、前言
1.程序的大致流程
- 任务就是函数,每一个任务都相当于一个独立运行的小main函数,由链表进行管理,在FreeRTOS操作系统中,将链表中存储的任务结构体称为任务控制块(TCB,Task Control Block)
- 默认会创建出一个任务函数
- 每一个任务函数在内存中都需要有自己的栈帧空间存储程序
- 任务之间亦有优先级高低之别
- 有动态和静态两种方法创建任务(使用函数)

程序总体流程大致如下:

2.任务函数的原型与使用
任务函数原型如下:
void ATaskFunction( void *pvParameters )
- 无返回值,内部需要使用死循环,不允许程序执行完,执行完会导致程序错误,具体原因见 四(3)空闲任务,如果想要结束一个任务只能使用删除函数将其删除
- 如果任务函数无需参数需要使用 void* 类型的指针进行说明
- 同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数
- 函数内部尽量使用局部变量,防止同一个变量被多个任务混乱使用
示例
void ATaskFunction( void *pvParameters ) { /* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */ int32_t lVariableExample = 0; /* 任务函数通常实现为一个无限循环 */ for( ;; ) { /* 任务的代码 */ } /* 如果程序从循环中退出,一定要使用vTaskDelete删除自己,NULL表示删除的是自己 */ vTaskDelete( NULL ); /* 程序不会执行到这里, 如果执行到这里就出错了 */ }
可以回顾:【FreeRTOS】第二课:创建第一个多任务系统 _xtaskcreate参数-CSDN博客
二、创建任务函数的函数原型
1、创建任务
1)动态(常用)
动态开辟一块内存,此时的栈是初始化的大小,可以不断地扩容
使用动态创建任务得到的链表任务结构体(最后一个参数)也是动态的
使用动态创建任务的函数,需要在FreeRTOSConfig.h中将宏configSUPPORT_DYNAMIC_ALLOCATION设置为1(默认就是1)
- 创建任务函数的函数原型
- 举例
数据类型解释
- typedef long BaseType_t;
- typedef void (*TaskFunction_t)( void * );
- #define configSTACK_DEPTH_TYPE uint16_t
- typedef unsigned long UBaseType_t;
- typedef void * TaskHandle_t;
新的类型BaseType_t实际是long
新的类型TaskFunction_t实际是函数指针变量类型,保存的是void ( void * )类型的函数的地址
新的类型configSTACK_DEPTH_TYPE实际是uint16_t
新的类型UBaseType_t实际是unsigned long
新的类型TaskHandle_t实际是void*类型
2)静态
静态分配中也有栈的大小这一参数,但此时栈的大小指的是静态分配的栈的大小,也就是自己创建的数组的大小
使用静态创建任务得到的链表任务结构体(最后一个参数)也是静态的
使用静态创建任务的函数,需要在FreeRTOSConfig.h中将宏configSUPPORT_STATIC_ALLOCATION设置为1
- 创建任务函数的函数原型
- 举例
数据类型解释
- typedef void * TaskHandle_t;
- typedef void (*TaskFunction_t)( void * );
- typedef unsigned long UBaseType_t;
- typedef portSTACK_TYPE StackType_t; #define portSTACK_TYPE uint32_t
- typedef struct xSTATIC_TCB{...}StaticTask_t;
新的类型TaskHandle_t实际是void*类型
新的类型TaskFunction_t实际是函数指针变量类型,保存的是void ( void * )类型的函数的地址
新的类型UBaseType_t实际是unsigned long
新的类型StackType_t实际是uint32_t
新的类型StaticTask_t实际是struct xSTATIC_TCB{...}结构体
2、删除任务
每一个创建的任务必须是死循环,不能存在任务运行结束或任务有返回值导致任务提前退出的情况,否则会导致程序进入prvTaskExitError( void )函数,让程序死机
所以想要让任务退出只能删除任务
删除任务有两种方法:
- 自己删除自己
- 别人删除自己
任务删除函数可以删除处于任何状态的任务
1)自删
自己处于运行状态,调用函数vTaskDelete(),传入参数为NULL,将自己删除
2)他删
别人处于运行状态,调用函数vTaskDelete(),传入参数为要删除的任务的句柄,将
删除任务函数的函数原型
函数传入的参数类型实际上是void*
不论是静态还是动态创建任务都会有函数句柄这一参数
静态任务的返回值就是句柄,动态任务创建会用到句柄作为参数
以蜂鸣器播放音乐为例:虽然任务被删除了但是功能函数可能还在执行,所以需要将功能函数也关闭
3.其他注意事项
1)后创建的任务先执行
在后面的任务管理片段处也有介绍


此处文字相当于图片的解释
任务由一个数组链表进行管理
每一个数组元素都是一个链表,数组的下标表示优先级,数字越大优先级越高
由于每创建一个任务都会让链表的指针pxCurrentTCB往后移动,所以创建任务结束后只能得到最后一个任务的地址
程序执行pxCurrentTCB指向的任务,经过Tick时长后中断再重新遍历数组去寻找下一个任务
pxCurrentTCB按数组下标从大到小(即任务优先级从高到低)遍历数组,当数组元素非空时将pxCurrentTCB指向此时数组元素的链表中的第一个任务,当第一个任务执行一个Tick时间后再次中断,每个链表中都有一个专门的变量index用于记录此链表已被执行的任务位置以保障每个任务都被执行前不会发生有某一个任务被重复执行的情况,
当执行完一个任务后pxCurrentTCB会再次重新遍历数组,如此往复,除非阻塞或者停止高优先级的任务,否则低优先级的任务永远不会执行
当高优先级的任务就绪后会立即打断低优先级的任务立刻执行,如果想要让高优先级的任务不影响低优先级的任务需要将其阻塞一段Tick时间,或者将其暂停,或者将其删除
- 调用阻塞函数vTaskDelay()传入要阻塞的Tick时间周期会使任务进入阻塞状态,处于阻塞状态的任务会被任务数组删去,转而放在专门存放阻塞任务的数组中,低优先级的任务从而得以执行
当阻塞时间到达设定值后,任务会自动被放进原任务数组下标的链表中运行,此时所有优先级低于它的任务全都再次不能运行
- 调用暂停函数vTaskSuspend()传入要暂停的任务的句柄会使任务进入暂停状态,处于暂停状态的任务会被任务数组删去,转而放在专门存放暂停任务的数组中,低优先级的任务从而得以执行
直到暂停状态被转换为运行状态后,任务才会自动被放进原任务数组下标的链表中运行,此时所有优先级低于它的任务全都再次不能运行
2)在任务函数中可以再创建任务
在任务函数中可以再使用任务创建函数创建任务

3)头文件中可能涉及的宏的修改
动态创建任务
使用动态创建任务的函数,需要在FreeRTOSConfig.h中将宏configSUPPORT_DYNAMIC_ALLOCATION设置为1(默认就是1)
静态创建任务
使用静态创建任务的函数,需要在FreeRTOSConfig.h中将宏configSUPPORT_STATIC_ALLOCATION设置为1
删除任务
使用删除任务的函数,需要在FreeRTOSConfig.h中将宏INCLUDE_vTaskDelete设置为1
四、任务状态与任务调度(非常重要)
FreeRTOS本质也是使用定时器分时执行不同的程序,当任务过多时会存在卡顿的现象,比如蜂鸣器播放的音乐节奏变得拖拉
1.四种任务状态和转换

- Ready(就绪状态)
- Running(运行状态)
- Blocked(阻塞状态)
- Suspended(暂停状态)
任务一被创建就处于就绪状态,
正在执行就处于运行状态,
正在运行的任务可以让自己或其他任务处于暂停状态(通过调用函数传入任务句柄的方法)
正在运行的任务也可以让暂停状态的任务重新运行(通过调用函数传入任务句柄的方法)
正在运行的任务也可以让自己处于阻塞状态(有多种方法)
处于阻塞状态的任务经过设定的阻塞时间后会被调度器唤醒
处于暂停状态的任务除非有处于运行状态的任务接触它的暂停状态,否则永远不会被唤醒
示例
2.任务管理
- 高优先级的任务先运行
- 相同优先级的任务轮流运行
- 高优先级任务未执行完低优先级任务无法执行
- 高优先级任务一旦就绪马上运行
任务统一使用链表进行管理
数组每一项元素都是一个链表,数组下标为56,能够寻找的下标范围为0~55,对应任务优先级从低到高,默认优先级对应的数组元素下标为24
每个数组元素都是一个链表,链式存储该优先级下处于Ready或Running状态的任务
当优先级相同的情况下,每隔一个 Tick 时长就会自动切换任务
对于高优先级的任务,即使不需要延时函数,也应该在重要的程序被执行后调用阻塞函数,给低优先级的任务能被运行的机会
图解


3.空闲任务
由删除任务处的知识点可知:一个任务想要结束只能“自杀”或“他杀”
如果是“他杀”,也就是A任务把B任务删除了,A任务会替B任务“收尸”
如果是“自杀”,则只能由空闲的任务来“收尸”
“收尸”就是将被删除的任务原先的栈帧释放
空闲任务只会处于就绪或运行状态,永远不会阻塞
空闲任务的优先级最低,在任务数组下标为0的链表中,这也就意味着只要空闲任务前面有优先级高的任务不处于阻塞或暂停状态,即使有很多被删除空闲任务也无法得到运行的机会,这会导致内存空间严重不足,所以对于高优先级的任务,即使不需要延时函数,也应该在重要的程序被执行后调用阻塞函数,给低优先级的任务能被运行的机会

4.RTOS中的两个Delay函数
因为任务转换或是空闲任务替被删除的任务处理栈帧的原因,不建议使用自己的Delay函数,而是使用RTOS提供的Delay函数,系统的Delay函数可以让当前正在运行的程序进入阻塞状态,进入阻塞状态的任务会从运行数组移动到阻塞数组,从而让运行数组中低优先级的任务能够都到运行
两个Delay函数
- vTaskDelay():参数为要阻塞的 Tick 时间数,用于实现普通的延时,由于每次程序运行的时间不确定,所以被唤醒时总时间线上的Tick计数值无法确定,不能实现固定周期的唤醒,不过平常使用也足够了


- vTaskDelayUtil():第一个参数为起始时Tick的计数值(可以使用函数xTaskGetTickCount()获得),第二个参数为想要阻塞的Tick周期。由于只关心起始时总时间线上的Tick计数值和需要阻塞的Tick计数值,即保证程序运行时间和阻塞时间总共为x个Tick值,所以可以实现固定周期的阻塞和唤醒。
如果单单程序运行时间就超过了x个Tick值,那么这一次的阻塞会直接跳过。比如起始Tick为0,设置第二个参数为10个Tick,如果第一次程序运行用了3个Tick,那么阻塞时间就是7个Tick,如果第二次程序运行用了12个Tick,那么这一次的阻塞时间跳过,用下一个的唤醒时间计算,即会被阻塞8个Tick
五、任务函数大小的估计
一个空的函数自己最大需要R4~R11+LR共九个寄存器,即至少需要9*4比特空间
如果空的函数还调用了其他空的函数则最多需要n*36比特的空间
如果函数内部使用了很多数据以及使用了大空间的数组,则需要考虑开辟的空间更大
六、多个任务函数使用同一个功能函数(以显示屏为例)
1.多个任务使用显示屏不同区域(使用标志位进行保护)
例如任务1使用显示屏第一行,任务2使用显示屏第二行,任务3使用显示屏第三行,则三个任务轮流运行,因为任务使用的区域各自独立,所以只需要考虑通信时序是否完整的问题
又因为显示屏使用IIC通信,想要在显示屏上都能正常显示,需要通信时序不能被打断,所以需要将通信过程保护起来,每次任务切换前需要判断当前通信是否完成,同时需要加延时

更多推荐











当阻塞时间到达设定值后,任务会自动被放进原任务数组下标的链表中运行,此时所有优先级低于它的任务全都再次不能运行
所有评论(0)