• FreeRTOS队列(queue)


    队列(queue)可以用于"任务到任务"、 "任务到中断"、 "中断到任务"直接传输信息。

    1、队列的特性

    1、1常规操作

    队列的简化操如下图所示,从此图可知:

    • 队列中可以包含若干数据:队列中有若干项,这被称为“长度”;
    • 每个数据大小固定;
    • 创建队列时就要指定长度、数据大小;
    • 数据的操作采用先进先出的方法:写数据时放入尾部,读数据时从头部读;
    • 也可以强制写队列头部:覆盖头部数据。

    1、2 数据传输的两种方式

    使用队列传输数据时有两种方法:

    拷贝:把数据、变量的值复制进队列里;

    引用:把数据、变量的地址复制进队列里。

    FreeRTOS 使用拷贝值的方法,这更简单:
    ⚫ 局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据;
    ⚫ 无需分配 buffer 来保存数据,队列中有 buffer;
    ⚫ 局部变量可以马上再次使用;
    ⚫ 发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据;
    ⚫ 如果数据实在太大,你还是可以使用队列传输它的地址;
    ⚫ 队列的空间有 FreeRTOS 内核分配,无需任务操心;
    ⚫ 对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把数据复制出队列。

    1、3队列的阻塞访问

            只要知道队列的句柄,谁都可以读、写该队列。任务、 ISR 都可读、写队列。可以多个任务读写队列。
            任务读写队列时,简单地说:如果读写不成功,则阻塞;可以指定超时时间。口语化地说,就是可以定个闹钟:如果能读写了就马上进入就绪态,否则就阻塞直到超时。
            某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞状态:还可以指定阻塞的时间。如果队列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。
            既然读取队列的任务个数没有限制,那么当多个任务读取空队列时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态?


    ⚫ 优先级最高的任务
    ⚫ 如果大家的优先级相同,那等待时间最久的任务会进入就绪态

            跟读队列类似,一个任务要写队列时,如果队列满了,该任务也可以进入阻塞状态:还可以指定阻塞的时间。如果队列有空间了,则该阻塞的任务会变为就绪态。如果一直都没有空间,则时间到之后它也会进入就绪态。既然写队列的任务个数没有限制,那么当多个任务写"满队列"时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的空间。当队列中有空间时,哪个任务会进入就绪态?


    ⚫ 优先级最高的任务
    ⚫ 如果大家的优先级相同,那等待时间最久的任务会进入就绪态

    2、队列函数

    使用队列的流程:创建队列、写队列、读队列、删除队列。

    2.1创建

    队列的创建有两种方法:动态分配内存、静态分配内存
     

    1、动态分配内存: xQueueCreate,队列的内存在函数内部动态分配
     

    QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );

    2、静态分配内存: xQueueCreateStatic,队列的内存要事先分配好
    函数原型如下:
     

    1. QueueHandle_t xQueueCreateStatic(
    2. UBaseType_t uxQueueLength,
    3. UBaseType_t uxItemSize,
    4. uint8_t *pucQueueStorageBuffer,
    5. StaticQueue_t *pxQueueBuffer
    6. )

    示例代码:

    1. // 示例代码
    2. #define QUEUE_LENGTH 10
    3. #define ITEM_SIZE sizeof( uint32_t )
    4. // xQueueBuffer 用来保存队列结构体
    5. StaticQueue_t xQueueBuffer;
    6. // ucQueueStorage 用来保存队列的数据
    7. // 大小为: 队列长度 * 数据大小
    8. uint8_t ucQueueStorage[ QUEUE_LENGTH * ITEM_SIZE ];
    9. void vATask( void *pvParameters )
    10. {
    11. QueueHandle_t xQueue1;
    12. // 创建队列: 可以容纳 QUEUE_LENGTH 个数据,每个数据大小是 ITEM_SIZE
    13. xQueue1 = xQueueCreateStatic( QUEUE_LENGTH,
    14. ITEM_SIZE,
    15. ucQueueStorage,
    16. &xQueueBuffer );
    17. }

    2.2 复位

            队列刚被创建时,里面没有数据;使用过程中可以调用 xQueueReset()把队列恢复为初始状态,此函数原型为:

    1. /* pxQueue : 复位哪个队列;
    2. * 返回值: pdPASS(必定成功)
    3. */
    4. BaseType_t xQueueReset( QueueHandle_t pxQueue);

    2.3删除

            删除队列的函数为 vQueueDelete(),只能删除使用动态方法创建的队列,它会释放内存。 原型如下:

    void vQueueDelete( QueueHandle_t xQueue );

    2.4写队列

            可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR 中使用。函数原型如下:
          

    1. /* 等同于xQueueSendToBack
    2. * 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
    3. */
    4. BaseType_t xQueueSend(
    5. QueueHandle_t xQueue,
    6. const void *pvItemToQueue,
    7. TickType_t xTicksToWait
    8. );
    9. /*
    10. * 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
    11. */
    12. BaseType_t xQueueSendToBack(
    13. QueueHandle_t xQueue,
    14. const void *pvItemToQueue,
    15. TickType_t xTicksToWait
    16. );
    17. /*
    18. * 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
    19. */
    20. BaseType_t xQueueSendToBackFromISR(
    21. QueueHandle_t xQueue,
    22. const void *pvItemToQueue,
    23. BaseType_t *pxHigherPriorityTaskWoken
    24. );
    25. /*
    26. * 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
    27. */
    28. BaseType_t xQueueSendToFront(
    29. QueueHandle_t xQueue,
    30. const void *pvItemToQueue,
    31. TickType_t xTicksToWait
    32. );
    33. /*
    34. * 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
    35. */
    36. BaseType_t xQueueSendToFrontFromISR(
    37. QueueHandle_t xQueue,
    38. const void *pvItemToQueue,
    39. BaseType_t *pxHigherPriorityTaskWoken
    40. );

      

    2.5读队列

            使用 xQueueReceive()函数读队列,读到一个数据后,队列中该数据会被移除。这个
    函数有两个版本:在任务中使用、在 ISR 中使用。函数原型如下:

    1. BaseType_t xQueueReceive( QueueHandle_t xQueue,
    2. void * const pvBuffer,
    3. TickType_t xTicksToWait );
    4. BaseType_t xQueueReceiveFromISR(
    5. QueueHandle_t xQueue,
    6. void *pvBuffer,
    7. BaseType_t *pxTaskWoken
    8. );

    2.6查询

    可以查询队列中有多少个数据、有多少空余空间。函数原型如下:
     

    1. /*
    2. * 返回队列中可用数据的个数
    3. */
    4. UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
    5. /*
    6. * 返回队列中可用空间的个数
    7. */
    8. UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );

    2.7覆盖 or偷看

    当队列长度为 1 时,可以使用 xQueueOverwrite()或 xQueueOverwriteFromISR()
    来覆盖数据。
    注意,队列长度必须为 1。当队列满时,这些函数会覆盖里面的数据,这也以为着这
    些函数不会被阻塞
     

    1. /* 覆盖队列
    2. * xQueue: 写哪个队列
    3. * pvItemToQueue: 数据地址
    4. * 返回值: pdTRUE表示成功, pdFALSE表示失败
    5. */
    6. BaseType_t xQueueOverwrite(
    7. QueueHandle_t xQueue,
    8. const void * pvItemToQueue
    9. );
    10. BaseType_t xQueueOverwriteFromISR(
    11. QueueHandle_t xQueue,
    12. const void * pvItemToQueue,
    13. BaseType_t *pxHigherPriorityTaskWoken
    14. );

    如果想让队列中的数据供多方读取,也就是说读取时不要移除数据,要留给后来人。那
    么可以使用"窥视",也就是xQueuePeek()或xQueuePeekFromISR()。这些函数会从队列中
    复制出数据,但是不移除数据。这也意味着,如果队列中没有数据,那么"偷看"时会导致阻
    塞;一旦队列中有数据,以后每次"偷看"都会成功。
     

    1. /* 偷看队列
    2. * xQueue: 偷看哪个队列
    3. * pvItemToQueue: 数据地址, 用来保存复制出来的数据
    4. * xTicksToWait: 没有数据的话阻塞一会
    5. * 返回值: pdTRUE表示成功, pdFALSE表示失败
    6. */
    7. BaseType_t xQueuePeek(
    8. QueueHandle_t xQueue,
    9. void * const pvBuffer,
    10. TickType_t xTicksToWait
    11. );
    12. BaseType_t xQueuePeekFromISR(
    13. QueueHandle_t xQueue,
    14. void *pvBuffer,
    15. );

    3、传输大数据

    FreeRTOS的队列使用拷贝传输,也就是要传输uint32_t时,把4字节的数据拷贝进队列;
    要传输一个8字节的结构体时,把8字节的数据拷贝进队列。
    如果要传输1000字节的结构体呢?写队列时拷贝1000字节,读队列时再拷贝1000字节?
    不建议这么做,影响效率!
    这时候,我们要传输的是这个巨大结构体的地址:把它的地址写入队列,对方从队列得
    到这个地址,使用地址去访问那1000字节的数据。
    使用地址来间接传输数据时,这些数据放在RAM里,对于这块RAM,要保证这几点:
    ⚫ RAM 的所有者、操作者,必须清晰明了
    这块内存,就被称为"共享内存"。要确保不能同时修改 RAM。比如,在写队列之
    前只有由发送者修改这块 RAM,在读队列之后只能由接收者访问这块 RAM。
    ⚫ RAM 要保持可用
    这块 RAM 应该是全局变量,或者是动态分配的内存。对于动然分配的内存,要
    确保它不能提前释放:要等到接收者用完后再释放。 另外,不能是局部变量。
     

    实例

    程序会创建一个队列,然后创建1个发送任务、 1个接收任务:
    ⚫ 创建的队列:长度为 1,用来传输"char *"指针
    ⚫ 发送任务优先级为 1,在字符数组中写好数据后,把它的地址写入队列
    ⚫ 接收任务优先级为 2,读队列得到"char *"值,把它打印出来


            这个程序故意设置接收任务的优先级更高,在它访问数组的过程中,接收任务无法执行、无法写这个数组。

    main函数中创建了队列、创建了发送任务、接收任务,代码如下:
     

    1. /* 定义一个字符数组 */
    2. static char pcBuffer[100];
    3. /* vSenderTask被用来创建2个任务,用于写队列
    4. * vReceiverTask被用来创建1个任务,用于读队列
    5. */
    6. static void vSenderTask( void *pvParameters );
    7. static void vReceiverTask( void *pvParameters );
    8. /*-----------------------------------------------------------*/
    9. /* 队列句柄, 创建队列时会设置这个变量 */
    10. QueueHandle_t xQueue;
    11. int main( void )
    12. {
    13. prvSetupHardware();
    14. /* 创建队列: 长度为1,数据大小为4字节(存放一个char指针) */
    15. xQueue = xQueueCreate( 1, sizeof(char *) );
    16. if( xQueue != NULL )
    17. {
    18. /* 创建1个任务用于写队列
    19. * 任务函数会连续执行,构造buffer数据,把buffer地址写入队列
    20. * 优先级为1
    21. */
    22. xTaskCreate( vSenderTask, "Sender", 1000, NULL, 1, NULL );
    23. /* 创建1个任务用于读队列
    24. * 优先级为2, 高于上面的两个任务
    25. * 这意味着读队列得到buffer地址后,本任务使用buffer时不会被打断
    26. */
    27. xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );
    28. /* 启动调度器 */
    29. vTaskStartScheduler();
    30. }
    31. else
    32. {
    33. /* 无法创建队列 */
    34. }
    35. /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
    36. return 0;
    37. }

    发送任务的函数中,现在全局大数组pcBuffer中构造数据,然后把它的地址写入队列,
    代码如下
     

    1. static void vSenderTask( void *pvParameters )
    2. {
    3. BaseType_t xStatus;
    4. static int cnt = 0;
    5. char *buffer;
    6. /* 无限循环 */
    7. for( ;; )
    8. {
    9. sprintf(pcBuffer, "www.100ask.net Msg %d\r\n", cnt++);
    10. buffer = pcBuffer; // buffer变量等于数组的地址, 下面要把这个地址写入队列
    11. /* 写队列
    12. * xQueue: 写哪个队列
    13. * pvParameters: 写什么数据? 传入数据的地址, 会从这个地址把数据复制进队列
    14. * 0: 如果队列满的话, 即刻返回
    15. */
    16. xStatus = xQueueSendToBack( xQueue, &buffer, 0 ); /* 只需要写入4字节, 无需写入
    17. 整个buffer */
    18. if( xStatus != pdPASS )
    19. {
    20. printf( "Could not send to the queue.\r\n" );
    21. }
    22. }
    23. }

    接收任务的函数中,读取队列、得到buffer的地址、打印,代码如下
     

    1. static void vReceiverTask( void *pvParameters )
    2. {
    3. /* 读取队列时, 用这个变量来存放数据 */
    4. char *buffer;
    5. const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );
    6. BaseType_t xStatus;
    7. /* 无限循环 */
    8. for( ;; )
    9. {
    10. /* 读队列
    11. * xQueue: 读哪个队列
    12. * &xReceivedStructure: 读到的数据复制到这个地址
    13. * xTicksToWait: 没有数据就阻塞一会
    14. */
    15. xStatus = xQueueReceive( xQueue, &buffer, xTicksToWait); /* 得到buffer地址,只
    16. 4字节 */
    17. if( xStatus == pdPASS )
    18. {
    19. /* 读到了数据 */
    20. printf("Get: %s", buffer);
    21. }
    22. else
    23. {
    24. /* 没读到数据 */
    25. printf( "Could not receive from the queue.\r\n" );
    26. }
    27. }
    28. }

  • 相关阅读:
    C#面向对象程序设计课程实验二: 实验名称:Windows 窗体程序
    数组去重的六种方法
    【Java 进阶篇】保护你的应用:Java 过滤器实现敏感词汇过滤
    记录第一次给开源项目提 PR
    【软件测试】自动化测试selenium
    【Gradle-13】SNAPSHOT版本检查
    快速搭建React TypeScript项目
    Leetcode数据库系列题解合集(持续更新)
    react-pdf预览在线PDF的使用
    GoF之代理模式
  • 原文地址:https://blog.csdn.net/a1233219/article/details/139708399