STM32F407统一实测8款RTOS:上下文切换与中断延迟
2026/9/18 20:27:41 网站建设 项目流程

1. 先把这场实测的边界划清楚

同一块 MCU 上跑 8 款 RTOS,这件事的价值不在于排出一个"谁第一谁倒数"的榜,而在于把那些藏在编译选项、时钟树、中断优先级里的变量全部摁死,只让内核本身说话。我在过去两年里给不同的项目做过 MCU 选型,见过太多次"某款 RTOS 慢得没法用"的结论,最后追下去发现是 SysTick 优先级配错、或者是拿一个带 shell 和文件系统的全功能构建,去比人家裁剪到骨头的最小构建,这种对比毫无意义,还容易让团队做出错误的架构决策。

所以这篇文章要解决三个具体问题:第一,在一个完全统一的软硬件约束下,8 款主流 RTOS 在上下文切换、信号量、互斥量、中断到任务延迟、节拍开销、内存占用这六项指标上的真实差距到底有多大;第二,哪些差距是内核设计带来的,哪些差距纯粹是配置和测量方法造成的假象;第三,也是最关键的一点,哪些 RTOS 最容易被误判——被高估的和被低估的,分别是谁。

适合谁来读:正在做 MCU 选型、被"RTOS 性能"这个话题绕晕的嵌入式工程师;准备把裸机工程改成 RTOS、但不知道从哪一款下手的开发者;以及已经用着某款 RTOS、想看看到底有没有调优空间的同行。文中的绝对数值都是我在这块板子上的实测值,你换一块芯片、换一个编译器版本,数值一定会变,但结论的方向和那些坑的形态,大概率是一样的。这一点我在开头必须讲明白,否则后面所有的表格都会被人拿去当"真理"用。

1.1 为什么"同一块 MCU"是这场对比唯一的公平起点

市面上能查到的 RTOS 跑分,绝大多数来自各家自己的 BSP 或者官方开发板。FreeRTOS 的官方数据往往跑在 Cortex-M3 上,ThreadX 的公开数据来自某个高频 M7,Zephyr 的基准测试又分布在一堆不同的板子上,频率从 16MHz 到 480MHz 都有。把这些数字放在一张表里比较,本质上是在比芯片,不是在比内核。

我把 8 款 RTOS 全部收敛到同一块STM32F407VGT6上:Cortex-M4F,168MHz 主频,1MB Flash,192KB SRAM,外部 8MHz 晶振经 PLL 倍频,Flash 等待周期固定为 5WS,ART Accelerator 的指令缓存和数据缓存都打开。选 M4 而不是 M0 或者 M7 是有考虑的:M0 没有 DWT 的 CYCCNT 周期计数器,做不了周期级测量;M7 带 DCache,缓存命中与否会让同一段代码的耗时差出三倍,测出来的东西噪声太大,不适合做首次横向对比。

同一块板子的意义在于,Flash 等待周期、总线仲裁、SRAM 访问速度、中断控制器版本、SysTick 行为这些底层变量全部是同一套。剩下需要控制的就是软件侧:编译器、优化等级、节拍频率、堆分配器、任务栈大小、NVIC 优先级分组。这些东西我后面会逐条列成表格,每一项都要对齐,漏掉任何一项,结论就不可复现。

另外我还用GD32F103C8T6(Cortex-M3,108MHz,64KB Flash,20KB SRAM)做了一轮复核。选它是因为这颗芯片在成本敏感项目里太常见了,20KB SRAM 这个硬约束会直接筛掉一批选手,这个筛选结果比跑分本身更有参考价值。我先把话说在前面:NuttX 和 Zephyr 的全功能构建在这颗芯片上根本装不下,这不是它们"慢",是它们的目标场景本来就不在这。

1.2 八位选手的名单与版本锁定

版本锁定这件事,吃过亏的人都懂。FreeRTOS 10.4 和 11.x 在调度器内部有改动,RT-Thread Nano 和全功能 RT-Thread 是两个量级的东西,Zephyr 的 LTS 版本和主线版本在启动流程上差别很大。我用的具体版本如下:

编号RTOS版本内核形态授权模式
1FreeRTOSV11.1.0 (kernel only)微内核MIT
2RT-Thread Nano3.1.5微内核(裁剪)Apache-2.0
3RT-Thread(全功能)5.0.2含设备框架/DFS/shellApache-2.0
4Zephyr3.7 LTS单体内核Apache-2.0
5Eclipse ThreadX6.4.0微内核MIT
6Keil RTX55.9.0 (CMSIS-RTOS2)微内核Apache-2.0
7µC/OS-III3.08.01微内核商业/Apache 双轨
8LiteOS-M2.2微内核BSD-3

这里有个细节必须交代清楚:RT-Thread 我放了两份(Nano 和全功能),因为这两者在社区讨论里经常被混为一谈,而它们的实测数据差了将近一个数量级。这不是凑数,这是本文"误判"主题最典型的一个样本。同样地,Zephyr 我准备了三套配置:最小构建、带 logging 的构建、带 logging + shell + 网络的构建,后面的数据表里会分别列出来。

1.3 明确不测什么,避免结论被过度解读

我不测生态成熟度、不测文档质量、不测中间件丰富程度、不评商业授权成本,这些是选型时要考虑的因素,但它们不是"性能",混在一起谈会让整篇文章失焦。我也不测多核、不测 SMP、不测安全认证相关的功能开销。

还有一条我特意排除的:不做"综合得分"。我见过太多文章最后给一张加权总表,上下文切换占 30%、内存占 20%……这种权重完全是拍脑袋的产物。你做一个 200Hz 的电机控制,和做一个需要跑 TCP 连接的网关,对"快"的定义完全不同。数据给你,权重你自己定,这才是有用的输出。

2. 测试平台搭建与测量链路设计

测量方法错了,后面所有的表都是废纸。这一节我把整套测量链路拆开讲,包括为什么选 DWT 周期计数器做主力、为什么必须用示波器做交叉验证、以及两者结果不一致时该信谁。

2.1 用 DWT CYCCNT 做周期级测量:最小实现与三个陷阱

Cortex-M3/M4/M7 的 DWT 单元里有个 CYCCNT 寄存器,内核每过一个时钟周期就加一,读取它几乎没有开销(一条 LDR 指令)。用它测代码段耗时,精度是"周期",在 168MHz 下就是 5.95 纳秒,比任何软件打点方案都精确。

初始化代码很短,但有几个必须做的动作:

/* bench_dwt.h —— 最小侵入的周期计数测量 */ #include "stm32f4xx.h" static inline void bench_dwt_init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; /* 打开跟踪单元时钟,不开的话 CYCCNT 不动 */ DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; /* 使能周期计数器 */ __DSB(); /* 保证配置生效再往下走 */ } /* 打点用宏,避免函数调用本身引入额外周期 */ #define BENCH_RESET() do { DWT->CYCCNT = 0; } while (0) #define BENCH_CYCLES() (DWT->CYCCNT) #define CYCLES_TO_US(c) ((float)(c) / (float)SystemCoreClock)

第一个陷阱是DEMCRTRCENA位。很多人只开了DWT_CTRLCYCCNTENA,结果计数器纹丝不动,以为是芯片不支持。实际上 DWT 的时钟门控由DEMCR.TRCENA控制,Keil 和 IAR 的工程模板里通常初始化好了,GCC 的启动文件里往往没有,这是移植到 GCC 后第一个要补的地方。

第二个陷阱是调试器的影响。DWT->CYCCNT在连接调试器时会被调试单元的行为干扰——单步执行、断点命中都会让计数不准,更隐蔽的是某些调试配置会让内核在断点处停表。所以所有跑分数据必须在脱机状态下采集,通过串口或者 SWO 把结果发出来,而不是在 IDE 的 Watch 窗口里读数。

第三个陷阱是 32 位溢出。168MHz 下 CYCCNT 大约 25.6 秒回绕一次。单次测量都在微秒级,不会溢出,但如果你用它测整个任务的执行周期,就必须处理回绕。我的做法是测量窗口严格控制在 10ms 以内,超出就换用定时器。

2.2 GPIO 翻转加示波器:端到端延迟的交叉验证手段

DWT 测的是"两条指令之间的周期数",它能测内核内部的开销,但测不了"中断引脚拉高到任务里 GPIO 拉高"这种端到端的东西。原因很简单:从外部信号进来,经过 NVIC 采样、压栈、跳转到 ISR,这一段的起点不在 CPU 内部,DWT 无处打点。

端到端测量必须靠 GPIO:

#define TRACE_HIGH() (GPIOC->BSRR = GPIO_BSRR_BS_6) /* 用 BSRR,单周期,不带读改写 */ #define TRACE_LOW() (GPIOC->BSRR = GPIO_BSRR_BR_6)

注意用BSRR而不是ODR的读改写操作。GPIOC->ODR |= x编译出来是「读-或-写」三条指令,还会在总线上产生额外访问;BSRR是单次写,一个周期完成,把测量本身的扰动压到最小。

实测时我用信号发生器给一个外部中断引脚送方波,ISR 入口第一件事拉高 PC6,被唤醒的高优先级任务第一件事拉低 PC6,示波器直接读脉冲宽度。这个宽度里包含了:NVIC 响应时间、压栈时间、ISR 代码执行时间(含 RTOS 的 FromISR 调用)、PendSV 触发与调度器决策、出栈时间、任务恢复执行时间。这才是真正意义上的"中断到任务唤醒延迟"。

DWT 和示波器结果不一致的时候,信示波器。DWT 适合做相对比较和内核内部的精细分析,示波器适合做绝对值的最终确认。我这次的数据表里,上下文切换、信号量路径这类"内核内部"指标用 DWT,中断到任务延迟和节拍抖动用示波器。

2.3 差分法测切换开销:绕开指令计数误差的笨办法

上下文切换的开销很难直接测,因为一次切换之后你不能确定任务从哪里继续执行。社区里有个很实用的笨办法,我一直在用:让两个同优先级任务做乒乓,通过taskYIELDosThreadYield主动让出,任务体里翻转 GPIO,示波器读到一个完整周期的宽度。这个周期等于:

周期宽度 = 两次切换开销 + 两次任务体执行时间

然后把任务体里的空循环次数从 0 改到 100,再改到 200,测三组数据做线性拟合,斜率和截距一算,切换开销就出来了。这个方法的妙处在于它自动抵消了 GPIO 翻转、循环计数器、分支判断这些固定开销的影响,不需要你去数汇编指令。

用差分法测出的纯切换开销,比一些文章里"测一次 yield 的耗时"要准确得多。后者把 GPIO 操作、函数调用、参数检查全算进切换时间里了,数值能虚高 40% 以上。

2.4 统一约束清单:不对齐这九项,数据就没有意义

下面这张表是全文最重要的一张表。任何一项没对齐,你测出来的差异都可能不是内核的差异。

约束项统一取值不对齐会有什么后果
主频168MHz,Flash 5WS,ART 全开影响所有绝对数值,可造成 30% 以上偏差
编译工具链arm-none-eabi-gcc 13.2,-O2 -flto -fno-common不同优化等级可让切换耗时差 50%
Keil 复核AC6 (LLVM),同等级优化RTX5 用 GCC 编性能会明显劣化
节拍频率统一 1000Hz,tickless 关闭10kHz 节拍会额外吃掉 3%~8% CPU
NVIC 优先级分组NVIC_PRIORITYGROUP_4(4 位全抢占)FreeRTOS 会误入断言或临界区形同虚设
堆分配器静态分配优先,动态仅用于创建期分配器算法差异会污染切换数据
任务栈统一 512 字节,configCHECK_FOR_STACK_OVERFLOW=2栈溢出导致行为异常,数据不可信
空闲任务钩子全部为空,不做任何事有钩子里跑 GPIO 或喂狗会拉高 CPU 占用
调试状态全部脱机运行,结果经串口输出在线测量数据失真,且不可复现

特别说一下优先级分组。Cortex-M 的 NVIC 有 8 位优先级寄存器,但实际实现位数不同(STM32F4 是 4 位),分组方式决定了这 4 位里几位是抢占优先级、几位是子优先级。FreeRTOS 的 Cortex-M 移植要求全部 4 位都作为抢占优先级,也就是必须调用NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4)。如果工程里是NVIC_PRIORITYGROUP_2,那么你设置的configMAX_SYSCALL_INTERRUPT_PRIORITY就不会按预期生效,高优先级中断可以打断内核临界区——这时候测出来的"切换时间"是完全无意义的,因为你的临界区根本没在保护。

3. 八款 RTOS 的移植要点与配置差异

移植这件事本身就能筛掉一批方案。同样是 Cortex-M4,有的 RTOS 只需要两个文件加一个汇编端口,有的需要一整套 Kconfig 和 Devicetree。这一节重点讲配置层面最容易做错的几个地方。

3.1 FreeRTOS 与 RT-Thread Nano:改配置就能改结论的典型

FreeRTOS 的FreeRTOSConfig.h是性能结论的命门。同一个内核,配置不同,切

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询