对FreeRTOS的深入理解(二)

        接着对FreeRTOS的深入理解(一),上文是从任务创建开始进入这个操作系统,那么跳过自己创建任务的步骤,也是可以的,理由在下文就会清晰。本文讲述的是任务调度器初始化及开始调度的部分,希望各位大佬看见错了不要喷我谢谢,这些文章只是我对于FreeRTOS的源码个人的理解与整理。

        我们从vTaskStartScheduler();这个开启任务调度的API出发,首先,FreeRTOS会先创造一个空闲任务,当然会根据是使用什么样的分配方式来创建这个空闲任务,一般是动态分配。这个空闲任务的创建就解释了为什么我说创建自己任务的步骤可以跳过,因为这里固定会有一个空闲任务,这样的话你的所有任务列表的初始化是一定能得到保证的。

/* The Idle task is being created using dynamically allocated RAM. */
xReturn = xTaskCreate(	prvIdleTask,
						"IDLE", configMINIMAL_STACK_SIZE,
						( void * ) NULL,
						( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
						&xIdleTaskHandle );

        

        第二步会去创建一个定时器任务,也就是xTimerCreateTimerTask();。在这里面,首先FreeRTOS会去初始化两个关于定时器的列表,一个是pxCurrentTimerList(这里认为是一个为当前正在运行的定时器列表,即其中存储了当前正在执行的定时器),还有一个是pxOverflowTimerList(其中存储了因为某些原因(比如定时器事件太多)而无法立即执行的定时器),然后会去创建一个关于TIMER的消息队列,并且在注册表中注册消息队列,下文有一些关于这个消息队列的描述,对应源码如下几句:

xTimerQueue = xQueueCreate( ( UBaseType_t ) configTIMER_QUEUE_LENGTH, sizeof( DaemonTaskMessage_t ) );
vQueueAddToRegistry( xTimerQueue, "TmrQ" );

做完这些FreeRTOS会去创建一个定时器任务(注意此处为任务,上文为消息队列),这个定时器创建出来,作用可以从他的任务句柄中查看,其任务句柄为prvTimerTask,进入后源码如下:

static void prvTimerTask( void *pvParameters )
{
TickType_t xNextExpireTime;
BaseType_t xListWasEmpty;

	/* Just to avoid compiler warnings. */
	( void ) pvParameters;

	#if( configUSE_DAEMON_TASK_STARTUP_HOOK == 1 )
	{
		extern void vApplicationDaemonTaskStartupHook( void );

		/* Allow the application writer to execute some code in the context of
		this task at the point the task starts executing.  This is useful if the
		application includes initialisation code that would benefit from
		executing after the scheduler has been started. */
		vApplicationDaemonTaskStartupHook();
	}
	#endif /* configUSE_DAEMON_TASK_STARTUP_HOOK */

	for( ;; )
	{
		/* Query the timers list to see if it contains any timers, and if so,
		obtain the time at which the next timer will expire. */
		xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );

		/* If a timer has expired, process it.  Otherwise, block this task
		until either a timer does expire, or a command is received. */
		prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );

		/* Empty the command queue. */
		prvProcessReceivedCommands();
	}
}

开始有一个宏定义的if是你有没有开启守护进程,关于守护进程的具体用法和详细概念作者实力有限,说不太出来,反正简单而言守护进程 (daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。 很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。 另一些只在需要的时候才启动,完成任务后就自动结束。对于守护进程,学习linux的小伙伴可以去深入研究一下这东西,作者反正还没用过这玩意,这里不过多赘述。

        在for循环中,有以下三步,第一步是去获取当前pxCurrentTimerList列表中即将到期的定时器到期时间。第二步源码如下:

static void prvProcessTimerOrBlockTask( const TickType_t xNextExpireTime, BaseType_t xListWasEmpty )
{
TickType_t xTimeNow;
BaseType_t xTimerListsWereSwitched;

	vTaskSuspendAll();
	{
		/* Obtain the time now to make an assessment as to whether the timer
		has expired or not.  If obtaining the time causes the lists to switch
		then don't process this timer as any timers that remained in the list
		when the lists were switched will have been processed within the
		prvSampleTimeNow() function. */
		xTimeNow = prvSampleTimeNow( &xTimerListsWereSwitched );
		if( xTimerListsWereSwitched == pdFALSE )
		{
			/* The tick count has not overflowed, has the timer expired? */
			if( ( xListWasEmpty == pdFALSE ) && ( xNextExpireTime <= xTimeNow ) )
			{
				( void ) xTaskResumeAll();
				prvProcessExpiredTimer( xNextExpireTime, xTimeNow );
			}
			else
			{
				/* The tick count has not overflowed, and the next expire
				time has not been reached yet.  This task should therefore
				block to wait for the next expire time or a command to be
				received - whichever comes first.  The following line cannot
				be reached unless xNextExpireTime > xTimeNow, except in the
				case when the current timer list is empty. */
				if( xListWasEmpty != pdFALSE )
				{
					/* The current timer list is empty - is the overflow list
					also empty? */
					xListWasEmpty = listLIST_IS_EMPTY( pxOverflowTimerList );
				}

				vQueueWaitForMessageRestricted( xTimerQueue, ( xNextExpireTime - xTimeNow ), xListWasEmpty );

				if( xTaskResumeAll() == pdFALSE )
				{
					/* Yield to wait for either a command to arrive, or the
					block time to expire.  If a command arrived between the
					critical section being exited and this yield then the yield
					will not cause the task to block. */
					portYIELD_WITHIN_API();
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
		else
		{
			( void ) xTaskResumeAll();
		}
	}
}

FreeRTOS使用prvSampleTimeNow判断当前系统时钟是否溢出(通过比较xTimeNow和xLastTime),并及时交换两个TimerList(之前的文章中应该有说过那两个定时器列表),如下所示:

static TickType_t prvSampleTimeNow( BaseType_t * const pxTimerListsWereSwitched )
{
TickType_t xTimeNow;
PRIVILEGED_DATA static TickType_t xLastTime = ( TickType_t ) 0U; /*lint !e956 Variable is only accessible to one task. */

	xTimeNow = xTaskGetTickCount();

	if( xTimeNow < xLastTime )
	{
		prvSwitchTimerLists();
		*pxTimerListsWereSwitched = pdTRUE;
	}
	else
	{
		*pxTimerListsWereSwitched = pdFALSE;
	}

	xLastTime = xTimeNow;

	return xTimeNow;
}

        然而在交换过程中就考虑到当前这个定时器列表中的定时器是否到期了以及对那些能够自动重装载定时器的安置问题。这里直接上源码:

static void prvSwitchTimerLists( void )
{
TickType_t xNextExpireTime, xReloadTime;
List_t *pxTemp;
Timer_t *pxTimer;
BaseType_t xResult;

	/* The tick count has overflowed.  The timer lists must be switched.
	If there are any timers still referenced from the current timer list
	then they must have expired and should be processed before the lists
	are switched. */
	while( listLIST_IS_EMPTY( pxCurrentTimerList ) == pdFALSE )
	{
		xNextExpireTime = listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxCurrentTimerList );

		/* Remove the timer from the list. */
		pxTimer = ( Timer_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxCurrentTimerList );
		( void ) uxListRemove( &( pxTimer->xTimerListItem ) );
		traceTIMER_EXPIRED( pxTimer );

		/* Execute its callback, then send a command to restart the timer if
		it is an auto-reload timer.  It cannot be restarted here as the lists
		have not yet been switched. */
		pxTimer->pxCallbackFunction( ( TimerHandle_t ) pxTimer );

		if( pxTimer->uxAutoReload == ( UBaseType_t ) pdTRUE )
		{
			/* Calculate the reload value, and if the reload value results in
			the timer going into the same timer list then it has already expired
			and the timer should be re-inserted into the current list so it is
			processed again within this loop.  Otherwise a command should be sent
			to restart the timer to ensure it is only inserted into a list after
			the lists have been swapped. */
			xReloadTime = ( xNextExpireTime + pxTimer->xTimerPeriodInTicks );
			if( xReloadTime > xNextExpireTime )
			{
				listSET_LIST_ITEM_VALUE( &( pxTimer->xTimerListItem ), xReloadTime );
				listSET_LIST_ITEM_OWNER( &( pxTimer->xTimerListItem ), pxTimer );
				vListInsert( pxCurrentTimerList, &( pxTimer->xTimerListItem ) );
			}
			else
			{
				xResult = xTimerGenericCommand( pxTimer, tmrCOMMAND_START_DONT_TRACE, xNextExpireTime, NULL, tmrNO_DELAY );
				configASSERT( xResult );
				( void ) xResult;
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}

	pxTemp = pxCurrentTimerList;
	pxCurrentTimerList = pxOverflowTimerList;
	pxOverflowTimerList = pxTemp;
}

        可以看到while遍历了整个定时器列表中的所有在列表中的定时器,至于为什么说这里遍历的所有定时器一定是过期的呢?

        其实,作者现在还没搞明白,但是人家注释上说must have expired,这个可能需要结合那些FreeRTOS定时器运作机理再去理解,这里目前先打个问号。那么对于这些已经到期的定时器,那就一个不留,该执行执行该删除删除。对于自动重载的定时器,根据其到期时间和周期重新计算新的到期时间,并根据情况将其重新插入到当前列表中,这是ChatGPT的简单说辞,作者这里道行还不够,等慢慢研究透了再来补充这部分。

        ok,从因为系统时间溢出需要交换定时器列表回来分支回来,那么prvSampleTimeNow这部分总体就是在检查系统时间是否溢出和做溢出时把没人要的定时器处理掉的事情 。那么当系统时间没有溢出,并且当前定时器列表中不为空、有定时器已经过期了,那么就要开始出动了,恢复所有被挂起的任务(这可能是为了确保其他任务能够及时响应定时器到期的事件),然后直接用prvProcessExpiredTimer这个API去把过期的定时器掏出来然后执行对应的回调函数

        因为每次FreeRTOS确定这个过期的pxTimer都是直接找定时器列表的头,所以我觉得可能定时器列表是定时器过期时间升序排列的列表,这里先打个问号。那么对于自动重装载的定时器,如果定时器在添加到活动定时器列表之前就已经到期了,就重新加载定时器并触发相应的命令来启动定时器,这里也是ChatGPT智慧体的理解,等之后深入了解定时器这方面在详细讨论。

        剩下的就是没有定时器任务过期的部分,这部分要做的就是等待下一个到期时间或者接收到新的命令。这里可以看到上文那个不明所以的xTimerQueue定时器消息队列出现了,源码中使用了 vQueueWaitForMessageRestricted 函数来等待消息到达。如果有消息到达,可以触发相应的事件,例如开启某些定时器或执行特定的任务。所以这个队列通常用于存储定时器事件或者与定时器相关的消息,例如定时器到期时需要执行的任务或者发送的消息等

        所以第二步prvProcessTimerOrBlockTask(...);这个API大概做的事情就是检查系统时间是否溢出和做溢出时把没人要的定时器处理掉的事情 、执行过期定期器的定时器回调函数

        这个系统创建的定时器任务最后一步我把ChatGPT的解释放在下面,有兴趣的可以看一下。

这段代码是用于处理接收到的命令,主要是针对定时器的操作。让我逐步解释一下:

  1. vQueueWaitForMessageRestricted 函数用于从 xTimerQueue 队列中等待消息到达,超时时间为 tmrNO_DELAY,即立即返回,不会阻塞等待。

  2. 当从队列中接收到消息时(xQueueReceive 函数返回值不为 pdFAIL),会进入循环处理接收到的命令。

  3. 首先会检查接收到的命令是否是负数,如果是负数,则表示这是一个 pended function call,而不是定时器命令。这里会执行相应的回调函数。

  4. 对于正数的命令(大于等于0),表示这是一个定时器命令。会根据命令的不同执行不同的操作:

    • 如果是启动、重启、或者重置定时器的命令,则会根据命令参数设置新的到期时间,并将定时器插入到活动列表中,如果定时器已经过期则立即处理。
    • 如果是停止定时器的命令,则不做任何操作。
    • 如果是改变定时器周期的命令,则会更新定时器的周期,并重新插入到活动列表中。
    • 如果是删除定时器的命令,则会释放定时器的内存空间。

总的来说,这段代码负责处理接收到的定时器命令,包括启动、停止、重置、改变周期和删除定时器等操作。

        那么总的来说这个FreeRTOS创建的定时器任务是用来管理我们自己创建的定时器的,并且FreeRTOS也为我们创建了定时器的消息队列,为我们使用定时器提供了灵活的功能。

        第三步,关闭中断进入临界区(防止外来中断干扰),会将一些系统运行的基本参数更新,源码如下 :

xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE;
xTickCount = ( TickType_t ) 0U;

TickCount这里应该是记录系统时间;xNextTaskUnblockTime记录阻塞时间最近的任务他的阻塞时间,或是说在等到以当前时间+xNextTaskUnblockTime有一个任务将从suspend等待列表放入ready就绪列表中。这里将这三个变量初始化。

        第四步,将会运行到这个portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); API上。但是这个API是自定义的,#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() ConfigureTimeForRunTimeStats(),在STM32的例程中,他最终会是定义一个变量记录系统时间,并且初始化好为FreeRTOS提供时基的定时器,如下:

void ConfigureTimeForRunTimeStats(void){
	FreeRTOSRunTimeTicks=0;
	TIM3_Int_Init(50-1,84-1);	
}

但是我在使用ZYNQ开发时,Vivado SDK创建的FreeRTOS模板并没有在这里就配置,而是在开启任务调度之前做了这些配置操作(配置zynq软件定时器,将FreeRTOS的时钟中断和定时器中断相连接),这里姑且先当作统计任务运行时间,初始化定时器。

        

        第五步,开启任务调度器xPortStartScheduler()。在这里,1、FreeRTOS会先去检测用户在FreeRTOSConfig.h文件中的配置是否正确并将这这些正确的配置写入相应的寄存器,如下:

configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );
configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );

#if( configASSERT_DEFINED == 1 ){
		volatile uint32_t ulOriginalPriority;
		volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
		volatile uint8_t ucMaxPriorityValue;

		ulOriginalPriority = *pucFirstUserPriorityRegister;

		*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;

		ucMaxPriorityValue = *pucFirstUserPriorityRegister;

		configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );

		ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

		ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
		while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
		{
			ulMaxPRIGROUPValue--;
			ucMaxPriorityValue <<= ( uint8_t ) 0x01;
		}

		ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
		ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

		*pucFirstUserPriorityRegister = ulOriginalPriority;
	}
	#endif /* conifgASSERT_DEFINED */

        2、如果是M3-M4内核(目前只用过这两个内核),那么在我看到的FreeRTOS版本源码中,会将Systick滴答定时器的中断、PendSV(管理任务切换,上下文切换)中断的优先级设置为最低。但是在A9内核中,经查阅资料Contex-A处理核一般跑Linux那种操作系统,中断优先级和中断调度一般都是由操作系统管理,在A核中没有相应寄存器,但是在M核中是有的

/* Make PendSV and SysTick the lowest priority interrupts. */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

/* Start the timer that generates the tick ISR.  Interrupts are disabledhere already. */
vPortSetupTimerInterrupt();

        3、初始化临界区计数变量以及开启浮点处理器VFP,这个临界区计数变量是一对儿记录1个,如果你疯狂嵌套进入好几次临界区,到头来如果这个变量不是0,那惨了,你这要出问题了,如下:

/* Initialise the critical nesting count ready for the first task. */
uxCriticalNesting = 0;

/* Ensure the VFP is enabled - it should be anyway. */
prvEnableVFP();

        4、汇编开启第一个任务,启用SVC中断。

__asm void prvStartFirstTask( void )
{
	PRESERVE8

	/* Use the NVIC offset register to locate the stack. */
	ldr r0, =0xE000ED08
	ldr r0, [r0]
	ldr r0, [r0]
	/* Set the msp back to the start of the stack. */
	msr msp, r0
	/* Globally enable interrupts. */
	cpsie i
	cpsie f
	dsb
	isb
	/* Call SVC to start the first task. */
	svc 0
	nop
	nop
}

        至此,本文结束,这篇文章主要讲述了任务调度器开始部分,FreeRTOS是如何初始化一些基本参数和环境的,下篇文章将会讲述任务调度机制。

  • 28
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值