@

序言

前天学习FreeRTOS,想着通过对比着UCOSIII来理解,会更容易一点,在这对比的过程中,发现自己对UCOSIII还不是很熟悉,不仅如此,对CM3内核也不是很熟悉(学操作系统竟然不熟悉芯片内核,真是有点搞笑)。因此我打算重新复习一下UCOSIII,但又不能像刚开始学习UCOSIII那样从头撸起,于是我把UCOSIII的文件理了一下,我打算先从与移植相关的代码看起。与移植相关的代码里面,最核心的就是os_cpu_a.s汇编文件中与上下文切换相关的那段汇编代码,因此我打算用这篇文章将这段代码的分析过程记录下来,留着以后看看。本篇文章主要分为两个部分:

  • CM3内核介绍
  • 上下文切换源码分析

CM3内核介绍

CM3是基于ARMv7-M架构的一款内核,哈弗结构,32位地址总线与数据总线,它和其它ARM内核一样,拥有R0-R15寄存器组,R0-R12为通用寄存器,R13为SP栈指针(MSP/PSP,CONTROL可以设置,handler模式只能用MSP),R14为LR链接寄存器,R15为PC指针。如下图所示

除了上面这15个寄存器之外,CM3还有其它五个特殊功能寄存器,xPSR,状态寄存器(三个),PRIMASK中断屏蔽寄存器,FAULTMASK可以屏蔽FAULT异常的寄存器,BASEPRI用来设置高于优先级的寄存器都被屏蔽,以及CONTROL控制寄存器,用于堆栈指针选择以及权级设置,如下所示,具体每个寄存器的说明,见《CM3权威指南》

与其它的ARM内核有一点不同,CM3内核只有两种模式,handler模式和thread模式,同时支持两种特权级别,特权级和用户级。两种模式是用来区分普通代码和异常(中断)代码的,异常和中断属于handler模式,普通用户属于thread模式,两个特权级是用来赋予当前代码不同的权力的,handler的代码一定是特权级,thread的代码可以是特权级,也可以是用户级,如下图所示

CM3在内核水平上搭载了一个异常响应系统,支持15个异常,240个中断,我们这里列举一下15个异常,重点关注一下PendSV异常,因为下面要用

上面的只是内核的部分知识,用来帮助我们进行下面上下文切换的理解。

上下文切换源码分析

上面我们说到要重点关注一下PendSV异常,我们这里先来介绍一下,如下是中断向量表

__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

我们看到了PendSV_Handler在倒数第二行,这块空间存放的是一个地址,指向PendSV_Handler,我们需要去编写一个叫做PendSV_Handler的函数,这个函数需要用汇编编写。记住这个函数。下面我们来看看具体的上下文切换是如何进行的,如下是上下文切换函数

#define  NVIC_INT_CTRL                      *((CPU_REG32 *)0xE000ED04)
#define  NVIC_PENDSVSET                                    0x10000000
OSCtxSw
    LDR     R0, =NVIC_INT_CTRL                                  ; Trigger the PendSV exception (causes context switch)
    LDR     R1, =NVIC_PENDSVSET
    STR     R1, [R0]
    BX      LR

这段代码的意思是向地址0xE000ED04写入0x10000000,0xE000ED04地址处是中断控制寄存器,向里面写入0x10000000可以触发PendSV_Handler,这个寄存器的每个位说明如下所示

触发PendSV异常后,会发生下面的三件事

  • 寄存器入栈:这是硬件方式的,把R0-R3,R12,xPSR,LR,PC,8个寄存器压入当前的堆栈(MSP/PSP)
  • 取向量:从中断向量表中取出对应的服务程序的入口地址
  • 重新选择SP(MSP/PSP),更新LR,PC

我们以UCOSIII中实际场景为例,当进行任务切换时,PendSV异常被触发,当前的SP为PSP,PSP指向当前任务的堆栈,上面所说的8个寄存器会被硬件压入当前任务堆栈中,与此同时,中断程序入口地址被取出,接着,当前SP由PSP切换为MSP,LR,PC都被更新,开始执行异常服务例程,异常服务例程如下:

PendSV_Handler
    //关中断防止发生其它高优先级中断,其它四条指令:CPSID I/CPSIE I/CPSID F/CPSIE F
    CPSID   I                                                   ;
   //将异常发生之前的进程的PSP送进R0
    MRS     R0, PSP                                             ; PSP is process stack pointer
   //如果R0是0,也就是说第一次发生任务切换,现在要切换的任务就是第一个任务,没有什么需要保存的,直接加载任务寄存器
    CBZ     R0, PendSVHandler_nosave                     ; Skip register save the first time
   //如果不是第一次发生任务切换,那么就需要将剩下的9个寄存器(R4-R11、SP)压入堆栈
   //先将进程堆栈指针向低地址生长32个字节,用来压R4-R11
    SUBS    R0, R0, #0x20                                       ; Save remaining regs r4-11 on process stack
   //将R4-R11压入
    STM     R0, {R4-R11}
   //将当前任务的PSP压入栈顶
    LDR     R1, =OSTCBCurPtr                                    ; OSTCBCurPtr->OSTCBStkPtr = SP;
    LDR     R1, [R1]
    STR     R0, [R1]                                            ; R0 is SP of process being switched out

                                                                ; At this point, entire context of process has been saved
PendSVHandler_nosave
    //将连接寄存器压入MSP
    PUSH    {R14}                                               ; Save LR exc_return value
    //调用OSTaskSwHook
    LDR     R0, =OSTaskSwHook                                   ; OSTaskSwHook();
    //跳转到钩子函数
    BLX     R0
    //将钩子函数R14弹出
    POP     {R14}

    //更新当前优先级
    LDR     R0, =OSPrioCur                                      ; OSPrioCur   = OSPrioHighRdy;
    LDR     R1, =OSPrioHighRdy
    LDRB    R2, [R1]
    STRB    R2, [R0]

    //更新当前任务控制块指针
    LDR     R0, =OSTCBCurPtr                                    ; OSTCBCurPtr = OSTCBHighRdyPtr;
    LDR     R1, =OSTCBHighRdyPtr
    LDR     R2, [R1]
    STR     R2, [R0]

    //让R0为新的任务的SP
    LDR     R0, [R2]                                            ; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr;
    //加载新的进程的R4-R11
    LDM     R0, {R4-R11}                                        ; Restore r4-11 from new process stack
    //指针移动,并将R0赋给PSP,也就是说当前PSP加载完成
    ADDS    R0, R0, #0x20
    MSR     PSP, R0                                             ; Load PSP with new process SP
    //确保异常返回时用的是进程堆栈
    ORR     LR, LR, #0x04                                       ; Ensure exception return uses process stack
    //使能中断
    CPSIE   I
    //异常返回
    BX      LR                                                  ; Exception return will restore remaining context

    END

小感慨:上面的源码分析完之后(UCOSIII源码也看完了),感觉写一个RTOS基本上没什么问题了(前后大概花了半个月时间),于是上网搜了一下,想看看大家怎么看待我这个想法,在某乎看了一下,总结一下大多数观点:

  • 玩具内核太简单,可以写一下练练手,但没什么意义
  • 商用级内核涉及到很多东西,几乎不可能写出来,即使写出来也很难找到门路发展生态,也没卵用

一句话,简单的没意义,难的写不出来,最终结论就是不写,所以我听取大家的意见,看内核源码,学移植,写内核就算了。

12-29 16:22