FreeRTOS队列与事件组:任务间通信实战指南
👁 2 阅读 · 2026-08-15 · RTOS RTOS
在RTOS应用中,任务间通信是核心需求。FreeRTOS提供队列和事件组两大机制:队列实现数据传递,事件组实现事件同步。本文通过一个实战示例,深入讲解两者的工作原理、配置方法及协同使用技巧,帮助开发者构建高效、可靠的多任务系统。
## 为什么需要任务间通信
在嵌入式RTOS系统中,多个任务并发执行,任务间往往需要交换数据或同步状态。例如,传感器采集任务将数据发送给处理任务,或者按键任务通知LED控制任务。FreeRTOS提供了丰富的通信机制,其中**队列(Queue)**和**事件组(Event Group)**是使用最广泛的两种。
- 队列:用于传输数据块,适合点对点或一对多的数据流。
- 事件组:用于事件标志的置位与等待,适合多事件同步或条件触发。
熟读掌握这两个机制,是写出高效RTOS应用的关键。
## 队列原理与使用
队列是FreeRTOS中的FIFO缓冲区,通过`xQueueSend()`发送数据,`xQueueReceive()`接收数据。每个队列有固定长度和元素大小,数据通过拷贝方式传递,因此需要确保数据大小不超出队列元素尺寸。
核心API:
- `xQueueCreate( uxQueueLength, uxItemSize )`:创建队列。
- `xQueueSend( xQueue, pvItemToSend, xTicksToWait )`:发送数据。
- `xQueueReceive( xQueue, pvBuffer, xTicksToWait )`:接收数据(并移除)。
```c
QueueHandle_t xDataQueue;
#define QUEUE_LENGTH 10
#define ITEM_SIZE sizeof(uint32_t)
void vSenderTask(void *pvParameters) {
uint32_t value = 0;
while(1) {
// 发送数据
if(xQueueSend(xDataQueue, &value, pdMS_TO_TICKS(100)) == pdPASS) {
value++;
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void vReceiverTask(void *pvParameters) {
uint32_t received;
while(1) {
// 等待数据,超时100ms
if(xQueueReceive(xDataQueue, &received, pdMS_TO_TICKS(100)) == pdPASS) {
// 处理数据
}
}
}
```
## 事件组原理与使用
事件组用一个`EventBits_t`类型的变量表示多个事件标志,每个位代表一个事件。任务可等待一个或多个位,执行逻辑与或逻辑或操作,适合“等待多个条件同时满足”的场景。
核心API:
- `xEventGroupCreate()`:创建事件组。
- `xEventGroupSetBits( xEventGroup, bitsToSet )`:置位事件。
- `xEventGroupWaitBits( xEventGroup, bitsToWaitFor, xClearOnExit, xWaitForAllBits, xTicksToWait )`:等待事件位。
```c
EventGroupHandle_t xEventGroup;
#define EVENT_BIT_1 (1 << 0)
#define EVENT_BIT_2 (1 << 1)
void vEventTask(void *pvParameters) {
EventBits_t bits;
while(1) {
// 等待两个事件都置位,并自动清除
bits = xEventGroupWaitBits(xEventGroup, EVENT_BIT_1 | EVENT_BIT_2,
pdTRUE, pdTRUE, pdMS_TO_TICKS(1000));
if((bits & (EVENT_BIT_1 | EVENT_BIT_2)) == (EVENT_BIT_1 | EVENT_BIT_2)) {
// 两个事件都发生,执行动作
}
}
}
```
## 实战需求:传感器数据采集与报警
设计一个系统,包含两个任务:
- 采集任务:周期性读取传感器值,放入队列。
- 处理任务:从队列接收数据,当数据超过阈值时,置位“报警”事件。
- 控制任务:等待“报警”事件,触发LED闪烁。
这样我们同时使用了队列(传递数据)和事件组(同步报警)。
## 配置步骤
1. 在FreeRTOSConfig.h中启用队列和事件组支持(通常默认开启)。
2. 创建队列和事件组句柄。
3. 创建3个任务,设置合适的优先级(采集任务1,处理任务2,控制任务1)。
4. 启动调度器`vTaskStartScheduler()`。
## 完整代码示例
下面基于STM32 HAL库,使用FreeRTOS动态内存管理。
```c
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "event_groups.h"
// 句柄与宏定义
QueueHandle_t xSensorQueue;
EventGroupHandle_t xAlarmEventGroup;
#define SENSOR_QUEUE_LEN 5
#define SENSOR_ITEM_SIZE sizeof(uint16_t)
#define ALARM_EVENT (1 << 0)
#define THRESHOLD 100
// 采集任务
void vSensorTask(void *pvParameters) {
uint16_t sensor_value = 0;
for(;;) {
// 模拟读取传感器
sensor_value = (sensor_value + 17) % 150;
// 发送到队列
xQueueSend(xSensorQueue, &sensor_value, pdMS_TO_TICKS(10));
vTaskDelay(pdMS_TO_TICKS(500));
}
}
// 处理任务
void vProcessTask(void *pvParameters) {
uint16_t value;
for(;;) {
// 接收数据,超时等待100ms
if(xQueueReceive(xSensorQueue, &value, pdMS_TO_TICKS(100)) == pdPASS) {
// 判断是否超阈值
if(value > THRESHOLD) {
// 置位报警事件
xEventGroupSetBits(xAlarmEventGroup, ALARM_EVENT);
}
}
}
}
// 控制任务
void vControlTask(void *pvParameters) {
for(;;) {
// 等待报警事件
EventBits_t bits = xEventGroupWaitBits(xAlarmEventGroup, ALARM_EVENT,
pdTRUE, pdTRUE, pdMS_TO_TICKS(1000));
if((bits & ALARM_EVENT) != 0) {
// 触发LED闪烁(示例:翻转GPIO)
// HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
}
int main(void) {
// 外设初始化等(略)
// 创建队列和事件组
xSensorQueue = xQueueCreate(SENSOR_QUEUE_LEN, SENSOR_ITEM_SIZE);
xAlarmEventGroup = xEventGroupCreate();
if(xSensorQueue == NULL || xAlarmEventGroup == NULL) {
// 创建失败处理
for(;;);
}
// 创建任务
xTaskCreate(vSensorTask, "Sensor", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
xTaskCreate(vProcessTask, "Process", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
xTaskCreate(vControlTask, "Control", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
// 启动调度器
vTaskStartScheduler();
// 正常不会到这里
for(;;);
}
```
## 注意事项
- **队列数据拷贝**:队列传输是拷贝,不要传大块数据(如数组),可传递指针或结构体指针。若必须传大块,使用动态内存或内存池。
- **超时设置**:`xQueueSend`和`xQueueReceive`中的超时参数单位是tick,使用`pdMS_TO_TICKS(ms)`转换。在中断中不能使用带阻塞的版本,应使用`xQueueSendFromISR`和`xEventGroupSetBitsFromISR`。
- **事件组清除位**:`xEventGroupWaitBits`的`xClearOnExit`参数决定退出时是否自动清除已匹配的位,务必谨慎设置,避免事件丢失或重复触发。
- **优先级反转**:高优先级任务等待低优先级任务发数据时,可能造成优先级反转。可考虑使用二值信号量或互斥量,必要时使用优先级继承。
- **内存需求**:队列和事件组会消耗堆内存,注意调整`configTOTAL_HEAP_SIZE`。
- **调试技巧**:利用`uxQueueMessagesWaiting()`检查队列剩余消息数,使用`xEventGroupGetBits()`查看事件位状态,便于调试。
通过本文的实战,可以看到队列和事件组各司其职:队列承载数据流,事件组同步事件触发器。在实际RTOS项目中,合理组合它们能够高效构建出健壮的多任务系统。