FreeRTOS内核(三)队列的核心机制
一、队列的引入(队列解决两大问题)
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)
队列的数据存储区是一个连续的内存块,通过读索引和写索引实现循环使用。
static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
uint8_t * pucQueueStorage,
const uint8_t ucQueueType,
Queue_t * pxNewQueue )
3.核心三:链表实现阻塞与唤醒
这是队列优于普通全局变量的地方:无数据时不轮询,直接休眠让出 CPU。
A. 两个等待链表
队列结构体内部维护了两个链表:
xTasksWaitingToReceive: 等待接收的任务链表(队列空时,读者挂在这里)。xTasksWaitingToSend: 等待发送的任务链表(队列满时,写者挂在这里)。
B. 阻塞 (Block) 流程
当任务 B 读队列,但队列为空(如xQueueReceive(q, &val, 100) 表示最多等 100ms。只要等待时间 不等于 0,一旦队列空,都会调用阻塞流程):
- 记录:将任务 B 的 TCB 插入到
xTasksWaitingToReceive链表中。这样将来有数据时,写任务知道要唤醒谁。 - 状态变更:将任务 B 从 就绪链表 (Ready List) 移除,放入 延迟链表 (Delay List) (状态变为 Blocked)。
- 调度:触发任务切换,CPU 去运行其他就绪任务。
- 结果:任务 B 停止运行,不浪费任何 CPU 周期。
C. 唤醒 (Unblock) 流程
当任务 A 向队列写入数据:
- 检查:发现
xTasksWaitingToReceive链表不为空(有人在等)。 - 移除:从等待链表中取出任务 B 的 TCB。
- 状态变更:将任务 B 从 延迟链表 移回 就绪链表 (状态变为 Ready)。
- 调度:如果任务 B 优先级高于当前任务 A,立即触发切换,任务 B 马上运行并读到数据。
D.超时机制 (Timeout)流程
当你调用 xQueueReceive(q, &val, 100) 表示最多等 100ms
- 计算唤醒时间:
- 当前 Tick 计数 =
xTickCount(例如 500)。 - 超时时间 =
500 + 100 = 600。
- 当前 Tick 计数 =
- 插入延迟链表:
- 任务阻塞时,不仅加入队列的等待链表,还会根据
600这个时间点,按时间顺序插入到内核的全局 延迟链表 (Delayed Task List) 中。 - 延迟链表是按唤醒时间排序的(50, 100, 600, 800...)。
- 任务阻塞时,不仅加入队列的等待链表,还会根据
- SysTick 中断处理:
- 每 1ms,SysTick 中断执行
xTaskIncrementTick()。 - 检查延迟链表头部:如果
头部任务.唤醒时间 <= 当前Tick,说明超时了! - 动作:将该任务从延迟链表移除,加入就绪链表。
- 注意:此时任务虽然就绪了,但从队列角度看,它还没拿到数据。
- 每 1ms,SysTick 中断执行
- 任务恢复运行:
- 任务被调度运行后,从阻塞点醒来。
- 代码会再次检查队列:发现有数据了吗?
- 如果有(刚好被别的任务写了):拷贝数据,返回成功。
- 如果没有(真的是超时了):从队列的等待链表中移除自己,返回
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 队列是 “线程安全” 的(支持任务间 / 中断间读写),无需用户手动处理互斥。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)