第三篇FreeRTOS 通信机制 — 队列、事件组与任务通知

FreeRTOS 通信机制 — 队列、事件组与任务通知

FreeRTOS 提供了三大核心通信机制:队列、事件组、任务通知。掌握它们的原理、API 和适用场景,是写好多任务程序的关键。本文带你逐一攻破。


一、队列(Queue)

1.1 什么是队列

FreeRTOS 队列(又称消息队列)是一种线程安全的 FIFO 缓冲区,专门用于在任务与任务任务与中断之间传递数据。你可以把它想象成一个带锁的管道——一头塞数据,另一头取数据,系统自动保证不会"打架"。

         ┌────┬────┬────┬────┬────┐
Send ──► │ D1 │ D2 │ D3 │    │    │ ──► Receive
         └────┴────┴────┴────┴────┘
         队列满时 Send 阻塞    队列空时 Receive 阻塞

为什么不用全局变量?

在裸机开发中,全局变量是最常用的数据共享方式。但在 RTOS 多任务环境下,全局变量存在严重问题:

  • 数据竞争:任务 A 正在读取变量,任务 B 可能同时修改它,导致任务 A 拿到半新半旧的脏数据。
  • 无阻塞能力:全局变量没有"等待"机制。数据没准备好?只能轮询,白白浪费 CPU。
  • 无缓冲能力:生产者速度大于消费者时,全局变量只能保存最新一条,历史数据直接丢失。

而队列天然解决了这三个问题:互斥访问、阻塞等待、FIFO 缓冲

1.2 队列的核心特点

① 数据排列方式

  • 默认 FIFO(先进先出):先发送的数据先被读取,最常用。
  • 可选 LIFO(后进先出):通过 xQueueSendToFront() 实现"插队",适用于紧急消息优先处理。

② 数据传递方式

队列采用值传递(拷贝),发送时将数据完整拷贝到队列内部缓冲区:

方式 实现 优点 缺点
值传递 拷贝整个数据到队列 发送后原始数据可立即释放,安全可靠 大数据拷贝开销大
指针传递 只拷贝指针(sizeof(void*) 零拷贝,效率高 必须保证指针指向的内存在接收方处理前不被释放

实践建议:小数据(结构体 < 64 字节)直接值传递;大数据(图像缓冲区、音频帧)传指针。

③ 多任务安全访问

队列是全局共享的内核对象,不属于任何单个任务。任意数量的任务和 ISR 都可以同时向同一个队列发送或读取,FreeRTOS 内部已通过临界区保证线程安全。

④ 阻塞等待机制

当队列满(无法入队)或队列空(无法出队)时,可以通过超时参数控制行为:

超时参数 行为
0 不等待,立即返回成功或失败
pdMS_TO_TICKS(N) 最多等待 N 毫秒,超时后返回失败
portMAX_DELAY 永久阻塞,直到操作成功为止

阻塞期间任务处于 Blocked 状态不消耗 CPU,调度器会切换到其他就绪任务运行。

1.3 队列 API 详解

1)创建队列

QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,
                             UBaseType_t uxItemSize );
参数 说明
uxQueueLength 队列可容纳的最大消息条数
uxItemSize 每条消息的大小(字节)
返回值 成功返回队列句柄;内存不足返回 NULL

2)写队列(发送消息)

函数 说明
xQueueSend() 向队列尾部写入消息(FIFO)
xQueueSendToBack() xQueueSend(),别名
xQueueSendToFront() 向队列头部写入消息(LIFO)
xQueueOverwrite() 覆写消息(仅用于队列长度为 1 的场景)
xQueueSendFromISR() 中断版 — 尾部写入
xQueueSendToFrontFromISR() 中断版 — 头部写入
xQueueOverwriteFromISR() 中断版 — 覆写

函数原型(任务版):

BaseType_t xQueueSend( QueueHandle_t    xQueue,
                        const void      *pvItemToQueue,
                        TickType_t       xTicksToWait );
参数 说明
xQueue 目标队列句柄
pvItemToQueue 指向待发送数据的指针
xTicksToWait 阻塞超时时间(tick 数)
返回值 成功 pdTRUE;队列满且超时 errQUEUE_FULL

3)读队列(接收消息)

函数 说明
xQueueReceive() 从队列头部读取消息,读后删除
xQueuePeek() 从队列头部读取消息,读后保留
xQueueReceiveFromISR() 中断版 — 读取并删除
xQueuePeekFromISR() 中断版 — 读取并保留

函数原型(任务版):

BaseType_t xQueueReceive( QueueHandle_t    xQueue,
                           void            *pvBuffer,
                           TickType_t       xTicksToWait );
参数 说明
xQueue 源队列句柄
pvBuffer 接收数据的缓冲区指针
xTicksToWait 阻塞超时时间(tick 数)
返回值 成功 pdTRUE;队列空且超时 pdFALSE

4)查询队列状态

UBaseType_t uxQueueMessagesWaiting( xQueue );   // 当前已有消息数
UBaseType_t uxQueueSpacesAvailable( xQueue );   // 剩余可用空间

5)代码示例

// 创建队列:5 个元素,每个元素 sizeof(uint32_t) 字节
QueueHandle_t xQueue = xQueueCreate(5, sizeof(uint32_t));

// ---- 发送数据 ----
uint32_t ulValue = 100;
xQueueSend(xQueue, &ulValue, pdMS_TO_TICKS(100));      // 尾部入队,最多等 100ms
xQueueSendToFront(xQueue, &ulValue, 0);                // 头部入队,不等待

// ---- 接收数据 ----
uint32_t ulReceived;
xQueueReceive(xQueue, &ulReceived, portMAX_DELAY);     // 阻塞等待直到有数据
xQueuePeek(xQueue, &ulReceived, portMAX_DELAY);        // 查看数据但不出队

// ---- ISR 版本 ----
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(xQueue, &ulValue, &xHigherPriorityTaskWoken);
xQueueReceiveFromISR(xQueue, &ulReceived, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);           // 必要时触发上下文切换

1.4 队列的阻塞机制

xQueueSend() 为例,完整的执行流程如下:

deepseek_mermaid_20260308_0eae07|516

关键要点:

  1. 阻塞不浪费 CPU:任务阻塞后进入 Blocked 状态,CPU 资源交给其他就绪任务。
  2. 优先级抢占:多个任务等待同一个队列时,优先级最高的任务先获得操作权。
  3. ISR 中禁止阻塞:中断版函数(FromISR)没有超时参数,操作失败立即返回。

1.5 实验:按键收发队列

实验目标:创建一个队列,按下 KEY_B 向队列发送递增数据,按下 KEY_C 从队列读取数据,通过串口打印验证。

CubeMX 配置

image-20260308211237684

初始化代码(CubeMX 自动生成 + 手动补充)

/* ---- 句柄声明 ---- */
osThreadId taskKEY1sendDatHandle;    // 发送任务句柄
osThreadId taskKEY2RecieveHandle;    // 接收任务句柄
osMessageQId myQueue01Handle;        // 队列句柄

/* ---- 创建队列:深度 16,每项 sizeof(uint16_t) ---- */
osMessageQDef(myQueue01, 16, uint16_t);
myQueue01Handle = osMessageCreate(osMessageQ(myQueue01), NULL);

/* ---- 创建发送任务(KEY_B 触发) ---- */
osThreadDef(taskKEY1sendDat, sendDataToQueueTask, osPriorityIdle, 0, 128);
taskKEY1sendDatHandle = osThreadCreate(osThread(taskKEY1sendDat), NULL);

/* ---- 创建接收任务(KEY_C 触发) ---- */
osThreadDef(taskKEY2Recieve, recieveDataFromQueueTaskLED2, osPriorityIdle, 0, 128);
taskKEY2RecieveHandle = osThreadCreate(osThread(taskKEY2Recieve), NULL);

发送任务(生产者 — 按下 KEY_B 入队)

void sendDataToQueueTask(void const *argument)
{
    static uint16_t data = 0;   // static:每次按键自增,跨循环保持值
    BaseType_t result;

    for (;;) {
        /* 第一次检测:按键按下(低电平) */
        if (HAL_GPIO_ReadPin(KEYB_GPIO_Port, KEYB_Pin) == GPIO_PIN_RESET) {
            osDelay(30);  // 消抖延时 30ms
            /* 第二次检测:确认仍然按下 */
            if (HAL_GPIO_ReadPin(KEYB_GPIO_Port, KEYB_Pin) == GPIO_PIN_RESET) {
                data++;   // 数据自增
                result = xQueueSend(myQueue01Handle, &data, portMAX_DELAY);
                if (result == pdTRUE) {
                    printf("task1 send data:%d to myQueue01\r\n", data);
                } else {
                    printf("task1 send data error\r\n");
                }
            }
            /* 等待按键释放,防止重复触发 */
            while (HAL_GPIO_ReadPin(KEYB_GPIO_Port, KEYB_Pin) == GPIO_PIN_RESET);
        }
        osDelay(50);  // 轮询间隔 50ms,降低 CPU 占用
    }
}

接收任务(消费者 — 按下 KEY_C 出队)

void recieveDataFromQueueTaskLED2(void const *argument)
{
    BaseType_t result;

    for (;;) {
        /* 第一次检测:按键按下 */
        if (HAL_GPIO_ReadPin(KEYC_GPIO_Port, KEYC_Pin) == GPIO_PIN_RESET) {
            osDelay(50);  // 消抖延时 50ms
            /* 第二次检测:确认按下 */
            if (HAL_GPIO_ReadPin(KEYC_GPIO_Port, KEYC_Pin) == GPIO_PIN_RESET) {
                int data = 0;
                result = xQueueReceive(myQueue01Handle, &data, portMAX_DELAY);
                if (result == pdTRUE) {
                    printf("task2: recieve data:%d\r\n", data);
                } else {
                    printf("task2 read data error\r\n");
                }
            }
            /* 等待按键释放 */
            while (HAL_GPIO_ReadPin(KEYC_GPIO_Port, KEYC_Pin) == GPIO_PIN_RESET);
        }
        osDelay(50);
    }
}

运行结果

image-20260308212951279

实验要点:

  • 两次读取 GPIO + 延时实现软件消抖,避免机械按键抖动导致多次触发。
  • while 等待按键释放,确保一次按下只触发一次入队/出队。
  • 发送端用 static 变量保持自增计数,方便观察队列的 FIFO 顺序。
  • 队列深度 16 → 可连续按 16 次 KEY_B 再按 KEY_C 逐条读出,验证缓冲效果。

1.6 实战:传感器数据采集架构

典型的生产者-消费者模型:采集任务定时读取传感器数据并入队,处理任务从队列取出数据进行处理。

/* ---- 定义数据结构 ---- */
typedef struct {
    uint8_t  ucSensorID;
    float    fValue;
    uint32_t ulTimestamp;
} SensorData_t;

QueueHandle_t xSensorQueue;

/* ---- 主函数中创建队列 ---- */
void main(void)
{
    xSensorQueue = xQueueCreate(10, sizeof(SensorData_t));
    // ... 创建任务、启动调度器
}

/* ---- 采集任务(生产者) ---- */
void vSensorTask(void *p)
{
    SensorData_t xData;
    for(;;)
    {
        xData.ucSensorID  = 1;
        xData.fValue      = ReadTemperature();
        xData.ulTimestamp  = xTaskGetTickCount();

        // 入队,最多等 10ms(防止队列满时长时间阻塞采集)
        xQueueSend(xSensorQueue, &xData, pdMS_TO_TICKS(10));
        vTaskDelay(pdMS_TO_TICKS(100));   // 100ms 采集一次
    }
}

/* ---- 处理任务(消费者) ---- */
void vProcessTask(void *p)
{
    SensorData_t xReceived;
    for(;;)
    {
        // 永久阻塞等待,有数据才唤醒
        if(xQueueReceive(xSensorQueue, &xReceived, portMAX_DELAY) == pdTRUE)
        {
            printf("Sensor%d: %.1f at tick %lu\r\n",
                   xReceived.ucSensorID,
                   xReceived.fValue,
                   xReceived.ulTimestamp);
        }
    }
}

设计要点:

  • 生产者用短超时(pdMS_TO_TICKS(10)),避免队列满时阻塞采集节奏。
  • 消费者用 portMAX_DELAY,没有数据时休眠,零 CPU 占用。
  • 队列长度设为 10,为生产者与消费者之间的速度差提供缓冲余量。

二、事件组(Event Group)

2.1 什么是事件组

事件组 = 一组标志位(bit),任务可以等待 一个或多个 标志位的组合

事件标志组本质是一个无符号整型变量 EventBits_t(16 位或 32 位,由 configUSE_16_BIT_TICKS 决定)。虽然变量本身是 32 位,但高 8 位用于存储控制信息,低 24 位用于存储事件标志,因此一个事件组最多可以管理 24 个事件标志

事件组(EventBits_t = 24位可用):
Bit:  23 22 21 ... 7  6  5  4  3  2  1  0
Val:   0  0  0      0  0  1  0  0  1  1  0
                        |        |  |
                    WIFI连接   按键 温度就绪

image-20260308220312583

事件组的核心特点:

  • 无数据传递:只能传递"事件是否发生"(0/1),不能携带数据。
  • 多对多通信:多个任务可以同时等待同一个事件组,一次 SetBits 可唤醒所有等待该位的任务。
  • AND / OR 等待:可以等待多个事件位全部置 1(AND),或任一置 1(OR)。
  • 自动清除:可配置在等待成功后自动清除对应事件位。

2.2 事件组 API 详解

1)API 函数总览

函数 说明
xEventGroupCreate() 动态方式创建事件标志组
xEventGroupCreateStatic() 静态方式创建事件标志组
xEventGroupSetBits() 设置事件标志位
xEventGroupSetBitsFromISR() 中断版 — 设置事件标志位
xEventGroupClearBits() 清零事件标志位
xEventGroupClearBitsFromISR() 中断版 — 清零事件标志位
xEventGroupWaitBits() 等待事件标志位

2)创建事件标志组

EventGroupHandle_t xEventGroupCreate( void );
参数 说明
参数
返回值 成功返回事件标志组句柄;内存不足返回 NULL

3)设置事件标志位

EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
                                 const EventBits_t  uxBitsToSet );
参数 说明
xEventGroup 事件组句柄
uxBitsToSet 指定要设置的一个或多个事件位(按位或)
返回值 设置之后事件组中的事件标志位值

4)清除事件标志位

EventBits_t xEventGroupClearBits( EventGroupHandle_t xEventGroup,
                                   const EventBits_t  uxBitsToClear );
参数 说明
xEventGroup 事件组句柄
uxBitsToClear 指定要清除的一个或多个事件位(按位或)
返回值 清零之前事件组中的事件标志位值

5)等待事件标志位(最核心)

EventBits_t xEventGroupWaitBits(
    const EventGroupHandle_t xEventGroup,
    const EventBits_t        uxBitsToWaitFor,
    const BaseType_t         xClearOnExit,
    const BaseType_t         xWaitForAllBits,
    TickType_t               xTicksToWait );
参数 说明
xEventGroup 事件组句柄
uxBitsToWaitFor 要等待的一个或多个事件位(按位或)
xClearOnExit pdTRUE = 返回前自动清除等待的位;pdFALSE = 不清除
xWaitForAllBits pdTRUE = 所有位都为 1 才返回(AND);pdFALSE = 任一位为 1 即返回(OR
xTicksToWait 阻塞超时时间
返回值 成功:返回等待到的事件标志位值;超时:返回当前事件组中的标志位值

6)代码示例

// 创建事件组
EventGroupHandle_t xEventGroup = xEventGroupCreate();

// 定义事件位
#define EVENT_TEMP_READY   (1 << 0)   // Bit0: 温度数据就绪
#define EVENT_KEY_PRESSED  (1 << 1)   // Bit1: 按键按下
#define EVENT_WIFI_CONN    (1 << 5)   // Bit5: WiFi已连接

// ---- 设置事件位 ----
xEventGroupSetBits(xEventGroup, EVENT_TEMP_READY);
// ISR 版本
xEventGroupSetBitsFromISR(xEventGroup, EVENT_KEY_PRESSED, &xWoken);

// ---- 等待事件位(AND 模式:两个事件都发生才继续) ----
EventBits_t uxBits = xEventGroupWaitBits(
    xEventGroup,
    EVENT_TEMP_READY | EVENT_WIFI_CONN,  // 等待这些位
    pdTRUE,                              // 退出前清除等待的位
    pdTRUE,                              // AND 模式:所有位都置1才返回
    pdMS_TO_TICKS(5000)                  // 超时 5 秒
);

if((uxBits & (EVENT_TEMP_READY | EVENT_WIFI_CONN)) ==
   (EVENT_TEMP_READY | EVENT_WIFI_CONN))
{
    // 两个事件都发生了
}

// ---- 清除事件位 ----
xEventGroupClearBits(xEventGroup, EVENT_TEMP_READY);

2.3 实验

创建一个事件标志组和两个任务(taskl和task2),task1检测按键.如果检测到KEY1和
KEY2都按过,则执行task2
cubemx配置
image-20260308221454920
代码和运行

EventGroupHandle_t myEventHandle;
myEventHandle = xEventGroupCreate();

void checkKeyStatusTask(void const *argument) {
    /* USER CODE BEGIN checkKeyStatusTask */
    /* Infinite loop */
    for (;;) {
        if (HAL_GPIO_ReadPin(KEYB_GPIO_Port, KEYB_Pin) == GPIO_PIN_RESET) {
            osDelay(30);
            if (HAL_GPIO_ReadPin(KEYB_GPIO_Port, KEYB_Pin) == GPIO_PIN_RESET) {
                xEventGroupSetBits(myEventHandle, 0x01);
                printf("key1 被按下,条件1满足\r\n");
            }
            while (HAL_GPIO_ReadPin(KEYB_GPIO_Port, KEYB_Pin) == GPIO_PIN_RESET)
                ;
        }

        if (HAL_GPIO_ReadPin(KEYC_GPIO_Port, KEYC_Pin) == GPIO_PIN_RESET) {
            osDelay(50);
            if (HAL_GPIO_ReadPin(KEYC_GPIO_Port, KEYC_Pin) == GPIO_PIN_RESET) {
                xEventGroupSetBits(myEventHandle, 0x02);
                printf("key2 被按下,条件2满足\r\n");
            }
            while (HAL_GPIO_ReadPin(KEYC_GPIO_Port, KEYC_Pin) == GPIO_PIN_RESET)
                ;
        }
        osDelay(50);
    }
    /* USER CODE END checkKeyStatusTask */
}

void keyAllExecuteTask(void const *argument) {
    /* USER CODE BEGIN keyAllExecuteTask */
    uint32_t eventBit;
    /* Infinite loop */
    for (;;) {
        eventBit = xEventGroupWaitBits(myEventHandle, 0x01 | 0x02, pdTRUE, pdTRUE, portMAX_DELAY);
        if (eventBit == 0x03) {
            osDelay(100);
            printf("条件1&条件2均满足,开始执行\r\n");
            HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
        }
    }
    /* USER CODE END keyAllExecuteTask */
}

2.4 典型场景:多条件同步

// 系统初始化同步:等待所有模块就绪
#define INIT_SENSOR   (1 << 0)
#define INIT_DISPLAY  (1 << 1)
#define INIT_WIFI     (1 << 2)
#define INIT_ALL      (INIT_SENSOR | INIT_DISPLAY | INIT_WIFI)

// 传感器任务
void vSensorInit(void *p)
{
    InitSensor();
    xEventGroupSetBits(xInitEvent, INIT_SENSOR);
    // 等待所有模块都初始化完成
    xEventGroupWaitBits(xInitEvent, INIT_ALL, pdFALSE, pdTRUE, portMAX_DELAY);
    // 所有模块就绪,开始正式工作
    for(;;) { /* ... */ }
}

❓ 面试题:事件组 vs 信号量?

特性 事件组 信号量
传递信息量 多个标志位 单个计数
等待条件 AND / OR 组合 单一条件
一对多通知 ✅ 一次SetBits可唤醒多个任务 ❌ 一次Give只唤醒一个
数据传递 ❌ 只有0/1标志 ❌ 只有计数

三、任务通知(Task Notification)

3.1 任务通知的本质

每个任务自带一个 32位通知值 + 通知状态,可以替代轻量级信号量/队列/事件组

优点

  • 零额外RAM:不需要创建内核对象
  • 更快:比信号量快约 45%
  • 更省内存:不需要队列/信号量的结构体

限制

  • 只能 一对一(只能通知特定任务)
  • 不能在接收端等待多个发送者

3.2 任务通知 API

// 方式1:模拟二值信号量
xTaskNotifyGive(xTaskHandle);                    // 通知值+1
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);         // pdTRUE=取后清零, pdFALSE=取后-1

// 方式2:模拟事件组(按位操作)
xTaskNotify(xTaskHandle, (1<<0)|(1<<2), eSetBits);  // 设置Bit0和Bit2
xTaskNotifyWait(0, 0xFFFFFFFF, &ulNotifiedValue, portMAX_DELAY);

// 方式3:模拟队列(发送32位值)
xTaskNotify(xTaskHandle, ulValue, eSetValueWithOverwrite);    // 覆盖写
xTaskNotify(xTaskHandle, ulValue, eSetValueWithoutOverwrite); // 不覆盖
xTaskNotifyWait(0, 0, &ulReceived, portMAX_DELAY);

// ISR 版本
vTaskNotifyGiveFromISR(xTaskHandle, &xWoken);
xTaskNotifyFromISR(xTaskHandle, ulValue, eSetBits, &xWoken);

3.3 通知动作类型

eAction 效果 模拟
eSetBits 通知值 |= ulValue 事件组
eIncrement 通知值++ 计数信号量
eSetValueWithOverwrite 通知值 = ulValue 邮箱(覆盖)
eSetValueWithoutOverwrite 如果未读才写入 长度1的队列
eNoAction 只通知不改值 二值信号量

3.4 实战:用任务通知替代信号量

TaskHandle_t xReceiverHandle;

// 发送端(ISR)
void UART_IRQHandler(void)
{
    BaseType_t xWoken = pdFALSE;
    vTaskNotifyGiveFromISR(xReceiverHandle, &xWoken);
    portYIELD_FROM_ISR(xWoken);
}

// 接收端
void vUartProcessTask(void *p)
{
    for(;;)
    {
        // 等效于 xSemaphoreTake(),但更快更省内存
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        ProcessUartData();
    }
}

四、三大通信机制对比总结

特性 队列 事件组 任务通知
传递数据 ✅ 任意大小 ❌ 仅标志位 ⚠️ 仅32位值
多对多 ❌ 仅一对一
缓冲能力 ✅ FIFO缓冲 ❌ 仅1个值
AND/OR等待 ⚠️ 手动判断
RAM开销 高(队列体+缓冲区) 中(事件组结构体) (内置)
速度 最快
ISR可用 ✅ FromISR ✅ FromISR ✅ FromISR

选型口诀:

  • 传数据 → 队列
  • 多条件组合等待 → 事件组
  • 一对一轻量通知 → 任务通知
  • 不确定 → 先用队列(最通用)

五、队列集(Queue Set)

当一个任务需要同时等待多个队列/信号量时:

// 开启: #define configUSE_QUEUE_SETS 1

QueueSetHandle_t xQueueSet = xQueueCreateSet(10);  // 总容量

xQueueAddToSet(xQueue1, xQueueSet);
xQueueAddToSet(xQueue2, xQueueSet);
xQueueAddToSet(xSemaphore, xQueueSet);

void vListenerTask(void *p)
{
    for(;;)
    {
        // 等待集合中任一成员就绪
        QueueSetMemberHandle_t xActivatedMember =
            xQueueSelectFromSet(xQueueSet, portMAX_DELAY);

        if(xActivatedMember == xQueue1) {
            xQueueReceive(xQueue1, &data1, 0);
        } else if(xActivatedMember == xQueue2) {
            xQueueReceive(xQueue2, &data2, 0);
        } else if(xActivatedMember == xSemaphore) {
            xSemaphoreTake(xSemaphore, 0);
        }
    }
}

六、常见面试题

1. 队列是值传递还是引用传递?有什么影响?

值传递(拷贝)

  • 优点:发送后原始数据可以立即修改/释放,数据安全
  • 缺点:大数据拷贝开销大
  • 技巧:大数据时发送指针sizeof(void*)),但要确保指针指向的内存在接收方处理前不被释放

2. 队列满了发送会怎样?空的时候接收呢?

  • 队列满时 Send:根据超时参数决定(0=立即返回失败 / portMAX_DELAY=一直阻塞 / N=等N ticks)
  • 队列空时 Receive:同理阻塞或返回
  • 多个任务等待同一个队列时,优先级最高的先操作

3. 任务通知相比信号量有什么优缺点?

优点:速度快45%、零额外RAM、API更简洁

缺点:只能一对一通知、不能广播、缓冲深度只有1

最佳实践:能用任务通知替代的场景(如ISR通知任务),优先用任务通知

4. 事件组的 AND 和 OR 模式分别用在什么场景?

  • ANDpdTRUE):等待所有条件都满足。如系统初始化:传感器就绪 AND 网络就绪 AND 显示就绪
  • ORpdFALSE):任一条件满足即可。如等待任意一个传感器有数据

七、总结

  • 掌握队列的创建、发送、接收 API
  • 理解队列的阻塞机制和超时参数
  • 理解事件组的 AND/OR 等待模式
  • 掌握任务通知的三种使用方式
  • 能根据场景选择合适的通信机制