FreeRTOS-空闲任务和阻塞延时的实现

根据个人的学习方向,学习FreeRTOS。由于野火小哥把FreeRTOS讲得比较含蓄,打算在本专栏尽量细化一点。作为个人笔记,仅供参考或查阅。

配套资料:FreeRTOS内核实现与应用开发实战指南、野火FreeRTOS配套视频源码、b站野火FreeRTOS视频。搭配来看更佳哟!!!   

空闲任务与阻塞延时

FreeRTOS中的延时叫阻塞延时,即任务需要延时的时候,任务会放弃CPU的使用权,CPU转而去干其他的事情。当任务延时时间到,重新获取CPU使用权,任务继续运行。

细里说,当任务性需要延时时,进入阻塞状态。如果没有其他任务可以运行,FreeRTOS都会为CPU创建一个空闲任务,这个时候CPU就运行空闲任务。

在FreeRTOS中,空闲任务是系统在“启动任务调度器”的时候创建的优先级最低的任务,空闲任务主体主要是做一些系统内存的清理工作。

在实际应用中,当系统进入空闲任务时,可在空闲任务中让单片机进入休眠或低功耗等操作。在本节中,简易操作,在空闲任务中对一个全局变量进行计数。

实现空闲任务

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

#define configMINIMAL_STACK_SIZE	( ( unsigned short ) 128 )
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
TCB_t IdleTaskTCB;

创建空闲任务

空闲任务在任务调度器启动函数vTaskStartScheduler()中创建。

void vTaskStartScheduler( void )
{    
    /*创建空闲任务start*/
    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 ) );
    /*创建空闲任务end*/
                                         
    /* 手动指定第一个运行的任务 */
    pxCurrentTCB = &Task1TCB;
                                         
    /* 初始化系统时基计数器,与阻塞延时、Systick有关,下文会讲 */
    xTickCount = ( TickType_t ) 0U;
    
    /* 启动调度器 */
    if( xPortStartScheduler() != pdFALSE )
    {
        /* 调度器启动成功,则不会返回 */
    }
}

上面函数用到了vApplicationGetIdleTaskMemory函数。如下

/* 获取空闲任务的内存 */
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
TCB_t IdleTaskTCB;

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

空闲任务创建完毕。

阻塞延时

阻塞延时函数vTaskDelay()在task.c中定义

在实现阻塞延时函数之前,看一下TCB的结构体定义

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

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

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

可以看出,TCB的结构体定义比以前多了一个参数,TCB的结构体定义逐步完善。

xTicksToDelay用于延时,单位为Systick的中断周期,本节下面会讲,这里不展开。比如Systick的中断周期为10ms,调用vTaskDelay(2)则完成2*10ms的延时。

阻塞延时函数如下

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

上面函数中,任务切换时,调用taskYIELD()会产生PendSV中断,在PendSV中断服务函数中会调用上下文切换函数vTaskSwitchContext()。

该函数的作用是寻找最高优先级的就绪任务,然后更新pxCurrentTCB。

上下文切换函数如下。实现三个任务间的转换。

void vTaskSwitchContext( void )
{
	/* 如果当前线程是空闲线程,那么就去尝试执行线程1或者线程2,看看他们的延时时间是否结束,如果线程的延时时间均没有到期,那就返回继续执行空闲线程 */
	if( pxCurrentTCB == &IdleTaskTCB )         //当前TCB是空闲TCB,那么Task1和Task2哪个的延时结束就执行哪个Task,都没有延时结束,则继续执行空闲Task
	{
		if(Task1TCB.xTicksToDelay == 0)        //Task1TCB的延时结束     
		{            
            pxCurrentTCB =&Task1TCB;           //执行Task1
		}
		else if(Task2TCB.xTicksToDelay == 0)   //Task2TCB的延时结束     
		{
            pxCurrentTCB =&Task2TCB;           //执行Task2
		}
		else
		{
			return;		/* 线程延时均没有到期则返回,继续执行空闲线程 */
		} 
	}
	else                                       //当前TCB不是空闲任务
	{
		/*如果当前线程是线程1或者线程2的话,检查下另外一个线程,如果另外的线程不在延时中,就切换到该线程
        否则,判断下当前线程是否应该进入延时状态,如果是的话,就切换到空闲线程。否则就不进行任何切换 */
		if(pxCurrentTCB == &Task1TCB)          //当前TCB是Task1TCB
		{
			if(Task2TCB.xTicksToDelay == 0)    //Task2TCB的延时结束 
			{
                pxCurrentTCB =&Task2TCB;       //执行Task2
			}
			else if(pxCurrentTCB->xTicksToDelay != 0)    //Task1TCB和Task2TCB延时还没到零
			{
                pxCurrentTCB = &IdleTaskTCB;   //进入空闲任务TCB 
			}
			else 
			{
				return;		/* 返回,不进行切换,因为两个线程都处于延时中 */
			}
		}
		else if(pxCurrentTCB == &Task2TCB)     //当前TCB是Task2TCB
		{
			if(Task1TCB.xTicksToDelay == 0)    //Task1TCB的延时结束 
			{
                pxCurrentTCB =&Task1TCB;       //执行Task1
			}
			else if(pxCurrentTCB->xTicksToDelay != 0)    //Task1TCB和Task2TCB延时还没到零
			{    
                pxCurrentTCB = &IdleTaskTCB;   //进入空闲任务TCB
			}
			else 
			{
				return;		/* 返回,不进行切换,因为两个线程都处于延时中 */
			}
		}
	}
}

SysTick

SysTick中断服务函数

在本节中,我们得知xTickToDelay与SysTick相关。在操作系统中,最小的时间单位就是SysTick的中断周期,我们称为一个Tick。

SysTick中断服务函数在port.c实现。

void xPortSysTickHandler( void )
{
	/* 关中断 */
    vPortRaiseBASEPRI();
    
    //进入临界段保护
    /* 更新系统时基 */
    xTaskIncrementTick();
    //退出临界段保护

	/* 开中断 */
    vPortClearBASEPRIFromISR();
}

上面函数使用到xTaskIncrementTick()函数,用来更新系统时基。如下

void xTaskIncrementTick( void )
{
    TCB_t *pxTCB = NULL;
    BaseType_t i = 0;
    
    /* 更新系统时基计数器xTickCount,xTickCount是一个在port.c中定义的全局变量 */
    /* xTickCount在vTaskStartScheduler()中初始化为0 */
    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();
}

SysTick初始化

SysTick的中断服务函数要想被顺利执行,则SysTick必须先初始化。初始化函数定义在port.c。

一些SysTick相关宏定义

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

/* SysTick 时钟源选择 */
#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 )

SysTick初始化函数如下

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初始化函数在xPortStartScheduler()中被调用。

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

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

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

main()函数

无多大变动,主要的还是任务入口函数由原来的delay(100)变成vTaskDelay(2)这样。

主要函数:

空闲任务在任务调度器启动函数vTaskStartScheduler()中创建。

阻塞延时函数vTaskDelay()。

上下文切换函数vTaskSwitchContext()实现三个任务间的转换。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值