FreeRTOS教程8 任务通知
1、准备材料
STM32CubeMX软件(Version 6.10.0)
Keil µVision5 IDE(MDK-Arm)
2、学习目标
本文主要学习 FreeRTOS 任务通知的相关知识,包括FreeRTOS中的通信手段、任务通知的优缺点、任务通知 API 函数等知识
3、前提知识
3.1、FreeRTOS 中的通信手段
一个 FreeRTOS 负责的系统中,总是存在很多任务和中断,这些不同的任务和中断之间往往需要大量的通信来保证整个系统的运行,到本篇文章为止也已经介绍了包括队列、二值/计数信号量、互斥量、递归互斥量和事件组在内的多种通信方式
3.1.1、通过中介对象进行通信
但是目前已经介绍的这些通信方式有一个共同的特点,当使用通信对象时,事件和数据不会直接发送到接收任务或接收 ISR ,而是发送到通信对象。同样,任务和 ISR 从通信对象接收事件和数据,而不是直接从发送事件或数据的任务或 ISR 接收,这个特点可以用下图表示 (注释1)
3.1.2、直接任务通信
本篇文章所介绍的 “任务通知” 允许在不需要额外的中间媒介(通信对象)的情况下,实现任务与其他任务直接交互,并与 ISR 同步。通过使用任务通知,任务或 ISR 可以直接向接收任务发送事件,该特点可以用下图表示
可以在 FreeRTOSConfig.h 文件中设置 configUSE_TASK_NOTIFICATIONS 参数为 1 启动任务通知功能,启动该功能之后,会在每个任务的 TCB (任务控制块)中增加 8 字节空间,此时每个任务都有一个“通知状态”(可以是 “挂起” 或 “未挂起” )和一个 “通知值” (32 位无符号整数)。当任务收到通知时,其通知状态将设置为挂起,当任务读取其通知值时,其通知状态将设置为未挂起
3.2、任务通知的优缺点
3.2.1、优点
任务通知在性能和 RAM 占用上存在优势,具体为以下两点
- 使用任务通知向任务发送事件或数据比使用队列、信号量或事件组执行等效操作要快得多
- 启用任务通知功能的固定开销仅为每个任务 8 个字节的 RAM ,而队列、信号量、事件组等在使用前都必须创建,占用空间较大
3.2.2、缺点
任务通知比通信对象更快并且使用更少的 RAM ,但任务通知不能在所有场景中使用,如下所示记录了无法使用任务通知的场景
- 通信对象可用于将事件和数据从 ISR 发送到任务,以及从任务发送到 ISR;任务通知只能用于将事件和数据从 ISR 发送到任务,不能用于将事件或数据从任务发送到 ISR
- 任何知道通信对象句柄的任务和 ISR 都可以访问通信对象,因此多个任务或 ISR 都可以发送或接收消息;任务通知只能将事件和数据发送到某个具体的接收任务中,发送的事件和数据只能由接收任务使用处理
- 队列是一种通信对象,一次可以保存多个数据项,已发送到队列但尚未从队列接收的数据将缓冲在队列对象内;任务通知通过更新接收任务的通知值来向任务发送数据,任务的通知值一次只能保存一个值
- 事件组是一种通信对象,可用于一次向多个任务发送事件;任务通知直接发送给接收任务,因此只能由接收任务处理
- 如果通信对象暂时处于无法向其写入更多数据或事件的状态(例如,当队列已满时,无法向队列发送更多数据),则尝试写入该对象的任务可以选择进入阻塞状态以等待其写操作完成;如果任务尝试向已经有待处理通知的任务发送任务通知,则发送任务不可能在阻塞状态下等待接收任务重置其通知状态
3.3、任务通知 API 概述
在任务通知这一部分,FreeRTOS 为使用者提供了三组 API 函数,三组 API 的特点如下所述
- 强大通用但较复杂的 xTaskNotify() 和 xTaskNotifyWait() API 函数
- 用作二进制或计数信号量的更轻量级且更快的替代方案的 xTaskNotifyGive() 和 ulTaskNotifyTake() API 函数
- 在序号 1 的基础上增加 pulPreviousNotifyValue 参数值的 xTaskNotifyAndQuery() API函数
上面三组不同类型 API 还拥有其对应的中断安全版本函数, 任务通知可以用来代替二进制信号量、计数信号量、事件组,有时甚至可以代替队列, 但是在大多数情况下使用者可能不需要使用如上述序号 1 中所述 API 函数提供的完全灵活性,一组更简单的函数就足够了
因此开发者提供了 xTaskNotifyGive() API 函数以允许将任务通知用作二值或计数信号量的更轻量级且更快的替代方案,并且提供 ulTaskNotifyTake() API 函数作为 xTaskNotifyWait() 的更简单但灵活性较差的替代方案,所以具体使用哪一组 API 函数可以根据使用者的需要按需使用
3.4、xTaskNotifyGive() 和 ulTaskNotifyTake() API 函数
xTaskNotifyGive() 直接向任务发送通知,并对接收任务的通知值进行递增(加一,因为是模拟信号量),如果接收任务尚未挂起,则调用 xTaskNotifyGive() 会将接收任务的通知状态设置为挂起,该 API 实际上是作为宏实现的,而不是函数,其具体声明如下所述
/**
* @brief 任务通知用作轻量级且更快的二进制或计数信号量替代方案时所使用的通知发送函数
* @param xTaskToNotify:通知发送到的任务的句柄
* @retval 只会返回pdPASS
*/
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify);
/**
* @brief 上述函数的的中断安全版本函数
* @param xTaskToNotify:通知发送到的任务的句柄
* @param pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
* @retval None
*/
void vTaskNotifyGiveFromISR(TaskHandle_t xTaskToNotify,
BaseType_t *pxHigherPriorityTaskWoken);
当一个任务使用 xTaskNotifyGive() API 函数将通知值用作二值或等效计数信号量时, 则被通知的任务应使用 ulTaskNotifyTake() API 函数来接收或等待通知值
ulTaskNotifyTake() 允许任务在阻塞状态下等待其通知值大于零,并在返回之前递减(减一)或清除任务的通知值,其具体函数声明如下所述
/**
* @brief 任务通知被用作更快、更轻的二进制或计数信号量替代时使用通知接收函数
* @param xClearCountOnExit:设置为pdTRUE,则该函数返回之前,调用任务的通知值将被清零;设置为pdFASLE,并且通知值大于0,则调用任务的通知值将在该函数返回之前递减
* @param xTicksToWait:调用任务应保持阻塞状态以等待其通知值大于零的最长时间
* @retval 阻塞时间到期也没能等到消息则返回 0 ,阻塞时间到期前等到消息则返回之前的通知值
*/
uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait);
3.5、xTaskNotify() API 函数
xTaskNotify() 是 xTaskNotifyGive() 的功能更强大的版本,可用于通过以下任意方式更新接收任务的通知值
- 接收任务的通知值递增(加一),在这种情况下 xTaskNotify() 相当于 xTaskNotifyGive()
- 接收任务的通知值中设置一位或多位,这允许任务的通知值用作事件组的更轻量级和更快的替代方案
- 将一个全新的数字写入接收任务的通知值,但前提是接收任务自上次更新以来已读取其通知值,这允许任务的通知值提供与长度为 1 的队列提供的功能类似的功能
- 将一个全新的数字写入接收任务的通知值,即使接收任务自上次更新以来尚未读取其通知值,这允许任务的通知值提供与 xQueueOverwrite() API 函数提供的功能类似的功能,由此产生的行为有时被称为“邮箱”
xTaskNotify() 比 xTaskNotifyGive() 更灵活、更强大,并且由于额外的灵活性和强大功能,它的使用也稍微复杂一些,使用 xTaskNotify() 函数时,如果接收任务尚未挂起,则调用 xTaskNotify() 将始终将其设置为挂起状态,如下所示为其具体函数声明
/**
* @brief 任务通知函数
* @param xTaskToNotify:通知发送到的任务的句柄
* @param ulValue:ulValue的使用方式取决于eAction值,参考 “3.5.1、eAction 参数” 小节
* @param eAction:一个枚举类型,指定如何更新接收任务的通知值,参考 “3.5.1、eAction 参数” 小节
* @retval 除 “3.5.1、eAction 参数” 小节提到的一种情况外,均返回pdPASS
*/
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction);
/**
* @brief 任务通知的中断安全版本函数
* @param xTaskToNotify:通知发送到的任务的句柄
* @param ulValue:ulValue的使用方式取决于eAction值,参考 “3.5.1、eAction 参数” 小节
* @param eAction:一个枚举类型,指定如何更新接收任务的通知值,参考 “3.5.1、eAction 参数” 小节
* @param pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
* @retval 除 “3.5.1、eAction 参数” 小节提到的一种情况外,均返回pdPASS
*/
BaseType_t xTaskNotifyFromISR(TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction
BaseType_t *pxHigherPriorityTaskWoken);
3.5.1、eAction 参数
eAction 参数是一个 eNotifyAction 枚举类型,其定义了 5 中不同枚举类型,用于模拟二值信号量、计数信号量、队列、事件组和 ”邮箱“ 等功能,其具体定义如下所述
eNotifyAction 值 | 对接收任务的最终影响 |
---|---|
eNoAction | 接收任务的通知状态设置为待处理,而不更新其通知值,未使用 xTaskNotify() 中 ulValue 参数 |
eSetBits | 接收任务的通知值与 xTaskNotify() 中 ulValue 参数中传递的值进行按位或运算,例如:如果 ulValue 设置为 0x01,则接收任务的通知值中将置位第 0 位 |
eIncrement | 接收任务的通知值递增,未使用 xTaskNotify() 中 ulValue 参数 |
eSetValueWithoutOverwrite | 如果接收任务在调用 xTaskNotify() 之前有待处理的通知,则不执行任何操作,并且 xTaskNotify() 将返回 pdFAIL;如果在调用 xTaskNotify() 之前接收任务没有待处理的通知,则接收任务的通知值将设置为 xTaskNotify() 中 ulValue 参数中传递的值 |
eSetValueWithOverwrite | 接收任务的通知值设置为 xTaskNotify() ulValue 参数中传递的值,无论接收任务在调用 xTaskNotify() 之前是否有待处理的通知 |
3.6、xTaskNotifyWait() API 函数
xTaskNotifyWait() 是 ulTaskNotifyTake() 的功能更强大的版本,它允许任务以可选的超时等待调用任务的通知状态变为待处理(如果它尚未处于待处理状态),xTaskNotifyWait() 提供了在进入函数和退出函数时清除调用任务的通知值中的位的参数 ulBitsToClearOnEntry 和 ulBitsToClearOnExit
/**
* @brief 任务通知的中断安全版本函数
* @param ulBitsToClearOnEntry:参考 “3.6.1、ulBitsToClearOnEntry 参数” 小节
* @param ulBitsToClearOnExit:参考 “3.6.2、_ulBitsToClearOnExit_ 参数” 小节
* @param pulNotificationValue:用于传递任务的通知值,因为等待通知的函数可能由于 ulBitsToClearOnExit 参数在函数退出时收到的消息值已被更改
* @param xTicksToWait:调用任务应保持阻塞状态以等待其通知状态变为挂起状态的最长时间
* @retval 参考 “3.6.2、xTaskNotifyWait() 函数返回值” 小节
*/
BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait);
3.6.1、ulBitsToClearOnEntry 参数
如果调用任务在调用 xTaskNotifyWait() 之前没有待处理的通知,则在进入该函数时,将在任务的通知值中清除参数 ulBitsToClearOnEntry 中设置的任何位
例如,如果参数 ulBitsToClearOnEntry 为 0x01,则任务通知值的位 0 将被清除,再举一个例子,将参数 ulBitsToClearOnEntry 设置为 0xffffffff(ULONG_MAX)将清除任务通知值中的所有位,从而有效地将值清除为 0
3.6.2、ulBitsToClearOnExit 参数
如果调用任务因为收到通知而退出 xTaskNotifyWait() ,或者因为在调用 xTaskNotifyWait() 时已经有通知挂起,那么在参数 ulBitsToClearOnExit 中设置的任何位将在任务退出 xTaskNotifyWait() 函数之前在任务的通知值中被清除
例如,如果参数 ulBitsToClearOnExit 为 0x03 ,则任务通知值的位 0 和位 1 将在函数退出之前被清除,再举个例子,将参数 ulBitsToClearOnExit 为 0xffffffff(ULONG_MAX)将清除任务通知值中的所有位,从而有效地将值清除为 0
3.6.2、xTaskNotifyWait() 函数返回值
有两种可能的返回值,分别为 pdPASS 和 pdFALSE ,具体如下所述
① pdPASS
- 调用 xTaskNotifyWait() 时调用任务已经有待处理的通知
- 调用 xTaskNotifyWait() 时调用任务没有待处理的通知,由于设置了阻塞时间因此进入阻塞状态等待消息挂起,在阻塞时间到期之前成功等到消息挂起
② pdFALSE
- 调用 xTaskNotifyWait() 时调用任务没有待处理的通知,由于设置了阻塞时间因此进入阻塞状态等待消息挂起,但是直到阻塞时间到期都没有等到消息挂起
3.7、其他 API 函数
除了上面的一些常用 API 之外,还有一些工具或不常用的 API 函数,因为启用任务通知后会在任务控制块中增加一个任务状态和一个任务通知值,因此 FreeRTOS 提供了清除任务状态的 xTaskNotifyStateClear() API 函数和 清除任务通知值的 ulTaskNotifyValueClear() API 函数
另外增加了 "3.3、任务通知 API 概述" 小节中提到的在 xTaskNotify() API 函数上增加了 pulPreviousNotifyValue 参数的 xTaskNotifyAndQuery() API函数和其中断安全版本函数,上述提到的四个函数声明具体如下所述
/**
* @brief 清除任务通知状态
* @param xTask:要操作的任务句柄
* @retval 如果要操作的任务有待处理的通知,并且该通知已清除,则返回pdTRUE;如果该任务没有待处理的通知,则返回pdFALSE
*/
BaseType_t xTaskNotifyStateClear(TaskHandle_t xTask);
/**
* @brief 清除任务通知值
* @param xTask:要操作的任务句柄
* @param ulBitsToClear:xTask的通知值中要清除的位的位掩码,比如设置为0x01表示将通知值的第0位清除
* @retval ulBitsToClear指定的位被清除之前目标任务的通知值的值
*/
uint32_t ulTaskNotifyValueClear(TaskHandle_t xTask,
uint32_t ulBitsToClear);
/**
* @brief 执行与xTaskNotify()相同的操作,此外它还在附加的pulPreviousNotifyValue中返回目标任务的先前通知值(调用函数时的通知值,而不是函数返回时的通知值)
* @param xTaskToNotify:被通知任务的句柄
* @param ulValue:通知值,ulValue的使用方式取决于eAction值,参考 “3.5.1、eAction 参数” 小节
* @param eAction:一个枚举类型,指定如何更新接收任务的通知值,参考 “3.5.1、eAction 参数”
* @param pulPreviousNotifyValue:返回目标任务的先前通知值
* @retval 除 “3.5.1、eAction 参数” 小节提到的一种情况外,均返回pdPASS
*/
BaseType_t xTaskNotifyAndQuery(TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t *pulPreviousNotifyValue);
/**
* @brief 上述函数的中断安全版本
* @param pxHigherPriorityTaskWoken:通知应用程序编程者是否需要进行上下文切换
*/
BaseType_t xTaskNotifyAndQueryFromISR(TaskHandle_t xTaskToNotify,
uint32_t ulValue,
NotifyAction eAction,
uint32_t *pulPreviousNotifyValue,
BaseType_t *pxHigherPriorityTaskWoken);
4、实验一:使用任务通知替代信号量
4.1、实验目标
既然本实验目的是使用任务通知替代信号量,那么我们可以使用任务通知重新实现一下 "FreeRTOS教程5 信号量" 文章中 “4、实验一:二值信号量的应用” 小节内容
4.2、CubeMX相关配置
复制 “FreeRTOS教程5 信号量” 文章中 “4、实验一:二值信号量的应用”小节所描述的实验工程,然后通过 “.ioc” 后缀的文件打开该工程的 STM32CubeMX 软件配置界面,单击 Middleware and Software Packs/FREERTOS ,在 Configuration 中找到 Timers and Semaphores ,删除原来创建好名为 BinarySem_ADC 的二值信号量,然后直接重新生成工程代码即可
4.3、添加其他必要代码
重新实现 ADC 采集转换完成中断回调函数和任务函数 TASK_ADC ,主要是将原来使用二值信号量同步 ISR 和 TASK_ADC 的程序修改为使用任务通知,具体如下所示
/*ADC数据处理任务*/
void TASK_ADC(void *argument)
{
/* USER CODE BEGIN TASK_ADC */
//定义一个变量用于表示任务待处理的事件数量
uint32_t ulEventsToProcess;
/* Infinite loop */
for(;;)
{
//等待任务通知
ulEventsToProcess = ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(500));
//如果等到事件
if(ulEventsToProcess != 0)
{
//当待处理的事件不为0就一直处理,处理一次待处理的事件减少1
while(ulEventsToProcess > 0)
{
uint32_t Volt = (3300 * adc_value)>>12;
printf("val:%d, Volt:%d\r\n", adc_value, Volt);
ulEventsToProcess --;
}
}
}
/* USER CODE END TASK_ADC */
}
/*转换完成中断回调*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
/*定时器中断启动单通道转换*/
if(hadc->Instance == ADC1)
{
//获取原始ADC采集值
adc_value = HAL_ADC_GetValue(hadc);
BaseType_t highTaskWoken = pdFALSE;
if(task_ADCHandle != NULL)
{
//采集完毕给TASK_ADC任务发送采集完毕的通知
vTaskNotifyGiveFromISR(task_ADCHandle, &highTaskWoken);
portYIELD_FROM_ISR(highTaskWoken);
}
}
}
4.4、烧录验证
实验现象与 “FreeRTOS教程5 信号量” 文章 “4.4、烧录验证” 小节内容一样,如下图所示
5、实验二:使用任务通知传递数据
5.1、实验目标
“4、实验一:使用任务通知替代信号量” 小节的实验流程如下所述
- TASK_ADC 等待消息的到来
- ADC_ISR 获取原始 ADC 采集值,然后将采集值写入全局变量 adc_value 中,并且发送消息给 TASK_ADC 表示采集完成
- TASK_ADC 消息挂起,退出阻塞状态,然后对存入全局变量 adc_value 中的采集值进行处理,最后通过串口将信息输出
本实验要实现的目的仍然为单通道的 ADC 采集,只不过不需要定义全局变量 adc_value 来存储采集到的原始 ADC 的值,之前提到过启用任务通知后,任务会有一个 32 位的通知值,当我们需要传递的数据为 32 位或更低位的数据时我们可以用这个通知值来直接传递数据
但是注意不是所有情况下都可以用来传递数据的,这要根据 eAction 参数来决定,具体可以参考 “3.5.1 eAction 参数” 小节内容,这里我们将其选择为 eSetValueWithOverwrite ,但是要注意这时只能传递一个数据,传递完毕如果接收端不处理下次该数据就会被覆盖掉,本实验流程如下所述
- TASK_ADC 等待消息的到来
- ADC_ISR 获取原始 ADC 采集值,将原始 ADC 采集值作为通知值传递给 TASK_ADC
- TASK_ADC 消息挂起,然后退出阻塞状态,取出通知值进行处理,最后通过串口将信息输出
5.2、CubeMX相关配置
复制 “4、实验一:使用任务通知替代信号量” 小节配置好的工程即可
5.3、添加其他必要代码
打开工程代码,修改 TASK_ADC 任务函数体和 ADC 采集完毕中断回调函数,具体如下所述
/*ADC处理任务*/
void TASK_ADC(void *argument)
{
/* USER CODE BEGIN TASK_ADC */
//定义一个变量用于表示任务待处理的事件数量
uint32_t notifyValue;
/* Infinite loop */
for(;;)
{
//进入xTaskNotifyWait函数时不清除任何位
uint32_t ulBitsToClearOnEntry = 0x00;
//退出xTaskNotifyWait函数时清除所有位
uint32_t ulBitsToClearOnExit = 0xFFFFFFFF;
//等待任务通知
BaseType_t result = xTaskNotifyWait(ulBitsToClearOnEntry, ulBitsToClearOnExit, ¬ifyValue, portMAX_DELAY);
//如果等到事件
if(result == pdTRUE)
{
//对采集值处理并通过串口输出
uint32_t Volt = (3300 * notifyValue)>>12;
printf("val:%d, Volt:%d\r\n", notifyValue, Volt);
}
}
/* USER CODE END TASK_ADC */
}
/*ADC转换完成中断回调*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
//定时器中断启动单通道转换
if(hadc->Instance == ADC1)
{
//获取原始ADC采集值
uint32_t adc_value = HAL_ADC_GetValue(hadc);
BaseType_t highTaskWoken = pdFALSE;
if(task_ADCHandle != NULL)
{
//采集完毕后将采集值作为消息数据发送给TASK_ADC任务
xTaskNotifyFromISR(task_ADCHandle, adc_value, eSetValueWithOverwrite, &highTaskWoken);
portYIELD_FROM_ISR(highTaskWoken);
}
}
}
5.4、烧录验证
实验现象与 “FreeRTOS教程5 信号量” 文章 “4.4、烧录验证” 小节内容一样,此处不再赘述
6、注释详解
注释1:图片来源于 Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf
参考资料
Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf