FreeRTOS学习笔记四【任务管理-中】
任务优先级
在Free RTOS学习笔记三【任务管理-上】一文中介绍了xTaskCreate()
函数的各个参数,其中参数uxPriority
指定创建任务的初始优先级。在创建完成后可以通过
vTaskPrioritySet()
函数更改任务的优先级。优先级的最大值由FreeRTOSConfig.h的configMAX_PRIORITIES
设定,数值小的优先级为低优先级。因此,可用的优先级范围为0到(configMAX_PRIORITIES-1),任意数量的任务都可以共享相同的优先级。
FreeRTOS调度程序可以使用两种方法之一来确定哪个任务将处于运行状态:
- 通用方法
通用方法在C中实现,可以于所有FreeRTOS的移植一起使用。使用通用方法,可以不设置configMAX_PRIORITIES的最大值,但是建议将configMAX_PRIORITIES设置在适当的最小值,因为它越大,消耗的RAM就越多。
如果configUSE_PORT_OPTIMISED_TASK_SELECTION设置为0或者未定义configUSE_PORT_OPTIMISED_TASK_SELECTION,或者一些处理器没有优化的方法,则将使用通用方法。 - 处理器优化的方法
这种方法使用少量的汇编代码,比通用方法更快,如果使用这种方法,那么configMAX_PRIORITIES不能大于32,同样的也建议将configMAX_PRIORITIES设置在适当的最小值,因为它越大,消耗的RAM就越多。
注意:并非所有FreeRTOS端口都提供架构优化方法。
内核的调度程序始终选择当前可进入运行状态的任务执行,如果多个优先级相同的任务都处于可运行状态时,调度器将依次调度每个任务。
时间测量和嘀嗒中断
一个时间片执行一个任务,在时间片开始时进入运行状态,在时间片结束时退出运行状态。在下图中t1和t2之间的时间就表示一个时间片。
为了能够选择将要运行的下一个任务,调度程序本身必须在每个时间片末尾执行(如下图),即滴答中断。时间片的长度有滴答中断的频率设定,该中断频率由FreeRTOSConfig.h中的configTICK_RATE_HZ配置,例如configTICK_RATE_HZ设置为100(Hz),则时间片为10ms,两次滴答中断之间的时间称为滴答周期,一个时间片就等于一个滴答周期。configTICK_RATE_HZ的最佳值取决于正在开发的应用程序,但通常设置为100。
图中可看出,进入一次滴答中断,当前时间片就结束了,同时中断中选择下一个将要执行的任务。
pdMS_TO_TICKS()
将以毫秒为单位的时间转换为滴答周期中的等效时间。 分辨率取决于定义的滴答频率,如果滴答频率高于1KHz(configTICK_RATE_HZ大于1000),则不能使用pdMS_TO_TICKS()。建议不要直接在程序中具体的时间,而是使用pdMS_TO_TICKS()宏来转换,这样做确保当滴答周期改变后,应用程序中设定的时间不会改变。
/* xTimeInTicks为200毫秒时间对应滴答周期数 */
TickType_t xTimeInTicks = pdMS_TO_TICKS( 200 );
滴答计数值是自调度程序启动以来发生的滴答中断总数,如果滴答计数未溢出,用户应用程序在设置延迟时间时可以不必考虑溢出问题,因为时间一致性由FreeRTOS内部管理。
任务状态(补充)
在Free RTOS学习笔记三【任务管理-上】一文中介绍了一下任务状态,这里补充没有介绍完的内容,在介绍前先看一个示例。
/* 定义传入任务的字符串 */
static const char *pcTextForTask1 = "Task 1 is running\r\n";
static const char *pcTextForTask2 = "Task 2 is running\r\n";
int main( void )
{
/* 创建任务1 */
xTaskCreate( vTaskFunction, "Task 1", 1000, (void*)pcTextForTask1, 1, NULL );
/* 创建任务2 */
xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 2, NULL );
/* 启动任务调度器 */
vTaskStartScheduler();
for( ;; );
}
运行结果:
示例只改变了任务2的优先级,其余的与前面介绍的示例相同,但输出的结果却又很大的差异。产生的原因是调度器始终运行的是高优先级的任务,而任务函数中除了打印字符串就是一个空循环的延时,因为是高优先级所以可以一直占用处理器。而之前的示例是因为两个任务的优先级相同的,所以调度器是依次执行两个任务。
调度程序将始终选择能够运行的最高优先级任务。任务2的优先级高于任务1,并且始终能够运行;因此,任务2是进入运行状态的唯一任务。当任务1从未进入运行状态时,它永远不会打印出它的字符串。所以这种想象被称为任务1“饿死”。
前面创建的任务始终具有执行权,并且从不待任何事件,它们总是能够进入运行状态。这种“连续处理”任务的有用性十分有限,它们始终只能以最低优先级创建。如果它们的优先级不是最低的,它将组织比它优先级的任务运行。
为了使一个任务有用,则必须以事件驱动的方式实现。事件驱动的任务只有在发生触发它的事件时才执行,并且在该事件发生之前无法进入运行状态。调度程序始终选择能够运行的最高优先级任务,不能运行的高优先级任务意味着调度程序无法选择它们,从而必保证较低优先级的任务也能运行。因此,使用事件驱动的任务就不会产生任务饿死的现象。
非运行态的子状态
下面介绍任务的其他状态,它们都是非运行状态:
- 阻塞态
等待事件的任务就处于阻塞态。任务进入阻塞态后可以等待两种不同的事件类型:
- 时间事件(与时间相关):事件是延时时间到达或到达绝对时间。例如,任务可以进入阻塞态等待延时10ms到达。
- 同步事件:事件来自另一个任务或中断。例如,任务等待队列中的数据到达。
同步事件包含了广泛的事件类型,包括队列、二值信号量、计数信号量、互斥锁、递归互斥锁、事件组、任务通知,它们都可以创建同步事件。
任务可以在一段时间内等待同步事件。例如,任务可以最多等待队列中的数据10ms,如果数据在10ms内到达或者超过10ms数据都没有到达,任务将离开阻塞态。
-
暂停态
处于暂停状态的任务不可被调度程序调度。 进入暂停状态的唯一方法是通过调用vTaskSuspend() API函数,退出暂停态唯一的方法是通过调用vTaskResume()或xTaskResumeFromISR() API函数。 大多数应用程序不使用暂停态。 -
就绪态
当任务处于非运行状态而又没有处于阻塞态和运行态时,这种状态称为就绪态。这类任务可以被调度程序调度,因为已经准备就绪,但调度器还未调度到它。
各个状态的转换图
示例
前面的示例中,延时都是通过循环计数的方式实现,前面也说明了一些这种方法的缺点。
除了高优先级任务会饿死低优先级任务外,它的效率也是非常低的。延时期间这个任务没有做任何有用的工作,但是浪费了大量的处理时间。下面通过vTaskDelay()函数来改善这类缺点。要使用vTaskDelay()函数需要将FreeRTOSConfig.h中的INCLUDE_vTaskDelay设置为1。这个函数将调用任务切换到阻塞态,到延时时间到达后切换到就绪态,等待调度。因此任务仅在实际工作需要时才占用处理器时间。
vTaskDelay函数
vTaskDelay()的原型如下:
void vTaskDelay( TickType_t xTicksToDelay );
参数:
xTicksToDelay :在转换回就绪状态之前,调用任务将处于阻塞状态的tick中断的数量。例如,如果一个任务在tick计数为10000时调用vTaskDelay(100),那么它将立即进入阻塞状态 ,并保持在阻止状态,直到tick计数达到10100才进入到就绪态。
pdMS_TO_TICKS()可用于将以毫秒为单位指定的时间转换为刻度中指定的时间。 例如,调用vTaskDelay(pdMS_TO_TICKS(100))将会使调用任务保持在阻塞状态100毫秒。
任务函数如下,main函数同前:
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
const TickType_t xDelay250ms = pdMS_TO_TICKS( 250 );
pcTaskName = ( char * ) pvParameters;
for( ;; )
{
vPrintString( pcTaskName );
/* 延时xDelay250ms,此时任务进入阻塞态,直到250ms到达 */
vTaskDelay( xDelay250ms );
}
}
运行结果:
此时,就算任务的优先级不同也可以运行。忽略调度程序的运行,两个任务的调度情况如下,其中Idle任务由调度器创建。
使用循环计数实现延时的任务的CPU占用时100%的,因为每时每刻都需要处理。而使用事件机制的任务的CPU占用非常少,CPU大部分时间处于空闲状态,只有真正需要时才会占用一点CPU时间。
vTaskDelayUntil函数
vTaskDelayUntil()类似于vTaskDelay()。vTaskDelay()设定的是任务处于阻塞态的时间,是一个相对时间,而vTaskDelayUntil()设定的是任务下一次进入就绪态的确切时间,是一个绝对时间,它适用于以固定频率执行的任务。
vTaskDelayUntil()原型如下:
void vTaskDelayUntil( TickType_t * pxPreviousWakeTime, TickType_t xTimeIncrement );
参数:
- pxPreviousWakeTime
这个参数是假设vTaskDelayUntil()用于实现定期执行且具有固定频率的任务。 在这种情况下,pxPreviousWakeTime保存任务最后一次离开阻塞状态的时间(被’唤醒’)。 此时间用作参考点,用于计算任务下次离开阻止状态的时间。
pxPreviousWakeTime指向的变量在vTaskDelayUntil()函数中自动更新; 它通常不会被应用程序代码修改,但必须在第一次使用之前初始化为当前的滴答计数。 - xTimeIncrement
设定定期执行任务的频率,通常用宏pdMS_TO_TICKS()转换用户设定的时间。
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
TickType_t xLastWakeTime;
pcTaskName = ( char * ) pvParameters;
/* 使用当前的滴答计数初始化xLastWakeTime, 后面不需要再去修改这个变量,它由vTaskDelayUntil更新*/
xLastWakeTime = xTaskGetTickCount();
for( ;; )
{
vPrintString( pcTaskName );
/* 每250ms执行运行一次该任务 */
vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS( 250 ) );
}
}
运行结果与上面的结果相同。