CM4啟動彙編檔案詳解
一、啟動檔案解析
啟動檔案由彙編編寫,是系統上電覆位後第一個執行的程式。主要做了以下工作:
- 初始化堆疊指標SP=_initial_sp
- 初始化PC指標=Reset_Handler
- 初始化中斷向量表
- 配置系統時鐘
- 呼叫C庫函式_main初始化使用者堆疊,從而最終呼叫main函式去到C的世界
二、查詢ARM彙編指令
在講解啟動程式碼的時候,會涉及到ARM的彙編指令和Cortex核心的指令,有關Cortex核心的指令我們可以參考《CM3權威指南CnR2》第四章:指令集。剩下的ARM的彙編指令我們可以在MDK->Help->Uvision Help中搜索到,以EQU為例,檢索如下:
檢索出來的結果會有很多,我們只需要看Assembler User Guide 這部分即可。下面列出了啟動檔案中使用到的ARM彙編指令,該列表的指令全部從ARM Development Tools這個幫助文件裡面檢索而來。其中編譯器相關的指令WEAK和ALIGN為了方便也放在同一個表格了。
下表格是啟動檔案使用的ARM彙編指令彙總
指令名稱 |
作用 |
EQU |
給數字常量取一個符號名,相當於C語言中的define |
AREA |
彙編一個新的程式碼段或者資料段 |
SPACE |
分配記憶體空間 |
PRESERVE8 |
當前檔案堆疊需按照8位元組對齊 |
EXPORT |
宣告一個標號具有全域性屬性,可被外部的檔案使用 |
DCD |
以字為單位分配記憶體,要求4位元組對齊,並要求初始化這些記憶體 |
PROC |
定義子程式,與ENDP成對使用,表示子程式結束 |
WEAK |
弱定義,如果外部檔案聲明瞭一個標號,則優先使用外部檔案定義的標號,如果外部檔案沒有定義也不出錯。要注意的是:這個不是ARM的指令,是編譯器的,這裡放在一起只是為了方便。 |
IMPORT |
宣告標號來自外部檔案,跟C語言中的EXTERN關鍵字類似 |
B |
跳轉到一個標號 |
ALIGN |
編譯器對指令或者資料的存放地址進行對齊,一般需要跟一個立即數,預設表示4位元組對齊。要注意的是:這個不是ARM的指令,是編譯器的,這裡放在一起只是為了方便。 |
END |
到達檔案的末尾,檔案結束 |
IF,ELSE,ENDIF |
彙編條件分支語句,跟C語言的if else類似 |
三、啟動檔案程式碼講解
1. Stack—棧
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
開闢棧的大小為0X00000400(1KB),名字為STACK,NOINIT即不初始化,可讀可寫,8(2^3)位元組對齊。
棧的作用是用於區域性變數,函式呼叫,函式形參等的開銷,棧的大小不能超過內部SRAM的大小。如果編寫的程式比較大,定義的區域性變數很多,那麼就需要修改棧的大小。如果某一天,你寫的程式出現了莫名奇怪的錯誤,並進入了硬fault的時候,這時你就要考慮下是不是棧不夠大,溢位了。
EQU:巨集定義的偽指令,相當於等於,類似與C中的define。
AREA:告訴彙編器彙編一個新的程式碼段或者資料段。STACK表示段名,這個可以任意命名;NOINIT表示不初始化;READWRITE表示可讀可寫,ALIGN=3,表示按照2^3對齊,即8位元組對齊。
SPACE:用於分配一定大小的記憶體空間,單位為位元組。這裡指定大小等於Stack_Size。
標號__initial_sp緊挨著SPACE語句放置,表示棧的結束地址,即棧頂地址,棧是由高向低生長的。
2. Heap堆
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
開闢堆的大小為0X00000200(512位元組),名字為HEAP,NOINIT即不初始化,可讀可寫,8(2^3)位元組對齊。__heap_base表示對的起始地址,__heap_limit表示堆的結束地址。堆是由低向高生長的,跟棧的生長方向相反。
堆主要用來動態記憶體的分配,像malloc()函式申請的記憶體就在堆上面。這個在STM32裡面用的比較少。
PRESERVE8
THUMB
PRESERVE8:指定當前檔案的堆疊按照8位元組對齊。
THUMB:表示後面指令相容THUMB指令。THUBM是ARM以前的指令集,16bit,現在Cortex-M系列的都使用THUMB-2指令集,THUMB-2是32位的,相容16位和32位的指令,是THUMB的超級。
3. 向量表
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
定義一個數據段,名字為RESET,可讀。並宣告__Vectors、__Vectors_End和__Vectors_Size這三個標號具有全域性屬性,可供外部的檔案呼叫。
EXPORT:宣告一個標號可被外部的檔案使用,使標號具有全域性屬性。如果是IAR編譯器,則使用的是GLOBAL這個指令。
當核心響應了一個發生的異常後,對應的異常服務例程(ESR)就會執行。為了決定ESR 的入口地址,核心使用了"向量表查表機制"。這裡使用一張向量表。向量表其實是一個WORD(32 位整數)陣列,每個下標對應一種異常,該下標元素的值則是該ESR 的入口地址。向量表在地址空間中的位置是可以設定的,通過NVIC 中的一個重定位暫存器來指出向量表的地址。在復位後,該暫存器的值為0。因此,在地址0 (即FLASH 地址0)處必須包含一張向量表,用於初始時的異常分配。要注意的是這裡有個另類:0 號型別並不是什麼入口地址,而是給出了復位後MSP 的初值。
編號 |
優先順序 |
優先順序型別 |
名稱 |
說明 |
地址 |
- |
- |
- |
保留(實際存的是MSP地址) |
0X0000 0000 |
|
-3 |
固定 |
Reset |
復位 |
0X0000 0004 |
|
-2 |
固定 |
NMI |
不可遮蔽中斷。 RCC 時鐘安全系統(CSS) 連線到 NMI 向量 |
0X0000 0008 |
|
-1 |
固定 |
HardFault |
所有型別的錯誤 |
0X0000 000C |
|
0 |
可程式設計 |
MemManage |
儲存器管理 |
0X0000 0010 |
|
1 |
可程式設計 |
BusFault |
預取指失敗,儲存器訪問失敗 |
0X0000 0014 |
|
2 |
可程式設計 |
UsageFault |
未定義的指令或非法狀態 |
0X0000 0018 |
|
- |
- |
- |
保留 |
0X0000 001C- 0X0000 002B |
|
3 |
可程式設計 |
SVCall |
通過 SWI 指令呼叫的系統服務 |
0X0000 002C |
|
4 |
可程式設計 |
Debug Monitor |
除錯監控器 |
0X0000 0030 |
|
- |
- |
- |
保留 |
0X0000 0034 |
|
5 |
可程式設計 |
PendSV |
可掛起的系統服務 |
0X0000 0038 |
|
6 |
可程式設計 |
SysTick |
系統嘀嗒定時器 |
0X0000 003C |
|
0 |
7 |
可程式設計 |
- |
視窗看門狗中斷 |
0X0000 0040 |
1 |
8 |
可程式設計 |
PVD |
連線EXTI 線的可程式設計電壓檢測中斷 |
0X0000 0044 |
2 |
9 |
可程式設計 |
TAMP_STAMP |
連線EXTI 線的入侵和時間戳中斷 |
0X0000 0048 |
中間部分省略,詳情請參考《STM32F4xx 中文參考手冊》第十章-中斷和事件-向量表部分 |
|||||
84 |
91 |
可程式設計 |
SPI4 |
SPI4全域性中斷 |
0X0000 0190 |
85 |
92 |
可程式設計 |
SPI5 |
SPI5全域性中斷 |
0X0000 0194 |
86 |
93 |
可程式設計 |
SPI6 |
SPI6全域性中斷 |
0X0000 0198 |
87 |
94 |
可程式設計 |
SAI1 |
SAI1全域性中斷 |
0X0000 019C |
88 |
95 |
可程式設計 |
LTDC |
LTDC全域性中斷 |
0X0000 01A0 |
89 |
96 |
可程式設計 |
LTDC_ER |
LTDC_ER全域性中斷 |
0X0000 01A4 |
90 |
97 |
可程式設計 |
DMA2D |
DMA2D全域性中斷 |
0X0000 01A8 |
1 __Vectors DCD __initial_sp ;棧頂地址
2 DCD Reset_Handler ;復位程式地址
3 DCD NMI_Handler
4 DCD HardFault_Handler
5 DCD MemManage_Handler
6 DCD BusFault_Handler
7 DCD UsageFault_Handler
8 DCD
0 ; 0 表示保留
9 DCD
0
10 DCD
0
11 DCD
0
12 DCD SVC_Handler
13 DCD DebugMon_Handler
14 DCD
0
15 DCD PendSV_Handler
16 DCD SysTick_Handler
17
18
19 ;外部中斷開始
20 DCD WWDG_IRQHandler
21 DCD PVD_IRQHandler
22 DCD TAMP_STAMP_IRQHandler
23
24 ;限於篇幅,中間程式碼省略
25 DCD LTDC_IRQHandler
26 DCD LTDC_ER_IRQHandler
27 DCD DMA2D_IRQHandler
28 __Vectors_End
1 __Vectors_Size EQU __Vectors_End - __Vectors
__Vectors為向量表起始地址,__Vectors_End 為向量表結束地址,兩個相減即可算出向量表大小。
向量表從FLASH的0地址開始放置,以4個位元組為一個單位,地址0存放的是棧頂地址,0X04存放的是復位程式的地址,以此類推。從程式碼上看,向量表中存放的都是中斷服務函式的函式名,可我們知道C語言中的函式名就是一個地址。
DCD:分配一個或者多個以字為單位的記憶體,以四位元組對齊,並要求初始化這些記憶體。在向量表中,DCD分配了一堆記憶體,並且以ESR的入口地址初始化它們。
4. 復位程式
1 AREA |.text|, CODE, READONLY
定義一個名稱為.text的程式碼段,可讀。
1 Reset_Handler PROC
2 EXPORT Reset_Handler [WEAK]
3 IMPORT SystemInit
4 IMPORT __main
5
6 LDR R0, =SystemInit
7 BLX R0
8 LDR R0, =__main
9 BX R0
10 ENDP
復位子程式是系統上電後第一個執行的程式,呼叫SystemInit函式初始化系統時鐘,然後呼叫C庫函式_mian,最終呼叫main函式去到C的世界。
WEAK:表示弱定義,如果外部檔案優先定義了該標號則首先引用該標號,如果外部檔案沒有宣告也不會出錯。這裡表示復位子程式可以由使用者在其他檔案重新實現,這裡並不是唯一的。
IMPORT:表示該標號來自外部檔案,跟C語言中的EXTERN關鍵字類似。這裡表示SystemInit和__main這兩個函式均來自外部的檔案。
SystemInit()是一個標準的庫函式,在system_stm32f4xx.c這個庫檔案總定義。主要作用是配置系統時鐘,這裡呼叫這個函式之後,F429的系統時鐘配被配置為180M。
__main是一個標準的C庫函式,主要作用是初始化使用者堆疊,最終呼叫main函式去到C的世界。這就是為什麼我們寫的程式都有一個main函式的原因。如果我們在這裡不呼叫__main,那麼程式最終就不會呼叫我們C檔案裡面的main,如果是調皮的使用者就可以修改主函式的名稱,然後在這裡面IMPORT你寫的主函式名稱即可。
1 Reset_Handler PROC
2 EXPORT Reset_Handler [WEAK]
3 IMPORT SystemInit
4 IMPORT user_main
5 LDR R0, =SystemInit
7 BLX R0
8 LDR R0, =user_main
9 BX R0
10 ENDP
這個時候你在C檔案裡面寫的主函式名稱就不是main了,而是user_main了。
LDR、BLX、BX是CM4核心的指令,可在《CM3權威指南CnR2》第四章-指令集裡面查詢到,具體作用見下表:
指令名稱 |
作用 |
LDR |
從儲存器中載入字到一個暫存器中 |
BL |
跳轉到由暫存器/標號給出的地址,並把跳轉前的下條指令地址儲存到LR |
BLX |
跳轉到由暫存器給出的地址,並根據暫存器的LSE確定處理器的狀態,還要把跳轉前的下條指令地址儲存到LR |
BX |
跳轉到由暫存器/標號給出的地址,不用返回 |
5. 中斷服務程式
在啟動檔案裡面已經幫我們寫好所有中斷的中斷服務函式,跟我們平時寫的中斷服務函式不一樣的就是這些函式都是空的,真正的中斷復服務程式需要我們在外部的C檔案裡面重新實現,這裡只是提前佔了一個位置而已。
如果我們在使用某個外設的時候,開啟了某個中斷,但是又忘記編寫配套的中斷服務程式或者函式名寫錯,那當中斷來臨的時,程式就會跳轉到啟動檔案預先寫好的空的中斷服務程式中,並且在這個空函式中無線迴圈,即程式就死在這裡。
1 NMI_Handler PROC ;系統異常
2 EXPORT NMI_Handler [WEAK]
3 B .
4 ENDP
5
6 ;限於篇幅,中間程式碼省略
7 SysTick_Handler PROC
8 EXPORT SysTick_Handler [WEAK]
9 B .
10 ENDP
11
12 Default_Handler PROC ;外部中斷
13 EXPORT WWDG_IRQHandler [WEAK]
14 EXPORT PVD_IRQHandler [WEAK]
15 EXPORT TAMP_STAMP_IRQHandler [WEAK]
16
17 ;限於篇幅,中間程式碼省略
18 LTDC_IRQHandler
19 LTDC_ER_IRQHandler
20 DMA2D_IRQHandler
21 B .
22 ENDP
B:跳轉到一個標號。這裡跳轉到一個'.',即表示無線迴圈。
6. 使用者堆疊初始化
1 ALIGN
ALIGN:對指令或者資料存放的地址進行對齊,後面會跟一個立即數。預設表示4位元組對齊。
;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
IF :DEF:__MICROLIB
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limit
ELSE
IMPORT __use_two_region_memory
EXPORT __user_initial_stackheap
__user_initial_stackheap
LDR R0, = Heap_Mem
LDR R1, =(Stack_Mem + Stack_Size)
LDR R2, = (Heap_Mem + Heap_Size)
LDR R3, = Stack_Mem
BX LR
ALIGN
ENDIF
END
判斷是否定義了__MICROLIB ,如果定義了則賦予標號__initial_sp(棧頂地址)、__heap_base(堆起始地址)、__heap_limit(堆結束地址)全域性屬性,可供外部檔案呼叫。如果沒有定義(實際的情況就是我們沒定義__MICROLIB)則使用預設的C庫,然後初始化使用者堆疊大小,這部分有C庫函式__main來完成,當初始化完堆疊之後,就呼叫main函式去到C的世界。
IF,ELSE,ENDIF:彙編的條件分支語句,跟C語言的if ,else類似
END:檔案結束
四、系統啟動流程
下面這段話引用自《CM3權威指南CnR2》3.8—復位序列,CM4的復位序列跟CM3一樣。
在離開復位狀態後, CM3 做的第一件事就是讀取下列兩個 32 位整數的值:
1、從地址 0x0000,0000 處取出 MSP 的初始值。
2、從地址 0x0000,0004 處取出 PC 的初始值——這個值是復位向量, LSB 必須是 1。 然後從這個值所對應的地址處取指。
圖 142 復位序列
請注意,這與傳統的 ARM 架構不同——其實也和絕大多數的其它微控制器不同。傳統的 ARM 架構總是從 0 地址開始執行第一條指令。它們的 0 地址處總是一條跳轉指令。在 CM3 中,在 0 地址處提供 MSP 的初始值,然後緊跟著就是向量表。向量表中的數值是 32 位的地址,而不是跳轉指令。向量表的第一個條目指向復位後應執行的第一條指令,就是我們剛剛分析的Reset_Handler這個函式。
圖 143 初始化MSP和PC的一個範例
因為 CM3 使用的是向下生長的滿棧,所以 MSP 的初始值必須是堆疊記憶體的末地址加 1。舉例來說,如果我們的堆疊區域在 0x20007C00-0x20007FFF 之間,那麼 MSP 的初始值就必須是 0x20008000。
向量表跟隨在 MSP 的初始值之後——也就是第 2 個表目。要注意因為 CM3 是在 Thumb 態下執行,所以向量表中的每個數值都必須把 LSB 置 1(也就是奇數)。正是因為這個原因,圖 143中使用0x101 來表達地址 0x100。當 0x100 處的指令得到執行後,就正式開始了程式的執行(即去到C的世界)。在此之前初始化 MSP 是必需的,因為可能第 1 條指令還沒來得及執行,就發生了 NMI 或是其它 fault。 MSP 初始化好後就已經為它們的服務例程準備好了堆疊。