简单的说,调度器就是使用相关的调度算法来决定当前需要执行的任务。
FreeRTOS 操作系统支持三种调度方式:抢占式调度,时间片调度和合作式调度。
合作式调度器提供了一种单任务的系统结构:
当任务需要运行的时候,被添加到等待队列
任务在特定的时刻被调度运行(以周期性或者单次方式)
任务运行直到完成 (高优先级任务不可抢占 CPU),然后由调度器选择下一个任务
优点:调度简单,系统占用资源少 (单任务结构,运行时高优先级任务不会抢占 CPU,不需要给每个任务分配独立的栈空间)
缺点:系统实时性不够好
Note:
单片机资源越来越丰富,加上合作式调度器的系统实时性不够好,合作式调度已经很少用了,FreeRTOS 在新的版本中已不再更新,简单了解即可。
抢占式调度器提供了一种多任务的的系统结构,高优先级任务可以抢占低优先级任务的 CPU 使用权,使得系统实时性非常好。
使用抢占式调度器时,根据任务重要程度合理分配优先级,CPU 会优先执行就绪列表中优先级最高的任务。
下面图片: 任务 1 优先级 < 任务 2 优先级 < 任务 3 优先级
时间片调度针对同优先级的任务,调度算法给同优先级的任务分配一个专门的列表,用于记录当前就绪的任务,并为每个任务分配一个时间片。
下面图片: 任务 1 优先级 = 任务 2 优先级 = 任务 3 优先级 = 任务 4 优先级
实验方法:
创建 3 个任务, 情况如下:
任务 1: 优先级低,阻塞,打印信息
任务 2: 优先级中,运行时不阻塞,不挂起,闪灯
任务 3: 优先级高,阻塞,打印信息
实验分析:
因为任务 2 一直运行,不释放 CPU,使得低优先级的任务 1 无法获得 CPU 使用权,而任务 3 优先高,可以抢占 CPU 获得运行。
实验现象:
任务 2 指示灯闪烁,任务 3 打印信息输出。
Note:
可以将任务 2 的优先级设置为最高,观察实验现象。
实验方法:
创建 3 个相同优先级的任务,打印信息,延时 10ms,灯闪烁。 另外,方便测试,FreeRTOS 的 Tick 设置为 20,也就是每 50ms 进行一次系统调度。
实验分析:
3 个任务优先级相同,按时间片调度,调度时间为 50ms,因此每个任务可以连续执行 5 次。
实验现象:
3 个任务循环打印 5 次,闪 5 次灯。
Note:
可以禁止时间片调度,观察实验现象。
在 FreeRTOS 的应用设计中,每个任务都需要独立的栈空间,而且每个任务需要的栈大小也是不同的。将如下的几个选项简单的累加就可以得到一个粗略的栈大小:
1、函数
① 局部变量
② 函数形参 (针对函数嵌套)
③ 函数返回地址 (针对函数嵌套)
④ 函数内部的状态保存
2、任务切换
3、发生中断
实际应用中将这些都加起来是一件非常麻烦的工作,上面这些栈空间加起来的总和只是栈的最小需求,实际分配的栈大小可以在最小栈需求的基础上乘以一个安全系数,一般取 1.5-2。
上面的计算是我们用户可以确定的栈大小,项目应用中还存在无法确定的栈大小,比如调用 printf 函数就很难确定实际的栈消耗。又比如通过函数指针实现函数的间接调用,因为函数指针不是固定的指向一个函数进行调用,而是根据不同的程序设计可以指向不同的函数,使得栈大小的计算变得比较麻烦。
另外还要注意一点,建议不要编写递归代码,因为我们不知道递归的层数,栈的大小也是不好确定的。
建议:
可以事先给任务分配一个大的栈空间,然后通过第二讲介绍的调试方法打印任务栈的使用情况,运行一段时间就会有个大概的范围了,再乘以安全系数,即可得到需要使用的栈空间。
如下图:Test1-3 使用的栈空间:(128-106)=22 words, 乘以安全系数,分配 33 words 即可。
上节讲解了如何确定任务栈的大小,那什么又是栈溢出呢?简单的说就是用户分配的栈空间不够用了,溢出了。
下面我们举一个简单的实例,栈生长方向从高地址向低地址生长(M4 和 M3 是这种方式):
① 上图标识 1 的位置是 RTOS 的某个任务调用了函数 test() 前的 SP 栈指针位置
② 上图标识 2 的位置是调用了函数 test 需要保存返回地址到栈空间。这一步不是必须的,对于 M3 和 M4 内核是先将其保存到 LR 寄存器中,如果 LR 寄存器中有保存上一级函数的返回地址,需要将 LR 寄存器中的内容先入栈
③ 上图标识 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
实验方法:
本实验就是在任务 KEY_Task 中申请过大的数组,模拟栈溢出的情况,检测到按键 1 按下时,对数组赋值,模拟产生系统硬件错误。 检测溢出后触发钩子函数,将发生栈溢出的任务打印出来。
实验现象:
串口打印 KEY 任务发生栈溢出,按下 KEY1,系统产生硬件错误。
Note:
FreerRTOS 栈溢出检测机制的 2 种方法都试试。
#define configCHECK_FOR_STACK_OVERFLOW 1
#define configCHECK_FOR_STACK_OVERFLOW 2