事件组
引言与范围
人们已经注意到,实时嵌入式系统必须对事件采取行动。前几章描述了FreeRTOS允许事件与任务通信的特性。这类特性的例子包括信号量和队列,它们都具有以下属性:
它们允许任务在阻塞状态中等待单个事件的发生。
当事件发生时,它们解除阻塞单个任务——解除阻塞的任务是等待事件的最高优先级的任务。
事件组是FreeRTOS的另一个特性,它允许事件与任务通信。与队列和信号量不同:
事件组允许任务在阻塞状态下等待多个事件的组合发生。
事件组在事件发生时解除等待同一事件或事件组合的所有任务的阻塞。
事件组的这些独特的性质使其有用的多个任务同步广播事件的多个任务,允许任务阻塞状态等待任何发生的一系列事件之一,并允许一个任务在阻塞状态等多个操作完成。
事件组还提供了减少应用程序使用的RAM的机会,因为通常可以用单个事件组替换许多二进制信号量。
事件组功能是可选的。要包含事件组功能,构建FreeRTOS源文件event_groups.c
作为项目的一部分。
范围
本章旨在让读者更好地理解:
事件组的实际用途。
事件组相对于其他FreeRTOS特性的优点和缺点。
如何设置事件组中的位。
如何在阻塞状态下等待事件组中的位被设置。
如何使用事件组同步一组任务。
事件组的特征
事件组、事件标志和事件位
事件 "标志 "是一个布尔值(1或0),用于指示一个事件是否发生。事件 "组 "是一组事件标志。
事件标志只能为1或0,允许事件标志的状态存储在单个位中,事件组中所有事件标志的状态存储在单个变量中;事件组中每个事件标志的状态由类型为EventBits_t的变量中的单个位表示。因此,事件标志也被称为事件“位”。如果EventBits_t变量中的一位被设为1,则该位表示的事件已经发生。如果在EventBits_t变量中一个位被设置为0。那么由该位表示的事件没有发生。
图71显示了如何将单个事件标志映射到类型变量中的单个位EventBits_t。
图71 EventBits_t类型变量中的事件标志到位号的映射
例如,如果一个事件组的值是0x92(二进制1001 0010),那么只有事件位1、4和7被设置,因此只有由1、4和7表示的事件发生。图72显示了EventBits_t类型的变量,设置了事件位1、4和7,并清除了所有其他事件位,使事件组的值为0x92。
图72 一个事件组,其中只设置了1、4和7位,并且清除了所有其他事件标志,使事件组的值为0x92
由应用程序编写器为事件组中的单个位分配意义。例如,应用程序编写器可以创建一个事件组,然后:
将事件组中的第0位定义为“已从网络接收到消息”。
将事件组中的第1位定义为“消息已准备好发送到网络上”。
将事件组中的第2位定义为“终止当前网络连接”。
关于EventBits_t数据类型的更多信息
事件组中的事件位数依赖于FreeRTOSConfig.h
中的configUSE_16_BIT_ TICKS
编译时配置常量:
如果
configUSE_16_BIT_TICKS
为1,则每个事件组包含8个可用的事件位。如果
configUSE_16_BIT_TICKS
为0,则每个事件组包含24个可用的事件位。
FreeRTOSConfig.h:configUSE_16_BIT_TICKS配置用于保存RTOS滴答数的类型,因此似乎与事件组特性无关。它对EventBits_t类型的影响是FreeRTOS内部实现的结果,当FreeRTOS在一个能够比32位tvpes更有效地处理16位类型的架构上执行时,configUSE_16_BIT_TICKS应该只设置为1。
多任务访问
事件组是具有自身权限的对象,可以被任何知道其存在的任务或ISR访问。任意数量的任务可以在同一事件组中设置位,任意数量的任务可以从同一事件组中读取位。
一个使用事件组的实例
FreeRTOS+TCP TCP/IP栈的实现提供了一个实例,说明如何使用事件组同时简化设计,并最小化资源使用。
一个TCP套接字必须响应许多不同的事件。事件的例子包括接受事件、绑定事件、读取事件和关闭事件。套接字在任何给定时间所期望的事件取决于套接字的状态。例如,如果套接字已经创建,但还没有绑定到地址,那么它可以预期接收绑定事件,但不会预期接收读事件(如果没有地址,它就无法读取数据)。
一个FreeRTOS+TCP套接字的状态保存在一个叫做FreeRTOS_Socket_t的结构中。该结构包含一个事件组,该事件组为套接字必须处理的每个事件定义一个事件位。FreeRTOS+TCP API调用这个阻塞来等待一个事件或一组事件,只是在事件组上阻塞。
事件组还包含一个中止位,允许TCP连接被终止,不管套接字当时在等待哪个事件。
使用事件组的事件管理
xEventGroupCreate() API函数
FreeRTOS V9.0.0还包括xEventGroupCreateStatic()
函数,该函数在编译时分配静态创建事件组所需的内存:在使用事件组之前,必须显式地创建它。
使用EventGroupHandle_t类型的变量引用事件组。API函数xEventGroupCreate()
用于创建事件组,并返回一个EventGroupHandle_t来引用它创建的事件组。
清单 132. xEventGroupCreate()
API函数原型
表 42. xEventGroupCreate()
的返回值
xEventGroupSetBits() API函数
xEventGroupSetBits()
API函数在事件组中设置一个或多个位,通常用于通知任务,由被设置的位表示的事件已经发生。
注意:永远不要在中断服务程序中调用
xEventGroupSetBits()
。应该使用中断安全版本xEventGroupSetBitsFromISR()
来代替它。
清单133. xEventGroupSetBits()
API函数原型
表 43. xEventGroupSetBits()
参数和返回值
xEventGroupSetBitsFromlSR() API函数
xEventGroupSetBitsFromlSR()
是xEventGroupSetBits()
的中断安全版本。
给出信号量是一个确定性操作,因为预先知道给出信号量最多会导致一个任务离开阻塞状态。当在事件组中设置位时,不知道有多少任务将离开阻塞状态,因此在事件组中设置位不是确定性操作。
FreeRTOS设计和实现标准不允许不确定的操作在一个中断服务程序执行,或者当中断禁用这个原因,xEventGroupSetBitsFromlSR()
不直接设置事件比特在中断服务程序,而是延缓RTOS守护进程的行动任务。
清单 134. xEventGroupSetBitsFromISR()
API函数原型
表 44. xEventGroupSetBitsFromISR()
参数和返回值
xEventGroupWaitBits() API函数
xEventGroupWaitBits()
APl函数允许任务读取事件组的值,并且可以选择在阻塞状态等待事件组中的一个或多个事件位被设置,如果这些事件位还没有设置。
清单135. xEventGroupWaitBits()
API函数原型
调度程序用来确定任务是否进入阻塞状态,以及任务何时离开阻塞状态的条件称为“解封条件”。解封条件由uxBitsToWaitFor
和xWaitForAllBits
参数值的组合指定:
uxBitsToWaitFor
指定要测试事件组中的哪个事件位xWaitForAllBits
指定是使用位数OR测试,还是位数AND测试。
如果在调用xEventGroupWaitBits()
时,任务的解锁条件得到满足,那么该任务将不会进入阻塞状态。
表45提供了导致任务进入阻塞状态或退出阻塞状态的条件示例。表45只显示了事件组和uxBitsToWaitFor
值中最不重要的四个二进制位——这两个值的其他位被假定为零。
表45 uxBitsToWaitFor
和xWaitForAllBits
参数的影响
调用任务使用uxBitsToWaitFor
参数指定要测试的位,调用任务很可能需要在满足解封条件后将这些位清除为零。事件位可以使用xEventGroupClearBits()
API函数来清除,但如果使用该函数手动清除事件位将导致应用程序代码中的竞争条件:
使用同一事件组的任务不止一个。
位由不同的任务或中断服务程序在事件组中设置。
提供了xClearOnExit
参数以避免这些潜在的竞争条件。如果xClearOnExit
设置为pdTRUE,那么对调用任务来说,事件位的测试和清除似乎是一个原子操作(不能被其他任务或中断中断)。
表46 xEventGroupWaitBits()
参数和返回值
示例22. 实验事件组
这个例子演示了如何进行:
创建事件组。
从中断服务程序中设置事件组中的位。
从任务中设置事件组中的位。
阻塞事件组。
xEventGroupWaitBits()
xWaitForAllBits
参数的效果是通过首先执行xWaitForAllBits
设置为pdFALSE的示例,然后执行xWaitForAllBits
设置为pdTRUE的示例来演示的。
事件位0和事件位1由一个任务设置。事件位2是由中断服务程序设置的。例程设置。这三个位通过#define语句被赋予描述性的名字,如清单136所示。
清单136. 示例22中使用的事件位定义
清单137显示了设置事件位0和事件位1的任务的实现。它位于一个循环中,反复设置一个位,然后再设置另一个位,每次调用xEventGroupSetBits()
之间有200毫秒的延迟。在设置每个位之前打印一个字符串,以允许在控制台中看到执行序列。
清单137. 例22中设置事件组中两个比特的任务
清单138显示了在事件组中设置第2位的中断服务例程的实现。同样,在设置位之前打印一个字符串,以允许在控制台中看到执行序列。但是,在这种情况下,因为控制台输出不应该直接在中断服务例程中执行,所以使用xTimerPendFunctionCallFromISR()
在RTOS守护进程任务的上下文中执行输出。
与前面的示例一样,中断服务程序是由一个简单的周期性任务触发的,该任务强制软件中断。在本例中,中断每500毫秒产生一次。
清单138. 例22中设置事件组中第2位的ISR
清单139显示了调用xEventGroupWaitBits()
来阻塞事件组的任务的实现。任务为事件组中设置的每个位打印一个字符串。
xEventGroupWaitBits()
xClearOnExit
参数被设置为pdTRUE,因此导致调用xEventGroupWaitBits()
返回的事件位或位将在xEventGroupWaitBits()
返回之前被自动清除。
清单139. 例22中等待事件位被设置而阻塞的任务
main()
函数在启动调度程序之前创建事件组和任务。有关它的实现,请参见清单140。从事件组中进行读操作的任务优先级高于向事件组中进行写操作的任务优先级,确保每次满足读任务的解阻塞条件时,读任务都会抢占写任务。
清单140. 创建例22中的事件组和任务
在执行示例22时,将xEventGroupWaitBits()
xWaitForAllBits参数设置为pdFALSE,所产生的输出如图73所示。在图73中,可以看到,由于调用xEventGroupWaitBits()
中的xWaitForAllBits参数被设置为pdFALSE,从事件组中读取的任务将离开阻塞状态,并在每次设置任何事件位时立即执行。
图73 示例22执行xWaitForAllBits
设置为pdFALSE时产生的输出
在执行示例22时,将xEventGroupWaitBits()
xWaitForAllBits参数设置为pdTRUE,所产生的输出如图74所示。在图74中可以看到,因为xWaitForAllBits
参数被设置为pdTRUE,从事件组中读取的任务只有在设置了所有三个事件位之后才会离开状态阻塞。
图74 将xWaitForAlBits
设置为pdTRUE执行示例22时产生的输出
使用事件组进行任务同步
有时,应用程序的设计需要两个或多个任务来彼此同步。例如,考虑这样一种设计:任务A接收一个事件,然后将该事件所需的一些处理委托给另外三个任务:任务B、任务C和任务D。如果任务A无法接收到另一个事件,直到任务B、C和D都完成了对前一个事件的处理,那么这四个任务就需要彼此同步。每个任务的同步点将在该任务完成其处理之后,并且不能继续进行,直到其他每个任务都完成了同样的处理。只有当所有四个任务都到达它们的同步点时,任务A才能接收到另一个事件。
需要这种类型的任务同步的一个不那么抽象的例子可以在一个FreeRTOS+TCP演示项目中找到。演示在两个任务之间共享一个TCP套接字;一个任务向套接字发送数据,另一个任务从同一个套接字1接收数据。在确定其他任务不会再次尝试访问该套接字之前,关闭TCP套接字对任何一个任务来说都是不安全的。如果两个任务中有一个希望关闭套接字,那么它必须通知另一个任务它的意图,然后等待另一个任务停止使用该套接字,然后再继续。清单140所示的伪代码演示了将数据发送到希望关闭套接字的任务的场景。
清单140所展示的情景是微不足道的,因为只有两个任务需要互相同步,但很容易看出,如果有其他任务在执行同步,该方案会变得更复杂,需要更多的任务加入同步,如果其他的任务在执行依赖于套接字的处理的话。处理依赖于套接字被打开。
套接字:在编写本文的时候,这是在任务之间共享单个FreeRTOS+TCP套接字的唯一方法。
清单141.两个任务的伪代码,它们彼此同步以确保在套接字关闭之前,任何一个任务都不再使用共享的TCP套接字
事件组可用于创建同步点:
必须参与同步的每个任务在事件组中被分配一个唯一的事件位。
每个任务在到达同步点时设置自己的事件位。
设置了自己的事件位后,每个任务阻塞在事件组上,等待代表所有其他同步任务的事件位也被设置。
但是,xEventGroupSetBits()
和xEventGroupWaitBits()
API函数不能在此场景中使用。如果使用它们,那么位的设置(指示一个任务已经到达它的同步点)和位的测试(确定其他同步任务是否已经到达它们的同步点)将作为两个独立的操作执行。为了了解为什么这是一个问题,考虑一个场景,任务A任务B和任务C尝试使用事件组进行同步:
任务A和任务B已经到达同步点,事件位已经在事件组中设置,处于阻塞状态,等待任务C的事件位也被设置。
任务C到达同步点后,使用
xEventGroupSetBits()
设置其在事件组中的位。一旦任务C的位被设置,任务A和任务B就会离开阻塞状态,并清除所有三个事件位。任务C然后调用
xEventGroupWaitBits()
等待这三个事件比特成为集,但到那时,所有三个事件已经被清除,任务和任务B离开各自的同步点,所以同步失败了。
要成功地使用事件组创建同步点,事件位的设置和后续事件位的测试必须作为一个单独的不可中断操作执行。为此提供了xEventGroupSync()
API函数。
xEventGroupSync() API函数
提供xEventGroupSync()
允许两个或多个任务使用事件组彼此同步。该功能允许任务在一个事件组中设置一个或多个事件位,然后等待在同一事件组中设置多个事件位的组合,作为一个单独的不可中断操作。
xEventGroupSync()
uxBitsTolaitFor参数指定调用任务的解除阻塞条件。如果xEventGroupSync()
返回是因为满足了不阻塞条件,uxBitsTolaitFor
指定的事件位将在xEventGroupSync()
返回之前被清除回零。
清单142. xEventGroupSync()
API函数原型
表47. xEventGroupSync()
参数和返回值
示例23. 同步任务
示例23使用xEventGroupSync()
同步一个任务实现的三个实例。任务参数用于将任务调用xEventGroupSync()
时设置的事件位传递给每个实例。
任务在调用xEventGroupsync()
之前打印一条消息,在调用xEventGroupsync()
返回之后再次打印一条消息。每条消息都包含一个时间戳。这允许在生成的输出中观察执行顺序。伪随机延迟用于防止所有任务同时到达同步点。
有关任务的实现,请参见清单143。
清单143. 示例23中使用的任务的实现
main()
函数创建事件组,创建所有三个任务,然后启动调度程序。有关它的实现,请参见清单144。
清单144. 示例23中使用的main()
函数
执行示例23时产生的输出如图75所示。可以看到,即使每个任务在不同的(伪随机)时间到达同步点,每个任务在同一时间退出同步点1(这是最后一个任务到达同步点的时间)。
同步点:图75显示了在FreeRTOS Windows端口中运行的示例,它不提供真正的实时行为(特别是当使用Windows系统调用打印到控制台时),因此会显示一些时间变化。
图75执行示例23时产生的输出
Last updated