1. 按键事件驱动LED状态机
任务说明
本案例要求实现一个最小可运行的事件循环框架,支持事件注册、事件分发和回调执行三个核心能力。事件循环是事件驱动架构的基石:程序不主动轮询状态,而是注册感兴趣的事件及对应回调,随后进入循环等待事件发生,事件到达时由循环负责调用已注册的回调函数。完成本案例后,应能直观理解“注册—等待—分发—回调”这一基本运转机制。
核心设计
事件循环需要解决三个问题:事件如何表示、回调如何注册、循环如何等待并分发。本案例采用以下最小设计:
- 事件:用整数类型
event_id标识事件类型,事件载荷用void*传递,保持通用性。 - 注册表:使用固定大小的数组存储事件ID到回调函数的映射,每个事件ID最多注册一个回调(简化处理,避免链表管理)。
- 事件队列:使用环形缓冲区暂存待处理事件,事件循环从队列头部取出事件,查找注册表并调用对应回调。
- 触发方式:提供
event_loop_post接口向队列投递事件,模拟异步事件到达;循环运行后按顺序处理队列中所有事件。
该设计覆盖了事件驱动的基本要素,且代码量小、无外部依赖,便于独立验证。
代码实现
// event_loop.h
#ifndef EVENT_LOOP_H
#define EVENT_LOOP_H
#include <stdint.h>
#define MAX_EVENT_TYPES 16 // 支持的最大事件类型数
#define QUEUE_CAPACITY 8 // 事件队列容量
typedef uint16_t event_id_t;
typedef void (*event_handler_t)(event_id_t id, void* data);
typedef struct {
event_id_t id;
void* data;
} event_t;
typedef struct {
event_handler_t handlers[MAX_EVENT_TYPES]; // 事件ID到回调的映射表
event_t queue[QUEUE_CAPACITY]; // 环形事件队列
int head; // 队列头索引
int tail; // 队列尾索引
int count; // 队列中事件数量
} event_loop_t;
void event_loop_init(event_loop_t* loop);
int event_loop_register(event_loop_t* loop, event_id_t id, event_handler_t handler);
int event_loop_post(event_loop_t* loop, event_id_t id, void* data);
void event_loop_run(event_loop_t* loop); // 处理队列中所有事件,队列空时返回
#endif // EVENT_LOOP_H
// event_loop.c
#include "event_loop.h"
#include <string.h>
void event_loop_init(event_loop_t* loop) {
memset(loop, 0, sizeof(event_loop_t));
}
int event_loop_register(event_loop_t* loop, event_id_t id, event_handler_t handler) {
if (id >= MAX_EVENT_TYPES || handler == NULL) {
return -1; // 参数非法
}
loop->handlers[id] = handler;
return 0;
}
int event_loop_post(event_loop_t* loop, event_id_t id, void* data) {
if (loop->count >= QUEUE_CAPACITY) {
return -1; // 队列已满
}
loop->queue[loop->tail].id = id;
loop->queue[loop->tail].data = data;
loop->tail = (loop->tail + 1) % QUEUE_CAPACITY;
loop->count++;
return 0;
}
void event_loop_run(event_loop_t* loop) {
while (loop->count > 0) {
event_t ev = loop->queue[loop->head];
loop->head = (loop->head + 1) % QUEUE_CAPACITY;
loop->count--;
if (ev.id < MAX_EVENT_TYPES && loop->handlers[ev.id] != NULL) {
loop->handlers[ev.id](ev.id, ev.data);
}
// 未注册回调的事件被静默丢弃
}
}
// main.c
#include <stdio.h>
#include "event_loop.h"
// 两个示例事件ID
#define EV_BUTTON_PRESS 1
#define EV_TIMER_TICK 2
// 示例回调函数
void on_button_press(event_id_t id, void* data) {
int btn = *(int*)data;
printf("[回调] 按钮事件 id=%d, 按键编号=%d\n", id, btn);
}
void on_timer_tick(event_id_t id, void* data) {
int count = *(int*)data;
printf("[回调] 定时器事件 id=%d, 当前计数=%d\n", id, count);
}
int main(void) {
event_loop_t loop;
event_loop_init(&loop);
// 注册事件回调
event_loop_register(&loop, EV_BUTTON_PRESS, on_button_press);
event_loop_register(&loop, EV_TIMER_TICK, on_timer_tick);
// 模拟产生事件(实际系统中由中断/其他线程调用 post)
int btn_num = 3;
int tick_count = 100;
event_loop_post(&loop, EV_BUTTON_PRESS, &btn_num);
event_loop_post(&loop, EV_TIMER_TICK, &tick_count);
event_loop_post(&loop, EV_BUTTON_PRESS, &btn_num); // 同一事件再次触发
// 运行事件循环,处理队列中所有事件
event_loop_run(&loop);
// 队列已空,循环返回
printf("事件循环处理完毕,队列剩余事件数=%d\n", loop.count);
return 0;
}
编译与运行
将上述三个文件置于同一目录,执行以下命令编译:
gcc -Wall -o event_demo main.c event_loop.c
运行程序:
./event_demo
验证方法
程序运行后,观察标准输出。根据代码逻辑,事件队列中依次投递了3个事件:按钮事件(数据3)、定时器事件(数据100)、按钮事件(数据3)。事件循环按先进先出顺序取出并分发,因此预期输出为:
[回调] 按钮事件 id=1, 按键编号=3
[回调] 定时器事件 id=2, 当前计数=100
[回调] 按钮事件 id=1, 按键编号=3
事件循环处理完毕,队列剩余事件数=0
判定标准:
- 三个回调均被正确调用,且打印顺序与投递顺序一致,说明事件队列的FIFO行为正确;
- 同一事件ID(按钮事件)被多次投递时,每次都能触发回调,说明注册表可重复使用;
- 最后一行输出队列剩余事件数为0,说明循环正确消费了所有事件并正常退出。
如需进一步验证未注册事件的处理,可在 main 中投递一个未注册的事件ID(如 99),程序应静默跳过且不崩溃,队列计数正常递减。
事件循环工作机制
关键机制说明
本实现中,event_loop_run 采用“处理完当前队列中所有事件后返回”的批处理模式,而非无限阻塞等待。这种设计便于在裸机环境或单线程测试中验证框架正确性。在实际嵌入式系统中,可将 run 放入主循环,或由调度器在空闲时调用;事件投递则来自中断服务程序或其他任务。post 函数在单线程模型下无需加锁,若需支持多线程/中断并发投递,需在 post 与 run 的队列操作处增加临界区保护,这属于后续案例的扩展范围。