13. 任务通知

任务通知在CMSIS-RTOS中并没相对应的API(CMSIS-RTOS中的线程标志为阉割版的任务通知),属于FreeRTOS的特性, 因此本章节将介绍纯正原生的FreeRTOS任务通知。

13.1. 任务通知的基本概念

FreeRTOS 从V8.2.0版本开始提供任务通知这个功能,每个任务都有一个32位的通知值,在大多数情况下, 任务通知可以替代二值信号量、计数信号量、事件组,也可以替代长度为1的队列(可以保存一个32位整数或指针值)。

相对于以前使用FreeRTOS内核通信的资源,必须创建队列、二进制信号量、计数信号量或事件组的情况, 使用任务通知显然更灵活。按照 FreeRTOS 官方的说法,使用任务通知比通过信号量等ICP通信方式解除阻塞的任务要快 45%,并且更加省RAM内存空间(使用GCC编译器,-o2优化级别),任务通知的使用无需创建队列。想要使用任务通知, 必须将FreeRTOSConfig.h中的宏定义configUSE_TASK_NOTIFICATIONS设置为1,其实FreeRTOS默认是为1的, 所以任务通知是默认使能的。

FreeRTOS 提供以下几种方式发送通知给任务:

  • 发送通知给任务,如果有通知未读,不覆盖通知值。

  • 发送通知给任务,直接覆盖通知值。

  • 发送通知给任务,设置通知值的一个或者多个位,可以当做事件组来使用。

  • 发送通知给任务,递增通知值,可以当做计数信号量使用。

通过对以上任务通知方式的合理使用,可以在一定场合下替代FreeRTOS的信号量,队列、事件组等。

当然,凡是都有利弊,不然的话FreeRTOS还要内核的IPC通信机制干嘛,消息通知虽然处理更快,RAM开销更小,但也有以下限制:

  • 只能有一个任务接收通知消息,因为必须指定接收通知的任务。。

  • 只有等待通知的任务可以被阻塞,发送通知的任务,在任何情况下都不会因为发送失败而进入阻塞态。

13.2. 任务通知的运作机制

顾名思义,任务通知是属于任务中附带的资源,所以在任务被创建的时候,任务通知也被初始化的, 而在分析队列和信号量的章节中,我们知道在使用队列、信号量前,必须先创建队列和信号量, 目的是为了创建队列数据结构。比如使用osMessageQueueNew()函数创建队列, 用osSemaphoreNew()函数创建信号量等等。再来看任务通知,由于任务通知的数据结构包含在任务控制块中, 只要任务存在,任务通知数据结构就已经创建完毕,可以直接使用,所以使用的时候很是方便。

任务通知可以在任务中向指定任务发送通知,也可以在中断中向指定任务发送通知, FreeRTOS的每个任务都有一个32位的通知值,任务控制块中的成员变量ulNotifiedValue就是这个通知值。 只有在任务中可以等待通知,而不允许在中断中等待通知。如果任务在等待的通知暂时无效, 任务会根据用户指定的阻塞超时时间进入阻塞状态,我们可以将等待通知的任务看作是消费者; 其他任务和中断可以向等待通知的任务发送通知,发送通知的任务和中断服务函数可以看作是生产者, 当其他任务或者中断向这个任务发送任务通知,任务获得通知以后,该任务就会从阻塞态中解除, 这与FreeRTOS中内核的其他通信机制一致。

13.3. 任务通知的数据结构

从前文我们知道,任务通知是任务控制块的资源,那它也算任务控制块中的成员变量,包含在任务控制块中, 我们将其拿出来看看,任务控制块中的任务通知成员变量如下代码所示

任务控制块中的任务通知成员变量
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
typedef struct tskTaskControlBlock {

    volatile StackType_t  *pxTopOfStack;

    #if ( portUSING_MPU_WRAPPERS == 1 )
    xMPU_SETTINGS xMPUSettings;
    #endif

    ListItem_t xStateListItem;
    ListItem_t xEventListItem;
    UBaseType_t uxPriority;
    StackType_t  *pxStack;
    char pcTaskName[ configMAX_TASK_NAME_LEN ];

    #if ( portSTACK_GROWTH > 0 )
    StackType_t  *pxEndOfStack;
    #endif

    #if ( portCRITICAL_NESTING_IN_TCB == 1 )
    UBaseType_t uxCriticalNesting;
    #endif

    #if ( configUSE_TRACE_FACILITY == 1 )
    UBaseType_t uxTCBNumber;
    UBaseType_t uxTaskNumber;
    #endif

    #if ( configUSE_MUTEXES == 1 )
    UBaseType_t uxBasePriority;
    UBaseType_t uxMutexesHeld;
    #endif

    #if ( configUSE_APPLICATION_TASK_TAG == 1 )
    TaskHookFunction_t pxTaskTag;
    #endif

    #if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
    void  *pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
    #endif

    #if( configGENERATE_RUN_TIME_STATS == 1 )
    uint32_t ulRunTimeCounter;
    #endif

    #if ( configUSE_NEWLIB_REENTRANT == 1 )
    struct _reent xNewLib_reent;
    #endif

    #if( configUSE_TASK_NOTIFICATIONS == 1 )
    volatileuint32_t ulNotifiedValue; (1)
    volatileuint8_t ucNotifyState; (2)
    #endif

    #if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
    uint8_t ucStaticallyAllocated;
    #endif

    #if( INCLUDE_xTaskAbortDelay == 1 )
    uint8_t ucDelayAborted;
    #endif
} tskTCB;
typedef tskTCB TCB_t;

代码清单1 (1) :任务通知的值,可以保存一个32位整数或指针值。

代码清单1 (2) :任务通知状态,用于标识任务是否在等待通知。

13.4. 任务通知的函数接口讲解

13.4.1. 发送任务通知函数xTaskGenericNotify()

我们先看一下发送通知API函数。这类函数比较多,有6个。但仔细分析会发现它们只能完成3种操作, 每种操作有两个API函数,分别为带中断保护版本和不带中断保护版本。 FreeRTOS将API细分为带中断保护版本和不带中断保护版本是为了节省中断服务程序处理时间,提升性能。 任务通知发送函数利用宏定义来进行扩展的,所有的函数都是一个宏定义, 在任务中发送任务通知的函数均是调用xTaskGenericNotify()函数进行发送通知, 下面来看看xTaskGenericNotify()的源码,具体如下代码所示。

xTaskGenericNotify()源码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#if( configUSE_TASK_NOTIFICATIONS == 1 )

BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify,  (1)
                                uint32_t ulValue,  (2)
                                eNotifyAction eAction,  (3)
                                uint32_t  *pulPreviousNotificationValue )  (4)
{

    TCB_t  * pxTCB;
    BaseType_t xReturn = pdPASS;
    uint8_t ucOriginalNotifyState;

    configASSERT( xTaskToNotify );
    pxTCB = ( TCB_t  * ) xTaskToNotify;

    taskENTER_CRITICAL();
    {

        if ( pulPreviousNotificationValue != NULL ) {
            /*回传未被更新的任务通知值*/
            *pulPreviousNotificationValue = pxTCB->ulNotifiedValue;  (5)
        }

        /* 获取任务通知的状态,看看任务是否在等待通知,方便在发送通知后恢复任务  */
        ucOriginalNotifyState = pxTCB->ucNotifyState;  (6)

        /* 不管状态是怎么样的,反正现在发送通知,任务就收到任务通知  */
        pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;  (7)

        /* 指定更新任务通知的方式  */
        switch ( eAction ) {  (8)

            /*通知值按位或上ulValue。
            使用这种方法可以某些场景下代替事件组,但执行速度更快。*/
            case eSetBits :  (9)
                pxTCB->ulNotifiedValue  |= ulValue;
            break;

                /* 被通知任务的通知值增加1,这种发送通知方式,参数ulValue未使用  */
            case eIncrement:  (10)
                ( pxTCB->ulNotifiedValue )++;
            break;

            /* 将被通知任务的通知值设置为ulValue。无论任务是否还有通知,
            都覆盖当前任务通知值。使用这种方法,
            可以在某些场景下代替xQueueoverwrite()函数,但执行速度更快。  */
            case eSetValueWithOverwrite:  (11)
                pxTCB->ulNotifiedValue = ulValue;
            break;

            /* 如果被通知任务当前没有通知,则被通知任务的通知值设置为ulValue;
            在某些场景下替代长度为1的xQueuesend(),但速度更快。  */
            case eSetValueWithoutOverwrite :  (12)
                if ( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED ) {
                    pxTCB->ulNotifiedValue = ulValue;
                } else {

                /*如果被通知任务还没取走上一个通知,本次发送通知,
                任务又接收到了一个通知,则这次通知值丢弃,
                在这种情况下,函数调用失败并返回pdFALSE。*/
                    xReturn = pdFAIL;  (13)
                    }
            break;

            /* 发送通知但不更新通知值,这意味着参数ulValue未使用。  */
            case eNoAction:  (14)
            break;
        }

        traceTASK_NOTIFY();

        /* 如果被通知任务由于等待任务通知而挂起  */
        if ( ucOriginalNotifyState == taskWAITING_NOTIFICATION ) { (15)

            /* 唤醒任务,将任务从阻塞列表中移除,添加到就绪列表中  */
            ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
            prvAddTaskToReadyList( pxTCB );

            // 刚刚唤醒的任务优先级比当前任务高
            if ( pxTCB->uxPriority > pxCurrentTCB->uxPriority ) { (16)

                //任务切换
                taskYIELD_IF_USING_PREEMPTION();
            } else {
                mtCOVERAGE_TEST_MARKER();
            }
        } else {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    taskEXIT_CRITICAL();
    return xReturn;
}
#endif代码清单2

代码清单2 (1) :被通知的任务句柄,指定通知的任务。

代码清单2 (2) :发送的通知值。

代码清单2 (3) :枚举类型,指明更新通知值的方式。

代码清单2 (4) :任务原本的通知值返回。

代码清单2 (5) :回传任务原本的任务通值,保存在pulPreviousNotificationValue中。

代码清单2 (6) :获取任务通知的状态,看看任务是否在等待通知,方便在发送通知后恢复任务。

代码清单2 (7) :不管该任务的通知状态是怎么样的,现在调用发送通知函数,任务通知状态就要设置为收到任务通知, 因为发送通知是肯定能被收到。

代码清单2 (8) :指定更新任务通知的方式。

代码清单2 (9) :通知值与原本的通知值按位或,使用这种方法可以某些场景下代替事件组,执行速度更快。

代码清单2 (10) :被通知任务的通知值增加1,这种发送通知方式,参数ulValue的值未使用, 在某些场景可以代替信号量通信,并且执行速度更快。

代码清单2 (11) :将被通知任务的通知值设置为ulValue,无论任务是否还有通知,都覆盖当前任务通知值。 这种方法是覆盖写入,使用这种方法,可以在某些场景下代替xQueueoverwrite()函数,执行速度更快。

代码清单2 (12) :如果被通知任务当前没有通知,则被通知任务的通知值设置为ulValue; 在某些场景下替代队列长度为1的xQueuesend(),并且执行速度更快。

代码清单2 (13) :如果被通知任务还没取走上一个通知,本次发送通知,任务又接收到了一个通知, 则这次通知值将被丢弃,在这种情况下,函数调用失败并返回pdFALSE。

代码清单2 (14) :发送通知但不更新通知值,这意味着参数ulValue未使用。

代码清单2 (15) :如果被通知的任务由于等待任务通知而挂起,系统将唤醒任务, 将任务从阻塞列表中移除,添加到就绪列表中。

代码清单2 (16) :如果刚刚唤醒的任务优先级比当前任务高,就进行一次任务切换。

xTaskGenericNotify()函数是一个通用的任务通知发送函数,在任务中发送通知的API函数, 如xTaskNotifyGive()、xTaskNotify()、xTaskNotifyAndQuery(),都是以xTaskGenericNotify()为原型的, 只不过指定的发生方式不同而已。

13.4.1.1. xTaskNotifyGive()

xTaskNotifyGive()是一个宏,宏展开是调用函数xTaskNotify( ( xTaskToNotify ), ( 0 ), eIncrement ),即向一个任务发送通知,并将对方的任务通知值加1。该函数可以作为二值信号量和计数信号量的一种轻量型的实现, 速度更快,在这种情况下对象任务在等待任务通知的时候应该是使用函数ulTaskNotifyTake()而不是xTaskNotifyWait()。 xTaskNotifyGive()不能在中断里面使用,而是使用具有中断保护功能的vTaskNotifyGiveFromISR()来代替。 该函数的原型如下

xTaskNotifyGive函数原型
1
#define xTaskNotifyGive(xTaskToNotify)  xTaskGenericNotify((xTaskToNotify), (0), eIncrement,NULL)

功能:于在任务中向指定任务发送任务通知,并更新对方的任务通知值(加1操作)。

参数

  • TaskToNotify:收通知的任务句柄,并让其自身的任务通知值加1

返回值

  • 总是返回pdPASS。

13.4.1.2. vTaskNotifyGiveFromISR()

vTaskNotifyGiveFromISR()是vTaskNotifyGive()的中断保护版本。 用于在中断中向指定任务发送任务通知,并更新对方的任务通知值(加1操作), 在某些场景中可以替代信号量操作,因为这两个通知都是不带有通知值的。该函数介绍如下。

GiveFromISR 函数原型
1
vTaskNotify GiveFromISR(TaskHandle_t xTaskToNotify,BaseType_t *pxH igherPriorityTaskWoken);

功能:于在中断中向一个任务发送任务通知,并更新对方的任务通知值(加1操作)。

参数

  • TaskToNotify:收通知的任务句柄,并让其自身的任务通知值加1

  • pxHigherPriorityTaskWoken:在使用之前必须先初始化为pdFALSE。当调用该函数发送一个任务通知时, 目标任务接收到通知后将从阻塞态变为就绪态,并且如果其优先级比当前运行的任务的优先级高, 那么*pxHigherPriorityTaskWoken会被设置为pdTRUE,然后在中断退出前执行一次上下文切换, 去执行刚刚被唤醒的中断优先级较高的任务。pxHigherPriorityTaskWoken是一个可选的参数可以设置为NULL。 收通知的任务句柄,并让其自身的任务通知值加1。

返回值

从上面的函数说明我们大概知道vTaskNotifyGiveFromISR()函数作用,每次调用该函数都会增加任务的通知值, 任务通过接收函数返回值是否大于零,判断是否获取到了通知,任务通知值初始化为0, (如果与信号量做对比)则对应为信号量无效。当中断调用vTaskNotifyGiveFromISR()通知函数给任务的时候, 任务的通知值增加,使其大于零,使其表示的通知值变为有效,任务获取有效的通知值将会被恢复。 那么该函数是怎么实现的呢?下面一起来看看vTaskNotifyGiveFromISR()函数的源码,具体如以下代码所示。

vTaskNotifyGiveFromISR()源码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#if( configUSE_TASK_NOTIFICATIONS == 1 )
void vTaskNotifyGiveFromISR( TaskHandle_t xTaskToNotify,
BaseType_t *pxHigherPriorityTaskWoken )
{
    TCB_t * pxTCB;
    uint8_t ucOriginalNotifyState;
    UBaseType_t uxSavedInterruptStatus;
    configASSERT( xTaskToNotify );

        portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
        pxTCB = ( TCB_t * ) xTaskToNotify;

    //进入中断
    uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
    {
        //保存任务通知的原始状态,
        //看看任务是否在等待通知,方便在发送通知后恢复任务
        ucOriginalNotifyState = pxTCB->ucNotifyState;  (1)

        /* 不管状态是怎么样的,反正现在发送通知,任务就收到任务通知 */
        pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;  (2)

        /* 通知值自加,类似于信号量的释放 */
        ( pxTCB->ulNotifiedValue )++;  (3)
        traceTASK_NOTIFY_GIVE_FROM_ISR();

        /* 如果任务在阻塞等待通知 */
        if ( ucOriginalNotifyState == taskWAITING_NOTIFICATION ) { (4)

            //如果任务调度器运行中
            if ( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) {

                /* 唤醒任务,将任务从阻塞列表中移除,添加到就绪列表中 */
                ( void ) uxListRemove( &( pxTCB->xStateListItem ) );   (5)

                prvAddTaskToReadyList( pxTCB );
            } else {

                /* 调度器处于挂起状态,中断依然正常发生,但是不能直接操作就绪列表
                将任务加入到就绪挂起列表,任务调度恢复后会移动到就绪列表 */
                vListInsertEnd( &( xPendingReadyList ),
                &( pxTCB->xEventListItem ) );   (6)
            }

            /* 如果刚刚唤醒的任务优先级比当前任务高,
            则设置上下文切换标识,等退出函数后手动切换上下文,
            或者在系统节拍中断服务程序中自动切换上下文 */
            if ( pxTCB->uxPriority > pxCurrentTCB->uxPriority ) { (7)

                /* 设置返回参数,表示需要任务切换,在退出中断前进行任务切换 */
                if ( pxHigherPriorityTaskWoken != NULL ) {
                *pxHigherPriorityTaskWoken = pdTRUE;  (8)
                } else {
                    /* 设置自动切换标志 */
                    xYieldPending = pdTRUE;  (9)
                }
            } else {
            mtCOVERAGE_TEST_MARKER();
            }
        }
    }
    portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
    }
#endif

代码清单3 (1) :保存任务通知的原始状态,看看任务是否处于等待通知的阻塞态,方便在中断发送通知完成后恢复任务。

代码清单3 (2) :不管状态是怎么样的,反正现在发送通知,任务就收到任务通知。

代码清单3 (3) :通知值自加,类似于信号量的释放操作。

代码清单3 (4) :如果任务在阻塞等待通知,并且系统调度器处于运行状态。

代码清单3 (5) :唤醒任务,将任务从阻塞列表中移除,添加到就绪列表中。

代码清单3 (6) :调度器处于挂起状态,中断依然正常发生,但是不能直接操作就绪列表,将任务加入到就绪挂起列表, 任务调度恢复后会移动到就绪列表中。

代码清单3 (7) :如果刚刚唤醒的任务优先级比当前任务高,则设置上下文切换标识, 等退出函数后手动切换上下文,或者在系统节拍中断服务程序中自动切换上下文

代码清单3 (8) :设置返回参数,表示需要任务切换,在退出中断前进行任务切换。

代码清单3 (9) :否则就设置自动切换标志。

13.4.1.3. xTaskNotify()

FreeRTOS每个任务都有一个32位的变量用于实现任务通知,在任务创建的时候初始化为0。 这个32位的通知值在任务控制块TCB里面定义,具体见代码清单6。xTaskNotify()用于在任务中直接向另外一个任务发送一个事件, 接收到该任务通知的任务有可能解锁。如果你想使用任务通知来实现二值信号量和计数信号量, 那么应该使用更加简单的函数xTaskNotifyGive(),而不是使用xTaskNotify(), xTaskNotify()函数在发送任务通知的时候会指定一个通知值,并且用户可以指定通知值发送的方式。

注意:该函数不能在中断里面使用,而是使用具体中断保护功能的版本函数 xTaskNotifyFromISR()。 xTaskNotify()函数的具体说明见表3,。

函数原型如下

GiveFromISR 函数原型
1
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify,  uint32_t ulValue,eNotifyAction eAction );

功能:指定的任务发送一个任务通知,带有通知值并且用户可以指定通知值的发送方式。

参数

  • TaskToNotify:要接收通知的任务句柄。

  • ulValue:用于更新接收任务通知的任务通知值,具体如何更新由形参eAction决定。

  • eAction :任务通知值更新方式,具体如下

返回值

  • 参数eAction为eSetValueWithoutOverwrite时,如果被通知任务还没取走上一个通知,又接收到了一个通知, 则这次通知值 未能更新并返回pdFALSE,而其他情况均返回pdPASS。

eAction取值

  • eNoAction:对象任务接收任务通知,但是任务自身的任务通知值不更新,即形参ulValue没有用。

  • eSetBits :对象任务接收任务通知,同时任务自身的任务通知值与ulValue按位或。如果ulValue设置为0x01, 那么任务的通知值的位0将被置为1。同样的如果ulValue设置为0x04,那么任务的通知值的位2将被置为1。 在这种方式下,任务通知可以看成是事件标志的一种轻量型的实现,速度更快。

  • eIncrement :对象任务接收任务通知,任务自身的任务通知值加1,即形参ulValue没有用。 这个时候调用xTaskNotify()等同于调用xTaskNotifyGive()。

  • eSetValueWithOverwrite:对象任务接收任务通知,且任务自身的任务通知值会无条件的被设置为ulValue。 在这种方式下,任务通知可以看成是函数 xQueueOverwrite()的一种轻量型的实现,速度更快。

  • eSetValueWithoutOverwrite:对象任务接收任务通知,且对象任务没有通知值,那么通知值就会被设置为ulValue。 对象任务接收任务通知,但是上一次接收到的通知值并没有取走,那么本次的通知值将不会更新, 同时函数返回pdFALSE。在这种方式下,任务通知可以看成是函数xQueueSend()应用在队列深度为1的队列上的一种轻量型实现,速度更快。

13.4.1.4. xTaskNotifyFromISR()

xTaskNotifyFromISR()是xTaskNotify()的中断保护版本, 真正起作用的函数是中断发送任务通知通用函数xTaskGenericNotifyFromISR(), 而xTaskNotifyFromISR()是一个宏定义,具体代码如下,用于在中断中向指定的任务发送一个任务通知, 该任务通知是带有通知值并且用户可以指定通知的发送方式,不返回上一个任务在的通知值。

任务通知在任务控制块中的定义
1
2
3
4
5
6
7
8
9
#define xTaskNotifyFromISR( xTaskToNotify,
                            ulValue,
                            eAction,
                            pxHigherPriorityTaskWoken )
        xTaskGenericNotifyFromISR( ( xTaskToNotify ),
                                    ( ulValue ),
                                    ( eAction ),
                                    NULL,
                                    ( pxHigherPriorityTaskWoken ))

xTaskNotifyFromISR()函数说明

GiveFromISR 函数原型
1
2
3
4
BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
                                uint32_t ulValue,
                                eNotifyAction eAction,
                                BaseType_t *pxHigherPriorityTaskWoken );

功能:中断中向指定的任务发送一个任务通知。

参数

  • TaskToNotify:定接收通知的任务句柄。

  • ulValue:用于更新接收任务通知的任务通知值,具体如何更新由形参eAction决定。

  • eAction:任务通知值的状态,具体见表4。

返回值

  • 参数eAction为eSetValueWithoutOverwrite时,如果被通知任务还没取走上一个通知, 又接收到了一个通知,则这次通知值未能更新并返回pdFALSE,其他情况均返回pdPASS。

13.4.1.5. 中断中发送任务通知通用函数xTaskGenericNotifyFromISR()

xTaskGenericNotifyFromISR()是一个在中断中发送任务通知的通用函数, xTaskNotifyFromISR()、xTaskNotifyAndQueryFromISR()等函数都是以其为基础, 采用宏定义的方式实现。xTaskGenericNotifyFromISR()的源码具体如以下代码所示。

xTaskGenericNotifyFromISR()源码
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#if( configUSE_TASK_NOTIFICATIONS == 1 )
BaseType_t xTaskGenericNotifyFromISR( TaskHandle_t xTaskToNotify,   (1)
                                        uint32_t ulValue,  (2)
                                        eNotifyAction eAction,  (3)
                                        uint32_t *pulPreviousNotificationValue,   (4)
                                        BaseType_t *pxHigherPriorityTaskWoken )   (5)
{
    TCB_t * pxTCB;
    uint8_t ucOriginalNotifyState;
    BaseType_t xReturn = pdPASS;
    UBaseType_t uxSavedInterruptStatus;

    configASSERT( xTaskToNotify );
    portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
    pxTCB = ( TCB_t * ) xTaskToNotify;

    /* 进入中断临界区 */
    uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();  (6)
    {
        if ( pulPreviousNotificationValue != NULL ) {
            /*回传未被更新的任务通知值*/
            *pulPreviousNotificationValue = pxTCB->ulNotifiedValue;   (7)
        }

        //保存任务通知的原始状态,
        //看看任务是否在等待通知,方便在发送通知后恢复任务
        ucOriginalNotifyState = pxTCB->ucNotifyState;  (8)

        /* 不管状态是怎么样的,反正现在发送通知,任务就收到任务通知 */
        pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;  (9)

        /* 指定更新任务通知的方式 */
        switch ( eAction ) {  (10)

            /*通知值按位或上ulValue。
            使用这种方法可以某些场景下代替事件组,但执行速度更快。*/
            case eSetBits :  (11)
                pxTCB->ulNotifiedValue  |= ulValue;
            break;

            /* 被通知任务的通知值增加1,这种发送通知方式,参数ulValue未使用
            在某些场景下可以代替信号量,执行速度更快 */
            case eIncrement:  (12)
                ( pxTCB->ulNotifiedValue )++;
            break;

            /* 将被通知任务的通知值设置为ulValue。无论任务是否还有通知,
            都覆盖当前任务通知值。使用这种方法,
            可以在某些场景下代替xQueueoverwrite()函数,但执行速度更快。 */
            case eSetValueWithOverwrite:  (13)
                pxTCB->ulNotifiedValue = ulValue;
            break;

            //采用不覆盖发送任务通知的方式
            case eSetValueWithoutOverwrite :  (14)
                /* 如果被通知任务当前没有通知,则被通知任务的通知值设置为ulValue;
                在某些场景下替代长度为1的xQueuesend(),但速度更快。 */
                if ( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED ) {
                    pxTCB->ulNotifiedValue = ulValue;
                } else {
                    /*如果被通知任务还没取走上一个通知,本次发送通知,
                    任务又接收到了一个通知,则这次通知值丢弃,
                    在这种情况下,函数调用失败并返回pdFALSE。*/
                    xReturn = pdFAIL;  (15)
                }
            break;

            case eNoAction :
            /* 退出 */
            break;
        }
        traceTASK_NOTIFY_FROM_ISR();
        /* 如果任务在阻塞等待通知*/
        if ( ucOriginalNotifyState == taskWAITING_NOTIFICATION ) { (16)

            //如果任务调度器运行中,表示可用操作就绪级列表
            if ( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) {

                /* 唤醒任务,将任务从阻塞列表中移除,添加到就绪列表中 */
                ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
                prvAddTaskToReadyList( pxTCB );  (17)
            } else {

                /* 调度器处于挂起状态,中断依然正常发生,但是不能直接操作就绪列表
                将任务加入到就绪挂起列表,任务调度恢复后会移动到就绪列表 */
                vListInsertEnd( &( xPendingReadyList ),
                &( pxTCB->xEventListItem ) );  (18)
            }

            /* 如果刚刚唤醒的任务优先级比当前任务高,
            则设置上下文切换标识,等退出函数后手动切换上下文,
            或者自动切换上下文 */
            if ( pxTCB->uxPriority > pxCurrentTCB->uxPriority ) { (19)
                if ( pxHigherPriorityTaskWoken != NULL ) {

                    /* 设置返回参数,表示需要任务切换,在退出中断前进行任务切换 */
                    *pxHigherPriorityTaskWoken = pdTRUE;  (20)
                } else {

                    /*设置自动切换标志,等高优先级任务释放CPU使用权 */
                    xYieldPending = pdTRUE;  (21)
                }
            } else {

                mtCOVERAGE_TEST_MARKER();
            }
        }
    }
    /* 离开中断临界区 */
    portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );   (22)
    return xReturn;
    }
    #endif

代码清单4 (1) :指定接收通知的任务句柄。

代码清单4 (2) :用于更新接收任务通知值,具体如何更新由形参eAction决定。

代码清单4 (3) :任务通知值更新方式。

代码清单4 (4) :用于保存上一个任务通知值。

代码清单4 (5)*pxHigherPriorityTaskWoken在使用之前必须先初始化为pdFALSE。 当调用该函数发送一个任务通知时,目标任务接收到通知后将从阻塞态变为就绪态, 并且如果其优先级比当前运行的任务的优先级高,那么*pxHigherPriorityTaskWoken会被设置为pdTRUE, 然后在中断退出前执行一次上下文切换,去执行刚刚被唤醒的中断优先级较高的任务。 pxHigherPriorityTaskWoken是一个可选的参数可以设置为NULL。

代码清单4 (6) :进入中断临界区。

代码清单4 (7) :如果pulPreviousNotificationValue参数不为空,就需要返回上一次的任务通知值。

代码清单4 (8) :保存任务通知的原始状态,看看任务是否在等待通知,方便在发送通知后恢复任务。

代码清单4 (9) :不管当前任务通知状态是怎么样的,现在调用发送通知函数。 任务通知肯定是发送到指定任务,那么任务通知的状态就设置为收到任务通知。

代码清单4 (10) :指定更新任务通知的方式。

代码清单4 (11) :通知值与原本的通知值按位或,使用这种方法可以某些场景下代替事件组,执行速度更快。

代码清单4 (12) :被通知任务的通知值增加1,这种发送通知方式,参数ulValue的值未使用, 在某些场景可以代替信号量通信,并且执行速度更快。

代码清单4 (13) :将被通知任务的通知值设置为ulValue,无论任务是否还有通知, 都覆盖当前任务通知值。这种方法是覆盖写入,使用这种方法,可以在某些场景下代替xQueueoverwrite()函数,执行速度更快。

代码清单4 (14) :采用不覆盖发送通知方式,如果被通知任务当前没有通知, 则被通知任务的通知值设置为ulValue;在某些场景下替代队列长度为1的xQueuesend(),并且执行速度更快。

代码清单4 (15) :如果被通知任务还没取走上一个通知,本次发送通知, 任务又接收到了一个通知,则这次通知值将被丢弃,在这种情况下,函数调用失败并返回pdFALSE。

代码清单4 (16) :如果任务在阻塞等待通知。

代码清单4 (17) :如果任务调度器在运行中,表示可用操作就绪级列表。那么系统将唤醒任务, 将任务从阻塞列表中移除,添加到就绪列表中

代码清单4 (18) :如果调度器处于挂起状态,中断依然正常发生, 但是不能直接操作就绪列表,系统会将任务加入到就绪挂起列表,任务调度恢复后会将在该列表的任务移动到就绪列表中。

代码清单4 (19) :如果刚刚唤醒的任务优先级比当前任务高,则设置上下文切换标识, 等退出函数后手动切换上下文,或者按照任务优先级自动切换上下文。

代码清单4 (20) :设置返回参数,表示需要任务切换,在退出中断前进行任务切换。

代码清单4 (21) :设置自动切换标志,等高优先级任务释放CPU使用权。

代码清单4 (22) :离开中断临界区

13.4.1.6. xTaskNotifyAndQuery()

xTaskNotifyAndQuery()与xTaskNotify()很像, 都是调用通用的任务通知发送函数xTaskGenericNotify()来实现通知的发送, 不同的是多了一个附加的参数pulPreviousNotifyValue用于回传接收任务的上一个通知值。 xTaskNotifyAndQuery()函数不能用在中断中,而是必须使用带中断保护功能的xTaskNotifyAndQuery()FromISR来代替。

xTaskNotifyAndQuery()原型
1
2
3
4
5
6
7
8
    #define xTaskNotifyAndQuery( xTaskToNotify,
                            ulValue,
                            eAction,
                            pulPreviousNotifyValue )
    xTaskGenericNotify( ( xTaskToNotify ),
                    ( ulValue ),
                    ( eAction ),
                    ( pulPreviousNotifyValue ) )

功能

  • 指定的任务发送一个任务通知,并返回对象任务的上一个通知值。

参数

  • TaskToNotify:要接收通知的任务句柄。

  • ulValue:用于更新接收任务通知的任务通知值,具体如何更新由形参eAction决定。

  • eAction:任务通知值更新方式,具体同GiveFromISR函数

  • pulPreviousNotifyValue:对象任务的上一个任务通知值,如果为NULL,则不需要回传,这个时候就等价于函数xTaskNotify()。

返回值

  • 参数eAction为eSetValueWithoutOverwrite时,如果被通知任务还没取走上一个通知, 又接收到了一个通知,则这次通知值未能更新并返回pdFALSE,其他情况均返回pdPASS。

13.4.1.7. xTaskNotifyAndQueryFromISR()

xTaskNotifyAndQueryFromISR()是xTaskNotifyAndQuery()的中断版本, 用于向指定的任务发送一个任务通知,并返回对象任务的上一个通知值,该函数也是一个宏定义, 真正实现发送通知的是xTaskGenericNotifyFromISR()。

xTaskNotifyAndQueryFromISR 函数原型
1
2
3
4
5
6
BaseType_t xTaskNotifyAndQueryFromISR(TaskHandle_t xTaskToNotify,
                                        uint32_t ulValue,
                                        eNotifyAction eAction,
                                        uint32_t
                                        *pulPreviousNotifyValue,
                                        BaseType_t *p xHigherPriorityTaskWoken );

功能:中断中向指定的任务发送一个任务通知,并返回对象任务的上一个通知值。

参数

  • TaskToNotify:要接收通知的任务句柄。

  • ulValue:用于更新接收任务通知的任务通知值,具体如何更新由形参eAction决定。

  • eAction:任务通知值的状态,具体同GiveFromISR函数。

  • pulPreviousNotifyValue:对象任务的上一个任务通知值。如 果为NULL,则不需要回传。

  • pxHigherPriorityTaskWoken:*pxHigherPriorityTaskWoken在使用之前必须先初始化为pdFALSE。 当调用该函数发送一个任务通知时,目标任务接收到通知后将从阻塞态变为就绪态, 并且如果其优先级比当前运行的任务的 优先级高,那么*pxHigherPriorityTaskWoken会被设置为pdTRUE, 然后在中断退出前执行一次上下文切换,去执行刚刚被唤醒的中断优先级较高的任务。pxHigherPriorityTaskWoken是 一个可选的参数可以设置为NULL.

返回值

  • eAction为eSetValueWithoutOverwrite时,如果被通知任务还没取走上一个通知,又接收到了一个通知, 则这次通知值未能更新并返回pdFALSE其他情况均返回pdPASS。

13.4.2. 获取任务通知函数

既然FreeRTOS中发送任务的函数有那么多个,那么任务怎么获取到通知呢?我们说了, 任务通知在某些场景可以替代信号量、消息队列、事件等。获取任务通知函数只能用在任务中, 没有带中断保护版本,因此只有两个API函数:ulTaskNotifyTake()和xTaskNotifyWait()。 前者是为代替二值信号量和计数信号量而专门设计的,它和发送通知API函数xTaskNotifyGive()、 vTaskNotifyGiveFromISR()配合使用;后者是全功能版的等待通知,可以根据不同的参数实现轻量级二值信号量、 计数信号量、事件组和长度为1的队列。

所有的获取任务通知API函数都带有指定阻塞超时时间参数,当任务因为等待通知而进入阻塞时, 用来指定任务的阻塞时间,这些超时机制与FreeRTOS的消息队列、信号量、事件等的超时机制一致。

13.4.2.1. ulTaskNotifyTake()

ulTaskNotifyTake()作为二值信号量和计数信号量的一种轻量级实现,速度更快。 如果FreeRTOS中使用函数xSemaphoreTake() 来获取信号量,这个时候则可以试试使用函数ulTaskNotifyTake()来代替。

对于这个函数,任务通知值为0,对应信号量无效,如果任务设置了阻塞等待,任务被阻塞挂起。 当其他任务或中断发送了通知值使其不为0后,通知变为有效,等待通知的任务将获取到通知, 并且在退出时候根据用户传递的第一个参数xClearCountOnExit选择清零通知值或者执行减一操作。

xTaskNotifyTake()在退出的时候处理任务的通知值的时候有两种方法,一种是在函数退出时将通知值清零, 这种方法适用于实现二值信号量;另外一种是在函数退出时将通知值减1,这种方法适用于实现计数信号量。

当一个任务使用其自身的任务通知值作为二值信号量或者计数信号量时, 其他任务应该使用函数xTaskNotifyGive()或者xTaskNotify( (xTaskToNotify), (0), eIncrement)来向其发送信号量。 如果是在中断中,则应该使用他们的中断版本函数。

GiveFromISR 函数原型
1
uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait);

功能:用于获取一个任务通知,获取二值信号量、计数信号量类型的任务通知。

参数

  • ClearCountOnExit:设置为pdFALSE时,函数xTaskNotifyTake()退出前,将任务的通知值减1, 可以用来实现计数信号量;设置为pdTRUE时,函数xTaskNotifyTake()退出前,将任务通知值清零 可以用来实现二值信号量。

  • xTicksToWait:超时时间,单位为系统节拍周期。宏pdMS_TO_TICKS用于将毫秒转化为系统节拍数。

返回值

  • 返回任务的当前通知值,在其减1或者清0之前。

下面一起来看看ulTaskNotifyTake()源码的实现过程,其实也是很简单的,具体如下代码所示。

ulTaskNotifyTake()源码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#if( configUSE_TASK_NOTIFICATIONS == 1 )
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit,
                            TickType_t xTicksToWait )
{
    uint32_t ulReturn;
    taskENTER_CRITICAL(); //进入中断临界区
    {
        // 如果通知值为 0 ,阻塞任务
        // 默认初始化通知值为 0,说明没有未读通知
        if ( pxCurrentTCB->ulNotifiedValue == 0UL ) {  (1)
            /* 标记任务状态:等待消息通知 */
            pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;

            //用户指定超时时间了,那就进入等待状态
            if ( xTicksToWait > ( TickType_t ) 0 ) {  (2)
                //根据用户指定超时时间将任务添加到延时列表
                prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
                traceTASK_NOTIFY_TAKE_BLOCK();
                // 切换任务
                portYIELD_WITHIN_API();
            } else {
                mtCOVERAGE_TEST_MARKER();
            }
        } else {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    taskEXIT_CRITICAL();

    // 到这里说明其他任务或中断向这个任务发送了通知,或者任务阻塞超时,现在继续处理
    taskENTER_CRITICAL();  (3)
    {

        // 获取任务通知值
        traceTASK_NOTIFY_TAKE();
        ulReturn = pxCurrentTCB->ulNotifiedValue;

        // 看看任务通知是否有效,有效则返回
        if ( ulReturn != 0UL ) {  (4)

            //是否需要清除通知
            if ( xClearCountOnExit != pdFALSE ) {  (5)
                pxCurrentTCB->ulNotifiedValue = 0UL;
            } else {
                // 不清除,就减一
                pxCurrentTCB->ulNotifiedValue = ulReturn - 1;  (6)
            }
        } else {
            mtCOVERAGE_TEST_MARKER();
        }
        //恢复任务通知状态变量
        pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;   (7)
    }
    taskEXIT_CRITICAL();
    return ulReturn;
}
#endif

代码清单5 (1) :进入临界区,先看看任务通知值是否有效,有效才能获取,无效则根据指定超时时间等待, 标记一下任务状态,表示任务在等待通知。任务通知在任务初始化的时候是默认为无效的。

代码清单5 (2) :用户指定超时时间了,那就进入等待状态,根据用户指定超时时间将任务添加到延时列表, 然后切换任务,触发PendSV中断,等到退出临界区时立即执行任务切换。

代码清单5 (3) :进入临界区,程序能执行到这里说明其他任务或中断向这个任务发送了一个任务通知, 或者任务本身的阻塞超时时间到了,现在无论有没有任务通知都要继续处理。

代码清单5 (4) :先获取一下任务通知值,因为现在并不知道任务通知是否有效, 所以还是要再判断一下任务通知是否有效,有效则返回通知值,无效则退出,并且返回0,代表无效的任务通知值。

代码清单5 (5) :如果任务通知有效,那在函数前判断一下是否要清除任务通知, 根据用户指定的参数xClearCountOnExit处理,设置为pdFALSE时,函数xTaskNotifyTake()退出前, 将任务的通知值减1,可以用来实现计数信号量;设置为pdTRUE时,函数xTaskNotifyTake()退出前, 将任务通知值清零,可以用来实现二值信号量。

代码清单5 (6) :不清除,那任务通知值就减1。

代码清单5 (7) :恢复任务通知状态。

与获取二值信号量和获取计数信号量的函数相比,ulTaskNotifyTake()函数少了很多调用子函数开销、 少了很多判断、少了事件列表处理、少了队列上锁与解锁处理等等,因此ulTaskNotifyTake()函数相对效率很高。

13.4.2.2. xTaskNotifyWait()

xTaskNotifyWait()函数用于实现全功能版的等待任务通知,根据用户指定的参数的不同, 可以灵活的用于实现轻量级的消息队列队列、二值信号量、计数信号量和事件组功能,并带有超时等待。

GiveFromISR 函数原型
1
2
3
4
5
BaseType_txTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
                            uint32_t ulBitsToClearOnExit,
                            uint32_t *pulNotificationValue,
                            TickType_t
                            xTicksToWait );

功能:等待一个任务通知,并带有超时等待

参数

  • ulBitsToClearOnEntry:lBitsToClearOnEntry表示在使用通知之前,将任务通知值的哪些位清0, 实现过程就是将任务的通知值与参数ulBitsToClearOnEntry的按位取反值按位与操作。 | 如果ulBitsToClearOnEntry设置为0x01,那么在函数进入前,任务通知值的位1会被清0,其他位保持不变。 如果ulBitsToClearOnEntry设置为 0xFFFFFFFF(ULONG_MAX),那么在进入函数前任务通知值的所有位都会被清0, 表示清零任务通知值。

  • ulBitsToClearOnExit:ulBitsToClearOnExit表示在函数xTaskNotifyWait()退出前,决定任务接收到的通知值的哪些位会被清0, 实现过程就是将任务的通知值与参数ulBitsToClearOnExit的按位取反值按位与操作。在清0前, 接收到的任务通知值会先被保存到形参*pulNotificationValue中。 如果ulBitsToClearOnExit设置为0x03,那么在函数退出前,接收到的任务通知值的位0和位1会被清0, 其他位保持不变。如果ulBitsTo ClearOnExi设置为 0xFFFFF FFF(ULONG_MAX), 那么在退出函数前接收到的任务通知值的所有位都会被清0,表示退出时清零任务通知值。

  • pulNotificationValue:用于保存接收到的任务通知值。如果接收到的任务通知不需要使用,则设置为NULL即可。 这个通知值在参数ulBitsToClearOnExit起作用前将通知值拷贝到*pulNotificationValue中。

  • xTicksToWait:等待超时时间,单位为系统节拍周期。宏pdMS_TO_TICKS用于将单位毫秒转化为系统节拍数。

返回值

  • 如果获取任务通知成功则返回pdT RUE,失败则返回pdFALSE。

代码清单6 xTaskNotifyWait()源码

xTaskNotifyWait()源码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#if( configUSE_TASK_NOTIFICATIONS == 1 )
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
                            uint32_t ulBitsToClearOnExit,
                            uint32_t *pulNotificationValue,
                            TickType_t xTicksToWait )
{
    BaseType_t xReturn;

    /* 进入临界段 */
    taskENTER_CRITICAL();  (1)
    {
        /* 只有任务当前没有收到任务通知,才会将任务阻塞 */  (2)

        if ( pxCurrentTCB->ucNotifyState != taskNOTIFICATION_RECEIVED ) {

            /* 使用任务通知值之前,根据用户指定参数ulBitsToClearOnEntryClear
            将通知值的某些或全部位清零 */
            pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnEntry;   (3)

            /* 设置任务状态标识:等待通知 */
            pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;

            /* 挂起任务等待通知或者进入阻塞态 */
            if ( xTicksToWait > ( TickType_t ) 0 ) {  (4)

                /* 根据用户指定超时时间将任务添加到延时列表 */
                prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
                traceTASK_NOTIFY_WAIT_BLOCK();
                /* 任务切换 */
                portYIELD_WITHIN_API();  (5)
            } else {
                mtCOVERAGE_TEST_MARKER();
            }
        } else {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    taskEXIT_CRITICAL();

    /*程序能执行到这里说明其他任务或中断向这个任务发送了通知或者任务阻塞超时,
    现在继续处理*/
    taskENTER_CRITICAL();  (6)

    {
        traceTASK_NOTIFY_WAIT();
        if ( pulNotificationValue != NULL ) {  (7)
            /* 返回当前通知值,通过指针参数传递 */
            *pulNotificationValue = pxCurrentTCB->ulNotifiedValue;
        }
        /* 判断是否是因为任务阻塞超时,因为如果有
        任务发送了通知的话,任务通知状态会被改变 */
        if ( pxCurrentTCB->ucNotifyState == taskWAITING_NOTIFICATION ) {
            /* 没有收到任务通知,是阻塞超时 */
            xReturn = pdFALSE;  (8)
        } else {
            /* 收到任务值,先将参数ulBitsToClearOnExit取反后与通知值位做按位与运算
            在退出函数前,将通知值的某些或者全部位清零.*/
            pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnExit;
            xReturn = pdTRUE;  (9)
        }
        //重新设置任务通知状态
        pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;   (10)
    }
    taskEXIT_CRITICAL();
    return xReturn;
}
#endif

代码清单6 (1) :进入临界段。因为下面的操作可能会对任务的状态列表进行操作,系统不希望被打扰。

代码清单6 (2) :只有任务当前没有收到任务通知,才会将任务阻塞,先看看任务通知是否有效,无效的话就将任务阻塞。

代码清单6 (3) :使用任务通知值之前,根据用户指定参数ulBitsToClearOnEntryClear将通知值的某些或全部位清零。 然后设置任务状态标识,表示当前任务在等待通知。

代码清单6 (4) :如果用户指定了阻塞超时时间,那么系统将挂起任务等待通知或进入阻塞态, 根据用户指定超时时间将任务添加到延时列表。

代码清单6 (5) :然后进行任务切换。触发PendSV悬挂中断,在退出临界区的时候,进行任务切换。

代码清单6 (6) :程序能执行到这里说明其他任务或中断向这个任务发送了通知或者任务阻塞超时, 任务从阻塞态变成运行态,现在继续处理。

代码清单6 (7) :返回当前通知值,通过指针参数传递。

代码清单6 (8) :判断是否是因为任务阻塞超时才退出阻塞的, 还是因为其他任务或中断发送了任务通知导致任务被恢复,为什么简单判断一下任务状态就知道? 因为如果有任务发送了通知的话,任务通知状态会被改变,而阻塞退出的时候,任务通知状态还是原来的, 现在看来是阻塞超时时间到来才恢复运行的,并没有接收到如何通知,那么返回pdFALSE。

代码清单6 (9) :收到任务值,先将参数 ulBitsToClearOnExit 取反后与通知值位做按位与运算, 在退出函数前,将通知值的某些或者全部位清零。

代码清单6 (10) :重新设置任务通知状态。

纵观整个任务通知的实现,我们不难发现它比消息队列、信号量、事件的实现方式要简单很多。 它可以实现轻量级的消息队列、二值信号量、计数信号量和事件组,并且使用更方便、更节省RAM、更高效, xTaskNotifyWait()函数的使用很简单,具体见代码清单17。

至此,任务通知的函数基本讲解完成,但是我们有必要说明一下,任务通知并不能完全代替队列、 二值信号量、计数信号量和事件组,使用的时候需要用户按需处理,此外,再提一次任务通知的局限性:

  • 只能有一个任务接收通知事件。

  • 接收通知的任务可以因为等待通知而进入阻塞状态,但是发送通知的任务即便不能立即完成发送通知,也不能进入阻塞状态。

13.5. 任务通知实验

任务通知实验只需要创建任务即可,相信大家对于任务的创建已经了然于心,接下去我们将直接看实现代码。

13.5.1. 任务通知代替消息队列

任务通知代替消息队列是在FreeRTOS中创建了三个任务,其中两个任务是用于接收任务通知,另一个任务发送任务通知。 三个任务独立运行,发送消息任务是通过检测按键的按下情况来发送消息通知,另两个任务获取消息通知, 在任务通知中没有可用的通知之前就一直等待消息,一旦获取到消息通知就把消息打印在串口调试助手里。 代码如下所示

app_freertos.c
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
void Receive1_Task(void *argument)
{
/* USER CODE BEGIN Receive1_Task */
/* Infinite loop */
    BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */

    uint32_t r_num;

    for(;;)
    {
    /* BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry,
                                uint32_t ulBitsToClearOnExit,
                                uint32_t *pulNotificationValue,
                                TickType_t xTicksToWait );
    * ulBitsToClearOnEntry:当没有接收到任务通知的时候将任务通知值与此参数的取
    反值进行按位与运算,当此参数为Oxffffffff时就会将任务通知值清零。
    * ulBits ToClearOnExit:如果接收到了任务通知,在做完相应的处理退出函数之前将
    任务通知值与此参数的取反值进行按位与运算,当此参数为0xffffffff或者ULONG MAX的时候
    就会将任务通知值清零。
    * pulNotification Value:此参数用来保存任务通知值。
    * xTick ToWait:阻塞时间。
    *
    * 返回值:pdTRUE:获取到了任务通知。pdFALSE:任务通知获取失败。
    */
    //获取任务通知 ,没获取到则一直等待
        xReturn=xTaskNotifyWait(0x0,                        //进入函数的时候不清除任务bit
                            0xffffffff,       //退出函数的时候清除所有的bit
                            &r_num,           //保存任务通知值
                            portMAX_DELAY); //阻塞时间
        if( pdTRUE == xReturn )
        {
            printf("Receive1_Task任务通知消息为%ld \n",r_num);
        }

        LED1_TOGGLE;
    }
/* USER CODE END Receive1_Task */
}


void Receive2_Task(void *argument)
{
/* USER CODE BEGIN Receive2_Task */
    BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */

    uint32_t r_num;

    for(;;)
    {
    /* BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry,
                                uint32_t ulBitsToClearOnExit,
                                uint32_t *pulNotificationValue,
                                TickType_t xTicksToWait );
    * ulBitsToClearOnEntry:当没有接收到任务通知的时候将任务通知值与此参数的取
    反值进行按位与运算,当此参数为Oxffffffff时就会将任务通知值清零。
    * ulBits ToClearOnExit:如果接收到了任务通知,在做完相应的处理退出函数之前将
    任务通知值与此参数的取反值进行按位与运算,当此参数为0xffffffff或者ULONG MAX的时候
    就会将任务通知值清零。
    * pulNotification Value:此参数用来保存任务通知值。
    * xTick ToWait:阻塞时间。
    *
    * 返回值:pdTRUE:获取到了任务通知。pdFALSE:任务通知获取失败。
    */
    //获取任务通知 ,没获取到则一直等待
        xReturn=xTaskNotifyWait(0x0,                        //进入函数的时候不清除任务bit
                            0xffffffff,       //退出函数的时候清除所有的bit
                            &r_num,           //保存任务通知值
                            portMAX_DELAY); //阻塞时间
        if( pdTRUE == xReturn )
        {
            printf("Receive2_Task任务通知消息为%ld \n",r_num);
        }

        LED2_TOGGLE;
    }
/* USER CODE END Receive2_Task */
}


void Send_Task(void *argument)
{
/* USER CODE BEGIN Send_Task */

    uint32_t send1 = 1;
    uint32_t send2 = 2;

    for(;;)
    {
        /* KEY1 被按下 */
        if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )
        {
        /* 原型:BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,
                                        uint32_t ulValue,
                                        eNotifyAction eAction );
        * eNoAction = 0,通知任务而不更新其通知值。
        * eSetBits,     设置任务通知值中的位。
        * eIncrement,   增加任务的通知值。
        * eSetvaluewithoverwrite,覆盖当前通知
        * eSetValueWithoutoverwrite 不覆盖当前通知
        *
        * pdFAIL:当参数eAction设置为eSetValueWithoutOverwrite的时候,
                    * 如果任务通知值没有更新成功就返回pdFAIL。
        * pdPASS: eAction 设置为其他选项的时候统一返回pdPASS。
        */
        xTaskNotify( Receive1TaskHandle, /*任务句柄*/
                                send1, /* 发送的数据,最大为4字节 */
                                eSetValueWithOverwrite );/*覆盖当前通知*/

        }
        /* KEY2 被按下 */
        if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )
        {
        xTaskNotify( Receive2TaskHandle, /*任务句柄*/
                                send2, /* 发送的数据,最大为4字节 */
                                eSetValueWithOverwrite );/*覆盖当前通知*/

        }
        osDelay(20);
    }

}

13.5.2. 任务通知代替二值信号量

任务通知代替消息队列是在FreeRTOS中创建了三个任务,其中两个任务是用于接收任务通知, 另一个任务发送任务通知。三个任务独立运行,发送通知任务是通过检测按键的按下情况来发送通知, 另两个任务获取通知,在任务通知中没有可用的通知之前就一直等待任务通知,获取到通知以后就将通知值清0, 这样子是为了代替二值信号量,任务同步成功则继续执行, 然后在串口调试助手里将运行信息打印出来。

app_freertos.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
void Receive1_Task(void *argument)
{

    for(;;)
    {
        /* uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
        * xClearCountOnExit:pdTRUE 在退出函数的时候任务任务通知值清零,类似二值信号量
        * pdFALSE 在退出函数ulTaskNotifyTakeO的时候任务通知值减一,类似计数型信号量。
        */
        //获取任务通知 ,没获取到则一直等待
        ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
        printf("Receive1_Task 任务通知获取成功!\n");

        LED1_TOGGLE;
    }
}

void Receive2_Task(void *argument)
{

    for(;;)
    {
        /* uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
        * xClearCountOnExit:pdTRUE 在退出函数的时候任务任务通知值清零,类似二值信号量
        * pdFALSE 在退出函数ulTaskNotifyTakeO的时候任务通知值减一,类似计数型信号量。
        */
        //获取任务通知 ,没获取到则一直等待
        ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
        printf("Receive2_Task 任务通知获取成功!\n");

        LED2_TOGGLE;
    }

}


void Send_Task(void *argument)
{

    for(;;)
    {
        /* KEY1 被按下 */
        if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )
        {
        /* 原型:BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); */
        xTaskNotifyGive(Receive1TaskHandle);

        }

        /* KEY2 被按下 */
        if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )
        {
        xTaskNotifyGive(Receive2TaskHandle);
        /* 此函数只会返回pdPASS */
        }
        osDelay(20);
    }
}

13.5.3. 任务通知代替计数信号量

任务通知代替计数信号量是基于计数型信号量实验修改而来,模拟停车场工作运行。 并且在FreeRTOS中创建了两个任务:一个是获取任务通知,一个是发送任务通知,两个任务独立运行, 获取通知的任务是通过按下KEY1按键获取,模拟停车场停车操作,其等待时间是0; 发送通知的任务则是通过检测KEY2按键按下进行通知的发送,模拟停车场取车操作, 并且在串口调试助手输出相应信息。

app_freertos.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void Take_Task(void *argument)
{

    uint32_t take_num = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */
    /* 任务都是一个无限循环,不能返回 */
    for(;;)
    {
        //如果KEY1被单击
        if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )
        {
            /* uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
            * xClearCountOnExit:pdTRUE 在退出函数的时候任务任务通知值清零,类似二值信号量
            * pdFALSE 在退出函数ulTaskNotifyTakeO的时候任务通知值减一,类似计数型信号量。
            */
            //获取任务通知 ,没获取到则不等待
            take_num=ulTaskNotifyTake(pdFALSE,0);//
            if(take_num > 0)
            printf( "KEY1被按下,成功申请到停车位,当前车位为 %ld \n", take_num - 1);
                else
            printf( "KEY1被按下,车位已经没有了,请按KEY2释放车位\n" );
        }
        osDelay(20);     //每20ms扫描一次
}


void Give_Task(void *argument)
{

    BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
    /* 任务都是一个无限循环,不能返回 */
    for(;;)
    {
        //如果KEY2被单击
        if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )
        {
        /* 原型:BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); */
        /* 释放一个任务通知 */
        xTaskNotifyGive(TakeTaskHandle);//发送任务通知
        /* 此函数只会返回pdPASS */
        if ( pdPASS == xReturn )
        {
            printf( "KEY2被按下,释放1个停车位。\n" );
        }
        }
        osDelay(20);     //每20ms扫描一次
    }

}

13.5.4. 任务通知代替事件组

任务通知代替事件组实验是在事件标志组实验基础上进行修改,实验任务通知替代事件实现事件类型的通信, 该实验是在FreeRTOS中创建了两个任务,一个是发送事件通知任务,一个是等待事件通知任务,两个任务独立运行, 发送事件通知任务通过检测按键的按下情况设置不同的通知值位,等待事件通知任务则获取这任务通知值, 并且根据通知值判断两个事件是否都发生,如果是则输出相应信息,LED进行翻转。 等待事件通知任务的等待时间是portMAX_DELAY,一直在等待事件通知的发生, 等待获取到事件之后清除对应的任务通知值的位。

app_freertos.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
void KEY_Task(void *argument)
{

    /* 任务都是一个无限循环,不能返回 */
    for(;;)
    {
        if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )
        {
        printf ( "KEY1被按下\n" );
        /* 原型:BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,
                                        uint32_t ulValue,
                                        eNotifyAction eAction );
        * eNoAction = 0,通知任务而不更新其通知值。
        * eSetBits,     设置任务通知值中的位。
        * eIncrement,   增加任务的通知值。
        * eSetvaluewithoverwrite,覆盖当前通知
        * eSetValueWithoutoverwrite 不覆盖当前通知
        *
        * pdFAIL:当参数eAction设置为eSetValueWithoutOverwrite的时候,
        * 如果任务通知值没有更新成功就返回pdFAIL。
        * pdPASS: eAction 设置为其他选项的时候统一返回pdPASS。
        */
                /* 触发一个事件1 */
            xTaskNotify((TaskHandle_t       )LEDTaskHandle,//接收任务通知的任务句柄
                    (uint32_t               )KEY1_EVENT,                    //要触发的事件
                    (eNotifyAction)eSetBits);                       //设置任务通知值中的位
        }

        if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )
        {
            printf ( "KEY2被按下\n" );
            /* 触发一个事件2 */
            xTaskNotify((TaskHandle_t       )LEDTaskHandle,//接收任务通知的任务句柄
                        (uint32_t           )KEY2_EVENT,                    //要触发的事件
                        (eNotifyAction)eSetBits);                   //设置任务通知值中的位
        }
        osDelay(20);     //每20ms扫描一次
    }

}


void LED_Task(void *argument)
{

    uint32_t r_event = 0;  /* 定义一个事件接收变量 */
    uint32_t last_event = 0;/* 定义一个保存事件的变量 */
    BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */
    /* 任务都是一个无限循环,不能返回 */
    for(;;)
    {
        /* BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry,
                                    uint32_t ulBitsToClearOnExit,
                                    uint32_t *pulNotificationValue,
                                    TickType_t xTicksToWait );
        * ulBitsToClearOnEntry:当没有接收到任务通知的时候将任务通知值与此参数的取
        反值进行按位与运算,当此参数为Oxfffff或者ULONG_MAX的时候就会将任务通知值清零。
        * ulBits ToClearOnExit:如果接收到了任务通知,在做完相应的处理退出函数之前将
        任务通知值与此参数的取反值进行按位与运算,当此参数为0xfffff或者ULONG MAX的时候
        就会将任务通知值清零。
        * pulNotification Value:此参数用来保存任务通知值。
        * xTick ToWait:阻塞时间。
        *
        * 返回值:pdTRUE:获取到了任务通知。pdFALSE:任务通知获取失败。
        */
        //获取任务通知 ,没获取到则一直等待
        xReturn = xTaskNotifyWait(0x0,                      //进入函数的时候不清除任务bit
                                0xffffffff,   //退出函数的时候清除所有的bitR
                                &r_event,             //保存任务通知值
                                portMAX_DELAY);     //阻塞时间
        if( pdTRUE == xReturn )
        {
        last_event |= r_event;
        /* 如果接收完成并且正确 */
        if(last_event == (KEY1_EVENT|KEY2_EVENT))
        {
            last_event = 0;     /* 上一次的事件清零 */
            printf ( "Key1与Key2都按下\n");
            LED1_TOGGLE;       //LED1       反转
        }
        else  /* 否则就更新事件 */
            last_event = r_event;   /* 更新上一次触发的事件 */
        }

    }
}

13.6. 任务通知实验现象

13.6.1. 任务通知代替消息队列实验现象

按下开发版的KEY1按键发送消息1,按下KEY2按键发送消息2;如下图所示

实验现象

13.6.2. 任务通知代替二值信号量实验现象

如下图所示

实验现象

13.6.3. 任务通知代替计数信号量实验现象

按下开发版的KEY1按键获取信号量模拟停车, 按下KEY2按键释放信号量模拟取车,因为是使用任务通知代替信号量,所以任务通知值默认为0, 表当前车位为0;如图所示

实验现象

13.6.4. 任务通知代替事件组实验现象

按下开发版的KEY1按键发送事件通知1, 按下KEY2按键发送事件通知2;我们按下KEY1与KEY2试试,在串口调试助手中可以看到运行结果, 并且当事件1与事件2都发生的时候,开发板的LED会进行翻转,如下图所示。

实验现象