• 韦东山freeRTOS系列教程之【第九章】任务通知(Task Notifications)


    需要获取更好阅读体验的同学,请访问我专门设立的站点查看,地址:http://rtos.100ask.net/

    系列教程总目录

    本教程连载中,篇章会比较多,为方便同学们阅读,点击这里可以查看文章的 目录列表,目录列表页面地址:https://blog.csdn.net/thisway_diy/article/details/121399484

    概述

    所谓"任务通知",你可以反过来读"通知任务"。

    我们使用队列、信号量、事件组等等方法时,并不知道对方是谁。使用任务通知时,可以明确指定:通知哪个任务。

    使用队列、信号量、事件组时,我们都要事先创建对应的结构体,双方通过中间的结构体通信:

    在这里插入图片描述

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

    在这里插入图片描述

    本章涉及如下内容:

    • 任务通知:通知状态、通知值

    • 任务通知的使用场合

    • 任务通知的优势

    9.1 任务通知的特性

    9.1.1 优势及限制

    任务通知的优势:

    • 效率更高:使用任务通知来发送事件、数据给某个任务时,效率更高。比队列、信号量、事件组都有大的优势。
    • 更节省内存:使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。

    任务通知的限制:

    • 不能发送数据给ISR:
      ISR并没有任务结构体,所以无法使用任务通知的功能给ISR发送数据。但是ISR可以使用任务通知的功能,发数据给任务。
    • 数据只能给该任务独享
      使用队列、信号量、事件组时,数据保存在这些结构体中,其他任务、ISR都可以访问这些数据。使用任务通知时,数据存放入目标任务中,只有它可以访问这些数据。
      在日常工作中,这个限制影响不大。因为很多场合是从多个数据源把数据发给某个任务,而不是把一个数据源的数据发给多个任务。
    • 无法缓冲数据
      使用队列时,假设队列深度为N,那么它可以保持N个数据。
      使用任务通知时,任务结构体中只有一个任务通知值,只能保持一个数据。
    • 无法广播给多个任务
      使用事件组可以同时给多个任务发送事件。
      使用任务通知,只能发个一个任务。
    • 如果发送受阻,发送方无法进入阻塞状态等待
      假设队列已经满了,使用xQueueSendToBack()给队列发送数据时,任务可以进入阻塞状态等待发送完成。
      使用任务通知时,即使对方无法接收数据,发送方也无法阻塞等待,只能即刻返回错误。

    9.1.2 通知状态和通知值

    每个任务都有一个结构体:TCB(Task Control Block),里面有2个成员:

    • 一个是uint8_t类型,用来表示通知状态
    • 一个是uint32_t类型,用来表示通知值
    typedef struct tskTaskControlBlock
    {
        ......
        /* configTASK_NOTIFICATION_ARRAY_ENTRIES = 1 */
        volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
        volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
        ......
    } tskTCB;
    

    通知状态有3种取值:

    • taskNOT_WAITING_NOTIFICATION:任务没有在等待通知
    • taskWAITING_NOTIFICATION:任务在等待通知
    • taskNOTIFICATION_RECEIVED:任务接收到了通知,也被称为pending(有数据了,待处理)
    #define taskNOT_WAITING_NOTIFICATION              ( ( uint8_t ) 0 )  /* 也是初始状态 */
    #define taskWAITING_NOTIFICATION                  ( ( uint8_t ) 1 )
    #define taskNOTIFICATION_RECEIVED                 ( ( uint8_t ) 2 )
    

    通知值可以有很多种类型:

    • 计数值
    • 位(类似事件组)
    • 任意数值

    9.2 任务通知的使用

    使用任务通知,可以实现轻量级的队列(长度为1)、邮箱(覆盖的队列)、计数型信号量、二进制信号量、事件组。

    9.2.1 两类函数

    任务通知有2套函数,简化版、专业版,列表如下:

    • 简化版函数的使用比较简单,它实际上也是使用专业版函数实现的
    • 专业版函数支持很多参数,可以实现很多功能
    简化版 专业版
    发出通知 xTaskNotifyGive
    vTaskNotifyGiveFromISR
    xTaskNotify
    xTaskNotifyFromISR
    取出通知 ulTaskNotifyTake xTaskNotifyWait

    9.2.2 xTaskNotifyGive/ulTaskNotifyTake

    在任务中使用xTaskNotifyGive函数,在ISR中使用vTaskNotifyGiveFromISR函数,都是直接给其他任务发送通知:

    • 使得通知值加一
    • 并使得通知状态变为"pending",也就是taskNOTIFICATION_RECEIVED,表示有数据了、待处理

    可以使用ulTaskNotifyTake函数来取出通知值:

    • 如果通知值等于0,则阻塞(可以指定超时时间)
    • 当通知值大于0时,任务从阻塞态进入就绪态
    • 在ulTaskNotifyTake返回之前,还可以做些清理工作:把通知值减一,或者把通知值清零

    使用ulTaskNotifyTake函数可以实现轻量级的、高效的二进制信号量、计数型信号量。

    这几个函数的原型如下:

    BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );
    
    void vTaskNotifyGiveFromISR( TaskHandle_t xTaskHandle, BaseType_t *pxHigherPriorityTaskWoken );
    
    uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
    

    xTaskNotifyGive函数的参数说明如下:

    参数 说明
    xTaskToNotify 任务句柄(创建任务时得到),给哪个任务发通知
    返回值 必定返回pdPASS

    vTaskNotifyGiveFromISR函数的参数说明如下:

    参数 说明
    xTaskHandle 任务句柄(创建任务时得到),给哪个任务发通知
    pxHigherPriorityTaskWoken 被通知的任务,可能正处于阻塞状态。
    此函数发出通知后,会把它从阻塞状态切换为就绪态。
    如果被唤醒的任务的优先级,高于当前任务的优先级,
    则"*pxHigherPriorityTaskWoken"被设置为pdTRUE,
    这表示在中断返回之前要进行任务切换。

    ulTaskNotifyTake函数的参数说明如下:

    参数 说明
    xClearCountOnExit 函数返回前是否清零:
    pdTRUE:把通知值清零
    pdFALSE:如果通知值大于0,则把通知值减一
    xTicksToWait 任务进入阻塞态的超时时间,它在等待通知值大于0。
    0:不等待,即刻返回;
    portMAX_DELAY:一直等待,直到通知值大于0;
    其他值:Tick Count,可以用pdMS_TO_TICKS()把ms转换为Tick Count
    返回值 函数返回之前,在清零或减一之前的通知值。
    如果xTicksToWait非0,则返回值有2种情况:
    1. 大于0:在超时前,通知值被增加了
    2. 等于0:一直没有其他任务增加通知值,最后超时返回0

    9.2.3 xTaskNotify/xTaskNotifyWait

    xTaskNotify 函数功能更强大,可以使用不同参数实现各类功能,比如:

    • 让接收任务的通知值加一:这时xTaskNotify()等同于xTaskNotifyGive()
    • 设置接收任务的通知值的某一位、某些位,这就是一个轻量级的、更高效的事件组
    • 把一个新值写入接收任务的通知值:上一次的通知值被读走后,写入才成功。这就是轻量级的、长度为1的队列
    • 用一个新值覆盖接收任务的通知值:无论上一次的通知值是否被读走,覆盖都成功。类似xQueueOverwrite()函数,这就是轻量级的邮箱。

    xTaskNotify()xTaskNotifyGive()更灵活、强大,使用上也就更复杂。xTaskNotifyFromISR()是它对应的ISR版本。

    这两个函数用来发出任务通知,使用哪个函数来取出任务通知呢?

    使用xTaskNotifyWait()函数!它比ulTaskNotifyTake()更复杂:

    • 可以让任务等待(可以加上超时时间),等到任务状态为"pending"(也就是有数据)
    • 还可以在函数进入、退出时,清除通知值的指定位

    这几个函数的原型如下:

    BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction );
    
    BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
                                   uint32_t ulValue, 
                                   eNotifyAction eAction, 
                                   BaseType_t *pxHigherPriorityTaskWoken );
    
    BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, 
                                uint32_t ulBitsToClearOnExit, 
                                uint32_t *pulNotificationValue, 
                                TickType_t xTicksToWait );
    

    xTaskNotify函数的参数说明如下:

    参数 说明
    xTaskToNotify 任务句柄(创建任务时得到),给哪个任务发通知
    ulValue 怎么使用ulValue,由eAction参数决定
    eAction 见下表
    返回值 pdPASS:成功,大部分调用都会成功
    pdFAIL:只有一种情况会失败,当eAction为eSetValueWithoutOverwrite,
    并且通知状态为"pending"(表示有新数据未读),这时就会失败。

    eNotifyAction参数说明:

    eNotifyAction取值 说明
    eNoAction 仅仅是更新通知状态为"pending",未使用ulValue。
    这个选项相当于轻量级的、更高效的二进制信号量。
    eSetBits 通知值 = 原来的通知值 | ulValue,按位或。
    相当于轻量级的、更高效的事件组。
    eIncrement 通知值 = 原来的通知值 + 1,未使用ulValue。
    相当于轻量级的、更高效的二进制信号量、计数型信号量。
    相当于xTaskNotifyGive()函数。
    eSetValueWithoutOverwrite 不覆盖。
    如果通知状态为"pending"(表示有数据未读),
    则此次调用xTaskNotify不做任何事,返回pdFAIL。
    如果通知状态不是"pending"(表示没有新数据),
    则:通知值 = ulValue。
    eSetValueWithOverwrite 覆盖。
    无论如何,不管通知状态是否为"pendng",
    通知值 = ulValue。

    xTaskNotifyFromISR函数跟xTaskNotify很类似,就多了最后一个参数pxHigherPriorityTaskWoken。在很多ISR函数中,这个参数的作用都是类似的,使用场景如下:

    • 被通知的任务,可能正处于阻塞状态
    • xTaskNotifyFromISR函数发出通知后,会把接收任务从阻塞状态切换为就绪态
    • 如果被唤醒的任务的优先级,高于当前任务的优先级,则"*pxHigherPriorityTaskWoken"被设置为pdTRUE,这表示在中断返回之前要进行任务切换。

    xTaskNotifyWait函数列表如下:

    参数 说明
    ulBitsToClearOnEntry 在xTaskNotifyWait入口处,要清除通知值的哪些位?
    通知状态不是"pending"的情况下,才会清除。
    它的本意是:我想等待某些事件发生,所以先把"旧数据"的某些位清零。
    能清零的话:通知值 = 通知值 & ~(ulBitsToClearOnEntry)。
    比如传入0x01,表示清除通知值的bit0;
    传入0xffffffff即ULONG_MAX,表示清除所有位,即把值设置为0
    ulBitsToClearOnExit 在xTaskNotifyWait出口处,如果不是因为超时推出,而是因为得到了数据而退出时:
    通知值 = 通知值 & ~(ulBitsToClearOnExit)。
    在清除某些位之前,通知值先被赋给"*pulNotificationValue"。
    比如入0x03,表示清除通知值的bit0、bit1;
    传入0xffffffff即ULONG_MAX,表示清除所有位,即把值设置为0
    pulNotificationValue 用来取出通知值。
    在函数退出时,使用ulBitsToClearOnExit清除之前,把通知值赋给"*pulNotificationValue"。
    如果不需要取出通知值,可以设为NULL。
    xTicksToWait 任务进入阻塞态的超时时间,它在等待通知状态变为"pending"。
    0:不等待,即刻返回;
    portMAX_DELAY:一直等待,直到通知状态变为"pending";
    其他值:Tick Count,可以用pdMS_TO_TICKS()把ms转换为Tick Count
    返回值 1. pdPASS:成功
    这表示xTaskNotifyWait成功获得了通知:
    可能是调用函数之前,通知状态就是"pending";
    也可能是在阻塞期间,通知状态变为了"pending"。
    2. pdFAIL:没有得到通知。

    9.3 示例22: 传输计数值

    本节源码是FreeRTOS_22_tasknotify_tansfer_count,基于FreeRTOS_13_semaphore_circle_buffer修改。

    本程序创建2个任务:

    • 发送任务:把数据写入唤醒缓冲区,使用xTaskNotifyGive()让通知值加一
    • 接收任务:使用ulTaskNotifyTake()取出通知值,这表示字符数,打印字符

    main函数代码如下:

    int main( void )
    {
    	prvSetupHardware();
    
    	/* 创建1个任务用于发送任务通知
    	 * 优先级为2
    	 */
    	xTaskCreate( vSenderTask, "Sender", 1000, NULL, 2, NULL );
    
    	/* 创建1个任务用于接收任务通知
    	 * 优先级为1
    	 */
    	 xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, &xRecvTask );
    
    	/* 启动调度器 */
    	vTaskStartScheduler();
    
    	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
    	return 0;
    }
    

    发送任务、接收任务的代码和执行流程如下:

    • A:发送任务优先级最高,先执行。连续存入3个字符、发出3次任务通知:通知值累加为3
    • B:发送任务阻塞,让接收任务能执行
    • C:接收任务读到通知值为3,并把通知值清零
    • D:把3个字符依次读出、打印
    • E:再次读取任务通知,阻塞

    在这里插入图片描述

    运行结果如下图所示:

    在这里插入图片描述

    本程序使用xTaskNotifyGive/ulTaskNotifyTake实现了轻量级的计数型信号量,代码更简单:

    • 无需创建信号量
    • 消耗内存更少
    • 效率更高

    信号量是个公开的资源,任何任务、ISR都可以使用它:可以释放、获取信号量。

    而本节程序中,发送任务只能给指定的任务发送通知,目标明确;接收任务只能从自己的通知值中得到数据,来源明确。

    9.4 示例23: 传输任意值

    本节源码是FreeRTOS_23_tasknotify_tansfer_value

    在上述例子中使用任务通知来传输计数值、传输通知。

    本节程序使用任务通知来传输任意数据,它创建2个任务:

    • 发送任务:把数据通过xTaskNotify()发送给其他任务
    • 接收任务:使用xTaskNotifyWait取出通知值,这表示字符,并打印出来

    main函数代码如下:

    int main( void )
    {
    	prvSetupHardware();
    
    	/* 创建1个任务用于发送任务通知
    	 * 优先级为2
    	 */
    	xTaskCreate( vSenderTask, "Sender", 1000, NULL, 2, NULL );
    
    	/* 创建1个任务用于接收任务通知
    	 * 优先级为1
    	 */
    	 xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, &xRecvTask );
    
    	/* 启动调度器 */
    	vTaskStartScheduler();
    
    	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
    	return 0;
    }
    

    发送任务、接收任务的代码和执行流程如下:

    • A:发送任务优先级最高,先执行。连续给对方任务发送3个字符,只成功了1次
    • B:发送任务阻塞,让接收任务能执行
    • C:接收任务读取通知值
    • D:把读到的通知值作为字符打印出来
    • E:再次读取任务通知,阻塞

    在这里插入图片描述

    运行结果如下图所示:

    在这里插入图片描述

    本程序使用xTaskNotify/xTaskNotifyWait实现了轻量级的队列(该队列长度只有1),代码更简单:

    • 无需创建队列
    • 消耗内存更少
    • 效率更高

    队列是个公开的资源,任何任务、ISR都可以使用它:可以存入数据、取出数据。

    而本节程序中,发送任务只能给指定的任务发送通知,目标明确;接收任务只能从自己的通知值中得到数据,来源明确。

    注意:任务通知值只有一个,数据可能丢失,设计程序时要考虑这点。

    获取更多嵌入式干货,请关注威信baiwenkeji
  • 相关阅读:
    java.util.Date和java.sql.Date的区别及应用
    powderdesinger显示中英文表名
    Axure实现提示文本单击显示后自动消失的效果
    如何把Java的double类型变量保留两位小数
    MyBatis查询,返回值Map或List<Map>
    Spring Security整合JWT,实现单点登录,So Easy~!
    win10+mysql8.0安装
    Intellij IDEA导入JAVA项目并启动(哈哈哈,天天都有人问)
    色彩设计基础知识整理
    前端开发者必备的Nginx知识
  • 原文地址:https://www.cnblogs.com/weidongshan/p/15624143.html
Copyright © 2020-2023  润新知