现在的位置: 首页 > 综合 > 正文

实时嵌入式软件开发的25个常见错误(一)

2014年11月21日 ⁄ 综合 ⁄ 共 4615字 ⁄ 字号 评论关闭

英文原文由David B. Stewart撰写,  这篇论文对实时嵌入式软件开发的易犯错误做了深入分析,对我们的开发非常有指导意义。

 

David B. Stewart

Software Engineering for Real-Time Systems Laboratory

Department of Electrical and Computer Engineering and Institute for Advanced Computer Studies

University of Maryland, College Park, MD
20742

Email: dstewart@eng.umd.edu

Web:
http://www.ece.umd.edu/serts

 

 

摘要

这里将列出嵌入式实时软件开发中最常见的错误和缺陷,并着重阐述这些错误的起因和潜在的危险性。同时也将讨论解决的方法,包括更好的教育以及使用新技术和最新的研究成果。而这些常见错误包括从高层次的项目管理方法中的问题到低层次的设计和实现中的技术问题。作者总结了很多嵌入式程序员在软件设计和实现中的经验教训,包括从公司里经验丰富的专家到在学校刚刚开始学习的新手,确定了这些最常见的错误。

 

 

介绍

不管是在大学还是在公司里,新手和专家们一样,在开发实时软件的过程中都在不断重复着同样错误。在为学院里的项目代码进行总结和评价,以及作为一个顾问为公司的很多设计和代码进行评论的过程中我得出了这个结论。

大多数实时软件开发人员都没有意识到他们最喜欢的方法存在问题。通常专家们都是自学成才,因此他们会有和刚开始时同样的坏习惯,因为他们就从来没有见到过设计他们的嵌入式系统的更好方法。这些专家们然后教新手,新手们也就继承了同样的坏习惯。这篇文章将有助于大家清楚这些常见错误并开始避免这些错误,以设计出更可靠,更好维护的软件。

这篇文章原来描述了十个常见错误,但是由于常见错误越来越多,以至于后来想保持在二十五个以内都难。尽管文章题目是二十五个,这里实际上列出了三十个常见错误。

对于每个错误,这里都说明了错误的根源或是概念上的错误。同时也给出了降低或者避免这些错误的产生的可能的解决方法或是选择。如果读者不太熟悉这些替代解决方案的细节或者是术语,应该到图书馆或者是网络上查看相关的参考资料。大多数错误大家的看法都一样,但这里列出的某些错误以及建议的解决方法却可能存在争议。在这种情况下,简单的说明最好解决方法上的分歧将有利于鼓励设计人员拿自己的方法和其他各种方法比较,重新思考他们自己的方法是否更好

在一个项目中纠正这里列出的某一个错误,就可能节省人力几周甚至是几个月的时间(特别是在软件生命周期的维护阶段),或者是显著提高项目的质量和健壮性。如果多个错误相同并得到解决,就有可能为公司节省或者增加数千甚至数百万美元。因此,提倡大家都针对这里列出的每一个问题,问问自己目前的方法和准则,比较这里给出的解决方法和建议,然后作出选择。改变你目前的某些方法将可能不需要增加任何附加成本而为你的项目或者公司提高效益,提高软件的质量和健壮性。

这里列出的三十个最常见错误,越后列出的错误(#30在最前而#1在最后)对软件质量,开发时间和软件可维护性的影响越大。当然这个顺序也是我的观点而已。并不就是说前面列出的问题就没有后面列出的重要。重要的这些问题都列出来了,而这些都可能在你特定的环境中显的很重要。

 

 

#30 “我的问题与众不同”

很多设计者或者程序员拒绝听其他人员的经验,宣称他们的应用不同,并且更复杂。设计者对于这些相似的工作应该有更加开放的思维。如果从实时系统原理的具体细节上来看,即使是那些看起来大相径庭的应用也有可能是差不多完全相同的。例如,通讯工程师宣称他们的应用与控制工程师没有任何相似性,因为大容量的数据和需要特别的处理器如DSP。相应地,问“蜂窝电话的LCD显示软件和一个温度控制器的软件有什么不同?真的不同吗?”一一对比控制和通信系统,他们的特征都是具有输入和输出模块,以及相应的函数。一个DSP处理256x256图象的算法与一个320x200的点阵LCD的显示原理可能就没有什么很大的区别了。甚至,两者都使用了与应用程序大小相对应的内存和处理能力有限的硬件;两者都要求不仅在目标板上开发软件同时也在一个平台上进行,并且很多开发DSP软件的方法同样也适用于微控制器软件的开发。

虽然它们的时序和数据的容量是不同的,但是如果系统设计正确,那么这些参数仅仅是变量。分析内存和处理时间等资源的方法也是一样的,两者都需要相近的实时调度,并且两者都需要高速优先权倒置的中断处理器。

或许你会说控制系统和通讯系统是相似的,两个不同的控制系统或者通讯系统其实也同样是如此。每一个应用是唯一的,但是抛开定义、设计和实现的过程,程序其实是相同的。嵌入式软件的设计应该尽可能多地学习其他人地经验。不要由于应用领域地不同,就觉得别人的经验无所谓。

 

 

 

 

#29 工具的选择是由市场宣传的驱动,而不是技术需求的评估

嵌入式系统的软件工具的选择经常基于市场的亮点,因为很多人使用它,或者是因为那些看起来很诱人而实际上却没有任何不同的宣传。亮点:仅仅因为具有更漂亮的用户图形界面并不会让一个工具比别的工具更好。重要的是要根据实际开发应用的需要来考虑两者的技术能力。

用户的数量:从某个供应商那里买软件仅仅因为它是最大的供应商,并不意味着它是最好的供应商。 很多人使用一种软件的背后可能隐藏着这样的事实:很多人花了超过实际需要的冤枉钱,或者是很多人都拥有一些在发现并不合适后就将它们束之高阁的工具。

兼容性的承诺:经理人员是很容易受到一种产品兼容性许诺影响的。就算这种软件与POSIX百分之百兼容又怎么样?它合适吗?你有改变操作系统的计划吗?就算你要改用一个与POSIX百分之百兼容的操作系统,你又能得到什么呢?除了“扩展性”外,你什么都没有得到。但是假如有扩展的话,那么兼容性就丢失了,因此好处就不再有了。想想所谓的标准,如POSIX也没有证明对实时系统有好处,还是保持原来的最好。因此,不要因为许诺就假设产品的性能会更好。只要所有的设计者采用已被证明了的好的模块化的软件设计策略,那么轻便性和重用性就很容易获得。【4,5】

当选择工具的时候,应该首先考虑实际应用的需求,然后从技术的角度来比较这些成打(或是上百)的备选方案,因为他们是和应用的需求特别相关的。对一个特定的设计来说,最合适的工具并不一定是最流行的工具。

 

 

 

#28 特别大if-then-else和case 的描述

在嵌入式系统的代码中有很大的if-then-elsecase的结构是不正常的,这会引起三个方面的问题。

1、由于代码有很多不同的执行路径,所以它们将很难测试。假如是嵌套的话,就会变得更加的复杂。

2、最好和最坏情况下的代码执行时间会明显的不同。这将导致CPU效率低下,或者是当执行最长路径时可能出现时序错误。

3、结构代码覆盖测试的困难将会随着分支的数量成指数增长,所以结构分支应该最少。

相反,用数学计算方法可以得到相同的结果。用布尔代数做一个跳转表来实现一个有限状态机,或用查找表来实现的话,可以把100行有if-else结构的代码减少为 不到10行的代码。

这里有一个将if变成布尔代数的小例子:

if (x == 1)

     x=0;

else

     x=1;

用一个布尔代数的计算来实现:

x = !x; // x = NOT x; can also use x = 1-x.

虽然很简单,很多程序员却仍然在用上面的if结构来替代布尔运算。

 

 

#27 用空循环实现时延

实时软件经常需要增加时延来保证通过I/O口能有足够的时间来准备收发数据。这些时延通常都是通过增加一些空语句或空循环来实现(如果编译器有优化功能,要使用volatile保证变量不被优化)。如果这种代码在不同的处理器上使用,甚至对于一种处理器,只是运行在不同的主频(比如25MHZ或33MHZ)下,在快一些的处理器上这种代码很可能就会失效。这一点我们在设计时要特别注意,因为它将直接带来一些实时性问题,这种问题很难跟踪和解决,因为这类问题表现的症状是五花八门的。

其实,我们可以使用一个基于定时器的实现机制。一些实时操作系统(RTOS)提供了这样的功能,如果没有,我们也可以很容易造一个定时器。下面列举了两种通用的造时延函数delay(int usec):

大多数的递减定时器允许软件读取当前递减寄存器的值。我们可以使用一个系统变量来记录定时器的速率,单位为usec/tick。假如值为2 usec/tick,现在需要一个10usec的时延,那么时延函数的忙等待时间为5个tick。就算换了一个不同速率的处理器,定时器的tick数还是一样。若是定时器的频率更改了,则系统变量需要跟着修改,并且忙等待需要的tick也需要修改,但时延时间仍然保持不变。

如果定时器不支持读取瞬时计数值,另一种方法是在初始化时近似测出处理器的速度。

运行一段空循环,并且对两次定时中断之间的循环次数进行统计。从而得知定时中断的频率,每次循环的usec值也可以计算出来。该值可以用来动态衡量指定的时延到底需要多少个循环。在RTOS种使用这种方法,对于我们测试的任何处理器,时延函数的精度范围都在10%之内,而我们也不需要每种情况都修改代码。

 

 

 

 #26交互式的和不完整的测试程序

许多嵌入式设计人员创建了一系列的测试程序,每块测试程序是为了测试特定的一种特性。每个测试程序都必须单独运行,而且有时候需要用户输入一些内容(比如通过键盘或开关),然后观察输出响应。这种方法的问题就是编程人员仅仅想测试一下他们正在修改的部分。但是既然不相关代码之间需要共享资源,就必然有交互作用,每次修改后都必须将整个系统全面测试一次。

要完成这件事,就必须避免创建交互式的的测试程序。创建一个单独的测试程序,让他尽可能做到能自测,这样,任何时候即使有一点小改动,也能很容易而且迅速地完成一次完整的测试。不幸地是,说的总比做的容易,比如一些测试特别是I/O口的测试,只能交互式地完成。尽管如此,任何开发人员在编写测试用例前还是应该优先考虑到编写自动测试用例的原则,而不是写一步算一步,采用边写代码边测试的方法。

 

 

 #25  移植代码并非为移植而设计

不是专门为移植而设计的代码,形式上不会是一种抽象出来的数据类型或是对象。这种代码很可能和其他代码之间存在一定的交互性,因此如果采用所有的移植代码,那么就会存在很多我们不需要的代码在其中。如果只采用其中一部分,那么我们就必须象一个外科医生一样对代码进行解剖,如果我们对这些代码没有足够的认识,我们很可能在剔除这些不需要的部分时存在一定的风险,或是无意中影响到了其功能。如果代码不是为移植而设计,最好先分析一下现有程序的功能,然后重新设计和组合代码,将它改造成结构良好,可移植性好的软件模块【4】。这样代码就可以移植了。重新编写这个模块代码的时间将比直接修改和调试原始的移植代码的时间短得多。

通常,有种误解认为,既然软件已经分割成了各个独立模块,那么它自然是可移植的。这本身就是一个分离性的错误,因为生成的软件存在很多相互依赖性。详情请看错误#18“模块间的耦合性太强 ”。

 

 

 续篇:http://blog.csdn.net/myaccella/article/details/7007081

 

 

抱歉!评论已关闭.