內核移植
- 內核移植就是指將 RT-Thread 內核在不同的芯片架構、不同的板卡上運行起來,能夠具備線程管理和調度,內存管理,線程間同步和通信、定時器管理等功能。移植可分為 CPU 架構移植和 BSP(Board support package,板級支持包)移植兩部分。
CPU移植
- 為了使 RT-Thread 能夠在不同 CPU 架構的芯片上運行,RT-Thread 提供了一個 libcpu 抽象層來適配不同的 CPU 架構。libcpu 層向上對內核提供統一的接口,包括全局中斷的開關,線程棧的初始化,上下文切換等。
- RT-Thread 的 libcpu 抽象層向下提供了一套統一的 CPU 架構移植接口,這部分接口包含了全局中斷開關函數、線程上下文切換函數、時鍾節拍的配置和中斷函數、Cache 等等內容。下表是 CPU 架構移植需要實現的接口和變量。
-
函數和變量 描述 rt_base_t rt_hw_interrupt_disable(void); 關閉全局中斷 void rt_hw_interrupt_enable(rt_base_t level); 打開全局中斷 rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter, rt_uint8_t *stack_addr, void *texit); 線程棧的初始化,內核在線程創建和線程初始化里面會調用這個函數 void rt_hw_context_switch_to(rt_uint32 to); 沒有來源線程的上下文切換,在調度器啟動第一個線程的時候調用,以及在 signal 里面會調用 void rt_hw_context_switch(rt_uint32 from, rt_uint32 to); 從 from 線程切換到 to 線程,用於線程和線程之間的切換 void rt_hw_context_switch_interrupt(rt_uint32 from, rt_uint32 to); 從 from 線程切換到 to 線程,用於中斷里面進行切換的時候使用 rt_uint32_t rt_thread_switch_interrupt_flag; 表示需要在中斷里進行切換的標志 rt_uint32_t rt_interrupt_from_thread, rt_interrupt_to_thread; 在線程進行上下文切換時候,用來保存 from 和 to 線程
實現全局中斷開關
- 無論內核代碼還是用戶的代碼,都可能存在一些變量,需要在多個線程或者中斷里面使用,如果沒有相應的保護機制,那就可能導致臨界區問題。RT-Thread 里為了解決這個問題,提供了一系列的線程間同步和通信機制來解決。但是這些機制都需要用到 libcpu 里提供的全局中斷開關函數。分別是:
-
/* 關閉全局中斷 */ rt_base_t rt_hw_interrupt_disable(void); /* 打開全局中斷 */ void rt_hw_interrupt_enable(rt_base_t level);
-
Cortex-M 架構上如何實現這兩個函數,前文中曾提到過,Cortex-M 為了快速開關中斷,實現了 CPS 指令,可以用在此處
-
CPSID I ;PRIMASK=1, ; 關中斷 CPSIE I ;PRIMASK=0, ; 開中斷
實現全局中斷開關
-
在 rt_hw_interrupt_disable() 函數里面需要依序完成的功能是:
-
保存當前的全局中斷狀態,並把狀態作為函數的返回值。
-
關閉全局中斷。
- 在 context_rvds.S 中實現
-
;/* ; * rt_base_t rt_hw_interrupt_disable(void); ; */ rt_hw_interrupt_disable PROC ;PROC 偽指令定義函數 EXPORT rt_hw_interrupt_disable ;EXPORT 輸出定義的函數,類似於 C 語言 extern MRS r0, PRIMASK ; 讀取 PRIMASK 寄存器的值到 r0 寄存器 CPSID I ; 關閉全局中斷 BX LR ; 函數返回 ENDP ;ENDP 函數結束
打開全局中斷
-
在 rt_hw_interrupt_enable(rt_base_t level) 里,將變量 level 作為需要恢復的狀態,覆蓋芯片的全局中斷狀態。在 context_rvds.S 中實現;
-
;/* ; * void rt_hw_interrupt_enable(rt_base_t level); ; */ rt_hw_interrupt_enable PROC ; PROC 偽指令定義函數 EXPORT rt_hw_interrupt_enable ; EXPORT 輸出定義的函數,類似於 C 語言 extern MSR PRIMASK, r0 ; 將 r0 寄存器的值寫入到 PRIMASK 寄存器 BX LR ; 函數返回 ENDP ; ENDP 函數結束
實現線程棧初始化
- 在動態創建線程和初始化線程的時候,會使用到內部的線程初始化函數_rt_thread_init(),_rt_thread_init() 函數會調用棧初始化函數 rt_hw_stack_init(),在棧初始化函數里會手動構造一個上下文內容,這個上下文內容將被作為每個線程第一次執行的初始值。上下文在棧里的排布如下圖所示:
-
下代碼是棧初始化的代碼:在棧里構建上下文;在 cpuport.c中實現;
-
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); stk = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8); stk -= sizeof(struct stack_frame); /* 得到上下文的棧幀的指針 */ stack_frame = (struct stack_frame *)stk; /* 把所有寄存器的默認值設置為 0xdeadbeef */ for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++) { ((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef; } /* 根據 ARM APCS 調用標准,將第一個參數保存在 r0 寄存器 */ stack_frame->exception_stack_frame.r0 = (unsigned long)parameter; /* 將剩下的參數寄存器都設置為 0 */ stack_frame->exception_stack_frame.r1 = 0; /* r1 寄存器 */ stack_frame->exception_stack_frame.r2 = 0; /* r2 寄存器 */ stack_frame->exception_stack_frame.r3 = 0; /* r3 寄存器 */ /* 將 IP(Intra-Procedure-call scratch register.) 設置為 0 */ stack_frame->exception_stack_frame.r12 = 0; /* r12 寄存器 */ /* 將線程退出函數的地址保存在 lr 寄存器 */ stack_frame->exception_stack_frame.lr = (unsigned long)texit; /* 將線程入口函數的地址保存在 pc 寄存器 */ stack_frame->exception_stack_frame.pc = (unsigned long)tentry; /* 設置 psr 的值為 0x01000000L,表示默認切換過去是 Thumb 模式 */ stack_frame->exception_stack_frame.psr = 0x01000000L; /* 返回當前線程的棧地址 */ return stk; }
實現上下文切換
- 在不同的 CPU 架構里,線程之間的上下文切換和中斷到線程的上下文切換,上下文的寄存器部分可能是有差異的,也可能是一樣的。在 Cortex-M 里面上下文切換都是統一使用 PendSV 異常來完成,切換部分並沒有差異。但是為了能適應不同的 CPU 架構,RT-Thread 的 libcpu 抽象層還是需要實現三個線程切換相關的函數:
-
-
rt_hw_context_switch_to():沒有來源線程,切換到目標線程,在調度器啟動第一個線程的時候被調用。
-
rt_hw_context_switch():在線程環境下,從當前線程切換到目標線程。
-
rt_hw_context_switch_interrupt ():在中斷環境下,從當前線程切換到目標線程。
-
- 線程環境下,如果調用 rt_hw_context_switch() 函數,那么可以馬上進行上下文切換;而在中斷環境下,需要等待中斷處理函數完成之后才能進行切換;
- 在 ARM9 等平台,rt_hw_context_switch() 和 rt_hw_context_switch_interrupt() 的實現並不一樣。在中斷處理程序里如果觸發了線程的調度,調度函數里會調用 rt_hw_context_switch_interrupt() 觸發上下文切換。中斷處理程序里處理完中斷事務之后,中斷退出之前,檢查 rt_thread_switch_interrupt_flag 變量,如果該變量的值為 1,就根據 rt_interrupt_from_thread 變量和 rt_interrupt_to_thread 變量,完成線程的上下文切換。
- 在 Cortex-M 處理器架構里,基於自動部分壓棧和 PendSV 的特性,上下文切換可以實現地更加簡潔。
- 線程之間的上下文切換,如下圖表示:
- 硬件在進入 PendSV 中斷之前自動保存了 from 線程的 PSR、PC、LR、R12、R3-R0 寄存器,然后 PendSV 里保存 from 線程的 R11\~R4 寄存器,以及恢復 to 線程的 R4\~R11 寄存器,最后硬件在退出 PendSV 中斷之后,自動恢復 to 線程的 R0\~R3、R12、LR、PC、PSR 寄存器。
- 中斷到線程的上下文切換可以用下圖表示:
- 硬件在進入中斷之前自動保存了 from 線程的 PSR、PC、LR、R12、R3-R0 寄存器,然后觸發了 PendSV 異常。在 PendSV 異常處理函數里保存 from 線程的 R11\~R4 寄存器,以及恢復 to 線程的 R4\~R11 寄存器,最后硬件在退出 PendSV 中斷之后,自動恢復 to 線程的 R0\~R3、R12、PSR、PC、LR 寄存器。
- 顯然,在 Cortex-M 內核里 rt_hw_context_switch() 和 rt_hw_context_switch_interrupt() 功能一致,都是在 PendSV 里完成剩余上下文的保存和回復。所以我們僅僅需要實現一份代碼,簡化移植的工作。
實現 rt_hw_context_switch_to()
- rt_hw_context_switch_to() 只有目標線程,沒有來源線程。這個函數里實現切換到指定線程的功能,下圖是流程圖:
- rt_hw_context_switch_to() 實現
-
;/* ; * void rt_hw_context_switch_to(rt_uint32 to); ; * r0 --> to ; * this fucntion is used to perform the first thread switch ; */ rt_hw_context_switch_to PROC EXPORT rt_hw_context_switch_to ; r0 的值是一個指針,該指針指向 to 線程的線程控制塊的 SP 成員 ; 將 r0 寄存器的值保存到 rt_interrupt_to_thread 變量里 LDR r1, =rt_interrupt_to_thread STR r0, [r1] ; 設置 from 線程為空,表示不需要從保存 from 的上下文 LDR r1, =rt_interrupt_from_thread MOV r0, #0x0 STR r0, [r1] ; 設置標志為 1,表示需要切換,這個變量將在 PendSV 異常處理函數里切換的時被清零 LDR r1, =rt_thread_switch_interrupt_flag MOV r0, #1 STR r0, [r1] ; 設置 PendSV 異常優先級為最低優先級 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 ; 觸發 PendSV 異常 (將執行 PendSV 異常處理程序) LDR r0, =NVIC_INT_CTRL LDR r1, =NVIC_PENDSVSET STR r1, [r0] ; 放棄芯片啟動到第一次上下文切換之前的棧內容,將 MSP 設置啟動時的值 LDR r0, =SCB_VTOR LDR r0, [r0] LDR r0, [r0] MSR msp, r0 ; 使能全局中斷和全局異常,使能之后將進入 PendSV 異常處理函數 CPSIE F CPSIE I ; 不會執行到這里 ENDP
- rt_hw_context_switch()/ rt_hw_context_switch_interrupt()實現:
- 函數 rt_hw_context_switch() 和函數 rt_hw_context_switch_interrupt() 都有兩個參數,分別是 from 線程和 to 線程。它們實現從 from 線程切換到 to 線程的功能。下圖是具體的流程圖:
- rt_hw_context_switch()/rt_hw_context_switch_interrupt() 實現:、
-
;/* ; * void rt_hw_context_switch(rt_uint32 from, rt_uint32 to); ; * r0 --> from ; * r1 --> to ; */ rt_hw_context_switch_interrupt EXPORT rt_hw_context_switch_interrupt rt_hw_context_switch PROC EXPORT rt_hw_context_switch ; 檢查 rt_thread_switch_interrupt_flag 變量是否為 1 ; 如果變量為 1 就跳過更新 from 線程的內容 LDR r2, =rt_thread_switch_interrupt_flag LDR r3, [r2] CMP r3, #1 BEQ _reswitch ; 設置 rt_thread_switch_interrupt_flag 變量為 1 MOV r3, #1 STR r3, [r2] ; 從參數 r0 里更新 rt_interrupt_from_thread 變量 LDR r2, =rt_interrupt_from_thread STR r0, [r2] _reswitch ; 從參數 r1 里更新 rt_interrupt_to_thread 變量 LDR r2, =rt_interrupt_to_thread STR r1, [r2] ; 觸發 PendSV 異常,將進入 PendSV 異常處理函數里完成上下文切換 LDR r0, =NVIC_INT_CTRL LDR r1, =NVIC_PENDSVSET STR r1, [r0] BX LR
實現 PendSV 中斷
- 在 Cortex-M3 里,PendSV 中斷處理函數是 PendSV_Handler()。在 PendSV_Handler() 里完成線程切換的實際工作,下圖是具體的流程圖:
- PendSV_Handler 實現:
-
; 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 變量是否為 0 ; 如果為零就跳轉到 pendsv_exit LDR r0, =rt_thread_switch_interrupt_flag LDR r1, [r0] CBZ r1, pendsv_exit ; pendsv already handled ; 清零 rt_thread_switch_interrupt_flag 變量 MOV r1, #0x00 STR r1, [r0] ; 檢查 rt_thread_switch_interrupt_flag 變量 ; 如果為 0,就不進行 from 線程的上下文保存 LDR r0, =rt_interrupt_from_thread LDR r1, [r0] CBZ r1, switch_to_thread ; 保存 from 線程的上下文 MRS r1, psp ; 獲取 from 線程的棧指針 STMFD r1!, {r4 - r11} ; 將 r4~r11 保存到線程的棧里 LDR r0, [r0] STR r1, [r0] ; 更新線程的控制塊的 SP 指針 switch_to_thread LDR r1, =rt_interrupt_to_thread LDR r1, [r1] LDR r1, [r1] ; 獲取 to 線程的棧指針 LDMFD r1!, {r4 - r11} ; 從 to 線程的棧里恢復 to 線程的寄存器值 MSR psp, r1 ; 更新 r1 的值到 psp pendsv_exit ; 恢復全局中斷狀態 MSR PRIMASK, r2 ; 修改 lr 寄存器的 bit2,確保進程使用 PSP 堆棧指針 ORR lr, lr, #0x04 ; 退出中斷函數 BX lr ENDP
實現時鍾節拍
- 有了開關全局中斷和上下文切換功能的基礎,RTOS 就可以進行線程的創建、運行、調度等功能了。有了時鍾節拍支持,RT-Thread 可以實現對相同優先級的線程采用時間片輪轉的方式來調度,實現定時器功能,實現 rt_thread_delay() 延時函數等等。
- libcpu 的移植需要完成的工作,就是確保 rt_tick_increase() 函數會在時鍾節拍的中斷里被周期性的調用,調用周期取決於 rtconfig.h 的宏 RT_TICK_PER_SECOND 的值。
- 在 Cortex M 中,實現 SysTick 的中斷處理函數即可實現時鍾節拍功能。
-
void SysTick_Handler(void) { /* enter interrupt */ rt_interrupt_enter(); rt_tick_increase(); /* leave interrupt */ rt_interrupt_leave(); }
BSP移植
- RT-Thread 提供了 BSP 抽象層來適配常見的板卡。如果希望在一個板卡上使用 RT-Thread 內核,除了需要有相應的芯片架構的移植,還需要有針對板卡的移植,也就是實現一個基本的 BSP。主要任務是建立讓操作系統運行的基本環境,需要完成的主要工作是:
-
-
初始化 CPU 內部寄存器,設定 RAM 工作時序。
-
實現時鍾驅動及中斷控制器驅動,完善中斷管理。
-
實現串口和 GPIO 驅動。
-
初始化動態內存堆,實現動態堆內存管理。
-
參考
- 《RT-Thread 編程指南》