嵌入式系统设计与实践--(1)导论

        嵌入式系统是为了特定应用而专门构建的计算机系统。
因为嵌入式系统要完成的任务比通用计算机系统窄很多,所以对于完成与手头任务不相关的事情提供较少的支持。硬件通常也有很多约束。比如,为了节省电池电量,CPU运行得更慢;为了便于制造,使用更少的内存;处理器通常只能具有特定的速度,或者只支持一部分外设。
        在嵌入式系统中硬件并不是唯一受限制的部分。在某些系统中,软件的行为必须是确定的(每次必须以同样的方式运行)或者是实时的(任何时候对特定的事件快速响应)。有些系统要求软件能容错,当有错误发生时能以优雅的方式降级运行。例如,一个不能允许软件失效或者硬件损坏的系统(比如卫星或者鲸鱼的追踪标志系统)。另一些系统要求在第一个失效症状出现的时候,软件立刻停止操作,通常这时候软件会提供明确的错误信息(比如,心脏监护仪不应该悄无声息地失效)。

1.1 编译器、编程语言以及面向对象编程

        嵌入式系统的另一个特点就是在开发中都使用交叉编译器。虽然交叉编译器运行在台式计算机或者笔记本计算机上,但编译出来的代码却不是。交叉编译的映像文件在目标嵌入式系统上运行。由于编译完的代码需要在嵌入式处理器上运行,所以目标系统的供应商通常会提供一个交叉编译器或者一系列的交叉编译器供选择。不少大的处理器供应商使用基于GNU工具系列的交叉编译器。
        嵌入式软件编译器通常只支持C或者同时支持C和C++。而且,许多嵌入式C++编译器只实现了C++语言的子集(一般来说,多重继承、异常处理以及模板都没有实现)。Java在嵌入式系统中日益流行,但其内在的内存管理问题使得它只能使用在大型系统中。不管你在软件中使用什么语言,都可以使用面向对象的设计。封装、模块化以及数据抽象的设计原则可以应用在几乎任何应用程序和任何语言中。目的是让设计健壮、易维护和灵活。我们应该使用面向对象技术的所有优点。
        从总体上说,嵌入式系统可以看做是对象,尤其是作为一个更大系统的一部分时(如,与机顶盒通信的遥控器、工厂的分布式控制系统、汽车上的气囊展开传感器)。从更高的层面上说,一切都是面向对象的,因此把这一结论向下扩展到嵌入式软件是合乎逻辑的。
        另一方面,我不主张严格地遵循所有面向对象的设计原则。嵌入式系统有太多的设计目标,因此不可能确立一个一成不变的原则。一旦我们认识到了需要权衡之处,就可以在软件设计目标和系统设计目标之间进行权衡。

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的角色。我发现在软件正式发布之前捕获的缺陷就像礼物一样。错误发现的越早,解决这些错误的成本越低,对大家越有好处。
        当然,不必等着别人给我们送礼物。测试和质量向来是相互关联的。写测试代码对系统进行测试可以让系统质量更高,给代码提供一些文档,别人会认为我们开发出来的软件卓尔不群。
        写注释的目的是为了像你一样的开发人员,在一年之后再看你写的这些代码,那个时候的你可能正忙于其他事情并且忘记了当初你怎么想出这个创造性的解决方案,你可能甚至已经忘记了你写过这些代码。因此,请在代码里留下些痕迹以帮助你自己找回记忆(文件和函数头)。总的说来,假设读者和你具有相同的心智和背景,只需写清楚这段代码做了什么,而不是如何做。
        最后,在资源有限的系统开发过程中,我们常常会有尽早和尽可能多地去优化代码的想法。抑制住这个欲望。实现所有的功能、让系统运行、完成测试,然后再回来按照要求让代码更小或者运行得更快。
        时间是有限而宝贵的,因此在各个子系统能够运行后再来专注于那些最消耗资源的部分,看看能否得到更好的结果。为了运行速度去优化一个很少运行的函数不会带来任何好处,反而会减少花费在那些运行频率非常高的函数上的时间。有一点可以肯定的是,处理系统的资源约束需要一些优化。但在调优之前请务必搞清楚系统的资源消耗情况。
        “我们应该忘记小的性能提升,在97%的情况下,不成熟的优化是万恶之源”。——Donald Knuth

面试问题:Hello World
        这里有一个装了编译器和编辑器的计算机。请实现"Hello World"程序。在基本版本运行后,增加一个功能,从命令行中获取名字。最后,告诉我在你的代码开始执行之前(main()函数之前)发生了什么?
        在很多嵌入式系统中,需要从头开始开发。在这个任务的第一部分,我希望面试人能够从一个白板开始,填入基本的功能,即使是在一个不熟悉的开发环境中。我希望他具备解决这个直接明了问题的编程技能。
        这是一个基础的编程问题,因此最好能熟悉简历上所述的编程语言。对这个问题来说,任何语言都是一样的。在我要求实现"Hello world"时,我考察语言的细节(如包含哪个头文件,在C和C++中使用命令行参数)。我期望面试者能基于编译错误去发现和解决语法问题(然而,当他能输入整个程序而没有任何错误,即使是拼写错误也没有时,也会给我留下极其深刻的印象)。


注意:我自己是个不错的打字员,但如果有人在旁边看着我,我会每隔一个字母就输错。没有问题,很多人都会这样。所以,不要因为这而乱了方寸。专注在键盘和代码上,而不是在你的打字技巧上。


        这个问题的第二部分是关于嵌入式系统的切入点。纯计算机科学家会把计算机看做一个理想的盒子,在其中执行他的完美算法。当问及在main函数之前发生了什么时,他通常会说:“你知道的,程序运行了。”但他并没有理解其中的含义。
        但是,如果他提到"start"或者"cstart",那么他已经在面试中有个不错的开始了。总的说来,我希望他能知道除了我们看到的代码之外,程序还需要初始化,而不管是在什么平台。我期望能听到他说设置异常向量来处理中断、初始化关键外设、初始化栈空间、初始化变量,还有如果有C++对象则调用构造函数。如果他能说清楚编译器隐式地做了什么以及初始化代码显式地做了什么,那就更棒了。
        最好的回答就是一步一步描述清楚发生了什么,并且解释为什么这些事情如此重要以及它们如何在嵌入式系统中发生。有经验的嵌入式工程师通常会从向量表开始,从向量表重置到系统加电行为的描述。本书的后面章节将讨论这些问题,因此即使这些名词对大家来说是陌生的也不用担心。
        如果电气工程师(EE)来问这个问题,候选人就可以进一步讨论系统加电行为,那么他会给面试额外的分数。比如,解释为什么系统不可能在开关打开后1毫秒之内启动并运行。电气工程师期望听到加电时序、电压上升时间、时钟稳定时间、处理器重置/初始化延时等。

你可能感兴趣的:(嵌入式系统设计与实践)