接下来再说一说这个消息机制在程序中如何实现。在程序中,消息机制可以看做是一个独立的功能模块,一个功能模块的实现无非就是数据结构+算法。先来看消息机制的数据结构。这里的数据结构是指和消息机制有关的数据组织形式,包含 2 个部分: - 消息节点自身的数据组织形式
- 消息缓冲区的数据组织形式
程序清单 List10 所示就是消息机制的数据结构 。 「程序清单List10:」 typedef union msg_arg /*消息参数共用体*/
{
INT8U u8Arg; /*成员:8 位无符号*/
INT8U s8Arg; /*成员:8 位有符号*/
#if CFG_MSG_ARG_INT16_EN>0
INT16U u16Arg; /*可选成员:16 位无符号*/
INT16S s16Arg; /*可选成员:16 位有符号*/
#endif
#if CFG_MSG_ARG_INT32_EN>0
INT32U u32Arg; /*可选成员:32 位无符号*/
INT32S s32Arg; /*可选成员:32 位有符号*/
#endif
#if CFG_MSG_ARG_FP32_EN>0
FP32 f32Arg; /*可选成员:32 位单精度浮点*/
#endif
#if CFG_MSG_ARG_PTR_EN>0
void* pArg; /*可选成员:void 指针*/
#endif
}MSG_ARG;
typedef struct _msg /*消息结构体*/
{
INT8U u8MsgID; /*消息 ID*/
#if CFG_MSG_USR_SUM > 1
INT8U u8UsrID; /*消费者 ID*/
#endif
MSG_ARG uMsgArg; /*应用消息参数*/
} MSG;
typedef struct msg_box /*消息缓冲区结构体*/
{
INT8U u8MBLock; /*队列上锁标识*/
INT8U u8MsgSum; /*队列长度*/
INT8U u8MQHead; /*队列头结点位置*/
INT8U u8MQTail; /*队列尾节点位置*/
MSG arMsgBox[CFG_MSG_SUM_MAX]; /*存放队列的数组*/
} MB;
static MB g_stMsgUnit; /*消息管理单元全局变量*/
消息的数据结构包含 2 部分:消息头和消息参数,在消息结构体 MSG 中, u8MsgID 和u8UsrID 就是消息头,共用体 MSG_ARG 就是消息参数。 u8MsgID 是消息的类型标志,也就是生成此消息的事件的事件类型标志,程序根据这个成员选择对应的事件处理函数;u8UsrID 是消息的消费者代号, 如果应用代码中只有一个消费者,则成员 u8UsrID 可以忽略。MSG_ARG 就是消息附带的参数,也就是事件的内容信息。 系统中的事件是多种多样的,有的事件只需要类型标志即可, 有的事件可能还需要整型变量存储事件内容, 还有的事件可能需要大块的内存来存储一些附带的数据。为了将各种类型的事件生成的消息纳入统一管理, 要求 MSG_ARG 必须能存储各种类型的数据,因此 MSG_ARG 被定义成了共用体。 从程序清单 List10 中可以看出, MSG_ARG 既可以存储 8 位~32 位有符号无符号整型数据,又可以存储单精度浮点, 还可以存储 void* 型的指针变量, 而 void*的指针又可以强制转换成任意类型的指针,所以 MSG_ARG 可以存储指向任意类型的指针。 对于MSG_ARG中的某些成员, 还配备了预编译常量 CFG_MSG_ARG_XXX_EN加以控制,如果实际应用中不需要这些耗费内存较大的数据类型, 可以设置CFG_MSG_ARG_XXX_EN 去掉它们。全开的情况下, 每个消息节点占用 6 个字节的内存, 最精简的情况下, 每个消息节点只占用 2 个字节。 全局结构体变量 g_stMsgUnit 是消息缓冲区的数据结构。消息缓冲区是一个环形缓冲队列,这里将环形队列放在了一个一维数组中,也就是g_stMsgUnit 的成员 arMsgBox[],数组元素的数据类型就是消息结构体 MSG ,数组的大小由预编译常量 CFG_MSG_SUM_MAX 控制,该常量是环形缓冲队列的最大容量。 理论上, CFG_MSG_SUM_MAX 值的选取越大越好,但考虑到单片机的 RAM 资源有CFG_MSG_SUM_MAX 值的选取要在资源消耗和实际最大需求之间折中, 只要能保证在最坏情况下环形缓冲队列仍有裕量即可。用数组实现环形队列还需要一些辅助变量,也就是 g_stMsgUnit 剩余的成员。 u8MBLock 是队列的控制变量, u8MBLock>0 表示队列处于锁定/保护状态,不能写也不能读, u8MBLock=0 表示队列处于正常状态,可写可读;u8MsgSum 是队列长度计数器,记录着当前队列中存有多少条消息,存入一条消息u8MsgSum++,读出一条消息 u8MsgSum--;
u8MQHead 记录着当前队头消息节点在数组 arMsgBox[]中的位置,其值就是数组元素的下标,消息读取的时候所读出的就是 u8MQHead 所指向的节点,读完之后 u8MQHead 向队尾方向移动一个位置,指向新的队头节点;u8MQTail 记录着当前队尾消息节点在数组 arMsgBox[]中的位置,其值是数组元素的下标,新消息写入之前, u8MQTail 向队尾方向后移一个位置, 然后新写入的消息存入 u8MQTail 所指向的空闲节点; 图 8 所示为消息缓冲区结构体变量 g_stMsgUnit 的示意图 。
有了数据结构,还要有对应的算法实现,消息机制的数据主体就是一个数组化了的环形队列,环形队列的算法就是我们所要的算法。消息机制是一个独立的功能模块,应该对外屏蔽其内部实现细节,而仅对外界开放一定数量的接口函数,外界通过调用这些接口来使用消息功能,这也就是我在声明 g_stMsgUnit 变量的时候使用了 static 关键词的原因。 消息模块的接口函数一共有 9 个: - void mq_init(void) 消息队列初始化,负责初始化 g_stMsgUnit 。
- void mq_clear(void) 清空消息队列,效果同 mq_init(),可有可无。
- void mq_lock(void) 消息队列锁定,锁定的消息队列不可读不可写。
- void mq_unlock(void) 消息队列解锁,解锁后消息队列恢复正常功能。
- BOOL mq_is_empty(void) 消息队列判空,返回 TRUE 表示消息队列当前为空,返回 FALSE 表示有消息存储。
- INT8U mq_get_msg_cur_sum(void) 查询消息队列中当前存储的消息总数,函数返回值为查询结果
- INT8U mq_get_msg_sum_max(void) 查询消息队列的最大容量,函数返回值为查询结果。
- INT8U mq_msg_post_fifo(MSG* pMsg) 向消息队列中寄送消息,方式为先入先出,形参 pMsg 指向消息的备份内存,函数返回操作结果。该函数多被 ISR 调用,所以必须为可重入函数。
- INT8U mq_msg_req_fifo(MSG* pMsg) 从消息队列中读取消息, 方式为先入先出, 函数将读出的消息存入形参 pMsg 指向的内存,函数返回操作结果。该函数被主程序调用, 可以不是可重入函数, 但要对共享数据进行临界保护 。
事件/消息驱动机制是一个标准的通用的框架,配合 ISR,对任何系统输入都能应对自如。事件/消息驱动机制屏蔽了应用层程序获取各种系统输入的工作细节,将系统输入抽象整合, 以一种标准统一的格式提交应用代码处理, 极大地减轻了应用层代码获取系统输入的负担, 应用层只需要专注于高级功能的实现就可以了。
|