购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.2 嵌入式设备开发流程

嵌入式设备开发包括硬件开发和软件开发等。嵌入式系统开发流程如图1-2所示。

本书不仅阐述软件方面的应用开发,对具体硬件开发也给出了设计方法,相关内容在后面的硬件开发章节进行讲解。

熟悉嵌入式软件开发流程是非常重要的。在整个开发过程中,首先要从整个工程的角度去把握,然后逐个细分。从软件工程的角度来说,嵌入式应用软件开发也有一定的生命周期,需要经过需求分析、系统设计/概要设计/详细设计、代码编写、调试、测试和维护等阶段。

与其他通用软件相比,嵌入式软件开发有许多独特之处。

(1)需求分析时,必须考虑硬件功能和性能影响,具体功能和性能往往取决于设备所选定的硬件。

(2)系统设计阶段,重点考虑的是按照减少耦合性的原则来划分各任务和设计相互访问的接口、设备所提供的总体功能和各子功能。

(3)概要设计阶段,涉及具体模块的划分和模块之间的接口调用。

(4)详细设计阶段,涉及各个函数(包括功能、输入/输出参数说明)、消息传递、类型结构的定义等,需要更详细的模块内分解。

(5)编写代码并调试,在调试时采用交叉调试方式,调试完成后固化到设备中。

(6)测试分为单元测试、集成测试和系统测试。单元测试的原则是要运行到每个分支,集成测试和系统测试分别要检测各模块和产品的功能。测试方法有白盒测试和黑盒测试。

(7)测试结束,系统稳定运行,后期的维护工作较少。

图1-2 嵌入式系统开发流程

下面主要介绍需求分析和设计阶段的步骤与原则。

1.需求分析和系统设计

(1)需求分析

对需求加以分析产生需求说明文档,需求说明过程至少要给出系统功能需求,它包括以下5个方面。

● 系统提供的功能及性能指标。

● 从整体设备的使用角度来分析系统的输入/输出,除了数据外还有信号。

● 系统运行时的外部接口需求,如远程控制或服务器等接口。

● 文件/数据库系统的安全。

● 需要考虑异常情况下处理的稳定性。

(2)系统设计

在嵌入式系统运行时,芯片的状态经常会变化,此时常用状态转换图表示这种转变。这就需要在设计状态图时,应对系统运行过程进行详细考虑,包括对异常、中断和定时器操作等应有相应的处理算法。通过提供人机操作接口,开发人员/测试员/操作员与设备之间相互通信。因此提供操作手册是必要的,可为用户提供使用该系统的操作步骤。

在确定了硬件后,选择硬件平台时主要考虑处理器的处理速度、内存空间的大小、设备提供的功能和子功能、对信号的处理是否正确等因素。

对于软件平台而言,首先确定使用何种操作系统,如是Linux还是VxWorks;其次要考虑对实时性支持的程度;网络管理能力、测试场景、网络设备或移动设备功能也是要考虑的因素。另外还要考虑包括使用何种开发环境、是否有软件模拟运行环境、设备驱动支持何种业务处理等。

不管选用何种软件/硬件平台,都要考虑成本因素,最主要的就是芯片的成本。

2.概要设计

在进行需求分析和明确系统功能后,就需要划分出具体的任务和模块了。因为在设计一个较为复杂的多任务设备系统时,进行合理的任务和模块划分对设备的运行效率、实时性、吞吐量和后期的扩展影响都极大。任务分解过多,就要不断地发生切换,而任务之间的通信量也会很大,同时还包括从底层不断获取数据以及中断处理等。这将增加系统的开销,影响系统的性能。若任务分解过少,会影响系统的吞吐量,易形成瓶颈,增加开发的复杂度。为了实现合理规划,可采用以下步骤和原则。

(1)进行上/下行数据流分析

首先,从系统的功能需求开始分析系统中进程间通信(Inter-Process Communication,IPC)的数据流,分析从接收这些数据到处理完成所耗费的时间,并要考虑存在数据流不畅或拥塞的异常情形。

(2)划分进程和子进程

要完成系统的所有功能,在收发内外通信的数据流后,需要划分进程,减少进程间的耦合性,增强独立性。在定义进程优先级时尤其要考虑进程的顺序性和响应的实时性。进程的划分方法如下。

① 运行某项功能所需时间

对时间有较高要求的功能任务要以高优先级运行,可设置成一个独立的高优先级任务,运行时优先被调度。

② 计算速度需求

有些任务需要进行大量数据计算和完成不具有时间紧迫性的功能或功能集合,这时可将其以较低优先级的任务运行。一个多进程的注册代码例子如下。

/*进程注册结构*/
typedef struct PAT
{
    char             used;        /*使用标志*/
    char             *name;       /*进程名*/
    PROCESS_ENTRY Entry;         /*进程入口地址*/
    char             attr;         /*任务的属性*/
    char             PriLevel;      /*进程优先级等级*/
    short       StackSize;   /*栈大小*/
    short       DataSize;   /*数据区大小*/
    short       AsynMsgQLen; /*异步消息接收队列长度*/
    short       SynMsgQLen; /*同步消息接收队列长度*/
}MGPACK PATStruc;
typedef void (* PROCESS_ENTRY) (void *,void *,void *);
{1,  "SIGN"   ,  SignProcess  ,0,150,65535 , 0   ,256,8},
{1,  "MUX"   ,  MuxProcess  ,0,151,65535 , 0   ,256,8},
{1,  "PORTSCAN",  PortScan   ,0,150,65535 , 0   ,256,8},
{1,  "IGMP"   ,  IgmpProcess  ,0,150,65535 , 0   ,256,8},
{1,  "LOG"   ,  LogProcess  ,0,150,65535 , 0   ,32,8},
{1,  "Ftp"    ,  FtpProcess  ,0,152,65535  , 0   ,32,8},
{1,  "RMON"  ,  RmonProcess ,0,150,65535  , 0   ,32,8},
{1,  "OMC"   ,  OmcProcess  ,0,170,4096  , 0   ,1024,8},
{1,  "SYS"   ,  Control   ,0,150,4096  , 0   ,1024,8},
{1,  "RECV"  ,  Receive   ,0,160,4096  , 0   ,1024,8},
{1,  "PLAT"   ,  Process_Plat  ,0,150,4096  , 1024 ,32,8},

对不同的进程用不同的优先级来控制运行顺序,因进程有独立的空间,所以进程间用事件或消息来实现同步目的。目标任务等待事件的发生,收到源任务发送的事件信号后被激活。

③ 功能内聚

完成功能紧密相关的进程可以组成一个大进程,减少函数之间进/出栈的调用,保证模块和进程实现功能的内聚。

④ 周期执行

有些任务需要周期执行,且状态经常变换,则可作为一个独立的短任务,或者设置定时器,到一定的时间间隔后激活。这样的操作不宜过于复杂。

⑤ 定义任务之间接口

当划分好任务模块后,就可以确定彼此的通信接口了。任务间的接口调用,设计时不能太复杂,要事先确定好接口格式。在嵌入式开发中,进程间或模块间的通信一般会采用一个数据结构或者字符串的形式,并定义对该数据结构的访问过程。有时需要防止可能在两个任务中并发执行,如两个任务均要访问同一个文件,在访问过程中就必须提供必要的同步和互斥条件,否则任务运行后可能会影响数据的一致性和正确性。

(3)函数划分说明

详细设计主要体现在函数功能的划分和实现。具体模块或进程的功能以函数方式完成,对函数的定义说明一定要清晰,参数传递要明确。函数的定义有以下原则。

● 调用次数较多的函数不必写太长太复杂,可采用宏定义方式。

● 函数内部使用的逻辑不必太复杂,保证可读性强。

● 尽可能少用全局变量。

● 函数应有可重入性,调用或被调用的层次不宜太深。

● 对异常情况或出错情形最好提供异常处理过程。

对于函数内部实现的定义,需要给出流程图,便于后期维护,函数之间的参数传递要一致,不能错位。HTTP报文隧道处理流程如图1-3所示。

图1-3 HTTP报文隧道处理流程

在实现该处理过程时,函数的划分要根据此流程来进行,需要完成从起始收包到最终转发的整个流程规划,不合法则丢弃。数据传递不能出现空指针的操作,要增加空指针的判断,否则就可能发生整个程序异常退出的错误。

3.编写实现代码

嵌入式应用程序的生成包括以下3个阶段。

(1)编写源代码,包括头文件和实现文件。

(2)将源代码编译成目标文件模块。

(3)将目标模块和它所需的库文件连接起来,最后生成一个可执行程序。

由此可见,这个生成过程的关键集中在编译器和链接器上。编译器将源代码编译成特定目标处理器的目标代码,链接器将目标模块和支撑库链接成一个可执行程序。嵌入式系统要支持多种处理器,但不同的处理器对应的交叉编译器不完全相同。有些芯片厂商自行定制了编译器,当源码程序固化到设备后,设备就只能运行它编译出来的程序。

需要说明的是,代码编写过程中一定要注意降低处理过程的复杂性,尤其是底层开发,以获得尽可能快的响应,减少超时的发生概率,提高性能。

4.调试和运行

(1)调试

在编写完程序代码后,一般用GDB调试功能调试Linux程序,也可使用仿真软件进行调试。对于代码中的潜在错误,作者较多地使用 PCLint 工具检查。软件调试过程中需要制定运行条件,使每个细节或语句都运行到。

(2)运行

在调试完成后,就可以固化运行了。将系统镜像文件或二进制应用程序烧入设备的非易失性存储器(如只读存储器(Read-Only Memory, ROM)、Flash或通用串行总线(Universal Serial Bus, USB)中,在硬件环境下运行。

在固化运行时,初始化阶段要首先运行 boot 模块。boot 模块将在启动时作为系统程序的入口模块,完成对CPU环境的初始化。boot的具体实现和功能说明在后面的章节中阐述。其他应用将按照各代码模块的属性定位到相应的设备存储空间中,自动装载运行。

5.软件测试

软件测试在软件开发中非常重要,如果测试不完善,不可能推出好的设备。在项目管理过程中,每个模块或进程实现的每一个环节都要进行测试,保证系统在每个阶段都可以控制,还包括对测试场景和应用场景的模拟,因为软件测试中考虑的问题基本是项目管理中考虑的问题。一个较完整的软件测试流程如下。

(1)分析要测试的产品/项目,写出测试计划书。

(2)在开发的不同阶段设计测试用例。例如,根据需求分析写出系统测试用例,概要设计配备集成测试用例,详细设计完成设计单元测试用例,尤其要保证测试用例能覆盖关键性的测试需求。

(3)烧制软件版本,搭建测试环境,即调整不同设备的连接位置,执行测试用例。

(4)在测试过程中发现和提交软件/硬件问题,审核后分配给开发人员修正。

(5)撰写测试报告。

测试设计文档编写主要包括测试用例的编写和测试场景设计两方面,要包含运行环境、预定结果和既得结果、设备问题及原因、版本修改时间和结束时间等元素。

测试方法有以下8种。

(1)极值测试,输入程序中能处理的数值的最大数和最小数,或者为空。

(2)非法和异常测试,即输入的数据不正确,如应输入整数,但输入字符串。除数不能为0,强行输入0看能否激发异常。

(3)从开始收到一个数据,一直跟踪到最终处理结束,以确保流程及分支的正确性。

(4)多次测试函数间或任务间的通信接口,因为此处发生错误会影响整个系统运行。

(5)意外情况测试,设备运行时可能有意外,如不响应输入的数据,通信拥塞导致宕机。

(6)多设备连接测试,有些设备的软件系统运行或开发时依赖于下一跳服务器,让该服务器发生错误或者不回复,从中观察本系统受到的影响及运行效果。

(7)在系统运行宕机的地方,较为显著的是内核崩溃和段错误发生处要进行多次测试。

(8)不同系统的兼容性和移植性测试,有些程序在不同的处理器下运行速度有较大的差异,很有可能从中发现漏洞(bug)。

还有很多其他测试方法,关键是从一开始要遵循流程,这样可以总结发现的bug,有助于弥补缺陷。测试总体流程如图1-4所示,单元测试流程样例如图1-5所示。图1-4和图1-5具有普遍适用性。

图1-4 测试总体流程

图1-5 单元测试流程样例

在这个测试流程中,并不是所有的测试文档都要测试人员来完成,如单元测试文档就需要开发人员完成。图1-5的单元测试是模块设计和实现中必须要完成的。

因此,在进行单元测试时,需要结合详细设计文档、模块的实现代码、单元测试用例和测试报告,缺一不可。只有模块正常运行,才能支持更稳定的系统功能。

软件开发工作通常是与硬件紧密结合的,除此之外,读者还需要掌握一些硬件平台知识,相关信息可以在各个芯片厂商提供的资料说明文档中找到。 CeNOfriByomvNDidAMSnNmIfESIfZ0uGDROws2Pc+4G9fuFFuX6ycTQXA/V9sYwQ

点击中间区域
呼出菜单
上一章
目录
下一章
×