1.2 嵌入式ARM系统的启动代码分析
1.2.1 ARM上电启动概述
ARM系统上电启动后,从0x0地址处开始执行,根据系统的配置0x0地址可以映射在Nor FLASH或SDRAM中,在Nor FLASH或Nand FLASH中存放有系统的启动初始化程序,与计算机的BIOS类似,完成系统最底层的初始化工作。
ARM系统上电后,首先就运行系统初始化程序,系统初始化程序主要完成系统最基本的硬件初始化,为后面的C语言应用程序提供运行环境。ARM系统初始化启动代码完成的主要功能如下:
· 初始化ARM CPU异常处理向量表;
· 禁止看门狗;
· 禁止中断;
· 初始化系统时钟,包括CPU主频FCLK、系统总线时钟频率HCLK、外设总线时钟频率PCLK;
· 初始化SDRAM控制器;
· 设置ARM CPU在各种模式下的栈指针(栈顶);
·设置ARM中断向量表,安装中断处理程序;
· 搬运可执行映像文件的RW段到RAM中,并初始化ZI段为0;
· 跳转到C语言应用程序的Main函数,开始执行C语言应用程序。
到此,系统的初始化启动程序就完成了ARM系统的启动过程。
ARM系统的初始化启动代码一般用汇编语言编写,根据以上的分析,我们知道ARM系统初始化启动程序的流程如下图所示。
1.2.2 ARM上电初始化启动代码分析
ARM系统的初始化启动代码源文件为2440init.s,是汇编语言源文件。下面对汇编启动代码的主要部分进行分析讲解。
GET option.inc GET memcfg.inc GET 2440addr.inc
程序的开始是通过GET汇编伪指令设置该源码文件要用到的一些头文件的,其中option.inc定义ARM系统的一些配置选项,在这里主要定义中断向量起始地址_ISR_STARTADDRESS和系统时钟的分频系数M_MDIV、M_PDIV、M_SDIV等。GET伪指令也可以用INCLUDE伪指令替代。
UserStack EQU (_STACK_BASEADDRESS-0x3800) ;0x33ff4800~ SVCStack EQU (_STACK_BASEADDRESS-0x2800) ;0x33ff5800~ UndefStack EQU (_STACK_BASEADDRESS-0x2400) ;0x33ff5c00~ AbortStack EQU (_STACK_BASEADDRESS-0x2000) ;0x33ff6000~ IRQStack EQU (_STACK_BASEADDRESS-0x1000) ;0x33ff7000~ FIQStack EQU (_STACK_BASEADDRESS-0x0) ;0x33ff8000~
接下来程序定义ARM CPU在各种工作模式下的栈顶位置,在系统初始化程序的最后阶段需要设置CPU在各种工作模式下的栈顶指针(SP寄存器),为C语言程序的运行做准备。
_STACK_BASEADDRESS定义在option.inc文件中,如下所示。
; Start address of each stacks, _STACK_BASEADDRESS EQU 0x33ff8000
其中以“; ”开始的行代表注释。
MACRO $HandlerLabel HANDLER $HandleLabel $HandlerLabel sub sp, sp, #4 ; decrement sp(to store jump address) stmfdsp! , {r0} ; PUSH the work register to stack ldr r0, =$HandleLabel ; load the address of HandleXXX to r0 ldr r0, [r0] ; load the contents(service routine start address)of HandleXXX str r0, [sp, #4] ; store the contents(ISR)of HandleXXX to stack ldmfd sp! , {r0, pc} ; POP the work register and pc(jump to ISR) MEND
上面这段代码定义了一个宏,该宏实现的主要功能是把中断服务程序首地址HandleLabel装载到指令寄存器(PC寄存器)中,当中断产生时,系统能正确执行中断服务处理程序。在后面有关的中断系统处理的章节中,我们会详细分析该宏的使用方法。
接下来我们通过IMPORT伪指令导入启动代码中需要用到的外部符号。
;declare ARM-linker internel self-define variable and Main IMPORT —Image$$RO$$Limit— ; End of ROM code(=start of ROM data) IMPORT —Image$$RW$$Base— ; Base of RAM to initialise IMPORT —Image$$ZI$$Base— ; Base and limit of area IMPORT —Image$$ZI$$Limit— ; to zero initialise IMPORT Main
Main为C语言程序的入口函数,也可以改成别的。
接下来才是真正系统初始化程序的开始。首先使用AREA伪指令定义代码段,段名为Init,在链接时,通过链接选项把Init段链接到可执行映像文件的第一个段,如下所示。
AREA Init, CODE, READONLY ENTRY b ResetHandler b HandlerUndef ; handler for Undefined mode b HandlerSWI ; handler for SWI interrupt b HandlerPabort ; handler for PAbort b HandlerDabort ; handler for DAbort b . ; reserved b HandlerIRQ ; handler for IRQ interrupt b HandlerFIQ ; handler for FIQ interrupt ;@0x20 b . ; Must be@0x20.
ENTRY伪指令定义代码段的入口。系统启动程序的前32字节用来存放ARM异常向量表。当异常发生时,CPU自动跳转到异常向量表处执行异常处理程序。当系统上电时,首先执行第一条指令“b ResetHandler”,该代码通过一条跳转指令跳到ResetHandler处执行复位处理程序。
代码中“b.”表示跳到当前位置,“.”表示当前指令位置。
ResetHandler ldr r0, =WTCON ; watch dog disable ldr r1, =0x0 str r1, [r0] ldr r0, =INTMSK ldr r1, =0xffffffff ; all interrupt disable str r1, [r0] ldr r0, =INTSUBMSK ldr r1, =0x3ff ; all sub interrupt disable str r1, [r0] ;To reduce PLL lock time, adjust the LOCKTIME register ldr r0, =LOCKTIME ldr r1, =0xffffff str r1, [r0] ; Added for confirm clock divide. for 2440. set pll ; Setting value Fclk:Hclk:Pclk ldr r0, =CLKDIVN ldr r1, =CLKDIV_VAL ;3=1:2:4 str r1, [r0] ;Configure UPLL ldr r0, =UPLLCON ldr r1, =((U_MDIV<<12)+(U_PDIV<<4)+U_SDIV) str r1, [r0]
系统复位处理程序ResetHandler首先禁止看门狗,再禁止中断,然后设置CPU系统时钟和时钟分频系数,最后设置USB时钟和相应的分频系数。
设置好系统时钟后,紧接着开始初始化SDRAM控制器,主要设置SDRAM的总线宽度和操作时序等。设置完SDRAM控制器后,SDRAM就可以正常工作了。
;Set memory control registers ldr r0, =SMRDATA ldr r1, =BWSCON ; BWSCON Address add r2, r0, #52 ; End address of SMRDATA 0 ldr r3, [r0], #4 str r3, [r1], #4 cmp r2, r0 bne %B0 ;Initialize stacks bl InitStacks
完成SDRAM控制器的设置后,通过调用InitStacks函数来初始化ARM CPU在各种工作模式下的栈指针(SP寄存器)。
InitStacks函数的工作流程比较简单,首先设置CPU相应的工作模式,然后把在该工作模式下的栈指针寄存器SP的值设置为对应的栈顶地址即可。通过修改当前程序状态寄存器CPSR来修改CPU的当前工作模式。InitStacks代码如下所示。
InitStacks ;Don't use DRAM, such as stmfd, ldmfd… ;SVCstack is initialized before ;Under toolkit ver 2.5, 'msr cpsr, r1' can be used instead of' msr cpsr_cxsf, r1' mrs r0, cpsr bic r0, r0, #MODEMASK orr r1, r0, #UNDEFMODE—NOINT msr cpsr_cxsf, r1 ; UndefMode ldr sp, =UndefStack ; UndefStack=0x33FF_5C00 orr r1, r0, #ABORTMODE—NOINT msr cpsr_cxsf, r1 ; AbortMode ldr sp, =AbortStack ; AbortStack=0x33FF_6000 orr r1, r0, #IRQMODE—NOINT msr cpsr_cxsf, r1 ; IRQMode ldr sp, =IRQStack ; IRQStack=0x33FF_7000 orr r1, r0, #FIQMODE—NOINT msr cpsr_cxsf, r1 ; FIQMode ldr sp, =FIQStack ; FIQStack=0x33FF_8000 bic r0, r0, #MODEMASK—NOINT orr r1, r0, #SVCMODE msr cpsr_cxsf, r1 ; SVCMode ldr sp, =SVCStack ; SVCStack=0x33FF_5800 ;USER mode has not be initialized. mov pc, lr
系统栈初始化完成后,接下来进行代码的重定位。所谓代码的重定位,就是把应用程序的代码段和数据段从加载地址位置搬到运行地址位置。在初始化代码中,本例选择从Nor FLASH启动,代码段的加载地址和运行地址一致,所以不需要进行代码重定位。对于RW的系统全局初始化数据段需要进行重定位,要从ROM中的位置搬到RAM中去。系统重定位代码如下所示。
;Copy and paste RW data/zero initialized data LDR r0, =—Image$$RO$$Limit— ; Get pointer to ROM data LDR r1, =—Image$$RW$$Base— ; and RAM copy LDR r3, =—Image$$ZI$$Base— ;Zero init base => top of initialised data CMP r0, r1 ; Check that they are different BEQ %F2 1 CMP r1, r3 ; Copy init data LDRCC r2, [r0], #4 ; -->LDRCC r2, [r0]+ADD r0, r0, #4 STRCC r2, [r1], #4 ; -->STRCC r2, [r1]+ADD r1, r1, #4 BCC %B1 2 LDR r1, =—Image$$ZI$$Limit—; Top of zero init segment MOV r2, #0 3 CMP r3, r1 ; Zero init STRCC r2, [r3], #4 BCC %B3
首先把全局初始化数据段搬运到RW运行地址位置,全局初始化数据段在ROM中的位置从RO_Limit开始,数据长度为ZI_Base - RW_Base。把全局初始化数据段搬运到RW段后,紧接着初始化ZI段数据为0, ZI段的起始地址为ZI_Base,结束地址为ZI_Limit。
本例中,程序运行后(即运行时域), RO段存于ROM中,RW段和ZI段存于RAM中,加载时域(程序还没有执行)代码段和RW数据段存于ROM中。本例中可执行映像文件的代码重定位如下图所示。
代码重定位完成后,ARM系统的启动初始化工作基本完成了,接着通过一条跳转指令跳到C代码部分,开始执行C语言应用程序代码,如下所示。
; jump to Main , Main start execute bl Main b .
Main为C语言应用程序代码的总入口函数,系统启动代码把控制权提交给Main后,Main开始执行,且永不返回。到此,系统启动初始化代码的任务就完成了。