前言
初次接触 IAP 升级时,是直接搬运别人验证过的代码到自己的工程中,没有太过于深究其细节,对其如何保证稳定性的代码过程也是比较模糊的。当时只是知道所移植的 IAP 思路是:在 APP 工程中接收新的 APP 文件,并将其存放到存储区内,而后回到 Bootloader,擦除原有的 App,然后将新的 APP文件 从存储区搬运写入到 APP运行区,最后跳转到 APP 运行区执行。
当然了,上述思路只是 IAP 升级的方法之一,另外实际的升级过程也要稍微复杂一点,要考虑的东西也稍多,譬如:
- 如何保证接收的新APP文件是正确的?如何做校验?
- 这个从外界(上位机)接收文件的过程是怎样的?通过何种协议?为什么?有没有更好的方法?
- 如果在搬运升级的过程中,断电了或者因为其它原因失败了,如何保证设备不会变砖头?
- 在这个代码设计过程中,有哪些要注意的重点事项?有哪些具体升级方法和它们的适用环境?
鉴于上述一系列的问题,私以为有必要系统总结一下在 MCU 开发中的 IAP 升级,当然肯定不全是本人原创内容,只能说是基于前人的文章以及个人的实践应用和理解之上,做深度的总结,权当分享或者私人回顾用
IAP升级方法思路概述
IAP 升级有多种方法,以下参考其它站点文章,以在哪个运行区应用编程(即以在哪个区进行擦除Flash、搬运App)分类,做进一步的讲述
在Bootloader应用编程
思路一: 程序正常工作在 App 工程中,此时收到上位机的升级指令,MCU 通过复位或者直接跳转的方式回到 Bootloader,在 Bootloader 中直接擦除当前 APP 程序,然后接收新的 APP 并直接写入 APP 运行区中,最后检验通过则跳转至 APP 运行区执行程序。
优缺点分析:
- 更适用于内存资源小的情况,只使用一个 Bootloader 区和 一个 APP 运行区
- 可用性差,一般建议只用于内部产测或者专用的串口调试升级,不能用于消费者产品端上,当升级过程突然中断,那就代表着只有 Bootloader 程序了,只能一直处于等待升级中
- 兼容性、通用性差,如果要做空中 OTA 升级,则要在 Bootloader 工程支持产品的无线连接、传输协议等业务
思路二: 程序正常工作在 APP 运行区中,收到上位机的指令,则回到 Bootloader 中,此时不擦除当前的 APP 程序,首先接收新 APP 程序并将其写入到规划好的存储区中(此存储区可以是内部Flash某个空白部分,也可以是外部Flash空间),等新程序接收完毕并且校验无误后,再擦除 APP 运行区,并将新 APP 从存储区搬运复制到原 APP 运行区,最后跳转运行,完成升级
优缺点分析:
- 需要占用额外的空间,使用内部的 Flash 或者外部的空间来作为存储区都可
- 安全稳定可靠,无论是传输中断或者是在 Bootloader 搬运过程中断电,都可保障能升级成功。(接收传输中断了,原有 APP 继续工作;即使Bootloader 在搬运过程中断电了,但存储区程序还在,下次上电后 Bootloader 会继续从存储区搬运新程序)
- 适用于串口 IAP 升级,可移植性强;不建议用于空中 OTA 升级,那意味着bootloader需要开发无线通信相关的逻辑,换种说法是:不建议把跟业务相关的功能做到 Bootloader
思路三: 程序正常工作在 APP1 运行区中,收到上位机的指令,则回到 Bootloader 中,此时并不会擦除 APP1 运行区程序,而是首先擦除 APP2 运行区(内部Flash的另一块预设空间),然后接收新 APP 并将其写入到 APP2 运行区中,等接收完毕并校验无误后,设置 APP2 运行标志位,擦除 APP1 运行标志位,最后跳转至 APP2 运行区中执行程序。(同理:下次则是擦除 APP1 程序,接收新程序到 APP1 运行区中并设置 APP1 标志位,清除 APP2 标志,最后跳转执行 APP1)
优缺点分析:
- 用到一个 Bootloader、两个 APP 运行区,而是都要是可运行代码的内部 Flash 空间
- 操作较思路二绕一点,但安全可靠性是一致的
- 升级时不用擦除原有 APP 运行区程序,节省了这一步操作,但其实也不算优势,因为擦除时间本身就很短,建议优先用思路二方法
在APP应用编程
思路四: 程序运行在 APP 中,在 APP 中接收新 APP 程序,并将其写入到规划好的存储区内(可以是内部Flash,也可以是外部存储块),等待接收完毕并且检验通过后,通过复位或者跳转的方式回到 Bootlaoder 中,由 Bootloader 检验新程序无误后,再擦除 APP 运行区程序,然后将新 APP 从存储区搬运到 APP 运行区中,最后跳转至 APP 运行区执行新程序。
优缺点分析:
- APP 和 Bootloader 均涉及编程
- 升级存储区域可采用外部存储区,可选择范围广
思路五: 程序运行在 APP1 运行区中,开始接收新的 APP 程序并将其写入到内部Flash的 APP2 运行区,待接收完毕并校验无误后,清除 APP1 运行标志位,设置 APP2 运行标志位,通过复位或者跳转回到 Bootloader,由 Bootloader根据有效标志位选择跳转进入 APP1 或者 APP2 运行区。(同理:下次升级将 APP1 和 APP2 反转即可)
优缺点分析:
- 只能用于内部 Flash 够大的情况下,建议优先采用思路四
- Bootloader 不涉及编程操作
IAP升级方法小结
在 Bootloader 集成传输接收及升级操作
- APP 工程无需涉及升级相关代码,专注于业务即可
- Bootloader 稍为复杂,占用空间大
- Bootloader可移植,使得前期没时间写好 APP 工程时,也能先烧录 Bootloader。后续可以在没有 APP 的情况下也能更新 APP 工程
在 APP 集成接收传输:
- 可移植性稍差,每写一个工程,都得把升级相关代码加进工程里并验证
- Bootloader 程序简单,占用空间小
注意: 上述所说,是基于 MCU 的产品角度,IAP 升级是指产品应用串口 + Ymodem 协议与上位机连接进行升级,OTA 升级特指通过蓝牙或者WiFi与终端建立无线连接后,再通过私有的协议栈与终端交互进行传输升级
IAP具体设计
IAP环境及工具配置
Ymodem:通常串口的IAP升级传输都是应用 Ymodem 协议,该协议成熟稳定可靠,且支持单文件传输,多文件传输,断点续传,文件校验等功能,另外重要的一点是许多串口通信软件都内置了对 Ymodem 的支持,而无须开发者自行实现上位机,具有良好的易用性兼容性。
上位机软件的应用:SecureCRT,自带 Ymodem 协议,也支持脚本,非常适用自动化升级使用,不过要收费;Windterm ,支持 Ymodem 协议,但是不支持脚本;XShell等
升级详细设计流程
以思路四为例:
需要设置一个标志位以供判断,用于标志 当前传输是否完成、是否需要搬运升级、搬运是否完毕、升级是否成功
bin文件要包头包尾、文件校验码,传输时可以作为验证校对用
DCD配置,APP写入一些需要配置的硬件数据到指定的存储空间,Bootloader 读取这部分内容进行相应的配置以实现相应的功能,这样可以使得Bootloader更具可移植性,将与硬件相应的设置部分抽离出来
从APP转至运行Bootloader的方法
系统复位
直接调用 CMSIS 提供的接口NVIC_SystemReset()
即可,该操作会把内核及所有外设重置
1 | void reboot(void) |
内核复位
内核复位,主要包括内存、NVIC、Systick等内核部分,而不会复位外设,即当程序复位到 0x8000000 重新执行时,外设比如GPIO引脚、定时器等还保留为 APP 运行时的状态,可以利用这点实现特殊的业务设计:允许复位,但对外设又有特殊要求:某一个IO状态不能因为复位而改变,某一个定时器计数值不能改变等。
在内核复位的情况下,不考虑中断的禁用等问题,因为NVIC中断控制器会被重置为激活状态
1 | void kernel_reset(void) |
当然,使用内核复位要谨慎注意外设状态可能对系统运行状态的影响,如下:
- 应用了DMA的外设,要注意可能的问题
- 部分外设初始化流程不甚合规的,可能会导致复位后外设初始化失败,建议在配置初始化外设时首先先复位此外设。比如:ADC外设在初始化时,并没有先
DeInit
进行外设复位,而且在内核复位时也没有复位此外设,那么在内核复位后再次初始化ADC外设可能会失败。
跳转复位
设置好中断向量以及配置好栈顶指针后,直接跳转至 Bootloader 的复位中断开始运行,在这过程中内核及外设都不会自动复位,可由用户根据业务需要进行配置
示例代码参考如下:
1 | typedef void (*iapfun)(void); |
Bootloader跳转至APP代码片段
以思路四为例 —— 主要工作流程为芯片上电->执行bootloader代码->初始化时钟和配置必要外设->检测是否需要进行固件更新,是-则将新固件从存储区拷贝替换至应用程序运行区,然后跳转至应用程序入口;否-则直接跳转至应用程序入口开始执行业务。
适用于ARM-CC:
1 | typedef void (*iapfun)(void); |
适用于arm-none-eabi-gcc:
1 | //设置栈顶地址 |
注意点及疑惑解答
常规编码注意点
内部Flash和片外Flash的擦除单位可能不一致,2K、4K字节不等
SPI Flash的写,写内部Flash要以4的倍数字节写进,外部不限制
写Flash须考虑4字节对齐
内部Flash编程以一个字为单位,如果非4字节对齐,会导致硬件错误
内核复位需要关闭相应的外设,如DMA、等等
跳转复位需要关中断、清标志位
另外在外设初始化时,需要deinit或者disable一下,否则可能会导致ADC、DMA配置失败看门狗的喂狗问题,即使在ymodem传输过程中,也要保证喂狗避免芯片复位
文件升级如何进行保密
加密升级包:在服务器上对固件镜像进行加密,然后传输到设备。设备拥有正确的密钥,解密升级包,然后进行升级。
设备认证:升级前对设备进行身份验证,可以通过设备证书、密钥对验证、或者设备ID是否在数据库等方法。
此外,芯片也要加上读写保护,如L1 L2级别的保护
内核复位-IAP升级失败原因分析
常规原因排查:
- 跳转前未关中断;
- 中断控制器没复位清零;
- 中断向量表偏移未设置正确;
- 部分会影响到程序运行的外设未关闭,如DMA会刷新SRAM,可能会影响到bootloader程序的运行,导致crash
- 系统时钟配置不一致,bootloader与APP的系统时钟初始化应该保持一致
- 可能的原因:bootloader与APP的编译链接选项尽可能保持一致,如硬件浮点、函数/数据编译分小节、所用C库、优化选项等
在实际应用上述思路四的IAP升级时,使用内核复位至 bootloader 搬运更新 APP 程序,而后跳转至 APP 程序,在此过程中程序跑飞,未能成功升级,排查初步分析如下:
- 程序内核复位至bootloader后,能够正常擦除原有APP,并已将新APP写入APP运行区。所以推测认为是bootloader程序在跳转至新APP时,跑飞,导致升级失败。
- 有时发现测试IAP能够成功,发现如果原有APP跟升级APP的
text
代码段大小一样,则升级成功。如果原有APP跟升级APP的text
段大小不一样,则升级失败。(若代码段发生变化必会失败,但常量字符串大小变化即.rodata变化仍能够升级成功) - 如果是完全的系统复位至bootloader则能升级成功,所以初步认为是原APP对运行环境的影响导致的升级后跳转失败。
- 在bootloader跳转后并无任何信息打印,可以得出新APP在串口初始化时或之前已经跑飞。
- 有可能是:向量中断偏移错误导致,需要在bootloader跳转前就将向量中断偏移更新,APP代码段的变化会导致出错,因为代码段的变化会导致实际的中断服务函数地址变化
- 跟中断应该无关,内核复位时已经复位所有中断,且bootloader能够正常编程。所以有可能是外设或者某些代码片段重复初始化,且会涉及到代码段以及flash,从而才导致异常。
- 可能是系统时钟设置不一致问题,目前APP初始化时会重新设置系统时钟,如果没有这部分设置,会导致程序异常。
- 不同存储区跳转运行,需要关闭ICACHE
- 对比此前已成功实现内核复位升级的工程,有何区别?
- 移植CmBacktrace至bootloader,查看具体跑飞原因
- 用GDB在线调试运行bootloader查看hardfault时候的寄存器值,PC指针在何处,LR指针
问题排查解决分析结论:
- 跟芯片型号有关系,此前实现内核复位升级成功的型号为N32L406,而现型号为N32G452RC,后续与原厂FAE进行沟通联系
如何保证bootloader升级失败不会导致设备变砖?
保证接收到的新APP是正确的,传输完毕后校验文件无误
保证在搬运编程过程中,即使是突然断电或者复位后,也能继续搬运或者重新搬运至完成
在实时操作系统环境中,如何执行跳转操作?
操作系统在线程中使用PSP堆栈指针,在中断中使用MSP主堆栈指针
设置CONTROL寄存器的bit[1]
选择使用哪个堆栈指针。CONTROL[1]=0
选择主堆栈指针;CONTROL[1]=1
选择进程堆栈指针
直接PC跳转复位,实际上内核并不会自动切换MSP和PSP,因为此过程没有触发内核Handler模式,需要用户手动切换