0%

【MCU】基于MCU的IAP/OTA升级软件设计思路及流程分析

前言

初次接触 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
2
3
4
5
void reboot(void)
{
__set_FAULTMASK(1); // 关闭所有中断
NVIC_SystemReset(); // 系统复位,以上两个函数位于 core_cm3.h 文件或 core_cm4.h
}

内核复位

内核复位,主要包括内存、NVIC、Systick等内核部分,而不会复位外设,即当程序复位到 0x8000000 重新执行时,外设比如GPIO引脚、定时器等还保留为 APP 运行时的状态,可以利用这点实现特殊的业务设计:允许复位,但对外设又有特殊要求:某一个IO状态不能因为复位而改变,某一个定时器计数值不能改变等。

在内核复位的情况下,不考虑中断的禁用等问题,因为NVIC中断控制器会被重置为激活状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void kernel_reset(void)
{
__DSB(); // ARM汇编语言中的一条指令,用于确保在指令执行期间对内存的访问顺序
__disable_irq();

// 可选择在此设置看门狗
// 关闭或者设置有可能影响 Bootloader 程序运行的外设

SCB->AIRCR = ((0x5FA << SCB_AIRCR_VECTKEY_Pos) |
(SCB->AIRCR & SCB_AIRCR_PRIGROUP_Msk) |
SCB_AIRCR_VECTRESET_Msk);
__DSB();
while(1);
}

当然,使用内核复位要谨慎注意外设状态可能对系统运行状态的影响,如下:

  • 应用了DMA的外设,要注意可能的问题
  • 部分外设初始化流程不甚合规的,可能会导致复位后外设初始化失败,建议在配置初始化外设时首先先复位此外设。比如:ADC外设在初始化时,并没有先DeInit进行外设复位,而且在内核复位时也没有复位此外设,那么在内核复位后再次初始化ADC外设可能会失败。

跳转复位

设置好中断向量以及配置好栈顶指针后,直接跳转至 Bootloader 的复位中断开始运行,在这过程中内核及外设都不会自动复位,可由用户根据业务需要进行配置

示例代码参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
typedef void (*iapfun)(void);

iapfun jump2bootloader;

void app_jump2_bootloader(void)
{
// 可设置看门狗

u32 bootxaddr = 0x08000000;
if(((*(vu32*)bootxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法
{
__disable_irq();
SysTick->CTRL = 0;
SysTick->LOAD = 0;
SysTick->VAL = 0;
for (int i = 0; i < 8; i++)
{
NVIC->ICER[i] = 0xFFFFFFFF;
NVIC->ICPR[i] = 0xFFFFFFFF;
}

// 可配置必要外设 RCC_DeInit();

SCB->VTOR = 0x08000000; // 设置中断向量表基地址

__set_CONTROL(0); // 设置使用MSP主堆栈指针
// __set_PSP(*(volatile unsigned int*) bootxaddr);

jump2bootloader = (iapfun)*(vu32*)(bootxaddr+4); //代码区第二个字为程序开始地址(复位地址)
MSR_MSP(*(vu32*)bootxaddr); //初始化boot堆栈指针(代码区的第一个字用于存放栈顶地址)

jump2bootloader(); //跳转到bootloader.
}
}

Bootloader跳转至APP代码片段

以思路四为例 —— 主要工作流程为芯片上电->执行bootloader代码->初始化时钟和配置必要外设->检测是否需要进行固件更新,是-则将新固件从存储区拷贝替换至应用程序运行区,然后跳转至应用程序入口;否-则直接跳转至应用程序入口开始执行业务。

适用于ARM-CC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef void (*iapfun)(void);

//设置栈顶地址
//addr:栈顶地址
__asm void MSR_MSP(u32 addr)
{
MSR MSP, r0 //set Main Stack value
BX r14
}

//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法.
{
jump2app=(iapfun)*(vu32*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址)

// SCB->VTOR = appxaddr;

MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump2app(); //跳转到APP.
}
}

适用于arm-none-eabi-gcc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//设置栈顶地址
//addr:栈顶地址
void MSR_MSP(uint32_t addr)
{
__asm__ volatile (
"MSR MSP, %[addr] \n\t" // 设置主堆栈指针
"BX LR \n\t" // 返回
:
: [addr] "r" (addr)
);
}

//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法.
{
jump2app=(iapfun)*(vu32*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址)

// SCB->VTOR = appxaddr;

MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump2app(); //跳转到APP.
}
}

注意点及疑惑解答

常规编码注意点

  • 内部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模式,需要用户手动切换

参考站点