第14章 啟動文件詳解
第14章 啟動文件詳解
全套200集視頻教程和1000頁PDF教程請到秉火論壇下載:www.firebbs.cn
野火視頻教程優酷觀看網址:http://i.youku.com/firege
本章參考資料《STM32F4xx 中文參考手冊》第十章-中斷和事件:表 46. STM32F42xxx 和 STM32F43xxx 的向量表;MDK中的幫助手冊—ARM Development Tools:用來查詢ARM的匯編指令和編譯器相關的指令。
14.1 啟動文件簡介
啟動文件由匯編編寫,是系統上電復位後第一個執行的程序。主要做了以下工作:
1、初始化堆棧指針SP=_initial_sp
2、初始化PC指針=Reset_Handler
3、初始化中斷向量表
4、配置系統時鐘
5、調用C庫函數_main初始化用戶堆棧,從而最終調用main函數去到C的世界
14.2 查找ARM匯編指令
在講解啟動代碼的時候,會涉及到ARM的匯編指令和Cortex內核的指令,有關Cortex內核的指令我們可以參考《CM3權威指南CnR2》第四章:指令集。剩下的ARM的匯編指令我們可以在MDK->Help->Uvision Help中搜索到,以EQU為例,檢索如下:
圖 141 ARM 匯編指令索引
檢索出來的結果會有很多,我們只需要看Assembler User Guide 這部分即可。下面列出了啟動文件中使用到的ARM匯編指令,該列表的指令全部從ARM Development Tools這個幫助文檔裏面檢索而來。其中編譯器相關的指令WEAK和ALIGN為了方便也放在同一個表格了。
表格 10 啟動文件使用的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類似 |
14.3 啟動文件代碼講解
1. Stack—棧
1 Stack_Size EQU
0x00000400
2
3 AREA STACK, NOINIT, READWRITE, ALIGN=3
4 Stack_Mem SPACE Stack_Size
5 __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堆
1 Heap_Size EQU
0x00000200
2
3 AREA HEAP, NOINIT, READWRITE, ALIGN=3
4 __heap_base
5 Heap_Mem SPACE Heap_Size
6 __heap_limit
開辟堆的大小為0X00000200(512字節),名字為HEAP,NOINIT即不初始化,可讀可寫,8(2^3)字節對齊。__heap_base表示對的起始地址,__heap_limit表示堆的結束地址。堆是由低向高生長的,跟棧的生長方向相反。
堆主要用來動態內存的分配,像malloc()函數申請的內存就在堆上面。這個在STM32裏面用的比較少。
1 PRESERVE8
2 THUMB
PRESERVE8:指定當前文件的堆棧按照8字節對齊。
THUMB:表示後面指令兼容THUMB指令。THUBM是ARM以前的指令集,16bit,現在Cortex-M系列的都使用THUMB-2指令集,THUMB-2是32位的,兼容16位和32位的指令,是THUMB的超級。
3. 向量表
1 AREA RESET, DATA, READONLY
2 EXPORT __Vectors
3 EXPORT __Vectors_End
4 EXPORT __Vectors_Size
定義一個數據段,名字為RESET,可讀。並聲明__Vectors、__Vectors_End和__Vectors_Size這三個標號具有全局屬性,可供外部的文件調用。
EXPORT:聲明一個標號可被外部的文件使用,使標號具有全局屬性。如果是IAR編譯器,則使用的是GLOBAL這個指令。
當內核響應了一個發生的異常後,對應的異常服務例程(ESR)就會執行。為了決定ESR 的入口地址,內核使用了"向量表查表機制"。這裏使用一張向量表。向量表其實是一個WORD(32 位整數)數組,每個下標對應一種異常,該下標元素的值則是該ESR 的入口地址。向量表在地址空間中的位置是可以設置的,通過NVIC 中的一個重定位寄存器來指出向量表的地址。在復位後,該寄存器的值為0。因此,在地址0 (即FLASH 地址0)處必須包含一張向量表,用於初始時的異常分配。要註意的是這裏有個另類:0 號類型並不是什麽入口地址,而是給出了復位後MSP 的初值。
表格 11 F429向量表
編號 |
優先級 |
優先級類型 |
名稱 |
說明 |
地址 |
- |
- |
- |
保留(實際存的是MSP地址) |
0X0000 0000 |
|
-3 |
固定 |
Reset |
復位 |
0X0000 0004 |
|
-2 |
固定 |
NMI |
不可屏蔽中斷。 RCC 時鐘安全系統 |
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 |
代碼 12 向量表
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
6 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字節對齊。
1 ;用戶棧和堆初始化
2 IF :DEF:__MICROLIB
3
4 EXPORT __initial_sp
5 EXPORT __heap_base
6 EXPORT __heap_limit
7
8 ELSE
9
10 IMPORT __use_two_region_memory
11 EXPORT __user_initial_stackheap
12
13 __user_initial_stackheap
14
15 LDR R0, = Heap_Mem
16 LDR R1, =(Stack_Mem + Stack_Size)
17 LDR R2, = (Heap_Mem + Heap_Size)
18 LDR R3, = Stack_Mem
19
BX LR
20
21
ALIGN
22
23
ENDIF
24
END
判斷是否定義了__MICROLIB ,如果定義了則賦予標號__initial_sp(棧頂地址)、__heap_base(堆起始地址)、__heap_limit(堆結束地址)全局屬性,可供外部文件調用。如果沒有定義(實際的情況就是我們沒定義__MICROLIB)則使用默認的C庫,然後初始化用戶堆棧大小,這部分有C庫函數__main來完成,當初始化完堆棧之後,就調用main函數去到C的世界。
IF,ELSE,ENDIF:匯編的條件分支語句,跟C語言的if ,else類似
END:文件結束
14.4 系統啟動流程
下面這段話引用自《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 初始化好後就已經為它們的服務例程準備好了堆棧。
現在,程序就進入了我們熟悉的C世界,現在我們也應該明白main並不是系統執行的第一個程序了。
14.5 每課一問
1、啟動文件的主要作用是什麽?
2、FLASH地址0存放的是什麽?
3、熟悉啟動文件裏面的ARM匯編指令
第14章 啟動文件詳解