RT-Thread 內核移植學習

最近在看RT_THread, 其中對於內核的移植,感覺需要非常瞭解CPU架構,彙編指令集,這邊就以官方wiki文檔學習理解。

先放上原文,之後有疑問的地方對文檔做批註來加深學習。

 

經過前面內核章節的學習,大家對 RT-Thread 也有了不少的瞭解,但是如何將 RT-Thread 內核移植到不同的硬件平臺上,很多人還不一定熟悉。內核移植就是指將 RT-Thread 內核在不同的芯片架構、不同的板卡上運行起來,能夠具備線程管理和調度,內存管理,線程間同步和通信、定時器管理等功能。移植可分爲 CPU 架構移植和 BSP(Board support package,板級支持包)移植兩部分。

本章將展開介紹 CPU 架構移植和 BSP 移植,CPU 架構移植部分會結合 Cortex-M CPU 架構進行介紹,因此有必要回顧下上一章《中斷管理》介紹的 “Cortex-M CPU 架構基礎” 的內容,本章最後以實際移植到一個開發板的示例展示 RT-Thread 內核移植的完整過程,讀完本章,我們將瞭解如何完成 RT-Thread 的內核移植。

CPU 架構移植

在嵌入式領域有多種不同 CPU 架構,例如 Cortex-M、ARM920T、MIPS32、RISC-V 等等。爲了使 RT-Thread 能夠在不同 CPU 架構的芯片上運行,RT-Thread 提供了一個 libcpu 抽象層來適配不同的 CPU 架構。libcpu 層向上對內核提供統一的接口,包括全局中斷的開關,線程棧的初始化,上下文切換等。

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 裏爲了解決這個問題,提供了一系列的線程間同步和通信機制來解決。但是這些機制都需要用到 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() 函數裏面需要依序完成的功能是:

1). 保存當前的全局中斷狀態,並把狀態作爲函數的返回值。

2). 關閉全局中斷。

基於 MDK,在 Cortex-M 內核上實現關閉全局中斷,如下代碼所示:

;/*
; * 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 函數結束

上面的代碼首先是使用 MRS 指令將 PRIMASK 寄存器的值保存到 r0 寄存器裏,然後使用 “CPSID I” 指令關閉全局中斷,最後使用 BX 指令返回。r0 存儲的數據就是函數的返回值。中斷可以發生在 “MRS r0, PRIMASK” 指令和 “CPSID I” 之間,這並不會導致全局中斷狀態的錯亂。

關於寄存器在函數調用的時候和在中斷處理程序裏是如何管理的,不同的 CPU 架構有不同的約定。在 ARM 官方手冊《Procedure Call Standard for the ARM ® Architecture》裏可以找到關於 Cortex-M 的更詳細的介紹寄存器使用的約定。

在 rt_hw_interrupt_enable(rt_base_t level) 裏,將變量 level 作爲需要恢復的狀態,覆蓋芯片的全局中斷狀態。

基於 MDK,在 Cortex-M 內核上的實現打開全局中斷,如下代碼所示:

;/*
; * 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 函數結束

上面的代碼首先是使用 MSR 指令將 r0 的值寄存器寫入到 PRIMASK 寄存器,從而恢復之前的中斷狀態。

實現線程棧初始化

在動態創建線程和初始化線程的時候,會使用到內部的線程初始化函數_rt_thread_init(),_rt_thread_init() 函數會調用棧初始化函數 rt_hw_stack_init(),在棧初始化函數裏會手動構造一個上下文內容,這個上下文內容將被作爲每個線程第一次執行的初始值。上下文在棧裏的排布如下圖所示:

棧裏的上下文信息

下代碼是棧初始化的代碼:

在棧裏構建上下文

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 抽象層還是需要實現三個線程切換相關的函數:

1) rt_hw_context_switch_to():沒有來源線程,切換到目標線程,在調度器啓動第一個線程的時候被調用。

2) rt_hw_context_switch():在線程環境下,從當前線程切換到目標線程。

3) 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 寄存器。

 


注:

在中斷處理過程中, 如果需要進行任務調度, 該任務的上下文切換會置後到當前ISR處理完。另外上圖中容易引起誤解的是硬件壓棧過程是發生在中斷處理處理之前的,即符合異常/中斷處理典型的流程1)入棧: 把8個寄存器的值壓入棧;2)取向量:從向量表中找出對應的服務程序入口地址3)選擇堆棧指針MSP/PSP,更新堆棧指針SP,更新連接寄存器LR,更新程序計數器PC。

那麼如何理解這邊的上下文切換呢?答案是咬尾中斷。摘要Cortex-M3權威指南中的說明(這邊說句題外話,很多年前就看過這本書,現在重新拿起來看,發現已經忘記的很多。這也提醒自己需要多寫文檔,多做記錄,不然空耗精力結果忘的一乾二淨。)

咬尾中斷
CM3爲縮短中斷延遲做了很多努力,第一個要提的,就是新增的“咬尾中斷”(Tail‐Chaining)機制。
當處理器在響應某異常時,如果又發生其它異常,但它們優先級不夠高,則被阻塞。那麼在當前的異常執行返回後,系統處理懸起的異常時,倘若還是先POP然後又把POP出來的內容PUSH回去,這不成了砸鍋鍊鐵再鑄鍋,白白浪費CPU時間。正因此, CM3不會傻乎乎地POP這些寄存器,而是繼續使用上一個異常已經PUSH好的成果,消滅了這種鋪張浪費。這麼一來,看上去好像後一個異常把前一個的尾巴咬掉了,前前後後只執行了一次入棧/出棧操作。於是,這兩個異常之間
的“時間溝”變窄了很多,如下圖所示。



顯然,在 Cortex-M 內核裏 rt_hw_context_switch() 和 rt_hw_context_switch_interrupt() 功能一致,都是在 PendSV 裏完成剩餘上下文的保存和回覆。所以我們僅僅需要實現一份代碼,簡化移植的工作。


注:

在下面的rt_hw_context_switch_to/rt_hw_context_switch/rt_hw_context_switch_interrupt/PendSV_Handler 中是通過全局變量

rt_interrupt_to_thread&rt_interrupt_from_thread &rt_thread_switch_interrupt_flag&來互相配合的。

先拎出來看下它們的定義。

rt_interrupt_to_thread:切換入thread的堆棧SP 地址,指向to_thread的堆棧;

rt_interrupt_from_thread :想要切換出thread的堆棧SP 地址, 指向from_thread 的堆棧;

rt_thread_switch_interrupt_flag:該flag記錄當前是否需要在PendSV中切換thread, 同時也阻止對rt_interrupt_from_thread 的多次賦值,而改變被中斷的原始thread記錄。


 

實現 rt_hw_context_switch_to()

rt_hw_context_switch_to() 只有目標線程,沒有來源線程。這個函數裏實現切換到指定線程的功能,下圖是流程圖:

rt_hw_context_switch_to() 流程圖

在 Cortex-M3 內核上的 rt_hw_context_switch_to() 實現(基於 MDK),如下代碼所示:

MDK 版 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() 流程圖

在 Cortex-M3 內核上的 rt_hw_context_switch() 和 rt_hw_context_switch_interrupt() 實現(基於 MDK),如下代碼的所示:

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 中斷處理

如下代碼是 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 移植

相同的 CPU 架構在實際項目中,不同的板卡上可能使用相同的 CPU 架構,搭載不同的外設資源,完成不同的產品,所以我們也需要針對板卡做適配工作。RT-Thread 提供了 BSP 抽象層來適配常見的板卡。如果希望在一個板卡上使用 RT-Thread 內核,除了需要有相應的芯片架構的移植,還需要有針對板卡的移植,也就是實現一個基本的 BSP。主要任務是建立讓操作系統運行的基本環境,需要完成的主要工作是:

1)初始化 CPU 內部寄存器,設定 RAM 工作時序。

2)實現時鐘驅動及中斷控制器驅動,完善中斷管理。

3)實現串口和 GPIO 驅動。

4)初始化動態內存堆,實現動態堆內存管理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章