3. 嵌入式系统中的事件类型

事件驱动架构的核心,说白了就是「对发生的事情做出反应」。那嵌入式系统里到底会发生哪些事?我做了十几年嵌入式开发,接触过的项目少说也有几十个,总结下来,事件类型基本就这五类。咱们一个一个聊。

核心观点: 理解事件类型,是设计事件驱动架构的第一步。你连对手是谁都不知道,怎么排兵布阵?

3.1 外部中断事件

这是最「硬」的一类事件。外部设备通过芯片的物理引脚,直接触发 CPU 中断。比如按键按下、电平跳变、边沿触发。

我记得刚入行时做智能家居面板,按键扫描用的是轮询。产品上市后客户投诉反应慢,尤其在多任务场景下。后来改成外部中断触发,问题立刻解决。你想想看,轮询就像你每隔一秒去门口看有没有快递,中断则是快递员按门铃叫你。哪个效率高?

实战建议: 外部中断服务函数里,只做最轻量的事——比如设置一个标志位,或者往队列里扔一个事件。千万别在里面做复杂运算或延时。我见过有人直接在 ISR 里跑 printf,结果系统直接挂掉。

典型应用场景:

  • 按键/开关状态变化
  • 外部传感器触发(如红外感应)
  • 电源掉电检测
  • 机械限位开关触发

3.2 定时器事件

定时器事件是嵌入式系统的「心跳」。没有它,很多周期性任务就没法做。比如每 10ms 采集一次传感器数据,每 100ms 刷新一次屏幕,每 1s 上报一次设备状态。

我个人习惯把定时器事件分为两类:

类型 特点 典型用途
硬件定时器 精度高,占用硬件资源 PWM 生成、精确延时、电机控制
软件定时器 基于系统滴答,数量灵活 超时检测、周期性任务调度、看门狗喂狗

这里有个坑。我曾经在一个项目中,同时开了 20 多个软件定时器,结果发现系统响应越来越慢。排查后发现,定时器回调函数里做了太多事情,导致事件队列被塞满。后来我定了个规矩:定时器回调里只发事件,不处理事件。问题迎刃而解。

避坑指南: 我曾经在定时器回调里直接调用阻塞函数,导致整个系统滴答被卡住。记住,定时器回调不是普通函数,它运行在中断上下文。别在里面做任何可能阻塞的操作。

3.3 传感器数据就绪事件

这类事件在 IoT 设备里特别常见。传感器完成一次采集后,通过中断或 DMA 通知 CPU:「数据准备好了,快来取。」

为什么要把这个单独列出来?因为传感器数据就绪事件有个特点:它往往伴随着数据量。不像按键事件只传一个键值,传感器事件可能需要传递一个结构体,包含温度、湿度、时间戳等信息。

我建议的做法是:

  1. 传感器 ISR 中只设置「数据就绪」标志
  2. 主循环或任务中检测到标志后,读取传感器数据
  3. 将数据封装成事件结构体,放入事件队列
  4. 事件处理器根据数据类型分发处理
// 传感器数据就绪事件结构体示例
typedef struct {
    uint32_t event_id;      // 事件ID,如 EVT_TEMP_READY
    uint32_t timestamp;     // 时间戳
    union {
        struct {
            float temperature;
            float humidity;
        } env_sensor;
        struct {
            uint16_t distance;
            uint8_t  quality;
        } range_sensor;
    } data;
} sensor_event_t;

3.4 通信接口接收事件

UART、SPI、I2C、CAN、Ethernet……只要涉及通信,就离不开接收事件。数据来了,你得知道,还得及时处理。

这类事件有个麻烦:你不知道数据什么时候来,也不知道来多少。可能是 1 个字节,也可能是 1KB 的报文。

我的处理原则是:

  • 接收中断只做数据搬运(从硬件 FIFO 搬到内存缓冲区)
  • 当检测到完整报文(比如收到换行符或固定长度),才触发「接收完成事件」
  • 事件中携带数据指针和长度,避免数据拷贝
小技巧: 对于高频通信场景,建议使用 DMA + 环形缓冲区。CPU 只在 DMA 完成中断时被唤醒一次,而不是每个字节都中断一次。我在一个 1Mbps 的 UART 项目中,用这个方法把 CPU 占用率从 35% 降到了 3%。

3.5 软件触发事件

这类事件不是由硬件产生的,而是由软件自身触发的。比如:

  • 任务 A 处理完数据后,通知任务 B 可以开始工作了
  • 状态机检测到超时,触发超时事件
  • 用户通过命令行输入指令,触发配置更新事件

软件触发事件的好处是:你完全可控。不会像外部中断那样不可预测,也不会像定时器那样有固定周期。它是你用来编排系统逻辑的「软开关」。

我个人习惯把软件事件分为同步和异步两种:

类型 触发方式 典型场景
同步软件事件 函数调用直接触发 状态机跳转、函数回调
异步软件事件 通过事件队列投递 任务间通信、延迟处理

嗯,这里要注意:同步事件虽然简单直接,但如果处理时间过长,会阻塞调用者。异步事件虽然灵活,但需要管理好事件队列的生命周期。没有银弹,只有权衡。

3.6 事件类型关系图

下面这张图展示了五类事件在嵌入式系统中的位置和关系。你可以看到,所有事件最终都汇聚到事件队列,由事件处理器统一调度。

嵌入式系统事件类型关系图 外部中断事件 按键/电平/边沿 定时器事件 硬件/软件定时器 传感器就绪事件 数据采集完成 通信接收事件 UART/SPI/I2C/CAN 软件触发事件 任务间通信/超时/指令 事件队列 FIFO / 优先级队列 事件处理器 分发 / 处理 / 响应 所有事件统一入队 处理器统一调度
设计思路: 从图中可以看出,无论事件来自硬件还是软件,最终都汇聚到事件队列。这种「统一入口」的设计,让系统架构变得清晰可控。你只需要关注事件处理器怎么写,而不需要关心事件是怎么来的。

3.7 事件类型的优先级考量

不同事件对实时性的要求不一样。外部中断事件通常要求立即响应,而软件触发事件可以稍微排队。我个人习惯给事件分配优先级:

优先级 事件类型 响应要求 典型处理方式
最高 外部中断事件 微秒级 ISR 中直接处理或置标志
高 通信接收事件 毫秒级 DMA + 中断 + 事件队列
中 传感器就绪事件 毫秒~十毫秒级 事件队列 + 任务调度
中低 定时器事件 取决于周期 软件定时器 + 事件队列
低 软件触发事件 可排队 事件队列 + 空闲处理

当然,这个优先级不是死的。比如一个 1ms 的定时器事件,优先级可能比某些通信事件还高。关键是你得根据项目需求自己权衡。

避坑指南: 我曾经在一个项目中,把外部中断事件的优先级设得太低,结果被其他中断频繁打断,导致按键响应延迟达到 200ms。用户按了键,灯要过一会儿才亮。后来我把外部中断优先级提到最高,问题解决。记住:外部中断事件通常应该拥有最高优先级。