事件组

事件组(Event Groups)是一种用于任务间通信的机制,主要用于多任务之间的同步。事件组允许任务通过设置和清除标志位来彼此协作,完成类似于“事件通知”的功能。事件组适合用于当多个任务需要等待某些条件或事件时,它们之间进行同步。

事件位(事件标志)

事件位用于指示事件 是否发生。事件位通常称为事件标志。例如,应用程序可以:

  • 定义一个位(或标志),设置为 1 时表示“已收到消息并准备好处理”, 设置为 0 时表示“没有消息等待处理”。
  • 定义一个位(或标志),设置为 1 时表示“应用程序已将准备发送到网络的消息排队”, 设置为 0 时表示 “没有消息需要排队准备发送到网络”。
  • 定义一个位(或标志),设置为 1 时表示“需要向网络发送心跳消息”, 设置为 0 时表示“不需要向网络发送心跳消息”。

事件组

事件组就是一组事件位。事件组中的事件位通过位编号来引用。同样,以上面列出的三个例子为例:

  • 事件组位 0 号位表示“已收到消息并准备好处理”。
  • 事件组位 1 号位表示“应用程序已将准备发送到网络的消息排队”。
  • 事件组位 2 号位表示“需要向网络发送心跳消息”。

事件组和事件位数据类型

事件组由 EventGroupHandle_t 类型的变量引用。

如果 configUSE_16_BIT_TICKS 设为 1,则事件组中存储的位(或标志)数为 8; 如果 configUSE_16_BIT_TICKS 设为 0,则为 24。 configUSE_16_BIT_TICKS 的值取决于 任务内部实现中用于线程本地存储的数据类型。

事件组中的所有事件位都存储在 EventBits_t 类型的单个无符号整数变量中。事件位 0 存储在位 0 中, 事件位 1 存储在位1 中,依此类推。

包含 24 个事件位的事件组,其中包含了 3 个事件,在图片中仅设置了事件位 2

eventgroupshili

事件组相关函数

提供的事件组 API 函数允许任务在事件组中设置一个或多个事件位, 清除事件组中的一个或多个事件位,并挂起(进入阻塞状态, 因此任务不会消耗任何处理时间)以等待事件组中一个或多个事件位固定下来。

事件组也可用于同步任务,创建通常称为“集合”的任务。任务同步点是应用程序代码中的一个位置,在该位置任务将在阻塞状态(不消耗任何 CPU 时间)下等待,直到参与同步的所有其他任务也到达其同步点。

实现事件组时 RTOS 必须克服的挑战

在实现事件组时,RTOS 必须克服以下两项主要挑战:

  1. 避免在用户的应用程序中创建争用条件:
    如果出现以下情况,事件组实现将在应用程序中创建争用条件:

    • 不清楚各个位(或标志)由谁清除。
    • 不清楚何时清除位。
    • 不清楚在任务退出测试位值的 API 函数时是否设置或清除了位(可能是 另一个任务或中断已更改该位的状态)。

    FreeRTOS 事件组的实现通过内置智能来确保位的设置、测试和清除具有原子性, 从而消除了争用条件的可能性。线程本地存储和谨慎使用 API 函数返回值使之成为可能。

  2. 避免不确定性:

    事件组概念意味着非确定性行为,因为 不知道在一个事件组上有多少任务被阻塞,因此 不清楚在设置事件位时 需要测试多少条件或解除多少阻塞任务。

    根据 FreeRTOS 质量标准,无法在禁用中断时 或在中断服务程序中 执行非确定性操作。为确保在设置事件位时 不违反这些严格的质量标准,需要以下两点:

    • RTOS 调度器的锁定机制用于确保 在 RTOS 任务中设置事件位时中断仍处于启用状态。
    • 尝试在中断服务程序中设置事件位时, 集中延迟中断机制用于 将设置位的动作延迟到任务。

API

xEventGroupCreate

创建一个新的 RTOS 事件组,并返回可以引用新创建的事件组的句柄。

1
2
// event_groups.h
EventGroupHandle_t xEventGroupCreate( void );

要使此 RTOS API 函数可用:

  1. configSUPPORT_DYNAMIC_ALLOCATION 必须在 FreeRTOSConfig.h 中设置为 1,或保留未定义状态(此时 默认为 1)。
  2. 必须将 RTOS 源文件 FreeRTOS/source/event_groups.c 包含在构建中。

每个事件组都需要(非常)少量的 RAM 来保存 事件组的状态。如果使用 xEventGroupCreate() 创建事件组, 则所需的 RAM 将从 FreeRTOS 堆自动分配。 如果使用 xEventGroupCreateStatic() 创建事件组 则 RAM 由应用程序编写器提供,这需要用到一个附加参数, 但允许在编译时静态分配 RAM 。

事件组存储在 EventBits_t 类型的变量中。如果 configUSE_16_BIT_TICKS 设置为 1,则事件组内实现的位数(或标志数)为 8; 如果 configUSE_16_BIT_TICKS 设置为 0,则事件组内实现的位数(或标志数)为 24。 对 configUSE_16_BIT_TICKS 的依赖 取决于 RTOS 任务内部实现中用于线程本地存储的数据类型。

参数:

返回:

  • 如果创建了事件组,则返回事件组的句柄。
  • 如果没有足够的 FreeRTOS 堆来创建事件组,则返回 NULL。
1
2
3
4
5
6
EventGroupHandle_t xCreatedEventGroup;

if(xCreatedEventGroup == NULL) {
}
else {
}

xEventGroupSetBits

在 RTOS 事件组中设置位(标志)。该函数不能从中断中调用。xEventGroupSetBitsFromISR() 是可从中断调用的版本。

1
EventBits_t xEventGroupSetBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet );

在事件组中设置位时,所有等待该位的任务将自动解除阻塞。

必须将 RTOS 源文件 FreeRTOS/source/event_groups.c 包含在构建中,xEventGroupSetBits() 函数才可用。

参数:

  • xEventGroup

    要设置位的事件组。必须事先通过调用 xEventGroupCreate() 创建事件组。

  • uxBitsToSet

    指定要在事件组中设置的一个或多个位的按位值。例如,将 uxBitsToSet 设置为 0x08,则只设置第 3 位。将 uxBitsToSet 设置为 0x09,即可设置第 3 位和第 0 位。

返回:

返回的是事件组当前的值,即在调用该函数之后,事件组中所有位的最新状态。

调用 xEventGroupSetBits() 后,返回的事件组值可能会受到以下两种情况的影响:

  1. 如果设置某个事件位使得等待该位的任务从阻塞状态中解除,该位可能会被自动清除(具体取决于 xClearBitOnExit 参数的设置,参考 xEventGroupWaitBits() 函数)。
  2. 如果在调用 xEventGroupSetBits() 之后,有优先级更高的任务变为就绪状态并执行,它们可能会在返回之前修改事件组的值。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
EventBits_t uxBits;

uxBits = xEventGroupSetBits(xEventGroup, BIT_0 | BIT_4 );

if((uxBits & ( BIT_0 | BIT_4 ) ) == ( BIT_0 | BIT_4 ) ){
// 当前函数返回后 0、4 位报纸不变s
} else if((uxBits & BIT_0) != 0) {
// 当函数返回时,位0保持不变,但位4被清除。可能是由于等待位4的任务从阻塞状态中移除,位4被自动清除。
} else if((uxBits & BIT_4) != 0) {
// 当函数返回时,位4保持不变,但位0被清除。可能是由于等待位0的任务从阻塞状态中移除,位0被自动清除。.
} else {
// 位0和位4未被设置。可能有一个任务正在等待这两个位被设置,当任务离开“阻塞”状态时,这些位被清除。
}

xEventGroupWaitBits

读取 RTOS 事件组中的位,选择性地进入“阻塞”状态(已设置 超时值)以等待设置单个位或一组位。

1
2
3
4
5
6
7
// event_groups.h
EventBits_t xEventGroupWaitBits(
const EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );

无法从中断调用此函数。

必须将 RTOS 源文件 FreeRTOS/source/event_groups.c 包含在构建中,xEventGroupWaitBits() 函数才可用。

参数:

  • xEventGroup

    事件组的句柄,表示你想要操作的事件组。必须事先通过调用 xEventGroupCreate() 创建事件组。

  • uxBitsToWaitFor

    指定你希望等待的位。它是一个位掩码,每一位对应一个特定的事件标志。

    例如,要等待 第 0 位和/或第 2 位,将 uxBitsToWaitFor 设置为 0x05 即可。要等待第 0 位和/或第 1 位和/或第 2 位, 将 uxBitsToWaitFor 设置为 0x07 即可。uxBitsToWaitFor 不得 设置为 0。

  • xClearOnExit

    如果 xClearOnExit 设置为 pdTRUE,那么在发生以下情况之前,作为 uxBitsToWaitFor 参数传递的值中设置的任何位都将在 xEventGroupWaitBits() 返回之前在事件组中清除;xEventGroupWaitBits() 因超时以外的任何原因返回。超时值由 xTicksToWait 参数设置。(只要不是超时返回都将清除标志位)

    如果 xClearOnExit 设置为 pdFALSE,那么当调用 xEventGroupWaitBits() 返回时,事件组中设置的位不会改变。

  • xWaitForAllBits

    xWaitForAllBits 用于创建逻辑 AND(必须设置所有位) 或逻辑 OR(必须设置一个或多个位):

    如果 xWaitForAllBits 设置为 pdTRUE(逻辑 &&),那么 xEventGroupWaitBits() 在以下条件下将返回:

    • 作为 uxBitsToWaitFor 参数传递的值中的所有位在事件组中被设置(置 1)或指定的阻塞时间到期。

    如果 xWaitForAllBits 设置为 pdFALSE(逻辑 ||),那么 xEventGroupWaitBits() 在以下条件下将返回:

    • 作为 uxBitsToWaitFor 参数传递的值中的任何位在事件组中被设置(置 1)或指定的阻塞时间到期。
  • xTicksToWait

    等待以下情况发生的最长时间(以“滴答”为单位,取决于 xWaitForAllBits 值):

    • uxBitsToWaitFor 指定的一个/所有位被设置。

返回:

  • 事件位等待完成设置或阻塞时间过期时的事件组值。在以下情况下,事件组中事件位的当前值将与返回值不同: 高优先级任务或中断在调用任务解除“阻塞”状态和退出 xEventGroupWaitBits() 函数之间更改了事件位的值 。

    测试返回值以确定哪些位已完成设置。如果 xEventGroupWaitBits() 因为超时过期而返回, 则并非在等待的所有位都会进行设置。如果 xEventGroupWaitBits() 返回因为其等待的位被设置而返回, 则返回值是由于任何位因为 xClearOnExit 参数被设置为 pdTRUE

    而自动清除之前的事件组值。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define BIT_0	( 1 << 0 )
#define BIT_4 ( 1 << 4 )

uxBits = xEventGroupWaitBits(
xEventGroup,
BIT_0 | BIT_4,
pdTRUE,
pdFALSE,
xTicksToWait );

if((uxBits & ( BIT_0 | BIT_4)) == (BIT_0 | BIT_4)) {
// 两个位都是 1
} else if((uxBits & BIT_0) != 0) {
// 只设置了 bit 0
} else if(( uxBits & BIT_4) != 0) {
// 只设置了 bit 4
} else {
// 二者都没有修改
}

xEventGroupSync

以原子方式设置 RTOS 事件组中的位(标志),然后等待在同一事件组中设置位的组合。此功能通常用于同步多个任务(通常称为任务集合),其中每个 任务必须等待其他任务到达同步点后才能继续。

1
2
3
4
5
event_groups.h
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait );

不能从中断使用此函数。

如果设置了 uxBitsToWait 参数指定的位,或者在该时间内设置了这些位,则该函数将在其阻塞时间到期之前返回。这种情况下,uxBitsToWait 指定的所有位将在 函数返回之前自动清除。

必须将 RTOS 源文件 FreeRTOS/source/event_groups.c 包含在构建中,xEventGroupSync() 函数才可用。

参数:

  • xEventGroup

    事件组的句柄,表示你想要操作的事件组。必须事先通过调用 xEventGroupCreate() 创建事件组。

  • uxBitsToSet

    在确定 uxBitsToWait 参数指定的所有位是否都已设置(可能还要等待)之前,要在事件组中设置的一个或多个位。

    例如,将 uxBitsToSet 设置为 0x04, 即可设置事件组内的第 2 位。

  • uxBitsToWaitFor

    表示任务希望等待哪些位被设置为 1。只有当这些位中的所有位都被设置时,任务才会解除阻塞并继续执行。

    例如,将 uxBitsToWaitFor 设置为 0x05,即可等待第 0 位和第 2 位。将 uxBitsToWaitFor 设置为 0x07,即可等待第 0 位、第 1 位和第 2 位等。

  • xTicksToWait

    等待 uxBitsToWaitFor 参数值指定的所有位被设置的最长时间(以滴答为单位) 。

返回:

  • 等待置位时或阻塞到期时事件组的值。测表示事件组中所有位的当前状态。

    如果 xEventGroupSync() 因为超时过期而返回, 则并非在等待的所有位都会进行设置(有部分状态位没有被设置)。它会返回事件组的当前状态。

    如果 xEventGroupSync() 因其所等待的所有位都被设置而返回, 那么返回值是自动清除任何位之前的事件组值。

示例

1
2
3
4
5
6
7
8
9
10
11
// 设置 0 标志位,等待 ALL_SYNC_BITS 对应的标志位被置位

#define ALL_SYNC_BITS ( TASK_0_BIT | TASK_1_BIT | TASK_2_BIT )

uxReturn = xEventGroupSync( xEventBits,
TASK_0_BIT,
ALL_SYNC_BITS,
xTicksToWait );
if((uxReturn & ALL_SYNC_BITS) == ALL_SYNC_BITS) {
// 当三个任务同步时的操作
}

实现

按键被按下时,通过事件组触发三个任务依次执行,打印不同的消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "main.h"
#include "FreeRTOS.h"
#include "task.h"

#include "event_groups.h"
#include "Usart0.h"

#define EVENT_BIT(N) (1 << N)
#define EVENT_BIT_0 EVENT_BIT(0)
#define EVENT_BIT_1 EVENT_BIT(1)
#define EVENT_BIT_2 EVENT_BIT(2)
#define EVENT_BIT_3 EVENT_BIT(3)
#define EVENT_BIT_4 EVENT_BIT(4)
#define EVENT_BIT_5 EVENT_BIT(5)
#define EVENT_BIT_6 EVENT_BIT(6)
#define EVENT_BIT_7 EVENT_BIT(7)
#define EVENT_BIT_8 EVENT_BIT(8)
#define EVENT_BIT_9 EVENT_BIT(9)
#define EVENT_BIT_10 EVENT_BIT(10)
#define EVENT_BIT_11 EVENT_BIT(11)
#define EVENT_BIT_12 EVENT_BIT(12)
#define EVENT_BIT_13 EVENT_BIT(13)
#define EVENT_BIT_14 EVENT_BIT(14)
#define EVENT_BIT_15 EVENT_BIT(15)
#define EVENT_BIT_16 EVENT_BIT(16)
#define EVENT_BIT_17 EVENT_BIT(17)
#define EVENT_BIT_18 EVENT_BIT(18)
#define EVENT_BIT_19 EVENT_BIT(19)
#define EVENT_BIT_20 EVENT_BIT(20)
#define EVENT_BIT_21 EVENT_BIT(21)
#define EVENT_BIT_22 EVENT_BIT(22)
#define EVENT_BIT_23 EVENT_BIT(23)

TaskHandle_t task_handler;
TaskHandle_t task_key_handler;
TaskHandle_t task1_handler;
TaskHandle_t task2_handler;
TaskHandle_t task3_handler;
EventGroupHandle_t event_group;

void task1(void *pvParameters) {
EventBits_t bits;
while(1) {
bits = xEventGroupWaitBits(event_group, EVENT_BIT_0, pdTRUE, pdFALSE, portMAX_DELAY);
printf("task1: %d\r\n", bits);
vTaskDelay(1000);
}
vTaskDelete(NULL);
}

void task2(void *pvParameters) {
EventBits_t bits;
while(1) {
bits = xEventGroupWaitBits(event_group, EVENT_BIT_0, pdTRUE, pdTRUE, portMAX_DELAY);
printf("task2: %d\r\n", bits);
vTaskDelay(2000);
}
vTaskDelete(NULL);
}

void task3(void *pvParameters) {
EventBits_t bits = 0;
while(1) {
bits = xEventGroupWaitBits(event_group, EVENT_BIT_0, pdTRUE, pdTRUE, portMAX_DELAY);
printf("task3: %d\r\n", bits);
vTaskDelay(3000);
}
vTaskDelete(NULL);
}

void task_key(void *pvParameters) {
FlagStatus pre_state = RESET;
while(1) {
FlagStatus state = gpio_input_bit_get(GPIOA, GPIO_PIN_0);
if(SET == state && pre_state == RESET) {
// 当前高电平, 上一次为低电平,按下
pre_state = state;

printf("set bit \r\n");
xEventGroupSetBits(event_group, EVENT_BIT_0);
} else if(RESET == state && pre_state == SET) {
// 当前高电平, 上一次为低电平,抬起
pre_state = state;
}
vTaskDelay(20);
}
}

void start_task(void *pvParameters) {
GPIO_config();
Usart0_init();

event_group = xEventGroupCreate();
if(event_group != NULL) {
printf("group success\r\n");
}

taskENTER_CRITICAL();

xTaskCreate(task_key, "task_key", 64, NULL, 0, &task_key_handler);
xTaskCreate(task1, "task1", 64, NULL, 4, &task1_handler);
xTaskCreate(task2, "task2", 64, NULL, 3, &task2_handler);
xTaskCreate(task3, "task3", 64, NULL, 2, &task3_handler);
vTaskDelete(task_handler);

taskEXIT_CRITICAL();
}

void Usart0_recv(uint8_t *data, uint32_t len) {
// printf("recv: %s\n", data);
xEventGroupSetBitsFromISR(event_group, EVENT_BIT_0, NULL);
}

static void GPIO_config() {
// 时钟初始化
rcu_periph_clock_enable(RCU_GPIOA);
// 配置GPIO模式
gpio_mode_set(GPIOA, GPIO_MODE_INPUT, GPIO_PUPD_PULLDOWN, GPIO_PIN_0);
}

int main(void) {
nvic_priority_group_set(NVIC_PRIGROUP_PRE4_SUB0);
xTaskCreate(start_task, "start_task", 128, NULL, 0, &task_handler);
vTaskStartScheduler();

while(1) {}
}