队列集
- 通常在实际的产品设计中,我们对硬件的原始数据队列和参与操作的软件数据队列应该是分开设计
- 这样能保证我们的业务逻辑能更加清晰独立,并且硬件读取逻辑可以再不同产品中复用
- 会导致一个问题,我们需要在每个硬件读取后进行相应的数据转化任务,如果硬件过多会导致任务栈占据过多的内存,且使任务切换过于频繁污染了时间片
- 因此我们可以使用到队列集,使得写入同一个业务的软件队列的原始数据队列转换达成统一,使得任务数量减少,并且更容易修改移植我们的代码
业务逻辑
红外遥控器驱动程序里,它只应该把键值记录下来、写入某个队列,它不应该把键值转换为游戏的控制键。在红外遥控器的驱动程序里,不应该有游戏相关的代码,这样,切换使用场景时,这个驱动程序还可以继续使用。把红外遥控器的按键转换为游戏的控制键,应该在游戏的任务里实现。要支持多个输入设备时,我们需要实现一个"InputTask",它读取各个设备的队列,得到数据后再分别转换为游戏的控制键。
队列集的本质也是队列,只不过里面存放的是"队列句柄"。使用过程如下:
- 创建队列A,它的长度是n1
- 创建队列B,它的长度是n2
- 创建队列集S,它的长度是"n1+n2"
- 把队列A、B加入队列集S
- 这样,写队列A的时候,会顺便把队列A的句柄写入队列集S
- 这样,写队列B的时候,会顺便把队列B的句柄写入队列集S
- InputTask先读取队列集S,它的返回值是一个队列句柄,这样就可以知道哪个队列有有数据了;然后InputTask再读取这个队列句柄得到数据。
内部机制
每当在一个队列写入数据,系统自动在这个队列在的队列集的队列中写入一个此队列句柄,当队列集读取时就会依次返回出他的队列中存放的句柄,然后再凭借返回的句柄操作对应的队列
创建队列集
c
QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength )
uxQueueLength:队列集长度,可存放的队列句柄数
返回值非0:成功,返回句柄,以后使用句柄来操作队列
NULL:失败,因为内存不足
加入队列
c
BaseType_t xQueueAddToSet(
QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet
);
xQueueOrSemaphore:队列句柄,要放入的队列
xQueueSet:队列集句柄
返回值pdTRUE:成功
pdFALSE:失败
读取非空队列
c
QueueSetMemberHandle_txQueueSelectFromSet(
QueueSetHandle_t xQueueSet,
TickType_t const xTicksToWait
);
xQueueSet:队列集句柄
xTicksToWait:如果队列集空则无法读出数据,可以让任务进入阻塞状态,如果被设为0,无法读出数据时函数会立刻返回;如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写
返回值NULL:失败
队列句柄:成功
从队列集移除成员
c
BaseType_t xQueueRemoveFromSet(
QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet
);
xQueueOrSemaphore:队列句柄,要放入的队列
xQueueSet:队列集句柄
返回值pdTRUE:失败
pdFALSE:成功
删除队列集
c
void vQueueDeleteSet(QueueSetHandle_t xQueueSet);
xQueueSet:队列集句柄
注意:需先移除所有成员,否则可能导致内存泄漏!!!
简单程序示例
c
// 1. 创建队列和队列集
QueueHandle_t xKeyQueue = xQueueCreate(5, sizeof(uint8_t)); // 按键队列
QueueHandle_t xUartQueue = xQueueCreate(3, sizeof(char)); // 串口队列
QueueSetHandle_t xInputSet = xQueueCreateSet(5 + 3); // 队列集
// 2. 将队列加入队列集
xQueueAddToSet(xKeyQueue, xInputSet);
xQueueAddToSet(xUartQueue, xInputSet);
// 3. 数据写入任务(例如:按键中断、串口中断)
void vWriteTask(void *pvParameters) {
uint8_t key = 0x01;
xQueueSend(xKeyQueue, &key, 0); // 写入按键队列,自动触发队列集
}
// 4. InputTask:统一处理
void vInputTask(void *pvParameters) {
QueueSetMemberHandle_t xActivatedMember;
uint8_t keyData;
char uartData;
while (1) {
// 等待队列集有数据
xActivatedMember = xQueueSelectFromSet(xInputSet, portMAX_DELAY);
// 判断是哪个队列有数据
if (xActivatedMember == xKeyQueue) {
xQueueReceive(xKeyQueue, &keyData, 0);
// 处理按键数据(如转换为游戏控制键)
} else if (xActivatedMember == xUartQueue) {
xQueueReceive(xUartQueue, &uartData, 0);
// 处理串口数据
}
}
}
注意事项
- 队列集长度的设置
队列集长度需 ≥ 所有成员队列的总长度(或信号量的最大计数值之和)。例如:队列 A 长度为 5,队列 B 长度为 3,则队列集长度应 ≥ 8,最坏情况下,两个队列同时满,队列集需存放 8 个句柄 - 成员的 "独占性"
队列 / 信号量加入队列集后,不应再直接读写,否则会导致队列集状态不同步。需通过队列集机制间接访问,先xQueueSelectFromSet() 获取句柄,再读写对应队列,这个函数类似于xQueueReceive()的阻塞机制 - 中断中使用队列集
若在中断中操作队列集成员,需使用中断安全的队列读取函数( xQueueSendFromISR())