摘要:队列是一种先进先出(FIFO)的数据结构,支持入队(Enqueue)和出队(Dequeue)操作。在FreeRTOS中,队列具有重要作用:1) 实现任务解耦,使生产者和消费者无需直接交互;2) 提供任务同步与阻塞机制,提高CPU利用率;3) 保证数据安全,通过临界区保护实现线程安全;4) 作为缓冲层应对流量高峰。队列创建方式包括动态分配(运行时)和静态分配(编译时),各有适用场景。常用操作包括复位、删除、读写队列等,其中xQueueSend和xQueueReceive是最核心的函数。特别需要注意的是,中断服务程序(ISR)中的队列操作不能阻塞,必须使用带FromISR后缀的特殊函数。队列机制有效解决了嵌入式系统中的任务通信、数据同步和资源竞争等问题。

目录

一、什么是队列

二、为什么要使用队列

1. 解耦任务(Decoupling)

2. 任务同步与阻塞机制(Blocking & Sync)

3. 数据安全(线程安全)

4. 缓冲与流量削峰

三、队列创建

1.动态创建队列函数

2.静态创建队列 xQueueCreateStatic

参数详细拆解

1. uxQueueLength (队列长度)

2. uxItemSize (项目大小)

3. pucQueueStorageBuffer (存储区指针)

4. pxQueueBuffer (控制结构指针)

四、两种创建方式对比

五、队列常用的函数

1.复位队列

2.删除队列

3.写队列

(1)写在队尾:

(2)写在队头

(3)中断中写入队尾

(4)中断中写入队头

4.读队列

 区别:


一、什么是队列

队列(Queue) 是一种遵循 “先来后到” 原则的数据结构,在计算机科学中,这种特性被称为 FIFO(First In, First Out,先进先出)

核心操作:

  • 入队 (Enqueue): 在队列的末尾添加一个新元素。

  • 出队 (Dequeue): 从队列的前端移除一个元素。

二、为什么要使用队列

1. 解耦任务(Decoupling)

(1)在裸机编程中,如果你想让传感器数据触发屏幕显示,通常要在同一个循环里处理。

(2)在 FreeRTOS 中:

  • 发送者只需把数据丢进队列,不用管谁来处理。

  • 接收者只需从队列拿数据,不用管数据是谁发的。

这种解耦让代码结构更清晰,方便多人协作开发。

2. 任务同步与阻塞机制(Blocking & Sync)

  • 自动休眠: 如果一个任务尝试从空队列读或从满队列写数据,FreeRTOS 会自动让该任务进入“阻塞态”。系统调度器会把它从“就绪列表”移除,放进“等待列表”,CPU 此时去运行其他有意义的任务,完全不浪费功耗。

这比在循环里用 if(data_ready) 这种“忙等”方式要高效得多,能极大地提升 CPU 利用率。

  • 唤醒:                            

                (1)当数据到达时,系统会立即唤醒

                                a.等待该数据的最高优先级的任务

                                b.多个同优先级的任务等待时会唤醒等待最久的任务。

                (2)超过有限等待时间,系统也会被唤醒

调用 xQueueReceive(xQueue, &data, xTicksToWait) 时,阻塞参数 xTicksToWait 有三种设置方式:

  1. 不等待 (0):若队列为空,函数立即返回 errQUEUE_EMPTY。

  2. 有限等待 (1 ~ pdMS_TO_TICKS(500)):设置超时时间(如 500ms)。在超时前若有数据到达,任务将被唤醒;若超时后仍无数据,则返回错误代码。

  3. 永久等待 (portMAX_DELAY):任务将一直阻塞,直到队列中有数据到达才会被唤醒。

中断服务程序 (ISR) 绝对不能阻塞:如果在中断里调用 xQueueReceive 并设置了等待时间,系统会直接断言报错 (Assert)

3. 数据安全(线程安全)

在多任务环境下,如果两个任务同时修改同一个全局变量,会发生数据损坏。

  • FreeRTOS 的队列函数(如 xQueueSend)内部自带临界区保护

  • 它保证了即使有多个任务争抢,数据的存取也是原子的、安全的。

例子:

当一个全局变量 count = 10,两个任务都想对它执行 count++

  1. 任务 A 读取 count 到寄存器。

  2. 突然发生切换! 任务 A 还没来得及加 1,任务 B 抢占了 CPU。

  3. 任务 B 读取 count,加到 11,写回内存。

  4. 任务 A 恢复,继续它之前的操作:把寄存器里的 10 加到 11,写回内存。

count+=1在底层汇编语言中分三步:

  1. 取出count的数据大小LDR R0, [R1]
  2. 对count计算ADDS R0, R0, #1
  3. 将计算完的值写回STR R0, [R1]

结果: 两次自增操作,最终 count 是 11 而不是 12。

4. 缓冲与流量削峰

  • 异步处理: 比如中断服务程序(ISR)产生数据的速度极快,而处理任务较慢。队列可以充当缓冲区,防止数据丢失。

  • 通过值传递: FreeRTOS 队列默认采用“拷贝”方式(Pass by copy)。数据一旦入队,原始变量被修改也不会影响队列里的值。

三、队列创建

在 FreeRTOS 中创建队列主要有两种方式:动态分配(最常用)和静态分配

1.动态创建队列函数

QueueHandle_t xQueueCreate( 
    UBaseType_t uxQueueLength, // 队列长度(能容纳多少个项目)
    UBaseType_t uxItemSize     // 每个项目的大小(字节)
);

返回值: 非0即成功,返回句柄,以后使用句柄来操作队列

               NULL:失败,因为内存不足

假设我们要传递一个包含传感器数据的结构体:

  1. 先定义一个存储变量结构体
  2. 定义队列句柄
  3. 使用创建队列函数创建
// 1. 定义数据结构
typedef struct {
    uint32_t id;
    float value;
} Message_t;

// 2. 定义队列句柄
QueueHandle_t xSensorQueue;

void vSetup() {
    // 3. 创建队列:长度为 10,每个单元大小为 Message_t 的体积
    xSensorQueue = xQueueCreate(10, sizeof(Message_t));

    if (xSensorQueue != NULL) {
        // 队列创建成功
    } else {
        // 内存不足,创建失败
    }
}

2.静态创建队列 xQueueCreateStatic

QueueHandle_t xQueueCreateStatic( 
                           UBaseType_t uxQueueLength, 
                           UBaseType_t uxItemSize, 
                           uint8_t *pucQueueStorageBuffer, 
                           StaticQueue_t *pxQueueBuffer 
                       ); 

参数详细拆解

1. uxQueueLength (队列长度)

  • 含义:队列中最多可以容纳多少个“项目”。

  • 注意:这是项目的个数,不是字节数。

2. uxItemSize (项目大小)

  • 含义:每个项目的字节数(Byte)。

  • 技巧:通常使用如sizeof(Struct Sensor_data) 来确保准确。

3. pucQueueStorageBuffer (存储区指针)

  • 含义:这是存放实际数据的“仓库”。

  • 要求:它必须是一个 uint8_t 类型的数组,其大小至少为 (uxQueueLength * uxItemSize)

  • 物理意义:入队的数据会被复制到这里。

4. pxQueueBuffer (控制结构指针)

  • 含义:这是存放队列元数据的“办公室”。

  • 类型:指向一个 StaticQueue_t 类型的变量。

  • 物理意义:FreeRTOS 用它来记录队列的状态(比如现在谁在排队、读写指针在哪等)。

#define QUEUE_LENGTH 5
#define ITEM_SIZE    sizeof(uint32_t)

// 预留内存:队列内部控制结构 + 实际存储数据的缓冲区
StaticQueue_t xStaticQueue;
uint8_t ucQueueStorageArea[ QUEUE_LENGTH * ITEM_SIZE ];

void vSetup() {
    QueueHandle_t xHandle;
    xHandle = xQueueCreateStatic(
        QUEUE_LENGTH, 
        ITEM_SIZE, 
        ucQueueStorageArea, 
        &xStaticQueue
    );
}

关键注意事项

  • 队列长度选择: 长度过大会浪费 RAM,过小会导致生产者频繁进入阻塞状态。

  • 数据大小: 如果传递的数据非常大,建议在队列中传递指针,而不是整个数据块,以提高效率。

四、两种创建方式对比

特性 动态分配 (Dynamic) 静态分配 (Static)
分配时机 运行时 (Runtime) 编译阶段 (Compile time)
内存位置 FreeRTOS Heap (堆) 全局/静态变量区 (RAM)
创建失败风险 (堆空间不足或碎片化) (编译过得去就一定能用)

在内存极小(如 8KB RAM),设备要连续运行数月/数年的单片机上,使用静态分配。

常用函数解释

五、队列常用的函数

//复位
BaseType_t xQueueReset( QueueHandle_t pxQueue); //“一键清空”队列,并将其恢复到初始状态。


//删除
void vQueueDelete( QueueHandle_t xQueue );//销毁队列、释放资源


//写队尾
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 xQueueSendToBackFromISR(
    QueueHandle_t xQueue,
    const void *pvItemToQueue,
    BaseType_t *pxHigherPriorityTaskWoken // 重点:用于触发上下文切换
);


//中断写队头
BaseType_t xQueueSendToFrontFromISR( 
                                      QueueHandle_t xQueue, 
                                      const void *pvItemToQueue, 
                                      BaseType_t *pxHigherPriorityTaskWoken 
                                   ); 


//任务中读队列
BaseType_t xQueueReceive( QueueHandle_t xQueue, 
                          void * const pvBuffer, 
                          TickType_t xTicksToWait
 ); 


//在中断中读队列
 
BaseType_t xQueueReceiveFromISR( 
                                    QueueHandle_t    xQueue, 
                                    void             *pvBuffer, 
                                    BaseType_t       *pxTaskWoken 
                                ); 

1.复位队列

BaseType_t xQueueReset( QueueHandle_t pxQueue); //“一键清空”队列,并将其恢复到初始状态。

当你调用 xQueueReset(xQueue) 时,FreeRTOS 会在内部执行以下动作:

  1. 数据作废: 将队列中的现有消息数量直接设为 0

  2. 指针复位: 将队列的读指针和写指针重新指向存储区的起始位置。

  3. 状态刷新: 队列恢复到刚刚被 xQueueCreate 创建出来时的“空”状态。

核心注意: xQueueReset 不会真正擦除存储区里的字节(它只是逻辑上标记为无效),更不会释放内存。它仅仅是重置了队列的管理状态。

2.删除队列

void vQueueDelete( QueueHandle_t xQueue );//销毁队列、释放资源
  1. 释放内存:如果队列是动态创建(xQueueCreate)的,调用此函数后,FreeRTOS 会将队列占用的堆内存(控制块和存储区)归还给系统。

  2. 失效句柄:调用后,该队列句柄 xQueue 不再指向任何有效的资源

注意:如果有任务正在因为这个队列而阻塞(比如 Task A 正在 xQueueReceive 死等消息),不要直接删除队列。删除会导致Task A 将永远处于阻塞态,或者在被唤醒时访问已释放的非法内存,直接导致 HardFault(系统崩溃)。

3.写队列

(1)写在队尾:

BaseType_t xQueueSend( 
                                QueueHandle_t    xQueue, 
                                const void       *pvItemToQueue, 
                                TickType_t       xTicksToWait 
                            ); 

这是 FreeRTOS 中使用频率最高的函数。它负责将数据“塞进”队列。如果你把队列看作一个传送带,xQueueSend 就是那个往传送带上放货物的动作。

参数解析:

  • xQueue (队列句柄)

  • pvItemToQueue (数据指针): 指向你想要发送的数据的指针。

  • xTicksToWait (阻塞超时时间)

(2)写在队头

BaseType_t xQueueSendToFront( 
                                QueueHandle_t    xQueue, 
                                const void       *pvItemToQueue, 
                                TickType_t       xTicksToWait 
                            ); 

(3)中断中写入队尾

BaseType_t xQueueSendToBackFromISR(
    QueueHandle_t xQueue,
    const void *pvItemToQueue,
    BaseType_t *pxHigherPriorityTaskWoken // 重点:用于触发上下文切换
);

BaseType_t *pxHigherPriorityTaskWoken:告诉 CPU,中断结束后是否需要立刻切换到一个更高优先级的任务。

定义变量初始值 xHigherPriorityTaskWoken = pdFALSE;

如果 xQueueSend 解锁了一个高优先级任务,FreeRTOS 会在函数内部自动进行任务切换。但是在中断函数中不同

  • 中断具有最高的优先级,它会强行打断当前正在运行的任务。

  • 如果在中断里发了一个消息给队列,而这个消息刚好让一个更高优先级的任务从“阻塞”变成了“就绪”。

  • 按照 RTOS 的原则,我们应该立刻去执行那个高优先级任务,而不是回到刚才被打断的低优先级任务。

示例:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    uint32_t ulEventCode = 100;

    // 在中断中写队列
    xQueueSendFromISR(xEventQueue, &ulEventCode, &xHigherPriorityTaskWoken);

    // 如果发送导致了高优先级任务解锁,立即进行任务切换
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

portYIELD_FROM_ISR作用:在中断结束的一瞬间,强制触发一次任务调度(上下文切换)。

没有调用 portYIELD_FROM_ISR的情况

  • 功能依然正确:高优先级任务最终还是会运行。

  • 实时性变差:高优先级任务不会在中断结束时立刻运行,而是要等到下一次系统节拍中断发生时才会被调度。可能会带来延迟。

(4)中断中写入队头

BaseType_t xQueueSendToFrontFromISR( 
                                      QueueHandle_t xQueue, 
                                      const void *pvItemToQueue, 
                                      BaseType_t *pxHigherPriorityTaskWoken 
                                   ); 

4.读队列

BaseType_t xQueueReceive( QueueHandle_t xQueue, 
                          void * const pvBuffer, 
                          TickType_t xTicksToWait
 ); 
//在中断中
 
BaseType_t xQueueReceiveFromISR( 
                                    QueueHandle_t    xQueue, 
                                    void             *pvBuffer, 
                                    BaseType_t       *pxTaskWoken 
                                ); 

 区别:

  • xQueueReceive:如果队列是空的,可以指定等待时间,进入阻塞。比如设置 portMAX_DELAY

  • xQueueReceiveFromISR:中断服务程序(ISR)必须执行得尽可能快。如果队列是空的,它会立即返回 errQUEUE_EMPTY,绝不会停下来等。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐