读书频道 > 网站 > 网页设计 > 嵌入式系统设计与实践
2.2.6 例子:一个日志接口
13-06-21    奋斗的小年轻
收藏    我要投稿   

本文所属图书 > 嵌入式系统设计与实践

O’Reilly Media通过图书、杂志、在线服务、调查研究和会议等方式传播创新知识。自1978年开始,O’Reilly一直都是前沿发展的见证者和推动者。超级极客们正在开创着未来,而我们关注真正重要的技术趋势...立即去当当网订购

对于一个资源受限的系统来说,通常缺少和外部通信的途径。本例中的日志模块的目标就是要实现一个健壮和有用的日志系统。本节从定义接口需求开始,然后探讨接口(和本地存储器)的不同方案。至于通信方法是什么无关紧要。在面对限制针对这些接口编程的时候,可以在其他的系统上重用我们的代码。

注意: 将调试日志输出会严重降低处理器的性能。如果在打开和关闭日志的时候,代码行为发生了变化,就需要考虑不同子系统的时序如何一起工作。

具体的实现依赖于具体的系统。有时,可以将一根输入/输出线连接到发光二极管(LED),通过摩尔斯编码将日志消息往外发送(开个玩笑)。然而,大多数时候,需要将调试消息写到某个接口。将系统设计成可调试的是可维护性设计的一部分。即使代码是非常完美的,但在其他人需要增加新的特性的时候也许没有那么幸运了。日志子系统不仅在开发阶段很有帮助,在维护阶段也是非常有用的。

日志接口隐藏了日志的具体实现细节,也隐藏了变化(和复杂性)。但日志需求可能在产品开发周期中发生变化。例如,在开始阶段,开发工具可能有一个额外的串口,此时,可以将日志信息输出到计算机。在稍后的某个阶段,串口可能不再可用,于是,就需要简化日志并把它通过一个或者两个发光二极管输出。

有时候,我希望在系统异常的时候能够考虑得非常全面。不幸的是,没有足够的资源输出想要的一切信息。而且,日志输出方法可能在产品开发过程中发生变化。这点就是应该通过将发生变化的函数调用进行封装的地方。这里一点儿额外的开销会带来极大的灵活性。所以,如果可以面向接口编程,那么底层实现方法的变化就没有关系。

通常,我们想要通过一个相对狭窄的通信通道输出大量的信息。当系统日益增大的时候,这个通道就会显得更小。通道可以是通过RS232连接到计算机的简单的串行接口,这是一种需要特殊硬件的方法。还可以是通过网络传送的调试数据包。数据可以存储在外部RAM上,只有暂停处理器运行和读JTAG时才可以读取日志。只有运行在开发环境时日志才是可用的,而在用户的硬件环境上就不可用。日志模块的需求有三点。

第一点,日志接口应该可以处理各种不同的实现情况。

第二点,当我们正在调试系统的某一部分时,也许不希望看到来自其他部分的消息。所以,记录日志的方法应该是特定于某个子系统。当然,肯定需要知道其他子系统崩溃时发生的问题。

第三点,就是关于优先级。优先级可以让我们在调试某个子系统的细节时不至于忽略来自于其他部分的重要信息。

日志典型调用

定义一个模块的主要接口需求比定义本身来说要做更多的事情,尤其在设计阶段。但是,千言万语总可以总结成类似下面这句代码:

Void Log(enum eLogSubSystem sys, enum eLogLevel level, char *msg);

这个函数原型并不是固定的,它可能随着接口的发展而变化。但它提供了一个对于其他开发者来说非常有用的速记符号。

日志级别可以包括:空、信息、调试、警告、错误以及危险。子系统则取决于具体的系统,可以包括:通信、显示、系统、传感器、升级固件等。

注意,日志消息是个字符串,与printf和iostream中的可变参数不同。如果有这个功能,可以使用库来构造这个消息。但是,printf和iostream系列的函数在需要更多代码空间和内存的系统中,通常是首先要去掉的。如果情况是这样,那么我们可能需要自己去实现需要的功能,于是,这个接口应该具备满足需求的最小功能。除了打印字符串之外,通常还需要能够每次输出至少一个数字:

void LogWithNum(enum eLogSubSystem sys, enum eLogLevel level, char *msg, int number);

使用子系统标识符和优先级可以做到远程修改调试选项(如果系统允许这么做)。在开始调试的时候,可能将所有的子系统都设置为低优先级(如调试),并且当一个子系统调试完之后,就提升其优先级(如错误)。这样就可以只在需要的时候输出想要的信息。因此我们需要定义一个能调整子系统和优先级灵活性的接口:

void LogSetOutputLevel(enum eLogSubSystem sys, enum eLogLevel level)

因为调用代码不关心底层是如何实现的,所以调用代码不应该直接访问这个接口。所有的日志函数都应该调用这个日志接口。在理想情况下,底层接口不应该和任何其他模块共享,但架构图会告诉你是否正确。日志初始化函数应该调用任何它依赖的函数,不管是初始化一个串口驱动程序还是设置输入/输出线。

由于日志可以改变系统时序,所以有时候需要以一种全局的方式关掉日志输出。这样就可以肯定地说这里的调试子系统没有对任何其他代码造成干扰。虽然用的不是很多,但将打开/关闭(on/off)开关加到接口中是个很棒的方法:

void LogGlobalOn();

void LogGlobalOff();

其他子系统的设计不会(也不应该)依赖于日志系统的实现方式。如果我们能就模块的接口这么说(“其他子系统不依赖于XYZ子系统的实现方式,它们只需要调用其给出的接口”),那么系统接口的设计就比较成功了。

代码的版本

有时候,需要知道当前运行代码的准确版本。在现实应用中,在帮助/关于对话框中放置版本号是一个简单直接的做法。在嵌入式系统中, 版本号应该可以通过主要通信方式获取到(串口、I2C、其他总线等)。如果可以,应该在系统启动的时候通过这些方式自动输出版本号。如果这样不行,试着通过查询的方式获取版本号。如果这样还是行不通,那么就应该将版本号编译到对象文件中,存储在特定的地址内,以便在查询的时候可用。

理想的版本号采用A.B.C的方式:

 A是主版本号(1字节)

 B是次版本号(1字节)

 C是构建号(2字节)

如果构建号没有自动递增,就应该经常递增它(数字是不需要额外成本的)。根据具体的输出方式和系统原理,为了恰当地显示版本号,可以在日志代码中增加一个接口:
void LogVersion(struct sFirmwareVersion *v)

运行时代码不是系统中唯一需要版本号的部分。系统中每一个需要单独构建和升级的部分都需要版本号,这应当是规约的一部分。比如,如果在生产的时候需要对一块EEPROM编程, 那么它就应该有一个版本号以便代码在使用EEPROM之前检查。在某些情况下,可能没有足够的空间和能力做到向下兼容,但确保系统中各个运行组件之间当前能够相互兼容则非常关键。
日志状态

在设计系统架构的时候,有些部分比其他一些部分容易定义,尤其是那些与之前曾经做过的东西很类似的部分。在定义这些接口的时候,我们会感到成竹在胸,实现起来容易而且有趣,而所要做的就是立刻着手。

暂且抑制一下急于求成的心情吧,应该尽量将所有的部分保持在同一个级别。如果在定义一个模块与其他子系统的接口之前,把该模块实现得完美无缺,你就可能会最终发现这些子系统并不能很好地配合。

在对系统的各个模块做进一步的研究之后,就应该考虑这个模块的状态。总的说来,一个模块所拥有的状态越少越好(这样函数在每次被调用时都会做同样的事情)。但是,去掉所有的状态通常是不可能的(或者至少是件很棘手的事情)。

回到我们的日志模块:能设置所要的内部状态吗?LogGlobalOn和LogGlobalOff函数设置(和清除)同一个变量。LogSetOutPutLevel需要知道每个子系统的级别。

有多种不同的选项去实现这些变量。如果想去掉局部状态,可以将它们放到一个结构体(或者对象)中,这样每个调用日志模块的函数都必须拥有。但是,这需要将日志对象传递给所有需要日志功能的函数,以及每一个需要调用某个函数并且该函数需要日志功能的函数。

注意:你可能会认为像这样传递状态变量有些令人费解。对于日志模块,我同意。但是,你有没有想过当你打开一个文件的时候,获取的文件句柄中都有什么吗?打开文件的操作包含了大量的状态信息。

也许传递所有这些参数并不是一个很好的主意。那么,每个使用日志子系统的调用者如何访问它呢?如《Object-Oriented Programming in C》中提到的,也可以在C语言中创建一些面向对象的特性。即使在一个更面向对象的语言中,也可以有一些模块,在该模块中有些全局函数并将状态保存在一个局部对象中。然而,还有另外一种方法可以提供对日志对象的访问而不需要将该模块完全开放。

C语言中面向对象的编程

既然大多数系统都有比较好的C++编译器,那么为什么不使用C++呢?事实上,有大量的代码早已经用C写好了,有时候需要匹配早已完成的工作。或者我们因为C语言的速度而喜欢它。所有的这一切并不意味着可以将面向对象的原则抛之脑后。

其中一个最重要的思想就是数据隐藏。在面向对象语言中,对象(类)可以包含私有变量。这样我们可以说它们具有内部状态,这些内部状态对其他对象是透明的。C语言有多种不同的全局变量。可以通过适当的设置变量作用域来模拟私有变量(甚至友元对象)。首先,我们来看看C中与公共变量的对等实现,它们通常声明在C文件的顶部,在函数外部:
// everyone can see this global with an "extern tBoolean_t gLogOnPublic;" in the
// file or in the header
tBoolean gLogOnPublic;

这些全局变量会导致意大利面条式的代码。为了避免这些问题,可以在函数外部用static关键字定义一个私有变量,并且通常定义在C文件的顶部。
// file variables are globals with some encapsulation
static tBoolean gLogOnPrivate;

警告: static关键字在不同场合下意义不一样,这让人感到有点懊恼。对于函数和函数外变量,这个关键字的意思是“把我隐藏起来,这样其他模块都看不到”以限制其作用域。对于函数内部的变量来说,static关键字的意思是在不同的调用之间保持值,在这个函数内部起着全局变量的作用。

一组松散的变量有点难以追踪,所以可以考虑将一个模块内部的私有变量封装到结构体中:
// contain all the global variables into a structure:
struct {
  tBoolean logOn;
  static enum eLogLevel outputLevel[NUM_LOG_SUBSYSTEMS];
} sLogStruct;
static struct sLogStruct gLogData;

如果想让C代码看起来像个对象,那么这个结构体就不应该是模块的一部分,而应该在初始化(对于日志系统来说,就是LogInit)的时候创建(分配内存),然后将其返回给调用函数:
struct sLogStruct* LogInit() {
  int i;
  struct sLogStruct *logData = malloc(sizeof(*logData));
  logData->logOn = FALSE;
  for (i=0; i < NUM_LOG_SUBSYSTEMS; i++) {
    logData-> outputLevel = eNoLogging;
  }
  return logData;
}

这样就可以像对象一样传递这个结构。当然,还需要增加一个方法去释放这个对象,这只需要在接口中增加一个函数就可以了。
模式:单例

要确保系统中每个部分都可以访问相同日志对象还有一种方法,那就是采用另外一种设计模式,这个模式称为“单例”。

当需要一个类有且仅有一个实例时,单例模式是很常用的。在一个面向对象语言中,单例负责解析创建对象的请求并保持其独立的状态。对资源的访问是全局的,但是所有的访问都必须经过这个唯一实例。单例类中没有公共构造函数。在C++中,类似这样:
class Singleton {
public:
  static Singleton* Instance() {
    if (mInstance == 0) {
      mInstance = new Singleton;
    }
    return mInstance;
  }
protected:
  Singleton(); // 除了这个类本身没有其他类可以创建这个实例
private:
  static Singleton* mInstance = 0;
}

对于日志系统来说,单例可以让整个系统通过唯一的实例来访问日志对象。通常,当有一个单一的资源(如串口)在系统的多个部分之间共享,单例可以比较容易地避免冲突。
在面向对象语言中,单例也允许延迟资源分配和初始化,这样那些从来没有使用的模块就不会消耗资源。

共享私有全局变量

即使在面向过程的语言(如C语言)中,单例的思想也有一席之地。面向对象设计的数据隐藏的优点已经在 《Object-Oriented Programming in C》中讨论过。保护一个模块的变量不被其他文件所修改(或者使用)可以让你的设计更健壮。

但是,有时候需要一个后门去访问这些私有信息,或者需要重用某块内存以实现其他功能(第8章)或者因为需要从外部代理去测试某个模块(第3章)。在C++中,可以使用友元类来访问这些隐藏的内部成员。

在C语言中,移除static关键字可以让模块变量变成真正的全局变量,我们可以采用另一种稍微欺骗的方法,那就是返回指向私有变量的指针:
static struct sLogStruct gLogData;
struct sLogStruct* LogInternalState() { 
  return &gLogData;
}

从保持封装性和数据隐藏的角度来说,这并不是一个很好的方法,因此不能滥用。在正式的开发过程中可以对其进行某些限制:
static struct sLogStruct gLogData;
struct sLogStruct* LogInternalState() {
#if PRODUCTION 
  #error "Internal state of logging protected!"
#else 
  return &gLogData;
#endif /* PRODUCTION */
}

在设计接口并考虑模块的状态信息时,有一点需要铭记于心,那就是需要一些方法对系统进行验证。

点击复制链接 与好友分享!回本站首页
分享到: 更多
您对本文章有什么意见或着疑问吗?请到论坛讨论您的关注和建议是我们前行的参考和动力  
上一篇:1.3 功能
下一篇:1.5 小结
相关文章
图文推荐
JavaScript网页动画设
1.9 响应式
1.8 登陆页式
1.7 主题式
排行
热门
文章
下载
读书

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训
版权所有: 红黑联盟--致力于做最好的IT技术学习网站