2.1.3 常见嵌入式操作系统的加载方式
在本节中,我们对嵌入式操作系统的加载方式进行简单的描述。之所以对嵌入式操作系统的加载方式进行描述,是为了让读者更好地了解常见的嵌入式系统的启动过程,以便可以根据实际需要,把Hello China移植到特定的目标系统上。
1.加载方式之一:从Flash直接加载
这种加载方式下,嵌入式操作系统映像和应用程序映像,都存放在Flash当中。在编译的时候,操作系统和应用程序映像的二进制模块被编译器分成了不同的节,包括TEXT节、DATA节、BSS节等。其中,不同的节所存放的内容不同,TEXT节存放了可执行代码;DATA节存放了已经初始化的全局变量;而BSS节是一个预留节,存放了未经初始化的全局变量等。
在这种加载方式下,嵌入式系统的启动过程如图2-2所示。
图2-2 从Flash直接加载嵌入式系统
① CPU复位完成,执行启动向量所在的第一条指令(位于Boot ROM内),这条指令往往是一条跳转指令,跳转到Boot ROM内的硬件初始化代码位置,执行必需的硬件初始化工作。
② 硬件初始化代码完成CPU的初始化,比如设置CPU的段寄存器、堆栈指针等,以及其他硬件的初始化。
③ 完成硬件的初始化功能后,会通过一条跳转指令(或函数调用指令),跳转到Flash存储器的特定位置开始执行。这个位置,一定是代码段(TEXT段)中的一个特定位置。
④ 把Flash中DATA节代码复制到RAM中。
⑤ 完成DATA节的复制后,Flash中的代码会根据BSS节的大小,在RAM中预留相应的内存空间,留给未初始化的变量使用。
上述功能完成之后,嵌入式系统的执行环境已经准备完毕,进入操作系统初始化阶段。
需要注意的是,DATA节的搬迁和BSS节的预留工作,也可能由Boot ROM完成,即Boot ROM中的硬件初始化代码执行完毕之后,会通过一些内存搬移指令,把Flash中的DATA节搬移到RAM中,然后再根据BSS节的大小,预留BSS空间。这些工作完成之后,就可通过一条跳转指令,跳转到TEXT节的某个位置(一般是操作系统的入口函数)开始执行。这种情况下,Boot ROM中的代码需要知道DATA节和BSS节的详细信息(大小、起始地址等)。
在这种加载方式下,所有执行指令都是从Flash中读取的,RAM中只是存放了数据和堆栈。这种启动方式可以节约物理内存空间,因为不需要专门为代码预留内存空间,一般应用在内存数量受到限制的系统中。这种加载方式,有以下缺点。
(1)由于代码直接在Flash中执行,一般情况下对Flash的访问,速度会比对RAM的访问要慢很多,因此性能会受到影响。若CPU本身携带了较大数量的代码Cache,这个问题则会得到一定程度的缓解。
(2)代码直接从Flash中运行,而一般情况下,Flash是只读的,因此无法实现代码的自我修改功能(即动态修改代码),这样不利于代码级的调试。
(3)在对操作系统核心和应用程序代码进行编译的时候,必须指定TEXT节、DATA节和BSS节的起始地址,这个地址应该与它们在Flash和RAM中的最终位置相同。因此,会给开发带来一定的困难。
总之,这种加载方式在一些性能要求不是太关键、硬件配置受到限制的系统中,被大量地采用。Internet发展初期的一些低端路由器经常采用这种方式。
2.加载方式之二:从RAM中加载
与从Flash直接加载方式不同的是,在这种方式下,代码和数据都被加载到RAM中,从RAM中运行。这样从Flash直接运行的一些弊端就可以消除掉了。这种方式下,加载过程如图2-3所示。
图2-3 从RAM中加载的过程
① CPU复位完成,执行启动向量所在的第一条指令(位于Boot ROM内)。这条指令往往是一条跳转指令,跳转到Boot ROM内的硬件初始化代码位置,执行必需的硬件初始化工作。
② 硬件初始化代码完成CPU的初始化,比如设置CPU的段寄存器、堆栈指针等,以及其他硬件的初始化。
③ 位于Boot ROM中的代码,把位于Flash中的操作系统和应用程序的TEXT节(代码节),从Flash中搬移到RAM中。这个过程,需要Boot ROM内的搬移代码预先知道TEXT节的起始地址和大小,这可以通过在操作系统映像的开始处(该位置往往是固定的)设置一个数据结构,来指明这些节的大小,以及起始位置和目标位置等。这样在搬移的时候,Boot ROM中的代码就可以先从这个固定位置获得节的信息,然后再根据这些信息来完成搬移工作。
与第③步类似,Boot ROM中的代码把操作系统和应用程序映像的DATA节,搬移到RAM中。
⑤ Boot ROM中的启动代码完成BSS节的RAM空间预留。
⑥ 完成上述所有动作之后,Boot ROM中的代码通过一条跳转指令跳转到RAM中操作系统的入口点,正式启动操作系统。
这种启动方式是一种比较常见的启动方式,简便易行,而且克服了直接从Flash中运行代码的一些弊端。但这种方式需要嵌入式系统配置较多的RAM存储器,因为操作系统和应用程序的代码直接在RAM中执行。
3.启动方式之三:从文件系统加载运行
在上面介绍的两种启动方式中,操作系统和应用程序的二进制映像存放在Flash当中,而Flash直接映射到CPU的可寻址空间,因此在加载的时候,直接通过访存指令就可以完成,无需额外的设备驱动程序。但这种方式需要系统配置较多的Flash存储器,这在操作系统映像很大的情况下,矛盾尤其突出。而下面介绍的一种方式,是从外部存储器加载操作系统和应用程序映像的,可以解决该问题。
这种从文件系统加载操作系统映像的方式与从Flash加载方式不同的是,操作系统映像和应用程序映像是存放在外部存储器(比如,IDE接口的硬盘)中的。而对于外部存储器的访问所需要的驱动程序,则存放在Boot ROM中。这种方式的加载过程如图2-4所示。
图2-4 从文件系统加载运行过程
① CPU复位完成,执行启动向量所在的第一条指令(位于Boot ROM内),这条指令往往是一条跳转指令,跳转到Boot ROM内的硬件初始化代码位置,执行必需的硬件初始化工作。
② 硬件初始化代码完成CPU的初始化,比如设置CPU的段寄存器、堆栈指针等,以及其他硬件的初始化。
③ Boot ROM中的引导代码通过外部设备的驱动程序读取外部存储器,把操作系统和应用程序的映像加载到内存(RAM)中。这个过程中,可以不区分TEXT节和DATA节,而作为统一的二进制映像进行加载。
④ 完成二进制映像的加载后,Boot ROM中的启动代码需要进一步为操作系统预留BSS空间。
⑤ 上述一切动作完成、操作系统运行的环境就绪后,Boot ROM中的代码通过一条转移指令(跳转或CALL),跳转到操作系统的入口点,这样后续系统的运行就完全在操作系统的控制下进行了。
这种加载方式把操作系统和应用程序的映像都搬移到了外部存储介质上,不但可以节约Flash存储器,而且可以适应大容量的操作系统映像的情况。另外,由于引入了外部存储介质,嵌入式系统运行过程中产生的一些状态数据,比如告警、日志等信息,就可以存储在大容量的外部存储介质上,这样十分便于问题的定位。
但这种方式实现起来相对复杂,需要额外的存储介质访问接口电路(比如,IDE接口电路),而且需要软件实现针对这些外部存储介质的驱动程序,可能会大大延长开发周期。这种模型,一般应用到一些大型的嵌入式系统设计上,比如,位于Internet核心位置的核心路由器(GSR或TSR),就可能需要采用这种方式进行设计。
这种方式设计的另外一点好处,就是可以在线升级(系统运行过程中,对操作系统和应用程序软件进行升级)。实际上,在从RAM中运行代码的方式中,也可以实现在线升级,但采用外部存储介质的在线升级会更完善、更利于使用。比如,有的情况下,可能为不同的应用定制不同的软件和嵌入式操作系统,并编译成不同的模块,这些模块都存放在外部存储器上。在嵌入式设备安装完成后,可以根据需要,选择一种特定的功能进行加载,这时候,就可以通过修改位于外部存储器上的系统配置文件来告诉系统,默认情况下加载哪个模块(或版本)。
实际上,PC上的操作系统就是这样一种系统,对于PC使用的操作系统,一般位于外部存储介质,比如软盘或硬盘上,PC启动的时候,从这些外部存储介质上加载操作系统。从这一点上来说,PC本身就是一个复杂的嵌入式系统,而且很有典型意义,在嵌入式开发过程中遇到的普遍问题,都可以在PC上模拟。因此,对于嵌入式开发入门者来说,在PC上模拟嵌入式开发环境,以达到学习的目的,是十分可行的。但在嵌入式开发中遇到的一些专业问题,比如特定硬件芯片(比如,网络处理器等)的初始化和应用等,则在PC上可能无法模拟。但有了嵌入式开发的基本概念和技能,转而从事这类更专业的开发,所需要的仅仅是一个很短的学习周期而已。
除了我们介绍的几种加载方式之外,在嵌入式开发领域中,还有另外的一些加载方式,比如从串口(COM接口)加载、从以太网接口(Ethernet接口)加载等,这些加载方式与从外部存储设备加载类似,无非需要Boot ROM额外实现一个驱动程序来完成这些设备数据的读取,在此不进行详细描述。