4. 事件数据结构:结构体、枚举、参数传递与优先级
事件数据结构,说白了就是嵌入式GUI的"快递单"。你得把事件从产生到消费的每个环节都包装好。我刚开始做GUI时,觉得事件嘛,不就是个按键值吗?后来发现,事件里要带的东西远比想象的多。
4.1 事件结构体定义
先看一个我常用的基础结构体。嗯,这里要注意,结构体设计直接影响后续扩展性。
typedef struct {
uint32_t id; // 事件ID,唯一标识
uint16_t type; // 事件类型
uint16_t priority; // 优先级,0最高
uint32_t timestamp; // 时间戳,单位ms
uint16_t param_size; // 参数长度
void* param; // 参数指针
uint8_t reserved[8]; // 保留字段,对齐用
} Event_t;
为什么要有 reserved 字段?我在项目里吃过亏。一开始没留保留位,后来要加个"事件来源"字段,结果所有结构体都得改,牵一发动全身。留8个字节,够你折腾好几年。
个人习惯:结构体大小尽量对齐到4字节或8字节边界。ARM Cortex-M系列对未对齐访问会触发异常,或者降低性能。Event_t 大小正好32字节,完美对齐。
4.2 事件类型枚举
事件类型用枚举定义,清晰又安全。我一般把类型分成几个大类:
typedef enum {
// 输入事件
EVT_KEY_PRESS = 0x0101,
EVT_KEY_RELEASE = 0x0102,
EVT_TOUCH_DOWN = 0x0201,
EVT_TOUCH_MOVE = 0x0202,
EVT_TOUCH_UP = 0x0203,
// 系统事件
EVT_TIMER_EXPIRED = 0x1001,
EVT_REFRESH = 0x1002,
EVT_SLEEP = 0x1003,
// 用户自定义事件
EVT_USER_BASE = 0x8000
} EventType_t;
你想想看,为什么用 0x0101 而不是 0x01?高字节表示大类,低字节表示具体动作。这样调试时看十六进制值,一眼就知道是按键还是触摸。我曾经接手过一个项目,事件类型从0排到255,每次查bug都得翻文档,太痛苦了。
| 大类 | 范围 | 说明 |
|---|---|---|
| 按键事件 | 0x0100 - 0x01FF | 物理按键、矩阵键盘 |
| 触摸事件 | 0x0200 - 0x02FF | 电容屏、电阻屏 |
| 系统事件 | 0x1000 - 0x10FF | 定时器、刷新、休眠 |
| 用户事件 | 0x8000 - 0xFFFF | 应用层自定义 |
4.3 事件参数传递
参数传递是个坑。直接传值?还是传指针?我两种都用过,说说经验。
4.3.1 值传递
适合小数据,比如按键码、坐标值。直接把数据塞进结构体里。
typedef struct {
uint16_t key_code; // 按键值
uint8_t repeat; // 是否重复
} KeyParam_t;
// 使用
Event_t evt;
evt.type = EVT_KEY_PRESS;
evt.param_size = sizeof(KeyParam_t);
evt.param = &key_param; // 指向栈上变量
注意:千万别传栈上变量的指针!我曾经犯过这个错——事件处理时,栈变量已经被释放了,读出来的全是垃圾数据。要么用静态变量,要么用动态内存。
4.3.2 动态内存传递
大数据或不确定长度的参数,用动态分配。我习惯这样:
Event_t* Event_Create(uint16_t type, void* data, uint16_t size) {
Event_t* evt = (Event_t*)malloc(sizeof(Event_t));
if (!evt) return NULL;
evt->type = type;
evt->param_size = size;
evt->param = malloc(size);
if (evt->param) {
memcpy(evt->param, data, size);
}
evt->timestamp = GetSysTick();
return evt;
}
void Event_Delete(Event_t* evt) {
if (evt->param) free(evt->param);
free(evt);
}
说白了,就是谁分配谁释放。事件消费者用完参数后,必须调用 Event_Delete。否则内存泄漏等着你。
4.4 事件优先级
优先级决定了事件处理的顺序。我见过两种主流方案:
- 固定优先级:每个事件类型绑定一个优先级,简单粗暴。
- 动态优先级:根据上下文调整,比如用户正在拖拽时,触摸事件优先级自动提高。
我个人倾向固定优先级+少量动态调整。为什么呢?动态优先级容易导致优先级反转,调试起来头大。
// 优先级定义
#define PRIO_CRITICAL 0 // 紧急事件,如看门狗
#define PRIO_HIGH 1 // 用户交互事件
#define PRIO_NORMAL 2 // 定时器、刷新
#define PRIO_LOW 3 // 后台任务、日志
// 优先级队列插入
bool EventQueue_Push(Event_t* evt) {
// 按优先级插入,同优先级FIFO
EventNode_t* node = queue.head;
while (node && node->evt->priority <= evt->priority) {
node = node->next;
}
// 插入到node之前
...
}
核心原则:优先级0的事件,处理时间必须可控。我曾经把看门狗事件设为优先级0,结果处理函数里有个死循环...嗯,板子直接重启了。优先级越高,代码越要精简。
4.5 知识体系总览
下面这张图,把事件数据结构的核心逻辑串起来了。你看一遍,应该能有个整体印象。
从事件产生到最终被处理,数据流是单向的。每一步都有明确的职责。结构体负责打包,枚举负责分类,参数传递负责搬运,优先级负责排序。各司其职,系统才不乱。
我的建议:刚开始做GUI时,别急着搞复杂的优先级策略。先把结构体和枚举定义清楚,用最简单的FIFO队列跑通。等系统稳定了,再考虑优先级优化。迭代开发,比一步到位靠谱得多。