任务函数
任务是由 C 语言函数实现的。唯一特别的只是任务的函数原型,其必须返回 void,而且带有一个 void 指针参数。
void ATaskFunction( void *pvParameters );
每个任务本身就是一个小程序。它有一个入口点,通常会在无限循环中永远运行,不会退出。 FreeRTOS 任务不允许以任何方式从实现函数中返回——它们绝不能有一条”return”语句,也不能执行到函数末尾。如果一个任务不再需要,可以显式地将其删除。 一个任务函数可以用来创建若干个任务——创建出的任务均是独立的执行实例,拥有属于自己的栈空间,以及属于自己的自动变量(栈变量),即任务函数本身定义的变量。
最简单的任务函数
void ATaskFunction( void *pvParameters )
{
int iVariableExample = 0;
for( ;; )
{
printf("iVariableExample = %d\n",iVariableExample);
vTaskDelay(1000);
}
顶层任务状态
一个应用程序可以由许多任务组成。如果运行应用程序的处理器包含一个内核,那么在任何给定时间只能执行一个任务。这意味着一个任务可以存在于两种状态之一,运行和不运行。首先考虑的是这种简单化的模型,但请记住,这是一种过度简化。在后面的部分中,我们将看到“未运行”状态实际上包含许多子状态。 当任务处于运行状态时,处理器正在执行任务的代码。当任务处于非运行状态时,该任务处于休眠状态,其状态已保存,以便在计划程序决定下次进入运行状态时恢复执行。当任务恢复执行时,它会从上次离开运行状态之前要执行的指令开始执行。
对于单内核的芯片而言,任一任务要么处于运行态,要么处于非运行态。但同一时刻只能有一个任务处于运行态。这也是为什么这个图中①画的任务框是多个叠起来的,而②所示的任务只有一个框的原因。
任务从非运行态转移到运行态被称为”切换入或切入(switched in)”或”交换(swapped in)”。相反,任务从运行态转移到非运行态被称为”切换出或切(switched out)”或”交换出(swapped out)”。FreeRTOS 的调度器是能让任务切入切出的唯一实体。
创建任务
创建任务使用 FreeRTOS 的 API 函数 xTaskCreate()。
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
uint16_t usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *);
pvTaskCode——任务只是永不退出的 C 函数,实现常通常是一个死循环。参数pvTaskCode 只一个指向任务的实现函数的指针(效果上仅仅是函数名)。
pcName——具有描述性的任务名。这个参数不会被 FreeRTOS 使用。应用程序可以通过定义常量config_MAX_TASK_NAME_LEN 来定义任务名的最大长度——包括’\0’结束符。
usStackDepth——当任务创建时,内核会分为每个任务分配属于任务自己的唯一状态。usStackDepth 值用于告诉内核为它分配多大的栈空间。如果堆栈是32位宽的,usStackDepth作为100传入,那么将分配400字节的堆栈空间(100*4字节)。应用程序通过定义常量 configMINIMAL_STACK_SIZE 来决定空闲任务任用的栈空间大小。
pvParameters——任务函数接受一个指向 void 的指针(void*)。pvParameters 的值即是传递到任务中的值。
uxPriority——指定任务执行的优先级。优先级的取值范围可以从最低优先级 0 到最高优先级(configMAX_PRIORITIES – 1)。configMAX_PRIORITIES 是一个由用户定义的常量。优先级号并没有上限(除了受限于采用的数据类型和系统的有效内存空间),但最好使用实际需要的最小数值以避免内存浪费。
pxCreatedTask——用于传出任务的句柄。这个句柄将在 API 调用中对该创建出来的任务进行引用,比如改变任务优先级,或者删除任务。如果应用程序中不会用到这个任务的句柄,则 pxCreatedTask 可以被设为 NULL。
有两个可能的返回值:
- pdPASS 表明任务创建成功。
- pdFAIL 这表明任务尚未创建,因为FreeRTOS没有足够的堆内存来分配足够的RAM来保存任务数据结构和堆栈。
例1 创建任务
本例演示了创建并启动两个任务的必要步骤。这两个任务只是周期性地打印输出字符串,采用原始的空循环方式来产生周期延迟。两者在创建时指定了相同的优先级,并且在实现上除输出的字符串外完全一样
示例1
main.c
#include <stdio.h>
#include <stdlib.h>
#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#define mainDELAY_LOOP_COUNT ( 0xffffff )
void start_task( void * pvParameters );
void task1_task( void * pvParameters );
void task2_task( void * pvParameters );
static const char *pcTextForTask1 = "Task 1 is running\r\n";
static const char *pcTextForTask2 = "Task 2 is running\t\n";
int main( void )
{
xTaskCreate( task1_task,
"Task 1",
1000,
(void*)pcTextForTask1,
1,
NULL );
xTaskCreate( task2_task, "Task 2", 1000, (void*)pcTextForTask2, 1, NULL );
vTaskStartScheduler();
return 0;
}
void task1_task( void *pvParameters )
{
char *pcTaskName;
volatile unsigned long ul;
pcTaskName = ( char * ) pvParameters;
for( ;; )
{
printf("%s\n", pcTaskName );
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
}
}
}
void task2_task( void *pvParameters )
{
char *pcTaskName;
volatile unsigned long ul;
pcTaskName = ( char * ) pvParameters;
for( ;; )
{
printf("%s\n", pcTaskName );
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
}
}
}
void vMainQueueSendPassed( void )
{
return;
}
void vApplicationIdleHook( void )
{
return;
}
运行结果 显示了两个似乎同时执行的任务;然而,由于这两个任务都在同一个处理器内核上执行,因此情况并非如此。实际上,这两个任务都在快速进入和退出运行状态。这两个任务以相同的优先级运行,因此在同一处理器内核上共享时间。
Task 2 is running
Task 1 is running
Task 1 is running
Task 2 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
执行流程图 图中底部的箭头显示了从时间t1开始的时间流逝。彩色线条显示每个时间点正在执行的任务,例如,任务1在时间t1和时间t2之间执行。
示例2
示例1在启动调度程序之前,从main()中创建了这两个任务。也可以从另一个任务中创建任务。例如,任务2可以从任务1中创建
核心代码
void vTask1( void *pvParameters )
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile unsigned long ul;
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
for( ;; )
{
printf("%s\n", pcTaskName );
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
}
}
}
例 2. 使用任务参数
例1中创建的两个任务几乎相同,它们之间的唯一区别是打印出的文本字符串。相反,可以通过创建单个任务实现的两个实例来消除这种重复。然后,可以使用task参数向每个任务传递它应该打印出来的字符串。此单一函数替换示例1中使用的两个任务函数(vTask1和vTask2)。注意任务参数是如何转换为char*的,以获得任务应该打印出来的字符串。
main.c
#include <stdio.h>
#include <stdlib.h>
#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#define mainDELAY_LOOP_COUNT ( 0xffffff )
void vTaskFunction( void *pvParameters );
static const char *pcTextForTask1 = "Task 1 is running\r\n";
static const char *pcTextForTask2 = "Task 2 is running\t\n";
int main( void )
{
xTaskCreate( vTaskFunction,
"Task 1",
1000,
(void*)pcTextForTask1,
1,
NULL );
xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 1, NULL );
vTaskStartScheduler();
return 0;
}
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
volatile unsigned long ul;
pcTaskName = ( char * ) pvParameters;
for( ;; )
{
printf("%s\n", pcTaskName );
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
}
}
}
void vMainQueueSendPassed( void )
{
return;
}
void vApplicationIdleHook( void )
{
return;
}
任务优先级
xTaskCreate()API函数的uxPriority参数为正在创建的任务分配初始优先级。在调度程序启动后,可以使用vTaskPrioritySet()API函数更改优先级。
void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority )
此函数用来对任务的优先级进行修改,要使用此函数的话宏 INCLUDE_vTaskPrioritySet应该定义为 1
应用程序在文件 FreeRTOSConfig.h 中设定的编译时配置常量configMAX_PRIORITIES 的值,即是最多可具有的优先级数目。FreeRTOS 本身并没有限定这个常量的最大值,但这个值越大,则内核花销的内存空间就越多。所以总是建议将此常量设为能够用到的最小值。
UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask )
此函数用来获取指定任务的优先级,要使用此函数的话宏 INCLUDE_uxTaskPriorityGet 应该定义为 1
低优先级号表示任务的优先级低,优先级号 0 表示最低优先级。有效的优先级号范围从 0 到(configMAX_PRIORITES – 1)。
FreeRTOS调度器可以使用两种方法之一来决定哪个任务将处于Running状态。configMAX_PRIORITIES可以设置的最大值取决于所使用的方法: 1.泛型方法 当使用此种方法,FreeRTOS并不限制configMAX_PRIORITIES可以设置的最大值。但是,它总是明智自动的保持configMAX_PRIORITIES取值不大,因为它的值越高,将消耗更多的RAM,最坏情况执行时间越长 如果在FreeRTOSConfig.h头文件中将 configUSE_P ORT_OPTIMISED_TASK_SELECTION 设置为0,那么将使用泛型方法,或者configUSE_PORT_OPTIMISED_TASK_SELECTION未在头文件中定义,或者泛型方法是使用中的FreeRTOS端口提供的唯一方法 2.结构优化方法 架构优化方法使用了少量的汇编代码,其比Generic Method 方法更快。如果使用架构优化的方法,那么configMAX_PRIORITIES不能大于32,与通用方法一样,建议将configMAX_PRIORITIES保持在必要的最小值,因为它的值越高,就越多RAM将被消耗。 如果在FreeRTOSConfig.h中将configUSE_PORT_OPTIMISED_TASK_SELECTION设置为1,那么将使用架构优化的方法
FreeRTOS调度器将始终确保能够运行的最高优先级任务是选择进入Running状态的任务。当多个具有相同优先级的任务可以运行时,调度器将依次将每个任务转换为Running状态。
时间测量和滴答中断
当两个任务都以相同的优先级创建,并且这两个任务总是能够运行。因此,在一个“时间片”中执行的每个任务,在时间片开始时进入“Running”状态,在时间片结束时退出“Running”状态。 为了能够选择要运行的下一个任务,调度器本身必须在每个时间片的末尾执行。一个周期性的中断,称为“滴答中断”,就是为了这个目的。时间片的长度由tick中断频率有效地设置,该频率由FreeRTOSConfig.h中应用程序定义的configTICK_RATE_HZ编译时配置常量配置。例如,如果configTICK_RATE_HZ设置为100 (Hz),那么时间片将是10毫秒。两个周期之间的时间称为“周期”。一个时间切片等于一个周期。 其中最上面的一行显示了调度程序何时执行,细箭头显示了从一个任务到滴答中断,然后从滴答中断返回到另一个任务的执行顺序。 需要说明的是,FreeRTOS API 函数调用中指定的时间总是以心跳中断为单位(通常的提法为心跳”ticks”)。常量 portTICK_RATE_MS 用于将以心跳为单位的时间值转化为以毫秒为单位的时间值。有效精度依赖于系统心跳频率。
TickType_t xTimeInTicks = pdMS_TO_TICKS( 200 );
例3 优先级实验
核心代码
int main( void )
{
xTaskCreate( vTaskFunction,
"Task 1",
1000,
(void*)pcTextForTask1,
1,
NULL );
xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 2, NULL );
vTaskStartScheduler();
return 0;
}
调度器将始终选择能够运行的优先级最高的任务。Task 2的优先级高于Task 1,并且始终能够运行;因此,Task 2是唯一一个进入Running状态的任务。当Task 1永远不会进入Running状态时,它永远不会打印出它的字符串。Task 1被Task 2称为处理时间“匮乏”。任务2总是能够运行,因为它从不需要等待任何东西——它要么在一个空循环中循环,要么打印到终端。
扩展“不运行”状态
到目前为止,创建的任务始终需要执行处理,并且从来没有等待过任何东西,因为它们从来没有等待过任何东西,它们始终能够进入运行状态。这种类型的“连续处理”任务用处有限,因为它们只能以最低优先级创建。如果它们以任何其他优先级运行,它们将阻止优先级较低的任务运行。为了使任务有用,必须将它们重新编写为事件驱动的。事件驱动的任务只有在触发它的事件发生后才能执行工作(处理),并且在该事件发生之前无法进入运行状态。调度程序始终选择能够运行的最高优先级任务。无法运行的高优先级任务意味着计划程序无法选择它们,而必须选择能够运行的低优先级任务。因此,使用事件驱动任务意味着可以以不同的优先级创建任务,而最高优先级的任务不会占用所有低优先级任务的处理时间。 为了使任务变得有用,必须重写它们以使其成为事件驱动的。事件驱动的任务只有在触发它的事件发生之后才需要执行工作(处理),并且在事件发生之前不能进入Running状态。使用事件驱动的任务意味着可以创建不同优先级的任务,而不需要最高优先级的任务占用所有较低优先级的任务的处理时间。
阻塞态(Blocked)
如果一个任务正在等待某个事件,则称这个任务处于”阻塞态(blocked)”。阻塞态是非运行态的一个子状态。 任务可以进入Blocked状态等待两种不同类型的事件:
- 定时(时间相关)事件——这类事件可以是延迟到期或是绝对时间到点。比如说某个任务可以进入阻塞态以延迟 10ms。
- 同步事件——源于其它任务或中断的事件。比如说,某个任务可以进入阻塞态以等待队列中有数据到来。同步事件囊括了所有板级范围内的事件类型。
FreeRTOS队列、二进制信号量、计数信号量、互斥量、递归互斥量、事件组和直接任务通知都可以用来创建同步事件。
任务可以在进入阻塞态以等待同步事件时指定一个等待超时时间,这样可以有效地实现阻塞状态下同时等待两种类型的事件。比如说,某个任务可以等待队列中有数据到来,但最多只等 10ms。如果 10ms 内有数据到来,或是 10ms 过去了还没有数据到来,这两种情况下该任务都将退出阻塞态。
挂起态(Suspended)
“挂起(suspended)”也是非运行状态的子状态。处于挂起状态的任务对调度器而言是不可见的。让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend() API 函数;而把一个挂起状态的任务唤醒的唯一途径就是调用 vTaskResume() 或vTaskResumeFromISR() API 函数。大多数应用程序中都不会用到挂起状态。
就绪态(Ready)
如果任务处于非运行状态,但既没有阻塞也没有挂起,则这个任务处于就绪(ready,准备或就绪)状态。处于就绪态的任务能够被运行,但只是”准备(ready)”运行,而当前尚未运行。
完成状态转换图
目前为止所有用到的示例程序中创建的任务都还没有用到阻塞状态和挂起状态,仅仅是在就绪状态和运行状态之间转移——图中以粗线进行醒目提示。
例4 利用阻塞态实现延迟
之前的示例中所有创建的任务都是”周期性”的——它们延迟一个周期时间,打印输出字符串,再一次延迟,如此周而复始。而产生延迟的方法也相当原始地使用了空循环——不停地查询并递增一个循环计数直至计到某个指定值。例 3 明确的指出了这种方法的缺点。一直保持在运行态中执行空循环,可能将其它任务饿死。通过调用 vTaskDelay() API 函数来代替空循环,对这种”不良行为”进行纠正。 任何形式的投票都有其他几个缺点,尤其是效率低下。在轮询期间,任务实际上没有任何工作要做,但它仍然使用最长的处理时间,因此会浪费处理器周期。示例4通过调用vTaskDelay()API函数替换轮询空循环来纠正这种行为。
void vTaskDelay( portTickType xTicksToDelay ); 请注意vTaskDelay()API函数仅在FreeRTOSConfig.h中将INCLUDE_vTaskDelay设置为1时可用
xTicksToDelay 调用任务在转换回就绪状态之前将保持在阻止状态的勾号中断次数。例如,如果一个任务在滴答数为10000时被称为vTaskDelay(100),那么它将立即进入阻止状态,并保持在阻止状态,直到滴答数达到10100。 宏pdMS_TO_TICKS()可用于将以毫秒为单位指定的时间转换为以TICKS为单位指定的时间。例如,调用vTaskDelay(pdMS_TO_TICKS(100))将导致调用任务在阻塞状态下保持100毫秒。
核心代码
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
const TickType_t xDelay250ms = pdMS_TO_TICKS( 250 );
pcTaskName = ( char * ) pvParameters;
for( ;; )
{
printf("%s\n", pcTaskName );
vTaskDelay( xDelay250ms );
}
}
运行结果
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
图中显示了任务在整个延迟期内进入阻塞状态时的执行模式,因此仅当它们实际有需要执行的工作时(在本例中只是打印一条消息),才使用处理器时间,因此只使用可用处理时间的一小部分。 图的场景中,每次任务离开阻塞状态时,它们都会在重新进入阻塞状态之前执行一段时间。大多数情况下,没有能够运行的应用程序任务(没有处于就绪状态的应用程序任务),因此,没有可以选择进入运行状态的应用程序任务。在这种情况下,空闲任务将运行。分配给空闲设备的处理时间是系统中空闲处理能力的度量。通过允许应用程序完全由事件驱动,使用RTOS可以显著增加备用处理能力。
图中的粗线条表示例 4 中任务的状态转移过程。现在每个任务在返回就绪态之 前,都会经过阻塞状态。
vTaskDelayUntil() API 函数
vTaskDelayUntil()类似于 vTaskDelay()。和范例中演示的一样,函数 vTaskDelay()的参数用来指定任务在调用 vTaskDelay()到切出阻塞态整个过程包含多少个心跳周期。任务保持在阻塞态的时间量由 vTaskDelay()的入口参数指定,但任务离开阻塞态的时刻实际上是相对于 vTaskDelay()被调用那一刻的。 vTaskDelayUntil()的参数就是用来指定任务离开阻塞态进入就绪态那一刻的精确心跳计数值。API 函数 vTaskDelayUntil()可以用于实现一个固定执行周期的需求(当你需要让你的任务以固定频率周期性执行的时候)。由于调用此函数的任务解除阻塞的时间是绝对时刻,比起相对于调用时刻的相对时间更精确(即比调用 vTaskDelay()可以实现更精确的周期性)。
void vTaskDelayUntil( TickType_t * pxPreviousWakeTime, TickType_t xTimeIncrement );
pxPreviousWakeTime指向的变量在vTaskDelayUntil()函数中自动更新;应用程序代码通常不会修改它,但在第一次使用它之前,必须将其初始化为当前滴答计数。 xTimeIncrement用’ ticks '指定。宏pdMS_TO_TICKS()可用于将以毫秒为单位指定的时间转换为以刻度为单位指定的时间。
例5 转换示例任务使用 vTaskDelayUntil()
核心代码
void vTaskFunction( void *pvParameters )
{
portTickType xLastWakeTime;
char *pcTaskName;
pcTaskName = ( char * ) pvParameters;
xLastWakeTime = xTaskGetTickCount();
for( ;; )
{
printf("%s\n", pcTaskName );
vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS( 250 ) );
}
}
运行结果与分析与例4一样
例6 合并阻塞与非阻塞任务
在优先级 1 上创建两个任务。这两个任务只是不停地打印输出字符串,然它什么事情也不做。 第三个任务创建在优先级 2 上,高于另外两个任务的优先级。这个任务虽然也是打印输出字符串,但它是周期性的,所以调用了 vTaskDelayUntil(),在每两次打印之间让自己处于阻塞态。
核心代码
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
pcTaskName = ( char * ) pvParameters;
for( ;; )
{
printf("%s\n", pcTaskName );
vTaskDelay(1);
}
}’
void vPeriodicTask( void *pvParameters )
{
TickType_t xLastWakeTime;
const TickType_t xDelay3ms = pdMS_TO_TICKS( 3 );
xLastWakeTime = xTaskGetTickCount();
for( ;; )
{
printf( "Periodic task is running\r\n" );
vTaskDelayUntil( &xLastWakeTime, xDelay3ms );
}
}
例 6 的执行流程
空闲任务与空闲任务钩子函数
例4中创建的任务大部分时间处于阻塞状态。在这种状态下,它们无法运行,因此调度程序无法选择它们。 必须始终至少有一个任务可以进入运行状态1。为了确保这一点,当调用vTaskStartScheduler()时,调度程序会自动创建一个空闲任务。空闲任务只不过是坐在一个循环中,所以,就像最初的第一个示例中的任务一样,它总是能够运行。 空闲任务具有尽可能低的优先级(优先级为零),以确保它不会阻止优先级更高的应用程序任务进入运行状态,尽管没有什么可以阻止应用程序设计人员在空闲任务优先级下创建任务,从而共享空闲任务优先级(如果需要)。configIDLE_SHOULD_YIELD应在FreeRTOSConfig.h中生成编译时配置常量可用于防止空闲任务占用处理时间,而这些时间将更有效地分配给应用程序任务。 以最低优先级运行可确保在高优先级任务进入就绪状态时,空闲任务立即从运行状态转移。这可以在图17中的时间tn处看到,空闲任务立即被调出,以允许任务2在任务2离开阻塞状态的瞬间执行。据说任务2抢占了空闲任务。先发制人是自动发生的,而且不知道任务被先发制人。 注意:如果应用程序使用vTaskDelete()API函数,则必须确保空闲任务不缺少处理时间。这是因为空闲任务负责在任务被删除后清理内核资源。
空闲任务钩子函数
通过使用空闲钩子(或空闲回调)函数,可以将特定于应用程序的功能直接添加到空闲任务中——空闲任务在空闲任务循环的每次迭代中自动调用该函数一次。 任务钩子的常用用法包括: 1 执行低优先级、后台或连续处理功能。 2 测量空闲处理能力的数量 3 将处理器进入低功耗模式,提供一个简单的和自动的方法拯救力量只要没有执行应用程序处理
空闲任务钩子函数的实现限制
空闲任务钩子函数必须遵从以下规则: 一个空闲的任务钩子函数绝对不能尝试阻塞或挂起。 如果应用程序使用了vTaskDelete() API函数,那么Idle任务钩子必须总是在合理的时间内返回给它的调用者。这是因为Idle任务在任务被删除后负责清理内核资源。如果空闲任务永久地保持在idle钩子函数中,那么这种清理就不会发生。
void vApplicationIdleHook( void );
configUSE_IDLE_HOOK必须在FreeRTOSConfig.h中设置为1,以便调用空闲钩子函数。
例7 定义一个空闲任务钩子函数
main.c
#include <stdio.h>
#include <stdlib.h>
#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "FreeRTOS.h"
#include "task.h"
#define mainDELAY_LOOP_COUNT ( 0xffffff )
void vTaskFunction( void *pvParameters );
volatile uint32_t ulIdleCycleCount = 0UL;
const char *pcTextForTask1 = "Task 1 is running\r\n";
const char *pcTextForTask2 = "Task 2 is running\r\n";
int main( void )
{
xTaskCreate( vTaskFunction,
"Task 1",
1000,
(void*)pcTextForTask1,
1,
NULL );
xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 2, NULL );
vTaskStartScheduler();
for( ;; );
return 0;
}
void vApplicationIdleHook( void )
{
ulIdleCycleCount++;
}
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
const TickType_t xDelay250ms = pdMS_TO_TICKS( 250 );
pcTaskName = ( char * ) pvParameters;
for( ;; )
{
printf("%s\n",pcTaskName);
printf("ulIdleCycleCount = %d\n",ulIdleCycleCount);
vTaskDelay( xDelay250ms );
}
}
void vMainQueueSendPassed( void )
{
return;
}
运行结果
Task 2 is running
ulIdleCycleCount = 0
Task 1 is running
ulIdleCycleCount = 0
Task 2 is running
ulIdleCycleCount = 92296879
Task 1 is running
ulIdleCycleCount = 92296879
Task 2 is running
ulIdleCycleCount = 184470826
Task 1 is running
ulIdleCycleCount = 184753526
Task 2 is running
ulIdleCycleCount = 276757071
Task 1 is running
它显示空闲任务钩子函数在应用程序任务的每次迭代之间被调用大约9000万次(迭代的次数取决于执行演示的硬件的速度)。
修改任务优先级
vTaskPrioritySet() API函数可以用来在调度器启动后改变任何任务的优先级。注意,只有在FreeRTOSConfig.h中INCLUDE_vTaskPrioritySet设置为1时,vTaskPrioritySet() API函数才可用。
void vTaskPrioritySet( TaskHandle_t pxTask, UBaseType_t uxNewPriority );
pxTask 被修改优先级的任务句柄(即目标任务),任务可以通过传入 NULL 值来修改自己的优先级。 uxNewPriority 目标任务将被设置到哪个优先级上。如果设置的值超过了最大可用优先级(configMAX_PRIORITIES – 1),则会被自动封顶为最大值。常量 configMAX_PRIORITIES 是在 FreeRTOSConfig.h 头文件中设置的一个编译时选项。 uxTaskPriorityGet() API函数可以用来查询任务的优先级。注意,只有在FreeRTOSConfig.h中INCLUDE_uxTaskPriorityGet设置为1时,uxTaskPriorityGet() API函数才可用。
UBaseType_t uxTaskPriorityGet( TaskHandle_t pxTask );
返回值 当前分配给查询任务的优先级。
例8 改变任务优先级
调度器将始终选择处于最高Ready状态的任务作为进入Running状态的任务。示例8通过使用vTaskPrioritySet() API函数来改变两个任务的相对优先级来演示这一点。( FreeRTOS中优先级的数值越小,代表该任务的优先级越低,最低优先级为0) 以两个不同的优先级创建了两个任务。这两个任务都不会调用任何API函数,从而导致它进入Blocked状态,所以它们总是处于Ready状态或Running状态。因此,相对优先级最高的任务将始终是调度器选择的处于Running状态的任务。
具有以下行为方式: 任务 1创建在最高优先级,以保证其可以最先运行。任务 1 首先打印输出两个字符串,然后将任务 2的优先级提升到自己之上。 任务 2 一旦拥有最高优先级便启动执行(进入运行态)。由于任何时候只可能有 一个任务处于运行态,所以当任务 2 运行时,任务 1 处于就绪态。 任务 2 打印输出一个信息,然后把自己的优先级设回低于任务 1 的初始值。 任务 2 降低自己的优先级意味着任务 1 又成为具有最高优先级的任务,所以任务 1 重新进入运行态,任务 2 被强制切入就绪态。
main.c
#include <stdio.h>
#include <stdlib.h>
#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "FreeRTOS.h"
#include "task.h"
#define mainDELAY_LOOP_COUNT ( 0xffffff )
void vTaskFunction1( void *pvParameters );
void vTaskFunction2( void *pvParameters );
TaskHandle_t xTask2Handle = NULL;
const char *pcTextForTask1 = "Task 1 is running\r\n";
const char *pcTextForTask2 = "Task 2 is running\r\n";
int main( void )
{
xTaskCreate( vTaskFunction2,
"Task 1",
1000,
(void*)pcTextForTask1,
2,
NULL );
xTaskCreate( vTaskFunction1, "Task 2", 1000, (void*)pcTextForTask2, 1, xTask2Handle );
vTaskStartScheduler();
for( ;; );
}
void vTaskFunction1( void *pvParameters )
{
UBaseType_t uxPriority;
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
printf( "Task 1 is running\r\n" );
printf( "About to raise the Task 2 priority\r\n" );
vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );
vTaskDelay(1);
}
}
void vTaskFunction2( void *pvParameters )
{
UBaseType_t uxPriority;
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
printf( "Task 2 is running\r\n" );
printf( "About to lower the Task 2 priority\r\n" );
vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
vTaskDelay(1);
}
}
void vMainQueueSendPassed( void )
{
return;
}
void vApplicationIdleHook( void )
{
return;
}
运行结果
Task 2 is running
About to lower the Task 2 priority
Task 1 is running
About to raise the Task 2 priority
Task 1 is running
About to raise the Task 2 priority
Task 2 is running
About to lower the Task 2 priority
Task 1 is running
About to raise the Task 2 priority
.......
删除任务
任务可以使用vTaskDelete() API函数来删除自己或其他任务。注意,只有在FreeRTOSConfig.h中将INCLUDE_vTaskDelete设置为1时,vTaskDelete() API函数才可用。 删除后的任务将不再存在,不能再次进入“运行中”状态。 空闲任务的责任是要将分配给已删除任务的内存释放掉。因此有一点很重要,那就是使用 vTaskDelete() API 函数的任务千万不能把空闲任务的执行时间饿死。 注意:当任务被删除时,只有内核自己分配给任务的内存才会被自动释放。任务执行过程中分配的任何内存或其他资源都必须显式释放。
void vTaskDelete( TaskHandle_t pxTaskToDelete );
pxTaskToDelete 要删除的任务的句。任务可以通过传递NULL来代替有效的任务句柄来删除自己。
例9 删除任务
这是一个非常简单的示例,其行为如下所示:
- 任务1是由优先级为1的main()创建的。当它运行时,它以优先级2创建Task 2。任务2现在是最高优先级的任务,因此它立即开始执行。
- 任务2只做删除本身。它可以通过将NULL传递给vTaskDelete()来删除自己,但相反,为了演示目的,它使用自己的任务句柄。
3.当Task 2被删除时,Task 1再次成为优先级最高的任务,因此继续执行——此时它调用vTaskDelay()来阻塞一小段时间。 - Idle任务在task 1处于阻塞状态时执行,释放分配给task 2的内存。
- 当Task 1离开阻塞状态时,它再次成为最高优先级的Ready状态任务,从而抢占Idle状态任务。当它进入Running状态时,它再次创建Task 2,然后继续执行
main.c
#include <stdio.h>
#include <stdlib.h>
#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "FreeRTOS.h"
#include "task.h"
#define mainDELAY_LOOP_COUNT ( 0xffffff )
void vTaskFunction1( void *pvParameters );
void vTaskFunction2( void *pvParameters );
TaskHandle_t xTask2Handle = NULL;
const char *pcTextForTask1 = "Task 1 is running\r\n";
const char *pcTextForTask2 = "Task 2 is running\r\n";
int main( void )
{
xTaskCreate( vTaskFunction1, "Task 1", 1000, NULL, 1, NULL );
vTaskStartScheduler();
for( ;; );
}
void vTaskFunction1( void *pvParameters )
{
const TickType_t xDelay100ms = pdMS_TO_TICKS( 100UL );
for( ;; )
{
printf( "Task 1 is running\r\n" );
xTaskCreate( vTaskFunction2, "Task 2", 1000, NULL, 2, &xTask2Handle );
vTaskDelay( xDelay100ms );
}
}
void vTaskFunction2( void *pvParameters )
{
printf( "Task 2 is running and about to delete itself\r\n" );
vTaskDelete( xTask2Handle );
}
void vMainQueueSendPassed( void )
{
return;
}
void vApplicationIdleHook( void )
{
return;
}
运行结果
Task 1 is running
Task 2 is running and about to delete itself
Task 1 is running
Task 2 is running and about to delete itself
Task 1 is running
Task 2 is running and about to delete itself
调度算法
任务状态和事件的概述
实际正在运行(使用处理时间)的任务处于running状态。在一个单核处理器上,任何时候都只能有一个任务处于Running状态。 没有实际运行,但既不是处于Blocked状态,也不是Suspended状态的任务处于Ready状态。处于Ready状态的任务可以被调度器选择为进入Running状态的任务。调度器总是选择优先级最高的Ready状态任务进入Running状态。 任务可以处于Blocked状态等待事件发生,当事件发生时,任务会自动恢复到Ready状态。时间事件发生在特定的时间,例如,当块时间过期时,通常用于实现周期性或超时行为。同步事件发生在任务或中断服务例程使用任务通知、队列、事件组或多种类型的信号量之一发送信息时。它们通常用于通知异步活动,例如数据到达外设。
配置调度算法
调度算法是决定将哪个状态为Ready的任务转换为Running状态的软件例程。 到目前为止,所有的示例都使用了相同的调度算法,但是可以使用configUSE_PREEMPTION和configUSE_TIME_SLICING配置常量来更改算法。这两个常量都在FreeRTOSConfig.h中定义。 第三个配置常量configUSE_TICKLESS_IDLE也会影响调度算法,因为使用它会导致tick中断在很长一段时间内完全关闭。configUSE_TICKLESS_IDLE是一个高级选项,专门用于必须最小化功耗的应用程序。configUSE_TICKLESS_IDLE在第十章,低功耗支持中描述。本节的描述假设configUSE_TICKLESS_IDLE被设置为0,当常量未定义时,这是默认设置。 在所有可能的配置中,FreeRTOS调度器将确保共享优先级的任务依次选择进入Running状态。这种“轮流”策略通常被称为“循环调度”。循环调度算法不能保证同等优先级的任务之间的时间分配是均等的,只能保证处于就绪状态的任务依次进入运行状态。
术语
固定优先级 被称为“固定优先级”的调度算法不会改变分配给被调度任务的优先级,但也不会阻止任务本身改变自己的优先级或其他任务的优先级。 抢占式 优先级高于Running状态的任务进入Ready状态时,抢占式调度算法会立即“抢占”处于Running状态的任务。被抢占意味着不自觉地(不显式地让步或阻塞)从Running状态移到Ready状态,从而允许另一个任务进入Running状态。 时间片 时间切片用于在同等优先级的任务之间共享处理时间,即使任务没有显式让步或进入阻塞状态。使用“时间切片”的调度算法会在每个时间切片结束时选择一个新任务进入“运行”状态,如果有其他状态为“就绪”的任务和“运行中”的任务具有相同的优先级。一个时间片等于两个RTOS tick interrupt之间的时间。
有时间片的优先抢占式调度
表所示的配置将FreeRTOS调度器设置为使用一种称为“具有时间片的优先抢占调度”的调度算法,这是大多数小型RTOS应用程序使用的调度算法,也是本书迄今为止所有示例使用的算法。 图中显示了当应用程序中的所有任务都具有唯一优先级时,选择任务进入运行状态的顺序。 图中可看出: 空闲任务以最低优先级运行,因此每当高优先级任务进入就绪状态时(例如,在t3、t5和t9时刻),空闲任务就会被抢占。 任务3是一个事件驱动的任务,其执行优先级相对较低,但高于空闲优先级。它大部分时间都处于阻塞状态,等待感兴趣的事件,每次事件发生时,它都会从阻塞状态转换到就绪状态。所有FreeRTOS任务间通信机制(任务通知、队列、信号量、事件组等)都可以用于以这种方式发送事件信号和解除任务阻塞。事件发生在时间t3和t5,也发生在时间t9和t12之间。在t3和t5时刻发生的事件会立即得到处理,因为在这些时刻,任务3是能够运行的优先级最高的任务。在时间t9和t12之间发生的事件直到t12才被处理,因为在那之前,更高优先级的任务任务1和任务2仍在执行。只有在时间t12,任务1和任务2都处于阻塞状态,使任务3成为优先级最高的就绪状态任务。 任务2是一个周期性任务,其执行优先级高于任务3的优先级,但低于任务1的优先级。任务的周期间隔意味着任务2希望在时间t1、t6和t9执行。在时间t6,任务3处于运行状态,但任务2具有更高的相对优先级,因此抢占任务3并立即开始执行。任务2完成其处理并在时间t7重新进入阻塞状态,此时任务3可以重新进入运行状态以完成其处理。任务3本身在时间t8阻塞。 任务1也是一个事件驱动的任务。它以最高优先级执行,因此可以抢占系统中的任何其他任务。显示的唯一Task 1事件发生在时间t10,此时Task 1先于Task 2。只有在任务1在时间t11重新进入阻塞状态后,任务2才能完成其处理。
图中显示了当应用程序中的两个任务共享一个优先级时,选择任务进入运行状态的顺序。 空闲任务和任务2都是连续处理任务,它们的优先级都为0(可能的最低优先级)。当没有更高优先级的任务可以运行时,调度器仅将处理时间分配给优先级为0的任务,并通过时间切片共享分配给优先级为0的任务的时间。新的时间片从每个滴答声中断开始,在图27中是时间t1、t2、t3、t4、t5、t8、t9、t10和t11。空闲任务和任务2依次进入运行状态,这可能会导致两个任务在同一时间段的一部分处于运行状态,如时间t5和时间t8之间发生的情况。 任务1的优先级高于空闲优先级。任务1是一个事件驱动的任务,它的大部分时间都处于阻塞状态,等待感兴趣的事件,每次事件发生时都从阻塞状态转换到就绪状态。感兴趣的事件发生在时间t6,因此在t6,任务1成为能够运行的优先级最高的任务,因此任务1在时间片的一段时间内先占空闲任务。事件处理在时间t7完成,此时任务1重新进入阻塞状态。 上图显示了在相同优先级时应用程序任务与空闲任务占用相同的CPU时间,如果在实际应用中应用程序任务有工作需要处理,则不需要空闲任务占用过多的时间,此时可以通过配置configIDLE_SHOULD_YIELD改变空闲任务的时间分配方式: configIDLE_SHOULD_YIELD设置为0,则空闲任务与它相同优先级的任务分配相等的CPU时间。如上图。 configIDLE_SHOULD_YIELD设置为1,则如果其他与空闲任务优先级相同的任务进入就绪状态,空闲任务就会立即让出CPU的使用权,使该就绪任务立即执行。如下图。 图中也可看出,当configIDLE_SHOULD_YIELD设置为1时,空闲任务让出CPU使用权后进入运行态的任务不会重新开始一个新的时间片,而是使用空闲任务剩余的时间片执行。
无时间片的优先抢占式调度
无时间切片的优先抢占式调度保持了与前一节所述相同的任务选择和抢占算法,但不使用时间切片在同等优先级的任务之间共享处理时间。FreeRTOSConfig.h将FreeRTOS调度器配置为使用优先抢占式调度而不进行时间切片的设置如表所示。
如果使用时间片,并且有优先级相同的任务处于就绪态,那么调度程序将会在每个时间片结束时选择新的就绪任务在下一个时间片开始时进入运行态。如果未使用时间片,则调度程序只有在下列情况中的任意一种发生时切换任务: 优先级较高的任务进入就绪状态。 处于运行状态的任务进入阻止或挂起状态。 不使用时间片时与使用时间片相比,任务的上下文切换比较少,因此,不使用时间片会使调度程序的开销减少,但是不使用时间片切换任务会导致相同优先级的任务占用不同的CPU时间(差异可能很大)。处于这个原因,只有经验丰富的用户才可以使用没有时间片的调度程序。调度情况如下图。
图中,假设configIDLE_SHOULD_YIELD设置为0: 滴答中断发生在t1,t2,t3,t4,t5,t8,t11,t12和t13。 任务1是高优先级事件驱动的任务,其大部分时间都处于阻塞状态,等待其感兴趣的事件。每次事件发生时,任务1从阻塞状态转换到就绪状态,随后进入运行态。如图中的t6和t7之间,t9和t10之间。 空闲任务和任务2都是连续处理任务,两者的优先级均为0。连续处理任务不会进入阻止状态。因为未使用时间片,所以处于运行态的空闲优先级任务将保持运行态,直到它被优先级较高的任务1抢占为止。图中,空闲任务在t1开始运行,并且保持运行态,直到它被任务1在t6抢占(在进入运行状态之后超过4个完整的滴答周期)。任务2在t7开始运行,这是因为任务1重新进入阻塞态以等待另一个事件。任务2保持在运行态,直到它在t9被任务1抢占(在进入运行状态之后小于一个滴答周期)。在t10,空闲任务重新进入运行态,尽管已经占用了比任务2多四倍以上的CPU时间。
协作调度
FreeRTOS也可以使用协作调度。 配置如下 使用协作调度时,只有在运行态任务进入阻塞态或运行态任务调用taskYIELD()(应用程序请求重新调度)时才会发生上下文切换。任务永远不会被抢占,因此也不能使用时间片。调度情况如下,图中的虚线表示任务进入就绪态。 任务1优先级最高,一开始它处于阻塞态等待信号量,在t3,中断发出了信号量,任务1从阻塞态切换到就绪态,如果使用的使抢占式调度程序,它此时就会进入运行态,但这里使用的是协作式调度,直到t4,应用程序调用了taskYIELD(),它下切换到运行态。 任务1的优先级在任务1和任务3之间。开始时它处于阻塞态,在t2时任务3给它发送了消息。因为使用的是协作式调度程序,因此需要调用taskYIELD()或高优先级任务进入阻塞态或暂停态,在t4应用程序调用了taskYIELD(),但因为有高优先级的任务处于就绪态,所以t4任然是高优先级的任务进入运行态,直到t5任务1进入阻塞态,任务2才被调度,进入运行态。
在多任务的程序中,编写程序时必须注意同一资源(变量,外设等)不能被多个任务同时访问,因为同时访问时可能会破坏资源。如,两个任务向串口写入字符串,任务1写入"abcdefghijklmn",任务2写入"123456789": 任务1处于运行态开始写入字符串,当将"abcdefg"写入串口时时间片结束,切换到任务2。 任务2进入运行态,写入"123456789"。 任务1再次进入运行态,写入剩余字符串"hijklmn"。 这种情况下实际写入串口的字符串为"abcdefg123456789hijklmn",任务1写入的字符串没有按照预期的顺序写入串口,而且已经被损坏。 使用协作式调度程序时,通常会比使用抢占式调度程序更容易避免上面的问题(后面会介绍其他资源同步的一些方法),但使用协作式调度程序会使程序的响应变慢(或者称为实时性降低)。
|