FreeRTOS学习(四)任务调度与切换

1.任务调度

  在建立完任务后紧接着调用任务调度函数,便会使系统运行起来

void vTaskStartScheduler( void )  // 启动任务调度器

  概要来说,任务调度函数主要做了下面几件事:

  • 创建空闲任务,如果使用软件定时器,还会创建定时器
  • 设置中断优先级,包括 PendSVSysTick
  • 开启第一个任务,由 SVCHandler 实现

  任务调度器触发了 SVC 中断来启动第一个任务,之后的工作都靠 PendSVSysTick 中断触发来实现。接下来就来唠唠这三个中断,这三个中断都是在 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 上下文

  任务切换的本质是任务上下文的切换,上文其实就是待切出任务(当前正在运行)的任务控制块TCBCPU相关寄存器状态,下文就是待切入(抢占)任务的任务控制块TCBCPU相关寄存器状态
  寄存器是宝贵资源,某个时刻只能储存某个任务的值,要使得当前任务的寄存器状态得到保留,需要把当前的寄存器压栈,恢复的时候再弹出到寄存器,这样一个寄存器就可以不断使用了。
在这里插入图片描述

2.3 切换场景

  发生任务切换的场合有2个,一个是优先级抢占,另一个是时间片轮转。在FreeRTOS中的表达为:

  1. 系统调用 ⇌ 优先级抢占
  2. 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 表示硬件方法,否则使用通用方法。

  1. 通用方法:从就绪列表中选取,适用于任何处理器,但效率低
  2. 硬件方法:每个 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中断服务函数
  • 任务切换的过程为:保存现场、跳转到下一个任务、恢复现场
THE END
< <上一篇
下一篇>>