在嵌入式软件开发过程中,花在测试和花在编码的时间比通常在3:1左右(实际上可能更多)。这个比例会随着工程师编程、测试水平的提高而不断下降,但无论如何,软件测试都是嵌入式软件开发中至关重要的部分。

多年前,一位工程师为了对嵌入式拥有更深层次理解的追求,曾发出这样的疑问:“我怎么才能知道并懂得我的系统到底在干些什么呢?”。同时代的嵌入式开发人员问得最多的问题大都围绕“我怎么才能使程序跑得更快”、“什么编译器最好”,这个问题虽然不同寻常,但却异乎成熟。今天就让我们一起了解10条在业界广为流传的嵌入式开发测试秘诀。

1.懂得使用工具

嵌入式系统通常对可靠性要求较高,一旦发生安全问题可能就会导致灾难性的后果,即使与安全无关也会带来严重的经济损失,对嵌入式系统及软件有着严格的测试、确认和验证要求。随着越来越多的领域的嵌入式设备开始被软件和微处理器控制,对日益复杂的嵌入式软件进行快速有效的测试显得愈加重要。

好的修车匠需要好工具,好的程序员应该能够熟练运用各种软件工具。不同的工具有不同的使用范围、功能。合适的工具可以让工程师看到系统在干些什么,它又占用什么资源、到底和外界哪些东西打交道。工程师不应该害怕加入测试工具或测试模块到代码需要的技巧或可能引入新的错误,光靠不断修改、重新编译代码来消除Bug是不够的;也不应该因习惯使用printf之类的简单测试手段而不进行新的学习和探索。下面是一些嵌入式常用的测试工具。

源码级调试器【Source-levelDebugger】:此类调试器一般提供单步或多步调试、断点设置、内存检测、变量查看等功能,是嵌入式调试最基本的调试方法。

简单实用的打印显示工具【printf】:printf及类似的打印显示工具估计是最灵活、最简单的调试工具。打印代码执行过程中的各种变量可以让工程师获知代码执行的情况,但printf对正常的代码执行干扰比较大(一般printf会占用CPU较长时间),需要慎重使用,最好设置打印开关来控制打印。

ICE或JTAG调试器【In- circuitEmulator】:ICE是用来仿真CPU核心的设备,可以在不干扰运算器的正常运行情况下,实时检测CPU内部工作情况,也能像桌面调试软件一样提供复杂的条件断点、先进的实时跟踪、性能分析和端口分析等功能。ICE一般都有一个较为特殊的CPU,被称为外合(bond-out)CPU,是一种被打开了封装且通过特殊的连接可以访问CPU内部信号的CPU,这些信号在CPU被封装时是没法被“看到”的。当和工作站上强大的调试软件联合使用时,ICE就能提供几乎最全面的调试功能。然而ICE同样有着昂贵、不能全速工作的缺点;同样,并不是所有的CPU都可以作为外合CPU的,从另一个角度说,这些外合CPU也不大可能及时被新出的CPU所更换。JTAG(JointTestActionGroup)最初开发目的是监测IC和电路连接,但其扩展了包括调试支持在内的用途。

ROM监视器【ROMMonitor】:一款驻留在嵌入系统ROM中的小程序,通过串行或网络连接和运行在工作站上的调试软件通信。这是最低端的技术,相对便宜,除了要求一个通信端口和少量的内存空间外,不需要其它任何专门的硬件,提供下载代码、运行控制、断点、单步步进,以及观察、修改寄存器和内存等功能。由于ROM监控器是操作软件的一部分,所以如果想要检查CPU和应用程序的状态,必须先停下应用程序,再次进入ROM监控器。

Data监视器【DataMonitor】:在不停止CPU运行的情况下不仅可以显示指定变量内容,还可以收集并以图形形式显示各个变量的变化过程。

OS监视器【OperatingSystemMonitor】:操作系统监视器可以显示诸如任务切换、信号量收发、中断等事件。这些监视器能够呈现事件之间的关系和时间联系,还可以提供对信号量优先级反转、死锁和中断延时等问题的诊断。

性能分析工具【Profiler】:可以用来测试CPU消耗所在,了解系统瓶颈、CPU的使用率以及需要优化之处。

内存测试工具【MemoryTeseter】:可以找到内存使用的问题所在,比如内存泄露、内存碎片、内存崩溃等问题。如果发现系统出现不可预知或间歇性的问题,就应该使用内存测试工具进行尝试。

运行跟踪器【ExecutionTracer】:可以显示CPU执行了哪些函数、谁在调用、参数是什么、何时调用等情况,主要用于测试代码逻辑,可在大量事件中发现异常。

覆盖工具【CoverageTester】:主要显示CPU具体执行了哪些代码,便于了解代码分支未被执行的区域,有助于提高代码质量并消除无用代码。

GUI测试工具【GUITester】:大多嵌入式应用都带有某种特定形式的图形用户交互界面,部分系统的性能测试是根据用户输入响应时间来进行的。GUI测试工具可以作为开发环境中运行测试用例的脚本工具,其功能包括对操作的记录和回放、抓取屏幕显示供后续分析比较、设置和管理测试过程(Rational公司的robot和Mercury的Loadrunner工具是之中的杰出代表)。没有GUI的嵌入式设备可通过插装来运行GUI测试脚本,虽然需要更改被测代码,但是节省了功能测试和回归测试的时间。

天目全数字实时仿真软件【SkyEye】:作为基于可视化建模的硬件行为级仿真平台,为嵌入式系统提供虚拟化运行环境,支持主流嵌入式硬件平台,可运行国内外主流的操作系统,对国产生态的支持尤为出色。工程师可基于SkyEye的虚拟硬件快速搭建模型并提前进行开发、测试和验证工作,实现高效率、高质量的软件交付。

2.尽早发现内存问题

内存问题存在较大危害且不容易排查,主要有三种类型:内存泄露、内存碎片和内存崩溃。对待内存问题必须要明确早发现、早“治疗”的态度。

内存泄漏

内存泄露是软件设计最常见的内存难题,指由于不断分配的内存无法及时地被释放,逐渐耗尽系统内存。即使细心的编程老手也会遭遇内存泄露问题,因其一般隐藏很深,很难通过代码阅读发现,甚至可能出现在库当中——有可能库中本就有bug,也有可能是因为工程师没有正确理解接口说明文档而造成了错用。

大多数的内存泄露虽然无法探测,但会表现为随机的故障,往往会被认为是硬件问题。如果用户对系统稳定性要求较高,此类问题会导致客户对产品失去信心,项目也会因此失败。考虑到内存泄漏的巨大危害,现在已有众多解决工具,通过查找没有被引用或重复使用的代码块、垃圾内存收集、库跟踪等技术来发现内存泄露,尽管每款工具都有利有弊,但还是应防患于未然,尽量测试内存泄漏。

内存碎片

内存碎片比内存泄露有着更深的隐匿性。随着内存不断被分配并释放,大块内存被不断分解为小块内存,从而形成碎片,后续需要申请大块内存时就有可能会失败。系统内存够大或许可能可以坚持较长时间,但最终还是逃不出分配失败的厄运。在使用动态分配的系统中,内存碎片经常发生。

该问题当前最为有效的方法便是使用工具,通过显示系统内存使用情况来找到内存碎片的罪魁祸首并进行改进。很多公司为避免动态内存管理问题,会选择在嵌入式应用中禁用malloc/free来以绝后患。

内存崩溃

内存崩溃是内存使用最为严重的结果,主要造成原因有数组访问越界、指针计算错误、重复释放同一段内存、释放非动态内存等。此类问题发生通常是随机的,极难事先排查,目前也很少有可供排查的工具。

综上,使用内存管理单元必须要小心谨慎,严格遵守其使用规则。

3.深入理解代码优化

人们对嵌入式系统的关注点通常在于实时性和速度,这两个要素直接影响着代码效率,需要对代码进行优化。了解如何优化代码是每个嵌入式软件开发人员必须具备的技能,而优化代码的前提和必要条件则是找到真正需要优化之处,然后再对症下药。

上文提到的profile能够记录如各任务CPU占用率、优先级分配、数据拷贝次数、磁盘访问次数、是否调用网络收发程序、测试代码是否已经关闭等数据,但在分析实时系统性能方面仍有不足。一方面,profile的使用往往是在系统出现问题,即CPU耗尽之后,而profile本身对CPU占用较大,所以很有可能不起作用。根据Heisenberg效应,任何测试手段或多或少都会改变系统运行。

4.不要大海捞针

大海捞针是对调试的生动比喻。寻找bug时应先确实是否在开发时有过为了寻求捷径而没有严格遵守编码设计规范的情况,或是没有检测部分假设条件或算法的正确性、没有将可能存在问题的代码打上记号。可参照《高质量c++/c编程指南》或《关于C的0x8本“经书”》来学习。

为了尽可能地暴露和捕捉问题根源,可以设计较为全面的错误跟踪代码:尽可能处理每一个函数调用失败,尽可能检测每个参数输入输出的有效性,包括指针及是否过多或过少地调用某个过程。错误跟踪能够了解bug的大概位置。

5.重现并隔离问题

对于模块独立的大型项目,如果问题的出现是间歇性的,则有必要设法去重现并进行记录完整过程,以备在下一次出现问题是进行复用。

确保问题重现后可用隔离的方法来解决问题:用#ifdef把一些可能和问题无关的代码关闭,把系统最小化到仍能够重现问题的地步。如果还是无法定位问题所在,可以考虑打开“工具箱”:试着用ICE或数据监视器去查看某个可疑变量的变化;使用跟踪工具获得函数调用的情况(包括参数的传递);检查内存是否崩溃以及堆栈溢出的问题。

6.以退为进

猎人为了不使自己在森林里迷路常常会在树木上留下标记,对过去代码修改进行跟踪记录对将来出现问题之后的调试也很有帮助。代码控制系统SCS或代码控制系统SCS可以很好地解决修改回溯问题,将上个版本checkin下来后和当前测试版本比较,可采用SCS/VCS/CVS自带的diff工具或其他功能更强的比较工具,比如BeyondCompare和ExamDiff。通过比较、分析所有改动代码,可以得到所有可能导致问题的可疑代码的分析结果。

7.确定测试的完整性

覆盖率测试可供确认CPU到底执行了哪些代码,从而确认测试的完整性。覆盖率工具有不同的测试级别,用户可以根据自己的需要选择某个级别。

即使单元测试已经很全面且没有deadcode,覆盖率工具还是可以指出一些潜在问题。

以下方代码为例:

if(i>=0&& (almostAlwaysZero==0||(last=i)))

如果almostAlwaysZero为非0,那么last=i赋值语句就被跳过,无法完成目标。

此类问题可轻松通过覆盖率工具的条件测试功能完成解决,覆盖率测试工具对提高代码质量是很有帮助的。

8.提高代码质量意味着节省时间

有研究表明,超过80%的软件开发时间被用在下面几个方面:调试自己的代码(单元测试)、调试自己和其他相关的代码(模块间测试)、调试整个系统(系统测试),更糟糕的则是可能需要花费10-200倍的时间来找一个最开始时很容易就能发现的bug。

千里之堤毁于蚁穴,即使bug对整个系统的性能没有太大的影响,但仍然很可能会影响可以被看得到的部分,必须养成良好的编码习惯和测试手段,以求更高的代码质量,缩短代码的调试。

9.发现它,分析它,解决它

世界没有万能的膏药,工具再好用也有无法实现之处,对于隐藏很深、用尽所有工具也无法查到其根源的问题,则需要通过问题的外在表现或数据输出来寻找其中规律,从而找出异常。任何异常的发现都应深入理解并回溯其根源。

10.请利用初学者思维

“有些事情在初学者的脑子里可能有各种各样的情况,可在专家的头脑里可能就很单一”,简单问题想复杂、简单系统设计复杂很可能就是由于“专家思维”。被问题难住时,不妨换个思路,或许就能得到意想不到的启发。

11.测试工具推荐

嵌入式调试无疑是一门艺术,和其它艺术一样,想要取得成功就必须具备智慧、经验并懂得使用工具。天目全数字实时仿真软件SkyEye可供搭建嵌入式系统虚拟化运行环境,工程师可不受物理硬件限制,随时访问目标系统,快速搭建虚拟硬件模型并提前进行开发、测试和验证工作,实现高效率、高质量的软件交付。

▲Skyeye 覆盖率测试界面

原文链接:https://mp.weixin.qq.com/s/Yk36_435jz–oP4eVntTzA