FreeRTOS任务切换的实现

在进行FreeRTOS任务切换的介绍前,我们先来了解一下SVC和PendSV。

SVC和PendSV

SVC(系统服务调用,亦简称系统调用)和 PendSV(可悬起系统调用),它们多用于在操作系统之上的软件开发中。SVC用于产生系统函数调用的请求。操作系统不让用户直接访问硬件,而是通过提供一些系统服务函数,用户程序通过使用SVC发出对系统服务函数的调用请求,以系统服务函数间接的去访问硬件。因此当用户程序想要控制特定的硬件时,它就会产生一个SVC异常,然后操作系统提供的SVC异常服务例程得到执行,它再调用相关的操作系统函数,由它来完成用户程序请求的服务。

PendSV(可悬起的系统调用),它和 SVC 协同使用。一方面,SVC异常是必须立即得到响应的(若因优先级不比当前正处理的高,或是其它原因使之无法立即响应,将上访成硬 fault),应用程序执行 SVC 时都是希望所需的请求立即得到响应。另一方面,PendSV 则不同,它是可以像普通的中断一样被悬起的(不像 SVC那样会上访)。OS 可以利用它“缓期执行”一个异常,直到其它重要的任务完成后才执行动作。悬起 PendSV 的方法是:手工往 NVIC 的 PendSV 悬起寄存器中写 1。悬起后,如果优先级不够高,则将缓期等待执行。

PendSV 的典型使用场合是在上下文切换时(在不同任务之间切换)。例如,一个系统中有两个就绪的任务,上下文切换被触发的场合可以是:

1.执行一个系统调用

2.系统滴答定时器(SYSTICK)中断

举个简单的例子来辅助理解。假设有这么一个系统,里面有两个就绪的任务,并且通过 SysTick 异常启动上下文切换。

在典型的RTOS中,任务的执行时间被分为多个时间片,每个任务执行相对应时间片。上图的任务切换是在Systick中断中完成的,每触发一次Systick中断就会进行任务切换。但若在产生Systick异常时正在响应一个中断,则Systick异常会抢占其中断。在这种情况下,OS不得执行上下文切换,否则将使中断请求被延迟,,而且在真实系统中,延时时间往往是不可预知的,这对于实时操作系统来说是不能容忍的。由于存在中断请求,ARM Cortex-M 不允许返回线程模式,因此,将会产生用法错误异常(Usage Fault)。

在一些 RTOS 的设计中,会通过判断是否存在中断请求,来决定是否进行任务切换。虽然可以通过检查 xPSR 或 NVIC 中的中断活跃寄存器来判断是否存在中断请求,但是这样可能会影响系统的性能,甚至可能出现当某中断源的频率和 SysTick 异常的频率比较接近时,会发生“共振”。中断源在 SysTick 中断前后不断产生中断请求,导致系统无法进行任务切换的情况。

使用PendSV就能解决这个问题,PendSV 异常会自动延迟上下文切换的请求,直到其它的 ISR 都完成了处理后才放行。为实现这个机制,需要把 PendSV 编程为最低优先级的异常。如果 OS 检测到某 IRQ 正在活动并且被 SysTick 抢占,它将悬起一个 PendSV 异常,以便缓期执行上下文切换。

1.任务A呼叫SVC来请求任务切换(例如,任务A正在等待某些工作完成)。

2.内核接到请求,做好上下文切换的准备,并且悬起一个PendSV异常。

3.当退出SVC后,立即进入PendSV,在PendSV中执行上下文切换。

4.当PendSV执行完毕后,返回线程模式,切换到执行任务B。

5.发生了一个中断,开始执行中断服务程序。

6.在中断执行过程中,发生了Systick异常(用于内核时钟节拍),并且抢占了该中断。

7.操作系统执行必要的操作,如更新内部时间计数、遍历延迟任务队列等,然后挂起PendSV异常,准备进行任务切换。

8.当Systick退出后,回到先前被抢占的ISR中,ISR继续执行。

9.当中断执行完毕后,PendSV服务例程开始执行,完成任务切换。

10.当PendSV执行完毕后,回到任务A,同时系统再次进入线程模式。

FreeRTOS中的任务切换就是在PendSV中完成的。

PendSV 中断服务函数

FreeRTOS 在 PendSV 的中断中,完成任务切换,PendSV 的中断服务函数由 FreeRTOS 编写,将 PendSV 的中断服务函数定义成函数 xPortPendSVHandler()。针 对 ARM Cortex-M3 和针对 ARM Cortex-M4 和 ARM Cortex-M7 内 核 的 函 数xPortPendSVHandler()稍有不同,其主要原因在于 ARM Cortex-M4 和 ARM Cortex-M7 内核具有浮点单元,因此在进行任务切换的时候,还需考虑是否保护和恢复浮点寄存器的值。针对 ARM Cortex-M3 内核的函数xPortPendSVHandler(),具体的代码如下所示

__asm void xPortPendSVHandler( void )
{
	/* 导入全局变量及函数 */
	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;
	
	/* 8 字节对齐 */
	PRESERVE8

	/* 从 PSP 寄存器(进程堆栈指针)中获取当前任务的堆栈指针 */
	mrs r0, psp
	
	/* isb 指令用于确保指令的严格顺序执行,以提高代码的可靠性和正确性 */
	isb
	/* 将 pxCurrentTCB 的 地址值 加载到 r3 寄存器中,即指向当前运行任务控制块的指针 */
	ldr	r3, =pxCurrentTCB		/* Get the location of the current TCB. */
	/* 将 r3 寄存器中存储的内存地址所对应的数据加载到 r2 寄存器中,即当前运行任务控制块的首地址 */
	ldr	r2, [r3]

	/* 将 R4~R11 入栈到当前运行任务的任务栈中 */
	stmdb r0!, {r4-r11}			/* Save the remaining registers. */
	/* r0 的内容会被存储到由 R2 寄存器指定的内存地址中
	将当前任务的堆栈指针中的内容存到 R2. R2 指向的地址为此时的任务栈指针 */
	str r0, [r2]				/* Save the new top of stack into the first member of the TCB. */


	/* 将 r3 和 r14 保存到内核堆栈MSP中,然后设置 basepri 寄存器的值为
	configMAX_SYSCALL_INTERRUPT_PRIORITY,屏蔽受 FreeRTOS 管理的所有中断。
	然后执行数据同步操作(dsb)和指令同步操作(isb),确保之前的操作已完成 */
	stmdb sp!, {r3, r14}
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0
	dsb
	isb

	/* 跳转到函数 vTaskSeitchContext
	 * 主要用于更新 pxCurrentTCB,
	 * 使其指向最高优先级的就绪态任务
	 */
	bl vTaskSwitchContext
	mov r0, #0
	/* 将 basepri 寄存器的值恢复为 0,为 0 表示允许所有优先级的中断 */
	msr basepri, r0
	
	/* 这部分代码从栈中弹出 r3 和 r14 的值,这两个寄存器用于保存任务
	控制块指针和链接寄存器。然后,它加载当前任务控制块中的栈顶指针
	到 r0 中,接着将 r4-r11 的值从栈中弹出并存储到相应的寄存器中,
	最后使用 msr 指令将当前任务的栈指针存储到 psp 寄存器中 */
	

	/* 将 R3、R14 重新从 MSP 指向的栈中出栈 
	R3 为 pxCurrentTCB 的地址值,
	pxCurrentTCB 已经在函数 vTaskSwitchContext 中更新为最高优先级的就绪态任务
	因此 r1 为 pxCurrentTCB 的值,即当前最高优先级就绪态任务控制块的首地址 */
	ldmia sp!, {r3, r14}
	ldr r1, [r3]
	/* r0 为最高优先级就绪态任务的任务栈指针 */
	ldr r0, [r1]				/* pxCurrentTCB中的第一项是任务栈栈顶 */
	
	/* 从最高优先级就绪态任务的任务栈中出栈 R4~R11 和 R14 
	注意:这里出栈的 R14 为 EXC_RETURN,其保存了任务是否使用浮点单元的信息 */
	ldmia r0!, {r4-r11}			/* Pop the registers and the critical nesting count. */
	/* 使用 msr 指令将 r0 的值存储到 psp 寄存器中,用作任务的栈指针 */
	msr psp, r0

	isb
	/*  使用 bx 指令 跳转到切换后的任务运行
		执行此指令,CPU 会自动从 PSP 指向的任务栈中,
		出栈 R0、R1、R2、R3、R12、LR、PC、xPSR 寄存器,
		接着 CPU 就跳转到 PC 指向的代码位置运行,
		也就是任务上次切换时运行到的位置 */
	bx r14
	nop
}

所以从上面的代码可以分析出,FreeRTOS在进行任务切换的时候首先会对当前任务的状态信息进行入栈保存,保存到任务的任务栈中。然后调用vTaskSwitchContext函数更新当前最高优先级任务的指向,然后从切换后的任务的任务栈中进行出栈,恢复新的任务的状态信息,最后使用bx指令跳转到切换后的任务执行。

从该函数中只看到了程序保存和恢复CPU信息中的部分寄存器信息(R4R11),这是因为硬件会自动入栈和出栈其他CPU寄存器信息。在任务运行的时候,CPU使用进程堆栈指针PSP最为栈空间来使用,也就是使用运行任务的任务栈。当Systick中断发生时,在跳转到Systick中断服务函数运行前,硬件会自动除将R4R11寄存器的其他CPU寄存器入栈,因此就将任务切换前的部分信息保存到了对应任务的任务栈中,当退出PendSV时,会自动从栈空间中恢复这部分信息,以供任务正常运行。

可以通过结合下面两张图来结合理解:

任务栈如下图所示:

vTaskSwitchContext() 函数

void vTaskSwitchContext(void)
{
	/* 任务调度器未在运行 */
	if (uxSchedulerSuspended != (UBaseType_t)pdFALSE)
	{
		/* 调度程序当前挂起-不允许进行上下文切换,直接退出函数 */
		xYieldPending = pdTRUE;
	}
	else
	{
		xYieldPending = pdFALSE;
		traceTASK_SWITCHED_OUT();

#if (configGENERATE_RUN_TIME_STATS == 1)
		{
#ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
			portALT_GET_RUN_TIME_COUNTER_VALUE(ulTotalRunTime);
#else
			ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
#endif

			if (ulTotalRunTime > ulTaskSwitchedInTime)
			{
				pxCurrentTCB->ulRunTimeCounter += (ulTotalRunTime - ulTaskSwitchedInTime);
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
			ulTaskSwitchedInTime = ulTotalRunTime;
		}
#endif /* configGENERATE_RUN_TIME_STATS */

		taskCHECK_FOR_STACK_OVERFLOW();

		/* 此函数用于将 pxCurrentTCB 更新为指向优先级最高的就绪态任务 */
		taskSELECT_HIGHEST_PRIORITY_TASK();
		traceTASK_SWITCHED_IN();

#if (configUSE_NEWLIB_REENTRANT == 1)
		{
			_impure_ptr = &(pxCurrentTCB->xNewLib_reent);
		}
#endif /* configUSE_NEWLIB_REENTRANT */
	}
}

该函数内部通过调用宏函数 taskSELECT_HIGHEST_PRIORITY_TASK(),来将pxCurrentTCB 设置为指向优先级最高的就绪态任务。

taskSELECT_HIGHEST_PRIORITY_TASK()如下:

#define taskSELECT_HIGHEST_PRIORITY_TASK()                                               \
	{                                                                                    \
		UBaseType_t uxTopPriority;                                                       \
                                                                                         \
		/* Find the highest priority list that contains ready tasks. */                  \
		/* 查找就绪态任务列表中最高的任务优先级 */                     \
		portGET_HIGHEST_PRIORITY(uxTopPriority, uxTopReadyPriority);                     \
		/* 此任务优先级不能是最低的任务优先级 */                        \
		configASSERT(listCURRENT_LIST_LENGTH(&(pxReadyTasksLists[uxTopPriority])) > 0);  \
		/* 让 pxCurrentTCB 指向该任务优先级就绪态任务列表中的任务 */ \
		listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &(pxReadyTasksLists[uxTopPriority]));  \
	}\

需要注意的是,FreeRTOS 提供了两种从任务优先级记录中查找优先级最高任务优先等级的方式,一种是由纯 C 代码实现的,这种方式适用于所有运行 FreeRTOS 的 MCU;另外一种方式则是使用了硬件计算前导零的指令,因此这种方式并不适用于所有运行 FreeRTOS 的 MCU,而仅适用于具有有相应硬件指令的 MCU。具体使用哪种方式,用户可以在 FreeRTOSConfig.h 文件中进行配置。

使用此方法就限制了系统最大的优先级数量不能超过 32,即最高优先等级为 31,不过对于绝大多数的应用场合,32 个任务优先级等级已经足够使用了。如果不使用次方法,理论上任务优先级没有上限。

软件方式如下:

#define taskSELECT_HIGHEST_PRIORITY_TASK()                                              \
	{                                                                                   \
		UBaseType_t uxTopPriority = uxTopReadyPriority;                                 \
                                                                                        \
		/* Find the highest priority queue that contains ready tasks. */                \
		while (listLIST_IS_EMPTY(&(pxReadyTasksLists[uxTopPriority])))                  \
		{                                                                               \
			configASSERT(uxTopPriority);                                                \
			--uxTopPriority;                                                            \
		}                                                                               \
                                                                                        \
		/* listGET_OWNER_OF_NEXT_ENTRY indexes through the list, so the tasks of        \
		the	same priority get an equal share of the processor time. */                  \
		listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &(pxReadyTasksLists[uxTopPriority])); \
		uxTopReadyPriority = uxTopPriority;                                             \
	} /* taskSELECT_HIGHEST_PRIORITY_TASK */
posted @ 2024-08-09 19:55  峣者易折  阅读(220)  评论(0编辑  收藏  举报