c
QueueHandle_t xQueue;
void vSenderTask(void *pvParameters) {
int data = 0;
while(1) {
data++;
xQueueSend(xQueue, &data, portMAX_DELAY);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void vReceiverTask(void *pvParameters) {
int receivedData;
while(1) {
if(xQueueReceive(xQueue, &receivedData, portMAX_DELAY) == pdPASS) {
printf("Received: %d\r\n", receivedData);
}
}
}
下面将结合上述代码示例,详细讲解 FreeRTOS 中的队列(Queue)通信机制 。队列是 FreeRTOS 中任务间(或任务与中断间)传递数据的主要手段,它提供了线程安全 的、基于拷贝的数据交换方式,并支持阻塞等待。
一、队列的基本概念
- 队列是一个"先入先出"(FIFO)的线性表,用于存储固定大小的数据单元。
- 每个队列内部维护一段连续的内存缓冲区,以及读指针、写指针和记录当前消息个数的计数器。
- 核心特点 :
- 数据拷贝:发送时将数据拷贝到队列内部缓冲区,接收时将数据从队列缓冲区拷贝到用户变量。因此队列中保存的是数据的副本,而非指针或引用。
- 多对多通信:多个任务可以向同一个队列发送消息,多个任务也可以从同一个队列接收消息。
- 阻塞机制:当队列满时,发送任务可选择阻塞等待空间;当队列空时,接收任务可选择阻塞等待消息。阻塞超时时间可配置。
- 线程安全:所有队列 API 内部均使用临界区或关中断保护,无需用户额外加锁。
二、代码结构分析
代码片段包含了两个任务函数 vSenderTask 和 vReceiverTask,以及一个全局的队列句柄 xQueue。为了完整运行,还需要在 main 函数中创建队列和任务。下面逐部分讲解。
2.1 队列的创建(代码中未显示,但必不可少)
c
// 创建队列:每个消息大小为 sizeof(int),队列深度为 5(举例)
xQueue = xQueueCreate(5, sizeof(int));
if (xQueue == NULL) {
// 创建失败处理
}
xQueueCreate(5, sizeof(int)):创建能存放 5 个int类型数据的队列。- 返回值:成功返回队列句柄(
QueueHandle_t),失败返回NULL(通常因内存不足)。
2.2 发送任务 vSenderTask
c
void vSenderTask(void *pvParameters) {
int data = 0;
while(1) {
data++;
xQueueSend(xQueue, &data, portMAX_DELAY); // 发送数据
vTaskDelay(1000 / portTICK_PERIOD_MS); // 延时 1 秒
}
}
xQueueSend等价于xQueueSendToBack(向队列尾部写入)。- 参数1
xQueue:目标队列句柄。 - 参数2
&data:待发送数据的指针。队列会将这sizeof(int)字节的数据拷贝到其内部缓冲区。 - 参数3
portMAX_DELAY:阻塞最大时间。这里表示若队列已满,任务将无限期等待(直到队列有空位)。
- 参数1
vTaskDelay:使任务进入阻塞态1000 ms,让出 CPU。这样发送频率为每秒一次。
2.3 接收任务 vReceiverTask
c
void vReceiverTask(void *pvParameters) {
int receivedData;
while(1) {
if(xQueueReceive(xQueue, &receivedData, portMAX_DELAY) == pdPASS) {
printf("Received: %d\r\n", receivedData);
}
}
}
xQueueReceive:从队列头部取出一个消息,并删除该消息。- 参数2
&receivedData:接收缓冲区的指针,用于存放拷贝出的数据。 - 参数3
portMAX_DELAY:若队列为空,任务无限期等待直到有消息到达。
- 参数2
- 返回值
pdPASS表示成功接收到数据;pdFALSE(或errQUEUE_EMPTY)表示超时未收到。
三、工作流程与调度细节
假设创建队列深度为 5,以下展示典型运行流程:
- 初始状态:队列为空。
- 接收任务 :调用
xQueueReceive,因为队列空,接收任务进入阻塞态(等待消息)。 - 发送任务 :运行 →
data++(变成 1)→xQueueSend成功(队列空 → 变为 1 个消息)→vTaskDelay进入阻塞 1 秒。 - 此时接收任务 :因为有消息到达,从阻塞态被唤醒,取出数据
1并打印。然后再次调用xQueueReceive,队列再度变空,重新阻塞。 - 1 秒后 :发送任务恢复运行,发送
2,再次延时,接收任务收到2......循环往复。
如果发送速度超过接收速度,队列会逐渐填满。一旦队列满,发送任务调用 xQueueSend 时就会阻塞(参数 portMAX_DELAY 导致永久阻塞),直到接收任务取走一条消息,腾出空间。
四、队列的关键 API 说明
| 函数 | 作用 | 中断安全版本 |
|---|---|---|
xQueueCreate |
创建队列 | - |
xQueueSend / xQueueSendToBack |
从队尾发送数据 | xQueueSendToBackFromISR |
xQueueSendToFront |
从队首发送数据(LIFO 行为) | xQueueSendToFrontFromISR |
xQueueReceive |
从队首接收并删除数据 | xQueueReceiveFromISR |
xQueuePeek |
从队首接收但不删除数据 | xQueuePeekFromISR |
uxQueueMessagesWaiting |
查询队列中当前消息个数 | uxQueueMessagesWaitingFromISR |
- 阻塞时间 :单位是 tick,可以使用
pdMS_TO_TICKS(ms)转换毫秒。portMAX_DELAY表示无限等待(前提是INCLUDE_vTaskSuspend为 1)。 - 中断中使用 :必须调用带
FromISR后缀的版本,并传递pxHigherPriorityTaskWoken参数以决定是否在中断退出后执行上下文切换。
五、队列通信的优势与适用场景
优势
- 解耦:发送方和接收方不必知道彼此的存在,只需共享队列句柄。
- 数据完整性:通过拷贝方式避免共享内存的竞态条件。
- 自然实现阻塞同步:可以用队列空/满来实现任务间的同步(例如生产者-消费者模型)。
- 灵活的超时控制:既可以通过超时实现轮询,也可以通过无限等待实现精准同步。
适用场景
- 生产者-消费者模型(如传感器数据采集、日志打印)。
- 命令传递(如 GUI 任务向工作任务发送指令)。
- 多源数据汇聚(多个任务向同一队列发送,由一个任务集中处理)。
六、注意事项
- 队列大小与内存:队列内部缓冲区在创建时分配,若队列中消息长度较大(如结构体),需权衡内存占用。
- 任务优先级设定 :
- 如果生产者优先级高于消费者,且队列深度有限,生产者可能长期阻塞导致高优先级任务无法运行(反直觉)。
- 通常让消费者优先级稍高于生产者,确保数据被及时处理。
- 不要在中断中阻塞 :中断服务程序中只能使用非阻塞版本(超时参数为 0)或
FromISR版本。 - 数据拷贝开销:对于大数据(如几 KB 的数组),频繁拷贝会占用 CPU。此时可考虑用队列传递指向缓冲区的指针(但需自行管理内存和互斥)。
- 队列重置 :
xQueueReset可以清空队列所有消息,但要注意正在阻塞的任务处理。
七、扩展:队列集(Queue Set)
FreeRTOS 还提供了队列集 ,允许一个任务同时等待多个队列(或信号量)上的消息,类似 select/poll 机制。当任意一个队列有数据时,任务被唤醒并得知是哪个队列就绪。
总结
代码示例展示了 FreeRTOS 队列通信的基本模式:
- 创建队列。
- 发送任务周期性地调用
xQueueSend入队数据(可能阻塞)。 - 接收任务调用
xQueueReceive取出数据(可能阻塞)。
队列作为 FreeRTOS 的核心组件,使用简单、功能强大,是嵌入式多任务编程中不可或缺的通信工具。掌握其原理和 API,可以构建出高内聚、低耦合的实时系统。
附录
工程架构参考
user_program.c
- 可以在自己的工程目录main.c调用该函数
c
/**
* @brief 主应用函数:
*
* @note None
* @param None
* @return None
* @warning 此函数不会返回,内部包含无限循环
* @remark 通过函数指针从main()调用,支持灵活的启动架构
*/
void Current_Program3(void)
{
void vSenderTask(void *pvParameters);
void vReceiverTask(void *pvParameters);
LED_Init(); // 初始化LED设备
UART1_Config(); // UART1串口初始化
// 创建队列:每个消息大小为 sizeof(int),队列深度为 5(举例)
xQueue = xQueueCreate(5, sizeof(int));
if (xQueue == NULL) {
// 队列创建失败处理(例如死循环、点亮错误指示灯等)
while(1);
}
xTaskCreate(vSenderTask, "vSenderTask", 128, NULL, 1, NULL);
xTaskCreate(vReceiverTask, "vReceiverTask", 128, NULL, 1, NULL);
// 启动调度器(永远不会返回)
vTaskStartScheduler();
// 理论上程序不会执行到这里,但为了安全,可以加一个死循环
while(1){
}
}
user_task.c
c
/* 队列通信 */
QueueHandle_t xQueue;
// 发送任务 vSenderTask
void vSenderTask(void *pvParameters) {
int data = 0;
while(1) {
data++;
xQueueSend(xQueue, &data, portMAX_DELAY);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
// 接收任务 vReceiverTask
void vReceiverTask(void *pvParameters) {
int receivedData;
while(1) {
if(xQueueReceive(xQueue, &receivedData, portMAX_DELAY) == pdPASS) {
printf("Received: %d\r\n", receivedData);
}
}
}
user_task.h
c
/**
* @file user_task.c
* @brief user task module
* @author xyx-3v
* @date 2026-04-28
* @version 1.0
* @note
*/
#ifndef __USER_TASK_H
#define __USER_TASK_H
#include <stdint.h>
/* FreeRTOS三方库 */
#include "FreeRTOSConfig.h"
#include "FreeRTOS.h"
#include "task.h" // vTaskDelay等
#include "queue.h" // QueueHandle_t
/* 队列通信 */
extern QueueHandle_t xQueue;
// 发送任务 vSenderTask
void vSenderTask(void *pvParameters);
// 接收任务 vReceiverTask
void vReceiverTask(void *pvParameters);
#endif /* __USER_TASK_H */
实验现象演示
