在嵌入式开发中,任务间通信是核心难点之一。直接共享变量虽然实现简单,但极易引发竞态问题;而队列作为 RTOS 中成熟的通信机制,不仅能解决竞态,还能实现阻塞等待、任务唤醒等高级功能。本文将结合 FreeRTOS 队列的核心特性,通过完整代码示例,讲解如何构建稳定、可维护的任务通信系统。
一、任务直接共享变量:快,但隐患重重
当两个任务直接读写同一个全局变量时,虽然代码简洁,但会引发竞态条件:两个任务同时拥有写权限,最终结果取决于执行时序,调试时难以复现。
// 竞态示例:两个任务同时修改共享变量
uint32_t shared_state = 0;
void TaskA(void *pvParameters) {
while(1) {
shared_state++; // 读-修改-写操作,非原子
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void TaskB(void *pvParameters) {
while(1) {
shared_state--; // 同样存在竞态风险
vTaskDelay(pdMS_TO_TICKS(100));
}
}
问题本质:shared_state++和shared_state--并非原子操作,可能被其他任务打断,导致数据不一致。
二、队列:不是普通数组,而是有容量的消息通道
队列(Queue)本质是有容量的 FIFO 消息通道,发送端(生产者)将数据放入队列尾部,接收端(消费者)从队列头部按顺序取走数据,天然解决了竞态问题。
队列核心特性
- 容量限制:队列可容纳的最大消息数量,避免内存溢出
- FIFO 顺序:保证消息按发送顺序接收
- 阻塞机制:队列空时接收任务阻塞,队列满时发送任务阻塞
- 原子操作:入队 / 出队操作由 RTOS 内核保证原子性
三、RTOS 队列的进阶能力:缓冲 + 等待 + 唤醒
FreeRTOS 队列不仅是数据缓冲,还将等待机制和任务唤醒绑定在一起,实现高效的任务同步。
工作流程
- 队列空时:接收任务调用
xQueueReceive()会进入阻塞状态,让出 CPU 资源 - 消息到达时:发送任务调用
xQueueSend()会自动唤醒阻塞的接收任务 - 任务切换:接收任务从阻塞态转为就绪态,开始处理消息
// 队列基础使用示例
QueueHandle_t xQueue;
void ProducerTask(void *pvParameters) {
uint32_t send_data = 0;
while(1) {
// 发送数据到队列尾部,队列满时阻塞100ms
BaseType_t xStatus = xQueueSend(xQueue, &send_data, pdMS_TO_TICKS(100));
if(xStatus == pdPASS) {
printf("发送成功: %lu\n", send_data);
send_data++;
} else {
printf("队列满,发送失败\n");
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void ConsumerTask(void *pvParameters) {
uint32_t recv_data = 0;
while(1) {
// 从队列头部接收数据,队列空时永久阻塞
BaseType_t xStatus = xQueueReceive(xQueue, &recv_data, portMAX_DELAY);
if(xStatus == pdPASS) {
printf("接收成功: %lu\n", recv_data);
}
}
}
int main(void) {
// 创建队列:容量为5,每个元素大小为uint32_t
xQueue = xQueueCreate(5, sizeof(uint32_t));
if(xQueue == NULL) {
printf("队列创建失败\n");
while(1);
}
// 创建任务
xTaskCreate(ProducerTask, "生产者", 128, NULL, 1, NULL);
xTaskCreate(ConsumerTask, "消费者", 128, NULL, 1, NULL);
vTaskStartScheduler(); // 启动调度器
return 0;
}
四、队列满时的处理策略:提前定义,避免混乱
当队列满时,必须提前定义处理策略,否则会导致任务阻塞或数据丢失。常见策略有三种:

// 队列满时的三种策略示例
void ProducerTask(void *pvParameters) {
uint32_t send_data = 0;
while(1) {
// 策略1:等待100ms,队列满则阻塞
BaseType_t xStatus1 = xQueueSend(xQueue, &send_data, pdMS_TO_TICKS(100));
// 策略2:不等待,队列满则立即返回错误
BaseType_t xStatus2 = xQueueSend(xQueue, &send_data, 0);
// 策略3:等待永久,直到队列有空位
BaseType_t xStatus3 = xQueueSend(xQueue, &send_data, portMAX_DELAY);
send_data++;
vTaskDelay(pdMS_TO_TICKS(100));
}
}
五、回调函数的正确用法:只投送,不解析
在中断回调函数(如串口接收回调)中,应只负责将数据投送到队列,解析逻辑留给任务处理,避免中断上下文过长。
// 串口接收回调:只负责投送数据
QueueHandle_t uartQueue;
void UART_RxCallback(uint8_t data) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 从中断中发送数据到队列
xQueueSendFromISR(uartQueue, &data, &xHigherPriorityTaskWoken);
// 任务切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// 解析任务:负责处理数据
void ParserTask(void *pvParameters) {
uint8_t recv_data;
while(1) {
xQueueReceive(uartQueue, &recv_data, portMAX_DELAY);
// 解析逻辑(耗时操作)
printf("解析数据: 0x%02X\n", recv_data);
}
}
int main(void) {
uartQueue = xQueueCreate(10, sizeof(uint8_t));
xTaskCreate(ParserTask, "解析任务", 256, NULL, 2, NULL);
// 初始化串口并注册回调
UART_Init(UART_RxCallback);
vTaskStartScheduler();
return 0;
}
六、队列数据传递:值拷贝 vs 指针所有权
队列默认采用值拷贝传递数据,保证数据安全;对于大块数据,可传递指针,但必须明确所有权。
1. 小数据:值拷贝
// 小数据:值拷贝
typedef struct {
uint32_t id;
int16_t value;
} SensorMsg;
QueueHandle_t sensorQueue;
void SensorTask(void *pvParameters) {
SensorMsg msg = {0};
while(1) {
msg.id = 1;
msg.value = read_sensor();
// 队列会拷贝整个结构体
xQueueSend(sensorQueue, &msg, pdMS_TO_TICKS(100));
vTaskDelay(pdMS_TO_TICKS(100));
}
}
2. 大数据:指针传递 + 所有权转移
// 大数据:指针传递+所有权转移
typedef struct {
uint8_t data[1024];
uint16_t len;
} LargeData;
QueueHandle_t largeQueue;
void ProducerTask(void *pvParameters) {
while(1) {
// 分配内存
LargeData *pData = (LargeData *)pvPortMalloc(sizeof(LargeData));
if(pData != NULL) {
pData->len = 1024;
// 填充数据
memset(pData->data, 0xAA, 1024);
// 传递指针,所有权转移给接收任务
xQueueSend(largeQueue, &pData, pdMS_TO_TICKS(100));
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void ConsumerTask(void *pvParameters) {
LargeData *pData = NULL;
while(1) {
xQueueReceive(largeQueue, &pData, portMAX_DELAY);
if(pData != NULL) {
// 处理数据
printf("接收大数据,长度: %u\n", pData->len);
// 释放内存(接收任务负责释放)
vPortFree(pData);
}
}
}
七、队列真正解决的问题:边界 + 同步 + 解耦
队列的核心价值不仅是 “搬数据”,而是解决三个关键问题:
- 边界明确:数据有容量、有顺序、有错误策略,避免无限制的内存占用
- 内核同步:无需手动实现互斥锁,队列自带原子操作和阻塞机制
- 任务解耦:生产者和消费者无需知道对方的存在,只依赖队列接口
八、总结:嵌入式 C 的稳定之道
队列不是为了隐藏并发,而是为了把并发边界说清楚。在嵌入式开发中,合理使用队列可以:
- 避免竞态条件,提高系统稳定性
- 简化任务同步逻辑,减少代码复杂度
- 实现高效的中断 - 任务通信
- 提高代码可维护性和可扩展性
通过本文的代码示例,你可以快速在 FreeRTOS 项目中应用队列,构建稳定、清晰、可维护的嵌入式系统。
456

被折叠的 条评论
为什么被折叠?



