文章目录
一、 单任务系统(裸机)
主要是采用超级循环系统(前后台系统),应用程序是一个无限的循环,循环中调用相应的函数完成相应的操作,这部分可以看做后台行为;中断服务程序处理异步事件,这部分可以看做是前台行为。后台也可以叫做任务级,前台也叫作中断级。
前后台系统的编程思路有两种:轮询方式(实时性得不到保障,紧急与非紧急消息不能有效管理)、中断方式(可以保证一定的实时性,紧急消息可以得到响应)。
采用中断和查询结合的方式可以解决大部分裸机应用,但随着工程的复杂,裸机方式的缺点就暴露出来了:
- 必须在中断(ISR)内处理时间关键运算:
- ISR 函数变得非常复杂,并且需要很长执行时间。
- ISR 嵌套可能产生不可预测的执行时间和堆栈需求。
- 超级循环和 ISR 之间的数据交换是通过全局共享变量进行的:
- 应用程序的程序员必须确保数据一致性。
- 超级循环可以与系统计时器轻松同步,但:
- 如果系统需要多种不同的周期时间,则会很难实现。
- 超过超级循环周期的耗时函数需要做拆分。
- 增加软件开销,应用程序难以理解。
- 超级循环使得应用程序变得非常复杂,因此难以扩展:
- 一个简单的更改就可能产生不可预测的副作用,对这种副作用进行分析非常耗时。
- 超级循环概念的这些缺点可以通过使用实时操作系统 (RTOS) 来解决。
二、多任务系统(带OS)
采用多任务系统可以以上的裸机开发遇到的4大缺点。
RTOS的实现重点就在这个OS任务调度器上,调度器的作用就是使用相关的调度算法来决定当前需要执行的任务。FreeRTOS就是一款支持多任务运行的实时操作系统,具有时间片、抢占式和合作式三种调度方式。通过 FreeRTOS 实时操作系统可以将程序函数分成独立的任务,并为其提供合理的调度方式。
1. 任务堆栈
栈大小 0x400 = 1024,单位字节
在RTOS下,上面截图里设置的栈大小有了一个新名字叫做系统栈空间,而任务栈是不使用这里的空间,哪里使用这里的栈空间呢,实际上是中断函数和中断嵌套。
- 由于 Cortex-M3 和 M4 内核具有双堆栈指针,MSP 主堆栈指针和 PSP 进程堆栈指针,或者叫 PSP
任务堆栈指针也是可以的。在 FreeRTOS 操作系统中,主堆栈指针 MSP 是给系统栈空间使用的,进
程堆栈指针 PSP 是给任务栈使用的。也就是说,在 FreeRTOS 任务中,所有栈空间的使用都是通过
PSP 指针进行指向的。一旦进入了中断函数以及可能发生的中断嵌套都是用的 MSP 指针。这个知识
点要记住它,当前可以不知道这是为什么,但是一定要记住。- 实际应用中系统栈空间分配多大,主要是看可能发生的中断嵌套层数,下面我们就按照最坏执行情况
进行考虑,所有的寄存器都需要入栈,此时分为两种情况:
* 64 字节:
对于 Cortex-M3 内核和未使用 FPU(浮点运算单元)功能的 Cortex-M4 内核在发生中断时需要将 16 个通用寄存器全部入栈,每个寄存器占用 4 个字节,也就是 16*4 = 64 字节的空间。可能发生几次中断嵌套就是要 64 乘以几即可。当然,这种是最坏执行情况,也就是所有的寄存器都入栈。(注:任务执行的过程中发生中断的话,有 8 个寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余寄存器入栈以及发生中断嵌套都是用的系统栈)
* 200 字节
对于具有 FPU(浮点运算单元)功能的 Cortex-M4 内核,如果在任务中进行了浮点运算,那么在发生中断的时候除了 16 个通用寄存器需要入栈,还有 34 个浮点寄存器也是要入栈的,也就是(16+34)*4 = 200 字节的空间。当然,这种是最坏执行情况,也就是所有的寄存器都入栈。
1.1 任务栈大小确定
- 函数的栈大小计算起来是比较麻烦的,那么有没有简单的办法来计算呢?有的,一般 IDE 开发环境都有这样的功能,比如 MDK 会生成一个 htm 文件,通过这个文件用户可以知道每个被调用函数的最大栈需求以及各个函数之间的调用关系。但是 MDK 无法确定通过函数指针实现函数调用时的栈需求。另外,发生中断或中断嵌套时的现场保护需要的栈空间也不会统计。
- 一般来说,用户可以事先给任务分配一个大的栈空间,然后通过打印任务栈的使用情况,运行一段时间就会有个大概的范围了。这种方法比较简单且实用些。
1.2 栈溢出检测机制
栈生长方向从高地址向低地址生长(M4 和 M3 是这种方式)
- 上图标识 3 的位置是局部变量 int i 和 int array[10]占用的栈空间,但申请了栈空间后已经越界了。这个就是所谓的栈溢出了。如果用户在函数 test 中通过数组 array 修改了这部分越界区的数据且这部分越界的栈空间暂时没有用到或者数据不是很重要,情况还不算严重,但是如果存储的是关键数据,会直接导致系统崩溃。
- 上图标识 4 的位置是局部变量申请了栈空间后,栈指针向下偏移(返回地址+变量 i+10 个数组元素)*4 =48 个字节。
- 上图标识 5 的位置可能是其它任务的栈空间,也可能是全局变量或者其它用途的存储区,如果 test函数在使用中还有用到栈的地方就会从这里申请,这部分越界的空间暂时没有用到或者数据不是很重要,情况还不算严重,但是如果存储的是关键数据,会直接导致系统崩溃。
FreeRTOS 提供了两种栈溢出检测机制,这两种检测都是在任务切换时才会进行:
- 在任务切换时检测任务栈指针是否过界了,如果过界了,在任务切换的时候会触发栈溢出钩子函数。
void vApplicationStackOverflowHook( TaskHandle_t xTask,signed char *pcTaskName );
用户可以在钩子函数里面做一些处理。这种方法不能保证所有的栈溢出都能检测到。比如任务在执行的过程中出现过栈溢出。任务切换前栈指针又恢复到了正常水平,这种情况在任务切换的时候是检测不到的。又比如任务栈溢出后,把这部分栈区的数据修改了,这部分栈区的数据不重要或者暂时没有用到还好,但如果是重要数据被修改将直接导致系统进入硬件异常,这种情况下,栈溢出检测功能也是检测不到的。
使用方法一需要用户在 FreeRTOSConfig.h 文件中配置如下宏定义:
#define configCHECK_FOR_STACK_OVERFLOW 1
- 任务创建的时候将任务栈所有数据初始化为 0xa5,任务切换时进行任务栈检测的时候会检测末尾的 16 个字节是否都是 0xa5,通过这种方式来检测任务栈是否溢出了。相比方法一,这种方法的速度稍慢些,但是这样就有效地避免了方法一里面的部分情况。不过依然不能保证所有的栈溢出都能检测到,比如任务栈末尾的 16 个字节没有用到,即没有被修改,但是任务栈已经溢出了,这种情况是检测不到的。另外任务栈溢出后,任务栈末尾的 16 个字节没有修改,但是溢出部分的栈区数据被修改了,这部分栈区的数据不重要或者暂时没有用到还好,但如果是重要数据被修改将直接导致系统进入硬件异常,这种情况下,栈溢出检测功能也是检测不到的。
使用方法二需要用户在 FreeRTOSConfig.h 文件中配置如下宏定义:
#define configCHECK_FOR_STACK_OVERFLOW 2
除了 FreeRTOS 提供的这两种栈溢出检测机制,还有其它的栈溢出检测机制,大家可以在 Mircrium 官方发布的如下这个博文中学习:
https://www.micrium.com/detecting-stack-overflows-part-2-of-2/
钩子函数:
钩子函数的主要作用就是对原有函数的功能进行扩展,用户可以根据自己的需要往里面添加相关的测试代码,大家可以在 FreeRTOS 工程中检索这个钩子函数 vApplicationStackOverflowHook 所在的位置。
2. 任务状态
FreeRTOS的任务状态(4种)
1.运行态(Running) 2.就绪态(Ready) 3.阻塞态(Blocked) 4.挂起态(Suspended)
ucos的任务状态(5种)
1.睡眠状态 2.就绪状态 3.等待状态 4.中断服务状态 5.执行状态
- Running—运行态
当任务处于实际运行状态被称之为运行态,即 CPU 的使用权被这个任务占用。 - Ready—就绪态
处于就绪态的任务是指那些能够运行(没有被阻塞和挂起),但是当前没有运行的任务,因为同优先
级或更高优先级的任务正在运行。 - Blocked—阻塞态
由于等待信号量,消息队列,事件标志组等而处于的状态被称之为阻塞态,另外任务调用延迟函数也
会处于阻塞态。 - Suspended—挂起态
类似阻塞态,通过调用函数 vTaskSuspend()对指定任务进行挂起,挂起后这个任务将不被执行,只
有调用函数 xTaskResume()才可以将这个任务从挂起态恢复。
3. 任务优先级
3.1任务优先级说明
- FreeRTOS 中任务的最高优先级是通过 FreeRTOSConfig.h 文件中的 configMAX_PRIORITIES 进行配置的,用户实际可以使用的优先级范围是 0 到 configMAX_PRIORITIES – 1。比如我们配置此宏定义为 5,那么用户可以使用的优先级号是 0,1,2,3,4,不包含 5,对于这一点,初学者要特别的注意。
- 用户配置任务的优先级数值越小,那么此任务的优先级越低,空闲任务的优先级是 0。
- 建议用户配置宏定义 configMAX_PRIORITIES 的最大值不要超过 32,即用户任务可以使用的优先级范围是0到31。
3.2 任务优先级分配方案
- IRQ 任务:IRQ 任务是指通过中断服务程序进行触发的任务,此类任务应该设置为所有任务里面优先级最高的。
- 高优先级后台任务:比如按键检测,触摸检测,USB 消息处理,串口消息处理等,都可以归为这一类任务。
- 低优先级的时间片调度任务:比如 emWin 的界面显示,LED 数码管的显示等不需要实时执行的都可以归为这一类任务。 实际应用中用户不必拘泥于将这些任务都设置为优先级 1 的同优先级任务,可以设置多个优先级,只需注意这类任务不需要高实时性。
- 空闲任务:空闲任务是系统任务。
特别注意:IRQ 任务和高优先级任务必须设置为阻塞式(调用消息等待或者延迟等函数即可),只有这样,高优先级任务才会释放 CPU 的使用权,,从而低优先级任务才有机会得到执行。这里的优先级分配方案是我们推荐的一种方式,实际项目也可以不采用这种方法。 调试出适合项目需求的才是最好的。
3.3 任务优先级与终端优先级的区别
这两个之间没有任何关系,不管中断的优先级是多少,中断的优先级永远高于任何任务的优先级,即任务在执行的过程中,中断来了就开始执行中断服务程序。
另外对于 STM32F103,F407 和 F429 来说,中断优先级的数值越小,优先级越高。 而 FreeRTOS的任务优先级是,任务优先级数值越小,任务优先级越低。
4. 任务调度
FreeRTOS就是一款支持多任务运行的实时操作系统,具有时间片、抢占式和合作式三种调度方式。
- 合作式调度,主要用在资源有限的设备上面,现在已经很少使用了。出于这个原因,后面的
FreeRTOS 版本中不会将合作式调度删除掉,但也不会再进行升级了。 - 抢占式调度,每个任务都有不同的优先级,任务会一直运行直到被高优先级任务抢占或者遇到阻塞式的 API 函数,比如 vTaskDelay。
- 时间片调度,每个任务都有相同的优先级,任务会运行固定的时间片个数或者遇到阻塞式的 API 函数,比如vTaskDelay,才会执行同优先级任务之间的任务切换。
4.1 调度器
调度器就是使用相关的调度算法来决定当前需要执行的任务。所有的调度器有一个共同的
特性:
- 调度器可以区分就绪态任务和挂起任务(由于延迟,信号量等待,邮箱等待,事件组等待等原因而使
得任务被挂起)。 - 调度器可以选择就绪态中的一个任务,然后激活它(通过执行这个任务)。 当前正在执行的任务是运
行态的任务。 - 不同调度器之间最大的区别就是如何分配就绪态任务间的完成时间。
嵌入式实时操作系统的核心就是调度器和任务切换,调度器的核心就是调度算法。任务切换的实现在不同的嵌入式实时操作系统中区别不大,基本相同的硬件内核架构,任务切换也是相似的。调度算法就有些区别了。
4.1.1抢占式调度器
使用了抢占式调度,最高优先级的任务一旦就绪,总能得到 CPU 的控制权。 比如,当一个运行着的任务被其它高优先级的任务抢占,当前任务的 CPU 使用权就被剥夺了,或者说被挂起了,那个高优先级的任务立刻得到了 CPU 的控制权并运行。 又比如,如果中断服务程序使一个高优先级的任务进入就绪态,中断完成时,被中断的低优先级任务被挂起,优先级高的那个任务开始运行。使用抢占式调度器,使得最高优先级的任务什么时候可以得到 CPU 的控制权并运行是可知的,同时使得任务级响应时间得以最优化。
总的来说,学习抢占式调度要掌握的最关键一点是:每个任务都被分配了不同的优先级,抢占式调度器会获得就绪列表中优先级最高的任务,并运行这个任务。
在 FreeRTOS 的配置文件 FreeRTOSConfig.h 中禁止使用时间片调度,那么每个任务必须配
置不同的优先级。当 FreeRTOS 多任务启动执行后,基本会按照如下的方式去执行:
- 首先执行的最高优先级的任务 Task1,Task1 会一直运行直到遇到系统阻塞式的 API 函数,比如延迟,事件标志等待,信号量等待,Task1 任务会被挂起,也就是释放 CPU 的执行权,让低优先级的任务得到执行。
- FreeRTOS 操作系统继续执行任务就绪列表中下一个最高优先级的任务 Task2,Task2 执行过程中有两种情况:
- Task1由于延迟时间到,接收到信号量消息等方面的原因,使得 Task1从挂起状态恢复到就绪态,
在抢占式调度器的作用下,Task2 的执行会被 Task1 抢占。- Task2 会一直运行直到遇到系统阻塞式的 API 函数,比如延迟,事件标志等待,信号量等待,Task2任务会被挂起,继而执行就绪列表中下一个最高优先级的任务。
- 如果用户创建了多个任务并且采用抢占式调度器的话,基本都是按照上面两条来执行。 根据抢占式调度器,当前的任务要么被高优先级任务抢占,要么通过调用阻塞式 API 来释放 CPU 使用权让低优先级任务执行,没有用户任务执行时就执行空闲任务.
4.1.2 时间片调度器
在小型的嵌入式 RTOS 中,最常用的的时间片调度算法就是 Round-robin 调度算法。这种调度算法可以用于抢占式或者合作式的多任务中。另外,时间片调度适合用于不要求任务实时响应的情况。
实现 Round-robin 调度算法需要给同优先级的任务分配一个专门的列表,用于记录当前就绪的任务,并为每个任务分配一个时间片(也就是需要运行的时间长度,时间片用完了就进行任务切换)。
在 FreeRTOS 操作系统中只有同优先级任务才会使用时间片调度,另外还需要用户在FreeRTOSConfig.h 文件中使能宏定义:
#define configUSE_TIME_SLICING 1
默认情况下,此宏定义已经在 FreeRTOS.h 文件里面使能了,用户可以不用在FreeRTOSConfig.h 文件中再单独使能。
示例:
- 创建 4 个同优先级任务 Task1,Task2,Task3 和 Task4。
- 先运行任务 Task1,运行够 5 个系统时钟节拍后,通过时间片调度切换到任务 Task2。
- 任务 Task2 运行够 5 个系统时钟节拍后,通过时间片调度切换到任务 Task3。
- 任务 Task3 在运行期间调用了阻塞式 API 函数,调用函数时,虽然 5 个系统时钟节拍的时间片大小还没有用完,此时依然会通过时间片调度切换到下一个任务 Task4。 (注意,没有用完的时间片不会再使用,下次任务 Task3 得到执行还是按照 5 个系统时钟节拍运行)
- 任务 Task4 运行够 5 个系统时钟节拍后,通过时间片调度切换到任务 Task1。
5. 临界区、锁与系统时间
5.1 临界区与开关中断
5.2 锁
- 调度锁
- 任务锁
- 中断锁