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上,这就是串口驱动程序。移植的时候,所花的时间,比内核部分的移植还要大。从代码量上统计,也跟内核部分差不多,可见驱动程序移植只繁琐。虽然工作量大,但其难度却不是很大,不像内核一样,存在很多陷阱。

在移植过程中,还有其它一些注意的地方,在此就不做更多说明了。如有需要,在本文后面会提到。

原文地址:https://www.cnblogs.com/fengju/p/6174201.html