榆中建設局網(wǎng)站站長工具查詢域名
目錄
- STM32啟動文件簡介
- 啟動文件中的一些指令
- 啟動文件代碼詳解
- ??臻g的開辟
- 堆空間的開辟
- 中斷向量表定義(簡稱:向量表)
- 復位程序
- 對于weak的理解
- 對于_main函數(shù)的分析
- 中斷服務程序
- 用戶堆棧初始化
- 系統(tǒng)啟動流程
STM32啟動文件簡介
STM32啟動文件由ST官方提供,在官方的固件包里。啟動文件由匯編編寫,是系統(tǒng)上電復位后第一個執(zhí)行的程序。
啟動文件主要做了以下工作:
- 初始化堆棧指針
SP = _initial_sp
- 初始化程序計數(shù)器指針
PC = Reset_Handler
- 設置堆和棧的大小
- 初始化中斷向量表
- 配置外部SRAM作為數(shù)據(jù)存儲器(可選)
- 配置系統(tǒng)時鐘,通過調(diào)用SystemInit函數(shù)(可選)
- 調(diào)用C庫中的 _main 函數(shù)初始化用戶堆棧,最終調(diào)用 main 函數(shù)
ARM指針寄存器 —— 堆棧指針寄存器SP、程序計數(shù)器PC、連接寄存器LR
堆棧指針R13(SP):每一種異常模式都有其自己獨立的R13,它通常指向異常模式所專用的堆棧,也就是說五種異常模式、非異常模式(用戶模式和系統(tǒng)模式),都有各自獨立的堆棧,用不同的堆棧指針來索引。這樣當ARM進入異常模式的時候,程序就可以把一般通用寄存器壓入堆棧,返回時再出棧,保證了各種模式下程序的狀態(tài)的完整性;
連接寄存器R14(LR):每種模式下R14都有自身版組,它有兩個特殊功能;
-
保存子程序返回地址;使用BL或BLX時,跳轉指令自動把返回地址放入R14中;子程序通過把R14復制到PC來實現(xiàn)返回,通常用下列指令之一:
MOV PC, LR BX LR
通常子程序這樣寫,保證了子程序中還可以調(diào)用子程序:
stmfd sp!, {lr} ... ldmfd sp!, {pc}
-
當異常發(fā)生時,異常模式的R14用來保存異常返回地址,將R14:如??梢蕴幚砬短字袛?/p>
程序計數(shù)器R15(PC):PC是有讀寫限制的,當沒有超過讀取限制的時候,讀取的值是指令的地址加上8個字節(jié),由于ARM指令總是以字對齊的,故bit[1:0]總是00。當用str或stm存儲PC的時候,偏移量有可能是8或12等其它值。
ARM處理器使用流水線來增加處理器指令流的速度,這樣可使幾個操作同時進行,并使處理與存儲器系統(tǒng)之間的操作更加流暢,連續(xù),能提供0.9MIPS/MHZ的指令執(zhí)行速度。
在隨機存儲器區(qū)劃出一塊區(qū)域作為堆棧區(qū),數(shù)據(jù)可以一個個順序地存入(壓入)到這個區(qū)域之中,這個過程稱為壓棧(push )
。通常用一個指針(堆棧指針 SP—StackPointer)
實現(xiàn)做一次調(diào)整,SP總指向最后一個壓入堆棧的數(shù)據(jù)所在的數(shù)據(jù)單元(棧頂)。從堆棧中讀取數(shù)據(jù)時,按照堆棧 指針指向的堆棧單元讀取堆棧數(shù)據(jù),這個過程叫做彈出(pop )
,每彈出一個數(shù)據(jù),SP 即向相反方向做一次調(diào)整,如此就實現(xiàn)了后進先出的原則。
堆棧是計算機中廣泛應用的技術,基于堆棧具有的數(shù)據(jù)進出LIFO特性,常應用于:
- 保存中斷斷點;
- 保存子程序調(diào)用返回點;
- 保存CPU現(xiàn)場數(shù)據(jù)等;
- 也用于程序間傳遞參數(shù);
ARM處理器中通常將寄存器R13作為堆棧指針(SP)。ARM處理器針對不同的模式,共有 6 個堆棧指針(SP),其中用戶模式和系統(tǒng)模式共用一個SP,每種異常模式都有各自專用的R13寄存器(SP)。它們通常指向各模式所對應的專用堆棧,也就是ARM處理器允許用戶程序有六個不同的堆??臻g。這些堆棧指針分別為R13、R13_svc、R13_abt、R13_und、R13_irq、R13_fiq,如下表堆棧指針寄存器所示。
堆棧指針 | 用戶 | 系統(tǒng) | 管理 | 中止 | 未定義 | 中斷 | 快速中斷 |
---|---|---|---|---|---|---|---|
R13 (SP) | R13 | R13 | R13_svc | R13_abt | R13_und | R13_irq | R13_fiq |
為了更準確地描述堆棧,根據(jù)“壓棧”操作時堆棧指針的增減方向,將堆棧區(qū)分為‘遞增堆?!?#xff08;SP 向大數(shù)值方向變化)和‘遞減堆棧’(SP 向小數(shù)值方向變化);又根據(jù)SP 指針指向的存儲單元是否含有堆棧數(shù)據(jù),又將堆棧區(qū)分為‘滿堆?!?#xff08;SP 指向單元含有堆棧有效數(shù)據(jù))和‘空堆?!?#xff08;SP 指向單元不含有堆棧有效數(shù)據(jù))。
這樣兩兩組合共有四種堆棧方式——滿遞增、空遞增、滿遞減和空遞減。
ARM處理器的堆棧操作具有非常大的靈活性,對這四種類型的堆棧都支持。
ARM處理器中的R13被用作SP。當不使用堆棧時,R13 也可以用做通用數(shù)據(jù)寄存器。
棧的整體作用
- 保護現(xiàn)場;
- 傳遞參數(shù);
- 臨時變量保存在棧中;
深入理解ARM三個寄存器
PC 代表程序計數(shù)器,流水線使用三個階段,因此指令分為三個階段執(zhí)行:
- 取指:從存儲器裝載一條指令;
- 譯碼:識別將要被執(zhí)行的指令;
- 執(zhí)行:處理指令并將結果寫回寄存器;
R15(PC)總是指向 正在取指 的指令:ARM指令是三級流水線,取指、譯指、執(zhí)行是同時進行的,現(xiàn)在PC指向的是正在取指的地址,那么cpu正在譯指的指令地址是PC-4(假設在ARM狀態(tài) 下,一個指令占4個字節(jié)),cpu正在執(zhí)行的指令地址是PC-8,也就是說PC所指向的地址和現(xiàn)在所執(zhí)行的指令地址相差8。當突然發(fā)生中斷的時候,保存的是PC的地址這樣你就知道了,如果返回的時候返回PC,那么中間就有一個指令沒有執(zhí)行,所以用 SUB pc lr -lrq #4
啟動文件中的一些指令
指令名稱 | 作用 |
---|---|
EQU | 給數(shù)字常量取一個符號名,相當于C語言中的define |
AREA | 匯編一個新的代碼段或者數(shù)據(jù)段 |
ALIGN | 編譯器對指令或者數(shù)據(jù)的存放地址進行對齊,一般需要跟一個立即數(shù),缺省表示4字節(jié)對齊。要注意的是,這個不是ARM的指令,是編譯器的,這里放到一起為了方便 |
SPACE | 分配空間 |
PRESERVE8 | 當前文件堆棧需要按照8字節(jié)對齊 |
THUMB | 表示后面指令兼容 THUMB 指令。在 ARM 以前的指令集中有 16 位的THUMBM 指令,現(xiàn)在 Cortex-M 系列使用的都是 THUMB-2 指令集,THUMB-2 是 32 位的,兼容 16 位和 32 位的指令,是 THUMB 的超級版 |
EXPORT | 聲明一個標號具有全局屬性,可被外部的文件使用 |
DCD | 以字節(jié)為單位分配內(nèi)存,要求 4 字節(jié)對齊,并要求初始化這些內(nèi)存 |
PROC | 定義子程序,與 ENDP 成對使用,表示子程序結束 |
WEAK | 弱定義,如果外部文件聲明了一個標號,則優(yōu)先使用外部文件定義的標號,如果外部文件沒有定義也不會出錯。要注意的是,這個不是 ARM 的指令,是編譯器的,這里放到一起為了方便 |
IMPORT | 聲明標號來自外部文件,跟 C 語言中的 extern 關鍵字類似 |
LDR | 從存儲器中加載字到一個存儲器中 |
BLX | 跳轉到由寄存器給出的地址,并根據(jù)寄存器的 LSE 確定處理器的狀態(tài),還要把跳轉前的下條指令地址保存到 LR |
BX | 跳轉到由寄存器/標號給出的地址,不用返回 |
B | 跳轉到一個標號 |
IF,ELSE,ENDIF | 匯編條件分支語句,跟 C 語言的類似 |
END | 到達文件的末尾,文件結束 |
啟動文件代碼詳解
下面,我們以 STM32F103 的啟動代碼為例講解,版本是:STM32Cube_FW_F1_V1.8.0,
啟動文件名稱是:startup_stm32f103xe.s。把啟動代碼分成幾個功能段進行詳細的講解,詳
情如下
??臻g的開辟
Stack_Size EQU 0x00000400AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
**EQU:**宏定義的偽指令,給數(shù)字常量取一個符號名,類似與 C 中的 define。定義棧大小為 0x00000400 字節(jié),即 1024B(1KB),常量的符號是 Stack_Size。
AREA 匯編一個新的代碼段或者數(shù)據(jù)段。段名為 STACK,段名可以任意命名;NOINIT 表示不初始化; READWRITE 表示可讀可寫;ALIGN=3,表示按照 2^3 對齊,即 8 字節(jié)對齊。
SPACE 分配內(nèi)存指令,分配大小為 Stack_Size 字節(jié)連續(xù)的存儲單元給??臻g。
__initial_sp 緊挨著 SPACE 放置,表示棧的結束地址,棧是從高往低生長,所以結束地址就是棧頂?shù)刂贰?/p>
棧主要用于存放局部變量,函數(shù)形參等,屬于編譯器自動分配和釋放的內(nèi)存,棧的大小不能超過內(nèi)部 SRAM 的大小。如果工程的程序量比較大,定義的局部變量比較多,那么就需要在啟動代碼中修改棧的大小,即修改 Stack_Size 的值。如果程序出現(xiàn)了莫名其妙的錯誤,并進入了 HardFault 的時候,你就要考慮下是不是??臻g不夠大,溢出了的問題。
堆空間的開辟
Heap_Size EQU 0x00000200AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
堆空間開辟代碼跟??臻g開辟代碼是類似的了。這部分代碼的意思就是:開辟堆的大小為 0x00000200(512 字節(jié)),段名為 HEAP,不初始化,可讀可寫,(2^3 對齊)8 字節(jié)對齊。__heap_base表示堆的起始地址,__heap_limit 表示堆的結束地址。堆和棧的生長方向相反的,堆是由低向高生長,而棧是從高往低生長。
堆主要用于動態(tài)內(nèi)存的分配,像 malloc()、calloc()和 realloc()等函數(shù)申請的內(nèi)存就在堆上面。堆中的內(nèi)存一般由程序員分配和釋放,若程序員不釋放,程序結束時可能由操作系統(tǒng)回收。
PRESERVE8
THUMB
PRESERVE8:指示編譯器按照 8 字節(jié)對齊。
THUMB:指示編譯器之后的指令為 THUMB 指令。
中斷向量表定義(簡稱:向量表)
中斷向量表定義代碼
AREA RESET, DATA, READONLYEXPORT __VectorsEXPORT __Vectors_EndEXPORT __Vectors_Size
定義一個數(shù)據(jù)段,名字為RESET, READONLY表示只讀;EXPORT表示聲明一個標號具有全局屬性,可被外部的文件使用。這里是聲明了__Vectors、__Vectors_End 和 __Vectors_Size 三個標號具有全局性,可被外部的文件使用
當內(nèi)核響應了一個發(fā)生的異常后,對應的異常服務例程(ESR)就會執(zhí)行。為了決定 ESR的入口地址, 內(nèi)核使用了向量表查表機制。向量表其實是一個 WORD(32 位整數(shù))數(shù)組,每個下標對應一種異常,該下標元素的值則是該 ESR 的入口地址。向量表在地址空間中的位置是可以設置的,通過 NVIC 中的一個重定位寄存器來指出向量表的地址。在復位后,該寄存器的值為 0。因此,在地址 0 (即 FLASH 地址 0) 處必須包含一張向量表,用于初始時的異常分配。
位置 | 優(yōu)先級 | 優(yōu)先級類型 | 名稱 | 說明 | 地址 |
---|---|---|---|---|---|
- | - | - | 保留 | 0x0000_0000 | |
-3 | 固定 | Reset | 復位 | 0x0000_0004 | |
-2 | 固定 | NMI | 不可屏蔽中斷;RCC時鐘安全系統(tǒng)(CSS)聯(lián)接到NMI向量 | 0x0000_0008 | |
-1 | 固定 | 硬件失效(HardFault) | 所有類型的失效 | 0x0000_000C | |
0 | 可設置 | 存儲管理(MemManage) | 存儲器管理 | 0x0000_0010 | |
1 | 可設置 | 總線錯誤(BusFault) | 預取值失敗,存儲器訪問失敗 | 0x0000_0014 | |
2 | 可設置 | 錯誤應用(UsageFault) | 未定義的指令或非法狀態(tài) | 0x0000_0018 | |
- | - | - | 保留 | 0x0000_001C ~ 0x0000_002B | |
3 | 可設置 | SVCall | 通過SWI指令的系統(tǒng)服務調(diào)用 | 0x0000_002C | |
4 | 可設置 | 調(diào)式監(jiān)控(DebugMonitor) | 調(diào)試監(jiān)控器 | 0x0000_0030 | |
- | - | - | 保留 | 0x0000_0034 | |
5 | 可設置 | PendSV | 可掛起的系統(tǒng)服務 | 0x0000_0038 | |
6 | 可設置 | SysTick | 系統(tǒng)嘀嗒定時器 | 0x0000_003C | |
0 | 7 | 可設置 | WWDG | 窗口定時器中斷 | 0x0000_0040 |
1 | 8 | 可設置 | PVD | 連到EXTI的電源電壓檢測(PVD)中斷 | 0x0000_0044 |
2 | 9 | 可設置 | TAMPER | 侵入檢測中斷 | 0x0000_0048 |
3 | 10 | 可設置 | RTC | 實時時鐘(RTC)全局中斷 | 0x0000_004C |
4 | 11 | 可設置 | FLASH | 內(nèi)存全局中斷 | 0x0000_0050 |
中間部分省略,詳情請參考《STM32中文參考手冊》第九章 中斷和事件 中斷和異常向量
56 | 63 | 可設置 | DMA2通道1 | DMA2通道1全局中斷 | 0x0000_0120 |
---|---|---|---|---|---|
57 | 64 | 可設置 | DMA2通道2 | DMA2通道2全局中斷 | 0x0000_0124 |
58 | 65 | 可設置 | DMA2通道3 | DMA2通道3全局中斷 | 0x0000_0128 |
59 | 66 | 可設置 | DMA2通道4_5 | DMA2通道4和DMA2通道5全局中斷 | 0x0000_012C |
舉個例子,如果發(fā)生了異常 SVCall,則 NVIC 會計算出偏移移量是 11x4=0x2C,然后從那里取出服務例程的入口地址并跳入。要注意的是這里有個另類:地址 0x0000 0000 并不是什么入口地址,而是給出了復位后 MSP 的初值。更詳細的向量表,可以參考《STM32中文參考手冊》第九章-中斷和事件-中斷和異常向量
F103 的向量表格中灰色部分是系統(tǒng)內(nèi)核異常。表格中位置 0 到 59 是外部中斷,CM3內(nèi)核的芯片最大支持 240 個外部中斷,具體使用多少個由芯片廠家設計決定。如這個表格中的 103 芯片只是使用了 60 個。這里說的外部中斷是相對內(nèi)核而言
__Vectors DCD __initial_sp ; Top of Stack (棧頂?shù)刂?span id="vxwlu0yf4" class="token punctuation">)DCD Reset_Handler ; Reset Handler (復位程序地址)DCD NMI_Handler ; NMI HandlerDCD HardFault_Handler ; Hard Fault HandlerDCD MemManage_Handler ; MPU Fault HandlerDCD UsageFault_Handler ; Usage Fault HandlerDCD 0 ; ReservedDCD 0 ; ReservedDCD 0 ; ReservedDCD 0 ; ReservedDCD SVC_Handler ; SVCall HandlerDCD DebugMon_Handler ; Debug Monitor HandlerDCD 0 ; ReservedDCD PendSV_Handler ; SysTick Handler; External Interrupts(外部中斷)DCD WWDG_IRQHandler ; Window WatchdogDCD PVD_IRQHandler ; PVD through EXTI Line detectDCD TAMPER_IRQHandler ; TamperDCD RTC_IRQHandler ; RTCDCD FLASH_IRQHandler ; Flash; 中間篇幅太長, 省略掉, 代碼向量表與STM32F103的向量表對應DCD DMA2_Channel1_IRQHandler ; DMA2 Channel1DCD DMA2_Channel2_IRQHandler ; DMA2 Channel2DCD DMA2_Channel3_IRQHandler ; DMA2 Channel3DCD DMA2_Channel4_5_IRQHandler ; DMA2 Channel4 & Channel5
__Vectors_End__Vectors_Size EQU __Vectors_End - __Vectors
__Vectors 為向量表起始地址, __Vectors_End 為向量表結束地址,__Vectors_Size 為向量表大小,__Vectors_Size = __Vectors_End - __Vectors。
DCD:分配一個或者多個以字為單位的內(nèi)存,以四字節(jié)對齊,并要求初始化這些內(nèi)存。
中斷向量表被放置在代碼段的最前面。例如:當我們的程序在 FLASH 運行時,那么向量表的起始地址是:0x0800 0000。結合圖 2.3.2 可以知道,地址 0x0800 0000 存放的是棧頂?shù)刂?。DCD:以四字節(jié)對齊分配內(nèi)存,也就是下個地址是0x0800 0004,存放的是Reset_Handler中斷函數(shù)入口地址。
從代碼上看,向量表中存放的都是中斷服務函數(shù)的函數(shù)名,所以 C 語言中的函數(shù)名對芯片來說實際上就是一個地址。
復位程序
接下來是定義只讀代碼段
AREA |.text|, CORE, READONLY
定義一個段命為.text,只讀的代碼段,在CODE區(qū);復位子程序代碼
; Reset handler
Reset_Handler PROC // 子程序開始EXPORT Reset_Handler [WEAK] // 聲明復位中斷向量 Reset_Handler 為全局屬性,這樣外部文件就可以調(diào)用此復位中斷服務;WEAK: 表示弱定義,如果外部文件優(yōu)先定義了該標號則首先引用外部定義的標號,如果外部文件沒有聲明也不會出錯。這里表示復位子程序可以由用戶在其他文件重新實現(xiàn),這里并不是唯一的IMPORT __main // IMPORT 表示該標號來自外部文件IMPORT SystemInit // 這里表示 SystemInit 和__main 這兩個函數(shù)均來自外部的文件LDR R0 ,= SystemInit // LDR 表示從存儲器中加載字到一個存儲器中; SystemInit 是一個標準的庫函數(shù),在 system_stm32f1xx.c 文件中定義,主要作用是配置系統(tǒng)時鐘、還有就是初始化 FSMC/FMC總線上外掛的 SRAM(可選),前面說配置外部 SRAM 作為數(shù)據(jù)存儲器(可選)就是這個BLX R0 // BLX 表示跳轉到由寄存器給出的地址,并根據(jù)寄存器的 LSE 確定處理器的狀態(tài),還要把跳轉前的下條指令地址保存到 LRLDR R0 ,= __main // 把__main 的地址給 R0。__main 是一個標準的 C 庫函數(shù),主要作用是初始化用戶堆棧和變量等,最終調(diào)用 main 函數(shù)去到 C 的世界。這就是為什么我們寫的程序都有一個 main 函數(shù)的原因,如果不調(diào)用__main,那么程序最終就不會調(diào)用我們 C 文件里面的main,也就無法正常運行BX R0 // BX 表示跳轉到由寄存器/標號給出的地址,不用返回。這里表示切換到__main地址,最終調(diào)用 main 函數(shù),不返回,進入 C 的世界ENDP // 子程序結束
利用 PROC、ENDP 這一對偽指令把程序段分為若干個過程,使程序的結構加清晰;
LDR、BLX、BX 是內(nèi)核的指令,可在《CM3 權威指南 CnR2》第四章-指令集里面查詢到
對于weak的理解
weak 顧名思義是“弱”的意思,在匯編中,在函數(shù)名稱后面加[WEAK]來表示,而在 C語言中,在函數(shù)名稱前面加上 __weak 修飾符來表示,這樣的函數(shù)我們稱為“弱函數(shù)”;
被 [WEAK] 或 __weak 聲明的函數(shù),我們可以在自己的文件中重新定義一個同名函數(shù),最終編譯器編譯的時候,會選擇我們定義的函數(shù),如果我們沒有重新定義這個函數(shù),那么編譯器就會執(zhí)行[WEAK]或__weak 聲明的函數(shù),并且編譯器不會報錯;
HardFault_Handler\PROCEXPORT HardFault_Handler [WEAK]B .ENDP
同樣我們打開stm32f1xx_it.c文件中也定義了HardFault_Handler中斷函數(shù)
void HardFault_Handler(void)
{/* Go to infinite loop when Hard Fault exception occurs */while(1){}
}
在 stm32f1xx_it.c 文件定義了 HardFault_Handler 中斷函數(shù)的情況下,當HardFault_Handler 中斷來到的時候,代碼會運行到 stm32f1xx_it.c 文件的 HardFault_Handler中斷函數(shù),且進入 while(1);
下面,我們注釋掉 stm32f1xx_it.c 的 HardFault_Handler 中斷函數(shù),然后進行編譯,發(fā)現(xiàn)不會報錯。這時候當 HardFault_Handler 中斷來到的時候,代碼會運行到啟動文件的“弱函數(shù)”中,即在啟動文件中 164 行代碼,進行原地跳轉(即無限循環(huán));
對于_main函數(shù)的分析
當看到__main 函數(shù)時,估計有不少人認為這個是 main 函數(shù)的別名或是編譯之后的名字,否則在啟動代碼中再也無法找到和 main 相關的字眼了。可事實是,_main 和 main 是兩個完全不同的函數(shù)。_main 代碼是編譯器自動創(chuàng)建的,因此無法找到_main 代碼。MDK 文檔中有一句說明:it is automatically craated by the linker when it sees a definition of main() 。大體意思可以理解為:當編譯器發(fā)現(xiàn)定義了 main 函數(shù),那么就會自動創(chuàng)建_main;
程序經(jīng)過匯編啟動代碼,執(zhí)行到__main()后,可以看出有兩個大的函數(shù):
- __scatterload():負責把 RW/RO 輸出段從裝載域地址復制到運行域地址,并完成了 ZI運行域的初始化工作;
- __rt_entry():負責初始化堆棧,完成庫函數(shù)的初始化,最后自動跳轉向 main()函數(shù);
中斷服務程序
接下來就是中斷服務程序了
; 系統(tǒng)異常中斷
NMI_Handler PROCEXPORT NMI_Handler [WEAK]B . ;原地跳轉(即無限循環(huán))ENDP
HardFault_Handler\PROCEXPORT HardFault_Handler [WEAK]B .ENDP
;中間代碼太長, 已經(jīng)省略掉
SysTick_Handler PROCEXPORT SysTick_Handler [WEAK]B .ENDP
;外部中斷
Default_Handler PROCEXPORT WWDG_IRQHandler [WEAK]EXPORT PVD_IRQHandler [WEAK]EXPORT TAMPER_IRQHandler [WEAK]EXPORT RTC_IRQHandler [WEAK]EXPORT FLASH_IRQHandler [WEAK]
;中間代碼太長, 已經(jīng)省略掉
DMA2_Channel1_IRQHandler
DMA2_Channel2_IRQHandler
DMA2_Channel3_IRQHandler
DMA2_Channel4_5_IRQHandlerB .ENDP
可以看到這些中斷服務函數(shù)都被[WEAK]聲明為弱定義函數(shù),如果外部文件聲明了一個標號,則優(yōu)先使用外部文件定義的標號,如果外部文件沒有定義也不會出錯;
這些中斷函數(shù)分為系統(tǒng)異常中斷和外部中斷,外部中斷根據(jù)不同芯片有所變化。B 指令是跳轉到一個標號,這里跳轉到一個‘.’,表示無限循環(huán);
在啟動文件代碼中,已經(jīng)把我們所有中斷的中斷服務函數(shù)寫好了,但都是聲明為弱定義,所以真正的中斷服務函數(shù)需要我們在外部實現(xiàn);
如果我們開啟了某個中斷,但是忘記寫對應的中斷服務程序函數(shù)又或者把中斷服務函數(shù)名寫錯,那么中斷發(fā)生時,程序就會跳轉到啟動文件預先寫好的弱定義的中斷服務程序中,并且在 B 指令作用下跳轉到一個‘.’中,無限循環(huán);
這里的系統(tǒng)異常中斷是內(nèi)核的,外部中斷是外設的;
用戶堆棧初始化
ALIGN指令
ALIGN
ALIGN 表示對指令或者數(shù)據(jù)的存放地址進行對齊,一般需要跟一個立即數(shù),缺省表示4 字節(jié)對齊。要注意的是,這個不是 ARM 的指令,是編譯器的;
接下就是啟動文件最后一部分代碼,用戶堆棧初始化代碼
IF :DEF:__MICROLIB // 判斷是否定義了__MICROLIB。關于__MICROLIB 這個宏定義// 如果定義__MICROLIB,聲明__initial_sp、__heap_base 和__heap_limit這三個標號具有全局屬性,可被外部的文件使用。//__initial_sp 表示棧頂?shù)刂?#xff0c;__heap_base表示堆起始地址,__heap_limit 表示堆結束地址EXPORT __initial_spEXPORT __heap_baseEXPORT __heap_limitELSE // 沒有定義__MICROLIB,實際的情況就是我們沒有定義__MICROLIB,所以使用默認的 C 庫運行。堆棧的初始化由 C 庫函數(shù)__main 來完成IMPORT __use_two_region_memory // IMPORT 聲明__use_two_region_memory 標號來自外部文件EXPORT __user_initial_stackheap // EXPORT 聲明__user_initial_stackheap 具有全局屬性,可被外部的文件使用__user_initial_stackheap // 標號__user_initial_stackheap,表示用戶堆棧初始化程序入口// 接下來進行堆??臻g初始化,堆是從低到高生長,棧是從高到低生長,是兩個互相獨立的數(shù)據(jù)段,并且不能交叉使用LDR R0 ,= Heap_Mem // 保存堆起始地址LDR R1 ,= (Stack_Mem + Stack_Size) // 保存棧大小LDR R2 ,= (Heap_Mem + Heap_Size) // 保存堆大小LDR R3 ,= (Stack_Mem) // 保存棧頂指針BX LR // 跳轉到 LR 標號給出的地址,不用返回ALIGNENDIFEND // END 表示到達文件的末尾,文件結束
IF, ELSE, ENDIF 是匯編的條件分支語句
系統(tǒng)啟動流程
在以前 ARM7/ARM9 內(nèi)核的控制器在復位后,CPU 會從存儲空間的絕對地址0x00000000 取出第一條指令執(zhí)行復位中斷服務程序的方式啟動,即固定了復位后的起始地址為 0x00000000(PC = 0x00000000),同時中斷向量表的位置也是固定的。而 Cortex-M3內(nèi)核復位后的起始地址和中斷向量表的位置可以被重映射。充映射的方法是通過啟動模式的選擇,有以下 3 種情況:
- 通過 boot 引腳設置可以將中斷向量表定位于 SRAM 區(qū),即起始地址為 0x2000000,同時復位后 PC 指針位于
0x2000000 處; - 通過 boot 引腳設置可以將中斷向量表定位于 FLASH 區(qū),即起始地址為 0x8000000,同時復位后 PC 指針位于
0x8000000 處; - 通過 boot 引腳設置可以將中斷向量表定位于內(nèi)置 Bootloader 區(qū),本文不對這種情況做論述;
Cortex-M3 內(nèi)核規(guī)定,起始地址必須存放堆頂指針,而第二個地址則必須存放復位中斷入口向量地址,這樣在 Cortex-M3 內(nèi)核復位后,會自動從起始地址的下一個 32 位空間取出復位中斷入口向量,跳轉執(zhí)行復位中斷服務程序。
下面將結合《Cortex-M3 權威指南(中文)》chpt03-復位序列的內(nèi)容進行講解。
啟動模式不同,啟動的起始地址是不一樣的,下面我們以代碼下載到內(nèi)部 FLASH 的情況舉例,即代碼從地址 0x0800 0000 開始被執(zhí)行。
我們知道的復位方式有三種:上電復位,硬件復位和軟件復位。當產(chǎn)生復位,并且離開復位狀態(tài)后,CM3 內(nèi)核做的第一件事就是讀取下列兩個 32 位整數(shù)的值:
- 從地址 0x0800 0000 處取出堆棧指針 MSP 的初始值,該值就是棧頂?shù)刂?#xff1b;
- 從地址 0x0800 0004 處取出程序計數(shù)器指針 PC 的初始值,該值指向復位后執(zhí)行的第一條指令;
下面用示意圖表示
請注意,這與傳統(tǒng)的 ARM 架構不同——其實也和絕大多數(shù)的其它單片機不同。傳統(tǒng)的 ARM 架構總是從 0 地址開始執(zhí)行第一條指令。它們的 0 地址處總是一條跳轉指令。而 在 CM3 內(nèi)核中,0 地址處提供 MSP 的初始值,然后就是向量表(向量表在以后還可以被移至其它位置)。向量表中的數(shù)值是 32 位的地址,而不是跳轉指令。向量表的第一個條目指向復位后應執(zhí)行的第一條指令,就是 Reset_Handler 這個函數(shù)。下面繼續(xù)以戰(zhàn)艦開發(fā)板 HAL庫例程的實驗 1 跑馬燈實驗為例,代碼從地址 0x0800 0000 開始被執(zhí)行,講解一下系統(tǒng)啟動,初始化堆棧、MSP 和 PC 后的內(nèi)存情況
因為 CM3 使用的是向下生長的滿棧,所以 MSP 的初始值必須是堆棧內(nèi)存的末地址加 1。舉例來說,如果你的棧區(qū)域在 0x20000388‐0x20000787 之間,那么 MSP 的初始值就必須是 0x20000788。
向量表跟隨在 MSP 的初始值之后——也就是第 2 個表目。
R15 是程序計數(shù)器,在匯編代碼中,可以使用名字“PC”來訪問它。ARM 規(guī)定:PC最低兩位并不表示真實地址,最低位 LSB 用于表示是 ARM 指令(0)還是 Thumb 指令(1),因為 CM3 主要執(zhí)行 Thumb 指令,所以這些指令的最低位都是 1(都是奇數(shù))。因為 CM3 內(nèi)
部使用了指令流水線,讀 PC 時返回的值是當前指令的地址+4。比如說:
0x1000: MOV R0, PC ; R0 = 0x1004
如果向 PC 寫數(shù)據(jù),就會引起一次程序的分支(但是不更新 LR 寄存器)。CM3 中的指令至少是半字對齊的,所以 PC 的 LSB 總是讀回 0。然而,在分支時,無論是直接寫 PC 的值還是使用分支指令,都必須保證加載到 PC 的數(shù)值是奇數(shù)(即 LSB=1),表明是在Thumb 狀態(tài)下執(zhí)行。倘若寫了 0,則視為轉入 ARM 模式,CM3 將產(chǎn)生一個 fault 異常。
正因為 上 述 原 因 , 圖 3.3 中使用 0x080001CD 來表達地址 0x080001CC 。 當0x080001CD 處的指令得到執(zhí)行后,就正式開始了程序的執(zhí)行(即去到 C 的世界)。所以在此之前初始化 MSP 是必需的,因為可能第 1 條指令還沒執(zhí)行就會被 NMI 或是其它 fault 打斷。MSP 初始化好后就已經(jīng)為它們的服務例程準備好了堆棧。