Understanding the Boot Process of ARM Embedded System – Based on the AT91SAM9261 Micro Processor and RealView Toolchain
对于不带操作系统的ARM嵌入式应用程序来说,可以这样理解引导过程,它始于系统上电后,终于main函数。虽然不同的应用程序的引导细节各不相同,但是总的来说主要包括初始化关键外围设备、执行存储器重映射和安排存储器布局等。本文首先介绍了一些关键的概念,然后从系统上电执行的第一条指令开始,详细阐述了ARM嵌入式系统整个引导过程。本文中的示例程序运行的主板以Atmel公司AT91SAM9261-EK评估板为原型,扩展了一个片选NCS0,地址为0x10000000,大小为4M字节的NorFlash,同时将BMS引脚接地,这样系统上电后自扩展的片外NorFlash引导。集成开发环境为Keil公司的μVision3.62c,配置使用RealView工具链。
对于任意类型的存储器,必须首先为它分配一个地址空间才能对其进行访问,这样就在地址空间和存储器之间建立了某种对应关系,称为存储器映射。不管是微处理器内置的片内存储器,还是扩展的外部存储器,在电路板制作完成之后,它的存储器映射也就自然的固定下来,这个特定的地址空间也称为它的固有地址空间。例如对于AT91SAM9261来说,地址空间0x30,0000-0x40,0000映射到片内SRAM,即片内SRAM的固有地址空间为0x30,0000-0x40,0000。
对于ARM处理器来说,为了获得多种引导选项或者提高性能,某个特定的地址空间可以映射到不同的存储器,或者反过来一个存储器可以对应多个地址空间,这种现象称之为存储器重映射。例如AT91SAM9261没有为地址空间0x0-0xF,FFFF安排固有存储器映射,根据上电时BMS引脚的状态,或者通过编程MATRIX_MCFG寄存器可以将不同的存储器映射到0x0-0xF,FFFF,如图1所示,也正是由于这个原因这段地址空间又称为引导存储器,或者重映射区域。在本文中,BMS引脚接地,系统上电后,NorFlash重映射到0x0,此时除了可以通过它的固有存储器映射0x1000,0000访问它,还可以通过它的重映射地址0x0访问它。如果之后将片内SRAM重映射到地址0x0,此时再访问地址0x0,那么实际访问的是片内SRAM。
图1 重映射
总之,从地址空间的角度看,这个特殊的重映射区域对应的存储器映射具有多重性和临时性,但是在任意时刻它的存储器映射具有唯一性。除了这个特殊的重映射区域,其它地址空间和存储器之间是一一对应的。反之从存储器的角度看,无论何时总是可以通过它的固有地址空间访问它,但是只有当满足某些条件时才能从重映射区域访问它。
RealView工具链利用分散加载机制创建复杂的映像文件。分散加载文件能够精确的描述映像中每个区的加载和执行视图。加载视图描述映像开始执行前的存储器布局,执行视图描述映像执行时存储器布局。分散加载文件可以描述的最小单元,对于汇编语言来说为一个.s文件中的AREA,例如board_cstartup_keil.s中的cstartup和VECTOR;对于C语言来说为一个.c文件,例如board_lowlevel.c和board_memories.c。
本文使用的分散加载文件norflash.sct如下所示。从中可以看出:这里只有一个名字为Load_region的加载区,即映像文件存储在地址为0x10000000的NorFlash中;执行区一共有四个,分别为Fixed_region(包含cstartup区域和大部分具有RO属性的节),Relocate_region(包含异常向量、板级初始化代码编译后生成的对象文件以及属性为RW和ZI的节),ARM_LIB_HEAP(堆区)和ARM_LIB_STACK(自片内SRAM顶部开始向下增长的堆栈区)。
Load_region 0x10000000 0x400000 {
Fixed_region 0x10000000 {
*(cstartup +First)
.ANY (+RO)
}
Relocate_region 0x300000 0x28000 {
*.o (VECTOR, +First)
board_lowlevel.o (+RO)
board_memories.o (+RO)
.ANY (+RW +ZI)
}
ARM_LIB_HEAP 0x326000 EMPTY 0x1000 {
}
ARM_LIB_STACK 0x328000 EMPTY -0x1000 {
}
}
ARM核总是从地址0x0取出第一条指令开始引导过程。在本文中映像文件烧写到NorFlash中,为了从NorFlash引导,BMS引脚接地,这样复位时,NorFlash重映射到地址0x0。cstartup区域部分代码如下所示:
AREA cstartup, CODE
ENTRY
resetHandler
; The first instruction Set pc to actual code location (i.e. not in remap zone)
LDR pc, =label
label
LDR r0, = |Image$$ARM_LIB_STACK$$ZI$$Limit|
MOV sp, r0
; other codes
由于cstartup区域定位于NorFlash的起始位置,因此执行的第一条指令是LDR pc, =label。这是一条伪指令,汇编器将label的值放在一个文字池(literal pool)中,并生成一个相对PC寻址的LDR指令从文字池中装载该值。这可以从IDA-The Interactive Disassembler反汇编生成的.axf文件的输出得到确认:
0x10000000 ldr pc, [pc, #0x34] ;34 F0 9F E5
…
0x1000003C DCD 0x10000004
可以看到label的值等于0x10000004,它存储在地址0x1000003C处。首条指令的编码为34 F0 9F E5,即ldr pc, [pc, #0x34],执行此指令后,PC=PC+0x8+0x34=PC+0x3C,取出当前指令地址偏移0x3C处的值(即0x10000004),赋给PC寄存器,也就是跳转到绝对地址0x10000004处继续执行。其后的两条汇编语句设置临时堆栈,为调用C语言编写的函数LowLevelInit做准备。
系统引导时必须首先初始化一些关键的外围设备,例如晶振和锁相环,高级中断控制器,看门狗等,它们都是在LowLevelInit函数中完成的。由于不同的应用程序的外设初始化代码相差较大,因此此处不做详细说明。
所有的ARM系统在地址0x0都有一个向量表。向量表虽然不是初始化序列的一部分,但是必须存在,它是转到各个异常处理器的跳转指令表。当发生异常时(例如,数据中止,未定义指令,IRQ等),ARM核立即取出位于地址0x0和0x1C之间的8条指令中的一条,并执行它。如果程序不需要处理这个异常,那么可以放置一个无限循环,例如:
undefVector
b undefVector
如果必须处理这个异常,例如复位异常,那么可以放置一个相对PC的跳转指令,例如:
LDR pc, =resetHandler
很明显,当前的存储器布局不能满足这种要求,因为地址0x0-0xF,FFFF此时映射到NorFlash,而NorFlash的起始位置存储的是cstartup区域,而不是异常向量。为了满足ARM核对异常向量的位置要求,引导过程的下一步就是将片内SRAM重映射到0x0-0xF,FFFF,这是通过调用BOARD_RemapRam函数完成的。注意,此时异常向量并未真正就绪,只有当随后使用分散加载机制将VECTOR区域从加载区拷贝到片内SRAM的起始处后,才能访问异常向量。
最后调用BOARD_ConfigureNorFlash函数配置外部总线接口片选0(EBI CS0),因为默认配置参数并不是最优的,所以需要根据存储器特性进行重新配置。
ARM处理器的堆栈主要有两个特点,其一每种模式都有自己的堆栈指针SP,其二堆栈是递减的。设置方法是依次进入各种模式,赋给SP正确的值。一般Supervisor和User模式的堆栈最大,IRQ和FIQ模式次之,其它模式经常只需要几个字节。在本文中,首先设置IRQ模式堆栈设置到片内SRAM顶部,然后设置Supervisor模式的堆栈,同时使能IRQ和FIQ。
链接器生成可执行映像的同时也定义了映像中各个符合的绝对地址,只有将它们移动到对应的地址,应用程序才能正常运行。以可读写的RW节为例,如果在改写它的值之前,没有将其从Flash存储器移动到可读写的RAM存储器,那么必然会发生异常。
ARM库中的__main例程负责将加载视图转变为执行视图,最后调用C语言编写的main函数。__main例程的主要功能有三个:拷贝需要移动的区、零初始化ZI区和初始化堆栈和堆,分别对应于图2中的①②③。
大多数RO属性的节都位于NorFlash中,它的加载视图和执行视图的起始地址相同,因此不需要移动。
Relocate_region区的加载视图和执行视图的起始地址不同,因此需要移动。将VECTOR节定位到片内SRAM主要是为了满足ARM架构对异常向量的位置要求,在执行完初始化序列中的SRAM重映射之后,就可以在地址0x0访问异常向量。board_lowlevel.o和board_memories.o紧随VECTOR之后,之所以将它们定位到片内SRAM主要是为了提高系统性能。
最后放置属性为ZI的节。 ZI节在映像中不占据空间,它们在SRAM中创建和零初始化。
ARM_LIB_STACK执行区用来为各种模式分配堆栈空间,它自SRAM顶部,即地址0x328000,开始向下增长。ARM_LIB_HEAP执行区,即堆空间,它自地址0x326000开始向上增长。
图2 从加载视图到执行视图
__main例程在完成任务之后最后调用C语言编写的main函数,引导过程结束,用户程序开始执行。
[1] Andrew N.Sloss, Dominic Symes, Chris Wright. ARM嵌入式系统开发:软件设计与优化[M].沈建华, 译. 北京: 北京航空航天大学出版社, 2005.
[2] Randal E.Bryant, DavidO’Hallaron. 深入理解计算机系统[M]. 龚奕利, 雷迎春, 译. 修订版. 北京:中国电力出版社, 2004.
[3] David Seal. ARM Architecture Reference Manual(2nd Edition)[M]. Addison-Wesley, 2001.
[4] Getting Started with the AT91SAM9261 Microcontroller[EB/OL]. http://www.atmel.com/dyn/resources/prod_documents/doc6298.pdf
[5] John R. Levine. Linkers and Loaders[M]. Morgan Kaufmann, 1999.
注:源代码下载地址http://files.cnblogs.com/dreamliner/getting-started-project-at91sam9261-ek.rar