赞
踩
1、FreeRTOS是ROTS中一个开源的、轻量级的实时操作系统,适用于大多数的嵌入式开发板(如STM32板子等)。
2、操作系统允许多个任务同时运行,这个叫做多任务。实际上,一个处理器核心在某一时刻只能运行一个任务。操作系统中任务调度器的责任就是决定在某一时刻究竟运行哪个任务。
3、FreeRTOS 操作系统则是由用户给每个任务分配一个任务优先级,任务调度器就可以根据此优先级来决定下一刻应该运行哪个任务。
1、FreeRTOS 的内核支持抢占式,合作式和时间片调度
2、提供了一个用于低功耗的 Tickless 模式
3、系统的组件在创建时可以选择动态或者静态的 RAM,比如任务、消息队列、信号量、软件定时器等等
4、FreeRTOS 系统简单、小巧、易用,通常情况下内核占用 4k-9k 字节的空间
5、高可移植性,代码主要 C 语言编写
6、高效的软件定时器
7、强大的跟踪执行功能
8、堆栈溢出检测功能
9、任务数量不限,任务优先级不限
10、系统时钟频率为1000
11、任务优先级,数字越大优先级越高
#define portMAX_DELAY ( TickType_t )0xffffffffUL (时间很长,死等,等很久的意思)
即延迟时间的范围是:1- 0xFFFFFFFF
CPU为何能及时响应中断:每次CPU在执行完指令周期之后,还会执行一段中断周期,来检查是否产生中断,所以CPU能及时相应中断请求
1.1)、优先级
①每个任务分配一个从0~configMAX_PRIORITIES-1
的优先级,优先级的数字越低表示任务的优先级越低;
②高优先级抢占低优先级
当一个任务A正在运行,另一个任务B(优先级高于任务A)阻塞时间到或者事件触发处于就绪态,那么B会从A那抢占处理器,B开始运行,A停止运行
FreeRTOS 调度器确保处于就绪态或运行态的高优先级的任务获取处理器使用权,换句话说就是处于就绪态的最高优先级的任务才会运行。
③时间片轮转:当宏configUSE_TIME_SLICING定义为1的时候多任务可以共用一个优先级,数量不限。此时处于就绪的优先级相同的任务就会使用时间片轮转调度器获取运行时间。
1.2)任务控制块/任务堆栈
①任务控制块(TCB)
FreeRTOS的每个任务都有一些属性需要存储,FreeRTOS把这些属性集合到一起用一个结构体来表示,这个结构体叫做任务控制块。
属性:任务名字,优先级,任务堆栈大小,任务句柄等。
②任务堆栈:
FreeRTOS之所以能正确的恢复一个任务的运行就是因为有任务堆栈的保驾护航,任务调度器正在进行任务切换的时候会将当前任务的现场(CPU寄存器值等)保存在此任务堆栈中,等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场,恢复现场以后任务就会接着上次中断的地方开始运行。
①运行态
当一个任务正在运行时,那么就说这个任务处于运行态, 处于运行态的任务就是当前正在使用处理器的任务。 如果使用的是核处理器的话那么不管在任何时刻永远都只有一个任务处于运行态。
②就绪态
处于就绪态的任务是那些已经准备就绪(这些任务没有被阻塞或者挂起 可以运行的任务,但是处于就绪态的任务还没有运行,因为有一个同优先级或者更高优先级的任务正在运行!
③阻塞态
如果一个任务当前正在等待某个外部事件的话就说它是处于阻塞态,比如说如果某个任务调用了函数 vTaskDelay()的话就会进入阻塞态, 直到延时周期完成。任务在等待队列、信号量、事件组、通知或互斥信号量的时候也会进入阻塞态 。任务进入阻塞态会有一个超时时间,当超过这个超时时间任务就会退出阻塞态,即使所等待的事件还没有来临!
④挂起态
像阻塞态一样,任务进入挂起态以后不能被任务调度器调用进入运行态,但是进入挂起态的任务没有超时时间。任务进入和退出挂起态通过调用函数 vTaskSuspend()和 xTaskResume()
任务之间的切换关系:
创建相对于删除;挂起相对于恢复
①所有的通信和同步机制都是基于队列实现的
②队列不但可以传递数组,也可以传递结构体
信号量是深度为1的队列
任务通知来代替信号量、消息队列、事件标志组成等,使用任务通知的话效率更高
4.1)STM32支持的低功耗模式
①sleep:睡眠
在 SLEEP 模式下,只有内核停止了工作,而外设仍然在运行。
在进入 SLEEP 模式后,所有中断(‘外部中断、串口中断、定时器中断等’)均可唤醒 MCU,从而退出 SLEEP 模式。
②stop:停止
在 STOP 模式下,内核停止工作,并且所有的时钟(如 HCLK, PCLK1, PCLK2 等)也停止工作,即所有外设停止工作,这里有一点要特别注意,此时 SYSTICK 也会被停掉。当然,我们产品中的 RTC 还在继续运行,因为它的时钟源为外部的 32.768K 晶振。在进入 STOP 模式后,只有外部中断(EXTI)才能唤醒 MCU由于 RTC 中断挂在外部中断线上,所以 RTC 中断也能唤醒 MCU
③standby:待机
在 STANDBY 模式下,内核、所有的时钟、以及后备 1.2V 电源全部停止工作。
从 STANDBY 模式中唤醒后,系统相当于执行了一次复位操作,程序会从头来过。
4.2)、空闲任务的钩子函数实现的低功耗
①钩子函数
通过空闲任务钩子函数(或称回调,hook,or call_back),可以直接再空闲任务中添加应用程序相关功能。空闲任务钩子函数会被空闲任务每循环一次就自动调用一次。通常空闲任务钩子函数被用于:
②FreeRTOS是通过在处理器处理空闲任务的时候将处理器设置为低功耗模式来降低能耗。一般会在空闲任务的钩子函数中执
行低功耗相关处理,比如设置处理器进入低功耗模式(上面4.1提到的三种方式)、关闭其他外设时钟、降低系统主频等等。
4.3)Tickless模式
FreeRTOS系统提供的低功耗模式,当处理器进入空闲任务周期以后就关闭系统节拍中断(滴答定时器中断),只有当其他中断发生或者其他任务需要处理的时侯处理器才会从低功耗模式中唤醒。
列表是FreeRTOS 中的一个数据结构,概念上和链表有点类似,列表被用来跟踪FreeRTOS中的任务。
内存管理是一个系统基本组成部分,FreeRTOS中大量使用到了内存管理,比如创建任务、信号量、队列等会自动从堆中申请内存。用户应用层代码也可以 FreeRTOS提供的内存管理函数来申请和释放内存。
FreeRTOS任务相关API函数 - 简书 (jianshu.com)
(53条消息) FreeRTOS基本函数说明_chao-@的博客-CSDN博客_freertos函数
vTaskStartScheduler() 开启任务调度器
xTaskCreate( TaskFunction_t pxTaskCode,//任务函数名称
const char * const pcName,//字符串函数名字
const uint16_t usStackDepth,//任务堆栈大小
void * const pvParameters,//一般为NULL
UBaseType_t uxPriority,//任务优先级
TaskHandle_t * const pxCreatedTask ) 动态任务创建 //pxCreatedTask 任务句柄
taskENTER_CRITICAL() 进入临界资源
taskEXIT_CRITICAL() 退出临界资源
vTaskDelay(x) 系统延时函数
vTaskDelete(X) 任务删除
uxTaskPriorityGet() 查询某个任务的优先级。
vTaskPrioritySet() 改变某个任务的任务优先级。
uxTaskGetSystemState() 获取系统中任务状态。
vTaskGetInfo() 获取某个任务信息。
xTaskGetApplicationTaskTag() 获取某个任务的标签(Tag)值。
xTaskGetCurrentTaskHandle() 获取当前正在运行的任务的任务句柄。
xTaskGetHandle() 根据任务名字查找某个任务的句柄
xTaskGetIdleTaskHandle() 获取空闲任务的任务句柄。
uxTaskGetStackHighWaterMark() 获取任务的堆栈的历史剩余最小值, FreeRTOS 中叫做“高水位线”
eTaskGetState() 获取某个任务的壮态,这个壮态是 eTaskState 类型。
pcTaskGetName() 获取某个任务的任务名字。
xTaskGetTickCount() 获取系统时间计数器值。
xTaskGetTickCountFromISR() 在中断服务函数中获取时间计数器值
xTaskGetSchedulerState() 获取任务调度器的壮态,开启或未开启。
uxTaskGetNumberOfTasks() 获取当前系统中存在的任务数量。
vTaskList() 以一种表格的形式输出当前系统中所有任务的详细信息。
vTaskGetRunTimeStats() 获取每个任务的运行时间。
vTaskSetApplicationTaskTag() 设置任务标签(Tag)值。
SetThreadLocalStoragePointer() 设置线程本地存储指针
GetThreadLocalStoragePointer() 获取线程本地存储指针
信号量常常用于控制对共享资源的访问和任务同步。信号量用于控制共享资源访问的场景相当于一个上锁机制,代码只有获得了这个锁的钥匙才能够执行
信号量的另一个重要的应用场合就是任务同步,用于任务与任务或中断与任务之间的同步。在执行中断服务函数的时候可以通过向任务发送信号量来通知任务它所期待的事件发生了,当退出中断服务函数以后在任务调度器的调度下同步的任务就会执行。
信号量分为:二值信号量,计数型信号量和互斥信号量
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1DtNBAUz-1646407081532)(QQ截图20220225195810.png)]
void vSemaphoreCreateBinary( SemaphoreHandle_t xSemaphore )
参数:xSemaphore:保存创建成功的二值信号量句柄。
返回值:NULL: 二值信号量创建失败。
其他值: 二值信号量创建成功。
SemaphoreHandle_t xSemaphoreCreateBinary( void )
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer )
参数:pxSemaphoreBuffer:此参数指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体。
返回值:NULL: 二值信号量创建失败。
其他值: 创建成功的二值信号量句柄。
BaseType_t xSemaphoreGive( xSemaphore )
参数:xSemaphore:要释放的信号量句柄。
返回值:pdPASS: 释放信号量成功。
errQUEUE_FULL: 释放信号量失败。
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t * pxHigherPriorityTaskWoken)
参数:xSemaphore: 要释放的信号量句柄。
pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值由这三个函数来设置的,用户不用进行设置,用 户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:pdPASS: 释放信号量成功。
errQUEUE_FULL: 释放信号量失败。
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,TickType_t xBlockTime)
参数:xSemaphore:要获取的信号量句柄。
xBlockTime: 阻塞时间。
返回值:pdTRUE: 获取信号量成功。
pdFALSE: 超时,获取信号量失败。
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, BaseType_t * pxHigherPriorityTaskWoken)
参数:xSemaphore: 要获取的信号量句柄。
pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值由这三个函数来设置的,用户不用进行设置,用 户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:pdPASS: 获取信号量成功。
pdFALSE: 获取信号量失败。
例程看正点原子相关代码
计数型信号量叫做数值信号量,二值信号量相当于长度为 1 的队列,那么计数型信号量就是长度大于 1 的队列。同二值信号量一样,用户不需要关心队列中存储了什么数据,只需要关心队列是否为空即可。计数型信号量通常用于如下两个场合:事件计数和资源管理
**事件计数:**每次事件发生的时候就在事件处理函数中释放信号量(增加信号量的计数值),其他任务会获取信号量(信号量计数值减一,信号量值就是队列结构体成员变量uxMessagesWaiting)来处理事件。
**资源管理:**信号量值代表当前资源的可用数量,一个任务要想获得资源的使用权,首先必须获取信号量,信号量获取成功以后信号量值就会减一。当信号量值为 0 的时候说明没有资源了。当一个任务使用完资源以后一定要释放信号量,释放信号量以后信号量值会加一。
计数型信号量:
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount )
参数:uxMaxCount: 计数信号量最大计数值,当信号量值等于此值的时候释放信号量就会失败。
uxInitialCount: 计数信号量初始值。
返回值:NULL: 计数型信号量创建失败。
其他值: 计数型信号量创建成功,返回计数型信号量句柄。
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount, UBaseType_t uxInitialCount, StaticSemaphore_t * pxSemaphoreBuffer )
参数:uxMaxCount: 计数信号量最大计数值,当信号量值等于此值的时候释放信号量就会失败。
uxInitialCount: 计数信号量初始值。
pxSemaphoreBuffer:指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体。
返回值:NULL: 计数型信号量创建失败。
其他值: 计数型号量创建成功,返回计数型信号量句柄。
计数型信号量的释放和获取与二值信号量相同,参考上面的,例程参考相关代码
互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或中断与任务之间的同步)二值信号量最适合。互斥信号量适合用于那些需要互斥访问的应用中。在互斥访问中互斥信号量相当于一个钥匙,当任务想要使用资源的时候就必须先获得这个钥匙,当使用完资源以后就必须归还这个钥匙,这样其他的任务就可以拿着这个钥匙去使用资源。
不同于二值信号量的是互斥信号量具有优先级继承的特性。当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。优先级继承尽可能的降低了高优先级任务处于阻塞态的时间,并且将已经出现的“优先级翻转”的影响降到最低。
优先级继承并不能完全的消除优先级翻转,它只是尽可能的降低优先级翻转带来的影响
优先级继承的处理说白了就是将任务的当前优先级降低到任务的基优先级,所以要把当前任务先从任务就绪表中移除。当任务优先级恢复为原来的优先级以后再重新加入到就绪表中
互斥信号量不能用于中断服务函数中,原因如下:
● 互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
● 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态
SemaphoreHandle_t xSemaphoreCreateMutex( void )
参数:无
返回值:NULL: 互斥信号量创建失败。
其他值: 创建成功的互斥信号量的句柄。
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer )
参数:pxMutexBuffer:此参数指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体
返回值:NULL: 互斥信号量创建失败。
其他值: 创建成功的互斥信号量的句柄。
互斥信号量创建成功以后会调用函数 xQueueGenericSend()释放一次信号量,说明互斥信号量默认就是有效的
互斥信号量的获取和释放与二值信号量是一样的,参考上面的即可
例程参考正点原子代码即可
(1) 任务 H 和任务 M 处于挂起状态,等待某一事件的发生,任务 L 正在运行。
(2) 某一时刻任务 L 想要访问共享资源,在此之前它必须先获得对应该资源的信号量。
(3) 任务 L 获得信号量并开始使用该共享资源。
(4) 由于任务 H 优先级高,它等待的事件发生后便剥夺了任务 L 的 CPU 使用权。
(5) 任务 H 开始运行。
(6) 任务 H 运行过程中也要使用任务 L 正在使用着的资源,由于该资源的信号量还被任务
L 占用着,任务 H 只能进入挂起状态,等待任务 L 释放该信号量。
(7) 任务 L 继续运行。
(8) 由于任务 M 的优先级高于任务 L,当任务 M 等待的事件发生后,任务 M 剥夺了任务
L 的 CPU 使用权。
(9) 任务 M 处理该处理的事。
(10) 任务 M 执行完毕后,将 CPU 使用权归还给任务 L。
(11) 任务 L 继续运行。
(12) 最终任务 L 完成所有的工作并释放了信号量,到此为止,由于实时内核知道有个高
优先级的任务在等待这个信号量,故内核做任务切换。
(13) 任务 H 得到该信号量并接着运行。
在这种情况下,任务 H 的优先级实际上降到了任务 L 的优先级水平。因为任务 H 要一直
等待直到任务 L 释放其占用的那个共享资源。由于任务 M 剥夺了任务 L 的 CPU 使用权,使
得任务 H 的情况更加恶化,这样就相当于任务 M 的优先级高于任务 H,导致优先级翻转。
任务控制块
在其他RTOS中,任务一般是由:任务堆栈、任务控制块和任务函数三部分组成。
任务堆栈:上下文切换的时候用来保存任务的工作环境,就是STM32的内部寄存器值。
任务控制块:任务控制块用来记录任务的各个属性。
任务函数:由用户编写的任务处理代码(一般无返回值,单个void *参数,不会返回)
列表是 FreeRTOS 中的一个数据结构,概念上和链表有点类似,列表被用来跟踪 FreeRTOS中的任务。
在 list.h 中定义了一个叫 List_t 的结构体,如下:
typedef struct xLIST
{
listFIRST_LIST_INTEGRITY_CHECK_VALUE (1)
configLIST_VOLATILE UBaseType_t uxNumberOfItems; (2)
ListItem_t * configLIST_VOLATILE pxIndex; (3)
MiniListItem_t xListEnd; (4)
listSECOND_LIST_INTEGRITY_CHECK_VALUE (5)
} List_t;
(1) 和 (5) 、 这 两 个 都 是 用 来 检 查 列 表 完 整 性 的 , 需 要 将 宏configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 设置为 1,开启以后会向这两个地方分别添加一个变量 xListIntegrityValue1 和 xListIntegrityValue2,在初始化列表的时候会这两个变量中写入一个特殊的值,默认不开启这个功能。
(2)、uxNumberOfItems 用来记录列表中列表项的数量。
(3)、pxIndex 用来记录当前列表项索引号,用于遍历列表。
(4)、列表中最后一个列表项,用来表示列表结束,此变量类型为 MiniListItem_t,这是一个迷你列表项
列表项就是存放在列表中的项目,FreeRTOS 提供了两种列表项:列表项和迷你列表项。这两个都在文件 list.h 中有定义,先来看一下列表项,定义如下:
struct xLIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE (1)
configLIST_VOLATILE TickType_t xItemValue; (2)
struct xLIST_ITEM * configLIST_VOLATILE pxNext; (3)
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; (4)
void * pvOwner; (5)
void * configLIST_VOLATILE pvContainer; (6)
listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE (7)
};
typedef struct xLIST_ITEM ListItem_t;
(1)和(7)、用法和列表一样,用来检查列表项完整性的。
(2)、xItemValue 为列表项值。
(3)、pxNext 指向下一个列表项。
(4)、pxPrevious 指向前一个列表项,和 pxNext 配合起来实现类似双向链表的功能。
(5)、pvOwner 记录此链表项归谁拥有,通常是任务控制块。
(6)、pvContainer 用来记录此列表项归哪个列表。
迷你列表项
struct xMINI_LIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE (1)
configLIST_VOLATILE TickType_t xItemValue; (2)
struct xLIST_ITEM * configLIST_VOLATILE pxNext; (3)
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; (4)
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;
(1)用于检查迷你列表项的完整性
(2)、xItemValue 记录列表列表项值。
(3)、pxNext 指向下一个列表项。
(4)、pxPrevious 指向上一个列表项。
列表初始化:
void vListInitialise( List_t * const pxList )
{
pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd ); (1)
pxList->xListEnd.xItemValue = portMAX_DELAY; (2)
pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd ); (3)
pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd ); (4)
pxList->uxNumberOfItems = ( UBaseType_t ) 0U; (5)
listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList ); (6)
listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList ); (7)
}
(1)、xListEnd 用来表示列表的末尾,而 pxIndex 表示列表项的索引号,此时列表只有一个列表项,那就是 xListEnd,所以 pxIndex 指向 xListEnd。
(2)、xListEnd 的列表项值初始化为 portMAX_DELAY, portMAX_DELAY 是个宏,在文件portmacro.h 中有定义。根据所使用的 MCU 的不同,portMAX_DELAY 值也不相同,可以为 0xffff或者 0xffffffffUL,本教程中为 0xffffffffUL。
(3)、初始化列表项 xListEnd 的 pxNext 变量,因为此时列表只有一个列表项 xListEnd,因此 pxNext 只能指向自身。
(4)、同(3)一样,初始化 xListEnd 的 pxPrevious 变量,指向 xListEnd 自身。
(5)、由于此时没有其他的列表项,因此 uxNumberOfItems 为 0,注意,这里没有算 xListEnd。
(6) 和 (7) 、 初 始 化 列 表 项 中 用 于 完 整 性 检 查 字 段 , 只 有 宏configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 为 1 的时候才有效。
列表项的初始化:
void vListInitialiseItem( ListItem_t * const pxItem )
{
pxItem->pvContainer = NULL; //初始化 pvContainer 为 NULL
//初始化用于完整性检查的变量,如果开启了这个功能的话。
listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
}
列表项的初始化很简单,只是将列表项成员变量 pvContainer 初始化为 NULL,并且给用于完整性检查的变量赋值。有朋友可能会问,列表项的成员变量比列表要多,怎么初始化函数就这么短?其他的成员变量什么时候初始化呢?这是因为列表项要根据实际使用情况来初始化,比如任务创建函数 xTaskCreate()就会对任务堆栈中的 xStateListItem 和 xEventListItem 这两个列表项中的其他成员变量在做初化。
void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )
参数:pxList: 列表项要插入的列表。
pxNewListItem: 要插入的列表项
返回值:void
函数 vListInsert()的参数 pxList 决定了列表项要插入到哪个列表中,pxNewListItem 决定了要插入的列表项。要插入的位置由列表项中成员变量xItemValue 来决定。列表项的插入根据 xItemValue 的值按照升序的方式排列
函数vListInsert()源码:
void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem ) { ListItem_t *pxIterator; const TickType_t xValueOfInsertion = pxNewListItem->xItemValue; (1) listTEST_LIST_INTEGRITY( pxList ); (2) listTEST_LIST_ITEM_INTEGRITY( pxNewListItem ); if( xValueOfInsertion == portMAX_DELAY ) (3) { pxIterator = pxList->xListEnd.pxPrevious; (4) } else { for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd ); pxIterator->\ (5) pxNext->xItemValue <=xValueOfInsertion; pxIterator = pxIterator->pxNext ) { //空循环,什么也不做! } } pxNewListItem->pxNext = pxIterator->pxNext; (6) pxNewListItem->pxNext->pxPrevious = pxNewListItem; pxNewListItem->pxPrevious = pxIterator; pxIterator->pxNext = pxNewListItem; pxNewListItem->pvContainer = ( void * ) pxList; (7) ( pxList->uxNumberOfItems )++; (8) }
(1)、获取要插入的列表项值,即列表项成员变量 xItemValue 的值,因为要根据这个值来确定列表项要插入的位置。
(2)、这一行和下一行代码用来检查列表和列表项的完整性的。其实就是检查列表和列表项中用于完整性检查的变量值是否被改变。这些变量的值在列表和列表项初始化的时候就被写入了,这两行代码需要实现函数 configASSERT()!
(3)、要插入列表项,第一步就是要获取该列表项要插入到什么位置!如果要插入的列表项的值等于 portMAX_DELAY,也就是说列表项值为最大值,这种情况最好办了,要插入的位置就是列表最末尾了。
(4)、获取要插入点,注意!列表中的 xListEnd 用来表示列表末尾,在初始化列表的时候xListEnd的列表值也是portMAX_DELAY,此时要插入的列表项的列表值也是portMAX_DELAY。这两个的顺序该怎么放啊?通过这行代码可以看出要插入的列表项会被放到 xListEnd 前面。
(5)、要插入的列表项的值如果不等于 portMAX_DELAY 那么就需要在列表中一个一个的找自己的位置,这个 for 循环就是找位置的过程,当找到合适列表项的位置的时候就会跳出。由于这个 for 循环是用来寻找列表项插入点的,所以 for 循环体里面没有任何东西。这个查找过程是按照升序的方式查找列表项插入点的。
(6)、经过上面的查找,我们已经找到列表项的插入点了,从本行开始接下来的四行代码就是将列表项插入到列表中,插入过程和数据结构中双向链表的插入类似。像 FreeRTOS 这种RTOS 系统和一些协议栈都会大量用到数据结构的知识,所以建议大家没事的时候多看看数据结构方面的书籍,否则的话看源码会很吃力的。
(7)、列表项已经插入到列表中了,那么列表项的成员变量 pvContainer 也该记录此列表项属于哪个列表的了。
(8)、列表的成员变量 uxNumberOfItems 加一,表示又添加了一个列表项。
列表项插入过程图示:
在一个空的列表 List 中插入一个列表值为 40 的列表项 ListItem1,插入完成以后如图所示
插入完成以后列表 List 和列表项 ListItem1 中各个成员变量之间的变化,比如列表 List 中的 uxNumberOfItems 变为了 1,表示现在列表中有一个列表项。列表项 ListItem1 中的pvContainer 变成了 List,表示此列表项属于列表 List。
接着再插入一个值为 60 的列表项 ListItem2,插入完成以后如图所示
在上面的列表中再插入一个值为 50 的列表项 ListItem3,插入完成以后如图
void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
参数:pxList: 列表项要插入的列表。
pxNewListItem: 要插入的列表项。
返回值:void
函数vListInsertEnd()源码如下:
void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
{
ListItem_t * const pxIndex = pxList->pxIndex;
listTEST_LIST_INTEGRITY( pxList ); (1)
listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );
pxNewListItem->pxNext = pxIndex; (2)
pxNewListItem->pxPrevious = pxIndex->pxPrevious;
mtCOVERAGE_TEST_DELAY();
pxIndex->pxPrevious->pxNext = pxNewListItem;
pxIndex->pxPrevious = pxNewListItem;
pxNewListItem->pvContainer = ( void * ) pxList; (3)
( pxList->uxNumberOfItems )++; (4)
}
(1)、与下面的一行代码完成对列表和列表项的完整性检查。
(2)、从本行开始到(3)之间的代码就是将要插入的列表项插入到列表末尾。使用函数vListInsert()向列表中插入一个列表项的时候这个列表项的位置是通过列表项的值,也就是列表项成员变量 xItemValue 来确定。vListInsertEnd()是往列表的末尾添加列表项的,我们知道列表中的 xListEnd 成员变量表示列表末尾的,那么函数 vListInsertEnd()插入一个列表项是不是就是插到 xListEnd 的前面或后面啊?这个是不一定的,这里所谓的末尾要根据列表的成员变量pxIndex 来确定的!前面说了列表中的 pxIndex 成员变量是用来遍历列表的,pxIndex 所指向的列表项就是要遍历的开始列表项,也就是说 pxIndex 所指向的列表项就代表列表头!由于是个环形列表,所以新的列表项就应该插入到 pxIndex 所指向的列表项的前面。
(3)、标记新的列表项 pxNewListItem 属于列表 pxList。
(4)、记录列表中的列表项数目的变量加一,更新列表项数目。
列表项末尾插入图示
列表项末尾插入与列表项插入方式本质上是相同的。
列表的 pxIndex 所指向的列表项,这里为 ListItem1,不再是 xListEnd
在上面的列表中插入一个值为 50 的列表项 ListItem3,插入完成以后如图
UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
参数:pxItemToRemove: 要删除的列表项
返回值:返回删除列表项以后的列表剩余列表项数目。
函数 uxListRemove()的源码如下:
UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove ) { List_t * const pxList = ( List_t * ) pxItemToRemove->pvContainer; (1) pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious; (2) pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext; mtCOVERAGE_TEST_DELAY(); if( pxList->pxIndex == pxItemToRemove ) { pxList->pxIndex = pxItemToRemove->pxPrevious; (3) } else { mtCOVERAGE_TEST_MARKER(); } pxItemToRemove->pvContainer = NULL; (4) ( pxList->uxNumberOfItems )--; return pxList->uxNumberOfItems; (5) }
(1)、要删除一个列表项我们得先知道这个列表项处于哪个列表中,直接读取列表项中的成员变量 pvContainer 就可以得到此列表项处于哪 个列表中。
(2)、与下面一行完成列表项的删除,其实就是将要删除的列表项的前后两个列表项“连接”在一起。
(3)、如果列表的 pxIndex 正好指向要删除的列表项,那么在删除列表项以后要重新给pxIndex 找个“对象”啊,这个新的对象就是被删除的 列表项的前一个列表项。
(4)、被删除列表项的成员变量 pvContainer 清零。
(5)、返回新列表的当前列表项数目。
列表 List_t 中的成员变量 pxIndex 是用来遍历列表的,FreeRTOS提供了一个函数来完成列表的遍历,这个函数是 listGET_OWNER_OF_NEXT_ENTRY()。每调用一次这个函数列表的 pxIndex 变量就会指向下一个列表项,并且返回这个列表项的 pxOwner变量值。这个函数本质上是一个宏,这个宏在文件 list.h 中如下定义:
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \ (1)
{ \
List_t * const pxConstList = ( pxList ); \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \ (2)
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )\ (3)
{ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \ (4)
} \
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \ (5)
}
(1)、pxTCB 用来保存 pxIndex 所指向的列表项的 pvOwner 变量值,也就是这个列表项属于谁的?通常是一个任务的任务控制块。pxList 表示要遍历的列表。
(2)、列表的 pxIndex 变量指向下一个列表项。
(3)、如果 pxIndex 指向了列表的 xListEnd 成员变量,表示到了列表末尾。
(4)、如果到了列表末尾的话就跳过 xListEnd,pxIndex 再一次重新指向处于列表头的列表项,这样就完成了一次对列表的遍历。
(5)、将 pxIndex 所指向的新列表项的 pvOwner 赋值给 pxTCB。此函数用于从多个同优先级的就绪任务中查找下一个要运行的任务。
队列是为了任务与任务、任务与中断之间的通信而准备的,可以在任务与任务、任务与中断之间传递消息,队列中可以存储有限的、大小固定的数据项目。任务与任务、任务与中断之间要交流的数据保存在队列中,叫做队列项目。队列所能保存的最大数据项目数量叫做队列的长度,创建队列的时候会指定数据项目的大小和队列的长度。由于队列用来传递消息的,所以也称为消息队列。
数据存储、多任务访问、出队阻塞、入队阻塞
通常队列采用先进先出(FIFO)的存储缓冲机制,也就是往队列发送数据的时候(也叫入队)永远都是发送到队列的尾部,而从队列提取数据的时候(也叫出队)是从队列的头部提取的。但是也可以使用 LIFO 的存储缓冲,也就是后进先出
多任务访问:
队列不是属于某个特别指定的任务的,任何任务都可以向队列中发送消息,或者从队列中提取消息。
出队阻塞:
当任务尝试从一个队列中读取消息的时候可以指定一个阻塞时间,这个阻塞时间就是当任务从队列中读取消息无效的时候任务阻塞的时间。出队就是就从队列中读取消息,出队阻塞是针对从队列中读取消息的任务而言的。
入队阻塞:
入队说的是向队列中发送消息,将消息加入到队列中。和出队阻塞一样,当一个任务向队列发送消息的话也可以设置阻塞时间。
中任务 A 的变量 x 值为 10,将这个值发送到消息队列中。此时队列剩余长度就是3 了。前面说了向队列中发送消息是采用拷贝的方式,所以一旦消息发送完成变量 x 就可以再次被使用,赋其他的值
任务 A 又向队列发送了一个消息,即新的 x 的值,这里是 20。此时队列剩余长度为 2。
任务 B 从队列中读取消息,并将读取到的消息值赋值给 y,这样 y 就等于 10了。任务 B 从队列中读取消息完成以后可以选择清除掉这个消息或者不清除。当选择清除这个消息的话其他任务或中断就不能获取这个消息了,而且队列剩余大小就会加一,变成 3。如果不清除的话其他任务或中断也可以获取这个消息,而队列剩余大小依旧是 2。
QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength,UBaseType_t uxItemSize,uint8_t * pucQueueStorageBuffer, StaticQueue_t * pxQueueBuffer) 参数:uxQueueLength: 要创建的队列的队列长度,这里是队列的项目数。 uxItemSize: 队列中每个项目(消息)的长度,单位为字节 pucQueueStorage: 指向队列项目的存储区,也就是消息的存储区,这个存储区需要用户自行分配。此参数必须指向一个 uint8_t 类型 的数组。这个存储区要大于等于(uxQueueLength * uxItemsSize)字节。 pxQueueBuffer: 此参数指向一个 StaticQueue_t 类型的变量,用来保存队列结构体。 返回值:其他值: 队列创捷成功以后的队列句柄! NULL: 队列创建失败 QueueHandle_t xQueueGenericCreateStatic( const UBaseType_t uxQueueLength,const UBaseType_t uxItemSize, uint8_t * pucQueueStorage, StaticQueue_t * pxStaticQueue, const uint8_t ucQueueType ) 参数: uxQueueLength: 要创建的队列的队列长度,这里是队列的项目数。 uxItemSize: 队列中每个项目(消息)的长度,单位为字节 pucQueueStorage: 指向队列项目的存储区,也就是消息的存储区,这个存储区需要用户自 行分配。此参数必须指向一个 uint8_t 类型的数组。这个存储区要大于等 于(uxQueueLength * uxItemsSize)字节。 pxStaticQueue: 此参数指向一个 StaticQueue_t 类型的变量,用来保存队列结构体。 ucQueueType: 队列类型。 返回值: 其他值: 队列创捷成功以后队列句柄! NULL: 队列创建失败。
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize,const uint8_t ucQueueType ) 参数:uxQueueLength:要创建的队列的队列长度,这里是队列的项目数。 uxItemSize: 队列中每个项目(消息)的长度,单位为字节。 ucQueueType: 队列类型,由于 FreeRTOS 中的信号量等也是通过队列来实现的,创建信号量的函数最终也是使用此函数的,因此在创 建的时候需要指定此队列的用途,也就是队列类型,一共有六种类型: queueQUEUE_TYPE_BASE 普通的消息队列 queueQUEUE_TYPE_SET 队列集 queueQUEUE_TYPE_MUTEX 互斥信号量 queueQUEUE_TYPE_COUNTING_SEMAPHORE 计数型信号量 queueQUEUE_TYPE_BINARY_SEMAPHORE 二值信号量 queueQUEUE_TYPE_RECURSIVE_MUTEX 递归互斥信号量 函 数 xQueueCreate() 创 建 队 列 的 时 候 此 参 数 默 认 选 择 的 就 是 queueQUEUE_TYPE_BASE。 返回值:其他值: 队列创捷成功以后的队列句柄! NULL: 队列创建失败。 QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength,UBaseType_t uxItemSize) 此函数本质上是一个宏,用来动态创建队列,此宏最终调用的是函数 xQueueGenericCreate() 参数:uxQueueLength:要创建的队列的队列长度,这里是队列的项目数。 uxItemSize: 队列中每个项目(消息)的长度,单位为字节 返回值:其他值: 队列创捷成功以后的队列句柄! NULL: 队列创建失败。
队列复位和初始化在正点原子官方文档207页
函数 xQueueSend()、xQueueSendToBack()和xQueueSendToFront()三个函数都是用于向队列中发送消息的,这三个函数本质都是宏,其中函数 xQueueSend()和 xQueueSendToBack()是一样的,都是后向入队,即将新的消息插入到队列的后面。函数xQueueSendToToFront()是前向入队,即将新消息插入到队列的前面。然而!这三个函数最后都是调用的同一个函数:xQueueGenericSend()。
BaseType_t xQueueSend( QueueHandle_t xQueue,const void * pvItemToQueue,TickType_t xTicksToWait);
BaseType_t xQueueSendToBack(QueueHandle_t xQueue,const void* pvItemToQueue,TickType_t xTicksToWait);
BaseType_t xQueueSendToToFront(QueueHandle_t xQueue,const void *pvItemToQueue,TickType_t xTicksToWait);
参数:xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。
pvItemToQueue:指向要发送的消息,发送时候会将这个消息拷贝到队列中。
xTicksToWait: 阻塞时间,此参数指示当队列满的时候任务进入阻塞态等待队列空闲的最大时间。如果为 0 的话当队列满的时候就立 即返回;当为 portMAX_DELAY 的话就会一直等待,直到队列有空闲的队列 项,也就是死等,但是宏
INCLUDE_vTaskSuspend 必须为 1。
返回值:pdPASS: 向队列发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败。
BaseType_t xQueueOverwrite(QueueHandle_t xQueue,const void * pvItemToQueue);
参数:xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。
pvItemToQueue:指向要发送的消息,发送的时候会将这个消息拷贝到队列中。
返回值:pdPASS: 向队列发送消息成功,此函数也只会返回 pdPASS!因为此函数执行过程中不在乎队列满不满,满了的话我就覆写掉旧的数 据,总之肯定能成功。
此函数也是用于向队列发送数据的,当队列满了以后会覆写掉旧的数据,不管这个旧数据有没有被其他任务或中断取走。这个函数常用于向那些长度为 1 的队列发送消息,此函数也是一个宏,最终调用的也是函数 xQueueGenericSend(),
BaseType_t xQueueGenericSend( QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait, const BaseType_t xCopyPosition )
参数:xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。
pvItemToQueue:指向要发送的消息,发送的过程中会将这个消息拷贝到队列中。
xTicksToWait: 阻塞时间。
xCopyPosition: 入队方式,有三种入队方式:
queueSEND_TO_BACK: 后向入队
queueSEND_TO_FRONT: 前向入队
queueOVERWRITE: 覆写入队。
上面讲解的入队 API 函数就是通过此参数来决定采用哪种入队方式的。
返回值:pdTRUE: 向队列发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败。
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue,const void * pvItemToQueue,BaseType_t * pxHigherPriorityTaskWoken);
BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue,const void * pvItemToQueue,BaseType_t * pxHigherPriorityTaskWoken);
BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue,const void * pvItemToQueue,BaseType_t * pxHigherPriorityTaskWoken);
参数:xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。
pvItemToQueue:指向要发送的消息,发送的时候会将这个消息拷贝到队列中。
pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值由这三个函数来设置的,用户不用进行设置,用 户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:pdTRUE: 向队列中发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败。
这三个函数也是向队列中发送消息的,这三个函数用于中断服务函数中。这三个函数本质也宏,其中函数 xQueueSendFromISR ()和 xQueueSendToBackFromISR ()是一样的,都是后向入队,即将新的消息插入到队列的后面。函数 xQueueSendToFrontFromISR ()是前向入队,即将新消息插入到队列的前面。实际调用的也是函数 xQueueGenericSendFromISR()。
BaseType_t xQueueOverwriteFromISR(QueueHandle_t xQueue,const void * pvItemToQueue, BaseType_t * pxHigherPriorityTaskWoken);
参数、返回值与上面的是一样的
BaseType_t xQueueGenericSendFromISR(QueueHandle_t xQueue,const void* pvItemToQueue,BaseType_t* pxHigherPriorityTaskWoken,BaseType_t xCopyPosition);
参数:xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。
pvItemToQueue:指向要发送的消息,发送的过程中会将这个消息拷贝到队列中。
pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值由这三个函数来设置的,用户不用进行设置,用 户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
xCopyPosition: 入队方式,有三种入队方式:
queueSEND_TO_BACK: 后向入队
queueSEND_TO_FRONT: 前向入队
queueOVERWRITE: 覆写入队。
返回值:pdTRUE: 向队列发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败。
任务级通用入队函数和中断级通用入队函数的时候都提到了队列的上锁和解锁,队列的上锁和解锁是两个 API 函数:prvLockQueue()和 prvUnlockQueue()。
源码在文档218页
BaseType_t xQueueReceive(QueueHandle_t xQueue,void * pvBuffer,TickType_t xTicksToWait);
参数:xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄。
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。
xTicksToWait: 阻塞时间,此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最大时间。如果为 0 的话当队列空的时候就 立即返回;当为 portMAX_DELAY的 话 就 会 一 直 等 待 , 直 到 队 列 有 数 据 , 也 就 是 死 等 , 但 是 宏
INCLUDE_vTaskSuspend 必须为 1。
返回值:pdTRUE: 从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
此函数用于在任务中从队列中读取一条(请求)消息,读取成功以后就会将队列中的这条数据删除,此函数的本质是一个宏,真正执行的函数是 xQueueGenericReceive()。此函数在读取消息的时候是采用拷贝方式的,所以用户需要提供一个数组或缓冲区来保存读取到的数据,所读取的数据长度是创建队列的时候所设定的每个队列项目的长度
BaseType_t xQueuePeek(QueueHandle_t xQueue,void * pvBuffer,TickType_t xTicksToWait);
参数:xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄。
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。
xTicksToWait: 阻塞时间,此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最大时间。如果为 0 的话当队列空的时候就 立即返回;当为 portMAX_DELAY的 话 就 会 一 直 等 待 , 直 到 队 列 有 数 据 , 也 就 是 死 等 , 但 是 宏
INCLUDE_vTaskSuspend 必须为 1。
返回值:pdTRUE: 从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
BaseType_t xQueueGenericReceive(QueueHandle_t xQueue,void* pvBuffer,TickType_t xTicksToWait,BaseType_t xJustPeek)
参数:xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄。
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。
xTicksToWait: 阻塞时间,此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最大时间。如果为 0 的话当队列空的时候就 立即返回;当为 portMAX_DELAY的 话 就 会 一 直 等 待 , 直 到 队 列 有 数 据 , 也 就 是 死 等 , 但 是 宏
INCLUDE_vTaskSuspend 必须为 1。
xJustPeek: 标记当读取成功以后是否删除掉队列项,当为 pdTRUE 的时候就不用删除,也就是说你后面再调用函数xQueueReceive() 获取到的队列项是一样的。当为pdFALSE 的时候就会删除掉这个队列项。
返回值:pdTRUE: 从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
不 管 是 函 数 xQueueReceive() 还 是 xQueuePeek() ,最终都是调用的函数xQueueGenericReceive(),此函数是真正干事的
BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue,void* pvBuffer,BaseType_t * pxTaskWoken);
参数:xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄。
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。
pxTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值是由函数来设置的,用户不用进行设置,用户只需要提供一个变量 来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:pdTRUE: 从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue,void * pvBuffer)
参数:xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄。
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。
返回值:pdTRUE: 从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
此函数是 xQueuePeek()的中断版本,此函数在读取成功以后不会将消息删除
例程在正点原子官方代码文档230页
FreeRTOS 提供了 5 种内存分配方法,FreeRTOS 使用者可以其中的某一个方法,或者自己的内存分配方法。这 5 种方法是 5 个文件,分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c 和heap_5.c。
heap_1 实现起来就是当需要 RAM 的时候就从一个大数组(内存堆)中分一小块出来,大数组(内存堆)的容量为 configTOTAL_HEAP_SIZE,上面已经说了。使用函数 xPortGetFreeHeapSize()可以获取内存堆中剩余内存大小。
heap_1 特性如下:
1、适用于那些一旦创建好任务、信号量和队列就再也不会删除的应用,实际上大多数的FreeRTOS 应用都是这样的。
2、具有可确定性(执行所花费的时间大多数都是一样的),而且不会导致内存碎片。
3、代码实现和内存分配过程都非常简单,内存是从一个静态数组中分配到的,也就是适合于那些不需要动态内存分配的应用。
heap_2 的特性如下:
1、可以使用在那些可能会重复的删除任务、队列、信号量等的应用中,要注意有内存碎片产生!
2、如果分配和释放的内存 n 大小是随机的,那么就要慎重使用了,比如下面的示例:
● 如果一个应用动态的创建和删除任务,而且任务需要分配的堆栈大小都是一样的,那么 heap_2 就非常合适。如果任务所需的堆栈大小每次都是不同,那么 heap_2 就不适合了,因为这样会导致内存碎片产生,最终导致任务分配不到合适的堆栈!不过 heap_4 就很适合这种场景了。
● 如果一个应用中所使用的队列存储区域每次都不同,那么 heap_2 就不适合了,和上面一样,此时可以使用 heap_4。
● 应用需要调用 pvPortMalloc()和 vPortFree()来申请和释放内存,而不是通过其他FreeRTOS 的其他 API 函数来间接的调用,这种情况下 heap_2 不适合。
3、如果应用中的任务、队列、信号量和互斥信号量具有不可预料性(如所需的内存大小不能确定,每次所需的内存都不相同,或者说大多数情况下所需的内存都是不同的)的话可能会导致内存碎片。虽然这是小概率事件,但是还是要引起我们的注意!
4、具有不可确定性,但是也远比标准 C 中的 mallo()和 free()效率高!heap_2 基本上可以适用于大多数的需要动态分配内存的工程中,而 heap_4 更是具有将内存碎片合并成一个大的空闲内存块(就是内存碎片回收)的功能。
同 heap_1 一样,heap_2 整个内存堆为 ucHeap[],大小为 configTOTAL_HEAP_SIZE。可以通过函数 xPortGetFreeHeapSize()来获取剩余的内存大小。
heap_3 的特性如下:
1、需要编译器提供一个内存堆,编译器库要提供 malloc()和 free()函数。比如使用 STM32的话可以通过修改启动文件中的 Heap_Size 来修改内存堆的大小
2、具有不确定性
3、可能会增加代码量。
注意,在 heap_3 中 configTOTAL_HEAP_SIZE 是没用的!
heap_4 特性如下:
1、可以用在那些需要重复创建和删除任务、队列、信号量和互斥信号量等的应用中。
2、不会像 heap_2 那样产生严重的内存碎片,即使分配的内存大小是随机的。
3、具有不确定性,但是远比 C 标准库中的 malloc()和 free()效率高。heap_4 非常适合于那些需要直接调用函数 pvPortMalloc()和 vPortFree()来申请和释放内存的应用,注意,我们移植 FreeRTOS 的时候就选择的 heap_4!heap_4 也使用链表结构来管理空闲内存块,链表结构体与 heap_2 一样。heap_4 也定义了两个局部静态变量 xStart 和 pxEnd 来表示链表头和尾,其中 pxEnd 是指向 BlockLink_t 的指针。
heap_5 使用了和 heap_4 相同的合并算法,内存管理实现起来基本相同,但是 heap_5 允许内存堆跨越多个不连续的内存段。比如 STM32 的内部 RAM 可以作为内存堆,但是 STM32 内 部 RAM 比较小,遇到那些需要大容量 RAM 的应用就不行了,如音视频处理。不过 STM32 可以外接 SRAM 甚至大容量的 SDRAM,如果使用 heap_4 的话你就只能在内部 RAM 和外部SRAM 或 SDRAM 之间二选一了,使用 heap_5 的话就不存在这个问题,两个都可以一起作为内存堆来用。如果使用 heap_5 的话,在调用 API 函数之前需要先调用函数 vPortDefineHeapRegions ()来对内存堆做初始化处理,在 vPortDefineHeapRegions()未执行完之前禁止调用任何可能会调用pvPortMalloc()的 API 函数!比如创建任务、信号量、队列等函数。函数 vPortDefineHeapRegions()只有一个参数,参数是一个 HeapRegion_t 类型的数组,HeapRegion 为一个结构体
heap_5 和 heap_4 基本是一样的
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。