RTOS从名字上可以分为free和RTOS两部分。free是免费的意思,RTOS全称是Real Time Operation System,译为实时操作系统。那FreeRTOS的意思就是“免费的实时操作系统”。RTOS不是指某一个特定的系统,而是一类系统。比如uC/OS,FreeRTOS,RTX,RT-Thread等都属于RTOS类操作系统。
操作系统允许多个任务同时运行,这个叫做多任务,实际上,一个处理器核心在某一时刻只能运行一个任务。操作系统中任务调度器的责任就是决定在某一时刻究竟运行哪个任务,任务调度在各个任务之间的切换非常快!这就给人们造成了同一时刻有多个任务同时运行的错觉。
操作系统的分类方式可以由任务调度器的工作方式决定,比如有的操作系统给每个任务分配相同的运行时间,时间到了就轮到下一个任务,Unix操作系统就是这样。RTOS的任务调度器被设计为可预测的,而这正是嵌入式实时操作系统所需要的,实时环境中要求操作系统必须对一个事件作出实时的响应,因此系统任务调度器的行为必须是可预测的。像FreeRTOS这种传统的RTOS类操作系统是由用户给每个任务分配一个任务优先级,任务调度器就可以根据此优先级来决定下一刻应该运行哪个任务。
特点:
文件 | 优点 | 缺点 |
---|---|---|
heap_1.c | 分配简单,时间确定 | 只分配、不回收 |
heap_2.c | 动态分配、最佳匹配 | 碎片、时间不定 |
heap_3.c | 调用标准库函数 | 速度慢、时间不定 |
heap_4.c | 相邻空闲内存可合并 | 速度慢、时间不定 |
heap_5.c | 在heap_4基础上支持分隔的内存块 | 可解决碎片问题、时间不定 |
(以Keil工具下STM32F103芯片为例)
它只实现了pvPortMalloc(分配内存),没有实现vPortFree(释放内存)。
如果程序不需要删除内核对象,那么可以使用heap_1:
实现原理:
先定义一个大数组:
然后,对于pvPortMalloc调用时,从这个数组中分配空间。
FreeRTOS在创建任务时,需要2个内核对象:task control block(TCB)、stack。
使用heap_1时,内存分配过程如下图所示:
Heap_2虽然效率远高于malloc、free,但是会产生内存碎片,时间不定,之所以还保留,只是为了兼容以前的代码。新设计中不再推荐使用Heap_2。建议使用Heap_4来替代Heap_2,更加高效。
Heap_2也是在数组上分配内存,跟Heap_1不一样的地方在于:
最佳匹配算法:
与Heap_4相比,Heap_2不会合并相邻的空闲内存,所以Heap_2会导致严重的"碎片化"问题。但是,如果申请、分配内存时大小总是相同的,这类场景下Heap_2没有碎片化的问题。所以它适合这种场景:频繁地创建、删除任务,但是任务的栈大小都是相同的(创建任务时,需要分配TCB和栈,TCB总是一样的)。
使用heap_2时,内存分配过程如下图所示:
Heap_3使用标准C库里的malloc、free函数,所以堆大小由链接器的配置决定,配置项configTOTAL_HEAP_SIZE不再起作用。
C库里的malloc、free函数并非线程安全的,Heap_3中先暂停FreeRTOS的调度器,再去调用这些函数,使用这种方法实现了线程安全。
跟Heap_1、Heap_2一样,Heap_4也是使用大数组来分配内存。
Heap_4使用首次适应算法(first fit)来分配内存。它还会把相邻的空闲内存合并为一个更大的空闲内存,这有助于较少内存的碎片问题。
首次适应算法:
Heap_4会把相邻空闲内存合并为一个大的空闲内存,可以较少内存的碎片化问题。适用于这种场景:频繁地分配、释放不同大小的内存。
Heap_4的使用过程举例如下:
Heap_4执行的时间是不确定的,但是它的效率高于标准库的malloc、free。
Heap_5分配内存、释放内存的算法跟Heap_4是一样的(首次适应算法(first fit))。
相比于Heap_4,Heap_5并不局限于管理一个大数组:它可以管理多块、分隔开的内存。
在嵌入式系统中,内存的地址可能并不连续,这种场景下可以使用Heap_5。
既然内存是分隔开的,那么就需要进行初始化:确定这些内存块在哪、多大:
指定一块内存,使用如下结构体:
指定多块内存,使用一个HeapRegion_t数组,在这个数组中,低地址在前、高地址在后。
比如:
vPortDefineHeapRegions函数原型如下:
把xHeapRegions数组传给vPortDefineHeapRegions函数,即可初始化Heap_5。
函数原型:
作用:分配内存、释放内存。
如果分配内存不成功,则返回值为NULL。
函数原型:
当前还有多少空闲内存,这函数可以用来优化内存的使用情况。比如当所有内核对象都分配好后,执行此函数返回2000,那么configTOTAL_HEAP_SIZE就可减小2000。
(注意:在heap_3中无法使用。)
函数原型:
返回:程序运行过程中,空闲内存容量的最小值。
注意:只有heap_4、heap_5支持此函数。
在pvPortMalloc函数内部:
对于整个单片机程序,我们称之为application,应用程序。
使用FreeRTOS时,我们可以在application中创建多个任务(task),有些文档把任务也称为线程(thread)。
以日常生活为例,比如这个母亲要同时做两件事:
可以引入很多概念:
创建任务时使用的函数如下:
参数 | 描述 |
---|---|
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函数:
运行结果如下:
优先级0是最低优先级,可以通过再FreeRTOSConfig.h文件中设置configMAX_priorities来配置最高优先级数。
注意:不同任务可以共用同一个优先级。
两个Delay函数:
函数原型:
画图说明:
空闲任务(IdleTask)的作用:释放被删除的任务的内存。
空闲任务的特点:
所以对空闲任务来说,它一直都处于就绪状态,只有当其他优先级比它高的任务都执行完了,都在阻塞状态里,空闲任务才会执行。
队列,可以容纳有限数量固定大小的数据。一般采用FIFO存储方式(First in First out)。而在FreeRTOS中,队列的传输用的是copy方式。
使用队列的流程:创建队列、写队列、读队列、删除队列。
队列的特点:
例如在中断的时候,如果中断执行的时间很长,数据处理量很大,会影响正常任务的运行。因此我们通过把数据转移到任务里面再作处理,减小中断开销。
详细操作:
队列的创建有两种方法:动态分配内存、静态分配内存,
函数原型:
参数 | 说明 |
---|---|
uxQueueLength | 队列长度,最多能存放多少个数据(item) |
uxItemSize | 每个数据(item)的大小:以字节为单位 |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为内存不足 |
函数原型:
参数 | 说明 |
---|---|
uxQueueLength | 队列长度,最多能存放多少个数据(item) |
uxItemSize | 每个数据(item)的大小:以字节为单位 |
pucQueueStorageBuffer | 如果uxItemSize非0,pucQueueStorageBuffer必须指向一个uint8_t数组,此数组大小至少为"uxQueueLength * uxItemSize" |
pxQueueBuffer | 必须执行一个StaticQueue_t结构体,用来保存队列的数据结构 |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为pxQueueBuffer为NULL |
示例:
队列刚被创建时,里面没有数据;使用过程中可以调用xQueueReset()把队列恢复为初始状态,函数原型:
删除队列的函数为vQueueDelete(),只能删除使用动态方法创建的队列,它会释放内存。函数原型:
可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。函数原型:
这些函数用到的参数是类似的,统一说明:
参数 | 说明 |
---|---|
xQueue | 队列句柄,要写哪个队列 |
pvItemToQueue | 数据指针,这个数据的值会被复制进队列 复制多大的数据? 在创建队列时已经指定了数据大小 |
xTicksToWait | 如果队列满则无法写入新数据,可以让任务进入阻塞状态 xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法写入数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写 |
返回值 | pdPASS:数据成功写入了队列 errQUEUE_FULL:写入失败,因为队列满了 |
使用xQueueReceive()函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在ISR中使用。函数原型:
参数说明:
参数 | 说明 |
---|---|
xQueue | 队列句柄,要读哪个队列 |
pvBuffer | bufer指针,队列的数据会被复制到这个buffer 复制多大的数据?在创建队列时已经指定了数据大小 |
xTicksToWait | 果队列空则无法读出数据,可以让任务进入阻塞状态 xTicksToWait表示阻塞的最大时间(Tick Count) 如果被设为0,无法读出数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写 |
返回值 | pdPASS:从队列读出数据入 errQUEUE_EMPTY:读取失败,因为队列空了 |
可以查询队列中有多少个数据、有多少空余空间。函数原型如下:
当队列长度为1时,可以使用xQueueOverwrite()或xQueueOverwriteFromISR()来覆盖数据。 注意:队列长度必须为1。当队列满时,这些函数会覆盖里面的数据,这也以为着这些函数不会被阻塞。 函数原型如下:
此程序会创建一个队列,然后创建2个发送任务、1个接收任务:
main函数中创建的队列、创建了发送任务、接收任务,代码如下:
发送任务的函数中,不断往队列中写入数值,代码如下:
接收任务的函数中,读取队列、判断返回值、打印,代码如下:
程序运行结果如下:
任务调度情况如下图所示:
资料参考链接