延时函数从裸机到FreeRTOS的移植(超详细)

在FreeRTOS中可以使用的延时函数分为两类,第一类是操作系统提供的延时函数,另一类是自己编写的延时函数,也就是我们从裸机移植到操作系统中的延时函数。这两种函数各有优劣,下面就来详细介绍一下这些延时函数。

一、FreeRTOS中的延时函数

        FreeRTOS中的延时函数有相对模式和绝对模式,vTaskDelay()是相对模式,vTaskDelayUntil()是绝对模式,这些函数都是在FreeRTOS中封装好的,只需开启其宏定义即可使用,我们只需了解其函数接口调用即可。

	void vTaskDelay( const TickType_t xTicksToDelay )
	{
	BaseType_t xAlreadyYielded = pdFALSE;

		/* A delay time of zero just forces a reschedule. */
		if( xTicksToDelay > ( TickType_t ) 0U )
		{
			configASSERT( uxSchedulerSuspended == 0 );
			vTaskSuspendAll();
			{
				traceTASK_DELAY();

				/* A task that is removed from the event list while the
				scheduler is suspended will not get placed in the ready
				list or removed from the blocked list until the scheduler
				is resumed.

				This task cannot be in an event list as it is the currently
				executing task. */
				prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
			}
			xAlreadyYielded = xTaskResumeAll();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		/* Force a reschedule if xTaskResumeAll has not already done so, we may
		have put ourselves to sleep. */
		if( xAlreadyYielded == pdFALSE )
		{
			portYIELD_WITHIN_API();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
	{
	TickType_t xTimeToWake;
	BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;

		configASSERT( pxPreviousWakeTime );
		configASSERT( ( xTimeIncrement > 0U ) );
		configASSERT( uxSchedulerSuspended == 0 );

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

			/* Generate the tick time at which the task wants to wake. */
			xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;

			if( xConstTickCount < *pxPreviousWakeTime )
			{
				/* The tick count has overflowed since this function was
				lasted called.  In this case the only time we should ever
				actually delay is if the wake time has also	overflowed,
				and the wake time is greater than the tick time.  When this
				is the case it is as if neither time had overflowed. */
				if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
				{
					xShouldDelay = pdTRUE;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				/* The tick time has not overflowed.  In this case we will
				delay if either the wake time has overflowed, and/or the
				tick time is less than the wake time. */
				if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
				{
					xShouldDelay = pdTRUE;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}

			/* Update the wake time ready for the next call. */
			*pxPreviousWakeTime = xTimeToWake;

			if( xShouldDelay != pdFALSE )
			{
				traceTASK_DELAY_UNTIL( xTimeToWake );

				/* prvAddCurrentTaskToDelayedList() needs the block time, not
				the time to wake, so subtract the current tick count. */
				prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		xAlreadyYielded = xTaskResumeAll();

		/* Force a reschedule if xTaskResumeAll has not already done so, we may
		have put ourselves to sleep. */
		if( xAlreadyYielded == pdFALSE )
		{
			portYIELD_WITHIN_API();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}

        当执行vTaskDelay()和vTaskDelayUntil()时当前任务会进入阻塞态并进行任务切换,在A任务延时的时候会去执行B任务,这样的好处就是能充分利用CPU资源,提高利用率。

二、移植裸机中的延时函数

        裸机中的延时函数有很多实现方法,可以通过定时器实现,比如systick滴答定时器或者芯片的timer定时器资源或RTC时钟等等,也可以重复执行同样的操作比如__NOP()来实现延时。一般微秒级以上的使用定时器延时,纳秒级使用__NOP()延时。

         1、定时器延时(以systick为例)

        笔者选择以systick为例是因为它比较特殊,是FreeRTOS的系统时钟节拍计数器,这意味着我们不能使用完延时后关闭systick定时器,也不能随意更改systick的计数频率。

        所有的定时器延时都要考虑延时函数的可重入性问题,在裸机我们基本上不需要考虑这类问题,毕竟是单线程,不会在调用延时函数delay_ms的中途中又调用一次这个函数delay_ms。但是在操作系统中我们就得考虑这是否会造成冲突了,这个就叫函数的可重入性。

        不符合可重入性的函数是什么样的呢,举个例子:

//不满足可重入性的延时函数
void delay_ms(uint32_t count)
{	 		  	  
	u32 temp;		   
	SysTick->LOAD=(u32)count*fac_ms;			//时间加载(SysTick->LOAD为24bit)
	SysTick->VAL =0x00;           //清空计数器
	SysTick->CTRL=0x05 ;          //开始倒数  
	do
	{
		temp=SysTick->CTRL;
	}while((temp&0x01)&&!(temp&(1<<16)));	//等待时间到达,看CTRL的第16位(COUNTFLAG)是否为1,看STRL的第0位(ENABLE)是否为1   
	SysTick->CTRL=0x00;       //关闭计数器
	SysTick->VAL =0X00;       //清空计数器	  	    
} 

       systick是一个24位的倒数定时器,这里通过改变systick的重装载值,计算好重装载值让计数器倒数到0时刚好完成延时所需的时间并触发CTRL的第16位置高。在循环里死等直到检测到CTRL的第16位为1时跳出循环结束延时,然后关闭systick。这个延时函数在裸机上当然没有问题,但是移植到FreeRTOS中就有两个问题:首先systick作为操作系统时钟节拍不能关闭,其次不满足函数可重入性,假设线程A进入delay_ms,计数器重装载值开始倒数计时,这时另一个优先级更高的线程B打断线程A也进入了delay_ms,计数器又重装载值开始倒数计时,是不是就不满足可重入性了?

        这个可重入性问题也是可以解决的,只需要换一种方法来延时,这个方法叫时钟摘取法,我们不改变systick的配置,而是不断的读取CTRL和VAL的值,等到计数值满足延时时间要求跳出循环。

//满足可重入性的延时函数
void delay_ms(uint32_t count)
{
	uint8_t i;
	uint32_t told=SysTick->VAL;
	uint32_t ticks;
	uint32_t tnow;
	uint32_t tcnt=0;
	uint32_t reload=SysTick->LOAD;
	ticks=(SystemCoreClock/1000)*count;//计算完成延时时间所需的systick计数值
	told=SysTick->VAL;//开始延时前记录当前的计数器值
	while(1)//循环等待延时结束
	{
		tnow=SysTick->VAL;
		if(tnow!=told)
		{
			if(tnow<told)
				tcnt+=told-tnow;//记录systick的计数值
			else
				tcnt+=reload+told-tnow;//如果systick重装载了,记录systick的计数值
			told=tnow;
			if(tcnt>=ticks)//systick的计数值达到所需延时时间
				break;//结束延时
		}
	}
}

        工作原理注释里应该写的比较清楚了,如果不了解systick寄存器可以查一下手册,补充一点代码里SystemCoreClock是systick的时钟源即系统时钟,如果是微秒级延时delay_us就是ticks=(SystemCoreClock/1000000)*count。

        2、__NOP()延时

         在高精度的比如纳秒级延时中,受限于MCU的主频,systick的延时理论值往往和实际值相差甚远,以GD32F305为例,最高120M的系统时钟,理论上计数一次1/120M=8.3ns,可以纳秒延时,但实际上延时1微秒就已经有0.2微秒的误差了,推测是延时函数进栈出栈以及一些运算操作造成了延时误差,想要实现精度更高的ns级延时就需要用__NOP()来实现。比如GD32F305这款芯片120M系统时钟,那么一个机器周期就是1/120M=8.3333ns,调用一条汇编指令__NOP()就多花8.3333ns的时间,笔者通过逻辑分析仪抓波形实测用这个__NOP()实现ns级延时是非常准的,很好用,但是据说存在编译器优化导致延时不准的问题,所以使用__NOP()时最好不要开或者使用等级低的编译器优化。

总结

        FreeRTOS的延时函数vTaskDelay()和vTaskDelayUntil()可以通过任务调度在延时的进入阻塞态,执行其它任务,提高CPU利用率,但是任务调度需要花费时间,尤其是频繁的任务调度会浪费大量时间,微秒级或者精度更高的延时也会造成较大的延时误差。

        FreeRTOS的延时函数CPU利用率高,但执行效率低,裸机的延时函数执行效率高,但CPU利用率低,因此在选择操作系统和裸机的延时函数时需要我们去权衡利弊。延时时间较长的情况下适用FreeRTOS的延时函数vTaskDelay()和vTaskDelayUntil(),延时时间短的情况下适用裸机的定时器延时函数delay_us()。

  • 24
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
裸机代码移植FreeRTOS 可以按照以下步骤进行: 1. 确定目标平台和处理器架构:首先要确定你的裸机代码的目标平台和处理器架构,因为 FreeRTOS 需要根据目标平台进行适当的配置和调整。 2. 理解 FreeRTOS 架构:了解 FreeRTOS 的基本原理和架构,这将有助于你在移植过程中正确设置任务、中断和系统资源。 3. 创建 FreeRTOS 项目:在开始移植之前,创建一个合适的 FreeRTOS 项目,并确保你的开发环境正确配置。 4. 移植任务:将你的裸机任务转换为 FreeRTOS 任务。可以使用 FreeRTOS 提供的任务创建函数来创建任务,并使用任务优先级来调度任务。 5. 移植中断处理:如果你的裸机代码使用了中断处理函数,需要将这些中断处理函数转换为 FreeRTOS 的中断服务例程(ISR)。可以使用 FreeRTOS 提供的 API 来创建和管理中断服务例程。 6. 处理系统资源:如果你的裸机代码使用了共享资源(如内存、外设等),需要在移植过程中正确处理资源的访问和共享。可以使用 FreeRTOS 提供的资源管理函数来实现这些功能。 7. 配置 FreeRTOS:根据你的应用需求,对 FreeRTOS 进行适当的配置。这包括堆栈大小、调度策略、空闲任务处理等方面的设置。 8. 调试和测试:完成移植后,进行全面的调试和测试,确保 FreeRTOS 的功能和性能与原始裸机代码相匹配。 需要注意的是,裸机代码的移植FreeRTOS 可能需要一些重写和调整,因为 FreeRTOS 是一个实时操作系统,具有自己的任务调度、中断管理和资源分配机制。这就需要你在移植过程中仔细理解 FreeRTOS 的特性,并相应地修改你的裸机代码。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值