在应用中,常常会遇到一个任务或者中断服务需要和另一个任务进行数据交互,也成为消息传递。在没有操作系统的时候,消息传递一般使用全局变量的方式,但如果在使用操作系统的应用中用全局变量来传递消息,就会涉及到“资源管理”的问题。FreeRTOS对此提供了一个叫做“队列”的机制来完成任务与任务,任务与中断之间的消息传递。
队列是为了任务与任务、任务与中断之间的通信而准备的。可以在任务与任务,任务与中断之间传递消息。队列中可以存储有限的,大小固定的数据项目。任务与任务、任务与中断之间要交流的数据保存在队列中,叫做队列项目。队列所能保存的最大数据项目数量叫做队列的长度,创建队列的时候会指定数据项目的大小和队列的长度。由于队列用来传递消息,所以也成为消息队列。FreeRTOS中的信号量也是一句队列实现的。
队列通常采用先进先出(FIFO)的存储缓存机制,也就是往队列中发送数据(入队)的时候永远都是发送到队列的尾部,而从队列提取数据(出队)的时候,是从队列的头部提取的。也可以选择后进先出(LIFO)的存储缓存。
数据发送到队列中会导致数据拷贝,也就是将要发送的数据拷贝到队列中,这就意味着在队列中存储的是数据的原始值,而不是原数据的引用(即只传递数据的指针),这个也叫作值传递。传递消息指针的方式被称为(引用传递)。引用传递的方式,消息内容必须保持一致性,也就是消息内容必须持续存在。那么局部变量这种可能随时被删掉的东西就不能用来传递消息。采用引用传递会节约时间,毕竟不需要拷贝数据。大数据量传输的时候考虑使用。采用值传递的话有数据拷贝的动作,会浪费一点时间。但是一旦消息入队,那么原始数据的缓冲区就可以删掉或者覆盖,这样的话这些缓冲区就可以被重复的使用。
出队阻塞
当任务尝试从给一个队列中读取消息的时候,可以设置一个阻塞时间,即从队列中读取消息无效的时候任务阻塞的时间。如果阻塞时间设置为0,没有数据的话就马上返回任务继续执行代码。如果阻塞时间1~portMAX_DELAY-1,没有数据的话任务会进入阻塞状态,等待有数据。超时后,则返回任务继续执行代码。如果阻塞时间portMAX_DELAY,没有数据的话任务会进入到阻塞状态,直到有数据。
入队阻塞
入队阻塞与出队阻塞相对应,即设置超时时间。如果阻塞时间设置为0,队列满的话就马上返回任务继续执行代码。如果阻塞时间1~portMAX_DELAY-1,队列满的话任务会进入阻塞状态,等待队列有空闲位置。超时后,则返回任务继续执行代码。如果阻塞时间portMAX_DELAY,队列满的话任务会进入到阻塞状态,直到队列有空闲位置。
队列结构体
typedef struct QueueDefinition /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
int8_t *pcHead; /*< 指向队列存储区开始地址Points to the beginning of the queue storage area. */
int8_t *pcWriteTo; /*< 指向存储区中下一个空闲区域Points to the free next place in the storage area. */
union
{
QueuePointers_t xQueue; /*< Data required exclusively when this structure is used as a queue. */
SemaphoreData_t xSemaphore; /*< Data required exclusively when this structure is used as a semaphore. */
} u;
List_t xTasksWaitingToSend; /*< 等待发送任务列表,那些因为队列满导致入队失败而进入阻塞态的任务就会挂在此列表上List of tasks that are blocked waiting to post onto this queue. Stored in priority order. */
List_t xTasksWaitingToReceive; /*< 等待接收任务列表,那么因为队列空导致出队失败而进入阻塞态的任务就会挂在此列表上List of tasks that are blocked waiting to read from this queue. Stored in priority order. */
volatile UBaseType_t uxMessagesWaiting;/*< 队列中当前队列数量,就是消息数The number of items currently in the queue. */
UBaseType_t uxLength; /*<创建队列时指定的队列长度,也就是队列中最大允许的队列项(消息)数量 The length of the queue defined as the number of items it will hold, not the number of bytes. */
UBaseType_t uxItemSize; /*< 创建爱你队列时指定的每个队列项(消息)最大长度,The size of each items that the queue will hold. */
volatile int8_t cRxLock; /*< 当队列上锁以后用来统计从队列中接收到的队列项数量,也就是出对的队列项数量,当队列没有上锁的话此字段为queueUNLOCKEDStores the number of items received from the queue (removed from the queue) while the queue was locked. Set to queueUNLOCKED when the queue is not locked. */
volatile int8_t cTxLock; /*< 当队列上锁以后用来统计发送到队列中的队列项数量,也就是如对的队列项数量。当队列没有上所的话,此字段为queueUNLOCKED Stores the number of items transmitted to the queue (added to the queue) while the queue was locked. Set to queueUNLOCKED when the queue is not locked. */
#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated; /*< 如果使用静态存储的话,此字段设置为pdTRUESet to pdTRUE if the memory used by the queue was statically allocated to ensure no attempt is made to free the memory. */
#endif
#if ( configUSE_QUEUE_SETS == 1 )
struct QueueDefinition *pxQueueSetContainer; //队列集
#endif
#if ( configUSE_TRACE_FACILITY == 1 ) //跟踪调试
UBaseType_t uxQueueNumber;
uint8_t ucQueueType;
#endif
} xQUEUE;
队列创建
1.xQueueCreate()动态创建队列
队列原型
#define xQueueCreate( uxQueueLength, uxItemSize ) xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
参数:
uxQueueLength:要创建的队列的队列长度,即项目数
uxItemSize:队列中每个项目(消息)的长度
返回值:
NULL:队列创建失败
其他值:队列创建成功后返回的队列句柄
2.xQueueCreateStatic()静态创建队列
队列原型:
#define xQueueCreateStatic( uxQueueLength, uxItemSize, pucQueueStorage, pxQueueBuffer ) xQueueGenericCreateStatic( ( uxQueueLength ), ( uxItemSize ), ( pucQueueStorage ), ( pxQueueBuffer ), ( queueQUEUE_TYPE_BASE ) )
参数:
uxQueueLength:要创建的队列的队列长度,即项目数
uxItemSize:队列中每个项目(消息)的长度
pucQueueStorage:指向队列项目的存储区,也就是消息的存储区。这个存储区需要用户自行分配。此参数必须指向一个uint8_t类型的数组。这个存储区要大于等于uxQueueLength * uxItemsSize字节
pxQueueBuffer:此参数指向一个StaticQueue_t类型的变量,用来保存队列结构体
返回值:
NULL:队列创建失败
其他值:队列创建成功后返回的队列句柄
3.xQueueSend() 入队
xQueueSend()和xQueueSendToBack()相同,都是从后方入队。xQueueSendToFront()是从队列前方入队。以上函数只能用于任务函数中,不能用于中断服务函数,中断服务函数有专用的而函数,已FromISR结尾。
函数原型:
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
参数:
xQueue:队列句柄,指明要向哪个队列发送数据,创建队列成功后,会返回此队列的队列句柄
pvItemToQueue:指向要发送的消息,发送时候会将这个消息拷贝到队列中。
xTicksToWait:阻塞时间,
返回值:
pdPASS:向队列发送消息成功
errQUEUE_FULL:队列已经满了,消息发送失败。
4.xQueueOverwrite() 强行入队
使用次函数进入消息数据入队时,如果队列满了,则强行覆盖掉旧的数据,不管这个旧数据有没有被其他任务或中断取走。此函数只能用于长度为1的队列发送消息。
函数原型:
#define xQueueOverwrite( xQueue, pvItemToQueue ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), 0, queueOVERWRITE )
参数:
xQueue:队列句柄,指明要向哪个队列发送数据,创建队列成功后,会返回此队列的队列句柄
pvItemToQueue:指向要发送的消息,发送时候会将这个消息拷贝到队列中。
返回值:
pdPASS:向队列发消息成功。此函数只会返回成功。
注:查看底层代码,数据入队时,最终调用了memcpy的函数,说明是拷贝了原始数据到队列中,而不是拷贝了原始数据的地址。
5.xQueueSendFromISR() 中断数据入队
xQueueSendFromISR()和xQueueSendToBackFromISR()相同,都是从后方入队。xQueueSendToFrontFromISR()是从队列前方入队。
函数原型:
#define xQueueSendFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )
参数:
xQueue:队列句柄,指明要向哪个队列发送数据,创建队列成功后,会返回此队列的队列句柄
pvItemToQueue:指向要发送的消息,发送时候会将这个消息拷贝到队列中。
pxHigherPriorityTaskWoken:退出中断以后是否进行任务切换。如果此值为pdTRUE,在对出中断服务函数之前一定要进行一次任务切换。
返回值:
pdTRUE:发送消息成功
errQUEUE_FULL:队列已满,发送失败。
注意:该函数没有阻塞时间。因为这些函数都是在中断服务函数中执行,并不在任务中,所以不存在阻塞。
6.xQueueOverwriteFromISR() 中断数据强行入队
函数原型:
#define xQueueOverwriteFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueOVERWRITE )
参数与xQueueSendFromISR函数相同
7.xQueueReceive() 从队列读取数据(读取后从队列中删除数据)
此函数用于在任务中从队列中读取一条消息,读取成功后将消息从队列中删除。
函数原型:
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait )
参数:
xQueue:队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的句柄
pvBuffer:保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中
xTicksToWait:阻塞时间。
返回值:
pdTRUE:从队列中读取数据成功
pdFALSE:从独立俄中读取数据失败
底层源码中查到确实调用了memcpy函数进行了数据拷贝。
8.xQueuePeek()从队列读取数据(读取后不会删除数据)
此函数从队列中读取一条消息,只能用在任务中。读取成功后不会删除消息。
函数原型:
BaseType_t xQueuePeek( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait )
参数:
xQueue:队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的句柄
pvBuffer:保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中
xTicksToWait:阻塞时间。
返回值:
pdTRUE:从队列中读取数据成功
pdFALSE:从独立俄中读取数据失败
9.xQueueReceiveFromISR()在中断中从队列中读取消息
此函数用于在中断服务函数中从队列中读取一条消息,成功后将队列中的这条数据删除。
函数原型:
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue, void * const pvBuffer, BaseType_t * const pxHigherPriorityTaskWoken )
参数:
xQueue:队列句柄,指明要从哪个队列读取数据,创建队列成功后,会返回此队列的队列句柄
pvBuffer:保存读取到的消息,会将这个消息拷贝到缓冲区中。
pxHigherPriorityTaskWoken:退出中断以后是否进行任务切换。如果此值为pdTRUE,在对出中断服务函数之前一定要进行一次任务切换。
返回值:
pdTRUE:读取消息成功
errQUEUE_FULL:队列已满,读取失败。
注意:该函数没有阻塞时间。因为这些函数都是在中断服务函数中执行,并不在任务中,所以不存在阻塞。
代码验证,
创建两个任务,TASK1和TASK2
xTaskCreate(vTestTask1, "freertos_example", configMINIMAL_STACK_SIZE, NULL, TASK_PRIORITY_LOW, NULL);
xTaskCreate(vTestTask2, "freertos_example2", configMINIMAL_STACK_SIZE, NULL, TASK_PRIORITY_LOW, NULL);
任务1中创建队列并且向该队列发送数据,数据发送后,任务挂起500ms。
static void vTestTask1(void *pvParameters)
{
test_queue = xQueueCreate(10,sizeof(queue_item));//创建队列
if(test_queue == NULL)
{
LOG_I(common,"[TASK1]:queue create fail"); //失败
}
else
{
LOG_I(common,"[TASK1]:queue create sucess"); //成功
}
uint8_t print_arr[20] = {0};
static uint32_t cnt = 0;
while (1) {
if(test_queue != NULL) //创建成功
{
cnt++;
sprintf(print_arr,"hello,cnt:%d",cnt);
if(xQueueSend(test_queue, print_arr, 0) == pdTRUE) //入队
{
LOG_I(common,"[TASK1]:queue send sucess,data"); //成功
}
else
{
LOG_I(common,"[TASK1]:queue send fail"); //失败
}
}
vTaskDelay(500);
}
任务2等待接受,接受阻塞时间未100ms。
static void vTestTask2(void *pvParameters)
{
uint8_t print_arr[20] = {0};
while(1)
{
if(xQueueReceive(test_queue,print_arr,100) == pdTRUE) //接收,等待100ms
{
LOG_I(common,"[TASK2]:queue receive success,data:%s",print_arr); //成功
}
else
{
LOG_I(common,"[TASK2]:queue receive fail,timeout"); //失败
}
}
}
结果为:
从打印的LOG可以看到,任务1在发送数据后,任务2马上就读取到了数据。因为任务1数据发送的周期为500ms,而任务2的阻塞时间只有100ms。所以,任务2在每次读取完之后会超时4次。然后再次接收到任务1发送的队列消息。
此时,将队列项的长度改变。
test_queue = xQueueCreate(10,5);//创建队列,队列总长长度10 ,单个项目长度5
发送的数据内容也改变
sprintf(print_arr,"1234567890:",cnt);
其他内容不变。
看打印结果:
结果显示,读取到的数据,只有5个字节,跟队列创建时的长度匹配。所以,在使用的时候,一定要考虑好队列中每个项目的长度。否则容易造成数据丢失。