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“饿死”。

前面创建的任务始终具有执行权,并且从不待任何事件,它们总是能够进入运行状态。这种“连续处理”任务的有用性十分有限,它们始终只能以最低优先级创建。如果它们的优先级不是最低的,它将组织比它优先级的任务运行。

为了使一个任务有用,则必须以事件驱动的方式实现。事件驱动的任务只有在发生触发它的事件时才执行,并且在该事件发生之前无法进入运行状态。调度程序始终选择能够运行的最高优先级任务,不能运行的高优先级任务意味着调度程序无法选择它们,从而必保证较低优先级的任务也能运行。因此,使用事件驱动的任务就不会产生任务饿死的现象。

非运行态的子状态

下面介绍任务的其他状态,它们都是非运行状态:

  • 阻塞态
    等待事件的任务就处于阻塞态。任务进入阻塞态后可以等待两种不同的事件类型:
  1. 时间事件(与时间相关):事件是延时时间到达或到达绝对时间。例如,任务可以进入阻塞态等待延时10ms到达。
  2. 同步事件:事件来自另一个任务或中断。例如,任务等待队列中的数据到达。

同步事件包含了广泛的事件类型,包括队列、二值信号量、计数信号量、互斥锁、递归互斥锁、事件组、任务通知,它们都可以创建同步事件。
任务可以在一段时间内等待同步事件。例如,任务可以最多等待队列中的数据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 ) );
	}
}

运行结果与上面的结果相同。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值