《 嵌入式系统设计与实践》一一1.2 嵌入式系统开发
1.2 嵌入式系统开发
嵌入式系统是特殊的,因此也给开发者带来一些特殊的挑战。许多嵌入式软件工程师开发了工具箱来处理各种约束。在我们开始构建自己的系统之前,先来看看开发一个嵌入式系统会有哪些困难。在熟悉了嵌入式系统开发会如何受到限制之后,我们再开始讨论一些设计原则并借此指导我们找到更好的解决方案。
1.2.1 调试
如果在计算机上运行调试软件,就可以在这台计算机上编译和调试。系统有足够的资源在运行程序的同时调试程序。事实上,硬件根本不知道是在调试程序,因为这是由软件完成的。
嵌入式系统就不是这样了。除了需要交叉编译器外,还需要一个交叉调试器。这个调试器运行在计算机上,通过特殊的处理器接口和目标处理器通信(见图1-1)。这个接口是专门用来在处理器工作时对它进行侦听的。这个接口通常称为JTAG(发音“Jay-tag”),而不管有没有真正地实现这个广泛应用的标准。
图1-1:计算机和目标处理器
处理器必须通过扩展某些资源以支持这个调试接口,允许调试器在运行时挂起它,并提供调试信息。支持调试操作增加了处理器的成本。为了节省成本,一些处理器只支持一个受限的功能子集。比如,增加一个断点会让处理器修改机器代码以停在断点处。但是,如果代码是执行在闪存(或者任何其他的只读存储器)中,那么处理器将会设置一个内部寄存器(硬件断点),并在每个执行周期时将其与运行地址比较,如果相等则停止程序运行。这样可以改变代码的时序,在调试(或者没有调试)时出现一些奇怪的问题。内部寄存器也消耗资源,因此常常只有极其有限的硬件断点可用(大多数情况下,只有两个)。
总之,处理器支持调试,但与纯软件开发相比,就没有我们习以为常的那么多调试功能。
在计算机与目标系统之间通信的设备通常叫做仿真器、在线仿真器(ICE)或者JTAG适配器。这些都可能指同一个东西(不怎么合适),或者三个不同的设备。仿真器是为特定的处理器(或者处理器系列)设计的,因此不要以为在一个项目中使用的仿真器可以在另一个项目中正常使用。仿真器会增加成本,尤其是当有多个仿真器或者有一个比较大的团队在开发系统时。
为了避免购买仿真器或者处理器的限制,许多嵌入式系统都通过其他一些手段实现调试,如使用printf或者一些轻量级日志向一个没有使用的通信接口输出。这些方法非常有用,但也会改变系统的时序,导致一些问题只有在关掉调试输出之后才能得到解决。
嵌入式系统的软件开发有些棘手,因为需要平衡系统的要求和硬件的约束。现在,在待办事项列表里加上一项:在不那么友好的硬件环境中,让软件具备比较好的可调试性。
1.2.2 更多挑战
嵌入式系统是为了完成特定的任务,所以会去掉所有与完成任务不相关的资源。这里的资源包括:
内存(RAM)
代码空间(ROM或闪存)
处理器周期或者速度
耗电量(电池寿命)
处理器外设
从某种程度上说,这些是可以互换的。比如,以代码空间换CPU周期,在写代码时可以让部分代码占据更多的空间这样就可以运行得更快。或者可以降低处理器的运行速度以减少耗电量。如果没有特定的外围设备接口,则可以利用输入/输出线和处理器周期在软件里模拟这个接口。但是,即使再怎么权衡,以上各种资源依然是非常有限的。资源限制所带来的挑战是所有挑战中最明显的。
另一类挑战来自硬件。交叉调试带来的额外压力是令人沮丧的。在电路板调试的过程中,对于一个缺陷是由硬件还是软件造成通常是不确定的,这让问题变得更难以解决。与计算机不同,在嵌入式系统中,我们编写的软件可能对硬件造成实际的破坏。因此,大多数情况下,需要了解硬件以及硬件能做什么。虽然,这些知识可能在设计另外一个系统的时候毫无用处,但是你必须面对挑战,快速学习。
开发和测试完成了之后,就进入系统生产制造阶段。这是大多数纯软件开发工程师不曾考虑过的事情。构建一个系统,并且以比较合理的成本去生产它,这是软件工程师和硬件工程师都该铭记于心的一个目标。对可制造性的支持,是确保系统可以以较高的精度重复制造的一种方法。
制造完成之后,产品就进入市场了。对消费类产品来说,同时意味着千家万户会“享受”产品中的缺陷。对于医疗、航空或者其他关键产品,这些缺陷将是灾难性的。(这就是为什么现在要做这么多研究工作的原因)。对于科学研究和监控设备,应用现场可能是那些装备难以收回(或者需要巨大的风险和代价才能收回,比如在火山口的装置),因此这些装置最好能正常运行。系统在从我们手中诞生之后会带来什么样的生活,这也是设计软件时必须面对的一个挑战。
在对所有这些问题了然于心,并且在设计系统的时候有了确定的方法解决这些问题之后,还有一个最大的挑战,这对所有的工程师来说都很常见的挑战:变更。不仅仅产品目标会变更,项目的需求也会在整个项目周期内变更。最初,可能只是想试验一些新的想法,去做一些尝试。随着对产品目标的认识逐渐加深以及对硬件越来越了解,我们就会开始设计更多的机制让软件变得可调试、健壮和灵活。在资源受限的环境里,需要决定在开发时间,内存、代码空间以及处理器周期这几个方面能提供哪些基础设施。通常,最初的设计并不是在你开发完成后所得到的那个,并且开发似乎永无止境。
不幸的是,为了特定的应用目的设计出来的嵌入式系统有一个副作用:当应用发生变化时,系统可能难以支持变更。设计开发嵌入式系统并不仅仅是关于严格的限制和系统的最终完成,这里的挑战是要找出这些约束中哪些会在产品开发的后期产生问题。因此,需要能够预测可能导致变更的原因,设计足够灵活的软件来适应可能发生的应用程序变化。
1.2.3 解决问题的原则
嵌入式系统就像个智力拼图,每一小部分都相互锁在一起(只能以一种方式)。有时候,虽然可以使用蛮力将各个部分拼在一起,但结果却可能和盒子上的图像相差甚远。我们应该摒弃这样的观点,即在项目结束时将最终的结果作为唯一发布的代码版本。
事实上,智力拼图有个时间维度揭示了其整个生命周期的不同变化:概念设计、原型化、电路板调试、系统调试、测试、发布、维护,如此循环往复。灵活性并不仅仅指代码现在能做什么,而且指在其整个生命周期里面能做什么。我们的目标是要做到足够灵活,这样才能在满足产品目标的同时能够很好地处理资源约束和其他一些嵌入式系统内在的设计挑战。
我们可以应用软件设计上的很多优秀的设计原则来让系统变得更加灵活。通过使用模块,我们将功能分离在子系统里,并隐藏各个子系统的数据。使用封装,我们设计子系统之间的接口,以使各个子系统相互独立。一旦我们拥有了松耦合的多个子系统(或者对象),就可以在修改软件的某一部分时相信这个修改不会影响其他部分。这样我们就可以分拆我们的系统,然后在需要的时候按照不同的方式再把它们组装起来。
知道在哪里将一个系统分解为各个部分需要更多的实践。一个比较好的原则是考虑哪些部分会独立地发生变化。在嵌入式系统里,应用这一原则需要我们考虑各个不同的物理对象。比如,如果传感器X需要通过通道通信Y通信,那么这两个独立的对象就是两个候选子系统(也是两个代码模块)。
我们把子系统分解为对象之后,就可以对这些对象进行测试。我很幸运,曾经在一些项目中有非常优秀的质量保证(QA)团队。在其他的一些项目中,不曾有过任何人在我的代码和那些将要使用我的系统的人们之间承担QA的角色。我发现在软件正式发布之前捕获的缺陷就像礼物一样。错误发现的越早,解决这些错误的成本越低,对大家越有好处。
当然,不必等着别人给我们送礼物。测试和质量向来是相互关联的。写测试代码对系统进行测试可以让系统质量更高,给代码提供一些文档,别人会认为我们开发出来的软件卓尔不群。
对代码文档化是另一个减少缺陷的方法。但如下这样注释代码,让人很难理解详细的程度。
i++; // increment the index
不需要这样做,其实这样的代码行很少需要注释。写注释的目的是为了像你一样的开发人员,在一年之后再看你写的这些代码,那个时候的你可能正忙于其他事情并且忘记了当初你怎么想出这个创造性的解决方案,你可能甚至已经忘记了你写过这些代码。因此,请在代码里留下些痕迹以帮助你自己找回记忆(文件和函数头)。总的说来,假设读者和你具有相同的心智和背景,只需写清楚这段代码做了什么,而不是如何做。
最后,在资源有限的系统开发过程中,我们常常会有尽早和尽可能多地去优化代码的想法。抑制住这个欲望。实现所有的功能、让系统运行、完成测试,然后再回来按照要求让代码更小或者运行得更快。
时间是有限而宝贵的,因此在各个子系统能够运行后再来专注于那些最消耗资源的部分,看看能否得到更好的结果。为了运行速度去优化一个很少运行的函数不会带来任何好处,反而会减少花费在那些运行频率非常高的函数上的时间。有一点可以肯定的是,处理系统的资源约束需要一些优化。但在调优之前请务必搞清楚系统的资源消耗情况。
“我们应该忘记小的性能提升,在97%的情况下,不成熟的优化是万恶之源”
——Donald Knuth