声明
这里在内核方面主要借鉴了网上某位网友的一篇文章《自己实现一个实时操作系统》和《嵌入式实时操作系统ucOS-2》,并做了适当的改进和简化,可能有些地方和上面提到的资料有些地方雷同,毕竟是个相对简单的rtos,无法摆脱这个框架,不过目的也很单纯,怀着纯粹的兴趣开发,或者说移植,为了能帮助更多有相同兴趣的人理解学习,本人当时也是怀着学习的心情看了资料搞出这个rtos的,也希望在今后的开发中留下一个纪念,所以在这里和大家分享下。
好了,开始吧,先把我的实现框架和系统构成介绍下,作为一个操作系统,基本组成是不变的,内核,内存管理,文件系统,进城通信,网络,就是这基本的五大块构成的,本人在目前的公司的产品板子上利用业余时间开发,主要实现了除网络模块以外的其余部门,由于板子上没有安网络芯片,而且网络更加偏向于驱动,这里就不放到rtos里了,那么,就从内核开始吧。
平台
内核,首先要介绍下平台,也就是cpu架构,我们公司的产品是使用的是arm7架构的cortex-M3内核,与《自己实现一个实时操作系统》作者使用的相同的芯片,目前st这款芯片在国内的低端处理器市场卖的很好,主要优势就是价格和速度,由于是arm7架构,那么不支持mmu,非常适合ucOS-2和ucLinux的移植,后面会介绍下cortex芯片在内核开发中的具体使用。
启动
内核在操作系统中的启动流程,大致就是初始化接口,寄存器,变量,然后创建一个空闲任务,把控制权交给这个任务,然后开启进程中断,开始任务调度。
初始化
主要包括驱动用到的接口,串口这一类的初始化,当然,这些也可以在驱动中自己进行操作,开辟任务堆栈,这个比较重要,每个任务都需要自己的堆栈,堆栈主要用于在进程调度中存放压栈的指针和寄存器。在创建任务的时候,先初始化堆栈,代码如下
PUSH {R4-R6,LR}
MOV R4,R0
MOV R3,R1
MOV R5,#0x21000000
STR R5,[R3] //XPSR
SUB R3,R3,#4
STR R4,[R3] //PC
SUB R3,R3,#4
STR R4,[R3]
;STR R5,[R3] //LR
SUB R3,R3,#4
MOV R4,#11
STR R4,[R3] //R12
SUB R3,R3,#4
MOV R4,#12
STR R4,[R3] //R3
SUB R3,R3,#4
MOV R4,#13
STR R4,[R3] //R2
SUB R3,R3,#4
;MOV R4,#14
ASRS R5,R5,#1
STR R5,[R3] //R1
SUB R3,R3,#4
;MOV R4,#15
STR R1,[R3] //R0
SUB R3,R3,#4
MOV R4,#6
STR R4,[R3]
SUB R3,R3,#4
MOV R4,#7
STR R4,[R3]
SUB R3,R3,#4
MOV R4,#8
STR R4,[R3]
SUB R3,R3,#4
MOV R4,#9
STR R4,[R3]
SUB R3,R3,#4
MOV R4,#10
STR R4,[R3]
SUB R3,R3,#4
MOV R4,#11
STR R4,[R3]
SUB R3,R3,#4
MOV R4,#12
STR R4,[R3]
SUB R3,R3,#4
MOV R4,#13
STR R4,[R3]
MOV R0,R3
POP {R4-R6,PC}
BX LR
这是基于arm7的寄存器代码,主要是往每个任务的初始化堆栈里放点东西,其中需要注意的是,在压xpsr这个寄存器的时候,必须用#0x21000000,否则在开始调度的时候会有问题。
进程调度
当任务A正在运行时,时间片轮询到了,这时产生一个异常,这时,cpu就需要把任务A的进程信息都保存起来,放到任务A指定的堆栈中,如果时间片轮询到了,告诉A继续运行,那cpu将刚刚放到堆栈中的进程信息取出来,这样任务A就继续跑起来了,这里压栈和出栈根据不同的cpu架构,方法和规则有所不同,但其实现的本质是一样的。这里进程调度的核心代码如下:
__OS_Sched
MRS R0, PSP
SUB R0, R0, #0x20
STM R0, {R4-R11}
LDR R4, =TOSCurTCB
LDR R4, [R4]
STR R0, [R4] //将当前任务的寄存器压栈
LDR R4, =TOSCurTCB
LDR R6, =TOSNewTCB
LDR R6, [R6]
STR R6, [R4]
LDR R0, [R6]
LDM R0, {R4-R11}
ADD R0, R0, #0x20
MSR PSP, R0
ORR LR, LR, #0x04 //新任务的寄存器出栈,并进入新任务运行
BX LR
临界区控制
在操作系统运行过程中,会有很多临界区,例如,有些任务在进行过程中如果进入中断就会使任务无法正常运行,这时就需要关闭中断,这样就避免代码和中断进入临界区,通常微处理器一般都有关中断/开中断指令,这里arm7的操作指令如下
关闭中断,并保存当前中断状态
MRS R0, PRIMASK
CPSID I
BX LR
打开中断,
MSR PRIMASK, R0
BX LR
创建第一个任务,并开始运行
将第一个任务初始化后,打开处理器的systick中断,这就是操作系统进行调度和运作的发动机,然后将第一个任务的寄存器出栈,将堆栈指针指向psp(任务堆栈),这样,第一个任务就跑起来了,加载任务的代码如下:
LDR R4, =TOSCurTCB //当前任务的结构体指针
LDR R0, [R4]
LDR R0, [R0]
LDM R0,{R4-R11}
ADD R0,R0,#0x20
MSR PSP, R0 //将堆栈指针指向psp
ORR LR, LR, #0x04
BX LR //返回,直接进入psp的堆栈模式运行。
优先级选择
这是任务调度的核心问题,对于一个rtos,实时性永远是其最最关心的问题,这里不详细列举各个rtos的实时性处理,简单的介绍下本操作系统的实时性的简单实现框架,在systick的中断函数中实现其实时性选择调度
SysTick()
{
//关中断
TCB *pTCB;
unsigned char index = 0;
TOSNewTCB = NULL;
/*判断任务调度是否关闭*/
if(TosParam.OSScheLock == TOS_TaskSchedLock)
{
TOS_INT_Enable();
return;
}
/*遍历所有进程任务*/
for(index = 0;index < MAX_TASK_NUM;index ++)
{
pTCB = TOSTCBTable+index;
if(NULL == pTCB->pStackTop)
continue;
/*判断时间片是否到*/
if(pTCB->TCBDelay)
{
pTCB->TCBDelay --;
continue;
}
/*判断任务是否挂起*/
if (pTCB->TaskStat == TOS_Task_Pend)
{
continue;
}
/*判断优先级*/
if (TOSCurTCB->CurPriority < pTCB->CurPriority)
{
TOSNewTCB = pTCB;
continue;
}
if (TOSCurTCB->CurPriority > pTCB->CurPriority)
{
if (TOSNewTCB == NULL)
{
TOSNewTCB = pTCB;
Continue;
}
}
if (TOSCurTCB->CurPriority == pTCB->CurPriority)
{
if(TOSCurTCB == pTCB)
{
if(TOSNewTCB == NULL)
TOSNewTCB = pTCB;
continue;
}else if(pTCB < TOSCurTCB){
if(TOSNewTCB == NULL)
TOSNewTCB = pTCB;
continue;
}else if(pTCB > TOSCurTCB){
TOSNewTCB = pTCB;
break;
}
}
}
if(TOSCurTCB != TOSNewTCB)
TOSCtxSw(); //开始调度
//开中断
}
1. 首先判断进程调度是否关闭
2. 查询进程任务表,选择非空的任务进程
3. 进程时间片没有到或者进程被挂起,继续查找下一个
4. 找到最高优先级,调入新进程
5. 如果优先级相等,则调入新进程。
6. 如果新进程不等于当前进程,进行任务调度
这样的调度方法思路很单一,就是找优先级最高的,优先级低的任务根本没有机会轮询,不过对于一个实时性要求非常高的场合,这个方法还是实用的。
进程管理
这里进程的结构体是
typedef struct taskControlBlock
{
/*堆栈指针*/
STACK_TYPE *pStackTop;
/*优先级*/
PRIORITY_TYPE CurPriority;
/*进程状态*/
unsigned char TaskStat;
/*时间片*/
unsigned long TCBDelay;
/*ID*/
unsigned char TCB_ID;
/*通信管道*/
ECB *TCB_ECB;
unsigned char TCB_MSGNUM;
} TCB, TASK_TYPE;
给每个进程分配相应的堆栈,时间片,id,进程就可以跑起来了,本操作系统主要采用静态分配进程表,在一个进程数组中对进程进行管理,这样好处是代码实现方便,但无法支持动态增加进程,并且进程数有限定,不过对于一个本例,已经足够。当需要加入一个新任务时,只需要挑选一个空的进程空间就可以。
这样,基本的内核体系就实现了。