FreeRTOS事件组和任务通知详解:为什么任务通知比队列更轻量?

在 FreeRTOS 中,任务之间经常需要进行:
- 任务同步
- 状态等待
- 事件通知
- 数据传递
前面我们学习了:
- 二值信号量
- 计数信号量
- 队列 Queue
- 互斥量 Mutex
这些机制解决了不同的问题。
但是在实际项目中,还会遇到两个非常典型的问题:
问题1:
一个任务需要等待多个条件同时满足。
例如:
系统启动:
网络初始化完成
传感器初始化完成
Flash初始化完成
三个条件都满足:
系统才能进入正常运行。
这种场景:
使用:
Event Group(事件组)
问题2:
一个任务只需要通知另一个任务:
"我有事情,你运行一下"。
例如:
UART收到数据:
UART中断
↓
通知UART任务处理
这种场景:
使用:
Task Notification(任务通知)
而且任务通知相比队列:
更加轻量。
一、什么是事件组 Event Group?
Event Group:
中文:
事件组
它是 FreeRTOS 提供的一种:
多事件同步机制。
它通过:
text
bit位
表示不同事件。
例如:
一个8位事件组:
BIT7 BIT6 BIT5 BIT4 BIT3 BIT2 BIT1 BIT0
0 0 0 0 0 1 0 1
每一个bit代表一个事件。
例如:
BIT0 = 网络连接完成
BIT1 = 传感器初始化完成
BIT2 = Flash初始化完成
如果:
BIT0=1
BIT1=1
BIT2=1
表示:
三个条件全部完成。
二、为什么不用多个二值信号量?
假设系统启动需要等待:
三个模块:
网络
传感器
存储
使用二值信号量:
可能设计:
Network_Sem
Sensor_Sem
Flash_Sem
系统任务:
需要:
c
xSemaphoreTake(Network_Sem);
xSemaphoreTake(Sensor_Sem);
xSemaphoreTake(Flash_Sem);
问题:
- 对象太多
- 管理复杂
- 判断困难
如果增加:
蓝牙
GPS
摄像头
代码会越来越复杂。
事件组:
只需要:
一个 Event Group
管理:
多个事件。
结构:
Event Group
BIT0 网络完成
BIT1 传感器完成
BIT2 Flash完成
BIT3 蓝牙完成
更加清晰。
三、事件组的核心操作
FreeRTOS事件组主要有三个操作。
1. 创建事件组
函数:
c
xEventGroupCreate()
示例:
c
EventGroupHandle_t SystemEvent;
SystemEvent =
xEventGroupCreate();
2. 设置事件
函数:
c
xEventGroupSetBits()
例如:
网络初始化完成:
c
xEventGroupSetBits(
SystemEvent,
BIT0
);
表示:
BIT0事件发生。
3. 等待事件
函数:
c
xEventGroupWaitBits()
例如:
系统任务等待:
网络 + 传感器 + Flash
c
xEventGroupWaitBits(
SystemEvent,
BIT0 | BIT1 | BIT2,
pdTRUE,
pdTRUE,
portMAX_DELAY
);
含义:
等待:
BIT0
+
BIT1
+
BIT2
全部为1。
四、事件组典型应用场景
场景1:系统启动同步
这是最经典应用。
例如:
嵌入式设备启动:
Boot
↓
初始化网络
↓
初始化传感器
↓
初始化存储
↓
进入业务
每个任务完成后:
设置自己的bit。
例如:
网络任务:
c
xEventGroupSetBits(
SystemEvent,
NET_READY
);
传感器:
c
xEventGroupSetBits(
SystemEvent,
SENSOR_READY
);
主任务:
等待:
c
NET_READY |
SENSOR_READY
场景2:多条件报警
例如:
设备报警任务。
需要关注:
温度异常
风扇异常
通信异常
定义:
c
#define TEMP_ERROR (1<<0)
#define FAN_ERROR (1<<1)
#define COMM_ERROR (1<<2)
任何一个发生:
设置对应bit。
报警任务:
等待:
c
TEMP_ERROR |
FAN_ERROR |
COMM_ERROR
即可。
场景3:模块状态汇总
例如:
智能设备:
WiFi连接
MQTT连接
时间同步
App任务:
需要知道:
当前系统状态。
事件组:
非常适合。
五、事件组不适合什么?
事件组不是万能的。
1. 需要传递数据
例如:
温度值:
25.6℃
事件组只能告诉:
温度数据来了
但是不能传:
25.6
这种情况:
使用:
Queue。
2. 保护共享资源
例如:
两个任务访问:
UART
I2C
SPI
需要:
Mutex。
3. 单个简单通知
例如:
按键一次触发。
使用:
二值信号量。
更简单。
六、什么是任务通知 Task Notification?
任务通知:
是 FreeRTOS 中一种特殊通信机制。
特点:
每个任务内部自带一个通知值。
也就是说:
普通队列:
Task
↓
Queue对象
↓
Task
需要额外创建:
Queue。
任务通知:
Task A
↓
Task B内部通知值
直接通知目标任务。
七、任务通知为什么比队列轻量?
这是重点。
1. 少一个内核对象
Queue:
需要:
Queue Control Block
+
Queue Storage
例如:
任务
↓
队列对象
↓
数据
任务通知:
直接:
任务
↓
Notification Value
少了一层。
2. 占用RAM更少
队列需要:
- 队列控制结构
- 缓冲空间
- 数据存储
任务通知:
只需要:
任务自身的:
32bit通知值
所以:
RAM更小。
3. 执行速度更快
队列:
流程:
发送任务
↓
访问Queue
↓
复制数据
↓
唤醒任务
任务通知:
发送
↓
修改通知值
↓
唤醒任务
路径更短。
八、任务通知使用方式
发送通知
任务:
c
xTaskNotifyGive(TaskHandle);
接收通知
任务:
c
ulTaskNotifyTake(
pdTRUE,
portMAX_DELAY
);
例如:
UART任务:
c
void UART_Task(void *arg)
{
while(1)
{
ulTaskNotifyTake(
pdTRUE,
portMAX_DELAY
);
Process_UART();
}
}
中断:
c
void UART_IRQHandler()
{
vTaskNotifyGiveFromISR(
UART_TaskHandle,
NULL
);
}
流程:
UART中断
↓
任务通知
↓
UART任务运行
九、任务通知支持哪些功能?
FreeRTOS任务通知不仅可以通知。
还可以模拟:
1. 二值信号量
类似:
0/1
2. 计数信号量
例如:
事件次数
3. 事件标志
例如:
BIT0
BIT1
BIT2
所以:
任务通知实际上非常强大。
十、任务通知和队列区别
| 比较 | Queue | Task Notification |
|---|---|---|
| 通信对象 | 独立队列 | 任务自身 |
| RAM占用 | 较高 | 很低 |
| 速度 | 较慢 | 更快 |
| 数据传输 | 支持任意数据 | 主要传递32位值 |
| 多个接收者 | 支持 | 通常一对一 |
| 使用复杂度 | 较高 | 简单 |
十一、什么时候使用任务通知?
适合:
1. 中断通知任务
例如:
UART
DMA
ADC
GPIO
2. 一个任务唤醒另一个任务
例如:
采集任务
↓
处理任务
3. 计数事件
例如:
收到多少次脉冲。
十二、什么时候不要使用任务通知?
1. 多任务共享数据
使用:
Queue。
2. 多个任务等待同一个消息
使用:
Queue/Event Group。
3. 复杂数据传输
例如:
结构体:
c
typedef struct
{
int temp;
int voltage;
int state;
}Data;
使用:
Queue。
十三、事件组 VS 任务通知总结
| 功能 | 事件组 | 任务通知 |
|---|---|---|
| 核心作用 | 多个事件同步 | 任务间快速通知 |
| 管理对象 | 多个bit | 单个任务 |
| 数据传输 | 不能 | 有限 |
| RAM占用 | 较低 | 最低 |
| 速度 | 快 | 最快 |
| 典型场景 | 等待多个条件 | 中断唤醒任务 |
十四、工程选择建议
如果你的问题是:
"我需要等待多个条件"
例如:
网络完成
传感器完成
存储完成
选择:
Event Group
如果你的问题是:
"我要快速通知另一个任务"
例如:
UART中断
DMA完成
定时器触发
选择:
Task Notification
如果你的问题是:
"我要传递数据"
例如:
温度值
传感器结构体
通信数据包
选择:
Queue
十五、总结
FreeRTOS提供了很多任务同步机制:
- Semaphore
- Queue
- Event Group
- Task Notification
- Mutex
它们解决的问题不同。
一句话总结:
事件组适合"一个任务等待多个事件",任务通知适合"一对一快速唤醒任务"。
再进一步理解:
队列传递数据,事件组管理状态,任务通知追求速度和轻量。
掌握这些机制后,FreeRTOS中的任务协作设计就基本形成完整体系。