1. 环境选择与前提

1.1 开发环境

组件版本/型号说明
芯片STM32F407VET6Cortex-M4F,含 FPU
IDEKeil MDK 完整版无 32KB 代码限制,支持 RTOS Plugin
编译器ARM Compiler 5避免 __forceinlineARM_CM4F port 的兼容问题
调试器ST-Link / J-Link需确认接线 CLK→SWCLK、DIO→SWDIO、GND→GND
晶振外部 8MHz HSECubeMX 时钟树配置基准

1.2 源码获取

FreeRTOS 内核源码从官方仓库获取:FreeRTOS-Kernel(GitHub)。手动移植需复制以下文件到工程目录的 FreeRTOS/ 文件夹:

  • Source/tasks.cqueue.clist.ctimers.cevent_groups.c
  • Source/include/*.h
  • Source/portable/RVDS/ARM_CM4F/port.c(Keil 对应 RVDS 目录)
  • Source/portable/RVDS/ARM_CM4F/portmacro.h
  • Source/portable/MemMang/heap_4.c

注意:GCC 版本的 port.cportable/GCC/ARM_CM4F/)使用内联汇编语法与 ARM Compiler 5 不兼容,不可直接替换。


2. 裸机工程搭建

2.1 CubeMX 配置清单

生成 Keil 裸机工程时,不勾选 FreeRTOS,由后续手动添加源码。

配置项设置值作用
RCC → HSECrystal/Ceramic Resonator外部晶振输入
SYS → DebugSerial Wire保留 SWD 调试接口
SYS → Timebase SourceTIM6将 SysTick 留给 FreeRTOS 内核
Clock → HCLK168 MHz主频,后续 configCPU_CLOCK_HZ 的依据
USART1Asynchronous, 115200PA9/PA10,调试输出
GPIOPA6 = OutputLED 控制引脚(按实际板子调整)

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.cqueue.clist.ctimers.cevent_groups.c
  • port.cheap_4.c

Options for Target → C/C++ → Include Paths 中添加:

1
2
.\FreeRTOS
.\FreeRTOS\include

3.2 编译器设置

设置项推荐值原因
Target → Floating Point HardwareSingle PrecisionF407 含 FPU,需开启硬件浮点
Target → ARM CompilerUse default compiler version 5RVDS/ARM_CM4Fportmacro.h 使用 __forceinline,ARM Compiler 6 不认识该关键字
C/C++ → OptimizationLevel 0 (-O0)调试阶段保留符号信息

4. FreeRTOSConfig.h 配置

4.1 文件定位

放置于 FreeRTOS/FreeRTOSConfig.h,与 tasks.cport.c 同级。该文件基于 FreeRTOS 官方 CORTEX_M4F_STM32F407ZG-SK Demo 模板修改。

4.2 核心宏解析

4.2.1 时钟与节拍

1
2
#define configCPU_CLOCK_HZ      (168000000UL)
#define configTICK_RATE_HZ      (1000)

configCPU_CLOCK_HZ 必须与 CubeMX 时钟树 HCLK 一致。SysTick 重装载值计算:

1
2
Reload = configCPU_CLOCK_HZ / configTICK_RATE_HZ - 1
       = 168000000 / 1000 - 1 = 167999 (0x28F9F)

4.2.2 堆与栈

1
2
#define configTOTAL_HEAP_SIZE       (20 * 1024)   /* 20 KB */
#define configMINIMAL_STACK_SIZE    (128)         /* 128 words = 512 bytes */

heap_4.c 内部维护静态数组 ucHeap[configTOTAL_HEAP_SIZE]xTaskCreate() 的栈大小参数单位为 word(1 word = 4 bytes),512 表示 2048 字节。configMINIMAL_STACK_SIZE 仅用于空闲任务和定时器任务的最小栈分配。

4.2.3 中断优先级配置

1
2
3
4
5
#define configPRIO_BITS                                 4
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY         15
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY    5
#define configKERNEL_INTERRUPT_PRIORITY                 (15 << 4)
#define configMAX_SYSCALL_INTERRUPT_PRIORITY            (5 << 4)

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(如 xQueueSendFromISRxSemaphoreGiveFromISR),否则触发 configASSERT

4.2.4 钩子与调试宏

1
2
3
4
5
#define configUSE_IDLE_HOOK                     0
#define configUSE_TICK_HOOK                     0
#define configUSE_MALLOC_FAILED_HOOK            0   /* 学习阶段关闭 */
#define configCHECK_FOR_STACK_OVERFLOW          0   /* 学习阶段关闭 */
#define configUSE_TRACE_FACILITY                1

configUSE_MALLOC_FAILED_HOOKconfigCHECK_FOR_STACK_OVERFLOW 开启后,需在代码中实现 vApplicationMallocFailedHook()vApplicationStackOverflowHook(),否则链接报错 Undefined symbolconfigUSE_TRACE_FACILITY 开启后支持 uxTaskGetStackHighWaterMark() 等调试 API。

4.2.5 中断向量映射

1
2
3
#define vPortSVCHandler         SVC_Handler
#define xPortPendSVHandler      PendSV_Handler
#define xPortSysTickHandler     SysTick_Handler

宏将 FreeRTOS port.c 中的内部函数名映射为 CMSIS 标准中断处理函数名。链接后,port.c 中的 xPortSysTickHandler 实际符号即为 SysTick_Handler,直接填入中断向量表。


5. 中断处理文件修改

5.1 删除重复定义

stm32f4xx_it.c 中由 CubeMX 生成的以下空函数必须注释或删除,否则与 port.c 中的定义冲突:

  • SVC_Handler
  • PendSV_Handler
  • SysTick_Handler

5.2 SysTick 处理方案

方案 A:自定义转发(推荐用于理解阶段)

stm32f4xx_it.c 中保留 SysTick_Handler,手动调用 FreeRTOS 与 HAL 的 Tick 处理:

1
2
3
4
5
6
7
8
9
extern void xPortSysTickHandler(void);

void SysTick_Handler(void)
{
    if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) {
        xPortSysTickHandler();
    }
    HAL_IncTick();
}

此时 FreeRTOSConfig.h不定义 #define xPortSysTickHandler SysTick_Handler

方案 B:完全由 port.c 接管

恢复宏定义:

1
#define xPortSysTickHandler     SysTick_Handler

彻底删除 stm32f4xx_it.c 中所有 SysTick_Handler 相关代码(包括 extern 声明与函数体)。port.c 直接提供 SysTick_Handler 符号,HAL 时基由 TIM6 中断单独维护,避免 HAL_IncTick() 重复调用。


6. 任务创建与启动

6.1 最小可运行示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "FreeRTOS.h"
#include "task.h"

static void _task_led(void *pvParameters)
{
    (void)pvParameters;
    for (;;) {
        HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();

    xTaskCreate(_task_led, "LED", 512, NULL, 1, NULL);
    vTaskStartScheduler();

    while (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 undefinedFreeRTOSConfig.h 仅识别 __ICCARM__,未覆盖 Keil 宏扩展条件编译:defined(__CC_ARM) || defined(__ARMCC_VERSION)
Symbol PendSV_Handler multiply definedstm32f4xx_it.cport.c 均定义了 PendSV_Handler删除 stm32f4xx_it.c 中的空函数定义
unknown type name '__forceinline'使用了 ARM Compiler 6,不认识 RVDS 关键字切换为 ARM Compiler 5,或改用 GCC 版 port(需额外适配)
Undefined symbol vApplicationMallocFailedHookconfigUSE_MALLOC_FAILED_HOOK 设为 1 但未实现函数改为 0 关闭,或在代码中实现该钩子函数
Undefined symbol vApplicationStackOverflowHookconfigCHECK_FOR_STACK_OVERFLOW 非 0 但未实现函数改为 0 关闭,或实现钩子函数

8. 关键概念辨析

8.1 HAL 时基与 FreeRTOS SysTick

维度HAL 时基FreeRTOS SysTick
物理来源任意定时器(TIM6/TIM7 等)Cortex-M 内核 SysTick(地址固定 0xE000_E010
维护变量uwTickxTickCount(内核私有)
用途HAL_Delay()HAL_GetTick()任务调度、延时到期检查、时间片轮转
中断优先级一般较低通常设为最低(或按 configKERNEL_INTERRUPT_PRIORITY
可否替换可替换为其他定时器不可替换,FreeRTOS 内核写死使用 SysTick

8.2 优先级数值规则

FreeRTOS 中优先级数值越大,逻辑优先级越高。空闲任务固定为 0,用户任务一般从 1 开始分配。configMAX_PRIORITIES 定义系统支持的优先级总数,每个优先级对应一个就绪链表,数值越大 RAM 开销越高。


附录:移植检查清单

  • CubeMX 生成裸机工程,验证 LED 闪烁与串口输出正常
  • 复制 FreeRTOS 源码到 FreeRTOS/ 目录,选择 RVDS/ARM_CM4F port
  • Keil 工程添加 FreeRTOS Group 与 Include 路径
  • 创建 FreeRTOSConfig.h,确认 configCPU_CLOCK_HZ 与 HCLK 一致
  • 删除 stm32f4xx_it.c 中的 SVC_HandlerPendSV_HandlerSysTick_Handler 空函数
  • 确认 SysTick_Handlerport.c 或自定义转发逻辑正确提供
  • 确认 TIM6 中断回调中调用 HAL_IncTick()
  • 编译器选择 ARM Compiler 5,开启 Single Precision FPU
  • main() 中调用 xTaskCreate()vTaskStartScheduler()
  • 烧录验证 LED 按预期周期闪烁