在學習STM32的時候了解了在MAIN函數之前程序干了什么,其中有一些內容是參考別人的。
main函數之前執行的啟動程序: 在執行main函數之前還有一些引導的程序,以開發板的型號為例,啟動程序為startup_stm32f10x_cl.s: 開始的部分是聲明的內容 【1】MODULE 控制指令是用來標記 modules 源碼的開始,后邊的 ?cstartup 是模塊的名字,此文檔的最后的 END 表明模塊的結束,與前面的MODULE對應。 【2】SECTION 指令是聲明段,一個段不能同時包含 public symbol 和 pubweak symbol ,模塊只有在相同的名字的模塊沒有被鏈接進來的時候才會被鏈接進來。 語法格式:SECTION section:type [flag] [(align)] align,是用于指定地址對齊到 2^align,他的取值是 0 到 30 flag,取值NOROOT、ROOT、REORDER、NOREORDER,默認是ROOT,NOROOT表示如果這個段中的符號沒有被引用,將會被連接器舍棄,即可被優化。ROOT表示不可被優化。REORDER表示開始一個新的名字是 section 的段(section),NOREORDER表示開始一個新的名字為 section 的片段(fragment),多個片段組成一個段(section)。 type,memory 的類型,取值是 CODE、CONST、DATA section,段的名字 【3】EXTERN 用導入其他模塊的 symbol(符號)。 【4】PUBLIC 導出 symbol(符號)。 【1】DATA 表示下邊中的標簽是 32 位的標簽,THUMB 表示下邊的標簽是 16 位的標簽,所謂的標簽是 地址的別名,不占用代碼空間,給編譯器看的。 【2】 DCD 是數據定義或者 重定位指令,為的是定義一個值,或者保留 memory,DCD 別名是 DC32,用于聲明一個 32 位的常量,這部分是中斷向量表的內容,需要注意的是,他們的順序不能改變,此部分會放到 flash 的最開始部分,當系統啟動的時候會加載前一個地址,第一個地址是 C 程序的棧的棧頂地址,第二個地址是向量表的開始地址,中斷發生時會根據向量表的首地址和偏移量來找到程序的入口。 【3】sfe 指令作用是返回棧的結尾,因為棧的增長方向是反方向的。 【1】THUMB 表明下邊是 thumb 指令。 【2】Reset_Handler 在開機或者復位的時候執行 R0 = SystemInit 跳轉到 SystemInit 函數,并將處理器切換到 thumb 狀態 R0 = __iar_program_start 跳轉到 __iar_program_start 函數,狀態也是切換到 thumb 狀態 【3】此處的 __iar_program_start 在程序中找不到是因為它已經被封裝到了 IAR 自帶的C庫啟動代碼中了,當我們編譯的時候,在項目屬性的 linker,library中勾選了 Automatic runtime library ,就告訴了編譯器用庫中的 __iar_program_start ,具體實現了什么,我們可以查看 IAR 工具為我們提供的源碼,具體路徑在IAR 安裝目錄下的 arm\src\lib\thumb ,我們可以看到有的文件分別的提供了 匯編代碼和 c 代碼。 其中的 cmain.s 文件中有 可以看到在執行過__iar_program_start函數之后就進入了main函數。 分析啟動過程: 當STM32復位后,首先從地址0x0000_0000處取出棧頂地址放入MSP(主堆棧指針)寄存器中(即寄存器R13)。接著從地址0x0000_0004處取出復位向量地址放入PC寄存器中(即寄存器R15),再接著程序就從復位處理函數處(Reset_Handler)開始執行。 對比兩張圖片可以發現,R13所指向的地址與0x0000_0000地址的內容相等;R15所指向的地址與0x0000_0004地址的內容相等。 接下來我們分析Reset_Handler做了哪些事: 我們可以看到,只是執行了兩個函數SystemInit和__iar_program_start,通過單步跟蹤發現在SystemInit里進行清理中斷,時鐘初始化,Flash讀取初始化等操作。 而在__iar_program_start里實際上只執行了__low_level_init和__iar_data_init2兩個函數,因為__low_level_init里只是將R0賦0x1,所以再將R0和0x0進行比較結果不相等(CMP R0, #0x0),__iar_init$$done這個函數沒有得到執行。
系統復位時,Cortex-M3從代碼區偏移0x0000'0000處獲取棧頂地址,用來初始化MSP(主堆棧指針)寄存器的值。接下來從代碼區偏移0x0000'0004獲取第一個指令的跳轉地址。這些地址,是CM3要求放置中斷向量表的地方。 這里是一個程序的啟動區的反匯編: __vector_table:
08004000 2600 08004002 2000 08004004 7E1D (注解:下面的文字老是說 IAP 很奇怪 不是 IAR么) 08004006 0800 (轉發人注解:我知道 IAP 意思是“在應用編程”。) 這個程序是由IAP程序來啟動的,IAP程序獲取0x0800'4000處的MSP值(0x20002600),并設置為MSP的值,即主堆棧最大范圍是0x2000'0000~0x2000'25FF。接下來IAP程序獲取0x0800'4004處的Reset_Handler的地址(0x0800'7E1D),并跳轉到Reset_Handler()執行。 IAP在這里完全是模仿了Cortex-M3的復位序列,也就是說,在沒有IAP的系統上,CM3只能從0x0800'0000獲取MSP,從0x0800'0004獲取第一條指令所處地址。而IAP就存在在0x0800'0000這個地址上,IAP的啟動,已經消耗掉了這個復位序列,所以IAP要啟動UserApp程序的時候,也是完全模仿Cortex-M3的復位序列的。 接下來我們看看復位后第一句指令——Reset_Handler()函數里有什么。 若我們使用的是ST公司標準外設庫,那么已經有了現成的Reset_Handler,不過他是弱定義——PUBWEAK,可以被我們重寫的同名函數覆蓋。一般來說,我們使用的都是ST提供的Reset_Handler,在V3.4版本的庫中,可以在startup_stm32f10x_xx.s中找到這個函數: PUBWEAK Reset_Handler
SECTION .text:CODE:REORDER(2)
Reset_Handler
LDR R0, =SystemInit
BLX R0
LDR R0, =__iar_program_start
BX R0 看來ST沒有做太多的事,他只調用了自家庫提供的SystemInit函數進行系統時鐘、Flash讀取的初始化,并把大權交給了__iar_program_start這個IAR提供的“內部函數”了,我們就跟緊這個__iar_program_start跳轉,看看IAR做了什么,上面一段代碼的反匯編如下: Reset_Handler:
__iar_section$$root:
08007E1C 4801 LDR R0, [PC, #0x4]; LDR R0, =SystemInit
08007E1E 4780 BLX R0;BLX R0
08007E20 4801 LDR R0, [PC, #0x4];LDR R0, =__iar_program_start
08007E22 4700 BX R0;BX R0
08007E24 6C69 08007E26 0800 08007E28 7D8D 08007E2A 0800 細心的觀眾會發現地址是0x0800'7E1C,比我們查到的0x0800'7E1D差了1,這是ARM家族的遺留問題,因為ARM處理器的指令至少是半字對齊的(16位THUMB指令集 or 32位ARM指令集),所以PC指針的LSB是常為0的,為了充分利用寄存器,ARM公司給PC的LSB了一個重要的使命,那就是在執行分支跳轉時,PC的LSB=1,表示使用THUMB模式,LSB=0,表示使用ARM模式,但在最新的Cortex-M3內核上,只使用了THUMB-2指令集挑大梁,所以這一位要常保持1,所以我們查到的地址是0x0800'7E1D(C=1100,D=1101),放心,我們的CM3內核會忽略掉LSB(除非為0,那么會引起一個fault),從而正確跳轉到0x0800'7E1C。 從0x0800'7E20處的加載指令,我們可以算出__iar_program_start所處的位置,就是當前PC指針(0x0800'7E24),再加上4,即0x0800'7E28處的所指向的地址——0x0800'7D8D(0x0800'7D8C),我們跟緊著跳轉,__iar_program_start果然在這里: __iar_program_start:
08007D8C F000F88C BL __low_level_init
08007D90 2800 CMP R0, #0x0
08007D92 D001 BEQ __iar_init$$done
08007D94 F7FFFFDE BL __iar_data_init2 08007D98 2000 MOVS R0, #0x0
08007D9A F7FDFC49 BL main 我們看到IAR提供了__low_level_init這個函數進行了“底層”的初始化,進一步跟蹤,我們可以查到__low_level_init這個函數做了些什么,是不是我們想象中的不可告人。 __low_level_init:
08007EA8 2001 MOVS R0, #0x1
08007EAA 4770 BX LR __low_level_init出乎想象的簡單,只是往R0寄存器寫入了1,就立即執行"BX LR"回到調用處了,接下來,__iar_program_start檢查了R0是否為0,為0,則執行__iar_init$$done,若不是0,就執行__iar_data_init2。__iar_init$$done這個函數很簡單,只有2句話,第一句是把R0清零,第二句就直接"BL main",跳轉到main()函數了。不過既然__low_level_init已經往R0寫入了1,那么我們還是得走下遠路——看看__iar_data_init2做了些什么,雖然距離main只有一步之遙,不過這中間隱藏了編譯器的思想,我們得耐心看下去。 __iar_data_init2:
08007D54 B510 PUSH {R4,LR}
08007D56 4804 LDR R0, [PC, #0x10]
08007D58 4C04 LDR R4, [PC, #0x10]
08007D5A E002 B 0x8007D62
08007D5C F8501B04 LDR R1, [R0], #0x4
08007D60 4788 BLX R1
08007D62 42A0 CMP R0, R4
08007D64 D1FA BNE 0x8007D5C
08007D66 BD10 POP {R4,PC}
08007D68 7C78 08007D6A 0800 08007D6C 7C9C 08007D6E 0800 看來IAR遲遲不執行main()函數,就是為了執行__iar_data_init2,我們來分析分析IAR都干了些什么壞事~ 首先壓R4,LR入棧,然后加載0x0800'7C78至R0,0x0800'7C9C至R4,馬上跳轉到0x0800'7D62執行R0,R4的比較,結果若是相等,則彈出R4,PC,然后立即進入main()。不過IAR請君入甕是自不會那么快放我們出來的——結果不相等,跳轉到0x0800'7D5C執行,在這里,把R0指向的地址——0x0800'7C78中的值——0x0800'7D71加載到R1,并且R0中的值自加4,更新為0x0800'7C7C,并跳轉到R1指向的地址處執行,這里是另一個IAR函數:__iar_zero_init2: __iar_zero_init2:
08007D70 2300 MOVS R3, #0x0
08007D72 E005 B 0x8007D80
08007D74 F8501B04 LDR R1, [R0], #0x4
08007D78 F8413B04 STR R3, [R1], #0x4
08007D7C 1F12 SUBS R2, R2, #0x4
08007D7E D1FB BNE 0x8007D78
08007D80 F8502B04 LDR R2, [R0], #0x4
08007D84 2A00 CMP R2, #0x0
08007D86 D1F5 BNE 0x8007D74
08007D88 4770 BX LR
08007D8A 0000 MOVS R0, R0 __iar_data_init2還沒執行完畢,就跳轉到了這個__iar_zero_inti2,且看我們慢慢分析這個幫兇——__iar_zero_inti2做了什么。 __iar_zero_inti2將R3寄存器清零,立即跳轉到0x0800'7D80執行'LDR R2, [R0], #0x4',這句指令與剛才在__iar_data_init2見到的'LDR R1, [R0], #0x4'很類似,都為“后索引”。這回,將R0指向的地址——0x0800'7C7C中的值——0x0000'02F4加載到R2寄存器,然后R0中的值自加4,更新為0x0800'7C80。接下來的指令檢查了R2是否為0,顯然這個函數沒那么簡單想放我我們,R2的值為2F4,我們又被帶到了0x0800'7D74處,隨后4條指令做了如下的事情: 1、將R0指向的地址——0x0800'7C80中的值——0x2000'27D4加載到R1寄存器,然后R0中的值自加4,更新為0x0800'7C84。 2、將R1指向的地址——0x2000'27D4中的值——改寫為R3寄存器的值——0,然后R1中的值自加4,更新為0x2000'27D8。 3、R2自減4 4、檢查R2是否為0,不為0,跳轉到第二條執行。不為,則執行下一條。 這簡直就是一個循環!——C語言的循環for(r2=0x2F4;r2-=4;r!=0){...},我們看看循環中做了什么。 第一條指令把一個地址加載到了R1——0x2000'27D4 是一個RAM地址,以這個為起點,在循環中,對長度為2F4的RAM空間進行了清零的操作。那為什么IAR要做這個事情呢?消除什么記錄么?用Jlink查看這片內存區域,可以發現這片區域是我們定義的全局變量的所在地。也就是說,IAR在每次系統復位后,都會自動將我們定義的全局變量清零0。 清零完畢后,接下來的指令"LDR R2, [R0], #0x4"將R0指向的地址——0x0800'7C84中的值——0加載到R2寄存器,然后R0中的值自加4,更新為0x0800'7C88。隨后檢查R2是否為0,這里R2為0,執行'BX LR'返回到__iar_data_init2函數,若是不為0,我們可以發現又會跳轉至“4指令”處進行一個循環清零的操作。 讀到這里,我們應該可以猜到IAR的意圖了:__iar_data_init2一開始加載了0x0800'7C78至R0,0x0800'7C9C至R4,[R0,R4]就是一段啟動代碼區,在這個區域內保存了要“處理”的所有地址與信息——執行的函數地址或者參數,實際上,這片區域也有一個名字,叫做:Region$$Table$$Base。在這個區域內,程序以R0為索引,R4為上限,當R0=R4,__iar_data_init2執行完畢,跳轉至main()函數。 好了,保持我們這個猜想,繼續跟蹤我們的PC指針——我們回到了__iar_data_init2函數中,第一件事就是比較R0,R4的值,可惜的是,仍然不相等,我們又被帶到了0x0800'7D5C,至此,我們應該能看出這是一個__iar_data_init2的“主循環”,這也驗證了我們對IAR意圖的猜想~ __iar_data_init2中的“主循環”: 08007D5C F8501B04 LDR R1, [R0], #0x4
08007D60 4788 BLX R1
08007D62 42A0 CMP R0, R4 我們可以等價寫為:for(r0=0x0800'7C78,r4=0x0800'7C9C;r0!=r4;r0+=4){...} 此時,我們的R0為0x0800'7C88,經過“指令1”,R0變為0x0800'7C8C,R1為0x0800'7C55。我們來看看,7C55處,IAR又要執行何種操作。 __iar_copy_init2:
08007C54 B418 PUSH {R3,R4}
08007C56 E009 B 0x8007C6C
08007C58 F8501B04 LDR R1, [R0], #0x4
08007C5C F8502B04 LDR R2, [R0], #0x4
08007C60 F8514B04 LDR R4, [R1], #0x4
08007C64 F8424B04 STR R4, [R2], #0x4
08007C68 1F1B SUBS R3, R3, #0x4
08007C6A D1F9 BNE 0x8007C60
08007C6C F8503B04 LDR R3, [R0], #0x4
08007C70 2B00 CMP R3, #0x0
08007C72 D1F1 BNE 0x8007C58
08007C74 BC12 POP {R1,R4}
08007C76 4770 BX LR 這是一個名為__iar_copy_init2的函數,他執行了什么"copy"操作呢? 首先壓R3,R4入棧,然后跳轉到0x0800'7C6C,從R0——Region$$Table$$Base中取出參數0x238放入R3,接下來的指令大家應該都熟悉了,0x238不為0,所以我們被帶至7C58處,再次從Region$$Table$$Base中取出參數0x0800'7F14放入R1,從Region$$Table$$Base取出參數0x2000'2AC8放入R2處。細心的觀眾應該能察覺這和__iar_zero_init2中取參數的幾乎一樣:先取出大小,隨后取出了地址——只不過這里多出了1個地址,沒錯這就是"copy",隨后的指令 08007C60 F8514B04 LDR R4, [R1], #0x4
08007C64 F8424B04 STR R4, [R2], #0x4
08007C68 1F1B SUBS R3, R3, #0x4
08007C6A D1F9 BNE 0x8007C60
則是另一個“4指令”,指令1將R1指向地址的數據讀到R4,指令2將R2指向地址的數據改寫為R4的數據,指令3、4是完成一個循環。 說到這里大家都應該明白了——這就是一個"copy"的操作,從Flash地址0x0800'7F14起,將長度0x238的數據拷貝到RAM地址0x2000'2AC8中。 通過Jlink,我們可以看到這片區域是我們定義的并且已初始化的全局變量。也就是說,每次復位后,IAR在此處進行全局變量的初始化。 在這“4指令”執行完畢后,再次從Region$$Table$$Base中取出參數,為0,比較之后條件符合,函數返回__iar_data_init2。 此時的R0已經為0x0800'7C9C與R4相等,__iar_data_init2終于完成它的使命。 08007D98 2000 MOVS R0, #0x0
08007D9A F7FDFC49 BL main 將R0清零以后,IAR放棄主動權,把PC指針交給了用戶程序的入口——main()。 但請注意,這里使用的是BL指令進行main跳轉,也就是說,main函數只是IAR手中的一個子程序,若是main函數執行到了結尾,接下來則會執行exit等IAR提供的“退出”函數。這些函數,等待下回分解~ 總之,IAR在啟動main()函數以前,執行了Reset_Handler,調用SystemInit()(ST庫提供)進行時鐘,Flash讀取初始化,并轉入__iar_program_start中執行__low_level_init與__iar_data_init2,并在__iar_data_init2中,先后調用__iar_zero_init2與__iar_copy_init2對全局變量、全局已初始化變量進行相應的初始化操作。最后,調用main()函數執行。 這就是IAR在啟動main()函數之前做的事情,它并沒有那么神秘,只要花些時間,就可以跟跟蹤分析出這個過程。 STM32的啟動分析 一、STM32的復位序列 當STM32產生復位后,做的第一件事就是讀取下列兩個32位整數的值: 1、從地址0x0000,0000處取出MSP(主堆棧指針)的初始值放入MSP寄存器中; 2、從地址0x0000,0004處取出復位向量放入PC寄存器中,然后從PC中存取的地 址出取指并開始執行。 圖1:復位序列 請注意,這與傳統的ARM架構以及其他的單片機完全不同,他們復位后一般是從 0x0000,0000地址處取出第一條指令并執行,而一般0x0000,0000都是一條跳轉指令。而在STM32中,在0地址處提供的是MSP的初始值,然后緊跟著就是向量表(上電復位時向量表是被默認放在0x04地址處,但是通過修改向量表偏移量寄存器(VTOR)可以將其定義在其他位置)。另外,向量表中的數值是32位的地址,而不是跳轉指令,系統會自動將該數值存入PC寄存器中后從該32為地址指向的地址出開始執行,這有點像指針的指針。 圖2:初始化MSP及PC的初始化的一個范例因為SMT32使用的是向下生長的滿棧, 所以MSP初始值必須是堆棧內存的末地址加1。舉例來說,如果你的堆棧區域在 0x20007C00-0x20007FFF之間,那么MSP的初始值就必須是0x20008000。 向量表跟隨在MSP的初始化之后——也就是第2個標目。要注意因為STM32是在Thumb態下執行,所以向量表中每個數值必須把LSB(最低權重位)置1.正是因為這個原因,圖2中就是用0x101來表示0x100.當0x100處的指令得到執行后,就正是開始了程序的執行。在這之前MSP是必須的,因為可能第1條指令還沒來得及執行,就發生了NMI(不可屏蔽中斷)或者其他的Fault,MSP初始化好后就已經為他們的服務例程準備好了堆棧。 二、STM32的3種啟動模式 在STM32中,可以通過BOOT[1:0]引腳選擇三種不同的啟動模式,如表1: 表1:STM32的三種啟動模式 根據選定的啟動模式,主閃存存儲器、系統存儲器或SRAM可以按照以下方式訪問: 1、 從主閃存存儲器啟動: 主閃存存儲器被映射到啟動空間(0x0000,0000),但仍然能夠在原有的地址(0x8000,0000)訪問它,即閃存存儲器的內容可以在兩個地 址區域訪問,0x0000,0000或者0x8000,0000. 2、從系統存儲器啟動: 系統存儲器被映射到啟動空間(0x0000,0000),但仍然能夠在原有的地址(0x1FFF,F000)訪問它。 3、 從內置SRAM啟動: 只能在0x2000,0000開始的地址區訪問SARM。當從內置的SRAM中啟動,在應用程序的初始化代碼中,必須使用NVIC的異常表和偏移寄存器,從新映射向量表到SRAM之中。 《stm32權威指南》中有這樣的說法,待理解。
完整的Word格式文檔51黑下載地址(共12頁):
STM32在main函數之前執行的啟動程序.docx
(392.57 KB, 下載次數: 45)
2017-10-20 15:53 上傳
點擊文件名下載附件
STM32 下載積分: 黑幣 -5
|