一、文檔背景
分析以STM321x系列為例的啟動文件解析,了解相關的代碼原理和啟動文件的配置內容。對MDK的售前和售后培訓有一定的積累幫助,以及客戶對相關匯編語言的咨詢,同時我們可以更加深入了解到功能更復雜的CPU啟動文件。
二、 啟動文件簡介
啟動文件由匯編編寫,是系統上電復位后第一個執行的程序。主要做了以下工作:
1. 初始化堆棧指針SP(__initial_sp)
2. 初始化PC指針(Reset_Handler)
3. 初始化中斷向量表(__Vectors)
4. 配置系統時鐘(SystemInit)
5. 調用C庫函數_main初始化用戶堆棧,從而最終調用main函數去到C的代碼世界。
三、查找ARM匯編指令
在MDK內可以搜索到ARM的匯編指令,以EQU為例,檢索步驟如下:
打開MDK軟件界面,點擊“Help”->“Uvision Help“后進入”ARM Development Tools“界面進入搜索界面,輸入檢索名稱,選中”只搜索標題“后回車搜索。

圖3-1
下面列出了啟動文件中使用到的ARM匯編指令,該列表的指令全部從ARM Development Tools這個幫助文檔里面檢索而來。其中編譯器相關的指令WEAK和ALIGN為了方便也放在同一個表格內。
表 2?1 啟動文件使用的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類似 |
注意:經測試MDK5.36及以前版本搜索出來標題為”Assember User Guide:“
表 2?1 啟動文件使用的ARM匯編指令匯總

圖3-2
四、啟動文件代碼解析
1. 注釋說明

圖4-1
該啟動代碼適用的芯片系列、代碼版本、日期、版權所有者等相關信息。
2. Stack棧

圖4-2
EQU:給數字常量取一個符號名,相當于C語言中的define。
本例中使用EQU命名 Stack_Size為0x00000400。
AREA:匯編一個新的代碼段或者數據段。
STACK:命名為(HEAP)棧,
NOINIT:不進行初始化
READWRITE:可讀可寫
ALIGN=3 :8(2^3)字節對齊,為8字節對齊。
SPACE:用于分配一定大小的內存空間,單位為字節。
本例中使用SPACE分配 Stack_Mem大小為 Stack_Size的內存空間,即為0x00000400(1024個字節)(1K)
標號__initial_sp緊挨著SPACE語句放置,表示棧的結束地址,即棧頂地址(棧的增長方向是從高地址到低地址)
注:
棧存儲函數的形參、以及函數里定義的局部變量,所以在本例中函數的局部變量、數組這些不能超過1K(含嵌套的函數),否則程序就會崩潰進入hardfaul.
除去這些局部變量以外,還有一些實時操作系統的現場保護、返回地址都是存儲在棧里面。
3. Heap堆

圖4-3
EQU:給數字常量取一個符號名,相當于C語言中的define。
本例中使用EQU命名 Heap_Size為0x00000200。
AREA:匯編一個新的代碼段或者數據段。
HEAP:命名為(HEAP)棧,
NOINIT:不進行初始化
READWRITE:可讀可寫
ALIGN=3 :8(2^3)字節對齊,為8字節對齊。
SPACE:用于分配一定大小的內存空間,單位為字節。
本例中使用SPACE分配 Heap_Mem大小為 Heap_Size的內存空間,即為0x00000200(512個字節)
標號_heap_limit緊挨著SPACE語句放置,表示堆的結束地址(堆的增長方向是從低地址到高地址)
注:
堆主要用來動態內存的分配,意味著如果你用malloc()函數,那么最大分配的內存不能大于512字節,否則程序會崩潰。

PRESERVE8:指定當前文件的堆棧按照8字節對齊。
THUMB:THUMB指令指示匯編程序使用UAL語法將后續指令解釋為T32指令。
4. 向量表

圖4-4-1
AREA:匯編一個新的代碼段或者數據段。
RESET:命名為RESET(復位),
DATA:包含數據,而不是指令。默認為READWRITE
READONLY:只可讀不可寫
EXPORT:聲明一個標號可被外部的文件使用,使標號具有全局屬性。
本例中:聲明 __Vectors、__Vectors_End和__Vectors_Size這三個標號具有全局屬性,可供外部的文件調用。

圖4-4-2
DCD:分配一個或者多個以字為單位的內存,以四字節對齊,并要求初始化這些內存。在向量表中,DCD分配了一堆內存,并且以ESR的入口地址初始化它們。
__initial_sp:棧頂地址 0x0000 0000
Reset_Handler:復位程序 0x0000 0004
NMI_Handler:不可屏蔽中斷,RCC時鐘安全程序連接到NMI向量0x0000 0008
HardFault_Handler: 所有類型的錯誤 0x0000 000C
MemManage_Handler:存儲器管理 0x0000 0010
BusFault_Handler:預取指失敗,存儲器訪問失敗 0x0000 0014
UsageFault_Handler:未定義的指令或非法狀態 0x0000 0018
0:保留函數當前狀態,0x0000 001C - 0x0000 002B
SVC_Handler:通過SWI指令的系統服務調用 0x0000 002C
DebugMon_Handler :調試監控器 0x0000 0030
0:0:保留函數當前狀態 0x0000 0030 - 0x0000 0034
PendSV_Handler :可掛起的系統服務 :0x0000 0034
SysTick_Handler:系統嘀嗒定時器:0x0000 0038
外部中斷:
WWDG_IRQHandler :窗口定時器中斷 0x0000 0040
PVD_IRQHandler:連到EXTI的電源電壓檢測(PVD)中斷 0x0000 0044
TAMPER_IRQHandler:侵入檢測中斷 0x0000 0048
RTC_IRQHandler:實時時鐘(RTC)全局中斷 0x0000 004C
FLASH_IRQHandler:閃存全局中斷 0x0000 0050
RCC_IRQHandler:復位和時鐘控制(RCC)中斷 0x0000 0054
根據芯片手冊,在起始文件內進行地址的設置。
__Vectors為向量表起始地址,__Vectors_End 為向量表結束地址,兩者標號的差值即可算出向量表大小。
向量表從FLASH的0地址開始放置,以4個字節為一個單位,地址0存放的是棧頂地址,0X04存放的是復位程序的地址,以此類推。從代碼上看,向量表中存放的都是中斷服務函數的函數名,可我們知道C語言中的函數名就是一個地址。
5. 復位程序

圖4-5
AREA:定義一個名稱為.text的代碼段,可讀。
復位子程序是系統上電后第一個執行的程序,調用SystemInit函數初始化系統時鐘,然后調用C庫函數_mian,最終調用main函數去到C的世界。
WEAK:表示弱定義,如果外部文件優先定義了該標號則首先引用該標號,如果外部文件沒有聲明也不會出錯。這里表示復位子程序可以由用戶在其他文件重新實現,這里并不是唯一的。
IMPORT:表示該標號來自外部文件,跟C語言中的EXTERN關鍵字類似。這里表示SystemInit和__main這兩個函數均來自外部的文件。
SystemInit()是一個標準的庫函數,在system_stm32f103xe.c這個庫文件中定義。主要作用是配置系統時鐘,這里調用這個函數之后,單片機的系統時鐘被配置為72M。
| 指令名稱 | 作用 |
| LDR | 從存儲器中加載字到一個寄存器中 |
| BL | 跳轉到由寄存器/標號給出的地址,并把跳轉前的下條指令地址保存到LR |
| BLX | 跳轉到由寄存器給出的地址,并根據寄存器的LSE確定處理器的狀態,還要把跳轉前的下一條指令地址保存到LR |
| BX | 跳轉到由寄存器/標號給出的地址,不用返回__main是一個標準的C庫函數,主要作用是初始化用戶堆棧,并在函數的最后調用main函數去到C的世界。這就是為什么我們寫的程序都有一個main函數的原因。 |
6. 中斷服務程序
在啟動文件里面已經幫我們寫好所有中斷的中斷服務函數,跟我們平時寫的中斷服務函數不一樣的就是這些函數都是空的,真正的中斷復服務程序需要我們在外部的C文件里面重新實現,這里只是提前占了一個位置而已。
如果我們在使用某個外設的時候,開啟了某個中斷,但是又忘記編寫配套的中斷服務程序或者函數名寫錯,那當中斷來臨時,程序就會跳轉到啟動文件預先寫好的空的中斷服務程序中,并且在這個空函數中無限循環,即程序就死在這
里。

圖4-6
例如:NMI_Handler
PROC/ENDP:定義子程序,PROC與ENDP成對使用,表示子程序結束
EXPORT:聲明一個標號具有全局屬性,可被外部的文件使用
B:跳轉到一個標號。這里跳轉到一個‘.’,即表示無限循環。
7. 用戶堆棧初始化
ALIGN:對指令或者數據存放的地址進行對齊,后面會跟一個立即數。缺省表示4字節對齊。

圖4-7-1
首先判斷是否定義了__MICROLIB,如果定義了這個宏則賦予標號__initial_sp(棧頂地址)、 __heap_base(堆起始地址)、__heap_limit(堆結束地址)全局屬性, 可供外部文件調用。有關這個宏我們在KEIL里面配置,具體見下圖。然后堆棧的初始化就由C庫函數_main來完成。

圖4-7-2
如果沒有定義__MICROLIB,則插入標號__use_two_region_memory,這個函數需要用戶自己實現,具體要實現成什么樣, 可在KEIL的幫助文檔里面查詢到。

圖4-7-3
然后聲明標號__user_initial_stackheap具有全局屬性,可供外部文件調用,并實現這個標號的內容。
IF,ELSE,ENDIF?:匯編的條件分支語句,跟C語言的if ,else類似
END?:程序結束。
五、討論分析
1. 如何快速修改啟動文件的值?
在編輯界面將”Text Editor“選擇為”Configuration Wizard“,在編輯器中打開文件。大多數啟動文件都包含對配置向導提供類似 GUI 的控件來設置值。在下方”Value“內修改相關數據即可。

圖5-1
六、總結
無論是何種MCU 都必須有啟動文件,因為對于嵌入式開發,絕大部分情況都是使用C語言,而C語言一般都是從main 函數開始,但是對于MCU來說,它是如何找到并執行main函數的,就需要用到“啟動文件”,就是各種 startup_xxxx.s 文件。
啟動文件是使用機器可以理解的匯編語言,經過一些必要的配置,最終能夠調用 main 函數,使得用戶程序能夠在 MCU上正常運行起來的必備文件。
本文對較為簡易的STM32F1x系列的啟動文件進行分析,對內部的匯編語言進行剖析,進行逐行分析和說明注釋。

首頁 > 資源中心 > FAQ
