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