[轉載]自己動手編寫嵌入式Bootloader之(1)

第一部分:基本功能流程

CPU上電後會從IO空間的某地址取第一條指令。但此時:PLL沒有啓動,CPU工作頻率爲外部輸入晶振頻率,非常低;CPU工作模式、中斷設置等不確定;存儲空間的各個BANK(包括內存)都沒有驅動,內存不能使用。在這種情況下必須在第一條指令處做一些初始化工作,這段初始化程序與操作系統獨立分開,稱之爲bootloader。

實際上,很少有必要自己寫一個Bootloader,因爲U-Boot已經強大到能夠滿足各種需要。但是強大必然複雜,一個初學者想要分析U-Boot的源代碼,還是有些難度的。出於學習的目的,我寫了這個史上最簡單的啓動加載器,它只包含最基本的功能,卻囊括了一個嵌入式Bootloader應該有的核心和精華。我把這個啓動加載器命名爲S-Boot, 是Simple Bootloader的縮寫,亦可進一步簡稱爲SB。

使用的實驗環境爲OK2440開發板,板上處理器爲S3C2440A,有64M內存,Nand存儲器爲K9F1208,64M。網口芯片爲CS8900A。我們要實現的功能是:從串口下載Linux內核映像到RAM;從網口下載Linux內核映像到RAM;從RAM啓動內核掛載NFS根文件系統。

1. 第一階段的彙編代碼:start.S

一個嵌入式Bootloader最初始部分的代碼幾乎必須是用彙編語言寫成的,因爲開發板剛上電後沒有準備好C程序運行環境,比如堆棧指針SP沒有指到正確的位置。彙編代碼應該完成最原始的硬件設備初始化,並準備好C運行環境,這樣後面的功能就可以用C語言來寫了。

對我們的S-Boot來說,上電後的起始運行代碼是 start/start.S。

.text
.global _start
_start:
b Reset @ 0x00: 發生復位異常時從地址零處開始運行
    b HandleUndef @ 0x04: 未定義指令中止模式的向量地址
    b HandleSWI   @ 0x08: 管理模式的向量地址,通過SWI指令進入此模式
    b HandlePrefetchAbort @ 0x0C: 指令預取終止導致的異常的向量地址
    b HandleDataAbort     @ 0x10: 數據訪問終止導致的異常的向量地址
    b HandleNotUsed       @ 0x14: 保留
    b HandleIRQ           @ 0x18: 中斷模式的向量地址
    b HandleFIQ           @ 0x1C: 快中斷模式的向量地址
 
這裏,彙編指示符.text表明以下內容屬於代碼段,.global _start指明_start是全局可訪問的符號(Give the symbol external linkage)。按照ARM920T的規定,從地址0x00到0x1C放置異常向量表,向量表每個條目佔四個字節,正好可以放置一條跳轉指令,跳轉到相應異常的服務程序中去。在S-Boot中沒有使用中斷,所以除Reset異常外,其它異常的服務程序都可簡單地寫個死循環。Reset異常是系統上電後自動觸發的,所以我們的代碼都寫在Reset的服務程序裏面。
 
實際上,異常向量表不一定非要位於地址0x00處,CP15協處理器中的c1寄存器的第13位用來控制異常向量表的起始地址。該位爲0時,異常向量表位於低地址0x00處;該位爲1時,異常向量表位於高地址 0xFFFF0000處。我們沒有必要改變這個位的值,使用默認的低地址就行了。
 
Reset:
        mrs r0,cpsr       @set cpu to SVC32 mode
        bic r0,r0,#0x1F
        orr r0,r0,#0xD3
        msr cpsr,r0       @cpsr=11x10011, IRQ/FIQ disabled
 
代碼最初始的任務是設置CPU工作在SVC32模式,關閉所有中斷,禁用看門狗。實際上,即使不設置工作模式,CPU在復位之後將自動工作在管理模式。在整個S-Boot運行期間,我們沒有使用中斷,也沒有改變CPU工作模式,它將一直工作在SVC32模式。
 
MMU、ICache、DCache的打開和關閉都是由CP15協處理器的c1寄存器控制的。實際上在復位之後這三者都是自動關閉的,所以省略了關閉它們的代碼。


 
S3C2440A的PSR寄存器(Program Status Reguster)中每個Bit位的含義如圖1所示。Bit4~Bit0爲模式位,用來設置CPU工作模式,現在只要知道 M[4:0] = 10011 表示SVC32模式就行了。Bit5爲狀態位,T=0表示工作在ARM狀態,T=1表示工作在Thumb狀態,默認爲0,不需要改變。Bit6爲快速中斷禁止位,F=1爲禁止快速中斷,F=0爲使能快速中斷。Bit7爲中斷禁止位,I=1爲禁止中斷,F=0爲使能中斷。其它Bit位暫時可以不必理會。

mrs 和msr是在PSR寄存器和其它寄存器間傳遞數據的指令。如:mrs r0,cpsr 把cpsr的值傳送到r0中, msr cpsr,r0 把r0的值傳送到cpsr中。bic是位清零(Bit Clear)指令,bic r0,r0,#0x1F 意思是把r0的Bit[4:0]位清零(由0x1F指示),然後把結果寫入r0中。 orr是按位求或指令,orr r0,r0,#0xD3 表示把r0的 Bit7,Bit6,Bit4,Bit1,Bit0 置爲1,其它位保持不變。

執行完上述操作後,cpsr中的 I=1, F=1, T保持不變(默認爲0),M[4:0]=10011,意思是禁止IRQ,禁止FIQ,工作在ARM狀態,工作在SVC32模式。
 
 
        ldr r0, =0x53000000
        mov r1, #0x0
        str r1, [r0] @disable watch dog
 
禁用看門狗更簡單,因爲WTCON寄存器的地址爲0x53000000,直接向該寄存器寫0即可。

到目前爲止,CPU工作在外接晶振12MHz頻率之下。使用以下代碼設置PLL,提升工作頻率。
 
 
        ldr r0, =0x4C000014 @CLKDIVN register

        mov r1, #0x05 @FCLK:HCLK:PCLK = 1:4:8
        str r1, [r0]

        mrc p15,0,r0,c1,c0,0 @if HDIVN Not 0, must asynchronous bus mode
        orr r0,r0,#0xC0000000 @see S3C2440A manual P7-9
        mcr p15,0,r0,c1,c0,0

        ldr r0, =0x4C000004 @MPLLCON register
        ldr r1, =0x0005C011 @((92<<12)|(1<<4)|(1))
        str r1, [r0] @FCLK is 400 MHz !
 
最後的結果是,FCLK=400MHz,HCLK=100MHz,PCLK=50MHz。
 
        @ SDRAM Init
        mov r1, #0x48000000 @MEM_CTL_BASE
        adrl r2, mem_cfg_val
        add r3, r1, #52
1:
        ldr r4, [r2], #4 @ 讀取設置值,並讓r2加4
        str r4, [r1], #4 @ 將此值寫入寄存器,並讓r1加4
        cmp r1, r3 @ 判斷是否設置完所有13個寄存器
        bne 1b @ 若沒有寫成,繼續
 
設置存儲控制器。
 
        ldr sp, =0x32FFF000 @設置堆棧
        bl nand_init @初始化NAND Flash
                                            @nand_read_ll函數需要3個參數:
        ldr r0, =0x33000000 @1. 目標地址=0x30000000,這是SDRAM的起始地址
        mov r1, #0 @2. 源地址 =0,S-Boot代碼都存在NAND地址0開始處
        mov r2, #102400 @3. 複製長度=102400(bytes)
        bl nand_read @調用C函數nand_read


        ldr lr, =halt_loop @設置返回地址
        ldr pc, =main @b指令和bl指令只能前後跳轉32M的範圍,故使用向pc賦值的方法進行跳轉

halt_loop:
        b halt_loop
 
這裏把所有的代碼從Nand拷貝到RAM中,然後跳轉到main函數去執行。此後程序便在RAM中運行了。但是到目前爲止,前面的程序都是在SteppingStone裏運行的。所謂SteppingStone,是指S3C2440A的內部的4KB的RAM緩存,它總是映射到地址0x00處。硬件加電後會自動將Nand Flash中的前4KB的數據拷貝到Stepping Stone中,然後從地址0x00處開始運行

如果代碼足夠小(小於4KB)的話,那隻在SteppingStone中運行,加載Linux內核到內存即可。但通常代碼肯定會大於4KB。所以Bootloader一般分爲兩部分,Stage1的代碼在SteppingStone中運行,它會把Stage2的代碼拷貝到RAM中,並跳轉到RAM中執行;Stage2的代碼在RAM中執行,它可以完成加載內核及其它任何複雜的功能。因爲Stage2的起始位置不好確定,爲了方便,我們把所有的代碼都拷貝到RAM中了。

C 函數nand_read有三個參數,第一個參數爲目的地起始地址,第二個參數爲源起始地址,第三個參數爲要複製的數據長度,以字節爲單位。根據ATPCS 函數調用規則,三個參數分別用寄存器r0,r1,r2來傳遞。我們在內存的0x33000000處存放Bootloader,複製長度根據編譯生成的S- Boot.bin映像文件大小,向上取512字節的整數倍。

這裏先來規劃一下內存空間的分配。RAM的地址範圍是從0x30000000到0x34000000共64MByte。把S-Boot和Kernel放在高地址處,S-Boot從 0x33000000開始,預留8MByte的空間,內核從0x33800000開始,可供使用的空間也是8MByte。因棧空間是向下生長的,我們在 S-Boot下面預留4096Byte的空閒區域,然後向下爲棧空間,故棧指針SP初始化爲 0x32FFF000。其實留不留空閒區域是無所謂的,這裏只是爲了把二者更明顯地區分開。我們只設置SVC模式下的SP,不使用CPU的其它工作模式,所以也沒必要設置其它模式下的棧指針。另外,程序中不使用動態內存分配,故而也不必分配堆空間。
 
 
 

2. nand讀操作

在編譯連接時,我們把上述 start.S 代碼放在生成的二進制映像文件的最開始位置,因而也被燒寫到 Nand Flash 的最起始位置,因而會被自動拷貝到 SteppingStone 裏運行。start.S 要完成的任務之一,是把S-Boot的所有代碼從Nand Flash拷貝到內存中,這裏需要對NAND的讀操作,因此對NAND的初始化和讀操作要在第一階段寫好。

以開發板上使用的K9F1208爲例,每個頁(page)爲512Byte數據和16Byte校驗,每個塊(Block)爲32個頁,即16KByte數據和512Byte校驗。
 
Nand Flash只用8根線與CPU的DATA0-7連接,位寬爲8位,不管是數據、地址或控制字都通過這8根線傳遞,如果讀寫數據的話每次只能傳輸一個字節數據。Nand Flash的操作通過NFCONF、NFCMD、NFADDR、NFDATA、NFSTAT和NFECC六個寄存器來完成。在S3C2440A數據手冊第218頁可以看到讀寫Nand Flash的操作時序:1. 通過NFCONF寄存器配置Nand Flash;2.寫Nand Flash命令到NFCMD寄存器;3.寫Nand Flash地址到 NFADDR寄存器;4在讀寫數據時,通過NFSTAT寄存器獲得Nand Flash的狀態信息。應該在讀操作前或寫操作後檢查R/nB信號(Ready/Busy信號)。

初始化NAND Flash:S3C2440的NFCONF寄存器用來設置時序參數TACLS、TWRPH0、TWRPH1,設置數據位寬;還有一些只讀位。TACLS、 TWRPH0、TWRPH1這三個參數控制的是Nand Flash信號線CLE/ALE與寫控制信號nWE的時序關係。
 
 
注意,寄存器值轉換成實際的時鐘週期值時,TACLS不需加1,而TWRPH0和TWRPH1需要加1。比如NFCONF寄存器中設置 TACLS=1,TWRPH0=3,TWRPH1=0,意思是時序圖中 TACLS=1個HCLK時鐘,TWRPH0=4個HCLK時鐘,TWRPH1=1個HCLK時鐘。
 
 
void nand_init(void)
{
    //時間參數設爲:TACLS=0 TWRPH0=3 TWRPH1=0
    NFCONF = 0x300;
    /* 使能NAND Flash控制器, 初始化ECC, 禁止片選 */
    NFCONT = (1<<4)|(1<<1)|(1<<0);
    /* 復位NAND Flash */
    NFCONT &= ~(1<<1); //發出片選信號
    NFCMMD = 0xFF; //復位命令
    s3c2440_wait_idle();//循環查詢NFSTAT位0,直到它等於1
    NFCONT |= 0x2; //取消片選信號
}

讀操作:讀操作也是以頁(512Byte)爲單位進行的。在初始上電時,器件進入缺省的“讀方式1模式”。在這一模式下,頁讀操作通過將0x00寫入指令寄存器,接着寫入3個地址(1個列地址和2個行地址)來啓動。一旦頁讀指令被器件鎖存,下面的頁讀操作就不需要再重複寫入頁讀指令了。寫入頁讀指令和地址後,處理器可以通過對信號線R//B的分析來判斷頁讀操作是否完成。如果信號爲低電平,表示器件正忙;如果信號爲高電平,表示器件內部操作完成,要讀取的數據被送入了數據寄存器。外部控制器可以再以50ns爲週期的連續/RE脈衝信號的控制下,從IO口依次讀出數據。連續頁讀操作中,輸出的數據是從指定的列地址開始,直到該頁最後一個列地址的數據爲止。
 
     for(i=start_addr; i < (start_addr + size);)
     {
         NFCMMD = 0; //發出READ0命令
         s3c2440_write_addr(i); //Write Address
         s3c2440_wait_idle(); //循環查詢NFSTAT位0,直到它等於1
         for(j=0; j < NAND_SECTOR_SIZE; j++, i++)
         {
             *buf = (unsigned char)NFDATA;
             buf++;
         }
     }
 
缺點:沒有使用ECC校驗和糾錯;沒有使用壞塊檢查;

3. main 函數

串口初始化,以便能夠向用戶輸出一些信息;網口初始化,以便能夠從主機下載內核映像;輸出一些菜單,以便用戶選擇執行所需要的功能。比如,用戶可以選擇從串口或網口下載內核映像到RAM中某個地址,然後運行這個內核。關於下載內核映像的實現,在後文會詳細介紹。這裏只看當內核映像已經存在於RAM中時,怎樣才能把這個內核啓動起來。

4. 啓動參數的傳遞

 

啓動Linux內核之前需要設置好一些必要的啓動參數,這些參數以TAG列表的形式傳遞給內核。所謂TAG列表,就是多個TAG在內存空間中按順序排列。每個TAG,其實都是一個結構體,每個結構體中又包含了一個頭部結構體和一個內容結構體稱。頭部結構體指明瞭本TAG的類型、佔用空間大小;所謂TAG的類型,就是一個宏定義,用一個確定的整數來識別該標記。內容結構體包含了該TAG的具體內容。

下面以具體的例子做說明。

在atag.h中就有:

 

#define ATAG_CORE 0x54410001
#define ATAG_MEM 0x54410002
#define ATAG_CMDLINE 0x54410009
#define ATAG_NONE 0x00000000

 

這些都是TAG的類型,注意這些整數跟地址沒有關係,只是一個用來識別標記類型的符號而已。

每個Tag都用結構體表示,包含TagHeader 頭結構體以及隨後的參數值數據結構。如 ATAG_CORE

struct Atag {

             struct TagHeader  stHdr;

             struct TagCore stCore;

};

其中包含兩個結構體。第一個結構體TagHeader含兩個整型變量,用以表示本結構體的長度、標記類型;nSzie賦值爲頭部TagHeader和數據TagCore的大小之和,注意是以字(即4字節)爲單位;ulTag 就賦值爲先前定義的宏ATAG_CORE。第二個結構體就是實際的數據了。

struct TagHeader {
UINT32 nSize;
UINT32 ulTag;
};

struct TagCore {
UINT32 ulFlags;
UINT32 nPageSize;
UINT32 ulRootDev;
};


由於每個Tag都由一個TagHeader加一個數據部分組成,因此通常的做法是使用StructUnion相結合來定義:

struct Atag {
           struct TagHeader stHdr;
           union {
                  struct TagCore stCore;
                  struct TagMem32 stMem;
                  struct TagVideoText stVideoText;
                  struct TagRamDisk stRamDisk;
                  struct TagInitrd stInitRd;
                  struct TagSerialnr stSerialNr;
                  struct TagRevision stRevision;
                  struct TagVideolfb stVideoLfb;
                  struct TagCmdline stCmdLine;
           };
};


其中涉及到的所有數據結構均可在 Linux 內核源碼的include/asm/setup.h 頭文件找到,我們把這些定義放在Bootloader的頭文件atag.h中。



啓動參數標記列表以標記 ATAG_CORE 開始,以標記 ATAG_NONE 結束。每個標記由標識被傳遞參數的 tag_header 結構以及隨後的參數值數據結構來組成。數據結構 tag 和 tag_header 定義在 Linux 內核源碼的include/asm/setup.h 頭文件中,在我們的S-Boot中對應的頭文件爲 atag.h。

在嵌入式 Linux 系統中,通常需要由 Boot Loader 設置的常見啓動參數有:ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。

向內核傳遞參數的方法,先在內存中某個起始地址開始,連續存放多個
Tag, 組成Tag列表。列表中的每個Tag包括頭部TagHeader和數據結構體。按規定,第一個Tag必須是ATAG_CORE 最末一個Tag必須是ATAG_NONE,而且中間必須包含至少一個ATAG_MEM 注意的是末尾的ATAG_NONE只包括頭部,沒有數據內容。如圖所示。

在編程時先定義好起始地址,然後用一個指針,每設置完畢一個Tag的內容就向後移動相應的長度,然後設置下一個Tag內容,以保證各個Tag的連續存放。

下面具體說明幾個關鍵Tag的數據區域內容的設置。struct TagCore結構體已經在前面列出,它包含三個整型變量,ulFlags一般設爲零,nPageSize表示分頁內存管理中每一頁的大小,一般爲4096字節,ulRootDev是系統啓動的設備號,設爲零即可,因爲通常在後面的命令行參數Cmdline中覆蓋這個設置。Struct TagMem用來描述系統的物理內存地址空間,定義如下:

 

struct atag_mem {
            UINT32 nSize; /* size of the area */
            UINT32 ulStart; /* physical start address */
};

 

其中nSzie表示內存的總大小,ulStart爲內存的起始物理地址,二者結合告訴內核系統可用的物理內存空間是哪些。Struct TagCmdline結構體的定義就更簡單了,只是一個字符數組,初始長度爲1,如下所示:

struct TagCmdline {
           char cCmdLine[1]; /* this is the minimum size */
};

 

實際上命令行參數不可能只有一個字節,我們通常使用strcpy函數把命令行參數拷貝到cCmdLine地址處,在結尾附加一個字符串結束符’/0’,然後用strlen函數獲得cCmdLine數組的實際長度(包括字符串結束符)。常見的命令行參數如:root=/dev/mtdblock2 init=/linuxrc console=ttySAC0,115200 mem=65536。我們知道的是,Bootloader以標記列表的形式向內核傳遞的參數,大概有10種不同類型的Tag,而命令行參數只是其中的一種。其它需要設置的Tag包括ATAG_RAMDISKATAG_INITRD等,此處不再詳細介紹。

 

在我們的S-Boot中設置了ATAG_CORE,ATAG_MEM,ATAG_CMDLINE,ATAG_NONE 四項。其中CmdLine 使用的是:

const char *CmdLine = "root=/dev/nfs nfsroot=192.168.1.249:/home/hongwang/mkrootfs/rootfs ip=192.168.1.252:192.168.1.249:192.168.1.1:255.255.255.0:hwlee.net:eth0:off console=ttySAC0,115200 init=/linuxrc mem=65536K console=tty1 fbcon=rotate:2";

這裏root=/dev/nfs表示使用NFS做根文件系統,注意並不真的存在/dev/nfs這個設備,它只是一個符號而已,告訴內核使用NFS而不是使用真正的設備做根文件系統。

nfsroot=[<server-ip>:]<root-dir>[,<nfs-options>]

nfsroot=192.168.1.249:/home/hongwang/mkrootfs/rootfs是NFS服務器地址及要掛載的目錄。

ip=<client-ip>:<server-ip>:<gw-ip>:<netmask>:<hostname>:<device>:<autoconf>

ip=192.168.1.252:192.168.1.249:192.168.1.1:255.255.255.0:hwlee.net:eth0:off

只說明一下autoconf,這一個選項指明開發板使用的自動配置IP地址的方法,有時開發板可以設置成通過DHCP或者BOOTP等協議從服務器獲取IP地址。off 或 none 表示不使用自動配置,使用指定的靜態IP地址信息。

console=ttySAC0,115200 串口控制檯

console=tty1 fbcon=rotate:2 液晶屏Framebuffer控制檯,如果內核支持,可以在LCD屏幕上顯示Linux內核啓動過程,起點結束後在LCD屏幕上進入Shell控制檯供用戶操作。fbcon=rotate:2表示控制檯旋轉180度,若爲1表示旋轉90度,3旋轉270度,0不旋轉。

 

4. boot kernel zImage

zImage 二進制文件包含兩部分內容,起始部分是解壓縮程序,後面是壓縮的內核。解壓縮程序是最先運行的,內核中文件是:arch/arm/boot /compressed/head.S,它負責把壓縮的內核解壓到0x30008000處。因此zImage可以下載到RAM任意位置處,由解壓縮程序負責搬移到正確的運行地址。

所以 Bootloader啓動Linux內核的方法就是直接跳轉到內核的第一條指令處,也就是跳轉到內存中存放內核映像的開始地址,內核映像具有自解壓功能,會把自己釋放到正確的運行地址。Tag列表怎樣傳給內核呢?使用的方法是把Tag列表的起始地址傳給內核。首先,定義一個指向函數的指針:

 typedef void (*LINUX_KERNEL_ENTRY)(int, int, UINT32);

LINUX_KERNEL_ENTRY pfExecKernel;

 這樣pfExecKernel就是一個函數指針,函數具有三個整型變量。然後,讓pfExecKernel指向內核映像的起始地址處,這裏使用強制類型轉換把地址轉換成函數指針類型:

 pfExecKernel = (LINUX_KERNEL_ENTRY)pKernelStartAddr;

 最後,以三個參數調用pfExecKernel函數:

 pfExecKernel(0, MACH_ID, ATAG_BASE);

 其中第一個參數默認爲零,可以不必理會。第二個參數是機器ID號,不同的CPU有不同的號碼與之對應,可以在內核源代碼的linux/arch/arm/tools/mach-types 文件中查到,S3C2440 對應的MACH_ID 362。第三個參數ATAG_BASE就是上文講到的Tag列表的首地址。


這個函數調用的作用其實就是設置 r0=0,r1=機器ID,r2=TAG首地址,然後跳到
arch/arm/boot/compressed/head.S文件中的第一條指令處。

既然可以把TAG首地址傳遞給內核,那麼TAG LIST就可以放在RAM中的任何位置了,只要不與其它有用內容衝突即可。但是事實卻並不是想象的這樣。實驗發現,第三個參數傳遞進去的TAG首地址似乎沒有起到作用,因爲啓動時總是找不到正確的啓動參數。後來發現內核有個默認的TAG首地址0x30000100,它總是到0x30000100去尋找啓動參數,而不理會我們傳進來的第三個參數。所以,S-Boot中把TAG首地址就設置爲0x30000100。

5. 小結

綜上所述,包含最基本功能的S-Boot運行流程已經很清楚了。下圖對此作了一個總結。

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