目录
1. xQueueCreate()创建队列(指定长度和每项大小)
2. xQueueSend() / xQueueSendToBack():向队列尾部发送数据(阻塞可选)
3. xQueueReceive() 从队列接收数据(阻塞可选)
(4)队列接收或者发送函数中,xTicksToWait什么时候用portMAX_DELAY,什么场景?什么时候需要设置具体时间?
4. xQueueSendFromISR()从 ISR 向队列发送(专用于中断)
5. xQueueReceiveFromISR()中断服务函数(ISR)中接收队列数据
6. uxQueueMessagesWaiting()获取当前队列中已有多少条数据
一. 基础概念
1.概念
队列是FreeRTOS中最常见的任务通讯机制之一,它充当一个缓冲区的角色,允许:
-
一个任务“发送”数据进去;
-
另一个任务“接收”数据出来;
-
数据自动排队,先进先出(FIFO);
-
多个任务/中断也可以共用同一个队列(需要注意互斥);
队列是一种先进先出的数据结构,就像我们在学校食堂里排队打饭:前面先来的人先打饭(先被处理),而后面新来的人只能排在队伍的最后(等待);所以,对于队列来说,数据是从队列头(Front)被取出的(xQueueReceive),在队列尾(Back)被添加进去的(xQueueSend),这样能保证发送顺序,数据不会“插队”;
2.消息队列实现的原理
FreeRTOS 中的消息队列本质上是一个内存结构,主要包括:
结构部分
|
作用
|
循环缓冲区
|
实际存放消息的数据空间(FIFO结构);
|
读写指针
|
控制读和写的位置(实现先进先出);
|
计数器
|
跟踪当前存储了多少条消息、队列总容量;
|
等待任务链表
|
等待写入/读取的任务挂起链表;
|
互斥访问机制
|
保证多任务访问时线程安全(使用临界区、调度器挂起等);
|
3.队列的使用场景
队列主要用于 任务之间发送“具体数据”。下面是常见应用场景:
应用场景
|
说明
|
中断→任务传数据
|
ISR采集数据后,通过xQueueSendFromISR发送给任务处理,任务避免做过多处理
|
多个任务→集中处理
|
多个任务把“命令”或“日志”放入队列,由一个集中任务处理,例如日志记录
|
模块解耦通信
|
比如UI任务通过队列向控制任务发送消息,不直接调用函数,降低耦合
|
缓冲作用
|
数据接收频率高于处理频率时,队列可缓冲避免数据丢失
|
生产者-消费者模型
|
一个任务采集/生成数据(生产者),另一个任务处理(消费者)
|
二. 队列相关的函数及示例代码
1. xQueueCreate()创建队列(指定长度和每项大小)
(1)函数原型
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
//参数解释:
// 1. uxQueueLength:队列最多能存多少个元素(不是字节,单位是第2个参数);
// 2. uxItemSize:每个元素的大小(字节),比如你要存 int 类型就写 sizeof(int);
(2)返回值
成功:返回一个队列句柄(后面所有函数都用这个); 失败:返回 NULL(说明内存不够);
(3)代码示例
QueueHandle_t xQueue;
xQueue = xQueueCreate(10, sizeof(int)); // 创建一个可存 10 个 int 的队列
(4)什么时候只要创建一个队列,什么时候需要创建多个队列?
FreeRTOS 的每个队列只能传输固定大小的一种数据类型(例如 int、float、结构体等),所有发送和接收这个队列的任务都必须按照这个类型来处理,所以争对不同的用法区分如下:
①只创建一个队列的情况:数据类型一致,任务功能一致,如:
-
多个传感器上报温度值到同一个处理任务;
-
同一个类型的控制命令传递(如 LED 控制命令);
②需要多个队列的情况:数据类型不同(int vs float vs struct),或者通信方向不同,避免交叉干扰,或者数据源或用途完全不同,比如:
-
一个任务处理温度,另一个处理按键,数据结构不一样;
-
一个任务接收传感器数据,另一个任务收网络数据;
③创建一个队列复用的情况:系统资源有限(如在某些阶段复用同一个队列),或数据类型一致 + 使用场景严格区分 + 不会并发访问,比如:
-
在不同状态机状态中,切换使用同一个队列;
-
某个任务先传感器采集用,后期用于其他目的,但中间清空队列、确认无冲突;
类别 | 场景说明 | 队列个数推荐 | 队列内容类型示例 | 说明与建议 |
传感器采集任务 | 周期性读取温度、湿度、光照等传感器 | 每类传感器一个队列 | SensorData结构体 | 同类数据可共享队列,便于统一处理 |
外部输入任务 | 按键输入、编码器、UART等 | 每种设备一个队列 | uint8_t、char或自定义结构体 | 区分不同源输入,避免混淆处理逻辑 |
控制/状态任务 | 控制继电器、电机、模式切换等 | 一个或多个 | 带命令码的结构体 | 控制中心任务接收各任务消息,统一调度 |
网络通信任务 | 上传传感器数据、接收控制指令 | 上行+下行各一个 | DataMsg/CtrlMsg结构体 | 一般分离处理上传和下发 |
日志/串口输出任务 | 所有任务统一打印/调试信息 | 一个 | LogMsg结构体(字符串) | 减少各任务直接 printf带来的阻塞风险 |
UI显示任务 | OLED/LCD显示数据/状态信息 | 一个 | UiMsg结构体(带来源字段) | UI任务统一更新,显示逻辑集中 |
OTA更新 / 系统控制 | OTA触发、系统重启、低功耗切换等 | 一个 | 控制指令结构体或枚举类型 | 专用控制通道,防止误触发 |
ISR中断通信 | 中断中将事件通知给任务 | 一个短小队列 | 简单标志或ID(uint8_t) | 短小快速,不可阻塞 |
2. xQueueSend() / xQueueSendToBack():向队列尾部发送数据(阻塞可选)
(1)函数原型
BaseType_t xQueueSend( QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait);
//参数1. xQueue:你创建好的队列句柄;
//参数2.pvItemToQueue:要发送的数据地址(是指针!);
//参数3. 如果队列满了,最多等待多少 Tick 时间(0 表示不等待);
(2)返回值
(3)代码示例
3. xQueueReceive() 从队列接收数据(阻塞可选)
(1)函数原型
BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);
//参数1. xQueue:队列句柄;
//参数2. pvBuffer:用来保存接收到数据的地址;
//参数3. xTicksToWait:如果队列为空,最多等多久(0 表示不等);
(2)返回值
pdPASS:发送成功; errQUEUE_FULL:接收失败;
(3)代码示例
//实现:任务 A 每秒发送一个数字,任务 B 接收并打印;
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
QueueHandle_t xQueue;
void TaskA(void *pvParameters) {
int count = 0;
while (1) {
count++;
xQueueSend(xQueue, &count, portMAX_DELAY);
printf("Task A sent: %d\n", count);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void TaskB(void *pvParameters) {
int receivedValue;
while (1) {
if (xQueueReceive(xQueue, &receivedValue, portMAX_DELAY)) {
printf("Task B received: %d\n", receivedValue);
}
}
}
void app_main() {
xQueue = xQueueCreate(5, sizeof(int)); // 队列长度5,每项是 int 类型
if (xQueue == NULL) {
printf("Queue creation failed!\n");//检查队列是否创建成功,失败就退出;
return;
}
xTaskCreate(TaskA, "Sender", 2048, NULL, 1, NULL);
xTaskCreate(TaskB, "Receiver", 2048, NULL, 1, NULL);
}
(4)队列接收或者发送函数中,xTicksToWait什么时候用portMAX_DELAY,什么场景?什么时候需要设置具体时间?
xTicksToWait 是 在队列空(接收)或满(发送)时,任务愿意等待的最大时间(以 Tick 为单位),它决定了任务是 阻塞、非阻塞还是定时等待。下面区分一下不同的使用场景:
①使用 0(非阻塞)的场景
场景类别
|
示例
|
使用理由
|
任务不应该因为队列阻塞
|
状态检测、周期任务
|
快速尝试,无数据就跳过
|
在中断中使用队列函数
|
ISR中用 xQueueSendFromISR
|
中断不能阻塞,只能立即返回
|
任务定期检查某事物状态
|
检查设备是否发回响应
|
若无数据立即返回,执行其他逻辑
|
②使用具体时间(如 pdMS_TO_TICKS(100))的场景:
场景类别
|
示例
|
使用理由
|
实时性要求高,不能长时间阻塞
|
UART 数据收发
|
防止任务卡住系统,需适时超时处理
|
有备选操作或周期性行为
|
传感器采集任务尝试发送数据,失败就记录
|
不能一直卡在队列操作上,否则影响采集节奏
|
处理非关键数据
|
日志打印任务
|
如果队列满,可以丢弃不重要日志,保证系统流畅
|
系统初始化阶段
|
等待其他模块准备就绪
|
超时后可报告错误、重试或进入降级模式
|
③ 使用 portMAX_DELAY 的场景(一直等下去):
场景类别
|
示例
|
使用理由
|
任务接收关键数据,不可漏掉
|
传感器上传数据任务一直等队列有数据
|
数据必须接收,不能错过,因此等到为止
|
系统空闲时等待事件发生
|
按键检测任务或状态机任务
|
等事件触发,不用浪费 CPU 资源
|
接收队列作为任务的触发机制
|
控制任务仅在接收到命令时运行
|
使用队列“唤醒”任务是一种高效的事件驱动方式
|
4. xQueueSendFromISR()从 ISR 向队列发送(专用于中断)
它是中断服务程序(ISR)中往队列里发送数据的函数。作用就像 xQueueSend(),但是普通的 xQueueSend()不能在 ISR 中使用,否则会造成系统崩溃或不确定行为,所以是 xQueueSendFromISR()函数是专为中断场景设计的,线程安全且效率更高。
(1)函数原型
BaseType_t xQueueSendFromISR( QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken);
//参数1. xQueue:队列句柄(之前用 xQueueCreate() 创建的)
//参数2.pvItemToQueue:要发送的数据地址(指针)
//参数3. pxHigherPriorityTaskWoken:
//指向一个变量,如果发送数据后唤醒了一个更高优先级任务,这个变量会被设为 pdTRUE,让我们中断后立即切换去执行被唤醒的这个更高优先级任务;
(2)返回值
pdPASS(值为 1)表示发送成功,数据已成功放入队列; errQUEUE_FULL(值为 0):队列已满,发送失败;
(3)示例代码
//在一个中断函数中:
void IRAM_ATTR gpio_isr_handler(void* arg)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;//先设置成FALSE;
int val = 123
// 从中断发送数据到队列
xQueueSendFromISR(xQueue, &val, &xHigherPriorityTaskWoken);
// 如果有高优先级任务被唤醒,就请求上下文切换
if (xHigherPriorityTaskWoken == pdTRUE) {
portYIELD_FROM_ISR();
}
}
5. xQueueReceiveFromISR()中断服务函数(ISR)中接收队列数据
(1)函数原型
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxHigherPriorityTaskWoken
);
//参数1. xQueue:队列的句柄,即之前用 xQueueCreate() 创建的队列;
//参数2.pvBuffer:指向变量的指针,用于接收从队列中取出的数据;
//参数3. pxHigherPriorityTaskWoken:
//指向一个变量的指针,用来告知是否需要在中断退出时让系统切换到更高优先级的任务。可以为 NULL(但建议使用);
(2)返回值
pdPASS(值为1)表示接收成功,已经从队列中取出了一个数据项; pdFALSE(值为0)表示队列为空,接收失败;
(3)示例代码
void IRAM_ATTR gpio_isr_handler(void* arg) {
static uint32_t value = 123;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 向队列发送数据(假设你在别处已经创建好了队列 xQueue)
xQueueSendFromISR(xQueue, &value, &xHigherPriorityTaskWoken);
// 接收数据
uint32_t received = 0;
if (xQueueReceiveFromISR(xQueue, &received, &xHigherPriorityTaskWoken) == pdPASS) {
// 接收到的数据 stored in 'received'
}
// 如果需要切换上下文(抢占),通知调度器
if (xHigherPriorityTaskWoken) {
portYIELD_FROM_ISR(); // 退出中断后立刻切换任务
}
}
6. uxQueueMessagesWaiting()获取当前队列中已有多少条数据
(1)函数原型
UBaseType_t uxQueueMessagesWaiting(QueueHandle_t xQueue);
//参数:xQueue是要查询的队列句柄(通过 xQueueCreate() 得到);
(2)返回值
0:队列中没有可接收的消息 ; >0:当前队列中可用的消息数量(最大不会超过队列长度);
(3)示例代码
//示例:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include <stdio.h>
QueueHandle_t xQueue;
void sender_task(void *pvParameters)
{
int count = 0;
while (1) {
xQueueSend(xQueue, &count, portMAX_DELAY);
count++;
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void monitor_task(void *pvParameters)
{
while (1) {
UBaseType_t messages_waiting = uxQueueMessagesWaiting(xQueue);
printf(" 队列中有 %d 条消息等待接收。\n", messages_waiting);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void receiver_task(void *pvParameters)
{
int value;
while (1) {
if (xQueueReceive(xQueue, &value, portMAX_DELAY)) {
printf("接收到消息:%d\n", value);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void)
{
xQueue = xQueueCreate(10, sizeof(int));
xTaskCreate(sender_task, "Sender", 2048, NULL, 2, NULL);
xTaskCreate(receiver_task, "Receiver", 2048, NULL, 2, NULL);
xTaskCreate(monitor_task, "Monitor", 2048, NULL, 1, NULL);
}