FreeRTOS事件组

发布于:2025-06-13 ⋅ 阅读:(18) ⋅ 点赞:(0)

img

1.概念

事件组可以简单地认为是一个整数,每一位代表一个独立的事件。程序员可以为每一位事件赋予不同的含义,例如:

  • Bit0:表示串口是否就绪
  • Bit1:表示按键是否被按下等等。
    当某个位的值为1时,表示该事件已发生;为0则表示事件未发生。

事件组使用一个整数来存储所有事件。

该整数的高位部分(8位或更多)由内核保留,供内部使用。

事件组可用的位数取决于配置参数 configUSE_16_BIT_TICKS

  • 如果 configUSE_16_BIT_TICKS 为1,则表示处理器使用16位计时器,此时事件组是16位的,低8位用于表示事件;
  • 如果 configUSE_16_BIT_TICKS 为0,则使用32位计时器,事件组就是32位的,其中低24位用于表示事件。

一个或多个任务、甚至 ISR 都可以设置(写入)事件位,也可以读取这些事件。

不同任务可以同时等待同一个事件组中的不同位或组合位发生变化。

2.事件组的操作

  1. 唤醒行为(广播效果):
    • 队列、信号量: 当事件发生时,通常只唤醒一个等待该资源的任务。
    • 事件组: 当满足等待条件的事件发生时,所有等待该条件的任务都会被唤醒。这种机制具有“广播”效果,适合多个任务需要同时知道事件发生的情况。
  1. 事件清除(消耗型与非消耗型):
    • 队列、信号量: 数据或计数值被读取后,就被消耗掉。
    • 事件组: 当一个任务等待事件组时,可以选择在获得事件后是否清除对应的事件位。也就是说,任务获得事件时可以选择“保留”事件(允许后续任务继续看到该事件)或“清除”事件(将事件位复位为0)。

假设系统中有两个任务,一个任务(任务A)等待“串口就绪”事件(Bit0),另一个任务(任务B)等待“按键按下”事件(Bit1)。

  • 当外部中断或另一个任务检测到串口就绪时,会设置事件组中 Bit0 为1;任务A立即被唤醒,检查到事件成立后,可以选择清除 Bit0 或保留它。
  • 同样,当检测到按键按下时,会设置 Bit1 为1;任务B被唤醒并执行相应处理。

此外,如果有任务需要同时等待这两个事件中的任一一个发生,则可以用“或”关系等待;如果要求必须同时满足两个事件才能继续操作,则设置为“与”关系等待。

3.事件组相关函数

3.1 创建

动态创建:

EventGroupHandle_t xEventGroupCreate(void);
  • 此函数在内部会动态分配内存来存储事件组的数据结构。
  • 创建成功后返回一个非 NULL 的事件组句柄;否则返回 NULL。
  • 使用动态创建的事件组适合内存充足的系统,且方便使用。

静态创建:

EventGroupHandle_t xEventGroupCreateStatic(StaticEventGroup_t *pxEventGroupBuffer);
  • 静态创建要求用户先定义一个 StaticEventGroup_t 类型的变量(内存缓冲区),并将其地址传入。
  • 该函数不使用动态内存分配,而是利用用户提供的内存来存储事件组结构。
  • 成功返回非 NULL 的句柄,适用于对内存管理要求严格或不希望使用动态分配的系统。

3.2 删除

当不再需要事件组时,可以通过删除事件组来回收内存(仅适用于动态创建的事件组)。

void vEventGroupDelete(EventGroupHandle_t xEventGroup);
  • 删除指定的事件组,释放由动态分配内存创建的事件组所占用的资源。
  • 静态创建的事件组的内存由用户管理,因此调用此函数不会释放用户提供的内存。

3.3 设置事件

设置事件组的操作即是将某个位或多个位置为1,表示对应的事件已发生。事件的写操作有两种版本:任务中使用和 ISR 中使用。

在任务中设置事件:

EventBits_t xEventGroupSetBits(EventGroupHandle_t xEventGroup, 
                               const EventBits_t uxBitsToSet);
  • 参数 xEventGroup 指定要设置的事件组。
  • 参数 uxBitsToSet 指定要设置的位(例如 0x15 表示设置 bit4、bit2、bit0)。
  • 该函数将事件组中指定的位设置为1,并返回设置之前的事件组值(由于其他任务可能同时修改,返回值通常用作调试)。
  • 设置操作会唤醒所有等待这些位(或这些位组合)的任务,具有广播效果。

在ISR中设置事件:

BaseType_t xEventGroupSetBitsFromISR(EventGroupHandle_t xEventGroup,
                                      const EventBits_t uxBitsToSet,
                                      BaseType_t *pxHigherPriorityTaskWoken);
  • 用于在中断服务例程中设置事件组中的位。
  • 与任务中使用的版本类似,但由于在 ISR 中不允许直接阻塞或做较复杂操作,此函数不是直接设置事件组,而是通过向 FreeRTOS 后台任务(daemon task)发送队列数据来间接设置事件组。
  • 参数 pxHigherPriorityTaskWoken 用于指示是否有更高优先级任务因该设置操作而进入就绪状态。如果后台任务的优先级高于当前中断任务,则该变量会被设置为 pdTRUE
  • 返回值为 pdPASS 表示操作成功。

3.4 等待事件

任务可以使用事件组等待某一位或多位事件发生。等待函数允许你指定等待的条件(“与”或“或”关系)、是否在退出时清除事件位以及等待的最长时间。

EventBits_t xEventGroupWaitBits(EventGroupHandle_t xEventGroup,
                                const EventBits_t uxBitsToWaitFor,
                                const BaseType_t xClearOnExit,
                                const BaseType_t xWaitForAllBits,
                                TickType_t xTicksToWait);
  • xEventGroup:要等待的事件组句柄。

  • uxBitsToWaitFor:指定位掩码,指定任务需要等待哪些位。例如,0x15 表示任务等待 bit0、bit2 和 bit4。

  • xClearOnExit

    • pdTRUE:在退出等待前,自动清除 uxBitsToWaitFor 指定的位。
    • pdFALSE:退出前不清除。
    • 清除操作在函数内部以原子方式完成,可以避免等待和清除之间被其他任务打断。
  • xWaitForAllBits

    • pdTRUE:表示任务需要等待 uxBitsToWaitFor 指定的所有位均为1。
    • pdFALSE:表示只要其中任意一位为1即可。
  • xTicksToWait:如果所等待的事件未发生,任务将阻塞等待指定的 Tick 数。可以设置为:

    • 0:不阻塞,立即返回当前事件组值。
    • portMAX_DELAY:无限等待。
    • 其他值:阻塞指定 Tick 数(通常用 pdMS_TO_TICKS() 转换为 Tick Count)。
  • 返回值:

    • 返回的是事件组当前的位值,即在“非阻塞条件成立”时的事件组值;如果超时则返回超时时刻的事件组值。

xEventGroupWaitBits返回前一定会清除事件吗?

  • 参数xClearOnExit为真以及成功等待事件时才会清除

3.5 同步点

事件组不仅可以用来等待单个或多个事件,还可以用于实现多个任务的同步。所谓“同步点”,指的是多个任务各自完成某个操作后,需要等待其他任务也完成,才能共同进入下一阶段。

EventBits_t xEventGroupSync(EventGroupHandle_t xEventGroup,
                            const EventBits_t uxBitsToSet,
                            const EventBits_t uxBitsToWaitFor,
                            TickType_t xTicksToWait);
  • uxBitsToSet:每个任务调用该函数时,会将自己对应的事件位设置为1,表示“我已经完成了我的部分”。
  • uxBitsToWaitFor:任务需要等待的其他任务所设置的位。只有当这些位满足条件时(通常为所有任务都完成),该函数才返回,表示同步点达成。
  • 同步成功返回后,事件组中这些位会被自动清除,以便下次使用。
  • xTicksToWait 指定了等待的最长时间,超时则返回。

img

4.示例

4.1 等待事件

// 全局变量:共享数据与句柄

static int sum = 0;                      // 全局变量,用于累加计算(由Task1更新)
static int dec = 0;                      // 全局变量,用于累减计算(由Task2更新)

static volatile int flagCalcEnd = 0;       // 标志计算是否结束(本例中未使用,可作扩展)
static volatile int flagUARTused = 0;      // 标志UART是否使用(本例中未使用)

static QueueHandle_t xQueueCalcHandle;     // 队列句柄,用于在任务间传递计算结果(数据为int类型)
static EventGroupHandle_t xEventGroupCalc;   // 事件组句柄,用于同步两个任务的事件(事件组中不同位表示不同任务已完成操作)


/*-----------------------------------------------------------
 * Task1Function:
 * 任务1负责执行一个简单的累加操作,计算完成后将计算结果发送到队列中,
 * 同时设置事件组中的位0,表示任务1的操作已完成。
 */
void Task1Function(void * param)
{
    volatile int i = 0;
    while (1)
    {
        // 累加操作:从0到99999,每次累加1,更新全局变量sum
        for (i = 0; i < 100000; i++)
            sum++;
        
        // 将计算结果sum通过队列发送出去,参数0表示不阻塞,如果队列已满则丢弃数据
        xQueueSend(xQueueCalcHandle, &sum, 0);
        
        /* 设置事件组中的事件位0 (即Bit0) 为1,表示Task1已完成其累加操作 */
        xEventGroupSetBits(xEventGroupCalc, (1 << 0));
        
        // 由于任务中没有延时,因此此循环会连续不断执行;
        // 在实际应用中可能需要添加vTaskDelay()避免CPU占用过高。
    }
}


/*-----------------------------------------------------------
 * Task2Function:
 * 任务2负责执行累减操作,将结果发送到同一个队列中,
 * 并设置事件组中的位1,表示任务2的操作已完成。
 */
void Task2Function(void * param)
{
    volatile int i = 0;
    while (1)
    {
        // 累减操作:从0到-99999,每次递减1,更新全局变量dec
        for (i = 0; i < 100000; i++)
            dec--;
        
        // 将累减后的结果通过队列发送出去
        xQueueSend(xQueueCalcHandle, &dec, 0);
        
        /* 设置事件组中的事件位1 (即Bit1) 为1,表示Task2已完成其累减操作 */
        xEventGroupSetBits(xEventGroupCalc, (1 << 1));
        
        // 同样,此处可添加适当延时以平衡任务执行频率
    }
}


/*-----------------------------------------------------------
 * Task3Function:
 * 任务3负责等待事件组中的两个事件都发生后,再从队列中读取数据,
 * 并打印出读取到的数据。
 *
 * 等待条件:同时等待事件组中Bit0和Bit1均为1 (xWaitForAllBits = pdTRUE)
 * 清除模式:等待成功后自动清除这两位 (xClearOnExit = pdTRUE)
 * 阻塞时间:无限等待 (portMAX_DELAY)
 */
void Task3Function(void * param)
{
    int val1, val2;
    while (1)
    {
        /* 等待事件:
         * 任务等待事件组中Bit0和Bit1同时为1。
         * 当两个事件都发生后,会清除这两位(因为xClearOnExit设置为pdTRUE)。
         */
        xEventGroupWaitBits(xEventGroupCalc, (1 << 0) | (1 << 1), pdTRUE, pdTRUE, portMAX_DELAY);
    
        // 从队列中读取数据,首先读取Task1传送的累加结果
        xQueueReceive(xQueueCalcHandle, &val1, 0);
        // 再读取Task2传送的累减结果
        xQueueReceive(xQueueCalcHandle, &val2, 0);
        
        // 打印两任务传递的结果
        printf("val1 = %d, val2 = %d\r\n", val1, val2);
    }
}


/*-----------------------------------------------------------
 * main函数:
 * 1. 硬件初始化
 * 2. 创建事件组与队列
 * 3. 创建三个任务:Task1, Task2, Task3
 * 4. 启动调度器,进入多任务调度状态
 */
int main( void )
{
    TaskHandle_t xHandleTask1;
        
#ifdef DEBUG
    debug();
#endif

    // 硬件初始化(用户需根据具体平台实现)
    prvSetupHardware();

    printf("Hello, world!\r\n");

    /* 创建事件组:用于同步Task1和Task2的操作事件 */
    xEventGroupCalc = xEventGroupCreate();

    /* 创建队列:长度为2,每个数据项大小为int
       用于传递Task1和Task2的计算结果 */
    xQueueCalcHandle = xQueueCreate(2, sizeof(int));
    if (xQueueCalcHandle == NULL)
    {
        printf("can not create queue\r\n");
    }

    // 创建任务:Task1进行累加,Task2进行累减,Task3等待事件并打印数据
    xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
    xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
    xTaskCreate(Task3Function, "Task3", 100, NULL, 1, NULL);

    // 启动调度器,开始任务调度
    vTaskStartScheduler();

    // 如果调度器启动失败(例如内存不足),将执行到这里
    return 0;
}
  • 事件组 在本例中用于同步两个任务的执行。Task1和Task2分别完成计算后通过事件组设置自己的事件位;Task3等待两个事件同时满足后,再从队列中读取数据并打印。
  • 队列 用于传递实际的计算结果数据,确保数据不会丢失或混淆。

4.2 同步点

三个任务分别负责准备演示、设置设备和冲咖啡,只有当三个任务都完成自己的准备工作后,才能“开会”。任务之间通过 xEventGroupSync 来达到同步点。

#include "FreeRTOS.h"
#include "task.h"
#include "event_groups.h"
#include "stdio.h"

/*-----------------------------------------------------------
 * 模拟场景:会议开始前的准备工作同步
 * 有三个任务:
 *   - 演示任务(Presentation):准备演示文稿
 *   - 设备任务(Equipment):设置会议设备
 *   - 咖啡任务(Coffee):冲咖啡
 *
 * 每个任务完成自己工作后,调用 xEventGroupSync() 将自己对应的事件位设置,
 * 并等待其他任务也完成。只有所有任务都完成后,
 * xEventGroupSync() 才返回,然后各任务输出“会议开始”的信息。
 *-----------------------------------------------------------*/

/* 定义事件组中各个事件位 */
#define PRESENTATION   (1 << 0)    // Bit0:演示任务完成
#define EQUIPMENT      (1 << 1)    // Bit1:设备任务完成
#define COFFEE         (1 << 2)    // Bit2:咖啡任务完成
#define ALL_EVENTS     (PRESENTATION | EQUIPMENT | COFFEE)

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

/* 函数原型 */
static void vPresentationTask(void *pvParameters);
static void vEquipmentTask(void *pvParameters);
static void vCoffeeTask(void *pvParameters);

int main(void)
{
    /* 硬件初始化,具体实现视平台而定 */
    prvSetupHardware();
    
    printf("System starting: Meeting preparation simulation...\r\n");

    /* 创建事件组 */
    xMeetingEventGroup = xEventGroupCreate();
    if (xMeetingEventGroup != NULL)
    {
        /* 创建3个任务,各自负责一项准备工作
         * 这里传入的参数是一个字符串,用于打印任务名称
         */
        xTaskCreate(vPresentationTask, "PresentationTask", 1000, "Presenter", 1, NULL);
        xTaskCreate(vEquipmentTask,    "EquipmentTask",    1000, "Equipment",   1, NULL);
        xTaskCreate(vCoffeeTask,       "CoffeeTask",       1000, "Barista",     1, NULL);

        /* 启动调度器 */
        vTaskStartScheduler();
    }
    else
    {
        /* 如果事件组创建失败,则打印错误信息 */
        printf("Error: Unable to create event group!\r\n");
    }

    /* 如果程序运行到这里通常表示内存不足等严重错误 */
    return 0;
}

/*-----------------------------------------------------------
 * vPresentationTask:
 * 模拟准备演示文稿的任务
 * 每次准备完成后,通过 xEventGroupSync() 设置 PRESENTATION 位,
 * 并等待所有任务都完成(ALL_EVENTS)。
 * 当同步点成立后,任务打印“会议开始”信息。
 *-----------------------------------------------------------*/
static void vPresentationTask(void *pvParameters)
{
    const TickType_t xDelay = pdMS_TO_TICKS(100UL); // 100毫秒延时
    int iteration = 0;

    for (;;)
    {
        /* 执行自己的工作:准备演示文稿 */
        printf("%s: Preparing presentation, iteration %d...\r\n", (char *)pvParameters, iteration);
        vTaskDelay(xDelay);

        /* 到达同步点:
         * 将 PRESENTATION 位设置为1,并等待 ALL_EVENTS 中所有位都为1。
         * xClearOnExit设置为pdTRUE,表示当等待成功后自动清除这些位。
         * 阻塞等待时间设置为 portMAX_DELAY,表示一直等待直到所有任务都完成。
         */
        xEventGroupSync(xMeetingEventGroup, PRESENTATION, ALL_EVENTS, portMAX_DELAY);

        /* 同步点成立,所有任务都完成准备工作,会议开始 */
        printf("%s: Presentation ready. Meeting is starting, iteration %d.\r\n", (char *)pvParameters, iteration);
        iteration++;
        vTaskDelay(xDelay);
    }
}

/*-----------------------------------------------------------
 * vEquipmentTask:
 * 模拟设置会议设备的任务
 * 完成工作后设置 EQUIPMENT 位,并等待所有任务同步后打印“会议开始”信息。
 *-----------------------------------------------------------*/
static void vEquipmentTask(void *pvParameters)
{
    const TickType_t xDelay = pdMS_TO_TICKS(100UL); // 100毫秒延时
    int iteration = 0;

    for (;;)
    {
        /* 执行自己的工作:设置会议设备 */
        printf("%s: Setting up equipment, iteration %d...\r\n", (char *)pvParameters, iteration);
        vTaskDelay(xDelay);

        /* 到达同步点:
         * 将 EQUIPMENT 位设置为1,并等待 ALL_EVENTS 中所有位都为1,
         * 自动清除等待位后继续执行。
         */
        xEventGroupSync(xMeetingEventGroup, EQUIPMENT, ALL_EVENTS, portMAX_DELAY);

        /* 同步点成立,会议开始 */
        printf("%s: Equipment ready. Meeting is starting, iteration %d.\r\n", (char *)pvParameters, iteration);
        iteration++;
        vTaskDelay(xDelay);
    }
}

/*-----------------------------------------------------------
 * vCoffeeTask:
 * 模拟冲咖啡的任务
 * 完成工作后设置 COFFEE 位,并等待所有任务同步后打印“会议开始”信息。
 *-----------------------------------------------------------*/
static void vCoffeeTask(void *pvParameters)
{
    const TickType_t xDelay = pdMS_TO_TICKS(100UL); // 100毫秒延时
    int iteration = 0;

    for (;;)
    {
        /* 执行自己的工作:冲咖啡 */
        printf("%s: Brewing coffee, iteration %d...\r\n", (char *)pvParameters, iteration);
        vTaskDelay(xDelay);

        /* 到达同步点:
         * 将 COFFEE 位设置为1,并等待所有任务都完成(ALL_EVENTS)。
         */
        xEventGroupSync(xMeetingEventGroup, COFFEE, ALL_EVENTS, portMAX_DELAY);

        /* 同步点成立,会议开始 */
        printf("%s: Coffee ready. Meeting is starting, iteration %d.\r\n", (char *)pvParameters, iteration);
        iteration++;
        vTaskDelay(xDelay);
    }
}

5.内部源码

5.1 结构体

/* 事件组结构体定义 */
typedef struct EventGroupDef_t
{
    /* 当前事件位(事件标志集合)
     *  类型 EventBits_t 通常为 uint32_t,最多支持 32 个独立事件位(bit)
     *  每个位表示一个事件状态(如 0x00000001 表示位0被置位)
     *  使用 xEventGroupSetBits() 设置位,xEventGroupClearBits() 清除位
     *  任务通过 xEventGroupWaitBits() 等待特定位的组合
     * 示例:等待位0和位1同时置位(uxEventBits & 0x03 == 0x03)
     */
    EventBits_t uxEventBits;

    /* 等待事件位的任务列表
     *  使用 FreeRTOS 的链表(List_t)管理所有因等待事件位而阻塞的任务
     *  当调用 xEventGroupSetBits() 时,检查列表中的任务是否满足等待条件
     *  满足条件的任务会被唤醒并移至就绪队列
     */
    List_t xTasksWaitingForBits;

    /* 跟踪和调试标识符(仅在启用 configUSE_TRACE_FACILITY 时生效)
     *  每个事件组创建时被分配唯一的编号,便于调试工具(如 Tracealyzer)追踪
     *  通过 uxEventGroupGetNumber() 可获取此标识符
     */
    #if ( configUSE_TRACE_FACILITY == 1 )
        UBaseType_t uxEventGroupNumber;
    #endif

    /* 静态分配标记(仅在同时启用静态和动态分配时生效)
     *  若事件组通过 xEventGroupCreateStatic() 静态创建,此字段设为 pdTRUE
     *  防止误用 vEventGroupDelete() 删除静态分配的事件组(因其内存不可释放)
     */
    #if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
        uint8_t ucStaticallyAllocated;
    #endif
} EventGroup_t;

5.2 等待事件

在之前的队列、信号量或者是互斥量,实现互斥访问都是依靠关闭中断。事件组却不是这样,而是靠关闭调度器。这意味着并不会在中断中去使用事件组

来看看等待事件发生的函数xEventGroupWaitBits

EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
                                 const EventBits_t uxBitsToWaitFor,
                                 const BaseType_t xClearOnExit,
                                 const BaseType_t xWaitForAllBits,
                                 TickType_t xTicksToWait )
{
    /* 将通用事件组句柄转换为内部事件组结构指针 */
    EventGroup_t * pxEventBits = xEventGroup;
    /* 用于存储最终返回的事件位值 */
    EventBits_t uxReturn;
    /* 用于记录调用者的控制要求,如清除标志和“等待所有位”标志 */
    EventBits_t uxControlBits = 0;
    BaseType_t xWaitConditionMet, xAlreadyYielded;
    BaseType_t xTimeoutOccurred = pdFALSE;

    /* 断言检查:
     * 1. xEventGroup 必须有效。
     * 2. 请求等待的位中不能包含内核使用的控制位 (eventEVENT_BITS_CONTROL_BYTES)。
     * 3. 至少有一位被请求。
     */
    configASSERT( xEventGroup );
    configASSERT( ( uxBitsToWaitFor & eventEVENT_BITS_CONTROL_BYTES ) == 0 );
    configASSERT( uxBitsToWaitFor != 0 );

    /* 如果调度器处于挂起状态,则不允许阻塞等待 */
    #if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
    {
        configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );
    }
    #endif

    /* 暂停任务调度以便原子性操作,不被其他任务打断 */
    vTaskSuspendAll();
    {
        /* 读取当前事件组中的事件位 */
        const EventBits_t uxCurrentEventBits = pxEventBits->uxEventBits;

        /* 调用内部函数检查是否满足等待条件:
         * 如果 xWaitForAllBits 为 pdTRUE,则要求 uxCurrentEventBits 包含所有 uxBitsToWaitFor 指定的位,
         * 否则只要求其中任一位被置位。
         */
        xWaitConditionMet = prvTestWaitCondition( uxCurrentEventBits, uxBitsToWaitFor, xWaitForAllBits );

        if( xWaitConditionMet != pdFALSE )
        {
            /* 如果条件已经满足:
             * 1. 直接返回当前的事件位。
             * 2. 同时将 xTicksToWait 设置为 0,表示不需要阻塞等待。
             */
            uxReturn = uxCurrentEventBits;
            xTicksToWait = ( TickType_t ) 0;

            /* 根据参数判断是否在返回前清除等待的位 */
            if( xClearOnExit != pdFALSE )
            {
                pxEventBits->uxEventBits &= ~uxBitsToWaitFor;
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else if( xTicksToWait == ( TickType_t ) 0 )
        {
            /* 如果等待条件不满足且不允许阻塞等待,则:
             * 返回当前事件位,同时标记为超时(未满足条件) */
            uxReturn = uxCurrentEventBits;
            xTimeoutOccurred = pdTRUE;
        }
        else
        {
            /* 如果条件不满足且允许阻塞等待,则设置任务的等待行为:
             *
             * uxControlBits 用于记录以下调用者要求:
             *  如果传入参数 xClearOnExit 为 pdTRUE,则在任务解除阻塞时清除事件位。
             *  如果传入参数 xWaitForAllBits 为 pdTRUE,则要求等待所有指定的位。
             */
            if( xClearOnExit != pdFALSE )
            {
                uxControlBits |= eventCLEAR_EVENTS_ON_EXIT_BIT;
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }

            if( xWaitForAllBits != pdFALSE )
            {
                uxControlBits |= eventWAIT_FOR_ALL_BITS;
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }

            /* 将任务希望等待的事件位和控制标志(通过按位或组合)存储到任务的事件列表项中,
             * 并将任务放入 pxEventBits->xTasksWaitingForBits 等待列表中,同时指定最大等待时间。
             * 当这些事件位满足等待条件时,内核会解除任务阻塞。 */
            vTaskPlaceOnUnorderedEventList( &( pxEventBits->xTasksWaitingForBits ),
                                            ( uxBitsToWaitFor | uxControlBits ),
                                            xTicksToWait );

            /* 虽然在任务解除阻塞后 uxReturn 会被重新设置,但为了防止某些编译器发出未初始化变量警告,
             * 这里先将 uxReturn 初始化为 0 */
            uxReturn = 0;

            /* 跟踪记录任务进入等待状态 */
            traceEVENT_GROUP_WAIT_BITS_BLOCK( xEventGroup, uxBitsToWaitFor );
        }
    }
    /* 恢复任务调度,xAlreadyYielded 表示是否有任务因为恢复调度而已切换 */
    xAlreadyYielded = xTaskResumeAll();

    /* 如果任务进入阻塞等待,即 xTicksToWait 仍不为 0 */
    if( xTicksToWait != ( TickType_t ) 0 )
    {
        /* 如果恢复调度后没有发生上下文切换,则调用 yield 进行任务切换 */
        if( xAlreadyYielded == pdFALSE )
        {
            portYIELD_WITHIN_API();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

        /* 当任务解除阻塞后,等待的事件位会存储在任务的事件列表项中,
         * 调用 uxTaskResetEventItemValue() 获取解除阻塞时设置的事件位。
         * 此时可能是因为指定的事件位被置位而解除阻塞,也可能是因为等待超时解除阻塞。 */
        uxReturn = uxTaskResetEventItemValue();

        /* 检查返回值中是否包含 eventUNBLOCKED_DUE_TO_BIT_SET 标志,
         * 如果没有该标志,则说明任务是因超时而解除阻塞 */
        if( ( uxReturn & eventUNBLOCKED_DUE_TO_BIT_SET ) == ( EventBits_t ) 0 )
        {
            /* 进入临界区保护对事件组的访问 */
            taskENTER_CRITICAL();
            {
                /* 将返回值设置为当前的事件组事件位 */
                uxReturn = pxEventBits->uxEventBits;

                /* 由于任务解除阻塞后,事件组的位可能已被更新,
                 * 再次检查等待条件,如果满足,则根据 xClearOnExit 参数决定是否清除事件位 */
                if( prvTestWaitCondition( uxReturn, uxBitsToWaitFor, xWaitForAllBits ) != pdFALSE )
                {
                    if( xClearOnExit != pdFALSE )
                    {
                        pxEventBits->uxEventBits &= ~uxBitsToWaitFor;
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }

                /* 标记此次解除阻塞是由于超时而非事件位被设置 */
                xTimeoutOccurred = pdTRUE;
            }
            taskEXIT_CRITICAL();
        }
        else
        {
            /* 如果任务解除阻塞原因是事件位被置位,则无需超时处理 */
        }

        /* 清除返回值中可能设置的控制位,确保返回值只包含实际的事件位 */
        uxReturn &= ~eventEVENT_BITS_CONTROL_BYTES;
    }

    /* 跟踪记录等待结束事件,传入事件组句柄、等待的位和是否超时 */
    traceEVENT_GROUP_WAIT_BITS_END( xEventGroup, uxBitsToWaitFor, xTimeoutOccurred );

    /* 防止当跟踪宏未使用时产生编译器警告 */
    ( void ) xTimeoutOccurred;

    /* 返回最终的事件位,表示任务等待结束时事件组的状态 */
    return uxReturn;
}

无非就是内部调用prvTestWaitCondition,检查是否满足等待条件。

然后根据其返回值判断:

  • 不满足的话,根据用户要求来决定是否阻塞,如果阻塞的话就调用vTaskPlaceOnUnorderedEventList将当前任务放到任务等待链表当中,其实现如下,已经标注了详细的代码
void vTaskPlaceOnUnorderedEventList( List_t * pxEventList,
                                     const TickType_t xItemValue,
                                     const TickType_t xTicksToWait )
{
    /* 1. 检查传入的事件列表指针是否有效 */
    configASSERT( pxEventList );

    /* 2. 断言要求调度器必须处于挂起状态。
     * 这保证了本函数在调用期间不会发生任务切换,
     * 从而确保对当前任务的事件列表项和事件列表的操作具有原子性。
     * 该函数主要用于事件组实现,要求在调度器挂起时调用。 */
    configASSERT( uxSchedulerSuspended != 0 );

    /* 3. 设置当前任务的事件列表项的值。
     * 将传入的 xItemValue 与标志 taskEVENT_LIST_ITEM_VALUE_IN_USE 进行按位或运算,
     * 表示该事件列表项正被使用,并存储指定的值。
     * 注意:这里使用 pxCurrentTCB->xEventListItem,代表当前任务控制块(TCB)的事件列表项,
     * 并且由于当前任务处于阻塞状态,其他中断不会访问其事件列表项。 */
    listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xEventListItem ), xItemValue | taskEVENT_LIST_ITEM_VALUE_IN_USE );

    /* 4. 将当前任务的事件列表项插入到指定的事件列表(pxEventList)的末尾。
     * 这样,任务就被放入等待列表中,等待相关事件发生。
     * 由于该事件列表属于事件组实现,且中断不会直接访问事件组,
     * 所以这里的插入操作是安全的。 */
    listINSERT_END( pxEventList, &( pxCurrentTCB->xEventListItem ) );

    /* 5. 将当前任务放入延时列表中,等待 xTicksToWait 指定的时钟节拍数。
     * 该函数 prvAddCurrentTaskToDelayedList() 将当前任务设置为延时状态,
     * 在超时时间到达或等待的事件条件满足时将任务从延时列表中移除。
     * 参数 pdTRUE 通常表示任务阻塞后任务状态被标记为等待事件。 */
    prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
}
  • xEventGroupWaitBits函数的下半部分则是一些解除阻塞后的一些处理,分两者情况

    • 超时的话,事件组的位可能已被更新,再次检查等待条件,如果满足,则根据 xClearOnExit 参数决定是否清除事件位
    • 不超时而是被xEventGroupSetBits设置了等待位满足条件而退出阻塞的话,则不做处理

5.3 设置事件

来看看xEventGroupSetBits是怎么去设置事件,让阻塞等待事件的任务被唤醒,进行运行。

其实没看之前也能猜一下,是不是也是将等待事件的任务从任务等待链表中取出放到就绪任务等待链表呢???

img

EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
                                const EventBits_t uxBitsToSet )
{
    /* 定义用于遍历等待任务的列表项指针 */
    ListItem_t * pxListItem, * pxNext;
    /* 定义事件列表的结束标记指针,用于判断遍历是否结束 */
    ListItem_t const * pxListEnd;
    /* 定义指向事件组中等待任务列表的指针 */
    List_t const * pxList;
    /* 记录待清除的事件位,初始为 0 */
    EventBits_t uxBitsToClear = 0;
    /* 存储每个任务等待的事件位(不包含控制位) */
    EventBits_t uxBitsWaitedFor;
    /* 存储每个任务等待条件中的控制位(如等待全部位、退出时清除位) */
    EventBits_t uxControlBits;
    /* 将传入的事件组句柄转换为内部使用的事件组结构指针 */
    EventGroup_t * pxEventBits = xEventGroup;
    /* 标记是否找到满足等待条件的任务 */
    BaseType_t xMatchFound = pdFALSE;

    /* 断言检查:
     * 1. xEventGroup 必须有效。
     * 2. 要设置的位不能包含内核保留的控制位(eventEVENT_BITS_CONTROL_BYTES)。
     */
    configASSERT( xEventGroup );
    configASSERT( ( uxBitsToSet & eventEVENT_BITS_CONTROL_BYTES ) == 0 );

    /* 获取等待该事件组的任务列表 */
    pxList = &( pxEventBits->xTasksWaitingForBits );
    /* 获取该列表的结束标记,用于遍历判断 */
    pxListEnd = listGET_END_MARKER( pxList ); /* 此处使用 mini list 作为结束标记,节省内存 */

    /* 暂停任务调度,确保下面对事件组状态的修改原子性 */
    vTaskSuspendAll();
    {
        /* 跟踪记录事件组设置位操作(用于调试或性能分析) */
        traceEVENT_GROUP_SET_BITS( xEventGroup, uxBitsToSet );

        /* 从事件组等待列表的头部开始遍历 */
        pxListItem = listGET_HEAD_ENTRY( pxList );

        /* 1. 设置指定的事件位:
         * 使用按位或操作将 uxBitsToSet 加入当前事件组的事件位中 */
        pxEventBits->uxEventBits |= uxBitsToSet;

        /* 2. 遍历等待列表,检查是否有任务等待的事件条件已满足 */
        while( pxListItem != pxListEnd )
        {
            /* 保存下一个列表项,因为当前列表项可能被移除 */
            pxNext = listGET_NEXT( pxListItem );
            /* 取得该等待任务所要求的事件位和控制信息 */
            uxBitsWaitedFor = listGET_LIST_ITEM_VALUE( pxListItem );
            /* 初始化匹配标志为未匹配 */
            xMatchFound = pdFALSE;

            /* 3. 分离等待的事件位和控制位:
             *     控制位在 eventEVENT_BITS_CONTROL_BYTES 中,包含等待全部位和退出时清除位等标志。
             *     剩下的位就是该任务真正等待的事件位。 */
            uxControlBits = uxBitsWaitedFor & eventEVENT_BITS_CONTROL_BYTES;
            uxBitsWaitedFor &= ~eventEVENT_BITS_CONTROL_BYTES;

            /* 4. 判断当前事件组的事件位是否满足任务等待条件:
             *     如果任务不要求等待所有位(xWaitForAllBits == pdFALSE),只需有任一位匹配即可。
             *     如果任务要求等待所有位(xWaitForAllBits == pdTRUE),则必须所有位都匹配。 */
            if( ( uxControlBits & eventWAIT_FOR_ALL_BITS ) == ( EventBits_t ) 0 )
            {
                /* 如果任一位匹配,则满足条件 */
                if( ( uxBitsWaitedFor & pxEventBits->uxEventBits ) != ( EventBits_t ) 0 )
                {
                    xMatchFound = pdTRUE;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else if( ( uxBitsWaitedFor & pxEventBits->uxEventBits ) == uxBitsWaitedFor )
            {
                /* 如果要求所有位匹配,且当前事件位包含所有这些位,则满足条件 */
                xMatchFound = pdTRUE;
            }
            else
            {
                /* 等待条件不满足:对于要求所有位匹配的情况,并非所有等待位都已置位 */
            }

            /* 5. 如果任务等待的条件满足,则进行解除阻塞操作 */
            if( xMatchFound != pdFALSE )
            {
                /* 判断是否在任务解除阻塞时需要清除等待的位:
                 * 如果控制位中设置了 eventCLEAR_EVENTS_ON_EXIT_BIT,则需要在退出时清除这些位 */
                if( ( uxControlBits & eventCLEAR_EVENTS_ON_EXIT_BIT ) != ( EventBits_t ) 0 )
                {
                    /* 累加这些等待的位到待清除变量中 */
                    uxBitsToClear |= uxBitsWaitedFor;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }

                /* 将当前任务的等待条件(以及当前事件组的事件位状态)写入任务的事件列表项中,
                 * 并通过设置 eventUNBLOCKED_DUE_TO_BIT_SET 标志,表明该任务解除阻塞的原因是等待位匹配成功,
                 * 而非超时。随后,调用 vTaskRemoveFromUnorderedEventList 将该任务从等待列表中移除,
                 * 并将任务放入就绪或待处理列表中。 */
                vTaskRemoveFromUnorderedEventList( pxListItem, pxEventBits->uxEventBits | eventUNBLOCKED_DUE_TO_BIT_SET );
            }

            /* 6. 继续遍历下一个等待任务。
             * 注意:直接使用保存的 pxNext 而不是当前 pxListItem->pxNext,
             * 因为当前列表项可能已经从等待列表中移除。 */
            pxListItem = pxNext;
        }

        /* 7. 在解除阻塞任务后,如果有任务要求在退出时清除它们等待的位,
         * 则将这些位从事件组中清除。 */
        pxEventBits->uxEventBits &= ~uxBitsToClear;
    }
    /* 恢复任务调度 */
    ( void ) xTaskResumeAll();

    /* 返回当前事件组中的事件位状态 */
    return pxEventBits->uxEventBits;
}

可以看出其实也是差不多的。

5.4 为什么不是关闭中断

因为中断是不会使用到事件组的,假设使用事件组,处理事件的时间是无法确定的,因为是可以设置多个事件的,这就会导致中断的执行程序的时间无法确定,违背了freeRTOS中关于中断程序的要求:越早结束越好。

但是实际上确实是有这么一个函数:xEventGroupSetBitsFromISR,可以在中断程序中去设置事件,但是实际上它其实并不是在中断程序中去设置事件的,而是交由定时器任务去设置的,可以来看一下这个函数的内部:

BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
                                          const EventBits_t uxBitsToSet,
                                          BaseType_t * pxHigherPriorityTaskWoken )
    {
        BaseType_t xReturn;

        traceEVENT_GROUP_SET_BITS_FROM_ISR( xEventGroup, uxBitsToSet );
        xReturn = xTimerPendFunctionCallFromISR( vEventGroupSetBitsCallback, ( void * ) xEventGroup, ( uint32_t ) uxBitsToSet, pxHigherPriorityTaskWoken ); /*lint !e9087 Can't avoid cast to void* as a generic callback function not specific to this use case. Callback casts back to original type so safe. */

        return xReturn;
    }

继续进入xTimerPendFunctionCallFromISR函数查看,这里需要注意一下传入的参数:函数vEventGroupSetBitsCallback

BaseType_t xTimerPendFunctionCallFromISR( PendedFunction_t xFunctionToPend,
                                                  void * pvParameter1,
                                                  uint32_t ulParameter2,
                                                  BaseType_t * pxHigherPriorityTaskWoken )
        {
            DaemonTaskMessage_t xMessage;
            BaseType_t xReturn;

            /* Complete the message with the function parameters and post it to the
             * daemon task. */
            xMessage.xMessageID = tmrCOMMAND_EXECUTE_CALLBACK_FROM_ISR;
            xMessage.u.xCallbackParameters.pxCallbackFunction = xFunctionToPend;
            xMessage.u.xCallbackParameters.pvParameter1 = pvParameter1;
            xMessage.u.xCallbackParameters.ulParameter2 = ulParameter2;

            xReturn = xQueueSendFromISR( xTimerQueue, &xMessage, pxHigherPriorityTaskWoken );

            tracePEND_FUNC_CALL_FROM_ISR( xFunctionToPend, pvParameter1, ulParameter2, xReturn );

            return xReturn;
        }

实际上就是将带有vEventGroupSetBitsCallback()xMessage结构体存放进定时器队列xTimerQueue

那么既然存放到定时器任务的队列中,那定时器也肯定是会去取出,然后执行该vEventGroupSetBitsCallback事件设置函数。

所以说实际上在中断程序去执行这个设置事件函数xEventGroupSetBitsFromISR,实际上却还是在任务中去操作的

疑问

疑问1

使用事件组,可以适用于哪些场景?

  • 某个事件
  • 若干个事件中的某个事件
  • 若干个事件中的所有事件

等待的事件中,它们要么是或的关系,要么是与的关系。也就是可以等待若干个事件中的任一个,可以等待若干个事件中的所有。不能在若于个事件中指定某些事件。

疑问2

事件组能进行数据的传输或是保存吗?

  • 不行,要想实现不同任务之间的数据的传输或是保存,可以借助队列等

疑问3

为什么变量的定义,必须放在函数开头那里?不能放在代码之后?

  • Keil默认支持的C语言标准是C89,必须这样做
  • 如果是C99的话,变量的定义可以放在任何地方

可以在Keil中指定使用C99标准:

img