下面将详细介绍如何在 Keil MDK 环境下将 FreeRTOS 手动移植到 STM32G473VET6 微控制器上。内容涵盖工程创建、获取源码、文件组织、移植层适配、测试任务编写以及编译调试等步骤。
1. 工程搭建(Keil 项目创建)
- 创建基础工程:首先准备一个基础的 STM32G473VET6 工程(例如一个点灯工程)。可以使用 STM32CubeMX 生成 Keil uVision 工程,勾选需要的外设驱动和初始化代码(如 HAL 库)以简化后续开发。确保工程包含 STM32G4的启动文件(如
startup_stm32g473xx.s
)和系统时钟配置代码(SystemInit()
或 HAL 的SystemClock_Config()
)。 - 配置 Keil 工程选项:在 Keil 中选择正确的器件和编译器。对于 Cortex-M4F 内核,启用硬件 FPU 支持。使用 ARM Compiler 5 可以直接使用 FreeRTOS 提供的 RVDS 移植代码;如果使用 ARM Compiler 6,需要确保使用对应的移植代码或兼容设置。
- 包含 HAL 库(可选):如果使用 STM32CubeMX 生成的工程并选择了 HAL 驱动,则工程已包含 HAL 库和CMSIS设备支持。HAL 库的使用是可选的,但如果使用HAL,需要注意其与 FreeRTOS 的时间基准(SysTick)的配合。
完成上述基础工程搭建后,确保一个简单的裸机程序(如LED闪烁)可以编译运行,以此作为移植 FreeRTOS 的起点。
2. 获取 FreeRTOS 源码
- 下载最新 FreeRTOS 内核:从 FreeRTOS 官方网站获取最新版本的内核源码压缩包。解压后,会看到包含
FreeRTOS/Source
源码目录以及各类 Demo 示例等。我们主要关注FreeRTOS/Source
下的文件。 - 整理所需源码文件:FreeRTOS 提供了很多可选组件,我们在移植时只保留必要的内核源文件。将以下文件从
FreeRTOS/Source
复制到工程目录(建议放入Middlewares/FreeRTOS/Source
):- 内核核心:
tasks.c
(任务调度与管理)、queue.c
(队列和信号量实现)、list.c
(内核链表)、timers.c
(软件定时器,可选)、event_groups.c
(事件标志组,可选)、croutine.c
(协程,可选,根据需要)等。这些文件实现了任务管理、调度器、队列、软件定时器、事件组等核心功能。 - 头文件:复制
FreeRTOS/Source/include
目录下的所有头文件到工程的FreeRTOS/Source/include
(或指定一个包含路径)。这些头文件定义了FreeRTOS API和配置项。
- 内核核心:
- 移植层与内存管理:在
FreeRTOS/Source/portable
目录下,根据使用的内核架构和编译器选择正确的移植层代码:- 针对 STM32G473 (Cortex-M4F 内核) 和 Keil MDK 编译器,使用路径
portable/RVDS/ARM_CM4F
下的文件。这通常包含port.c
和portmacro.h
,实现与 Cortex-M4F 内核相关的上下文切换和中断处理代码。将ARM_CM4F
文件夹复制到工程的FreeRTOS/Source/portable
下。 - 内存管理方面,FreeRTOS 提供多种堆管理实现。在
portable/MemMang
目录下选择一款堆实现源码文件,例如常用的 heap_4.c(最佳适应算法,支持分配和释放)或 heap_5.c(支持多内存区)。复制所选的heap_x.c
文件到FreeRTOS/Source/portable/MemMang
并加入工程。其余未用的portable
子目录和堆实现文件可不添加,以减少干扰。
- 针对 STM32G473 (Cortex-M4F 内核) 和 Keil MDK 编译器,使用路径
- 添加到 Keil 工程:在 Keil 中,为 FreeRTOS 创建分组并添加上述源文件。例如,新建 “FreeRTOS_CORE” 分组添加内核
.c
文件(tasks.c、queue.c 等),新建 “FreeRTOS_PORT” 分组添加移植层和堆文件(如port.c
、heap_4.c
等)。同时,在工程的 C/C++ Include 路径中增加 FreeRTOS 的头文件目录(例如Middlewares\FreeRTOS\Source\include
以及Middlewares\FreeRTOS\Source\portable\RVDS\ARM_CM4F
),以便编译器能够找到FreeRTOS.h
、portmacro.h
等头文件。 - 获取或创建 FreeRTOSConfig.h:
FreeRTOSConfig.h
是用于配置 FreeRTOS 内核的头文件,不包含在上述源码中。我们需要为 STM32G4 创建该文件并根据需求进行配置。可以参考 FreeRTOS 提供的示例配置(例如在 FreeRTOS/Demo 或 STM32 的示例中查找类似 STM32 的配置),复制并修改后加入工程的 Inc 或 FreeRTOS/include 目录。确保在编译选项的包含路径中能找到该文件。下面将在下一节详细说明关键配置项。
3. 文件结构整理与 FreeRTOSConfig 配置
- 工程目录组织:按照惯例,可将 FreeRTOS 源码放置在工程目录下的
Middlewares/FreeRTOS
文件夹中,并划分子目录:Middlewares/FreeRTOS/Source
:放置 FreeRTOS内核.c
源文件和include
头文件。Middlewares/FreeRTOS/Source/portable
:放置移植层相关文件。其中portable/RVDS/ARM_CM4F
存放 Cortex-M4F + Keil 移植代码,portable/MemMang
存放所选择的堆管理实现。- (如果使用 CubeMX 自动生成中间件结构,可直接将文件对号入座到对应文件夹。)
- FreeRTOSConfig.h 关键配置:打开新建的
FreeRTOSConfig.h
,根据 STM32G473VET6 的硬件参数和应用需求设置各项宏定义。以下是常用配置项:- 系统频率:
configCPU_CLOCK_HZ
定义CPU时钟频率(Hz)。可设置为系统时钟频率数值或使用SystemCoreClock
变量。例如:#define configCPU_CLOCK_HZ (SystemCoreClock)
- 系统频率:
确保这个值与系统实际运行频率匹配。
- Tick 定时频率:
configTICK_RATE_HZ
定义RTOS滴答时钟频率,即系统节拍中断频率。常用设为1000Hz(1ms周期)。需与 SysTick 配置匹配。 - 最大优先级数:
configMAX_PRIORITIES
定义系统可用的任务优先级数量(优先级从0到configMAX_PRIORITIES-1
)。根据应用需要设置一个合适值,比如5或以上。注意至少要大于等于使用的优先级数,Idle任务优先级为0。 - 最小空闲任务栈:
configMINIMAL_STACK_SIZE
定义空闲任务的栈深度(以字为单位)。Cortex-M4上一般设置为128(即512字节)或根据需求调整。 - 总堆大小:
configTOTAL_HEAP_SIZE
定义FreeRTOS可用的堆内存总字节数(仅对heap_1.c, heap_2.c, heap_4.c, heap_5.c等有效)。根据创建的任务、队列等数量估算所需内存并设置。例如设为(10*1024)表示10KB,用于容纳所有动态分配对象。 - 内核特性开关:根据需要启用或禁用内核功能宏:
configUSE_PREEMPTION
设置为1启用抢占式调度(常用),为0则为协作式调度。configUSE_TIME_SLICING
为1则同优先级任务时间片轮转。configUSE_IDLE_HOOK
/configUSE_TICK_HOOK
设置是否使用空闲任务和Tick中断的钩子函数(如不需要可设0)。configUSE_MUTEXES
、configUSE_COUNTING_SEMAPHORES
等设为1启用互斥信号量和计数信号量。configUSE_TRACE_FACILITY
和configUSE_STATS_FORMATTING_FUNCTIONS
可用于启用运行时统计(如 uxTaskGetSystemState)。configGENERATE_RUN_TIME_STATS
如需启用运行时间统计(需要提供时钟源)。- 其他比如
configCHECK_FOR_STACK_OVERFLOW
(栈溢出检查),configUSE_MALLOC_FAILED_HOOK
(内存分配失败钩子)可按需设置。
- 软件定时器和事件组:如果使用软件定时器和事件标志组:
- 设置
configUSE_TIMERS
为1,并配置configTIMER_TASK_PRIORITY
(定时器服务任务优先级,一般高于普通任务),configTIMER_QUEUE_LENGTH
(定时器命令队列长度),configTIMER_TASK_STACK_DEPTH
(定时器任务栈深度)。 - 设置
configUSE_EVENT_GROUPS
为1 以启用事件组机制。
- 设置
- 中断优先级配置:针对 Cortex-M 内核,以下配置 极为重要,必须正确设置中断优先级相关宏,以确保 FreeRTOS 安全运行:
configPRIO_BITS
:NVIC 可用优先级位数。STM32G4 系列有 4 位优先级(0-15级),因此configPRIO_BITS
应定义为4(如果CMSIS的__NVIC_PRIO_BITS
已定义则可用它)。configLIBRARY_LOWEST_INTERRUPT_PRIORITY
:应用可设置的最低中断优先级数值。STM32优先级数值越大优先级越低,一般设为15(表示最低优先级)。configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
:使用FreeRTOS系统调用的最高中断优先级数值。建议选择一个较高的优先级等级,例如5(表示任何优先级数值<=5的中断服务例程 不应调用FreeRTOS API)。configKERNEL_INTERRUPT_PRIORITY
:内核所使用的中断优先级(用于PendSV和SysTick)。通常定义为configLIBRARY_LOWEST_INTERRUPT_PRIORITY
左移适当位数,使其成为最低优先级。例如:
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
若优先级位4位,上式计算结果为 15 << 4 = 240
(0xF0),对应 NVIC 优先级15。这确保RTOS内核的中断(PendSV/SysTick)设为最低优先级。
configMAX_SYSCALL_INTERRUPT_PRIORITY
:可调用系统API的最高中断优先级(临界值)。定义为configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
左移 (8 -configPRIO_BITS
) 位。例如优先级5则计算为5 << 4 = 80
(0x50),对应 NVIC 优先级5- 设置上述优先级配置后,务必在应用中确保所有使用 FreeRTOS API的中断的NVIC优先级数值大于等于
configMAX_SYSCALL_INTERRUPT_PRIORITY
(例如设为5或更大,即优先级不高于5级),否则会触发优先级嵌套错误。 - 。这意味着优先级数值小于5的中断(更高优先级的中断)不应调用FreeRTOS安全API。
配置完成后,保存 FreeRTOSConfig.h
。经过以上设置,FreeRTOS内核行为就根据目标硬件和应用需求进行了调整。
4. 移植层适配(中断向量与启动文件调整)
在将 FreeRTOS 集成到 STM32G4 时,需要确保 Cortex-M 内核的几个特殊中断(SVC、PendSV、SysTick)正确地连接到 FreeRTOS 的调度机制。这涉及移植层代码和启动文件的适配:
重定向内核中断处理函数:FreeRTOS 的 Cortex-M 移植在
port.c
中实现了 SVC、PendSV 和 SysTick 中断处理逻辑。但我们需要把这些处理例程挂接到实际的中断向量。常用方法有两种:宏定义映射法:在
FreeRTOSConfig.h
中添加宏,将 FreeRTOS 移植层的中断处理函数名映射为标准中断名。例如:#define vPortSVCHandler SVC_Handler #define xPortPendSVHandler PendSV_Handler #define xPortSysTickHandler SysTick_Handler
这样,编译器在编译移植层代码时会将实现函数名替换为对应的中断名,从而覆盖启动文件中的默认中断处理。STM32CubeMX 生成的FreeRTOS配置常用此方法,其中将 SysTick 的映射宏默认注释是因为 HAL 自带 SysTick handler(如果我们希望RTOS接管SysTick,需要启用该宏)。通过这种方式,FreeRTOS 的
vPortSVCHandler()
实际编译为SVC_Handler
,xPortPendSVHandler()
编译为PendSV_Handler
,xPortSysTickHandler()
编译为SysTick_Handler
,从而自动替换掉弱定义的默认 handlers。启动文件重定向法:直接修改启动文件
startup_stm32g473xx.s
,将向量表中相应中断的入口指向 FreeRTOS 提供的函数。具体做法是在启动文件中声明移植层函数为外部符号,并将中断入口替换为跳转。例如,在启动文件的中断向量表区域,找到PendSV_Handler
、SysTick_Handler
和SVC_Handler
的默认实现,替换为:EXTERN vPortSVCHandler EXTERN xPortPendSVHandler EXTERN xPortSysTickHandler SVC_Handler B vPortSVCHandler ; 跳转到FreeRTOS的SVC处理 PendSV_Handler B xPortPendSVHandler ; 跳转到FreeRTOS的PendSV处理 SysTick_Handler B xPortSysTickHandler ; 跳转到FreeRTOS的SysTick处理
如此修改后,这三个中断会触发 FreeRTOS 对应的服务例程,实现上下文切换和心跳节拍。
注意:采用上述两种方法之一即可,通常推荐使用第一种映射宏的方法,修改少且清晰。如果使用HAL库且其SysTick作为时基,默认HAL会定义自己的
SysTick_Handler
,这时更需要使用映射或修改启动文件,确保RTOS的SysTick处理生效而HAL的时基不中断系统滴答(可选择在FreeRTOS的滴答钩子中调用HAL_IncTick()
以保持HAL时基)。
SysTick 定时器设置:FreeRTOS利用 SysTick 产生固定频率的节拍中断用于任务调度。需要保证 SysTick 定时中断按照
configTICK_RATE_HZ
配置的频率触发:- 如果基础工程使用了 HAL,
HAL_Init()
默认将 SysTick 配置为1ms中断(1000Hz)。若configTICK_RATE_HZ
也是1000,则默认配置可用,但要确保 SysTick 中断由 FreeRTOS接管。如上所述,可以在 FreeRTOS的 SysTick_Handler 中调用xPortSysTickHandler()
实现RTOS心跳,同时也可调用HAL_IncTick()
保持HAL的时基计数。 - 如果未使用 HAL 或需手动配置:可以在系统初始化时配置 SysTick 寄存器。计算加载值:
reload = SystemCoreClock / configTICK_RATE_HZ
,然后设置 SysTick:SysTick->LOAD = reload - 1; // 装载值 SysTick->VAL = 0; // 清零计数器 SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk; // 使能SysTick中断 SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; // 启动SysTick
同时选择合适的时钟源(如使用外部HCLK)。这样SysTick每
1/configTICK_RATE_HZ
秒产生一次中断。在 SysTick 中断服务函数中,应调用 FreeRTOS 的心跳处理函数。例如:void SysTick_Handler(void) { if(xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) { xPortSysTickHandler(); } }
述实现确保只有在调度器启动后才调用 RTOS 的滴答处理(在调度器启动前 SysTick 中断也许已经开启,此时不应调用RTOS API)。
- 如果基础工程使用了 HAL,
- PendSV 和 SVC 中断:PendSV 用于触发上下文切换,应设置为最低优先级,以免打断高优先级中断。SVC(Supervisor Call)在 FreeRTOS 中用于启动第一个任务,之后不再使用。通常无需手动配置其优先级(默认即可),PendSV 和 SysTick 优先级会在 FreeRTOS启动时根据
configKERNEL_INTERRUPT_PRIORITY
设置。在移植过程中,只需确保上述映射正确,FreeRTOS 会将 PendSV/SysTick 设置为最低优先级。 - NVIC 中断优先级分组:STM32 默认优先级分组通常将全部4位用于抢占优先级(无子优先级)。确保保持这种配置(一般无需额外修改SCB->AIRCR)。这保证了
configMAX_SYSCALL_INTERRUPT_PRIORITY
规定的优先级阈值正确生效。
完成移植层适配后,FreeRTOS 核心应能正确接管 SVC、PendSV、SysTick 中断,实现其调度功能。可以编译工程,确保链接阶段没有未定义引用(尤其是 vPortSVCHandler
等应已映射或定义)。
5. 编写测试任务
移植完成后,在 main.c
中创建示例任务以验证 RTOS 调度运行是否正常:
初始化:在创建任务之前,执行必要的硬件初始化。例如调用
HAL_Init()
和SystemClock_Config()
(若使用HAL);初始化用于测试的外设,如配置GPIO用于控制LED,初始化UART用于串口打印等。确认此时不要调用会引起延时阻塞的函数(如HAL_Delay()
),以免在RTOS启动前产生不确定延时。创建任务:使用 FreeRTOS 提供的 API 创建至少两个任务进行演示:
- LED 闪烁任务:例如创建一个周期性闪烁板上 LED 的任务。任务函数中反复切换 LED 引脚状态并调用
vTaskDelay()
延时一定 Tick 数(如500ms),以测试定时调度功能。 - 串口打印任务:创建另一个任务,周期性地通过串口打印消息(比如每1秒打印一行文本或计数值)。这可以测试多个任务并行运行,以及任务间的独立性。如使用HAL UART发送,可在任务中调用
HAL_UART_Transmit()
发送字符串(注意需确保串口初始化在RTOS启动前完成,或使用互斥确保线程安全)。
使用
xTaskCreate()
创建任务时,需要提供任务入口函数、任务名、栈大小、任务参数、优先级和任务句柄等参数。例如:xTaskCreate(LED_Task, "LEDTask", 128, NULL, 2, NULL); xTaskCreate(UART_Task, "UARTTask", 256, NULL, 2, NULL);
上述示例创建了LED_Task和UART_Task,分别指定了栈深度(128和256字,具体值视功能需求而定)和优先级(这里都为2,同优先级将时间片轮转执行)。可以根据需要调整优先级以验证优先级调度效果(优先级数值越大,优先级越高)。
- LED 闪烁任务:例如创建一个周期性闪烁板上 LED 的任务。任务函数中反复切换 LED 引脚状态并调用
启动调度器:所有任务创建完成后,调用
vTaskStartScheduler()
启动FreeRTOS调度。该函数调用后,RTOS 接管CPU控制权,开始根据优先级和时间片调度任务。注意:vTaskStartScheduler()
若返回则意味着调度启动失败(通常是因为堆内存不足无法创建Idle任务),这种情况下可以在main函数中添加错误处理,例如进入死循环或触发断言,以便调试发现问题。正常情况下该调用不会返回。任务函数示例:下面给出简要的任务函数代码片段示例:
void LED_Task(void *argument) { for(;;) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // 切换LED引脚状态 vTaskDelay(pdMS_TO_TICKS(500)); // 延时500ms (pdMS_TO_TICKS宏将毫秒转换为节拍数) } } void UART_Task(void *argument) { const char *msg = "Hello from FreeRTOS!\r\n"; for(;;) { HAL_UART_Transmit(&huart2, (uint8_t*)msg, strlen(msg), 100); vTaskDelay(1000); // 延时1000ms } }
在上述代码中,LED任务每0.5秒翻转一次LED状态,UART任务每1秒通过串口发送一条消息。两个任务的死循环中都使用了
vTaskDelay
或等效宏进行阻塞延时,让出CPU给其他任务。注意 HAL 与 RTOS 的配合:如果使用了HAL库,在RTOS启动后不要使用
HAL_Delay()
进行延时,因为HAL_Delay依赖于SysTick全局变量的中断更新。在FreeRTOS接管SysTick后,默认HAL的滴答不再增长(除非在SysTick_Handler中继续调用HAL_IncTick)。应改用vTaskDelay
等RTOS延时机制来替代阻塞延时。此外,如果串口中断等外设中断需要调用FreeRTOS API(如xQueueSendFromISR),务必确保这些中断优先级符合前述configMAX_SYSCALL_INTERRUPT_PRIORITY
限制。
完成上述移植和任务创建后,进行编译、下载并调试,重点关注以下方面:
系统滴答验证:调试时,可以在 SysTick_Handler 中打断点,或读取
xTaskGetTickCount()
返回值,确认滴答计数在持续增长,频率符合预期的configTICK_RATE_HZ
。例如,可在UART打印任务中定期打印xTaskGetTickCount()
值来观察。任务调度检查:观察板上 LED 是否按设计频率闪烁,串口打印是否按周期输出。如果只有一个任务运行、另一个任务饿死,可能是优先级配置不当或某任务陷入死循环未调用阻塞API。确保每个任务在适当位置会阻塞或延时,以让出CPU。还可以使用Keil的调试器查看FreeRTOS线程列表(如果安装了FreeRTOS调试插件)以确认多个任务都处于就绪/阻塞状态并被调度。
中断优先级问题:如果程序跑一段时间后出现HardFault或异常,常见原因是中断优先级配置不正确导致违反了FreeRTOS的中断安全策略。检查 FreeRTOSConfig.h 中
configMAX_SYSCALL_INTERRUPT_PRIORITY
的设置以及有无中断使用了过高的优先级调用了RTOS API。如有需要,可在 FreeRTOSConfig.h 中定义configASSERT()
钩子,以捕获运行时的优先级违规等错误。堆内存使用:使用
xPortGetFreeHeapSize()
查询剩余堆内存。该函数返回当前未被分配的堆空间大小,有助于判断configTOTAL_HEAP_SIZE
设置是否合理。示例:在所有任务创建后调用xPortGetFreeHeapSize()
,如果返回值很小(接近0),说明堆几乎耗尽,需要增大 configTOTAL_HEAP_SIZE;如果返回值远大于实际需要,可以优化减小 configTOTAL_HEAP_SIZE 以节省RAM。FreeRTOS还提供xPortGetMinimumEverFreeHeapSize()
可查询历史最低剩余堆空间,用于评估最糟情况内存占用。其他调试技巧:可开启
configCHECK_FOR_STACK_OVERFLOW
(并实现vApplicationStackOverflowHook
)来捕获任务栈溢出;开启configUSE_MALLOC_FAILED_HOOK
(并实现vApplicationMallocFailedHook
)来捕获内存分配失败。这些钩子在调试阶段很有帮助。一旦系统运行正常,可以选择关闭或保留这些检查。
经过以上步骤,如果 LED 按预期闪烁且串口输出正常,就表明 FreeRTOS 在 STM32G473VET6 上已成功移植并运行。由此可以进一步开发应用,比如创建更多任务,利用队列、信号量等进行任务间通信。在实际项目中,按照上述指南配置 FreeRTOS,可确保系统稳定运行于 Keil MDK 环境下。