@
序言
前天学习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基本上没什么问题了(前后大概花了半个月时间),于是上网搜了一下,想看看大家怎么看待我这个想法,在某乎看了一下,总结一下大多数观点:
- 玩具内核太简单,可以写一下练练手,但没什么意义
- 商用级内核涉及到很多东西,几乎不可能写出来,即使写出来也很难找到门路发展生态,也没卵用
一句话,简单的没意义,难的写不出来,最终结论就是不写,所以我听取大家的意见,看内核源码,学移植,写内核就算了。