任务函数
任务被实现为C函数。它们唯一特别的地方是它们的原型,它必须返回void并接受void指针参数。以下是函数原型。
c
void ATaskFunction( void *pvParameters );
每个任务本身都是一个小程序。它有一个入口点,通常会在无限循环中永远运行,不会退出。
FreeRTOS任务不得以任何方式从其实现函数返回------它们不得包含"return"语句,也不得在函数结束后执行。如果不再需要某个任务,则应将其明确删除。
单个任务函数定义可用于创建任意数量的任务------每个创建的任务都是一个单独的执行实例,具有自己的堆栈和任务本身中定义的任何自动(堆栈)变量的副本。
c
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 );
}
顶层任务状态
一个应用程序可以包含许多任务。如果运行应用程序的处理器包含一个内核,那么在任何给定时间只能执行一个任务。这意味着任务可以存在于两种状态之一,Running 和Not Running.。首先考虑这种简单的模型,但请记住,这是一种过度简化。Not Running状态实际上包含多个子状态。
当任务处于运行状态时,处理器正在执行任务的代码。当任务处于"未运行"状态时,该任务处于休眠状态,其状态已被保存,以便在调度程序决定下次进入"运行"状态后恢复执行。
当任务恢复执行时,它会从上次离开运行状态之前即将执行的指令开始执行。
从"未运行"状态转换为"运行"状态的任务被称为已"switched in"或"swapped in"。相反,从"运行"状态转换为"未运行"状态的任务被称为已"switched out"或"swapped out"。FreeRTOS调度程序是唯一可以切换任务的实体。
创建任务
xTaskCreate() API 函数
任务是使用FreeRTOS xTaskCreate()API函数创建的。
这可能是所有API函数中最复杂的一个,它是第一个遇到的,但任务必须首先掌握,因为它们是多任务系统中最基本的组件。
c
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
uint16_t usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask );
pcName:
任务的描述性名称。FreeRTOS不会以任何方式使用它。它纯粹是作为调试辅助工具而包含的。通过人类可读的名称识别任务比试图通过句柄识别任务要简单得多。
应用程序定义的常量configMAX_TASK_NAME_LEN定义了任务名称可以采用的最大长度,包括NULL终止符。提供超过此最大值的字符串将导致字符串被自动截断。
usStackDepth
每个任务都有自己的唯一堆栈,在创建任务时由内核分配给该任务。usStackDepth值告诉内核制作堆栈的大小。
该值指定堆栈可以容纳的字,而不是字节数。例如,如果堆栈的宽度为32位,并且usStackDepth作为100传入,则将分配400字节的堆栈空间(100*4字节)。堆栈深度乘以堆栈宽度不得超过uint16_t类型变量中可以包含的最大值。
Idle任务使用的堆栈大小由应用程序定义的常量configMINIMAL_ACK_size 1定义。在FreeRTOS演示应用程序中,为所使用的处理器架构分配给此常数的值是任何任务建议的最小值。如果你的任务使用了大量的堆栈空间,那么你必须分配一个更大的值。
没有简单的方法来确定任务所需的堆栈空间。
这可能被计算出来,但大多数用户只会分配他们认为合理的值,然后使用FreeRTOS提供的功能来确保分配的空间确实足够,并且RAM不会被不必要地浪费。堆栈溢出包含有关如何查询任务实际使用的最大堆栈空间的信息。
pvParameters
任务函数接受指向void(void*)的指针类型的参数。分配给pvParameters的值是传递给任务的值。
uxPriority
定义任务执行的优先级。优先级可以从最低优先级的0分配到最高优先级的(configMAX_Priorities--1)。configMAX_PRI ORITIES是一个用户定义的常数。
传递上述uxPriority值(configMAX_PRIORITIES--1)将导致分配给任务的优先级被默认地限制在最大合法值。
pxCreatedTask
pxCreatedTask可用于向正在创建的任务传递句柄。然后可以使用此句柄引用API调用中的任务,例如,更改任务优先级或删除任务。
如果你的应用程序不使用任务句柄,则可以将pxCreatedTask设置为NULL
Returned value
有两种可能的返回值:
1.pdPASS
这表示任务已成功创建。
2.pdFAIL
这表示任务尚未创建,因为FreeRTOS没有足够的堆内存来分配足够的RAM来保存任务数据结构和堆栈。
任务优先级
xTaskCreate()API函数的uxPriority参数为正在创建的任务分配初始优先级。启动调度程序后,可以使用vTaskPrioritySet()API函数更改优先级。
可用优先级的最大数量由FreeRTOSConfig.h中应用程序定义的configMAX_priorities编译时配置常数设置。低数字优先级值表示低优先级任务,优先级0是可能的最低优先级。因此,可用优先级的范围为0到(configMAX_priorities--1)。 任何数量的任务都可以共享相同的优先级,以确保最大的设计灵活性。
FreeRTOS调度程序可以使用两种方法之一来决定哪个任务将处于运行状态。configMAX_PRIORITIES可以设置的最大值取决于所使用的方法:
1.通用方法
通用方法用C实现,可以与所有FreeRTOS架构端口一起使用。
使用通用方法时,FreeRTOS不会限制configMAX_PRIORITIES可以设置的最大值。但是,始终建议将configMAX_PRIORITIES值保持在必要的最小值,因为其值越高,消耗的RAM就越多,最坏情况下的执行时间就越长。
如果在FreeRTOSConfig.h中将configUSE_PORT_OPTIMISED_TASK_SELECTION设置为0,或者如果configUSE_PORT _OPTIMISE_TASK_SELCTION未定义,或者如果通用方法是为正在使用的FreeRTOS端口提供的唯一方法,则将使用通用方法。
2.架构优化方法
架构优化方法使用少量汇编代码,比通用方法更快。configMAX_PRIORITIES设置不会影响最坏情况下的执行时间。
如果使用架构优化方法,则configMAX_PRIORITIES不能大于32。与通用方法一样,建议将configMAX_PRIORITIES保持在必要的最小值,因为其值越高,消耗的RAM就越多。
如果在FreeRTOSConfig.h中将configUSE_PORT_OPTIMISED_TASK_SELECTION设置为1,则将使用架构优化方法。
并非所有FreeRTOS端口都提供架构优化方法。
FreeRTOS调度程序将始终确保能够运行的最高优先级任务是选择进入运行状态的任务。如果能够运行多个具有相同优先级的任务,调度程序将依次将每个任务转换为运行状态和退出运行状态。
时间测量和滴答中断
调度算法,描述了一个名为"时间切片"的可选功能。到目前为止,所展示的示例中使用了时间切片,这是在它们产生的输出中观察到的行为。在这些示例中,两个任务都以相同的优先级创建,并且两个任务始终能够运行。因此,每个任务在一个"时间片"内执行,在时间片开始时进入运行状态,在时间段结束时退出运行状态。下图中,t1和t2之间的时间等于一个时间片。
为了能够选择要运行的下一个任务,调度器本身必须在每个时间片结束时执行。周期性中断,称为"滴答中断",用于此目的。时间片的长度实际上由滴答中断频率设置,该频率由FreeRTOSConfig.h中应用程序定义的configTICK_RATE_HZ编译时配置常数配置。例如,如果configTICK.RATE_HZ设置为100(HZ),则时间片将为10毫秒。两次滴答中断之间的时间称为"滴答周期"。
一个时间片等于一个刻度周期。
这如图12所示,其中最上面一行显示了调度器何时执行,细箭头显示了从任务到滴答中断,然后从滴答中断回到另一个任务的执行顺序。
configTICK_RATE_HZ的最佳值取决于正在开发的应用程序,尽管通常为100。
FreeRTOS API调用总是以tick周期的倍数指定时间,通常简称为"tick"。pdMS_TO_TICKS()宏将以毫秒为单位指定的时间转换为以刻度为单位的时间。可用的分辨率取决于定义的滴答频率,如果滴答频率高于1KHz(如果configTICK_RATE_HZ大于1000),则不能使用pdMS_TO_TICKS()。下面显示了如何使用pdMS_to_TICKS()将指定为200毫秒的时间转换为以滴答数指定的等效时间。
c
/* 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()宏以毫秒为单位指定时间,这样做可以确保应用程序中指定的时间在滴答频率更改时不会更改。
"滴答计数"值是自调度程序启动以来发生的滴答中断的总数,假设滴答计数没有溢出。用户应用程序在指定延迟期时不必考虑溢出,因为时间一致性由FreeRTOS内部管理。
优先级实验
调度程序将始终确保能够运行的最高优先级任务是选择进入运行状态的任务。在示例中,以相同的优先级创建了两个任务,因此这两个任务都依次进入和退出了"运行"状态。本例着眼于当示例2中创建的两个任务之一的优先级发生变化时会发生什么。这一次,将以优先级1创建第一个任务,以优先级2创建第二个任务。创建任务的代码如清单21所示。实现这两项任务的单一功能没有改变;它仍然只是定期打印一个字符串,使用null循环来创建延迟。
c
/* 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始终能够运行,因为它永远不必等待任何事情------它要么围绕空循环循环,要么打印到终端。
下图显示了执行顺序。
Not Running状态的扩展
到目前为止,创建的任务总是有处理要执行,从来没有等待过任何事情------因为它们永远不需要等待任何事情,所以它们总是能够进入运行状态。这种类型的"连续处理"任务的用处有限,因为它们只能以最低的优先级创建。如果它们以任何其他优先级运行,它们将完全阻止低优先级的任务运行。
为了使任务有用,必须将其重写为事件驱动。事件驱动的任务只有在触发它的事件发生后才能执行工作(处理),并且在该事件发生之前无法进入运行状态。调度程序始终选择能够运行的最高优先级任务。高优先级任务无法运行意味着调度程序无法选择它们,必须选择一个能够运行的低优先级任务。因此,使用事件驱动任务意味着可以以不同的优先级创建任务,而最高优先级的任务不会占用所有较低优先级的任务的处理时间。
阻塞态
等待事件的任务被称为处于"阻塞"状态,这是"Not Running"状态的子状态。
任务可以进入"阻塞"状态以等待两种不同类型的事件:
1.时间(时间相关)事件------该事件要么是延迟期到期,要么是到达的绝对时间。例如,一个任务可能会进入"阻塞"状态,等待10毫秒。
2.同步事件------事件源自另一个任务或中断。例如,任务可能会进入"阻塞"状态,等待数据到达队列。
同步事件涵盖了广泛的事件类型
FreeRTOS队列、二进制信号量、计数信号量、互斥量、递归互斥量、事件组和直接到任务通知都可以用于创建同步事件。
任务可能会在同步事件上阻塞超时,从而有效地同时阻塞这两种类型的事件。例如,一个任务可以选择等待最多10毫秒的数据到达队列。如果数据在10毫秒内到达,或者10毫秒后没有数据到达,则任务将离开"阻塞"状态。
挂起状态
"挂起"也是"未运行"的一个子状态。处于挂起状态的任务对计划程序不可用。进入起状态的唯一方法是通过调用vTaskSuspend()API函数,唯一方法是调用vTaskResume()或xTaskResumeFromISR()API函数。大多数应用程序不使用挂起状态。
就绪状态
处于非运行状态但未被阻塞或挂起的任务称为处于就绪状态。它们能够运行,因此"准备好"运行,但当前未处于运行状态。
完成状态转换图
图15扩展了之前过于简化的状态图,包括本节中描述的所有未运行子状态。到目前为止,示例中创建的任务尚未使用"阻塞"或"挂起"状态;它们仅在就绪状态和运行状态之间转换,如图15中的粗线所示。
使用"阻塞"状态创建延迟
到目前为止,示例中创建的所有任务都是"周期性的"------它们延迟了一段时间并打印出字符串,然后再次延迟,以此类推。延迟是使用空循环非常粗略地生成的------任务有效地轮询了一个递增的循环计数器,直到它达到一个固定值。例3清楚地表明了这种方法的缺点。较高优先级的任务在执行空循环时仍处于运行状态,在任何处理时间内"饥饿"较低优先级的任务。
任何形式的轮询都有其他几个缺点,其中最重要的是效率低下。在轮询过程中,任务实际上没有任何工作要做,但它仍然使用了最长的处理时间,因此浪费了处理器周期。示例4通过将轮询空循环替换为对vTaskDelay()API函数的调用来纠正这种行为,该函数的原型如清单22所示。新的任务定义如清单23所示。请注意,只有当在FreeRTOSConfig.h中将INCLUDE_vTaskDelay设置为1时,vTaskDelay()API函数才可用。
vTaskDelay()将调用任务置于阻塞状态,持续固定数量的滴答中断。
任务处于"阻塞"状态时不使用任何处理时间,因此任务仅在实际有工作要做时使用处理时间。
c
void vTaskDelay( TickType_t xTicksToDelay );
xTicksToDelay
调用任务在转换回就绪状态之前将保持在阻塞状态的滴答中断次数。
例如,如果一个名为vTaskDelay(100)的任务在分时计数为10000时,它将立即进入阻止状态,并保持在阻止状态,直到分时计数达到10100。
宏pdMS_TO_TICKS()可用于将以毫秒为单位指定的时间转换为以刻度为单位的时间。例如,调用vTaskDelay(pdMS_TO_TICKS(100))将导致调用任务在100毫秒内保持阻塞状态。
c
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 );
}
}
尽管这两个任务仍在以不同的优先级创建,但现在都将运行。示例4的输出如图16所示,证实了预期的行为。
图17所示的执行序列解释了为什么这两个任务都会运行,即使它们是以不同的优先级创建的。为了简单起见,省略了调度器本身的执行。
当调度程序启动时,会自动创建空闲任务,以确保始终至少有一个任务能够运行(至少一个任务处于就绪状态)。
只有这两个任务的实现发生了变化,而不是它们的功能。将图17与图12进行比较清楚地表明,此功能正以更有效的方式实现。
图12显示了当任务使用空循环创建延迟时的执行模式,因此始终能够运行,因此在它们之间使用了100%的可用处理器时间。图17显示了任务在整个延迟期内进入阻塞状态时的执行模式,因此只有当任务实际有需要执行的工作时(在这种情况下,只是打印出一条消息),才使用处理器时间
结果只使用了可用处理时间的一小部分。
在图17的场景中,每次任务离开阻塞状态时,它们都会在重新进入阻塞状态之前执行一小段时间。大多数时候,没有能够运行的应用程序任务(没有处于就绪状态的应用程序工作),因此,没有可以选择进入运行状态的应用程式工作。在这种情况下,空闲任务将运行。分配给空闲的处理时间量是系统中空闲处理能力的度量。使用RTOS可以通过允许应用程序完全由事件驱动来显著增加备用处理能力
图18中的粗线显示了示例4中任务执行的转换,每个任务现在都在返回就绪状态之前转换到阻塞状态。
vTaskDelayUntil()API函数
vTaskDelayUntil()类似于vTaskDelay()。正如刚才演示的那样,vTaskDelay()参数指定了在调用vTaskDelay()的任务和再次从阻塞状态转换出来的同一任务之间应该发生的滴答中断的数量。任务保持阻塞状态的时间长度由vTaskDelay()参数指定,但任务离开阻塞状态的时刻与调用vTaskDelay()的时刻有关。
vTaskDelayUntil()的参数指定了调用任务应从"阻塞"状态移动到"就绪"状态的确切滴答计数值。vTaskDelayUntil()是API函数,当需要固定的执行周期(您希望任务以固定的频率定期执行)时,应该使用该函数,因为调用任务被取消阻止的时间是绝对的,而不是相对于函数被调用的时间(如vTaskDelay())。
c
void vTaskDelayUntil( TickType_t * pxPreviousWakeTime, TickType_t xTimeIncrement );
pxPreviousWakeTime
此参数的命名是基于以下假设:vTaskDelayUntil()用于实现定期执行且频率固定的任务。在这种情况下,pxPreviousWakeTime保存任务最后一次离开阻塞状态(被"唤醒")的时间。此时间用作参考点,以计算任务下一次离开"阻塞"状态的时间。
pxPreviewWakeTime指向的变量在vTaskDelayUntil()函数中自动更新;它通常不会被应用程序代码修改,但在首次使用之前必须初始化为当前的滴答计数。
xTimeIncrement
此参数也是基于以下假设命名的:vTaskDelayUntil()用于实现一个定期执行且频率固定的任务,该频率由xTimeIncrement值设置。
xTimeIncrement在"tick"中指定。宏pdMS_TO_TICKS()可用于将以毫秒为单位指定的时间转换为以刻度为单位的时间。
示例5。将示例任务转换为使用vTaskDelayUntil()
示例4中创建的两个任务是周期性任务,但使用vTaskDelay()并不能保证它们运行的频率是固定的,因为任务离开Blocked状态的时间与它们调用vTaskDelay()的时间有关。将任务转换为使用vTaskDelayUntil()而不是vTaskDelay()可以解决这个潜在问题。
c
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 ) );
}
}
示例6 组合阻塞和非阻塞任务
前面的示例已经单独检查了轮询和阻塞任务的行为。
此示例通过演示执行来重新执行所声明的预期系统行为
当两个方案组合时的顺序如下。
1.创建了两个优先级为1的任务。这些只会连续打印出一个字符串。
这些任务从不进行任何可能导致其进入阻止状态的API函数调用,因此始终处于就绪或正在运行状态。这种性质的任务被称为"连续处理"任务,因为它们总是有工作要做(尽管在这种情况下是相当琐碎的工作)。清单26显示了连续处理任务的源代码。
2.然后以优先级2创建第三个任务,因此高于其他两个任务的优先级。第三个任务也只是打印出一个字符串,但这一次是周期性的,因此使用vTaskDelayUntil()API函数在每次打印迭代之间将自己置于Blocked状态。
周期性任务的源代码如清单27所示。
c
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 );
}
}
Listing 26. The continuous processing task used in Example 6
c
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 );
}
}
Listing 27. The periodic task used in Example 6
图19显示了示例6产生的输出,并解释了图20所示的执行序列所观察到的行为。
空闲任务和空闲任务钩子
示例4中创建的任务大部分时间处于"阻止"状态。在此状态下,它们无法运行,因此无法被调度程序选择。
必须始终至少有一个任务可以进入运行状态1。为了确保这一点,当调用vTaskStartScheduler()时,调度器会自动创建Idle任务。空闲任务只不过是坐在循环中,所以,就像最初的第一个例子中的任务一样,它总是能够运行的。
空闲任务具有尽可能低的优先级(优先级为零),以确保它永远不会阻止更高优先级的应用程序任务进入运行状态------尽管如果需要,没有什么可以阻止应用程序设计人员以空闲任务优先级创建任务,从而共享空闲任务优先级。中的configIDL_SHOULD_YIELD编译时配置常量
FreeRTOSConfig.h可用于防止Idle任务消耗处理时间,而这些时间本可以更有效地分配给应用程序任务。
以最低优先级运行可确保一旦更高优先级的任务进入就绪状态,空闲任务就会从运行状态转换出来。这可以在图17中的时间tn看到,其中Idle任务立即被换出,以允许task 2在task 2离开Blocked状态的瞬间执行。任务2已经抢占了空闲任务。抢占是自动发生的,并且不知道任务被抢占了。
注意:如果应用程序使用vTaskDelete()API函数,则Idle任务不会缺少处理时间是至关重要的。这是因为Idle任务负责在删除任务后清理内核资源
空闲任务钩函数
通过使用空闲挂钩(或空闲回调)函数,可以将特定于应用程序的功能直接添加到空闲任务中,该函数在空闲任务循环的每次迭代中由空闲任务自动调用一次。
Idle任务钩子的常见用途包括:
1 执行低优先级、后台或连续处理功能。
2 测量空闲处理能力的数量。(只有当所有更高优先级的应用程序任务都没有工作要执行时,空闲任务才会运行;因此,测量分配给空闲任务的处理时间量可以清楚地表明有多少处理时间是空闲的。)
3 将处理器置于低功耗模式,在没有应用程序处理时提供一种简单而自动的节能方法。
空闲任务钩子函数实现的局限性
空闲任务挂钩函数必须遵守以下规则。
1.Idle任务挂钩函数不得尝试阻塞或挂起。
注意:以任何方式阻止空闲任务都可能导致没有任务可以进入运行状态的情况。
2.如果应用程序使用vTaskDelete()API函数,则Idle任务挂钩必须始终在合理的时间段内返回到其调用方。这是因为Idle任务负责在删除任务后清理内核资源。
如果空闲任务永久保留在idle挂钩函数中,则无法进行此清理。
空闲任务挂钩函数必须具有以下所示的名称和原型。
c
void vApplicationIdleHook( void );
示例7 定义空闲任务挂钩函数
示例4中阻止vTaskDelay()API调用的使用在idle任务执行时创建了大量空闲时间,因为两个应用程序任务都处于阻止状态。示例7通过添加idle挂钩函数来利用这个空闲时间,其源代码如下所示。
c
/* Declare a variable that will be incremented by the hook function. */
volatile uint32_t ulIdleCycleCount = 0UL;
/* Idle hook functions MUST be called vApplicationIdleHook(), take no parameters,
and return void. */
void vApplicationIdleHook( void )
{
/* This hook function does nothing but increment a counter. */
ulIdleCycleCount++;
}
必须在FreeRTOSConfig.h中将configUSE_IDLE_HOOK设置为1,才能调用空闲挂钩函数。
实现所创建任务的函数稍作修改,以打印出ulIdleCycleCount值,如清单30所示。
c
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 AND the number of times ulIdleCycleCount
has been incremented. */
vPrintStringAndNumber( pcTaskName, ulIdleCycleCount );
/* Delay for a period of 250 milliseconds. */
vTaskDelay( xDelay250ms );
}
}
输出如图21所示。它显示,在应用程序任务的每次迭代之间,空闲任务挂钩函数被调用了大约400万次(迭代次数取决于执行演示的硬件的速度)。
更改任务的优先级
vTaskPrioritySet()API函数
vTaskPrioritySet()API函数可用于在启动调度程序后更改任何任务的优先级。请注意,只有在FreeRTOSConfig.h中将INCLUDE_vTaskPrioritySet()API函数设置为1时,该函数才可用。
c
void vTaskPrioritySet( TaskHandle_t pxTask, UBaseType_t uxNewPriority );
pxTask
优先级被修改的任务的句柄(主任务)
任务可以通过传递NULL代替有效的任务句柄来更改自己的优先级。
uxNewPriority
设置主题任务的优先级。这会自动限制为最大可用优先级(configMAX_PRIORITIES--1),其中configMAX_PRI ORITIES是FreeRTOSConfig.h头文件中设置的编译时常数
uxTaskPriorityGet()API函数
uxTaskPriorityGet()API函数可用于查询任务的优先级。请注意,只有在FreeRTOSConfig.h中将INCLUDE_uxTaskPriorityGet设置为1时,uxTaskPriorityGet()API函数才可用。
c
UBaseType_t uxTaskPriorityGet( TaskHandle_t pxTask );
pxTask
正在查询优先级的任务的句柄(主任务)
任务可以通过传递NULL代替有效的任务句柄来查询自己的优先级
返回值 当前分配给正在查询的任务的优先级。
改变任务优先级
调度程序将始终选择最高就绪状态的任务作为进入运行状态的任务。示例8通过使用vTaskPrioritySet()API函数来更改两个任务相对于彼此的优先级来演示这一点。
示例8创建了两个具有不同优先级的任务。两个任务都不进行任何可能导致其进入阻止状态的API函数调用,因此两个任务始终处于就绪状态或运行状态。因此,具有最高相对优先级的任务将始终是调度器选择的处于运行状态的任务。
行为如下:
1.任务1是以最高优先级创建的,因此保证首先运行。
任务1在将任务2的优先级提升到高于其自身优先级之前打印出几个字符串。
2.任务2一旦具有最高相对优先级就开始运行(进入运行态)。一次只能有一个任务处于运行状态,因此当任务2处于运行状态时,任务1处于就绪状态。
3.任务2在将自己的优先级设置回低于任务1的优先级之前打印出一条消息。
4.任务2将其优先级调低意味着任务1再次成为最高优先级的任务,因此任务1重新进入运行状态,迫使任务2回到就绪状态。
c
void vTask1( void *pvParameters )
{
UBaseType_t uxPriority;
/* This task will always run before Task 2 as it is created with the higher
priority. Neither Task 1 nor Task 2 ever block so both will always be in
either the Running or the Ready state.
Query the priority at which this task is running - passing in NULL means
"return the calling task's priority". */
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
/* Print out the name of this task. */
vPrintString( "Task 1 is running\r\n" );
/* Setting the Task 2 priority above the Task 1 priority will cause
Task 2 to immediately start running (as then Task 2 will have the higher
priority of the two created tasks). Note the use of the handle to task
2 (xTask2Handle) in the call to vTaskPrioritySet(). Listing 35 shows how
the handle was obtained. */
vPrintString( "About to raise the Task 2 priority\r\n" );
vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );
/* Task 1 will only run when it has a priority higher than Task 2.
Therefore, for this task to reach this point, Task 2 must already have
executed and set its priority back down to below the priority of this
task. */
}
}
void vTask2( void *pvParameters )
{
UBaseType_t uxPriority;
/* Task 1 will always run before this task as Task 1 is created with the
higher priority. Neither Task 1 nor Task 2 ever block so will always be
in either the Running or the Ready state.
Query the priority at which this task is running - passing in NULL means
"return the calling task's priority". */
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
/* For this task to reach this point Task 1 must have already run and
set the priority of this task higher than its own.
Print out the name of this task. */
vPrintString( "Task 2 is running\r\n" );
/* Set the priority of this task back down to its original value.
Passing in NULL as the task handle means "change the priority of the
calling task". Setting the priority below that of Task 1 will cause
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的优先级时。为了允许任务1执行此操作,在创建任务2时获取并保存任务2句柄,如下注释所示。
c
/* Declare a variable that is used to hold the handle of Task 2. */
TaskHandle_t xTask2Handle = NULL;
int main( void )
{
/* Create the first task at priority 2. The task parameter is not used
and set to NULL. The task handle is also not used so is also set to NULL. */
xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
/* The task is created at priority 2 ______^. */
/* Create the second task at priority 1 - which is lower than the priority
given to Task 1. Again the task parameter is not used so is set to NULL -
BUT this time the task handle is required so the address of xTask2Handle
is passed in the last parameter. */
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, &xTask2Handle );
/* The task handle is the last parameter _____^^^^^^^^^^^^^ */
/* 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 there
was insufficient heap memory available for the idle task to be created.
Chapter 2 provides more information on heap memory management. */
for( ;; );
}
图22展示了示例8任务的执行顺序,结果输出如图23所示。
删除任务
vTaskDelete()API函数
任务可以使用vTaskDelete()API函数删除自身或任何其他任务。请注意,只有当在FreeRTOSConfig.h中将INCLUDE_vTaskDelete设置为1时,vTaskDelete()API函数才可用。
已删除的任务不再存在,无法再次进入运行状态。
空闲任务有责任释放分配给已删除任务的内存。因此,重要的是,使用vTaskDelete()API函数的应用程序不要完全耗尽空闲任务的所有处理时间。
注意:删除任务时,只有内核本身分配给任务的内存才会自动释放。必须显式释放任务实现分配的任何内存或其他资源。
c
void vTaskDelete( TaskHandle_t pxTaskToDelete );
pxTaskToDelete
要删除的任务(主任务)的句柄
任务可以通过传递NULL代替有效的任务句柄来删除自己
示例9。删除任务
这是一个非常简单的示例,其行为如下。
1 任务1由main()创建,优先级为1。当它运行时,它会创建优先级为2的任务2。
任务2现在是最高优先级的任务,因此它立即开始执行。
2.任务2除了删除自身外什么也不做。它可以通过向vTaskDelete()传递NULL来删除自己,但出于演示目的,它使用自己的任务句柄。
3.删除任务2后,任务1再次成为最高优先级的任务,因此继续执行------此时它调用vTaskDelay()在短时间内阻塞。
4.空闲任务在任务1处于阻塞状态时执行,并释放分配给现已删除的任务2的内存。
5.当任务1离开阻塞状态时,它再次成为最高优先级的就绪状态任务,因此优先于空闲任务。当它进入运行状态时,它会再次创建任务2,然后继续。
c
int main( void )
{
/* Create the first task at priority 1. The task parameter is not used
so is set to NULL. The task handle is also not used so likewise is set
to NULL. */
xTaskCreate( vTask1, "Task 1", 1000, NULL, 1, NULL );
/* The task is created at priority 1 ______^. */
/* Start the scheduler so the task starts executing. */
vTaskStartScheduler();
/* main() should never reach here as the scheduler has been started. */
for( ;; );
}
TaskHandle_t xTask2Handle = NULL;
void vTask1( void *pvParameters )
{
const TickType_t xDelay100ms = pdMS_TO_TICKS( 100UL );
for( ;; )
{
/* Print out the name of this task. */
vPrintString( "Task 1 is running\r\n" );
/* Create task 2 at a higher priority. Again the task parameter is not
used so is set to NULL - BUT this time the task handle is required so
the address of xTask2Handle is passed as the last parameter. */
xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
/* The task handle is the last parameter _____^^^^^^^^^^^^^ */
/* Task 2 has/had the higher priority, so for Task 1 to reach here Task 2
must have already executed and deleted itself. Delay for 100
milliseconds. */
vTaskDelay( xDelay100ms );
}
}
void vTask2( void *pvParameters )
{
/* Task 2 does nothing but delete itself. To do this it could call vTaskDelete()
using NULL as the parameter, but instead, and purely for demonstration purposes,
it calls vTaskDelete() passing its own task handle. */
vPrintString( "Task 2 is running and about to delete itself\r\n" );
vTaskDelete( xTask2Handle );
}
调度算法
任务状态和事件
实际运行的任务(使用处理时间)处于"正在运行"状态。在单核处理器上,在任何给定时间只能有一个任务处于运行状态。
实际上没有运行,但既没有处于阻塞状态也没有处于挂起状态的任务处于就绪状态。处于就绪状态的任务可供调度程序选择,作为进入运行状态的任务。调度程序将始终选择优先级最高的就绪状态任务进入运行状态。
任务可以在"阻塞"状态下等待事件,并在事件发生时自动移回"就绪"状态。时间事件发生在特定时间,例如块时间到期时,通常用于实现周期性或超时行为。
当任务或中断服务例程使用任务通知、队列、事件组或多种信号量之一发送信息时,会发生同步事件。它们通常用于表示异步活动,例如到达外围设备的数据。
在所有可能的配置中,FreeRTOS调度器将确保选择共享优先级的任务依次进入运行状态。这种"轮流"策略通常被称为"循环调度"。循环调度算法不能保证等优先级任务之间的时间平均分配,只能保证等优先级的就绪状态任务依次进入运行状态。
基于时间切片的优先级抢占调度
表14所示的配置将FreeRTOS调度器设置为使用名为"带时间切片的固定优先级抢占调度"的调度算法,这是大多数小型RTOS应用程序使用的调度算法。
描述调度策略的术语的解释
Fixed Priority
被称为"固定优先级"的调度算法不会改变分配给正在调度的任务的优先级,也不会阻止任务本身改变其自身或其他任务的优先级。
Pre-emptive
如果优先级高于运行状态任务的任务进入就绪状态,抢占的调度算法将立即"抢占"运行状态任务。被抢占意味着非自愿地(没有明确屈服或阻塞)从运行状态移动到就绪状态,以允许不同的任务进入运行状态。
Time Slicing
时间切片用于在具有相同优先级的任务之间共享处理时间,即使任务没有明确屈服或进入"阻塞"状态。
如果存在与"运行"任务具有相同优先级的其他就绪状态任务,则描述为使用"时间片"的调度算法将在每个时间片结束时选择一个新任务进入"运行"状态。时间片等于两个RTOS滴答中断之间的时间。
图26和图27展示了当使用具有时间切片算法的固定优先级抢占式调度时,任务是如何调度的。图26显示了当应用程序中的所有任务都具有唯一优先级时,选择任务进入运行状态的顺序。图27显示了选择任务以进入
应用程序中两个任务共享优先级时的运行状态。
参见图26:
1.空闲任务
空闲任务以最低优先级运行,因此每当更高优先级的任务进入就绪状态时,例如在时间t3、t5和t9,它都会被抢占。
2.任务3
任务3是一个事件驱动的任务,其执行优先级相对较低,但高于空闲优先级。它大部分时间都处于阻塞状态,等待其感兴趣的事件,每次事件发生时都会从阻塞状态转换到就绪状态。所有FreeRTOS任务间通信机制(任务通知、队列、信号量、事件组等)都可以用于以这种方式发送事件信号和取消阻止任务。
事件发生在时间t3和t5,也发生在t9和t12之间。在时间t3和t5发生的事件被立即处理,因为在这些时间,任务3是能够运行的最高优先级任务。在时间t9和t12之间发生的事件直到t12才被处理,因为在此之前,更高优先级的任务Task 1和Task 2仍在执行。只有在时间t12,任务1和任务2都处于阻止状态,使任务3成为最高优先级的就绪状态任务。
3 任务2
任务2是一个周期性任务,其执行优先级高于任务3,但低于任务1。任务的周期间隔意味着任务2希望在时间t1、t6和t9执行。
在时间t6,任务3处于运行状态,但任务2具有较高的相对优先级,因此优先于任务3并立即开始执行。任务2完成其处理并在时间t7重新进入阻塞状态,此时任务3可以重新进入运行状态以完成其处理。任务3本身在时间t8处阻塞。
4 任务1
任务1也是一个事件驱动的任务。它以最高优先级执行,因此可以优先于系统中的任何其他任务。显示的唯一任务1事件发生在时间t10,此时任务1先于任务2。只有在任务1在时间t11重新进入阻止状态后,任务2才能完成其处理。
参见图27:
1.空闲任务和任务2
Idle任务和task 2都是连续处理任务,并且都具有优先级0(尽可能低的优先级)。当没有更高优先级的任务能够运行时,调度器仅将处理时间分配给优先级为0的任务,并通过时间切片共享分配给优先级0的任务的时间。新的时间片从每个滴答中断开始,在图27中为时间t1、t2、t3、t4、t5、t8、t9、t10和t11。
空闲任务和任务2依次进入运行状态,这可能导致两个任务在同一时间片的一部分处于运行状态,就像在时间t5和时间t8之间发生的那样。
2.任务1
任务1的优先级高于空闲优先级。任务1是一个事件驱动的任务,它的大部分时间都处于阻塞状态,等待其感兴趣的事件,每次事件发生时都会从阻塞状态转换到就绪状态。
感兴趣的事件发生在时间t6,因此在t6,任务1成为能够运行的最高优先级任务,因此任务1在时间片的中途抢占空闲任务。事件的处理在时间t7完成,此时任务1重新进入阻止状态。
图27显示了空闲任务与应用程序编写者创建的任务共享处理时间。如果应用程序编写者创建的Idle优先级任务有工作要做,但Idle任务没有工作,那么将那么多处理时间分配给Idle任务可能是不可取的。configIDLE_SHOULD_YIELD编译时配置常量可用于更改Idle任务的调度方式:
如果configIDL_SHOULD_YIELD设置为0,则Idle任务将在其整个时间片内保持运行状态,除非它被更高优先级的任务抢占。
如果configIDL_SHOULD_YIELD设置为1,那么如果有其他空闲优先级任务处于就绪状态,空闲任务将在其循环的每次迭代中屈服(自愿放弃其分配的时间片的剩余部分)。
图27所示的执行模式是当configIDL_SHOULD_YIELD设置为0时观察到的。图28所示的执行模式是当configIDL_SHOULD_YIELD设置为1时在相同场景中观察到的。
图28还显示,当configIDLE_SHOULD_YIELD设置为1时,在空闲任务之后选择进入运行状态的任务不会在整个时间片内执行,而是在空闲任务产生的时间片的剩余时间内执行。
抢占调度(无时间切片)
没有时间切片的优先级抢占式调度保持了与前一节所述相同的任务选择和抢占算法,但没有使用时间切片在同等优先级的任务之间共享处理时间。
表16显示了FreeRTOSConfig.h设置,该设置将FreeRTOS调度器配置为使用优先级抢占式调度,而不进行时间切片。
如图27所示,如果使用时间切片,并且有多个处于最高优先级的就绪状态任务可以运行,那么调度器将在每个RTOS滴答中断(标记时间切片结束的滴答中断)期间选择一个新任务进入运行状态。如果不使用时间切片,那么调度程序将仅在以下任一情况下选择一个新任务进入运行状态:
优先级较高的任务进入就绪状态。
处于"运行"状态的任务将进入"阻塞"或"挂起"状态。
与使用时间切片时相比,不使用时间切片的任务上下文切换更少。因此,关闭时间切片可以减少调度器的处理开销。然而,关闭时间切片也可能导致具有相同优先级的任务接收到截然不同的处理时间,如图29所示。因此,运行没有时间切片的调度程序被认为是一种高级技术,只应由有经验的用户使用。
参考图29,假设configIDLE_SHOULD_YIELD设置为0:
1 tick中断
滴答中断发生在时间t1、t2、t3、t4、t5、t8、t11、t12和t13。
2.任务1
任务1是一个高优先级的事件驱动任务,它的大部分时间都处于阻塞状态,等待其感兴趣的事件。每次事件发生时,任务1都会从阻塞状态转换为就绪状态(随后,由于它是最高优先级的就绪状态任务,因此会转换为运行状态)。图29显示了任务1在时间t6和t7之间处理事件,然后在时间t9和t10之间再次处理事件。
3空闲任务和任务2
Idle任务和task 2都是连续处理任务,并且都具有优先级0(空闲优先级)。连续处理任务不会进入"阻塞"状态。
未使用时间切片,因此处于运行状态的空闲优先级任务将保持在运行状态,直到它被更高优先级的任务1抢占。
在图29中,Idle任务在时间t1开始运行,并保持在running状态,直到在时间t6被任务1抢占------这是它进入running状态后的四个完整的滴答周期。
任务2在时间t7开始运行,此时任务1重新进入阻止状态以等待另一个事件。任务2保持在运行状态,直到它在时间t9也被任务1抢占------这是在它进入运行状态后不到一个刻度周期。
在时间t10,空闲任务重新进入运行状态,尽管已经接收到的处理时间是任务2的四倍多。
协作调度
本书侧重于抢占的调度,但FreeRTOS也可以使用协作调度。配置FreeRTOS调度器以使用协作调度的FreeRTOSConfig.h设置如表17所示。
当使用协作调度程序时,只有当运行状态任务进入阻塞状态,或者运行状态任务通过调用taskYIELD()显式产生(手动请求重新调度)时,才会发生上下文切换。任务永远不会被抢占,因此不能使用时间切片。
图30展示了协作调度器的行为。图30中的水平虚线显示了任务何时处于就绪状态。
参见图30:
1.任务1
任务1具有最高优先级。它从Blocked状态开始,等待信号量。
在时间t3,中断发出信号,导致任务1离开阻塞状态并进入就绪状态。
在时间t3,任务1是最高优先级的就绪状态任务,如果抢占调度程序已使用,任务1将成为运行状态任务。然而,当使用协作调度器时,任务1一直处于就绪状态,直到时间t4------此时运行状态任务调用taskYIELD()。
2.任务2
任务2的优先级介于任务1和任务3之间。它在阻塞状态下启动,等待任务3在时间t2发送给它的消息。
在时间t2,任务2是最高优先级的就绪状态任务,如果使用了抢占的调度器,任务2将成为运行状态任务。但是,在使用协作调度程序时,任务2保持在就绪状态,直到运行状态任务进入调用taskYIELD()的阻塞状态。
运行状态任务在时间t4调用taskYIELD(),但到那时,任务1是优先级最高的就绪状态任务,因此任务2实际上不会成为运行状态任务,直到任务1在时间t5重新进入阻塞状态。
在时间t6,任务2重新进入阻塞状态以等待下一条消息,此时任务3再次成为最高优先级的就绪状态任务。
在多任务应用程序中,应用程序编写者必须注意一个资源不能同时被多个任务访问,因为同时访问可能会损坏资源。例如,考虑以下场景,其中正在访问的资源是UART(串行端口)。两个任务是向UART写入字符串;任务1正在编写"abcdefghijklmnop",任务2正在编写"123456789":
1.任务1处于运行状态并开始写入其字符串。它将"abcdefg"写入UART,但在写入任何其他字符之前离开运行状态。
2.任务2进入运行状态,并在离开运行状态之前将"123456789"写入UART。
3.任务1重新进入运行状态,并将其字符串的剩余字符写入UART。
在这种情况下,实际写入UART的是"abcdefg123456789hijklmnop"。任务1写入的字符串没有按预期以完整的顺序写入UART,而是被损坏了,因为任务2写入UART的字符串出现在其中。
当使用协作调度器时,通常比使用抢占的调度器更容易避免同时访问造成的问题:
当使用抢占的调度程序时,运行状态任务可以在任何时候被抢占,包括当它与另一个任务共享的资源处于不一致的状态时。正如UART示例所示,将资源保持在不一致的状态可能会导致数据损坏。
当使用协作调度器时,应用程序编写器控制何时可以切换到另一个任务。因此,应用程序编写器可以确保在资源处于不一致状态时不会切换到另一个任务。
在上述UART示例中,应用程序编写器可以确保任务1在其整个字符串被写入UART之前不会离开运行状态,这样做可以消除字符串被另一个任务的激活损坏的可能性。
如图30所示,当使用协作调度器时,系统的响应速度将低于使用抢占调度器时的响应速度:
1 当使用抢占调度器时,当任务成为最高优先级的就绪状态任务时,调度器将立即开始运行任务。这在必须在规定时间内对高优先级事件做出响应的实时系统中通常是必不可少的。
2 当使用协作调度程序时,在运行状态任务进入阻塞状态或调用taskYIELD()之前,不会执行切换到已成为最高优先级就绪状态任务的操作。