Hello China操作系統STM32移植指南(一)

Hello China操作系統移植指南

首先說明一下,爲了適應更多的文化背景,對Hello China操作系統的名字做了修改,修改爲“Hello X”,或者連接在一起,寫爲“HelloX“。其中X是不固定的,可以根據具體應用的國家,甚至城市,進行定製化。比如在中國,我仍然會叫做”Hello China“,但是如果有人在美國使用了,則可以叫做”Hello USA“,在香港使用了,可以叫做”Hello Hongkong“,等等。但這些都是HelloX的分支,必須明確說明。這樣做,主要目的是爲了Hello China操作系統的全球化推廣,後續將作爲開源項目託管到github上,屆時任何國家和地區的人都可以參與開發。

經過一段時間的嘗試,現在已經成功把Hello China V1.76版的內核代碼,移植到STM32芯片上。本文就對這個移植過程進行詳細說明。在正式介紹移植步驟之前,先說明一下移植原理。

STM32移植原理

STM32芯片是ST公司開發的基於Cortex-M3 CPU(ARM系列)的SoC,在各行各業中得到了廣泛的應用。往STM32上移植,本質上就是向Cortex-M3 CPU上的移植,移植後的內核,可以很容易的再移植到其它基於Cortex-M3 CPU的芯片上。關於STM32更多的背景及特性等,這裏就不多說了。爲了更好的理解下面的內容,建議讀者先了解一下Cortex-M3 CPU的一些基礎機制,這樣閱讀起來更加容易。

如果您一直在x86系列CPU上做開發,那麼當接觸到ARM系列CPU的時候,您會發現非常容易。ARM CPU非常簡潔,邏輯比x86簡單得多。根據我個人的經驗,x86CPU的保護模式編程,比實模式編程要簡單,而ARM CPU的編程,則比x86的保護模式更簡單。雖然模型簡單了,但由於指令系統不同,對中斷/異常的處理機制也不一樣,導致移植過程並不是很容易。主要集中在下列幾個方面:

中斷機制的移植

首先是中斷處理機制的移植,這是任何操作系統向不同CPU上的移植,都需要涉及到的問題。x86 CPU採用中斷描述表的方式,每個中斷佔用一個表項,表裏面記錄了處理中斷的中斷向量(函數)。一旦中斷髮生,CPU會調用對應中斷號的中斷向量。Cortex-M3CPU的中斷機制與此基本類似,但是其中斷向量表卻簡單得多,不像x86一樣,內嵌了很多的控制信息。而且Cortex-M3的中斷向量表的位置是固定的,就是在物理內存的起始處。Hello China在設計的時候,爲了兼容多種CPU,其中斷機制採用了“統一入口”的形式。即針對CPU的所有中斷向量表,都填寫一個唯一的入口-GeneralIntHandler,這個入口由Hello China操作系統實現。然後GeneralIntHandler再根據實際的中斷向量號,調用具體的中斷處理程序。這樣的機制,可以適應大多數CPU的需求。因爲有的CPU只有一箇中斷向量表,不像x86那樣,每個中斷都有向量表對應。因此在把中斷處理機制移植到Cortex-M3的時候,必須要把每個中斷向量跟GeneralIntHandler鏈接起來。下面是Cortex-M3的典型中斷向量表結構:

__Vectors

DCD     __initial_sp               ; Top of Stack

                DCD     Reset_Handler              ; Reset Handler

                DCD     NMI_Handler                ; NMI Handler

                DCD     HardFault_Handler          ; Hard Fault Handler

                DCD     MemManage_Handler          ; MPU Fault Handler

                DCD     BusFault_Handler           ; Bus Fault Handler

                DCD     UsageFault_Handler         ; Usage Fault Handler

                DCD     0                          ; Reserved

                DCD     0                          ; Reserved

                DCD     0                          ; Reserved

                DCD     0                          ; Reserved

                DCD     SVC_Handler                ; SVCall Handler

                DCD     DebugMon_Handler           ; Debug Monitor Handler

                DCD     0                          ; Reserved

                DCD     PendSV_Handler             ; PendSV Handler

                DCD     SysTick_Handler            ; SysTick Handler

                ; ExternalInterrupts

                DCD     WWDG_IRQHandler            ; Window Watchdog

                DCD     PVD_IRQHandler             ; PVD through EXTI Line detect

                DCD     TAMPER_IRQHandler          ; Tamper

                DCD     RTC_IRQHandler             ; RTC

                DCD     FLASH_IRQHandler           ; Flash

                ………

向量表的第一個表項,是CPU的初始堆棧指針(即MSP寄存器的值),後續每個表項,都對應特定異常或中斷的處理程序。最簡單的修改方式,就是把上述每個中斷向量(除初始堆棧指針外),替換爲GeneralIntHandler函數。但這樣做有一個問題,那就是無法確定中斷向量號。因爲GeneralIntHandlr是用C語言實現的,接受中斷向量作爲參數。因此又用彙編語言實現了一個Int_Entry_Wrapper的函數,作爲中間的一層封裝。這個函數代碼如下:

Int_Entry_Wrapper    PROC

                EXPORT  Int_Entry_Wrapper

          IMPORT GeneralIntHandler   ;Implementedin C file.

            MRS R0,IPSR

            AND R0,R0,#0xFF

            MRS R1,MSP

            LDR R2,=GeneralIntHandler

            BX  R2

            ENDP

函數的邏輯很簡單,首先讀取IPSR寄存器,從中得到中斷向量號,然後再以中斷向量號和堆棧指針爲參數,調用GeneralIntHandler。使用Int_Entry_Wrapper替換Cortex-M3的中斷向量表中的所有項,即可實現中斷的連接。

用戶如果要編寫設備驅動程序,需要在中斷向量表中增加中斷向量的時候,就無需修改啓動文件(比如MDK生成的是startup_stm32f10x_xx.S文件)了,直接調用Hello China提供的中斷接口函數-ConnectInterrupt和DisconnectInterrupt即可,屏蔽了底層的中斷實現。

用戶在實現中斷函數的時候,也無需在進入中斷和離開中斷時調用操作系統的“夾層函數”(諸如InterruptEnter/InterruptLeave等函數),因爲夾層功能都已經在GeneralIntHandler中實現了。這種編程模型,符合操作系統對硬件的抽象理念,同時擴展性也非常強。

需要說明的是,在基於MDK開發環境的STM32移植中,我們並沒有直接使用上述方法,而是做了適當變通,使得這個過程更加簡單。因爲直接用Int_Entry_Wrapper替換中斷向量表,畢竟有很多的編輯工作,需要一條一條的修改,容易出錯。在MDK生成的啓動文件中,對所有的中斷處理函數都做了一個缺省實現,如下:

………

USART1_IRQHandler

USART2_IRQHandler

USART3_IRQHandler

EXTI15_10_IRQHandler

RTCAlarm_IRQHandler

USBWakeUp_IRQHandler

                B .

                ENDP

這是一個死循環,因此,只要用下面的代碼,替換這個死循環實現即可:

………

RTCAlarm_IRQHandler

USBWakeUp_IRQHandler

           IMPORTInt_Entry_Wrapper

           LDRR0,=Int_Entry_Wrapper

           BX R0

               ENDP

在具體移植過程中,只要找到startup_stm32f10x_XX.S文件,用上述代碼替換缺省實現的死循環,即可完成中斷鏈接。需要說明的是,Int_Entry_Wrapper是在另外一個彙編文件-osadapt.S中實現的。Osadapt.S是Hello China爲了向STM32移植而增加的一個文件,裏面還實現了其它底層功能,後面會詳細講解。

任務切換機制的移植

任務切換是操作系統移植的最重要的地方,畢竟不同的CPU,其底層機制是不同的。操作系統的任何功能模塊都可以用C語言編寫,但任務切換確是例外,目前尚未看到能夠用C語言直接實現任務切換的CPU。

Hello China在實現的時候,任務切換機制做了優化,所需的彙編代碼非常少。但是爲了迎合不同的任務切換場景,分成了兩種任務切換時機:

1.         中斷內的任務切換,這種情形下,CPU的硬件中斷處理機制已經幫助操作系統把任務(或者線程)的堆棧框架搭建好,操作系統無需自行建立堆棧框架,只需選擇合適的線程,恢復堆棧框架即可;

2.         非中斷內的任務切換(也叫做進程內的任務切換),這種情況下,一般是由系統調用引發。用戶程序調用系統功能,比如調用CreateKernelThread創建一個核心線程,操作系統內核會在線程創建完成之後,重新檢查系統中的所有內核線程,看是否有比當前核心線程優先級更高的線程。如果有,則內核會保存當前線程的堆棧框架,然後選擇優先級最高的處於就緒狀態的線程,恢復其堆棧框架,讓其運行。與中斷過程不同,這裏需要操作系統內核自己建立待切換出線程的堆棧框架,工作要稍微多一些。

對應上述兩種情況,Hello China操作系統實現了兩個函數:ShceduleFromInt和ScheduleFromProc,分別用以實現。這兩個函數分別調用更加底層的用彙編語言實現的兩個函數:__SwitchTo和__SaveAndSwitch,來實現核心線程的切換。這種切換機制,因爲不用引發額外的系統中斷,所以我們稱爲inline schedule,對應config.h文件中的__CFG_SYS_IS。大多數CPU都是採用這種切換方式,高效簡潔,易於理解。

但到了Cortex-M3的CPU,由於其底層機制的制約,無法使用上述inlineschedule機制。Cortex-M3的推薦做法是,專門預留了一個系統中斷(異常),叫做PendSV,用於線程切換。任何希望切換線程的代碼,需要引發這個異常,所有具體的切換工作,在這個異常處理程序中進行。這樣就不分中斷切換和進程切換了,所有切換統一進行。爲了區分上面的inline schedule方式,我們把這種情形稱爲uniform schedule(簡稱unischedule,統一切換)。Unischedule的好處是不分切換所處的狀態,相對簡潔,但是不好之處在於,兼容性比較差,很少有CPU只支持這種方式,因此其通用性欠佳。

Hello China支持上述兩種方式。在移植的時候,如果在config.h文件中定義了宏__CFG_SYS_IS(Inline Schedule),則採用第一種方式切換,如果把這個宏定義註釋掉,則採用第二種方式切換。在Cortex-M3上,我們註釋掉該宏定義,採用第二種方式切換。要引發任務切換,操作系統內核代碼必須引發一個PendSV中斷,下面是引發該中斷的彙編代碼:

ScheduleFromInt

ScheduleFromProc

PROC

               EXPORTScheduleFromInt

               EXPORTScheduleFromProc

           PUSH {R4,R5}

           LDRR4,=NVIC_INT_CTRL

           LDRR5,=NVIC_PENDSVSET

           STR R5,[R4]

           POP {R4,R5}

           BX LR

           NOP

           ENDP

要切換線程的時候,只要調用ScheduleFromInt或者ScheduleFromProc兩個函數即可,這兩個函數的實現是一樣的,就是引發一個PendSV異常。

異常引發之後,在所有系統中斷都處理完畢後,PendSV異常處理程序會被調用。由於我們在系統初始化的時候,設置PendSV異常的優先級是最低的,因此如果有任何中斷處理程序在運行,PendSV異常處理程序就不會被調用。下面是PendSV處理程序的代碼:

PendSV_Handler PROC

      EXPORT PendSV_Handler

        IMPORT UniSchedule    ;UniSchedule is implemented in C file.

        PUSH {R4-R11}    ;Save un-saved registers.

        MOV R4,LR

        MRS R0,MSP

        MRSR1,MSP

        LDR R2,=UniSchedule

        BLX R2           ;Now R0 contains the new thread toswitch to.

        MOV LR,R4

        MSR MSP,R0

        POP {R4-R11}

        BX LR

        ENDP

這個程序也非常簡單,先保存R4到R11寄存器(其它的寄存器,在進入異常處理程序的時候,已經由CPU保存),然後以當前堆棧指針爲參數,調用UniSchedule函數。這個函數返回一個優先級最高的可調度線程的堆棧指針,然後PendSV直接恢復返回的線程的堆棧,即可切換到目標線程繼續運行。

UniSchedule是用C語言實現的一個函數,位於ktmgr.c文件中。這個函數從系統中選擇一個優先級最高的,且狀態是ready的線程,返回其上下文指針(即堆棧指針)。需要注意的是,如果系統中沒有比當前核心線程優先級更高的線程,則該函數直接返回當前核心線程的上下文,這樣的結果就是,當前核心線程繼續執行。

驅動程序的移植

個人認爲,驅動程序移植是操作系統移植過程中工作量最大,也是最難的工作。畢竟不同的CPU,其外設管理方式不同,訪問方式也不同。比如x86 CPU,是通過讀寫端口(in/out指令)來實現外設訪問的。但是ARM系列CPU,則是通過內存映射方式,直接讀寫外設控制寄存器的。在實現驅動程序的時候,一般是針對某種CPU寫的,在程序中內嵌了很多訪問外設的代碼。在移植的時候,這些代碼都要一行一行的變換。如果說操作系統內核的移植是“黑盒移植”,無需審視每一行代碼,只要移植幾個接口函數即可,那麼驅動程序的移植,則必須是“白盒移植”,需要認真檢查每一行代碼,確保每一行代碼都能夠在目標設備上工作。

在Hello China V1.76的移植中,只移植了一個USART設備的驅動程序。在基於x86的PC上,這就是串口驅動程序。移植的時候,所花的時間,比內核部分的移植還要大。從代碼量上統計,也跟內核部分差不多,可見驅動程序移植只繁瑣。雖然工作量大,但其難度卻不是很大,不像內核一樣,存在很多陷阱。

在移植過程中,還有其它一些注意的地方,在此就不做更多說明了。如有需要,在本文後面會提到。

發佈了88 篇原創文章 · 獲贊 168 · 訪問量 51萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章