← 返回文章库

在 STM32 上跑 FreeRTOS:和 ESP-IDF 的异同

最后更新 2026-06-22
⏱ 约 19 分钟 🟢 软件/低风险
你将学到
  • 用 CubeMX 的 Middleware → FreeRTOS 勾出工程,看清它和 ESP-IDF 内置 FreeRTOS 的集成差异
  • 搞懂 CubeMX 默认生成的 CMSIS-RTOS v2 封装是什么、它和原生 FreeRTOS API、ESP-IDF 三方怎么对照
  • 躲过 SysTick 时基冲突这个 STM32 跑 FreeRTOS 的头号坑——把 HAL 时基换到别的 TIM
  • 用 osThreadNew 建两个线程闪灯 + 打印,把第 9 章的 xTaskCreate 双任务平移到 STM32

你在第 9 章已经把 FreeRTOS 啃透了:xTaskCreate 建任务、栈单位是"字"、优先级数字越大越高、任务必须 for(;;) 死循环、vTaskDelay 让出 CPU、队列和信号量传数据。那一整套是在 ESP32-S3 上跑的。现在你换到 STM32,第一个想问的多半是:这套还能用吗?得重学吗?

好消息:几乎不用重学,因为内核是同一个 FreeRTOS。 ESP-IDF 底下跑的 FreeRTOS、STM32 上跑的 FreeRTOS,是 Richard Barry 那同一套开源内核,xTaskCreatexQueueSendxSemaphoreTake 这些原生 API 字字相同、行为一致。你在第 9 章建立的任务模型——一件事一个任务、各睡各醒、调度器分 CPU——原封不动迁过来

那这一篇讲什么?讲异同里那个"异":FreeRTOS 在 STM32 上怎么集成进工程(和 ESP-IDF 内置不一样)、CubeMX 默认给你套了一层叫 CMSIS-RTOS v2 的封装(API 名字变了样、但底下还是 FreeRTOS)、还有一个几乎人人都踩的 SysTick 时基冲突坑。把这三件"不一样"讲清楚,你的第 9 章功力就能在 STM32 上原地复用。

📌 说明

这是 S 卷(STM32 迁移)里讲 RTOS 的一篇,丑话先说:STM32 上的 FreeRTOS 集成强依赖你的 CubeMX 版本、HAL 库版本、芯片型号。 本篇给的是主干流程参考——osThreadNew/osDelay 这些 CMSIS-RTOS v2 函数名、生成的句柄变量名、引脚号,请以你自己用 CubeMX 实际生成出来的工程 + ST 官方文档(UM1722)+ FreeRTOS / CMSIS-RTOS v2 官方文档为准。CMSIS-RTOS v2 的封装在不同 CubeMX 版本里生成的样子有差异,本篇不把任何一行代码呈现为"确定无误"。FreeRTOS 的概念本身——任务、栈、优先级、队列、信号量——已经在 第 9 章 FreeRTOS 任务 讲透了,本篇默认你看过,只补 STM32 这边的差异,不重讲概念。

读这篇前,你最好已经跑通过 第 9 章的 FreeRTOS 双任务——知道任务、xTaskCreate 六个参数、vTaskDelay 为什么是共存的关键。也最好走过 S 卷的 CubeMX + HAL 点灯,会用 CubeMX 配引脚、配时钟、GENERATE。这两块是本篇的地基。


集成方式:STM32 要在 CubeMX 里"勾",ESP-IDF 内置不用勾

第一个不一样,在你还没写一行代码时就来了:FreeRTOS 在两个平台上"从哪来",完全不同。

ESP-IDF:内置,无需勾选。 你在第 9 章 #include "freertos/FreeRTOS.h" 直接就能用 xTaskCreate——因为 ESP-IDF 把 FreeRTOS 当成系统的一部分编译进去了,app_main 本身就跑在一个任务里。FreeRTOS 对 ESP-IDF 来说不是"可选组件",是地基,你想绕都绕不开。

STM32:要在 CubeMX 里手动勾出来。 STM32 的 HAL 工程默认是裸机的(一个 main() + while(1),就像 S 卷点灯那篇)。要上 FreeRTOS,你得在 CubeMX 里:

  1. 打开你的工程(或新建),在左侧 Categories 里找到 Middleware and Software Packs(旧版叫 Middleware)。
  2. 点开 FREERTOS,在 Mode 里把 Interface 选成 CMSIS_V2(默认推荐,也可选 CMSIS_V1,但 V2 是现在的标准)。
  3. 切到 Configuration → Tasks and Queues,可以在图形界面里点着加任务、加队列——CubeMX 会把它们翻译成生成代码里的 osThreadNew / osMessageQueueNew 调用。也可以一个都不加,生成后自己在 main.c 里写。
  4. GENERATE CODE。 这一下 CubeMX 干了几件事:把 FreeRTOS 源码拉进工程、生成一个 freertos.c(放默认线程)、把 main() 改成 创建调度器并启动osKernelInitialize() → 建默认线程 → osKernelStart()),从此你的 while(1) 不再是裸机循环,而是跑在 RTOS 之下。

一句话:ESP-IDF 是"FreeRTOS 已经在那了,你直接用";STM32 是"你在 CubeMX 里点一下把它装进来"。 装进来之后,内核行为一模一样。


API 层:CMSIS-RTOS v2 是什么——三方对照表(本篇的灵魂)

这是最容易让人懵的一点。你在第 9 章写的是 xTaskCreatevTaskDelay,到 STM32 一看 CubeMX 生成的代码,怎么变成了 osThreadNewosDelay这俩是同一回事吗?

是。 CubeMX 默认生成的不是原生 FreeRTOS API,而是一层叫 CMSIS-RTOS v2 的封装。

📌 说明

CMSIS-RTOS v2(Cortex Microcontroller Software Interface Standard - RTOS API v2)是 ARM 定的一套标准 RTOS 接口。 它本身不是一个 RTOS,而是一层统一的 API 壳子——底下可以套 FreeRTOS、也可以套 RTX 或别的内核。CubeMX 选 CMSIS_V2 时,底下套的就是 FreeRTOS,osThreadNew 这些函数内部最终调的还是 xTaskCreate。ARM 搞它的目的是:你的应用代码用 os... 这套标准接口写,将来换底层 RTOS 不用大改。

所以在 STM32 上你有两种写法都能用

  • 用 CMSIS-RTOS v2(CubeMX 默认)osThreadNewosDelayosMessageQueueNewosMutexNew……和 CubeMX 生成的代码风格统一,推荐跟着它走。
  • 直接用原生 FreeRTOS APIxTaskCreatevTaskDelayxQueueSend……和你第 9 章学的字字相同。CMSIS 层只是封装,原生 API 一样能调(#include "FreeRTOS.h" / "task.h" 还在)。混用也行,但同一处别来回换风格。

下面这张三方对照表是本篇核心。左边是你第 9 章已经会的原生 FreeRTOS,中间是 STM32 CubeMX 默认的 CMSIS-RTOS v2,右边对回 ESP-IDF 语境,把你的肌肉记忆一条条接上:

干的事 原生 FreeRTOS(第 9 章学的) CMSIS-RTOS v2(STM32 CubeMX 默认生成) 在 ESP-IDF 里
建任务 / 线程 xTaskCreate(fn, name, stack, arg, prio, &h) osThreadNew(fn, arg, &attr) xTaskCreate(...)(同原生)
任务延时 / 让出 CPU vTaskDelay(pdMS_TO_TICKS(500)) osDelay(500)(直接毫秒,不用换算) vTaskDelay(pdMS_TO_TICKS(500))
删除自己 vTaskDelete(NULL) osThreadExit() vTaskDelete(NULL)
建队列 xQueueCreate(len, size) osMessageQueueNew(len, size, &attr) xQueueCreate(...)(同原生)
入队 / 出队 xQueueSend / xQueueReceive osMessageQueuePut / osMessageQueueGet xQueueSend / xQueueReceive
建互斥量 xSemaphoreCreateMutex() osMutexNew(&attr) xSemaphoreCreateMutex()
上锁 / 解锁 xSemaphoreTake / xSemaphoreGive osMutexAcquire / osMutexRelease xSemaphoreTake / xSemaphoreGive
建二值信号量 xSemaphoreCreateBinary() osSemaphoreNew(1, 0, &attr) xSemaphoreCreateBinary()
启动调度器 vTaskStartScheduler() osKernelStart()(CubeMX 在 main 里替你写好) ESP-IDF 启动时自动起,app_main 已在任务里

最该记的三条:一是 CMSIS-RTOS v2 的 osDelay 直接收毫秒,不像原生 vTaskDelaypdMS_TO_TICKS 换算,少个坑;二是 osThreadNew 的栈、优先级是通过一个 osThreadAttr_t 结构体的 attr 传的,不像 xTaskCreate 平铺六个参数——但栈那个"字 vs 字节"的本质没变(见下方 demo 注释);三是 ESP-IDF 这一列基本等于原生 FreeRTOS 列,因为 ESP-IDF 用的就是原生 API,没套 CMSIS 这层。


时基坑:SysTick 给谁用——STM32 跑 FreeRTOS 的头号翻车点

这是 STM32 上 FreeRTOS 最隐蔽、也最高频的坑,单拎出来讲。

问题的根:FreeRTOS 要一个**周期性时钟节拍(tick)**来驱动调度器和 vTaskDelay 计时——默认它用 Cortex-M 内核里的 SysTick 定时器,每个 tick 触发一次中断。可巧了,STM32 的 HAL 库默认也用 SysTick 来给 HAL_Delay() 和各种 HAL 超时计时(HAL 内部维护一个 uwTick 毫秒计数,靠 SysTick 中断累加)。

两个都要 SysTick,就打架了:FreeRTOS 接管 SysTick 后,HAL 的 uwTick 可能不再按预期递增,导致裸机阶段(调度器还没启动时)调用的 HAL_Delay 行为异常,或者两套对 SysTick 中断优先级的预期冲突,表现是程序卡死、调度不动、HAL_Delay 不准——非常难凭直觉定位。

ST 官方的解法,CubeMX 里点一下就行把 HAL 的时基从 SysTick 换到一个普通定时器(TIM)上,SysTick 专留给 FreeRTOS。

  • 在 CubeMX 里:System Core → SYS → Timebase Source,从默认的 SysTick 改成一个你没在用的 TIMx(比如 TIM6、TIM7 这种基础定时器,最适合,因为它们功能简单、不占用你要做正事的高级定时器)。
  • 实际上,当你在 CubeMX 里勾上 FreeRTOS 时,新版 CubeMX 会主动弹窗提示你"HAL 时基还在 SysTick 上,建议换到别的 TIM"——别嫌烦点掉,照它说的换。换完 GENERATE,HAL 用 TIMx 计时、FreeRTOS 用 SysTick,各走各的,井水不犯河水。

对比一下 ESP-IDF:你在第 9 章压根没遇到这个问题——因为 ESP32 的 FreeRTOS tick 用的是芯片专门的定时器,ESP-IDF 把这些底层时基都安排好了,对你透明。这又是一例"STM32 把 ESP-IDF 替你藏起来的事摊到了你面前":时基归谁管,STM32 要你自己点清楚。

🚧 避坑

勾了 FreeRTOS 没换 HAL 时基,是 STM32 跑 RTOS 第一周最容易栽的坑。症状往往是"代码看着没错,烧进去就卡死或乱跑",新手第一反应去查任务代码,其实根在 SysTick 被两边争。记住这条规矩:STM32 + FreeRTOS = HAL 时基必须从 SysTick 挪到 TIMx。 CubeMX 弹窗提示时顺手改掉,能省你半天 debug。


核数:STM32 单核(多数)vs ESP32-S3 双核

第 9 章末尾提过 ESP32-S3 是双核,能用 xTaskCreatePinnedToCore 把任务钉到指定核。这里点一下差异:

  • STM32 绝大多数型号是单核(F1/F4/G4/F7/H7 单核系列等)。单核上 FreeRTOS 跑的就是经典的单核抢占式调度 + 同优先级时间片轮转——和你第 9 章理解的模型完全吻合,没有"钉核"这回事,所有任务在一个核上轮流跑。
  • ESP32-S3 是双核,所以才有 xTaskCreatePinnedToCore、才需要操心任务在哪个核、跨核数据同步。

所以从 ESP32-S3 迁到单核 STM32,调度模型反而更简单了——少了一个核要操心。(注意:STM32H7 也有双核型号、MP1 是异构多核,但那是另一类话题,主线单核 STM32 不涉及。)一句话:单核让你少想一层,第 9 章的单核心智模型在 STM32 上正好够用。


完整 demo:CubeMX 勾 FreeRTOS → 两个线程闪灯 + 打印

现在把第 9 章那个"闪灯 + 心跳"双任务,原样平移到 STM32,用 CMSIS-RTOS v2 写。对照着看,你会发现结构一模一样,只是 API 换了壳

CubeMX 这边先配好(接 S 卷点灯那篇的工程,蓝丸 STM32F103C8,LED 在 PC13):

  1. 配好 PC13 GPIO_Output、配好时钟树(HCLK 拉满不标红)——这步和点灯篇一样。
  2. 配一个 USART1(PA9/PA10,115200,Asynchronous) 用来打印,并按点灯篇变体二重定向好 printf(或直接用 HAL_UART_Transmit)。
  3. Middleware → FREERTOS → Interface 选 CMSIS_V2
  4. System Core → SYS → Timebase Source 改成 TIM6(躲 SysTick 坑,CubeMX 弹窗也会提醒)。
  5. GENERATE CODE。

生成后,在 Core/Src/main.c(或 CubeMX 生成的 freertos.c 的 USER CODE 区)里,把两个线程写出来:

/* USER CODE BEGIN 0 */
#include "cmsis_os2.h"   // CMSIS-RTOS v2 接口
#include <stdio.h>

// 线程一:闪灯。对照第 9 章的 blink_task,结构完全一样,只是 API 换壳
void blink_thread(void *arg)
{
  for (;;) {                                   // 和原生一样:线程体必须死循环,不能跑到底
    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);    // 翻转 PC13(≈ ESP-IDF 里翻 GPIO 电平)
    osDelay(500);                              // CMSIS 的 osDelay 直接收毫秒,不用 pdMS_TO_TICKS
  }
}

// 线程二:心跳打印。和闪灯线程并行跑,各睡各醒
void log_thread(void *arg)
{
  int count = 0;
  for (;;) {
    printf("心跳 #%d,闪灯线程此刻还在自己闪\r\n", count++);  // 已重定向到 USART1
    osDelay(1000);                             // 每秒打一行
  }
}
/* USER CODE END 0 */

然后在 main() 的 RTOS 初始化处,用 osThreadNew 把这俩线程建出来。CubeMX 生成的 main() 里已经有 osKernelInitialize()osKernelStart() 的骨架,你只在它俩之间把线程加进去:

/* USER CODE BEGIN 2 */
osKernelInitialize();    // CubeMX 生成的内核初始化(≈ 原生 vTaskStartScheduler 前的准备)

// attr 里带名字和栈大小。stack_size 单位是【字节】(CMSIS-RTOS v2 的约定,
// 和原生 xTaskCreate 那个"字"不同——这是迁移时要重新校的一个点!)
const osThreadAttr_t blink_attr = { .name = "blink", .stack_size = 512 * 4, .priority = osPriorityNormal };
const osThreadAttr_t log_attr   = { .name = "log",   .stack_size = 512 * 4, .priority = osPriorityNormal };

osThreadNew(blink_thread, NULL, &blink_attr);   // 对照第 9 章 xTaskCreate(blink_task, ...)
osThreadNew(log_thread,   NULL, &log_attr);     // 对照第 9 章 xTaskCreate(log_task, ...)

osKernelStart();         // 启动调度器,从此两个线程并行跑(CubeMX 已生成,不用你写)
/* USER CODE END 2 */
while (1) { }            // osKernelStart 后一般跑不到这,调度器接管了

你应该看到什么

烧录运行后,和第 9 章那个 ESP32-S3 的画面一模一样——这正是"内核是同一个"的直观证明:

  • 板载 LED(PC13)稳定地每半秒翻一次,一明一暗不停。
  • 同时串口(USART1,115200)每秒滚出一行心跳,计数往上走:
心跳 #0,闪灯线程此刻还在自己闪
心跳 #1,闪灯线程此刻还在自己闪
心跳 #2,闪灯线程此刻还在自己闪

两个节奏完全不同、谁也没卡住谁——osDelay 让出 CPU 的时候,调度器把它调给了另一个线程。这套机制和第 9 章 vTaskDelay 让出 CPU 的原理字字对应,因为底下就是同一个 FreeRTOS。

如果灯不闪、串口没输出、或整个卡死——先别怀疑线程代码,八成是时基没换(SysTick 坑)或栈给小了。对着下面的表查。


故障排查:STM32 上 FreeRTOS 跑不对,按这个查

现象 最可能的原因 怎么办
烧进去整个卡死 / 调度器不转 / HAL_Delay 不准 HAL 时基还在 SysTick 上,和 FreeRTOS 争 回 CubeMX:SYS → Timebase Source 改成 TIM6/TIM7,重新 GENERATE(本篇头号坑)
线程跑一会儿就崩 / 进 HardFault 线程栈给小了(溢出),或 osThreadAttr_tstack_size 填错 stack_size 调大(CMSIS v2 单位是字节,别按原生那个"字"算);开 configCHECK_FOR_STACK_OVERFLOW
线程压根不跑 优先级被饿死 / 忘了 osKernelStart() / 线程函数跑到底返回了 确认线程体是 for(;;) 死循环;确认 osKernelStart() 被调到;给霸 CPU 的线程加 osDelay
改完 CubeMX 重新生成,自己写的线程代码没了 代码写到了 USER CODE BEGIN/END 区外 所有自己的代码必须写在配对注释之间,区外会被 CubeMX 覆盖(S 卷点灯篇讲过的老规矩)
串口没输出但灯在闪 printf 没重定向 / USART 没配对 按点灯篇变体二把 _write(GCC)或 fputc(Keil)指到 USART;或直接用 HAL_UART_Transmit
osThreadNew 返回 NULL,线程建不出来 堆不够(configTOTAL_HEAP_SIZE 太小或栈要太多) 在 CubeMX FreeRTOS Config 里把 TOTAL_HEAP_SIZE 调大;或削小每个线程的栈
编译报找不到 osThreadNew / osDelay FreeRTOS 没勾 / 选了 CMSIS_V1 但用了 V2 的 API 确认 Middleware → FREERTOS Interface 选的是 CMSIS_V2;确认 #include "cmsis_os2.h"
🚧 避坑

表里头两条——时基没换栈单位算错——是从 ESP-IDF 迁过来的人最容易栽的。时基坑 ESP-IDF 不存在(它替你管了);栈单位坑则是因为你脑子里记着第 9 章"栈单位是字",到了 CMSIS-RTOS v2 的 osThreadAttr_t.stack_size 这里单位变成了字节,照着字的数填就小了 4 倍直接溢出。迁移时这两点务必重新校一遍。


小结 · 你现在掌握了什么

  • 你想通了最关键的一条:STM32 和 ESP-IDF 上的 FreeRTOS 内核是同一个,第 9 章学的任务 / 队列 / 信号量模型直接迁过来,不用重学概念——概念在 第 9 章 FreeRTOS 任务 已经讲透,本篇只补 STM32 的差异。
  • 你搞清了集成方式的异:STM32 要在 CubeMX 的 Middleware → FREERTOS 里勾出来(选 CMSIS_V2),ESP-IDF 内置无需勾。
  • 你拿到了 CMSIS-RTOS v2 ↔ 原生 FreeRTOS ↔ ESP-IDF 三方对照表:CubeMX 默认生成的 osThreadNew/osDelay/osMessageQueueNew/osMutexNew 是 ARM 标准封装,底下还是 FreeRTOS;原生 xTaskCreate 那套也能直接用,和 ESP-IDF 字字相同。
  • 你躲过了头号坑:STM32 + FreeRTOS 必须把 HAL 时基从 SysTick 挪到 TIMx,否则 SysTick 被两边争,卡死或 HAL_Delay 不准——这是 ESP-IDF 没有、STM32 独有的功课。
  • 你知道了核数差异:STM32 多数单核(调度模型更简单,无需钉核),ESP32-S3 双核才有 xTaskCreatePinnedToCore
  • 你把第 9 章的 xTaskCreate 双任务,用 osThreadNew 原样平移成 STM32 的闪灯 + 打印双线程,亲眼看到画面一致——这就是"同一个内核"的最好证明。
  • 最重要的一条:本篇 CMSIS-RTOS v2 / HAL 代码是主干流程参考,函数名、osThreadAttr_t 字段、句柄名、引脚强依赖你的 CubeMX 版本与 HAL 版本,以 CubeMX 实际生成的工程 + ST UM1722 + FreeRTOS / CMSIS-RTOS v2 官方文档为准,别当成确定无误。

RTOS 在 STM32 上跑起来了,下一步就该让它驱动真正会动的东西——用 HAL + 定时器 PWM 控一个电机,把转速调起来。接着看 STM32 控电机:PWM 调速与驱动选型。想回看整条迁移路线,去 STM32 迁移卷总览

📄 来源 / 自校链接

本文为公开资料整理,非亲测。关键参数与代码请结合实物与下列官方来源验证。

内容有错、看不懂、或想看下一期?告诉我们 →

本文为公开资料的学习整理,非亲测。涉接线/花钱/合规的步骤请结合实物与官方最新资料验证,风险自负。见免责声明