深入理解嵌入式Linux设备驱动程序
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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开始执行,且永不返回。到此,系统启动初始化代码的任务就完成了。