FreeRTOS是个操作系统,FreeRTOS的任务(task)其实就是像我们电脑中的一个独立程序,表现在源码中,其实就是一个函数。本文从学会使用任务(task)到理解逐步深入。
参考资料:
《Mastering the FreeRTOS™ Real Time Kernel》-Chapter 3
Task Management
目录
1.在FreeRTOS中创建任务
1.1任务的语法
void ATaskFunction( void *pvParameters )
{
变量定义
for( ;; )
{
功能代码
}
vTaskDelete( NULL );
}
(1)任务本质是函数,ATaskFunction,就是函数名,也就是任务名。任务函数的返回类型一定要为 void 类型,也就是无返回值,而且任务的参数也是 void 指针类型的。
(2) 任务的具体执行过程是一个大循环,在这个无限循环里实现功能。
(3)任务函数一般不允许直接跳出循环,如果任务不再需要,跳出循环后必需调用vTaskDelete( NULL );把自己删掉。
注意,只有当FreeRTOSConfig.h中的INCLUDE_vTaskDelete设置为1时,vTaskDelete() API函数才可用。
1.2创建任务
创建用FreeRTOS的API:xTaskCreate(),原型如下:
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
uint16_t usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask );
参数 | 作用 |
pvTaskCode | pvTaskCode参数只是一个指向实现任务的函数的指针(实际上就是任务函数的名称)。 |
pcName | 任务的名称。注意这个名称是自己设置的方便认得名称,FreeRTOS用不到,只是自己用来看的。名称的最大长度在configMAX_TASK_NAME_LEN定义(包括NULL结束符)。 |
usStackDepth | 每个任务都有自己独特的栈,在创建任务时由内核分配给该任务。usStackDepth值告诉内核栈的大小。 该值指定栈可以容纳的字数,而不是字节数。例如,如果栈是32位宽的,而usStackDepth被传递为100,那么将分配400字节的堆栈空间(100*4字节)。 根据你的实际情况分配,任务所需的堆栈空间是可以计算的,但很麻烦,简单地分配合理的值,确保分配的空间确实是足够的,以及内存没有太多浪费就行。 |
pvParameters | 传递给任务的参数。 |
uxPriority | 定义任务优先级。优先级可以从0(最低优先级)分配到(configMAX_PRIORITIES - 1)(最高优先级)。configMAX_PRIORITIES是一个用户定义的常量,假如超过这个值,会被自动改为最大值。 |
pxCreatedTask | 把这个任务的任务句柄传递出去,给别的用,用不到输入NULL就行,任务句柄就是这个任务标识,指代这个任务。后面有例子 |
返回值 | 有两个可能的返回值: 1. pdPASS 表示任务创建成功。 2. pdFAIL 表明任务还没有创建,内存不够。 |
示例可以参考这篇文章末尾的两个任务:STM32F4移植FreeRTOS
官方示例如下:
两个任务都是实现简单的打印功能,同优先级任务同时运行。
void vTask1( void *pvParameters )
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile uint32_t 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;
for( ;; )
{
vPrintString( pcTaskName );
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
}
}
}
int main( void )
{
xTaskCreate( vTask1, "Task 1",1000, NULL,1, NULL );
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
vTaskStartScheduler();
for( ;; );
}
vTaskStartScheduler();为启动调度器,这个后面讲。
运行结果:
注意:例子中延时是用循环实现,在多任务中,完全可以把这个任务挂起,去执行别的任务,使CPU利用率更高,这个后面讲。
我们注意到两个任务功能是完全一样的,只是输出的字符串不一样,把字符串作为任务参数pvParameters传入任务,可以完全使用一段代码,构建两个任务
static const char *pcTextForTask1 = "Task 1 is running\r\n";
static const char *pcTextForTask2 = "Task 2 is running\r\n";
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
volatile uint32_t ul;
pcTaskName = ( char * ) pvParameters;
for( ;; )
{
vPrintString( pcTaskName );
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
}
}
}
int main( void )
{
xTaskCreate( vTaskFunction, "Task 1", 1000,(void*)pcTextForTask1, 1, NULL );
xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 1, NULL );
vTaskStartScheduler();
for( ;; );
}
1.3任务的基本状态
一个FreeRTOS程序可以由许多任务组成。如果运行应用程序的处理器为单核,那么在任何给定时间只能执行一个任务。但是同优先级任务是同时运行的,前面的例子中也看到两个任务同时运行。
为什么看起来两个任务是同时运行?因为他们在快速切换运行,Task1运行一下,保存,切换到Task2运行一下,保存,切换回Task1,无限循环,切换够快就像同时运行。
任务也就有了两种基本状态:运行和非运行。当然还有别的,在后面讲。
多久切换?这段时间叫做时间片(time slice)
谁来切换?FreeRTOS中的一段程序做这个工作,叫做调度器。
2.时间片和Tick中断
调度器在每次时间片(time slice)结束时切换任务,也就是每个时间片结束了,中断一下,去调度。这个中断就叫“tick中断”。
tick中断频率
由FreeRTOSConfig.h中定义的configTICK_RATE_HZ配置。例如,如果configTICK RATE HZ设置为100 (HZ),那么时间片将是10毫秒。两次滴答中断之间的时间称为“滴答周期”(tick period)。一个时间片等于一个滴答周期。
这个频率自己根据程序实际情况设置,比较常用的值就是100。
时间转tick宏
在FreeRTOS提供的API中,用到时间的,参数不是ms这类的时间,而是tick。可以用宏pdMS_TO_TICKS() 把毫秒转换为tick
(如果Tick频率高于1KHz(也就是configTICK_RATE_HZ大于1000),pdMS_TO_TICKS()不能使用)
'tick count'值是自启动调度器以来发生的tick中断的总数(在tick计数没有溢出的情况下)。用户应用程序不用考虑溢出,由FreeRTOS内部管理。
调度器总是选择优先级最高的程序运行,相同优先级任务会轮换运行。
上面的例子中,要是把任务2的优先级改为2。
xTaskCreate( vTaskFunction, "Task 1", 1000,(void*)pcTextForTask1, 1, NULL );
xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 2, NULL );
可以看到,只有任务二运行,任务一不运行了。
3.任务状态
在上一节的例子中我们知道,高优先级任务会占据CPU,低优先级的不会运行。比如我们在学习,到饭点得吃饭,吃饭优先级高,但是不能一开始就吃饭吧。那么就需要高优先级任务先不运行,有个东西来通知高优先级任务,通知到了,就开始。
这就是事件驱动的任务,只有在触发它的事件发生之后才有执行,并且在该事件发生之前无法进入运行态。
因此,使用事件驱动的任务意味着可以按照不同的优先级创建任务,而不会让最高优先级的任务耗尽所有低优先级任务的处理时间。
这样意味着基本的任务状态:运行和非运行不够用了,我们需要新的状态。
3.1阻塞态(The Blocked State)
如果一个任务等待某个事件,他就是“阻塞”态。
任务可以进入阻塞状态来等待两种不同类型的事件:
1. 时间(与时间相关)事件——事件要么是延迟周期到期,要么是到达的绝对时间。例如,一个任务可能进入阻塞状态等待10毫秒通过。
2. 同步事件——任务等待的事件源自另一个任务或中断。例如,任务可能进入阻塞状态以等待数据到达队列。
FreeRTOS队列、二进制信号量、计数信号量、互斥量、递归互斥量、事件组和直接到任务的通知都可以用来创建同步事件。所有这些特性在以后的文章中介绍。
时间事件和同步事件也可以同时用
例如,一个任务阻塞,等待数据到达队列,且只等待10毫秒。如果数据在10毫秒内到达,或者10毫秒后没有数据到达,任务将离开Blocked状态。
3.2挂起态(The Suspended State)
处于挂起状态的任务对调度程序不可用。进入挂起状态的唯一方法是通过调用vTaskSuspend() API函数,唯一退出的方法是通过调用vTaskResume()或xTaskResumeFromISRO API函数。大多数应用程序不使用挂起状态。
3.3就绪态(The Ready State)
处于“未运行”状态但未被阻塞或挂起的任务称为“就绪态”。它们能够运行,因此“准备”运行,但当前不在运行状态,一般是有同优先级或者更高优先级的任务在运行。
最后就是运行态(The Running state),就是正在运行的任务的状态。
四种状态切换的图如下:
在1.2中的任务延时方法为:
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
}
就是一个循环,这种方法占用CPU,我们可以用FreeRTOS的一个函数代替
void vTaskDelay( TickType_t xTicksToDelay )
调用这个函数时,任务就会进入阻塞态,阻塞多长时间?就是参数xTicksToDelay表示阻塞的tick数,注意不是时间而是tick数,用宏pdMS_TO_TICKS() 把毫秒转换为tick,延时10毫秒:
vTaskDelay(pdMS_TO_TICKS(10))
我们之前说,两个不同优先级的任务:
xTaskCreate( vTaskFunction, "Task 1", 1000,(void*)pcTextForTask1, 1, NULL );
xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 2, NULL );
任务二优先级高,会一直执行,任务一无法执行。
假如,我们把任务二中的延时,用vTaskDelay()函数代替,则任务二延时时进入阻塞态,不会占用CPU,低优先级的任务一就可以执行了。大大提高了CPU利用率。
上面两个任务中都用vTaskDelay()函数代替延时,那么肯定有一个时刻两个任务都处于阻塞态。例如:
任务二优先级高,任务二执行,然后阻塞等10ms,任务一执行,然后阻塞等待10ms。这个时候,任务一的10ms还没结束,所以两个任务都是阻塞态。
这段时间CPU不能闲着啊,所以就有了空闲任务。
4.空闲任务和空闲任务钩子函数
空闲任务
空闲任务在启动调度器时自动创建,空闲任务具有最低的优先级(优先级为0),以确保它永远不会阻止更高优先级的应用任务进入运行态。
用户可以创建与空闲任务优先级相同的应用任务,当FreeRTOSConfig.h中的宏configIDLE_SHOULD_YIELD为1的话。应用任务就可以使用空闲任务的时间片,也就是说空闲任务会让出时间片给同优先级的应用任务。
注意:文章开头提到vTaskDelete()可以删除的任务,空闲任务负责释放分配给已删除任务的内存。所以要留给空闲任务执行的机会,不要完全占用。
空闲任务钩子函数
钩子函数就是回调函数,空闲任务钩子函数就是我们自己定义一个函数,这个函数会在空闲任务中运行。
要使用空闲任务钩子函数首先要在 FreeRTOSConfig.h 中将宏 configUSE_IDLE_HOOK 改为1,然后自己编写空闲任务钩子函数vApplicationIdleHook()。注意名字要一样哦。
空闲任务钩子的常用用法包括:
(1)执行低优先级、后台或连续处理功能。
(2)测量CPU空闲处理能力的数量。空闲任务只在所有高优先级的应用程序任务都没有工作要执行时才会运行。因此,测量分配给空闲任务的处理时间可以清楚地表明有多少CPU处理时间是空闲的。
(3)将处理器置于低功耗模式,在没有应用程序处理要执行时可以通过空闲任务进入低功耗模式(还有更省电的方法)。
空闲任务钩子函数必须遵守以下规则:
(1)空闲任务钩子函数永远不能尝试阻塞或挂起。
(2) 如果应用程序使用了vTaskDelete() 函数,那么空闲任务钩子函数必须总是在合理的时间内返回给它的调用者。这是因为空闲任务负责在删除任务后清理内核资源。如果空闲任务永久地保留在空闲钩子函数中,那么这个清理就不会发生。
5.改变和查询任务优先级
vTaskPrioritySet()函数可用于在启动调度器后更改任何任务的优先级。
注意,只有FreeRTOSConfig.h中的INCLUDE_vTaskPrioritySet设置为1时,vTaskPrioritySet() API函数才可用。
void vTaskPrioritySet(TaskHandle_t pxTask, UBaseType_t uxNewPriority)
pxTask为要修改的任务的任务句柄
uxNewPriority为新的优先级
uxTaskPriorityGet()函数用于查询任务的优先级
。注意,只有FreeRTOSConfig.h中的INCLUDE_uxTaskPriorityGet设置为1时,uxTaskPriorityGet() API函数才可用。
UBaseType_t uxTaskPriorityGet( TaskHandle_t pxTask );
例子:
void vTask1( void *pvParameters )
{
UBaseType_t uxPriority;
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
vPrintString( "Task 1 is running\r\n" );
vPrintString( "About to raise the Task 2 priority\r\n" );
vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );
}
}
void vTask2( void *pvParameters )
{
UBaseType_t uxPriority;
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
vPrintString( "Task 2 is running\r\n" );
vPrintString( "About to lower the Task 2 priority\r\n" );
vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
}
}
/*任务句柄*/
TaskHandle_t xTask2Handle = NULL;
int main( void )
{
xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, &xTask2Handle );
vTaskStartScheduler();
for( ;; );
}
任务调度算法将在下篇文章中详解。