前言
我们常见的MCU / CPU 代码烧录方式主要有以下三种:
- ICP(In Circuit Programing):在电路编程,可通过CPU的Debug Access Port 烧录代码,比如ARM Cortex的Debug Interface主要是SWD(Serial Wire Debug)或JTAG(Joint Test Action Group);
- ISP(In System Programing):在系统编程,可借助MCU厂商预置的Bootloader 实现通过板载UART或USB接口烧录代码,比如STM32存储映射Code分区中的System memory可以预置厂商的Bootloader,让MCU支持通过UART下载(不限于UART,具体由厂商预置Bootloader实现而定);
- IAP(In Applicating Programing):在应用编程,由开发者实现Bootloader功能,比如STM32存储映射Code分区中的Flash本是存储用户应用程序的区间(上电从此处执行用户代码),开发者可以将自己实现的Bootloader存放到Flash区间,MCU上电启动先执行用户的Bootloader代码,该代码可为用户应用程序的下载、校验、增量/补丁更新、升级、恢复等提供支持,如果用户代码提供了网络访问功能,IAP 还能通过无线网络下载更新代码,实现OTA空中升级功能。
一、ICP 与 JTAG / SWD
ICP 主要通过CPU的DAP(Debug Access Port) 烧录代码,下面以ARM Cortex-M3/M4 为例,展示Debug Interface 如下:
ARM Cortex 内部包含了CoreSight 调试架构,CoreSight 包括调试接口协议、调试总线协议、对调试组件的控制、安全特性、跟踪接口等。以前的ARM 处理器都提供JTAG 接口,通过它来控制对寄存器和存储器的访问,CoreSight 则通过DAP(Debug Access Port) 来控制处理器上的总线逻辑。从上图也可以看出,CoreSight 除了提供Debug Interface,还提供了Trace Interface,Trace Interface 主要用来观察数据中间值的变化、跟踪指令的执行状态等,只能单向读取跟踪数据,属于非侵入式调试,不能用于烧录代码,因此本文不对此做过多介绍。
ARM Cortex 目前支持两种Debug Interface:
- SW-DP (Serial Wire - Debug Port):新提供的串行线调试接口SWD,只需要两条信号线(SWCLK、SWDIO);
- JTAG-DP (Joint Test Action Group - Debug Port):广为使用的联合调试接口JTAG,至少需要四根信号线(JTCK、JTMS、JTDI、JTDO,JTRST 为可选信号线);
- SWJ-DP (SW + JTAG - DP):支持SW-DP 与 JTAG-DP 两种接口协议。
STM32 同时支持SW-DP 和 JTAG-DP,也即支持的是SWJ-DP 调试接口协议,STM32-L4 的SWJ Debug Port 如下图所示:
从上图中可以看出,JTAG-DP 与SW-DP 两个接口都需要提供Clock 驱动信号,二者的数据传输引脚不同,SW-DP 接口使用半双工的SWDIO 引脚传输数据,JTAG-DP 接口使用两个单工的JTDI 和JTDO 引脚组成全双工通信。JTAP-DP 除了时钟引脚和数据传输引脚外,还有个重要的引脚JTMS 测试模式选择引脚,可以控制TAP(Test Access Port) 状态机的切换,JTRST 主要用来对TAP 状态机进行复位,为可选引脚(因此有四线JTAG 与五线JTAG 之分),JTMS 也可以实现TAP 状态机的复位。JTAG-DP 与SW-DP 引脚的功能描述如下:
早期的MCU 是可插拔的,向其内烧录代码时常需要将其拔下来,借助烧录器或仿真调试器通过Debug Interface 将代码烧录进MCU 后,再把芯片插回到电路板上。后来MCU 直接焊接在电路板上,电路板上直接引出了Debug Interface,比如常见的JTAG 和 SWD 接口,可以借助JLINK 或 STLINK 等仿真器,通过JTAG 或 SWD 接口直接将代码烧录到MCU 中,省去了MCU 插拔操作,方便了代码烧录和调试操作。
不同的IC 厂家会自己定义自家产品专属的JTAG 接口,来下载和调试程序。嵌入式系统中常用的有20pin、14pin、10pin 等JTAG 接口类型,不同类型的JTAG 接口都包含前面提到的五个引脚,另外包括VRef 和 GND 等引脚,它们的不同之处主要在于GND引脚的数量、一些用户自定义引脚、保留引脚等。一般JTAG 接口同时支持 SWD 接口,但有些主板受空间限制,只提供了SWD 接口,只需要四个引脚,大大节约了空间。
IC 厂家提供的部分开发板上直接集成了 JLINK 或STLINK 模块,相当于将JLINK / STLINK 仿真器内置到开发板上了(比如STM32L4 Pandora 或 NRF52 DK),不再需要额外的仿真器(也不再需要板载JTAG 或 SWD 接口),直接通过USB 线连接电脑,就可以方便的进行代码烧录和调试,对于手头没有仿真器的开发者更加友好,毕竟正版仿真调试器价格还是有点贵的。
二、ISP 与 Boot Mode
ISP 可以通过USB接口(可以是UART协议或USB协议等)直接烧录代码,ARM Cortex 提供的Debug Interface 并不支持UART 或USB 协议,这就需要IC 厂商预置UART 协议或USB 协议代码,让MCU / CPU 支持UART 通讯或USB 通讯。UART 协议比较简单、传输速率比较低,比如STM32 这类MCU 支持UART 协议烧录代码;USB 协议相对复杂、传输速率比较高,比如高通骁龙这类CPU 支持USB 协议烧录代码。
以STM32 为例,先看看有哪些启动模式:
STM32 有三种Boot modes 开始执行代码:
- Main Flash memory:STM32内置的Flash,一般我们使用JTAG / SWD 烧录代码都是直接烧录到这个存储区间,重启后也直接从这里的代码开始执行;
- System memory:这种模式启动的程序功能是由厂家设置的,STM32 在出厂时由ST 在这个存储区间内部预置了一段BootLoader(也即ISP 程序),这段程序出厂后无法修改。厂家提供的BootLoader 一般支持UART 协议,可以让我们直接通过串口将程序代码烧录到Main Flash memory 中;
- SRAM1:这个启动模式一般用于程序调试,假如我只修改了代码中一个很小的地方,重新烧录到Flash比较费时,可以从STM32 内存中启动代码,进行快速的程序调试,等程序调试完成后,再将程序烧录到Main Flash memory 中。
Main Flash memory、System memory、SRAM1 的存储区间分布如下图所示(在博文:存储管理与虚拟内存中也有介绍):
从STM32 的启动模式可以看出,ISP 通过USB 接口直接烧录代码,主要是靠System memory 中被厂家预置的Bootloader 代码提供的UART 协议实现的,要想使用System memory 中的ISP 代码,需要将BOOT0 设置为高电平、BOOT1 设置为低电平,然后按下复位键,MCU 从System memory 启动BootLoader,靠ISP 代码中提供的UART 协议,用户可以通过串口将程序代码烧录到Main Flash memory 中。通过串口烧录代码完成后,需要将BOOT0 设置为低电平,然后按下复位键,MCU 就可以从刚才烧录进去的代码处(也即Main Flash memory)开始执行了。
借助ISP 代码通过USB 接口烧录程序的方法需要切换Boot modes,也即需要切换BOOT0 与BOOT1 引脚的电平,常使用跳线帽来切换这两个BOOT 引脚的电平。但手动插拔跳线帽略嫌麻烦,能否不手动操作,直接通过软件方式控制两个BOOT 引脚的电平呢?
正点原子设计的STM32 开发板借用串口的两个流控引脚DTR、RTS 信号,分别控制STM32 的复位(需要复位才能切换启动模式)和BOOT0 (从Main Flash memory 启动与从System memory 启动主要看BOOT0 电平高低,跟BOOT1 电平无关),配合上位机软件FlyMcu,就可以通过软件控制流控引脚DTR 和RTS 的信号电平,实现通过软件控制RESET 和 BOOT0 电平高低的效果。这样就不再需要用户来回拔插跳线帽,直接通过软件比如FlyMcu 自动控制RESET 与BOOT0 的电平高低,从而实现一键烧录功能,对开发者烧录代码更友好。
上图FlyMcu 设置“DTR的低电平复位,RTS高电平进BootLoader” 和“编程后执行”,在右侧Log 数据中可以看到MCU 复位、选择进入Bootloader、烧录程序代码的过程,待烧录完成后还会看到MCU 复位、选择进入Main Flash memory 开始执行代码的过程。
ISP 使用哪种接口烧录程序代码取决于厂商预置的ISP 代码支持哪种通讯协议,UART 算是最简单且的比较常用的通讯协议,如果厂商的ISP 代码支持,ISP 还可以使用SPI、CAN、IIC、USB 等通讯协议烧录代码到Main Flash memory 存储区间。厂商的ISP 代码是怎么预置到System memory 中的呢?答案正是靠前面介绍的处理器Debug Interface(比如ARM 的JTAG / SWD 接口) 实现ISP 代码烧录的。
如果ISP 指的是MCU / CPU 不用脱离电路板,可以直接通过电路板上预留的接口烧录或擦除程序代码的话,前面介绍ICP 时提到的直接在电路板上预留JTAG / SWD 接口、甚至直接将JLINK / STLINK 仿真模块预置到开发板上只留出USB接口,都可以通过电路板上预留的接口烧录或擦除程序代码,虽然仍是通过Debug Interface 烧录代码的,却可以称之为ISP。
三、IAP 与 Vector Table
IAP 可以在系统运行过程中完成内部程序代码的更新,由Main Flash memory 存储区间内运行的程序自身实现更新代码的下载、校验、烧录等工作。IAP 并不是通过Debug Interface 完成更新代码烧录的,自然也需要类似ISP 代码的Bootloader 完成这些工作,看起来IAP 就相当于将ISP 中厂商预置到System memory 中的Bootloader 搬移到Main Flash memory 中了,MCU / CPU 不再需要切换BOOT Mode,直接从Main Flash memory 启动即可。
将Bootloader 搬移到Main Flash memory 中,最明显的好处就是Bootloader 的功能可以由开发者自由实现(ISP 中厂家预置到System memory 的Bootloader 出厂后不可修改),如果存储资源足够,可以增加很多功能,比如增量更新、空中升级、数据线烧录、SD卡烧录、系统恢复等。
由于Bootloader 主要是在代码烧录和更新过程中用到,并不涉及处理业务逻辑,需要将Bootloader 和用户的Application 分割开来,将二者分割最简单的方式就是将Bootloader 与Application 存放在不同的存储区间,而且Bootloader 应该放置到Main Flash memory 开始位置,便于上电从Bootloader 代码开始执行,Application 放置到Bootloader 后面。举例说明,比如Bootloader 占用存储空间64KB,存放在Main Flash memory 初始地址0x0800 0000 开始的一段存储区间内,将 Application 存放在0x0800 0000 向后偏移Bootloader 占用的64KB 距离处的0x0801 0000 开始的一段存储区间内,同时要在Bootloader 中设置要跳转到Application 代码的起始地址0x0801 0000,这里的地址只是举了个例子,Application 存放的起始地址可由开发者设定,并能被Bootloader 识别并跳转过去即可。
IAP 中由开发者实现的Bootloader (也称IAP 程序)是怎样烧录到Main Flash memory 存储区间的呢?答案自然是前面介绍的ICP 或 ISP 方式,也即通过JTAG /SWD 接口或者ISP 程序将用户的Bootloader 烧录到Main Flash memory 内。由于MCU / CPU 上电后默认从Main Flash memory 开始执行,也即从开发者实现的Bootloader 代码处开始执行,处理业务逻辑的Application 代码如何烧录到对应存储区间呢?
Application 代码既可以使用前面介绍的JTAG /SWD 接口或者ISP 程序烧录到Main Flash memory 内指定地址区间(记得修改默认的烧录地址,为用户Bootloader 预留出空间,如果从默认的Main Flash memory 起始地址开始烧录,将会覆盖用户的Bootloader 代码,运行时由于Application 代码修改了中断向量表偏移地址,不烧录到设定地址会导致指令执行错误),也可以使用IAP 代码烧录到Main Flash memory 内指定地址区间。烧录Application 代码使用哪种接口,就要在Bootloader 中实现相应的通讯协议,如果ISP 代码中不支持想要的通讯协议,可以在IAP 代码中添加相应的通讯协议,然后借助IAP 代码将Application 代码烧录到Main Flash memory 内指定地址区间。
IAP 烧录的应用程序代码在IAP 代码后面,也即默认的Main Flash memory 起始地址向后偏移一段距离之后(假如偏移量64KB,Application 代码应存储在0x0801 0000 起始的地址区间内)。如果通过IAP 程序来烧录Application 代码,可以在IAP 程序中控制Application 代码应被存储的区间地址,Application 代码文件可以使用bin 格式的纯数据文件以减小文件大小;如果使用ISP代码或JTAG/SWD 接口烧录Application 代码,烧录文件一般使用包含地址信息的Hex格式,以便将代码数据正确烧录到指定地址区间。
以Keil MDK 开发环境为例,编译生成hex 文件或者借助JLINK / STLINK 仿真器直接烧录Application 代码到指定的地址区间(也即0x0800 0000 + Offset 起始的地址区间),相比直接烧录到Main Flash memory 起始的地址空间应该修改哪些配置呢?答案是修改ROM 地址区间(一般Bootloader 与Application 并不同时运行,可以不用修改RAM 地址区间),Keil MDK 主要在Target 和Linker 两个地方修改ROM 起始地址和区间大小参数。以STM32 L475为例,Main Flash memory 的起始地址Start 为0x0800 0000,区间大小Size 为0x80000(也即512KB),现在IAP 代码存储在0x0800 0000 处,占用地址区间长度为0x10000(也即64KB),Application 代码应存储在IAP 代码之后,也即起始地址Start 为0x0801 0000,剩余地址空间大小为0x70000,地址区间配置界面如下图示:
点击上图Linker 界面的“Edit” 按钮,即可编辑链接脚本文件“link.sct”,按照Target 地址区间配置修改链接脚本中的ROM 地址区间如下:
// .\link.sct
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x08010000 0x00070000 { ; load region size_region
ER_IROM1 0x08010000 0x00070000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00018000 { ; RW data
.ANY (+RW +ZI)
}
}
将Application 代码烧录到指定的位置(也即IAP 代码之后)还不足以保证代码的正常运行,首先遇到的问题是MCU/CPU 怎么找到并跳转至Application 代码处开始执行?MCU / CPU 运行过程中怎么找到并跳转至中断、异常等服务函数的入口地址?
ARM Cortex 使用中断向量表来管理main函数、中断/异常服务函数的入口地址,每个独立的程序代码都有自己的中断向量表,某程序所属的中断向量表位于该程序代码的起始地址处(可参考博文:系统启动和中断管理)。ARM Cortex 中断向量表的前几项及中断服务程序跳转过程如下图示:
中断向量表的前两项分别保存了MSP与PC的初始值,处理器刚上电复位后,硬件会自动根据向量表偏移地址(查询VTOR向量表偏移量寄存器)找到中断向量表,硬件会自动从向量表首地址(初始地址0x0000 0000,设置BOOT Modes从Main Flash memory启动时则映射到0x0800 0000,设置BOOT Modes从System memory启动时则映射到0x1FFF 0000)处读取数据并赋给栈指针SP,然后自动从向量表第二项地址处(向量表每一项占用4 字节,也即向量表首地址 + 4 字节处)读取数据赋给程序计数器PC,开始执行PC地址上存储的指令,PC初始值指向复位向量,所以刚上电后首先执行的是复位指令。
中断向量表中的异常 / 中断向量也都是基于中断向量表首地址偏移一定字节得到的,我们只需要让MCU / CPU 能正确找到中断向量表的首地址就可以让Application 代码正确执行,中断向量表首地址是通过查询VTOR 寄存器获得的,自然也可以通过设置VTOR(Vector Table Offset Register)让MCU / CPU 能找到Application 的中断向量表,VTOR 寄存器结构如下:
ARM Cortex M3与M4的VTOR 寄存器结构还有些区别,以M4 为例,要求VTOR 的bit 6:0为0(由于向量表起始地址要求对齐到2 的整次幂,比如MCU 共使用了31 个系统异常与中断向量,也即占用31*4 = 124 字节,对齐到2 的整次幂就是27 = 128 = 0x80 字节,VTOR 低7 位就是0 了),只有bit 31:7 为向量表偏移值TBLOFF,我们可是使用掩码将bit 6:0 设置为0,设置SCB->VTOR 寄存器的代码如下:
/* 将中断向量表起始地址重新设置为 app 分区的起始地址 */
#define NVIC_VTOR_MASK 0xFFFFFF80
#define APP_START_ADDR 0x08010000
/* 根据应用设置向量表 */
SCB->VTOR = APP_START_ADDR & NVIC_VTOR_MASK;
对于STM32L475 芯片,一共有98 个异常与中断向量,中断向量表占用空间 98*4 = 392 字节,对齐到2 的整次幂为 29 = 512 = 0x200 字节,因此对于STM32L475 的Application 代码及其中断向量表起始地址应设置为0x200 的整数倍,也可以将STM32L475 的NVIC_VTOR_MASK 设置为0xFFFF FE00,每个芯片支持的中断向量数不同,因此需要根据情况设置。
为什么我们不使用IAP 代码时不需要设置VTOR 寄存器呢?还记得前面介绍的BOOT Mode 吧,我们设置BOOT Modes从Main Flash memory启动时,MCU 硬件会自动从Main Flash memory 起始地址处读取中断向量表的值;设置BOOT Modes从System memory启动时,MCU 硬件会自动从System memory 起始地址处读取中断向量表的值。当我们使用IAP 代码管理Application 代码的烧录、升级时,MCU 硬件不会到Application 代码起始地址处读取中断向量表,默认依然从Main Flash memory 起始地址处(也即IAP 代码起始地址)读取中断向量表。这就需要我们在Application 代码中通过设置VTOR 寄存器的值,告诉MCU 应该到哪去读取Application 程序的中断向量表,将Application 代码的起始地址设置到VTOR 寄存器,MCU 就可以通过读取VTOR 的值找到Application 程序的中断向量表,Application 代码就可以正确执行了。
四、Bootloader 与 OTA
ISP 与IAP 烧录代码都用到了Bootloader 程序,不同的是ISP 的Bootloader 是厂家预置的,功能比较简单且固定,IAP 的Bootloader 是开发者自己实现的,功能比较灵活且丰富。那么,Bootloader 都能实现哪些功能呢?
Bootloader 实际上就是一段代码,跟我们编写裸机程序差不多,ISP 代码比较简单,以UART Bootloader 为例,主要实现UART 驱动,可以通过UART 通讯协议传输数据,当然也要能够解析出HEX 文件中的地址信息,将UART 接收到的数据写入到特定的地址区间。
IC 厂商预置的ISP 代码一般只实现了通过常用的UART 或USB 等通讯协议烧录Application 代码的功能,有些场景我们需要使用其它的通讯接口比如CAN 通讯实现代码烧录与更新,这就需要我们自己实现IAP 代码,由IAP 代码实现通过CAN 通讯协议烧录Application 代码的功能。如果你对手机进行过刷机操作,应该了解过fastboot 刷机功能,fastboot 的功能实际就是由IAP 代码实现的,如果要使用fastboot 功能需要先通过ISP 代码提供的USB 通讯协议向CPU 中刷入Bootloader 代码(一般Bootloader 代码与Application 代码一起通过USB 通讯协议烧录进CPU 中)。
IAP 代码可以实现ISP 代码的功能,但开发者既然选择自己实现Bootloader,IAP 代码就不只是具有烧录Application 代码的功能,更多的是为了升级代码,甚至是空中升级代码。
我们应该已经习惯升级系统版本或者给系统打补丁解决Bug等,不管是版本升级还是打补丁,都涉及到在系统运行过程中,重新烧录部分代码数据。如果没有IAP 代码,我们只能借助ISP 代码使用第三方设备和烧录工具,完全覆盖先前的代码,这对普通用户是极不友好的。我们借助IAP 代码就可以不依赖第三方设备与烧录工具,仅靠自身就可以完成版本升级或打补丁的功能,不管是PC 还是手机操作系统也都是依靠系统开发商提供的Bootloader 实现系统版本或补丁升级的。
IAP 代码怎么实现系统版本升级的呢?回想我们在PC 或手机上升级应用软件,都是先将软件安装包下载到本地存储介质中,然后再运行软件安装包将软件执行代码复制到系统安装目录下,也即需要占用存储介质中一定的存储空间来暂存软件安装包。升级Application 程序版本的过程与此类似,也是将Application 代码文件暂存到某个存储介质中(可以是RAM、ROM、SD卡、U盘等存储介质),Bootloader 会先校验Application 代码文件的有效性,通过校验后再将Application 代码从存储介质搬移到特定的存储区间(比如Main Flash memory 中IAP 代码后的存储区间)。
如果我们开发的产品不具备网络连接能力,也只能先用电脑下载好固件升级包,暂存到SD卡或者U盘中,再插到我们的产品上。在IAP 代码中可以实现SD卡通讯协议或USB 通讯协议,支持从SD卡或U盘读取固件升级包,校验通过后搬移到Main Flash memory 的特定存储区间,实现Application 版本升级的目的。
随着物联网设备的普及,开发的产品越来越多支持网络访问能力,我们可以将Application 版本升级过程设计的更简单。直接借助产品的网络访问能力,从特定服务器将系统升级包下载到本地存储介质特定分区内(可以是RAM、片内Flash、片外Flash、SD卡等本地存储介质),校验通过后再将代码搬移到Main Flash memory 的特定存储区间。如果系统升级包是通过无线网络(比如移动蜂窝网、WLAN、WPAN等无线协议)下载的,这种通过无线网络下载升级包,并通过Bootloader 实现版本升级的技术称为OTA(Over-the-air programming)空中升级或空中编程技术。
要具备OTA 空中升级能力,只有负责固件升级包校验升级的Bootloader 分区和负责业务逻辑的Application 分区是不够的,还需要有暂存固件升级包的Downloader 分区。每次MCU / CPU 上电启动都从Bootloader 开始执行代码,Bootloader 会到Downloader 分区检查是否有可升级的固件包,如果没有则直接跳转到Application 代码执行,如果有则对固件升级包校验通过后,将新的固件代码搬移到Application 分区,完成Application 代码更新后,再跳转到新的Application 代码执行。
为了增强系统更新包网络传输的安全性,防止被网络攻击者篡改,可以对通过网络下载的固件升级包先进行加密认证(包括数据加密、数据完整性校验、客户端与服务器身份校验等),Bootloader 就要能对固件升级包进行解密和认证校验。为了减少网络传输开销(也即减少消耗的网络流量),可以对固件升级包进行压缩后传输,Bootloader 就要能够对固件升级包进行解压缩。
有时候我们只是想升级Application 的部分代码,如果将全部代码下载后升级比较耗费资源且效率较低,这时候就需要Bootloader 提供只更新部分代码的能力。将更新代码做成差分升级包(相当于补丁文件),通过网络将差分升级包下载到Downloader 分区,Bootloader 对其解压缩、解密、校验通过后,将要更新的那部分代码搬移到Application 特定的存储区间内(差分升级包包含了要更新代码的地址信息,Bootloader 可以解析出地址信息,并将更新代码搬移到目标存储区间)。差分升级包一般比整个固件升级包占用的存储空间小得多,可以极大节省存储空间和网络流量,也为整个升级过程节约了不少时间,效率更高。
使用OTA 空中编程技术对产品进行固件升级对用户是比较友好的,只要能访问网络,就可以实现一键升级功能。但Application 代码运行过程中有可能会出现一些预料之外的Bug,比如某些重要文件被删除或篡改、被网络攻击、中病毒、垃圾过多耗尽资源等,这就需要具备一键恢复能力。PC 和手机上都提供了recovery 恢复功能,该功能是怎么实现的呢?原理跟升级类似,可以再划分出一个recovery 分区,将出厂的软件版本压缩后存放到该分区内,如果系统运行出现了错误,可以将recovery 分区内的代码搬移到Application 分区,相当于实现了恢复出厂设置的功能。recovery 分区内也不一定一直存储出厂版本,可以同步更新为前一个比较稳定的系统版本,使用recovery 恢复功能就相当于回退到了前一个稳定版本,即便系统因为中病毒或网络攻击等特殊情况,也能恢复过来继续正常使用。
Bootloader 除了具备固件升级和固件恢复外,还应针对升级过程中突然中断(比如断电了)的情况妥善处理,尽量减少返厂的机会。Bootloader 该怎么应对升级突然被打断的情况呢?正常情况下RAM 中的数据断电后就消失了,我们可以借鉴断点续传技术的实现方案,Bootloader 在升级代码过程中,在ROM 中同步记录升级进度与状态,可以让MCU 再次上电从Bootloader 启动时,从ROM 中读取升级进度与状态,继续完成先前的升级过程。