FreeRTOS任务调度最后篇

“FreeRTOS开启任务调度”一篇说到启动任务调度最后启动Systick定时器,通过SVC中断引导第一个任务执行。然后系统就在Systick的定时中断下调度任务执行,这次介绍最后的部分,Systick和PendSV。

SysTick时钟是STM32的一个定时器,使能之后设置中断频率就会按频率触发SysTick中断。这样就可以用SysTick中断给OS切换任务提供时机。因为硬件是中断来了就会把CPU当前任务压栈,会执行中断处理函数,中断处理完成后会出栈之前数据。继续执行之前被中断的任务。在没OS情况下都是在MSP执行。既然需要OS调度,那么就要使能control寄存器,让CPU在非中断时候使用PSP栈指针。在中断MSP逻辑控制执行的PSP。所以OS调度离不开中断,同时用到双堆栈MSP和PSP隔离任务和OS。即内核空间和用户空间,OS调度在中断执行,运行在内核空间。任务在非中断执行,运行在用户空间,任务通过SVC中断调用内核。

如果SysTick不设置最低优先级,在SysTick里切换任务还会导致硬件异常。
因为在Cortex-M3中,如果OS在某个中断活跃时,抢占了该中断,而且又发生了任务调度,并执行了任务,切换到了线程运行模式,将直接触发Fault异常。(所以把SysTick和PendSV中断优先级设置最低也是有原因的,就是为了OS调度不抢占其他中断导致Fault的问题)

按理SysTick设置最低优先级就可以切换任务上下文了。一般OS在调度任务时,会关闭中断,也就是进入临界区,而OS任务调度是要耗时的,这就会出现一种情况:在任务调度期间,如果新的外部IRQ发生,CPU将不能够快速响应处理。比如串口在发数据,在OS进入临界区屏蔽中断执行任务切换期间就可能丢失读取串口发来的数据。(这是在SysTick执行任务切换带来的降低其他中断处理速度问题)

所以为了解决SysTick切换任务导致IRQ处理速度降低问题,就利用PendSV【缓期执行】的特点。滴答定时器中断,只做业务调度前的判断工作,不做任务切换。触发PendSV,PendSV并不会立即执行,因为PendSV的优先级最低,如果此时正好有IRQ请求,那么先响应IRQ,最后等到所有优先级高于PendSV的IRQ都执行完毕,再执行PendSV,进行任务调度(这样就解决了IRQ性能降低的问题)

这样也不是没缺点的,因为SysTick设置最低,如果其他中断特别频繁,就会导致SysTick执行频繁挂起,导致时钟不准,任务调度慢。因为SysTick自身执行机会被抢的很少,但是这些问题影响面已经很小了。

那如果把SysTick优先级设置最高,PendSV优先级设置最低是不是可以完美解决呢。因为SysTick的优先级最高,而且又是周期性的触发,会导致经常抢占外部IRQ,这就会导致外部IRQ响应变慢,这在一些对实时性要求高的,比如按键、断电中断等待,是不能接受的,你肯定不希望你的按键扫描体验卡顿。(SysTick又频繁抢占其他IRQ)

所以没有完美的方法,一般把SysTick和PendSV设置最低满足大部分情况,毕竟外部IRQ那么多的情况不多,一般不会影响SysTick的周期性。如果外部IRQ很多,那么可以考虑提高SysTick优先级,PendSV一直设置最低,来适当解决OS调度SysTick被抢占太厉害的问题。

设置SysTick和PendSV中断的资料来源如下图:
FreeRTOS任务调度最后篇_第1张图片

//启动调度器
BaseType_t xPortStartScheduler(void)
{
	//使PendSV成为最低优先级的中断。11111111<<16为00000000111111110000000000000000
	portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
	//使SysTick成为最低先级的中断。11111111<<24为11111111000000000000000000000000
	portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

	//启动滴答计时器,设置频率。中断在这里被禁用
	vPortSetupTimerInterrupt();

	//初始化为第一个任务准备的关键嵌套计数。
	uxCriticalNesting = 0;

	//开启第一个任务
	prvStartFirstTask();

	//正常执行不到这里
	return 0;
}

要点:
1.SysTick和PendSV一般设置最低优先级,来保证及时处理外部中断,因为是嵌入式实时系统对不。
2.SysTick可以按实际适当提高优先级,比重要中断低,比不重要中断高。PendSV还是以最低优先级执行。
3.SysTick在最低优先级是能切换任务的,只是会多执行切换逻辑,导致IRQ处理性能降低。引入PendSV不是必须的,主要利用PendSV缓期执行的特点提高IRQ性能。

SysTick逻辑如下,就是判断是否要切换任务,要切换的话就触发PendSV完事

//滴答时钟中断。内核通过滴答中断判断是否需要切换上下文
//如果需要切换上下文则设置PendSV寄存器触发PendSV中断进行上下文切换
void xPortSysTickHandler(void)
{
	/*SysTick 以最低的中断优先级运行,所以当这个中断
	* 执行所有中断必须被取消屏蔽。 因此没有必要
	* 保存然后恢复中断屏蔽值,因为它的值已经是
	* 已知 - 因此使用稍快的 vPortRaiseBASEPRI() 函数
	* 代替 portSET_INTERRUPT_MASK_FROM_ISR()。*/
	//屏蔽比OS级别低的中断和刷新指令和数据流水线
	vPortRaiseBASEPRI();
	{
		//增加OS的节拍数。返回是否需要切换上下文
		if (xTaskIncrementTick() != pdFALSE)
		{
			//需要上下文切换。产生PendSV中断 。
			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
		}
	}
	//取消屏蔽中断
	vPortClearBASEPRIFromISR();
}

PendSV逻辑如下,就是压栈上下文,切换pxCurrentTCB后恢复新任务上下文,然后退出中断到PSP执行新任务。

/*PendSV是可悬起异常,如果我们把它配置最低优先级,那么如果同时有多个异常被触发,它会在其他异常执行完毕后再执行,
而且任何异常都可以中断它。更详细的内容在《Cortex-M3 权威指南》里有介绍,下面我摘抄了一段。
OS 可以利用它“缓期执行”一个异常——直到其它重要的任务完成后才执行动 作。悬起 PendSV 的方法是:
手工往NVIC的PendSV悬起寄存器中写 1。悬起后,如果优先级不够 高,则将缓期等待执行。
PendSV的典型使用场合是在上下文切换时(在不同任务之间切换)。例如,一个系统中有两个就绪的任务,上下文切换被触发的场合可以是:
1、执行一个系统调用
2、系统滴答定时器(SYSTICK)中断,(轮转调度中需要)
让我们举个简单的例子来辅助理解。假设有这么一个系统,里面有两个就绪的任务,并且通过SysTick异常启动上下文切换。
但若在产生 SysTick 异常时正在响应一个中断,则 SysTick异常会抢占其 ISR。在这种情况下,OS是不能执行上下文切换的,
否则将使中断请求被延迟,而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能容忍这 种事。
因此,在 CM3 中也是严禁没商量——如果 OS 在某中断活跃时尝试切入线程模式,将触犯用法fault异常。
为解决此问题,早期的 OS 大多会检测当前是否有中断在活跃中,只有在无任何中断需要响应 时,才执行上下文切换(切换期间无法响应中断)。
然而,这种方法的弊端在于,它可以把任务切 换动作拖延很久(因为如果抢占了 IRQ,则本次 SysTick在执行后不得作上下文切换,
只能等待下 一次SysTick异常),尤其是当某中断源的频率和SysTick异常的频率比较接近时,会发生“共振”, 使上下文切换迟迟不能进行。
现在好了,PendSV来完美解决这个问题了。PendSV异常会自动延迟上下文切换的请求,直到 其它的 ISR都完成了处理后才放行。
为实现这个机制,需要把 PendSV编程为最低优先级的异常。如果 OS检测到某 IRQ正在活动并且被 SysTick抢占,
它将悬起一个 PendSV异常,以便缓期执行 上下文切换。
1.关闭中断
2.保存上文
3.加载下文
4.打开中断
*/
//进入该中断后讲使用MSP栈指针,PSP不会被盖。这时候Context内核已经自动压栈了xPSR,R15(PC),R14(LR),R12(SP),R3,R2,R1,R0到用户栈空间
__asm void xPortPendSVHandler(void)
{
	extern uxCriticalNesting;
	//当前运行的任务
	extern pxCurrentTCB;
	//任务切换上下文
	extern vTaskSwitchContext;

	//指定当前文件的堆栈按照 8 字节对齐
	PRESERVE8
		//R0=PSP  Process Stack Pointer (PSP)
		//取出用户栈指针到r0.r0存当前用户栈地址
		mrs r0, psp
		//指令同步屏障,清除指令流水线
		isb
		//LDR指令用于从存储器中将一个32位的字数据传送到目的寄存器中。该指令通常用亍从存储器中读取32位的字数据到通用寄存器,然后对数据迕行处理
	  //获取pxCurrentTCB变量的地址到r3(这时候pxCurrentTCB变量存当前运行任务的tcb首地址)
		ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */
		//把pxCurrentTCB变量指向的tcb地址读入r2
		ldr r2, [r3]
		//stmdb将寄存器压栈,保存剩余的寄存器。叹号标识自动调整地址
		//r0里面地址从高到低自动调节,依次把r4到r11寄存器的内容储存r0的任务栈里面
	  //把r4到r11寄存器值压入当前用户的栈(PSP指向的栈)
		stmdb r0 !, { r4 - r11 } /* Save the remaining registers. */
		//STR{条件} 源寄存器,<存储器地址>
		//STR指令用亍从源寄存器中将一个32位的字数据传送到存储器中。该指令在程序设计中比较常用
		//将新的栈顶保存到TCB的第一个成员中。
		//把r0存的用户栈地址存入r2指向的地址里,即pxCurrentTCB的开始位置,也就是第一个成员里
		str r0, [r2] /* Save the new top of stack into the first member of the TCB. */
		//stmdb将寄存器压栈,入栈保存R3(即&pxCurrentTCB)和 R14(链接寄存器)。叹号标识自动调整地址
		//sp里面地址从高到低自动调节,依次把r3和r14寄存器的内容储存sp的任务栈里面
		//把r3(现在存的pxCurrentTCB变量地址)和r14(链接寄存器)值压入主栈
		stmdb sp !, { r3, r14 }
		//读入最大优先级进入r0
		mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
		//设置屏蔽中断寄存器basepri为OS所能调用管理的最高优先级
		//这里中断优先级0-255,越大优先级越低。即屏蔽所有比设置的OS所能管理的最大优先级低的中断
		msr basepri, r0
		//数据同步令牌,清除数据流水线
		dsb
		//指令同步屏障,清除指令流水线
		isb
		//进行任务切换,在临界段切换就绪队列中优先级最高的任务,更新pxCurrentTCB
		//跳转到vTaskSwitchContext执行需要返回
		bl vTaskSwitchContext
		//把0读入r0
		mov r0, #0
		//设置屏蔽中断寄存器basepri为0,开启中断
		msr basepri, r0
		//从主堆栈中恢复寄存器R3和R14的值,此时SP使用的是MSP。叹号标识自动调整地址
		//sp里面地址由低到高自动调节,依次把sp的任务栈弹出到r3到r14寄存器
		//从主栈里恢复r3(r3存的是pxCurrentTCB变量的地址)和r14(链接寄存器,也就是做切换逻辑前的链接)
		//和stmdb sp !, { r3, r14 }是对应的,切换任务前压入主栈了,切换完成后从主栈弹出
		ldmia sp !, { r3, r14 }
		//r3里面是pxCurrentTCB变量指向的地址。把该地址存的当前tcb地址读入r1(由于切换了任务pxCurrentTCB变量存了新任务的tcb首地址)
		ldr r1, [r3]
		//pxCurrentTCB中的第一项是栈顶任务,读出PCB存的栈顶位置到r0(pxCurrentTCB这时候已经存的新任务的tcb,这样就得到新任务的栈顶)
		//和str r0, [ r2 ]是对应的,str r0, [ r2 ]存PSP到tcb第一个元素的栈顶指针变量里
		ldr r0, [r1] /* The first item in pxCurrentTCB is the task top of stack. */
		//弹出寄存器和关键嵌套计数
		//r0里面地址由低到高自动调节,依次把r0的任务栈弹出到r4到r11寄存器。叹号标识自动调整地址
		//从r0指向的栈依次弹出r4-r11(这时候r0存的新任务的栈顶,因此是弹出新要执行的任务的寄存器值)
		ldmia r0 !, { r4 - r11 } /* Pop the registers and the critical nesting count. */
		//PSP=R0,更新PSP使异常退出时PSP为基地址进行其他寄存器的自动出栈
		//把新要执行任务栈顶地址写入用户栈顶指针寄存器
		msr psp, r0
		//指令同步屏障,清除指令流水线
		isb
		//系统以PSP作为SP指针出栈,把新任务的任务堆栈中剩下的内容加载到CPU寄存器:
		//R0(任务形参)、R1、R2、R3、R12、R14(LR)、R15(PC)和xPSR,切换到新任务
		//调转到r14的指令不用返回
		bx r14
		//空指令,占用一个时钟周期
		nop
}

这就是我对SysTick和PendSV的理解。调度策略也是看系统定位是看重实时性还是均衡调度。及按照IRQ的数量和不同中断类型重要决定SysTick优先级,如果不能评估,那么最低优先级本来就是不错的选择。

所以方案没有完美的方案,就看关注什么了而已。注重OS调度性能的话那么就会占用其他IRQ的处理时间,可能导致重要中断超时(比如导弹飞过头了)。重视IRQ处理那么可能会因为频繁的IRQ导致OS调度很缓慢。适合自己的就是最好的。

你可能感兴趣的