新闻中心

EEPW首页 > 嵌入式系统 > 设计应用 > 复杂的实时嵌入式系统的调试

复杂的实时嵌入式系统的调试

作者:ADI公司供稿时间:2008-04-15来源:电子产品世界收藏

  随着的复杂程度不断提高,低效率的方法的成本日益增加。鉴于当前嵌入式应用的复杂性还有继续上升的趋势,对这些系统的将成为加速产品上市和提供鲁棒性最终产品的关键因素。随着应用对多线程和中断嵌套的使用,开发商的大部分时间目前都花在上。应用的属性使得将伴随同时发生多个事件的故障问题孤立起来变得更为困难。本文将讨论常见的调试问题以及预防和检查这些故障问题的一些方法。

本文引用地址:http://www.eepw.com.cn/article/81550.htm

  从历史角度上来看,嵌入式应用代码的调试流程可以分为两类。第一类调试流程是回答 “我的代码现在执行到哪里?” 的问题。当开发商依靠打印语句或者LED的闪烁来指示应用程序执行到某个节点的调试方法时,往往就属于这种情形。如果开发工具支持这种调试方法,可以沿着应用程序执行的路径插入断点。第二类调试流程是帮助回答“我看到的数据是从哪里来的?”这一问题。在这种情况下,人们往往依靠寄存器显示窗口观察变量信息、处理器内存的内容。人们还可以尝试单步执行,并且观察所有这些数据窗口以了解某个寄存器状态何时出现错误,内存位置何时得到错误的数据,抑或指针何时出现了误用。

  当开发商写完全部代码后,如果无需了解网络基础设施,也没有操作系统的任务调度需要考虑,那么就可以利用这些调试方法使一个应用程序运行起来。然而,现在的情况并非如此。嵌入式处理器以超过600 MHz的速度运行,并且拥有可支持Ethernet和USB等协议的嵌入式外设,支持功能齐备的操作系统,例如mClinux,而且这些操作系统所调度的各种应用程序是由数千行代码构成。使用打印语句和利用LED来调试是不现实的,因为现在常常有如此之多的功能在执行是不可能的,或者它们会影响标准I/O口,从而造成处理器性能大幅度下降。

  软件最常见的调试问题可以大致划分为如下几类:

  1. 同步问题;

  2. 内存和寄存器讹误;

  3. 与中断相关的问题;

  4. 编译器问题;

  5. 异常情况。

  下文对上述问题作简要介绍。

  同步问题

  在任何系统中,只要有多串序线程或者进程运行,而且是异步共享数据,则系统必然存在同步问题。对于共享数据的全部操作必须是顺序化的,也就是说,只有在一个线程或者进程完成对数据的操作后,其它的线程才能对数据进行操作。

  以图1为例,线程A和线程B对共享变量“counter”进行操作,A让counter 增加,而B则让counter减少。下方示出了线程A的counter++和线程B counter—的汇编代码。假设线程B的优先级要高于线程A,而线程A目前正在运行,则线程B将被阻止。

  举例来说,假设初始的计数值是2,而线程A是执行线程。则线程A读入计数值,并送入一个寄存器,在使其增加一个增量后,再将其写回计数器变量上。

  在可抢先的多线程系统中,高优先级的线程的执行可以抢先于低优先级的线程。如果数据被一个线程和中断例程共享,也会出现上面的问题,因为中断的执行与线程的执行之间是异步关系。

  同步化方面的问题常常是很难进行调试的,因为它们取决于时序,是随着软件对数据的操作而随机出现的。幸运的是,大多数的实时操作系统可以提供同步化原语。开发商 可以使用最适当的机制来保护共享数据,而不至于影响系统的性能。如果数据在多个线程之间共享,则开发商将有如下的选择:

  a. 关闭调度器以便当前的线程永远不会被其它线程抢先;(无调度区)

  b. 使用信号量(Semaphore)或者互斥信号量(Mutex)来保护共享数据;

  c. 利用关键区域来进行保护,即屏蔽所有的中断。

  调试的一个小窍门就是,如果共享的数据被破坏,则编程者就应当首先检查出任何一种多个线程或者中断对共享数据同时进行的操作。如果线程和中断共享了数据,那么在线程代码中必须将中断关闭。如果数据在多个中断例程之间共享的话,则中断也应当被关闭,因为高优先级的中断可以抢先于低优先级的中断。



  另外一个同步化问题则与线程优先级的不恰当的分配有关。应当确保系统的初始化线程在引导时间内就启动,并在生成其它的优先级更高的线程之前,完成整个系统的初始化。

  内存和寄存器的数据讹误

  大多数的都采用了平面化的内存模式,也没有内存管理单元(MMU),因而没有硬件支持的内存保护机制。即使采用能提供这种功能的处理器,也需要由开发商来实现对某些内存区域的保护。进程和线程将对其它进程和线程的内存空间有完全的访问权限。这可能会造成下面所描述的、各种类型的内存讹误问题。

  堆栈溢出运行时堆栈是在函数调用进程中所使用的一种暂存空间,用于存储局部变量。硬件寄存器指针(SP)将跟踪堆栈指针的地址。如果你在高级的语言中编程,如C语音,则编译器所生成的代码将使用与C语言运行时间模型相一致的堆栈。运行时间模式定义了变量是如何存储在堆栈中的以及编译器将如何使用堆栈。局部的变量被放置在当前的堆栈中。图2给出的例子描述在堆栈上采用的某些关键性的内存。



  当堆栈指针超出了所指定的边界时,就会出现堆栈溢出。这将造成内存的讹误,并最终造成系统的失效。在上述的实例中,如果总的堆栈内存区不足以容纳所有的局部变量,堆栈溢出就会发生。

  调试的一个技巧就是,如果你担心溢出,一个好的办法就是将堆栈安排在内存边界上,这样,如果在调试过程中出现了溢出,则仿真器将触发一个硬件异常提示。

  有些实时操作系统可能会提供调试功能,例如保护位可以形成对堆栈溢出的防护。这些操作系统要么记录关于堆栈溢出的错误信息,要么提交一个异常报告,以便动态地增加堆栈。最起码当前的大多数实时操作系统都能报告堆栈以及已经被线程和进程所采用的堆栈的情况。
在任何中断驱动的系统中,堆栈的分配方式都必须考虑到中断服务例程所采用的空间。如果中断例程的设计目标是使用当前的执行对象栈,则在这种情况下,每一个线程或进程所拥有的最小的堆栈尺寸都应大于或者等于执行对象所要求的堆栈尺寸加上所有中断例程累积起来所需要的最大的堆栈尺寸。

  嵌入式系统开发商必须掌握各种应用链接库。例如,第三方的库可能会认定堆栈上为其提供了空间。

  与中断相关的问题

  在嵌入式系统中,一般情况下,出于性能方面的考虑,中断服务例程是以汇编形式编写的。中断本质上是异步的,在应用执行中的任何时刻都有可能出现。汇编层次上的中断例程最常见的问题,是寄存器的讹误。在中断服务例程中所采用的寄存器所存储的数据,在寄存器被使用之前都必须被保存,而在从中断服务例程返回之前,这些数据将被恢复。开发商必须了解状态寄存器的情况,而任何一种ALU的操作都会改变其状态。在这种情形下,ISR应该保存其状态并进行恢复,仿佛它是一个已被使用的寄存器一般。

  如果中断例程是用C语言编写 的,它们的开发也是为了使用当前的堆栈,则开发商就应该针对堆栈溢出情况进行防护,即每个线程都应该拥有足够多的堆栈,来满足中断或者嵌套的中断堆栈的要求。最好的做法,就是让中断例程的规模尽可能小,推迟处理过程,交给一个线程或者优先级较低的中断。在开发过程中,开发商可以在中断的开始和结束部分添加诊断功能,对基础的架构中的寄存器的状态进行比较。

  中断嵌套可以让一个高优先级的中断抢先于低优先级的中断例程执行。开发商应该考虑到堆栈要求的峰值,并为其分配充足的空间(考虑最差的情况,即系统中的每一个中断都被一个优先级更高的中断所抢先)。

  有时,某些函数是以汇编语言编写的,将被C函数所调用。如果汇编代码并未按照C函数运行时间调用规范来编写,即按照编译器所要求的那样进行,则会导致参数传递无效和讹误。

  编译器的问题

  编译器的优化,即使实现了逻辑上的正确性,有时也仍然会造成故障。采用低水平的设备驱动器时,这一问题特别关键。重排指令是实现更高性能的常用方法,因为处理器常常支持单个周期内执行多条指令。因此,编译器将试图调度指令,使得所有的指令时间片都得到充分的利用,即使这意味着在寄存器使用前很久就载入数据,或者在数值被计算完毕后很久,也让内存保持载入的数据。

  例如,假设一个设备必须在向其发任何指令前就完成初始化。编译器可能会移动指令位置,以便改善性能。这可能会造成设备的故障。如果设备驱动器调试后的版本是可行的,而采用经过优化的版本时会出现故障,那么你要查看设备的初始化中是否有被移动的指令。

  有时,将代码从一个架构移植到另一种架构上,也会带来某种数据类型上的问题。例如,一种架构内的整数是32 bit的,而其它的架构中是48 bit或者64 bit的。这可能会导致数据的失效或者被截断。

  异常情况所带来的问题

  如果异常是与程序的执行相同步的,则往往是一种不当的操作的结果,例如零作为除数所造成的异常,某些异常则是架构所特有的。处理异常的最佳方法是采用缺省的异常处理器,并在出现异常时检查异常出现的环境。异常所处的环境背景是寄存器量值的集合,包括状态寄存器。大多数架构将拥有一个指令地址寄存器,用来保存造成问题的指令地址。在多数情况下,要知道一个异常是如何发生的并不难,但是,是何种指令路径可以隔离出这一失效,则是调试的难点。有些架构支持跟踪,可以看到程序顺序执行的指令的历史。

  不能执行错误检验的代码会造成内存的讹误。由于性能方面的原因,开发商可能会放弃对错误的检查。跳过错误检查将让内存泄漏等事件无法为人所知,而最终导致内存讹误。某些处理器架构就容许应用监测数据总线的活动,从而能抓住相应事件。

  探寻架构特有的功能,大多数嵌入式处理器都支持某种层次上的调试功能。内置的跟踪单元就是一种得到硬件支持的跟踪机制。例如,ADI公司的Blackfin处理器系列就具有硬件跟踪单元,当硬件跟踪缓冲器充满后,就会产生跟踪异常。使用这种跟踪单元后,人们可以构建出完整的执行路径。免费工具可从http://www.blackfin.org/网站下载。

  观察点可以让你监测特定的内存位置或者内存块区正在被更改时出现的情况。观察点可以监测内部的数据总线传送,如果在观察点寄存器中,发现任何匹配的对象,则让处理器暂停。如果一个特定内存位置不断出现讹误,则观察点就非常有用。对内存块区进行观察以查看是否有任何正在损毁存储器数据的恶意代码。大多数当前的调试环境都容许对内存和寄存器的内容进行修改。有时,修改寄存器的内容,可以让我们洞察何处出现了故障。例如,通过更改程序计数器,你可以迫使程序在特定函数出现时恢复执行。

  结语

  总之,由于调试是开发过程的最后步骤,因此它将对产品上市时间造成直接的影响。调试本身也是难以调度的,因为所发现的问题在复杂性和可避免性方面都大相径庭,上面所讨论的是一些在嵌入式系统开发期间常见的问题。这些调试技巧和提示旨在着重强调节省时间,因此在开发复杂的嵌入式系统时,应用现代的开发工具和拥有丰富调试功能的处理器能够改善投资收益。



评论


相关推荐

技术专区

关闭