实时操作系统-9-[FreeRTOS]事件

基本概念

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

每一个事件组只需要很少的 RAM 空间来保存事件组的状态。事件组存储 在一个 EventBits_t 类 型 的变量中 , 该变量在事件组结构体中定义 。

  • 如果宏 configUSE_16_BIT_TICKS 定义为 1,那么变量 uxEventBits 就是 16 位的,其中有 8 个位用来存储事件组;
  • 如果宏 configUSE_16_BIT_TICKS 定义为 0,那么变量 uxEventBits 就是 32 位的 , 其中有 24 个位用来存储事件组 。

在 STM32 中 , 我们一般将 configUSE_16_BIT_TICKS 定义为 0,那么 uxEventBits 是 32 位的,有 24 个位用来实现事件标志组。每一位代表一个事件,任务通过“逻辑与”或“逻辑或”与一个或多个事件建立关联,形成一个事件组。

  • 事件的“逻辑或”也被称作是独立型同步,指的是任务感兴趣的所有事件任一件发生即可被唤醒;
  • 事件“逻辑与”则被称为是关联型同步,指的是任务感兴趣的若干事件都发生时才被唤醒,并且事件发生的时间可以不同步。

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

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

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

  1. 事件只与任务相关联,事件相互独立,一个 32 位的事件集合(EventBits_t 类型的变量,实际可用与表示事件的只有 24位),用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0 表示该事件类型未发生、1 表示该事件类型已经发生),一共 24 种事件类型。
  2. 事件仅用于同步,不提供数据传输功能。
  3. 事件无排队性,即多次向任务设置同一事件(如果任务还未来得及读走),等效于只设置一次。
  4. 允许多个任务对同一事件进行读写操作。
  5. 支持事件等待超时机制。

应用场景

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

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

运作机制

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

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

清除事件时,根据入参数事件句柄和待清除的事件类型,对事件对应位进行清 0 操作。

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

事件唤醒机制,当任务因为等待某个或者多个事件发生而进入阻塞态,当事件发生的时候会被唤醒:
image

任务 1 对事件 3 或事件 5 感兴趣(逻辑或),当发生其中的某一个事件都会被唤醒,并且执行相应操作。

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

控制块

事件标志组存储在一个 EventBits_t 类型的变量中,该变量在事件组结构体中定义:

typedef struct xEventGroupDefinition {
    EventBits_t uxEventBits;               /* 当前事件标志位(32-bit变量) */
    List_t xTasksWaitingForBits;           /* 等待事件位的任务链表 */
 
#if (configUSE_TRACE_FACILITY == 1)
    UBaseType_t uxEventGroupNumber;        /* 调试用的事件组编号 */
#endif
 
#if ( (configSUPPORT_STATIC_ALLOCATION == 1) && \
      (configSUPPORT_DYNAMIC_ALLOCATION == 1) )
    uint8_t ucStaticallyAllocated;         /* 标识是否静态分配:pdTRUE/pdFALSE */
#endif
} EventGroup_t;
  • 如果宏 configUSE_16_BIT_TICKS 定义为 1,那么变量 uxEventBits 就是 16 位的,其中有 8 个位用来存储事件组;
  • 如果宏 configUSE_16_BIT_TICKS 定义为 0,那么变量 uxEventBits 就是 32 位的 , 其中有 24 个位用来存储事件组 。

事件函数接口

事件创建函数 xEventGroupCreate()

xEventGroupCreate()用于创建一个事件组,并返回对应的句柄。要想使用该函数必须在头文件 FreeRTOSConfig.h 定义宏 configSUPPORT_DYNAMIC_ALLOCATION 为 1(在FreeRTOS.h 中默认定义为 1)且需要把 FreeRTOS/source/event_groups.c 这个 C 文件添加到工程中。

  • 如果使用函数 xEventGroupCreate()来创建一个事件,那么需要的 RAM 是动态分配的。
  • 如果使用函数xEventGroupCreateStatic()来创建一个事件,那么需要的 RAM 是静态分配的。

xEventGroupCreate()源码:

#if (configSUPPORT_DYNAMIC_ALLOCATION == 1)
 
EventGroupHandle_t xEventGroupCreate(void)
{
    EventGroup_t *pxEventBits;
 
    /* 动态分配事件控制块内存 */
    pxEventBits = (EventGroup_t *)pvPortMalloc(sizeof(EventGroup_t));  // (1)
 
    if (pxEventBits != NULL) {                                        // (2)
        /* 初始化事件标志位 */
        pxEventBits->uxEventBits = 0;
        
        /* 初始化等待任务链表 */
        vListInitialise(&(pxEventBits->xTasksWaitingForBits));
 
        #if (configSUPPORT_STATIC_ALLOCATION == 1)
        {
            /* 标记为动态分配(与静态分配区分) */
            pxEventBits->ucStaticallyAllocated = pdFALSE;             // (3)
        }
        #endif
 
        traceEVENT_GROUP_CREATE(pxEventBits);                        // (4)
    } else {
        traceEVENT_GROUP_CREATE_FAILED();                            // (5)
    }
 
    return (EventGroupHandle_t)pxEventBits;                          // (6)
}
 
#endif /* configSUPPORT_DYNAMIC_ALLOCATION */

image

举个例子
首先进行头文件包含:

#include "event_groups.h"

并且在头文件 FreeRTOSConfig.h 定义宏 configSUPPORT_DYNAMIC_ALLOCATION 为 1,也就是动态内存分配,我们已经设成1了因此不用动:
image

在使用创建函数前我们需要有一个事件的句柄,以便后续访问:

static EventGroupHandle_t Event_Handle =NULL;

然后在任务创建函数AppTaskCreate()调用事件创建函数:

//任务创建函数
static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  
  taskENTER_CRITICAL();           //进入临界区
 
  /* 创建 Event_Handle */
  Event_Handle = xEventGroupCreate();	 
  if(NULL != Event_Handle)
    printf("Event_Handle 事件创建成功!\r\n");  
	else
    printf("Event_Handle 事件创建失败!\r\n");  		
  
  vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
  
  taskEXIT_CRITICAL();            //退出临界区
}

完整main函数代码:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
 
#include "LED.h"
#include "Usart.h"
#include "Key.h"  
 
/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "event_groups.h"
 
/* 
 * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
 * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
 * 这个句柄可以为NULL。
 */
static TaskHandle_t AppTaskCreate_Handle = NULL; /* 创建任务句柄 */
 
/********************************** 内核对象句柄 *********************************/
/*
 * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
 * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
 * 们就可以通过这个句柄操作这些内核对象。
 *
 * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
 * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
 * 来完成的
 * 
 */
static EventGroupHandle_t Event_Handle =NULL;
 
/******************************* 宏定义 ************************************/
/*
 * 当我们在写应用程序的时候,可能需要用到一些宏定义。
 */
 
 
//一些函数声明
static void AppTaskCreate(void);/* 用于创建任务 */
static void All_Function_Init(void);/* 用于初始化板载相关资源 */
 
int main(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
 
	All_Function_Init();//硬件初始化
	
	while (1)
	{
		 /* 创建AppTaskCreate任务 */
		xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
													(const char*    )"AppTaskCreate",/* 任务名字 */
													(uint16_t       )512,  /* 任务栈大小 */
													(void*          )NULL,/* 任务入口函数参数 */
													(UBaseType_t    )1, /* 任务的优先级 */
													(TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
		/* 启动任务调度 */           
		if(pdPASS == xReturn)
			vTaskStartScheduler();   /* 启动任务,开启调度 */
		else
			return -1;  
	}
}
 
//任务创建函数
static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  
  taskENTER_CRITICAL();           //进入临界区
 
  /* 创建 Event_Handle */
  Event_Handle = xEventGroupCreate();	 
  if(NULL != Event_Handle)
    printf("Event_Handle 事件创建成功!\r\n");  
	else
    printf("Event_Handle 事件创建失败!\r\n");  		
  
  vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
  
  taskEXIT_CRITICAL();            //退出临界区
}
 
 
//初始化声明
static void All_Function_Init(void)
{
	/*
	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
	 * 都统一用这个优先级分组,千万不要再分组,切忌。
	 */
	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
	
	/* LED 初始化 */
	LED_GPIO_Config();
 
	/* 串口初始化	*/
	USART_Config();
 
	//按键初始化
	Key_GPIO_Config();
  
}

运行结果:
image

事件删除函数 vEventGroupDelete()

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

/*-----------------------------------------------------------*/
void vEventGroupDelete(EventGroupHandle_t xEventGroup)
{
    EventGroup_t *pxEventBits = (EventGroup_t *)xEventGroup;
    const List_t *pxTasksWaitingForBits = &(pxEventBits->xTasksWaitingForBits);
 
    vTaskSuspendAll();  /* (1) 挂起所有任务,因为接下来的操作不知道需要多长的时间,并且在删除的时候,不希望其他任务来操作这个事件标志组,所以暂时把调度器挂起,让当前任务占有 CPU。 */
    {
        traceEVENT_GROUP_DELETE(xEventGroup);
 
        /* (2) 处理所有等待该事件的任务 */
        while (listCURRENT_LIST_LENGTH(pxTasksWaitingForBits) > (UBaseType_t)0) 
        {
            configASSERT(pxTasksWaitingForBits->xListEnd.pxNext != 
                       (ListItem_t *)&(pxTasksWaitingForBits->xListEnd));
 
            /* (3) 将任务移出事件等待列表 */
            (void)xTaskRemoveFromUnorderedEventList(
                pxTasksWaitingForBits->xListEnd.pxNext,
                eventUNBLOCKED_DUE_TO_BIT_SET);
        }
 
        #if ((configSUPPORT_DYNAMIC_ALLOCATION == 1) && \
             (configSUPPORT_STATIC_ALLOCATION == 0))
        {
            /* (4) 释放动态分配的内存 */
            vPortFree(pxEventBits);
        }
        #endif
 
        /* 静态分配处理部分已省略 */
    }
    (void)xTaskResumeAll();  /* (5) 恢复任务调度 */
}
/*-----------------------------------------------------------*/

vEventGroupDelete()用于删除由函数 xEventGroupCreate()创建的事件组,只有被创建成功的事件才能被删除,但是需要注意的是该函数不允许在中断里面使用。当事件组被删除之后,阻塞在该事件组上的任务都会被解锁,并向等待事件的任务返回事件组的值为 0。

这里在之前代码的基础上对AppTaskCreate()进行修改,其实只要添加一句话就够了:

		vEventGroupDelete(Event_Handle);

不过为了更容易看到实验现象,增加一些验证:

		Event_Handle = NULL;              // 手动置空句柄
 
		// 验证是否删除
		if (Event_Handle == NULL) 
		{
				printf("事件组已删除!\r\n");
		} 
		else 
		{
				printf("事件组删除失败!\r\n");
		}

如果事件组已被删除,再次操作(如 xEventGroupSetBits()下面会讲,这里先使用验证一下)会导致内存错误或断言失败:

		// 尝试操作已删除的事件组(验证是否崩溃)
		xEventGroupSetBits(Event_Handle, 0x01);  // 如果已删除,可能会触发断言或 HardFault

完整更改代码:

//任务创建函数
static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  
  taskENTER_CRITICAL();           //进入临界区
 
  /* 创建 Event_Handle */
  Event_Handle = xEventGroupCreate();	 
  if(NULL != Event_Handle)
	{
    printf("Event_Handle 事件创建成功!\r\n"); 
		vEventGroupDelete(Event_Handle);
 
		Event_Handle = NULL;              // 手动置空句柄
 
		// 验证是否删除
		if (Event_Handle == NULL) 
		{
				printf("事件组已删除!\r\n");
		} 
		else 
		{
				printf("事件组删除失败!\r\n");
		}
 
		// 尝试操作已删除的事件组(验证是否崩溃)
		xEventGroupSetBits(Event_Handle, 0x01);  // 如果已删除,可能会触发断言或 HardFault
	}
	else
    printf("Event_Handle 事件创建失败!\r\n");  		
  
  vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
  
  taskEXIT_CRITICAL();            //退出临界区
}

运行结果,会发现事件删除后,再去调用会出现错误:
image

事件组置位函数 xEventGroupSetBits()(非中断)

xEventGroupSetBits()用于置位事件组中指定的位,当位被置位之后,阻塞在该位上的任务将会被解锁。使用该函数接口时,通过参数指定的事件标志来设定事件的标志位,然后遍历等待在事件对象上的事件等待列表,判断是否有任务的事件激活要求与当前事件对象标志值匹配,如果有,则唤醒该任务。

简单来说,就是设置我们自己定义的事件标志位为 1,并且看看有没有任务在等待这个事件,有的话就唤醒它。

注意的是该函数不允许在中断中使用。

image

函数源码:

/*-----------------------------------------------------------*/
EventBits_t xEventGroupSetBits(EventGroupHandle_t xEventGroup,
                              const EventBits_t uxBitsToSet)
{
    ListItem_t *pxListItem, *pxNext;
    ListItem_t const *pxListEnd;
    List_t *pxList;
    EventBits_t uxBitsToClear = 0, uxBitsWaitedFor, uxControlBits;
    EventGroup_t *pxEventBits = (EventGroup_t *)xEventGroup;
    BaseType_t xMatchFound = pdFALSE;
 
    /* 参数有效性检查 */
    configASSERT(xEventGroup);
    configASSERT((uxBitsToSet & eventEVENT_BITS_CONTROL_BYTES) == 0);  /* (1) */
 
    pxList = &(pxEventBits->xTasksWaitingForBits);
    pxListEnd = listGET_END_MARKER(pxList);
 
    vTaskSuspendAll();  /* (2) 挂起所有任务 */
    {
        traceEVENT_GROUP_SET_BITS(xEventGroup, uxBitsToSet);
 
        /* 设置事件标志位 */
        pxEventBits->uxEventBits |= uxBitsToSet;  /* (3)根据用户指定的 uxBitsToSet 设置事件标志位。 */
 
        /* 遍历等待事件的任务列表 */
        pxListItem = listGET_HEAD_ENTRY(pxList);
        while (pxListItem != pxListEnd) {  /* (4)设置这个事件标志位可能是某个任务在等待的事件,就需要遍历等待事件列表中的任务,看看这个事件是否与任务等待的事件匹配。 */
            pxNext = listGET_NEXT(pxListItem);
            uxBitsWaitedFor = listGET_LIST_ITEM_VALUE(pxListItem);
            xMatchFound = pdFALSE;
 
            /* 解析等待条件,是逻辑与还是逻辑或 */
            uxControlBits = uxBitsWaitedFor & eventEVENT_BITS_CONTROL_BYTES;  /* (5) 获取要等待事件的标记信息,是逻辑与还是逻辑或*/
            uxBitsWaitedFor &= ~eventEVENT_BITS_CONTROL_BYTES;  /* (6) 再获取任务的等待事件是什么*/
 
            /* 检查事件匹配条件 */
            if ((uxControlBits & eventWAIT_FOR_ALL_BITS) == (EventBits_t)0) {  /* (7) */
                /* 任意位匹配模式 */
                if ((uxBitsWaitedFor & pxEventBits->uxEventBits) != (EventBits_t)0) {
                    xMatchFound = pdTRUE;  /* (8) 判断要等待的事件是否发生了,发生了就需要把任务恢复,在这里记录一下要恢复的任务。*/
                } else {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else if ((uxBitsWaitedFor & pxEventBits->uxEventBits) == uxBitsWaitedFor) {  /* (9) 如果任务等待的事件都要发生的时候(也是我们常说的“逻辑与”),就需要就要所有判断事件标志组中的事件是否都发生,如果是的话任务才能从阻塞中恢复,同样也需要标记一下要恢复的任务。*/
                /* 全部位匹配模式 */
                xMatchFound = pdTRUE;
            } else {
                /* 不满足全部位条件 */  /* (10) */
            }
 
            /* 处理匹配成功的任务 */
            if (xMatchFound != pdFALSE) {  /* (11) */
                /* 检查是否需要清除事件位 */
                if ((uxControlBits & eventCLEAR_EVENTS_ON_EXIT_BIT) != (EventBits_t)0) {
                    uxBitsToClear |= uxBitsWaitedFor;  /* (12)运用或运算,标记一下要清除的事件标志位是哪些。 */
                } else {
                    mtCOVERAGE_TEST_MARKER();
                }
 
                /* 将任务移出等待列表 */
                (void)xTaskRemoveFromUnorderedEventList(
                    pxListItem,
                    pxEventBits->uxEventBits | eventUNBLOCKED_DUE_TO_BIT_SET);  /* (13) 将满足事件条件的任务从等待列表中移除,并且添加到就绪列表*/
            }
 
            pxListItem = pxNext;  /* (14) 移至下一任务 */
        }
 
        /* 清除需要复位的事件位 */
        pxEventBits->uxEventBits &= ~uxBitsToClear;  /* (15) */
    }
    (void)xTaskResumeAll();  /* (16) 恢复任务调度 */
 
    return pxEventBits->uxEventBits;  /* (17) 返回当前事件位 */
}
/*-----------------------------------------------------------*/

(1):断言,判断要设置的事件标志位是否有效,因为一个 32 位的事件标志组变量只有 24 位是用于设置事件的,而 16 位的事件标志组变量只有 8 位用于设置事件,高 8 位不允许设置事件,有其他用途,事件组高8位的用途:

#if configUSE_16_BIT_TICKS == 1
#define eventCLEAR_EVENTS_ON_EXIT_BIT 0x0100U
#define eventUNBLOCKED_DUE_TO_BIT_SET 0x0200U
#define eventWAIT_FOR_ALL_BITS 0x0400U
#define eventEVENT_BITS_CONTROL_BYTES 0xff00U
#else
#define eventCLEAR_EVENTS_ON_EXIT_BIT 0x01000000UL
#define eventUNBLOCKED_DUE_TO_BIT_SET 0x02000000UL
#define eventWAIT_FOR_ALL_BITS 0x04000000UL
#define eventEVENT_BITS_CONTROL_BYTES 0xff000000UL
#endif

(2):挂起调度器,因为接下来的操作不知道需要多长的时间,因为需要遍历等待事件列表,并且有可能不止一个任务在等待事件,所以在满足任务等待的事件时候,任务允许被恢复,但是不允许运行,只有遍历完成的时候,任务才能被系统调度,在遍历期间,系统也不希望其他任务来操作这个事件标志组,所以暂时把调度器挂起,让当前任务占有 CPU。
举个例子,主要完成逻辑,判断KEY1还是KEY2按下,如果KEY1按下设置KEY1_EVENT,如果KEY2按下设置KEY2_EVENT,其他任务可以等待这两个事件的发生在进行操作(事件的等待函数下面会将,这里主要熟悉事件组置位函数的使用):
image

首先由于我们需要用的按键,那就在创建一个按键的任务,开始创建按键任务的句柄:

static TaskHandle_t KEY_Task_Handle = NULL;/* KEY_Task任务句柄 */

我们知道,事件组置位函数 xEventGroupSetBits()有两个参数,一个是事件句柄,这个我们在创建函数的时候已经声明过了不需要再进行声明,第二个参数是指定事件中的事件标志位,这里我们用了两个事件按键KEY1按下和按键KEY2按下,那么我们给他们创建两个宏定义(直接写可以,不过为了方便维护,我们宏定义的方式展现出来):

#define KEY1_EVENT  (0x01 << 0)//设置事件掩码的位0
#define KEY2_EVENT  (0x01 << 1)//设置事件掩码的位1

说明一下,不计入实际代码工程当中:
image
假设我们不写宏定义,需要写入的形式:

xEventGroupSetBits(Event_Handle, 0x01);   // 等价于设置bit0(最低位)
xEventGroupSetBits(Event_Handle, 0x02);   // 等价于设置bit1 
xEventGroupSetBits(Event_Handle, 0x03);   // 同时设置bit0和bit1(1 | 2)

引入宏定义:

#define KEY1_EVENT  (0x01 << 0)//设置事件掩码的位0
#define KEY2_EVENT  (0x01 << 1)//设置事件掩码的位1
 
xEventGroupSetBits(Event_Handle,KEY1_EVENT);  	
xEventGroupSetBits(Event_Handle,KEY2_EVENT); 			
xEventGroupSetBits(Event_Handle, KEY1_EVENT | KEY2_EVENT); 

创建按键的任务主体,如果KEY1按下设置KEY1_EVENT,如果KEY2按下设置KEY2_EVENT:

//KEY_Task任务主体
static void KEY_Task(void* parameter)
{	 
    /* 任务都是一个无限循环,不能返回 */
  while (1)
  {
    if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )       //如果KEY2被单击
		{
            printf ( "KEY1被按下!\r\n" );
			/* 触发一个事件1 */
			xEventGroupSetBits(Event_Handle,KEY1_EVENT);  					
		}
    
		if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )       //如果KEY2被单击
		{
            printf ( "KEY2被按下!\r\n" );	
			/* 触发一个事件2 */
			xEventGroupSetBits(Event_Handle,KEY2_EVENT); 				
		}
		vTaskDelay(20);     //每20ms扫描一次		
  }
}

在AppTaskCreate()将任务的参数设置一下:

//任务创建函数
static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  
  taskENTER_CRITICAL();           //进入临界区
 
  /* 创建 Event_Handle */
  Event_Handle = xEventGroupCreate();	 
  if(NULL != Event_Handle)
    printf("Event_Handle 事件创建成功!\r\n");  
	else
    printf("Event_Handle 事件创建失败!\r\n");  	
 
  /* 创建KEY_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )KEY_Task,  /* 任务入口函数 */
                        (const char*    )"KEY_Task",/* 任务名字 */
                        (uint16_t       )512,  /* 任务栈大小 */
                        (void*          )NULL,/* 任务入口函数参数 */
                        (UBaseType_t    )3, /* 任务的优先级 */
                        (TaskHandle_t*  )&KEY_Task_Handle);/* 任务控制块指针 */ 
  if(pdPASS == xReturn)
    printf("创建KEY_Task任务成功!!\r\n");	
  
  vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
  
  taskEXIT_CRITICAL();            //退出临界区
}

main函数完整代码:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
 
#include "LED.h"
#include "Usart.h"
#include "Key.h"  
 
/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "event_groups.h"
 
/* 
 * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
 * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
 * 这个句柄可以为NULL。
 */
static TaskHandle_t AppTaskCreate_Handle = NULL; /* 创建任务句柄 */
static TaskHandle_t KEY_Task_Handle = NULL;/* KEY_Task任务句柄 */
 
/********************************** 内核对象句柄 *********************************/
/*
 * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
 * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
 * 们就可以通过这个句柄操作这些内核对象。
 *
 * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
 * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
 * 来完成的
 * 
 */
static EventGroupHandle_t Event_Handle =NULL;
 
/******************************* 宏定义 ************************************/
/*
 * 当我们在写应用程序的时候,可能需要用到一些宏定义。
 */
#define KEY1_EVENT  (0x01 << 0)//设置事件掩码的位0
#define KEY2_EVENT  (0x01 << 1)//设置事件掩码的位1
 
//一些函数声明
static void AppTaskCreate(void);/* 用于创建任务 */
 
static void KEY_Task(void* pvParameters);/* KEY_Task 任务实现 */
 
static void All_Function_Init(void);/* 用于初始化板载相关资源 */
 
int main(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
 
	All_Function_Init();//硬件初始化
	
	while (1)
	{
		 /* 创建AppTaskCreate任务 */
		xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
													(const char*    )"AppTaskCreate",/* 任务名字 */
													(uint16_t       )512,  /* 任务栈大小 */
													(void*          )NULL,/* 任务入口函数参数 */
													(UBaseType_t    )1, /* 任务的优先级 */
													(TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
		/* 启动任务调度 */           
		if(pdPASS == xReturn)
			vTaskStartScheduler();   /* 启动任务,开启调度 */
		else
			return -1;  
	}
}
 
//任务创建函数
static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  
  taskENTER_CRITICAL();           //进入临界区
 
  /* 创建 Event_Handle */
  Event_Handle = xEventGroupCreate();	 
  if(NULL != Event_Handle)
    printf("Event_Handle 事件创建成功!\r\n");  
	else
    printf("Event_Handle 事件创建失败!\r\n");  	
 
  /* 创建KEY_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )KEY_Task,  /* 任务入口函数 */
                        (const char*    )"KEY_Task",/* 任务名字 */
                        (uint16_t       )512,  /* 任务栈大小 */
                        (void*          )NULL,/* 任务入口函数参数 */
                        (UBaseType_t    )3, /* 任务的优先级 */
                        (TaskHandle_t*  )&KEY_Task_Handle);/* 任务控制块指针 */ 
  if(pdPASS == xReturn)
    printf("创建KEY_Task任务成功!!\r\n");	
  
  vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
  
  taskEXIT_CRITICAL();            //退出临界区
}
 
//KEY_Task任务主体
static void KEY_Task(void* parameter)
{	 
    /* 任务都是一个无限循环,不能返回 */
  while (1)
  {
    if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )       //如果KEY2被单击
		{
      printf ( "KEY1被按下!\r\n" );
			/* 触发一个事件1 */
			xEventGroupSetBits(Event_Handle,KEY1_EVENT);  					
		}
    
		if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )       //如果KEY2被单击
		{
      printf ( "KEY2被按下!\r\n" );	
			/* 触发一个事件2 */
			xEventGroupSetBits(Event_Handle,KEY2_EVENT); 				
		}
		vTaskDelay(20);     //每20ms扫描一次		
  }
}
 
 
//初始化声明
static void All_Function_Init(void)
{
	/*
	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
	 * 都统一用这个优先级分组,千万不要再分组,切忌。
	 */
	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
	
	/* LED 初始化 */
	LED_GPIO_Config();
 
	/* 串口初始化	*/
	USART_Config();
 
	//按键初始化
	Key_GPIO_Config();
  
}

运行结果:
image

事件组置位函数 xEventGroupSetBitsFromISR()(中断)

xEventGroupSetBitsFromISR()是 xEventGroupSetBits()的中断版本,用于置位事件组中指定的位。置位事件组中的标志位是一个不确定的操作,因为阻塞在事件组的标志位上的任务的个数是不确定的。FreeRTOS 是不允许不确定的操作在中断和临界段中发生的,所以xEventGroupSetBitsFromISR()给 FreeRTOS 的守护任务发送一个消息,让置位事件组的操作在守护任务里面完成,守护任务是基于调度锁而非临界段的机制来实现的。

正如上文提到的那样,在中断中事件标志的置位是在守护任务(也叫软件定时器服务任务)中完成的。因此 FreeRTOS 的守护任务与其他任务一样,都是系统调度器根据其优先级进行任务调度的,但守护任务的优先级必须比任何任务的优先级都要高,保证在需要的时候能立即切换任务从而达到快速处理的目的,因为这是在中断中让事件标志位置位,其优先级由 FreeRTOSConfig.h 中的宏 configTIMER_TASK_PRIORITY 来定义。

其实 xEventGroupSetBitsFromISR()函数真正调用的也是 xEventGroupSetBits()函数,只不过是在守护任务中进行调用的,所以它实际上执行的上下文环境依旧是在任务中。

要想使用该函数,必须 把 configUSE_TIMERS 和 INCLUDE_xTimerPendFunctionCall 这些宏在 FreeRTOSConfig.h 中都定义为 1,并且把FreeRTOS/source/event_groups.c 这个 C文件添加到工程中编译。
image

等待事件函数 xEventGroupWaitBits()

FreeRTOS 提供了一个等待指定事件的函数— —xEventGroupWaitBits(),通过这个函数,任务可以知道事件标志组中的哪些位,有什么事件发生了,然后通过 “逻辑与”、“逻辑或”等操作对感兴趣的事件进行获取,并且这个函数实现了等待超时机制,当且仅当任务等待的事件发生时,任务才能获取到事件信息。

在这段时间中,如果事件一直没发生,该任务将保持阻塞状态以等待事件发生。当其它任务或中断服务程序往其等待的事件设置对应的标志位,该任务将自动由阻塞态转为就绪态。当任务等待的时间超过了指定的阻塞时间,即使事件还未发生,任务也会自动从阻塞态转移为就绪态。这样子很有效的体现了操作系统的实时性,如果事件正确获取(等待到)则返回对应的事件标志位,由用户判断再做处理,因为在事件超时的时候也会返回一个不能确定的事件值,所以需要判断任务所等待的事件是否真的发生。
image

/*-----------------------------------------------------------*/
EventBits_t xEventGroupWaitBits(EventGroupHandle_t xEventGroup,
                               const EventBits_t uxBitsToWaitFor,
                               const BaseType_t xClearOnExit,
                               const BaseType_t xWaitForAllBits,
                               TickType_t xTicksToWait)
{
    EventGroup_t *pxEventBits = (EventGroup_t *)xEventGroup;
    EventBits_t uxReturn, uxControlBits = 0;
    BaseType_t xWaitConditionMet, xAlreadyYielded;
    BaseType_t xTimeoutOccurred = pdFALSE;
 
    /* 参数有效性检查 */
    configASSERT(xEventGroup);
    configASSERT((uxBitsToWaitFor & eventEVENT_BITS_CONTROL_BYTES) == 0);
    configASSERT(uxBitsToWaitFor != 0);
    
#if ((INCLUDE_xTaskGetSchedulerState == 1) || (configUSE_TIMERS == 1))
    {
        configASSERT(!((xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED) && 
                      (xTicksToWait != 0)));
    }
#endif
 
    vTaskSuspendAll();  /* (1) 挂起调度器 */
    {
        const EventBits_t uxCurrentEventBits = pxEventBits->uxEventBits;
 
        /* (2) 检查当前事件位是否满足条件,其实就是判断一下用户等待的事件是否与当前事件标志位一致 */
        xWaitConditionMet = prvTestWaitCondition(uxCurrentEventBits,
                                               uxBitsToWaitFor,
                                               xWaitForAllBits);
 
        if (xWaitConditionMet != pdFALSE) {  /* (3) 条件已满足 */
            uxReturn = uxCurrentEventBits;
            xTicksToWait = (TickType_t)0;
 
            /* (4) 检查是否需要清除事件位,如果 xClearOnExit 为 pdTRUE 则需要清除事件标志位,如果为 pdFALSE 就不需要清除。 */
            if (xClearOnExit != pdFALSE) {
                pxEventBits->uxEventBits &= ~uxBitsToWaitFor;
            } else {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        /* (5) 不满足条件且不等待 */
        else if (xTicksToWait == (TickType_t)0) {
            uxReturn = uxCurrentEventBits;
        }
        /* (6) 需要阻塞等待 */
        else {
            /* 设置控制位 */
            if (xClearOnExit != pdFALSE) {
                uxControlBits |= eventCLEAR_EVENTS_ON_EXIT_BIT;
            }
            if (xWaitForAllBits != pdFALSE) {
                uxControlBits |= eventWAIT_FOR_ALL_BITS;
            }
 
            /* (7) 将任务加入事件等待列表,任务将被阻塞指定时间 xTicksToWait,并且这个列表项的值是用于保存任务等待事件需求的信息标记,以便在事件标志位置位的时候对等待事件的任务进行相应的操作。 */
            vTaskPlaceOnUnorderedEventList(
                &(pxEventBits->xTasksWaitingForBits),
                (uxBitsToWaitFor | uxControlBits),
                xTicksToWait);
 
            uxReturn = 0;
            traceEVENT_GROUP_WAIT_BITS_BLOCK(xEventGroup, uxBitsToWaitFor);
        }
    }
    xAlreadyYielded = xTaskResumeAll();  /* (8) 恢复调度器 */
 
    if (xTicksToWait != (TickType_t)0) {
        if (xAlreadyYielded == pdFALSE) {
            portYIELD_WITHIN_API();  /* (9) 触发任务切换 */
        }
 
        /* (10) 获取被唤醒时的事件值 */
        uxReturn = uxTaskResetEventItemValue();
 
        if ((uxReturn & eventUNBLOCKED_DUE_TO_BIT_SET) == (EventBits_t)0) {  /* (11) 超时唤醒 */
            taskENTER_CRITICAL();
            {
                uxReturn = pxEventBits->uxEventBits;
                
                /* (12) 再次检查事件条件 */
                if (prvTestWaitCondition(uxReturn, uxBitsToWaitFor, xWaitForAllBits) != pdFALSE) {
                    if (xClearOnExit != pdFALSE) {
                        pxEventBits->uxEventBits &= ~uxBitsToWaitFor;  /* (13) 清除事件位 */
                    }
                }
            }
            taskEXIT_CRITICAL();
            xTimeoutOccurred = pdFALSE;
        }
        
        /* (14) 过滤控制位 */
        uxReturn &= ~eventEVENT_BITS_CONTROL_BYTES;
    }
 
    traceEVENT_GROUP_WAIT_BITS_END(xEventGroup, uxBitsToWaitFor, xTimeoutOccurred);
    return uxReturn;
}
/*-----------------------------------------------------------*/

举个例子来看一下其实际的调用
首先为了方便观察实验现象我们在LED.c里面先去创建一个电平翻转函数,主要作用就是每次调用能够进行一次电平翻转,观察灯珠的亮灭,来查看实验现象,记得在.h文件声明一下:

//红灯电平翻转
void Toggle_LED_R(void)
{
	BitAction LED_R = (BitAction)(1 - GPIO_ReadOutputDataBit(LED1_GPIO_PORT, LED1_GPIO_PIN));
	GPIO_WriteBit(LED1_GPIO_PORT, LED1_GPIO_PIN, LED_R);
}

然后我们回到main函数,既然观察电平翻转,那么我们就创建一个LED的任务,首先创建一个LED的任务句柄:

static TaskHandle_t LED_Task_Handle = NULL;/* LED_Task任务句柄 */

然后是LED的任务主体:

//LED_Task任务主体
static void LED_Task(void* parameter)
{	
  EventBits_t r_event;  /* 定义一个事件接收变量 */
  /* 任务都是一个无限循环,不能返回 */
  while (1)
	{
    /*******************************************************************
     * 等待接收事件标志 
     * 
     * 如果xClearOnExit设置为pdTRUE,那么在xEventGroupWaitBits()返回之前,
     * 如果满足等待条件(如果函数返回的原因不是超时),那么在事件组中设置
     * 的uxBitsToWaitFor中的任何位都将被清除。 
     * 如果xClearOnExit设置为pdFALSE,
     * 则在调用xEventGroupWaitBits()时,不会更改事件组中设置的位。
     *
     * xWaitForAllBits如果xWaitForAllBits设置为pdTRUE,则当uxBitsToWaitFor中
     * 的所有位都设置或指定的块时间到期时,xEventGroupWaitBits()才返回。 
     * 如果xWaitForAllBits设置为pdFALSE,则当设置uxBitsToWaitFor中设置的任何
     * 一个位置1 或指定的块时间到期时,xEventGroupWaitBits()都会返回。 
     * 阻塞时间由xTicksToWait参数指定。          
      *********************************************************/
    r_event = xEventGroupWaitBits(Event_Handle,  /* 事件对象句柄 */
                                  KEY1_EVENT|KEY2_EVENT,/* 接收线程感兴趣的事件 */
                                  pdTRUE,   /* 退出时清除事件位 */
                                  pdTRUE,   /* 满足感兴趣的所有事件 */
                                  portMAX_DELAY);/* 指定超时事件,一直等 */
                        
    if((r_event & (KEY1_EVENT|KEY2_EVENT)) == (KEY1_EVENT|KEY2_EVENT)) 
    {
      /* 如果接收完成并且正确 */
      printf ( "\r\nKEY1与KEY2都按下!\r\n");		
			Toggle_LED_R();       //电平反转
    }
    else
      printf ( "事件错误!\r\n");	
  }
}

不在函数代码中,仅解释,这些宏定义是FreeRTOS内部封装好的:

#define pdTRUE      ( ( BaseType_t ) 1 )  // 代表逻辑真
#define pdFALSE     ( ( BaseType_t ) 0 )  // 代表逻辑假

在 FreeRTOS 中,pdTRUE 是一个宏定义,表示逻辑真(true),通常用于布尔类型的参数或返回值。它的值通常是 1,而对应的 pdFALSE 表示逻辑假(false),通常是 0。

#if( configUSE_16_BIT_TICKS == 1 )
	typedef uint16_t TickType_t;
	#define portMAX_DELAY ( TickType_t ) 0xffff
#else
	typedef uint32_t TickType_t;
	#define portMAX_DELAY ( TickType_t ) 0xffffffffUL

portMAX_DELAY 是 FreeRTOS 的一个宏,通常定义为 0xFFFFFFFF(或类似的最大值),表示 任务可以无限期阻塞,直到事件发生。

如果希望任务 有限时间等待,可以设置一个具体的 tick 值(如 100 / portTICK_PERIOD_MS 表示 100ms)。

r_event = xEventGroupWaitBits(
    Event_Handle,
    KEY1_EVENT | KEY2_EVENT,
    pdTRUE,   // 退出时清除事件位
    pdTRUE,   // 需要所有事件都发生
    pdMS_TO_TICKS(1000)  // 等待 1000ms,超时后返回
);

回归正题,既然任务主体创建好了,那么我们还需要对LED分配好栈空间,优先级等参数,找到AppTaskCreate()函数,添加:

  /* 创建LED_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )LED_Task, /* 任务入口函数 */
                        (const char*    )"LED_Task",/* 任务名字 */
                        (uint16_t       )512,   /* 任务栈大小 */
                        (void*          )NULL,	/* 任务入口函数参数 */
                        (UBaseType_t    )2,	    /* 任务的优先级 */
                        (TaskHandle_t*  )&LED_Task_Handle);/* 任务控制块指针 */
  if(pdPASS == xReturn)
    printf("创建LED_Task任务成功!\r\n");

如:

//任务创建函数
static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  
  taskENTER_CRITICAL();           //进入临界区
 
  /* 创建 Event_Handle */
  Event_Handle = xEventGroupCreate();	 
  if(NULL != Event_Handle)
    printf("Event_Handle 事件创建成功!\r\n");  
	else
    printf("Event_Handle 事件创建失败!\r\n");  	
 
  /* 创建LED_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )LED_Task, /* 任务入口函数 */
                        (const char*    )"LED_Task",/* 任务名字 */
                        (uint16_t       )512,   /* 任务栈大小 */
                        (void*          )NULL,	/* 任务入口函数参数 */
                        (UBaseType_t    )2,	    /* 任务的优先级 */
                        (TaskHandle_t*  )&LED_Task_Handle);/* 任务控制块指针 */
  if(pdPASS == xReturn)
    printf("创建LED_Task任务成功!\r\n");
 
  /* 创建KEY_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )KEY_Task,  /* 任务入口函数 */
                        (const char*    )"KEY_Task",/* 任务名字 */
                        (uint16_t       )512,  /* 任务栈大小 */
                        (void*          )NULL,/* 任务入口函数参数 */
                        (UBaseType_t    )3, /* 任务的优先级 */
                        (TaskHandle_t*  )&KEY_Task_Handle);/* 任务控制块指针 */ 
  if(pdPASS == xReturn)
    printf("创建KEY_Task任务成功!!\r\n");	
  
  vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
  
  taskEXIT_CRITICAL();            //退出临界区
}

完整的main函数:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
 
#include "LED.h"
#include "Usart.h"
#include "Key.h"  
 
/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "event_groups.h"
 
/* 
 * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
 * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
 * 这个句柄可以为NULL。
 */
static TaskHandle_t AppTaskCreate_Handle = NULL; /* 创建任务句柄 */
static TaskHandle_t LED_Task_Handle = NULL;/* LED_Task任务句柄 */
static TaskHandle_t KEY_Task_Handle = NULL;/* KEY_Task任务句柄 */
 
/********************************** 内核对象句柄 *********************************/
/*
 * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
 * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
 * 们就可以通过这个句柄操作这些内核对象。
 *
 * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
 * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
 * 来完成的
 * 
 */
static EventGroupHandle_t Event_Handle =NULL;
 
/******************************* 宏定义 ************************************/
/*
 * 当我们在写应用程序的时候,可能需要用到一些宏定义。
 */
#define KEY1_EVENT  (0x01 << 0)//设置事件掩码的位0
#define KEY2_EVENT  (0x01 << 1)//设置事件掩码的位1
 
//一些函数声明
static void AppTaskCreate(void);/* 用于创建任务 */
 
static void LED_Task(void* pvParameters);/* LED_Task 任务实现 */
static void KEY_Task(void* pvParameters);/* KEY_Task 任务实现 */
 
static void All_Function_Init(void);/* 用于初始化板载相关资源 */
 
int main(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
 
	All_Function_Init();//硬件初始化
	
	while (1)
	{
		 /* 创建AppTaskCreate任务 */
		xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
													(const char*    )"AppTaskCreate",/* 任务名字 */
													(uint16_t       )512,  /* 任务栈大小 */
													(void*          )NULL,/* 任务入口函数参数 */
													(UBaseType_t    )1, /* 任务的优先级 */
													(TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
		/* 启动任务调度 */           
		if(pdPASS == xReturn)
			vTaskStartScheduler();   /* 启动任务,开启调度 */
		else
			return -1;  
	}
}
 
//任务创建函数
static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  
  taskENTER_CRITICAL();           //进入临界区
 
  /* 创建 Event_Handle */
  Event_Handle = xEventGroupCreate();	 
  if(NULL != Event_Handle)
    printf("Event_Handle 事件创建成功!\r\n");  
	else
    printf("Event_Handle 事件创建失败!\r\n");  	
 
  /* 创建LED_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )LED_Task, /* 任务入口函数 */
                        (const char*    )"LED_Task",/* 任务名字 */
                        (uint16_t       )512,   /* 任务栈大小 */
                        (void*          )NULL,	/* 任务入口函数参数 */
                        (UBaseType_t    )2,	    /* 任务的优先级 */
                        (TaskHandle_t*  )&LED_Task_Handle);/* 任务控制块指针 */
  if(pdPASS == xReturn)
    printf("创建LED_Task任务成功!\r\n");
 
  /* 创建KEY_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )KEY_Task,  /* 任务入口函数 */
                        (const char*    )"KEY_Task",/* 任务名字 */
                        (uint16_t       )512,  /* 任务栈大小 */
                        (void*          )NULL,/* 任务入口函数参数 */
                        (UBaseType_t    )3, /* 任务的优先级 */
                        (TaskHandle_t*  )&KEY_Task_Handle);/* 任务控制块指针 */ 
  if(pdPASS == xReturn)
    printf("创建KEY_Task任务成功!!\r\n");	
  
  vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
  
  taskEXIT_CRITICAL();            //退出临界区
}
 
//LED_Task任务主体
static void LED_Task(void* parameter)
{	
  EventBits_t r_event;  /* 定义一个事件接收变量 */
  /* 任务都是一个无限循环,不能返回 */
  while (1)
	{
    /*******************************************************************
     * 等待接收事件标志 
     * 
     * 如果xClearOnExit设置为pdTRUE,那么在xEventGroupWaitBits()返回之前,
     * 如果满足等待条件(如果函数返回的原因不是超时),那么在事件组中设置
     * 的uxBitsToWaitFor中的任何位都将被清除。 
     * 如果xClearOnExit设置为pdFALSE,
     * 则在调用xEventGroupWaitBits()时,不会更改事件组中设置的位。
     *
     * xWaitForAllBits如果xWaitForAllBits设置为pdTRUE,则当uxBitsToWaitFor中
     * 的所有位都设置或指定的块时间到期时,xEventGroupWaitBits()才返回。 
     * 如果xWaitForAllBits设置为pdFALSE,则当设置uxBitsToWaitFor中设置的任何
     * 一个位置1 或指定的块时间到期时,xEventGroupWaitBits()都会返回。 
     * 阻塞时间由xTicksToWait参数指定。          
      *********************************************************/
    r_event = xEventGroupWaitBits(Event_Handle,  /* 事件对象句柄 */
                                  KEY1_EVENT|KEY2_EVENT,/* 接收线程感兴趣的事件 */
                                  pdTRUE,   /* 退出时清除事件位 */
                                  pdTRUE,   /* 满足感兴趣的所有事件 */
                                  portMAX_DELAY);/* 指定超时事件,一直等 */
                        
    if((r_event & (KEY1_EVENT|KEY2_EVENT)) == (KEY1_EVENT|KEY2_EVENT)) 
    {
      /* 如果接收完成并且正确 */
      printf ( "\r\nKEY1与KEY2都按下!\r\n");		
			Toggle_LED_R();       //电平反转
    }
    else
      printf ( "事件错误!\r\n");	
  }
}
 
//KEY_Task任务主体
static void KEY_Task(void* parameter)
{	 
    /* 任务都是一个无限循环,不能返回 */
  while (1)
  {
    if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )       //如果KEY2被单击
		{
      printf ( "\r\nKEY1被按下!\r\n" );
			/* 触发一个事件1 */
			xEventGroupSetBits(Event_Handle,KEY1_EVENT);  					
		}
    
		if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )       //如果KEY2被单击
		{
      printf ( "\r\nKEY2被按下!\r\n" );	
			/* 触发一个事件2 */
			xEventGroupSetBits(Event_Handle,KEY2_EVENT); 				
		}
		vTaskDelay(20);     //每20ms扫描一次		
  }
}
 
 
//初始化声明
static void All_Function_Init(void)
{
	/*
	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
	 * 都统一用这个优先级分组,千万不要再分组,切忌。
	 */
	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
	
	/* LED 初始化 */
	LED_GPIO_Config();
 
	/* 串口初始化	*/
	USART_Config();
 
	//按键初始化
	Key_GPIO_Config();
  
}

运行结果:
image

可以看出必须两个按键都被按下才会正常运行,如果仅有一个按键按下并不会满足条件,我们再试试不一直等待的情况,这里其他地方不用动,只更改主体代码:

//LED_Task任务主体
static void LED_Task(void* parameter)
{	
  EventBits_t r_event;  /* 定义一个事件接收变量 */
  /* 任务都是一个无限循环,不能返回 */
  while (1)
	{
    /*******************************************************************
     * 等待接收事件标志 
     * 
     * 如果xClearOnExit设置为pdTRUE,那么在xEventGroupWaitBits()返回之前,
     * 如果满足等待条件(如果函数返回的原因不是超时),那么在事件组中设置
     * 的uxBitsToWaitFor中的任何位都将被清除。 
     * 如果xClearOnExit设置为pdFALSE,
     * 则在调用xEventGroupWaitBits()时,不会更改事件组中设置的位。
     *
     * xWaitForAllBits如果xWaitForAllBits设置为pdTRUE,则当uxBitsToWaitFor中
     * 的所有位都设置或指定的块时间到期时,xEventGroupWaitBits()才返回。 
     * 如果xWaitForAllBits设置为pdFALSE,则当设置uxBitsToWaitFor中设置的任何
     * 一个位置1 或指定的块时间到期时,xEventGroupWaitBits()都会返回。 
     * 阻塞时间由xTicksToWait参数指定。          
      *********************************************************/
    r_event = xEventGroupWaitBits(Event_Handle,  /* 事件对象句柄 */
                                  KEY1_EVENT|KEY2_EVENT,/* 接收线程感兴趣的事件 */
                                  pdTRUE,   /* 退出时清除事件位 */
                                  pdTRUE,   /* 满足感兴趣的所有事件 */
//                                  portMAX_DELAY/* 指定超时事件,一直等 */
																	pdMS_TO_TICKS(5000)  // 等待 1000ms,超时后返回
																	);
                        
    if((r_event & (KEY1_EVENT|KEY2_EVENT)) == (KEY1_EVENT|KEY2_EVENT)) 
    {
      /* 如果接收完成并且正确 */
      printf ( "\r\nKEY1与KEY2都按下!\r\n");		
			Toggle_LED_R();       //电平反转
    }
    else
			printf("等待超时或事件错误!\r\n"); 	
  }
}

这里每隔5s进行一次检测,若是仅有一个按键按下或者没有按键按下,则会返回超时错误,只有两个按键都被按下才会正常显示:
image

xEventGroupClearBits()与 xEventGroupClearBitsFromISR()

xEventGroupClearBits()与 xEventGroupClearBitsFromISR()都是用于清除事件组指定的位,如果在获取事件的时候没有将对应的标志位清除,那么就需要用这个函数来进行显式清除,xEventGroupClearBits()函数不能在中断中使用,而是由具有中断保护功能的xEventGroupClearBitsFromISR() 来代替,中断清除事件标志位的操作在守护任务(也叫定时器服务任务) 里面完成。守护进程的优先级由 FreeRTOSConfig.h 中的宏configTIMER_TASK_PRIORITY 来定义 。 要想使用该函数必须把 FreeRTOS/source/event_groups.c 这个 C 文件添加到工程中。

image

简单举个例子:

#define BIT_0 (1 << 0)
#define BIT_4 (1 << 4)
 
void aFunction(EventGroupHandle_t xEventGroup) 
{
    EventBits_t uxBits;
 
    /* 清除事件组的 bit 0 和 bit 4 */
    uxBits = xEventGroupClearBits(xEventGroup, BIT_0 | BIT_4);
 
    if ((uxBits & (BIT_0 | BIT_4)) == (BIT_0 | BIT_4)) {
        /* 在调用 xEventGroupClearBits()之前 bit0 和 bit4 都置位
           但是现在是被清除了 */
    } 
    else if ((uxBits & BIT_0) != 0) {
        /* 在调用 xEventGroupClearBits()之前 bit0 已经置位
           但是现在是被清除了 */
    } 
    else if ((uxBits & BIT_4) != 0) {
        /* 在调用 xEventGroupClearBits()之前 bit4 已经置位
           但是现在是被清除了 */
    } 
    else {
        /* 在调用 xEventGroupClearBits()之前 bit0 和 bit4 都没被置位 */
    }
}
posted @ 2026-09-12 14:49  灵垚克府  阅读(5)  评论(0)    收藏  举报