引入:
在裸机中,若写入以下程序,若函数A一直没有将flag置1,则函数B需要一直判断,非常占用CPU资源
void Fun_A()
{
if(条件成立)
flag = 1;
}
void Fun_B()
{
if(flag = 1)
进行操作;
}
int flag = 0;
int main()
{
while(1)
{
Fun_A();
Fun_B();
}
}
若在实时操作系统中,使用消息队列传递信息,当消息队列非空,任务B读取数据;否则,任务阻塞,直到队列中有数据,任务B才可以运行,极大提高CPU的利用率
void task_A()
{
while(1)
{
if(条件成立)
写队列;
}
}
void task_B()
{
while(1)
{
读队列;
进行操作;
}
}
int main()
{
创建消息队列;
创建task_A;
创建task_B;
开启任务调度器;
}
一、队列简介
队列:任务与任务、任务与中断之间进行数据交流的一种机制(消息传递)
FreeRTOS基于队列, 实现了多种功能,其中包括队列集、互斥信号量、计数型信号量、
二值信号量、 递归互斥信号量
队列的核心:关中断(实现互斥)、链表(实现休眠和唤醒)、环形缓冲区(保存数据)
Q:队列作用与全局变量作用的区别?
答:
功能类似于全局变量,但又比全局变量更安全
假设全局变量a= 0,现有两个任务都在操作全局变量a,正常情况下,a = 2,然而,在操作系统中:
对于简单的变量自增,其中也包含许多步骤。当a的值读到CPU中的 r0 寄存器中,正在修改数据时,此时任务2处于就绪态(任务2的优先级 > 任务1的优先级),就会打断任务1,任务对变量a操作完成后,此时a=1。当任务2执行完成后,任务1继续执行,a = 0,恢复出来后,进行修改,则a = 1,但实际值a应该等于2!
全局变量的弊端**:数据无保护,导致数据不安全,当多个任务同时**对同一变量操作时,数据易受损
对于队列而言,在写队列 和读队列 中都会进行关中断、开中断的操作 ,可以防止多任务同时访问冲突
在队列中可以存储数量有限、大小固定的数据。队列中的每一个数据叫做"队列项目 ",队列能够存储"队列项目"的最大数量称为队列的长度, 在创建队列时,队列长度以及队列项目的大小由编程者指定
FreeRTOS队列的特点:
-
数据入队出队方式:队列通常采用"先进先出"(FIFO)的数据存储缓冲机制,即先入队的数据会先从队列中被读取,FreeRTOS中也可以配置为"后进先出"LIFO方式;
-
数据传递方式:FreeRTOS中队列采用实际值传递,即将数据拷贝到队列中进行传递 , FreeRTOS 采用拷贝数据传递,也可以传递指针**,** 所以传递较大的数据的时候采用指针传递(1)值拷贝的方式入队
uint8_t value = 0; xQueueSend(Queue1,&value,portMAX_DELAY);
(2)传递指针的方式入队
uint8_t value = 0; uint8_t* p = &value; xQueueSend(Queue1,&p,portMAX_DELAY);
-
多任务访问:队列不属于某个任务,任何任务和中断都可以向队列发送/读取消息
-
出队、入队阻塞:当任务向一个队列发送消息时,可以指定一个阻塞时间,假设此时当队列已满无法入队或者队列已空无法出队
① 若阻塞时间为 0 : 直接返回 不会等待;
② 若阻塞时间为 0~port_MAX_DELAY : 等待设定的阻塞时间 ,若在该时间内还无法入队,超时后直接返回不再等待;
③ 若阻塞时间为 port_MAX_DELAY :死等, 一直等到可以入队或出队为止
出/入队阻塞:
当多个任务写入消息给一个"满队列"时或者在一个空队列中接收数据,这些任务都会进入阻塞状态 ,当队列有空间或者有队列项目时:
1、优先级最高的任务
2、如果大家的优先级相同,那等待时间最久的任务会进入就绪态
二、队列结构体
typedef struct QueueDefinition
{
int8_t * pcHead; /* 存储区域的起始地址 */
int8_t * pcWriteTo; /* 下一个写入的位置 */
union
{
QueuePointers_t xQueue;
SemaphoreData_t xSemaphore;
} u ;
List_t xTasksWaitingToSend; /* 等待发送列表 */
/*队列满,而入队失败进入阻塞状态的任务,则挂在此列表上 */
List_t xTasksWaitingToReceive; /* 等待接收列表 */
/*队列空,而出队失败进入阻塞状态的任务,则挂在此列表上 */
volatile UBaseType_t uxMessagesWaiting; /* 非空闲队列项目的数量 */
UBaseType_t uxLength; /* 队列长度 */
UBaseType_t uxItemSize; /* 队列项目的大小 */
volatile int8_t cRxLock; /* 读取上锁计数器 ,queueUNLOCKED:未上锁*/
/*上锁后,统计出队的队列项数量 */
volatile int8_t cTxLock; /* 写入上锁计数器 ,queueUNLOCKED:未上锁*/
/*上锁后,统计入队的队列项数量 */
/* 其他的一些条件编译 */
} xQUEUE;
队列锁:当上锁后,无法操作队列 等待发送列表 、等待接收列表,但可以正常进行对队列的读写操作,默认初始化,不上锁
共用体:
队列的基本操作示意图:
三、队列相关API函数
使用队列的主要流程:创建队列 ------> 写队列------> 读队列
3.1 动态创建队列:xQueueCreate()
头文件:queue.h
使用条件:configSUPPORT_DYNAMIC_ALLOCATION 必须在 FreeRTOSConfig.h 中被设置为 1,或保留未定义状态(此时,它默认为 1)
函数原型:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,
UBaseType_t uxItemSize );
参数说明:
|---------------|--------------------------|
| 参数 | 描述 |
| uxQueueLength | 队列可同时容纳的最大项目数 |
| uxItemSize | 存储队列中的每个数据项所需的大小(以字节为单位) |
| 返回值 | 成功:返回所创建的队列句柄 失败:NULL |
用法示例:
struct AMessage
{
char ucMessageID;
char ucData[ 20 ];
};
void vATask( void *pvParameters )
{
QueueHandle_t xQueue1, xQueue2;
/* Create a queue capable of containing 10 unsigned long values. */
xQueue1 = xQueueCreate( 10, sizeof( unsigned long ) );
if( xQueue1 == NULL )
{
/* Queue was not created and must not be used. */
}
/* Create a queue capable of containing 10 pointers to AMessage
structures. These are to be queued by pointers as they are
relatively large structures. */
xQueue2 = xQueueCreate( 10, sizeof( struct AMessage * ) );
if( xQueue2 == NULL )
{
/* Queue was not created and must not be used. */
}
/* ... Rest of task code. */
}
注:
#define xQueueCreate ( uxQueueLength, uxItemSize )
xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), (queueQUEUE_TYPE_BASE ))
动态创建队列函数实际是通过一个宏实现的,被定义的函数有三个参数,最后一个参数指定了参数的不同类型
动态创建队列函数详解:
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
const uint8_t ucQueueType )
{
Queue_t * pxNewQueue = NULL;
size_t xQueueSizeInBytes;
uint8_t * pucQueueStorage;
/* 判断队列长度是否大于0,以及是否溢出
if( ( uxQueueLength > ( UBaseType_t ) 0 ) &&
( ( SIZE_MAX / uxQueueLength ) >= uxItemSize ) &&
/* Check for addition overflow. */
( ( SIZE_MAX - sizeof( Queue_t ) ) >= ( uxQueueLength * uxItemSize ) ) )
{
/* 计算队列项的总大小 */
xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize );
/* 申请队列结构体以及环形缓冲区的内存空间 */
pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );
/* 申请成功 */
if( pxNewQueue != NULL )
{
pucQueueStorage = ( uint8_t * ) pxNewQueue; //队列结构体的首地址
pucQueueStorage += sizeof( Queue_t ); //队列环形缓冲区的首地址
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
{
pxNewQueue->ucStaticallyAllocated = pdFALSE;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
/* 初始化队列 */
prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );
}
else
{
traceQUEUE_CREATE_FAILED( ucQueueType );
mtCOVERAGE_TEST_MARKER();
}
}
else
{
configASSERT( pxNewQueue );
mtCOVERAGE_TEST_MARKER();
}
return pxNewQueue;
}
动态创建队列流程图:
各个成员的初值如图所示:
3.2 写入消息(入队)
|----------------------------|----------------------------|
| 函数 | 描述 |
| xQueueSend() | 往队列的尾部写入消息 |
| xQueueSendToBack() | 同 xQueueSend() |
| xQueueSendToFront() | 往队列的头部写入消息 |
| xQueueOverwrite() | 覆写队列消息(只用于队列长度为 1 的情况) |
| xQueueSendFromISR() | 在中断中往队列的尾部写入消息 |
| xQueueSendToBackFromISR() | 同 xQueueSendFromISR() |
| xQueueSendToFrontFromISR() | 在中断中往队列的头部写入消息 |
| xQueueOverwriteFromISR() | 在中断中覆写队列消息(只用于队列长度为 1 的情况) |
可以看出:写入函数调用的是同一个函数xQueueGenericSend( ),只是指定了不同的写入位置以及阻塞时间
队列写入位置一共有三种:
注意:覆写方式写入队列,只有在队列的队列长度为 1 时,才能够使用,不论队列中是否有数据,都可以写进去,并且不会阻塞
总结:
写入队列尾部:从 pcWriteTo 位置开始写入,之后地址++,即向上增长,如果超过既定区域,返回头尾部(pcWriteTo指向初始位置,即pcHead指向的位置)
写入队列头部:从 pcReadFrom 位置开始写入,之后地址--,即向下增长,如果超过既定区域,返回头部 (pcReadFrom指向初始位置,即pcTail - 一个队列项的大小)
写入队列流程图:
3.3 读取消息(出队)
|------------------------|--------------------------|
| 函数 | 描述 |
| xQueueReceive() | 从队列头部读取 消息,并删除消息 |
| xQueuePeek() | 从队列头部读取 消息 |
| xQueueReceiveFromISR() | 在中断中从队列头部读取消息,并删除消息 |
| xQueuePeekFromISR() | 在中断中从队列头部读取消息 |
(1)xQueueReceive()
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait )
此函数用于在任务中,从队列中读取消息,并且消息读取成功后,会将消息从队列中移除。
(2)xQueuePeek()
BaseType_t xQueuePeek( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait )
此函数用于在任务中,从队列中读取消息, 但与函数 xQueueReceive()不同,此函数在成功读取消息后,并不会移除已读取的消息
读消息:
将pcReadFrom指向位置的数据拷贝出来
如果读取的位置等于尾部,则将头部位置赋给读取的位置,进行读取消息