一,前言
復習完FreeRTOS的任務切換匯編,來分析下NuttxOS的任務切換匯編設計思路。這里我重點分析的不是任務調度算法哦。今天分析的是第一次任務切換,先走一個溫故而知新的路線。
二,回顧
我先簡單回顧下FreeRTOS中基于cortexM3/M4上下文切換的原理。
-
進入中斷:
上一個任務中xPSR, PC, R14, R12, R3-R0這些寄存器的值會自動存儲到任務的棧中,同時PSP 會自動更新(在更新之前 PSP 指向任務棧的棧頂)如下圖
這個入棧指將內核寄存器的值進行保存,保存到PSP或MSP地址(向低地址方向push入)中的內容更新。
image.png
然后程序員要在中斷函數(shù)中手工添加R4-R11的入棧,PSP棧地址變的更小。根據(jù)對棧的posh和pop動作,棧頂?shù)刂窌?jīng)常變動,但是棧底地址是不變。
-
退出中斷:
在退出中斷前從PSP或MSP【通過lr寄存器的最后2個bit來選擇】的棧頂?shù)刂烽_始自動pop到實際的寄存器??梢岳斫鉃橥ㄟ^pop來恢復現(xiàn)場。就是把棧地址中的地址恢復到當前內核寄存器中。
除了control特殊寄存器可以控制模式切換。中斷中R14(lr)return value也可以。需要使用命令bx lr,不是bl reg。
image.png - 總結:
關于任務上下文切換的設計思想,就是選用中斷的方式進行棧替換,并且設置出棧內容和入棧內容不同,來進行任務切換。
A . 正常的外設中斷,不進行上下文任務切換,先保護現(xiàn)場到棧,進入中斷執(zhí)行,然后通過?;謴同F(xiàn)場。
B. 那么中斷要用于切換任務,就是要切換棧。而切換棧的方式就是找到棧地址,然后操作棧地址中的內容。可以理解為入棧保護是有的,但是出棧的時候棧頂?shù)刂诽鎿Q了,所以切換到另外一個函數(shù)了,這就是實現(xiàn)上下文切換的原理。
三,nuttx 10.01的任務上下文切換源碼分析
在我的stm32F407開發(fā)板上,編譯通過后燒寫代碼,用arm官網(wǎng)的ozone進行調試并截圖的,因為之前搭建的codeblocks交叉編譯調試環(huán)境感覺速度慢,而且寄存器無法查看。代碼從__start開始,一路進入up_unblock_task函數(shù)。

1)此函數(shù)中最后調用
arm_switchcontext(rtcb->xcp.regs, nexttcb->xcp.regs);進入?yún)R編的上下文切換代碼。當調用函數(shù)后,發(fā)現(xiàn)MSP棧地址已經(jīng)發(fā)生了變化。此時MSP=0x20000EA8。
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
這段切換代碼是將傳入?yún)?shù)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及之后的都設置了入口函數(shù)為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函數(shù)后。此時MSP已經(jīng)自動入棧了8個字節(jié),所以棧頂?shù)刂纷冃???梢钥创撕瘮?shù)的注釋,此時MSP=0x20000E88。此時已經(jīng)將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的內容如下

4)exception_common函數(shù)中的內容分析
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標識中執(zhí)行。
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字節(jié)的地址保存到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}一開始進入中斷函數(shù)有8個字節(jié)自動入棧,sp地址自動加32,現(xiàn)在繼續(xù)入棧sp繼續(xù)增加,這里和FreeRTOS明顯不同,它由程序員手工入棧的寄存器不僅R4-R11,還多了r2和r3及r14。而r2保存的是進入中斷前的MSP地址,r3保存的是中斷屏蔽狀態(tài)。R14保存的是進入svc中斷前用的是MSP還是PSP。此時MSP=0x20000E88-11*4=0x20000E5C。

8)把sp保存到r1,此時的sp已經(jīng)完成push動作,所以是棧頂?shù)刂贰?br>
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函數(shù)前截圖,參考r0和r1參數(shù)值,reg參數(shù)是0x20000E5C。

bl arm_doirq進入c函數(shù)。沒什么特別的,一路進入arm_hardfault。此時傳入的參數(shù)中context就是0x20000E5C。

arm_hardfault中從reg參數(shù)0x20000E5C獲取PC的值-1的地址為,為REG_PC為棧頂?shù)刂?(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 0xdf00uint16_t insn = *pc;這塊svc0觸發(fā)時候為什么地址中的內容是0xdf00,不太清楚,反正取這個PC值的目的就是做判斷,是否svc0觸發(fā)的中斷,是的話就調用arm_svcall
if (insn == INSN_SVC0)
{
return arm_svcall(irq, context, arg);
}
重點來了,還記得一開始調用arm_switchcontext時候修改的r0,r1,r2寄存器么,它是被調用了svc 0后自動入棧的。此時就通過0x20000E5C棧頂?shù)刂穼⑵渫ㄟ^regs[REG_R0]方式把值拿出來用。
下圖的case就是說判斷上下文切換,若r1(rtcb->xcp.regs)和r2(nexttcb->xcp.regs)棧地址不同,則將當前已經(jīng)入棧的數(shù)據(jù),都保存到r1指向的地址中進行入棧。因為(uint32_t *)regs[REG_R1]代表取regs[REG_R1]為地址中的內容。這樣理解的話0x20000E5C就是一個臨時棧。0x20000324(rtcb->xcp.regs)才是正主。

執(zhí)行
memcpy((uint32_t *)regs[REG_R1], regs, XCPTCONTEXT_SIZE);后就是把regs[REG_R1]地址中的內容0x20000324作為dest地址。將regs地址0x20000E5C中的17*4的寄存器內容全部copy到0x20000324地址中,此地址就是rtcb->xcp.regs傳入的值,這樣就模擬了現(xiàn)場保護。
接著執(zhí)行
CURRENT_REGS = (uint32_t *)regs[REG_R2];就是把regs[REG_R2]中的值作為地址,這個值是0x10000420,其實就是exttcb->xcp.regs傳入的值,此時CURRENT_REGS指向了0x10000420待切換地址。

然后就是一路返回到arm_doirq函數(shù)中,將0x10000420賦值給reg最后進行返回,同時CURRENT_REGS清0。
regs = (uint32_t *)CURRENT_REGS;
CURRENT_REGS = savestate;
#endif
board_autoled_off(LED_INIRQ);
return regs;
小插曲,說說r4的值,因為退出arm_doirq函數(shù)后還會用到。
Arm_svcall的switch(cmd)命令轉換為匯編會把R1移動到R4。而R1的地址是0x20000E5C當初進入arm_doirq函數(shù)時候傳遞的參數(shù)。至于為什么移動到r4,我理解有3個參數(shù),已經(jīng)占用了3個寄存器,所以對于函數(shù)內的臨時變量,就放入r4了。這是我猜的,不知道為什么一定是r4。

退出arm_doirq函數(shù)后,寄存器的狀態(tài)如下。

10)把r4保存到r1,r1是主棧地址,然后r0是待切換的棧,檢查是否地址相同,相同則不要切換上下文跳入2, 不同則繼續(xù)執(zhí)行匯編,進行上下文切換。此時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變成了要切換的棧的棧頂?shù)刂贰?br>
add r1, r0, #SW_XCPT_SIZE至于為什么不是8,而是11,這要看r0棧地址0x10000420內容中的含義了。這個地址中的值中的內容是在創(chuàng)建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。

12)接著2句就是把r0棧頂?shù)刂分械闹当4娴絩1。就是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地址。從而棧頂?shù)刂沸薷臑?x10000F58。這樣就構造了一個sp進入中斷后的自動入棧。把自己task的lr,sp,xpsr都保存到自己的棧地址,不就是入棧~

接著
ldmia r0!, {r2-r11,r14} 把r0地址0x10000420中的內容pop出來。這是將當前寄存器也夠構造為第一個task在運行時候并且進行中斷的狀態(tài)吧~13)然后就是跳入3f標識符,將r1的值0x10000F58放入MSP中。最后通過bx r14自動將其內容pop出來作為中斷現(xiàn)場恢復的內容。由于MSP和剛入中斷時候已經(jīng)出現(xiàn)了變更,說明進行任務切換。
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執(zhí)行前的截圖

最后跳入了sched\task\task_start.c的nxtask_start函數(shù),并沒有返回一開始arm_switchcontext函數(shù)調用svc 0后,說明進入了第一次任務上下文切換。
Nxtask_start函熟悉。nxtask_init里面有nxtask_setup_scheduler的參數(shù)是nxtask_start,而up_initial_state中是初始化構造棧中REG_PC賦值就是tcb->start就是nxtask_start。來源于
xcp->regs[REG_PC] = (uint32_t)tcb->start;所以這一步我理解為初始化的時候狗仔了要第一個切入執(zhí)行的函數(shù)就是Nxtask_start。至此第一個上下文切換的函數(shù)為Nxtask_start分析完畢。
四,總體思路小結
rtcb->xcp.regs是0x20000324 (saveregs)
exttcb->xcp.regs是0x10000420 (restoreregs)
至于0x20000324和0x10000420的來歷是up_unblock_task函數(shù)中,this_task()的返回值返回了rtcb如下。0x20000324就是xcp.regs基于rtcb 0x200002B4的offset地址。

nxsched_add_readytorun(tcb)又添加一個tcb后,通過調用struct tcb_s *nexttcb = this_task();此時this_task返回的值增加為如下。

然后通過arm_switchcontext函數(shù)后調用svc 0觸發(fā)硬件異常。
- 進入exception_common中斷后是自動入棧了8個寄存器,MSP地址變成0x20000E88。
- 單獨設置一個r2,用來保存進入exception_common中斷前的棧底地址(因為有4個寄存器自動入棧,包括了exttcb->xcp.regs和rtcb->xcp.regs的值)。0x20000E88+32=0x20000EA8。
- 將r2-r11,r14的值在當前棧頂0x20000E88地址開始繼續(xù)入棧。地址變成0x20000E5C。
- 后來又將0x20000E5C進行8字節(jié)對齊,變成0x20000E58。
此時可以理解為修改了棧中的內容,做了push入棧保護動作。從0x20000EA8地址到0x20000E88自動保存了進入exception_common中斷8個寄存器。從0x20000E88地址到0x20000E58地址是在exception_common中斷中程序員保存了r2-r11,r14及一個temp值。 - 然后進入arm_doirq后更新待切換的棧地址為0x10000420,與跳入中斷前執(zhí)行的task棧棧地址對比,不同則模擬新棧地址進行硬件入棧(這個入棧的內容是svc 0之前task棧的內容,目的是假裝沒有執(zhí)行為svc0,是連貫的2個task的切換)。
- 最后pop出新棧內容恢復,然后進行BX lr指令還原。
五,總結
對比下Freertos的進入第一個任務是什么流程,F(xiàn)reeRTOS在svc中斷中直接把要切換的棧頂給出,然后pop出r4-r11手工保存的內容,然后就BX lr還原自動保存的內容。Nuttx OS的第一個任務切換就感覺很繞,估計原因是這個svc0中斷和其它中斷混用,所以邏輯上做的比較復雜。

