嵌入式架构师成长之路系列之架构设计

详见微信公众号,二进制人生。

目录:

  1. 嵌入式环境下软件设计的特点
  2. 设计目标
  3. 设计思路
  4. 多进程解耦

嵌入式环境下软件设计的特点

要谈嵌入式的软件架构,首先必须了解嵌入式软件设计的特点。下面的这一段摘自http://www.uml.org.cn/embeded/201906123.asp,个人觉得写的非常有道理,应该是出自一个至少有5年嵌入式软件开发经验的高级工程师之手,和大家分享下。

1 和硬件密切相关

嵌入式软件普遍对硬件有着相当的依赖性。这体现在几个方面:

1)一些功能只能通过硬件实现,软件操作硬件,驱动硬件。

2)硬件的差异/变更会对软件产生重大影响。

3)没有硬件或者硬件不完善时,软件无法运行或无法完整运行。

这些特点导致几方面的后果:

1. 软件工程师对硬件的理解和熟练程度会很大程度的决定软件的性能/稳定性等非功能性指标,而这部分一向是相对复杂的,需要资深的工程师才能保证质量。

10. 软件对硬件设计高度依赖,不能保持相对稳定,可维护性和可重用性差

11. 软件不能离开硬件单独测试和验证,往往需要和硬件验证同步进行,造成进度前松后紧,错误定位范围扩大。

针对这些问题,有几方面的解决思路:

  1. 用软件实现硬件功能。选用更强大的处理器,用软件来实现部分硬件功能,不仅可以降低对硬件的依赖,在响应变化,避免对特定型号和厂商的依赖方面都很有好处。这在一些行业里已经成为了趋势。

2. 将对硬件的依赖独立成硬件抽象层,尽可能使软件的其他部分硬件无关,并可以脱离硬件运行。一方面将硬件变更甚至换件的风险控制在有限的范围内,另一方面提高软件部分的可测试性。

2. 稳定性要求高

大部分嵌入式软件都对程序的长期稳定运行有较高的要求。比如手机经常几个月开机,通讯设备则要求24*7正常运行,即使是通讯上的测试设备也要求至少正常运行8小时。为了稳定性的目标,有一些比较常用的设计手段:

1. 将不同的任务分布在独立的进程中。良好的模块化设计是关键

2. Watch Dog, Heart beat,重新启动失效的进程。

3. 完善而统一的日志系统以快速定位问题。嵌入式设备一般缺乏有力的调试器,日志系统尤其重要。

4. 将错误孤立在最小的范围内,避免错误的扩散和连锁反应。核心代码要经过充分的验证,对非核心代码,可以在监控或者沙盒中运行,避免其破坏整个系统。

举例,Symbian上的GPRS访问受不同硬件和操作系统版本影响,功能不是非常稳定。其中有一个版本上当关闭GPRS连接时一定会崩溃,而且属于known issue。将GPRS连接,HTTP协议处理,文件下载等操作独立到一个进程中,虽然每次操作完毕该进程都会崩溃,对用户却没有影响。

5. 有条件的话可以对程序进行备份

设计目标

架构设计要做到以下几个点:

1、层次分明,结构清晰

2、尽可能的方便后续的功能扩展和移植。

3、实现最大限度的代码复用,也就是避免重复造轮子。

4、尽可能的达到高内聚低耦合。 

 

设计思路

用分层的思想对整个架构进行规划,一般采用3到5层,层数太多会导致无用代码增多,函数调用太深,效率下降,层次太少则增加代码耦合度。我们分层的目的是使得某一层的改动最多只对上一层造成影响,而不会影响上上层。上层也无需关心下层的实现,只需要调用

常用的分层:

硬件驱动层-->硬件适配层-->功能模块层-->业务逻辑层-->应用层

对应的软件架构图:

 

为了实现上述设计目标,有几点要求:

1.层与层之间最好不要跨层调用。

跨层调用就违背了分层设计的思想,这样做的坏处我举个例子:功能模块层跨过硬件适配层直接调用硬件驱动层的接口,某一天换了个不同厂家的芯片,驱动进行了重写,导致功能模块层直接调用的地方也要做相应的修改。

2.模块与模块尽可能各自独立,无依赖关系。

这里有一个特例,就是模块可以调用通用模块提供的api,除此之外尽量不要调用其他模块的接口。什么是通用模块,比如日志模块,其他模块可能也要打印日志,还有一些封装过后的基础机制,比如共享内存,套接字,内存分配等。

3.每一层都提供统一的接口供上层调用,模块的内外接口分明。

最常用的做法是将对外接口定义在单独的.c文件和.h头文件中,文件可以命名为模块名_API.c,模块名_API.h,接口可以命名成模块名_函数功能_API,内部接口和内部变量全部定义成staic,实现对外不可见。

这样子查看头文件就可以一目了然的知道该模块对外提供了哪些接口,在阅读代码时通过接口名字也快速可以该接口知道来自哪一个模块,提高代码可读性。

我们来对每一层做个说明:

硬件驱动层

硬件驱动层包含板载硬件资源正常运行所需的所有驱动程序并提供API给上层调用。

硬件适配层

这一层是我自己额外提出的,也可以理解为硬件驱动层的一部分。本来的话功能模块层直接调用硬件驱动层就可以了,硬件适配层的出现是为了应对多平台的情况,对多个平台提供的驱动接口进行再次封装,保持统一的对外接口。比如,每个平台的芯片操作io口的函数并不相同,硬件适配层可以将io读写抽象成:

Gpio_read(int group,int num,int *vaule)

Gpio_write(int group,int num,int value)

group表示GPIO组

num表示组内序号

Value是读到的或者是要写入的值。

这样不管底层如何改动,硬件适配层对上的接口都不会改动。

功能模块层

实现具体的功能模块,通过调用硬件适配层API实现相应功能,同时提供可调用的API给应用层。

建议在完成每个功能模块后,都输出相应的测试用例。单元测试是软件测试的最基本单位,是由开发人员执行以保证其所开发代码正确的过程。开发人员应该提交经过测试的代码。未经单元测试的代码在进入软件后,不仅发现问题后很难定位,而且通过系统测试是很难做到对代码分支的完全覆盖的。

业务逻辑层

这一层有时候可能和功能模块层合并在一起,并不是必须的。

应用层

将各个功能模块进行整合调用,完成整个产品的功能。

在目录结构上我习惯这样来组织整个工程

|--Module

|--模块1

|--模块2

|--object生成的临时文件,比如.o,.d文件

|--src .c,.cpp文件

|--inc头文件

|--lib生成的.so或者.a库

|--unit_test测试用例

多进程解耦

对于带操作系统的程序而言,还有一个方法可以实现解耦,那便是采用多进程的方式。一些独立的功能可以考虑拆分成独立的进程,拆分的依据就要按实际情况来了。多进程的方式除了可以实现程序解耦,还有利于项目的并行开发,分配任务和后续维护也可以按进程来划分。

 

讲到这里,我们再扩展的说下多进程的好处,下面是某位前辈的语录,已经找不到出处了,按他自己的介绍,从事嵌入式软件开发已经有8、9年了。

1.模块的解耦:很多开发人员维护开发的多线程模型项目应该都多少会存在下面的问题:跨模块间的直接调用,如果不相信,好,你的项目一定是分模块的吧,现在随机的删掉一个模块,build下看能build通过吗(只需要build不需要运行),我相信大部分情况下一定会遇到某个函数调用,某个全局变量找不到的情况,这种情况说明你的模块间存在强耦合了。

 

由于多线程天然的优势,地址空间的相互可见,导致直接调用十分容易,很多经验尚浅的工程师,很容易就写出直接调用的简单粗暴的接口,如果遇到个static接口的函数,图方便也会把static去掉,直接拿过来用了。这样整个工程随着功能不断的添加,模块间的交叉越来越多,耦合越高。其实我自己偷懒的时候也这样做过。

而我之所以推崇多进程的原因就是,多进程能从物理上隔绝了这种“方便”的通讯方式,导致在想实现一个模块交互时,会多思考下这个交互是必要的吗,如果是必要的,则会进一步思考接口定义是否简单明了(因为进程间的通讯相对会麻烦些,开发人员会本着能减少交互,明确接口的想法去仔细考虑接口,协议的定义,否则折腾的是自己了),这如同人生,如果一直顺风顺水,人们可能不会想太多,思考太多,而如果道路上有些坎坷,则会有另一种感悟吧。

所以我的想法是多进程的模型会逼迫你去更多的思考想程序的设计,物理上减少模块的耦合。

抽象通用组件,分离通用功能和业务逻辑功能:当把一个多线程模型修改为多进程模型的过程中,经常会发现有些接口代码重复的出现在多个进程模块中,因为之前接口函数是在一个进程空间,大家都可以直接调用的,比如接口A被模块a,b调用,模块a,b分离为两个独立的进程后,接口A需要在a,b中分别实现了,无需解释,重复代码这个在软件工程中是大忌,必须消除。做法也很简单,将这些被多个模块调用的接口分离处理做成通用模块,供其他模块调用,当你完成这部分工作后,你发现了什么,是不是剥离的接口,可以作为整个项目的通用组件存在了。

2方便定位问题:多线程模型中当又一个线程异常退出,会导致整个进程退出,当然通过一些crash信息,可以定位是哪个线程死掉。但如果这些线程模块是由多个小组、人员维护,当整个进程崩溃掉后,如何判断由哪个小组解决,会是一个大的问题。而且有时还会出现的现象是挂在一个线程,但其实是另外一个线程模块引起的(耦合的祸端),遇到这种情况,难免出现小组间的扯皮,推诿。(自信自私的工程师都认为我的代码没有问题)。

而如果采用多进程的模型,好吧,你的服务进程挂了,你自己找原因吧,没什么可争辩的了。

3方便性能测试:多线程种单个线程的资源占用不是很好查看(至少有些嵌入式系统没有完善的命令),当整个进程资源消耗很高时,如何判断定位时哪个模块线程的问题,同前边问题一样难以抉择。而如果是多进程的模型,谁的进程占了好多资源,谁就去查下吧,其实这个还是个颗粒度的问题。同样的系统,划分成多个进程,复杂度一定比只有一个进程的复杂度低的多,复杂度降低,也就更容易定位查找各种问题。

 

每一句讲的都很有道理,如果你不能理解,只能说明你的水平还不够。所谓的年少不知曲中意,再听已是曲中人。

 

各位看官,喜欢的话点个在看吧,让我有点动力写下去。

 

 

你可能感兴趣的:(专辑10,---,嵌入式linux界面开发Qt,专辑8,---,嵌入式linux,C基础)