事件类型定义:用户输入事件、系统事件、定时器事件、自定义事件

事件类型,说白了就是给每个事件贴个标签。你想想看,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 事件类型总览图

下面这张图把四种事件类型的关系和流转路径画清楚了。你可以看到,所有事件最终都进入同一个事件队列,由事件分发器统一处理。

嵌入式GUI事件类型总览 用户输入事件 触摸 / 按键 / 编码器 系统事件 休眠 / 电量 / 初始化 定时器事件 单次 / 周期 / 软件定时 自定义事件 网络 / 传感器 / 蓝牙 统一事件队列(FIFO) 事件分发器(Dispatcher) 输入事件处理 系统事件处理 定时器事件处理 自定义事件处理 所有事件类型统一入队 → 分发器按类型路由 → 对应处理器执行回调

2.6 事件类型设计原则

最后,我总结几条在实际项目中踩过坑之后定下的规矩:

  1. 类型码要预留扩展空间:别从0开始排到255,中间留一些空档。我见过一个项目把事件类型码排满了,后来加新功能只能重构——那叫一个痛苦。
  2. 事件结构体尽量小:嵌入式环境内存金贵。能放uint8_t就别用uint32_t,能用位域就别用独立字段。
  3. 明确事件的所有权:事件入队后,原始数据是拷贝还是转移?这个必须在设计文档里写清楚。我曾经因为没约定好,导致一个事件数据被两个模块同时释放,double free直接hard fault。
  4. 事件处理不能阻塞:任何事件回调里都不能有while(1)等待、不能有超过1ms的延时。如果确实需要耗时操作,用状态机分步处理。

小技巧:我习惯在事件结构体里加一个uint8_t的debug字段,调试时填入文件名哈希值。这样出问题时,通过打印事件队列就能定位到是哪个模块产生的事件——省去了翻代码查来源的时间。