詳解Boot-Loader


上電之后(Boot-Load階段)該做什么


1、第一行程序

拿到空PCB板之后,硬件工程師首先會測試各主要線路是否通連,各焊點是否有空焊、斷接或短路的情況,然后逐個模塊焊接上去。之后需要驗證系統上電之后,CPU與各組件的供電電壓是否正常,供給CPU的震盪電路能否能夠正常起振,外部存儲器能否正常讀寫。當把我們的程序用JTAG工具下載到板子上后,在真正調試系統前需要做好以下檢查:

  • 利用調試工具,在程序的第一行設定斷點,確定程序有停下來;
  • 檢查CPU的程序計數器PC是否正確;
  • 檢查CPU內部RAM的內容和我們下載的可執行文件是否相同;
  • 程序的第一行命令為設定CPU狀態寄存器,並觀察CPU的狀態寄存器是否如預期改變;
  • 繼續單步執行,確認PC寄存器是否會跟着改變,且每行命令的執行結果都是正確的。

檢查完以上各項后,只能證明板子上的電源電路以及CPU是正常的,接下來要繼續驗證CPU與外圍設備,確認板子的正確性與穩定性后,才能進行下一步測試。


2、基本硬件測試

既然Boot-Loader的責任是幫其它程序布置可運行的環境,那么就要做好以下驗證:

  • CPU寄存器(狀態寄存器、通用寄存器、內存映射寄存器)操作測試;
// 設定SP(Stack Point)寄存器
//
asm("xld.w %r15, 0x2000");
asm("ld.w %sp, %r15");

// 設定CPU的狀態寄存器
//
asm("xld.w %r15, 0x200010");
asm("ld.w %psr, %r15");

// 將寄存器0x300023的bit 1設為1
*(volatile unsigned char *)0x300023 |= 0x2;
  • Stack Pointer的設置是否正確?函數調用是否正確運行?
  • 中斷是量表設置是否正確?中斷矢量程序是否正常運行?
  • 存儲器初始化及其操作測試,保證所有的存儲器都可以正常讀寫;
  • 將數據段載入RAM,對bss段設定初值,並將需要在RAM中運行的程序載入到RAM。保證當主程序執行起來后,全局變量的初始值都是正確的。

只有確保以上測試通過后才能進行下一步工作。


(1)確認函數調用能否正常運行

正確設置堆棧(Stack)是函數能否成功調用的前提,在嵌入式系統開發時,系統要自行管理堆棧,如果管理不當,可能會發生函數調用或調用幾層之后就死機的狀況。因為C語言利用堆棧完成以下事項:

  • 存儲函數返回地址;
  • 函數調用時的參數傳遞(參數較多時);
  • 存儲函數內部的局部變量;
  • 中斷服務程序執行時(發生中斷時),存儲CPU當前狀態及返回地址。

堆棧頂點地址(Stack Point)的配置是一件很重要的事,但卻極易被人忽略。主要是在Windows或Linux上編程時,操作系統在產生可執行文件時,linker會自動幫程序加上一段Startup Code,其中就包含了Stack存儲器的配置。但在無操作系統的嵌入式系統中,調用任何函數之前都要先為其設置好堆棧空間(Stack Point)。

當用C語言調用了一個函數,例如fun(a,b),編譯后的機器碼應該包含以下動作:

  • 執行指令push,將參數a和b存入Stack,同時堆棧指針SP減一;
  • 將當前程序計數寄存器PC的值(也即返回地址:函數調用指令的下一條指令地址)存到堆棧中;
  • 執行指令Call,把PC的值設為函數fun()的地址,下一個被執行的指令就是函數的第一條命令。
  • 當函數fun執行時,可利用當前SP的值計算出參數a和b的地址;
  • 如果函數內部有局部變量,則依次將這些變量存到堆棧中。所以在嵌入式開發中盡量不要定義size太大的變量,否則有棧溢出(Stack Overflow)的風險。
  • 當函數執行完畢,CPU會執行ret命令,該命令會從Stack頂層取出返回地址,然后賦值給PC寄存器,則下個指令就會執行函數后面的下一行指令,從而完成函數的調用。

如果SP寄存器沒有設定到正確的地址,或是沒有配置足夠大的存儲區域作為棧空間,那么在調用函數時很可能就會出錯。下圖就是一個棧空間溢出,破壞程序數據段的例子:

局部變量太大導致Stack Overflow

為避免以上情況的發生,一般會選擇某塊RAM 的頂端(最大地址)當作SP寄存器的初值,但具體棧的大小定位多少合適要根據具體軟硬件環境和項目要求。一般采用的方法是,剛開始稍微定義大一點,例如2KB-4KB左右,然后讓測試人員運行完系統所有功能(函數)后,記錄下SP在每次函數調用后的最小值,它與棧頂地址的差就是所需最小棧空間,一般會稍微再放一點。


(2)確認中斷系統能否正常運行

負責寫驅動程序的工程師要將中斷服務程序的地址填入中斷矢量表,並必須保證當驅動程序被執行時,中斷系統是正常的。一般來說主要做好以下工作:

  • 中斷矢量表數組,詳細注解每個entry代表的中斷源;
  • 如果是外接中斷控制器,要先完成中斷控制器的驅動程序,才能開始中斷系統的測試。
  • 設定CPU的中斷矢量表地址寄存器(有些CPU無中斷矢量表地址寄存器,但它會指定某個固定地址為中斷矢量表的地址)
  • 設定CPU的中斷控制寄存器(優先級、中斷允許位等)
  • 確定中斷被觸發后,對應的ISR會被執行。
  • 提供ISR的范例,讓ISR編寫者不用知道中斷系統的細節。
// ISR模板
//
void isr_template(void)
{
    // 將所有通用目的寄存器存到堆棧
    //
    asm("pushn %r15"); /*將r0 - r15 都存到堆棧中 */
    
    //將ALR與AHR寄存器通過r1存到堆棧
    //你無需搞清ALR和AHR是什么寄存器,不同的CPU有不同的寄存器需要存儲
    //
    asm("ld.w %r1, %alr");
    asm("ld.w %r0, %ahr");
    asm("pushn %r1");
    
    //調用C語言函數your_ISR,即真正ISR要處理的事寫在該函數里就行
    //
    asm("xcall your_ISR");
    
    //從堆棧中取回被調用時的ALR和AHR寄存器的值
    //
    asm("popn %r1");
    asm("ld.w %alr, %r1");
    asm("ld.w %ahr, %r0");
    
    //從堆棧中取回r1 - r15的值
    //
    asm("popn %r15");
    
    //執行中斷返回指令,返回被中斷的程序
    //
    asm("reti");
}

在以上各環節中容易出錯的地方有:

  • 中斷優先級寄存器沒設正確;
  • 中斷矢量表中各個entry與中斷源的對應關系錯誤;
  • 中斷矢量表地址設置錯誤,很多CPU會要求中斷矢量表的地址要設置在偶數地址或是4的倍數,甚至是128KB的倍數。

那如何判斷ISR有沒有被正確執行呢?一般的方法是選擇一個簡單的中斷源(例如除0錯誤中斷),在其ISR中設定一個斷點,然后單步執行,看能否順利執行ISR程序及正確返回中斷發生的地方(除零指令的下一條語句)。

(3)存儲器測試

存儲器出問題的地方有:

  • 硬件方面:數據線、地址線連接錯誤;

  • 軟件方面:SRAM、NOR Flash、ROM不需要額外電路,直接可以使用,但SDRAM則還需要額外的SDRAM Controler電路才能使用,程序必須先設定好SDRAM Controler的配置(SDRAM大小、速度等);

  • 外部存儲器的時序設置,若時序設定太快,系統會不穩定,太慢,則系統性能變差。一般CPU的Timing設定表會說明應該如何設定。

  • 在進行下部工作前要先測試存儲器的每一個Byte,確保讀寫(如果可以寫入的話)正常。方法是對每一個字節依次寫入0x00、0xFF、0x55、0xAA,確保每一位都會被寫入0與1。

  • int SRAM_testing(void)
    {
        int i,counter =0;
        //待測RAM起始地址為0x2000000,大小為2MB.
        unsigned char *pointer = (unsigned char *)0x2000000;
        unsigned char data[4]={0x00,0xFF,0x55,0xAA};
        
        for(i=0; i<4; i++)
        {    // 逐一對每個字節寫入某特殊值
            for(j=0; j<(8*1024*1024); j++)
                pointer[i] = data[i] 
             // 逐一讀出每個字節,判斷寫入的值是否正確 
            for(j=0; j<(8*1024*1024); j++)
                pointer[i]==data[i]?::counter++;
        }      
        return counter; //返回出錯字節的個數 
    }
    
  • 對於只讀ROM,如何驗證燒錄到存儲器中的數據和原始映像文件一致呢?一般會采用校驗和檢驗法。即分別計算原始映像文件和燒錄到ROM中文件的校驗和是否相等。

  • /*************************************************************** Function Name: calculate_ROM_checksum Function Purpuse:計算起始地址為0x2000000,size為8MB存儲器的校驗和 ****************************************************************/
    unsigned long calculate_ROM_checksum(void)
    {
        unsigned long checksum = 0;
        unsigned char *pointer = 0x2000000;
        for(i=0; i<(8*1024*1024); i++)
            checksum += pointer[i];
        return checksum;
    }
    

(4)CPU初始化

在Boot-Loader階段因該做好以下CPU相關的設定:

  • 設定堆棧指針寄存器SP;
  • 設定狀態寄存器,禁止中斷;
  • 設定中斷矢量表指針;
  • 設定CPU執行狀態(時鍾時序);
  • 設定存儲器控制器(如果用到了類似SDRAM的存儲器);
  • 設定CPU操作各存儲器的時序;
  • 設定CPU的PIN腳功能;
  • 初始化外圍設備(LCD Controler、USB Controler、SD卡接口等)

3、載入程序段與數據初始化

(1)載入data段

有初值的全局變量必須被存儲在可執行文件中、被燒錄到ROM里。但執行時因為這些全局變量的值會被改變,所以當然不能在ROM里運行,連接時必須尋址到RAM中。正因為這種 “存儲在ROM,運行在RAM” 的特性,才有傳輸data段的需要,且必須在所有程序使用全局變量前完成這些事。

執行時期的存儲器使用狀況

上圖中,data段的內容原本在可執行文件中的rodata段之后,但執行時,需要將data段復制到RAM中的bss段之后。連接腳本如下:

.data __END_bss : AT(__END_rodata)
{
    __START_data = .;
    *(.data);
    __END_data = .;
    
    // 定義可在程序中使用的變量“__START_data_LMA”,表示data段的存儲起始地址LMA
    __START_data_LMA = LOADADDR(.data);
    
    //定義可在程序中使用的變量“__SIZE_DATA”,表示data段的大小
    __SIZE_DATA = __END_data - __START_data;
}

傳輸程序如下:

/************************************************** Function Name: copy_data_section() Function Purpuse:將可執行文件中的數據段復制到內存中 ***************************************************/
extern unsigned long *__START_data;
extern unsigned long *__START_data_LMA;
extern int __SIZE_DATA;

void copy_data_section(void)
{
   int i;
   unsigned long *dest = __START_data;
   unsigned long *src = __START_data_LMA;
   //假設data段的大小是4的整數倍個字節
   for(i=0; i<(__SIZE_DATA/4); i++)
       dest[i] = src[i];    
}

(2)設定bss段

bss段的設定較為簡單,因為bss段里的成員都是沒有初始值的全局變量,所有根本不需要存儲空間,在執行時只要把bss段的執行空間(VMA)都設為0即可。

/******************************************* 定義bss段,起始地址(VMA)從0開始 ******************************************/
.bss 0x0 : 
{
    __START_bss = .;
    *(.bss);
    __END_bss = .;
    
    //定義可在程序中使用的變量:__SIZE_BSS
    __SIZE_BSS = __END_bss - __START_bss;
}

設定bss段為0的代碼如下:

/************************************************** Function Name: clear_bss_section() Function Purpuse:將bss段清零 ***************************************************/
extern unsigned long * __START_bss;
extern int __START_BSS;

void clear_bss_section(void)
{
    int i;
    unsigned long * dest = __START_bss;
    //假設bss段的大小為4的整數倍字節大小
    for(i=0; i<(__SIZE_BSS/4); i++)
        dest[i] = 0;
}

Attention:在boot階段,data段和bss段一定要先設定,否則執行期間全局變量的值就不正確。換句話說,在設定完data和bss段之前,boot-load程序是不能使用全局變量的,如果一定要使用,那就避免在定義全局變量時賦值,一定要在程序內明確賦值才行。例如:

Boot-Loader程序使用全局變量時必須謹慎

(3)載入text段

當某個系統程序或者應用程序模塊需要較高的執行速度時,往往可以將他們復制到系統內存中執行。但系統內存往往空間有限,不可能同時全部加載進去。所以我們一般會寫一個函數,並尋址到同一個地址,在需要時才做載入的動作。

各種類型的存儲器性能由大至小分別為:CPU寄存器、CPU cache、CPU內部RAM、外部SRAM、NOR Flash、SDRAM、Mask ROM、NAND Flash。

NAND Flash:價格低,容量大,可把其想象成類似硬盤的設備,只不過無法直接尋址操作,程序無法再上面直接執行;

NOR Flash:價格高,容量小,但讀數據快,可把其想象成可重復寫的ROM,程序可在上面直接運行。

Mask ROM:成本高,容量有限,但程序可直接在上面運行;

SDRAM:性價比高,一般作為系統的外置內存,程序可直接在上面運行;

SRAM:價格昂貴,容量小,一般作為系統的內置內存,程序可在上面直接運行。

(4)幾種系統存儲器架構

  • 從NAND Flash啟動的架構:

  • image-20201125230828425

  • 啟動流程為

    • 上電后,CPU內置程序會從NAND Flash的特定地址(一般是第一個block塊地址)讀出Boot-Loader程序到CPU的內部內存中。
    • CPU將控制權交給內部存儲器中的Boot-Loader;
    • Boot-Loader初始化SDRAM,再從NAND Flash中將主程序載入到SDRAM中;
    • Boot-Loader將控制權交給主程序。

    獲取更多知識,請點擊關注:
    嵌入式Linux&ARM
    CSDN博客
    簡書博客
    oader程序到CPU的內部內存中。

    • CPU將控制權交給內部存儲器中的Boot-Loader;
    • Boot-Loader初始化SDRAM,再從NAND Flash中將主程序載入到SDRAM中;
    • Boot-Loader將控制權交給主程序。

    獲取更多知識,請點擊關注:
    嵌入式Linux&ARM
    CSDN博客
    簡書博客
    知乎專欄


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM