FreeRTOS学习(五)- 队列/IPC 模块

源码:queue.c + queue.h

这是 FreeRTOS 最"全能"的模块。它不是单独的"队列",而是 队列 + 信号量 + 互斥锁 + 递归互斥锁 + 队列集 的统一实现。


一、一个生活化的类比

想象一家邮局

FreeRTOS 概念邮局类比
Queue_t 结构体邮局窗口 + 后面存放信件的格子墙
uxMessagesWaiting当前格子里有多少封信
pcWriteTo / pcReadFrom柜员手里的"下一封信放哪/从哪取"指针
xTasksWaitingToSend排队要寄信但窗口满了的人
xTasksWaitingToReceive排队要取信但格子空了的人
prvCopyDataToQueue把信塞进格子
prvCopyDataFromQueue从格子里取出信
信号量 (uxItemSize=0)一张"取餐号码牌"(不用传真正数据,只有计数语义)
互斥锁 (pcHead=NULL)一个"共用钥匙盒"—— 存 1 把钥匙,谁拿了谁能进会议室
优先级继承钥匙被低优先级人拿走时,前台帮他"临时 VIP"插队,让他赶快用完好还给高优先级人
队列集"多窗口总叫号机" —— 哪个窗口先来人就叫哪个

二、核心数据结构:一形多用的 Queue_t

queue.c#L103-L140

typedef struct QueueDefinition {
    int8_t *pcHead;                    // ① 存储区起点(同时充当 uxQueueType,=NULL 表示互斥锁!)
    int8_t *pcWriteTo;                 // ② 下一个写入位置

    union {
        QueuePointers_t xQueue;        // ③-A 普通队列用:pcTail(存储区尾+1 哨兵位)+ pcReadFrom(上一次读位置)
        SemaphoreData_t xSemaphore;    // ③-B 互斥锁用:xMutexHolder + uxRecursiveCallCount
    } u;

    List_t xTasksWaitingToSend;        // ④ 等发送的任务链表(按反向优先级排)
    List_t xTasksWaitingToReceive;     // ⑤ 等接收的任务链表(按反向优先级排)

    volatile UBaseType_t uxMessagesWaiting; // ⑥ 当前消息数 / 信号量计数
    UBaseType_t uxLength;              // ⑦ 队列容量(消息数)
    UBaseType_t uxItemSize;            // ⑧ 单条消息大小(字节)

    volatile int8_t cRxLock;           // ⑨ 接收锁:记录"锁住期间"有几条被 ISR 取走
    volatile int8_t cTxLock;           // ⑩ 发送锁:记录"锁住期间"有几条被 ISR 放入
} Queue_t;

2.1 关键的"身份识别"设计

同一个结构体通过两个标志位区分 4 种对象:

对象类型uxItemSizepcHead (uxQueueType)说明
普通队列>0指向存储区 (非 NULL)有数据复制
二值/计数信号量=0非 NULL(安全值,指向 Queue_t 自己)uxMessagesWaiting 就是信号量计数
互斥锁/递归互斥锁=0= NULL (queueQUEUE_IS_MUTEX)区分信号量 vs 互斥锁的唯一标志!启用优先级继承

代码中用宏实现"同一个字段两个名字":

#define uxQueueType               pcHead
#define queueQUEUE_IS_MUTEX       NULL

2.2 互斥锁的 SemaphoreData_t

typedef struct SemaphoreData {
    TaskHandle_t xMutexHolder;        // 当前持有者 TCB 指针
    UBaseType_t uxRecursiveCallCount; // 递归互斥锁"嵌套 take 次数"
} SemaphoreData_t;

这两个字段与队列的 pcTail/pcReadFrom 共用同一块内存(union),所以互斥锁完全不浪费存储区指针。

2.3 创建时的内存布局

以 xQueueCreate(5, 8)(5 条消息,每条 8 字节)为例,xQueueGenericCreate 一次 pvPortMalloc 同时分配:

  ┌──────────────────────────┐  ← pxNewQueue (= 返回的句柄)
  │ Queue_t 结构体           │  sizeof(Queue_t) ≈ 70~90 字节
  ├──────────────────────────┤  ← pucQueueStorage = pxNewQueue + sizeof(Queue_t)
  │ [0] 8 字节 (消息 1 槽)   │
  │ [1] 8 字节 (消息 2 槽)   │
  │ [2] 8 字节 (消息 3 槽)   │
  │ [3] 8 字节 (消息 4 槽)   │
  │ [4] 8 字节 (消息 5 槽)   │
  └──────────────────────────┘

一次 malloc、一个连续块 —— 避免碎片化。pcHead = pucQueueStorage 指向存储区起点,pcTail = pcHead + 5*8 指向末尾哨兵位。

信号量/互斥锁则 uxItemSize=0,存储区为 0 字节。


三、环形缓冲区:数据是怎么放怎么取的

3.1 prvCopyDataToQueue(入队)

queue.c#L2393-L2473

三种写入方式(xPosition 参数):

模式写入位置用途
queueSEND_TO_BACK (0)pcWriteTo,写完 pcWriteTo 前进,超尾绕回头xQueueSend(),FIFO 尾部
queueSEND_TO_FRONT (1)pcReadFrom 后退一位,写完 pcReadFrom 减,越头绕到尾xQueueSendToFront(),LIFO 头部
queueOVERWRITE (2)同 FRONT,但如果已有消息 → 先 --uxMessagesWaiting 抵消新增,结果就是覆盖xQueueOverwrite(),仅适用于长度=1

SEND_TO_BACK 的流程(经典环形)

1. memcpy(pcWriteTo, pvItemToQueue, uxItemSize)   // 写数据
2. pcWriteTo += uxItemSize
3. if (pcWriteTo >= pcTail)                        // 越过尾哨位
       pcWriteTo = pcHead;                         // 绕回存储区起点
4. uxMessagesWaiting++

互斥锁特殊路径:uxItemSize==0 且 uxQueueType==MUTEX 时,不 memcpy,而是直接:

xReturn = xTaskPriorityDisinherit(xMutexHolder);  // ★ 归还优先级!
xMutexHolder = NULL;

这意味着 mutex 的 give = prvCopyDataToQueue(与任务调度模块的优先级继承实现深度耦合)。

3.2 prvCopyDataFromQueue(出队)

queue.c#L2476-L2494

1. pcReadFrom += uxItemSize                       // 指针先前进一位
2. if (pcReadFrom >= pcTail)
       pcReadFrom = pcHead;
3. memcpy(pvBuffer, pcReadFrom, uxItemSize)       // 从新位置复制

注意"先加后读"的细节:因为写入时是直接写当前 pcWriteTo;而读取从"pcReadFrom 的下一项"才是队首(第一项在初始化后 pcReadFrom=pcHead,要先 +uxItemSize 才能取到第一条)。这样 SEND_TO_BACK → RECEIVE 的 FIFO 顺序天然正确。

信号量/互斥锁的 uxItemSize==0,此函数直接空返回。取信号量直接由 uxMessagesWaiting-- 实现。


四、xQueueGenericSend:发送(带阻塞)的完整流程

queue.c#L949-L1164

这是一个 for(;;) 死循环结构,分两阶段

阶段 1:临界区内"快速路径"

taskENTER_CRITICAL()
  if (队列未满 || OVERWRITE)
    ├─ prvCopyDataToQueue()  // 拷贝 + uxMessagesWaiting++
    │
    ├─ 有等接收的任务吗?→ 是:从 xTasksWaitingToReceive 摘最高优先级任务
    │                        移入就绪表;若它比当前任务优先级高 → request yield
    └─ return pdPASS
  else(队列满)
    if (xTicksToWait == 0) → return errQUEUE_FULL(立即失败)
    if (首次进入) → 记录超时起始时刻 xTimeOut
taskEXIT_CRITICAL()

注意:只要队列有空间,不经过阻塞、一次完成,临界区保护数据与链表的原子性。

阶段 2:队列满 → 挂调度器 + 锁队列 + 阻塞

vTaskSuspendAll();            // 挂调度器,防任务调度但允许 ISR
prvLockQueue(pxQueue);        // 锁队列:cRxLock=0, cTxLock=0

if (尚未超时)
  if (队列还是满)
     vTaskPlaceOnEventList(xTasksWaitingToSend, xTicksToWait);  // 当前任务挂等发送链表
     prvUnlockQueue(pxQueue);  // ISR 若发数据 → cTxLock 计数
     if (xTaskResumeAll()==FALSE) taskYIELD();  // 让出 CPU,进入阻塞
  else
     prvUnlockQueue(); xTaskResumeAll();  // 临界空出了 → 回 for(;;) 重试
else (超时了)
     prvUnlockQueue(); xTaskResumeAll();
     return errQUEUE_FULL

为什么要"锁队列"(prvLockQueue)? 锁队列不阻止 ISR 修改队列数据(复制+uxMessagesWaiting),但阻止 ISR 直接操作 xTasksWaitingToSend / xTasksWaitingToReceive 两条事件链表。因为此时调度器挂起,事件链表改了也不能立即把任务移就绪表,会错乱。

ISR 侧如果发现 cTxLock != queueUNLOCKED(-1) → 不调 xTaskRemoveFromEventList,只做 cTxLock++ 记一下"期间有 N 条新数据"。等 prvUnlockQueue 时集中补处理 N 次。


五、xQueueReceive:接收(带阻塞)的完整流程

结构与 Send 对称:快速路径判空→拷数据→若有等发送任务摘表;否则挂阻塞。详见 queue.c#L1509-L1656

取数据prvCopyDataFromQueue(pxQueue, pvBuffer) → uxMessagesWaiting--

唤醒:数据被取走=队列有空位了 → xTasksWaitingToSend 中最高优先级的任务被解阻塞(若更高则 yield)。


六、xQueueSemaphoreTake:信号量/互斥锁的"获取"

queue.c#L1659-L1881

与 xQueueReceive 结构相同,但有两处互斥锁专属逻辑

6.1 拿锁成功时(快速路径)

pxQueue->uxMessagesWaiting = uxSemaphoreCount - 1;   // 计数-1(互斥锁从1→0)
if (uxQueueType == queueQUEUE_IS_MUTEX)
    xMutexHolder = pvTaskIncrementMutexHeldCount();  // 记录持有者 TCB,并给该 TCB 的 uxMutexesHeld++

6.2 拿不到要阻塞前(互斥锁专属 → 优先级继承)

if (uxQueueType == queueQUEUE_IS_MUTEX)
    xInheritanceOccurred = xTaskPriorityInherit(xMutexHolder);  // ★ 抬升持有者优先级
vTaskPlaceOnEventList(xTasksWaitingToReceive, xTicksToWait);    // 再挂入等待表

6.3 超时退出时(互斥锁专属 → 部分降级)

if (xInheritanceOccurred == TRUE) {
    uxNextHighest = prvGetHighestPriorityOfWaitToReceiveList(pxQueue); // 等待表中剩余最高优先级
    vTaskPriorityDisinheritAfterTimeout(xMutexHolder, uxNextHighest);   // 降到 max(基准, 次高等待者)
}

七、xQueueGiveFromISR / xQueueGenericSendFromISR:中断安全版

xQueueGiveFromISR —— 用于 ISR 中 Give 信号量

核心差异(与任务版比):

维度FromISR 版任务版
阻塞时间永远 0(ISR 不能阻塞)可传 xTicksToWait
解阻塞唤醒方式不直接切换上下文,返回 *pxHigherPriorityTaskWoken=pdTRUE 标志,交给 ISR 末尾决定是否请求 PendSV立即 queueYIELD_IF_USING_PREEMPTION()
临界区方式taskENTER_CRITICAL_FROM_ISR() + 返回旧 BASEPRItaskENTER_CRITICAL()(关中断到 configMAX_SYSCALL)
队列锁 cTxLock若发现队列被锁 → 只 cTxLock++,不碰事件链表任务版不进这个分支(任务走锁前已经在调度器挂起里)

返回标志的使用模板(必须记住)

void EXTI0_IRQHandler(void) {
    BaseType_t xHPtw = pdFALSE;                // ★ 初始化为 FALSE
    xSemaphoreGiveFromISR(xSem, &xHPtw);       // 若唤醒了比被打断任务高的 → xHPtw=TRUE
    // ...
    portYIELD_FROM_ISR(xHPtw);                 // ★ 最后统一根据标志决定是否 yield
}

八、队列锁(cRxLock / cTxLock)的机制详解

这是 FreeRTOS 队列"ISR 与任务并发安全"的灵魂设计。

8.1 三个锁状态值

queue.c#L52-L53

queueUNLOCKED          = -1    队列未锁(常态)
queueLOCKED_UNMODIFIED = 0     队列刚被锁,期间 ISR 还没动过数据
> 0                          队列被锁,且期间 ISR 已经动过 N 次数据(cTxLock=N 发送;cRxLock=N 接收)

8.2 锁/解锁时序(以任务发送阻塞为例)

  任务 T:队列满,要阻塞
   │
   ├─ vTaskSuspendAll()
   ├─ prvLockQueue() → cRxLock=0, cTxLock=0  (关临界区改)
   │   【现在队列处于"锁住"】
   │
   │  <此时一个高优先级中断进来>
   │   │
   │   ├─ ISR 中 xQueueGenericSendFromISR() 执行:
   │   │   成功写数据
   │   │   读取 cTxLock=0(≠-1 → 队列被锁!)
   │   │   不操作事件链表 → 只做 prvIncrementQueueTxLock → cTxLock = 1
   │   │   (注:若等接收链表空,其实也不用操作,所以没问题)
   │   └─ 退出 ISR
   │
   ├─ 挂自己到 xTasksWaitingToSend
   ├─ prvUnlockQueue() → 关键!
   │     进入临界区
   │     cTxLock=1 > 0:循环 1 次
   │       检查 xTasksWaitingToReceive → 有任务则摘出解阻塞
   │       若涉及队列集 → 也通知容器
   │     cTxLock = -1(queueUNLOCKED)
   │     cRxLock 同理检查
   │     退出临界区
   └─ xTaskResumeAll() → 如果解阻塞的任务优先级更高 → PendSV 切换

设计妙处:ISR 的高频热路径(放入数据)只做计数,不做链表操作。链表操作(O(1)~O(n) 复杂度、可能引起连锁调度)被延迟到任务上下文的 prvUnlockQueue 时批量执行,最大限度减小了 ISR 关中断/关调度的时间。


九、互斥锁与信号量的创建流程

互斥锁创建示例 queue.c#L646-L659

xQueueCreateMutex(ucQueueType):
  // 1. 先以"长度=1,每项=0字节"创建一个普通队列壳子
  xNewQueue = xQueueGenericCreate(1, 0, ucQueueType);

  // 2. 然后覆写为互斥锁语义
  prvInitialiseMutex(xNewQueue):
      xMutexHolder = NULL;
      uxQueueType  = queueQUEUE_IS_MUTEX;     // ★ pcHead = NULL,标记"我是互斥锁"
      uxRecursiveCallCount = 0;
      // 3. 预放 1 个"令牌"到队列里(uxMessagesWaiting=1),这样 xSemaphoreTake 第一次能拿到
      xQueueGenericSend(xNewQueue, NULL, 0, queueSEND_TO_BACK);

这就是为什么互斥锁创建后"可直接 take"—— 初始就给了一个"可用计数=1",与二值信号量创建后的"可用计数=0"恰好相反(二值信号量需要先 Give 才能 Take)。

信号量/递归互斥锁/队列集的创建都沿着这个模式:先造一个通用 Queue_t 骨架,再微调字段语义


十、对外 API 一览与适用场景

API 族底层实现典型用途
xQueueCreate/CreateStaticxQueueGenericCreate任务间数据传递(ISR→任务、TaskA→TaskB)
xQueueSend / SendToFront / OverwritexQueueGenericSend(COPY_POS)标准 FIFO / 紧急插队 / 单槽覆盖
xQueueSendFromISRxQueueGenericSendFromISRISR 中向任务发数据
xQueueReceive / PeekxQueueGenericReceive / xQueuePeek取数据 / 仅看一眼不拿走
xSemaphoreCreateBinary / CountingxQueueGenericCreate(uxLength,0) + prvInitialiseNewQueue资源计数、事件同步、ISR 唤醒任务
xSemaphoreCreateMutex / RecursiveMutexxQueueCreateMutex + prvInitialiseMutex共享资源保护(有 PI)
xSemaphoreTake / GivexQueueSemaphoreTake / xQueueGenericSend(互斥锁 Give 触发 Disinherit)拿/放 信号量/互斥锁
xSemaphoreGiveFromISRxQueueGiveFromISR(特殊:不用 memcpy,不处理 PI)ISR 中释放二值/计数信号量
xQueueCreateSet / AddToSet / SelectFromSet一条"元队列"里装各子队列的"来数据了"通知"在 N 个 IPC 源上多路复用等待"(类似 select/poll)
xTimerPendFunctionCall向定时器守护任务的命令队列发 CallbackParameters把函数执行 defer 到线程上下文

十一、一张总图:发送与阻塞/唤醒的全链路

应用任务 TaskA (P=5)               ISR                        应用任务 TaskB (P=10)
       │  xQueueSend(Q, data, 100)                                                    │
       │                                                                               │
       ├─ 临界区:Q 已满                                                                │
       │   vTaskSuspendAll()                                                           │
       │   prvLockQueue(Q)                                                             │
       │   vTaskPlaceOnEventList(Q.xTasksWaitingToSend, 100)                           │
       │     → A 被挂 Q.WaitToSend 链表                                                │
       │     → A 同时挂 pxDelayedTaskList (超时 100 tick)                               │
       │   prvUnlockQueue(Q)                                                           │
       │   xTaskResumeAll(); yield;  → A 被阻塞,B 因 P=10 高而开始运行                │
       │                                                                               │
       │                                    ISR 来了:xQueueSendFromISR(Q, isr_data, &hpTw) │
       │                                      Q 有空间(A 发送的空间也有但这次 ISR 是发) │
       │                                      prvCopyDataToQueue(Q); uxMessagesWaiting++ │
       │                                      cRxLock=-1 (没锁)                        │
       │                                        → xTaskRemoveFromEventList(Q.WaitToReceive)│
       │                                          → 把 B 从等接收链表摘掉(如果在)      │
       │                                          → B 进就绪表(P=10 高→hpTw=TRUE)    │
       │                                    portYIELD_FROM_ISR(hpTw) → 立即切到 B       │
       │                                                                               │
       │                                                                    B 恢复运行 │
       │                                                                    xQueueReceive(Q, buf, 0) │
       │                                                                      uxMessagesWaiting>0:直接取 │
       │                                                                    发现 Q.WaitToSend 有任务 A:│
       │                                                                      xTaskRemoveFromEventList() │
       │                                                                      A 进就绪表(P=5 < B=10)│
       │                                                                    B 继续执行(不用 yield,自己更高)

十二、常见坑点与注意事项

  1. FromISR 的 pxHigherPriorityTaskWoken:必须在进入 ISR 时初始化为 pdFALSE;所有 FromISR 调用传这个指针;ISR 末尾用 portYIELD_FROM_ISR(flag) 只 yield 一次(不是每个调用都 yield 一次)。
  1. 互斥锁不能在 ISR Give:因为 Give 互斥锁会触发 xTaskPriorityDisinherit,涉及 TCB.uxPriority 与就绪表搬移,需要调度上下文中进行。xQueueGiveFromISR 里有专门 configASSERT 断言:若 Queue 是 Mutex 且 holder≠NULL → 直接崩溃。
  1. 队列长度=1 + queueOVERWRITE 很常用:用于"只关心最新值"的场景(例如最新传感器读数),旧数据自动被覆盖。
  1. 按引用传 vs 按值传:FreeRTOS 队列是按值复制(uxItemSize≠0 时 memcpy),不是传指针。若数据大(如 >64B),建议队列里只存指针,真正的数据用另一种方式管理(静态缓冲区池 + 引用计数/互斥)。
  1. 不要在回调里阻塞:定时器回调、PendFunctionCall、互斥锁回调都运行在某个任务线程(守护任务或 holder 线程)里,阻塞会影响所有排队的定时器/函数。这与队列 API 本身无关,但实际是 80% 的定时器队列问题的根因。
  1. 队列集(Queue Set)的限制:同一条队列不能同时加入两个集合;加入集合的队列在有数据时不能被普通 Receive 先抢走(否则集合永远收不到"可用"通知而误判为空);正确用法是 SelectFromSet 返回哪个子队列有数据→再从该子队列 Receive 一次。

十三、设计哲学总结

设计为什么这么做
多态:一个 Queue_t 同时承载队列/信号量/互斥锁/队列集成员减小代码量,复用环形缓冲与事件等待逻辑
按值复制(非引用)发送方 buffer 失效时不会引起悬垂指针(特别适合 ISR→任务)
锁计数 cTxLock/cRxLock(延迟处理)最大限度压缩 ISR 热路径时间,链表操作推迟到任务线程批量做
事件链表按反向优先级排序vListInsert 天然按 itemValue 升序,O(1) 取头就是最高优先级等待者,保证优先唤醒高优任务
取/放操作的对称结构:快速路径→超时判定→锁+等→超时/重试 循环代码路径稳定,易于维护与 MISRA 检查
uxItemSize=0 + pcHead=NULL 区分 Mutex用两个现成字段的特殊取值做 RTTI,不增加额外成员,零成本

队列模块是 FreeRTOS 中最大的单文件(~3400 行),也是功能密度最高的模块。它通过"一个数据结构多态复用 + 锁计数延迟处理 + 与任务调度深度耦合(优先级继承/挂调度器/事件链表)"实现了完整的 IPC 能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值