12. FreeRTOS的任务通知

一、FreeRTOS的任务通知

1.1、FreeRTOS的任务通知的简介

  在 FreeRTOS 中,每一个任务都有两个用于任务通知功能的数组,分别为 任务通知数组任务通知状态数组。其中 任务通知数组 中的每一个元素都是一个 32 位无符号类型的通知值;而 任务通知状态数组 中的元素则表示与之对应的任务通知的状态。

  任务通知数组 中的 32 位无符号通知值,用于 任务到任务中断到任务 发送通知的“媒介”。当通知值为 0 时,表示没有任务通知;当通知值不为 0 时,表示有任务通知,并且通知值就是通知的内容。

  任务通知状态数组 中的元素,用于标记任务通知数组中通知的状态,任务通知有三种状态,分别为未等待通知状态、等待通知状态和等待接收通知状态。其中未等待通知状态为任务通知的复位状态;当任务在没有通知的时候接收通知时,在任务阻塞等待任务通知的这段时间内,任务所等待的任务通知就处于等待通知状态;当有其他任务向任务发送通知,但任务还未接收这一通知的这段期间内,任务通知就处于等待接收通知状态。

  任务通知功能所使用到的 任务通知数组任务通知状态数组 为任务控制块中的成员变量,因此任务通知的传输是直接传出到任务中的,不同通过任务的通讯对象(队列、事件标志组和信号量就属于通讯对象)这个间接的方式。间接通讯示意图如下所示:

间接通讯示意图

  任务通知则是直接地往任务中发送通知,直接通讯示意图如下所示:

直接通讯示意图

使用队列、信号量、事件标志组时都需另外创建一个结构体,通过中间的结构体进行间接通信。

使用任务通知时,任务结构体 TCB 中就包含了内部对象,可以直接接收别人发过来的“通知”。

1.2、任务通知值的更新方式

  • 不覆盖接受任务的通知值。
  • 覆盖接受任务的通知值。
  • 更新接受任务通知值的一个或多个 bit。
  • 增加接受任务的通知值。

只要合理、灵活的利用任务通知的特点,可以在一些场合中替代队列、信号量和事件标志组。

1.3、任务通知的优势

  • 效率更高:使用任务通知向任务发送事件或数据比使用队列、事件标志组或信号量快得多。
  • 使用内存更小:使用其它方法时都要创建对应的结构体,而使用任务通知无需额外创建结构体,每个通知只需要在每个任务中占用固定的 5 字节内存。

1.4、任务通知的缺点

  虽然任务通知功能相比通讯对象,有着更快、占用内存少的优点,但是任务通知功能并不能适用于所有情况,例如以下列出的几种情况:

【1】、无法发送数据到 ISR

  通讯对象可以发送事件或数据从中断到任务,或从任务到中断,但是由于任务通知依赖于任务控制块中的两个成员变量,并且中断不是任务,因此任务通知功能并不适用于从任务往中断发送事件或数据的这种情况,但是任务通知功能可以在任务之间或从中断到任务发送事件或数据。

ISR 没有任务结构体,所以无法给 ISR 发送数据。但是 ISR 可以使用任务通知的功能,发数据给任务。

【2】、无法广播给多个任务

  通讯对象可以被已知通讯对象句柄的任意多个任务或中断访问(发送或接收),但任务通知是直接发送事件或数据到指定接收任务的,因传输的事件或数据只能由接收任务处理。然而在实际中很少受到这种情况的限制,因为,虽然多个任务和中断发送事件或数据到一个通讯对象是很常见的,但很少出现多个任务或中断接收同一个通讯对象的情况。

讯对象中的事件标志组是可以将一个事件同时发送到多个任务中的,但任务通知只能是被指定的一个接收任务接收并处理。

【3】、无法缓冲多个数据项

  通讯对象中的队列是可以一次性保存多个已经被发送到队列但还未被接收的事件或数据的,也就是说,通讯对象有着一定的缓冲多个数据的能力,但是任务通知是通过更新任务通知值来发送事件或数据的,一个任务通知值只能保存一次。

任务通知是通过更新任务通知值来发送数据的,任务结构体中只有一个任务通知值,只能保持一个数据。

【4】、发送受阻不支持阻塞

  当通讯对象处于暂时无法写入的状态(例如队列已满,此时无法再向队列写入消息)时,发送任务是可以选择阻塞等待接收任务接收,但是任务因尝试发送任务通知到已有任务通知但还未处理的任务而进行阻塞等待的。但是任务通知也很少在实际情况中收到这种情况的限制。

二、FreeRTOS任务通知相关API函数

2.1、任务通知和任务通知状态

  任务都有一个结构体:任务控制块 TCB,它里边有两个结构体成员变量:

typedef struct tskTaskControlBlock
{
    ...
    #if ( configUSE_TASK_NOTIFICATIONS == 1 )
        // 任务通知数组
        volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
        // 任务通知状态数组
        volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
    #endif
    ...
} tskTCB;

#define configTASK_NOTIFICATION_ARRAY_ENTRIES           1                       // 定义任务通知数组的大小,默认: 1 

  任务通知值的更新方式有多种类型:

  • 计数值(数值累加,类似信号量)。
  • 相应位置 1(类似事件标志组)。
  • 任意数值(支持覆写和不覆写,类似队列)

  任务通知状态共有 3 种取值:

#define taskNOT_WAITING_NOTIFICATION              ( ( uint8_t ) 0 )             // 任务未等待通知
#define taskWAITING_NOTIFICATION                  ( ( uint8_t ) 1 )             // 等待通知
#define taskNOTIFICATION_RECEIVED                 ( ( uint8_t ) 2 )             // 等待接收
  • 任务未等待通知:任务通知默认的初始化状态。
  • 等待通知:接收方已经准备好了(调用了接收任务通知函数),等待发送方给个通知。
  • 等待接收:发送方已经发送出去(调用了发送任务通知函数),等待接收方接收。

2.2、发送通知

  发送通知 API 函数可以用于任务和中断服务函数中。

2.2.1、在任务中发送通知

// 在任务中发送通知,通知方式可以自由指定,并且不获取发送任务通知前任务通知的通知值
#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 ) )

// 在任务中发送通知,通知方式为将通知值加 1,并且不获取发送任务通知前任务通知的通知值
#define xTaskNotifyGive( xTaskToNotify ) \
    xTaskGenericNotify( ( xTaskToNotify ), ( tskDEFAULT_INDEX_TO_NOTIFY ), ( 0 ), eIncrement, NULL )

  从上面的代码中可以看出,三个用于在任务中发送任务通知的函数,实际上都是调用了函数 xTaskGenericNotify() 来发送任务通知的,只是传入了不同的参数。函数 xTaskGenericNotify() 的源代码如下所示:

// 返回值: pdPASS: 任务通知发送成功; pdFAIL: 任务通知发送失败;
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;

    traceENTER_xTaskGenericNotify( xTaskToNotify, uxIndexToNotify, ulValue, eAction, pulPreviousNotificationValue );

    // 数组下标不能越界,宏 configTASK_NOTIFICATION_ARRAY_ENTRIES 用于定义一个任务包含的最大通知数量,即任务通知相关数组中元素的个数
    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 )
        {
            // 此通知方式用于将通知值的指定比特位置1,CE类似于事件标志组
            case eSetBits:
                pxTCB->ulNotifiedValue[ uxIndexToNotify ] |= ulValue;
                break;

            // 此通知方式用于将通知值加1,类似于计数型信号量
            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:
                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

            taskYIELD_ANY_CORE_IF_USING_PREEMPTION( pxTCB );
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    taskEXIT_CRITICAL();                                                        // 退出临界区

    traceRETURN_xTaskGenericNotify( xReturn );

    return xReturn;
}

  任务通知方式共有以下几种:

typedef enum
{
    eNoAction = 0,                  // 无操作
    eSetBits,                       // 更新指定bit
    eIncrement,                     // 通知值加一
    eSetValueWithOverwrite,         // 覆写的方式更新通知值
    eSetValueWithoutOverwrite       // 不覆写通知值
} eNotifyAction;

函数 xTaskNotify():此函数用于往指定任务发送任务通知,通知方式可以自由指定,并且不获取发送任务通知前任务通知的通知值。

函数 xTaskNotifyAndQuery():此函数用于往指定任务发送任务通知,通知方式可以自由指定,并且获取发送任务通知前任务通知的通知值。

函数 xTaskNotifyGive():此函数用于往指定任务发送任务通知,通知方式为将通知值加 1,并且不获取发送任务通知前任务通知的通知值。

2.2.2、在中断中发送通知

// 在中断中发送任务通知,通知方式可以自由指定,并且不获取发送任务通知前任务通知的通知值
#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 ) )

// 在中断中发送任务通知,通知方式为将通知值加 1,并且不获取发送任务通知前任务通知的通知值
#define vTaskNotifyGiveFromISR( xTaskToNotify, pxHigherPriorityTaskWoken ) \
    vTaskGenericNotifyGiveFromISR( ( xTaskToNotify ), ( tskDEFAULT_INDEX_TO_NOTIFY ), ( pxHigherPriorityTaskWoken ) )

  从上面的代码可以看出,函数 xTaskNotifyFromISR() 和函数 xTaskNotifyAndQueryFromISR() 实际上都是调用了函数 xTaskGenericNotifyFromISR(),而函数 vTaskNotifyGiveFromISR() 实际上则是调用了函数 vTaskGenericNotifyGiveFromISR()

  函数 xTaskGenericNotifyFromISR() 的源代码如下所示:

// 返回值: pdPASS: 任务通知发送成功; pdFAIL: 任务通知发送失败;
BaseType_t xTaskGenericNotifyFromISR( TaskHandle_t xTaskToNotify,               // 接收任务通知的任务
                                      UBaseType_t uxIndexToNotify,              // 任务的指定通知 (任务通知相关数组下标)
                                      uint32_t ulValue,                         // 通知值
                                      eNotifyAction eAction,                    // 通知方式
                                      uint32_t * pulPreviousNotificationValue,  // 用于获取发送通知前的通知值
                                      BaseType_t * pxHigherPriorityTaskWoken )  // 用于标记函数退出后是否需要进行任务切换
{
    TCB_t * pxTCB;
    uint8_t ucOriginalNotifyState;
    BaseType_t xReturn = pdPASS;
    UBaseType_t uxSavedInterruptStatus;

    traceENTER_xTaskGenericNotifyFromISR( xTaskToNotify, uxIndexToNotify, ulValue, eAction, pulPreviousNotificationValue, pxHigherPriorityTaskWoken );

    configASSERT( xTaskToNotify );
    // 数组下标不能越界,宏 configTASK_NOTIFICATION_ARRAY_ENTRIES  用于定义一个任务包含的最大通知数量, 即任务通知相关数组中元素的个数
    configASSERT( uxIndexToNotify < configTASK_NOTIFICATION_ARRAY_ENTRIES );

    portASSERT_IF_INTERRUPT_PRIORITY_INVALID();                                 // 只有受FreeRTOS 管理的中断才能调用该函数

    pxTCB = xTaskToNotify;

    uxSavedInterruptStatus = ( UBaseType_t ) taskENTER_CRITICAL_FROM_ISR();     // 获取中断状态,并屏蔽中断
    {
        if( pulPreviousNotificationValue != NULL )                              // 判断是否获取发送通知前的通知值
        {
            // 获取发送通知前的通知值
            *pulPreviousNotificationValue = pxTCB->ulNotifiedValue[ uxIndexToNotify ];
        }

        ucOriginalNotifyState = pxTCB->ucNotifyState[ uxIndexToNotify ];        // 记录发送通知前任务通知的状态
        // 将任务通知的状态设置为等待接收通知状态,因为下面要发送通知了,将任务通知设置为等待接收通知状态后,
        // 接收任务接收任务时,就能够通过任务通知的状态,来判断是否有待接收的任务通知
        pxTCB->ucNotifyState[ uxIndexToNotify ] = taskNOTIFICATION_RECEIVED;

        switch( eAction )
        {
            //  此通知方式用于将通知值的指定比特位置1,类似于事件标志组
            case eSetBits:
                pxTCB->ulNotifiedValue[ uxIndexToNotify ] |= ulValue;
                break;

            // 此通知方式用于将通知值加1,类似于计数型信号量
            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:
                // 输入参数有误,强行断言
                configASSERT( xTickCount == ( TickType_t ) 0 );
                break;
        }

        traceTASK_NOTIFY_FROM_ISR( uxIndexToNotify );                           // 用于调试

        if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )                 // 如果在此之前,任务因等待任务通知而被阻塞,则现在解除阻塞
        {
            // 任务是因为等待任务通知被阻塞,而不是等待事件被阻塞,因此任务不应该在任何一个事件列表中
            configASSERT( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) == NULL );

            if( uxSchedulerSuspended == ( UBaseType_t ) 0U )                    // 判断任务调度器是否运行
            {
                listREMOVE_ITEM( &( pxTCB->xStateListItem ) );                  // 将任务从所在任务状态列表中移除 
                prvAddTaskToReadyList( pxTCB );                                 // 将任务添加到就绪态任务列表中
            }
            else
            {
                // 任务调度器被挂起,则将任务添加到挂起态任务列表
                listINSERT_END( &( xPendingReadyList ), &( pxTCB->xEventListItem ) );
            }

            #if ( configNUMBER_OF_CORES == 1 )
            {
                if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )              // 有任务解除阻塞后,就应该判断是否需要进行任务切换
                {
                    if( pxHigherPriorityTaskWoken != NULL )                     // 根据需要返回需要进行任务切换
                    {
                        *pxHigherPriorityTaskWoken = pdTRUE;
                    }

                    xYieldPendings[ 0 ] = pdTRUE;                               // 标记需要进行任务切换
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            #else /* #if ( configNUMBER_OF_CORES == 1 ) */
            {
                #if ( configUSE_PREEMPTION == 1 )
                {
                    prvYieldForTask( pxTCB );

                    if( xYieldPendings[ portGET_CORE_ID() ] == pdTRUE )
                    {
                        if( pxHigherPriorityTaskWoken != NULL )
                        {
                            *pxHigherPriorityTaskWoken = pdTRUE;
                        }
                    }
                }
                #endif /* if ( configUSE_PREEMPTION == 1 ) */
            }
            #endif /* #if ( configNUMBER_OF_CORES == 1 ) */
        }
    }
    taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );                       //  恢复中断状态

    traceRETURN_xTaskGenericNotifyFromISR( xReturn );

    return xReturn;
}

  从上面的代码中可以看出,函数 xTaskGenericNotifyFromISR() 与函数 xTaskNotify() 是很相似的,只是多了对中断做了一些相应的处理。

  函数 vTaskGenericNotifyGiveFromISR() 的源代码如下所示:

void vTaskGenericNotifyGiveFromISR( TaskHandle_t xTaskToNotify,                 // 接收任务通知的任务
                                    UBaseType_t uxIndexToNotify,                // 任务的指定通知(任务通知相关数组下标)
                                    BaseType_t * pxHigherPriorityTaskWoken )    // 用于标记函数退出后是否需要进行任务切换
{
    TCB_t * pxTCB;
    uint8_t ucOriginalNotifyState;
    UBaseType_t uxSavedInterruptStatus;

    traceENTER_vTaskGenericNotifyGiveFromISR( xTaskToNotify, uxIndexToNotify, pxHigherPriorityTaskWoken );

    configASSERT( xTaskToNotify );
    // 数组下标不能越界,宏 configTASK_NOTIFICATION_ARRAY_ENTRIES 用于定义一个任务包含的最大通知数量,即任务通知相关数组中元素的个数
    configASSERT( uxIndexToNotify < configTASK_NOTIFICATION_ARRAY_ENTRIES );

    portASSERT_IF_INTERRUPT_PRIORITY_INVALID();                                 // 有受FreeRTOS 管理的中断才能调用该函数

    pxTCB = xTaskToNotify;

    uxSavedInterruptStatus = ( UBaseType_t ) taskENTER_CRITICAL_FROM_ISR();     // 保存中断状态,并屏蔽中断
    {
        ucOriginalNotifyState = pxTCB->ucNotifyState[ uxIndexToNotify ];        // 记录发送通知前任务通知的状态
        // 将任务通知的状态设置为等待接收通知状态,因为下面要发送通知了,将任务通知设置为等待接收通知状态后,
        // 接收任务接收任务时,就能够通过任务通知的状态,来判断是否有待接收的任务通知
        pxTCB->ucNotifyState[ uxIndexToNotify ] = taskNOTIFICATION_RECEIVED;

        // 通知值加1,因此此函数类似于通知方式为 eIncrement 的函数 xTaskGenericNotifyFromISR()
        ( pxTCB->ulNotifiedValue[ uxIndexToNotify ] )++;

        traceTASK_NOTIFY_GIVE_FROM_ISR( uxIndexToNotify );                      // 用于调试

        // 如果在此之前,任务因等待任务通知而被阻塞,则现在解除阻塞
        if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )
        {
            // 任务是因为等待任务通知被阻塞,而不是等待事件被阻塞,此任务不应该在任何一个事件列表中
            configASSERT( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) == NULL );

            if( uxSchedulerSuspended == ( UBaseType_t ) 0U )                    // 判断任务调度器是否运行
            {
                listREMOVE_ITEM( &( pxTCB->xStateListItem ) );                  // 将任务从所在任务状态列表中移除
                prvAddTaskToReadyList( pxTCB );                                 // 将任务添加到就绪态任务列表中
            }
            else
            {
                // 任务调度器被挂起,则将任务添加到挂起态任务列表
                listINSERT_END( &( xPendingReadyList ), &( pxTCB->xEventListItem ) );
            }

            #if ( configNUMBER_OF_CORES == 1 )
            {
                if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )              // 有任务解除阻塞后,就应该判断是否需要进行任务切换
                {
                    if( pxHigherPriorityTaskWoken != NULL )                     // 根据需要返回需要进行任务切换
                    {
                        *pxHigherPriorityTaskWoken = pdTRUE;                    // 标记需要进行任务切换
                    }

                    xYieldPendings[ 0 ] = pdTRUE;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            #else /* #if ( configNUMBER_OF_CORES == 1 ) */
            {
                #if ( configUSE_PREEMPTION == 1 )
                {
                    prvYieldForTask( pxTCB );

                    if( xYieldPendings[ portGET_CORE_ID() ] == pdTRUE )
                    {
                        if( pxHigherPriorityTaskWoken != NULL )
                        {
                            *pxHigherPriorityTaskWoken = pdTRUE;
                        }
                    }
                }
                #endif /* #if ( configUSE_PREEMPTION == 1 ) */
            }
            #endif /* #if ( configNUMBER_OF_CORES == 1 ) */
        }
    }
    taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );                       //  恢复中断状态

    traceRETURN_vTaskGenericNotifyGiveFromISR();
}

  从以上代码中可以看出,函数 vTaskGenericNotifyGiveFromISR() 就是通知方式为 eIncrement 并且没有返回值的函数 xTaskGenericNotifyFromISR()

函数 xTaskNotifyFromISR():此函数用于在中断中往指定任务发送任务通知,通知方式可以自由指定,并且不获取发送任务通知前任务通知的通知值,但获取发送通知后是否需要进行任务切换的标志。

函数 xTaskNotifyAndQueryFromISR():此函数用于在中断中往指定任务发送任务通知,通知方式可以自由指定,并且获取发送任务通知前任务通知的通知值,和发送通知后是否需要进行任务切换的标志。

函数 vTaskNotifyGiveFromISR():此函数用于在中断中往指定任务发送任务通知,通知方式为将通知值加 1,并且不获取发送任务通知前任务通知的通知值,但获取发送通知后是否需要进行任务切换的标志。

2.3、接收通知

  接收通知 API 函数只能用在任务中。用于获取任务通知的 API 函数有两个,分别为函数 ulTaskNotifyTake() 和函数 xTaskNotifyWait()

  ulTaskNotifyTake() 函数用于获取任务通知的通知值,并且在成功获取任务通知的通知值后,可以指定将通知值清零或减 1。此函数实际上是一个宏定义,具体的代码如下所示:

#define ulTaskNotifyTake( xClearCountOnExit, xTicksToWait ) \
    ulTaskGenericNotifyTake( ( tskDEFAULT_INDEX_TO_NOTIFY ), ( xClearCountOnExit ), ( xTicksToWait ) )

  从上面的代码中可以看出,函数 ulTaskNotifyTake() 实际上是调用了函数 ulTaskGenericNotifyTake(),函数 ulTaskGenericNotifyTake() 的的源代码如下所示:

// 返回值: 0: 接收失败; 非0: 接收成功,返回任务通知的通知值;
uint32_t ulTaskGenericNotifyTake( UBaseType_t uxIndexToWaitOn,                  // 任务的指定通知(任务通知相关数组下标)
                                    BaseType_t xClearCountOnExit,               // 指定在成功接收通知后,将通知值清零或减1,pdTRUE:把通知值清零;pdFALSE:把通知值减一
                                    TickType_t xTicksToWait )                   // 阻塞等待任务通知值的最大时间
{
    uint32_t ulReturn;
    BaseType_t xAlreadyYielded, xShouldBlock = pdFALSE;

    traceENTER_ulTaskGenericNotifyTake( uxIndexToWaitOn, xClearCountOnExit, xTicksToWait );

    // 数组下标不能越界,宏 configTASK_NOTIFICATION_ARRAY_ENTRIES 用于定义一个任务包含的最大通知数量, 即任务通知相关数组中元素的个数
    configASSERT( uxIndexToWaitOn < configTASK_NOTIFICATION_ARRAY_ENTRIES );

    vTaskSuspendAll(); 
    {
        taskENTER_CRITICAL();                                                   // 进入临界区
        {
            if( pxCurrentTCB->ulNotifiedValue[ uxIndexToWaitOn ] == 0U )        // 判断任务通知的通知值是否为 0,为 0 表示没有收到任务通知
            {
                // 设置任务通知的状态为等待通知状态
                pxCurrentTCB->ucNotifyState[ uxIndexToWaitOn ] = taskWAITING_NOTIFICATION;

                if( xTicksToWait > ( TickType_t ) 0 )                           // 判断是否允许阻塞等待任务通知
                {
                    xShouldBlock = pdTRUE;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        taskEXIT_CRITICAL();                                                    // 退出临界区

        if( xShouldBlock == pdTRUE )                                            // 如果在此之前,任务被阻塞,则解除阻塞后会执行到这
        {
            traceTASK_NOTIFY_TAKE_BLOCK( uxIndexToWaitOn );
            prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );             // 将当前任务添加到阻塞态任务列表
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    xAlreadyYielded = xTaskResumeAll();

    /* Force a reschedule if xTaskResumeAll has not already done so. */
    if( ( xShouldBlock == pdTRUE ) && ( xAlreadyYielded == pdFALSE ) )
    {
        taskYIELD_WITHIN_API();                                                 // 挂起 PendSV 准备进行任务切换,任务切换后,当前任务就被阻塞了
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    taskENTER_CRITICAL();                                                       // 进入临界区
    {
        traceTASK_NOTIFY_TAKE( uxIndexToWaitOn );                               // 用于调试
        ulReturn = pxCurrentTCB->ulNotifiedValue[ uxIndexToWaitOn ];            // 再次获取任务通知的通知值

        if( ulReturn != 0U )                                                    // 当通知值不为 0 的时候,说明任务已经收到通知了
        {
            if( xClearCountOnExit != pdFALSE )                                  // 判断是否需要在成功读取通知后,将通知值清零
            {
                // 将通知值清零
                pxCurrentTCB->ulNotifiedValue[ uxIndexToWaitOn ] = ( uint32_t ) 0U;
            }
            else
            {
                // 将通知值减 1
                pxCurrentTCB->ulNotifiedValue[ uxIndexToWaitOn ] = ulReturn - ( uint32_t ) 1;
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

        // 不论接收通知成功或者失败,都将任务通知的状态标记为未等待通知状态
        pxCurrentTCB->ucNotifyState[ uxIndexToWaitOn ] = taskNOT_WAITING_NOTIFICATION;
    }
    taskEXIT_CRITICAL();                                                        // 退出临界区

    traceRETURN_ulTaskGenericNotifyTake( ulReturn );

    // 返回 0:接收通知失败; 返回非 0:接收通知成功,返回通知值;
    return ulReturn;
}

  xTaskNotifyWait() 函数用于等待任务通知通知值中的指定比特位被置一,此函数可以在等待前和成功等待到任务通知通知值中的指定比特位被置一后清零指定比特位,并且还能获取等待超时后任务通知的通知值。此函数实际上是一个宏定义,具体的代码如下所示:

#define xTaskNotifyWait( ulBitsToClearOnEntry, ulBitsToClearOnExit, pulNotificationValue, xTicksToWait ) \
    xTaskGenericNotifyWait( tskDEFAULT_INDEX_TO_NOTIFY, ( ulBitsToClearOnEntry ), ( ulBitsToClearOnExit ), ( pulNotificationValue ), ( xTicksToWait ) )

  从上面的代码中可以看出,函数 xTaskNotifyWait() 实际上是调用了函数 xTaskGenericNotifyWait(),函数 xTaskGenericNotifyWait() 的源代码如下所示:

// 返回值: pdTRUE: 等待任务通知成功; pdFALSE: 等待任务通知失败;
BaseType_t xTaskGenericNotifyWait( UBaseType_t uxIndexToWaitOn,                 // 任务的指定通知(任务通知相关数组下标)
                                    uint32_t ulBitsToClearOnEntry,              // 等待前指定清零的任务通知通知值比特位(旧值对应bit清0)
                                    uint32_t ulBitsToClearOnExit,               // 成功等待后指定清零的任务通知通知值比特位(新值对应bit清0)
                                    uint32_t * pulNotificationValue,            // 等待超时后任务通知的通知值(如果不需要取出,可设为NULL)
                                    TickType_t xTicksToWait )                   // 阻塞等待任务通知值的最大时间
{
    BaseType_t xReturn, xAlreadyYielded, xShouldBlock = pdFALSE;

    traceENTER_xTaskGenericNotifyWait( uxIndexToWaitOn, ulBitsToClearOnEntry, ulBitsToClearOnExit, pulNotificationValue, xTicksToWait );

    // 数组下标不能越界, 宏 configTASK_NOTIFICATION_ARRAY_ENTRIES 用于定义一个任务包含的最大通知数量,即任务通知相关数组中元素的个数
    configASSERT( uxIndexToWaitOn < configTASK_NOTIFICATION_ARRAY_ENTRIES );

    vTaskSuspendAll();
    {
        taskENTER_CRITICAL();                                                   // 进入临界区
        {
            // 判断任务通知的状态是否不为等待接收通知状态,即判断任务是否无通知
            if( pxCurrentTCB->ucNotifyState[ uxIndexToWaitOn ] != taskNOTIFICATION_RECEIVED )
            {
                // 等待任务通知前将任务通知通知值的指定比特位清零
                pxCurrentTCB->ulNotifiedValue[ uxIndexToWaitOn ] &= ~ulBitsToClearOnEntry;

                // 设置任务通知的状态为等待通知状态
                pxCurrentTCB->ucNotifyState[ uxIndexToWaitOn ] = taskWAITING_NOTIFICATION;

                if( xTicksToWait > ( TickType_t ) 0 )                           // 判断是否允许阻塞等待任务通知
                {
                    xShouldBlock = pdTRUE;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        taskEXIT_CRITICAL();                                                    // 退出临界区

        if( xShouldBlock == pdTRUE )
        {
            traceTASK_NOTIFY_WAIT_BLOCK( uxIndexToWaitOn );                     // 用于调试
            prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );             // 将当前任务添加到阻塞态任务列表
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    xAlreadyYielded = xTaskResumeAll();

    if( ( xShouldBlock == pdTRUE ) && ( xAlreadyYielded == pdFALSE ) )
    {
        // 挂起 PendSv,准备进行任务切换,任务切换后,任务将进入阻塞状态
        taskYIELD_WITHIN_API();
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    taskENTER_CRITICAL();                                                       // 进入临界区
    {
        traceTASK_NOTIFY_WAIT( uxIndexToWaitOn );                               // 用于调试

        if( pulNotificationValue != NULL )                                      // 判断是否记录操作前的任务通知值
        {
            // 记录操作前的任务通知值
            *pulNotificationValue = pxCurrentTCB->ulNotifiedValue[ uxIndexToWaitOn ];
        }

        // 再次判断任务通知的状态是否不为等待接收通知状态,因为如果接收到通知,任务通知值的状态会被置为等待接收通知状态
        if( pxCurrentTCB->ucNotifyState[ uxIndexToWaitOn ] != taskNOTIFICATION_RECEIVED )
        {
            xReturn = pdFALSE;                                                  // 未接收到通知
        }
        else
        {
            // 接收到通知,在成功接收到通知后将任务通知通知值的指定比特位清零
            pxCurrentTCB->ulNotifiedValue[ uxIndexToWaitOn ] &= ~ulBitsToClearOnExit;
            xReturn = pdTRUE;
        }

        // 不论接收通知成功或者失败,都将任务通知的状态标记为未等待通知状态
        pxCurrentTCB->ucNotifyState[ uxIndexToWaitOn ] = taskNOT_WAITING_NOTIFICATION;
    }
    taskEXIT_CRITICAL();                                                        // 退出临界区

    traceRETURN_xTaskGenericNotifyWait( xReturn );

    return xReturn;
}

ulTaskNotifyTake() 函数:获取任务通知,可以设置在退出此函数的时候将任务通知值清零或者减一。
当任务通知用作二值信号量或者计数信号量的时候,使用此函数来获取信号量。

xTaskNotifyWait() 函数:获取任务通知,比 ulTaskNotifyTak() 更为复杂,可获取通知值和清除通知值的指定位。

当任务通知用作于信号量时,使用函数获取信号量:ulTaskNotifyTake()

当任务通知用作于事件标志组或队列时,使用此函数来获取: xTaskNotifyWait()

三、实验例程

3.1、任务通知模拟信号量

  main() 函数内容如下:

int main(void)
{
    HAL_Init();
    System_Clock_Init(8, 336, 2, 7);
    Delay_Init(168);

    UART_Init(&g_usart1_handle, USART1, 115200);
    Key_Init();

    freertos_demo();
  
    return 0;
}

  FreeRTOS 例程入口函数:

/**
 * @brief FreeRTOS的入口函数
 * 
 */
void freertos_demo(void)
{
    xTaskCreate((TaskFunction_t        ) start_task,                            // 任务函数
                (char *                ) "start_task",                          // 任务名
                (configSTACK_DEPTH_TYPE) START_TASK_STACK_SIZE,                 // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) START_TASK_PRIORITY,                   // 任务优先级
                (TaskHandle_t *        ) start_task_handle);                    // 任务句柄

    vTaskStartScheduler();                                                      // 开启任务调度器
}

  START_TASK 任务配置:

/**
 * START_TASK 任务配置
 * 包括: 任务优先级 任务栈大小 任务句柄 任务函数
 */
#define START_TASK_PRIORITY     1
#define START_TASK_STACK_SIZE   128

TaskHandle_t start_task_handle;

void start_task(void *pvParameters );

/**
 * @brief 开始任务的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();                                                       // 进入临界区,关闭中断

    xTaskCreate((TaskFunction_t        ) task1,                                 // 任务函数
                (char *                ) "task1",                               // 任务名
                (configSTACK_DEPTH_TYPE) TASK1_STACK_SIZE,                      // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) TASK1_PRIORITY,                        // 任务优先级
                (TaskHandle_t *        ) &task1_handle);                        // 任务句柄

    xTaskCreate((TaskFunction_t        ) task2,                                 // 任务函数
                (char *                ) "task2",                               // 任务名
                (configSTACK_DEPTH_TYPE) TASK2_STACK_SIZE,                      // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) TASK2_PRIORITY,                        // 任务优先级
                (TaskHandle_t *        ) &task2_handle);                        // 任务句柄

    vTaskDelete(NULL);                                                          // 删除任务自身

    taskEXIT_CRITICAL();                                                        // 退出临界区,重新开启中断
}

  TASK1 任务配置:

/**
 * TASK1 任务配置
 * 包括: 任务优先级 任务栈大小 任务句柄 任务函数
 */
#define TASK1_PRIORITY     2
#define TASK1_STACK_SIZE   128

TaskHandle_t task1_handle;

void task1(void *pvParameters);

/**
 * @brief 任务1的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void task1(void *pvParameters)
{
    uint8_t key = 0;
    BaseType_t result = 0;

    while (1)
    {
        key = Key_Scan(0);
        if (key == KEY1_PRESS)
        {
            result = xTaskNotifyGive(task2_handle);                             // 发送任务通知
            if (result == pdPASS)
            {
                printf("任务通知模拟二值信号量释放\r\n");
            }
        
        }
    
        vTaskDelay(10);
    }
}

  TASK2 任务配置:

#define EVENT_BIT1      (0x01 << 1)
#define EVENT_BIT2      (0x01 << 2)

/**
 * TASK2 任务配置
 * 包括: 任务优先级 任务栈大小 任务句柄 任务控制块  任务栈 任务函数
 */
#define TASK2_PRIORITY     3
#define TASK2_STACK_SIZE   128

TaskHandle_t task2_handle;

void task2(void *pvParameters);

void task2(void *pvParameters )
{
    uint32_t receive = 0;

    while (1)
    {
        receive = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);                      // 接收任务通知
        if (receive != 0)
        {
            printf("任务通知接收成功,模拟二值信号量获取\r\n");
        }
    }
}

  如果想要模拟计数型信号量,只需要在将接收任务通知 ulTaskNotifyTake() 函数的第一个参数从 pdTRUE 改为 pdFALSE 即可。修改 task2() 函数的内容如下:

/**
 * @brief 任务2的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void task2(void *pvParameters )
{
    uint32_t receive = 0;

    while (1)
    {
        receive = ulTaskNotifyTake(pdFALSE, portMAX_DELAY);                      // 接收任务通知
        if (receive != 0)
        {
            printf("任务通知接收成功,模拟计数型信号量获取,计数值:%ld\r\n", receive);
        }
        vTaskDelay(1000);
    }
}

3.2、任务通知模拟消息邮箱

  修改 task1() 函数:

/**
 * @brief 任务1的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void task1(void *pvParameters)
{
    uint8_t key = 0;
    BaseType_t result = 0;

    while (1)
    {
        taskENTER_CRITICAL();                                                   // 进入临界区,关闭中断
        key = Key_Scan(0);
        if (key != 0 && task2_handle != NULL)
        {
            result = xTaskNotify(task2_handle, key, eSetValueWithoutOverwrite);
            if (result == pdPASS)
            {
                printf("任务通知模拟消息邮箱发送,发送的键值为:%d\r\n", key);
            }
        }
        taskEXIT_CRITICAL();                                                    // 退出临界区,重新开启中断
    
        vTaskDelay(10);
    }
}

  修改 task2() 函数:

/**
 * @brief 任务2的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void task2(void *pvParameters )
{
    uint32_t notify_value = 0;
    BaseType_t result =0;

    while (1)
    {
        result = xTaskNotifyWait(0, 0xFFFFFFFF, &notify_value, portMAX_DELAY); // 接收任务通知
        if (result != 0)
        {
            printf("任务通知模拟消息邮箱接收,接收到的键值为:%ld\r\n", notify_value);
        }
        vTaskDelay(1000);
    }
}

3.3、任务通知模拟事件标志组

  修改 task1() 函数:

/**
 * @brief 任务1的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void task1(void *pvParameters)
{
    uint8_t key = 0;
    BaseType_t result = 0;

    while (1)
    {
        taskENTER_CRITICAL();                                                   // 进入临界区,关闭中断
        key = Key_Scan(0);
        if (key != 0 && task2_handle != NULL)
        {
            result = xTaskNotify(task2_handle, 0x01 << (key - 1), eSetBits);
            if (result == pdPASS)
            {
                printf("任务通知模拟事件标志组成功,更新bit%d位置1\r\n", key - 1);
            }
        }
        taskEXIT_CRITICAL();                                                    // 退出临界区,重新开启中断
    
        vTaskDelay(10);
    }
}

  修改 task2() 函数:

#define EVENT_BIT1      (0x01 << 1)
#define EVENT_BIT2      (0x01 << 2)

/**
 * @brief 任务2的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void task2(void *pvParameters )
{
    uint32_t notify_value = 0, event_bit = 0;
    BaseType_t result =0;

    while (1)
    {
        result = xTaskNotifyWait(0, 0xFFFFFFFF, &notify_value, portMAX_DELAY); // 接收任务通知
        if (result != 0)
        {
            if (notify_value & EVENT_BIT1)
            {
                event_bit |= EVENT_BIT1;
            }
            if (notify_value & EVENT_BIT2)
            {
                event_bit |= EVENT_BIT2;
            }
            if (event_bit == (EVENT_BIT1 | EVENT_BIT2))
            {
                printf("任务通知模拟事件标志组接收成功,值:%ld\r\n", event_bit);
                event_bit = 0;
            }
        }
        vTaskDelay(1000);
    }
}
posted @ 2024-03-21 19:28  星光映梦  阅读(672)  评论(0编辑  收藏  举报