固件升级经验总结
导读:上面理论+实践当初花了3天时间弄完的,但是,当你真正做项目的时候,你会发现,只有上面的这些知识还不够,还有更多的细节要去处理:
1. APP程序怎么跳转到BootLoader程序?
2. APP程序和BootLoader之间是否会互相影响
3. APP和BootLoader之间如何传递参数?
4. 固件更新一到一半,因为某种原因失败了(通信错误、掉电),该如何处理?
5. 如何确保更新的APP是你需要的APP,而不是别的一个APP?
经过一个项目的固件升级功能洗礼,以上问题都得到了较好的解决,为了避免以后忘记,在此记录一下。芯片为:STM32F103ZET6
第一个问题,APP程序怎么跳转到BootLoader程序?看似很简单,因为这是基本的功能,但是实际情况并不简单。
由前面的小节了解到,从BootLoader跳转到APP可以通过指针进行跳转,但是当你从APP通过指针跳转到BootLoader时,发现会出现问题(具体原因不明,有机会的话去研究一下)。那么又该怎么办?
可以通过复位的方式,让程序重新从开始地址运行,有以下几种方式复位:
1、 内核复位
2、 系统复位
3、 上电复位
第一、第二种方式都是通过设置相关寄存器使单片机发生复位的,两者的区别就是,内核复位只复位芯片的内核,但对单片机的片上外设并不进行复位,比如USART、SPI、USB等外设是不会进行复位的。
系统复位的话,就会对整个芯片进行复位,不管是外设还是内核,都会回到最初始的状态,就如按下复位按键一样。
最后一种上电复位,其实和系统复位、按键复位的效果差不多,都是会进行全部复位的,不过这个需要外部硬件控制单片机的电源的开启与关闭,增加了额外的硬件。
一开始鱼鹰准备采用系统复位的,直接设置寄存器触发导致复位,因为这样更彻底,测试发现项目中的单片机根本无法复位,而我自己的开发板是能进行复位的,后来经过硬件工程师的查找,发现是看门狗电路导致无法复位,这样一来,系统复位这条路堵死了(因为项目的硬件已经确定,无法再更改了)。
那么是否有其它方法,前面提到的指针跳转的方式发现会出现问题,因为项目比较急,就没怎么花心思解决。后来在调试过程中,突然发现KEIL中的复位按钮是能进行复位的,那么问题就简单了,既然调试器能进行复位,那我应该也能进行复位才对,之前说了系统复位不好使,那么按下复位按钮时应该是采用的内核复位(实际上CMSIS-DAP调试器是有单独的一条复位线的,但是当时没考虑它可能采用了这种方式,只考虑可能采用了内核复位,阴差阳错)。
那么就试试内核复位吧,一试发现果然有效,但是因为内核复位不彻底,导致出现了问题。
这就到了第二个问题,两个程序之间是否会有影响?
第一,首先从BootLoader对APP的影响考虑,我们知道,BootLoader程序也是需要一些资源的,比如串口之类的用于固件的传输,如果说BootLoader的寄存器和APP的寄存器配置要求不一样,那么就可能出现问题(鱼鹰的项目中还用了一个定时器喂狗,发现一进入APP程序就挂了,后来才找到这个原因)。
比如BootLoader采用串口查询的方式接收数据,而APP为了提高效率,使用DMA+空闲中断的方式处理,那么两者的寄存器配置肯定不同,那么该怎么消除BootLoader程序对APP的影响呢?
有人说,让BootLoader程序用完串口之后自动复位串口外设即可,确实,这是一种方法,但是你是否考虑过两个程序是独立的,万一后面的人在BootLoader程序中忘记了复位串口呢?所以说,靠别人不如靠自己,与其担心害怕别人不靠谱,不如APP自己去复位串口,即APP在配置串口之前,可以先复位串口,再进行配置(从这里可以知道,为什么有些代码会使用XXX_DeInit()之类的函数在配置前复位片上外设,一开始以为是多余的,毕竟一般程序开始运行的时候一般都是上电之后才运行的,这个时候已经复位外设了,为什么还要多此一举,直到现在才明白这才是安全的做法)。
第二,从APP对BootLoader的影响考虑,APP程序使用的资源一般比BootLoader的资源多,如果两者之间使用了相同的资源,比如串口,那么肯定得考虑两者的差异性,所以根据上面的考虑,也可以让BootLoader程序在使用串口之前先进行复位,然后再进行配置,这是比较安全的做法。但是仅仅如此就足够了吗?
在项目里的APP程序中,有一个加热过程,如果说APP跳转到BootLoader之前没有考虑这一点就盲目的运行到BootLoader,那么很可能出现APP正在加热,但是因为跳转到了BootLoader中运行,导致无法对温度进行控制,那么结果将是灾难性的,轻点的只是设备烧毁,重的可能就引发火灾了。
所以说,两者之间的影响一定要慎重考虑。
事实上,如果采用系统复位或者上电复位的方式,第二点关于APP对BootLoader的影响是可以不考虑的,因为系统复位或者上电复位自动将外设进行初始化了,但是你不能肯定你现在采用这些方式,以后就不会采用内核复位的方式,所以为了安全,还是要考虑进去。
现在说说第三个问题,APP和BootLoader之间如何传递参数?
首先思考为什么要传递参数?
在前面的小节中,选择让BootLoader程序在开始复位时等待一段时间再进入APP运行,在等待的过程中,就可以判断是否需要固件升级,比如等待时,由上位机发送一条特殊的命令确定是否升级,或者通过引脚电平等方式,反正就是要让BootLoader程序知道,下面我要开始升级了,别急着进入APP运行。
但这里有一个问题就是,这里需要一个冷启动的过程,即先上电后再接收命令,而且时间短暂,有一个好处就是,即使单片机中暂时没有APP程序,也能够实现固件升级过程,这样保证了由BootLoader接收升级命令而不是由APP接收,所以当初在无法解决升级到一半时如何恢复时有考虑使用这种强制升级的方式。
那么有没有更好一点方式,不需要冷启动过程,而是由APP决定是否升级?有的。
既然是APP决定是否升级,那么肯定需要在进入BootLoader之前给它传递一个参数,告诉它,这次复位需要升级,不能直接跳到APP中运行,那么BootLoader就会乖乖地等着升级了。
那么怎么传递呢?有人说往FLASH中写入参数,这样复位的时候就可以判断是否需要升级了,这确实是一个方法,但是我们知道,如果我们要往FLASH写入参数,那么必须先进行擦除工作才行,而擦除的往往是一个扇区,为了写入几个字节的参数,擦除几K的数据,鱼鹰感觉实在是太浪费了;还有这个参数保存地址也是需要好好考虑的,放在APP区还是BootLoader区?那么有没有更好的方式?
有的。还不只一种。
一开始鱼鹰想到的是利用后备域保存参数,因为如果有电池存在的话,它的数据是不会丢失的,但不巧的是,这个项目没有这个功能。
还有可以使用外部的FLASH空间,有些FLASH芯片是可以进行字节编程的,不需要整片擦除,挺合适的。但是缺点就是,你的项目要有这种芯片,而你的BootLoader需要写相应的代码驱动这个芯片,显然很麻烦。
最后鱼鹰采用的是RAM传参。鱼鹰在之前的小节说过,APP和BootLoader共用RAM,如果说能用RAM传递参数的话,只是操作一个变量,相当方便。
但是怎么保证两者之间顺利传递参数呢?
我们知道,C语言申请的变量空间是由编译器自动分配的,也就是说,同样申明一个同名变量,APP和BootLoader申请的变量地址不一定是一样的,而且还有一点就是,即使你申请的变量通过某些方法让它地址固定,也会有问题,因为申请的变量会在进入main函数之前会被初始化掉,当然你可以说通过某种方式让它不被初始化,但是鱼鹰想到了更好的方法。
通过指针直接操作RAM空间最后几个字节用于参数传递(之前有看到说STM32单片机中有个寄存器可以直接掉电不丢失,但具体不知道是哪一个)。
因为采用指针操作,所以编译器并不会对你指向的地址进行初始化,这样可以很方便的绕过编译器的处理。其次,通过操作最后几个字节,保证了这个空间不会被程序的其他变量占用(其实占用了也关系不大,只要你传递的参数足够特别,比如0x05055555,就问题不大)。
这样,BootLoader在复位后只要检查这个地址的值,就可以轻松知道是否该升级了。
但是还有一个隐患就是,在上电那一刻,如果这个地址的值刚好是你设置的特殊值(因为上电后,RAM的值是随机的),那么必然会出现问题,但这种可能性微乎其微,因为要让四个字节在复位哪一刻刚好都变成你设置的特殊值,简直比中彩票还要困难。
不过即使你真的中彩票了,重新上电复位一下就好了,如果说第一次中彩票还能接受,第二次还如此,那就需要烧烧香、拜拜佛了。
需要注意的是,一旦使用完这个参数,必须清零,防止下次内核复位又进入升级了(比如在线调试时可能会使用KEIL中的复位按钮)。
第四个问题,固件更新一到一半,因为某种原因失败了(通信错误,掉电),该如何处理?
我们知道,升级过程中很大可能是会失败的,但是单片机升级不像电脑升级,这次升级不成功,恢复成原来的系统就是了。单片机空间有限,没办法同时保存两份APP程序的,那么又该如何处理呢?
现在换个角度思考,你如何确定你升级失败了?如果能做到这一点,那么你的BootLoader程序就可以在升级失败后继续运行BootLoader的程序,而不进入APP运行那只升级到一半的程序(运行这种半残程序,鬼知道会发生什么怪异事件呢)。
如果从这个角度来看,其实就简单了,只要上位机把bin文件的大小发下来,然后由BootLoader程序判断是否升级完成就可以了,而Ymodem刚好可以有这个功能。
但是这样一来,就出现了一个问题,需要一个掉电不丢失的参数来保存是否升级成功,否则下次上电又会继续运行APP。但是鱼鹰对整个扇区擦除的方式很反感,就是不愿使用这种方式,怎么办?
苦思冥想之下,终于找到了一个巧妙的方式去处理。
我们知道,运行APP之前,一般会对APP的前面8个字节的栈顶指针、复位地址的合法性进行判断,判断是否是有效的APP程序,毕竟随便拿一个程序去升级,还不乱套了。
不管怎样,APP程序都是要往FLASH更新程序的,那么我们是否能利用这个过程呢?
APP更新之前,必定把该擦除的扇区进行擦除了,如果说我们一开始,就对写入的工作进行特殊处理,那么是否可以达到我们想要的效果呢?
比如说,前面8个字节,本来是在一开始的时候就会被写入的,如果我们在一开始写入数据的时候,跳过这8个字节的写入,然后把剩余的代码全部写入,当判断已全部接收到(大于等于bin文件大小,因为Ymodem协议稍微有点特殊,最后一帧数据可能填充0)固件之后,最后再对前面8个字节写入,这样一来,就保证了程序的完整性,如果说你中途数据中断了(掉电或上位机中断),那么前面那8个字节肯定不会写入,也就无法正常进入APP中运行了。
第五个问题,如何确保更新的APP是你需要的APP,而不是别的一个APP?
前面的问题保证了更新的程序是完整的,但是完整的程序不一定就是你需要的程序,那么怎么确定是你需要的APP呢
通过bin文件名来确定吗?这是一个方法,但是bin文件名可以被用户轻易更改,那就只能从bin文件内容本身入手了。
常规方法是,通过某些工具,在bin文件中加几个字节标着文件的特殊性,但是众所周知的是,鱼鹰比较懒,看似简单的只是加入几个字节的事情,如果产品成型的话比较好说,更新次数比较少,但是一旦产品处于测试阶段,更新频繁,累人不说,还可能出错。
那么有什么办法呢?就从代码本身入手好了。
方案确定下来了,但是怎么处理呢,标志位放在哪里,怎么放?又是一番苦思冥想。
一开始想到的是想将标志位放在bin文件最后,但是bin文件的大小是不固定的,虽然说上位机可以把bin文件大小传下来,但是怎么放是个问题,开始打算通过修改链接文件实现,但是发现自己对链接过程不熟,对汇编语言(汇编可以指定地址)也不熟,怎么办?
最终鱼鹰选择把程序标志放在向量表的后面,即通过指定变量地址的方式保证地址唯一性(需要注意一点的就是,通过指定地址的方式,不一定就确保最终的地址就是你设定的地址,如果和其它地址冲突了的话,可能不一致,需要看map文件确定)。而且在设置APP程序的时候,一般都会重新定义向量表的位置,那么可以在定义标志地址时利用这个地址进行偏移。
当BootLoader在写前面8个字节前,只要再判断这个地址的标志位是否正确即可。
到此,固件升级方面的知识应该比较完善了,但还有一个问题,如何对bin文件加密与解密呢?
这个问题只有下次项目需要的时候再研究了。
------------------------------------------------------------------------------2019-06-30 Osprey
推荐阅读:
-THE END-
如果觉得文章对你有帮助,欢迎转发、分享给朋友,感谢你的支持!
如果对本文有问题,欢迎留言!即使没有问题也可以留下走心评论。
如需转载请联系我。
微信公众号「鱼鹰谈单片机」
每周一更单片机知识
长按后识别图中二维码关注