FreeRTOS事件组与任务通知源码详解
一、事件组与任务通知概述
事件组和任务通知是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中的通知字段实现高效的点对点通信,提供五种通知动作满足不同场景需求。
实际项目中,应根据具体需求选择合适的通信机制:对于需要广播或多条件等待的场景优先使用事件组,对于性能敏感的简单同步场景优先使用任务通知。
更多推荐



所有评论(0)