1. 程式人生 > >關於ARM CM3的啟動文件分析

關於ARM CM3的啟動文件分析

缺省 清零 傳遞 退出 blog 我們 處理程序 示意圖 text

下面以ARM Cortex_M3裸核的啟動代碼為例,做一下簡單的分析。首先,在啟動文件中完成了三項工作:

    1、 堆棧以及堆的初始化

    2、 定位中斷向量表

    3、 調用Reset Handler。

  在介紹之前,我們先了解一下ARM芯片啟動文件中涉及到的一些匯編指令的用法。

技術分享圖片

  補充一下,其中DCD相當於C語言當中的&,定義地址。

1堆棧以及堆的初始化

1.1 堆棧的初始化

技術分享圖片

Startup_xxx.s中的堆棧初始化代碼

  Stack_Size EQU 0x00000400,這個語句相當於Stack_Size這個標號(標號:鏈接器的術語,下文中提到的所有“標號”,指的都是指的鏈接器中的標號)等於0x00000400相當於C語言中的#define Stack_Size 0x00000400 ,也就是說此語句只是一個聲明,並未分配地址。

  AREA STACK, NOINIT, READWRITE, ALIGN=3,此語句定義了一個叫STACK的代碼段,並指明8字節對齊(ALIGN = 3)。其中NOINIT表示未初始化,READWRITE表示可讀可寫,ALIGN = 3,即表示2^3 = 8,八字節對齊。

  Stack_Mem SPACE Stack_Size,為Stack_Mem分配Stack_Size大小的一塊內存區域,註意這裏分配的是RAM,即分配了大小為1KB的內存空間(0x00000400 = 1024)。

  __initial_sp ,緊跟著棧分配內存後,所以其為棧頂(滿遞減棧)。此標號有一層隱含的意思就是在M3中堆棧是滿遞減堆棧,因為它指定了堆棧指針位於堆棧的高地址(在Stack_Mem之後),具體如下圖所示。

技術分享圖片

堆棧指針sp位置

  上圖來自Cortex_M3的一個工程的xxx.map文件。可以看出棧的起始地址為0x20000c68,大小為1024字節(即0x00000400 = Stack_Size)。而堆棧指針的位置在0x20001068,其等於棧的起始地址0x2000c68+0x00000400,說明本系列的Cortex_M3微控制器的堆棧為滿遞減堆棧。

  所以__initial_sp為1KB空間棧的棧頂,棧主要用於局部變量和形參的調用過程的臨時存儲,屬於編譯器自動分配和釋放的內存,所以這裏需要註意如果你的函數所占的內存過大,那麽這個空間應調整其大小但一定要小於內部SRAM的大小。堆是程序員空間是程序員進行分配和釋放的,如果程序中未釋放最後由系統回收。

1.2 堆的初始化

技術分享圖片

Startup_xxx.s中的堆初始化代碼

  堆的初始化過程與堆棧的初始化相同。

2、中斷向量表的初始化

技術分享圖片

中斷向量表的初始化代碼(部分)

PRESERVE8指定了以下的代碼為8字節對齊,這是keil編譯器的一個編程要求,對齊情況如下圖所示:

技術分享圖片

xxx.list文件中的8字節對齊示意圖

  THUMB指定了接下來的代碼為THUMB指令集。

  AREA RESET, DATA, READONLY,此語句聲明RESET數據段。

  EXPORT __Vectors,導出向量表標號,EXPORT作用類似於C語言中的extern。之後的代碼就是為向量表分配存儲區域。中斷向量表從FLASH的0x00000000地址開始放置,以4個字節為一個單位,地址0存放的是棧頂指針(sp)的地址,0x00000004存放的是復位程序的地址,往後以此類推,這裏我們只設置了一個Reset_Handler向量。從代碼上看,向量表中存放的都是中斷服務函數的函數名,可我們知道C語言中的函數名就是一個地址。(由此我們知道,中斷函數的函數名都已經知道了,我們在寫對應的中斷服務程序時,從對應的地址取服務例程的入口地址並跳入執行)。但是此處有一個要註意的,就是0號地址不是什麽入口地址,而是給出的復位後的MSP的初值。

3、調用Reset Handler

技術分享圖片

調用Reset Handler的代碼

  此段代碼只完成了一個功能,引導程序進入__main。__main的具體行為在後面做具體描述。

  PROC與ENDP組合在匯編中定義了一段子函數。

用戶堆棧的初始化

技術分享圖片

具體的堆棧以及堆的初始化行為

  這一部分也就是把初始化的堆棧地址賦值給單片機的對應寄存器以方便C程序進行分配釋放使用。

4、其他代碼

有一些芯片廠商對芯片的加密的加密級別的代碼也會放在這裏,芯片上電後會自動讀取這一地址的值以確定芯片的加密方式。

5ARM芯片的啟動過程詳解

  接下來介紹__main函數的具體實現過程。

  首先在介紹__main函數之前,我們先了解一些關於ARM芯片在啟動過程中的基本知識。

“ARM程序”是指在ARM系統中正在執行的程序,而非保存在ROM中的.bin(.axf,.hex)映像(image)文件。

一個ARM程序包含3部分:RO,RW和ZI

RO 就是只讀數據,是程序中指令和常量;

RW是可讀寫的數據,程序中已初始化變量;

ZI 是程序中未初始化的變量和初始化為0的變量。

簡單理解就是:

RO就是readonly,RW就是read/write,ZI就是zero initial。

技術分享圖片

ARM芯片的啟動過程詳解

註意,以上的過程並非絕對的,不同的ARM架構或者是不同的代碼以上的執行過程是不同的。

復位處理程序是在匯編器中編寫的短模塊,系統一啟動就立即執行。復位處理程序最少要為應用程序的運行模式初始化堆棧指針。對於具有本地內存系統(如緩存、TCM、MMU和MPU)的處理器,某些配置必須在初始化過程的這一階段完成。復位處理程序在執行之後,通常跳到__main以開始C庫初始化序列。

__main中的__scatterload負責設置內存,而__rt_entry負責設置運行時的環境。__scatterload中負責把RO/RW(非零)輸出段從裝載域地址復制到運行域地址(執行代碼和數據復制、解壓縮),並完成ZI段運行域數據的0初始化工作。然後跳到__rt_entry設置堆棧和堆、初始化庫函數和靜態數據。然後,__rt_entry跳轉到應用程序的入口main()。主應用程序結束執行後,__rt_entry將庫關閉,然後把控制權交換給調試器。函數標簽main()具有特殊含義。Main()函數的存在強制鏈接器鏈接到__main和__rt_entry中的代碼。如果沒有標記為main()的函數,則沒有鏈接到初始化序列,因而部分標準C庫功能得不到支持。

6、結合代碼來看芯片啟動過程

上電後硬件設置sp、pc,剛上電復位後,硬件會自動根據向量表地址找到向量表。

技術分享圖片技術分享圖片

  在離開復位狀態後, CM3 做的第一件事就是讀取下列兩個 32 位整數的值:

    1、從地址 0x0000 0000 處取出 MSP 的初始值。

    2、從地址 0x0000 0004 處取出 PC 的初始值,這個值是復位向量, LSB 必須是 1。 然後從這個值所對應的地址處取指。

  硬件自動從0x0000 0000位置處讀取數據賦給棧指針sp,然後從0x0000 0004位置處讀取數據賦給pc指針,完成復位,結果為:

SP = 0x2000 1068

PC = 0x0000 011D

技術分享圖片

  這與傳統的 ARM 架構不同——其實也和絕大多數的其它單片機不同。傳統的 ARM 架構總是從 0 地址開始執行第一條指令。它們的 0 地址處總是一條跳轉指令。在 CM3 中,在 0 地址處提供 MSP 的初始值,然後緊跟著就是向量表。向量表中的數值是 32 位的地址,而不是跳轉指令。向量表的第一個條目指向復位後應執行的第一條指令,就是我們上面分析的Reset_Handler這個函數。

進入__main

  LDR R0, =__main

  BX   R0

技術分享圖片

執行上兩條指令,跳轉到__main程序段運行,__main的地址是0x0000 0080,上一步指令pc = 0x0000 011D的地址沒有對齊,硬件自動對齊到0x0000 011C,執行__main。

技術分享圖片

pc指針通過立即數尋址,跳轉到0x0000 0081處執行,同上這裏也會自動對齊到0x0000 0080處。

技術分享圖片

  在__scatterload函數中又會進入__scatterload_copy,在__scatterload_copy中進行代碼搬運,主要是加載已經初始化的數據段和未初始化的數據段,同時還會初始化棧空間,即ZI段清零(其中搬運次數由代碼中聲明的變量類型和變量多少來決定)。

技術分享圖片

技術分享圖片

  然後會跳轉到__rt_entry函數執行,__rt_entry是使用ARM C庫的程序的起點。將所有分散加載區重新定位到其執行地址後,會將控制權傳遞給__rt_entry。如下圖,在__rt_entry中主要實現如下幾個功能:

  1、 設置用戶的堆和堆棧

  2、 調用__rt_lib_init以初始化C庫

  3、 調用main()

  4、 調用__rt_lib_shutdown以關閉C庫

  5、 退出

技術分享圖片

  __rt_lib_init函數是庫函數初始化函數,它與__rt_lib_shutdown配合使用。並且這個函數緊靠__rt_stackheap_init()後面調用,即緊跟堆和堆棧初始化後面調用,並且傳遞一個要用作堆的初始內存塊。此函數是標準ARM庫初始化函數,不能重新實現此函數。

  註意:最後兩步是在程序退出main()函數的時候才會執行,而我們嵌入式程序一般都是死循環,所以基本上不會執行這兩個過程。還有以上過程是針對使用標準C Library而言的,不包括使用MDK提供的microlib庫的情況。

在__rt_entry_main中,用戶程序就開始正式執行了(進入C的世界)。在此之前初始化 MSP 是必需的,因為可能第 1 條指令還沒來得及執行,就發生了 NMI 或是其它 fault。 MSP 初始化好後就已經為它們的服務例程準備好了堆棧。這也就是__main中做的事情。

7、最後關於microlib

Microlib 是缺省C庫的備選庫。它旨在與需要裝入到極少量內存中的深層嵌入式應用程序配合使用。這些應用程序不在操作系統中運行,因此microlib 進行了高度優化以使代碼變得很小,當然它的功能相比缺省C庫少,並且根本不具備某些ISO C特性。某些庫函數的運行速度也比較慢,比如memcpy()。

Microlib與缺省C庫之間的主要差異是:

  Microlib不符合ISO C 庫標準。不支持,某些ISO特性,並且其他特性具有的功能也比較少;

  Microlib不符合IEEE754 二進制浮點算法標準;

  Microlib進行了高度優化以使代碼變得很小;

  無法對區域設置進行配置。缺省C區域設置是唯一可用的區域設置;

  不能將main()聲明為使用參數,並且不能返回內容;

  不支持stdio,但未緩沖的stdin、stdout和stderr除外;

  Microlib對C99函數提供有限的支持;

  Microlib不支持操作系統函數;

  Microlib不支持與位置無關的代碼;

  Microlib不提供互斥鎖來防止非線程安全的代碼;

  Microlib不支持寬字符或多字節字符串;

  與stdlib不同,microlib不支持可選的單或雙區內存模型。Microlib只提供雙區內存模型,即單獨的堆棧和堆區。

8、關於生成的xxx.map文件

想要更好的了解啟動代碼的運行機制,我們就有必要了解一下由Keil的鏈接器“armlink”生成的描述文件,即xxx.map文件。

技術分享圖片

目標文件的組成

上圖即是armlink的鏈接器為測試代碼生成的xxx.map文件中的一部分,其描述了鏡像文件的組成信息,其中可以明顯看到其由兩部分構成:

User Code生成的目標文件

C Library生成的目標文件

可見我們在上文中所描述的啟動過程中看到的__main、__rt_entry、__scartterload以及__rt_lib_init等,就是C library中的代碼。

所以,我們每次燒錄的可執行的ARM的bin文件中不僅有開發者編寫的代碼,還有C Library的代碼。

技術分享圖片

上圖為存放在RAM中的RW段。

以上就是CM3芯片的基本啟動過程,有高手發現有低級錯誤,還望指正,謝謝!

關於ARM CM3的啟動文件分析