一、队列的引入(队列解决两大问题)

1.g_Count++ 的灾难(需要互斥性)

  • RTOS 系统 (多任务)
    • 任务看似“同时”运行(实际是毫秒级切换)。
    • 问题:如果任务 A 和任务 B 同时修改同一个全局变量 int g_Count,会发生数据竞争 (Race Condition)
      int fun_A()
      {
          while(1)
          {
              A++;
          }
      }
      
      int fun_B()
      {
          while(1)
          {
              A++;
          }
      }
      //相同的底层汇编
      LDR R0, [A]   ; 1. 读内存中A的值到寄存器R0
      ADD R0, R0, #1; 2. 寄存器内加1
      STR R0, [A]   ; 3. 写回内存

      A任务在运行中被tick中断执行任务B

      时间轴
      |
      v
      [任务 A] 读取 A (R0=0) (这里的A是变量,不是任务A)
               | (发生任务切换!A 被挂起,R0=0 被保存)
               v
      [任务 B] 读取 A (R0=0) 
               修改 (R0=1) 
               写回 (A = 1)
               |(B 运行结束或切换)
               v
      [任务 A] 恢复现场 (R0 恢复为 0) 
               修改 (R0=1) 
               写回 (A = 1)  <-- 错误!期望是 2,结果是 1

2. “轮询等待” 的效率问题(任务需要有休眠唤醒机制)

任务 B 需要轮询判断flag是否置位(哪怕flag很久才置位,B 也要判断百万次),浪费 CPU 资源;而 RTOS 进一步引入 “事件驱动” —— 数据就绪时主动唤醒任务,而非轮询。

int fun_A()
{
    while(1)
    {
        ... ... 
        flag = 1;
    }
}

int fun_B()
{
    while(1)
    {
        if(flag == 1)
        {
            ... ...
        }
    }
}

二、队列的核心原理

1.核心一:关中断实现互斥

在裸机中,我们可能用 cli/sti 关中断。在 FreeRTOS 队列操作中,API 内部自动完成了这一步。

  • 原理
    • 任务切换依赖于 SysTick 中断
    • 如果在操作队列的关键代码段(如拷贝数据、修改指针)关闭中断,SysTick 就无法触发,任务切换就不会发生。(注意其余硬件中断还能执行,串口GPIO等)
    • 此时,当前任务独享 CPU,就像在裸机环境中一样安全。

如下:写队列,进入临界区,执行操作,最后退出临界区

读队列同理

2.核心二:环形缓冲区 (Ring Buffer)

队列的数据存储区是一个连续的内存块,通过读索引写索引实现循环使用。

参考环形缓冲区的使用-CSDN博客


static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength,
                                   const UBaseType_t uxItemSize,
                                   uint8_t * pucQueueStorage,
                                   const uint8_t ucQueueType,
                                   Queue_t * pxNewQueue )

3.核心三:链表实现阻塞与唤醒

这是队列优于普通全局变量的地方:无数据时不轮询,直接休眠让出 CPU

A. 两个等待链表

队列结构体内部维护了两个链表:

  1. xTasksWaitingToReceive等待接收的任务链表(队列空时,读者挂在这里)。
  2. xTasksWaitingToSend等待发送的任务链表(队列满时,写者挂在这里)。
B. 阻塞 (Block) 流程

当任务 B 读队列,但队列为空(如xQueueReceive(q, &val, 100) 表示最多等 100ms。只要等待时间 不等于 0,一旦队列空,都会调用阻塞流程):

  1. 记录:将任务 B 的 TCB 插入到 xTasksWaitingToReceive 链表中。这样将来有数据时,写任务知道要唤醒谁。
  2. 状态变更:将任务 B 从 就绪链表 (Ready List) 移除,放入 延迟链表 (Delay List) (状态变为 Blocked)。
  3. 调度:触发任务切换,CPU 去运行其他就绪任务。
  4. 结果:任务 B 停止运行,不浪费任何 CPU 周期
C. 唤醒 (Unblock) 流程

当任务 A 向队列写入数据:

  1. 检查:发现 xTasksWaitingToReceive 链表不为空(有人在等)。
  2. 移除:从等待链表中取出任务 B 的 TCB。
  3. 状态变更:将任务 B 从 延迟链表 移回 就绪链表 (状态变为 Ready)。
  4. 调度:如果任务 B 优先级高于当前任务 A,立即触发切换,任务 B 马上运行并读到数据。
    D.超时机制 (Timeout)流程

    当你调用 xQueueReceive(q, &val, 100) 表示最多等 100ms

    1. 计算唤醒时间
      • 当前 Tick 计数 = xTickCount (例如 500)。
      • 超时时间 = 500 + 100 = 600
    2. 插入延迟链表
      • 任务阻塞时,不仅加入队列的等待链表,还会根据 600 这个时间点,按时间顺序插入到内核的全局 延迟链表 (Delayed Task List) 中。
      • 延迟链表是按唤醒时间排序的(50, 100, 600, 800...)。
    3. SysTick 中断处理
      • 每 1ms,SysTick 中断执行 xTaskIncrementTick()
      • 检查延迟链表头部:如果 头部任务.唤醒时间 <= 当前Tick,说明超时了!
      • 动作:将该任务从延迟链表移除,加入就绪链表
      • 注意:此时任务虽然就绪了,但从队列角度看,它还没拿到数据。
    4. 任务恢复运行
      • 任务被调度运行后,从阻塞点醒来。
      • 代码会再次检查队列:发现有数据了吗?
        • 如果有(刚好被别的任务写了):拷贝数据,返回成功。
        • 如果没有(真的是超时了):从队列的等待链表中移除自己,返回 errQUEUE_TIMEOUT

    总结一下就是:

    • 任务读队列无数据→将自己加入xTasksWaitingToReceive→从就绪链表移到延迟链表(休眠);
    • 其他任务写队列→唤醒xTasksWaitingToReceive中的任务→任务移回就绪链表;
    • 任务写队列满→将自己加入xTasksWaitingToSend→休眠;
    • 其他任务读队列→唤醒xTasksWaitingToSend中的任务。

    以下为例子:

    任务 B 设置等待 100ms。
    动作 1:把自己从就绪链表拔掉,扔进延迟链表,设定‘闹钟’为 100ms 后。
    动作 2:立即让出 CPU,触发任务切换。
    结果:任务 B 彻底停止运行,CPU 去跑任务 C、D、E 或者进入低功耗模式。
    
    情况 一(有人写数据了):
    比如在第 5ms,任务 A 写了数据。内核发现任务 B 在等,
    立刻把 B 从延迟链表拽回就绪链表。B 醒来,读到数据,返回成功。
    (耗时仅 5ms,且中间 B 没占 CPU)。
    
    情况 二(没人写数据):
    时间一分一秒过去,B 一直在睡觉。直到第 100ms,
    SysTick 中断发现 B 的‘闹钟’响了。内核把 B 从延迟链表拽回就绪链表。
    B 醒来,发现还是没数据(或数据被更高优先级抢了),返回超时错误。

    以下为队列结构体源码:

    /*
     * 调度器使用的队列结构体定义。
     * 队列中的数据以"拷贝"方式入队(而非引用/指针),设计原因参考:
     * https://www.FreeRTOS.org/Embedded-RTOS-Queues.html
     */
    typedef struct QueueDefinition /* 保留旧命名规则,避免内核调试器兼容问题 */
    {
        int8_t * pcHead;           /*< 指向队列存储区的起始地址(环形缓冲区的头)。
                                         注:队列内存布局 = 队列结构体 + 连续的存储缓冲区,
                                         此字段指向缓冲区第一个字节,是环形Buffer的基准地址 */
    
        int8_t * pcWriteTo;        /*< 指向缓冲区中「下一个可写入数据」的位置(写指针)。
                                         每次写入数据后,该指针会按 uxItemSize 偏移,
                                         到缓冲区末尾时绕回头部(环形特性) */
    
        union
        {
            QueuePointers_t xQueue;     /*< 当结构体用作「队列」时的专属数据:
                                             包含读指针(pcReadFrom)等核心字段,
                                             是环形缓冲区读操作的关键 */
            SemaphoreData_t xSemaphore; /*< 当结构体用作「信号量」时的专属数据:
                                             FreeRTOS中信号量是队列的特例(长度=1的队列),
                                             此字段复用队列结构体实现信号量功能 */
        } u;
    
        List_t xTasksWaitingToSend;             /*< 因「写队列失败(队列满)」而阻塞的任务链表。
                                                     特性:
                                                     1. 按任务优先级排序(高优先级任务优先被唤醒);
                                                     2. 当队列有空闲空间时,从链表头部唤醒任务 */
        List_t xTasksWaitingToReceive;          /*< 因「读队列失败(队列空)」而阻塞的任务链表。
                                                     特性:
                                                     1. 按任务优先级排序;
                                                     2. 当队列有新数据时,从链表头部唤醒任务 */
    
        volatile UBaseType_t uxMessagesWaiting; /*< 队列中当前有效数据的个数(已写入未读取)。
                                                     核心作用:
                                                     1. 判断队列是否为空(=0)/满(=uxLength);
                                                     2. 读写操作时原子更新(关中断保护) */
        UBaseType_t uxLength;                   /*< 队列长度:指队列能容纳的「元素个数」(非字节数)。
                                                     例:uxLength=5 表示队列最多存5个int/char等元素 */
        UBaseType_t uxItemSize;                 /*< 队列中单个元素的字节大小。
                                                     例:uxItemSize=4 表示每个元素是int类型(4字节);
                                                     写入/读取时按此大小拷贝数据 */
    
        volatile int8_t cRxLock;                /*< 队列「读锁」计数器:
                                                     作用场景:中断中读队列时,为避免嵌套问题会先锁队列;
                                                     取值规则:
                                                     - 等于 queueUNLOCKED(-1):队列未锁定;
                                                     - 大于0:表示队列锁定期间,成功读取的元素个数;
                                                     解锁时统一处理这些读取操作 */
        volatile int8_t cTxLock;                /*< 队列「写锁」计数器:
                                                     作用场景:中断中写队列时的锁机制;
                                                     取值规则同cRxLock,记录锁定期间写入的元素个数 */
    
        #if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
            uint8_t ucStaticallyAllocated; /*< 标记队列内存是否为「静态分配」:
                                                 - pdTRUE:静态分配(不允许free内存);
                                                 - pdFALSE:动态分配(可调用vQueueDelete释放) */
        #endif
    
        #if ( configUSE_QUEUE_SETS == 1 )
            struct QueueDefinition * pxQueueSetContainer; /*< 指向队列所属的「队列集」(Queue Set)。
                                                             队列集用于监听多个队列/信号量,
                                                             此字段标记当前队列归属哪个队列集 */
        #endif
    
        #if ( configUSE_TRACE_FACILITY == 1 )
            UBaseType_t uxQueueNumber; /*< 队列唯一编号(调试用):
                                           跟踪工具(如FreeRTOS+Trace)通过此编号识别不同队列 */
            uint8_t ucQueueType;       /*< 队列类型标记(调试用):
                                           区分普通队列、信号量、互斥量等不同类型 */
        #endif
    } xQUEUE;

    三、读写流程图

    四、总结

    队列的本质:带互斥保护 + 休眠唤醒机制的环形缓冲区,解决多任务数据传输的 “数据破坏” 和 “轮询低效” 问题;

    核心保障:关中断实现读写互斥,链表管理等待任务,环形 Buffer 存储数据;

    核心操作

    • 读队列:无数据则休眠,有数据 / 超时则唤醒,读成功后唤醒等待写的任务;
    • 写队列:无空间则休眠,有空间 / 超时则唤醒,写成功后唤醒等待读的任务;

    关键特性:FreeRTOS 队列是 “线程安全” 的(支持任务间 / 中断间读写),无需用户手动处理互斥。

    Logo

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

    更多推荐