目录
- FreeRTOS 事件标志组
- FreeRTOS 事件标志组简介
- FreeRTOS 事件标志组相关API 函数
- FreeRTOS 事件标志组实验
- 功能设计
- 软件设计
- 下载验证
- FreeRTOS 任务通知
- FreeRTOS 任务通知简介
- 任务通知的优势
- 任务通知的缺点
- FreeRTOS 任务通知相关API 函数
FreeRTOS 事件标志组
事件标志组与信号量一样属于任务间同步的机制,但是信号量一般用于任务间的单事件同
步,对于任务间的多事件同步,仅使用信号量就显得力不从心了。FreeRTOS 提供的事件标志组
可以很好的处理多事件情况下的任务同步。本章就来学习FreeRTOS 中事件标志组的相关内容。
FreeRTOS 事件标志组简介
- 事件标志
事件标志是一个用于指示事件是否发生的布尔值,一个事件标志只有0 或1 两种状态,
FreeRTOS 将多个事件标志储存在一个变量类型为EventBits_t 变量中,这个变量就是事件组。 - 事件组
事件组是一组事件标志的集合,一个事件组就包含了一个EventBites_t 数据类型的变量,
变量类型EventBits_t 的定义如下所示:
typedef TickType_t EventBits_t;#
if (configUSE_16_BIT_TICKS == 1)
typedef uint16_t TickType_t;#
else
typedef uint32_t TickType_t;#
endif# define configUSE_16_BIT_TICKS 0
从上面可以看出,EventBits_t 实际上是一个16 位或32 位无符号的数据类型。当
configUSE_16_BIT_TICKS 配置为0 时,EventBits_t 是一个32 位无符号的数据类型;当
configUSE_16_BIT_TICKS 配置为1 时,EventBits_t 是一个16 为无符号的数据类型。在本套教
程的所有配套例程中,都将配置项configUSE_16_BIT_TICKS 配置为0,因此本文就以
EventBits_t 为32 位无符号数据类型为例进行讲解,对于另外一种情况,也是大同小异的。
虽然说使用了32 位无符号的数据类型变量来存储事件标志,但这并不意味着,一个
EventBits_t 数据类型的变量能够存储32 个事件标志,FreeRTOS 将这个EventBits_t 数据类型的
变量拆分成两部分,其中低24 位[23:0] (configUSE_16_BIT_TICKS 配置位1 时,是低8 位[7:0])
用于存储事件标志,而高8 位[31:24](configUSE_16_BIT_TICKS 配置位1 时,依然是高8 位
[15:8])用作存储事件标志组的一些控制信息,也就是说一个事件组最多可以存储24 个事件标
志。EventBits_t 数据类型变量的位使用情况如下图所示:
从上图中可以看到,变量中低24 位中的每一位都是一个事件标志,当某一位被置一时,就
表示这一位对应的事件发生了。
FreeRTOS 事件标志组相关API 函数
FreeRTOS 提供了事件标志组的一些相关操作函数,如下表所示:
- 创建事件标志组
FreeRTOS 提供了两种创建事件标志组的方式,分别为动态方式创建事件标志组和静态方
式创建事件标志组,两者的区别在于静态方式创建事件标志组时,需要用户提供创建事件标志
组所需的内存空间,而使用动态方式创建事件标志组时,FreeRTOS 会自动从FreeRTOS 管理的
堆中分配创建事件标志组所需的内存空间。
动态方式创建事件标志组API 函数的函数原型如下所示:
EventGroupHandle_t xEventGroupCreate(void);
函数xEventGroupCreate()的形参描述,如下表所示:
静态方式创建事件标志组API 函数的函数原型如下所示:
EventGroupHandle_t xEventGroupCreateStatic(
StaticEventGroup_t * pxEventGroupBuffer);
函数xEventGroupCreateStatic()的形参描述,如下表所示:
2. 删除事件标志组
FreeRTOS 提供了用于删除事件标志组的API 函数,函数原型如下所示:
void vEventGroupDelete(EventGroupHandle_t xEventGroup);
3. 等待事件标志位
等待事件标志位使用的是函数xEventGroupWaitBits(),其函数原型如下所示:
EventBits_t xEventGroupWaitBits(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait)
4. 设置事件标志位
FreeRTOS 提供了两个用于设置事件标志位的API 函数,这个两个函数分别用于在任务和
在中断中设置事件标志位。
在任务中设置事件标志位API 函数的函数原型如下所示:
EventBits_t xEventGroupSetBits(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet)
在中断中设置事件标志位API 函数的函数原型如下所示:
BaseType_t xEventGroupSetBitsFromISR(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t * pxHigherPriorityTaskWoken)
函数xEventGroupSetBitsFromISR()的形参描述,如下表所示:
5. 清零事件标志位
FreeRTOS 提供了两个用于清零事件标志位的API 函数,这个两个函数分别用于在任务和
在中断中清零事件标志位。
在任务中清零事件标志位API 函数的函数原型如下所示:
EventBits_t xEventGroupClearBits(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToClear)
函数xEventGroupClearBits()的形参描述,如下表所示:
函数xEventGroupClearBits()的返回值,如下表所示:
在中断中清零事件标志位API 函数的函数原型如下所示:
BaseType_t xEventGroupClearBitsFromISR(
EventGroupHandle_t EventGroup,
const EventBits_t uxBitsToClear)
6. 获取事件组中事件标志位的值
FreeRTOS 提供了两个用于获取事件组中事件标志位值的API 函数,这个两个函数分别用
于在任务和在中断中获取事件组中事件标志位的值。
在任务中获取事件组中事件标志位值API 函数的函数原型如下所示:
EventBits_t xEventGroupGetBits(xEventGroup);
7. 函数xEventGroupSync()
此函数一般用于多任务同步,其中每个任务都必须等待其他任务达到同步点,然后才能继
续执行。函数xEventGroupSync()的函数原型如下所示:
EventBits_t xEventGroupSync(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait)
FreeRTOS 事件标志组实验
功能设计
- 例程功能
本实验主要用于学习FreeRTOS 事件标志组相关API 函数的使用,本实验设计了四个任务,
这四个任务的功能如下表所示:
该实验的实验工程,请参考《FreeRTOS 实验例程16 FreeRTOS 事件标志组实验》。
软件设计
- 程序流程图
本实验的程序流程图,如下图所示:
- 程序解析
整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
(1) start_task 任务
start_task 任务的入口函数代码如下所示:
/**
* @brief start_task
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void start_task(void * pvParameters) {
taskENTER_CRITICAL(); /* 进入临界区*/
/* 创建事件标志组*/
EventGroupHandler = xEventGroupCreate();
/* 创建任务1 */
xTaskCreate((TaskFunction_t) task1, (const char * )
"task1", (uint16_t) TASK1_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK1_PRIO, (TaskHandle_t * ) & Task1Task_Handler);
/* 创建任务2 */
xTaskCreate((TaskFunction_t) task2, (const char * )
"task2", (uint16_t) TASK2_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK2_PRIO, (TaskHandle_t * ) & Task2Task_Handler);
/* 创建任务3 */
xTaskCreate((TaskFunction_t) task3, (const char * )
"task3", (uint16_t) TASK3_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK3_PRIO, (TaskHandle_t * ) & Task3Task_Handler);
vTaskDelete(StartTask_Handler); /* 删除开始任务*/
taskEXIT_CRITICAL(); /* 退出临界区*/
}
start_task 任务主要用于创建事件标志组、task1 任务、task2 任务和task3 任务。
(2) task1 任务
/**
* @brief task1
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void task1(void * pvParameters) {
uint8_t key = 0;
while (1) {
key = key_scan(0);
switch (key) {
case KEY0_PRES:
{
/* 设置事件组的事件标志0 */
xEventGroupSetBits((EventGroupHandle_t) EventGroupHandler, (EventBits_t) EVENTBIT_0);
break;
}
case KEY1_PRES:
{
/* 设置事件组的事件标志1 */
xEventGroupSetBits((EventGroupHandle_t) EventGroupHandler, (EventBits_t) EVENTBIT_1);
break;
}
default:
{
break;
}
}
vTaskDelay(10);
}
}
task1 任务主要用于扫描按键,当按下按键0 时,设置事件组的事件标志0,当按下按键1
时,设置事件组的事件标志1。
(3) task2 任务
/**
* @brief task2
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void task2(void * pvParameters) {
uint32_t task2_num = 0;
while (1) {
/* 等待事件标志0和事件标志1
* 如果成功等待到了事件标志0和1,则清零事件标志0和1
* 等待的方式为逻辑与,即事件标志0和1需要被同时设置
*/
xEventGroupWaitBits((EventGroupHandle_t) EventGroupHandler, (EventBits_t) EVENTBIT_ALL, (BaseType_t) pdTRUE, (BaseType_t) pdTRUE, (TickType_t) portMAX_DELAY);
/* LCD区域刷新*/
lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]);
vTaskDelay(10);
}
}
可以看到task2 任务等待事件组中的事件标志0 和1 同时被设置,只有事件组中的视角标
志0 和1 被同时设置,task2 任务才会继续执行,同时如果成功等待到了事件标志0 和1 被同时
设置,那么还会自动清零事件组中的事件标志0 和1,无需手动清零。
(4) task3 任务
/**
* @brief task3
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void task3(void * pvParameters) {
EventBits_t event_val = 0;
while (1) {
/* 获取事件组的所有事件标志值*/
event_val = xEventGroupGetBits((EventGroupHandle_t) EventGroupHandler);
/* 在LCD上显示事件值*/
lcd_show_xnum(182, 110, event_val, 1, 16, 0, BLUE);
vTaskDelay(10);
}
}
从上面的代码可以看出,task3 任务获取了事件组的事件标志值,并将事件组的事件标志值
实时显示到LCD 屏幕上。
下载验证
编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
一开始可以看到,事件组的事件标志值为0。此时按键按键0,来设置事件组的事件标志0,
可以看到LCD 上显示的事件组事件标志值为1,此值正是事件标志0 被设置后的值,如下图所
示:
接着多次按下按键0,LCD 显示的内容都不会发生变化,因为事件标志0 已经被设置了。
接着按键按键1,来设置事件组的事件标志1,可以看到LCD 上显示的事件组事件标志值
直接清零,并且LCD 屏幕区域也刷新了,如下图所示:
这是因为,当事件标志1 被设置后,task2 任务立马就等待到了事件标志0 和1 同时被设
置,因此task2 任务就将事件组的事件标志0 和1 清零了,并且task2 解除阻塞得以运行,所以
LCD 屏幕区域被刷新了。
在本次实验中,不论按键0 或者按键1 中的哪一个按键先被按下,即不论事件组中的事件
标志0 还是事件标志1 中的哪一个先被设置,只要事件标志0 和事件标志1 存在同时处于被设
置的状态,那么task2 任务就等待事件组中的事件标志成功,于是task2 任务解除阻塞,得以执
行。
FreeRTOS 任务通知
FreeRTOS 内核V8.2.0 版本发布的时候,FreeRTOS 新增了任务通知这个功能,任务通知也
是用于任务间进行同步和通讯的一种机制,但是相对于前面章节介绍的队列、事件标志组和信
号量等而言,任务通知在内存占用和效率方面都有很大的优势。本章就来学习FreeRTOS 中任
务通知的相关内容。
FreeRTOS 任务通知简介
在FreeRTOS 中,每一个任务都有两个用于任务通知功能的数组,分别为任务通知数组和
任务通知状态数组。其中任务通知数组中的每一个元素都是一个32 位无符号类型的通知值;而
任务通知状态数组中的元素则表示与之对应的任务通知的状态。
任务通知数组中的32 位无符号通知值,用于任务到任务或中断到任务发送通知的“媒介”。
当通知值为0 时,表示没有任务通知;当通知值不为0 时,表示有任务通知,并且通知值就是
通知的内容。
任务通知状态数组中的元素,用于标记任务通知数组中通知的状态,任务通知有三种状态,
分别为未等待通知状态、等待通知状态和等待接收通知状态。其中未等待通知状态为任务通知
的复位状态;当任务在没有通知的时候接收通知时,在任务阻塞等待任务通知的这段时间内,
任务所等待的任务通知就处于等待通知状态;当有其他任务向任务发送通知,但任务还未接收
这一通知的这段期间内,任务通知就处于等待接收通知状态。
任务通知功能所使用到的任务通知数组和任务通知状态数组为任务控制块中的成员变量,
因此任务通知的传输是直接传出到任务中的,不同通过任务的通讯对象(队列、事件标志组和
信号量就属于通讯对象)这个间接的方式。间接通讯示意图如下所示:
任务通知则是直接地往任务中发送通知,直接通讯示意图如下所示:
任务通知的优势
使用任务通知向任务发送事件或数据比使用队列、事件标志组或信号量快得多;并且使用
任务通知代替队列、事件标志组或信号量,可以节省大量的内存,这是因为每个通讯对象在使
用之前都需要被创建,而任务通知功能中的每个通知只需要在每个任务中占用固定的5 字节内
存。
任务通知的缺点
虽然任务通知功能相比通讯对象,有着更快、占用内存少的优点,但是任务通知功能并不
能适用于所有情况,例如以下列出的几种情况:
- 发送事件或数据到中断
通讯对象可以发送事件或数据从中断到任务,或从任务到中断,但是由于任务通知依赖于
任务控制块中的两个成员变量,并且中断不是任务,因此任务通知功能并不适用于从任务往中
断发送事件或数据的这种情况,但是任务通知功能可以在任务之间或从中断到任务发送事件或
数据。 - 存在多个接收任务
通讯对象可以被已知通讯对象句柄的任意多个任务或中断访问(发送或接收),但任务通知
是直接发送事件或数据到指定接收任务的,因传输的事件或数据只能由接收任务处理。然而在
实际中很少受到这种情况的限制,因为,虽然多个任务和中断发送事件或数据到一个通讯对象
是很常见的,但很少出现多个任务或中断接收同一个通讯对象的情况。 - 缓冲多个数据项
通讯对象中的队列是可以一次性保存多个已经被发送到队列,但还未被接收的事件或数据
的,也就是说,通讯对象有着一定的缓冲多个数据的能力,但是任务通知是通过更新任务通知
值来发送事件或数据的,一个任务通知值只能保存一次。 - 广播到多个任务
通讯对象中的事件标志组是可以将一个事件同时发送到多个任务中的,但任务通知只能是
被指定的一个接收任务接收并处理。 - 阻塞等待接收任务
当通讯对象处于暂时无法写入的状态(例如队列已满,此时无法再向队列写入消息)时,
发送任务是可以选择阻塞等待接收任务接收,但是任务因尝试发送任务通知到已有任务通知但
还未处理的任务而进行阻塞等待的。但是任务通知也很少在实际情况中收到这种情况的限制。
FreeRTOS 任务通知相关API 函数
FreeRTOS 提供了任务通知的一些相关操作函数,其中任务通知相关API 函数,如下两表所示:
以上两表列出了FreeRTOS 提供的几个任务通知相关的操作函数,从第17.1 小节《FreeRTOS
任务通知简介》中,可以知道任务的任务控制块中,与任务通知功能相关的两个成员变量,任
务通知值和任务通知状态,是两个数组,也就是说,一个任务可以有多个任务通知,多个通知
就通过数组的下标进行索引。
表17.2.1 所列出的API 函数都是对任务通知相关数组中下标为0 的元素进行操作,而表
17.2.2 中列出的API 函数可以指定对任务通知相关数组中的元素进行操作。两表中对应的API
函数原理上是一样的,只是表17.2.1 中的API 是固定对任务的任务通知0 进行操作,而表17.2.2
中的API 函数可以对任务的指定任务通知进行操作,本文以表17.2.1 中的函数为例进行讲解。
- 发送任务通知
表17.2.1 中发送任务通知的三个API 函数的定义如下所示:
#
define xTaskNotify(xTaskToNotify, \
ulValue, \
eAction)\
xTaskGenericNotify((xTaskToNotify), \ (tskDEFAULT_INDEX_TO_NOTIFY), \ (ulValue), \ (eAction), \
NULL)# define xTaskNotifyAndQuery(xTaskToNotify, \
ulValue, \
eAction, \
pulPreviousNotifyValue)\
xTaskGenericNotify((xTaskToNotify), \ (tskDEFAULT_INDEX_TO_NOTIFY), \ (ulValue), \ (eAction), \ (pulPreviousNotifyValue))# define xTaskNotifyGive(xTaskToNotify)\
xTaskGenericNotify((xTaskToNotify), \ (tskDEFAULT_INDEX_TO_NOTIFY), \ (0), \
eIncrement, \
NULL)
从上面的代码中可以看出,三个用于在任务中发送任务通知的函数,实际上都是调用了函
数xTaskGenericNotify()来发送任务通知的,只是传入了不同的参数。函数xTaskGenericNotify()
的函数原型如下所示:
BaseType_t xTaskGenericNotify(TaskHandle_t xTaskToNotify,
UBaseType_t uxIndexToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t * pulPreviousNotificationValue);
函数xTaskGenericNotify()的形参描述,如下表所示:
函数xTaskGenericNotify()的返回值,如下表所示:
函数xTaskGenericNotify()的源代码如下所示:
。。。。
结合函数xTaskNotify()、函数xTaskNotifyAndQuery()、函数xTaskNotifyGive()的定义和以
上代码,可以知道函数xTaskNotify()、函数xTaskNotifyAndQuery()、函数xTaskNotifyGive()的
作用如下所示:
函数xTaskNotify():
此函数用于往指定任务发送任务通知,通知方式可以自由指定,并且不获取发送任务通知
前任务通知的通知值。
函数xTaskNotifyAndQuery():
此函数用于往指定任务发送任务通知,通知方式可以自由指定,并且获取发送任务通知前
任务通知的通知值。
函数xTaskNotifyGive():
此函数用于往指定任务发送任务通知,通知方式为将通知值加1,并且不获取发送任务通
知前任务通知的通知值。
2. 在中断中发送任务通知
表17.2.1 中在中断中发送任务通知的三个API 函数的定义如下所示:
#define xTaskNotifyFromISR( xTaskToNotify, \
ulValue, \
eAction, \
pxHigherPriorityTaskWoken) \
xTaskGenericNotifyFromISR( (xTaskToNotify), \
(tskDEFAULT_INDEX_TO_NOTIFY), \
(ulValue), \
(eAction), \
NULL, \
(pxHigherPriorityTaskWoken))
#define xTaskNotifyAndQueryFromISR( xTaskToNotify, \
ulValue, \
eAction, \
pulPreviousNotificationValue, \
pxHigherPriorityTaskWoken) \
xTaskGenericNotifyFromISR( (xTaskToNotify), \
(tskDEFAULT_INDEX_TO_NOTIFY), \
(ulValue), \
(eAction), \
(pulPreviousNotificationValue), \
(pxHigherPriorityTaskWoken))
#define vTaskNotifyGiveFromISR( xTaskToNotify, \
pxHigherPriorityTaskWoken ) \
vTaskGenericNotifyGiveFromISR( (xTaskToNotify), \
(tskDEFAULT_INDEX_TO_NOTIFY), \
( pxHigherPriorityTaskWoken));
从上面的代码可以看出,函数xTaskNotifyFromISR()和函数xTaskNotifyAndQueryFromISR()
实际上都是调用了函数xTaskGenericNotifyFromISR(),而函数vTaskNotifyGiveFromISR()实际上
则是调用了函数vTaskGenericNotifyGiveFromISR()。下面就分别看一下以上这两个实际被调用
的函数。
函数xTaskGenericNotifyFromISR()的函数原型如下所示:
BaseType_t xTaskGenericNotifyFromISR(
TaskHandle_t xTaskToNotify,
UBaseType_t uxIndexToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t * pulPreviousNotificationValue,
BaseType_t * pxHigherPriorityTaskWoken);
函数xTaskGenericNotifyFromISR()的源代码如下所示:
。。。。
从上面的代码中可以看出,函数xTaskGenericNotifyFromISR()于函数xTaskNotify()是很相
似的,只是多了对中断做了一些相应的处理。
函数vTaskGenericNotifyGiveFromISR()的函数原型如下所示:
void vTaskGenericNotifyGiveFromISR(
TaskHandle_t xTaskToNotify,
UBaseType_t uxIndexToNotify,
BaseType_t * pxHigherPriorityTaskWoken);
函数vTaskGenericNotifyGiveFromISR()的源代码如下所示:
。。。。
从以上代码中可以看出,函数vTaskGenericNotifyGiveFromISR()就是通知方式为eIncrement
并且没有返回值的函数xTaskGenericNotifyFromISR()。
结合以上函数xTaskGenericNotifyFromISR()和函数vTaskGenericNotifyGiveFromISR()的源
代码和函数xTaskNotifyFromISR() 和函数xTaskNotifyAndQueryFromISR() 和函数
vTaskNotifyGiveFromISR()的定义,表17.2.1 中列出的三个在中断中发送任务通知的API 函数作
用如下:
函数xTaskNotifyFromISR()
此函数用于在中断中往指定任务发送任务通知,通知方式可以自由指定,并且不获取发送
任务通知前任务通知的通知值,但获取发送通知后是否需要进行任务切换的标志。
函数xTaskNotifyAndQueryFromISR()
此函数用于在中断中往指定任务发送任务通知,通知方式可以自由指定,并且获取发送任
务通知前任务通知的通知值,和发送通知后是否需要进行任务切换的标志。
函数vTaskNotifyGiveFromISR()
此函数用于在中断中往指定任务发送任务通知,通知方式为将通知值加1,并且不获取发
送任务通知前任务通知的通知值,但获取发送通知后是否需要进行任务切换的标志。
2. 接收任务通知
用于获取任务通知的API 函数有两个,分别为函数ulTaskNotifyTake() 和函数
xTaskNotifyWait()。
函数ulTaskNotifyTake()
此函数用于获取任务通知的通知值,并且在成功获取任务通知的通知值后,可以指定将通知
值清零或减1。此函数实际上是一个宏定义,具体的代码如下所示:
#define ulTaskNotifyTake(xClearCountOnExit, \
xTicksToWait)\
ulTaskGenericNotifyTake((tskDEFAULT_INDEX_TO_NOTIFY), \ (xClearCountOnExit), \ (xTicksToWait))