1 FreeRTOS特性
1、实时性
FreeRTOS “可以”配置成为一个硬(Hard)实时操作系统内核。要注意这里用的是“可以”,FreeRTOS 也可以配置为非实时型内核,甚至于部分任务是实时性的,部分不是。
2、任务数量
FreeRTOS对任务数没有限制,同一优先级也可以有多个任务,没有限制可以使用的优先级。
3、抢占式或协作式调度算法
任务调度既可以为抢占式也可以为协作式。采用协作式调度算法后,一个处于运行态任务除非主动要求任务切换(Yielding),否则是不会被调度出运行态的。
4、任务调度的时间点
调度器会在每次定时中断到来时决定任务调度,同时外部异步事件也会引起调度器任务调度。
5、调度算法
任务调度算法首先满足高优先级任务最先执行,当多于1个任务具有相同的高优先级时,采用round robin 算法调度。
6、任务间通信
FreeRTOS 支持队列和几种基本的任务同步机制。
(1)队列:任务间传递信息可以采用队列方式,FreeRTOS 实现的队列机制传递信息是采用传值方式,因此对于传递大量数据效率有些低。但可以通过传递指针的方式提高效率。中断处理函数中读写队列都是非阻塞型的。任务中读写队列可以为阻塞型也可以配置非阻塞型。当配置为阻塞型时可以指定一个阻塞的最大时间限(Timeout)。
(2)任务间同步:FreeRTOS 支持基本的信号量功能。FreeRTOS 采用队列来实现信号量的功能,可以认为一个值为n的信号量就是一个长度为n的队列,队列中每个元素的大小为0。这样的队列并不会浪费宝贵的内存空间。
(3)对于死锁(Deadlock)的处理:FreeRTOS 并没有实现一种可以完全避免死锁的机制。只是通过指定一个阻塞的最大时间限(Timeout)来减少死锁现象的发生。或者说是给出了当死锁现象发生时解锁的可能。当然能不能真的解锁要依赖于使用者的处理代码是否合适。
(4)临界区:FreeRTOS 采用开关中断的方式实现临界区保护。任务代码中临界区可以嵌套,FreeRTOS 会自动记录每个任务中临界区嵌套的层数。
(5)暂停调度:与进入临界区类似,FreeRTOS 可以通过暂时关闭任务调度来保证任务代码不被更高优先级的其他任务打断,与临界区不同,关闭任务调度并不会关闭中断,这样中断处理函数仍会照常的执行。
(6)内存分配:FreeRTOS 提供了多种内存动态分配的方法,具体程序中需要选择其中一种。最简单的内存分配方式提供了一种非常简单的固定内存分配算法,这种方式下只支持内存的分配,不支持分配内存的回收。因此,任务建立后就不能被删除。其他几种内存分配算法支持分配内存的回收,有的方法支持邻接内存块的合并,有些不支持。
7、有效的任务定时器
8、低功耗模式
(1)空闲任务的钩子函数实现的低功耗
通过空闲任务钩子函数(或称回调,hook, or call-back),可以直接在空闲任务中添
加应用程序相关的功能。空闲任务钩子函数会被空闲任务每循环一次就自动调用一次。
通常空闲任务钩子函数被用于:
●执行低优先级,后台或需要不停处理的功能代码。
●测试处系统处理裕量(空闲任务只会在所有其它任务都不运行时才有机会执行,所以测量出空闲任务占用的处理时间就可以清楚的知道系统有多少富余的处理时间)。
●将处理器配置到低功耗模式——提供一种自动省电方法,使得在没有任何应用功能需要处理的时候,系统自动进入省电模式。
FreeRTOS是通过在处理器处理空闲任务的时候将处理器设置为低功耗模式来降低能耗。一般会在空闲任务的钩子函数中执行低功耗相关处理,比如设置处理器进入低功耗模式(上面提到的三种方式)、关闭其他外设时钟、降低系统主频等等。
(2)Tickless 模式
FreeRTOS系统提供的低功耗模式,当处理器进入空闲任务周期以后就关闭系统节拍中断(滴答定时器中断),只有当其他中断发生或者其他任务需要处理的时侯处理器才会从低功耗模式中唤醒。
2 FreeRTOSConfig.h 文件
FreeRTOS 的配置基本是通过在 FreeRTOSConfig.h 中使用“#define” 这样的语句来定义宏定义实现的。 在 FreeRTOS 的官方 demo 中,每个工程都有一个 FreeRTOSConfig.h 文件,我们在使用的时候可以参考这个文件,甚至直接复制粘贴使用。
2.1 “INCLUDE_”开始的宏
使用“INCLUDE_”开头的宏用来表示使能或除能 FreeRTOS 中相应的 API 函数, 作用就是用来配置FreeRTOS 中的可选 API 函数的。比如当宏 INCLUDE_vTaskPrioritySet 设置为 0 的时候表 示不 能使用 函数 vTaskPrioritySet(), 当 设置为 1 的时候 就表 示可以 使用 函数vTaskPrioritySet()。这个功能其实就是条件编译
下面来看看“INCLUDE_”开始的都有哪些宏,它们的功能都是什么。
1、 INCLUDE_xSemaphoreGetMutexHolder
如果要使用函数 xQueueGetMutexHolder()的话宏 INCLUDE_xSemaphoreGetMutexHolder 必须定义为 1。
2、 INCLUDE_xTaskAbortDelay
如果要使用函数 xTaskAbortDelay()的话将宏 INCLUDE_xTaskAbortDelay 定义为 1。
3、 INCLUDE_vTaskDelay
如果要使用函数 vTaskDelay()的话需要将宏 INCLUDE_vTaskDelay 定义为 1。
4、 INCLUDE_vTaskDelayUntil
如果要使用函数 vTaskDelayUntil()的话需要将宏 INCLUDE_vTaskDelayUntil 定义为 1。
5、 INCLUDE_vTaskDelete
如果要使用函数 vTaskDelete()的话需要将宏 INCLUDE_vTaskDelete 定义为 1。
6、 INCLUDE_xTaskGetCurrentTaskHandle
如果要使用函数 xTaskGetCurentTaskHandle() 的话需要将 宏INCLUDE_xTaskGetCurrentTaskHandle 定义为 1。
7、 INCLUDE_xTaskGetHandle
如果要使用函数 xTaskGetHandle()的话需要将宏 INCLUDE_xTaskGetHandle 定义为 1。
8、 INCLUDE_xTaskGetIdleTaskHandle
如果要使用函 数 xTaskGetIdleTaskHandle() 的 话 需 要 将 宏INCLUDE_xTaskGetIdleTaskHandle 定义为 1。
9、 INCLUDE_xTaskGetSchedulerState
如果要使用函数 xTaskGetSchedulerState()的话需要将宏 INCLUDE_xTaskGetSchedulerState
定义为 1。
10、 INCLUDE_uxTaskGetStackHighWaterMark
如 果 要 使 用 函 数 uxTaskGetStackHighWaterMark() 的 话 需 要 将 宏INCLUDE_uxTaskGetStackHighWaterMark 定义为 1。
11、 INCLUDE_uxTaskPriorityGet
如果要使用函数 uxTaskPriorityGet()的话需要将宏 INCLUDE_uxTaskPriorityGet 定义为 1。
12、 INCLUDE_vTaskPrioritySet
如果要使用函数 vTaskPrioritySet()的话需要将宏 INCLUDE_vTaskPrioritySet 定义为 1。
13、 INCLUDE_xTaskResumeFromISR
如果要使用函数 xTaskResumeFromISR()的话需要将宏 INCLUDE_xTaskResumeFromISR 和
INCLUDE_vTaskSuspend 都定义为 1。
14、 INCLUDE_eTaskGetState
如果要使用函数 eTaskGetState()的话需要将宏 INCLUDE_eTaskGetState 定义为 1。
15、 INCLUDE_vTaskSuspend
如果要使用函数vTaskSuspend() 、 vTaskResume() 、 prvTaskIsTaskSuspended() 、xTaskResumeFromISR()的话宏 INCLUDE_vTaskSuspend 要定义为 1。
如 果 要 使 用 函 数 xTaskResumeFromISR() 的 话 宏 INCLUDE_xTaskResumeFromISR 和INCLUDE_vTaskSuspend 都必须定义为 1。
16、 INCLUDE_xTimerPendFunctionCall
如果要使用函数 xTimerPendFunctionCall()和 xTimerPendFunctionCallFromISR()的话宏INCLUDE_xTimerPendFunctionCall 和 configUSE_TIMERS 都必须定义为 1。
2.2 “config”开始的宏
“config”开始的宏和“INCLUDE_”开始的宏一样,都是用来完成 FreeRTOS 的配置和裁剪的,接下来我们就看一下这些“config”开始的宏。
1、 configAPPLICATION_ALLOCATED_HEAP
默认情况下FreeRTOS的堆内存是由编译器来分配 的 , 将 宏configAPPLICATION_ALLOCATED_HEAP 定义为 1 的话堆内存可以由用户自行设置,堆内存在 heap_1.c、 heap_2.c、 heap_3.c、 heap_4.c 和 heap_5.c 中有定义,具体在哪个文件取决于用户的选择哪种内存管理方式。
2、 configASSERT
断言,类似 C 标准库中的 assert()函数, 调试代码的时候可以检查传入的参数是否合理,FreeRTOS 内核中的关键点都会调用 configASSERT(x), 当 x 为 0 的时候说明有错误发生,使用断言的话会导致开销加大,一般在调试阶段使用。 configASSERT()需要在 FreeRTOSConfig.h 文件中定义, 如下实例:
#define configASSERT((x)) if((x)==0) vAssertCalled(__FILE_,__LINE__);
注意, vAssertCalled()函数需要用户自行去定义,可以是显示到 LCD 上的函数,也可以是通过串口打印出来的函数,例如:
//断言
#define vAssertCalled(char,int) printf("Error:%s,%d\r\n",char,int)
#define configASSERT(x) if((x)==0) vAssertCalled(__FILE__,__LINE__)
当参数 x 错误的时候就通过串口打印出发生错误的文件名和错误所在的行号,调试代码的可以使用断言,当调试完成以后尽量去掉断言, 防止增加开销!
3、 configCHECK_FOR_STACK_OVERFLOW
设置堆栈溢出检测,每个任务都有一个任务堆栈,如果使用函数 xTaskCreate()创建一个任务的话那么这个任务的堆栈是自动从 FreeRTOS 的堆(ucHeap)中分配的,堆栈的大小是由函数xTaskCreate()的参数 usStackDepth 来决定的。如果使用函数 xTaskCreateStatic()创建任务的话任务堆栈是由用户设置的,参数 pxStackBuffer 为任务堆栈,一般是一个数组。堆栈溢出是导致应用程序不稳定的主要因素, FreeRTOS 提供了两种可选的机制来帮助检测和调试堆栈溢出, 不管使用哪种机制都要设置宏configCHECK_FOR_STACK_OVERFLOW。如果使能了堆栈检测功能的话,即宏 configCHECK_FOR_STACK_OVERFLOW 不为 0,那么用户必须提供一个钩子函数(回调函数),当内核检测到堆栈溢出以后就会调用这个钩子函数,此钩子函数原型如下:
void vApplicationStackOverflowHook( TaskHandle_t xTask,char * pcTaskName );
参数 xTask 是任务句柄, pcTaskName 是任务名字,要注意的是堆栈溢出太严重的话可能会损毁这两个参数, 如果发生这种情况的话可以直接查看变量 pxCurrentTCB 来确定哪个任务发生了堆栈溢出。有些处理器可能在堆栈溢出的时候生成一个 fault 中断来提示这种错误,另外,堆栈溢出检测会增加上下文切换的开销,建议在调试的时候使用。
configCHECK_FOR_STACK_OVERFLOW==1,使用堆栈溢出检测方法 1。
上下文切换的时候需要保存现场,现场是保存在堆栈中的,这个时候任务堆栈使用率很可能达到最大值,方法一就是不断的检测任务堆栈指针是否指向有效空间,如果指向了无效空间的话就会调用钩子函数。 方法一的优点就是快!但是缺点就是不能检测所有的堆栈溢出。
configCHECK_FOR_STACK_OVERFLOW==2,使用堆栈溢出检测方法 2。
使用方法二的话在创建任务的时候会向任务堆栈填充一个已知的标记值,方法二会一直检测堆栈后面的几个 bytes(标记值)是否被改写,如果被改写的话就会调用堆栈溢出钩子函数,方法二也会使用方法一中的机制!方法二比方法一要慢一些,但是对用户而言还是很快的!方法二能检测到几乎所有的堆栈溢出, 但是也有一些情况检测不到,比如溢出值和标记值相同的时候。
3、 configCPU_CLOCK_HZ
设置 CPU 的频率。
4、 configSUPPORT_DYNAMIC_ALLOCATION
定义为 1 的话在创建 FreeRTOS 的内核对象的时候所需要的 RAM 就会从 FreeRTOS 的堆中动态的获取内存,如果定义为 0 的话所需的 RAM 就需要用户自行提供,默认情况下宏configSUPPORT_DYNAMIC_ALLOCATION 为 1。
5、 configENABLE_BACKWARD_COMPATIBILITY
FreeRTOS.h 中由一些列的#define 宏定义,这些宏定义都是一些数据类型名字,这些宏保证了你的代码从 V8.0.0之前的版本升级到最新版本的时候不需要做出修改,默认情况下宏configENABLE_BACKWARD_COMPATIBILITY 为 1。
6、 configGENERATE_RUN_TIME_STATS
设置为 1 开启时间统计功能, 相应的 API 函数会被编译, 为 0 时关闭时间统计功能。 如果宏 configGENERATE_RUN_TIME_STATS 为 1 的话还需要定义xia表中的宏。
宏 | 描述 |
---|---|
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() | 此宏用来初始化一个外设来作为时间统计的基准时钟。 |
portGET_RUN_TIME_COUNTER_VALUE()或portALT_GET_RUN_TIME_COUNTER_VALUE(Time) | 此宏用来返回当前基准时钟的时钟值。 |
7、 configIDLE_SHOULD_YIELD
此宏定义了与空闲任务(idle Task)处于同等优先级的其他用户任务的行为, 当为 0 的时候空闲任务不会为其他处于同优先级的任务让出 CPU 使用权。 当为 1 的时候空闲任务就会为处于同等优先级的用户任务让出 CPU 使用权, 除非没有就绪的用户任务, 这样花费在空闲任务上的时间就会很少, 但是这种方法也带了副作用,见图。
图中有三个用户任务: A、 B、 C,还有一个空闲任务 I,用户任务和空闲任务处于同一优先级,任务切换发生在 T0~T7 时刻。 T0~T1 之间的时间为一个时间片,从图中可以看出一开始任务 B、 C 都执行了一个完成的时间片,在 T2 时刻空闲任务 I 开始执行, I 任务运行了一段时间以后被 A 任务抢走了 CPU 使用权, A 任务运行到 T3 时刻发生任务切换, B 任务开始运行。可以看出其实任务 I 和任务 A 一起使用了一个时间片,所以任务 A 运行的时间就比其他任务少!
一般建议大家关闭这个功能,毕竟空闲任务用不了多少时间,而且现在的 MCU 性能都很强!
8、 configKERNEL_INTERRUPT_PRIORITY、
configMAX_SYSCALL_INTERRUPT_PRIORITY、
configMAX_API_CALL_INTERRUPT_PRIORITY
这三个宏和 FreeRTOS 的中断配置有关!
9、 configMAX_CO_ROUTINE_PRIORITIES
设置可以分配给协程的最大优先级, 也就是协程的优先级数。设置号以后协程的优先级可
以 从 0 到 configMAX_CO_ROUTINE_PRIORITIES-1 , 其 中 0 是 最 低 的 优 先 级 ,
configMAX_CO_ROUTINE_PRIORITIES-1 为最高的优先级。
10、 configMAX_PRIORITIES
设置任务的优先级数量,设置好以后任务就可以使用从 0 到 configMAX_PRIORITIES-1 的
优先级,其中 0 是最低优先级, configMAX_PRIORITIES-1 是最高优先级。 注意和 uC/OS 的区
别, uC/OS 中 0 是最高优先级!
11、 configMAX_TASK_NAME_LEN
设置任务名最大长度。
12、 configMINIMAL_STACK_SIZE
设置空闲任务的最小任务堆栈大小,以字为单位,不是字节。比如在 STM32 上设置为 100的话,那么真正的堆栈大小就是 100*4=400 字节。
13、 configNUM_THREAD_LOCAL_STORAGE_POINTERS
设置每个任务的本地存储指针数组大小, 任务控制块中有本地存储数组指针,用户应用程序可以在这些本地存储中存入一些数据。
14、 configQUEUE_REGISTRY_SIZE
设置可以注册的队列和信号量的最大数量,在使用内核调试器查看信号量和队列的时候需要设置此宏,而且要先将消息队列和信号量进行注册,只有注册了的队列和信号量才会再内核调试器中看到,如果不使用内核调试器的话此宏设置为 0 即可。
15、 configSUPPORT_STATIC_ALLOCATION
当此宏定义为 1,在创建一些内核对象的时候需要用户指定 RAM,当为 0 的时候就会自使用 heap.c 中的动态内存管理函数来自动的申请 RAM。
16、 configTICK_RATE_HZ
设置 FreeRTOS 的系统时钟节拍频率,单位为 HZ,此频率就是滴答定时器的中断频率,需要使用此宏来配置滴答定时器的中断,前面在讲 delay.c 文件的时候已经说过了。 为了兼容 ST最新的 HAL 库,我们将此宏设置为 1000,周期就是 1ms。
17、 configTIMER_QUEUE_LENGTH
此宏是配置 FreeRTOS 软件定时器的, FreeRTOS 的软件定时器 API 函数会通过命令队列向软件定时器任务发送消息,此宏用来设置这个软件定时器的命令队列长度。
18、 configTIMER_TASK_PRIORITY
设置软件定时器任务的任务优先级。
19、 configTIMER_TASK_STACK_DEPTH
设置定时器服务任务的任务堆栈大小。
20、 configTOTAL_HEAP_SIZE
设置堆大小,如果使用了动态内存管理的话, FreeRTOS 在创建任务、信号量、队列等的时候 就 会 使 用 heap_x.c(x 为 1~5) 中 的 内 存 申 请 函 数 来 申 请 内 存 。 这 些 内 存 就 是 从 堆ucHeap[configTOTAL_HEAP_SIZE]中申请的,堆的大小由 configTOTAL_HEAP_SIZE 来定义。
21、 configUSE_16_BIT_TICKS
设置系统节拍计数器变量数据类型,系统节拍计数器变量类型为 TickType_t,当configUSE_16_BIT_TICKS 为 1 的时候 TickType_t 就是 16 位的,当 configUSE_16_BIT_TICKS为 0 的话 TickType_t 就是 32 位的。
22、 configUSE_APPLICATION_TASK_TAG
此 宏 设 置 为 1 的 话 函 数 configUSE_APPLICATION_TASK_TAGF() 和xTaskCallApplicationTaskHook()就会被编译。
23、 configUSE_CO_ROUTINES
此宏为 1 的时候启用协程,协程可以节省开销,但是功能有限,现在的 MCU 性能已经非常强大了,建议关闭协程。
24、 configUSE_COUNTING_SEMAPHORES
设置为 1 的时候启用计数型信号量,相关的 API 函数会被编译。
25、 configUSE_DAEMON_TASK_STARTUP_HOOK
当宏 configUSE_TIMERS 和 configUSE_DAEMON_TASK_STARTUP_HOOK 都为 1 的时需
要定义函数 vApplicationDaemonTaskStartupHook(),函数原型如下:
void vApplicationDaemonTaskStartupHook( void )
26、 configUSE_IDLE_HOOK
为 1 时使用空闲任务钩子函数, 用户需要实现空闲任务钩子函数,函数的原型如下:void vApplicationIdleHook( void )
27、 configUSE_MALLOC_FAILED_HOOK
为 1 时使用内存分配失败钩子函数,用户需要实现内存分配失败钩子函数,函数原型如下;void vApplicationMallocFailedHook( void )
28、 configUSE_MUTEXES
为 1 时使用互斥信号量,相关的 API 函数会被编译。
29、 configUSE_PORT_OPTIMISED_TASK_SELECTION
FreeRTOS 有两种方法来选择下一个要运行的任务,一个是通用的方法,另外一个是特殊的方法, 也就是硬件方法, 使用 MCU 自带的硬件指令来实现。
通用方法:
● 当宏 configUSE_PORT_OPTIMISED_TASK_SELECTION 为 0,或者硬件不支持的时候。
● 希望所有硬件通用的时候。
● 全部用 C 语言来实现,但是效率比特殊方法低。
● 不限制最大优先级数目的时候。
特殊方法:
● 不是所有的硬件都支持。
● 当宏 configUSE_PORT_OPTIMISED_TASK_SELECTION 为 1 的时候。
● 硬件拥有特殊的指令,比如计算前导零(CLZ)指令。
● 比通用方法效率高。
● 会限制优先级数目,一般是 32 个。
STM32有计算前导零的指令,所以我们可以使用特殊方法 ,即将宏configUSE_PORT_OPTIMISED_TASK_SELECTION 定义为 1。 计算前导零的指令在 uC/OSIII 也用到了,也是用来查找下一个要运行的任务的。
30、 configUSE_PREEMPTION
为 1 时使用抢占式调度器,为 0 时使用协程。如果使用抢占式调度器的话内核会在每个时钟节拍中断中进行任务切换,当使用协程的话会在如下地方进行任务切换:
● 一个任务调用了函数 taskYIELD()。
● 一个任务调用了可以使任务进入阻塞态的 API 函数。
● 应用程序明确定义了在中断中执行上下文切换。
31、 configUSE_QUEUE_SETS
为 1 时启用队列集功能。
32、 configUSE_RECURSIVE_MUTEXES
为 1 时使用递归互斥信号量,相关的 API 函数会被编译。
33、 configUSE_STATS_FORMATTING_FUNCTIONS
宏 configUSE_TRACE_FACILITY 和 configUSE_STATS_FORMATTING_FUNCTIONS 都为
1 的时候函数 vTaskList()和 vTaskGetRunTimeStats()会被编译。
34、 configUSE_TASK_NOTIFICATIONS
为 1 的时候使用任务通知功能, 相关的 API 函数会被编译,开启了此功能的话每个任务会多消耗 8 个字节。
35、 configUSE_TICK_HOOK
为 1 时使能时间片钩子函数,用户需要实现时间片钩子函数,函数的原型如下:void vApplicationTickHook( void )
36、 configUSE_TICKLESS_IDLE
为 1 时使能低功耗 tickless 模式。
37、 configUSE_TIMERS
为 1 时使用软件定时器,相关的 API 函数会被编译,当宏 configUSE_TIMERS 为 1 的话,那 么 宏 configTIMER_TASK_PRIORITY 、 configTIMER_QUEUE_LENGTH 和configTIMER_TASK_STACK_DEPTH 必须定义。
38、 configUSE_TIME_SLICING
默认情况下, FreeRTOS 使用抢占式调度器,这意味着调度器永远都在执行已经就绪了的最高优先级任务, 优先级相同的任务在时钟节拍中断中进行切换, 当宏 configUSE_TIME_SLICING为 0 的时候不会在时钟节拍中断中执行相同优先级任务的任务切换,默认情况下宏configUSE_TIME_SLICING 为 1。
39、 configUSE_TRACE_FACILITY
为 1 启用可视化跟踪调试, 会增加一些结构体成员和 API 函数。FreeRTOS 的配置文件基本就这些,还有一些其他的配置宏由于使用的比较少这里并没有列出来,这些配置具体使用到的时候在具体查看就行了。