一、队列概述
1.什么是队列
队列又称消息队列,是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任 务间传递信息。
为什么不使用全局变量?
如果使用全局变量,兔子(任务1)修改了变量 a ,等待树獭(任务3)处理,但树獭处理速度很 慢,在处理数据的过程中,狐狸(任务2)有可能又修改了变量 a ,导致树獭有可能得到的不是正确的数据。
在这种情况下,就可以使用队列。兔子和狐狸产生的数据放在流水线上,树獭可以慢慢一个个依次处理。
关于队列的几个名词:
队列项目:队列中的每一个数据;
队列长度:队列能够存储队列项目的最大数量;
创建队列时,需要指定队列长度及队列项目大小。
2.队列的操作
队列的简化操如入下图所示,从此图可知:
·队列可以包含若干个数据:队列中有若干项,这被称为"长度"(length)
·每个数据大小固定
·创建队列时就要指定长度、数据大小
·数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部 读
·也可以强制写队列头部:覆盖头部数据
更详细的操作入下图所示:
3.队列的特点
数据传递方式:
采用实际值传递,即将数据拷贝到队列中进行传递,也可以传递指针,在传递较大的数据的时候采用指针传递。
多任务访问:
队列不属于某个任务,任何任务和中断都可以向队列发送/读取消息。
出队、入队阻塞:
当任务向一个队列发送消息时,可以指定一个阻塞时间。
假设此时队列已满无法入队,阻塞时间如果设置为:
·0:直接返回不会等待;
·0~port_MAX_DELAY:等待设定的阻塞时间,若在该时间内还无法入队,超时后直接返回 不再等待;
·port_MAX_DELAY:死等,一直等到可以入队为止。出队阻塞与入队阻塞类似;
二、队列函数
1.创建
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,
UBaseType_t uxItemSize );
2.写队列
写队列总共有以下几个函数:
一般使用xQueueSend函数,原型如下:
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait
);
3.读队列
读队列总共有以下几个函数:
一般也只用第一个,原型如下:
BaseType_t xQueueReceive(
QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait
);
三、队列的基本使用代码示例
光看函数是没用的,唯有实操才知道怎么用,由于我之前学了Linux进程间通信的相关API,对于freeRTOS同步互斥的操作还是比较好上手和理解的,这里的句柄相当于Linux的各种标识符,通过句柄来标识不同的对象。
本程序会创建一个队列,然后创建2个发送任务、1个接收任务:
·发送任务优先级为1,分别往队列中写入100、200
·接收任务优先级为2,读队列、打印数值
main函数中创建的队列、创建了发送任务、接收任务,代码如下:
/* 队列句柄, 创建队列时会设置这个变量 */
QueueHandle_t xQueue;
int main( void )
{
prvSetupHardware();
/* 创建队列: 长度为5,数据大小为4字节(存放一个整数) */
xQueue = xQueueCreate( 5, sizeof( int32_t ) );
if( xQueue != NULL )
{
/* 创建2个任务用于写队列, 传入的参数分别是100、200
* 任务函数会连续执行,向队列发送数值100、200
* 优先级为1
*/
xTaskCreate( vSenderTask, "Sender1", 1000, ( void * ) 100, 1, NULL );
xTaskCreate( vSenderTask, "Sender2", 1000, ( void * ) 200, 1, NULL );
/* 创建1个任务用于读队列
* 优先级为2, 高于上面的两个任务
* 这意味着队列一有数据就会被读走
*/
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );
/* 启动调度器 */
vTaskStartScheduler();
}
else
{
/* 无法创建队列 */
}
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
发送任务的函数中,不断往队列中写入数值,代码如下:
static void vSenderTask( void *pvParameters )
{
int32_t lValueToSend;
BaseType_t xStatus;
/* 我们会使用这个函数创建2个任务
* 这些任务的pvParameters不一样
*/
lValueToSend = ( int32_t ) pvParameters;
/* 无限循环 */
for( ;; )
{
/* 写队列
* xQueue: 写哪个队列
* &lValueToSend: 写什么数据? 传入数据的地址, 会从这个地址把数据复制进队列
* 0: 不阻塞, 如果队列满的话, 写入失败, 立刻返回
*/
xStatus = xQueueSendToBack( xQueue, &lValueToSend, 0 );
if( xStatus != pdPASS )
{
printf( "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( 100UL );
/* 无限循环 */
for( ;; )
{
/* 读队列
* xQueue: 读哪个队列
* &lReceivedValue: 读到的数据复制到这个地址
* xTicksToWait: 如果队列为空, 阻塞一会
*/
xStatus = xQueueReceive( xQueue, &lReceivedValue, xTicksToWait );
if( xStatus == pdPASS )
{
/* 读到了数据 */
printf( "Received = %d\r\n", lReceivedValue );
}
else
{
/* 没读到数据 */
printf( "Could not receive from the queue.\r\n" );
}
}
}
补充:
关于队列的写入,由于send函数的第二个参数是将传入指针所指向的数据复制到队列里,我们当然可以传入一个指向结构体的指针,或指向字符串的指针。就可以做到分辨数据源(当我们想知道是哪个任务传入数据时,可以让任务传入结构体指针,其中包含每个任务的ID号,用于分辨数据源);当传输大型数据时,我们只需要传入指向该大型数据的地址即可。
四、邮箱
用于补充说明,用的不多,因为过于简单
FreeRTOS的邮箱概念跟别的RTOS不一样,这里的邮箱称为"橱窗"也许更恰当:
· 它是一个队列,队列长度只有1
· 写邮箱:新数据覆盖旧数据,在任务中使用 xQueueOverwrite() ,在中断中使 用xQueueOverwriteFromISR() 。
既然是覆盖,那么无论邮箱中是否有数据,这些函数总能成功写入数据。
· 读邮箱:读数据时,数据不会被移除;在任务中使用 xQueuePeek() ,在中断中使用
xQueuePeekFromISR() 。
这意味着,第一次调用时会因为无数据而阻塞,一旦曾经写入数据,以后读邮箱时总能成 功。
五、队列集
队列集一句话概括就是一个队列中,每个项目都是其他队列的句柄。这个队列叫队列集,它可以包含其他队列,在FreeRTOS中,队列集的作用是将多个队列和计数信号量聚合在一起,允许任务以一种更简洁的方式管理这些同步和通信机制。它使得任务能够一次性接收来自多个队列或信号量的消息,提高了系统的灵活性和效率。(想从多个队列得到数据,使用队列集)
1.创建函数
//创建成功返回句柄
QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength );
传入的参数为队列集的长度:
要监测3个队列A、B、C
队列集的长度是:队列A的长度+队列B的长度 +队列C的长度,否则在A、B、C都满的情况下,队列集没有空间存放所有的handle。
2.将要监测的队列的handle放入队列集
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet );
第一个参数是要放入的句柄,第二个参数是队列集的句柄。
3.监测哪个队列有数据
//返回有数据的队列的handle
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,
TickType_t const xTicksToWait );
第一个参数传入要监测的队列集,第二个参数传入超时时间。
4.队列集代码示例
main函数:
static QueueHandle_t xQueueHandle1;
static QueueHandle_t xQueueHandle2;
static QueueSetHandle_t xQueueSet;
//句柄
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
/* 1. 创建2个queue */
xQueueHandle1 = xQueueCreate(2, sizeof(int));
if (xQueueHandle1 == NULL)
{
printf("can not create queue\r\n");
}
xQueueHandle2 = xQueueCreate(2, sizeof(int));
if (xQueueHandle2 == NULL)
{
printf("can not create queue\r\n");
}
/* 2. 创建queue set */
xQueueSet = xQueueCreateSet(4);
/* 3. 把2个queue添加进queue set */
xQueueAddToSet(xQueueHandle1, xQueueSet);
xQueueAddToSet(xQueueHandle2, xQueueSet);
/* 4. 创建3个任务 */
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
xTaskCreate(Task3Function, "Task3", 100, NULL, 1, NULL);
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
任务1和任务2往自己的队列里写数据,任务3监测队列集,有数据就读出来:
void Task1Function(void * param)
{
int i = 0;
while (1)
{
xQueueSend(xQueueHandle1, &i, portMAX_DELAY);
i++;
vTaskDelay(10);
}
}
void Task2Function(void * param)
{
int i = -1;
while (1)
{
xQueueSend(xQueueHandle2, &i, portMAX_DELAY);
i--;
vTaskDelay(20);
}
}
void Task3Function(void * param)
{
QueueSetMemberHandle_t handle;
int i;
while (1)
{
/* 1. read queue set: which queue has data */
handle = xQueueSelectFromSet(xQueueSet, portMAX_DELAY);
/* 2. read queue */
xQueueReceive(handle, &i, 0);
/* 3. print */
printf("get data : %d\r\n", i);
}
}
注意:要使用队列集相关函数,我们要自己配置一个宏
需要在FreeRTOSConfig.h自己加上标蓝的那一个宏。
每当有任务往被监测的队列里写数据时,会同时将该队列的handle写入建立了连接的队列集;xQueueSelectFromSet函数会阻塞,直到监测的队列有数据或者自己设置的超时时间到了(我们这里设置的是永不超时),监测到有数据的队列会返回它的handle,我们再根据拿到的handle来进行后续操作,读出数据。
运行结果: