登录后台

页面导航

本文编写于 256 天前,最后修改于 256 天前,其中某些信息可能已经过时。

事件组

嵌入式实时系统必须花费精力在事件响应上。FreeRTOS可以让事件和任务沟通。这些功能的支持对象包括信号量,队列,它们都有下面的一些属性:

  • 它们都允许任务在阻塞状态等待一个单独的事件发生
  • 事件发生时它们会解锁一个单独的任务,解锁的任务是正在等待事件的最高优先级任务

事件组是FreeRTOS任务和事件交流的另外一个功能。不同于队列和信号量:

  • 事件组允许一个任务在阻塞状态等待一个或多个事件的组合发生
  • 一旦事件组事件发生会解锁所有等待这个事件组的任务,而不单单是优先级最高的任务

事件组的这些特性,使它用于多任务同步或向多个任务广播事件的时候很有用。它允许一个任务在阻塞状态等待一个事件集合的发生,也可以允许一个任务阻塞等待多个操作完成。
用事件组也可以让程序减少RAM的使用,通常也可以用几个二进制信号量替代事件组。
事件组功能是一个可选项。要想使用事件组功能,需要在你的项目中包含event_group.c作为项目的一部分。

事件组特性

事件组、事件标志和事件位

一个事件标志是一个布尔值,用作指示一个事件是否发生。事件组是一个事件标志的集合。
一个事件标志只能设置为1或者是0,一个事件标志可以被存放在一个单独的位里面,所有事件的状态标志被存放在一个单独的变量里面;每个事件组里的事件标志表现为变量里面的单独的位,格式为EventBit_t。因此事件标志也叫做事件位。如果EventBit_t变量设置为1,那么表示这个事件已经发生了。如果一个EventBit_t设置为0表示时事件还没有发生。

作为一个例子,如果事件组的值是0x92,那么只有事件位1,4,7被设置,也就是表示只有1,4,7代表的事件发生了。

FreeRTOS中事件组的事件位个数依赖于FreeRTOSConfig.h中的configUSE_16BIT_TICKS的值:

  • 如果configUSE_16BIT_TICKS是1,那么每个事件组包含8个可用的事件位
  • 如果configUSE_16BIT_TICKS是0,那么每个事件组包含24个可用事件位

一个使用事件组的实际例子

FreeRTOS+TCP TCP/IP栈实现提供了一个实际的例子,关于事件组如何用于简化设计,以及减少资源使用。
一个TCP套接字必须响应许多不同的事件。事件包括接收事件,绑定事件,读取事件和关闭事件。任何时间套接字期望事件都依赖于他的状态。比如,如果已经创建套接字,但没有绑定固定的IP地址,那么它就希望收到一个绑定事件,但不希望收到一个读取事件,因为如果没有绑定IP地址就不能读取数据。
FreeRTOS+TCP套接字的状态保存在一个FreeRTOS_Socket_t格式的变量中。这个结构体中有一个事件组,这个事件组为每一个套接字必须处理的事件都分配了一个事件位。FreeRTOS+TCP API调用阻塞等待一个事件或事件组,就是阻塞在这个事件组上。
事件组也有一个禁用位,允许禁用TCP连接,无论这个套接字在等待什么事件。

用事件组管理事件

xEventGroupCreate()函数

xEventGroupCreate()函数就是用于创建一个事件组,返回一个EventGroupHandle_t格式的事件组引用。

// xEventGroupCreate()函数原型
EventGroupHandle_t xEventGroupCreate(void);

/* 返回值: 
 * 如果返回NULL,那么事件组创建失败,因为FreeRTOS没有足够的堆空间分配给事件组数据结构
 * 如果返回一个非NULL的值,表示事件组成功创建。这个返回值就是成功创建的事件组的引用
 */

xEventGroupSetBits()函数

xEventGroupSetBits()函数用于设置一位或多位事件组值,通常用于通知任务,这1位或多位代表的事件已经发生。
注意:不要在中断处理程序中调用xEventGroupSetBits(),应该使用xEventGroupSetBitsFromISR()替代。它是xEventGroupSetBits()的中断安全版本。
释放信号量是一个确定的操作,因为我们已经知道,释放它很可能会有一个任务离开阻塞态。当设置事件组上的位时,我们不知道有多少任务会离开阻塞态,所以设置事件组的位不是一个确定操作。
FreeRTOS的设计和标准实现不允许非确定操作包含进中断处理程序中,或当中断被禁用时。由于这个原因,xEventGroupSetBitsFromISR()不会立即在中断处理程序中设置事件位,而是在FreeRTOS的守护任务中进行。

// xEventGroupSetBits()函数原型
EventBit_t xEventGroupSetBits(EventGroupHandle_t xEventGroup, const EventBit_t uxBitsToSet);
/* 参数
 * xEventGroup: 将被设置位的事件组句柄。这个事件组句柄是调用xEventGroupCreate()函数成功创建事件组后返回的。
 * uxBitsToSet: 将要被设置为1的事件组位。事件组的值是将之前事件组的值和通过uxBitsToSet传递的值按位或后的值。
 *              作为一个例子,设置uxBitsToSet为0x04(二进制0100),这会导致事件组第三位被设置为1,如果它没有被设置。其他所有位的值不会改变。
 * 返回值: 返回调用xEventGroupSetBits()返回时的值。注意返回值不必定设置了uxBitsToSet位,因为这个位可能已经被其他任务清除。
 */
// xEventGroupSetBitsFromISR()函数原型
BaseType_t xEventGroupSetBitsFromISR(EventGroupHandle_t xEventGroup, const EventBit_t uxBitsToSet, BaseType_t *pxHigherPriorityTaskWoken);
/* 参数
 * xEventGroup: 将要设置事件位的事件组句柄。这是调用xEventGroupCreate()函数成功创建事件组后返回的事件组句柄
 * uxBitsToSet:要被设置为1的时间组的位。事件组的值是原来事件组的值和uxBitsToSet的值进行按位或后的值。
                作为例子: uxBitsToSet为0x05(二进制0101),会设置事件组中的第0和3位为1。其他的位保持不变。
 * pxHigherPriorityTaskWoken: xEventGroupSetBitsFromISR()不会立即在中断处理程序中设置事件位,而是通过发送一个命令给时间命令队列,让守护任务进行处理。如果守护在等待时间命令队列数据,那么就会唤醒守护任务。如果守护任务优先级更高,那么pxHigherPriorityTaskWoken就会设置为1。
  * 如果xEventGroupSetBitsFromISR()将这个值设置为pdTRUE,那么中断退出前就应当进行一个上下文切换。这样可以确保中断返回后立即进入守护任务,因为守护任务是最高优先级就绪任务。
  * 返回值:
  * 可能有两个返回值
  * pdPASS: 数据成功发送到时间命令队列,返回pdPASS
  * pdFAILE: 如果"设置位"命令不能写入到时间命令队列,因为它满了,就会返回pdFAILE。
  */

xEventGroupWaitBits()函数

xEventGroupWaitBits()用于读取事件组的值,可以选择等待1位或多位事件位被设置

// xEventGroupWaitBits()原型
EventBits_t xEventGroupWaitBits(const EventGroupHandle_t xWventGroup, const EventBits_t uxBitsToWaitFor, const BaseType_t xClearOnExit, const BaseType_t xWaitForAllBits, TickType_t xTicksToWait);

调度器用于决定一个任务是否进入阻塞,或离开阻塞的条件叫做"解锁条件"。解锁条件是由uxBitsToWaitFor和xWaitForAllBits两个变量共同决定的:

  • uxBitsToWaitFor: 决定事件组的那些事件位会用于测试
  • xWaitForAllBits决定是否用一个按位或或按位与测试
    如果解锁条件在调用xEventGroupWaitBits()函数时已经达成,任务就不会进入阻塞状态。
当前事件组值 uxBitsToWaitFor xWaitForAllBits 导致结果
0000 0101 pdFAILE 调用任务进入阻塞,因为事件组第0,2位都没有设置。当事件组的第0位或第2位被设置时离开阻塞。
0100 0110 pdFAILE 调用任务不会进入阻塞,因为xWaitForAllBits是pdFAILE,uxBitsToWaitFor两位中的一位已经被设置。
0100 0110 pdTRUE 调用任务会进入阻塞,因为xWaitForAllBits是pdTRUE,事件组中uxBitsToWaitFor指定的两位只有其中一位被设置。只有事件组中指定的2位都被设置后才能离开阻塞状态。

调用任务用uxBitsToWaitFor参数指定要等待的位,可能调用任务需要在解锁条件达成后清除那些位。事件位可以使用xEventGroupClearBits()函数清除位,但用这个函数进行清除事件位操作会导致程序代码在下面情况下进入竞争关系:

  • 同时有多个任务使用相同的事件组
  • 事件组有不同的任务设置位,或被中断处理程序设置
    xClearOnExit()参数就是用于避免出现竞争关系的。如果xClearOnExit设置为pdTRUE,那么就会以原子操作的方式测试和清除想要测试的事件位,也就是测试和清除操作不会被中断。
# xEventGroupWaitBits()参数和返回值
# xEventGroup: 要被读取事件位的事件组句柄。这个事件组是调用xEventGroupCreate()后成功创建事件组所返回的事件组句柄。
# uxBitsToWaitFor: 指定事件组需要测试的事件位的位掩码。
                   比如,如果调用任务想要等待事件位0与或事件位2被设置,那么uxBitsToWaitFor就设置成0x05(二进制0101)
# xClearOnExit: 如果调用任务解锁条件已经发生,而且xClearOnExit设置为pdTRUE,那么由uxBitsToWaitFor指定的事件位在任务退出xEventGroupWaitBits()函数之前会清理这些位为0。
                如果xClearOnExit设置为pdFAILE,那么调用任务退出xEventGroupWaitBits()函数前不会改变事件组的值。
# xWaitForAllBits: 用于指定事件组要测试的事件位。xWaitForAllBits用于设定,当事件组的uxBitsToWaitFor掩码中的1位或多位被设置时,调用任务能否解除阻塞,或者只有所有由uxBitsToWaitFor掩码指定位全部被设置才能离开阻塞态。
                   如果xWaitForAllBits设置为pdFAILE,那么只要uxBitsToWaitFor掩码指定的1位或多于1位被设置为1,调用任务就会离开阻塞状态。
                   如果xWaitForAllBits设置为pdTRUE,那么只有uxBitsToWaitFor指定掩码的所有位都设置为1时,调用任务才可以解除阻塞态
# xTicksToWait: 调用任务可以在阻塞态,等待解锁条件的最大时间计数。
                如果xTicksToWait为0,或解锁条件已经达成,xEventGroupWaitBits()会立即返回。
                这个阻塞时间是用tick周期指定的,因此绝对时钟时间依赖于tick周期。可以用pdMS_TO_TICKS宏将绝对时钟时间转换为一个tick计数。
                将xTicksToWait设置为portMAX_DELAY,会导致任务无限等待,但需要在FreeRTOSConfig.h中设置INCLUDE_vTaskSuspend为1。
# 返回值: 如果是因为调用任务解锁条件任务达成而返回,那么返回值是事件组解锁条件达成时的值(如果xClearOnExit为pdTRUE,这个值是自动清除某些位之前的值)。所以返回值总是满足解锁条件。
          如果是因为由xTicksToWait参数指定的超时时间到期而返回,那么返回值就是事件组超时时间到期时的值。因此这里的返回值总是不满足解锁条件。
博主已关闭本页面的评论功能