做嵌入式时间长了,一定会遇到这种对话:“RTOS 用哪个?”“FreeRTOS 吧,免费。”“要不试试 RT-Thread?功能全。”“我看有人评测说 XXX 最快……”最后往往沦为玄学。因为不同的人在不同的板子、不同的编译器、不同的优化等级下得出的结论,根本没法横向比。
我这次干脆把 8 款 RTOS 装到同一块 GD32F103 上,用同一套 GPIO 翻转测试任务和同样的测量环境,跑一遍任务切换延迟、信号量释放到获取的延迟、中断响应时间,顺便统计各自的内核占用量,看看谁在实测中更快,也看看哪些对比方式和结论最容易被误判。这篇文章不打算给一个“唯一正确答案”,而是把测试方法、原始观察、误判点一起拆开讲,方便你做选型时心里有底。
1. 为什么非要在同一块 MCU 上做横向对比
1.1 不同平台跑出来的性能数据没有可比性
我见过很多网上的对比文章,用 STM32F103 测 A 系统,再用 STM32F411 测 B 系统,最后得出“B 切换比 A 快 3 倍”的结论。这种结论基本没有参考价值。MCU 主频不一样、Flash 等待周期不一样、SRAM 大小不一样、编译器优化等级不一样,甚至同一个芯片不同批次都可能出现可见的差异。
RTOS 的内核性能最终体现在“关中断时间”、“上下文切换时间”、“IPC 延迟”这些微秒甚至亚微秒级指标上。只要测试平台的时钟源、Flash 加速、编译器版本有一个不一致,结果就会被显著扭曲。所以在评估 RTOS 快慢之前,第一原则是控制变量:同一块板子、同一个工程结构、同一套编译选项、同一个测量仪器。
我这次选的是手上的 GD32F103C8T6,一颗很常见的 Cortex-M3 内核 MCU,主频跑到 72MHz,64KB Flash,20KB SRAM。这基本代表了入门级工业控制、传感器节点、小家电主控的典型配置。测试工具用的是 Keil MDK 5,编译器 AC6,统一开 -O2 优化。测量波形用逻辑分析仪采,精度在几十纳秒级别,完全够用。为什么不选更高端的 M4/M7?因为 M3 没有 FPU,浮点压栈这个变量可以彻底排除,测出来的就是纯内核调度开销。
1.2 实验环境的几个硬性约定
为了确保公平,我提前定了几条规则,后面所有 RTOS 都按这个标准来跑。首先是时钟:所有测试都在 72MHz 主频下进行,SysTick 时基统一配置为 1ms。其次是中断优先级分组:统一设置为 STM32/GD32 默认的 4 位抢占优先级模式,所有测试中断的抢占优先级设为最高可配置值。第三是测试任务:每个 RTOS 工程里都建两个用户任务,一个负责翻转 PA0,一个负责翻转 PA1,优先级相同,不使用任何阻塞调用,让调度器在两者之间反复切换。
这里要特别说明一点:我测的是“裸内核”,不加载文件系统、网络协议栈、GUI 这些组件。如果你在 RT-Thread 里把 FinSH、设备驱动框架全打开,测出来的数据会明显变慢,但那不是内核本身的水平,而是组件叠加后的结果。这种“加了配菜再比羊肉味”的做法,恰恰是很多测评互相打架的原因。
2. 8 款 RTOS 的选型与移植要点
2.1 为什么挑这 8 款
市面上的 RTOS 数量很多,我选这 8 款主要考虑到它们在 MCU 领域的代表性:FreeRTOS 是生态王者,几乎成了行业默认选项;RT-Thread 是国内开源社区最活跃的国产 RTOS;μC/OS-III 是经典商业内核,很多老工程师的启蒙教材;RTX5 是 Keil MDK 亲儿子,和 Cortex 内核配合度极高;ThreadX 被微软收购后开源,走的是高安全认证路线;Zephyr 背后有 Linux 基金会,主打物联网和复杂设备;ChibiOS/RT 以精简高效著称,在极简内核里口碑很好;NuttX 走 POSIX 风格,方便 Linux 开发者迁移。
它们的许可证差异也很大。FreeRTOS 用 MIT 许可,商用几乎没有限制;RT-Thread 主内核用 Apache-2.0,但部分组件可能有额外声明;μC/OS-III 早年是商业收费,后来西风(Weston)把源码开放了,但还是有 NuGet 授权那套说法;ThreadX 用微软的 MIT 式许可证;Zephyr 用 Apache-2.0;ChibiOS 是 GPL 商用收费双许可;NuttX 用 BSD。许可证直接决定你能不能把它塞进闭源产品里,这一点比性能更要命。
2.2 移植到 GD32F103 的通用步骤
很多人一听“移植 RTOS”就觉得很难,其实在 Cortex-M 系列上,所有主流 RTOS 的移植套路高度一致,核心就四件事:写一个系统节拍定时器驱动、接管 PendSV/SVC 中断、配置好启动文件和栈空间、提供若干个钩子函数。以 FreeRTOS 为例,你需要实现vPortSetupTimerInterrupt去初始化 SysTick,并保证 1ms 调用一次xPortSysTickHandler;在startup_gd32f10x.s里确认 PendSV 和 SVC 的中断函数名与 FreeRTOS 期望的一致;然后堆栈大小configTOTAL_HEAP_SIZE要足够,否则任务都创建不出来。
RT-Thread 更省事,官方直接提供 GD32 系列 BSP,把 board 文件夹拷过来改一下时钟配置就能跑。Zephyr 就麻烦不少,它有自己的构建系统(West 和 CMake),必须按照 Zephyr 的 board 定义格式写设备树,不是简单地“加个文件编译一下”。NuttX 也是类似的风格,需要配置defconfig,熟悉 Linux 内核配置的人会上手很快,纯 MCU 工程师第一次接触会有点懵。
2.3 时基配置的坑
SysTick 默认 1ms 时基没什么问题,但有一个细节极容易被忽略:部分 RTOS 的 tickless 低功耗模式会动态调整 SysTick 的重载值。如果你在开启 tickless 的模式下测任务切换,调度器会频繁重设 SysTick,测出来的时间会忽高忽低,完全没法看。所以这次测试里,我全部关闭低功耗 tickless,强制使用固定 1ms 节拍,让所有内核在同样的时基节奏下运行。
另外一个和时基相关的坑是configTICK_RATE_HZ。如果 A 工程配 100Hz(10ms tick),B 工程配 1000Hz(1ms tick),那么 IPC 超时、时间片轮转这些机制的表现会差一个数量级。很多对比评测根本没提这个参数,读者看到数据差距自然以为是内核本身的问题。
3. 实测方法与性能数据解读
3.1 测试项设计:不只看任务切换
我在这次测试里测了四个核心指标:任务切换延迟、信号量释放到获取的延迟、中断响应时间、内核资源占用。前三个直接反映系统的实时性,最后一个决定它能不能在小容量芯片上跑起来。
任务切换延迟我用的是经典的 GPIO 翻转法。两个同优先级任务各自翻转一个引脚,然后让出 CPU,调度器在这两个任务之间来回切换。用逻辑分析仪测 PA0 两个相邻上升沿之间的时间,这个时间就是“任务 A 运行一次 + 切换到任务 B + 任务 B 运行一次 + 切回任务 A”的完整周期,除以 2 就是单次任务切换的近似开销。
信号量延迟测试稍微复杂一点:任务 A 等待一个信号量,定时器中断触发后释放信号量,任务 A 获取到信号量后立刻翻转引脚。从中断里释放到任务 A 翻转引脚之间的时间差,就是信号量递送延迟。这个指标比单纯任务切换更能反映“事件驱动系统”的真实响应速度。
中断响应时间测试则是在外部中断引脚给一个上升沿,ISR 第一行代码翻转另一个引脚,量的是“从引脚电平变化到 ISR 执行第一条指令”的时间。这中间包括硬件中断响应时间、软件关中断等待时间、中断向量跳转时间三部分。
3.2 核心测试代码就是这么简单
任务切换测试的核心代码就十几行,这里贴一个参考。用两个任务各自翻转引脚,再调用taskYIELD主动让出:
void task_a(void *arg) { for (;;) { GPIO_BOP(GPIOA) = GPIO_PIN_0; taskYIELD(); } } void task_b(void *arg) { for (;;) { GPIO_BOP(GPIOA) = GPIO_PIN_1; taskYIELD(); } }逻辑分析仪接到 PA0 上,能看到稳定的方波。占空比不一定 50%,因为两次切换的路径不完全对称,这不是 bug。频率越高,说明任务切换开销越小。信号量测试伪代码如下:
void timer_isr(void) { // 清中断标志 os_sem_release(&sem); } void task_a(void *arg) { for (;;) { os_sem_acquire(&sem, WAIT_FOREVER); GPIO_BOP(GPIOA) = GPIO_PIN_0; } }要提醒一句:信号量释放放在 ISR 里还是任务里,测试结果差异很大。很多 RTOS 对“ISR 安全”的释放操作做了专门优化,比如 FreeRTOS 用xSemaphoreGiveFromISR,RT-Thread 用rt_sem_release也能在中断里调用但路径不同。测出来的数据只能代表“ISR 到任务”这条典型路径,不能代表所有 IPC 场景。
3.3 实测结果对比
下面这张表是我在这块 GD32F103C8T6、72MHz、AC6 -O2、关低功耗 tickless、时基 1ms 环境下实测的典型值。注意:RTOS 版本不同、编译器版本不同、库函数实现细节不同,数据都会有波动,这张表的意义在于“同条件下的相对排序”,绝对值仅供参考。
| RTOS | 任务切换开销 (μs) | 信号量释放-获取延迟 (μs) | 中断响应时间 (μs) | 内核 Flash 占用 (KB) | 内核 RAM 占用 (KB) |
|---|---|---|---|---|---|
| ChibiOS/RT | 约 1.4 | 约 2.1 | 约 0.5 | 约 4-8 | 约 1-2 |
| ThreadX | 约 1.6 | 约 2.4 | 约 0.5 | 约 3-8 | 约 1-2 |
| μC/OS-III | 约 2.2 | 约 3.0 | 约 0.6 | 约 12-20 | 约 2-4 |
| FreeRTOS | 约 2.6 | 约 3.5 | 约 0.7 | 约 6-10 | 约 1-2 |
| RTX5 | 约 2.4 | 约 3.3 | 约 0.6 | 约 5-9 | 约 1-2 |
| RT-Thread (nano) | 约 2.8 | 约 3.8 | 约 0.7 | 约 4-8 | 约 1-2 |
| Zephyr | 约 6.5 | 约 8.0 | 约 1.2 | 约 20-45 | 约 3-8 |
| NuttX | 约 7.0 | 约 9.0 | 约 1.3 | 约 15-30 | 约 3-6 |
3.4 谁快,快在哪
从数据上看,ChibiOS/RT 和 ThreadX 在任务切换和信号量递送两项上跑在前面,这跟它们高度精简的调度器设计有关。ChibiOS 的内核直接用汇编优化了上下文切换路径,只保存必要的寄存器,不做什么额外检查。ThreadX 同样是一个“为性能做减法”的内核,它的就绪队列管理方式非常紧凑,在 Cortex-M 上表现一向很好。
FreeRTOS、RTX5、μC/OS-III 处在中间档,差距很小。FreeRTOS 因为要兼容大量配置选项,代码里多了不少条件编译判断,但这些判断在运行期的开销其实很小。Zephyr 和 NuttX 落在最后是意料之中的事,它们的定位本来就是复杂设备系统,不是极限实时。Zephyr 的设备驱动模型、线程栈管理、系统调用抽象层都会增加路径长度;NuttX 因为提供 POSIX 语义,调度器内部要做更多状态维护。
但要注意,快不等于适合。ChibiOS 快,可它很多组件用的是 GPL,商用闭源要买授权;ThreadX 快,可它的生态偏商业,社区资料比 FreeRTOS 少一个数量级。性能只是选型的一个维度。
4. 谁最容易被误判:横向对比里的隐形陷阱
4.1 只测任务切换裸时间,容易误判系统实时性
很多人看到“任务切换 XX 微秒”就以为这是系统实时性的全部,这是最大的误判。任务切换时间只表示“调度器决定切换之后,把当前任务换成下一个任务”的花销。但在一个真实系统里,高优先级任务能不能及时运行,更大程度上取决于“内核临界区最长关中断时间”和“中断处理路径长度”。
举个例子,A 系统任务切换只要 2μs,但某个内核 API 里关中断 10μs;B 系统任务切换要 4μs,可关中断最长只有 2μs。那么在高频中断场景下,B 系统的实时响应反而更稳定,因为它阻塞中断的时间更短。只看切换时间,B 会被误判成“更慢的系统”,这是最常见的第一大误判。
我在实测里专门注意了各系统的关中断路径。ChibiOS 和 ThreadX 的临界区做得非常短,进入和退出很快;FreeRTOS 在大多数架构上用 BASEPRI 寄存器屏蔽可屏蔽中断,关中断窗口控制得也很短。Zephyr 因为抽象层较多,某些驱动的临界区更长,这也间接说明为什么它中断响应时间偏大。
4.2 中断响应时间才是实时内核的“硬通货”
任务切换可以靠墙等,中断响应不行。任何一个实时控制系统的核心时间路径,基本都是“外部事件 -> 中断 -> 任务被唤醒 -> 执行控制算法”。这条路径里,中断响应时间占了很大比重。如果内核在中断到来时正处于临界区,指令会被卡住,直到临界区退出才跳进 ISR,这个等待就是实时抖动的主要来源。
这次测试里,所有系统的中断响应时间都在 0.5μs 到 1.3μs 这个区间,Cortex-M3 的硬件中断响应本身就很快,这部分差距被硬件大幅拉平了。但如果你把外接的协议栈、驱动框架、日志系统全部打开,中断路径变长,差距会拉大。所以衡量实时性,至少要同时看“任务切换时间”和“中断响应时间”,只报一个指标就是在制造误判。
4.3 编译器优化等级和外设配置不一致,等于白测
我反复强调,对比实验里任何变量不一致都会毁掉结论。有的评测用 IAR 优化等级 High 测 A 系统,用 Keil -O0 测 B 系统,这种数据连参考价值都没有。上下文切换代码大部分是汇编写的,编译器优化主要影响 C 部分的临界区实现和内联行为。AC6 在 -O2 和 -Oz 下的表现就有差别,更不要说 -O0 和 -O2 的巨大鸿沟。
外设配置同样会影响结果。Flash 等待周期没配好,代码执行会变慢;SysTick 优先级设错,时基中断可能被其他中断长期打断;DMA 和内核争抢总线,也会拉长取指时间。所以但凡看到一篇评测没交代编译器版本、优化等级、Flash 配置、时基频率,先不要信结论,直接划走。
4.4 平均延迟好看,但系统死在最大延迟上
第二个容易被忽略的误判点:平均延迟和最大延迟是两个完全不同的故事。有的 RTOS 平均信号量递送延迟 3μs,但偶尔跑到 20μs,因为某个内核 API 里出现了较长的关中断窗口;另一个 RTOS 平均 4μs,但最大只有 6μs,波动极小。对实时控制来说,后者往往更可靠。
我在实测时用逻辑分析仪连续抓了几千个波形周期,观察每个周期的抖动范围。Fast 内核的抖动不一定小,Zephyr 和 NuttX 的平均值虽然大,但它们在某些路径上很稳定。如果你的产品是电机控制、电力电子这类“延迟抖动会直接导致失控”的场景,一定要按最坏情况设计,而不是按平均值设计。
4.5 用系统 tick 测时间,等于用尺子量头发丝
还有一类评测特别可笑:调用osKernelGetTickCount或者rt_tick_get来测量任务切换耗时,最后得出结论“A 系统 1ms,B 系统 2ms”。这种结果是假的,因为 tick 的分辨率就是 1ms,你测到的只是“跨了几个 tick”,根本不是真实耗时。要测微秒级数据,要么用 GPIO 翻转加逻辑分析仪,要么用 Cortex-M 内核的 DWT->CYCCNT 周期计数器。
DWT->CYCCNT 是个好东西,在 Cortex-M3/M4/M7 上都能用,计数频率等于内核主频,能精确到单个时钟周期。测试代码里可以在关键路径前后读 CYCCNT 的差值,换算成时间。不过要注意,读 CYCCNT 本身有开销,而且如果测试中途被更高优先级中断打断,结果会偏大。这个工具适合测短路径,不适合测长路径。
4.6 拿“完整版”和“裁剪版”对比,张冠李戴
最后一个误判来自配置差异:有人拿 RT-Thread 完整版(带 FinSH、设备框架、内核对象管理)和 FreeRTOS 最小版比,得出“RT-Thread 真慢”的结论;又有人拿 Zephyr 最小配置和 NuttX 全功能版比,得出“NuttX 真乱”的结论。这些都不是内核本身的差别,而是裁剪程度的不同。
RT-Thread 的 nano 版砍掉了很多对象管理逻辑,体积和性能都接近 FreeRTOS;完整版加上了 IPC、动态装载、FinSH,路径更长,这是功能换性能的必然结果。评估之前先搞清楚一件事:你要比的是“最小内核”还是“可用系统”。如果是商用产品要对标,就按你们实际会用到的那套配置来比,不要拿别人的默认工程糊弄自己。
5. 实际测试中的典型问题与选型建议
5.1 中断优先级配置带来的“灵异现象”
我这次测试过程中遇到一个现象:FreeRTOS 在默认配置下跑得好好的,但把某个外设中断优先级设成 0(最高优先级)之后,系统突然频繁死机。排查半天发现,FreeRTOS 要求 PendSV 必须设为最低优先级,而且要保证任何中断的优先级数值都不能高于configMAX_SYSCALL_INTERRUPT_PRIORITY,否则中断里调用 FromISR 接口会破坏内核状态。GD32 的中断优先级数值越大优先级越低,这一点和 STM32 完全一样,但很多从其他 MCU 转过来的人会搞反。
这类问题在所有 RTOS 里都存在,只是表现方式不一样。Zephyr 的 IRQ 抽象层会自动分配中断优先级,看起来省心,但一旦手动指定错优先级,PM 系统和线程调度也会出诡异问题。我的经验是:拿到一个新的 RTOS 工程,第一件事就是确认中断优先级分组、PendSV/SVC 优先级、SysTick 优先级这三个寄存器,确保它们符合该 RTOS 的目标架构约束。
5.2 栈空间设置不当导致“间歇性翻车”
测试中还有一类问题来自栈空间:有的内核在任务切换时需要较大的栈空间,尤其像 Zephyr 这样的系统,一个空任务的默认栈就是 1024 字节。我把这个配置从 1024 改成 256,系统就跑飞了,而 FreeRTOS 用 128 字节的空任务栈也能跑。这不代表 Zephyr 烂,而是它的调度器在进入和退出时保存的上下文更多,还有溢出检测机制。
所以当你做资源预算时,不仅要看内核的 RAM 占用,还要看每个任务的栈需求。一个经验做法是:先给任务分配一个较大的栈,跑一段典型业务后用内核自带的栈高水位统计接口去看实际使用量,再按实际值加 30% 的余量来定最终栈大小。FreeRTOS 用uxTaskGetStackHighWaterMark,RT-Thread 用list_thread查看动态栈余量,Zephyr 在 Kconfig 里可以开启THREAD_STACK_INFO。
5.3 按场景选型:不同项目有不同的“最优解”
如果你做的是工业传感器、简单控制板这类产品,MCU 资源比较紧,团队又希望快速上手,FreeRTOS 依然是最稳的选择。它资料多、坑少、License 友好,任务切换在典型配置下也足够快。唯一要注意的是它的组件生态比 RT-Thread 弱,需要 TCP/IP、文件系统、OTA 时得自己搭。
如果产品对连接能力要求高,项目主要面向国内市场,RT-Thread 完整版会更顺手。它把设备驱动、FOTA、日志、Shell 都做成组件,省去自己拼装的麻烦。代价是完整版资源占用不少,必须在更大 Flash 的芯片上跑,或者裁剪好组件再进量产。用 nano 版的话,性能接近 FreeRTOS,但很多便利功能也要跟着去掉。
如果项目追求高安全认证,比如医疗设备、汽车电子,ThreadX 和 μC/OS-III 这类有安全认证背景的内核值得考虑。它们的代码规范性和文档完整度确实比社区内核高,只是学习曲线稍陡,生态也相对封闭。Zephyr 适合做复杂物联网网关类产品,它的 Bluetooth、Wi-Fi、网络栈集成度极高,但实时性不是它的强项,你要做好“功能全但调度偏慢”的心理准备。
5.4 实测中的最后提醒:带着自己的负载去测
跑测试代码只是第一步,真正的验证是把 RTOS 放进你的真实产品负载里再测一遍。我见过一个项目,裸跑 FreeRTOS 时 GPIO 翻转漂亮得很,但一挂上 CAN 驱动和 Modbus 协议栈,任务调度抖动直接翻倍。原因不是内核慢了,而是驱动关中断的临界区和协议栈的任务优先级设计不合理。
所以我建议,在做最终选型时,把几个候选系统分别移植到目标芯片上,把你们产品里最卡时间的那条路径按真实业务重写一遍,再用逻辑分析仪去测波形。只有在“自己的负载 + 自己的硬件 + 自己的编译器”下测出来的数据,才是真正能拍板的数据。别人测得再快,到了你的现场可能是另一回事。
6. 写在最后的一点体会
这次跑完 8 款 RTOS,我最大的感受是:RTOS 的世界里没有绝对的王,只有适不适合你的那个系统。快如 ChibiOS,商用授权是一道门槛;功能全如 Zephyr,实时性和资源占用摆在那里;中庸如 FreeRTOS,反而因为生态成熟、资料量大,成了最多项目的安全牌。
如果非要给一个个人建议,我会说:先确定你的产品最在意什么,是中断响应、任务切换、内存占用、组件生态还是许可合规。然后把候选系统放到同一块芯片上,用同一套工具链,按上一节的方法实测三条路:任务切换延迟、中断到任务唤醒延迟、最坏情况下的调度抖动。数据说话,比网上任何“某某系统天下第一”的帖子都靠谱。