队列
队列
FreeRTOS 中,任务之间经常需要进行数据传递。
没有操作系统时,不同函数之间可以通过全局变量、共享内存等方式传递数据;而在 FreeRTOS 中,可以使用队列(Queue)实现任务之间的数据通信。
队列本质上是一块由 FreeRTOS 管理的环形缓冲区 + 队列控制块 + 任务等待链表。
最核心的思想可以概括为:
发送任务把数据放入队列,接收任务从队列取出数据。
如果队列暂时没有数据,接收任务可以阻塞等待;如果队列已经满了,发送任务也可以阻塞等待。
队列的基本结构
一个 FreeRTOS 队列主要由两部分组成:
- Queue_t:队列控制块
- 消息存储区:真正存放消息数据的内存区域
可以简单理解为:
QueueHandle_t
│
▼
┌─────────────────────────┐
│ Queue_t │
│ │
│ pcHead │
│ pcTail │
│ pcWriteTo │
│ pcReadFrom │
│ │
│ uxMessagesWaiting │
│ uxLength │
│ uxItemSize │
│ │
│ xTasksWaitingToSend │
│ xTasksWaitingToReceive │
│ │
│ cTxLock / cRxLock │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ 消息存储区 │
│ │
│ [消息][消息][消息] ... │
│ │
└─────────────────────────┘其中:
uxLength:队列最多能够保存多少个消息。uxItemSize:每个消息占多少字节。uxMessagesWaiting:当前队列中有多少条消息。pcWriteTo:下一次发送数据的位置。pcReadFrom:下一次读取数据的位置。xTasksWaitingToSend:因为队列满而等待发送的任务。xTasksWaitingToReceive:因为队列空而等待接收的任务。cTxLock / cRxLock:队列锁定期间记录发送、接收操作次数。
Queue_t 队列控制块
FreeRTOS 内部使用 Queue_t 表示一个队列。
不同 FreeRTOS 版本的结构体成员可能略有区别,下面以典型实现为例:
typedef struct QueueDefinition
{
/* ================= 存储区域 ================= */
int8_t *pcHead;
int8_t *pcTail;
int8_t *pcWriteTo;
union
{
int8_t *pcReadFrom;
UBaseType_t uxRecursiveCallCount;
} u;
/* ================= 任务等待列表 ================= */
List_t xTasksWaitingToSend;
List_t xTasksWaitingToReceive;
/* ================= 队列状态 ================= */
volatile UBaseType_t uxMessagesWaiting;
UBaseType_t uxLength;
UBaseType_t uxItemSize;
/* ================= 队列锁 ================= */
volatile int8_t cRxLock;
volatile int8_t cTxLock;
/* ================= 其他配置 ================= */
#if ( configSUPPORT_STATIC_ALLOCATION == 1 ) && \
( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
uint8_t ucStaticallyAllocated;
#endif
#if ( configUSE_QUEUE_SETS == 1 )
struct QueueDefinition *pxQueueSetContainer;
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxQueueNumber;
uint8_t ucQueueType;
#endif
} xQUEUE;
typedef xQUEUE Queue_t;这里最值得注意的是:
Queue_t并不是用来直接存放所有消息的。
它更像一个“队列管理器”,里面保存:
队列在哪里、队列多大、现在有多少数据、下一次在哪里读写、哪些任务正在等待等信息。
真正的数据存储在另外一块消息缓冲区中。
队列的数据传递方式
使用队列发送数据时,FreeRTOS 默认采用的是数据拷贝。
例如:
uint32_t data = 100;
xQueueSend(queue, &data, portMAX_DELAY);这里不是把 data 本身放进队列,而是:
任务中的 data
│
│ memcpy
▼
队列内部存储区因此:
data = 200;不会改变已经存进队列里的 100。
这种方式的优点是:
- 数据生命周期由发送方负责;
- 队列拥有自己的数据副本;
- 发送方原来的变量可以随时修改;
- 不容易出现悬空指针问题。
缺点是:
发送数据时需要进行内存拷贝。
如果数据比较大,频繁拷贝会带来一定的性能开销。
使用指针传递大数据
如果消息本身比较大,可以让队列传递一个指针:
MyData *pData;
xQueueSend(queue, &pData, portMAX_DELAY);此时队列拷贝的只是:
指针而不是整个结构体。
因此:
┌───────────────┐
│ 队列 │
│ │
│ pData ──────┼──────────────┐
└───────────────┘ │
▼
┌────────────┐
│ MyData │
│ 大量数据 │
└────────────┘这样可以减少数据拷贝。
但是必须注意:
队列只保存了指针,并不负责管理指针指向的内存。
因此必须保证:
- 指针指向的内存在接收任务使用期间一直有效;
- 不能发送局部变量地址后函数立即返回;
- 如果使用动态内存,需要明确谁负责释放。
所以,指针传递虽然效率更高,但需要自己管理数据生命周期。
队列创建
FreeRTOS 创建队列主要有两种方式:
- 动态创建
- 静态创建
这里主要分析动态创建。
xQueueGenericCreate()
队列创建最终会进入类似:
xQueueGenericCreate()其核心工作并不复杂:
申请一块内存,然后初始化 Queue_t 和消息存储区。
典型流程:
xQueueGenericCreate()
│
├── 检查参数
│
├── 计算消息存储区大小
│
├── 动态申请内存
│
├── 初始化 Queue_t
│
└── 返回 QueueHandle_t典型代码:
QueueHandle_t xQueueGenericCreate(
const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
const uint8_t ucQueueType
)
{
Queue_t *pxNewQueue;
size_t xQueueSizeInBytes;
uint8_t *pucQueueStorage;
configASSERT( uxQueueLength > 0 );
if( uxItemSize == 0 )
{
xQueueSizeInBytes = 0;
}
else
{
xQueueSizeInBytes =
( size_t )( uxQueueLength * uxItemSize );
}
pxNewQueue =
( Queue_t * ) pvPortMalloc(
sizeof( Queue_t ) + xQueueSizeInBytes
);
if( pxNewQueue != NULL )
{
pucQueueStorage =
( ( uint8_t * ) pxNewQueue )
+ sizeof( Queue_t );
prvInitialiseNewQueue(
uxQueueLength,
uxItemSize,
pucQueueStorage,
ucQueueType,
pxNewQueue
);
}
return pxNewQueue;
}队列内存布局
假设:
xQueueCreate(10, sizeof(uint32_t));那么需要:
sizeof(Queue_t)
+
10 × sizeof(uint32_t)的内存。
可以理解成:
低地址
↓
┌──────────────────────────────┐
│ Queue_t │
│ │
│ pcHead │
│ pcTail │
│ pcWriteTo │
│ pcReadFrom │
│ uxLength │
│ uxItemSize │
│ uxMessagesWaiting │
│ ... │
└──────────────────────────────┘
│
│ pcHead
▼
┌──────────────────────────────┐
│ 消息0 │
├──────────────────────────────┤
│ 消息1 │
├──────────────────────────────┤
│ 消息2 │
├──────────────────────────────┤
│ ... │
├──────────────────────────────┤
│ 消息9 │
└──────────────────────────────┘
高地址因此,Queue_t 负责管理队列,而后面的存储区负责真正保存消息。
prvInitialiseNewQueue()
申请内存以后,需要对 Queue_t 进行初始化。
这个工作由:
prvInitialiseNewQueue()完成。
主要包括:
- 设置消息存储区;
- 设置队列长度;
- 设置消息大小;
- 初始化读写指针;
- 清零消息数量;
- 初始化任务等待链表;
- 初始化队列锁。
核心代码:
static void prvInitialiseNewQueue(
const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
uint8_t *pucQueueStorage,
const uint8_t ucQueueType,
Queue_t *pxNewQueue
)
{
( void ) ucQueueType;
if( uxItemSize == 0 )
{
pxNewQueue->pcHead =
( int8_t * ) pxNewQueue;
}
else
{
pxNewQueue->pcHead =
( int8_t * ) pucQueueStorage;
}
pxNewQueue->uxLength = uxQueueLength;
pxNewQueue->uxItemSize = uxItemSize;
( void ) xQueueGenericReset(
pxNewQueue,
pdTRUE
);
#if ( configUSE_TRACE_FACILITY == 1 )
pxNewQueue->ucQueueType = ucQueueType;
#endif
}其中:
uxLength表示:
队列最多能存多少个“消息”。
而:
uxItemSize表示:
每个消息占多少字节。
例如:
xQueueCreate(10, sizeof(MyStruct));表示:
创建一个最多存储 10 个
MyStruct的队列。
xQueueGenericReset()
初始化队列的核心状态最终会进入:
xQueueGenericReset()这个函数负责把队列恢复到“空队列”的初始状态。
核心代码:
pxQueue->u.xQueue.pcTail =
pxQueue->pcHead +
( pxQueue->uxLength * pxQueue->uxItemSize );
pxQueue->u.xQueue.pcReadFrom =
pxQueue->pcHead +
( ( pxQueue->uxLength - 1U )
* pxQueue->uxItemSize );
pxQueue->uxMessagesWaiting = 0;
pxQueue->xRxLock = queueUNLOCKED;
pxQueue->xTxLock = queueUNLOCKED;然后初始化:
xTasksWaitingToSend
xTasksWaitingToReceive两个任务等待链表。
最终形成:
xQueueGenericCreate()
│
▼
申请 Queue_t + 消息存储区
│
▼
prvInitialiseNewQueue()
│
▼
xQueueGenericReset()
│
├── 初始化 pcTail
├── 初始化 pcReadFrom
├── 初始化 pcWriteTo
├── uxMessagesWaiting = 0
├── 初始化发送等待链表
├── 初始化接收等待链表
└── 初始化 Queue Lock至此,一个队列就基本创建完成了。
队列发送
FreeRTOS 的队列发送主要分为:
- 任务级发送;
- 中断级发送。
从发送位置来看,又可以分成:
- 尾部发送:正常发送;
- 头部发送:将数据插入队列头部;
- 覆盖发送:队列满时覆盖原来的数据。
最常用的是:
xQueueSend()它本质上属于:
向队列尾部发送数据。
任务级发送
典型接口:
xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);三个参数分别表示:
xQueue
↓
要操作的队列
pvItemToQueue
↓
要发送的数据地址
xTicksToWait
↓
队列满时最多等待多长时间例如:
uint32_t data = 100;
xQueueSend(
MessageQueue,
&data,
portMAX_DELAY
);如果队列没有满:
发送任务
│
│ memcpy
▼
队列立即发送成功。
如果队列满了:
队列满
│
▼
当前任务是否允许等待?
│
├── 不等待
│ └── 立即返回 errQUEUE_FULL
│
└── 允许等待
│
▼
加入 xTasksWaitingToSend
│
▼
进入阻塞态xQueueGenericSend()
最终普通队列发送会进入:
xQueueGenericSend()整个函数可以分成两个核心阶段:
第一阶段:尝试立即发送。
第二阶段:如果队列满,则阻塞等待。
队列未满时的发送流程
核心逻辑:
taskENTER_CRITICAL();
if( pxQueue->uxMessagesWaiting < pxQueue->uxLength )
{
prvCopyDataToQueue(
pxQueue,
pvItemToQueue,
xCopyPosition
);
...
}
taskEXIT_CRITICAL();首先进入临界区,防止在修改队列内部状态时被其他中断干扰。
然后判断:
uxMessagesWaiting < uxLength如果成立,说明队列还有空间。
于是调用:
prvCopyDataToQueue()把用户数据拷贝到队列存储区。
发送后唤醒接收任务
假设:
任务A:正在等待接收队列数据
任务B:向队列发送数据任务 A 因为队列为空进入阻塞:
xTasksWaitingToReceive
│
▼
任务A此时任务 B 发送数据:
任务B
│
│ Send
▼
队列
│
│ 有数据
▼
唤醒任务AFreeRTOS 会检查:
xTasksWaitingToReceive如果里面有任务:
xTaskRemoveFromEventList()将等待任务移出事件列表并恢复到就绪状态。
如果被唤醒的任务优先级更高,就可能立即发生任务切换。
因此:
发送数据不仅仅是“把数据复制进队列”,还可能导致等待接收任务被唤醒。
队列已满时的发送
如果:
uxMessagesWaiting == uxLength说明队列已经满了。
此时根据:
xTicksToWait决定是否等待。
如果:
xTicksToWait == 0表示:
不等待,立即返回。
于是:
return errQUEUE_FULL;如果:
xTicksToWait > 0则当前任务允许阻塞等待。
FreeRTOS 首先记录超时信息:
vTaskSetTimeOutState( &xTimeOut );然后:
挂起调度器
│
▼
锁定队列
│
▼
再次确认队列是否仍然满
│
▼
加入 xTasksWaitingToSend
│
▼
阻塞当前任务
│
▼
其他任务运行当其他任务从队列取走数据后:
队列出现空位
│
▼
唤醒发送任务
│
▼
发送任务重新运行
│
▼
重新检查队列
│
▼
发送数据这里非常重要:
任务从阻塞态恢复以后,不是直接假设队列一定有空位,而是重新回到循环,再次检查队列状态。
这样可以避免并发情况下的状态变化造成错误。
xQueueGenericSend() 总体流程
xQueueGenericSend()
│
▼
进入临界区
│
▼
队列是否有空间?
┌────┴────┐
│ │
是 否
│ │
▼ ▼
拷贝数据 xTicksToWait == 0 ?
│ │
│ ┌─┴─┐
│ │ │
│ 是 否
│ │ │
│ ▼ ▼
│ 返回 记录超时信息
│ FULL │
│ ▼
│ 挂起调度器
│ │
│ 锁定队列
│ │
│ 仍然满?
│ ┌──┴──┐
│ │ │
│ 是 否
│ │ │
│ ▼ ▼
│ 加入发送 重新尝试
│ 等待列表
│ │
│ ▼
│ 阻塞等待
│
▼
唤醒等待接收任务
│
▼
可能触发任务切换
│
▼
返回 pdPASS队列接收
队列接收与发送基本是完全对称的。
常见接口:
xQueueReceive()最终进入:
xQueueGenericReceive()核心逻辑就是:
有数据 → 取数据;没数据 → 根据等待时间决定是否阻塞。
xQueueGenericReceive()
首先检查:
uxMessagesWaiting如果:
uxMessagesWaiting > 0说明队列里面存在数据。
于是:
prvCopyDataFromQueue()把数据复制到用户提供的缓冲区。
例如:
uint32_t data;
xQueueReceive(
MessageQueue,
&data,
portMAX_DELAY
);最终:
队列内部数据
│
│ memcpy
▼
用户变量 data接收数据后唤醒发送任务
假设:
任务A:队列已经满,等待发送
任务B:从队列中接收数据任务 A:
xTasksWaitingToSend
│
▼
任务A任务 B 接收一个消息:
队列满
│
│ Receive
▼
少一个消息
│
▼
出现空位
│
▼
唤醒任务A所以接收数据之后,FreeRTOS 会检查:
xTasksWaitingToSend如果存在等待发送任务,就将其唤醒。
这与发送操作正好对应:
发送数据
↓
唤醒接收任务
接收数据
↓
唤醒发送任务队列为空时的接收
如果:
uxMessagesWaiting == 0说明队列为空。
此时同样根据:
xTicksToWait判断是否等待。
如果:
xTicksToWait == 0直接:
return errQUEUE_EMPTY;如果允许等待:
队列为空
│
▼
记录超时时间
│
▼
挂起调度器
│
▼
锁定队列
│
▼
再次检查队列
│
▼
仍然为空?
│
▼
加入 xTasksWaitingToReceive
│
▼
当前任务阻塞之后其他任务发送数据:
其他任务 Send
│
▼
队列出现数据
│
▼
唤醒等待接收任务
│
▼
接收任务恢复运行
│
▼
重新检查队列
│
▼
ReceivexQueueGenericReceive() 总体流程
xQueueGenericReceive()
│
▼
进入临界区
│
▼
队列是否有数据?
┌────┴────┐
│ │
是 否
│ │
▼ ▼
复制数据 xTicksToWait == 0 ?
│ │
│ ┌─┴─┐
│ │ │
│ 是 否
│ │ │
│ ▼ ▼
│ 返回 记录超时
│ EMPTY │
│ ▼
│ 挂起调度器
│ │
│ 锁定队列
│ │
│ 再次检查
│ │
│ 队列仍为空?
│ ┌───┴───┐
│ │ │
│ 是 否
│ │ │
│ ▼ ▼
│ 加入接收 重新尝试
│ 等待列表
│ │
│ ▼
│ 阻塞等待
│
▼
减少消息数量
│
▼
唤醒等待发送任务
│
▼
可能触发任务切换
│
▼
返回 pdPASS队列的上锁与解锁
队列源码中比较容易让人困惑的是:
prvLockQueue()和:
prvUnlockQueue()这里需要特别注意:
队列锁并不等同于互斥锁。
它的目的主要是:
当调度器被挂起、队列暂时不能立即处理任务唤醒时,先记录队列发生了多少次发送/接收操作,等之后统一处理。
为什么需要队列锁
假设某个任务执行:
vTaskSuspendAll();此时:
调度器被挂起,不能发生正常的任务切换。
但是在调度器挂起期间,队列本身仍可能发生变化。
例如:
任务A正在等待接收
│
▼
xTasksWaitingToReceive
│
▼
任务A
调度器暂时被挂起
↓
某处向队列发送数据
↓
队列确实有数据了
↓
理论上应该唤醒任务A但是此时调度器处于挂起状态,不能马上进行正常的任务调度。
所以 FreeRTOS 不会立即处理完整的唤醒流程,而是:
先记录:“队列发生了一次发送操作”。
等队列解锁以后,再根据记录统一处理。
cTxLock 和 cRxLock
这两个变量最容易搞反。
可以直接记:
| 变量 | 表示什么 | 锁期间发生什么 | 解锁后主要处理 |
|---|---|---|---|
cTxLock | 发送方向发生的次数 | 有数据发送进入队列 | 唤醒等待接收的任务 |
cRxLock | 接收方向发生的次数 | 有数据从队列取出 | 唤醒等待发送的任务 |
所以:
发送数据
│
▼
cTxLock
│
▼
最终处理 xTasksWaitingToReceive而:
接收数据
│
▼
cRxLock
│
▼
最终处理 xTasksWaitingToSend可以这样记忆:
Tx = Transmit = 发送数据 → 有数据了 → 唤醒 Receiver。
Rx = Receive = 接收数据 → 有空位了 → 唤醒 Sender。
prvLockQueue()
prvLockQueue() 本质上就是将:
cRxLock
cTxLock从:
queueUNLOCKED变成:
queueLOCKED_UNMODIFIED之后,如果队列在锁定期间发生发送或接收操作,就会增加对应计数。
注意这里你原笔记里的代码有一个明显笔误:
if( ( pxQueue )->cTxLock == queueUNLOCKED )
{
( pxQueue )->cRxLock = queueLOCKED_UNMODIFIED;
}这里应该是:
if( ( pxQueue )->cTxLock == queueUNLOCKED )
{
( pxQueue )->cTxLock = queueLOCKED_UNMODIFIED;
}否则会把 cRxLock 设置两次,而 cTxLock 没有正确上锁。
prvUnlockQueue()
队列解锁时,FreeRTOS 会检查:
cTxLock和:
cRxLock记录了多少次操作。
首先处理:
cTxLock因为它表示:
队列锁定期间发生了多少次发送。
如果:
cTxLock > queueLOCKED_UNMODIFIED说明锁定期间确实有数据发送进队列。
那么就检查:
xTasksWaitingToReceive如果有任务等待接收,就将等待任务移出事件列表。
然后:
cTxLock--;继续处理下一次发送事件。
cRxLock 的处理
接下来处理:
cRxLock它表示:
队列锁定期间发生了多少次接收。
接收意味着:
队列中的消息减少
↓
队列出现空位
↓
可能有等待发送任务可以继续运行因此解锁时检查:
xTasksWaitingToSend如果存在等待发送任务:
xTaskRemoveFromEventList()唤醒它。
队列锁的完整逻辑
可以把整个过程理解成:
调度器挂起
│
▼
队列加锁
│
┌──────────┴──────────┐
│ │
发生发送 发生接收
│ │
▼ ▼
cTxLock++ cRxLock++
│ │
│ │
└──────────┬──────────┘
│
▼
队列解锁
│
┌───────┴───────┐
│ │
处理 cTxLock 处理 cRxLock
│ │
▼ ▼
唤醒等待接收任务 唤醒等待发送任务
│ │
└───────┬───────┘
▼
记录任务切换
│
▼
恢复调度器
│
▼
必要时触发 PendSV这里最关键的一点是:
队列锁不是为了禁止队列读写,而是为了延迟处理“队列状态变化所导致的任务唤醒”。
队列发送与接收的整体关系
把发送和接收放到一起就非常清楚了。
队列
│
┌──────────┴──────────┐
│ │
Send Receive
│ │
▼ ▼
队列增加数据 队列减少数据
│ │
▼ ▼
是否有等待接收? 是否有等待发送?
│ │
▼ ▼
唤醒 Receive Task 唤醒 Send Task也就是:
队列
│
┌──────────┴──────────┐
│ │
▼ ▼
发送 接收
│ │
▼ ▼
数据 +1 数据 -1
│ │
▼ ▼
唤醒等待接收任务 唤醒等待发送任务任务级 API 与中断级 API
FreeRTOS 队列 API 分为:
任务级 API和:
中断级 API例如:
xQueueSend()
xQueueReceive()属于任务级 API。
而:
xQueueSendFromISR()
xQueueReceiveFromISR()属于中断级 API。
为什么中断 API 没有阻塞时间
任务级发送:
xQueueSend(
queue,
data,
xTicksToWait
);有:
xTicksToWait因为任务可以进入阻塞态。
例如:
队列满
↓
任务A进入阻塞
↓
任务B运行
↓
任务B取走数据
↓
任务A恢复但是中断不能这样做。
中断执行期间:
不能让 ISR 自己进入阻塞态等待其他任务操作队列。
所以:
xQueueSendFromISR()
xQueueReceiveFromISR()没有:
xTicksToWait因为中断只能:
立即执行,立即返回。
pxHigherPriorityTaskWoken
中断级 API 通常会多一个参数:
BaseType_t xHigherPriorityTaskWoken;例如:
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(
MessageQueue,
&data,
&xHigherPriorityTaskWoken
);这个变量不是让用户自己决定“要不要切换”。
而是:
FreeRTOS 在 ISR 内部通过这个变量告诉我们:这次队列操作是否唤醒了一个更高优先级任务。
例如:
ISR
│
│ Send
▼
队列
│
▼
唤醒高优先级任务
│
▼
xHigherPriorityTaskWoken = pdTRUE
│
▼
portYIELD_FROM_ISR()
│
▼
退出 ISR 后进行任务切换因此典型写法:
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(
queue,
&data,
&xHigherPriorityTaskWoken
);
portYIELD_FROM_ISR(
xHigherPriorityTaskWoken
);这里的核心思想是:
ISR 中不直接进行普通任务切换,而是先记录“是否需要切换”,然后在退出中断时根据这个结果决定是否触发 PendSV。
队列阻塞与任务调度
队列和 FreeRTOS 调度器之间的关系可以总结成:
任务调用 xQueueReceive()
│
▼
队列为空?
┌────┴────┐
│ │
否 是
│ │
▼ ▼
立即取数据 是否允许等待?
│
┌──┴──┐
│ │
否 是
│ │
▼ ▼
返回错误 加入等待链表
│
▼
阻塞
│
▼
其他任务运行
│
▼
其他任务 Send
│
▼
唤醒 Receive Task
│
▼
进入 Ready List
│
▼
如果优先级足够高
│
▼
PendSV
│
▼
切换到接收任务所以,队列不仅仅是一个“数组”。
它实际上把:
数据存储
+
任务阻塞
+
任务唤醒
+
调度这几件事情结合到了一起。
队列的核心数据结构关系
最后把整个队列内部结构串起来:
QueueHandle_t
│
▼
┌────────────────────┐
│ Queue_t │
│ │
│ uxLength │
│ uxItemSize │
│ uxMessagesWaiting │
│ │
│ pcHead │
│ pcTail │
│ pcWriteTo │
│ pcReadFrom │
│ │
│ cTxLock │
│ cRxLock │
│ │
│ xTasksWaiting │
│ ToSend │
│ │
│ xTasksWaiting │
│ ToReceive │
└─────────┬──────────┘
│
▼
┌─────────────────────┐
│ 消息存储区 │
│ │
│ [ ][ ][ ][ ][ ]... │
└─────────────────────┘发送:
Send
↓
prvCopyDataToQueue()
↓
uxMessagesWaiting++
↓
唤醒 xTasksWaitingToReceive接收:
Receive
↓
prvCopyDataFromQueue()
↓
uxMessagesWaiting--
↓
唤醒 xTasksWaitingToSend队列满:
Send
↓
无法写入
↓
等待 xTasksWaitingToSend
↓
阻塞队列空:
Receive
↓
没有数据
↓
等待 xTasksWaitingToReceive
↓
阻塞队列的核心思想
学习 FreeRTOS 队列,不需要一开始就把所有源码都背下来。
真正需要抓住的是下面这条主线:
Queue
│
┌───────────┴───────────┐
│ │
Send Receive
│ │
▼ ▼
队列增加数据 队列减少数据
│ │
▼ ▼
唤醒等待 Receive 唤醒等待 Send
│ │
└───────────┬───────────┘
│
▼
任务进入 Ready
│
▼
优先级判断 / 调度
│
▼
PendSV
│
▼
上下文切换因此,从 FreeRTOS 内核的角度来看:
队列 = 数据缓冲机制 + 任务阻塞机制 + 任务唤醒机制。
而 xQueueGenericSend() 和 xQueueGenericReceive() 的源码,本质上都围绕着这三件事情展开:
- 队列当前能不能操作?
- 如果不能,当前任务要不要阻塞?
- 队列状态发生变化以后,要唤醒哪个等待任务?
把这三件事情搞明白以后,再去看 prvCopyDataToQueue()、prvCopyDataFromQueue()、prvLockQueue()、prvUnlockQueue() 等函数,源码就不会显得那么散乱了。

