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可以用符號“&”來代替。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章