Step 11. 繼續(xù)單步到rt_hw_context_switch_to函數(shù)處。
在rt_system_scheduler_start函數(shù)中,會(huì)依次獲取最高優(yōu)先級(jí)線程的線程控制塊,將其復(fù)制給to_thread。如圖所示,在表達(dá)式窗口的to_thread就是main線程。
&to_thread->spthread->sp的地址,在Debug中,地址編號(hào)為0x200010C8,即0x200010C8內(nèi)存單元中存放的數(shù)據(jù)是0x200018F4。
Q2. 在單獨(dú)進(jìn)入到rt_hw_context_switch_to之前,觀察輸出結(jié)果,main線程被remove。為什么在啟動(dòng)調(diào)度器的函數(shù)中,要先將線程從就緒列表中移除呢?
A2. 下一步要啟動(dòng)main線程,將其從Ready狀態(tài)變成Running狀態(tài),所以需要將該線程從就緒列表中刪除,RT-Thread后續(xù)在調(diào)度時(shí)暫時(shí)不考慮該線程,直到該線程狀態(tài)再次從Running發(fā)生變化。
Step 12. 單步到進(jìn)入到rt_hw_context_switch_to函數(shù)處,該函數(shù)位于context_gcc.S文件,由匯編語(yǔ)言編寫實(shí)現(xiàn)。
rt_hw_context_switch_to僅僅在調(diào)度器啟動(dòng)時(shí)運(yùn)行一次。該函數(shù)的C語(yǔ)言實(shí)現(xiàn)接口中,有一個(gè)參數(shù),傳入thread->sp變量的地址。
對(duì)于參數(shù)個(gè)數(shù)不大于4的C語(yǔ)言接口函數(shù),編譯器會(huì)按參數(shù)在列表中的順序,自左向右 為參數(shù)分配寄存器r0-r3。
對(duì)于參數(shù)個(gè)數(shù)大于4的C語(yǔ)言接口函數(shù),編譯器會(huì)按參數(shù)在列表中的順序,多余參數(shù)按自右向左的順序壓入棧中,即參數(shù)入棧順序與參數(shù)順序相反。
如上述Tips,thread->sp的地址通過(guò)r0傳遞。在下圖左側(cè)寄存器窗口中,可以看到r0的值為0x200010C8。
165行,將變量rt_interrupt_to_thread變量的地址賦值給r1。
165行,將r0的值賦值給r1指向的單元,即將r0的值賦值給變量rt_interrupt_to_thread。如果此時(shí)在表達(dá)式窗口觀察rt_interrupt_to_thread,會(huì)發(fā)現(xiàn)它的值為0x200010C8。
此時(shí),main線程的線程結(jié)構(gòu)體和線程??臻g不變,但是r0, r1, rt_interrupr_to_thread的內(nèi)容均發(fā)生了變化。
對(duì)于rt_hw_context_switch_to函數(shù)的其他行,依次分析如下:
168行至172行,處理浮點(diǎn)寄存器入??刂?,與Cortex M4內(nèi)核的Lazy Stacking有關(guān),但與本文主線無(wú)關(guān),不做探討。
176至178行,將rt_interrupt_from_thread變量清零。因此本次是RT-Thread第一次調(diào)度最高優(yōu)先級(jí)線程,只有to,沒有from。
181至183行,將rt_thread_switch_interrupt_flag變量至1,該值將在PendSV中斷中使用。
186-194行,設(shè)置SysTick和PendSV中斷的優(yōu)先級(jí),且觸發(fā)PendSV,但現(xiàn)在不跳轉(zhuǎn),因?yàn)橹袛酁榻埂?/p>
197-201行,很有意思的一段操作,將0x08000000處的棧頂指針?lè)胖玫組SP中,相當(dāng)于特權(quán)模式的棧頂指針復(fù)位了。CPU從匯編編寫的啟動(dòng)代碼,直到運(yùn)行到此處,均在特權(quán)模式下運(yùn)行,使用MSP作為棧頂指針。將來(lái)切換到線程后,會(huì)以PSP作為棧頂指針。啟動(dòng)流程不會(huì)重來(lái)一次,也沒有任何函數(shù)再需要返回。所以,對(duì)于截止到目前使用的MSP棧,可以舍棄棧中的數(shù)據(jù),MSP棧重置。
204-205行,使能中斷。首先在context_gcc.S的89行設(shè)置斷點(diǎn),然后當(dāng)PC運(yùn)行在204行時(shí)按F5,會(huì)運(yùn)行至PendSV中斷服務(wù)程序。
Step 13. PendSV函數(shù)分析。
在PendSV中斷服務(wù)程序中:
94行-96行,判斷rt_thread_switch_interrupt_flag的值,為0則退出,為1則繼續(xù);
99行-105行,rt_thread_switch_interrupt_flag清0,判斷rt_interrupt_from_thread的值,為0表示OS第一次進(jìn)行最高優(yōu)先級(jí)就緒狀態(tài)線程的運(yùn)行,無(wú)需恢復(fù)psp,直接跳轉(zhuǎn)到switch_to_thread;為1表示從from線程切換至to線程,需要恢復(fù)psp。Debug到此處,rt_interrupt_from_thread的值為0,是第一次進(jìn)行線程運(yùn)行。
此處直接分析127行開始的switch_to_thread部分。
128行,將rt_interrupt_to_thread的地址賦值給r1。
129行,從r1指向的地址中取出值,賦值給r1,此時(shí)r1指向到main線程的thread->sp。
130行,從r1指向的地址中取出值,賦值給r1,此時(shí)r1指向到0x200018F4,如下圖所示。
133行-136行,將r1指向的0x200018F4開始的單元內(nèi)容,依次裝載到r3, r4-r11中。執(zhí)行完畢后,R3中是flag的值,r4-r11中均為0xDEAFBEEF,且r1指向0x20001918。
139-140行,由于r3為0,浮點(diǎn)寄存器不做處理。r1保持不變。
143行,將r1的值賦值給PSP,線程棧頂指針PSP目前為0x20001918。后續(xù)PSP還會(huì)自動(dòng)更新。
155行,使得LR寄存器的Bit2為1,確保PendSV異常返回使用的棧指針是PSP。
156行,異常返回。此時(shí),線程棧中剩下內(nèi)容,即從0x20001918-0x20001934的內(nèi)容,會(huì)自動(dòng)加載到R0, R1, R2, R3, R12, R14 (線程返回地址), PC (線程入口地址), xPSR。且,PSP會(huì)自動(dòng)更新至0x20001938,即創(chuàng)建main線程時(shí)的棧頂指針。
Step 14. 光標(biāo)在BX LR上時(shí),按F5,自動(dòng)運(yùn)行到main線程入口地址main_thread_entry。
如下圖所示,棧幀中的r0-r15, xPSR均已順利從線程棧中進(jìn)行了恢復(fù),此時(shí)thread->sp = PSP = 0x20001938。開始順利執(zhí)行線程。
通過(guò)本文對(duì)線程啟動(dòng)過(guò)程的了解,對(duì)于兩個(gè)線程/多個(gè)線程之間的互相切換能奠定堅(jiān)實(shí)的基礎(chǔ),化繁為簡(jiǎn),結(jié)合論壇關(guān)于上下文切換的代碼注釋,能幫助快速抓住主線。
使用的軟硬件環(huán)境如下:
IDE工具 - RT-Thread Studio 2.2.6
硬件 - STM32L431RCT6,Cortex M4內(nèi)核
軟件 - RT-Thread 4.0.5版本
配置 - 僅使能main線程和tidle0線程
一、工程設(shè)置
Step 1. 新建名稱為EVBMX_RTThread405_Switch的4.0.5版本工程
Step 2. 不使能軟件定時(shí)器,使能線程狀態(tài)更改的調(diào)試
關(guān)閉軟件定時(shí)器線程,避免干擾。
Step 3. 關(guān)閉msh shell,禁用Finsh
關(guān)閉tshell線程,避免干擾。僅僅保留main線程和tidle0線程。
Step 4. 修改main函數(shù)
修改main函數(shù)后,線程進(jìn)入一次,休眠且切換1次,再次切回且return,然后徹底退出,只留下tidle0線程。
#include
#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#include
int main(void)
{
rt_thread_mdelay(1000);
return RT_EOK;
}
Step 5. 下載程序,觀察輸出結(jié)果
讀完全文后,對(duì)下方輸出結(jié)果的每一行語(yǔ)句所代表的含義和發(fā)生時(shí)刻,能有更深刻體會(huì)。
二、調(diào)試運(yùn)行
Step 6. 在component.c中257行按F9設(shè)置斷點(diǎn);F5全速運(yùn)行到此處后,再按F9關(guān)閉此處斷點(diǎn)。
Step 7. 依次進(jìn)入rt_thread_create, _thread_init, 停留在thread.c的164行。
將變量thread添加到表達(dá)式窗口,可以查看各個(gè)成員的值,其中,thread->stack_addr = 0x20001138, thread->stack_size = 0x800,分別表示棧底位置和??臻g大小。
164行的函數(shù)rt_hw_stack_init對(duì)于理解線程切換是一個(gè)相當(dāng)重要的函數(shù),其形參分別為:
線程入口函數(shù):main_thread_entry
線程參數(shù)RT_NULL:
線程棧棧頂?shù)刂罚簍hread->stack_addr + thread->stack_size - 4 = 0x20001138 + 0x800 - 4 = 0x20001934
Q1:為什么此處需要減4?
A2: 很有意思的一個(gè)問(wèn)題。答案可參考本人在論壇的一個(gè)回答。RT-Thread-小白求助,關(guān)于rtt 的一段源碼RT-Thread問(wèn)答社區(qū) - RT-Thread
Step 8. 單步進(jìn)入到rt_hw_stack_init函數(shù)內(nèi)部,開展分析
149行,由于傳遞進(jìn)來(lái)的stack_addr = 0x20001934,執(zhí)行完畢后,stk為0x20001938。從0x20001138(含)到0x20001934(含),合計(jì)是0x800 = 2048字節(jié)。STM32使用的滿遞減棧,所以此處的stk是0x20001938。
150行,此處設(shè)置8字節(jié)對(duì)齊。由于0x20001938 = (536877368)Decimal,該數(shù)據(jù)除8等于67109671,能被8整除,該語(yǔ)句執(zhí)行棧對(duì)齊操作后,stk依然為0x20001938。
Step 9. 繼續(xù)了解rt_hw_stack_init函數(shù)。
151行,更新stk的值,減去struct stack_frame結(jié)構(gòu)體的大小。執(zhí)行完畢后,stk = 0x200018F4。
153行,stack_frame指針指向0x200018F4。
156至159行,通過(guò)for循環(huán)將0x200018F4至0x20001938的所有內(nèi)存變成0xdeadbeaf魔法字。
161行至168行,將stack_frame成員的exception_stack_frame中的r0~psr共8個(gè)寄存器分別設(shè)置為:線程參數(shù),4個(gè)0,線程返回地址,線程入口地址,0x01000000。
175行,返回stk的值,此時(shí)變成0x200018F4。這個(gè)值在初始化線程時(shí),將返回給thread->sp,即線程棧的臨時(shí)棧頂指針。
依次將線程的形參、r1-r3, r12, 線程返回地址、線程入口地址,線程的xPSR寫入異常棧幀結(jié)構(gòu)中。
在初入門時(shí),這里是難點(diǎn)。C語(yǔ)言中使用結(jié)構(gòu)體定義的棧結(jié)構(gòu),如何和實(shí)際寄存器的順序進(jìn)行一一對(duì)應(yīng)?,后文會(huì)通過(guò)逐步Debug揭示這個(gè)問(wèn)題答案。
返回的stk指向0x200018F4部分。
至此,main線程創(chuàng)建完畢后,線程結(jié)構(gòu)體和線程??臻g如下所示。
Step 10. 繼續(xù)單步到rt_system_scheduler_start函數(shù)處,并單獨(dú)跟蹤進(jìn)入到該函數(shù)內(nèi)部。
期間,RT-Thread會(huì)調(diào)用rt_thread_idle_init函數(shù),在該函數(shù)中使用靜態(tài)創(chuàng)建方式初始化tidle0線程??梢园凑丈鲜鲞^(guò)程記錄tidle0線程的??臻g。
Step 11. 繼續(xù)單步到rt_hw_context_switch_to函數(shù)處。
在rt_system_scheduler_start函數(shù)中,會(huì)依次獲取最高優(yōu)先級(jí)線程的線程控制塊,將其復(fù)制給to_thread。如圖所示,在表達(dá)式窗口的to_thread就是main線程。
&to_thread->spthread->sp的地址,在Debug中,地址編號(hào)為0x200010C8,即0x200010C8內(nèi)存單元中存放的數(shù)據(jù)是0x200018F4。
Q2. 在單獨(dú)進(jìn)入到rt_hw_context_switch_to之前,觀察輸出結(jié)果,main線程被remove。為什么在啟動(dòng)調(diào)度器的函數(shù)中,要先將線程從就緒列表中移除呢?
A2. 下一步要啟動(dòng)main線程,將其從Ready狀態(tài)變成Running狀態(tài),所以需要將該線程從就緒列表中刪除,RT-Thread后續(xù)在調(diào)度時(shí)暫時(shí)不考慮該線程,直到該線程狀態(tài)再次從Running發(fā)生變化。
Step 12. 單步到進(jìn)入到rt_hw_context_switch_to函數(shù)處,該函數(shù)位于context_gcc.S文件,由匯編語(yǔ)言編寫實(shí)現(xiàn)。
rt_hw_context_switch_to僅僅在調(diào)度器啟動(dòng)時(shí)運(yùn)行一次。該函數(shù)的C語(yǔ)言實(shí)現(xiàn)接口中,有一個(gè)參數(shù),傳入thread->sp變量的地址。
對(duì)于參數(shù)個(gè)數(shù)不大于4的C語(yǔ)言接口函數(shù),編譯器會(huì)按參數(shù)在列表中的順序,自左向右 為參數(shù)分配寄存器r0-r3。
對(duì)于參數(shù)個(gè)數(shù)大于4的C語(yǔ)言接口函數(shù),編譯器會(huì)按參數(shù)在列表中的順序,多余參數(shù)按自右向左的順序壓入棧中,即參數(shù)入棧順序與參數(shù)順序相反。
如上述Tips,thread->sp的地址通過(guò)r0傳遞。在下圖左側(cè)寄存器窗口中,可以看到r0的值為0x200010C8。
165行,將變量rt_interrupt_to_thread變量的地址賦值給r1。
165行,將r0的值賦值給r1指向的單元,即將r0的值賦值給變量rt_interrupt_to_thread。如果此時(shí)在表達(dá)式窗口觀察rt_interrupt_to_thread,會(huì)發(fā)現(xiàn)它的值為0x200010C8。
此時(shí),main線程的線程結(jié)構(gòu)體和線程??臻g不變,但是r0, r1, rt_interrupr_to_thread的內(nèi)容均發(fā)生了變化。
對(duì)于rt_hw_context_switch_to函數(shù)的其他行,依次分析如下:
168行至172行,處理浮點(diǎn)寄存器入??刂疲cCortex M4內(nèi)核的Lazy Stacking有關(guān),但與本文主線無(wú)關(guān),不做探討。
176至178行,將rt_interrupt_from_thread變量清零。因此本次是RT-Thread第一次調(diào)度最高優(yōu)先級(jí)線程,只有to,沒有from。
181至183行,將rt_thread_switch_interrupt_flag變量至1,該值將在PendSV中斷中使用。
186-194行,設(shè)置SysTick和PendSV中斷的優(yōu)先級(jí),且觸發(fā)PendSV,但現(xiàn)在不跳轉(zhuǎn),因?yàn)橹袛酁榻埂?br /> 197-201行,很有意思的一段操作,將0x08000000處的棧頂指針?lè)胖玫組SP中,相當(dāng)于特權(quán)模式的棧頂指針復(fù)位了。CPU從匯編編寫的啟動(dòng)代碼,直到運(yùn)行到此處,均在特權(quán)模式下運(yùn)行,使用MSP作為棧頂指針。將來(lái)切換到線程后,會(huì)以PSP作為棧頂指針。啟動(dòng)流程不會(huì)重來(lái)一次,也沒有任何函數(shù)再需要返回。所以,對(duì)于截止到目前使用的MSP棧,可以舍棄棧中的數(shù)據(jù),MSP棧重置。
204-205行,使能中斷。首先在context_gcc.S的89行設(shè)置斷點(diǎn),然后當(dāng)PC運(yùn)行在204行時(shí)按F5,會(huì)運(yùn)行至PendSV中斷服務(wù)程序。
Step 13. PendSV函數(shù)分析。
在PendSV中斷服務(wù)程序中:
94行-96行,判斷rt_thread_switch_interrupt_flag的值,為0則退出,為1則繼續(xù);
99行-105行,rt_thread_switch_interrupt_flag清0,判斷rt_interrupt_from_thread的值,為0表示OS第一次進(jìn)行最高優(yōu)先級(jí)就緒狀態(tài)線程的運(yùn)行,無(wú)需恢復(fù)psp,直接跳轉(zhuǎn)到switch_to_thread;為1表示從from線程切換至to線程,需要恢復(fù)psp。Debug到此處,rt_interrupt_from_thread的值為0,是第一次進(jìn)行線程運(yùn)行。
此處直接分析127行開始的switch_to_thread部分。
128行,將rt_interrupt_to_thread的地址賦值給r1。
129行,從r1指向的地址中取出值,賦值給r1,此時(shí)r1指向到main線程的thread->sp。
130行,從r1指向的地址中取出值,賦值給r1,此時(shí)r1指向到0x200018F4,如下圖所示。
133行-136行,將r1指向的0x200018F4開始的單元內(nèi)容,依次裝載到r3, r4-r11中。執(zhí)行完畢后,R3中是flag的值,r4-r11中均為0xDEAFBEEF,且r1指向0x20001918。
139-140行,由于r3為0,浮點(diǎn)寄存器不做處理。r1保持不變。
143行,將r1的值賦值給PSP,線程棧頂指針PSP目前為0x20001918。后續(xù)PSP還會(huì)自動(dòng)更新。
155行,使得LR寄存器的Bit2為1,確保PendSV異常返回使用的棧指針是PSP。
156行,異常返回。此時(shí),線程棧中剩下內(nèi)容,即從0x20001918-0x20001934的內(nèi)容,會(huì)自動(dòng)加載到R0, R1, R2, R3, R12, R14 (線程返回地址), PC (線程入口地址), xPSR。且,PSP會(huì)自動(dòng)更新至0x20001938,即創(chuàng)建main線程時(shí)的棧頂指針。
Step 14. 光標(biāo)在BX LR上時(shí),按F5,自動(dòng)運(yùn)行到main線程入口地址main_thread_entry。
如下圖所示,棧幀中的r0-r15, xPSR均已順利從線程棧中進(jìn)行了恢復(fù),此時(shí)thread->sp = PSP = 0x20001938。開始順利執(zhí)行線程。
三、修改rt_hw_context_switch_to函數(shù),使用SVC進(jìn)入第一個(gè)線程
FreeRTOS使用SVC進(jìn)入第一個(gè)線程,通過(guò)簡(jiǎn)單修改,在STM32L431RCT6 Cortex-M4內(nèi)核上也可以支持用SVC進(jìn)入第一個(gè)線程。 計(jì)劃在線下課程中,與學(xué)生們面對(duì)面深入探討一次。
對(duì)rt_hw_context_switch_to函數(shù)的修改過(guò)程如下:
刪除對(duì)rt_interrupt_from_thread的清零
刪除對(duì)rt_thread_switch_interrupt_flag的置1
刪除對(duì)PendSV的觸發(fā)
新增dsb isb
新增SVC 0
毫無(wú)意義,對(duì)R0賦值,通過(guò)Debug觀察到該語(yǔ)句不會(huì)被執(zhí)行
修改后的rt_hw_context_switch_to函數(shù)和SVC_Handler函數(shù)如下:
.global rt_hw_context_switch_to
.type rt_hw_context_switch_to, %function
rt_hw_context_switch_to:
LDR r1, =rt_interrupt_to_thread
STR r0, [r1]
#if defined ( VFP_FP ) && !defined( SOFTFP )
/* CLEAR CONTROL.FPCA /
MRS r2, CONTROL / read /
BIC r2, #0x04 / modify /
MSR CONTROL, r2 / write-back /
#endif
/ set the PendSV and SysTick exception priority /
LDR r0, =NVIC_SYSPRI2
LDR r1, =NVIC_PENDSV_PRI
LDR.W r2, [r0,#0x00] / read /
ORR r1,r1,r2 / modify /
STR r1, [r0] / write-back /
/ restore MSP /
LDR r0, =SCB_VTOR
LDR r0, [r0]
LDR r0, [r0]
NOP
MSR msp, r0
/ enable interrupts at processor level /
CPSIE F
CPSIE I
dsb
isb
SVC 0
/ never reach here! /
LDR r0, =0x12345678 / debug according to blta's comment /
.global SVC_Handler
.type SVC_Handler, %function
SVC_Handler:
/ disable interrupt to protect context switch /
MRS r2, PRIMASK
CPSID I
/ get rt_thread_switch_interrupt_flag /
switch_to_first_thread:
LDR r1, =rt_interrupt_to_thread
LDR r1, [r1]
LDR r1, [r1] / load thread stack pointer /
#if defined ( VFP_FP ) && !defined( SOFTFP )
LDMFD r1!, {r3} / pop flag /
#endif
LDMFD r1!, {r4 - r11} / pop r4 - r11 register /
#if defined ( VFP_FP ) && !defined( SOFTFP )
CMP r3, #0 / if(flag_r3 != 0) */
VLDMIANE r1!, {d8 - d15} /* pop FPU register s16~s31 */
#endif
MSR psp, r1 /* update stack pointer */
#if defined (__VFP_FP__) && !defined(__SOFTFP__)
ORR lr, lr, #0x10 /* lr |= (1 << 4), clean FPCA. */
CMP r3, #0 /* if(flag_r3 != 0) */
BICNE lr, lr, #0x10 /* lr &= ~(1 << 4), set FPCA. */
#endif
svc_exit:
/* restore interrupt */
MSR PRIMASK, r2
ORR lr, lr, #0x04
BX lr
四、小結(jié)
本文簡(jiǎn)單探討了RT-Thread 4.0.5版本在STM32L431RCTx Cortex-M4內(nèi)核上,創(chuàng)建main線程、tidle0線程后,從使用MSP的特權(quán)模式,啟動(dòng)至使用PSP線程模式的main線程棧幀恢復(fù)全過(guò)程。
SP寄存器有兩個(gè),分別是MSP和PSP,其中,從復(fù)位啟動(dòng)后使用MSP,通過(guò)啟動(dòng)代碼、RT-Thread初始化、啟動(dòng)調(diào)度器的過(guò)程,切換至使用PSP的線程中運(yùn)行。
每個(gè)線程均有獨(dú)立的棧。使用rt_thread_create創(chuàng)建的線程,棧位于heap中;使用rt_thread_init創(chuàng)建的棧,棧位于自定義的數(shù)組中。
線程切換,即保存所有寄存器的快照到線程棧中,r0-r15, xPSR,浮點(diǎn)寄存器。線程恢復(fù),即從線程棧中恢復(fù)寄存器快照。
在線程模式下,如果發(fā)生中斷,會(huì)繼續(xù)使用MSP。
Cortex M4發(fā)生中斷,會(huì)有系列寄存器自動(dòng)入棧處理的操作,本文不展開討論。
RT-Thread的上下文切換的Context_gcc.S文件中rt_hw_context_switch_to也可以用SVC進(jìn)行線程處理。
-
寄存器
+關(guān)注
關(guān)注
31文章
5429瀏覽量
123847 -
Cortex-M4
+關(guān)注
關(guān)注
6文章
98瀏覽量
47118 -
SVC
+關(guān)注
關(guān)注
0文章
33瀏覽量
12410 -
RT-Thread
+關(guān)注
關(guān)注
32文章
1384瀏覽量
41650 -
STM32L4
+關(guān)注
關(guān)注
1文章
42瀏覽量
9634
發(fā)布評(píng)論請(qǐng)先 登錄
RT-Thread啟動(dòng)進(jìn)入就緒態(tài)最高優(yōu)先級(jí)線程的全過(guò)程與棧幀分析(上)

深度剖析 RT-Thread 線程調(diào)度流程

評(píng)論