freeRTOS手册 第八章 . 事件组

如果我对本翻译内容享有所有权。允许任何人复制使用本文章,不会收取任何费用。如有平台向你收取费用与本人无任何关系

第八章 . 事件组

章节介绍和范围

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

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

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

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

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

范围

本章包含下面内容:

  • 事件组使用练习
  • 事件组相比于FreeRTOS的优点和缺点
  • 怎么样在事件组中设置位
  • 怎么样在阻塞状态等待位被设置
  • 如何用事件组同步任务集

事件组特性

事件组,事件标志和事件位

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

# 事件标志映射为`EventBit_t`格式的变量。图71
---------------------------------------------------------------------------------
|   事件标志作为一个EventBit_t格式变量                                            |
| X X X X X X X X 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 |
---------------------------------------------------------------------------------

作为一个例子,如果事件组的值是0x92,那么只有事件位1,4,7被设置,也就是表示只有1,4,7代表的事件发生了。图72展示了一个1,4,7位被设置的EventBit_t格式的变量,其他所有的位被清除的,,设置事件组为0x92。

# 只设置了1,4,7位的事件组,其他的位
---------------------------------------------------------------------------------
|   事件标志作为一个EventBit_t格式变量                                            |
| X X X X X X X X 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 1 0               |
---------------------------------------------------------------------------------

每个事件组中每个独立位是什么意义,取决于开发者的设计。比如,开发者可能创建一个事件组,然后:

  • 定义第0位表示,从网络接收到消息
  • 第1位表示,已经准备好向网络发送消息
  • 第2位表示,当前网络连接被阻止了

更多关于EventBit_t格式数据

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()函数

FreeRTOS V9.0.0也有一个xEventGroupCreateStatic()函数,它会在编译期间静态的为事件组分配必要空间: 一个事件组使用之前必须显式的创建。
事件组是用一个EventGroupHandle_t格式的变量引用的。xEventGroupCreate()函数就是用于创建一个事件组,返回一个EventGroupHandle_t格式的事件组引用。

// xEventGroupCreate()函数原型。列表132
EventGroupHandle_t xEventGroupCreate(void);

/* 返回值: 
 * 如果返回NULL,那么事件组创建失败,因为FreeRTOS没有足够的堆空间分配给事件组数据结构。第二章提供堆空间管理很多的相关信息。
 * 如果返回一个非NULL的值,表示事件组成功创建。这个返回值就是成功创建的事件组的引用
 */

xEventGroupSetBits()函数

xEventGroupSetBits()函数用于设置一位或多位事件组值,通常用于通知任务,这1位或多位代表的事件已经发生。
注意:不要在中断处理程序中调用xEventGroupSetBits(),应该使用xEventGroupSetBitsFromISR()替代

// xEventGroupSetBits()函数原型。列表133
EventBit_t xEventGroupSetBits(EventGroupHandle_t xEventGroup, const EventBit_t uxBitsToSet);
/* 参数
 * xEventGroup: 将被设置位的事件组句柄。这个事件组句柄是调用xEventGroupCreate()函数成功创建事件组后返回的。
 * uxBitsToSet: 将要被设置为1的事件组位。事件组的值是将之前事件组的值和通过uxBitsToSet传递的值按位或后的值。
 *              作为一个例子,设置uxBitsToSet为0x04(二进制0100),这会导致事件组第三位被设置为1,如果它没有被设置。其他所有位的值不会改变。
 * 返回值: 返回调用xEventGroupSetBits()返回时的值。注意返回值不必定设置了uxBitsToSet位,因为这个位可能已经被其他任务清除。
 */

xEventGroupSetBitsFromISR函数

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

// xEventGroupSetBitsFromISR()函数原型。列表134
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()原型。列表135
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()函数时已经达成,任务就不会进入阻塞状态。

一个解锁条件不会使任务进入阻塞状态或退出阻塞的例子在表45中。表45只显示了事件组重要的4位uxBitsToWaitFor的值。其他的事件组位都是0。

列表45,uxBitsToWaitFor和xWaitForAllBits的影响

当前事件组值uxBitsToWaitFor值xWaitForAllBits值导致结果
00000101pdFAILE调用任务进入阻塞,因为事件组第0,2位都没有设置。当事件组的第0位或第2位被设置时离开阻塞。
01000110pdFAILE调用任务不会进入阻塞,因为xWaitForAllBits是pdFAILE,uxBitsToWaitFor两位中的一位已经被设置。
01000110pdTRUE调用任务会进入阻塞,因为xWaitForAllBits是pdTRUE,事件组中uxBitsToWaitFor指定的两位只有其中一位被设置。只有事件组中指定的2位都被设置后才能离开阻塞状态。

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

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

例22事件组使用经验

这个例子展示了如何进行下列操作:

  • 创建一个事件组
  • 用中断处理程序设置事件组中的事件位
  • 在任务中设置事件组中的事件位
  • 阻塞在一个事件组上
    第一次我们设置xWaitForAllBits为pdFALSE,观察它对xEventGroupWaitBits()函数影响。第二次设置为pdTRUE,再次观察他对xWaitForAllBits()的影响。
    事件位0和事件位1由任务设置。事件位2由中断处理程序设定。这3个位用一个#define状态来描述名字,列在列表136中。
// 例22用到的事件定义。列表136
/* 定义事件组中的事件位 */

#define `mainFIRST_TASK_BIT`   (1UL << 0UL)   // 事件位0,由任务设定
#define `mainSECOND_TASK_BIT`  (1UL << 1UL)   // 事件位1,由任务设定
#define `mainISR_BIT`          (1UL << 2UL)   // 事件位2,由中断服务程序设定

列表137展示了设定事件位0和事件位1的任务实现。它在一个循环中设定一个位,然后设定另外一个,每个xEventGroupSetBits()调用之间有一个200ms的延迟。为了方便在中断中观察,每设置一个位都打印一个消息。

// 设定两个事件组位的任务实现。列表137
static void vEventBitSettingTask(void *pvParameters){
    const TickType_t xDelay200ms = pdMS_TO_TICKS(200UL), xDontBlock = 0;
    for(;;){
        /* 在下一次循环前,先延迟一下*/
        xTaskDelay(xDelay200ms);
        /* 打印一个任务设置事件位0的消息*/
        vPrintString("Bit setting task-\t about to set bit 0.\r\n");
        xEventGroupSetBits(xEventGroup, mainFIRST_TASK_BIT);

        /* 设置另外一个位之前再延迟一下*/
        xTaskDelay(xDelay200ms);

        /* 打印一个任务设置事件位1的消息*/
        vPrintString("Bit setting task-\t about to set bit 1.\r\n");
        xEventGroupSetBits(xEventGroup, mainSECOND_TASK_BIT);
    }
}

列表138展示了设定事件组第2位的中断处理程序实现。同样,为了在终端中观察,在设置事件组第2位之前会打印一个相关消息。但是不能直接在中断处理程序中直接向终端输出消息,所以使用xTimerPendFunctionCallFromISR()让文本输出在RTOS的守护任务中执行。
和前面一样,中断处理程序用一个简单的周期任务激活,这个任务会产生一个软件中断。这个例子中,每500ms产生一个中断。

// 例22,设置事件组第二位的中断处理程序
static uint32_t ulEventBitSettingISR(void){
    /* 不是在中断服务程序中打印字符串,而是发送给RTOS守护任务打印。定义为静态的是确保编译器不会将它分配在中断处理程序的栈上,因为中断处理程序的栈在守护任务打印字符串时不存在。*/
    static const char *pcString = "Bit setting ISR -\t about to set bit 2\r\n";
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    /* 打印设置事件组第二位的消息,消息不会在中断中打印,因此将它传递给RTOS的守护任务,通过调用xTimerPendFunctionCallFromISR()实现切换到RTOS守护任务的。*/
    xTimerPendFunctionCallFromISR(vPrintStringFromDeamonTask, (void *)pcString, 0, &xHigherPriorityTaskWoken);
    
    /* 设置事件组第2位 */
    xEventGroupSetBitsFromISR(xEventGroup, mainISR_BIT, &xHigherPriorityTaskWoken);
    
    /* xTimerPendFunctionCallFromISR()和xEventGroupSetBitsFromISR()都向时间任务队列写命令,都用同一个xHigherPriorityTaskWoken变量。如果向时间命令队列写命令导致守护任务离开阻塞,而且守护任务优先级比当前任务(当前中断优先级)优先级高,那么xHigherPriorityTaskWoken会被设置为pTRUE。*/
    /* xHigherPriorityTaskWoken是用于portYIELD_FROM_ISR()函数的,如果xHigherPriorityTaskWoken是pdTRUE,那么调用portYIELD_FROM_ISR()会请求一个上下文切换。如果xHigherPriorityTaskWoken依然是pdFAILSE,portYIELD_FROM_ISR()啥都不会做。*/
    /* Windows版本的portYIELD_FROM_ISR()实现包含一个返回状态,所以在这里就不用再单独写一个返回语句了*/
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

列表139展示了调用xEventGroupWaitBits()等待事件组,进入阻塞的任务实现。每当事件组中的事件位被设置就会打印一个字符串。
这里的xEventGroupWaitBits()中的xClearOnExit设置为pTRUE,所以每次调用xEventGroupWaitBits()返回之前都会自动清理其等待的事件位。

// 例22,阻塞等待事件位被设置的任务实现。列表139
static void vEventBitReadingTask(void *pvParameters){
    EventBit_t xEventGroupValue;
    const EventBits_t xBitsToWaitFor = (mainFIRST_TASK_BIT | mainSECOND_TASK_BIT | mainISR_BIT); 
    for(;;){
        /* 阻塞事件组中的时间为被设置*/ 
        xEventGroupValue = xEventGroupWaitBits(xEventGroup, xBitsToWaitFor, pdTRUE, pdFAIL, 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 ISR -\t Event bit 2 was set\r\n");
    }
}

main()函数创建事件组和任务,在启动调度器之前。列表140就是它的实现。读事件组任务优先级比写事件组优先级高,确保每次读任务解锁条件达成时读任务总是会抢占写任务。

// 例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版本。这里的语法只能用于Windows版本的FreeRTOS,这里的中断只是模拟中断*/
    vPortSetInterruptHandler(mainINTERRUPT_NUMBER, ulEventBitSettingISR);
    
    /* 启动调度器,创建的任务就开始运行了*/
    vTaskStartScheduler();
    
    /* 下面的代码不应该执行 */
    for(;;);
    return 0;
}

例22在xWaitForAllBits为pdFALSE时的执行结果展示在图73。在图73中,可以看到,因为xWaitForAllBits设置为pdFAILE,一旦事件组中等待的事件位有一位被设置,等待读取事件位的任务就会立即离开阻塞状态。

# 例22,xWaitForAllBits为pdFAILE时的执行结果。图73
Bit Setting task -      about to set bit 1.
Bit reading task -      event bit 1 was set

Bit Setting task -      about to set bit 0.
Bit reading task -      event bit 0 was set

Bit Setting task -      about to set bit 1.
Bit reading task -      event bit 1 was set

Bit Setting ISR -      about to set bit 2.
Bit reading ISR -      event bit 2 was set

Bit Setting task -      about to set bit 0.
Bit reading task -      event bit 0 was set

例22在xWaitForAllBits为pdTRUE时的执行结果展示在图74。从图74可以看出,因为xWaitForAllBits设置为pdTRUE,读取事件组任务只有在所有等待的时间为都被设置后才会离开阻塞状态。

# 例22,xWaitForAllBits为pdTRUE时的执行结果。图74
Bit setting task -      about to set bit 1.
Bit setting task -      about to set bit 0.
Bit setting ISR -      about to set bit 2.
Bit reading task -      event bit 0 was set
Bit reading task -      event bit 1 was set
Bit reading ISR -      event bit 2 was set

Bit setting task -      about to set bit 1.
Bit setting task -      about to set bit 0.
Bit setting ISR -      about to set bit 2.
Bit reading task -      event bit 0 was set
Bit reading task -      event bit 1 was set
Bit reading ISR -      event bit 2 was set

Bit setting task -      about to set bit 1.
Bit setting task -      about to set bit 0.
Bit setting task -      about to set bit 1.
Bit setting ISR -      about to set bit 2.
Bit reading task -      event bit 0 was set
Bit reading task -      event bit 1 was set
Bit reading ISR -      event bit 2 was set

Bit setting task -      about to set bit 1.
Bit setting task -      about to set bit 0.
Bit setting ISR -      about to set bit 2.
Bit reading task -      event bit 0 was set
Bit reading task -      event bit 1 was set
Bit reading ISR -      event bit 2 was set

Bit setting task -      about to set bit 1.
Bit setting task -      about to set bit 0.
Bit setting ISR -      about to set bit 2.
Bit reading task -      event bit 0 was set
Bit reading task -      event bit 1 was set
Bit reading ISR -      event bit 2 was set

用事件组实现同步

有时候程序设计者需要两个或以上任务实现同步。比如,任务A接收一个事件,然后将必要的一些操作委托给其他三个任务,任务B,任务C和任务D。如果任务A在任务B,任务C和任务D处理完成前不能接收其他的事件,那么所有的4个任务就需要互相同步。每个任务的同步点将是在完成其处理之后,而且在其他任务也同样处理完成之前不能继续下去。这里任务A只能在4个任务都到达它们的同步点以后才能接收其他任务。
一个更抽象的例子关于任务同步方式是一个FreeRTOS+TCP项目。这个演示例子共享一个TCP套接字在两个任务之间;一个任务发送消息给套接字,另外一个任务从套接字接收数据。两个任务在确认另外一个任务是否访问套接字之前关闭它都是不安全的。如果两个任务都一样关闭套接字,那么必须确保另外一个任务意向,然后必须等待另外一个任务停止使用套接字,然后就可以关闭套接字了。发送数据的任务希望关闭套接字的情形在列表140中使用伪代码展示了。
列表140展示的情形是无意义的,因为这里只有2和任务需要同步,容易看出如果需要同步的任务更多的情况下会变得多复杂,如果其他任务的操作依赖于套接字已经被打开。

在写这本书时,只有一个方式可以实现在多个任务之间共享一个FreeRTOS+TCP套节字

// 相互同步的两个任务的伪代码,确保其中一个任务关闭它时不再有任务使用它。列表141
void SocketTxTask(void *pvParameters){
  xSocket_t xSocket;
  uint32_t ulTxCount = 0UL;

  for(;;){
    /* 创建一个套节字。这个任务会向套节字发送数据,另外一个任务会从套节字接收数据 */
    xSocket = FreeRTOS_socket(...);
    
    /* 连接套节字 */
    FreeRTOS_connect(xSocket, ...);
    
    /* 用一个队列将套节字发送给接收数据的任务 */
    xQueueSend(xSocketPassingQueue, &xSocket, portMAX_DELAY);
    
    /* 关闭套节字之前向它发送1000个消息 */
    for(ulTxCount = 0; ulTxCount < 1000, ulTxCount++){
      if(FreeRTOS_send(xSocket, ...) < 0){
        /* 这里不希望有错误,退出循环,然后关闭套节字 */
        break;
      }
    }

    /* 通知接收任务,发送任务想要关闭套节字了 */
    TxTaskWantsToCloseSocket();
    
    /* 这里就是发送任务的同步点。发送任务会在这里等待接收任务到达它的同步点。无论接收任务使用套节字多久,都会到达它自己的同步点,这时这个套节字才可以安全的关闭 */
    xEventGroupSync(...);
    
    /* 两个任务都使用这个套节字。先断开套节字连接,然后关闭套节字 */
    FreeRTOS_shutdown(xSocket, ...);
    WaitForSocketToDisconnect();
    FreeRTOS_closesocket(xSock);
  }
}

void SocketRxTask(void *pvParameters){
  xSocket xSocket;

  for(;;){
    /* 等待从套节字接收数据,这个套节字是被发送任务创建和连接的 */
    xQueueReceive(xSocketPassingQueue, &xSocket, portMAX_DELAY);
    /* 保持从套节字接收数据,直到发送任务想要关闭套节字*/
    while(TxTaskWantsToCloseSocket() == pdFALSE){
          /* 接收处理数据 */
          FreeRTOS_recv(xSocket, ...);
          ProcessReceivedData();
    }
    /* 这里就是接收任务的同步点,只有在它不使用套节字时都会以到达这里,在这之后发送任务就可以安全的关闭套节字了 */
    xEventGroupSync(...);
  }
}

可以用一个事件组创创建一个同步点:

  • 每个参与同步的任务都要设计来使用事件组的单独事件位
  • 每个任务到达同步点时就会设置它自己的事件位
  • 已经设置它自己的事件位的任务会进入阻塞,等待事件组的其它位被设置,其它的任务要进入到同步点后都会设置事件组。
    虽然这里不能使用xEventGroupSetBits()和xEventGroupWaitBits()函数。如果使用它们,那么设置事件位(表示任务已经到达同步点)和测试事件位(确定任务是否已经到达同步点)会被当做2个部分进行处理。为了观察为什么会出现问题,考虑下面情况,任务A,任务B和任务C试图用事件组同步:
  1. 事件A和事件B已经到达同步点,所以它们的事件位已经设置进事件组,它们在阻塞态,等待任务C设置它的事件位。
  2. 事件C到达同步点,使用xEventGroupSetBits()设置它的事件位,一旦任务C设置它的事件位,任务A和任务B就离开阻塞,清除所有3个事件位。
  3. 任务C然后调用xEventGroupWaitBits()等待所有3个事件位被设置,因为3个事件位已经被清除,任务A和任务B已经超过它们的同步点,所以任务C的同步就会失败。
    为了正确使用事件组创建一个同步点,这里的设置事件位和接下来的测试事件位必需在一个单独不中断的情况下处理。就是因为这个原因才会提供了xEventGroupSync()这个函数。

xEventGroupSync()函数

xEventGroupSync()就是为了让二个或更多的任务用事件组进行同步。它允许一个任务设置一个或更多的事件组的事件位,然后等待事件位组合被设置,作为一个单独不中断操作。
xEventGroupSync()的uxBitsToWaitFor参数指定调用任务的解锁条件。如果是因为xEventGroupSync()的解锁条件达成而返回,xEventGroupSync()返回之前会清除uxBitsToWaitFor指定的位为0。

// xEventGroupSync()函数原型。列表142
EventBits_t xEventGroupSync(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, const EventBits_t uxBitsToWaitFor, TickType_t xTicksToWait);
/* 参数
 * xEventGroup:要被设置事件位和测试的事件组句柄。这个事件组句柄就是调用xEventGroupCreate()函数成功创建事件组后返回的句柄。
 * uxBitsToSet:要设置的事件位掩码。最终事件组的值会是uxBitsToSet值与原来事件组的值按位或的结果。
 *             一个例子,设置uxBitsToSet为0x04(二进制0100)会导致第3个事件位被设置(如果它还没有被设置),其它的事件位不会改变。
 * uxBitsToWaitFor:指定要测试的事件组事件位的掩码。一个例子,如果调用任务想测试事件组第0位,第1位和第2位,那么就设置它为0x07(二进制0111)。
 * xTicksToWait:任务等待解锁条件达成而阻塞,能保持的最大时间计数。它如果为0或解锁条件达成,xEventGroupSync()会立即返回。
 *              这个阻塞时间是一个tick周期数量,因此绝对的时钟时间信赖于tick的周期。可以用pdMS_TO_TICKS()宏将时钟时间转换成特殊的tick周期数量。
 * 返回值:
 *         如果因为解锁条件达成而返回,那么就会返回事件组在解锁条件达成时的值(在自动清除为0之前的值)。因此这种情况下返回的值总是满足解锁条件。
 *         如果是因为阻塞时间到期而返回,那么返回值就是事件组在阻塞时间到期时的值。在这种情况下返回的值总是不满足解锁条件。
 */

例23同步任务

例23用xEventGroupSync()同步3个任务的具体实现。任务参数用于传递每个任务实现调用xEventGroupSync()要设置的事件位。
任务在调用xEventGroupSync()之前会打印一个消息,调用xEventGroupSync()返回后也会打印一个消息。每个消息都包含一个时间信息。这就可以通过观察打印的消息知道执行的顺序。用一个伪随机延时来防止任务同时到达同步点。
列表143是任务的实现。

// 例23使用任务的实现。列表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;
  // 这里的随机三个任务都会是随机同一个数,因此要加入一个随机种子才能实现随机数字
  static int lSeed = 0;

  EventBits_t uxThisTaskSyncBit;
  const EventBits_t uxAllSyncBits = (mainFIRST_TASK_BIT | mainSECOND_TASK_BIT | mainTHIRD_TASK_BIT);

  /* 会有3个具体的任务实例,每个任务实例用不同的事件位进行同步。每个任务使用的事件位通过任务参数传递。保存在uxThisTaskSyncBit变量中 */
  uxThisTaskSyncBit = (EventBits_t)pvParameters;
  srand(lSeed);
  
  for(;;){
    /* 用一个伪随机延迟模拟任务处理一些事情。这会阻止3个任务实例同时到达同步点,以方便观察这个例子的行为 */
    xDelayTime = (rand() % xMaxDelay) + xMinDelay;
    /* 随机种子自加 */
    lSeed++;
    vTaskDelay(xDelayTime);
    /* 打印出这个任务已经到达同步点。pcTaskGetTaskName()会返回一个已经创造的任务名字。*/
    vPrintTwoStrings(pcTaskGetTaskName(NULL), "readched sync point");
    /* 等待所有任务到达它们的同步点。*/
    xEventGroupSync( /* 用于同步的事件组。 */
                    xEventGroup,
                    /* 指定任务到达同步点时要设置的事件位。*/
                    uxThisTaskSyncBit,
                    /* 要等待的所有事件位 */
                    uxAllSyncBits,
                    /* 阻塞的最大时间,这里无限等待下去 */
                    portMAX_DELAY 
    );

    /* 打印任务已经通过同步点的消息。因为在同步点用了一个无限的等待时间,所以只有3个任务都到达他们的同步点都会返回。*/
    vPrintTwoStrings(pcTaskGetTaskName(NULL), "exited sync point");
  }
}

main()函数中创建事件组,三个任务实例然后开启调度器。列表144是它的实现。

//例23的main()实现。列表144
/* 定义事件组要使用的事件位 */
#define mainFIRST_TASK_BIT (1UL << 0UL)       /* 事件位0,被第一个任务设置*/
#define mainSECOND_TASK_BIT (1UL << 1UL)      /* 事件位1,被第二个任务设置*/
#define mainTHIRD_TASK_BIT  (1UL << 2UL)      /* 事件位2,被第三个任务设置*/

/* 定义事件组用于同步3个任务*/
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;
}

例23运行时的输出展示在图75中。可以看到,尽管每个任务到达同步点的时间不一样(用一个伪随机延时时间),每个任务退出同步点的时间是一样的(就是最后一个任务到达同步点的时间).
图75展示的是例子在windows版本的FreeRTOS中运行的结果,它没有提供真正的实时系统(特别是调用Windows系统调用打印到终端时),因此显示一些时间变化

# 例23执行时的输出结果。图75
At time 211664: Task1 reached sync point
At time 211664: Task1 exited sync point
At time 211664: Task2 exited sync point
At time 211664: Task3 exited sync point
At time 212702: Task2 reached sync point
At time 214400: Task1 reached sync point
At time 215439: Task3 reached sync point
At time 215439: Task3 exited sync point
At time 215439: Task2 exited sync point
At time 215440: Task1 exited sync point
At time 217671: Task2 reached sync point
At time 218622: Task1 reached sync point
At time 219402: Task3 reached sync point
At time 219402: Task3 exited sync point
At time 219402: Task2 exited sync point
At time 219402: Task1 exited sync point
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值