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

时间:2022-11-28 16:37:04
 

英文原文由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“模块间的耦合性太强 ”。

#24  基于单一架构的归纳

嵌入式软件的设计者可能需要开发能运用在不同的处理器上的软件。在此情况下,编程人员通常会先在其中的一种开发平台开始编软件,但是会在晚些时候为包装代码而做大量的准备工作。

不幸的是,这样做通常弊大于利。这种设计试图过份的归纳出不同架构下的相同点,而不是不同点,但是设计者并不能预见到这些不同点。

一种比较好的设计策略是在多个架构下同步设计和开发代码,归纳出那些不同架构下的差别,有意地挑选3到4种差别较大的处理器(比如不同开发厂商的产品或是采用不同架构设计的产品)

 

 

 #23 一个大循环

当实时操作系统的代码被设计成一个单独的大的循环体,我们就无法独立地修改不同部分代码的执行时间了。很少有实时系统需要所有的模块都以同样的速率运行。如果CPU超负荷,其中一项可以利用来降低CPU占用率的方法是减慢部分非关键代码的运行速度。然而这种方法只对RTOS的多任务操作系统奏效,否则代码就是设计成基于灵活风格或商业实时运行环境中。

 

 

#22  超负荷设计系统

如果处理器和存储器的平均利用率小于90%,而峰值利用率小于100%,那么我们就说这个系统属于超负荷设计。对于设计者而言,写程序使用过多的资源实在是一种奢侈的行为。在某种情况下,这种奢侈能直接导致盈利和破产的区别!软件工程师有责任尽量减少一个嵌入式系统的价格和能源消耗。如果CPU的利用率只有45%,那么就可以使用运行速度只有一半的处理器,因而减少了4倍的能量,而且可能每个处理器还能省下1美元或者更多的钱。

如果这个产品大批量生产,每个处理器省下1美元,仅这一项整个产品就能省下100万美元。如果该产品是电池驱动的,电池就可以延长寿命,从而提升该产品的市场上的需求量。作为一种计算机家族中电源消耗的一种极端例子——便携机,一般使用一种很沉的电池,最多能使用3个小时。而一块手表,重量轻,电池便宜,却能使用3年!尽管软件通常和能量消耗没有直接的关系,但它确实扮演了一个重要的角色。

使用快速的处理器和更多的内存确实导致设计者懒于考虑这方面的设计。建议开始设计时先考虑低速的处理器和少一些的内存,而只有在实际需要的情况下再升级处理器。如果我们的软件能利用更高效的硬件就更好一些,我们就不会是采用一个高速的处理器,然后却要尽量删除一些周边部件来降低系统成本。

 

 

#21  在开始软件设计之前没有分析硬件特性

两个8-bit数相加需要多少时间?16-bit?32-bit?两个浮点数相加呢?还有一个8-bit数加上一个浮点数呢?如果软件设计者不能在头脑中立即反应,针对他使用的处理器回答出上述问题,说明他还没有为设计和编制实时软件做好充分的准备。

下面是一个6MHZ的Z180处理器的例子,针对以上问题的答案是(us为单位):7,12,28,137,308!值得注意的是:浮点数加一个字节的时间要比浮点数加一个浮点数的时间多出250%,主要原因是中间增加了很多从字节转换到浮点数的时间。这种不规则操作往往是导致处理器过载的源头。

另一个例子,一种专用来处理浮点数的加法器处理浮点加法/乘法的速度比33MHZ处理器68882快将近10倍,但是sin()和cos()函数的处理时间相同。主要原因是68882处理器有一个内置于硬件的三角函数处理器,而浮点数加法器只能通过软件处理。

当需要为一个实时系统编写软件时,首先要考虑的是键入计算机的 每一行代码相关的实时性问题。要理解使用的处理器的性能和限制,并且如果执行代码使用了大量的长指令,最好重新设计应用程序。比如说,对于Z180,最好全部使用浮点数运算,而不要采用部分变量使用浮点数,而另一部分为整数的设计,因为这样会带来大量的混合运算。

 

 

 #20 第一次设计时过度优化

与第21个问题相反的一个问题也是一种通用的错误。一些编程者预见到了这种不规则现象(有些是很实际的,有些则有些奇怪)有一种奇怪的不规则现象的例子就是乘法比加法要长得多。一些设计者会将3*x写成x+x+x.而在有些嵌入式处理器中,乘法比加法处理时间的两倍要少一些,因此x+x+x的处理时间比3*x要慢一些。

一个能预见所有的不规则现象的编程人员可能会为了优化代码,将第一个版本的代码编得可读性很差。那是因为他并不知道是否真正需要优化。一般的原则是,在实现过程中不要使用完全优化。

以后,如果已经证明优化后性能提高,再对代码实施优化。如果不需要优化,则还是保留可读性较好的代码。如果CPU过载,那么最好知道,代码中仍有很多地方可以采用直接优化的方式就能使性能得到明显提高。

 

#19 “只不过是小故障而已”

有些编程人员往往一次又一次地使用同一个工作区,因为系统有一处小BUG。编程人员的典型的反应是,只要是使用过的工作区就能一直运行正常。殊不知,影响一个工作区的错误可能在不同时候表现出不同的形式。不管任何时候,只要存在“小故障”,它就说明系统中确实存在一些问题。最好的方法是按照适当的方法一步一步将问题的本质理解清楚。使用一个工作区可能可以保证按时交差,但是如果多花一点时间,查出问题,保证问题在任何时候都不重现,如下一次重要演示会,可能意义更大,虽然可能会比期限时间晚一些完成。

 

 

#18 模块间耦合性太强

好的软件设计中,模块之间的耦合关系可以被描述成一个树状结构,就象图1(a)中所示。从属关系图是由节点和箭头组成,每一个节点代表一个模块(例如:一个源代码文件),而每个箭头标明这个模块所依赖的其他模块。位于最底层一行的模块不依赖于任何其他的软件模块。为使软件重用性最大化,箭头方向应当是向下,而不是向上或双向的。例如:图中abc模块如果在其代码中有:#include "def.h" 或在abc.c文件中有用extern声明的def.c文件中所定义的变量或者是函数,那么它就依赖于def模块。

模块的耦合关系图是一项非常有价值的软件工程工具。通过这张图,可以轻易地做以下工作:

(1)识别软件的哪些部分可以被重用;

(2)为模块的测试工作建立一种策略;

(3)为限制错误在整个系统中的传播提供一种方法。

每一个环状的依赖关系(例如:在图中的环状连接)都会降低软件模块的重用能力。测试工作只能针对相互关联的模块集,问题将很难被孤立到一个独立的模块中。如果在从属关系图中有太多这样的环状连接,或者在图中最底层模块与最高层模块之间存在这样的环状连接,那么整个软件将没有一个独立的模块是可以重用的。

图1(b)和(c)都包含这种环状的依赖关系。如果这样的循环依赖关系是不可避免的,那么宁愿选择(b)而不是选择(c),因为在(b)中仍有部分模块是可以重用的。图1(b)中的限制是模块pqr和xyz只有接合在一起才能被重用。而在图1(c)中,由于在模块之间耦合性太强,没有一个子模块是可以重用的。还有,由于有一个大的环状依赖关系,即使是模块xyz,本来它位于关系图中的最底层,应该不依赖于任何其他模块,现在却要依赖于模块abc。仅仅是因为这样一个大环,就导致整个应用程序的所有模块都不可以重用。不幸的是,目前绝大多数应用程序都更类似于(c),而不是(a)和(b),因此也导致我们现存应用程序中软件模块重用的困难。

  要更好的使用依赖关系图来分析软件的可重用性和可维护性。在编写代码的时候就注意使代码更容易生成关系图。这意味着,在模块xxx中,函数中用extern声明的所有外部变量都要在文件xxx.h中定义。在模块yyy中,只要简单的察看一下有哪些头文件用#include包含了,就可以明确所依赖的模块。如果不遵守上面的规则,yyy.c中存在extern声明而不是用#include包含相应的文件,那么将导致依赖关系图的错误,同时导致试图重用那些看起来似乎是独立于其他模块的代码将非常困难。

 

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


 

 

#17 “最后期限临近”!没有时间休息

很多程序员会为一个问题,连续工作几个小时而废寝忘食。他们连续工作的主要原因是因为有“最后期限”,这样的最后期限可能是公司里产品开发的截止时间,或学校布置的家庭作业完成时间。但事实上,如果你在连续工作了一个小时而没有任何进展的时候稍做休息,往往可以“节省”很多的时间。你可以沿着湖边转转、来杯啤酒、打个盹什么的...

清醒的大脑来部分源于精神的放松,它使你更容易分析发生了什么,更快的找出问题解决的方案。即使是在“最后期限临近”的时候,也不要忘了两个小时的休息,可能节省一整天的时间;离开电脑10分钟的短暂休息,往往会节省一个小时的时间。

#16 使用消息传送作为主要的进程间通信方式

当软件按照功能模块划分进行开发的时候,首先想到的是以消息作为输入、输出。尽管这种方式在非实时环境(例如:分布式网络)应用的很好,但在实时系统应用中,却存在一些问题。

在实时系统中,使用消息传输会引发三种主要的问题:

1.消息传送需要同步,这是实时调度不可预知的主要原因。如果功能模块同步终止执行,将导致系统的时序分析变得困难,即便不是不可能。

2.在存在进程间双向通信或反馈回路的系统中,都有死锁的可能性。相反,应该使用基于状态的系统,比如:要说明“打开制动装置”的操作,状态的更改被描述为“制动装置应当打开”。

3.与使用共享内存的方法相比,存在着开销更大的问题。通过网络和串行线路通信可能需要通过消息,而如果能够随机访问数据的话,比如单处理器上的进程间通信,消息传送通常是效率低下。

在嵌入式系统中,更推荐使用基于状态的通信方式以确保更高的可靠性。一个基于状态的系统使用结构化的共享内存,可以使通信的开销降低。当进程需要状态信息的时候,总是可以得到最近的状态信息。Streenstrup和Arbib发展了一种port-automaton(端口自动仪)的理论来正式地证明一个稳定、可靠的控制系统可以仅仅通过读取最新的数据来建立[6]。通过创建共享数据的本地拷贝,确保每个进程可以互斥的访问自己所需要信息 就消除了代价昂贵的阻塞。如果系统有可能出现消息丢失,或者如果不是所有模块按照相同的速度运行以及如果想应用共享内存以降低操作系统的开销,使用状态而不是消息都可以为系统提供更好的稳定性。在[4]中,给出了一个高效的基于状态通信协议的例子。

将一个基于消息通信的控制系统转换成一个基于状态通信的控制系统通常是容易的。例如:为实现列车管理的最大化,一个智能铁路控制系统可以独立的控制每一个车上的制动装置。当需要停车时,为让列车在最短的距离内停止,就需要所有车上的制动装置一起启动。由于每一个制动装置的输入/输出逻辑是受控于一个独立的进程,而控制模块必须通知每一个制动模块去打开制动开关。当使用基于消息的系统时,控制系统发送一条消息,“打开制动开关”给每一个制动进程。这种方式会导致:很大的通信开销;如果任务运行的频率不同,还有丢消息的潜在危险;不确定的阻塞;每个进程一份独立的消息拷贝;以及存在死锁的可能性。由于进程之间的相互依赖性,消息通信方式建立起的实时系统是难于分析,而且不适合需要重新配置的系统。相反,在基于状态的通信机制中,每一个制动模块周期性的执行,检测制动变量brake来及时更新自己的制动I/O端口的状态。因为进程是周期性的,因此操作时序分析相对容易。每个进程也只需要状态变量表中的一个单一元素相关,这样消除了进程之间的直接依赖关系。相对与消息传送系统,共享内存的通信方式也减少了系统的开销。

当在对象之间传送数据流的时候,需要在共享内存中,建造一个“制造者/消费者”型的缓冲区,这样在每个周期循环中可以处理最大数量的数据。

 

 

#15 没有人可以帮助我

几乎所有的教师都有这样的共识:通过讲授的方式,可以是你对一个问题理解的更深刻。

在遇到问题的时候,实时系统的程序员经常感到无助,如:操作的I/O设备并不象文档中描述的的那样工作。在多数情况下,团队中的其他人都没有他本人在这个领域了解的知识丰富,导致只有一个人独自面对眼前的问题。由于没有找到合理的处理方法,这种错误的观点往往会导致整个产品开发进度和开发质量的下降。当面对这样的情况时,如果没有人比自己更有经验,那么就将相关的材料教授给哪些缺少相关经验的人们,使他们更好的理解问题。

如果没有对知识理解更深刻的人来帮助,那么就去向那些理解相对较浅的人寻求帮助。特别是很多团队中有一些非常愿意学习新东西以获取经验的新人,向这些渴求知识的人去讲解程序是如何工作的,以及目前存在的问题。他们很可能不能完全理解问题,但他们所提出的问题也许会使你产生一些想法或暴露出一些被忽视的问题,并最终使你产生解决问题的方法。

这种处理问题的方法还有一个非常好的副作用。他对新员工起到了培训的作用,当公司需对这方面高级编程技能人员有需求时,就不是单单一个人可以胜任了。

 

 

#14 只有一张设计图表

大多数软件系统的设计,整个系统通过一张设计图表来描述(更有甚者,干脆没有)。然而,一个象桌、椅这样的物理实体,尽管他们没有软件项目那么复杂,但仍可以有很多种图表来描述它,如:顶视图、侧视图、底视图、细节图、功能图...

当设计软件的时候,将整个设计在图纸上展现出来是最基本的。普遍被接受的方式是通过软件设计图的建立来实现。整个设计有很多不同的设计图,每张图的设计都从一个不同的角度展现了系统。

此外,还存在在好设计图和差设计图的区别。一份好的设计图在图纸上清晰地反映了设计者的思想;而差的设计图则是混乱的,模棱两可的,在图中遗留了很多未解的问题的。为建立一个优秀的软件,优秀的软件设计图是基础。

通过好的设计图来实现设计的常用技巧有以下一些:

1)一张大型项目自顶向下分解的结构设计图。它可以是描述对象、模块之间关系的数据流图,也可以是基于子系统之间数据交换的子系统图。

2)在结构设计图中的每一个成员,都需要通过一张详细设计图来描述。这张设计图要充分详细,使编程员可以毫无疑问地执行设计中的细节。应当说明的是,在一个多层分解的结构设计图中,在某一层面的详细设计可能成为更低一级设计的结构设计图。因此,相同的图表技巧可以应用在各种类型的图表中。

软件设计人员必须明确区分出进行的是面向过程还是面向数据的设计。

1)面向过程的设计,如:很多控制和通信系统中的设计,应当有数据流图(例如:控制系统描述)、处理流图(也被称为流程图),和有限状态机描述。

2)面向数据的设计,通常在一些基本应用和数据库中使用,应当包含关系图、数据结构图、层次结构,以及表格。

3)面向对象的设计是一种将面向处理和面向数据相结合的设计,应当包含表现不同视角的图表。

 

 

#13 在设计图中没有图例

即便是有了设计图表,很多情况下也是没有图例的。这使得图表中的数据流和处理流模块被混淆,而且由于图中的矛盾和模棱两可使整个图表的设计失败。即使是在一些软件工程教科书中的图表也存在有这样问题!!!

评判一张设计图是否存在缺陷的最快捷的方法是看它的图例,确认图中每个方块、每条线、每个点、每个箭头、粗细、填充色以及其他标志是否都与在图例中规定的功能匹配。这条简单的准则就象一个语法检测器一样,使开发人员和检视人员能够快速的的找到设计中的问题。此外,它强迫每个不同类型的块、线和箭头被画成不同的样式,使得不同类型的对象在视觉上容易区分。

事实上,是否采用象UML这样的标准或采用公司开发出的一套规范去画图并不重要。重要的是在每张设计图中,都有图例,而且在同一类型的图表中,使用相同的图例。一致性是关键。

下面是创建一致的数据流图、进程流图和数据结构图的一些方法。对于应用所需的其他类型的图表,也可以建立起类似的方法。

 

数据流图

这类图表,根据模块之间通信的数据来描述模块之间的关系和依赖性。它通常用于模块分解阶段,是在结构设计一层中最常使用的图表。不幸的是,大多数的数据流图设计的很糟糕,而主要的原因往往就是在于图中的混乱和矛盾。

要做出好的数据流图,要按照下面的方法去做:建立一种规范,并严格的遵守它,做一些图例来解释这个规范。要尽量减少进程或模块之间的连线(数据流)数目。要意识到在流图中,每个方块将成为一个模块或进程,而每一条连线将成为模块之间的某种耦合或进程之间的通信。因此连线越少越好。

一些典型的数据流图规范包括下面几点:

1)矩形代表数据存储区,比如缓冲区、消息队列或共享内存。

2)圆角矩形代表有自己进程执行的模块;

3)直线代表从一个进程或模块中输出到输入另一个进程或模块的数据。

在[4]中,给出了了一些关于控制系统数据流图的案例。

 

进程流图

这类图表通常描述在模块或进程内部的细节。他们通常使用于详细设计阶段。

向数据流图的方法一样,建立一个规范,并严格遵守它,并做一些图例来解释这个规范。进程流图的典型规范有如下几点:

1)矩形代表处理过程或计算过程;

2)菱形代表判断;

3)圆形代表开始、结束或转换点;

4)直线代表执行代码的顺序;

5)椭圆形代表进程间通信;

6)平行四边形代表I/O;

7)条状物代表同步点。

 

数据结构图和类结构图

数据结构图和类结构图描述的是多个数据结构和对象之间的关系。这类图表应包含足够的细节,可以直接在模块的.h文件中创建结构(在C语言中)或类(在C++中)。

这类图表典型的规范如下:

1)单个矩形代表一个结构或类中的一个域;

2)一组相邻的矩形代表同一结构或类中的所有域;

3)非临近的矩形说明同他们属于不同的结构或类;

4)从一个矩形伸出的箭头代表一个指针;箭头的另一侧代表被指向的结构或对象;

5)实线代表类之间的关系。在图中,应当有描述不同关系类型的图例。不同的关系类型,应当采用一条不同线宽、颜色或类型的线来代替。

在图2中有一个数据结构图的例子。在[3]中有一个多种关系的类结构图的例子。

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


 

 

#12 使用POSIX风格的设备驱动

设备驱动是用来提供一个对硬件I/O设备的操作接口层,这样高层软件就可以通过统一的,与硬件无关的方式访问设备。不幸的是,很多商用的实时操作系统采用的UNIX/POSIX 风格的设备驱动并不能满足嵌入式系统设计的需要。

特别要说的是,目前系统中使用的接口,如 open(),read(),write(),ioctl(),以及 close()等都是为文件或者是其他面向流的设备而设计的。相反,大多数实时 I/O 都有连接到I/O端口的传感器和激励器。I/O端口包括并口,模数转换器,数模转换器,串口,或者是其他特殊用途的处理器,如处理照相机或麦克风数据的DSP。为了尽量在这些设备上适应POSIX设备驱动应用程序设计风格,程序员将不得不在应用层编码实现硬件特性。

考虑下面例子:控制一个机电设备的实时软件要打开两个螺线管,它们分别连接在一个8-bit 数字I/O输出板的bit-3和bit-7,但是不能影响端口其他六位的值。没有任何POSIX接口能让程序员实现这样的功能。

在实际中,常用三种方法将硬件和这个设备的接口映射起来。一种是修改 write() 函数的参数,如将原来表示写入字节数的第三个参数改为表示写哪个端口。修改标准的API定义的参数后,就没有了驱动程序以及调用它的代码的与硬件无关的特性,因为不能保证不同的I/O设备驱动程序都以同样的格式定义参数。如我们想要定义一个8端口的I/O单板的端口4的bit 3和bit 7,怎么办?对于这块单板就需要采用不同的参数定义。

第二种方法是使用 ioctl()。请求和值作为参数输入。但不幸的是,没有请求的标准,每个设备都可以*选择自己支持的一套请求。一个关于这个问题的例子就是设置串口波特率为9600bps。不同的设备驱动程序使用不同的位图请求结构来实现这个功能。从而,本来应该具有相同硬件操作接口的设备却不兼容了。这样,使用这些设备的应用程序就依赖于设备特性,在另一种配置环境下就没用了。而且,和read()write()操作不同,ioctl()主要是在初始化时使用,因为ioctl()通常首要的一点是决定有什么请求,并为该请求转换适当形式的参数。

第三种,一个常用的方法是使用 mmap()来映射设备的寄存器。这种方法允许程序员直接访问设备寄存器。虽然这种方法提供了最好的性能,它损害了使用设备驱动来为设备建立一个与硬件无关的操作接口的目的。用这种方法来写代码是不可移植的,通常难以维护,并且不能在其他配置环境下发挥作用。。

另外一个可选择的方法是把设备驱动程序为封装一个它自己的线程。设备数据通过共享存储器来转发(不是象错误#16 使用消息传送作为进程间的主要通信方式 中讨论的那样通过消息传送)。设备驱动程序就成为了一个可以根据设备存在与否或需要与否来执行的独立进程。如【4】中讨论的那样,这个方法已经被证明能够开发出可移植的设备驱动程序。

 

#11 错误检测和处理是在事后进行,并且是在尝试和错误中实现

错误检测和处理很少在软件设计阶段以一种很有意义的方式具体体现。。更多时候,软件设计主要关注正常操作,任何异常和处理都是程序员在出现错误后补上去的。程序员可能 (1)到处加入错误检测,很多没有必要的地方都加了以至于它的存在影响了性能和时序;(2)除非是测试时发现了问题后加入有限的代码,否则没有任何错误处理代码。上述两种情况都是没有对错误处理进行设计,它的维护将是一个恶梦。

相反,错误检测应该系统设计中作为另一种状态具体体现。因此,如果应用情况是一个有限状态机,异常情况可以看作是一个引起动作和状态迁移的新状态输入。具体实现的最佳方法仍然是学院中的一个研究课题。

 

#10 没有分析存储器空间

大多数嵌入式系统的存储空间是有限的。但是绝大多数程序员并不清楚他们设计中的存储空间的使用情况。当被问起某段程序或者某个结构占用了多少存储空间时,就算说错数量级也不是少见的事情。

在微控制器和DSP中,访问ROM,内部RAM,或是外部RAM的性能是明显不同的。综合分析存储器和性能,并将最常用的代码和数据段放入最快的存储器中有助于最有效地使用性能最好的存储器。带CACHE的处理器也能提高性能。

在现代的大多数开发环境下分析内存是一件简单的事情。大多数环境都在编译和连接阶段提供关于存储器分配数据的 .map 文件。要综合分析存储器和性能就困难很多,但是如果性能是个问题的话就当然值得这么做。

 

 

#9 用#define声明定义的配置信息

嵌入式程序员经常在他们的 C 代码中使用 #define 声明定义寄存器地址,数组上限,以及配置常量等。尽管这种方法经常使用,却并不是很好。因为这样的话紧急情况下就不能在线打补丁,而且也增加了在其他项目中复用代码的困难。

因为代码中到处都是 #define,这个问题就更为麻烦。这个值可能出现在代码中20个不同的地方。如果需要修改这些值,准确的找到修改的地方并不容易。

举个应急补丁的例子,假设用户在他们的应用中发现,硬件设置的64ms的周期定时器不够长,需要改为256ms。如果使用了#define,那整个工程都需要重新编译,或者机器代码中每个使用这个值的地方都需要打补丁。

相反,如果这个信息存在一个可以配置的变量中(也许是存在非易失性的存储器中),那就只需要修改一个地方的值,然后代码也不需要重新编译。最坏的情况就是复位重启系统。如果需要重新编译的话就不能在线升级,因为用户通常不愿接收重新编译并加载代码。另外,设计人员也需要修改并升级一个新版本。

举一个软件复用性的例子,假设操作I/O设备的代码中用 #define 定义了每一个寄存器的地址。如果系统中增加一个同样的设备,原来的代码就不能直接拿来使用。而需要拷贝代码,同时修改端口地址。

其实可以使用一个映射I/O设备可能使用到的所有寄存器地址的数据结构。举个例子,一个I/O设备有一个8位的状态端口,一个8位的控制端口,和一个16位的数据端口,地址分别 0x4080,0x4081,0x4082。我们就可以象下面一样定义:

typedef struct {

     uchar_t        status;

     uchar_t       control;

     short          data

} xyzReg_t;

 

xyzReg_t *xyzbase = (xyzReg_t *) 0x4080;

     .

     .

xyzInit(xyzbase);

etc.

要在地址 0x7E0 上增加第二个设备,只需要轻松的象下面一样增加另一个变量:

     xyzReg_t *xyzbase2 = (xyzReg_t *) 0x7E0;

                 .

                 .

     xyzInit(xyzbase2);

如果需要为这个值打应急补丁,可以从符号表中得到变量 xyzbase 的地址,因此就能预先知道需要修改的存储器的准确地址。

 

#8 第一个正确答案不是唯一的答案

没有经验的程序员特别容易认为他们得到第一个正确答案是唯一的答案。开发嵌入式系统软件经常会让人感到灰心。花了数天才搞明白怎样设置寄存器以让硬件按照自己的意愿工作。在某个时候,噢!它终于能跑了。一旦跑起来了,程序员就会删去所有调试代码,然后将这些代码作为好代码合入模块中。永远不要再修改这些代码了,因为花了这么长时间调试,没有人愿意去破坏它。

很不幸的是,第一次的成功往往不是最好的解决方法。它的确是重要的一步,因为比起让系统跑起来,改善一个能跑的系统更为容易。人们却很少再去改进一个已经运转起来的系统,特别是这个系统看起来已经跑的不错。但是,我还要拐弯抹角的说,这可能是一个需要耗费巨大系统资源的糟糕设计,比如占用太多处理器时间或存储器,或者是如果它优先级高的话,使系统时序异常。

一个优秀的设计,通常至少有两个以上的方案。大多数情况下,最好的设计是其他方案的折中考虑的结果。如果一个开发人员只能给出唯一的解决方案,那就应该咨询其他专家以得到另外的方案。

 

#7 #include "globals.h"

一个包含所有系统常量,变量定义,数据类型定义,和/或函数原型的头文件是代码不能复用的信号。在代码走读中,只需要花五秒钟时间看看是否有这样的文件存在就能知道代码能否复用(请看错误 #25 复用代码不是为复用而设计)。立即发现这个问题的诀窍就是看看是否有一个头文件的存在,通常叫 global.h,但有的也叫 project.h,defines.h,和 prototype.h。这些文件包含所有的数据类型,变量,#define 定义,函数原型,以及其他的应用需要的头信息。

程序员说这样会使他们的生活更为轻松,因为在每一个模块中,他们所需要做的就是在他们的 .c 文件中只包含一个 .h 文件。很不幸的是,这个偷懒的代价就是显著增加开发和维护的时间,同时也因为高度耦合(请看 #18)而使得不能在其他项目中复用这里的任何代码。

正确的方法是遵守严谨的模块化规范。任何模块都分为两个文件, .c 和 .h 文件。.h文件中仅包含模块为外部提供的接口信息,而 .c 文件中包含模块的其他所有内部信息。关于强制的模块化规范的更详细信息在后面的错误 #2 无命名和风格规范中。

 

#6 编码完成后写文档

每个人都知道大多数情况下系统文档是隐晦难懂的。很多组织都尽力使任何东西都文档化,但是文档并不是在正确的时间内完成。问题就是文档经常是在代码写完后补上的。

文档必须在编码之前或者之中完成,而永远不能在之后。在编码开始之前,应该先进行详细设计并写文档。这将成为以后用户和系统文档的基础。完全按照文档来编码。发现文档中任何不明确的地方,应该先纠正文档。这不仅保证了文档及时更新,还保证了程序员按照文档来编码。

在编码过程中更新文档就象代码走读。通常,程序员在写代码的过程中发现他们的BUG。比如,“如果成功,函数返回1”。程序员就会想,如果不成功话,返回什么呢?他们就会去看他们的代码,然后就发现不成功情况没有正确处理。

 

 

 #5 无代码走读

很多程序员,包括初学者和专家,保护他们的代码就像发明家保护他们的专利思想一样。很不幸的是这种情况破坏了应用的健壮性。通常,这些程序员知道他们的代码凌乱,所以他们害怕别人看见这些代码并发表意见。因此,他们就象人们不让父母看到自己凌乱的房间一样藏起他们的代码。

为保证软件的健壮性,正式的代码走读(也叫软件检视)必不可少。代码走读应该定期进行,并针对所有合入系统中的代码。正式的代码走读应该由多个人来执行,在纸上做下记录并进行跟踪。软件工程研究表明,一天代码走读发现的BUG超过一个星期的调试。

程序员也应该养成自己回看代码的习惯。很多程序员在计算机上敲进代码,然后运行,看看有什么问题,如果出错,就开始调试它,甚至没有任何纸面的跟踪记录。花一天时间人工跟踪代码能节省数天甚至是数星期的痛苦调试。

 

#4 不分青红皂白的使用中断

中断可能是实时系统中优先级倒置的最大原因,使得系统不能满足它的时序要求。原因是中断会抢占其他所有的一切,并且时间是不确定的。如果它抢占一个定期发生的事件,就可能产生不可预料的行为。在一个完美的实时系统中,不应该有中断的存在。

很多程序员将百分之八九十的处理放到中断服务程序中。完整的I/O命令处理和周期循环体是中断服务程序中常见的东西。程序员认为中断服务程序可以减轻操作系统的负荷,使系统运行的更好。当然这样相对报文切换来讲是可以减轻系统的负荷,但是基于下面两个主要理由系统并没有必要运行的更好:

Ÿ   时间不能调度以提供执行保证,因为可能产生rate-monotonic或者earliest-deadline-first等实时调度算法问题。

Ÿ   中断服务程序只能通过全局变量来交换数据。下一个错误会解释这为什么不好。

   相反,尽可能使每个线程周期执行,避免不定期事件。如果使用一个采用固定优先级调度算法的实时操作系统,就使用rate-monotonic算法为每一个线程安排优先级。如果使用一个采用动态调度的实时操作系统,就使用earlist-deadline-first 算法。如果没有实时操作系统,可以使用[4]中描述的非抢占的earlist-deadline-first 算法。

 

#3 使用全局变量!

全局变量经常遭到软件工程师的反对,因为它与面向对象设计的封装原则背道而驰,使软件更难以维护。把这个原则放到实时软件开发中,在系统中避免使用全局变量就更为重要。

在大多数实时操作系统中,进程为线程或者是轻量的进程。进程共享相同的地址空间以减少执行系统调用和报文切换时的负荷。同时带来的负面影响就是一个全局变量将自动为所有进程共享。因此,使用同一个模块内定义的一个全局变量的两个进程就要共享同一个值。这样的冲突会破坏功能,而不仅仅是软件的可维护性。

很多实时软件程序员把这个作为一个使用相同存储器的好方法。但是,在这种情况下,就要小心保证互斥操作以防止竞争而产生的难以预料的问题。不幸的是,很多防止竞争的结构,如信号量,并没有很好的实时性,并且会产生难以预料的阻塞。同时,象优先级反转协议(the priority ceiling protocol),明显使用过度。

互斥和竞争在任何操作系统的课本中都有讲到。

 

#2 无命名和风格规范

在非实时系统开发中,这个错误应该放在 #1。

没有命名和风格规范下开发软件,就象盖房子没有任何建筑图纸。没有规范,组织中每个程序员都按照他们自己的喜爱行事。当其他人要读这些代码时问题就出来了(如果项目中正确的按照错误“#5 无代码走读”中所说的做代码走读,问题就立刻出来了,而不会等到后来)。举个例子,设想两个不同的程序员开发同一个模块。第一个程序员花了一个小时能理解并校正代码,让另一个程序员来做就需要一天,也就是说要增加800%的人力资源。

影响代码可读性的主要因素就是命名规范。如果遵守严谨的命名规范,看到一个名字就知道它的意义,在哪里定义,它是一个变量,常量,宏,函数,数据类型,还是其他的声明。这个规范必须写出来,就象设计图纸上的图例,以让任何读这个代码的人都知道这个规范。

下面引用了Maryland大学实时系统软件工程实验室(SERTS)强制执行的命名规范。那些花很少时间学习了这些规范的研究人员都很欣赏他们写出来的代码的可读性,特别是在他们读过其他没有任何编程规范的人写出来的代码后。一个组织是喜欢这套规范还是他们自己的规范都不要紧,重要的是能给出一个选择这套命名规范的好理由,能够把这套规范写出来并发给所有的开发人员,并能严格遵守。

SERTS 命名规范

要想让软件的可维护性在一个很高的水平,仅仅开发出一个个模块是不够的。维护软件的一个最大成本就是花时间去理解原来的程序员在代码中的思路。想降低这个成本,严谨的风格和命名规范就必须在整个组织中得到严格的执行。这里就给出了一套已经被证明是很不错的命名规范。

一个组织应该坚持让所有的开发人员都使用一套命名规范。代码走读的一部分工作就要检查规范的执行情况。如果有必要,一个公司可以对没有遵守这套命名规范的开发人员采取经济制裁。这看起来似乎很可笑,但是如果想到因为开发人员没有遵守规范而在下一年花费公司50000美元时就不会这样认为了。如果开发人员宁愿使用自己的规范,那就不幸了。就像艺术家必须严格遵守指导书以使自己的设计能够得到建筑审查人员的批准一样,一个软件工程师必须严格遵守公司建立的规范以使他们的程序得到QA部门的批准。

衡量软件可维护性的基本标准如下:

Ÿ   如果客户投诉软件错误,多快能够找到这个错误?

Ÿ   如果客户要求增加一个新功能,多快能够完成?

Ÿ   一旦错误定位,需要修改多少行代码?

   显然,上述问题很大程度上依赖实际应用的情况和问题的难度。但是,如果把两段具有相同功能,需要相同修改的代码放在一起,哪段程序遵守的规范能够让工作更快的完成?这就是衡量软件可维护性的主要准则,不仅仅是比较设计,还应该比较风格和规范。

   命名规范尤其重要。软件的可维护性与使用的命名规范直接相关。看到任何名字,就可能立刻区分出是常量,变量,宏,还是函数,也知道它是局部的,全局的,内部的,还是外部的。统一采用表1中的命名规范就能够从名字中得到这些信息。

   表1:为提高C语言程序的软件可维护性采用的SERTS命名规范

Synbol

Description

xyz.c

模块“xyz”的源程序文件。

xyz.h

模块“xyz”的头文件。

任何在这个文件中定义的名字都必须有一个 xyz 或者 XYZ 的前缀,并且必须是模块为外部提供的接口。

xyz_t

模块 xyz 的主要数据类型。

在文件 xyz.h 中定义。

xyzAbcde_t

模块 xyz 的次要数据类型“Abcde”。

在文件 xyz.h 中定义。

xyzAbcde()

使用数据类型 xyz_t 中元素的函数“Abcde”。

XYZ_ABCDE

模块 XYZ 中的常量。

必须在文件 xyz.h 中定义。

XYZ_ABCDE()

模块 XYZ 中定义的宏。

必须在文件 xyz.h 中定义。

xyz_abcde

模块 xyz 为外部提供的全局变量。

必须在文件 xyz.c 中定义,并且在 xyz.h 中用 extern 声明。全局变量应该尽量避免使用。

ABCDE

_ABCDE

_ABCDE_FGH

模块内部使用的局部常量。 必须在文件 xyz.c 的最前面定义。

第三种格式允许使用多个单词,如_ABCDE_FGH。如果仅仅是“ABCDE_FGH”,就表示模块“abcde”。

abcde

局部变量。必须在函数内部定义。

结构内部的定义也使用这种方式。

_abcde

内部全局变量。必须在文件 xyz.c 的最前面用“static”定义。

_abcde()

内部函数。必须用“static”定义。原型放在文件 xyz.c 的最前面。

函数体必须在放在文件 xyz.c 的后面,并且在所有外部函数体之后。

_abcde_t

内部定义的数据类型。必须放在文件 xyz.c 的最前面。

abc_e

模块 abc 为外部提供的枚举数据类型。

每个枚举数据类型的入口必须使用相同的规范定位为常量。

 

   函数通常应该象表2所示命名,每个为外部提供的函数都存在一个意义相对的函数。成双的定义函数名有两个重要好处。它能强制开发人员保证设计的完整性,允许开发人员同时创建两个函数,并用一个函数去测试另一个。另外确信两个成双的函数具有相对的意义。比如,当采用表1中的命名规范时,就能保证 send 相对函数不是 read,create 的相对函数不是 finish。再举个例子,如果同时创建读和写两个函数,就可以先写再读来验证两个函数。另外,在不同的模块中使用相同的规范也是应该的。

2:常见的成双定义的函数名

xyzCreate <--> xyzDestroy

xyzInit <--> xyzTerm

xyzStart <--> xyzFinish

xyzOn <--> xyzOff

xyzAlloc <--> xyzFree

xyzSnd <--> xyzRcv

xyzRead <--> xyzWrite

xyzOpen <--> xyzClose

xyzStatus <--> xyzControl

xyzNext <--> xyzPrev

xyzUp <--> xyzDown

xyzStop <--> xyzGo

  

   这样来写软件,在需要分解时就能很快完成。函数命名时应该按照“从大到小”的顺序,而不是按照自然读的顺序。比如,如果模块 xyz 有一个子结构 xyzFile_t ,那么使用这个结构的函数就应该按照下面方式来命名:

   xyzFileCreate

   xyzFileDestroy

   xyzFileRead

   xyzFileWrite

   而不是:

   xyzCreateFile

   xyzDestroyFile

   xyzReadFile

   xyzWriteFile

   注意到任何函数名的最后一个单词是表示函数执行动作的动词,中间是表示动作使用的模块的名词。

   这样的规范就很明显的表示 xyzFile 是模块 xyz 的子模块,而在第二种命名中就没有这么明显。而且,如果模块 xyz 规模不断增加而设计人员决定进一步分解它,就很容易将整个子模块 xyzFile 和相应的函数分开成一个独立模块,比如叫做 xyzfile。只要全局搜索和替换 xyzFile 为 xyzfile 就完成了全部修改,几分钟内分解就完成了。如果没有按照这套规范来命名,就需要花很长的时间来把名字转换为新模块中使用的名字。

   对模块命名时使用一些意义模糊的短名字是可以接受的,因为这个名字将要作为所有命名的前缀。仅仅用意义明确的缩写作为函数名。如果没有这样的缩写,就使用全名。如果使用了缩写,就应该在整个项目中都使用。

   比如,规范中使用 xyzInit 作为模块 xyz 的初始化函数,就不要再用 xyzInitialize。另外一个例子,可以使用 snd 和 rcv,或者是 send 和 receive,但不能混用。其他常见的缩写有 intr 为 interrupt,fwd 为 forward,rev 为 reverse,sync 为 synchronization,stat 为 status,ctrl 为 control。但是,意义不明确的缩写是不推荐的,因为降低了可读性,如 trfm 也许是 transform 的缩写。这种情况下,函数名不缩写是一个更好的选择,如 xyzTransform()。另外一个过分使用缩写的例子,比较一下 xyzFileCreate() 和 xyzFileCrt()。后面一个使用了不经常用的缩写,在软件维护阶段读代码时就不太好懂了。相比不能明确表达函数的实际意思,使用一个稍稍长点的函数名更好一些。

 

#1 没有测量执行时间

很多程序员在进行实时系统的设计,但是他们对他们任何模块的代码执行时间都一无所知。举个例子,一个公司让我们定位他们系统中偶尔发生的错误。依照我们的经验,这是一个时序或者是同步上的错误。因此我们首先提了一个小小的要求,一张系统中进程和中断服务程序的清单以及相应的执行时间。这个清单对他们是很容易得到的,所以他们欣然提供给我们。但是,他们并没有测量执行时间,而仅仅由设计人员在代码执行之前估计了一下。

我们的首要任务就是测量各个进程和中断服务程序的执行时间,然后很快就定位系统发生错误的原因是因为系统负荷过重。这个公司也说:“对,我们也很清楚这个!”但是,他们听到空闲进程竟然占了20%的执行时间时非常惊奇。(如果测试所有的任务,就包括空闲任务)问题是他们所有对执行时间的估计都是错误的。甚至有一个中断服务程序按照估计是几百微秒,而实际执行时间竟达6毫秒!

当开发一个实时系统时,应该测量每一步的执行时间。这包括代码的每一行,每个循环,每个函数等等。这应该是一个不间断的过程,就象测试功能一样。在测量执行时间的同时,对照一下估计的结果。如果测量出来的时间不可思议,就要分析它,计算每一步的时间。

有些程序员等到所有的事情都做完了才开始测量时间。这样的话系统经常会出现很多时序上的问题,而仅仅测量一下时间就会为解决这些提供足够的线索。“实时系统”中的关键词就是时间

 

参考

[1] M. Hassani and D. Stewart, "A Mechanism for Communicating in Dynamically Reconfigurable Embedded Systems",?inProc. of High Assurance Systems Engineering Workshop, Washington DC., August 1997.

 

[2] B. L. Jacob, "Cache design for embedded real-time systems", Embedded Systems Conference Summer, Danvers  MA, June 1999.

 

[3] S. Shlaer and S. J. Mellor, "Recursive design of an application-independent architecture",?IEEE Software, v.14, n.1, pp. 61?2, Jan/Feb 1997.

 

[4] D. Stewart, "Designing Software Components for Real-Time Applications",?inProc. of Embedded Systems Con-ference, San Jose, CA, September 1999.

 

[5] D.B. Stewart, R.A. Volpe, and P.K. Khosla, "Design of dynamically reconfigurable real-time software using port-based objects",?IEEE Trans. on Software Engineer-ing, v.23, n.12, Dec. 1997.

 

[6] M. Steenstrup, M. Arbib, and E.G. Manes. Port Automata and the Algebra of Concurrent Processes,Journal of Computer and System Sciences, v. 27, n.1, pp. 29-50, Jan. 1983.