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 优先级划分原则
我总结了一套优先级划分的“三问法”:
- 这个事件不及时处理会怎样?(系统崩溃?数据丢失?还是只是体验差?)
- 这个事件的处理时间有多长?(微秒级?毫秒级?还是秒级?)
- 这个事件依赖哪些资源?(会不会和其他事件争抢资源?)
| 优先级等级 | 适用事件 | 处理时间要求 |
|---|---|---|
| 最高 (0-1) | 系统滴答、看门狗、紧急故障 | < 10μs |
| 高 (2-3) | 高速通信中断、DMA 完成 | < 100μs |
| 中 (4-5) | GPIO 中断、定时器事件 | < 1ms |
| 低 (6-7) | 消息队列、信号量、任务通知 | ms 级或更低 |
血的教训:我曾经把 UART 接收中断设成最高优先级,结果系统滴答定时器被阻塞,整个 RTOS 调度停摆。记住:系统心跳的优先级必须最高,其他硬件中断再重要也不能超过它。
2.3.2 优先级反转与解决方案
优先级反转是个经典问题。低优先级任务拿着锁,高优先级任务等着锁,中优先级任务抢了 CPU——结果高优先级任务反而被低优先级任务“拖死”。
解决方案:
- 优先级继承:低优先级任务拿到锁后,临时提升到等待该锁的最高优先级
- 优先级天花板:锁的优先级设为所有可能使用该锁的任务的最高优先级
- 关中断保护:临界区代码尽量短,关中断时间控制在微秒级
我建议在项目初期就规划好事件优先级矩阵,把所有事件源、优先级、处理函数、最大处理时间列出来。别等到系统崩了再调优先级——那时候改一处可能牵动全身。
2.4 本章知识体系图
下面这张图把事件源、事件类型和优先级划分的关系梳理清楚了:
本章小结:硬件事件是系统的“手脚”,响应快但处理要轻;软件事件是系统的“神经”,灵活但要注意同步。优先级划分是事件驱动架构的“宪法”——定好了就别轻易改。