外观
1.8 事件组、队列与任务通知
这节要搞懂:任务之间怎么互相传消息。 需要基础:1.7 ⭐ 本章重点
一、什么是队列
队列(Queue)是 FreeRTOS 提供的任务间数据传递机制。它是一块由内核管理的缓冲区,遵循先进先出(FIFO)的原则:一个任务往里放数据,另一个任务从里面取。
打个比方:队列就是一条传送带 —— 一边往上放东西,另一边取下来,先放的先取。
二、队列怎么用
c
QueueHandle_t temp_queue; // 队列句柄
void app_init(void)
{
// 创建队列:最多放 10 个,每个是一个 float
temp_queue = xQueueCreate(10, sizeof(float));
}
// 发送方(采集任务)
void task_sensor(void *pvParameters)
{
while (1) {
float temperature = 26.5;
// 最后的 0 表示:队列满了就别等,直接走
xQueueSend(temp_queue, &temperature, 0);
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
// 接收方(显示任务)
void task_display(void *pvParameters)
{
float value;
while (1) {
// portMAX_DELAY 表示:没数据就一直睡着等
if (xQueueReceive(temp_queue, &value, portMAX_DELAY) == pdTRUE) {
display_temperature(value);
}
}
}队列的两个好处
- 接收方没数据时会自动睡着,一点 CPU 都不占 —— 这正是我们想要的
- 数据是复制过去的,发送方发完就能改自己的变量,不用担心冲突
三、传结构体(更实用)
实际项目里常常要一次传一组数据,用结构体:
c
typedef struct {
float temperature;
float humidity;
} sensor_data_t;
QueueHandle_t sensor_queue = xQueueCreate(5, sizeof(sensor_data_t));
// 发送
sensor_data_t data = { .temperature = 26.5, .humidity = 62.0 };
xQueueSend(sensor_queue, &data, 0);
// 接收
sensor_data_t received;
xQueueReceive(sensor_queue, &received, portMAX_DELAY);别忘了判断返回值
c
if (xQueueSend(sensor_queue, &data, 0) != pdTRUE) {
printf("队列满了,这次数据丢了\n");
}队列满的时候数据是真的丢了。不判断的话你根本不知道丢过。
四、什么是事件组
事件组(Event Group)是一组二进制标志位的集合。每一位代表一个事件是否发生。任务可以等待其中的某一位,也可以等待多位同时满足。
打个比方:事件组就是一排小旗子,每面旗代表一件事。任务可以站在那儿等"某几面旗都升起来"。
什么时候用它:你不需要传数据,只想知道"某件事完成了没有"。
c
#define BIT_WIFI_OK BIT0 // 第 0 面旗:网络好了
#define BIT_TIME_OK BIT1 // 第 1 面旗:时间对好了
EventGroupHandle_t app_events;
void app_init(void)
{
app_events = xEventGroupCreate();
}
// WiFi 连上了,把第 0 面旗升起来
void on_wifi_connected(void)
{
xEventGroupSetBits(app_events, BIT_WIFI_OK);
}
// 主任务:等这两面旗都升起来才继续
void task_main(void *pvParameters)
{
xEventGroupWaitBits(app_events,
BIT_WIFI_OK | BIT_TIME_OK, // 等这两面旗
pdFALSE, // 等到后不放下旗子
pdTRUE, // pdTRUE=两面都要,pdFALSE=有一面就行
portMAX_DELAY); // 一直等
start_business(); // 两面都升起来了,开工
}五、什么是任务通知
任务通知(Task Notification)是 FreeRTOS 提供的一种轻量级通知机制。每个任务自带一个通知值,不需要额外创建队列或信号量对象,因此速度更快、更省内存。
可以理解成"每个任务自带的一个信号量" —— 用法和二值信号量几乎一样,但不用额外创建对象。
任务通知确实更划算
| 二值信号量 | 任务通知 | |
|---|---|---|
| 要不要创建对象 | 要 xSemaphoreCreateBinary() | 不用,任务自带 |
| 速度 | 快 | 更快(官方数据约快 45%) |
| 额外占内存 | 每个信号量一份 | 0 |
所以只是"通知某一个任务"的话,优先用任务通知。
那为什么还会用二值信号量 ⭐
既然任务通知又快又省,为什么课程里还讲信号量、实际项目里也到处是它?
因为下面四种情况,任务通知做不到。
① 多个任务等待同一个事件
任务通知是一对一绑定的 —— 它就存在某个任务的 TCB(任务控制块)里,只能通知一个指定的任务。
而二值信号量是个独立对象,多个任务可以同时等在它上面:
c
SemaphoreHandle_t motor_sem;
void app_init(void)
{
motor_sem = xSemaphoreCreateBinary();
// 三个电机任务,都等在同一个信号量上
xTaskCreate(task_motor_a, "motor_a", 2048, NULL, 5, NULL);
xTaskCreate(task_motor_b, "motor_b", 2048, NULL, 6, NULL); // 优先级最高
xTaskCreate(task_motor_c, "motor_c", 2048, NULL, 4, NULL);
}
// 中断里释放一次
void IRAM_ATTR limit_switch_isr(void *arg)
{
BaseType_t woken = pdFALSE;
xSemaphoreGiveFromISR(motor_sem, &woken); // 三个任务里,优先级最高的 B 先拿到
portYIELD_FROM_ISR(woken);
}任务通知做不到这种"一对多" —— 你得知道三个句柄,然后发三次。
一次 Give 只唤醒一个
注意别理解成"广播" —— 二值信号量里只有 1 个令牌,Give 一次只会唤醒其中一个任务(就绪的里面优先级最高的那个),不是三个一起醒。
想一次叫醒所有等待者,要用事件组(本节第四部分)。
② 发送方想知道"这次信号有没有被丢掉"
两者的返回值含义不一样:
| 上一次信号还没被取走时,再发一次 | |
|---|---|
| 二值信号量 | xSemaphoreGive() 返回 pdFALSE —— 明确告诉你"满了,这次丢了" |
| 任务通知 | xTaskNotifyGive() 永远返回成功,值默默累加,你察觉不到 |
c
if (xSemaphoreGive(key_sem) != pdTRUE) {
// 接收方还没处理完上一次,说明它跟不上了
ESP_LOGW(TAG, "上一个事件还没处理完");
}接收方处理速度可能跟不上、又需要知道有没有丢事件时,信号量给的信息更多。
顺带说清一个误解
这两个 Give 都不会阻塞,都是"发完就走"。
真正能让发送方阻塞等待的是队列(xQueueSend 有超时参数),信号量和任务通知都没有这个能力。要"等接收方处理完",得用队列,或者两个信号量做一次握手。
③ 发送方不知道谁是接收方
xTaskNotifyGive(handle) 必须持有目标任务的 TaskHandle_t 才能发送。
而信号量是"我把铃放在这里,谁来谁按" —— 发送方根本不需要知道谁在等:
c
// 任务通知:必须先把句柄传进这个模块
xTaskNotifyGive(g_worker_handle);
// 信号量:只认识这个铃就行
xSemaphoreGive(key_sem);这是最常见的原因。 做一个通用的事件发布机制时,底层驱动按个铃就走,它不应该知道上层是哪个任务在接 —— 一旦要传句柄,模块之间就绑死了。
④ 拿来当锁用
二值信号量有 "获取 / 释放" 这个概念,所以简单场景下可以拿它当一把轻量级的锁。
任务通知完全做不到 —— 它只是一个计数值,没有"所有权"的概念,谈不上"谁拿了、谁该还"。
但保护资源请用互斥锁
二值信号量当锁用是能跑,但不推荐:它没有优先级继承机制,容易出现高优先级任务被低优先级任务长期卡住的问题。
保护共享资源,老老实实用 xSemaphoreCreateMutex()。
一句话总结
| 情况 | 用什么 |
|---|---|
| 一对一,且拿得到句柄 | 任务通知 ⚡ 更快更省 |
| 多个任务等同一个事件 | 信号量 |
| 需要知道信号有没有丢 | 信号量 |
| 不知道谁来接 | 信号量 |
| 要当锁用 | 互斥锁(别用二值信号量) |
新手阶段两个都能用的场合,用哪个都不算错。 先把信号量用熟,因为它适用面更广。
六、四种怎么选
| 我想…… | 用什么 |
|---|---|
| 传数据(温度、按键值) | 队列 ✅ 最常用 |
| 保护共享资源 | 互斥锁(1.7) |
| 通知一个特定任务(拿得到句柄) | 任务通知 ⚡ 最快 |
| 通知,但不确定谁在等 / 有多个在等 | 信号量(1.7) |
| 等好几件事都完成 | 事件组 |
本节要点
- 队列 = 任务间传数据的先进先出缓冲区,最常用
- 接收方没数据会自动睡,不占 CPU
- 发送一定要判断返回值,队列满了数据会丢
- 事件组 = 一排标志位,用来等几件事都完成
- 任务通知 = 每个任务自带的信号量,一对一通知时更快更省内存
- 但多个任务等同一事件、要知道信号丢没丢、拿不到句柄、要当锁用时,只能用信号量
动手试试
- 写两个任务:一个每秒往队列发一个递增的数字,另一个收到就打印。
- 把队列长度设成 2,让发送方发得比接收方快,观察"队列满了"的提示。
- 用事件组模拟开机流程:任务 A 在 3 秒后升第一面旗,任务 B 在 5 秒后升第二面,主任务等两面都升起来再打印"启动完成"。