资料来源于硬件家园:资料汇总 - FreeRTOS实时操作系统课程(多任务管理)
目录
一、事件的概念与应用
1、事件的概念
2、事件的应用
二、事件的运作机制
1、FreeRTOS中事件组的句柄
2、FreeRTOS 任务间事件标志组的实现
3、FreeRTOS 中断方式事件标志组的实现
三、事件的API函数
1、事件的典型流程与API
2、事件组创建与删除
3、任务内置位事件组
4、中断内置位事件组
5、等待事件组
四、事件的应用 - 任务与任务
1、任务配置
2、创建事件组
3、按键任务部分代码
4、同步任务代码
五、事件的应用 - 中断与任务
1、任务配置
2、开启FreeRTOS软件定时器
3、创建事件组
4、同步任务代码
5、串口回调函数代码
一、事件的概念与应用
1、事件的概念
事件是实现任务与任务或任务与中断间通信的机制,用于同步,无数据传输。
与信号量不同的是,事件可以实现一对多、多对多的同步,即一个任务可以等待多个事件的发生:可以是任意一个事件发生时唤醒任务进行事件处理;也可以是几个事件都发生后才唤醒任务进行事件处理。同样,也可以是多个任务同步多个事件。
FreeRTOS提供的事件具有如下特点:
① 事件相互独立,一个32位的事件集合(EventBitst类型的变量,实际可用于表示事件的只有低24位)用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0表示该事件类型未发生,1表示该事件类型已经发生),一共有 24种事件类型。
②事件仅用于同步,不提供数据传输功能。
③ 事件无排队性,即多次向任务设置同一事件(如果任务还未来得及读取),等效于只设置一次。
④允许多个任务对同一事件进行读写操作。
⑤ 支持事件等待超时机制。
在FreeRTOS 事件中,获取每个事件时,用户可以选择感兴趣的事件,并且选择读取事件信息标记。它有3个属性,分别是逻辑与、逻辑或以及是否清除标记。当任务等待事件同步时,可以通过任务感兴趣的事件位和事件信息标记来判断当前接收的事件是否满足要求,如果满足,则说明任务等到对应的事件,系统将唤醒等待的任务;否则,任务会根据用户指定的阻塞超时时间继续等待下去。
2、事件的应用
FrecRTOS的事件用于任务与任务或任务与中断间的同步。为什么不直接用变量呢?那样岂不是更有效率?若是在裸机编程中,用全局变量是最有效的方法,但是在操作系统中,使用全局变量就要考虑以下问题了:
①如何对全局变量进行保护?如何处理多任务同时对它进行访问的情况?
②如何让内核对事件进行有效管理?
如果使用全局变量,就需要在任务中轮询查看事件是否发送,这会造成CPU 资源的浪费,此外,用户还需要自己去实现等待超时机制。所以,在操作系统中最好还是使用系统提供的通信机制,简单、方便、实用。
在某些场合,可能需要多个事件发生后才能进行下一步操作,比如一些危险机器的启动,需要检查各项指标,当指标不达标时就无法启动。但是检查各个指标时,不会立刻检测完毕,所以需要事件来做统一的等待。当所有的事件都完成了,那么机器才允许启动,这只是事件的应用之一
事件可用于多种场合,能够在一定程度上替代信号量,用于任务与任务间、中断与任务间的同步。一个任务或中断服务例程发送一个事件给事件对象,而后等待的任务被唤醒并对相应的事件进行处理。但是事件与信号量不同的是,事件的发送操作是不可累计的,而信号量的释放动作是可累计的。事件的另外一个特性是,接收任务可等待多种事件,即多个事件对应一个任务或多个任务。同时按照任务等待的参数,可选择是“逻辑或”触发还是“逻辑与”触发。这个特性也是信号量等所不具备的,信号量只能识别单一同步动作,而不能同时等待多个事件的同步。
各个事件可分别发送或一起发送给事件对象,而任务可以等待多个事件,任务仅对感兴趣的事件进行关注。当有它们感兴趣的事件发生并且符合条件时,任务将被唤醒并进行后续的处理动作。
二、事件的运作机制
1、FreeRTOS中事件组的句柄
uxEventBits: 对于STM32,此变量为32位,其中低24位用于事件位,高8位用于其他用途。
2、FreeRTOS 任务间事件标志组的实现
任务间事件标志组的实现是指各个任务之间使用事件标志组实现任务的同步机制。
下面的框图说明FreeRTOS 事件标志的实现:
运行条件: 创建 2 个任务:Task1 和 Task2
运行过程描述如下:
任务 Task1 运行过程中调用函数 xEventGroupWaitBits,等待事件标志位被设置,任务 Task1 由运行态进入到阻塞态。
任务 Task2 设置 Task1 等待的事件标志,任务 Task1 由阻塞态进入到就绪态,在调度器的作用下由就绪态又进入到运行态。
上面就是一个简单的 FreeRTOS 任务间事件标志通信过程。
3、FreeRTOS 中断方式事件标志组的实现
FreeRTOS 中断方式事件标志组的实现是指中断函数和 FreeRTOS 任务之间使用事件标志。
下面的框图说明FreeRTOS 事件标志的实现:
运行条件: 创建一个任务和一个串口接收中断
运行过程描述如下:
任务 Task1 运行过程中调用函数 xEventGroupWaitBits,等待事件标志位被设置,任务 Task1 由运行态进入到阻塞态。
Task1 阻塞的情况下,串口接收到数据进入到了串口中断服务程序,在串口中断服务程序中设置 Task1等待的事件标志,任务 Task1 由阻塞态进入到就绪态,在调度器的作用下由就绪态又进入到运行态。
上面就是一个简单的 FreeRTOS 中断方式事件标志通信过程。
实际应用中,中断方式的消息机制要注意以下三个问题:
中断函数的执行时间越短越好,防止其它低于这个中断优先级的异常不能得到及时响应。
实际应用中,建议不要在中断中实现消息处理,用户可以在中断服务程序里面发送消息通知任务,在任务中实现消息处理,这样可以有效地保证中断服务程序的实时响应。同时此任务也需要设置为高优先级,以便退出中断函数后任务可以得到及时执行。
中断服务程序中一定要调用专用于中断的事件标志设置函数,即以 FromISR 结尾的函数。
三、事件的API函数
1、事件的典型流程与API
> 创建事件组 xEventGroupCreate()
> 置位事件组 xEventGroupSetBits() , xEventGroupSetBitsFromISR()
> 等待事件组 xEventGroupWaitBits()
> 删除事件组 vEventGroupDelete()
2、事件组创建与删除
>事件组控制块(句柄)
uxEventBits: 对于STM32,此变量为32位,其中低24位用于事件位
xTasksWaitingForBits: 任务等待链表
>创建事件组
函数原型:EventGroupHandle_t xEventGroupCreate( void );
函数描述:函数 xEventGroupCreate 用于创建事件标志组。
返回值,如果创建成功,此函数返回事件标志组的句柄,如果 FreeRTOSConfig.h 文件中定义的 heap空间不足会返回 NULL
使用举例:
>删除事件组
函数原型:vEventGroupDelete(EventGroupHandle_t xEventGroup);
函数描述:函数 vEventGroupDelete可用于删除事件组。
3、任务内置位事件组
函数原型:EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );
函数描述:函数 xEventGroupSetBits 用于设置指定的事件标志位为 1。
第 1 个参数是事件标志组句柄。
第 2 个参数表示 24 个可设置的事件标志位,EventBits_t 是定义的 32 位变量,低 24 位用于事件标志设置。变量 uxBitsToSet 的低 24 位的某个位设置为 1,那么被设置的事件标志组的相应位就设置为 1。变量 uxBitsToSet 设置为 0 的位对事件标志相应位没有影响。比如设置变量 uxBitsToSet = 0x0003 就表示将事件标志的位 0 和位 1 设置为 1,其余位没有变化。
返回当前的事件标志组数值。
使用这个函数要注意以下问题:
1. 使用前一定要保证事件标志组已经通过函数 xEventGroupCreate 创建了。
2. 此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,中断服务程序中使用的是xEventGroupSetBitsFromISR
3. 用户通过参数 uxBitsToSet 设置的标志位并不一定会保留到此函数的返回值中,下面举两种情况:
a. 调用此函数的过程中,其它高优先级的任务就绪了,并且也修改了事件标志,此函数返回的事件标志位会发生变化。
b. 调用此函数的任务是一个低优先级任务,通过此函数设置了事件标志后,让一个等待此事件标志的高优先级任务就绪了,会立即切换到高优先级任务去执行,相应的事件标志位会被函数
xEventGroupWaitBits 清除掉,等从高优先级任务返回到低优先级任务后,函数
xEventGroupSetBits 的返回值已经被修改。
使用举例:
4、中断内置位事件组
函数原型:BaseType_t xEventGroupSetBitsFromISR(EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t *pxHigherPriorityTaskWoken );
函数描述:函数 xEventGroupSetBits 用于设置指定的事件标志位为 1。
第 1 个参数是事件标志组句柄。
第 2 个参数表示 24 个可设置的事件标志位,EventBits_t 是定义的 32 位变量,低 24 位用于事件标志设置。变量 uxBitsToSet 的低 24 位的某个位设置为 1,那么被设置的事件标志组的相应位就设置为 1。变量 uxBitsToSet 设置为 0 的位对事件标志相应位没有影响。比如设置变量 uxBitsToSet = 0x0003 就表示将事件标志的位 0 和位 1 设置为 1,其余位没有变化。
第 3 个参数用于保存是否有高优先级任务准备就绪。如果函数执行完毕后,此参数的数值是 pdTRUE,说明有高优先级任务要执行,否则没有。
返回值,如果消息成功发送给守护任务(就是FreeRTOS 的定时器服务任务)返回 pdPASS,否则返回 pdFAIL,另外守护任务中的消息队列满了也会返回 pdFAIL。
使用这个函数要注意以下问题:
1. 使用前一定要保证事件标志已经通过函数 xEventGroupCreate 创建了。同时要在 FreeRTOSConfig.h
文件中使能如下三个宏定义:
#define INCLUDE_xEventGroupSetBitFromISR 1
#define configUSE_TIMERS 1
#define INCLUDE_xTimerPendFunctionCall 1
2. 函数 xEventGroupSetBitsFromISR 是用于中断服务程序中调用的,故不可以在任务代码中调用此函数,任务代码中使用的是 xEventGroupSetBits。
3. 函数 xEventGroupSetBitsFromISR 对事件标志组的操作是不确定性操作,因为不知道当前有多少个任务在等待此事件标志。而 FreeRTOS 不允许在中断服务程序和临界段中执行不确定性操作。为了不在中断服务程序中执行,就通过此函数给 FreeRTOS 的守护任务(就是 FreeRTOS 的定时器服务任务,内核自动创建的)发送消息,在守护任务中执行事件标志的置位操作。同时也为了不在临界段中执行此不确定操作,将临界段改成由调度锁来完成。这样不确定性操作在中断服务程序和临界段中执行的问题就都得到解决了。
4. 由于函数 xEventGroupSetBitsFromISR 对事件标志的置位操作是在守护任务里面执行的,如果想让置位操作立即生效,即让等此事件标志的任务能够得到及时执行,需要设置守护任务的优先级高于使用此事件标志组的所有其它任务。
使用举例:
5、等待事件组
函数原型:
EventBits_t xEventGroupWaitBits(
const EventGroupHandle_t xEventGroup, /* 事件标志组句柄 */
const EventBits_t uxBitsToWaitFor, /* 等待被设置的事件标志位 */
const BaseType_t xClearOnExit, /* 选择是否清零被置位的事件标志位 */
const BaseType_t xWaitForAllBits, /* 选择是否等待所有标
TickType_t xTicksToWait ); /* 设置等待时间 */
函数描述:
函数 xEventGroupWaitBits 等待事件标志被设置。
第 1 个参数是事件标志组句柄。
第 2 个参数表示等待 24 个事件标志位中的指定标志,EventBits_t 是定义的 32 位变量,低 24 位用于事件标志设置。比如设置变量 uxBitsToWaitFor = 0x0003 就表示等待事件标志的位 0 和位 1 设置为 1。此参数切不可设置为 0。
第 3 个参数选择是否清除已经被置位的事件标志,如果这个参数设置为 pdTRUE,且函数
xEventGroupWaitBits 在参数 xTicksToWait 设置的溢出时间内返回或等到满足任务唤醒的事件时,相应被设置的事件标志位会被清零。如果这个参数设置为 pdFALSE,对已经被设置的事件标志位没有影响。
第 4 个参数选择是否等待所有的标志位都被设置,如果这个参数设置为 pdTRUE,要等待第 2 个参数 uxBitsToWaitFor 所指定的标志位全部被置 1,函数才可以返回。当然,超出了在参数xTicksToWait 设置的溢出时间也是会返回的。如果这个参数设置为 pdFALSE,第 2 个参数uxBitsToWaitFor 所指定的任何标志位被置 1,函数都会返回,超出溢出时间也会返回。
第 5 个参数设置等待时间,单位时钟节拍周期。如果设置为 portMAX_DELAY,表示永久等待。
返回值,由于设置的时间超时或者指定的事件标志位被置 1,导致函数退出时返回的事件标志组数值。
使用这个函数要注意以下问题:
1. 此函数切不可在中断服务程序中调用。
2. 着重说明下这个函数的返回值,通过返回值用户可以检测是哪个事件标志位被置 1 了
如果由于设置的等待时间超时,函数的返回值可会有部分事件标志位被置 1。
如果由于指定的事件标志位被置1而返回,并且设置了这个函数的参数xClearOnExit为pdTRUE,那么此函数的返回值是清零前的事件标志组数值。
另外,调用此函数的任务在离开阻塞状态到退出函数 xEventGroupWaitBits 之间这段时间,如果一个高优先级的任务抢占执行了,并且修改了事件标志位,那么此函数的返回值会跟当前的事件标志组数值不同。
使用举例:
四、事件的应用 - 任务与任务
1、任务配置
LED0_Task: 运行指示灯
KEY_Task: 按键,打印任务信息,触发事件
Event_Sync_Task: 等待事件,同步任务
2、创建事件组
定义
/* USER CODE BEGIN Variables */
EventGroupHandle_t MyEvent01Handle=NULL; //不能配置,只能自己写
/* USER CODE END Variables */
创建
/* USER CODE BEGIN Init */
MyEvent01Handle = xEventGroupCreate();
if(MyEvent01Handle==NULL)
{
sprintf(buff,"%s \r\n","创建事件组失败");
HAL_UART_Transmit(&huart2, (uint8_t*)buff,strlen(buff), HAL_MAX_DELAY);
}
else
{
sprintf(buff,"%s \r\n","创建事件组成功");
HAL_UART_Transmit(&huart2, (uint8_t*)buff,strlen(buff), HAL_MAX_DELAY);
}
/* USER CODE END Init */
3、设置事件
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define KEY0_EVENT (EventBits_t)(0x0001 << 0)//设置事件掩码位0
#define KEY1_EVENT (EventBits_t)(0x0001 << 8)//设置事件掩码位8
/* USER CODE END PD */
4、按键任务部分代码
//KEY0
if(KeyCode==KEY0)
{
sprintf(buff,"%s \r\n","触发 KEY0 事件");
HAL_UART_Transmit(&huart2, (uint8_t*)buff,strlen(buff), HAL_MAX_DELAY);
xEventGroupSetBits(MyEvent01Handle, KEY0_EVENT);
}
//KEY1
if(KeyCode==KEY1)
{
sprintf(buff,"%s \r\n","触发 KEY1 事件");
HAL_UART_Transmit(&huart2, (uint8_t*)buff,strlen(buff), HAL_MAX_DELAY);
xEventGroupSetBits(MyEvent01Handle, KEY1_EVENT);
}
5、预编译宏
#define Event_WaitAllBits //预编译,看是否需要等待所有事件
6、同步任务代码
void Event_Sync_Task(void const * argument)
{
/* USER CODE BEGIN Event_Sync_Task */
EventBits_t xEvent;
int SyncCnt = 0; //同步计数
/* Infinite loop */
#ifdef Event_WaitAllBits
for(;;)
{
sprintf(buff,"%s \r\n","等待事件同步信号,无限等待");
HAL_UART_Transmit(&huart2, (uint8_t*)buff,strlen(buff), HAL_MAX_DELAY);
xEvent = xEventGroupWaitBits(
MyEvent01Handle, //事件句柄
KEY0_EVENT|KEY1_EVENT, //事件 按键0,1
pdTRUE, //退出时清除事件位
pdTRUE, //"逻辑或" - 满足任一事件
portMAX_DELAY //无限等待
);
if( (xEvent&(KEY0_EVENT|KEY1_EVENT)) == (KEY0_EVENT|KEY1_EVENT))
{
sprintf(buff,"成功接受到事件同步信号,次数 = %u\r\n",++SyncCnt);
HAL_UART_Transmit(&huart2, (uint8_t*)buff,strlen(buff), HAL_MAX_DELAY);
}
}
#else
for(;;)
{
sprintf(buff,"%s \r\n","等待事件同步信号,无限等待");
HAL_UART_Transmit(&huart2, (uint8_t*)buff,strlen(buff), HAL_MAX_DELAY);
xEvent = xEventGroupWaitBits(
MyEvent01Handle, //事件句柄
KEY0_EVENT|KEY1_EVENT, //事件 按键0,1
pdTRUE, //退出时清除事件位
pdFALSE, //"逻辑或" - 满足任一事件
portMAX_DELAY //无限等待
);
if(((xEvent&KEY0_EVENT) == KEY0_EVENT) || ((xEvent&KEY1_EVENT) == KEY1_EVENT))
{
sprintf(buff,"成功接受到事件同步信号,次数 = %d\r\n",++SyncCnt);
HAL_UART_Transmit(&huart2, (uint8_t*)buff,strlen(buff), HAL_MAX_DELAY);
}
}
#endif
/* USER CODE END Event_Sync_Task */
/* USER CODE BEGIN BinarySem_Sync_Task */
}
五、事件的应用 - 中断与任务
1、任务配置
2、开启FreeRTOS软件定时器
3、创建事件组
同上任务与任务
4、同步代码
void Event_Sync_ISR_Task(void const * argument)
{
/* USER CODE BEGIN Event_Sync_Task */
EventBits_t xEvent;
char rxBuff[13];
/* Infinite loop */
for(;;)
{
//通过串口2中断接收12个字符
HAL_UART_Receive_IT(&huart2, (uint8_t *)rxBuff, 12);
sprintf(buff,"请通过UART2发送12个字符\r\n");
HAL_UART_Transmit(&huart2, (uint8_t*)buff,strlen(buff), HAL_MAX_DELAY);
sprintf(buff,"等待事件同步信号,无限等待\r\n");
HAL_UART_Transmit(&huart2, (uint8_t*)buff,strlen(buff), HAL_MAX_DELAY);
xEvent = xEventGroupWaitBits(
MyEvent01Handle, //事件句柄
UART2_RecEVENT, //事件 - 串口3接收
pdTRUE, //退出时清除事件位
pdTRUE, //"逻辑与" - 满足所有事件
portMAX_DELAY //无限等待
);
if((xEvent&UART2_RecEVENT) == UART2_RecEVENT)
{
sprintf(buff,"接收到的串口数据:%s\r\n\r\n",rxBuff);
HAL_UART_Transmit(&huart2, (uint8_t*)buff,strlen(buff), HAL_MAX_DELAY);
}
HAL_UART_Receive_IT(&huart2, (uint8_t *)rxBuff, 12);
}
/* USER CODE END Event_Sync_Task */
}
5、串口回调函数代码
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if(huart->Instance == huart2.Instance)
{
xEventGroupSetBitsFromISR(MyEvent01Handle,UART2_RecEVENT,&xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}