目录
引言:队列(queue)可以用于"任务到任务"、"任务到中断"、"中断到任务"直接传输信息。
1.队列的特性
直入主题,直接说特点
1)队列可以包含若干个数据:队列中有若干项,这被称为"长度"(length)
2)每个数据大小固定
3)创建队列时就要指定长度,数据大小
4)数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读
流程如下所示:
并且传输数据的两种方法:
拷贝:把数据、把变量的值复制进队列里
引用:把数据、把变量的地址复制进队列里
FreeRTOS使用拷贝值的方法,这更简单:
局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据
无需分配buffer来保存数据,队列中有buffer
局部变量可以马上再次使用
发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据
如果数据实在太大,你还是可以使用队列传输它的地址
队列的空间有FreeRTOS内核分配,无需任务操心
对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个 地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把 数据复制出队列。
2.队列函数
使用队列的流程:创建队列、写队列、读队列、删除队列。
动态创建:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
静态创建:
QueueHandle_t xQueueCreateStatic(
UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorageBuffer,
StaticQueue_t *pxQueueBuffer
);
复位:
/* pxQueue : 复位哪个队列;
* 返回值: pdPASS(必定成功)
*/
BaseType_t xQueueReset( QueueHandle_t pxQueue);
删除:
void vQueueDelete( QueueHandle_t xQueue );
写队列(涉及中断写法函数):
/* 等同于xQueueSendToBack
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
/*
* 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
读队列:
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken
);
查询:
/*
* 返回队列中可用数据的个数
*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
/*
* 返回队列中可用空间的个数
*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );
覆盖/偷看:
/* 覆盖队列
* xQueue: 写哪个队列
* pvItemToQueue: 数据地址
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueueOverwrite(
QueueHandle_t xQueue,
const void * pvItemToQueue
);
BaseType_t xQueueOverwriteFromISR(
QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
/* 偷看队列
* 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,
);
3.队列内部机制(重点)
总的来说也就是有数据读数据,切换任务状态,没有数据就休眠,让CPU空出时间片轮转的位置给别的程序全速运行,同时利用同步通信与互斥的机制,保证数据的稳定性。
内部机制流程:
1)关中断,防止给切换,导致数据有问题,也可以从汇编来看
2)环形buffer
3)链表操作方式,因为涉及链式结构,进行读以及写
4.示例:队列的基本使用
本程序会创建一个队列,然后创建2个发送任务、1个接收任务:
发送任务优先级为1,分别往队列中写入100、200
接收任务优先级为2,读队列、打印数值
mian函数:
/* 队列句柄, 创建队列时会设置这个变量 */
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" );
}
}
}
5.示例:分辨数据源
当有多个发送任务,通过同一个队列发出数据,接收任务如何分辨数据来源?数据本身带有"来源"信 息,比如写入队列的数据是一个结构体,结构体中的lDataSouceID用来表示数据来源:
typedef struct {
ID_t eDataID;
int32_t lDataValue;
}Data_t;
main函数:
/* 定义2种数据来源(ID) */
typedef enum
{
eMotorSpeed,
eSpeedSetPoint
} ID_t;
/* 定义在队列中传输的数据的格式 */
typedef struct {
ID_t eDataID;
int32_t lDataValue;
}Data_t;
/* 定义2个结构体 */
static const Data_t xStructsToSend[ 2 ] =
{
{ eMotorSpeed, 10 }, /* CAN任务发送的数据 */
{ eSpeedSetPoint, 5 } /* HMI任务发送的数据 */
};
/* vSenderTask被用来创建2个任务,用于写队列
* vReceiverTask被用来创建1个任务,用于读队列
*/
static void vSenderTask( void *pvParameters );
static void vReceiverTask( void *pvParameters );
/*-----------------------------------------------------------*/
/* 队列句柄, 创建队列时会设置这个变量 */
QueueHandle_t xQueue;
int main( void )
{
prvSetupHardware();
/* 创建队列: 长度为5,数据大小为4字节(存放一个整数) */
xQueue = xQueueCreate( 5, sizeof( Data_t ) );
if( xQueue != NULL )
{
/* 创建2个任务用于写队列, 传入的参数是不同的结构体地址
* 任务函数会连续执行,向队列发送结构体
* 优先级为2
*/
xTaskCreate(vSenderTask, "CAN Task", 1000, (void *) &
(xStructsToSend[0]), 2, NULL);
xTaskCreate(vSenderTask, "HMI Task", 1000, (void *) &(
xStructsToSend[1]), 2, NULL);
/* 创建1个任务用于读队列
* 优先级为1, 低于上面的两个任务
* 这意味着发送任务优先写队列,队列常常是满的状态
*/
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );
/* 启动调度器 */
vTaskStartScheduler();
}
else
{
/* 无法创建队列 */
}
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
发送任务的函数中,不断往队列中写入数值,代码如下:
static void vSenderTask( void *pvParameters )
{
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );
/* 无限循环 */
for( ;; )
{
/* 写队列
* xQueue: 写哪个队列
* pvParameters: 写什么数据? 传入数据的地址, 会从这个地址把数据复制进队列
* xTicksToWait: 如果队列满的话, 阻塞一会
*/
xStatus = xQueueSendToBack( xQueue, pvParameters, xTicksToWait );
if( xStatus != pdPASS )
{
printf( "Could not send to the queue.\r\n" );
}
}
}
接收函数:
static void vReceiverTask( void *pvParameters )
{
/* 读取队列时, 用这个变量来存放数据 */
Data_t xReceivedStructure;
BaseType_t xStatus;
/* 无限循环 */
for( ;; )
{
/* 读队列
* xQueue: 读哪个队列
* &xReceivedStructure: 读到的数据复制到这个地址
* 0: 没有数据就即刻返回,不阻塞
*/
xStatus = xQueueReceive( xQueue, &xReceivedStructure, 0 );
if( xStatus == pdPASS )
{
/* 读到了数据 */
if( xReceivedStructure.eDataID == eMotorSpeed )
{
printf( "From CAN, MotorSpeed = %d\r\n",
xReceivedStructure.lDataValue );
}
else if( xReceivedStructure.eDataID == eSpeedSetPoint )
{
printf( "From HMI, SpeedSetPoint = %d\r\n",
xReceivedStructure.lDataValue );
}
}
else
{
/* 没读到数据 */
printf( "Could not receive from the queue.\r\n" );
}
}
}