首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

实现一个什么都不能做的嵌入式操作系统[转帖]

实现一个什么都不能做的嵌入式操作系统[转帖]

1.首先确定CPU,在这里为了简单,就选用嵌入式的CPU,比如ARM系列,之所以用RISC(简单指令集)
类型的CPU,其方便之处是没有实模式与保护模式之分,采用线性的统一寻址,也就是不需要进行段
页式内存管理,还有就是芯片内部集成了一些常用外设控制器,比如以太网卡,串口等等,不需要像
在PC机的主板上那么多外设芯片
2.确定要实现的模块和功能,为了简单,只实现多任务调度(但有限制,比如最多不超过10),实
现中断处理(不支持中断优先级),不进行动态SHELL交互,不实现动态模块加载,不实现fork之类
的动态进程派生和加载(也就是说要想在你的操作系统上加入用户程序,只能静态编译进内核中;不
支持文件系统,不支持网络,不支持PCI,USB,磁盘等外设(除了支持串口,呵呵,串口最简单嘛),
不支持虚拟内存管理(也就是说多任务中的每个进程都可以访问到任何地址,这样做的话,一个程序
死了,那么这个操作系统也就玩完了)
3.确定要使用的编译器,这里采用GCC,文件采用ELF格式,当然,最终的文件就是BIN格式,GCC和
LINUX有着紧密的联系,自己的操作系统,需要C库支持和系统调用支持,所以需要自己去裁剪C库,
自己去实现系统调用
4.实现步骤:首先是CPU选型,交叉编译环境的建立,然后就是写BOOTLOADER,写操作系统


如何实现BOOTLOADER


1.之所以要实现一个专用的BOOTLOADER,一是为了更好的移植和自身的升级,二是为了方便操作系统的调试,当然,你完全可以将这部分所要实现的与操作系统相关的功能集成到


操作系统中去
2.确定一个简单的BOOTLOADER所要完成的功能:我们这里只需要完成两个主要功能,一是将操作系统加载到内存中去运行,二是将自己和操作系统内核固化到ROM存储区(这里的


ROM可以是很多设备热缜度胧叫酒械?/SPAN>FLASH,PC机上的软盘,U盘,硬盘等)


3.BOOTLOADER的编写:
第一步:要进行相关硬件的初使化,比如在at91rm9200这块嵌入式板子上(以后都使用这一款芯片,主要是我对这款芯片比较熟悉,嘿嘿),大概要做接下来的几方面的工作,其


一:将CPU模式切换进系统模式,关闭系统中断,关闭看门狗,根据具体情况进行内存区域映射,初始化内存控制区,包括所使用的内存条的相关参数,刷新频率等,其二:设定系


统运行频率,包括使用外部晶振,设置
CPU频率,设置总线频率,设置外部设备所采用的频率等。其三:设置系统中断相关,包括定时器中断,是否使用FIQ中断,外部中断等,还有就是中断优先级设置,这里只实现两


个优先级,只有时钟中断高一级,其它都一样,而中断向量初始化时都将这些中断向量指向0x18处,并关闭这里的所有中断,如果板子还接有诸如FLASH设备的话,还需要设置诸如


FLASH相关操制寄存器,其四:需要关闭CACHE,到此为止,芯片相关内容就完成初始化了


第二步:中断向量表,ARM的中断与PC机芯片的中断向量表有一点差异,嵌入式设备为了简单,当发生中断时,由CPU直接跳入由0x0开始的一部分区域(ARM芯片自身决定了它中断


时就会跳入0x0开始的一片区域内,具体跳到哪个地址是由中断的模式决定的,一般用到的就是复位中断,FIQ,IRQ中断,SWI中断,指令异常中断,数据异常中断,预取指令异常


中断),而当CPU进入相应的由0x0开始的向量表中时,这就需要用户自己编程接管中断处理程序了,这就是需要用户自己编写中断向量表,中断向量表里存放的就是一些跳转指令


,比如当CPU发生一个IRQ中断时,就会自动跳入到0x18处,这里就是用户自己编写的一个跳转指令,假如用户在此编写了一条跳转到0x20010000处的指令,那么这个地址就是一个


总的IRQ中断处理入口,一个CPU可能有多个IRQ中断,在这个总的入口处如何区分不同的中断呢?就由用户编程来决定了,具体实现请参见以后相关部分,中断向量表的一般用一个


vector.S文件,当然,如何命名那是你自己的喜爱,但有一点需要声明,那就是在链接时一定要将它定位在0x0处



第三步:设置堆栈,一般使用三个栈,一个是IRQ栈,一个是系统模式下的栈(系统模式下和用户模式共享寄存器和内存空间,这主要是为了简单),设置栈的目的主要是为了进行


函数调用和局部变量的存放,不可能全用汇编,也不可能不用局部变量



第四步:将自己以后的代码段和数据段全部拷贝至内存,并将BSS段清零



第五步:进行串口的初始化(主要是为了与用户交互,进行与PC机的文件传输),FLASH的初始化这里在FLASH中存放BOOT和内核),FLASH驱动的编写(这里的驱动有别于平常所说


的驱动,由于FLASH不像SDRAM,只要设定了相关控制器之后就可以直接读写指定地址的数据,对FLASH的写操作是一块一块数据进行,而不是一个字节一个字节地写,具体请查阅相


关资料)
第六步:等待一定的秒数,来接收用户进行输入,如果在指定的秒数内用户未输入任何字符,那么
BOOT就开始在FLASH中的指定位置(可以由自己指定,这么做主要是为了简单)读取内核的所有数据到内存中(具体是内存中的什么位置由自己指定,也可以采用LINUX之类的做法


,就是在内存的起始位置加上一个0x8000处),将跳转到内核的第一条代码处);如果用户在指定的秒数内键入了字符(这主要是为了方便开发,如果开发定型之后完全可以不要


这段代码),那么就在串口与用户进行交互,接受用户在串口输入的命令,比如用户要求下载文件在FLASH中指定的位置等,具体内容可参考U-BOOT之类的开源项目到这里为止,


BOOT部分已完成,这个BOOT非常简单,仅仅只是将PC机上传下来的文件固化到FLASH中,然后再将FLASH中的操作系统内核部分加载进内存中,并将CPU的控制权交给操作系统,下一


页开始讲解如何写一个最简单的操作系统,呵,到现在才开始切入正题呢!!!!

51 c8051f(f020,f040) msp430 arm(2410,2510) fpga(xc3s4000) dsp(5116 dm642) keilc vc++ matlab linux protel Ten_layerPCB mpegx h.26x Rscode Turbocode ofdm VideoBroadcasting ldpc_code(now!)
如何实现一个最简单的操作系统

这里为了简单,就不考虑可移植性开求,不从BOOT部分来接收参数,也不对硬件进行检测,
也不需要进行DATA段,代码段的重定位。我只是读了LINUX内核相关部分,并未自己去实现
一个操作系统,所以我以下所说的只是概念性的东西:


1.接管系统的中断处理,由于BOOT部分的代码决定了那个中断向量表,从而决定了系统中断
之后进入的内存位置,但BOOT并不知道操作系统的中断处理函数位置所在啊,怎么办呢?
有几种方法,其一是:如果你的板子可以重映射地址,也就是可以将内存条所在的位置
重映射成0x0开始,那么在链接内核的时候,就将操作系统自己的中断向量表定位在0x0处
并且在BOOTLOADER引导结束时就完成映射操作,并让CPU跳转到0x0处执行;如果没有重映
射功能,我就不晓得怎么办了,不过我想到一个折衷的办法,就是在BOOTLOADER启动完成
时(也就是将CPU控制权交给操作系统内核时),重新改写FLASH的0x0区域,就是将操作
系统的内核的中断向量表写入FLASH区的0x0处,比如,当一个IRQ发生时,CPU决定了会
跳入0x18(假设这里FLASH占用地址总线0x0至0x0fffffff,内存占用0x20000000至0x2fffffff)
,而BOOTLOADER在最后将0x18处的代码修改成了0x20000000加上0x18的地址处的代码,而这个
地址就是内核的中断向量表中的相关跳转指令,就相当于跳转进了内核所关联的IRQ处理函数
的地址上去执行中断处理函数了,而这样的不好之处在于:当系统重新上电之后,BOOT的
中断向量表已经被修改,除非BOOT本身不使用中断,呵,在这样简单的系统中,BOOT是不
需要中断功能的

2.这里为了简单,所以没有使用分页内存管理,就不需要建立页表等操作,直接进行操作
系统的堆栈设置,同BOOT一样的设置过程一样,接着就进行BSS段清零操作,这里的BSS段
是指操作系统自身的BSS段,与BOOT的BSS段是同一个含义只是用在了不同的地方了,接着
就跳入了MAIN函数

3.为了最大可能的简单,采用静态建立任务结构数组,比如只建立十个任务,那么首先要
为这十个任务结构分配段内存,可以在堆上分配(这个分配的内存直到操作系统结束才会
被释放,当然也可以指定一片操作系统的其它地方都用不到的内存区域,不过这样写的话
就有点外行的味道了,而符务结构数组的指针却是全局变量,存放在BSS段或者DATA段),
由于在上一步中已经分配了一个系统堆栈,那么我们这十个任务就分享这总体的堆栈区域
这里的重点就是如果定义每个任务结构数组里面的结构,可以参照LINUX的相关部分设计

4.中断处理:在第一步中已经确定了CPU进行相关的几类型的中断跳转地址,而相同类型
的中断却只有一个入口地址,这里的中断处理就会完成以几个动作:
其一:入栈操作,包括所有寄存器入栈,至于这个栈,就是在第二步中所设置的IRQ栈,
其二:屏掉所有中断,呵,这里为了简单起见,所以在处理中断时不允许再次发生中断
其三:读取中断相关的寄存器,判别是发生了什么中断,以至于跳进相关的中断处理函
数中去执行(在这里只包括两种中断,一是时钟中断,另一个是SWI中断,也就是所谓
的系统调用时需要用到的)
其四:等待中断处理完成,然后就开启中断并出栈,恢复现场,将CPU控制权交给被中断
的代码处
注意:
其一:在MIAN中必须首先确定整个系统有哪些需要处理的中断,也就是有哪些中断处理
函数,然后才编写这里的中断处理函数
其二:本操作系统不处理虚拟内存,其至连CPU异常都不处理(一切都为了简单),一旦
发生异常,系统就死机

5.对TIMER的实现,首先确定时间片,为了让系统更稳定,而且我们不需要实时功能,尽
可能让时间片设置长一点,比如我们让一个任务运行20个时钟滴答数,然后应根据系统
频率来确定每个系统滴答所占用的毫秒,这里使用5毫秒让系统定时器中断一次,那么就
需要写时钟寄存器,具体参阅芯片资料,计算下来,一个任务最大可能连续运行100毫秒
,注意:我们的操作系统不支持内核抢占,同时只支持两级中断优先级,就是只有时钟
中断的优先级高一点,其它的优先级都低一级,但是在中断处理一节中却屏掉了这个功能
因为一进入中断处理,就禁止中断,所以不管其它中断优先级有多高都没有用的,这样做
优点是简单了,但不好之处显而易见,特别在相关中断处理函数如果进入了死循环,那么
整个系统就死了,而且时间片也变得不准确了,反正都不用实时,也不需要实时钟支持嘛
至于中断优先级设置请参阅芯片资料

51 c8051f(f020,f040) msp430 arm(2410,2510) fpga(xc3s4000) dsp(5116 dm642) keilc vc++ matlab linux protel Ten_layerPCB mpegx h.26x Rscode Turbocode ofdm VideoBroadcasting ldpc_code(now!)
6.进程调度的实现,也就是do_timer函数(时钟中断处理函数),有一个全局变量指针,
指向的就是当前任务结构数组(或者链表),当时钟中断时,就进入此函数中,首先判断
任务结构体中的时间片是否用完,如未用完,就减一,然后退出中断,让CPU继续运行当
前的任结构,若用完了时间片,就重置时间片,并重新寻找任何结构数组中的下一个等待
运行的任务,若找到了,就切换至新的任务,至于如何切换,请见下一页描述,如果未找
到就切换到IDLE任务(类似于LINUX,呵呵,所有的处理就是模仿LINUX,由于本人水平太
差,所就不能自创一招),注意:为了简单,所以没有实现任务优先级,也未实现任务
休眠等,也就是说只要静态地决定了有十个任务,这十个任务就按先后顺序一个一个执行
而且每个任务都不允许结束,就是说在每个进程中的最后一句代码都必须用死循环,不然
的话系统就跑飞了),还有一点,进程不支持信号,没有休眠与唤醒操作,这个CPU就是
不停地在运行,呵呵,反正CPU又不是人,所以不需要人权的哈!!!这种调度是不是简
单得不能再简单了?????!!!!

7.串口不使用中断,这就是最大可能的降低难度,串口使用论询的方式来实现读写(当
然是阻塞的方式了哦,而且只有写,不允许读,因为读的时候需要涉及到采用中断方式,
因为轮询方式有个不好的地方,那就是正在读的时候,这里有可能当前进程的时间片用
完了,系统切换到另一个进程,这里你在PC机的串口输入的数据就丢弃了,唉,又是为
了简单嘛)

8,最后一步就是MIAN函数的最后一部分,将本进程当作IDLE进程(相当于修改任务结构
数组中的数据),开启中断,将当前进程加入一段死循环,以免它退出去。

9.编译你的BOOTLOADER,KERNEL,并烧写至FLASH,反复调试

10.至此将你的at91rm9200(或者是其它相类似的芯片)的串口接上PC机,打开超级终端,
打开板子电源,说不定你的操作系统就打印出了"hello,world"了!!!一个最简单的操作
系统就出来了

下一页是具体的功能模块实现

任务结构数组(或链表)的实现

我们的任务结构就采用链表形式吧,但其长度是限定了的,头指针是一个全局指针变量(
指针变量是一个无符号整型指针,其指针本身所在的地址是在BSS段,但其指向的内容是分
配在堆上的一片内存),分配内核内存的函数就用kmalloc吧,kmalloc函数需要自己编写
呵,为了简单,这个函数只接受一个参数,就是所需分配大小,这个函数做得很简单,首先
有一个全局针指,它在初始化时指向了整个堆的起始位置,并且固定大小,就是所谓的内核
堆栈,在内核堆栈之后就是用户堆栈,由于总共有十个任务,当然不包括内核本身的任务,
所以整个堆栈就平均分成十一部分,注意:在所有任务初始化完成之后,还有一个步骤就是
将内核这个任务移到用户态,相当于要将自己的任务结构的堆栈指针修改一下就行了),
判断大小是否超出了内核堆的可分配范围,还有一点,需要维护内核堆和其它任务的堆,
需要进行分块,并且有一个全局的内存使用标识,就用数组吧,简单,0表示相应的内存
部分未占用,1就表示占用,对应的kfree就相当于把标志置0),
对于内存的维护,比较复杂,为了简单,就定为4K,并且不能进行大于四K的内存申请,因为
大于4K之后,由于没有虚拟地址的概念,就不能实现堆上的连续分配地址,当然在栈上分配
是可以大于4K的,栈是由编译器和CPU所决定了的

任务结构包括:
1.所剩的时间片
2.本任务所指向的代码段内存地址,这里也就是函数入口地址
3.本任务所指向的数据段地址,这里的数据段被包含进了整个内核中,所以并没有用,作为保留
4.本任务的函数体是否存在,也就是否会被调度
51 c8051f(f020,f040) msp430 arm(2410,2510) fpga(xc3s4000) dsp(5116 dm642) keilc vc++ matlab linux protel Ten_layerPCB mpegx h.26x Rscode Turbocode ofdm VideoBroadcasting ldpc_code(now!)
返回列表