從事嵌入式開發(fā)的伙伴可能會(huì)思考過一個(gè)問題,我們一般都是使用芯片廠商提供的驅(qū)動(dòng)庫和初始化文件,直接從main函數(shù)開始寫程序,那么系統(tǒng)上電之后,程序怎么引導(dǎo)進(jìn)main函數(shù)執(zhí)行的呢?還有,系統(tǒng)上電之后RAM的數(shù)據(jù)是隨機(jī)的,那么定義的全局變量的初始值又是怎么實(shí)現(xiàn)的呢?下面我將帶著這兩個(gè)問題,以Cortex-M架構(gòu)為例,采用IAR EWARM作為編譯工具鏈,從系統(tǒng)上電之后執(zhí)行的第一條代碼開始,梳理系統(tǒng)的啟動(dòng)過程,了解編譯器在此期間所做的工作。其他的工具鏈,如Keil和GCC在系統(tǒng)初始化過程所做的工作也是相似的,但具體的實(shí)現(xiàn)有所差異。
一、啟動(dòng)文件
芯片廠商提供的啟動(dòng)文件,一般是采用匯編語言編寫,少數(shù)用C語言。在啟動(dòng)文件中一般至少存在下面兩個(gè)部分內(nèi)容:向量表默認(rèn)的中斷和異常處理程序
向量表實(shí)際上是一個(gè)數(shù)組,放置在存儲(chǔ)器的零地址,每個(gè)元素存儲(chǔ)的是各個(gè)中斷或異常處理程序的入口地址。以STM32F107芯片基于IAR工具的啟動(dòng)文件為例:
文件的開頭定義了一個(gè)名為__vector_table的全局符號(hào),“DATA”的作用是在代碼段中定義一個(gè)數(shù)據(jù)區(qū),用作向量表。數(shù)據(jù)區(qū)的內(nèi)容是使用DCD指令定義的32位寬度常量,除了第一個(gè)sfe(CSTACK)比較特殊以為,其他的常量都是異常和中斷服務(wù)程序的地址(在編譯時(shí)函數(shù)名會(huì)被替換成函數(shù)的入口地址)。sfe(CSTACK)是IAR匯編器段操作,用于獲取段(section)的結(jié)束地址,在這里意欲何為呢?實(shí)際上這是獲取堆?;刂返牟僮鳌AR在鏈接器腳本(*.icf)文件中定義堆棧,實(shí)際是定義了一個(gè)名為“CSTACK”的空閑塊(block),如下圖的腳本命令所示。所謂的塊就是保留一段連續(xù)的地址空間,用來作為堆棧或者堆。當(dāng)然,塊也可以是用內(nèi)容的,例如可以用來管理段,但不在今天的討論范圍。我們知道Cortex-M架構(gòu)的堆棧模型是滿減棧,堆棧從高地址向低地址增長,因此堆棧的基地址是CSTACK的結(jié)束地址。向量表的第一個(gè)元素是?;愤@是由Cortex-M架構(gòu)定義的。系統(tǒng)上電后硬件自動(dòng)從向量表中獲取,并設(shè)置主堆棧指針MSP,而不是像其他ARM架構(gòu),堆棧指針需要通過軟件來設(shè)置。向量表中第二個(gè)元素是復(fù)位異常(Reset_Handler)的入口地址。系統(tǒng)上電后,硬件自動(dòng)從__vector_table + 4的位置讀取,并從讀取到的地址開始執(zhí)行。系統(tǒng)上電后CPU執(zhí)行的第一條是Reset_Handler函數(shù)的第一條語句。
上面的THUMB命令表示接下來的代碼采用THUMB模式(Cortex-M只支持Thumb-2指令集);SECTION用于定義一個(gè)段,段名為“.ResetHandler”,段的類型是代碼(CODE);REODER指示用給定的名稱開啟一個(gè)新的段;ROOT指示鏈接器,當(dāng)段內(nèi)的符號(hào)沒有被引用,鏈接器也不可以丟棄這個(gè)段。
PUBWEAK是弱定義,如果用戶在其他位置編寫了中斷處理函數(shù),在連接時(shí)實(shí)際鏈接用戶所編寫的,啟動(dòng)文件中用匯編寫的服務(wù)函數(shù)會(huì)忽略。之所以要在啟動(dòng)文件中以弱定義的方式編寫全部的異常和中斷服務(wù)函數(shù),是為了防止用戶在沒有編寫服務(wù)函數(shù)的情況下開啟并觸發(fā)了中斷,導(dǎo)致系統(tǒng)的不確定。
二、系統(tǒng)初始化過程
在EWARM的工程Options > Debugger > Setup中將“Run to”勾選取消,這樣在進(jìn)入調(diào)試之后就會(huì)停第一條要執(zhí)行的代碼的位置:
進(jìn)入調(diào)試之后會(huì)停在啟動(dòng)文件Reset_Handler函數(shù)第一條匯編指令位置:
此時(shí),通過寄存器觀察窗口查看SP的值為0x20009820。通過鏈接時(shí)生成的map文件,查看CSTACK的地址范圍,0x20009820正好是CSTACK的結(jié)束地址。有了MSP,C代碼就能運(yùn)行了。
ystemInit函數(shù)是芯片廠商根據(jù)ARM的CMSIS標(biāo)準(zhǔn)提供的一個(gè)系統(tǒng)基礎(chǔ)配置函數(shù),配置基礎(chǔ)的時(shí)鐘系統(tǒng)和向量表重定位等。這里的LDR是偽指令,它將SystemInit函數(shù)的地址加載到寄存器R0,實(shí)際上是通過PC偏移尋址來獲取SystemInit的地址。
從上面的圖可以發(fā)現(xiàn)一個(gè)問題,在反匯編窗口可以觀察到SystemInit的地址是0x20000150,但加載到R0寄存器后卻是0x20000151。這是因?yàn)樵谑褂锰D(zhuǎn)指令更新PC時(shí),需要置PC的LSB為1,以表示THUMB模式,由于Cortex-M不支持ARM模式,因此LSB總是1。
執(zhí)行完芯片廠商提供的SystemInit函數(shù)之后,跳轉(zhuǎn)到__iar_program_start,這是IAR編譯器提供的初始化代碼的入口。
__iar_program_start首先會(huì)執(zhí)行兩個(gè)函數(shù):__iar_init_core和__iar_init_vfp,可以完成一些CPU和FPU相關(guān)的初始化操作,在某些ARM架構(gòu)打包好的運(yùn)行時(shí)庫會(huì)有這兩個(gè)函數(shù),用戶也可以重寫這兩個(gè)函數(shù)來自己實(shí)現(xiàn)一些相關(guān)的操作。
之后,跳轉(zhuǎn)到__cmain函數(shù)執(zhí)行。在__cmain中調(diào)用了一個(gè)__low_level_init函數(shù),該函數(shù)專門用于提供給用戶編寫一個(gè)初階的初始化操作,它在全局變量初始化之前執(zhí)行,例如可用在__low_level_init中初始化SDRAM,這樣就可以將全局變量定義到SDRAM中使用。
__low_level_init可以在任意的C文件中編寫,注意它的返回值,如果返回0,后續(xù)就會(huì)跳過變量初始化操作,正常一般都是返回1。
三、全局變量的初始化
此后進(jìn)入到__iar_data_init3函數(shù),在這里會(huì)完成所有具有初始值的全局/靜態(tài)變量的賦值,以及零初始化全局/靜態(tài)變量的清零操作,分別調(diào)用__iar_copy_init3和__iar_zero_init3,將保存在ROM區(qū)由鏈接器生成的變量初始值復(fù)制到變量的地址。注意,新的EWARM版本默認(rèn)變量初始化操作可能會(huì)采用壓縮算法,實(shí)際變量初始化調(diào)用的函數(shù)可能有區(qū)別。
在全局變量未初始化之前,通過watch窗口可以看到,變量的值都是隨機(jī)數(shù)。
在__iar_data_init3執(zhí)行完成后,全部變量的初值賦值已經(jīng)完成。
在__cmain函數(shù)的最后,跳轉(zhuǎn)到用戶的main函數(shù),最終開始用戶的代碼執(zhí)行。
四、總結(jié)
了解了編譯器所提供的初始化過程和處理器架構(gòu),我們可以根據(jù)自己的需求定制系統(tǒng)的初始化。例如,在進(jìn)入__iar_program_start之前,就可以執(zhí)行必要的硬件初始化操作,可以用匯編寫,也可以用C寫。還可以手動(dòng)控制變量的初始化操作,自己實(shí)現(xiàn)變量的初始化。甚至,完全不采用IAR編譯器提供的初始化操作,自己從復(fù)位序列引導(dǎo)至main函數(shù)那也是可以的。
-
芯片
+關(guān)注
關(guān)注
452文章
49985瀏覽量
419670 -
單片機(jī)
+關(guān)注
關(guān)注
6026文章
44455瀏覽量
630943 -
嵌入式
+關(guān)注
關(guān)注
5052文章
18916瀏覽量
300819
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論