zoukankan      html  css  js  c++  java
  • 韦东山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
  • 相关阅读:
    用Python完成一个汇率转换器
    鸿蒙如何用JS开发智能手表App
    鸿蒙如何用JS开发智能手表App
    SAP Spartacus SplitViewComponent Migration 的一个具体例子
    SAP Spartacus B2B 页面 Popover Component 的条件显示逻辑
    SAP Spartacus 升级时关于 schematics 的更新
    SAP Spartacus B2B 页面 Disable 按钮的显示原理
    SAP Spartacus B2B 页面 Disable Confirmation 对话框的显示原理
    通过 Feature Level 动态控制 SAP Spartacus 的页面显示
    SAP Commerce Cloud Build Manifest Components
  • 原文地址:https://www.cnblogs.com/weidongshan/p/15624143.html
Copyright © 2011-2022 走看看