Chinaunix首页 | 论坛 | 博客
  • 博客访问: 192636
  • 博文数量: 71
  • 博客积分: 10
  • 博客等级: 民兵
  • 技术积分: 210
  • 用 户 组: 普通用户
  • 注册时间: 2013-01-13 14:49
文章分类
文章存档

2017年(1)

2015年(5)

2014年(10)

2013年(55)

我的朋友

分类: 嵌入式

2013-08-03 15:31:03

原文地址:ARM下启动代码 作者:number007cool

1、

IAR汇编指令SFB和SFE

SFB Segment begin 段开始

语法格式
SFB(segment [{+|-} offset])

参数
segment: 可重定位段的段名, 必须在SFB使用前已定义
offset : 从开始地址的偏移, 是一个可选参数, 当偏移量省略时, 可以不添加小括号

描述
SFB 右边可以接受一个操作数, 而且这个操作数必须是一个可重位段的段名.
这个操作符计算段的首字节地址. 这个操作发生在连接时.

NAME demo
RSEG CODE
start: DC16 SFB(CODE)
即使上面的代码和多个其他的模块进行连接, start标号处仍被置为段的首字节地址

 

语法格式
SFE (segment [{+|-} offset])

参数
segment: 可重定位段的段名, 必须在SFB使用前已定义
offset : 从开始地址的偏移, 是一个可选参数, 当偏移量省略时, 可以不添加小括号

描述
SFE在其右边接收一个操作数. 操作数必须是一个可重定位段的段名. SFE操作符将段起始地址和段大小相加. 这个操作在连接时发生.
SFE accepts a single operand to its right. The operand must be the name of a relocatable segment. The operator evaluates to the segment start address plus the segment size. This evaluation takes place at linking time.


NAME demo
RSEG CODE
end: DC16 SFE(CODE)
即使当上面的代码被和多个模块想连接时, end标号仍然会被置为段最后一个字节的地址. Even if the above code is linked with many other modules, end will still be set to the address of the last byte of the segment.

段MY_SEGMENT的大小可以通过以下方式计算而得:
SFE(MY_SEGMENT)-SFB(MY_SEGMENT)


arm汇编的跳转指令无非是b和ldr。但是如果没有足够理解,别人灵活的用一下你就犯晕了。
首先我们要知道两者的两个本质区别:
1、b是位置无关的,ldr不是位置无关的。
2、b的范围只能是+—32MB,而ldr是4GB。
 
在arm的启动汇编的中断向量表是必然用跳转指令的,但是就是这里也有很多实现形式:
方式1:
                B           InitReset           ; 0x00 Reset handler
undefvec:
                B           undefvec            ; 0x04 Undefined Instruction
swivec:
                B           swivec              ; 0x08 Software Interrupt
pabtvec:
                B           pabtvec             ; 0x0C Prefetch Abort
dabtvec:
                B           dabtvec             ; 0x10 Data Abort
rsvdvec:
                B           rsvdvec             ; 0x14 reserved
irqvec:
                B           IRQ_Handler_Entry   ; 0x18 IRQ
这个我很容易理解,实现的标号油InitReset和IRQ_Handler_Entry,其他没有实现的在原地跳转。
方式二:
        LDR     pc, =resetHandler        ; Reset
        LDR     pc, Undefined_Addr       ; Undefined instructions
        LDR     pc, SWI_Addr             ; Software interrupt (SWI/SYS)
        LDR     pc, Prefetch_Addr        ; Prefetch abort
        LDR     pc, Abort_Addr           ; Data abort
        B       .                        ; RESERVED
        LDR     pc, =irqHandler          ; IRQ
        LDR     pc, FIQ_Addr             ; FIQ
  LDR PC,[PC,#0x18]
Undefined_Addr: DCD   Undefined_Handler
SWI_Addr:       DCD   SWI_Handler
Prefetch_Addr:  DCD   Prefetch_Handler
Abort_Addr:     DCD   Abort_Handler
FIQ_Addr:       DCD   FIQ_Handler
我们注意到两种ldr
一种LDR PC,=label,这时把LDR当做伪指令,他要被翻译成:
LDR PC,[PC,offset_to_label2]
label2:DCD resetHandler
这种label是在程序中标记的了,如resetHandler和irqHandler
还有一种LDR PC,label,这时直接把label地址内存内容copy到PC中
这种label都是 label:DCD label2 ,这些label2可以在任何地方实现
 
我们来理解b和ldr两者的不同
1,b是位置无关,因为他的跳转都是相对PC来的,而ldr不是位置无关的,因为他的跳转是根据DCD里面的值,这个值是连接的时候确定的,他是跟连接地址有关的
2,b的范围只能是32M,是因为指令里面给偏移的空间只有24bit,而ldr是一个32bit的DCD,所以他是32bit的
 
注意:像上面方式二
LDR     pc, =resetHandler        ; Reset
resetHandler:
 ....
 ....
如果运行地址是0,那么LDR     pc, =resetHandler还在以基址为0的空间运行
但是执行完了,假设装载地址是0x3000000,所以resetHandler的基址不是0,而是0x30000000
所以resetHandler标号以后的指令应该存在以0x3000000为基址的空间,pc跳过去了
这种技巧在at91的remap模式经常用到


理解启动代码(ADS)
  所谓启动代码,就是处理器在启动的时候执行的一段代码,主要任务是初始化处理器模式,设置堆栈,初始化变量等等.由于以上的操作均与处理器体系结构和系统配置密切相关,所以一般由汇编来编写.
  具体到S64,启动代码分成两部分,一是与ARM7TDMI内核相关的部分,包括处理器各异常向量的配置,各处理器模式的堆栈设置,如有必要,复制向量到RAM,以便remap之后处理器正确处理异常,初始化数据(包括RW与ZI),最后跳转到Main.二是与处理器外部设备相关的部分,这和厂商的联系比较大.虽然都采用了ARM7TDMI的内核,但是不同的厂家整合了不同的片上外设,需要不同的初始化,其中比较重要的是初始化WDT,初始化各子系统时钟,有必要的话,进行remap.这一部分与一般控制器的初始化类似,因此,本文不作重点描述.
  在进行分析之前,请确认如下相关概念:
S64片上FLASH起始于0x100000,共64kB,片上RAM起始于0x200000,共16kB.
S64复位之后,程序会从0开始执行,此时FLASH被映射到0地址,因此,S64可以取得指令并执行.显然,此时还是驻留在0x100000地址.如果使用remap命令,将会把RAM映射到0地址,同样的这时0地址的内容也只是RAM的镜像.
S64的FLASH可以保证在最差情况时以30MHz进行单周期访问,而RAM可以保证在最大速度时的单周期访问.
OK,以下开始分析启动代码.

一,处理器异常
     S64将异常向量至于0地址开始的几个直接,这些是必需要处理的.由于复位向量位于0,也需要一条跳转指令.具体代码如下:
     RESET
          B       SYSINIT                        ; Reset
          B       UDFHANDLER            ; UNDEFINED
          B       SWIHANDLER             ; SWI
          B       PABTHANDLER          ; PREFETCH ABORT
          B       DABTHANDLER          ; DATA ABORT
          B       .                                       ; RESERVED 
          B       VECTORED_IRQ_HANDLER 
          B       .                                       ; ADD FIQ CODE HERE

UDFHANDLER
          B       .

SWIHANDLER
          B       .

PABTHANDLER
          B       .

DABTHANDLER
          B       .

请注意,B指令经汇编后会替换为当前PC值加上一个修正值(+/-),所以这条指令是代码位置无关的,也就是不管这条指令是在0地址还是在0x100000执行,都能跳转到指定的位置,而LDR PC,=???将向PC直接装载一个标号的值,请注意,标号在编译过后将被替换为一个与RO相对应的值,也就是说,这样的指令无论在哪里执行,都只会跳转到一个指定的位置.下面举一个具体的例子来说明两者的区别:
      假定有如下程序:
       RESET
                   B       INIT        或者   LDR      PC,=INIT
                   …

       INIT
                   …
      其中RESET为起始时的代码,也就是这条代码的偏移为0,设INIT的偏移量为offset.如果将这段程序按照RO=0x1000000编译, 那么B INIT可理解为ADD   PC, PC, #offset,而LDR     PC,=INIT可被理解为 MOV PC,#(RO+offset) .显然当系统复位时,程序从0开始运行,而0地址有FLASH的副本,执行B     INIT将把PC指向位于0地址处的镜像代码位置,也即INIT;如果执行LDR    PC,=INIT将会将PC直接指向位于FLASH中的原始代码.因此以上两者都能正确运行.下面将RO设置为0x200000,编译后生成代码,还是得烧写到FLASH中,也就是还是0x100000,系统复位后从0地址执行,还是FLASH的副本,此时执行B     INIT,将跳到副本中的INIT位置执行,此处有对应的代码;但是如果执行LDR     PC,=INIT,将向PC加载0x200000+offset,这将使得PC跳到RAM中,而此时由于代码没有复制,RAM中的指定位置并没有代码,程序无法运行.

二,处理器模式
         ARM的处理器可工作于多种模式,不同模式有不同的堆栈 ,以下设置各模式及其堆栈.
          预定义一些参数:
MODUSR     EQU     0x10
MODSYS     EQU     0x1F 
MODSVC     EQU     0x13
MODABT     EQU     0x17
MODUDF     EQU     0x1B
MODIRQ     EQU     0x12
MODFIQ     EQU     0x11

IRQBIT     EQU     0x80 
FIQBIT     EQU     0x40

RAMEND     EQU     0x00204000   ; S64 : 16KB RAM

VECTSIZE   EQU     0x100        ; 

UsrStkSz     EQU    8           ; size of   USR   stack
SysStkSz     EQU    128         ; size of   SYS   stack
SvcStkSz     EQU    8           ; size of   SVC   stack
UdfStkSz     EQU    8           ; size of   UDF   stack
AbtStkSz     EQU    8           ; size of   ABT   stack
IrqStkSz     EQU    128         ; size of   IRQ   stack
FiqStkSz     EQU    16          ; size of   FIQ   stack

修改这些值即可修改相应模式堆栈的尺寸.
以下为各模式代码:
SYSINIT
                                 ;
         MRS     R0,CPSR
         BIC     R0,R0,#0x1F
  
         MOV     R2,#RAMEND
         ORR     R1,R0,#(MODSVC :OR: IRQBIT :OR: FIQBIT)
         MSR     cpsr_cxsf,R1      ; ENTER SVC MODE
         MOV     sp,R2
         SUB     R2,R2,#SvcStkSz
  
         ORR     R1,R0,#(MODFIQ :OR: IRQBIT :OR: FIQBIT)
         MSR     CPSR_cxsf,R1      ; ENTER FIQ MODE
         MOV     sp,R2
         SUB     R2,R2,#FiqStkSz

         ORR     R1,R0,#(MODIRQ :OR: IRQBIT :OR: FIQBIT)
         MSR     CPSR_cxsf,R1      ; ENTER IRQ MODE
         MOV     sp,R2
         SUB     R2,R2,#IrqStkSz

         ORR     R1,R0,#(MODUDF :OR: IRQBIT :OR: FIQBIT)
         MSR     CPSR_cxsf,R1      ; ENTER UDF MODE
         MOV     sp,R2
         SUB     R2,R2,#UdfStkSz

         ORR     R1,R0,#(MODABT :OR: IRQBIT :OR: FIQBIT)
         MSR     CPSR_cxsf,R1      ; ENTER ABT MODE
         MOV     sp,R2
         SUB     R2,R2,#AbtStkSz

         ;ORR     R1,R0,#(MODUSR :OR: IRQBIT :OR: FIQBIT)
         ;MSR     CPSR_cxsf,R1     ; ENTER USR MODE
         ;MOV     sp,R2
         ;SUB     R2,R2,#UsrStkSz

         ORR     R1,R0,#(MODSYS :OR: IRQBIT :OR: FIQBIT)
         MSR     CPSR_cxsf,R1      ; ENTER SYS MODE
         MOV     sp,R2             ; 

三,初始化变量      
        编译完成之后,连接器会生成三个基本的段,分别是RO,RW,ZI,并会在image中顺序摆放.显然,RW,ZI在运行开始时并不位于指定的RW位置,因此必须初始化
         LDR     R0,=|Image$$RO$$Limit|
         LDR     R1,=|Image$$RW$$Base|
         LDR     R2,=|Image$$ZI$$Base|
1       
         CMP     R1,R2
         LDRLO   R3,[R0],#4
         STRLO   R3,[R1],#4
         BLO     %B1  

         MOV     R3,#0
         LDR     R1,=|Image$$ZI$$Limit|
2
         CMP     R2,R1
         STRLO   R3,[R2],#4
         BLO     %B2    

四,复制异常向量
         由于代码于RAM运行时,有明显的速度优势,而且变量可以动态配置,因此可以通过remap将RAM映射到0,使得出现异常时ARM从RAM中取得向量.
         IMPORT |Image$$RO$$Base|
         IMPORT |Image$$RO$$Limit|
         IMPORT |Image$$RW$$Base|
         IMPORT |Image$$RW$$Limit|
         IMPORT |Image$$ZI$$Base|
         IMPORT |Image$$ZI$$Limit|

                                          
COPY_VECT_TO_RAM
                         LDR     R0,=|Image$$RO$$Base|
   LDR     R1,=SYSINIT 
   LDR     R2,=0x200000      ; RAM START  
0  
                         CMP     R0,R1
   LDRLO   R3,[R0],#4
   STRLO   R3,[R2],#4
   BLO     %B0 

这段程序将SYSINIT之前的代码,也就是异常处理函数,全部复制到RAM中, 这就意味着不能将RW设置为0x200000,这样会使得向量被冲掉.

四,在RAM中运行
         如果有必要,且代码足够小,可以将代码置于RAM中运行,由于RAM中本身没有代码,就需要将代码复制到RAM中:
         COPY_BEGIN
                         LDR     R0,=0x200000
   LDR     R1,=RESET         ; =|Image$$RO$$Base|
   CMP     R1,R0             ; 
   BLO     COPY_END          ;
  
   ADR     R0,RESET
   ADR     R2,COPY_END
   SUB     R0,R2,R0
   ADD     R1,R1,R0
  
   LDR     R3,=|Image$$RO$$Limit|
3  
                         CMP     R1,R3
   LDRLO   R4,[R2],#4
   STRLO   R4,[R1],#4
   BLO     %B3
  
   LDR     PC,=COPY_END
  
COPY_END
程序首先取得RESET的连接地址,判断程序是否时是在RAM中运行,方法是与RAM起始地址比较,如果小于,那么就跳过代码复制.
在复制代码的时候需要注意,在这段程序结束之前的代码没有必要复制,因为这些代码都已经执行过了,所以,先取得COPY_END,作为复制起始地址,然后计算其相对RESET的偏移,然后以RO的值加上这个偏移,就是复制目的地的起始地址,然后开始复制.

五,开始主程序
         以上步骤完成,就可以跳转到main运行
         IMPORT Main

         LDR     PC,=Main
         B       .

 
六,器件初始化
         主程序首先要进行器件的初始化,对S64而言,应该先初始化WDT,因为默认情况下,WDT是打开的,然后是各设备的时钟分配,最后应该remap

RemapSRAM:
MOV R0, #0x40000000 //RAM 区首地址
LDR R1, =Vectors //向量表首地址
#下面一段程序是把从0x00000000 开始的64 个字节(FLASH 中的中断向量表和地址表)搬移到以
#0x40000000 为首地址的RAM 区中
LDMIA R1!, {R2-R9} //把以[R1]为首地址的32 个字节数据装载到R2-R9 中
STMIA R0!, {R2-R9} //把R2-R9 中的数据存入以[R0]为首地址的单元中
LDMIA R1!, {R2-R9} //把以[R1]为首地址的32 个字节数据装载到R2-R9 中
STMIA R0!, {R2-R9} ////把R2-R9 中的数据存入以[R0]为首地址的单元中

ARM的启动代码分析

相信使用过MDK的朋友都会发现,在新建任意一个ARM芯片的工程时,MDK开发环境都会自动的在代码中加入一个Startup.s的文件。
大家不能小看这个代码的功能了,这个代码中就是对应芯片的启动代码了。
       本文就以在MDK开发环境下对于LPC2103的启动代码分析了。
       在MDK开发环境下,系统复位后,都是会将ARM芯片切换到系统模式下工作。启动代码会在系统复位后立即执行,其功能如下:
1、定义中断和异常向量。
2、配置CPU时钟源(对于一些设备来说)。
3、使用内存重映射命令拷贝ROM中的异常向量到RAM中(例如:Atmel)。
4、初始化外部总线控制器和执行内存重映射(如果有必要)。
5、如果有必要,初始化其它外设。
6、为所有的模式预留和初始化堆栈。
7、清零数据初始化(仅针对GNU)。
8、跳转到C语言函数执行。
本文只是在探讨第一个步骤(定义中断和异常向量)。因为这个将会在之后我们要讲到的在uC/OS-II中进行中断函数的编写有关系,其他步骤,如果大家有兴趣,可以到网上去查阅相关资料。
下面代码就是我在MDK中,拷贝出来的一段初始化异常向量的代码了,详细代码如下:

AREA
RESET, CODE, READONLY

ARM

Vectors

LDRPC, Reset_Addr

LDRPC, Undef_Addr


LDRPC, SWI_Addr

LDRPC, PAbt_Addr

LDRPC, DAbt_Addr

NOP
;LDRPC, IRQ_Addr

LDRPC, [PC, #-0x0FF0]

LDRPC, FIQ_Addr

Reset_Addr DCD Reset_Handler
Undef_Addr DCD Undef_Handler
SWI_Addr DCD SWI_Handler
PAbt_Addr DCD PAbt_Handler
DAbt_Addr DCD DAbt_Handler

DCD 0
IRQ_Addr DCD IRQ_Handler
FIQ_Addr DCD FIQ_Handler

Undef_Handler B Undef_Handler
SWI_Handler B SWI_Handler
PAbt_Handler B PAbt_Handler
DAbt_Handler B DAbt_Handler
IRQ_Handler B IRQ_Handler
FIQ_Handler B FIQ_Handler
下面就进行一步一步代码分析:
1、
AREA
RESET, CODE, READONLY

ARM
这两段话表达的意思是:在代码段中定义一个段名为RESET的只读段,该代码段中的代码都在ARM指令集下进行。
2、Vectors

LDR PC, Reset_Addr

LDR PC, Undef_Addr

LDR PC, SWI_Addr


LDR PC, PAbt_Addr

LDR PC, DAbt_Addr

NOP
;LDR PC, IRQ_Addr

LDR PC, [PC, #-0x0FF0]

LDR PC, FIQ_Addr
上面的代码是ARM内核定义的异常像量表

异常类型
异常向量地址
该地址中的内容

复位
0x00000000(或0xFFFF0000)
LDR PC, Reset_Addr

未定义异常
0x00000004(或0xFFFF0004)
LDR PC, Undef_Addr

软件中断
0x00000008(或0xFFFF0008)
LDR PC, SWI_Addr

指令预取异常
0x0000000C(或0xFFFF000C)
LDR PC, PAbt_Addr

数据预取异常
0x00000010(或0xFFFF0010)
LDR PC, DAbt_Addr

预留
0x00000014(或0xFFFF0014)
NOP

IRQ中断
0x00000018(或0xFFFF0018)
LDR PC, [PC, #-0x0FF0]

FIQ中断
0x0000001C(或0xFFFF001C)
LDR PC, FIQ_Addr

大家注意一下,在中断向量地址中必须保存一条ARM指令集。在对应的异常向量地址中将跳转到对应的异常处理程序。
当然大家在这里也可以使用B指令,但是大家要注意一下,使用LDR指令可以实现±4GB地址空间实现。但是使用B执行跳转的话,就只能在±32M地址空间。所以建议大家使用LDR指令实现跳转。


可能大家看到了在IRQ中断向量地址中装载的是LDR PC, [PC, #-0x0FF0]指令,这里要和大家解释一下这条语句,在和大家解释这条语句时,希望大家去看看LPC2103的datasheet。
当处理器开始执行LDR PC, [PC, #-0x0FF0]
指令,PC寄存器的值就已经变成PC+8 = 0x20(这个不清楚的朋友必须去查询一下《ARM+Architecture+Reference+Manual(2nd+Edition)》中的Prefetch and self-modifying code),
Result = 0x20 – 0xff0 = 0xFFFFF030(实在不知道怎么算的就用电脑自带的计算器算算);
在查询LPC2103数据手册后,发现该地址对应的寄存器为VICVectAddr。
在这条语句执行完毕后,处理器就会跳转到导致产生IRQ中断,并跳转到VICVectAddr寄存器所指向的IRQ异常处理函数中进行操作。

实际运用中,我们可以更改一下IRQ异常向量地址的语句,让大家更加好看。更改如下:
LDR PC, [PC, #0xFFFFF020]
这条语句和刚才的MDK自带的语句实现的效果是一样的!

实例
软件实现中断处理过程

实验芯片:LPC2103


开发环境:MDK3.50


实现功能:在不使用__irq关键词时,如果编写中断服务程序。

知识要点:

1、在MDK开发环境下,对于LPC2000系列处理器,MDK默认的启动模式位系统模式。

2、在不使用__irq关键字声明时,只将中断处理函数当成普通函数进行处理,而不会在进入中断时对通用寄存器的内容进行保存,也不会再退出中断时对通用寄存器的内容进行恢复,因此这部分功能是必须手动添加的。

下面进行代码讲述,下面就是当进入IRQ中断处理函数时的处理程序,代码全部用汇编语句完成详细代码如下:

       EXPORT IRQ_Uart0

IMPORT DuleUart0


REQUIRE8

PRESERVE8

AREA CODE, CODE, READONLY

CODE32


IRQ_Uart0

STMFD SP!,{R0-R12,LR}


;保存当前处理器的所有寄存器


MRS R0,SPSR
;保存当前处理器的SPSR

STMFD SP!,{R0}

MRS R0,CPSR ;保存当前处理器的CPSR

STMFD SP!,{R0}

MOV R12, SP ;保存当前的SP寄存器


MRS R0,CPSR ;重新打开FIQ或IRQ中断


BIC R0,R0,#0xC0

MSR CPSR_cxsf,R0

BL DuleUart0 ;执行串口0的实际处理程序


MOV SP,R12 ;恢复SP寄存器的值


LDMFD SP!,{R0} ;恢复中断时的CPSR寄存器值


MSR CPSR_cxsf,R0


LDMFD SP!,{R0};恢复中断前的SPSR寄存器状态


MSR SPSR_cxsf,R0


LDMFD SP!,{R0-R12};恢复中断时的所有寄存器值


LDMFD SP!,{LR};恢复进入中断时的PC返回地址


SUBS PC,LR,#4;返回中断服务程序


END


大家可能存在的问题如下:

1、为什么我们在进入这段中断处理程序中还在使用LDM和STM语句呢,不是说在用户或系统模式中使用该语句会产生不可预料的结果么?

答:当系统进入IRQ异常时,就会将处理器模式切换到IRQ异常模式。

2、为什么会在最后一句执行SUBS PC,LR,#4(这一句就不在解释了),不清楚的就去看看《ARM体系结构与编程》

好了,我们现在对照着在uC/OS-II中的中断编写方法:

Void ISP_Function( void )


{


         保存全部的CPU寄存器;

         调用OSIntEnter()或OSIntNesting++;


If(OSIntNesting == 1 )

{

OSTCBCur->OSTCBStkPtr= SP ;

}


         清中断源;

         重新打开中断;

         执行用户代码做中断服务;

         调用OSIntExit();

         恢复所有CPU寄存器;

         执行中断返回指令;

}


而在我们的处理代码中,我们的执行过程如下伪代码所示:

Void ISRFunction( void )


{


         保存全部寄存器的内容;

         清中断源;

         执行用户代码;

         恢复所有CPU寄存器;

         执行中断返回;

}


对于没有的部分,大家可以试着去编写一下!

详细代码可以下载,也可以移植到你现有的开发板上去试试。


基于ARM的芯片多数为复杂的片上系统,这种复杂系统里的多数硬件模块都是可配置的,需要由软件来设置

 

其需要的工作状态。因此在用户的应用程序之前,需要由专门的一段代码来完成对系统的初始化。由于这

 

类代码直接面对处理器内核和硬件控制器进行编程,一般都是用汇编语言。一般通用的内容包括:

 

中断向量表

初始化存储器系统

初始化堆栈

初始化有特殊要求的断口,设备

初始化用户程序执行环境

改变处理器模式

呼叫主应用程序

 

中断向量表

      ARM要求中断向量表必须放置在从0地址开始,连续8X4字节的空间内。

      每当一个中断发生以后,ARM处理器便强制把PC指针置为向量表中对应中断类型的地址值。因为每

 

个中断只占据向量表中1个字的存储空间,只能放置一条ARM指令,使程序跳转到存储器的其他地方,再执行

 

中断处理。

中断向量表的程序实现通常如下表示:

AREA Boot ,CODE, READONLY

ENTRY

B  ResetHandler

B  UndefHandler

B  SWIHandler

B  PreAbortHandler

B  DataAbortHandler

B

B  IRQHandler

B  FIQHandler

其中关键字ENTRY是指定编译器保留这段代码,因为编译器可能会认为这是一段亢余代码而加以优化。链

 

接的时候要确保这段代码被链接在0地址处,并且作为整个程序的入口。

 

初始化存储器系统

 

存储器类型和时序配置

 

      通常FlashSRAM同属于静态存储器类型,可以合用同一个存储器端口;DRAM因为有动态刷新和地

 

址线复用等特性,通常配有专用的存储器端口。

 

      存储器端口的接口时序优化是非常重要的,这会影响到整个系统的性能。因为一般系统运行的速度

 

瓶颈都存在于存储器访问,所以存储器访问时序应尽可能的快;而同时又要考虑到由此带来的稳定性问题

 

存储器地址分布

 

一种典型的情况是启动ROM的地址重映射。

 

初始化堆栈

因为ARM7种执

行状态,每一种状态的堆栈指针寄存器(SP)都是独立的。因此,对程序中需要用到的每一种模式都要给

 

SP定义一个堆栈地址。方法是改变状态寄存器内的状态位,使处理器切换到不同的状态,让后给SP赋值。

 

注意:不要切换到User模式进行User模式的堆栈设置,因为进入User模式后就不能再操作CPSR 回到别的

 

模式了,可能会对接下去的程序执行造成影响。

 

这是一段堆栈初始化的代码示例,其中只定义了三种模式的SP指针:

MRS  R0,CPSR

BIC  R0,R0,#MODEMASK 安全起见,屏蔽模式位以外的其他位

ORR  R1,R0,#IRQMODE

MSR  CPSR_cxfs,R1

LDR  SP,=UndefStack

 

ORR  R1,R0,#FIQMODE

MSR  CPSR_cxsf,R1

LDR  SP,=FIQStack

 

ORR  R1,R0,#SVCMODE

MSR  CPSR_cxsf,R1

LDR  SP,=SVCStack

初始化有特殊要求的端口,设备初始化应用程序执行环境
映像一开始总是存储在ROM/Flash里面的,其RO部分即可以在ROM/Flash里面执行,也可以转移到速度更快的RAM中执行;而RW和ZI这两部分是必须转移到可写的RAM里去。所谓应用程序执行环境的初始化,就是完成必要的从ROM到RAM的数据传输和内容清零。
下面是在ADS下,一种常用存储器模型的直接实现:
LDR  r0,=|Image$$RO$$Limit| 得到RW数据源的起始地址
LDR  r1,=|Image$$RW$$Base| RW区在RAM里的执行区起始地址
LDR  r2,=|Image$$ZI$$Base| ZI区在RAM里面的起始地址
CMP  r0,r1         比较它们是否相等
   BEQ  %F1
0   CMP  r1,r3
   LDRCC r2,[r0],#4STRCC r2,[r1],#4
   BCC  %B0
1   LDR  r1,=|Image$$ZI$$Limit|
   MOV  r2,#0
2   CMP  r3,r1
   STRCC r2,[r3],#4
   BCC  %B2
程序实现了RW数据的拷贝和ZI区域的清零功能。其中引用到的4个符号是由链接器第一输出的。
|Image$$RO$$Limit|:表示RO区末地址后面的地址,即RW数据源的起始地址
|Image$$RW$$Base|:RW区在RAM里的执行区起始地址,也就是编译器选项RW_Base指定的地址
|Image$$ZI$$Base|:ZI区在RAM里面的起始地址
|Image$$ZI$$Limit|:ZI区在RAM里面的结束地址后面的一个地址
程序先把ROM里|Image$$RO$$Limt|开始的RW初始数据拷贝到RAM里面|Image$$RW$$Base|开始的地址,当RAM这边的目标地址到达|Image$$ZI$$Base|后就表示RW区的结束和ZI区的开始,接下去就对这片ZI区进行清零操作,直到遇到结束地址|Image$$ZI$$Limit|

改变处理器模式
因为在初始化过程中,许多操作需要在特权模式下才能进行(比如对CPSR的修改),所以要特别注意不能过早的进入用户模式。
内核级的中断使能也可以考虑在这一步进行。如果系统中另外存在一个专门的中断控制器,这么做总是安全的。
呼叫主应用程序
当所有的系统初始化工作完成之后,就需要把程序流程转入主应用程序。最简单的一种情况是:
IMPORT main


B   main
直接从启动代码跳转到应用程序的主函数入口,当然主函数名字可以由用户随便定义。
在ARM ADS环境中,还另外提供了一套系统级的呼叫机制。
IMPORT __main

B   __main
__main()是编译系统提供的一个函数,负责完成库函数的初始化和初始化应用程序执行环境,最后自动跳转到main()函数。(

 


本文主要以philips公司ARM7TDMI核的LPC2119为例来分析如何编写ARM7的启动代码。
  1、启动代码
  在嵌入式系统软件的开发中,应用程序通常是在嵌入式操作系统的开发平台上采用C语言编写的。然而,在ARM系统上电复位后,需要设置中断向量表、初始化各模式堆栈、设置系统时钟频率等,而这些过程都是针对ARM内部寄存器结构的操作,用C语言编程是很难实现的。因此在转到应用程序的c/c++编写之前,需要用ARM的汇编语言编写启动代码,由启动代码完成系统初始化以及跳转到用户C程序。在ARM设计开发中,启动代码的编写是一个极重要的过程。然而启动代码随具体的目标系统和开发系统有所区别,但通常包含以下部分:
  ·向量表定义
  ·地址重映射及中断向量表的转移
  ·堆栈初始化
  ·设置系统时钟频率
  ·中断寄存器的初始化
  ·进入C应用程序
  下面就结合PHILIPS的LPC2119的启动代码来分析与说明ARM7处理器的启动代码的编写。
  1.1向量表定义
  ARM芯片上电或复位后,系统进入管理模式、ARM状态、PC(R15)指向0x00000000地址处。中断向量表为每一个中断设置1个字的存储空间,存放一条跳转指令,通过这条指令使PC指针指向相应的中断服务程序入口,继而执行相应的中断处理程序。LPC2219的中断向量表和其它基于ARM核的芯片中断向量表较类似,只要注意LPC2219要使向量表所有数据32位累加和为零(0x00000000-0x0000001C的8个字的机器码累加), 才能使用户的程序脱机运行。
  1.2 地址重映射及中断向量表的转移
  ARM7处理器在复位后从地址0读取第一条指令并执行,因此系统上电后地址0必须是非易失的ROM/FLASH,这样才能保证处理器有正确可用的指令。为了加快对中断的处理以及实现在不同操作系统模式下对中断的处理,这就需要重新映射中断向量表、Bootblock和SRAM空间的一小部分。ARM具有非常灵活的存储器地址分配特性。ARM处理器的地址重映射机制有两种情况:
  ①由专门的寄存器完成重映射(Remap),只需对相应的Remap寄存器相应位设置即可。
  ②没有专门的Remap控制寄存器需要重新改写用于控制存储器起始地址的块(Bank)寄存器来实现Remap。在LPC2119上的重映射,可以通过存储器映射控制器来实现。实现REMAP操作的程序实现如下:
  MOV R8,#0x40000000;            /设置新向量表起始地址/
  LDR R9,=Interrupt_Vector_Table;             /读原向量表源地址/
  LDMIA R9!,(R0-R7);               /复制中断向量表及中断处理程序的入口地址到RAM中(64字节)/
  STMIA R8!,(R0-R7)
  LDMIA R9!,(R0-R7)
  STMIA R8!,(R0-R7)
  LDR R8,=MEMMAP ;           /REMMAP操作/
  MOV R9,#0x02
  STR R9, [R8]
  1.3 堆栈初始化
  启动代码中各模式堆栈空间的设置是为中断处理和程序跳转时服务的。当系统响应中断或程序跳转时,需要将当前处理器的状态和部分重要参数保存在一段存储空间中,所以对每个模式都要进行堆栈初始化工作,给每个模式的SP定义一个堆栈基地址和堆栈的容量。堆栈的初始化有两种方法:第一种方法是结合ADS开发套件中的分散加载文件来定义堆栈。第二种方法是最简单也是最常用的一种就是直接进入对应的处理器模式,为SP寄存器指定相应的值。下面给出了用第二种方法初始化管理模式和中断模式堆栈的程序:
  MSR CPSR_c, #0xD3 ;           /切换到管理模式,并初始化管理模式的堆栈/
  LDR SP, Stack_Svc
  MSR CPSR_c, #0xD2 ;            /切换到IRQ模式,并初始化IRQ模式的堆栈/
  LDR SP, Stack_Irq
  …
  1.4 系统部分时钟初始化
  时钟是芯片各部分正常工作的基础,应该在进入main()函数前设置。部分ARM7片子内部集成有PLL(锁相环)电路,用户可以用低频率的晶振通过PLL电路获得一个较高频率的时钟。LPC2119内部的PLL电路接受的输入时钟频率范围为10~25MHz,输入频率通过一个电流控制振荡器(CCO)倍增到范围10~60MHz。同时为了使高速的ARM处理器与低速的外设正常通讯和降低功耗(降低外设运行速度使功耗降低),LPC2119又集成了一个额外的分频器。PLL的激活是由PLLCON寄存器控制。PLL倍频器和分频器的值由PLLCFG寄存器控制。对PLLCON或PLLCFG寄存器的更改必须遵循严格的顺序,否则所作更改是无法生效的(在连续的VPB周期内向PLLFEED寄存器写入0xAA、0x55,在此期间中断必须是被禁止的。)
  1.5 中断初始化
  ARM7的向量中断控制器(Vectored Interrupt Controller)可以将中断编程为3类:FIQ、向量IRQ、非向量IRQ。FIQ中断请求的优先级最高,其次是IRQ中断请求,非向量IRQ的优先级最低。VIC具有32个中断请求输入,但在LPC2219中只占用了17个中断输入。对于这17个中断源的IRQ/FIQ选择,由VICIntSelect寄存器控制,当对应位设置位1时,则此中断为FIQ中断,否则为IRQ中断。若再将IRQ中断设置到向量控制寄存器(VICVectCntIn)中,则此中断为向量IRQ中断,否则为非向量IRQ中断。FIQ中断是专门用来处理那些需要及时响应的特殊事件,尽可能地只给FIQ分配一个中断源。
  1.6 进入C应用程序
  至此,系统各部分的初始化基本完成,可以直接从启动代码转入到应用程序的main()函数入口。从启动代码转入到应用程序的实例代码如下:
  IMPORT main
  LDR R0,=main
  BX R0
  2、总结
  一个优秀的启动代码将给应用程序的开发提供一个良好的开发平台。本文中较详细的讨论了启动代码的编写及难点。其中在堆栈初始化过程中要特别的注意两点:
  ①要尽量给堆栈分配快速和高带宽的存储器。
  ②尽量避免过早将处理器切换到用户模式,一般在系统初始化的最后阶段才切换到用户模式(用户模式没有权限通过修改CPSR来进行模式切换)。
  嵌入式系统的迅猛发展,使启动代码的编写成为嵌入式系统开发人员应该具备的能力。本文有助于正在从事嵌入式ARM开发的读者理解启动代码的内涵与编写出适合自己的启动代码。
 
阅读(1886) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~