Boot Loader概述
简单地说,在操作系统内核运行之前,通过一小程序,可以初始化硬件设备、建立内存空间的映射图等,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核配置好相应的环境,也可以下载文件到系统板上的SDRAM,对Flash进行擦除与编程,这个小程序一般称为Boot Loader。可以说,一个功能完善的Boot Loader已经相当于一个微型的操作系统了。
Boot Loader作为系统复位或上电后首先运行的代码,一般应写入Flash存储器并从起始物理地址0x0开始。Boot Loader是非常依赖于硬件而实现的,而且根据实现的功能不同,其复杂程度也各不相同。一个简单的Boot Loader可以只完成USB口的初始化,而功能完善的Boot Loader可以支持比较复杂的命令集,对系统的软硬件资源进行合理的配置与管理。因此,建立一个通用的Boot Loader几乎是不可能的。
尽管Boot Loader的功能各不相同,我们仍然可以对Boot Loader归纳出一些通用的概念来,以指导用户对特定要求的Boot Loader设计与实现。Boot Loader一般应包括系统初始化过程,然后用户根据其系统的自身需求,具体设计。
系统初始化代码直接对ARM微处理器内核及硬件控制器编程,多采用汇编语言编程,初始化代码一般应包括如下典型任务:
1.       定义程序入口点;
2.       设置异常和中断向量表;
3.       初始化存储设备;
4.       初始化堆栈指针寄存器;
5.       初始化用户执行环境;
6.       呼叫主应用程序。
1.1 定义程序入口
    初始化代码必须定义整个程序的入口点。通过伪指令Entry指定编译器保留该段代码,同时配合链接器的设置,确定整个程序的入口点。
1.2 设置异常和中断向量表
异常和中断处理是嵌入式系统的重要核心部分。它们负责处理错误、中断和其它由外部系统触发的事件。
ARM要求异常向量表必须放置在从0地址开始,连续32(8X4)字节的空间内,各异常的位置如表1所示。
每当一个异常发生以后,系统执行完当前指令后,ARM微处理器会执行以下几步操作:
(1)          把cpsr保存到相应异常模式下的spsr;
(2)          把pc保存到相应异常模式下的lr;
(3)          设置cpsr为相应的异常模式(异常及其对应的模式见图1);
(4)          设置pc为相应异常处理程序的入口地址。
因为每个异常只占据向量表中1个字的存储空间,只能放置一条ARM指令,所以该指令必须使程序跳转到存储器存储异常处理程序的地方,再执行异常处理程序。
执行完异常处理程序,要从异常中断处理程序中返回。
从异常中断处理程序中返回包括下面两个基本操作:
(1)          恢复被屏蔽的程序的处理器状态
(2)          返回到发生异常中断的指令的下一条指令处继续执行。
当异常发生时,程序计数器PC所指的位置对于各种不同的异常是不同的,同样,返回地址对于各种不同的异常中断也是不同的。例外的是,复位异常中断处理程序不需要返回,因为整个应用系统是从复位异常中断处理程序开始执行的。
总的来说,异常处理过程为:当异常发生时,系统执行完当前指令后,将跳转到相应的异常处理程序处执行。当异常处理程序执行完成后,程序返回到发生异常的指令的下一条指令处执行。在进入异常处理程序时,要保存被中断的程序的执行现场。从异常处理程序退出时,要恢复被中断的程序的执行现场。
IRQ和FIQ异常是用来处理外围设备中断的,不同的ARM芯片生产商,生产的芯片会有所不同。
1.3初始化存储设备
初始化存储器系统主要是对系统存储器控制器的初始化,如果系统具有存储器管理单元,也必须对其进行初始化。
ARM微处理器架构的理论寻址能力为4GB,但是对于一个特定的系统来说,所配备的实际物理存储器远没有那么多。出于对芯片的面积、成本、使用灵活性等方面的考虑,这么大容量的存储器如果设计在芯片内部是不能被接受的。因此,基于ARM微处理器的系统一般都需要外扩大容量的存储器,这些存储器是由专门的存储器控制器控制的。
因为存储器控制器并非标准ARM微处理器架构的一部分,所以不同的ARM微处理器,设计也会各不相同,初始化代码自然也会有一些差异,但一般包含如下两个方面:
1.       存储器类型和时序的配置
ARM微处理器一般都设计有多种类型的存储器接口,需要根据具体的系统设计加以正确配置,且对于同在一种类型的存储器,也会因为访问速度的差异进行不同的时序设置。
存储器接口时序优化是非常重要的,这会影响到整个系统的性能。因为一般系统运行的速度瓶颈都存在于存储器访问,所以存储器访问时序应尽可能的快;而同时又要考虑到由此带来的稳定性问题,应根据不同的情况来配置。一般一个特定的系统,需要进行多次测试,才能确定最佳的时序配置。
2.       存储器的地址分配与地址重映射
ARM微处理器架构的理论寻址能力为4GB,但是对于一个特定的系统来说,所配备的实际物理存储器远没有那么多。因此,存储器的地址分配也就是将物理存储器定位在4GB地址空间的哪个具体位置。
    ARM微处理器通常采用两种方式来完成地址分配:一种方式采用固定的存储器地址分配,即物理存储器的起始地址是固定的;另一种方式采用存储器重映射的手段,使物理存储器的起始地址可以在有效的地址空间内任意改变,即物理存储器的地址是不固定的。
    基于固定存储器地址分配的ARM微处理器系统,当系统的硬件设计完成以后,物理存储器地址也就确定了,在软件设计的程序流程中考虑就简单一些。而对于具有存储器地址重映射功能的系统,这个过程就复杂的多。
    一种典型的存储器地址重映射过程描述如下:当系统上电或复位以后,PC指针指向0x0,程序从0x0地址开始执行,因此,为了能正确读取代码,要求此时Flash(或其它类型的ROM)的起始地址为0x0。但Flash(或其它类型的ROM)的访问速度大大低于RAM,每次产生异常后,都要从Flash(或其它类型的ROM)的异常向量表调转到相应的处理程序,会影响异常的响应速度,因此,系统便提供一种灵活的地址重映射方法,在系统完成必要地初始化以后,将RAM安排到0x0地址处,而将原来位于0x0处的Flash(或其它类型的ROM)安排到其他的地方上去,加快异常的响应速度。
    这个过程中最容易出错的地方是如何保证程序执行流程的连续性。因为PC指针最初在Flash里取指令执行,在进行地址重映射以后,Flash(或其它类型的ROM)被安排到其他地址上去了,而当前地址被安排为RAM,如果事先没有对RAM的内容进行正确地设置,在往下取指令执行就会出错,即程序的连续性被存储器地址重映射这种变化所打断。
    常用的处理方法是:先将Flash(或其它类型的ROM)的内容全部复制到RAM中,然后再进行地址重映射。此时尽管Flash(或其它类型的ROM)和RAM的物理地址发生了变化,但由于RAM中的内容与原来的Flash(或其它类型的ROM)是一样的,PC指针就可以继续取得正确地指令执行,从而保证了程序流程的连续性。
1.4 初始化堆栈
因为ARM有7种执行状态,每一种状态的堆栈指针寄存器(SP)都是独立的(注意System和User模式使用同一个SP)。所以,对程序中需要用到的每一种模式都要给SP定义一个堆栈地址。方法是改变状态寄存器内的状态位,使处理器切换到不同的状态,然后给SP赋值。注意:不要切换到User模式进行User模式的堆栈设置,因为进入User模式后就不能再操作CPSR回到别的模式了,可能会对接下去的程序执行造成影响。
1.5 初始化用户执行环境
主要包括初始化临界I/O设备,初始化应用程序执行环境,改变处理器的运行模式和状态,使能中断4部分。
初始化临界I/O设备。临界I/O设备是指哪些使能中断之前必须进行初始化的设备。如果不对这些设备进行必要的初始化,它们可能会在使能中断后产生一些没有意义的中断请求,从而影响程序的运行。
初始化应用程序执行环境。程序代码通过编译、链接后生成可执行映像文件,一个ARM映像文件由RO,RW和ZI三个段组成,其中RO为代码段,RW是已初始化的全局变量,ZI是未初始化的全局变量。映像一开始总是存储在ROM/Flash里面的,其RO部分即可以在ROM/Flash里面执行,也可以转移到速度更快的RAM中执行;而RW和ZI这两部分是必须转移到可写的RAM里去。所谓应用程序执行环境的初始化,就是完成必要的从ROM到RAM的数据传输和内容清零。
改变处理器的运行模式和状态。ARM微处理器在复位或上电状态下的默认模式为系统管理模式,而在初始化代码中可能需要切换到其它模式进行必要的操作,如初始化各个模式下的堆栈指针寄存器。因此,在系统的初始化过程中处理器模式一般会经历如图2所示的变化。同时,ARM微处理器在复位后总是处于ARM状态,对于兼容Thumb指令的处理器如果应用程序的入口点对应Thumb指令,则必须将微处理器切换到Thumb状态。
使能中断。如果系统需要使用中断,初始化代码应该使能中断。中断使能可以通过清除CPRS中的中断禁止位来完成。
1.6 呼叫主应用程序
当所有的系统初始化工作完成之后,就需要把程序流程转入主应用程序。如果主应用程序是由C代码编写,可以通过以下两种方式进入C代码运行,最简单的情况如下:
IMPORT         C_Entry       ;定义一个外部标号,最好不使用main
B              C_Entry       ;跳转到该处执行
在ARM的ADS编译环境中,还另外提供了一种进入C代码的机制:
IMPORT         __main
B              __main
__main()是编译器提供的一个函数,负责完成库函数的初始化和对C代码运行环境的初始化,最后自动调转到main()函数执行,此时应用程序的主函数名必须是main()。
用户可以根据需要选择是否使用main()函数,如果想让系统自动完成初始化过程,可以使用main()函数;如果所有的初始化过程都由用户自己完成,则不使用main()。