欢迎光临
我们一直在努力

FreeRTOS中队列入门总结

摘要:队列是一种先进先出(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 有三种设置方式:

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

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

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

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

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

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

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

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

    例子:

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

  • 任务 A 读取 count 到寄存器。

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

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

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

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

  • 取出count的数据大小LDR R0, [R1]
  • 对count计算ADDS R0, R0, #1
  • 将计算完的值写回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. 定义数据结构
    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 会在内部执行以下动作:

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

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

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

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

    2.删除队列

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

  • 释放内存:如果队列是动态创建(xQueueCreate)的,调用此函数后,FreeRTOS 会将队列占用的堆内存(控制块和存储区)归还给系统。

  • 失效句柄:调用后,该队列句柄 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,绝不会停下来等。

    赞(0)
    未经允许不得转载:171主机测评 » FreeRTOS中队列入门总结
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址