资讯动态

GD32F103八款实时内核实测:任务切换与选型误判解析

发布时间:2026/9/18 6:03:40 来源:尧图企业网站定制
同一块 GD32F103 的板子同一版测试代码八款实时内核轮流烧进去跑我前后折腾了将近一个月。有意思的是第一轮测完我兴冲冲地把排名表发到群里第二天就有朋友用他自己的板子复现结果前四名几乎换了个位置。这件事让我意识到内核性能对比里最容易出错的从来不是被测对象而是测量者自己。Cortex-M 内核、RTOS、任务切换时间、信号量、临界区这些概念看起来都很直白但真正落到寄存器周期级别去量化时每一个环节都能把结论带偏。这篇内容适合正在选型的内核使用者、准备内核移植题的开发者以及手上有具体项目、想知道到底该用哪个的工程师参考。我会把测试平台、计时方法、移植过程、原始数据、以及排名为什么会翻转全部摊开讲一遍。1. 先说说为什么同一块板子跑八款内核这件事天生不公平把八款内核塞进同一颗 MCU听起来是最公平的对比方式硬件相同、外设相同、时钟相同唯一变量就是内核。但实际做起来你会发现能锁死的变量其实没几个剩下的一大堆都在偷偷影响结果。我一开始也天真地以为只要代码一样就行直到发现同一份测试函数在两次编译之间就能差出 20% 的耗时。1.1 测试平台GD32F103 与它的三个频率档位我选的平台是 GD32F103C8T672MHz 主频下和 STM32F103 系列引脚兼容、外设布局接近而且它本身支持更高的主频档位方便我做频率横评。Cortex-M3 内核没有 FPU没有指令与数据缓存但有 Flash 预取缓冲和分支预测这几样东西恰恰是测量误差的主要来源之一。三个频率档位分别是 24MHz、72MHz 和 108MHz。之所以要测三个是因为 Flash 的等待周期是随主频分档变化的频率低的时候取指几乎无等待频率拉高之后 Flash 必须插等待周期而内核取指流水线的停顿会直接吃掉一部分性能。不同内核的代码体积、跳转密度、指令排布都不一样受等待周期的影响程度自然也不一样。这一点后面会重点讲因为它是我见过的、最容易让排名发生翻转的单因素。外设方面我只用到了几个 GPIO 和一路串口串口用来输出日志但正式测量时全部关掉。逻辑分析仪的探针接在一个空闲 GPIO 上用来做交叉验证这个习惯救过我不止一次。1.2 被我锁死的变量清单每一轮测试开始前我都会按下面这张清单过一遍。少做一项数据就可能不可信。变量项固定值不固定会怎样编译器与版本同一版本 ARMCC / GCC 交叉编译工具链代码生成策略不同切换耗时能差 30%优化等级统一 -O2另做 -O0/-Os 对照优化等级直接影响函数内联与栈帧时钟树统一的 HXTAL 输入与 PLL 配置主频抖动直接线性影响所有周期数中断优先级全部内核相关中断用同一优先级组抢占关系变化会改变测量语义测试顺序每项测量前先空跑 1000 次预热首次执行偏慢会把均值拉高编译产物同一份测试源文件只替换内核逻辑差异会污染对比供电与环境温度同一路稳压、同一室温温度影响 Flash 与内核时序余量表格里最后一行看起来有点夸张但夏天和冬天各测一轮确实能看到整体数据有 1% 到 2% 的浮动。对微秒级测量来说这已经不能忽略了。1.3 唯一没锁住的自变量内核配置硬件和工具链能锁死内核配置却没法完全统一因为八款内核对默认配置的理解天差地别。有的内核默认打开调度器统计、打开断言、打开调试钩子有的默认把空闲任务里挂一个低功耗指令有的默认用 1000Hz 的节拍有的默认 100Hz。我的处理方式是做两套数据一套叫出厂默认配置什么都不改直接跑另一套叫最小化配置把日志、Shell、断言、统计、设备框架这些全部关掉只保留调度器、同步原语和内存管理。这两套数据出来的排名差异正是标题里说的被误判的核心来源。默认配置下看着臃肿的内核裁剪之后可能非常能打反过来某些默认就很精简的内核你会发现它已经没什么可裁的了也就是说它的默认值已经接近它的性能上限。2. 计时方法决定排名三种测量手段的误差来源与选用原则用什么方法测比测什么更重要。我在这一块走了不少弯路下面按可靠性从低到高排一下并且说清楚每种方法在什么场景下会骗你。2.1 SysTick 计数法方便但自带系统性偏差最直觉的做法是读 SysTick 的当前值寄存器。Cortex-M3 的 SysTick 是一个 24 位递减计数器配置成 1ms 中断时在 72MHz 下每毫秒走 72000 个计数分辨率够看但有几个问题。第一读 SysTick 需要读寄存器并做减法还要处理计数回绕这段代码本身就要花几十个周期如果你的被测对象本身只有几百个周期误差比例就非常可观。第二SysTick 中断常常就是内核心跳的来源测量期间可能被中断打断导致结果里混进中断服务程序的时间。第三如果你要测的东西正好跟节拍处理有关用 SysTick 测就相当于用尺子量尺子。我后来只在测长耗时操作比如内存拷贝、大块数据搬运时才用 SysTick测内核原语一律换方法。2.2 DWT CYCCNT周期级精度的正确用法与两个坑Cortex-M3/M4 的数据观察点单元里有一个自由运行的周期计数器也就是 DWT 的 CYCCNT。它每来一个内核时钟周期就加一32 位在 72MHz 下大约 59 秒回绕一次对我们的测量完全够用。开启方法不复杂先通过调试控制寄存器把跟踪使能位打开再清一次 CYCCNT然后使能计数。核心代码大概是这样// Cortex-M3/M4 上的周期计数器初始化 volatile uint32_t *DEMCR (uint32_t *)0xE000EDFC; volatile uint32_t *DWT_CTRL (uint32_t *)0xE0001000; volatile uint32_t *DWT_CYCCNT (uint32_t *)0xE0001004; void dwt_init(void) { *DEMCR | (1u 24); // 使能跟踪 *DWT_CYCCNT 0; // 清零 *DWT_CTRL | (1u 0); // 使能周期计数 } static inline uint32_t cyccnt_get(void) { return *DWT_CYCCNT; }第一个坑是网上大量示例把这段代码写成普通指针访问编译器在 -O2 下会把两次读取重排、甚至认为第二次读的值和第一次相同而直接复用。必须用 volatile 修饰并且在关键测量点前后加编译屏障比如内联汇编的空语句并声明破坏内存确保编译器不敢乱动顺序。这个细节我在第一轮测试时没注意导致有几百个周期凭空消失了。第二个坑是很多入门级 M0/M0 内核根本没有 DWT移植时要去查对应内核的手册别照着 M3 的代码直接抄。2.3 GPIO 翻转加逻辑分析仪用来打脸前两种方法DWT 测得再准也只是软件在自说自话。我的做法是在每个测量点前后翻转一个 GPIO用逻辑分析仪记录脉宽再跟 DWT 的结果对照。两者如果能对上误差在几十纳秒内主要是 GPIO 翻转本身的开销数据才敢用。逻辑分析仪的采样率要足够高测微秒级脉宽至少选 100MSa/s 以上的档位否则量出来的不是脉宽而是采样点的量化误差。另一个技巧是GPIO 翻转的开销是固定的可以先单独测一遍空翻转的耗时再从所有结果里减掉这样两边就能严格对齐。这套交叉验证帮我抓出过一次严重问题某个内核的默认配置里开了调度器运行时统计统计代码在任务切换路径上插了一段计数逻辑DWT 测出来的切换耗时比逻辑分析仪量出来的多了将近四成。如果不做交叉验证我会直接把责任记在内核对切换实现上。2.4 该报平均值还是最小值这份数据我只看最小值和中位数测一组数据先跑 1000 次预热再采 10000 次。然后我会同时记录最小值、中位数、平均值和最大值。平均值在这类测量里参考价值很低因为任何一个中断、任何一次总线冲突都能把单次结果拉长几倍平均下来就把噪声均匀抹到所有内核头上反而掩盖了真实差异。最小值代表理想路径下的开销适合比较调度器核心路径的代码效率中位数代表大多数情况的典型表现最大值则暴露了内核在最坏情况下的抖动这对实时性要求高的场合非常关键。我在报告里会同时给出最小值和中位数两者差距大的内核往往意味着它的临界区处理或者中断关断策略不够稳定。3. 八款内核的移植现场谁十分钟跑通谁卡在构建系统性能数据再好移不进去也白搭。我选的八款内核分别是 FreeRTOS、RT-ThreadNano 与完整版各测一次、TencentOS-tiny、µC/OS-III、RTX5、LiteOS-M、Zephyr、NuttX。它们的移植难度大致分成三个梯队。3.1 极简移植派FreeRTOS、TencentOS-tiny、RT-Thread Nano这一类的共同特点是移植层很薄核心就是一个端口文件加几个配置宏。FreeRTOS 在 Cortex-M3 上只需要提供节拍中断处理、切换触发异常处理、以及启动第一个任务的汇编片段三个函数写完就能点灯。TencentOS-tiny 的移植层结构类似代码量更小文档里对寄存器操作的注释比较细对新手相对友好。RT-Thread Nano 走的是把内核剪出来单独用的路线配置项在一个头文件里集中管理改起来直观。这三个我都在半天内跑通了第一个任务。手感上的差别在于FreeRTOS 的示例代码最多几乎每个坑都能搜到现成答案TencentOS-tiny 的代码更短读起来轻松但社区里针对具体芯片的资料少一些RT-Thread Nano 的接口风格偏面向对象函数命名统一熟悉之后写业务代码很舒服。3.2 配置器依赖派RTX5、µC/OS-III这两款的特点是有一个功能很强的配置工具能自动生成初始化代码和配置结构体。好处是不容易配错优先级、节拍频率、栈大小这些东西坏处是一旦你脱离配置器手工移植就很容易漏掉某个默认值。我在 µC/OS-III 上踩过一个典型的坑手动移植时忘了设置某个与时间戳相关的配置项结果所有延时函数的实际行为跟预期不一致测出来的任务切换间接耗时完全对不上。RTX5 走的是与工具链深度绑定的路线用起来的体验是顺畅但不太好拆如果你想在非官方工具链里用它转换成本要提前算进去。3.3 构建体系派LiteOS-M、Zephyr、NuttX这三款的移植不是改一个文件而是接入一套构建体系。LiteOS-M 有自己的一套目录约定和组件配置方式Zephyr 用的是设备树加 Kconfig 加 CMake 的组合学习曲线明显更陡但一旦把板级支持包配好换芯片比谁都省事NuttX 的配置系统基于多个配置文件叠加选项多到需要花时间建立心理地图。客观说这一梯队的初始投入最大。我在 Zephyr 上花了整整两天才让第一个线程跑起来其中大半天时间花在搞明白 Kconfig 的依赖关系上。但跑通之后我改了一次板级配置从一颗芯片切到另一颗同系列芯片只动了几行描述文件这个体验是极简派给不了的。3.4 移植过程中踩到的四个具体坑把八个内核都跑一遍之后这些坑基本是共性的值得单独列出来节拍中断优先级设置过低。低优先级节拍在有高频外设中断时会被持续抢占导致延时误差累积跑起来像是内核变慢了其实是配置问题。任务栈单位混淆。有的内核对栈大小按字计有的按字节计混淆之后要么浪费内存要么直接栈溢出而且溢出往往表现为随机死机极难定位。空闲任务里的低功耗指令。默认打开时空闲任务会进入睡眠如果这时候调试器连着会出现单步跳不动的现象很多人误以为是内核卡死了。首任务启动方式差异。有的内核要求在启动调度器之前先创建任务有的支持在调度器里动态创建顺序搞反会直接进硬件异常。提示新内核第一次跑起来之前先在切换异常服务程序里翻转一个 GPIO用示波器看波形。有波形说明调度真的在跑比看串口打印可靠得多因为串口打印本身依赖驱动和缓冲出问题时它往往也一起失效。4. 实测数据拆开看切换、同步、通信、中断四组对比下面这组数据来自最小化配置、72MHz、-O2、DWT 加逻辑分析仪交叉验证的结果。说明一下不同芯片、不同工具链、不同配置下绝对值会有明显出入这里给的是量级和相对关系用来判断趋势不要当成芯片手册里的硬指标。4.1 任务切换Cortex-M 的硬件压栈帮了所有人Cortex-M 的异常进入和退出会自动压栈和恢复部分寄存器这个硬件机制让所有内核的任务切换都站在同一条起跑线上。所以八款内核在这项上的差距主要来自软件部分谁来保存剩余寄存器、谁来选下一个任务、以及临界区有多长。内核切换耗时最小值72MHz观感FreeRTOS约 1.2 微秒路径极短汇编干净RT-Thread Nano约 1.3 微秒与前者接近TencentOS-tiny约 1.4 微秒代码可读性最好RTX5约 1.5 微秒附加了少量状态维护µC/OS-III约 1.8 微秒就绪表管理带来额外开销LiteOS-M约 1.7 微秒默认统计功能影响明显Zephyr约 2.1 微秒默认配置包含较多钩子NuttX约 2.3 微秒结构偏向通用性切换耗时在 1.2 到 2.3 微秒这个区间相差不到一倍。这里要说清楚切换耗时短不等于系统吞吐高。调度器选下一个任务用的是什么数据结构、就绪表的查找复杂度是常数还是跟优先级数量相关这些在任务数少的时候看不出来任务数一多就会放大。4.2 信号量与互斥量优先级继承的代价差在哪信号量的释放和获取是实际项目里调用最频繁的原语。这组数据我测的是任务上下文中对已释放信号量的获取、以及从任务中释放信号量的耗时。不涉及优先级继承的二值信号量各家都在 0.8 到 1.6 微秒之间。而支持优先级继承的互斥量耗时普遍高出一截因为每次获取和释放都要判断是否发生了优先级反转、是否需要调整持有者的优先级。µC/OS-III 和 RTX5 在这块的状态维护比较完整开销自然也高一些FreeRTOS 的互斥量把优先级继承做成了可选宏打开和关闭的差距能到三成左右这一点在选型时值得注意因为很多项目默认是关着的。这里有个容易被误判的点信号量的性能高低很大程度上取决于释放时是否有任务在等以及等待任务是否需要唤醒并触发调度。同一款内核测无人等待的释放和有人等待的释放结果可能差两到三倍。我看过一些对比文章只测了前者那测出来的其实是入队和出队的耗时跟调度器几乎没关系。4.3 消息队列与内存池数据结构选择决定数量级消息队列是这一组里差异最大的一项因为各家用的底层数据结构完全不同。有的是环形缓冲加直接拷贝有的是链表加指针传递有的支持可变长消息有的只支持定长。定长消息、单生产者单消费者、队列深度 16 的条件下最快的内核单次入队大约 1 微秒出头最慢的接近 3 微秒。差距主要来自两个地方一是消息拷贝方式按值拷贝的每一次都要搬完整条数据二是入队时对等待任务列表的操作方式如果要用有序插入维护优先级顺序开销就会随等待任务数增长。内存池这边固定块分配普遍在 0.5 微秒以内差异不大。真正拉开差距的是变长分配涉及空闲链表查找和碎片合并的内核明显更慢。我个人在 MCU 项目上几乎不用变长分配固定块加预分配阵列的组合确定性最好也最容易做最坏情况分析。4.4 中断延迟与最大关中断时间这一项对实时性要求高的项目来说比切换耗时重要得多。中断延迟从硬件检测到中断、完成压栈、跳进用户的中断服务程序开始算Cortex-M 的硬件部分基本固定差异来自内核封装的那一层进中断时要更新嵌套计数、可能要做节拍处理、可能要检查是否需要切换。实测下来从信号触发到用户中断处理程序第一条指令执行各家在 0.5 到 0.9 微秒之间。差距不大但最坏情况下的抖动差别明显。我特别关注了关中断时间这一项因为内核在访问自己的临界数据结构时会关中断如果这段临界区里做了不该做的事系统的最坏响应就会被拉长。多数精简内核的临界区都在几十个周期内个别默认开启了统计功能的内核临界区长度会翻倍。4.5 最小系统的 ROM/RAM 账本内存占用是选型时的硬约束尤其是在只有几十 KB Flash 的芯片上。以下是我在最小化配置、单任务加一个空闲任务、一个信号量、一个队列的条件下统计的量级。内核Flash 占用量级RAM 基础占用备注FreeRTOS6 至 9 KB约 1 KB可裁剪性极强RT-Thread Nano4 至 7 KB约 1 KB内核剪裁干净TencentOS-tiny5 至 8 KB约 1 KB组件可选RTX56 至 10 KB约 1.2 KB与工具链绑定µC/OS-III10 至 16 KB约 1.5 KB功能齐全LiteOS-M12 至 20 KB约 2 KB依赖组件配置Zephyr20 KB 以上约 3 KB构建体系决定下限NuttX25 KB 以上约 3 KB更接近小型系统这里最需要注意的是Flash 占用不等于运行期性能。Zephyr 和 NuttX 的占用大是因为它们默认带了很多可用的子系统剪裁空间也大但在 Flash 只有 64KB 的芯片上这个下限就是实实在在的门槛。反过来FreeRTOS 的可裁剪性确实是它的核心优势我在一个只有 32KB Flash 的项目上把它剪到了 6KB 以内功能一点没少。5. 排行榜骗人的五种方式哪些结论是假象前面讲了数据这一节讲数据是怎么骗人的。我把一个月里遇到的误判场景归成五类每一类都实际发生过。5.1 编译优化等级与编译屏障这是我遇到过的、最容易让排名整体翻转的因素。同样一份测试代码-O0 和 -O2 下编译出来某些内核的切换耗时会差出四成。原因在于测量点的代码是否被内联、循环是否被展开、以及两次周期计数之间的指令是否被重排。最隐蔽的一种情况是编译器发现两次读取周期计数器之间的代码没有副作用直接把整段代码优化掉了你测出来的接近零的数字实际上是在测一个空操作。解决办法是在测量点前后加编译屏障并确保所有被测量的变量都是 volatile同时把测量函数的定义和调用放在同一个编译单元里避免跨单元优化带来的不确定性。5.2 默认配置下的隐藏负载这一项正是标题里被误判最主要的部分。默认配置下某些内核打开了运行时统计、断言检查、调试钩子、甚至任务级的栈使用量监测。这些东西在开发阶段非常有用但在性能测试里就是纯粹的负担。我印象最深的是某内核在默认配置下的切换耗时排到了倒数第二把统计和调试相关选项全部关掉之后直接进到前三。这说明两件事一拿默认配置的结果去否定一款内核是不负责的二反过来也一样如果一款内核默认就很干净它的默认成绩就已经接近上限了不能因此就说它优化空间大。公平的做法是先给出厂默认配置的数据再给最小化配置的数据两组一起看。只报一组都是有偏的。5.3 首次执行与稳态执行的差异第一次执行某段代码和第一万次执行耗时可能不同。原因包括 Flash 预取缓冲还没填充、分支预测器还没建立历史、以及某些内核在首次执行时做的惰性初始化。我的处理是每项测量前空跑 1000 次正式采样 10000 次然后只统计后 9000 次。如果某款内核在预热后数据明显改善说明它有惰性初始化路径这在启动阶段会有一次性的延迟值得在项目里留意。5.4 测量代码自身的时间被算进了被测代码读周期计数器的指令、编译屏障、函数调用的栈帧建立和销毁这些都要花时间。测一个只有几百个周期的小函数时测量本身的开销可能占了三成。解决办法是测空函数作为基线再从结果里减掉基线。但要注意基线本身也是会被优化和内联的所以要保证基线和被测量的调用形态完全一致否则减出来的数字反而更离谱。5.5 主频与 Flash 等待周期改变了排名这是我最后才意识到、但影响最大的一个因素。把主频从 72MHz 提到更高档位之后Flash 需要插入等待周期取指偶尔会停顿。不同内核的代码密度和跳转频率不一样受影响程度自然不同。结果就是在低频档位上排名靠后的内核到了高频档位反而可能反超因为它的核心路径代码更紧凑、跳转更少反过来某些在低频下表现优异的内核高频下被取指停顿拖累。同一份代码、同一块板子只改主频排名就能变。所以如果你的项目会跑在高频档位测试就必须在高频档位上做别拿低频数据去选高频方案。这一点我在和几个朋友交流时反复强调因为大家默认主频高就是快实际上高主频带来的收益并不是线性的。6. 把数据翻译成选型决策数据摆完该说结论了。但我更想给的是判断方法而不是一个可以照抄的排名因为你的芯片、你的工具链、你的项目形态跟我的都不一样。6.1 按项目形态给建议如果项目是中小规模、功能明确、实时性要求中等FreeRTOS 和 RT-Thread Nano 这类极简内核几乎是最省心的选择代码量小、资料多、最坏情况容易分析。如果项目要做设备框架、要接较多组件、团队里有不同背景的成员RT-Thread 完整版和 LiteOS-M 这类带组件生态的方案更合适代价是初始搭建要多花时间。如果项目对确定性要求极高比如要严格保证某个中断的响应上限那就要挑临界区短、可配置项少、路径可预测的内核µC/OS-III 和 RTX5 这类偏传统的设计在这里有优势。如果项目要跨多颗芯片甚至多条产品线Zephyr 或 NuttX 的构建体系前期投入虽然大但长期看是省事的换芯片的成本被大幅摊薄。6.2 移植完成后的验收清单我在每个内核跑通之后都会做这几项验收缺一项就不算移植完成节拍精度验证连续记录 1000 次 10 毫秒延时统计平均误差和最大偏差。栈溢出边界验证故意给一个小栈确认溢出能被检出并进入明确处理而不是随机死机。中断嵌套验证在低优先级任务里挂起用高频外部中断连续触发确认不丢中断。长时压力验证让系统连续跑 48 小时以上观察内存是否有缓慢下降。异常恢复验证故意触发一次使用错误确认处理程序能准确定位到出错任务。这套流程帮我在两个内核上提前发现了问题一个在长时间运行后出现了极缓慢的计数溢出另一个在中断嵌套达到一定深度时状态维护出错。这两个问题在短时间的功能测试里完全看不出来。6.3 我对这份数据的最终态度八款内核跑下来我的感受是在 Cortex-M3 这个量级的芯片上主流内核之间的性能差距远小于配置差异和测量误差带来的差距。真正拉开差距的是生态、文档、构建方式、以及团队熟悉程度而这些在微秒级的对比表里根本体现不出来。如果你只记住一件事我希望是这句任何一份内核性能对比数据如果没写清楚编译选项、主频、内核配置和计时方法那它给出的排名基本不值得采信包括我自己第一轮测出来的那一份。最后分享一个我一直在用的小技巧把测量代码和业务代码严格分开测量代码单独一个编译单元用固定的函数签名对外暴露业务代码永远不直接读周期计数器。这样既避免了测量代码被优化掉也避免了某天你需要把测量代码从发布版本里剥离时发现它已经渗透进几十个文件里了。踩过几次坑之后我现在做任何内核对比第一件事都是先把测量框架搭好再动手移植顺序反过来一定会返工。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价