1.簡介
本文主要介紹RT Thread操作系統在cortex-m3內核上的移植接口文件,通過本篇博客你將深入瞭解RTOS操作系統是怎麼通過觸發軟中斷實現任務切換的,怎麼實現內核異常信息的打印功能。
2.移植的接口文件
RT-Thread操作系統的移植接口文件主要用cpuport.c
,context_rvds.s
,backtrace.c
,div0.c
,showmem.c
。其中最重要的文件是cpuport.c
和context_rvds.s
這兩個文件,其他三個文件在cortex-M3內核移植時沒有實際的應用,這三個文件實際一些輔助的功能,打印內存,除數爲0,後臺跟蹤等操作,內容很簡單,可以自行查看。
3.任務切換context_rvds.s
這是一個彙編語言的文件,這個文件實現了任務切換,觸發軟件中斷,硬件異常錯誤處理等操作,是操作系統移植時要實現的最重要的功能。程序的內部邏輯根cortex-m3內核的編程模型有關,想了解此程序邏輯,需要對cortex-m3內核的編程模塊有一定的瞭解。
操作系統進行初始化芯片的時鐘,必要的外設後,開始進行第一個任務/線程調度時,會調用rt_hw_context_switch_to
,函數的輸入參數是進行切換的任務(線程)的堆棧指針。這個函數的具體功能如下:
請詳細查看我增加的中文註釋,有你不會的乾貨。
1;/*
2; * void rt_hw_context_switch_to(rt_uint32 to);
3; * r0 --> to
4; * this fucntion is used to perform the first thread switch
5; */
6rt_hw_context_switch_to PROC
7 EXPORT rt_hw_context_switch_to
8 ; set to thread
9 ; 把要切換到的線程的堆棧指針記錄到變量rt_interrupt_to_thread中來
10 LDR r1, =rt_interrupt_to_thread
11 STR r0, [r1]
12
13 ; set from thread to 0
14 ; 第一次進行線程切換,沒有上一次切換所以把rt_interrupt_from_thread設置爲0
15 LDR r1, =rt_interrupt_from_thread
16 MOV r0, #0x0
17 STR r0, [r1]
18
19 ; set interrupt flag to 1
20 ; 進行線程切換,把線程切換標誌變量rt_thread_switch_interrupt_flag設置爲1
21 LDR r1, =rt_thread_switch_interrupt_flag
22 MOV r0, #1
23 STR r0, [r1]
24
25 ; set the PendSV exception priority
26 ; 設置pendsv軟件中斷的優先級爲最低
27 LDR r0, =NVIC_SYSPRI2
28 LDR r1, =NVIC_PENDSV_PRI
29 LDR.W r2, [r0,#0x00] ; read
30 ORR r1,r1,r2 ; modify
31 STR r1, [r0] ; write-back
32
33 ; trigger the PendSV exception (causes context switch)
34 ;觸發pendsv軟件中斷,此時中斷關閉,並不會產生中斷
35 LDR r0, =NVIC_INT_CTRL
36 LDR r1, =NVIC_PENDSVSET
37 STR r1, [r0]
38
39 ; restore MSP
40 ;這段代碼實際是可以沒有的,移植時這樣做有了一個好處就是增加了MSP堆棧的使用空間
41 ;cortex-m3內核復位時使用msp堆棧,從復位到進行初始化操作時會調用很多函數,會進行一些壓棧操作
42 ;佔用一部分msp堆棧,由於程序不會退出到復位的位置,壓棧佔用的msp空間永遠不會釋放,產生了堆棧的
43 ;的空間浪費一小部分。
44 ; 下面的代碼實現的功能是讀取SCB_VTOR寄存器,這個寄存器保存了中斷向量表的起始位置,此位置的字
45 ; 就是MSP堆棧的指針,即啓動代碼裏面分配出來的堆棧的棧頂。經過2次 LDR r0, [r0]就是相當於取到
46 ;堆棧的棧頂,最後設置msp爲棧頂
47 LDR r0, =SCB_VTOR
48 LDR r0, [r0]
49 LDR r0, [r0]
50 MSR msp, r0
51
52 ; enable interrupts at processor level
53 ; 打開中斷
54 CPSIE F
55 CPSIE I
56
57 ; never reach here!
58 ENDP
在已經進行了一次線程切換後,再次進行線程切換時會調用void rt_hw_context_switch(rt_uint32 from, rt_uint32 to)
這個函數,這個函數與上面rt_hw_context_switch_to
函數的功能相比大同小異。
1;/*
2; * void rt_hw_context_switch(rt_uint32 from, rt_uint32 to);
3; * r0 --> from
4; * r1 --> to
5; */
6rt_hw_context_switch_interrupt
7 EXPORT rt_hw_context_switch_interrupt
8rt_hw_context_switch PROC
9 EXPORT rt_hw_context_switch
10
11 ; set rt_thread_switch_interrupt_flag to 1
12 ; 判斷線程切換標誌rt_thread_switch_interrupt_flag是否爲1
13 LDR r2, =rt_thread_switch_interrupt_flag
14 LDR r3, [r2]
15 CMP r3, #1
16 BEQ _reswitch
17 ;不爲1時,設置爲1,把from的線程的堆棧指針記錄到rt_interrupt_from_thread中
18 MOV r3, #1
19 STR r3, [r2]
20
21 LDR r2, =rt_interrupt_from_thread ; set rt_interrupt_from_thread
22 STR r0, [r2]
23
24_reswitch
25 ;爲1時,把to的線程的堆棧指針記錄到rt_interrupt_to_thread中來
26 LDR r2, =rt_interrupt_to_thread ; set rt_interrupt_to_thread
27 STR r1, [r2]
28
29 ;觸發pendsv中斷
30 LDR r0, =NVIC_INT_CTRL ; trigger the PendSV exception (causes context switch)
31 LDR r1, =NVIC_PENDSVSET
32 STR r1, [r0]
33 BX LR
34 ENDP
pendsv中斷是真正進行了線程切換操作的,前面介紹的2個函數主要在進行線程切換前,把要切換的線程的堆棧指針記錄到這個彙編的程序的變量中,在pendsv中斷中進行線程切換時使用,並且觸發中斷。下面介紹pendsv中斷內部實現線程切換的原理,一定要仔細看呀。
1; r0 --> switch from thread stack
2; r1 --> switch to thread stack
3; psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
4PendSV_Handler PROC
5 EXPORT PendSV_Handler
6 ;根據cortext-m3內核的編程模型,進行pendsv中斷前,內核已經自動的psr, pc, lr, r12, r3, r2, r1, r0把這些寄存器壓入到發生切換的線程的堆棧psp中的去了,跳到中斷程序,使用的堆棧自動切換成msp
7
8 ; disable interrupt to protect context switch
9 ; 記錄primask中斷開關寄存器的值到r2寄存器中,用於退出中斷後再打開中斷用
10 MRS r2, PRIMASK
11 ;關閉中斷
12 CPSID I
13
14 ; get rt_thread_switch_interrupt_flag
15 ; 判斷rt_thread_switch_interrupt_flag標誌爲1時,才進行線程切換,爲0時直接退出中斷
16 LDR r0, =rt_thread_switch_interrupt_flag
17 LDR r1, [r0]
18 CBZ r1, pendsv_exit ; pendsv already handled
19
20 ; clear rt_thread_switch_interrupt_flag to 0
21 ; 進行線程切換,清除rt_thread_switch_interrupt_flag變量
22 MOV r1, #0x00
23 STR r1, [r0]
24
25 ;判斷從哪個線程切換出去,要切換到第一個線程時,rt_interrupt_from_thread變量爲0
26 ;判斷此變量爲0表示切入第一個線程
27 LDR r0, =rt_interrupt_from_thread
28 LDR r1, [r0]
29 CBZ r1, switch_to_thread ; skip register save at the first time
30
31 ;不爲0時,把r4-r11這8個寄存器保存到當前要切換出去的線程堆棧psp中去,並且把當前線程的堆棧指針psp記錄到rt_interrupt_from_thread變量中來
32 MRS r1, psp ; get from thread stack pointer
33 STMFD r1!, {r4 - r11} ; push r4 - r11 register
34 LDR r0, [r0]
35 STR r1, [r0] ; update from thread stack pointer
36
37switch_to_thread
38 ;把要切入的線程的堆棧指針取出到r1寄存器中
39 LDR r1, =rt_interrupt_to_thread
40 LDR r1, [r1]
41 LDR r1, [r1] ; load thread stack pointer
42
43 ;從要切入線程堆棧中彈出這個線程中的寄存器r4-r11,把線程堆棧指針賦值到psp中。
44 LDMFD r1!, {r4 - r11} ; pop r4 - r11 register
45 MSR psp, r1 ; update stack pointer
46
47pendsv_exit
48 ; restore interrupt
49 ; 打開中斷
50 MSR PRIMASK, r2
51
52 ;cortex-m3內核中發生中斷時,在中斷程序中使用的堆棧是msp,操作系統線程設計使用的是psp線程,所以上面的線程切換就是實現是兩個線程的堆棧指針的切換,即把當前線程的堆棧psp保存到rt_interrupt_from_thread
53爲量中,把要切入的堆棧賦值到psp中去。
54 ;由於中斷中使用的msp堆棧,退出中斷是如果不做任何操作還是使用msp堆棧,而線程使用的是psp堆棧,所以對lr寄存的位3進行置1就控制退出中斷後使用psp中斷。
55 ORR lr, lr, #0x04
56 BX lr
57 ENDP
線程切換的核心就在上面的代碼註釋中,不懂的話要多看幾次同時參考cortex-m3的內核編程手冊來看。上面的代碼主要實現的是對切換進入和切換退出的線程堆棧指針的變換,即保存當前線程的psp,把要切入的線程的堆棧指針賦值到psp中去。
可能讀者關心線程切換不僅要切換線程的上下文,還要從一個線程跳到另外一個線程,這個是怎麼實現的呢?
從一個線程跳到另外一個線程中上面的代碼確實沒有實現,實際是靠cortex-m3內核自動完成的。發生pendsv中斷前,內核硬件自動(不用程序操作)把當前線程的上下文(psr, pc, lr, r12, r3, r2, r1, r0)
壓入線程自己的堆棧,可以看到發生中斷時的程序位置的pc指針已經自動保存到堆棧中,pendsv中斷程序把新切入的線程堆棧換到psp中,當中斷程序退出,新切入的線程的中斷上下文(psr, pc, lr, r12, r3, r2, r1, r0)
會自動(硬件執行,不用程序)的從線程中彈出,程序指針pc就獲得了新線程的pc和這個線程中使用的寄存器的值,程序就運行到新線程中去了。這就是cortex-m3線程切換的核心與精髓,你明白了麼?
還有兩個函數rt_hw_interrupt_enable
和rt_hw_interrupt_disable
是實現開中斷和關中斷,功能很簡單,不再詳細描述。
harfault_Handler中斷中當發生了硬件錯誤中斷時,比如非法內存訪問,外設初始化,操作非法會發生。這個中斷函數中實現了出打印出發生中斷異常點的函數的指針。
1HardFault_Handler PROC
2
3 ; get current context
4 ;中斷程序中lr表示的是EXC_RETURN寄存器的狀態,這個寄存器的位2表示進入中斷前使用的是psp還是
5 ;msp堆棧,在rt-thread中,如果硬件錯誤中斷髮生在線程中使用的是psp,如果是從另外一箇中斷髮生
6 ;硬件故障產生的中斷,使用的是msp
7 TST lr, #0x04 ; if(!EXC_RETURN[2])
8 ITE EQ
9 ;把發生中斷前的堆棧指針賦值到r0寄存器中去
10 MRSEQ r0, msp ; [2]=0 ==> Z=1, get fault context from handler.
11 MRSNE r0, psp ; [2]=1 ==> Z=0, get fault context from thread.
12
13 ;手動把r4-r11壓入堆棧中,再壓入lr寄存器,記住這裏多壓入了9個寄存器的值
14 ; 這裏這樣操作的原因是爲了rt_hw_hard_fault_exception函數中定義的結構體能對齊訪問到全部寄存器
15
16 STMFD r0!, {r4 - r11} ; push r4 - r11 register
17 STMFD r0!, {lr} ; push exec_return register
18
19 TST lr, #0x04 ; if(!EXC_RETURN[2])
20 ITE EQ
21 ;上面壓完堆棧後,把更新後的堆棧指針重新寫入到psp或msp中去,r0寄存器保存的是發生hardfault中
22 ; 斷前使用的堆棧指針,做爲參數會傳入函數rt_hw_hard_fault_exception中去
23 MSREQ msp, r0 ; [2]=0 ==> Z=1, update stack pointer to MSP.
24 MSRNE psp, r0 ; [2]=1 ==> Z=0, update stack pointer to PSP.
25
26 PUSH {lr}
27 BL rt_hw_hard_fault_exception
28 POP {lr}
29
30 ORR lr, lr, #0x04
31 BX lr
32 ENDP
33
34 ALIGN 4
35
36 END
4.cpu接口程序cpuport.c
這個程序主要有2個函數void rt_hw_hard_fault_exception(struct exception_info * exception_info),rt_uint8_t *rt_hw_stack_init(void *tentry,void *parameter, rt_uint8_t *stack_addr,void *texit)比較重要,另外幾個函數的功能都很簡單不做詳細介紹。
rt_hw_hard_fault_exception
函數中實現打印發生錯誤中斷前的程序的位置的上下位,即發生中斷時程序的出現故障的位置。還記得上面的程序段中如下的這些操作,這些操作是向堆中多壓入了9個寄存器,進入此函數中使用結構體來struct exception_info
來進行訪問使用的。
1;手動把r4-r11壓入堆棧中,再壓入lr寄存器,記住這裏多壓入了9個寄存器的值
2; 這裏這樣操作的原因是爲了rt_hw_hard_fault_exception函數中定義的結構體能對齊訪問到全部寄存器
1 STMFD r0!, {r4 - r11} ; push r4 - r11 register
2 STMFD r0!, {lr} ; push exec_return register
結構體struct exception_info的定義如下,
1struct exception_stack_frame
2{
3 rt_uint32_t r0;
4 rt_uint32_t r1;
5 rt_uint32_t r2;
6 rt_uint32_t r3;
7 rt_uint32_t r12;
8 rt_uint32_t lr;
9 rt_uint32_t pc;
10 rt_uint32_t psr;
11};
12
13struct stack_frame
14{
15 /* r4 ~ r11 register */
16 rt_uint32_t r4;
17 rt_uint32_t r5;
18 rt_uint32_t r6;
19 rt_uint32_t r7;
20 rt_uint32_t r8;
21 rt_uint32_t r9;
22 rt_uint32_t r10;
23 rt_uint32_t r11;
24
25 struct exception_stack_frame exception_stack_frame;
26};
27
28struct exception_info
29{
30 rt_uint32_t exc_return;
31 struct stack_frame stack_frame;
32};
從結構體的定義可以看出r0成員變量前面還有exc_return,r4-r11這9個成員變量,所以手動向堆棧中壓入9個寄存器,使用這個結構體來訪問發生中斷前的程序位置的pc,通過pc值就能找到哪段程序發生了錯誤中斷。
1void rt_hw_hard_fault_exception(struct exception_info * exception_info)
2{
3 extern long list_thread(void);
4 struct stack_frame* context = &exception_info->stack_frame;
5
6 if (rt_exception_hook != RT_NULL)
7 {
8 rt_err_t result;
9
10 result = rt_exception_hook(exception_info);
11 if (result == RT_EOK)
12 return;
13 }
14
15 rt_kprintf("psr: 0x%08x\n", context->exception_stack_frame.psr);
16
17 rt_kprintf("r00: 0x%08x\n", context->exception_stack_frame.r0);
18 rt_kprintf("r01: 0x%08x\n", context->exception_stack_frame.r1);
19 rt_kprintf("r02: 0x%08x\n", context->exception_stack_frame.r2);
20 rt_kprintf("r03: 0x%08x\n", context->exception_stack_frame.r3);
21 rt_kprintf("r04: 0x%08x\n", context->r4);
22 rt_kprintf("r05: 0x%08x\n", context->r5);
23 rt_kprintf("r06: 0x%08x\n", context->r6);
24 rt_kprintf("r07: 0x%08x\n", context->r7);
25 rt_kprintf("r08: 0x%08x\n", context->r8);
26 rt_kprintf("r09: 0x%08x\n", context->r9);
27 rt_kprintf("r10: 0x%08x\n", context->r10);
28 rt_kprintf("r11: 0x%08x\n", context->r11);
29 rt_kprintf("r12: 0x%08x\n", context->exception_stack_frame.r12);
30 rt_kprintf(" lr: 0x%08x\n", context->exception_stack_frame.lr);
31 rt_kprintf(" pc: 0x%08x\n", context->exception_stack_frame.pc);
32
33 if(exception_info->exc_return & (1 << 2) )
34 {
35 rt_kprintf("hard fault on thread: %s\r\n\r\n", rt_thread_self()->name);
36
37#ifdef RT_USING_FINSH
38 list_thread();
39#endif /* RT_USING_FINSH */
40 }
41 else
42 {
43 rt_kprintf("hard fault on handler\r\n\r\n");
44 }
45
46#ifdef RT_USING_FINSH
47 hard_fault_track();
48#endif /* RT_USING_FINSH */
49
50 while (1);
51}
rt_hw_stack_init
函數在創建線程時,對分配的線程的堆棧進行初始化,一個線程中使用全部的cortex-m3
的16個寄存器,所以這個函數在線程的堆棧的棧頂位置向下的16個字進行初始化,按照內核進入中斷時壓入堆棧的寄存器順序排列進行初始化,特別說明一下lr是返回地址,即線程退出後返回到rt_thread_exit
函數中,pc是線程的入口函數地址。
1/**
2 * This function will initialize thread stack
3 *
4 * @param tentry the entry of thread
5 * @param parameter the parameter of entry
6 * @param stack_addr the beginning stack address
7 * @param texit the function will be called when thread exit
8 *
9 * @return stack address
10 */
11rt_uint8_t *rt_hw_stack_init(void *tentry,
12 void *parameter,
13 rt_uint8_t *stack_addr,
14 void *texit)
15{
16 struct stack_frame *stack_frame;
17 rt_uint8_t *stk;
18 unsigned long i;
19
20 stk = stack_addr + sizeof(rt_uint32_t);
21 stk = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);
22 stk -= sizeof(struct stack_frame);
23
24 stack_frame = (struct stack_frame *)stk;
25
26 /* init all register */
27 for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
28 {
29 ((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
30 }
31
32 stack_frame->exception_stack_frame.r0 = (unsigned long)parameter; /* r0 : argument */
33 stack_frame->exception_stack_frame.r1 = 0; /* r1 */
34 stack_frame->exception_stack_frame.r2 = 0; /* r2 */
35 stack_frame->exception_stack_frame.r3 = 0; /* r3 */
36 stack_frame->exception_stack_frame.r12 = 0; /* r12 */
37 stack_frame->exception_stack_frame.lr = (unsigned long)texit; /* lr */
38 stack_frame->exception_stack_frame.pc = (unsigned long)tentry; /* entry point, pc */
39 stack_frame->exception_stack_frame.psr = 0x01000000L; /* PSR */
40
41 /* return task's current stack address */
42 return stk;
43}
1
至此已經完成了全部cortext-m3內核移植部分的關鍵代碼的講解,如有不懂的地方可以在下面留言。
作者:fhqmcu
來源:RT-Thread論壇
https://www.rt-thread.org/qa/thread-423637-1-1.html