一、CortexM3中断优先级
CortexM3支持多达240个外部中断和16个内部中断,每一个中断都对应一个中断都对应一个优先级寄存器。每一个优先级寄存器占用8位,STM32采用其中的高四位来表示优先级,低四位不可用。
FreeRtos一共会使用到三种中断:SysTick、SVC、PendSV。
- SVC在启动任务调度的时候使用;
- SysTIck定时器用于周期性的中断,为系统提供心跳;
- PendSV用于任务切换;
对于实时操作系统而言,我们一般外部中断优先得到响应,所以SysTick和PendSV的优先级通常设置为最低。
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 异常。
为解决此问题,早期的 OS 大多会检测当前是否有中断在活跃中,只有没有任何中断需要响应时,才执行上下文切换(切换期间无法响应中断)。然而,这种方法的弊端在于,它可以把任务切换动作拖延很久(因为如果抢占了 IRQ,则本次 SysTick 在执行后不得作上下文切换,只能等待下一次 SysTick 异常),尤其是当某中断源的频率和 SysTick 异常的频率比较接近时,会发生“共振”。
现在好了, PendSV 来完美解决这个问题了。PendSV 异常会自动延迟上下文切换的请求,直到其它的 ISR 都完成了处理后才放行。为实现这个机制,需要把 PendSV 编程为最低优先级的异常。在FreeRtos中,每一次进入SysTick中断,系统都会检测是否有新的进入就绪态的任务需要运行,如果有,则悬挂PendSV异常, 以便缓期执行上下文切换。如图 7.17 所示
关于任务切换的内容,强烈建议参考《CortexM3权威指南》第9章内容。
三、FreeRtos任务切换的两种场景
FreeRtos任务切换有两种场景:
- 在SysTick定时器中监测是否有新的就绪态任务需要运行,如果有则进行任务切换;
void xPortSysTickHandler( void )
{
portDISABLE_INTERRUPTS();
{
if( xTaskIncrementTick() != pdFALSE )
{
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
portENABLE_INTERRUPTS();
}
从上面的代码中可以看出,FreeRtos在进入SysTick中断后会屏蔽所有的其它中断,如果我们不使用用PendSV而在SysTick中断中来进行任务切换,那么SysTick中断会占用无法预知的时间,即使其它中断的优先级高于Systick,也依然要等到SysTick中断执行结束,从而导致系统发生不可预知的异常。
任务的被动切换很依赖于每一个任务中调用的系统延时或者阻塞,如果某个较高优先级的任务一直不停的运行,比如你在最高优先级的任务中写入了如下代码:
while(1);
那么你的系统有可能就会一直卡死在里面,或者不停的触发看门狗。
2. 用户主动调用portYIELD函数进行任务切换
#define portYIELD() \
{ \
\
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
__DSB(); \
__ISB(); \
}
从代码中可以看出,这两种任务切换方式的原理一样,都是向PendSV中断寄存器写1,触发一次PendSV中断。接下来我们看下PendSV中断函数:
xPortPendSVHandler:
mrs r0, psp
isb
ldr r3, =pxCurrentTCB
ldr r2, [r3]
tst r14, #0x10
it eq
vstmdbeq r0!, {s16-s31}
stmdb r0!, {r4-r11, r14}
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}
ldr r1, [r3]
ldr r0, [r1]
ldmia r0!, {r4-r11, r14}
tst r14, #0x10
it eq
vldmiaeq r0!, {s16-s31}
msr psp, r0
isb
#ifdef WORKAROUND_PMU_CM001
#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();
taskSELECT_HIGHEST_PRIORITY_TASK();
}
接下来我们看下taskSELECT_HIGHEST_PRIORITY_TASK函数是如何寻找最高优先级的任务的。
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority; \
\
\
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \
configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
}
首先我们看下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
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)