拓冰建站拓冰建站
首页 / 资讯中心 / 正文

8款RTOS在GD32F103C8T6上的任务切换与中断延迟实测

手上这块 GD32F103C8T6 的最小系统板过去大半年被我反复擦写了不下三十次。原因很简单做 MCU 项目的人迟早会卡在同一个问题上手上这个只有 64KB Flash、20KB SRAM、72MHz 主频的小片子到底该塞进哪一套 RTOS。网上的评测文章一抓一大把每篇都说自己测的那套快可当你把 FreeRTOS 换到 RT-Thread、再把 RT-Thread 换到 Zephyr实测出来的数字经常对不上——不是内核变了是测量方法变了。所以我干脆把 8 款主流 RTOS 全部搬到同一块板子上用同一套时钟、同一个编译器、同一段测量代码跑了一遍。这篇文章不讲哪个 RTOS 最好这种没有答案的问题只讲两件事同一块 MCU 上它们真实的性能排序是什么样以及哪些因素会让你把快的看成慢的、把慢的看成快的。MCU、RTOS 这两组关键词背后的选型逻辑我会尽量拆到寄存器级别。如果你正在做 RTOS 选型、准备移植或者只是被面试题里的任务切换要多少周期问住过下面的内容应该能直接用上。1. 被测平台与 8 款 RTOS 名单为什么要死磕同一块 MCU1.1 一块 GD32F103C8T6 为什么能当标尺选测试平台的时候我纠结过很久是上 STM32F407 还是留在 F103 这一档。最后落回 GD32F103C8T6理由有三条而且每一条都直接关系到数据能不能横向比。第一条是它的内核足够干净。Cortex-M3 没有指令缓存没有数据缓存也没有分支预测器带来的复杂度代码执行时间基本等于周期数除以主频。这一点非常关键——如果你在一颗带 Cache 的 Cortex-M7 上做同类测试同一段代码第一次跑和第二次跑的时间能差出两三倍测出来的切换时间其实是 Cache 命中率的函数跟 RTOS 本身关系不大。M3 没这个问题测出来 1.6 微秒就是 1.6 微秒重复性极好。第二条是它的主频和 Flash 特性。72MHz 主频下片内 Flash 通常需要插入 2 个等待周期而访问 SRAM 是 0 等待。这个差异会直接影响一个 RTOS 的实测表现因为它决定了内核热路径的代码放在哪、分支跳转会不会被等待周期拖住。这一点后面第 5 章会专门讲很多人测出来的差异本质上是 Flash 等待周期在搞鬼跟调度算法没半点关系。第三条是它的资源紧张程度刚好合适。64KB Flash、20KB SRAM这个容量能把大部分 RTOS 的默认配置逼出原形NuttX 不裁剪根本烧不进去Zephyr 默认配置能吃掉三分之一 Flash而 TencentOS-tiny 这类轻量内核反而显得从容。这种压力测试比在 1MB Flash 的板子上跑要有信息量得多因为真实项目里没人会给你 1MB 去放一个 RTOS。这里补一句关于 Flash 访问的基础知识因为它跟后面的性能数据强相关。Cortex-M3 内核通过 AHB-Lite 总线矩阵去访问片内 Flash中间隔着一个 Flash 控制器也叫闪存加速器或预取缓冲单元。内核发出取指请求后控制器负责从存储阵列读出数据并返回等待周期就是在说内核等几个 HCLK 周期才能拿到数据。72MHz 下插入 2 个等待周期意味着一次非连续的取指最坏要等 3 个周期。RTOS 内核的临界区代码、PendSV 处理函数、就绪表查找循环全都是密集的小跳转等待周期的影响会被放大。所以不同 RTOS 在这块板子上差 0.5 微秒很可能只是编译后代码布局不一样。1.2 8 款参赛选手的版本与配置为了不让版本差异污染结果我把 8 款 RTOS 都锁在当时的稳定版上并且统一采用最小可运行工程的配置思路两个用户任务加一个二值信号量一个 1ms 的 SysTick不用动态内存的除堆之外的部分尽量关掉与内核无关的组件。编号RTOS版本配置取向备注1FreeRTOS10.4.6内核 heap_4关闭 trace 和统计Cortex-M3 官方移植层2RT-Thread Nano3.1.5纯内核无设备框架只保留调度与 IPC3RT-Thread 完整版4.1.x含设备框架与 MSH 控制台关闭文件系统与网络4uC/OS-III3.08内核 事件标志关闭统计任务官方 M3 移植5TencentOS-tiny2.6.x最小内核配置关闭所有组件6Zephyr3.x LTS先跑默认配置再跑裁剪配置两套数据分开记录7NuttX12.x最小 defconfig需要大幅裁剪8LiteOS-M1.1.x最小内核关闭 shell 与组件这里我要特别说明 Zephyr 和 NuttX 为什么记了两套数据。这两款严格来说不是RTOS 内核而是带完整驱动模型、设备树、电源管理框架的嵌入式系统。它们默认配置下的代码量远超其他六款如果只看默认配置你会得出Zephyr 比 FreeRTOS 慢三倍的结论但只要把无关组件关掉内核热路径的差距会迅速缩小到 30% 以内。这就是标题里说的容易被误判最典型的一类。1.3 我给自己定的三条评测纪律做横向对比最容易犯的错误是变量没控制住。为了让数据站得住我给自己立了三条规矩也建议任何做类似测试的人照抄。第一条所有工程使用同一个工具链版本、同一个优化等级、同一套启动文件和链接脚本。工具链用的是 ARM GCC优化等级统一 -O2并且额外做了一组 -Os 的对照。为什么不用 -O0因为 -O0 下函数调用不做内联连 GPIO 直写都会被展开成多层访问测出来的时间几乎是 -O2 的两倍完全没有参考价值。真实项目里没人用 -O0 出货。第二条测量探针必须用寄存器直写不能用库函数。这一点听起来像废话但真的有很多人用 HAL_GPIO_WritePin 去翻转测量引脚然后抱怨 RTOS 慢。HAL 库的一次写操作里可能包含参数检查、句柄结构体解引用、位带计算几十个周期就没了而你测的切换时间本来也就一百多个周期。GD32 上我直接用 GPIO_BOP 和 GPIO_BC 寄存器STM32 上对应的就是 BSRR 和 BRR效果等价都是一个总线周期完成。/* GD32F103 上的测量探针单周期级别开销 */ #define PROBE_HIGH() (GPIOA-BOP (uint32_t)GPIO_PIN_0) #define PROBE_LOW() (GPIOA-BC (uint32_t)GPIO_PIN_0) /* STM32F103 上的等价写法 */ #define PROBE_HIGH() (GPIOA-BSRR GPIO_Pin_0) #define PROBE_LOW() (GPIOA-BRR GPIO_Pin_0)第三条每组数据至少测 200 次取中位数而不是平均值。为什么强调中位数因为示波器抓到的样本里总会有几个异常大的值可能来自 SysTick 恰好在测量窗口内触发也可能来自调试器后台访问总线。平均值会被这几个异常值拉偏中位数更接近你实际能感受到的稳定值。同时我会记录最大值和最小值如果最大值比中位数大出 50% 以上说明测量方法本身有问题需要回去查。2. 测量方法先立住任务切换、中断延迟、信号量到底怎么测2.1 任务切换时间GPIO 翻转法的最小实现任务切换时间的测法有很多版本网上的数据之所以打起来八成是因为大家测的根本不是同一个东西。我先说清楚我测的是哪一种再解释其他测法为什么数值不同。我测的是同优先级任务间的主动让出加调度也就是最常见的 yield 路径。做法是这样创建两个优先级相同的任务 A 和 BA 在自己的循环里先拉高探针引脚然后调用一次主动让出B 紧接着被调度运行第一件事就是拉低探针引脚。示波器上测高电平的宽度这个宽度约等于主动让出 API 的内部开销 PendSV 异常进入 上下文保存 调度器选任务 上下文恢复 PendSV 异常返回。这是 RTOS 切换成本里最纯粹的一段不含任何 IPC 语义。/* 任务 A */ void task_a(void *p) { for (;;) { PROBE_HIGH(); os_thread_yield(); /* 主动让出触发 PendSV */ } } /* 任务 B与 A 同优先级 */ void task_b(void *p) { for (;;) { PROBE_LOW(); os_thread_yield(); } }另一种常见测法是阻塞唤醒法任务 A 阻塞在一个信号量上任务 B 释放该信号量A 被唤醒后翻转引脚。这种方法测出来的时间明显更长因为它包含了信号量释放时的就绪表操作、可能的优先级继承判断、以及唤醒后的重新调度。两种测法差了 1.4 微秒左右如果你不清楚对方用的是哪种看到数字就直接比较是没意义的。还有第三种测法是中断触发切换用一个定时器中断去释放信号量测中断入口到任务恢复的时间。这个数最大因为它把中断延迟也算进去了。三种测法的数值关系大致是 1 : 1.9 : 2.5这个比例比绝对值更有参考价值。注意测任务切换时必须把探针代码放在切换 API 的外面绝对不要放在 RTOS 的钩子函数里。有些人在任务切换钩子里翻转引脚结果测量的同时引入了钩子本身的调用开销和栈操作数据会整体偏大而且因为钩子里不能随便调外设容易触发断言。2.2 中断延迟从触发到第一行代码的完整链路中断延迟的测量比任务切换更考验方法。我用的方案是一个 GPIO 配置为外部中断输入用信号发生器给它一个干净的方波然后在中断服务函数的第一行立刻拉高另一个探针引脚。示波器同时抓输入方波和探针输出两个上升沿之间的时间就是完整的触发到 ISR 第一行延迟。这个延迟由几段构成拆开看才有意义。前半段是硬件和内核级的固定开销GPIO 输入同步需要 2 到 3 个周期EXTI 边沿检测和 NVIC 挂起判定几个周期然后是内核的异常进入序列压栈 8 个寄存器、取向量、跳转到处理函数这一整套在 Cortex-M3 上大约是 12 个周期。72MHz 下就是 170 纳秒左右这部分跟你用什么 RTOS 关系不大。后半段才是 RTOS 引入的差异。RTOS 通常会用自己的中断入口包装函数替换默认向量包装函数里要做的是在栈上额外保存被调用者可能破坏的寄存器、递增嵌套计数、必要时切换栈指针、判断是否需要触发 PendSV。这一段做得粗糙的内核会多花几十个周期。我实测下来最紧凑的包装约 0.6 微秒最臃肿的在 0.9 微秒差距接近 50%。这里还有一个非常容易被忽略的点中断优先级的配置。Cortex-M3 的 NVIC 支持优先级分组如果你在初始化时没有正确设置优先级分组那么抢占优先级和子优先级的位数分配就是错的某些看似高优先级的中断实际上抢不动别人。很多人测出来中断延迟不稳定一会 0.6 微秒一会 3 微秒原因多半是 SysTick 或者某个定时器中断恰好在测量窗口里执行了临界区把外部中断挡住了。注意测量中断延迟时务必把 SysTick 的优先级设成所有中断里最低的。默认情况下很多移植层把 SysTick 设成最低但也有内核把它设在中间这时候 SysTick 处理函数会在你测外部中断时插队进来导致数据抖动明显。2.3 内存与启动时间不要只看 map 文件Flash 和 RAM 的占用我不看 map 文件的总和而是用一个更笨但更可靠的办法把工程编译成最小可运行版本然后用工具链的 size 命令读取 text、data、bss 三段并且把堆的大小单独列出来。为什么不能只看 map因为 map 文件里的段会包含库函数、会包含未被裁剪掉的死代码、还会因为链接顺序不同而对齐填充出不同的结果。你看到Flash 占用 8KB里面有 1KB 是填充实际代码可能只有 7KB。而用 size 命令配合段统计能把这些对齐开销看清楚。更重要的是堆的大小是运行时配置项它会进 bss如果堆开得大bss 就大但这不是 RTOS 内核本身的消耗必须单独说明。启动时间的测法我用了另一种思路在主函数入口拉高探针在第一个用户任务的第一行拉低探针。中间这段包含了 RTOS 的初始化比如就绪表清零、空闲任务创建、时钟节拍配置、堆初始化、以及某些内核的组件自动初始化。Zephyr 和 NuttX 的启动时间明显更长就是因为它们有一整套启动阶段的组件遍历流程这一块在真实项目里如果不需要是可以裁掉的。注意启动时间测量里有一大块是 BSS 段清零和 DATA 段搬运这部分跟你 RTOS 选谁无关只跟你的 RAM 用量和链接脚本有关。所以我在报告数据时会扣掉这一段只算RTOS 自身初始化的时间。3. 实测数据盘点8 款 RTOS 在同一块板子上的表现3.1 内核操作耗时对比先说结论在这块 72MHz 的 M3 上8 款 RTOS 的纯上下文切换时间落在 1.5 到 3.1 微秒区间也就是大约 108 到 223 个时钟周期。这个量级跟很多人想象的几百个周期相比偏低原因是 Cortex-M3 的硬件压栈机制帮了大忙——异常进入时内核自动压 8 个寄存器软件只需要再保存必要的几个省掉了一大半工作量。RTOS纯上下文切换信号量释放到被唤醒任务运行互斥量加锁解锁中断入口包装开销FreeRTOS1.6 us3.0 us2.2 us0.62 usRT-Thread Nano1.9 us3.4 us2.5 us0.66 usRT-Thread 完整版2.2 us4.1 us3.0 us0.72 usuC/OS-III1.5 us2.7 us2.1 us0.60 usTencentOS-tiny1.8 us3.2 us2.4 us0.64 usZephyr 默认配置2.4 us4.8 us3.4 us0.71 usZephyr 裁剪后1.9 us3.6 us2.6 us0.66 usNuttX 最小配置3.1 us6.3 us4.5 us0.85 usLiteOS-M2.0 us3.7 us2.6 us0.68 us看到这张表第一反应可能是uC/OS-III 最快NuttX 最慢。但请先别急着下结论这张表里有至少三个陷阱。第一个陷阱是绝对值的可信度。这些数字是在 -O2、Flash 2 等待周期、无调试器介入的条件下测的重复性在正负 5% 以内。但换一块 Flash 等待周期是 0 的板子或者把主频降到 24MHz排序可能会变。绝对值只能当量级参考真正有意义的是比例关系——比如 uC/OS-III 的信号量往返比 FreeRTOS 快 10%这个差距在不同平台上大致稳定而 NuttX 比其他几款慢一倍以上这个也是稳定的。第二个陷阱是最短的路径不代表最好的内核。uC/OS-III 的切换路径短一部分原因是它的设计取向就是保守和精简牺牲了一些灵活性。比如它的优先级数量可以配置二进制就绪表查找用硬件指令实现速度极快但如果你把优先级数量从 32 提到 256就绪表查找会多一层差异立刻缩小。所以谁最快这个问题必须附带配置条件。第三个陷阱是测量点。表格里的互斥量加锁解锁包含了优先级继承检查而优先级继承的实现成本差异很大。FreeRTOS 的优先级继承逻辑相对简单uC/OS-III 有一套完整的内核对象管理RT-Thread 的互斥量支持嵌套计数Zephyr 的互斥量还带超时链表管理。你在真实项目里用互斥量保护共享资源测出来的成本差异跟纯切换时间的差异完全是两回事。3.2 Flash 与 RAM 占用的真实差距内存占用这一块差距比时间差距大得多而且是选型时最应该优先考虑的约束。毕竟 64KB Flash 砍掉 20KB 之后剩下的空间要留给你的业务代码、协议栈、日志模块非常紧张。RTOSFlash最小工程静态 RAM默认堆裁剪潜力FreeRTOS6.6 KB0.9 KB4 KB中可关统计与 traceRT-Thread Nano7.2 KB1.0 KB按需高本身就是精简版RT-Thread 完整版21 KB3.5 KB按需高组件可逐个关闭uC/OS-III13 KB2.5 KB无堆概念中对象数量可配置TencentOS-tiny5.8 KB0.8 KB按需高Zephyr 默认配置26 KB6 KB按需极高可裁到 9 KB 左右NuttX 最小配置52 KB12 KB按需中高但基础框架吃掉很多LiteOS-M8.4 KB1.2 KB按需高这张表最能说明误判是怎么产生的。如果只看默认配置这一列Zephyr 和 NuttX 会被直接判死刑——26KB 和 52KB在一颗 64KB Flash 的片子上几乎没给业务代码留空间。但只要动手裁剪Zephyr 能压到 9KB 左右NuttX 也能压到 20KB 以内虽然还是偏大但已经不是完全不能用了。反过来说TencentOS-tiny 和 FreeRTOS 的默认占用小一个原因是它们本身设计就轻另一个原因是它们没有东西可关——你没法从一个纯内核里裁掉更多。这两类内核的对比其实不公平应该在功能对等的前提下比如果你的项目只需要任务调度和信号量那 5.8KB 的 TencentOS-tiny 是真正的优势如果你的项目两年后要加文件系统和网络协议栈那 FreeRTOS 加一堆第三方组件的总账可能比 RT-Thread 完整版还贵。注意统计 Flash 占用时一定要把 printf 相关的库去掉。newlib 或 newlib-nano 的 printf 家族能吃掉 8 到 15KB如果你的工程里任何一个地方调了 printf即使你没用它输出链接器也可能把它拉进来。我测第一轮的时候忘了这个FreeRTOS 的 Flash 占用直接报出 14KB排查了两个小时才发现是一个调试宏没关。3.3 启动时间、tick 抖动与调度确定性启动时间这一项扣掉 BSS 清零和 DATA 搬运之后各家的差距大致是uC/OS-III 和 TencentOS-tiny 在 1 毫秒以内FreeRTOS 和 LiteOS-M 在 1 到 2 毫秒之间RT-Thread Nano 在 2 毫秒左右RT-Thread 完整版因为要初始化设备框架落在 5 到 8 毫秒Zephyr 在 10 毫秒上下NuttX 最慢能到 20 毫秒以上。这个数据在什么场景下重要如果设备是电池供电、要求上电后 20 毫秒内就开始执行关键动作那 NuttX 和 Zephyr 就很不合适。但如果设备上电后有几百毫秒的稳压和自检流程这个差异就完全无所谓了。所以启动时间也是一项高度依赖场景的指标。tick 抖动我用了一个比较土但直观的办法创建一个最高优先级的任务在任务里翻转另一个引脚然后统计翻转周期的抖动。在 1ms tick 配置下各家内核的抖动都在正负 20 微秒以内差异主要来自 SysTick 中断处理函数的长度。这里真正值得关注的不是抖动本身而是tickless 模式是否开启——开启 tickless 之后空闲时 CPU 可以进低功耗模式tick 中断被动态调整功耗下来了但如果你在测量代码里依赖 tick 计数做时间基准数据就会变得很奇怪。调度确定性方面我做了另一组实验创建 8 个不同优先级的任务每个任务里翻转探针用逻辑分析仪抓 30 秒。结果是所有内核都能保证严格按优先级抢占同优先级按时间片轮转。但在极端情况下有差异比如当 8 个任务同时被信号量唤醒时uC/OS-III 和 FreeRTOS 的唤醒顺序完全确定Zephyr 在开启某些调度特性后会有细微差异NuttX 在任务数量多的时候唤醒延迟的抖动明显大于其他几款。这跟它就绪表实现和中断屏蔽策略有关。4. 数据背后的机制拆解快的为什么快慢的为什么慢4.1 调度器与就绪表实现差异RTOS 的性能差异八成出在就绪表的实现上。所谓就绪表就是内核用来记录哪些任务现在可以运行的数据结构。每次调度内核都要从就绪表里挑出优先级最高的任务这个挑选过程的效率直接决定了切换时间。主流的实现分三种。第一种是位图加硬件指令uC/OS-III 和 FreeRTOS 的增强移植版都用这种用几个 32 位变量表示优先级位图查找时用 CLZ 或者 RBIT 加 CLZ 这类单周期指令直接算出最高优先级。这种方式在没有浮点和除法单元的 M3 上极其高效查找过程只有几个周期。第二种是双向链表加位图混合RT-Thread 用的是这种同优先级任务串在一条链表上位图只标记该优先级是否有任务。查找时先用位图定位优先级再从链表头取任务。多了一层链表指针解引用所以比纯位图方案慢一点但支持同优先级多个任务的时间片轮转功能上更完整。第三种是红黑树或者有序链表NuttX 在某些配置下会用到。红黑树的插入删除是 O(log n)在任务数量多的时候性能稳定但常数因子大任务少的时候反而不如前两种。这解释了为什么 NuttX 在 8 个任务以内明显慢它的设计目标是支持几十上百个任务并且行为可预测在小规模场景下是过度设计。这个差异在真实项目里的影响取决于你的任务数量。如果你只有 3 到 5 个任务位图方案和链表方案的实际差异可能只有零点几微秒。如果你有 20 个以上任务并且频繁切换红黑树方案的优势可能会体现出来。所以哪个 RTOS 调度快这个问题必须先给出任务规模。4.2 抽象层的代价对象模型与设备框架RT-Thread 完整版比 Nano 版慢了 0.3 微秒代码大了将近 14KB这个代价换来了什么换来的是设备框架、统一的对象管理、以及可挂载的各种组件。这是典型的抽象层成本。具体来说RT-Thread 完整版里所有的内核对象线程、信号量、互斥量、事件、邮箱都是从一个基类对象派生出来的每个对象头部都有一个类型标识、一个链表节点、一个名字指针。操作对象时要先做类型检查出错时还要调用错误处理函数。这些检查在 RT-Thread Nano 里也是有的但完整版的检查更细日志输出路径更长。这种设计在调试阶段是巨大优势——对象用错了类型内核能立刻告诉你而不是让你去猜一个野指针。但在量产固件里这些检查就是纯开销。RT-Thread 提供了关闭断言和日志的选项关掉之后完整版和 Nano 的切换时间差距会缩小到 0.1 微秒左右。所以当你看到RT-Thread 完整版慢的结论时得先问清楚断言和日志有没有关。Zephyr 的情况类似但更极端。它的设备模型、设备树、初始化调度、电源管理钩子全都是为了大型项目和中长期维护服务的。你在一个 64KB 的片子上用 Zephyr等于把一套为中等规模系统设计的基础设施塞进随身工具包自然显得笨重。但如果换成 512KB Flash 的 M4Zephyr 这些开销立刻变成省下的开发时间性质完全变了。4.3 移植层写得糙不糙直接决定前三微秒这一点是最容易被忽略、但在实测中最容易拉开差距的因素。RTOS 内核本身是 C 语言写的跟架构无关真正决定性能的是那几百行汇编写的移植层PendSV 处理函数、SysTick 处理函数、上下文保存恢复的宏、开关中断的实现。同样是 Cortex-M3不同 RTOS 的移植层写法差异很大。uC/OS-III 的移植层非常紧凑PendSV 里保存的寄存器数量经过精心选择还用了硬件压栈特性所以切换快。FreeRTOS 的移植层同样用硬件压栈但为了兼容更多编译器和配置代码里有一些条件编译分支实际执行路径略长一点。Zephyr 的移植层为了支持对称多处理和其他高级特性结构更复杂热路径上有一些额外的判断。更粗糙的情况是自己移植。GD32F103 移植 RTOS 的时候很多人的做法是直接把 STM32F103 的移植文件改个名字拿来用。这本身没问题因为两者内核相同、外设寄存器高度兼容。但有几个细节必须改系统时钟配置函数要改成 GD32 的库函数或者你自己的寄存器配置SysTick 的时钟源确认以及中断向量表的偏移量。如果这几处没弄对编译能过、能跑起来但切换时间可能莫名其妙多出几十个周期——因为系统主频其实不是你以为的 72MHz而是 8MHz 内部时钟或者 108MHz 超频状态。我在第一轮测试时就吃过这个亏测出来所有 RTOS 都慢了三倍查了半天才发现是没有正确配置 PLLCPU 实际跑在内部 8MHz 上。5. 最容易导致误判的 6 个坑5.1 编译器优化等级与链接时优化优化等级的影响大到什么程度同一份 FreeRTOS 工程-O0 下切换时间 3.4 微秒-O2 下 1.6 微秒-Os 下 1.7 微秒。差了一倍以上。如果一篇评测文章说A 内核比 B 内核快两倍而两者一个用 -O2 一个用 -O0那结论完全是假的。链接时优化是另一个隐藏变量。开启链接时优化之后编译器能看到整个程序的调用图把跨文件的函数内联、把只调用一次的函数直接展开、把不可能走到的分支删掉。RTOS 内核里有很多包装函数比如某个 API 直接调用另一个内部函数链接时优化能把这一层调用完全消除。我实测过FreeRTOS 开启链接时优化后信号量往返时间少了 0.2 微秒。Zephyr 因为代码层次深链接时优化带来的收益更大能到 0.5 微秒以上。注意开启链接时优化之后某些 RTOS 的钩子函数或者调试符号会失效因为函数被内联或者删除了。做性能测试可以开做日常调试建议关掉切换成本太高。5.2 测量代码本身的开销这是最隐蔽的一类误判。你在测量代码里写的每一行都会进入测量结果所以必须把测量代码的开销压到最低并且清楚地知道它有多大。我在第 1 章里提到了用寄存器直写代替库函数这是第一层。第二层是探针代码的位置。如果把探针放在 API 调用之前测的是探针开销 API 调用 切换探针开销会被算进去如果把探针放在 API 调用之后的下一行测的是探针开销 切换的一部分。理想的做法是在两处都放探针用两个通道差分但大部分人的示波器没有那么多通道所以退而求其次保证所有对比工程用的探针代码完全一样这样探针开销就是一个常数偏移不影响相对排序。第三层是编译器可能把你辛辛苦苦写的探针优化掉。如果引脚的状态在后续代码里没有被读取某些激进的优化会认为这次写操作没有副作用——虽然对 volatile 修饰的寄存器不会这样但如果你写的是自己封装的一个函数、参数里没有 volatile就有可能被优化掉。测出来时间看起来很快其实是空转。所以探针必须写成宏直接操作 volatile 指针或者确认寄存器结构体定义里有 volatile 修饰。5.3 Flash 等待周期、时钟树与供电前面提过 Flash 等待周期的影响这里展开说清楚。Cortex-M3 访问片内 Flash 需要等待周期等待周期的数量取决于主频。以常见配置为例24MHz 以下通常 0 等待48MHz 需要 1 个等待周期72MHz 需要 2 个等待周期。如果你在移植时忘了配置 Flash 的等待周期寄存器或者配置成了一个偏保守的值CPU 的取指就会变慢所有的测试数据都会整体偏移。更麻烦的是 GD32F103 和 STM32F103 在这块的推荐值略有差异GD32 的部分型号在 108MHz 下还能稳定运行等待周期配置也不同。如果你照抄了另一块板子的配置可能既没跑在预期的频率也没配对的等待周期数据就没法比。供电也会影响。Cortex-M3 在 2.0V 到 3.6V 都能工作但在低电压下Flash 的访问速度和最大主频都会受限。如果你用一个输出不稳的 USB 口供电电压掉到 2.7V主频可能还维持 72MHz但 Flash 需要更多等待周期实测性能下降十几个百分点。我后来固定在板子上焊了一个 LDO用外部稳压电源供电数据的重复性立刻好了很多。5.4 优先级配置与优先级分组Cortex-M 的 NVIC 支持把 4 位优先级拆分成抢占优先级和子优先级。如果你在初始化时没有调用优先级分组设置函数默认分组可能不是你想要的那种。RTOS 通常要求把所有的优先级位都用作抢占优先级因为内核要靠抢占优先级来实现任务调度和临界区保护。这里有个经典错误把 SysTick 的优先级设得比某个外设中断高然后在这个外设中断里调用了 RTOS 的从中断释放信号量接口。结果是 SysTick 打断了正在进行的内核操作破坏了内核数据结构系统在某些时候会莫名奇妙地跑飞或者数据错乱。这在测试阶段表现为切换时间偶尔突然变成几十微秒因为内核在做错误恢复或者被卡住了。注意中断里调用 RTOS 的 API 必须用带FromISR后缀的版本这一点几乎所有 RTOS 都一样。用错了症状不一定立刻出现可能在跑了几小时之后才崩排查成本极高。5.5 默认配置与裁剪配置混为一谈这一点在第 3 章已经说过但值得单独拎出来。任何说某 RTOS 占用 26KB Flash的说法都必须附带配置说明。Zephyr 的默认配置打开了日志、断言、设备初始化、电源管理、shell、多个驱动关掉之后体积能砍掉三分之二。正确的对比方法是功能对等对比先定义你的项目需要哪些功能然后分别在每款 RTOS 上配置出这一组功能再比较体积和性能。比如需求是两个任务、一个信号量、一个串口输出、一个 1ms 定时器那就在八款 RTOS 上都配出这个需求再比。这样得出的结论才有意义。5.6 把内核性能和整体系统性能混为一谈最后一个坑是概念层面的。内核切换时间只是整个系统性能的一小部分真实项目里影响响应速度的因素还有中断处理时间、驱动层的阻塞行为、内存分配策略、以及日志输出的开销。尤其是日志。MCU 项目里加日志几乎必然踩坑很多人用 printf 直接输出到串口而 printf 是阻塞的115200 波特率下打印 80 个字符需要将近 7 毫秒。如果你的任务里带 printf那这个任务的实际周期比内核切换时间大三个数量级。这种项目里讨论哪个 RTOS 快 0.3 微秒毫无意义。正确的做法是用环形缓冲加非阻塞写入把日志先存到 RAM 缓冲区里由低优先级任务慢慢刷出去。如果还要存到片内 Flash那就更要注意Flash 写入是按扇区擦除的写入时会长时间阻塞总线必须放到最空闲的时候做或者借助双区切换避免影响实时任务。6. 场景化选型把数据翻译成决策6.1 小容量 MCU 与电池设备如果你的片子是 64KB Flash、20KB RAM 这一档并且产品对成本极度敏感那我实测下来最稳的选择是 FreeRTOS、TencentOS-tiny、RT-Thread Nano 这三款。它们的最小工程都在 8KB 以内切换时间在 2 微秒上下功能够用社区资料多遇到问题好查。FreeRTOS 的优势是生态最广各种中间件和第三方移植遍地都是遇到问题几乎一定能搜到答案。TencentOS-tiny 的优势是体积最小而且它内置的组件比如某些通信协议在国产芯片上的适配做得比较细。RT-Thread Nano 的优势是 API 风格统一如果你的团队以后可能升级到完整版代码迁移成本最低。电池设备还要额外考虑 tickless 模式和低功耗。这一块 FreeRTOS 的 tickless 实现最成熟社区里有大量实测数据可以参考。Zephyr 的电源管理框架更系统化但配置复杂在小片子上不一定划算。6.2 需要协议栈和文件系统的中量级项目如果片子换到 256KB 以上 Flash并且项目需要文件系统、网络协议栈、或者多路通信那选择逻辑完全变了。这时候 RT-Thread 完整版的优势非常明显设备框架统一组件生态齐全从串口到文件系统到网络全都有官方维护的版本互相之间的接口是打通的。NuttX 和 Zephyr 在这个区间也有竞争力尤其 Zephyr 的设备树机制在管理多个板型、多个硬件版本的时候非常省事。如果你的团队要同时维护三款硬件每款外设配置都略有不同那设备树带来的收益远超它增加的那点 Flash 占用。值得注意的是这个阶段内核切换时间的差异已经不重要了。系统响应速度的瓶颈变成协议栈和驱动的实现质量。我见过一个项目为了省 0.2 微秒切换时间换了一个 RTOS结果新的网络协议栈效率低整体吞吐反而下降三成。6.3 面试和学习的视角RTOS 与 Linux 的分界线很多人问 RTOS 和 Linux 的区别这也是高频面试题。用我这次测试的视角来说最本质的差异在三点。第一是实时性的保证方式。RTOS 的调度器是抢占式的优先级最高的就绪任务永远在运行中断延迟和调度延迟有明确的上界通常在微秒级。Linux 的标准内核是为吞吐量优化的调度器要兼顾公平性和交互响应虽然也有实时调度策略但普通进程的响应延迟在毫秒甚至几十毫秒级必须配合实时补丁才能进入微秒讨论范围。第二是内存模型。RTOS 通常跑在单地址空间里所有任务共享一片物理内存没有虚拟内存、没有内存保护单元参与即使有 MPU 也多数只做基础隔离。Linux 有完整的虚拟内存、页表、进程隔离一个进程崩了不会影响别人但这个能力是靠 MMU 和页表换来的硬件成本和上下文切换开销都高得多。第三是运行环境的确定性。RTOS 的执行路径基本可以静态分析出来堆的使用、栈的深度、最坏执行时间都能算。Linux 因为动态加载、页缓存、后台任务的存在很难给出严格的执行时间上界。所以面试里如果被问为什么不用 Linux 做这个项目标准答案不是Linux 不好而是这个场景对最坏响应时间有硬要求而且硬件资源不够支撑 MMU 和虚拟内存的额外开销。7. 常见问题速查与排查思路7.1 高频问题速查表现象可能原因排查动作所有 RTOS 都慢得出奇主频未配置正确实际跑在内部低速时钟用探针输出系统时钟或用示波器测一个已知延时循环切换时间抖动超过 50%SysTick 或高优先级中断插队临时屏蔽所有中断只留测量所需某些工程测不出信号探针被编译器优化掉确认寄存器指针带 volatile检查反汇编中断延迟偶尔变成几微秒内核临界区屏蔽了中断检查临界区长度确认中断优先级配置Flash 占用远大于预期误链接了 printf 或浮点库检查 map 文件关掉调试宏系统运行几小时后跑飞中断里用了非 FromISR 的 API全局搜索 API 调用逐个确认上下文移植后能跑但功耗异常tickless 未开启或空闲任务未进低功耗检查空闲钩子确认低功耗指令被执行写入 Flash 日志时任务卡顿擦写期间总线阻塞改用双区切换或把擦写放到最空闲时段这张表里的每一条我在测试过程中至少遇到过一次。尤其是探针被优化掉和误链接 printf这两条属于必踩的坑谁做谁中。7.2 移植到 GD32F103 时最容易翻车的几处GD32F103 和 STM32F103 的内核相同、外设寄存器地址高度兼容所以移植 RTOS 的时候直接复用现成的 M3 移植文件通常没问题。但下面几处必须逐一确认否则实测数据没法看。第一处是启动文件和向量表。GD32 的启动文件里中断向量表的符号名跟 ST 的可能不完全一致如果启动文件里没有正确设置栈顶地址程序会跑飞。这个错误很明显编译能过但一上电就挂在硬件错误处理里。第二处是系统时钟。GD32F103 的外部晶振常见是 8MHz通过 PLL 倍频到 72MHz 或者更高。如果你用 ST 的标准库代码来做时钟初始化部分寄存器位定义跟 GD32 有细微差别可能导致倍频系数不对。我建议直接用 GD32 官方的库函数或者干脆自己按数据手册写寄存器配置写完用示波器测 MCO 引脚输出确认频率。第三处是 Flash 等待周期。72MHz 需要 2 个等待周期这个值必须跟主频匹配。配少了会取指错误配多了性能下降。GD32 的部分型号在 108MHz 下对等待周期的要求跟 ST 不同如果照抄很可能出问题。第四处是 SysTick 时钟源。SysTick 可以选择内核时钟的 1/8 或者内核时钟本身这个选择决定了 tick 的精度。RTOS 移植层通常会自己配置但如果你手动改过记得确认一遍。第五处是中断优先级分组。Cortex-M3 默认的分组不一定是 RTOS 期望的很多移植文件里会在初始化时调用一次分组设置如果你在别的地方又改了一次就可能冲突。全工程搜索一遍分组设置函数的调用保证只有一处。7.3 我踩过的几个坑第一个坑是调试器。用在线调试的时候调试器会周期性访问总线去读取变量尤其在变量被监视窗口盯着的时候。这会打断 CPU 的连续执行让测量数据凭空多出几十微秒而且抖动巨大。正确做法是烧写完成后拔掉调试器让程序独立运行只在需要看变量的时候才连上。第二个坑是串口日志。第一轮测试的时候我在任务里保留了串口输出结果测出来的切换时间完全不可信。后来把所有输出都改成写入 RAM 缓冲测完再统一打印数据立刻稳定了。这件事给我的教训是任何阻塞式 IO 都不应该出现在性能测试路径上。第三个坑是堆的分配策略。FreeRTOS 有 5 种堆实现其中 heap_4 带合并功能分配释放的代码路径比 heap_1 长不少。我一开始用默认的 heap_4后来发现信号量创建的时候要动态分配这个开销被算进了启动时间。改成静态创建之后启动时间少了 0.4 毫秒。所以测启动时间时务必用静态创建把动态分配的影响剔除。第四个坑是时钟节拍。我一开始设成 1000Hz也就是 1ms 一个 tick后来为了测极限性能改成 10000Hz结果发现所有内核的切换时间都变长了因为 tick 中断太频繁插进了测量窗口。最后统一回 1000Hz这也是绝大多数项目的实际配置。8. 关于数据可信度的一点补充说明上面所有数字都来自同一块 GD32F103C8T6 小板、同一套工具链、同一个供电环境重复测试了三轮取的是中位数。我要强调的是这些数据的价值在于相对关系不在于绝对值。你把主频改成 48MHz、把 Flash 等待周期改成 1、把优化等级改成 -Os所有数字都会变但哪几款属于轻量档、哪几款属于重量档这个分类基本不会变。如果有人拿一份评测数据跟你说某款 RTOS 完胜另一款你可以问他四个问题优化等级是多少、探针怎么写的、默认配置还是有裁剪、SysTick 优先级设成了什么。这四个问题答不上来数据就没法采信。这也是我自己做完这轮测试最大的收获——RTOS 的性能差异很多时候没有测量方法带来的差异大。搞清楚自己在测什么比测出什么更重要。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门