跳转到内容
新建笔记

STM32CubeMX 与 FreeRTOS 工程集成

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-RTOS2osKernelInitialize(),创建对象或调用生成初始化,再 osKernelStart();线程常由 osThreadNew() 创建

CMSIS 是 API 包装层。不要在生成的 osKernelStart() 之外再启动一次原生调度器,也不要把 v1 的 osSystickHandler() 当成所有 v2/原生工程必有的函数。使用 CubeMX 时,先看生成的 main.c、freertos.c、中断文件及配置文件,再决定修改位置。使用用户代码区和工程自有模块,降低重新生成时覆盖改动的概率。

手动加入源码与头文件

跳转到“手动加入源码与头文件”

历史 FreeRTOS 下载包中的 Demo、License、Source、Test 与说明文件

图中的 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 时,额外加入该固件包所配套的包装源文件和头文件。

历史 portable 目录中指向 RVDS 的标注,旁边还列有 GCC 和 MemMang

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

MDK 工程组中的七个内核功能源文件以及 heap_4.c、port.c,共九个文件

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

MDK 头文件搜索路径中的内核 include、RVDS ARM_CM3、STM32 CMSIS 和 HAL 目录

CMake 工程做同样的事情:用源文件列表加入内核、端口和堆文件,用目标的头文件目录加入应用配置、内核公共接口和端口。已有 arm-none-eabi-gcc 工具链文件时,应沿用项目的 CPU、Thumb、ABI、链接脚本和启动文件设置。不要仅为了 FreeRTOS 再添加第二套启动文件或第二个向量表。

原 CubeMX 参数可作为读配置的例子,不能不经内存预算照搬:

参数/选项原例值或功能正确解释
configTICK_RATE_HZ1000名义 Tick 周期 1 ms;中断和调度开销仍需测量
configMAX_PRIORITIES56允许原生优先级 0–55;此值常用于 CMSIS 优先级映射
configUSE_PORT_OPTIMISED_TASK_SELECTION与端口相关V11.1.0 GCC ARM_CM3 的位图优化最多 32 级;56 级需设为 0
configMINIMAL_STACK_SIZE128Idle 任务栈及部分默认值的基准,单位是栈元素;不是所有任务强制的最小栈
configMAX_TASK_NAME_LEN16包含末尾 NUL 的存储长度,通常最多 15 个可见单字节字符
configTOTAL_HEAP_SIZE3072对 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 时基。

以下模块假定 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,而非优先调大所有优先级或注释掉头文件声明。