1.任务调度
在建立完任务后紧接着调用任务调度函数,便会使系统运行起来
void vTaskStartScheduler( void ) // 启动任务调度器
概要来说,任务调度函数主要做了下面几件事:
- 创建空闲任务,如果使用软件定时器,还会创建定时器
- 设置中断优先级,包括
PendSV
和SysTick
- 开启第一个任务,由
SVCHandler
实现
任务调度器触发了 SVC
中断来启动第一个任务,之后的工作都靠 PendSV
和 SysTick
中断触发来实现。接下来就来唠唠这三个中断,这三个中断都是在 FreeRTOSConfig.h
的配置文件中进行了宏定义。
/* Definitions that map the FreeRTOS port interrupt handlers to their CMSIS
standard names. */
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
/* IMPORTANT: FreeRTOS is using the SysTick as internal time base, thus make sure the system and peripherials are
using a different time base (TIM based for example).
*/
#define xPortSysTickHandler SysTick_Handler
如果这三个中断函数与移植平台有重定义冲突,前两个应该使用 FreeRTOS
定义的函数,SysTick_Handler
使用STM32
中断文件内定义的函数,但需要进行修改。至于原因,跟这三个中断的功能有关
SVC_Handler
:开启第一个任务函数,只在任务调度中使用一次PendSV_Handler
:触发任务切换SysTick_Handler
:时间片调度,同一优先级有多个任务,其本质也是触发 PendSV 中断
void SysTick_Handler(void)
{
HAL_IncTick();
/* USER CODE BEGIN SysTick_IRQn 1 */
#if (INCLUDE_xTaskGetSchedulerState == 1 )
if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
{
#endif /* INCLUDE_xTaskGetSchedulerState */
xPortSysTickHandler();
#if (INCLUDE_xTaskGetSchedulerState == 1 )
}
#endif /* INCLUDE_xTaskGetSchedulerState */
}
2.任务切换
2.1 SVC 和 PendSV
在上一节中也提到了这两个中断的中断服务函数的功能,这里再详细介绍一下。
(1) SVC 中断
SVC
是系统服务调用,由 SVC
指令触发调用。在 FreeRTOS 中用来在任务调度中开启第一个任务。触发指令:
svc 0
(2) PendSV 中断
与SVC
相关的是PendSV
中断,称为可悬起的系统调用。两者不同之处在于响应速度,SVC
中断是要求被立刻得到响应的,而PendSV
中断则可以延迟被响应,也就是PendSV
中断可以先“悬起”,待其它重要中断被执行完成后再处理它。
PendSV
中断的意义在于当需要发生任务切换时,如果当前正在执行一个中断,则等待中断执行完毕后,再进行任务切换。即不允许打断中断来切换任务。
悬起PendSV
的方法是:手工往NVIC的PendSV悬起寄存器中写1,悬起后,如果优先级不够其它中断高,则将延迟等待执行。
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; // 向中断控制和状态寄存器的bit28写入1挂起PendSV来启动PendSV中断
2.2 上下文
任务切换的本质是任务上下文的切换,上文其实就是待切出任务(当前正在运行)的任务控制块TCB
和CPU相关寄存器状态
,下文就是待切入(抢占)任务的任务控制块TCB
和CPU相关寄存器状态
。
寄存器是宝贵资源,某个时刻只能储存某个任务的值,要使得当前任务的寄存器状态得到保留,需要把当前的寄存器压栈,恢复的时候再弹出到寄存器,这样一个寄存器就可以不断使用了。
2.3 切换场景
发生任务切换的场合有2个,一个是优先级抢占,另一个是时间片轮转。在FreeRTOS中的表达为:
- 系统调用 ⇌ 优先级抢占
- SysTick中断 ⇌ 时间片轮转
其中优先级抢占不一定是高优先级抢占,可能的情况还有任务调用系统API主动放弃CPU,让其它任务先行等等。如执行一个系统调用:taskYIELD()
。
而时间片轮转发生在相同优先级的任务中,SysTick
中断是FreeRTOS
的周期性中断,每隔一段时间就会发生中断,调度器需要在里面执行一些自身的程序。
无论是哪个场合,最终都需要PendSV
中断的处理,比如系统调用taskYIELD()
,跟踪源码可以发现设置如下
taskYIELD()
portYIELD()
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
而跟踪SysTick
中断服务函数最终也会发现有一样的设置。表明时间片调度实现任务切换也是通过 PendSV
标志位置位触发 PendSV
中断实现,但有个前提条件,就是同一优先级有多个任务。
SysTick_Handler()
xPortSysTickHandler();
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
最终都是portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT
,意思是手工往NVIC的PendSV悬起寄存器中写1。
扩展一点,在任务切换选择下一个要运行的任务时,有两种方法,通过宏configUSE_PORT_OPTIMISED_TASK_SELECTION
控制,1 表示硬件方法,否则使用通用方法。
- 通用方法:从就绪列表中选取,适用于任何处理器,但效率低
- 硬件方法:每个 bit 代表一个优先级,获取前导零个数判断最高优先级
2.4 PendSV_Handler
因为任务切换最终都会发生PendSV
中断,在PendSV中断服务函数
中进行,中断服务函数为PendSV_Handler
,但是在FreeRTOSConfig.h
中被改名为xPortPendSVHandler
,内容使用汇编编写的,它主要的工作如下
比较关心的是改变pxCurrentTCB
(回顾上一文),用到了一个函数vTaskSwitchContext()
,调用层次下
vTaskSwitchContext()
taskSELECT_HIGHEST_PRIORITY_TASK()
portGET_HIGHEST_PRIORITY() //得到高优先级任务
listGET_OWNER_OF_NEXT_ENTRY() //设置pxCurrentTCB
到此,在PendSV_Handler中断服务函数中,就完成了任务的切换。
3.总结
SVC
中断就是软中断,给用户提供一个访问硬件的接口PendSV
中断相对SVC
来说,是可以被延迟执行的,用于任务切换- 任务切换可以发生在系统调用中,也可以发生在时间片轮转中
- 无论哪个情况,任务切换最终都会进入
PendSV
中断服务函数 - 任务切换的过程为:保存现场、跳转到下一个任务、恢复现场