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 事件优先级

优先级决定了事件处理的顺序。我见过两种主流方案:

  1. 固定优先级:每个事件类型绑定一个优先级,简单粗暴。
  2. 动态优先级:根据上下文调整,比如用户正在拖拽时,触摸事件优先级自动提高。

我个人倾向固定优先级+少量动态调整。为什么呢?动态优先级容易导致优先级反转,调试起来头大。

// 优先级定义
#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 知识体系总览

下面这张图,把事件数据结构的核心逻辑串起来了。你看一遍,应该能有个整体印象。

事件数据结构核心逻辑 事件产生 按键 / 触摸 / 定时器 结构体封装 id / type / priority timestamp / param 参数传递 值传递 / 指针传递 动态内存管理 优先级队列 0(最高) → 3(最低) 事件类型枚举 0x0101 ~ 0xFFFF 优先级规则 固定 + 动态调整 常见坑点 栈指针失效 内存泄漏

从事件产生到最终被处理,数据流是单向的。每一步都有明确的职责。结构体负责打包,枚举负责分类,参数传递负责搬运,优先级负责排序。各司其职,系统才不乱。

我的建议:刚开始做GUI时,别急着搞复杂的优先级策略。先把结构体和枚举定义清楚,用最简单的FIFO队列跑通。等系统稳定了,再考虑优先级优化。迭代开发,比一步到位靠谱得多。