Skip to content

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);
        }
    }
}

队列的两个好处

  1. 接收方没数据时会自动睡着,一点 CPU 都不占 —— 这正是我们想要的
  2. 数据是复制过去的,发送方发完就能改自己的变量,不用担心冲突

三、传结构体(更实用) ​

实际项目里常常要一次传一组数据,用结构体:

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
  • 发送一定要判断返回值,队列满了数据会丢
  • 事件组 = 一排标志位,用来等几件事都完成
  • 任务通知 = 每个任务自带的信号量,一对一通知时更快更省内存
  • 但多个任务等同一事件、要知道信号丢没丢、拿不到句柄、要当锁用时,只能用信号量

动手试试 ​

  1. 写两个任务:一个每秒往队列发一个递增的数字,另一个收到就打印。
  2. 把队列长度设成 2,让发送方发得比接收方快,观察"队列满了"的提示。
  3. 用事件组模拟开机流程:任务 A 在 3 秒后升第一面旗,任务 B 在 5 秒后升第二面,主任务等两面都升起来再打印"启动完成"。