FreeRtos源码分析之任务切换原理(四)

一、CortexM3中断优先级

CortexM3支持多达240个外部中断和16个内部中断,每一个中断都对应一个中断都对应一个优先级寄存器。每一个优先级寄存器占用8位,STM32采用其中的高四位来表示优先级,低四位不可用。
在这里插入图片描述
在这里插入图片描述

FreeRtos一共会使用到三种中断:SysTick、SVC、PendSV。

  • SVC在启动任务调度的时候使用;
  • SysTIck定时器用于周期性的中断,为系统提供心跳;
  • PendSV用于任务切换;

对于实时操作系统而言,我们一般外部中断优先得到响应,所以SysTick和PendSV的优先级通常设置为最低。

    /* 使 PendSV and SysTick 的优先级最低. */
    portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

portNVIC_PENDSV_PRI表示(0xf<<4)<<16,portNVIC_SYSTICK_PRI表示(0xf<<4)<<24,刚好将PendSV和SysTick优先级寄存器的最高4位全部置一(CortexM3的优先级寄存器值越大优先级越低)。

二、PendSV

SVC(系统服务调用,亦简称系统调用)和 PendSV(可悬起系统调用),它们多用于在操作系统之上的软件开发中。 SVC产生的中断必须立即得到响应,否则将触发硬Fault。PendSV是可悬挂的系统调用,如果有更高优先级的中断产生,PendSV中断会挂起,直到更高优先级的中断处理完成。
悬起 PendSV 的方法是: 手工往 NVIC 的 PendSV 悬起寄存器中写 1。

假设某个OS系统中存在比SysTick优先级更低的中断,那么当低优先级的IRQ在执行时会被SysTick打断,并在SyStick中断中执行上下文切换。由于执行上下文切换的时间在真实系统中所需要的时间是不可知的,所以低优先级的中断将会被延时执行。这种行为在任何一种实时操作系统中都是不能容忍的,在CortexM3中如果 OS 在某中断活跃时尝试切入线程模式,将触发fault 异常。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wr3mswWP-1611658781428)(en-resource://database/616:1)]
为解决此问题,早期的 OS 大多会检测当前是否有中断在活跃中,只有没有任何中断需要响应时,才执行上下文切换(切换期间无法响应中断)。然而,这种方法的弊端在于,它可以把任务切换动作拖延很久(因为如果抢占了 IRQ,则本次 SysTick 在执行后不得作上下文切换,只能等待下一次 SysTick 异常),尤其是当某中断源的频率和 SysTick 异常的频率比较接近时,会发生“共振”。
现在好了, PendSV 来完美解决这个问题了。PendSV 异常会自动延迟上下文切换的请求,直到其它的 ISR 都完成了处理后才放行。为实现这个机制,需要把 PendSV 编程为最低优先级的异常。在FreeRtos中,每一次进入SysTick中断,系统都会检测是否有新的进入就绪态的任务需要运行,如果有,则悬挂PendSV异常, 以便缓期执行上下文切换。如图 7.17 所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nrHpZdKc-1611658781431)(en-resource://database/618:1)]

关于任务切换的内容,强烈建议参考《CortexM3权威指南》第9章内容。

三、FreeRtos任务切换的两种场景

FreeRtos任务切换有两种场景:

  1. 在SysTick定时器中监测是否有新的就绪态任务需要运行,如果有则进行任务切换;
void xPortSysTickHandler( void )
{
    /* The SysTick runs at the lowest interrupt priority, so when this interrupt
     * executes all interrupts must be unmasked.  There is therefore no need to
     * save and then restore the interrupt mask value as its value is already
     * known. */
    portDISABLE_INTERRUPTS();
    {
        /* Increment the RTOS tick. */
        if( xTaskIncrementTick() != pdFALSE )
        {
            /* A context switch is required.  Context switching is performed in
             * the PendSV interrupt.  Pend the PendSV interrupt. */
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
    }
    portENABLE_INTERRUPTS();
}

从上面的代码中可以看出,FreeRtos在进入SysTick中断后会屏蔽所有的其它中断,如果我们不使用用PendSV而在SysTick中断中来进行任务切换,那么SysTick中断会占用无法预知的时间,即使其它中断的优先级高于Systick,也依然要等到SysTick中断执行结束,从而导致系统发生不可预知的异常。
任务的被动切换很依赖于每一个任务中调用的系统延时或者阻塞,如果某个较高优先级的任务一直不停的运行,比如你在最高优先级的任务中写入了如下代码:

while(1);

那么你的系统有可能就会一直卡死在里面,或者不停的触发看门狗。
2. 用户主动调用portYIELD函数进行任务切换

/* Scheduler utilities. */
    #define portYIELD()                                 \
    {                                                   \
        /* Set a PendSV to request a context switch. */ \
        portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
        __DSB();                                        \
        __ISB();                                        \
    }

从代码中可以看出,这两种任务切换方式的原理一样,都是向PendSV中断寄存器写1,触发一次PendSV中断。接下来我们看下PendSV中断函数:

xPortPendSVHandler:
	mrs r0, psp
	isb
	/* Get the location of the current TCB. */
	ldr	r3, =pxCurrentTCB
	ldr	r2, [r3]

	/* Is the task using the FPU context?  If so, push high vfp registers. */
	tst r14, #0x10
	it eq
	vstmdbeq r0!, {s16-s31}

	/* Save the core registers. */
	stmdb r0!, {r4-r11, r14}

	/* Save the new top of stack into the first member of the TCB. */
	str r0, [r2]

	stmdb sp!, {r0, r3}
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0
	dsb
	isb
	bl vTaskSwitchContext
	mov r0, #0
	msr basepri, r0
	ldmia sp!, {r0, r3}

	/* The first item in pxCurrentTCB is the task top of stack. */
	ldr r1, [r3]
	ldr r0, [r1]

	/* Pop the core registers. */
	ldmia r0!, {r4-r11, r14}

	/* Is the task using the FPU context?  If so, pop the high vfp registers
	too. */
	tst r14, #0x10
	it eq
	vldmiaeq r0!, {s16-s31}

	msr psp, r0
	isb
	#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
		#if WORKAROUND_PMU_CM001 == 1
			push { r14 }
			pop { pc }
		#endif
	#endif

	bx r14

博主汇编知识有限,所以就不一一介绍每句代码的含义了,感兴趣的同学可以自行百度。这里只介绍vTaskSwitchContext函数。vTaskSwitchContext的核心任务是找到当前处于就绪态的最高优先级的任务,代码如下:

if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
    {
        /* 如果当前任务调度器处于挂起状态- 不允许任务切换 */
        xYieldPending = pdTRUE;
    }
    else
    {
        xYieldPending = pdFALSE;
        traceTASK_SWITCHED_OUT();

        //此处省略部分代码

        /* 使用c或者汇编代码选择最高优先级的就绪态任务作为下一个将要运行的任务*/
        taskSELECT_HIGHEST_PRIORITY_TASK(); 

    }

接下来我们看下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 ); \
        listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );   \
    } /* taskSELECT_HIGHEST_PRIORITY_TASK() */

首先我们看下portGET_HIGHEST_PRIORITY函数,这个函数的作用是返回系统中最高有优先级任务的优先级值,其源码为:

        #define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities )    uxTopPriority = ( 31UL - ( ( uint32_t ) __CLZ( ( uxReadyPriorities ) ) ) )

__CLZ是一条汇编指令,用于计算最高符号位与第一个1之间的0的个数。比如( ( uint32_t ) __CLZ( ( 0x00FFFFF0 ) ) )得到的值为8。
由于相同优先级的任务可能会存在多个,所以接下来便需要从就绪任务列表中找到位于链表最前面的优先级,将其赋值给pxCurrentTCB。至此,vTaskSwitchContext函数分析完毕。

FreeRtos带注释源码Gitee地址:https://gitee.com/zBlackShadow/FreeRtos10.4.3.git

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值