在嵌入式飞控这个圈子里,ArduPilot 的每一个版本迭代背后,都隐藏着一个容易被忽视却又无比关键的底层决策:操作系统选型。最近几年,如果你打开 ArduPilot 的源码目录,或者准备给 Pixhawk 系列飞控刷固件,会发现一个反复出现的名字——ChibiOS。很多刚入门的开发者会有疑问:飞控为什么要专门选一个 RTOS?ArduPilot 早期用的不是别的系统吗?为什么最后大家都转向了 ChibiOS?
这篇文章不打算给你堆一堆枯燥的源码分析,我想从一个多年折腾飞控和嵌入式系统的从业者角度,聊聊 ArduPilot 在 RTOS 选型上踩过的坑、做过的权衡,以及 ChibiOS 到底凭什么能成为现代飞控的首选。无论你是准备基于 ArduPilot 做二次开发,还是正在为自家飞控挑 RTOS,或者只是对 “RTOS 和 Linux 有什么区别” 这类基础问题感兴趣,这篇文章都能给你一个比较完整的答案。
1. 飞控对操作系统的要求:为什么 APM 非要换掉自己的“内核”
1.1 飞控的实时性到底卡在哪里
飞控本质上是一个高速闭环控制系统。它需要在极短的时间内读取传感器数据(陀螺仪、加速度计、气压计、GPS 等),运行姿态解算和控制算法,然后输出 PWM 或 DShot 信号去驱动电机。这个“极短的时间”不是一句空话,而是硬性的毫秒级甚至微秒级要求。
以典型的 400Hz 姿态控制频率为例,控制系统每 2.5ms 就要完成一次完整的“传感器读取—姿态更新—控制计算—PWM 输出”流程。如果操作系统在这个节骨眼上突然跑去处理一个文件读写、网络请求或者界面刷新,控制回路就会产生抖动,轻则飞机漂移,重则直接炸机。
这正是桌面级操作系统和 RTOS 最本质的区别。Linux 这类通用系统追求的是“平均吞吐量”,它会用各种调度算法公平地分配 CPU 时间,任何一个进程都可能在某个瞬间被挂起。而一个合格的飞控 RTOS 追求的是“确定性”,也就是实时操作系统核心指标:在最坏情况下,每个任务的响应时间必须是可预测的。它不是“尽量快”,而是“必须在这个时间内完成,否则系统就不安全”。
ArduPilot 作为一套支持几十种硬件平台、涵盖固定翼、多旋翼、直升机、无人船、无人车的开源自动驾驶系统,它的底层必须具备这种严苛的确定性。而 ChibiOS 恰好就是为这种场景设计的极简实时内核。
1.2 从单线程到多任务:ArduPilot 的调度模型演进
很多人以为 ArduPilot 是从 Linux 起家的,其实这是个刻板印象。ArduPilot 和它的 AC3(ArduCopter 3.x)时代,主要运行在 Arduino 的裸机环境里。早期的 APM 飞控用的是 8 位 AVR 单片机,主频只有 16MHz,整个程序就是一个巨大的loop()循环,靠一堆if (millis() - last_time > interval)来判断该执行哪个任务。
这种经典的超级循环(Super Loop)架构在资源极度受限的 MCU 上非常实用,代码简单、没有上下文切换开销。但随着 Pixhawk 系列硬件的出现,情况变了。STM32 F4/F7 系列芯片拥有 Cortex-M4/M7 内核、主频 168MHz 甚至更高,Flash 和 RAM 容量也成倍增长。仅仅跑一个超级循环显然浪费了这颗强芯的潜力,而且把姿态控制、GPS 解析、遥测传输、任务调度全部塞进一个循环里,会出现很严重的耦合问题:GPS 的串口解析稍微拖慢一下,姿态环路的周期就被拉长,这个隐患极高。
于是 ArduPilot 开始引入多线程模型。不同的功能模块被拆分成独立的线程,由操作系统统一调度。这时候,选择一个合适的 RTOS 就成了最关键的一步棋。ArduPilot 的开发者并不是只做一个移植,他们要的是一个跨硬件平台都能稳定运行、实时性有保证、而且不会给 MCU 带来太大资源负担的操作系统抽象层。这个选择一旦做错,后面的硬件适配和社区扩展都将举步维艰。
2. ArduPilot 的 RTOS 选型史:NuttX、FreeRTOS 与 ChibiOS 的博弈
2.1 当年 ArduPilot 用的是什么,为什么要换
在 ChibiOS 之前,ArduPilot 的主流 RTOS 其实是 NuttX。Pixhawk 1 时代,PX4 和 ArduPilot 还共用 FMUv2 底层时,NuttX 就是那套硬件的标准操作系统。NuttX 虽然小众,但它提供了完整的 POSIX 接口,这意味着很多 Linux 下的程序可以直接交叉编译到飞控上,这在当时是很大的卖点。
然而,在 ArduPilot 社区持续维护之后,NuttX 的问题慢慢显现出来了。首先是构建系统非常复杂,这是最直接的问题:NuttX 需要专门配置内核、板级支持包和用户态组件,版本更新频繁,每次升级都要解决一堆依赖问题;其次 NuttX 面向更复杂的 RTOS 场景,内核体积相对较大,在只有 192KB RAM 的早期飞控板上,留给用户任务的内存空间会变得捉襟见肘;再者就是社区维护者的生态问题,ArduPilot 需要的是一个能被自己完全掌控、升级节奏跟得上的操作系统,而当时的 NuttX 主导团队跟无人机行业离得比较远,版本的演进步伐和飞控需求经常对不上号。
换一句话说,ArduPilot 需要的是一个“小而美”的嵌入式内核,而不是一个“功能齐全但体型臃肿”的迷你 Linux。NuttX 在国际上很多安全关键领域确实表现不错,但对 ArduPilot 这类硬件资源高度受限、又极度依赖社区贡献的飞控项目来说,它并不是一个最舒适的选项。
2.2 为什么是 ChibiOS 而不是 FreeRTOS 或 Zephyr
这里有人会问:FreeRTOS 不是 STM32 上最流行的 RTOS 吗?它的生态庞大,资料多到学不完,为什么不直接选 FreeRTOS?这个问题的答案要结合 ArduPilot 的实际需求来聊。
我在日常交流中,最常被人问到的一件事就是:到底怎么选 RTOS,是选用户多的还是选功能强的?我的看法是,对于飞控这种项目,你需要考虑四个维度:内核确定性、内存占用、驱动的可移植性、以及你对内核源码的掌控力。把这四个维度放到 FreeRTOS 和 ChibiOS 面前做对比,结论就非常清晰了。
| 对比维度 | FreeRTOS | ChibiOS | NuttX |
|---|---|---|---|
| 内核体积 | 极小 | 极小 | 较大 |
| 调度器确定性 | 较强 | 极强 | 较强 |
| 外设驱动库 | 自带较少,多靠厂商 SDK | 自带丰富 HAL,支持 RT 驱动模型 | POSIX 完备,但驱动适配复杂 |
| 构建集成难度 | 简单 | 简单 | 复杂 |
| 许可证 | MIT | GPL3 + 商业许可 | BSD |
| 对飞控任务的适配度 | 需大量自己封装 | 原生支持高精度定时器与并发原语 | 需裁剪和适配 |
ChibiOS 赢在哪?首先是它的内核设计非常贴近硬实时场景,支持完整的优先级继承机制,能有效避免优先级反转问题。同时它提供了一套完整的 RT 外设驱动框架,包括 SPI、I2C、UART、CAN、ADC、DMA 等都能在操作系统调度下稳定工作,这对飞控来说等于天生就配套好了。
还有一个很实际的点,ChibiOS 的许可证虽然是 GPL3,但 ArduPilot 本身也是开源协议,两者兼容。而且对于不愿意开源商业产品的公司,也可以购买 ChibiOS 的商业许可。这种双许可模式在嵌入式圈子里非常常见,既能保证开源社区的活跃,也不妨碍商用。更关键的是,ArduPilot 社区对 ChibiOS 的掌控力极强,他们甚至可以为了飞控的需求直接去修改内核调度器的行为,这在其他 RTOS 上只能打补丁。实际测试下来,ChibiOS 的内核中断延迟和上下文切换时间都在微秒级别,稳定性也可以打高分。正是这种级别的掌控力,让 ChibiOS 在候选名单里彻底胜出。
3. ChibiOS 在现代飞控中的具体落地:HAL 抽象与线程模型
3.1 HAL 层:换 RTOS 为什么换得这么干净
如果你去读 ArduPilot 的源码,你会发现一个很有意思的现象:大量代码都是高度抽象的,真正直接跟 RTOS 打交道的地方少之又少。这就是 ArduPilot 的 HAL(Hardware Abstraction Layer,硬件抽象层)设计。所有的线程创建、信号量、互斥锁、定时器、调度延迟,都被封装在一个统一的接口之下。而针对不同底层系统,提供了各自的实现模块。
ChibiOS 的底层实现藏在libraries/AP_HAL_ChibiOS这个目录里。它的职责就是把 ChibiOS 的 API 翻译成 ArduPilot 统一的 HAL 接口。例如,ArduPilot 的调度器要求一个线程能够按固定频率周期运行,ChibiOS 就用它的chThdCreateStatic和系统 tick 定时器来实现;ArduPilot 的 I2C 驱动需要带超时保护,ChibiOS 就提供了带超时参数的消息传递机制。
这种分层设计的好处非常明显。它意味着 ArduPilot 在切换到 ChibiOS 的时候,不需要把上层几百万行代码重写一遍,只需要替换掉底层那个“适配器”。同时这也让普通开发者能够在 PC 上的 Linux 仿真环境里(即 SITL,Software In The Loop)跑同一套代码,模拟整套飞控逻辑,而真的到硬件上时,又可以通过 ChibiOS 的实时内核来保证时序。
所以,你去看 ArduPilot 的主线仓库,几乎所有官方支持的板子上都默认使用 ChibiOS 驱动栈。这种生态一旦形成,硬件厂商在做新板卡时,往往第一时间就是去适配 ChibiOS 的板级配置,而不是自己另起炉灶。这种“操作系统换得干净”的背后,是整个社区在过去几年里一点点打磨出来的工程结晶。
3.2 线程优先级、内存池与定时器:核心配置拆解
ChibiOS 在飞控上跑得好不好,很大程度取决于线程配置和内存管理方式。我记得自己在第一次写 ChibiOS 线程的时候,就踩过一个典型的坑:默认栈大小设得太小,结果飞控跑着跑着就 stack overflow,系统直接 HardFault。
在 ChibiOS 里创建一个线程,核心是静态分配栈空间和线程控制块。ArduPilot 在多个任务之间精心分配了优先级,IMA(惯性测量单元)相关的任务拥有最高优先级,其次是控制输出任务,然后才是遥测和数据记录。这种优先级安排不是拍脑袋定的,它遵循的是一个基本原则:越容易导致失控的任务,优先级越高。
举个简单的代码示例来说明线程创建方式:
static THD_WORKING_AREA(io_thread_wa, 2048); static THD_FUNCTION(io_thread_fn, arg) { while (true) { // 处理串口收发或传感器读取 hal.scheduler->delay_microseconds(100); } } // 在其他初始化函数中: chThdCreateStatic(io_thread_wa, sizeof(io_thread_wa), APM_IO_PRIORITY, io_thread_fn, nullptr);注意这里用的是THD_WORKING_AREA静态分配,不是malloc动态分配。飞控系统基本都会避开动态内存分配,因为堆碎片是一个隐形的定时炸弹。ChibiOS 支持内存池(Memory Pool),ArduPilot 的很多 DMA 缓冲区就是通过这种方式预分配的。
定时器方面,ChibiOS 支持两种模式:一种是内核 tick 定时器,单位是系统 Tick;另一种是硬件定时器,能提供微秒级的延时。ArduPilot 的底层延时和任务周期性调度基本依赖后者,这样才能保证主控频率的精度。配置不当、Tick 频率太低,往往会导致调度抖动,这在飞控上会造成难以察觉的波形畸变,所以我个人的建议是,系统 Tick 至少配置在 1000Hz 以上。
3.3 MAVLink、UART 和传感器数据链路:ChibiOS 驱动的实测体验
聊完线程,再看数据链路。ArduPilot 和地面站之间通信靠的是 MAVLink 协议,同时通过串口或者 CAN 总线跟外设打交道。很多人在做二次开发时会发现,往飞控里发航点信息,用的是MAV_CMD_NAV_WAYPOINT之类的消息,而这条消息从地面站到飞控再到内存里的任务列表,中间要跨越串口中断、ChibiOS 的消息队列和任务调度等多个环节。
我最早在 STM32 上做 MAVLink 转发时,就试过用裸机方式在中断里解析数据,速度确实快,但极其痛苦:每个外设的状态都要自己管理,一旦数据多了,中断优先级稍微设置失误,整个系统就乱套。后来切到 ChibiOS 的 UART 驱动,利用 DMA 加接收回调,把数据放进消息队列,再唤醒一个高优先级线程去做协议解析和航点存储,代码忽然变得清爽且好维护多了。
ChibiOS 对串口的典型配置是这样的:通过sdStart初始化 Serial Driver,并注册接收回调函数;当 DMA 接收到一个完整的数据帧时,回调函数通知对应的信号量,然后干活线程被唤醒。ArduPilot 的诸多 UART 通道就是这么跑通的。实测下来,在 115200 波特率下保持长时间收发,基本不会出现丢包或者数据错位。相比之下,过度依赖中断做大量数据处理的做法,在系统高负载时很容易导致中断延迟不公平,从而影响控制回路。
4. 常见问题排查与实战心得
4.1 线程卡死、堆栈溢出和优先级反转的排查
在实际开发中,错误是难免的。我总结一下在 ArduPilot + ChibiOS 环境下最常见的三个问题:线程卡死、堆栈溢出、优先级反转。
线程卡死的典型表现是飞控突然失去响应,PWM 输出定格,地面站掉线。排查的第一步是确认是不是某个线程在不该阻塞的地方调用了阻塞型 API,比如在中断上下文里试图去获取互斥锁,或者在一个高优先级线程里等待一个永远不会被释放的信号量。ChibiOS 提供了一些内核调试选项,比如CH_DBG_ENABLE_CHECKS和CH_DBG_ENABLE_ASSERTS,打开后能帮你捕获很多非法操作。但注意,这些选项会显著增加中断延迟和系统负载,实机飞行时不要开。
堆栈溢出往往是内存配置不当。ChibiOS 有chDbgCheck和线程栈高水位检查的功能,可以通过chThdGetWorkingArea查看栈剩余空间。我自己习惯的做法是,每个线程初始化完成后先跑 24 小时测试,把栈空间使用量打出来,再根据实际峰值增加 30% 余量。千万别按理想值去设栈大小,因为函数调用嵌套、格式化打印等因素都会消耗大量栈空间。
优先级反转更危险也更隐蔽。假设有一个低优先级线程占用着 I2C 总线,这个线程被中断打断了;而高优先级线程想要立刻占用 I2C 总线,但由于低优先级线程还没执行完,它就会被卡住。如果中间还有一个中优先级线程一直抢占了 CPU,情况就变成高优先级线程在等低优先级线程,低优先级线程又没机会跑,系统就僵在那了。ChibiOS 的互斥锁支持优先级继承机制,它能临时把持锁者的优先级提升到等待者级别,从而避免这种死锁。所以在 ArduPilot 里编写自定义驱动时,尽量使用带优先级继承的chMtxLock,而不是简单的自旋锁。
4.2 从 NuttX 迁移到 ChibiOS 需要注意的差异
如果你手头还有一块老的 Pixhawk 板子,想自己把 ArduPilot 的底层从 NuttX 换成 ChibiOS,或者正在阅读两种系统的历史代码,有几个差异点值得留意。
首先是文件系统。NuttX 自带类 POSIX 文件系统,可以直接挂载 SD 卡并读写日志。ChibiOS 没有原生文件系统,ArduPilot 的做法是自己封装了一个 AP_Filesystem 模块,底层通过 ChibiOS 的块设备驱动对接 SD 卡。也就是说,日志记录这个功能对用户来说没有消失,但底层实现已经完全不同了。在 NuttX 上,你可以在代码里直接写open()、read()、write(),但是切到 ChibiOS 后,就必须用 ArduPilot 的AP_ROMFS或AP_Filesystem接口。
其次是启动流程。NuttX 的启动代码由系统自己接管,先初始化内核,再启动主线程。而 ChibiOS 的启动更多是板级代码说了算,在main()函数中先完成时钟、GPIO、电源管理初始化,然后调用chSysInit()启动内核,最后回到 ArduPilot 的启动流程。这个过程对移植新板卡尤其重要,所有外设时钟和引脚映射都需要在hwdef.dat文件里逐一配置。稍微配置错一个引脚映射,飞控可能能上电,但 GPS 完全认不到串口。
4.3 ChibiOS 生态下的调优建议
我在多个项目中实际使用 ChibiOS 作为飞控和机器人的底层 RTOS,也发现了一些可复用的调优经验,这里分享三个要点。
第一,合理设置系统 Tick 频率。ChibiOS 的默认 Tick 一般是 1000Hz,但在飞控场景下,如果你希望获得更精细的延时控制,可以考虑提升到 2000Hz 甚至 10000Hz。注意,Tick 频率越高,内核定时器中断越频繁,CPU 占用就越高,这会挤占控制计算的时间窗口。我经过多轮对比后认为,对大多数飞控任务,1000Hz 到 2000Hz 是最平衡的区间,既不会导致调度抖动,也不会令 CPU 空转。
第二,用硬件定时器处理高精度 PWM 和传感器同步。ChibiOS 内部的软件定时器虽然方便,但它依赖系统 Tick,在极端情况下可能产生几百微秒的误差。PWM 输出最好通过 STM32 的 TIM 定时器直接产生,由 DMA 控制更新,这样输出的波形抖动会小很多。ArduPilot 的主输出通道就是这样设计的,它绕过了 RTOS 调度,直接由硬件触发,这样即使系统忙乱,舵机信号也依然稳定。
第三,要注意中断优先级与内核临界区的关系。ChibiOS 把中断分为了普通中断和快速中断,快速中断可以抢占内核的临界区,但普通中断不行。如果你的传感器驱动需要非常低延迟的响应,建议将它的中断优先级设置为快速中断。但这时你的中断服务函数里不能调用任何 ChibiOS 的阻塞式 API,否则很容易破坏内核状态。这个坑我见过太多次了,凡是中断里调用了chSemSignal的不当用法,最后都会莫名其妙地死机。
5. 如果你也想自己上手:从哪开始更顺
讲了这么多选型和分析,最后来点实际的。如果你读完这篇文章,想感受一下 ArduPilot + ChibiOS 这套体系是怎么工作的,我的建议是从“组装一个最小系统”开始,而不是一头扎进完整飞控。
一个比较省力的路径是,先拿一块 STM32F405 核心板(比如常见的 OpenPilot Revolution 兼容板),在 ArduPilot 的 hwdef 里新建一个板级定义文件。按照官方文档逐步配置引脚映射,编译下载,然后接一个接收机和一个电调,先让它能解锁、能输出 PWM。这个过程能让你对整个 ChibiOS 的启动流程和线程模型快速建立直觉。
更深入一点,你可以试着在 SITL 仿真模式里自己写一个外部 MAVLink 模块,通过 UDP 往飞控发送航点消息,观察任务队列的变化。然后再把同一套逻辑移植到真实的 ChibiOS 硬件环境,用串口转 USB 调试。这种从虚拟到真实的对照过程,能帮你快速理解 RTOS 在飞控里的角色边界:哪些功能是上层算法该做的,哪些又是底层调度该负责的。
我个人在实际操作中最大的体会是:不要一上来就纠结用什么 RTOS 是最好的,关键是你能否驾驭它。ChibiOS 对 ArduPilot 来说之所以能成为首选,恰恰是因为它让飞控开发者能完全掌握内核的行为,而不是被操作系统牵着鼻子走。这种掌控感,才是保证一架无人机稳定飞行的底层基石。