FreeRTOS学习笔记(6)——事件
一、头文件
#include "FreeRTOS.h"
#include "event_groups.h"
二、事件
2.1 基本概念
事件是一种实现任务间通信的机制,主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输。 与信号量不同的是,它可以实现一对多,多对多的同步。即一个任务可以等待多个事件的发生:可以是任意一个事件发生时唤醒任务进行事件处理;也可以是几个事件都发生后才唤醒任务进行事件处理。同样,也可以是多个任务同步多个事件。
每一个事件组只需要很少的 RAM 空间来保存事件组的状态。事件组存储在一个 EventBits_t 类型的变量中,该变量在事件组结构体中定义 。在 STM32 中 , 我们一般 configUSE_16_BIT_TICKS 定义为 0,那么 uxEventBits 是 32 位的,有 24 个位用来实现事 件标志组。每一位代表一个事件,任务通过“逻辑与”或“逻辑或”与一个或多个事件建立关联,形成一个事件组。 事件的“逻辑或”也被称作是独立型同步,指的是任务感兴趣的所有事件任一件发生即可被唤醒;事件“逻辑与”则被称为是关联型同步,指的是任务感兴趣的若干事件都发生时才被唤醒,并且事件发生的时间可以不同步。
多任务环境下,任务、中断之间往往需要同步操作,一个事件发生会告知等待中的任务,即形成一个任务与任务、中断与任务间的同步。 事件可以提供一对多、多对多的同步操作。一对多同步模型:一个任务等待多个事件的触发,这种情况是比较常见的;多对多同步模型:多个任务等待多个事件的触发。
任务可以通过设置事件位来实现事件的触发和等待操作。FreeRTOS 的事件仅用于同步,不提供数据传输功能。
FreeRTOS 提供的事件具有如下特点:
- 事件只与任务相关联,事件相互独立,一个 32 位的事件集合(EventBits_t 类型的变量,实际可用与表示事件的只有 24 位),用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0 表示该事件类型未发生、1 表示该事件类型已经发生),一共 24 种事件类型。
- 事件仅用于同步,不提供数据传输功能。
- 事件无排队性,即多次向任务设置同一事件(如果任务还未来得及读走),等效于只设置一次。
- 允许多个任务对同一事件进行读写操作。
- 支持事件等待超时机制。
在 FreeRTOS 事件中,每个事件获取的时候,用户可以选择感兴趣的事件,并且选择读取事件信息标记,它有三个属性,分别是逻辑与,逻辑或以及是否清除标记。当任务等待事件同步时,可以通过任务感兴趣的事件位和事件信息标记来判断当前接收的事件是否满足要求,如果满足则说明任务等待到对应的事件,系统将唤醒等待的任务;否则,任务会根据用户指定的阻塞超时时间继续等待下去。
2.2 运作机制
接收事件时,可以根据感兴趣的参事件类型接收事件的单个或者多个事件类型。事件接收成功后,必须使用 xClearOnExit 选项来清除已接收到的事件类型,否则不会清除已接收到的事件,这样就需要用户显式清除事件位。用户可以自定义通过传入参数 xWaitForAllBits 选择读取模式,是等待所有感兴趣的事件还是等待感兴趣的任意一个事件。
设置事件时,对指定事件写入指定的事件类型,设置事件集合的对应事件位为 1,可以一次同时写多个事件类型,设置事件成功可能会触发任务调度。清除事件时,根据入参数事件句柄和待清除的事件类型,对事件对应位进行清 0 操作。事件不与任务相关联,事件相互独立,一个 32 位的变量(事件集合,实际用于表示事件的只有 24 位),用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0 表示该事件类型未发生、1 表示该事件类型已经发生),一共 24 种事件类型。
事件唤醒机制,当任务因为等待某个或者多个事件发生而进入阻塞态,当事件发生的时候会被唤醒。
任务 1 对事件 3 或事件 5 感兴趣(逻辑或),当发生其中的某一个事件都会被唤醒,并且执行相应操作。而任务 2 对事件 3 与事件 5 感兴趣(逻辑与),当且仅当事件 3 与事件 5 都发生的时候,任务 2 才会被唤醒,如果只有一个其中一个事件发生,那么任务还是会继续等待事件发生。如果接在收事件函数中设置了清除事件位 xClearOnExit,那么当任务唤醒后将把事件 3 和事件 5 的事件标志清零,否则事件标志将依然存在。
三、相关API说明
3.1 xEventGroupCreate
用于创建一个事件组,并返回对应的句柄。
函数 | EventGroupHandle_t xEventGroupCreate( void ) |
---|---|
参数 | 无 |
返回值 | 事件句柄 |
要想使用该函数必须在 FreeRTOSConfig.h
中把 configSUPPORT_DYNAMIC_ALLOCATION
定义为 1 来使能。
同时把
FreeRTOS/source/event_groups.c
这个 C 文件添加到工程中。
3.2 vEventGroupDelete
当系统不再使用事件对象时,可以通过删除事件对象控制块来释放系统资源。
函数 | void vEventGroupDelete( EventGroupHandle_t xEventGroup ) |
---|---|
参数 | 事件句柄 |
返回值 | 无 |
3.3 xEventGroupSetBits(任务)
用于置位事件组中指定的位,当位被置位之后,阻塞在该位上的任务将会被解锁。使用该函数接口时,通过参数指定的事件标志来设定事件的标志位,然后遍历等待在事件对象上的事件等待列表,判断是否有任务的事件激活要求与当前事件对象标志值匹配,如果有,则唤醒该任务。简单来说,就是设置我们自己定义的事件标志位为 1,并且看看有没有任务在等待这个事件,有的话就唤醒它。注意的是该函数不允许在中断中使用。
函数 | EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet ) |
---|---|
参数 |
xEventGroup: 事件句柄 uxBitsToSet: 指定事件中的事件标志位。如设置 uxBitsToSet 为 0x08 则只置位位 3,如果设置 uxBitsToSet 为 0x09 则位 3 和位 0 都需要被置位 |
返回值 | 返回调用 xEventGroupSetBits() 时事件组中的值 |
3.4 xEventGroupSetBitsFromISR(中断)
xEventGroupSetBitsFromISR()是 xEventGroupSetBits()的中断版本,用于置位事件组中指定的位。置位事件组中的标志位是一个不确定的操作,因为阻塞在事件组的标志位上的任务的个数是不确定的。FreeRTOS 是不允许不确定的操作在中断和临界段中发生的,所以 xEventGroupSetBitsFromISR() 给 FreeRTOS 的守护任务发送一个消息,让置位事件组的操作在守护任务里面完成,守护任务是基于调度锁而非临界段的机制来实现的。需要注意的地方:正如上文提到的那样,在中断中事件标志的置位是在守护任务(也叫软件定时器服务任务)中完成的。因此 FreeRTOS 的守护任务与其他任务一样,都是系统调度器根据其优先级进行任务调度的,但守护任务的优先级必须比任何任务的优先级都要高,保证在需要的时候能立即切换任务从而达到快速处理的目的,因为这是在中断中让事件标志位置位,其优先级由 FreeRTOSConfig.h 中的宏 configTIMER_TASK_PRIORITY 来定义。
函数 | BaseType_t xEventGroupSetBitsFromISR(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, BaseType_t *pxHigherPriorityTaskWoken) |
---|---|
参数 |
xEventGroup: 事件句柄 uxBitsToSet: 指定事件中的事件标志位。如设置 uxBitsToSet 为 0x08 则只置位位 3,如果设置 uxBitsToSet 为 0x09 则位 3 和位 0 都需要被置位 pxHigherPriorityTaskWoken: pxHigherPriorityTaskWoken 在使用之前必须初始化成 pdFALSE。调用 xEventGroupSetBitsFromISR() 会给守护任务发送一个消息,如果守护任务的优先级高于当前被中断的任务的优先级的话(一般情况下都需要将守护任务的优先级设置为所有任务中最高优先级),pxHigherPriorityTaskWoken 会被置为 pdTRUE,然后在中断退出前执行一次上下文切换 |
返回值 | 消息成功发送给守护任务之后则返回 pdTRUE,否则返回 pdFAIL。如果定时器服务队列满了将返回 pdFAIL |
要想使用该函数必须在 FreeRTOSConfig.h
中把 configUSE_TIMERS
和 INCLUDE_xTimerPendFunctionCall
定义为 1 来使能。
同时把 FreeRTOS/source/event_groups.c
这个 C 文件添加到工程中。
3.5 xEventGroupWaitBits
用于获取事件组中的一个或多个事件发生标志,当要读取的事件标 志 位没有被置位时任务将进入阻塞等待状态。
函数 | EventBits_t xEventGroupWaitBits(const EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToWaitFor, const BaseType_t xClearOnExit, const BaseType_t xWaitForAllBits, TickType_t xTicksToWait ) |
---|---|
参数 |
xEventGroup: 事件句柄 uxBitsToWaitFor: 一个按位或的值,指定需要等待事件组中的哪些位置 1。如果需要等待 bit 0 and/or bit 2 那么 uxBitsToWaitFor 配置为 0x05(0101b)。如果需要等待 bits 0 and/or bit 1 and/or bit 2 那么 uxBitsToWaitFor 配置为 0x07(0111b) xClearOnExit: pdTRUE:当 xEventGroupWaitBits()等待到满足任务唤醒的事件时,系统将清除由形参 uxBitsToWaitFor 指定的事件标志位。pdFALSE:不会清除由形参 uxBitsToWaitFor 指定的事件标志位。 xWaitForAllBits: pdTRUE : 当 形 参 uxBitsToWaitFor 指 定 的 位 都 置 位 的 时 候 ,xEventGroupWaitBits()才满足任务唤醒的条件,这也是“逻辑与”等待事件,并且在没有超时的情况下返回对应的事件标志位的值。pdFALSE:当形参 uxBitsToWaitFor 指定的位有其中任意一个置位的时候,这也是常说的“逻辑或”等待事件,在没有超时的情况下函数返回对应的事件标志位的值。 xTicksToWait: 最大超时时间,单位为系统节拍周期,常量 portTICK_PERIOD_MS 用于辅助把时间转换成 MS |
返回值 | 返回事件中的哪些事件标志位被置位,返回值很可能并不是用户指定的事件位,需要对返回值进行判断再处理 |
3.6 xEventGroupClearBits(任务)
用于清除事件组指定的位,如果在获取事件的时候没有将对应的标志位清除,那么就需要用这个函数来进行显式清除,xEventGroupClearBits() 函数不能在中断中使用。
函数 | EventBits_t xEventGroupClearBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear ) |
---|---|
参数 |
xEventGroup: 事件句柄 uxBitsToClear: 指定事件组中的哪个位需要清除。如设置 uxBitsToSet 为 0x08 则只清除位 3,如果设置 uxBitsToSet 为 0x09 则位 3 和位 0 都需要被清除 |
返回值 | 事件在还没有清除指定位之前的值 |
3.7 xEventGroupClearBitsFromISR(中断)
用于清除事件组指定的位,如果在获取事件的时候没有将对应的标志位清除,那么就需要用这个函数来进行显式清除。中断清除事件标志位的操作在守护任务(也叫定时器服务任务)里面完成。守护进程的优先级 FreeRTOSConfig.h 中的宏 configTIMER_TASK_PRIORITY 来定义 。
函数 | BaseType_t xEventGroupClearBitsFromISR(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear ) |
---|---|
参数 |
xEventGroup: 事件句柄 uxBitsToClear: 指定事件组中的哪个位需要清除。如设置 uxBitsToSet 为 0x08 则只清除位 3,如果设置 uxBitsToSet 为 0x09 则位 3 和位 0 都需要被清除 |
返回值 | 事件在还没有清除指定位之前的值 |
四、示例
4.1 任务式
/* FreeRTOS 头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "event_groups.h"
/* 开发板硬件 bsp 头文件 */
#include "bsp_led.h"
#include "bsp_usart.h"
#include "bsp_key.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 BSP_Init(void);/* 用于初始化板载相关资源 */
int main(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
/* 开发板硬件初始化 */
BSP_Init();
/* 创建 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;
}
while (1); /* 正常不会执行到这里 */
}
/***********************************************************************
* @ 函数名 : 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");
}
/* 创建 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 任务成功!\n");\
}
vTaskDelete(AppTaskCreate_Handle); //删除 AppTaskCreate 任务
taskEXIT_CRITICAL(); //退出临界区
}
/**********************************************************************
* @ 函数名 : LED_Task
* @ 功能说明: 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 ( "KEY1 与 KEY2 都按下\n");
LED1_TOGGLE; //LED1 反转
}
else
{
printf ( "事件错误!\n");
}
}
}
/**********************************************************************
* @ 函数名 : KEY_Task
* @ 功能说明: KEY_Task 任务主体
* @ 参数 :
* @ 返回值 : 无
********************************************************************/
static void KEY_Task(void* parameter)
{
/* 任务都是一个无限循环,不能返回 */
while (1)
{
//如果 KEY2 被按下
if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{
printf ( "KEY1 被按下\n" );
/* 触发一个事件 1 */
xEventGroupSetBits(Event_Handle,KEY1_EVENT);
}
//如果 KEY2 被按下
if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{
printf ( "KEY2 被按下\n" );
/* 触发一个事件 2 */
xEventGroupSetBits(Event_Handle,KEY2_EVENT);
}
vTaskDelay(20); //每 20ms 扫描一次
}
}
/***********************************************************************
* @ 函数名 : BSP_Init
* @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
* @ 参数 :
* @ 返回值 : 无
*********************************************************************/
static void BSP_Init(void)
{
/*
* STM32 中断优先级分组为 4,即 4bit 都用来表示抢占优先级,范围为:0~15
* 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
* 都统一用这个优先级分组,千万不要再分组,切忌。
*/
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
/* LED 初始化 */
LED_GPIO_Config();
/* 串口初始化 */
USART_Config();
/* 按键初始化 */
Key_GPIO_Config();
}
4.2 中断式
#define BIT_0 ( 1 << 0 )
#define BIT_4 ( 1 << 4 )
/* 假定事件组已经被创建 */
EventGroupHandle_t xEventGroup;
/* 中断 ISR */
void anInterruptHandler( void )
{
BaseType_t xHigherPriorityTaskWoken, xResult;
/* xHigherPriorityTaskWoken 在使用之前必须先初始化为 pdFALSE */
xHigherPriorityTaskWoken = pdFALSE;
/* 置位事件组 xEventGroup 的的 Bit0 和 Bit4 */
xResult = xEventGroupSetBitsFromISR(xEventGroup, BIT_0 | BIT_4, &xHigherPriorityTaskWoken );
/* 信息是否发送成功 */
if ( xResult != pdFAIL )
{
/* 如果 xHigherPriorityTaskWoken 的值为 pdTRUE 则进行一次上下文切换*/
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
}
• 由 Leung 写于 2020 年 11 月 24 日
• 参考:野火FreeRTOS视频与PDF教程