目录
一、PendSV异常
二、任务切换场合
1、执行系统调用
2、滴答定时器(SysTick)中断
三、PendSV中断服务函数
1、分析xPortPendSVHandler中断服务函数
四、查找下一个要运行的任务的两种方法
1、获取下一个要运行的任务
2、查找下一个要运行的任务的两种方法
2.1、通用方法:所有处理器都可以使用的方法
2.2、硬件方法:需要硬件支持的方法
一、PendSV异常
PendSV(可挂起的系统调用)其优先级可以通过编程设置。可以通过将中断控制和壮态寄存器 ICSR 的 bit28,也就是 PendSV 的挂起位置 1 来触发 PendSV 中断。
与SVC异常不同,它是不精确的,因此它的挂起壮态可在更高优先级异常处理内设置,且会在高优先级处理完成后执行。利用该特性,若将PendSV设置为最低的异常优先级,可以让PendSV异常处理在所有其他中断处理完成后执行,这对于上下文切换非常有用,也是各种OS设计中的关键。
在具有嵌入式OS的典型系统中,处理时间被划分为了多个时间片。若系统中只有两个任务,这两个任务会交替执行,如下图所示:
二、任务切换场合
1、执行系统调用
执行系统调用就是执行FreeRTOS系统提供的相关API函数,比如任务切换函数taskYIELD(), FreeRTOS有些API函数也会调用函数taskYIELD(),这些API函数都会导致任务切换,这些API函数和任务切换函数taskYIELD()都统称为系统调用。函数taskYIELD()其 实就是个宏,在文件task.h中有如下定义:
在文件portmacro.h中有如下定义:
通过向中断控制和壮态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中断;这样就可以在PendSV中断服务函数中进行任务切换了。
2、滴答定时器(SysTick)中断
FreeRTOS中滴答定时器(SysTick)中断服务函数中也会进行任务切换,滴答定时器中断服务函数如下:
(1) 关闭中断。
(2) 通过向中断控制和壮态寄存器ICSR的bit28写入1挂起PendSV来启动PendSV中断;这 样就可以在PendSV中断服务函数中进行任务切换了。
(3) 打开中断。
三、PendSV中断服务函数
1、分析xPortPendSVHandler中断服务函数
(1)、读取进程栈指针,保存在寄存器RO里面。
(2)和(3),获取当前任务的任务控制块,并将任务控制块的地址保存在寄存器R2里面。
(4)和(5)、判断任务是否使用了FPU,如果任务使用了FPU的话在进行任务切换的时候就需要将FPU寄存器s16-s31手动保存到任务堆栈中,其中s0-s15和FPSCR是自动保存的。
(6)、保存s16-s31这16个FPU寄存器。
(7)、保存r4-r11和R14这几个寄存器的值。
(8)、将寄存器RO的值写入到寄存器R2所保存的地址中去,也就是将新的栈顶保存在任务控制块的第一个字段中。此时的寄存器RO保存着最新的堆栈栈顶指针值,所以要将这个最新的栈顶指针写入到当前任务的任务控制块第一个字段,而经过(2)和(3)已经获取到了任务控制块,并将任务控制块的首地址写如到了寄存器R2中。
(9)、将寄存器 R3 的值临时压栈,寄存器 R3 中保存了当前任务的任务控制块,而接下来要调用函数vTaskSwitchContext(),为了防止R3的值被改写,所以这里临时将R3的值先压栈。
(10)和(11)、关闭中断,进入临界区。
(12)、调用函数vTaskSwitchContext(),此函数用来获取下一个要运行的任务,并将pxCurrentTCB更新为这个要运行的任务。
(13)和(14)、打开中断,退出临界区。
(15)、刚保存的寄存器R3的值出栈,恢复寄存器R3的值。注意,经过(12)步,此时pxCurrentTCB的值已经改变了,所以读取R3所保存的地址处的数据就会发现其值改变了,成为了下一个要运行的任务的任务控制块。
(16)和(17)、获取新的要运行的任务的任务堆栈栈顶,并将栈顶保存在寄存器R0中。
(18)、R4-R11,R14出栈,也就是即将运行的任务的现场。
(19)、 (20)和(21)、判断即将运行的任务是否有使用到FPU,如果有的话还需要手工恢复FPU的 s16~s31 寄存器。
(22)、更新进程栈指针PSP的值。
(23)、执行此行代码以后硬件自动恢复寄存器RO-R3、 R12、LR、PC和xPSR的值,确定异常返回以后应该进入处理器模式?还是进程模式?,使用主栈指针(MSP)?还是进程栈指针(PSP)?。很明显这里会进入进程模式,并且使用进程栈指针(PSP),寄存器PC值会被恢复为即将运行的任务的任务函数,新的任务开始运行!至此,任务切换成功。
总结:将当前运行的任务的寄存器保存到栈中,再将就绪任务的数据从栈中保存到寄存器,然后运行。
四、查找下一个要运行的任务的两种方法
1、获取下一个要运行的任务
在PendSV中断服务程序中有调用函数vTaskSwitchContext()来获取下一个要运行的任务,也就是查找已经就绪了的优先级最高的任务:
(1)、如果调度器挂起那就不能进行任务切换。
(2)、调用函数taskSELECT_HIGHEST_PRIORITY_TASK()获取下一个要运行的任务。
2、查找下一个要运行的任务的两种方法
2.1、通用方法:所有处理器都可以使用的方法
(1) 从就绪任务列表找到优先级最高的就绪任务。
(2) 使用函数listGET_OWNER_OF_NEXT_ENTRY()来获取列表中的下一个列表项,然后将获取到的列表项所对应的任务控制块赋值给pxCurrentTCB,这样我们就确定了下 一个要运行的任务了。
2.2、硬件方法:需要硬件支持的方法
硬件方法就是使用处理器自带的硬件指令来实现的,比如Cortex-M处理器就带有的计算前导0个数指令: CLZ,函数如下:
(1)、通过函数 portGET_HIGHEST_PRIORITY() 获取处于就绪态的最高优先级。
(2)、使用函数listGET_OWNER_OF_NEXT_ENTRY()来获取列表中的下一个列表项,然后将获取到的列表项所对应的任务控制块赋值给pxCurrentTCB,这样我们就确定了下一个要运行的任务了。
可以看出硬件方法借助一个指令就可以快速的获取处于就绪态的最高优先级,但是会限制任务的优先级数,比如STM32只能有 32 个优先级,不过 32 个优先级已经完全够用了。要知道FreeRTOS是支持时间片的,每个优先级可以支持无限多个任务。