1、背景
本文章主要說明 rtthread 內核執行緒是如何切換的,初學者剛從裸機開發接觸 RTOS 時難免會有些不適應,明白這部分原理之后就會對 RTOS 有更深的理解,在學習內核執行緒切換原理之前需要有以下基礎知識鋪墊,本文以 arm 公司的 Cortex-M3 內核為例,
2、基礎知識
-
CM3 擁有通用暫存器 R0-R15 以及一些特殊功能暫存器(中斷屏蔽暫存器等等)
-
R0-R12 都是通用暫存器,用來臨時存盤程式運行時產生的資料
-
R13 這個暫存器存盤堆疊指標,在 CM3 內核中一共有兩個堆疊指標(MSP、PSP),于是 CM3 支持兩個堆疊,在啟動檔案中定義的那個堆疊空間屬于主堆疊,還有一個在我們創建執行緒時的堆疊屬于執行緒堆疊,這兩個堆疊空間不是同一個空間,
主堆疊指標(MSP),這是默認的堆疊指標,在裸機開發中只是用這一個指標,由 OS 內核、中斷服務程式以及所有需要特權訪問的應用程式代碼使用,
行程堆疊指標(PSP),用于常規的應用程式代碼,比如執行緒,
-
R14 也叫做連接暫存器LR,在呼叫子程式時存盤回傳地址
-
R15 也叫做程式計數器 (PC,program counter),因為 CM3 內部使用了指令流水線,PC 中存放的是當前指令的地址+4,也就是下一條指令的地址,
-
堆疊空間的定義 : 向下生長的堆疊,也就是說每次執行一個 push(壓堆疊)命令,堆疊指標向下減小一個單元,每次執行pop命令,堆疊指標增加一個單元,如下圖所示

3、代碼分析
3.1 內核暫存器結構體定義
struct exception_stack_frame
{
rt_uint32_t r0;
rt_uint32_t r1;
rt_uint32_t r2;
rt_uint32_t r3;
rt_uint32_t r12;
rt_uint32_t lr;
rt_uint32_t pc;
rt_uint32_t psr;
};
struct stack_frame
{
/* r4 ~ r11 register */
rt_uint32_t r4;
rt_uint32_t r5;
rt_uint32_t r6;
rt_uint32_t r7;
rt_uint32_t r8;
rt_uint32_t r9;
rt_uint32_t r10;
rt_uint32_t r11;
struct exception_stack_frame exception_stack_frame;
};
struct exception_info
{
rt_uint32_t exc_return;
struct stack_frame stack_frame;
};
3.2 初始化執行緒堆疊
rt_uint8_t *rt_hw_stack_init(void *tentry, //執行緒函式入口地址
void *parameter,//執行緒函式引數
rt_uint8_t *stack_addr,//堆疊地址
void *texit)//執行緒退出時的函式地址
{
struct stack_frame *stack_frame;
rt_uint8_t *stk;
unsigned long i;
?
stk = stack_addr + sizeof(rt_uint32_t);//堆疊地址 + 4 個位元組
stk = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);//向下8個位元組對齊
stk -= sizeof(struct stack_frame);//偏移16個字(16*4個位元組)
?
stack_frame = (struct stack_frame *)stk;//強制轉換為 struct stack_frame 型別
?
/* init all register */
for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
{
((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;//初始化這16個字的空間為 0xdeadbeef
}
/* 初始化高8個字的記憶體空間 */
stack_frame->exception_stack_frame.r0 = (unsigned long)parameter; /* r0 : argument */
stack_frame->exception_stack_frame.r1 = 0; /* r1 */
stack_frame->exception_stack_frame.r2 = 0; /* r2 */
stack_frame->exception_stack_frame.r3 = 0; /* r3 */
stack_frame->exception_stack_frame.r12 = 0; /* r12 */
stack_frame->exception_stack_frame.lr = (unsigned long)texit; /* lr */
stack_frame->exception_stack_frame.pc = (unsigned long)tentry; /* entry point, pc */
stack_frame->exception_stack_frame.psr = 0x01000000L; /* PSR */
?
#if USE_FPU
stack_frame->flag = 0;
#endif /* USE_FPU */
?
/* return task's current stack address */
return stk;
}
-
stack_addr這個引數為當前執行緒堆疊的結束地址,也就是最高的地址,為什么是最高地址?原因是上面說過的堆疊空間的定義, -
struct stack_frame這個結構體的定義可不是胡亂定義的,里面是有順序要求的, -
stk -= sizeof(struct stack_frame);//偏移16個字(16*4個位元組)為何偏移這么多位元組,因為這16個字的空間的每個地址要按照結構體成員變數的地址去存放,即 psr 要放到這個堆疊的最高地址,r4 在最低的地址,如圖所示,此圖出自野火,
3.3 執行執行緒切換
閱讀這段代碼之前得知道,cm3 內核執行中斷或例外時,r0、r1、r2、r3、r12、lr、pc、psr,這些暫存器是自動壓堆疊的,
rt_hw_context_switch PROC
EXPORT rt_hw_context_switch ;匯出函式,此操作能夠讓C側代碼呼叫,C側的第一個引數為當前執行緒堆疊sp的指標,第二個
;為將要執行的執行緒堆疊 sp 的指標
; set rt_thread_switch_interrupt_flag to 1
LDR r2, =rt_thread_switch_interrupt_flag;中斷標志位 L2 = &rt_thread_switch_interrupt_flag
LDR r3, [r2];r3 = *r2也就是 r3 = rt_thread_switch_interrupt_flag
CMP r3, #1 ;判斷rt_thread_switch_interrupt_flag 與 1是否相等
BEQ _reswitch ;相等跳轉 _reswitch,當第2次執行執行緒切換時,rt_thread_switch_interrupt_flag被pendsv置0
;既然是第二次,所以當前執行緒具有上文所以要把sp存到rt_interrupt_from_thread,直接跳轉_reswitch
;表示的是第一次切換執行緒,因為沒有上文,所以直接跳到 _reswitch
MOV r3, #1 ;不等則置1
STR r3, [r2] ;rt_thread_switch_interrupt_flag = 1
?
LDR r2, =rt_interrupt_from_thread ; set rt_interrupt_from_thread
STR r0, [r2] ;rt_interrupt_from_thread = r0,&sp,當前執行緒sp的地址
?
_reswitch
LDR r2, =rt_interrupt_to_thread ; set rt_interrupt_to_thread
STR r1, [r2] ;rt_interrupt_to_thread = r1,&sp,將要只要的執行緒的sp的地址
;觸發 pendsv 中斷,執行緒切換的核心
LDR r0, =NVIC_INT_CTRL ; trigger the PendSV exception (causes context switch)
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]
BX LR
ENDP
?
; r0 --> switch from thread stack
; r1 --> switch to thread stack
; psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
PendSV_Handler PROC
EXPORT PendSV_Handler
?
; 關閉所有中斷以保護這一程序不被打斷
MRS r2, PRIMASK
CPSID I
?
; rt_thread_switch_interrupt_flag 為 1時才繼續接下來的操作,為0則跳轉 pendsv_exit
LDR r0, =rt_thread_switch_interrupt_flag
LDR r1, [r0]
CBZ r1, pendsv_exit ; pendsv already handled
?
; 清楚中斷標志位
MOV r1, #0x00
STR r1, [r0]
;判斷 rt_interrupt_from_thread 是否為0,即是否是第一次切換執行緒,是0則跳轉至switch_to_thread
LDR r0, =rt_interrupt_from_thread
LDR r1, [r0]
CBZ r1, switch_to_thread ; skip register save at the first time
?
MRS r1, psp ; 獲取當前執行緒堆疊指標到r1中
STMFD r1!, {r4 - r11} ; 將r4 - r11暫存器中的值壓入當前堆疊空間中
LDR r0, [r0]
STR r1, [r0] ; 把當前執行緒堆疊指標記錄到 rt_interrupt_from_thread 中,即當前堆疊指標 sp 中
?
switch_to_thread
LDR r1, =rt_interrupt_to_thread;獲取將要執行的堆疊的sp的地址
LDR r1, [r1]
LDR r1, [r1]
?
LDMFD r1!, {r4 - r11} ; 從將要執行的堆疊中彈出這個執行緒中的暫存器r4-r11
MSR psp, r1 ; 并把要執行的執行緒的堆疊指標給到 psp
?
pendsv_exit
; 恢復中斷
MSR PRIMASK, r2
;由于cm3 內核發生中斷時,堆疊指標使用的是msp,因此退出中斷時,確保使用psp指標,實際操作就是對,lr寄存的位3進行置1就控制 ; 退出中斷后使用psp中斷
ORR lr, lr, #0x04
BX lr ;退出中斷時使用psp指標
ENDP
-
通過解讀 pendsv 中斷代碼我們知道,在進入 pendsv 中斷前,r0、r1、r2、r3、r12、lr、pc、psr 這些暫存器已經自動壓入了當前堆疊中,
-
當 pendsv 中斷退出時,新的將要執行的執行緒的中斷背景關系(r0、r1、r2、r3、r12、lr、pc、ps)會自動的從這個執行緒堆疊中彈出,程式計數器 PC 就得到了這個將要執行的執行緒的pc值,這個執行緒中用到的其他暫存器的值也從這個新的執行緒堆疊中得到了(一部分手動pop,一部分自動pop),
問題點一:我可以通過這個執行緒堆疊指標訪問到R0~R15的值嗎?
-
答案是肯定的,因為我們傳入的 sp 地址就指向了執行緒堆疊地址的偏移16個字處,而內核壓堆疊時,先自動壓入 r0、r1、r2、r3、r12、lr、pc、psr 這8個字的空間,按照順序壓,先壓psr,然后我們手動壓 r4 - r11 ,也是按照順序壓,先壓r11,此時這16個字的空間就被填滿了,這也是為什么執行緒堆疊結構體中的成員變數的順序不是隨便填的(個人理解),
問題點二:當我進入hard_fault 例外時,我能否獲取到當前執行緒堆疊指標,從而拿到 pc 指標來判斷程式出錯的位置?
-
答案是可以的,rt-thread 已經幫我們重寫了 hard_fault 服務程式,其原理請看下回分解.................
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/401690.html
標籤:其他
上一篇:【Leetcode資料結構演算法題】輪轉陣列(順序表篇)
下一篇:源火星球 青龍羊毛簡易教學
