FreeRTOS 任务管理
FreeRTOS 的任务管理非常重要,了解任务管理的目的就是让初学者从裸机的,单任务编程过渡到带 OS 的,多任务编程上来。搞清楚了这一点, 那么 FreeRTOS 学习就算入门了。
单任务系统
学习多任务系统之前,我们先来回顾下单任务系统的编程框架,即裸机时的编程框架。
裸机编程主要是采用超级循环(super-loops)系统,又称前后台系统。应用程序是一个无限的循环,循环中调用相应的函数完成相应的操作,循环这部分可以看做后台行为;
中断服务程序处理异步事件,中断服务这部分可以看做是前台行为。后台也可以叫做任务级,前台也叫作中断级
对于前后台系统的编程思路主要有以下两种方式:
1 查询方式
对于一些简单的应用,处理器可以查询数据或者消息是否就绪,就绪后进行处理,然后再等待,如此循环下去。对于简单的任务,这种方式简单易处理。但大多数情况下,需要处理多个接口数据或者消息,那就需要多次处理,如下面的流程图所示:

用查询方式处理简单的应用,效果比较好,但是随着工程的复杂,采用查询方式实现的工程就变得很难维护,同时,由于无法定义查询任务的优先级,这种查询方式会使得重要的接口消息得不到及时响应。
比如程序一直在等待一个非紧急消息就绪,如果这个消息后面还有一个紧急的消息需要处理,那么就会使得紧急消息长时间得不到执行。
2 中断方式
对于查询方式无法有效执行紧急任务的情况,采用中断方式就有效地解决了这个问题,下面是中断方式简单的流程图:

采用中断和查询结合的方式可以解决大部分裸机应用,但随着工程的复杂,裸机方式的缺点就暴露出来了:
◆ 必须在中断(ISR)内处理时间关键运算:
ISR 函数变得非常复杂,并且需要很长执行时间。
ISR 嵌套可能产生不可预测的执行时间和堆栈需求。
◆ 超级循环和 ISR 之间的数据交换是通过全局共享变量进行的:
应用程序的程序员必须确保数据一致性。
◆ 超级循环可以与系统计时器轻松同步,但:
如果系统需要多种不同的周期时间,则会很难实现。
超过超级循环周期的耗时函数需要做拆分。
增加软件开销,应用程序难以理解。
◆ 超级循环使得应用程序变得非常复杂,因此难以扩展:
一个简单的更改就可能产生不可预测的副作用,对这种副作用进行分析非常耗时。
超级循环概念的这些缺点可以通过使用实时操作系统 (RTOS) 来解决。
多任务系统
针对这些情况,使用多任务系统就可以解决这些问题了。下面是一个多任务系统的流程图:

多任务系统或者说 RTOS 的实现,重点就在这个调度器上,而调度器的作用就是使用相关的调度算法来决定当前需要执行的任务。
如上图所示的那样,创建了任务并完成 OS 初始化后,就可以通过调度器来决定任务 A,任务 B 和任务 C 的运行,从而实现多任务系统。另外需要初学者注意的是,这里所说的多任务系统同一时刻只能有一个任务可以运行,只是通过调度器的决策,看起来像所有任务同时运行一样。为 了更好的说明这个问题,再举一个详细的运行例子,运行条件如下:
◆ 使用抢占式调度器。
◆ 1 个空闲任务,优先级最低。
◆ 2 个应用任务,一个高优先级和一个低优先级,优先级都比空闲任务优先级高。
◆ 中断服务程序,含 USB 中断,串口中断和系统滴答定时器中断。
下图所示是任务的运行过程,其中横坐标是任务优先级由低到高排列,纵坐标是运行时间,时间刻度有小到大。

(1) 启动 RTOS,首先执行高优先级任务(vTaskStartScheduler)。
(2) 高优先级任务等待事件标志(xEventGroupWaitBits)被阻塞,低优先级任务得到执行。
(3) 低优先级任务执行的过程中产生 USB 中断,进入 USB 中断服务程序。
(4) 退出 USB 中断复位程序,回到低优先级任务继续执行。
(5) 低优先级任务执行过程中产生串口接收中断,进入串口接收中断服务程序。
(6) 退出串口接收中断复位程序,并发送事件标志设置消息(xEventGroupSetBitsFromISR), 被阻塞的高优先级任务就会重新进入就绪状态,这个时候高优先级任务和低优先级任务都在就绪态,抢占式调度器就会让高优先级的任务先执行,所以此时就会进入高优先级任务。
(7) 高优先级任务由于等待事件标志(xEventGroupWaitBits)会再次被阻塞,低优先级任务开始继续执行。
(8) 低优先级任务调用函数 vTaskDelay,低优先级任务被挂起,从而空闲任务得到执行。 (9) 空闲任务执行期间发生滴答定时器中断,进入滴答定时器中断服务程序。
(10) 退出滴答定时器中断,由于低优先级任务延时时间到,低优先级任务继续执行。
(11) 低优先级任务再次调用延迟函数vTaskDelay,低优先级任务被挂起,从而切换到空闲任务。 空闲任务得到执行。
FreeRTOS 就是一款支持多任务运行的实时操作系统,具有时间片,抢占式和合作式三种调度方法。 通过 FreeRTOS 实时操作系统可以将程序函数分成独立的任务,并为其提供合理的调度方式。
FreeRTOS 的任务栈设置
不管是裸机编程还是 RTOS 编程,栈的分配大小都非常重要。
局部变量,函数调用时的现场保护和返回地址,函数的形参,进入中断函数前和中断嵌套等都需要栈空间,栈空间定义小了会造成系统崩溃。
裸机的情况下,用户可以在这里配置栈大小:

不同于裸机编程,在 RTOS 下,每个任务都有自己的栈空间。
对于 FreeRTOS 来说,任务栈空间是在任务创建的时候从 FreeRTOSConfig.h 文件中定义的 heap 空间中申请的
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) )
具体每个任务的栈大小是在创建 FreeRTOS 的任务时进行设置的:
FreeRTOS 的系统栈设置
裸机的情况下,凡是用到栈空间的地方 都是在这里配置的栈空间:

在 RTOS 下,上图中设置的栈大小有了一个新的名字叫系统栈空间,而任务栈是不使用这里的空间的。任务栈不使用这里的栈空间,哪里使用这里的栈空间呢?
答案就在中断函数和中断嵌套。
◆ 由于 Cortex-M3 和 M4 内核具有双堆栈指针,MSP 主堆栈指针和 PSP 进程堆栈指针,或者叫 PSP 任务堆栈指针也是可以的。在 FreeRTOS 操作系统中,主堆栈指针 MSP 是给系统栈空间使用的,进程堆栈指针 PSP 是给任务栈使用的。
也就是说,在FreeRTOS 任务中,所有栈空间的使用都是通过 PSP 指针进行指向的。一旦进入了中断函数以及可能发生的中断嵌套都是用的 MSP 指针。这个知识点要记住它,当前可以不知道这是为什么,但是一定要记住。
◆ 实际应用中系统栈空间分配多大,主要是看可能发生的中断嵌套层数,下面我们就按照最坏执行情况 进行考虑,所有的寄存器都需要入栈,此时分为两种情况
情况1: 64 字节
对于 Cortex-M3 内核和未使用 FPU(浮点运算单元)功能的 Cortex-M4 内核在发生中断时需要将 16 个通用寄存器全部入栈,每个寄存器占用 4 个字节,也就是 16*4 = 64 字节的空间。 可能发生几次中断嵌套就是要 64 乘以几即可。当然,这种是最坏执行情况,也就是所有的寄存器都入栈。
(注:任务执行的过程中发生中断的话,有 8 个寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余寄存器入栈以及发生中断嵌套都是用的系统栈)
情况2: 200 字节
对于具有 FPU(浮点运算单元)功能的 Cortex-M4 内核,如果在任务中进行了浮点运算,那么在发生中断的时候除了 16 个通用寄存器需要入栈,还有 34 个浮点寄存器也是要入栈的,也就是 (16+34)*4 = 200 字节的空间。当然,这种是最坏执行情况,也就是所有的寄存器都入栈。
(注:任务执行的过程中发送中断的话,有 8 个通用寄存器和 18 个浮点寄存器是自动入栈的, 这个栈是任务栈,进入中断以后其余通用寄存器和浮点寄存器入栈以及发生中断嵌套都是用的系 统栈)
FreeRTOS 的任务状态
FreeRTOS 的运行支持以下四种状态:
◆ Running—运行态
当任务处于实际运行状态被称之为运行态,即 CPU 的使用权被这个任务占用。
◆ Ready—就绪态
处于就绪态的任务是指那些能够运行(没有被阻塞和挂起),但是当前没有运行的任务,因为同优先级或更高优先级的任务正在运行。
◆ Blocked—阻塞态
由于等待信号量,消息队列,事件标志组等而处于的状态被称之为阻塞态,另外任务调用延迟函数也会处于阻塞态。
◆ Suspended—挂起态
类似阻塞态,通过调用函数 vTaskSuspend()对指定任务进行挂起,挂起后这个任务将不被执行,只有调用函数 xTaskResume()才可以将这个任务从挂起态恢复。

FreeRTOS 启动
使用如下函数即可启动 FreeRTOS:
◆ vTaskStartScheduler(); 关于这个函数的讲解及其使用方法可以看 FreeRTOS 在线版手册
函数 vTaskStartScheduler 用于启动 FreeRTOS 调度器,即启动 FreeRTOS 的多任务执行。 使用这个函数要注意以下几个问题:
1. 空闲任务和可选的定时器任务是在调用这个函数后自动创建的。
2. 正常情况下这个函数是不会返回的,运行到这里极有可能是用于定时器任务或者空闲任务的 heap 空间不足造成创建失败,此时需要加大 FreeRTOSConfig.h 文件中定义的 heap 大小: #define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) )
FreeRTOS 的任务创建
使用如下函数可以实现 FreeRTOS 的任务创建:
◆ xTaskCreate() 关于这个函数的讲解及其使用方法可以看 FreeRTOS 在线版手册:
函数 xTaskCreate 用于实现 FreeRTOS 操作系统的任务创建,并且还可以自定义任务栈的大小。
◆ 第 1 个参数填创建任务的函数名。
◆ 第 2 个参数是任务名,这个参数主要是用于调试目的,调试的时候方便看是哪个任务。
◆ 第 3 个参数是任务栈大小,单位 word,也就是 4 字节。
◆ 第 4 个参数是创建的任务函数的形参。
◆ 第 5 个参数是任务句柄,用于区分不同的任务。
FreeRTOS 的任务删除
使用如下函数可以实现 FreeRTOS 的任务删除:
◆ vTaskDelete() 关于这个函数的讲解及其使用方法可以看 FreeRTOS 在线版手册:
◆ 第 1 个参数填要删除任务的句柄 使用这个函数要注意以下问题:
1 使用此函数需要在 FreeRTOSConfig.h 配置文件中配置如下宏定义为
#define INCLUDE_vTaskDelete 1
2 如果用往此函数里面填的任务 ID 是 NULL,即数值 0 的话,那么删除的就是当前正在执行的任务,此任务被删除后,FreeRTOS 会切换到任务就绪列表里面下一个要执行的最高优先级任务。
3 在 FreeRTOS 中,创建任务所需的内存需要在空闲任务中释放,如果用户在 FreeRTOS 中调用了这个函数的话,一定要让空闲任务有执行的机会,否则这块内存是无法释放的。
另外,创建的这个任务在使用中申请了动态内存,这个内存不会因为此任务被删除而删除,这一点要注意,一定要在删除前将此内存释放。
FreeRTOS 的任务挂起
使用如下函数可以实现 FreeRTOS 的任务挂起:
◆ xTaskSuspend()
函数 vTaskSuspend 用于实现 FreeRTOS 操作系统的任务挂起。
◆ 第 1 个参数填要挂起任务的句柄 使用这个函数要注意以下问题:
1. 使用此函数需要在 FreeRTOSConfig.h 配置文件中配置如下宏定义为 1
#define INCLUDE_vTaskSuspend 1
2. 如果用往此函数里面填的任务 ID 是 NULL,即数值 0 的话,那么挂起的就是当前正在执行的任务, 此任务被挂起后,FreeRTOS 会切换到任务就绪列表里面下一个要执行的高优先级任务。
3. 多次调用此函数的话,只需调用一次 vTaskResume 即可将任务从挂起态恢复。
FreeRTOS 的任务恢复
使用如下函数可以实现 FreeRTOS 的任务恢复:
◆ xTaskResume()
◆ 第 1 个参数填要恢复任务的句柄 使用这个函数要注意以下问题:
1. 使用此函数需要在 FreeRTOSConfig.h 配置文件中配置如下宏定义为 1
#define INCLUDE_vTaskSuspend 1
2. 多次调用函数 vTaskSuspend 的话,只需调用一次 vTaskResume 即可将任务从挂起态恢复。
3. 此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,中断服务程序中使用的 xTaskResumeFromISR(),以后缀 FromISR 结尾。
FreeRTOS 的任务恢复(中断方式)
◆ xTaskResumeFromISR()
函数 vTaskResumeFromISR 用于实现 FreeRTOS 操作系统的任务恢复。
◆ 第 1 个参数填要恢复任务的句柄 使用这个函数要注意以下问题:
1.使用此函数需要在 FreeRTOSConfig.h 配置文件中配置如下宏定义为 1
#define INCLUDE_xResumeFromISR 1
2. 多次调用函数 vTaskSuspend 的话,只需调用一次 vTaskResumeFromISR 即可将任务从挂起态恢复。
3. 如果用户打算采用这个函数实现中断与任务的同步,要注意一种情况,如果此函数的调用优先于函数 vTaskSuspend 被调用,那么此次同步会丢失,这种情况下建议使用信号量来实现同步。
4. 此函数是用于中断服务程序中调用的,故不可以在任务中使用此函数,任务中使用的是 vTaskResume。
FreeRTOS 的空闲任务
几乎所有的小型 RTOS 中都会有一个空闲任务,空闲任务属于系统任务,是必须要执行的,用户程 序不能将其关闭。不光小型系统中有空闲任务,大型的系统里面也有的,比如 WIN7。
空闲任务主要有以下几个作用:
◆ 用户不能让系统一直在执行各个应用任务,这样的话系统利用率就是 100%,系统就会一直超负荷运行,所以空闲任务很有必要。
◆ 为了更好的实现低功耗,空闲任务也很有必要,用户可以在空闲任务中实现睡眠,停机等低功耗措施。