FreeRTOS学习笔记七【队列-上】
目的
- 如何创建队列。
- 队列如何管理它的数据。
- 如何将数据发送到队列。
- 如何从队列接收数据。
- 什么是阻塞队列。
- 如何让多个队列阻止。
- 如何覆盖队列中的数据。
- 如何清空队列。
- 入队列和出队时对任务优先级的影响。
队列的特征
数据存储
队列是一种先进先出(FIFO)的缓冲区,数据从队尾入队,从队头出队。通常有两种方法实现队列:
- 数据采用副本的方式
采用副本方式的队列在入队时将数据逐字节复制到队列中。 - 数据采用引用的方式
采用引用方式的队列在入队时只保存数据的指针,而不是数据本身。
在FreeRTOS中使用的是副本方式的队列,这个方式比引用方式更加强大与简单。理由如下:
- 自动变量(栈变量)可以直接入队,即使该变量已经不存在了也依然可是使用它的值。
- 可以直接将数据入队,而不需要用户预先分配存储区域保存数据。
- 数据入队后,它的存储区域可以立即使用,或者可以立即修改它的值。
- 入队任务和出队任务完全解耦合,应用程序无需关心哪些任务拥有数据或何时需要释放数据的内存。
- 副本方式的队列依然可以通过引用方式使用,如,当数据太大时,也可以将指向数据的指针入队。
- 使用副本方式的队列时,完全由FreeRTOS负责管理(分配、释放)存储数据的内存。
- 在内存受保护的系统中,任务可以访问的RAM受限,这时如果使用引用方式的队列,必须将数据存放在两个任务都可以访问的RAM中才可以实现队列,而副本方式的队列没有这个限制。
多个任务访问
队列本身也是对象,任何任务都可以通过队列的句柄访问它。
出队时阻塞任务
当任务从队列中读取数据时,而队列里没有数据,此时该任务将会阻塞,直到由队列中有数据(其他任务将数据写入队列)时该任务才会进入就绪态。一个队列可以有多个任务从中获取数据,因此它可以阻塞多个任务,这种情况下每次只有一个任务在数据到达时进入就绪态,被阻塞的任务中始终是优先级高的任务先进入就绪态,如果所有任务优先级相同,那么阻塞时间长的任务先进入就绪态。
入队时阻塞任务
如果队列已满,此时入队,任务将会阻塞,同样的一个队列也可以阻塞多个任务,当有空间可以使数据入队时,只有一个任务可以进入就绪态,被阻塞的任务中始终是优先级高的任务先进入就绪态,如果所有任务优先级相同,那么阻塞时间长的任务先进入就绪态。
多个队列阻塞任务
队列可以分组,同样的任务需要等待数据在队列集合上的任意队列可用时才会进入就绪态。
队列的使用
xQueueCreate()
使用队列前必须先创建它,QueueHandle_t是队列句柄的变量类型,调用xQueueCreate()可以创建一个队列,创建成功时返回指向队列的QueueHandle_t指针,如果没有足够队空间,则创建失败返回NULL。下面是它的原型:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数:
- uxQueueLength
队列可以保存数据的最大项数。 - uxItemSize
每项数据的字节数。
返回值:
- 非NULL,表示已成功创建队列。 返回的值为创建的队列的句柄。
- NULL,则队列创建失败,因为没有足够的堆内存来分配队列数据结构和存储区域。
后面使用过程中可以用xQueueReset()函数将队列清空到初始状态。
xQueueSendToBack() 、 xQueueSendToFront()
xQueueSendToBack()用于将数据发送到队尾部,xQueueSendToFront()用于将数据发送到队头xQueueSen()与xQueueSendToBack()等效,效果完全相同。
注意:不要在中断服务函数中调用xQueueSendToFront()或xQueueSendToBack()。 中断安全版本应该使用xQueueSendToFrontFromISR()和QueueSendToBackFromISR(),这两个函数在后面介绍。函数原型:
BaseType_t xQueueSendToFront( QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait );
BaseType_t xQueueSendToFront( QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait );
参数:
- xQueue
队列的句柄,xQueueCreate()的返回值。 - pvItemToQueue
指向要入队数据的指针,在创建队列时设置了每项的字节数,因此这里最多从pvItemToQueue中复制前面设置的字节数。 - xTicksToWait
如果队列已满,任务保持在阻塞态等待队列空间的最长时间。如果xTicksToWait为零并且队列已满,则xQueueSendToFront()和xQueueSendToBack()将立即返回。指定的时间可以通过pdMS_TO_TICKS()转换。如果将FreeRTOSConfig.h中INCLUDE_vTaskSuspend设置为1,将xTicksToWait设置为portMAX_DELAY,任务将无限期地等待(没有超时),直到有空间。
返回值:
- pdPASS
仅当数据成功入队,才会返回pdPASS。如果xTicksToWait不为零,则调用任务可能处于阻塞态,等待队列的空间可用,将数据入队,然后返回函数。 - errQUEUE_FULL
如果队列已满,无法将数据入队,xTicksToWait为0,将直接返回errQUEUE_FULL。
如果xTicksToWait不为零,则调用任务将被置于阻塞状态以等待队列中空间可用,该时间到达后也会返回errQUEUE_FULL。
xQueueReceive()
xQueueReceive()用于从队列读取数据。 被读取后的数据项将从队列中删除。
注意:切勿从中断服务程序调用xQueueReceive()。中断安全版本应该使用xQueueReceiveFromISR()。
xQueueReceive()的原型如下:
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait );
参数:
- xQueue
队列句柄。 - pvBuffer
指向存放从队列中出队数据的内存,该内存必须能够容纳创建队列时设置的每项数据的大小。 - xTicksToWait
这个参数和xQueueSendToBack() 、 xQueueSendToFront()中的参数一样。
返回值: - pdPASS
仅当从队列中获取数据成功时,才会返回pdPASS。如果xTicksToWait不为零,则调用任务可能处于阻塞态,等待队列中的数据,将数据获取到后,然后返回函数。 - errQUEUE_EMPTY
如果队列为空,xTicksToWait为0,将直接返回errQUEUE_EMPTY。
如果xTicksToWait不为零,则调用任务将被置于阻塞状态以等待队列中的数据到达,该时间到达后也会返回errQUEUE_EMPTY。
uxQueueMessagesWaiting()
uxQueueMessagesWaiting()用于查询当前队列中的数据项数。
注意:切勿从中断服务程序调用uxQueueMessagesWaiting()。 中断应该使用uxQueueMessagesWaitingFromISR()。
uxQueueMessagesWaiting()原型如下:
UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue );
参数:
- xQueue
队列的句柄。
返回值:
- 队列中数据的项数。
API的使用示例
示例演示了队列的创建,从多个任务写入向队列中写入数据,并从一个任务中读取数据。示例中,一个任务一直向队列中写入100,一个任务移植将200写入队列,还有一个任务读取队列中的数据。
static void vSenderTask( void *pvParameters )
{
int32_t lValueToSend;
BaseType_t xStatus;
/* 任务的参数就是要入队的内容 */
lValueToSend = ( int32_t ) pvParameters;
for( ;; )
{
/* 数据入队,满队时直接返回不阻塞 */
xStatus = xQueueSendToBack( xQueue, &lValueToSend, 0 );
if( xStatus != pdPASS )
{
/* 满队时输出提示信息 */
vPrintString( "Could not send to the queue.\r\n" );
}
}
}
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" );
}
/* 从队列中获取数据,如果是空队,阻塞等待 */
xStatus = xQueueReceive( xQueue, &lReceivedValue, xTicksToWait );
if( xStatus == pdPASS )
{
/* 输出获取到的数据 */
vPrintStringAndNumber( "Received = ", lReceivedValue );
}
else
{
/* 设定阻塞时间到达时输出提示 */
vPrintString( "Could not receive from the queue.\r\n" );
}
}
}
/* 保存队列的句柄 */
QueueHandle_t xQueue;
int main( void )
{
/* 创建一个队列,它最大可以存放5项数据存放的数据是int32_t */
xQueue = xQueueCreate( 5, sizeof( int32_t ) );
if( xQueue != NULL ) /* 队列创建成功 */
{
/* 创建两个发送数据的任务 */
xTaskCreate( vSenderTask, "Sender1", 1000, ( void * ) 100, 1, NULL );
xTaskCreate( vSenderTask, "Sender2", 1000, ( void * ) 200, 1, NULL );
/* 创建一个接受数据的任务 */
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );
/* 启动调度器 */
vTaskStartScheduler();
}
else
{
/* 队列创建失败 */
}
for( ;; );
}
输出结果:
三个任务的调度顺序:
从多个数据源接收数据
从多个数据源接收数据在Free RTOS中非常常见。接收数据的任务需要知道数据都来自于哪些数据源便于处理各个数据。一个简单的设计方案就是使用队列传输具有数据值和数据源标识的数据结构,所图所示。
下面是具体分析:
- 创建一个包含Data_t类的队列,Data_t结构体包含数据和数据源的ID。
- Controller任务用于执行数据的处理。
- CAN总线任务用于封装CAN总线接口功能。当CAN总线任务有数据时,它将数据通过Data_t包装,发送到队列,eDataID让Controller任务知道这个数据(lDataValue)表示什么(如,电机的转速),而lDataValue就用于存放电机的转速。
- 人机界面(HMI)任务用于封装所有HMI功能。操作员可能以多种方式输入命令和查询值,这些方式必须在HMI任务中检测和解释。此时也需要利用eDataID表示这个数据代表什么,而lDataValue存放具体的数据。