FreeRTOS 快速入门(七)之事件组

@toc


一、事件组的概念

事件组可以简单地认为就是一个整数:

  • 每一位表示一个事件
  • 每一位事件的含义由程序员决定,比如:Bit0 表示用来串口是否就绪,Bit1 表示按键是否被按下。这些位,值为 1 表示事件发生了,值为 0 表示事件没发生
  • 一个或多个任务、ISR 都可以去写这些位;一个或多个任务、ISR 都可以去读这些位
  • 可以等待某一位、某些位中的任意一个,也可以等待多位

事件组用一个整数来表示,其中的高 8 位留给内核使用,只能用其他的位来表示事件。这个整数的位数由宏 configUSE_16_BIT_TICKS 决定:

  • 如果 configUSE_16_BIT_TICKS 是 1,那么这个整数就是 16 位的,低 8 位用来表示事件
  • 如果 configUSE_16_BIT_TICKS 是 0,那么这个整数就是 32 位的,低 24 位用来表示事件

configUSE_16_BIT_TICKS 是用来表示 Tick Count 的,怎么会影响事件组?这只是基于效率来考虑

  • 如果 configUSE_16_BIT_TICKS 是 1,就表示该处理器使用 16 位更高效,所以事件组也使用 16 位
  • 如果 configUSE_16_BIT_TICKS 是 0,就表示该处理器使用 32 位更高效,所以事件组也使用 32 位

如果 EvenBits_t 变量中的某个位为 1,则表示该位表示的事件以发生。如果 EvenBits_t 变量中的某个位为 0,则表示该位表示的事件未发生。

例如:事件组的值为 0x92,即事件位1、4、7为1,因此仅发生由位1、4、7表示的事件,如下图:

1、事件组和队列、信号量的对比

事件组和队列、信号量等不太一样,主要集中在两个地方:

  • 唤醒谁?
    • 队列、信号量:事件发生时,只会唤醒一个任务
    • 事件组:事件发生时,会唤醒所有符号条件的任务,简单地说它有”广播”的作用
  • 是否清除事件?
    • 队列、信号量:是消耗型的资源,队列的数据被读走就没了;信号量被获取后就减少了
    • 事件组:被唤醒的任务有两个选择,可以让事件保留不动,也可以清除事件

二、事件组函数

1、创建

使用事件组之前,要先创建,得到一个句柄;使用事件组时,要使用句柄来表明使用哪个事件组。有两种创建方法:动态分配内存、静态分配内存。函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
/* 创建一个事件组,返回它的句柄。
* 此函数内部会分配事件组结构体
* 返回值: 返回句柄,非NULL表示成功
*/
EventGroupHandle_t xEventGroupCreate(void);

/* 创建一个事件组,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticEventGroup_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
EventGroupHandle_t xEventGroupCreateStatic(
StaticEventGroup_t *pxEventGroupBuffer);

2、删除

对于动态创建的事件组,不再需要它们时,可以删除它们以回收内存。

1
2
3
4
/*
* xEventGroup: 事件组句柄,你要删除哪个事件组
*/
void vEventGroupDelete(EventGroupHandle_t xEventGroup)

3、设置事件

可以设置事件组的某个位、某些位,使用的函数有 2 个:

  • 在任务中使用 xEventGroupSetBits()
  • 在 ISR 中使用 xEventGroupSetBitsFromISR()

有一个或多个任务在等待事件,如果这些事件符合这些任务的期望,那么任务还会被唤醒。函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** 
* 设置事件组中的位
* xEventGroup: 哪个事件组
* uxBitsToSet: 设置哪些位?
* 如果uxBitsToSet的bitX, bitY为1, 那么事件组中的bitX, bitY被设置为1
* 可以用来设置多个位,比如 0x15 就表示设置bit4, bit2, bit0
* 返回值: 返回原来的事件值(没什么意义, 因为很可能已经被其他任务修改了)
*/
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );

/**
* 设置事件组中的位
* xEventGroup: 哪个事件组
* uxBitsToSet: 设置哪些位?
* 如果uxBitsToSet的bitX, bitY为1, 那么事件组中的bitX, bitY被设置为1
* 可以用来设置多个位,比如 0x15 就表示设置bit4, bit2, bit0
* pxHigherPriorityTaskWoken: 有没有导致更高优先级的任务进入就绪态? pdTRUE-有,
pdFALSE-没有
* 返回值: pdPASS-成功, pdFALSE-失败
*/
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t * pxHigherPriorityTaskWoken );

值得注意的是,ISR 中的函数,比如队列函数 xQueueSendToBackFromISR 、信号量函数 xSemaphoreGiveFromISR,它们会唤醒某个任务,最多只会唤醒一个任务。

但是设置事件组时,有可能导致多个任务被唤醒,这会带来很大的不确定性。所以 xEventGroupSetBitsFromISR 函数不是直接去设置事件组,而是给一个 FreeRTOS 后台任务发送队列数据,由这个任务来设置事件组。

如果后台任务的优先级比当前被中断的任务优先级高,xEventGroupSetBitsFromISR 会设置 *pxHigherPriorityTaskWoken 为 pdTRUE。

如果后台任务成功地把队列数据发送给了后台任务,那么 xEventGroupSetBitsFromISR 的返回值就是 pdPASS

4、等待事件

使用 xEventGroupWaitBits 来等待事件,可以等待某一位、某些位中的任意一个,也可以等待多位;等到期望的事件后,还可以清除某些位。函数原型如下:

1
2
3
4
5
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );

unblock condition
一个任务在等待事件发生时,它处于阻塞状态;当期望的时间发生时,这个状态就叫”unblock condition”,非阻塞条件,或称为”非阻塞条件成立”;当”非阻塞条件成立”后,该任务就可以变为就绪态。

函数参数说明列表如下:

参数 说明
xEventGroup 等待哪个事件组
uxBitsToWaitFor 等待哪些位,哪些位要被测试
xWaitForAllBits 怎么测试?是”AND”还是”OR”?
pdTRUE: 等待的位,全部为 1;
pdFALSE: 等待的位,某一个为 1 即可
xClearOnExit 函数提出前是否要清除事件?
pdTRUE: 清除 uxBitsToWaitFor 指定的位
pdFALSE: 不清除
xTicksToWait 如果期待的事件未发生,阻塞多久。
可以设置为 0:判断后即刻返回;
可设置为 portMAX_DELAY:一定等到成功才返回;
可以设置为期望的 Tick Count,一般用 pdMS_TO_TICKS() 把 ms 转换为 Tick
Count
返回值 返回的是事件值,
如果期待的事件发生了,返回的是”非阻塞条件成立”时的事件值;
如果是超时退出,返回的是超时时刻的事件值。

可以使用 xEventGroupWaitBits() 等待期望的事件,它发生之后再使用 xEventGroupClearBits() 来清除。但是这两个函数之间,有可能被其他任务或中断抢占,它们可能会修改事件组。

可以使用设置 xClearOnExit 为 pdTRUE,使得对事件组的测试、清零都在 xEventGroupWaitBits() 函数内部完成,这是一个原子操作。

5、同步点

使用 xEventGroupSync() 函数可以同步多个任务:

  • 可以设置某位、某些位,表示自己做了什么事
  • 可以等待某位、某些位,表示要等等其他任务
  • 期望的时间发生后,xEventGroupSync() 才会成功返回。
  • xEventGroupSync 成功返回后,会清除事件

函数原型如下:

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

参数列表如下:

参数 说明
xEventGroup 哪个事件组
uxBitsToSet 要设置哪些事件,完成了哪些事件
比如 0x05(二进制为 0101)会导致事件组的 bit0,bit2 被设置为 1
uxBitsToWaitFor 等待那个位、哪些位
比如 0x15(二级制 10101),表示要等待 bit0,bit2,bit4 都为 1
xTicksToWait 如果期待的事件未发生,阻塞多久。
可以设置为 0:判断后即刻返回;
可设置为 portMAX_DELAY:一定等到成功才返回;
可以设置为期望的 Tick Count,一般用 pdMS_TO_TICKS() 把 ms 转换为 Tick
Count
返回值 返回的是事件值,
如果期待的事件发生了,返回的是”非阻塞条件成立”时的事件值;
如果是超时退出,返回的是超时时刻的事件值。

三、例程

1、例一:等待多个任务

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
/* 定义事件位的意义 */
#define FIRST_TASK_BIT ( 1UL << 0UL )
#define SECOND_TASK_BIT ( 1UL << 1UL )
#define ISR_BIT ( 1UL << 2UL )

/* 设置事件位的任务 */
static void vEventBitSettingTask( void *pvParameters )
{
const TickType_t xDelay200ms = pdMS_TO_TICKS( 200UL ), xDontBlock = 0;
for( ;; )
{
vTaskDelay( xDelay200ms );
/* 设置bit 0 */
vPrintString( "Bit setting task -\t about to set bit 0.\r\n" );
xEventGroupSetBits( xEventGroup, FIRST_TASK_BIT );
vTaskDelay( xDelay200ms );
/* 设置bit 1 */
vPrintString( "Bit setting task -\t about to set bit 1.\r\n" );
xEventGroupSetBits( xEventGroup, SECOND_TASK_BIT );
}
}

/* ISR */
static uint32_t ulEventBitSettingISR( void )
{
static const char *pcString = "Bit setting ISR -\t about to set bit 2.\r\n";
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* ISR中输出提示信息 */
xTimerPendFunctionCallFromISR( vPrintStringFromDaemonTask,
(void *) pcString,
0,
&xHigherPriorityTaskWoken );
/* 设置bit 2 */
xEventGroupSetBitsFromISR( xEventGroup, ISR_BIT, &xHigherPriorityTaskWoken );
/* 根据xHigherPriorityTaskWoken判断是否需要调度程序 */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

/* 获取事件位的任务 */
static void vEventBitReadingTask( void *pvParameters )
{
EventBits_t xEventGroupValue;
const EventBits_t xBitsToWaitFor = ( FIRST_TASK_BIT |
SECOND_TASK_BIT |
ISR_BIT );
for( ;; )
{
/* 获取事件位 */
xEventGroupValue = xEventGroupWaitBits( xEventGroup,/* 事件组的句柄 */
xBitsToWaitFor,/* 待测试的事件位 */
pdTRUE,/* 满足添加时清除上面的事件位 */
pdFALSE, /* 任意事件位被设置就会退出阻塞态 */
portMAX_DELAY );/* 没有超时 */
/* 根据相应的事件位输出提示信息 */
if( ( xEventGroupValue & FIRST_TASK_BIT ) != 0 )
vPrintString( "Bit reading task -\t Event bit 0 was set\r\n" );
if( ( xEventGroupValue & SECOND_TASK_BIT ) != 0 )
vPrintString( "Bit reading task -\t Event bit 1 was set\r\n" );
if( ( xEventGroupValue & ISR_BIT ) != 0 )
vPrintString( "Bit reading task -\t Event bit 2 was set\r\n" );
}
}

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 );
vPortSetInterruptHandler( mainINTERRUPT_NUMBER, ulEventBitSettingISR );
vTaskStartScheduler();
for( ;; );
return 0;
}

xEventGroupWaitBits()xWaitForAllBits 设置为 pdFALSE 的运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 task - about to set bit 2.
Bit reading task - Event bit 2 was set

Bit setting task - about to set bit 0.
Bit reading task - Event bit 0 was set
......

xEventGroupWaitBits()xWaitForAllBits 设置为 pdTRUE 的运行结果如下:

1
2
3
4
5
6
7
8
Bit setting task -         about to set bit 1. 
Bit setting task - about to set bit 0.
Bit setting task - about to set bit 2.
Bit reading task - Event bit 1 was set
Bit reading task - Event bit 0 was set
Bit reading task - Event bit 2 was set

......

2、例二:同步任务

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
/* 定义事件组事件位的意义 */
#define FIRST_TASK_BIT ( 1UL << 0UL )
#define SECOND_TASK_BIT( 1UL << 1UL )
#define THIRD_TASK_BIT ( 1UL << 2UL )

/* 事件组句柄 */
EventGroupHandle_t xEventGroup;

/* 任务函数 */
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 = ( FIRST_TASK_BIT |
SECOND_TASK_BIT |
THIRD_TASK_BIT );

uxThisTasksSyncBit = ( EventBits_t ) pvParameters;

for( ;; )
{
/* 随机的延时,防止单个任务一直同时到达同步点(用于模拟每个任务处理事件的时间不同) */
xDelayTime = ( rand() % xMaxDelay ) + xMinDelay;
vTaskDelay( xDelayTime );

vPrintTwoStrings( pcTaskGetTaskName( NULL ), "reached sync point" );
/* 等待同步 */
xEventGroupSync( xEventGroup,
uxThisTasksSyncBit,/* 表示此任务到达同步点设置的位 */
uxAllSyncBits, /* 需要等待同步的位 */
portMAX_DELAY );
vPrintTwoStrings( pcTaskGetTaskName( NULL ), "exited sync point" );
}
}

int main( void )
{
/* 创建用于同步的事件组 */
xEventGroup = xEventGroupCreate();
/* 创建三个任务 */
xTaskCreate( vSyncingTask, "Task 1", 1000, FIRST_TASK_BIT, 1, NULL );
xTaskCreate( vSyncingTask, "Task 2", 1000, SECOND_TASK_BIT, 1, NULL );
xTaskCreate( vSyncingTask, "Task 3", 1000, THIRD_TASK_BIT, 1, NULL );
vTaskStartScheduler();
for( ;; );
return 0;
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
At time 211664: Task 1 reached sync point
At time 211664: Task 1 exited sync point
At time 211664: Task 2 exited sync point
At time 211664: Task 3 exited sync point
At time 212702: Task 2 reached sync point
At time 214400: Task 1 reached sync point
At time 215439: Task 3 reached sync point
At time 215439: Task 3 exited sync point
At time 215439: Task 2 exited sync point
At time 215440: Task 1 exited sync point
At time 217671: Task 2 reached sync point
At time 218622: Task 1 reached sync point
At time 219402: Task 3 reached sync point
At time 219402: Task 3 exited sync point
At time 219402: Task 2 exited sync point
At time 219402: Task 1 exited sync point
......


FreeRTOS 快速入门(七)之事件组
http://example.com/2024/08/24/FreeRTOS入门七/
作者
Yu xin
发布于
2024年8月24日
许可协议