11. 事件

11.1. 事件的基本概念

事件是一种实现任务间通信的机制,主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输。 与信号量不同的是,它可以实现一对多,多对多的同步。 即一个任务可以等待多个事件的发生:可以是任意一个事件发生时唤醒任务进行事件处理; 也可以是几个事件都发生后才唤醒任务进行事件处理。同样,也可以是多个任务同步多个事件。

每一个事件组只需要很少的RAM空间来保存事件组的状态。事件组存储在一个EventBits_t类型的变量中,该变量在事件组结构体中定义。 如果宏configUSE_16_BIT_TICKS(STM32CubeIDE默认定义为0)定义为1,那么变量uxEventBits就是16位的,其中有8个位用来存储事件组; 而如果宏configUSE_16_BIT_TICKS定义为0,那么变量uxEventBits就是32位的,其中有24个位用来存储事件组。 在STM32中,我们一般将configUSE_16_BIT_TICKS定义为0,那么uxEventBits是32位的,有24个位用来实现事件标志组。 每一位代表一个事件,任务通过“逻辑与”或“逻辑或”与一个或多个事件建立关联,形成一个事件组。 事件的“逻辑或”也被称作是独立型同步,指的是任务感兴趣的所有事件任一件发生即可被唤醒; 事件“逻辑与”则被称为是关联型同步,指的是任务感兴趣的若干事件都发生时才被唤醒,并且事件发生的时间可以不同步。

多任务环境下,任务、中断之间往往需要同步操作,一个事件发生会告知等待中的任务,即形成一个任务与任务、中断与任务间的同步。 事件可以提供一对多、多对多的同步操作。一对多同步模型:一个任务等待多个事件的触发,这种情况是比较常见的; 多对多同步模型:多个任务等待多个事件的触发。

任务可以通过设置事件位来实现事件的触发和等待操作。FreeRTOS的事件仅用于同步,不提供数据传输功能。

FreeRTOS提供的事件具有如下特点:

  • 事件只与任务相关联,事件相互独立,一个32位的事件集合(EventBits_t类型的变量,实际可用与表示事件的只有24位), 用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0表示该事件类型未发生、1表示该事件类型已经发生),一共24种事件类型。

  • 事件仅用于同步,不提供数据传输功能。

  • 事件无排队性,即多次向任务设置同一事件(如果任务还未来得及读走),等效于只设置一次。

  • 允许多个任务对同一事件进行读写操作。

  • 支持事件等待超时机制。

在FreeRTOS事件中,每个事件获取的时候,用户可以选择感兴趣的事件,并且选择读取事件信息标记,它有三个属性,分别是逻辑与, 逻辑或以及是否清除标记。当任务等待事件同步时,可以通过任务感兴趣的事件位和事件信息标记来判断当前接收的事件是否满足要求, 如果满足则说明任务等待到对应的事件,系统将唤醒等待的任务;否则,任务会根据用户指定的阻塞超时时间继续等待下去。

11.2. 事件的应用场景

FreeRTOS的事件用于事件类型的通信,无数据传输,也就是说,我们可以用事件来做标志位,判断某些事件是否发生了, 然后根据结果做处理,那很多人又会问了,为什么我不直接用变量做标志呢,岂不是更好更有效率?非也非也,若是在裸机编程中, 用全局变量是最为有效的方法,这点我不否认,但是在操作系统中,使用全局变量就要考虑以下问题了:

  • 如何对全局变量进行保护呢,如何处理多任务同时对它进行访问?

  • 如何让内核对事件进行有效管理呢?使用全局变量的话,就需要在任务中轮询查看事件是否发送,这简直就是在浪费CPU资源啊, 还有等待超时机制,使用全局变量的话需要用户自己去实现。

所以,在操作系统中,还是使用操作系统给我们提供的通信机制就好了,简单方便还实用。

在某些场合,可能需要多个时间发生了才能进行下一步操作,比如一些危险机器的启动,需要检查各项指标, 当指标不达标的时候,无法启动,但是检查各个指标的时候,不能一下子检测完毕啊,所以,需要事件来做统一的等待, 当所有的事件都完成了,那么机器才允许启动,这只是事件的其中一个应用。

事件可使用于多种场合,它能够在一定程度上替代信号量,用于任务与任务间,中断与任务间的同步。 一个任务或中断服务例程发送一个事件给事件对象,而后等待的任务被唤醒并对相应的事件进行处理。 但是它与信号量不同的是,事件的发送操作是不可累计的,而信号量的释放动作是可累计的。事件另外一个特性是, 接收任务可等待多种事件,即多个事件对应一个任务或多个任务。同时按照任务等待的参数,可选择是“逻辑或”触发还是“逻辑与”触发。 这个特性也是信号量等所不具备的,信号量只能识别单一同步动作,而不能同时等待多个事件的同步。

各个事件可分别发送或一起发送给事件对象,而任务可以等待多个事件,任务仅对感兴趣的事件进行关注。 当有它们感兴趣的事件发生时并且符合感兴趣的条件,任务将被唤醒并进行后续的处理动作。

11.3. 事件运作机制

接收事件时,可以根据感兴趣的参事件类型接收事件的单个或者多个事件类型。事件接收成功后, 必须使用xClearOnExit选项来清除已接收到的事件类型,否则不会清除已接收到的事件,这样就需要用户显式清除事件位。 用户可以自定义通过传入参数xWaitForAllBits选择读取模式,是等待所有感兴趣的事件还是等待感兴趣的任意一个事件。

设置事件时,对指定事件写入指定的事件类型,设置事件集合的对应事件位为1,可以一次同时写多个事件类型,设置事件成功可能会触发任务调度。

清除事件时,根据入参数事件句柄和待清除的事件类型,对事件对应位进行清0操作。事件不与任务相关联,事件相互独立, 一个32位的变量(事件集合,实际用于表示事件的只有24位),用于标识该任务发生的事件类型, 其中每一位表示一种事件类型(0表示该事件类型未发生、1表示该事件类型已经发生),一共24种事件类型具体如下。

事件集合set(一个32位的变量)

图18‑1事件集合set(一个32位的变量)

事件唤醒机制,当任务因为等待某个或者多个事件发生而进入阻塞态,当事件发生的时候会被唤醒,其过程具体如下。

事件唤醒任务示意图

任务1对事件3或事件5感兴趣(逻辑或),当发生其中的某一个事件都会被唤醒,并且执行相应操作。 而任务2对事件3与事件5感兴趣(逻辑与),当且仅当事件3与事件5都发生的时候,任务2才会被唤醒, 如果只有一个其中一个事件发生,那么任务还是会继续等待事件发生。如果接在收事件函数中设置了清除事件位, 那么当任务唤醒后将把事件3和事件5的事件标志清零,否则事件标志将依然存在。

11.4. 事件控制块

cmsis_os2.h
1
2
3
4
5
6
typedef struct {
   const char                   *name;   //事件名
   uint32_t                 attr_bits;   //FreeRTOS未使用到
   void                      *cb_mem;    //控制块(静态创建)内存
   uint32_t                   cb_size;   //控制块(静态创建)大小
} osEventFlagsAttr_t;

其中cb_mem、cb_size成员变量用于信号量的静态创建, 当不对这两个成员变量进行赋值时将使用动态内存分配的方式创建信号量。

11.5. 事件函数接口讲解

11.5.1. 事件创建函数osEventFlagsNew()

osEventFlagsNew()用于创建一个事件组,并返回对应的事件组ID。 其函数源码如下

cmsis_os2.c
 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
osEventFlagsId_t osEventFlagsNew (const osEventFlagsAttr_t *attr) {
   EventGroupHandle_t hEventGroup;
   int32_t mem;

   hEventGroup = NULL;
   //不能再中断中使用osEventFlagsNew函数
   if (!IS_IRQ()) {
      mem = -1;

      if (attr != NULL) {
         if ((attr->cb_mem != NULL) && (attr->cb_size >= sizeof(StaticEventGroup_t))) {
            mem = 1; //静态创建事件标志
         }
         else {
            if ((attr->cb_mem == NULL) && (attr->cb_size == 0U)) {
               mem = 0;      //动态创建事件标志
            }
         }
      }
      else {
            mem = 0;
      }

      if (mem == 1) {
         hEventGroup = xEventGroupCreateStatic (attr->cb_mem); //静态创建事件
      }
      else {
         if (mem == 0) {
            hEventGroup = xEventGroupCreate(); //动态创建事件
         }
      }
   }

   return ((osEventFlagsId_t)hEventGroup);
}

参数

  • attr :描述事件组属性的结构体

返回值: 若创建成功则返回事件组ID,用于访问创建的事件组。创建失败则返回NULL

11.5.2. 事件删除函数osEventFlagsDelete()

在很多场合,某些事件只用一次的,就好比在事件应用场景说的危险机器的启动,假如各项指标都达到了, 并且机器启动成功了,那这个事件之后可能就没用了,那就可以进行销毁了。 想要删除事件怎么办?CMSIS-RTOS给我们提供了一个删除事件的函数——osEventFlagsDelete(), 使用它就能将事件进行删除了。当系统不再使用事件对象时,可以通过删除事件对象控制块来释放系统资源, 其函数源码如下

cmsis_os2.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
osStatus_t osEventFlagsDelete (osEventFlagsId_t ef_id) {
   EventGroupHandle_t hEventGroup = (EventGroupHandle_t)ef_id;
   osStatus_t stat;

#ifndef USE_FreeRTOS_HEAP_1
   //不能够在中断中使用osEventFlagsDelete函数
   if (IS_IRQ()) {
      stat = osErrorISR;
   }
   else if (hEventGroup == NULL) {
      stat = osErrorParameter;
   }
   else {
      stat = osOK;
      vEventGroupDelete (hEventGroup);
   }
#else
   stat = osError;
#endif

   return (stat);
}

参数

  • ef_id :事件组ID

返回值:当成功时返回osOK,失败返回负值。

11.5.3. 事件组置位函数osEventFlagsSet()

xEventGroupSetBits()用于置位事件组中指定的位,当位被置位之后,阻塞在该位上的任务将会被解锁。 使用该函数接口时,通过参数指定的事件标志来设定事件的标志位,然后遍历等待在事件对象上的事件等待列表, 判断是否有任务的事件激活要求与当前事件对象标志值匹配,如果有,则唤醒该任务。简单来说, 就是设置我们自己定义的事件标志位为 1,并且看看有没有任务在等待这个事件,有的话就唤醒它。

cmsis_os2.c
 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
uint32_t osEventFlagsSet (osEventFlagsId_t ef_id, uint32_t flags) {
   EventGroupHandle_t hEventGroup = (EventGroupHandle_t)ef_id;
   uint32_t rflags;
   BaseType_t yield;

   //参数判断,能够设置事件位为24位,超过则直接返回失败
   if ((hEventGroup == NULL) || ((flags & EVENT_FLAGS_INVALID_BITS) != 0U)) {
      rflags = (uint32_t)osErrorParameter;
   }
   //在中断中设置事件
   else if (IS_IRQ()) {
      yield = pdFALSE;

      if (xEventGroupSetBitsFromISR (hEventGroup, (EventBits_t)flags, &yield) == pdFAIL) {
         rflags = (uint32_t)osErrorResource;
      } else {
         rflags = flags;
         portYIELD_FROM_ISR (yield);
      }
   }
   //在线程中设置事件
   else {
      rflags = xEventGroupSetBits (hEventGroup, (EventBits_t)flags);
   }

   return (rflags);
}

参数

  • ef_id :事件组ID

  • flags :指定事件中的事件标志位

返回值:当成功时返回事件组中的值,失败返回负值。

11.5.4. 等待事件函数osEventFlagsWait()

既然标记了事件的发生,那么我怎么知道他到底有没有发生,这也是需要一个函数来获取事件是否已经发生, CMSIS-RTOS提供了一个等待指定事件的函数——osEventFlagsWait(),通过这个函数, 任务可以知道事件标志组中的哪些位,有什么事件发生了,然后通过 “逻辑与”、“逻辑或”等操作对感兴趣的事件进行获取, 并且这个函数实现了等待超时机制,当且仅当任务等待的事件发生时,任务才能获取到事件信息。在这段时间中, 如果事件一直没发生,该任务将保持阻塞状态以等待事件发生。当其它任务或中断服务程序往其等待的事件设置对应的标志位, 该任务将自动由阻塞态转为就绪态。当任务等待的时间超过了指定的阻塞时间,即使事件还未发生, 任务也会自动从阻塞态转移为就绪态。这样子很有效的体现了操作系统的实时性, 如果事件正确获取(等待到)则返回对应的事件标志位,由用户判断再做处理, 因为在事件超时的时候也会返回一个不能确定的事件值,所以需要判断任务所等待的事件是否真的发生。 其函数源码如下

cmsis_os2.c
 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
uint32_t osEventFlagsWait (osEventFlagsId_t ef_id, uint32_t flags, uint32_t options, uint32_t timeout) {
   EventGroupHandle_t hEventGroup = (EventGroupHandle_t)ef_id;
   BaseType_t wait_all;
   BaseType_t exit_clr;
   uint32_t rflags;

   //参数判断
   if ((hEventGroup == NULL) || ((flags & EVENT_FLAGS_INVALID_BITS) != 0U)) {
      rflags = (uint32_t)osErrorParameter;
   }
   //不能够在中断中使用osEventFlagsWait函数
   else if (IS_IRQ()) {
      rflags = (uint32_t)osErrorISR;
   }

   else {
      //等待事件全部发送
      if (options & osFlagsWaitAll) {
         wait_all = pdTRUE;
      } else {
         wait_all = pdFAIL;
      }
      //不清除指定等待的标志
      if (options & osFlagsNoClear) {
         exit_clr = pdFAIL;
      } else {
         exit_clr = pdTRUE;
      }

      //调用FreeRTOS等待事件函数
      //返回事件中的哪些事件标志位被置位,返回值很可能并不是用户指定的事件位,需要对返回值进行判断再处理
      rflags = xEventGroupWaitBits (hEventGroup, (EventBits_t)flags, exit_clr, wait_all, (TickType_t)timeout);

      //根据获取到的事件判断
      if (options & osFlagsWaitAll) {
         if (flags != rflags) {
            if (timeout > 0U) {
               rflags = (uint32_t)osErrorTimeout;
            } else {
               rflags = (uint32_t)osErrorResource;
            }
         }
      }
      else {
         if ((flags & rflags) == 0U) {
            if (timeout > 0U) {
               rflags = (uint32_t)osErrorTimeout;
            } else {
               rflags = (uint32_t)osErrorResource;
            }
         }
      }
   }
   return (rflags);
}

参数

  • ef_id :事件组ID

  • flags :指定事件中的事件标志位

  • options

  • timeout :线程超时等待时间

返回值:当成功时返回置位前事件中的值,失败返回负值。

11.5.5. 清除事件函数osEventFlagsClear()

osEventFlagsClear()函数用于清除事件组指定的位,如果在获取事件的时候没有将对应的标志位清除, 那么就需要用这个函数来进行显式清除, 其函数源码如下

cmsis_os2.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
uint32_t osEventFlagsClear (osEventFlagsId_t ef_id, uint32_t flags) {
   EventGroupHandle_t hEventGroup = (EventGroupHandle_t)ef_id;
   uint32_t rflags;

   //参数判断
   if ((hEventGroup == NULL) || ((flags & EVENT_FLAGS_INVALID_BITS) != 0U)) {
      rflags = (uint32_t)osErrorParameter;
   }
   //在中断中清除事件
   else if (IS_IRQ()) {
      rflags = xEventGroupGetBitsFromISR (hEventGroup);

      if (xEventGroupClearBitsFromISR (hEventGroup, (EventBits_t)flags) == pdFAIL) {
         rflags = (uint32_t)osErrorResource;
      }
   }
   //在线程中清除事件
   else {
      rflags = xEventGroupClearBits (hEventGroup, (EventBits_t)flags);
   }

   return (rflags);
}
  • ef_id :事件组ID

  • flags :指定事件中的事件标志位

返回值:当成功时返回事件在还没有清除指定位之前的值,失败返回负值。

11.6. 事件实验

事件标志组实验是在FreeRTOS中创建了两个线程,一个是设置事件线程,一个是等待事件线程, 设置事件线程通过检测按键的按下情况设置不同的事件标志位,等待事件线程则获取这两个事件标志位, 并且判断两个事件是否都发生,如果是则输出相应信息,LED进行翻转。创建两个线程,其配置如下

等待事件线程LEDTASK

等待事件线程

设置事件线程KEYTASK

设置事件线程

添加一个事件组,其配置如下

事件组配置

与事件相关的配置选项并不多,此处我们只需要改个变量名即可

  • Semaphore Name :创建的事件变量名

  • Allocation :创建事件的方式,可选择使用静态创建还是使用动态内存分配。

  • Control Block Name 使用静态创建时事件内存控制块类型变量名。

使用STM32CubeIDE配置完FreeRTOS之后便可生成工程代码。 生成与FreeRTOS相关的代码在app_freertos.c文件中。

11.6.1. app_freertos.c

本小节只讲解重点部分代码,完整代码请打开工程查看。

11.6.1.1. MX_FREERTOS_Init函数

app_freertos.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void MX_FREERTOS_Init(void) {

   printf("野火STM32MP157 CM4 FreeRTOS 事件组实验\n");

   LEDTaskHandle = osThreadNew(LED_Task, NULL, &LEDTask_attributes);

   KEYTaskHandle = osThreadNew(KEY_Task, NULL, &KEYTask_attributes);

   Event01Handle = osEventFlagsNew(&Event01_attributes);

}

MX_FREERTOS_Init函数内容主要由STM32CubeIDE自动生成,只手动添加了一句打印函数,用于程序运行后串口输出。

11.6.1.2. LED_Task

app_freertos.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#define KEY1_EVENT  (0x01 << 0)//设置事件掩码的位0
#define KEY2_EVENT  (0x01 << 1)//设置事件掩码的位1

void LED_Task(void *argument)
{

   for(;;)
   {
      // 永远等待按键1和按键2事件均发生过
      osEventFlagsWait(Event01Handle,KEY1_EVENT|KEY2_EVENT,osFlagsWaitAll,osWaitForever);
      printf ( "KEY1与KEY2都按下\n");
      LED1_TOGGLE;       //LED1反转

      osDelay(200);
   }

}

LED_Task线程将一直在等待事件的发生,当等待事件KEY1_EVENT与KEY2_EVENT均发生时,进行翻转LED。

11.6.1.3. KEY_Task

app_freertos.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void KEY_Task(void *argument)
{
   for(;;)
   {

      if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )       //如果KEY2被单击
      {
         printf ( "KEY1被按下\n" );
         /* 触发一个事件1 */
         osEventFlagsSet(Event01Handle,KEY1_EVENT);
      }

      if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )       //如果KEY2被单击
      {
         printf ( "KEY2被按下\n" );
         /* 触发一个事件2 */
         osEventFlagsSet(Event01Handle,KEY2_EVENT);
      }
      osDelay(20);     //每20ms扫描一次   (20);
   }
}

当按键KEY1被按下时将触发KEY1_EVENT事件,当按键KEY2被按下时将触发KEY2_EVENT事件。

11.6.1.4. main函数

main.c
 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
int main(void)
{

   HAL_Init();               //初始化时钟

   if(IS_ENGINEERING_BOOT_MODE())
   {
      /* Configure the system clock */
      SystemClock_Config();  //时钟配置
   }


   MX_GPIO_Init();            //初始化GPIO
   MX_USART3_UART_Init(); //初始化串口

   osKernelInitialize();  //初始化内核状态
   MX_FREERTOS_Init();        //创建任务

   osKernelStart();          //启动任务调度器,

   //当成功启动任务调度器之后以下代码将不会被执行
   while (1)
   {

   }

}

11.7. 事件实验现象

当程序开始运行时,当个按键均被按下时,LED_Task在将会打印”KEY1与KEY2都按下”,并翻转LED1。 串口打印信息如下图所示

事件实验现象