目录
1. 什么是多任务系统?
2. FreeRTOS 任务与协程
2.1 任务 (Task) 的特性
2.2 协程(Co - routine)的特性
3. 任务状态
4. 任务优先级
5. 任务实现
6. 任务控制块
7. 任务堆栈
RTOS 系统的核心就是任务管理,FreeRTOS 也不例外,我们学习 RTOS 主要是为了使用 RTOS 的多任务处理功能;首先要做的就是必须掌握任务的创建、删除、挂起和恢复等操作,由此可见任务管理的重要性。
我们之前在学习51单片机、STM32单片机裸机(未使用系统)的时候,一般都是在 main 函数里面用 while(1) 做一个大循环来完成所有的处理,也就是说应用程序是一个无限的循环,在循环中调用相应的函数完成所需的处理,有的时候我们也会在中断里面添加一些函数完成一些处理。
相对于多任务系统而言,这个就是单任务系统,也称作前后台系统,中断服务函数作为前台程序,大循环 while(1) 作为后台程序。
前后台系统的实时性差,前后台系统各个任务(应用程序)都是排队等着轮流执行,不管你这个程序现在有多紧急,没轮到你就只能等着!相当于所有任务(应用程序)的优先级都是一样的。但是前后台系统的优点明显是简单,而且资源消耗的也少!但是在稍微大一点的嵌入式应用中前后台系统就明显力不从心了,此时就需要多任务系统出马了。
多任务系统会把一个大问题(应用) “分而治之” ,把大问题划分成很多个小问题,逐步地把小问题解决掉,大问题也就随之解决了,这些小问题可以单独的作为一个小任务来处理。这些小任务是并发处理的,注意,并不是说同一时刻一起执行很多个任务,而是由于每个任务执行的时间很短,导致看起来像是同一时刻执行了很多个任务一样。(这就好比我们数学上的定积分是一样的,定积分处理的思想就是把无限长的曲线截成无限段,那么每一小段都可以近似的看做是直线,这样分别的处理每一小段进行叠加即可,多任务也是这样,大任务分成无限多的小任务,每一个小任务占用系统的时间很小,因此导致看起来像是同一时刻执行了很多个任务,但是实际上并不是这样,这里需要注意)
多任务系统带来一个新的问题,究竟哪个任务先运行,哪个任务后运行呢?完成这个功能的东西在 RTOS系统中叫做任务调度器。不同的系统其任务调度器的实现方法也不同,比如 FreeRTOS 是一个抢占式的实时多任务系统,那么其任务调度器也是抢占式的。
在抢占式多任务系统中,高优先级的任务可以打断低优先级的任务的运行而取得 CPU 的使用权,这样就保证了那些紧急任务的运行。这样我们就可以为那些对实时性要求高的任务设置一个很高的优先级,比如自动驾驶中的障碍物检测任务等。高优先级的任务执行完成以后重新把 CPU 的使用权归还给低优先级的任务,这个就是抢占式多任务系统的基本原理。
在 FreeRTOS 中应用既可以使用任务,也可以使用协程(Co-Routine),或者两者混合使用。但是任务和协程使用不同的 API 函数,因此不能通过队列(或信号量)将数据从任务发送给协程,反之亦然。
什么叫 API 函数?
API(Application Programming Interface,应用程序编程接口)是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。 ——百度百科
在文件系统中,以C语言为例,我们使用 fopen() 函数可以打开一个文件,在写程序时,我们感觉非常简单。文件保存在硬盘上,要经过复杂的处理才能显示,这些细节对于我们来说是透明的,使用操作系统来完成。也就是说,我们调用 fopen() 函数来通知操作系统,让操作系统来打开一个文件。
那么如何告诉操作系统打开文件呢?
其实看似简单的调用函数 fopen,实际上在底层是非常复杂的,打开文件首先要扫描硬盘,找到文件的位置,然后从文件中读取一部分数据,将数据放进 I/O 缓冲区,放进内存;这些数据都是 0 1 序列,操作系统还要对照ASCII表或Unicode表 “翻译” 成字符,然后再在显示器上显示出来。试想一下,如果这个过程让程序员来完成,简直是噩梦!!!
基于此,操作系统想了一个办法,我把上述这一系列的打开文件的操作写在一个函数里,编译成一个组件(一般是动态链接库)随操作系统一起发布,并且附上说明文档,程序员只需要简单的调用这个函数就可以完成复杂的工作;这些封装好的函数,就叫做API(Application Programming Interface),即应用程序编译接口!
说的更通俗易懂一点,就是别人写好的代码,或者编译好的程序,提供给我们使用,就叫做API。你使用了别人的代码(或者程序)中的某个函数、类、对象,就叫做使用了某个API。
例如:
C语言 API 以函数的形式呈现,例如 printf()、scanf()、fopen() 等。
Java API 主要以类的形式呈现,例如 String、Thread、Date 等。
在使用 RTOS 的时候一个实时应用可以作为一个独立的任务。每个任务都有自己的运行环境,不依赖于系统中其他的任务或者 RTOS 调度器。任何一个时间点只能有一个任务运行,具体哪一个任务运行是由 RTOS 调度器来决定的,RTOS 调度器因此会重复的开启、关闭每个任务。(这一点在上面已经解释过了)
任务不需要了解 RTOS 调度器的具体行为,RTOS 调度器的职责是确保当一个任务开始执行的时候其上下文环境(寄存器值,堆栈内容等)和任务上一次退出的时候相同。为了做到这一点,每个任务都必须有一个堆栈,当任务切换的时候将上下文环境保存在堆栈中,这样当任务再次执行的时候就可以从堆栈中取出上下文环境,任务恢复运行。
任务特性:
1. 简单
2. 没有使用限制
3. 支持抢占
4. 支持优先级
5. 每个任务都拥有堆栈导致了 RAM 使用量增大
6. 如果使用抢占的话必须仔细的考虑重入的问题
协程是为那些资源很少的 MCU 而做的,但是随着 MCU 的飞速发展,性能越来越强大,现在协程几乎很少用到了!(FreeRTOS 目前还没有打算把协程移出,但是也不会再更新和维护协程了)
在概念上协程和任务是相似的,但是有如下根本上的不同:
1. 堆栈使用
所有的协程使用同一个堆栈(但是如果是任务的话每个任务都有自己的堆栈),这样使用协程就比使用任务消耗更少的 RAM。
2. 调度器和优先级
协程使用合作式的调度器,但是可以在使用抢占式的调度器中使用协程。
3. 宏定义
协程是通过宏定义来实现的。
4. 使用限制
为了降低对 RAM 的消耗做了很多的限制。
FreeRTOS 中的任务永远处于下面几个状态中的某一个:
运行态:
当一个任务正在运行时,那么就说这个任务处于运行态,处于运行态的任务就是当前正在使用处理器的任务。如果使用的是单核处理器的话那么不管在任何时刻永远都只有一个任务处于运行态。
就绪态:
处于就绪态的任务是那些已经准备就绪(这些任务没有被阻塞或者挂起),可以运行的任务,但是处于就绪态的任务还没有运行,因为有一个同优先级或者更高优先级的任务正在运行!
阻塞态:
如果一个任务当前正在等待某个外部事件的话就说它处于阻塞态,比如说如果某个任务调用了函数 vTaskDelay() 的话就会进入阻塞态,直到延时周期完成。任务在等待队列、信号量、事件组、通知或互斥信号量的时候也会进入阻塞态。任务进入阻塞态会有一个超时时间,当超过这个超时时间就会退出阻塞态,即使所等待的事件还没有来临!
挂起态:
像阻塞态一样,任务进入挂起态以后也不能被调度器调用进入运行态,但是进入挂起态的任务没有超时时间(也就是说任务不会自动退出挂起态)。任务进入和退出挂起态通过调用函数 vTaskSuspend() 和 xTaskPesume() 。
每个任务都可以分配一个从 0~(configMAX_PRIORITIES - 1) 的优先级,configMAX_PRIORITIES 在文件 FreeRTOSConfig.h 中有定义。如果所使用的硬件平台支持类似计算前导零这样的指令(可以通过该指令选择下一个要运行的任务,Cortex - M 处理器支持该指令),并且宏 configUSE_PORT_OPTIMISED_TASK_SELECTION 也就同时被设置为了 1 ,那么宏 configMAX_PRIORITIES 不能超过 32 !也就是优先级不能超过 32 级。其他情况下宏 configMAX_PRIORITIES 可以设置为任何值,但是考虑到 RAM 的消耗,宏 configMAX_PRIORITIES 最好设置为一个满足应用的最小值。
优先级数字越低表示任务的优先级越低, 0 的优先级最低,configMAX_PRIORITIES - 1 的优先级最高。空闲任务的优先级最低,为 0 。
FreeRTOS 调度器确保处于就绪态或运行态的高优先级的任务获取处理器使用权,换句话说就是处于就绪态的最高优先级的任务才会运行。当宏 configUSE_TIME_SLICING 定义为 1 的时候多个任务可以共用一个优先级,数量不限。默认情况下宏 configUSE_TIME_SLICING 在文件 FreeRTOS.h 中已经定义为 1 。此时处于就绪态的优先级相同的任务就会使用时间片轮转调度器获取运行时间。
在使用 FreeRTOS 的过程中,我们要使用函数 xTaskCreate() 或 xTaskCreateStatic() 来创建任务,这两个函数的第一个参数 pxTaskCode,就是这个任务的任务函数。
什么是任务函数?任务函数就是完成本任务工作的函数。就是说我这个任务要干嘛?要做什么?要完成什么样的功能都是在这个任务函数中实现的。(比如说我要做个任务,这个任务要点个流水灯,那么这个流水灯的程序就是任务函数中实现的)
FreeRTOS 官方给出的任务函数模板如下:
void vATaskFunction(void *pvParameters) (1) { for(;;) (2) { --任务应用程序-- (3) vTaskDelay(); (4) } //不能从任务函数中返回或者退出,从任务函数中返回或者退出的话就会调用 configASSERT(),前提是我们定义了 configASSERT()。 //如果一定要从任务函数中退出的话那一定要调用函数 vTaskDelete(NULL) 来删除此任务 vTaskDelete(NULL); (5) }
(1)、任务函数本质上也是函数,所以肯定也是有任务名什么的,不过这里我们需要注意:任务函数的返回类型一定要为 void 类型,也就是无返回值,而且任务的参数也是 void 指针类型的!任务函数名可以根据实际情况定义。
(2)、任务的具体执行过程是一个大循环,for( ; ; ) 就表示一个循环,作用和 while(1) 一样,具体使用时根据个人习惯即可。
(3)、循环里面就是真正的任务代码了,此任务具体要干的活就在这里实现!
(4)、FreeRTOS 的延时函数,此处不一定要用延时函数,其他只要能让 FreeRTOS 发生任务切换的 API 函数都可以,比如请求信号量、队列等,甚至直接调用任务调度器。只不过最常用的还是 FreeRTOS 的延时函数。
(5)、任务函数一般不允许跳出循环,如果一定要跳出循环的话在跳出循环以后一定要调用函数 vTaskDelete(NULL) 删除此任务!
FreeRTOS 的任务函数和 UCOS 的任务函数模式基本上相同,不止 FreeRTOS,其他 RTOS 的任务函数基本上也是这种方式。
FreeRTOS 的每个任务都有一些属性需要存储,FreeRTOS 把这些属性集合到一起用一个结构体来表示,这个结构体叫做任务控制块:TCB_t,在使用函数 xTaskCreate() 创建任务的时候就会自动的给每一个任务分配一个任务控制块。在老版本的 FreeRTOS 中任务控制块叫做 tskTCB,新版本重命名为 TCB_t,但是本质上还是 tskTCB,对于 FreeRTOS 的学习中,凡是提到任务控制块的话均用 TCB_t 表示,此结构体在文件 task.c 中有定义;
/*
* Task control block. A task control block (TCB) is allocated for each task,
* and stores task state information, including a pointer to the task's context
* (the task's run time environment, including register values)
*/
/*
* 任务控制块。为每个任务分配一个任务控制块(TCB),并存储任务状态信息,包括指向任务上下文的指针(任务的运行时环境,包括寄存器值)。
*/
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /*< 任务堆栈栈顶 */
//指向任务堆栈中最后一项的位置,这必须是 TCB 结构体的第一个成员
//"Volatile"是C语言的一个关键字,它可以用于修饰某个变量,告诉编译器该变量的值可能随时会发生改变,因此在读取该变量的值时,应该直接从内存中获取,而不要使用缓存。
//具体来说,当一个变量被声明为volatile时,意味着该变量可能被多个线程或进程同时访问,并且其值可能随时会被改变,因此编译器不能优化对该变量的访问方式,
//而是始终直接从内存中读取该变量的值。这种变量的用途通常是用来存储硬件状态或者与硬件相关的变量,以保证程序正确地与硬件交互。
#if ( portUSING_MPU_WRAPPERS == 1 )
//宏定义#define portUSING_MPU_WRAPPERS 0
xMPU_SETTINGS xMPUSettings; /*< MPU相关设置 */
#endif //MPU设置被定义为端口层的一部分。这必须是TCB结构体的第二个成员。
ListItem_t xStateListItem; /*< 任务的状态列表项引用的列表表示该任务的状态(就绪、阻塞、挂起) */ //状态列表项
ListItem_t xEventListItem; /*< 用于从事件列表中引用任务。 */ //事件列表项
UBaseType_t uxPriority; /*< 任务的优先级,0为最低的优先级 */ //任务优先级
StackType_t *pxStack; /*< 指向堆栈的起点 */ //任务堆栈起始位置
char pcTaskName[ configMAX_TASK_NAME_LEN ];/*< 创建时给任务的描述性名称。只方便调试。 */ //任务名字
#if ( portSTACK_GROWTH > 0 ) //#define portSTACK_GROWTH ( -1 )
StackType_t *pxEndOfStack;Points to the start of the stack. /*< 在堆栈从低内存增长的体系结构中,指向堆栈的末端。 */ //任务堆栈栈底
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 ) //#define portCRITICAL_NESTING_IN_TCB 0
UBaseType_t uxCriticalNesting; /*< 保存在端口层中不保持自己计数的端口的关键区域嵌套深度。 */ //临界区嵌套深度
#endif
#if ( configUSE_TRACE_FACILITY == 1 ) //#define configUSE_TRACE_FACILITY 1 //trace 或到 debug 的时候用到
UBaseType_t uxTCBNumber; /*< 存储一个数字,每次创建TCB时该数字递增。它允许调试器确定任务何时被删除,然后重新创建。 */
UBaseType_t uxTaskNumber; /*< 存储专门供第三方跟踪代码使用的数字。 */
#endif
#if ( configUSE_MUTEXES == 1 ) //#define configUSE_MUTEXES 1
UBaseType_t uxBasePriority; /*< 最后分配给任务的优先级——由优先级继承机制使用。 */ //任务基础优先级,优先级反转的时候用到
UBaseType_t uxMutexesHeld; //任务获取到的互斥信号量个数
#endif
#if ( configUSE_APPLICATION_TASK_TAG == 1 ) //#define configUSE_APPLICATION_TASK_TAG 0
TaskHookFunction_t pxTaskTag;
#endif
#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 ) //#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 0 //与本地存储有关
void *pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif
#if( configGENERATE_RUN_TIME_STATS == 1 ) //#define configGENERATE_RUN_TIME_STATS 0
uint32_t ulRunTimeCounter; /*< 存储任务在“运行”状态下花费的时间。 */ //用来记录任务运行总时间
#endif
#if ( configUSE_NEWLIB_REENTRANT == 1 ) //#define configUSE_NEWLIB_REENTRANT 0
/* Allocate a Newlib reent structure that is specific to this task.
Note Newlib support has been included by popular demand, but is not
used by the FreeRTOS maintainers themselves. FreeRTOS is not
responsible for resulting newlib operation. User must be familiar with
newlib and must provide system-wide implementations of the necessary
stubs. Be warned that (at the time of writing) the current newlib design
implements a system-wide malloc() that must be provided with locks. */
//分配一个特定于此任务的Newlib最近结构。注意:Newlib支持是根据大众需求包含的,但是FreeRTOS维护者自己并不使用。FreeRTOS不对由此产生的newlib操作负责。
//用户必须熟悉newlib,并且必须提供必要存根的系统范围实现。请注意(在撰写本文时)当前的newlib设计
//实现必须与锁一起提供的系统范围malloc()。
struct _reent xNewLib_reent; //定义一个 newlib 结构体变量
#endif
#if( configUSE_TASK_NOTIFICATIONS == 1 ) //#define configUSE_TASK_NOTIFICATIONS 1 //任务通知相关变量
volatile uint32_t ulNotifiedValue; //任务通知值
volatile uint8_t ucNotifyState; //任务通知状态
#endif
/* 请参阅上面关于tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE定义的注释。 */
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
//用来标记任务是动态创建的还是静态创建的,如果是静态创建的此变量就为 pdTURE,如果是动态创建的就为 pdFALSE
//#define tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE ( ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) ) || ( portUSING_MPU_WRAPPERS == 1 ) )
uint8_t ucStaticallyAllocated; /*< 如果任务是静态分配的,则设置为pdTRUE,以确保不尝试释放内存。 */
#endif
#if( INCLUDE_xTaskAbortDelay == 1 ) //#define INCLUDE_xTaskAbortDelay 0
uint8_t ucDelayAborted;
#endif
} tskTCB;
FreeRTOS 之所以能正确的恢复一个任务的运行就是因为有任务堆栈在保驾护航,任务调度器在进行任务切换的时候会将当前任务的现场(CPU寄存器值等)保存在此任务的任务堆栈中,等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场,恢复现场以后任务就会接着从上次中断的地方开始运行。
创建任务的时候需要给任务指定堆栈,如果使用的是函数 xTaskCreate() 创建任务(动态方法)的话那么任务堆栈就会由函数 xTaskCreate() 自动创建;
如果使用函数 xTaskCreateStatic() 创建任务(静态方法)的话就需要程序员自行定义任务堆栈,然后堆栈首地址作为函数的参数 puxStackBuffer 传递给函数
TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode, //任务函数
const char *const pcName, //任务名称
const uint32_t ulStackDepth, //任务堆栈大小
void *const pvParameters, //传递给任务函数的参数
UBaseType_t uxPriority, //任务优先级
StackType_t* const puxStackBuffer, (1)
StaticTask_t* const pxTaskBuffer
)
(1)、任务堆栈,需要用户定义,然后将堆栈首地址传递给这个参数
堆栈大小:
我们不管是使用函数 xTaskCreate() 动态创建还是 xTaskCreateStatic() 静态创建任务都需要指定任务堆栈大小。任务堆栈的数据类型为 StackType_t,StackType_t 本质上是 uint32_t,在 portmacro.h 中有定义,如下:
#define portSTACK_TYPE uint32_t
#define portBASE_TYPE long
typedef portSTACK_TYPE StackType_t;
typedef long BaseType_t;
typedef unsigned long UBaseType_t;
可以看出 StackType_t 类型的变量为 4 个字节,那么任务的实际堆栈大小就应该是我们所定义的 4 倍;