一,前言
复习完FreeRTOS的任务切换汇编,来分析下NuttxOS的任务切换汇编设计思路。这里我重点分析的不是任务调度算法哦。今天分析的是第一次任务切换,先走一个温故而知新的路线。
二,回顾
我先简单回顾下FreeRTOS中基于cortexM3/M4上下文切换的原理。
-
进入中断:
上一个任务中xPSR, PC, R14, R12, R3-R0这些寄存器的值会自动存储到任务的栈中,同时PSP 会自动更新(在更新之前 PSP 指向任务栈的栈顶)如下图
这个入栈指将内核寄存器的值进行保存,保存到PSP或MSP地址(向低地址方向push入)中的内容更新。
image.png
然后程序员要在中断函数中手工添加R4-R11的入栈,PSP栈地址变的更小。根据对栈的posh和pop动作,栈顶地址会经常变动,但是栈底地址是不变。
-
退出中断:
在退出中断前从PSP或MSP【通过lr寄存器的最后2个bit来选择】的栈顶地址开始自动pop到实际的寄存器。可以理解为通过pop来恢复现场。就是把栈地址中的地址恢复到当前内核寄存器中。
除了control特殊寄存器可以控制模式切换。中断中R14(lr)return value也可以。需要使用命令bx lr,不是bl reg。
image.png - 总结:
关于任务上下文切换的设计思想,就是选用中断的方式进行栈替换,并且设置出栈内容和入栈内容不同,来进行任务切换。
A . 正常的外设中断,不进行上下文任务切换,先保护现场到栈,进入中断执行,然后通过栈恢复现场。
B. 那么中断要用于切换任务,就是要切换栈。而切换栈的方式就是找到栈地址,然后操作栈地址中的内容。可以理解为入栈保护是有的,但是出栈的时候栈顶地址替换了,所以切换到另外一个函数了,这就是实现上下文切换的原理。
三,nuttx 10.01的任务上下文切换源码分析
在我的stm32F407开发板上,编译通过后烧写代码,用arm官网的ozone进行调试并截图的,因为之前搭建的codeblocks交叉编译调试环境感觉速度慢,而且寄存器无法查看。代码从__start开始,一路进入up_unblock_task函数。
1)此函数中最后调用
arm_switchcontext(rtcb->xcp.regs, nexttcb->xcp.regs);
进入汇编的上下文切换代码。当调用函数后,发现MSP栈地址已经发生了变化。此时MSP=0x20000EA8。image.png
rtcb->xcp.regs的地址传入r0寄存器exttcb->xcp.regs传入r1寄存器。其实rtcb->xcp.regs的地址是用来保存的,nexttcb->xcp.regs地址是用来切换的。
#define SYS_switch_context (2)
这个将来switch case中要用到的。
arm_switchcontext:
/* Perform the System call with R0=1, R1=saveregs, R2=restoreregs */
1. mov r2, r1 /* R2: restoreregs */
2. mov r1, r0 /* R1: saveregs */
3. mov r0, #SYS_switch_context /* R0: context switch */
4. svc 0 /* Force synchronous SVCall (or Hard Fault) */
/* We will get here only after the rerturn from the context switch */
5. bx lr
6. .size arm_switchcontext, .-arm_switchcontext
7. .end
这段切换代码是将传入参数r0移动到r1,r1移动到r2,然后r0赋值为2,调用svc中断,这次并不会返回哦。
2)走到SVC 0则进入了exception_common,其文件路径在arch\arm\src\armv7-m\gnu\arm_exception.S
我们先来看看为什么svc 0会进入exception_common。原因就在中断向量表将中断号2及之后的都设置了入口函数为exception_common。在arch\arm\src\armv7-m\arm_vectors.c中
1. unsigned _vectors[] __attribute__((section(".vectors"))) =
2. {
3. /* Initial stack */
4. IDLE_STACK,
5. /* Reset exception handler */
6. (unsigned)&__start,
7. /* Vectors 2 - n point directly at the generic handler */
8. [2 ... (15 + ARMV7M_PERIPHERAL_INTERRUPTS)] = (unsigned)&exception_common
9. };
3)进入exception_common函数后。此时MSP已经自动入栈了8个字节,所以栈顶地址变小。可以看此函数的注释,此时MSP=0x20000E88。此时已经将arm_switchcontext中设置的r0,r1,r2的值都自动入栈了,所以地址从0x20000EA8变成了0x20000E88。
/* Common exception handling logic. On entry here, the return stack is on either
* the PSP or the MSP and looks like the following:
*
* REG_XPSR
* REG_R15
* REG_R14
* REG_R12
* REG_R3
* REG_R2
* REG_R1
* MSP->REG_R0
*
* And
* IPSR contains the IRQ number
* R14 Contains the EXC_RETURN value
* We are in handler mode and the current SP is the MSP
*/
栈中push的内容如下
image.png
4)exception_common函数中的内容分析
mrs r0, ipsr先保存中断号到R0中。
5)tst r14, #EXC_RETURN_PROCESS_STACK的目的就是验证进入中断前的栈是保存在MSP还是PSP中的,因为进入中断后的SP值用的是MSP的值。所以若之前是PSP则要将PSP的值保存到SP中。此时r14的是值是0xfffffff9,tst测试bit2是否为0,由于bit2为0,所以跳入1f标识中执行。
tst r14, #EXC_RETURN_PROCESS_STACK /* nonzero if context on process stack */
beq 1f /* Branch if context already on the MSP */
mrs r1, psp /* R1=The process stack pointer (PSP) */
mov sp, r1 /* Set the MSP to the PSP *
6)接着1f标识中将sp+32字节的地址保存到r2,就是这个sp的栈底地址保存到r2。
mov r2, sp /* R2=Copy of the main/process stack pointer */
add r2, #HW_XCPT_SIZE /* R2=MSP/PSP before the interrupt was taken */
把中断屏蔽寄存器保存到r3。mrs r3, primask /* R3=Current PRIMASK setting */
7)stmdb sp!, {r2-r11,r14}
一开始进入中断函数有8个字节自动入栈,sp地址自动加32,现在继续入栈sp继续增加,这里和FreeRTOS明显不同,它由程序员手工入栈的寄存器不仅R4-R11,还多了r2和r3及r14。而r2保存的是进入中断前的MSP地址,r3保存的是中断屏蔽状态。R14保存的是进入svc中断前用的是MSP还是PSP。此时MSP=0x20000E88-11*4=0x20000E5C。
8)把sp保存到r1,此时的sp已经完成push动作,所以是栈顶地址。
mov r1, sp
同时再把sp保存到r4,后3个bit清0,变成8-byte alignment后保存到sp。等于上图划线处是push进去的值,然后0x2000E58没有push值,是一个temp值,把它当做栈顶。等于多一个temp值。
mov r4, sp
bic r2, r4, #7
mov sp, r2
9)调用arm_doirq函数前截图,参考r0和r1参数值,reg参数是0x20000E5C。
bl arm_doirq
进入c函数。没什么特别的,一路进入arm_hardfault。此时传入的参数中context就是0x20000E5C。image.png
image.png
arm_hardfault中从reg参数0x20000E5C获取PC的值-1的地址为,为REG_PC为栈顶地址+(11+6)4地址中的内容再-1(uint16),最后
uint16_t *pc = (uint16_t *)regs[REG_PC] - 1;
就是0x08004290-2=0x0800428E。
#define REG_R0 (SW_XCPT_REGS + 0) /* R0 */
#define REG_R1 (SW_XCPT_REGS + 1) /* R1 */
#define REG_R2 (SW_XCPT_REGS + 2) /* R2 */
#define REG_R3 (SW_XCPT_REGS + 3) /* R3 */
#define REG_R12 (SW_XCPT_REGS + 4) /* R12 */
#define REG_R14 (SW_XCPT_REGS + 5) /* R14 = LR */
#define REG_R15 (SW_XCPT_REGS + 6) /* R15 = PC */
#define REG_XPSR (SW_XCPT_REGS + 7) /* xPSR */
#define HW_INT_REGS (8)
然后#define INSN_SVC0 0xdf00
uint16_t insn = *pc;这块svc0触发时候为什么地址中的内容是0xdf00,不太清楚,反正取这个PC值的目的就是做判断,是否svc0触发的中断,是的话就调用arm_svcall
if (insn == INSN_SVC0)
{
return arm_svcall(irq, context, arg);
}
重点来了,还记得一开始调用arm_switchcontext时候修改的r0,r1,r2寄存器么,它是被调用了svc 0后自动入栈的。此时就通过0x20000E5C栈顶地址将其通过regs[REG_R0]方式把值拿出来用。
下图的case就是说判断上下文切换,若r1(rtcb->xcp.regs)和r2(nexttcb->xcp.regs)栈地址不同,则将当前已经入栈的数据,都保存到r1指向的地址中进行入栈。因为(uint32_t *)regs[REG_R1]
代表取regs[REG_R1]为地址中的内容。这样理解的话0x20000E5C就是一个临时栈。0x20000324(rtcb->xcp.regs)才是正主。
执行
memcpy((uint32_t *)regs[REG_R1], regs, XCPTCONTEXT_SIZE);
后就是把regs[REG_R1]地址中的内容0x20000324作为dest地址。将regs地址0x20000E5C中的17*4的寄存器内容全部copy到0x20000324地址中,此地址就是rtcb->xcp.regs传入的值,这样就模拟了现场保护。image.png
接着执行
CURRENT_REGS = (uint32_t *)regs[REG_R2];
就是把regs[REG_R2]中的值作为地址,这个值是0x10000420,其实就是exttcb->xcp.regs传入的值,此时CURRENT_REGS指向了0x10000420待切换地址。
image.png
然后就是一路返回到arm_doirq函数中,将0x10000420赋值给reg最后进行返回,同时CURRENT_REGS清0。
regs = (uint32_t *)CURRENT_REGS;
CURRENT_REGS = savestate;
#endif
board_autoled_off(LED_INIRQ);
return regs;
小插曲,说说r4的值,因为退出arm_doirq函数后还会用到。
Arm_svcall的switch(cmd)命令转换为汇编会把R1移动到R4。而R1的地址是0x20000E5C当初进入arm_doirq函数时候传递的参数。至于为什么移动到r4,我理解有3个参数,已经占用了3个寄存器,所以对于函数内的临时变量,就放入r4了。这是我猜的,不知道为什么一定是r4。
image.png
退出arm_doirq函数后,寄存器的状态如下。
image.png
10)把r4保存到r1,r1是主栈地址,然后r0是待切换的栈,检查是否地址相同,相同则不要切换上下文跳入2, 不同则继续执行汇编,进行上下文切换。此时r0=0x10000420,r1=0x20000E5C,值不同。
mov r1, r4
cmp r0, r1 /* Context switch? */
beq 2f /* Branch if no context switch */
11)当前栈r1要切换到栈r0,就是用r0来替换r1进行中断返回。r0加11*4的地址保存到r1。等于r1变成了要切换的栈的栈顶地址。
add r1, r0, #SW_XCPT_SIZE
至于为什么不是8,而是11,这要看r0栈地址0x10000420内容中的含义了。这个地址中的值中的内容是在创建tcb任务的时候设置的。memset(xcp, 0, sizeof(struct xcptcontext));代表一共17个寄存器。这个结构体中uint32_t regs[XCPTCONTEXT_REGS];
XCPTCONTEXT_REGS为17,分别为11一个软件工程师保存+8个硬件自动保存。#define XCPTCONTEXT_REGS (HW_XCPT_REGS + SW_XCPT_REGS)
来分析下nxthread_setup_scheduler->up_initial_state中就是为其内容赋值。先设置都为0,然后设置4个值sp,pc,xpsr,lr。
void up_initial_state(struct tcb_s *tcb)
{
struct xcptcontext *xcp = &tcb->xcp;
/* Initialize the idle thread stack */
if (tcb->pid == 0)
{
up_use_stack(tcb, (void *)(g_idle_topstack -
CONFIG_IDLETHREAD_STACKSIZE), CONFIG_IDLETHREAD_STACKSIZE);
}
/* Initialize the initial exception register context structure */
memset(xcp, 0, sizeof(struct xcptcontext));
/* Save the initial stack pointer */
xcp->regs[REG_SP] = (uint32_t)tcb->adj_stack_ptr;
/* Save the task entry point (stripping off the thumb bit) */
xcp->regs[REG_PC] = (uint32_t)tcb->start & ~1;
/* Specify thumb mode */
xcp->regs[REG_XPSR] = ARMV7M_XPSR_T;
xcp->regs[REG_EXC_RETURN] = EXC_RETURN_PRIVTHR;
所以这里地址先加11,等于跳过了软件的11个寄存器内容。
#define REG_R13 (0) /* R13 = SP at time of interrupt */
#define REG_BASEPRI (1) /* BASEPRI */
#define REG_R4 (2) /* R4 */
#define REG_R5 (3) /* R5 */
#define REG_R6 (4) /* R6 */
#define REG_R7 (5) /* R7 */
#define REG_R8 (6) /* R8 */
#define REG_R9 (7) /* R9 */
#define REG_R10 (8) /* R10 */
#define REG_R11 (9) /* R11 */
#define REG_EXC_RETURN (10) /* EXC_RETURN */
地址增加为0x1000044C后截图如下,剩下的就是硬件的8个。
ldmia r1!, {r4-r11}从0x1000044C地址开始pop到R4-R11。等于把之前初始化硬件的8个寄存器先pop出到r4-r11。
image.png
12)接着2句就是把r0栈顶地址中的值保存到r1。就是0x10000420中的值0x10000F78放入R1。REG_SP的值在arm_v7中定义的是0。
#define REG_R13 (0)
ldr r1, [r0, #(4*REG_SP)] /* R1=Value of SP before interrupt */
stmdb r1!, {r4-r11}
理解为之前从0x1000044C地址pop出来的8个寄存器值push入0x10000F78地址。从而栈顶地址修改为0x10000F58。这样就构造了一个sp进入中断后的自动入栈。把自己task的lr,sp,xpsr都保存到自己的栈地址,不就是入栈~
接着
ldmia r0!, {r2-r11,r14}
把r0地址0x10000420中的内容pop出来。这是将当前寄存器也够构造为第一个task在运行时候并且进行中断的状态吧~13)然后就是跳入3f标识符,将r1的值0x10000F58放入MSP中。最后通过bx r14自动将其内容pop出来作为中断现场恢复的内容。由于MSP和刚入中断时候已经出现了变更,说明进行任务切换。
tst r14, #EXC_RETURN_PROCESS_STACK /* nonzero if context on process stack */
ite eq /* next two instructions conditional */
msreq msp, r1 /* R1=The main stack pointer */
msrne psp, r1 /* R1=The process stack pointer */
bx r14
bx r14执行前的截图
最后跳入了sched\task\task_start.c的nxtask_start函数,并没有返回一开始arm_switchcontext函数调用svc 0后,说明进入了第一次任务上下文切换。
Nxtask_start函熟悉。nxtask_init里面有nxtask_setup_scheduler的参数是nxtask_start,而up_initial_state中是初始化构造栈中REG_PC赋值就是tcb->start就是nxtask_start。来源于
xcp->regs[REG_PC] = (uint32_t)tcb->start;
所以这一步我理解为初始化的时候狗仔了要第一个切入执行的函数就是Nxtask_start。至此第一个上下文切换的函数为Nxtask_start分析完毕。
四,总体思路小结
rtcb->xcp.regs是0x20000324 (saveregs)
exttcb->xcp.regs是0x10000420 (restoreregs)
至于0x20000324和0x10000420的来历是up_unblock_task函数中,this_task()的返回值返回了rtcb如下。0x20000324就是xcp.regs基于rtcb 0x200002B4的offset地址。
image.png
nxsched_add_readytorun(tcb)又添加一个tcb后,通过调用struct tcb_s *nexttcb = this_task();此时this_task返回的值增加为如下。
image.png
然后通过arm_switchcontext函数后调用svc 0触发硬件异常。
- 进入exception_common中断后是自动入栈了8个寄存器,MSP地址变成0x20000E88。
- 单独设置一个r2,用来保存进入exception_common中断前的栈底地址(因为有4个寄存器自动入栈,包括了exttcb->xcp.regs和rtcb->xcp.regs的值)。0x20000E88+32=0x20000EA8。
- 将r2-r11,r14的值在当前栈顶0x20000E88地址开始继续入栈。地址变成0x20000E5C。
- 后来又将0x20000E5C进行8字节对齐,变成0x20000E58。
此时可以理解为修改了栈中的内容,做了push入栈保护动作。从0x20000EA8地址到0x20000E88自动保存了进入exception_common中断8个寄存器。从0x20000E88地址到0x20000E58地址是在exception_common中断中程序员保存了r2-r11,r14及一个temp值。 - 然后进入arm_doirq后更新待切换的栈地址为0x10000420,与跳入中断前执行的task栈栈地址对比,不同则模拟新栈地址进行硬件入栈(这个入栈的内容是svc 0之前task栈的内容,目的是假装没有执行为svc0,是连贯的2个task的切换)。
- 最后pop出新栈内容恢复,然后进行BX lr指令还原。
五,总结
对比下Freertos的进入第一个任务是什么流程,FreeRTOS在svc中断中直接把要切换的栈顶给出,然后pop出r4-r11手工保存的内容,然后就BX lr还原自动保存的内容。Nuttx OS的第一个任务切换就感觉很绕,估计原因是这个svc0中断和其它中断混用,所以逻辑上做的比较复杂。
网友评论