一、事件组与任务通知概述

事件组和任务通知是FreeRTOS提供的两种任务间通信机制,它们在嵌入式系统中承担着任务同步与数据传递的重要角色。事件组采用广播模式,当特定事件发生时,所有满足等待条件的任务都可能被唤醒;任务通知则是高效的点对点通信方式,专门用于向单个任务发送数据。这两种机制的设计体现了FreeRTOS在功能丰富性与性能优化之间的平衡考量。

二、事件组核心实现

2.1 数据结构定义

事件组的核心数据结构仅包含两个必要的成员:

typedef struct xEventGroupDefinition
{
  EventBits_t uxEventBits;              /* 事件标志存储,每个Bit代表一个事件 */
  List_t xTasksWaitingForBits;         /* 等待事件的任务链表 */
} EventGroup_t;

结构体成员说明:

  • uxEventBits:32位无符号整数,每一位都可以独立表示一个事件。例如,第0位可表示网络连接事件,第1位表示传感器数据就绪事件,第2位表示用户按键事件。最大支持32种不同的事件类型。
  • xTasksWaitingForBits:FreeRTOS内置的链表结构,用于管理所有等待该事件组的任务。当任务调用等待函数且条件不满足时,任务会被挂载到这个链表上。事件发生时,系统遍历此链表检查哪些任务的等待条件已满足。

2.2 事件组创建

EventGroupHandle_t xEventGroupCreate( void )
{
  EventGroup_t *pxEventBits;
  
  /* 使用FreeRTOS的内存分配函数为事件组结构体分配内存空间 */
  /* pvPortMalloc是平台无关的内存分配接口 */
  pxEventBits = ( EventGroup_t * ) pvPortMalloc( sizeof( EventGroup_t ) );
  
  /* 初始化事件标志为0,表示初始状态下没有任何事件发生 */
  pxEventBits->uxEventBits = 0;
  
  /* 初始化等待任务链表,创建一个空链表 */
  vListInitialise( &( pxEventBits->xTasksWaitingForBits ) );
  
 句柄,这是指向 /* 返回事件组EventGroup_t的指针 */
  /* 开发者通过此句柄来操作对应的事件组 */
  return ( EventGroupHandle_t ) pxEventBits;
}

创建流程解析:

1.内存分配:调用pvPortMalloc为EventGroup_t结构体分配动态内存。注意在嵌入式系统中,内存分配可能失败,实际应用中应检查返回值。

2.初始化事件标志:将uxEventBits设置为0,表示所有事件位初始状态均为"未发生"。

3.初始化等待链表:调用vListInitialise将等待链表初始化为空链表,此时没有任何任务在等待该事件组。

4.返回句柄:返回事件组句柄供后续操作使用。

    2.3 事件组删除

    void vEventGroupDelete( EventGroupHandle_t xEventGroup )
    {
      /* 将句柄转换为事件组指针 */
      EventGroup_t *pxEventBits = ( EventGroup_t * ) xEventGroup;
      
      /* 获取等待任务链表的指针 */
      const List_t *pxTasksWaitingForBits = &( pxEventBits->xTasksWaitingForBits );
      
      /* 暂停调度器,防止删除过程中发生任务切换 */
      /* 注意:这里只暂停调度,不关闭中断,所以中断仍可执行 */
      vTaskSuspendAll();
      
      /* 检查等待链表中是否有等待任务 */
      /* 如果有,遍历链表将所有等待任务移除 */
      while( listCURRENT_LIST_LENGTH( pxTasksWaitingForBits ) > ( UBaseType_t ) 0 )
      {  
        /* 从等待链表中移除任务 */
        /* 第二个参数表示任务是因为事件被设置而意外解锁的 */
        vTaskRemoveFromUnorderedEventList( pxTasksWaitingForBits->xListEnd.pxNext, eventUNBLOCKED_DUE_TO_BIT_SET );
      }
      
      /* 释放事件组占用的内存空间 */
      vPortFree( pxEventBits );
      
      /* 恢复调度器,继续任务调度 */
      ( void ) xTaskResumeAll();
    }
    

    删除流程解析:

    1.暂停调度:调用vTaskSuspendAll()暂停调度器,这是进入临界区的一种方式,可以防止在删除过程中发生任务切换导致的竞态条件。

    2.清理等待任务:遍历等待链表,使用vTaskRemoveFromUnorderedEventList将每个等待任务从链表中移除。这是必要的清理步骤,确保不会有悬空指针。

    3.释放内存:调用vPortFree释放事件组占用的动态内存。

    4.恢复调度:调用xTaskResumeAll()恢复调度器的正常工作。

      2.4 设置事件位

      设置事件是事件组最常用的操作,用于向事件组发送事件通知:

      EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet )
      {
        List_t *pxList;
        BaseType_t xMatchFound = pdFALSE;
        
        /* 获取事件组指针 */
        EventGroup_t *pxEventBits = ( EventGroup_t * ) xEventGroup;
        
        /* 获取等待任务链表指针 */
        pxList = &( pxEventBits->xTasksWaitingForBits );
        
        /* 获取链表尾节点,用于遍历结束判断 */
        pxListEnd = listGET_END_MARKER( pxList );
        
        /* 暂停调度器,确保操作的原子性 */
        vTaskSuspendAll();
        
        /* 获取链表头节点,开始遍历 */
        pxListItem = listGET_HEAD_ENTRY( pxList );
        
        /* 设置事件位:使用位或操作,将uxBitsToSet中为1的位设置到uxEventBits中 */
        /* 例如:uxEventBits=0010,uxBitsToSet=0100,则结果为0110 */
        pxEventBits->uxEventBits |= uxBitsToSet;
        
        /* 遍历等待链表,检查哪些任务的等待条件已满足 */
        while( pxListItem != pxListEnd )
        {
          /* 获取下一个节点 */
          pxNext = listGET_NEXT( pxListItem );
          
          /* 这里省略了具体的条件检查代码 */
          /* 检查当前任务的等待条件是否满足 */
          /* 如果满足,则唤醒该任务 */
          
          /* 移动到下一个节点 */
          pxListItem = pxNext;
        }
        
        /* 清除需要清除的事件位 */
        /* 例如,某些等待条件需要在唤醒后自动清除对应的标志位 */
        pxEventBits->uxEventBits &= ~uxBitsToClear;
        
        /* 恢复调度器 */
        ( void ) xTaskResumeAll();
        
        /* 返回当前的事件组状态 */
        return pxEventBits->uxEventBits;
      }
      

      设置事件流程详解:

      1.暂停调度:首先暂停调度器,这是为了保证"设置事件位"和"检查等待任务"这两个操作作为一个整体被执行,不会被其他任务干扰。

      2.设置事件位:使用按位或操作符(|=)将指定的事件位置1。这是核心操作,将事件信息写入事件组。

      3.遍历等待队列:从链表头开始,逐个检查每个等待任务的等待条件。FreeRTOS会判断每个任务所等待的位是否已经被设置。

      4.唤醒符合条件的任务:当某个任务的等待条件满足时,系统会将其从等待链表移除,加入就绪队列。

      5.清除事件位(可选):根据具体的等待条件配置,可能需要清除某些事件位。

      6.恢复调度:恢复调度器,如果被唤醒的任务优先级更高,则会发生任务切换。

        2.5 等待事件位

        EventBits_t xEventGroupWaitBits(
            EventGroupHandle_t xEventGroup,      /* 事件组句柄,指定要等待哪个事件组 */
            const EventBits_t uxBitsToWaitFor,   /* 要等待的事件位,例如0x03表示等待第0位和第1位 */
            const BaseType_t xClearOnExit,       /* pdTRUE:返回时清除等待的位; pdFALSE:保持不变 */
            const BaseType_t xWaitForAllBits,    /* pdTRUE:"与"模式(所有位都满足); pdFALSE:"或"模式(任一位满足) */
            TickType_t xTicksToWait              /* 等待超时时间,portMAX_DELAY表示无限等待 */
        )
        {
          /* 函数实现会根据上述参数将当前任务挂起到等待链表 */
          /* 直到指定事件发生或超时后返回当前的事件组状态 */
        }
        

        参数详解:

        • xEventGroup:事件组句柄,指定要等待哪个事件组。这是操作的目标上下文。
        • uxBitsToWaitFor:指定任务等待的事件位组合。例如等待第0位和第1位,则设置为0x03(二进制为0011)。
        • xClearOnExit:函数返回时是否清除已等待的事件位。设置为pdTRUE时,返回前会将这些位清零;设置为pdFALSE时,保持这些位的当前状态。这一选项适用于需要保留事件状态的场景。
        • xWaitForAllBits:等待模式选择。pdTRUE表示"与"模式,只有当所有指定的位都被设置时任务才会被唤醒;pdFALSE表示"或"模式,任意一个指定的位被设置就会唤醒任务。
        • xTicksToWait:等待超时时间。设置为0表示立即返回(不等待);设置为portMAX_DELAY表示无限等待,直到事件发生。

        三、任务通知机制

        3.1 核心概念

        任务通知(Task Notification)是FreeRTOS 8.2.0引入的特性,它直接操作任务控制块(TCB)中的通知相关字段来实现任务间通信。与队列、信号量等传统机制相比,任务通知具有以下显著优势:

        • 零内存分配:不需要创建额外的数据结构,直接利用TCB中已有的存储空间
        • 极高效率:发送通知仅需数条CPU指令,开销远小于队列操作
        • 灵活的通知方式:支持五种不同的通知动作,满足多样化通信需求

        3.2 通用通知函数

        /* 便捷宏:最常用的"敲门"通知方式,每次通知使计数值+1 */
        #define xTaskNotifyGive( xTaskToNotify ) xTaskGenericNotify( ( xTaskToNotify ), ( 0 ), eIncrement, NULL )
        
        
        /**
         * 通用任务通知函数
         * @param xTaskToNotify 要通知的任务句柄(目标任务)
         * @param ulValue 通知的数值(具体含义取决于eAction)
         * @param eAction 通知动作类型,决定如何处理ulValue
         * @param pulPreviousNotificationValue 用于返回通知前的原始值(可选参数,可以为NULL)
         * @return pdPASS:通知成功; pdFAIL:通知失败(如eSetValueWithoutOverwrite时目标已有未处理通知)
         */
        BaseType_t xTaskGenericNotify( 
            TaskHandle_t xTaskToNotify,     /* 目标任务的任务句柄 */
            uint32_t ulValue,               /* 通知携带的数值 */
            eNotifyAction eAction,          /* 通知动作类型 */
            uint32_t *pulPreviousNotificationValue  /* 用于输出之前的通知值 */
        )
        {
          /* 获取目标任务的TCB(任务控制块)指针 */
          /* TCB是FreeRTOS用于管理每个任务的数据结构 */
          TCB_t * pxTCB = ( TCB_t * ) xTaskToNotify;
          
          BaseType_t xReturn = pdPASS;      /* 默认返回成功 */
          uint8_t ucOriginalNotifyState;   /* 用于保存原始通知状态 */
          
          /* 进入临界区,关闭中断,保证操作的原子性 */
          /* 临界区内的代码不会被中断打断 */
          taskENTER_CRITICAL();
          
          /* 记录目标任务的原始通知状态 */
          ucOriginalNotifyState = pxTCB->ucNotifyState;
          
          /* 设置任务的通知状态为"已接收到通知"(待处理状态) */
          pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;
          
          /* 根据通知动作类型执行相应的操作 */
          switch( eAction )
          {
            case eSetBits:
              /* 按位或操作:将ulValue的位合并到通知值中 */
              /* 例如:原通知值为0x01,ulValue为0x04,结果为0x05 */
              /* 适用于需要传递多个独立标志的场景 */
              pxTCB->ulNotifiedValue |= ulValue;
              break;
              
            case eIncrement:
              /* 通知值加1 */
              /* 这是最高效的通知方式,常用于"信号量"类型的同步 */
              /* 类似于:有人敲门,敲一声表示来了一次 */
              ( pxTCB->ulNotifiedValue )++;
              break;
              
            case eSetValueWithOverwrite:
              /* 直接覆写通知值,不检查原有值 */
              /* 这是一种"发送即忘"的模式,发送方不关心接收方是否已处理之前的通知 */
              /* 类似于:直接把新纸条塞进盒子,覆盖旧的内容 */
              pxTCB->ulNotifiedValue = ulValue;
              break;
              
            case eSetValueWithoutOverwrite:
              /* 只有当任务没有未处理通知时才写入新值 */
              /* 如果通知状态为taskNOTIFICATION_RECEIVED(已有未处理通知),则写入失败 */
              /* 适用于需要确保每条通知都被处理的场景,防止通知丢失 */
              if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED )
              {
                pxTCB->ulNotifiedValue = ulValue;
              }
              else
              {
                /* 目标已有未处理通知,写入失败 */
                xReturn = pdFAIL;
              }
              break;
              
            case eNoAction:
              /* 什么也不做,只唤醒任务 */
              /* 适用于只需要触发任务执行而不传递任何数据的场景 */
              /* 类似于:只打电话但不说话,对方接起就知道有事了 */
              break;
          }
          
          /* 检查目标任务是否正处于等待通知状态 */
          /* 如果是,说明目标之前调用了ulTaskNotifyTake()正在等待通知 */
          if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )
          {
            /* 将任务从等待链表中移除 */
            ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
            
            /* 将任务加入就绪队列,任务可以开始执行了 */
            prvAddTaskToReadyList( pxTCB );
            
            /* 优先级继承检查:如果被唤醒任务的优先级高于当前任务 */
            /* 则需要触发任务调度,让高优先级任务先执行 */
            if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
            {
              /* 触发PendSV中断或直接进行任务切换 */
              taskYIELD_IF_USING_PREEMPTION();
            }
          }
          
          /* 退出临界区,重新开启中断 */
          taskEXIT_CRITICAL();
          
          /* 返回操作结果 */
          return xReturn;
        }
        

        通知动作(eNotifyAction)详解:

        • eSetBits(设置位):将ulValue与通知值进行按位或操作。适合需要传递多个独立标志的场景,例如通知值可以同时表示多个不同的状态位。
        • eIncrement(递增):将通知值加1。这是最高效的通知方式,适用于只需要知道"有事件发生"而不需要携带具体数据的场景。FreeRTOS提供了便捷宏xTaskNotifyGive来使用这种方式。
        • eSetValueWithOverwrite(覆写):直接用ulValue覆盖通知值,不管任务之前是否已有未处理的通知。这是一种"发送即忘"的模式。
        • eSetValueWithoutOverwrite(不覆写):只有当任务没有未处理通知时才写入新值。如果任务已有未处理通知(通知状态为taskNOTIFICATION_RECEIVED),则返回pdFAIL。这种方式可以防止通知数据被覆盖丢失。
        • eNoAction(无动作):仅唤醒任务,不改变通知值。适用于只需要触发任务执行而不传递数据的场景。

        3.3 接收通知

        /**
         * 获取当前任务的通知值
         * @param xClearCountOnExit pdTRUE:返回后清零通知值; pdFALSE:返回通知值减1
         * @param xTicksToWait 等待超时时间,0表示立即返回,portMAX_DELAY表示无限等待
         * @return 通知值(返回时已根据xClearCountOnExit参数处理过)
         */
        uint32_t ulTaskNotifyTake( 
            BaseType_t xClearCountOnExit,   /* pdTRUE:清零; pdFALSE:减1 */
            TickType_t xTicksToWait         /* 等待超时时间 */
        )
        {
          uint32_t ulReturn;
          
          /* 进入临界区 */
          taskENTER_CRITICAL();
          
          /* 检查当前任务的的通知值是否为0 */
          /* 如果为0,说明没有未处理的通知,需要进入等待状态 */
          if( pxCurrentTCB->ulNotifiedValue == 0UL )
          {
            /* 设置任务的通知状态为"等待通知" */
            pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;
            
            /* 检查是否设置了等待时间 */
            /* 如果xTicksToWait为0,则不等待,直接返回(此时通知值为0) */
            if( xTicksToWait > ( TickType_t ) 0 )
            {
              /* 将当前任务添加到延迟等待列表 */
              /* 第二个参数pdTRUE表示这是一个任务级别的等待(不是ISR) */
              prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
              
              /* 触发任务调度,让出CPU控制权 */
              /* 任务将在这里暂停执行,直到被通知唤醒或超时 */
              portYIELD_WITHIN_API();
            }
          }
          
          /* 退出临界区 */
          taskEXIT_CRITICAL();
          
          /* 再次进入临界区,获取通知值 */
          /* 此时任务已经被唤醒(要么收到了通知,要么超时了) */
          taskENTER_CRITICAL();
          
          /* 获取当前的通知值 */
          ulReturn = pxCurrentTCB->ulNotifiedValue;
          
          /* 如果通知值不为0,说明确实收到了通知 */
          if( ulReturn != 0UL )
          {
            /* 根据xClearCountOnExit参数决定如何处理通知值 */
            if( xClearCountOnExit != pdFALSE )
            {
              /* pdTRUE:直接清零 */
              /* 适用于一次性通知,不需要保留计数 */
              pxCurrentTCB->ulNotifiedValue = 0UL;
            }
            else
            {
              /* pdFALSE:将通知值减1后返回 */
              /* 这种设计是为了配合eIncrement模式 */
              /* 每次调用处理一次通知,逐次递减 */
              /* 例如:收到3次通知,第一次调用返回3,盒子里剩2 */
              /* 第二次调用返回2,盒子里剩1...依此类推 */
              pxCurrentTCB->ulNotifiedValue = ulReturn - ( uint32_t ) 1;
            }
          }
          
          /* 重置任务的通知状态为"未等待通知" */
          /* 表示任务已经处理完当前的通知,可以接收新的通知了 */
          pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
          
          /* 退出临界区 */
          taskEXIT_CRITICAL();
          
          /* 返回通知值 */
          /* 如果是超时返回且没有收到通知,这里返回0 */
          return ulReturn;
        }
        

        参数说明:

        • xClearCountOnExit

          • 设置为pdTRUE时:返回后直接将通知值清零。这适用于一次性通知场景,通知被处理后即清除。
          • 设置为pdFALSE时:返回通知值减1。这主要配合eIncrement(递增)模式使用,允许任务逐次处理每一次通知。例如,某个任务收到3次通知,调用ulTaskNotifyTake(pdFALSE, ...)会返回3,盒子里的值变为2;再次调用返回2,盒子里的值变为1;第三次调用返回1,盒子里的值变为0。
        • xTicksToWait

          • 设置为0:立即返回,不等待。如果当前没有通知,返回0。
          • 设置为portMAX_DELAY:无限等待,直到收到通知(需要configUSE_16BIT_TICKS和configUSE_TIMERS配置支持)。
          • 设置具体数值:等待指定的Tick数后超时返回。

        四、关键概念补充

        4.1 临界区与原子性

        源码中多次出现taskENTER_CRITICAL()taskEXIT_CRITICAL(),这是FreeRTOS实现临界区(Critical Section)的机制。临界区是一段不能被中断打断的代码区域,用于保证某些操作的原子性。

        **为什么需要临界区?**考虑以下场景:任务A正在检查通知值是否为0,如果为0则设置等待状态。如果不加以保护,任务A检查完值为0后、正要设置等待状态之前,任务B发送了一个通知并检查了任务A的状态。此时任务A的状态仍然是"未等待通知",所以任务B不会唤醒任务A。任务A设置完等待状态后就永久等待了,导致通知丢失。这种由于执行顺序不确定导致的bug就是竞态条件。

        vTaskSuspendAll()与taskENTER_CRITICAL()的区别:

        • vTaskSuspendAll():只暂停调度,不关闭中断。中断服务程序仍可执行,但不会发生任务切换。适用于需要保护较长时间的临界区。
        • taskENTER_CRITICAL():同时暂停调度和关闭中断,提供更强的保护。但会延长中断响应时间,应尽量缩短临界区代码的执行时间。

        4.2 任务状态转换

        事件组和任务通知都涉及任务状态的转换:

        • 运行态(Running):任务正在CPU上执行
        • 就绪态(Ready):任务已准备好运行,等待调度器分配CPU时间
        • 阻塞态(Blocked):任务正在等待某个事件或资源
        • 挂起态(Suspended):任务被显式挂起,无法运行

        当任务调用xEventGroupWaitBits或ulTaskNotifyTake且条件不满足时,任务从运行态转变为阻塞态,挂载到相应的等待链表上。当事件发生后,系统将任务从等待链表移除,加入就绪队列。

        五、事件组与任务通知对比

        特性 事件组 任务通知
        通信模式 广播(一对多) 点对点(一对一)
        内存开销 需要额外EventGroup_t结构体 利用TCB已有空间,无需分配
        通知方式 通过位标志 计数(eIncrement)、值(eSetValue*)、位(eSetBits)
        等待模式 "与"和"或"两种模式 单一等待模式
        典型开销 中等(需遍历等待链表) 极低(仅数条CPU指令)

        选择建议:

        • 需要广播通知(一个事件唤醒多个任务)时使用事件组
        • 对性能要求极高且只需点对点通信时使用任务通知
        • 需要等待多个条件组合(如A且B都发生)时使用事件组
        • 只需要简单同步(如完成信号)时使用任务通知

        六、总结

        本文基于FreeRTOS源码详细分析了事件组和任务通知两种通信机制的实现原理。事件组通过位标志和等待链表实现多任务广播同步,支持32种事件标志和灵活的"与/或"等待条件配置。任务通知则通过直接操作TCB中的通知字段实现高效的点对点通信,提供五种通知动作满足不同场景需求。

        实际项目中,应根据具体需求选择合适的通信机制:对于需要广播或多条件等待的场景优先使用事件组,对于性能敏感的简单同步场景优先使用任务通知。

        Logo

        智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

        更多推荐