freeRTOS学习(三)

任务管理

任务功能:任务以C函数的形式实现。唯一特别的地方是它们的原型,它必须返回void并接受一个void指针形参。

void ATaskFunction(void *pvParameters);

**每个任务本身都是一个小程序。它有一个入口点,通常在无限循环中永远运行,不会退出。**典型任务的结构如Listing12所示。

FreeRTOS任务不能以任何方式从它们的实现函数中返回——它们不能包含’return’语句,也不能在函数结束后执行。如果不再需要某个任务,应该显示删除它。

单个任务函数定义可用于创建任意数量的任务,每个创建的任务都是一个单独的执行实例,具有自己的堆栈和任务本身中定义的任何自动(堆栈)变量的副本。
在这里插入图片描述

void ATaskFunction(void *pvParameters)
{
	/*可以像普通函数一样声明变量。
	使用这个实例函数创建的每个任务实例都有自己的lVariableExample变量副本。
	如果变量被声明为静态,这种情况下,变量只存在一个副本,并且这个副本将由每个创建的任务实例共享。*/
	int32_t lVariableExample = 0;
	
	/*一个任务通常被实现为无限循环*/
	for(;;)
	{
		/*实现任务的功能代码*/
	}
	/*如果任务实现突破了上面的循环,那么必须在达到其实现功能的结束之前删除任务。
	传递给vTaskDelete()函数的NULL参数表示要删除的任务是调用(this)任务
	*/
	vTaskDele(NULL);
}

顶级任务状态

当任务处于Running状态时,处理器正在执行任务的代码。当任务处于Not Running状态时,该任务处于休眠状态,它的状态已被保存,以便下次调度器决定它应该进入Running状态时恢复执行。当任务恢复执行时,它从上次离开Running状态之前即将执行的指令开始执行。
在这里插入图片描述
从“未运行”状态转换到“运行”状态的任务被称为“换入”或“换入”。相反,从运行状态转换到非运行状态的任务被称为“换出”或“换出”。
FreeRTOS调度器是唯一可以切换任务的实体。

创造任务

xTaskCreate() API函数:任务是使用FreeRTOS的xTaskCreate() API函数创建的。

BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,  
                        const char * const pcName,  
                        uint16_t usStackDepth,  
                        void *pvParameters,  
                        UBaseType_t uxPriority,  
                        TaskHandle_t *pxCreatedTask ); 

参数详细介绍

  • pvTaskCode:任务只是从不退出的C函数,因此,通常以无限循环的形式实现。pvTaskCode参数只是一个指向现实任务的函数的指针(函数名)。
  • pcName:任务的描述性功能。FreeRTOS不以任何方式使用此功能。包含它纯粹是作为调试辅助。通过人类可读的名称标识任务要比试图通过句柄标识任务简单得多。应用程序定义的常量configMAX_TASK_NAME_LEN定义了任务名称的最大长度,包括NULL终止字符。提供一个长于此最大值的字符串将导致字符串被静默截断。
  • usStackDepth:每个任务都有自己唯一的堆栈,在创建任务时由内核分配给该任务。usStackDepth值告诉内核堆栈的大小。该值指定堆栈可以容纳的字数。如果堆栈是32位,并且usStackDepth作为100传入,那么将分配400字节的堆栈空间(100 * 4字节)。堆栈深度乘以堆栈宽度不能超过uint16_t类型变量所能包含的最大值。
    Idle任务使用的堆栈大小由应用程序定义的常量configMINIMAL_STACK_SIZE1定义。在FreeRTOS演示应用程序中,为正在使用的处理器体系结构分配这个常量的值是任何任务推荐的最小值。Idle任务使用的堆栈大小由应用程序定义的常量configMINIMAL_STACK_SIZE1定义。在FreeRTOS演示应用程序中,为正在使用的处理器体系结构分配给这个常量的值是任何任务推荐的最小值。如果任务使用了大量堆栈空间,则必须分配更大的值。
    没有一种简单的方法可以确定任务所需的堆栈空间。这是可以计算的,但大多数用户只会分配他们认为合理的值,然后使用FreeRTOS提供的特性来确保分配的空间确实足够,RAM不会被不必要地浪费。第12.3节“堆栈溢出”包含了如何查询任务实际使用的最大堆栈空间。
  • pvParameters:任务创造函数接受一个指向void(void *)指针类型的形参。分配给pvParameters的值就是传递给任务的值。
  • uxPriority:定义任务执行时的优先级。优先级可以从0(最低优先级)分配到(configMAX_PRIORITIES-1)(最高优先级)。configMAX_PRIORITIES是一个用户定义的常量。在上面传递一个uxPriority值(configMAX_PRIORITIES - 1)将导致分配给任务的优先级被默认封顶为最大合法值。
  • pxCreatedTask:向正在创建的任务传递句柄。这个句柄可以用于在API调用中引用任务,例如改变任务优先级或删除任务。如果您的应用程序不使用任务句柄,那么pxCreatedTask可以设置为NULL。
  • 返回值:有两种可能。1.pdPASS:任务创建成功。2.pdFalse:没有创建任务,因为FreeRTOS没有足够的堆内存来分配足够的RAM来容纳任务数据结构和堆栈。

例1.创建任务
(这个例子演示了创建两个简单任务,然后开始执行这些任务所需的步骤。任务只是定期打印一个字符串,使用一个粗糙的空循环来创建周期延迟。这两个任务以相同的优先级创建,除了输出的字符串外是相同的)

void vTask1(void *pvParameters)
{
	const char *pcTaskName = "Task 1 is running\r\n";
	volatile uint32_t ul; /*volatile以确保ul没有被优化掉*/
	/*与大多数任务一样,该任务是在无限循环中实现的。*/
	for(;;)
	{
		/*打印出此任务的名称。*/
		vPrintString(pcTaskName);
		/*延迟一段时间*/
		for(ul = 0; ul < mainDELAY_LOOP_COUNT; ul++)
		{
			/**/
		}
	}
}
void vTask2(void *pvParameters)
{
	const char *pcTaskName = "Task 2 is running\r\n";
	volatile uint32_t ul; /*volatile以确保ul没有被优化掉*/
	/*与大多数任务一样,该任务是在无限循环中实现的。*/
	for(;;)
	{
		/*打印出此任务的名称。*/
		vPrintString(pcTaskName);
		/*延迟一段时间*/
		for(ul = 0; ul < mainDELAY_LOOP_COUNT; ul++)
		{
			/**/
		}
	}
}

main()函数在启动调度器之前创建了任务

int main(void)
{
	/*创建两个任务中的一个。真正的应用程序应该检查xTaskCreate()调用的返回值,来确保任务被成功创建。*/
	 xTaskCreate(    vTask1,  /*指向实现任务的函数指针。*/ 
                    "Task 1",/*任务的文本名称 */ 
                    1000,    /* 堆栈深度,小型微控制器使用的堆栈比这还少 */ 
                    NULL,    /*未使用task参数。 */ 
                    1,       /*任务以优先级1运行。*/ 
                    NULL );  /*不使用任务句柄 */ 
    /*以完全相同的方式和相同的优先级创建另一个任务。* /
    xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );

	/*启动调度程序开始执行*/
	vTaskStartScheduler();

	/*如果一切正常,main()将永远不会到达这里,因为调度器现在正在运行任务。如果main()到达这里,则很可能没有足够的堆内存用于创建空闲任务。*/
	for(;;);
}

在这里插入图片描述
两个任务以相同的优先级运行,因此在相同的处理器核心上共享时间。它们的实际执行模式如图11所示。
在这里插入图片描述
同一时间只能有一个任务处于Running状态。因此,当一个任务进入Running状态(该任务被切换进来)时,另一个任务进入Not Running状态(该任务被切换出去)。

示例1在启动调度器之前,从main()中创建了这两个任务。也可以从另一个任务中创建一个任务。例如,可以从Task1中创建Task2.

void vTask1(void *pvParameters)
{
	const char *pcTaskName = "Task 1 is running\r\n";
	volatile uint32_t ul; /*volatile以确保ul没有被优化掉*/
	/*在进入无限循环之前创建另一个任务。*/
	xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
	/*与大多数任务一样,该任务是在无限循环中实现的。*/
	for(;;)
	{
		/*打印出此任务的名称。*/
		vPrintString(pcTaskName);
		/*延迟一段时间*/
		for(ul = 0; ul < mainDELAY_LOOP_COUNT; ul++)
		{
			/**/
		}
	}
}

例2.使用task参数
在示例1中创建的两个任务几乎相同,唯一的区别是它们打印出的文本字符串。
我们可以通过创建单个任务实现的两个实例来消除这种重复。然后可以使用task参数向每个任务传递它应该打印出来的字符串。

void vTaskFunction(void *pvParameter)
{
	char *pcTaskName;
	volatile uint32_t ul;
	pcTaskName = (char *)pvParameter;
	for(;;)
	{
		vPrintString(pcTaskName);
		for(ul = 0; ul < mainDELAY_LOOP_COUNT; ul++)
		{
			
		}
	}
}

即使现在只有一个任务实现(vTaskFunction),也可以创建已定义任务的多个实例。每个创建的实例将在FreeRTOS调度程序的控制下独立运行。

/*定义将作为任务参数传入的字符串。
它们被定义为const,而不是在堆栈上,以确保它们在任务执行时仍然有效*/
static const char *pcTextForTask1 = "Task 1 is running\r\n"; 
static const char *pcTextForTask2 = "Task 2 is running\r\n";

int main(void)
{
	xTaskCreate(    vTaskFunction,          /* 指向实现任务的函数指针 */ 
                    "Task 1",               /* 任务的文本名称。只是为了方便调试。 */ 
                    1000,                   /*堆栈深度,小型微控制器使用的堆栈比这要少得多。 */ 
                    (void*)pcTextForTask1,  /*使用task参数将要打印的文本传递到任务中。*/ 
                    1,                      /*任务优先级为1 */ 
                    NULL );                 /*没有使用文件句柄。 */
}

/*以完全相同的方式创建另一个任务。注意,这一次将从SAME任务实现(vTaskFunction)创建多个任务。只有参数中传递的值不同。正在创建同一个任务的两个实例。* /
xTaskCreate(vTaskFunction, "Task 2", 1000, (void *)pcTextForTask2, 1, NULL );

/*启动调度程序*/
vTaskStartScheduler();

/*如果一切正常,main()将永远不会到达这里,因为调度器现在正在运行任务。如果main()到达这里,则很可能没有足够的堆内存用于创建空闲任务。第2章提供了关于堆内存管理的更多信息。* /
for(;;);

任务优先级

xTaskCreate() API函数的uxPriority参数为正在创建的任务分配初始优先级。通过使用vTaskPrioritySet() API函数,可以在启动调度器之后更改优先级

可用的最大优先级数由FreeRTOSConfig.h中应用程序定义的configMAX_PRIORITIES编译时配置常量设置。
低数值优先级值表示低优先级任务,低优先级0是可能的最低优先级。
优先级范围(0 ~ configMAX_PRIORITIES-1)。任务数量的任务都可以共享相同的优先级,确保最大的设计灵活性。

FreeRTOS调度器使用两种方法决定哪个任务处于Running状态。configMAX_PRIORITIES可以设置的最大值取决于所使用的方法:

  1. 通用方法
    通用方法是用C实现的,可以与所有FreeRTOS体系结构端口一起使用。
    当使用通用方法时,FreeRTOS不限制configMAX_PRIORITIES可以设置的最大值。但是,总是建议**将configMAX_PRIORITIES值保持在必要的最小值,因为它的值越高,消耗的RAM越多,最坏情况下的执行时间就越长。
    **
    如果configUSE_PORT_OPTIMIDES_TASK_SELECTION在FreeRTOSConfig.h中设置为0,或者未定义,或者通用方法是正在使用的FreeRTOS端口提供的唯一方法,将使用通用方法。
  2. 结构优化方法
    体系结构优化方法使用了少量的汇编程序代码,并且比通用方法更快。configMAX_PRIORITIES设置不会影响最坏情况下的执行时间。
    如果使用架构优化方法,那么configMAX_PRIORITIES不能大于32.与通用方法一样,建议将configMAX_PRIORITIES保持在必要的最小值,因为它的值越高,消耗的RAM越高。
    如果在FreeRTOSConfig.h中configUSE_PORT_OPTIMISED_TASK_SELECTION设置为1,将使用架构优化方法。

FreeRTOS调度器将始终确保能够运行的优先级最高的任务是被选中进入运行状态的任务。当多个具有相同优先级的任务能够运行时,调度器将依次将每个任务转换为Running状态和退出Running状态。

时间测量和Tick中断

为了能够选择要运行的下一个任务,调度器本身必须在每个时间切片1结束时执行。周期性中断,称为“tick interrupt”。
时间片的长度由tick中断频率设置,该频率由FreeRTOSConfig.h中应用程序定义的configTICK_RATE_HZ编译时间配置常量。

如果configTICK_RATE_HZ被设置为100HZ,那么时间片就是10毫秒。
configTICK_RATE_HZ的最佳值取决于正在开发的应用程序。

两次tick中断之间的时间被称为tick 周期,一个事件切片就等同于一个tick周期。

在这里插入图片描述

  1. Tick中断发生。
  2. 内核在Tick中断中运行以选择下一个任务。
  3. 当tick中断完成后,新选择的任务将运行。

重要的是要注意,时间片的结束并不是调度器可以选择要运行的新任务的唯一位置;正如本书将演示的那样,当当前执行的任务进入Blocked状态,或者当中断将一个高优先级的任务移到Ready状态时,调度程序也将选择一个新任务来立即运行。

FreeRTOS API调用总是以多个Tick 周期指定时间,这通常称为Ticks。
pdMS_TO_TICKS()宏将以毫秒为单位指定的时间转换为以tick为单位指定的时间。
可用的分辨率取决于定义的tick频率,如果tick频率高于1KHZ(configTICK_RATE_HZ大于1000),pdMS_TO_TICKS()不能使用。

/*pdMS_TO_TICKS()将以毫秒为单位的时间作为唯一参数,并计算为以tick为周期的等效时间。这个示例显示将xTimeInTicks设为相当于200ms的tick周期的数量*/
TickType_t xTimeInTicks = pdMS_TO_TICKS(200);

不建议在程序中直接指定以tick为单位的时间,而是使用pdMS_TO_TICKS()宏指定以毫秒为单位的世界,这样做可以确保在tick频率发生更改时,在应用程序中指定的时间不会更改。

'tick count’值是从调度程序启动以来发生tick的总数,假设tick计数没有溢出。

实例3.带有优先级的试验
调度器将始终确保能够运行的优先级的最高的任务是被选中进入Running状态的任务。到目前为止,在我们的示例中,已经以相同的优先级创建了两个任务,因此它们依次进入和退出Running状态。这个示例将查看在示例2中创建的两个任务之一的优先级发生更改时发生的情况。这一次,第一个任务将在优先级1上创建,第二个任务将在优先级2上创建。
在这里插入图片描述
在这里插入图片描述
调度器始终选择能够运行的优先级最高的任务。Task2优先级高于Task1,所以Task 2是唯一进入Running状态的任务。因为Task 1从来没有进入Running状态,所以它从来没有打印出它的字符串。Task 1被Task 2“耗尽”了处理时间。

在这里插入图片描述

  1. Tick中断发生
  2. 调度程序在tick中断中运行,但选择相同的任务。任务2始终处于运行状态,任务1始终处于未运行状态

扩展“未运行”状态

到目前为止,创建的任务总是有处理要执行,而且从不需要等待任何事情——因为它们从不需要等待任何事情,所以它们总是能够进入Running状态。这种“持续处理”任务的用处有限,因为它们只能在最低优先级的情况下创建。如果它们以任何其他优先级运行,它们将完全阻止低优先级的任务运行。

为了使任务有用,必须将它们重写为事件驱动的。事件驱动的任务只有在触发它的事件发生之后才有工作(处理)要执行,并且在该事件发生之前无法进入Running状态。调度器总是选择能够运行的优先级最高的任务。高优先级任务不能运行意味着调度器不能选择它们,而必须选择能够运行的低优先级任务。因此,使用事件驱动的任务意味着可以在不同的优先级上创建任务,而不会让最高优先级的任务耗尽所有低优先级任务的处理时间。

阻塞状态
等待事件的任务被称为处于“Blocked”状态,这也是“Not Running”状态的子状态。

任务可以进入Blocked状态来等待两种不同类型的事件:

  1. 临时事件(与时间相关)——事件要么是延迟期到期,要么是到达的绝对时间。
    例如,一个任务可能进入阻塞状态等待10秒通过。
  2. 同步事件——事件起源于另一个任务或中断。
    例如,一个任务可能进入Blocked状态来等待数据到达队列。同步事件涵盖了广泛的事件类型。

FreeRTOS队列、二进制信号量、计数信号量、互斥锁、递归互斥锁、事件组和直接接到的通知都可以用来创建同步事件。

任务可以通过超时阻塞同步事件,有效地同时阻塞两种类型的事件。
例如,一个任务可以选择等待数据到达队列的最长时间为10毫秒,如果有数据在10毫秒内到达,或者10毫秒过后没有数据到达,任务将离开Blocked状态。

挂起状态
处于挂起状态的任务对调度器不可用。进入Suspended状态的唯一方法是调用vTaskSuspend()API函数,唯一离开的方法是调用vTaskResume()或xTaskResumeFromISR() API函数。大多数应用程序不使用Suspended状态。

就绪状态
处于“未运行”状态但没有阻塞或挂起的任务称为“就绪”状态。它们能够运行,因此“准备好”运行,但当前没有处于Running状态。

完成状态转换图
在这里插入图片描述
例4.使用Blocked状态来创建延迟
上述例子高优先级任务在执行空循环时保持Running状态,“限制”低优先级任务的任何处理时间。
任何形式的轮询都有几个缺点,尤其是效率低下。
在轮询期间,任务实际上没有任何工作要做,但它仍然使用最大处理时间,因此浪费处理器周期。
使用vTaskDelay() API函数替换轮询空循环,从而纠正这种行为。
只有在FreeRTOSConfig.h中将INCLUDE_vTaskDelay设置为1时,vTaskDelay() API函数才可用。

vTaskDelay()将调用任务置于Blocked状态,进行固定数量的tick中断,该任务在处于Blocked状态时,不使用任何处理时间,因此该任务仅在有实际工作需要完成时使用处理时间。

void vTaskDelay(TickType_t xTicksToDelay);
  • xTicksToDelay:tick中断数。
    如果一个名为vTaskDelay(100)的任务在tick count为10000时被调用,它会立即进入Blocked状态,并一直保持Blocked状态,直到tick_count为10100。

宏pdMS_TO_TICKS()可用于将毫秒转换为指定的tick数,例如调用vTaskDelay(pdMS_TO_TICKS(100)),导致调用任务保持阻塞状态100毫秒。

void vTaskFunction(void *pvParamaters)
{
	char *pcTaskName;
	const TickType_t xDelay250ms = pdMS_TO_TICKS(250);

	/*要打印出来的字符串是通过参数传入的。将其转换为字符指针。*/
	pcTaskName = (char *)pvParamaters;
	 /* 和大多数任务一样,这个任务是在一个无限循环中实现的。*/ 
	for(;;)
	{
		vPrintString(pcTaskName);
		/*
			延迟一段时间,这一次使用对vTaskDelay()的调用,它将任务置于Blocked状态,直到延迟期过期。参数的时间用'ticks'指定
		*/
		vTaskDelay(xDelay250ms);
	}
}

空闲任务是在启动调度器时自动创建的,以确保始终至少有一个任务能够运行(至少有一个任务处于Ready状态)。
在这里插入图片描述

  1. Task 2具有最高的优先级,所以首先运行。它打印出它的字符串,然后调用vTaskDelay()——这样就进入了Blocked状态,允许较低优先级的Task 1执行。
  2. Task 1打印出它的字符串,然后它也通过调用vTaskDelay()进入Blocked状态。
  3. 此时两个应用程序任务都处于Blocked状态——因此Idle任务运行。
  4. 当延迟到期时,调度器将任务移回到就绪状态,在再次调用vTaskDelay()之前,两者都将再次执行,导致它们重新进入Blocked状态。Task 2优先执行,因为它具有更高的优先级。

任务在整个延迟期间进入Blocked状态,只有真正需要执行工作时才使用处理器。

大多数情况下,没有能够运行的应用程序任务(没有可以选择进入running状态的应用程序任务。)**在这种情况下,空闲任务将运行。分配给空闲任务的处理时间量是系统中空闲处理能力的度量。**通过允许应用程序完全由事件驱动,使用RTOS可以显著增加空闲处理能力。

vTaskDelayUntil()API函数
vTaskDelayUntil()类似于vTaskDelay()。

  • vTaskDelay()参数指定了调用vTaskDelay()的任务与再次脱离Blocked状态的相同任务之间应该发生的tick中断数量。任务保持在阻塞状态的时间长度有vTaskDelay()参数指定。但是任务离开阻塞状态的世界与调用vTaskDelay()的时间相关
  • 相反,vTaskDelayUntil()的参数指定了调用任务应该从Blocked状态移到Ready状态的确切的tick计数值。当需要一个固定的执行周期时应该使用它。因为调用任务被解除阻塞的时间是绝对的,而不是相对于函数被调用的时间。
void vTaskDelayUntil(TickType_t * pxPreviousWakeTime, TickType_t xTimeIncrement);
  • pxPreviousWakeTime:这个参数的命名假设是使用vTaskDelayUntil()来实现一个定期执行且频率固定的任务。这种情况下,pxPreviousWakeTime保存了任务上一次离开Blocked状态的时间(被唤醒)。这个时间作为参考点,计算任务下一次离开Blocked状态的时间。
    pxPreviousWakeTime指向的变量在vTaskDelayUntil()函数中自动更新;它通常不会被应用程序代码修改,但是在第一次使用它之前必须初始化为当前的tick计数值。
  • xTimeIncrement:这个参数的命名也基于这样一个假设:使用vTaskDelayUntil()来实现一个定期执行的任务,该任务的执行频率是固定的,频率由——xTimeIncrement值设定。xTimeIncrement由’ticks’定义。

例5。将示例任务转换为使用vTaskDelayUntil()
例4中创建的两个任务是周期性任务,但是**使用vTaskDelay()不能保证它们运行的频率是固定的,**因为任务离开Blocked状态的时间与它们调用vTaskDelay()时间是相对的。
将任务转换为使用vTaskDelayUntil()而不是vTaskDelay()解决了这个潜在的问题。

void vTaskFunction(void *pvParameters)
{
	char *pcTaskName;
	TickType_t xLastWakeTime;
	
	pcTaskName = (char *)pvParameters;
	/*xLastWakeTime变量需要用当前tick计数初始化。注意,这是唯一一次显式写入变量。之后xLastWakeTime在vTaskDelayUntil()中自动更新。*/
	xLastWakeTime = xTaskGetTickCount();
	for(;;)
	{
		vPrintString( pcTaskName ); 
		/*该任务每250毫秒执行一次。*/				vTaskDelayUntil(&xLastWakeTime,pdMS_TO_TICKS(250));
	}
	
}

例6.组合阻塞和非阻塞任务

  1. 在优先级1上创建两个任务。这些函数除了连续输出字符串外什么也不做。这些任务从不进行任何可能导致它们进入Blocked状态的API函数调用,因此总是处于Ready或Running状态。这种性质的任务被称为“连续处理”任务,因为它们总是有工作要做(尽管在这种情况下是相当琐碎的工作)。
  2. 然后在优先级2上创建第三个任务,因此在其他两个任务的优先级之上。第三个任务也只是打印出一个字符串,但这一次是周期性的,因此使用vTaskDelayUntil() API函数在每次打印迭代之间将自己置于Blocked状态。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 连续任务1运行一个完整的tick周期,在此期间它可以多次打印其字符串。
  2. 当程序调度程序选择要运行的新任务时,会发生tick中断。由于两个连续任务具有相同的优先级,并且都始终能够运行,调度器在两者之间共享处理器时间——因此连续2任务进入运行状态,并在整个tick周期内保持该状态——在此期间,它可以多次打印其字符串。
  3. 在时间t3,tick中断再次运行,导致任务切换回连续任务1,以此类推。
  4. 在时间t5时,tick中断发现周期任务块周期已经过期了,因此将周期任务移到就绪状态。周期任务是优先级最高的任务,因此立即进入运行状态,在调用vTaskDelayUntil()返回Blocked状态之前,它只打印一次字符串。
  5. Idle任务永远不会进入Running状态,因为总是有更高优先级的任务能够进入Running状态。

空闲任务和空闲任务钩子

规定总是至少有一个任务可以进入running状态。为了确保这种情况,调度程序在调用vTaskStartScheduler()时自动创建Idle任务。空闲任务除了在一个循环中运行之外,没有做其他的。

空闲任务具有尽可能低的优先级(优先级为0),确保它不会阻止更高优先级的应用程序任务进入运行状态。
FreeRTOSConfig.h中的configIDLE_SHOULD_YIELD编译时配置常量可用于防止Idle任务占用处理时间。

以最低优先级运行可以确保当高优先级任务进入就绪状态时,Idle任务将从Running状态转换出来。

如果应用程序使用vTaskDelete()API函数,那么Idle任务不缺乏处理时间是很重要的。因为Idle任务负责在删除任务后清理内核资源。

空闲任务钩子函数
可以通过使用空闲钩子函数(空闲回调),将特定于应用程序的功能直接添加到空闲任务中——空闲任务在每次迭代空闲任务循环时自动调用该函数。

Idle任务钩子的常用用法:

  1. 执行低优先级、后台或连续处理功能。
  2. 测量备用处理能力。(只有当所有高优先级的应用程序任务都没有工作要执行时,空闲任务才会运行;因此,测量分配给空闲任务的处理时间可以清楚地表明有多少处理时间是空闲的。)
  3. 将处理器置于低功耗模式,在没有应用程序处理要执行时提供一种简单和自动的省电方法。

实现空闲任务钩子函数的限制
空闲任务钩子函数必须遵守以下规则。

  1. Idle任务钩子函数绝对不能阻塞或挂起。(以任何方式阻塞空闲任务都可能导致没有可用任务进入运行状态)
  2. 如果应用程序使用vTakDelete() API函数,那么Idle任务钩子必须总是在合理的时间段内返回给它的调用者。因为Idle任务负责在删除任务后清理内核资源。
void vApplicationIdleHook(void);

定义一个空闲任务钩子函数

/*声明一个钩子函数递增的变量。*/
volatile uint32_t ulIdleCycleCount = 0UL;

/*空闲钩子函数必须调用vApplicationIdleHook(),不接受参数,并返回void*/
void vApplicationIdleHook(void)
{
	ulIdleCycleCount++;
}

configUSE_IDLE_HOOK必须在FreeRTOSConfig.h中设置为1,才能调用空闲钩子函数。

在这里插入图片描述
在这里插入图片描述
示例7产生的输出如图21所示。它显示了在应用程序任务的每次迭代之间大约调用了400万次空闲任务钩子函数(迭代的次数取决于执行演示的硬件的速度)。

修改任务的优先级
vTaskPrioritySet() API函数可用于在启动调度器后更改任何任务的优先级。
只有在FreeRTOSConfig.h中将INCLUDE_vTaskPrioritySet设置为1时,vTaskPrioritySet() API函数才可用。

void vTaskPrioritySet(TaskHandle_t pxTask,UBaseType_t uxNewPriority);
  • pxTask:正在修改优先级的任务的句柄(主题任务)。任务可以通过传递NULL来代替有效的任务句柄来改变自己的优先级。
  • uxNewPriority:要设置任务的优先级,浙江自动限制为(configMAX_PRIORITIE-1)的最大可用优先级,其中configMAX_PRIORITIES是在FreeRTOSConfig.h头文件中设置的编译时间常数。

**uxTaskPriorityGet()**API函数可用于查询任务的优先级。
只有在FreeRTOSConfig.h中将INCLUDE_uxTaskPriorityGet设置为1时,uxTaskPriorityGet()才可以使用。

UBaseType_t uxTaskPriorityGet(TaskHandle_t pxTask);
  • pxTask:正在查询优先级的任务的句柄。任务可以通过传递NULL来查询自己的优先级。
  • 返回值:当前分配给查询任务的优先级。

例8.改变任务优先级
调度器将始终选择最高的Ready状态任务作为进入Running状态的任务。示例8通过使用vTaskPrioritySet() API函数来改变两个任务的优先级。
例8创建两个具有两个不同优先级的任务。这两个任务都没有进行任何可能导致它进入Blocked状态的API函数调用,因此它们总是处于Ready状态或Running状态。因此,相对优先级最高的任务将始终是调度器选择的处于Running状态的任务。

  1. Task 1(清单33)是用最高优先级创建的,因此保证首先运行。Task 1在将Task 2(清单34)的优先级提高到高于它自己的优先级之前打印出两个字符串。
  2. 当Task 2的相对优先级最高时,Task 2立即开始运行(进入Running状态)。同一时间只能有一个任务处于“运行中”状态,因此当任务2处于“运行中”状态时,任务1处于“就绪”状态。
  3. Task 2在将自己的优先级降低到Task 1以下之前打印出一条消息。
  4. Task 2将优先级降低意味着Task 1再次成为优先级最高的任务,因此Task 1重新进入Running状态,迫使Task 2回到Ready状态。
void vTask1(void *pvParamaters)
{
	UBaseType_t uxPriority;
	/*此任务总是在task 2之前运行,因为它是用更高的优先级创建的。Task 1和Task 2都不会阻塞,所以它们总是处于Running或Ready状态。

查询任务运行时的优先级—传入NULL表示“返回调用任务的优先级”。* /
}
	uxPriority = uxTaskPriorityGet(NULL);
	for(;;)
	{
		vPrintString( "Task 1 is running\r\n" ); 
	}
	/*将Task 2的优先级设置在Task 1的优先级之上,将导致Task 2立即开始运行(这样Task 2的优先级将高于创建的两个任务)。注意在对vTaskPrioritySet()的调用中使用了task 2的句柄(xTask2Handle)。清单35显示了如何获取句柄。* /
	vPrintString( "About to raise the Task 2 priority\r\n" ); 
	vTaskPrioritySet(xTask2Handle,(uxPriority + 1));
void vTask2( void *pvParameters ) 
{
	UBaseType_t uxPriority; 
	/* Task 1总是在此任务之前运行,因为Task 1的优先级更高。Task 1和Task 2都不会阻塞,所以它们总是处于Running或Ready状态。

查询任务运行时的优先级—传入NULL表示“返回调用任务的优先级”。* /
}
	 uxPriority = uxTaskPriorityGet( NULL ); 
	 for(;;)
	 {
	 	 vPrintString( "Task 2 is running\r\n" ); 
	 	 /*将该任务的优先级降低到初始值。传递NULL作为任务句柄意味着“更改调用任务的优先级”。将优先级设置为低于Task 1的优先级将导致Task 1立即重新开始运行—抢占此任务。* /
	 	 Task 1 to immediately start running again – pre-empting this task. */ 
        vPrintString( "About to lower the Task 2 priority\r\n" ); 
        vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
	 }

每个任务都可以查询和设置自己的优先级,而不需要使用有效的任务句柄,只需使用NULL即可。只有当任务希望引用自身以外的任务时,例如当任务1更改任务2的优先级时,才需要任务句柄。为了允许Task 1这样做,在创建Task 2时获取并保存Task 2句柄,如清单35中的注释所突出显示的那样。

TaskHandle_t xTask2Handle = NULL;
int main(void)
{
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 2, NULL);
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 2, &xTask2Handle);
	vTaskStartScheduler();  
	for(;;);
}

在这里插入图片描述

  1. Task1首先运行,因为它具有最高的优先级。
  2. 每当Task1将Task2的优先级设置为最高时,Task2就会运行。
  3. 当Task2将自己的优先级降低到低于Task1优先级时,Task1会再次运行,以此类推。
  4. Idle任务永远不会运行,因为两个应用程序任务总是能够运行,并且总是具有高于Idle优先级的优先级。

删除任务
**vTaskDelete()**函数可以删除自己或任何其它任务。只有在FreeRTOSConfig.h中将INCLUDE_vTaskDelete设置为1时,vTaskDelete() API函数才可用。

删除的任务不再存在,无法再次进入运行状态。
空闲任务负责释放分配给已删除任务的内存。
当删除任务时,只有内核本身分配给任务的内存才会自动释放。必须显式释放任务实现所分配的任何内存或其他资源。

void vTaskDelete(TaskHandle_t pxTaskToDelete);
  • pxTaskToDelete:要删除的任务的句柄。任务可以通过传递NULL来代替有效的任务句柄来删除自己。

例9.删除任务

  1. 任务1由main()创建,优先级为1。当它运行时,它以优先级2创建Task 2。Task 2现在是优先级最高的任务,因此它立即开始执行。
  2. Task 2除了删除自身之外什么都不做。它可以通过将NULL传递给vTaskDelete()来删除自己,但相反,为了演示目的,它使用自己的任务句柄。
  3. 当Task 2被删除后,Task 1再次成为优先级最高的任务,因此继续执行——这时它调用vTaskDelay()来阻塞一小段时间。
  4. Idle任务在task 1处于阻塞状态时执行,释放分配给现在已删除的task 2的内存。
  5. 当Task 1离开阻塞状态时,它再次成为最高优先级的Ready状态任务,因此抢占Idle任务。当它进入Running状态时,它再次创建Task 2,以此类推。
int main(void)
{
	xTaskCreate(vTask1,"Task 1", 1000, NULL, 1, NULL);
	vTaskStartScheduler();
	for(;;);
}
TaskHandle_t xTask2Handle = NULL;
void vTask1(void *pvParamater)
{
	const TickType_t xDelay100ms = pdMS_TO_TICKS(100UL);
	for(;;)
	{
		vPrintString( "Task 1 is running\r\n" ); 
		xTaskCreate(vTask2,"Task 2",1000,NULL,2,&xTask2Handle);
		vTaskDelay(xDelay100ms);
	}
}
void vTask2( void *pvParameters ) 
{
	vPrintString( "Task 2 is running and about to delete itself\r\n" ); 
	vTaskDelete( xTask2Handle ); 
}

在这里插入图片描述

  1. 运行Task1并创建Task2.Task2立即开始运行,因为它具有更高的优先级。
  2. Task 2除了删除自身外什么都不做,允许执行返回到Task 1。
  3. Task 1调用vTaskDelay(),允许空闲任务运行,直到延迟时间到期,然后重复整个序列。

调度算法

任务状态和事件的概述
在单核处理器上,在任何给定时间内只能有一个任务处于Running状态。

任务可以在阻塞状态等待事件发生,并在事件发生时自动移回就绪状态。临时事件发生在特定的时间,例如,当块时间到期时,通常用于实现周期性或超时行为。同步事件发生在任务或中断服务例程使用任务通知、队列、事件组或总多类型的信号量之一发送信息时。

配置调度算法
调度算法是决定哪个Ready状态的任务转换到Running状态的软件例程。
可以使用configUSE_PREEMPTION和configUSE_TIME_SLICING配置常量来更改算法。这两个常量都在FreeRTOSConfig.h中定义。
第三个配置常量configUSE_TICKLESS_IDLE也会影响调度算法,因为使用它会导致tick中断在很长一段时间内完全关闭。configUSE_TICKLESS_IDLE是一个高级选项,专门用于必须最小化能耗的应用程序。

在所有可能的配置中,FreeRTOS调度器将确保共享优先级的任务依次被选中进入运行状态。这种“轮流进行”的策略通常被称为“轮询调度”。轮询调度算法不保证相同优先级的任务之间的时间平等共享,只保证相同优先级的“就绪”状态的任务依次进入“运行”状态。

基于时间切片的优先抢占调度
表14所示的配置将FreeRTOS调度器设置为使用一种名为“固定优先级与时间切片抢占式调度”的调度算法这是大多数小型RTOS应用程序使用的调度算法。

FreeRTOSConfig.h设置,配置内核使用带时间切片的优先抢占调度。
在这里插入图片描述

  • Fixed Priority:被描述为“固定优先级”的调度算法不会改变分配给调度任务的优先级,但也不会组织任务本身改变自己的优先级或其它优先级的等级。
  • Pre-emptive:抢占式调度算法将立即“抢占”Running状态任务,如果一个优先级高于Running状态任务的任务进入Ready状态。抢占意味着不自觉地从Running状态移出并进入Ready状态,以允许不同的任务进入Running状态。
  • Time Slicing:时间切片用于在具有相同优先级的任务之间共享处理时间,即使任务没有显示地让步或进入Blocked状态。使用“时间切片”描述的调度算法将在每个时间切片结束时选择一个新任务进入运行状态,如果有其他Ready状态的任务具有与Running任务相同的优先级。一个时间片等于两个RTOS tick中断之间的时间。

在这里插入图片描述

  1. 空闲任务:空闲任务以最低优先级运行,因此每当高优先级任务进入Ready状态时就会被抢占。
  2. Task3:是一个事件驱动的任务,它的执行优先级相对较低,但高于Idle优先级。它的大部分时间都处于Blocked状态,等待事件发生。每当事件发生时,它都从Blocked状态过渡到Ready状态。所有FreeRTOS任务间通信机制(任务通知、队列、信号量、事件组等)都可以用这种方式向事件发送信号别解除阻塞任务。
  3. Task 2:是一个周期任务,其优先级高于Task 3,但低于Task 1。
  4. Task 1也是一个事件驱动的任务。它以最高的优先级执行,因此可以抢占系统中的任何其他任务。

如果configIDLE_SHOULD_YIELD被设置为0,那么Idle任务将在整个时间片内保持Running状态,除非它被更高优先级的任务抢占。
如果configIDLE_SHOULD_YIELD被设置为1,那么Idle任务将在其循环的每次迭代中让步(自愿放弃其分配的时间片的剩余部分),如果有其他Idle优先级任务处于Ready状态。

在这里插入图片描述

  1. Idle任务在其实现循环的一次迭代中运行,然后让步,允许调度器选择另一个任务。
  2. Task2运行时间片的剩余部分。
  3. Task1离开Blocked状态,抢占Task2
  4. Task1重新进入“Blocked”状态,允许Task2继续运行。

当configIDLE_SHOULD_YIELD设置为1时,Idle任务之后选择进入Running状态的任务不会执行整个时间片,而是执行Idle任务产生期间剩余的时间片。

优先抢占式调度(无时间切片)
FreeRTOSConfig.h设置将FreeRTOS调度器配置为使用优先级抢占调度而不进行时间切片。
在这里插入图片描述

  • configUSE_PREEMPTION:1
  • configUSE_TIME_SLICING:0

如果使用时间切片,并且有多个具有最高优先级的就绪状态任务可以运行,那么叼赌气将在每个RTOS tick中断期间选择一个新任务进入Running状态。

如果没有使用时间切片,那么调度程序将只选择一个新任务进入运行状态,当:

  • 优先级更高的任务处于就绪状态。
  • 运行中的任务进入“阻塞”或“挂起”状态。

不使用时间切片的任务上下文切换比使用时间切片时的任务上下文切换少。因此,关闭时间切片可以减少调度器的处理开销。然而,关闭时间切片也会导致具有相同优先级的任务接收到的处理时间相差很大。

在这里插入图片描述
假设configIDLE_SHOULD_YIELD设置为0.

合作调度
这本书的重点是抢占式调度,但FreeRTOS也可以是合作调度。
在这里插入图片描述

  • configUSE_PREEMPTION:设置为0.
  • configUSE_TIME_SLICING:Any Value。
    当使用合作调度时,只有当Running状态任务进入Blocked状态,或者Running状态任务通过调用taskYIELD()手动请求重新调度时,才会发生上下文切换。任务永远不会被抢占,因此不能使用时间切片。

在这里插入图片描述

  1. 当Task3写入队列时,Task2解除阻塞。
  2. Task1在中断写入信号量时解除阻塞。
  3. Task3调用taskYIELD(),允许Task1进入Running状态。
  4. Task1进入Blocked状态,Task2进入Running状态。

通常情况下,使用合作调度器比使用抢占式调度器更容易避免同时访问引起的问题:

  1. 当使用抢占式调度时,允许状态的任务可以在任何时候被抢占,包括它与另一个任务共享的资源处于不一致状态时。将资源置于不一致的状态会导致数据损坏。
  2. 当使用协同调度程序时,应用程序编写器将控制何时切换到另一个任务。因此,应用程序编写者可以确保在资源处于不一致状态时不会发生切换到另一个任务的情况。
  3. 在上面的UART示例中,应用程序编写器可以确保Task 1在其整个字符串被写入UART之前不会离开Running状态,这样做可以消除字符串被另一个任务的活动损坏的可能性。

使用抢占式调度器相比,使用协同调度器时系统的响应会更慢:

  1. 当使用抢占式调度器时,调度器将立即开始运行一个任务,直到该任务成为最高优先级的Ready状态任务。这在必须在规定时间内响应高优先级事件的实时系统中通常是必不可少的。
  2. 当使用协同调度程序时,直到Running状态任务进入Blocked状态或调用taskYIELD(),才执行切换到已成为最高优先级Ready状态任务的任务。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

饼干饼干圆又圆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值