FreeRTOS的学习系列文章目录
FreeRTOS的学习(一)——STM32上的移植问题
FreeRTOS的学习(二)——任务优先级问题
FreeRTOS的学习(三)——中断机制
FreeRTOS的学习(四)——列表
FreeRTOS的学习(五)——系统延时
FreeRTOS的学习(六)——系统时钟
FreeRTOS的学习(七)——1.队列概念
FreeRTOS的学习(七)——2.队列入队源码分析
FreeRTOS的学习(七)——3.队列出队源码分析
FreeRTOS的学习(八)——1.二值信号量
FreeRTOS的学习(八)——2.计数型信号量
FreeRTOS的学习(八)——3.优先级翻转问题
FreeRTOS的学习(八)——4.互斥信号量
FreeRTOS的学习(九)——软件定时器
FreeRTOS的学习(十)——事件标志组
FreeRTOS的学习(十一)——任务通知
目录
前言
队列在FreeRTOS中起到比较重要的作用,主要用于任务之间消息的传递,取代了裸机时代中的全局变量交互功能。队列的机制实现了任务与任务、任务与中断之间的消息传递。
1 队列的结构体
新版的队列结构体与旧版的结构体的表达方式存在一些区别,也多了一些内容,主要表现在:利用QueuePointers_t(队列指针结构体)和SemaphoreData_t(信号量结构体)这两个结构体分别去包含了如下成员。
typedef struct QueuePointers
{
//指向队列存储区最后一个字节
int8_t * pcTail;
//当用作队列的时候指向最后一个出队的队列项首地址
int8_t * pcReadFrom;
} QueuePointers_t;
typedef struct SemaphoreData
{
//持有互斥量的任务的句柄。
TaskHandle_t xMutexHolder;
//当用作递归互斥量的时候用来记录递归互斥量被调用的次数。
UBaseType_t uxRecursiveCallCount;
} SemaphoreData_t;
队列结构体的声明如下,代码已经做了注释说明:
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;
//当队列上锁以后用来统计从队列中接收到的队列项数量,也就是出队的队列项数量,当队列没有上锁的话此字段为 queueUNLOCKED
volatile int8_t cRxLock;
//当队列上锁以后用来统计发送到队列中的队列项数量,也就是入队的队列项数量,当队列没有上锁的话此字段为 queueUNLOCKED
volatile int8_t cTxLock;
//如果使用静态存储的话此字段设置为 pdTURE。
#if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated;
#endif
//队列集相关宏
#if ( configUSE_QUEUE_SETS == 1 )
struct QueueDefinition * pxQueueSetContainer;
#endif
//跟踪调试相关宏
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxQueueNumber;
uint8_t ucQueueType;
#endif
} xQUEUE;
2 队列的收发函数
队列作用实质上就是对于变量的接收和发送,因此,本文对于代码的解析主要针对通用的接收和发送函数,更多的队列用的解析将会在后面的文章中给出。另外值得说明的是,接收和发送程序也分为任务使用和中断使用的。中断使用的话会多一些现场保护等的中断专用功能。
另外发送函数可以发送到队列的最前面,最后面或者直接覆写(此功能常用于向那些长度为 1 的队列发送消息)队列中的值。
//任务级发送
#define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_FRONT )
#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
//默认就是xQueueSendBack,也就是入队后是放在最后面的
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
#define xQueueOverwrite( xQueue, pvItemToQueue ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), 0, queueOVERWRITE )
//中断级发送
#define xQueueSendToFrontFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) \
xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_FRONT )
#define xQueueSendToBackFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) \
xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )
#define xQueueOverwriteFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) \
xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueOVERWRITE )
#define xQueueSendFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) \
xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )
//任务级发送
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait ) PRIVILEGED_FUNCTION;
//从队列中读取消息,并且读取之后不删除队列项
BaseType_t xQueuePeek( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait ) PRIVILEGED_FUNCTION;
//中断级接收
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue,
void * const pvBuffer,
BaseType_t * const pxHigherPriorityTaskWoken ) PRIVILEGED_FUNCTION;
BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue,
void * const pvBuffer ) PRIVILEGED_FUNCTION;
2.1 xQueueGenericSend
队列所有的发送函数,包括前向入队、后向入队以及覆写都是基于任务级通用发送(入队)函数(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:覆写入队。
通过选择xCopyPosition的不同数值实现不同的入队方式。
返回值:
pdPASS,向队列发送消息成功!
errQUEUE_FULL,队列已满,消息发送失败。
2.1.1 整体函数逻辑讲解
接下来开始分析入队函数的具体过程,下面给出基本的代码(省略冗余的东西,默认内存采用动态方式):
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 = xQueue;
for( ; ; )
{
taskENTER_CRITICAL();
{
//检查一下队列是不是满的,如果是满的话肯定不能发送的。当队列未满或者是覆写入队的话就可以将消息入队了
if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
//如果队列未满,或者属于覆写功能
{ /*省略语句内函数*/ }
else
//队列已满且不是覆写功能
{ /*省略语句内函数*/ }
}
taskEXIT_CRITICAL();
//退出临界区后,中断和其他任务可以发送和接收队列信息。
vTaskSuspendAll();//挂起任务调度器
prvLockQueue( pxQueue );//队列上锁
//更新时间壮态,检查是否有超时产生
if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
{
if( prvIsQueueFull( pxQueue ) != pdFALSE )
{
traceBLOCKING_ON_QUEUE_SEND( pxQueue );
vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait );
//队列解锁
prvUnlockQueue( pxQueue );
/* Resuming the scheduler will move tasks from the pending
* ready list into the ready list - so it is feasible that this
* task is already in the ready list before it yields - in which
* case the yield will not cause a context switch unless there
* is also a higher priority task in the pending ready list. */
if( xTaskResumeAll() == pdFALSE )
{
portYIELD_WITHIN_API();
}
}
else
{
//重试一次
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
}
}
else
{
//超时产生
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
traceQUEUE_SEND_FAILED( pxQueue );
return errQUEUE_FULL;
}
} /*lint -restore */
}
根据上面的函数可以发现,其主要流程如下:
- 进入一个无条件的for循环,虽说是个死循环,但是在任务调度的时候可以发生中断,从而跳出死循环。
- 入队的操作全部是在代码临界区完成的,因此其不受系统中断和任务的干扰。
- 要向队列发送数据,需要先检查一下队列是不是满的,如果是满的话肯定不能发送的。当队列未满或者是覆写入队的话就可以将消息入队了。
- 如果队列满了+不能覆写+xTicksToWait不为0,那么就会执行下面的任务调度器挂起等操作(注意return会终止循环)。因为,如果进了一开始的if语句,则在完成入队后会进行任务切换。
- 调用vTaskSuspendAll挂起任务调度器,并调用函数 prvLockQueue给队列上锁。
- 调用函数 xTaskCheckForTimeOut更新超时结构体变量 xTimeOut,并且检查阻塞时间是否到了。超时结构体是当队列满了之后,且阻塞时间不为0时才进行初始化的。主要是用来记录初始化时的系统时钟节拍计数值,用来记录阻塞时间的。
- 如果阻塞时间还没到,那就检查队列是否还是满的。
- 经过6和7得出阻塞时间没到,而且队列依旧是满的,那就调用函数vTaskPlaceOnEventList将任务添加到队列的 xTasksWaitingToSend 列表和延时列表中,并且将任务从就绪列表中移除。注意!如果阻塞时间是 portMAX_DELAY 并且宏INCLUDE_vTaskSuspend 为 1 的话,函数 vTaskPlaceOnEventList会将任务添加到列表xSuspendedTaskList 上。
- 操作完成,调用函数 prvUnlockQueue解锁队列。
- 调用函数 xTaskResumeAll恢复任务调度器。判断其返回值,是否要进行任务调度。
- 如果阻塞时间没到,并且对列是有空闲的,则重试一次,也就是再走一次for循环。
- 相比于第6步,如果阻塞时间已经到了,那么任务就不用添加到那些列表中,直接解锁队列,恢复任务调度器。
- 返回错误errQUEUE_FULL,表示队列已满。
2.1.2 入队的局部函数讲解
接下来给出省略的判断语句内部函数如下:
//查询队列现在是否还有剩余存储空间,如果采用覆写方式入队的话那就不用在乎队列是不是满的。
if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
{
traceQUEUE_SEND( pxQueue );
#if ( configUSE_QUEUE_SETS == 1 )
{
/* 省略掉与队列集相关代码 */
/* 讲解时默认不用队列集的功能 */
}
#else /* configUSE_QUEUE_SETS */
{
xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );
/* If there was a task waiting for data to arrive on the
* queue then unblock it now. */
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
{
/* The unblocked task has a priority higher than
* our own so yield immediately. Yes it is ok to do
* this from within the critical section - the kernel
* takes care of that. */
queueYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else if( xYieldRequired != pdFALSE )
{
/* This path is a special case that will only get
* executed if the task was holding multiple mutexes and
* the mutexes were given back in an order that is
* different to that in which they were taken. */
queueYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_QUEUE_SETS */
taskEXIT_CRITICAL();
return pdPASS;
}
else
{
/* 与上面的if语句,没有队列集的功能一致 */
}
}
在临界区中,进行了数据入队的操作,其中关于队列集的内容还没有接触,暂时不谈了,大概功能应该就是整合多个队列为一个集合吧,意义不是特别大目前来看。
具体步骤如下:
- 调用函数 prvCopyDataToQueue将消息拷贝到队列中。前面说了入队分为后向入队、前向入队和覆写入队,他们的具体实现就是在函数 prvCopyDataToQueue中完成的。如果选择后向入队 queueSEND_TO_BACK 的话就将消息拷贝到队列结构体成员 pcWriteTo 所指向的队列项,拷贝成功以后 pcWriteTo 增加 uxItemSize 个字节,指向下一个队列项目。当选择前向入队queueSEND_TO_FRONT或者queueOVERWRITE的话就将消息拷贝到pcReadFrom所指向的队列项目,同样的需要调整 pcReadFrom的位置。当向队列写入一个消息以后队列中统计当前消息数量的成员 uxMessagesWaiting 就会加一,但是选择覆写入队 queueOVERWRITE 的话还会将 uxMessagesWaiting 减一,这样一减一加相当于队列当前消息数量没有变。
- 检查是否有任务由于请求队列消息而阻塞,阻塞的任务会挂在队列的xTasksWaitingToReceive 列表上。
- 有任务由于请求消息而阻塞,因为在第1步中已将向队列中发送了一条消息了,所以调用函数 xTaskRemoveFromEventList将阻塞的任务从列表 xTasksWaitingToReceive 上移除,并且把这个任务添加到就绪列表中,如果调度器挂起的话这些任务就会挂到列表 xPendingReadyList 上。当函数xTaskRemoveFromEventList返回值为 pdTRUE 的话,也就是取消阻塞的任务优先级比当前正在运行的任务优先级高需要进行任务切换。
- else if( xYieldRequired != pdFALSE)的判断语句是用来处理一种特殊情况。只有当任务持有多个互斥量,并且互斥量以与获取互斥量不同的顺序返回时,才会执行该路径。具体我现在也不太清除,需要后面深入学习互斥量的概念。
- 至此入队成功,返回pdPASS,标记入队成功。
- 1~5处理的都是较为理想的情况,也就是队列未满或者覆写入队。如果队列已满,则需要根据判断阻塞时间,也就是在阻塞判断是否发送的函数要等待队列有空了之后再发。等待的过程中发送的任务进入阻塞态。
- 如果阻塞时间为0,则即使队列已满,也立即离开,也就是当前的数据不要了,准备发送下一个数据。这种情况下会返回errQUEUE_FULL。
- 如果阻塞时间不为0,并且进入该函数时入栈的xEntryTimeSet为pdFALSE。则调用函数vTaskInternalSetTimeOutState初始化一次超时结构体变量,并将xEntryTimeSet赋值为pdTURE,防止多次初始化。其中超时结构体就是记录当前的系统时钟节拍计数器的值xTickCount 和溢出次数xNumOfOverflows。
- 如果阻塞时间不为0,且超时结构体已经初始化,则无动作,准备进入接下来的满队列情况下的阻塞处理。
2.2 队列上锁和解锁
在上面讲解任务级通用入队函数时提到了队列的上锁和解锁,队列的上锁和解锁是两个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()
上锁的过程实际上就是设置队列的成员变量,判断发送和接收的锁是否上好了,上锁之后,队列项是可以从队列中加入或者移除的,但是相应的列表不会更新。
接下来看一下解锁的过程(省略队列集相关的函数):
static void prvUnlockQueue( Queue_t * const pxQueue )
{
/* THIS FUNCTION MUST BE CALLED WITH THE SCHEDULER SUSPENDED. */
//上锁计数器(cTxLock 和 cRxLock)记录了在队列上锁期间,入队或出队的数量,当队列
//上锁以后队列项是可以从队列中加入或者移除的,但是相应的列表不会更新。
taskENTER_CRITICAL();
{
int8_t cTxLock = pxQueue->cTxLock;
while( cTxLock > queueLOCKED_UNMODIFIED )
{
#if ( configUSE_QUEUE_SETS == 1 )
{
/* 省略掉与队列集相关代码 */
/* 讲解时默认不用队列集的功能 */
}
#else /* configUSE_QUEUE_SETS */
{
}
#endif /* configUSE_QUEUE_SETS */
--cTxLock;
}
pxQueue->cTxLock = queueUNLOCKED;
}
taskEXIT_CRITICAL();
/* Do the same for the Rx lock. */
taskENTER_CRITICAL();
{
int8_t cRxLock = pxQueue->cRxLock;
while( cRxLock > queueLOCKED_UNMODIFIED )
{
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();
}
- 解锁函数必须在任务调度器挂起时才能使用。
- 只有当队列上锁过程中,有新的数据入队时才会执行while( cTxLock > queueLOCKED_UNMODIFIED )。在中断级通用入队函数中,如果当队列上锁时向队列发送消息成功以后会将入队计数器cTXLock加一。
- 判断列表xTasksWaitingToReceive是否为空,如果不为空的话,就要将相应阻塞时间达到的任务从列表中移除。
- 如果刚刚从列表 xTasksWaitingToReceive 中移除的任务优先级比当前任务的优先级高,那么就要标记需要进行任务切换。这里调用函数 vTaskMissedYield来完成此任务,函数vTaskMissedYield只是简单的将全局变量 xYieldPending 设置为 pdTRUE。那么真正的任务切换是在哪里完成的呢?在时钟节拍处理函数 xTaskIncrementTick中,此函数会判断 xYieldPending的值,从而决定是否进行任务切换。
- 每处理完一种上述的情况就将cTxLock 减一,直到处理完所有的情况。
- 当处理完以后就标记cTxLock 为queueUNLOCKED,也就是解锁。
- 处理完cTxLock以后接下来就要处理xRxLock了,处理过程与xTLock 类似,这里就不赘述了。
3 写在后面
队列这部分的东西是在有点多,最近精力也不如之前,目前先发一部分入队的内容……