博客主頁开瞭,歡迎訪問:blog.spursgo.com
之前接觸較多的是stm32F103單片機捞稿,在開始學(xué)習(xí)的時候,為了深入地學(xué)習(xí)荞胡,在這款單片機的啟動部分也花了不少時間妈踊。但是,當(dāng)時作為初學(xué)者泪漂,還沒有養(yǎng)成學(xué)習(xí)之后做筆記的習(xí)慣廊营。以至于,過了一段時間后萝勤,對這部分又有所忘卻赘风,剛好這次電子設(shè)計競賽,又要使用stm32的另外一種型號的單片機stm32F407纵刘,借此機會邀窃,對之前的知識做一個簡單的復(fù)習(xí)與總結(jié)。并且作為筆記記錄下來假哎,當(dāng)再次來看這些知識的時候瞬捕,會輕松很多,也希望能夠給正在學(xué)習(xí)stm32的朋友帶來一點幫助舵抹。
對于stm32系列的單片機肪虎,就啟動代碼來說,基本上沒有什么區(qū)別惧蛹。在這里扇救,我就以stm32F407型號的單片機為例,來講解一下啟動問題香嗓。
1.首先迅腔,我們來講解一下stm32的啟動方式。
stm32的啟動方式我一直覺得是一個比較有趣的地方:通過BOOT0和BOOT1兩個管腳的不同電平狀態(tài)來決定單片機從何處啟動運行代碼靠娱,這給我們的代碼調(diào)試以及芯片的升級帶來了很大的便利沧烈。
從這張圖片中我們可以看到,stm32一共有3種不同的啟動方式像云。
在講解具體的啟動方式之前锌雀,可能之前沒有接觸過stm32的朋友會不理解0x00000000和0x00000004地址被映射到不同的地址是什么意思。
下面迅诬,我們通過講解stm32的復(fù)位過程來理解上面所提到的幾個地址腋逆。
對所有單片機來說 ,當(dāng)它獲取到復(fù)位REESET信號后侈贷,首先它會做兩個事情:取出棧指針SP的初始值和取出程序指針PC的初始值惩歉。對于不同位數(shù)的單片機來說,取出這些值的地址表示是不同的。還是以stm32來說柬泽,作為32位的處理器慎菲,那么以16進制來表示地址的話,就應(yīng)該是8位數(shù)字锨并。所以露该,棧指針SP的初始值就是從地址0x00000000處取出,程序指針PC的初始值從地址0x00000004處取出第煮。而且可以看出解幼,我們?nèi)〕龅倪@些值正好也是32位的,占用4個字節(jié)包警,因為我們存儲的是地址嘛撵摆,32位的,沒毛病害晦。
了解了0x00000000和0x00000004是怎么回事了之后特铝,就來繼續(xù)說啟動方式的問題吧。
既然stm32要想實現(xiàn)多種方式的啟動壹瘟,那么我們的SP和PC就應(yīng)該從不同的位置取呀鲫剿!而前面所提到的將0x00000000地址映射到0x08000000地址做的正是這個是呀,懂了映射是怎么回事了嗎稻轨?
好灵莲!正式講解具體的啟動方式:
(1)內(nèi)部FLASH啟動方式
當(dāng)芯片上電后采樣到BOOT0引腳為低電平時,0x00000000和0x00000004地址被映射到內(nèi)部FLASH的首地址0x08000000和0x08000004殴俱。因此政冻,內(nèi)核離開復(fù)位狀態(tài)后,讀取內(nèi)部FLASH的0x08000000地址空間存儲的內(nèi)容线欲,賦值給棧指針SP明场,作為棧頂?shù)刂罚僮x取內(nèi)部FLASH的0x08000004地址空間存儲的內(nèi)容询筏,賦值給程序指針PC榕堰,作為將要執(zhí)行的第一條指令所在的地址。具備這兩個條件后嫌套,內(nèi)核就可以開始從PC指向的地址中讀取指令執(zhí)行了。
(2)內(nèi)部SRAM啟動方式
類似地圾旨,當(dāng)芯片上電后采樣到BOOT0和BOOT1引腳均為高電平時踱讨,0x00000000和0x00000004地址被映射到內(nèi)部SRAM的首地址0x20000000和0x20000004,內(nèi)核從SRAM空間獲取內(nèi)容進行自舉砍的。其實自舉就是設(shè)置運行環(huán)境并執(zhí)行主體程序痹筛,只是聽起來高大上罷了。
(3)系統(tǒng)存儲器啟動方式
當(dāng)芯片上電后采樣到BOOT0引腳為高電平,BOOT1為低電平時帚稠,內(nèi)核將從系統(tǒng)存儲器的0x1FFF0000及0x1FFF0004獲取MSP及PC值進行自舉谣旁。系統(tǒng)存儲器是一段特殊的空間,用戶不能訪問滋早,ST公司在芯片出廠前就在系統(tǒng)存儲器中固化了一段代碼榄审。因而使用系統(tǒng)存儲器啟動方式時,內(nèi)核會執(zhí)行該代碼杆麸,該代碼運行時搁进,會為ISP提供支持(In System Program),如檢測USART1/3昔头、CAN2及USB通訊接口傳輸過來的信息饼问,并根據(jù)這些信息更新自己內(nèi)部FLASH的內(nèi)容,達到升級產(chǎn)品應(yīng)用程序的目的揭斧,因此這種啟動方式也稱為ISP啟動方式莱革。
很好,方式這個問題我們已經(jīng)講完了讹开,那現(xiàn)在來講講難啃的啟動代碼吧盅视。
我們在這里以內(nèi)部FLASH的啟動過程為例,來講解啟動匯編代碼萧吠。
2.啟動代碼匯編分析
注意到圖片中左邊的紅色框了嗎左冬?startup_stm32f40xx.s文件就是我們的啟動代碼所在的文件,很奇怪吧纸型!怎么是.s后綴呢拇砰?沒啥奇怪的,因為里面是匯編代碼呀狰腌,不是c語言寫的哦除破!啟動代碼可是要求運行速度非常快的呀琼腔,雖然c語言速度也很快了瑰枫,但是和匯編比起來,呵呵丹莲,你懂得光坝。
右邊是官方給出的英文注釋,本人英語渣渣甥材,在這里我也不去獻丑翻譯啦盯另,想了解的朋友可以自行翻譯,但是個人覺得沒有必要去看懂洲赵。
下面不廢話了鸳惯,直接來干貨商蕴,講解匯編代碼。
下面對匯編代碼進行逐一解釋芝发,涉及到相關(guān)匯編指令和偽指令绪商,想詳細了解的朋友自行百度,這里只是簡單的介紹一下辅鲸,不做深入探討格郁。
1)堆和棧的初始化
Stack_Size? ? ? EQU? ? 0x00000400
這段代碼很簡單,就是給Stack_Size賦一個值而已瓢湃,用來定義棧區(qū)大小理张。
棧區(qū)(stack)— 由編譯器自動分配釋放 ,存放函數(shù)的參數(shù)值绵患,局部變量的值等雾叭。其操作方式類似于數(shù)據(jù)結(jié)構(gòu)中的棧。
AREA? ? STACK, ?NOINIT, ?READWRITE, ?ALIGN=3
AREA 偽指令用于定義一個段落蝙,如代碼段织狐、數(shù)據(jù)段或者堆棧段。(注意:段是匯編語言中非常重要的一個概念筏勒,可以去詳細了解一下) 移迫;STACK表示我們定義的是棧,其實這里僅僅是一個便于人理解的一個單詞啦管行;NOINIT指定此數(shù)據(jù)段僅僅保留了內(nèi)存單元厨埋,而沒有將各初始值寫入內(nèi)存單元,或者將各個內(nèi)存單元值初始化為0捐顷;READWRITE屬性荡陷,指定本段為可讀可寫;ALIGN屬性迅涮,用來指定數(shù)據(jù)對齊的方式废赞,為2的ALIGN次方,這是ALIGN=3叮姑,也就是按照字節(jié)對齊唉地。
Stack_Mem? ? SPACE? Stack_Size?
SPACE用來分配一片連續(xù)的存儲區(qū)域并初始化為0。這里也就是分配一篇大小為0x400的連續(xù)存儲區(qū)域传透,并初始化為0耘沼,并且該區(qū)域的起始地址為Stack_Mem。
__initial_sp?
是匯編代碼地址標(biāo)號朱盐,在這里我們用來表示椄剑空間頂?shù)刂贰??
Heap_Size? ? ? EQU? ? 0x00000200?
用來定義堆區(qū)大小。
堆區(qū)(heap) — 一般由程序員分配釋放托享, 若程序員不釋放骚烧,程序結(jié)束時可能由OS回收 。注意它與數(shù)據(jù)結(jié)構(gòu)中的堆是兩回事闰围,分配方式倒是類似于鏈表赃绊。
AREA? ? HEAP, ?NOINIT, ?READWRITE, ?ALIGN=3
STACK表示我們定義的是堆,其他見上文
Heap_Mem? ? ? ? SPACE? Heap_Size
這里也就是分配一篇大小為0x200的連續(xù)存儲區(qū)域羡榴,并初始化為0碧查,并且該區(qū)域的起始地址為Stack_Mem。
__heap_limit ?
是匯編代碼地址標(biāo)號校仑,在這里我們用來表示堆空間結(jié)束地址忠售。
2)中斷向量表定義
PRESERVE8
THUMB
PRESERVE8指定當(dāng)前文件要求堆棧8位對齊。
THUMB指定所用的指令集為thumb迄沫。
其實這兩條代碼我也不是很懂稻扬,希望懂得朋友可以指點一下。
這里我們需要注意一下這條注釋:
; Vector Table Mapped to Address 0 at Reset
它指的Address 0并不是真正意義上的0x00000000羊瘩。在我們之前的假設(shè)下(假設(shè)STM32從FLASH啟動)則此中斷向量表起始地址為0x8000000泰佳,實際上是在CODE區(qū)。
AREA? ? RESET, ?DATA, ?READONLY?
AREA定義一塊數(shù)據(jù)段尘吗,只可讀逝她,段名字是RESET;DATA屬性:用于定義數(shù)據(jù)段睬捶,默認(rèn)為READWRITE黔宛。指定本段為可讀可寫。
EXPORT? __Vectors ? ??
EXPORT偽指令用于在程序中聲明一個全局的標(biāo)號擒贸,該標(biāo)號可在其他的文件中引用臀晃。EXPORT可用GLOBAL代替。標(biāo)號在程序中區(qū)分大小寫酗宋。在程序中聲明一個全局的標(biāo)號__Vectors积仗,該標(biāo)號可在其他的文件中引用
EXPORT??__Vectors_End?
在程序中聲明一個全局的標(biāo)號__Vectors_End
EXPORT??__Vectors_Size?
在程序中聲明一個全局的標(biāo)號__Vectors_Size
__Vectors? ? ? DCD? ? __initial_sp ? ? ? ? ? ? ?
DCD ?用于分配一片連續(xù)的字存儲單元并用指定的數(shù)據(jù)初始化。向量表第一個表項是棧頂?shù)刂吠擅ǎ撎幬锢淼刂分导礊?__Vetors?標(biāo)號所表示的值寂曹,該地址中存儲__initial_sp所表示的地址值,大小為一個字(32bit)回右。
DCD? ? Reset_Handler ? ? ? ? ?
向量表第二個表項是復(fù)位中斷服務(wù)函數(shù)Reset Handler入口地址
DCD? ? NMI_Handler
...
...
...
DCD? ? OTG_FS_IRQHandler ? ? ? ?
這些都是在中斷向量表中注冊函數(shù)入口地址隆圆,就不在這里挨個解釋了。
__Vectors_End ?
表示中斷向量表結(jié)束
__Vectors_Size ? EQU ? __Vectors_End ?- ?__Vectors ?
得到向量表的大小
3)地址重映射及中斷向量表的轉(zhuǎn)移
AREA? ? |.text|, ?CODE, ?READONLY ??
定義一個代碼段翔烁,可讀渺氧,段名字是.text? 段名若以數(shù)字或者標(biāo)點開頭,則該段名需用"|"括起來蹬屹,如|1_test|侣背。
Reset_Handler? ? PROC?
標(biāo)記一個函數(shù)的開始白华。利用PROC、ENDP這一對偽指令把程序段分為若干個過程贩耐,使程序的結(jié)構(gòu)加清晰弧腥。
EXPORT? Reset_Handler? ? ? ? ? ? [WEAK] ?
EXPORT偽指令用于在程序中聲明一個全局的標(biāo)號,[WEAK]選項聲明其他的同名標(biāo)號優(yōu)先于該標(biāo)號被引用潮太。在外部沒有定義該符號時導(dǎo)出該符號Reset_Handler管搪。
IMPORT? SystemInit
IMPORT? __main?
?IMPORT偽指令用于通知編譯器要使用的標(biāo)號在其他的源文件中定義
LDR? ? R0, =SystemInit
BLX? ? R0
把SystemInit函數(shù)的地址裝載到R0,BLX為跳轉(zhuǎn)指令铡买,跳轉(zhuǎn)到R0中的地址處更鲁,換句話說,就是執(zhí)行SystemInit()這個函數(shù)啦奇钞。函數(shù)SystemInit()主要作用是設(shè)置系統(tǒng)時鐘頻率和中斷寄存器的初始化澡为。
LDR? ? R0, =__main?
BX? ? ? R0 ?
把main()函數(shù)的地址裝載到R0,BLX為跳轉(zhuǎn)指令蛇券,跳轉(zhuǎn)到R0中的地址處缀壤,換句話說,就是執(zhí)行main()這個函數(shù)啦纠亚,進入C的世界塘慕。
ENDP
函數(shù)結(jié)束
然后由很長的代碼都是用來定義相關(guān)函數(shù)的,這里就不解釋了蒂胞。
B
B本來也是跳轉(zhuǎn)指令图呢。它的作用相當(dāng)于 MOV PC, 目標(biāo)地址骗随。但是這里B后面沒有接上目標(biāo)地址蛤织,我也不知道為什么要用上這么一句。
ENDP
ALIGN
ENDP不多說鸿染。ALIGN屬性:使用方式為ALIGN 表達式指蚜。在默認(rèn)時,ELF(可執(zhí)行連接文件)的代碼段和數(shù)據(jù)段是按字對齊的涨椒,表達式的取值范圍為0~31摊鸡,相應(yīng)的對齊方式為2表達式次方。
4)堆和棧的初始化
IF? ? ? :DEF:__MICROLIB ?
判斷是否使用DEF:__MICROLIB(micro lib)
EXPORT??__initial_sp ? ??
EXPORT??__heap_base ?;使外部程序可以使用
EXPORT??__heap_limit
將__initial_sp蚕冬、__heap_base免猾、__heap_limit賦予全局屬性、
ELSE ??
IMPORT??__use_two_region_memory ?
定義全局標(biāo)號 __use_two_region_memory
EXPORT??__user_initial_stackheap ??
聲明全局標(biāo)號__user_initial_stackheap囤热,這樣外程序也可調(diào)用此標(biāo)號猎提。允許進行棧和堆的賦值,在__main函數(shù)執(zhí)行過程中調(diào)用旁蔼。
__user_initial_stackheap ??
標(biāo)號__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
相當(dāng)于MOV ?PC, LR
LR就是連接寄存器(Link Register疙教,LR)在ARM體系結(jié)構(gòu)中LR的特殊用途有兩種:一是用來保存子程序返回地址;二是當(dāng)異常發(fā)生時蚓炬,LR中保存的值等于異常發(fā)生時PC的值減4(或者減2)松逊,因此在各種異常模式下可以根據(jù)LR的值返回到異常發(fā)生前的相應(yīng)位置繼續(xù)執(zhí)行。在這里是第一種功能肯夏,那么結(jié)合BX的用法,就是回到之前保存的返回地址處犀暑。
ALIGN
同上一個ALIGN
ENDIF
END
over喏驯击!
最后我們來總結(jié)一下啟動代碼的作用:
(1)堆和棧的初始化;
(2)向量表定義耐亏;
(3)地址重映射及中斷向量表的轉(zhuǎn)移徊都;
(4)設(shè)置系統(tǒng)時鐘頻率;
(5)中斷寄存器的初始化广辰;
(6)進入C應(yīng)用程序暇矫。
我廢話了這么多,還是希望各位要好好消化一下哦择吊!