嵌入式微系统
上QQ阅读APP看书,第一时间看更新

1.3.3 消息机制

消息机制是一种常用的信息传递机制,外设中断或者节拍例行处理往往是消息的来源,通过预处理压缩提取特征值后,按类型和值组合成消息规定的格式发送到消息队列中保存。大循环从消息队列中读取消息(一般采用先进先出原则读取),根据消息的类型找到相应的事件处理函数,对消息值进行处理。

一般来说,我们把外设中断或者节拍例行处理产生消息这么一个事情,叫作“事件”,以PC为例,按下一次鼠标、键盘,都是一个事件,这样我们就可以理解为:事件产生消息,大循环获取消息执行相应的处理,所以消息机制也叫事件驱动编程机制(图1-11)。

图1-11 消息机制原理

节拍例行处理中扫描到按键,先分析按键是否正确,之后抛出消息在大循环中执行界面程序。那么大家可能会问,为什么不直接在中断中处理呢?一般建议在中断中尽可能少地处理事情,仅处理需要紧急处理的事情,比如检测到一个信号、接收一个数据等,因为中断有优先级,若当前中断优先级较高,需要处理这个事件,过长的处理时间会耽误其他优先级低的中断事件。一般地讲,捕获一个事件必须要高速,而处理这个事件往往不需要很高的速度。比如按键,需要及时准确捕获,但处理这个按键事件,因人对于菜单界面响应速度可以说是百毫秒级别的,还有因为菜单界面的数据量较大,尤其是彩屏,所以需要较长时间,这取决于屏幕的规格,都不能太快,所以可以放在大循环中执行(多媒体视频除外)。

可以说,消息机制是连接前后台架构三要素中断、节拍与大循环的纽带,它有效解决了各外部设备因为速度、时间、数据量等不同引起的处理及时性问题,协调了系统的运转。需要注意的是,消息只能在大循环中接收,不能在中断或者节拍中接收。相比于最简单的标记位,消息机制形式固定,结构清晰,可阅读性强,并且携带更多的信息量,也具有缓冲存储特点,方便了使用。

MS的消息采用16 bit数据形式,高八位为消息类型,低八位是消息值,两条宏定义用于读取16 bit消息的消息类型与消息值,这样就无需关心到底是高八位,还是低八位,直接用宏定义代替,方便阅读。

代码清单1-4:消息获取宏定义

#define GetMessageType(message)  UshortToByte1(message)         // 获取消息类型
#define GetMessageData(message)  UshortToByte0(message)         // 获取消息值

消息队列如同日常中的银行办事排队,银行的大厅就是整个数组,每个人就是一个消息,银行的规矩就是这个数据结构,它要求先来的先办,后来的顺序排队,这就是一种最简单的消息机制,先进先出,其工作步骤如图1-12所示。

图1-12 消息机制工作步骤

代码清单1-5:消息队列数据结构

#define MessageBufferSum 4                                // 消息队列深度
struct MessageQueue                                       // 定义一个队列结构体
{
    ushort  * Start;                                      // 指针指向队列开始
    ushort  * End;                                        // 指针指向队列结束
    ushort  * In;                                         // 指针插入一个消息
    ushort  * Out;                                        // 指针取出一个消息
    Byte       Size;                                      // 队列长度
    Byte       Entries;                                   // 消息长度
    ushort    Buffer[MessageBufferSum];                   // 队列存储数组
}
static struct MessageQueue idata MessageQueue;            // 定义消息队列实体

消息使用前,必须对消息队列进行初始化,代码如下。

代码清单1-6:消息队列初始化

void InitMessageQueue(void)
{
     MessageQueue.Start = MessageQueue.Buffer;                            // 队列头
     MessageQueue.End = MessageQueue.Buffer+MessageBufferSum-1;           // 队列尾
     MessageQueue.In = MessageQueue.Start;                                // 插入点
     MessageQueue.Out = MessageQueue.Start;                               // 取出点
     MessageQueue.Size = MessageBufferSum;                                // 队列长度
     MessageQueue.Entries = 0;                                            // 消息长度
}

消息使用前,必须定义消息类型,用一个枚举定义了各种消息类型,代码如下。

代码清单1-7:消息类型枚举

typedef enum
{
    MessageKey      = 0xFF,                        // 按键消息类型
    MessageUsart    = 0xFE,                        // 串口消息类型 
    MessageInt0     = 0xFD,                        // 外部中断0消息类型
    MessageInt1     = 0xFC,                        // 外部中断1消息类型
    // 请填充类型
    MessageTimer    = 0xF0                         // 最小值,不允许其他类型小于它
MessageEnum;
}

发送消息需要把8 bit消息类型和8 bit消息值合并成16 bit类型,再发送到消息队列中去。考虑到兼容软件定时器为特殊类型消息的情况,消息定义从0xFF开始定义,从高向低定义,最低不得低于MessageTimer消息。默认允许16个消息类型,当需要更多消息类型时,把MessageTimer类型值降低。之所以这么做,是因为软件定时器Timer消息类型需要传递一个16 bit的地址,而消息整个的长度也是16 bit,这样就无法再来用消息类型和消息值这种方式了,常规的做法:一种是发送两条定时器消息,把16 bit的地址分两次发送;另一种是设计一个静态全局地址变量,临时存放,发送一条Timer消息之后,从这个地址变量中取16 bit地址即可。MS采用一种全新的设计思路,因为考虑到只有Timer才需要16 bit消息值,因为它的唯一性,把等于或者低于MessageTimer类型值的类型,都看作是Timer消息的高八位值,这样跟原来的消息值组成了16 bit。因为消息类型实际使用非常有限,往往只需要几个。因此,只要程序大小不大于0xF000,也就是61K,就不会出现任何冲突,而超过61K的MCU51程序,非常非常之少。一旦真的超过,又必须要使用时,请采用前面说的两种办法进行修改即可。MS针对Timer采用特殊处理带来的好处是,消息的长度统一,这部分请读者仔细理解。

消息源产生消息,需要发送消息到消息队列中,发送消息不能被其他中断打断,以免引起混乱。发送消息代码如下。

代码清单1-8:发送消息

void PostMessage(MessageEnum message, ushort value)
{
    EnterCritical();                                                // 进入临界态
    Assert(MessageQueue.Entries <= MessageQueue.Size);              // 断言,判断是否满
    if(message == MessageTimer)                                     // 软件定时器类型
        *MessageQueue.In = value;                                   // 直接赋16 bit地址
    else                                                            // 常规消息
    {
        UshortToByte1(*MessageQueue.In) = messageType;              // 把类型赋给高地址
        UshortToByte0(*MessageQueue.In) = value & 0xFF;             // 把值赋给低地址
    }
    MessageQueue.In++;                                              // 消息入口加一
    if(MessageQueue.In > MessageQueue.End)                          // 入口越界循环
        MessageQueue.In = MessageQueue.Start;
    MessageQueue.Entries++;                                         // 消息数量加一
    ExitCritical();                                                 // 退出临界态
}

EnterCritical()与ExitCritical()函数总是成对出现的,这是为了保证在整个发送消息的过程中,不被中断打断。因为发送消息分好多步,这个过程中一旦有中断进来,而这个中断也要发送消息,可能会导致消息的顺序等产生其他异常,不可预测,这种情况叫临界态,EnterCritical是进入临界态保护,保证中断关闭,ExitCritical是退出临界态,恢复原状态。

Assert为断言,其实就是一条宏指令,是自己定义的,用于判断条件是否正确,假设Entries<Size,说明消息队列没有满,可以继续执行;若这个条件不满足,就会导致断言成功,打印出错误信息。断言是一种简单、有效的缺陷捕获机制,可以简化编程。出厂前测试常用断言,让机器长期运行,经历各种状态,压力测试,一旦出现异常就可以捕获。出厂时,建议屏蔽断言,用(void)0空操作代替即可。因为出厂之后,一旦出现异常,导致致命情况是不允许的。

代码清单1-9:断言宏原型

#define Assert(express)  if (!(express)) {printf("\nAssert: " #express "\n"); while(1);}

if(messageType==MessageTimer),判断是否为软件定时器消息,软件定时器消息是唯一特殊消息,当等于或者小于这个消息类型,传递的消息就是这个软件定时器16 bit的地址信息;当大于Timer消息类型时,就是常规的填充消息。

等待消息比较简单,先检测消息队列是否存在消息,若没有,一直检测,若需要看门狗喂狗,建议在这个检测中添加;当发现存在消息时,读取即可。在等待的过程中,可以喂狗,防止系统崩溃,自动重启。代码如下。

代码清单1-10:等待消息

ushort PendMessageQueue(void)
{
      ushort message;
      while(MessageQueue.Entries == 0)                              // 等待消息
      { /* 推荐在这里喂狗 */ };
      message = *MessageQueue.Out++;                                // 消息出口加一
      MessageQueue.Entries--;                                       // 消息数量减一
      if (MessageQueue.Out > MessageQueue.End)                      // 出口越界循环
          MessageQueue.Out = MessageQueue.Start;
      return(message);                                              // 返回消息
}

消息机制除了用于连接中断、节拍、大循环三者解决高速响应与低速执行的矛盾外,消息机制还可以灵活应用,比如可以把消息理解为一个连接符,那么可以用它来把一些执行时间长的事件分解为短的几块。以录音为例,在存储时,又有按键响应需要关闭存储,存储时间因为较长,会产生一种按键失效的错觉,此时可以利用消息机制有效地避免这个问题,可以把存储分解为好几段,各段存储完后,发出存储下一段的消息,用消息把各个小段连接起来,这样当有按键事件产生时,可将其插入到各段的消息之中,因为按键被提前了,所以提高了关闭的响应速度。消息机制还可以用于解决函数嵌套的深度问题,有些函数调用嵌套过深,会导致内存消耗过大而编译失败,这时可以通过消息机制,把一个很长的函数调用分解为多个很短的函数调用,并且入口都在大循环main函数里面。从而有效地解决了内存的问题。此法在MCU51下特别有效,当初调用一个别人提供的CH375 U盘存储库文件,这个库文件本身函数嵌套已很深了,而需要调用这个函数的是一个菜单目录也较深的界面,所以导致内存不够用,后来采用在调用菜单位置抛出一个消息,在大循环中调用U盘库,这个问题得以完美解决。

消息机制很清晰地把中断、节拍和大循环进行分工,通过消息连接起来,其好处是保证了中断、节拍不会修改大循环中的变量,这样不会出现变量的临界态问题,提高了系统的稳定性,关于临界态问题,后续有讲解。