分析FreeRTOS内核对象API的源码实现(三)

消息队列:

(1)消息队列的基本概念:

队列又称消息队列,是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任务间传递信息,实现了任务接收来自其他任务或中断的不固定长度的消息,任务能够从队列里面读取消息,当队列中的消息是空时,读取消息的任务将被阻塞,用户还可以指定阻塞的任务时间 xTicksToWait,在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当队列中有新消息时,被阻塞的任务会被唤醒并处理新消息;当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转为就绪态。消息队列是一种异步的通信方式。 通过消息队列服务,任务或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个任务可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常是将先进入消息队列的消息先传给任务,也就是说,任务先得到的是最先进入消息队列的消息,即先进先出原则(FIFO),但是也支持后进先出原则(LIFO)。 FreeRTOS 中使用队列数据结构实现任务异步通信工作,具有如下特性:

 消息支持先进先出方式排队,支持异步读写工作方式。

 读写队列均支持超时机制。

 消息支持后进先出方式排队,往队首发送消息(LIFO)。

 可以允许不同长度(不超过队列节点最大值)的任意类型消息。

 一个任务能够从任意一个消息队列接收和发送消息。

 多个任务能够从同一个消息队列接收和发送消息。

 当队列使用结束后,可以通过删除队列函数进行删除。

(2)消息队列的运作机制

创建消息队列时 FreeRTOS 会先给消息队列分配一块内存空间,这块内存的大小等于消息队列控制块大小加上(单个消息空间大小与消息队列长度的乘积),接着再初始化消息队列,此时消息队列为空。FreeRTOS 的消息队列控制块由多个元素组成,当消息队列被创建时,系统会为控制块分配对应的内存空间,用于保存消息队列的一些信息如消息的存储位置,头指针 pcHead、尾指针 pcTail、消息大小 uxItemSize 以及队列长度 uxLength 等。同时每个消息队列都与消息空间在同一段连续的内存空间中,在创建成功的时候,这些内存就被占用了,只有删除了消息队列的时候,这段内存才会被释放掉,创建成功的时候就已经分配好每个消息空间与消息队列的容量,无法更改,每个消息空间可以存放不大于消息大小 uxItemSize 的任意类型的数据,所有消息队列中的消息空间总数即是消息队列的长度,这个长度可在消息队列创建时指定。

任务或者中断服务 程序都可以给消息队列发送消息,当发送消息时,如果队列未满或者允许覆盖入队,FreeRTOS 会将消息拷贝到消息队列队尾,否则,会根据用户指定的阻塞超时时间进行阻塞,在这段时间中,如果队列一直不允许入队,该任务将保持阻塞状态以等待队列允许入队。当其它任务从其等待的队列中读取入了数据(队列未满),该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中还不允许入队,任务也会自动从阻塞态转移为就绪态,此时发送消息的任务或者中断程序会收到一个错误码 errQUEUE_FULL。

发送紧急消息的过程与发送消息几乎一样,唯一的不同是,当发送紧急消息时,发送的位置是消息队列队头而非队尾,这样,接收者就能够优先接收到紧急消息,从而及时进行消息处理。

当某个任务试图读一个队列时,其可以指定一个阻塞超时时间。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务程序往其等待的队列中写入了数据,该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。

当消息队列不再被使用时,应该删除它以释放系统资源,一旦操作完成,消息队列将被永久性的删除。

(注意:这里描述的不够仔细,无法表达真正的如何将一个消息紧急插入到下一个要发送的消息,在后面我们学习通过队列发送一个消息的源码时,会更详细的了解)

(3)消息队列的阻塞机制

很简单,因为 FreeRTOS 已经为我们做好了,我们直接使用就好了,每个对消息队列读写的函数,都有这种机制,我称之为阻塞机制。假设有一个任务 A 对某个队列进行读操作的时候(也就是我们所说的出队),发现它没有消息,那么此时任务 A 有 3 个选择:第一个选择,任务 A 扭头就走,既然队列没有消息,那我也不等了,干其它事情去,这样子任务 A 不会进入阻塞态;第二个选择,任务 A 还是在这里等等吧,可能过一会队列就有消息,此时任务 A 会进入阻塞状态,在等待着消息的道来,而任务 A 的等待时间就由我们自己定义,比如设置 1000 个系统时钟节拍 tick 的等待,在这 1000 个 tick 到来之前任务 A 都是处于阻塞态,当阻塞的这段时间任务 A 等到了队列的消息,那么任务 A 就会从阻塞态变成就绪态,如果此时任务 A 比当前运行的任务优先级还高,那么,任务 A 就会得到消息并且运行;假如 1000 个 tick 都过去了,队列还没消息,那任务 A 就不等了,从阻塞态中唤醒,返回一个没等到消息的错误代码,然后继续执行任务 A 的其他代码;第三个选择,任务 A 死等,不等到消息就不走了,这样子任务 A 就会进入阻塞态,直到完成读取队列的消息。

而在发送消息操作的时候,为了保护数据,当且仅当队列允许入队的时候,发送者才能成功发送消息;队列中无可用消息空间时,说明消息队列已满,此时,系统会根据用户指定的阻塞超时时间将任务阻塞,在指定的超时时间内如果还不能完成入队操作,发送消息的任务或者中断服务程序会收到一个错误码 errQUEUE_FULL,然后解除阻塞状态**;当然,只有在任务中发送消息才允许进行阻塞状态,而在中断中发送消息不允许带有阻塞机制的,需要调用在中断中发送消息的 API 函数接口,因为发送消息的上下文环境是在中断中,不允许有阻塞的情况。**

假如有多个任务阻塞在一个消息队列中,那么这些阻塞的任务将按照任务优先级进行排序,优先级高的任务将优先获得队列的访问权。

(4)消息队列的应用场景

消息队列可以应用于发送不定长消息的场合,包括任务与任务间的消息交换,队列是FreeRTOS 主要的任务间通讯方式,可以在任务与任务间、中断和任务间传送信息,发送到队列的消息是通过拷贝方式( 复刻内容。队列里收的是数据的复印件)实现的,这意味着队列存储的数据是原数据,而不是原数据的引用。

(5)消息队列控制块

c 复制代码
typedef struct QueueDefinition
{
	int8_t *pcHead;					/*< Points to the beginning of the queue storage area. */
	int8_t *pcTail;					/*< Points to the byte at the end of the queue storage area.  Once more byte is allocated than necessary to store the queue items, this is used as a marker. */
	int8_t *pcWriteTo;				/*< Points to the free next place in the storage area. */

	union							/* Use of a union is an exception to the coding standard to ensure two mutually exclusive structure members don't appear simultaneously (wasting RAM). */
	{
		int8_t *pcReadFrom;			/*< Points to the last place that a queued item was read from when the structure is used as a queue. */
		UBaseType_t uxRecursiveCallCount;/*< Maintains a count of the number of times a recursive mutex has been recursively 'taken' when the structure is used as a mutex. */
	} u;

	List_t xTasksWaitingToSend;		/*< List of tasks that are blocked waiting to post onto this queue.  Stored in priority order. */
	List_t xTasksWaitingToReceive;	/*< List of tasks that are blocked waiting to read from this queue.  Stored in priority order. */

	volatile UBaseType_t uxMessagesWaiting;/*< The number of items currently in the queue. */
	UBaseType_t uxLength;			/*< The length of the queue defined as the number of items it will hold, not the number of bytes. */
	UBaseType_t uxItemSize;			/*< The size of each items that the queue will hold. */

	volatile int8_t cRxLock;		/*< Stores the number of items received from the queue (removed from the queue) while the queue was locked.  Set to queueUNLOCKED when the queue is not locked. */
	volatile int8_t cTxLock;		/*< Stores the number of items transmitted to the queue (added to the queue) while the queue was locked.  Set to queueUNLOCKED when the queue is not locked. */

	#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
		uint8_t ucStaticallyAllocated;	/*< Set to pdTRUE if the memory used by the queue was statically allocated to ensure no attempt is made to free the memory. */
	#endif

	#if ( configUSE_QUEUE_SETS == 1 )
		struct QueueDefinition *pxQueueSetContainer;
	#endif

	#if ( configUSE_TRACE_FACILITY == 1 )
		UBaseType_t uxQueueNumber;
		uint8_t ucQueueType;
	#endif

} xQUEUE;

(1)pcHead 指向队列消息存储区起始位置,即第一个消息空间。

(2)pcTail 指向队列消息存储区结束位置地址。

(3)pcWriteTo 指向队列消息存储区下一个可用消息空间。

(4)pcReadFrom 与 uxRecursiveCallCount 是一对互斥变量,使用联合体用来确保两个互斥的结构体成员不会同时出现(意思是:编译器对它的全部保证就一条:&u.pcReadFrom 和 &u.uxRecursiveCallCount 是同一个地址,也就是说它们的值都相等 )。当结构体用于队列时,pcReadFrom 指向出队消息空间的最后一个,见文知义,就是读取消息时候是从 pcReadFrom 指向的空间读取消息内容。

(这里注意pcTail指向的位置和pcReadFrom指向的位置不同:前者是"站在最后一格上的标记",后者是"最后一格右边的围栏"。数据发送流程如下;

  • 初始:写指针(pcWriteTo)在 0x00,读指针(pcReadFrom)在 0x10(最后一格);
  • 发一条:memcpy 到 pcWriteTo,然后 pcWriteTo 往后挪一格(0x00→0x04);
  • 收一条:pcReadFrom 先往后挪一格------0x10+0x04=0x14(pcTail指向的位置),撞上 pcTail,于是绕回 pcHead(0x00),再从格0 拷数据给你。
    )

各自的用途:

指针 在哪 动不动 作用
pcHead 格0 的起点 不动 起点 / 绕回目标
pcTail 最后一格之后(格外边界) 不动 绕回判断的"终点线"
pcWriteTo 下一个空格 每发一条挪一格 写定位
pcReadFrom 上一次读走的格 每收一条挪一格 读定位(下次读它后一格)

(5)当结构体用于互斥量时,uxRecursiveCallCount 用于计数,记录递归互斥量被"调用"的次数。

(6)xTasksWaitingToSend 是一个发送消息阻塞列表,用于保存阻塞在此队列的任务,任务按照优先级进行排序,由于队列已满,想要发送消息的任务无法发送消息。

(7)xTasksWaitingToReceive 是一个获取消息阻塞列表,用于保存阻塞在此队列的任务,任务按照优先级进行排序,由于队列是空的,想要获取消息的任务无法获取到消息。

(8)uxMessagesWaiting 用于记录当前消息队列的消息个数,如果消息队列被用于信号量的时候,这个值就表示有效信号量个数。

(9)uxLength 表示队列的长度,也就是能存放多少消息。

(10)uxItemSize 表示单个消息的大小。

(11)队列上锁后,储存从队列收到的列表项数目,也就是出队的数量,如果队列没有上锁,设置为 queueUNLOCKED。

(12)队列上锁后,储存发送到队列的列表项数目,也就是入队的数量,如果队列没有上锁,设置为 queueUNLOCKED。

这两个成员变量为 queueUNLOCKED 时,表示队列未上锁;当这两个成员变量为queueLOCKED_UNMODIFIED 时,表示队列上锁。

对(11)(12)操作分析:

(11)(12)的机制与我们之前学习的挂起调度器期间,进入xTaskIncrementTick()函数不运行任务,纯"记账"类似,这里也是在已上锁期间中断出队 n 条(cRxLock)/ 入队 n 条(cTxLock),欠着 n 次唤醒,解锁时就要补。

为什么要记账------这是个"漏唤醒"问题,看这段时序:

任务A:查了队列"满" → 打算把自己挂进 xTasksWaitingToSend

↓ 就在这两步之间的窗口里......

中断: 从队列取走一条 → 按逻辑该唤醒"等发送"的任务?

可是 A 还没挂上等待列表,现在去动事件列表根本看不见 A → 漏唤醒!

A 明明马上就有空位可发,却没人叫醒它(要等到下一次别人操作或超时)。于是设计成这样:中断发现队列锁着,就不动事件列表,只把 cRxLock++(queue.c:1508,SendFromISR 里是cTxLock++),然后走人。等 A 把自己挂好、执行 prvUnlockQueue() 时,看到 cRxLock > 0,就知道"上锁期间被取走了几条",回头按账把等待列表里的任务补唤醒(prvUnlockQueue里 queue.c:1887 那个 while( cRxLock > queueLOCKED_UNMODIFIED ) 循环),最后清零回 queueUNLOCKED。

(6)消息队列常用函数讲解

1.消息队列创建函数 xQueueCreate()

xQueueCreate()用于创建一个新的队列并返回可用于访问这个队列的队列句柄。队列句柄其实就是一个指向队列数据结构类型的指针(也就是消息队列控制块,这跟创建任务的操作是类似的)。

队列就是一个数据结构,用于任务间的数据的传递。每创建一个新的队列都需要为其分配 RAM,一部分用于存储队列的状态,剩下的作为队列消息的存储区域。使用xQueueCreate()创建队列时,使用的是动态内存分配,所以要想使用该函数必须 在FreeRTOSConfig.h 中把 configSUPPORT_DYNAMIC_ALLOCATION 定义为 1 来使能,这是个用于使能动态内存分配的宏,通常情况下,在 FreeRTOS 中,凡是创建任务,队列,信号量和互斥量等内核对象都需要使用动态内存分配,所以这个宏默认在 FreeRTOS.h 头文件中已经使能(即定义为 1) 。如果想使用静态内存,则可以使用 xQueueCreateStatic() 函数来创建一个队列。使用静态创建消息队列函数创建队列时需要的形参更多,需要的内存

由编译的时候预先分配好,一般很少使用这种方法

(1)xQueueCreate()函数:

c 复制代码
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
	#define xQueueCreate( uxQueueLength, uxItemSize ) xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
#endif

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )

其内部源码如下:

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 )
		{
			/* There is not going to be a queue storage area. */
			xQueueSizeInBytes = ( size_t ) 0;
		}
		else
		{
			/* Allocate enough space to hold the maximum number of items that
			can be in the queue at any time. */
			xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
		}

		pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );

		if( pxNewQueue != NULL )
		{
			/* Jump past the queue structure to find the location of the queue
			storage area. */
			pucQueueStorage = ( ( uint8_t * ) pxNewQueue ) + sizeof( Queue_t );

			#if( configSUPPORT_STATIC_ALLOCATION == 1 )
			{
				/* Queues can be created either statically or dynamically, so
				note this task was created dynamically in case it is later
				deleted. */
				pxNewQueue->ucStaticallyAllocated = pdFALSE;
			}
			#endif /* configSUPPORT_STATIC_ALLOCATION */

			prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );
		}

		return pxNewQueue;
	}

#endif /* configSUPPORT_STATIC_ALLOCATION */

(1):这个参数是给调试/追踪用的铭牌,只有开启 configUSE_TRACE_FACILITY 时才会被存进结构体。我们调用各类 API时不用手填它------各创建函数自动填好:xQueueCreate 填 queueQUEUE_TYPE_BASE,互斥量的创建函数填 queueQUEUE_TYPE_MUTEX......

(2)断言,如果传入的队列长度大于0就继续执行,否则警告

(3)这一步是判断传入的单个消息大小否等于0,等于0,消息空间大小就等于0,否则消息空间的大小就等于:队列长度 × 单个消息大小(uxQueueLength × uxItemSize,单位字节)

(4)FreeRTOS 调用 pvPortMalloc()函数向系统申请内存空间,内存大小为消息队列控制块大小加上消息存储空间大小,因为这段内存空间是需要保证连续的(这个保证这段内存空间是连续的意思是指:消息空间大小必须是连续的,因为环形机制的指针行走(pcWriteTo += uxItemSize、撞 pcTail 绕回)和 memcpy 都建立在"这 len×uxItemSize 字节是线性排列"的假设上。而消息队列控制块和消息空间大小要不要连在一起,这个不做强制要求,我们在学习消息队列控制块结构体也看到了那四个指针都指向了这个消息空间,这跟我们前面学习创建任务,任务控制块TCB成员保存有任务栈信息是一样的)

那为什么动态创建队列是一起分配空间,而且动态创建任务确实先给任务栈分配空间再给任务TCB分配空间呢?好处如下:

  1. 省开销:一次 malloc 只付一个堆块头;队列对象小而多(信号量、互斥量全是它),省下来可观;
  2. 一次成败:不用写回滚代码(对比任务那两段 706/732);
  3. 删除省事:vQueueDelete 一次 vPortFree 清干净,不用记两个指针、判断哪块该不该还。

总结:同一个模式,三种取舍

对象 管理结构 工作缓冲区 分配 谁记着缓冲区地址
任务 TCB 栈 两块(申请顺序按栈生长方向排,防溢出踩 TCB) pxStack
动态队列 Queue_t 存储区 一块(一次申请总和) pcHead
静态队列 StaticQueue_t 用户数组 两块(用户各给各的) pcHead

(5)如果创建了队列空间就进入该if()语句

(6)计算消息空间的首地址 pucQueueStorage。前面讲到,消息队列控制块和消息存储空间是一起分配的,所以 pvPortMalloc() 返回的 pxNewQueue 只是这一整块连续内存的首地址,也就是消息队列控制块的首地址,消息空间的地址并不能直接得到。因此需要手动计算:先把 pxNewQueue 强转成 uint8_t *,让指针按字节进行运算;再加上消息队列控制块的大小 sizeof( Queue_t ),指针便会向后跳过整个控制块,正好落在消息存储空间的起始位置上。

简单理解成:消息空间首地址 = 整块内存首地址 + 控制块大小。

(2)prvInitialiseNewQueue()函数

(8)这一步跟我们创建任务调用prvInitialiseNewTask()函数在里面对任务TCB的成员赋值操作类似,源代码如下:

c 复制代码
static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, uint8_t *pucQueueStorage, const uint8_t ucQueueType, Queue_t *pxNewQueue )
{
	/* Remove compiler warnings about unused parameters should
	configUSE_TRACE_FACILITY not be set to 1. */
	( void ) ucQueueType;

	if( uxItemSize == ( UBaseType_t ) 0 )
	{
		/* No RAM was allocated for the queue storage area, but PC head cannot
		be set to NULL because NULL is used as a key to say the queue is used as
		a mutex.  Therefore just set pcHead to point to the queue as a benign
		value that is known to be within the memory map. */
		pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;
	}
	else
	{
		/* Set the head to the start of the queue storage area. */
		pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage;
	}

	/* Initialise the queue members as described where the queue type is
	defined. */
	pxNewQueue->uxLength = uxQueueLength;
	pxNewQueue->uxItemSize = uxItemSize;
	( void ) xQueueGenericReset( pxNewQueue, pdTRUE );

	#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 );
}

先分析我们的参数:

uxQueueLength:消息队列长度。

uxItemSize:单个消息大小

pucQueueStorage:消息空间的起始地址

ucQueueType:消息队列类型:

 queueQUEUE_TYPE_BASE:表示队列 。

 queueQUEUE_TYPE_SET:表示队列集合 。

 queueQUEUE_TYPE_MUTEX:表示互斥量。

 queueQUEUE_TYPE_COUNTING_SEMAPHORE:表示计数信号量 。

 queueQUEUE_TYPE_BINARY_SEMAPHORE:表示二进制信号量 。

 queueQUEUE_TYPE_RECURSIVE_MUTEX :表示递归互斥量。

(注意:ucQueueType 只是给调试器看的铭牌。与我们后面内核运行时的所有判断都无关)

pxNewQueue:消息队列控制块首地址

(1)这段逻辑根据"有没有为队列分配消息存储空间"分两种情况给 pcHead 赋值。

先看判断条件:当 uxItemSize == 0 时,说明没有为消息分配存储空间。此时 pcHead 不能设置为 NULL,因为 NULL 已经被内核预定为"当前队列用作互斥量"的暗号。所以这里只能让 pcHead 指向一个已知的、位于内存映射范围内的区域,也就是指向消息队列控制块 pxNewQueue 自己。

如果分配了存储消息的内存空间,则正常设置 pcHead 指向消息存储空间的起始地址 pucQueueStorage。

补充:从这段就能看出,pcHead 会成为后续内核判断"该队列是否用作互斥量"的暗号。相关的两个"暗号"宏定义如下:

c 复制代码
/* ... When the Queue_t structure is used to represent a mutex pcHead and pcTail
pointers are not necessary, and the pcHead pointer is set to NULL to indicate
that the pcTail pointer actually points to the mutex holder (if any). ... */

#define pxMutexHolder         pcTail   // 互斥量的"持有者"存在 pcTail 那格
#define uxQueueType           pcHead   // "是不是互斥量"存在 pcHead 那格
#define queueQUEUE_IS_MUTEX   NULL     // 暗号的值 = NULL

也就是说:当 pcHead == NULL 时,表示该队列用作互斥量;此时 pcTail 不再是普通队列的"存储区末尾标记",而是指向获得互斥量的任务的 TCB,即 pxMutexHolder。

之所以要这样设计,关键原因是:不只是互斥量的 uxItemSize == 0、没有消息存储空间,信号量等很多内核对象也一样。如果这些 uxItemSize == 0 的对象都把 pcHead 填成 NULL,内核就会把它们误认成互斥量------比如在释放(give)时会错误地走 xTaskPriorityDisinherit(pxMutexHolder) 分支,把 pcTail 里那个根本不是持有者的值当成任务句柄处理,直接乱套。

所以对于这些"没有消息空间、但不是互斥量"的对象,这个分支只能塞一个"良性非 NULL"值:让 pcHead 指向控制块自己。这样既保证地址有效,又不会触发互斥量判断;反正此时 uxItemSize == 0,没人会拿 pcHead 去 memcpy。

而真正的互斥量则是:先同样走这个分支,把 pcHead 暂时指向自己;随后在 prvInitialiseMutex() 中通过一行代码:

c 复制代码
pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX;

展开后等价于:

c 复制代码
pxNewQueue->pcHead = NULL;

这样才正式宣告"我是互斥量"。这部分已经超出我们目前对源码的理解范围,等后续研究互斥量相关源码时再深入展开。

(2)(3):将创建好的队列空间消息赋值给消息队列控制块:消息队列的长度与消息的大小(为什么只初始化这两个,因为后面的部分成员的参数都是基于这两个成员得到的)

(4)重置消息队列,在消息队列初始化的时候,需要重置一下相关参数,该函数源代码如下:

(3)xQueueGenericReset()函数

c 复制代码
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->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 there are tasks blocked waiting to read from the queue, then
			the tasks will remain blocked as after this function exits the queue
			will still be empty.  If there are tasks blocked waiting to write to
			the queue, then one should be unblocked as after this function exits
			it will be possible to write to it. */
			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
		{
			/* Ensure the event queues start in the correct state. */
			vListInitialise( &( pxQueue->xTasksWaitingToSend ) );
			vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );
		}
	}
	taskEXIT_CRITICAL();

	/* A value is returned for calling semantic consistency with previous
	versions. */
	return pdPASS;
}

(1):将传进来的消息队列控制块地址传给pxQueue

(2):重置消息队列的成员变量,pcTail 指向存储消息内存空间的结束地址。

(3):当前消息队列中的消息个数 uxMessagesWaiting 为 0。

(4):pcWriteTo 指向队列消息存储区下一个可用消息空间,因为是重置消息队列,就指向消息队列的第一个消息空间,也就是 pcHead 指向的空间。

(5):pcReadFrom 指向消息队列最后一个消息空间。

(6):消息队列没有上锁,设置为 queueUNLOCKED。

(7)上面这段"如果不是新建队列"的分支,其实就是 xQueueGenericReset() 第二个参数 xNewQueue 为 pdFALSE 时的处理逻辑。

创建队列时,prvInitialiseNewQueue() 传进来的是 pdTRUE;而 FreeRTOS 还给用户提供了一个运行时复位队列的 API:

c 复制代码
#define xQueueReset( xQueue )    xQueueGenericReset( ( xQueue ), pdFALSE )

那什么时候需要复位一个已经在运行的队列呢?比如通信协议出错、想快速清空队列重新开始,或者发生异常后想让队列回到"刚创建好的空状态"时,就可以调用 xQueueReset()。复位时不会重建两张等待链表,而是要处理好原本就阻塞在队列上的任务。

先看 pdFALSE 分支的判断条件:

c 复制代码
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )

也就是说,只要还有任务因为"队列满、等着发消息"而阻塞,就要把它从等待发送链表中捞出来。捞任务的工作交给 xTaskRemoveFromEventList(),我们进到函数里看一下。

这个函数的逻辑可以拆成三步:

第一步:取出优先级最高的阻塞任务。

等待链表 xTasksWaitingToSend 是按照任务优先级排序的,所以链表头的哨兵后面第一个节点,就是当前等待发送任务里优先级最高的那个。直接通过:

c 复制代码
pxUnblockedTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxEventList );

拿到它的 TCB。

第二步:把任务从事件链表摘下来,再放到正确的地方。

它先把这个任务的 xEventListItem 事件节点从事件链表(这里就是 xTasksWaitingToSend)里移除,然后判断调度器当前是否被挂起:

  • 调度器没有被挂起:把任务的 xStateListItem 状态节点从阻塞链表里移除,再插入就绪链表;
  • 调度器被挂起:因为现在不能直接操作就绪链表,就先把这个任务的 xEventListItem 事件节点插入到待处理就绪链表 xPendingReadyList。

这里正好和我们前面"恢复调度器"那部分呼应上了:一旦任务是因为调度器挂起又被事件唤醒就放入待处理就绪链表,而插入的一定是事件节点 xEventListItem,而不是状态节点 xStateListItem。

第三步:判断是否需要触发任务切换。

c 复制代码
if( pxUnblockedTCB->uxPriority > pxCurrentTCB->uxPriority )
{
    xReturn = pdTRUE;
    xYieldPending = pdTRUE;
}
else
{
    xReturn = pdFALSE;
}

如果刚刚被唤醒的任务优先级比当前正在运行的任务高,就返回 pdTRUE,并标记 xYieldPending,表示需要尽快进行一次任务切换;否则返回 pdFALSE,让唤醒的任务先在就绪链表里等着调度。

回到 xQueueGenericReset(),调用处的写法是:

c 复制代码
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
{
    queueYIELD_IF_USING_PREEMPTION();
}

意思就是:如果刚才捞出来的任务优先级更高,就立即请求切换。

那为什么复位的时候只唤醒"等待发送"的任务,不唤醒"等待接收"的任务?因为复位之后,队列被清空,又变成了一个空队列:

  • 原来因为队列满而阻塞的发送任务,现在有空位了,所以需要把它唤醒;
  • 原来因为队列空而阻塞的接收任务,复位后队列依然是空的,它读不到数据,所以即使唤醒它,它也会继续阻塞,没有必要动它。

(8)反过来,如果走的是 xNewQueue == pdTRUE 分支,说明现在是新建队列,队列里还不存在任何阻塞任务。这时只需要把两张等待链表初始化干净即可:

c 复制代码
vListInitialise( &( pxQueue->xTasksWaitingToSend ) );
vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );

为什么复位路径要多加 pdFALSE 那个分支,而不是简单地把两张链表重新初始化?这其实是为了避免"任务被冻结"的问题。

如果复位一个运行中的队列时,也像新建队列那样直接把 xTasksWaitingToSend 和 xTasksWaitingToReceive 两张链表初始化掉,就会把链表上挂着的阻塞任务全部"抹掉"。但这些任务此刻仍然处于阻塞态,它们的状态节点还留在阻塞链表上,只是事件节点已经不在任何等待链表里了。结果就是:以后无论队列收到消息还是发生其他操作,都没有任何路径能再找到它、唤醒它,任务就永远卡死在阻塞态。

所以复位路径必须走"交接"而不是"重建":只重置队列的数据指针和计数,已经阻塞在等待链表上的任务要继续保留在原链表中,再根据情况唤醒其中"等待发送"的任务。

顺便补充一下,队列复位时,控制块里到底复位了哪些内容:

  • 三个指针归位:
    • pcTail 重新指向存储区末尾;
    • pcWriteTo 重新指向 pcHead,也就是第一个消息空间;
    • u.pcReadFrom 重新指向最后一个消息空间;
  • uxMessagesWaiting = 0:队列里原有的数据被直接丢弃,并且不会给任何任务发通知;
  • cRxLock / cTxLock 清零,恢复成 queueUNLOCKED,表示队列当前没有上锁。

最后xQueueGenericCreate()函数调用prvInitialiseNewQueue()函数将消息队列控制块(也就是队列句柄)初始化好后,将该任务句柄返回,所以我们要创建一个队列句柄来获取xQueueGenericCreate()函数返回的队列句柄。

(4)小总结:

简单来说,队列就是一个环形缓冲区。我们创建队列,本质上就是申请一块能存放消息的连续存储区;而创建队列句柄,就是拿到这块存储区的使用权。任务拿到这个使用权就可以通过这块存储区发送和接收消息。

如果底层还是同一个队列结构,但不再真正搬运消息数据,只通过 uxMessagesWaiting 这个计数值来表示"当前可用资源有几个",那它就变成了信号量;如果又进一步把这个计数值的最大值限制为 1,再加上优先级继承机制,那它就变成了互斥量。

最后,把这三个函数串起来看整体调用链:

第一步:xQueueCreate() / xQueueGenericCreate() 做"外部准备"。

  1. 根据我们传入的队列长度和单个消息大小,调用 pvPortMalloc() 为"消息队列控制块 + 消息存储空间"分配一块动态内存;
  2. 再通过"整块首地址 + 控制块大小"计算出消息空间的首地址 pucQueueStorage;
  3. 最后把这些参数传入 prvInitialiseNewQueue() 函数。

第二步:prvInitialiseNewQueue() 做"关键成员初始化"。

  1. 根据是否分配了消息存储空间,设置 pcHead 的指向;
  2. 将 uxQueueLength(队列长度)、uxItemSize(单个消息大小)赋值给消息队列控制块;
  3. 再把初始化好部分成员的消息队列控制块和这些参数传入 xQueueGenericReset() 函数,由它接着处理。

第三步:xQueueGenericReset() 做"收尾重置"。

按传进来的参数继续初始化消息队列控制块中剩余的成员,包括指针归位、消息个数清零、锁状态恢复等。

调用代码示例:

c 复制代码
QueueHandle_t Queue1_Handle;

/*创建应用任务*/
void APP_CreatTask(void* parameter)
{
	xTaskCreate(Task1,"Task1",128,NULL,1,&Task1_Handle);
	xTaskCreate(Task2,"Task2",128,NULL,1,NULL);
	Queue1_Handle=xQueueCreate(100,1);
	vTaskDelete(APP_TaskHandel);
}

2.消息队列静态创建函数 xQueueCreateStatic()

xQueueCreateStatic()用于创建一个新的队列并返回可用于访问这个队列的队列句柄。队列句柄其实就是一个指向队列数据结构类型的指针。

队列就是一个数据结构,用于任务间的数据的传递。每创建一个新的队列都需要为其分 配 RAM , 一 部 分 用 于 存 储 队 列 的 状 态 , 剩 下 的 作 为 队 列 的 存 储 区 。 使 用xQueueCreateStatic()创建队列时,使用的是静态内存分配,所以要想使用该函数必须在FreeRTOSConfig.h 中把 configSUPPORT_STATIC_ALLOCATION 定义为 1 来使能。这是个用于使能静态内存分配的宏,需要的内存在程序编译的时候分配好,由用户自己定义,其实创建过程与 xQueueCreate()都是差不多的。

xQueueCreateStatic()函数宏定义:

c 复制代码
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
	#define xQueueCreateStatic( uxQueueLength, uxItemSize, pucQueueStorage, pxQueueBuffer ) xQueueGenericCreateStatic( ( uxQueueLength ), ( uxItemSize ), ( pucQueueStorage ), ( pxQueueBuffer ), ( queueQUEUE_TYPE_BASE ) )
#endif /* configSUPPORT_STATIC_ALLOCATION */

xQueueGenericCreateStatic()函数:

c 复制代码
#if( configSUPPORT_STATIC_ALLOCATION == 1 )

	QueueHandle_t xQueueGenericCreateStatic( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, uint8_t *pucQueueStorage, StaticQueue_t *pxStaticQueue, const uint8_t ucQueueType )
	{
	Queue_t *pxNewQueue;

		configASSERT( uxQueueLength > ( UBaseType_t ) 0 );

		/* The StaticQueue_t structure and the queue storage area must be
		supplied. */
		configASSERT( pxStaticQueue != NULL );

		/* A queue storage area should be provided if the item size is not 0, and
		should not be provided if the item size is 0. */
		configASSERT( !( ( pucQueueStorage != NULL ) && ( uxItemSize == 0 ) ) );
		configASSERT( !( ( pucQueueStorage == NULL ) && ( uxItemSize != 0 ) ) );

		#if( configASSERT_DEFINED == 1 )
		{
			/* Sanity check that the size of the structure used to declare a
			variable of type StaticQueue_t or StaticSemaphore_t equals the size of
			the real queue and semaphore structures. */
			volatile size_t xSize = sizeof( StaticQueue_t );
			configASSERT( xSize == sizeof( Queue_t ) );
		}
		#endif /* configASSERT_DEFINED */

		/* The address of a statically allocated queue was passed in, use it.
		The address of a statically allocated storage area was also passed in
		but is already set. */
		pxNewQueue = ( Queue_t * ) pxStaticQueue; /*lint !e740 Unusual cast is ok as the structures are designed to have the same alignment, and the size is checked by an assert. */

		if( pxNewQueue != NULL )
		{
			#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
			{
				/* Queues can be allocated wither statically or dynamically, so
				note this queue was allocated statically in case the queue is
				later deleted. */
				pxNewQueue->ucStaticallyAllocated = pdTRUE;
			}
			#endif /* configSUPPORT_DYNAMIC_ALLOCATION */

			prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );
		}

		return pxNewQueue;
	}

#endif /* configSUPPORT_STATIC_ALLOCATION */

我们只用研究xQueueGenericCreateStatic()函数的源码实现就行了,因为后面的跟动态创建队列是一样的,静态创建只是在给队列控制块和信息空间大小的分配栈的方式不一样,所以我就简单说一下:

对比xQueueCreat()函数,xQueueGenericCreateStatic()函数多了两个参数:1.信息空间的首地址,2.队列控制块的首地址

(1)函数入口处先放了四个 configASSERT,用来校验调用者传进来的参数是否合法:

c 复制代码
configASSERT( uxQueueLength > ( UBaseType_t ) 0 );                        /* 队列长度必须大于 0 */
configASSERT( pxStaticQueue != NULL );                                    /* 静态队列控制块地址必须传入 */
configASSERT( !( ( pucQueueStorage != NULL ) && ( uxItemSize == 0 ) ) );  /* 有消息空间时,单个消息大小不能为 0 */
configASSERT( !( ( pucQueueStorage == NULL ) && ( uxItemSize != 0 ) ) );  /* 没有消息空间时,单个消息大小必须为 0 */
  • 第一个断言检查队列长度 uxQueueLength 是否大于 0;

  • 第二个断言检查静态队列控制块首地址 pxStaticQueue 是否传进来了;

  • 后面两个断言是在约束"消息空间地址"与"单个消息大小"的组合关系:

    1. 如果你传入了消息空间 pucQueueStorage != NULL,那么单个消息大小 uxItemSize 就不应该为 0;
    2. 如果你没有传入消息空间 pucQueueStorage == NULL,那么单个消息大小 uxItemSize 就应该为 0。

只有这两种组合才被认为是合法配置,任何一个条件不满足,断言都会直接卡住,函数无法继续往下执行。

(2)接着是一个"结构体大小校验"断言。它先取你声明的 StaticQueue_t 结构的字节大小,再和内核真正使用的 Queue_t 结构体字节大小比较,两者相等才会继续执行。目的是确认用户提供的静态队列控制块,和内核内部的队列控制块在内存尺寸、布局上完全一致;否则后面把这块内存强转成 Queue_t * 去读写成员时,就可能越界或访问到错误位置。

c 复制代码
volatile size_t xSize = sizeof( StaticQueue_t );
configASSERT( xSize == sizeof( Queue_t ) );

(3)校验通过后,把从外部传进来的静态队列控制块首地址 pxStaticQueue 强转成 Queue_t * 类型,再赋值给 pxNewQueue。也就是说,从这一步开始,后续代码统一使用 pxNewQueue 这个内核队列类型来操作这块静态内存。

(4)最后判断 pxNewQueue 是否为 NULL。只要不为空,就调用 prvInitialiseNewQueue() 函数,接下来便和动态创建队列走完全相同的初始化流程了。

3.消息队列删除函数 vQueueDelete()

队列删除函数是根据消息队列句柄直接删除的,删除之后这个消息队列的所有信息都会被系统回收清空,而且不能再次使用这个消息队列了,但是需要注意的是,如果某个消息队列没有被创建,那也是无法被删除的,动脑子想想都知道,没创建的东西就不存在,怎么可能被删除。xQueue 是 vQueueDelete()函数的形参,是消息队列句柄,表示的是要删除哪个想队列

c 复制代码
void vQueueDelete( QueueHandle_t xQueue )
{
Queue_t * const pxQueue = ( Queue_t * ) xQueue;

	configASSERT( pxQueue );
	traceQUEUE_DELETE( pxQueue );

	#if ( configQUEUE_REGISTRY_SIZE > 0 )
	{
		vQueueUnregisterQueue( pxQueue );
	}
	#endif

	#if( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 0 ) )
	{
		/* The queue can only have been allocated dynamically - free it
		again. */
		vPortFree( pxQueue );
	}
	#elif( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 1 ) )
	{
		/* The queue could have been allocated statically or dynamically, so
		check before attempting to free the memory. */
		if( pxQueue->ucStaticallyAllocated == ( uint8_t ) pdFALSE )
		{
			vPortFree( pxQueue );
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
	#else
	{
		/* The queue must have been statically allocated, so is not going to be
		deleted.  Avoid compiler warnings about the unused parameter. */
		( void ) pxQueue;
	}
	#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
}

可以看到这个函数实现很简单,首先是队列注册表注销------调试花名册(configQUEUE_REGISTRY_SIZE 决定大小,本工程 10 格),删除对象时把"句柄→名字"登记清空;不开就是调试设施,不影响调度。

然后判断你是否支持静态内存,支持就走第二个分支,不支持走第一个分支,在上面介绍静态创建队列函数的源码时有一段代码:

c 复制代码
			#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
			{
				/* Queues can be allocated wither statically or dynamically, so
				note this queue was allocated statically in case the queue is
				later deleted. */
				pxNewQueue->ucStaticallyAllocated = pdTRUE;
			}

可以看到当支持静态内存时ucStaticallyAllocated会被设置为pdTRUE,所以第二个分支不调用vPortFree( )函数,队列控制块和信息空间是你创建,它不可能帮你释放掉吧,跟我们删除任务时调用prvDeleteTCB()函数,"盖章"和"验章"流程是一样的

(1)vPortFree()函数

c 复制代码
void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;

	if( pv != NULL )
	{
		/* The memory being freed will have an BlockLink_t structure immediately
		before it. */
		puc -= xHeapStructSize;

		/* This casting is to keep the compiler from issuing warnings. */
		pxLink = ( void * ) puc;

		/* Check the block is actually allocated. */
		configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
		configASSERT( pxLink->pxNextFreeBlock == NULL );

		if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 )
		{
			if( pxLink->pxNextFreeBlock == NULL )
			{
				/* The block is being returned to the heap - it is no longer
				allocated. */
				pxLink->xBlockSize &= ~xBlockAllocatedBit;

				vTaskSuspendAll();
				{
					/* Add this block to the list of free blocks. */
					xFreeBytesRemaining += pxLink->xBlockSize;
					traceFREE( pv, pxLink->xBlockSize );
					prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
				}
				( void ) xTaskResumeAll();
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
}

这函数在我们后面学习内存管理在详细学习,现在只用知道调用它来释放空间就行了

或许有同学疑惑?为什么前面我们研究队列复位的情况时,就队列复位后,要将`xTasksWaitingToSend(发送消息等待链表)的任务移除并插到就绪链表或待处理就绪链表,防止后面进入直接初始化两个链表的分支而导致任务冻结,这里删除队列却不用考虑这种将任务冻结的情况呢?

这个问题问得非常到位,正好点中了 xQueueGenericReset() 和 vQueueDelete() 在"善后"逻辑上的本质区别。核心原因就一句话:复位是"让活着的队列继续干活",删除是"让队列彻底谢幕",两者的语义完全不同,所以对阻塞任务的处理方式也完全不同。

  1. 复位和删除的语义根本不是一回事
  • 复位:对象还活着、还要继续工作。唤醒一个"等发送"的任务,是为了让这个活着的队列继续正常服务------叫"业务需要",不是"善后"。
  • 删除:对象谢幕,此后无业务。这时候把任务唤醒,然后呢?让它对着一块已经释放的内存继续收发?没有归宿。唤醒 = 让程序沿一条已经错的路继续跑。
  1. 内核的"节点归属"原则
    为什么删任务要摘、删队列不摘?看"被销毁对象自己的节点,挂在谁家里":
场景 自己死的时候,节点状况 内核怎么办 后果
删任务 任务的 xEventListItem 挂在队列的等待链上 必须摘掉 不摘的话,队列以后会踩到已释放的 TCB------坑的是别人
删队列 队列没有节点挂在任务那边;挂在队列上的是任务们的节点 不碰 悬空指针在任务自己的 TCB 里------是"那些任务"的问题,队列没法替它们做主

删任务为什么必须摘节点?

任务被删除时,它的 TCB 和栈都会被 vPortFree() 释放掉。可如果这个任务此刻正阻塞在某个队列的 xTasksWaitingToSend 或 xTasksWaitingToReceive 等待链上,它的 xEventListItem 事件节点还挂在队列的链表里。队列的链表节点里存的是"指向这个 TCB 的指针",TCB 一释放,这个指针就成了悬空指针。以后队列一旦有消息进来,内核会顺着链表找到这个节点,再通过节点里的指针去访问那个已经释放的 TCB------轻则读到脏数据,重则直接踩到被复用的内存,把别的任务的数据搞坏。所以删任务时,内核必须先把任务从它挂着的所有等待链上摘下来,这叫"善后",是为了不坑别人。

删队列为什么不摘?

反过来看,队列被删除时,队列自己身上并没有任何节点挂在任务那边。挂在队列等待链上的,是那些阻塞任务的 xEventListItem 节点。这些节点是任务自己的,节点里存的是"指向任务 TCB 的指针"。队列被释放后,这些节点本身还留在任务的内存里(节点是任务 TCB 的一部分),并不会因为队列没了就变成悬空指针。真正悬空的是任务 TCB 里指向队列的指针------比如任务醒来后还想往这个队列发消息,但队列已经没了。可这个悬空指针在任务自己的 TCB 里,是"那些任务"自己的问题,队列已经死了,没法替它们做主。内核在删队列时,既没有义务、也没有能力去挨个通知那些阻塞的任务"你们的队列没了"。

一句话总结:谁死,谁负责把自己挂在别人家里的节点收走;别人挂在自己身上的节点,自己死了也管不着。

调用代码示例:

c 复制代码
/*创建应用任务*/
void APP_CreatTask(void* parameter)
{
	xTaskCreate(Task1,"Task1",128,NULL,1,&Task1_Handle);
	xTaskCreate(Task2,"Task2",128,NULL,1,NULL);
	Queue1_Handle=xQueueCreate(100,1);
	if(Queue1_Handle!=NULL)
	{
		printf("队列创建成功\r\n");
	}
	else
	{
		vQueueDelete(Queue1_Handle);
	}
	vTaskDelete(APP_TaskHandel);
}

4.向消息队列发送消息函数 :

介绍:xQueueSend()与 xQueueSendToBack() 函数

任务或中断服务程序都可以向消息队列发送消息。发送消息时,如果队列未满,或者允许覆盖入队,FreeRTOS 会将消息拷贝到当前 *pcWriteTo(写指针)指向的消息空间地址;否则,会根据用户指定的阻塞时间进行阻塞。在这段时间内,如果队列一直不允许入队,该任务将保持阻塞状态以等待队列允许入队。当其他任务从该队列中读取了数据(队列不再满)后,等待中的任务会自动由阻塞态转为就绪态。如果任务等待的时间超过了指定的阻塞时间,即使队列仍不允许入队,任务也会自动从阻塞态转移为就绪态,此时发送消息的任务或中断服务程序会收到错误码 errQUEUE_FULL。

这里也解释一下前面提到的紧急消息是如何发送的。与正常发送队列消息不同,发送紧急消息不是把消息拷贝到 *pcWriteTo(写指针)指向的消息空间地址,而是把新消息写入"读指针当前指向的那一格",然后将*pcReadFrom读指针往回退一格。这样,接下来的读取位置就指向了这个紧急插入的消息,相当于让这条紧急消息"插队"优先被读取。

实际上,消息队列的发送函数有好几个,它们都是通过宏定义展开的。其中有些只能在任务中调用,有些只能在中断服务程序中调用,具体将在下文讲解。

1. xQueueSend()与 xQueueSendToBack() (将数据发送到队尾函数)

这两个函数的宏定义如下:

c 复制代码
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )

#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )

(注意:这里队头是指pcReadFrom 的下一格(下一个被取走的),队尾pcWriteTo(写指针处)(新消息从这里进))

2.xQueueSendToFront() (将数据发送到队头函数)

该函数的宏定义如下:

c 复制代码
#define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_FRONT )

xQueueSend() 是一个宏,宏展开后调用的是 xQueueGenericSend() 函数,这个函数的具体实现过程将在后面详细讲解。该宏是为了向后兼容那些没有包含 xQueueSendToFront() 和 xQueueSendToBack() 这两个宏的 FreeRTOS 版本。xQueueSend() 等同于 xQueueSendToBack()。

xQueueSend() 用于向"当前 *pcWriteTo(写指针)指向的消息空间地址"发送一个队列消息。消息以拷贝的形式入队,而不是以引用的形式。该函数绝对不能在中断服务程序里被调用,中断中必须使用带有中断保护功能的 xQueueSendFromISR() 来代替(在前面学习 xTaskResumeFromISR() 函数时,我们已经大致了解过这两类函数的区别了)。

xQueueSendToFront() 也是一个宏,宏展开后调用的同样是 xQueueGenericSend() 函数。xQueueSendToFront() 用于向队列队首发送一个消息。消息以拷贝的形式入队,而不是以引用的形式。该函数同样绝不能在中断服务程序里被调用,必须使用带有中断保护功能的 xQueueSendToFrontFromISR() 来代替。xQueueSendToFront() 函数的使用方式与 xQueueSend() 函数一致。

这三个函数宏展开后调用的都是 xQueueGenericSend(),但 xQueueSend() 和 xQueueSendToBack() 在宏展开时传入的第三个参数是 queueSEND_TO_BACK,而 xQueueSendToFront() 传入的则是 queueSEND_TO_FRONT。从这里就可以看出:xQueueSend() 和 xQueueSendToBack() 是向队尾发送消息(也就是正常发送消息),而 xQueueSendToFront() 是向队头发送消息(也就是紧急发送消息)。

3.通用消息队列发送函数 xQueueGenericSend()

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;

	configASSERT( pxQueue );
	configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );
	configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) );
	#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
	{
		configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );
	}
	#endif


	/* This function relaxes the coding standard somewhat to allow return
	statements within the function itself.  This is done in the interest
	of execution time efficiency. */
	for( ;; )
	{
		taskENTER_CRITICAL();
		{
			/* Is there room on the queue now?  The running task must be the
			highest priority task wanting to access the queue.  If the head item
			in the queue is to be overwritten then it does not matter if the
			queue is full. */
			if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
			{
				traceQUEUE_SEND( pxQueue );
				xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );

				#if ( configUSE_QUEUE_SETS == 1 )
				{
					if( pxQueue->pxQueueSetContainer != NULL )
					{
						if( prvNotifyQueueSetContainer( pxQueue, xCopyPosition ) != pdFALSE )
						{
							/* The queue is a member of a queue set, and posting
							to the queue set caused a higher priority task to
							unblock. A context switch is required. */
							queueYIELD_IF_USING_PREEMPTION();
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					else
					{
						/* 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();
						}
					}
				}
				#else /* configUSE_QUEUE_SETS */
				{
					/* 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( xTicksToWait == ( TickType_t ) 0 )
				{
					/* The queue was full and no block time is specified (or
					the block time has expired) so leave now. */
					taskEXIT_CRITICAL();

					/* Return to the original privilege level before exiting
					the function. */
					traceQUEUE_SEND_FAILED( pxQueue );
					return errQUEUE_FULL;
				}
				else if( xEntryTimeSet == pdFALSE )
				{
					/* The queue was full and a block time was specified so
					configure the timeout structure. */
					vTaskSetTimeOutState( &xTimeOut );
					xEntryTimeSet = pdTRUE;
				}
				else
				{
					/* Entry time was already set. */
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
		taskEXIT_CRITICAL();

		/* Interrupts and other tasks can send to and receive from the queue
		now the critical section has been exited. */

		vTaskSuspendAll();
		prvLockQueue( pxQueue );

		/* Update the timeout state to see if it has expired yet. */
		if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
		{
			if( prvIsQueueFull( pxQueue ) != pdFALSE )
			{
				traceBLOCKING_ON_QUEUE_SEND( pxQueue );
				vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait );

				/* Unlocking the queue means queue events can effect the
				event list.  It is possible	that interrupts occurring now
				remove this task from the event	list again - but as the
				scheduler is suspended the task will go onto the pending
				ready last instead of the actual ready list. */
				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 a 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
			{
				/* Try again. */
				prvUnlockQueue( pxQueue );
				( void ) xTaskResumeAll();
			}
		}
		else
		{
			/* The timeout has expired. */
			prvUnlockQueue( pxQueue );
			( void ) xTaskResumeAll();

			traceQUEUE_SEND_FAILED( pxQueue );
			return errQUEUE_FULL;
		}
	}
}

(1):传入队列句柄,表示要向这个队列发送消息

(2):指针,指向要发送的消息

(3):指定阻塞超时时间。

(4):发送数据到消息队列的位置,有以下 3 个选择,在 queue.h 中有定义,queueSEND_TO_BACK:发送到队尾;queueSEND_TO_FRONT:发送到队头;queueOVERWRITE:以覆盖的方式发送。

(5):xYieldRequired我们在学习恢复调度器提到过,就是切换任务申请

(6):断言,对传入的参数进行判断

(7):进入临界段

(8)判断队列是否已满或者是否是覆盖模式,如果是未满或者覆盖模式就进入该if()语句(覆盖模式是指不管队列满没满,新任务都会从队头位置重新开始写入,也就是覆盖旧消息,所以不管满没满,都可以直接继续发送消息)

(9)调用prvCopyDataToQueue()函数(将数据拷贝到队列函数),该源码如下:
c 复制代码
static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue, const void *pvItemToQueue, const BaseType_t xPosition )
{
BaseType_t xReturn = pdFALSE;
UBaseType_t uxMessagesWaiting;

	/* This function is called from a critical section. */

	uxMessagesWaiting = pxQueue->uxMessagesWaiting;

	if( pxQueue->uxItemSize == ( UBaseType_t ) 0 )
	{
		#if ( configUSE_MUTEXES == 1 )
		{
			if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
			{
				/* The mutex is no longer being held. */
				xReturn = xTaskPriorityDisinherit( ( void * ) pxQueue->pxMutexHolder );
				pxQueue->pxMutexHolder = NULL;
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		#endif /* configUSE_MUTEXES */
	}
	else if( xPosition == queueSEND_TO_BACK )
	{
		( void ) memcpy( ( void * ) pxQueue->pcWriteTo, pvItemToQueue, ( size_t ) pxQueue->uxItemSize ); /*lint !e961 !e418 MISRA exception as the casts are only redundant for some ports, plus previous logic ensures a null pointer can only be passed to memcpy() if the copy size is 0. */
		pxQueue->pcWriteTo += pxQueue->uxItemSize;
		if( pxQueue->pcWriteTo >= pxQueue->pcTail ) /*lint !e946 MISRA exception justified as comparison of pointers is the cleanest solution. */
		{
			pxQueue->pcWriteTo = pxQueue->pcHead;
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
	else
	{
		( void ) memcpy( ( void * ) pxQueue->u.pcReadFrom, pvItemToQueue, ( size_t ) pxQueue->uxItemSize ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
		pxQueue->u.pcReadFrom -= pxQueue->uxItemSize;
		if( pxQueue->u.pcReadFrom < pxQueue->pcHead ) /*lint !e946 MISRA exception justified as comparison of pointers is the cleanest solution. */
		{
			pxQueue->u.pcReadFrom = ( pxQueue->pcTail - pxQueue->uxItemSize );
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		if( xPosition == queueOVERWRITE )
		{
			if( uxMessagesWaiting > ( UBaseType_t ) 0 )
			{
				/* An item is not being added but overwritten, so subtract
				one from the recorded number of items in the queue so when
				one is added again below the number of recorded items remains
				correct. */
				--uxMessagesWaiting;
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}

	pxQueue->uxMessagesWaiting = uxMessagesWaiting + 1;

	return xReturn;
}

(1)获取该队列的已有的消息数量赋值给uxMessagesWaiting

(2)判断这个队列的单个消息的大小是否为0

(3)如果这个队列的单个消息的大小不为0,并且如果是队尾传入

(4)memcpy()函数宏定义如下:

c 复制代码
  void * __cdecl memcpy(void * __restrict__ _Dst,const void * __restrict__ _Src,size_t _Size) __MINGW_ATTRIB_DEPRECATED_SEC_WARN;

这个代码逻辑是:把 src 处的 n 个字节拷到dst,这里 n = uxItemSize。

(5)接着将pcWriteTo挪动下一个位置,如果该位置是pcTail(尾),就回到pcHead(头)位置

(6)就是将传入的数据拷贝到队头(u.pcReadFrom),也就是紧急插入一个数据

(7)memcpy 先把新消息写进 pcReadFrom 当前所指的那一格(那格里是已被取走的旧数据,覆盖无害);然后 pcReadFrom 往后退一格(=uxItemSize)。这一退,让"下一个读取位置(pcReadFrom+1)"正好落到刚写进去的那格------新消息就成了队头。

(8)当 u.pcReadFrom 回退后超出了 pcHead(也就是小于消息空间的起始位置)时,说明读指针已经越过队首,需要把它回绕到消息空间的最后一个有效数据格,即 pxQueue->pcTail - pxQueue->uxItemSize。这里要特别注意:pcHead 指向的是消息空间中第一个数据格的起始地址,而 pcTail 指向的是整个消息空间的末尾边界,它本身并不是某一个数据格。因此,pcTail 的"上一个位置"并不是简单地减 1,而是 pcTail - uxItemSize,也就是从末尾边界往回退一个队列项大小的位置;这个位置的大小,正是由队列中单个消息的大小 uxItemSize 决定的,也是队列中最后一个可以存放消息的数据格。

(9)如果选择覆盖模式(不管队列满没满都会从队头位置写入)

(10)(11)这里意思是在覆盖模式下,上面已经把队头数据覆盖了,拷贝完成--,然后写入了新数据++,(在覆盖模式模式下,队列满写入的数据的数量不会增加的,因为是覆盖数据),如果不是覆盖模式就正常++

回到xQueueGenericSend()函数:

(10)如果使能队列集,我们没使能,所以跳过

(11)该函数宏定义如下:

c 复制代码
#define listLIST_IS_EMPTY( pxList )	( ( BaseType_t ) ( ( pxList )->uxNumberOfItems == ( UBaseType_t ) 0 ) )

就是如果该队列的xTasksWaitingToReceive链表上有任务,就进入该if()语句,这一步就是去等待接收数据链表找一个最高优先级的任务唤醒

(12)调用xTaskRemoveFromEventList()(将接收任务从事件链表移除函数),该函数源代码如下:
c 复制代码
BaseType_t xTaskRemoveFromEventList( const List_t * const pxEventList )
{
TCB_t *pxUnblockedTCB;
BaseType_t xReturn;

	/* THIS FUNCTION MUST BE CALLED FROM A CRITICAL SECTION.  It can also be
	called from a critical section within an ISR. */

	/* The event list is sorted in priority order, so the first in the list can
	be removed as it is known to be the highest priority.  Remove the TCB from
	the delayed list, and add it to the ready list.

	If an event is for a queue that is locked then this function will never
	get called - the lock count on the queue will get modified instead.  This
	means exclusive access to the event list is guaranteed here.

	This function assumes that a check has already been made to ensure that
	pxEventList is not empty. */
	pxUnblockedTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxEventList );
	configASSERT( pxUnblockedTCB );
	( void ) uxListRemove( &( pxUnblockedTCB->xEventListItem ) );

	if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
	{
		( void ) uxListRemove( &( pxUnblockedTCB->xStateListItem ) );
		prvAddTaskToReadyList( pxUnblockedTCB );
	}
	else
	{
		/* The delayed and ready lists cannot be accessed, so hold this task
		pending until the scheduler is resumed. */
		vListInsertEnd( &( xPendingReadyList ), &( pxUnblockedTCB->xEventListItem ) );
	}

	if( pxUnblockedTCB->uxPriority > pxCurrentTCB->uxPriority )
	{
		/* Return true if the task removed from the event list has a higher
		priority than the calling task.  This allows the calling task to know if
		it should force a context switch now. */
		xReturn = pdTRUE;

		/* Mark that a yield is pending in case the user is not using the
		"xHigherPriorityTaskWoken" parameter to an ISR safe FreeRTOS function. */
		xYieldPending = pdTRUE;
	}
	else
	{
		xReturn = pdFALSE;
	}

	#if( configUSE_TICKLESS_IDLE != 0 )
	{
		/* If a task is blocked on a kernel object then xNextTaskUnblockTime
		might be set to the blocked task's time out time.  If the task is
		unblocked for a reason other than a timeout xNextTaskUnblockTime is
		normally left unchanged, because it is automatically reset to a new
		value when the tick count equals xNextTaskUnblockTime.  However if
		tickless idling is used it might be more important to enter sleep mode
		at the earliest possible time - so reset xNextTaskUnblockTime here to
		ensure it is updated at the earliest possible time. */
		prvResetNextTaskUnblockTime();
	}
	#endif

	return xReturn;
}

1.这一步我们已经很熟悉了:调用 listGET_OWNER_OF_HEAD_ENTRY() 获取事件链表头节点(也就是哨兵节点下一个节点)所属的任务 TCB。

这里有一个容易困惑的地方:前面提到,无论是唤醒等待接收数据的任务,还是唤醒等待发送数据的任务,FreeRTOS 都会唤醒其中优先级最高的那个任务。但链表哨兵节点的下一个节点,不是通常对应优先级最低的任务吗?这看起来与前面的结论相反。其实,关键在于 xEventListItem(事件节点)中的 xItemValue 成员。我们之前学习过,在 xStateListItem(状态节点)中,xItemValue 用来表示任务会被阻塞到哪个时刻;而在 xEventListItem(事件节点)中,xItemValue 则用来表示任务在事件链表中的优先级顺序。这是怎么做到的呢?相关宏定义如下:

c 复制代码
/* Event lists are always in priority order. */
#define listSET_LIST_ITEM_VALUE( pxListItem, xValue )	( ( pxListItem )->xItemValue = ( xValue ) )
listSET_LIST_ITEM_VALUE( &amp;( pxNewTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority ); 

#define listSET_LIST_ITEM_OWNER( pxListItem, pxOwner )		( ( pxListItem )->pvOwner = ( void * ) ( pxOwner ) )
listSET_LIST_ITEM_OWNER( &amp;( pxNewTCB->xEventListItem ), pxNewTCB );

可以看到,这两段代码的核心思想是:用系统支持的最高优先级减去任务自身的优先级,然后把结果赋给 xItemValue。也就是说,假设最高优先级为 31,某个任务的优先级也是 32,那么它的 xEventListItem.xItemValue 就等于 0;如果这个任务的优先级为 0,那么 xEventListItem.xItemValue 就等于 32。因此,事件链表会按照这些任务节点的 xItemValue 从小到大排序,xItemValue 越小,说明任务优先级越高,也就越靠近链表头部。这样就解决了"如果按通常方式排序,哨兵节点后面反而是优先级最低的任务"的问题,保证了链表哨兵节点的下一个任务一定是等待事件中优先级最高的任务。

既然说到这了,再把下面那两个函数定义解释一下:listSET_LIST_ITEM_OWNER 把任务 TCB 记进节点的 pvOwner 字段------listGET_OWNER_OF_HEAD_ENTRY 正是靠它从链表节点找回任务的 TCB。创建任务时两个节点都设了 owner

2.将该任务的xItemValue节点从事件链表移除

3.接下来判断调度器当前是否处于挂起状态。如果调度器没有挂起,就把该任务的 xStateListItem 节点从阻塞状态链表中移除,并插入到就绪链表;如果调度器已经挂起,则把该任务的 xEventListItem 节点插入到 xPendingReadyList(待处理就绪链表)中。这里再次印证了我在介绍 xTaskResumeAll() 函数源码时提到的结论:当调度器处于挂起期间唤醒任务时,FreeRTOS 是把该任务的 xEventListItem 节点插入到 xPendingReadyList 中,而不是直接放入正式的就绪链表。

这里还要注意一个细节:源码中调用的是 vListInsertEnd() 函数,也就是尾插法。这说明把任务插入到待处理就绪链表时,并不考虑该任务的优先级。原因在于,我们前面学习 xTaskResumeAll() 时已经分析过:调度器恢复运行后,会先检查待处理就绪链表中是否存在比当前运行任务优先级更高的任务,如果有就申请一次任务切换;至于其他任务,则会在恢复过程中被依次移入正式的就绪链表。

4.这步跟我上面说的同理,判断唤醒的任务是否比当前运行的任务高,是的话申请一次任务切换(注意我说的申请一次任务切换是"账单",而执行任务切换才是"一笔勾销")

5.返回

回到xQueueGenericSend()函数:

如果xTaskRemoveFromEventList()放回了任务申请,那就执行任务申请

(13)这里也是任务申请,只不过这里不是xTaskRemoveFromEventList()函数的销账,而是销前面prvCopyDataToQueue()函数的账,在prvCopyDataToQueue()函数中还有关于互斥量的代码逻辑,我没讲解,后面学习在了解,所以这里就是销互斥量的账

(14)退出临界段,发送一个数据完成,退出xQueueGenericSend()函数

(15)这里的是走队列满了,而且没选择覆盖模式的分支(也就是说要判断你的超时时间情况)

(16)如果设置的超时时间为0(也就是如果不能入队,就直接返回errQUEUE_FULL)

(17)vTaskSetTimeOutState()函数

如果设置有超时时间,并且是第一次进入循环(xEntryTimeSet 初始值为pdFALSE),那就调用vTaskSetTimeOutState()函数,了解这个函数内部之前,先了解定义这个xTimeOut变量结构体长啥样,它们的代码如下:

c 复制代码
typedef struct xTIME_OUT
{
	BaseType_t xOverflowCount;
	TickType_t xTimeOnEntering;
} TimeOut_t;

void vTaskSetTimeOutState( TimeOut_t * const pxTimeOut )
{
	configASSERT( pxTimeOut );
	pxTimeOut->xOverflowCount = xNumOfOverflows;
	pxTimeOut->xTimeOnEntering = xTickCount;
}

可以看到TimeOut_t结构体的成员有两个:1.进入阻塞时,记录节拍计数器已经溢出多少次,2.进入阻塞等待那一时刻,记录当时的系统节拍tick

从这可以了解到vTaskSetTimeOutState()函数就是初始化这两个成员

最后调用xEntryTimeSet = pdTRUE,也就是说进入第二次循环就不会走这个分支了

(18)退出临界段

(19)挂起调度器,开始走第二个分支任务,也就是阻塞等待入队,直到超时或者入队唤醒

(20)上锁,该函数内部如下:

c 复制代码
#define prvLockQueue( pxQueue )								\
	taskENTER_CRITICAL();									\
	{														\
		if( ( pxQueue )->cRxLock == queueUNLOCKED )			\
		{													\
			( pxQueue )->cRxLock = queueLOCKED_UNMODIFIED;	\
		}													\
		if( ( pxQueue )->cTxLock == queueUNLOCKED )			\
		{													\
			( pxQueue )->cTxLock = queueLOCKED_UNMODIFIED;	\
		}													\
	}														\
	taskEXIT_CRITICAL()

queueUNLOCKED初始值为-1,queueLOCKED_UNMODIFIED初始值为0

(21)xTaskCheckForTimeOut()函数(检查是否超时函数)

检查是否超时,调用xTaskCheckForTimeOut()函数,该函数内部如下:

c 复制代码
BaseType_t xTaskCheckForTimeOut( TimeOut_t * const pxTimeOut, TickType_t * const pxTicksToWait )
{
BaseType_t xReturn;

	configASSERT( pxTimeOut );
	configASSERT( pxTicksToWait );

	taskENTER_CRITICAL();
	{
		/* Minor optimisation.  The tick count cannot change in this block. */
		const TickType_t xConstTickCount = xTickCount;

		#if( INCLUDE_xTaskAbortDelay == 1 )
			if( pxCurrentTCB->ucDelayAborted != pdFALSE )
			{
				/* The delay was aborted, which is not the same as a time out,
				but has the same result. */
				pxCurrentTCB->ucDelayAborted = pdFALSE;
				xReturn = pdTRUE;
			}
			else
		#endif

		#if ( INCLUDE_vTaskSuspend == 1 )
			if( *pxTicksToWait == portMAX_DELAY )
			{
				/* If INCLUDE_vTaskSuspend is set to 1 and the block time
				specified is the maximum block time then the task should block
				indefinitely, and therefore never time out. */
				xReturn = pdFALSE;
			}
			else
		#endif

		if( ( xNumOfOverflows != pxTimeOut->xOverflowCount ) && ( xConstTickCount >= pxTimeOut->xTimeOnEntering ) ) /*lint !e525 Indentation preferred as is to make code within pre-processor directives clearer. */
		{
			/* The tick count is greater than the time at which
			vTaskSetTimeout() was called, but has also overflowed since
			vTaskSetTimeOut() was called.  It must have wrapped all the way
			around and gone past again. This passed since vTaskSetTimeout()
			was called. */
			xReturn = pdTRUE;
		}
		else if( ( ( TickType_t ) ( xConstTickCount - pxTimeOut->xTimeOnEntering ) ) < *pxTicksToWait ) /*lint !e961 Explicit casting is only redundant with some compilers, whereas others require it to prevent integer conversion errors. */
		{
			/* Not a genuine timeout. Adjust parameters for time remaining. */
			*pxTicksToWait -= ( xConstTickCount - pxTimeOut->xTimeOnEntering );
			vTaskSetTimeOutState( pxTimeOut );
			xReturn = pdFALSE;
		}
		else
		{
			xReturn = pdTRUE;
		}
	}
	taskEXIT_CRITICAL();

	return xReturn;
}

1.这是初始化好的判断是否超时的时间结构体

2.设置的阻塞时间

3.进入临界段

4.如果设置的阻塞时间为无穷,就一直死等,返回pdFALSE(pdFALSE代表任务继续阻塞)

5.这里需要两个条件同时成立 才会进入该 if() 语句。

条件 1:( xNumOfOverflows != pxTimeOut->xOverflowCount )

xNumOfOverflows 什么时候会改变?它会在 taskSWITCH_DELAYED_LISTS() 函数中进行自增。那什么情况下会调用 taskSWITCH_DELAYED_LISTS() 呢?如下:

c 复制代码
  /* Increment the RTOS tick, switching the delayed and
  overflowed delayed lists if it wraps to 0. */
  xTickCount = xConstTickCount;
  if( xConstTickCount == ( TickType_t ) 0U )   // ← 32 位计数回绕的那一下
  {
      taskSWITCH_DELAYED_LISTS();               // → 宏里 xNumOfOverflows++
  }

从上可知,当 xConstTickCount 这个系统节拍计数器发生溢出、重新回到 0 的那一瞬间,xNumOfOverflows 就会加 1。

所以条件 1 的意思是:当前系统节拍计数器的溢出次数,已经和我们最初记录阻塞时间时的溢出次数不同了。

条件 2:( xConstTickCount >= pxTimeOut->xTimeOnEntering )

它的意思是:当前系统节拍计数器的值,已经大于或等于我们上一次记录任务进入阻塞时的 tick 值。

也就是说,当这两个条件同时成立时,说明任务已经超时。这里再整合理解一下:

  • 情况一 :正常情况下,第一次进入阻塞时的 tick 时刻和预期的超时时刻都处在同一轮计数周期内,那么条件 1 不成立。虽然条件 2 成立(当前 tick 时刻大于上一次记录任务进入阻塞时的 tick 时刻),但因为是 && 同时成立才会进入该 if(),所以整体不成立,任务还没有超时。

  • 情况二:第一次进入阻塞时的 tick 时刻和预期的超时时刻不在同一轮计数周期内,此时条件 1 成立。但条件 2 不一定成立,因为 tick 已经重新计数,当前系统 tick时刻通常不会大于你上一次记录任务进入阻塞时的 tick 时刻;只有当当前 tick 值再次增大并超过原来记录的值时,条件 2 才会成立,此时才会真正判定为超时。

所以这一步是判断系统时间计数器溢出的情况,因为这种情况会导致后面(6)计算出现错误

6.这是 else if 分支,判断条件为:

c 复制代码
( ( TickType_t ) ( xConstTickCount - pxTimeOut->xTimeOnEntering ) ) < *pxTicksToWait

它的含义是:从上次记录进入阻塞的时刻到现在,实际已经等待的 tick 数为 xConstTickCount - pxTimeOut->xTimeOnEntering。如果这个已经等待的时间小于我们设置的剩余阻塞时间 *pxTicksToWait,就说明当前还没到超时唤醒的时间,任务还需要继续阻塞。

这个时候会执行:

c 复制代码
*pxTicksToWait -= ( xConstTickCount - pxTimeOut->xTimeOnEntering );
vTaskSetTimeOutState( pxTimeOut );
xReturn = pdFALSE;

具体过程如下:

  1. 用原来的剩余阻塞时间减去已经等待的 tick 数,得到本次循环之后还剩下的阻塞时间,再写回 *pxTicksToWait;
  2. 调用 vTaskSetTimeOutState(),把超时判断的基准更新为当前 tick,也就是重新记录当前的 xOverflowCount 和 xTimeOnEntering,这样下一次循环再判断是否超时时,就是基于"当前这一次进入阻塞的时刻"来重新计算;
  3. 返回 pdFALSE,表示任务还没有超时,需要继续阻塞等待入队。

这样下一次再进入 xTaskCheckForTimeOut() 时,就会使用更新后的剩余阻塞时间和新的超时基准继续判断。

否则就是超时了,返回pdTRUE,要唤醒任务

回到xQueueGenericSend()函数:

为了方便查看,重新将xQueueGenericSend()函数的分支图插入:

(22)调用prvIsQueueFull()函数,查看队列是否满了,该函数内部如下:

c 复制代码
static BaseType_t prvIsQueueFull( const Queue_t *pxQueue )
{
BaseType_t xReturn;

	taskENTER_CRITICAL();
	{
		if( pxQueue->uxMessagesWaiting == pxQueue->uxLength )
		{
			xReturn = pdTRUE;
		}
		else
		{
			xReturn = pdFALSE;
		}
	}
	taskEXIT_CRITICAL();

	return xReturn;
}

这个函数很简单,就是判断队列的uxMessagesWaiting成员(记录当前数据个数),如果uxMessagesWaiting等于队列长度,那就是满了,返回

pdTRUE,否则返回pdFALSE

所以如果队列满了就接着走if()语句的分支:

(23)vTaskPlaceOnEventList()函数(将任务插入到事件链表函数)

队列满了就调用vTaskPlaceOnEventList()函数,该函数内部如下:

c 复制代码
void vTaskPlaceOnEventList( List_t * const pxEventList, const TickType_t xTicksToWait )
{
	configASSERT( pxEventList );

	/* THIS FUNCTION MUST BE CALLED WITH EITHER INTERRUPTS DISABLED OR THE
	SCHEDULER SUSPENDED AND THE QUEUE BEING ACCESSED LOCKED. */

	/* Place the event list item of the TCB in the appropriate event list.
	This is placed in the list in priority order so the highest priority task
	is the first to be woken by the event.  The queue that contains the event
	list is locked, preventing simultaneous access from interrupts. */
	vListInsert( pxEventList, &( pxCurrentTCB->xEventListItem ) );

	prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
}

既然队列满了,而任务又没有超时,那就将该任务的 xEventListItem 插到等待发送数据链表 xTasksWaitingToSend 中。这里可以看到调用的是 vListInsert() 函数(按序插入链表函数),而 vListInsert() 函数是按照节点的 xItemValue 来排序的,xItemValue 越小,越靠近哨兵节点。这里再次印证了前面提到的结论:等待发送数据链表和等待接收数据链表中的任务,越靠近哨兵节点,优先级越高。

为什么就绪链表不用 vListInsert() 函数,而是用尾插法函数呢?因为不同优先级各自拥有独立的就绪链表,同一个优先级的就绪链表中,所有任务的优先级都是相同的。为了公平,同一优先级的任务之间要使用尾插法,保证先就绪的任务先被运行,而同优先级任务之间轮流调度。而像 xTasksWaitingToSend、xTasksWaitingToReceive 这类等待链表,整个队列只有一个链表,但里面会挂载不同优先级的任务,所以必须用 vListInsert() 按 xItemValue 进行排序,这样才能保证链表头部始终是当前等待任务中优先级最高的那一个。

此外,vTaskPlaceOnEventList() 函数还做了第二件事:调用 prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE ),把当前任务从就绪链表移除,接着分两种情况:1.并按指定的超时时间挂入延迟链表。这样当前任务会一直保持阻塞状态,直到队列可用被唤醒,或者阻塞超时后被唤醒。2.如果传入的的是portMAX_DELAY,那么该任务会被挂入暂停任务链表,直到被事件唤醒

c 复制代码
if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )
{
	/* Add the task to the suspended task list instead of a delayed task
	list to ensure it is not woken by a timing event.  It will block
	indefinitely. */
	vListInsertEnd( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
}

这期间必须保证要么关闭中断,要么挂起调度器,而且队列已经上锁,因为"把任务挂进事件链表"和"把它从就绪表摘掉"必须原子完成,否则调度器会看到一个既不在就绪表、也没挂进事件表的任务

(24)prvUnlockQueue()解锁函数:

该函数内部如下:

c 复制代码
static void prvUnlockQueue( Queue_t * const pxQueue )
{
	/* THIS FUNCTION MUST BE CALLED WITH THE SCHEDULER SUSPENDED. */

	/* The lock counts contains the number of extra data items placed or
	removed from the queue while the queue was locked.  When a queue is
	locked items can be added or removed, but the event lists cannot be
	updated. */
	taskENTER_CRITICAL();
	{
		int8_t cTxLock = pxQueue->cTxLock;

		/* See if data was added to the queue while it was locked. */
		while( cTxLock > queueLOCKED_UNMODIFIED )
		{
			/* Data was posted while the queue was locked.  Are any tasks
			blocked waiting for data to become available? */
			#if ( configUSE_QUEUE_SETS == 1 )
			{
				if( pxQueue->pxQueueSetContainer != NULL )
				{
					if( prvNotifyQueueSetContainer( pxQueue, queueSEND_TO_BACK ) != pdFALSE )
					{
						/* The queue is a member of a queue set, and posting to
						the queue set caused a higher priority task to unblock.
						A context switch is required. */
						vTaskMissedYield();
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
				else
				{
					/* Tasks that are removed from the event list will get
					added to the pending ready list as the scheduler is still
					suspended. */
					if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
					{
						if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
						{
							/* The task waiting has a higher priority so record that a
							context	switch is required. */
							vTaskMissedYield();
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					else
					{
						break;
					}
				}
			}
			#else /* configUSE_QUEUE_SETS */
			{
				/* Tasks that are removed from the event list will get added to
				the pending ready list as the scheduler is still suspended. */
				if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
				{
					if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
					{
						/* The task waiting has a higher priority so record that
						a context switch is required. */
						vTaskMissedYield();
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
				else
				{
					break;
				}
			}
			#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();
}
/*-----------------------------------------------------------*/

static BaseType_t prvIsQueueEmpty( const Queue_t *pxQueue )
{
BaseType_t xReturn;

	taskENTER_CRITICAL();
	{
		if( pxQueue->uxMessagesWaiting == ( UBaseType_t )  0 )
		{
			xReturn = pdTRUE;
		}
		else
		{
			xReturn = pdFALSE;
		}
	}
	taskEXIT_CRITICAL();

	return xReturn;
}

#if ( configUSE_QUEUE_SETS == 1 )这一段是开启队列集才进入,我们还没开启,所以不分析

while( cTxLock > queueLOCKED_UNMODIFIED ): 如果cTxLock大于0,就进入该循环,前面上锁的时候我们不是设置为0吗?怎么会出现大于0的情况呢?这跟我们前面学习的调度器期间tick中断响应,不执行任何操作,单纯计数,后面调用vTaskResumeALL()一个个补tick是一样的操作,所以在上锁期间,如果来了个中断,这个中断又唤醒了发送队列或者接受队列的任务 ,这时肯定不能往队列写数据啊,也只能先记一笔帐,然后再解锁的时候一笔笔还

1.如果xTasksWaitingToReceive链表有任务就进入if()语句

2.这里调用xTaskRemoveFromEventList()函数,将xTasksWaitingToReceive链表优先级最高的任务唤醒,然后申请任务切换,上面讲过

3.如果没有链表没有任务就退出该if()语句

4.然后cTxLock--,补齐

5.补齐后,跳出while()循环,重新上锁

后面唤醒等待发送链表就不说了,操作是一样的

回到xQueueGenericSend()函数:

(25)恢复调度器,并判断开启调度器期间有没有申请过执行任务切换,如果没有,就"补刀",因为该发送任务进入阻塞状态了,绝对要执行任务切换的

(26)如果队列没满,上锁,恢复调度器,可能有同学疑惑,这里队列没满不应该走前面的发送数据,然后跳出循环的分支吗?怎么这里也有一个队列没满,关键就在于退出临界点,又开启调度器和上锁这"空窗期",假如这时候来了个中断,那这个中断刚刚好又读走这个队列的一个数据,那这个队列不就空了吗?所以这时就要重新走这个for(;;)循环,直到发送数据成功,或者走(27)任务超时

(27)解锁,恢复调度器,超时返回errQUEUE_FULL,跳出循环,注意:超时任务恢复就绪态是由xTaskIncrementTick() 帮完成的,在前面我们已经调用void vTaskPlaceOnEventList( )将任务阻塞时间传到xTaskIncrementTick(),可以规划一个时间线流程:

三拍时间线(超时路径)

拍1|SysTick 里(任务还在睡):xTaskIncrementTick 的超时循环发现"到点了",做三件事------

  • 把状态节点从延迟链表摘下来;
  • 若事件节点还挂在队列的等待链上,一并摘掉(tasks.c:2451-2453,就是你老朋友那段 "Is the task waiting on an event also?");
  • prvAddTaskToReadyList 插进就绪表。

此刻任务 = 就绪,但还没运行。这就是你说的"帮恢复"。

拍2|被调度器切回来:任务从当初 portYIELD 的地方醒(xQueueGenericSend 阻塞分支的末尾)→ 循环体跑完 → 回到 for(;😉 顶部。

拍3|重试与"结账":顶部临界区先查空间------

  • 有空间(睡觉期间有人腾了位)→ 直接发送成功,return pdPASS!
  • 没空间 → 再走一遍阻塞流程 → 走到 xTaskCheckForTimeOut 才判定"早已超时" → prvUnlockQueue + xTaskResumeAll → return errQUEUE_FULL。
(28)总结:
text 复制代码
xQueueGenericSend()                        ← queue.c(主角;for(;;) 从 742 行起)
  │
  ├─【第一段:临界区里的"快发"】
  │   ├─ prvCopyDataToQueue()                queue.c:1697(static 帮手)------真正的"搬数据"
  │   │   ├─ memcpy()                        后插→写 pcWriteTo 格;前插→写 pcReadFrom 格
  │   │   └─ xTaskPriorityDisinherit()       tasks.c------仅互斥量 give 时用(后面章节细看)
  │   ├─ xTaskRemoveFromEventList()          tasks.c:2894------从等待接收链表捞走最高优先级任务
  │   │   ├─ listGET_OWNER_OF_HEAD_ENTRY     宏------取哨兵后第一个节点(靠"倒写值"保证=最高优先级)
  │   │   ├─ uxListRemove()                  摘事件节点;调度器没挂起时再摘状态节点
  │   │   ├─ prvAddTaskToReadyList()         插就绪表;挂起时→vListInsertEnd 进 pending ready
  │   │   └─ 置 xYieldPending,返回"要不要切"
  │   └─ queueYIELD_IF_USING_PREEMPTION()    宏 = portYIELD_WITHIN_API()(点 PendSV)
  │
  ├─【第二段:发不了------为"睡"做准备】(退出临界区后)
  │   ├─ vTaskSetTimeOutState()              tasks.c:3007------快照 xNumOfOverflows + xTickCount
  │   ├─ vTaskSuspendAll()                   tasks.c:1944------挂起调度器
  │   ├─ prvLockQueue()                      宏 queue.c:264------上锁(cRx/cTxLock:-1 → 0)
  │   ├─ xTaskCheckForTimeOut()              tasks.c:3015------超时账本(回绕双条件 + 剩余扣减)
  │   │   └─ vTaskSetTimeOutState()          "没超时"分支里再快照一次 = 重新出发
  │   └─ prvIsQueueFull()                    queue.c 内的快查(带自己的小临界区)
  │
  ├─【第三段:真的睡------挂链 + 销账 + 让出】
  │   ├─ vTaskPlaceOnEventList()             tasks.c:2820------"把自己挂起来"(调用点见 2833)
  │   │   ├─ vListInsert()                   list.c:145------按倒写优先级插进 xTasksWaitingToSend
  │   │   └─ prvAddCurrentTaskToDelayedList() tasks.c(4700 行一带)
  │   │       ├─ uxListRemove()              从就绪表摘掉自己
  │   │       ├─ 有限超时:写唤醒时刻→插 xDelayedTaskList(按唤醒时刻排序)
  │   │       └─ portMAX_DELAY:vListInsertEnd(xSuspendedTaskList)------永不被 tick 唤醒
  │   ├─ prvUnlockQueue()                    queue.c:1794------销账:
  │   │   └─ xTaskRemoveFromEventList()      锁窗口里中断欠的唤醒,此刻补上
  │   ├─ xTaskResumeAll()                    tasks.c:2017------恢复调度器:
  │   │   ├─ pending ready 表 → 就绪表(prvAddTaskToReadyList)
  │   │   ├─ 补 xPendedTicks(xTaskIncrementTick 一个个来)
  │   │   └─ 视需要 taskYIELD_IF_USING_PREEMPTION()
  │   └─ portYIELD_WITHIN_API()              需要时点 PendSV------真正让出 CPU
  │
  【唤醒侧】(不在这个函数里,但和它成对------你睡着时发生的事)
  │   ├─ 超时到:SysTick → xTaskIncrementTick() → 把状态节点(+还在链表上的事件节点)摘下 → 回就绪
  │   └─ 有人收数据:xQueueReceive → xTaskRemoveFromEventList(&xTasksWaitingToSend) → 把你捞出来
  │        (若调度器正挂起 → 先挂 pending ready,等 xTaskResumeAll 统一处理)

看这张图要抓的两条线:

① 一条文件边界:prv 开头的帮手全在 queue.c(管数据------格子、指针、拷贝);vTask*/xTask* 全在 tasks.c(管任务------就绪/延迟/事件链表)。xQueueGenericSend 就是个胶水层:有空间时叫queue.c 的帮手搬数据,没空间时就向 tasks.c 求助把自己挂起来。

② 一句话主线:这个函数一辈子只干两件事------有空间:【拷贝 + 唤醒一个接收者】;没空间:【把自己交给调度器睡着,等别人用同一对设施(vTaskPlaceOnEventList /xTaskRemoveFromEventList)把自己捞回来】。三层结构(快发 → 备睡 → 睡)之外,全是围绕这两件事的账务(超时账、锁账)。

4.消息队列发送函数 xQueueGenericSendFromISR()(中断)

1.介绍:

xQueueSendFromISR ()和xQueueSendToBackFromISR ()中断向队列发送数据函数

既然有任务中发送消息的函数,当然也需要有在中断中发送消息函数,其实这个函数跟 xQueueGenericSend() 函 数 很 像 , 只 不 过 是 执 行 的 上下文环境是不一样的 ,xQueueGenericSendFromISR()函数只能用于中断中执行,是不带阻塞机制的xQueueSendFromISR ()和xQueueSendToBackFromISR ()的宏定义如下

c 复制代码
#define xQueueSendFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )

#define xQueueSendToBackFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )

所以xQueueSendToBackFromISR ()也等同于 xQueueSendFromISR ()。

2.xQueueGenericSendFromISR源码如下:

c 复制代码
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;

	configASSERT( pxQueue );
	configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );
	configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) );

	/* RTOS ports that support interrupt nesting have the concept of a maximum
	system call (or maximum API call) interrupt priority.  Interrupts that are
	above the maximum system call priority are kept permanently enabled, even
	when the RTOS kernel is in a critical section, but cannot make any calls to
	FreeRTOS API functions.  If configASSERT() is defined in FreeRTOSConfig.h
	then portASSERT_IF_INTERRUPT_PRIORITY_INVALID() will result in an assertion
	failure if a FreeRTOS API function is called from an interrupt that has been
	assigned a priority above the configured maximum system call priority.
	Only FreeRTOS functions that end in FromISR can be called from interrupts
	that have been assigned a priority at or (logically) below the maximum
	system call	interrupt priority.  FreeRTOS maintains a separate interrupt
	safe API to ensure interrupt entry is as fast and as simple as possible.
	More information (albeit Cortex-M specific) is provided on the following
	link: http://www.freertos.org/RTOS-Cortex-M3-M4.html */
	portASSERT_IF_INTERRUPT_PRIORITY_INVALID();

	/* Similar to xQueueGenericSend, except without blocking if there is no room
	in the queue.  Also don't directly wake a task that was blocked on a queue
	read, instead return a flag to say whether a context switch is required or
	not (i.e. has a task with a higher priority than us been woken by this
	post). */
	uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
	{
		if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
		{
			const int8_t cTxLock = pxQueue->cTxLock;

			traceQUEUE_SEND_FROM_ISR( pxQueue );

			/* Semaphores use xQueueGiveFromISR(), so pxQueue will not be a
			semaphore or mutex.  That means prvCopyDataToQueue() cannot result
			in a task disinheriting a priority and prvCopyDataToQueue() can be
			called here even though the disinherit function does not check if
			the scheduler is suspended before accessing the ready lists. */
			( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );

			/* The event list is not altered if the queue is locked.  This will
			be done when the queue is unlocked later. */
			if( cTxLock == queueUNLOCKED )
			{
				#if ( configUSE_QUEUE_SETS == 1 )
				{
					if( pxQueue->pxQueueSetContainer != NULL )
					{
						if( prvNotifyQueueSetContainer( pxQueue, xCopyPosition ) != pdFALSE )
						{
							/* The queue is a member of a queue set, and posting
							to the queue set caused a higher priority task to
							unblock.  A context switch is required. */
							if( pxHigherPriorityTaskWoken != NULL )
							{
								*pxHigherPriorityTaskWoken = pdTRUE;
							}
							else
							{
								mtCOVERAGE_TEST_MARKER();
							}
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					else
					{
						if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
						{
							if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
							{
								/* The task waiting has a higher priority so
								record that a context switch is required. */
								if( pxHigherPriorityTaskWoken != NULL )
								{
									*pxHigherPriorityTaskWoken = pdTRUE;
								}
								else
								{
									mtCOVERAGE_TEST_MARKER();
								}
							}
							else
							{
								mtCOVERAGE_TEST_MARKER();
							}
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
				}
				#else /* configUSE_QUEUE_SETS */
				{
					if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
					{
						if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
						{
							/* The task waiting has a higher priority so record that a
							context	switch is required. */
							if( pxHigherPriorityTaskWoken != NULL )
							{
								*pxHigherPriorityTaskWoken = pdTRUE;
							}
							else
							{
								mtCOVERAGE_TEST_MARKER();
							}
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
				#endif /* configUSE_QUEUE_SETS */
			}
			else
			{
				/* Increment the lock count so the task that unlocks the queue
				knows that data was posted while it was locked. */
				pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 );
			}

			xReturn = pdPASS;
		}
		else
		{
			traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue );
			xReturn = errQUEUE_FULL;
		}
	}
	portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );

	return xReturn;
}

(1)要发送的队列句柄

(2)要发送的单个数据

(3)这是xQueueSend()函数没有的参数,这个在我们学习xTaskResumeFromISR()函数的时候已经了解过了(想详细了解过程可以去看那章),这里简单说说:在前面学习xQueueSend()函数,我们知道如果发送完一个数据,并且等待接收链表有任务,那就要从这个链表中将优先级最高的任务唤醒,假如这个任务的优先级高于当前运行的任务的优先级,那我们就要发送一个任务切换请求,所以对应的就将*pxHigherPriorityTaskWoken设置成pdTURE,最后由我们应用层来统一销账

(4)断言,防止传错参数

(5)FromISR 版用的不是 taskENTER_CRITICAL(),而是 portSET_INTERRUPT_MASK_FROM_ISR()------把 BASEPRI 抬到configMAX_SYSCALL_INTERRUPT_PRIORITY,挡掉"有资格调用 RTOS API 的那一档"中断,并返回旧的 BASEPRI。退出时 portCLEAR_INTERRUPT_MASK_FROM_ISR(x) 把旧值写回 BASEPRI。

(6)这里跟xQueueSend()函数是一样的,就是发送一个数据,所以略过(注意:if( cTxLock == queueUNLOCKED )这里涉及上节的队列开关锁知识,这段代码意思是如果cTxLock等于-1,也就是开锁,就进入该分支)

(7)开锁分支里面的另外一个分支

(8)这里也是跟xQueueSend()一样的代码逻辑:判断等待接收链表是否有任务,如果有就将该任务的事件节点和状态节点从所属的链表移除,再判断调度是否被挂起,如果被挂起,就将事件节点插到待处理就绪链表,如果调度器没有被挂起,就将任务的状态节点插到就绪链表中,最后再判断唤醒的任务优先级是否高于当前运行任务的优先级,如果高于就返回pdTURE,同时将xYieldPending赋值pdTURE,否则就返回pdFALSE

(9)进入xTaskRemoveFromEventList()函数返回pdTURE分支,这里的处理与xQueueSend()函数不同,我们在前面说过,不能在中断直接任务切换,而且你切换也没用,因为PendSV的中断优先级最低,所以这里是申请一次任务切换

(10)这是走队列上锁的分支,因为队列已经上锁了:队列上锁时,数据虽然照样入队,但"唤醒等待接收任务"这个动作不能做(会动事件链表)------于是把"刚才有一条数据入了队"记进 cTxLock(+1),等解锁时由 prvUnlockQueue 补做唤醒。

(13)这里是"队列已满且没有选择覆盖模式"的分支。在中断执行上下文中,代码必须尽可能快地完成,不能有任何延迟。

以我们熟悉的 Tick 中断为例:如果程序一直卡在某个中断里,Tick 就无法按时加 1。对于依赖 Tick 运行的程序来说,任务切换、任务阻塞唤醒等操作都会被延迟。更重要的是,FreeRTOS 的设计目标就是不浪费 CPU 资源,这也是一个任务阻塞后必须有另一个任务立即接上的原因,绝不能出现"死等某个环节"的情况。

因此,在中断中发送队列消息时,一旦队列已满,xQueueSendFromISR() 会立即返回 errQUEUE_FULL,而不是像任务中那样进入阻塞等待。

(14)退出临界段,恢复之前保存的中断屏蔽寄存器配置。第(5)条里已经看到。(之所以需要这段临界区,是为了防止 API 函数在操作内核对象期间被"同样有资格调用 RTOS API 的更高优先级中断"抢占。例如当前正在把某个节点从所属链表移除,或者往某个链表中插入节点,如果此时被更高优先级中断打断,而那个中断服务程序也去访问这些内核链表,就会发生并发修改,导致链表结构错乱、程序出错。因此要先屏蔽这一类中断,待内核操作完成后再恢复。)

(15)返回程序结果

总结:xQueueSendFromISR() 与 xQueueSend() 的不同点主要有以下三点:
  1. 不直接触发任务切换 :xQueueSendFromISR() 不会在函数内部直接切换到更高优先级的任务执行,而是通过传入的参数 pxHigherPriorityTaskWoken 作为一份"任务切换申请清单"。如果需要切换任务,就在这份清单中写入"申请任务切换"标志,也就是把 *pxHigherPriorityTaskWoken 置为 pdTRUE。其他 FromISR() 类函数也可以在这份清单上标记是否需要申请任务切换,最后由应用程序统一回收这份清单,并对这些中断期间产生的切换请求一次性执行。

  2. 增加队列锁判断 :在队列操作上多了一步"开关锁"判断。如果队列当前处于上锁状态,xQueueSendFromISR() 不会直接操作事件链表,而是把"刚有一条数据入队"记进 cTxLock(+1),等 prvUnlockQueue() 解锁时再补做被推迟的唤醒。

  3. 队列满时立即返回 :如果队列已满,xQueueSendFromISR() 会直接返回 errQUEUE_FULL,绝不等待,保证中断上下文中的执行速度。

5.从消息队列读取消息函数

1.xQueueReceive()函数和xQueuePeek() 函数

当任务试图读队列中的消息时,可以指定一个阻塞超时时间,当且仅当消息队列中有消息的时候,任务才能读取到消息。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务程序往其等待的队列中写入了数据,该任务将自动由阻塞态转为就绪态。当任务等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。

c 复制代码
BaseType_t xQueueGenericReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait, const BaseType_t xJustPeeking )
{
BaseType_t xEntryTimeSet = pdFALSE;
TimeOut_t xTimeOut;
int8_t *pcOriginalReadPosition;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;

	configASSERT( pxQueue );
	configASSERT( !( ( pvBuffer == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );
	#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
	{
		configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );
	}
	#endif

	/* This function relaxes the coding standard somewhat to allow return
	statements within the function itself.  This is done in the interest
	of execution time efficiency. */

	for( ;; )
	{
		taskENTER_CRITICAL();
		{
			const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;

			/* Is there data in the queue now?  To be running the calling task
			must be the highest priority task wanting to access the queue. */
			if( uxMessagesWaiting > ( UBaseType_t ) 0 )
			{
				/* Remember the read position in case the queue is only being
				peeked. */
				pcOriginalReadPosition = pxQueue->u.pcReadFrom;

				prvCopyDataFromQueue( pxQueue, pvBuffer );

				if( xJustPeeking == pdFALSE )
				{
					traceQUEUE_RECEIVE( pxQueue );

					/* Actually removing data, not just peeking. */
					pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;

					#if ( configUSE_MUTEXES == 1 )
					{
						if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
						{
							/* Record the information required to implement
							priority inheritance should it become necessary. */
							pxQueue->pxMutexHolder = ( int8_t * ) pvTaskIncrementMutexHeldCount(); /*lint !e961 Cast is not redundant as TaskHandle_t is a typedef. */
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					#endif /* configUSE_MUTEXES */

					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
				{
					traceQUEUE_PEEK( pxQueue );

					/* The data is not being removed, so reset the read
					pointer. */
					pxQueue->u.pcReadFrom = pcOriginalReadPosition;

					/* The data is being left in the queue, so see if there are
					any other tasks waiting for the data. */
					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();
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}

				taskEXIT_CRITICAL();
				return pdPASS;
			}
			else
			{
				if( xTicksToWait == ( TickType_t ) 0 )
				{
					/* The queue was empty and no block time is specified (or
					the block time has expired) so leave now. */
					taskEXIT_CRITICAL();
					traceQUEUE_RECEIVE_FAILED( pxQueue );
					return errQUEUE_EMPTY;
				}
				else if( xEntryTimeSet == pdFALSE )
				{
					/* The queue was empty and a block time was specified so
					configure the timeout structure. */
					vTaskSetTimeOutState( &xTimeOut );
					xEntryTimeSet = pdTRUE;
				}
				else
				{
					/* Entry time was already set. */
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
		taskEXIT_CRITICAL();

		/* Interrupts and other tasks can send to and receive from the queue
		now the critical section has been exited. */

		vTaskSuspendAll();
		prvLockQueue( pxQueue );

		/* Update the timeout state to see if it has expired yet. */
		if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
		{
			if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
			{
				traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );

				#if ( configUSE_MUTEXES == 1 )
				{
					if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
					{
						taskENTER_CRITICAL();
						{
							vTaskPriorityInherit( ( void * ) pxQueue->pxMutexHolder );
						}
						taskEXIT_CRITICAL();
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
				#endif

				vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );
				prvUnlockQueue( pxQueue );
				if( xTaskResumeAll() == pdFALSE )
				{
					portYIELD_WITHIN_API();
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				/* Try again. */
				prvUnlockQueue( pxQueue );
				( void ) xTaskResumeAll();
			}
		}
		else
		{
			prvUnlockQueue( pxQueue );
			( void ) xTaskResumeAll();

			if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
			{
				traceQUEUE_RECEIVE_FAILED( pxQueue );
				return errQUEUE_EMPTY;
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
	}
}

(1)xJustPeeking 用于标记消息是否需要出队,如果是 pdFALSE,表示读取消息之后会进行出队操作,即读取消息后会把消息从队列中删除;如果是 pdTRUE,则读取消息之后不会进行出队操作,消息还会保留在队列中。 (我们在宏定义中默认传入的是pdFALSE)

(2)后面初始化阻塞时间的时候会用该变量来判断该函数是否是第一次调用

(3)用于记录读取数据的位置

(4)进入临界段

(5)获取队列信息空间有多少个数据

(6)判断队列数据是否大于0,大于0就进入该if()语句

(7)如果有消息,先记录读消息位置(第一次调用指向的是信息空间最后一个数据,也就是说第一次读取是在pxQueue->u.pcReadFrom的下一个,也就是信息空间第一个数据),防止仅仅是读取消息,而不进行消息出队操作

(8)prvCopyDataFromQueue( )函数(从队列中拷贝数据函数)

从队列中拷贝数据到pvBuffer,该函数内部如下:

c 复制代码
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 ) /*lint !e946 MISRA exception justified as use of the relational operator is the cleanest solutions. */
		{
			pxQueue->u.pcReadFrom = pxQueue->pcHead;
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
		( void ) memcpy( ( void * ) pvBuffer, ( void * ) pxQueue->u.pcReadFrom, ( size_t ) pxQueue->uxItemSize ); /*lint !e961 !e418 MISRA exception as the casts are only redundant for some ports.  Also previous logic ensures a null pointer can only be passed to memcpy() when the count is 0. */
	}
}

1.判断队列中,单个数据大小是否大于0,大于0进入该if()语句

2.将队列中的u.pcReadFrom指针移到下一个位置(在前面说过u.pcReadFrom指向的是队列中已经读取过数据的位置),该函数就是印证这一步

3.判断u.pcReadFrom指向的位置是否是pcTail指向的位置,如果超出这个位置或者等于这个位置,就将它移到pcHead指向的位置

4.将u.pcReadFrom指向的位置以uxItemSize字节大小拷贝到pvBuffer数组

回到xQueueReceive()函数:

(9)如果选择的是读取数据后从这个队列删除数据,走这个if()分支

(10)既然选择读取数据后从这个队列删除数据,那上面读取数据后肯定要将队列数据数量减1,这一步就是对应该操作

(11)这里是对应互斥量的操作,跳过

(12)读取完数据,对应的就要唤醒等待数据发送的任务,所以这里是检查队列的xTasksWaitingToSend链表是否为空,如果非空 就代表有任务,进入该if()语句(补充:① xTaskRemoveFromEventList 的前提就是链表非空------空链表去操作,listGET_OWNER_OF_HEAD_ENTRY 会把哨兵节点当任务,链表直接毁掉;② 竞争兜底:如果某个等待发送任务恰好同时超时,Tick已经帮它把两个节点都摘走(tasks.c:2451)------这里查到空表就自然跳过,不会对已移除的节点再动一次。)

(13)既然有任务就调用xTaskRemoveFromEventList()函数将该任务唤醒,这里与前面xQueueSend()函数是一样的,最后根据xTaskRemoveFromEventList()函数返回的结果,判断要不要执行一次任务切换

(14)这走的是读取数据而不进行数据出队的操作(出队的意思就是删除已经读取的数据)

(15)关于读取数据而不进行数据出队的操作

这里不再是对pxQueue->uxMessagesWaiting--,而是将记录的读取数据前的位置赋值给u.pcReadFrom(意思就是我读完数据,又回到我原来的位置,所以这个模式是给想多个任务读取同一个数据的操作用到,假设任务A读完这个数据又回到原来的位置,那下次任务B调用这个函数也选择这模式,读取数据时,也是任务A读取数据的位置)

(16)这一步的操作与上面的同理

(17)退出临界段

(18)返回pdPASS

(19)这里走的是,队列没有数据的分支

(21)如果你传进来的xTicksToWait等于0,说明该任务不等待有任务向队列发送数据,接着直接退出临界段,返回errQUEUE_EMPTY

(22)如果你选择等待,在第一次调用的时候,就会调用vTaskSetTimeOutState()函数来初始化阻塞时间

(23)退出临界段

(24)这一步与 xQueueSend() 中的阻塞分支几乎一样,唯一的差别在于:这里调用 vTaskPlaceOnEventList() 时,是把当前任务的事件节点插入到等待接收数据链表 pxQueue->xTasksWaitingToReceive;而 xQueueSend() 中插入的是等待发送数据链表 pxQueue->xTasksWaitingToSend。

这里再补充一个细节:执行 if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ) 这一步时,可能出现的几种情况。xTaskCheckForTimeOut() 内部会先检查任务是否已经超时:

  • 如果已经超时,函数返回 pdTRUE,外层 if 不成立,走下面的超时分支,任务不再进入阻塞;
  • 如果还没有超时,函数返回 pdFALSE,同时计算出剩余阻塞时间,并更新 xTimeOut 时间结构体,作为下一次判断超时的基准。

那为什么要特意重新做这次超时检查?因为在前面退出临界段之后,完全可能恰好来一次中断或更高优先级任务抢占,把当前任务切走。等任务再被调度回来执行到这里时,距离最初记录的超时起点可能已经过了很久,有可能已经超时,所以必须先判断一次。如果确认还没超时,才会继续执行后面的步骤,让任务进入阻塞状态。

任务陷入阻塞后,再次被唤醒只有两种情况:

  1. 被队列事件唤醒 :有任务或中断向队列写入了数据,当前任务被从等待接收链表捞出来。任务回到 for(;;) 循环开头,执行前面的 prvCopyDataFromQueue(),把队列数据拷贝到指定缓冲区,然后正常返回 pdPASS,不会再走到这一步。

  2. 超时唤醒 :Tick 中断在调用 xTaskIncrementTick() 时,会顺带把该任务的状态节点从延时链表移除、事件节点从队列事件链表移除,任务随之回到就绪链表,之后可能被调度器再次切换运行。任务最后一次进入该函数时,如果队列此时仍没有数据,就会执行到这里,xTaskCheckForTimeOut() 判定超时,最终返回错误码并退出函数。

所以正常流程中,这一步"未超时、真正陷入阻塞"的逻辑通常只会执行一次。

(25)这里的操作也是一样的,说一下这一步,前面说了退出临界段可能会响应中断或者切换到更高优先级任务,所以中断或者更高优先级的任务可能会发生数据到队列的,这时队列就是有数据的,所以这里不阻塞该任务,而且是开锁、恢复调度器后,回到for(;;)循环

2.xQueueReceiveFromISR()与 xQueuePeekFromISR() 函数(接收队列数据中断函数)

xQueueReceiveFromISR()是 xQueueReceive ()的中断版本,用于在中断服务程序中接收一个队列消息并把消息从队列中删除;xQueuePeekFromISR()是 xQueuePeek()的中断版本,用于在中断中从一个队列中接收消息,但并不会把消息从队列中移除。

说白了这两个函数只能用于中断,是不带有阻塞机制的,并且是在中断中可以安全调用,关于这两个函数参数如下:

可以注意到xQueuePeekFromISR() 函数,是没有参数三的,也就是说它没有"切换任务清单",为什么呢?前面介绍该模式的时候说了,它们读取数据后会将u.pcReadFrom移回到用来位置,所以后面程序调用该函数还是会读到相同位置的相同数据,那就没有必要唤醒向队列发生数据的任务了。

xQueueReceiveFromISR()函数源代码如下:

c 复制代码
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue, void * const pvBuffer, BaseType_t * const pxHigherPriorityTaskWoken )
{
BaseType_t xReturn;
UBaseType_t uxSavedInterruptStatus;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;

	configASSERT( pxQueue );
	configASSERT( !( ( pvBuffer == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );

	/* RTOS ports that support interrupt nesting have the concept of a maximum
	system call (or maximum API call) interrupt priority.  Interrupts that are
	above the maximum system call priority are kept permanently enabled, even
	when the RTOS kernel is in a critical section, but cannot make any calls to
	FreeRTOS API functions.  If configASSERT() is defined in FreeRTOSConfig.h
	then portASSERT_IF_INTERRUPT_PRIORITY_INVALID() will result in an assertion
	failure if a FreeRTOS API function is called from an interrupt that has been
	assigned a priority above the configured maximum system call priority.
	Only FreeRTOS functions that end in FromISR can be called from interrupts
	that have been assigned a priority at or (logically) below the maximum
	system call	interrupt priority.  FreeRTOS maintains a separate interrupt
	safe API to ensure interrupt entry is as fast and as simple as possible.
	More information (albeit Cortex-M specific) is provided on the following
	link: http://www.freertos.org/RTOS-Cortex-M3-M4.html */
	portASSERT_IF_INTERRUPT_PRIORITY_INVALID();

	uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
	{
		const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;

		/* Cannot block in an ISR, so check there is data available. */
		if( uxMessagesWaiting > ( UBaseType_t ) 0 )
		{
			const int8_t cRxLock = pxQueue->cRxLock;

			traceQUEUE_RECEIVE_FROM_ISR( pxQueue );

			prvCopyDataFromQueue( pxQueue, pvBuffer );
			pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;

			/* If the queue is locked the event list will not be modified.
			Instead update the lock count so the task that unlocks the queue
			will know that an ISR has removed data while the queue was
			locked. */
			if( cRxLock == queueUNLOCKED )
			{
				if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
				{
					if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
					{
						/* The task waiting has a higher priority than us so
						force a context switch. */
						if( pxHigherPriorityTaskWoken != NULL )
						{
							*pxHigherPriorityTaskWoken = pdTRUE;
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				/* Increment the lock count so the task that unlocks the queue
				knows that data was removed while it was locked. */
				pxQueue->cRxLock = ( int8_t ) ( cRxLock + 1 );
			}

			xReturn = pdPASS;
		}
		else
		{
			xReturn = pdFAIL;
			traceQUEUE_RECEIVE_FROM_ISR_FAILED( pxQueue );
		}
	}
	portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );

	return xReturn;
}

可以看到操作步骤与xQueueSendFromISR()函数一模一样,所以这里不再介绍了

补充:关于读取完数据"删除"数据的操作

从上面的分析可以看出,调用这些函数从队列读取数据时,并不是真正意义上把某个位置的数据"删除"或"擦除",而只是把队列的 uxMessagesWaiting 减 1。无论是向队列发送数据的函数,还是从队列接收数据的函数,本质上都依赖 uxMessagesWaiting 这个"有效数据计数"来驱动指针移位,并决定后续还能不能继续操作。因此,这里所说的"删除"其实只是限制了指针可以移动的次数;那些已经被读过的地址空间里的旧数据仍然保留着,并没有被物理清空。

而队列本身就是通过指针来访问地址:当下一次调用 xQueueSend() 时,写指针 pcWriteTo 会再次指向这个曾经被读过的位置,然后把本次要入队的值直接拷贝覆盖到该地址上。正因如此,FreeRTOS 队列始终是"以拷贝的方式"保存消息,而不是保存引用,这也是前文一直强调"向队列拷贝数据"的原因。

6.消息队列使用注意事项

在使用 FreeRTOS 提供的消息队列函数的时候,需要了解以下几点:

  1. 使用 xQueueSend()、xQueueSendFromISR()、xQueueReceive()等这些函数之前应先创建需消息队列,并根据队列句柄进行操作。

  2. 队列读取采用的是先进先出(FIFO)模式,会先读取先存储在队列中的数据。当然也 FreeRTOS 也支持后进先出(LIFO)模式,那么读取的时候就会读取到后进队列的数据。

  3. 在获取队列中的消息时候,我们必须要定义一个存储读取数据的地方,并且该数据区域大小不小于消息大小,否则,很可能引发地址非法的错误。

  4. 无论是发送或者是接收消息都是以拷贝的方式进行,如果消息过于庞大,可以将消息的地址作为消息进行发送、接收。

  5. 队列是具有自己独立权限的内核对象,并不属于任何任务。所有任务都可以向同一队列写入和读出。一个队列由多任务或中断写入是经常的事,但由多个任务读出倒是用的比较少。

7.消息队列实验:

实验一:

该实验要实现任务A检测按键按下,如果按键按下向队列发送数据,任务B等待接收数据

c 复制代码
#include "stm32f4xx.h"
#include "./led/bsp_led.h"
#include "bsp_debug_usart.h"
#include "key.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"

TaskHandle_t APP_TaskHandel=NULL;
TaskHandle_t Task1_Handle=NULL;
QueueHandle_t Queue1_Handle=NULL;


static void Hadware_Init(void);
void APP_CreatTask(void* parameter);
void Task1(void* parameter);
void Task2(void* parameter);


uint8_t	Flag1;
uint8_t	Flag2;

int main(void)
{
  Hadware_Init();

  /*创建应用任务*/
  xTaskCreate(APP_CreatTask,"APP_Task",128,NULL,2,&APP_TaskHandel);

  if(APP_TaskHandel!=NULL)
  {
	printf("任务创建成功\r\n");
  }
  else
  {
	printf("任务创建失败\r\n");
  }

  /*开启任务调度*/
  vTaskStartScheduler();

  while(1)
  {

  }
}

static void Hadware_Init(void)
{
	/*
	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
	 * 都统一用这个优先级分组,千万不要再分组,切忌。
	 */
	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
	
	/* LED 初始化 */
	LED_GPIO_Config();

	/* 串口初始化	*/
	Debug_USART_Config();

	Key_Init();
  
}

/*创建应用任务*/
void APP_CreatTask(void* parameter)
{
	xTaskCreate(Task1,"Task1",128,NULL,1,&Task1_Handle);
	xTaskCreate(Task2,"Task2",128,NULL,2,NULL);
	Queue1_Handle=xQueueCreate(100,1);
	vTaskDelete(APP_TaskHandel);
}
/*创建任务1*/
void Task1(void* parameter)
{
	uint8_t i=0;
	BaseType_t x;
	while(1)
	{
		x=xQueueReceive(Queue1_Handle,&i,portMAX_DELAY);
		if(x==pdPASS)
		{
			printf("接受到的数据:%d\r\n",i);
		}
	}
}

/*创建任务2*/
void Task2(void* parameter)
{
	uint8_t i=1;
	BaseType_t x;
	while(1)
	{
		if(Key_State()==1)
		{
			x=xQueueSend(Queue1_Handle,&i,portMAX_DELAY);
			if(x==pdPASS)
			{
				printf("向队列写入数据成功\r\n");
				vTaskDelay(500);
			}
		}
	}
}

实验结果如下:

因为要确保任务A在发送数据时不切换任务,所以将任务A的优先级设置为最高,然后在任务A向队列发送完数据后,在进入阻塞状态给任务B接受数据,注意:发完数据,把 B 放进就绪表后,因为 B 优先级更低,不会请求切换;A 继续打印、继续循环,直到执行 vTaskDelay(500) 主动阻塞,B 才被调度运行。

实验二:

电脑往单片机发送数据后,触发USART1接受中断,接着在中断内向队列发送数据,随后任务B接受数据,代码如下:

stm32f4xx_it.c:

c 复制代码
extern QueueHandle_t Queue1_Handle;
void USART1_IRQHandler(void)
{
  uint16_t ch=0;
  BaseType_t queueYIELD=pdFALSE;;
  if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET)
  {
    ch=USART_ReceiveData(USART1);

    xQueueSendFromISR(Queue1_Handle,&ch,&queueYIELD);

    portYIELD_FROM_ISR(queueYIELD);
  }
}

main.c

c 复制代码
#include "stm32f4xx.h"
#include "./led/bsp_led.h"
#include "bsp_debug_usart.h"
#include "key.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"

TaskHandle_t APP_TaskHandel=NULL;
TaskHandle_t Task1_Handle=NULL;
QueueHandle_t Queue1_Handle=NULL;

static void Hadware_Init(void);
void APP_CreatTask(void* parameter);
void Task1(void* parameter);
void Task2(void* parameter);

uint8_t	Flag1;
uint8_t	Flag2;

int main(void)
{
  Hadware_Init();

  /*创建应用任务*/
  xTaskCreate(APP_CreatTask,"APP_Task",128,NULL,2,&APP_TaskHandel);

  if(APP_TaskHandel!=NULL)
  {
	printf("任务创建成功\r\n");
  }
  else
  {
	printf("任务创建失败\r\n");
  }

  /*开启任务调度*/
  vTaskStartScheduler();

  while(1)
  {

  }
}

static void Hadware_Init(void)
{
	/*
	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
	 * 都统一用这个优先级分组,千万不要再分组,切忌。
	 */
	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
	
	/* LED 初始化 */
	LED_GPIO_Config();

	/* 串口初始化	*/
	Debug_USART_Config();

	Key_Init();
  
}

/*创建应用任务*/
void APP_CreatTask(void* parameter)
{
	xTaskCreate(Task1,"Task1",128,NULL,2,&Task1_Handle);
	xTaskCreate(Task2,"Task2",128,NULL,1,NULL);
	Queue1_Handle=xQueueCreate(100,2);
	vTaskDelete(APP_TaskHandel);
}
/*创建任务1*/
void Task1(void* parameter)
{
	uint16_t i=0;
	BaseType_t x;
	while(1)
	{
		x=xQueueReceive(Queue1_Handle,&i,portMAX_DELAY);
		if(x==pdPASS)
		{
			printf("接受到的数据:0X%x\r\n",i);
		}
	}
}

/*创建任务2*/
void Task2(void* parameter)
{
	while(1)
	{
		printf("wait send\r\n");
		vTaskDelay(200);
	}
}

数据结果如下;

这里要将任务B的优先级设置为最高,因为在中断调用 xQueueSendFromISR()函数后,会将任务B从阻塞链表中转移到就绪链表,接着在退出中断前会将PendSV置1,最后退出中断,执行任务切换,这时因为任务B优先级最高,所以会执行任务B。(最后提醒:一定要将queueYIELD=pdFALSE,因为后面没人给你将queueYIELD恢复回pdFALSE)

相关推荐
姓刘的哦1 小时前
GT911触摸芯片
linux·stm32
HRTOS1 小时前
HRTOS 4.0 实测:8051 操作系统毫秒级任务延时精度测试
经验分享·单片机·51单片机
HRTOS1 小时前
HRTOS 4.0 实测:指定任务运行机制与任务切换耗时
经验分享·单片机·51单片机
HRTOS2 小时前
HRTOS 4.0 实测:事件阻塞与唤醒调度耗时测试
经验分享·单片机·51单片机
笨鸟先飞的橘猫2 小时前
C 扩展开发指南:从 Lua 到 skynet 的完整链路
c语言·lua
Navigator_Z11 小时前
LeetCode //C - 1289. Minimum Falling Path Sum II
c语言·算法·leetcode
himobrinehacken12 小时前
取模运算的汇编奥秘揭秘
c语言·安全
巴渝小禹13 小时前
单片机死循环delay_us()计算方法
单片机·嵌入式硬件
青岚_Nox14 小时前
C语言实战:带你手写一个带"计时功能"的猜数字小游戏
c语言