《Mastering the FreeRTOS Real Time Kernel》读书笔记(2)任务管理
3.任务管理
如何为每个任务分配处理时间,如何选择在任何给定时间执行何种任务,任务优先级,任务状态。
3.2 任务功能
每个任务必须返回void,并接受一个void类型指针。这些任务一般会写成一个无限循环,由内核来调度,完成任务安排,创建和删除。
3.3 顶层任务状态
由于一般单片机处理器为单核单线程,实际同时只能执行一个任务,被激活的任务状态只有运行和未运行两种,从运行状态切换成休眠(未运行)状态时,其状态(变量、程序指针等)会被保存,以便于恢复。
切换任务状态只能由FreeRTOS的调度程序执行,不能由用户操作。
3.4 创建任务
使用FreeRTOS的xTaskCreate()API函数创建任务。这可能是所有API函数中最复杂的一个,因此不幸的是,它是第一个遇到的,实在是太惨了,欲练此功,挥刀自宫。
(V9.0.0还包括xTaskCreateStatic()函数,该函数分配在编译时静态创建任务所需的内存)
BaseType_t xTaskCreate(TaskFunction_t pvTaskCode,
const char * const pcName,
uint16_t usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask);
1.pvTaskCode:只是指向实现任务的函数的指针(实际上,只是函数的名称)。
2.pcName:任务的描述性姓名,通过人类可读的名称识别任务比试图通过句柄识别任务简单得多。
3.usStackDepth:每个任务都有自己唯一的栈,在创建任务时内核会将其分配给任务。usStackDepth值告诉内核制作栈的大小。大小单位是由栈的位宽决定的。总大小不超过65535。
4.pvParameters:任务函数接受指向void(void)的指针类型的参数。分配给pvParameters的值是传递到任务中的值。
5.uxPriority:执行任务的优先级。
6.pxCreatedTask:可用于向正在创建的任务传递句柄。然后可以使用此句柄引用API调用中的任务,例如,更改任务优先级或删除任务。不使用的话,可以设置为NULL。
返回值:pdPASS或者pdFAIL
例子1
创建两个任务,设置相同延时来打印信息。
xTaskCreate( vTask1, /* Pointer to the function that implements the task. */
"Task 1",/* Text name for the task. This is to facilitate
debugging only. */
1000, /* Stack depth - small microcontrollers will use much
less stack than this. */
NULL, /* This example does not use the task parameter. */
1, /* This task will run at priority 1. */
NULL ); /* This example does not use the task handle. */
创建完后使用vTaskStartScheduler();
任务2也可以在任务1中被创建。
例子2
相同结构的任务可以通过改变传入的参数,来实例化出不同的任务,通过设置4.*pvParameters来改变传入的参数。比如如上例,修改不同的打印内容。
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
volatile uint32_t ul; /* volatile to ensure ul is not optimized away. */
/* The string to print out is passed in via the parameter. Cast this to a
character pointer. */
pcTaskName = ( char * ) pvParameters;
………………………………
}
int main(void)
{
xTaskCreate( vTaskFunction,
"Task 1",
1000,
(void*)pcTextForTask1, /* Pass the text to be printed into the
task using the task parameter. */
1,
NULL );
…………………………
}
3.5 任务优先级
可用优先级的范围为0到(configMAX_priorities–1)。
FreeRTOS调度器可以使用以下两种方法中的一种来决定哪个任务将处于运行状态。configMAX_PRIORITIES的最大值取决于使用的方法:
1.通用方法:FreeRTOS不会限制可设置configMAX_PRIORITIES的最大值。然而,始终建议将configMAX_PRIORITIES值保持在必要的最小值,因为其值越高,消耗的RAM越多,最坏情况下的执行时间也就越长。
如果在FreeRTOSConfig.h中将configUSE_PORT_OPTIMISED_TAK_SELECTION设置为0,或者如果未定义configUSE_PORT_OPTIMIED_TAK_SOLECTION,或者如果通用方法是为正在使用的FreeRTOS端口提供的唯一方法,则将使用通用方法。
2.架构优化方法:架构优化方法使用少量汇编代码,并且比通用方法更快。configMAX_PRIORITIES设置不会影响最坏情况下的执行时间。如果使用架构优化方法,则configMAX_PRIORITIES不能大于32。与通用方法一样,建议将configMAX_PRIORITIES保持在必要的最小值,因为它的值越高,消耗的RAM就越多。
如果在FreeRTOSConfig.h中将configUSE_PORT_OPTIMISED_TAK_SELECTION设置为1,则将使用体系结构优化方法。并非所有FreeRTOS端口都提供体系结构优化的方法。
3.6 时间管理和Tick中断
调度程序在FreeRTOS的心跳时钟Tick中断中执行,检测各个任务的优先级,完成调度。两个Tick中断之间的时间称为Tick周期,一个时间片等于一个Tick周期,如图11,上例1的图。
任务只能在Tick中断时被切换,configTICK_RATE_HZ的最佳值取决于正在开发的应用程序,尽管通常值为100,(10ms)。
FreeRTOS总是以Tick周期的倍数指定时间,可用pdMS_TO_TICKS()宏将以毫秒为单位指定的时间转换为以tick为单位的时间。如果tick周期不是1ms,建议pdMS_TO_TICKS()宏使用以毫秒为单位指定时间。比如使用vTaskDelay()的时候;
例子3:试验优先级
在例子1中略加修改,将任务1的优先级设置为1,任务2的优先级设置为2,由于任务中没有设置例如vTaskDelay()的内容,所以任务2会一直抢占任务1,任务1没有时间片执行。
3.7 扩展未运行(休眠)状态
可以利用休眠,来使低优先级的任务也能够被执行。这时,单纯的未运行状态已经不能满足需求,需要进行扩展。
阻塞状态
任务可以进入阻止状态以等待两种不同类型的事件:
1。时间(与时间相关)事件——该事件要么是延迟期到期,要么是达到的绝对时间。例如,一个任务可能会进入“阻止”状态,等待10毫秒过去。
2.同步事件——事件源自另一个任务或中断。例如,任务可能会进入“阻止”状态,以等待数据到达队列。同步事件涵盖了广泛的事件类型。
FreeRTOS的队列、二进制信号量、计数信号量、互斥、递归互斥、事件组和直接到任务通知都可以用于创建同步事件。
挂起状态
进入Suspended状态的唯一方法是通过调用vTaskSuspend()API函数,唯一方法是调用vTaskResume()或xTaskResumeFromISR()API函数。大多数应用程序不使用挂起状态。处于挂起状态的任务对任务调度不可用。
就绪状态
处于未运行状态,但未被阻止或挂起,该状态的任务称为处于就绪状态。
例子4:使用阻塞制作一个延时
纠正上述例子中使用循环来制作延时的做法,将轮询空循环替换为对vTaskDelay()API函数的调用。
void vTaskDelay( TickType_t xTicksToDelay );
输入参数以Tick周期为单位。vTaskDelay(100)代表在接下来100个Tick周期中,该任务被阻塞。调用vTaskDelay(pdMS_TO_TICKS(100))将导致调用任务在100毫秒内保持“阻塞”状态。
vTaskDelayUntil()
当需要固定的执行周期(您希望任务以固定的频率定期执行)时,应该使用该函数,因为调用任务被取消阻塞的时间是绝对的,而不是相对于函数被调用的时间,如vTaskDelay()。
void vTaskDelayUntil( TickType_t * pxPreviousWakeTime, TickType_t xTimeIncrement );
pxPreviousWakeTime就是任务开始的时间戳,xTimeIncrement同vTaskDelay()的输入参数。
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
TickType_t xLastWakeTime;
pcTaskName = ( char * ) pvParameters;
xLastWakeTime = xTaskGetTickCount();
for( ;; )
{
vPrintString( pcTaskName );
vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS( 250 ) );
}
}
例子6:结合阻塞和非阻塞的任务
就是经典的,优先级高但是执行频率不高的任务,抢占低优先级的连续任务,的场景。这也是我们为什么要使用RTOS要达成的效果。
3.8 空闲任务与空闲任务钩子
vTaskStartScheduler()时会自动创建空闲任务。空闲任务优先级为0(最低),只做循环。FreeRTOSConfig.h中的configIDLE_SHOULD_YIELD编译时配置常量可用于防止空闲任务消耗处理时间,而这些处理时间将更有效地分配给应用程序任务。
空闲任务还有一些特殊用途,比如在一个任务被删除时清理内核资源等?。
空闲任务钩子函数
void vApplicationIdleHook( void );
在空闲任务的循环中,每经过一次循环就会自动调用的函数。常见用途有:
1.执行一些最低优先级的任务。(一般不会了)
2.测量备用处理能力的数量。(只有当所有优先级较高的应用程序任务都没有工作要执行时,空闲任务才会运行;因此,测量分配给空闲任务的处理时间可以清楚地指示空闲处理时间。)
3.将处理器置于低功耗模式,无论何时无需执行应用程序处理,都可以提供一种简单而自动的省电方法。但是第10章中的低功耗支持内容中的模式有更好的节电效果。
注意:
1.空闲任务挂钩函数决不能试图阻止或挂起。
2.如果应用程序使用vTaskDelete()API函数,则Idle任务钩子必须始终在合理的时间段内返回到其调用方。这是因为空闲任务负责在删除任务后清理内核资源。如果空闲任务永久保持在空闲钩子函数功能中,则无法进行此清理。
例子7:定义一个空闲任务钩子函数
例子是一个使用空闲任务钩子函数来简单测量备用处理能力的数量。可以看系统的占用率,还剩多少性能可用。
3.9 改变一个任务的优先级
void vTaskPrioritySet( TaskHandle_t pxTask, UBaseType_t uxNewPriority );
1.pxTask:优先级被修改的任务的句柄。本任务可以通过传递NULL来更改自己的优先级。
2.uxNewPriority:将要被设置的目标优先级。
可以调用uxTaskPriorityGet()来获得任务的优先级。
UBaseType_t uxTaskPriorityGet( TaskHandle_t pxTask );
例子8为改变任务的优先级,不再讨论。
3.10 删除一个任务
void vTaskDelete( TaskHandle_t pxTaskToDelete );
一个任务删除另一个任务后,当时就会释放内存。如果一个任务自己删除了自己,则退出时,没有程序为它释放内存。这个任务就做到了空闲任务身上。在运行到空闲任务时,它会去收尸,并打扫现场,迎接下一个要来申请内存的顾客。
例子9为删除一个任务的实际操作流程,不再讨论。
3.11 线程的本地存储
没有内容
3.12 调度算法
可以使用configUSE_PREEMPTION和configUSE_TIME_SLICING配置常量来更改算法。这两个常量都在FreeRTOSConfig.h中定义。第三个配置常量configUSE_TICKLESS_IDLE也会影响调度算法,因为它的使用可能会导致tick中断在很长一段时间内完全关闭,一般用于低功耗,本节为默认值0。
FreeRTOS遵循简单的轮流调度算法逻辑(Round Robin Scheduling),但并不能保证相同优先级的任务有相同的时间。
可配置的算法大约有三种,如下:
基于时间片的优先级抢占式调度算法(最常用)
configUSE_PREEMPTION = 1
configUSE_TIME_SLICING = 1
特点:
1.固定优先级:控制器本身不会更改分配给任务的优先级,但也不会阻止任务本身更改其自身或其他任务的优先级。
2.抢占规则:有优先级更高的任务就绪时,正在运行的任务会切换为就绪,使更高优先级的任务先运行。
3.有时间片控制:同优先级的任务会按照时间片规定的时间,依次运行。每个时间切片结束时选下一个同优先级任务以进入“运行”状态。
注意:configIDLE_SHOULD_YIELD设置为1时,如果有其他空闲优先级任务处于就绪状态,则空闲任务将在其循环的每次迭代中产生(自愿放弃其分配的时间片的剩余部分)。
无时间片的优先级抢占式调度算法
configUSE_PREEMPTION = 1
configUSE_TIME_SLICING = 1
优先级和抢占规则和上面的算法一样,但是同优先级的任务不能交替或者依次执行。与使用时间切片时相比,不使用时间切片的情况下任务上下文切换更少。因此,关闭时间切片可以减少调度器的处理开销。然而,关闭时间切片也会导致同等优先级的任务获得截然不同的处理时间,如图29所示。因此,在没有时间切片的情况下运行调度程序被认为是一种高级技术,只有经验丰富的用户才能使用。
同优先级的任务将不会被判断,是否需要运行,以减小开销。
合作调度算法(已不再更新)
configUSE_PREEMPTION = 0
configUSE_TIME_SLICING = any value
当使用协作调度程序时,只有当运行状态任务进入阻止状态,或者运行状态任务通过调用taskYIELD()显式生成(手动请求重新调度)时,才会发生上下文切换。任务从不被预先占用,因此不能使用时间切片。
简而言之,只能手动调度,不再自动调度。