在 FreeRTOS 中,每个执行线程都被称为”任务”。
本章的目的
- FreeRTOS 如何为各任务分配处理时间。
- FreeRTOS 如何选择任务投入运行。
- 任务优先级如何影响系统行为。
- 任务存在哪些状态。
以及
- 如何实现一个任务。
- 如何创建一个或多个任务的实例。
- 如何使用任务参数。
- 如何改变一个已创建任务的优先级。
- 如何删除任务。
- 如何实现周期性处理。
- 空闲任务何时运行,可以用来干什么。
1.任务函数
任务是由 C 语言函数实现的。唯一特别的只是任务的函数原型,其必须返回 void,
而且带有一个 void 指针参数。
void ATaskFunction( void *pvParameters );
FreeRTOS 任务不允许以任何方式从实现函数中返回——它们绝不能有一条”return”语句,也==不能执行到函数末尾==。如果一个任务不再需要,可以显式地将其删除。
一个任务函数可以用来创建若干个任务——创建出的任务均是独立的执行实例,拥有属于自己的栈空间,以及属于自己的自动变量(栈变量),即任务函数本身定义的变量。
void ATaskFunction( void *pvParameters )
{
/* 可以像普通函数一样定义变量。用这个函数创建的每个任务实例都有一个属于自己的iVarialbleExample变量。但如果iVariableExample被定义为static,这一点则不成立 – 这种情况下只存在一个变量,所有的任务实例将会共享这个变量。 */
int iVariableExample = 0;
/* 任务通常实现在一个死循环中。 */
for( ;; )
{
/* 完成任务功能的代码将放在这里。 */
}
/* 如果任务的具体实现会跳出上面的死循环,则此任务必须在函数运行完之前删除。传入NULL参数表示删除的是当前任务 */
vTaskDelete( NULL );
}
2.任务状态
- 运行状态
- 非运行状态
当某个任务处于运行态时,处理器就正在执行它的代码。当一个任务处于非运行态
时,该任务进行休眠,它的所有状态都被妥善保存,以便在下一次调试器决定让它进入
运行态时可以恢复执行。
3.创建任务
创建任务使用 FreeRTOS 的 API 函数 xTaskCreate()。
portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode, //指向任务的实现函数的指针
const signed portCHAR * const pcName, //具有描述性的任务名
unsigned portSHORT usStackDepth, //usStackDepth 值用于告诉内核为它分配多大的栈空间,栈空间可以保存多少个字(word)
void *pvParameters, //pvParameters 的值即是传递到任务中的值。
unsigned portBASE_TYPE uxPriority, //任务执行的优先级。优先级的取值范围可以从最低优先级 0 到最高优先级(configMAX_PRIORITIES – 1)。
xTaskHandle *pxCreatedTask ); //pxCreatedTask 用于传出任务的句柄。比如改变任务优先级,或者删除任务。
//1. pdTRUE:表明任务创建成功
//2. errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY由于内存堆空间不足, FreeRTOS 无法分配足够的空间来保存任务结构数据和任务栈,因此无法创建任务。
创建任务示例
本例演示了创建并启动两个任务的必要步骤。这两个任务只是周期性地打印输出字
符串,采用原始的空循环方式来产生周期延迟。两者在创建时指定了相同的优先级,并
且在实现上除输出的字符串外完全一样。
void vTask1( void *pvParameters )
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile unsigned long ul;
/* 和大多数任务一样,该任务处于一个死循环中。 */
for( ;; )
{
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* 延迟,以产生一个周期 */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/* 这个空循环是最原始的延迟实现方式。在循环中不做任何事情。后面的示例程序将采用
delay/sleep函数代替这个原始空循环。 */
}
}
}
void vTask2( void *pvParameters )
{
const char *pcTaskName = "Task 2 is running\r\n";
volatile unsigned long ul;
/* 和大多数任务一样,该任务处于一个死循环中。 */
for( ;; )
{
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* 延迟,以产生一个周期 */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/* 这个空循环是最原始的延迟实现方式。在循环中不做任何事情。后面的示例程序将采用
delay/sleep函数代替这个原始空循环。 */
}
}
}
int main( void )
{
/* 创建第一个任务。 需要说明的是一个实用的应用程序中应当检测函数xTaskCreate()的返回值,以确保任务创建成功。 */
xTaskCreate( vTask1, /* 指向任务函数的指针 */
"Task 1", /* 任务的文本名字,只会在调试中用到 */
1000, /* 栈深度 – 大多数小型微控制器会使用的值会比此值小得多 */
NULL, /* 没有任务参数 */
1, /* 此任务运行在优先级1上. */
NULL ); /* 不会用到任务句柄 */
/* Create the other task in exactly the same way and at the same priority. */
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
/* 启动调度器,任务开始执行 */
vTaskStartScheduler();
/* 如果一切正常, main()函数不应该会执行到这里。但如果执行到这里,很可能是内存堆空间不足导致空闲任务无法创建。第五章有讲述更多关于内存管理方面的信息 */
for( ;; );
}
使用任务参数示例
与上例中创建的两个任务几乎完全相同,唯一的区别就是打印输出的字符串。
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
volatile unsigned long ul;
/* 需要打印输出的字符串从入口参数传入。强制转换为字符指针。 */
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. */
}
}
}
/* 定义将要通过任务参数传递的字符串。定义为const,且不是在栈空间上,以保证任务执行时也有效。 */
static const char *pcTextForTask1 = “Task 1 is running\r\n”;
static const char *pcTextForTask2 = “Task 2 is running\t\n”;
int main( void )
{
/* Create one of the two tasks. */
xTaskCreate( vTaskFunction, /* 指向任务函数的指针. */
"Task 1", /* 任务名. */
1000, /* 栈深度. */
(void*)pcTextForTask1, /* 通过任务参数传入需要打印输出的文本. */
1, /* 此任务运行在优先级1上. */
NULL ); /* 不会用到此任务的句柄. */
/* 同样的方法创建另一个任务。至此,由相同的任务代码(vTaskFunction)创建了多个任务,仅仅是传入的参数不同。同一个任务创建了两个实例。 */
xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 1, NULL );
/* Start the scheduler so our 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 5 provides more information on memory management. */
for( ;; );
}
4.任务优先级
xTaskCreate() API 函数的参数 uxPriority 为创建的任务赋予了一个初始优先级。这个侁先级可以在调度器启动后调用 ==vTaskPrioritySet()== API 函数进行修改。
在 文 件 ==FreeRTOSConfig.h== 中 设 定 的 编 译 时 配 置 常 量==configMAX_PRIORITIES== 的值,即是最多可具有的优先级数目。
低优先级号表示任务的优先级低,优先级号 0 表示最低优先级。有效的优先级号范围从 0 到(configMAX_PRIORITES – 1)。
调度器基本工作原理
调度器保证总是在所有可运行的任务中选择具有最高优先级的任务,并使其进入运行态。如果被选中的优先级上具有不止一个任务,调度器会让这些任务轮流执行。(任务在时间片起始时刻进入运行态,在时间片结束时刻又退出运行态。)
要能够选择下一个运行的任务,调度器需要在每个时间片的结束时刻运行自己本身。一个称为心跳(tick,有些地方被称为时钟滴答,本文中一律称为时钟心跳)中断的周期性中断用于此目的。==时间片的长度通过心跳中断的频率进行设定==,心跳中断频率由FreeRTOSConfig.h 中的编译时配置常量 ==configTICK_RATE_HZ== 进行配置。比如说,如果 configTICK_RATE_HZ 设为 100(HZ),则时间片长度为 10ms。
-
简略程序执行流程
注意:如果高优先级任务不释放资源,低优先级任务是没办法执行任务的。所以需要引入“非运行态”。
改变任务优先级
API 函数 vTaskPriofitySet()可以用于在调度器启动后改变任何任务的优先级。
void vTaskPrioritySet( xTaskHandle pxTask, //被修改优先级的任务句柄(即目标任务),任务可以通过传入 NULL 值来修改自己的优先级。
unsigned portBASE_TYPE uxNewPriority ); //目标任务将被设置到哪个优先级上。
uxTaskPriorityGet() API 函数用于查询一个任务的优先级。
unsigned portBASE_TYPE uxTaskPriorityGet( xTaskHandle pxTask );//被查询任务的句柄(目标任务) ,任务可以通过传入 NULL 值来查询自己的优先级.
改变任务优先级示例
void vTask1( void *pvParameters )
{
unsigned portBASE_TYPE uxPriority;
/* 本任务将会比任务2更先运行,因为本任务创建在更高的优先级上。任务1和任务2都不会阻塞,所以两者要么处于就绪态,要么处于运行态。查询本任务当前运行的优先级 – 传递一个NULL值表示说“返回我自己的优先级”。 */
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
/* Print out the name of this task. */
vPrintString( "Task1 is running\r\n" );
/* 把任务2的优先级设置到高于任务1的优先级,会使得任务2立即得到执行(因为任务2现在是所有任务中具有最高优先级的任务)。注意调用vTaskPrioritySet()时用到的任务2的句柄。程序清单24将展示如何得到这个句柄。 */
vPrintString( "About to raise the Task2 priority\r\n" );
vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );
/* 本任务只会在其优先级高于任务2时才会得到执行。因此,当此任务运行到这里时,
任务2必然已经执行过了,并且将其自身的优先级设置回比任务1更低的优先级。 */
}
}
void vTask2( void *pvParameters )
{
unsigned portBASE_TYPE uxPriority;
/* 任务1比本任务更先启动,因为任务1创建在更高的优先级。任务1和任务2都不会阻塞,
所以两者要么处于就绪态,要么处于运行态。查询本任务当前运行的优先级 – 传递一个NULL值表示说“返回我自己的优先级”。 */
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
/* 当任务运行到这里,任务1必然已经运行过了,并将本身务的优先级设置到高于任务1本身。 */
vPrintString( "Task2 is running\r\n" );
/* 将自己的优先级设置回原来的值。传递NULL句柄值意味“改变我己自的优先级”。
把优先级设置到低于任务1使得任务1立即得到执行 – 任务1抢占本任务。 */
vPrintString( "About to lower the Task2 priority\r\n" );
vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
}
}
/* 声明变量用于保存任务2的句柄。 */
xTaskHandle xTask2Handle;
int main( void )
{
/* 任务1创建在优先级2上。任务参数没有用到,设为NULL。任务句柄也不会用到,也设为NULL */
xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
/* The task is created at priority 2 ______^. */
/* 任务2创建在优先级1上 – 此优先级低于任务1。任务参数没有用到,设为NULL。但任务2的任务句柄会被
用到,故将xTask2Handle的地址传入。 */
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 that
there was insufficient heap memory available for the idle task to be created.
CHAPTER 5 provides more information on memory management. */
for( ;; );
}
5.非运行态
为了使我们的任务切实有用,我们需要通过某种方式来进行事件驱动。一个事件驱动任务只会在事件发生后触发工作(处理),而在事件没有发生时是不能进入运行态的。调度器总是选择所有能够进入运行态的任务中具有最高优先级的任务。一个高优先级但不能够运行的任务意味着不会被调度器选中,而代之以另一个优先级虽然更低但能够运行的任务。因此,采用事件驱动任务的意义就在于任务可以被创建在许多不同的优先级上,并且最高优先级任务不会把所有的低优先级任务饿死。
阻塞状态
如果一个任务正在等待某个事件,则称这个任务处于”阻塞态(blocked)”。阻塞态是非运行态的一个子状态
任务可以进入阻塞态以等待以下两种不同类型的事件:
-
定时(时间相关)事件,延迟到期或精准时间到点。比如说某个任务可以进入阻塞态以延迟 10ms。
- 延迟到期
void vTaskDelay( portTickType xTicksToDelay ); //xTicksToDelay:延迟多少个心跳周期。调用该延迟函数的任务将进入阻塞态,经延迟指定的心跳周期数后,再转移到就绪态。 //常量portTICK_RATE_MS可以用来在毫秒和心跳周期之间相换转换。如:vTaskDelay( 250/portTICK_RATE_MS );
- 精准时间到期
vTaskDelayUntil()的参数就是用来指定任务离开阻塞态进入就绪态那一刻的精确心跳计数值。可以用于实现一个固定执行周期的需求.
void vTaskDelayUntil( portTickType * pxPreviousWakeTime, portTickType xTimeIncrement ); //pxPreviousWakeTime保存了任务上一次离开阻塞态(被唤醒)的时刻。这个时刻被用作一个参考点来计算该任务下一次离开阻塞态的时刻。 //pxPreviousWakeTime 指 向 的 变 量 值 会 在 API 函数vTaskDelayUntil()调用过程中自动更新,应用程序除了该变量第一次初始化外,通常都不要修改它的值。 //xTimeIncrement: 指定任务固定周期执行的频率
- 同步事件,源于其它任务或中断的事件。
例如:- FreeRTOS 的队列
- 二值信号量
- 计数信号量
- 互斥信号量
- 互斥量
挂起状态
“挂起(suspended)”也是非运行状态的子状态。处于挂起状态的任务对调度器而言是不可见的。
让一个任务进入挂起状态的唯一办法就是调用 ==vTaskSuspend()== API 函数;而 把 一 个 挂 起 状 态 的 任 务 唤 醒 的 唯 一 途 径 就 是 调 用 ==vTaskResume()== 或==vTaskResumeFromISR()== API 函数。
就绪状态
如果任务处于非运行状态,但既没有阻塞也没有挂起,则这个任务处于就绪(ready,准备或就绪)状态。处于就绪态的任务能够被运行。
完整的状态转移图
延时任务示例
void vTaskFunction( 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. */
vPrintString( pcTaskName );
/* 延迟一个循环周期。 调用vTaskDelay()以让任务在延迟期间保持在阻塞态。延迟时间以心跳周期为单位,常量portTICK_RATE_MS可以用来在毫秒和心跳周期之间相换转换。本例设定250毫秒的循环周期。 */
vTaskDelay( 250 / portTICK_RATE_MS );
}
}
精准周期任务示例
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
portTickType xLastWakeTime;
/* The string to print out is passed in via the parameter. Cast this to a
character pointer. */
pcTaskName = ( char * ) pvParameters;
/* 变量xLastWakeTime需要被初始化为当前心跳计数值。说明一下,这是该变量唯一一次被显式赋值。之后,xLastWakeTime将在函数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 );
/* 本任务将精确的以250毫秒为周期执行。同vTaskDelay()函数一样,时间值是以心跳周期为单位的,可以使用常量portTICK_RATE_MS将毫秒转换为心跳周期。变量xLastWakeTime会在vTaskDelayUntil()中自动更新,因此不需要应用程序进行显示更新。 */
vTaskDelayUntil( &xLastWakeTime, ( 250 / portTICK_RATE_MS ) );
}
}
6.空闲任务与钩子函数
当调用 vTaskStartScheduler()时,调度器会自动创建一个空闲任务。空闲任务是一个非常短小的循环。空闲任务拥有最低优先级(优先级 0)以保证其不会妨碍具有更高优先级的应用任务进入运行态。
空闲任务钩子函数
通过空闲任务钩子函数(或称回调, hook, or call-back),可以直接在空闲任务中添加应用程序相关的功能。==空闲任务钩子函数会被空闲任务每循环一次就自动调用一次==。
//函数原型
void vApplicationIdleHook( void );
应用范围
- 执行低优先级,后台或需要不停处理的功能代码。
- 测试系统处理裕量。
- 将处理器配置到低功耗模式——提供一种自动省电方法。
实现限制
- 绝不能阻塞或挂起。
- 如果应用程序用到了 vTaskDelete() AP 函数,则空闲钩子函数必须能够尽快返回。让空闲任务负责回收内核资源。
空闲任务钩子函数使用示例
FreeRTOSConfig.h 中的配置常量 configUSE_IDLE_HOOK 必须定义为 1,这样空闲任务钩子函数才会被调用。
对应用任务实现函数进行了少量的修改,用以打印输出变量 ulIdleCycleCount 的值。
/* Declare a variable that will be incremented by the hook function. */
unsigned long ulIdleCycleCount = 0UL;
/* 空闲钩子函数必须命名为vApplicationIdleHook(),无参数也无返回值。 */
void vApplicationIdleHook( void )
{
/* This hook function does nothing but increment a counter. */
ulIdleCycleCount++;
}
void vTaskFunction( 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( ;; )
{
/* 打印输出任务名,以及调用计数器ulIdleCycleCount的值。 */
vPrintStringAndNumber( pcTaskName, ulIdleCycleCount );
/* Delay for a period for 250 milliseconds. */
vTaskDelay( 250 / portTICK_RATE_MS );
}
}
//钩子函数相当于空闲任务一个子函数,因此在mcu空闲时期,钩子函数计数会一直增加。
7.删除任务
任务可以使用 API 函数 vTaskDelete()删除自己或其它任务。
空闲任务的责任是要将分配给已删除任务的内存释放掉。因此有一点很重要,那就
是使用 vTaskDelete() API 函数的任务千万不能把空闲任务的执行时间饿死。
(只有内核为任务分配的内存空间才会在任务被删除后自动回收。
任务自己占用的内存或资源需要由应用程序自己显式地释放。)
void vTaskDelete( xTaskHandle pxTaskToDelete ); //被删除任务的句柄(目标任务),任务可以通过传入 NULL 值来删除自己。
删除任务示例
void vTask1( void *pvParameters )
{
const portTickType xDelay100ms = 100 / portTICK_RATE_MS;
for( ;; )
{
/* Print out the name of this task. */
vPrintString( "Task1 is running\r\n" );
/* 创建任务2为最高优先级。 */
xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
/* The task handle is the last parameter _____^^^^^^^^^^^^^ */
/* 因为任务2具有最高优先级,所以任务1运行到这里时,任务2已经完成执行,删除了自己。任务1得以执行,延迟100ms */
vTaskDelay( xDelay100ms );
}
}
void vTask2( void *pvParameters )
{
/* 任务2什么也没做,只是删除自己。删除自己可以传入NULL值,这里为了演示,还是传入其自己的句柄。 */
vPrintString( "Task2 is running and about to delete itself\r\n" );
vTaskDelete( xTask2Handle );
}
int main( void )
{
/* 任务1创建在优先级1上 */
xTaskCreate( vTask1, "Task 1", 1000, NULL, 1, NULL );
/* The task is created at priority 1 ______^. */
/* Start the scheduler so the tasks start executing. */
vTaskStartScheduler();
/* main() should never reach here as the scheduler has been started. */
for( ;; );
}
8.调度算法
优先级抢占式调度
- 每个任务都赋予了一个优先级。
- 每个任务都可以存在于一个或多个状态。
- 在任何时候都只有一个任务可以处于运行状态。
- 调度器总是在所有处于就绪态的任务中选择具有最高优先级的任务来执行。
这种类型的调度方案被称为”固定优先级抢占式调度”。每个任务都被赋予了一个优先级,这个优先级不能被内核本身改变(只能被任务修改)。
任务可以在阻塞状态等待一个事件,当事件发生时其将自动回到就绪态。时间事件
发生在某个特定的时刻,比如阻塞超时。时间事件通常用于周期性或超时行为。任务或
中断服务例程往队列发送消息或发送任务一种信号量,都将触发同步事件。同步事件通
常用于触发同步行为,比如某个外围的数据到达了。
协作式调度
采用一个纯粹的协作式调度器,只可能在运行态任务进入阻塞态或是运行态任务显
式调用 taskYIELD()时,才会进行上下文切换。任务永远不会被抢占,而具有相同优先
级的任务也不会自动共享处理器时间。协作式调度的这作工作方式虽然比较简单,但可
能会导致系统响应不够快。