STM32CubeMX学习笔记(35)——FreeRTOS实时操作系统使用(内存管理)
Posted Leung_ManWah
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STM32CubeMX学习笔记(35)——FreeRTOS实时操作系统使用(内存管理)相关的知识,希望对你有一定的参考价值。
一、FreeRTOS简介
FreeRTOS 是一个可裁剪、可剥夺型的多任务内核,而且没有任务数限制。FreeRTOS 提供了实时操作系统所需的所有功能,包括资源管理、同步、任务通信等。
FreeRTOS 是用 C 和汇编来写的,其中绝大部分都是用 C 语言编写的,只有极少数的与处理器密切相关的部分代码才是用汇编写的,FreeRTOS 结构简洁,可读性很强!最主要的是非常适合初次接触嵌入式实时操作系统学生、嵌入式系统开发人员和爱好者学习。
最新版本 V9.0.0(2016年),尽管现在 FreeRTOS 的版本已经更新到 V10.4.1 了,但是我们还是选择 V9.0.0,因为内核很稳定,并且网上资料很多,因为 V10.0.0 版本之后是亚马逊收购了FreeRTOS之后才出来的版本,主要添加了一些云端组件,一般采用 V9.0.0 版本足以。
二、新建工程
1. 打开 STM32CubeMX 软件,点击“新建工程”
2. 选择 MCU 和封装
3. 配置时钟
RCC 设置,选择 HSE(外部高速时钟) 为 Crystal/Ceramic Resonator(晶振/陶瓷谐振器)
选择 Clock Configuration,配置系统时钟 SYSCLK 为 72MHz
修改 HCLK 的值为 72 后,输入回车,软件会自动修改所有配置
4. 配置调试模式
非常重要的一步,否则会造成第一次烧录程序后续无法识别调试器
SYS 设置,选择 Debug 为 Serial Wire
三、SYS Timebase Source
在 System Core
中选择 SYS
,对 Timebase Source
进行设置,选择 TIM1
作为HAL库的时基(除了 SysTick
外都可以)。
在基于STM32 HAL的项目中,一般需要维护的 “时基” 主要有2个:
- HAL的时基,SYS Timebase Source
- OS的时基(仅在使用OS的情况下才考虑)
而这些 “时基” 该去如何维护,主要分为两种情况考虑:
-
裸机运行:
可以通过SysTick
(滴答定时器)或 (TIMx
)定时器 的方式来维护SYS Timebase Source
,也就是HAL库中的uwTick
,这是HAL库中维护的一个全局变量。在裸机运行的情况下,我们一般选择默认的SysTick
(滴答定时器) 方式即可,也就是直接放在SysTick_Handler()
中断服务函数中来维护。 -
带OS运行:
前面提到的SYS Timebase Source
是STM32的HAL库中的新增部分,主要用于实现HAL_Delay()
以及作为各种 timeout 的时钟基准。在使用了OS(操作系统)之后,OS的运行也需要一个时钟基准(简称“时基”),来对任务和时间等进行管理。而OS的这个 时基 一般也都是通过
SysTick
(滴答定时器) 来维护的,这时就需要考虑 “HAL的时基” 和 “OS的时基” 是否要共用SysTick
(滴答定时器) 了。如果共用SysTick,当我们在CubeMX中选择启用FreeRTOS之后,在生成代码时,CubeMX一定会报如下提示:
强烈建议用户在使用FreeRTOS的时候,不要使用
SysTick
(滴答定时器)作为 “HAL的时基”,因为FreeRTOS要用,最好是要换一个!!!如果共用,潜在一定风险。
四、FreeRTOS
4.1 参数配置
在 Middleware
中选择 FREERTOS
设置,并选择 CMSIS_V1
接口版本
CMSIS是一种接口标准,目的是屏蔽软硬件差异以提高软件的兼容性。RTOS v1使得软件能够在不同的实时操作系统下运行(屏蔽不同RTOS提供的API的差别),而RTOS v2则是拓展了RTOS v1,兼容更多的CPU架构和实时操作系统。因此我们在使用时可以根据实际情况选择,如果学习过程中使用STM32F1、F4等单片机时没必要选择RTOS v2,更高的兼容性背后时更加冗余的代码,理解起来比较困难。
在 Config parameters
进行具体参数配置。
Kernel settings:
- USE_PREEMPTION:
Enabled
:RTOS使用抢占式调度器;Disabled:RTOS使用协作式调度器(时间片)。 - TICK_RATE_HZ: 值设置为
1000
,即周期就是1ms。RTOS系统节拍中断的频率,单位为HZ。 - MAX_PRIORITIES: 可使用的最大优先级数量。设置好以后任务就可以使用从0到(MAX_PRIORITIES - 1)的优先级,其中0位最低优先级,(MAX_PRIORITIES - 1)为最高优先级。
- MINIMAL_STACK_SIZE: 设置空闲任务的最小任务堆栈大小,以字为单位,而不是字节。如该值设置为
128
Words,那么真正的堆栈大小就是 128*4 = 512 Byte。 - MAX_TASK_NAME_LEN: 设置任务名最大长度。
- IDLE_SHOULD_YIELD:
Enabled
空闲任务放弃CPU使用权给其他同优先级的用户任务。 - USE_MUTEXES: 为1时使用互斥信号量,相关的API函数会被编译。
- USE_RECURSIVE_MUTEXES: 为1时使用递归互斥信号量,相关的API函数会被编译。
- USE_COUNTING_SEMAPHORES: 为1时启用计数型信号量, 相关的API函数会被编译。
- QUEUE_REGISTRY_SIZE: 设置可以注册的队列和信号量的最大数量,在使用内核调试器查看信号量和队列的时候需要设置此宏,而且要先将消息队列和信号量进行注册,只有注册了的队列和信号量才会在内核调试器中看到,如果不使用内核调试器的话次宏设置为0即可。
- USE_APPLICATION_TASK_TAG: 为1时可以使用vTaskSetApplicationTaskTag函数。
- ENABLE_BACKWARD_COMPATIBILITY: 为1时可以使V8.0.0之前的FreeRTOS用户代码直接升级到V8.0.0之后,而不需要做任何修改。
- USE_PORT_OPTIMISED_TASK_SELECTION: FreeRTOS有两种方法来选择下一个要运行的任务,一个是通用的方法,另外一个是特殊的方法,也就是硬件方法,使用MCU自带的硬件指令来实现。STM32有计算前导零指令吗,所以这里强制置1。
- USE_TICKLESS_IDLE: 置1:使能低功耗tickless模式;置0:保持系统节拍(tick)中断一直运行。假设开启低功耗的话可能会导致下载出现问题,因为程序在睡眠中,可用ISP下载办法解决。
- USE_TASK_NOTIFICATIONS: 为1时使用任务通知功能,相关的API函数会被编译。开启了此功能,每个任务会多消耗8个字节。
- RECORD_STACK_HIGH_ADDRESS: 为1时栈开始地址会被保存到每个任务的TCB中(假如栈是向下生长的)。
Memory management settings:
- Memory Allocation:
Dynamic/Static
支持动态/静态内存申请 - TOTAL_HEAP_SIZE: 设置堆大小,如果使用了动态内存管理,FreeRTOS在创建 task, queue, mutex, software timer or semaphore的时候就会使用heap_x.c(x为1~5)中的内存申请函数来申请内存。这些内存就是从堆ucHeap[configTOTAL_HEAP_SIZE]中申请的。
- Memory Management scheme: 内存管理策略
heap_4
。
Hook function related definitions:
- USE_IDLE_HOOK: 置1:使用空闲钩子(Idle Hook类似于回调函数);置0:忽略空闲钩子。
- USE_TICK_HOOK: 置1:使用时间片钩子(Tick Hook);置0:忽略时间片钩子。
- USE_MALLOC_FAILED_HOOK: 使用内存申请失败钩子函数。
- CHECK_FOR_STACK_OVERFLOW: 大于0时启用堆栈溢出检测功能,如果使用此功能用户必须提供一个栈溢出钩子函数,如果使用的话此值可以为1或者2,因为有两种栈溢出检测方法。
Run time and task stats gathering related definitions:
- GENERATE_RUN_TIME_STATS: 启用运行时间统计功能。
- USE_TRACE_FACILITY: 启用可视化跟踪调试。
- USE_STATS_FORMATTING_FUNCTIONS: 与宏configUSE_TRACE_FACILITY同时为1时会编译下面3个函数prvWriteNameToBuffer()、vTaskList()、vTaskGetRunTimeStats()。
Co-routine related definitions:
- USE_CO_ROUTINES: 启用协程。
- MAX_CO_ROUTINE_PRIORITIES: 协程的有效优先级数目。
Software timer definitions:
- USE_TIMERS: 启用软件定时器。
Interrupt nesting behaviour configuration:
- LIBRARY_LOWEST_INTERRUPT_PRIORITY: 中断最低优先级。
- LIBRARY_LOWEST_INTERRUPT_PRIORITY: 系统可管理的最高中断优先级。
4.2 内存管理策略
在 Config parameters
的 Memory management settings
中默认内存管理策略为 heap_4
五、UART串口打印
查看 STM32CubeMX学习笔记(6)——USART串口使用
六、生成代码
输入项目名和项目路径
选择应用的 IDE 开发环境 MDK-ARM V5
每个外设生成独立的 ’.c/.h’
文件
不勾:所有初始化代码都生成在 main.c
勾选:初始化代码生成在对应的外设文件。 如 GPIO 初始化代码生成在 gpio.c 中。
点击 GENERATE CODE 生成代码
七、内存管理
7.1 基本概念
FreeRTOS 操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些内存管理函数是如何实现的,所以在 FreeRTOS 中提供了多种内存分配算法(分配策略),但是上层接口(API)却是统一的。这样做可以增加系统的灵活性:用户可以选择对自己更有利的内存管理策略,在不同的应用场合使用不同的内存分配策略。
在嵌入式程序设计中内存分配应该是根据所设计系统的特点来决定选择使用动态内存分配还是静态内存分配算法,一些可靠性要求非常高的系统应选择使用静态的,而普通的业务系统可以使用动态来提高内存使用效率。静态可以保证设备的可靠性但是需要考虑内存上限,内存使用效率低,而动态则是相反。
每当任务,队列或是信号量被创建时,内核需要进行动态内存分配。虽然可以调用标准的 malloc()与 free()库函数,但必须承担以下若干问题:
- 这两个函数在小型嵌入式系统中可能不可用,小型嵌入式设备中的 RAM 不足。
- 这两个函数的具体实现可能会相对较大,会占用较多宝贵的代码空间。
- 这两个函数通常不具备线程安全特性。
- 这两个函数具有不确定性。每次调用时的时间开销都可能不同。
- 这两个函数会产生内存碎片。
- 这两个函数会使得链接器配置得复杂。
FreeRTOS 对内存管理做了很多事情,FreeRTOS 的 V9.0.0 版本为我们提供了 5 种内存管理算法,分别是 heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c,源文件存放于 FreeRTOS\\Source\\portable\\MemMang 路径下,在使用的时候选择其中一个添加到我们的工程中去即可。
FreeRTOS 的内存管理模块通过对内存的申请、释放操作,来管理用户和系统对内存的使用,使内存的利用率和使用效率达到最优,同时最大限度地解决系统可能产生的内存碎片问题。
7.2 内存管理方案
7.2.1 heap_1.c
heap_1.c 管理方案是 FreeRTOS 提供所有内存管理方案中最简单的一个,它只能申请内存而不能进行内存释放,并且申请内存的时间是一个常量,这样子对于要求安全的嵌入式设备来说是最好的,因为不允许内存释放,就不会产生内存碎片而导致系统崩溃,但是也有缺点,那就是内存利用率不高,某段内存只能用于内存申请的地方,即使该内存只使用一次,也无法让系统回收重新利用。
实际上,大多数的嵌入式系统并不会经常动态申请与释放内存,一般都是在系统完成的时候,就一直使用下去,永不删除,所以这个内存管理方案实现简洁、安全可靠,使用的非常广泛。
内 存 分 配 时 需 要 的 总 的 堆 空 间 由 文 件
FreeRTOSConfig.h
中 的 宏·configTOTAL_HEAP_SIZE
配置,单位为字。通过调用函数 xPortGetFreeHeapSize() 我们可以知道还剩下多少内存没有使用,但是并不包括内存碎片。这样一来我们可以实时的调整和优化configTOTAL_HEAP_SIZE
的大小。heap_1.c 实现了一个非常基本的 pvPortMalloc() 版本,而且没有实现 vPortFree()。
heap1.c 方案具有以下特点:
- 用于从不删除任务、队列、信号量、互斥量等的应用程序(实际上大多数使用 FreeRTOS 的应用程序都符合这个条件)。
- 函数的执行时间是确定的并且不会产生内存碎片。
7.2.2 heap_2.c
heap_2.c 方案与 heap_1.c 方案采用的内存管理算法不一样,它采用一种最佳匹配算法(best fit algorithm),比如我们申请 100 字节的内存,而可申请内存中有三块对应大小 200 字节, 500 字节和 1000 字节大小的内存块,按照算法的最佳匹配,这时候系统会把 200 字节大小的内存块进行分割并返回申请内存的起始地址,剩余的内存则插回链表留待下次申请。heap_2.c 方案支持释放申请的内存,但是它不能把相邻的两个小的内存块合成一个大的内存块,对于每次申请内存大小都比较固定的,这个方式是没有问题的,而对于每次申请并不是固定内存大小的则会造成内存碎片,后面要讲解的 heap_4.c 方案采用的内存管理算法能解决内存碎片的问题,可以把这些释放的相邻的小的内存块合并成一个大的内存块。
同样的,内存分配时需要的总的内存堆空间由文件
FreeRTOSConfig.h
中的宏configTOTAL_HEAP_SIZE
配置,单位为字。通过调用函数 xPortGetFreeHeapSize() 我们可以知道还剩下多少内存没有使用,但是并不包括内存碎片,这样一来我们可以实时的调整和优化configTOTAL_HEAP_SIZE
的大小。
heap_2.c 方案具有以下特点:
- 可以用在那些反复的删除任务、队列、信号量、等内核对象且不担心内存碎片的应用程序。
- 如果我们的应用程序中的队列、任务、信号量、等工作在一个不可预料的顺序,这样子也有可能会导致内存碎片。
- 具有不确定性,但是效率比标准 C 库中的 malloc 函数高得多。
- 不能用于那些内存分配和释放是随机大小的应用程序。
7.2.3 heap_3.c
heap_3.c 方案只是简单的封装了标准 C 库中的 malloc()和 free()函数,并且能满足常用的编译器。重新封装后的 malloc()和 free()函数具有保护功能,采用的封装方式是操作内存前挂起调度器、完成后再恢复调度器。
heap_3.c 方案具有以下特点:
- 需要链接器设置一个堆,malloc()和 free()函数由编译器提供。
- 具有不确定性。
- 很可能增大 RTOS 内核的代码大小。
要注意的是在 使 用 heap_3.c 方 案 时 , FreeRTOSConfig.h 文件中的 configTOTAL_HEAP_SIZE
宏定义不起作用。在 STM32 系列的工程中,这个由编译器定义的堆都在启动文件里面设置,单位为字节,我们具体以 STM32F10x 系列为例
7.2.4 heap_4.c
heap_4.c 方案与 heap_2.c 方案一样都采用最佳匹配算法来实现动态的内存分配,但是不一样的是 heap_4.c 方案还包含了一种合并算法,能把相邻的空闲的内存块合并成一个更大的块,这样可以减少内存碎片。heap_4.c 方案特别适用于移植层中可以直接使用 pvPortMalloc()和 vPortFree()函数来分配和释放内存的代码。
内 存 分 配 时 需 要 的 总 的 堆 空 间 由 文 件
FreeRTOSConfig.h
中 的 宏·configTOTAL_HEAP_SIZE
配置,单位为字。通过调用函数 xPortGetFreeHeapSize() 我们可以知道还剩下多少内存没有使用,但是并不包括内存碎片。这样一来我们可以实时的调整和优化configTOTAL_HEAP_SIZE
的大小。heap_4.c 方案的空闲内存块也是以单链表的形式连接起来的,BlockLink_t 类型的局部静态变量 xStart 表示链表头,但 heap_4.c 内存管理方案的链表尾部则保存在内存堆空间最后位置,并使用 BlockLink_t 指针类型局部静态变量 pxEnd 指向这个区域(而 heap_2.c 内存管理方案则使用 BlockLink_t 类型的静态变量 xEnd 表示链表尾)
heap_4.c 内存管理方案的空闲块链表不是以内存块大小进行排序的,而是以内存块起始地址大小排序,内存地址小的在前,地址大的在后,因为 heap_4.c 方案还有一个内存合并算法,在释放内存的时候,假如相邻的两个空闲内存块在地址上是连续的,那么就可以合并为一个内存块,这也是为了适应合并算法而作的改变。
heap_4.c 方案具有以下特点:
- 可用于重复删除任务、队列、信号量、互斥量等的应用程序。
- 可用于分配和释放随机字节内存的应用程序,但并不像 heap2.c 那样产生严重的内存碎片。
- 具有不确定性,但是效率比标准 C 库中的 malloc 函数高得多。
7.2.5 heap_5.c
heap_5.c 方案在实现动态内存分配时与 heap4.c 方案一样,采用最佳匹配算法和合并算法,并且允许内存堆跨越多个非连续的内存区,也就是允许在不连续的内存堆中实现内存分配,比如用户在片内 RAM 中定义一个内存堆,还可以在外部 SDRAM 再定义一个或多个内存堆,这些内存都归系统管理。
heap_5.c 方案通过调用 vPortDefineHeapRegions()函数来实现系统管理的内存初始化,在内存初始化未完成前不允许使用内存分配和释放函数。如创建 FreeRTOS 对象(任务、队列、信号量等)时会隐式的调用 pvPortMalloc()函数,因此必须注意:使用 heap_5.c 内存管理方案创建任何对象前,要先调用 vPortDefineHeapRegions()函数将内存初始化。
heap_5.c 方案具有以下特点:
- 可用于重复删除任务、队列、信号量、互斥量等的应用程序。
- 可用于分配和释放随机字节内存的应用程序,但并不像 heap2.c 那样产生严重的内存碎片。
- 具有不确定性,但是效率比标准 C 库中的 malloc 函数高得多。
- 允许内存堆跨越多个非连续的内存区,也就是允许在不连续的内存堆中实现内存分配。
八、相关API说明
8.1 osPoolCreate
创建内存池。
函数 | osPoolId osPoolCreate (const osPoolDef_t *pool_def) |
---|---|
参数 | pool_def: 引用由osThreadDef定义的任务 |
返回值 | 成功返回内存池ID,失败返回0 |
8.2 osPoolAlloc
通过内存池开辟空间。
函数 | void *osPoolAlloc (osPoolId pool_id) |
---|---|
参数 | pool_id: 内存池ID |
返回值 | 成功返回分配的内存块地址,失败返回0 |
8.3 osPoolCAlloc
通过内存池开辟空间,并初始化为0。
函数 | void *osPoolCAlloc (osPoolId pool_id) |
---|---|
参数 | pool_id: 内存池ID |
返回值 | 成功返回分配的内存块地址,失败返回0 |
8.4 osPoolFree
释放开辟的空间。
函数 | osStatus osPoolFree (osPoolId pool_id, void *block) |
---|---|
参数 | pool_id: 内存池ID 要返回内存池的内存块地址 |
返回值 | 错误码 |
九、示例
Memory Handler定义
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
osPoolId poolHandle;
/* USER CODE END PV */
Memory分配
/* USER CODE BEGIN Header_StartDefaultTask */
/**
* @brief Function implementing the defaultTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void const * argument)
/* USER CODE BEGIN 5 */
osPoolDef(Memory, 10, uint8_t); //定义了10个大小为uint8_t的内存池
poolHandle = osPoolCreate(osPool(Memory)); //创建内存池
uint8_t *pBuffer = osPoolAlloc(poolHandle); //申请1个内存地址
/* Infinite loop */
for(;;)
osDelay(1);
/* USER CODE END 5 */
十、注意事项
用户代码要加在 USER CODE BEGIN N
和 USER CODE END N
之间,否则下次使用 STM32CubeMX 重新生成代码后,会被删除。
• 由 Leung 写于 2022 年 1 月 6 日
• 参考:FreeRTOS STM32CubeMX配置 内存管理 任务管理
STM32内存结构介绍,FreeRTOS内存分配技巧,Stack_Size和Heap_Size大小设置
STM32CubeIDE(十一):FreeRTOS选项中Disable、CMSIS_V1和CMSIS_V2的区别
HAL库中的 SYS Timebase Source 和 SysTick_Handler()
以上是关于STM32CubeMX学习笔记(35)——FreeRTOS实时操作系统使用(内存管理)的主要内容,如果未能解决你的问题,请参考以下文章
STM32学习笔记 二基于STM32F103C8T6和STM32CubeMX实现UART串口通信数据收发
STM32学习笔记 二基于STM32F103C8T6和STM32CubeMX实现UART串口通信数据收发
STM32学习笔记 一基于STM32F103C8T6最小系统板和STM32CubeMX实现板载LED灯循环闪烁
STM32CubeMX学习笔记(32)——FreeRTOS实时操作系统使用(事件)