队列介绍(Queue)
使用背景
在实际的应用中,常常会遇到一个任务或者中断服务需要和另外一个任务进行“沟通交流”,这个“沟通交流”的过程其实就是消息传递的过程。在没有操作系统的时候两个应用程序进行消息传递一般使用全局变量的方式,但是如果在使用操作系统的应用中用全局变量来传递消息就会涉及到“资源管理”的问题。FreeRTOS对此提供了一个叫做“队列”的机制来完成任务与任务、任务与中断之间的消息传递。
队列的特性
队列可以容纳有限数量的固定大小的数据项。一个队列可以容纳的最大项目数称为它的“长度”。每个数据项的长度和大小都在创建队列时设置。
队列通常用作先进先出(FIFO)缓冲区,其中数据被写入队列的末尾(尾部),并从队列的前端(头部)删除。
队列传输数据方法
-
按复制,复制队列是指将发送到队列的数据复制一份到队列中。
-
按引用,引用队列意味着队列中只保存指向发送到队列的数据的指针,而不是数据本身。
FreeRTOS使用复制队列的方法。通过复制排队比通过引用排队更强大,使用起来更简单,因为:
- 栈中的变量可以直接发送到队列,即使栈变量会在声明它的函数退出后将不存在。
- 不用预先为数据分配空间。
- 发送任务发送完数据后可以立即重用这个变量。
- 发送任务和接收任务是完全分离的——应用程序设计人员不需要关心哪个任务“拥有”数据,或者哪个任务负责发布数据。
- 复制队列可以同时使用引用功能。例如,当队列中的数据的大小使得将数据复制到队列中不切实际时,则可以将指向数据的指针复制到队列中。
- RTOS完全负责分配用于存储数据的内存。
- 在内存保护的系统中,任务可以访问的RAM将受到限制。在这种情况下,只有当发送和接收任务都可以访问存储数据的RAM时,才可以使用引用队列。复制队列没有这种限制;内核总是以完全权限运行,允许使用队列跨内存保护边界传递数据。
读取队列时阻塞
当一个任务试图从队列中读取数据时,它可以指定一个“阻塞”时间。
队列为空的时候无法读取,这个时候就会阻塞,当另一个任务或中断将数据放入队列中时,处于阻塞态的任务将自动移动到就绪态。如果指定的阻塞时间到了,还是没有数据,任务也将自动移到就绪态。
可能存在多个任务读取队列,因此单个队列上可能阻塞了多个等待数据的任务。在这种情况下,当有数据时,只有一个任务将被解除阻塞。被解除阻塞的任务将始终是等待数据的任务中优先级最高的那个任务。如果阻塞的任务具有相同的优先级,则等待数据时间最长的任务将被解除阻塞。
写入队列时阻塞
就像从队列中读取一样,任务在写入队列时可以指定阻塞时间。在这种情况下,发生阻塞是因为队列是满的,没有可以写入的空间。
可能有多个任务试图写入队列,因此一个队列上可能阻塞了多个等待完成发送(写入)操作的任务。在这种情况下,当队列有空间时,只有一个任务将被解除阻塞。被解除阻塞的任务将始终是等待空间的任务中最高优先级的任务。如果阻塞的任务具有相同的优先级,则等待空间时间最长的任务将被解除阻塞
队列使用 (Usage)
创建
队列的创建有两种方法:动态分配内存、静态分配内存
动态分配内存: xQueueCreate,队列的内存在函数内部动态分配
静态分配内存: xQueueCreateStatic,队列的内存要事先分配好
复位
删除
写队列
可以把数据写到队列头部,也可以写到尾部,xQueueSendToBack()用于将数据发送到队列的后面(尾部),xQueueSendToFront()用于将数据发送到队列的前面(头部)。还有个函数xQueueSend()等价于xQueueSendToBack(),他们完全一样。
注意:不要在中断服务程序中调用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 );
读队列
xQueueReceive()用于从队列中接收(读取)一个项。队列里的项(数据)被任务给接收走后,队列就会把这个项(数据)删除。
注意:不要在中断服务程序中调用xQueueReceive()。使用中断安全的xQueueReceiveFromISR() 函数。
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
查询队列
可以查询队列中有多少个数据、有多少空余空间。
/*
* 返回队列中可用数据的个数
*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
/*
* 返回队列中可用空间的个数
*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );
偷窥队列
/* 偷看队列
* 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,
);
指针数据传输
如果存储在队列中的数据很大,那么最好使用队列来传输指向数据的指针,而不是逐个字节地将数据本身复制到队列中或复制出队列。传输指针在处理时间和创建队列所需的RAM量方面更加高效。但是在对指针进行排队时,必须非常小心,以确保:
- 指针指向的RAM空间的所有者必需清晰
当通过指针在两个任务之间共享内存时,必须确保两者不会同时修改内存内容,也不会采取可能导致内存内容无效或不一致的任何其他操作。理想情况下,在指针进入队列前,只有发送任务被允许访问指针指向的内存。在指针从队列出去后,只有接收任务可以访问指针指向的内存
- 指针所指向的RAM要保持有效。
如果所指向的内存是动态分配的,或者是从内存池中获得的的预分配缓冲区,则应该有一个任务负责释放内存。在释放内存之后,任何任务都不应该尝试访问内存。
指针绝不能用来访问已分配到任务栈上的数据。在栈帧改变后,数据将无效。
QueueHandle_t xPointerQueue;
xPointerQueue = xQueueCreate( 5, sizeof( char * ) );
void vStringSendingTask( void *pvParameters )
{
char *pcStringToSend;
const size_t xMaxStringLength = 50;
BaseType_t xStringNumber = 0;
for( ;; ) {
pcStringToSend = ( char * ) prvGetBuffer( xMaxStringLength );
snprintf( pcStringToSend, xMaxStringLength, "String number %d\r\n", xStringNumber );
xStringNumber++;
xQueueSend( xPointerQueue,&pcStringToSend, portMAX_DELAY );
}
}
void vStringReceivingTask( void *pvParameters )
{
char *pcReceivedString;
for( ;; ) {
xQueueReceive( xPointerQueue, &pcReceivedString,portMAX_DELAY );
vPrintString( pcReceivedString );
prvReleaseBuffer( pcReceivedString );
}
}
- 创建了一个最多可以容纳5个指针的队列。
- 在发送任务vStringSendingTask中分配一个缓冲区,向缓冲区写入一个字符串,然后向队列发送一个指向缓冲区的指针。
- 在接收任务vStringReceivingTask中从队列接收一个指向缓冲区的指针,然后打印缓冲区中包含的字符串,打印完就释放缓冲区。