Mini2440启动代码的编写(裸奔)

启动代码是系统上电或复位以后运行的第一段代码,它的作用是在用户程序运行之前对系统硬件及软件运行环境进行必要的初始化并在最后使程序跳转到用户程序,它直接面对ARM处理器内核及硬件控制器进行编程,所执行的操作与具体的目标系统紧密相关。
因为启动代码与目标系统紧密相连,所以在讲述Mini2440开发板的启动代码之前让我们先来了解一下它的启动方式。
S3C2440支持两种方式的启动:Nor Flash启动和Nand Flash启动。Nor Flash和Nand Flash都是非易失性存储器,Nor Flash的特点是芯片内执行,程序可以直接在其中运行,而不必将程序读取到RAM中运行。Nor Flash虽然具有这个优点,但是它的性价比远低于Nand Flash,因而很多系统采用Nand Flash启动。Nand Flash的特点是采用非线性存储模式,程序无法在其中运行,它只能作为程序或数据的存储载体,存储在其中的程序只能先拷贝到RAM中才能运行。
从Nor Flash启动时,与nGCS0相连的Nor Flash就被映射到nGCS0片选的空间,其地址被映射为0x00000000;从Nand Flash启动时,S3C2440芯片内部自带的一块容量为4K的被称为“Steppingstone”(“起步阶石”)的BootSRAM被映射到nGCS0片选的空间,其地址被映射为0x00000000。当系统上电或复位时,程序会从0地址处开始执行,因此我们编写的启动代码要确保存储在0地址处。
当启动方式为Nor Flash启动时,没有额外需要考虑的问题,因为这种情况下程序在系统启动前就存储在Nor Flash中,我们只要保证将启动代码保存在Nor Flash开始的位置即可,系统上电或复位时,0地址处的启动代码就会被执行。
在启动方式为Nand Flash启动的情况下,系统启动前所有的程序存储在Nand Flash中,系统的启动过程稍微有点复杂:系统上电或复位时,0地址处为S3C2440内部自带的BootSRAM,启动前里面没有任何存储内容,启动后S3C2440先通过硬件机制将Nand Flash前4K的内容拷贝至其中,然后再运行里面的程序(从0地址处)。这种情况下我们需要保证将启动代码保存在Nand Flash开始的位置,并且启动代码的大小要小于4K。如果系统的所有程序在编译链接后的大小小于4K,那在系统的启动代码中无需考虑将程序从Nand Flash搬运到SDRAM这个问题,因为所有的程序在启动时即全部由Nand Flash拷贝至BootSRAM,程序在BootSRAM中运行即可;如果系统的所有程序在编译连接后的大小大于4K,那在系统的启动代码中需要包含一段将系统的全部程序从Nand Flash搬运到SDRAM的代码,因为系统启动时只将Nand Flash的前4K拷贝到了BootSRAM中,还有部分程序保存在Nand Flash中, Nand Flash中是无法运行程序的,需要将所有程序拷贝至SDRAM并在其中运行,所以在系统的启动代码中要包含这段有关程序拷贝的代码,并在所有程序拷贝完成后使程序跳转到SDRAM中运行。也就是说在启动方式为Nand Flash启动的情况下,因为Nand Flash的特性,程序需要涉及到两次的搬移,一次是从Nand Flash搬移到BootSRAM,搬运的程序量大小是4K,目的是使系统能够启动;第二次搬运是从Nand Flash搬运到系统的SDRAM,如果系统的所有程序量小于4K,这一步可以省略,搬运的程序量大小是系统的所有程序,目的是使程序在SDRAM中运行。第一次搬运是S3C2440通过硬件机制自动实现的,我们无需干预,第二次则需要我们程序员来实现,这部分在下面的有关内容中会详细讲解。
Mini2440开发板带有两种FLASH:2M的 Nor Flash和128M的Nand Flash。按照开发板的设计目的以及很多实际的应用,系统一般选择从Nand FLASH启动,此文档讲解的内容也是基于Nand FLASH启动的,但厂家为了方便大家学习还是保留了Nor Flash,我们也可以将启动代码烧写至Nor Flash,并从中启动。两种启动方式的启动代码稍有不同,不同点主要在上述的第二次程序拷贝。启动方式可通过拨键开关S2来选择。
通过上述讲解我们简单了解了S3C2440的启动过程,现在就来讲述一下启动代码的编写过程。启动代码是面向ARM处理器内核和硬件控制器的,所以它的绝大部分都是通过汇编语言实现的。

一.启动代码的功能与实现

启动代码主要是在主程序运行之前初始化系统硬件及软件的运行环境,它的主要功能包括以下几个方面:
■        建立异常向量表
■        初始化系统堆栈
■        初始化硬件
■        应用程序执行环境初始化
■  跳转至主函数
下面我们来分别讲解一下:
1.        建立异常向量表
异常向量表一般位于启动代码的开始部分,它是用户程序与启动代码之间以及启动代码的各部分之间联系的纽带。它由一个一个的跳转函数组成,它就像一个普通的散转函数,只不过散转的过程中有硬件机制参与,当系统发生异常时,ARM处理器会通过硬件机制强制将PC指针指向异常向量表中对应的异常跳转函数存储的地址,然后程序会跳转到相应的异常中断服务程序去执行。下面我们来逐步了解一下异常向量表。
ARM有7种异常,它们分别是:
复位异常,当开发板上电或复位时进入;
未定义指令异常,当遇到无法识别的指令时进入;
软中断异常,当发生软中断时进入;
预取中止异常,发生指令预取错误时进入;
数据中止异常,对数据访问不能完成时进入;
IRQ异常,当发生IRQ中断时进入;
FIQ异常,当发生FIQ中断时进入;
除了这7种异常之外,异常向量表中还有一个保留位置,所以建立异常向量表需要开辟一块大小为8×4字节的空间,每个异常占据一个字(4个字节)的空间,这一个字的空间包含的是一个跳转指令,通过这条指令使PC指向相应异常处理函数的入口,具体的异常处理函数在别处实现。
异常向量表中指令、存储地址等对照表如表1所示:
表1
地址        指令        异常名称        进入时处理器模式
0x00        B   Reset        复位异常        管理模式
0x04        B   HandlerUndef        未定义指令异常        未定义模式
0x08        B   HandlerSWI        软中断异常        管理模式
0x0c        B   HandlerPabort        指令预取异常        中止模式
0x10        B   HandlerDabort        数据中止异常        中止模式
0x14        B   .        保留        ——
0x18        B   HandlerIRQ        IRQ中断异常        中断模式
0x1c        B   HandlerFIQ        FIQ中断异常        快中断模式

异常向量表有关的代码如下:
CODE32                   //表明是ARM指令
              AREA  startup,CODE,READONLY //AREA表明段,startup是段名,CODE表明是代码段,//READONLY是性质-只读
              ENTRY                        //函数入口
          B   ResetInit                   //复位异常
          B   HandlerUndef               //未定义异常
          B   HandlerSWI                //软中断异常
          B   HandlerPabort              //取指中止异常
          B   HandlerDabort              //数据中止异常
          B   .                         //保留
          B   HandlerIRQ                //中断异常
          B   HandlerFIQ                //快中断异常
ARM要求异常向量表必须存储在0地址处,这样当开发板上电或复位时,PC会指向0地址处,进而跳转到复位异常处理函数函数ResetInit,这个函数负责完成系统的初始化工作,即初始化堆栈、初始化硬件、初始化应用程序执行环境、跳转至主函数;发生其他异常时ARM通过硬件机制将PC指向异常向量表对应的位置,进而跳转至相应的异常处理函数。

复位异常的处理函数完成的功能我们下面会讲解(2至5大点),在此不再赘述,我们来分别看一下其他异常处理函数是怎么处理的。

1.1未定义异常处理函数
当ARM处理器不认识当前指令时,它就将该指令发送至协处理器,如果协处理器也不能识别此指令,就产生未定义异常。
发生未定义异常之后,CPU会将函数的返回地址及此时的CPSR保存至未定义异常专用的R14和SPSR,然后再修改CPSR使系统进入未定义异常模式运行,PC被强制指向0x00000004,然后程序就跳转至未定义异常处理函数。上述过程都是通过硬件机制实现的,我们无需干预。如果应用中没有特殊的要求,比如用未定义异常来仿真某些器件的功能(例如浮点运算),我们可以将此处理函数编写为一个简单的死循环。
程序的代码如下:
HandlerUndef
           B   HandlerUndef

1.2软中断异常处理函数
软件中断异常是由用户定义的中断指令,它可以用于用户模式下的程序调用特权模式下的操作指令;在实时操作系统中可以通过该机制实现系统功能调用。
ARM处理器一共有7种模式,他们分别是用户模式、系统模式、管理模式、中止模式、未定义模式、中断模式和快速中断模式,七种模式之中除了用户模式以外其它六种被称为特权模式,特权模式下可以对CPSR进行修改,用户模式则只能读取CPSR的值而无法对其进行修改。在应用中一般所有任务都是在用户模式下运行的,但是用户模式下无法修改CPSR,所以无法切换到其它模式。在这种情况下我们可以通过SWI指令来解决这一问题,SWI指令可以强迫处理器从用户模式切换至管理模式, SWI是从用户模式切换到特权模式的唯一途径。
处理器响应SWI异常的流程如下:发生SWI异常以后处理器自动将函数返回地址及CPSR保存至SWI模式专用的R14和SPSR,然后通过修改CPSR进入管理模式、禁止IRQ中断,最后强制PC指向0x00000008,此时程序会跳转至软中断异常处理函数。
在本文档涉及的启动代码中,软中断异常处理函数的作用是控制IRQ、FIQ中断的开启和关闭。在初始化系统堆栈时,最后使系统运行在系统模式(见第2大点详述),并且IRQ和FIQ是默认关闭的,如果我们在程序中需要使用IRQ或FIQ,在使用之前需要先通过SWI将相应的中断开启。下面来了解一下。
相关的代码段:
(1)
……
int Main(void)
{
//请在此进行中断初始化
……
IRQEnable();  //开启IRQ中断
//在此等待中断发生
}
(2)
// 使能/禁能IRQ、FIQ中断
__swi(0x00)  void SwiHandle1(int Handle);
#define  IRQDisable()    SwiHandle1(0)
#define  IRQEnable()     SwiHandle1(1)
#define  FIQDisable()    SwiHandle1(2)
#define  FIQEnable()     SwiHandle1(3)
(3)
HandlerSWI
               CMP    R0,#4      //R0保存的是软中断函数传递的参数,比其值和4,原因是预设了4个软中断,要根据实际需要设置
         LDRLO  PC,[PC,R0,LSL #2]    //如果小于4则执行SwiFunction
         MOVS   PC,LR              //如果大于等于4则返回
         
SwiFunction                          //软中断散转函数,根据软中断号来执行相应的处理函数
         DCD    IRQDisable           //参数为0的处理函数
         DCD    IRQEnable           //参数为1的处理函数
         DCD    FIQDisable           //参数为2的处理函数
         DCD    FIQEnable           //参数为3的处理函数
IRQDisable                           //关闭IRQ中断
         MRS    R0,SPSR             //将SPSR的值(软中断前的CPSR)放入寄存器R0
         ORR    R0,R0,#IRQMSK      //将此值修改,屏蔽IRQ中断
         MSR    SPSR_c,R0           //将此值修改入SPSR,注意_c代表只能修改低8位
         MOVS   PC,LR              //返回原来的程序

IRQEnable                            //使能IRQ中断
         MRS    R0,SPSR
         BIC     R0,R0,#IRQMSK
         MSR    SPSR_c,R0
         MOVS   PC,LR
FIQDisable                           //关闭FIQ中断
         MRS    R0,SPSR
         ORR    R0,R0,#FIQMSK
         MSR    SPSR_c,R0
         MOVS   PC,LR
FIQEnable                            //使能FIQ中断
         MRS    R0,SPSR
         BIC     R0,R0,#FIQMSK
         MSR    SPSR_c,R0
         MOVS   PC,LR

先来看一下代码段(2),这一代码段位于启动代码中。__swi是ADS编译器的关键字,用它做前缀可以声明一个软中断调用,格式为:
__swi(功能号)   返回值类型  名称 (参数列表)
功能号:软中断功能号,类似软中断指令中的立即数,即软中断号
名  称:即调用软中断时用于描述软中断的函数名称
参  数:软中断函数的参数,根据ATPCS规则,如果软中断函数有不超过4个参数时,通过寄存器R0~R3传递,超过4个参数时用堆栈来传递。
在代码段(2)中是这样来编写的:__swi(0x00) void SwiHandle1(int Handle)。其中0x00为软中断功能号;软中断函数名称为SwiHandle1;函数只有一个参数,则使用寄存器R0来传递;函数没有返回值。
紧接着这句代码定义了4个宏,分别表示禁能IRQ函数、使能IRQ函数、禁能FIQ函数、使能IFQ函数,其实这四个宏调用的软中断函数都是一样的,只是参数不同而已。如代码段(1)所示,在用户程序中调用“IRQEnable( );”时,处理器会产生软中断,软中断函数的参数为1。
代码段(3),这一段也位于启动代码中,当发生软中断时,PC被强制指向0x00000008,这个地址中存放的是一条跳转到软中断异常处理函数的指令,所以程序会跳转至标号“HandlerSWI”处执行(即软中断异常处理函数)。HandlerSWI函数的功能是判断寄存器R0的值(R0的值为软中断函数传递过来的参数)是否小于4,如果小于4则程序跳转至标号“SwiFunction”执行,如果不是则函数返回。SwiFunction函数是一个散转函数,它的功能是根据寄存器R0的值跳转至对应的函数处执行,即如果参数为1,则函数会跳转至IRQEnable处执行,将IRQ中断使能。
代码段(3)的四个函数的流程都是一样的,先将SPSR(SPSR_svc)的值放入一个寄存器,SPSR的值为发生软中断以前的CPSR的值,发生软中断以后,处理器的模式切换为管理模式,CPSR的值的模式位会改变,不过在改变以前,原来模式的CPSR值会保存在当前模式的SPSR中。SPSR的值放入寄存器以后再将寄存器的值进行修改,然后再写入SPSR,函数返回时SPSR的值会拷贝至CPSR。此时读者可能注意到了我们使用的是SPSR_c,这是什么意思呢?在使用MSR指令对PSR进行操作的时候,我们在PSR的后面加一些标志来对可修改的位进行限定,目的是避免对某些位进行操作时会影响其他位的值。这些标志代表的意思如下:
PSR_c:只会修改PSR[7:0]位,控制位包括模式位、T位(状态位)和中断禁止位;
PSR_x:只会修改PSR[15:8]位,保留位;
PSR_s:只会修改PSR[23:16]位,保留位;
PSR_f:只会修改PSR[31:24]位,高4位为条件代码标志位,其余为保留位。
需要提醒两点:只有异常模式才有SPSR寄存器,用户模式和系统模式是没有的。对PSR的修改推荐使用“读------修改------写”的方式。


1.3取指中止异常处理函数
ARM9采用的是5级流水线的哈佛结构,这5级流水线是:取指、译码、执行、缓冲/数据、回写,如果处理器预取的指令的地址不存在,或者该地址不允许当前指令访问,则当该预取的指令被执行时就会产生指令预取中止异常。
取指异常的发生及处理过程如下:指令预取时,如果发现目标地址非法,该指令会被标记,但该指令之前的指令继续执行,当执行至被标记的指令时,处理器就产生取指中止异常中断。发生取指中止异常之后,处理器将函数返回地址(PC-4,即被标记的指令的地址+4)及CPSR保存至取指中止异常专用的R14和SPSR,然后通过修改CPSR将处理器切换至取指异常状态,最后将PC强制指向0x0000000C,这时程序会自动跳转至取指异常处理函数。上述过程都是通过硬件机制实现的,我们无需干预。
如果应用中不涉及MMU,取指异常处理函数可以设计成一个简单的死循环即可。具体程序如下:
HandlerPabort
            B   HandlerPabort

1.4数据中止异常处理函数
ARM指令集和Thumb指令集中都有专门的数据处理指令,如果数据访问指令的目标地址不存在或者该地址不允许此指令访问,处理器就会产生数据中指指令。
发生数据中止异常后,处理器将函数的返回地址及CPSR保存至数据中止异常模式专用的R14和SPSR,然后通过修改CPSR的值进入数据访问中止模式,最后强制PC指向0x00000010,程序就会跳转至数据中止异常处理函数执行。
如果应用中不涉及MMU,数据中止异常的处理函数可设计为简单的死循环。具体的程序如下:
HandlerDabort
             B    HandlerDabort

1.5 IRQ中断异常处理函数
ARM内核为了处理外部设备向CPU发出的服务请求,特别是一些紧急事件,而特别设计了中断处理机制,当发生中断时,处理器暂停正在执行的程序,然后跳转至中断处理函数去执行,中断处理函数执行完毕后再返回至原来的程序跳转处继续向下执行。
当处理器的外部中断请求引脚被拉低,并且CPSR寄存器中的I位被清零时,处理器就会产生IRQ中断异常。产生IRQ中断以后,ARM内核将函数返回地址保存至IRQ模式专用的R14,将CPSR保存至IRQ模式专用的SPSR,然后修改CPSR禁止新的IRQ产生,并将处理器模式切换至IRQ模式,最后强制PC指向0x00000018,程序跳转至IRQ中断处理函数执行。
在编写IRQ中断处理函数的时候我们需要注意中断的可重入性,即在有些IRQ的中断处理函数中,可以允许新的IRQ中断产生,但这种情况下需要注意,原先的中断处理程序使用的一些寄存器值会被新的中断给刷新,比如LR,所以还需要做一些准备工作来避免这种情况出现。满足这些条件的中断处理函数叫做可重入的中断处理函数(中断嵌套)。
为了使程序简单化便于大家理解,我们在此介绍的是不可重入的中断。IRQ的处理函数可参照下列代码段编写,它是在别的文件中用C语言来实现的:
void __irq IRQHandle(void)
{
   void    (*p)(void);      //定义一个函数指针
   int     irq_no;         //中断号
   uint32  intpnd;         //中断挂起号
   
   intpnd=rINTPND;       //从中断挂起寄存器读取中断号
   //将中断挂起号转换为中断号
   for(irq_no=0;irq_no<32;irq_no++)
   {
      intpnd=intpnd>>1;
      if(intpnd==0)
      break;
   }
   //利用函数指针取得中断服务程序的地址
   p=(void(*)(void))VICVectAddr[irq_no];
   p( );                     //运行中断服务程序
}
在汇编语言实现的启动代码中,需要在异常向量表之前引入这个函数,可以通过以下代码实现:
IMPORT      IRQHandle
因为上述的IRQ处理函数不具有可重入性,所以我们可以使用关键字__irq来说明,使用这个关键字可以自动实现进入中断处理函数时一些关键寄存器的保存及函数处理完毕后的自动返回,程序编写者不用自己编码实现。
上述程序只是获得中断源以后激活相应的中断服务程序,具体的中断服务程序还需要程序员自己实现,可以参考以下例子来实现。
例如一个应用中使用了两个外部中断,一个是EINT0,另一个是EINT1,两个中断的处理函数分别为:
Void IRQ_Eint0(void)
{
  //中断处理的内容自己编写
}
Void IRQ_Eint1(void)
{
  //中断处理的内容自己编写
}
编写完成中断处理函数以后还需要以下两个语句来使其和IRQ异常处理函数建立联系:
VICVectAddr[0] = (uint32) IRQ_Eint0;
VICVectAddr[1] = (uint32) IRQ_Eint1;
经过上述处理之后,发生外部中断以后系统就能正确反应了。有几点需要注意:ARM有32个外部IRQ中断源,S3C2440的外部中断8-23是占用一个IRQ源的,使用到这个范围内的多个外部中断时,需要再添加判断语句;中断处理程序的最后需要将中断标志清掉,以免系统不断的响应。具体的程序可以见附带的工程模板。
在一些对实时性要求严格的应用中,特别是使用操作系统时可能需要使用可重入的IRQ异常处理。使用可重入的IRQ中断时,必须在中断服务程序中重新使能IRQ中断,因为内核进入IRQ中断时默认的是关闭IRQ中断。还需要将“老”IRQ用到的一些寄存器的值保存至堆栈中,防止“新”的IRQ将其中的数据破坏。可重入的IRQ中断请大家参阅有关资料,在此不再赘述。

1.6 FIQ异常中断处理函数
在某些具体应用中,系统对实时性比较敏感,比如数据传输或通道处理时就需要非常短的中断响应时间,于是ARM在IRQ外还设计了一种更快速的中断类型:FIQ。ARM在硬件结构及资源分配上都给予了FIQ足够的支持,比如FIQ模式具有8个专用的寄存器,当系统从其他模式切换为FIQ模式时,这些寄存器的值无需进栈,节省了寄存器入栈的时间;另外FIQ异常的入口处于异常向量表的最后,我们可以紧接着异常向量表就进行FIQ异常处理,无需跳转,这样也可以节省反应时间。
当处理器的外部快速中断引脚被拉低,并且CPSR寄存器中的F位被清零时,处理器就会产生FIQ中断异常。FIQ的中断优先级最高,当系统进入FIQ中断异常后,其他的所有外部中断就会被屏蔽。
发生FIQ异常以后,ARM内核将函数返回地址及CPSR保存至FIQ异常模式专用的R14及SPSR中,然后通过修改CPSR切换至FIQ模式并禁止FIQ和IRQ,然后强制PC指向0x0000001C,程序就会跳转至FIQ异常处理函数。
FIQ异常处理函数可以参照以下函数段来编写:
HandlerFIQ
        STMFD   SP!, {R0-R3, LR}        //保存返回地址
        BL        FIQ_Exception                        // FIQ中断处理
        LDMFD   SP!, {R0-R3, LR}        //保存的寄存器出栈
        SUBS     PC,  LR,  #4          //返回
其中“FIQ_Exception”是在别的文件中用C语言实现的FIQ处理函数,参照以下函数段编写:
void  FIQ_Exception(void)
{
    while(1);                      // 这一句替换为自己的代码
}
当然在汇编语言实现的启动代码中需要在HandlerFIQ前先引入这个函数,代码为:
IMPORT    FIQ_Exception
在实际应用中不宜安排过多的FIQ中断,这样需要在FIQ异常处理函数中增加中断源判断,使反应时间增长,失去了快速中断的意义,同理也不宜允许FIQ中断嵌套(可重入)。
注意,在本文档所述的启动代码编写方法中初始化系统堆栈时已将FIQ屏蔽,在使用FIQ之前请使用SWI中断通过修改CPSR来允许FIQ中断。

2.        系统堆栈的初始化
ARM有7种模式,它们是用户模式、快速中断模式、中断模式、管理模式、中止模式、未定义模式和系统模式。其中除用户模式以外其他6种为特种模式,在特权模式下程序可以任意的切换到其他模式并能访问所有的系统资源,而用户模式无法改变CPSR,无法切换到其它模式。在特权模式中除系统模式以外其他5种模式为异常模式,异常模式可以通过软件修改CPSR的值进入,还可以由特定的异常进入,例如发生外部中断时,系统会进入中断模式。用户模式与系统模式无法由异常进入,只能通过修改CPSR的值进入。
表2
处理器模式        描述
用户模式        运行用户程序
快速中断模式        用于高速数据传输和通道处理,FIQ发生时进入
外部中断模式        IRQ发生时进入
管理模式        供操作系统使用的一种保护模式
中止模式        用于虚拟内存及存储保护
未定义指令模式        用于支持通过软件仿真硬件的协处理器
系统模式        用于运行特权级的操作系统任务

系统堆栈的初始化主要是给各个处理器模式分配堆栈空间。堆栈是为中断或程序跳转服务的,当发生中断或程序跳转时,需要将当前处理器的状态及一些参数保留在堆栈中,当中断处理完毕以后或程序执行完后返回时,再将堆栈内保存的现场数据予以恢复,以保证原来的程序正确运行。例如各种模式下可以共用寄存器R0~R7,模式切换的前后(即异常或中断发生前后)可能都需要使用它们,如果不保存的话,当异常或中断返回后,原来的程序可能因为这些寄存器中的值被破坏而无法运行。
        系统堆栈的初始化函数可以参照如下代码编写:
StacksInit          //函数标号
   
         MOV  R0,LR   ;//保存函数返回地址,请注意一定要保存,否则LR会变化
        
         ;//设置管理模式的堆栈
             MSR  CPSR_c,#(SVCMODE|IRQMSK|FIQMSK)//切换到管理模式,屏蔽中断
             LDR        SP,=StackSvc
             ;//设置未定义模式的堆栈
             MSR  CPSR_c,#(UNDMODE|IRQMSK|FIQMSK)//切换到未定义模式,屏蔽中断
             LDR        SP,=StackUnd               
             ;//设置中止模式的堆栈
             MSR  CPSR_c,#(ABTMODE|IRQMSK|FIQMSK)//切换到中止模式,屏蔽中断
             LDR        SP,=StackAbt               
             ;//设置中断模式的堆栈
             MSR  CPSR_c,#(IRQMODE|IRQMSK|FIQMSK)//切换到IRQ模式,屏蔽中断
             LDR        SP,=StackIrq               
             ;//设置快中断模式的堆栈
             MSR  CPSR_c,#(FIQMODE|IRQMSK|FIQMSK)//切换到FIQ模式,屏蔽中断
             LDR        SP,=StackFiq
             ;//设置系统模式的堆栈
             MSR  CPSR_c,#(SYSMODE|IRQMSK|FIQMSK)//切换到系统模式,屏蔽中断
         LDR  SP,=StackUse               
             
             MOV        PC,R0   ;//返回
通过以上程序可以看出,初始化某种模式的堆栈需先进入这种模式,即将CPSR的模式位置为预先定义好的数据,然后将对应模式的堆栈的入口地址赋值给这种模式下的堆栈指针SP。这样当程序进入异常模式时可以将需要保护的寄存器的值放入SP指向的堆栈,当异常处理完毕返回时,从对应的堆栈中将保存的数据恢复,这种入栈出栈的方式能够确保异常返回后程序可正常运行。
初始化系统堆栈需要注意以下几点:
(1)        系统初始化的准备工作
工作模式位的设定
USRMODE        EQU     0x10 ;//用户模式
FIQMODE         EQU     0x11 ;//快中断模式
IRQMODE         EQU     0x12 ;//中断模式
SVCMODE        EQU     0x13 ;//管理模式
ABTMODE        EQU     0x17 ;//中止模式
UNDMODE        EQU     0x1B ;//未定义模式
SYSMODE         EQU     0x1F ;//系统模式
         中断的屏蔽
IRQMSK         EQU     0x80 ;//CPSR的中断屏蔽位
FIQMSK         EQU     0x40 ;//CPSR的快中断屏蔽位
NOINT           EQU            0xc0;//两种中断都屏蔽
系统堆栈空间的设定
StackUse   EQU   (_STACKBASEADDR-0x3800)  ;//用户模式的堆栈空间从0x33ff4800开始
StackSvc   EQU   (_STACKBASEADDR-0x2800)  ;//管理模式的堆栈空间从0x33ff5800开始
StackUnd   EQU   (_STACKBASEADDR-0x2400)  ;//未定义模式的堆栈空间从0x33ff5c00开始
StackAbt   EQU    (_STACKBASEADDR-0x2000)  ;//中止模式的堆栈空间从0x33ff6000开始
StackIrq    EQU   (_STACKBASEADDR-0x1000)  ;//中断模式的堆栈空间从0x33ff7000开始
StackFiq   EQU    (_STACKBASEADDR-0x0)    ;//快中断模式的堆栈空间从0x33ff8000开始
其中_STACKBASEADDR决定堆栈在内存中的存放位置,在此我们设置为0x33ff8000,各种模式的堆栈起始位置我们可以从上述程序的注释中获得。
在分配堆栈空间时需要合理规划堆栈的大小,太小了会使栈泄露,太大会浪费内存。系统需要初始化哪些模式的堆栈还要看实际应用中会使用到哪些模式,一般来说管理模式堆栈必须分配,因为开发板上电或复位时,系统运行的模式是管理模式。如果应用中使用了IRQ中断,则IRQ模式的堆栈需要分配,而且堆栈的大小还要考虑IRQ中断的嵌套层数。
(2)        堆栈初始化的顺序。
堆栈初始化的顺序决定系统最后运行在哪种处理器模式,最后初始化哪种模式的堆栈,系统就运行在哪种模式。通过前述我们已经知道7种处理器模式中除用户模式以外其它被称为特权模式,特权模式中除系统模式外其它模式被称为异常模式。管理、中止、未定义、中断和快中断这五种异常模式由相应的异常进入,用户程序不适合在这些模式下运行。这样留给我们选择的只有用户模式和系统模式,系统模式可以访问所有的系统资源并可切换到其他模式,而用户模式却无法切换到其它模式,所以在堆栈初始化的时候我们可以在最后初始化系统模式的堆栈,使程序运行在系统模式。
另外在启动代码中如果是通过BL指令跳转到堆栈初始化函数的话,需要注意一点:堆栈初始化后如果系统运行在非管理模式,则堆栈初始化函数的最后不应该通过MOV        PC,LR返回,因为系统上电或复位后系统运行在管理模式,而各种特权处理器模式下都有自己对应的LR,上面的这条指令放入PC的是最后初始化的模式的LR,而不是管理模式的LR,所以无法正确返回。正确的方法是刚进入堆栈初始化函数时将LR保存在一个通用寄存器,函数返回时将这个寄存器中的值放入PC即可。如果最后初始化的是管理模式,可以通过MOV        PC,LR返回。

3.        初始化硬件
硬件初始化的目的是为主程序的运行创造一个合适的硬件环境,这一部分是和具体的应用系统密切相关的。初始化硬件一般包括以下几个方面:
■        关闭看门狗
■        屏蔽所有中断
■        初始化时钟和PLL
■        初始化存储系统
我们来详细讲解一下以上各部分:
(1)        关闭看门狗
看门狗,又叫 watchdog timer,简称WDT,是一个定时器电路,一般有一个输入,叫喂狗端,还有一个输出端连接到MCU的RST端。MCU正常工作的时候,每隔一段时间输出一个信号到喂狗端,给WDT清零,如果超过规定的时间不喂狗,比如在程序跑飞时,WDT就输出一个复位信号到MCU的RST端,使MCU复位,以防止MCU死机。看门狗的作用就是防止程序发生死循环而造成系统死机。
WDT虽然可以防止程序跑飞,保证系统的稳定性,但有时也可能会给我们造成麻烦。ARM重启以后,WDT是默认打开的,看门狗开始自动计数,如果到了一定的时间还不去清看门狗,那么看门狗计数器就会溢出从而引起看门狗中断,造成系统复位。为避免WDT造成的系统复位,我们在程序中需要周期的执行喂狗动作,这样会造成一定的资源浪费。为防止在系统启动过程中再发生重启,我们在启动代码中需要将WDT关掉,系统成功启动以后如确有需要可以再“养狗”。
关闭看门狗的程序如下:
LDR  R0,=WTCON
             LDR  R1,=0x0
             STR  R1,[R0]
将看门狗控制寄存器的最低位清零即可关闭看门狗。
(2)        屏蔽所有中断
中断的服务程序一般是在用户程序中实现的,启动代码无需处理中断,另外为避免在启动过程中触发中断,所以我们可以通过写中断屏蔽寄存器来将所有的中断予以屏蔽。
屏蔽中断的程序如下:
//屏蔽所有的中断
              LDR  R0,=INTMSK
              LDR  R1,=0xFFFFFFFF
              STR  R1,[R0]
              //屏蔽所有子中断
              LDR  R0,=INTSUBMSK
              LDR  R1,=0x3FF
              STR  R1,[R0]
中断屏蔽寄存器中某位置位,则对应的中断被屏蔽。
(3)        初始化PLL和时钟
时钟是处理器运行的脉搏,在进入主函数之前应该对其进行设置。PLL,锁相环,其作用是将外部晶振的输入频率倍频到一个较高的频率。S3C2440有两个锁相环:MPLL和UPLL。MPLL用于CPU及其它外围设备,通过MPLL会产生三个时钟频率:FCLK、HCLK、PCLK,FCLK就是CPU的运行时钟频率,HCLK驱动AHB总线设备(例如SDRAM),PCLK驱动APB总线设备(例如UART);UPLL用于USB模块。具体的设置方法请参照S3C2440A的数据手册。
PLL和时钟初始化的程序如下:
//初始化PLL和时钟
              LDR  R0,=LOCKTIME      ;//PLL重置延迟
              LDR  R1,=0xFFFFFFFF     ;//由于配置或其他原因导致主频变化时
              STR  R1,[R0]              ;//PLL新的输出需要一个稳定过渡时间
              
              LDR   R0,=CLKDIVN        ;//设置分频数
              MOV  R1,#7
              STR   R1,[R0]
              
              ;//当设置UPLLCON和MPLLCON时,需要先设置UPLLCON
              LDR  R0,=UPLLCON        ;//USB时钟
              LDR  R1,=((U_MDIV<<12)+(U_PDIV<<4)+U_SDIV)
              NOP                      ;//USB时钟设置后需要7个时钟延时
              NOP
              NOP
              NOP
              NOP  
              NOP
              NOP
            
             LDR  R0,=MPLLCON         ;//锁相环控制寄存器
             LDR  R1,=((M_MDIV<<12)+(M_PDIV<<4)+M_SDIV)
             STR  R1,[R0]
LOCKTIME为时间锁存计数器寄存器,为什么要设置这个寄存器呢?由于软件配置、系统上电或重启及其他原因导致PLL变化时,刚开始PLL处于一个不稳定的状态,此时不使用PLL作为FCLK的输出而使用外部时钟。经过LOCKTIME时间以后PLL稳定了,才将PLL解锁,使用PLL给系统提供时钟。S3C2440A的数据手册规定的锁定时间U_LTIME、M_LTIME均为300us,外部时钟为12M的晶振,要使(1/12M)×N>300(N为计数器值),则N>3600,即将U_LTIME、M_LTIME都设置为大于3600。LOCKTIME寄存器的[15:0]为M_LTIME,[31:16]为U_LTIME,将它们的值设置为默认值0xFFFFFFFF即可。
CLKDIVN 为时钟分频控制寄存器,作用是设置PCLK、HCLK、FCLK的分频值,具体的设置请见S3C2440A数据手册有关CLOCK的章节。
UPLLCON 为UPLL配置寄存器,作用是设置UPLL的输出频率,寄存器[19:12]是主分频器控制位,用来设置U_MDIV;[9:4]时预分频器控制位,用来设置U_PDIV的值;[1:0]是后分频器控制位,用来设置U_SDIV的值。UPLL的输出频率计算公式为=(m×FIN)/(p×2 ),其中m=U_MDIV+8;p=U_PDIV+2;s=U_SDIV;FIN是外部的晶振的频率12MHz。因为ARM920T内核是5级流水线(取指、译码、执行、缓冲/数据、回写),需要经过延迟5个指令周期以后指令才能生效,所以紧接着又添加了7个NOP的空指令。
MPLLCON为MPLL配置寄存器,作用是设置MPLL的输出频率,寄存器[19:12]是主分频器控制位,用来设置M_MDIV;[9:4]时预分频器控制位,用来设置M_PDIV的值;[1:0]是后分频器控制位,用来设置M_SDIV的值。MPLL的输出频率计算公式为=(2×m×FIN)/(p×2 ),其中m=U_MDIV+8;p=U_PDIV+2;s=U_SDIV;FIN是外部的晶振的频率12MHz。
根据数据手册的规定,如果UPLL和MPLL都需要设置的话,需要先设置UPLL。如果HDIVN位不为0的话,需要在运行主程序前将CPU的总线模式设置为异步总线模式,这一点请看下面的代码段,这段代码为S3C2440的数据手册提供的:
AsyncBusMode                            //函数标号
             MRC  p15,0,r0,c1,c0,0        //P15为协处理器
             ORR  r0,r0,#0xc0000000
             MCR  p15,0,r0,c1,c0,0
             MOV  PC,LR
MRC为协处理器指令,作用是将协处理器寄存器中的数据传送到ARM处理器的寄存器中。其使用格式为:
MRC{条件} 协处理器编码,协处理器操作码 1,目的寄存器,源寄存器 1,源寄存器 2,协处理器操作码 2
协处理器编码为需要进行操作的协处理器,协处理器操作码1、2为协处理将要进行的操作,目的寄存器为ARM处理器的寄存器,源寄存器1、2为协处理器的寄存器。若协处理器不能完成操作,则会产生未定义指令。
MCR为协处理器指令,作用是将ARM处理器的寄存器中的数据传送到协处理器中。
P15为系统协处理器,它通过协处理器指令MCR和MRC以及相应的寄存器来配置和控制ARM处理器的caches、MMU,从而保护系统、配置时钟模式。

(4)        初始化存储系统
S3C2440A的存储地址空间为0-0x3FFFFFFF,共1G字节,这些地址空间被分为了8个BANK,每个BANK有128M字节的存储空间。
8个BANK的读写位宽可以通过编程来设定,具体要看外挂的存储器的类型,BANK0可以配置为16位或32位,其他BANK可以配置成8位、16位或32位。
8个BANK中前6个BANK可以挂载ROM、SRAM等类型的存储器,后2个可以挂载ROM、SRAM、SDRAM等类型的存储器。
8个BANK中BANK0~BANK6的起始地址都是固定的,BANK7的起始地址可以调整,因为BANK6、BANK7的容量是可以调整的。
程序或系统在运行的过程中需要不断的对存储器进行读写,所以在正式运行程序前需要对存储系统进行初始化。例如Mini2440是从Nand Flash启动的,Nand Flash因为自身的特性,程序是无法在其中运行的,它只能作为断电之后程序和操作系统存储的载体。系统在启动的的过程中需要将存储在Nand Flash中的内容拷贝到SDRAM,程序在运行的过程中也需要不断的对SDRAM进行读取和写入,所以在启动文件中就要包含对SDRAM进行初始化的部分。
存储系统的初始化代码如下所示:
LDR    R0,=BUSINIT                                        ①
             LDR    R1,=BWSCON                                       ②
             LDMIA  R0!, {R2-R8}                                        ③
             STMIA  R1!, {R2-R8}                                        ④
             LDMIA  R0!, {R2-R7}                                        ⑤
             STMIA  R1!, {R2-R7}                                        ⑥
……
LTORG                                                     ⑦
BUSINIT                                                             ⑧
             DCD  B7_BWCON<<28)|(B6_BWCON<<24)|(B5_BWCON<<20)|(B4_BWCON<<16)
|(B3_BWCON<<12)|(B2_BWCON<<8)|(B1_BWCON<<4)    ; BWSCON  寄存器
             DCD  (0<<13)|(0<<11)|(7<<8)|(0<<6)|(0<<4)|(0<<2)|(0<<0)         ; BANKCON0寄存器
             DCD  (0<<13)|(0<<11)|(7<<8)|(0<<6)|(0<<4)|(0<<2)|(0<<0)         ; BANKCON1寄存器   
             DCD  (0<<13)|(0<<11)|(7<<8)|(0<<6)|(0<<4)|(0<<2)|(0<<0)         ; BANKCON2寄存器
             DCD  (0<<13)|(0<<11)|(7<<8)|(0<<6)|(0<<4)|(0<<2)|(0<<0)        ; BANKCON3寄存器
             DCD  (0<<13)|(0<<11)|(7<<8)|(0<<6)|(0<<4)|(0<<2)|(0<<0)         ; BANKCON4寄存器
             DCD  (0<<13)|(0<<11)|(7<<8)|(0<<6)|(0<<4)|(0<<2)|(0<<0)         ; BANKCON5寄存器
             DCD  (3<<15)|(2<<2)|(1<<0)                         ; BANKCON6寄存器(SDRAM)
             DCD  (3<<15)|(2<<2)|(1<<0)                         ; BANKCON7寄存器(NC)
             DCD  (1<<23)|(0<<22)|(2<<20)|(2<<18)|(2<<16)|(1653)   ; REFRESH 寄存器(SDRAM)
             DCD  (1<<5)|(1<<4)|(2<<0)                          ; BANKSIZE寄存器(128MB)
             DCD  (3<<4)                                      ; MRSRB6  寄存器
             DCD  (3<<4)                                      ; MRSRB7  寄存器
现在对上述程序解释一下:(前面的序号为程序代码段后的标号)
①        将存储器配置值的数据表的地址放入寄存器R0;
这时需要着重注意,如果程序编译链接以后大小小于4K,这条伪指令是可以用LDR的。LDR这条伪指令的第二个操作数是绝对地址,也就是地址在链接的时候就确定了,在程序的运行过程中都不会变化。系统从Nand Flash启动时,Nand Flash的前4K会被复制到芯片内部的BootSRAM中。在程序量小于4K的情况下所有的程序都被复制到了BootSRAM中,不会再将代码从Nand Flash搬运到SDRAM,并且所有的程序是在BootSRAM中运行,代码在运行过程中地址不会改变(运行时地址即链接时的地址)。所以可以使用LDR伪指令,这条指令是与地址有关的,使用的时候需要特别小心。
当程序大于4K的时候,程序的加载过程变得稍微复杂,涉及到代码从Nand Flash到SDRAM的搬运,程序运行时的地址已经不是编译链接时确定的地址,所以启动代码中的指令尽量使用与地址无关的,此时这条指令可变为ADR  R0,BUSINIT。具体的关于代码编写所涉及的注意事项大家可以参阅有关书籍,在此只是提醒一下,不再赘述。
②        将总线数据宽度和等待控制寄存器的地址放入寄存器R1,因为寄存器的地址是在S3C2440的头文件中定义好的,不会改变,所以可以使用LDR指令。
③        至⑥ 给一些列的寄存器赋值,一共13个值。
首先设置的是BWSCON寄存器:
BWSCON寄存器:总线宽度、等待状态控制寄存器,用来设置BANK0~7的数据总线的宽度、等待状态及是否启动SDRAM的数据掩码引脚。每个BANK占据4位,具体请参照数据手册有关章节。
BANK0对应的4位只有BWSCON [2:1]是有效的,而且其设置方法也比较特别。BANK0的数据总线的宽度是将OM[1:0]位通过硬件跳线来设置的,因而BWSCON[2:1]是只读的。设置BWSCON寄存器时只要从BWSCON [4]位开始就可以了。
BANK7在Mini2440开发板中是未接的;BANK6挂接的是由两片64M、16位的SDRAM组成的128M、32位带宽的存储器;其他各BANK需要根据开发板的实际挂接情况设置。
有关的设置在汇编的头文件中是按如下定义的:
B1_BWCON        EQU   (DW32)        //0010
B2_BWCON        EQU   (DW16)        //0001
B3_BWCON        EQU   (DW16)        //0001
B4_BWCON        EQU   (DW16)        //0001
B5_BWCON        EQU   (DW16)        //0001
B6_BWCON        EQU   (DW32)        //0010
B7_BWCON        EQU   (DW32)        //0010

DW8                EQU        (0x0)
DW16                EQU        (0x1)
DW32                EQU        (0x2)
WAIT                EQU        (0x1<<2)
UBLB                EQU        (0x1<<3)
接下来设置的寄存器是BANK0~BANK7的控制寄存器,具体的位的作用请参照数据手册,其中BANK0~BANK5不是针对SDRAM存储器的,可以使用默认值0x00000700。BANK6~BANK7可以接SDRAM,所以这两个BANK对应的控制寄存器的值设置成0x00018005。
紧接着设置的是刷新控制寄存器、BANK大小寄存器、BANK6~7的模式寄存器,具体值请见上述的程序代码,具体值需要根据S3C2440和SDRAM的数据手册确定。
⑦        LTORG这个伪指令的作用是声明一个文字池的开始,文字池一般放置于一个代码段的最后面,或者一个文件的END的前面 。文字池的作用是在代码中分配一段存储空间来存放变量。
⑧        BUSINIT,文字池的标号。
在文字池中有一个数据定义伪操作“DCD”,DCD用于分配一片连续的存储单元,并用指定的表达式来对其进行初始化,表达式可以为程序标号或数字表达式,DCD可以用符号“&”来代替。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值