我发现学习 RTOS 是学习 Linux 内核的好方法。大有弯道超车的可能。
主要是采用超级循环系统(前后台系统),应用程序是一个无限的循环,循环中调用相应的函数完成相应的操作,这部分可以看做后台行为;中断服务程序处理异步事件,这部分可以看做是前台行为。后台也可以叫做任务级,前台也叫作中断级。
前后台系统的编程思路有两种:轮询方式(实时性得不到保障,紧急与非紧急消息不能有效管理)、中断方式(可以保证一定的实时性,紧急消息可以得到响应)。
采用中断和查询结合的方式可以解决大部分裸机应用,但随着工程的复杂,裸机方式的缺点就暴露出来了:
采用多任务系统可以以上的裸机开发遇到的4大缺点。
RTOS的实现重点就在这个OS任务调度器上,调度器的作用就是使用相关的调度算法来决定当前需要执行的任务。FreeRTOS就是一款支持多任务运行的实时操作系统,具有时间片、抢占式和合作式三种调度方式。通过 FreeRTOS 实时操作系统可以将程序函数分成独立的任务,并为其提供合理的调度方式。
栈大小 0x400 = 1024,单位字节。在RTOS下,上面截图里设置的栈大小有了一个新名字叫做系统栈空间,而任务栈是不使用这里的空间,哪里使用这里的栈空间呢,实际上是中断函数和中断嵌套。
64 字节:对于 Cortex-M3 内核和未使用 FPU(浮点运算单元)功能的 Cortex-M4 内核在发生中断时需要将 16 个通用寄存器全部入栈,每个寄存器占用 4 个字节,也就是 16*4 = 64 字节的空间。可能发生几次中断嵌套就是要 64 乘以几即可。当然,这种是最坏执行情况,也就是所有的寄存器都入栈。(注:任务执行的过程中发生中断的话,有 8 个寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余寄存器入栈以及发生中断嵌套都是用的系统栈) 200 字节: 对于具有 FPU(浮点运算单元)功能的 Cortex-M4 内核,如果在任务中进行了浮点运算,那么在发生中断的时候除了 16 个通用寄存器需要入栈,还有 34 个浮点寄存器也是要入栈的,也就是(16+34)*4 = 200 字节的空间。当然,这种是最坏执行情况,也就是所有的寄存器都入栈。
栈生长方向从高地址向低地址生长(M4 和 M3 是这种方式)
上图标识 3 的位置是局部变量 int i 和 int array[10]占用的栈空间,但申请了栈空间后已经越界了。这个就是所谓的栈溢出了。如果用户在函数 test 中通过数组 array 修改了这部分越界区的数据且这部分越界的栈空间暂时没有用到或者数据不是很重要,情况还不算严重,但是如果存储的是关键数据,会直接导致系统崩溃。 上图标识 4 的位置是局部变量申请了栈空间后,栈指针向下偏移(返回地址+变量 i+10 个数组元素)*4 =48 个字节。 上图标识 5 的位置可能是其它任务的栈空间,也可能是全局变量或者其它用途的存储区,如果 test函数在使用中还有用到栈的地方就会从这里申请,这部分越界的空间暂时没有用到或者数据不是很重要,情况还不算严重,但是如果存储的是关键数据,会直接导致系统崩溃。
FreeRTOS 提供了两种栈溢出检测机制,这两种检测都是在任务切换时才会进行:
使用方法一需要用户在 FreeRTOSConfig.h 文件中配置如下宏定义:
#define configCHECK_FOR_STACK_OVERFLOW 1
使用方法二需要用户在 FreeRTOSConfig.h 文件中配置如下宏定义:
#define configCHECK_FOR_STACK_OVERFLOW 2
除了 FreeRTOS 提供的这两种栈溢出检测机制,还有其它的栈溢出检测机制,大家可以在 Mircrium 官方发布的如下这个博文中学习:https://www.micrium.com/detecting-stack-overflows-part-2-of-2/
FreeRTOS的任务状态(4种):1.运行态(Running) 2.就绪态(Ready) 3.阻塞态(Blocked) 4.挂起态(Suspended)
ucos的任务状态(5种):1.睡眠状态 2.就绪状态 3.等待状态 4.中断服务状态 5.执行状态
当任务处于实际运行状态被称之为运行态,即 CPU 的使用权被这个任务占用。
处于就绪态的任务是指那些能够运行(没有被阻塞和挂起),但是当前没有运行的任务,因为同优先 级或更高优先级的任务正在运行。
由于等待信号量,消息队列,事件标志组等而处于的状态被称之为阻塞态,另外任务调用延迟函数也 会处于阻塞态。
类似阻塞态,通过调用函数 vTaskSuspend()对指定任务进行挂起,挂起后这个任务将不被执行,只 有调用函数 xTaskResume()才可以将这个任务从挂起态恢复。
特别注意:IRQ 任务和高优先级任务必须设置为阻塞式(调用消息等待或者延迟等函数即可),只有这样,高优先级任务才会释放 CPU 的使用权,,从而低优先级任务才有机会得到执行。这里的优先级分配方案是我们推荐的一种方式,实际项目也可以不采用这种方法。调试出适合项目需求的才是最好的。
这两个之间没有任何关系,不管中断的优先级是多少,中断的优先级永远高于任何任务的优先级,即任务在执行的过程中,中断来了就开始执行中断服务程序。
另外对于 STM32F103,F407 和 F429 来说,中断优先级的数值越小,优先级越高。而 FreeRTOS的任务优先级是,任务优先级数值越小,任务优先级越低。
FreeRTOS就是一款支持多任务运行的实时操作系统,具有时间片、抢占式和合作式三种调度方式。
调度器就是使用相关的调度算法来决定当前需要执行的任务。所有的调度器有一个共同的 特性:
嵌入式实时操作系统的核心就是调度器和任务切换,调度器的核心就是调度算法。任务切换的实现在不同的嵌 入式实时操作系统中区别不大,基本相同的硬件内核架构,任务切换也是相似的。调度算法就有些区别了。
使用了抢占式调度,最高优先级的任务一旦就绪,总能得到 CPU 的控制权。比如,当一个运行着的任务被其它高优先级的任务抢占,当前任务的 CPU 使用权就被剥夺了,或者说被挂起了,那个高优先级的任务立刻得到了 CPU 的控制权并运行。又比如,如果中断服务程序使一个高优先级的任务进入就绪态,中断完成时,被中断的低优先级任务被挂起,优先级高的那个任务开始运行。使用抢占式调度器,使得最高优先级的任务什么时候可以得到 CPU 的控制权并运行是可知的,同时使得任务级响应时间得以最优化。
总的来说,学习抢占式调度要掌握的最关键一点是:每个任务都被分配了不同的优先级,抢占式调度器会获得就绪列表中优先级最高的任务,并运行这个任务。
在 FreeRTOS 的配置文件 FreeRTOSConfig.h 中禁止使用时间片调度,那么每个任务必须配置不同的优先级。当 FreeRTOS 多任务启动执行后,基本会按照如下的方式去执行:
在小型的嵌入式 RTOS 中,最常用的的时间片调度算法就是 Round-robin 调度算法。这种调度算法可以用于抢占式或者合作式的多任务中。另外,时间片调度适合用于不要求任务实时响应的情况。
实现 Round-robin 调度算法需要给同优先级的任务分配一个专门的列表,用于记录当前就绪的任务,并为每个任务分配一个时间片(也就是需要运行的时间长度,时间片用完了就进行任务切换)。
在 FreeRTOS 操作系统中只有同优先级任务才会使用时间片调度,另外还需要用户在FreeRTOSConfig.h 文件中使能宏定义:
#define configUSE_TIME_SLICING 1
默认情况下,此宏定义已经在 FreeRTOS.h 文件里面使能了,用户可以不用在FreeRTOSConfig.h 文件中再单独使能。示例:
代码的临界临界区,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界区代码的执行不被中断,在进入临界区之前须关中断,而临界区代码执行完毕后,要立即开中断。
FreeRTOS 的源码中有多处临界段的地方, 临界段虽然保护了关键代码的执行不被打断, 但也会影响系统的实时性。比如此时某个任务正在调用系统 API 函数,而且此时中断正好关闭了,也就是进入到了临界区中,这个时候如果有一个紧急的中断事件被触发,这个中断就不能得到及时执行,必须等到中断开启才可以得到执行, 如果关中断时间超过了紧急中断能够容忍的限度, 危害是可想而知的。
FreeRTOS 源码中就有多处临界段的处理,跟 FreeRTOS 一样,uCOS-II 和 uCOS-III 源码中都是有临界段的,而 RTX 的源码中不存在临界段。另外,除了 FreeRTOS 操作系统源码所带的临界段以外,用户写应用的时候也有临界段的问题,比如以下两种:
重入函数的实现特征一般而言,重入函数具有如下特征:
FreeRTOS 任务代码中临界段的进入和退出主要是通过操作寄存器 basepri 实现的。进入临界段前操作寄存器 basepri 关闭了所有小于等于宏定义configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY所定义的中断优先级,这样临界段代码就不会被中断干扰到,而且实现任务切换功能的 PendSV 中断和滴答定时器中断是最低优先级中断,所以此任务在执行临界段代码期间是不会被其它高优先级任务打断的。退出临界段时重新操作 basepri 寄存器,即打开被关闭的中断(这里我们不考虑不受 FreeRTOS 管理的更高优先级中断)。
与任务代码里临界段的处理方式类似,中断服务程序里面临界段的处理也有一对开关中断函数。
调度锁就是 RTOS 提供的调度器开关函数,如果某个任务调用了调度锁开关函数,处于调度锁开和调度锁关之间的代码在执行期间是不会被高优先级的任务抢占的,即任务调度被禁止。这一点要跟临界段的作用区分开,调度锁只是禁止了任务调度,并没有关闭任何中断,中断还是正常执行的。而临界段进行了开关中断操作。调度锁相关函数;
简单的说,为了防止当前任务的执行被其它高优先级的任务打断而提供的锁机制就是任务锁。
FreeRTOS 也没有专门的任务锁函数,但是使用 FreeRTOS 现有的功能有两种实现方法:
中断锁就是 RTOS 提供的开关中断函数,FreeRTOS 没有专门的中断锁函数,使用中断服务程序临界段处理函数就可以实现同样效果。
任何操作系统都需要提供一个时钟节拍,以供系统处理诸如延时、 超时等与时间相关的事件。时钟节拍是特定的周期性中断,这个中断可以看做是系统心跳。中断之间的时间间隔取决于不同的应用,一般是 1ms – 100ms。时钟的节拍中断使得内核可以将任务延迟若干个时钟节拍,以及当任务等待事件发生时,提供等待超时等依据。时钟节拍率越快,系统的额外开销就越大。任何操作系统都需要提供一个时钟节拍,以供系统处理诸如延时、 超时等与时间相关的事件。时钟节拍是特定的周期性中断,这个中断可以看做是系统心跳。中断之间的时间间隔取决于不同的应用,一般是 1ms – 100ms。时钟的节拍中断使得内核可以将任务延迟若干个时钟节拍,以及当任务等待事件发生时,提供等待超时等依据。时钟节拍率越快,系统的额外开销就越大。
对于 Cortex-M3 内核的 STM32F103 和 Cortex-M4 内核的 STM32F407 以及 F429,教程配套的例子都是用滴答定时器来实现系统时钟节拍的。
时间管理功能是 FreeRTOS 操作系统里面最基本的功能,同时也是必须要掌握好的。
FreeRTOS 中的时间延迟函数主要有以下两个作用:
通过如下的框图来说明一下延迟函数对任务运行状态的影响,有一个形象的认识。
对任务 Task1 的运行状态做说明,调度器支持时间片调度和抢占式调度。运行过程描述如下: