FreeRTOS 任务通知

简述

FreeRTOS 从版本 V8.2.0开始提供任务通知这个功能,每个任务都有一个32位的通知值。按照 FreeRTOS 官方的说法,使用消息通知比通过二进制信号量方式解除阻塞任务快 45%, 并且更加省内存(无需创建队列)。

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

  • 发送消息给任务, 如果有通知未读, 不覆盖通知值
  • 发送消息给任务,直接覆盖通知值
  • 发送消息给任务,设置通知值的一个或者多个位
  • 发送消息给任务,递增通知值

通过对以上方式的合理使用,可以在一定场合下替代原本的信号量,队列等。

当然,消息通知也有其限制 :

  • 通知只能一对一,因为通知必须指定任务
  • 等待通知的任务可以被阻塞, 但是发送消息的任务,任何情况下都不会被阻塞等待

分析的源码版本是 v9.0.0

通知 API

FreeRTOS 关于任务通知的 API 如下

API

功能

xTaskNotifyGive()

发送通知,没有通知值 (信号量类型)

ulTaskNotifyTake()

获取通知,(对应 Give)

xTaskNotify()

发送通知, 带通知值

xTaskNotifyAndQuery()

发送通知,带通知值,并且返回原通知值

xTaskNotifyWait()

等待通知

vTaskNotifyGiveFromISR()

xTaskNotifyGive 的中断版本

xTaskNotifyAndQueryFromISR()

xTaskNotifyAndQuery 的中断版本

xTaskNotifyFromISR()

ulTaskNotifyTake 的中断版本

xTaskNotifyStateClear()

清除所有未读消息

可能你会想,消息通知就一个发送一个接收 API 不就好了,为什么要搞出这么多个 API ? 实际上, 以上的 API,有的是宏定义,而如此实现是方便特定情况下使用,比如用通知去实现轻量化的二进制信号量,计数信号量,队列等。

数据结构

方便下文叙述,先介绍下实现的相关变量定义。 为了实现任务通知,任务控制块 TCB_t 结构体中有两个任务通知的相关变量, 默认情况下, 任务通知这个功能是打开的,也就是宏 configUSE_TASK_NOTIFICATIONS 设置为 1 源码

#if( configUSE_TASK_NOTIFICATIONS == 1 )
    volatile uint32_t ulNotifiedValue;
    volatile uint8_t ucNotifyState;
#endif
  1. 变量 ulNotifiedValue 存储任务通知的数值, 初始化为 0。
  2. 变量 ucNotifyState 存储当前任务通知的状态,对应存在以下三种状态
// 1 没有未读通知,任务没有等待通知
#define taskNOT_WAITING_NOTIFICATION    ( ( uint8_t ) 0 )
// 2 任务等待通知
#define taskWAITING_NOTIFICATION        ( ( uint8_t ) 1 )
// 3 通知等待任务读取
#define taskNOTIFICATION_RECEIVED       ( ( uint8_t ) 2 )

该变量初始化为 taskNOT_WAITING_NOTIFICATION

文章开头提到发送任务通知的几种方式,对应系统源码中定义了如下 5 种命令类型 :

typedef enum
{
    // 1 发送通知,但是没有更新通知值
    eNoAction = 0,              
    // 2 发送通知,将新通知值与原通知值或操作(置位)
    eSetBits,                   
    // 3 发送通知,原通知值加 1
    eIncrement,                 
    // 4 发送通知,直接修改通知值(不过上次通知是否已经读取)
    eSetValueWithOverwrite,     
    // 5 发送通知,如果没有未读消息,设置通知值
    eSetValueWithoutOverwrite   
} eNotifyAction;

轻量级二进制信号量

我在 << FreeRTOS 信号量 >> 一文中举过一个例子,用二进制信号量进行同步。 这里,我们使用任务通知来实现同样的任务同步功能。

先看例子源码 :

// 等待通知的任务句柄
static TaskHandle_t xTaskToNotify = NULL;

void vATask( void * pvParameters )
{
    uint32_t ulNotificationValue;
    // 设置等待通知阻塞时间
    const TickType_t xMaxBlockTime = pdMS_TO_TICKS( 200 );
    // 获取任务句柄
    xTaskToNotify = xTaskGetCurrentTaskHandle();
    // ...
    for (;;) {
        // ...
        // 等待通知
        ulNotificationValue = ulTaskNotifyTake( pdTRUE,
                                            xMaxBlockTime );
        if( ulNotificationValue > 0 ) {
            // 收到通知
        } else {
           // 超时
        }   
    }
}

// 中断 发送通知
void vTimerISR( void * pvParameters )
{
    static signed BaseType_t xHigherPriorityTaskWoken;
    xHigherPriorityTaskWoken = pdFALSE;
    // 发送通知
    vTaskNotifyGiveFromISR( xTaskToNotify, 
        &xHigherPriorityTaskWoken );
    // 传递参数判断是否有高优先级任务就绪
    // 判断是否需要触发任务切换
    portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

上面例子, 任务调用 API ulTaskNotifyTake 等待中断发送通知,中断中调用 API vTaskNotifyGiveFromISR给任务发送通知, 如果是在任务中发送消息,则调用 API vTaskNotifyGive。 例子中的任务通知实现了二进制型号量的任务同步功能。

下面分析下任务通知这个功能如何实现信号量获取和释放。

获取信号量

任务中,调用了函数 ulTaskNotifyTake等待通知,相当于尝试获取信号量。

为了实现二进制信号量,函数的第一个参数设置为pdTRUE, 在接收到通知后,读取并清除通知值(设置为0)。(此处可以对比后续的计数信号量) 第二参数是阻塞等待时间。

直接查看该函数的实现 :

uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit,
                            TickType_t xTicksToWait )
{
    uint32_t ulReturn;
    taskENTER_CRITICAL();
    {
        // 如果通知值为 0 ,阻塞任务
        // 默认初始化通知值为 0, 说明没有未读通知
        if( pxCurrentTCB->ulNotifiedValue == 0UL )
        {
            // 标记任务状态 : 等待消息通知
            pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;
            // 挂起任务等待消息或者超时
            if( xTicksToWait > ( TickType_t ) 0 )
            {
                prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
                portYIELD_WITHIN_API();
            }
        }
    }
    taskEXIT_CRITICAL();
    // 到此处,有通知 或者 等待超时
    taskENTER_CRITICAL();
    {
        // 判断通知值
        ulReturn = pxCurrentTCB->ulNotifiedValue;
        if( ulReturn != 0UL )
        {
            // 接收到通知
            // 第一个参数为真,读取后清零通知值
            if( xClearCountOnExit != pdFALSE )
            {
                pxCurrentTCB->ulNotifiedValue = 0UL;
            }
            else
            {
                // 否则 通知值减 1
                pxCurrentTCB->ulNotifiedValue = ulReturn - 1;
            }
        }
        // 恢复任务通知状态变量
        pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
    }
    taskEXIT_CRITICAL();
    return ulReturn;
}

对于这个函数,任务通知值为0, 对应信号量无效,如果任务设置了阻塞等待,任务被阻塞挂起。当其他任务或中断发送通知修改了通知值使其不为0后,信号量变为有效,等待通知的任务会读取通知,根据传递的第一个参数清零通知值或者执行递减操作。 对于二进制信号量,信号量读取一次后就失效,所以直接清零。

释放信号量

例子中是在中断中发送通知,所以必须调用带有FromISR后缀的API。发送通知调用的函数是 vTaskNotifyGiveFromISR, 对应函数名,也可以看出是一个释放信号量的操作。

对该函数进行简化说明,看看其实如何给出信号量的

void vTaskNotifyGiveFromISR( TaskHandle_t xTaskToNotify, 
    BaseType_t *pxHigherPriorityTaskWoken )
{
    TCB_t * pxTCB;
    uint8_t ucOriginalNotifyState;
    UBaseType_t uxSavedInterruptStatus;

    pxTCB = ( TCB_t * ) xTaskToNotify;
    // 中断优先级临时设置
    uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
    {
        // 保存任务通知的原状态
        ucOriginalNotifyState = pxTCB->ucNotifyState;
        // 更新通知状态 : 收到通知
        pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;

        // Give 类似返回信号量, 直接递增通知值
        ( pxTCB->ulNotifiedValue )++;

        // 判断被通知任务是否正在阻塞等待通知
        if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )
        {
            // 如果任务调度器运行中
            if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
            {
                // 把等待任务移到就绪链表
                ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
                prvAddTaskToReadyList( pxTCB );
            }
            else
            {
                // 调度器挂起,中断依然正常发生,但是不能直接操作就绪链表
                // 加入到就绪挂起链表,任务调度恢复后会移动到就绪链表
                vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) );
            }

            // 被通知任务优先级高
            if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
            {
                // 设置返回参数,标识需要任务切换
                if( pxHigherPriorityTaskWoken != NULL )
                {
                    *pxHigherPriorityTaskWoken = pdTRUE;
                }
                else
                {
                    // 如果用户没有设置参数,则使用系统全局变量
                    // 目的都是说明,需要任务切换
                    xYieldPending = pdTRUE;
                }
            }
        }
    }
    // 恢复中断的优先级
    portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
}

每次调用该函数都会递增任务通知值。 在开头的例子中, 任务通过接收函数返回值是否大于零,判断是否获取到了通知,任务通知值初始化为0, 对应表达信号量无效。 当任务或者中断调用发送函数给出信号量时,递增通知值,使其大于零,使其表示的信号量变为有效,恢复阻塞等待的任务。

如果是在任务中调用,则调用的接口变为 xTaskNotifyGive, 查看该接口,发现实际是一个宏

#define xTaskNotifyGive( xTaskToNotify ) 
    xTaskGenericNotify( ( xTaskToNotify ), ( 0 ), eIncrement, NULL )

该宏对一个比较通用的函数进行了封装,xTaskGenericNotify 这个函数实际上实现了所有任务通知的方式,通过在调用的时候指定命令类型。

该函数调用说明 :

BaseType_t xTaskGenericNotify( 
        /*通知任务句柄*/
        TaskHandle_t xTaskToNotify,
        /*新通知值*/
        uint32_t ulValue, 
        /*发送通知命令类型*/
        eNotifyAction eAction,
        /*任务原本通知值返回*/
        uint32_t *pulPreviousNotificationValue );

该函数实现如下,

BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify,
        uint32_t ulValue, eNotifyAction eAction,
        uint32_t *pulPreviousNotificationValue )
{
    TCB_t * pxTCB;
    BaseType_t xReturn = pdPASS;
    uint8_t ucOriginalNotifyState;
    pxTCB = ( TCB_t * ) xTaskToNotify;
    
    taskENTER_CRITICAL();
    {
        // 返回任务原来的通知值到传递参数
        if( pulPreviousNotificationValue != NULL )
        {
            *pulPreviousNotificationValue = pxTCB->ulNotifiedValue;
        }

        // 保存原来的任务通知状态
        ucOriginalNotifyState = pxTCB->ucNotifyState;
        // 设置新的任务通知状态 : 收到消息
        pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;

        switch( eAction )
        {
        case eSetBits   :
            // 置位 : 原通知值与新通知值进行或
            pxTCB->ulNotifiedValue |= ulValue;
            break;
        case eIncrement :
            // 增加 : 原通知值加 1
            ( pxTCB->ulNotifiedValue )++;
            break;
        case eSetValueWithOverwrite :
            // 覆盖 : 新通知值直接覆盖
            pxTCB->ulNotifiedValue = ulValue;
            break;
        case eSetValueWithoutOverwrite :
            //不覆盖 : 不覆盖未读消息
            if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED )
            {
                // 没有未读消息,设置通知值
                pxTCB->ulNotifiedValue = ulValue;
            }
            else
            {
                // 有未读消息, 不覆盖通知值
                xReturn = pdFAIL;
            }
            break;
        case eNoAction:
            // 不修改通知值
            break;
        }

        // 如果被通知任务由于等待任务通知而挂起
        if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )
        {
            // 唤醒任务, 插入就绪链表
            ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
            prvAddTaskToReadyList( pxTCB );

#if( configUSE_TICKLESS_IDLE != 0 )
            {
                //更新下一个阻塞任务超时时间
                prvResetNextTaskUnblockTime();
            }
#endif
            // 被通知任务优先级比当前任务高
            if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
            {
                // 触发 PendSV , 切换高优先级任务运行
                taskYIELD_IF_USING_PREEMPTION();
            }
        }
    }
    taskEXIT_CRITICAL();
    return xReturn;
}

该函数基本流程如下

  1. 保存原有通知值,写入传入的指针
  2. 根据命令类型设置通知值
  3. 判断被通知任务是否等待通知,根据优先级判断是否需要唤醒
  4. 返回操作结果

如果调用函数的时候传递了读取旧值的指针, 函数会把原有通知值写到该指针所指内存,之后按照命令类型更新通知值。

回到这一部分二进制信号量,看到该宏调用上面这个函数时,传递的命令类型是 eIncrement, 也就是在原有通知值基础上递增1, 这样看来,就和 xTaskNotifyGiveFromISR的效果一样了。

轻量级计数信号量

上面提到二进制信号量,在被通知任务,也就是获取信号量的任务获取了信号量后,会把通知值直接设置为0,这对应了二进制信号量的特点 : 不管任务或者中断调用了几次通知发送函数递增通知值,只要被通知任务读取了一次通知,就会直接把该值清零 。 而计数信号量不同在于读取一次通知后不会直接把通知值清零,而是递减1,因此,任务被通知几次,对应被通知任务就可以执行读取几次,直到通知值递减为0。

到此,我们基本直到,为了实现计数信号量,只需要简单地修改下二进制信号量的获取函数的第一个参数,就可以了。

ulNotificationValue = ulTaskNotifyTake( pdFALSE,/*调用递减不直接清0*/
                                            xMaxBlockTime );

第一个参数设置为 pdFALSE, 函数ulTaskNotifyTake不会直接把通知值清零,而是每调用一次递减1,直到0为止。 可以查看上文该函数的实现。

轻量级事件标记组

二进制信号量或者计数信号量只能通知任务一个事件,如果有两种不同的事件,他们就无法实现了。这时候就需要利用事件分组了。

举个应用例子, 一个处理串口事件的任务,串口事件包括接收和发送,对应在其中断中发送通知,我们利用任务通知实现事件分组如下实现 :

/定义事件位标记
#define TX_BIT    0x01
#define RX_BIT    0x02
//任务句柄
static TaskHandle_t xHandlingTask;

// 发送事件中断
void vTxISR( void )
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // ...
    // 发送通知
    xTaskNotifyFromISR( xHandlingTask,
            TX_BIT,
            eSetBits,
            &xHigherPriorityTaskWoken );

    portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

//接收事件中断
void vRxISR( void )
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // ...
    // 发送通知
    xTaskNotifyFromISR( xHandlingTask,
            RX_BIT,
            eSetBits,
            &xHigherPriorityTaskWoken );

    portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

// 处理任务 被通知任务
static void prvHandlingTask( void *pvParameter )
{
    const TickType_t xMaxBlockTime = pdMS_TO_TICKS( 500 );
    BaseType_t xResult;

    for( ;; )
    {
        // 等待通知
        xResult = xTaskNotifyWait( pdFALSE,/*接收前不清除任何位*/
                ULONG_MAX,/* 接收后清除所有位*/
                &ulNotifiedValue, /* 保存通知值*/
                xMaxBlockTime );

        if( xResult == pdPASS )
        {
            if( ( ulNotifiedValue & TX_BIT ) != 0 )
            {
                prvProcessTx();
            }

            if( ( ulNotifiedValue & RX_BIT ) != 0 )
            {
                prvProcessRx();
            }
        }
        else
        {
            prvCheckForErrors();
        }
    }
}

前面介绍到一个通知发送函数 xTaskGenericNotify, 可以设置命令类型,上面例子中,使用的命令类型是 eSetBits。在开头可以看到对应每个事件, 用一个bit 去对应指代他,当该事件发生时,发送通知,并且置位通知值的对应位,这样,被通知任务就可以根据通知值的位区分出什么事件通知。

前面实现信号量提到的接收通知的函数是 ulTaskNotifyTake, 该函数判断是否有未读通知是根据通知值是否为零,相对来说,该函数实现主要是针对信号量那种类型。

例子中任务调用的等待函数,xTaskNotifyWait,该函数判断是否有通知是依据另外一个变量 ucNotifyState, 算起来,这里,通知值才算真正承载了有用的通知内容。

该函数的参数说明 :

BaseType_t xTaskNotifyWait( 
    /*接收通知前清除通知值指定位 对应 1 的bit清除*/
    uint32_t ulBitsToClearOnEntry, 
    /*接收通知后清除通知值指定位*/
    uint32_t ulBitsToClearOnExit, 
    /*接收到的通知值*/
    uint32_t *pulNotificationValue, 
    /*等待时间*/
    TickType_t xTicksToWait );

具体看看该函数实现 :

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 )
        {
            // 接收通知前,清除通知值中的指定位
            pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnEntry;

            // 设置任务通知状态 : 等待消息通知
            pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;

            // 挂起任务等待通知或者超时
            if( xTicksToWait > ( TickType_t ) 0 )
            {
                prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
                portYIELD_WITHIN_API();
            }
        }
    }
    taskEXIT_CRITICAL();

    taskENTER_CRITICAL();
    {
        if( pulNotificationValue != NULL )
        {
            // 返回通知值 (超时情况下,该值并没有被更新)
            *pulNotificationValue = pxCurrentTCB->ulNotifiedValue;
        }

        // 通过任务通知状态,判断是否收到通知
        if( pxCurrentTCB->ucNotifyState == taskWAITING_NOTIFICATION )
        {
            // 超时,没有收到通知
            xReturn = pdFALSE;
        }
        else
        {
            // 接收通知后,清除通知值的指定位
            pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnExit;
            // 返回接收到通知
            xReturn = pdTRUE;
        }
        // 设置任务通知状态
        pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
    }
    taskEXIT_CRITICAL();
    return xReturn;
}

调用该函数, 在接收到新的通知值前, 会根据第一个参数清除通知值上的特定位(第一个参数为1的位,对应通知值清0)。 接收到通知后,读取通知值保存到参数 *pulNotificationValue后,会根据第二个参数清除通知值上对应位的值。 在事件分组这个例子中,任务接收到事件通知后,通过通知值上置的位判断什么事件发生了,然后清除通知值,等待下一次事件发生,置位通知。

轻量级消息邮箱

把通知值作为内容,任务通知相当于是一个深度为1的队列。给任务发送通知就相当于投递了。 另外,如果邮件内容大于32bit,也可以把指针作为内容发送出去,接收任务读取通知值后,转换为指针读取实际内容。 思路如此,不做赘述。


参考

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏JetpropelledSnake

ELK学习笔记之Logstash和Filebeat解析对java异常堆栈下多行日志配置支持

logstash官方最新文档。 假设有几十台服务器,每台服务器要监控系统日志syslog、tomcat日志、nginx日志、mysql日志等等,监控OOM、内存...

56810
来自专栏张戈的专栏

Linux系统crontab备份数据库执行不成功?可能是百分号%在作怪!

之前博客分享过一篇《Linux/vps 本地七天循环备份和七牛远程备份脚本》,我自己也一直在用。某天检查备份的时候,突然发现数据库的备份的压缩包是空的! 看了下...

32240
来自专栏java一日一条

Java Spring中同时访问多种不同数据库

开发企业应用时我们常常遇到要同时访问多种不同数据库的问题,有时是必须把数据归档到某种数据仓库中,有时是要把数据变更推送到第三方数据库中。使用Spring框架时,...

27910
来自专栏liulun

kotlin web开发教程【一】从零搭建kotlin与spring boot开发环境

IDEA中文输入法的智能提示框不会跟随光标的问题 我用的开发工具是IDEA image.png 这个版本的IDEA有一个问题; 就是中文输入法的智能提示框不...

39360
来自专栏程序猿

SQLmap命令的介绍

SQLmap这个工具的使用。(结尾有彩蛋) URL---> 判断注入的参数, 判断是使用的那种SQL注入 识别出那种数据库 根据用户的选择,读出数据 五种注入模...

32930
来自专栏乐沙弥的世界

Linux 下安装及配置heartbeat

a、配置主机host解析 b、配置等效验证 c、高可用的相关服务配置(如httpd,myqld等),关闭自启动 d、如需要用到共享存储,还应配置相关...

44640
来自专栏Jackson0714

PHP内核之旅-1.生命周期

12720
来自专栏Java架构师历程

ssh工作原理

1.通过Configuration().configure();读取并解析hibernate.cfg.xml配置文件

15030
来自专栏SpringBoot 核心技术

第四十三章: 基于SpringBoot & RabbitMQ完成TopicExchange分布式消息消费

409150
来自专栏SpringBoot 核心技术

SpringCloud组件:Eureka服务注册中心内置的REST节点列表

你有没有考虑过Eureka Client与Eureka Server是通过什么方式进行通讯的? 为什么Client启动成功后Server就会被注册到Serve...

92420

扫码关注云+社区

领取腾讯云代金券