为什么需要任务间通信
在嵌入式RTOS系统中,多个任务并发执行,任务间往往需要交换数据或同步状态。例如,传感器采集任务将数据发送给处理任务,或者按键任务通知LED控制任务。FreeRTOS提供了丰富的通信机制,其中**队列(Queue)和事件组(Event Group)**是使用最广泛的两种。
- 队列:用于传输数据块,适合点对点或一对多的数据流。
- 事件组:用于事件标志的置位与等待,适合多事件同步或条件触发。
熟读掌握这两个机制,是写出高效RTOS应用的关键。
队列原理与使用
队列是FreeRTOS中的FIFO缓冲区,通过xQueueSend()发送数据,xQueueReceive()接收数据。每个队列有固定长度和元素大小,数据通过拷贝方式传递,因此需要确保数据大小不超出队列元素尺寸。
核心API:
-
xQueueCreate( uxQueueLength, uxItemSize ):创建队列。 -
xQueueSend( xQueue, pvItemToSend, xTicksToWait ):发送数据。 -
xQueueReceive( xQueue, pvBuffer, xTicksToWait ):接收数据(并移除)。
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 ):等待事件位。
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闪烁。
这样我们同时使用了队列(传递数据)和事件组(同步报警)。
配置步骤
- 在FreeRTOSConfig.h中启用队列和事件组支持(通常默认开启)。
- 创建队列和事件组句柄。
- 创建3个任务,设置合适的优先级(采集任务1,处理任务2,控制任务1)。
- 启动调度器
vTaskStartScheduler()。
完整代码示例
下面基于STM32 HAL库,使用FreeRTOS动态内存管理。
#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项目中,合理组合它们能够高效构建出健壮的多任务系统。