【FreeRTOS】【应用篇】消息队列【下篇】

前言

  • 本篇文章主要对 FreeRTOS 中消息队列的概念和相关函数进行了详解
  • 消息队列【下篇】详细剖析了消息队列中发送、接收时队列消息控制块中各种指针的行为,以及几个发送消息和接收消息的函数的运作流程
  • 笔者有关于 【FreeRTOS】【应用篇】消息队列【上篇】——队列基本概念、创建和删除,讲解了消息队列的基本概念(作用、存储结构、出入队列逻辑、阻塞机制)以及相关函数(队列创建和删除)
  • 一部分代码和图片参考野火 FreeRTOS 教程

    文章目录

    • 前言
    • 一、发送、接收时队列消息控制块中各种指针的行为
      • 1. 初始化后
      • 2. 发送消息到队列时
        • ① 发送到队尾(普通消息)
        • ② 发送到队头(紧急消息)
      • 3. 从队列中读取消息时
    • 二、消息发送 API
      • 1. 概要
        • ① 适用于任务中的消息发送函数
        • ② 适用于中断中的消息发送函数
      • 2. xQueueSend()与 xQueueSendToBack()
        • ① xQueueSend() 代码
        • ② xQueueSend() 使用示例
        • ③ xQueueSendToBack() 代码
      • 3. xQueueSendFromISR()与 xQueueSendToBackFromISR()
        • ① 代码
        • ② 使用示例
      • 4. xQueueSendToFront()
        • ① 代码
      • 5. xQueueSendToFrontFromISR()
        • ① 代码
    • 三、通用的任务中发送消息函数 xQueueGenericSend()
      • 1. xQueueGenericSend() 函数定义
      • 2. xQueueGenericSend() 函数流程图
      • 3. prvCopyDataToQueue() 函数说明
        • ① 如何被调用
        • ② 参数说明
        • ③ prvCopyDataToQueue() 函数完整代码
      • 4. 超时状态结构的配置 vTaskSetTimeOutState()
        • ① 超时状态结构如何配置
        • ② vTaskSetTimeOutState() 函数定义
      • 5. 挂起调度器和锁上队列的意义
        • ① 如何操作
        • ② 意义
        • ③ 队列上锁代码
      • 6. 检查延时是否到期函数 xTaskCheckForTimeOut()
        • ① 使用
        • ② 主要逻辑
        • ③ 源代码
      • 7. 将任务插入任务等待发送列表 vTaskPlaceOnEventList()
        • ① 使用
        • ② 函数定义
      • 8. 队列解锁函数 prvUnlockQueue()
        • ① 函数原型
        • ② 函数结构分析
        • ③ 计数锁的原理
    • 四、用于中断中的 通用的消息发送函数 xQueueGenericSendFromISR()
      • 1. 函数概要
      • 2. 函数代码
    • 五、消息读取 API
      • 1. 简介
      • 2. xQueueReceive()
        • ① 原型及作用
        • ② 宏展开
        • ③ 应用
      • 3. xQueuePeek()
        • ① 作用
        • ② 宏展开
      • 4. xQueueReceiveFromISR() 与 xQueuePeekFromISR()
        • ① xQueueReceiveFromISR() 使用示例
    • 六、通用读取消息函数 xQueueGenericReceive()
      • 1. 函数结构分析
      • 2. 函数定义
    • 四、性能提示
    • 后记

一、发送、接收时队列消息控制块中各种指针的行为

1. 初始化后

  • 消息空间从第一个单元到最后一个单元是从低地址增长到高地址
  • pcHead 指向消息空间第一个消息单元,此后固定不动,是为了方便快速定位消息空间头部而设定的指针
  • pcTail 指向消息空间最后一个消息单元(这个消息单元不作存储消息使用,前一个单元才是被用作存储的最后一个单元),此后固定不动,是为了方便快速定位消息空间尾部而设定的指针
  • pcWriteTo 指向 pcHead,指向下一个插入到队尾的消息的存储单元
  • pcReadFrom 指向 pcTail 的前一个消息单元,指向队列中下一个要被读取的消息的前一个单元

2. 发送消息到队列时

① 发送到队尾(普通消息)

  • xQueueSend() 和 xQueueSendToBack()
  • 消息被拷贝到 pcWriteTo 指向的消息单元,然后 pcWriteTo 自增一个消息单元
( void ) memcpy( ( void * ) pxQueue->pcWriteTo, pvItemToQueue, ( size_t ) pxQueue->uxItemSize );
pxQueue->pcWriteTo += pxQueue->uxItemSize;
  • 当 pcWriteTo 自增到 pcTail 位置时,返回 pcHead
if( pxQueue->pcWriteTo >= pxQueue->pcTail ) 
		{
			pxQueue->pcWriteTo = pxQueue->pcHead;
		}

② 发送到队头(紧急消息)

  • xQueueSendToFront()
  • 消息被拷贝到 pcReadFrom 指向的消息单元,然后 pcReadFrom 自减一个消息单元
( void ) memcpy( ( void * ) pxQueue->u.pcReadFrom, pvItemToQueue, ( size_t ) pxQueue->uxItemSize ); 
pxQueue->u.pcReadFrom -= pxQueue->uxItemSize;
  • pcReadFrom 指向 pcHead 的上一个消息单元时(此时越界),就重置 pcReadFrom 指向 pcTail 的前一个消息单元
if( pxQueue->u.pcReadFrom < pxQueue->pcHead )
		{
			pxQueue->u.pcReadFrom = ( pxQueue->pcTail - pxQueue->uxItemSize );
		}

3. 从队列中读取消息时

  • 首先自增 pcReadFrom,使其指向要读取的消息单元(pcReadFrom 初始化后保持为指向要读取的消息单元的前一个单元)
pxQueue->u.pcReadFrom += pxQueue->uxItemSize;
  • 然后判断自增后的 pcReadFrom 是否越界,如果指向的位置等于或者大于 pcTail 指向的位置,那么使 pcReadFrom 重新指向 pcHead
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;
		}
  • 最后从队列中将 pcReadFrom 指向的要读取的消息拷贝出来
( void ) memcpy( ( void * ) pvBuffer, ( void * ) pxQueue->u.pcReadFrom, ( size_t ) pxQueue->uxItemSize ); 

二、消息发送 API

1. 概要

① 适用于任务中的消息发送函数

适用于任务中的消息发送函数有三个版本

  • xQueueSend() 等同于 xQueueSendToBack(),作用都是将消息插入队列尾部

    • 从尾部插入的消息是普通消息,会按照插入顺序的先后被先后读取
    • 注意不是指 pcTail 所指的位置,pcTail 只是一个为了方便快速定位消息空间尾部设定的指针,在队列创建后就固定不动了
    • 这里所说的插入到队尾是指插入到 pcWriteTo 所指的位置,插入完成后,pcWriteTo 自增
  • xQueueSendToFront() 作用是将消息插入队列头部

    • 从头部插入的消息是紧急消息,紧急消息会比普通消息先被读取
    • 这里所说的插入头部并不是指插入到 pcHead 所指的位置,和 pcTail 一样,pcHead 只是一个为了方便快速定位消息空间头部设定的指针,在队列创建后就固定不动了
    • 这里所说的插入到队头是指插入到 pcReadFrom 所指的位置,插入完成后,pcReadFrom 自减(保持指向下一个要读取的消息的前一个消息
  • 这三个适用于任务中的消息发送函数实际上都调用了 xQueueGenericSend(),只是传入的发送位置的参数不同
    【FreeRTOS】【应用篇】消息队列【下篇】_第1张图片

② 适用于中断中的消息发送函数

适用于中断中的消息发送函数也有三个版本(只能用于中断中执行,是不带阻塞机制的):

  • xQueueSendToBackFromISR() 等同于 xQueueSendFromISR()
    • 作用是将消息插入队列尾部,与上文的 xQueueSend() 说明一致
  • xQueueSendToFrontFromISR() 将消息插入队列头部,与上文的 xQueueSendToFront() 说明一致
    【FreeRTOS】【应用篇】消息队列【下篇】_第2张图片

2. xQueueSend()与 xQueueSendToBack()

① xQueueSend() 代码

  • 原型
BaseType_t xQueueSend(QueueHandle_t xQueue,
 const void * pvItemToQueue,
 TickType_t xTicksToWait);
  • 参数说明

    • xQueue:队列句柄
    • pvItemToQueue:指针,指向要发送到队列尾部的队列消息
    • xTicksToWait:队列满时,等待队列空闲的最大超时时间。如果队列满并且 xTicksToWait 被设置成 0,函数立刻返回。超时时间的单位为系统节拍周期,常量 portTICK_PERIOD_MS 用于辅助计算真实的时间,单位为 ms。如果 INCLUDE_vTaskSuspend 设置成 1,并且指定延时为 portMAX_DELAY 将导致任务挂起(没有超时)
  • 宏定义

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

② xQueueSend() 使用示例

该宏是为了向后兼容没有包含 xQueueSendToFront() 和 xQueueSendToBack() 这两个宏的 FreeRTOS 版本。

static void Send_Task(void* parameter)
{
    BaseType_t xReturn = pdPASS; /* 定义一个创建信息返回值,默认为 pdPASS */
    uint32_t send_data1 = 1;
    uint32_t send_data2 = 2;
    while (1) {
        if (Key_Scan(KEY1_GPIO_PORT, KEY1_GPIO_PIN) == KEY_ON) {
            /* K1 被按下 */
            printf("发送消息 send_data1!\n");
            xReturn = xQueueSend(Test_Queue, /* 消息队列的句柄 */
                                 &send_data1, /* 发送的消息内容 */
                                 0); /* 等待时间 0 */
            if (pdPASS == xReturn)
                printf("消息 send_data1 发送成功!\n\n");
        }
        if (Key_Scan(KEY2_GPIO_PORT, KEY2_GPIO_PIN) == KEY_ON) {
            /* K2 被按下 */
            printf("发送消息 send_data2!\n");
            xReturn = xQueueSend(Test_Queue, /* 消息队列的句柄 */
                                 &send_data2, /* 发送的消息内容 */
                                 0); /* 等待时间 0 */
            if (pdPASS == xReturn)
                printf("消息 send_data2 发送成功!\n\n");
        }
        vTaskDelay(20); /* 延时 20 个 tick */
    }
}

③ xQueueSendToBack() 代码

  • 原型
    与 xQueueSend() 一样
  • 宏定义
#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )

3. xQueueSendFromISR()与 xQueueSendToBackFromISR()

xQueueSendFromISR() 与 xQueueSendToBackFromISR() 是等同的。

① 代码

  • 原型
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue,
 const void *pvItemToQueue,
 BaseType_t *pxHigherPriorityTaskWoken);
  • 参数说明
    • xQueue:队列句柄
    • pvItemToQueue:指针,指向要发送到队列尾部的消息
    • pxHigherPriorityTaskWoken:如果入队导致一个任务解锁,并且解锁的任务优先级高于当前被中断的任务,则将 *pxHigherPriorityTaskWoken设置成 pdTRUE,然后在中断退出前需要进行一次上下文切换, 去执行被唤醒的优先级更高的任务 。
  • 宏定义
#define xQueueSendToFrontFromISR(xQueue, pvItemToQueue, pxHigherPriorityTaskWoken) \
    xQueueGenericSendFromISR((xQueue), (pvItemToQueue), (pxHigherPriorityTaskWoken), queueSEND_TO_FRONT)
#define xQueueSendToBackFromISR(xQueue, pvItemToQueue, pxHigherPriorityTaskWoken) \
    xQueueGenericSendFromISR((xQueue), (pvItemToQueue), (pxHigherPriorityTaskWoken), queueSEND_TO_BACK)

② 使用示例

使用 pxHigherPriorityTaskWoken 来确定是否需要上下文切换:

void vBufferISR(void)
{
    char cIn;
    BaseType_t xHigherPriorityTaskWoken;

    /* 在 ISR 开始的时候,我们并没有唤醒任务 */
    xHigherPriorityTaskWoken = pdFALSE;

    /* 直到缓冲区为空 */
    do {
        /* 从缓冲区获取一个字节的数据 */
        cIn = portINPUT_BYTE(RX_REGISTER_ADDRESS);

        /* 发送这个数据 */
        xQueueSendFromISR(xRxQueue, &cIn, &xHigherPriorityTaskWoken);

    } while (portINPUT_BYTE(BUFFER_COUNT));

    /* 这时候 buffer 已经为空,如果需要则进行上下文切换 */
    if (xHigherPriorityTaskWoken) {
        /* 上下文切换,这是一个宏,不同的处理器,具体的方法不一样 */
        taskYIELD_FROM_ISR();
    }
}

4. xQueueSendToFront()

用于向队列队首发送一个消息,该消息会被优先读取。

① 代码

  • 原型
BaseType_t xQueueSendToFront( QueueHandle_t xQueue,
 const void * pvItemToQueue,
 TickType_t xTicksToWait );
  • 参数说明
    • xQueue 队列句柄
    • pvItemToQueue 指针,指向要发送到队首的消息
    • xTicksToWait 队列满时,等待队列空闲的最大超时时间。如果队列满并且 xTicksToWait 被设置成 0,函数立刻返回。超时时间的单位为系统节拍周期,常量 portTICK_PERIOD_MS 用于辅助计算真实的时间,单位为 ms。如果 INCLUDE_vTaskSuspend 设置成 1,并且指定延时为 portMAX_DELAY 将导致任务无限阻塞(没有超时)
  • 宏定义
#define xQueueSendToFront(xQueue, pvItemToQueue, xTicksToWait) \
    xQueueGenericSend((xQueue), (pvItemToQueue), \
    (xTicksToWait), queueSEND_TO_FRONT)

5. xQueueSendToFrontFromISR()

用于在中断中向队首发送信息。

① 代码

  • 原型
BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue,
 const void *pvItemToQueue,
 BaseType_t *pxHigherPriorityTaskWoken);
  • 参数说明
    • xQueue 队列句柄
    • pvItemToQueue 指针,指向要发送到队首的消息
    • pxHigherPriorityTaskWoken 如果入队导致一个任务解锁,并且解锁的任务优先级高于当前被中断的任务,则将 *pxHigherPriorityTaskWoken 设置成 pdTRUE,然后在中断退出前需要进行一次上下文切换,去执行被唤醒的优先级更高的任务 。
  • 宏定义
#define xQueueSendToFrontFromISR(xQueue, pvItemToQueue, pxHigherPriorityTaskWoken) \
    xQueueGenericSendFromISR((xQueue), (pvItemToQueue), \
    (pxHigherPriorityTaskWoken), queueSEND_TO_FRONT)

三、通用的任务中发送消息函数 xQueueGenericSend()

上述的任务中的消息发送函数 API 经过宏定义展开后实际上就是传入不同参数的 xQueueGenericSend(),下面将对这个函数进行详细的介绍。

1. xQueueGenericSend() 函数定义

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 );
				//此处省略队列集相关代码
				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. */
					//消息空间已满且不允许覆盖写入,且阻塞时间设置为0 或 阻塞时间已过
					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. */
					//消息空间已满且不允许覆盖写入,并且阻塞时间大于0所以需要配置超时结构
					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. */

		//挂起调度器 并 锁定队列的等待发送任务列表和等待接收任务列表:
		//这两个操作都是为了防止其它任务或者中断操作 xTasksWaitingToReceive 列表和 xTasksWaitingToSend 列表而引起优先级翻转
		//问:为什么可能会优先级翻转?
		//答:设想当前要进入阻塞的任务B 的优先级比已经在阻塞中的任务A 的优先级高。
		//正常情况下,当任务B 也进入阻塞后,此时两个任务都在阻塞中(比如都在阻塞等待入队),
		//那么当可以入队时应该是优先级高的B 线解除阻塞并入队;
		//但是可能会发生这样一种情况,在设置任务B 进入阻塞的过程中,
		//其它任务或者中断操作了 xTasksWaitingToReceive 列表和 xTasksWaitingToSend 列表导致可以入队了,
		//正确的逻辑应该是解除优先级较高的任务B 的阻塞而任务A 继续阻塞等待入队,
		//但是此时由于任务B 还在设置进入阻塞的过程中,所以阻塞列表中只有任务A,导致只能解除
		//任务A 的阻塞。挂起调度器来禁止其它任务对两个列表的操作 和 锁定中断对两个列表的操作可以确保
		//在任务B 成功进入阻塞后在通过优先级高低来解除任务的阻塞,防止优先级较低的任务A 先于任务B 解除阻塞。
		
		vTaskSuspendAll();			//暂停任务切换,防止在运行其它任务时该任务修改队列的等待接收和等待发送列表
		prvLockQueue( pxQueue );	//锁定队列,防止此时有中断触发修改队列的等待接收和等待发送列表

		/* Update the timeout state to see if it has expired yet. */
		if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )//延时未到期且还未插进去
		{
			if( prvIsQueueFull( pxQueue ) != pdFALSE )//如果队列还是满,就把未到期且未成功插入(等待插入)的任务放入 xTasksWaitingToSend 列表中
			{
				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;
		}
	}
}
/*-----------------------------------------------------------*/

2. xQueueGenericSend() 函数流程图

下面将对 xQueueGenericSend() 中调用到的函数进行详细的说明:

  1. prvCopyDataToQueue() 消息拷贝到队列函数
  2. vTaskSetTimeOutState() 超时结构配置函数
  3. 挂起调度器函数 vTaskSuspendAll() 和 队列上锁函数 prvLockQueue() 的作用
  4. 检查延时是否到期函数 xTaskCheckForTimeOut()
  5. 将任务插入任务等待发送列表 vTaskPlaceOnEventList()
  6. 队列解锁函数 prvUnlockQueue()
    【FreeRTOS】【应用篇】消息队列【下篇】_第3张图片

3. prvCopyDataToQueue() 函数说明

① 如何被调用

  • 在 xQueueGenericSend() 中被调用
  • 队列未满或者允许覆盖写入时,使用 prvCopyDataToQueue() 将消息拷贝到队列中:
if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
			{
				traceQUEUE_SEND( pxQueue );
				xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );
			}

② 参数说明

  • prvCopyDataToQueue() 需要三个参数:
    • 指向操作的队列指针
    • 指向要拷贝到队列中的消息指针
    • 说明拷贝位置的参数(队头、队尾、覆盖)

  • 插入队尾时(xPosition == queueSEND_TO_BACK),为普通消息的发送,将消息拷贝到 pcWriteTo 所指向的消息单元后 pcWriteTo 自增,pcWriteTo 自增越界后返回 pcHead
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;
		}
	}
  • 插入队头时,为紧急消息的发送,将消息拷贝到 pcReadFrom 指向的消息单元,然后 pcReadFrom 自减一个单元。当 pcReadFrom 自减越界时,重置为指向 pcTail 的前一个单元
( 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 );
		}
  • 覆盖写入时( xPosition == queueOVERWRITE ),即使队列已满也可以写入队列,这个功能旨在用于长度为 1 的队列(来自 FreeRTOS 官方),插入的位置和插入队头的位置是一样的
    【FreeRTOS】【应用篇】消息队列【下篇】_第4张图片

③ prvCopyDataToQueue() 函数完整代码

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;
}
/*-----------------------------------------------------------*/

4. 超时状态结构的配置 vTaskSetTimeOutState()

① 超时状态结构如何配置

  • 在 xQueueGenericSend() 中被调用
  • 消息空间已满且不允许覆盖写入,并且阻塞时间大于 0 且未配置超时结构时需要配置超时状态结构:
vTaskSetTimeOutState( &xTimeOut );
xEntryTimeSet = pdTRUE;
  • 超时结构的配置主要是 溢出次数进入阻塞时间的配置:

② vTaskSetTimeOutState() 函数定义

void vTaskSetTimeOutState( TimeOut_t * const pxTimeOut )
{
	configASSERT( pxTimeOut );
	pxTimeOut->xOverflowCount = xNumOfOverflows;	//溢出次数
	pxTimeOut->xTimeOnEntering = xTickCount;		//进入时间 == 当前时间
}

5. 挂起调度器和锁上队列的意义

① 如何操作

//挂起调度器 并 锁定队列的等待发送任务列表和等待接收任务列表:
//这两个操作都是为了防止其它任务或者中断操作 xTasksWaitingToReceive 列表 xTasksWaitingToSend 列表而引起优先级翻转
vTaskSuspendAll();			//暂停任务切换,防止在运行其它任务时该任务修改队列的等待接收和等待发送列表
		prvLockQueue( pxQueue );	//锁定队列,防止此时有中断触发修改队列的等待接收和等待发送列表

② 意义

对于挂起调度器和锁上队列的意义,笔者思考了很久,如下:
挂起调度器 并 锁定队列的等待发送任务列表和等待接收任务列表:这两个操作都是为了防止其它任务或者中断操作 xTasksWaitingToReceive 列表和 xTasksWaitingToSend 列表而引起优先级翻转。

  1. 当要使任务进入阻塞时,假设此任务(任务B)的优先级比已经在阻塞中的任务(任务A)优先级高。

  2. 正常情况下,两个任务都应该在阻塞列表中等待操作,当可以进行队列操作时,应该优先解除优先级高的任务B的阻塞并进行操作。

  3. 但是在某种情况下,可能发生在设置任务B进入阻塞的过程中,有其他任务或者中断对等待接收和等待发送列表进行了操作,使得可以进行队列操作。

    • 正确的行为应该是先解除优先级高的任务B的阻塞,任务A继续保持阻塞状态等待入队。
    • 但是由于任务B还在设置进入阻塞的过程中,阻塞列表中只有任务A,导致只能解除任务A的阻塞。这就导致了优先级较低的任务A先于任务B解除阻塞,出现了优先级翻转。
  4. 为了防止这种情况发生,代码中使用了两个控制操作:

    • vTaskSuspendAll() 函数暂停任务切换,即暂停调度器,防止在运行其他任务时当前任务修改队列的等待接收和等待发送列表。
    • prvLockQueue(pxQueue) 函数锁定队列,防止此时发生中断并触发了对等待接收和等待发送列表的修改。

通过挂起调度器和锁定队列,可以确保在任务B成功进入阻塞状态后,根据优先级高低来解除任务阻塞,防止优先级较低的任务A先于任务B解除阻塞,从而避免了优先级翻转的问题。

这样的保护措施可以确保在任务切换及队列操作过程中,保持任务的正确优先级顺序和避免优先级翻转对系统的影响。

③ 队列上锁代码

挂起调度器的代码在前面的文章中我们已经介绍过,这里不再赘述。

【学习日记】【FreeRTOS】调度器函数实现详解

此处介绍队列上锁代码:

  • 进入临界段
  • 标记队列控制块中的发送锁和接收锁
/*
 * Macro to mark a queue as locked.  Locking a queue prevents an ISR from
 * accessing the queue event lists.
 */
#define prvLockQueue( pxQueue )								\
	taskENTER_CRITICAL();									\
	{														\
		if( ( pxQueue )->cRxLock == queueUNLOCKED )			\
		{													\
			( pxQueue )->cRxLock = queueLOCKED_UNMODIFIED;	\
		}													\
		if( ( pxQueue )->cTxLock == queueUNLOCKED )			\
		{													\
			( pxQueue )->cTxLock = queueLOCKED_UNMODIFIED;	\
		}													\
	}														\
	taskEXIT_CRITICAL()
/*-----------------------------------------------------------*/

6. 检查延时是否到期函数 xTaskCheckForTimeOut()

① 使用

传入超时状态结构和设置的超时时长进行检查:

if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
	{
		//...
	}

② 主要逻辑

  1. 当使能了任务挂起且设定延时时间为 portMAX_DELAY 时,任务将永远被阻塞不会超时
  2. 如果现在的时间比进入延时的时间大 且 现在的时间计时溢出过,这意味着时基计数已经跑过一轮了,那么延时肯定到期了
  3. 或者是当前时间减进入时间 < 需要等待的时间,意味着延时还未到期
    • 现在需要等待的时间 = 原来需要等待的时间 - 已经等待的时间

③ 源代码

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

7. 将任务插入任务等待发送列表 vTaskPlaceOnEventList()

① 使用

用于将延时未到期且暂时无法入队的任务插入任务等待列表:

  • 传入指向任务等待发送列表的指针
  • 传入要延时的时长
vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait );//等待发送的任务放进等待发送列表中

② 函数定义

  • 将当前 TCB 的事件项目插入 xTasksWaitingToSend 标识该任务正在等待发送消息
  • 将当前 TCB 插入延时列表中进行阻塞延时
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 );
}
/*-----------------------------------------------------------*/

8. 队列解锁函数 prvUnlockQueue()

① 函数原型

/* Constants used with the cRxLock and cTxLock structure members. */
#define queueUNLOCKED					( ( int8_t ) -1 )
#define queueLOCKED_UNMODIFIED			( ( int8_t ) 0 )


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();
}
/*-----------------------------------------------------------*/

② 函数结构分析

这段代码是 FreeRTOS 中的 prvUnlockQueue 函数的实现。以下是函数执行的主要步骤:

  1. 进入临界区。
  2. 检查发送锁的计数器 cTxLock,如果大于 queueLOCKED_UNMODIFIED(表示有锁被添加或删除),则执行以下步骤:
    a. 如果队列是队列集的成员,且添加数据导致更高优先级的任务解除阻塞,则记录需要进行任务切换。
    b. 否则,从等待接收任务列表中移除任务,并记录需要进行任务切换。
    c. 递减 cTxLock 计数器。
  3. 将发送锁设置为解锁状态。
  4. 再次进入临界区。
  5. 检查接收锁的计数器 cRxLock,如果大于 queueLOCKED_UNMODIFIED(表示有锁被添加或删除),则执行以下步骤:
    a. 从等待发送任务列表中移除任务,并记录需要进行任务切换。
    b. 递减 cRxLock 计数器。
  6. 将接收锁设置为解锁状态。
  7. 离开临界区。

可以看到,忽略锁的计数器操作(锁的计数器主要用于对队列的并发访问),3 和 6 说明只要调用这个函数,队列的等待发送任务列表和等待接收任务列表都会解锁。

③ 计数锁的原理

在FreeRTOS中,队列的cRxLock大于0的情况是在调用接收数据的API(比如xQueueReceive()函数)时。这是由于队列需要在多任务环境中保持线程安全性。当一个任务正在从队列接收数据时,会通过增加cRxLock的计数来锁定队列,防止其他任务同时进行接收操作。

具体来说,以下情况会使得cRxLock大于0:

  • 任务 A 调用了接收数据的 API,队列的 cRxLock 值增加到 1。
  • 在任务 A 还未完成接收操作之前,另一个任务 B 也调用了接收数据的 API,此时 cRxLock 值增加到 2。
  • 当任务 A 完成接收操作后,cRxLock 值减少到 1。
  • 当任务 B 完成接收操作后,cRxLock 值减少到 0。

这个锁定机制确保了队列在多任务环境中的正确操作,避免了竞态条件的发生。

四、用于中断中的 通用的消息发送函数 xQueueGenericSendFromISR()

上述的任务中的消息发送函数 API 经过宏定义展开后实际上就是传入不同参数的 xQueueGenericSendFromISR(),下面将对这个函数进行详细的介绍。

1. 函数概要

  • 这个函数用于在中断中向队列发送消息,由于是在中断中使用的,所以不带阻塞机制
  • 队列未满时或者可以覆盖写入时直接进行消息拷贝,否则直接返回队列已满错误
    • 因为是在中断中执行,需要对队列发送锁进行判断和操作
      • 如果队列发送锁是解锁状态,表示没有任务正在进行队列的发送操作,因此其他任务可以尝试从阻塞状态中恢复,并依据优先级高低进行上下文切换
      • 如果队列发送锁是上锁状态,每调用一次 xQueueGenericSendFromISR(),队列发送锁就自增 1,任务需要等待锁释放才能进行队列的发送操作,队列发送锁记录队列上锁时有多少数据尝试入队

2. 函数代码

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;
}
/*-----------------------------------------------------------*/

五、消息读取 API

1. 简介

  • 任务中读取消息时,也可以指定阻塞超时时间,在队列中没有消息可供读取时对任务进行阻塞。队列中存入数据后自动转为就绪态进行数据读取或者等到超时后变回就绪态
  • 而中断中读取消息时,就不能指定阻塞机制,但同样可以输入一个提示是否进行上下文切换的参数来进行上下文的切换判断
  • 读取消息的函数分为读完将消息从队列中删除读完后将消息放回队列中原位的两种函数

2. xQueueReceive()

① 原型及作用

  • 用于从一个队列中接收消息并把消息从队列中删除
  • 接收的消息是以拷贝的形式进行的
BaseType_t xQueueReceive(QueueHandle_t xQueue,
 void *pvBuffer,
 TickType_t xTicksToWait);
  • xQueue 队列句柄
  • pvBuffer 指针,指向接收到要保存的数据
  • xTicksToWait 队列空时,阻塞超时的最大时间。如果该参数设置为 0,函数立刻返回。超时时间的单位为系统节拍周期,常量 portTICK_PERIOD_MS 用于辅助计算真实的时间,单位为 ms。如果 INCLUDE_vTaskSuspend 设置成 1,并且指定延时为 portMAX_DELAY 将导致任务无限阻塞(永远不会超时)

② 宏展开

 #define xQueueReceive( xQueue, pvBuffer, xTicksToWait ) \
		xQueueGenericReceive( ( xQueue ), ( pvBuffer ), \
		( xTicksToWait ), pdFALSE )

③ 应用

static void Receive_Task(void* parameter)
{
    BaseType_t xReturn = pdTRUE; /* 定义一个创建信息返回值,默认为pdPASS */
    uint32_t r_queue; /* 定义一个接收消息的变量 */

    while (1) {
        xReturn = xQueueReceive(Test_Queue, /* 消息队列的句柄 */
                                &r_queue, /* 接收的消息内容 */
                                portMAX_DELAY); /* 等待时间 一直等 */

        if (pdTRUE == xReturn)
            printf("本次接收到的数据是:%d\n\n", r_queue);
        else
            printf("数据接收出错,错误代码: 0x%lx\n", xReturn);
    }
}

3. xQueuePeek()

① 作用

xQueuePeek()函数接收消息完毕不会删除消息队列中的消息。

② 宏展开

#define xQueuePeek( xQueue, pvBuffer, xTicksToWait ) \
		xQueueGenericReceive( ( xQueue ), ( pvBuffer ), \
		( xTicksToWait ), pdTRUE )

4. xQueueReceiveFromISR() 与 xQueuePeekFromISR()

  • 类同于上文 API,但是只能在中断中使用
  • 队列项接收成功返回 pdTRUE,否则返回 pdFALSE

① xQueueReceiveFromISR() 使用示例

QueueHandle_t xQueue;

/* 创建一个队列,并往队列里面发送一些数据 */
void vAFunction(void *pvParameters)
{
    char cValueToPost;
    const TickType_t xTicksToWait = (TickType_t)0xff;

    /* 创建一个可以容纳10个字符的队列 */
    xQueue = xQueueCreate(10, sizeof(char));
    if (xQueue == 0) {
        /* 队列创建失败 */
    }

    /* ... 任务其他代码 */

    /* 往队列里面发送两个字符
    如果队列满了则等待xTicksToWait个系统节拍周期*/
    cValueToPost = 'a';
    xQueueSend(xQueue, (void *)&cValueToPost, xTicksToWait);
    cValueToPost = 'b';
    xQueueSend(xQueue, (void *)&cValueToPost, xTicksToWait);

    /* 继续往队列里面发送字符
    当队列满的时候该任务将被阻塞 */
    cValueToPost = 'c';
    xQueueSend(xQueue, (void *)&cValueToPost, xTicksToWait);
}

/* 中断服务程序:输出所有从队列中接收到的字符 */
void vISR_Routine(void)
{
    BaseType_t xTaskWokenByReceive = pdFALSE;
    char cRxedChar;

    while (xQueueReceiveFromISR(xQueue,
                                (void *)&cRxedChar,
                                &xTaskWokenByReceive)) {

        /* 接收到一个字符,然后输出这个字符 */
        vOutputCharacter(cRxedChar);

        /* 如果从队列移除一个字符串后唤醒了向此队列投递字符的任务,
        那么参数xTaskWokenByReceive将会设置成pdTRUE,这个循环无论重复多少次,
        仅会有一个任务被唤醒 */
    }

    if (xTaskWokenByReceive != pdFALSE) {
        /* 我们应该进行一次上下文切换,当ISR返回的时候则执行另外一个任务 */
        /* 这是一个上下文切换的宏,不同的处理器,具体处理的方式不一样 */
        taskYIELD();
    }
}

六、通用读取消息函数 xQueueGenericReceive()

1. 函数结构分析

这段代码是FreeRTOS中的xQueueGenericReceive函数的实现。以下是函数执行的主要步骤:

  1. 检查输入参数的有效性。
  2. 进入临界区。
  3. 检查队列中是否有数据。
    • 如果有数据,则:
      • 如果只是查看数据(xJustPeeking为pdTRUE),则将数据复制到pvBuffer,并恢复读指针,然后从等待接收任务列表中移除任务。
      • 如果要删除数据(xJustPeeking为pdFALSE),则将数据复制到pvBuffer,并更新队列中的消息计数、处理互斥(如果队列类型是互斥),并从等待发送任务列表中移除任务。
      • 退出临界区,返回pdPASS表示成功。
    • 如果队列为空且等待时间为0,则退出临界区,返回errQUEUE_EMPTY表示队列为空。
    • 如果队列为空且需要等待一段时间,则设置超时结构体xTimeOut,并继续循环。
  4. 退出临界区。
  5. 挂起调度器,并锁定队列。
  6. 更新超时状态,并检查超时时间是否已过期。
    • 如果超时时间未过期且队列仍为空,则:
      • 如果队列是互斥队列类型,则提升互斥的持有任务的优先级。
      • 将任务添加到等待接收任务列表中,并解锁队列。
      • 恢复调度器状态,如果返回pdFALSE则进行任务切换。
    • 如果超时时间已过期,则返回errQUEUE_EMPTY表示队列为空。
    • 如果队列非空,则继续循环。
  7. 循环直到成功或超时。

总之,这个函数的主要功能是从队列中接收数据,并在数据不可用时进行阻塞等待,直到有数据可用或超时。它还处理了互斥队列和任务优先级的协调。

2. 函数定义

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();
			}
		}
	}
}
/*-----------------------------------------------------------*/

四、性能提示

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

后记

如果您觉得本文写得不错,可以点个赞激励一下作者!
如果您发现本文的问题,欢迎在评论区或者私信共同探讨!
共勉!

你可能感兴趣的:(RTOS,FreeRTOS,RTOS,stm32,嵌入式硬件,keil,学习)