bootloader嵌入式芯片启动过程全解析
参考文章
什么是Bootloader
在嵌入式操作系统中,BootLoader是在操作系统内核运行之前运行。可以初始化硬件设备、建立内存空间映射图,从而将系统的软硬件环境带到一个合适状态,以便为最终调用操作系统内核准备好正确的环境。
在嵌入式系统中,通常并没有像BIOS那样的固件程序(注,有的嵌入式CPU也会内嵌一段短小的启动程序),因此整个系统的加载启动任务就完全由BootLoader来完成。
BootLoader 起到了桥梁的作用,连接了硬件启动与高级软件运行之间的环节,确保系统能够从一个初始、裸机的状态过渡到一个完整的、可操作的运行环境。
两种Bootloader
MCU的Bootloader和嵌入式linux或pc有所不同,与不同芯片才用的存储架构有关
-
MCU
MCU一般为单核或多核同构,主频小于1GHZ,也没有MMU内存管理单元,最多只能运行像FreeRTOS等的实时操作系统
MCU下程序运行的主要介质是NOR_FLASH(传统存储器单元,支持随机访问),与RAM一样有分离的地址线和数据线,以字节长度精确寻址。 -
linux SOC
嵌入式linux的SOC一般将他的操作系统、文件系统和应用程序存放在nand flash(NAND Flash的内部结构更适合大容量、顺序读写的应用场景。它采用页和块的结构,通常需要使用控制器来管理读写操作。)
在处理器运行代码时,先从nand中到sram内存中,比MCU的多了一步。
本文以GD32单片机进行BootLoader的配置,后边会逐渐补上
Bootloader的作用
- MCU中
- 关闭看门狗,初始化中断和trap向量表,进行时钟和外设初始化,让芯片正常运行起来。
- 提供CAN、UART、ETH等用于通讯功能的驱动,能够接收外部数据传输请求
- 提供FLASH的读写与擦除驱动,设计服务来对通讯端口接收到的更新代码进行校验、存储,以及跳转操作系统或后续应用程序代码。
- 如有必要,还会开发一些基础诊断服务,串口交互程序等等。
- SoC
- 硬件初始化
- 内存管理
- 引导加载操作系统
- 从非易失性存储器(如Flash、EEPROM、NAND/NOR Flash等)中读取并验证操作系统的内核映像。
- 将内核映像加载到RAM中指定的位置,并按照内核所需的特定格式设置启动参数和环境变量。
- 固件升级
- 系统诊断与恢复
- 多重引导支持
在某些系统中,BootLoader 可能支持选择加载不同的操作系统版本或应用程序,提供多启动选项,增强系统的灵活性和可定制性。
单片机的程序(指令)生成
交叉编译会将我们的c代码编译成二进制指令,然后编译器会将这些二进制指令烧录到单片机的程序存储区,cpu会去一条条的读取指令执行。
指令存放的位置
下面就是我在梁山派gd32F470用户手册里找的一张图片,因为我们将程序(指令)写到单片机中,必须是烧录到固定的位置里面。这样的话单片机才能去找到这些指令并执行以完成我们需要的功能。
烧录进单片机的程序是有固定位置的,并且程序存储区是有固定的大小,所以当生成的程序合适时,程序存储区就可以同时存放几段程序代码。
在理论上来说,只需要确定好boot程序和app程序的大小,给他们在ROM内分配合适的区块。在需要进行代码跳转的时候,做好中断向量表重定向。就可以在一个单片机中存放多块不同的程序段。
GD32的启动模式
启动模式只决定程序烧录的位置,加载完程序之后会有一个重映射(映射到0x00000000地址位置);真正产生复位信号的时候,CPU还是从开始位置执行。
- 初始化堆栈指针 SP=_initial_sp,初始化 PC 指针=Reset_Handler
- 初始化中断向量表
- 配置系统时钟
- 调用 C 库函数_main 初始化用户堆栈,然后进入 main 函数。
- 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的引脚电平状态,来选择复位后的启动模式。如下图所示:
GD32上电复位以后,代码区都是从0x00000000开始的,三种启动模式只是将各自存储空间的地址映射到0x00000000中。
启动文件分析
栈定义
栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部SRAM 的大小。当程序较大时,需要修改栈的大小,不然可能会出现的HardFault的错误。
Stack_Size 0x00000400 |
堆的定义
堆主要用来动态内存的分配,像 malloc()函数申请的内存就在堆中。
Heap_Size 0x00000400 |
向量表
向量表是一个WORD( 32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0)处必须包含一张向量表,用于初始时的异常分配。
RESET, , |
复位程序
复位程序是系统上电后执行的第一个程序,复位程序也是中断程序
Reset_Handler |
这里的__main和C语言中的main()不是一样东西,__main是C lib中的函数,也就是在Keil中自带的;而main()函数是C的入口,main()会被__main调用。
中断服务程序
我们平时要使用哪个中断,就需要编写相应的中断服务程序,只是启动文件把这些函数留出来了,但是内容都是空的,真正的中断复服务程序需要我们在外部的 C 文件里面重新实现,这里只是提前占了一个位置罢了。
NMI_Handler |
GD32的启动流程分析
与前面分析的一致
- 初始化SP、PC、向量表
- 设置系统时钟,接下来就会进入SystemInit函数中。
- 初始化堆栈并进入main
MCU上电后从0x0800 0000处读取栈顶地址并保存,然后从0x0800 0004读取中断向量表的起始地址,这就是复位程序的入口地址,接着跳转到复位程序入口处,初始向量表,然后设置时钟,设置堆栈,最后跳转到C空间的main函数,即进入用户程序。
BootLoader和APP的关系
上面讲了MCU的整个启动的流程,无论是BootLoader还是APP都必须要按照上面的流程进行启动,只是APP的运行需要在BootLoader中进行跳转(其实这种跳转就是走楼梯一级一级上),即在BootLoader对MSP和PC进行重新赋值成APP.bin文件中的参数。
- BootLoader:验证下载新固件完整性,从固件备份区拷贝新固件数据到APP区,跳转到APP中;
- APP:业务应用程序设计,下载新固件到备份区(APP_back)并复位。
程序的启动,在bootloader引导程序中,我们会启用标志位,每个app对应一个标志位,如果判断成功就跳转到相应的地址去启动相应的程序。