開(kāi)發(fā)環(huán)境:
MDK:Keil 5.30
開(kāi)發(fā)板:GD32F207I-EVAL
MCU:GD32F207IK
對(duì)于我們常用的桌面操作系統(tǒng)而言,我們?cè)陂_(kāi)發(fā)應(yīng)用時(shí),并不關(guān)心系統(tǒng)的初始化,絕大多數(shù)應(yīng)用程序是在操作系統(tǒng)運(yùn)行后才開(kāi)始運(yùn)行的,操作系統(tǒng)已經(jīng)提供了一個(gè)合適的運(yùn)行環(huán)境,然而對(duì)于嵌入式設(shè)備而言,在設(shè)備上電后,所有的一切都需要由開(kāi)發(fā)者來(lái)設(shè)置,這里處理器是沒(méi)有堆棧,沒(méi)有中斷,更沒(méi)有外圍設(shè)備,這些工作是需要軟件來(lái)指定的,而且不同的CPU類(lèi)型、不同大小的內(nèi)存和不同種類(lèi)的外設(shè),其初始化工作都是不同的。本文將以GD32F207IK (基于Cortex-M3)為例進(jìn)行講解。
在開(kāi)始正式講解之前,你需要了解ARM寄存器、匯編以及反編譯相關(guān)的知識(shí),這些可以參考筆者博文。
深入理解ARM寄存器:https://bruceou.blog.csdn.net/article/details/117866186
ARM匯編入門(mén):https://bruceou.blog.csdn.net/article/details/117897496
Keil反編譯入門(mén)(一):https://bruceou.blog.csdn.net/article/details/118314875
Keil反編譯入門(mén)(二):https://bruceou.blog.csdn.net/article/details/118400368
下面我們就來(lái)具體看一下用戶(hù)從Flash啟動(dòng)GD32的過(guò)程,主要講解從上電復(fù)位到main函數(shù)的過(guò)程。主要有以下步驟:
1.初始化堆棧指針 SP=_initial_sp,初始化 PC 指針=Reset_Handler
2.初始化中斷向量表
3.配置系統(tǒng)時(shí)鐘
4.調(diào)用 C 庫(kù)函數(shù)_main 初始化用戶(hù)堆棧,然后進(jìn)入 main 函數(shù)。
在開(kāi)始講解之前,我們需要了解GD32的啟動(dòng)模式。
1 GD32的啟動(dòng)模式
首先要講一下GD32的啟動(dòng)模式,因?yàn)閱?dòng)模式?jīng)Q定了向量表的位置,GD32有三種啟動(dòng)模式:
1)主閃存存儲(chǔ)器(Main Flash)啟動(dòng):從GD32內(nèi)置的Flash啟動(dòng)(0x0800 0000-0x0807 FFFF),一般我們使用JTAG或者SWD模式下載程序時(shí),就是下載到這個(gè)里面,重啟后也直接從這啟動(dòng)程序。以0x08000000 對(duì)應(yīng)的內(nèi)存為例,則該塊內(nèi)存既可以通過(guò)0x00000000 操作也可以通過(guò)0x08000000 操作,且都是操作的同一塊內(nèi)存。
2)系統(tǒng)存儲(chǔ)器(System Memory)啟動(dòng):從系統(tǒng)存儲(chǔ)器啟動(dòng)(0x1FFFF000 - 0x1FFF F7FF),這種模式啟動(dòng)的程序功能是由廠家設(shè)置的。一般來(lái)說(shuō),我們選用這種啟動(dòng)模式時(shí),是為了從串口下載程序,因?yàn)樵趶S家提供的ISP程序中,提供了串口下載程序的固件,可以通過(guò)這個(gè)ISP程序?qū)⒂脩?hù)程序下載到系統(tǒng)的Flash中。以0x1FFFFFF0對(duì)應(yīng)的內(nèi)存為例,則該塊內(nèi)存既可以通過(guò)0x00000000 操作也可以通過(guò)0x1FFFFFF0操作,且都是操作的同一塊內(nèi)存。
3)片上SRAM啟動(dòng):從內(nèi)置SRAM啟動(dòng)(0x2000 0000-0x3FFFFFFF),既然是SRAM,自然也就沒(méi)有程序存儲(chǔ)的能力了,這個(gè)模式一般用于程序調(diào)試。SRAM 只能通過(guò)0x20000000進(jìn)行操作,與上述兩者不同。從SRAM 啟動(dòng)時(shí),需要在應(yīng)用程序初始化代碼中重新設(shè)置向量表的位置。
用戶(hù)可以通過(guò)設(shè)置BOOT0和BOOT1的引腳電平狀態(tài),來(lái)選擇復(fù)位后的啟動(dòng)模式。如下圖所示:
啟動(dòng)模式只決定程序燒錄的位置 ,加載完程序之后會(huì)有一個(gè)重映射(映射到0x00000000地址位置);真正產(chǎn)生復(fù)位信號(hào)的時(shí)候,CPU還是從開(kāi)始位置執(zhí)行。
值得注意的是GD32上電復(fù)位以后,代碼區(qū)都是從0x00000000開(kāi)始的,三種啟動(dòng)模式只是將各自存儲(chǔ)空間的地址映射到0x00000000中。
Bootloader 存放在系統(tǒng)(System)存儲(chǔ)內(nèi),可以在 MCU 啟動(dòng)后對(duì) Flash 進(jìn)行再編程。在GD32F20x 系列產(chǎn)品中,Bootloader 通過(guò) USART0 與外界交互。
GD32F20x芯片支持嵌入式引導(dǎo)程序通過(guò)多種接口方式來(lái)更新Flash??梢杂?或2個(gè)USART端口和標(biāo)準(zhǔn)USB端口用于GD32F205xx和GD32F207xx互聯(lián)型產(chǎn)品。如下表所示。
產(chǎn)品線 | 產(chǎn)品 | 支持串行外設(shè) |
---|---|---|
互聯(lián)型 | GD32F205xx | USART0(PA9 PA10)USART1(PD5 PD6)USB(PA9 PA10 PA11 PA12) |
GD32F207xx | USART0(PA9 PA10)USART1(PD5 PD6)USB(PA9 PA10 PA11 PA12) |
2 GD32的啟動(dòng)文件分析
因?yàn)閱?dòng)過(guò)程主要是由匯編完成的,因此GD32的啟動(dòng)的大部分內(nèi)容都是在啟動(dòng)文件里。筆者的啟動(dòng)文件是startup_gd32f20x_cl.s。
2.1堆棧定義
1. Stack棧
棧的作用是用于局部變量,函數(shù)調(diào)用,函數(shù)形參等的開(kāi)銷(xiāo),棧的大小不能超過(guò)內(nèi)部SRAM 的大小。當(dāng)程序較大時(shí),需要修改棧的大小,不然可能會(huì)出現(xiàn)的HardFault的錯(cuò)誤。
第43行:表示開(kāi)辟棧的大小為 0X00000400(1KB),EQU是偽指令,相當(dāng)于C 中的 define。
第45行:開(kāi)辟一段可讀可寫(xiě)數(shù)據(jù)空間,ARER 偽指令表示下面將開(kāi)始定義一個(gè)代碼段或者數(shù)據(jù)段。此處是定義數(shù)據(jù)段。ARER 后面的關(guān)鍵字表示這個(gè)段的屬性。段名為STACK,可以任意命名;NOINIT 表示不初始化;READWRITE 表示可讀可寫(xiě),ALIGN=3,表示按照 8 字節(jié)對(duì)齊。
第46行:SPACE 用于分配大小等于 Stack_Size連續(xù)內(nèi)存空間,單位為字節(jié)。
第47行: __initial_sp表示棧頂?shù)刂?。棧是由高向低生長(zhǎng)的。
2.Heap堆
堆主要用來(lái)動(dòng)態(tài)內(nèi)存的分配,像 malloc()函數(shù)申請(qǐng)的內(nèi)存就在堆中。
開(kāi)辟堆的大小為 0X00000200(512 字節(jié)),名字為 HEAP,NOINIT 即不初始化,可讀可寫(xiě),8字節(jié)對(duì)齊。__heap_base 表示對(duì)的起始地址,__heap_limit 表示堆的結(jié)束地址。
2.2 向量表
向量表是一個(gè)WORD( 32 位整數(shù))數(shù)組,每個(gè)下標(biāo)對(duì)應(yīng)一種異常,該下標(biāo)元素的值則是該 ESR 的入口地址。向量表在地址空間中的位置是可以設(shè)置的,通過(guò) NVIC 中的一個(gè)重定位寄存器來(lái)指出向量表的地址。在復(fù)位后,該寄存器的值為 0。因此,在地址 0 (即 FLASH 地址 0)處必須包含一張向量表,用于初始時(shí)的異常分配。
值得注意的是這里有個(gè)另類(lèi):0號(hào)類(lèi)型并不是什么入口地址,而是給出了復(fù)位后 MSP 的初值,后面會(huì)具體講解。
……
第66行:定義一塊代碼段,段名字是RESET,READONLY 表示只讀。
第67-69行:使用EXPORT將3個(gè)標(biāo)識(shí)符申明為可被外部引用,聲明 __Vectors、__Vectors_End 和__Vectors_Size 具有全局屬性。這幾個(gè)變量在Keil分散加載時(shí)會(huì)用到。
第71行:__Vectors 表示向量表起始地址,DCD 表示分配 1 個(gè) 4 字節(jié)的空間。每行 DCD 都會(huì)生成一個(gè) 4 字節(jié)的二進(jìn)制代碼,中斷向量表 存放的實(shí)際上是中斷服務(wù)程序的入口地址。當(dāng)異常(也即是中斷事件)發(fā)生時(shí),CPU 的中斷系統(tǒng)會(huì)將相應(yīng)的入口地址賦值給 PC 程序計(jì)數(shù)器,之后就開(kāi)始執(zhí)行中斷服務(wù)程序。在60行之后,依次定義了中斷服務(wù)程序的入口地址。
第179行:__Vectors_End 為向量表結(jié)束地址。
第181行:__Vectors_Size則是向量表的大小,向量表的大小是通過(guò)__Vectors 和__Vectors_End 相減得到的。
上述向量表可以在《GD32F20x_User_Manual_EN_Rev2.4》中找到的,筆者這里只截取了部分。
筆者只截取了部分。
2.3 復(fù)位程序
復(fù)位程序是系統(tǒng)上電后執(zhí)行的第一個(gè)程序,復(fù)位程序也是中斷程序,只是這個(gè)程序比較特殊,因此單獨(dú)提出來(lái)講解。
第186行:定義了一個(gè)服務(wù)程序,PROC表示程序的開(kāi)始。
第187行:使用EXPORT將Reset_Handler申明為可被外部引用,后面WEAK表示弱定義,如果外部文件定義了該標(biāo)號(hào)則首先引用該標(biāo)號(hào),如果外部文件沒(méi)有聲明也不會(huì)出錯(cuò)。這里表示復(fù)位程序可以由用戶(hù)在其他文件重新實(shí)現(xiàn)。
第188-189行:表示該標(biāo)號(hào)來(lái)自外部文件,SystemInit()是一個(gè)庫(kù)函數(shù),在system_gd32f10x.c中定義的,__main 是一個(gè)標(biāo)準(zhǔn)的 C 庫(kù)函數(shù),主要作用是初始化用戶(hù)堆棧,這個(gè)是由編譯器完成的,該函數(shù)最終會(huì)調(diào)用我們自己寫(xiě)的main函數(shù),從而進(jìn)入C世界中。
第190行:這是一條匯編指令,表示從存儲(chǔ)器中加載SystemInit到一個(gè)寄存器R0的地址中。R0~R3 寄存器通常用于函數(shù)入?yún)⒊鰠⒒蜃映绦蛘{(diào)用。
第191行:匯編指令,表示跳轉(zhuǎn)到寄存器R0的地址,并根據(jù)寄存器的 LSE 確定處理器的狀態(tài),還要把跳轉(zhuǎn)前的下條指令地址保存到 LR。
第192行:和190行是一個(gè)意思,表示從存儲(chǔ)器中加載__main到一個(gè)寄存器R0的地址中。
第193行:和191稍微不同,這里跳轉(zhuǎn)到至指定寄存器的地址后,不會(huì)返回。
第194行:和PROC是對(duì)應(yīng)的,表示程序的結(jié)束。
值得注意的是,這里的__main和C語(yǔ)言中的main()不是一樣?xùn)|西,__main是C lib中的函數(shù),也就是在Keil中自帶的;而main()函數(shù)是C的入口,main()會(huì)被__main調(diào)用。
2.4 中斷服務(wù)程序
我們平時(shí)要使用哪個(gè)中斷,就需要編寫(xiě)相應(yīng)的中斷服務(wù)程序,只是啟動(dòng)文件把這些函數(shù)留出來(lái)了,但是內(nèi)容都是空的,真正的中斷復(fù)服務(wù)程序需要我們?cè)谕獠康?C 文件里面重新實(shí)現(xiàn),這里只是提前占了一個(gè)位置罷了。
這部分沒(méi)啥好說(shuō)的,和服務(wù)程序類(lèi)似的,只需要注意‘B .’語(yǔ)句,B表示跳轉(zhuǎn),這里跳轉(zhuǎn)到一個(gè)‘.’,即表示無(wú)線循環(huán)。
2.5 堆棧初始化
堆棧初始化是由一個(gè)IF條件來(lái)實(shí)現(xiàn)的,MICROLIB的定義與否決定了堆棧的初始化方式。
這個(gè)定義是在Options->Target中設(shè)置的。
如果沒(méi)有定義__MICROLIB , 則會(huì)使用雙段存儲(chǔ)器模式,且聲明了__user_initial_stackheap 具有全局屬性,這需要開(kāi)發(fā)者自己來(lái)初始化堆棧。
這部分也沒(méi)啥講的,需要注意的是,ALIGN表示對(duì)指令或者數(shù)據(jù)存放的地址進(jìn)行對(duì)齊,缺省表示4字節(jié)對(duì)齊。
2.6 其他
第62行:PRESERVE8 用于指定當(dāng)前文件的堆棧按照 8 字節(jié)對(duì)齊。
第63行:THUMB 表示后面指令兼容 THUMB 指令。現(xiàn)在 Cortex-M 系列的都使用 THUMB-2 指令集,THUMB-2 是 32 位的,兼容 16 位和 32 位的指令,是 THUMB 的超集。
3 GD32的啟動(dòng)流程實(shí)例分析
有了前面的分析,接下來(lái)就來(lái)具體看看GD32啟動(dòng)流程的具體內(nèi)容。
3.1初始化SP、PC、向量表
當(dāng)系統(tǒng)復(fù)位后,處理器首先讀取向量表中的前兩個(gè)字(8 個(gè)字節(jié)),第一個(gè)字存入 MSP,第二個(gè)字為復(fù)位向量,也就是程序執(zhí)行的起始地址。
這里通過(guò)J-Flash打開(kāi)hex文件。
硬件這時(shí)自動(dòng)從0x0800 0000位置處讀取數(shù)據(jù)賦給棧指針SP,然后自動(dòng)從0x0800 0004位置處讀取數(shù)據(jù)賦給PC,完成了復(fù)位操作,SP= 0x0200 2008,PC = 0x0800 01BD。
初始化SP、PC緊接著就初始化向量表,如果感覺(jué)看HEX文件抽象,我們看看反匯編文件吧。
是不是更容易些,是不是和《GD32F20x_User_Manual_EN_Rev2.4》中的向量表對(duì)應(yīng)起來(lái)了。其實(shí)看反匯編文件更好理解GD32的啟動(dòng)流程,只是有些抽象。
生成反匯編的方法如下。
在KEIL的User選項(xiàng)中,如下圖添加這兩項(xiàng):
fromelf --bin --output=GD32F207I_EVAL.bin ../Output/GD32F207I_EVAL.axf
fromelf --text -a -c --output=GD32F207I_EVAL.dis ../Output/GD32F207I_EVAL.axf
然后重新編譯,即可得到二進(jìn)制文件GD32F207I_EVAL.bin(以后會(huì)分析)、反匯編文件GD32F207I_EVAL.dis。
如下圖所示:
3.2 設(shè)置系統(tǒng)時(shí)鐘
細(xì)心的朋友可能發(fā)現(xiàn),PC=0x080001BD,這里表明MCU運(yùn)行的是THUMB模式,最低位為1表示為T(mén)HUMB狀態(tài)。然后在反匯編文件中卻是這樣的:
當(dāng)然也可通過(guò)硬件調(diào)試來(lái)確認(rèn)上面的分析:
接下來(lái)就會(huì)進(jìn)入SystemInit函數(shù)中。
SystemInit函數(shù)內(nèi)容如下:
/*!
\\\\\\\\brief setup the micro-controller system, initialize the system
\\\\\\\\param[in] none
\\\\\\\\param[out] none
\\\\\\\\retval none
*/
void SystemInit(void)
{
/* reset the RCC clock configuration to the default reset state */
/* enable IRC8M */
RCU_CTL |= RCU_CTL_IRC8MEN;
RCU_CFG0 &= ~RCU_CFG0_SCS;
/* reset HXTALEN, CKMEN, PLLEN bits */
RCU_CTL &= ~(RCU_CTL_HXTALEN | RCU_CTL_CKMEN | RCU_CTL_PLLEN);
/* reset SCS, AHBPSC, APB1PSC, APB2PSC, ADCPSC, CKOUT0SEL bits */
RCU_CFG0 &= ~(RCU_CFG0_SCS | RCU_CFG0_AHBPSC | RCU_CFG0_APB1PSC | RCU_CFG0_APB2PSC |
RCU_CFG0_ADCPSC | RCU_CFG0_ADCPSC_2 | RCU_CFG0_CKOUT0SEL);
/* reset HXTALEN, CKMEN, PLLEN bits */
RCU_CTL &= ~(RCU_CTL_HXTALEN | RCU_CTL_CKMEN | RCU_CTL_PLLEN);
/* Reset HXTALBPS bit */
RCU_CTL &= ~(RCU_CTL_HXTALBPS);
/* reset PLLSEL, PREDV0_LSB, PLLMF, USBFSPSC bits */
RCU_CFG0 &= ~(RCU_CFG0_PLLSEL | RCU_CFG0_PREDV0_LSB | RCU_CFG0_PLLMF |
RCU_CFG0_USBFSPSC | RCU_CFG0_PLLMF_4);
/* reset PLL1EN and PLL2EN bits */
RCU_CTL &= ~(RCU_CTL_PLL1EN | RCU_CTL_PLL2EN);
/* reset CFG1 register */
RCU_CFG1 = 0x00000000U;
/* reset INT register */
RCU_INT = 0x00FF0000U;
/* reset CFG2 register */
RCU_CFG2 = 0x00000000U;
/* reset PLLTCTL register */
RCU_PLLTCTL &= (~RCU_PLLTCTL_PLLTEN);
/* reset PLLTINT register */
RCU_PLLTINT = 0x00400000U;
/* reset PLLTCFG register */
RCU_PLLTCFG = 0x20003010U;
/* configure the system clock source, PLL multiplier, AHB/APBx prescalers and flash settings */
system_clock_config();
}
前面部分是配置時(shí)鐘的,具體參考手冊(cè)吧。
/*!
\\\\\\\\brief configure the system clock to 120M by PLL which selects HXTAL(8M) as its clock source
\\\\\\\\param[in] none
\\\\\\\\param[out] none
\\\\\\\\retval none
*/
static void system_clock_120m_hxtal(void)
{
uint32_t timeout = 0U;
uint32_t stab_flag = 0U;
/* enable HXTAL */
RCU_CTL |= RCU_CTL_HXTALEN;
/* wait until HXTAL is stable or the startup time is longer than HXTAL_STARTUP_TIMEOUT */
do {
timeout++;
stab_flag = (RCU_CTL & RCU_CTL_HXTALSTB);
} while((0U == stab_flag) && (HXTAL_STARTUP_TIMEOUT != timeout));
/* if fail */
if(0U == (RCU_CTL & RCU_CTL_HXTALSTB)) {
while(1) {
}
}
/* HXTAL is stable */
/* AHB = SYSCLK */
RCU_CFG0 |= RCU_AHB_CKSYS_DIV1;
/* APB2 = AHB/1 */
RCU_CFG0 |= RCU_APB2_CKAHB_DIV1;
/* APB1 = AHB/2 */
RCU_CFG0 |= RCU_APB1_CKAHB_DIV2;
/* CK_PLL = (CK_PREDIV0) * 10 = 120 MHz */
RCU_CFG0 &= ~(RCU_CFG0_PLLMF | RCU_CFG0_PLLMF_4 | RCU_CFG0_PREDV0_LSB | RCU_CFG0_PLLSEL);
RCU_CFG0 |= (RCU_PLLSRC_HXTAL | RCU_PLL_MUL10);
/* CK_PREDIV0 = (CK_HXTAL) / 5 * 12 /5 = 12 MHz */
RCU_CFG1 &= ~(RCU_CFG1_PREDV0SEL | RCU_CFG1_PLL1MF | RCU_CFG1_PREDV1 | RCU_CFG1_PREDV0);
RCU_CFG1 |= (RCU_PREDV0SRC_CKPLL1 | RCU_PLL1_MUL12 | RCU_PREDV1_DIV5 | RCU_PREDV0_DIV5);
/* enable PLL1 */
RCU_CTL |= RCU_CTL_PLL1EN;
/* wait till PLL1 is ready */
while((RCU_CTL & RCU_CTL_PLL1STB) == 0U) {
}
/* enable PLL */
RCU_CTL |= RCU_CTL_PLLEN;
/* wait until PLL is stable */
while(0U == (RCU_CTL & RCU_CTL_PLLSTB)) {
}
/* select PLL as system clock */
RCU_CFG0 &= ~RCU_CFG0_SCS;
RCU_CFG0 |= RCU_CKSYSSRC_PLL;
/* wait until PLL is selected as system clock */
while(0U == (RCU_CFG0 & RCU_SCSS_PLL)) {
}
}
3.3 初始化堆棧并進(jìn)入main
執(zhí)行指令LDR R0, =__main,然后就跳轉(zhuǎn)到__main程序段運(yùn)行,當(dāng)然這里指標(biāo)準(zhǔn)庫(kù)的__main函數(shù)。
這中間初始化了棧區(qū)。
這段代碼是個(gè)循環(huán)(BCC 0x080001e6),實(shí)際運(yùn)行時(shí)候循環(huán)了兩次。第一次運(yùn)行的時(shí)候,讀取“加載數(shù)據(jù)段的函數(shù)”的地址并跳轉(zhuǎn)到該函數(shù)處運(yùn)行(注意加載已初始化數(shù)據(jù)段和未初始化數(shù)據(jù)段用的是同一個(gè)函數(shù));第二次運(yùn)行的時(shí)候,讀取“初始化棧的函數(shù)”的地址并跳轉(zhuǎn)到該函數(shù)處運(yùn)行。
最后就進(jìn)入C文件的main函數(shù)中,至此,啟動(dòng)過(guò)程到此結(jié)束。
最后,總結(jié)下GD32 從flash的啟動(dòng)流程。
MCU上電后從0x0800 0000處讀取棧頂?shù)刂凡⒈4?,然后?x0800 0004讀取中斷向量表的起始地址,這就是復(fù)位程序的入口地址,接著跳轉(zhuǎn)到復(fù)位程序入口處,初始向量表,然后設(shè)置時(shí)鐘,設(shè)置堆棧,最后跳轉(zhuǎn)到C空間的main函數(shù),即進(jìn)入用戶(hù)程序。
-
mcu
+關(guān)注
關(guān)注
146文章
17148瀏覽量
351197 -
開(kāi)發(fā)板
+關(guān)注
關(guān)注
25文章
5050瀏覽量
97471 -
keil
+關(guān)注
關(guān)注
68文章
1213瀏覽量
166878 -
Cortex-M
+關(guān)注
關(guān)注
2文章
229瀏覽量
29763 -
GD32
+關(guān)注
關(guān)注
7文章
403瀏覽量
24351
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論