事件组

引言与范围

人们已经注意到,实时嵌入式系统必须对事件采取行动。前几章描述了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来引用它创建的事件组。

EventGroupHandle_t xEventGroupCreate( void );

清单 132. xEventGroupCreate() API函数原型

表 42. xEventGroupCreate() 的返回值

xEventGroupSetBits() API函数

xEventGroupSetBits() API函数在事件组中设置一个或多个位,通常用于通知任务,由被设置的位表示的事件已经发生。

注意:永远不要在中断服务程序中调用xEventGroupSetBits()。应该使用中断安全版本xEventGroupSetBitsFromISR()来代替它。

EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
								const EventBits_t uxBitsToSet );

清单133. xEventGroupSetBits() API函数原型

表 43. xEventGroupSetBits()参数和返回值

xEventGroupSetBitsFromlSR() API函数

xEventGroupSetBitsFromlSR()xEventGroupSetBits()的中断安全版本。

给出信号量是一个确定性操作,因为预先知道给出信号量最多会导致一个任务离开阻塞状态。当在事件组中设置位时,不知道有多少任务将离开阻塞状态,因此在事件组中设置位不是确定性操作。

FreeRTOS设计和实现标准不允许不确定的操作在一个中断服务程序执行,或者当中断禁用这个原因,xEventGroupSetBitsFromlSR()不直接设置事件比特在中断服务程序,而是延缓RTOS守护进程的行动任务。

BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
 									  const EventBits_t uxBitsToSet,
									  BaseType_t *pxHigherPriorityTaskWoken );

清单 134. xEventGroupSetBitsFromISR() API函数原型

表 44. xEventGroupSetBitsFromISR()参数和返回值

xEventGroupWaitBits() API函数

xEventGroupWaitBits() APl函数允许任务读取事件组的值,并且可以选择在阻塞状态等待事件组中的一个或多个事件位被设置,如果这些事件位还没有设置。

EventBits_t xEventGroupWaitBits( const EventGroupHandle_t xEventGroup,
 								 const EventBits_t uxBitsToWaitFor,
 								 const BaseType_t xClearOnExit,
 								 const BaseType_t xWaitForAllBits,
 								 TickType_t xTicksToWait );

清单135. xEventGroupWaitBits() API函数原型

调度程序用来确定任务是否进入阻塞状态,以及任务何时离开阻塞状态的条件称为“解封条件”。解封条件由uxBitsToWaitForxWaitForAllBits参数值的组合指定:

  • uxBitsToWaitFor指定要测试事件组中的哪个事件位

  • xWaitForAllBits指定是使用位数OR测试,还是位数AND测试。

如果在调用xEventGroupWaitBits()时,任务的解锁条件得到满足,那么该任务将不会进入阻塞状态。

表45提供了导致任务进入阻塞状态或退出阻塞状态的条件示例。表45只显示了事件组和uxBitsToWaitFor值中最不重要的四个二进制位——这两个值的其他位被假定为零。

表45 uxBitsToWaitForxWaitForAllBits参数的影响

调用任务使用uxBitsToWaitFor参数指定要测试的位,调用任务很可能需要在满足解封条件后将这些位清除为零。事件位可以使用xEventGroupClearBits() API函数来清除,但如果使用该函数手动清除事件位将导致应用程序代码中的竞争条件:

  • 使用同一事件组的任务不止一个。

  • 位由不同的任务或中断服务程序在事件组中设置。

提供了xClearOnExit参数以避免这些潜在的竞争条件。如果xClearOnExit设置为pdTRUE,那么对调用任务来说,事件位的测试和清除似乎是一个原子操作(不能被其他任务或中断中断)。

表46 xEventGroupWaitBits()参数和返回值

示例22. 实验事件组

这个例子演示了如何进行:

  • 创建事件组。

  • 从中断服务程序中设置事件组中的位。

  • 从任务中设置事件组中的位。

  • 阻塞事件组。

xEventGroupWaitBits() xWaitForAllBits参数的效果是通过首先执行xWaitForAllBits设置为pdFALSE的示例,然后执行xWaitForAllBits设置为pdTRUE的示例来演示的。

事件位0和事件位1由一个任务设置。事件位2是由中断服务程序设置的。例程设置。这三个位通过#define语句被赋予描述性的名字,如清单136所示。

/*"事件组中事件位的定义。 */
#define mainFIRST_TASK_BIT  ( 1UL << 0UL ) /* 事件位0,由任务设置。 */
#define mainSECOND_TASK_BIT ( 1UL << 1UL ) /* 事件位1,由任务设置。 */
#define mainISR_BIT         ( 1UL << 2UL ) /* 事件位2,由ISR设置。 */

清单136. 示例22中使用的事件位定义

清单137显示了设置事件位0和事件位1的任务的实现。它位于一个循环中,反复设置一个位,然后再设置另一个位,每次调用xEventGroupSetBits()之间有200毫秒的延迟。在设置每个位之前打印一个字符串,以允许在控制台中看到执行序列。

static void vEventBitSettingTask( void *pvParameters )
{
const TickType_t xDelay200ms = pdMS_TO_TICKS( 200UL ), xDontBlock = 0;
	for( ;; )
	{
 		/*在开始下一个循环之前稍作延迟。 */
 		vTaskDelay( xDelay200ms );
        
 		/*输出一条消息,表示任务即将设置事件位0,然后设置事件位0。*/
 		vPrintString( "Bit setting task -\t about to set bit 0.\r\n" );
 		xEventGroupSetBits( xEventGroup, mainFIRST_TASK_BIT );
        
 		/*在设置其他位之前稍作延迟。 */
 		vTaskDelay( xDelay200ms );
        
 		/*输出一个消息,说事件位1即将被任务设置,然后设置事件位1。*/
 		vPrintString( "Bit setting task -\t about to set bit 1.\r\n" );
 		xEventGroupSetBits( xEventGroup, mainSECOND_TASK_BIT );
 	} 
}

清单137. 例22中设置事件组中两个比特的任务

清单138显示了在事件组中设置第2位的中断服务例程的实现。同样,在设置位之前打印一个字符串,以允许在控制台中看到执行序列。但是,在这种情况下,因为控制台输出不应该直接在中断服务例程中执行,所以使用xTimerPendFunctionCallFromISR()在RTOS守护进程任务的上下文中执行输出。

与前面的示例一样,中断服务程序是由一个简单的周期性任务触发的,该任务强制软件中断。在本例中,中断每500毫秒产生一次。

static uint32_t ulEventBitSettingISR( void )
{
/* 该字符串没有在中断服务程序中打印,而是被发送到RTOS守护进程任务中打印。因此,它被声明为static,以确保编译器不会在ISR的堆栈上分配字符串,因为当从守护进程任务打印字符串时,ISR的堆栈帧将不存在。*/
static const char *pcString = "Bit setting ISR -\t about to set bit 2.\r\n";
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    /*输出一个消息说第2位即将被设置。消息不能从ISR打印,因此通过挂起函数调用在RTOS守护进程任务上下文中运行,将实际输出推迟到RTOS守护进程任务。*/
 	xTimerPendFunctionCallFromISR( vPrintStringFromDaemonTask, 
 				( void * ) pcString, 
 				0, 
				&xHigherPriorityTaskWoken );
    
    /* 在事件组中设置位2。 */
 	xEventGroupSetBitsFromISR( xEventGroup, mainISR_BIT, &xHigherPriorityTaskWoken );
    
    /* xTimerPendFunctionCallFromISR()和xEventGroupsetBitsFromISR()都写定时器命令队列,并使用相同的xHigherPriorityraskwoken变量。如果写入定时器命令队列导致RTOS守护进程任务,并且RTOS守护进程任务的优先级较高,则返回Blocked状态比当前正在执行的任务(此中断的任务)的优先级高然后xhigherprioritytaskkoken将被设置为pdTRUE。
    
    xHigherPriorityTaskwoken用作portYIELD FROM ISR()的参数。如果xHigherPriorityraskwoken等于pdTRUE,那么从ISR()调用portYIEID将请求一个上下文切换。如果xHigherPriorityraskwoken仍然是pdFALSE,那么从ISR()调用portYIELD将没有效果。
    
    由windows端口使用的portYIELD_FROM_ISR()的实现包括一个返回语句,这就是为什么这个函数没有显式返回值。*/
	portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

清单138. 例22中设置事件组中第2位的ISR

清单139显示了调用xEventGroupWaitBits()来阻塞事件组的任务的实现。任务为事件组中设置的每个位打印一个字符串。

xEventGroupWaitBits() xClearOnExit参数被设置为pdTRUE,因此导致调用xEventGroupWaitBits()返回的事件位或位将在xEventGroupWaitBits()返回之前被自动清除。

static void vEventBitReadingTask( void *pvParameters )
{
EventBits_t xEventGroupValue;
const EventBits_t xBitsToWaitFor = ( mainFIRST_TASK_BIT  | 
 				     mainSECOND_TASK_BIT | 
 				     mainISR_BIT );
 	for( ;; )
 	{
        /* 阻塞以等待事件位在事件组中被设置。*/
 		xEventGroupValue = xEventGroupWaitBits( /* 要读取的事件组。 */
 							xEventGroup,
 							/* 位测试。 */
 							xBitsToWaitFor, 

 							/*如果满足解封条件,则在退出时清除位。*/
 							pdTRUE,    

 							/*不要等待所有的位。对于第二次执行,该参数被设置为pdTRUE。 */
 							pdFALSE,   

 							/* 不要超时。 */
 							portMAX_DELAY );

 		/*为设置的每个位打印一条消息。 */
 		if( ( xEventGroupValue & mainFIRST_TASK_BIT ) != 0 )
 		{
 			vPrintString( "Bit reading task -\t Event bit 0 was set\r\n" );
 		}
 		if( ( xEventGroupValue & mainSECOND_TASK_BIT ) != 0 )
 		{
 			vPrintString( "Bit reading task -\t Event bit 1 was set\r\n" );
 		}
 		if( ( xEventGroupValue & mainISR_BIT ) != 0 )
 		{
 			vPrintString( "Bit reading task -\t Event bit 2 was set\r\n" );
 		}
 	} 
}

清单139. 例22中等待事件位被设置而阻塞的任务

main()函数在启动调度程序之前创建事件组和任务。有关它的实现,请参见清单140。从事件组中进行读操作的任务优先级高于向事件组中进行写操作的任务优先级,确保每次满足读任务的解阻塞条件时,读任务都会抢占写任务。

int main(void)
{
    /* 在使用事件组之前,必须先创建事件组。 */
    xEventGroup = xEventGroupCreate();
    
    /* 创建在事件组中设置事件位的任务。 */
    xTaskCreate(vEventBitSettingTask, "Bit Setter", 1000, NULL, 1, NULL);
    
    /* 创建等待事件组中事件位设置的任务。*/
    xTaskCreate(vEventBitReadingTask, "Bit Reader", 1000, NULL, 2, NULL);
    
    /* 创建用于周期性产生软件中断的任务。 */
    xTaskCreate(vInterruptGenerator, "Int Gen", 1000, NULL, 3, NULL);
    
    /*安装软件中断的处理程序。完成此操作所需的语法取决于所使用的FreeRTOS端口。这里显示的语法只能在FreeRros Windows端口中使用,在该端口中这种中断只能被模拟。 */
    vPortSetInterruptHandler(mainINTERRUPT_NUMBER, ulEventBitSettingISR);
    
    /* 启动调度程序,使创建的任务开始执行。 */
    vTaskStartScheduler();
    
    /* 启动调度程序,使已创建的任务开始执行。 */
    for (;;);
    return 0;
}

清单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套接字的唯一方法。

void SocketTxTask(void *pvParameters)
{
xSocket_t xSocket;
uint32_t ulTxCount = 0UL;
    
    for (;;)
    {
       /*创建一个新的socket。这个任务将发送到这个套接字,而另一个任务将从这个套接字接收。*/
        xSocket = FreeRTOS_socket(...);
        
        /* 连接套接字。 */
        FreeRTOS_connect(xSocket, ...);

        /* 使用队列将套接字发送到接收数据的任务。 */
        xQueueSend(xSocketPassingQueue, &xSocket, portMAX_DELAY);
        
        /* 在关闭套接字之前向套接字发送1000条消息。*/
        for (ulTxCount = 0; ulTxCount < 1000; ulTxCount++)
        {
            if (FreeRTOS_send(xSocket, ...) < 0)
            {
                /* 意外错误-退出循环,之后套接字将被关闭。 */
                break;
            }
        }
        
        /* 让Rx任务知道Tx任务想要关闭套接字。 */
        TxTaskWantsToCloseSocket();
        
        /* 这是Tx任务的同步点。Tx任务在这里等待Rx任务到达它的同步点。Rx任务只有在不再使用套接字时才会到达它的同步点,并且可以安全地关闭套接字。*/
        xEventGroupSync(...);
        
        /* 这两个任务都没有使用套接字。关闭连接,然后关闭套接字。*/
        FreeRTOS_shutdown(xSocket, ...);
        WaitForSocketToDisconnect();
        FreeRTOS_closesocket(xSocket);
    }
}
/*-----------------------------------------------------------*/

void SocketRxTask(void *pvParameters)
{
xSocket_t xSocket;
    
    for (;;)
    {
        /* 等待接收由Tx任务创建并连接的套接字。 */
        xQueueReceive(xSocketPassingQueue, &xSocket, portMAX_DELAY);
        
        /* 继续接收套接字,直到Tx任务想要关闭套接字。 */
        while (TxTaskWantsToCloseSocket() == pdFALSE)
        {
            /* 接收然后处理数据。 */
            FreeRTOS_recv(xSocket, ...);
            ProcessReceivedData();
        }
        
        /*这是Rx任务的同步点——它只在不再使用套接字时到达这里,因此Tx任务关闭套接字是安全的。*/
        xEventGroupSync(...);
    }
}

清单141.两个任务的伪代码,它们彼此同步以确保在套接字关闭之前,任何一个任务都不再使用共享的TCP套接字

事件组可用于创建同步点:

  • 必须参与同步的每个任务在事件组中被分配一个唯一的事件位。

  • 每个任务在到达同步点时设置自己的事件位。

  • 设置了自己的事件位后,每个任务阻塞在事件组上,等待代表所有其他同步任务的事件位也被设置。

但是,xEventGroupSetBits()xEventGroupWaitBits() API函数不能在此场景中使用。如果使用它们,那么位的设置(指示一个任务已经到达它的同步点)和位的测试(确定其他同步任务是否已经到达它们的同步点)将作为两个独立的操作执行。为了了解为什么这是一个问题,考虑一个场景,任务A任务B和任务C尝试使用事件组进行同步:

  1. 任务A和任务B已经到达同步点,事件位已经在事件组中设置,处于阻塞状态,等待任务C的事件位也被设置。

  2. 任务C到达同步点后,使用xEventGroupSetBits()设置其在事件组中的位。一旦任务C的位被设置,任务A和任务B就会离开阻塞状态,并清除所有三个事件位。

  3. 任务C然后调用xEventGroupWaitBits()等待这三个事件比特成为集,但到那时,所有三个事件已经被清除,任务和任务B离开各自的同步点,所以同步失败了。

要成功地使用事件组创建同步点,事件位的设置和后续事件位的测试必须作为一个单独的不可中断操作执行。为此提供了xEventGroupSync() API函数。

xEventGroupSync() API函数

提供xEventGroupSync()允许两个或多个任务使用事件组彼此同步。该功能允许任务在一个事件组中设置一个或多个事件位,然后等待在同一事件组中设置多个事件位的组合,作为一个单独的不可中断操作。

xEventGroupSync() uxBitsTolaitFor参数指定调用任务的解除阻塞条件。如果xEventGroupSync()返回是因为满足了不阻塞条件,uxBitsTolaitFor指定的事件位将在xEventGroupSync()返回之前被清除回零。

EventBits_t xEventGroupSync(EventGroupHandle_t xEventGroup,
                            const EventBits_t uxBitsToSet,
                            const EventBits_t uxBitsToWaitFor,
                            TickType_t xTicksToWait);

清单142. xEventGroupSync() API函数原型

表47. xEventGroupSync()参数和返回值

示例23. 同步任务

示例23使用xEventGroupSync()同步一个任务实现的三个实例。任务参数用于将任务调用xEventGroupSync()时设置的事件位传递给每个实例。

任务在调用xEventGroupsync()之前打印一条消息,在调用xEventGroupsync()返回之后再次打印一条消息。每条消息都包含一个时间戳。这允许在生成的输出中观察执行顺序。伪随机延迟用于防止所有任务同时到达同步点。

有关任务的实现,请参见清单143。

static void vSyncingTask(void *pvParameters)
{
    const TickType_t xMaxDelay = pdMS_TO_TICKS(4000UL);
    const TickType_t xMinDelay = pdMS_TO_TICKS(200UL);
    TickType_t xDelayTime;
    EventBits_t uxThisTasksSyncBit;
    const EventBits_t uxAllSyncBits = (mainFIRST_TASK_BIT |
                                       mainSECOND_TASK_BIT |
                                       mainTHIRD_TASK_BIT);

    /*创建该任务的三个实例-每个任务在同步中使用不同的事件位。使用的事件位通过任务参数传递到每个任务实例。将其存储在uxthisaskssyncbit变量中。*/
    uxThisTasksSyncBit = (EventBits_t)pvParameters;
    
    for (;;)
    {
        /*通过延迟一个伪随机时间来模拟这个任务花费一些时间来执行一个动作。这可以防止该任务的所有三个实例同时到达同步点,因此可以更容易地观察示例的行为。*/
        xDelayTime = (rand() % xMaxDelay) + xMinDelay;
        vTaskDelay(xDelayTime);

        /*打印一条消息,显示这个任务已经到达它的同步点。pcTaskGetTaskName()是一个API函数,它返回在创建任务时分配给任务的名称。*/
        vPrintTwoStrings(pcTaskGetTaskName(NULL), "reached sync point");

        /*等待所有任务到达各自的同步点。*/
        xEventGroupSync(/* 用于同步的事件组。 */
                        xEventGroup,
            
                        /* 该任务设置的位,表示它已到达同步点。 */
                        uxThisTasksSyncBit,
            
                        /*需要等待的比特,每个参与同步的任务都有一个位*/
                        uxAllSyncBits,
            
                        /*无限等待所有三个任务到达vnchronization点。*/
                        portMAX_DELAY);

        /*打印一条消息,显示这个任务已经通过了它的同步点。由于使用了无限期延迟,所以只有在所有任务到达各自的同步点后才会执行下面的行。*/
        vPrintTwoStrings(pcTaskGetTaskName(NULL), "exited sync point");
    }
}

清单143. 示例23中使用的任务的实现

main()函数创建事件组,创建所有三个任务,然后启动调度程序。有关它的实现,请参见清单144。

/* 事件组中的事件位的定义。 */
#define mainFIRST_TASK_BIT (1UL << 0UL) /* 事件位0,由第一个任务设置。 */
#define mainSECOND_TASK_BIT(1UL << 1UL) /* 事件位1,由第二个任务设置。 */
#define mainTHIRD_TASK_BIT (1UL << 2UL) /* 事件位2,由第三个任务设置。 */

/* 声明用于同步三个任务的事件组。 */
EventGroupHandle_t xEventGroup;

int main(void)
{
    /* 在使用事件组之前,必须先创建它。 */
    xEventGroup = xEventGroupCreate();
    
    /*创建任务的三个实例。每个任务都有一个不同的名称,稍后将打印出来,以直观地指示正在执行的任务。当任务到达其同步点时要使用的事件位使用任务参数传递给任务。*/
    xTaskCreate(vSyncingTask, "Task 1", 1000, mainFIRST_TASK_BIT, 1, NULL);
    xTaskCreate(vSyncingTask, "Task 2", 1000, mainSECOND_TASK_BIT, 1, NULL);
    xTaskCreate(vSyncingTask, "Task 3", 1000, mainTHIRD_TASK_BIT, 1, NULL);
    
    /* 启动调度器,使创建的任务开始执行。 */
    vTaskStartScheduler();
    
    /* 像往常一样,永远不要达到下面这行。 */
    for (;;);
    return 0;
}

清单144. 示例23中使用的main()函数

执行示例23时产生的输出如图75所示。可以看到,即使每个任务在不同的(伪随机)时间到达同步点,每个任务在同一时间退出同步点1(这是最后一个任务到达同步点的时间)。

同步点:图75显示了在FreeRTOS Windows端口中运行的示例,它不提供真正的实时行为(特别是当使用Windows系统调用打印到控制台时),因此会显示一些时间变化。

图75执行示例23时产生的输出

Last updated