S1-07 事件组

事件组

在 FreeRTOS 中,事件组(Event Group)是一种用于任务间通信的机制,用于在多个任务之间同步和传递事件。
事件组主要包含一下两个概念:

  1. 事件标志位(Event Flags):每个事件标志位代表一个独立的事件状态。
  2. 事件组控制块(Event Group Control Block,EGCB):事件组的信息结构体,维护了事件标志位的状态等信息。

通过调用 FreeRTOS 提供的 API 函数,任务可以进行如下操作:

  1. 设置指定的事件标志位为已发生。
  2. 查询事件标志位是否已经被设置。
  3. 等待多个事件标志位中的任意一个或全部都被设置。

对于等待事件标志位的任务,可以选择在等待时阻塞自己等待事件的发生,也可以在等待时设置超时时间,如果超时仍然没有事件发生,则任务会自动解除阻塞并返回相应值。

事件组由 EventGroupHandle_t 类型的变量引用。
如果 configUSE_16_BIT_TICKS 如果 configUSE_16_BIT_TICKS 设置为 0,则为 24。configUSE_16_BIT_TICKS 的值取决于 任务内部实现中用于线程本地存储的数据类型。
事件组中的所有事件位都 存储在 EventBits_t 类型的单个无符号整数变量中。 事件位 0 存储在位 0 中, 事件位 1 存储在位1 中,依此类推。
下图表示一个 24 位事件组, 使用 3 个位来保存前面描述的 3 个示例事件。 在图片中,仅设置了 事件位 2。
在这里插入图片描述

第一个餐厅的厨师(作为事件标志位)

回到我们厨子和吃货的世界中,本次出场的只有厨子,另外还有一些服务员,服务员负责给厨子配菜,这时候厨子做一个汉堡需要等待三样东西,分别是面包、肉饼、蔬菜,做蔬菜的服务员等肉饼做好后再做蔬菜,做肉饼的则要等待做面包的,而做面包的需要等待厨子的号令,一切是那么的竟然有序。

代码共享位置:https://wokwi.com/projects/362876432326851585

#define START_FLAG      (1 << 0)
#define BREAD_FLAG      (1 << 1)
#define MEAT_FLAG       (1 << 2)
#define VEGETABLE_FLAG  (1 << 3)
EventGroupHandle_t xEventHamburg = NULL;  // 做汉堡的事件组
// 面包服务员
void waiter_bread_task(void *param_t){
  EventBits_t uxBits;
  uxBits = xEventGroupWaitBits(xEventHamburg, // 事件组句柄
                              START_FLAG,     // 等待开始事件
                              pdTRUE,         // 读取后清空标志位
                              pdTRUE,         // ADN关系,1个值无所谓
                              portMAX_DELAY);
  printf("[BRED] 等到开始事件 : %X\n", uxBits);
  vTaskDelay(pdMS_TO_TICKS(random(500,2000)));
  uxBits = xEventGroupSetBits(xEventHamburg, BREAD_FLAG); // 设置面包标志位
  printf("[BRED] 面包已就绪 : %X\n", uxBits);
  vTaskDelete(NULL);
}
// 肉饼服务员
void waiter_meat_task(void *param_t){
  EventBits_t uxBits;
  uxBits = xEventGroupWaitBits(xEventHamburg, // 事件组句柄
                              BREAD_FLAG,     // 等待面包事件
                              pdFALSE,        // 读取后不清空
                              pdTRUE,         // ADN关系,1个值无所谓
                              portMAX_DELAY);
  printf("[MEAT] 等到面包做好 : %X\n", uxBits);
  vTaskDelay(pdMS_TO_TICKS(random(500,2000)));
  uxBits = xEventGroupSetBits(xEventHamburg, MEAT_FLAG); // 设置肉饼标志位
  printf("[MEAT] 肉饼已就绪 : %X\n", uxBits);
  vTaskDelete(NULL);
}
// 蔬菜服务员
void waiter_vegetable_task(void *param_t){
  EventBits_t uxBits;
  uxBits = xEventGroupWaitBits(xEventHamburg, // 事件组句柄
                              MEAT_FLAG,      // 等待开始事件
                              pdFALSE,        // 读取后不清空
                              pdTRUE,         // ADN关系,1个值无所谓
                              portMAX_DELAY);
  printf("[VEGE] 等到肉饼做好 : %X\n", uxBits);
  vTaskDelay(pdMS_TO_TICKS(random(500,2000)));
  uxBits = xEventGroupSetBits(xEventHamburg, VEGETABLE_FLAG); // 设置蔬菜标志位
  printf("[VEGE] 蔬菜已就绪 : %X\n", uxBits);
  vTaskDelete(NULL);
}
// 厨师线程
void chef_task(void *param_t){
  pinMode(4, INPUT_PULLUP);
  while(1){
    if(digitalRead(4) == LOW){
      // 开始做汉堡
      EventBits_t uxBits;  // 设置事件标志位的返回值
      uxBits = xEventGroupSetBits(xEventHamburg, START_FLAG);  // 这个返回值有可能会清空标志位,具体读文档
      printf("[CHEF] 开始做汉堡 : %X\n", uxBits);
      uxBits = xEventGroupWaitBits(xEventHamburg,                            // 事件句柄
                                  BREAD_FLAG | MEAT_FLAG | VEGETABLE_FLAG,    // 等待代表面包、肉饼、蔬菜的标志位
                                  pdFALSE,                                    // 是否清空对应标志位
                                  pdTRUE,                                     // 等待的Bits判断关系 True为 AND, False为 OR
                                  portMAX_DELAY);                             // 等待超时时间
      printf("[CHEF] 汉堡做完了 : %X\n", uxBits);
      // 重置事件组
      xEventGroupClearBits(xEventHamburg, START_FLAG | BREAD_FLAG | MEAT_FLAG | VEGETABLE_FLAG);
      uxBits = xEventGroupGetBits(xEventHamburg);
      printf("[CHEF] 汉堡做好了,我下班了 : %X\n", uxBits);
      vTaskDelete(NULL);
    }
    vTaskDelay(100);
  }
}
void setup() {
  Serial.begin(115200);
  xEventHamburg = xEventGroupCreate();  //初始化事件组
  // 启动各个线程
  xTaskCreate(chef_task, "Chef", 1024*2, NULL, 1, NULL);
  xTaskCreate(waiter_bread_task, "Bread", 1024*2, NULL, 1, NULL);
  xTaskCreate(waiter_meat_task, "Meat", 1024*2, NULL, 1, NULL);
  xTaskCreate(waiter_vegetable_task, "Vegetable", 1024*2, NULL, 1, NULL);
}
void loop() {
  delay(100);
}

代码中首先在setup中通过 xEventGroupCreate 创建了一个事件组。
chef_task 中不断判断是否按下了按钮,当按钮被按下时,首先设置事件组的第一位为1,表示开始信号,此时的3个字节数据为(此时事件组值为1)
在这里插入图片描述

设置完之后,就开始等待第2 3 4位的数据,这里我们用的都是 xEventGroupWaitBits 等待,该函数一共有5个参数,函数原型如下:

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

xEventGroup : 事件组句柄
uxBitsToWaitFor : 指定事件组中要测试的一个或多个事件位的按位值,可以用 | 运算指定多个,例如,等待第0位则为1,等待第二位则为2,等待第三位则为4,等待第四位则为8,如果等待第1位和第三位,则为1|3=5。
xClearOnExit : 是否清除时间位,设置为 pdTRUE, 那么在作为 uxBitsToWaitFor 参数传递的值中设置的任何位 会在 xEventGroupWaitBits() 返回某个值之前在事件组中清除掉, 前提是 xEventGroupWaitBits() 因超时以外的原因而返回值。
xWaitForAllBits
用于创建逻辑与测试 (必须设置所有位)或逻辑或测试(必须设置一个 或多个位),如下所示:如果 xWaitForAllBits 设置为 pdTRUE, 那么当在作为 uxBitsToWaitFor 参数传递的值中设置的所有位 均已在事件组中设置好,或指定的阻塞时间已过期,则 xEventGroupWaitBits() 会返回相应值。如果 xWaitForAllBits 设置为 pdFALSE,那么当在作为 uxBitsToWaitFor 参数传递的值中设置的任何位已在事件组中设置好, 或指定的阻塞时间已过期,则 xEventGroupWaitBits() 会返回相应值。
xTicksToWait : 超时时间。
这里我们等待选择不清空,并且使用同时等待第2、3、4位事件都到达。
xEventGroupSetBits 函数有个返回值,是当前事件组的值,但需要注意的是,因为执行完该函数后,系统调度器会对任务进行一次调度,看是否有任务等到了某个事件,如果有事件被触发,根据任务优先级,会先执行另外的事件,在返回。
因为在 waiter_bread_task 任务中等待该标志位的时候选择了清空标志位,所以这时候获得的返回值为0,其实我们已经设置了 START_FLAG 标志位,可以通过修改 waiter_bread_task 任务中 xEventGroupWaitBits 函数的 xClearOnExit 测试。

待三个事件都到达时,使用 xEventGroupClearBits 清空我们使用过的前四位。

waiter_bread_task 任务中,首先等待 START_FLAG 时间到达,也就是第一位置1,这里的等待函数 xClearOnExit 参数位 pdTRUE,表示当收到这个信号后将立刻清空。
此时任务组数据如下(此时事件组值为0):
在这里插入图片描述

然后通过 xEventGroupSetBits 函数设置第二位为1(此时事件组值为2):
在这里插入图片描述

waiter_meat_task 任务启动后一直等待 BREAD_FLAG 事件到达,当事件到达后,通过 xEventGroupSetBits(xEventHamburg, BREAD_FLAG) 设置事件组为6:
在这里插入图片描述

waiter_vegetable_task 任务启动后一直等待 MEAT_FLAG 事件到达, 当事件到达后,通过 xEventGroupSetBits(xEventHamburg, VEGETABLE_FLAG) 设置事件组为14(也就是E):
在这里插入图片描述

最后回到 chef_task 任务中,等待三个值凑齐,继续往下执行,通过 xEventGroupClearBits(xEventHamburg, START_FLAG | BREAD_FLAG | MEAT_FLAG | VEGETABLE_FLAG) 清空事件组,此时事件组值再次归零。

标志位的应用方向

事件标志为用于按顺序执行的业务逻辑,举一个智慧物流的例子,机械臂为一辆自动行驶的电动车,装载上货物,电动车开到指定的厂房,中间需要经过一个电动门。机械臂收到指令后触发装货的信号,等装完货之后,机械臂又触发小车的移动信号,等小车移动到门口的时候又触发电动门开闸的信号,然后等电动门开启完毕后,再给小车一个移动的信号,以此类推,直到小车运到下一个厂房机械臂将货物卸下。
这里边用到了多个信号量协同,但是他们之间是有顺序关系的。

二号餐厅的厨师(改进的标志位)

在一号餐厅中,所有服务员必须等上一名服务员完成工作后才会做自己的工作,但本质上面包、肉饼和蔬菜的准备并不是顺序关系,而是并行关系,三个线程完全可以独立运行,他们只需要共同等待一个开始信号,信号到达后各自做自己的工作,而厨师的任务与他们三个是并行的,所以厨师还是需要等待另外三个人工作完成后才能开动。
所以,我们需要对第一个餐厅的代码进行一次修改

代码共享位置:https://wokwi.com/projects/362946353284773889

#define START_FLAG      (1 << 0)
#define BREAD_FLAG      (1 << 1)
#define MEAT_FLAG       (1 << 2)
#define VEGETABLE_FLAG  (1 << 3)
EventGroupHandle_t xEventHamburg = NULL;  // 做汉堡的事件组
// 面包服务员
void waiter_bread_task(void *param_t){
  EventBits_t uxBits;
  uxBits = xEventGroupWaitBits(xEventHamburg, // 事件组句柄
                              START_FLAG,     // 等待开始事件
                              pdFALSE,        // 读取后不清空标志位
                              pdTRUE,         // ADN关系,1个值无所谓
                              portMAX_DELAY);
  printf("[BRED] 等到开始事件 : %X\n", uxBits);
  vTaskDelay(pdMS_TO_TICKS(random(500,2000)));
  uxBits = xEventGroupSetBits(xEventHamburg, BREAD_FLAG); // 设置面包标志位
  printf("[BRED] 面包已就绪 : %X\n", uxBits);
  vTaskDelete(NULL);
}
// 肉饼服务员
void waiter_meat_task(void *param_t){
  EventBits_t uxBits;
  uxBits = xEventGroupWaitBits(xEventHamburg, // 事件组句柄
                              START_FLAG,     // 等待开始事件
                              pdFALSE,        // 读取后不清空标志位
                              pdTRUE,         // ADN关系,1个值无所谓
                              portMAX_DELAY);
  printf("[MEAT] 等到开始事件 : %X\n", uxBits);
  vTaskDelay(pdMS_TO_TICKS(random(500,2000)));
  uxBits = xEventGroupSetBits(xEventHamburg, MEAT_FLAG); // 设置肉饼标志位
  printf("[MEAT] 肉饼已就绪 : %X\n", uxBits);
  vTaskDelete(NULL);
}
// 蔬菜服务员
void waiter_vegetable_task(void *param_t){
  EventBits_t uxBits;
  uxBits = xEventGroupWaitBits(xEventHamburg, // 事件组句柄
                              START_FLAG,     // 等待开始事件
                              pdFALSE,        // 读取后不清空标志位
                              pdTRUE,         // ADN关系,1个值无所谓
                              portMAX_DELAY);
  printf("[VEGE] 等到开始事件 : %X\n", uxBits);
  vTaskDelay(pdMS_TO_TICKS(random(500,2000)));
  uxBits = xEventGroupSetBits(xEventHamburg, VEGETABLE_FLAG); // 设置蔬菜标志位
  printf("[VEGE] 蔬菜已就绪 : %X\n", uxBits);
  vTaskDelete(NULL);
}
// 厨师线程
void chef_task(void *param_t){
  pinMode(4, INPUT_PULLUP);
  while(1){
    if(digitalRead(4) == LOW){
      // 开始做汉堡
      EventBits_t uxBits;  // 设置事件标志位的返回值
      uxBits = xEventGroupSetBits(xEventHamburg, START_FLAG);  // 这个返回值有可能会清空标志位,具体读文档
      printf("[CHEF] 开始做汉堡 : %X\n", uxBits);
      uxBits = xEventGroupWaitBits(xEventHamburg,                            // 事件句柄
                                  BREAD_FLAG | MEAT_FLAG | VEGETABLE_FLAG,    // 等待代表面包、肉饼、蔬菜的标志位
                                  pdFALSE,                                    // 是否清空对应标志位
                                  pdTRUE,                                     // 等待的Bits判断关系 True为 AND, False为 OR
                                  portMAX_DELAY);                             // 等待超时时间
      printf("[CHEF] 汉堡做完了 : %X\n", uxBits);
      // 重置事件组
      xEventGroupClearBits(xEventHamburg, START_FLAG | BREAD_FLAG | MEAT_FLAG | VEGETABLE_FLAG);
      uxBits = xEventGroupGetBits(xEventHamburg);
      printf("[CHEF] 汉堡做好了,我下班了 : %X\n", uxBits);
      vTaskDelete(NULL);
    }
    vTaskDelay(100);
  }
}
void setup() {
  Serial.begin(115200);
  xEventHamburg = xEventGroupCreate();  //初始化事件组
  // 启动各个线程
  xTaskCreate(chef_task, "Chef", 1024*2, NULL, 1, NULL);
  xTaskCreate(waiter_bread_task, "Bread", 1024*2, NULL, 1, NULL);
  xTaskCreate(waiter_meat_task, "Meat", 1024*2, NULL, 1, NULL);
  xTaskCreate(waiter_vegetable_task, "Vegetable", 1024*2, NULL, 1, NULL);
}
void loop() {
  delay(100);
}

这个例程基本和第一个例程是一样的,只是我们等待的标志位发生了改变,让所有的服务员都等待 START_FLAG 的信号,而所有人都不得清空这个信号,如果一旦有一个人清空了,那后面有人就会丢失这个信号。

此类表示为的应用方向

还是以智慧物流为例,我们现在的需求是用一辆车运送三个物资到另外的仓库中,当我们下达指令之后,调取物资的机械手并不是顺序运行的,而是各自运行寻找物资,待物资全部装车后车才会启动。这时候就由运货车(或调度台)发送一个“运送”指令,电动车就为等待,三个机械臂分别寻找和装在三种物资到车上,待所有物资都齐全之后,电动车发车。

三号餐厅的厨师(同步)

当这三种配料都准备齐的时候,厨子才开始制作汉堡,而这时候所有的服务员都已经下班走了,厨师就不爽了,凭什么你们做完都走了,我还在工作!
所以,三号餐厅的厨师给老板提了个建议,说要有些服务员的工作太轻松了,下班太早不利于同事间的团结,所以我建议我们必须等到所有人的工作完成之后大家一起下班。
黑心老板也采纳了这个建议,于是,同步就出现了!

代码共享地址:https://wokwi.com/projects/362947317933844481

#define BURG_FLAG       (1 << 0)
#define BREAD_FLAG      (1 << 1)
#define MEAT_FLAG       (1 << 2)
#define VEGETABLE_FLAG  (1 << 3)
EventGroupHandle_t xEventHamburg = NULL;  // 做汉堡的事件组
// 面包服务员
void waiter_bread_task(void *param_t){
  EventBits_t uxBits;
  printf("[BRED] 骂骂咧咧的开始烤面包...\n");
  vTaskDelay(pdMS_TO_TICKS(random(1000,5000)));
  printf("[BRED] 面包烤好了,我打算设置标志位!\n");
  uxBits = xEventGroupSync(xEventHamburg,     // 事件句柄
                          BREAD_FLAG,         // 要设置的标志位
                          BURG_FLAG | BREAD_FLAG | MEAT_FLAG | VEGETABLE_FLAG,  // 同步等待的标志位
                          portMAX_DELAY);     // 超时时间
  printf("[BRED] 面包已就绪 : %X\n", uxBits);
  vTaskDelete(NULL);
}
// 肉饼服务员
void waiter_meat_task(void *param_t){
  EventBits_t uxBits;
  printf("[MEAT] 叽叽歪歪的开始煎肉饼...\n");
  vTaskDelay(pdMS_TO_TICKS(random(1000,5000)));
  printf("[MEAT] 肉饼煎好了,我打算设置标志位!\n");
  uxBits = xEventGroupSync(xEventHamburg,     // 事件句柄
                          MEAT_FLAG,          // 要设置的标志位
                          BURG_FLAG | BREAD_FLAG | MEAT_FLAG | VEGETABLE_FLAG,  // 同步等待的标志位
                          portMAX_DELAY);     // 超时时间
  printf("[MEAT] 肉饼已就绪 : %X\n", uxBits);
  vTaskDelete(NULL);
}
// 蔬菜服务员
void waiter_vegetable_task(void *param_t){
  EventBits_t uxBits;
  printf("[VEGE] 哼哼吱吱的做开始洗蔬菜...\n");
  vTaskDelay(pdMS_TO_TICKS(random(1000,5000)));
  printf("[VEGE] 蔬菜洗好了,我打算设置标志位!\n");
  uxBits = xEventGroupSync(xEventHamburg,     // 事件句柄
                          VEGETABLE_FLAG,     // 要设置的标志位
                          BURG_FLAG | BREAD_FLAG | MEAT_FLAG | VEGETABLE_FLAG,  // 同步等待的标志位
                          portMAX_DELAY);     // 超时时间
  printf("[VEGE] 蔬菜已就绪 : %X\n", uxBits);
  vTaskDelete(NULL);
}
// 厨师线程
void chef_task(void *param_t){
  pinMode(4, INPUT_PULLUP);
  EventBits_t uxBits;
  while(1){
    if(digitalRead(4) == LOW){
      // 开始做汉堡
      printf("[CHEF] 美滋滋的做开始磨洋工...\n");
      uxBits = xEventGroupSync(xEventHamburg,     // 事件句柄
                              BURG_FLAG,          // 要设置的标志位
                              BURG_FLAG | BREAD_FLAG | MEAT_FLAG | VEGETABLE_FLAG,  // 同步等待的标志位
                              portMAX_DELAY);     // 超时时间
      printf("[CHEF] 汉堡做好了,大家可以下班了 : %X\n", uxBits);
      vTaskDelete(NULL);
    }
    vTaskDelay(100);
  }
}
void setup() {
  Serial.begin(115200);
  xEventHamburg = xEventGroupCreate();  //初始化事件组
  // 启动各个线程
  xTaskCreate(chef_task, "Chef", 1024*2, NULL, 1, NULL);
  xTaskCreate(waiter_bread_task, "Bread", 1024*2, NULL, 1, NULL);
  xTaskCreate(waiter_meat_task, "Meat", 1024*2, NULL, 1, NULL);
  xTaskCreate(waiter_vegetable_task, "Vegetable", 1024*2, NULL, 1, NULL);
}
void loop() {
  delay(100);
}

在上面的代码中,我们把原来的 START_FLAG 改成了 BURG_FLAG 表示汉堡就绪的标志位,所有线程启动后就开始忙碌自己的工作了,当忙完之后就把自己的标志位设置成1,同时等待其他四位同时的标志位,只有 chef_task 的线程是等待我们操作按钮的。
如果我们启动后不按按钮,那另外三个线程依然会把自己的事情做完,然后开始等待厨子;
但如果启动后马上按动按钮,那么厨子是会提前把自己的汉堡做完,同时等待另外几个同事的(所以这里的例子在实际生活中可能有些不合理)。
例程中我们用到了一个新的函数,同步函数

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

该函数的作用是 将指定标志位设置为1,同时等待其他标志位到达后继续执行。
xEventGroup :事件句柄;
uxBitsToSet :在确定(且可能等待)uxBitsToWait参数指定的所有位 设置完成之前, 要设置事件组中的一个或多个位。
uxBitsToWaitFor:指定事件组中要测试的一个或多个事件位 的按位值。
xTicksToWait:等待时间。
需要注意的是:uxBitsToSet 参数可以是一个位,也可以是是多个位,使用或运算符(|)拼接,但 uxBitsToSet 的值并不一定非得包含在 uxBitsToWaitFor 中。

同步事件组的应用方向

以动车组为例,动车组和普通火车的区别在于,每节车厢都是一个独立的系统,都各自提供动力运行,所以才叫动车组,所以动车组在运行的时候必须等待所有列车就绪的信号才能发车,这样才能最大的节省能量提高效率。
动车组在开车前,先要装在每节车厢的乘客,然后各自关门,当1号车厢的门关闭后,发出一个同步信号,并等待其他车厢就绪,陆续所有车厢的门关闭之后,大家一同出发。
需要注意的是,每节车厢关门就绪的时间是不同的,但最终所有车厢是需要在同一时间发车的,所有车厢是并行的,如果再用标志位的方式,就无法实现了(或者实现起来很啰嗦),所以就用到了 同步 的概念。

关于事件组的所有API,可以参考:https://www.freertos.org/zh-cn-cmn-s/event-groups-API.html

  • 19
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值