实时操作系统-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 提供的事件具有如下特点:
- 事件只与任务相关联,事件相互独立,一个 32 位的事件集合(EventBits_t 类型的变量,实际可用与表示事件的只有 24位),用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0 表示该事件类型未发生、1 表示该事件类型已经发生),一共 24 种事件类型。
- 事件仅用于同步,不提供数据传输功能。
- 事件无排队性,即多次向任务设置同一事件(如果任务还未来得及读走),等效于只设置一次。
- 允许多个任务对同一事件进行读写操作。
- 支持事件等待超时机制。
应用场景
比如一些危险机器的启动,需要检查各项指标,当指标不达标的时候,无法启动,但是检查各个指标的时候,不能一下子检测完毕啊,所以,需要事件来做统一的等待,当所有的事件都完成了,那么机器才允许启动,这只是事件的其中一个应用。
事件可使用于多种场合,它能够在一定程度上替代信号量,用于任务与任务间,中断与任务间的同步。一个任务或中断服务例程发送一个事件给事件对象,而后等待的任务被唤醒并对相应的事件进行处理。但是它与信号量不同的是,事件的发送操作是不可累计的,而信号量的释放动作是可累计的。事件另外一个特性是,接收任务可等待多种事件,即多个事件对应一个任务或多个任务。同时按照任务等待的参数,可选择是“逻辑或”触发还是“逻辑与”触发。这个特性也是信号量等所不具备的,信号量只能识别单一同步动作,而不能同时等待多个事件的同步。
运作机制
事件接收成功后,必须使用 xClearOnExit 选项来清除已接收到的事件类 型,否则不会清除已接收到的事件,这样就需要用户显式清除事件位。 用户可以自定义通过传入参数 xWaitForAllBits 选择读取模式,是等待所有感兴趣的事件还是等待感兴趣的任意一个事件。
设置事件时,对指定事件写入指定的事件类型,设置事件集合的对应事件位为 1,可以一次同时写多个事件类型,设置事件成功可能会触发任务调度。
清除事件时,根据入参数事件句柄和待清除的事件类型,对事件对应位进行清 0 操作。
事件不与任务相关联,事件相互独立,一个 32位的变量(事件集合,实际用于表示事件的只有 24 位),用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0 表示该事件类型未发生、1表示该事件类型已经发生),一共 24种事件类型具体见图:

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

任务 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 */

举个例子
首先进行头文件包含:
#include "event_groups.h"
并且在头文件 FreeRTOSConfig.h 定义宏 configSUPPORT_DYNAMIC_ALLOCATION 为 1,也就是动态内存分配,我们已经设成1了因此不用动:

在使用创建函数前我们需要有一个事件的句柄,以便后续访问:
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();
}
运行结果:

事件删除函数 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(); //退出临界区
}
运行结果,会发现事件删除后,再去调用会出现错误:

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

函数源码:
/*-----------------------------------------------------------*/
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,其他任务可以等待这两个事件的发生在进行操作(事件的等待函数下面会将,这里主要熟悉事件组置位函数的使用):

首先由于我们需要用的按键,那就在创建一个按键的任务,开始创建按键任务的句柄:
static TaskHandle_t KEY_Task_Handle = NULL;/* KEY_Task任务句柄 */
我们知道,事件组置位函数 xEventGroupSetBits()有两个参数,一个是事件句柄,这个我们在创建函数的时候已经声明过了不需要再进行声明,第二个参数是指定事件中的事件标志位,这里我们用了两个事件按键KEY1按下和按键KEY2按下,那么我们给他们创建两个宏定义(直接写可以,不过为了方便维护,我们宏定义的方式展现出来):
#define KEY1_EVENT (0x01 << 0)//设置事件掩码的位0
#define KEY2_EVENT (0x01 << 1)//设置事件掩码的位1
说明一下,不计入实际代码工程当中:

假设我们不写宏定义,需要写入的形式:
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();
}
运行结果:

事件组置位函数 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文件添加到工程中编译。

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

/*-----------------------------------------------------------*/
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();
}
运行结果:

可以看出必须两个按键都被按下才会正常运行,如果仅有一个按键按下并不会满足条件,我们再试试不一直等待的情况,这里其他地方不用动,只更改主体代码:
//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进行一次检测,若是仅有一个按键按下或者没有按键按下,则会返回超时错误,只有两个按键都被按下才会正常显示:

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

简单举个例子:
#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 都没被置位 */
}
}

浙公网安备 33010602011771号