消息队列(Message Queue)(以下简称队列)是 FreeRTOS 中一种 线程安全 的通信机制,允许任务或中断服务程序(ISR)之间以 异步方式 传递数据。它通过 FIFO(先进先出) 的缓冲区管理数据,支持变长消息、阻塞访问和超时控制,是多任务系统中数据交换的核心组件之一。
1. 为什么要引入队列?
在 FreeRTOS 或多任务系统中,“消息队列可用于数据共享” 指的是通过队列机制,实现不同任务或中断服务程序(ISR)之间 安全、有序、异步地传递数据。其核心在于解决传统全局变量共享数据时的线程安全问题,并提供阻塞/唤醒机制以优化资源利用。
问题:若多个任务或中断直接读写同一全局变量或数组,任务A在修改变量时被任务B打断,导致数据不一致。
int counter = 0; // 全局变量
void Task1(void) {
counter++; // 可能被打断
}
void Task2(void) {
counter--; // 导致counter最终值不确定
}
任务A和任务C均将传递同一变量数据给任务B时,引入队列
2. 如何实现将任务中数据写入队列?
xQueueSend()
是 FreeRTOS 中用于向消息队列发送数据的核心 API(应用程序编程接口),实现任务间或任务与中断之间的线程安全数据传递。
xQueueSend()向队列中写入数据,可简化为一下3个步骤:
(1)短暂关闭中断或调度器,确保拷贝数据时不被中断;
(2)向队列中写入数据;
(3)打开中断或调度器。
xQueueSend()
是 FreeRTOS 中用于向消息队列发送数据的核心 API,实现任务间或任务与中断之间的线程安全数据传递。以下是其工作原理、使用方法和注意事项的全面解析:
2.1 函数原型
BaseType_t xQueueSend(
QueueHandle_t xQueue, // 目标队列句柄
const void *pvItemToSend, // 待发送数据的指针
TickType_t xTicksToWait // 阻塞超时时间
);
2.2 参数说明
参数 | 类型 | 说明 |
---|---|---|
xQueue | QueueHandle_t | 队列句柄,由 xQueueCreate() 创建。 |
pvItemToSend | const void* | 指向待发送数据的指针,数据会被拷贝到队列中。 |
xTicksToWait | TickType_t | 队列满时的最大阻塞时间: • 0 :立即返回(非阻塞)• portMAX_DELAY :永久阻塞 |
2.3 返回值
返回值 | 含义 |
---|---|
pdPASS (1) | 数据成功写入队列。 |
errQUEUE_FULL (0) | 队列已满且未在超时时间内等到空闲空间(非阻塞或超时)。 |
3. 队列的休眠与唤醒机制
在 FreeRTOS 中,队列的休眠与唤醒机制 是任务阻塞和调度的核心部分,它通过 任务阻塞列表 和 事件触发机制 实现高效的任务同步。
3.1 休眠(阻塞)的触发条件
当任务调用队列操作函数(如 xQueueSend()
或 xQueueReceive()
)时,若队列不满足操作条件,任务将进入休眠(阻塞)状态:
-
xQueueSend()
阻塞:队列已满,无法写入新数据。 -
xQueueReceive()
阻塞:队列为空,无数据可读。
QueueHandle_t xQueue = xQueueCreate(5, sizeof(int)); // 队列容量为5
void vSenderTask(void *pvParameters) {
int data = 42;
// 若队列满,任务将阻塞(休眠)直到超时或队列有空间
xQueueSend(xQueue, &data, portMAX_DELAY);
}
3.2 休眠的底层操作流程
(1) 任务挂起到阻塞列表
-
步骤:
-
任务从 就绪列表(Ready List) 移除。
-
根据阻塞原因,挂载到队列的 发送阻塞列表(链表)(
xTasksWaitingToSend
)或 接收阻塞列表(xTasksWaitingToReceive
)。 -
记录唤醒时间(若设置了超时
xTicksToWait
)。
-
-
关键数据结构(队列内部):
typedef struct QueueDefinition { List_t xTasksWaitingToSend; // 发送阻塞列表 List_t xTasksWaitingToReceive; // 接收阻塞列表 // ...其他字段(缓冲区、头尾指针等) } Queue_t;
(2) 任务状态切换
-
任务状态由 运行态(Running) 变为 阻塞态(Blocked)。
-
调度器触发上下文切换,执行其他就绪任务。
3.3 唤醒的触发条件
当队列状态变化时,系统检查阻塞列表并唤醒任务:
-
xQueueSend()
唤醒:队列有空间后,唤醒接收阻塞列表中的任务(若有数据可读)。 -
xQueueReceive()
唤醒:队列有数据后,唤醒发送阻塞列表中的任务(若有空间可写)。
示例场景
-
队列初始为空:
-
任务A调用
xQueueReceive()
被阻塞,挂到xTasksWaitingToReceive
。
-
-
任务B发送数据:
-
任务B调用
xQueueSend()
写入数据,并检查xTasksWaitingToReceive
。 -
发现任务A在等待,将其从阻塞列表移回 就绪列表。
-
4. 唤醒的底层操作流程
(1) 检查阻塞列表
-
发送数据时(
xQueueSend()
):if (listLIST_IS_EMPTY(&xQueue->xTasksWaitingToReceive) == pdFALSE) { xTaskRemoveFromEventList(&xQueue->xTasksWaitingToReceive); // 唤醒接收任务 }
-
接收数据时(
xQueueReceive()
):if (listLIST_IS_EMPTY(&xQueue->xTasksWaitingToSend) == pdFALSE) { xTaskRemoveFromEventList(&xQueue->xTasksWaitingToSend); // 唤醒发送任务 }
(2) 任务状态恢复
-
被唤醒的任务状态由 阻塞态(Blocked) 变为 就绪态(Ready)。
-
若任务优先级高于当前任务,触发调度器切换(抢占式调度)。
5. 超时唤醒机制
若任务设置了阻塞超时(如 xTicksToWait = 100
),系统会在以下情况唤醒任务:
-
正常唤醒:队列条件满足(如队列从满变为非满)。
-
超时唤醒:
xTickCount
到达xTicksToWait + 阻塞开始时间
,任务被移回就绪列表,API 返回errQUEUE_FULL
或errQUEUE_EMPTY
举例说明:休眠与唤醒机制
假设TaskB中设置某一条件,当达到该条件时,令flag=1;在TaskA中设置:当flag=1时,进行下面的操作,否则不执行。
如果在裸机情况下(无RTOS),那么每次运行完TaskB后,TaskA都要运行判断TaskB中的flag是否等于1,只有满足flag=1时,TaskA才能继续运行下去。那么入TaskB要运行100万次,则相应的TaskA也要进行100万次判断,这可能占用CPU很大的资源。
在RTOS情况下,引入队列机制,当运行TaskB达到设置将flag置1的条件时,之间将其写入队列中,而TaskA中达到flag=1条件时,选择读队列。那么如果TaskB中如果未能达到条件将flag=1,没有写入队列时,队列内容为空,则将TaskA进行阻塞列表;TaskB中如果达到条件将flag=1,写入队列时,则将TaskA进行唤醒,移回就绪列表。这样就会只在TaskB中flag=1时才会运行TaskA,节省CPU资源。
休眠与唤醒的流程图解
6. 队列中的环形缓冲区
环形缓冲区(Circular Buffer 或 Ring Buffer)是 FreeRTOS 队列(Queue)的核心底层数据结构,用于高效管理任务间传递的数据。它通过 循环复用固定大小的内存块,实现 FIFO(先进先出)的数据存取,同时避免动态内存分配的开销。
动态图解过程如下:
-
循环复用:读写指针到达尾部后自动绕回头部(读:可设置为R=(R+1)%LEN;写:可设置为W=(W+1)%LEN,其中LEN为队列空间的大小),避免内存浪费。
-
高效操作:插入/删除时间复杂度为 O(1)。
-
线程安全:FreeRTOS 通过临界区保护指针和计数器。
通过此环形缓冲区设计,FreeRTOS 队列实现了高效、确定性的数据传递,非常适合嵌入式实时系统。
7. 队列的组成
队列由队列的头部(也称为队列控制块)和队列缓冲区组成。
7.1 队列头部的定义
-
队列头部 = 队列控制块(Queue_t 结构体)
它是 FreeRTOS 内部用于管理队列状态的核心数据结构,包含队列的元信息(如读写指针、阻塞列表等),而非实际存储数据的缓冲区。 -
数据缓冲区的起始位置:
队列的 数据存储区 是另一块独立内存(动态或静态分配),由队列控制块中的pcHead
指针指向。
7.2 队列控制块(Queue_t)的关键字段
FreeRTOS 中队列控制块的定义(简化版)
typedef struct QueueDefinition {
int8_t *pcHead; // 指向数据缓冲区起始地址
int8_t *pcTail; // 指向数据缓冲区结束地址
int8_t *pcWriteTo; // 下一个写入位置(移动后自动循环)
int8_t *pcReadFrom; // 下一个读取位置(移动后自动循环)
List_t xTasksWaitingToSend; // 发送阻塞任务列表
List_t xTasksWaitingToReceive; // 接收阻塞任务列表
UBaseType_t uxMessagesWaiting; // 当前队列中的消息数
UBaseType_t uxLength; // 队列最大长度(uxQueueLength)
UBaseType_t uxItemSize; // 单条消息大小(字节)
// ...其他字段(如队列类型、互斥量等)
} Queue_t;
7.3 队列头部(控制块)与数据缓冲区的关系
动态创建队列时
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
-
内存分配:
FreeRTOS 会 同时分配队列控制块和数据缓冲区,并将pcHead
指向缓冲区起始地址。
7.4 为什么需要区分“队列头部”和“数据缓冲区”?
组件 | 作用 | 是否用户可见 |
---|---|---|
队列头部 | 管理队列状态(如读写指针、阻塞任务),由 FreeRTOS 内核维护。 | ❌ 不直接操作(通过句柄访问) |
数据缓冲区 | 实际存储消息内容的内存区域,用户数据通过 API 间接读写。 | ✅ 需指定大小 |
5. 实际应用中的注意事项
-
队列句柄(QueueHandle_t)的本质:
它是指向 队列控制块(头部) 的指针,而非数据缓冲区。QueueHandle_t xQueue = xQueueCreate(5, sizeof(int)); // xQueue 指向控制块
-
直接操作缓冲区的风险:
用户不应绕过xQueueSend()
/xQueueReceive()
直接访问pcHead
指向的内存,否则会破坏线程安全。 -
调试技巧:
通过uxQueueMessagesWaiting()
和uxQueueSpacesAvailable()
监控队列状态,而非直接检查控制块