事件类型定义:用户输入事件、系统事件、定时器事件、自定义事件
事件类型,说白了就是给每个事件贴个标签。你想想看,GUI系统里同时跑着触摸、按键、定时刷新、网络数据……如果没有分类,事件处理函数就得写一堆if-else,维护起来简直要命。
我个人习惯,在设计事件系统时先把类型分清楚。这就像文件柜里的文件夹——分好了类,找东西才快。今天咱们就把这四种核心事件类型掰开揉碎了讲。
2.1 用户输入事件
这类事件来自人和设备的交互。触摸屏、按键、鼠标滚轮、旋钮……都属于这个范畴。我在做工业手持设备时遇到过一个问题:触摸事件和按键事件的处理优先级没定好,结果用户按物理按键时,触摸中断也触发了,导致误操作。
用户输入事件通常包含以下信息:
- 事件类型码:TOUCH_DOWN、TOUCH_MOVE、TOUCH_UP、KEY_PRESS、KEY_RELEASE
- 坐标或键值:触摸点的x/y坐标,或者按键的扫描码
- 时间戳:事件发生的系统滴答数,用于去抖和长按检测
核心原则:用户输入事件必须携带硬件原始信息,不要提前做“语义化”处理。比如触摸事件只上报坐标,不要自己判断“用户是不是想点这个按钮”——那是事件路由层的事。
/* 用户输入事件结构体示例 */
typedef enum {
EVT_TOUCH_DOWN = 0x10,
EVT_TOUCH_MOVE,
EVT_TOUCH_UP,
EVT_KEY_PRESS,
EVT_KEY_RELEASE,
EVT_ENCODER_TURN
} input_event_type_t;
typedef struct {
input_event_type_t type; /* 事件类型 */
uint16_t x; /* 触摸X坐标 / 编码器位置 */
uint16_t y; /* 触摸Y坐标 */
uint8_t key_code; /* 按键扫描码 */
uint32_t timestamp; /* 系统滴答数 */
} input_event_t;
2.2 系统事件
系统事件是GUI框架内部产生的,或者由操作系统/底层驱动上报的。比如屏幕旋转、休眠唤醒、电量低、内存不足……这些事件不直接来自用户,但会影响UI的行为。
我记得有一次做智能家居面板,系统事件没处理好。设备在低电量时应该弹出提示框,但事件处理函数里直接调用了malloc分配内存——结果内存不足,系统直接挂了。嗯,这里要注意:系统事件的处理函数里,千万别做可能失败的操作。
| 系统事件类型 | 触发时机 | 典型处理 |
|---|---|---|
| EVT_SYS_INIT | GUI系统初始化完成 | 加载默认界面、启动定时器 |
| EVT_SYS_SLEEP | 系统即将进入低功耗 | 保存界面状态、关闭背光 |
| EVT_SYS_WAKEUP | 系统从休眠中唤醒 | 恢复界面、刷新显示 |
| EVT_SYS_LOW_BATTERY | 电量低于阈值 | 弹出警告、限制动画 |
| EVT_SYS_LANGUAGE_CHANGE | 用户切换语言 | 重新加载字符串资源 |
避坑指南:我曾经在系统事件回调里直接调用了GUI刷新函数,结果造成了递归调用——系统事件触发刷新,刷新过程中又产生了系统事件。解决方案是使用“延迟处理”机制:系统事件只设置标志位,在主循环里统一处理。
2.3 定时器事件
定时器事件是嵌入式GUI里最常用的“心跳”。动画、闪烁光标、自动翻页、长按检测……都离不开它。说白了,定时器事件就是告诉系统:“时间到了,该干活了。”
我一般把定时器分为三类:
- 单次定时器:触发一次后自动停止,适合超时提示、延迟跳转
- 周期定时器:每隔固定时间触发一次,适合动画帧更新、光标闪烁
- 软件定时器:基于系统滴答计数,不占用硬件资源,适合短周期任务
/* 定时器事件结构体示例 */
typedef struct {
uint8_t timer_id; /* 定时器ID,用于区分多个定时器 */
uint8_t repeat_count; /* 剩余触发次数,0xFF表示无限循环 */
uint32_t interval_ms; /* 触发间隔(毫秒) */
uint32_t elapsed_ms; /* 已过去的时间 */
} timer_event_t;
/* 创建周期定时器示例 */
timer_handle_t htimer = timer_create(100, TIMER_REPEAT_INFINITE);
timer_set_callback(htimer, on_animation_tick);
timer_start(htimer);
经验之谈:定时器回调函数要尽量短。我在一个项目里把复杂的波形绘制逻辑直接放在定时器回调里,结果导致其他事件响应延迟。正确的做法是:定时器回调只设置“需要更新”的标志位,真正的绘制工作放在主循环里做。
2.4 自定义事件
自定义事件是给开发者留的“后门”。框架预定义的事件类型不够用时,你可以自己定义。比如网络数据到达、传感器数值变化、蓝牙连接状态变更……这些都可以封装成自定义事件。
自定义事件的设计要点:
- 类型码从用户区域开始:比如0x80以上留给用户自定义
- 携带数据指针:事件结构体里留一个void*指针,指向自定义数据块
- 明确生命周期:事件处理完后,谁负责释放数据内存?这个必须约定清楚
/* 自定义事件结构体示例 */
#define EVT_USER_BASE 0x80
typedef enum {
EVT_USER_NET_DATA_ARRIVED = EVT_USER_BASE,
EVT_USER_SENSOR_UPDATE,
EVT_USER_BT_CONNECTED,
EVT_USER_BT_DISCONNECTED
} user_event_type_t;
typedef struct {
uint8_t event_type; /* 自定义事件类型码 */
void *data; /* 指向自定义数据 */
uint16_t data_len; /* 数据长度 */
uint8_t free_data; /* 处理完后是否需要释放data */
} user_event_t;
2.5 事件类型总览图
下面这张图把四种事件类型的关系和流转路径画清楚了。你可以看到,所有事件最终都进入同一个事件队列,由事件分发器统一处理。
2.6 事件类型设计原则
最后,我总结几条在实际项目中踩过坑之后定下的规矩:
- 类型码要预留扩展空间:别从0开始排到255,中间留一些空档。我见过一个项目把事件类型码排满了,后来加新功能只能重构——那叫一个痛苦。
- 事件结构体尽量小:嵌入式环境内存金贵。能放uint8_t就别用uint32_t,能用位域就别用独立字段。
- 明确事件的所有权:事件入队后,原始数据是拷贝还是转移?这个必须在设计文档里写清楚。我曾经因为没约定好,导致一个事件数据被两个模块同时释放,double free直接hard fault。
- 事件处理不能阻塞:任何事件回调里都不能有while(1)等待、不能有超过1ms的延时。如果确实需要耗时操作,用状态机分步处理。
小技巧:我习惯在事件结构体里加一个uint8_t的debug字段,调试时填入文件名哈希值。这样出问题时,通过打印事件队列就能定位到是哪个模块产生的事件——省去了翻代码查来源的时间。