这是 FreeRTOS 最"全能"的模块。它不是单独的"队列",而是 队列 + 信号量 + 互斥锁 + 递归互斥锁 + 队列集 的统一实现。
一、一个生活化的类比
想象一家邮局:
| FreeRTOS 概念 | 邮局类比 |
|---|---|
| Queue_t 结构体 | 邮局窗口 + 后面存放信件的格子墙 |
| uxMessagesWaiting | 当前格子里有多少封信 |
| pcWriteTo / pcReadFrom | 柜员手里的"下一封信放哪/从哪取"指针 |
| xTasksWaitingToSend | 排队要寄信但窗口满了的人 |
| xTasksWaitingToReceive | 排队要取信但格子空了的人 |
| prvCopyDataToQueue | 把信塞进格子 |
| prvCopyDataFromQueue | 从格子里取出信 |
| 信号量 (uxItemSize=0) | 一张"取餐号码牌"(不用传真正数据,只有计数语义) |
| 互斥锁 (pcHead=NULL) | 一个"共用钥匙盒"—— 存 1 把钥匙,谁拿了谁能进会议室 |
| 优先级继承 | 钥匙被低优先级人拿走时,前台帮他"临时 VIP"插队,让他赶快用完好还给高优先级人 |
| 队列集 | "多窗口总叫号机" —— 哪个窗口先来人就叫哪个 |
二、核心数据结构:一形多用的 Queue_t
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 种对象:
| 对象类型 | uxItemSize | pcHead (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(入队)
三种写入方式(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(出队)
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:发送(带阻塞)的完整流程
这是一个 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:信号量/互斥锁的"获取"
与 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() + 返回旧 BASEPRI | taskENTER_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 三个锁状态值
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/CreateStatic | xQueueGenericCreate | 任务间数据传递(ISR→任务、TaskA→TaskB) |
xQueueSend / SendToFront / Overwrite | xQueueGenericSend(COPY_POS) | 标准 FIFO / 紧急插队 / 单槽覆盖 |
xQueueSendFromISR | xQueueGenericSendFromISR | ISR 中向任务发数据 |
xQueueReceive / Peek | xQueueGenericReceive / xQueuePeek | 取数据 / 仅看一眼不拿走 |
xSemaphoreCreateBinary / Counting | xQueueGenericCreate(uxLength,0) + prvInitialiseNewQueue | 资源计数、事件同步、ISR 唤醒任务 |
xSemaphoreCreateMutex / RecursiveMutex | xQueueCreateMutex + prvInitialiseMutex | 共享资源保护(有 PI) |
xSemaphoreTake / Give | xQueueSemaphoreTake / xQueueGenericSend(互斥锁 Give 触发 Disinherit) | 拿/放 信号量/互斥锁 |
xSemaphoreGiveFromISR | xQueueGiveFromISR(特殊:不用 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,自己更高)
十二、常见坑点与注意事项
- FromISR 的 pxHigherPriorityTaskWoken:必须在进入 ISR 时初始化为 pdFALSE;所有 FromISR 调用传这个指针;ISR 末尾用
portYIELD_FROM_ISR(flag)只 yield 一次(不是每个调用都 yield 一次)。
- 互斥锁不能在 ISR Give:因为 Give 互斥锁会触发
xTaskPriorityDisinherit,涉及 TCB.uxPriority 与就绪表搬移,需要调度上下文中进行。xQueueGiveFromISR里有专门configASSERT断言:若 Queue 是 Mutex 且 holder≠NULL → 直接崩溃。
- 队列长度=1 + queueOVERWRITE 很常用:用于"只关心最新值"的场景(例如最新传感器读数),旧数据自动被覆盖。
- 按引用传 vs 按值传:FreeRTOS 队列是按值复制(uxItemSize≠0 时 memcpy),不是传指针。若数据大(如 >64B),建议队列里只存指针,真正的数据用另一种方式管理(静态缓冲区池 + 引用计数/互斥)。
- 不要在回调里阻塞:定时器回调、PendFunctionCall、互斥锁回调都运行在某个任务线程(守护任务或 holder 线程)里,阻塞会影响所有排队的定时器/函数。这与队列 API 本身无关,但实际是 80% 的定时器队列问题的根因。
- 队列集(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 能力。
- 队列IPC 模块&spm=1001.2101.3001.5002&articleId=163834909&d=1&t=3&u=efac9a03a6724546ac40cf0e1511a2ee)
1781

被折叠的 条评论
为什么被折叠?



