FreeRTOS——空闲任务与阻塞延迟实现

在裸机运行中,我们是使用软件延时来实现延时的功能(delay()),即是让CPU空等来达到延时的目的。使用RTOS的很大优势就是榨干CPU性能,永远不让他闲着,任务需要延时也就不需要让CPU空等来实现延时的效果。
RTOS中的延时叫做阻塞延时,即任务需要延时的时候,任务会放弃CPU的使用权,CPU可以去干其他的事情,当任务延时时间到,重新获取CPU使用权,任务继续运行。这样就可以充分利用CPU资源了。
当任务进入阻塞状态,如果没有其他任务可以运行,RTOS都会为CPU创建一个空闲任务,这个时候CPU就去运行空闲任务。在FreeRTOS中,空闲任务是系统在【启动调度器】的时候创建的优先级最低的任务,空闲任务主要做一些系统内存清理的任务。

一、实现空闲任务

目前我们在创建任务时使用的栈和TCB都是使用的静态内存,即预先定义好的内存,空闲任务也不例外。

1、定义空闲任务的栈

空闲任务的栈在main.c 中定义:

/* 定义空闲任务的栈 */
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) (2)
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE]; (1)

(1):空闲任务的栈是一个定义好的数组,大小由FreeRTOSConfig.h 中
定义的宏 configMINIMAL_STACK_SIZE 控制,大小为128,单位是字。

2、定义空闲任务的任务控制块

空闲任务的任务控制块在main.c中定义:

/* 定义空闲任务的任务控制块 */
TCB_t IdleTaskTCB;

3、定义空闲任务的任务控制块

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

extern TCB_t IdleTaskTCB;
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
									StackType_t **ppxIdleTaskStackBuffer,
									uint32_t *pulIdleTaskStackSize );
									void vTaskStartScheduler( void )
{
 /*=======================创建空闲任务 start=======================*/
 TCB_t *pxIdleTaskTCBBuffer = NULL; /* 用于指向空闲任务控制块 */
 StackType_t *pxIdleTaskStackBuffer = NULL; /* 用于空闲任务栈起始地址 */
 uint32_t ulIdleTaskStackSize;

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

 /* 手动指定第一个运行的任务 */
 pxCurrentTCB = &Task1TCB;

 /* 启动调度器 */
 if ( xPortStartScheduler() != pdFALSE )
 {
 /* 调度器启动成功,则不会返回,即不会来到这里 */
 }
}

(1):获 取 空 闲 任 务 的 内 存 , 即 将 pxIdleTaskTCBBuffer 和pxIdleTaskStackBuffer 这两个接下来要作为形参传到xTaskCreateStatic()函数的指针分别指向空闲任务的 TCB 和栈的起始地址,这个操作由函数 vApplicationGetIdleTaskMemory()来实现:
vApplicationGetIdleTaskMemory():

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

(2):调用 xTaskCreateStatic()函数创建空闲任务。
(3):将空闲任务插入到就绪列表的开头。

二、实现阻塞延时

1、vTaskDelay ()函数

阻塞延时的阻塞是指任务调用该延时函数后,任务会被剥离CPU使用权,然后进入阻塞状态,直到延时结束,任务重新获取CPU使用权才可以继续运行。在任务阻塞这段时间,CPU可以去执行其他的任务,如果其他任务也处于阻塞状态,CPU就会执行空闲任务。

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

(1):获取当前任务控制块。
(2):xTicksToDelay是任务控制块的一个成员,用于记录需要延时的时间,单位为SysTick的中断周期。
xTicksToDelay定义:

typedef struct tskTaskControlBlock
{
 volatile StackType_t *pxTopOfStack; /* 栈顶 */
 ListItem_t xStateListItem; /* 任务节点 */
 StackType_t *pxStack; /* 任务栈起始地址 */
 char pcTaskName[ configMAX_TASK_NAME_LEN ];/* 任务名称,字符串形式 */
 TickType_t xTicksToDelay; /* 用于延时 */
} tskTCB;

(3):任务切换。调用taskYIELD()会产生PendSV中断,在PendSV中断服务函数终会调用任务切换函数vTaskSwitchContext(),该任务的作用是寻找最高优先级的就绪任务,然后更新pxCurrentTCB.因为这一章比上一章多一个空闲任务,需要让pxCurrentTCB 在这三个任务中切换, 算法需要改变,vTaskSwitchContext()函数修改如下:

void vTaskSwitchContext( void )
{
 /* 如果当前任务是空闲任务,那么就去尝试执行任务 1 或者任务 2,  
 看看他们的延时时间是否结束,如果任务的延时时间均没有到期,
 那就返回继续执行空闲任务 */
 if ( pxCurrentTCB == &IdleTaskTCB ) (1)
 {
 	if (Task1TCB.xTicksToDelay == 0)
 	{
  		pxCurrentTCB =&Task1TCB;
 	}
 	else if (Task2TCB.xTicksToDelay == 0)
 	{
 		pxCurrentTCB =&Task2TCB;
 	}
 	else
 	{
 		return; /* 任务延时均没有到期则返回,继续执行空闲任务 */
 	}
 }
 else /* 当前任务不是空闲任务则会执行到这里 */ (2)
 {
	 /*如果当前任务是任务 1 或者任务 2 的话,检查下另外一个任务,
	 如果另外的任务不在延时中,就切换到该任务
	 否则,判断下当前任务是否应该进入延时状态,
	 如果是的话,就切换到空闲任务。否则就不进行任何切换 */
	 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 中断服务函数

1、SysTick 中断服务函数

在任务上下文切换函数 vTaskSwitchContext ()中,会判断每个任务的任务控制块中的延时成员xTicksToDelay的值是否为0,如果为0就要将对应的任务就绪,如果不为0就继续延时。如果一个任务延时,一开始xTicksToDelay不为0,当xTicksToDelay变为0后表示延时结束。在FreeRTOS中,操作系统中的最小时间单位是SysTick的中断周期,我们称之为一个tick ,xTicksToDelay就是以tick为周期递减的。
SysTick 中断服务函数

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

xTaskIncrementTick()函数

void xTaskIncrementTick( void )
{
 TCB_t *pxTCB = NULL;
 BaseType_t i = 0;

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

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

 /* 执行一次任务切换 */(3)
 portYIELD();
}

2、SysTick 初始化函数

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

/* SysTick 控制寄存器 */ (1)
#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 )

void vPortSetupTimerInterrupt( void ) (2)
{
 /* 设置重装载寄存器的值 */ (2)-①
 portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;

 /* 设置系统定时器的时钟等于内核时钟 (2)-②
 使能 SysTick 定时器中断
 使能 SysTick 定时器 */
 portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT |
 portNVIC_SYSTICK_INT_BIT |
 portNVIC_SYSTICK_ENABLE_BIT );
}

SysTick 初 始 化 函 数 vPortSetupTimerInterrupt() , 在
xPortStartScheduler()中被调用。

BaseType_t xPortStartScheduler( void )
{
 /* 配置 PendSV 和 SysTick 的中断优先级为最低 */
 portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
 portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
 /* 初始化 SysTick */
 vPortSetupTimerInterrupt();
 /* 启动第一个任务,不再返回 */
 prvStartFirstTask();
 /* 不应该运行到这里 */
 return 0;
}

(2)-①:设置重装载寄存器的值,决定SysTick 的中断周期。
configCPU_CLOCK_HZ 与 configTICK_RATE_HZ 宏定义:

#define configCPU_CLOCK_HZ (( unsigned long ) 25000000) (1)
#define configTICK_RATE_HZ (( TickType_t ) 100)         (2)

(1):系统时钟大小,目前是软件仿真,需要配置成与system_ARMCM3.c文件中的 SYSTEM_CLOCK的一样, 即等于 25M。如果有具体的硬件,则配置成与硬件系统时钟一样
(2):SysTick 每秒中断多少次,目前配置为 100,即每 10ms 中断一次。
(2)-②:设置系统定时器的时钟等于内核时钟,使能 SysTick 定时器中断,使能 SysTick 定时器。

参考:[野火®]《FreeRTOS 内核实现与应用开发实战—基于STM32》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值