本文以Arm Cortex-M為例,介紹了在IAR Embedded Workbench中微控制器(MCU)的啟動過程。在MCU復位后,程序計數(shù)器(PC)會指向相應的復位向量,并開始執(zhí)行啟動代碼(startup code)。如果MCU支持浮點單元(FPU),則在啟動過程中,首先會調(diào)用__iar_init_vfp來初始化FPU,然后繼續(xù)執(zhí)行__iar_program_start。接著,__iar_program_start會調(diào)用__cmain函數(shù)。在__cmain中,會先調(diào)用__low_level_init函數(shù),然后調(diào)用__iar_data_init3來進行全局和靜態(tài)變量的初始化。在__iar_data_init3中,首先會調(diào)用__iar_zero_init3來初始化初始值為0的全局和靜態(tài)變量,隨后會調(diào)用__iar_copy_init3來初始化初始值為非0的全局和靜態(tài)變量。最終,在啟動過程的最后階段,會通過調(diào)用__call_main來跳轉(zhuǎn)到main函數(shù),從而開始執(zhí)行主程序。
MCU啟動過程
MCU啟動過程指的是從MCU復位到main函數(shù)之前的過程。
當MCU復位之后,MCU會從對應的復位向量開始運行,初始化Stack pointer指向指定Stack區(qū)域的末尾,然后調(diào)用__low_level_init函數(shù)進行相關的初始化。
(在微控制器(Microcontroller,縮寫為MCU)中,復位向量(Reset Vector)是一個特殊的內(nèi)存地址,用于指示MCU在復位或啟動時應該開始執(zhí)行的第一條指令。當MCU發(fā)生復位事件(如上電復位、外部復位、看門狗定時器復位等)時,它會將程序計數(shù)器(PC)設置為復位向量的地址,從而開始執(zhí)行存儲在這個地址上的指令。
復位向量通常位于MCU的存儲器中的固定位置,通常是在芯片的起始位置。這確保了在復位時能夠始終從相同的地址開始執(zhí)行,從而確??煽康南到y(tǒng)啟動。
復位向量的內(nèi)容可以是任何有效的機器指令,通常是一條跳轉(zhuǎn)指令(比如跳轉(zhuǎn)到主程序的入口點),以便MCU能夠開始執(zhí)行實際的應用程序代碼。
總之,復位向量是一個重要的概念,它確保了在MCU復位時,程序能夠從可控的、確定的位置開始執(zhí)行,從而使系統(tǒng)能夠正常啟動并運行。)
接下來是全局和靜態(tài)變量的初始化:初始值為0的變量對應的RAM區(qū)域會清零,初始值為非0的變量,會從ROM拷貝到RAM(注意:如果__low_level_init函數(shù)返回0,這一步將會跳過)。
然后是C++動態(tài)初始化:構造靜態(tài) C++ 對象,最后會調(diào)用main函數(shù)。
更具體一點:
當MCU復位之后,PC指針會指向?qū)膹臀幌蛄?,然后運行對應的啟動代碼(startup code),啟動代碼首先會初始化Stack pointer指向指定Stack區(qū)域的末尾。
然后初始化初始值為0的存儲在RAM中的全局和靜態(tài)變量(比如 int i = 0;):
初始化初始值為非0的存儲在RAM中的全局和靜態(tài)變量(比如 int i = 1;),對應的初始值從相應的ROM拷貝到對應的RAM:
最后,調(diào)用main函數(shù):
啟動代碼
通常情況下,如果ICF文件中添加了initialize by copy 命令,linker會自動選擇并添加對應的啟動代碼來完成對應的啟動過程。對應的啟動代碼通過庫文件的方式進行l(wèi)ink。對應的啟動代碼在安裝目錄armsrclib下面:
armsrclibthumbcstartup_M.s (__iar_program_start)
armsrclibthumbcmain.s (__cmain,__call_main)
armsrclibruntimelow_level_init.c (__low_level_init)
armsrclibruntimedata_init.c (__iar_data_init3)
armsrclibruntimezero_init3.c (__iar_zero_init3)
armsrclibruntimecopy_init3.c (__iar_copy_init3)
對應的啟動代碼和相關文件信息會在map文件里面列出來:
同時map文件里面INIT TABLE章節(jié)會列出對應的全局和靜態(tài)變量的初始化信息:初始值為0的會使用__iar_zero_init3進行初始化,初始值為非0的會使用__iar_copy_init3進行初始化:
調(diào)試
為了能夠調(diào)試查看對應的啟動代碼和啟動過程,需要配置Debugger選項里面的Run to,即不要勾選Run to,這樣調(diào)試的時候復位之后PC會停在復位向量而不是main函數(shù),然后就可以調(diào)試對應的啟動代碼和啟動過程。
復位之后,PC會停在復位向量Reset_Handler,Reset_Handler首先會調(diào)用SystemInit函數(shù)進行相關的配置和初始化(這個是Cortex-M CMSIS的標準),然后會調(diào)用__iar_program_start:
如果對應的MCU有FPU,__iar_program_start首先會調(diào)用__iar_init_vfp對FPU進行初始化:
然后__iar_program_start會調(diào)用__cmain:
__cmain首先會調(diào)用__low_level_init(默認實現(xiàn)為空,僅返回 1):
__cmain然后會調(diào)用__iar_data_init3進行全局和靜態(tài)變量的初始化:
__iar_data_init3首先會調(diào)用__iar_zero_init3進行初始值為0的全局和靜態(tài)變量的初始化:
__iar_data_init3然后會調(diào)用__iar_copy_init3進行初始值為非0的全局和靜態(tài)變量的初始化:
最后__call_main會調(diào)用main函數(shù)跳轉(zhuǎn)到main函數(shù):
至此MCU從復位向量開始,運行啟動代碼之后就跳轉(zhuǎn)到main函數(shù),然后開始運行用戶的代碼:
注意事項
Cortex-M的MSP賦值是通過硬件自動操作完成的,在復位后會從中斷向量表的0地址偏移處獲取值并賦給MSP寄存器。因此,上述啟動代碼和啟動過程中并未顯式體現(xiàn)這一步驟。然而,若需要手動對MSP進行賦值(例如在bootloader跳轉(zhuǎn)到application時需要手動為application設置MSP值),則需要在啟動代碼的起始部分執(zhí)行這一操作。
IAR默認的啟動代碼是在鏈接(link)過程中由鏈接器自動添加的。如果需要手動進行MSP賦值等操作,這些代碼可以在啟動代碼的最開始部分進行添加。此外,為了支持這種操作,需要在ICF(IAR Configuration File)文件中添加"initialize by copy"命令。
對于初始化操作,用戶可以通過實現(xiàn)__low_level_init函數(shù)來進行。特別是對于支持ECC(Error Correction Code)機制的MCU的RAM,需要在__low_level_init函數(shù)中根據(jù)ECC的位寬對RAM區(qū)域進行一次寫操作,以避免后續(xù)RAM操作引發(fā)ECC錯誤。需要注意的是,__low_level_init函數(shù)在全局和靜態(tài)變量初始化之前執(zhí)行,因此其中不能使用這些全局和靜態(tài)變量。此外,__low_level_init函數(shù)的返回值決定是否需要對全局和靜態(tài)變量進行初始化,返回1表示需要初始化,返回0表示不需要初始化。
在IAR中,__iar_program_start是默認的程序開始標簽。如果代碼中使用了其他程序開始標簽,可以通過鏈接器選項--entry來指定相應的程序開始標簽。
總結
本文以Arm Cortex-M為例,介紹了在IAR Embedded Workbench中微控制器(MCU)的啟動過程。在MCU復位后,程序計數(shù)器(PC)會指向相應的復位向量,并開始執(zhí)行啟動代碼(startup code)。如果MCU支持浮點單元(FPU),則在啟動過程中,首先會調(diào)用__iar_init_vfp來初始化FPU,然后繼續(xù)執(zhí)行__iar_program_start。接著,__iar_program_start會調(diào)用__cmain函數(shù)。在__cmain中,會先調(diào)用__low_level_init函數(shù),然后調(diào)用__iar_data_init3來進行全局和靜態(tài)變量的初始化。在__iar_data_init3中,首先會調(diào)用__iar_zero_init3來初始化初始值為0的全局和靜態(tài)變量,隨后會調(diào)用__iar_copy_init3來初始化初始值為非0的全局和靜態(tài)變量。最終,在啟動過程的最后階段,會通過調(diào)用__call_main來跳轉(zhuǎn)到main函數(shù),從而開始執(zhí)行主程序。