详情请进入 湖南阳光电子学校 已关注:人 咨询电话:13807313137 微信号:yp941688, yp94168
嵌入式是软件设计领域的一个分支,它自身的诸多特点决定了系统架构师的选择,同时它的一些问题又具有相当的通用性,可以推广到其他的领域。
提起嵌入式软件设计,传统的印象是单片机,汇编,高度依赖硬件。传统的嵌入式软件开发者往往只关注实现功能本身,而忽视诸如代码复用,数据和界面分离,可测试性等因素。从而导致嵌入式软件的质量高度依赖开发者的水平,成败系之一身。随着嵌入式软硬件的飞速发展,今天的嵌入式系统在功能,规模和复杂度各方面都有了极大的提升。比如,Marvell公司的PXA3xx系列的 高主频已经达到800Mhz,内建USB,WIFI,2D图形加速,32位DDR内存。在硬件上,今天的嵌入式系统已经达到甚至超过了数年前的PC平台。在软件方面,完善的操作系统已经成熟,比如Symbian, Linux, WinCE。基于完善的操作系统,诸如字处理,图像,视频,音频,游戏,网页浏览等各种应用程序层出不穷,其功能性和复杂度比诸PC软件不遑多让。原来多选用专用硬件和专用系统的一些商业设备公司也开始转换思路,以出色而廉价的硬件和完善的操作系统为基础,用软件的方式代替以前使用专有硬件实现的功能,从而实现更低的成本和更高的可变更,可维护性。
2.决定架构的因素和架构的影响架构不是一个孤立的技术的产物,它受多方面因素的影响。同时,一个架构又对软件开发的诸多方面造成影响。
下面举一个具体的例子。
摩托车的发动机在出厂前必须通过一系列的测试。在流水线上,发动机被送到每个工位上,由工人进行诸如转速,噪音,振动等方面的测试。要求实现一个嵌入式设备,具备以下基本功能:
安装在工位上,工人上班前开启并登录。
通过传感器自动采集测试数据,并显示在屏幕上。
记录所有的测试结果,并提供统计功能。比如次品率。
如果你是这个设备的架构师,哪些问题是在设计架构的时候应该关注的呢?
2.1. 常见的误解 2.1.1. 小型的系统不需要架构有相当多的嵌入式系统规模都较小,一般是为了某些特定的目的而设计的。受工程师认识,客户规模和项目进度的影响,经常不做任何架构设计,直接以实现功能为目标进行编码。这种行为表面上看满足了进度,成本,功能各方面的需求,但是从长远来看,在扩展和维护上付出的成本,要远远高于 初节约的成本。如果系统的 初开发者继续留在组织内并负责这个项目,那么可能一切都会正常,一旦他离开,后续者因为对系统细节的理解不足,就可能引入更多的错误。要注意,嵌入式系统的变更成本要远远高于一般的软件系统。好的软件架构,可以从宏观和微观的不同层次上描述系统,并将各个部分隔离,从而使新特性的添加和后续维护变得相对简单。
举一个城铁刷卡机的例子,这个例子在前面的课程中出现过。简单的城铁刷卡机只需要实现如下功能:
一个While循环足以实现这个系统,直接就可以开始编码调试。但是从一个架构师的角度,这里有没有值得抽象和剥离的部分呢?
计费系统。计费系统是必须抽象的,比如从单次计费到按里程计费。
传感器系统。传感器括磁卡感应器,投币器等。设备可能更换。
故障处理和恢复。考虑到较高的可靠性和较短的故障恢复时间,这部分有必要单独设计。
未来很可能出现的需求变更:
操作界面。是否需要抽象出专门的Model来?以备将来实现View。
数据统计。是否需要引入关系型数据库?
如果直接以上面的流程图编码,当出现变更后,有多少代码可以复用?
不过,也不要因此产生过度的设计。架构应当立足满足当前需求,并适当的考虑重用和变更。
2.1.2. 敏捷开发不需要架构极限编程,敏捷开发的出现使一些人误以为软件开发无需再做架构了。这是一个很大的误解。敏捷开发是在传统瀑布式开发流程出现明显弊端后提出的解决方案,所以它必然有一个更高的起点和对开发更严格的要求。而不是倒退到石器时代。事实上,架构是敏捷开发的一部分,只不过在形式上,敏捷开发推荐使用更高效,简单的方式来做设计。比如画在白板上然后用数码相机拍下的UML图;用用户故事代替用户用例等。测试驱动的敏捷开发更是强迫工程师在写实际代码前设计好组件的功能和接口,而不是直接开始写代码。敏捷开发的一些特征:
针对比传统开发流程更大的系统
承认变化,迭代架构
简洁而不混乱
强调测试和重构
2. 嵌入式环境下软件设计的特点要谈嵌入式的软件架构,首先必须了解嵌入式软件设计的特点。
2.1. 和硬件密切相关嵌入式软件普遍对硬件有着相当的依赖性。这体现在几个方面:
一些功能只能通过硬件实现,软件操作硬件,驱动硬件。
硬件的差异/变更会对软件产生重大影响。
没有硬件或者硬件不完善时,软件无法运行或无法完整运行。
这些特点导致几方面的后果:
软件工程师对硬件的理解和熟练程度会很大程度的决定软件的性能/稳定性等非功能性指标,而这部分一向是相对复杂的,需要资深的工程师才能保证质量。
软件对硬件设计高度依赖,不能保持相对稳定,可维护性和可重用性差
软件不能离开硬件单独测试和验证,往往需要和硬件验证同步进行,造成进度前松后紧,错误定位范围扩大。
针对这些问题,有几方面的解决思路:
用软件实现硬件功能。选用更强大的处理器,用软件来实现部分硬件功能,不仅可以降低对硬件的依赖,在响应变化,避免对特定型号和厂商的依赖方面都很有好处。这在一些行业里已经成为了趋势。在PC平台也经历了这样的过程,比如早期的汉卡。
将对硬件的依赖独立成硬件抽象层,尽可能使软件的其他部分硬件无关,并可以脱离硬件运行。一方面将硬件变更甚至换件的风险控制在有限的范围内,另一方面提高软件部分的可测试性。
2.2. 稳定性要求高大部分嵌入式软件都对程序的长期稳定运行有较高的要求。比如手机经常几个月开机,通讯设备则要求24*7正常运行,即使是通讯上的测试设备也要求至少正常运行8小时。为了稳定性的目标,有一些比较常用的设计手段:
将不同的任务分布在独立的进程中。良好的模块化设计是关键
Watch Dog, Heart beat,重新启动失效的进程。
完善而统一的日志系统以快速定位问题。嵌入式设备一般缺乏有力的调试器,日志系统尤其重要。
将错误孤立在 小的范围内,避免错误的扩散和连锁反应。核心代码要经过充分的验证,对非核心代码,可以在监控或者沙盒中运行,避免其破坏整个系统。
举例,Symbian上的GPRS访问受不同硬件和操作系统版本影响,功能不是非常稳定。其中有一个版本上当关闭GPRS连接时一定会崩溃,而且属于known issue。将GPRS连接,HTTP协议处理,文件下载等操作独立到一个进程中,虽然每次操作完毕该进程都会崩溃,对用户却没有影响。
双备份这样的手段较少采用
2.3. 内存不足虽然当今的嵌入式系统的内存比之以K计数的时代已经有了很大的提高,但是随着软件规模的增长,内存不足的问题依然时时困扰着系统架构师。有一些原则,架构师在进行设计决策的时候可以参考:
2.3.1. 虚拟内存技术有一些嵌入式设备需要处理巨大的数据量,而这些数据不可能全部装入内存中。一些嵌入式操作系统不提供虚拟内存技术,比如WinCE4.2每个程序 多只能使用32M内存。对这样的应用,架构师应该特别设计自己的虚拟内存技术。所谓的虚拟内存技术的核心是,将暂时不太可能使用的数据移出内存。这涉及到一些技术点:
引用计数,正在使用的数据不能移出。
使用预测,预测下一个阶段某个数据的使用可能性。基于预测移出数据或者提前装入数据。
占位数据/对象。
高速缓存。在复杂数据结果下缓存高频率使用的数据,直接访问。
快速的持久化和装载。
下图是一个全国电信机房管理系统的界面示意图:
每个节点下都有大量的数据需要装载,可以使用上述技术将内存占用降到 低。
2.3.2. 两段式构造在内存有限的系统里,对象构造失败是必须要处理的问题,失败的原因中 常见的则是内存不足(实际上这也是对PC平台的要求,但是在实际中往往忽略,因为内存实在便宜)。两段式构造就是一种常用而有效的设计。举例来说:
CMySimpleClass:当创建CMyCompoundClass的时候会发生什么呢?
CMyCompoundClass* myCompoundClass = new CMyCompoundClass;
为CMyCompoundClass的对象分配内存
调用CMyCompoundClass对象的构造函数
在构造函数中创建一个CMySimpleClass的实例
构造函数结束返回
一切看起来都很简单,但是如果第三步创建CMySimpleClass对象的时候发生内存不足的错误怎么办呢?构造函数无法返回任何错误信息以提示调用者构造没有成功。调用者于是获得了一个指向CMyCompoundClass的指针,但是这个对象并没有构造完整。
如果在构造函数中抛出异常会怎么样呢?这是个著名的噩梦,因为析构函数不会被调用,在创建CMySimpleClass对象之前如果分配了资源就会泄露。关于在构造函数中抛出异常可以单讲一个小时,但是有一个建议是:尽量避免在构造函数中抛出异常。
所以,使用两段式构造法是一个更好的选择。简单的说,就是在构造函数避免任何可能产生错误的动作,比如分配内存,而把这些动作放在构造完成之后,调用另一个函数。比如:
AddressBook* book = new AddressBook()这样可以保证当Construct不成功的时候释放已经分配的资源。
在 重要的手机操作系统Symbian上,二段式构造法普遍使用。
2.3.3. 内存分配器不同的系统有着不同的内存分配的特点。有些要求分配很多小内存,有的则需要经常增长已经分配的内存。一个好的内存分配器对嵌入式的软件的性能有时具有重大的意义。应该在系统设计时保证整个系统使用统一的内存分配器,并且可以随时更换。
2.3.4. 内存泄漏内存泄漏对嵌入式系统有限的内存是非常严重的。通过使用自己的内存分配器,可以很容易的跟踪内存的分配释放情况,从而检测出内存泄漏的情况。
2.4. 处理器能力有限,性能要求高这里不讨论实时系统,那是一块很大的专业话题。对一般的嵌入式系统而言,由于处理器能力有限,要特别注意性能的问题。一些很好的架构设计由于不能满足性能要求, 终导致整个项目的失败。
2.4.1. 抵御新技术的诱惑架构师必须明白,新技术常常意味着复杂和更低的性能。即使这不是绝对的,由于嵌入式系统硬件性能所限,弹性较低。一旦发现新技术有和当初设想不同之处,就更难通过修改来适应。比如GWT技术。这是Google推出的Ajax开发工具,它可以让程序员像开发一个桌面应用程序一样开发Web的Ajax程序。这使得在嵌入式系统上用一套代码实现远程和本地操作界面成为了很容易的一件事。但是在嵌入式设备上运行B-S结构的应用,性能上是一个很大的挑战。同时,浏览器兼容方面的问题也很严重,GWT目前的版本还不够完善。
事实证明,嵌入式的远程控制方案还是要采用Activex,VNC或者其他的方案。
2.4.2. 不要有太多的层次分层结构有利于清晰的划分系统职责,实现系统的解耦,但是每多一个层次,就意味着性能的一次损失。尤其是当层和层之间需要传递大量数据的时候。对嵌入式系统而言,在采用分层结构时要控制层次数量,并且尽量不要传递大量数据,尤其是在不同进程的层次之间。如果一定要传递数据,要避免大量的数据格式转换,如XML到二进制,C++结构到Python结构。
嵌入式系统能力有限,一定要将有限的能力用在系统的核心功能上。
2.5. 存储设备易损坏,速度较慢受体积和成本的限制,大部分的嵌入式设备使用诸如Compact Flash, SD, mini SD, MMC等作为存储设备。这些设备虽然有着不担心机械运动损坏的优点,但是其本身的使用寿命都比较短暂。比如,CF卡一般只能写100万次。而SD更短,只有10万次。对于像数码相机这样的应用,也许是足够的。但是对于需要频繁擦写磁盘的应用,比如历史数据库,磁盘的损坏问题会很快显现。比如有一个应用式每天向CF卡上写一个16M的文件,文件系统是FAT16, 每簇大小是2K,那么写完这个16M的文件,分区表需要写8192次,于是一个100万次寿命的CF实际能够工作的时间是1000000/8192 = 122天。而损坏的时候,CF卡的其他绝大部分地方的使用次数不过万分之一。
除了因为静态的文件分区表等区块被频繁的读写而提前损坏,一些嵌入式设备还要面对直接断电的挑战,这会在存储设备上产生不完整的数据。
2.5.1. 损耗均衡损耗均衡的基本思路是平均地使用存储器上的各个区块。需要维护一张存储器区块使用情况的表,这个表括区块的偏移位置,当前是否可用,以及已经擦写地次数。当有新的擦写请求的时候,根据以下原则选择区块:
尽量连续
擦写次数 少
即使是更新已经存在的数据,也会使用以上原则分配新的区块。同样,这张表的存放位置也不能是固定不变的,否则这张表所占据的区块就会 先损坏。当要更新这张表的时候,同样要使用以上算法分配区块。
如果存储器上有大量的静态数据,那么上述算法就只能针对剩下的空间生效,这种情况下还要实现对这些静态数据的搬运的算法。但是这种算法会降低写操作的性能,也增加了算法的复杂度。一般都只使用动态均衡算法。
目前比较成熟的损耗均衡的文件系统有JFFS2, 和 YAFFS。也有另一种思路就是在FAT16等传统文件系统上实现损耗均衡,只要事先分配一块足够大的文件,在文件内部实现损耗均衡算法。不过必须修改FAT16的代码,关闭对 后修改时间的更新。
现在的CF卡和SD卡有的已经在内部实现了损耗均衡,这种情况下就不需要软件实现了。
2.5.2. 错误恢复如果在向存储器写数据的时候发生断电或者被拔出,那么所写的区域的数据就处于未知的状态。在一些应用中,这会导致不完整的文件,而在另一些应用中,则会导致系统失败。所以对这类错误的恢复也是嵌入式软件设计必须考虑的。常用的思路有两种:
日志型的文件系统
这种文件系统并不是直接存储数据,而是一条条的日志,所以当发生断电的时候,总可以恢复到之前的状态。这类文件系统的代表如ext3。
双备份
双备份的思路更简单,所有的数据都写两份。每次交替使用。文件分区表也必须是双备份的。假设有数据块A,A1是他的备份块,在初始时刻和A的内容是一致的。在分区表中,F指向数据块A,F1是他的备份块。当修改文件时,首先修改数据块A1的内容,如果此时断电,A1的内容错误,但因为F指向的是完好的A,所以数据没有损坏。如果A1修改成功,则修改F1的内容,如果此时断电,因为F是完好的,所以依然没有问题。
现在的Flash设备,有的已经内置错误检测和错误校正技术,可以保证在断电时数据的完整。还有的括自动的动态/静态损耗均衡算法和坏块处理,完全无须上层软件额外对待,可以当作硬盘使用。所以,硬件越发达,软件就会越可靠,技术不断的进步,将让我们可以把更多的精力投入到软件功能的本身,这是发展的趋势。
2.6. 故障成本高昂
嵌入式产品都是软硬件一起销售的给用户的,所以这带来了一个纯软件所不具备的问题,那就是当产品发生故障时,如果需要返厂才能修复,则成本就很高。嵌入式设备常见有以下的几类故障:
a) 数据故障。由于某些原因导致数据不能读出或者不一致。比如断电引起的数据库错误。
b) 软件故障。软件本身的缺陷,需要通过发布补丁程序或者新版本的软件修正。
c) 系统故障。比如用户下载了错误的系统内核,导致系统无法启动。
d) 硬件故障。这种故障只有返厂,不属于我们的讨论范围。
针对前三类故障,要尽可能保证客户自己,或者现场技术人员就可以解决。从架构的角度考虑,如下原则可以参考:
a) 使用具备错误恢复能力的数据管理设计。当数据发生错误时,用户可以接受的处理依次是:
i. 错误被纠正,所有数据有效
ii. 错误发生时的数据(可能不完整)丢失,之前的数据有效。
iii. 所有数据丢失
iv. 数据引擎崩溃无法继续工作
一般而言,满足第二个条件即可。(日志,事务,备份,错误识别)
b) 将应用程序和系统分离。应用程序应该放置在可插拔的Flash卡上,可以通过读卡器进行文件复制升级。非必要的情况不要使用专用应用软件来升级应用程序。
c) 要有“安全模式”。即当主系统被损坏后,设备依然可以启动,重新升级系统。常见的uboot可以保证这一点,在系统损坏后,可以进入uboot通过tftp重新升级。
3. 软件框架在桌面系统和网络系统上,框架是普遍应用的,比如著名的ACE, MFC, Ruby On Rails等。而在嵌入式系统中,框架则是很少使用的。究其原因,大概是认为嵌入式系统简单,没有重复性,过于注重功能的实现和性能的优化。在前言中我们已经提到,现在的嵌入式发展趋势是向着复杂化,大型化,系列化发展的。所以,在嵌入式下设计软件框架也是很有必要,也很有价值的。
3.1. 嵌入式软件架构面临的问题前面我们讲到,嵌入式系统软件架构所面临的一些问题,其中很重要的一点是,对硬件的依赖和硬件相关软件的复杂性。还括嵌入式软件在稳定性和内存占用等方面的苛刻要求。如果团队中的每个人都是这些方面的话,也许有可能开发出高质量的软件,但事实是一个团队中可能只有一两个资深人员,其他大部分都是初级工程师。人人都去和硬件打交道,都负责稳定性,性能等等指标的话,是很难保证 终产品质量的。如果组件团队时都是精通硬件等底层技术的人才,又很难设计出在可用性,扩展性等方面出色的软件。术业有专攻,架构师的选择决定着团队的组成方式。
同时,嵌入式软件开发虽然复杂,但是也存在大量的重用的可能性。如何重用,又如何应对将来的变更?
所以,如何将复杂性对大多数人屏蔽,如何将关注点分离,如何保证系统的关键非功能指标,是嵌入式软件架构设计师应该解决的问题。一种可能的解决方案就是软件框架。
3.2. 什么是框架框架是在一个给定的问题领域内,为了重用和应对未来需求变化而设计的软件半成品。框架强调对特定领域的抽象,含大量的专业领域知识,以缩短软件的开发周期,提高软件质量为目的。使用框架的二次开发者通过重写子类或组装对象的方式来实现特殊的功能。
3.2.1. 软件复用的层次复用是在我们经常谈到的话题,“不要重复发明轮子”也是耳熟能详的戒条。不过对于复用的理解实际上是有很多个层次的。
基础的复用是复制粘贴。某个功能以前曾经实现过,再次需要的时候就复制过来,修改一下就可以使用。经验丰富的程序员一般都会有自己的程序库,这样他们实现的时候就会比新的程序员快。复制粘贴的缺点是代码没有经过抽象,往往并不完全的适用,所以需要进行修改,经过多次复用后,代码将会变得混乱,难以理解。很多公司的产品都有这个问题,一个产品的代码从另一个产品复制而来,修改一下就用,有时候甚至类名变量名都不改。按照“只有为复用设计的代码才能真正复用”的标准,这称不上是复用,或者说是低水平的复用。
更高级的复用是则是库。这种功能需要对经常使用的功能进行抽象,提取出其中恒定不变的部分,以库的形式提供给二次开发程序员使用。因为设计库的时候不知道二次开发者会如何使用,所以对设计者有着很高的要求。这是使用 广泛的一种复用,比如标准C库,STL库。现在非常流行的Python语言的重要优势之一就是其库支持非常广泛,相反C++一直缺少一个强大统一的库支持,成为短板。在公司内部的开发中总结常用功能并开发成库是很有价值的,缺点是对库的升级会影响到很多的产品,必须慎之又慎。
框架是另一种复用。和库一样,框架也是对系统中不变的部分进行抽象并加以实现,由二次开发者实现其他变化的部分。典型的框架和库的 大的区别是,库是静态的,由二次开发者调用的;框架是活着的,它是主控者,二次开发者的代码必须符合框架的设计,由框架决定在何时调用。
举个例子,一个网络应用总是要涉及到连接的建立,数据收发和连接的关闭。以库的形式提供是这样的:
conn = connect(host,port);框架则是这样的:
class mycomm:class connect框架会在“适当”的时机创建mycomm对象,并查询host和port,然后建立连接。在连接建立后,调用onconnected()接口,给二次开发者提供进行处理的机会。当数据到达的时候调用ondataarrived接口让二次开发者处理。这是好莱坞原则,“不要来找我们,我们会去找你”。
当然,一个完整的框架通常也要提供各种库供二次开发者使用。比如MFC提供了很多的库,如CString, 但本质上它是一个框架。比如实现一个对话框的OnInitDialog接口,就是由框架规定的。
3.2.2. 针对高度特定领域的抽象和库比较起来,框架是更针对特定领域的抽象。库,比如C库,是面向所有的应用的。而框架相对来说则要狭窄的多。比如MFC提供的框架只适合于Windows平台的桌面应用程序开发,ACE则是针对网络应用开发的框架,Ruby On Rails是为快速开发web站点设计的。
越是针对特定的领域,抽象就可以做的越强,二次开发就可以越简单,因为共性的东西越多。比如我们上面谈到嵌入式系统软件开发的诸多特点,这就是特定领域的共性,就属于可以抽象的部分。具体到实际的嵌入式应用,又会有更多的共性可以抽象。
框架的设计目的是总结特定领域的共性,以框架的方式实现,并规定二次开发者的实现方式,从而简化开发。相应的,针对一个领域开发的框架就不能服务于另一个领域。对企业而言,框架是一种极好的积累知识,降低成本的技术手段。
3.2.3. 解除耦合和应对变化框架设计的一个重要目的就是应对变化。应对变化的本质就是解耦。从架构师的角度看,解耦可以分为三种:
逻辑解耦。逻辑解耦是将逻辑上不同的模块抽象并分离处理。如数据和界面的解耦。这也是我们 常做的解耦。
知识解耦。知识解耦是通过设计让掌握不同知识的人仅仅通过接口工作。典型的如测试工程师所掌握的专业知识和开发工程师所掌握的程序设计和实现的知识。传统的测试脚本通常是将这二者合二为一的。所以要求测试工程师同时具备编程的能力。通过适当的方式,可以让测试工程师以 简单的方式实现他的测试用例,而开发人员编写传统的程序代码来执行这些用例。
变与不变的解耦。这是框架的重要特征。框架通过对领域知识的分析,将共性,也就是不变的内容固定下来,而将可能发生变化的部分交给二次开发者实现。
3.2.4. 框架可以实现和规定非功能性需求非功能性需求是指如性能,可靠性,可测试性,可移植性等。这些特性可以通过框架来实现。以下我们一一举例。
性能。对性能的优化 忌讳的就是普遍优化。系统的性能往往取决于一些特定的点。比如在嵌入式系统中,对存储设备的访问是比较慢的。如果开发者不注意这方面的问题,频繁的读写存储设备,就会造成性能下降。如果对存储设备的读写由框架设计,二次开发者只作为数据的提供和处理者,那么就可以在框架中对读写的频率进行调节,从而达到优化性能的目的。由于框架都是单独开发的,完成后供广泛使用,所以就有条件对关键的性能点进行充分的优化。
可靠性。以上面的网络通讯程序为例,由于框架负责了连接的创建和管理,也处理了各种可能的网络错误,具体的实现者无须了解这方面的知识,也无须实现这方面错误处理的代码,就可以保证整个系统在网络通讯方面的可靠性。以框架的方式设计在可靠性方面的 大优势就是:二次开发的代码是在框架的掌控之内运行的。一方面框架可以将容易出错的部分实现,另一方面对二次开发的代码产生的错误也可以捕获和处理。而库则不能代替使用者处理错误。
可测试性。可测试性是软件架构需要考虑的一个重要方面。下面的章节会讲到,软件的可测试性是由优良的设计来保证的。一方面,由于框架规定了二次开发的接口,所以可以迫使二次开发者开发出便于进行单元测试的代码。另一方面,框架也可以在系统测试的层面上提供易于实现自动化测试和回归测试的设计,例如统一提供的TL1接口。
可移植性。如果软件的可移植性是软件设计的目标,框架设计者可以在设计阶段来保证这一点。一种方式是通过跨平台的库来屏蔽系统差异,另一种可能的方式更加极端,基于框架的二次开发可以是脚本化的。组态软件是这方面的一个例子,在PC上组态的工程,也可以在嵌入式设备上运行。
3.3. 一个框架设计的实例 3.3.1. 基本架构 3.3.2. 功能特点上面是一个产品系列的架构图,其特点是硬件部分是模块化的,可以随时插拔。不同的硬件应用于不同的通讯测试场合。比如光通讯测试,xDSL测试,Cable Modem测试等等。针对不同的硬件,需要开发不同的固件和软件。固件层的功能主要是通过USB接口接收来自软件的指令,并读写相应的硬件接口,再进行一些计算后,将结果返回给软件。软件运行在WinCE平台,除了提供一个触摸式的图形化界面外,还对外提供基于XML(SOAP)接口和TL1接口。为了实现自动化测试,还提供了基于Lua的脚本语言接口。整个产品系列有几十个不同的硬件模块,相应的需要开发几十套软件。这些软件虽然服务于不同的硬件,但是彼此之间有着高度的相似性。所以,选择先开发一个框架,再基于框架开发具体的模块软件成了 优的选择。
### 3.3.3. 分析
软件部分的结构分析如下:
系统分为软件,固件和硬件三大块。软件和固件运行在两块独立的板子上,有各自的处理器和操作系统。硬件则插在固件所在的板子上,是可以替换的。
软件和固件其实都是软件,下面我们分别分析。
软件软件的主要工作是提供各种用户界面。括本地图形化界面,SOAP访问界面,TL1访问界面。
整个软件部分分为五大部分:
通讯层
协议层
图形界面
SOAP服务器
TL1服务器
通讯层要屏蔽用户对具体通信介质和协议的了解,无论是USB还是socket,对上层都不产生影响。通讯层负责提供可靠的通讯服务和适当的错误处理。通过配置文件,用户可以改变所使用的通讯层。
协议层的目的是将数据进行编码和解码。编码的产生物是可以在通讯层发送的流,按照嵌入式软件的特点,我们选择二进制作为流的格式。解码的产生物是多种的,既有供界面使用的C Struct,也可以是XML数据,还可以是Lua的数据结构(tablegt)。如果需要,还可以产生JSON,TL1,Python数据,TCL数据等等。这一层在框架中是通过机器自动生成的,我们后面会讲到。
内存数据库,SOAP Server和TL1 Server都是协议层的用户。图形界面通过读写内存数据库和底层通讯。
图形界面是框架设计的重点之一,原因是这里工作量 大,重复而无聊的工作 多。
让我们分析一下在图形界面开发工作中 主要的事情是什么。
收集用户输入的数据和命令
将数据和命令发给底层
接收底层反馈
将数据显示在界面上
同时有一些库用来进一步简化开发:
这是一个简化的例子,但是很好的说明了框架的特点:
客户代码必须按照规定的接口实现
框架在适当的时候调用客户实现的接口
每个接口都被设计为只完成特定的单一功能
将各个步骤有机的串起来是框架的事,二次开发者不知道,也无须知道。
通常都要有附带的库。
固件固件的主要工作是接受来自软件的命令,驱动硬件工作;获取硬件的状态,进行一定的计算后返回给软件。早期的固件是很薄的一层,因为绝大部分工作是由硬件完成的,固件只起到一个中转通讯的作用。随着时代发展,现在的固件开始承担越来越多原来由硬件完成的工作。
整个固件部分分为五大部分:
硬件抽象层,提供对硬件的访问接口
互相独立的任务群
任务/消息派发器
协议层
通讯层
针对不同的设备,工作量集中在硬件抽象层和任务群上。硬件抽象层是以库的形式提供的,由对硬件 熟悉,经验 丰富的工程师来实现。任务群则由一系列的任务组成,他们分别代表不同的业务应用。比如测量误码率。这部分由相对经验较少的工程师来实现,他们的主要工作是实现规定的接口,按照标准化文档定义的方式实现算法。
任务定义了如下接口,由具体开发者来实现:
OnInit();框架的代码流程如下:(伪代码)
CTask* task = new CBertTask();这样,具体任务的实现者所关注的 重要的事情就是实现这几个接口。其他如硬件的初始化,消息的收发,编码解码,结果的上报等等事情都由框架进行了处理。避免了每个工程师都必须处理从上到下的所有方面。并且这样的任务代码还有很高的重用性,比如是在以太网上还是在Cable Modem上实现PING的算法都是一样的。
3.3.4. 实际效果在实际项目中,框架大大降低了开发难度。对软件部分尤其明显,由实习生即可完成高质量的界面开发,开发周期缩短50%以上。产品质量大大提升。对固件部分的贡献在于降低了对精通底层硬件的工程师的需要,一般的工程师熟知测量算法即可。同时,框架的存在保证了性能,稳定和可测试性等要素。
3.4. 框架设计中的常用模式 3.4.1. 模板方法模式模板方法模式是框架中 常用的设计模式。其根本的思路是将算法由框架固定,而将算法中具体的操作交给二次开发者实现。例如一个设备初始化的逻辑,框架代码如下:
TBool CBaseDevice::Init()DownloadFPGA和InitKeyPad都是CBaseDevice定义的虚函数,二次开发者创建一个继承于CBaseDevice的子类,具体来实现这两个接口。框架定义了调用的次序和错误的处理方式,二次开发者无须关心,也无权决定。
3.4.2. 创建型模式由于框架通常都涉及到各种不同子类对象的创建,创建型模式是经常使用的。例如一个绘图软件的框架,有一个基类定义了图形对象的接口,基于它可以派生出椭圆,矩形,直线各种子类。当用户绘制一个图形时,框架就要实例化该子类。这时候可以用工厂方法,原型方法等等。
class CDrawObj消息订阅模式是 常用的分离数据和界面的方式。界面开发者只需要注册需要的数据,当数据变化时框架就会将数据“推”到界面。界面开发者可以无须关注数据的来源和内部组织形式。
消息订阅模式 常见的问题是同步模式下如何处理重入和超时。作为框架设计者,一定要考虑好这个问题。所谓重入,是二次开发者在消息的回调函数中执行订阅/取消订阅的操作,这会破坏消息订阅的机制。所谓超时是指二次开发者的消息回调函数处理时间过长,导致其他消息无法响应。 简单的办法是使用异步模式,让订阅者和数据发布者在独立进程/线程中运行。如果不具备此条件,则必须作为框架的重要约定,禁止二次开发者产生此类问题。
3.4.4. 装饰器模式装饰器模式赋予了框架在后期增加功能的能力。框架定义装饰器的抽象基类,而由具体的实现者实现,动态地添加到框架中。
举一个游戏中的例子,图形绘制引擎是一个独立的模块,比如可以绘制人物的静止,跑动等图像。如果策划决定在游戏中增加一种叫“隐身衣”的道具,要求穿着此道具的玩家在屏幕上显示的是若有若无的半透明图像。应该如何设计图像引擎来适应后期的游戏升级呢?
当隐身衣被装备后,就向图像引擎添加一个过滤器。这是个极度简化的例子,实际的游戏引擎要比这个复杂。装饰器模式还常见用于数据的前置和后置处理上。
3.5. 框架的缺点一个好的框架可以大大提高产品的开发效率和质量,但也有它的缺点。
框架一般都比较复杂,设计和实现一个好的框架需要相当的时间。所以,一般只有在框架可以被多次反复应用的时候适合,这时候,前提投入的成本会得到丰厚的回报。
框架规定了一系列的接口和规则,这虽然简化了二次开发工作,但同时也要求二次开发者必须记住很多规定,如果违反了这些规定,就不能正常工作。但是由于框架屏蔽了大量的领域细节,相对而言,其学习成本还是大大降低了的。
框架的升级对已有产品可能会造成严重的影响,导致需要完整的回归测试。对这个问题有两个办法。第一是对框架本身进行严格的测试,有必要建立完善的单元测试库,同时开发示例项目,用来测试框架的所有功能。第二则是使用静态链接,让已有产品不轻易跟随升级。当然,如果已有产品有较好的回归测试手段,就更好。
性能损失。由于框架对系统进行了抽象,增加了系统的复杂性。诸如多态这样的手段使用也会普遍的降低系统的性能。但是从整体上来看,框架可以保证系统的性能处于一个较高的水平。
4. 自动代码生成 4.1. 机器能做的事就不要让人来做懒惰是程序员的美德,更是架构师的美德。软件开发的过程就是人告诉机器如何做事的过程。如果一件事情机器自己就可以做,那就不要让人来做。因为机器不仅不知疲倦,而且绝不会犯错。我们的工作是让客户的工作自动化,多想一点,就能让我们自己的工作也部分自动化。极有耐心的程序员是好的,也是不好的。
经过良好设计的系统,往往会出现很多高度类似而且具有很强规律的代码。未经良好设计的系统则可能对同一类功能产生很多不同的实现。前面关于框架设计的部分已经证明了这一点。有时候,我们更进一步,分析出这些相似代码之中的规律,用格式化的数据来描述这些功能,而由机器来产生代码。
4.2. 举例 4.2.1. 消息的编码和解码上面关于框架的实例中,可以看到消息编解码的部分已经被独立出来,和其他部分没有耦合。加上他本身的特点,非常适合进一步将其“规则化”,用机器产生代码。
编码,就是把数据结构流化;解码反之。以编码为例,代码无非是这样的:(二进制协议)
.(编辑:泰顺电工培训学校)