乐于分享
好东西不私藏

在all in AI的时代下,我依然在坚持手搓bootloader

在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-MBootloader科普文章,借助这些文章,一些较为经典的代码片断和技巧得到了广泛的传播。

在从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漏洞究竟有多少人中招?

解决方案:

完全用汇编来处理从BootloaderApp的最后步骤,才是最稳定可靠的方案:

#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空间占用如下:

描述
用户数据备份区(192 字节)
用户数据区(192字节)
Magic1 (64字节)
Magic2 (64字节)
Magic3 (64字节)
enter_bootloader
0XFFFFFFFF…
user_data
0XFFFFFFFF
0XFFFFFFFF
0x00000000
begin_download user_data
0XFFFFFFFF…
0XFFFFFFFF
0x00000000
0XFFFFFFFF
finalize_download user_data
0XFFFFFFFF…
0x00000000
0x00000000
0XFFFFFFFF
  • APP 参数写入:在需要固件升级时,APP 会将接口类型、波特率等升级信息写入指定的用户数据区。
  • Boot参数读取:Bootloader 在启动时首先读取该存储区域,提取接口和波特率参数,并据此初始化通信配置。

用户数据在FLASH中的位置,以及变化过程如下图所示:

bootloader在升级app程序的不同阶段数据变化:

阶段1:进入Bootloader(enter_bootloader)

  • 对于全片擦除过的单片机,此时Magic1Magic2的值为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,并将用户区的数据重新写到用户数据备份区
  • 此时,Magic10xFFFFFFFFMagic3也保持为0xFFFFFFFF,这些状态便于系统在出现断电时判断下载是否已部分完成,从而支持断电重启还能留在bootloader,并且可以从用户数据备份区获取用户数据。
voidbegin_download(void){memset(chBootMagic, 0X55sizeof(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, 0X55sizeof(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在检查到Magic1Magic2均为0x55555555,而Magic30xFFFFFFFF时,就会识别到这是一个升级完成的状态。

  • 这时,MicroBoot无需对外设进行反初始化,而是直接跳转到APP,从而为应用程序提供一个干净的外设环境。

阶段5:从APP再次进入Bootloader

  • 在APP正常运行后,如果想再次进入bootloader进行升级,调用MicroBoot提供的进入bootloader的接口,将会把Magic3设置为0x55555555,复位后,将会再次回到阶段1。

  • 当下次进入bootloader时,看到Magic1Magic2Magic3均为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段里的变量具有如下特性:

场景
变量是否保留
上电(Power On)
不确定(随机值)
软复位(Reset)
保留

所以设计思路就很明确了:

  • 上电时变量无效 → 进入 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/

彩蛋

下篇将为您一一解密模块化组件的工作原理,敬请关注!

。长按加我微信,备注

加我微信