FreeRTOS-事件组详解
事件是一种实现任务/中断间通信的机制,主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输。
✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
📃个人主页:@rivencode的个人主页
🔥系列专栏:玩转FreeRTOS
💬保持学习、保持热爱、认真分享、一起进步!!!
目录
前言
很久没有更新了,主要是因为在实习,忙论文的事,最主要的是事把又模电重新完完整整学了一遍,感觉我对电路的理解得到了升华,接下来继续把FreeRTOS更新完,最后上STM32单片机大师篇,前面已经把FreeRTOS最重要的应用,队列与信号量讲解完了,核心其实已经OK了,接下来就是将Freertos其他应用讲解完,本文要讲解的事件组应用非常简单,所以我们直接源码讲解,理解到底是如何实现的。
一、事件组的简介
事件是一种实现任务/中断间通信的机制,主要用于实现多任务间的同步,但事件通信只能
是事件类型的通信,无数据传输。其实事件组的本质就是一个整数(16/32位)。
- 与队列/信号量的区别:
1.信号量/队列当事件发生时只会去唤醒一个任务,而事件组可以唤醒多个任务起到一个广播的作用。
2.信号量/队列是一个消耗性资源,即数据读走了则就减少,而事件组可以选择清除事件也可以选择保留事件。
3.事件组只能是起到一个同步的作用,并不能传递数据。
4.最重要的一点事件组可以实现多个任务之间的同步,队列/信号量则只能是两个任务之间的同步(看完源码你就懂了)。
- 全局变量的区别
其实事件组相当于多个全局变量flag组成的,但是在操作系统中,事件组的优势明显。
1.全局变量使用在操作系统中存在被多个任务同时读写(前面的文章讲过为什么不能同时操作一个变量)的风险,则事件组它会直接禁止任务调度来规避风险。
2.使用全局变量需要自己去实现阻塞机制(成本太高)。
3.使用事件组能更方便的实现多任务之间的同步。
事件组的特点:
1.一个 32 位的事件集合(EventBits_t 类型的变量,实际可用与表示事件的只有 24 位,还有8位用于管理事件),其中每一位表示一种事件类型(0 表示该事件类型未发生、1 表示该事件类型已经发
生)。
2.事件仅用于同步,不提供数据传输功能。
3. 与信号量/队列不同设置事件组不会阻塞,即多次向任务设置同一事件等效于只设置一次。
4. 支持事件等待超时机制,即等待该事件类型(该事件还未发生)的任务会进入阻塞态。
5.事件获取的时候,有两个选择:1.逻辑或:任务所期望的事件中只要有任意一个事件发生,任务即可被唤醒。2.逻辑或:任务所期望的事件必须全部发生,任务才能被唤醒。
二、事件组源码分析
我们只需要搞清楚事件组是一个什么样的结构体,以及xEventGroupCreate()、xEventGroupSetBits()、xEventGroupWaitBits(),这三个函数的源码实现,还要搞明白上文提到的那8位是怎样管理事件组的。
1.事件组的创建
1.事件组结构体
uxEventBits:EventBits_t类型的变量,在32位平台下(stm32),其实就是一个32位的无符号整形,其中高8位是控制为,低24位则用来存储事件,每一位代表一个不同事件(1:事件发生 0:事件未发生)。
xTasksWaitingForBits:是一个用来挂载等待事件发生的任务(阻塞机制)
一个变量,一条链表就是一个事件组,多么的简单。
2.动态创建事件组
创建事件组非常简单:
1.为事件组结构体分配内存
2.初始化事件组
3.初始化等待链表
2.等待事件函数 xEventGroupWaitBits()
为什么要先讲解 xEventGroupWaitBits()这个函数,我们先明白前面8位控制位的作用,等后面的xEventGroupSetBits()就很好理解了。
xEventGroupWaitBits()函数原型:
函数参数:
1.xEventGroup:事件组的句柄(传入事件组结构体的指针)。
2.uxBitsToWaitFor:等待的事件标志位,可以用逻辑或等待多个事件标志位,例如某任务想等待事件1和事件3,即(0x01 | 0x03)传入即可。
3.xClearOnExit:
pdTRUE:当 xEventGroupWaitBits()等待到满足任务唤醒的事件
时,系统将清除由形参 uxBitsToWaitFor 指定的事件标志位。
pdFALSE:不会清除由形参 uxBitsToWaitFor 指定的事件标志位。
(注意:这里清除只是清除该任务所期望的事件,并不会把24位都清零(因为其他任务所期望的事件与该任务无关))
4.xWaitForAllBits:
pdTRUE : 当形 参 uxBitsToWaitFor 指 定 的 位都 置 位 的 时 候 ,
xEventGroupWaitBits()才满足任务唤醒的条件,这也是“逻辑与”
等待事件,并且在没有超时的情况下返回对应的事件标志位的值。
pdFALSE:当形参 uxBitsToWaitFor 指定的位有其中任意一个置位
的时候,这也是常说的“逻辑或”等待事件,在没有超时的情况下
函数返回对应的事件标志位的值。
5.xTicksToWait:获取等待的阻塞时间
返回值:函数返回时,事件组中的事件标志位值
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, uxControlBits = 0;
BaseType_t xWaitConditionMet, xAlreadyYielded;
BaseType_t xTimeoutOccurred = pdFALSE;
/* 检查用户没有去尝试等待内核本身使用的位(高8位),并且至少请求了一个位(低24位)。*/
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();
{
/* 取出该事件组的32位变量赋给uxCurrentEventBits */
const EventBits_t uxCurrentEventBits = pxEventBits->uxEventBits;
/* 检查是否已满足等待条件。 */
xWaitConditionMet = prvTestWaitCondition( uxCurrentEventBits, uxBitsToWaitFor, xWaitForAllBits );
/* 已满足等待条件 */
if( xWaitConditionMet != pdFALSE )
{
/* 已满足等待条件,任务无需阻塞(怕你之前设置了等待时间) */
uxReturn = uxCurrentEventBits;
xTicksToWait = ( TickType_t ) 0;
/* 如果请求清除等待位 */
if( xClearOnExit != pdFALSE )
{
/* 清除事件组中uxBitsToWaitFor设置的事件位 */
pxEventBits->uxEventBits &= ~uxBitsToWaitFor;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else if( xTicksToWait == ( TickType_t ) 0 )
{
/* 未满足等待条件,但没有阻塞时间指定,因此只需返回当前值。 */
uxReturn = uxCurrentEventBits;
xTimeoutOccurred = pdTRUE;
}
else
{
/*任务将被阻塞以等待其所需的位设置。uxControlBits变量
用于记住此调用 xEventGroupWaitBits()的指定行为(1.是否要清除 2.是逻辑与还是逻辑或)
- 用于事件位取消阻止任务。 */
if( xClearOnExit != pdFALSE )
{
/* 若需要清除事件,将uxControlBits的第25位置1 */
uxControlBits |= eventCLEAR_EVENTS_ON_EXIT_BIT;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( xWaitForAllBits != pdFALSE )
{
/* 若使用逻辑与,将uxControlBits的第26位置1 */
uxControlBits |= eventWAIT_FOR_ALL_BITS;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 1.将任务需要等待的事件位和事件控制位(即上文uxControlBits的值)或操作存储在任务的
xItemValue(原来辅助排序的值)中。
2.将任务添加至等待事件组的列表中。
3.将任务添加至延时列表中,任务进入阻塞态。
*/
vTaskPlaceOnUnorderedEventList( &( pxEventBits->xTasksWaitingForBits ), ( uxBitsToWaitFor | uxControlBits ), xTicksToWait );
uxReturn = 0;
traceEVENT_GROUP_WAIT_BITS_BLOCK( xEventGroup, uxBitsToWaitFor );
}
}
/* 恢复调度器 */
xAlreadyYielded = xTaskResumeAll();
if( xTicksToWait != ( TickType_t ) 0 )
{
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 任务被阻塞以等待其所需的位被设置 - 此时已设置所需位或阻塞时间超时。
如果设置了所需的位,它们将存储在任务的事件列表项中,现在应该检索然后清除它们。 */
uxReturn = uxTaskResetEventItemValue();
/* 如果是阻塞时间超时导致的任务唤被醒,而不是任务所期待事件已发生了 */
if( ( uxReturn & eventUNBLOCKED_DUE_TO_BIT_SET ) == ( EventBits_t ) 0 )
{
taskENTER_CRITICAL();
{
/* 任务超时,只需返回当前事件位值。 */
uxReturn = pxEventBits->uxEventBits;
/* 事件位可能在此任务离开“已阻塞”状态并再次运行之间进行了更新
重新检测是否满足任务的等待条件*/
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;
}
/*-----------------------------------------------------------*/
挑几个重点来讲:
1.如何检测任务已瞒住等待条件
uxBitsToWaitFor设置的是任务期望的事件,但满住任务的等待条件有两个选择,xWaitForAllBits==PdTURE则任务需要等待任务期望的事件全部发生,若xWaitForAllBits == PdFALSE 则任务只需等待其中任意一个事件发生就行。
2.当任务未满足等待的条件时,此时任务需要进入进入阻塞态了,但是进入阻塞态之前有个问题,之后任务该如何被唤醒?
所以我们要考虑几个问题:
1.我们至少需要保存该任务所期望的是那些事件(那些位)
2.需要保存任务到底是逻辑与还是逻辑或
3.满足条件后是否需要清除uxBitsToWaitFor设置的事件位
最后一个问题将这些东西保存在哪里?
答:保存在任务结构体中的xItemValue这个变量中
所以这就是事件组高8位的作用:
将这些信息保存在任务的xItemValue这个变量中,等下次调用xEventGroupSetBits()函数就知道该任务的是否已满足等待条件,以及满足等待条件后是否清除uxBitsToWaitFor设置的事件。
taskEVENT_LIST_ITEM_VALUE_IN_USE的作用就是提醒xItemValue已经被事件所使用,原来是用于任务的优先级辅助排序的。
有其他不懂的请看上面的整体代码解析
3.事件组置位函数 xEventGroupSetBits()
xEventGroupSetBits()函数原型:
函数参数:
xEventGroup:事件组句柄
uxBitsToSet:需要设置指定事件中的事件标志位。
返回值:函数返回时,事件组中的事件标志位值
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet )
{
ListItem_t * pxListItem, * pxNext;
ListItem_t const * pxListEnd;
List_t const * pxList;
EventBits_t uxBitsToClear = 0, uxBitsWaitedFor, uxControlBits;
EventGroup_t * pxEventBits = xEventGroup;
BaseType_t xMatchFound = pdFALSE;
/* 不允许用户区去设置内核使用的位本身。 */
configASSERT( xEventGroup );
configASSERT( ( uxBitsToSet & eventEVENT_BITS_CONTROL_BYTES ) == 0 );
/* 获得该链表xTasksWaitingForBits的指针 */
pxList = &( pxEventBits->xTasksWaitingForBits );
/* 获得链表xTasksWaitingForBits的头结点 */
pxListEnd = listGET_END_MARKER( pxList );
/* 挂起调度器 */
vTaskSuspendAll();
{
traceEVENT_GROUP_SET_BITS( xEventGroup, uxBitsToSet );
/* 获取链表中第一个任务 */
pxListItem = listGET_HEAD_ENTRY( pxList );
/* 根据用户指定的 uxBitsToSet 设置事件标志位。 */
pxEventBits->uxEventBits |= uxBitsToSet;
/* 设置这个事件标志位可能是某个任务在等待的事件,就需要遍历
等待事件列表中的任务,看看这个事件是否与任务等待的事件匹配。
*/
while( pxListItem != pxListEnd )
{
pxNext = listGET_NEXT( pxListItem );
/* 取出该任务xItemValue(在xEventGroupWaitBits()函数中存入的信息) */
uxBitsWaitedFor = listGET_LIST_ITEM_VALUE( pxListItem );
xMatchFound = pdFALSE;
/* 事件组控制高8位存入uxControlBits变量中*/
uxControlBits = uxBitsWaitedFor & eventEVENT_BITS_CONTROL_BYTES;
/* 低24位存入uxBitsWaitedFor中 */
uxBitsWaitedFor &= ~eventEVENT_BITS_CONTROL_BYTES;
/* 此时uxControlBits就派上用场了 */
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
{
/* 需要设置所有位,但并非所有位都已设置。*/
}
if( xMatchFound != pdFALSE )
{
/* 位匹配,退出时是否应该清除位?*/
if( ( uxControlBits & eventCLEAR_EVENTS_ON_EXIT_BIT ) != ( EventBits_t ) 0 )
{
uxBitsToClear |= uxBitsWaitedFor;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 在从事件列表中删除任务之前,将实际事件标志值存储在任务的事件列表项中。
这设置eventUNBLOCKED_DUE_TO_BIT_SET位,以便任务知道由于所需的位匹配而被唤醒,
而不是因为它阻塞超时。(与xEventGroupWaitBits()函数相呼应)*/
vTaskRemoveFromUnorderedEventList( pxListItem, pxEventBits->uxEventBits | eventUNBLOCKED_DUE_TO_BIT_SET );
}
/* 移至下一个任务。*/
pxListItem = pxNext;
}
/*清除在控制字中设置eventCLEAR_EVENTS_ON_EXIT_BITbit时匹配的任何位. */
pxEventBits->uxEventBits &= ~uxBitsToClear;
}
/* 恢复调度器 */
( void ) xTaskResumeAll();
return pxEventBits->uxEventBits;
}
/*-----------------------------------------------------------*/
讲解完xEventGroupWaitBits函数,xEventGroupSetBits就非常简单了,直接看代码注释就OK了。
不过这里需要提醒一下:
这个大的while循环是将所有挂载在xTasksWaitingForBits列表中的任务都遍历一遍,只要满足条件的任务即可唤醒,所以说事件组具有广播的作用,而且清除事件的时候是所有的任务需要清除事件的或。
4.任务同步函数xEventGroupSync
xEventGroupSync函数原型:
函数参数:
xEventGroup :事件组句柄
uxBitsToSet: 达到同步点后,要设置的事件标志
uxBitsToWaitFor :等待的事件标志
xTicksToWait: 等待的阻塞时间
函数返回值:
等待事件标志位成功,返回等待到的事件标志位
等待事件标志位失败,返回事件组中的事件标志位
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait )
{
EventBits_t uxOriginalBitValue, uxReturn;
EventGroup_t * pxEventBits = xEventGroup;
BaseType_t xAlreadyYielded;
BaseType_t xTimeoutOccurred = pdFALSE;
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();
{
uxOriginalBitValue = pxEventBits->uxEventBits;
/* 调用位设置函数(逻辑与),用该函数唤醒阻塞的任务 */
( void ) xEventGroupSetBits( xEventGroup, uxBitsToSet );
/* 所有任务都同步了 */
if( ( ( uxOriginalBitValue | uxBitsToSet ) & uxBitsToWaitFor ) == uxBitsToWaitFor )
{
/* All the rendezvous bits are now set - no need to block. */
uxReturn = ( uxOriginalBitValue | uxBitsToSet );
/* 所有任务都同步后总是清除位 */
pxEventBits->uxEventBits &= ~uxBitsToWaitFor;
xTicksToWait = 0;
}
else
{
if( xTicksToWait != ( TickType_t ) 0 )
{
traceEVENT_GROUP_SYNC_BLOCK( xEventGroup, uxBitsToSet, uxBitsToWaitFor );
/* 1.eventCLEAR_EVENTS_ON_EXIT_BIT:清除位
2.eventWAIT_FOR_ALL_BITS:逻辑与
3.任务添加至xTasksWaitingForBits列表
4.任务添加至延时链表,进入阻塞态*/
vTaskPlaceOnUnorderedEventList( &( pxEventBits->xTasksWaitingForBits ), ( uxBitsToWaitFor | eventCLEAR_EVENTS_ON_EXIT_BIT | eventWAIT_FOR_ALL_BITS ), xTicksToWait );
uxReturn = 0;
}
else
{
/* 未指定超时时间,只需返回当前事件位值。 */
uxReturn = pxEventBits->uxEventBits;
xTimeoutOccurred = pdTRUE;
}
}
}
xAlreadyYielded = xTaskResumeAll();
if( xTicksToWait != ( TickType_t ) 0 )
{
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 任务被阻塞以等待其所需的位被设置 - 此时已设置所需位或阻塞时间超时。
如果设置了所需的位,它们将存储在任务的事件列表项中,现在应该检索然后清除它们。 */
uxReturn = uxTaskResetEventItemValue();
if( ( uxReturn & eventUNBLOCKED_DUE_TO_BIT_SET ) == ( EventBits_t ) 0 )
{
/* The task timed out, just return the current event bit value. */
taskENTER_CRITICAL();
{
uxReturn = pxEventBits->uxEventBits;
/* 尽管任务因为超时而到这里,
事件位可能在此任务离开阻状态到再次运行之间进行了更新。
如果是这种情况,那么它需要在退出之前清除位。 */
if( ( uxReturn & uxBitsToWaitFor ) == uxBitsToWaitFor )
{
pxEventBits->uxEventBits &= ~uxBitsToWaitFor;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
xTimeoutOccurred = pdTRUE;
}
else
{
/* The task unblocked because the bits were set. */
}
/* Control bits might be set as the task had blocked should not be
* returned. */
uxReturn &= ~eventEVENT_BITS_CONTROL_BYTES;
}
traceEVENT_GROUP_SYNC_END( xEventGroup, uxBitsToSet, uxBitsToWaitFor, xTimeoutOccurred );
/* Prevent compiler warnings when trace macros are not used. */
( void ) xTimeoutOccurred;
return uxReturn;
}
/*-----------------------------------------------------------*/
这个函数也简单:
唯一注意的是,任务的唤醒是在xEventGroupSync中调用了xEventGroupSetBits函数,当所期望的事件都发生了,则三个任务同步被唤醒。
三、总结
基础不牢地动山摇,底层都会了还怕不会运用嘛,只不过是熟练度的问题,加油加油加油!!!
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)