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_MUTEXES,configUSE_RECURSIVE_MUTEXES,configUSE_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-15(0最高,15最低)。
configKERNEL_INTERRUPT_PRIORITY:设置SysTick和PendSV异常(用于任务切换)的优先级。必须设置为最低优先级,在STM32F4(4位优先级)中,就是15。因为任务切换不应该打断高优先级的中断服务程序(ISR)。在代码中常写成15 << 4(因为优先级寄存器是高4位有效)或直接写255(因为8位寄存器下,15<<4=240,但某些移植层定义不同,需查看portmacro.h)。configMAX_SYSCALL_INTERRUPT_PRIORITY:这是一个阈值。所有优先级数值高于(即逻辑优先级低于)这个阈值的中断,才可以安全地调用FreeRTOS的“FromISR”结尾的API(如xQueueSendFromISR,xSemaphoreGiveFromISR)。优先级高于这个阈值的中断,是“不可屏蔽”或“高实时性”中断,不允许调用任何可能导致任务切换的RTOS API,以免破坏内核数据。通常这个值设置为5(即优先级5-15的中断可以调用FromISR API,0-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()函数可以查询任务运行后剩余栈空间的最小值,据此优化栈大小。
- 排查:FreeRTOS提供了栈溢出检测钩子函数。在
- 中断优先级冲突:如果某个高优先级中断(优先级数值小于
configMAX_SYSCALL_INTERRUPT_PRIORITY对应的数值)中,错误地调用了FreeRTOS的API(即使是FromISR版本的),或者进行了非原子的操作,可能破坏内核数据结构。 - 内存堆溢出:
configTOTAL_HEAP_SIZE设置太小,导致创建任务或内核对象时分配内存失败,但错误未被正确处理,后续访问非法内存。 - 浮点数使用:在任务中使用了浮点运算,但任务上下文切换时没有正确保存/恢复FPU寄存器。确保你使用的是
ARM_CM4F的移植层,它已经处理了FPU上下文保存。
5.3 任务调度看似正常,但系统“卡死”
- 某个任务陷入死循环且未主动让出CPU:在协作式调度(
configUSE_PREEMPTION=0)下会发生。在抢占式调度下,如果该任务优先级最高且不调用任何能引起阻塞的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必须为1000(1ms)。
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个tick(10ms) 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 i=0; i<3; 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是最佳选择。