开发环境:
mdk:keil 5.30
开发板:gd32f207i-eval
mcu:gd32f207ik
对于我们常用的桌面操作系统而言,我们在开发应用时,并不关心系统的初始化,绝大多数应用程序是在操作系统运行后才开始运行的,操作系统已经提供了一个合适的运行环境,然而对于嵌入式设备而言,在设备上电后,所有的一切都需要由开发者来设置,这里处理器是没有堆栈,没有中断,更没有外围设备,这些工作是需要软件来指定的,而且不同的cpu类型、不同大小的内存和不同种类的外设,其初始化工作都是不同的。本文将以gd32f207ik (基于cortex-m3)为例进行讲解。
在开始正式讲解之前,你需要了解arm寄存器、汇编以及反编译相关的知识,这些可以参考笔者博文。
深入理解arm寄存器:https://bruceou.blog.csdn.net/article/details/117866186
arm汇编入门:https://bruceou.blog.csdn.net/article/details/117897496
keil反编译入门(一):https://bruceou.blog.csdn.net/article/details/118314875
keil反编译入门(二):https://bruceou.blog.csdn.net/article/details/118400368
下面我们就来具体看一下用户从flash启动gd32的过程,主要讲解从上电复位到main函数的过程。主要有以下步骤:
1.初始化堆栈指针 sp=_initial_sp,初始化 pc 指针=reset_handler
2.初始化中断向量表
3.配置系统时钟
4.调用 c 库函数_main 初始化用户堆栈,然后进入 main 函数。
在开始讲解之前,我们需要了解gd32的启动模式。
1 gd32的启动模式首先要讲一下gd32的启动模式,因为启动模式决定了向量表的位置,gd32有三种启动模式:
1)主闪存存储器(main flash)启动:从gd32内置的flash启动(0x0800 0000-0x0807 ffff),一般我们使用jtag或者swd模式下载程序时,就是下载到这个里面,重启后也直接从这启动程序。以0x08000000 对应的内存为例,则该块内存既可以通过0x00000000 操作也可以通过0x08000000 操作,且都是操作的同一块内存。
2)系统存储器(system memory)启动:从系统存储器启动(0x1ffff000 - 0x1fff f7ff),这种模式启动的程序功能是由厂家设置的。一般来说,我们选用这种启动模式时,是为了从串口下载程序,因为在厂家提供的isp程序中,提供了串口下载程序的固件,可以通过这个isp程序将用户程序下载到系统的flash中。以0x1ffffff0对应的内存为例,则该块内存既可以通过0x00000000 操作也可以通过0x1ffffff0操作,且都是操作的同一块内存。
3)片上sram启动:从内置sram启动(0x2000 0000-0x3fffffff),既然是sram,自然也就没有程序存储的能力了,这个模式一般用于程序调试。sram 只能通过0x20000000进行操作,与上述两者不同。从sram 启动时,需要在应用程序初始化代码中重新设置向量表的位置。
用户可以通过设置boot0和boot1的引脚电平状态,来选择复位后的启动模式。如下图所示:
启动模式只决定程序烧录的位置 ,加载完程序之后会有一个重映射(映射到0x00000000地址位置);真正产生复位信号的时候,cpu还是从开始位置执行。
值得注意的是gd32上电复位以后,代码区都是从0x00000000开始的,三种启动模式只是将各自存储空间的地址映射到0x00000000中。
bootloader 存放在系统(system)存储内,可以在 mcu 启动后对 flash 进行再编程。在gd32f20x 系列产品中,bootloader 通过 usart0 与外界交互。
gd32f20x芯片支持嵌入式引导程序通过多种接口方式来更新flash。可以有1或2个usart端口和标准usb端口用于gd32f205xx和gd32f207xx互联型产品。如下表所示。
产品线产品支持串行外设
互联型 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的启动文件分析因为启动过程主要是由汇编完成的,因此gd32的启动的大部分内容都是在启动文件里。笔者的启动文件是startup_gd32f20x_cl.s。
2.1堆栈定义1. stack栈
栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部sram 的大小。当程序较大时,需要修改栈的大小,不然可能会出现的hardfault的错误。
第43行:表示开辟栈的大小为 0x00000400(1kb),equ是伪指令,相当于c 中的 define。
第45行:开辟一段可读可写数据空间,arer 伪指令表示下面将开始定义一个代码段或者数据段。此处是定义数据段。arer 后面的关键字表示这个段的属性。段名为stack,可以任意命名;noinit 表示不初始化;readwrite 表示可读可写,align=3,表示按照 8 字节对齐。
第46行:space 用于分配大小等于 stack_size连续内存空间,单位为字节。
第47行: __initial_sp表示栈顶地址。栈是由高向低生长的。
2.heap堆
堆主要用来动态内存的分配,像 malloc()函数申请的内存就在堆中。
开辟堆的大小为 0x00000200(512 字节),名字为 heap,noinit 即不初始化,可读可写,8字节对齐。__heap_base 表示对的起始地址,__heap_limit 表示堆的结束地址。
2.2 向量表向量表是一个word( 32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 esr 的入口地址。向量表在地址空间中的位置是可以设置的,通过 nvic 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 flash 地址 0)处必须包含一张向量表,用于初始时的异常分配。
值得注意的是这里有个另类:0号类型并不是什么入口地址,而是给出了复位后 msp 的初值,后面会具体讲解。
……
第66行:定义一块代码段,段名字是reset,readonly 表示只读。
第67-69行:使用export将3个标识符申明为可被外部引用,声明 __vectors、__vectors_end 和__vectors_size 具有全局属性。这几个变量在keil分散加载时会用到。
第71行:__vectors 表示向量表起始地址,dcd 表示分配 1 个 4 字节的空间。每行 dcd 都会生成一个 4 字节的二进制代码,中断向量表 存放的实际上是中断服务程序的入口地址。当异常(也即是中断事件)发生时,cpu 的中断系统会将相应的入口地址赋值给 pc 程序计数器,之后就开始执行中断服务程序。在60行之后,依次定义了中断服务程序的入口地址。
第179行:__vectors_end 为向量表结束地址。
第181行:__vectors_size则是向量表的大小,向量表的大小是通过__vectors 和__vectors_end 相减得到的。
上述向量表可以在《gd32f20x_user_manual_en_rev2.4》中找到的,笔者这里只截取了部分。
笔者只截取了部分。
2.3 复位程序复位程序是系统上电后执行的第一个程序,复位程序也是中断程序,只是这个程序比较特殊,因此单独提出来讲解。
第186行:定义了一个服务程序,proc表示程序的开始。
第187行:使用export将reset_handler申明为可被外部引用,后面weak表示弱定义,如果外部文件定义了该标号则首先引用该标号,如果外部文件没有声明也不会出错。这里表示复位程序可以由用户在其他文件重新实现。
第188-189行:表示该标号来自外部文件,systeminit()是一个库函数,在system_gd32f10x.c中定义的,__main 是一个标准的 c 库函数,主要作用是初始化用户堆栈,这个是由编译器完成的,该函数最终会调用我们自己写的main函数,从而进入c世界中。
第190行:这是一条汇编指令,表示从存储器中加载systeminit到一个寄存器r0的地址中。r0~r3 寄存器通常用于函数入参出参或子程序调用。
第191行:汇编指令,表示跳转到寄存器r0的地址,并根据寄存器的 lse 确定处理器的状态,还要把跳转前的下条指令地址保存到 lr。
第192行:和190行是一个意思,表示从存储器中加载__main到一个寄存器r0的地址中。
第193行:和191稍微不同,这里跳转到至指定寄存器的地址后,不会返回。
第194行:和proc是对应的,表示程序的结束。
值得注意的是,这里的__main和c语言中的main()不是一样东西,__main是c lib中的函数,也就是在keil中自带的;而main()函数是c的入口,main()会被__main调用。
2.4 中断服务程序我们平时要使用哪个中断,就需要编写相应的中断服务程序,只是启动文件把这些函数留出来了,但是内容都是空的,真正的中断复服务程序需要我们在外部的 c 文件里面重新实现,这里只是提前占了一个位置罢了。
这部分没啥好说的,和服务程序类似的,只需要注意‘b .’语句,b表示跳转,这里跳转到一个‘.’,即表示无线循环。
2.5 堆栈初始化堆栈初始化是由一个if条件来实现的,microlib的定义与否决定了堆栈的初始化方式。
这个定义是在options->target中设置的。
如果没有定义__microlib , 则会使用双段存储器模式,且声明了__user_initial_stackheap 具有全局属性,这需要开发者自己来初始化堆栈。
这部分也没啥讲的,需要注意的是,align表示对指令或者数据存放的地址进行对齐,缺省表示4字节对齐。
2.6 其他
第62行:preserve8 用于指定当前文件的堆栈按照 8 字节对齐。
第63行:thumb 表示后面指令兼容 thumb 指令。现在 cortex-m 系列的都使用 thumb-2 指令集,thumb-2 是 32 位的,兼容 16 位和 32 位的指令,是 thumb 的超集。
3 gd32的启动流程实例分析有了前面的分析,接下来就来具体看看gd32启动流程的具体内容。
3.1初始化sp、pc、向量表当系统复位后,处理器首先读取向量表中的前两个字(8 个字节),第一个字存入 msp,第二个字为复位向量,也就是程序执行的起始地址。
这里通过j-flash打开hex文件。
硬件这时自动从0x0800 0000位置处读取数据赋给栈指针sp,然后自动从0x0800 0004位置处读取数据赋给pc,完成了复位操作,sp= 0x0200 2008,pc = 0x0800 01bd。
初始化sp、pc紧接着就初始化向量表,如果感觉看hex文件抽象,我们看看反汇编文件吧。
是不是更容易些,是不是和《gd32f20x_user_manual_en_rev2.4》中的向量表对应起来了。其实看反汇编文件更好理解gd32的启动流程,只是有些抽象。
生成反汇编的方法如下。
在keil的user选项中,如下图添加这两项:
fromelf --bin --output=gd32f207i_eval.bin ../output/gd32f207i_eval.axf
fromelf --text -a -c --output=gd32f207i_eval.dis ../output/gd32f207i_eval.axf
然后重新编译,即可得到二进制文件gd32f207i_eval.bin(以后会分析)、反汇编文件gd32f207i_eval.dis。
如下图所示:
3.2 设置系统时钟细心的朋友可能发现,pc=0x080001bd,这里表明mcu运行的是thumb模式,最低位为1表示为thumb状态。然后在反汇编文件中却是这样的:
当然也可通过硬件调试来确认上面的分析:
接下来就会进入systeminit函数中。
systeminit函数内容如下:
/*! \\\\\\\\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();}前面部分是配置时钟的,具体参考手册吧。
/*! \\\\\\\\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 初始化堆栈并进入main执行指令ldr r0, =__main,然后就跳转到__main程序段运行,当然这里指标准库的__main函数。
这中间初始化了栈区。
这段代码是个循环(bcc 0x080001e6),实际运行时候循环了两次。第一次运行的时候,读取“加载数据段的函数”的地址并跳转到该函数处运行(注意加载已初始化数据段和未初始化数据段用的是同一个函数);第二次运行的时候,读取“初始化栈的函数”的地址并跳转到该函数处运行。
最后就进入c文件的main函数中,至此,启动过程到此结束。
最后,总结下gd32 从flash的启动流程。
mcu上电后从0x0800 0000处读取栈顶地址并保存,然后从0x0800 0004读取中断向量表的起始地址,这就是复位程序的入口地址,接着跳转到复位程序入口处,初始向量表,然后设置时钟,设置堆栈,最后跳转到c空间的main函数,即进入用户程序。
新一代印制电路板的核心技术是什么?
!!销售/收购! HP8657B HP8657B 信号源 H
RT9173D应用电路及参数资料
讯飞扫描词典笔获ZOL年度推荐产品奖:英语学习烦恼,一笔横扫
Home键消失,面容ID将至_iPad Pro该如何进行人机交互
GD32开发实战指南(基础篇) 第4章 GD32启动流程详解(Keil版)
洗浆机轴封位磨损原因及修复方法
四个腿的电感是什么?有什么作用?
Redmi Note11系列今晚发布!参数配置详情曝光
TI推出65nm工艺MCU两款新产品AM387x和StellarisLM4F×
深度解析华为“很吓人的技术”:GPU Turbo
Wi-Fi网络可以成为大学里一个强大的联系追踪工具
智慧酒店有人工智能的加入会有什么效果
数据规模拓展无极限,2023亚马逊云科技re:Invent大会首日精彩内容
油电混合无人机在救灾领域起到的重要作用
智慧城市是变革的主要机遇
“安卓之父”创业失败 主要原因是他没有找对智能手机的发展方向
AI在医疗领域有哪些应用?
直通HDC2022 | 使能智慧金融终端产业化,润和软件发布OpenHarmony金融行业重要成果
I/O电缆辐射发射问题的技术分析