1. 程式人生 > >CM4啟動彙編檔案詳解

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 初始化好後就已經為它們的服務例程準備好了堆疊。