FreeRTOS源码解析集合(全网最详细)
手把手教你FreeRTOS源码解析(一)——内存管理
手把手教你FreeRTOS源码详解(二)——任务管理
手把手教你FreeRTOS源码详解(三)——队列
手把手教你FreeRTOS源码详解(四)——信号量、互斥量、递归互斥量
队列与任务相同,同样也有一个结构体用来描述队列。删除部分宏定义后如下。
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*
} xQUEUE;
/*更名*/
typedef xQUEUE Queue_t;
创建普通的消息队列、创建队列集、创建互斥信号量等,最后都是调用通用的队列创建函数xQueueGenericCreate。
xQueueGenericCreate( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
const uint8_t ucQueueType )
其中:
uxQueueLength为队列长度
uxItemSize为队列项大小
ucQueueType 队列的类型,共有6种类型如下
queueQUEUE_TYPE_BASE
普通的消息队列
queueQUEUE_TYPE_SET 队列集
queueQUEUE_TYPE_MUTEX 互斥信号量
queueQUEUE_TYPE_COUNTING_SEMAPHORE 计数型信号量
queueQUEUE_TYPE_BINARY_SEMAPHORE 二值信号量
queueQUEUE_TYPE_RECURSIVE_MUTEX 递归互斥信号量
if( uxItemSize == ( UBaseType_t ) 0 )
{
/* 队列项大小为0,因此不需要存储区 */
xQueueSizeInBytes = ( size_t ) 0;
}
else
{
/* 分配足够的存储区,以便于随时能够保存所有的消息
存储区=队列长度*队列项大小*/
xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
}
首先确定队列存储区的大小,如果队列项的大小为0,则队列不需要存储区,xQueueSizeInBytes 赋值为0;若队列项的大小不为0,则存储区的大小为队列长度*队列项大小
pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );
计算队列总共需要的内存空间,需在存储区基础上再加上结构体Queue_t 大小
pucQueueStorage = ( ( uint8_t * ) pxNewQueue ) + sizeof( Queue_t );
pucQueueStorage 指向队列存储区的起始地址,在所申请的总内存空间的起始位置上偏移一个结构体Queue_t 大小
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
pxNewQueue->ucStaticallyAllocated = pdFALSE;
}
如果队列是动态创建的,则将其ucStaticallyAllocated标志位置为pdFALSE 便于后续删除
prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );
初始化队列
if( uxItemSize == ( UBaseType_t ) 0 )
{
pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;
}
else
{
pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage;
}
首先初始化队列存储区的起始地址,若队列长度为0(队列没有存储区),则指向队列的起始地址,若队列长度不为0,指向队列存储区的起始地址
pxNewQueue->uxLength = uxQueueLength;
初始化队列长度
pxNewQueue->uxItemSize = uxItemSize;
初始化队列每项大小
( void ) xQueueGenericReset( pxNewQueue, pdTRUE );
复位队列
我们首先来看一下xQueueGenericReset的参数
BaseType_t xQueueGenericReset( QueueHandle_t xQueue,
BaseType_t xNewQueue )
xQueue:需要复位或者初始化队列的句柄
xNewQueue :为pdFALSE,表明队列不是第一次初始化,只复位队列即可
为pdTRUE,表明队列是第一次初始化
taskENTER_CRITICAL();
首先进入临界区,防止队列初始化、复位过程被打断
pxQueue->pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->uxItemSize );
pcTail指向队列存储区的尾部
pxQueue->uxMessagesWaiting = ( UBaseType_t ) 0U;
队列刚创建好时,队列当前项数为0,即消息数为0
pxQueue->pcWriteTo = pxQueue->pcHead;
pcWriteTo指向队列存储区下一块空闲区域,初始创建队列时,下一块空闲区域即为pcHead
pxQueue->u.pcReadFrom = pxQueue->pcHead + ( ( pxQueue->uxLength - ( UBaseType_t ) 1U ) * pxQueue->uxItemSize );
pcReadFrom指向队列最后一项出队时的首地址
pxQueue->cRxLock = queueUNLOCKED;
pxQueue->cTxLock = queueUNLOCKED;
队列未上锁,cRxLock,cTxLock均为queueUNLOCKED
if( xNewQueue == pdFALSE )
{
/* 队列复位后为空,出队阻塞的任务仍然保存阻塞状态,但入队阻塞的任务不再阻塞,应该从对应的列表中删除*/
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
{
queueYIELD_IF_USING_PREEMPTION();
}
else
{
/* 初始化入队阻塞、出队阻塞列表 */
vListInitialise( &( pxQueue->xTasksWaitingToSend ) );
vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );
}
当xNewQueue为pdTRUE时,即该队列为第一次创建,因此需要初始化入队阻塞列表和出队阻塞列表
当xNewQueue为pdFALSE时,该队列已经初始化过了,只需要进行复位,队列复位后为空,出队阻塞的任务仍然保存阻塞状态,但入队阻塞的任务不再阻塞,应该从对应的列表中删除
普通的消息队列复位函数xQueueReset最终调用的函数仍是xQueueGenericReset,如下:
#define xQueueReset( xQueue ) xQueueGenericReset( xQueue, pdFALSE )
FreeRtos的队列有很多入队函数,但是这些入队函数最终调用的都是通用入队函数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:覆写入队
taskENTER_CRITICAL();
首先进入临界区
if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
判断队列是否还有剩余存储空间,如果采用覆写就不用在乎是否还有存储空间
xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );
若队列还有剩余存储空间,将入队消息复制到队列中
此处我们不考虑使用队列集的情况,将队列集部分的宏定义删除
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
消息入队以后,先检查是否有任务因等待读取消息而阻塞
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
{
/* 解除阻塞的任务具有更高优先级,因此需要进行一次任务切换 */
queueYIELD_IF_USING_PREEMPTION();
}
如果有任务因等待读取消息而阻塞,则将该任务从阻塞列表中移除,因解除阻塞的任务具有更高优先级,因此需要进行一次任务切换
若队列已满,没有剩余的存储空间:
if( xTicksToWait == ( TickType_t ) 0 )
{
taskEXIT_CRITICAL();
return errQUEUE_FULL;
}
若不阻塞,则直接退出临界区,并返回errQUEUE_FULL
else if( xEntryTimeSet == pdFALSE )
{
vTaskSetTimeOutState( &xTimeOut );
xEntryTimeSet = pdTRUE;
}
若阻塞时间不为0,则初始化超时结构体,并将超时标志位xEntryTimeSet 设为pdTRUE,表明超时结构体已经初始化了
taskEXIT_CRITICAL();
退出临界区
vTaskSuspendAll();
prvLockQueue( pxQueue );
挂起任务调度器,并将队列上锁
if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
更新超时状态,并判断时间是否已到
if( prvIsQueueFull( pxQueue ) != pdFALSE )
如果超时时间未到,判断队列是否已满
vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait );
prvUnlockQueue( pxQueue );
if( xTaskResumeAll() == pdFALSE )
{
portYIELD_WITHIN_API();
}
如果超时时间未到,且队列已满,则将入队任务置于对应的阻塞列表中,然后恢复任务调度器
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
如果超时时间未到,队列不满,则解锁队列,恢复任务调度器,重新执行通用入队任务一次
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
return errQUEUE_FULL;
若超时时间已到,则恢复任务调度器,返回errQUEUE_FULL,表明入队失败
uxMessagesWaiting = pxQueue->uxMessagesWaiting;
首先获取当前队列中的消息数目
一个队列初始化完成后,大致如上图所示;数据复制采用的是c/c++中的函数memcpy,其原型如下:
void *memcpy(void *destin, void *source, unsigned n);
其作用是以source指向的地址为起点,将连续的n个字节数据,复制到以destin指向的地址为起点的内存中,注意n的单位是字节
我们去掉互斥锁相关宏定义
else if( xPosition == queueSEND_TO_BACK )
采用后向入队方式:
( void ) memcpy( ( void * ) pxQueue->pcWriteTo, pvItemToQueue, ( size_t ) pxQueue->uxItemSize );
队列首次初始化完成后,第一个消息的入队起始地址为pcHead(pcWriteTo),将消息pvItemToQueue复制到对应的区域
pxQueue->pcWriteTo += pxQueue->uxItemSize;
pcWriteTo指向下一个消息入队的起始地址(下一块空闲区域)
if( pxQueue->pcWriteTo >= pxQueue->pcTail ) /*lint !e946 MISRA exception justified as comparison of pointers is the cleanest solution. */
{
pxQueue->pcWriteTo = pxQueue->pcHead;
}
如果消息已经写至队尾pcTail ,则pcWriteTo 重新指向队列头部pcHead
采用前向入队方式:
( void ) memcpy( ( void * ) pxQueue->u.pcReadFrom, pvItemToQueue, ( size_t ) pxQueue->uxItemSize );
第一个消息的入队起始地址为pcReadFrom,将消息pvItemToQueue复制到对应的区域
pxQueue->u.pcReadFrom -= pxQueue->uxItemSize;
pcReadFrom 指向下一个消息入队的起始地址
if( pxQueue->u.pcReadFrom < pxQueue->pcHead )
{
pxQueue->u.pcReadFrom = ( pxQueue->pcTail - pxQueue->uxItemSize );
}
如果消息已经写至队首pcHead ,则pcReadFrom 重新指向最后一个出队的队列项首地址
采用覆写入队方式
需要注意的是采用覆写入队的队列长度必须为1
if( xPosition == queueOVERWRITE )
{
if( uxMessagesWaiting > ( UBaseType_t ) 0 )
{
--uxMessagesWaiting;
}
}
如果采用覆写入队,直接将队列消息数目减1,在任务结尾会重新将队列消息数目加1,这样队列中的消息数目会一直为0,队列不会进行阻塞,在下一次入队时会直接将前面的数据进行覆盖
pxQueue->uxMessagesWaiting = uxMessagesWaiting + 1;
将队列消息数目增1
我们来看一下常用的几种入队函数的封装:
xQueueSend和xQueueSendToBack:
xQueueSend和xQueueSendToBack的封装完全一样:
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
xQueueSendToFront:
#define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_FRONT )
xQueueOverwrite:
#define xQueueOverwrite( xQueue, pvItemToQueue ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), 0, queueOVERWRITE )
上述几种封装都类似,仅仅对通用入队函数xQueueGenericSend的参数xCopyPosition进行了不同选择
taskENTER_CRITICAL();
首先进入临界区
const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;
获取队列中的消息数目,保存至uxMessagesWaiting
if( uxMessagesWaiting > ( UBaseType_t ) 0 )
首先判断队列中是否有消息可读取,若队列中的消息不为空:
pcOriginalReadPosition = pxQueue->u.pcReadFrom;
记录当前读取消息的地址位置,用于实现“偷看”,即只读取消息,不删除消息
prvCopyDataFromQueue( pxQueue, pvBuffer );
将需要读取的消息复制至缓存区pvBuffer
如果消息出队,不是“偷看消息”:
pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;
消息出队,消息数减一
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
/*将任务从入队阻塞列表中移除*/
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
{
queueYIELD_IF_USING_PREEMPTION();
}
}
消息出队后,队列就不满了,一些在入队阻塞列表中的任务就可以解除阻塞状态了,上述代码即判断入队阻塞列表是否为空,不为空则将任务从阻塞列表中移除,再进行一次任务切换
“偷看”消息:
pxQueue->u.pcReadFrom = pcOriginalReadPosition;
重新赋值下次读取消息的地址位置,这样读取消息后而不会删除队列消息
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
{
/* The task waiting has a higher priority than this task. */
queueYIELD_IF_USING_PREEMPTION();
}
}
判断是否有任务因等待消息而阻塞,如果有,则将任务从出队阻塞列表中删除,并进行一次任务切换
队列中的消息为空:
if( xTicksToWait == ( TickType_t ) 0 )
{
taskEXIT_CRITICAL();
traceQUEUE_RECEIVE_FAILED( pxQueue );
return errQUEUE_EMPTY;
}
未设置阻塞时间,则直接退出临界区,返回errQUEUE_EMPTY
else if( xEntryTimeSet == pdFALSE )
{
vTaskSetTimeOutState( &xTimeOut );
xEntryTimeSet = pdTRUE;
}
设置了阻塞时间,即阻塞时间不为0,初始化超时结构体,并将超时标志位xEntryTimeSet 置为pdTRUE
taskEXIT_CRITICAL();
vTaskSuspendAll();
prvLockQueue( pxQueue );
退出临界区,挂起任务调度器,将队列上锁
if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
更新超时状态,并判断超时时间是否结束
if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
判断队列是否为空
vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );
prvUnlockQueue( pxQueue );
若超时时间未结束且队列为空,则直接将出队任务挂载至出队阻塞列表,再重新将任务解锁
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
若超时时间未结束,队列不为空,则将任务解锁,恢复任务调度器,重新执行一次出队任务
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
{
traceQUEUE_RECEIVE_FAILED( pxQueue );
return errQUEUE_EMPTY;
}
若超时时间结束,则将队列解锁,恢复任务调度器,并判断队列是否为空,若队列此时仍为空,表明无消息可读,返回errQUEUE_EMPTY;若队列不为空,则再次执行该函数进行消息读取
队列通常采用的是先进先出(FIFO)缓冲机制,当队列入队采用后向入队时(往队列发送数据时,发送到队列尾部,从队列读取数据时,从队列头部读取)
假设队列采用后向入队,入队了两个消息,项目1先入队,项目2随后入队,则此时队列结构如下图所示(注意pcWriteTo和pcReadFrom指向)
按照出队函数的流程:
if( pxQueue->uxItemSize != ( UBaseType_t ) 0 )
首先判断队列项是否不为0
pxQueue->u.pcReadFrom += pxQueue->uxItemSize;
然后将pcReadFrom 向后偏移一个队列项大小,偏移后如下图所示:
if( pxQueue->u.pcReadFrom >= pxQueue->pcTail )
{
pxQueue->u.pcReadFrom = pxQueue->pcHead;
}
按上图所示pcReadFrom 指向了队列尾pcTail ,应重新将pcReadFrom 指向队列头pcHead,如下图所示:
( void ) memcpy( ( void * ) pvBuffer, ( void * ) pxQueue->u.pcReadFrom, ( size_t ) pxQueue->uxItemSize );
最后调用memcpy函数,将以pcReadFrom为起始地址,向后uxItemSize个字节的内容复制到缓冲区pvBuffer中,即实现了先入队的消息先出队FIFO
假设队列采用前向入队,入队了两个消息,则此时队列结构如下图所示(注意pcWriteTo和pcReadFrom指向)
其中第一个入队的消息为项目1,第二个入队的消息为项目2
同样,我们按照出队函数的流程:
pxQueue->u.pcReadFrom += pxQueue->uxItemSize;
然后将pcReadFrom 向后偏移一个队列项大小,偏移后如下图所示:
if( pxQueue->u.pcReadFrom >= pxQueue->pcTail )
{
pxQueue->u.pcReadFrom = pxQueue->pcHead;
}
判断是否到队列尾部pcTail,目前还未到队列尾部,忽略该段代码
( void ) memcpy( ( void * ) pvBuffer, ( void * ) pxQueue->u.pcReadFrom, ( size_t ) pxQueue->uxItemSize );
最后调用memcpy函数,将以pcReadFrom为起始地址,向后uxItemSize个字节的内容复制到缓冲区pvBuffer中,优先将后入队的项目2先读取,即实现了后入队先出队LIFO
最后我们来看一下两个常用出队函数的封装
xQueueReceive:
#define xQueueReceive( xQueue, pvBuffer, xTicksToWait ) xQueueGenericReceive( ( xQueue ), ( pvBuffer ), ( xTicksToWait ), pdFALSE )
该函数直接将“偷看”置为pdFALSE
xQueuePeek:
#define xQueuePeek( xQueue, pvBuffer, xTicksToWait ) xQueueGenericReceive( ( xQueue ), ( pvBuffer ), ( xTicksToWait ), pdTRUE )
相反,peek函数直接将“偷看”标志位置为pdTRUE