STM32CubeMX 可以生成时钟、GPIO、HAL 和 RTOS 包装层代码,也可以只生成裸机框架后由工程手动加入 FreeRTOS。两条路线都需要核对内核、移植层、编译器、包装层和异常向量,不能把不同教程中的文件直接拼接。
本文保留 STM32F103、PB5 每 500 ms 翻转一次,以及 CubeMX/CMake/GCC、MDK 两种工程背景。具体代码按 STM32F103xB、GCC ARM_CM3 和原生 FreeRTOS V11.1.0 说明;原 F103ZET6 属于 xE 高密度器件,具有不同的外设集合。两者不能共用未经核对的 TIM6 假设。
先确定集成路线
跳转到“先确定集成路线”| 路线 | 应用入口与启动方法 |
|---|---|
| 原生 FreeRTOS | 创建队列/任务等对象,检查结果,调用一次 vTaskStartScheduler() |
| CMSIS-RTOS v1 | 使用与固件包配套的 v1 包装层和生成入口,例如相应 osThreadCreate / osKernelStart 流程 |
| CMSIS-RTOS2 | osKernelInitialize(),创建对象或调用生成初始化,再 osKernelStart();线程常由 osThreadNew() 创建 |
CMSIS 是 API 包装层。不要在生成的 osKernelStart() 之外再启动一次原生调度器,也不要把 v1 的 osSystickHandler() 当成所有 v2/原生工程必有的函数。使用 CubeMX 时,先看生成的 main.c、freertos.c、中断文件及配置文件,再决定修改位置。使用用户代码区和工程自有模块,降低重新生成时覆盖改动的概率。
手动加入源码与头文件
跳转到“手动加入源码与头文件”
图中的 Source 是这份历史包的内核入口。单独的 FreeRTOS-Kernel 仓库则直接把内核放在根目录。先固定版本,再选取所需文件:
- 内核基础文件
tasks.c、queue.c、list.c;按功能加入timers.c、event_groups.c、stream_buffer.c等。 - 一个与处理器及编译器匹配的移植,如本文的
portable/GCC/ARM_CM3/port.c。 - 一种内存方案,例如
portable/MemMang/heap_4.c。 - 公共
include、选定移植的目录,以及应用自己的FreeRTOSConfig.h目录。 - 使用 CMSIS 时,额外加入该固件包所配套的包装源文件和头文件。

旧 MDK 教程选择 RVDS/ARM_CM3 与其当时工具链有关。MDK 是开发环境名称,使用 Arm Compiler 5、Arm Compiler 6 等组合时,要按内核移植说明选目录;不能把“使用 MDK”直接等同于固定一个移植目录。

图中包括历史协程文件 croutine.c。它展示曾使用的工程文件集合,不代表每个应用都必须启用所有功能。

CMake 工程做同样的事情:用源文件列表加入内核、端口和堆文件,用目标的头文件目录加入应用配置、内核公共接口和端口。已有 arm-none-eabi-gcc 工具链文件时,应沿用项目的 CPU、Thumb、ABI、链接脚本和启动文件设置。不要仅为了 FreeRTOS 再添加第二套启动文件或第二个向量表。
配置项的真实单位
跳转到“配置项的真实单位”原 CubeMX 参数可作为读配置的例子,不能不经内存预算照搬:
| 参数/选项 | 原例值或功能 | 正确解释 |
|---|---|---|
configTICK_RATE_HZ | 1000 | 名义 Tick 周期 1 ms;中断和调度开销仍需测量 |
configMAX_PRIORITIES | 56 | 允许原生优先级 0–55;此值常用于 CMSIS 优先级映射 |
configUSE_PORT_OPTIMISED_TASK_SELECTION | 与端口相关 | V11.1.0 GCC ARM_CM3 的位图优化最多 32 级;56 级需设为 0 |
configMINIMAL_STACK_SIZE | 128 | Idle 任务栈及部分默认值的基准,单位是栈元素;不是所有任务强制的最小栈 |
configMAX_TASK_NAME_LEN | 16 | 包含末尾 NUL 的存储长度,通常最多 15 个可见单字节字符 |
configTOTAL_HEAP_SIZE | 3072 | 对 heap_4.c 为堆数组总字节数,含 TCB、任务栈、队列及分配器开销等 |
| 互斥量/递归互斥量/计数信号量 | 按需要启用 | 同时核对包装层对这些配置的要求 |
| 任务通知 | 按需要启用 | 可用于任务事件/计数;通知槽及语义须与其他组件协调 |
3072 字节堆可能连多个 512 元素的任务栈都容纳不了,还要给 Idle、启用时的 Timer 任务和其他对象留空间。它既不是整个 MCU RAM 大小,也不是所有 heap_x.c 方案共用的容量定义。静态分配还需正确提供内核要求的静态任务内存。
原 CMSIS-RTOS2 伺服任务属性中的 512 * 4 表示 2048 字节:
#include "cmsis_os2.h"
extern void servoTask(void *argument);
osThreadId_t create_servo_thread(void){ static const osThreadAttr_t attributes = { .name = "servoTask", .stack_size = 512U * 4U, .priority = osPriorityHigh }; return osThreadNew(servoTask, NULL, &attributes);}调用方须在内核初始化后检查返回值非空。ST CMSIS-RTOS2 包装层的核对版本把 stack_size 除以 sizeof(StackType_t),把 CMSIS 优先级映射为 prio - 1;osPriorityHigh 不是 NVIC 中断优先级,也不能不查包装层就当作原生 FreeRTOS 的同值优先级。
异常向量只能有一个实现
跳转到“异常向量只能有一个实现”本文 V11.1.0 GCC ARM_CM3 原生端口可采用直接名称映射,在应用配置中将移植函数绑定到启动文件的异常名:
/* FreeRTOSConfig.h 中的一种原生端口绑定方式。 */#define vPortSVCHandler SVC_Handler#define xPortPendSVHandler PendSV_Handler#define xPortSysTickHandler SysTick_Handler使用这种方式时,工程不能再编译另一份同名强定义。头文件中的函数声明不会产生重复符号,删除 .h 中的原型无法解决 .c 里存在两份实现的问题。检查链接映射中 SVC_Handler、PendSV_Handler、SysTick_Handler 的来源,再检查启动向量确实指向它们。
SVC/PendSV 是带有汇编上下文约束的移植入口,不宜随意包一层普通 C 函数再调用。SysTick 若还承担 HAL Tick,则不能直接照搬上面的独占映射;需要与项目包装层和生命周期匹配的单一分派入口,详见HAL 与 FreeRTOS 时基。
PB5 翻转的原生任务
跳转到“PB5 翻转的原生任务”以下模块假定 CubeMX 已完成时钟和 PB5 推挽输出配置,HAL Tick 已有正确来源。原时钟示例为外部 8 MHz 晶振经 PLL ×9 得到 72 MHz,APB1 二分频、APB2 不分频;必须与实际晶振和芯片限制一致。UART1 可保留为诊断外设,但不在此任务中反复阻塞打印。
#include "stm32f1xx_hal.h"#include "FreeRTOS.h"#include "task.h"
extern void application_fatal_error(void);
static void blink_task(void *argument){ (void)argument; const TickType_t half_period = pdMS_TO_TICKS(500); configASSERT(half_period > 0); for (;;) { HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5); vTaskDelay(half_period); }}
void start_native_blink_application(void){ /* 栈深度 256 个元素需按实际调用路径和水位测量复核。 */ if (xTaskCreate(blink_task, "Blink", 256, NULL, tskIDLE_PRIORITY + 1U, NULL) != pdPASS) { application_fatal_error(); for (;;) { } } vTaskStartScheduler(); /* 正常启动后不返回;返回时检查 Idle/Timer 等创建失败。 */ application_fatal_error(); for (;;) { }}每次翻转间隔约 500 ms,完整高低周期约 1 秒,实际运行还受 Tick 相位和调度延迟影响。LED 是高电平亮还是低电平亮由板级电路决定。需要稳定释放相位时可改用 xTaskDelayUntil();需要严格硬件波形则使用定时器输出。
原 F103ZET6 示例中 HAL_TIM_PeriodElapsedCallback() 判断 TIM6 后调用 HAL_IncTick(),仅在工程真的配置 TIM6 为 HAL 时基、其 IRQ 和句柄都正确时成立。F103x8/xB 没有 TIM6;复制一个回调不能凭空创建定时器时基。
最小验证顺序
跳转到“最小验证顺序”先确认裸机时钟、GPIO 和 HAL Tick 正常,再核对任务创建返回值和剩余堆;启动后观察 Tick、任务状态与栈水位。遇到硬故障或任务不切换时,检查向量归属、端口匹配、中断优先级分组和 configASSERT,而非优先调大所有优先级或注释掉头文件声明。
参考
跳转到“参考”- FreeRTOS-Kernel V11.1.0 ARM_CM3 端口
- ST 的 CMSIS-RTOS2 包装层(核对日期:2026-10-03;实际工程应固定固件包版本)
- CMSIS-RTOS2 线程属性
- STM32CubeMX 用户手册 UM1718