FreeRTOS-任务通知详解
本文的重点主要有两点:1.搞明白任务通知的三个状态(实现任务通知的关键),2.明白任务通知的优缺点,以及任务通知模拟出来的队列、信号量、事件组与真实的有何区别。
✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
📃个人主页:@rivencode的个人主页
🔥系列专栏:玩转FreeRTOS
💬保持学习、保持热爱、认真分享、一起进步!!!
目录
前言
FreeRTOS越学越简单,前面已经把真实的队列、信号量、事件组全部学完,现在学个任务通知去模拟这些东西,真的都学会了,模拟的还不会嘛,所以我们的本文的重点主要有两点:1.搞明白任务通知的三个状态(实现任务通知的关键),2.明白任务通知的优缺点,以及任务通知模拟出来的队列、信号量、事件组与真实的有何区别。
一、任务通知的简介
说是任务通知,倒不如说通知任务,所谓任务通知核心就是一个32位的无符号整数和一个8位的通知状态,而这两玩意就在任务控制块中,则所谓通知任务就是一个任务或者中断改写另外一个任务中的32位的无符号整数,只不过改写这个整数的方式可以有所不同(1.可以让这个整数加1: 模拟信号量 2. 设置该整数的指定的某些位:模拟事件组 3.直接选择覆盖或者不覆盖写入: 模拟消息队列)。
- 1.任务的通知状态:任务通知有三种状态
未等待通知状态:就是任务的初始状态
等待通知状态:当任务在没有通知的时候接收通知时(也就是任务没有接收到通知的时候调用了接收通知的函数,则此时必定接收不到通知,把该任务标记为等待通知状态(去等别的任务发给我通知),任务进入阻塞态),这样做的用处是什么呢? 答:当另外一个任务发通知给该任务时,此时发现任务处于等待通知的状态,然后就可以即可把该任务唤醒。
等待接收通知状态:当有其他任务向任务发送通知,但任务还未接收这一通知的这段期间内(当其他任务给该任务发了通知,但是该任务还没有接收,则将该任务标记为等待接收通知状态),这样做的用处就是当该任务调用了接收通知的函数,发现自身的状态为等待接收通知状态,则不用进入阻塞,直接接收通知值。
为什么要搞一个这样的通知状态?
答:
1.为了判断任务是否接收到了通知
2.不需要一个链表来挂载因等不到通知而阻塞任务,可以直接将任务挂入阻塞链表,因为当调用发送通知函数去唤醒该任务时只需要判断它是否处于等待通知状态(因等待通知进入阻塞)。
像队列它有一个当前消息个数的变量可以知道队列中是否有消息,像信号量0就是没消息
那为什么任务通知不能以通知值是否为0判断是否有消息呢?
确实模拟信号量确实是怎么做的,但是如果是模拟队列的话,就不能怎么搞了,因为我发送一个0,0也算是数据,所以需要一个
如果现在还搞不懂这三个状态什么意思,没关系看后面的源码就懂了。
- 2.任务通知的优缺点:
(1).任务通知的优点
按照FreeRTOS官方的说法使用任务通知比通过队列、事件标志组或信号量通信方式解除阻塞的任务要快 45%,并且更加省 RAM 内存空间,因为像队列、信号量、事件组这些通信方式使用前必须先创建,拿队列来说如下图所示,申请内存的时候至少需要下图这么多变量,而任务通知是任务结构体中自带的一个32位的无符号整数,一个8位的通知状态变量,一共就5个字节。
使用任务通知不需要创建,因为当创建任务的时候就已经默认创建了这两个变量,任务控制块中的两个变量如下图所示,当然这里是一个数组(为了方便以后扩展),但是数组的元素个数默认为1。
(2).任务通知的缺点
虽然说任务通知可以模拟这么多通信方式,但是肯定有限制、有缺点,不然还要这些队列、信号量、事件组干嘛。
1.不能发送通知到中断
原因很简单,任务通知、任务通知,人家通知的是任务,是修改任务控制块中那个32位无符号整数的值,中断并没有任务控制块这一说,但为什么队列、信号量、事件组这些就可以呢,说到底人家创建了一个独立的队列、信号量、事件组结构体当然谁都可以访问里面的内容,但是可以在中断中发送通知给其他任务,这个是没毛病的。
2.不能发送通知给多个任务
任务通知只能指定发送给某一个任务而不能广播,而队列、信号量、事件组任何中断和任务都能访问,不过很少出现多个任务或中断接收同一个通讯对象的情况
3.发送通知的任务不能进入阻塞
只有等待通知的任务可以被阻塞,发送通知的任务,在任何情况下都不会因为发送失败而进入阻塞态,像队列:写队列当队列满的时候,可以进入阻塞态
4.通知值只有一个32位的无符号的整形
加粗样式不像队列,可以缓存多个任意类型的数据,而任务通知只有一个消息,而且只能作用一次(接收到通知值等待通知的任务才能被唤醒)
接下来就开始分析任务通知有关函数的源码,其中会穿插着讲解用任务通知模拟出来的队列、信号量、事件组与真实的有何区别。
二、任务通知源码分析
任务通知的创建就不用说了,任务被创建时则就便有了任务通知,而且FreeRTOS默认任务通知是开启的。
1.发送通知函数xTaskGenericNotify
FreeRTOS定义了三个发送通知的函数,其实他们都是宏定义最终调用的是xTaskGenericNotify(),只不过他们的传入的参数有所区别,这都是FreeRTOS的老套路了,所以我们先分析完xTaskGenericNotify()函数的实现,再谈谈这三个函数应用上有何区别。
1.xTaskGenericNotify()函数原型:
- 函数参数:
1.xTaskToNotify:传入接收任务通知的任务控制块
2.uxIndexToNotify:任务的指定通知(任务通知相关数组下标,默认为0,使用数组第一个元素当做通知)
3.ulValue:要写入的通知值
4.eAction:通知的方式(模拟信号量,消息队列、事件组中方式),可取的值如下所示
第一个取值:只起通知作用也是有用的,至少可以唤醒,因未等待到通知而阻塞的任务。
5.pulPreviousNotificationValue:用于获取发送通知前的通知值
- 函数返回值
pdPASS 任务通知发送成功
pdFAIL 任务通知发送失败
2.TaskGenericNotify()源码分析:
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify,
UBaseType_t uxIndexToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t * pulPreviousNotificationValue )
{
TCB_t * pxTCB;
BaseType_t xReturn = pdPASS;
uint8_t ucOriginalNotifyState;
configASSERT( uxIndexToNotify < configTASK_NOTIFICATION_ARRAY_ENTRIES );
configASSERT( xTaskToNotify );
pxTCB = xTaskToNotify;
taskENTER_CRITICAL();
{
/* 判断是否需要通知前的通知值 */
if( pulPreviousNotificationValue != NULL )
{
/* 获取发送通知前的通知值 */
*pulPreviousNotificationValue = pxTCB->ulNotifiedValue[ uxIndexToNotify ];
}
/* 记录发送通知前的任务通知状态 */
ucOriginalNotifyState = pxTCB->ucNotifyState[ uxIndexToNotify ];
/* 将要接收的通知的任务的状态设置为等待接收通知状态 */
pxTCB->ucNotifyState[ uxIndexToNotify ] = taskNOTIFICATION_RECEIVED;
switch( eAction )
{
/* 模拟事件组:将通知值的某些位置一 */
case eSetBits:
pxTCB->ulNotifiedValue[ uxIndexToNotify ] |= ulValue;
break;
/* 模拟计数型信号量:将通知值的某些位置一 */
case eIncrement:
( pxTCB->ulNotifiedValue[ uxIndexToNotify ] )++;
break;
/* 模拟队列:覆写通知值 */
case eSetValueWithOverwrite:
pxTCB->ulNotifiedValue[ uxIndexToNotify ] = ulValue;
break;
/* 模拟队列:(不覆盖)写通知值 */
case eSetValueWithoutOverwrite:
/* 如果任务状态不处于等待接收状态,说明通知值已被读走,
可以写入*/
if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED )
{
pxTCB->ulNotifiedValue[ uxIndexToNotify ] = ulValue;
}
else
{
/* 通知值未被读走,不能覆写 */
xReturn = pdFAIL;
}
break;
case eNoAction:
/* 只将任务标记为等待接收通知状态
并不修改通知值 */
break;
default:
/* Should not get here if all enums are handled.
* Artificially force an assert by testing a value the
* compiler can't assume is const. */
configASSERT( xTickCount == ( TickType_t ) 0 );
break;
}
traceTASK_NOTIFY( uxIndexToNotify );
/* 如果在此之前,任务因等待任务通知而被阻塞(该任务为等待通知状态),则现在解除阻塞 */
if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )
{
/* 将任务从所在任务状态列表(延时列表)中移除 */
listREMOVE_ITEM( &( pxTCB->xStateListItem ) );
/* 将任务添加到就绪任务列表中 */
prvAddTaskToReadyList( pxTCB );
/* 该任务不应在事件列表中 */
configASSERT( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) == NULL );
#if ( configUSE_TICKLESS_IDLE != 0 )
{
/* 更新下一个解除阻塞的任务 */
prvResetNextTaskUnblockTime();
}
#endif
/* 有任务解除阻塞后,就应该判断是否需要进行任务切换 */
if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
{
/* 悬起 PendSV中断,准备进行任务切换 */
taskYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
return xReturn;
}
#endif /* configUSE_TASK_NOTIFICATIONS */
/*-----------------------------------------------------------*/
源码解析:
1.如果传入的pulPreviousNotificationValue不为NULL(就是一个32位变量的地址),则代表需要获取发送通知前的通知值。
2.这里解释一下为什么要将任务的通知状态改为等待接收通知状态
首先要明白一点我们改的是要接收通知的任务通知的状态,则当调用接收通知函数时,判断一下任务的通知状态是不是等待接收通知状态如果是那说明有通知已经发送过来了。
3.解释一下框中的内容
不能覆写:代表已经有通知值,说明前面已经调用过一次发送通知函数xTaskGenericNotify,将任务的通知状态改为等待接收通知,如果再一次调用xTaskGenericNotify去发送通知,发送这次的任务通知状态已经不等于等待接收通知了,说明通知值被取走(其中调用过接收通知函数,会将任务的状态改写,讲接收通知函数你就知道了)。
4.如果任务为等待通知状态,说明该任务在此之前已经调用过接收通知函数(如果没有通知,会将任务的通知状态改写成等待通知状态,并进入阻塞态),此时我正好发送通知过去,就需要唤醒该任务去取通知值。
当然光看一个发送通知函数,可能还是不太理解这些通知状态,所以一定是要发送/接收通知函数配合起来看,才能连贯起来,就豁然开朗。
函数 xTaskNotify():
此函数用于往指定任务发送任务通知,通知方式可以自由指定,并且不获取发送任务通知前任务通知的通知值。
函数 xTaskNotifyAndQuery():
此函数用于往指定任务发送任务通知,通知方式可以自由指定,并且获取发送任务通知前任务通知的通知值。
函数 xTaskNotifyGive():
此函数用于往指定任务发送任务通知,通知方式为将通知值加 1,并且不获取发送任务通知前任务通知的通知值(用于二值/计数型信号量)
当然中断也可以向某任务发送通知值,函数跟上面的差不多,只不过多了中断保护。
2.接收任务通知 ulTaskNotifyTake()
ulTaskNotifyTake()函数其实是一个宏真正调用的是ulTaskGenericNotifyTake()函数,该函数就是专门服务与模拟二值信号量/计数型信号量的。
1.ulTaskNotifyTake()函数原型
- 函数参数
1.uxIndexToWaitOn :任务的指定通知(任务通知相关数组下标)
2.xClearCountOnExit: 在成功接收通知后,将通知值清零或减 1(分别对应二值信号量,计数型信号量)
3.xTicksToWait: 阻塞等待任务通知值的最大时间
- 函数返回值
0:接收失败
非 0: 接收成功,返回任务通知的通知值(当前计数值)
2.ulTaskNotifyTake()函数源码
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
uint32_t ulTaskGenericNotifyTake( UBaseType_t uxIndexToWait,
BaseType_t xClearCountOnExit,
TickType_t xTicksToWait )
{
uint32_t ulReturn;
configASSERT( uxIndexToWait < configTASK_NOTIFICATION_ARRAY_ENTRIES );
taskENTER_CRITICAL();
{
/* 通知值为0代表没有接收到通知 */
if( pxCurrentTCB->ulNotifiedValue[ uxIndexToWait ] == 0UL )
{
/* 将任务通知的状态设置为等待通知状态 */
pxCurrentTCB->ucNotifyState[ uxIndexToWait ] = taskWAITING_NOTIFICATION;
/* 如果设置了等待时间 */
if( xTicksToWait > ( TickType_t ) 0 )
{
/* 将当前任务添加到阻塞态任务列表 */
prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
traceTASK_NOTIFY_TAKE_BLOCK( uxIndexToWait );
/* 悬起 PendSV中断 准备进行任务切换 */
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
/* 任务如果进入阻塞,在这段时间里,任务可能被
发送通知的函数唤醒,唤醒则继续往下执行 */
taskENTER_CRITICAL();
{
/* 代码执行到这里有三种情况
1.一直没有接收到通知,任务阻塞超时被唤醒。
2.一进该函数通知值就不等于0,说明有通知,任务也不需要阻塞。
3.任务因没通知而阻塞,但是在该任务阻塞期间
有其他任务向该任务发送了通知,并唤醒该任务。*/
traceTASK_NOTIFY_TAKE( uxIndexToWait );
/* 再次获取任务通知的通知值,如果等于0说明接收通知失败*/
ulReturn = pxCurrentTCB->ulNotifiedValue[ uxIndexToWait ];
if( ulReturn != 0UL )
{
/* 接收通知成功
xClearCountOnExit == pdTRUE 通知值清0 : 二值信号量
xClearCountOnExit == pdFALSE 通知值减一 :计数信号量*/
if( xClearCountOnExit != pdFALSE )
{
pxCurrentTCB->ulNotifiedValue[ uxIndexToWait ] = 0UL;
}
else
{
pxCurrentTCB->ulNotifiedValue[ uxIndexToWait ] = ulReturn - ( uint32_t ) 1;
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 不论接收通知成功或者失败,都将任务通知的状态标记为未等待通知状态 */
pxCurrentTCB->ucNotifyState[ uxIndexToWait ] = taskNOT_WAITING_NOTIFICATION;
}
taskEXIT_CRITICAL();
return ulReturn;
}
#endif /* configUSE_TASK_NOTIFICATIONS */
/*-----------------------------------------------------------*/
源码分析:
1.如果任务没有通知的情况下,调用了接收通知函数,则将任务通知状态设置为等待通知状态,这样等调用发送通知函数的时候,就方便去唤醒该任务(有通知了,唤醒该任务去接收通知做后续处理)。
2.下图也印证了上面发送通知函数的是否可以覆写的问题
用任务通知模拟的二值或者计数型信号量与真实的有何区别?
真实的计数型信号量可以指定初始值,且有最大值,而模拟的初始值为0且不能设置最大值。
自己对比一下:
FreeRTOS信号量详解
3.接收任务通知函数xTaskNotifyWait()
该函数一般用来模拟队列或者事件组的接收,该函数可以在等待前和成功等待到任务通知后清除通知指定位,与消息队列不同的是在任务等待超时后任务通仍然可以获取通知值。但假设用该函数获取了一次通知值,但是没有清除通知值,等下次又调用该函数时会直接进入阻塞(因为判断是否有通知不是取决于通知是否不等于0(当然模拟信号量的确实以是否为0作为是否有通知的标准),而是别的任务发送通知到该任务)。
当然xTaskNotifyWait()函数只是一个宏,真正调用的是xTaskGenericNotifyWait()函数.
1.xTaskGenericNotifyWait()函数原型:
-
函数参数
1.uxIndexToWaitOn :任务的指定通知(任务通知相关数组下标)
2.ulBitesToClearOnEntry: 等待前指定清零的任务通知通知值比特位
3.ulBitesToClearOnExit :成功等待后指定清零的任务通知通知值比特位
4.pulNotificationValue :要获取的通知值(队列消息或者事件组)
5.xTicksToWait :阻塞等待任务通知值的最大时间 -
函数返回值
pdTRUE:等待任务通知成功
pdFALSE:等待任务通知失败
xTaskGenericNotifyWait()函数源码:
/*-----------------------------------------------------------*/
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
BaseType_t xTaskGenericNotifyWait( UBaseType_t uxIndexToWait,
uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t * pulNotificationValue,
TickType_t xTicksToWait )
{
BaseType_t xReturn;
/* 不能数组越界 */
configASSERT( uxIndexToWait < configTASK_NOTIFICATION_ARRAY_ENTRIES );
taskENTER_CRITICAL();
{
/* 若不为等待接收通知状态,说明其他任务没有发送通知给该任务
如果设置了超时时间,则任务需要进入阻塞态 */
if( pxCurrentTCB->ucNotifyState[ uxIndexToWait ] != taskNOTIFICATION_RECEIVED )
{
/* 等待通知前清除任务通知值中的位,因为通知任务或中断可能会设置位。 这可用于将值清除为零。 */
pxCurrentTCB->ulNotifiedValue[ uxIndexToWait ] &= ~ulBitsToClearOnEntry;
/* 设置任务通知的状态为等待通知状态 */
pxCurrentTCB->ucNotifyState[ uxIndexToWait ] = taskWAITING_NOTIFICATION;
/* 如果允许阻塞 */
if( xTicksToWait > ( TickType_t ) 0 )
{
/* 设置任务通知的状态为等待通知状态 */
prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
traceTASK_NOTIFY_WAIT_BLOCK( uxIndexToWait );
/* 悬起 PendSv中断,准备进行任务切换*/
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
/* 如果该任务被阻塞了,解除阻塞后继续从这往下执行 */
taskENTER_CRITICAL();
{
traceTASK_NOTIFY_WAIT( uxIndexToWait );
/* 当代码执行到这里可能是以下三种情况:
1.前面的判断不成立,一进来就有通知任务不需要进入阻塞
2.前面的判断成立,任务进入阻塞,但是有其他任务向该任务发送
通知并将该任务唤醒
3.前面的判断处理,任务进入阻塞,但是阻塞超时 任务被迫唤醒
*/
if( pulNotificationValue != NULL )
{
/* 输出当前通知值,该值可能已更改,也可能未更改
未更改说明是任务是超时被唤醒的此时虽然能获取通知值
但是通知值是上次的,其实是获取通知值失败的 */
*pulNotificationValue = pxCurrentTCB->ulNotifiedValue[ uxIndexToWait ];
}
/*如果设置了 ucNotifyValue,则任务从未进入阻止状态
(因为通知已挂起)或任务由于通知被唤醒。
否则,任务由于超时而解除阻止 */
/* 如果任务通知的状态还不等于等待接收通知状态的话
说明任务还是没有接收到通知任务只不过是超时被唤醒*/
if( pxCurrentTCB->ucNotifyState[ uxIndexToWait ] != taskNOTIFICATION_RECEIVED )
{
/* A notification was not received. */
xReturn = pdFALSE;
}
else
{
/* 在成功接收到通知后将通知值的指定比特位清零 */
pxCurrentTCB->ulNotifiedValue[ uxIndexToWait ] &= ~ulBitsToClearOnExit;
xReturn = pdTRUE;1
}
/* 不论接收通知成功或者失败都将任务通知的状态标记为未等待通知状态*/
pxCurrentTCB->ucNotifyState[ uxIndexToWait ] = taskNOT_WAITING_NOTIFICATION;
}
taskEXIT_CRITICAL();
return xReturn;
}
#endif /* configUSE_TASK_NOTIFICATIONS */
/*-----------------------------------------------------------*/
源码我就分析了,直接看代码注释,一定要接收/发送两个通知函数一起分析,这样理解起来轻松多了,然后再对比一下ulTaskNotifyTake()与xTaskNotifyWait函数有何区别,其实他们最大的区别就是服务对象不一样,前者为模拟信号量服务,后着为队列,事件组服务。
1.模拟的队列与真实的队列有何区别?
(1).真实的队列可以容纳多个数据而且数据大小可以指定,而任务通知只有一个数据(32位的无符号整数)
(2).真实的队列,写队列时可以阻塞,任务通知则不能
与事件组的区别:
其实你真正看懂源码,区别显而易见,留给你们的作业,哈哈哈哈哈哈哈
事件组详解
三.总结
发现学通了,都是有套路的,FreeRTOS全程都是一个风格,还怕研究不透嘛,只需将源码学透,接下来应用就是熟练度的问题了。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)