十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F407统一实测8款RTOS:上下文切换与中断延迟

STM32F407统一实测8款RTOS:上下文切换与中断延迟 1. 先把这场实测的边界划清楚同一块 MCU 上跑 8 款 RTOS这件事的价值不在于排出一个谁第一谁倒数的榜而在于把那些藏在编译选项、时钟树、中断优先级里的变量全部摁死只让内核本身说话。我在过去两年里给不同的项目做过 MCU 选型见过太多次某款 RTOS 慢得没法用的结论最后追下去发现是 SysTick 优先级配错、或者是拿一个带 shell 和文件系统的全功能构建去比人家裁剪到骨头的最小构建这种对比毫无意义还容易让团队做出错误的架构决策。所以这篇文章要解决三个具体问题第一在一个完全统一的软硬件约束下8 款主流 RTOS 在上下文切换、信号量、互斥量、中断到任务延迟、节拍开销、内存占用这六项指标上的真实差距到底有多大第二哪些差距是内核设计带来的哪些差距纯粹是配置和测量方法造成的假象第三也是最关键的一点哪些 RTOS 最容易被误判——被高估的和被低估的分别是谁。适合谁来读正在做 MCU 选型、被RTOS 性能这个话题绕晕的嵌入式工程师准备把裸机工程改成 RTOS、但不知道从哪一款下手的开发者以及已经用着某款 RTOS、想看看到底有没有调优空间的同行。文中的绝对数值都是我在这块板子上的实测值你换一块芯片、换一个编译器版本数值一定会变但结论的方向和那些坑的形态大概率是一样的。这一点我在开头必须讲明白否则后面所有的表格都会被人拿去当真理用。1.1 为什么同一块 MCU是这场对比唯一的公平起点市面上能查到的 RTOS 跑分绝大多数来自各家自己的 BSP 或者官方开发板。FreeRTOS 的官方数据往往跑在 Cortex-M3 上ThreadX 的公开数据来自某个高频 M7Zephyr 的基准测试又分布在一堆不同的板子上频率从 16MHz 到 480MHz 都有。把这些数字放在一张表里比较本质上是在比芯片不是在比内核。我把 8 款 RTOS 全部收敛到同一块STM32F407VGT6上Cortex-M4F168MHz 主频1MB Flash192KB SRAM外部 8MHz 晶振经 PLL 倍频Flash 等待周期固定为 5WSART Accelerator 的指令缓存和数据缓存都打开。选 M4 而不是 M0 或者 M7 是有考虑的M0 没有 DWT 的 CYCCNT 周期计数器做不了周期级测量M7 带 DCache缓存命中与否会让同一段代码的耗时差出三倍测出来的东西噪声太大不适合做首次横向对比。同一块板子的意义在于Flash 等待周期、总线仲裁、SRAM 访问速度、中断控制器版本、SysTick 行为这些底层变量全部是同一套。剩下需要控制的就是软件侧编译器、优化等级、节拍频率、堆分配器、任务栈大小、NVIC 优先级分组。这些东西我后面会逐条列成表格每一项都要对齐漏掉任何一项结论就不可复现。另外我还用GD32F103C8T6Cortex-M3108MHz64KB Flash20KB 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)微内核MIT2RT-Thread Nano3.1.5微内核裁剪Apache-2.03RT-Thread全功能5.0.2含设备框架/DFS/shellApache-2.04Zephyr3.7 LTS单体内核Apache-2.05Eclipse ThreadX6.4.0微内核MIT6Keil RTX55.9.0 (CMSIS-RTOS2)微内核Apache-2.07µ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)第一个陷阱是DEMCR的TRCENA位。很多人只开了DWT_CTRL的CYCCNTENA结果计数器纹丝不动以为是芯片不支持。实际上 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 差分法测切换开销绕开指令计数误差的笨办法上下文切换的开销很难直接测因为一次切换之后你不能确定任务从哪里继续执行。社区里有个很实用的笨办法我一直在用让两个同优先级任务做乒乓通过taskYIELD或osThreadYield主动让出任务体里翻转 GPIO示波器读到一个完整周期的宽度。这个周期等于周期宽度 两次切换开销 两次任务体执行时间然后把任务体里的空循环次数从 0 改到 100再改到 200测三组数据做线性拟合斜率和截距一算切换开销就出来了。这个方法的妙处在于它自动抵消了 GPIO 翻转、循环计数器、分支判断这些固定开销的影响不需要你去数汇编指令。用差分法测出的纯切换开销比一些文章里测一次 yield 的耗时要准确得多。后者把 GPIO 操作、函数调用、参数检查全算进切换时间里了数值能虚高 40% 以上。2.4 统一约束清单不对齐这九项数据就没有意义下面这张表是全文最重要的一张表。任何一项没对齐你测出来的差异都可能不是内核的差异。约束项统一取值不对齐会有什么后果主频168MHzFlash 5WSART 全开影响所有绝对数值可造成 30% 以上偏差编译工具链arm-none-eabi-gcc 13.2-O2 -flto -fno-common不同优化等级可让切换耗时差 50%Keil 复核AC6 (LLVM)同等级优化RTX5 用 GCC 编性能会明显劣化节拍频率统一 1000Hztickless 关闭10kHz 节拍会额外吃掉 3%~8% CPUNVIC 优先级分组NVIC_PRIORITYGROUP_44 位全抢占FreeRTOS 会误入断言或临界区形同虚设堆分配器静态分配优先动态仅用于创建期分配器算法差异会污染切换数据任务栈统一 512 字节configCHECK_FOR_STACK_OVERFLOW2栈溢出导致行为异常数据不可信空闲任务钩子全部为空不做任何事有钩子里跑 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是性能结论的命门。同一个内核配置不同切
返回列表