FreeRTOS事件组:多任务同步的利器

在嵌入式实时操作系统中,任务间的同步与通信是核心问题。FreeRTOS提供了多种机制,如信号量、队列、事件组等。其中,事件组(Event Groups) 是一种非常灵活且高效的同步方式,特别适用于一个任务需要等待多个事件发生的场景。本文将用通俗易懂的语言,带你快速掌握FreeRTOS事件组的使用方法。


1. 什么是事件组?

想象一下,你有一个任务需要等待"温度传感器数据就绪" "串口发送完成"两个条件同时满足才能继续执行。如果用信号量,你可能需要多个信号量并小心处理,而事件组可以直接解决这类"多条件等待"的问题。

事件组本质上是一个32位的二进制数,每一位(bit)代表一个独立的事件。任务可以:

  • 设置某一位或某几位(表示事件发生)
  • 等待某一位或某几位被设置(可以等待任意一位为1,或所有指定位都为1)
  • 清除某一位(可选)

事件组允许任务阻塞等待,并支持超时机制,是任务间同步的强大工具。


2. 常用API函数

FreeRTOS提供了以下常用事件组API:

函数 功能
xEventGroupCreate() 创建一个事件组
xEventGroupSetBits() 设置事件组中的指定位(任务级)
xEventGroupSetBitsFromISR() 在中断中设置事件位
xEventGroupWaitBits() 等待事件组中的指定位被设置
xEventGroupClearBits() 清除事件组中的指定位
vEventGroupDelete() 删除事件组

本文重点介绍最核心的三个:创建、设置、等待。


3. 示例场景

硬件平台 :STM32F103C8T6,使用标准外设库(StdPeriph_Lib)

开发环境 :Keil MDK + FreeRTOS V10.x

功能描述

  • 任务 Task1:每1秒设置事件组的BIT0,模拟"传感器数据就绪"事件。
  • 任务 Task2:阻塞等待BIT0被设置,一旦等到,翻转LED(PC13),并打印信息。
  • 同时任务 Task1 还设置BIT1,但任务 Task2 等待的是BIT0BIT1同时为1(演示多事件等待)。

为了演示完整,我们将使用xEventGroupWaitBitsxWaitForAllBits参数为pdTRUE,表示必须所有指定位都为1才返回。


4. 完整代码示例

4.1 FreeRTOS配置(FreeRTOSConfig.h关键项)

c 复制代码
#define configUSE_PREEMPTION            1
#define configUSE_IDLE_HOOK             0
#define configUSE_TICK_HOOK             0
#define configCPU_CLOCK_HZ              ( SystemCoreClock )
#define configTICK_RATE_HZ              ( ( TickType_t ) 1000 )
#define configMAX_PRIORITIES            ( 5 )
#define configMINIMAL_STACK_SIZE        ( ( unsigned short ) 128 )
#define configTOTAL_HEAP_SIZE           ( ( size_t ) ( 17 * 1024 ) )
#define configMAX_TASK_NAME_LEN         ( 16 )
#define configUSE_TRACE_FACILITY        0
#define configUSE_16_BIT_TICKS          0
#define configIDLE_SHOULD_YIELD          1
#define configUSE_MUTEXES                1
#define configQUEUE_REGISTRY_SIZE        8
#define configCHECK_FOR_STACK_OVERFLOW   2
#define configUSE_RECURSIVE_MUTEXES      0
#define configUSE_MALLOC_FAILED_HOOK     1
#define configUSE_APPLICATION_TASK_TAG   0
#define configUSE_COUNTING_SEMAPHORES    0
#define configGENERATE_RUN_TIME_STATS    0

4.2 主程序(main.c)

c 复制代码
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "event_groups.h"

/* 事件组句柄 */
EventGroupHandle_t xEventGroup;

/* 事件位定义 */
#define EVENT_DATA_READY   ( 1 << 0 )   // BIT0
#define EVENT_UART_SENT    ( 1 << 1 )   // BIT1
#define EVENT_ALL_BITS     ( EVENT_DATA_READY | EVENT_UART_SENT )

/* 任务句柄 */
TaskHandle_t xTask1Handle = NULL;
TaskHandle_t xTask2Handle = NULL;

/* 函数声明 */
static void GPIO_Config(void);
static void Task1_Producer(void *pvParameters);
static void Task2_Consumer(void *pvParameters);

/* 主函数 */
int main(void)
{
    /* 配置系统时钟(标准库默认) */
    SystemInit();

    /* 初始化GPIO(LED) */
    GPIO_Config();

    /* 创建事件组 */
    xEventGroup = xEventGroupCreate();
    if (xEventGroup == NULL) {
        /* 创建失败,死循环 */
        while (1);
    }

    /* 创建任务 */
    xTaskCreate(Task1_Producer, "Producer", configMINIMAL_STACK_SIZE, NULL, 1, &xTask1Handle);
    xTaskCreate(Task2_Consumer, "Consumer", configMINIMAL_STACK_SIZE, NULL, 2, &xTask2Handle);

    /* 启动调度器 */
    vTaskStartScheduler();

    /* 如果程序运行到这里,说明堆内存不足导致调度器启动失败 */
    while (1);
}

/* GPIO初始化:LED连接在PC13 */
static void GPIO_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitStructure);

    /* 默认关闭LED */
    GPIO_SetBits(GPIOC, GPIO_Pin_13);
}

/* 任务1:生产者,周期性设置事件位 */
static void Task1_Producer(void *pvParameters)
{
    TickType_t xLastWakeTime = xTaskGetTickCount();
    const TickType_t xPeriod = pdMS_TO_TICKS(1000);

    for (;;) {
        /* 设置BIT0(数据就绪) */
        xEventGroupSetBits(xEventGroup, EVENT_DATA_READY);

        /* 模拟一些延迟后设置BIT1(发送完成) */
        vTaskDelay(pdMS_TO_TICKS(500));
        xEventGroupSetBits(xEventGroup, EVENT_UART_SENT);

        /* 等待下一个周期 */
        vTaskDelayUntil(&xLastWakeTime, xPeriod);
    }
}

/* 任务2:消费者,等待所有事件位都被设置 */
static void Task2_Consumer(void *pvParameters)
{
    EventBits_t uxBits;
    const TickType_t xTimeout = pdMS_TO_TICKS(2000); // 超时2秒

    for (;;) {
        /* 等待BIT0和BIT1都被设置(AND条件) */
        uxBits = xEventGroupWaitBits(
                     xEventGroup,          // 事件组句柄
                     EVENT_ALL_BITS,       // 等待的位
                     pdTRUE,               // 清除这些位(退出时自动清除)
                     pdTRUE,               // 等待所有位都为1(AND)
                     xTimeout              // 超时时间
                 );

        /* 检查返回结果 */
        if ((uxBits & EVENT_ALL_BITS) == EVENT_ALL_BITS) {
            /* 所有事件都发生,执行任务(翻转LED) */
            GPIO_WriteBit(GPIOC, GPIO_Pin_13, 
                (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13)));
        } else {
            /* 超时未等到所有事件,可以记录错误或执行其他操作 */
            // 此处简单忽略
        }
    }
}

4.3 中断服务函数示例(可选)

如果需要在中断中设置事件位,使用xEventGroupSetBitsFromISR

c 复制代码
void EXTI0_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
        /* 在中断中设置事件位 */
        xEventGroupSetBitsFromISR(xEventGroup, EVENT_DATA_READY, &xHigherPriorityTaskWoken);

        /* 清除中断标志 */
        EXTI_ClearITPendingBit(EXTI_Line0);
    }

    /* 如果设置了更高优先级任务就绪,则触发上下文切换 */
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

5. 代码解析

  1. 事件组创建xEventGroupCreate()返回句柄,失败返回NULL。
  2. 设置事件位 :任务1周期性设置BIT0BIT1xEventGroupSetBits不阻塞。
  3. 等待事件位 :任务2调用xEventGroupWaitBits
    • 第2参数指定要等待的位(EVENT_ALL_BITS = 0x03
    • 第3参数为pdTRUE表示等待成功后自动清除这些位,否则需手动清除
    • 第4参数为pdTRUE表示等待所有指定位都为1(AND),pdFALSE表示等待任意一位为1(OR)
    • 第5参数为超时时间,这里设置为2秒。如果超时未等到,返回的实际位可能不满足条件。
  4. 返回值:返回事件组当前的值,通过位与判断是否成功。

6. 注意事项

  • 中断中使用 :必须使用xEventGroupSetBitsFromISR,并配合portYIELD_FROM_ISR
  • 内存占用 :事件组基于RAM,创建后一直占用,删除时需调用vEventGroupDelete
  • 等待位时不能指定优先级继承,因此不适合用于互斥资源保护(应使用互斥信号量)。
  • 位数量:事件组是32位,但最高8位(bit24~bit31)被FreeRTOS内部使用,用户只能使用低24位。
  • 超时处理:务必检查返回值,避免误判事件已发生。

7. 总结

事件组是FreeRTOS中实现多事件同步的利器,通过简单的位操作即可完成复杂的等待逻辑。本文通过一个简单的生产者-消费者示例,展示了事件组的创建、设置和等待过程。在实际项目中,你可以根据需求灵活使用AND/OR等待、自动清除、超时等功能,让任务间同步更加优雅高效。

希望这篇博客能帮助你快速上手FreeRTOS事件组。如果觉得有用,欢迎点赞收藏!


参考资料

FreeRTOS官方文档 - Event Groups

STM32F10x标准外设库

相关推荐
笨笨饿2 小时前
#110_SCH3000 平台图传系统配置指南:从 DVP 摄像头到 UVC/WiFi 双通道传输
c语言·stm32·单片机·嵌入式硬件·物联网·vim
小僧景贤4 小时前
STM32 startup.s 启动文件与 .map 映射文件深度技术解析(md/hd 版本差异化对比)
stm32·内存映射·startup.s·map 文件·单片机启动流程
zcmodeltech6 小时前
水利水电沙盘模型控制系统设计与实现:多场景多工况联动方案
网络·stm32·嵌入式硬件·制造·多分类
LCG元8 小时前
STM32 软件模拟 I2C 驱动 AT24C02 EEPROM:从时序原理到实测读写全记录
stm32·单片机·嵌入式硬件
振南的单片机世界8 小时前
终端电阻硬核拆解:不接120Ω,RS485信号“撞墙反弹”乱码
arm开发·stm32·单片机·嵌入式硬件
aspirant-complete9 小时前
MAC(Apple Silicon)嵌入式基于Clion(stm32CubeMX)环境搭建
stm32·macos
hsjiasb9 小时前
FreeRTOS学习(三十八)——性能优化
学习·学习笔记·freertos
嵌入式阿蔡20 小时前
传感器_03:电机驱动——让 GPIO 当指挥官,别让 GPIO 当电源
网络·stm32·单片机·嵌入式硬件·嵌入式实时数据库
hsjiasb1 天前
FreeRTOS学习(三十七)——常见错误与工程规范
学习·学习笔记·freertos