FreeRTOS 教程指南 学习笔记 第三章 任务管理(一)
本章中介绍的概念对于理解如何使用FreeRTOS以及FreeRTOS应用程序的行为非常基础。因此,这是书中最详细且最重要的一章,用心学习本章对于其他RTOS系统的理解也非常有帮助。
一、简介
本章旨在让读者很好地理解:
- FreeRTOS如何为应用程序中的每个任务分配处理时间。
- FreeRTOS如何选择在任何给定时间应该执行的任务。
- 每个任务的相对优先级如何影响系统行为。
- 任务可以存在的状态。
读者还应该能很好地理解:
- 如何实现任务。
- 如何创建一个或多个任务的实例。
- 如何使用任务参数。
- 如何更改已经创建的任务的优先级。
- 如何删除一个任务。
- 如何使用任务实现周期性处理(软件计时器将在后面的一章中讨论)。
- 空闲任务的执行时间以及如何使用它。
二、任务函数
任务作为C函数实现。它们唯一的特别之处是它们的原型,它必须返回void并接受一个void指针参数。示例11展示了这个原型。
//Listing 11. The task function prototype
void ATaskFunction( void *pvParameters );
每个任务本身都是一个小程序。它有一个入口点,通常会在一个无限的循环中永远运行,并且不会退出。一个典型任务的结构如示例12所示。
不允许FreeRTOS任务以任何方式从其实现函数返回——它们不能包含‘erturn”语句,也不允许在函数结束后执行。如果不再需要一个任务,则应该显式地删除它。在示例12中也演示了这一点。
单个任务函数定义可用于创建任意数量的任务——每个创建的任务都是一个单独的执行实例,每个实例都有自己的栈空间,任务中定义的每个变量,在任务的每个实例重都将具有它自己的空间拷贝。
//Listing 12. The structure of a typical task function
void ATaskFunction( void *pvParameters )
{
/* Variables can be declared just as per a normal function. Each instance of a task created using this example function will have its own copy of the lVariableExample variable. This would not be true if the variable was declared static – in which case only one copy of the variable would exist, and this copy would be shared by each created instance of the task. (The prefixes added to variable names are described in section 1.5, Data Types and Coding Style Guide.) */
int32_t lVariableExample = 0;
/* A task will normally be implemented as an infinite loop. */
for( ;; )
{
/* The code to implement the task functionality will go here. */
}
/* Should the task implementation ever break out of the above loop, then the task must be deleted before reaching the end of its implementing function. The NULL parameter passed to the vTaskDelete() API function indicates that the task to be deleted is the calling (this) task. The convention used to name API functions is described in section 0, Projects that use a FreeRTOS version older than V9.0.0 must build one of the heap_n.c files. From FreeRTOS V9.0.0 a heap_n.c file is only required if configSUPPORT_DYNAMIC_ALLOCATION is set to 1 in FreeRTOSConfig.h or if configSUPPORT_DYNAMIC_ALLOCATION is left undefined. Refer to Chapter 2, Heap Memory Management, for more information.Data Types and Coding Style Guide. */
vTaskDelete( NULL );
}
三、任务状态
一个应用程序可以包含许多任务。如果运行应用程序的处理器只有一个核心,那么在任何给定的时间只能执行一个任务。这意味着任务可以处于两种状态之一,运行和不运行。首先考虑这个简单的模型,但请记住,它过于简化。本章后面显示“未运行”状态实际上包含许多子状态。
当一个任务处于“正在运行”的状态时,处理器正在执行该任务的代码。当任务处于“不运行”状态时,该任务处于休眠状态,其状态已保存,以便下次调度程序决定进入运行状态时恢复执行。当任务恢复执行时,它从上次离开运行状态之前将要执行的指令执行。
任务从“未正在运行”状态转换到“正在运行”状态可以成为‘switched in’ or ‘swapped in’。相反,任务从“运行”状态转换到“不运行”状态可以被称为‘switched out’ or ‘swapped out’。FreeRTOS调度程序是唯一可以切换输入和输出任务的实体。
四、创建任务
The xTaskCreate() API Function
使用FreeRTOS的xTaskCreate()API函数创建任务。这可能是所有API函数中最复杂的,所以不幸的是,它是第一次遇到的,但必须首先掌握任务,因为它们是多任务处理系统中最基本的组件。本书附带的所有示例都使用了xTaskCreate()函数,所以有许多示例需要参考。
//Listing 13. The xTaskCreate() API function prototype
/*参数pvTaskCode:任务是一个从不退出的C函数,因此,通常被实现为一个无限循环。pvTaskCode参数只是一个指向实现该任务的函数的指针(实际上,它只是该函数的名称)。*/
/*参数pcName:该任务的描述性名称。FreeRTOS没有以任何方式使用它。它纯粹作为调试辅助工具。通过一个人类可读的名称来识别一个任务比试图通过其句柄来识别它要简单得多。应用程序定义的常量configMAX_TASK_NAME_LEN定义了任务名称可以获得的最大长度——包括NULL终止符。提供超过此最大值的字符串将导致该字符串被静默地截断。*/
/*参数usStackDepth:每个任务都有自己的唯一栈空间,在创建任务时由内核分配给任务。usStackDepth深度值告诉内核使堆栈有多大。该值指定堆栈可以保存的words,而不是byte。例如,如果栈是32位宽,并且usStackDepth为100,那么将分配400字节的栈空间(100*4字节)。栈深度乘以栈宽度不能超过uint16_t类型的变量中可以包含的最大值。空闲任务所使用的栈的大小由应用程序定义的常数configMINIMAL_STACK_SIZE1来定义。在FreeRTOS演示应用程序中,为正在使用的处理器体系结构分配给这个常量的值是为任何任务推荐的最小值。如果您的任务使用了大量的栈空间,则必须分配一个更大的值。没有一种简单的方法来确定任务所需的堆栈空间。这是可以计算的,但大多数用户会简单地分配他们认为合理的值,然后使用FreeRTOS提供的特性来确保分配的空间确实足够,并且RAM不会被不必要地浪费。第12.3节,*/
/*参数pvParameters:指针类型void(void*)的参数。分配给pvParameters的值是传递到任务中的值。本书中的一些例子演示了如何使用该参数*/
/*参数uxPriority:定义要执行的任务的优先级。优先级可以从最低优先级0分配到最高优先级(configMAX_PRIORITIES-1)。configMAX_PRIORITIES是在第3.5节中描述的一个用户定义的常量。传递一个高在上面的uxPriority值(configMAX_PRIORITIES-1)将导致分配给任务的优先级被静默地限制为最大的合法值。*/
/*参数pxCreatedTask:pxCreatedTask是被函数传出的句柄指针型参数,它会传出一个正在被创建的任务的句柄。这个句柄可以用于引用API调用中的任务,例如,更改任务优先级或删除任务。如果应用程序不需要该句柄,那么pxCreatedTask可以被设置为NULL。*/
/*返回值BaseType_t:有两个可能的返回值:1.pdPASS。这表示该任务已成功创建。 2. pdFAIL表明任务没有创建。*/
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
uint16_t usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask );
Example 1. Creating tasks
下面的例子显示了如何创建任务:
void vTask1( void *pvParameters )
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile uint32_t ul; /* volatile to ensure ul is not optimized away. */
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* Delay for a period. */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/* This loop is just a very crude delay implementation. There is nothing to do in here. Later examples will replace this crude loop with a proper delay/sleep function. */
}
}
}
void vTask2( void *pvParameters )
{
const char *pcTaskName = "Task 2 is running\r\n";
volatile uint32_t ul; /* volatile to ensure ul is not optimized away. */
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* Delay for a period. */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/* This loop is just a very crude delay implementation. There is nothing to do in here. Later examples will replace this crude loop with a proper delay/sleep function. */
}
}
}
int main( void )
{
/* Create one of the two tasks. Note that a real application should check the return value of the xTaskCreate() call to ensure the task was created successfully. */
xTaskCreate( vTask1, /* Pointer to the function that implements the task. */
"Task 1",/* Text name for the task. This is to facilitate debugging only. */
1000, /* Stack depth - small microcontrollers will use much less stack than this. */
NULL, /* This example does not use the task parameter. */
1, /* This task will run at priority 1. */
NULL ); /* This example does not use the task handle. */
/* Create the other task in exactly the same way and at the same priority. */
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
/* Start the scheduler so the tasks start executing. */
vTaskStartScheduler();
/* If all is well then main() will never reach here as the scheduler will now be running the tasks. If main() does reach here then it is likely that there was insufficient heap memory available for the idle task to be created. Chapter 2 provides more information on heap memory management. */
for( ;; );
}
图10显示了似乎同时执行的两个任务;但是,由于这两个任务都在同一个处理器核心上执行,因此情况并非如此。实际上,这两个任务都在快速进入和退出运行状态。这两个任务以相同的优先级运行,因此在同一处理器核心上共享时间。它们的实际执行模式如图11所示。图11底部的箭头显示了从时间t1开始的时间流逝。彩色的线表示在每个时间点上正在执行哪个任务——例如,任务1在时间t1和时间t2之间正在执行。任何时候只能有一个任务处于运行状态。因此,当一个任务进入“运行”状态(任务切换)时,另一个任务进入“不运行”状态(任务切换)。
示例1在启动调度程序之前,从main()函数中创建了这两个任务。也可以从另一个任务中创建一个任务。例如,任务2可以从任务1中创建,如下示例所示:
void vTask1( void *pvParameters )
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile uint32_t ul; /* volatile to ensure ul is not optimized away. */
/* If this task code is executing then the scheduler must already have been started. Create the other task before entering the infinite loop. */
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
for( ;; )
{
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* Delay for a period. */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/* This loop is just a very crude delay implementation. There is nothing to do in here. Later examples will replace this crude loop with a proper delay/sleep function. */
}
}
}
Example 2. Using the task parameter
在示例1中创建的两个任务几乎是相同的,它们之间唯一的区别是它们打印出的文本字符串。可以通过创建单个任务的两个实例来删除此重复。然后,可以使用任务参数将它应该打印出来的字符串传递给每个任务实例。如下示例包含了示例2所使用的单个任务函数(vTaskFunction)的代码。这个单一函数取代了示例1中使用的两个任务函数(vTask1和vTask2)。注意如何将任务参数转换为char*以获得任务应该打印出的字符串。
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
volatile uint32_t ul; /* volatile to ensure ul is not optimized away. */
/* The string to print out is passed in via the parameter. Cast this to a character pointer. */
pcTaskName = ( char * ) pvParameters;
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* Delay for a period. */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/* This loop is just a very crude delay implementation. There is nothing to do in here. Later exercises will replace this crude loop with a proper delay/sleep function. */
}
}
}
/* Define the strings that will be passed in as the task parameters. These are defined const and not on the stack to ensure they remain valid when the tasks are executing. */
static const char *pcTextForTask1 = "Task 1 is running\r\n";
static const char *pcTextForTask2 = "Task 2 is running\r\n";
int main( void )
{
/* Create one of the two tasks. */
xTaskCreate( vTaskFunction, /* Pointer to the function that implements the task. */
"Task 1", /* Text name for the task. This is to facilitate debugging only. */
1000, /* Stack depth - small microcontrollers will use much less stack than this. */
(void*)pcTextForTask1, /* Pass the text to be printed into the task using the task parameter. */
1, /* This task will run at priority 1. */
NULL ); /* The task handle is not used in this example. */
/* Create the other task in exactly the same way. Note this time that multiple tasks are being created from the SAME task implementation (vTaskFunction). Only the value passed in the parameter is different. Two instances of the same task are being created. */
xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 1, NULL );
/* Start the scheduler so the tasks start executing. */
vTaskStartScheduler();
/* If all is well then main() will never reach here as the scheduler will now be running the tasks. If main() does reach here then it is likely that there was insufficient heap memory available for the idle task to be created. Chapter 2 provides more information on heap memory management. */
for( ;; );
}
尽管现在只有一个任务实现(vTaskFunction),但也可以创建已定义任务的多个实例。每个创建的实例都将在FreeRTOS调度程序的控制下独立执行。清单19显示了如何使用xTaskCreate()函数的pv参数参数来将文本字符串传递到任务中。
五、任务优先级
xTaskCreate()API函数的uxPriority参数为正在创建的任务分配一个初始优先级。可以在调度程序启动后使用vTaskPrioritySet()API函数更改优先级。
可用的最大优先级数量由FreeRTOSConfig.h中的应用程序定义的configMAX_PRIORITIES编译时配置常数设置。低数字优先级值表示低优先级任务,优先级0是可能的最低优先级。因此,可用优先级的范围为0到(configMAX_PRIORITIES-1)。任意数量的任务都可以共享相同的优先级——确保最大限度的设计灵活性。
FreeRTOS调度程序可以使用两种方法之一来决定哪个任务将处于运行状态。configMAX_PRIORITIES可以设置到的最大值取决于所使用的方法:
- 通用方法
通用方法用C语言实现,可以与所有FreeRTOS体系结构分支一起使用。
当使用通用方法时,FreeRTOS不限制configMAX_PRIORITIES可以设置到的最大值。但是,最好将configMAX_PRIORITIES值保持在必要的最小值,因为其值越高,消耗的RAM就越多,最坏情况的执行时间就越长。
如果在FreeRTOSConfig.h中configUSE_PORT_OPTIMISED_TASK_SELECTION设置为0,或者如果未定义configUSE_PORT_OPTIMISED_TASK_SELECTION,或者通用方法是为使用的FreeRTOS分支提供的唯一方法,则将使用通用方法。 - 体系结构优化方法
体系结构优化方法使用了少量的汇编代码,并且比通用方法更快。configMAX_PRIORITIES设置不会影响最坏情况下的执行时间。
如果使用了体系结构优化的方法,则configMAX_PRIORITIES不能大于32。与通用方法一样,建议将configMAX_PRIORITIES保持在必要的最小值,因为它的值越高,所消耗的RAM就越多。
如果在FreeRTOSConfig.h中将configUSE_PORT_OPTIMISED_TASK_SELECTION设置为1,将使用体系结构优化方法。并非所有的FreeRTOS分支都提供了一种架构优化的方法。
FreeRTOS调度程序将始终确保能够运行的最高优先级任务是选择要进入运行状态的任务。当能够运行多个具有相同优先级的任务时,调度程序将依次将每个任务转换为和退出“正在运行”状态。
六、时间测量和计时器中断
两个任务都以相同的优先级创建,并且两个任务总是能够运行。每个任务在一个“时间片”执行,在时间片开始时进入“运行”状态,在时间片结束时退出“运行”状态。在图11中,t1和t2之间的时间等于单个时间片。
为了能够选择下一个要运行的任务,调度程序本身必须在每个时间片的末尾执行。一个称为“Tick Interrupt”的周期性中断被用于形成时间片。时间片的长度Tick Interrupt频率设置,Tick Interrupt频率由FreeRTOSConfig.h中的应用程序定义的configTICK_RATE_HZ进行配置。例如,如果configTICK_RATE_HZ被设置为100(Hz),那么时间片将为10毫秒。两次Tick Interrupt之间的时间被称为“Tick Period”。一个时间切片等于一个滴答周期。
图12显示调度程序本身在执行顺序中的执行位置。configTICK_RATE_HZ的最佳值取决于正在开发的应用程序,尽管100的值是典型的。
FreeRTOS API调用总是以多个Tick Interrupt来指定时间,这通常被简单地称为“Tick”。pdMS_TO_TICKS()宏将以毫秒为单位指定的时间转换为以刻度数指定的时间。可用的分辨率取决于所定义的滴答频率,如果滴答频率高于1 KHz(如果configTICK_RATE_HZ大于1000),则不能使用pdMS_TO_TICKS()。下列示例显示了如何使用pdMS_TO_TICKS()将指定为200毫秒的时间转换为在刻度中指定的等效时间。
//Listing 20. Using the pdMS_TO_TICKS() macro to convert 200 milliseconds into an equivalent time in tick periods
/* pdMS_TO_TICKS() takes a time in milliseconds as its only parameter, and evaluates to the equivalent time in tick periods. This example shows xTimeInTicks being set to the number of tick periods that are equivalent to 200 milliseconds. */
TickType_t xTimeInTicks = pdMS_TO_TICKS( 200 );
注意:不建议直接在应用程序中指定时间,而是使用pdMS_TO_TICKS()宏以毫秒为单位指定时间,这样做,确保在滴答频率改变时,应用程序中指定的时间不会改变。
“tick count”值是自调度程序启动以来发生的tick interrupt的总数,假设bit count没有溢出。用户应用程序在指定延迟周期时不必考虑溢出,因为时间一致性是由FreeRTOS在内部管理的。
第3.12节,调度算法,描述了影响调度程序何时选择要运行的新任务,以及何时执行tick interrupt的配置常量。
Example 3. Experimenting with priorities
调度程序将始终确保选择进入运行状态的最高优先级任务能进入运行。在我们到目前为止的示例中,有两个任务以相同的优先级创建了,因此它们都依次进入和退出了运行状态。本示例查看了当改变示例2中创建的两个任务之一的优先级时,会发生什么。这一次,第一个任务将在优先级1处创建,第二个任务将在优先级2处创建。创建这些任务的代码如清单21所示。实现这两个任务的单个函数没有改变;它仍然只是定期周期性地打印出一个字符串,使用一个空循环来创建一个延迟。
/* Define the strings that will be passed in as the task parameters. These are defined const and not on the stack to ensure they remain valid when the tasks are executing. */
static const char *pcTextForTask1 = "Task 1 is running\r\n";
static const char *pcTextForTask2 = "Task 2 is running\r\n";
int main( void )
{
/* Create the first task at priority 1. The priority is the second to last parameter. */
xTaskCreate( vTaskFunction, "Task 1", 1000, (void*)pcTextForTask1, 1, NULL );
/* Create the second task at priority 2, which is higher than a priority of 1. The priority is the second to last parameter. */
xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 2, NULL );
/* Start the scheduler so the tasks start executing. */
vTaskStartScheduler();
/* Will not reach here. */
return 0;
}
产生的输出如图13所示。调度程序将始终选择能够运行的最高优先级的任务。任务2的优先级高于任务1,并且始终能够运行;因此,任务2是唯一一个进入“正在运行”状态的任务。由于任务1从未进入“正在运行”状态,因此它从未打印出其字符串。任务1被任务2称为“缺乏”处理时间。任务2总是能够运行,因为它不需要等待任何事情——它要么绕着空循环循环,要么打印到终端。
七、扩展“未运行”状态
到目前为止,创建的任务总是被处理执行,从来没有需要等待任何事情——因为它们不必等待任何事情,它们总是能够进入运行状态。这种类型的“连续处理”任务的用途有限,因为它们只能在非常最低的优先级下创建。如果它们以任何其他优先级运行,它们将完全阻止优先级较低的任务运行。为了使任务有用,必须将它们重写为事件驱动的。事件驱动的任务只有在触发它的事件发生后才可以执行工作(处理),并且不能在该事件发生之前进入运行状态。调度程序总是选择能够运行的最高优先级的任务。高优先级任务无法运行意味着调度程序无法选择它们,而必须选择一个能够运行的低优先级任务。因此,使用事件驱动的任务意味着可以在不同的优先级上创建任务,而不会使最高优先级的任务占用处理时间中的所有较低优先级的任务。
被阻止的状态
等待事件的任务被认为是处于“已阻止”状态,这是“未运行”状态的子状态。任务可以进入阻塞状态,以等待两种不同类型的事件:
- 时间(与时间相关)事件—事件是延迟溢出或一段绝对时间到达。例如,一个任务可能会进入阻塞状态,等待10毫秒通过。
- 同步事件——事件来自另一个任务或中断。例如,任务可能进入阻止状态,等待数据到达队列。同步事件涵盖了广泛的事件类型。
FreeRTOS队列、二进制信号、计数信号、互斥、递归互斥、事件组和任务通知 都可以用于创建同步事件。所有这些特点都将在这本书的未来章节中介绍。
任务可以用超时阻塞同步事件,有效地同时阻塞两种类型的事件。例如,一个任务可能会选择为数据到达队列而等待10毫秒时间。数据到达队列的最长时间为10毫秒。如果任何一个数据在10毫秒内到达,或者在10毫秒内没有数据到达,则该任务将离开阻塞状态。
挂起状态
“挂起”也是不运行的子状态。处于“已挂起”状态下的任务对调度程序不可用。进入挂起状态的唯一方法是通过调用vTaskSuspend()API函数,退出挂起状态的唯一方法是通过调用vTaskResume()或vTaskResumeFromISR()API函数。大多数应用程序都不使用“已挂起”状态。
就绪状态
处于“未运行”状态但未被阻止或挂起的“就绪状态”任务称为处于“就绪”状态。它们能够运行,因此“准备好”运行,但目前没有处于运行状态。
完整的状态转换图:
Example 4. Using the Blocked state to create a delay
到目前为止,在示例中创建的所有任务都是“周期性的”——它们延迟了一段时间,并打印出字符串,然后再次延迟,以此类推。延迟非常粗暴的使用一个计数循环生成。示例3清楚地说明了该方法的缺点。较高优先级的任务在执行空循环时仍然处于运行状态,任何优先级低的任务处于时间“饥饿”。
任何形式的循环都有其他几个缺点,尤其是它的效率低下。在循环期间,该任务确实没有任何工作要做,但它仍然使用最大的处理时间,因此浪费了处理器周期。示例4通过用对vTaskDelay()API函数的调用替换空循环来纠正这种行为,它的原型如清单22所示。新的任务定义如清单23所示。注意,只有在FreeRTOSConfig.h中将INCLUDE_vTaskDelay设置为1时,vTaskDelay()API函数才可用。
vTaskDelay()将调用任务置于阻塞状态,以获得固定数量的滴答中断。任务处于“阻止”状态时不使用任何处理器时间,因此该任务仅在有实际工作需要完成时才使用处理时间。
//Listing 22. The vTaskDelay() API function prototype
void vTaskDelay( TickType_t xTicksToDelay );
/*参数xTicksToDelay :在切换回已就绪状态之前,调用任务将处于“阻止”状态的中断数。例如,如果一个名为vTaskDelay(100)的任务,当tick count为10,000时,它将立即进入阻塞状态,并保持阻塞状态,直到tick cout达到10,100。宏pdMS_TO_TICKS()可用于将以毫秒为单位指定的时间转换为在刻度中指定的时间。例如,调用vTaskDelay(pdMS_TO_TICKS(100))将导致调用任务保持在阻塞状态100毫秒。*/
//Listing 23. The source code for the example task after the null loop delay has been replaced by a call to vTaskDelay()
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
const TickType_t xDelay250ms = pdMS_TO_TICKS( 250 );
/* The string to print out is passed in via the parameter. Cast this to a character pointer. */
pcTaskName = ( char * ) pvParameters;
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* Delay for a period. This time a call to vTaskDelay() is used which places the task into the Blocked state until the delay period has expired. The parameter takes a time specified in ‘ticks’, and the pdMS_TO_TICKS() macro is used (where the xDelay250ms constant is declared) to convert 250 milliseconds into an equivalent time in ticks. */
vTaskDelay( xDelay250ms );
}
}
图17中所示的执行序列解释了为什么这两个任务都会运行,即使它们是在不同的优先级下创建的。为了简单起见,我们省略了调度程序本身的执行。空闲任务将在调度程序启动时自动创建,以确保始终至少有一个任务能够运行(至少有一个任务处于就绪状态)。
只有这两个任务的实现发生了变化,而不是它们的功能。将图17和图12进行比较,可以清楚地表明,该功能正在以更有效的方式实现。
图12显示了任务使用空循环来创建延迟时的执行模式——因此总是能够运行,并因此在它们之间使用100%的可用处理器时间。图17显示了任务在整个延迟期间进入阻塞状态时的执行模式,因此只有当处理器实际有需要执行的工作时才使用处理器时间(在这种情况下只是打印一条消息),因此只使用可用处理时间的一小部分。
在图17的场景中,每次任务离开阻塞状态时,它们在重新进入阻塞状态之前只消耗了少量滴答周期去执行任务。大多数情况下,没有能够运行的应用程序任务(没有处于就绪状态的应用程序任务),因此,也没有可以选择来进入运行状态的应用程序任务。在这种情况下,空闲的任务将会运行。分配给空闲时间的处理时间是系统中备用处理能力的度量。仅仅通过允许应用程序完全被事件驱动,使用RTOS就可以显著增加备用处理容量。
图18中的粗体线显示了示例4中任务执行的转换,每个任务在返回到就绪状态之前通过阻塞状态转换。
The vTaskDelayUntil() API Function
vTaskDelayUntil()类似于()vTaskDelay()。如刚才演示的,vTaskDelay()参数指定了任务在调用vTaskDelay()和该任务再一次过渡到阻塞状态之间应该发生的滴答中断的次数。任务保持在阻塞状态的时间长度由vTaskDelay()参数指定,但是任务离开阻塞状态的时间与调用vTaskDelay()的时间点有关。
vTaskDelayUntil()的参数指定调用任务从阻塞状态移动到就绪状态的确切tick count。vTaskDelayUntil()API函数在当你需要一个固定的执行周期时使用(任务有固定的执行频率),它的调用任务的时间是绝对的,而不是相对于函数被调用的时间(与vTaskDelay())。
//Listing 24. vTaskDelayUntil() API function prototype
void vTaskDelayUntil( TickType_t * pxPreviousWakeTime, TickType_t xTimeIncrement );
/*参数pxPreviousWakeTime:这个参数的命名是基于这样一个假设,vTaskDelayUntil()被用于实现一个定期执行且频率固定的任务。在这种情况下,pxPreviousWakeTime保存了任务最后一次离开阻塞状态(被“唤醒”)的时间。此时间被用作参考点,以计算任务下一次离开阻塞状态的时间。pxPreviousWakeTime所指向的变量在函数中自动更新;它通常不会被应用程序代码修改,但必须初始化为当前的滴答计数,然后第一次使用。清单25演示了如何执行初始化。*/
/*参数xTimeIncrement :这个参数的命名也是基于以下假设,vTaskDelayUntil()被用于实现一个定期执行且频率固定的任务——该频率由xTimeIncrement值设置。xTimeIncrement以“tick”计量。宏pdMS_TO_TICKS()可用于将以毫秒为单位指定的时间转换为在刻度中指定的时间。*/
Example 5. Converting the example tasks to use vTaskDelayUntil()
示例4中创建的两个任务是周期性任务,但是使用vTaskDelay()并不能保证它们运行的频率是固定的,因为任务离开阻塞状态的时间取决于它们调用vTaskDelay()的时间。将任务转换为使用vTaskDelayUntil()而不是vTaskDelay()解决了这个潜在的问题。
//Listing 25. The implementation of the example task using vTaskDelayUntil()
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
TickType_t xLastWakeTime;
/* The string to print out is passed in via the parameter. Cast this to a character pointer. */
pcTaskName = ( char * ) pvParameters;
/* The xLastWakeTime variable needs to be initialized with the current tick count. Note that this is the only time the variable is written to explicitly. After this xLastWakeTime is automatically updated within vTaskDelayUntil(). */
xLastWakeTime = xTaskGetTickCount();
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* This task should execute every 250 milliseconds exactly. As per the vTaskDelay() function, time is measured in ticks, and the pdMS_TO_TICKS() macro is used to convert milliseconds into ticks. xLastWakeTime is automatically updated within vTaskDelayUntil(), so is not explicitly updated by the task. */
vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS( 250 ) );
}
}
Example 6. Combining blocking and non-blocking tasks
以前的例子已经单独检查了轮询和阻塞任务的行为。这个示例通过演示两种方案组合时的执行序列来加强预期系统行为,如下所示。
- 创建了两个优先级为1任务。他们只是不断地打印字符串。
这些任务永远不会进行任何可能导致它们进入阻塞状态的API函数调用,因此总是处于准备状态或运行状态。这种性质的任务被称为“连续处理”任务,因为它们总是有工作要做(尽管在这种情况下是相当琐碎的工作)。连续处理任务的源代码如清单26所示。 - 然后在优先级2处创建第三个任务,因此高于其他两个任务的优先级。第三个任务也只是打印出字符串,但这次是周期性的,所以它使用vTaskDelayUntil()API函数在两次打印之间让它自己进入阻塞状态。
void vContinuousProcessingTask( void *pvParameters )
{
char *pcTaskName;
/* The string to print out is passed in via the parameter. Cast this to a character pointer. */
pcTaskName = ( char * ) pvParameters;
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. This task just does this repeatedly without ever blocking or delaying. */
vPrintString( pcTaskName );
}
}
void vPeriodicTask( void *pvParameters )
{
TickType_t xLastWakeTime;
const TickType_t xDelay3ms = pdMS_TO_TICKS( 3 );
/* The xLastWakeTime variable needs to be initialized with the current tick count. Note that this is the only time the variable is explicitly written to. After this xLastWakeTime is managed automatically by the vTaskDelayUntil() API function. */
xLastWakeTime = xTaskGetTickCount();
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintString( "Periodic task is running\r\n" );
/* The task should execute every 3 milliseconds exactly – see the declaration of xDelay3ms in this function. */
vTaskDelayUntil( &xLastWakeTime, xDelay3ms );
}
}
图19显示了示例6所产生的输出,并解释了图20中所示的执行序列所给出的观察到的行为。