一、Free RTOS介绍
目录
RTOS从名字上可以分为free和RTOS两部分。free是免费的意思,RTOS全称是Real Time Operation System,译为实时操作系统。那FreeRTOS的意思就是“免费的实时操作系统”。RTOS不是指某一个特定的系统,而是一类系统。比如uC/OS,FreeRTOS,RTX,RT-Thread等都属于RTOS类操作系统。
操作系统允许多个任务同时运行,这个叫做多任务,实际上,一个处理器核心在某一时刻只能运行一个任务。操作系统中任务调度器的责任就是决定在某一时刻究竟运行哪个任务,任务调度在各个任务之间的切换非常快!这就给人们造成了同一时刻有多个任务同时运行的错觉。
操作系统的分类方式可以由任务调度器的工作方式决定,比如有的操作系统给每个任务分配相同的运行时间,时间到了就轮到下一个任务,Unix操作系统就是这样。RTOS的任务调度器被设计为可预测的,而这正是嵌入式实时操作系统所需要的,实时环境中要求操作系统必须对一个事件作出实时的响应,因此系统任务调度器的行为必须是可预测的。像FreeRTOS这种传统的RTOS类操作系统是由用户给每个任务分配一个任务优先级,任务调度器就可以根据此优先级来决定下一刻应该运行哪个任务。
特点:
- 用户无需关心时间信息,内核负责计时,并由相关的API完成,从而使得用户的应用程序代码结构更简单。
- 模块化、可拓展性强,由于第一点原因,程序性能不易受底层硬件更改的影响。并且,各个任务是独立的模块,每个模块都有明确的目的,降低了代码的耦合性。
- 效率高,内核可以让软件完全由事件驱动,因此,轮询未发生的事件是不浪费时间的。相当于用中断来进行任务切换。
- 中断进程更短,通过把中断的处理推迟到用户创建的任务中,可以使得中断处理程序非常短。
二、Free RTOS核心功能
三、内存管理
文件 | 优点 | 缺点 |
---|---|---|
heap_1.c | 分配简单,时间确定 | 只分配、不回收 |
heap_2.c | 动态分配、最佳匹配 | 碎片、时间不定 |
heap_3.c | 调用标准库函数 | 速度慢、时间不定 |
heap_4.c | 相邻空闲内存可合并 | 速度慢、时间不定 |
heap_5.c | 在heap_4基础上支持分隔的内存块 | 可解决碎片问题、时间不定 |
(以Keil工具下STM32F103芯片为例)
1.Heap_1
它只实现了pvPortMalloc(分配内存),没有实现vPortFree(释放内存)。
如果程序不需要删除内核对象,那么可以使用heap_1:
- 实现最简单
- 没有碎片问题
- 一些要求非常严格的系统里,不允许使用动态内存,就可以使用heap_1
实现原理:
先定义一个大数组:
然后,对于pvPortMalloc调用时,从这个数组中分配空间。
FreeRTOS在创建任务时,需要2个内核对象:task control block(TCB)、stack。
使用heap_1时,内存分配过程如下图所示:
- A:创建任务之前整个数组都是空闲的
- B:创建第1个任务之后,蓝色区域被分配出去了
- C:创建3个任务之后的数组使用情况
2.Heap_2
Heap_2虽然效率远高于malloc、free,但是会产生内存碎片,时间不定,之所以还保留,只是为了兼容以前的代码。新设计中不再推荐使用Heap_2。建议使用Heap_4来替代Heap_2,更加高效。
Heap_2也是在数组上分配内存,跟Heap_1不一样的地方在于:
- Heap_2使用**最佳匹配算法(best fit)**来分配内存
- 它支持vPortFree
最佳匹配算法:
- 假设heap有3块空闲内存:5字节、25字节、100字节
- pvPortMalloc想申请20字节
- 找出最小的、能满足pvPortMalloc的内存:25字节
- 把它划分为20字节、5字节
- 返回这20字节的地址
- 剩下的5字节仍然是空闲状态,留给后续的pvPortMalloc使用
与Heap_4相比,Heap_2不会合并相邻的空闲内存,所以Heap_2会导致严重的"碎片化"问题。但是,如果申请、分配内存时大小总是相同的,这类场景下Heap_2没有碎片化的问题。所以它适合这种场景:频繁地创建、删除任务,但是任务的栈大小都是相同的(创建任务时,需要分配TCB和栈,TCB总是一样的)。
使用heap_2时,内存分配过程如下图所示:
- A:创建了3个任务
- B:删除了一个任务,空闲内存有3部分:顶层的、被删除任务的TCB空间、被删除任务的Stack空间
- C:创建了一个新任务,因为TCB、栈大小跟前面被删除任务的TCB、栈大小一致,所以刚好分配到原来的内存
3.Heap_3
Heap_3使用标准C库里的malloc、free函数,所以堆大小由链接器的配置决定,配置项configTOTAL_HEAP_SIZE不再起作用。
C库里的malloc、free函数并非线程安全的,Heap_3中先暂停FreeRTOS的调度器,再去调用这些函数,使用这种方法实现了线程安全。
4.Heap_4
跟Heap_1、Heap_2一样,Heap_4也是使用大数组来分配内存。
Heap_4使用首次适应算法(first fit)来分配内存。它还会把相邻的空闲内存合并为一个更大的空闲内存,这有助于较少内存的碎片问题。
首次适应算法:
- 假设堆中有3块空闲内存:5字节、200字节、100字节
- pvPortMalloc想申请20字节
- 找出第1个能满足pvPortMalloc的内存:200字节
- 把它划分为20字节、180字节
- 返回这20字节的地址
- 剩下的180字节仍然是空闲状态,留给后续的pvPortMalloc使用
Heap_4会把相邻空闲内存合并为一个大的空闲内存,可以较少内存的碎片化问题。适用于这种场景:频繁地分配、释放不同大小的内存。
Heap_4的使用过程举例如下:
- A:创建了3个任务
- B:删除了一个任务,空闲内存有2部分:
- 顶层的
- 被删除任务的TCB空间、被删除任务的Stack空间合并起来的
- C:分配了一个Queue,从第1个空闲块中分配空间
- D:分配了一个User数据,从Queue之后的空闲块中分配
- E:释放的Queue,User前后都有一块空闲内存
- F:释放了User数据,User前后的内存、User本身占据的内存,合并为一个大的空闲内存
Heap_4执行的时间是不确定的,但是它的效率高于标准库的malloc、free。
5.Heap_5
Heap_5分配内存、释放内存的算法跟Heap_4是一样的(首次适应算法(first fit))。
相比于Heap_4,Heap_5并不局限于管理一个大数组:它可以管理多块、分隔开的内存。
在嵌入式系统中,内存的地址可能并不连续,这种场景下可以使用Heap_5。
既然内存是分隔开的,那么就需要进行初始化:确定这些内存块在哪、多大:
- 在使用pvPortMalloc之前,必须先指定内存块的信息
- 使用vPortDefineHeapRegions来指定这些信息
指定一块内存,使用如下结构体:
指定多块内存,使用一个HeapRegion_t数组,在这个数组中,低地址在前、高地址在后。
比如:
vPortDefineHeapRegions函数原型如下:
把xHeapRegions数组传给vPortDefineHeapRegions函数,即可初始化Heap_5。
6.Heap相关的函数
1)pvPortMalloc/vPortFree
函数原型:
作用:分配内存、释放内存。
如果分配内存不成功,则返回值为NULL。
2)xPortGetFreeHeapSize
函数原型:
当前还有多少空闲内存,这函数可以用来优化内存的使用情况。比如当所有内核对象都分配好后,执行此函数返回2000,那么configTOTAL_HEAP_SIZE就可减小2000。
(注意:在heap_3中无法使用。)
3)xPortGetMinimumEverFreeHeapSize
函数原型:
返回:程序运行过程中,空闲内存容量的最小值。
注意:只有heap_4、heap_5支持此函数。
4)malloc失败的钩子函数
在pvPortMalloc函数内部:
四、任务管理
1.基本概念
对于整个单片机程序,我们称之为application,应用程序。
使用FreeRTOS时,我们可以在application中创建多个任务(task),有些文档把任务也称为线程(thread)。
以日常生活为例,比如这个母亲要同时做两件事:
- 喂饭:这是一个任务
- 回信息:这是另一个任务
可以引入很多概念:
- 任务状态(State):
- 当前正在喂饭,它是running状态;另一个"回信息"的任务就是"not running"状态
- "not running"状态还可以细分:
- ready:就绪,随时可以运行
- blocked:阻塞,卡住了,母亲在等待同事回信息
- suspended:挂起,同事废话太多,不管他了
- 优先级(Priority):
- 我工作生活兼顾:喂饭、回信息优先级一样,轮流做
- 我忙里偷闲:还有空闲任务,休息一下
- 厨房着火了,什么都别说了,先灭火:优先级更高
- 栈(Stack):
- 喂小孩时,我要记得上一口喂了米饭,这口要喂青菜了
- 回信息时,我要记得刚才聊的是啥
- 做不同的任务,这些细节不一样
- 对于人来说,当然是记在脑子里
- 对于程序,是记在栈里
- 每个任务有自己的栈
- 时间驱动:
- 孩子吃饭太慢:先休息一会,等他咽下去了、等他提醒我了,再喂下一口
- 协助式调度(Co-operative Scheduling):
- 你在给同事回信息
- 同事说:好了,你先去给小孩喂一口饭吧,你才能离开
- 同事不放你走,即使孩子哭了你也不能走
- 你好不容易可以给孩子喂饭了
- 孩子说:好了,妈妈你去处理一下工作吧,你才能离开
- 孩子不放你走,即使同事连发信息你也不能走
- 你在给同事回信息
2.创建任务
创建任务时使用的函数如下:
参数 | 描述 |
---|---|
pvTaskCode | 函数指针,可以简单地认为任务就是一个C函数。 它稍微特殊一点:永远不退出,或者退出时要调用"vTaskDelete(NULL)" |
pcName | 任务的名字,FreeRTOS内部不使用它,仅仅起调试作用。 长度为:configMAX_TASK_NAME_LEN |
usStackDepth | 每个任务都有自己的栈,这里指定栈大小。 单位是word,比如传入100,表示栈大小为100 word,也就是400字节。 最大值为uint16_t的最大值。 怎么确定栈的大小,并不容易,很多时候是估计。 精确的办法是看反汇编码。 |
pvParameters | 调用pvTaskCode函数指针时用到:pvTaskCode(pvParameters) |
uxPriority | 优先级范围:0~(configMAX_PRIORITIES - 1) 数值越小优先级越低,如果传入过大的值,xTaskCreate会把它调整为(configMAX_PRIORITIES - 1) |
pxCreatedTask | 用来保存xTaskCreate的输出结果:task handle。 以后如果想操作这个任务,比如修改它的优先级,就需要这个handle。 如果不想使用该handle,可以传入NULL。 |
返回值 | 成功:pdPASS; 失败:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足) 注意:文档里都说失败时返回值是pdFAIL,这不对。 pdFAIL是0,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY是-1。 |
使用2个函数分别创建2个任务。
任务1的代码:
任务2的代码:
main函数:
运行结果如下:
3.优先级
优先级0是最低优先级,可以通过再FreeRTOSConfig.h文件中设置configMAX_priorities来配置最高优先级数。
注意:不同任务可以共用同一个优先级。
4.Delay函数
两个Delay函数:
- vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
- vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态
函数原型:
画图说明:
- 使用vTaskDelay(n)时,进入、退出vTaskDelay的时间间隔至少是n个Tick中断
- 使用xTaskDelayUntil(&Pre, n)时,前后两次退出xTaskDelayUntil的时间至少是n个Tick中断
- 退出xTaskDelayUntil时任务就进入的就绪状态,一般都能得到执行机会
- 所以可以使用xTaskDelayUntil来让任务周期性地运行
5.任务在不同的状态间切换
6.空闲任务
空闲任务(IdleTask)的作用:释放被删除的任务的内存。
空闲任务的特点:
- 空闲任务优先级为0:它不能阻碍用户任务运行
- 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
所以对空闲任务来说,它一直都处于就绪状态,只有当其他优先级比它高的任务都执行完了,都在阻塞状态里,空闲任务才会执行。
五、通信管理(队列)
1.队列的原理
队列,可以容纳有限数量固定大小的数据。一般采用FIFO存储方式(First in First out)。而在FreeRTOS中,队列的传输用的是copy方式。
使用队列的流程:创建队列、写队列、读队列、删除队列。
- 队列可以包含若干个数据:队列中有若干项,这被称为"长度"(length)
- 每个数据大小固定
- 创建队列时就要指定长度、数据大小
- 数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读
- 也可以强制写队列头部:覆盖头部数据
队列的特点:
例如在中断的时候,如果中断执行的时间很长,数据处理量很大,会影响正常任务的运行。因此我们通过把数据转移到任务里面再作处理,减小中断开销。
详细操作:
- 拷贝:把数据、把变量的值复制进队列里
- 引用:把数据、把变量的地址复制进队列里
2.队列创建
队列的创建有两种方法:动态分配内存、静态分配内存,
- 动态分配内存:xQueueCreate,队列的内存在函数内部动态分配
函数原型:
参数 | 说明 |
---|---|
uxQueueLength | 队列长度,最多能存放多少个数据(item) |
uxItemSize | 每个数据(item)的大小:以字节为单位 |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为内存不足 |
- 静态分配内存:xQueueCreateStatic,队列的内存要事先分配好
函数原型:
参数 | 说明 |
---|---|
uxQueueLength | 队列长度,最多能存放多少个数据(item) |
uxItemSize | 每个数据(item)的大小:以字节为单位 |
pucQueueStorageBuffer | 如果uxItemSize非0,pucQueueStorageBuffer必须指向一个uint8_t数组,此数组大小至少为"uxQueueLength * uxItemSize" |
pxQueueBuffer | 必须执行一个StaticQueue_t结构体,用来保存队列的数据结构 |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为pxQueueBuffer为NULL |
示例:
3.队列复位
队列刚被创建时,里面没有数据;使用过程中可以调用xQueueReset()把队列恢复为初始状态,函数原型:
4.队列删除
删除队列的函数为vQueueDelete(),只能删除使用动态方法创建的队列,它会释放内存。函数原型:
5.写队列
可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。函数原型:
这些函数用到的参数是类似的,统一说明:
参数 | 说明 |
---|---|
xQueue | 队列句柄,要写哪个队列 |
pvItemToQueue | 数据指针,这个数据的值会被复制进队列 复制多大的数据? 在创建队列时已经指定了数据大小 |
xTicksToWait | 如果队列满则无法写入新数据,可以让任务进入阻塞状态 xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法写入数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写 |
返回值 | pdPASS:数据成功写入了队列 errQUEUE_FULL:写入失败,因为队列满了 |
6.读队列
使用xQueueReceive()函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在ISR中使用。函数原型:
参数说明:
参数 | 说明 |
---|---|
xQueue | 队列句柄,要读哪个队列 |
pvBuffer | bufer指针,队列的数据会被复制到这个buffer 复制多大的数据?在创建队列时已经指定了数据大小 |
xTicksToWait | 果队列空则无法读出数据,可以让任务进入阻塞状态 xTicksToWait表示阻塞的最大时间(Tick Count) 如果被设为0,无法读出数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写 |
返回值 | pdPASS:从队列读出数据入 errQUEUE_EMPTY:读取失败,因为队列空了 |
7.队列查询
可以查询队列中有多少个数据、有多少空余空间。函数原型如下:
8.队列覆盖/偷看
当队列长度为1时,可以使用xQueueOverwrite()或xQueueOverwriteFromISR()来覆盖数据。 注意:队列长度必须为1。当队列满时,这些函数会覆盖里面的数据,这也以为着这些函数不会被阻塞。 函数原型如下:
9.举例
此程序会创建一个队列,然后创建2个发送任务、1个接收任务:
- 发送任务优先级为1,分别往队列中写入100、200
- 接收任务优先级为2,读队列、打印数值
main函数中创建的队列、创建了发送任务、接收任务,代码如下:
发送任务的函数中,不断往队列中写入数值,代码如下:
接收任务的函数中,读取队列、判断返回值、打印,代码如下:
程序运行结果如下:
任务调度情况如下图所示: