嵌入式开发<单片机软件升级>
前言
本人从事单片机软件设计工作多年,既从事过裸机系统的设计,也在小型嵌入式实时操作系统下进行过设计。在这些年的开发工作中,对产品设计逐步形成了一定的认识,并进行了较多的总结。因此,一直以来,都想整理这些东西,希望对有需要的人能提供一些帮助或参考。
在诸多的想法中进行系统的整理其实还是有一定的难度。之前本人已经对一些较普通的驱动进行了整理,并进行了优化,感兴趣的朋友可以前去看看(其中包含完整代码并已经调试通过)。
其实,在产品设计中涉及到诸多方面,比如产品的信息与参数,产品的调试信息与运行状态数据(甚至形成关键日志),产品的软件升级方法等等。这些东西应该形成一个统一的规范,便于公司及开发同事形成共同的方法,对于设计与维护都会有益。关于产品的信息与参数这方面的内容,我在之前的嵌入式调试工具(串口调试工具与网络调试工具中有详细说明)。
当然,这些东西太多,肯定不会一股脑的写出来。这次的主题是软件升级,而在这里主要讲的是单片机在线升级,其可用于本地升级也可以用于远程升级。软件升级不仅解决其本身问题,也要解决软件升级安全性等一系列问题。
一、单片机软件升级方式
对于这个,从大的原则来讲,应该就是两种方式:ISP升级与IAP升级。
ISP升级主要完成整个软件从起始地址开始的擦除及写入,并且一般会提供开机的boot模式设置,然后利用官方或第三方的专用软件,执行升级操作。这种方式的缺点显而易见,就是只能在本地且在启动时升级,不能完成远程升级功能。并且,这种升级方式一般提供UART接口,同时与之有相同功能的是使用ST-LINK或JLINK工具进行升级。
IAP升级属于本文重点需要讲解的一种升级方式。其特点是软件升级发生在应用程序执行过程中,并且在应用中检测到软件升级,然后执行文件接收与写入。这种升级方式可以通过多种接口进行,既可以使用UART接口,也可以使用网口、GPRS网络、无线网络、SD卡、USB等等。其使用的接口其实没有任何限制,只要能与应用程序进行通信即可。
这里首先作一个说明,凡是你使用的MCU具备IAP功能的,也就是能够在代码里操作FLASH的读写与擦除的,基本都可以做IAP升级功能。另外,有些MCU具有通过寄存器对中断向量进行修改,如ARM M3/M4内核的MCU,还有一些不能通过寄存器修改中断向量,如ARM的M0及一些51 MCU。如此,就有两种设计方法,这里首先对第一种进行说明(大部分都是这类的方式),最后在第六章节中介绍第二种。
二、IAP升级原理
IAP升级被称为在应用升级,其原理是利用一个区域的BOOT完成对另外一个区域的APP代码写入。要完成一个IAP升级的设计,需要涉及到几个方面,下面依次说明。
1.FLASH区域划分
要设计IAP升级,首先需要规划FLASH区域的划分,这里提供3种划分方法,分别介绍如下
1) BOOT区、APP区、存储区
A. BOOT区是以MCU复位后的地址开始,指定的一片区域,其大小要能完成BOOT代码的存储即可;
B. APP区则是应用软件运行区域,其代码大小应该是整个FLASH减去BOOT大小后除以2。当然一般不能整除,只需要取整数即可;
C. 存储区则是除应用软件外剩余的区域;
2) BOOT区、APP1区、APP2区
其区域划分与第一种相同,只是APP1与APP2两个区域均可运行应用软件;
3) BOOT区、APP区
这里实际上就是没有存储区,以这种方式划分,主要是FLASH大小不够,只能全部分给APP区域;
2. FLASH各个区域作用
以上区域的划分中,归纳起来则只有3部分,即BOOT区/APP区/存储区,以下分别说明其功能作用。
1) BOOT区
在其中存储BOOT代码,主要完成升级文件的接收(可选)、APP区代码擦除、APP区代码写入等操作。并对写入代码进行验证,最后执行APP运行代码的跳入;
2) APP区域
在其中存储的是应用程序代码,其执行需要依赖于BOOT程序,并且其执行也由BOOT程序跳入。另外,为了能正确执行代码,在应用开始执行时,应该改写中断向量(ARM MCU除M0内核外);
对于此区域被称为APP1以及存储区被称为APP2的情况有些不同。应用层程序应该能识别其所运行的区域,以便在修改中断向量时可以根据自己运行的区域进行修改,否则会出现有一个区域的运行不正常;
3) 存储区
这个区域主要用来存储在应用中接收通信传输过来的升级软件代码。并在接收存储完成后,进入BOOT进行升级文件写入APP区;
三、IAP软件BOOT设计
如何设计BOOT代码呢,这得从BOOT代码的作用说起。原则上来说,软件运行首先进入的是BOOT代码,而BOOT代码要完成两个工作,一个是等待用户升级,另一个是跳转到应用程序中。而如何等待用户升级呢,我见到过有两种设计。
1. 第一种设计方法
BOOT运行时,检测一个I/O口,如果I/O口符合要求,则跳转到升级代码部分,执行升级功能;否则跳转到APP区域运行应用程序。这种方法,需要人为的去设置I/O口,最为常见的应该就是设置跳冒。这种方法其实无异于ISP,使用IAP设计就是画蛇添足,除非官方没有提供ISP方法,仅能使用ST-LINK或JLINK;
2. 第二种设计方法
BOOT运行时,定时几秒等待用户升级,如果在等待时间内接收到了用户的升级命令,则执行升级功能;否则跳转到APP区域运行应用程序。这种方法比起第一种方法应该会有所改进。但是还是有一个问题,我们的设备大多不会执行软件升级,那为什么每次运行时都要等上几秒呢;
3. 第三种设计方法
这是我所采用的一种设计方法。主要是设计一个升级运行标志,其被存储在升级参数中,定义包括但不限于以下几种状态:
1) 准备升级
这种状态,表示BOOT需要立即进入升级状态,并进行APP区域FLASH擦除;此状态需要准备接收代码文件,再写入FLASH;
2) 准备写FLASH
这种状态,表示BOOT需要立即进入升级状态,并进行APP区域FALSH写入;此状态表示之前已经接收完代码文件,现在仅需要写入FLASH即可(即不需要再接收文件);
3) 准备跳转APP
这种状态,表示BOOT已经完成软件升级,准备执行APP代码,并由此执行跳转工作;
4) 准备运行APP
这种状态,表示BOOT马上启动跳转APP试运行,并在APP中检测运行正常后,将此标志修改为运行正常;
5) 运行APP正常
这种状态,表示APP已经升级并运行正常,BOOT仅执行跳转工作即可;
所以,在设计了这个标志后,BOOT运行时,仅需要检查以上标志。在检查到除运行APP正常标志以外的,均需要执行BOOT后面的工作。以下将介绍所有的设计细节,但这里不再使用代码说明,仅讲解逻辑部分。
4. BOOT运行的几种工作模式
其实,所要描述的几种模式与前面介绍状态的一致,在这里只是稍加详细的描述其工作的内容。
1) 准备升级模式
这种模式有两种情况进入,一种是初始状态,也就是FLASH完全是没有软件的,仅在写入BOOT后执行检查标志时,没有一个符合的标志状态数据,则被初始为此标志。另一种情况是在APP中接收到本地升级命令,则在APP中修改此标志,然后重启设备执行BOOT时检测到此标志;
在此状态,则意味着需要进行升级,此时应该进入APP FLASH擦除操作,并在完成后执行自定义的升级协议,执行升级软件代码接收与写入,并完成代码验证,最后修改标志为准备跳转APP;
2) 准备写FLASH模式
这种模式属于应用程序在通信中接收完成APP代码数据,并存在存储区,进行标志修改后重启设备进入BOOT。此时,BOOT仅需要完成APP代码写入即可。并在完成后修改标志为准备跳转APP;
3) 准备跳转APP模式
这种模式属于BOOT中完成APP写入后,执行复位前写入的准备跳转标志。目的是复位后可以执行跳转(没有在写完后执行跳转,主要是需要复位硬件部分)。在这个状态执行时,需要设置准备运行APP标志,然后跳转到APP;
4) 准备运行APP模式
这种模式说明跳转APP后,运行异常,可能是APP代码有问题,可能是用户干预了APP的执行(比如在运行开始按下了复位按钮),此时应该交给用户选择是否再次升级,在此期间设置几秒钟的等待升级时间,当用户执行升级时,则进入准备好升级状态,并执行后续的升级工作;
5) 运行APP正常模式
这种模式说明APP运行正常,无需执行BOOT的其它工作,仅执行跳转APP即可,也无需修改任何标志;
以上几种模式,是我在编写BOOT时设计的几种工作模式,同时也说明了对应的代码设计工作。也就是实现了整个完整的BOOT设计逻辑。当然,这里没有描述具体的升级协议,这个由你自行定义,也可以通过我写的串口调试工具或网络调试工具软件获取我使用的通信协议,并使用此工具完成在线升级。
当然,远程升级的通信协议就需要你自己完成,我并没有提供此协议内容。
以下是我写的两个工具软件下载链接,里面包括我所讲到的升级协议内容及其上位机的实现
串口调试工具 软件下载
网络调试工具 软件下载
四、APP软件的设计
APP代码中主要进行应用升级的设计,这里主要包括两部分。一个是软件升级后的试运行处理,另一个是在线升级设计。当然,还包括一个工作,则是在运行起始位置,修改中断向量。
1. 软件升级后的试运行处理
这个概念是我定义的,其指的是升级跳转后,首次运行APP时,检查到升级标志为试运行APP,则启动一个定时操作(如定时10秒)。定时时间到达后,在主程序或主任务中写入运行APP正常标志。
这个设计原则上应该保证,APP运行异常时,不会执行主程序或主任务中的写入运行APP正常标志,否则就没有意义。当然,这个也只能是一般性的保证,但基本可以检测升级代码是否正常运行,在实际运用中效果还是比较明显的。它保证了我们在升级有重大问题的APP代码后,提供了再次升级的机会。
2. 在线升级处理
这个包括两种方式,一个是本地升级(或无法划分存储区情况),另一个是远程升级。其执行的方式完全不同。
1) 本地升级
这里主要是通过通信接口(如串口),接收升级命令,然后应用程序完成升级标志的修改,即修改为准备升级,再执行复位操作并进入BOOT升级;
当然,本地升级还包括一种方式,即SD卡或U盘升级,这种方式以读取指定文件名方式识别升级文件,并在分析文件后,进行存储区的写入。然后设置准备写FLASH标志,复位设备进入BOOT升级。
注意:对于SD卡或U盘升级,你完全可以在存储卡插入时,检测到升级文件后,读取文件,并检查版本号与文件日期(或校验和),以确定是否需要升级。我认为软件应该尽量做到此类工作,而不是一味的弹出提示框要用户去选择是否升级。
2) 远程升级
这里将完成在通信层面的远程升级文件接收与存储区的写入。此类升级一般用在无线或以太网接口上执行。尤其是GPRS网络,此时最好设计断点续传,也就是升级中断后,下次可以从断点位置继续接收文件。
五、完善的软件升级设计
如果设计软件升级仅仅包括以上内容是不够,它应该包括更多的内容以辅助完成升级操作,以及提升软件升级的功能,以下将介绍升级参数中更多的内容细节。
1. 提供必要的信息
软件升级在启动时,应该提供升级文件大小、日期、文件校验和、传输包大小。当然,其中的日期不是必要的,但其可以用于简单的判断升级文件与本地运行代码是否是一致的。其它的信息就非常必要,并且也是必须的;
1) 文件大小
可用于检查APP FLASH区域是否能够存储下即将需要升级的文件,也可以用于在升级完成后读取FLASH区域的数据大小,用于验证文件校验和。还可以根据传输包大小计算传输总包数;
2) 文件校验和
用于与从FLASH中读取文件计算的校验和进行比较,判断写入FLASH是否完全正确;
3) 文件包大小
可以灵活的修改传输文件包大小,使得升级文件传输更加灵活;
2. 提供安全升级的更多信息
记得有一次我在一个公司面试时,面试官问了我一个问题:说怎样防止软件升级使设备变砖?在我告诉了他我下面的方法后,他理解了我的设计是可以避免那种情况的。也就是以下的两个方法,可以避免设备升级出现问题后无法再次升级(或可以保证不因为错误升级导致设备不能正常运行)。
我们考虑如下两个问题:
1) 比如用户使用其它的软件对我们的设备进行升级,这会怎样呢。轻则运行不正确,重则无法运行。甚至如果我们的产品处于正常情况下无法维护时,这将是灾难性的后果;
2) 如果我们设计的软件存在某种暂时未检测到的BUG,使得其在特定的环境下,无法正常运行,可能处于反复重启中,或死机(通过看门狗复位),其实这也是一种不可接受的灾难性后果;
出现这两种情况时,那我们如何尽量避免问题的发生呢,考虑到这两点,我们可以做以下两种设计:
A. 设计可识别的产品型号与版本号
在我们的APP代码中,在固定的位置(这个可以自定义,只要不占用中断向量位置即可),预定义软件产品型号与版本号(其实就是在固定FLASH位置存储数据),并且一定是便于识别(有固定识别符)。同时,在BOOT中定义此产品型号及读取方法。
此时,可通过两种方式共同进行验证产品软件的合法性。
a. 在启动升级时,要求升级工具发送产品型号,此时BOOT很容易检测到是否为合法的软件;
b. 在升级过程中,在文件的预定义位置读取产品型号,再次进行检测,进一步验证软件的合法性;
通过以上两种方式,只要不是预先知道我们的设计细节,并进行恶意的破坏。基本不会出现升错软件的情况。并且在发现错误软件时,及时拒绝升级。
B. 在BOOT区设计两个APP区域
也就是设计APP1与APP2区域,如此设计的目的就是在软件运行错误时,可以回溯到前一个软件版本执行。
如此设计就需要做三个工作:
a. BOOT设计需要记录当前运行的APP区域,使得其在试运行错误时,可以切换跳转到另外一个APP区域;
b. APP设计需要检测当前运行的APP区域,以便修改正确的中断向量;
c. APP在应用中升级时,接收的文件直接写入前一个版本的FLASH区域,并在完成后修改APP运行区域及升级标志,并重启软件;
注:设计两个APP运行区域,其最大的作用应该体现在此。
另外,使用两个APP区域还有一个问题需要注意,就是需要严格规定版本编译设置,比如奇数版本在编译环境中定义的地址对应APP1区域,偶数版本在编译环境中定义的地址对应APP2区域,否则是不能正确运行的。如果不能严格按照规定执行,则每次升级软件时,需要检测代码是属于哪个区域的(通过中断向量可以检测),假如升级代码是当前运行区域,则只能跳转到BOOT中执行(不能将代码写到当前运行区域)。所以,这个方法还是有一些限制和约束,比较麻烦一些。
但是,如果系统中存在SPI FLASH之类较大的存储器,则可以在SPI FLASH中划分2个APP存储区域,用以交替存储APP代码,而MCU FLASH中则恢复到只有一个运行区域,并且代码写入需要放在BOOT中。这样实现上述两个APP区域交替升级,以及版本回溯就完全没有问题,而且相对非常简单。
3. 提供多途径升级的接口
在一个应用中,可能会提供多个接口,比如RS485作为通信,RS232用于调试。而BOOT里面首先应该考虑RS232本地升级,但实际上还可以通过RS485通信接口进行升级。此时,我们可以设计两个升级接口,并在BOOT中对两个接口进行检测,任意一个接口收到升级命令时均可执行,尤其是还可以自由切换升级接口。
当然,你也可以在升级参数中设计升级端口参数,并在BOOT运行时检测当前升级端口,并有目的的在指定的升级端口执行升级。比如,在APP应用中如果接收到的是RS232升级,则将升级端口设定在RS232中,反之设定在RS485中升级。
另外,这种设计方法在有些情况下可以避免拆机升级(如RS232接口没有接出来,就可以利用RS485接口进行升级)。
六、不能修改中断向量的软件升级设计
最后,针对ARM M0 及51 单片机等(具备IAP功能的51 MCU)特殊的MCU如何进行IAP设计做一个简单说明。同样,有两种设计方法介绍如下。
1. 修改中断向量中的入口地址
这里需要定义另外一个BOOT中断向量备份区域,用以存储BOOT的中断向量代码(一般是前面255个字节,当然这个需要你了解芯片资料),并且在更新BOOT并首次执行时,拷贝BOOT中断向量到BOOT备份区域。
在BOOT启动时,判断需要进入BOOT区域时,而当前的中断向量却是APP的,则首先执行备份区BOOT中断向量拷贝到BOOT中断向量区,再执行其后功能。
同时,在BOOT功能执行完,需要跳转到APP区域时,执行APP中断向量拷贝到BOOT中断向量区进行覆盖。
但是,请注意!此时需要修改复位中断向量的地址指向BOOT执行代码,因为每次复位运行时,首先应该执行BOOT代码进行检查,并在确定需要跳入APP时,才将原复位向量指向的APP地址设置为跳转目标。
2. 备份BOOT代码
这个方法与第一个有些相似,但与之相比有一些复杂。(你也可以忽略此部分的阅读)
这里需要定义额外的两个FLASH区域,一个作为BOOT代码备份区域,用以存储BOOT代码(这个就不需要了解芯片资料),在运行BOOT时,检测BOOT代码备份区是否有BOOT代码,并且在没有时进行复制。另一个作为APP前面一段代码的临时区域(其长度为BOOT代码长度),并且接收到升级文件的前面一段代码(长度为BOOT代码长度)写入临时区域,其余代码从APP代码区域开始写入。在代码写完后,需要将APP代码临时区域的数据复制到BOOT区域进行覆盖(并且这部分的执行代码不能在BOOT区域)。
这里有一点与前面的介绍不一样,即APP在下载后并启动时,就没有BOOT代码(因为其已经被覆盖),而仅运行APP代码。但注意,BOOT代码在备份区仍然存在,仅仅在需要升级时,将复制备份区代码到BOOT区再复位进行升级。
另外,在软件升级过程中,最后将APP代码临时区域复制到BOOT区域时,其使用的调用代码不能放在BOOT区,只能放在额外不可覆盖区域。
所以,这个方法有些复杂(写出来好像却体现了自己有些愚笨)。只是因为我在一个项目中使用过而已。也仅仅是当时我没有想到使用第一种方法,所以,我还是建议你使用第一种方法更好。
3. 51 MCU升级的额外说明
最后需要说明一下51 MCU在使用第一种方法时需要注意的问题。因为51单片机一般FLASH都不会很大,所以为了节约FLASH空间,最好的方法不是存储整个中断向量内容,而是有目的的直接修改其需要的向量地址,尤其是当你知道需要具体修改哪一个时更好。我在一个项目中就是这样做的,其实也非常方便实用。
七、扩展说明–如何利用通信接口完成设备升级设计
1,我们常用的软件升级接口是使用RS232,也就是利用设备里面的调试串口升级,但当设备已经被装上外壳后,使用调试串口就不太方便了。这时,有些人会想到将调试串口引出来,并在外壳上提供接口,这样确实也可以使用,但在有些情况下其实这是完全没有必要的;
2,当设备能对外通信时,设备往往具备RS232/RS485/CAN/ETH/GPRS等等接口,我们完全可以利用这些接口实现软件升级,不必完全依赖于调试串口,更不用将调试串口接出来仅仅为了调试升级;
3,那么如何巧妙的利用这些接口进行软件升级呢,在这里,我想仅仅针对常见的RS232/RS485/ETH接口做一个设计说明(因为我自己本身有这样的针对性设计);
1)首先,当然你可以想到的是利用通信协议来进行升级,也就是约定好,增加通信命令,用来传输升级文件数据,这也没啥问提,至少是可以实现的;
2)但是,如果你使用的是RS485总线并且实现的是Modbus RTU协议,这个时候你会发现它的问题所在,因为Modbus协议规定了一帧仅能传输最多255个字节的数据,如果想利用这种协议升级文件,假如你的升级
文件有几百KB,想想一次升级要花费多少时间呢,我曾经的工作当中还真有同事使用过这个方法,具体升级时间不记得了,但确实是比较长的;
4,我设计过两款通用的调试工具软件,一个是串口调试工具,另一个是网络调试工具,都提供了我自定义的软件升级模块,其本身就可以很方便的对设备进行在线升级。我的方法是建立在此工具软件基础之上的,下面分别说明
1)如果设备使用RS232/RS485通信接口,可以首先停止设备原有的通信软件(PC端)运行,打开串口调试工具软件,然后逐一单独对线路上个的每一个设备进行升级(不用将设备从总线上拆开);
其实,完成这个功能非常简单,只需要在原有的升级设计基础之上,增加一个设备地址作为升级参数。
每一个设备从通信接口RS232/RS485上接收到数据时,首先检测是否为升级命令,并同时检测升级设备地址,如果设备地址是本设备,则可以进入BOOT或在应用内接收后续的升级文件,最后完成升级文件的写入。在设备重启后,向总线上发送一个升级完成的字符串信息即可。
至于其它设备,在检测到升级设备地址不是本设备时,则进入通信静默状态,此时其不再执行通信协议的解析,对升级文件包也不做应答,仅仅需要解析特定命令字符串,包括升级设备的完成通知和其它命令串,并在解析成功时,恢复通信工作。当然,这里需要设置一个最大等到时间,使得其在超时后可以自动恢复通信。
当然,如果设检测到的数据不是升级命令,则可执行原有的通信协议解析处理。
最后,其实这里还可以利用通信接口实现调试工作,如果在执行通信协议解析处理时,发现其内容并不是原本的通信数据,则可以将数据抛给调试解析模块处理。
2)如果设备使用的是ETH/GPRS通信接口,这个就更简单了,只需要创建一个调试服务端,接收远程客户端连接,这样既可以实现远程调试,也可以实现远程升级。而其对应的则是网络调试工具软件,我的这个软件在调试升级功能上与串口调试工具软件完全一致,不需要额外的软件设计开销。另外,当使用RS485通信,但使用了串口服务器时,可以将工具软件换成网络调试工具执行相同的升级。
以上设计说明可能不是非常的详细,但我主要是特别想说明一点,设备的软件升级是可以利用现有的一切接口去实现的,只要最底层与最上层我们设计好了统一的结构做支持,中间的层面其实是非常容易适应及转换的。
附
如果你感兴趣,可查看其它文档.
单片机软件常用设计分享(一)驱动设计之按键设计
单片机软件常用设计分享(二)驱动设计之LED灯显示设计
单片机软件常用设计分享(三)驱动设计之数码屏显示设计
单片机软件常用设计分析(四)驱动设计之串口驱动设计
嵌入式开发<串口调试工具>
嵌入式开发<网络调试工具>
嵌入式开发<单片机软件调试>