队列管理
队列提供了任务到任务、任务到中断和中断到任务的通信机制。
队列的特征
数据存储
队列可以保存有限数量的固定大小的数据项。一个队列所能容纳的最大条目数称为它的长度。每个数据项的长度和大小都在创建队列时设置。
队列通常用作先进先出(FIFO)缓冲区,其中数据被写入队列的末端,并从队列的头部删除。
图31展示了向用作FIFO的队列写入和读取数据的过程。
- 创建一个队列。允许任务A和任务B通信。队列中最多可以容纳五个整数。当队列被创建时,它不包含任何值,因此为空。
- 任务A将一个局部变量的值写入到队列的后面。由于队列之前是空的,所以写入的值现在是队列中唯一的项。因此它既是队列后的值也是队列前的值。
- 任务A改变本地变量的值,并再次写入队列。队列现在包含两个值。第一个写入的值保持在队列前面,新值插入到队列的末尾。
- 任务B从队列头读取一个值。
- 任务B删除了一个项,只留下任务A写入的第二个值留在队列中。
有两种方式可以实现队列行为:
- 复制队列:将发送到队列的数据逐个字节地复制到队列中。
- 引用队列:队列只保存指向发送给队列的数据的指针,而不是数据本身。
**FreeRTOS使用复制队列的方法,**通过复制队列比引用队列更强大和更简单,因为:
- 栈变量可以直接发送到队列中,即使在声明它的函数退出后该变量不存在。
- 可以直接将数据发送到队列,而不需要首先分配一个缓冲区来保存数据,然后将数据复制到已分配的缓冲区中。
- 发送任务可以立即重用发送到队列的变量或缓冲区。
- 发送任务和接收任务是完全分离的——程序设计人员不用关心哪个任务拥有数据,或者哪个任务发布数据。
- 通过复制队列并不阻止队列被引用。例如,当队列中的数据的大小使得将数据复制到队列中不现实时,可以将指向数据的指针复制到队列中。
- RTOS完全负责分配用于存储数据的内存。
- 在受内存保护的系统中,任务可以访问的RAM将受到限制。在这种情况下,只有当发送和接收任务都可以访问存储数据的RAM时,才可以使用引用队列。复制队列不存在这种限制:内核总是以完全特权运行,允许使用队列跨内存保护边界传递数据。
多任务访问
队列本身就是对象,任何知道它们存在的任务或ISR都可以访问它们。任意数量的任务都可以写同一个队列,也可以从同一个队列读任意数量的任务。
队列读取阻塞
当任务试图从队列中读取时,它可以选择指定一个“阻塞”时间。如果队列已经为空,则该任务保持在Blocked状态,以等待从队列中获得数据。
处于阻塞状态的任务正在等待队列中的数据可用,当另一个任务或中断将数据放入队列中时,该任务会自动移动到Ready状态。如果指定的块时间在数据可用之前过期,任务也会自动从Blocked状态移动到Ready状态。
队列可以有多个读取者,因此单个队列上可能阻塞多个任务等待数据。在这种情况下,当数据可用时,只有一个任务被解除阻塞。未阻塞的任务将始终是等待数据的最高优先级任务。如果阻塞的任务具有相同的优先级,那么等待数据时间最长的任务被解除阻塞。
队列写阻塞
与从队列读取时一样,任务也可以在写入队列时指定阻塞时间。在这种情况下,阻塞时间是在队列已经满的情况下,任务保持在Blocked状态来等待队列上有可用空间的最长时间。
队列可以有多个写入者,因此一个完整的队列上可能阻塞了多个任务,等待完成发送操作。
在这种情况下,当队列上的空间变为可用时,只有一个任务将被解除阻塞。未阻塞的任务将始终是等待空间的最高优先级任务。如果阻塞的任务具有相同的优先级,那么等待空间最长的任务将被解除阻塞。
多队列阻塞
可以将队列分组到集合中,允许任务进入Blocked状态以等待数据在集合中的任何队列上变为可用。
使用队列
**xQueueCreate()**API 函数:在使用队列之前,必须显示地创建队列。创建一个队列并返回引用它所创建的队列的QueueHandle_t。
队列由句柄引用,句柄是QueueHandle_t类型的变量。
当创建队列时,FreeRTOS从FreeRTOS堆中分配RAM。RAM用于保存队列数据结构和队列中包含的项。如果要创建的队列没有足够的堆RAM可用,xQueueCreate()返回NULL。
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength,UBaseType_t uxItemSize);
- uxQueueLength :正在创建的队列在任何时候可以容纳的最大项数。
- uxItemSize:每个数据项大小。
- Return Value:如果返回NULL,则不能创建队列,因为FreeRTOS没有足够的堆内存来分配队列数据结构和存储区域。返回的非NULL值作为已经创建队列的句柄。
在创建队列之后,可以使用xQueueReset()API函数将队列返回到原始的空状态。
xQueueSendToBack()和xQueueSendToFront()
- xQueueSendToBack():将数据发送到队列的尾部。等价于xQueueSend()。
- xQueueSendToFront():将数据发送到队列的头部。
注意:永远不要从中断服务程序中调用xQueueSendToFront()或xQueueSendToBack()。在中断服务程序中应该使用xQueueSendToFrontFromISR()和xQueueSendToBackFromISR()
BaseType_t xQueueSendToFront(QueueHandle_t xQueue,const void * pvItemToQueue,TickType_t xTicksToWait);
BaseType_t xQueueSendToBack(QueueHandle_t xQueue,const void * pvItemToQueue,TickType_t xTicksToWait);
- xQueue:数据被写入的队列的句柄。队列句柄从从用于创建队列的xQueueCreate()调用中返回。
- pvItemToQueue:指向要复制到队列中的数据的指针。队列可以保存的每个项的大小是在创建队列时设置的,因此这些字节将从pvItemToQueue复制到队列存储区域。
- xTicksToWait:如果队列已满,任务保持在Blocked状态等待队列上有可用空间的最长时间。
如果xTicksToWait为零且队列已满,xQueueSendToFront()和xQueueSendToBack()将立即返回。
阻塞时间以Tick周期指定,因此它表示绝对时间依赖于Tick频率。宏pdMS_TO_TICKS()可用于将毫秒转换为指定的ticks。
如果在FreeRTOSConfig.h中将INCLUDE_vTackSuspend设置为1,那么将xTicksToWait设置为portMAX_DELAY将导致任务无限期等待。 - Returned value:1.pdPASS:当数据成功发送到队列时,才会返回pdPASS。如果指定了块时间(xTicksToWait不为零),那么在函数返回之前,调用任务可能被置于Blocked状态以等待队列中有可用的空间,但在块时间过期之前,数据被成功写入队列。2.errQUEUE_FULL:队列已满,无法将数据写入队列。。如果指定了块时间(xTicksToWait不为零),那么调用任务将被置于Blocked状态,等待另一个任务或中断在队列中腾出空间,但指定的块时间在此发生之前过期了。
**xQueueReceive()**API函数:从队列中读取数据项,然后从队列中删除此项目。
注意:永远不要从中断服务例程调用xQueueReceive()。要使用xQueueReceiveFromISR()
BaseType_t xQueueReceive(QueueHandle_t xQueue,void * const pvBuffer, TickType_t xTicksToWait);
- xQueue:读取数据的队列句柄。队列句柄有xQueueCreate()返回。
- pvBuffer:一个指针,指向接收到的数据被复制到其中的内存。队列所包含的每个数据项的大小必须在创建队列时设置。pvBuffer所指向的内存必须足够大,能够容纳这么多字节。
- xTicksToWait:如果队列已经为空,任务应该保持Blocked状态,等待队列上的数据变为可用的最长时间。如果xTicksToWait为零,且队列已经为空,则函数立即返回。
阻塞时间以Tick周期指定,因此它表示的绝对时间依赖于tick频率,宏pdMS_TO_TICKS()将毫秒转换为yicks。
如果在FreeRTOSConfig.h中将INCLUDE_vTaskSuspend设置为1,再将xTicksToWait设置为portMAX_DELAY,将导致任务无限期等待。 - Returned value :1.pdPASS:只有当数据成功从队列中读取时,才会返回pdPASS。若指定了块时间(xTicksToWait不为零),那么调用任务可能被置于Blocked状态,等待队列上数据可用,但在块时间过期之前成功地读取到了数据。2.errQUEUE_EMPTY:如果由于队列已经为空,无法从队列中读取数据,则返回errQUEUE_EMPTY。如果指定了阻塞时间(xTicksToWait不为零),那么调用任务将被置于阻塞状态,以等待另一个任务或中断向队列发送数据,但阻塞时间在此之前已经过期。
**uxQueueMessagesWaiting()**API函数:查询当前在队列中的项的数量。
永远不要从中断服务例程调用uxQueueMessagesWaiting()。应该使用uxQueueMessagesWaitingFromISR()。
UBaseType_t uxQueueMessagesWaiting(QueueHandle_t xQueue);
- xQueue:正在查询队列的句柄。
- Returned value:正在查询的队列当前持有的项数。如果返回零,则队列为空。
例10.当从队列接收数据时阻塞
- 这个例子演示了创建一个队列,多个任务向队列发数据,以及从队列接收数据。
- 创建该队列保存类型为int32_t的数据项。
- 发送数据到队列的任务不指定阻塞时间,而从队列接收数据指定阻塞时间。
- 发送数据到队列的任务的优先级低于从队列读取数据的任务的优先级。
- 这意味着队列不应该包含超过一个项,因为一旦数据被发送到队列,接收任务将解除阻塞,抢占发送任务,并删除数据——队列再次为空。
- 创建两个写入队列的任务,一个持续将值100写入队列,另一个持续将值200写入同一队列。task参数用于将这些值传递给每个任务实例。
static void vSendrTask(void *pvParameters)
{
int32_t lValueToSend;
BaseType_t xSatus;
/*创建了该任务的两个实例,因此发送到队列的值通过task参数传入—这样,每个实例可以使用不同的值。创建队列是为了保存类型为int32_t的值,因此将参数强制转换为所需的类型。*/
lValueToSend = (int32_t)pvParameters;
/*和大多数任务一样,这个任务是在一个无限循环中实现的。*/
for(;;)
{
/*将值发送到队列。
第一个参数是数据被发送到的队列。队列是在启动调度器之前创建的,因此是在此任务开始执行之前。
第二个参数是要发送的数据的地址,在本例中是lValueToSend的地址。
第三个参数是阻塞时间——在队列已经满的情况下,任务应该保持阻塞状态以等待队列上有可用空间的时间。在这种情况下,没有指定块时间,因为队列永远不应该包含一个以上的项,因此永远不会满。*/
xStatus = xQueueSendToBack(xQueue,&lValueToSend,0);
if(xStatus != pdPASS)
{
/*发送操作无法完成,因为队列已满-这一定是一个错误,因为队列不应该包含超过一个项!* /
vPrintString( "Could not send to the queue.\r\n" );
}
}
}
接收任务指定了100毫秒的阻塞时间,因此将进入阻塞状态等待数据可用。当队列上有数据可用或者超过100毫秒时,它会离开Blocked状态。100毫秒的超时应该永远不会过期,因为有两个任务不断地向队列写入。
static void vReceiverTask(void *pvParameters)
{
int32_t lReceivedValue;
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS(100);
for(;;)
{
/*该调用应该总是发现队列为空,因为该任务将立即删除写入队列的任何数据。*/
if(uxQueueMessagesWaiting(xQueue) != 0)
{
vPrintString( "Queue should have been empty!\r\n" );
}
/*从队列接收数据。
第一个参数是接收数据的队列。队列是在调度程序启动之前创建的,因此是在此任务第一次运行之前。
第二个参数是缓冲区,接收到的数据将放在其中。在这种情况下,缓冲区只是一个变量的地址,该变量具有容纳接收到的数据所需的大小。最后一个参数是块时间——如果队列已经为空,任务将保持在Blocked状态以等待数据可用的最长时间。*/
xStatus = xQueueReceive(xQueue,&lReceivedValue,xTicksToWait);
if(xStatus == pdPASS)
{
/*从队列中成功接收数据,打印接收的值。*/
vPrintStringAndNumber( "Received = ", lReceivedValue );
}
else{
/*队列等待100ms后仍未收到数据。
这一定是一个错误,因为发送任务是自由运行的,将不断写入队列。*/
vPrintString( "Could not receive from the queue.\r\n" );
}
}
}
在启动调度器之前创建队列和三个任务,创建该队列以保存最多5个int32_t值。
/*声明一个QueueHandle_t类型的变量,用于存储三个任务都要访问的队列的句柄。*/
int main()
{
/*创建队列以保存最多5个值,每个值都足够容纳一个类型为int32_t的变量*/
xQueue = xQueueCreate(5,sizeof(int32_t));
if (xQueue != NULL)
{
/*创建两个将发送到队列的任务实例。task参数用于传递任务将写入队列的值,因此一个任务将持续向队列写入100,而另一个任务将持续向队列写入200。两个任务都在优先级1上创建。*/
xTaskCreate(vSenderTask,"Sender 1",1000, (void *)100, 1,NULL);
xTaskCreate(vSenderTask,"Sender 2",1000, (void *)200, 1,NULL);
/*创建从队列中读取的任务。该任务的优先级为2,因此高于发送方任务的优先级。*/
xTaskCreate(vReceiverTask,"Receiver",1000,NULL,2,NULL);
vTaskStartScheduler();
}
else
{
/*无法创建队列。*/
}
/*如果一切正常,main()将永远不会到达这里,因为调度器现在正在运行任务。如果main()到达这里,那么很可能没有足够的FreeRTOS堆内存用于创建空闲任务。第2章提供了关于堆内存管理的更多信息。*/
for(;;);
}
发送到队列的两个任务具有相同的优先级。这将导致两个发送任务依次向队列发送数据。示例10产生的输出如图32所示。
- Receiver任务首先运行,因为它具有最高优先级。它尝试从队列中读取。队列为空,因此接收器进入阻塞状态,等待数据可用。Sender 2在Receiver阻塞后运行。
- Sender 2写入队列,导致Receiver退出阻塞状态,因为Receiver拥有最高优先级,因此优先于Sender 2。
- Receiver清空队列,再次进入阻塞状态。这一次,Sender 1在阻塞后运行。
- Sender 1写入队列,导致Receiver退出Blocked状态并抢占Sender 1—因此它继续…
从多个来源接收数据
在FreeRTOS设计中,任务从多个源接收数据是很常见的。接收任务需要知道数据来自哪里,来确定该如何处理数据。
一个简单的设计方案是使用队列来传输结构,结构的字段中包含数据的值和数据的源。
- 创建一个包含Data_t类型结构的队列。结构成员允许数据值和枚举类型,来指示在一条消息中发送到队列的数据意味着什么。
typedef struct{
ID_t eDataID;
int32_t lDataValue;
}Data_t;
- 中央控制器任务用于执行主要的系统功能。它必须对队列上与之通信的系统状态的输入和更改做出反应。
- CAN总线任务用于封装CAN总线接口功能。当CAN总线任务接收并解码一条消息时,它将把已解码的消息以Data_t结构发送给Controller任务。传输结构的eDataID成员用于让Controller任务知道数据是什么——在所描述的情况下,它是一个电机转速值。被传输结构的IdaraValue成员让Controller任务知道实际的电机转速值。
- Human Machine Interface(HMI)任务用于封装所有HMI功能。机器操作员可能可以以121种方式输入命令和查询值,这些都必须在HMI任务中检测和解释。当一个新命令被输入时,HMI任务以Data_t结构将该命令发送给Controller任务。传输结构的eDataID成员用于让Controller知道数据是什么。IDataValue成员用于让Controller任务知道实际的设定值。
例11.发送消息到队列时阻塞,并在队列上发送有结构的消息
- 接收任务的优先级低于发送任务。
- 队列用于传递结构而不是整数。
/*用于标识数据来源的枚举类型。*/
typedef enum
{
eSender1,
eSender2
}DataSource_t;
/*定义将传递给队列的结构类型。*/
typedef struct{
uint8_t ucValue;
DataSource_t eDataSource;
}Data_t;
/*声明两个Data_t类型的变量,它们将在队列上传递。*/
static const Data_t xStructsToSend[2] =
{
{100,eSender1},
{200,eSender2}
}
- 发送任务具有更高的优先级,所以此时队列通常是满的。因为一旦接收任务从队列中删除一个项,它会被发送任务中的一个抢占,然后重新填充队列。发送任务重新进入Blcoked状态,等待队列上再次有可用的空间。
- 发送任务指定了100毫秒的阻塞时间,因此它进入阻塞状态,以便在每次队列满时等待可用空间。
- 当队列上有可用空间,或者过了100毫秒没有可用空间时,它会离开Blocked状态。在本例中,100毫秒的超时永远不会过期,因为接收任务不断地通过队列中删除项来创造空间。
static void vSenderTask(void *pvParameters)
{
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS(100);
for(;;)
{
/*发送消息到队列
第二个参数是被发送的结构的地址,地址作为任务参数传入,因此直接使用pvParameters。第三个参数是阻塞时间——如果队列已经满了,任务应该保持阻塞状态以等待队列上有可用空间的时间。之所以指定阻塞时间,是因为发送任务的优先级高于接收任务,因此队列预计将被填满。当两个发送任务都处于Blocked状态时,接收任务将从队列中删除项。*/
xStatus = xQueueSendToBack(xQueue,pvParameters,xTicksToWait);
if(xStatus != PASS)
{
/*发送操作无法完成,即使在等待100ms后。
这一定是一个错误,因为只要两个发送任务都处于Blocked状态,接收任务就应该在队列中腾出空间。*/
vPrintString( "Could not send to the queue.\r\n" );
}
}
}
- 接收任务的优先级最低,因此只有当两个发送任务都处于Blocked状态时,它才会运行。发送任务只有在队列满时才会进入Blocked状态,因此接收任务也只能在队列满时执行。
static void vReceiverTask(void *pvParameters)
{
/*声明保存从队列接收到的值的结构*/
Data_t xReceivedStructure;
BaseType_t xstatus;
for(;;)
{
if(uxQueueMessagesWaiting(xQueue) != 3)
{
vPrintString( "Queue should have been full!\r\n" );
}
/*第二个参数是缓冲区地址*/
xStatus = xQueueReceive(xQueue,&xReceivedStructure,0);
if(xStatus == pdPASS)
{
/*从队列中成功接收数据,打印出接收的值和值的来源。*/
if(xReceivedStructure.eDataScource == eSender1){
vPrintStringAndNumber( "From Sender 1 = ", xReceivedStructure.ucValue );
}
else{
vPrintStringAndNumber( "From Sender 2 = ", xReceivedStructure.ucValue );
}
}
else{
/*没有从队列中收到任何东西。这一定是一个错误,因为该任务应该只在队列已满时运行。*/
vPrintString( "Could not receive from the queue.\r\n" );
}
}
}
int main(void)
{
xQueue = xQueueCreate(3,sizeof(Data_t));
if(xQueue != NULL)
{
xTaskCreate(vSenderTask,"sender1",1000,&(xStrcutsToSend[0]),2,NULL);
xTaskCreate(vSenderTask,"sender2",1000,&(xStrcutsToSend[1]),2,NULL);
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );
vTaskStartScheduler();
}
else{
}
for(;;);
}
处理大型或可变大小的数据
如果存储在队列中的数据的大小很大,那么最好使用队列来传输指向数据的指针,而不是一个字节一个字节地将数据复制到队列中。传输指针在处理时间和创建队列所需的RAM量方面都更有效。
当使用指针队列时,必须特别小心,以确保:
- 被指向的RAM的所有者是明确定义的。
当通过指针在任务之间共享内存时,必须确保两个任务不会同时修改内存内容,或采取任何可能导致内存内容无效或不一致的其它操作。 - 被指向的RAM仍然有效。
如果所指向的内存是动态分配的,或者是从预分配的缓冲池中获得的,那么应该只有一个任务负责释放内存。在释放内存之后,任何任务都不应该尝试访问它。
如何使用队列将指向缓冲区的指针从一个任务发送到另一个任务。