十招编写更易维护的嵌入式代码

来源:岁月联盟 编辑:zhu 时间:2008-12-30
  应用程序开发的一个重要方面就是代码的维护,但也却是被更快的产品上市要求所忽略。这对于某一些程序来说,也许并不严重。这是因为这类产品的生命周期很短,或者这类产品一旦部署就再也不会动它了。

  然而,嵌入式软件的生命周期却往往长达数年,这就意味着前期的一些失误会导致后期的大量损失。

  嵌入式软件就意味着有较长的生命周期,在设计和实现的时候就一定要考虑维护的问题。下面的技巧虽然不能保证完整,但的确能够点出一些常见的问题。记住他们并不要成为那些拥有痛苦记忆的一员。

  技巧#1 避免汇编代码

  当然,在一些低端PIC单片机上你没得选择,而在一些高端ARM处理器上你也可能不需要,但在此两个极端之间,还有很多平台使用汇编来提高性能,减少代码。然而,简单的使用汇编来也可能把你的项目打回几个月前。

  汇编可以直接访问机器功能,但性能的提升会被真正理解程序在做什么这件事所替代。这也就是为什么高级语言,例如C和Java,被设计出来的原因。

  当调试的时候,每一段汇编代码都可疑,而高级语言的异常安全特性就显得容易的多了。如果必须使用汇编,尝试逐句注释。C和Java中,注释可能会把代码弄乱,但在汇编语言中,注释可就会节约很多时间并不会让人觉得挫败了。

  注释也可以针对程序块,但确保程序块不要超过5~6条语句。理想情况下,算法可以用伪码的形式写在注释中。(参见技巧#8)

  技巧#2 避免注释变更

  这是一条通用编程技巧,但在嵌入式编程中,保证代码和其注释关联尤其重要。当代码更新的时候,这中错误非常容易发生,并且导致代码很难理解。下面的例子就展示了随时间流逝,注释是多么容易被变更。

  (译者注:为了理解方便,代码中的注释不做翻译)

// This function adds two numbers and returns the result
#if __DEBUG
void printNumber(int num) {
printf("Output: %dn", num);
}
#endif
// This function multiplies two numbers and returns the result
int multiply(int a, int b) {
return a*b;
}
int add(int a, int b) {
#if __DEBUG
// Debugging output
printNumber(a+b);
#endif
return a+b;
}

  可以看到函数Add的注释和代码之间插入了printNumber函数。后来的人发现这个Add函数并挨着它加入了multiply函数,这样就使得Add函数和它的注释文档断开了。为了避免这个问题,将注释可以写在函数内部,或者用线条来前后隔开注释。

  技巧#3 不要过早的优化

  程序设计中的一条大罪就是过早优化。然而,这条戒律却常常被时间限制,邋遢的代码或者过于热心的程序员所破坏。任何你所编写的程序应当尽可能从简单着手,并严格按照设计功能实现。如果要求性能,那也应当以简单实现为优先。

  只有完成了完整代码块(可能是一个程序或者一个大系统的组件)的测试,再回头去做优化。无计划的,随意性的代码优化很容易造成维护的噩梦。优化后的代码往往难于理解,并可能无法达到优化的目的。最好是能够使用分析工具(例如gprof或者intel的Vtune)来查找瓶颈,针对瓶颈来优化可能会达到意外的效果。

  技巧#4 中断服务程序要尽可能的简单

  同时考虑到性能和维护,中断服务程序必须要尽可能的简单。与生俱来的异步特性使得中断服务程序的调试要比通常的程序难了许多。让其做的事情尽可能少对于维护程序来说就尤为重要。把ISR中的数据处理移到主程序中,这样ISR可以解放出来去仅仅抓取数据(例如从硬件设备)并把数据放到临时buffer中以备用。可以用一个标志来通知主程序数据待处理。

  技巧#5 把调式代码从源文件中移除

  在开发过程中,你可能会加入大量的调式代码,例如冗长的文本输出,断言,LED闪烁等等。当项目结束,就应当把这些代码清除掉,尤其是那些随意加入的调式代码。

  清扫代码是一个高尚的工作,但去掉调试信息也可能带来的问题。维护代码的兄弟将可能会加入产生这些代码,如果代码已经有了,就会使得维护变得容易。如果这些代码需要在产品构建的时候去掉,使用条件编译或者把调试代码放进中介模块或者库中,并保证不会链接到产品中去。最初的开发应当考虑到编写文档和清理调式代码的时间;这些额外的时间将会物有所值。

  技巧#6 给提供调用封装接口

  尽可能将底层I/O接口与上层应用逻辑通过接口来分离,写到一起会让程序维护变得异常困难。把程序的所有功能放到几个大函数中会让代码变得难于理解并更难调试。这对于硬件接口来说更加中药。你可能会直接访问硬件寄存器或者I/O,或者平台提供的API,但更建议封装自己的接口。

  通常你无法控制硬件行为,如果你还不得不修改平台,在程序中写有硬件相关的代码(API或者直接操作,无论哪种方式)将使得代码更难以移植。

  如果你封装了自己的接口,可能是一些宏来定义硬件API,你的代码就可以保值一致,所有需要移植的仅仅是那些集中起来的接口而已。

  技巧#7 根据需要分裂函数

  嵌入式软件与PC软件不同之处在于代码与使用的硬件有很大关系。将函数单元尽可能分割成为最小块并不建议-在一个范围内保证大概少于6~6个函数调用为宜。硬件功能单元与软件代码块要对应。

  过分分割程序将会将调用关系复杂,使得调式和理解变得困难。

  技巧#8 编写文档

  根据代码维护文档,最好也有对应的硬件副本。当为程序写文档的时候,尽量把设计和程序原型写到代码中。如果必须要分离开来,作为注释的源文件放到项目中。(译者注:这里没有将link译作链接是因为个人判断。注释不会被连接器链接到程序中的,因此就简单译作了“放”这个词)

 

  如果你拥有版本管理系统(例如CVS或者Microsoft Source Safe),把文档和代码放到同一目录下。如果没有放到一起,文档那个很容易被遗失(译者注:这一点个人不敢苟同,还是有很多方法来维护文档的,将代码和文档放到一起会让项目文件变得混乱)

  最好将所有文档和源码刻到CD(或者你愿意的可移动存储设备)上并放到安全位置。你的继任者将会非常感谢你的。

  技巧#9 不要投机取巧

  就像预先优化一样,取巧的代码可能会导致大麻烦。C和C++在嵌入式世界中居于统治地位,有大把的解决问题的方法。模板,继承,goto,三元运算符(?),这个列表可以很长。

  真正聪明的程序员会想出极端缜密和优雅的方法来使用这些工具来解决问题。但问题是通常只有这个程序员才理解这些方法(并也很可能忘记它是如何工作的)

  唯一解决方法就是避免取巧和使用那与理解的语言特性。例如:不要依赖C语言的短路赋值语句(译者注:||或者&&运算)和三元运算符来控制程序(使用if语言).

  技巧#10 将所有定义放在一个地方

  这个简单;如果有大量的常量定义或者条件定义,把他们集中放置。可以是一个单独文件或者源代码路径。如果你把定义深深隐藏在代码中,它会让你吃苦头的。

  Timothy Stapko是Digi International 的首席程序员,专注于嵌入式产品中的Rabbit。Stapko用于8年以上开发经验,是《实用嵌入式系统安全》的作者。