第三篇FreeRTOS 通信机制 — 队列、事件组与任务通知
- 嵌入式开发
- 1天前
- 23热度
- 0评论
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() 为例,完整的执行流程如下:

关键要点:
- 阻塞不浪费 CPU:任务阻塞后进入 Blocked 状态,CPU 资源交给其他就绪任务。
- 优先级抢占:多个任务等待同一个队列时,优先级最高的任务先获得操作权。
- ISR 中禁止阻塞:中断版函数(
FromISR)没有超时参数,操作失败立即返回。
1.5 实验:按键收发队列
实验目标:创建一个队列,按下 KEY_B 向队列发送递增数据,按下 KEY_C 从队列读取数据,通过串口打印验证。
CubeMX 配置

初始化代码(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);
}
}
运行结果

实验要点:
- 两次读取 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连接 按键 温度就绪

事件组的核心特点:
- 无数据传递:只能传递"事件是否发生"(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配置

代码和运行
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 模式分别用在什么场景?
- AND(
pdTRUE):等待所有条件都满足。如系统初始化:传感器就绪 AND 网络就绪 AND 显示就绪 - OR(
pdFALSE):任一条件满足即可。如等待任意一个传感器有数据
七、总结
- 掌握队列的创建、发送、接收 API
- 理解队列的阻塞机制和超时参数
- 理解事件组的 AND/OR 等待模式
- 掌握任务通知的三种使用方式
- 能根据场景选择合适的通信机制