1. 简介:
- 在 FreeRTOS 中没有线程和进程的区别,只有一个被翻译成任务的程序,相当于进程的概念,拥有独立的栈空间。
- 对于实时性,可以分为 软实时、硬实时:桌面电脑的输入处理可以看做是软实时,当键盘按下在某个时间内没有做出相应,只是做出提示,提示超时,只会给人一种反应慢的印象,不能说不能用。硬实时则是像汽车上的安全气囊,必须在特定时间内完成,一旦失败,就算是不能使用的标志。
- FreeRTOS 内核支持优先级调度算法,CPU 总是让处于就绪态和优先级高的任务先运行。
- FreeRTOS 内核同时支持轮询调度算法, CPU 对享有相同优先级的任务,平分 CPU 时间。
- FreeRTOS 内核可以根据用户需要设置成可剥夺型或不可剥夺型内核,可剥夺指的是高优先级的任务能剥夺正在执行的低优先级的任务,可以保证系统满足实时性的要求。不可剥夺,遇到同时发生的任务会一直等待着先发生的任务完成,可以提高 CPU 的运行效率。
- FreeRTOS 对系统任务的数量没有限制。
- 任务因为自己的优先级而一直得不到运行的状态叫做被饿死(starve)
2. 函数:
(1)变量类型定义:
- /* Type definitions. */
- #define portCHAR char
- #define portFLOAT float
- #define portDOUBLE double
- #define portLONG long
- #define portSHORT short
- #define portSTACK_TYPE uint32_t
- #define portBASE_TYPE long
- typedef portSTACK_TYPE StackType_t;
- typedef long BaseType_t;
- typedef unsigned long UBaseType_t;
(2)创建任务:
需要包含的头文件:#include "task.h"
xTaskCreate函数原型:
- BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
- const char * const pcName,
- const uint16_t usStackDepth,
- void * const pvParameters,
- UBaseType_t uxPriority,
- TaskHandle_t * const pxCreatedTask )
参数:
①pxTaskCode,是一个任务的函数指针
typedef void (*TaskFunction_t)( void * );
②pcName,任务的名字,有最大长度限制,包括 ‘\0’结束符,最大长度是 config_MAX_TASK_NAME_LEN,如果超过最大,会自动截断
③usStackDepth,指定任务的栈的大小,单位是字(word)= 4 字节,不是字节(byte),用户通过定义 configMINIMAL_STACK_SIZE 来决定空闲任务用的栈空间大小
④pvParameters,传递到创建任务的函数中的参数值
⑤uxPriority,任务的优先级,取值[0,configMAX_PRIORITIES-1],没有最大限制,configMAX_PRIORITIES 变量值时系统之前设置好的,如果优先级取值大于区间,将会取区间里的最大值
⑥pxCreatedTask,用于传出任务的句柄,可以用来改变任务的优先级,或者删除任务,如果用不到,用户可以设为 NULL
返回值:
pdTRUE,创建成功
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY,创建失败,一般是由于内存堆空间不足所致
(3)作为任务的函数的原型:
void func(void * pvParameters);
【1】函数中没有return
【2】一个任务函数可以用来创建若干个任务,创建的每个任务都有独立的栈空间
【3】函数模板:
- func(void * pvParameters)
- {
- ...
- for(;;)
- {
- }
- vTaskDelete(NULL);/* 一般不会执行到这里,传入 NULL 表示删除当前任务 */
- }
(4)删除任务:
需要包含的头文件:task.h
vTaskDelete函数原型:
- void vTaskDelete( TaskHandle_t xTaskToDelete )
参数:
xTaskToDelete,要删除的任务,是NULL时,删除自己
说明:
【1】需要配置 INCLUDE_vTaskDelete = 1,才能使用这个函数,从 RTOS 实时内核管理中移除任务,要删除的任务将从就绪、封锁、挂起事件列表中移除。
【2】空闲任务负责释放内核分配的内存,任务自己占用的内存需要应用程序自己显示的释放。
(5)任务延时:
需要包含头文件:task.h
vTaskDelay函数原型:
- void vTaskDelay( const TickType_t xTicksToDelay )
参数:
xTicksToDelay,延时多少个心跳周期,延迟的任务进入阻塞态,经过指定的心跳周期后,转移到就绪态,将毫秒单位转换成心跳周期单位使用常量 portTICK_RATE_MS ,比如要延时 250ms,则 xTicksToDelay = 250 / portTICK_RATE_MS
(6)周期性延时:
需要包含头文件:task.h
vTaskDelayUntil函数原型:
- void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
- const TickType_t xTimeIncrement )
参数:
pxPreviousWakeTime,用于保存任务上一次离开阻塞态的时刻,这个时刻用于作为参考点计算任务下一次离开阻塞态的时刻,这个值自动更新,只需要付初始值即可,一般是 pxPreviousWakeTime = xTaskGetTickCount()
xTimeIncrement,循环周期时间,要延时 250ms,则 xTimeIncrement = 250 / portTICK_RATE_MS
说明:
【1】需要配置 INCLUDE_vTaskDelayUntil = 1,才能使用这个函数
【2】使用的时候,需要将这个函数放在循环中例如for(;;){vPrintString(pcTaskName);vTaskDelayUntil(...);}
【3】执行完任务后,任务本身进入阻塞态,等待时间的到达
【4】既然要作为周期任务,优先级可能要设置成最高才能实现
(7)挂起任务:
需要包含头文件:task.h
vTaskSuspend函数原型:
- void vTaskSuspend( TaskHandle_t xTaskToSuspend )
参数:
xTaskToSuspend,要挂起的任务句柄,是NULL挂起调用此函数的任务
说明:
【1】需要配置 INCLUDE_vTaskSuspend = 1 ,才能使用这个函数
【2】挂起的任务不再占用控制器的处理时间
(8)唤醒挂起的任务:
需要包含的头文件:task.h
vTaskResume函数原型:
- void vTaskResume( TaskHandle_t xTaskToResume )
参数:
xTaskToResume,要唤醒的任务句柄,唤醒(7)的任务
说明:
【1】需要配置 INCLUDE_vTaskSuspend = 1 才能使用此函数
(9)从中断唤醒挂起的任务:
需要包含的头文件:task.h
xTaskResumeFromISR函数原型:
- BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume )
参数:
xTaskToResume,要唤醒的任务句柄
返回值:
pdTRUE,成功
pdFALSE,失败
说明:
【1】需要配置 INCLUDE_xTaskResumeFromISR = 1 和 INCLUDE_vTaskSuspend = 1 才能使用此函数
【2】当唤醒成功将引起上下文切换,失败用于 ISR 确定是否上下文切换
(10)为任务分配标签值:
需要包含的头文件:task.h
vTaskSetApplicationTaskTag函数原型:
- void vTaskSetApplicationTaskTag( TaskHandle_t xTask, TaskHookFunction_t pxHookFunction )
参数:
xTask,要分配标签的任务,NULL是调用函数的任务
pxHookFunction,分配给任务的标签值
说明:
【1】需要配置 configUSE_APPLICATION_TASK_TAG = 1 才能使用此函数
【2】分配的标签只对应用程序有用,内核不使用
(11)启动实时内核:
需要包含的头文件:task.h
vTaskStartScheduler函数原型:
- void vTaskStartScheduler( void )
说明:
【1】当此函数调用时,还有一个空闲任务自动被创建,这个函数调用成功后不会返回,直到执行任务调用 vTaskEndScheduler
【2】函数调用失败的可能原因是可供给空闲任务的 RAM 不足
(12)停止实时内核运行:
需要包含头文件:task.h
vTaskEndScheduler函数原型:
- void vTaskEndScheduler( void )
说明:
【1】所有创建的任务将自动删除,并且多任务将停止
【2】此函数导致所有由内核分配的资源释放,但是由应用程序分配的资源需要应用程序自己去释放
(13)挂起所有活动的实时内核,同时允许中断(包括内核滴答中断)
vTaskSuspendAll函数原型:
- void vTaskSuspendAll( void )
说明:
【1】任务调用此函数后,此任务将继续执行,不会有任何被切换的危险,直到调用 xTaskResumeAll() 函数重启内核
【2】当调用了此函数后,之后将不能在出现影响上下文切换的的函数(比如:vTaskDelayUntil(),xQueueSend()等)
3. 任务优先级:
- 优先级最大值由配置文件 FreeRTOSConfig.h 中 configMAX_PRIORITIES 变量设置,这个值越大,内核花销的内存空间就越大。
- 优先级0最小
- 对优先级操作的函数:
(1)任务优先级的获取:
需要包含的头文件是:task.h
uxTaskPriorityGet函数原型:
- UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask )
参数:
xTask,需要操作的任务,NULL表示自己
返回值:
任务的优先级
说明:
【1】此函数的使用需要配置 INCLUDE_uxTaskPriorityGet = 1
(2)任务优先级的重新设置:
需要包含头文件:task.h
vTaskPrioritySet函数原型:
- void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority )
参数:
xTask,任务
uxNewPriority,要设置的优先级
说明:
【1】需要设置 INCLUDE_vTaskPrioritySet = 1
【2】如果更改的优先级高于当前的任务的优先级,上下文的切换发生在此函数返回前
4. 任务调动说明:
要能够选择下一个运行的任务,调度器需要在每个时间片的结束时刻运行自己本身。一个称为心跳(tick,有些地方被称为时钟滴答,本文中一律称为时钟心跳)中断的周期性中断用于此目的。时间片的长度通过心跳中断的频率进行设定,心跳中断频率由FreeRTOSConfig.h 中的编译时配置常量 configTICK_RATE_HZ 进行配置。比如说,如果 configTICK_RATE_HZ 设为 1000(HZ),则时间片长度为 1ms。
心跳计数(tick count)值表示的是从调度器启动开始,心跳中断的总数,并假定心跳计数器不会溢出。用户程序在指定延迟周期时不必考虑心跳计数溢出问题,因为时间连
贯性在内核中进行管理。
将调度器本身的执行时间在整个执行流程中体现出来的过程如下图中红色部分
任务的运行状态被分为了 Running、Ready、Blocked、Suspended。
Running可以被翻译成 运行状态
这种状态表示内核正在执行的状态
Ready被翻译成 准备好状态
这种状态表示任务准备好了,只要由内核调用,就能当下执行
Blocked被翻译成 阻塞状态
处于等待某个事件的任务叫做阻塞状态,处于阻塞等待的任务可以被两类事件唤醒:时间超时、其他任务的同步事件,处于此种状态的任务不参与调度。
Suspended被翻译成 暂停状态
处于暂停状态的任务不参与调度,一般应用程序用不到这个状态。
任务所有状态构成的状态机如下:
5. 空闲任务与他的钩子函数:
当调用 vTaskStartScheduler() 函数后,空闲任务就被自动的创建了,空闲任务是一个非常短小的循环,其任务优先级是最小的 0,其他任务可以设置成和空闲任务相同的优先级
通过空闲任务钩子函数,可以直接在空闲任务中添加应用程序相关的功能,空闲任务钩子函数会被空闲任务每循环一次就自动调用一次。空闲任务中可以做的事情可以但不完全是:可以测量设计的系统有多少富裕的处理时间、在系统空闲的时候让系统自动进入省电模式。
空闲任务钩子函数一定要注意的是,当应用程序用到vTaskDelete() 函数,一定要能尽快返回,因为在任务被删除后,空闲任务负责回收资源。
空闲任务钩子函数原型:
- void vApplicationIdleHook(void)
说明:
【1】需要配置 configUSE_IDLE_HOOK = 1
【2】函数的名字不能变,一定要是上边函数的名字
【3】当配置了变量/宏,可以使用此函数后,在main.c中直接声明,并实现其中的功能就可以使用了
6. 优先级的实例图表:
(1)TaskDelay 函数的图表表示:
当我们同时创建了两个相同的任务,然后任务中都有一段250ms的vTaskDelay延时,任务2优先级2,任务1优先级1,同时运行,则执行的图表表示如下:
【1】表中我们可以看到,任务优先级高的却接近了空闲状态,空闲状态下是时间线。
【2】vTaskDelay延时就相当于放弃任务被调度的权利多长时间。
(2)运行中重新设置优先级的程序和图表表示:
行为说明:
- 任务1 创建在最高优先级,以保证其可以最先运行。任务1 首先打印输出两个字符串,然后将任务2 的优先级提升到自己之上。
- 任务2 一旦拥有最高优先级便启动执行(进入运行态)。由于任何时候只可能有一个任务处于运行态,所以当任务2 运行时,任务1 处于就绪态。
- 任务2 打印输出一个信息,然后把自己的优先级设回低于任务 1的初始值。
- 任务2 降低自己的优先级意味着任务1 又成为具有最高优先级的任务,所以任务1 重新进入运行态,任务2 被强制切入就绪态。
程序实现如下:
- void vTask1( void *pvParameters )
- {
- unsigned portBASE_TYPE uxPriority;
- uxPriority = uxTaskPriorityGet( NULL ); // 返回自己的优先级
- for( ;; )
- {
- vPrintString( "Task1 is running\r\n" );
- vPrintString( "About to raise the Task2 priority\r\n" );
- vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );
- }
- }
- void vTask2( void *pvParameters )
- {
- unsigned portBASE_TYPE uxPriority;
- uxPriority = uxTaskPriorityGet( NULL );
- for( ;; )
- {
- vPrintString( "Task2 is running\r\n" );
- vPrintString( "About to lower the Task2 priority\r\n" );
- vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
- }
- }
- xTaskHandle xTask2Handle;
- int main( void )
- {
- xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
- xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, &xTask2Handle );
- vTaskStartScheduler();
- for( ;; );
- }
图表表示如下:
【1】从图表中可以看出任务高的优先级在高的位置,这种方式,我们理解的更容易
【2】对任务的优先级的再次更改需要任务创建时的任务句柄(最后一个参数)
【3】到这里我们可以看到,图表好的形式如下:
(3)周期性延时的实现:
行为说明:
- 在优先级1 上创建两个任务,这两个任务只是不停地打印输出字符串。
- 第三个任务创建在优先级 2 上,高于另外两个任务的优先级。这个任务虽然也是打印输出字符串,但它是周期性的,调用了vTaskDelayUntil(),在每两次打印之间让自己处于阻塞态。
程序实现:
- void vContinuousProcessingTask( void *pvParameters )
- {
- char *pcTaskName;
- pcTaskName = ( char * ) pvParameters;
- for( ;; )
- {
- vPrintString( pcTaskName );
- }
- }
- void vPeriodicTask( void *pvParameters )
- {
- portTickType xLastWakeTime;
- xLastWakeTime = xTaskGetTickCount();
- for( ;; )
- {
- vPrintString( "Periodic task is running\r\n" );
- vTaskDelayUntil( &xLastWakeTime, ( 10 / portTICK_RATE_MS ) );
- }
- }
图表表示如下:
【1】两个相同优先级的任务在图中也分了高低,是为了好区分
【2】此图表表示的不是简单的创建两个相同的打印的函数,至少,保证了打印能完成才去调度,在图中红色段也至少含有几个tick