实时操作系统-4-[FreeRTOS]任务管理

任务的基本概念

从系统的角度看,任务是竞争系统资源的最小运行单元。例如我们创建的任务LED1、LED2等,当我们的任务在运行时,就会抢占CPU的资源,此时我们引入FreeRTOS操作系统 。

FreeRTOS 是一个支持多任务的操作系统。在 FreeRTOS 中,任务可以使用或等待 CPU、使用内存空间等系统资源,并独立于其它任务运行,任何数量的任务可以共享同一个优先级,如果宏 configUSE_TIME_SLICING 定义为 1,处于就绪态的多个相同优先级任务将会以时间片切换的方式共享处理器。

其实在任何时间 FreeRTOS 只有一个任务得到运行,FreeRTOS 调度器决定运行哪个任务。调度器会不断的启动、停止每一个任务,宏观看上去所有的任务都在同时在执行:

image

作为任务,不需要对调度器的活动有所了解,在任务切入切出时保存上下文环境(寄存器值、堆栈内容)是调度器主要的职责。为了实现这点,每个 FreeRTOS 任务都需要有自己的栈空间。当任务切出时,它的执行环境会被保存在该任务的栈空间中,这样当任务再次运行时,就能从堆栈中正确的恢复上次的运行环境,任务越多,需要的堆栈空间就越大,而一个系统能运行多少个任务,取决于系统的可用的 SRAM。

FreeRTOS 的可以给用户提供多个任务单独享有独立的堆栈空间,系统可以决定任务的状态,决定任务是否可以运行,同时还能运用内核的 IPC 通信资源,实现了任务之间的通信,帮助用户管理业务程序流程。

FreeRTOS 中的任务是抢占式调度机制,高优先级的任务可打断低优先级任务,低优先级任务必须在高优先级任务阻塞或结束后才能得到调度。同时 FreeRTOS 也支持时间片轮转调度方式,只不过时间片的调度是不允许抢占任务的 CPU 使用权。

任务通常会运行在一个死循环中,也不会退出,如果一个任务不再需要,可以调用 FreeRTOS 中的任务删除 API 函数接口显式地将其删除。

任务调度器的基本概念

FreeRTOS 中提供的任务调度器是基于优先级的全抢占式调度:在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的。

在系统中,当有比当前任务优先级更高的任务就绪时,当前任务将立刻被换出,高优先级任务抢占处理器运行。

FreeRTOS 内核中也允许创建相同优先级的任务。相同优先级的任务采用时间片轮转方式进行调度(也就是通常说的分时调度器),时间片轮转调度仅在当前系统中无更高优先级就绪任务存在的情况下才有效。为了保证系统的实时性,系统尽最大可能地保证高优先级的任务得以运行。

任务调度的原则是一旦任务状态发生了改变,并且当前运行的任务优先级小于优先级队列组中任务最高优先级时,立刻进行任务切换(除非当前系统处于中断处理程序中或禁止任务切换的状态)。

任务状态迁移

就绪(Ready):该任务在就绪列表中,就绪的任务已经具备执行的能力,只等待调度器进行调度,新创建的任务会初始化为就绪态。

运行(Running):该状态表明任务正在执行,此时它占用处理器,FreeRTOS 调度器选择运行的永远是处于最高优先级的就绪态任务,当任务被运行的一刻,它的任务状态就变成了运行态。

阻塞(Blocked):如果任务当前正在等待某个时序或外部中断,我们就说这个任务处于阻塞状态,该任务不在就绪列表中。包含任务被挂起、任务被延时、任务正在等待信号量、读写队列或者等待读写事件等。

挂起态(Suspended):处于挂起态的任务对调度器而言是不可见的,让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend()函数;而把一个挂起状态的任务恢复的唯一途径就是调用 vTaskResume() 或 vTaskResumeFromISR()函数,我们可以这么理解挂起态与阻塞态的区别,当任务有较长的时间不允许运行的时候,我们可以挂起任务,这样子调度器就不会管这个任务的任何信息,直到我们调用恢复任务的 API 函数;而任务处于阻塞态的时候,系统还需要判断阻塞态的任务是否超时,是否可以解除阻塞。

image

① 创建任务→就绪态 (ready):任务创建完成后进入就绪态,表明任务已准备就绪,随时可以运行,只等待调度器进行调度。
② 就绪态→运行态 (running):发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运行态。
③ 运行态→就绪态:有更高优先级任务创建或者恢复后,会发生任务调度,此刻就绪列表中最高优先级任务变为运行态,那么原先运行的任务由运行态变为就绪态,依然在就绪列表中,等待最高优先级的任务运行完毕,继续运行原来的任务 (此处可以看做是CPU使用权被更高优先级的任务抢占了)。
④ 运行态→阻塞态 (blocked):正在运行的任务发生阻塞 (延时、读信号量等待) 时,该任务会从就绪列表中删除,任务状态由运行态变成阻塞态,然后发生任务切换,运行就绪列表中当前最高优先级任务。
⑤ 阻塞态→就绪态:阻塞的任务被恢复后 (任务恢复、延时时间超时、读信号量超时 或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态;如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。
⑥⑦⑧ 就绪态、阻塞态、运行态→挂起态 (suspended):任务可以通过调用 vTaskSuspend() API 函数都可以将处于任何状态的任务挂起,被挂起的任务得不到CPU的使用权,也不会参与调度,除非它从挂起态中解除。
⑨ 挂起态→就绪态:把一个挂起状态的任务恢复的唯一途径就是调用 vTaskResume() 或 vTaskResumeFromISR() API 函数,如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生 任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。

常用任务函数

任务挂起函数

vTaskSuspend()

vTaskSuspend():挂起指定任务。被挂起的任务绝不会得到 CPU 的使用权,不管该任务具有什么优先级。

任务可以通过调用 vTaskSuspend()函数都可以将处于任何状态的任务挂起,被挂起的任务得不到 CPU 的使用权,也不会参与调度,它相对于调度器而言是不可见的,除非它从挂起态中解除:

 
#if ( INCLUDE_vTaskSuspend == 1 )//如果想要使用任务挂起函数 vTaskSuspend()则必须将宏定义 INCLUDE_vTaskSuspend 配置为 1
 
	void vTaskSuspend( TaskHandle_t xTaskToSuspend )//xTaskToSuspend 是挂起指定任务的任务句柄,任务必须为已创建的任务,可以通过传递 NULL 来挂起任务自己。
	{
	TCB_t *pxTCB;
 
		taskENTER_CRITICAL();
		{
			/* If null is passed in here then it is the running task that is
			being suspended. */
			pxTCB = prvGetTCBFromHandle( xTaskToSuspend );//利用任务句柄 xTaskToSuspend 来获取任务控制块,通过调用 prvGetTCBFromHandle()API 函数得到对应的任务控制块。
 
			traceTASK_SUSPEND( pxTCB );
 
			/* Remove task from the ready/delayed list and place in the
			suspended list. */
			if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
			{
				/*
				从就绪/阻塞列表中删除即将要挂起的任务。然后更新"最高优先级变量 uxReadyPriorities",目的是维护这个变量,这个变量的如下功能:
				1. 在使用通用方法找到最高优先级任务时,它用来记录最高优先级任务的优先级。
				2. 在使用硬件方法找到最高优先级任务时,它的每一位(共 32bit)的状态代表这个优先级上边,有没有就绪的任务。
				*/
				taskRESET_READY_PRIORITY( pxTCB->uxPriority );
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
 
			/* Is the task waiting on an event also? */
			if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
			{
				( void ) uxListRemove( &( pxTCB->xEventListItem ) );//如果任务在等待事件,也将任务从等待事件列表中移除。
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
			/*
			将任务状态添加到挂起列表中。在 FreeRTOS 中有专门的列表用于记录任务的状态,
			记录任务挂起态的列表就是 xSuspendedTaskList,所有被挂起的任务都会放到这个列表中。
			*/
			vListInsertEnd( &xSuspendedTaskList, &( pxTCB->xStateListItem ) );
		}
		taskEXIT_CRITICAL();
 
		if( xSchedulerRunning != pdFALSE )
		{
			/* Reset the next expected unblock time in case it referred to the
			task that is now in the Suspended state. */
			/* 重置下一个任务的解除阻塞时间。重新计算一下还要多长时间执行下一个任务。
			如果下个任务的解锁,刚好是被挂起的那个任务,那么变量 NextTaskUnblockTime 就不对了,所以要重新从延时列表中获取一下。
			*/
			taskENTER_CRITICAL();
			{
				prvResetNextTaskUnblockTime();
			}
			taskEXIT_CRITICAL();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
 
		if( pxTCB == pxCurrentTCB )
		{
			if( xSchedulerRunning != pdFALSE )
			{
				/* The current task has just been suspended. 当前的任务已经被挂起。 */
				configASSERT( uxSchedulerSuspended == 0 );
				/* 调度器在运行时,如果这个挂起的任务是当前任务,立即切换任务。 */
				portYIELD_WITHIN_API();
			}
			else
			{
				/* The scheduler is not running, but the task that was pointed
				to by pxCurrentTCB has just been suspended and pxCurrentTCB
				must be adjusted to point to a different task. */
				/*
				调度器未运行(xSchedulerRunning == pdFALSE ),
				但 pxCurrentTCB 指向的任务刚刚被暂停,所以必须调整 pxCurrentTCB 以指向其他任务。
				首先调用函数 listCURRENT_LIST_LENGTH()判断一下系统中所有的任务是不是都被挂起了,也就是查看列表 xSuspendedTaskList的长度是不是等于 uxCurrentNumberOfTasks,
				事实上并不会发生这种情况,因为空闲任务是不允许被挂起和阻塞的,
				必须保证系统中无论如何都有一个任务可以运行
				*/
				if( listCURRENT_LIST_LENGTH( &xSuspendedTaskList ) == uxCurrentNumberOfTasks )
				{
					/* No other tasks are ready, so set pxCurrentTCB back to
					NULL so when the next task is created pxCurrentTCB will
					be set to point to it no matter what its relative priority
					is.
					没有其他任务准备就绪,因此将 pxCurrentTCB 设置回 NULL,以便在创建下一个任务时 pxCurrentTCB 将被设置为指向它,实际上并不会执行到这里 
					*/
					pxCurrentTCB = NULL;
				}
				else
				{
					vTaskSwitchContext();
				}
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
 
#endif /* INCLUDE_vTaskSuspend */

任务可以调用 vTaskSuspend()这个函数来挂起任务自身,但是在挂起自身的时候会进行一次任务上下文切换,需要挂起自身就将 xTaskToSuspend 设置为 NULL 传递进来即可。无论任务是什么状态都可以被挂起,只要调用了 vTaskSuspend()这个函数就会挂起成功,不论是挂起其他任务还是挂起任务自身。

主函数:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
 
#include "LED.h"
#include "Usart.h"
#include "Key.h"  
 
#include "FreeRTOS.h"
#include "task.h"
 
 /* 创建任务句柄 */
static TaskHandle_t AppTaskCreate_Handle = NULL;
/* LED任务句柄 */
static TaskHandle_t LED_Task_Handle = NULL;
/* LED2任务句柄 */
static TaskHandle_t LED2_Task_Handle = NULL;
/* 按键任务句柄 */
static TaskHandle_t KEY_Task_Handle = NULL;
 
//一些函数声明
static void AppTaskCreate(void);/* 用于创建任务 */
static void LED_Task(void* pvParameters);/* LED_Task任务实现 */
static void LED2_Task(void* pvParameters);/* LED2_Task任务实现 */
static void KEY_Task(void* pvParameters);/* KEY_Task任务实现 */
static void All_Function_Init(void);/* 用于初始化板载相关资源 */
 
int main(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
 
	All_Function_Init();//硬件初始化
	
	while (1)
	{
		 /* 创建AppTaskCreate任务 */
		xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
													(const char*    )"AppTaskCreate",/* 任务名字 */
													(uint16_t       )512,  /* 任务栈大小 */
													(void*          )NULL,/* 任务入口函数参数 */
													(UBaseType_t    )1, /* 任务的优先级 */
													(TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
		/* 启动任务调度 */           
		if(pdPASS == xReturn)
			vTaskStartScheduler();   /* 启动任务,开启调度 */
		else
			return -1;  
	}
}
 
//任务创建函数
static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  
  taskENTER_CRITICAL();           //进入临界区
  
  /* 创建LED_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )LED_Task, /* 任务入口函数 */
                        (const char*    )"LED_Task",/* 任务名字 */
                        (uint16_t       )512,   /* 任务栈大小 */
                        (void*          )NULL,	/* 任务入口函数参数 */
                        (UBaseType_t    )2,	    /* 任务的优先级 */
                        (TaskHandle_t*  )&LED_Task_Handle);/* 任务控制块指针 */
  if(pdPASS == xReturn)
    printf("创建LED_Task任务成功!\r\n");
 
	/* 创建LED_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )LED2_Task, /* 任务入口函数 */
                        (const char*    )"LED2_Task",/* 任务名字 */
                        (uint16_t       )512,   /* 任务栈大小 */
                        (void*          )NULL,	/* 任务入口函数参数 */
                        (UBaseType_t    )3,	    /* 任务的优先级 */
                        (TaskHandle_t*  )&LED2_Task_Handle);/* 任务控制块指针 */
  if(pdPASS == xReturn)
    printf("创建LED2_Task任务成功!\r\n");
 
  /* 创建KEY_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )KEY_Task,  /* 任务入口函数 */
                        (const char*    )"KEY_Task",/* 任务名字 */
                        (uint16_t       )512,  /* 任务栈大小 */
                        (void*          )NULL,/* 任务入口函数参数 */
                        (UBaseType_t    )3, /* 任务的优先级 */
                        (TaskHandle_t*  )&KEY_Task_Handle);/* 任务控制块指针 */ 
  if(pdPASS == xReturn)
    printf("创建KEY_Task任务成功!\r\n");
  
  vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
  
  taskEXIT_CRITICAL();            //退出临界区
}
 
//LED1任务主体
static void LED_Task(void* parameter)
{	
	while (1)
	{
		LED1_ON;
		vTaskDelay(500);   /* 延时500个tick */
		printf("LED_Task Running,LED1_ON\r\n");
		
		LED1_OFF;     
		vTaskDelay(500);   /* 延时500个tick */		 		
		printf("LED_Task Running,LED1_OFF\r\n");
	}
}
 
//LED2任务主体
static void LED2_Task(void* parameter)
{	
	while (1)
	{
		LED2_ON;
		vTaskDelay(500);   /* 延时500个tick */
		printf("LED2_Task Running,LED2_ON\r\n");
 
		LED2_OFF;     
		vTaskDelay(500);   /* 延时500个tick */		 		
		printf("LED2_Task Running,LED2_OFF\r\n");
	}
}
 
//按键任务主体
static void KEY_Task(void* parameter)
{	
  while (1)
  {
    if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
    {/* K1 被按下 */
      printf("挂起LED任务!\n");
      vTaskSuspend(LED_Task_Handle);/* 挂起LED任务 */
      printf("挂起LED任务成功!\n");
    } 
    vTaskDelay(20);/* 延时20个tick */
  }
}
 
//初始化声明
static void All_Function_Init(void)
{
	/*
	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
	 * 都统一用这个优先级分组,千万不要再分组,切忌。
	 */
	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
	
	/* LED 初始化 */
	LED_GPIO_Config();
 
	/* 串口初始化	*/
	USART_Config();
 
	//按键初始化
	Key_GPIO_Config();
  
}
 

运行结果:
image

vTaskSuspendAll()

锁定调度器,挂起所有任务。因为任务的切换是需要靠调度器进行来切换的,如果我们将调度器挂起则不能进行上下文切换,任务将停止运行,相当于挂起所有任务,但是中断还是使能的当调度器被挂起的时候,如果有中断需要进行上下文切换, 那么这个任务将会被挂起,在调度器恢复之后才执行切换任务。

注意:调用了多少次的 vTaskSuspendAll() 就要调用多少次xTaskResumeAll()进行恢复。

函数源码 :

void vTaskSuspendAll( void )
{
	/* A critical section is not required as the variable is of type
	BaseType_t.  Please read Richard Barry's reply in the following link to a
	post in the FreeRTOS support forum before reporting this as a bug! -
	http://goo.gl/wu4acr */
	++uxSchedulerSuspended;
}

直接使用上面任务挂起的代码,将按键主体任务进行一个更改:

//按键任务主体
static void KEY_Task(void* parameter)
{	
  while (1)
  {
    if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
    {/* K1 被按下 */
	  printf("按键K1被按下!\r\n");
      printf("挂起所有任务!\r\n");
      vTaskSuspendAll();
//xTaskResumeAll();
    } 
    vTaskDelay(20);/* 延时20个tick */
  }
}

我们先不调用恢复函数,会发现LED 和LED2任务不在打印数据,并且串口打印数据报错:
image

如果我们加上任务恢复函数:

//按键任务主体
static void KEY_Task(void* parameter)
{	
  while (1)
  {
    if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
    {/* K1 被按下 */
	  printf("按键K1被按下!\r\n");
      printf("挂起所有任务!\r\n");
      vTaskSuspendAll();
	  xTaskResumeAll();
    } 
    vTaskDelay(20);/* 延时20个tick */
  }
}

image

如果我们挂起两次只恢复一次,会发现还是报错:

//按键任务主体
static void KEY_Task(void* parameter)
{	
  while (1)
  {
    if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
    {/* K1 被按下 */
	  printf("按键K1被按下!\r\n");
      printf("挂起所有任务!\r\n");
      vTaskSuspendAll();
      vTaskSuspendAll();
	  xTaskResumeAll();
    } 
    vTaskDelay(20);/* 延时20个tick */
  }
}

image

任务恢复函数

vTaskResume()

上面我们提到了任务的挂起,那么我们可以思考当任务被挂起该如何回复呢?我们前面提到任务挂起有任务挂起的函数,那么任务恢复一样有恢复,不然任务怎么恢复呢,任务恢复就是让挂起的任务重新进入就绪状态,恢复的任务会保留挂起前的状态信息,在恢复的时候根据挂起时的状态继续运行。

如果被恢复任务在所有就绪态任务中,处于最高优先级列表的第一位,那么系统将进行任务上下文的切换。

#if ( INCLUDE_vTaskSuspend == 1 )     //(1)
 
	void vTaskResume( TaskHandle_t xTaskToResume )
	{
		/* 根据 xTaskToResume 获取对应的任务控制块 */
		TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume;
 
		/* It does not make sense to resume the calling task. */
		/* 检查要恢复的任务是否被挂起,如果没被挂起,恢复调用任务没有意义*/
		configASSERT( xTaskToResume );
 
		/* The parameter cannot be NULL as it is impossible to resume the
		currently executing task. */
		/*该参数不能为 NULL,同时也无法恢复当前正在执行的任务,因为当前正在运行的任务不需要恢复,只能恢复处于挂起态的任务*/
		if( ( pxTCB != NULL ) && ( pxTCB != pxCurrentTCB ) )
		{
			taskENTER_CRITICAL();//进入临界区域
			{
				if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE )
				{
					traceTASK_RESUME( pxTCB );
 
					/* As we are in a critical section we can access the ready
					lists even if the scheduler is suspended. */
					/* 由于我们处于临界区,即使任务被挂起,我们也可以访问任务的状态列表。将要恢复的任务从挂起列表中删除 */
					( void ) uxListRemove(  &( pxTCB->xStateListItem ) );
					/* 将要恢复的任务添加到就绪列表中去 */
					prvAddTaskToReadyList( pxTCB );
 
					/* We may have just resumed a higher priority task. */
					/* 如果刚刚恢复的任务优先级比当前任务优先级更高则需要进行任务的切换 */
					if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
					{
						/* This yield may not cause the task just resumed to run,
						but will leave the lists in the correct state for the
						next yield. */
						/* 因为恢复的任务在当前情况下的优先级最高调用 taskYIELD_IF_USING_PREEMPTION()进行一次任务切换*/
						taskYIELD_IF_USING_PREEMPTION();
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			taskEXIT_CRITICAL();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
 
#endif /* INCLUDE_vTaskSuspend */

(1):如果想要使用任务恢复函数 vTaskResume()则必须将宏定义 INCLUDE_vTaskSuspend 配置为 1,因为任务挂起只能通过调用 vTaskSuspend()函数进行挂起 ,没挂起的任务就无需恢复,当年需要调vTaskSuspend() 函数就必须使能INCLUDE_vTaskSuspend 这个宏定义,所以想要使用 FreeRTOS 的任务挂起与恢复函数就必须将这个宏定义配置为 1。

示例演示,我们直接在挂起代码的基础上进行修改,对按键函数主体进行如下修改:

//按键任务主体
static void KEY_Task(void* parameter)
{	
  while (1)
  {
    if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
    {/* K1 被按下 */
      printf("按键K1被按下!\r\n");
      printf("挂起LED任务!\r\n");
      vTaskSuspend(LED_Task_Handle);/* 挂起LED任务 */
      printf("挂起LED任务成功!\r\n");
    } 
    if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
    {/* K2 被按下 */
      printf("按键K2被按下!\r\n");
      printf("恢复LED任务!\n");
      vTaskResume(LED_Task_Handle);/* 恢复LED任务! */
      printf("恢复LED任务成功!\n");
    }
    vTaskDelay(20);/* 延时20个tick */
  }
}

可以发现通过按键挂起和恢复LED1的任务函数:
image

xTaskResumeFromISR()

xTaskResumeFromISR()与 vTaskResume()一样都是用于恢复被挂起的任务,不一样的是 xTaskResumeFromISR() 专门用在中断服务程序中。无论通过调用一次或多次vTaskSuspend()函数而被挂起的任务,也只需调用一次 xTaskResumeFromISR()函数即可解挂。
要想使用该函数必须在 FreeRTOSConfig.h 中 把 INCLUDE_vTaskSuspend 和INCLUDE_vTaskResumeFromISR 都定义为 1 才有效。任务还没有处于挂起态的时候,调用xTaskResumeFromISR()函数是没有任何意义的。

#if ( ( INCLUDE_xTaskResumeFromISR == 1 ) && ( INCLUDE_vTaskSuspend == 1 ) )
 
	BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume )//xTaskToResume 是恢复指定任务的任务句柄。
	{
	BaseType_t xYieldRequired = pdFALSE;//定义一个是否需要进行任务切换的变量 xYieldRequired,默认为pdFALSE,当任务恢复成功并且需要任务切换的话则重置为 pdTRUE,以表示需要进行任务切换。
	TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume;//根据 xTaskToResume 任务句柄获取对应的任务控制块。
	UBaseType_t uxSavedInterruptStatus;//定义一个变量 uxSavedInterruptStatus 用于保存关闭中断的状态。
 
		configASSERT( xTaskToResume );//检查要恢复的任务是存在,如果不存在,调用恢复任务函数没有任何意义。
 
		/* RTOS ports that support interrupt nesting have the concept of a
		maximum	system call (or maximum API call) interrupt priority.
		Interrupts that are	above the maximum system call priority are keep
		permanently enabled, even when the RTOS kernel is in a critical section,
		but cannot make any calls to FreeRTOS API functions.  If configASSERT()
		is defined in FreeRTOSConfig.h then
		portASSERT_IF_INTERRUPT_PRIORITY_INVALID() will result in an assertion
		failure if a FreeRTOS API function is called from an interrupt that has
		been assigned a priority above the configured maximum system call
		priority.  Only FreeRTOS functions that end in FromISR can be called
		from interrupts	that have been assigned a priority at or (logically)
		below the maximum system call interrupt priority.  FreeRTOS maintains a
		separate interrupt safe API to ensure interrupt entry is as fast and as
		simple as possible.  More information (albeit Cortex-M specific) is
		provided on the following link:
		http://www.freertos.org/RTOS-Cortex-M3-M4.html */
		portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
 
		/*
		调用 portSET_INTERRUPT_MASK_FROM_ISR()函数设置 basepri寄存器用于屏蔽系统可管理的中断,防止被处理被其他中断打断,
		当 basepri 设置为configMAX_SYSCALL_INTERRUPT_PRIORITY 的时候(该宏在 FreeRTOSConfig.h 中定义,现在配置为 5),
		会让系统不响应比该优先级低的中断,而优先级比之更高的中断则不受影响。就是说当这个宏定义配置为 5 的时候,中断优先级
		数值在 0、1、2、3、4 的这些中断是不受 FreeRTOS 管理的,不可被屏蔽,而中断优先级在 5 到 15 的中断是受到系统管理,可用被屏蔽的。
		*/
		uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
		{
			if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE )//判断要恢复的任务是否真的被挂起了,如果被挂起才需要恢复,没被挂起那当然也不需要恢复。
			{
				traceTASK_RESUME_FROM_ISR( pxTCB );
 
				/* Check the ready lists can be accessed. */
				if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )//检查可以访问的就绪列表,检查调度器是否被挂起
				{
					/* Ready lists can be accessed so move the task from the
					suspended list to the ready list directly. */
					if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )//如果刚刚恢复的任务优先级比当前任务优先级更高需要进行一次任务的切换,重置 xYieldRequired = pdTRUE 表示需要进行任务切换。
					{
						xYieldRequired = pdTRUE;
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
 
					( void ) uxListRemove( &( pxTCB->xStateListItem ) );
					prvAddTaskToReadyList( pxTCB );
				}
				else
				{
					/* The delayed or ready lists cannot be accessed so the task
					is held in the pending ready list until the scheduler is
					unsuspended. */
					vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) );
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
 
		return xYieldRequired;
	}
 
#endif /* ( ( INCLUDE_xTaskResumeFromISR == 1 ) && ( INCLUDE_vTaskSuspend == 1 ) ) */

使用 xTaskResumeFromISR()的时候有几个需要注意的地方:
① 当函数的返回值为 pdTRUE 时:恢复运行的任务的优先级等于或高于正在运行的任务,表明在中断服务函数退出后必须进行一次上下文切换,使用portYIELD_FROM_ISR()进行上下文切换。当函数的返回值为 pdFALSE 时:恢复运行的任务的优先级低于当前正在运行的任务,表明在中断服务函数退出后不需要进行上下文切换。
② xTaskResumeFromISR() 通常被认为是一个危险的函数,因为它的调用并非是固定的,中断可能随时来来临。所以,xTaskResumeFromISR()不能用于任务和中断间的同步,如果中断恰巧在任务被挂起之前到达,这就会导致一次中断丢失(任务还没有挂起,调用 xTaskResumeFromISR()函数是没有意义的,只能等下一次中断)。这种情况下,可以使用信号量或者任务通知来同步就可以避免这种情况。

xTaskResumeAll()

当调用了 vTaskSuspendAll()函数将调度器挂起,想要恢复调度器的时候我们就需要调用 xTaskResumeAll()函数:

BaseType_t xTaskResumeAll( void )
{
TCB_t *pxTCB = NULL;
BaseType_t xAlreadyYielded = pdFALSE;
 
	/* If uxSchedulerSuspended is zero then this function does not match a
	previous call to vTaskSuspendAll(). */
	/*如果 uxSchedulerSuspended 为 0,则此函数与先前对 vTaskSuspendAll()的调用不匹配,不需要调用 xTaskResumeAll()恢复调度器。*/
	configASSERT( uxSchedulerSuspended );
 
	/* It is possible that an ISR caused a task to be removed from an event
	list while the scheduler was suspended.  If this was the case then the
	removed task will have been added to the xPendingReadyList.  Once the
	scheduler has been resumed it is safe to move all the pending ready
	tasks from this list into their appropriate ready list. */
	taskENTER_CRITICAL();
	{
		--uxSchedulerSuspended;//我们知道,每调用一次 vTaskSuspendAll() 函数就会将uxSchedulerSuspended 变量加一,那么调用对应的 xTaskResumeAll()肯定就是将变量减一。
 
		if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )//如果调度器恢复正常工作,也就是调度器没有被挂起,就可以将所有待处理的就绪任务从待处理就绪列表 xPendingReadyList 移动到适当的就绪列表中。
		{
			if( uxCurrentNumberOfTasks > ( UBaseType_t ) 0U )
			{
				/* Move any readied tasks from the pending list into the
				appropriate ready list. */
				while( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE )//当待处理就绪列表 xPendingReadyList 中是非空的时候,就需要将待处理就绪列表中的任务移除,添加到就绪列表中去。
				{
					pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &xPendingReadyList ) );
					( void ) uxListRemove( &( pxTCB->xEventListItem ) );
					( void ) uxListRemove( &( pxTCB->xStateListItem ) );
					prvAddTaskToReadyList( pxTCB );
 
					/* If the moved task has a priority higher than the current
					task then a yield must be performed. 
					如果移动的任务的优先级高于当前任务,需要进行一次任务的切换
					xYieldPending = pdTRUE 表示需要进行任务切换*/
					if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
					{
						xYieldPending = pdTRUE;
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
 
				if( pxTCB != NULL )
				{
					/* A task was unblocked while the scheduler was suspended,
					which may have prevented the next unblock time from being
					re-calculated, in which case re-calculate it now.  Mainly
					important for low power tickless implementations, where
					this can prevent an unnecessary exit from low power
					state. */
					prvResetNextTaskUnblockTime();
				}
 
				/* If any ticks occurred while the scheduler was suspended then
				they should be processed now.  This ensures the tick count does
				not	slip, and that any delayed tasks are resumed at the correct
				time. */
				{
					UBaseType_t uxPendedCounts = uxPendedTicks; /* Non-volatile copy. */
 
					if( uxPendedCounts > ( UBaseType_t ) 0U )
					{
						do
						{
							if( xTaskIncrementTick() != pdFALSE )
							{
								xYieldPending = pdTRUE;
							}
							else
							{
								mtCOVERAGE_TEST_MARKER();
							}
							--uxPendedCounts;
						} while( uxPendedCounts > ( UBaseType_t ) 0U );
 
						uxPendedTicks = 0;
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
 
				if( xYieldPending != pdFALSE )
				{
					#if( configUSE_PREEMPTION != 0 )
					{
						xAlreadyYielded = pdTRUE;
					}
					#endif
					taskYIELD_IF_USING_PREEMPTION();
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
	taskEXIT_CRITICAL();
 
	return xAlreadyYielded;
}

任务删除函数

vTaskDelete()用于删除一个任务。当一个任务删除另外一个任务时,形参为要删除任务创建时返回的任务句柄,如果是删除自身, 则形参为 NULL。 要想使用该函数必须在FreeRTOSConfig.h 中把 INCLUDE_vTaskDelete 定义为 1,删除的任务将从所有就绪,阻塞,挂起和事件列表中删除:
image

为什么设为1,我们可以看一下vTaskDelete()的源码:

 
#if ( INCLUDE_vTaskDelete == 1 )
 
	void vTaskDelete( TaskHandle_t xTaskToDelete )
	{
	TCB_t *pxTCB;
 
		taskENTER_CRITICAL();
		{
			/* If null is passed in here then it is the calling task that is
			being deleted. 
			获取任务控制块,如果 xTaskToDelete 为 null则删除任务自身*/
			pxTCB = prvGetTCBFromHandle( xTaskToDelete );
 
			/* Remove task from the ready list. 将任务从就绪列表中移除*/
			if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
			{
			  /* 清除任务的就绪优先级变量中的标志位,如果删除后就绪列表的长度为 0,当
					前没有就绪的任务,应该调用 taskRESET_READY_PRIORITY()函数清除任务的最高就绪
					优先级变量 uxTopReadyPriority 中的位。*/
				taskRESET_READY_PRIORITY( pxTCB->uxPriority );
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
 
			/* Is the task waiting on an event also? 如果当前任务在等待事件,那么将任务从事件列表中移除*/
			if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
			{
				( void ) uxListRemove( &( pxTCB->xEventListItem ) );
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
 
			/* Increment the uxTaskNumber also so kernel aware debuggers can
			detect that the task lists need re-generating.  This is done before
			portPRE_TASK_DELETE_HOOK() as in the Windows port that macro will
			not return. */
			uxTaskNumber++;
 
			if( pxTCB == pxCurrentTCB )
			{
				/* A task is deleting itself.  This cannot complete within the
				task itself, as a context switch to another task is required.
				Place the task in the termination list.  The idle task will
				check the termination list and free up any memory allocated by
				the scheduler for the TCB and stack of the deleted task. 
				任务正在删除自己。 这不能在任务本身内完成,因为需要上下文切换到另一个任务。
				将任务放在结束列表中。空闲任务会检查结束列表并释放掉删除的任务控制块和已删除任务的堆栈的任何内存。*/
				vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) );
 
				/* Increment the ucTasksDeleted variable so the idle task knows
				there is a task that has been deleted and that it should therefore
				check the xTasksWaitingTermination list. 
				增加 uxDeletedTasksWaitingCleanUp 变量,记录有多少个任务需要释放内存,
				以便空闲任务知道有一个已删除的任务,然后进行内存释放空闲任务会检查结束
				列表 xTasksWaitingTermination*/
				++uxDeletedTasksWaitingCleanUp;
 
				/* The pre-delete hook is primarily for the Windows simulator,
				in which Windows specific clean up operations are performed,
				after which it is not possible to yield away from this task -
				hence xYieldPending is used to latch that a context switch is
				required. */
				portPRE_TASK_DELETE_HOOK( pxTCB, &xYieldPending );
			}
			else
			{
				--uxCurrentNumberOfTasks;
				prvDeleteTCB( pxTCB );
 
				/* Reset the next expected unblock time in case it referred to
				the task that has just been deleted. */
				prvResetNextTaskUnblockTime();
			}
 
			traceTASK_DELETE( pxTCB );
		}
		taskEXIT_CRITICAL();
 
		/* Force a reschedule if it is the currently running task that has just
		been deleted. 如删除的是当前的任务,则需要发起一次任务切换*/
		if( xSchedulerRunning != pdFALSE )
		{
			if( pxTCB == pxCurrentTCB )
			{
				configASSERT( uxSchedulerSuspended == 0 );
				portYIELD_WITHIN_API();
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
	}
 
#endif /* INCLUDE_vTaskDelete */

点击按键1删除LED2任务,首先,其他代码保持不变,将按键任务进行更改,更改如下:

//按键任务主体
static void KEY_Task(void* parameter)
{	
  while (1)
  {
    if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
    {/* K1 被按下 */
      printf("按键K1被按下!\r\n");
      
      // 检查LED2任务是否存在
      if(LED2_Task_Handle != NULL)
      {
        printf("正在删除LED2任务...\r\n");
        vTaskDelete(LED2_Task_Handle);  // 删除LED2任务
        LED2_Task_Handle = NULL;        // 将句柄置为NULL
        printf("LED2任务已删除!\r\n");
      }
      else
      {
        printf("LED2任务不存在或已被删除!\r\n");
      }
    } 
    vTaskDelay(20);/* 延时20个tick */
  }
}

image

任务延时函数

vTaskDelay()

TaskDelay()在我们任务中用得非常之多,每个任务都必须是死循环,并且是必须要有阻塞的情况,否则低优先级的任务就无法被运行了。
要想使用 FreeRTOS 中的 vTaskDelay()函数必须在 FreeRTOSConfig.h 中把 INCLUDE_vTaskDelay 定义为 1 来使能。

vTaskDelay()用于阻塞延时,调用该函数后,任务将进入阻塞状态,进入阻塞态的任务将让出 CPU 资源。延时的时长由形参 xTicksToDelay 决定,单位为系统节拍周期, 比如系统的时钟节拍周期为 1ms,那么调用vTaskDelay(1)的延时时间则为1ms。

vTaskDelay()延时是相对性的延时,它指定的延时时间是从调用 vTaskDelay()结束后开始计算的,经过指定的时间后延时结束。比如 vTaskDelay(100), 从调用 vTaskDelay()结束后,任务进入阻塞状态,经过 100 个系统时钟节拍周期后,任务解除阻塞。因此,vTaskDelay()并不适用与周期性执行任务的场合。此外,其它任务和中断活动, 也会影响到 vTaskDelay()的调用(比如调用前高优先级任务抢占了当前任务),进而影响到任务的下一次执行的时间:

image

如上图,我们正在以1s的延时进行运行,相当于1000个系统节拍周期,如果此时在运行到4000的时候,被中断打断,假如打断0.3秒也就是300个周期,我们会发现后续的时间节点会发生变化,不是按照固定的周期进行运行。

举个例子:

//LED1任务主体
static void LED_Task(void* parameter)
{	
	while (1)
	{
		LED1_ON;
		vTaskDelay(500);   /* 延时500个tick */
		printf("time = %d\r\n",xTaskGetTickCount());
		printf("LED_Task Running,LED1_ON\r\n");
 
		LED1_OFF;     
		vTaskDelay(500);   /* 延时500个tick */		
		printf("time = %d\r\n",xTaskGetTickCount()); 		
		printf("LED_Task Running,LED1_OFF\r\n");
	}
}

可以看出可能因为任务转换需要少量的耗时,做种出现的结果并不是501、1001、1501等等:
image

vTaskDelayUntil()

在 FreeRTOS 中,除了相对延时函数,还有绝对延时函数 vTaskDelayUntil(),这个绝对延时常用于较精确的周期运行任务,比如我有一个任务,希望它以固定频率定期执行,而不受外部的影响,任务从上一次运行开始到下一次运行开始的时间间隔是绝对的,而不是相对的:
image

任务会先调用vTaskDelayUntil()使任务进入阻塞态,等到时间到了就从阻塞中解除,然后执行主体代码,任务主体代码执行完毕。会继续调用vTaskDelayUntil()使任务进入阻塞态,然后就是循环这样子执行。即使任务在执行过程中发生中断,那么也不会影响这个任务的运行周期,仅仅是缩短了阻塞的时间而已,到了要唤醒的时间依旧会将任务唤醒。

简单点来说,假如我们点个灯,想让他亮一秒每一秒,突然有一次有个中断占用了0.3秒的时间但是它的亮灯时间的计数器并没有因为中断占用而停止,当中断任务结束,只能在亮0.7秒的灯。

vTaskDelayUntil() 与 vTaskDelay () 一 样都是用来实现任务的周期性延时。 但vTaskDelay ()的延时是相对的,是不确定的,它的延时是等 vTaskDelay ()调用完毕后开始计算的。并且 vTaskDelay ()延时的时间到了之后,如果有高优先级的任务或者中断正在执行,被延时阻塞的任务并不会马上解除阻塞,所有每次执行任务的周期并不完全确定。而vTaskDelayUntil()延时是绝对的,适用于周期性执行的任务。当(*pxPreviousWakeTime + xTimeIncrement)时间到达后,vTaskDelayUntil()函数立刻返回,如果任务是最高优先级的,那么任务会立马解除阻塞,所以说 vTaskDelayUntil()函数的延时是绝对性:

#if ( INCLUDE_vTaskDelayUntil == 1 )
 
	void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, //指针,指向一个变量,该变量保存任务最后一次解除阻塞的的时刻。第一次使用时,该变量必须初始化为当前时间,之后这个变量会在 vTaskDelayUntil()函数内自动更新。
																		 const TickType_t xTimeIncrement )//周期循环时间。 当时间等于 (*pxPreviousWakeTime + xTimeIncrement)时,任务解除阻塞。如果不改变参数 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;
 
			/*pxPreviousWakeTime 中保存的是上次唤醒时间,唤醒后需要一定时间执行任务主体代码,如果上次唤醒时间大于当前时间,说明节拍计数器溢出了*/
			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()函数需要的是阻塞时间而不是唤醒时间,因此减去当前的滴答计数。*/
				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();
		}
	}
 
#endif /* INCLUDE_vTaskDelayUntil */

对于其中的代码片段:

				/* 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();
				}

注意记住下面单词表示的含义:
xTimeIncrement:任务周期时间。
pxPreviousWakeTime:上一次唤醒任务的时间点。
xTimeToWake:本次要唤醒任务的时间点。
xConstTickCount:进入延时的时间点。

image

只是唤醒时间 xTimeToWake 溢出的情况,或者是 xTickCount 与xTimeToWake 都没溢出的情况,都是符合要求的,因为都保证了周期性延时时间大于任务主体代码的执行时间:
只有任务唤醒时间溢出:
image

都没有溢出:
image

可以看出无论是溢出还是没有溢出,都要求在下次唤醒任务之前,当前任务主体代码必须被执行完。也就是说任务执行的时间必须小于任务周期时间 xTimeIncrement,总不能存在任务周期为 10ms 的任务,其主体代码执行时间为 20ms,这样子根本执行不完任务主体代码。计算的唤醒时间合法后,就将当前任务加入延时列表,同样延时列表也有两个。每次产生系统节拍中断,都会检查这两个延时列表,查看延时的任务是否到期,如果时间到,则将任务从延时列表中删除,重新加入就绪列表,任务从阻塞态变成就绪态,如果此时的任务优先级是最高的,则会触发一次上下文切换。

举个例子:
这一次我们使用绝对延时看看现象,和相对延时的差别,更改LED的代码,我们知道绝对延时的下次唤醒时间 = 上次唤醒时间 + 间隔时间,因此我们根据上面单词创建pxPreviousWakeTime(上一次唤醒任务的时间点),创建TimeIncrement(表示任务周期时间),通过调用xTaskGetTickCount();函数获取当前系统时间:

//LED1任务主体
static void LED_Task(void* parameter)
{	
	/* 用于保存上次时间。调用后系统自动更新 */ 
	static portTickType pxPreviousWakeTime;
	/* 设置延时时间,将时间转为节拍数 */ 
	const portTickType TimeIncrement = pdMS_TO_TICKS(500);
	
	/* 获取当前系统时间 */ 
	pxPreviousWakeTime = xTaskGetTickCount();
 
	while (1)
	{
		LED1_ON;
		vTaskDelayUntil( &pxPreviousWakeTime,TimeIncrement );//调用绝对延时函数,任务时间间隔为 500 个 tick 
		printf("time = %d\r\n",xTaskGetTickCount());
		printf("LED_Task Running,LED1_ON\r\n");
 
		LED1_OFF;     
		vTaskDelayUntil( &pxPreviousWakeTime,TimeIncrement );//调用绝对延时函数,任务时间间隔为 500 个 tick 
		printf("time = %d\r\n",xTaskGetTickCount()); 		
		printf("LED_Task Running,LED1_OFF\r\n");
	}
}

此时我们可以看出此时的延时是绝对的,说是500ms就是500ms,没有想相对延时一样出现差距:
image

我们在实验一下超过500的情况,即任务执行的时间大于任务周期时间:

//LED1任务主体
static void LED_Task(void* parameter)
{	
	/* 用于保存上次时间。调用后系统自动更新 */ 
	static portTickType pxPreviousWakeTime;
	/* 设置延时时间,将时间转为节拍数 */ 
	const portTickType TimeIncrement = pdMS_TO_TICKS(500);
	
	/* 获取当前系统时间 */ 
	pxPreviousWakeTime = xTaskGetTickCount();
 
	while (1)
	{
		LED1_ON;
		vTaskDelayUntil( &pxPreviousWakeTime,TimeIncrement );//调用绝对延时函数,任务时间间隔为 500 个 tick 
		printf("time = %d\r\n",xTaskGetTickCount());
		printf("LED_Task Running,LED1_ON\r\n");
 
		vTaskDelay(520);//用于模拟其他任务占用的时间
 
		LED1_OFF;     
		vTaskDelayUntil( &pxPreviousWakeTime,TimeIncrement );//调用绝对延时函数,任务时间间隔为 500 个 tick 
		printf("time = %d\r\n",xTaskGetTickCount()); 		
		printf("LED_Task Running,LED1_OFF\r\n");
	}
}

可以看到在关闭LED的时候,发生了20的偏移,不过由于没有超过完整的运行周期,在开启的时候有纠正了回去,相当于开启480ms,关闭520ms:
image

如果超过完整的运行周期:

//LED1任务主体
static void LED_Task(void* parameter)
{	
	/* 用于保存上次时间。调用后系统自动更新 */ 
	static portTickType pxPreviousWakeTime;
	/* 设置延时时间,将时间转为节拍数 */ 
	const portTickType TimeIncrement = pdMS_TO_TICKS(500);
	
	/* 获取当前系统时间 */ 
	pxPreviousWakeTime = xTaskGetTickCount();
 
	while (1)
	{
		LED1_ON;
		vTaskDelayUntil( &pxPreviousWakeTime,TimeIncrement );//调用绝对延时函数,任务时间间隔为 500 个 tick 
		printf("time = %d\r\n",xTaskGetTickCount());
		printf("LED_Task Running,LED1_ON\r\n");
 
		vTaskDelay(1200);//用于模拟其他任务占用的时间
 
		LED1_OFF;     
		vTaskDelayUntil( &pxPreviousWakeTime,TimeIncrement );//调用绝对延时函数,任务时间间隔为 500 个 tick 
		printf("time = %d\r\n",xTaskGetTickCount()); 		
		printf("LED_Task Running,LED1_OFF\r\n");
	}
}

会发现由于延时过长,扰乱了正常的时序,导致LED关闭后直接开启:
image

posted @ 2026-09-11 19:25  灵垚克府  阅读(5)  评论(0)    收藏  举报