Step 11. 繼續(xù)單步到rt_hw_context_switch_to函數(shù)處。
在rt_system_scheduler_start函數(shù)中,會依次獲取最高優(yōu)先級線程的線程控制塊,將其復制給to_thread。如圖所示,在表達式窗口的to_thread就是main線程。
&to_thread->spthread->sp的地址,在Debug中,地址編號為0x200010C8,即0x200010C8內存單元中存放的數(shù)據(jù)是0x200018F4。
Q2. 在單獨進入到rt_hw_context_switch_to之前,觀察輸出結果,main線程被remove。為什么在啟動調度器的函數(shù)中,要先將線程從就緒列表中移除呢?
A2. 下一步要啟動main線程,將其從Ready狀態(tài)變成Running狀態(tài),所以需要將該線程從就緒列表中刪除,RT-Thread后續(xù)在調度時暫時不考慮該線程,直到該線程狀態(tài)再次從Running發(fā)生變化。
Step 12. 單步到進入到rt_hw_context_switch_to函數(shù)處,該函數(shù)位于context_gcc.S文件,由匯編語言編寫實現(xiàn)。
rt_hw_context_switch_to僅僅在調度器啟動時運行一次。該函數(shù)的C語言實現(xiàn)接口中,有一個參數(shù),傳入thread->sp變量的地址。
對于參數(shù)個數(shù)不大于4的C語言接口函數(shù),編譯器會按參數(shù)在列表中的順序,自左向右 為參數(shù)分配寄存器r0-r3。
對于參數(shù)個數(shù)大于4的C語言接口函數(shù),編譯器會按參數(shù)在列表中的順序,多余參數(shù)按自右向左的順序壓入棧中,即參數(shù)入棧順序與參數(shù)順序相反。
如上述Tips,thread->sp的地址通過r0傳遞。在下圖左側寄存器窗口中,可以看到r0的值為0x200010C8。
165行,將變量rt_interrupt_to_thread變量的地址賦值給r1。
165行,將r0的值賦值給r1指向的單元,即將r0的值賦值給變量rt_interrupt_to_thread。如果此時在表達式窗口觀察rt_interrupt_to_thread,會發(fā)現(xiàn)它的值為0x200010C8。
此時,main線程的線程結構體和線程??臻g不變,但是r0, r1, rt_interrupr_to_thread的內容均發(fā)生了變化。
對于rt_hw_context_switch_to函數(shù)的其他行,依次分析如下:
168行至172行,處理浮點寄存器入??刂?,與Cortex M4內核的Lazy Stacking有關,但與本文主線無關,不做探討。
176至178行,將rt_interrupt_from_thread變量清零。因此本次是RT-Thread第一次調度最高優(yōu)先級線程,只有to,沒有from。
181至183行,將rt_thread_switch_interrupt_flag變量至1,該值將在PendSV中斷中使用。
186-194行,設置SysTick和PendSV中斷的優(yōu)先級,且觸發(fā)PendSV,但現(xiàn)在不跳轉,因為中斷為禁止。
197-201行,很有意思的一段操作,將0x08000000處的棧頂指針放置到MSP中,相當于特權模式的棧頂指針復位了。CPU從匯編編寫的啟動代碼,直到運行到此處,均在特權模式下運行,使用MSP作為棧頂指針。將來切換到線程后,會以PSP作為棧頂指針。啟動流程不會重來一次,也沒有任何函數(shù)再需要返回。所以,對于截止到目前使用的MSP棧,可以舍棄棧中的數(shù)據(jù),MSP棧重置。
204-205行,使能中斷。首先在context_gcc.S的89行設置斷點,然后當PC運行在204行時按F5,會運行至PendSV中斷服務程序。
Step 13. PendSV函數(shù)分析。
在PendSV中斷服務程序中:
94行-96行,判斷rt_thread_switch_interrupt_flag的值,為0則退出,為1則繼續(xù);
99行-105行,rt_thread_switch_interrupt_flag清0,判斷rt_interrupt_from_thread的值,為0表示OS第一次進行最高優(yōu)先級就緒狀態(tài)線程的運行,無需恢復psp,直接跳轉到switch_to_thread;為1表示從from線程切換至to線程,需要恢復psp。Debug到此處,rt_interrupt_from_thread的值為0,是第一次進行線程運行。
此處直接分析127行開始的switch_to_thread部分。
128行,將rt_interrupt_to_thread的地址賦值給r1。
129行,從r1指向的地址中取出值,賦值給r1,此時r1指向到main線程的thread->sp。
130行,從r1指向的地址中取出值,賦值給r1,此時r1指向到0x200018F4,如下圖所示。
133行-136行,將r1指向的0x200018F4開始的單元內容,依次裝載到r3, r4-r11中。執(zhí)行完畢后,R3中是flag的值,r4-r11中均為0xDEAFBEEF,且r1指向0x20001918。
139-140行,由于r3為0,浮點寄存器不做處理。r1保持不變。
143行,將r1的值賦值給PSP,線程棧頂指針PSP目前為0x20001918。后續(xù)PSP還會自動更新。
155行,使得LR寄存器的Bit2為1,確保PendSV異常返回使用的棧指針是PSP。
156行,異常返回。此時,線程棧中剩下內容,即從0x20001918-0x20001934的內容,會自動加載到R0, R1, R2, R3, R12, R14 (線程返回地址), PC (線程入口地址), xPSR。且,PSP會自動更新至0x20001938,即創(chuàng)建main線程時的棧頂指針。
Step 14. 光標在BX LR上時,按F5,自動運行到main線程入口地址main_thread_entry。
如下圖所示,棧幀中的r0-r15, xPSR均已順利從線程棧中進行了恢復,此時thread->sp = PSP = 0x20001938。開始順利執(zhí)行線程。
通過本文對線程啟動過程的了解,對于兩個線程/多個線程之間的互相切換能奠定堅實的基礎,化繁為簡,結合論壇關于上下文切換的代碼注釋,能幫助快速抓住主線。
使用的軟硬件環(huán)境如下:
IDE工具 - RT-Thread Studio 2.2.6
硬件 - STM32L431RCT6,Cortex M4內核
軟件 - RT-Thread 4.0.5版本
配置 - 僅使能main線程和tidle0線程
一、工程設置
Step 1. 新建名稱為EVBMX_RTThread405_Switch的4.0.5版本工程
Step 2. 不使能軟件定時器,使能線程狀態(tài)更改的調試
關閉軟件定時器線程,避免干擾。
Step 3. 關閉msh shell,禁用Finsh
關閉tshell線程,避免干擾。僅僅保留main線程和tidle0線程。
Step 4. 修改main函數(shù)
修改main函數(shù)后,線程進入一次,休眠且切換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. 下載程序,觀察輸出結果
讀完全文后,對下方輸出結果的每一行語句所代表的含義和發(fā)生時刻,能有更深刻體會。
二、調試運行
Step 6. 在component.c中257行按F9設置斷點;F5全速運行到此處后,再按F9關閉此處斷點。
Step 7. 依次進入rt_thread_create, _thread_init, 停留在thread.c的164行。
將變量thread添加到表達式窗口,可以查看各個成員的值,其中,thread->stack_addr = 0x20001138, thread->stack_size = 0x800,分別表示棧底位置和??臻g大小。
164行的函數(shù)rt_hw_stack_init對于理解線程切換是一個相當重要的函數(shù),其形參分別為:
線程入口函數(shù):main_thread_entry
線程參數(shù)RT_NULL:
線程棧棧頂?shù)刂罚簍hread->stack_addr + thread->stack_size - 4 = 0x20001138 + 0x800 - 4 = 0x20001934
Q1:為什么此處需要減4?
A2: 很有意思的一個問題。答案可參考本人在論壇的一個回答。RT-Thread-小白求助,關于rtt 的一段源碼RT-Thread問答社區(qū) - RT-Thread
Step 8. 單步進入到rt_hw_stack_init函數(shù)內部,開展分析
149行,由于傳遞進來的stack_addr = 0x20001934,執(zhí)行完畢后,stk為0x20001938。從0x20001138(含)到0x20001934(含),合計是0x800 = 2048字節(jié)。STM32使用的滿遞減棧,所以此處的stk是0x20001938。
150行,此處設置8字節(jié)對齊。由于0x20001938 = (536877368)Decimal,該數(shù)據(jù)除8等于67109671,能被8整除,該語句執(zhí)行棧對齊操作后,stk依然為0x20001938。
Step 9. 繼續(xù)了解rt_hw_stack_init函數(shù)。
151行,更新stk的值,減去struct stack_frame結構體的大小。執(zhí)行完畢后,stk = 0x200018F4。
153行,stack_frame指針指向0x200018F4。
156至159行,通過for循環(huán)將0x200018F4至0x20001938的所有內存變成0xdeadbeaf魔法字。
161行至168行,將stack_frame成員的exception_stack_frame中的r0~psr共8個寄存器分別設置為:線程參數(shù),4個0,線程返回地址,線程入口地址,0x01000000。
175行,返回stk的值,此時變成0x200018F4。這個值在初始化線程時,將返回給thread->sp,即線程棧的臨時棧頂指針。
依次將線程的形參、r1-r3, r12, 線程返回地址、線程入口地址,線程的xPSR寫入異常棧幀結構中。
在初入門時,這里是難點。C語言中使用結構體定義的棧結構,如何和實際寄存器的順序進行一一對應?,后文會通過逐步Debug揭示這個問題答案。
返回的stk指向0x200018F4部分。
至此,main線程創(chuàng)建完畢后,線程結構體和線程棧空間如下所示。
Step 10. 繼續(xù)單步到rt_system_scheduler_start函數(shù)處,并單獨跟蹤進入到該函數(shù)內部。
期間,RT-Thread會調用rt_thread_idle_init函數(shù),在該函數(shù)中使用靜態(tài)創(chuàng)建方式初始化tidle0線程。可以按照上述過程記錄tidle0線程的??臻g。
Step 11. 繼續(xù)單步到rt_hw_context_switch_to函數(shù)處。
在rt_system_scheduler_start函數(shù)中,會依次獲取最高優(yōu)先級線程的線程控制塊,將其復制給to_thread。如圖所示,在表達式窗口的to_thread就是main線程。
&to_thread->spthread->sp的地址,在Debug中,地址編號為0x200010C8,即0x200010C8內存單元中存放的數(shù)據(jù)是0x200018F4。
Q2. 在單獨進入到rt_hw_context_switch_to之前,觀察輸出結果,main線程被remove。為什么在啟動調度器的函數(shù)中,要先將線程從就緒列表中移除呢?
A2. 下一步要啟動main線程,將其從Ready狀態(tài)變成Running狀態(tài),所以需要將該線程從就緒列表中刪除,RT-Thread后續(xù)在調度時暫時不考慮該線程,直到該線程狀態(tài)再次從Running發(fā)生變化。
Step 12. 單步到進入到rt_hw_context_switch_to函數(shù)處,該函數(shù)位于context_gcc.S文件,由匯編語言編寫實現(xiàn)。
rt_hw_context_switch_to僅僅在調度器啟動時運行一次。該函數(shù)的C語言實現(xiàn)接口中,有一個參數(shù),傳入thread->sp變量的地址。
對于參數(shù)個數(shù)不大于4的C語言接口函數(shù),編譯器會按參數(shù)在列表中的順序,自左向右 為參數(shù)分配寄存器r0-r3。
對于參數(shù)個數(shù)大于4的C語言接口函數(shù),編譯器會按參數(shù)在列表中的順序,多余參數(shù)按自右向左的順序壓入棧中,即參數(shù)入棧順序與參數(shù)順序相反。
如上述Tips,thread->sp的地址通過r0傳遞。在下圖左側寄存器窗口中,可以看到r0的值為0x200010C8。
165行,將變量rt_interrupt_to_thread變量的地址賦值給r1。
165行,將r0的值賦值給r1指向的單元,即將r0的值賦值給變量rt_interrupt_to_thread。如果此時在表達式窗口觀察rt_interrupt_to_thread,會發(fā)現(xiàn)它的值為0x200010C8。
此時,main線程的線程結構體和線程??臻g不變,但是r0, r1, rt_interrupr_to_thread的內容均發(fā)生了變化。
對于rt_hw_context_switch_to函數(shù)的其他行,依次分析如下:
168行至172行,處理浮點寄存器入??刂?,與Cortex M4內核的Lazy Stacking有關,但與本文主線無關,不做探討。
176至178行,將rt_interrupt_from_thread變量清零。因此本次是RT-Thread第一次調度最高優(yōu)先級線程,只有to,沒有from。
181至183行,將rt_thread_switch_interrupt_flag變量至1,該值將在PendSV中斷中使用。
186-194行,設置SysTick和PendSV中斷的優(yōu)先級,且觸發(fā)PendSV,但現(xiàn)在不跳轉,因為中斷為禁止。
197-201行,很有意思的一段操作,將0x08000000處的棧頂指針放置到MSP中,相當于特權模式的棧頂指針復位了。CPU從匯編編寫的啟動代碼,直到運行到此處,均在特權模式下運行,使用MSP作為棧頂指針。將來切換到線程后,會以PSP作為棧頂指針。啟動流程不會重來一次,也沒有任何函數(shù)再需要返回。所以,對于截止到目前使用的MSP棧,可以舍棄棧中的數(shù)據(jù),MSP棧重置。
204-205行,使能中斷。首先在context_gcc.S的89行設置斷點,然后當PC運行在204行時按F5,會運行至PendSV中斷服務程序。
Step 13. PendSV函數(shù)分析。
在PendSV中斷服務程序中:
94行-96行,判斷rt_thread_switch_interrupt_flag的值,為0則退出,為1則繼續(xù);
99行-105行,rt_thread_switch_interrupt_flag清0,判斷rt_interrupt_from_thread的值,為0表示OS第一次進行最高優(yōu)先級就緒狀態(tài)線程的運行,無需恢復psp,直接跳轉到switch_to_thread;為1表示從from線程切換至to線程,需要恢復psp。Debug到此處,rt_interrupt_from_thread的值為0,是第一次進行線程運行。
此處直接分析127行開始的switch_to_thread部分。
128行,將rt_interrupt_to_thread的地址賦值給r1。
129行,從r1指向的地址中取出值,賦值給r1,此時r1指向到main線程的thread->sp。
130行,從r1指向的地址中取出值,賦值給r1,此時r1指向到0x200018F4,如下圖所示。
133行-136行,將r1指向的0x200018F4開始的單元內容,依次裝載到r3, r4-r11中。執(zhí)行完畢后,R3中是flag的值,r4-r11中均為0xDEAFBEEF,且r1指向0x20001918。
139-140行,由于r3為0,浮點寄存器不做處理。r1保持不變。
143行,將r1的值賦值給PSP,線程棧頂指針PSP目前為0x20001918。后續(xù)PSP還會自動更新。
155行,使得LR寄存器的Bit2為1,確保PendSV異常返回使用的棧指針是PSP。
156行,異常返回。此時,線程棧中剩下內容,即從0x20001918-0x20001934的內容,會自動加載到R0, R1, R2, R3, R12, R14 (線程返回地址), PC (線程入口地址), xPSR。且,PSP會自動更新至0x20001938,即創(chuàng)建main線程時的棧頂指針。
Step 14. 光標在BX LR上時,按F5,自動運行到main線程入口地址main_thread_entry。
如下圖所示,棧幀中的r0-r15, xPSR均已順利從線程棧中進行了恢復,此時thread->sp = PSP = 0x20001938。開始順利執(zhí)行線程。
三、修改rt_hw_context_switch_to函數(shù),使用SVC進入第一個線程
FreeRTOS使用SVC進入第一個線程,通過簡單修改,在STM32L431RCT6 Cortex-M4內核上也可以支持用SVC進入第一個線程。 計劃在線下課程中,與學生們面對面深入探討一次。
對rt_hw_context_switch_to函數(shù)的修改過程如下:
刪除對rt_interrupt_from_thread的清零
刪除對rt_thread_switch_interrupt_flag的置1
刪除對PendSV的觸發(fā)
新增dsb isb
新增SVC 0
毫無意義,對R0賦值,通過Debug觀察到該語句不會被執(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
四、小結
本文簡單探討了RT-Thread 4.0.5版本在STM32L431RCTx Cortex-M4內核上,創(chuàng)建main線程、tidle0線程后,從使用MSP的特權模式,啟動至使用PSP線程模式的main線程棧幀恢復全過程。
SP寄存器有兩個,分別是MSP和PSP,其中,從復位啟動后使用MSP,通過啟動代碼、RT-Thread初始化、啟動調度器的過程,切換至使用PSP的線程中運行。
每個線程均有獨立的棧。使用rt_thread_create創(chuàng)建的線程,棧位于heap中;使用rt_thread_init創(chuàng)建的棧,棧位于自定義的數(shù)組中。
線程切換,即保存所有寄存器的快照到線程棧中,r0-r15, xPSR,浮點寄存器。線程恢復,即從線程棧中恢復寄存器快照。
在線程模式下,如果發(fā)生中斷,會繼續(xù)使用MSP。
Cortex M4發(fā)生中斷,會有系列寄存器自動入棧處理的操作,本文不展開討論。
RT-Thread的上下文切換的Context_gcc.S文件中rt_hw_context_switch_to也可以用SVC進行線程處理。
-
寄存器
+關注
關注
31文章
5301瀏覽量
119861 -
Cortex-M4
+關注
關注
6文章
89瀏覽量
46494 -
SVC
+關注
關注
0文章
33瀏覽量
12100 -
RT-Thread
+關注
關注
31文章
1265瀏覽量
39852 -
STM32L4
+關注
關注
1文章
42瀏覽量
9376
發(fā)布評論請先 登錄
相關推薦
評論