大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家分享的是IAR啟動函式流程及其__low_level_init設計對函式重定向的影響。
上一篇文章 《IAR下RT-Thread工程自定義函式段重定向失效分析》 裡我們找出了影響 IAR 連結器處理自定義程式段重定向的原因,主要跟 __low_level_init() 函式有關,這個函式屬於 IAR 底層設計,它在 IAR 啟動函式 __iar_program_start() 中會被自動呼叫。
__iar_program_start() 是 IAR 標準啟動函式,也屬於 IDE 底層設計。在任何一個 Cortex-M 廠商晶片的啟動檔案裡(startup_xxDevice.s)都能看到它的身影,它是復位函式 Reset_Handler() 和 主函式 main() 之間的橋樑,今天我們就仔細說說這個啟動函式以及其中 __low_level_init 設計:
- Note 1: 閱讀本文前需要對 《IAR連結檔案(.icf)》、《IAR對映檔案(.map)》 這兩種檔案有所瞭解。
- Note 2: 本文使用的 IAR EWARM 軟體版本是 v9.10.2。
一、通用晶片上電啟動流程
在深入挖掘 IAR 啟動函式原始碼之前,有必要先整體瞭解一下通用的晶片上電啟動流程,即進入使用者 main 函式之前核心必須要做的事情,注意這裡並不包含晶片底層外設的初始化(這是因晶片而異的)。
通用啟動流程簡單來說分為如下四步:第一步是從 ROM 區域中斷向量表裡獲取入口函式開始執行,設定好初始棧指標,有了正確的棧,核心就具備函式跳轉執行的能力了。第二步和第三步是全域性變數的初始化(將全域性變數初值從 ROM 區域拷貝到變數所連結的 RAM 區域),全域性變數初始化完成,應用程式就有了正確的初始態,最後一步就是跳轉到 main 函式。
二、從原始碼角度看啟動流程
在上一節通用啟動流程的指導下,我們還需要增加一些 MCU 外設相關的初始化便形成了完整的晶片啟動流程,現在我們從原始碼角度再來看一下具體實現。
2.1 典型的 Cortex-M 復位函式
我們知道復位函式 Reset_Handler() 是晶片上電啟動執行的第一個函式(有時又叫入口函式),它完成了進入使用者 main() 函式之前的全部動作。隨便下載一家 Cortex-M 廠商晶片 SDK 包,找到 IAR 版啟動檔案,其復位函式流程都差不多,這是 Cortex-M 核心架構決定的。
如下是典型的復位函式程式碼。復位函式裡的操作包括關全域性中斷、設定中斷向量表首地址、設定棧頂、系統初始化、開全域性中斷、進啟動函式。其中系統初始化 SystemInit() 函式是因晶片而異的,各廠商 SDK 裡會有具體原始碼實現(一般在 system_xxDevice.c 檔案裡),這裡面主要做晶片硬體層面的初始化,比如關看門狗、Cache 初步設定等,保證核心不受硬體模組狀態影響,能正常執行指令。
THUMB
PUBWEAK Reset_Handler
SECTION .text:CODE:REORDER:NOROOT(2)
Reset_Handler
CPSID I ; Mask interrupts
LDR R0, =0xE000ED08
LDR R1, =__vector_table
STR R1, [R0]
LDR R2, [R1]
MSR MSP, R2
LDR R0, =SystemInit
BLX R0
CPSIE I ; Unmask interrupts
LDR R0, =__iar_program_start
BX R0
2.2 __iar_program_start() 到底幹了啥?
上一小節裡我們知道復位函式裡的最後一個動作就是跳轉到啟動函式,將核心執行權交給 __iar_program_start(),這個啟動函式原始碼並不在廠商 SDK 包裡,而在 IAR 安裝目錄下,因為它是 IAR 的通用底層設計。
為了找到 __iar_program_start() 的原始碼,我們可以隨便編譯一個 SDK 例程(痞子衡選擇的是 \SDK_2.11.0_MIMXRT1170-EVK\boards\evkmimxrt1170\demo_apps\hello_world\cm7\iar),檢視其對應對映檔案(.map),發現啟動函式來自於 cstartup_M.o,然後我們在 IAR 安裝目錄下搜尋 cstartup_M.c/.s 檔案,最終我們在如下路徑找到了啟動函式相關的全部原始檔。
\IAR Systems\Embedded Workbench 9.10.2\arm\src\lib\thumb\cstartup_M.s
\IAR Systems\Embedded Workbench 9.10.2\arm\src\lib\thumb\cmain.s
\IAR Systems\Embedded Workbench 9.10.2\arm\src\lib\runtime\low_level_init.c
\IAR Systems\Embedded Workbench 9.10.2\arm\src\lib\init\data_init.c
結合啟動函式相關原始檔裡的程式碼,我們終於搞清了啟動函式全部流程,也找到了我們最關心的 __low_level_init() 函式呼叫位置,它在 .data/.bss/.textrw 段初始化之前被執行,所以它的功能應該跟 SystemInit() 差不多。預設 __low_level_init() 函式是空的,返回值是 1(返回值 0/1 決定後面的 __iar_data_init3() 要不要執行,1 是要執行),如果你想啟用這個函式,需要在自己的原始檔裡重新定義實現,IAR 編譯時會優先引用重新定義的版本。
__iar_program_start() ->
__cmain() ->
__low_level_init() -> // 底層初始化,預設是個空函式
__iar_data_init3() -> // .data, .bss, .textrw 段初始化
main()
2.3 __low_level_init() 設計注意事項
在 EWARM_DevelopmentGuide.ENU 手冊裡搜尋 __low_level_init,我們可以找到這個函式的設計初衷,官方說法是為了給應用程式一個早期初始化的機會,本質上就是跟 SystemInit() 一樣的作用,但是因為這個 __low_level_init 函式只在 IAR 環境下適用,如果用了它,應該程式程式碼就不具備跨 IDE 的通用性,因此在各廠商 SDK 包裡選擇了統一定義的 SystemInit() 來完成早期初始化工作。
IAR 開發手冊: \IAR Systems\Embedded Workbench 9.10.2\arm\doc\EWARM_DevelopmentGuide.ENU
EWARM_DevelopmentGuide.ENU 手冊裡還特別提了幾點跟 __low_level_init 相關的注意事項,均跟 IAR 連結器所識別的 initialize by copy 連結語法有關,概括來說就是因為 __low_level_init 是在 .data/.bss/.textrw 段初始化之前被執行的,所以其程式碼本身及其呼叫的全部程式碼都不受 initialize by copy 作用,也就是這些程式碼都不應是 RAMFUNC 型。
- Note: 更準確地說 initialize by copy 作用範圍其實是 __iar_data_init3() 之後的程式碼
三、一個 __low_level_init() 相關的重定向實驗
最後我們再做個 __low_level_init() 相關的小實驗,在 \SDK_2.11.0_MIMXRT1170-EVK\boards\evkmimxrt1170\demo_apps\hello_world\cm7\iar 例程基礎上(flexspi_nor_debug build),建立一個包含如下內容的 ramfunc_test.c 原始檔,並將其新增進工程編譯。
ramfunc_test1/3() 函式放入自定義程式段 CodeQuickAccess,ramfunc_test2/4() 函式放到預設 .textrw 段,然後重寫 __low_level_init() 函式,在 __low_level_init() 函式裡分別呼叫 ramfunc_test1/2/3/4(),其中 ramfunc_test1/2() 函式的呼叫在 __iar_data_init3() 前面,ramfunc_test1/2() 函式的呼叫在 __iar_data_init3() 後面。
void ramfunc_test1(void) @"CodeQuickAccess"
{
__NOP();
}
__ramfunc void ramfunc_test2(void)
{
__NOP();
}
void ramfunc_test3(void) @"CodeQuickAccess"
{
__NOP();
}
__ramfunc void ramfunc_test4(void)
{
__NOP();
}
// 重定義此函式,讓 IAR 編譯器使用這個版本,而不是預設版本
int __low_level_init(void)
{
extern void __iar_data_init3(void);
ramfunc_test1();
ramfunc_test2();
// 這裡增加 .data/.bss/.textrw 的初始化呼叫,
// 便於區分 ramfunc_test1/2 和 ramfunc_test3/4 位置
__iar_data_init3();
ramfunc_test3();
ramfunc_test4();
return 0;
}
編譯連結修改後的測試工程,檢視其對映檔案,以及在板子上實測,得到如下結果:
- 結論1:放入自定義程式段的函式,無論其呼叫位置在 __iar_data_init3() 之前還是之後,一律被 initialize by copy 忽略,函式直接連結在目標 RAM 區,函式重定向無效;
- 結論2:放入預設 .textrw 段的函式,如果其呼叫位置在 __iar_data_init3() 之後,能夠被 initialize by copy 作用,函式重定向生效;
- 結論3:放入預設 .textrw 段的函式,如果其呼叫位置在 __iar_data_init3() 之前,從對映檔案裡看其能夠被 initialize by copy 作用,但在板子上實測,發現執行到該函式時返回會產生匯流排錯誤,因此函式重定向也是無效的;
*******************************************************************************
*** PLACEMENT SUMMARY
***
"P1": place in [from 0x3000'2000 to 0x30fb'ffff] { ro };
"P2": place in [from 0x2000'0000 to 0x2003'fbff] { rw };
"P8": place in [from 0x0 to 0x3'ffff] { section CodeQuickAccess };
initialize by copy { rw, section .textrw, section CodeQuickAccess };
Section Kind Address Size Object
------- ---- ------- ---- ------
"P8": 0x8
CodeQuickAccess ro code 0x0 0x8 ramfunc_test.o [6]
"P2-P3|P5|P9", part 1 of 2: 0xc
RW 0x2000'0000 0xc <Block>
.textrw inited 0x2000'0004 0x8 ramfunc_test.o [6]
"P1": 0x443a
.text ro code 0x3000'63d0 0x1a ramfunc_test.o [6]
*******************************************************************************
*** MODULE SUMMARY
***
Module ro code rw code ro data rw data
------ ------- ------- ------- -------
ramfunc_test.o 34 8 8
*******************************************************************************
*** ENTRY LIST
***
Entry Address Size Type Object
---- ------- ---- ---- ------
ramfunc_test1 0x1 0x4 Code Gb ramfunc_test.o [6]
ramfunc_test2 0x2000'0005 0x4 Code Gb ramfunc_test.o [6]
ramfunc_test3 0x5 0x4 Code Gb ramfunc_test.o [6]
ramfunc_test4 0x2000'0009 0x4 Code Gb ramfunc_test.o [6]
至此,IAR啟動函式流程及其__low_level_init設計對函式重定向的影響痞子衡便介紹完畢了,掌聲在哪裡~~~
歡迎訂閱
文章會同時釋出到我的 部落格園主頁、CSDN主頁、知乎主頁、微信公眾號 平臺上。
微信搜尋"痞子衡嵌入式"或者掃描下面二維碼,就可以在手機上第一時間看了哦。