啓動流程就不必再說了。詳情就看熊譜翔的書吧。這裏只是說一下對自己來說比較新鮮的地方及所得。
自動初始化機制:
只要在函數定義處通過宏定義的方式進行聲明,就會在系統啓動過程中被執行。這裏當宏理解成開關就可以了,還有其用法,是放在了函數定義處,注意一下就可以,還是比較好理解的,如:
int rt_hw_usart_init(void) /* 串口初始化函數 */
{
. . . . ..
/* 註冊串口1 設備 */
rt_hw_serial_register(&serial1, "uart1",
RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX,
uart);
return 0;
}
INIT_BOARD_EXPORT(rt_hw_usart_init); /* 使用組件自動初始化機制 */
內核對象的管理架構,完全是基於面向對象的方法來設計的。兩個特點:
1、內核對象包括線程、信號量、互斥量、事件、郵箱、消息隊列和定時器、內存池、設備驅動等。對象容器中包含每類內核對象的信息,包括對象類型、大小等。對象容器給每類內核對象分配了一個鏈表,所有的內核對象都被鏈接到該鏈表上。
2、要了解各種類之的繼承關係。當然,這裏的類指容器。
關於內核移植:主要是針對cotex-M的CPU架構:
準備知識:
M核通用寄存器就不用說了,見下圖:
程序狀態字寄存器裏保存算術與邏輯標誌,例如負數標誌,零結果標誌,溢出標誌等等。
中斷屏蔽寄存器組控制 Cortex-M 的中斷除能。
控制寄存器用來定義特權級別和當前使用哪個堆棧指針。
MSP,適用於中斷模式下。PSP適用於多任務狀態。
如果是具有浮點單元的 Cortex-M4 或者 Cortex-M7,控制寄存器也用來指示浮點單元當前是否在使用,浮點單元包含了 32 個浮點通用寄存器 S0\~S31 和特殊 FPSCR 寄存器(Floating point status and control register)
中斷的過程:
當系統正在服務一箇中斷時,如果有一個更高優先級的中斷觸發,那麼處理器同樣會打斷當前運行的中斷服務程序,然後把這個中斷服務程序上下文的 PSR、PC、LR、R12、R3-R0 寄存器自動保存到中斷棧中。
具體過程如下圖:
中斷前導程序
中斷前導程序主要工作如下:
1)保存 CPU 中斷現場,這部分跟 CPU 架構相關,不同 CPU 架構的實現方式有差異。
對於 Cortex-M 來說,該工作由硬件自動完成。當一箇中斷觸發並且系統進行響應時,處理器硬件會將當前運行部分的上下文寄存器自動壓入中斷棧中,這部分的寄存器包括 PSR、PC、LR、R12、R3-R0 寄存器。
2)通知內核進入中斷狀態,調用 rt_interrupt_enter() 函數,作用是把全局變量 rt_interrupt_nest 加 1,用它來記錄中斷嵌套的層數,代碼如下所示。
void rt_interrupt_enter(void)
{
rt_base_t level;
level = rt_hw_interrupt_disable();
rt_interrupt_nest ++;
rt_hw_interrupt_enable(level);
}
用戶中斷服務程序
在用戶中斷服務程序(ISR)中,分爲兩種情況,第一種情況是不進行線程切換,這種情況下用戶中斷服務程序和中斷後續程序運行完畢後退出中斷模式,返回被中斷的線程。
另一種情況是,在中斷處理過程中需要進行線程切換,這種情況會調用 rt_hw_context_switch_interrupt() 函數進行上下文切換,該函數跟 CPU 架構相關,不同 CPU 架構的實現方式有差異。
rt_hw_context_switch_interrupt()函數處理流程:
它將設置需要切換到的線程 rt_interrupt_to_thread 變量,然後觸發 PendSV 異常(PendSV 異常是專門用來輔助上下文切換的,且被初始化爲最低優先級的異常)。PendSV 異常被觸發後,不會立即進行 PendSV 異常中斷處理程序,因爲此時還在中斷處理中,只有當中斷後續程序運行完畢,真正退出中斷處理後,才進入 PendSV 異常中斷處理程序。
中斷後續程序
中斷後續程序主要完成的工作是:
1 通知內核離開中斷狀態,通過調用 rt_interrupt_leave() 函數,將全局變量 rt_interrupt_nest 減 1,代碼如下所示。
void rt_interrupt_leave(void)
{
rt_base_t level;
level = rt_hw_interrupt_disable();
rt_interrupt_nest --;
rt_hw_interrupt_enable(level);
2 恢復中斷前的 CPU 上下文,如果在中斷處理過程中未進行線程切換,那麼恢復 from 線程(上文)的 CPU 上下文,如果在中斷中進行了線程切換,那麼恢復 to (下文)線程的 CPU 上下文。這部分實現跟 CPU 架構相關,不同 CPU 架構的實現方式有差異,在 Cortex-M 架構中實現流程如下圖所示。
關於RTT的中斷棧的處理:
RT-Thread 採用的方式是提供獨立的中斷棧,即中斷髮生時,中斷的前期處理程序會將用戶的棧指針更換到系統事先留出的中斷棧空間中,等中斷退出時再恢復用戶的棧指針。這樣中斷就不會佔用線程的棧空間,從而提高了內存空間的利用率,且隨着線程的增加,這種減少內存佔用的效果也越明顯。
在 Cortex-M 處理器內核裏有兩個堆棧指針,一個是主堆棧指針(MSP),是默認的堆棧指針,在運行第一個線程之前、在中斷和異常服務程序裏使用;另一個是線程堆棧指針(PSP),在線程裏使用。在中斷和異常服務程序退出時,修改 LR 寄存器的第 2 位的值爲 1,線程的 SP 就由 MSP 切換到 PSP。
關於RTT中斷的抽象管理
中斷服務程序掛接
系統把用戶的中斷服務程序 (handler) 和指定的中斷號關聯起來,可調用如下的接口掛載一個新的中斷服務程序:
rt_isr_handler_t rt_hw_interrupt_install(int vector,
rt_isr_handler_t handler,
void *param,
char *name);
調用 rt_hw_interrupt_install() 後,當這個中斷源產生中斷時,系統將自動調用裝載的中斷服務程序。
注:
這個 API 並不會出現在每一個移植分支中,例如通常 Cortex-M0/M3/M4 的移植分支中就沒有這個 API。
中斷源管理
通常在 ISR 準備處理某個中斷信號之前,我們需要先屏蔽該中斷源,在 ISR 處理完狀態或數據以後,及時的打開之前被屏蔽的中斷源。
屏蔽中斷源可以保證在接下來的處理過程中硬件狀態或者數據不會受到干擾,可調用下面這個函數接口:
void rt_hw_interrupt_mask(int vector);
注:
這個 API 並不會出現在每一個移植分支中,例如通常 Cortex-M0/M3/M4 的移植分支中就沒有這個 API。
全局中斷開關
全局中斷開關也稱爲中斷鎖,是禁止多線程訪問臨界區最簡單的一種方式,即通過關閉中斷的方式,來保證當前線程不會被其他事件打斷(因爲整個系統已經不再響應那些可以觸發線程重新調度的外部事件),也就是當前線程不會被搶佔,除非這個線程主動放棄了處理器控制權。當需要關閉整個系統的中斷時,可調用下面的函數接口:
rt_base_t rt_hw_interrupt_disable(void);
開中斷:
void rt_hw_interrupt_enable(rt_base_t level);
移植過程:
RT-Thread 的 libcpu 抽象層向下提供了一套統一的 CPU 架構移植接口,這部分接口包含了全局中斷開關函數、線程上下文切換函數、時鐘節拍的配置和中斷函數、Cache 等等內容。下表是 CPU 架構移植需要實現的接口和變量。
libcpu 移植相關 API
函數和變量 | 描述 |
---|---|
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_init(),_rt_thread_init() 函數會調用棧初始化函數 rt_hw_stack_init(),在棧初始化函數裏會手動構造一個上下文內容,這個上下文內容將被作爲每個線程第一次執行的初始值。上下文在棧裏的排布如下圖所示:
實現上下文切換
在不同的 CPU 架構裏,線程之間的上下文切換和中斷到線程的上下文切換,上下文的寄存器部分可能是有差異的,也可能是一樣的。在 Cortex-M 裏面上下文切換都是統一使用 PendSV 異常來完成,切換部分並沒有差異。但是爲了能適應不同的 CPU 架構,RT-Thread 的 libcpu 抽象層還是需要實現三個線程切換相關的函數:
1) rt_hw_context_switch_to():沒有來源線程,切換到目標線程,在調度器啓動第一個線程的時候被調用。
2) rt_hw_context_switch():在線程環境下,從當前線程切換到目標線程。
3) rt_hw_context_switch_interrupt ():在中斷環境下,從當前線程切換到目標線程。
在線程環境下進行切換和在中斷環境進行切換是存在差異的。線程環境下,如果調用 rt_hw_context_switch() 函數,那麼可以馬上進行上下文切換;而在中斷環境下,需要等待中斷處理函數完成之後才能進行切換。
在 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 裏完成剩餘上下文的保存和回覆。所以我們僅僅需要實現一份代碼,簡化移植的工作。
實現時鐘節拍
有了開關全局中斷和上下文切換功能的基礎,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 移植
實現一個基本的 BSP。主要任務是建立讓操作系統運行的基本環境,需要完成的主要工作是:
1)初始化 CPU 內部寄存器,設定 RAM 工作時序。
2)實現時鐘驅動及中斷控制器驅動,完善中斷管理。
3)實現串口和 GPIO 驅動。
4)初始化動態內存堆,實現動態堆內存管理。
這些內容,其實都是原理性的,實踐中再總結真正的實現!!
總結:任務切換流程:
任務初始化後:如下圖:
當該任務被調度執行時,CPU會自動將任務棧中最前面的8個寄存器值加載到CPU寄存器中,PendSV異常中完成下文環境切換,那麼如何觸發PendSV呢???
答案是寄存器,操作CPU中的寄存器就可以,如同其他中斷一樣。