ARM 启动代码详解

ARM 174浏览

摘自:http://yangqish.blog.163.com/blog/static/8162847320100107751618/

启动代码是芯片复位后进入C语言的main()函数前执行的一段代码,主要是为运行C语言程序提供基本运行环境,如初始化存储器系统等。ARM公司只设计内核,不自己生产芯片,只是把内核授权给其它厂商,其它厂商购买了授权且加入自己的外设后生产出各具特色的芯片。这样就促进了基于ARM处理器核的芯片多元化,但也使得每一种芯片的启动代码差别很大,不易编写出统一的启动代码。ADS(针对ARM处理器核的C语言编译器)的策略是不提供完整的启动代码,启动代码不足部分或者由厂商提供,或者自己编写。启动代码划分为4个文件:Vectors.c、Init.s、Target.c、
Target.h。Vectors.c包含异常向量表、堆栈初始化及中断服务程序与C程序的接口。Init.s包含统初始化代码,并跳转到ADS提供的初始化代码。Target.c和 Target.h包含目标板特殊的代码,包括异常处理程序和目标板初始化程序。这样做的目的是为了尽量减少汇编代码,同时把不需要修改的代码独立出来以减少错误。

§4.2.1 Vectors.c文件的编写

§4.2.1.1 中断向量表

Vectors

        LDR     PC, ResetAddr

        LDR    PC, UndefinedAddr

        LDR     PC, SWI_Addr

        LDR     PC, PrefetchAddr

        LDR     PC, DataAbortAddr

        DCD     0xb9205f80

        LDR     PC, [PC, #-0xff0]

        LDR     PC, FIQ_Addr

ResetAddr           DCD     Reset

UndefinedAddr        DCD     Undefined

SWI_Addr           DCD     SoftwareInterrupt

PrefetchAddr        DCD     PrefetchAbort

DataAbortAddr       DCD     DataAbort

nouse               DCD     0

IRQ_Addr           DCD     IRQ_Handler

FIQ_Addr           DCD     FIQ_Handler

异常是由内部或外部源产生的以引起处理器处理的一个事件。ARM处理器核支持7种类型的异常。异常出现后,CPU强制从异常类型对应的固定存储地址开始执行程序。这个固定的地址就是异常向量。向量从上到下依次为复位、未定义指令异常、软件中断、预取指令中止、预取数据中止、保留的异常、IRQ和 FIQ。IRQ向量“LDR   PC, [PC, #-0xff0]” 使用的指令与其它向量不同。在正常情况下这条指令所在地址为0X00000018。当CPU执行这条指令但还没有跳转时,PC的值为0X00000020,0X00000020减去
0X00000FF0为 0XFFFFF030,这是向量中断控制器(VIC)的特殊寄存器VICVectAddr。这个寄存器保存当前将要服务的IRQ的中断服务程序的入口,用这一条指令就可以直接跳转到需要的中断服务程序中。至于在保留的异常向量“DCD 0xb9205f80”位置填数据0xb9205f8是为了使向量表中所有的数据32位累加和为0。

§4.2.1.2 初始化CPU堆栈

InitStack   

        MOV     R0, LR  

        MSR     CPSR_c, #0xd2 ;设置中断模式堆栈

        LDR     SP, StackIrq  

        MSR     CPSR_c, #0xd1 ;设置快速中断模式堆栈

        LDR     SP, StackFiq

        MSR     CPSR_c, #0xd7 ;设置中止模式堆栈

        LDR     SP, StackAbt

        MSR     CPSR_c, #0xdb ;设置未定义模式堆栈

        LDR     SP, StackUnd

        MSR     CPSR_c, #0xdf ;设置系统模式堆栈

        LDR     SP, StackSys

        MOV     PC, R0

StackIrq   DCD     (IrqStackSpace + IRQ_STACK_LEGTH * 4 - 4)

StackFiq   DCD     (FiqStackSpace + FIQ_STACK_LEGTH * 4 - 4)

StackAbt    DCD     (AbtStackSpace + ABT_STACK_LEGTH * 4 - 4)

StackUnd DCD     (UndtStackSpace + UND_STACK_LEGTH * 4 - 4)

StackSys    DCD    (SysStackSpace + SYS_STACK_LEGTH * 4 -4 )

;/* 分配堆栈空间 */

        AREA    MyStacks, DATA, NOINIT

IrqStackSpace   SPACE IRQ_STACK_LEGTH * 4 ;中断模式堆栈

FiqStackSpace   SPACE FIQ_STACK_LEGTH * 4 ;快速中断模式堆栈

AbtStackSpace   SPACE ABT_STACK_LEGTH * 4 ;中止义模式堆栈

UndtStackSpace SPACE UND_STACK_LEGTH * 4 ;未定义模式堆栈

SysStackSpace   SPACE SYS_STACK_LEGTH * 4 ; 系统模式堆栈

因为程序需要切换模式,而且程序退出时CPU的模式已经不再是管理模式而是系统模式LR已经不再保存返回程序地址,所以程序首先把返回地址保存到 R0中,同时使用R0返回。然后程序把处理器模式转化为IRQ模式,并设置IRQ模式的堆栈指针。其中变量Stacklrq保存着IRQ模式的堆栈指针的初始值,Irqstackspace是分配给 IRQ模式的堆栈空间的开始地址,IRQ_STACK_LEGTH是用户定义的常量,用于设置 IRQ模式的堆栈空间的大小。程序使用同样的方法设置FIQ模式堆栈指针、中止模式堆栈指针、未定义堆栈指针和系统模式堆栈指针。

程序使用编译器分配的空间作为堆栈,而不是按照通常的做法把堆栈分配到 RAM的顶端,之所以这样是因为这样做不必知道RAM顶端位置,移植更加方便;编译器给出的占用RAM空间的大小就是实际占用的大小,便于控制RAM的分配。对于 LPC2106来说,中止模式是不需要分配堆栈空间的,这是因为 LPC2106没有外部总线,也没有虚拟内存机制,如果出现取数据中止或取指令中止肯定是程序有问题。而一般情况下也不需要模拟协处理器指令或扩充指令,未定义中止也就意味着程序有错误,也不需要分配堆栈空间。

§4.2.1.3   异常处理代码与C语言的接口程序

μC/OS-Ⅱ中断服务子程序流程图如图4-1所示:

ARM 启动代码详解(Vectors.c、Init.s、Target.c、 Target.h) - 杨青胜——网络家园 - 杨青胜 BLOG

图4-1 中断服务子程序流程图

异常处理代码与C语言的接口程序如下:

MACRO

$IRQ_Label HANDLER $IRQ_Exception

        EXPORT $IRQ_Label                      ;输出的标号

        IMPORT $IRQ_Exception                  ;引用的外部标号

$IRQ_Label

        SUB     LR, LR, #4                        ;计算返回地址

        STMFD   SP!, {R0-R3, R12, LR}            ;保存任务环境

        MRS     R3, SPSR                        ;保存状态

        STMFD   SP!, {R3}

        LDR     R2, =OSIntNesting              ;OSIntNesting++

        LDRB    R1, [R2]

        ADD     R1, R1, #1

        STRB    R1, [R2]

       

        BL      $IRQ_Exception          ;调用c语言的中断处理程序

        MSR    CPSR_c, #0x92            ;关中断

        BL      OSIntExit

        LDR     R0, =OSTCBHighRdy

        LDR     R0, [R0]

        LDR     R1, =OSTCBCur

        LDR     R1, [R1]

        CMP     R0, R1

       

        LDMFD   SP!, {R3}

        MSR     SPSR_cxsf, R3

        LDMEQFD SP!, {R0-R3, R12, PC}^          ;不进行任务切换

        LDR     PC, =OSIntCtxSw                 ;进行任务切换

    MEND

Undefined                                      ;未定义指令

        b       Undefined

PrefetchAbort                                   ;取指令中止

        b       PrefetchAbort

DataAbort                                      ;取数据中止

        b       DataAbort

IRQ_Handler      HANDLER IRQ_Exception         ;中断

FIQ_Handler                                    ;快速中断

        b       FIQ_Handler

Timer0_Handler HANDLER Timer0              ;定时器0中断

未定义指令异常、取指令中止异常、取数据中止异常均是死循环,其中原因在上一小节已经说明。而快速中断在本应用中并未使用,所以也设置为死循环。LPC2106使用向量中断控制器,各个 IRQ中断的人口不一样,所以使用了一个宏来简化中断服务程序与C语言的接口编写。由ARM处理器核的文档可知,处理器进入IRQ中断服务程序时(LR-4)的值为中断返回地址,为了使任务无论在主动放弃CPU时还是中断时堆栈结构都一样,在这里先把LR减4。其它的部分与μC/OS-Ⅱ要求的基本一致。ARM处理核在进入中断服务程序时处理器模式变为IRQ模式,与任务的模式不同,它们的堆栈指针SP也不一样,而寄存器应当保存到用户的堆栈中,为了减少不必要的CPU时间和RAM空间的浪费,本移植仅在必要时将处理器的寄存器保存到用户的堆栈中,其它时候还是保存到IRQ模式的堆栈中。同时,从编译器的函数调用规范可知,C语言函数返回时,寄存器R4—R11、SP不会改变,所以只需要保存CPSR、R0—R3、R12和返回地址LR,在后面保存
CPSR是为了必要时将寄存器保存到用户堆栈比较方便。

在异常处理代码与C语言的接口程序中没有与中断服务子程序流程图中的判断语句对应的语句。判断语句是为了避免在函数OSIntCtxsw()调整堆栈指针,这个调整量是与编译器、编译器选项、μC/OS -Ⅱ配置选项都相关的变量。在这里进行这些处理相对其它处理器结构可能增加的处理器时间很少,但对于ARM来说,由于中断(IRQ)有独立的堆栈,在这里这样做就需要把所有寄存器从中断的堆栈拷贝到任务的堆栈,需要花费比较多的额外时间。而变量OSIntNesting为0时,并不一定会进行任务切换,所以本移植没有与之对应的程序,而在函数OSIntCtxsw()中做这一项工作。这样,仅在需要时才处理这些事物,程序效率得以提高。

在中断调用后,如果需要任务切换,则变量OSTCBHighRdy和变量OSTCBCur的值不同;如果不需要任务切换这两个变量则相同。本移植通过判断这两个变量来决定是进行任务切换,还是不进行任务切换。通过比较,如果需要任务切换则执行“LDR PC, =OSIntCtxSw”跳转到OSIntCtxSw处进行任务切换;如果不需要任务切换则执行“LDMEQFD SP!, {R0-R3, R12, PC}^”中断返回。

这里需要对“MSR    CPSR_c, #0x92”说明下,这条指令的作用是关IRQ中断。因为中断(IRQ)模式的LR寄存器在处理器响应中断时用于保存中断返回地址,所以在处理器响应中断时中断(IRQ)模式的LR寄存器不能保存有效数据。而BL指令要用LR寄存器保存BL下一条指令的位置,所以在中断(IRQ)模式时,在BL指令之前必须关中断,在保存LR后才能开中断。

§4.2.2 Target.c文件的编写

为了使系统基本能够工作,必须在进人 main()函数前对系统进行一些基本的初始化工作,这些工作由函数TargetResetInit()完成。

void TargetResetInit(void)

{

    uint32 i;

    uint32 *cp1;

    uint32 *cp2;

    extern void Vectors(void) ;

    /* 拷贝向量表,保证在flash和ram中程序均可正确运行 */

    cp1 = (uint32 *)Vectors;

    cp2 = (uint32 *)0x40000000;

    for (i = 0; i < 2 * 8; i++)

    {

        *cp2++ = *cp1++;

    }

   

   MEMMAP = 0x2;                  

  

PINSEL0 = (PINSEL0 & 0xFFFF0000) | UART0_PCB_PINSEL_CFG | 0x50;

PLLCON = 1;                 /* 设置系统各部分时钟 */

   VPBDIV = 0;

   PLLCFG =0x23;

   PLLFEED = 0xaa;

   PLLFEED = 0x55;

   while((PLLSTAT & (1 << 10)) = = 0) ;

   PLLCON = 3;

    PLLFEED = 0xaa;

    PLLFEED = 0x55;

    MAMCR = 2;        /* 设置存储器加速模块 */

#if Fcclk < 20000000

    MAMTIM = 1;

#else

#if Fcclk < 40000000

    MAMTIM = 2;

#else

    MAMTIM = 3;

#endif

#endif

   

首先向量表拷贝到RAM底部,加上这部分是为了代码无论从Flash基地址开始编译还是从RAM基地址开始编译程序均运行正确。而把RAM底部映射到向量表“MEMMAP = 0x2”也是为了同一个目的。至于复制16个字而不是8个字,是因为后8个字存储跳转的地址是通过 PC指针间接寻址的,它们与对应指令(在向量表中)相对位置是不能变化的。

因为在进入多任务环境前使用了一些外设,部分外设使用了芯片的引脚,而 LPC2106的所有引脚都是多功能的,所以需要设置引脚功能。同时串口也进行了设置。时钟是芯片各部分正常工作的基础,虽然时钟可以在任何时候设置,但为了避免混乱,最好在进入 main()函数前设置。程序首先使能PLL但不连接PLL,然后设置外设时钟(VPB时钟pclk)与系统时钟(cclk)的分频比。接着设置PLL的乘因子和除因子。设置完成后,使用“PLLFEED = 0xaa; PLLFEED = 0x55;”的访问序列把数据正确写人硬件,并等待PLL跟踪完成。最后,使能PLL,并使PLL联上系统。本应用外接的晶振频率(Fosc)为11.0592MHz,倍增器的值M=4,所以处理器时钟(Fcclk)为44.2368
MHz。为了使电流控制振荡器频率(Fcco)满足156-320MHz,所以分频器的值P=2,使得Fcco= Fcclk×2×P=176.9472 MHz。取VPB分频器的分频值为1/4,所以外设时钟(Fpclk)= Fcclk/4=11.0592 MHz,则记数周期为0.09042μs,定时0.2ms,则记数值为2212个,这些时钟的定义都在config.h文件中。

用户程序最终是要在Flash中运行的,而系统复位时Flash是以最低速度运行,这对发挥芯片的性能极其不利。虽然存储器加速模块可以在任何时候设置,但为了避免混乱,最好在进入 main()函数前设置。首先使存储器加速模块全速工作,然后根据系统主时钟利用条件编译将Flash的访问时钟设置到合适的值。

§4.2.3 Init.s文件的编写

由于LPC2106微控制器的存储系统比较简单,所以系统初始化代码也比较简单,代码如下:

Reset

        BL      InitStack              ;初始化堆栈

        BL      TargetResetInit         ;目标板基本初始化

B       __main              ;跳转到c语言入口

在芯片复位在芯片复位时程序会跳转到标号Reset处,程序首先调用Initstack初始化各种模式的堆栈,然后调用TargetResetlnit对系统进行基本初始化,最后跳转到ADS提供的启动代码__main。_main是 ADS提供的启动代码起始位置,它初始化库并最终引导CPU进入main函数。