FreeRTOS-05-队列

说明

本文仅作为学习FreeRTOS的记录文档,作为初学者肯定很多理解不对甚至错误的地方,望网友指正。
FreeRTOS是一个RTOS(实时操作系统)系统,支持抢占式、合作式和时间片调度。适用于微处理器或小型微处理器的实时应用。
本文档使用的FreeRTOS版本:FreeRTOS Kernel V10.4.1
参考文档:《FreeRTOS_Reference_Manual_V10.0.0.pdf》《FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf》《STM32F4 FreeRTOS开发手册_V1.1.pdf》
参考视频:正点原子FreeRTOS手把手教学-基于STM32_哔哩哔哩_bilibili

8 队列

8.1 简介

队列是为了任务与任务、任务与中断之间的通信而准备的,可以在任务与任务、任务与中断之间传递消息,队列中可以存储有限的、大小固定的数据项目。队列所能保存的最大数据项目数量叫做队列的长度,创建队列的时候会指定数据项目的大小和队列的长度。队列是用来传递消息的,所以也称为消息队列。信号量也是依据队列实现的。

8.1.1 数据存储

队列采用先进先出(FIFO)的存储缓冲机制,往队列中放数据叫做入队(放队尾),从队列中取数据叫做出队(从队头)。也可以使用LIFO的存储缓冲,也就是后进先出,FreeRTOS也提供了LIFO的存储缓冲机制。

数据存放到队列中会导致值拷贝,也就是放数据到队列中,而不是数据的指针,这叫值传递。采用值传递,当消息放到队列中后原始的数据缓冲区就可以删除,缓冲区就可以重复使用。FreeRTOS使用的是数据拷贝,但是也可以采用引用(指针)来传递消息,直接往队列中放入发送消息缓冲区的地址的指针。

8.1.2 多任务访问

队列不属于某个特定的任务,任何任务都可以向队列中发送消息,或者从队列中提取消息。

8.1.3 出队阻塞

当任务从一个队列中提取消息的时候可以指定一个阻塞时间,这个阻塞时间就是当任务从队列中提取消息无效的时候任务阻塞的时间。出队就是从队列中提取消息,出队阻塞是针对从队列中提取消息的任务而言的。阻塞时间的单位是时钟节拍数,阻塞时间为0就是不阻塞。如果阻塞时间是0~portMAX_DELAY,当任务没有从队列中获取到消息的话就会进入阻塞态,阻塞时间指定了任务进入阻塞态的时间,当阻塞时间到了以后还没有接收到消息就立即退出阻塞态;如果在阻塞时间收到了数据就立即返回。当阻塞时间设置为portMAX_DELAY,任务就会一直进入阻塞态等待,直到接收到数据为止。

8.1.4 入队阻塞

入队就是往队列中发送消息,将消息加入到队列中。入队也可以设置阻塞时间。比如入队时,队列已经满了。

8.1.5 队列操作过程

创建队列:创建一个队列,用于任务A和任务B之间通信,队列数据项目的个数为5,创建时队列为空。

image-20210808195543746

往队列中发送第一个消息:任务A往队列中发送了一个消息,值为10。

image-20210808195721559

往队列中再发送一个消息:任务A往队列中再发送了一个消息,值为20,这时队列剩余空间大小为3。

image-20210808200109085

从队列中取一个消息:任务B从队列中取一个消息,从队列头开始取(值为10),队列中剩下一个消息,剩余空间大小为4。

image-20210808200219586

下图演示了一次完整的任务A和任务B通过队列传递消息的过程:

img

8.2 队列结构体

队列结构体为Queue_t,在queue.c中定义:

typedef struct QueuePointers
{
    int8_t * pcTail;      //指向队列存储区最后一个字节
    int8_t * pcReadFrom;  //作为队列使用时指向最后一个出队的队列项首地址
} QueuePointers_t;

typedef struct SemaphoreData
{
    TaskHandle_t xMutexHolder;
    UBaseType_t uxRecursiveCallCount;  //作为递归互斥量的时候用来记录递归互斥量被调用的次数
} SemaphoreData_t;

typedef struct QueueDefinition
{
    int8_t * pcHead;     //指向队列存储区开始地址
    int8_t * pcWriteTo;  //指向存储区中下一个空闲区域

    union
    {
        QueuePointers_t xQueue;
        SemaphoreData_t xSemaphore;
    } u;

    List_t xTasksWaitingToSend;     //等待发送任务列表,因队满导致入队失败而阻塞的任务挂在这个列表上
    List_t xTasksWaitingToReceive;  //等待接收任务列表,因队空导致出队失败而阻塞的任务挂在这个列表上

    volatile UBaseType_t uxMessagesWaiting; //队列中当前数据项数量,也就是消息数
    UBaseType_t uxLength;   //队列创建时指定的队列长度,队列允许的最大队列项个数
    UBaseType_t uxItemSize; //队列创建时指定的每个队列项最大长度,单位字节

    volatile int8_t cRxLock;  //当队列上锁后用来统计接收到的队列项个数,也就是出队的队列项个数
    volatile int8_t cTxLock;  //当队列上锁后用来统计发送到队列的队列项个数,也就是入队的队列项个数

    #if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
        uint8_t ucStaticallyAllocated;  //如果使用静态存储,则设置字段值为pdTURE
    #endif

    #if ( configUSE_QUEUE_SETS == 1 )
        struct QueueDefinition * pxQueueSetContainer;
    #endif

    #if ( configUSE_TRACE_FACILITY == 1 )
        UBaseType_t uxQueueNumber;
        uint8_t ucQueueType;
    #endif
} xQUEUE;

typedef xQUEUE Queue_t;

8.3 队列创建

8.3.1 动态队列创建

函数原型:

#include “FreeRTOS.h”
#include “queue.h”
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,
                           UBaseType_t uxItemSize );

函数描述:创建一个新的队列,并返回指向队列的句柄。使用这个函数需要将宏configSUPPORT_DYNAMIC_ALLOCATION置为1。每一个队列需要存储空间来存放队列的状态和队列项,如果使用xQueueCreate()创建队列,则存储空间由系统自动分配。如果使用xQueueCreateStatic()创建,存储空间由用户指定。

函数参数:uxQueueLength:要创建的队列项个数。uxItemSize:单个队列项的大小,单位为字节。

返回值:NULL:表示分配存储空间失败。其它值:队列创建成功,返回值是指向创建队列的句柄。

8.3.2 静态队列创建

函数原型:

#include “FreeRTOS.h”
#include “queue.h”
QueueHandle_t xQueueCreateStatic( UBaseType_t uxQueueLength,
                                 UBaseType_t uxItemSize,
                                 uint8_t *pucQueueStorageBuffer,
                                 StaticQueue_t *pxQueueBuffer );

函数描述:创建一个新的队列,并返回指向队列的句柄。存储空间由用户指定。

函数参数:uxQueueLength:要创建的队列项个数。

uxItemSize:单个队列项的大小,单位为字节。

pucQueueStorageBuffer:指向队列项目的存储区,也就是消息存储区,存储区要大于等于uxQueueLength * uxItemSizede 字节。

pxQueueBuffer:指向用来存放队列结构体的空间。

返回值:NULL:表示分配存储空间失败。其它值:队列创建成功,返回值是指向创建队列的句柄。

队列创建的具体实现过程可以参考源码,这里给出创建一个有4个队列项,每个队列项长度为32字节的队列成功的示意图。

image-20210808204617758

8.4 向队列发送消息

向队列中发送消息分为任务级入队函数和中断级入队函数。

8.4.1 任务级入队函数

任务级入队函数有3个,分别为xQueueSend()、xQueueSendToFront()、xQueueSendToBack() 、xQueueOverwrite()。

先介绍xQueueSend()、xQueueSendToFront()、xQueueSendToBack() 。

函数原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xQueueSend( QueueHandle_t xQueue, 
                      const void * pvItemToQueue,
                      TickType_t xTicksToWait );
BaseType_t xQueueSendToFront( QueueHandle_t xQueue, 
                             const void * pvItemToQueue,
                             TickType_t xTicksToWait );
BaseType_t xQueueSendToBack( QueueHandle_t xQueue,
                            const void * pvItemToQueue,
                            TickType_t xTicksToWait );

函数描述:发送一个队列项到队列头或者队列尾。xQueueSend()和xQueueSendToBack()是同样的操作,都是发送数据到队列的尾部,xQueueSend()是原始版本,当前使用xQueueSendToBack()替换它。xQueueSendToFront()是发送数据到队列项的首部。

函数参数:xQueue:队列句柄,由创建队列的函数返回。

pvItemToQueue:指向待发送的队列项的指针,队列项是拷贝到队列中的。队列项的大小由创建队列的时候指定。

xTicksToWait:阻塞时间,此参数表示队列满的时候任务进入阻塞态等待队列空闲的最大时间。如果为0,任务立即返回;如果为portMAX_DELAY,任务会一直死等,宏portMAX_DELAY需要置为1;其余值为等到的时钟计数值,可使用宏pdMS_TO_TICKS()转换时钟计数值为毫秒。

返回值:pdPASS:数据成功发送到队列;errQUEUE_FULL:队列满,消息发送失败。

另外还有xQueueOverwrite()函数为消息覆盖写函数。

函数原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xQueueOverwrite( QueueHandle_t xQueue, const void *pvItemToQueue );

函数描述:向队列中发送数据,当队列满了以后会覆盖掉旧的数据,不管这个旧的数据有没有被其它任务或中断取走。这个函数常用于向那些长度为1的队列发送消息。

函数参数:xQueue:队列句柄,由创建队列的函数返回。

pvItemToQueue:指向待发送的队列项的指针,队列项是拷贝到队列中的。队列项的大小由创建队列的时候指定。

返回值:pdPASS:数据成功发送到队列,因为队列满了之后会覆盖写,所以不存在失败的情况。

8.4.2 中断级入队函数

任务级入队函数有3个,分别为xQueueSendFromISR()、xQueueSendToBackFromISR()、xQueueSendToFrontFromISR() 、xQueueOverwriteFromISR ()。

先介绍xQueueSendFromISR()、xQueueSendToBackFromISR()、xQueueSendToFrontFromISR()。

函数原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xQueueSendFromISR( QueueHandle_t xQueue,
                             const void *pvItemToQueue,
                             BaseType_t *pxHigherPriorityTaskWoken );
BaseType_t xQueueSendToBackFromISR( QueueHandle_t xQueue,
                                   const void *pvItemToQueue,
                                   BaseType_t *pxHigherPriorityTaskWoken );
BaseType_t xQueueSendToFrontFromISR( QueueHandle_t xQueue,
                                    const void *pvItemToQueue,
                                    BaseType_t *pxHigherPriorityTaskWoken );

函数描述:发送一个队列项到队列头或者队列尾,用于中断服务函数中。xQueueSendFromISR()和xQueueSendToBackFromISR()是同样的操作,都是发送数据到队列的尾部。xQueueSendToFrontFromISR()是发送数据到队列项的首部。和任务级入队函数不同的是中断级入队函数不允许指定任务的阻塞时间。

函数参数:xQueue:队列句柄,由创建队列的函数返回。

pvItemToQueue:指向待发送的队列项的指针,队列项是拷贝到队列中的。队列项的大小由创建队列的时候指定。

pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,这个变量的值由函数指定,用户不进行设置,用户仅需要提供一个变量来保存值。当此值为pdTRUE的时候在退出中断服务函数之前一定要进行一次任务切换。

返回值:pdPASS:数据成功发送到队列;errQUEUE_FULL:队列满,消息发送失败。

另外xQueueOverwriteFromISR()函数为消息覆盖写函数。

函数原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xQueueOverwriteFromISR( QueueHandle_t xQueue,
                                  const void *pvItemToQueue,
                                  BaseType_t *pxHigherPriorityTaskWoken );

函数描述:向队列中发送数据,当队列满了以后会覆盖掉旧的数据,从队尾写入。

函数参数:xQueue:队列句柄,由创建队列的函数返回。

pvItemToQueue:指向待发送的队列项的指针,队列项是拷贝到队列中的。队列项的大小由创建队列的时候指定。

pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,这个变量的值由函数指定,用户不进行设置,用户仅需要提供一个变量来保存值。当此值为pdTRUE的时候在退出中断服务函数之前一定要进行一次任务切换。

返回值:pdPASS:数据成功发送到队列,因为队列满了之后会覆盖写,所以不存在失败的情况。

8.5 从队列中读取消息

出队就是从队列中获取队列项。也分为任务级出队函数和中断级出队函数。

8.4.1 任务级出队函数

任务级出队函数有2个,分别为xQueueReceive()、xQueuePeek()。

函数原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xQueueReceive( QueueHandle_t xQueue,
                         void *pvBuffer,
                         TickType_t xTicksToWait );

函数描述:从队列中读取一个队列项,读取成功以后就会将队列中的这条消息删除。读取消息时是采用的拷贝方式,所以用户需要提供一个数组或缓冲区来保存读取到的数据,所读取的数据长度是队列创建的时候设定的每个队列项的长度。

函数参数:xQueue:队列句柄,由创建队列的函数返回。

pvBuffer:保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。

xTicksToWait:阻塞时间,此参数表示队列空的时候任务进入阻塞态等待队列空闲的最大时间。如果为0,任务立即返回;如果为portMAX_DELAY,任务会一直死等,宏portMAX_DELAY需要置为1;其余值为等到的时钟计数值,可使用宏pdMS_TO_TICKS()转换时钟计数值为毫秒。

返回值:pdPASS:成功从队列中读取到数据;errQUEUE_EMPTY:队列空,消息读取失败。

函数原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xQueuePeek( QueueHandle_t xQueue,
                      void *pvBuffer,
                      TickType_t xTicksToWait );

函数描述:从队列中读取一个队列项,读取成功以后不会将队列中的这条消息删除。下一次读取的时候仍然可以从这个队列中读取这个消息。读取消息时是采用的拷贝方式,所以用户需要提供一个数组或缓冲区来保存读取到的数据,所读取的数据长度是队列创建的时候设定的每个队列项的长度。

函数参数:xQueue:队列句柄,由创建队列的函数返回。

pvBuffer:保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。

xTicksToWait:阻塞时间,此参数表示队列空的时候任务进入阻塞态等待队列空闲的最大时间。如果为0,任务立即返回;如果为portMAX_DELAY,任务会一直死等,宏portMAX_DELAY需要置为1;其余值为等到的时钟计数值,可使用宏pdMS_TO_TICKS()转换时钟计数值为毫秒。

返回值:pdPASS:成功从队列中读取到数据;errQUEUE_EMPTY:队列空,消息读取失败。

8.4.2 中断级出队函数

中断级出队函数xQueueReceiveFromISR()、xQueuePeekFromISR ()。

函数原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxHigherPriorityTaskWoken );

函数描述:用于在中断服务函数中从队列中读取一条消息,读取成功后就会将队列中的这条数据删除。读取消息的方式采用值拷贝方式,需要用户提供一个数组或者缓冲区来保存读取到的数据,所读取的数据长度是创建队列的时候指定的每个队列项的长度。

函数参数:xQueue:队列句柄,由创建队列的函数返回。

pvBuffer:保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。

pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,这个变量的值由函数指定,用户不进行设置,用户仅需要提供一个变量来保存值。当此值为pdTRUE的时候在退出中断服务函数之前一定要进行一次任务切换。

返回值:pdPASS:成功从队列中读取到数据;pdFAIL:队列空,消息读取失败。

函数原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue, void *pvBuffer );

函数描述:xQueuePeek()的中断版本,函数读取成功以后不会将消息删除。

函数参数:xQueue:队列句柄,由创建队列的函数返回。

pvBuffer:保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。

返回值:pdPASS:成功从队列中读取到数据;pdFAIL:队列空,消息读取失败。

8.6 队列操作实验

目的:熟悉队列的使用

设计:创建两个任务,任务task0每隔2秒发送1个数据到队列中,并每隔4秒检查队列余量;task1读取队列中的消息,阻塞时间为5秒。队列的容量为4,每个队列项的大小为4字节。

测试代码:

/* task00 info */
configSTACK_DEPTH_TYPE Task00_STACK_SIZE = 5;
UBaseType_t  Task00_Priority = 1;
TaskHandle_t Task00_xHandle;

/* task01 info */
configSTACK_DEPTH_TYPE Task01_STACK_SIZE = 5;
UBaseType_t  Task01_Priority = 1;
TaskHandle_t Task01_xHandle;

#define MSG_QUEUE_NUM        4
#define MSG_QUEUE_ITEM_SIZE  4
QueueHandle_t Message_Queue;


void check_msg_queue(void)
{
    unsigned char *p = NULL;
    unsigned char msgq_remain_size = 0;
    unsigned char msgq_total_size = 0;

    taskENTER_CRITICAL();
    msgq_remain_size = uxQueueSpacesAvailable(Message_Queue);
    msgq_total_size = uxQueueMessagesWaiting(Message_Queue) + msgq_remain_size;
    PRINT(" total size:%d  remain size:%d", msgq_total_size, msgq_remain_size);
    taskEXIT_CRITICAL();
}


void vTask00_Code(void *para)
{
    static unsigned int cnt = 0;
    BaseType_t err;
    unsigned int item = 0;
    for (;;)
    {
        PRINT(" task00 cnt %u...", cnt);
        if (cnt%2 == 0) {
            item = cnt*100 + 1;
            err = xQueueSend(Message_Queue, &item, pdMS_TO_TICKS(5000));
            if (err == errQUEUE_FULL)
                PRINT("queue full!");
        }
        if (cnt%4 == 0)
            check_msg_queue();
        cnt++;
        vTaskDelay(1000);
    }
}

void vTask01_Code(void *para)
{
    static unsigned int cnt = 0;
    unsigned int item = 0;
    BaseType_t err;
    for (;;)
    {
        PRINT(" task01 cnt %u...", cnt);
        err = xQueueReceive(Message_Queue, &item, pdMS_TO_TICKS(5000));
        if (err == errQUEUE_EMPTY)
            PRINT("queue empty!");
        else
            PRINT(" item: %d", item);
        cnt++;
        vTaskDelay(1000);
    }
}

void test_queue()
{
    Message_Queue = xQueueCreate(MSG_QUEUE_NUM, MSG_QUEUE_ITEM_SIZE);
    
    if (xTaskCreate(vTask00_Code, "task00 task", 
        Task00_STACK_SIZE, NULL, Task00_Priority,
        &Task00_xHandle) != pdPASS)
    {
        PRINT("creat task00 failed!\n");
    }
    
    if (xTaskCreate(vTask01_Code, "task01 task", 
        Task01_STACK_SIZE, NULL, Task01_Priority,
        &Task01_xHandle) != pdPASS)
    {
        PRINT("creat task01 failed!\n");
    }
}

void creat_task(void)
{
    test_queue();
}

编译、运行,结果如下:

$ ./build/freertos-simulator 
 task00 cnt 0...
 total size:4  remain size:3
 task01 cnt 0...
 item: 1
 task00 cnt 1...
 task01 cnt 1...
 task00 cnt 2...
 item: 201
 task00 cnt 3...
 task01 cnt 2...
 task00 cnt 4...
 total size:4  remain size:3
 item: 401
 task00 cnt 5...
 task01 cnt 3...
 task00 cnt 6...
 item: 601
 task00 cnt 7...
 task01 cnt 4...

可以看出,task01会阻塞性的等待队列中的值(见item:201)。

下面测试队列空的情况,task00不往队列中放入数据:

void vTask00_Code(void *para)
{
    static unsigned int cnt = 0;
    BaseType_t err;
    unsigned int item = 0;
    for (;;)
    {
        PRINT(" task00 cnt %u...", cnt);
        if (cnt%4 == 0)
            check_msg_queue();
        cnt++;
        vTaskDelay(1000);
    }
}

void vTask01_Code(void *para)
{
    static unsigned int cnt = 0;
    unsigned int item = 0;
    BaseType_t err;
    for (;;)
    {
        PRINT(" task01 cnt %u...", cnt);
        err = xQueueReceive(Message_Queue, &item, pdMS_TO_TICKS(5000));
        if (err == errQUEUE_EMPTY)
            PRINT("queue empty!");
        else
            PRINT(" item: %d", item);
        cnt++;
        vTaskDelay(1000);
    }
}

编译、运行,结果如下,task01阻塞了5秒:

$ ./build/freertos-simulator 
 task00 cnt 0...
 total size:4  remain size:4
 task01 cnt 0...
 task00 cnt 1...
 task00 cnt 2...
 task00 cnt 3...
 task00 cnt 4...
 total size:4  remain size:4
queue empty!
 task00 cnt 5...
 task01 cnt 1...
 task00 cnt 6...

下面测试队列空的情况,task01不从队列中取数据:

void vTask00_Code(void *para)
{
    static unsigned int cnt = 0;
    BaseType_t err;
    unsigned int item = 0;
    for (;;)
    {
        PRINT(" task00 cnt %u...", cnt);
        item = cnt*100 + 1;
        err = xQueueSend(Message_Queue, &item, pdMS_TO_TICKS(5000));
        if (err == errQUEUE_FULL)
        	PRINT("queue full!");
        if (cnt%4 == 0)
            check_msg_queue();
        cnt++;
        vTaskDelay(1000);
    }
}

void vTask01_Code(void *para)
{
    static unsigned int cnt = 0;
    unsigned int item = 0;
    BaseType_t err;
    for (;;)
    {
        PRINT(" task01 cnt %u...", cnt);
        cnt++;
        vTaskDelay(1000);
    }
}

编译、运行,结果如下,task00往队列中放了4个数据项之后,就阻塞了,每次阻塞时间为5秒:

$ ./build/freertos-simulator 
 task00 cnt 0...
 total size:4  remain size:3
 task01 cnt 0...
 task00 cnt 1...
 task01 cnt 1...
 task00 cnt 2...
 task01 cnt 2...
 task00 cnt 3...
 task01 cnt 3...
 task00 cnt 4...
 task01 cnt 4...
 task01 cnt 5...
 task01 cnt 6...
 task01 cnt 7...
 task01 cnt 8...
queue full!
 total size:4  remain size:0
 task01 cnt 9...
 task00 cnt 5...
 task01 cnt 10...
 task01 cnt 11...
 task01 cnt 12...
 task01 cnt 13...
 task01 cnt 14...
queue full!
 task01 cnt 15...
 task00 cnt 6...
posted @ 2021-08-16 19:38  zhengcixi  阅读(711)  评论(0编辑  收藏  举报
回到顶部