在all in AI的时代下,我依然在坚持手搓bootloader
1、引言
那些让人崩溃的BootLoader升级灾难
你有没有为写BootLoader无从下手发愁过?
你有没有为出厂的产品还需要开盖刷程序苦恼过?
你有没有为程序升级失败,而抓耳挠腮过?
你有没有因为固件升级Bug被老板当众灵魂拷问:“为啥烧进去就起不来了?”
2、MicroBoot:彻底的解决升级难题
问题1:bootloader 在跳转到app前没有清理干净存在的痕迹
问题描述: 固件更新完毕后从 bootloader 跳转至 APP 前需对所用的外设 deinit ,以使外设恢复至上电时的初始状态。但是当使用复杂的外设收取固件包时, deinit 也将变得复杂,甚至很难排除对 APP 的影响。因此最佳的方法是升级完固件进行软复位,再次进入bootloader在程序运行之前,先通过判断标志的方式,来直接跳转到app,这样就相当于给APP提供了一个干净的外设环境。
程序复位执行流程如下图所示:

设置标志位的常见方法
传统的设置标志位以实现复位后保留状态的方法通常有以下两种::
备份寄存器:利用芯片中的不受软件复位影响的可供用户使用的寄存器 (如 STM32 中的备份寄存器);
No-Init 数据段:将标志位放在特殊的内存段(例如
.bss.noinit),使其在复位后避免被重新初始化。
问题2: APP单独运行没有问题,通过Bootloader跳转到APP运行莫名死机
问题描述:在近几年的嵌入式社区中,流传着不少关于面相Cortex-M的Bootloader科普文章,借助这些文章,一些较为经典的代码片断和技巧得到了广泛的传播。
在从Bootloader跳转到用户APP的过程中,使用函数指针而非传统的汇编代码则成了一个家喻户晓的小技巧。相信类似下面 JumpToApp() 函数,你一定不会感到陌生:
typedefvoid(*pFunction)(void);voidJumpToApp(uint32_t addr){ pFunction Jump_To_Application; __IO uint32_t StackAddr; __IO uint32_t ResetVector; __IO uint32_t JumpMask; JumpMask = ~((MCU_SIZE-1)|0xD000FFFF);if (((*(__IO uint32_t *)addr) & JumpMask ) == 0x20000000) { StackAddr = *(__IO uint32_t*)addr; ResetVector = *(__IO uint32_t *)(addr + 4); __set_MSP(StackAddr); Jump_To_Application = (pFunction)ResetVector; Jump_To_Application(); }}
但是这段家喻户晓,被世人奉为真理的代码,却隐藏着很深的BUG,相信很多小伙伴都遇到过通过Bootloader跳转到APP后,程序时好时坏的灵异事件,具体详情请看这篇文章:震惊!这个隐藏的Bootloader漏洞究竟有多少人中招?
解决方案:
完全用汇编来处理从Bootloader到App的最后步骤,才是最稳定可靠的方案:
#if defined (__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050)/* Avoids the semihosting issue */__asm(" .global __ARM_use_no_argv\n");#elif defined(__GNUC__)/* Disables part of C/C++ runtime startup/teardown */void __libc_init_array (void) {}#endif#if defined(__CC_ARM)__asm voidmodify_stack_pointer_and_start_app(uint32_t r0_sp, uint32_t r1_pc){ MOV SP, R0 BX R1}#elif defined(__GNUC__)voidmodify_stack_pointer_and_start_app(uint32_t r0_sp, uint32_t r1_pc){uint32_t z = 0;__asm volatile( "msr control, %[z] \n\t""isb \n\t""mov sp, %[r0_sp] \n\t""bx %[r1_pc]" : : [z] "l" (z), [r0_sp] "l" (r0_sp), [r1_pc] "l" (r1_pc) );}#else#error"Unknown compiler!"#endif
温馨提示:看不懂汇编没关系,可以复制到AI帮你理解!
问题3: 需要 APP 传递信息给 Bootloader
问题描述:在一些嵌入式设备中,由于产品型号和应用场景的不同,固件升级的接口(如 UART、CAN、USB等)和波特率配置需要改变。这种情况下,很多人的思路是希望bootloader也能自己给自己更新,但是这不就更加增加了工程的复杂性,变成了无限套娃!正确的解法应该是参考内置在单片机的bootrom那样,bootrom提供必要的接口API,可以让app传递参数给Bootloader,尽量让bootloader固化,一个稳定可靠的bootloader,是产品快速迭代的基石!,开发bootloader不能像开发app那样的思路野蛮生长!
解决方案:
1.定义传递信息的用户数据结构体:
BootLoader定义了一个默认的用户数据结构体,一共192个字节,APP可以在192个字节内随意向bootloader传递数据:
// <o>The user data size// <i>Default: 192#define USER_DATA_SIZE 192typedefstruct {char chProjectName[16];char chHardWareVersion[16];char chSoftBootVersion[16];char chSoftAppVersion[16]; } msgSig_t;typedefstruct {union { msgSig_t sig;uint8_t B[USER_DATA_SIZE]; } msg_data;} user_magic_data_t;
2.提供类似bootrom的函数接口:
BootLoader提供了一组函数的API,并将接口位置固定到BOOT_FLASH_OPS_ADDR地址,这样APP就可以方便调用了
typedefstruct {void (*fnGoToBoot)(uint8_t *pchDate, uint16_t hwLength); bool (*target_flash_init)(uint32_t addr); bool (*target_flash_uninit)(uint32_t addr); int (*target_flash_read)(uint32_t addr, uint8_t *buf, size_t size); int (*target_flash_write)(uint32_t addr, constuint8_t *buf, size_t size); int (*target_flash_erase)(uint32_t addr, size_t size); } boot_ops_t;#define BOOT_FLASH_OPS_ADDR 0x08001000#define __ARM_AT(x) ".ARM.__at_"#x#define ARM_AT(x) __ARM_AT(x)#if defined(__CC_ARM) // ARM Compiler 5 (AC5)#define BOOT_FLASH_SECTION __attribute__((at(BOOT_FLASH_OPS_ADDR)))#elif defined(__ARMCC_VERSION) && (__ARMCC_VERSION >= 6000000) // ARM Compiler 6 (AC6)#define BOOT_FLASH_SECTION __attribute__((section(ARM_AT(BOOT_FLASH_OPS_ADDR))))#elif defined(__GNUC__) // GCC#define BOOT_FLASH_SECTION __attribute__((section(".boot_flash_ops"), used, aligned(4)))#else#define BOOT_FLASH_SECTION#endif__attribute__((used))staticvolatileconstboot_ops_t tBootOps BOOT_FLASH_SECTION = { .fnGoToBoot = enter_bootloader, .target_flash_init = target_flash_init, .target_flash_erase = target_flash_erase, .target_flash_write = target_flash_write, .target_flash_read = target_flash_read, .target_flash_uninit = target_flash_uninit};
3.APP的代码可以调用这些函数,并传递数据给bootloader:
APP需要重新定义用户数据,添加需要向bootloader传递的数据。
typedefstruct {/*定义用户数据*/char chProjectName[16];char chHardWareVersion[16];char chSoftBootVersion[16];char chSoftAppVersion[16];char chPortName[16];int wPortBaudrate;} msgSig_t;typedefstruct {union { msgSig_t sig;char B[sizeof(msgSig_t)]; } msg_data;} user_data_t;user_data_t tUserData = { .msg_data.sig.chProjectName = "app", .msg_data.sig.chHardWareVersion = "V1.0.0", .msg_data.sig.chSoftBootVersion = "V1.0.0", .msg_data.sig.chSoftAppVersion = "V1.0.0",};typedefstruct {void (*fnGoToBoot)(uint8_t *pchDate, uint16_t hwLength);bool (*target_flash_init)(uint32_t addr);bool (*target_flash_uninit)(uint32_t addr);int (*target_flash_read)(uint32_t addr, uint8_t *buf, size_t size);int (*target_flash_write)(uint32_t addr, constuint8_t *buf, size_t size);int (*target_flash_erase)(uint32_t addr, size_t size);} boot_ops_t;
比如通过UART接口来升级程序,就可以这样使用:
voiduart_boot(){ rt_memcpy(tUserData.msg_data.sig.chPortName, "UART1", rt_strlen("UART1")); tUserData.msg_data.sig.wPortBaudrate = 115200;boot_ops_t *ptBootOps = (boot_ops_t *) 0x08001000; ptBootOps->fnGoToBoot((uint8_t *)tUserData.msg_data.B, sizeof(tUserData)); rt_hw_cpu_reset();}MSH_CMD_EXPORT(uart_boot, go to bootloader);
APP的程序如果有操作flash的需求,便可以直接使用bootloader提供的这组API了。这样不仅APP和Bootlader可以共用一个flash驱动来节省空间,还有一个更妙的作用:
如果你使用的是类似STM32H750这样的芯片,内部只有128K的flash,一般内部flash的128k是用来做bootloader,app程序是需要跑在外部nor flash中的,我们知道nor flash是不支持Read-while-write特性的,因此在APP程序中,是不支持对flash进行擦写的。但是如果app调用的是bootloader提供的flash驱动函数时,程序已经运行在了单片机的内部flash中,这个时候便可以对nor flash进行擦写了!如果你碰巧又有保存数据的需求,那就妥妥的为你省了另外一颗flash芯片,这省下的都是成本!
4.防止参数掉电丢失,将数据固化在flash合理的位置:
该方案在APP分配的FLASH空间末端一共占用了570个字节,具体作用可以看下边的表格。为什么要放在app区域的末端,是一个巧妙的设计,聪明的你肯定会觉得flash是按照扇区擦除的,那这相当于占用了APP的一整个扇区,远远高于570个字节!但是实际上并不会占用整个扇区,app的代码依然可以使用最后一个扇区,具体原因可以往下看。
FLASH空间占用如下:
|
|
|
|
|
|
|
|---|---|---|---|---|---|
| enter_bootloader |
|
user_data |
|
|
|
| begin_download | user_data |
|
|
|
|
| finalize_download | user_data |
|
|
|
|
-
APP 参数写入:在需要固件升级时,APP 会将接口类型、波特率等升级信息写入指定的用户数据区。 -
Boot参数读取:Bootloader 在启动时首先读取该存储区域,提取接口和波特率参数,并据此初始化通信配置。
用户数据在FLASH中的位置,以及变化过程如下图所示:

bootloader在升级app程序的不同阶段数据变化:
阶段1:进入Bootloader(enter_bootloader)
-
对于全片擦除过的单片机,此时Magic1和Magic2的值为 0xFFFFFFFF,表示还未开始下载过程,Magic3被设置为0x55555555,表明这是一个等待升级程序的状态。此时user data的值为0xFFFFFFFF,当APP调用enter_bootloader接口,将会把app传递进来的数据写进用户区。
voidenter_bootloader(void){uint32_t wData = 0X55555555; target_flash_init(APP_PART_ADDR); target_flash_write((APP_PART_ADDR + APP_PART_SIZE - (3*MARK_SIZE) - (USER_DATA_SIZE)), pchDate, USER_DATA_SIZE); target_flash_write((APP_PART_ADDR + APP_PART_SIZE - MARK_SIZE), (constuint8_t *)&wData, sizeof(wData)); target_flash_uninit(APP_PART_ADDR);}
阶段2:开始下载(begin_download)
-
当固件下载开始时,MicroBoot会首先对Magic所在的扇区擦除,然后将Magic2的值设置为 0x55555555,并将用户区的数据重新写到用户数据备份区。 -
此时,Magic1为 0xFFFFFFFF,Magic3也保持为0xFFFFFFFF,这些状态便于系统在出现断电时判断下载是否已部分完成,从而支持断电重启还能留在bootloader,并且可以从用户数据备份区获取用户数据。
voidbegin_download(void){memset(chBootMagic, 0X55, sizeof(chBootMagic)); target_flash_init(APP_PART_ADDR); target_flash_erase(APP_PART_ADDR + APP_PART_SIZE - (3*MARK_SIZE), 3*MARK_SIZE); target_flash_write((APP_PART_ADDR + APP_PART_SIZE - (3*MARK_SIZE) - 2 * (USER_DATA_SIZE)), tUserMagicData.msg_data.B, USER_DATA_SIZE); target_flash_write((APP_PART_ADDR + APP_PART_SIZE - (2*MARK_SIZE)), chBootMagic[1], MARK_SIZE); target_flash_uninit(APP_PART_ADDR);}
阶段3:完成下载(finalize_download)
-
当固件下载完成且数据写入成功后,MicroBoot会将Magic1的值设置为
0x55555555,标志着下载过程已顺利完成。 -
此时,Magic2的值仍为
0x55555555,而Magic3的值保持为0xFFFFFFFF,从而标识此阶段为下载完成、准备进入应用程序的状态。
voidfinalize_download(void){memset(chBootMagic, 0X55, sizeof(chBootMagic)); target_flash_init(APP_PART_ADDR); target_flash_write((APP_PART_ADDR + APP_PART_SIZE - 3*MARK_SIZE), chBootMagic[0], MARK_SIZE); target_flash_uninit(APP_PART_ADDR);}
阶段4:重新进入Bootloader并跳转到APP
-
在系统完成固件升级后,MicroBoot会执行软复位,系统重新进入bootloader。
-
bootloader在检查到Magic1和Magic2均为
0x55555555,而Magic3为0xFFFFFFFF时,就会识别到这是一个升级完成的状态。 -
这时,MicroBoot无需对外设进行反初始化,而是直接跳转到APP,从而为应用程序提供一个干净的外设环境。
阶段5:从APP再次进入Bootloader
-
在APP正常运行后,如果想再次进入bootloader进行升级,调用MicroBoot提供的进入bootloader的接口,将会把Magic3设置为
0x55555555,复位后,将会再次回到阶段1。 -
当下次进入bootloader时,看到Magic1、Magic2和Magic3均为
0x55555555。
程序在升级的全流程中,只在开始升级的时候,调用的begin_download()函数对app区域进行了全部擦除,并不会对app的最后一个扇区做单独的擦除操作,因此并不会影响app代码使用最后一个扇区,也就是bootloader使用的标志及数据,仅占用了app区域的最后570个字节!
bootloader安全跳转代码如下:
__attribute__((constructor))staticvoidenter_application(void){ target_flash_init(APP_PART_ADDR);do {// User-defined conditions for entering the bootloaderif(user_enter_bootloader()){ target_flash_read((APP_PART_ADDR + APP_PART_SIZE - (3 * MARK_SIZE) - USER_DATA_SIZE), tUserMagicData.msg_data.B, USER_DATA_SIZE);break; }// Read the magic values from flash memory to determine the next action target_flash_read((APP_PART_ADDR + APP_PART_SIZE - 3 * MARK_SIZE), chBootMagic[0], 3 * MARK_SIZE);// Check if Magic3 is 0X55555555, indicating to read user data from a specific locationif ((0X55555555 == *(uint32_t *)&chBootMagic[2]) || (0 == *(uint32_t *)&chBootMagic[2])) { target_flash_read((APP_PART_ADDR + APP_PART_SIZE - (3 * MARK_SIZE) - USER_DATA_SIZE), tUserMagicData.msg_data.B, USER_DATA_SIZE);break; }// Check if Magic2 is 0X55555555,Magic1 is !0X55555555, indicating to read user data from a specific locationif ((0X55555555 == *(uint32_t *)&chBootMagic[1]) && (0X55555555 != *(uint32_t *)&chBootMagic[0])) { target_flash_read((APP_PART_ADDR + APP_PART_SIZE - (3 * MARK_SIZE) - 2 * USER_DATA_SIZE), tUserMagicData.msg_data.B, USER_DATA_SIZE);break; }//Check if the value at the address (APP_PART_ADDR + 4) has the expected application identifieruint32_t wValue = 0; target_flash_read((APP_PART_ADDR + APP_PART_OFFSET), (uint8_t *)&wValue, 4); target_flash_uninit(APP_PART_ADDR); // If all checks are passed, modify the stack pointer and start the application#ifdef __riscvif ((wValue) == (0xffffffff) || (wValue) == (0) ) {break; } modify_stack_pointer_and_start_app((APP_PART_ADDR + APP_PART_OFFSET));#else// Check if the value at the address (APP_PART_ADDR + 4) has the expected application identifierif ((wValue & 0xff000000) != (APP_PART_ADDR & 0xff000000)) {break; } modify_stack_pointer_and_start_app(*(volatileuint32_t *)APP_PART_ADDR, (*(volatileuint32_t *)(APP_PART_ADDR + APP_PART_OFFSET)));#endif } while(0); target_flash_uninit(APP_PART_ADDR); }
函数修饰符 __attribute__((constructor))告诉编译器在程序启动时自动调用这个函数。即在主程序的 main() 函数之前执行,它的主要功能是检查系统当前的状态,并根据状态决定是进入APP还是停留在bootloader。代码中增加了用户自定义的进入bootloader条件,代码通过调用user_enter_bootloader()检查用户是否指定了进入bootloader模式,这个检查是为了给用户留出手动控制的空间,比如通过外部按键强制进入bootloader,或者延时启动app,如果返回值为true,则直接退出函数,保持在bootloader中。
问题4: APP有bug,跳转后,直接死机怎么办?
很多人做 Bootloader 时,都会遇到一个经典问题:
设备一上电就直接跳转到 APP,一旦 APP 异常,设备就“锁死”了
比如:
-
APP 崩溃 -
向量表错误 -
Flash 写坏 -
升级中断
结果:设备无法再次进入 Bootloader,直接变砖。
解决方案:
-
上电先进入 Boot,给一个“安全时间窗口”,等待用户下发升级指令。 -
通过外部按键等接口,上电强制留在bootloader。
microboot在上电执行跳转之前,为用户预留了一个user_enter_bootloader的弱函数,用户可以通过实现这个函数,来自定义强制进入bootloader的方式。
__attribute__((weak))booluser_enter_bootloader(void){returnfalse;}
在 Bootloader 设计中,一个关键问题是:
-
如何区分“上电复位”和“软复位”?
因为:
-
只有上电时,才需要进入 Boot 的安全窗口 -
软复位后,应该直接进入 APP
解决方法是使用“不被复位初始化的变量(NOINIT),通过修改链接脚本,将一段内存定义为 NOINIT 区域:
LR_IROM1 0x080000000x00040000 { ; load region size_region ER_IROM1 0x08000000 0x00040000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) .ANY (+XO) } RW_IRAM1 0x20000000 0x0000C000 { ; RW data .ANY (+RW +ZI) } RW_IRAM_NO_INIT +0 UNINIT { * (.bss.noinit) } }
温馨提示:看不懂链接脚本没关系,复制到AI帮你理解!
放在这个noinit段里的变量具有如下特性:
|
|
|
|---|---|
|
|
|
|
|
|
所以设计思路就很明确了:
-
上电时变量无效 → 进入 Boot -
倒计时结束 → 设置变量 + 软复位 -
复位后变量有效 → 进入 APP
代码如下:
/* * @brief Boot control flag stored in NOINIT section. * This variable is preserved across reset and used to control * whether the system should jump to APP after reboot. */NOINITstaticvolatileuint32_t s_wEnterAppFlag;#define ENTER_APP_FLAG 0x55555555 /* Magic value indicating that APP entry is allowed *//* * @brief Check if entering APP is allowed. * @retval true APP can be entered * @retval false Stay in bootloader */boolcan_enter_app(void){return (s_wEnterAppFlag == ENTER_APP_FLAG);}/* * @brief Enable APP entry. * This sets the flag so that after reboot the system jumps to APP. */voidenable_enter_app(void){ s_wEnterAppFlag = ENTER_APP_FLAG;}/* * @brief Decide whether to stay in bootloader. * @retval true Stay in bootloader * @retval false Jump to APP */booluser_enter_bootloader(void){return !can_enter_app();}/* * @brief Delay timer callback. * If timeout occurs and APP entry is not yet enabled, * enable it and reboot to enter APP. * * @param status Timer status * @param pTag User context (unused) */voidon_delay_event(multiple_delay_report_status_t status, void *pTag){user_magic_data_t *ptMagicData = (user_magic_data_t *)pTag;if (status == MULTIPLE_DELAY_TIMEOUT) { if(!can_enter_app()){ enable_enter_app(); reboot();/* Reboot to apply APP entry */ } }}
问题5: 自定义升级协议传输不稳定
问题描述:对于许多工业或者车载项目,现场环境干扰很多,会导致数据传输不稳定,常常因为收到错误数据导致升级失败,需要重新开始升级,很浪费时间。
解决方案:
MicroBoot 集成了成熟的 Ymodem 文件传输协议 来实现固件升级。相比简单的裸数据传输,Ymodem 在可靠性和工程可用性上有明显优势。
-
分包传输 + 自动校验 -
自动重传机制(抗干扰核心)
有了 Bootloader 之后,还需要一个工具来发送固件。传统做法要么自己开发上位机,要么借助xshell这种支持ymodem协议的串口助手,流程繁琐且容易出错。为此我专门给我制作的microlink下载器内置了 Ymodem 协议,支持一键升级,无需额外开发上位机,也不需要关心协议细节。连接设备后即可完成固件下载,大幅降低使用门槛,尤其适合现场和售后升级场景。
升级演示视频如下:
下载器链接:
https://item.taobao.com/item.htm?ft=t&id=1020501356342
问题6: 固件需要加密传输?还需要兼容更多的协议?能不能支持A/B分区升级呀?什么!还想要差分升级?
MicroBoot 从一开始就没有试图“包打天下”,而是只做一件事:
-
把最核心的“安全启动 + 跳转机制”做到极致稳定
除此之外:
-
所有升级能力全部模块化、可插拔
MicroBoot 不是一个“功能很多的 Bootloader”,而是一个“可以承载任何功能的 Bootloader 框架”
3、MicroBoot架构
3.1 层次框架

3.2 模块化组件
-
环形队列
一个用C语言编写的支持多类型、函数重载与线程安全的环形队列
-
信号槽
一个用C语言模拟QT的信号槽的功能
-
发布订阅
一个支持任意参数类型和数量的发布订阅机制
-
shell
一个支持shell命令行的模块
-
ymodem
一个完全用状态机编写的ymodem模块
-
multiple_delay
一个基于统一 tick 的轻量级多路软件延时服务,支持高/中/低优先级分别在中断或主循环中分发超时回调。
4、MicroBoot使用教程
https://microboot.readthedocs.io/zh-cn/latest/
彩蛋

如果你觉得文章还不错,就请点击右上角选择发送给朋友或者转发到朋友圈。您的支持和鼓励是我最大的动力。长按加我微信,备注“加群”,拉入微信技术交流群。

长按二维码
加我微信
夜雨聆风