1、实时软件开发中二十五个最常见错误(仅供内部使用)目 录摘要 4介绍 4#30 “我的问题与众不同 ” 5#29 工具的选择是由市场宣传的驱动,而不是技术需求的评估 5#28 特别大if-then-else和case 的描述 6#27 用空循环实现时延 6#26 交互式和不完整的测试程序 7#25 移植代码并非为移植而设计 7#24 基于单一架构的归纳 8#23 一个大循环 8#22 超负荷设计系统 8#21 在开始软件设计之前没有考虑硬件特性 8#20. Fine-grain optimization during first implementation. 9#19 “只不过是小故障而已”
2、9#18 太多模块间的从属关系 10#17 “最后期限临近 ”!没有时间休息 10#16 使用消息传输作为基本的进程间通信 11#15 没有人可以帮助我 12#14 只有单一的设计图表 12#13 在设计图中没有图例 13#12 使用POSIX风格的设备驱动 14#11 错误检测和处理是在事后进行,并且是在尝试和错误中实现 15#10 没有分析存储器空间 15#9 用#define声明定义的配置信息 16#8 第一个正确答案不是唯一的答案 17#7 #include “globals.h“ 17#6 编码完成后写文档 17#5 无代码走读 18#4 不分青红皂白的使用中断 18#3 使用全局变
3、量! 18#2 无命名和风格规范 19#1 没有测量执行时间 22参考 22实时软件开发中二十五个最常见错误David B. StewartSoftware Engineering for Real-Time Systems LaboratoryDepartment of Electrical and Computer Engineering and Institute for Advanced Computer StudiesUniversity of Maryland, College Park, MD 20742Email: dstewarteng.umd.eduWeb: http:/w
4、ww.ece.umd.edu/serts1 摘要这里将列出嵌入式实时软件开发中最常见的错误和缺陷,并着重阐述这些错误的起因和潜在的危险性。同时也将讨论解决的方法,包括更好的教育以及使用新技术和最新的研究成果。而这些常见错误包括从高层次的项目管理方法中的问题到低层次的设计和实现中的技术问题。作者总结了很多嵌入式程序员在软件设计和实现中的经验教训,包括从公司里经验丰富的专家到在学校刚刚开始学习的新手,确定了这些最常见的错误。2 介绍不管是在大学还是在公司里,新手和专家们一样,在开发实时软件的过程中都在不断重复着同样错误。在为学院里的项目代码进行总结和评价,以及作为一个顾问为公司的很多设计和代码进行
5、评论的过程中我得出了这个结论。大多数实时软件开发人员都没有意识到他们最喜欢的方法存在问题。通常专家们都是自学成才,因此他们会有和刚开始时同样的坏习惯,因为他们就从来没有见到过设计他们的嵌入式系统的更好方法。这些专家们然后教新手,新手们也就继承了同样的坏习惯。这篇文章将有助于大家清楚这些常见错误并开始避免这些错误,以设计出更可靠,更好维护的软件。这篇文章原来描述了十个常见错误,但是由于常见错误越来越多,以至于后来想保持在二十五个以内都难。尽管文章题目是二十五个,这里实际上列出了三十个常见错误。对于每个错误,这里都说明了错误的根源或是概念上的错误。同时也给出了降低或者避免这些错误的产生的可能的解决
6、方法或是选择。如果读者不太熟悉这些替代解决方案的细节或者是术语,应该到图书馆或者是网络上查看相关的参考资料。大多数错误大家的看法都一样,但这里列出的某些错误以及建议的解决方法却可能存在争议。在这种情况下,简单的说明最好解决方法上的分歧将有利于鼓励设计人员拿自己的方法和其他各种方法比较,重新思考他们自己的方法是否更好。在一个项目中纠正这里列出的某一个错误,就可能节省人力几周甚至是几个月的时间(特别是在软件生命周期的维护阶段),或者是显著提高项目的质量和健壮性。如果多个错误相同并得到解决,就有可能为公司节省或者增加数千甚至数百万美元。因此,提倡大家都针对这里列出的每一个问题,问问自己目前的方法和准
7、则,比较这里给出的解决方法和建议,然后作出选择。改变你目前的某些方法将可能不需要增加任何附加成本而为你的项目或者公司提高效益,提高软件的质量和健壮性。这里列出的三十个最常见错误,越后列出的错误(#30在最前而#1在最后)对软件质量,开发时间和软件可维护性的影响越大。当然这个顺序也是我的观点而已。并不就是说前面列出的问题就没有后面列出的重要。重要的这些问题都列出来了,而这些都可能在你特定的环境中显的很重要。3 #30 “我的问题与众不同”很多设计者或者程序员拒绝听其他人员的经验,宣称他们的应用不同,并且更复杂。设计者对于这些相似的工作应该有更加开放的思维。如果从实时系统原理的具体细节上来看,即使
8、是那些看起来大相径庭的应用也有可能是差不多完全相同的。例如,通讯工程师宣称他们的应用与控制工程师没有任何相似性,因为大容量的数据和需要特别的处理器如DSP。相应地,问“蜂窝电话的LCD显示软件和一个温度控制器的软件有什么不同?真的不同吗?”一一对比控制和通信系统,他们的特征都是具有输入和输出模块,以及相应的函数。一个DSP处理256x256图象的算法与一个320x200的点阵LCD的显示原理可能就没有什么很大的区别了。甚至,两者都使用了与应用程序大小相对应的内存和处理能力有限的硬件;两者都要求不仅在目标板上开发软件同时也在一个平台上进行,并且很多开发DSP软件的方法同样也适用于微控制器软件的开
9、发。虽然它们的时序和数据的容量是不同的,但是如果系统设计正确,那么这些参数仅仅是变量。分析内存和处理时间等资源的方法也是一样的,两者都需要相近的实时调度,并且两者都需要高速优先权倒置的中断处理器。或许你会说控制系统和通讯系统是相似的,两个不同的控制系统或者通讯系统其实也同样是如此。每一个应用是唯一的,但是抛开定义、设计和实现的过程,程序其实是相同的。嵌入式软件的设计应该尽可能多地学习其他人地经验。不要由于应用领域地不同,就觉得别人的经验无所谓。评述:开发人员(尤其是一些优秀的开发人员),往往有其刚愎自用的一面,总是认为自己的设计方法是最好的,不需要别人指手画脚。这样的心态往往限制了自己的提升和
10、发展,我们都知道有“触类旁通”的说法,很多发明的构想都是在与此不相干的事情中产生的,多听听别人的意见,必然会对自己的设计产生积极的影响,哪怕是很幼稚的意见,至少也可以让你去指点别人,只要是有意义的事情,为什么不做一下呢?这个错误虽然看起来简单,但它却涉及到我们的思维模式,你觉得自己有这样的问题吗?你能不能改正呢?4 #29 工具的选择是由市场宣传的驱动,而不是技术需求的评估嵌入式系统的软件工具的选择经常基于市场的亮点,因为很多人使用它,或者是因为那些看起来很诱人而实际上却没有任何不同的宣传。亮点:仅仅因为具有更漂亮的用户图形界面并不会让一个工具比别的工具更好。重要的是要根据实际开发应用的需要来
11、考虑两者的技术能力。用户的数量:从某个供应商那里买软件仅仅因为它是最大的供应商,并不意味着它是最好的供应商。 很多人使用一种软件的背后可能隐藏着这样的事实:很多人花了超过实际需要的冤枉钱,或者是很多人都拥有一些在发现并不合适后就将它们束之高阁的工具。兼容性的承诺:经理人员是很容易受到一种产品兼容性许诺影响的。就算这种软件与POSIX百分之百兼容又怎么样?它合适吗?你有改变操作系统的计划吗?就算你要改用一个与POSIX百分之百兼容的操作系统,你又能得到什么呢?除了“扩展性”外,你什么都没有得到。但是假如有扩展的话,那么兼容性就丢失了,因此好处就不再有了。想想所谓的标准,如POSIX也没有证明对实
12、时系统有好处,还是保持原来的最好。因此,不要因为许诺就假设产品的性能会更好。只要所有的设计者采用已被证明了的好的模块化的软件设计策略,那么轻便性和重用性就很容易获得。【4,5】当选择工具的时候,应该首先考虑实际应用的需求,然后从技术的角度来比较这些成打(或是上百)的备选方案,因为他们是和应用的需求特别相关的。对一个特定的设计来说,最合适的工具并不一定是最流行的工具。评述:很多事务的选择最终归结为它是否符合我们的实际需求,也就是它的实用性。在这方面局域网是一个很好的例子,虽然在开始阶段很多公司都提出了自己的设计方案,实践证明技术最优的并不是赢家,以太网成为最终赢家,原因就是实用性。5 #28 特
13、别大if-then-else 和case 的描述在嵌入式系统的代码中有很大的if-then-else或case的结构是不正常的,这会引起三个方面的问题。1、由于代码有很多不同的执行路径,所以它们将很难测试。假如是嵌套的话,就会变得更加的复杂。2、最好和最坏情况下的代码执行时间会明显的不同。这将导致CPU效率低下,或者是当执行最长路径时可能出现时序错误。3、结构代码覆盖测试的困难将会随着分支的数量成指数增长,所以结构分支应该最少。相反,用数学计算方法可以得到相同的结果。用布尔代数做一个跳转表来实现一个有限状态机,或用查找表来实现的话,可以把100行有ifelse结构的代码减少为 不到10行的代码
14、。这里有一个将if变成布尔代数的小例子:if (x = 1)x=0;elsex=1;用一个布尔代数的计算来实现:x = !x; / x = NOT x; can also use x = 1-x.虽然很简单,很多程序员却仍然在用上面的if结构来替代布尔运算。评述:复杂多层的if-then-else会给人一种头晕目眩的感觉,合理的使用一些技巧是会是复杂的逻辑变得很简单。下面是一个案例:在单板软件编程中,会处理主机下发的应用层消息包。处理消息的前提至少有一下几点:1.消息是发送到本板的(框号和槽号必须匹配);2.消息报命令字必须合法;3.消息报长度必须正确;每种不满足条件的情况,按照规定都应当有处
15、理方法。如果你的程序是下面的方式,则比较糟糕,竟然有3重判断:if ( ( 消息报的目的框号 = 本板框号 ) 将上面的逻辑判断整个取反试一试,每次只有一次逻辑判断,简单明了:if ( ( 消息报的目的框号 != 本板框号 ) | ( 消息报的目的槽号 != 本板槽号 ) )错误消息处理1;return;if ( 消息包的命令字不合法 )错误消息处理2;return;if ( 消息包长度不正确 )错误消息处理3;return;合法消息包处理;return;当然还有一些技巧在里面,比如:将最容易出现错误的判断放在前面,这样函数执行的效率比较高。6 #27 用空循环实现时延实时软件经常需要增加时延
16、来保证通过I/O口能有足够的时间来准备收发数据。这些时延通常都是通过增加一些空语句或空循环来实现(如果编译器有优化功能,要使用volatile保证变量不被优化)。如果这种代码在不同的处理器上使用,甚至对于一种处理器,只是运行在不同的主频(比如25MHZ或33MHZ)下,在快一些的处理器上这种代码很可能就会失效。这一点我们在设计时要特别注意,因为它将直接带来一些实时性问题,这种问题很难跟踪和解决,因为这类问题表现的症状是五花八门的。其实,我们可以使用一个基于定时器的实现机制。一些实时操作系统(RTOS)提供了这样的功能,如果没有,我们也可以很容易造一个定时器。下面列举了两种通用的造时延函数del
17、ay(int usec):大多数的递减定时器允许软件读取当前递减寄存器的值。我们可以使用一个系统变量来记录定时器的速率,单位为usec/tick。假如值为2usec/tick,现在需要一个10usec的时延,那么时延函数的忙等待时间为5个tick。就算换了一个不同速率的处理器,定时器的tick数还是一样。若是定时器的频率更改了,则系统变量需要跟着修改,并且忙等待需要的tick也需要修改,但时延时间仍然保持不变。如果定时器不支持读取瞬时计数值,另一种方法是在初始化时近似测出处理器的速度。运行一段空循环,并且对两次定时中断之间的循环次数进行统计。从而得知定时中断的频率,每次循环的usec值也可以计
18、算出来。该值可以用来动态衡量指定的时延到底需要多少个循环。在RTOS种使用这种方法,对于我们测试的任何处理器,时延函数的精度范围都在10之内,而我们也不需要每种情况都修改代码。评述:嵌入式软件与硬件接合紧密,软件的可移植性与操作系统、硬件紧密相关,上面提到的两种方法,对嵌入式软件的可移植性是非常有帮助的,值得借鉴和推广。7 #26交互式的和不完整的测试程序许多嵌入式设计人员创建了一系列的测试程序,每块测试程序是为了测试特定的一种特性。每个测试程序都必须单独运行,而且有时候需要用户输入一些内容(比如通过键盘或开关),然后观察输出响应。这种方法的问题就是编程人员仅仅想测试一下他们正在修改的部分。但
19、是既然不相关代码之间需要共享资源,就必然有交互作用,每次修改后都必须将整个系统全面测试一次。要完成这件事,就必须避免创建交互式的的测试程序。创建一个单独的测试程序,让他尽可能做到能自测,这样,任何时候即使有一点小改动,也能很容易而且迅速地完成一次完整的测试。不幸地是,说的总比做的容易,比如一些测试特别是I/O口的测试,只能交互式地完成。尽管如此,任何开发人员在编写测试用例前还是应该优先考虑到编写自动测试用例的原则,而不是写一步算一步,采用边写代码边测试的方法。8 #25 移植代码并非为移植而设计不是专门为移植而设计的代码,形式上不会是一种抽象出来的数据类型或是对象。这种代码很可能和其他代码之间
20、存在一定的交互性,因此如果采用所有的移植代码,那么就会存在很多我们不需要的代码在其中。如果只采用其中一部分,那么我们就必须象一个外科医生一样对代码进行解剖,如果我们对这些代码没有足够的认识,我们很可能在剔除这些不需要的部分时存在一定的风险,或是无意中影响到了其功能。如果代码不是为移植而设计,最好先分析一下现有程序的功能,然后重新设计和组合代码,将它改造成结构良好,可移植性好的软件模块【4】。这样代码就可以移植了。重新编写这个模块代码的时间将比直接修改和调试原始的移植代码的时间短得多。通常,有种误解认为,既然软件已经分割成了各个独立模块,那么它自然是可移植的。这本身就是一个分离性的错误,因为生成
21、的软件存在很多相互依赖性。详情请看错误18“模块间的耦合性太强 ”。评述:与其要移植缺少移植性的代码,还不如重新写过。9 #24 基于单一架构的归纳嵌入式软件的设计者可能需要开发能运用在不同的处理器上的软件。在此情况下,编程人员通常会先在其中的一种开发平台开始编软件,但是会在晚些时候为包装代码而做大量的准备工作。不幸的是,这样做通常弊大于利。这种设计试图过份的归纳出不同架构下的相同点,而不是不同点,但是设计者并不能预见到这些不同点。一种比较好的设计策略是在多个架构下同步设计和开发代码,归纳出那些不同架构下的差别,有意地挑选3到4种差别较大的处理器(比如不同开发厂商的产品或是采用不同架构设计的产
22、品)10#23 一个大循环当实时操作系统的代码被设计成一个单独的大的循环体,我们就无法独立地修改不同部分代码的执行时间了。很少有实时系统需要所有的模块都以同样的速率运行。如果CPU超负荷,其中一项可以利用来降低CPU占用率的方法是减慢部分非关键代码的运行速度。然而这种方法只对RTOS的多任务操作系统奏效,否则代码就是设计成基于灵活风格或商业实时运行环境中。11#22 超负荷设计系统如果处理器和存储器的平均利用率小于90,而峰值利用率小于100,那么我们就说这个系统属于超负荷设计。对于设计者而言,写程序使用过多的资源实在是一种奢侈的行为。在某种情况下,这种奢侈能直接导致盈利和破产的区别!软件工程
23、师有责任尽量减少一个嵌入式系统的价格和能源消耗。如果CPU的利用率只有45% ,那么就可以使用运行速度只有一半的处理器,因而减少了4倍的能量,而且可能每个处理器还能省下1美元或者更多的钱。如果这个产品大批量生产,每个处理器省下1美元,仅这一项整个产品就能省下100万美元。如果该产品是电池驱动的,电池就可以延长寿命,从而提升该产品的市场上的需求量。作为一种计算机家族中电源消耗的一种极端例子便携机,一般使用一种很沉的电池,最多能使用3个小时。而一块手表,重量轻,电池便宜,却能使用3年!尽管软件通常和能量消耗没有直接的关系,但它确实扮演了一个重要的角色。使用快速的处理器和更多的内存确实导致设计者懒于
24、考虑这方面的设计。建议开始设计时先考虑低速的处理器和少一些的内存,而只有在实际需要的情况下再升级处理器。如果我们的软件能利用更高效的硬件就更好一些,我们就不会是采用一个高速的处理器,然后却要尽量删除一些周边部件来降低系统成本。12#21 在开始软件设计之前没有分析硬件特性两个8-bit数相加需要多少时间?16-bit ?32-bit?两个浮点数相加呢?还有一个8-bit数加上一个浮点数呢?如果软件设计者不能在头脑中立即反应,针对他使用的处理器回答出上述问题,说明他还没有为设计和编制实时软件做好充分的准备。下面是一个6MHZ的Z180处理器的例子,针对以上问题的答案是(us 为单位):7, 12
25、,28,137,308!值得注意的是:浮点数加一个字节的时间要比浮点数加一个浮点数的时间多出250,主要原因是中间增加了很多从字节转换到浮点数的时间。这种不规则操作往往是导致处理器过载的源头。另一个例子,一种专用来处理浮点数的加法器处理浮点加法/乘法的速度比33MHZ处理器68882快将近10倍,但是sin()和 cos()函数的处理时间相同。主要原因是68882处理器有一个内置于硬件的三角函数处理器,而浮点数加法器只能通过软件处理。当需要为一个实时系统编写软件时,首先要考虑的是键入计算机的 每一行代码相关的实时性问题。要理解使用的处理器的性能和限制,并且如果执行代码使用了大量的长指令,最好重
26、新设计应用程序。比如说,对于Z180,最好全部使用浮点数运算,而不要采用部分变量使用浮点数,而另一部分为整数的设计,因为这样会带来大量的混合运算。13 #20. 第一次设计时过度优化与第21个问题相反的一个问题也是一种通用的错误。一些编程者预见到了这种不规则现象(有些是很实际的,有些则有些奇怪)有一种奇怪的不规则现象的例子就是乘法比加法要长得多。一些设计者会将3*x写成x+x+x.而在有些嵌入式处理器中,乘法比加法处理时间的两倍要少一些,因此x+x+x 的处理时间比3*x要慢一些。一个能预见所有的不规则现象的编程人员可能会为了优化代码,将第一个版本的代码编得可读性很差。那是因为他并不知道是否真
27、正需要优化。一般的原则是,在实现过程中不要使用完全优化。以后,如果已经证明优化后性能提高,再对代码实施优化。如果不需要优化,则还是保留可读性较好的代码。如果CPU过载,那么最好知道,代码中仍有很多地方可以采用直接优化的方式就能使性能得到明显提高。14#19 “只不过是小故障而已”有些编程人员往往一次又一次地使用同一个工作区,因为系统有一处小BUG。编程人员的典型的反应是,只要是使用过的工作区就能一直运行正常。殊不知,影响一个工作区的错误可能在不同时候表现出不同的形式。不管任何时候,只要存在“小故障”,它就说明系统中确实存在一些问题。最好的方法是按照适当的方法一步一步将问题的本质理解清楚。使用一
28、个工作区可能可以保证按时交差,但是如果多花一点时间,查出问题,保证问题在任何时候都不重现,如下一次重要演示会,可能意义更大,虽然可能会比期限时间晚一些完成。15 #18 模块间耦合性太强好的软件设计中,模块之间的耦合关系可以被描述成一个树状结构,就象图1(a)中所示。从属关系图是由节点和箭头组成,每一个节点代表一个模块(例如:一个源代码文件),而每个箭头标明这个模块所依赖的其他模块。位于最底层一行的模块不依赖于任何其他的软件模块。为使软件重用性最大化,箭头方向应当是向下,而不是向上或双向的。例如:图中abc模块如果在其代码中有:#include “def.h“ 或在abc.c文件中有用exte
29、rn声明的def.c 文件中所定义的变量或者是函数,那么它就依赖于def模块。模块的耦合关系图是一项非常有价值的软件工程工具。通过这张图,可以轻易地做以下工作:(1)识别软件的哪些部分可以被重用;(2)为模块的测试工作建立一种策略;(3)为限制错误在整个系统中的传播提供一种方法。每一个环状的依赖关系(例如:在图中的环状连接)都会降低软件模块的重用能力。测试工作只能针对相互关联的模块集,问题将很难被孤立到一个独立的模块中。如果在从属关系图中有太多这样的环状连接,或者在图中最底层模块与最高层模块之间存在这样的环状连接,那么整个软件将没有一个独立的模块是可以重用的。图1(b)和(c)都包含这种环状的
30、依赖关系。如果这样的循环依赖关系是不可避免的,那么宁愿选择(b)而不是选择 (c),因为在(b)中仍有部分模块是可以重用的。图1(b) 中的限制是模块pqr和xyz只有接合在一起才能被重用。而在图1(c) 中,由于在模块之间耦合性太强,没有一个子模块是可以重用的。还有,由于有一个大的环状依赖关系,即使是模块xyz,本来它位于关系图中的最底层,应该不依赖于任何其他模块,现在却要依赖于模块abc。仅仅是因为这样一个大环,就导致整个应用程序的所有模块都不可以重用。不幸的是,目前绝大多数应用程序都更类似于(c),而不是(a) 和(b) ,因此也导致我们现存应用程序中软件模块重用的困难。要更好的使用依赖
31、关系图来分析软件的可重用性和可维护性。在编写代码的时候就注意使代码更容易生成关系图。这意味着,在模块xxx中,函数中用extern声明的所有外部变量都要在文件xxx.h中定义。在模块yyy中,只要简单的察看一下有哪些头文件用#include包含了,就可以明确所依赖的模块。如果不遵守上面的规则,yyy.c中存在extern 声明而不是用#include包含相应的文件,那么将导致依赖关系图的错误,同时导致试图重用那些看起来似乎是独立于其他模块的代码将非常困难。16 #17 “最后期限临近”!没有时间休息很多程序员会为一个问题,连续工作几个小时而废寝忘食。他们连续工作的主要原因是因为有“最后期限”,
32、这样的最后期限可能是公司里产品开发的截止时间,或学校布置的家庭作业完成时间。但事实上,如果你在连续工作了一个小时而没有任何进展的时候稍做休息,往往可以“节省”很多的时间。你可以沿着湖边转转、来杯啤酒、打个盹什么的.清醒的大脑来部分源于精神的放松,它使你更容易分析发生了什么,更快的找出问题解决的方案。即使是在“ 最后期限临近”的时候,也不要忘了两个小时的休息,可能节省一整天的时间;离开电脑10分钟的短暂休息,往往会节省一个小时的时间。17#16 使用消息传送作为主要的进程间通信方式当软件按照功能模块划分进行开发的时候,首先想到的是以消息作为输入、输出。尽管这种方式在非实时环境(例如:分布式网络)
33、应用的很好,但在实时系统应用中,却存在一些问题。在实时系统中,使用消息传输会引发三种主要的问题:1.消息传送需要同步,这是实时调度不可预知的主要原因。如果功能模块同步终止执行,将导致系统的时序分析变得困难,即便不是不可能。2.在存在进程间双向通信或反馈回路的系统中,都有死锁的可能性。相反,应该使用基于状态的系统,比如:要说明“打开制动装置 ”的操作,状态的更改被描述为“制动装置应当打开” 。3.与使用共享内存的方法相比,存在着开销更大的问题。通过网络和串行线路通信可能需要通过消息,而如果能够随机访问数据的话,比如单处理器上的进程间通信,消息传送通常是效率低下。在嵌入式系统中,更推荐使用基于状态
34、的通信方式以确保更高的可靠性。一个基于状态的系统使用结构化的共享内存,可以使通信的开销降低。当进程需要状态信息的时候,总是可以得到最近的状态信息。Streenstrup和Arbib发展了一种port automaton(端口自动仪) 的理论来正式地证明一个稳定、可靠的控制系统可以仅仅通过读取最新的数据来建立6。通过创建共享数据的本地拷贝,确保每个进程可以互斥的访问自己所需要信息 就消除了代价昂贵的阻塞。如果系统有可能出现消息丢失,或者如果不是所有模块按照相同的速度运行以及如果想应用共享内存以降低操作系统的开销,使用状态而不是消息都可以为系统提供更好的稳定性。在4 中,给出了一个高效的基于状态通
35、信协议的例子。将一个基于消息通信的控制系统转换成一个基于状态通信的控制系统通常是容易的。例如:为实现列车管理的最大化,一个智能铁路控制系统可以独立的控制每一个车上的制动装置。当需要停车时,为让列车在最短的距离内停止,就需要所有车上的制动装置一起启动。由于每一个制动装置的输入/输出逻辑是受控于一个独立的进程,而控制模块必须通知每一个制动模块去打开制动开关。当使用基于消息的系统时,控制系统发送一条消息,“ 打开制动开关 ”给每一个制动进程。这种方式会导致:很大的通信开销;如果任务运行的频率不同,还有丢消息的潜在危险;不确定的阻塞;每个进程一份独立的消息拷贝;以及存在死锁的可能性。由于进程之间的相互
36、依赖性,消息通信方式建立起的实时系统是难于分析,而且不适合需要重新配置的系统。相反,在基于状态的通信机制中,每一个制动模块周期性的执行,检测制动变量brake来及时更新自己的制动I/O端口的状态。因为进程是周期性的,因此操作时序分析相对容易。每个进程也只需要状态变量表中的一个单一元素相关,这样消除了进程之间的直接依赖关系。相对与消息传送系统,共享内存的通信方式也减少了系统的开销。当在对象之间传送数据流的时候,需要在共享内存中,建造一个“制造者/ 消费者”型的缓冲区,这样在每个周期循环中可以处理最大数量的数据。18 #15 没有人可以帮助我几乎所有的教师都有这样的共识:通过讲授的方式,可以是你对
37、一个问题理解的更深刻。在遇到问题的时候,实时系统的程序员经常感到无助,如:操作的I/O设备并不象文档中描述的的那样工作。在多数情况下,团队中的其他人都没有他本人在这个领域了解的知识丰富,导致只有一个人独自面对眼前的问题。由于没有找到合理的处理方法,这种错误的观点往往会导致整个产品开发进度和开发质量的下降。当面对这样的情况时,如果没有人比自己更有经验,那么就将相关的材料教授给哪些缺少相关经验的人们,使他们更好的理解问题。如果没有对知识理解更深刻的人来帮助,那么就去向那些理解相对较浅的人寻求帮助。特别是很多团队中有一些非常愿意学习新东西以获取经验的新人,向这些渴求知识的人去讲解程序是如何工作的,以
38、及目前存在的问题。他们很可能不能完全理解问题,但他们所提出的问题也许会使你产生一些想法或暴露出一些被忽视的问题,并最终使你产生解决问题的方法。这种处理问题的方法还有一个非常好的副作用。他对新员工起到了培训的作用,当公司需对这方面高级编程技能人员有需求时,就不是单单一个人可以胜任了。19#14 只有一张设计图表大多数软件系统的设计,整个系统通过一张设计图表来描述(更有甚者,干脆没有)。然而,一个象桌、椅这样的物理实体,尽管他们没有软件项目那么复杂,但仍可以有很多种图表来描述它,如:顶视图、侧视图、底视图、细节图、功能图.当设计软件的时候,将整个设计在图纸上展现出来是最基本的。普遍被接受的方式是通
39、过软件设计图的建立来实现。整个设计有很多不同的设计图,每张图的设计都从一个不同的角度展现了系统。此外,还存在在好设计图和差设计图的区别。一份好的设计图在图纸上清晰地反映了设计者的思想;而差的设计图则是混乱的,模棱两可的,在图中遗留了很多未解的问题的。为建立一个优秀的软件,优秀的软件设计图是基础。通过好的设计图来实现设计的常用技巧有以下一些:1)一张大型项目自顶向下分解的结构设计图。它可以是描述对象、模块之间关系的数据流图,也可以是基于子系统之间数据交换的子系统图。2)在结构设计图中的每一个成员,都需要通过一张详细设计图来描述。这张设计图要充分详细,使编程员可以毫无疑问地执行设计中的细节。应当说
40、明的是,在一个多层分解的结构设计图中,在某一层面的详细设计可能成为更低一级设计的结构设计图。因此,相同的图表技巧可以应用在各种类型的图表中。软件设计人员必须明确区分出进行的是面向过程还是面向数据的设计。1)面向过程的设计,如:很多控制和通信系统中的设计,应当有数据流图(例如:控制系统描述)、处理流图(也被称为流程图),和有限状态机描述。2)面向数据的设计,通常在一些基本应用和数据库中使用,应当包含关系图、数据结构图、层次结构,以及表格。3)面向对象的设计是一种将面向处理和面向数据相结合的设计,应当包含表现不同视角的图表。20#13 在设计图中没有图例即便是有了设计图表,很多情况下也是没有图例的
41、。这使得图表中的数据流和处理流模块被混淆,而且由于图中的矛盾和模棱两可使整个图表的设计失败。即使是在一些软件工程教科书中的图表也存在有这样问题!评判一张设计图是否存在缺陷的最快捷的方法是看它的图例,确认图中每个方块、每条线、每个点、每个箭头、粗细、填充色以及其他标志是否都与在图例中规定的功能匹配。这条简单的准则就象一个语法检测器一样,使开发人员和检视人员能够快速的的找到设计中的问题。此外,它强迫每个不同类型的块、线和箭头被画成不同的样式,使得不同类型的对象在视觉上容易区分。事实上,是否采用象UML 这样的标准或采用公司开发出的一套规范去画图并不重要。重要的是在每张设计图中,都有图例,而且在同一
42、类型的图表中,使用相同的图例。一致性是关键。下面是创建一致的数据流图、进程流图和数据结构图的一些方法。对于应用所需的其他类型的图表,也可以建立起类似的方法。数据流图这类图表,根据模块之间通信的数据来描述模块之间的关系和依赖性。它通常用于模块分解阶段,是在结构设计一层中最常使用的图表。不幸的是,大多数的数据流图设计的很糟糕,而主要的原因往往就是在于图中的混乱和矛盾。要做出好的数据流图,要按照下面的方法去做:建立一种规范,并严格的遵守它,做一些图例来解释这个规范。要尽量减少进程或模块之间的连线(数据流)数目。要意识到在流图中,每个方块将成为一个模块或进程,而每一条连线将成为模块之间的某种耦合或进程
43、之间的通信。因此连线越少越好。一些典型的数据流图规范包括下面几点:1)矩形代表数据存储区,比如缓冲区、消息队列或共享内存。2)圆角矩形代表有自己进程执行的模块;3)直线代表从一个进程或模块中输出到输入另一个进程或模块的数据。在4中,给出了了一些关于控制系统数据流图的案例。进程流图这类图表通常描述在模块或进程内部的细节。他们通常使用于详细设计阶段。向数据流图的方法一样,建立一个规范,并严格遵守它,并做一些图例来解释这个规范。进程流图的典型规范有如下几点:1)矩形代表处理过程或计算过程;2)菱形代表判断;3)圆形代表开始、结束或转换点;4)直线代表执行代码的顺序;5)椭圆形代表进程间通信;6)平行
44、四边形代表I/O;7)条状物代表同步点。数据结构图和类结构图数据结构图和类结构图描述的是多个数据结构和对象之间的关系。这类图表应包含足够的细节,可以直接在模块的.h文件中创建结构(在C语言中)或类(在C+中)。这类图表典型的规范如下:1)单个矩形代表一个结构或类中的一个域;2)一组相邻的矩形代表同一结构或类中的所有域;3)非临近的矩形说明同他们属于不同的结构或类;4)从一个矩形伸出的箭头代表一个指针;箭头的另一侧代表被指向的结构或对象;5)实线代表类之间的关系。在图中,应当有描述不同关系类型的图例。不同的关系类型,应当采用一条不同线宽、颜色或类型的线来代替。在图2中有一个数据结构图的例子。在3
45、中有一个多种关系的类结构图的例子。1 #12 使用POSIX风格的设备驱动设备驱动是用来提供一个对硬件I/O设备的操作接口层,这样高层软件就可以通过统一的,与硬件无关的方式访问设备。不幸的是,很多商用的实时操作系统采用的UNIX/POSIX 风格的设备驱动并不能满足嵌入式系统设计的需要。特别要说的是,目前系统中使用的接口,如 open(), read(), write(), ioctl(),以及 close()等都是为文件或者是其他面向流的设备而设计的。相反,大多数实时 I/O 都有连接到I/O 端口的传感器和激励器。 I/O端口包括并口,模数转换器,数模转换器,串口,或者是其他特殊用途的处理
46、器,如处理照相机或麦克风数据的DSP。为了尽量在这些设备上适应POSIX 设备驱动应用程序设计风格,程序员将不得不在应用层编码实现硬件特性。考虑下面例子:控制一个机电设备的实时软件要打开两个螺线管,它们分别连接在一个8-bit 数字I/O输出板的bit-3和bit-7 ,但是不能影响端口其他六位的值。没有任何POSIX接口能让程序员实现这样的功能。在实际中,常用三种方法将硬件和这个设备的接口映射起来。一种是修改 write() 函数的参数,如将原来表示写入字节数的第三个参数改为表示写哪个端口。修改标准的API定义的参数后,就没有了驱动程序以及调用它的代码的与硬件无关的特性,因为不能保证不同的I
47、/O设备驱动程序都以同样的格式定义参数。如我们想要定义一个8端口的I/O单板的端口4的bit 3和bit 7,怎么办?对于这块单板就需要采用不同的参数定义。第二种方法是使用 ioctl()。请求和值作为参数输入。但不幸的是,没有请求的标准,每个设备都可以自由选择自己支持的一套请求。一个关于这个问题的例子就是设置串口波特率为9600bps。不同的设备驱动程序使用不同的位图请求结构来实现这个功能。从而,本来应该具有相同硬件操作接口的设备却不兼容了。这样,使用这些设备的应用程序就依赖于设备特性,在另一种配置环境下就没用了。而且, 和 read()和write()操作不同,ioctl()主要是在初始化
48、时使用,因为 ioctl()通常首要的一点是决定有什么请求, 并为该请求转换适当形式的参数。第三种,一个常用的方法是使用 mmap()来映射设备的寄存器。这种方法允许程序员直接访问设备寄存器。虽然这种方法提供了最好的性能,它损害了使用设备驱动来为设备建立一个与硬件无关的操作接口的目的。用这种方法来写代码是不可移植的,通常难以维护,并且不能在其他配置环境下发挥作用。另外一个可选择的方法是把设备驱动程序为封装一个它自己的线程。设备数据通过共享存储器来转发(不是象错误#16 使用消息传送作为进程间的主要通信方式 中讨论的那样通过消息传送)。设备驱动程序就成为了一个可以根据设备存在与否或需要与否来执行
49、的独立进程。如【4】中讨论的那样,这个方法已经被证明能够开发出可移植的设备驱动程序。21#11 错误检测和处理是在事后进行,并且是在尝试和错误中实现错误检测和处理很少在软件设计阶段以一种很有意义的方式具体体现。更多时候,软件设计主要关注正常操作,任何异常和处理都是程序员在出现错误后补上去的。程序员可能 (1)到处加入错误检测,很多没有必要的地方都加了以至于它的存在影响了性能和时序;(2)除非是测试时发现了问题后加入有限的代码,否则没有任何错误处理代码。上述两种情况都是没有对错误处理进行设计,它的维护将是一个恶梦。相反,错误检测应该系统设计中作为另一种状态具体体现。因此,如果应用情况是一个有限状态机,异常情况可以看作是一个引起动作和状态迁移的新状态输入。具体实现的最佳方法仍然是学院中的一个研究课题。22#10 没有分析存储器空间大多数嵌入式系统的存储空间是有限的。但是绝大多数程序员并不清楚他们设计中的存储空间的使用情况。当被问起某段程序或者某个结构占用了多少存储空间时,就算说错数量级也不是少见的事情。在微控制器和DSP中,访问ROM ,内部RAM,或是外部 RAM的性能是明显不同的。综合分析存储器和性能,并将最常用的代码和数据段放入最快的存储器中有助于最有效地使用性能最好的存储器。带CACHE的处理器也能提高性能。在