1 什么是bootloader?
Bootloader,也被称为引导加载程序,是操作系统启动过程中的一个重要组成部分。它是存储在非易失性存储器中的一段小程序,负责在操作系统内核运行之前加载并启动一些必要的系统组件。
当计算机开机后,BIOS(基本输入/输出系统)会首先运行,检查并初始化系统硬件,然后从设定的启动设备(如硬盘、U盘等)中找到Bootloader并把控制权交给它。Bootloader随后开始执行,它会加载操作系统内核到内存,并传递一些必要的系统参数。
2 为什么需要Bootloader?
Bootloader的任务很重要,因为它构成了硬件和操作系统之间的桥梁。如果没有Bootloader,操作系统就无法启动。Bootloader还通常包括一个用户接口,允许用户在多个操作系统中选择一个进行启动,或者修改操作系统的启动参数。
3 Bootloader实例
Bootloader的具体实现和功能可能根据操作系统和硬件的不同而有所差异。例如,Linux系统常见的Bootloader有GRUB和LILO,而Windows系统通常使用的是NTLDR或BootMGR。
说实话,从单片机角度来说,上述例子并不典型。这里举个最为典型的例子:ArduinoIDE为何能给Arduino开发板下载程序?就是因为开发板上搭载了Bootloader。当串口启动后,Arduino即进入复位,进入Bootloader程序,进行监听串口,接受串口下发的应用程序,将应用程序写入arduino芯片中。这样就可以不通过ISP,JTAG等方式,而是通过串口实现了芯片上程序的更新。
具体原理,笔者在之前的文章写过,可跳转:
Arduino是如何实现打开串口时,程序复位的?-CSDN博客
4 如何实现在STM32的bootloader程序?
根据上面举的arduino的例子,我们来实现一个STM32版本的,通过串口更新程序的bootloader吧。
其运行原理大致可以归纳为以下几个步骤:
-
上电复位:首次给STM32供电或者按下复位键后,STM32会开始从预设的启动地址(一般是内部Flash的起始地址或者其他特定内存位置)开始运行程序。
-
启动阶段:在供电后的最初阶段,STM32的Bootloader会先被执行。Bootloader是一个预先在STM32内部ROM中烧录的小程序,其主要任务包括初始化硬件设备、设置系统时钟、配置内存等。
-
程序加载:Bootloader完成启动后,会开始加载用户程序。该程序通常存储在内部的Flash内存或者外部的存储设备中。程序被加载到SRAM中执行。如果用户程序存储在Flash中,Bootloader可以直接跳转到Flash的起始地址开始执行。
-
主循环:用户程序通常会包含一个主循环(main loop),在这个主循环中,程序会周期性地或者根据特定事件执行特定任务。例如,它可能会每隔一段时间读取一个传感器的数据,或者当接收到一个网络包时进行处理。
-
中断处理:在程序运行过程中,可能会发生各种中断,比如定时器到期、外部引脚状态变化、接收到串口数据等。当中断发生时,CPU会停止当前的任务,跳转到对应的中断服务程序进行处理,处理完毕后再返回到原来的任务继续执行。
4.1 keil编译后,HEX文件内容解读
要实现串口下载功能,就得先知道烧录程序HEX或者BIN文件究竟是个什么东西。关于HEX我以前也写过相关文章,可以跳转一下:
STM32的hex文件格式的分析-CSDN博客
关于BIN,目前还未过深的了解,印象中是二进制版本的HEX文件。
4.2 如何实现程序跳转
STM32的程序实际上就是从FLASH地址由上到下,根据指令内容,跳转到FLASH相应地址继续执行的而已。
因此,如果我们要实现从bootloader跳转到我们flash写入的地址,如何实现呢?这里给个bootloader跳转到用户程序的参考例子:
#define FLASH_SAVE_ADDR 0x08020000 //设置 FLASH 地址(必须为偶数,且其值要大于本代码所占用FLASH的大小+0X08000000)
typedef void (*iapfun)(void); //定义一个函数类型的参数.
//设置栈顶地址
//addr:栈顶地址
__asm void MSR_MSP(uint32_t addr) {
MSR MSP, r0 //set Main Stack value
BX r14
}iapfun jump2app;
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(uint32_t appxaddr) {
if(((*(__IO uint32_t*)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法.
{
jump2app=(iapfun)*(__IO uint32_t*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址)
MSR_MSP(*(__IO uint32_t*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump2app(); //跳转到APP.
}
}
int main(void) {
//判断是否 PA5 按键状态,按下则跳转到 FLASH_SAVE_ADDR 地址运行另外一个程序
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == RESET) {
iap_load_app(FLASH_SAVE_ADDR);//跳转执行FLASH_APP代码
}
}
我们烧写的用户应用程序则需要注意修改一下地址便宜:
根据实际需要进行修改。图中Start地址与#define FLASH_SAVE_ADDR保持一致。图中Size大小则应该是0x8040000-Start地址。
并且修改Flash地址偏移(在main首句或之前赋值即可):
SCB->VTOR = FLASH_BASE | 0x20000; //内部FLASH的向量表重定位
根据实际需要进行修改。这偏移值应该是 Start地址-0x8000000。
4.3 如何实现对STM32内部Flash的写入
STM32的标准库有对FLASH操作的API,可以使用这些API对FLASH进行操作。其大致流程如下:
-
解锁Flash:在写入Flash之前,首先需要对Flash进行解锁。STM32的Flash有写保护功能,为了防止意外操作导致的数据损坏,需要手动解除保护。解锁操作需要向特定的解锁寄存器(如KEYR)写入特定的密钥序列。
-
等待Flash就绪:解锁完成后,需要确保Flash处于就绪状态。可以通过检查FLASH_SR寄存器的相关位(如BSY位)来判断Flash是否处于忙碌状态。务必等待Flash就绪后再进行后续操作。
-
擦除目标扇区:在写入数据之前,需要先擦除目标扇区。STM32的Flash是按扇区进行擦写的,擦除一个扇区会把该扇区的所有数据设置为全1(0xFF)。擦除操作需要设置FLASH_CR寄存器的相关位,并指定需要擦除的扇区。
-
等待擦除完成:擦除操作需要一定的时间,因此需要等待擦除完成。可以通过检查FLASH_SR寄存器的相关位(如EOP位)来判断擦除是否完成。务必等待擦除完成后再进行后续操作。
-
写入数据:擦除完成后,就可以开始向目标地址写入数据了。通常,可以选择按字(32位)或半字(16位)进行写入。写入操作需要设置FLASH_CR寄存器的相关位,并将数据写入到目标地址。
-
等待写入完成:与擦除操作类似,写入操作也需要一定的时间。因此需要等待写入完成。可以通过检查FLASH_SR寄存器的相关位(如EOP位)来判断写入是否完成。
-
锁定Flash:写入操作完成后,为了防止意外操作导致的数据损坏,建议重新锁定Flash。锁定操作需要设置FLASH_CR寄存器的相关位(如LOCK位)。
具体的开发实例,可以参考野火或者正点原子的教程。笔者比较喜欢实用标准库,因此,喜欢实用野火的教程作为参考。
4.4 实现的Bootloader展示
这里就不用过多篇幅去展示我的程序代码了,有需要跳转下方链接自行下载(包含上下位机):
基于stm32实现串口烧录程序资源-CSDN文库
大概说明一下,笔者实现了,开机时按下key1按钮,待LED熄灭,释放key1按钮即进入程序烧录模式。如按下的key2按钮,将自动烧录在bootloader中准备好的点亮LED1的程序。收录完成后即会自行跳转到用户应用程序。
笔者这示例,用户应用程序是从地址0x8010000开始的,size为0x30000。
需要先进入烧录模式再启动python脚本进行程序烧录。因为只是测试用例,并未设计得太用户化。
5 总结
bootloader的功能并不止程序更新这一种,还应该包括开机自检等功能,不过这些功能都需要结合实际情况进行开发了,笔者也就不再过多叙述。这此实验算是解开了笔者从前对arduino的一些疑惑,有不少的收货和感悟。