电力设备用MCU嵌入式程序分块更新技术

2016-06-21 07:02江苏林洋能源股份有限公司陈昊琦沈煜迪黄柳胜
电子世界 2016年11期

江苏林洋能源股份有限公司 陈昊琦 沈煜迪 黄柳胜

电力设备用MCU嵌入式程序分块更新技术

江苏林洋能源股份有限公司 陈昊琦 沈煜迪 黄柳胜

【摘要】文章介绍电力设备MCU的嵌入式程序按功能或按函数级别对增加、修改、删除代码进行精确分块更新的工作原理,指出采用分散加载机制方法,可对MCU划分代码区域,叙述使用伪指令、函数壳、void ★、void ★★、void ★★★方法保证因修改程序代码或变量,需要更新的程序范围不会扩大的设计方法。该技术已经在实际项目中使用,可以根据产品功能需求的变化精确、快速更新程序。

【关键词】嵌入式程序分块更新;分散加载机制;函数壳;空类型指针

一、引言

随着智能电网建设的不断推进,在电力设备中MCU的应用越来越广泛,在很多情况下,如现场使用的设备需要增加新功能、现场应用运行后发现了BUG等等,都要求更新MCU内部的嵌入式程序;同时由于电力设备往往工作在无人值守的环境下,且数量众多,一旦需要对数以万计的终端进行现场更新,就会耗费大量现场服务的人力、物力和时间。

随着通信技术的发展,电力设备使用GPRS/CDMA进行远程通信成为现实;同时IAP在线应用编程技术的出现使得MCU不必增加专用编程接口,就可实现本地或远程下载来实现程序的更新。

一些厂家采用了IAP + GPRS/CDMA通信方式实现远程程序更新。该方案虽然解决了现场升级的繁重工作量。但由于目前广泛使用的程序更新方式都是将MCU的嵌入式程序全部更新,即如果仅仅修改了一个函数的代码或增加、删除一个功能甚至只修改一行代码都需要更新整个程序。

同时由于GPRS/CDMA的传输速度低、并根据流量收费,当程序代码量大、需要频繁升级时,仍会导致升级的时间长、流量大、收费高等问题的产生。

本文叙述的嵌入式程序分块更新技术,可以按功能或按函数级别对修改的代码进行精确的分块更新,从根本上避免在任何情况下都必须更新全部程序的低效率工作方式,同时也有效解决通过GPRS/CDMA远程更新设备程序所造成的升级时间长、流量大、收费高的问题。

二、嵌入式程序编译、运行原理

目前,国内使用的MCU大部分属于中低端设备,不具备运行Linux的条件,普遍采用前后台系统或嵌入式实时操作系统如uCos运行程序。同时,为了提高开发效率,很多嵌入式程序采用C、C++语言编写。

根据编译原理如图1所示,源代码形成可执行文件需要经过预处理、编译、汇编、链接四个阶段。最终生成的目标程序包含了代码段(Code)、只读数据段(RO-data)、已初始化读写数据段(RW-data)、未初始化数据段(BSS)、堆栈段等信息。

图1 生成目标程序流程

图2 目标程序运行时布局

1、代码段(Code)由程序中可执行的机器代码组成。程序语句进行编译后,形成机器代码。在执行程序的过程中,CPU的程序计数器指向代码段的每一条机器代码,并由处理器依次运行。

2、只读数据段(RO-data)是程序运行时不会被更改的数据,如常量。使用这些数据类似查表式操作,由于不需要更改,因此只需放置在只读存储器中。

3、已初始化读写数据段(RW-data)是在程序中声明且具有初值的变量,如已初始化的全局变量、静态变量。这些变量需要占用存储器的空间,在程序执行时它们需要位于可读写的内存区域内,并具有初值,以供程序运行时读写。

4、未初始化数据段(BSS)也是在程序中声明,但没有初始化的变量,如未初始化的全局变量和静态变量。这些变量在程序运行之前不需要占用存储器的空间。

5、堆内存只在程序运行时出现,一般由程序员分配和释放空间,如new、delete操作。

6、栈内存也只在运行时出现,程序临时创建的局部变量、函数内部使用的变量、函数的参数以及返回值将使用栈空间,栈空间由编译器自动分配。

Code、RO-data、RW-data、BSS属于静态存储区域;堆和栈属于动态区域。Code、RO-data和RW-data在链接之后生成,BSS在程序初始化时候开辟,而堆和栈在程序运行中分配和释放。

目标程序运行时分为映像文件和运行两种状态,具体布局如图2所示。通过工具将映像文件固化在MCU的ROM或Nor-Flash中,映像文件中包含了Code、RO-data以及RW-data;

程序运行的初始化阶段,各段必须载入到内存RAM的恰当位置,其中RW-data复制到内存的Data区域中,各个BSS区域将在内存中开辟空间并清零,堆和栈空间一般是向前扩展,栈由编译器管理,程序运行后堆大小分配由程序决定(见图2)。

三、分块更新要点和设计思路

通过上文对嵌入式程序的编译和运行原理分析介绍,需要关注以下2点:

1、程序编译后生成的目标代码顺序存放,紧密地靠在一起。若增删程序后再编译,生成的目标代码将和原来大相径庭。

2、目标程序使用的存储空间见表1:

表1 目标程序使用存储空间汇总

程序编译后,由于RW-data段会保存在目标代码中,并和其他段紧靠一起。若增删RW-data中的变量,也会导致再编译生成的目标代码和原来区别非常大。

RO-data段存放在ROM中,由于程序运行时不会被修改,所以也可以看成Code段。

分块更新的目标是“嵌入式程序可以按功能或按函数级别对增加、修改、删除代码进行精确分块更新,减少更新的代码量”,即只更新增加、修改、删除的代码,而且是按照功能模块或函数级别更新。但本节提到的2个关注点已经指出,若直接修改代码或RW-data段中的变量,会导致编译生成的目标代码改动非常大,给人一种“牵一发而动全身”的感觉,无法满足分块更新的要求。

为实现分块更新的目标,需要满足以下3点:

1、按功能模块或函数级别对代码划分区域

源程序编译后,目标代码顺序紧靠存放。修改源程序后必然导致目标代码的大小变化,其后的代码位置也随之改动。按功能模块或函数级别对目标代码划分开独立的区域,区域的位置按更新的频率设定,更新频率低的区域放置在ROM低地址处,更新频率高的区域放置在ROM高地址处;区域的大小根据实际情况预留一部分备用空间,在实际使用过程中若区域不够时,可在ROM中划出一块还未使用的区域继续。

源程序中函数一定是很多的,按函数级别划分区域带来的工作量相当大。在实际使用时,建议采用按功能模块级别划分区域。

使用伪指令划分区域,根据编译原理应在预处理阶段实施。本文将在下一节介绍使用分散加载机制方法划分代码。

2、代码精确更新,减少更新程序的范围

如图3所示,功能模块之间、函数之间、模块和函数之间需要相互调用。若仅仅对代码划分区域,一个区域内的代码修改编译后,与之相互调用函数的区域会改变,随之又影响到其他有关联的区域。

图3 区域关联

图4 区域之间调用函数流程

这种情况导致更新一个区域时,与之有关联的区域,甚至与关联区域有关联的区域都需要更新。

使用伪指令和函数壳既切断区域之间连带更新,又保证关联区域正常工作。本文将在下一节阐述处理的方法。

3、RAM中变量改变时减少更新程序的范围

由于模块、函数之间相互调用,若函数的参数或模块内的变量需要改变时,也会导致更新一个区域后,与之有关联的其它区域都需要更新。

为了减少更新程序的范围,使用1级、2级甚至3级空类型指针变量,使变量修改后再编译减少对目标代码的影响。

四、实施分块更新的方法

上一节讨论了需要满足3要点以实现分块更新的目标,这节详细阐释每个要点实施的方法。

(一)分散加载机制划分代码区域

在第2节中已经介绍了映像文件及其组成,要构建映像文件的存储器映射,链接器必须有:描述如何分组成区的分组信息、描述映像区在存储器映射中放置地址的放置信息。

分散加载机制允许为链接器指定映像文件的存储器映射信息,可实现对映像组件分组和布局的全面控制,如固定函数的位置,即使周围程序已经被修改并重新编译。分散加载通常用于具有复杂存储器映射的映像(尽管也可用于简单映像),适合在加载和执行时映射中多个区域是分散的情况。

源程序在编译前,利用开发平台如Keil μVision自带的分散加载机制功能,按程序设计的功能模块划分多个区域,并设置区域位置及大小。下面例子是相关代码及注释。

使用分散加载机制将所有程序文件按功能级别划分ROM的地址空间。区域划分后,功能模块内部的函数相互调用、修改,不影响其他区域。

(二)伪指令和函数壳减少更新程序的范围

功能模块划分完区域,区域之间会相互调用函数,为减少程序更新的范围,需要在不影响正常运行的前提下切断区域之间的连带更新。对需要相互调用的函数建立相应函数壳,同时使用伪指令固定函数壳在区域内的位置。下面例子是相关代码及注释。

2个区域之间调用函数的流程如图4所示。

函数壳起到“中间人”的角色,区域之间调用函数时都需要先通过函数壳。同时固定了函数壳的位置后,对应的函数修改代码只会改变该函数所在的区域,不会影响到调用它的其他区域,从而切断区域之间的连带更新,达到了减少更新程序范围的目的。

(三)使用空类型指针减少更新程序的范围

4.2节的例子中函数没有使用参数,实际情况下函数经常需要传递各种参数。同时功能模块中还有各种变量需要使用或被其他模块调用。本节对这2种情况的处理做详细说明。

函数传递各种参数、或更新代码后传递的参数改变了,如果直接使用函数壳加xxx参数,还是会造成关联区域更新范围扩大。使用1级(或2级)空类型指针传递实际参数,即使实际参数改变也不会扩大关联区域的更新范围。下面例子是相关代码及注释。

前文中对目标程序使用的存储空间做了汇总,详见表1。功能模块中已初始化的全局变量、静态变量(属于RW-data段)需要占用ROM存储空间,而且其他模块也能调用这些变量。若修改变量也会造成关联区域更新范围扩大。使用2级(或3级)空类型指针代替变量,保证了即使实际变量改变也不会扩大关联区域的更新范围。下面例子是相关代码及注释。

void** pVariate;// 2级空指针变量

pVariate = new void *[RowCount]; // 动态申请变量数组

// 指针变量关联实际变量

使用n级空类型指针可以避免编译器为RW-data段分配ROM存储空间。

五、结语

采用本文介绍的分块更新技术,将电力设备用MCU的嵌入式程序按功能模块或函数级别进行精确的分块,产品投运后可以根据功能需求变化,只对更改了功能的模块进行更新,从而避免了在任何情况下都必须更新全部程序的低效率工作方式,同时也有效解决了通过GPRS/CDMA远程更新设备程序所造成的升级时间长、流量大、收费高的问题。

分块更新技术已经在公司多个项目中实际应用,效果极佳。我们在II型集中器项目中以修改LED驱动为例做过统计,若没有使用分块更新技术编译、链接后生成的Bin文件达到330k左右,通过GPRS网络远程更新时每2秒传输1024字节,需要11~15分钟传输完毕。使用分块更新技术后生成的LED驱动Bin文件只有3k左右,通过GPRS网络仅需6~10秒传输完毕。分块更新技术降低了产品投运后的维护成本,增强了产品的市场竞争力。

当然,分块更新技术在实施过程中,需要注意到动态申请空类型指针变量会多占用一些内存,在内存不足的情况下需要及时释放;而且,在编写调试程序的时候也会有一定的难度。

参考文献

[1]陈火旺.程序设计语言∶编译原理(第3版)[M].国防工业出版社, 2014.

[2] Kenneth A.Reek. C和指针[M].人民邮电出版社,2008.

[3] Keil uVision4中文教程,2009.