[ 附件影像 RECORD ]
FreeRTOS学习笔记 · 第十七讲:7.1事件组

FreeRTOS学习笔记 · 第十七讲:7.1事件组

[ SCAN_URL ]
[ 归档时间 ]:2026-04-08 18:12 [ 课题责任人 ]:文止墨 [ 档案分类 ]:嵌入式, 文章, 编程

事件组的功能特点

传统进程通信技术的局限性

队列、信号量等进程间通信技术通常一次只能处理一个事件,无法等待多个事件的发生。如果需要处理多个事件,可能需要分解为多个任务并设置多个信号量。此外,在事件发生时,它们只能解除最高优先级的任务的阻塞状态,无法同时解除多个任务的阻塞状态。它们具有排他性,不能解决需要多个任务同时解除阻塞状态来做出响应的问题。

事件组的优势与特点

事件组是FreeRTOS中一种特殊的进程间通信技术。它允许任务等待一个或多个事件的组合发生。事件组会解除所有等待同一事件的任务的阻塞状态,从而实现事件的广播。事件组支持多个事件触发一个或多个任务的运行,并且支持逻辑与运算和逻辑或运算响应。

有点像 Qt 里的信号与槽函数。不过信号除了传递“信号”外还可以传递数据;而事件组虽不能传递数据,却可以对多个事件进行组合逻辑判断。

事件组的适用场景

事件组适用于任务等待一组事件中的某个事件发生后做出响应的或运算关系场景。它也适用于一组事件都发生后做出响应的与运算关系场景。此外,它适用于将事件广播给多个任务、同时解锁多个任务的场景,以及实现多个任务之间同步的场景。

事件组工作原理

事件组变量与事件位

事件组在FreeRTOS中是一个对象。事件组需要被创建,创建后会有一个内部变量存储事件标志。当参数configUSE_16_BIT_TICKS为0时,这个内部变量是32位的,否则为16位。

在STM32处理器上,该变量是32位的。

一个事件组中的所有事件标志保存在一个EventBits_t类型的变量里,因此一个事件又称为一个事件位。

事件标志只能是0或1,用单独的一个位来存储。如果一个事件位被置为1,表示这个事件发生了;如果为0,表示事件未发生。

32位事件组的存储结构

在32位的事件组变量存储结构中,第24至31位是保留的。第0至23位是事件位。每一个位是一个事件标志,事件发生时将相应的位置1。

因此,32位的事件组最多可以处理24个事件。

多事件触发任务的运行原理

使用事件组进行多个事件触发任务运行时,需要首先设置事件组中的某一个位与某一个事件对应。在检测到事件发生时,通过函数将相应的位置为1,表示事件已发生。可以有一个或多个任务在阻塞状态下等待事件组中的事件发生。当等待的事件条件成立时,所有处于阻塞状态的任务都会被解除阻塞状态,从而实现事件广播功能,使多个任务同时解除阻塞后运行。

事件组相关函数

创建与操作事件组函数

事件组不能在CubeMX里可视化地创建,需要通过编程自行创建。

即便你在任务中调用创建事件组的 API,事件组所消耗的内存也会从系统共享内存中申请。这点对于队列也适用。

创建事件组xEventGroupCreate

可以使用xEventGroupCreate函数以动态分配内存方式创建事件组,该函数无需传递参数,返回所创建事件组的句柄变量。

可以使用xEventGroupCreateStatic函数以静态分配内存方式创建事件组。

删除事件组vEventGroupDelete

C
void vEventGroupDelete( EventGroupHandle_t xEventGroup ) PRIVILEGED_FUNCTION;

该函数删除已经创建的事件组。

PRIVILEGED_FUNCTION 表示 这个函数只能在单片机的“特权模式 (Privileged Mode)”下被调用和执行。

特权模式 与 非特权模式

特权模式 (Privileged Mode):拥有所有的权限,可以访问所有系统控制寄存器(如 NVIC、SysTick),可以任意修改底层硬件状态。系统启动阶段和所有的硬件中断 (ISR) 默认都在特权模式下运行。

非特权模式 / 用户模式 (Unprivileged Mode):权限受到严格限制。不能访问关键系统寄存器,且只能读写被 MPU 明确授权的特定内存区域。

只有在开发汽车、医疗器械系统这种对安全性要求高且需要使用第三方代码是才会用到这些知识,用于防止第三方代码恶意越界、植入病毒。

设置、读取事件组编号

虽然这两个不是创建对象的 API,但它们依然没有 ISR 函数,这是因为它们的设计目标是调试用的。在正常系统业务中不会有调用这两个函数的场景。

可以使用vEventGroupSetNumber函数给事件组设置由用户定义的编号,并使用uxEventGroupGetNumber函数读取事件组编号。

事件位置位xEventGroupSetBits

该函数对应的中断版本为xEventGroupSetBitsFromISR

该函数用于在任务函数中将事件组的某些事件位置1。

该函数需要传入事件组句柄和需要置位的事件位掩码作为参数,并返回置位成功后事件组当前的值。掩码中需要置位的事件位用1表示,其他位用0表示。

置位有一个小妙招:

假如你想置第 0 位和第 7 位,不要直接写 0X81,而是写 (1<<0) | (1<<7),这样代码的可读性更好。

在ISR中对事件组进行置位操作实际上是向定时器守护任务发送消息,将置位操作延后到定时器守护任务中执行。

这句话的意思是在中断里调用的xEventGroupSetBitsFromISR实际上是向系统后台的隐藏队列里发送一条消息,系统接收到消息后会等退出中断后再进行置位操作。

也正是因为这个原因,使用事件组时需要一并把软件定时器打开(configUSE_TIMERS

实际上,该函数的中断函数分为两版:

当开启系统追踪时(configUSE_TRACE_FACILITY):

C
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;
}

不开启时:

C
#define xEventGroupSetBitsFromISR( xEventGroup, uxBitsToSet, pxHigherPriorityTaskWoken ) xTimerPendFunctionCallFromISR( vEventGroupSetBitsCallback, ( void * ) xEventGroup, ( uint32_t ) uxBitsToSet, pxHigherPriorityTaskWoken )

可以看到,开启系统追踪时除了调用底层执行委托的通用函数外,还会调用traceEVENT_GROUP_SET_BITS_FROM_ISR,这是一个钩子函数,它可以像 __weak 函数一样被第三方追踪库(如Tracealyzer)重写。

除此之外,xEventGroupSetBitsFromISR就像信号量释放一样,需要一个指针接收是否有跟高优先级的任务因为此次操作被唤醒。

事件位清零xEventGroupClearBits

该函数对应的中断版本为xEventGroupClearBitsFromISR

该函数用于在任务函数中将事件组的某些事件位清零。

该函数需要传入事件组句柄和需要清零的事件位掩码作为参数,并返回事件位被清零之前的事件组的值。

同样的,它也有两个版本的 ISR 函数。

读取事件组当前值xEventGroupGetBits

该函数对应的中断版本为xEventGroupGetBitsFromISR

C
#define xEventGroupGetBits( xEventGroup ) xEventGroupClearBits( xEventGroup, 0 )

该函数用于读取事件组当前的值。

它实际上是一个宏函数,执行了清除事件位的函数,但传递的事件位掩码是0,即不清除任何事件位,从而返回事件组当前的值。

等待事件组条件成立xEventGroupWaitBits

该函数没有对应的中断版本。

C
EventBits_t xEventGroupWaitBits(
    const EventGroupHandle_t xEventGroup,   // 1. 监听哪个事件组?
    const EventBits_t uxBitsToWaitFor,      // 2. 监听哪几个特定的位?
    const BaseType_t xClearOnExit,          // 3. 走的时候要不要顺手把位清了?
    const BaseType_t xWaitForAllBits,       // 4. 是“凑齐才走(AND)”还是“见一个就走(OR)”?
    TickType_t xTicksToWait                 // 5. 最多等多久(超时时间)?
);

该函数用于使当前任务进入阻塞状态,等待事件组中多个事件位表示的事件成立后再退出阻塞状态。

事件组成立的条件可以是掩码中所有事件位都被置位,即逻辑与运算;也可以是其中的某个事件位被置位,即逻辑或运算。

该函数的参数xClearOnExit如果设置为pdTRUE,则在条件成立退出阻塞状态时会将指定的掩码位清零;如果因为超时退出,则即使设置为pdTRUE也不会清零。

不可以采取“任务醒来后手动清零”来代替这个参数,因为这么做无法保证操作的原子性。

参数xWaitForAllBits用于设置是等待所有位置1的逻辑与运算还是某一个位置1的逻辑或运算。

理想的使用方法是醒来后读取该函数的返回值,来确认自己究竟时因为哪个事件醒来的,还是因为超时了醒来的。如下所示:

C
EventBits_t uxBits;
const EventBits_t uxBitToWaitFor = ( 1 << 0 ) | ( 1 << 1 ); // 等待 bit0 和 bit1

// 开始雷达监听
uxBits = xEventGroupWaitBits(
            xEventGroup,
            uxBitToWaitFor,
            pdTRUE,          // 退出时自动清零
            pdTRUE,          // 必须 bit0 和 bit1 同时满足 (AND)
            pdMS_TO_TICKS( 5000 ) // 最多等 5 秒
         );

// 醒来后,必须进行校验!
if( ( uxBits & uxBitToWaitFor ) == uxBitToWaitFor ) {
    // 完美!两个位都等到了
    System_Init_Complete();
} 
else {
    // 糟糕!5秒超时了,此时 uxBits 记录了死前的状态
    if( ( uxBits & (1<<0) ) == 0 ) {
        printf("Error: Wi-Fi 连不上!\n");
    }
    if( ( uxBits & (1<<1) ) == 0 ) {
        printf("Error: IP 没获取到!\n");
    }
}

通过事件组进行多任务同步

多任务同步原理

多个任务可以分别对应事件组中的多个事件位。这些任务需要在等待某个条件成立之后再开始同步执行各自后面的程序,这个位置被称为同步点。

任务在同步点将各自的事件位置1之后,再等待其他事件位也被置1后才开始运行。

当事件组中表示同步条件的事件位都被置1后,相关任务会被同时解除阻塞状态,从而达到多个任务在某个同步点同步运行的目的。

多任务同步xEventGroupSync

任务的同步操作可以通过先后执行置位函数和等待函数来完成,但这样不是一步操作。

xEventGroupSync函数可以替代这两个函数实现一步操作,专门用于实现多任务之间的同步。该函数需要传入操作的事件组对象、任务要置位的事件位掩码、需要等待的同步条件掩码以及在阻塞状态下等待的节拍数。

更进一步的解释是:让多个任务在执行到某个关键节点时停下来互相等待。每个任务到达时,都会点亮属于自己的“签到灯”,然后死等所有人的“签到灯”都亮起。一旦全员签到完毕,系统会将所有灯熄灭,并同时放行所有任务。


同样的,处于原子性的考虑,你最好不要通过“先置位再等待”这样的操作区替代这个函数。

[ 发起通讯连接 / INITIATE COMM-LINK ]

[SYS]: 您的回传节点(邮箱)将被严格保密。带有 * 的字段为必填项。


> 终止读取并返回主控制台 <