一、章节介绍与范围

1. 核心定位

队列是 FreeRTOS 核心通信机制,支持任务与任务、任务与中断、中断与任务之间的数据交互,本章仅讲解任务到任务的通信,任务与中断的通信在后续章节说明。

2. 学习目标

  1. 掌握队列的创建方式
  2. 理解队列的数据管理逻辑
  3. 学会向队列发送数据、从队列接收数据
  4. 理解队列阻塞的含义,以及多队列阻塞机制
  5. 掌握队列数据覆盖、队列清空操作
  6. 了解队列读写对任务优先级的影响

二、队列的核心特征

1. 数据存储规则

  1. 基础属性:队列可存放有限数量、固定大小的数据单元;队列最大容纳单元数称为长度,单个数据单元大小、队列长度均在创建时指定
  2. 读写规则:默认遵循 FIFO(先入先出),数据写入队列尾部(tail),从队列头部(head)读出;也支持向队列头部写入数据、覆盖头部已有数据。
  3. 运行示例(Task A 发数据、Task B 收数据):
    • 队列创建后默认空;Task A 写入 10,队列仅存 10;
    • Task A 再写入 20,队列头部为 10、尾部为 20;
    • Task B 读取数据,优先取出头部的 10,队列剩余 20。

2. 两种实现方式

FreeRTOS 采用数据复制方式实现队列,同时兼容指针传递,两种方式对比如下:

表格

实现方式 原理 FreeRTOS 选用原因
复制队列(FreeRTOS 默认) 逐字节将数据拷贝至队列存储空间 1. 可直接发送栈变量,函数退出后数据依然有效;2. 无需提前分配缓冲区;3. 发送任务可立即复用原变量;4. 收发任务完全解耦;5. 适配内存保护系统,可跨内存边界传数据
引用队列 队列仅存储数据指针,不存储数据本身 适用于大数据传输,减少拷贝开销;需保证指针指向内存有效、读写互斥

补充:FreeRTOS 若需使用引用模式,只需将数据指针作为队列单元存入即可。

3. 多任务访问特性

  1. 队列是独立内核对象,所有知晓队列句柄的任务 / 中断均可访问。
  2. 支持多任务写入同一个队列(常用场景)、多任务从同一个队列读取(较少用)。

4. 阻塞机制(核心)

(1)队列读取阻塞

任务读取空队列时,可指定阻塞时长:

  • 队列无数据:任务进入阻塞态,等待数据入队;
  • 数据入队:阻塞任务转为就绪态;超时无数据,任务也自动转为就绪态;
  • 多任务阻塞读:数据到来时,最高优先级任务优先解除阻塞;优先级相同时,等待时间最长的任务优先。
(2)队列写入阻塞

任务向满队列写数据时,可指定阻塞时长:

  • 队列已满:任务进入阻塞态,等待队列腾出空间;
  • 队列有空位:阻塞任务转为就绪态;超时无空间,任务也自动转为就绪态;
  • 多任务阻塞写:队列腾出空间时,最高优先级任务优先解除阻塞;优先级相同时,等待时间最长的任务优先。
(3)阻塞多个队列

支持将多个队列编入队列集合,任务可阻塞等待集合内任意队列有数据,无需轮询所有队列。

三、队列基础 API 函数(任务上下文专用,中断有独立版本)

1. 队列创建函数

(1)xQueueCreate()(动态分配内存)
  • 函数原型:

    c

    运行

    QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
    
  • 参数说明:
    • uxQueueLength:队列最大可容纳的数据单元个数;
    • uxItemSize:单个数据单元的字节大小。
  • 返回值:返回队列句柄(QueueHandle_t 类型);内存不足时返回 NULL
  • 补充:队列内存从 FreeRTOS 堆中分配。
(2)xQueueCreateStatic()(静态分配内存,V9.0.0 新增)

编译阶段预先分配内存,适合对堆内存管控严格的场景。

(3)辅助函数 xQueueReset()

将队列重置为空状态,恢复初始状态。

2. 队列数据发送函数

注意:禁止在中断中调用,中断需使用 xxxFromISR 后缀的中断安全版本。

  1. xQueueSendToBack() / xQueueSend():两者完全等价,向队列尾部写入数据(标准 FIFO 用法)。
  2. xQueueSendToFront():向队列头部写入数据,优先级高于原有数据。
  • 函数原型(以 xQueueSendToBack 为例):

    c

    运行

    BaseType_t xQueueSendToBack( QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait );
    
  • 参数说明:
    • xQueue:目标队列句柄;
    • pvItemToQueue:待发送数据的指针,内核会拷贝对应字节数据;
    • xTicksToWait:队列满时的阻塞时长(系统节拍数);0 表示不阻塞,portMAX_DELAY 表示永久阻塞(需开启任务挂起功能)。
  • 返回值:pdPASS 写入成功;errQUEUE_FULL 队列满、阻塞超时写入失败。

3. 队列数据接收函数 xQueueReceive()

注意:禁止在中断中调用,中断使用 xQueueReceiveFromISR()

  • 函数原型:

    c

    运行

    BaseType_t xQueueReceive( QueueHandle_t xQueue, void *const pvBuffer, TickType_t xTicksToWait );
    
  • 功能:从队列头部读取数据,读取后数据会从队列中删除
  • 参数说明:
    • xQueue:目标队列句柄;
    • pvBuffer:接收数据的缓冲区指针,大小需匹配队列单元尺寸;
    • xTicksToWait:队列为空时的阻塞时长,规则同发送函数。
  • 返回值:pdPASS 读取成功;errQUEUE_EMPTY 队列为空、阻塞超时读取失败。

4. 队列消息计数 uxQueueMessagesWaiting()

注意:禁止在中断中调用,中断使用 uxQueueMessagesWaitingFromISR()

  • 函数原型:UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue );
  • 功能:查询队列当前已存储的数据单元数量,返回 0 表示队列为空。

四、基础使用示例(任务间通信)

示例 1:整型数据收发(高优先级接收任务)

场景说明

创建 2 个发送任务(优先级 1)、1 个接收任务(优先级 2);接收任务优先级更高,数据一入队就会被立即读取,队列基本不会缓存数据。

  1. main 函数:创建队列、任务,启动调度器

    c

    运行

    QueueHandle_t xQueue;
    int main( void )
    {
        // 创建队列:最大存5个int32_t数据
        xQueue = xQueueCreate( 5, sizeof( int32_t) );
        if( xQueue != NULL )
        {
            // 创建两个发送任务实例,分别发送100、200
            xTaskCreate( vSenderTask, "Sender1", 1000, ( void * ) 100, 1, NULL );
            xTaskCreate( vSenderTask, "Sender2", 1000, ( void * ) 200, 1, NULL );
            // 创建接收任务(高优先级)
            xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );
            vTaskStartScheduler(); // 启动调度器
        }
        for( ;; );
    }
    
  2. 发送任务 vSenderTask:循环向队列发数据,不阻塞

    c

    运行

    static void vSenderTask( void *pvParameters )
    {
        int32_t lValueToSend = ( int32_t ) pvParameters;
        BaseType_t xStatus;
        for( ;; )
        {
            xStatus = xQueueSendToBack( xQueue, &lValueToSend, 0 );
            if( xStatus != pdPASS )
            {
                vPrintString( "Could not send to the queue.\r\n" );
            }
        }
    }
    
  3. 接收任务 vReceiverTask:阻塞 100ms 等待数据,读取并打印

    c

    运行

    static void vReceiverTask( void *pvParameters )
    {
        int32_t lReceivedValue;
        BaseType_t xStatus;
        const TickType_t xTicksToWait = pdMS_TO_TICKS( 100 );
        for( ;; )
        {
            if( uxQueueMessagesWaiting( xQueue ) != 0 )
            {
                vPrintString( "Queue should have been empty!\r\n" );
            }
            xStatus = xQueueReceive( xQueue, &lReceivedValue, xTicksToWait );
            if( xStatus == pdPASS )
            {
                vPrintStringAndNumber( "Received = ", lReceivedValue );
            }
            else
            {
                vPrintString( "Could not receive from the queue.\r\n" );
            }
        }
    }
    

示例 2:结构体数据收发(高优先级发送任务)

场景说明

发送任务优先级(2)高于接收任务(1),队列会被写满,发送任务阻塞等待队列释放空间;队列传输结构体,同时携带数据源标识

  1. 定义结构体与枚举

    c

    运行

    typedef enum { eSender1, eSender2 } DataSource_t;
    typedef struct
    {
        uint8_t ucValue;
        DataSource_t eDataSource;
    } Data_t;
    static const Data_t xStructsToSend[ 2 ] = {{ 100, eSender1 },{ 200, eSender2 }};
    
  2. main 函数:创建可存 3 个结构体的队列

    c

    运行

    int main( void )
    {
        xQueue = xQueueCreate( 3, sizeof( Data_t) );
        if( xQueue != NULL )
        {
            xTaskCreate( vSenderTask, "Sender1", 1000, &( xStructsToSend[ 0 ] ), 2, NULL);
            xTaskCreate( vSenderTask, "Sender2", 1000, &( xStructsToSend[ 1 ] ), 2, NULL);
            xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );
            vTaskStartScheduler();
        }
        for( ;; );
    }
    
  3. 发送任务:队列满时阻塞 100ms

    c

    运行

    static void vSenderTask( void *pvParameters )
    {
        BaseType_t xStatus;
        const TickType_t xTicksToWait = pdMS_TO_TICKS( 100 );
        for( ;; )
        {
            xStatus = xQueueSendToBack( xQueue, pvParameters, xTicksToWait );
            if( xStatus != pdPASS )
            {
                vPrintString( "Could not send to the queue.\r\n" );
            }
        }
    }
    
  4. 接收任务:仅队列满时运行,读取并区分数据源

    c

    运行

    static void vReceiverTask( void *pvParameters )
    {
        Data_t xReceivedStructure;
        BaseType_t xStatus;
        for( ;; )
        {
            if( uxQueueMessagesWaiting( xQueue ) != 3 )
            {
                vPrintString( "Queue should have been full!\r\n" );
            }
            xStatus = xQueueReceive( xQueue, &xReceivedStructure, 0 );
            if( xStatus == pdPASS )
            {
                if( xReceivedStructure.eDataSource== eSender1 )
                    vPrintStringAndNumber( "From Sender 1 = ", xReceivedStructure.ucValue );
                else
                    vPrintStringAndNumber( "From Sender 2 = ", xReceivedStructure.ucValue );
            }
            else
            {
                vPrintString( "Could not receive from the queue.\r\n" );
            }
        }
    }
    

示例 3:队列传递指针(大数据优化方案)

针对大数据场景,队列存储数据缓冲区指针,减少数据拷贝,核心代码:

  1. 创建指针队列

    c

    运行

    QueueHandle_t xPointerQueue;
    xPointerQueue = xQueueCreate( 5, sizeof( char * ) );
    
  2. 发送任务:申请缓冲区、写入数据、发送指针

    c

    运行

    void vStringSendingTask( void *pvParameters )
    {
        char *pcStringToSend;
        const size_t xMaxStringLength = 50;
        BaseType_t xStringNumber = 0;
        for( ;; )
        {
            pcStringToSend = ( char * ) prvGetBuffer( xMaxStringLength ); // 获取缓冲区
            snprintf( pcStringToSend, xMaxStringLength, "String number %d\r\n", xStringNumber );
            xStringNumber++;
            xQueueSend( xPointerQueue, &pcStringToSend, portMAX_DELAY ); // 发送指针
        }
    }
    
  3. 接收任务:读取指针、处理数据、释放缓冲区

    c

    运行

    void vStringReceivingTask( void *pvParameters )
    {
        char *pcReceivedString;
        for( ;; )
        {
            xQueueReceive( xPointerQueue, &pcReceivedString, portMAX_DELAY );
            vPrintString( pcReceivedString );
            prvReleaseBuffer( pcReceivedString ); // 释放缓冲区
        }
    }
    

注意事项:保证指针指向内存有效、明确内存归属,禁止传递任务栈指针

拓展:通用事件队列(结构体 + 指针组合)

参考 FreeRTOS+TCP 设计,用结构体封装事件类型 + 通用数据指针,实现单队列传输多类型数据:

  1. 事件结构体定义

    c

    运行

    typedef enum { eNetworkDownEvent, eNetworkRxEvent, eTCPAcceptEvent } eIPEvent_t;
    typedef struct IP_TASK_COMMANDS
    {
        eIPEvent_t eEventType; // 事件类型
        void *pvData;          // 通用数据指针
    } IPStackEvent_t;
    
  2. 发送不同事件(伪代码):网络接收事件、连接事件、网络断开事件
  3. 接收任务:通过 eEventType 区分事件,解析 pvData 处理对应数据。

五、队列集(Queue Set)

1. 功能与适用场景

  1. 作用:将多个队列 / 信号量加入集合,任务可阻塞等待集合内任意成员有数据,替代轮询,提升效率。
  2. 局限性:相比单结构体队列,代码繁琐、效率偏低,仅在第三方组件、多独立队列等约束场景使用。
  3. 前置配置:FreeRTOSConfig.h 中开启 configUSE_QUEUE_SETS = 1

2. 队列集核心 API

  1. xQueueCreateSet():创建队列集
    • 原型:QueueSetHandle_t xQueueCreateSet(const UBaseType_t uxEventQueueLength);
    • 参数:uxEventQueueLength = 集合内所有队列 / 信号量最大容量之和;
    • 返回值:队列集句柄,内存不足返回 NULL
  2. xQueueAddToSet():将队列 / 信号量添加到队列集
    • 原型:BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet );
    • 规则:队列 / 二进制信号量必须为空、计数信号量计数值为 0 时才能添加;一个成员仅能归属一个队列集。
  3. xQueueSelectFromSet():从队列集获取有数据的成员句柄
    • 原型:QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet, const TickType_t xTicksToWait );
    • 规则:返回有数据的队列 / 信号量句柄;超时返回 NULL;获取句柄后,需单独读取原队列 / 信号量

3. 队列集使用示例

两个独立队列加入队列集,接收任务阻塞监听队列集:

  1. main 函数:创建队列、队列集、任务

    c

    运行

    static QueueHandle_t xQueue1 = NULL, xQueue2 = NULL;
    static QueueSetHandle_t xQueueSet = NULL;
    int main( void )
    {
        xQueue1 = xQueueCreate( 1, sizeof( char * ) );
        xQueue2 = xQueueCreate( 1, sizeof( char * ) );
        xQueueSet = xQueueCreateSet( 2 ); // 总容量 2
        xQueueAddToSet( xQueue1, xQueueSet );
        xQueueAddToSet( xQueue2, xQueueSet );
        xTaskCreate( vSenderTask1, "Sender1", 1000, NULL, 1, NULL );
        xTaskCreate( vSenderTask2, "Sender2", 1000, NULL, 1, NULL );
        xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );
        vTaskStartScheduler();
        for( ;; );
        return 0;
    }
    
  2. 两个发送任务:定时向不同队列发数据
  3. 接收任务:从队列集获取句柄,再读取对应队列

    c

    运行

    void vReceiverTask( void *pvParameters )
    {
        QueueHandle_t xQueueThatContainsData;
        char *pcReceivedString;
        for( ;; )
        {
            xQueueThatContainsData = ( QueueHandle_t ) xQueueSelectFromSet( xQueueSet, portMAX_DELAY );
            xQueueReceive( xQueueThatContainsData, &pcReceivedString, 0 );
            vPrintString( pcReceivedString );
        }
    }
    
  4. 进阶用法:队列集同时包含队列 + 二进制信号量,通过句柄类型区分处理逻辑。

六、队列实现邮箱(Mailbox)

1. 定义

FreeRTOS 中邮箱 = 长度为 1 的队列,使用逻辑和普通队列不同:

  • 队列:数据流转,读取后数据删除;
  • 邮箱:数据常驻,读取后数据保留,支持覆盖更新。

2. 专属 API 函数

(1)xQueueOverwrite()(覆盖写入,邮箱专用)
  • 原型:BaseType_t xQueueOverwrite( QueueHandle_t xQueue, const void * pvItemToQueue );
  • 规则:仅支持长度为 1 的队列;无论队列是否已满,直接覆盖原有数据;无阻塞,永远返回 pdPASS;中断使用 xQueueOverwriteFromISR()
(2)xQueuePeek()(偷看数据,不删除)
  • 原型:BaseType_t xQueuePeek( QueueHandle_t xQueue, void *const pvBuffer, TickType_t xTicksToWait );
  • 功能:读取队列头部数据,数据不从队列删除;参数、返回值规则同 xQueueReceive();中断使用 xQueuePeekFromISR()

3. 邮箱完整示例

  1. 定义数据结构体(带时间戳)、创建邮箱

    c

    运行

    typedef struct xExampleStructure
    {
        TickType_t xTimeStamp;
        uint32_t ulValue;
    } Example_t;
    QueueHandle_t xMailbox;
    void vAFunction( void )
    {
        xMailbox = xQueueCreate( 1, sizeof( Example_t ) ); // 长度为1的队列 = 邮箱
    }
    
  2. 更新邮箱(覆盖写入)

    c

    运行

    void vUpdateMailbox( uint32_t ulNewValue )
    {
        Example_t xData;
        xData.ulValue = ulNewValue;
        xData.xTimeStamp = xTaskGetTickCount(); // 记录时间戳
        xQueueOverwrite( xMailbox, &xData );
    }
    
  3. 读取邮箱(数据保留,判断数据是否更新)

    c

    运行

    BaseType_t vReadMailbox( Example_t *pxData )
    {
        TickType_t xPreviousTimeStamp = pxData->xTimeStamp;
        xQueuePeek( xMailbox, pxData, portMAX_DELAY ); // 读取不删除
        return ( pxData->xTimeStamp > xPreviousTimeStamp ) ? pdTRUE : pdFALSE;
    }
    

七、补充规则总结

  1. 区分任务版 API中断版 API:常规队列函数禁止在中断中调用,中断必须使用 xxxFromISR 后缀函数。
  2. 阻塞时间单位:系统节拍,可通过 pdMS_TO_TICKS() 将毫秒转为节拍数。
  3. 优先级影响:队列阻塞任务的唤醒顺序,严格遵循优先级 > 等待时长规则。
  4. 内存安全:引用队列 / 指针队列,必须保证内存生命周期、读写互斥。
Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐