Q: 什么是任务通知?
A: FreeRTOS 从版本 V8.2.0 开始提供任务通知这个功能,每个任务都有一个 32 位的通知值。按照 FreeRTOS 官方的说法,使用消息通知比通过二进制信号量方式解除阻塞任务快 45%, 并且更加省内存(无需创建队列)。 在大多数情况下,任务通知可以替代二值信号量、计数信号量、事件标志组,可以替代长度为 1 的队列(可以保存一个 32 位整数或指针值),并且任务通知速度更快、使用的RAM更少!
任务通知值的更新方式
FreeRTOS 提供以下几种方式发送通知给任务 :
- 发送消息给任务,如果有通知未读,不覆盖通知值
- 发送消息给任务,直接覆盖通知值
- 发送消息给任务,设置通知值的一个或者多个位
- 发送消息给任务,递增通知值
通过对以上方式的合理使用,可以在一定场合下替代原本的队列、信号量、事件标志组等。
任务通知的优势和劣势
任务通知的优势
- 使用任务通知向任务发送事件或数据,比使用队列、事件标志组或信号量快得多。
- 使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。
任务通知的劣势
- 只有任务可以等待通知,中断服务函数中不可以,因为中断没有 TCB(TCB可以简易理解为任务创建时在内存中开辟的空间) 。
- 通知只能一对一,因为通知必须指定任务。
- 等待通知的任务可以被阻塞, 但是发送消息的任务,任何情况下都不会被阻塞等待。(而信号量或者事件标志组就可以阻塞等待)
- 任务通知是通过更新任务通知值来发送数据的,任务结构体中只有一个任务通知值,只能保 持一个数据。(这也是为什么任务通知只能模拟长度为1的队列)
任务通知相关 API 函数
发送通知
发送通知,带有通知值
- xTaskToNotify:需要接收通知的任务句柄
- ulValue:用于更新接收任务通知值, 具体如何更新由形参 eAction 决定
- eAction:一个枚举,代表如何使用任务通知的值
- 返回值: 如果被通知任务还没取走上一个通知,又接收了一个通知,则这次通知值未能更新并返回 pdFALSE(按照枚举的表格,这种情况仅可能出现在枚举被设置为“eSetValueWithoutOverWrite”时出现), 而其他情况均返回pdPASS
关于枚举值:
其中,eSetBits可以模拟事件标志组;elncrement可以模拟信号量;eSetValueWithOverWrite可以模拟为覆写的消息队列;eSetValueWithoutOverwrite可以模拟为不覆写的消息队列。
发送通知,带有通知值并且保留接收任务的原通知值
- xTaskToNotify:需要接收通知的任务句柄
- ulValue:用于更新接收任务通知值, 具体如何更新 由形参 eAction 决定
- eAction:一个枚举,代表如何使用任务通知的值
- pulPreviousNotifyValue:对象任务的上一个任务通知值,如果为 NULL, 则不需要回传, 这个时候就等价于函数 xTaskNotify()
- 返回值: 如果被通知任务还没取走上一个通知,又接收了一个通知,则这次通知值未能更新并返回 pdFALSE(按照枚举的表格,这种情况仅可能出现在枚举被设置为“eSetValueWithoutOverWrite”时出现), 而其他情况均返回pdPASS
发送通知,不带通知值
- xTaskToNotify:接收通知的任务句柄, 并让其自身的任务通知值加 1 (模拟信号量)
- 返回值: 总是返回 pdPASS
等待通知
注意!等待通知API函数只能用在任务,不可应用于中断中!
获取任务通知的函数1
- xClearCountOnExit:指定在成功接收通知后,将通知值清零或减 1,pdTRUE:把通知值清零(类似二值信号量);pdFALSE:把通知值减一(类似计数型信号量)
- xTicksToWait:阻塞等待任务通知值的最大时间
- 返回值: 0:接收失败 ;非0:接收成功,返回任务通知的通知值
获取任务通知的函数2
- ulBitsToClearOnEntry:函数执行前清零任务通知值那些位,1清零,0不清零
- ulBitsToClearOnExit:表示在函数退出前,清零任务通知值那些位,1清零,0不清零,在清 0 前,接收到的任务通知值会先被保存到形参 *pulNotificationValue 中
注意“1清零,0不清零”是对于“位”来说,即如果希望全部清零,那应该是0xFFFFFFFF
- pulNotificationValue:用于保存接收到的任务通知值。 如果 不需要使 用,则设置为 NULL 即可
- xTicksToWait:等待消息通知的最大等待时间
实操演示
在 C:\mjm_CubeMX_proj 路径下,复制一份Cube的母版并重命名为 :mjm_freeRTOS_note:
打开相应的Cube文件:
1. 配置按钮的GPIO:
2. 找到左侧的Middleware --> FREERTOS,然后在下方找到"Task and Queues",然后创建两个任务:
3. 生成代码打开Keil:
模拟二值信号量
#include "stdio.h"
void StartTask_send(void const * argument)
{
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){ //按键消抖
printf("KEY1 has been pressed\r\n");
xTaskNotifyGive(Task_receiveHandle); //相当于一个指定了对象,一对一的二值信号量释放,使通知量加1
printf("Task note: successfully give\r\n");
}
while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET); //等待按键松开,防止出现按钮一直按着,就一直删除创建任务
}
osDelay(1);
}
}
void StartTask_receive(void const * argument)
{
uint32_t recv = 0;
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){ //按键消抖
printf("KEY2 has been pressed\r\n");
recv = ulTaskNotifyTake(pdTRUE, portMAX_DELAY); //收到通知将通知清零,模拟二值信号量获取
if(recv != 0){
printf("Task note: successfully take\r\n");
}
}
while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET); //等待按键松开,防止出现按钮一直按着,就一直删除创建任务
}
osDelay(1);
}
}
实现效果1
打开串口助手:
分别按下KEY1和KEY2:
但如果再按一下KEY2:
由于模拟二值信号量已经被获取了,再次获取注定是不成功的,且我将等待时间拉到了最长,所以会一直死等,进入接收阻塞
模拟计数型信号量
根据上面所讲,模拟二值和模拟计数信号量的唯一区别,就是将ulTaskNotifyTake()函数的第一个参数从 pdTRUE 改成 pdFALSE:
#include "stdio.h"
void StartTask_send(void const * argument)
{
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){ //按键消抖
printf("KEY1 has been pressed\r\n");
xTaskNotifyGive(Task_receiveHandle); //相当于一个指定了对象,一对一的二值信号量释放,使通知量加1
printf("Task note: successfully give\r\n");
}
while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET); //等待按键松开,防止出现按钮一直按着,就一直删除创建任务
}
osDelay(1);
}
}
void StartTask_receive(void const * argument)
{
uint32_t recv = 0;
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){ //按键消抖
printf("KEY2 has been pressed\r\n");
recv = ulTaskNotifyTake(pdFALSE, portMAX_DELAY); //收到通知将通知减1,模拟计数信号量获取
if(recv != 0){
printf("Task note: successfully take\r\n");
}
}
while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET); //等待按键松开,防止出现按钮一直按着,就一直删除创建任务
}
osDelay(1);
}
}
实现效果2
打开串口助手:
按三下KEY1(模拟计数信号量,使得计数量为3)
再按三下KEY2 (使得计数量归0)
最后再按一下KEY2,由于模拟的计数信号量值为0,所以会开始死等,进入接收阻塞
模拟事件标志组
#include "stdio.h"
void StartTask_send(void const * argument)
{
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){ //按键消抖
printf("KEY1 has been pressed\r\n");
printf("bit0 has been set to 1\r\n");
xTaskNotify(Task_receiveHandle, 0x01, eSetBits); //被通知任务的任务值按位或0x01,相当于将bit0置1
//printf("bit0 has been set to 1\r\n"); 注意这句话不能加在这里,任务通知之后,会立刻跳转到始终等待阻塞的其他任务,这句话的存在可能会导致程序的错误
}
while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET); //等待按键松开,防止出现按钮一直按着,就一直删除创建任务
}
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){ //按键消抖
printf("KEY2 has been pressed\r\n");
printf("bit1 has been set to 1\r\n");
xTaskNotify(Task_receiveHandle, 0x02, eSetBits); //被通知任务的任务值按位或0x02,相当于将bit1置1
//printf("bit1 has been set to 1\r\n"); 注意这句话不能加在这里,任务通知之后,会立刻跳转到始终等待阻塞的其他任务,这句话的存在可能会导致程序的错误
}
while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET); //等待按键松开,防止出现按钮一直按着,就一直删除创建任务
}
osDelay(1);
}
}
void StartTask_receive(void const * argument)
{
uint32_t recv = 0;
uint32_t event_bit = 0;
for(;;)
{
xTaskNotifyWait(0, 0xFFFFFFFF, &recv, portMAX_DELAY); //函数执行前不清零,将通知值保存到recv中,在函数结束前清零,并一直等待消息通知
//为什么有8个F,是因为变量类型是32位的整型
if(recv & 0x01){ //此处其实也可以直接写成 “recv == 0x01”,但是如果代码量大,可能会出现各种状况,“recv & 0x01”的写法是最好且不会出错的,因为这句话就只判断特定的位的值
event_bit |= 0x01; //将event_bit的 bit0 置1
printf("bit0 qualified\r\n");
}
if(recv & 0x02){
event_bit |= 0x02; //将event_bit的 bit1 置1
printf("bit1 qualified\r\n");
}
if(event_bit == 0x03){
printf("good good good\r\n");
event_bit = 0;
}
osDelay(1);
}
}
实现效果3
打开串口助手:
只有KEY1和KEY2都被按下过,才会发送“good good good”
模拟邮箱
所谓邮箱,其实就是消息的收发,也就是“任务通知”最原始的含义。
其实,和模拟事件标志组的代码差不多,主要区别就是将枚举换成“eSetValueWithOverwrite”或“eSetValueWithoutOverwrite”:
此处使用覆写(eSetValueWithOverwrite)或不覆写(eSetValueWithoutOverwrite)都可以,因为不管覆不覆写,在我的代码中,只要发送了任意一条通知,接收通知的任务在退出前都会将通知值全部清0,所以实现的效果是一样的。
#include "stdio.h"
void StartTask_send(void const * argument)
{
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){ //按键消抖
printf("KEY1 has been pressed\r\n");
printf("bit0 has been set to 1\r\n");
xTaskNotify(Task_receiveHandle, 0x01, eSetValueWithOverwrite); //被通知任务的任务值按位或0x01,相当于将bit0置1
}
while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET); //等待按键松开,防止出现按钮一直按着,就一直删除创建任务
}
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){ //按键消抖
printf("KEY2 has been pressed\r\n");
printf("bit1 has been set to 1\r\n");
xTaskNotify(Task_receiveHandle, 0x02, eSetValueWithOverwrite); //被通知任务的任务值按位或0x02,相当于将bit1置1
}
while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET); //等待按键松开,防止出现按钮一直按着,就一直删除创建任务
}
osDelay(1);
}
}
void StartTask_receive(void const * argument)
{
uint32_t recv = 0;
for(;;)
{
xTaskNotifyWait(0, 0xFFFFFFFF, &recv, portMAX_DELAY); //函数执行前不清零,将通知值保存到recv中,在函数结束前清零,并一直等待消息通知
//为什么有8个F,是因为变量类型是32位的整型
printf("msg received, saying\"%d\"\r\n",recv);
osDelay(1);
}
}
实现效果4
打开串口助手:
按下KEY1:
按下KEY2:
就像任务一在向任务二发邮件一样。