跟着野火从零开始手搓FreeRTOS(5)空闲任务与阻塞延时

        裸机系统的软件延时通过让CPU等待来达到延时效果;而RTOS的优点就是完全发挥CPU的性能,即使延时也不会让CPU空等。操作系统的延时叫做阻塞延时,当任务需要延时的时候,任务会放弃CPU的使用权,直到延时结束,重新获得CPU进行运行。在延时期间,CPU可以处理其他的事情。

        在当前任务进入阻塞状态时,CPU可以去处理别的任务。假如当前没有别的任务可运行,CPU就会进入系统创建的空闲任务。空闲任务在调度器里是优先级最低的任务,其主体主要是做一些系统内存的清理工作。在实际应用中,当系统进入空闲任务后,可以让单片机进入休眠或者低功耗等操作。

        这里野火让空闲任务实现对一个全局变量进行计数的功能。 

实现空闲任务

        目前创建栈和任务控制块都是静态创建,使用静态内存。因此,需要预先定义内存空间。

定义空闲任务栈和任务控制块

        栈和TCB的内存空间均在main.c中定义。

       任务栈是一个定义好的数组,大小由 FreeRTOSConfig.h 中定义的宏  控制,默认为 128, 单位为字(byte),即 512 个字节(bit)。

/* 定义空闲任务的栈 */
/* 在FreeRTOSConfig.h中定义 */
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )

/* 获取空闲任务的内存 */
/* 在main.c中定义 */
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
TCB_t IdleTaskTCB; //定义空闲任务发任务控制块

创建任务

        在定义好空闲任务的栈和任务控制块后,才可以创建空闲任务。空闲任务在调度器启动函数 vTaskStartScheduler()中创建。

        该部分在task.c中实现。

extern TCB_t IdleTaskTCB;

/* 传参函数声明 */
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer, 
                                    StackType_t **ppxIdleTaskStackBuffer, 
                                    uint32_t *pulIdleTaskStackSize );

void vTaskStartScheduler( void )
{   
    TCB_t *pxIdleTaskTCBBuffer = NULL;               /* 用于指向空闲任务控制块 */
    StackType_t *pxIdleTaskStackBuffer = NULL;       /* 用于空闲任务栈起始地址 */
    uint32_t ulIdleTaskStackSize;
    
    /* 获取空闲任务的内存:任务栈和任务TCB */
    vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, 
                                   &pxIdleTaskStackBuffer, 
                                   &ulIdleTaskStackSize );    
    
    xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask,              /* 任务入口 */
					                     (char *)"IDLE",                           /* 任务名称,字符串形式 */
					                     (uint32_t)ulIdleTaskStackSize ,           /* 任务栈大小,单位为字 */
					                     (void *) NULL,                            /* 任务形参 */
					                     (StackType_t *)pxIdleTaskStackBuffer,     /* 任务栈起始地址 */
					                     (TCB_t *)pxIdleTaskTCBBuffer );           /* 任务控制块 */
    /* 将任务添加到就绪列表 */                                 
    vListInsertEnd( &( pxReadyTasksLists[0] ), &( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem ) );
}

        vApplicationGetIdleTaskMemory()的前两个指针参数将作为形参传入空闲任务的TCB 与 栈的起始地址 ,这个函数是由用户自定义的,在task.c中声明,该部分已在上述代码中实现。实现代码在main.c中,具体代码如下:

void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer, 
                                    StackType_t **ppxIdleTaskStackBuffer, 
                                    uint32_t *pulIdleTaskStackSize )
{
		*ppxIdleTaskTCBBuffer=&IdleTaskTCB;
		*ppxIdleTaskStackBuffer=IdleTaskStack; 
		*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}

实现阻塞延时

        阻塞,就是在任务调用这个延时函数后,任务会失去CPU使用权,并进入阻塞状态。在延时结束后,重新得到CPU使用权,继续运行。在该任务在阻塞状态时,菜CPU可以去执行其他任务。如果当前没有可运行的任务,CPU就会进入空闲任务。

  阻塞延时函数vTaskDelay()

        阻塞延时函数在task.c中实现。

void vTaskDelay( const TickType_t xTicksToDelay )
{
    TCB_t *pxTCB = NULL;
    
    /* 获取当前任务的TCB */
    pxTCB = pxCurrentTCB;
    
    /* 设置延时时间 */
    pxTCB->xTicksToDelay = xTicksToDelay;
    
    /* 任务切换 */
    taskYIELD();
}

        xTicksToDelay 是任务控制块的成员,用于记录延时时间,单位为SysTick的中断周期。野火的中断周期为10ms,这里调用xTicksToDelay完成20ms的延时。

        带xTicksToDelay的任务控制块定义FreeRTOS.h下,具体如下:

typedef struct tskTaskControlBlock
{
	volatile StackType_t    *pxTopOfStack;    /* 栈顶 */

	ListItem_t			    xStateListItem;   /* 任务节点 */
    
    StackType_t             *pxStack;         /* 任务栈起始地址 */
	                                          /* 任务名称,字符串形式 */
	char                    pcTaskName[ configMAX_TASK_NAME_LEN ];

    TickType_t xTicksToDelay; /* 用于延时 */    
} tskTCB;

        这里需要把 task.c中的定义注释掉,之后再在FreeRTOS.h中定义。

修改 vTaskSwitchContext()

        调用 tashYIELD()(在portmacro.h定义)会产生 PendSV中断,在 PendSV中断服务函数中会调用上下文切换函数 vTaskSwitchContext(),其作用是寻找最高优先级的就绪任务,然后更新 pxCurrentTCB。 因为增加了一个空闲任务,所以需要让pxCurrentTCB 在这三个任务中切换,因此我们需要修改一下切换函数。

void vTaskSwitchContext( void )
{
	if( pxCurrentTCB == &IdleTaskTCB )
	{
		if(Task1TCB.xTicksToDelay == 0)
		{            
            pxCurrentTCB =&Task1TCB;
		}
		else if(Task2TCB.xTicksToDelay == 0)
		{
            pxCurrentTCB =&Task2TCB;
		}
		else
		{
			return;		/* 线程延时均没有到期则返回,继续执行空闲线程 */
		} 
	}
	else
	{
		if(pxCurrentTCB == &Task1TCB)
		{
			if(Task2TCB.xTicksToDelay == 0)
			{
                pxCurrentTCB =&Task2TCB;
			}
			else if(pxCurrentTCB->xTicksToDelay != 0)
			{
                pxCurrentTCB = &IdleTaskTCB;
			}
			else 
			{
				return;		/* 返回,不进行切换,因为两个线程都处于延时中 */
			}
		}
		else if(pxCurrentTCB == &Task2TCB)
		{
			if(Task1TCB.xTicksToDelay == 0)
			{
                pxCurrentTCB =&Task1TCB;
			}
			else if(pxCurrentTCB->xTicksToDelay != 0)
			{
                pxCurrentTCB = &IdleTaskTCB;
			}
			else 
			{
				return;		/* 返回,不进行切换,因为两个线程都处于延时中 */
			}
		}
	}
}

        如果当前任务是空闲任务的话,如果检测到另外两个任务的延时结束,那么就执行他们;延时没有结束则返回空闲任务。

        如果当前有一个任务在执行,那么就检查另外一个任务是否在延时。,如果另一个不在延时中,就切换到那任务;如果当前任务需要进行延时,那么就切换到空闲任务,否则继续运行当前任务。

SysTick

SysTick初始化

        SysTick 必须先初始化,然后中断服务函数才能被调用。 SysTick 初始化函数在 port.c 中定义。

/* SysTick 配置寄存器 */
#define portNVIC_SYSTICK_CTRL_REG			( * ( ( volatile uint32_t * ) 0xe000e010 ) )
#define portNVIC_SYSTICK_LOAD_REG			( * ( ( volatile uint32_t * ) 0xe000e014 ) )

/* 时钟源选择 */
#ifndef configSYSTICK_CLOCK_HZ
	#define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ

	/* 确保SysTick的时钟与内核时钟一致 */
	#define portNVIC_SYSTICK_CLK_BIT	( 1UL << 2UL )
#else
	#define portNVIC_SYSTICK_CLK_BIT	( 0 )
#endif

#define portNVIC_SYSTICK_INT_BIT			( 1UL << 1UL )
#define portNVIC_SYSTICK_ENABLE_BIT			( 1UL << 0UL )

void vPortSetupTimerInterrupt( void );

void vPortSetupTimerInterrupt( void )
{
     /* 设置重装载寄存器的值 */
    portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
    
    /* 设置系统定时器的时钟等于内核时钟
       使能SysTick 定时器中断
       使能SysTick 定时器 */
    portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | 
                                  portNVIC_SYSTICK_INT_BIT |
                                  portNVIC_SYSTICK_ENABLE_BIT ); 
}

        目前SysTick配置为100ms中断一次。

        SysTick的初始化函数将在xPortStartScheduler()中被调用。

        重装载寄存器的值决定着SysTick的中断周期。如果这个宏没有定义,就会默认等于cpu的时钟频率。configSYSTICK_CLOCK_HZ和configTICK_RATE_HZ都在FreeRTOSConfig.h中定义。

BaseType_t xPortStartScheduler( void )
{
/* 配置 PendSV 和 SysTick 的中断优先级为最低 */
    portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

/* 初始化 SysTick */
    vPortSetupTimerInterrupt();

/* 启动第一个任务,不再返回 */
    prvStartFirstTask();

/* 不应该运行到这里 */
    return 0;
}

中断服务函数

        在实现任务切换的过程中,需要判断每个任务的延时是否为0。当任务需要延时的时候,xTicksToDelay会进行递减至0。而延时的周期递减由SysTick中断提供,同时操作系统的最小时间单位就是SysTick的中断周期,一个周期被称为一个tick。中断函数在port.c中实现。

void xPortSysTickHandler( void )
{
	/* 关中断 */
    vPortRaiseBASEPRI();
    
    /* 更新系统时基 */
    xTaskIncrementTick();

	/* 退出临界段,开中断 */
    vPortClearBASEPRIFromISR();
}

更新时基函数xTaskIncrementTick()

        更新系统时基函数在 task.c 中定义。

void xTaskIncrementTick( void )
{
    TCB_t *pxTCB = NULL;
    BaseType_t i = 0;
    
    /* 更新系统时基计数器xTickCount,xTickCount是一个在port.c中定义的全局变量 */
    const TickType_t xConstTickCount = xTickCount + 1;
    xTickCount = xConstTickCount;

    
    /* 扫描就绪列表中所有线程的xTicksToDelay,如果不为0,则减1 */
	for(i=0; i<configMAX_PRIORITIES; i++)
	{
        pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &pxReadyTasksLists[i] ) );
		if(pxTCB->xTicksToDelay > 0)
		{
			pxTCB->xTicksToDelay --;
		}
	}
    
    /* 任务切换 */
    portYIELD();
}

        时基即周期,更新系统时基计数器 xTickCount, 加一操作。xTickCount 是在port.c 中定义的全局变量,在函数 vTaskStartScheduler()中调用 xPortStartScheduler()函数前初始化。

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值