FreeRTOS中怎么创建队列?为什么要使用队列?
队列是一种先进先出(FIFO)的数据结构,支持入队(Enqueue)和出队(Dequeue)操作。在FreeRTOS中,队列具有重要作用:1) 实现任务解耦,使生产者和消费者无需直接交互;2) 提供任务同步与阻塞机制,提高CPU利用率;3) 保证数据安全,通过临界区保护实现线程安全;4) 作为缓冲层应对流量高峰。队列创建方式包括动态分配(运行时)和静态分配(编译时),各有适用场景。常用操作包括复位
摘要:队列是一种先进先出(FIFO)的数据结构,支持入队(Enqueue)和出队(Dequeue)操作。在FreeRTOS中,队列具有重要作用:1) 实现任务解耦,使生产者和消费者无需直接交互;2) 提供任务同步与阻塞机制,提高CPU利用率;3) 保证数据安全,通过临界区保护实现线程安全;4) 作为缓冲层应对流量高峰。队列创建方式包括动态分配(运行时)和静态分配(编译时),各有适用场景。常用操作包括复位、删除、读写队列等,其中xQueueSend和xQueueReceive是最核心的函数。特别需要注意的是,中断服务程序(ISR)中的队列操作不能阻塞,必须使用带FromISR后缀的特殊函数。队列机制有效解决了嵌入式系统中的任务通信、数据同步和资源竞争等问题。
目录
3. pucQueueStorageBuffer (存储区指针)
一、什么是队列
队列(Queue) 是一种遵循 “先来后到” 原则的数据结构,在计算机科学中,这种特性被称为 FIFO(First In, First Out,先进先出)。
核心操作:
-
入队 (Enqueue): 在队列的末尾添加一个新元素。
-
出队 (Dequeue): 从队列的前端移除一个元素。
二、为什么要使用队列
1. 解耦任务(Decoupling)
(1)在裸机编程中,如果你想让传感器数据触发屏幕显示,通常要在同一个循环里处理。
(2)在 FreeRTOS 中:
-
发送者只需把数据丢进队列,不用管谁来处理。
-
接收者只需从队列拿数据,不用管数据是谁发的。
这种解耦让代码结构更清晰,方便多人协作开发。
2. 任务同步与阻塞机制(Blocking & Sync)
-
自动休眠: 如果一个任务尝试从空队列读或从满队列写数据,FreeRTOS 会自动让该任务进入“阻塞态”。系统调度器会把它从“就绪列表”移除,放进“等待列表”,CPU 此时去运行其他有意义的任务,完全不浪费功耗。
这比在循环里用
if(data_ready)这种“忙等”方式要高效得多,能极大地提升 CPU 利用率。
-
唤醒:
(1)当数据到达时,系统会立即唤醒
a.等待该数据的最高优先级的任务
b.多个同优先级的任务等待时会唤醒等待最久的任务。
(2)超过有限等待时间,系统也会被唤醒。
调用 xQueueReceive(xQueue, &data, xTicksToWait) 时,阻塞参数 xTicksToWait 有三种设置方式:
-
不等待 (0):若队列为空,函数立即返回 errQUEUE_EMPTY。
-
有限等待 (1 ~ pdMS_TO_TICKS(500)):设置超时时间(如 500ms)。在超时前若有数据到达,任务将被唤醒;若超时后仍无数据,则返回错误代码。
-
永久等待 (portMAX_DELAY):任务将一直阻塞,直到队列中有数据到达才会被唤醒。
中断服务程序 (ISR) 绝对不能阻塞:如果在中断里调用
xQueueReceive并设置了等待时间,系统会直接断言报错 (Assert)
3. 数据安全(线程安全)
在多任务环境下,如果两个任务同时修改同一个全局变量,会发生数据损坏。
-
FreeRTOS 的队列函数(如
xQueueSend)内部自带临界区保护。 -
它保证了即使有多个任务争抢,数据的存取也是原子的、安全的。
例子:
当一个全局变量
count = 10,两个任务都想对它执行count++:
-
任务 A 读取
count到寄存器。 -
突然发生切换! 任务 A 还没来得及加 1,任务 B 抢占了 CPU。
-
任务 B 读取
count,加到 11,写回内存。 -
任务 A 恢复,继续它之前的操作:把寄存器里的 10 加到 11,写回内存。
count+=1在底层汇编语言中分三步:
- 取出count的数据大小
LDR R0, [R1]- 对count计算
ADDS R0, R0, #1- 将计算完的值写回
STR R0, [R1]
结果: 两次自增操作,最终 count 是 11 而不是 12。
4. 缓冲与流量削峰
-
异步处理: 比如中断服务程序(ISR)产生数据的速度极快,而处理任务较慢。队列可以充当缓冲区,防止数据丢失。
-
通过值传递: FreeRTOS 队列默认采用“拷贝”方式(Pass by copy)。数据一旦入队,原始变量被修改也不会影响队列里的值。
三、队列创建
在 FreeRTOS 中创建队列主要有两种方式:动态分配(最常用)和静态分配。
1.动态创建队列函数
QueueHandle_t xQueueCreate(
UBaseType_t uxQueueLength, // 队列长度(能容纳多少个项目)
UBaseType_t uxItemSize // 每个项目的大小(字节)
);
返回值: 非0即成功,返回句柄,以后使用句柄来操作队列
NULL:失败,因为内存不足
假设我们要传递一个包含传感器数据的结构体:
- 先定义一个存储变量结构体
- 定义队列句柄
- 使用创建队列函数创建
// 1. 定义数据结构
typedef struct {
uint32_t id;
float value;
} Message_t;
// 2. 定义队列句柄
QueueHandle_t xSensorQueue;
void vSetup() {
// 3. 创建队列:长度为 10,每个单元大小为 Message_t 的体积
xSensorQueue = xQueueCreate(10, sizeof(Message_t));
if (xSensorQueue != NULL) {
// 队列创建成功
} else {
// 内存不足,创建失败
}
}
2.静态创建队列 xQueueCreateStatic
QueueHandle_t xQueueCreateStatic(
UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorageBuffer,
StaticQueue_t *pxQueueBuffer
);
参数详细拆解
1. uxQueueLength (队列长度)
-
含义:队列中最多可以容纳多少个“项目”。
-
注意:这是项目的个数,不是字节数。
2. uxItemSize (项目大小)
-
含义:每个项目的字节数(Byte)。
-
技巧:通常使用如
sizeof(Struct Sensor_data)来确保准确。
3. pucQueueStorageBuffer (存储区指针)
-
含义:这是存放实际数据的“仓库”。
-
要求:它必须是一个
uint8_t类型的数组,其大小至少为(uxQueueLength * uxItemSize)。 -
物理意义:入队的数据会被复制到这里。
4. pxQueueBuffer (控制结构指针)
-
含义:这是存放队列元数据的“办公室”。
-
类型:指向一个
StaticQueue_t类型的变量。 -
物理意义:FreeRTOS 用它来记录队列的状态(比如现在谁在排队、读写指针在哪等)。
#define QUEUE_LENGTH 5
#define ITEM_SIZE sizeof(uint32_t)
// 预留内存:队列内部控制结构 + 实际存储数据的缓冲区
StaticQueue_t xStaticQueue;
uint8_t ucQueueStorageArea[ QUEUE_LENGTH * ITEM_SIZE ];
void vSetup() {
QueueHandle_t xHandle;
xHandle = xQueueCreateStatic(
QUEUE_LENGTH,
ITEM_SIZE,
ucQueueStorageArea,
&xStaticQueue
);
}
关键注意事项
-
队列长度选择: 长度过大会浪费 RAM,过小会导致生产者频繁进入阻塞状态。
-
数据大小: 如果传递的数据非常大,建议在队列中传递指针,而不是整个数据块,以提高效率。
四、两种创建方式对比
| 特性 | 动态分配 (Dynamic) | 静态分配 (Static) |
| 分配时机 | 运行时 (Runtime) | 编译阶段 (Compile time) |
| 内存位置 | FreeRTOS Heap (堆) | 全局/静态变量区 (RAM) |
| 创建失败风险 | 有(堆空间不足或碎片化) | 无(编译过得去就一定能用) |
在内存极小(如 8KB RAM),设备要连续运行数月/数年的单片机上,使用静态分配。
常用函数解释
五、队列常用的函数
//复位
BaseType_t xQueueReset( QueueHandle_t pxQueue); //“一键清空”队列,并将其恢复到初始状态。
//删除
void vQueueDelete( QueueHandle_t xQueue );//销毁队列、释放资源
//写队尾
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
//写队头
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
//中断写队尾
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken // 重点:用于触发上下文切换
);
//中断写队头
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
//任务中读队列
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait
);
//在中断中读队列
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken
);
1.复位队列
BaseType_t xQueueReset( QueueHandle_t pxQueue); //“一键清空”队列,并将其恢复到初始状态。
当你调用 xQueueReset(xQueue) 时,FreeRTOS 会在内部执行以下动作:
-
数据作废: 将队列中的现有消息数量直接设为 0。
-
指针复位: 将队列的读指针和写指针重新指向存储区的起始位置。
-
状态刷新: 队列恢复到刚刚被
xQueueCreate创建出来时的“空”状态。
核心注意:
xQueueReset不会真正擦除存储区里的字节(它只是逻辑上标记为无效),更不会释放内存。它仅仅是重置了队列的管理状态。
2.删除队列
void vQueueDelete( QueueHandle_t xQueue );//销毁队列、释放资源
-
释放内存:如果队列是动态创建(
xQueueCreate)的,调用此函数后,FreeRTOS 会将队列占用的堆内存(控制块和存储区)归还给系统。 -
失效句柄:调用后,该队列句柄
xQueue不再指向任何有效的资源
注意:如果有任务正在因为这个队列而阻塞(比如 Task A 正在
xQueueReceive死等消息),不要直接删除队列。删除会导致Task A 将永远处于阻塞态,或者在被唤醒时访问已释放的非法内存,直接导致 HardFault(系统崩溃)。
3.写队列
(1)写在队尾:
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
这是 FreeRTOS 中使用频率最高的函数。它负责将数据“塞进”队列。如果你把队列看作一个传送带,xQueueSend 就是那个往传送带上放货物的动作。
参数解析:
-
xQueue(队列句柄) -
pvItemToQueue(数据指针): 指向你想要发送的数据的指针。 -
xTicksToWait(阻塞超时时间)
(2)写在队头
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
(3)中断中写入队尾
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken // 重点:用于触发上下文切换
);
BaseType_t *pxHigherPriorityTaskWoken:告诉 CPU,中断结束后是否需要立刻切换到一个更高优先级的任务。
定义变量初始值 xHigherPriorityTaskWoken = pdFALSE;
如果 xQueueSend 解锁了一个高优先级任务,FreeRTOS 会在函数内部自动进行任务切换。但是在中断函数中不同
-
中断具有最高的优先级,它会强行打断当前正在运行的任务。
-
如果在中断里发了一个消息给队列,而这个消息刚好让一个更高优先级的任务从“阻塞”变成了“就绪”。
-
按照 RTOS 的原则,我们应该立刻去执行那个高优先级任务,而不是回到刚才被打断的低优先级任务。
示例:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
uint32_t ulEventCode = 100;
// 在中断中写队列
xQueueSendFromISR(xEventQueue, &ulEventCode, &xHigherPriorityTaskWoken);
// 如果发送导致了高优先级任务解锁,立即进行任务切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
portYIELD_FROM_ISR作用:在中断结束的一瞬间,强制触发一次任务调度(上下文切换)。
没有调用 portYIELD_FROM_ISR的情况:
-
功能依然正确:高优先级任务最终还是会运行。
-
实时性变差:高优先级任务不会在中断结束时立刻运行,而是要等到下一次系统节拍中断发生时才会被调度。可能会带来延迟。
(4)中断中写入队头
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
4.读队列
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait
);
//在中断中
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken
);
区别:
-
xQueueReceive:如果队列是空的,可以指定等待时间,进入阻塞。比如设置portMAX_DELAY -
xQueueReceiveFromISR:中断服务程序(ISR)必须执行得尽可能快。如果队列是空的,它会立即返回errQUEUE_EMPTY,绝不会停下来等。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐
所有评论(0)