PendSV
Preface
最近又去鼓捣MCU和RTOS了,先把一些基础性的东西搞的清楚的更有利于理解它的运作原理,本文中的PendSV就在Cortex-M系列和RTOS中plays an important role。
PendSV Intro
The PendSV feature allows software to trigger an exception. Like IRQs, PendSV is asynchronous. An embedded OS or RTOS can use the PendSV exception to defer processing tasks instead of using an IRQ. Unlike IRQs, where the exception number assignments are device-specific, the same PendSV control code can be used by all Arm Cortex-M processors. This is because the PendSV exception is part of the architecture and present on all Cortex-M processors. PendSV therefore allows an embedded OS or RTOS to run out-of-the-box on all Cortex-M based systems without customization. The PendSV exception is not invoked by a specific instruction, but rather by privileged software setting ICSR.PENDSVSET to 1’b1.
In an RTOS environment, PendSV is usually configured to have the lowest interrupt priority level. The OS kernel code, which executes in Handler mode at high priority level is therefore able to schedule some OS operations using PendSV, to be carried out at a later time. By using PendSV, those deferred OS operations can be carried out at the lowest exception priority level when no other exception handler is running. One of these deferred OS operations is an OS context switch, which forms an essential part of a multitasking system.
上文摘自ARM官方文档,总结下来就是:
- PendSV是ARM Cortex-M架构中的一部分,适用于所有的Cortex-M的芯片
- 它是个异步exception
- 通过设置ICSR中的PENDSVSET bit来触发PendSV exception
- 在RTOS中,它通常被设为最低优先级,这样,高优先级的exception可以用它来延后处理一些事务
- Context Switch这个系统事务就是在PendSV中处理的,这在多任务系统中很重要
这样看来PendSV这个exception在Cortex-M架构中的主要设计目的就是用来做Context Switch的了。
Benefit
用PendSV在多任务系统中做Context Switch的好处是处理并发的时候更容易
众所周知,OS有很多种调度策略,最简单的比如时间片轮转,优先级等等。如果每条路径上各自实现Context Switch,一个简单的race condition如:时间片到下一个task运行,开始进行Context Switch,途中优先级较高的task运行条件成熟,也需要进行Context Switch,这样矛盾就产生了。而把Context Switch只放在PendSV里处理则简单多了,只有一条路径,每个task在其他exception中都不会被切出,也就是都会默认回到原先task,且因为PendSV被设为最低优先级,进入PendSV exception的时候没有其他的interrupt,可以专心进行Context Switch,逻辑和实现上都简单。
一些文档里还提到只使用PendSV效率更高,原因是节省了为其他exception保存现场的时间。这点博主并不这样认为,该来的exception还会来,该保存的现场还得保存,并不存在节省一说,甚至因为每个引起Context Switch的事件(比如时间片中system tick exception)都要退出后再进PendSV,还多保存了一次现场,但简单确实简单了,简单即高效。仅个人理解,有误请指正。
Flow
关于context stacking可以查阅Stack Frames,这里有更详细的解释。
Application in FreeRTOS
接下来来看下FreeRTOS里是怎么利用PendSV来做Context Switch的。
Version of FreeRTOS: V11.1.0
Portable CPU core: ARM-CM33
之前一直提到要把PendSV这个exception的priority设为最低,FreeRTOS在xPortStartScheduler函数里通过SHPR3寄存器设置priority为255。
1
2
3
4
5
/* Make PendSV and SysTick the lowest priority interrupts, and make SVCall
* the highest priority. */
portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
portNVIC_SHPR2_REG = 0;
再来看PendSV_Handler(为了简单,这里只看nonsecure和configENABLE_MPU = 0的情况)的实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void PendSV_Handler( void ) /* __attribute__ (( naked )) PRIVILEGED_FUNCTION */
{
__asm volatile
(
" .syntax unified \n"
" \n"
" mrs r0, psp \n" /* Read PSP in r0. */
" \n"
#if ( ( configENABLE_FPU == 1 ) || ( configENABLE_MVE == 1 ) )
" tst lr, #0x10 \n" /* Test Bit[4] in LR. Bit[4] of EXC_RETURN is 0 if the Extended Stack Frame is in use. */
" it eq \n"
" vstmdbeq r0!, {s16-s31} \n" /* Store the additional FP context registers which are not saved automatically. */
#endif /* configENABLE_FPU || configENABLE_MVE */
" \n"
" mrs r2, psplim \n" /* r2 = PSPLIM. */
" mov r3, lr \n" /* r3 = LR/EXC_RETURN. */
" stmdb r0!, {r2-r11} \n" /* Store on the stack - PSPLIM, LR and registers that are not automatically saved. */
" \n"
" ldr r2, =pxCurrentTCB \n" /* Read the location of pxCurrentTCB i.e. &( pxCurrentTCB ). */
" ldr r1, [r2] \n" /* Read pxCurrentTCB. */
" str r0, [r1] \n" /* Save the new top of stack in TCB. */
" \n"
" mov r0, %0 \n" /* r0 = configMAX_SYSCALL_INTERRUPT_PRIORITY */
" msr basepri, r0 \n" /* Disable interrupts upto configMAX_SYSCALL_INTERRUPT_PRIORITY. */
" dsb \n"
" isb \n"
" bl vTaskSwitchContext \n"
" mov r0, #0 \n" /* r0 = 0. */
" msr basepri, r0 \n" /* Enable interrupts. */
" \n"
" ldr r2, =pxCurrentTCB \n" /* Read the location of pxCurrentTCB i.e. &( pxCurrentTCB ). */
" ldr r1, [r2] \n" /* Read pxCurrentTCB. */
" ldr r0, [r1] \n" /* The first item in pxCurrentTCB is the task top of stack. r0 now points to the top of stack. */
" \n"
" ldmia r0!, {r2-r11} \n" /* Read from stack - r2 = PSPLIM, r3 = LR and r4-r11 restored. */
" \n"
#if ( ( configENABLE_FPU == 1 ) || ( configENABLE_MVE == 1 ) )
" tst r3, #0x10 \n" /* Test Bit[4] in LR. Bit[4] of EXC_RETURN is 0 if the Extended Stack Frame is in use. */
" it eq \n"
" vldmiaeq r0!, {s16-s31} \n" /* Restore the additional FP context registers which are not restored automatically. */
#endif /* configENABLE_FPU || configENABLE_MVE */
" \n"
" msr psplim, r2 \n" /* Restore the PSPLIM register value for the task. */
" msr psp, r0 \n" /* Remember the new top of stack for the task. */
" bx r3 \n"
::"i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY )
);
}
注释写的真好,流程基本就是Flow中第二张图里的步骤。另外configENABLE_MPU = 1的情况就是多了save和restore MPU setting的步骤,同理,有Secure的case也是多了save和restore secure context的步骤。
最后看PendSV是在哪里被引发的。不过要首先了解FreeRTOS的调度机制。
By default, FreeRTOS uses a fixed-priority preemptive scheduling policy, with round-robin time-slicing of equal priority tasks:
“Fixed priority” means the scheduler will not permanently change the priority of a task, although it may temporarily boost the priority of a task due to priority inheritance.
“Preemptive” means the scheduler always runs the highest priority RTOS task that is able to run, regardless of when a task becomes able to run. For example, if an interrupt service routine (ISR) changes the highest priority task that is able to run, the scheduler will stop the currently running lower priority task and start the higher priority task - even if that occurs within a time slice. In this case, the lower priority task is said to have been “preempted” by the higher priority task.
“Round-robin” means tasks that share a priority take turns entering the Running state.
“Time sliced” means the scheduler will switch between tasks of equal priority on each tick interrupt - the time between tick interrupts being one time slice. (The tick interrupt is the periodic interrupt used by the RTOS to measure time.)
- 首先是在SysTick_Handler里,这表明每个RTOS的tick都有可能产生一次Context Switch。
- 调用vTaskSuspend会导致参数所指task或者calling task进入suspend状态,当然vTaskResume会把参数所指task状态变回ready,当然并非马上就能run。
- vTaskPrioritySet也会引发Context Switch,前提是为某task新设的priority大于当前正在run的task的priority。
- xTaskResumeAll也可能引发Context Switch(返回true才表面发生了Context Switch)。
- 当新创建一个task并且该task的priority高于当前正在运行task的priority,也会引发PendSV。
- vTaskDelay等的函数都可能引起Context Switch。
总之Context Switch是多任务系统最重要的部分之一,在ARM Cortex-M系列芯片架构中提供了PendSV这个Exception来实现这个功能。
Reference
ARM doc - PendSV
PendSV and SVC
Benefits of PendSV
PendSV in FreeRTOS
Example: Context Switch
PendSV is safe to do CS
Task Scheduling of FreeRTOS