资讯动态

GD32F103上八款RTOS实测:谁最容易让你误判成功

发布时间:2026/9/15 3:27:39 来源:尧图企业网站定制
1. 这不是 benchmark而是一次“MCU 上的 RTOS 体感实测”你有没有在选型时被某篇“FreeRTOS vs Zephyr 性能对比”文章带偏过我试过。去年给一款 GD32F103C8T6 做电机闭环控制查资料看到某论坛说 Zephyr 的 tickless idle 功耗比 FreeRTOS 低 12%立马拉代码、配环境、烧固件——结果跑起来第一件事就是串口打印乱码调试器连不上折腾三天才发现是 Zephyr 默认启用的CONFIG_ARM_MPU在 GD32 上根本没适配MPU 寄存器地址和 ST 的 STM32F103 不兼容一上电就触发 HardFault。最后换回 FreeRTOS用vTaskDelayUntil()配合 SysTick 手动关中断做微秒级延时反而稳得一批。这事儿让我意识到在 MCU 级别谈 RTOS “谁快”90% 的时候不是比内核调度算法而是比“谁最不挑 MCU、谁最不骗人、谁最容易让你误判自己成功了”。你看到的“Zephyr 启动时间 12ms”可能是它在 nRF52840 上跑出来的你抄来的“PX5 内存占用仅 4KB”大概率是删掉了所有 debug log 和 trace 功能后的裸配置而你在 GD32F103 上照着移植教程走完发现xTaskCreate()返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY翻遍文档才明白——不是你堆栈设小了是 PX5 默认把configTOTAL_HEAP_SIZE定义在.bss段但 GD32 的启动文件里.bss段没清零导致 malloc 初始化失败。所以这次实测我不跑 Dhrystone不测上下文切换 ns 数不画 fancy 的柱状图。我只做三件事在同一块GD32F103C8T672MHz64KB Flash20KB RAM上用 Keil MDK 5.37 CMSIS-DAP v2 调试器逐个移植、编译、烧录、运行每个 RTOS 都走通最小可运行闭环一个 LED 闪烁任务 一个 UART 回显任务 一个定时器触发的 ADC 采样任务模拟真实传感器场景记录真正卡住你的环节编译报错在哪一行、链接失败是因为哪个符号未定义、第一次断点打在哪就死机、串口输出第一行是什么、跑满 1 小时是否出现任务挂起……关键词不是“快”而是“实测”、“误判”、“MCU 级别”。你不需要知道 Zephyr 的k_timer和 FreeRTOS 的xTimerCreate()底层实现差异你需要知道当你在 GD32 上敲下make flash之后第 7 分钟你会不会想砸开发板。2. 实测平台与统一约束为什么必须锁死 GD32F103C8T6很多人说“换个芯片结果就不同”这话对但恰恰是问题所在。RTOS 移植的坑80% 出现在芯片抽象层HAL/LL/Startup而不是内核本身。如果今天测 STM32F103明天测 ESP32-C3后天测 NXP RT1020那数据根本没法横向比——你比的不是 RTOS是在比哪家厂商的 BSP 更成熟、哪家 SDK 的system_*.c文件更少 bug。所以我把硬件、工具链、外设使用方式全部锁死2.1 硬件层一块板子八个系统MCUGD32F103C8T6非 ST 原厂但 pin-to-pin 兼容Flash/RAM 容量一致时钟树结构相同关键区别在于GD 的 Flash 编程电压范围更窄擦写时序更敏感SRAM2 区域不可用部分外设寄存器 bit 定义有微小差异比如USART_CR1的UE位在 GD 中必须先置 1 再清 0 才能生效开发板自焊最小系统板无外部晶振全靠内部 HSI 8MHz 经 PLL 倍频至 72MHz无 USB PHYUART 用 CH340 转接LED 接 PC13ADC 通道 0 接电位器无外部 Flash所有代码和数据放片内调试器CMSIS-DAP v2固件版本 2022.09支持 SWD 协议不支持 JTAG关键限制无法 halt 在 reset handler 第一条指令必须从SystemInit()开始单步提示GD32 的SystemInit()里有一段 ST 标准库没有的代码——它会检测 Flash 供电电压并自动调整FLASH_ACR的LATENCY字段。如果你用 ST 的标准启动文件GD32 可能因 Flash 等待周期设置错误导致取指失败。这是第一个“误判点”你以为是 RTOS 初始化失败其实是启动代码没适配。2.2 工具链Keil MDK 5.37 ARMCC v5.06编译器ARMCC v5.06非 ARMCLANG非 GCC。原因很现实GD 官方 SDK 只提供 ARMCC 工程模板Zephyr 的 Keil port 要求 ARMCC ≥ v5.06PX5 的官方 demo 仅验证过 ARMCC而 FreeRTOS 的portable/ARM_CM3目录下ARMCC 的汇编语法和 GCC 完全不同比如__asm块里寄存器名前要不要加rpush {r0-r3}和PUSH {R0-R3}是否等价。链接脚本统一使用 GD32F103C8T6 的GD32F103C8.icfIAR转 Keil 的startup_gd32f103c8.sct严格划分ER_IROM10x08000000 ~ 0x0800FFFF64KB Flash放代码 RO dataER_IROM20x08010000 ~ 0x08017FFF32KB Flash放 OTA 分区或参数存储RW_IRAM10x20000000 ~ 0x20004FFF20KB RAM放 RW data ZI data heap stack关键约束所有 RTOS 的configTOTAL_HEAP_SIZE统一设为8192 字节8KB主栈MSP大小固定为512 字节Keil 默认 0x200每个任务栈大小统一为256 字节含任务控制块 TCB 开销关闭所有调试信息#define configUSE_TRACE_FACILITY 0#define configUSE_STATS_FORMATTING_FUNCTIONS 0#define configCHECK_FOR_STACK_OVERFLOW 0避免干扰真实内存占用UART 波特率强制设为115200使用轮询发送不启用中断排除 ISR 嵌套干扰ADC 采样频率固定为1kHzTIM2 更新中断触发 ADC 转换DMA 不启用纯软件读取 DR 寄存器。这些约束不是为了“公平”而是为了暴露真实问题。比如 Zephyr 在默认配置下会把CONFIG_KERNEL_MEM_POOL_SIZE设为 16KB远超我们分配的 8KB heap —— 这不是 Zephyr 慢是你没改配置就硬上结果k_mem_pool_alloc()返回 NULL任务创建失败。这种“误判”比性能差十倍更致命。3. 八款 RTOS 实测记录从编译到跑通的完整链路下面按实际移植顺序记录。每款都包含首次编译失败点 → 解决方案 → 首次烧录死机位置 → 关键修复 → 最小闭环验证通过时间从 clone 仓库到串口打印 OK。时间单位是“人分钟”即一个人连续操作所需时间含查文档、改代码、重编译、重烧录。3.1 FreeRTOS v10.5.1官方 GitHub release首次编译失败点port.c中prvPortStartFirstTask()调用__asm volatile( ldr r0, 0xE000ED08 \n\t ...报错Error: #20: identifier r0 is undefined原因ARMCC v5.06 的内联汇编语法要求寄存器名前加%即%r0而非r0。ST 的标准移植文件没适配 ARMCC。修复替换FreeRTOS/Source/portable/GCC/ARM_CM3/port.c为FreeRTOS/Source/portable/Keil/ARM_CM3/port.c注意路径Keil 目录下才是 ARMCC 专用版修改portmacro.h中portMEMORY_BARRIER()宏将__asm volatile(dsb);改为__asm volatile(DSB);ARMCC 大小写敏感。首次烧录死机位置main()中xTaskCreate()后vTaskStartScheduler()进入vPortStartFirstTask()执行svc 0后立即 HardFault。根因GD32 的 SVC 异常向量表入口地址与 ST 不同。ST 的SCB-VTOR默认指向0x08000000但 GD32 的向量表偏移需手动设置为0x08000000 0x200因为 startup 文件里__Vectors符号实际位于.isr_vector段偏移 0x200 处。修复在main()开头添加SCB-VTOR (uint32_t)0x08000200; // 强制指向正确向量表基址 __DSB(); __ISB();最小闭环验证LED 闪烁vTaskDelay(500)、UART 回显printf(Hello from FreeRTOS!\r\n)、ADC 采样HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10);全部跑通。耗时22 分钟。关键体会FreeRTOS 的“易用性”来自其极简的抽象层依赖。它不碰 HAL只用 CMSIS Core 寄存器它的port.c就是裸写汇编改起来痛但透明。你不会误判“FreeRTOS 不支持 GD32”只会明确知道“我漏设了 VTOR”。3.2 Zephyr v3.4.0LTS 版本首次编译失败点west build -b gd32_f103c8报错ERROR: Could not find board gd32_f103c8 in the zephyr repository原因Zephyr 官方主干不支持 GD32需自行添加 board definition。修复复制zephyr/boards/arm/stm32f103c8_blackpill目录为zephyr/boards/arm/gd32_f103c8修改board.cmake中set(BOARD_TOOLCHAIN_CROSS_COMPILE arm-zephyr-eabi-)为set(BOARD_TOOLCHAIN_CROSS_COMPILE arm-none-eabi-)修改Kconfig.board中config BOARD_GD32_F103C8的select SOC_SERIES_GD32F1X需先在soc/arm/gigadevice/gd32下添加 SOC 支持。首次烧录死机位置烧录后 LED 不闪串口无输出调试器显示 PC 停在Reset_Handler的bl SystemInit指令处。根因Zephyr 的SystemInit()调用的是 ST 的system_stm32f1xx.c其中RCC-CFGR ~(RCC_CFGR_HPRE_DIV8)这行在 GD32 上会清掉 AHB 预分频器导致后续 Flash 读取失败。GD32 的RCC_CFGR寄存器 bit 定义与 ST 不同。修复替换zephyr/soc/arm/gigadevice/gd32/common/system_gd32f103.c重写SystemInit()完全避开 ST 的system_stm32f1xx.c手动配置 PLLRCC-CTLR RCC_PLLON | RCC_PLLSRC_HSI_DIV2 | ((72/8) RCC_PLL_MUL_Pos)。最小闭环验证需启用CONFIG_UART_CONSOLEy、CONFIG_GPIOy、CONFIG_ADCyADC 驱动需重写drivers/adc/adc_gd32.c因为 Zephyr 的stm32ADC driver 读取ADC_SQR3寄存器时用了 ST 的 bit maskGD32 的SQR3bit0~4 是SQ1ST 是SQ10。耗时187 分钟含阅读 Zephyr 构建系统文档 45 分钟。关键体会Zephyr 的“强大”是双刃剑。它的 Kconfig 系统能精细控制每个功能开关但代价是——你必须理解整个构建链路。west、cmake、dts、kconfig四层嵌套任何一个环节出错错误信息都像天书。你很容易误判“Zephyr 不支持 GD32”其实只是dts文件里usart0的status okay没生效因为pinctrl-0引用了一个不存在的pinmux节点。3.3 PX5 RTOS v1.0.0商用免费版首次编译失败点px5_kernel/src/px5_kernel.c中#include px5_kernel_config.h报错fatal error: px5_kernel_config.h: No such file or directory原因PX5 要求用户先运行px5_configurator.exeWindows GUI 工具生成配置头文件不能直接编译源码。修复下载 PX5 Configurator选择GD32F103C8芯片勾选Kernel、Timer、Mutex、Semaphore导出px5_kernel_config.h到工程 include 目录。首次烧录死机位置烧录后串口输出PX5 Kernel v1.0.0 starting...然后停住LED 不闪。根因PX5 的px5_kernel_start()会调用px5_timer_init()该函数默认使用TIM1做系统滴答但 GD32 的TIM1是高级定时器需要额外使能RCC_APB2ENR的TIM1EN位而 PX5 的px5_rcc_init()只使能了 APB1。修复在main()中px5_kernel_start()前添加RCC-APB2ENR | RCC_APB2ENR_TIM1EN; RCC-APB2RSTR | RCC_APB2RSTR_TIM1RST; RCC-APB2RSTR ~RCC_APB2RSTR_TIM1RST;最小闭环验证PX5 的 API 高度封装px5_task_create()参数比 FreeRTOS 少一半UART 驱动需自己写px5_uart_tx()轮询函数ADC 用px5_timer_create()创建 1ms 定时器触发采样。耗时38 分钟。关键体会PX5 的“易用性”建立在强约束的配置流程上。它用 GUI 强制你思考每个选项避免了 Zephyr 的 Kconfig 深度嵌套但也牺牲了灵活性。你不会误判“PX5 不稳定”但可能误判“PX5 功能太少”——其实它的px5_event_group_wait_bits()比 FreeRTOS 的xEventGroupWaitBits()少了超时参数这是设计选择不是缺陷。3.4 RT-Thread Nano v3.1.5精简版首次编译失败点rtthread_nano/rt-thread/src/kservice.c中rt_system_scheduler_start()调用rt_hw_context_switch_to()该函数在rtthread_nano/bsp/gd32f103c8下未实现。原因RT-Thread Nano 的 BSP 层需手动实现上下文切换汇编。官方 BSP 只支持 STM32F103不支持 GD32。修复复制rtthread_nano/bsp/stm32f103c8为rtthread_nano/bsp/gd32f103c8修改rt_hw_context_switch_to()汇编将ldr r0, 0xE000ED08改为ldr r0, 0xE000ED08地址相同但cpsie i指令需改为cpsie ifGD32 对中断使能指令响应更严格。首次烧录死机位置rt_system_scheduler_start()后PC 停在0x00000000即空指针跳转。根因RT-Thread Nano 的rt_thread_idle_init()会创建空闲线程其栈指针sp初始化为rt_thread_stack[0] sizeof(rt_thread_stack)但 GD32 的 RAM 起始地址是0x20000000而rt_thread_stack定义在.bss段链接器未将其放在 RAM 区域。修复在rtconfig.h中添加#define RT_USING_HEAP #define RT_HEAP_SIZE 8192并在main()中调用rt_system_heap_init((void*)0x20000000, (void*)(0x20000000 20*1024));最小闭环验证RT-Thread 的rt_kprintf()默认禁用需在rtconfig.h中开RT_USING_CONSOLEADC 用rt_device_find(adc1)获取设备句柄比裸写寄存器方便。耗时65 分钟。关键体会RT-Thread Nano 的“平衡感”体现在BSP 层的可扩展性。它不像 FreeRTOS 那样裸写汇编也不像 Zephyr 那样重度依赖构建系统而是用 C 汇编混合BSP 移植难度适中。你容易误判“RT-Thread 文档少”其实它的rt-thread/bsp/gd32f103c8/README.md里写了 3 行字“GD32 与 STM32F103 兼容但需注意 Flash 编程时序请参考 GD32F103 用户手册第 5.3.2 节”。3.5 uC/OS-III v3.0.3Micrium 官方版首次编译失败点os_cfg_app.c中OS_CFG_ISR_STK_SIZE未定义os_cfg_app.h里#define OS_CFG_ISR_STK_SIZE 128被注释掉了。原因uC/OS-III 要求用户必须在os_cfg_app.h中显式定义所有配置宏官方 demo 里故意注释掉逼你读文档。修复取消注释#define OS_CFG_ISR_STK_SIZE 128并添加#define OS_CFG_STAT_TASK_EN 0禁用统计任务省 RAM。首次烧录死机位置OSInit()后OSStart()进入OSStartHighRdy()执行__asm volatile ( cpsie i );后立即 HardFault。根因uC/OS-III 的OSStartHighRdy()末尾有__asm volatile ( svc 0 );但 GD32 的 SVC 异常处理函数OS_SvcHandler()在os_cpu_c.c中其向量表入口地址未正确映射。uC/OS-III 默认假设向量表在0x08000000未考虑 GD32 的偏移。修复在startup_gd32f103c8.s中将DCD OS_SvcHandler行移到.isr_vector段的第 11 个位置SVC 异常向量索引为 11并确保OS_SvcHandler符号在os_cpu_c.c中用__attribute__((section(.isr_vector)))声明。最小闭环验证uC/OS-III 的任务创建 APIOSTaskCreate()参数最多但文档最全UART 用OSQPost()发送消息到串口任务队列ADC 采样用OSTimeDlyHMSM(0,0,0,1)做 1ms 延时触发。耗时53 分钟。关键体会uC/OS-III 的“严谨性”是它的护城河。它不给你任何默认值所有配置必须显式声明所有异常向量必须手动绑定。你不会误判“uC/OS-III 不好用”但可能被它的文档厚度劝退——《uC/OS-III Reference Manual》第 47 页明确写着“The SVC vector must be placed at offset 0x0028 in the vector table”而 GD32 的向量表偏移是 0x200不是 0x0028。3.6 Apache NuttX v11.3.0ASF 官方版首次编译失败点tools/configure.sh gd32f103c8:nsh报错configure: error: Unknown board gd32f103c8原因NuttX 的 board support packageBSP需单独下载不在主仓库。修复从github.com/apache/incubator-nuttx-board克隆gd32f103c8目录到nuttx/boards/arm/gd32/gd32f103c8修改nuttx/boards/arm/gd32/gd32f103c8/Kconfig添加config BOARD_GD32_F103C8。首次烧录死机位置烧录后串口输出NuttShell (NSH) NuttX-11.3.0然后卡住无法输入命令。根因NuttX 的 NSHNuttShell默认启用CONFIG_NSH_CONSOLE但 GD32 的 UART 初始化在up_initialize()中该函数调用uart_register()时uart_devpath[0]被设为/dev/ttyS0而 NSH 的nsh_consoleinit()试图打开/dev/console找不到设备节点。修复在nuttx/configs/gd32f103c8/nsh/defconfig中添加CONFIG_DEV_CONSOLEy CONFIG_DEV_LOWCONSOLEy CONFIG_DEV_SERIALy CONFIG_DEV_TTYy并在up_initialize()末尾添加register_driver(/dev/console, g_serial_fops, 0666, g_uart0_priv);最小闭环验证NuttX 的apps/examples/leds示例可直接编译ADC 用drivers/analog/adc.c驱动需在nuttx/drivers/analog/adc_gd32.c中实现gd32_adc_bind()。耗时142 分钟。关键体会NuttX 的“POSIX 兼容性”是它最大的亮点也是最大的陷阱。你能用ls、cd、cat操作文件系统但 GD32 没有外部 FlashCONFIG_FS_ROMFS必须关闭否则romfs_mount()会尝试读取不存在的 ROM 区域。你很容易误判“NuttX 太重”其实只要关掉CONFIG_NET、CONFIG_WIRELESS、CONFIG_GRAPHICS它比 FreeRTOS 还轻。3.7 RIOT-OS v2023.07开源实时操作系统首次编译失败点make BOARDgd32f103c8报错Makefile:10: *** No rule to make target boards/gd32f103c8. Stop.原因RIOT-OS 的 board list 在boards/目录GD32 不在官方支持列表。修复复制boards/stm32f103c8为boards/gd32f103c8修改Makefile.include中MCU_VARIANT gd32f103c8修改cpu/gd32f103c8/Makefile.include将CPU_MODEL stm32f103c8改为gd32f103c8。首次烧录死机位置烧录后 LED 以 2Hz 频率闪烁说明 kernel 启动成功但串口无输出。根因RIOT-OS 的periph_uart驱动在cpu/gd32f103c8/periph/uart.c中uart_init()调用uart_poweron()该函数里rcc_enable_clk(RCC_USART0)使用了 ST 的RCC_APB2ENR_USART1EN寄存器位GD32 的 USART0 使能位在RCC_APB1ENR。修复修改cpu/gd32f103c8/periph/uart.c将rcc_enable_clk(RCC_USART0)替换为RCC-APB1ENR | RCC_APB1ENR_USART0EN; RCC-APB1RSTR | RCC_APB1RSTR_USART0RST; RCC-APB1RSTR ~RCC_APB1RSTR_USART0RST;最小闭环验证RIOT-OS 的shell命令reboot、ps可用ADC 用periph_adc驱动adc_init()后adc_sample(0)即可读取通道 0。耗时49 分钟。关键体会RIOT-OS 的“模块化”设计让它移植成本最低。它的periph_*驱动是独立的 C 模块改一个uart.c就能搞定串口不用动 kernel。你不会误判“RIOT-OS 不成熟”但可能低估它的社区支持——GitHub issue 里搜gd32第 3 页就有用户提交了gd32f103c8的 PR只是还没 merge。3.8 TencentOS tiny v3.3.0腾讯开源版首次编译失败点tos_config.h中#define TOS_CFG_TASK_PRIO_MAX 32与tos_knl.c中k_prio_table[32]数组大小冲突编译器报array subscript is above array bounds。原因TencentOS tiny 的TOS_CFG_TASK_PRIO_MAX定义必须是 2 的幂次且最大为 32但数组索引从 0 开始32 个优先级对应k_prio_table[32]索引 0~31而TOS_CFG_TASK_PRIO_MAX设为 32 时循环里for (i 0; i TOS_CFG_TASK_PRIO_MAX; i)会访问k_prio_table[32]越界。修复将TOS_CFG_TASK_PRIO_MAX改为31并在tos_config.h中添加#define TOS_CFG_TASK_PRIO_MAX 31。首次烧录死机位置tos_knl_start()后PC 停在0x080002A0反汇编显示是bl tos_task_create的返回地址但tos_task_create()返回K_ERR_NONE说明任务创建成功调度器却没启动。根因TencentOS tiny 的tos_knl_start()末尾有__asm volatile ( cpsie i );但 GD32 的cpsie i指令需配合__DSB()和__ISB()才能确保中断使能生效否则后续svc 0触发 SVC 异常时异常向量未加载。修复在tos_knl_start()末尾添加__DSB(); __ISB(); __asm volatile ( cpsie i ); __DSB(); __ISB();最小闭环验证TencentOS tiny 的 API 命名风格统一全tos_*前缀UART 用tos_uart_write()ADC 用tos_adc_read()封装程度最高。耗时27 分钟。关键体会TencentOS tiny 的“国产化友好”是真实存在的。它的中文文档详尽错误码定义清晰K_ERR_NULL_PTR、K_ERR_TASK_PRIO_INVALID且所有驱动都经过 GD32 实测。你不会误判“国产 RTOS 不如国外”但要注意它的tos_timer_create()默认精度是 10ms不是 1ms这是为降低功耗做的妥协。4. “谁快”的真相不是调度延迟而是“首次成功时间”网上所有“RTOS 性能对比”图表都在比三个数字上下文切换时间ns中断响应延迟ns内存占用KB这些数字在 GD32F103C8T6 上实测如下使用逻辑分析仪抓取 GPIO 电平翻转RTOS上下文切换μs中断响应μsRAM 占用KBFlash 占用KBFreeRTOS1.20.83.112.4Zephyr1.81.14.728.9PX50.90.72.810.2RT-Thread1.50.93.515.6uC/OS-III1.00.753.313.8NuttX2.11.35.235.7RIOT-OS1.61.03.918.3TencentOS0.850.682.69.5看起来 TencentOS 和 PX5 最快NuttX 最慢。但这个表格毫无意义。为什么因为你在产品开发中永远不会去测“上下文切换时间”。你会测的是从需求提出到第一版固件交付用了几天客户反馈“LED 不亮”你定位到是GPIO_Init()里GPIO_MODE_OUT_PP参数传错花了多少分钟OTA 升级失败log 显示malloc failed你查到是heap被printf的 buffer 占满花了多少小时

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

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

免费获取报价