最近由于项目需要,在AT91FR40162平台上实现了通过串口传输目标板二进制可执行文件并更新固化到储存执行代码的片内ROM中。在这之前,我进入公司以来,这个平台上,通过仿真器把程序下载到目标板是更新目标板固化程序的唯一途径。随着对嵌入式系统的逐步了解,我认识到存在不通过仿真器升级程序的方法:我们日常使用的嵌入式设备,如有线电视机顶盒,可以通过电视线升级软件,如路由器,可以通过网线升级软件,诸如此类,理论上有数据传输的通路就可以实现目标板软件升级。上网翻了下AT91FR40162的资料,不知道现在还有多少人用这块arm7...还好找到几年前的一些文档资料,Q&A,还有ATMEL免费提供的上位机软件ATMEL Host loader,但传说中配合使用的AT91 Flash Uploader却没找到。结合某已离职大牛遗留下没人再用的代码,还有其他平台的经验,终于在这个平台实现了串口升级。当中的一些学习心得和经验体会写下来作个记录并分享。
首先简要总结串口升级的原理。在我们的目标板里,这块ARM上电复位并执行memory remap之后,0地址映射到片内RAM,0x400000映射到FLASH ROM。由于片内RAM为256k,FLASH ROM为2M,目标板的执行代码储存在FLASH ROM,运行时大部分在FLASH ROM执行,只有少部分对速度要求高的映射到片内RAM。实现软件串口升级的基本思路,就是编写实现接收串口数据并把数据转存至FLASH ROM功能的代码,将其用仿真器下载至目标板,并保持以后每次软件串口升级之后FLASH ROM都保持有这样功能的代码。
接着就具体不同的需求来分析不同的实现方案。我之前在另一个DSP平台实现过串口升级,方法是目标板的日常运行代码里有串口升级的模块,当收到串口升级命令之后,按照预定的流程把二进制可执行文件数据从串口接收下来,储存在RAM里,在确认无误后,再刷写固化入FLASH ROM中。这个方案的要点是,负责串口升级的模块代码,特别是负责把数据刷写入FLASH ROM以及之后操作的相关执行代码和访问到的常量,必须加载到RAM里,以免升级FLASH ROM后代码位置改变而不能执行;此外RAM要提供一个大的缓冲区,用作储存二进制可执行文件数据。我分析过这次ARM7平台的代码量在100k多一点,也就是二进制可执行文件大小100多k,需要加载到RAM的代码和变量数据之和小于100k,这样256k的RAM刚好能满足要求。就这样,参照离职大牛的代码,了解这块ARM的FLASH操作之后,移植DSP平台的串口升级模块代码,便实现了软件串口升级的功能。我认为这方案特点是:一,对内存的使用量比较多,比较适合RAM相对较充足的情况;二,运行的时候随时升级,但存在风险:一旦在写FLASH的过程中掉电,或者刷入了不含串口升级功能的代码,之后就只能接仿真器刷了;三,编码简单,移植方便。然而,虽然目前我这个项目的代码量还不多,但如果以后要增加功能,势必会增加代码大小和内存使用量,那时候使用这方案就不合适了,因而我必须思考其他实现方案。
既然内存不充裕,那我就减少升级代码缓冲区的大小,例如设置一个固定的FLASH ROM页面大小,刚好是FLASH进行清除操作的最小单元;这样填满了这个缓冲区,就进行写FLASH操作,写好继续接收。现在我常用的串口波特率最高是115200bps,就是说每秒最多传14400字节,假如刷写一个页面的FLASH ROM需要x个毫秒,从串口驱动的接收缓冲区拷数据到升级代码缓冲区需要y毫秒,那么串口驱动的接收缓冲区必须大于14.4(x+y)个字节,才能避免丢失串口数。我没有查资料和做准确测试,这x+y估计在100以下。所以,相对于方案一,这个方案节省了大量内存,从原来的100多k减少到一个页面4k,而且还不需要限制代码量的大小,只要FLASH ROM装得下;不过,风险大大增加:方案一在串口传输的过程中如果掉电,只是升级失败,旧程序还在,而这方案如果在那时候掉电,新程序还没完全刷好,那又得接仿真器重刷了。一般FLASH ROM的重刷只需不到一秒,但100k的数据传输需要好几秒,为了避免这一风险的增加,我想到在FLASH ROM上做备份。例如现在FLASH ROM有2M字节这么多,假如我现在的代码翻十几倍,还可以多放1个相同的镜像。因此,我在接收时不刷写旧代码所在的页面,找空闲的页面先把新的代码刷进去,在接收完成并确认无误后,再把新的代码从备份页面拷贝到旧代码的页面。这样掉电的风险将和方案一相差不大。
参照方案二的改进版,要完全消除软件升级时掉电的风险,可以在FLASH ROM中腾出专门一个页面,放置串口升级相关代码;升级时只清空更新其他页面,而这个页面的代码永不擦除,驻留在FLASH ROM。而且,这部分代码每次启动后都要能执行。这也就是ATMEL的Flash Uploader的实现方法。具体的思路如下:当ARM启动之后,跳入串口升级相关代码,等待升级指令,如果接收到指令,便进入串口升级流程,否则等待一小段时间之后,跳入目标板执行的日常功能代码。例如,我在0x400000开始的FLASH ROM写入启动和串口升级代码,在0x500000开始的FLASH ROM写入日常功能代码,ARM的入口代码位于0x400000;当我需要串口升级时,启动目标板并不断通过串口发送预先定义好协议的同步指令,使目标板执行串口升级代码,从而更新位于0x500000开始的日常功能代码;当平时应用时,目标板启动后没有侦听到同步指令,在一小段等待时间后跳入位于0x500000,执行日常功能代码。相比于方案一和二,这个方案完全不担心掉电,代价是延长了日常功能代码的启动。在我慢慢理解离职大牛的代码后,发现原来他的代码就是这种方案的一种实现。启动代码就是bootloader,先执行串口升级的代码,再执行日常功能代码,而且在执行串口升级代码时,整个RAM除了中断表和串口升级代码要用到变量外,一般还有很大的余量,可以增大数据缓冲区,或者加入更多的机制来完善串口升级。受此启发,我还想到了更多复杂的应用方案。
例如,结合方案一和方案三,或者方案二和方案三,便既可以保留日常运行时升级的功能,又避免了偶然断电等因素导致设备不能再串口升级的风险。或者改进方案三,使目标板启动后先判断位于0x500000处的代码是否可执行,如果是则执行日常功能代码,否则跳入串口升级代码;在日常运行时也可以接收指令跳入串口升级代码,从而减少日常功能代码的启动时间,并且在日常运行中执行串口升级时可获得最大化的RAM资源,当然这里如何准确判断0x500000处的代码是否正常将是个重点。貌似现在有些ARM就驻留了这样的启动代码在FLASH ROM的首个扇区。
最后,总结一些经验体会:
一,这段时间为了实现这个功能,我翻查了不少资料,对ARM主要是AT91FR40162的启动过程有了进一步理解,对memory remap,boot loader等都有了更多的认识。相信这对我学习其他cpu编程来说是个好的基础。
二,通过一步一步对启动代码的调试,我克服了对ARM汇编的恐惧,虽然汇编指令没懂几个,而且看到一大段汇编代码还是会头疼,但至少那种神秘感消失了;通过调试汇编代码,某些问题的原因可以得到快速而准确的定位,从而采取了有效地解决方案。
三,经过多次串口升级的实践,我认为检测出传输错误、保证刷写的内容正确是关键的。目前我的方案是加入CRC校验,主要是日常功能代码里有用到CRC,这样可以减少加入额外代码,当然我得用相应上位机程序软件进行加工或配合。
四,串口升级代码的设计,最好要有周全的考虑,例如对FLASH ROM扇区的规划使用,串口升级指令的协议等等,不能只看目前应用的情况,还得考虑将来扩展的灵活性。虽说这部分代码本身也可以升级,但不变是最好的。“不变应万变”
五,不怕做不了,只怕想不到,我也算是重复造了个轮子,而且还打算增加更多的功能,不过仔细想想还是罢了,太复杂的东西恐怕会像离职大牛的代码一样,被丢着不用了。
六,简要列举三种主要方案的特点
|
方案一 传好再刷 |
方案二 边传边刷 |
方案二改进 边传边刷 + FLASH 备份 |
方案三 驻留并启动执行 |
方案三改进 驻留,启动条件执行 |
RAM 容量要求 |
高 |
中 |
中 |
低 |
低 |
ROM 容量要求 |
一般 |
一般 |
高 |
一般 |
一般 |
掉电风险 |
低 |
高 |
低 |
无 |
无 |
额外启动时间 |
无 |
无 |
无 |
有 |
无 |
代码实现 |
简单 |
一般 |
一般 |
复杂 |
复杂 |
代码移植性 |
容易 |
容易 |
容易 |
困难 |
困难 |
代码维护性 |
容易 |
一般 |
一般 |
困难 / 不维护 |
困难 / 不维护 |