这节内容是信号量的基础,因为创建以及发送/等待信号量所调用的底层函数,就是创建/发送/接受消息队列时所用到的通用创建函数,这里先补充一下数据结构中关于队列的知识。
目录
1. 队列原理
1.1 顺序队列操作
1.2 循环队列操作
2.消息队列原理
2.1消息队列的构成
2.2 消息队列出入队原则
2.3 消息队列发送/接收消息原理
2.4 队列锁机制
3. 消息队列创建及删除
3.1 创建消息队列函数
3.1.1 xQueueGenericCreate通用任务创建函数
3.1.1.1 prvInitialiseNewQueue初始化新队列函数
3.1.1.2 xQueueGenericReset通用重置队列函数
3.2 消息队列删除队列函数vQueueDelete
4. 消息队列发送信息
4.1 消息队列发送消息函数xQueueSend
4.1.1 prvCopyDataToQueue拷贝数据到队列函数(这里拷贝数据的方式有两种都在前面 "2.2节" 介绍了)
4.1.2 xTaskCheckForTimeOut检查任务超时函数
4.1.3 xQueueGenericSend通用消息队列发送函数
5.1 消息队列接受信息(具体实现原理都与发送消息函数一致,所以这里只介绍里面有区别的函数)
5.1.1 prvCopyDataFromQueue消息队列接受消息函数
1. 队列原理
若不知道可作为了解,跳过也不影响理解消息队列的访问机制。
1.1 顺序队列操作
空队时:rear = front = 0;
入队操作:先将要入队的元素,存入rear指向的队列空间,之后rear+1向后移动一位;
出队操作:将front指向的队列空间中的元素移除,之后将front+1向后移动一位;
1.2 循环队列操作
循环队列是首尾相连的,如下所示:
队列指针移动的方式和顺序队列一样;
最开始时,队空的条件是“rear == front”,但若入队元素入队速度>出队元素出队速度,则很快就又会有“rear == front”情况发生,显然这种情况下是不对的,因为此时队列中其实充满了元素,所以这里我们就空一个空间出来不存数据,如果满足(rear+1)%Maxsize == front的情况,那队列就满了,就不能再入队了,此时只能用front指针将元素清出去,才可以继续入队;
队满条件:(rear+1)%Maxsize == front;
队空条件:front == rear;
队列中元素的个数: (rear - front +Maxsize)%Maxsize;
2.消息队列原理
2.1消息队列的构成
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; //队列上锁后,表出队数量
volatile int8_t cTxLock; //队列上锁后,表入队数量
}xQUEUE;
typedef xQUEUE Queue_t;
它在创建后即被初始化为下图所示结构:(包括指针指向的初始化)
其中消息队列中的指针指向如下所示:
pchead:指向消息队列起始位置;
pcTail:指向消息队列结束位置;
pcWriteTo:指向队列消息存储区下一个可用消息空间;(就是下一次入队的空间)
pcReadFrom:初始化时指向消息队列倒数第二个位置,指向出队消息空间的最后一个,读取消息时候是从 pcReadFrom 指向的空间读取消息内容;
2.2 消息队列出入队原则
队列默认初始化状态:
这里先讲解出队的方式(因为出队方式只有一个)
1. 消息队列出队方式
具体就是先将pcReadFrom指针+1,之后从pcReadFrom指向的空间中读出消息数据(在指向pcTail队尾时,将其指向pchead)
2. 消息队列入队方式
消息队列入队是从pcWriteTo或pcReadFrom指针指向的消息空间入队的,具体用哪个指针入队看入队的配置方式,下面将分别阐明这两种情况:
(1) 通过pcWriteTo指针入队(也是FIFO的方式):
从pcWriteTo指针指向的队列空间入队,在通过pcWriteTo指针存入数据后,pcWriteTo指针会指向下一个队列空间(也就是pcWriteTo+1),具体如下:之所以这种入队方式是FIFO的方式是因为当第一次调用读队列操作时,pcReadFrom会+1后再读取其指向空间的值,但当它指向队列尾巴时,其会被跳转到队列头,指向第一个存入消息的空间(之后的读取操作显然就是先进入的消息先被读出),具体如下所示:
(2) 通过pcReadFrom指针入队(也就是LIFO方式):
此时通过pcReadFrom指针入队,当调用入队操作时,pcReadFrom指针先将消息入队再将pcReadFrom指针的指向-1个消息空间,如下所示:
之所以为LIFO是因为消息出队时,会先将pcReadFrom指针+1后从其指向的消息空间中取出消息(这时第一个出队的数据就会是最后一个入队的数据)
2.3 消息队列发送/接收消息原理
首先这里假设 "任务A" 是向消息队列发消息的任务,"任务B"是从消息队列收消息的任务,下面将设想一个比较全面的状况,来将队列发送/接收原理说明清楚(下面将通过4个步骤来简单的叙述消息队列接受/发送消息的大致过程):
1. 任务B开始从队列接收消息,但初始时刻队列空间中没有消息(“接受消息阻塞”具体情况)
首先taskB从消息队列中开始接收消息,但此时消息队列中无消息可接收,这时系统就会根据用户设置的延时等待时间将taskB任务节点挂载到系统时基的延时列表中(此时任务被转为阻塞态,只有当阻塞时间结束/消息队列中来了消息,才会将B任务就绪),而taskB的事件节点将被挂载到队列的等待链表中(这是为了当有消息队列的send函数被调用时(有send调用就证明现在消息空间有消息了),此时就能通过B的事件节点访问到B的TCB任务控制块,从而将B任务就绪(之后运行到B任务时,B任务就会将消息读出),后面会详述)
2. A任务向消息队列发送一个消息(解除“消息队列接受阻塞”具体情况)
现在taskA向消息队列发送消息“1”,在taskA调用消息队列发送函数时,发送函数会查询当前消息队列的“接受等待链表”是否为空(若不为空则证明还有任务在等待消息),而此时B就在“接受等待链表”(假设现在taskB的阻塞时间还没有到),所以在taskA调用的发送函数就会将taskB从阻塞态重新就绪,并将其从“接受等待链表”中移除(表B任务已收到消息),之后的任务调度中,调度到B任务后,会将消息队列中的“1”消息读出来(一般来说读了之后消息它会出队,这个要看调用函数时的参数配置情况,后面会详述);
3. A任务向消息队列连续发送4个消息(发送阻塞情况)
经过前两次操作,现在消息队列又空了,这之后taskA向消息队列发送4个消息,那么在发完前3个后,消息队列就会被存满,这时taskA再想要发送第4个消息就会被挂载在“发送等待链表”之中并且被阻塞。
4. B任务再次开始从消息队列上接受消息(解除“消息队列发送阻塞”具体情况)
第3步操作将队列存满了并有一个发送消息被阻塞了,此时B调用从消息队列中接受消息函数,假定其先将消息3读出(具体读出顺序要看函数配置参数,这里只是假定)同时消息3会被出队(消息3被出队后消息队列就有空间存入发送的消息了),读出消息后在接受消息队列函数中会去访问此消息队列的“发送等待链表”(看其中有没有被阻塞的任务),而此时发送等待链表中taskA就被挂载在内,这时接受消息函数就会将A任务就绪(因为前面已经出队过一个消息了,所以现在一定有消息空间去存A的消息),之后系统调度到A任务后,taskA任务就会又开始发送数据到消息队列,而此时消息4就会被成功发送到消息队列中;
2.4 队列锁机制
这里先说明一下任务从消息发送阻塞到阻塞恢复的过程:
如果现在我有个任务因为调用消息发送函数时,消息队列满了,此时调用消息队列的任务就会被置为阻塞态,直到其在规定时间内等到消息队列的空位后就会被置就绪,然后发送消息到消息队列。
由上面的过程现在引入两个问题:
1. 消息队列会在上面过程中的什么时候上锁?
在上面例子中任务从就绪态转换为阻塞态时,会进行一系列的链表操作(包括超时检测),在进行链表操作期间队列就会被上锁,保证此时使用此队列的那个任务它的链表操作不会被影响;
具体代码片段如下:(从"通用发送消息到队列函数"截取)
vTaskSuspendAll(); //队列上锁 prvLockQueue( pxQueue ); //超时判断 if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ) { //队列满判断 if( prvIsQueueFull( pxQueue ) != pdFALSE ) { //将任务事件节点插入到发送等待链表,并将任务的普通节点插入到系统的阻塞链表 vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait ); //队列解锁 prvUnlockQueue( pxQueue );
2. 消息队列上锁是为了什么?
首先要知道,队列锁是在 "消息在中断中发送/接受" 函数中才会被用到的,所以中断中同样会有对消息队列的一些操作。
若我当前有一个消息队列是满的,我现在在线程环境(任务环境)中调用消息队列发送函数时,那么我调用消息队列的那个任务就会被阻塞(阻塞时具体的链表操作在上面2.3有说明),若我这些链表操作没有队列锁的保护,此时来了一个在中断中接受此消息队列的操作,打断了我现有的链表操作,此时就可能会发生一下几种情况:
(1)运气好的情况下,调用消息队列操作的任务刚好做完链表操作,被阻塞且事件链表挂载在了消息队列的发送阻塞链表,此时中断中接受消息队列的函数就会将调用消息队列的任务马上恢复;(此时没错过操作)
(2) 若现在运气不那么好,任务一个链表操作都没做完,有一个中断就过来接收消息了,那么现在任务就可能会错过了一个发送消息的机会(因为中断中接收消息函数,它会先去判断当前队列是否有消息的,有消息的话它会先将消息读出,之后再去判断当前队列有无发送延时的任务,有就将其恢复(当前消息队列中已有空位),所以任务在没有将链表插入到阻塞链表时,会错过一个从阻塞态恢复到就绪态发送消息的机会);
为解决上面的两个问题,消息队列引入队列锁,当任务在线程环境中调用消息队列"发送/接收"函数时,其中的超时检测及链表操作就会被上锁,上锁时若来了个中断有从消息队列中读数据的操作,它就会先从消息队列中将数据读走(但不进行恢复任务的链表操作),而后将 "RxLock+1" 接收上锁+1(表我在你上锁的期间内从消息队列中读走了多少个数据),之后在任务做完链表及超时检测操作后,就会将消息队列解锁,解锁时就会判断RxLock的大小,并从发送阻塞中将RxLock个挂载在发送延时链表的阻塞任务恢复就绪(若阻塞任务的数量小于RxLock个,那么就将它们全部恢复就绪)。
1、中断中接收队列消息具体代码片段如下:
可以看到,其在能接收消息时,是先将消息队列中的消息取出来了,之后再去判断队列是否上锁了。
if( uxMessagesWaiting > ( UBaseType_t ) 0 ) { const int8_t cRxLock = pxQueue->cRxLock; //有消息就先接收 prvCopyDataFromQueue( pxQueue, pvBuffer ); pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1; //接收完消息再判断是否上锁 if( cRxLock == queueUNLOCKED ) { if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE ) { if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ) { if( pxHigherPriorityTaskWoken != NULL ) { *pxHigherPriorityTaskWoken = pdTRUE; } } } } else { pxQueue->cRxLock = ( int8_t ) ( cRxLock + 1 ); } xReturn = pdPASS; } else { xReturn = pdFAIL; } }
2、消息队列解锁代码片段:
可以看到,解锁时,会通过一个while循环,将所有错过恢复发送机会的任务都恢复;
//循环cRxLock次,恢复队列上锁时错过发送消息的任务 while( cRxLock> queueLOCKED_UNMODIFIED ) { #if ( configUSE_QUEUE_SETS == 1 ) //xxxxx #else { if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend) ) == pdFALSE ) { if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend) ) != pdFALSE ) { vTaskMissedYield(); } } } #endif --cRxLock; } pxQueue->cRxLock= queueUNLOCKED; }
3. 消息队列创建及删除
清楚队列原理后,这里首先介绍一下freertos中消息队列的自定义类型(这里将一些特殊操作时用到的成员都删除掉,我们只讨论一般情况)
3.1 创建消息队列函数
freertos中有两种创建消息队列的方式:
1. 静态创建消息队列函数xQueueCreateStatic;(需要自己创建一个数组空间,这个数组空间会被创建的队列使用,但 "静态方式" 在创建队列时不常用,所以这里不做详述)
2. 动态创建消息队列函数xQueueCreate;
(由于静态方式创建消息队列的方式不常用,故这里就只介绍动态创建的方式,而动态创建消息队列是通过heap_x源文件中的管理方式来申请空间的,管理方式之后章节会讲到)
3.1.1 xQueueGenericCreate通用任务创建函数
(这个函数需要把 configSUPPORT_DYNAMIC_ALLOCATION 定义为1来使能)
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, const uint8_t ucQueueType )
{
Queue_t *pxNewQueue;
size_t xQueueSizeInBytes;
uint8_t *pucQueueStorage;
if( uxItemSize == ( UBaseType_t ) 0 )
{
xQueueSizeInBytes = ( size_t ) 0;
}
else
{
xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize );
}
pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );
if( pxNewQueue != NULL )
{
pucQueueStorage = ( ( uint8_t * ) pxNewQueue ) + sizeof( Queue_t );
prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );
}
return pxNewQueue;
}
#endif
参数:uxQueueLength(队列长度)
uxItemSize(单个消息空间大小)
ucQueueType (消息队列类型):
queueQUEUE_TYPE_BASE:表示队列。
queueQUEUE_TYPE_SET:表示队列集合 。queueQUEUE_TYPE_MUTEX:表示互斥量。queueQUEUE_TYPE_COUNTING_SEMAPHORE:表示计数信号量。queueQUEUE_TYPE_BINARY_SEMAPHORE:表示二进制信号量。queueQUEUE_TYPE_RECURSIVE_MUTEX :表示递归互斥量。
xQueueGenericCreate函数具体做了如下操作:
1. 队列消息空间单个空间大小为0,则队列大小就是0,若单个队列空间大小不为0,则消息队列要申请的空间就是 ("队列总长度" x "单个队列空间大小");
2.计算要申请的空间的大小,调用动态申请空间大小函数pvPortMalloc,申请空间大小为,队列结构体大小+队列消息空间的总大小;(申请空间后返回的地址是所申请空间的首地址)
3.若当前空间大小不为空,计算出消息队列空间钟真正的头地,其之后马上就作为参数传给"新队列初始化函数";
4.等待调用完 "新队列初始化函数" ,返回队列句柄;
总结:
1. 首先计算出当前队列要申请的空间大小,之后立即申请空间;
2. 申请到已有空间后,将消息队列空间的首地址作为参数给到下面的 “初始化新队列函数”中;
3.1.1.1 prvInitialiseNewQueue初始化新队列函数
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 )
{
pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;
}
else
{
pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage;
}
pxNewQueue->uxLength = uxQueueLength;
pxNewQueue->uxItemSize = uxItemSize;
( void ) xQueueGenericReset( pxNewQueue, pdTRUE );
}
参数: uxQueueLength(队列长度) uxItemSize (单个队列空间大小)
pucQueueStorage(消息队列空间的首地址) ucQueueType(队列类型)
pxNewQueue(队列句柄)
其中橙色的参数并没有被调用;
prvInitialiseNewQueue函数具体做了一下操作:
1. 将没用到的参数做一次空操作防止编译器报警,判断当前消息队列的单个空间大小是否为0,若不为0则将 “pchead-->消息队列的首地址”,若为0则将 “pchead-->前面到的申请空间的首地址”;
2. 初始化队列长度为当前队列的长度,初始化消息队列空间为当前队列的消息空间大小;
3. 最后调用xQueueGenericReset函数,初始化队列中的指针项;
总结:
初始化了pchead的指向,及结构体中消息队列长度大小和消息空间的大小,最后调用xQueueGenericReset函数将结构体中所有参数都进行初始化;
3.1.1.2 xQueueGenericReset通用重置队列函数
BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue )
{
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
taskENTER_CRITICAL();
{
pxQueue->pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->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 )
{
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
//将传入根节点下的第一个节点移除出此阻塞链表,并添加到就绪列表,返回值是需要调度标志位
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
{
//移除一个节点,就进行一次任务调度
queueYIELD_IF_USING_PREEMPTION();
}
}
}
else
{
vListInitialise( &( pxQueue->xTasksWaitingToSend ) );
vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );
}
}
taskEXIT_CRITICAL();
return pdPASS;
}
参数: xQueue(要重置的队列) xNewQueue (1:重置队列为新队列 0:重置队列为已创建的队列)
xQueueGenericReset函数具体做了一下操作:
1. 将pcTail指向了消息队列的最后一个空间,pcReadFrom指向消息队列的倒数第二个空间,pcWriteTo 指向消息队列的首空间;
2. 将cRxLock 和cTxLock(初始化为queueUNLOCKED)以及uxMessagesWaiting(初始化为0表当前队列中的消息个数为0)这三个标识队列的成员赋予相应值;
3.判断当前重置队列是否为新队列:
新队列:初始化“发送消息阻塞链表”和“接受消息阻塞链表”根节点;
已创队列:重置时,当前等待发送消息阻塞列表中还有阻塞的任务,则这里将它们都从阻塞链表中移除(这里的xTaskRemoveFromEventList函数其实就是将属于当前阻塞链表下的第一个节点就绪,之后马上进行任务切换),因为这里已经将阻塞任务恢复为了就绪态,这时就需要马上进行一次任务调度,立即执行当前优先级最高的任务;
总结:
此函数初始化了队列的结构体成员,特殊情况为:
要初始化队列为已创建队列:首先要将这个队列的所有指针指向重置,并将记录消息个数的变量重置为0(这样队列就初始化为了最开始的样子,入队从消息空间首地址进入,出队从pcReadFrom指向的位置出队),重置完队列后判断这个队列的发送阻塞链表中是否有等待发送的任务,若有则立即将它移除(为的就是让这些任务不要再等待向队列发送了),若这些任务中有优先级比当前运行任务优先级高的任务,则之后立即进行调度保持操作系统的抢占性;
3.2 消息队列删除队列函数vQueueDelete
void vQueueDelete( QueueHandle_t xQueue )
{
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
#if ( configQUEUE_REGISTRY_SIZE > 0 )
{
vQueueUnregisterQueue( pxQueue );
}
#endif
#if( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 0 ) )
{
vPortFree( pxQueue );
}
#elif( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 1 ) )
{
if( pxQueue->ucStaticallyAllocated == ( uint8_t ) pdFALSE )
{
vPortFree( pxQueue );
}
}
#else
{
( void ) pxQueue;
}
#endif
}
参数: xQueue (要重置的队列的句柄)
vQueueDelete具体做了如下操作:
1. 将消息对了从注册表中删除,不过目前这里没有用注册表,不用理会;
2.调用vPortFree直接释放队列空间;
总结:
此函数通过释放队列所申请的空间,从而达到删除队列的目的;
4. 消息队列发送信息
4.1 消息队列发送消息函数xQueueSend
说明队列发送函数之前,来先说明一些消息发送函数会调用到的比较重要的函数,方便后面直接分析消息队列的发送;
4.1.1 prvCopyDataToQueue拷贝数据到队列函数(这里拷贝数据的方式有两种都在前面 "2.2节" 介绍了)
static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue, const void *pvItemToQueue, const BaseType_t xPosition )
{
BaseType_t xReturn = pdFALSE;
UBaseType_t uxMessagesWaiting;
uxMessagesWaiting = pxQueue->uxMessagesWaiting;
if( pxQueue->uxItemSize == ( UBaseType_t ) 0 )
{
#if ( configUSE_MUTEXES == 1 )
{
if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
{
xReturn = xTaskPriorityDisinherit( ( void * ) pxQueue->pxMutexHolder );
pxQueue->pxMutexHolder = NULL;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
}
else if( xPosition == queueSEND_TO_BACK )
{
( void ) memcpy( ( void * ) pxQueue->pcWriteTo, pvItemToQueue, ( size_t ) pxQueue->uxItemSize );
pxQueue->pcWriteTo += pxQueue->uxItemSize;
if( pxQueue->pcWriteTo >= pxQueue->pcTail )
{
pxQueue->pcWriteTo = pxQueue->pcHead;
}
}
else
{
( void ) memcpy( ( void * ) pxQueue->u.pcReadFrom, pvItemToQueue, ( size_t ) pxQueue->uxItemSize );
pxQueue->u.pcReadFrom -= pxQueue->uxItemSize;
if( pxQueue->u.pcReadFrom < pxQueue->pcHead )
{
pxQueue->u.pcReadFrom = ( pxQueue->pcTail - pxQueue->uxItemSize );
}
if( xPosition == queueOVERWRITE )
{
if( uxMessagesWaiting > ( UBaseType_t ) 0 )
{
--uxMessagesWaiting;
}
}
}
pxQueue->uxMessagesWaiting = uxMessagesWaiting + 1;
return xReturn;
}
参数:xQueue(数据项将发送到此队列)
pvItemToQueue(指向待入队数据项的指针,之后会将固定字节的数据从pvItemToQueue中复制到队列存储区域)
xPosition (队列发送消息方式):
queueSEND_TO_BACK(发送到队尾)
queueSEND_TO_FRONT(发送到队头)
queueOVERWRITE(以覆盖方式发送)
返回值:是否需要任务调度(只有在有互斥量时才会用到,这里不会用到,默认返回false)
prvCopyDataToQueue操作如下:
1. 首先通过判断单个空间大小,来区分是 “互斥信号量” 还是正常的消息队列(若为0就认为是个互斥信号量),进入其中判断当前的队列是否是互斥量,若是则调用互斥量的相关函数;
2. 若不是互斥量了,则现在判断队列的入队(拷贝)方式;
3. queueSEND_TO_BACK(此时消息是FIFO方式出入队):这里从pcWriteTo指向的当前消息队列的空闲空间开始,将pvItemToQueue中的数据拷贝到消息队列中,一次拷贝uxItemSize字节的数据到目标消息队列空间中,拷贝完成后就将pcWriteTo移动uxItemSize个单位(始终指向空闲的消息队列空间),最后判断消息队列复制数据后是否将要发生溢出,若是则重置pcWriteTo,将指向空闲队列空间指针;(具体如下,也就是上面2.2介绍过的消息队列的出入队原则:)
"queueSEND_TO_BACK" 方式入队:
"queueSEND_TO_BACK" 方式出队:
4.queueSEND_TO_FRONT / queueOVERWRITE (此时消息是LIFO方式出入队):
此时用pcReadFrom指针入队,先将一个空间大小的消息复制到pcReadFrom指向的消息空间中,之后将pcReadFrom向前移动一个消息空间(pcReadFrom-1,为的是之后实现LIFO方式出入队),若当pcReadFrom的指向小于pchead了,则重置pcReadFrom指向pcTail的前一个消息空间,若此时是以覆盖方式入队,则还下面会将当前消息队列消息个数变量uxMessagesWaiting-1(因为以覆盖方式入队的话其实当前消息的队列的消息个数其实是并没有增加的,但只要调用了消息拷贝到函数uxMessagesWaiting变量就会+1表有消息入队,所以这里uxMessagesWaiting要-1);(具体如下,也就是上面2.2介绍过的消息队列的出入队原则:)
"queueSEND_TO_FRONT / queueOVERWRITE "方式入队:
"queueSEND_TO_FRONT / queueOVERWRITE "方式出队:
总结:
prvCopyDataToQueue拷贝消息到队列函数一共有3种方式入队:
(1) 第一种是FIFO方式出入队(传入参数"queueSEND_TO_BACK"),此时将pcWriteTo指针作为入队指针,入队时先将消息传入其指向的空间,之后将pcWriteTo指针向后移动一个消息空间;
(2)第二种方式是LIFO方式(传入参数"queueSEND_TO_FRONT "),此时pcReadFrom作为入队指针,入队时先将消息传入其指向的空间,之后将pcWriteTo指针向前移动一个消息空间;
(3) 第三种方式是"覆盖方式",它也是以LIFO方式入队的(传入参数"queueOVERWRITE "),其入队方式和第二种方式一致,唯一的不同就是这种入队方式不会让消息队列种的消息数量增加,它会覆盖掉以前存在消息的消息空间;
4.1.2 xTaskCheckForTimeOut检查任务超时函数
BaseType_t xTaskCheckForTimeOut( TimeOut_t * const pxTimeOut, TickType_t * const pxTicksToWait )
{
BaseType_t xReturn;
taskENTER_CRITICAL();
{
const TickType_t xConstTickCount = xTickCount;
#if ( INCLUDE_vTaskSuspend == 1 )
if( *pxTicksToWait == portMAX_DELAY )
{
xReturn = pdFALSE;
}
else
#endif
if( ( xNumOfOverflows != pxTimeOut->xOverflowCount ) && ( xConstTickCount >= pxTimeOut->xTimeOnEntering ) )
{
xReturn = pdTRUE;
}
else if( ( ( TickType_t ) ( xConstTickCount - pxTimeOut->xTimeOnEntering ) ) < *pxTicksToWait )
{
*pxTicksToWait -= ( xConstTickCount - pxTimeOut->xTimeOnEntering );
vTaskSetTimeOutState( pxTimeOut );
xReturn = pdFALSE;
}
else
{
xReturn = pdTRUE;
}
}
taskEXIT_CRITICAL();
return xReturn;
}
参数:
pxTimeOut(超时结构体,存储着进入阻塞时)
pxTicksToWait(要延时的时间)
返回值:
xReturn(消息队列阻塞时间是否超时,ture(超时时间已到),false(超时时间未到))
xTaskCheckForTimeOut具体操作:(这段操作需要临界段保护)
1. 首先将当前 "时基" 存在临时变量xConstTickCount中,后面会用到;
2. 判断消息队列的延时方式:
(1)若为无限延时的话就一定不会超时,所以这里赋予false;
(2)若不为无限延时,则进入延时超时判断环节;
3. 这里进入延时判断超时环节,首先判断当前 "时基溢出次数" 是否比消息队列刚刚进入阻塞时的 "时基溢出次数" 要大,且溢出后的时基要>消息队列进入阻塞时候的时基,此时就退出阻塞;(这个操作其实是为了保证,消息队列在普通阻塞状态下的阻塞时间小于0xFFFFFFFFUL,普通阻塞状态下大于等于这个值,就立即解除消息队列的阻塞状态)
4. 若普通阻塞还没达到系统所限制的最大延时时间,则先判断延时时间是否到了(当前系统时基 - 消息队列进入阻塞时的时基 < 延时时间 ------> 证明延时时间到了,反之亦反):
(1)延时时间没到的话就更新延时时间、系统进入阻塞态时间,之后返回false表示消息队列阻塞时间还没到;(这里更新一系列的时间是为了防止当时基溢出发生后,下一次延时判断时,消息队列进入阻塞时间是时基溢出之前的值,这样让当前系统时基 - 消息队列进入阻塞时时基就会是个负数发生溢出,所以这里每次都要更新防止这样的情况发生)
(2)到了就返回ture,表当前消息队列的阻塞时间已到,可解除阻塞;
总结:
1. 首先判断消息队列阻塞方式(时间无限制阻塞,普通阻塞);
2. 若是无限制阻塞的话就不用再去判断阻塞时间是否到达了,直接返回false;
3. 普通阻塞状态下首先判断阻塞时间是否超过了系统所规定的值,若超过则解除阻塞;
4. 若没超过系统规定阻塞时间的话,就判断阻塞时间是否到了,没到就更新进入阻塞时间,和下一次需要延时的时间;
5. 延时时间到,返回ture;
4.1.3 xQueueGenericSend通用消息队列发送函数
这个函数的内容有些多,这里分开说明,它主要分为两段操作:
1、发送消息到消息队列段;(这段处在临界段中)
2、检查超时段;(函数的这段退出了临界段,挂起但了调度器,意为允许外设中断,但不允许系统自动进行的任务调度)
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) /
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
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 ) )
{
#if ( configUSE_QUEUE_SETS == 1 )
//xxxxx
#else
{
xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
{
queueYIELD_IF_USING_PREEMPTION();
}
}
else if( xYieldRequired != pdFALSE )
{
queueYIELD_IF_USING_PREEMPTION();
}
}
#endif
taskEXIT_CRITICAL();
return pdPASS;
}
else
{
if( xTicksToWait == ( TickType_t ) 0 )
{
taskEXIT_CRITICAL();
return errQUEUE_FULL;
}
else if( xEntryTimeSet == pdFALSE )
{
vTaskInternalSetTimeOutState( &xTimeOut );
xEntryTimeSet = pdTRUE;
}
}
}
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 );
if( xTaskResumeAll() == pdFALSE )
{
portYIELD_WITHIN_API();
}
}
else
{
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
}
}
else
{
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
return errQUEUE_FULL;
}
}
}
参数:xQueue(数据项将发送到此队列)
pvItemToQueue(指向待入队数据项的指针,之后会将固定字节的数据从pvItemToQueue中复制到队列存储区域)
xTicksToWait(若队列已满,则任务应进入阻塞态等待队列上出现可用空间所等待的最大时间)
xCopyPosition(队列发送消息方式):
queueSEND_TO_BACK(发送到队尾)
queueSEND_TO_FRONT(发送到队头)
queueOVERWRITE(以覆盖方式发送)
返回参数:
消息发送到消息队列失败/成功
这个函数的内容有些多,这里分开说明,它主要分为两段操作:
1、发送消息到消息队列段;(这段处在临界段中)
2、检查超时段;(函数的这段退出了临界段,挂起但了调度器,意为允许外设中断,但不允许系统自动进行的任务调度)
这里先说明第一段:(就是临界段内的代码,不允许被打断)
1、首先判断当前消息队列中是否还有空间(若以覆盖方式入队就不用判断当前队列是否还有空间),此时只要消息队列还有空间就继续进行第一段操作,无空间就开始判断传入的阻塞时间,若为0就不阻塞了并返回消息发送失败,<0就将任务进行阻塞的时间记录下来,之后程序进入到第二段开始检查是否超时;
2、调用prvCopyDataToQueue函数,将消息拷贝到队列空间中(具体入队方式就看自己调用函数时的配置,这里不涉及互斥量,所以返回不需要系统切换);
3、将消息放入消息空间后,就开始判断 "接受阻塞链表" 是否为空:
(1)若为空,则证明当前没有任务需要接受发送到队列空间中的消息,此时退出临界段后可直接返回"消息发送到队列成功";
(2)若不为空,则证明有任务为接受此队列消息被阻塞了,所以将当前插入到队列的消息直接给等待消息的任务即可,之后调用xTaskRemoveFromEventList函数(里面主要做的就是将传入的任务从其事件节点及普通节点所属的链表中移除(这里将接受阻塞的任务传入,那么这个任务就会从接受阻塞和系统阻塞链表中被移除),并插入到就绪列表(若没挂起调度器的话))将任务从阻塞态恢复到就绪态,若此任务的优先级>正在运行的任务,则立即进行一次任务调度;(此时系统开始正常调度)
其次是程序的第二段:(此段在调度器挂起状态中进行)
1、只有当消息没有发送出去要被阻塞时,程序才会进行到第二段,由于此时发送队列要被阻塞,所以首先要将队列上发送锁(防止中断中有发送消息任务时,队列在中断中被阻塞,具体详见上面的 "队列锁机制");
2、调用xTaskCheckForTimeOut函数检测当前任务阻塞是否超时:
(1)没有超时:首先在xTaskCheckForTimeOut函数中更新任务进入阻塞时的系统时基及系统时基溢出次数(它内部的具体实现上面已经说明过了);
(2)已经超时:解锁队列,恢复调度器,并返回队列发送失败;(表阻塞时间内,还没等到队列空间发送消息)
3、若检测任务阻塞后发现任务还没有超时的话,则退出检测任务超时任务后,然后判断队列是否还是满的:
(1)若不满:则说明任务阻塞时间内发送任务已经等到了消息队列的空余空间,可以发送消息,此时直接解除队列锁,并恢复任务调度,之后任务调度重新调度此任务后,在for死循环内就会再次循环到程序的第一段,之后在第一段程序中重新将任务要发送的消息发送出去;(在阻塞超时时段内,等到了队列空位的情况)
(2)若满:则说明消息队列还没有空位,还需要继续等待,此时调用vTaskPlaceOnEventList函数将此任务插入到发送阻塞队列(这个函数会将任务的事件节点挂载到 "队列发送阻塞" 链表,将任务的普通节点挂载到 "系统延时阻塞" 链表(正式进入阻塞态),其中 "队列发送阻塞" 链表,之后会在每次接收消息操作时被访问,这是因为每次接收消息后,消息队列上就会多一个空位,消息队列有空位了意味着想要发送消息的任务现在可以发送消息到队列,所以每次接收消息时都要访问一遍"发送阻塞队列"),做完队列操作后,之后调用prvUnlockQueue将队列解锁(解锁会恢复上锁期间错过的发送消息的机会的阻塞任务),做完这些操作后重新开启调度器产生系统调度;
xQueueGenericSend函数总结:
1、首先它会判断队列是否有空间存储发送的消息,若有则拷贝消息到队列,没有就将任务阻塞;
2、消息队列有空间,就将消息复制到队列中,之后再看是否有被接收阻塞的任务,若有就将它恢复(因为现在队列空间中已有消息);
3、在1的判断中若消息队列已满,就将当前发送消息的任务阻塞,并挂载到发送延时列表,之后若有消息接收消息队列中的消息时,就会将此任务恢复就绪,就绪之后又重新调度到此任务时,它就会重新发送消息到消息队列;
4.1.4 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;
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
{
if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
{
const int8_t cTxLock = pxQueue->cTxLock;
( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );
if( cTxLock == queueUNLOCKED )
{
#if ( configUSE_QUEUE_SETS == 1 )
//xxxxxxx
#else
{
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
{
if( pxHigherPriorityTaskWoken != NULL )
{
*pxHigherPriorityTaskWoken = pdTRUE;
}
}
}
}
#endif
}
else
{
pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 );
}
xReturn = pdPASS;
}
else
{
xReturn = errQUEUE_FULL;
}
}
portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
return xReturn;
}
参数:
xQueue:要发送消息到的队列
pvItemToQueue:指向待入队数据项的指针,之后会将固定字节的数据从pvItemToQueue中复制到队列存储区域
pxHigherPriorityTaskWoken:是否需要上下文切换标志(有优先级更高的任务被唤醒,就立马需要上下问切换)
xCopyPosition:消息发送到队列的方式
xQueueGenericSendFromISR函数做了如下操作:
1、首先它判断了消息队列中是否有空间能容纳下新消息:
(1)若能容纳就先将消息复制入队;
(2)若消息空间满,则直接返回Error,不用阻塞任务(因为要在中断不属于操作系统所管辖的任务,如果在这里调用发送阻塞,则会将当前运行的任务挂载到阻塞链表,这显然不对);
2、在消息队列有空间的情况下,复制完消息到队列后,首先判断队列是否上锁:
(1)若队列没上锁,则检查接受延时列表,看看现在有没有接受阻塞的任务,若有则将它从接受阻塞状态中恢复就绪,若就绪任务优先级>现有任务优先级,就启用一次系统调度;
(2)若队列上锁,此时就不做任何链表操作,取而代之的是将cTxLock队列锁变量+1,代表中断中已经发送过一次消息,之后在线程发送消息函数中会根据这个变量将对应数量的任务恢复,此时返回Pass;
总结:
1、有消息空间的情况下:
(1)有队列锁:先将消息入队,但不做任何链表操作,链表操作之后会在线程发送消息函数中进行;
(2)无队列锁:同样消息要入队,但还要检查接受阻塞链表中是否有阻塞任务,有就将其恢复;
2、无消息空间,就直接返回Error即可;
5.1 消息队列接受信息(具体实现原理都与发送消息函数一致,所以这里只介绍里面有区别的函数)
消息队列接受和发送消息的相关函数实现原理都是差不多的,区别无非就是一下几点:
1、在复制消息时,一个是复制消息到队列,一个是从队列中取出消息;
2、在检查阻塞时,一个检查的是接受阻塞链表(发送操作中),一个检查的是发送阻塞链表(接受操作中);(这个函数)
所以下面只介绍有区别的函数函数操作,其他在原理上没有区别的函数就不再做详述。
首先讲解prvCopyDataFromQueue,从消息队列取出消息函数
5.1.1 prvCopyDataFromQueue消息队列接受消息函数
static void prvCopyDataFromQueue( Queue_t * const pxQueue, void * const pvBuffer )
{
if( pxQueue->uxItemSize != ( UBaseType_t ) 0 )
{
pxQueue->u.pcReadFrom += pxQueue->uxItemSize;
if( pxQueue->u.pcReadFrom >= pxQueue->pcTail )
{
pxQueue->u.pcReadFrom = pxQueue->pcHead;
}
( void ) memcpy( ( void * ) pvBuffer, ( void * ) pxQueue->u.pcReadFrom, ( size_t ) pxQueue->uxItemSize );
}
}
参数:pxQueue(从此队列接受消息)
pvBuffer (接受消息的地址)
返回值:无
prvCopyDataFromQueue做了如下操作:
1、首先先确认当前队列里有消息后,再进行取消息操作;
2、先将队列出队指针向后移动一个单位空间;
3、判断队列出队指针移动后所指向的消息空间是否超出队列,若超出则复位队列出队指针到队列头;
4、从当前队列出队指针指向的空间中,将此空间内存储的消息复制到pvBuffer要存储消息的地址;
总结:
1、这个函数主要就是利用pcReadFrom指针从其所指向的消息队列空间中取出消息到目标地址;
2、它首先会将pcReadFrom+1指向下一个消息空间,之后做一个防止指针指向非法空间的操作,最后调用memcpy函数将要取出的消息复制到目标存储地址中;
至于xQueueGenericReceive通用接受消息函数,和xQueueReceiveFromISR从中断中接受消息函数,不在做详细叙述,它们的实现基本一致(不一致的地方就是在每次接受完消息后,会检查发送延时列表下是否有阻塞的任务,若有就恢复此任务,因为此时队列又有了空间给发送消息的任务。要是理解了前面发送消息的原理及实现,那么理解这段话肯定是没有任何难度的,这两个函数的具体代码实现,可以去freertos的官方下载源码)