1. 简介
事件组是FreeRTOS提供的另一种同步机制。它与队列和信号量的区别是:
- 允许任务进入blocked状态,等待一个或者多个事件;
- 事件组会唤醒所有等待这个事件或事件组的任务。
所以,事件组通常用于多个任务(中断)之间的同步。
1.1. 事件组(Event Groups),事件标志(Event Flags)和事件位图(Event Bits)
事件标志:是一个布尔值,用于指示一个事件是否发生;因此可以用一个位来表示。
事件组:是一组事件标志。
事件位图:是事件组的一种实现方式,将事件组中的所有事件标志用一张位图来表示。如果某个位为1,则表明对应的事件已发生。
2. 创建事件组
函数原型:
EventGroupHandle_t xEventGroupCreate( void ) PRIVILEGED_FUNCION;
创建时,FreeRTOS为事件组创建一小片内存,用于存储事件组的元信息。如果创建成功则返回事件组的句柄,否则返回NULL
。
下面来看看其具体实现:
创建事件组的过程比较简单,在申请到所需的内存后,只是初始化事件组的事件位图以及等待任务队列。
3. 事件组的使用
3.1. 等待与查询事件
等待事件的函数原型:
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToWaitFor, const BaseType_t xClearOnExit, const BaseType_t xWaitForAllBits, TickType_t xTicksToWait ) PRIVILEGED_FUNCTION;
可以阻塞地等待事件组xEventGroup
中的一个或多个事件的发生。不能在中断处理函数中调用。
其中,uxBitsToWaitFor
指示了调用者希望等待的事件组,用事件位图表示。
xClearOnExit
如果设置为pdTRUE
,那么在这个函数正常返回的时候(非超时),就会清除掉系统内部与uxBitsToWaitFor
对应的事件位图;否则不会。
xWaitForAllBits
如果设置为pdTRUE
,表示只有在uxBitsToWaitFor
事件位图中的所有事件发生时,才会唤醒任务。
xTicksToWait
表示等待的时限。
如果是超时返回,那么返回的事件位图表示超时时刻已经发生的事件。因此,调用者一定要确认返回值(除非是不限期的等待)。
该函数的实现涉及到三个比较重要的行为:
-
检查等待条件是否已经满足,所以在介绍具体的实现之前,先来看看该检查的流程(
static BaseType_t prvTestWaitCondition( const EventBits_t uxCurrentEventBits, const EventBits_t uxBitsToWaitFor, const BaseType_t xWaitForAllBits )
):{ 初始化`xWaitConditionMet = pdFALSE`。 如果是等待事件位图中某个事件的情况(`xWaitForAllBits == pdFALSE`): { 如果事件位图满足条件(`( uxCurrentEventBits & uxBitsToWaitFor ) != 0`),则设置条件满足标志(`xWaitConditionMet = pdTRUE`)。 } 否则,即等待事件位图中所有事件: { 如果事件位图满足条件(`( uxCurrentEventBits & uxBitsToWaitFor ) == uxBitsToWaitFor`),则设置条件满足标志(`xWaitConditionMet = pdFALSE`)。 } 返回`xWaitConditionMet`。 }
-
将任务放置到无序事件队列(
void vTaskPlaceOnUnorderedEventList( List_t * pxEventList, const TickType_t xItemValue, const TickType_t xTicksToWait )
):{ 参数检查:Assert `pxEventList != NULL`。 关键条件检查:Assert 调度器必须被挂起(`uxSchedulerSuspended != 0`)。 设置任务队列的事件队列项值为等待的事件位图,并且设置占用标志(`listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xEventListItem ), xItemValue | taskEVENT_LIST_ITEM_VALUE_IN_USE ) `)。 将任务插入到事件队列的尾部(`vListInsertEnd( pxEventList, &( pxCurrentTCB->xEventListItem ) )`)。 同时将任务加入到Delayed任务队列(`prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE )`)。 }
-
从任务事件队列中获取当前的事件位图(
TickType_t uxTaskResetEventItemValue( void )
):{ 获取当前任务的事件位图(uxReturn = listGET_LIST_ITEM_VALUE( &( pxCurrentTCB->xEventListItem ) )); 将当前任务的事件队列项的值恢复到默认, 以便于基于队列的任务同步机制使用(listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xEventListItem ), ( ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) pxCurrentTCB->uxPriority ) )); }
下面来看看其具体的实现:
可以看到,事件组的实现和队列的实现不一样。队列在使用事件任务队列时,将任务按照优先级顺序排列,以便于唤醒优先级最高的队列;而事件组则不需要,因为它可能一次性唤醒所有任务,并且每个任务等待的事件位图不一致。
如果不希望阻塞地等事件,则可以通过以下接口获取事件:
#define xEventGroupGetBits( xEventGroup ) xEventGroupClearBits( xEventGroup, 0 )
该函数实际是通过xEventGroupClearBits()
实现的,只是它清除的事件位图为0,所以并不会影响该事件组当前的事件位图。
另外,该函数还有ISR的版本:
EventBits_t xEventGroupGetBitsFromISR( EventGroupHandle_t xEventGroup ) PRIVILEGED_FUNCTION;
其具体实现如下:
{
初始化`pxEventBits = ( EventGroup_t * ) xEventGroup`。
进入临界区(`uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR()`)。
获取事件组的事件位图(`uxReturn = pxEventBits->uxEventBits`)。
退出临界区(`portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus )`)。
返回事件位图(`xReturn`)。
}
3.2. 设置事件
函数原型:
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet ) PRIVILEGED_FUNCTION;
该函数可以设置事件组xEventGroup
的事件位图,按照uxBitsToSet
事件位图来进行设置。这可能唤醒正在等待事件的任务。
函数返回值是该函数返回时刻的事件位图:例如,如果在xEventGroupWaitBits()
中设置了自动清除,那么函数返回时uxBitsToSet
所代表的事件位图就会被清空;或者更高优先级的任务被唤醒,它操作了同一个事件组,也会导致该函数返回的事件位图与设置的uxBitsToSet
不一致。因此这个返回值的意义不是特别大.
该函数涉及到从无序任务队列中删除任务,需要特别说明一下(BaseType_t xTaskRemoveFromUnorderedEventList( ListItem_t * pxEventListItem, const TickType_t xItemValue )
):FreeRTOSv10后取消了该函数的返回值。
{
条件检查:Assert 调度器必须挂起(`uxSchedulerSuspended != pdFALSE`)。
设置任务事件队列列表项的值(`listSET_LIST_ITEM_VALUE( pxEventListItem, xItemValue | taskEVENT_LIST_ITEM_VALUE_IN_USE )`)。
获取被唤醒任务的TCB(`pxUnblockedTCB = ( TCB_t * ) listGET_LIST_ITEM_OWNER( pxEventListItem )`)。
将任务从事件任务队列中移除(`uxListRemove( pxEventListItem )`)。
将任务从Delayed任务队列中删除(`uxListRemove( &( pxUnblockedTCB->xStateListItem ) )`)。
将任务加入到Ready任务队列(`prvAddTaskToReadyList( pxUnblockedTCB )`)。
如果被唤醒任务的优先级较高(`pxUnblockedTCB->uxPriority > pxCurrentTCB->uxPriority`):
{
设置调度请求标志用于返回(`xReturn = pdTRUE`)。
设置调度挂起标志(`xYieldPending = pdTRUE`)。
}
否则:清除调度请求标志用于返回(`xReturn = pdFALSE`)。
返回`xReturn`。
}
下面来看看该函数的具体实现:
该函数的中断版本如下:
#if (configUSE_TRACE_FACILITY == 1 )
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, BaseType_t *pxHigherPriorityTaskWoken ) PRIVILEGED_FUNCTION;
#else
#define xEventGroupSetBitsFromISR( xEventGroup, uxBitsToSet, pxHigherPriorityTaskWoken ) xTimerPendFunctionCallFromISR( vEventGroupSetBitsCallback, ( void * ) xEventGroup, ( uint32_t ) uxBitsToSet, pxHigherPriorityTaskWoken )
#endif
void vEventGroupSetBitsCallback( void *pvEventGroup, const uint32_t ulBItsToSet ) PRIVILEGED_FUNCTION;
下面看看具体的实现:
{
调用通过Pended函数完成`xReturn = xTimerPendFunctionCallFromISR( vEventGroupSetBitsCallback, ( void * ) xEventGroup, ( uint32_t ) uxBitsToSet, pxHigherPriorityTaskWoken ) `。
}
其中vEventGroupSetBitsCallback()
的实现如下:
{
xEventGroupSetBits( pvEventGroup, ( EventBits_t ) ulBitsToSet )。
}
所以最终还是通过Pend函数调用xEventGroupSetBits()
完成设置。
3.3. 清除事件
函数原型:
EventBits_t xEventGroupClearBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear ) PRIVILEGED_FUNCTION;
清除事件组xEventGroup
的事件位图,以uxBitsToClear
为清除的事件位图。函数会返回被清除之前的位图。
该函数的具体实现如下:
{
初始化`pxEventBits = ( EventGroup_t * ) xEventGroup`。
参数检验:Assert `xEventGroup != NULL`,`( uxBitsToClear & eventEVENT_BITS_CONTROL_BYTES ) == 0`。
进入临界区(`taskENTER_CRITICAL()`)。
暂存事件组当前的事件位图用于返回(`uxReturn = pxEventBits->uxEventBits`)。
清除事件组当前的事件位图(`pxEventBits->uxEventBits &= ~uxBitsToClear`)。
退出临界区(`taskEXIT_CRITICAL()`)。
返回事件位图`uxReturn`。
}
该函数不能在中断上下文中调用。但可以使用中断上下文中的版本:
#if ( configUSE_TRACE_FACILITY == 1 )
BaseType_t xEventGroupClearBitsFromISR( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet ) PRIVILEGED_FUNCTION;
#else
#define xEventGroupClearBitsFromISR( xEventGroup, uxBitsToClear ) xTimerPendFunctionCallFromISR( vEventGroupClearBitsCallback, ( void * ) xEventGroup, ( uint32_t ) uxBitsToClear, NULL )
#endif
void vEventGroupClearBitsCallback( void *pvEventGroup, const uint32_t ulBitsToClear ) PRIVILEGED_FUNCTION;
从函数原型定义就可以看出,如果是没有开启调试的功能,则可以通过Pend函数功能来实现。
具体实现如下:
{
调用Pend函数`xReturn = xTimerPendFunctionCallFromISR( vEventGroupClearBitsCallback, ( void * ) xEventGroup, ( uint32_t ) uxBitsToClear, NULL )`。
}
而vEventGroupClearBitsCallback()
的实现如下:
{
xEventGroupClearBits( pvEventGroup, ( EventBits_t ) ulBitsToClear )
}
可以看到,与设置事件的ISR版本一样,ISR的版本实际上是通过Pend函数,调用xEventGroupClearBits()
实现。
3.4. 同步事件
同步事件主要用于以下场景:多个任务之间的同步,通常指的是需要一起达到某个同步点(即每个任务都需要达到一个预设的状态)后,他们才能继续往下执行,否则先抵达同步点的任务处于Blocked状态。
函数原型:
EventBits_t xEventGroupSyc( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, const EventBits_t uxBitsToWaitFor, TickType_t xTicksToWait ) RPIVILEGED_FUNCTION;
如果在等待的过程中,uxBitsToWaitFor
对应的事件位图已经被设置,函数则正常返回,并且对应的事件位图也会被清除。
uxBitsToSet
是被同步的多个任务分别传入的事件位图,如果同步成功,那么这些任务调用时传入的uxBitsToSet
将会组成完整的uxBitsToWaitFor
。
xTicksToWait
是等待的时间。
如果同步成功,那么函数返回的事件位图就是uxBitsToWaitFor
所代表的事件位图;如果超时,那么则返回超时时刻的事件位图。
其具体实现如下:
{
初始化`pxEventBits = ( EventGroup_t * ) xEventGroup`,`xTimeoutOccured = pdFALSE`。
参数校验:Assert `( uxBitsToWaitFor & eventEVENT_BITS_CONTROL_BYTES ) == 0`,`uxBitsToWaitFor != 0`
(仅开启xTaskGetSchedulerState || USE_TIMERS)参数校验:Assert `!( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPEND ) && ( xTicksToWait != 0 ) )`。
挂起调度器(`vTaskSuspendAll()`)。
获取事件组的事件位图(`uxOriginalBitValue = pxEventBits->uxEventBits`)。
设置事件组的事件位图(`xEventGroupSetBits( xEventGroup, uxBitsToSet )`)。
如果条件满足唤醒条件(`( ( uxOriginalBitValue | uxBitsToSet ) & uxBitsToWaitFor ) == uxBitsToWaitFor`):
{
暂存事件位图用于返回(`uxReturn = uxOriginalBitsValue | uxBitsToSet`)。
清除事件组的事件位图(`pxEventBits->uxEventBits &= ~uxBitsToWaitFor`)。
清空等待事件(`xTicksToWait = 0`)。
}
否则:
{
如果调用者愿意阻塞(`xTicksToWait != 0`):
{
将任务加入到无序事件任务队列(`vTaskPlaceOnUnorderedEventList( &( pxEventBits->xTasksWaitingForBits ), ( uxBitsToWaitFor | eventCLEAR_EVENTS_ON_EXIT_BIT | eventWAIT_FOR_ALL_BITS ), xTicksToWait )`)。
}
否则,即调用者不愿意阻塞:
{
缓存当前事件组的事件位图(`uxReturn = pxEventBits->uxEventBits`)。
设置超时标志`xTimeoutOccured = pdTRUE`。
}
}
恢复调度器,并获取调度标志(`xTasksResumeAll()`)。
如果调用者愿意阻塞(`xTicksToWait != 0`):
{
如果恢复调度器时为发生调度(`xAlReadyYielded == pdFALSE`),则强制触发一次调度(`portYIELD_WITHIN_API()`)。
// 此时任务已经被唤醒
复位并获取事件组当前的事件位图(`uxReturn = uxTaskResetEventItemValue()`)。
如果该任务是因为超时而被唤醒(`( uxReturn & eventUNBLOCKED_DUE_TO_BIT_SET ) == 0`):
{
进入临界区(`taskENTER_CRITICAL()`)。
获取当前的事件位图(`uxReturn = pxEventBits->uxEventBits`)。
再次判断是否满足需求,如果满足(`( uxReturn & uxBitsToWaitFor ) == uxBitsToWaitFor`),清除对应的事件位图(`pxEventBits->uxEventBits &= ~uxBitsToWaitFor`)。
退出临界区(`taskEXIT_CRITICAL()`)。
设置超时标志(`xTimeoutOccured = pdTRUE`)。
}
清除控制位信息(`uxReturn &= ~eventEVENT_BITS_CONTROL_BYTES`)。
}
返回缓存的事件位图`uxReturn`。
}
可以看到,在同步事件中调用了xEventGroupSetBits()
,因此可以实现由多个任务来等待并更新该事件组的事件位图的需求。
4. 删除事件组
函数原型:
void vEventGroupDelete( EventGroupHandle_t xEventGroup ) PRIVILEGED_FUNCTION;
该函数会删除事件组xEventGroup
,当前所有等待该事件组的任务都会被唤醒,并且获取的事件位图为0。
具体实现如下:
{
初始化`pxEventBits = ( EventGroup_t * ) xEventGroup`,`pxTaskWaitingForBits = &( pxEventBits->xTasksWaitingForBits )`。
挂起调度器(`vTaskSuspendAll()`)。
遍历事件组的事件任务队列,直到队列为空(`while ( listCURRENT_LIST_LENGTH( pxTasksWaitingForBits ) > 0 )`):
{
将任务从无序事件队列中删除(`xTaskRemoveFromUnorderedEventList( pxTaskWaitingForBits->xListEnd.pxNext, eventUNBLOCKED_DUE_TO_BIT_SET`)
}
(仅开启SUPPORT_DYNAMIC_ALLOCATION && SUPPORT_STATIC_ALLOCATION)如果没有设置静态申请标志(`pxEventBits->ucStaticallyAllocated == pdFALSE`),则回收事件组所使用的内存(`pxEventBits`)。
恢复调度器(`vTaskResumeAll()`)。
}
5. 事件组调试接口
获取事件组编号的函数原型:
UBaseType_t uxEventGroupGetNumber( void* xEventGroup ) PRIVILEGED_FUNCTION;
6. 事件组的实现细节
6.1. 事件组的接口
6.1.1. 事件组的数据结构接口
6.1.1.1. 事件组句柄
句柄定义:
typedef void * EventGroupHandle_t;
创建事件组时返回的句柄,用作所有的事件组API的参数。
6.1.1.2. 事件位图
类型定义:
typedef TickType_t EventBits_t;
TickType_t
被定义为系统的机器字长。因此,事件位图的大小与体系结构相关。
另外,TickType_t
还可以通过configUSE_16_BIT_TICKS
定义成16位。
但具体实现时,最高有效8位是系统预留事件位,不可用。
6.2. 事件组的内涵
6.2.1. 事件组依赖的头文件
C标准库头文件:
#include <stdlib.h>
FreeRTOS头文件:
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"
#include "event_groups.h"
可以看出,事件组的事件与任务和软件定时器有关。
6.2.2. 事件组的数据结构
6.2.2.1. 事件组的元信息结构体
结构体定义:
typedef struct xEventGroupDefinition
{
EventBits_t uxEventBits;
List_t xTasksWaitingForBits; /*< List of tasks waiting for a bitmap to be set */
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxEventGroupNumber;
#endif
#if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated; /*< Set to pdTRUE if the event group is statically allocated to ensure no attempt is made to free the memory. */
#endif
} EventGroup_t;
6.2.3. 事件组的私有宏
6.2.3.1. 事件组的私有常量
事件组的内部事件标志定义:
#if ( configUSE_16_BIT_TICKS == 1 )
#define eventCLEAR_EVENTS_ON_EXIT_BIT 0x0100U
#define eventUNBLOCKED_DUE_TO_BIT_SET 0x0200U
#define eventWAIT_FOR_ALL_BITS 0x0400U
#define eventEVENT_BITS_CONTROL_BYTES 0xff00U
#else
#define eventCLEAR_EVENTS_ON_EXIT_BIT 0x01000000UL
#define eventUNBLOCKED_DUE_TO_BIT_SET 0x02000000UL
#define eventWAIT_FOR_ALL_BITS 0x04000000UL
#define eventEVENT_BITS_CONTROL_BYITES 0xff000000UL
#endif
可以看到,事件位图的最高8位用于系统控制。其中最高有效位用于表示xItemValue
被用于其他用途。详情可参看队列管理。