ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32F4移植FreeRTOS实战:从内核配置到多任务通信避坑指南

STM32F4移植FreeRTOS实战:从内核配置到多任务通信避坑指南 1. 项目缘起为什么要在STM32F4上折腾FreeRTOS如果你手头有一块STM32F4系列的开发板比如经典的STM32F407或者F429并且已经玩转了裸机编程点亮过LED驱动过串口那你大概率会开始琢磨下一步怎么让这个强大的Cortex-M4内核同时干好几件事比如一边采集传感器数据一边通过Wi-Fi上传同时还能响应按键操作更新屏幕显示。在裸机环境下你可能会用超级循环配合状态机或者上前后台系统但代码很快就会变得复杂且难以维护任务间的优先级和响应实时性也很难保证。这时候一个成熟、免费且开源的实时操作系统RTOS就成了刚需。FreeRTOS正是这个领域的佼佼者它内核小巧可裁剪性强社区生态极其丰富几乎成了嵌入式实时系统的“事实标准”。将FreeRTOS移植到STM32F4上意味着你为你的项目引入了一个强大的任务调度器、一套高效的进程间通信机制队列、信号量、事件组等以及更可靠的内存和时间管理能力。这不仅仅是技术上的升级更是开发思维从“顺序执行”到“并发任务”的转变。我最初接触FreeRTOS移植时也被各种配置项和编译错误搞得头大但一旦跑通那种“解放生产力”的感觉是无与伦比的。本文将基于我多次在STM32F4标准库和HAL库环境下移植FreeRTOS的经验手把手带你走通流程并重点分享那些官方手册里不会写的“坑”和技巧。2. 移植前的战略准备选型、源码与工程框架在动手写一行代码之前清晰的准备工作能避免一半的麻烦。很多人拿到FreeRTOS源码包就直接往工程里扔这是最容易导致编译失败和后续诡异问题的根源。2.1 FreeRTOS源码获取与版本选择首先去FreeRTOS的官方网站或GitHub仓库下载源码。我强烈建议直接从GitHub克隆最新稳定版因为里面包含了所有历史版本和针对不同编译器、不同芯片的移植层代码。源码包的结构通常如下FreeRTOS/ ├── Source/ │ ├── include/ (核心头文件如 task.h, queue.h) │ ├── portable/ (移植层代码这是我们的重点) │ │ ├── GCC/ │ │ ├── IAR/ │ │ ├── Keil/ (ARMCC/ARMClang) │ │ └── [其他编译器] │ └── croutine.c (协程现在很少用) │ └── event_groups.c │ └── list.c │ └── queue.c │ └── tasks.c (核心调度器) │ └── timers.c └── Demo/ (各种芯片的演示工程参考价值极大)对于STM32F4我们关心的是portable目录。因为STM32F4是基于ARM Cortex-M4内核的所以我们需要portable/GCC/ARM_CM4F如果你用GCC编译器如STM32CubeIDE或TrueSTUDIO或者portable/Keil/ARM_CM4F如果你用Keil MDK-ARM。注意后面的CM4F这个“F”至关重要它表示芯片带有硬件浮点单元FPU而STM32F4系列是具备FPU的。如果你错误地选择了ARM_CM4不带FPU的端口在任务切换涉及浮点寄存器时可能会发生数据损坏或硬错误。2.2 工程框架搭建标准库 vs HAL库 vs CubeMX这是第二个关键决策点。你的STM32F4工程是基于标准外设库StdPeriph Lib、硬件抽象层库HAL还是直接使用STM32CubeMX生成标准库相对老旧但直接代码量小执行效率高。如果你接手的是一个老项目或者对芯片寄存器操作有偏好可能会选它。移植FreeRTOS时需要手动配置滴答定时器SysTick或另一个通用定时器作为系统时钟节拍。HAL库与CubeMX这是ST主推的现代开发方式。强烈推荐给新手和大多数项目。STM32CubeMX可以图形化配置FreeRTOS自动生成初始化代码、任务骨架并处理好与HAL库的时间基准冲突问题能节省大量时间避免低级错误。我的建议是除非有历史包袱否则一律使用STM32CubeMX HAL库开始你的FreeRTOS项目。它能帮你处理好时钟树配置、外设初始化、FreeRTOS内核参数配置以及最重要的——系统滴答定时器SysTick的归属权问题。在裸机中HAL库依赖SysTick提供HAL_Delay()的时基。在FreeRTOS中SysTick需要被FreeRTOS接管以进行任务调度。CubeMX会自动将HAL的时基切换到另一个定时器如TIM1从而完美解决冲突。2.3 文件筛选往工程里添加哪些文件无论是否使用CubeMX你都需要将FreeRTOS的源码文件添加到你的工程中。不需要全部添加只添加必需的即可以减小工程体积。核心必需文件位于FreeRTOS/Source/tasks.c- 任务管理queue.c- 队列管理list.c- 内核内部列表timers.c- 软件定时器如果你需要的话event_groups.c- 事件组如果你需要的话heap_x.c- 内存管理方案从portable/MemMang/里选一个常用heap_4.c它支持内存碎片合并移植层文件关键根据你的编译器和芯片内核选择。例如用Keil MDK for ARM就添加portable/Keil/ARM_CM4F/port.c。对应的头文件路径也要包含FreeRTOS/Source/include和FreeRTOS/Source/portable/Keil/ARM_CM4F。配置文件FreeRTOSConfig.h这是FreeRTOS的“大脑”所有内核功能的开关、参数定义都在这里。CubeMX会自动生成这个文件。如果手动移植你可以从Demo文件夹里找一个STM32F4相近的工程复制过来修改这是移植成败的核心。注意手动复制文件时务必注意编译器的区别。Keil ARMCC和GCC的移植层port.c汇编部分写法不同不可混用。3. 核心战场深度解析与配置FreeRTOSConfig.h这个文件是移植的灵魂也是问题高发区。我们逐项解析关键配置并解释“为什么”。// FreeRTOSConfig.h 片段示例 #define configUSE_PREEMPTION 1 // 1使用抢占式调度0使用协作式。务必设为1发挥RTOS价值。 #define configUSE_TIME_SLICING 1 // 1启用时间片轮转同优先级任务轮流执行。通常开启。 #define configUSE_IDLE_HOOK 0 // 1启用空闲任务钩子函数可用于低功耗处理。按需开启。 #define configUSE_TICK_HOOK 0 // 1启用时钟节拍钩子函数。慎用会影响定时精度。 #define configCPU_CLOCK_HZ ( ( unsigned long ) 168000000 ) // CPU主频STM32F407常为168MHz #define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) // 系统节拍频率即1ms一个tick。常用1000(1ms)或100(10ms)。configCPU_CLOCK_HZ和configTICK_RATE_HZ这两个参数共同决定了SysTick重装载值。计算公式是重装载值 (CPU时钟频率 / configTICK_RATE_HZ) - 1。例如168MHz / 1000Hz 168000减1后为167999。这个计算由port.c中的vPortSetupTimerInterrupt()函数完成如果使用SysTick。务必保证这里填写的CPU频率和你的系统时钟设置一致否则系统时间会快或慢。configTOTAL_HEAP_SIZE这是FreeRTOS动态内存堆的总大小单位字节。所有任务栈、队列、信号量等内核对象都从这个堆里分配如果你使用heap_1.c, heap_2.c, heap_3.c, heap_4.c, heap_5.c。#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 30 * 1024 ) ) // 例如分配30KB堆这里是个大坑很多人任务创建失败返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY根源就是这里设小了。你需要估算每个任务的栈大小在xTaskCreate参数里设总和加上你创建的队列、信号量等对象的大小。保险起见在资源允许的情况下可以设大一点比如50KB-100KB然后通过xPortGetFreeHeapSize()函数在运行时监控剩余堆空间。configMINIMAL_STACK_SIZE空闲任务Idle Task的栈大小单位是字Word对于32位机是4字节。这个值不能太小如果空闲任务栈溢出系统会崩溃。通常设为128-256字即512-1024字节是比较安全的。configMAX_PRIORITIES最大优先级数。FreeRTOS优先级数越高优先级越高。这个值决定了优先级就绪列表数组的大小。不宜设得过大够用即可比如5-15。设太大会浪费RAM。configUSE_MUTEXESconfigUSE_RECURSIVE_MUTEXESconfigUSE_COUNTING_SEMAPHORES根据你的需求开启互斥锁、递归锁、计数信号量等功能。如果你要用xSemaphoreCreateMutex()就必须把configUSE_MUTEXES设为1否则编译虽然可能通过但运行时创建会失败。中断优先级配置STM32F4关键#define configKERNEL_INTERRUPT_PRIORITY 255 // 对应Cortex-M的优先级最低 #define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 // 或 5 (8-4) 取决于优先级位数这是FreeRTOS与Cortex-M内核中断控制器NVIC协同工作的核心。Cortex-M包括M4的中断优先级数值越小优先级越高。STM32F4使用了4位优先级所以优先级范围是0-150最高15最低。configKERNEL_INTERRUPT_PRIORITY设置SysTick和PendSV异常用于任务切换的优先级。必须设置为最低优先级在STM32F44位优先级中就是15。因为任务切换不应该打断高优先级的中断服务程序ISR。在代码中常写成15 4因为优先级寄存器是高4位有效或直接写255因为8位寄存器下154240但某些移植层定义不同需查看portmacro.h。configMAX_SYSCALL_INTERRUPT_PRIORITY这是一个阈值。所有优先级数值高于即逻辑优先级低于这个阈值的中断才可以安全地调用FreeRTOS的“FromISR”结尾的API如xQueueSendFromISR,xSemaphoreGiveFromISR。优先级高于这个阈值的中断是“不可屏蔽”或“高实时性”中断不允许调用任何可能导致任务切换的RTOS API以免破坏内核数据。通常这个值设置为5即优先级5-15的中断可以调用FromISR API0-4的不可以。在代码中可能体现为5 4。务必核对你需要打开你使用的移植层下的portmacro.h文件查看里面关于优先级位移的定义。一个常见的错误是portmacro.h中的configPRIO_BITS定义与你芯片实际的NVIC优先级位数不符或者configMAX_SYSCALL_INTERRUPT_PRIORITY的计算方式与portmacro.h的期望不匹配这会导致..\freertos\port\portmacro.h(73): error: #35: #error directive: configtick_t这类令人抓狂的编译错误。通常需要你根据portmacro.h的说明正确地在FreeRTOSConfig.h中定义这两个宏。4. 移植实操步骤与系统启动流程假设我们使用Keil MDK和标准库手动移植作为例子流程更具普适性。CubeMX用户大部分步骤已自动化但理解原理至关重要。4.1 步骤一工程文件与路径设置在工程目录下新建一个FreeRTOS文件夹将Source下的核心C文件tasks.c,queue.c等和portable/Keil/ARM_CM4F/port.c以及portable/MemMang/heap_4.c复制过来。在Keil工程中新建相应的分组Group例如FreeRTOS/Core和FreeRTOS/Port并将上述C文件添加到对应分组。在Keil的“Options for Target” - “C/C” - “Include Paths”中添加以下头文件路径../YourProject/FreeRTOS/Source/include../YourProject/FreeRTOS/Source/portable/Keil/ARM_CM4F将FreeRTOSConfig.h文件放在工程根目录或某个include路径下。4.2 步骤二修改系统滴答定时器SysTick中断服务程序在裸机工程中stm32f4xx_it.c文件里有一个SysTick_Handler()函数。FreeRTOS需要接管这个中断。你需要注释掉或删除原有的SysTick_Handler()函数。FreeRTOS的SysTick处理在port.c中已经定义好了函数名可能是xPortSysTickHandler()具体名称查port.c。你需要确保在启动文件中SysTick中断向量指向了正确的处理函数。通常在startup_stm32f4xx.s汇编启动文件里SysTick的中断服务程序标签是SysTick_Handler。你不需要修改启动文件因为port.c里会用#define SysTick_Handler xPortSysTickHandler这样的方式重定向。你只需要确保编译时能找到xPortSysTickHandler的定义。更常见的做法也是CubeMX的做法是在FreeRTOSConfig.h中通过宏定义重命名。// FreeRTOSConfig.h 中 #define xPortSysTickHandler SysTick_Handler这样port.c里实现的xPortSysTickHandler在编译时就会被替换成SysTick_Handler与启动文件中的向量表完美匹配。4.3 步骤三实现必要的底层函数FreeRTOS需要几个依赖于编译器和硬件的函数它们通常已经在port.c中实现了但你需要确保它们被正确链接。最重要的是vPortSetupTimerInterrupt()初始化SysTick和用于启动第一个任务的prvStartFirstTask()通常由port.c中的汇编代码实现。这些在标准的Cortex-M4F移植层中都已提供一般无需修改。你需要提供一个函数vApplicationIdleHook如果configUSE_IDLE_HOOK设为1和vApplicationTickHook如果configUSE_TICK_HOOK设为1这两个是弱定义weak的函数你可以在自己的main.c里实现它们用于空闲任务处理和每tick的定时处理。4.4 步骤四启动FreeRTOS调度器在main()函数中完成必要的硬件初始化时钟、GPIO等后创建你的第一个任务例如一个闪烁LED的任务然后调用vTaskStartScheduler()。这个函数永远不会返回因为一旦调用FreeRTOS就接管了CPU的控制权。int main(void) { // 1. 硬件初始化系统时钟、外设等 SystemInit(); LED_GPIO_Config(); // 2. 创建初始任务 xTaskCreate(LED_Task, LED Blink, 128, NULL, 2, NULL); // 栈128字优先级2 // 3. 启动调度器永不返回 vTaskStartScheduler(); // 4. 如果调度器启动失败才会执行到这里通常是堆空间不足 while(1); }4.5 系统启动的底层视角当vTaskStartScheduler()被调用时它内部会创建空闲任务Idle Task和可选的定时器服务任务如果使能了软件定时器。初始化SysTick定时器根据configTICK_RATE_HZ设置中断周期。调用port.c中的xPortStartScheduler()。xPortStartScheduler()会设置PendSV和SysTick的中断优先级为最低然后触发一个SVC系统调用中断或直接调用prvStartFirstTask()。在prvStartFirstTask()中是一段汇编代码它手动加载第一个任务的上下文栈指针、程序计数器等然后执行一个异常返回指令CPU就跳转到了第一个任务的函数入口开始执行。至此多任务环境正式运行。5. 避坑指南那些让我debug到深夜的典型问题移植过程很少一帆风顺以下是几个高频问题及排查思路。5.1 编译错误portmacro.h中的#error directive错误信息类似..\freertos\port\portmacro.h(73): error: #35: #error directive: configtick_t。 这几乎100%是由于中断优先级配置错误引起的。打开portmacro.h找到出错的那一行它通常是在检查configMAX_SYSCALL_INTERRUPT_PRIORITY和configKERNEL_INTERRUPT_PRIORITY的定义是否合理。解决方案确认你的STM32F4芯片的NVIC优先级位数通常是4。在FreeRTOSConfig.h中按照portmacro.h文件开头注释的示例来定义这两个宏。不同版本的移植层写法可能略有差异。常见写法#define configPRIO_BITS 4 // 4位优先级 #define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 // 最低优先级数值 #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 // 可调用FromISR API的最高优先级数值 #define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY (8 - configPRIO_BITS) ) #define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY (8 - configPRIO_BITS) )确保FreeRTOSConfig.h中包含了#include “stm32f4xx.h”或其他能定义__NVIC_PRIO_BITS的头文件因为portmacro.h可能会依赖这个宏。5.2 系统运行不稳定随机硬错误HardFault这是最令人头疼的问题原因多样。栈溢出这是首要怀疑对象。每个任务都有自己的栈如果任务函数局部变量太大、递归调用太深或者中断嵌套太复杂都会导致栈溢出破坏相邻内存区域可能是另一个任务的栈或堆数据引发HardFault。排查FreeRTOS提供了栈溢出检测钩子函数。在FreeRTOSConfig.h中将configCHECK_FOR_STACK_OVERFLOW设为1或2。然后实现vApplicationStackOverflowHook函数一旦检测到溢出就会进入这个函数你可以在这里打印出错的任务句柄名pxCurrentTCB-pcTaskName快速定位元凶。预防创建任务时给栈空间留足余量。对于简单的任务128字512字节是起步价复杂的任务可能需要512字甚至更多。使用uxTaskGetStackHighWaterMark()函数可以查询任务运行后剩余栈空间的最小值据此优化栈大小。中断优先级冲突如果某个高优先级中断优先级数值小于configMAX_SYSCALL_INTERRUPT_PRIORITY对应的数值中错误地调用了FreeRTOS的API即使是FromISR版本的或者进行了非原子的操作可能破坏内核数据结构。内存堆溢出configTOTAL_HEAP_SIZE设置太小导致创建任务或内核对象时分配内存失败但错误未被正确处理后续访问非法内存。浮点数使用在任务中使用了浮点运算但任务上下文切换时没有正确保存/恢复FPU寄存器。确保你使用的是ARM_CM4F的移植层它已经处理了FPU上下文保存。5.3 任务调度看似正常但系统“卡死”某个任务陷入死循环且未主动让出CPU在协作式调度configUSE_PREEMPTION0下会发生。在抢占式调度下如果该任务优先级最高且不调用任何能引起阻塞的API如vTaskDelay,xQueueReceive也会一直霸占CPU。确保高优先级任务中有阻塞延时或等待事件的操作。中断服务程序ISR处理时间过长中断会打断任务如果某个中断频繁发生且处理代码很长会导致低优先级任务长期得不到执行。优化ISR只做最紧急的处理如清除标志、读取数据将非紧急处理放到一个任务中通过信号量或队列来触发。优先级反转虽然FreeRTOS的互斥量Mutex有优先级继承机制但如果使用不当比如用二进制信号量做互斥仍可能发生。理解并正确使用互斥锁。5.4 与HAL库延时函数HAL_Delay()的冲突在CubeMX生成的工程中这个问题已自动解决HAL时基源被切换到其他定时器。但在手动移植的标准库工程中你需要处理。现象调用HAL_Delay()会导致系统卡住因为HAL_Delay()依赖的HAL_GetTick()可能不再更新如果SysTick被FreeRTOS完全接管。解决方案推荐将HAL库的时基源切换到其他硬件定时器如TIM1。在CubeMX的“Pinout Configuration” - “System Core” - “SYS”中将“Timebase Source”改为除SysTick外的其他定时器。在代码中需要你自己初始化该定时器并产生1ms中断在中断里调用HAL_IncTick()。替代方案如果不想动硬件定时器可以在FreeRTOS的vApplicationTickHook函数每系统tick调用一次里调用HAL_IncTick()。但这会引入额外开销且要求configTICK_RATE_HZ必须为10001ms。6. 进阶实战创建多任务与通信示例移植成功只是第一步让多个任务协同工作才是目的。这里提供一个经典的生产者-消费者模型示例。假设我们有两个任务一个Sensor_Task生产者模拟读取传感器数据一个Display_Task消费者负责处理并显示数据。它们通过一个队列Queue进行通信。// 定义数据单元 typedef struct { uint32_t timestamp; float temperature; float humidity; } SensorData_t; // 队列句柄 QueueHandle_t xSensorQueue; // 生产者任务 void Sensor_Task(void *pvParameters) { SensorData_t data; const TickType_t xDelay pdMS_TO_TICKS(1000); // 每1秒采集一次 while(1) { // 模拟采集数据 data.timestamp HAL_GetTick(); data.temperature read_temperature_sensor(); // 假设的函数 data.humidity read_humidity_sensor(); // 假设的函数 // 发送数据到队列等待最多10个tick10ms if (xQueueSend(xSensorQueue, data, pdMS_TO_TICKS(10)) ! pdPASS) { // 发送失败可能是队列满可以记录错误或重试 printf(Queue send failed!\r\n); } vTaskDelay(xDelay); // 阻塞延时让出CPU } } // 消费者任务 void Display_Task(void *pvParameters) { SensorData_t receivedData; while(1) { // 从队列接收数据无限期等待 if (xQueueReceive(xSensorQueue, receivedData, portMAX_DELAY) pdPASS) { // 成功接收到数据进行处理和显示 printf([%lu] Temp: %.2f, Humi: %.2f\r\n, receivedData.timestamp, receivedData.temperature, receivedData.humidity); // 这里可以调用显示驱动 } } } // 在main函数中创建队列和任务 int main(void) { // ... 硬件初始化 ... // 创建队列能存储5个SensorData_t元素 xSensorQueue xQueueCreate(5, sizeof(SensorData_t)); if (xSensorQueue NULL) { // 队列创建失败可能是堆内存不足 Error_Handler(); } // 创建任务 xTaskCreate(Sensor_Task, Sensor, 256, NULL, 3, NULL); // 优先级3 xTaskCreate(Display_Task, Display, 256, NULL, 2, NULL); // 优先级2 vTaskStartScheduler(); while(1); }关键点分析队列深度xQueueCreate(5, ...)中的5表示队列最多能缓存5条消息。如果生产者生产过快队列满了xQueueSend在指定阻塞时间内会等待。这里设置了一个10ms的超时防止任务因队列满而永久阻塞。任务优先级生产者优先级3比消费者优先级2高。这意味着一旦传感器数据就绪生产者能更快被调度执行将数据放入队列减少了数据丢失的风险。消费者任务在队列为空时会阻塞在xQueueReceive上不消耗CPU时间。阻塞延时vTaskDelay(pdMS_TO_TICKS(1000))是FreeRTOS中正确的延时方式它会让任务进入阻塞态调度器会去运行其他就绪的任务。绝对不要在RTOS任务中使用for循环空转的忙等待延时那会浪费CPU资源。portMAX_DELAY消费者在接收队列时使用了portMAX_DELAY这意味着如果队列为空它会无限期等待下去直到有数据到来。这比轮询队列效率高得多。7. 性能调优与资源监控系统跑起来后我们还需要关注其“健康度”。7.1 堆栈使用情况监控如前所述使用uxTaskGetStackHighWaterMark()来检查每个任务运行过程中栈空间的历史最低水位线即最大使用量。这个值越接近创建任务时分配的栈大小说明栈空间越紧张。通常建议保留10%-20%的余量。void Monitor_Task(void *pvParameters) { TaskHandle_t xTaskHandles[3]; // 存储其他任务的句柄 UBaseType_t uxHighWaterMark; while(1) { for(int i0; i3; i) { uxHighWaterMark uxTaskGetStackHighWaterMark(xTaskHandles[i]); printf(Task %d Stack HWM: %lu words\r\n, i, uxHighWaterMark); } vTaskDelay(pdMS_TO_TICKS(5000)); // 每5秒检查一次 } }创建任务时通过xTaskCreate的最后一个参数获取任务句柄保存起来供监控任务使用。7.2 CPU利用率统计FreeRTOS可以配置一个功能来粗略统计CPU利用率。在FreeRTOSConfig.h中设置configUSE_TRACE_FACILITY和configGENERATE_RUN_TIME_STATS为1。然后你需要实现两个宏// FreeRTOSConfig.h extern volatile unsigned long ulHighFrequencyTimerTicks; #define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() (ulHighFrequencyTimerTicks 0UL) #define portGET_RUN_TIME_COUNTER_VALUE() ulHighFrequencyTimerTicks你需要一个比系统tick频率高得多的定时器比如1MHz在其中断中递增ulHighFrequencyTimerTicks。然后你可以调用vTaskGetRunTimeStats()函数需要使能configUSE_STATS_FORMATTING_FUNCTIONS来获取一个字符串显示每个任务占用CPU时间的百分比。这对于发现哪个任务过于“繁忙”非常有帮助。7.3 选择合适的内存管理方案portable/MemMang/下有5个堆管理方案heap_1.c到heap_5.c。heap_1.c只分配不释放。适用于确定性强的、永不删除任务/队列的应用。heap_2.c可以释放但会产生碎片。已不推荐使用。heap_3.c简单包装了标准的malloc和free线程安全。heap_4.c最常用。可以释放并且会将相邻的空闲内存块合并成一个大的块有效减少碎片。适用于需要动态创建/删除任务、队列的应用。heap_5.c在heap_4的基础上允许堆内存分布在多个不连续的内存区域。适用于内存布局复杂的场景。对于大多数STM32F4应用heap_4.c是最佳选择。
返回列表