FreeRTOS-任务通知
-
前几章我们介绍了信号量、消息队列、事件标志组等用于任务之间同步或通信的工具,当信号量、消息队列等创建完成后所有任务都能访问这些资源。实际上,在应用过程中,很多时候任务通信或同步是一对一的,这时就可以使用任务通知来代替上面的功能,并且使用任务通知效率高,占用的RAM也小。
-
在使用任务通知前,需要将configUSE_TASK_NOTIFICATIONS置为1。FreeRTOS中每个任务都有32位的通知值,当某个任务要等待任务通知值进入阻塞状态的时候,向该任务发送任务通知就可以解除阻塞状态,并且接收的任务通知值也可以按一定的方式进行更新,方法如下。
-
- 不覆盖接收任务的通知值
-
- 覆盖接收任务的通知值
-
- 更新接收任务通知值得某些bit
-
- 增加接收任务的通知值
-
因此,只要能够合理的运用上面这些更新方法,就能够在一些场合中替代信号量、队列、事件标志组。
任务通知API
- 与队列、信号量、事件标志组一样,任务通知相关的API也有任务级的和中断级的,这里我们着重分析任务级API,中断级与之类似。
发送任务通知
- 发送任务通知主要有xTaskNotify()、xTaskNotifyGive()、xTaskNotifyAndQuery()这三个API。定义如下。
#define xTaskNotify( xTaskToNotify, ulValue, eAction ) xTaskGenericNotify( ( xTaskToNotify ), ( ulValue ), ( eAction ), NULL )
#define xTaskNotifyGive( xTaskToNotify ) xTaskGenericNotify( ( xTaskToNotify ), ( 0 ), eIncrement, NULL )
#define xTaskNotifyAndQuery( xTaskToNotify, ulValue, eAction, pulPreviousNotifyValue ) xTaskGenericNotify( ( xTaskToNotify ), ( ulValue ), ( eAction ), ( pulPreviousNotifyValue ) )
- 从上面定义可见,这三个API都是一个宏,实际上最终都调用了xTaskGenericNotify()这个函数,所以接下来我们只需要深入分析一下这个函数即可,函数定义如下。
BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, uint32_t *pulPreviousNotificationValue )
{
TCB_t * pxTCB;
BaseType_t xReturn = pdPASS;
uint8_t ucOriginalNotifyState;
configASSERT( xTaskToNotify );
pxTCB = ( TCB_t * ) xTaskToNotify;
taskENTER_CRITICAL();
{
if( pulPreviousNotificationValue != NULL )----1
{
*pulPreviousNotificationValue = pxTCB->ulNotifiedValue;
}
ucOriginalNotifyState = pxTCB->ucNotifyState;----2
pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;----3
switch( eAction )----4
{
case eSetBits :
pxTCB->ulNotifiedValue |= ulValue;----5
break;
case eIncrement :
( pxTCB->ulNotifiedValue )++;----6
break;
case eSetValueWithOverwrite :
pxTCB->ulNotifiedValue = ulValue;----7
break;
case eSetValueWithoutOverwrite :
if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED )----8
{
pxTCB->ulNotifiedValue = ulValue;
}
else
{
xReturn = pdFAIL;
}
break;
case eNoAction:----9
break;
}
traceTASK_NOTIFY();
if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )----10
{
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB );
configASSERT( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) == NULL );
#if( configUSE_TICKLESS_IDLE != 0 )
{
prvResetNextTaskUnblockTime();
}
#endif
if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
{
taskYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
return xReturn;
}
- 该函数的输入参数有四个第一个xTaskToNotify是发送对象任务的句柄,ulValue为发送的任务通知值,eAction是任务通知的模式,有五个参数稍后分析,pulPreviousNotificationValue 是用以记录前一个任务通知值。
- eAction是一个枚举类型的数据,每个参数的定义和作用如下。
typedef enum
{
eNoAction = 0, /* 只通知任务而不更新任务通知值 */
eSetBits, /*设置任务通知值的某些bit*/
eIncrement, /* 增加任务通知值 */
eSetValueWithOverwrite, /* 无论前一个任务通知值是否被处理,复写任务通知值 */
eSetValueWithoutOverwrite /* 如果前一个任务通知值被处理了,复写任务通知值*/
} eNotifyAction;
- 接下来分析一下上面函数的源码。
- 判断pulPreviousNotificationValue是否是一个空指针,不是空指针,说明需要保留原来的人物通知值。
- 记录原来任务的状态
- 更新现在的任务状态为接收完成
- 根据设置的任务通知值更新方式来做处理
- eaciton = eSetBits,则将相应的位置1,置位方法与事件标志组相同
- eaciton = eIncrement,任务通知值在原来的基础上+1
- eaciton = eSetValueWithOverwrite 直接覆盖原来的任务通知值
- eaciton = eSetValueWithoutOverwrite先判断原来任务是否已经将前一个任务通知值处理完成了,如果是则更新任务通知值,否则返回更新失败
- eaciton = eNoAction,只做通知,不对任务通知值做任何操作
- 判断原来任务状态是否为等待任务通知,如果是,则接受完成,将任务阻塞态解除,并判断是否要进行一次任务调度
- 所以从上面的分析可以判断发送任务API的功能如下表所示
函数名 | 功能描述 |
---|---|
xTaskNotify() | 按照任务通知值更新方式发送任务通知值,不保留原任务通知值 |
xTaskNotifyGive() | 不发送任务通知值,只发送任务通知,但任务通知值会自动+1,不保留原任务通知值 |
xTaskNotifyAndQuery | 发送任务通知值,并保留原任务通知值,该函数可以替代上面两个函数 |
接收任务通知
- 接收任务通知的API有ulTaskNotifyTake和xTaskNotifyWait()这两个函数, 下面分别来说分析一下这两个函数。
ulTaskNotifyTake()
- 函数定义如下。
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait )
{
uint32_t ulReturn;
taskENTER_CRITICAL();
{
if( pxCurrentTCB->ulNotifiedValue == 0UL )-----1
{
pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;-----2
if( xTicksToWait > ( TickType_t ) 0 )-----3
{
prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
traceTASK_NOTIFY_TAKE_BLOCK();
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
taskENTER_CRITICAL();
{
traceTASK_NOTIFY_TAKE();
ulReturn = pxCurrentTCB->ulNotifiedValue;-----4
if( ulReturn != 0UL )-----5
{
if( xClearCountOnExit != pdFALSE )-----6
{
pxCurrentTCB->ulNotifiedValue = 0UL;
}
else-----7
{
pxCurrentTCB->ulNotifiedValue = ulReturn - 1;
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;-----8
}
taskEXIT_CRITICAL();
return ulReturn;
}
- 在分析本段代码前,需要说明的是,FreeRTOS官方给出这么一个函数的目的就是为了实现二值信号量或计数信号量的快速通信问题,所以这个函数只能用于模拟二值信号量或计数信号量的接收。
- 既然是只针对二值信号量和计数信号量,那么只需要判断任务通知值是否为0就可以判断该任务是否接收到数据
- 如果没有接收到数据,说明还处在等待任务通知状态,所以更新任务状态
- 判断阻塞时间是否大于0,如果大于0则将任务添加到阻塞列表中,进入阻塞状态,并进行一次任务调度
- 将任务通知值赋值给返回值
- 判断返回值是否为0
- 如果返回值不为0,则判断退出时是否要清除任务通知值,如果清除,则此时相当于二值信号量,每次接受都会清零,每次发送都会+1,所以只有0和1这两种状态
- 如果不清除,则此时为计数信号量,每次接受任务通知值都会-1,每次发送任务通知值都会+1
- 将任务状态更新为等待任务通知
xTaskNotifyWait()
- 函数定义如下
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait )
{
BaseType_t xReturn;
taskENTER_CRITICAL();
{
if( pxCurrentTCB->ucNotifyState != taskNOTIFICATION_RECEIVED )----1
{
pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnEntry;----2
pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;----3
if( xTicksToWait > ( TickType_t ) 0 )----4
{
prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
traceTASK_NOTIFY_WAIT_BLOCK();
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
taskENTER_CRITICAL();
{
traceTASK_NOTIFY_WAIT();
if( pulNotificationValue != NULL )----5
{
*pulNotificationValue = pxCurrentTCB->ulNotifiedValue;
}
if( pxCurrentTCB->ucNotifyState == taskWAITING_NOTIFICATION )----6
{
xReturn = pdFALSE;
}
else----7
{
pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnExit;
xReturn = pdTRUE;
}
pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;----8
}
taskEXIT_CRITICAL();
return xReturn;
}
- 输入参数有四个,ulBitsToClearOnEntry设置进入时需要清除的位,ulBitsToClearOnExit设置退出时需要清除的位,这两个参数设置机制与事件标志组相同;pulNotificationValue记录原任务通知值;xTicksToWait 阻塞时间。值得提到的是,该函数更适合模拟事件标志位或二值信号量时使用。
- 判断任务是否是接收完成的状态,因为任务通知只有两个状态,要么等待接收状态(即阻塞态),要么接收完成状态
- 该任务是阻塞态,进入阻塞态前将一些位清零
- 更新任务状态
- 判断设置阻塞时间是否有效,有效则将任务添加到阻塞列表中
- 判断是否要保留原任务通知值
- 判断任务是否处于等待任务通知状态,如果是,则说明没有接收到任务通知值
- 接收到任务通知值了,退出接收状态时进行位清零,并返回接收成功
- 更新任务状态
- 到这里就将任务级的API分析完了,接下来给出一些使用示例。
模拟二值信号量
使用任务通知来模拟二值信号量,用按键任务来发送任务通知,
在另一个任务中接收任务通知,并输出相关信息
-
根据上面分析,模拟二值信号量的时候三个发送函数任选一个即可,两个接收函数任选一个即可。这里选择xTaskNotify()和ulTaskNotifyTake()这两个函数
-
按键任务
u8 keyFlag=0;
void key_task(void* pvParameters)
{
while(1)
{
if (Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON)
{
if ( (keyFlag&0x01) == 0x01)
keyFlag &= ~(0x01<<0);
else
keyFlag |= (0x01<<0);
}
vTaskDelay(10);
}
}
- 发送任务
#define TASK1_TASK_PRIO 3
#define TASK1_STACK_SIZE 50
TaskHandle_t Task1Task_Handler;
void task1_task(void *pvParameters);
#define TASK1_ACTION eIncrement
void task1_task(void *pvParameters)
{
BaseType_t err;
while(1)
{
if ( (keyFlag&0x01) == 0x01)
{
err = xTaskNotify(Task2Task_Handler,1,TASK1_ACTION);
if (err != pdPASS)
{
printf("Task Notify Failed!!!\r\n");
}
else
{
printf("Task1 Notified!!!\r\n");
}
keyFlag &= ~(0x01<<0);
}
vTaskDelay(10);
}
}
- 接收任务
void task2_task(void *pvParameters)
{
BaseType_t err;
while(1)
{
err = ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
if (err == pdPASS)
{
printf("task2 Received!!\r\n");
}
LED1=~LED1;
vTaskDelay(10);
}
}
模拟计数信号量
- 在上面的基础上只需要稍作改动即可模拟计数信号量
void task4_task(void *pvParameters)
{
while(1)
{
xTaskNotifyGive(Task5Task_Handler);
vTaskDelay(500);
}
}
void task5_task(void *pvParameters)
{
uint32_t countValue=0;
while(1)
{
countValue = ulTaskNotifyTake(pdFALSE,portMAX_DELAY);
printf("countValue = %d\r\n",countValue);
vTaskDelay(1000);
}
}
模拟事件标志位
- 利用按键来发送任务通知,使标志位置1,在另外两个任务中接收任务通知
- 按键任务代码如下
void key_task(void* pvParameters)
{
BaseType_t err;
while(1)
{
if (Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON)
{
err = xTaskNotify(Task1Task_Handler,(0x01<<0),TASK1_ACTION);
if (err == pdPASS)
{
printf("Task1 SetBits!!!\r\n");
}
else
{
printf("Task1 SetBits Failed!!!\r\n");
}
}
if (Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON)
{
err = xTaskNotify(Task2Task_Handler,(0x01<<1),TASK2_ACTION);
if (err == pdPASS)
{
printf("Task2 SetBits!!!\r\n");
}
else
{
printf("Task2 SetBits Failed!!!\r\n");
}
}
vTaskDelay(10);
}
}
接收任务定义如下
void task1_task(void *pvParameters)
{
BaseType_t err;
while(1)
{
err = xTaskNotifyWait(0,ULONG_MAX,NULL,portMAX_DELAY);
if (err != pdPASS)
{
printf("Task1 Take Failed!!!\r\n");
}
else
{
printf("Task1 Taked!!!\r\n");
}
LED0=~LED0;
vTaskDelay(10);
}
}
void task2_task(void *pvParameters)
{
BaseType_t err;
while(1)
{
err = xTaskNotifyWait(0,ULONG_MAX,NULL,portMAX_DELAY);
if (err != pdPASS)
{
printf("Task2 Take Failed!!!\r\n");
}
else
{
printf("Task2 Taked!!!\r\n");
}
LED1=~LED1;
vTaskDelay(10);
}
}
- 至此就将FreeRTOS中的任务通知讲述完了,其实到这里,FreeRTOS的主干内容基本讲述完了,把剩下的低功耗模式和空闲任务以及内存管理讲述一下,FreeRTOS应用部分就完结了。