2. 事件源与事件类型:硬件事件与软件事件

做嵌入式事件驱动架构,第一件事就是搞清楚——事件从哪来?

我个人习惯把事件源分成两大类:硬件事件和软件事件。这两类事件的处理方式、优先级策略、调试手段都不一样。今天咱们就把它们掰开揉碎了讲清楚。

2.1 硬件事件:来自芯片外设的“硬中断”

硬件事件,说白了就是芯片内部外设模块产生的信号。它们直接连到中断控制器,响应速度极快。我遇到过不少新手,上来就把所有硬件中断都设成同一优先级,结果系统一跑就崩——嗯,这里要注意。

2.1.1 GPIO 中断

GPIO 中断是最常见的事件源。按键按下、传感器电平跳变、外部触发信号——这些都能产生 GPIO 中断。

关键配置参数:

  • 触发方式:上升沿、下降沿、双边沿、低电平、高电平
  • 去抖处理:硬件去抖(芯片自带)或软件去抖(定时器辅助)
  • 中断优先级:通常设为中高优先级

我曾经在一个工业控制项目里,把按键中断设成了最高优先级。结果按键一按,整个系统卡住——因为高优先级中断里我做了太多事情。后来改成中断只发事件,处理交给任务,问题就解决了。

2.1.2 定时器事件

定时器事件是事件驱动架构的“心跳”。周期任务调度、超时检测、PWM 生成、软件定时器——都靠它。

定时器类型 典型用途 优先级建议
系统滴答定时器 (SysTick) 操作系统心跳、时间片轮转 最高
通用定时器 周期任务、PWM、输入捕获 中高
看门狗定时器 系统异常复位 不可屏蔽

我的经验是:定时器中断里绝对不要做复杂运算。你想想看,定时器中断频率通常很高(1ms 甚至更短),在里面做浮点运算,系统不崩才怪。

2.1.3 DMA 完成事件

DMA(直接存储器访问)完成事件是个好东西。它告诉你“数据搬完了,你来处理吧”。

实用技巧:DMA 完成中断通常设为中优先级。因为 DMA 本身不占 CPU,中断只是通知任务来取数据。优先级太高反而会打断其他关键任务。

我记得有个音频采集项目,用 DMA 双缓冲采集 ADC 数据。DMA 完成中断里只做一件事:翻转缓冲区标志位。真正的数据处理放在任务里,优先级设得比中断低。这样既保证了数据不丢,又不影响其他实时任务。

2.2 软件事件:任务间的“软通信”

软件事件是任务与任务之间、中断与任务之间传递信息的机制。它们不像硬件事件那样“硬实时”,但灵活得多。

2.2.1 消息队列

消息队列是最常用的软件事件源。一个任务往队列里发消息,另一个任务从队列里取消息——典型的生产者-消费者模式。

// 消息队列示例(FreeRTOS)
QueueHandle_t xQueue;

// 发送任务(生产者)
void vSenderTask(void *pvParameters) {
    int32_t data = 100;
    xQueueSend(xQueue, &data, 0);
}

// 接收任务(消费者)
void vReceiverTask(void *pvParameters) {
    int32_t receivedData;
    xQueueReceive(xQueue, &receivedData, portMAX_DELAY);
    // 处理数据
}

避坑指南:我曾经在消息队列里传了很大的结构体,导致内存暴涨。后来改成传指针,问题解决。记住:消息队列传小数据或指针,别传大块数据。

2.2.2 信号量

信号量用于同步和互斥。二值信号量像一把钥匙,计数信号量像停车场空位。

信号量类型 典型场景 注意事项
二值信号量 中断通知任务、任务同步 注意优先级反转
计数信号量 资源池管理、多生产者同步 初始值要设对
互斥信号量 保护共享资源 支持优先级继承

你想想看,如果两个任务同时访问同一个全局变量,会发生什么?数据错乱、系统崩溃。互斥信号量就是干这个的——谁拿到锁谁用,用完释放。

2.2.3 任务通知

任务通知是 RTOS 里效率最高的软件事件机制。它比信号量快 30%-50%,而且不需要额外创建内核对象。

任务通知的优势:

  • 无需创建队列或信号量,节省内存
  • 发送和接收都是 O(1) 复杂度
  • 支持多种通知状态:二进制、计数、值传递

我个人习惯在中断服务函数里用任务通知代替信号量。因为任务通知不需要额外的内核对象,中断里调用更安全。但要注意:任务通知只能一对一,不能广播。

2.3 事件优先级划分

事件优先级划分是整个事件驱动架构的命门。优先级设错了,系统要么响应慢,要么死锁。

2.3.1 优先级划分原则

我总结了一套优先级划分的“三问法”:

  1. 这个事件不及时处理会怎样?(系统崩溃?数据丢失?还是只是体验差?)
  2. 这个事件的处理时间有多长?(微秒级?毫秒级?还是秒级?)
  3. 这个事件依赖哪些资源?(会不会和其他事件争抢资源?)
优先级等级 适用事件 处理时间要求
最高 (0-1) 系统滴答、看门狗、紧急故障 < 10μs
高 (2-3) 高速通信中断、DMA 完成 < 100μs
中 (4-5) GPIO 中断、定时器事件 < 1ms
低 (6-7) 消息队列、信号量、任务通知 ms 级或更低

血的教训:我曾经把 UART 接收中断设成最高优先级,结果系统滴答定时器被阻塞,整个 RTOS 调度停摆。记住:系统心跳的优先级必须最高,其他硬件中断再重要也不能超过它。

2.3.2 优先级反转与解决方案

优先级反转是个经典问题。低优先级任务拿着锁,高优先级任务等着锁,中优先级任务抢了 CPU——结果高优先级任务反而被低优先级任务“拖死”。

解决方案:

  • 优先级继承:低优先级任务拿到锁后,临时提升到等待该锁的最高优先级
  • 优先级天花板:锁的优先级设为所有可能使用该锁的任务的最高优先级
  • 关中断保护:临界区代码尽量短,关中断时间控制在微秒级

我建议在项目初期就规划好事件优先级矩阵,把所有事件源、优先级、处理函数、最大处理时间列出来。别等到系统崩了再调优先级——那时候改一处可能牵动全身。

2.4 本章知识体系图

下面这张图把事件源、事件类型和优先级划分的关系梳理清楚了:

事件源与事件类型知识体系 事件源与事件类型知识体系 事件驱动架构 硬件事件 软件事件 GPIO 中断 定时器事件 DMA 完成 消息队列 信号量 任务通知 事件优先级划分 最高:系统滴答/看门狗 高:通信中断/DMA 中:GPIO/定时器 低:消息/信号量/通知 核心原则:中断只做最简操作,事件处理交给任务 优先级规划要在项目初期完成,后期修改成本极高

本章小结:硬件事件是系统的“手脚”,响应快但处理要轻;软件事件是系统的“神经”,灵活但要注意同步。优先级划分是事件驱动架构的“宪法”——定好了就别轻易改。