为什么需要任务间通信

在嵌入式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闪烁。

这样我们同时使用了队列(传递数据)和事件组(同步报警)。

配置步骤

  1. 在FreeRTOSConfig.h中启用队列和事件组支持(通常默认开启)。
  2. 创建队列和事件组句柄。
  3. 创建3个任务,设置合适的优先级(采集任务1,处理任务2,控制任务1)。
  4. 启动调度器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(;;);
}

注意事项

  • 队列数据拷贝:队列传输是拷贝,不要传大块数据(如数组),可传递指针或结构体指针。若必须传大块,使用动态内存或内存池。
  • 超时设置xQueueSendxQueueReceive中的超时参数单位是tick,使用pdMS_TO_TICKS(ms)转换。在中断中不能使用带阻塞的版本,应使用xQueueSendFromISRxEventGroupSetBitsFromISR
  • 事件组清除位xEventGroupWaitBitsxClearOnExit参数决定退出时是否自动清除已匹配的位,务必谨慎设置,避免事件丢失或重复触发。
  • 优先级反转:高优先级任务等待低优先级任务发数据时,可能造成优先级反转。可考虑使用二值信号量或互斥量,必要时使用优先级继承。
  • 内存需求:队列和事件组会消耗堆内存,注意调整configTOTAL_HEAP_SIZE
  • 调试技巧:利用uxQueueMessagesWaiting()检查队列剩余消息数,使用xEventGroupGetBits()查看事件位状态,便于调试。

通过本文的实战,可以看到队列和事件组各司其职:队列承载数据流,事件组同步事件触发器。在实际RTOS项目中,合理组合它们能够高效构建出健壮的多任务系统。