1. 环境选择与前提
1.1 开发环境
| 组件 | 版本/型号 | 说明 |
|---|---|---|
| 芯片 | STM32F407VET6 | Cortex-M4F,含 FPU |
| IDE | Keil MDK 完整版 | 无 32KB 代码限制,支持 RTOS Plugin |
| 编译器 | ARM Compiler 5 | 避免 __forceinline 与 ARM_CM4F port 的兼容问题 |
| 调试器 | ST-Link / J-Link | 需确认接线 CLK→SWCLK、DIO→SWDIO、GND→GND |
| 晶振 | 外部 8MHz HSE | CubeMX 时钟树配置基准 |
1.2 源码获取
FreeRTOS 内核源码从官方仓库获取:FreeRTOS-Kernel(GitHub)。手动移植需复制以下文件到工程目录的 FreeRTOS/ 文件夹:
Source/tasks.c、queue.c、list.c、timers.c、event_groups.cSource/include/*.hSource/portable/RVDS/ARM_CM4F/port.c(Keil 对应 RVDS 目录)Source/portable/RVDS/ARM_CM4F/portmacro.hSource/portable/MemMang/heap_4.c
注意:GCC 版本的
port.c(portable/GCC/ARM_CM4F/)使用内联汇编语法与 ARM Compiler 5 不兼容,不可直接替换。
2. 裸机工程搭建
2.1 CubeMX 配置清单
生成 Keil 裸机工程时,不勾选 FreeRTOS,由后续手动添加源码。
| 配置项 | 设置值 | 作用 |
|---|---|---|
| RCC → HSE | Crystal/Ceramic Resonator | 外部晶振输入 |
| SYS → Debug | Serial Wire | 保留 SWD 调试接口 |
| SYS → Timebase Source | TIM6 | 将 SysTick 留给 FreeRTOS 内核 |
| Clock → HCLK | 168 MHz | 主频,后续 configCPU_CLOCK_HZ 的依据 |
| USART1 | Asynchronous, 115200 | PA9/PA10,调试输出 |
| GPIO | PA6 = Output | LED 控制引脚(按实际板子调整) |
2.2 时基源分离原理
HAL 库依赖全局变量 uwTick 提供延时基准(HAL_Delay())。默认由 SysTick 中断驱动 HAL_IncTick()。FreeRTOS 内核同样需要周期性 SysTick 中断进行任务调度(xTaskIncrementTick())。
两者均试图占用 SysTick_Handler,因此必须分离:
- SysTick → 由 FreeRTOS
port.c完全接管,用于任务调度 - TIM6 → 作为 HAL 时基源,每 1ms 中断调用
HAL_IncTick()
TIM6 属于基本定时器,无输入捕获、PWM 等附加功能,作为时基源占用资源最少。TIM14 等通用定时器技术上可行,但存在引脚复用冲突风险,且不同型号间兼容性较差。
2.3 Project Manager 设置
- Toolchain / IDE:MDK-ARM V5
- 勾选
Generate peripheral initialization as a pair of .c/.h files - 勾选
Keep User Code when re-generating
若 Toolchain / IDE 下拉框为灰色不可选,说明工程已生成过代码。需关闭 CubeMX,删除已生成的 MDK-ARM/、Core/、Drivers/ 等目录,或直接修改 .ioc 文件中的 ProjectManager.ToolChain 字段后重新打开。
3. Keil 工程集成
3.1 文件添加与路径
在 Keil 的 Manage Project Items 中新建 Group FreeRTOS,添加以下文件:
tasks.c、queue.c、list.c、timers.c、event_groups.cport.c、heap_4.c
在 Options for Target → C/C++ → Include Paths 中添加:
| |
3.2 编译器设置
| 设置项 | 推荐值 | 原因 |
|---|---|---|
| Target → Floating Point Hardware | Single Precision | F407 含 FPU,需开启硬件浮点 |
| Target → ARM Compiler | Use default compiler version 5 | RVDS/ARM_CM4F 的 portmacro.h 使用 __forceinline,ARM Compiler 6 不认识该关键字 |
| C/C++ → Optimization | Level 0 (-O0) | 调试阶段保留符号信息 |
4. FreeRTOSConfig.h 配置
4.1 文件定位
放置于 FreeRTOS/FreeRTOSConfig.h,与 tasks.c、port.c 同级。该文件基于 FreeRTOS 官方 CORTEX_M4F_STM32F407ZG-SK Demo 模板修改。
4.2 核心宏解析
4.2.1 时钟与节拍
| |
configCPU_CLOCK_HZ 必须与 CubeMX 时钟树 HCLK 一致。SysTick 重装载值计算:
| |
4.2.2 堆与栈
| |
heap_4.c 内部维护静态数组 ucHeap[configTOTAL_HEAP_SIZE]。xTaskCreate() 的栈大小参数单位为 word(1 word = 4 bytes),512 表示 2048 字节。configMINIMAL_STACK_SIZE 仅用于空闲任务和定时器任务的最小栈分配。
4.2.3 中断优先级配置
| |
STM32F4 的 NVIC 寄存器为 8 位宽,但仅实现高 4 位 [7:4],故实际优先级为 0 ~ 15。写入寄存器时需左移 (8 - 4) = 4 位,优先级 5 对应寄存器值 0x50。
机制:FreeRTOS 通过 BASEPRI 寄存器屏蔽中断。BASEPRI = 0x50 时,优先级数值 >= 5 的中断被屏蔽(5, 6, … 15)。优先级 0 ~ 4 的中断仍可响应,但其 ISR 中禁止调用任何 FreeRTOS API(如 xQueueSendFromISR、xSemaphoreGiveFromISR),否则触发 configASSERT。
4.2.4 钩子与调试宏
| |
configUSE_MALLOC_FAILED_HOOK 和 configCHECK_FOR_STACK_OVERFLOW 开启后,需在代码中实现 vApplicationMallocFailedHook() 和 vApplicationStackOverflowHook(),否则链接报错 Undefined symbol。configUSE_TRACE_FACILITY 开启后支持 uxTaskGetStackHighWaterMark() 等调试 API。
4.2.5 中断向量映射
| |
宏将 FreeRTOS port.c 中的内部函数名映射为 CMSIS 标准中断处理函数名。链接后,port.c 中的 xPortSysTickHandler 实际符号即为 SysTick_Handler,直接填入中断向量表。
5. 中断处理文件修改
5.1 删除重复定义
stm32f4xx_it.c 中由 CubeMX 生成的以下空函数必须注释或删除,否则与 port.c 中的定义冲突:
SVC_HandlerPendSV_HandlerSysTick_Handler
5.2 SysTick 处理方案
方案 A:自定义转发(推荐用于理解阶段)
在 stm32f4xx_it.c 中保留 SysTick_Handler,手动调用 FreeRTOS 与 HAL 的 Tick 处理:
| |
此时 FreeRTOSConfig.h 中不定义 #define xPortSysTickHandler SysTick_Handler。
方案 B:完全由 port.c 接管
恢复宏定义:
| |
并彻底删除 stm32f4xx_it.c 中所有 SysTick_Handler 相关代码(包括 extern 声明与函数体)。port.c 直接提供 SysTick_Handler 符号,HAL 时基由 TIM6 中断单独维护,避免 HAL_IncTick() 重复调用。
6. 任务创建与启动
6.1 最小可运行示例
| |
6.2 常见编码错误
| 错误现象 | 原因 | 修正 |
|---|---|---|
| 任务未运行,系统无反应 | xTaskCreate 第一个参数传入 NULL 变量而非函数名 | 直接传入函数名,如 _task_led |
| 编译警告或 HardFault | 任务函数签名不匹配,声明为 void func(void) | 必须改为 void func(void *pvParameters),并用 (void)pvParameters; 消除未使用参数警告 |
| 栈溢出或任务创建失败 | 栈大小参数误用 ARM_MPU_REGION_SIZE_512B(数值为 0x08) | 应传入 512(表示 512 words = 2048 bytes) |
HAL_Delay() 时间减半 | HAL_IncTick() 被重复调用(SysTick + TIM6 同时执行) | 采用方案 B,删除 stm32f4xx_it.c 中的 SysTick_Handler,仅由 TIM6 维护 HAL 时基 |
7. 编译错误速查
| 错误信息 | 根因 | 解决方案 |
|---|---|---|
identifier "SystemCoreClock" is undefined | FreeRTOSConfig.h 仅识别 __ICCARM__,未覆盖 Keil 宏 | 扩展条件编译:defined(__CC_ARM) || defined(__ARMCC_VERSION) |
Symbol PendSV_Handler multiply defined | stm32f4xx_it.c 与 port.c 均定义了 PendSV_Handler | 删除 stm32f4xx_it.c 中的空函数定义 |
unknown type name '__forceinline' | 使用了 ARM Compiler 6,不认识 RVDS 关键字 | 切换为 ARM Compiler 5,或改用 GCC 版 port(需额外适配) |
Undefined symbol vApplicationMallocFailedHook | configUSE_MALLOC_FAILED_HOOK 设为 1 但未实现函数 | 改为 0 关闭,或在代码中实现该钩子函数 |
Undefined symbol vApplicationStackOverflowHook | configCHECK_FOR_STACK_OVERFLOW 非 0 但未实现函数 | 改为 0 关闭,或实现钩子函数 |
8. 关键概念辨析
8.1 HAL 时基与 FreeRTOS SysTick
| 维度 | HAL 时基 | FreeRTOS SysTick |
|---|---|---|
| 物理来源 | 任意定时器(TIM6/TIM7 等) | Cortex-M 内核 SysTick(地址固定 0xE000_E010) |
| 维护变量 | uwTick | xTickCount(内核私有) |
| 用途 | HAL_Delay()、HAL_GetTick() | 任务调度、延时到期检查、时间片轮转 |
| 中断优先级 | 一般较低 | 通常设为最低(或按 configKERNEL_INTERRUPT_PRIORITY) |
| 可否替换 | 可替换为其他定时器 | 不可替换,FreeRTOS 内核写死使用 SysTick |
8.2 优先级数值规则
FreeRTOS 中优先级数值越大,逻辑优先级越高。空闲任务固定为 0,用户任务一般从 1 开始分配。configMAX_PRIORITIES 定义系统支持的优先级总数,每个优先级对应一个就绪链表,数值越大 RAM 开销越高。
附录:移植检查清单
- CubeMX 生成裸机工程,验证 LED 闪烁与串口输出正常
- 复制 FreeRTOS 源码到
FreeRTOS/目录,选择RVDS/ARM_CM4Fport - Keil 工程添加
FreeRTOSGroup 与 Include 路径 - 创建
FreeRTOSConfig.h,确认configCPU_CLOCK_HZ与 HCLK 一致 - 删除
stm32f4xx_it.c中的SVC_Handler、PendSV_Handler、SysTick_Handler空函数 - 确认
SysTick_Handler由port.c或自定义转发逻辑正确提供 - 确认 TIM6 中断回调中调用
HAL_IncTick() - 编译器选择 ARM Compiler 5,开启 Single Precision FPU
-
main()中调用xTaskCreate()与vTaskStartScheduler() - 烧录验证 LED 按预期周期闪烁