——————(正点原子FreeRTOS学习笔记)
开始学习FreeRTOS,学习参考书籍和视频来自正点原子FreeRTOS源码详解与应用开发,北京航空航天大学出版社
1 队列简介
队列是为了任务与任务、任务与中断之间的通信而准备的,可以在任务与任务、任务与中断之间传递消息,队列中可以存储有限的、大小固定的数据项目。任务与任务、任务与中断之间要交流的数据保存在队列中,叫做队列项目。队列所能保存的最大数据项目数量叫做队列的长度,创建队列的时候会指定数据项目的大小和队列的长度。由于队列用来传递消息的,所以也称为消息队列。FreeRTOS
中的信号量的也是依据队列实现的!所以有必要深入的了解FreeRTOS 的队列。
1 、数据存储
通常队列采用先进先出(FIFO)的存储缓冲机制,也就是往队列发送数据的时候(也叫入队)永远都是发送到队列的尾部,而从队列提取数据的时候(也叫出队)是从队列的头部提取的。但是也可以使用 LIFO 的存储缓冲,也就是后进先出,FreeRTOS 中的队列也提供了 LIFO 的存储缓冲机制。数据发送到队列中会导致数据拷贝,也就是将要发送的数据拷贝到队列中,这就意味着在队列中存储的是数据的原始值,而不是原数据的引用(即只传递数据的指针),这个也叫做值传递。学过
uC/OS 的同学应该知道,uC/OS 的消息队列采用的是引用传递,传递的是消息指针。采用引用传递的话消息内容就必须一直保持可见性,也就是消息内容必须有效,那么局部变量这种可能会随时被删掉的东西就不能用来传递消息,但是采用引用传递会节省时间啊!因为不
用进行数据拷贝。采用值传递的话虽然会导致数据拷贝,会浪费一点时间,但是一旦将消息发送到队列中原始的数据缓冲区就可以删除掉或者覆写,这样的话这些缓冲区就可以被重复的使用。FreeRTOS中使用队列传递消息的话虽然使用的是数据拷贝,但是也可以使用引用来传递消息啊,我直接往队列中发送指向这个消息的地址指针不就可以了!这样当我要发送的消息数据太大的时候就可以直接发送消息缓冲区的地址指针,比如在网络应用环境中,网络的数据量往往都很大的,采用数据拷贝的话就不现实。
2 、多任务访问
队列不是属于某个特别指定的任务的,任何任务都可以向队列中发送消息,或者从队列中提取消息。
3 、出队阻塞
当任务尝试从一个队列中读取消息的时候可以指定一个阻塞时间,这个阻塞时间就是当任务从队列中读取消息无效的时候任务阻塞的时间。出队就是就从队列中读取消息,出队阻塞是针对从队列中读取消息的任务而言的。比如任务 A 用于处理串口接收到的数据,串口接收到数据以后就会放到队列 Q 中,任务
A 从队列 Q 中读取数据。但是如果此时队列 Q 是空的,说明还没有数据,任务 A 这时候来读取的话肯定是获取不到任何东西,那该怎么办呢?任务 A 现在有三种选择,一:二话不说扭头就走,二:要不我在等等吧,等一会看看,说不定一会就有数据了,三:死等,死也要等到你有数据!选哪一个就是由这个阻塞时间决定的,这个阻塞时间单位是时钟节拍数。阻塞时间为
0 的话就是不阻塞,没有数据的话就马上返回任务继续执行接下来的代码,对应第一种选择。如果阻塞时间为 0~ portMAX_DELAY,当任务没有从队列中获取到消息的话就进入阻塞态,阻塞时间指定了任务进入阻塞态的时间,当阻塞时间到了以后还没有接收到数据的话就退出阻塞态,返回任务接着运行下面的代码,如果在阻塞时间内接收到了数据就立即返回,执行任务中下面的代码,这种情况对应第二种选择。当阻塞时间设置为portMAX_DELAY
的话,任务就会一直进入阻塞态等待,直到接收到数据为止!这个就是第三种选择。
4 、入队阻塞
入队说的是向队列中发送消息,将消息加入到队列中。和出队阻塞一样,当一个任务向队列发送消息的话也可以设置阻塞时间。比如任务 B 向消息队列 Q 发送消息,但是此时队列 Q 是满的,那肯定是发送失败的。此时任务 B 就会遇到和上面任务 A 一样的问题,这两种情况的处理过程是类似的,只不过一个是向队列
Q 发送消息,一个是从队列 Q 读取消息而已。
5 、队列操作过程图示
下面几幅图简单的演示了一下队列的入队和出队过程。
图 13.1.1 中任务 A 要向任务 B 发送消息,这个消息是 x 变量的值。首先创建一个队列,并且指定队列的长度和每条消息的长度。这里我们创建了一个长度为 4 的队列,因为要传递的是
x 值,而 x 是个 int 类型的变量,所以每条消息的长度就是 int 类型的长度,在 STM32 中就是 4字节,即每条消息是 4 个字节的。
图 13.1.2 中任务 A 的变量 x 值为 10,将这个值发送到消息队列中。此时队列剩余长度就是3 了。前面说了向队列中发送消息是采用拷贝的方式,所以一旦消息发送完成变量 x 就可以再
次被使用,赋其他的值。
图 13.1.3 中任务 A 又向队列发送了一个消息,即新的 x 的值,这里是 20。此时队列剩余长度为 2。
图 13.1.4 中任务 B 从队列中读取消息,并将读取到的消息值赋值给 y,这样 y 就等于 10了。任务 B 从队列中读取消息完成以后可以选择清除掉这个消息或者不清除。当选择清除这个
消息的话其他任务或中断就不能获取这个消息了,而且队列剩余大小就会加一,变成 3。如果不清除的话其他任务或中断也可以获取这个消息,而队列剩余大小依旧是 2。
1 队列结构体
有一个结构体用于描述队列,叫做 Queue_t,这个结构体在文件 queue.c 中定义如下:
typedef struct QueueDefinition
{
int8_t *pcHead; //指向队列存储区开始地址。
int8_t *pcTail; //指向队列存储区最后一个字节。
int8_t *pcWriteTo; //指向存储区中下一个空闲区域。
union
{
int8_t *pcReadFrom; //当用作队列的时候指向最后一个出队的队列项首地址
UBaseType_t uxRecursiveCallCount;//当用作递归互斥量的时候用来记录递归互斥量被调用的次数。
} 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
#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) &&\
( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated; //如果使用静态存储的话此字段设置为 pdTURE。
#endif
#if ( configUSE_QUEUE_SETS == 1 ) //队列集相关宏
struct QueueDefinition *pxQueueSetContainer;
#endif
#if ( configUSE_TRACE_FACILITY == 1 ) //跟踪调试相关宏
UBaseType_t uxQueueNumber;
uint8_t ucQueueType;
#endif
} xQUEUE;
typedef xQUEUE Queue_t;
老版本的 FreeRTOS 中队列可能会使用 xQUEUE 这个名字,新版本 FreeRTOS 中队列的名字都使用 Queue_t。
2 队列创建
2.1 、函数原型
在使用队列之前必须先创建队列,有两种创建队列的方法,一种是静态的,使用函数xQueueCreateStatic();另一个是动态的,使用函数 xQueueCreate()。这两个函数本质上都是宏,真正完成队列创建的函数是
xQueueGenericCreate()和 xQueueGenericCreateStatic(),这两个函数在文件 queue.c 中有定义,这四个函数的原型如下。
1 、函数
xQueueCreate()
此函数本质上是一个宏,用来动态创建队列,此宏最终调用的是函数 xQueueGenericCreate(),函数原型如下:
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength,
UBaseType_t uxItemSize)
参数:
uxQueueLength: 要创建的队列的队列长度,这里是队列的项目数。
uxItemSize: 队列中每个项目(消息)的长度,单位为字节
返回值:
其他值: 队列创捷成功以后返回的队列句柄!
NULL: 队列创建失败。
2 、函数
xQueueCreateStatic()
此函数也是用于创建队列的,但是使用的静态方法创建队列,队列所需要的内存由用户自行分配,此函数本质上也是一个宏,此宏最终调用的是函数 xQueueGenericCreateStatic(),函数原型如下:
QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t * pucQueueStorageBuffer,
StaticQueue_t * pxQueueBuffer)
参数:
uxQueueLength: 要创建的队列的队列长度,这里是队列的项目数。
uxItemSize: 队列中每个项目(消息)的长度,单位为字节
pucQueueStorage: 指向队列项目的存储区,也就是消息的存储区,这个存储区需要用户自行分配。此参数必须指向一个 uint8_t 类型的数组。这个存储区要大于等于(uxQueueLength * uxItemsSize)字节。
pxQueueBuffer: 此参数指向一个 StaticQueue_t 类型的变量,用来保存队列结构体。
返回值:
其他值: 队列创捷成功以后的队列句柄!
NULL: 队列创建失败。
3 、函数 xQueueGenericCreate()
函数 xQueueGenericCreate()用于动态创建队列,创建队列过程中需要的内存均通过FreeRTOS 中的动态内存管理函数 pvPortMalloc()分配,函数原型如下:
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
const uint8_t ucQueueType )
参数:
uxQueueLength: 要创建的队列的队列长度,这里是队列的项目数。
uxItemSize: 队列中每个项目(消息)的长度,单位为字节。
ucQueueType : 队列类型,由于 FreeRTOS 中的信号量等也是通过队列来实现的,创建信号量的函数最终也是使用此函数的,因此在创建的时候需要指定此队列的用途,也就是队列类型,一共有六种类型:
queueQUEUE_TYPE_BASE 普通的消息队列
queueQUEUE_TYPE_SET 队列集
queueQUEUE_TYPE_MUTEX 互斥信号量
queueQUEUE_TYPE_COUNTING_SEMAPHORE 计数型信号量
queueQUEUE_TYPE_BINARY_SEMAPHORE 二值信号量
queueQUEUE_TYPE_RECURSIVE_MUTEX 递归互斥信号量
函 数 xQueueCreate() 创 建 队 列 的 时 候 此 参 数 默 认 选 择 的 就 是
queueQUEUE_TYPE_BASE。
返回值:
其他值: 队列创捷成功以后的队列句柄!
NULL: 队列创建失败。
3 、函数 xQueueGenericCreateStatic()
此函数用于动态创建队列,创建队列过程中需要的内存需要由用户自行分配好,函数原型如下:
QueueHandle_t xQueueGenericCreateStatic( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
uint8_t * pucQueueStorage,
StaticQueue_t * pxStaticQueue,
const uint8_t ucQueueType )
参数:
uxQueueLength: 要创建的队列的队列长度,这里是队列的项目数。
uxItemSize: 队列中每个项目(消息)的长度,单位为字节
pucQueueStorage: 指向队列项目的存储区,也就是消息的存储区,这个存储区需要用户自行分配。此参数必须指向一个 uint8_t 类型的数组。这个存储区要大于等
于(uxQueueLength * uxItemsSize)字节。
pxStaticQueue: 此参数指向一个 StaticQueue_t 类型的变量,用来保存队列结构体。
ucQueueType : 队列类型。
返回值:
其他值: 队列创捷成功以后队列句柄!
NULL: 队列创建失败。
2.2 队列创建函数详解
最终完成队列创建的函数有两个,一个是静态方法的 xQueueGenericCreateStatic(),另外一个 就 是 动 态 方 法 的 xQueueGenericCreate() 。 我 们 来 详 细 的 分 析 一 下 动 态 创 建 函 数xQueueGenericCreate(),静态方法大同小异,大家可以自行分析一下。函数 xQueueGenericCreate()在文件
queue.c 中有如下定义:
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
const uint8_t ucQueueType )
{
Queue_t *pxNewQueue;
size_t xQueueSizeInBytes;
uint8_t *pucQueueStorage;
configASSERT( uxQueueLength > ( UBaseType_t ) 0 );
if( uxItemSize == ( UBaseType_t ) 0 )
{
//队列项大小为 0,那么就不需要存储区。
xQueueSizeInBytes = ( size_t ) 0;
}
else
{
//分配足够的存储区,确保随时随地都可以保存所有的项目(消息),
xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize ); (1)
}
pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes ); (2)
//内存申请成功
if( pxNewQueue != NULL )
{
pucQueueStorage = ( ( uint8_t * ) pxNewQueue ) + sizeof( Queue_t ); (3)
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
//队列是使用动态方法创建的,所以队列字段 ucStaticallyAllocated 标记为 pdFALSE。
pxNewQueue->ucStaticallyAllocated = pdFALSE;
}
#endif
prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, \ (4)
ucQueueType, pxNewQueue );
}
return pxNewQueue;
}
(1)、 队列是要存储消息的,所以必须要有消息的存储区,函数的参数 uxQueueLength 和uxItemSize 指定了队列中最大队列项目(消息)数量和每个消息的长度,两者相乘就是消息存储区的大小。
(2)、 调用函数 pvPortMalloc()给队列分配内存,注意这里申请的内存大小是队列结构体和队列中消息存储区的总大小。
(3)、 计算出消息存储区的首地址,(2)中申请到的内存是队列结构体和队列中消存储区的总大小,队列结构体内存在前,紧跟在后面的就是消息存储区内存。
(4)、 调用函数 prvInitialiseNewQueue()初始化队列。
可以看出函数 xQueueGenericCreate()重要的工作就是给队列分配内存,当内存分配成功以
后调用函数 prvInitialiseNewQueue()来初始化队列。
2.3 队列初始化函数
队列初始化函数 prvInitialiseNewQueue()用于队列的初始化,此函数在文件 queue.c 中有定义,函数代码如下:
static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength, //队列长度
const UBaseType_t uxItemSize, //队列项目长度
uint8_t * pucQueueStorage, //队列项目存储区
const uint8_t ucQueueType, //队列类型
Queue_t * pxNewQueue ) //队列结构体
{
//防止编译器报错
( void ) ucQueueType;
if( uxItemSize == ( UBaseType_t ) 0 )
{
//队列项(消息)长度为 0,说明没有队列存储区,这里将 pcHead 指向队列开始地址
pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;
}
else
{
//设置 pcHead 指向队列项存储区首地址
pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage; (1)
}
//初始化队列结构体相关成员变量
pxNewQueue->uxLength = uxQueueLength; (2)
pxNewQueue->uxItemSize = uxItemSize;
( void ) xQueueGenericReset( pxNewQueue, pdTRUE ); (3)
#if ( configUSE_TRACE_FACILITY == 1 ) //跟踪调试相关字段初始化
{
pxNewQueue->ucQueueType = ucQueueType;
}
#endif /* configUSE_TRACE_FACILITY */
#if( configUSE_QUEUE_SETS == 1 ) //队列集相关字段初始化
{
pxNewQueue->pxQueueSetContainer = NULL;
}
#endif /* configUSE_QUEUE_SETS */
traceQUEUE_CREATE( pxNewQueue );
}
(1)、队列结构体中的成员变量 pcHead 指向队列存储区中首地址。
(2)、初始化队列结构体中的成员变量 uxQueueLength 和 uxItemSize,这两个成员变量保存队列的最大队列项目和每个队列项大小。
(3)、调用函数 xQueueGenericReset()复位队列。PS:发一句牢骚,绕来绕去的,函数调了一个又一个的。
2.4 队列复位函数
队列初始化函数 prvInitialiseNewQueue()中调用了函数 xQueueGenericReset()来复位队列,函数 xQueueGenericReset()代码如下:
BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue )
{
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
configASSERT( pxQueue );
taskENTER_CRITICAL();
{
//初始化队列相关成员变量
pxQueue->pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->\ (1)
uxItemSize );
pxQueue->uxMessagesWaiting = ( UBaseType_t ) 0U;
pxQueue->pcWriteTo = pxQueue->pcHead;
pxQueue->u.pcReadFrom = pxQueue->pcHead + ( ( pxQueue->uxLength - \
( UBaseType_t ) 1U ) * pxQueue->uxItemSize );
pxQueue->cRxLock = queueUNLOCKED;
pxQueue->cTxLock = queueUNLOCKED;
if( xNewQueue == pdFALSE ) (2)
{
//由于复位队列以后队列依旧是空的,所以对于那些由于出队(从队列中读取消
//息)而阻塞的任务就依旧保持阻塞壮态。但是对于那些由于入队(向队列中发送
//消息)而阻塞的任务就不同了,这些任务要解除阻塞壮态,从队列的相应列表中
//移除。
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->\
xTasksWaitingToSend ) ) != pdFALSE )
{
queueYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
//初始化队列中的列表
vListInitialise( &( pxQueue->xTasksWaitingToSend ) ); (3)
vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );
}
}
taskEXIT_CRITICAL();
return pdPASS;
}
(1)、初始化队列中的相关成员变量。
(2)、根据参数 xNewQueue 确定要复位的队列是否是新创建的队列,如果不是的话还需要
做其他的处理
(3)、初始化队列中的列表 xTasksWaitingToSend 和 xTasksWaitingToReceive。
至此,队列创建成功,比如我们创建一个有 4 个队列项,每个队列项长度为 32 个字节的队列 TestQueue,创建成功的队列如图 13.3.4.1 所示:
3 向队列发送消息
3.1 函数原型
创建好队列以后就可以向队列发送消息了,FreeRTOS 提供了 8 个向队列发送消息的 API 函数,如表 13.4.1 所示:
1 、函数 xQueueSend() 、xQueueSendToBack()和 和 xQueueSendToFront()
这三个函数都是用于向队列中发送消息的,这三个函数本质都是宏,其中函数 xQueueSend()和
xQueueSendToBack()是一样的,都是后向入队,即将新的消息插入到队列的后面。函数
xQueueSendToToFront()是前向入队,即将新消息插入到队列的前面。然而!这三个函数最后都是调用的同一个函数:xQueueGenericSend()。这三个函数只能用于任务函数中,不能用于中断
服务函数,中断服务函数有专用的函数,它们以“FromISR”结尾,这三个函数的原型如下:
BaseType_t xQueueSend( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait);
BaseType_t xQueueSendToBack(QueueHandle_t xQueue,
const void* pvItemToQueue,
TickType_t xTicksToWait);
BaseType_t xQueueSendToToFront(QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait);
参数:
xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。
pvItemToQueue: 指向要发送的消息,发送时候会将这个消息拷贝到队列中。
xTicksToWait: 阻塞时间,此参数指示当队列满的时候任务进入阻塞态等待队列空闲的最大时间。如果为 0 的话当队列满的时候就立即返回;当为 portMAX_DELAY 的话就会一直等待,直到队列有空闲的队列项,也就是死等,但是宏INCLUDE_vTaskSuspend 必须为 1。
返回值:
pdPASS: 向队列发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败。
2 、函数 xQueueOverwrite()
此函数也是用于向队列发送数据的,当队列满了以后会覆写掉旧的数据,不管这个旧数据有没有被其他任务或中断取走。这个函数常用于向那些长度为 1 的队列发送消息,此函数也是
一个宏,最终调用的也是函数 xQueueGenericSend(),函数原型如下:
BaseType_t xQueueOverwrite(QueueHandle_t xQueue,
const void * pvItemToQueue);
参数:
xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。
pvItemToQueue:指向要发送的消息,发送的时候会将这个消息拷贝到队列中。
返回值:
pdPASS: 向队列发送消息成功,此函数也只会返回 pdPASS!因为此函数执行过程中不在乎队列满不满,满了的话我就覆写掉旧的数据,总之肯定能成功。
3 、函数 xQueueGenericSend()
此函数才是真正干活的,上面讲的所有的任务级入队函数最终都是调用的此函数,此函数也是我们后面重点要讲解的,先来看一下函数原型:
BaseType_t xQueueGenericSend( QueueHandle_t xQueue,
const void * const pvItemToQueue,
TickType_t xTicksToWait,
const BaseType_t xCopyPosition )
参数:
xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。
pvItemToQueue:指向要发送的消息,发送的过程中会将这个消息拷贝到队列中。
xTicksToWait : 阻塞时间。
xCopyPosition: 入队方式,有三种入队方式:
queueSEND_TO_BACK: 后向入队
queueSEND_TO_FRONT: 前向入队
queueOVERWRITE: 覆写入队。
上面讲解的入队 API 函数就是通过此参数来决定采用哪种入队方式的。
返回值:
pdTRUE: 向队列发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败。
4 、函数 xQueueSendFromISR() 、xQueueSendToBackFromISR() 、xQueueSendToFrontFromISR()
这三个函数也是向队列中发送消息的,这三个函数用于中断服务函数中。这三个函数本质也宏,其中函数 xQueueSendFromISR ()和 xQueueSendToBackFromISR ()是一样的,都是后向入
队,即将新的消息插入到队列的后面。函数 xQueueSendToFrontFromISR ()是前向入队,即将新消息插入到队列的前面。这三个函数同样调用同一个函数 xQueueGenericSendFromISR ()。这三
个函数的原型如下:
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
参数:
xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。
pvItemToQueue:指向要发送的消息,发送的时候会将这个消息拷贝到队列中。
pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值由这三个函数来设置的,用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdTRUE: 向队列中发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败。
我们注意观察,可以看出这些函数都没有设置阻塞时间值。原因很简单,这些函数都是在中断服务函数中调用的,并不是在任务中,所以也就没有阻塞这一说了!
5 、函数 xQueueOverwriteFromISR()
此函数是 xQueueOverwrite()的中断级版本,用在中断服务函数中,在队列满的时候自动覆写掉旧的数据,此函数也是一个宏,实际调用的也是函数 xQueueGenericSendFromISR(),此函
数原型如下:
BaseType_t xQueueOverwriteFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
此函数的参数和返回值同上面三个函数相同。
6 、函数 xQueueGenericSendFromISR()
上面说了 4 个中断级入队函数最终都是调用的函数 xQueueGenericSendFromISR(),这是真正干活的主啊,也是我们下面会详细讲解的函数,先来看一下这个函数的原型,如下:
BaseType_t xQueueGenericSendFromISR(QueueHandle_t xQueue,
const void* pvItemToQueue,
BaseType_t* pxHigherPriorityTaskWoken,
BaseType_t xCopyPosition);
参数:
xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。
pvItemToQueue:指向要发送的消息,发送的过程中会将这个消息拷贝到队列中。
pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值由这三个函数来设置的,用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
xCopyPosition: 入队方式,有三种入队方式:
queueSEND_TO_BACK: 后向入队
queueSEND_TO_FRONT: 前向入队
queueOVERWRITE: 覆写入队。
返回值:
pdTRUE: 向队列发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败。
3.2 任务级通用入队函数
不 管 是 后 向 入 队 、 前 向 入 队 还 是 覆 写 入 队 , 最 终 调 用 的 都 是 通 用 入 队 函 数xQueueGenericSend(),这个函数在文件 queue.c 文件中由定义,缩减后的函数代码如下:
BaseType_t xQueueGenericSend( QueueHandle_t xQueue,
const void * const pvItemToQueue,
TickType_t xTicksToWait,
const BaseType_t xCopyPosition )
{
BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired;
TimeOut_t xTimeOut;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
for( ;; )
{
taskENTER_CRITICAL(); //进入临界区
{
//查询队列现在是否还有剩余存储空间,如果采用覆写方式入队的话那就不用在
//乎队列是不是满的啦。
if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) ||\ (1)
( xCopyPosition == queueOVERWRITE ) )
{
traceQUEUE_SEND( pxQueue );
xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue,\ (2)
xCopyPosition );
/**************************************************************************/
/**************************省略掉与队列集相关代码**************************/
/**************************************************************************/
{
//检查是否有任务由于等待消息而进入阻塞态
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) ==\(3)
pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->\ (4)
xTasksWaitingToReceive ) ) != pdFALSE )
{
//解除阻塞态的任务优先级最高,因此要进行一次任务切换
queueYIELD_IF_USING_PREEMPTION(); (5)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else if( xYieldRequired != pdFALSE )
{
queueYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
return pdPASS; (6)
}
else
{
if( xTicksToWait == ( TickType_t ) 0 ) (7)
{
//队列是满的,并且没有设置阻塞时间的话就直接返回
taskEXIT_CRITICAL();
traceQUEUE_SEND_FAILED( pxQueue );
return errQUEUE_FULL; (8)
}
else if( xEntryTimeSet == pdFALSE ) (9)
{
//队列是满的并且指定了任务阻塞时间的话就初始化时间结构体
vTaskSetTimeOutState( &xTimeOut );
xEntryTimeSet = pdTRUE;
}
else
{
//时间结构体已经初始化过了,
mtCOVERAGE_TEST_MARKER();
}
}
}
taskEXIT_CRITICAL(); //退出临界区
vTaskSuspendAll(); (10)
prvLockQueue( pxQueue ); (11)
//更新时间壮态,检查是否有超时产生
if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ) (12)
{
if( prvIsQueueFull( pxQueue ) != pdFALSE ) (13)
{
traceBLOCKING_ON_QUEUE_SEND( pxQueue );
vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), \ (14)
xTicksToWait );
prvUnlockQueue( pxQueue ); (15)
if( xTaskResumeAll() == pdFALSE ) (16)
{
portYIELD_WITHIN_API();
}
}
else
{
//重试一次
prvUnlockQueue( pxQueue ); (17)
( void ) xTaskResumeAll();
}
}
else
{
//超时产生
prvUnlockQueue( pxQueue ); (18)
( void ) xTaskResumeAll();
traceQUEUE_SEND_FAILED( pxQueue );
return errQUEUE_FULL; (19)
}
}
}
(1)、要向队列发送数据,肯定要先检查一下队列是不是满的,如果是满的话肯定不能发送的。当队列未满或者是覆写入队的话就可以将消息入队了。
(2)、调用函数 prvCopyDataToQueue()将消息拷贝到队列中。前面说了入队分为后向入队、前向入队和覆写入队,他们的具体实现就是在函数 prvCopyDataToQueue()中完成的。如果选择
后向入队 queueSEND_TO_BACK 的话就将消息拷贝到队列结构体成员 pcWriteTo 所指向的队列项,拷贝成功以后 pcWriteTo 增加 uxItemSize 个字节,指向下一个队列项目。当选择前向入
队 queueSEND_TO_FRONT 或者 queueOVERWRITE 的话就将消息拷贝到 u.pcReadFrom 所指向的队列项目,同样的需要调整 u.pcReadFrom 的位置。当向队列写入一个消息以后队列中统计当
前消息数量的成员 uxMessagesWaiting 就会加一,但是选择覆写入队 queueOVERWRITE 的话还会将 uxMessagesWaiting 减一,这样一减一加相当于队列当前消息数量没有变。
(3) 、 检 查 是 否 有 任 务 由 于 请 求 队 列 消 息 而 阻 塞 , 阻 塞 的 任 务 会 挂 在 队 列 的xTasksWaitingToReceive 列表上。
(4)、有任务由于请求消息而阻塞,因为在(2)中已将向队列中发送了一条消息了,所以调用函数 xTaskRemoveFromEventList()将阻塞的任务从列表 xTasksWaitingToReceive 上移除,并且把
这个任务添加到就绪列表中,如果调度器上锁的话这些任务就会挂到列表 xPendingReadyList 上。如果取消阻塞的任务优先级比当前正在运行的任务优先级高还要标记需要进行任务切换。当函
数 xTaskRemoveFromEventList()返回值为 pdTRUE 的话就需要进行任务切换。
(5)、进行任务切换。
(6)、返回 pdPASS,标记入队成功。
(7)、(2)到(6)都是非常理想的效果,即消息队列未满,入队没有任何障碍。但是队列满了以后呢?首先判断设置的阻塞时间是否为 0,如果为 0 的话就说明没有阻塞时间。
(8)、由(7)得知阻塞时间为 0,那就直接返回 errQUEUE_FULL,标记队列已满就可以了。
(9)、如果阻塞时间不为0并且时间结构体还没有初始化的话就初始化一次超时结构体变量,调用函数 vTaskSetTimeOutState()完成超时结构体变量 xTimeOut 的初始化。其实就是记录当前的系统时钟节拍计数器的值 xTickCount 和溢出次数 xNumOfOverflows。
(10)、任务调度器上锁,代码执行到这里说明当前的状况是队列已满了,而且设置了不为 0的阻塞时间。那么接下来就要对任务采取相应的措施了,比如将任务加入到队列的xTasksWaitingToSend 列表中。
(11)、调用函数 prvLockQueue()给队列上锁,其实就是将队列中的成员变量 cRxLock 和cTxLock 设置为 queueLOCKED_UNMODIFIED。
(12)、调用函数 xTaskCheckForTimeOut()更新超时结构体变量 xTimeOut,并且检查阻塞时间是否到了。
(13)、阻塞时间还没到,那就检查队列是否还是满的。
(14)、经过(12)和(13)得出阻塞时间没到,而且队列依旧是满的,那就调用函数vTaskPlaceOnEventList()将任务添加到队列的 xTasksWaitingToSend 列表中和延时列表中,并且将 任 务 从 就 绪 列 表 中 移 除 。 注 意 ! 如 果 阻 塞 时 间 是 portMAX_DELAY 并 且 宏INCLUDE_vTaskSuspend
为 1 的话,函数 vTaskPlaceOnEventList()会将任务添加到列表xSuspendedTaskList 上。
(15)、操作完成,调用函数 prvUnlockQueue()解锁队列。
(16)、调用函数 xTaskResumeAll()恢复任务调度器
(17)、阻塞时间还没到,但是队列现在有空闲的队列项,那么就在重试一次。
(18)、相比于第(12)步,阻塞时间到了!那么任务就不用添加到那些列表中了,那就解锁队列,恢复任务调度器。
(19)、返回 errQUEUE_FULL,表示队列满了。
3.3 中断级 通用 入队函数
讲完任务级入队函数再来看一下中断级入队函数 xQueueGenericSendFromISR(),其他的中断级入队函数都是靠此函数来实现的。中断级入队函数和任务级入队函数大同小异,函数代码
如下:
BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue,
const void * const pvItemToQueue,
BaseType_t * const pxHigherPriorityTaskWoken,
const BaseType_t xCopyPosition )
{
BaseType_t xReturn;
UBaseType_t uxSavedInterruptStatus;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
{
if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) ||\ (1)
( xCopyPosition == queueOVERWRITE ) )
{
const int8_t cTxLock = pxQueue->cTxLock; (2)
traceQUEUE_SEND_FROM_ISR( pxQueue );
( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition ); (3)
//队列上锁的时候就不能操作事件列表,队列解锁的时候会补上这些操作的。
if( cTxLock == queueUNLOCKED ) (4)
{
/**************************************************************************/
/**************************省略掉与队列集相关代码**************************/
/**************************************************************************/
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == \ (5)
pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->\ (6)
xTasksWaitingToReceive ) ) != pdFALSE )
{
//刚刚从事件列表中移除的任务对应的任务优先级更高,所以 标记要进行任务切换
if( pxHigherPriorityTaskWoken != NULL )
{
*pxHigherPriorityTaskWoken = pdTRUE; (7)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
else
{
//cTxLock 加一,这样就知道在队列上锁期间向队列中发送了数据
pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 ); (8)
}
xReturn = pdPASS; (9)
}
else
{
traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue );
xReturn = errQUEUE_FULL; (10)
}
}
portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
return xReturn;
}
(1)、队列未满或者采用的覆写的入队方式,这是最理想的壮态。
(2)、读取队列的成员变量 xTxLock,用于判断队列是否上锁。
(3)、将数据拷贝到队列中。
(4)、队列上锁了,比如任务级入队函数在操作队列中的列表的时候就会对队列上锁。
(5)、判断队列列表 xTasksWaitingToReceive 是否为空,如果不为空的话说明有任务在请求消息的时候被阻塞了。
(6)、将相应的任务从列表 xTasksWaitingToReceive 上移除。跟任务级入队函数处理过程一样。
(7)、如果刚刚从列表 xTasksWaitingToReceive 中移除的任务优先级比当前任务的优先级高,那么标记 pxHigherPriorityTaskWoken 为 pdTRUE,表示要进行任务切换。如果要进行任务切换的话就需要在退出此函数以后,退出中断服务函数之前进行一次任务切换。
(8)、如果队列上锁的话那就将队列成员变量 cTxLock 加一,表示进行了一次入队操作,在队列解锁(prvUnlockQueue())的时候会对其做相应的处理。
(9)、返回 pdPASS,表示入队完成。
(10)、如果队列满的话就直接返回 errQUEUE_FULL,表示队列满。
4 队列上锁和解锁
在上面讲解任务级通用入队函数和中断级通用入队函数的时候都提到了队列的上锁和解锁,队列的上锁和解锁是两个 API 函数:prvLockQueue()和 prvUnlockQueue()。首先来看一下队列上
锁函数 prvLockQueue(),此函数本质上就是一个宏,定义如下:
#define prvLockQueue( pxQueue ) \
taskENTER_CRITICAL(); \
{ \
if( ( pxQueue )->cRxLock == queueUNLOCKED ) \
{ \
( pxQueue )->cRxLock = queueLOCKED_UNMODIFIED; \
} \
if( ( pxQueue )->cTxLock == queueUNLOCKED ) \
{ \
( pxQueue )->cTxLock = queueLOCKED_UNMODIFIED; \
} \
} \
taskEXIT_CRITICAL()
prvLockQueue()函数很简单,就是将队列中的成员变量 cRxLock 和 cTxLock 设置为queueLOCKED_UNMODIFIED 就行了。
再来看一下队列的解锁函数 prvUnlockQueue(),函数如下:
static void prvUnlockQueue( Queue_t * const pxQueue )
{
//上锁计数器(cTxLock 和 cRxLock)记录了在队列上锁期间,入队或出队的数量,当队列
//上锁以后队列项是可以加入或者移除队列的,但是相应的列表不会更新。
taskENTER_CRITICAL();
{
//处理 cTxLock。
int8_t cTxLock = pxQueue->cTxLock;
while( cTxLock > queueLOCKED_UNMODIFIED ) (1)
{
/**************************************************************************/
/ **************************省略掉与队列集相关代码**************************/
/**************************************************************************/
{
//将任务从事件列表中移除
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == \ (2)
pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->\ (3)
xTasksWaitingToReceive ) ) != pdFALSE )
{
//从列表中移除的任务优先级比当前任务的优先级高,因此要
//进行任务切换。
vTaskMissedYield(); (4)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
break;
}
}
--cTxLock; (5)
}
pxQueue->cTxLock = queueUNLOCKED; (6)
}
taskEXIT_CRITICAL();
//处理 cRxLock。
taskENTER_CRITICAL();
{
int8_t cRxLock = pxQueue->cRxLock;
while( cRxLock > queueLOCKED_UNMODIFIED ) (7)
{
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) !=\
pdFALSE )
{
vTaskMissedYield();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
--cRxLock;
}
else
{
break;
}
}
pxQueue->cRxLock = queueUNLOCKED;
}
taskEXIT_CRITICAL();
}
(1)、判断是否有中断向队列发送了消息,在 13.2.3 小节讲解中断级通用入队函数的时候说了,如果当队列上锁的话那么向队列发送消息成功以后会将入队计数器 cTxLock 加一。
(2)、判断列表 xTasksWaitingToReceive 是否为空,如果不为空的话就要将相应的任务从列表中移除。
(3)、将任务从列表 xTasksWaitingToReceive 中移除。
(4)、如果刚刚从列表 xTasksWaitingToReceive 中移除的任务优先级比当前任务的优先级高,那么就要标记需要进行任务切换。这里调用函数 vTaskMissedYield()来完成此任务,函数vTaskMissedYield()只是简单的将全局变量 xYieldPending 设置为 pdTRUE。那么真正的任务切换是在哪里完成的呢?在时钟节拍处理函数
xTaskIncrementTick()中,此函数会判断 xYieldPending
的值,从而决定是否进行任务切换,具体内容可以参考 12.2 小节。
(5)、每处理完一条就将 cTxLock 减一,直到处理完所有的。
(6)、当处理完以后标记 cTxLock 为 queueUNLOCKED,也就说 cTxLock 是没有上锁的了。
(7)、处理完 cTxLock 以后接下来就要处理 xRxLock 了,处理过程和 xTxLock 很类似。
5 从队列 读取 消息
有入队就有出队,出队就是从队列中获取队列项(消息),FreeRTOS 中出队函数如表 13.6.1.1所示:
1 、函数 xQueueReceive()
此函数用于在任务中从队列中读取一条(请求)消息,读取成功以后就会将队列中的这条数据删除,此函数的本质是一个宏,真正执行的函数是 xQueueGenericReceive()。此函数在读取消息的时候是采用拷贝方式的,所以用户需要提供一个数组或缓冲区来保存读取到的数据,所读取的数据长度是创建队列的时候所设定的每个队列项目的长度,函数原型如下:
BaseType_t xQueueReceive(QueueHandle_t xQueue,
void * pvBuffer,
TickType_t xTicksToWait);
参数:
xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄。
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。
xTicksToWait: 阻塞时间,此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最大时间。如果为 0 的话当队列空的时候就立即返回;当为 portMAX_DELAY的 话 就 会 一 直 等 待 , 直 到 队 列 有 数 据 , 也 就 是 死 等 , 但 是 宏INCLUDE_vTaskSuspend
必须为 1。
返回值:
pdTRUE: 从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
2 、函数 xQueuePeek()
此函数用于从队列读取一条(请求)消息,只能用在任务中!此函数在读取成功以后不会将消息删除,此函数是一个宏,真正执行的函数是 xQueueGenericReceive()。此函数在读取消息的时候是采用拷贝方式的,所以用户需要提供一个数组或缓冲区来保存读取到的数据,所读取的数据长度是创建队列的时候所设定的每个队列项目的长度,函数原型如下:
BaseType_t xQueuePeek(QueueHandle_t xQueue,
void * pvBuffer,
TickType_t xTicksToWait);
参数:
xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄。
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。
xTicksToWait: 阻塞时间,此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最大时间。如果为 0 的话当队列空的时候就立即返回;当为 portMAX_DELAY的 话 就 会 一 直 等 待 , 直 到 队 列 有 数 据 , 也 就 是 死 等 , 但 是 宏INCLUDE_vTaskSuspend
必须为 1。
返回值:
pdTRUE: 从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
3、数 函数 xQueueGenericReceive()
不 管 是 函 数 xQueueReceive() 还 是 xQueuePeek() , 最 终 都 是 调 用 的 函 数xQueueGenericReceive(),此函数是真正干事的,函数原型如下:
BaseType_t xQueueGenericReceive(QueueHandle_t xQueue,
void* pvBuffer,
TickType_t xTicksToWait
BaseType_t xJustPeek)
参数:
xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄。
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。
xTicksToWait: 阻塞时间,此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最大时间。如果为 0 的话当队列空的时候就立即返回;当为 portMAX_DELAY的 话 就 会 一 直 等 待 , 直 到 队 列 有 数 据 , 也 就 是 死 等 , 但 是 宏INCLUDE_vTaskSuspend
必须为 1。
xJustPeek : 标记当读取成功以后是否删除掉队列项,当为 pdTRUE 的时候就不用删除,也就是说你后面再调用函数 xQueueReceive()获取到的队列项是一样的。当为
pdFALSE 的时候就会删除掉这个队列项。
返回值:
pdTRUE: 从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
4 、函数 xQueueReceiveFromISR()
此函数是 xQueueReceive()的中断版本,用于在中断服务函数中从队列中读取(请求)一条消息,读取成功以后就会将队列中的这条数据删除。此函数在读取消息的时候是采用拷贝方式的,
所以需要用户提供一个数组或缓冲区来保存读取到的数据,所读取的数据长度是创建队列的时候所设定的每个队列项目的长度,函数原型如下:
BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue,
void* pvBuffer,
BaseType_t * pxTaskWoken);
参数:
xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄。
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。
pxTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值是由函数来设置的,用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdTRUE: 从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
5 、函数 xQueuePeekFromISR()
此函数是 xQueuePeek()的中断版本,此函数在读取成功以后不会将消息删除,此函数原型如下:
BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue,
void * pvBuffer)
参数:
xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄。
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。
返回值:
pdTRUE: 从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
出队函数的具体过程和入队函数类似,具体的过程就不在详细的分析了,有兴趣的,大家自行对照着源码看一下就可以了。