FreeRTOS队列的使用
队列是FreeRTOS任务间通信的核心机制。本文将带你掌握队列的创建、写入和读取三个核心操作,通过清晰的代码示例和关键要点。
1. 队列是什么?
在FreeRTOS中,队列是一种先进先出的数据结构,主要用于:
- 任务间的数据传递
- 中断与任务间的通信
- 实现生产者-消费者模型
可以把它想象成一条传送带:生产者把物品放上去,消费者按顺序取走。
2. 第一步:创建队列
2.1 核心函数
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
2.2 完整创建流程
/* ---------- 第一步:创建队列 ---------- */
// 1. 定义要在队列中传递的数据类型
typedef struct {
uint8_t cmd; // 命令类型
uint32_t value; // 数值参数
} message_t; // 消息结构体
// 2. 声明队列句柄(类似文件指针)
QueueHandle_t myQueue;
// 3. 创建队列
myQueue = xQueueCreate(
10, // 队列深度:最多能存10条消息
sizeof(message_t) // 消息大小:每条消息占用的字节数
);
// 4. 【重要】检查创建是否成功
if (myQueue == NULL) {
// 创建失败!通常是内存不足
// 应该进行错误处理,比如重启或报警
while(1); // 示例:死循环等待
}
2.3 创建要点总结
| 项目 | 说明 |
|---|---|
| 队列深度 | 根据实际需求设置,太小容易满,太大浪费内存 |
| 消息大小 | 必须准确计算数据类型的大小 |
| 返回值检查 | 必须检查,NULL表示创建失败 |
| 全局访问 | 队列句柄要能被所有使用它的任务访问 |
3. 第二步:写入队列
3.1 在任务中写入
/* ---------- 第二步:向队列写入数据 ---------- */
// 准备要发送的数据
message_t msg_to_send = {
.cmd = 1, // 命令编号1
.value = 100 // 数值100
};
// 核心写入函数
BaseType_t result = xQueueSend(
myQueue, // 队列句柄
&msg_to_send, // 要发送数据的地址
pdMS_TO_TICKS(100) // 等待时间:最多等100ms
);
// 等待时间参数说明:
// pdMS_TO_TICKS(100) -> 等待100毫秒
// 0 -> 不等待,立即返回
// portMAX_DELAY -> 一直等到队列有空位
// 检查写入结果
if (result != pdPASS) {
// 写入失败:队列始终满,超时返回
// 可以记录日志或采取其他措施
}
3.2 在中断中写入
/* ---------- 中断服务函数中写入 ---------- */
// 【注意】中断中必须使用特殊版本函数
void MyISR_Handler(void)
{
message_t msg = { .cmd = 2, .value = 200 };
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 使用FromISR版本
xQueueSendFromISR(
myQueue,
&msg,
&xHigherPriorityTaskWoken // 传出参数
);
// 检查是否需要立即进行任务切换
if (xHigherPriorityTaskWoken == pdTRUE) {
portYIELD_FROM_ISR(); // 执行一次任务切换
}
}
3.3 写入要点总结
| 场景 | 使用函数 | 关键区别 |
|---|---|---|
| 任务中 | xQueueSend | 可以设置等待时间 |
| 中断中 | xQueueSendFromISR | 不能等待,需要处理唤醒标志 |
4. 第三步:读取队列
4.1 在任务中读取
/* ---------- 第三步:从队列读取数据 ---------- */
// 准备接收数据的变量
message_t received_msg;
// 核心读取函数
BaseType_t result = xQueueReceive(
myQueue, // 队列句柄
&received_msg, // 接收数据的缓冲区
pdMS_TO_TICKS(200) // 等待时间:最多等200ms
);
// 处理读取结果
if (result == pdPASS) {
// 成功读取到数据
printf("收到命令: %d, 数值: %d\n",
received_msg.cmd,
received_msg.value);
// 根据命令执行相应操作
process_command(&received_msg);
} else {
// 读取失败:队列始终为空
// 可能是没有数据或超时
}
4.2 在中断中读取(较少使用)
/* ---------- 中断中读取 ---------- */
void AnotherISR_Handler(void)
{
message_t msg;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueReceiveFromISR(
myQueue,
&msg,
&xHigherPriorityTaskWoken
);
if (xHigherPriorityTaskWoken == pdTRUE) {
portYIELD_FROM_ISR();
}
// 注意:中断中处理要尽量简短
}
5. 关键对比与记忆要点
5.1 任务与中断API对比
| 操作 | 任务中函数 | 中断中函数 | 核心区别 |
|---|---|---|---|
| 写入 | xQueueSend | xQueueSendFromISR | 中断版不能等待 |
| 读取 | xQueueReceive | xQueueReceiveFromISR | 中断版不能等待 |
5.2 一句话记住核心
创建得句柄,写入传地址,读取收数据,中断用FromISR。
6. 实际应用示例
6.1 按键消息处理
// 按键消息定义
typedef struct {
uint8_t key_id; // 按键编号
uint8_t key_state; // 按下/释放
} key_msg_t;
// 创建按键消息队列
QueueHandle_t key_queue = xQueueCreate(5, sizeof(key_msg_t));
// 按键扫描任务(生产者)
void KeyScan_Task(void *param)
{
key_msg_t msg;
while(1) {
if (检测到按键按下) {
msg.key_id = 1;
msg.key_state = 1; // 按下
xQueueSend(key_queue, &msg, 0);
}
vTaskDelay(10); // 10ms扫描一次
}
}
// 按键处理任务(消费者)
void KeyProcess_Task(void *param)
{
key_msg_t msg;
while(1) {
if (xQueueReceive(key_queue, &msg, portMAX_DELAY) == pdPASS) {
if (msg.key_state == 1) {
LED_Toggle(); // 按键按下时切换LED
}
}
}
}
7. 常见问题与解决
7.1 队列满了怎么办?
- 增加队列深度
- 提高消费者任务优先级
- 检查是否有任务没有及时读取
7.2 队列一直空怎么办?
- 检查生产者任务是否正常运行
- 确认数据是否正确发送
- 使用
uxQueueMessagesWaiting()查看队列中消息数量
7.3 内存不足怎么办?
- 减少队列深度
- 减小消息大小
- 优化系统内存分配
8. 总结
FreeRTOS队列的使用可以总结为三个核心操作:
- 创建:
xQueueCreate(深度, 大小)→ 得到队列句柄 - 写入:
xQueueSend(句柄, &数据, 等待时间)→ 发送数据 - 读取:
xQueueReceive(句柄, &变量, 等待时间)→ 接收数据
记住关键规则:
- 创建后一定要检查返回值
- 任务和中断使用不同的API
- 数据传输是复制,不是引用
掌握这三个步骤,就能够使用队列在FreeRTOS任务间进行可靠的数据通信了。从简单的消息传递开始,逐步应用到更复杂的场景中,队列会成为你嵌入式开发的重要工具。