跳到主要内容

第11章 队列(queue)

队列(queue)可以用于"任务到任务"、"任务到中断"、"中断到任务"直接传输信息。

本章涉及如下内容:

  • 怎么创建、清除、删除队列
  • 队列中消息如何保存
  • 怎么向队列发送数据、怎么从队列读取数据、怎么覆盖队列的数据
  • 在队列上阻塞是什么意思
  • 怎么在多个队列上阻塞
  • 读写队列时如何影响任务的优先级

11.1 队列的特性

1.1.1 常规操作

队列的简化操如入下图所示,从此图可知:

  • 队列可以包含若干个数据:队列中有若干项,这被称为"长度"(length)
  • 每个数据大小固定
  • 创建队列时就要指定长度、数据大小
  • 数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读
  • 也可以强制写队列头部:覆盖头部数据

更详细的操作入下图所示:

11.1.2 传输数据的两种方法

使用队列传输数据时有两种方法:

  • 拷贝:把数据、把变量的值复制进队列里
  • 引用:把数据、把变量的地址复制进队列里

FreeRTOS使用拷贝值的方法,这更简单:

  • 局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据
  • 无需分配buffer来保存数据,队列中有buffer
  • 局部变量可以马上再次使用
  • 发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据
  • 如果数据实在太大,你还是可以使用队列传输它的地址
  • 队列的空间有FreeRTOS内核分配,无需任务操心
  • 对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把数据复制出队列。

11.1.3 队列的阻塞访问

只要知道队列的句柄,谁都可以读、写该队列。任务、ISR都可读、写队列。可以多个任务读写队列。

任务读写队列时,简单地说:如果读写不成功,则阻塞;可以指定超时时间。口语化地说,就是可以定个闹钟:如果能读写了就马上进入就绪态,否则就阻塞直到超时。

某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞状态:还可以指定阻塞的时间。如果队列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。

既然读取队列的任务个数没有限制,那么当多个任务读取空队列时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态?

  • 优先级最高的任务
  • 如果大家的优先级相同,那等待时间最久的任务会进入就绪态

跟读队列类似,一个任务要写队列时,如果队列满了,该任务也可以进入阻塞状态:还可以指定阻塞的时间。如果队列有空间了,则该阻塞的任务会变为就绪态。如果一直都没有空间,则时间到之后它也会进入就绪态。

既然写队列的任务个数没有限制,那么当多个任务写"满队列"时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的空间。当队列中有空间时,哪个任务会进入就绪态?

  • 优先级最高的任务
  • 如果大家的优先级相同,那等待时间最久的任务会进入就绪态

11.2 队列函数

使用队列的流程:创建队列、写队列、读队列、删除队列。

11.2.1 创建

队列的创建有两种方法:动态分配内存、静态分配内存,

  • 动态分配内存:xQueueCreate,队列的内存在函数内部动态分配

函数原型如下:

QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数说明
uxQueueLength队列长度,最多能存放多少个数据(item)
uxItemSize每个数据(item)的大小:以字节为单位
返回值非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为内存不足
  • 静态分配内存:xQueueCreateStatic,队列的内存要事先分配好

函数原型如下:

QueueHandle_t xQueueCreateStatic(*
UBaseType_t uxQueueLength,*
UBaseType_t uxItemSize,*
uint8_t *pucQueueStorageBuffer,*
StaticQueue_t *pxQueueBuffer*
);
参数说明
uxQueueLength队列长度,最多能存放多少个数据(item)
uxItemSize每个数据(item)的大小:以字节为单位
pucQueueStorageBuffer如果uxItemSize非0,pucQueueStorageBuffer必须指向一个uint8_t数组, 此数组大小至少为"uxQueueLength * uxItemSize"
pxQueueBuffer必须执行一个StaticQueue_t结构体,用来保存队列的数据结构
返回值非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为pxQueueBuffer为NULL

示例代码:

// 示例代码
#define QUEUE_LENGTH 10
#define ITEM_SIZE sizeof( uint32_t )

// xQueueBuffer用来保存队列结构体
StaticQueue_t xQueueBuffer;

// ucQueueStorage 用来保存队列的数据

// 大小为:队列长度 * 数据大小
uint8_t ucQueueStorage[ QUEUE_LENGTH * ITEM_SIZE ];

void vATask( void *pvParameters )
{
QueueHandle_t xQueue1;

// 创建队列: 可以容纳QUEUE_LENGTH个数据,每个数据大小是ITEM_SIZE
xQueue1 = xQueueCreateStatic( QUEUE_LENGTH,
ITEM_SIZE,
ucQueueStorage,
&xQueueBuffer );
}

11.2.2 复位

队列刚被创建时,里面没有数据;使用过程中可以调用 xQueueReset() 把队列恢复为初始状态,此函数原型为:

/*  pxQueue : 复位哪个队列;
* 返回值: pdPASS(必定成功)
*/
BaseType_t xQueueReset( QueueHandle_t pxQueue);

11.2.3 删除

删除队列的函数为 vQueueDelete() ,只能删除使用动态方法创建的队列,它会释放内存。原型如下:

void vQueueDelete( QueueHandle_t xQueue );

11.2.4 写队列

可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:

/* 等同于xQueueSendToBack
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);

/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);


/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);

/*
* 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);

/*
* 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);

这些函数用到的参数是类似的,统一说明如下:

参数说明
xQueue队列句柄,要写哪个队列
pvItemToQueue数据指针,这个数据的值会被复制进队列, 复制多大的数据?在创建队列时已经指定了数据大小
xTicksToWait如果队列满则无法写入新数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法写入数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写
返回值pdPASS:数据成功写入了队列 errQUEUE_FULL:写入失败,因为队列满了。

11.2.5 读队列

使用 xQueueReceive() 函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:

BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );

BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken
);

参数说明如下:

参数说明
xQueue队列句柄,要读哪个队列
pvBufferbufer指针,队列的数据会被复制到这个buffer 复制多大的数据?在创建队列时已经指定了数据大小
xTicksToWait果队列空则无法读出数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法读出数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写
返回值pdPASS:从队列读出数据入 errQUEUE_EMPTY:读取失败,因为队列空了。

11.2.6 查询

可以查询队列中有多少个数据、有多少空余空间。函数原型如下:

/*
* 返回队列中可用数据的个数
*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );

/*
* 返回队列中可用空间的个数
*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );

11.2.7 覆盖/偷看

当队列长度为1时,可以使用 xQueueOverwrite()xQueueOverwriteFromISR() 来覆盖数据。

注意,队列长度必须为1。当队列满时,这些函数会覆盖里面的数据,这也以为着这些函数不会被阻塞。

函数原型如下:

/* 覆盖队列
* xQueue: 写哪个队列
* pvItemToQueue: 数据地址
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueueOverwrite(
QueueHandle_t xQueue,
const void * pvItemToQueue
);

BaseType_t xQueueOverwriteFromISR(
QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);

如果想让队列中的数据供多方读取,也就是说读取时不要移除数据,要留给后来人。那么可以使用"窥视",也就是xQueuePeek()xQueuePeekFromISR()。这些函数会从队列中复制出数据,但是不移除数据。这也意味着,如果队列中没有数据,那么"偷看"时会导致阻塞;一旦队列中有数据,以后每次"偷看"都会成功。

函数原型如下:

/* 偷看队列
* xQueue: 偷看哪个队列
* pvItemToQueue: 数据地址, 用来保存复制出来的数据
* xTicksToWait: 没有数据的话阻塞一会
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueuePeek(
QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait
);

BaseType_t xQueuePeekFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
);

11.3 示例: 队列的基本使用

本节代码为:13_queue_game。以前使用环形缓冲区传输红外遥控器的数据,本程序改为使用队列。

11.3.1 程序框架

01_game_template使用轮询的方式从环形缓冲区读取红外遥控器的键值,13_queue_game把环形缓冲区改为队列。

13_queue_game程序的框架如下:

game1_task:游戏的主要逻辑判断,每次循环就移动一下球,判断球是否跟边沿、砖块、挡球板相碰,进而调整球的移动方向、消减砖块、统计分数。

platform_task:挡球板任务,根据遥控器左右移动挡球板。

IRReceiver_IRQ_Callback解析出遥控器键值后,写队列g_xQueuePlatform。

11.3.2 源码分析

IRReceiver_IRQ_Callback中断回调函数里,识别出红外遥控键值后,构造一个struct input_data结构体,然后使用xQueueSendFromISR函数把它写入队列g_xQueuePlatform。

写队列的代码如下:

struct input_data idata;

idata.dev = 0;

idata.val = 0;

xQueueSendToBackFromISR(g_xQueuePlatform, &idata, NULL);

挡球板任务从队列g_xQueuePlatform中读取数据,操作挡球板。代码如下:

01 /* 挡球板任务 */

02 static void platform_task(void *params)

03 {

04 byte platformXtmp = platformX;

05 uint8_t dev, data, last_data;

06 struct input_data idata;

07

08 // Draw platform

09 draw_bitmap(platformXtmp, g_yres - 8, platform, 12, 8, NOINVERT, 0);

10 draw_flushArea(platformXtmp, g_yres - 8, 12, 8);

11

12 while (1)

13 {

14 /* 读取红外遥控器 */

15 //if (0 == IRReceiver_Read(&dev, &data))

16 if (pdPASS == xQueueReceive(g_xQueuePlatform, &idata, portMAX_DELAY))

17 {

18 data = idata.val;

19 if (data == 0x00)

20 {

21 data = last_data;

22 }

23

24 if (data == 0xe0) /* Left */

25 {

26 btnLeft();

27 }

28

29 if (data == 0x90) /* Right */

30 {

31 btnRight();

32 }

33 last_data = data;

第15行是原来的代码,它使用轮询的方式读取遥控键值,效率很低。

第16行开始改为读取队列,如果没有数据,挡球板任务阻塞,在第16行的函数里不出来;当IRReceiver_IRQ_Callback中断回调函数把数据写入队列后,挡球板任务马上被唤醒,从第16行的函数里出来,继续执行后续代码。

11.3.3 上机实验

烧录程序后,使用红外遥控器的左、右按键移动挡球板。

11.4 示例: 使用队列实现多设备输入

本节代码为:14_queue_game_multi_input。

11.5 队列集

假设有2个输入设备:红外遥控器、旋转编码器,它们的驱动程序应该专注于“产生硬件数据”,不应该跟“业务有任何联系”。比如:红外遥控器驱动程序里,它只应该把键值记录下来、写入某个队列,它不应该把键值转换为游戏的控制键。在红外遥控器的驱动程序里,不应该有游戏相关的代码,这样,切换使用场景时,这个驱动程序还可以继续使用。

把红外遥控器的按键转换为游戏的控制键,应该在游戏的任务里实现。

要支持多个输入设备时,我们需要实现一个“InputTask”,它读取各个设备的队列,得到数据后再分别转换为游戏的控制键。

InputTask如何及时读取到多个队列的数据?要使用队列集。

队列集的本质也是队列,只不过里面存放的是“队列句柄”。使用过程如下:

  • 创建队列A,它的长度是n1
  • 创建队列B,它的长度是n2
  • 创建队列集S,它的长度是“n1+n2”
  • 把队列A、B加入队列集S
  • 这样,写队列A的时候,会顺便把队列A的句柄写入队列集S
  • 这样,写队列B的时候,会顺便把队列B的句柄写入队列集S
  • InputTask先读取队列集S,它的返回值是一个队列句柄,这样就可以知道哪个队列有有数据了;然后InputTask再读取这个队列句柄得到数据。

11.5.1 创建队列集

函数原型如下:

QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength )
参数说明
uxQueueLength队列集长度,最多能存放多少个数据(队列句柄)
返回值非0:成功,返回句柄,以后使用句柄来操作队列NULL:失败,因为内存不足

11.5.2 把队列加入队列集

函数原型如下:

BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,

QueueSetHandle_t xQueueSet );


参数说明
xQueueOrSemaphore队列句柄,这个队列要加入队列集
xQueueSet队列集句柄
返回值pdTRUE:成功pdFALSE:失败

11.5.3 读取队列集

函数原型如下:

QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,

TickType_t const xTicksToWait );
参数说明
xQueueSet队列集句柄
xTicksToWait如果队列集空则无法读出数据,可以让任务进入阻塞状态,xTicksToWait表示阻塞的最大时间(Tick Count)。如果被设为0,无法读出数据时函数会立刻返回;如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写
返回值NULL:失败,队列句柄:成功

11.6 示例: 使用队列集改善程序框架

本节代码为:15_queueset_game。

11.7 示例12: 遥控器数据分发给多个任务

本节代码为:17_queue_car_dispatch。

11.7.1 程序框架

17_queue_car_dispatch实现了另一个游戏:使用红外遥控器的1、2、3分别控制3辆汽车。

框架如下:

car1_task、car2_task、car3_task:创建自己的队列,并注册给devices\irda\dev_irda.c;读取队列,根据遥控器键值移动汽车。

IRReceiver_IRQ_Callback解析出遥控器键值后,写多个队列。

11.7.2 源码分析

从上往上分析,任务入口函数代码如下:

01 static void CarTask(void *params)

02 {

03 struct car *pcar = params;

04 struct ir_data idata;

05

06 /* 创建自己的队列 */

07 QueueHandle_t xQueueIR = xQueueCreate(10, sizeof(struct ir_data));

08

09 /* 注册队列 */

10 RegisterQueueHandle(xQueueIR);

11

12 /* 显示汽车 */

13 ShowCar(pcar);

14

15 while (1)

16 {

17 /* 读取按键值:读队列 */

18 xQueueReceive(xQueueIR, &idata, portMAX_DELAY);

19

20 /* 控制汽车往右移动 */

21 if (idata.val == pcar->control_key)

22 {

23 if (pcar->x < g_xres - CAR_LENGTH)

24 {

25 /* 隐藏汽车 */

26 HideCar(pcar);

27

28 /* 调整位置 */

29 pcar->x += 20;

30 if (pcar->x > g_xres - CAR_LENGTH)

31 {

32 pcar->x = g_xres - CAR_LENGTH;

33 }

34

35 /* 重新显示汽车 */

36 ShowCar(pcar);

37 }

38 }

39 }

40 }

第07行创建自己的队列,第10行把这个队列注册进底层的红外驱动。

红外驱动程序解析出按键值后,把数据写入多个队列,代码如下:

	/* 创建3个汽车任务 */

\#if 0

for (i = 0; i < 3; i++)

{

draw_bitmap(g_cars[i].x, g_cars[i].y, carImg, 15, 16, NOINVERT, 0);

draw_flushArea(g_cars[i].x, g_cars[i].y, 15, 16);

}

\#endif

xTaskCreate(CarTask, "car1", 128, &g_cars[0], osPriorityNormal, NULL);

xTaskCreate(CarTask, "car2", 128, &g_cars[1], osPriorityNormal, NULL);

xTaskCreate(CarTask, "car3", 128, &g_cars[2], osPriorityNormal, NULL);

}

11.7.3 上机实验

烧录程序后,使用红外遥控器的1、2、3按键分别移动三辆汽车。