做过多旋翼飞控开发的朋友大概率都有过这种经历:在Gazebo里把PID参数调得漂漂亮亮,飞机悬停稳得像钉在地上,信心满满地换到真机,结果刚解锁推油门,机身就开始高频哆嗦,严重的时候直接原地翻跟头。问题出在哪里?SITL(软件在环仿真)里,PX4固件跑在x86处理器上,传感器数据由仿真器以近乎理想的方式喂进来,调度时序、总线读取、中断优先级统统和真实飞控无关。于是你根本分不清是控制器参数不对、传感器噪声太大,还是电调响应跟不上。硬件在环仿真(HIL)就是为这个场景准备的——把真实飞控板接进仿真回路,用仿真世界的传感器数据喂给它,再把它的控制输出送回仿真世界,让整条链路的实时性、时序和采集噪声暴露在正式试飞之前。这篇文章我完整梳理一遍Simulink与PX4硬件在环仿真的实现路径,覆盖PX4端HITL模式配置、Simulink端动力学模型与MAVLink消息收发搭建、联调排错要点,以及这套平台能扩展的故障注入和自动化测试方法,给正在从纯仿真迈向真机验证的开发者一条可以直接走通的路线。
1. 为什么SITL出不了问题,却在真机上翻车:HIL到底补上了哪一环
1.1 纯软件仿真与硬件在环的本质差异
先把这个概念掰开。SITL(Software-in-the-Loop)里面,PX4是作为Linux/x86原生的进程跑在电脑上的,物理飞控板完全不存在。传感器驱动、I2C/SPI总线、中断处理都被模拟器数据替代或者干脆绕过了。这意味着飞控自身对"时间"的感知是理想化的——任务调度器不会因为总线等待而阻塞,EKF拿到的传感器数据没有真实采样过程中的时序抖动,控制输出经过网络回到仿真器时也没有额外延迟。
HITL(Hardware-in-the-Loop)则把PX4原样编译进真实飞控板,比如Pixhawk 4或者Holybro Pixhawk 6X。板子上的ARM处理器、任务调度器、中断机制全部真实运行,只是传感器数据来源从真实IMU/GPS换成了MAVLink注入的仿真数据。这个区别非常关键:你验证的不只是控制算法,还包括固件在真实硬件上的调度行为、传感器数据被EKF接收后的实际表现、MAVLink链路的通信质量。
我见过不少开发者在SITL里完成了整套姿态控制验证,上真机后第一次起飞就碰到EKF告警、position漂移甚至电机振荡。这些问题的根子大多不是控制律算错了,而是SITL环境下传感器数据的理想程度远高于真机,飞控里很多针对异常数据的安全逻辑根本没有触发。HIL的价值就是把这些"藏在时序和噪声里的问题"提前暴露出来,而且不需要承担炸机风险。
1.2 在HIL闭环里,Simulink到底扮演什么角色
很多人第一次接触HIL时会搞混Simulink和PX4的职责。在Simulink+PX4的硬件在环架构里,PX4是主飞控,跑在真实硬件上,负责姿态估计、控制解算、混控输出。Simulink不是用来当控制器的,而是扮演"虚拟环境+被控对象+传感器"这三个角色:接收PX4发出的执行器指令,解算飞行器刚体动力学,再把计算出的姿态、角速度、位置、气压、GPS信号编码成MAVLink消息,重新喂给PX4。
这个回路和真机飞行的数据流是严格对应的。真机上,IMU和GPS提供测量,PX4内部EKF融合后得到状态估计,控制器输出电机转速指令;在HIL里,Simulink模型相当于一个"可编程的物理世界",PX4感觉自己在飞,实际上是在飞一个数学方程描述的虚拟机体。
如果你想让Simulink做外环控制器、PX4只做底层的姿态稳定和电机输出,那架构上就是另一回事了——那叫控制器快速原型,数据流方向和控制权限分配都不同。为了不混淆,这篇文章全程假设PX4是主控,Simulink是虚拟被控对象,这也是HIL仿真的经典定义。
2. 三种落地路线对比:直连HIL、FMU模型交换还是嵌入式代码集成
2.1 路线A:MAVLink HIL消息直连,最正统的硬件在环
PX4固件原生支持HITL模式。开启后,飞控内部的传感器模块不再从I2C/SPI总线读取真实IMU,而是等待MAVLink的HIL_SENSOR消息注入;imu、气压计、GPS等uORB主题的数据源全部切到虚拟通道。PX4解算出的控制量也不会输出到电调,而是通过HIL_ACTUATOR_CONTROLS消息发回仿真器。
这条路线对固件的侵入最小,不需要改一行PX4代码,只需要Simulink端按照MAVLink协议编解码消息,完成"执行器指令到传感器数据"的换算。这也是我最终选定的方案,因为它保留了完整的PX4原生安全逻辑、Mag校准流程和EKF故障保护机制,跟你将来跑真机时使用的固件完全一致。
2.2 路线B:FMU导出与联合仿真,更适合算法阶段验证
FMI/FMU标准最近几年在飞控圈子越来越常见。可以把Simulink控制模型导出成FMU,也可以把PX4的SITL固件封装成FMU供Simulink调用。这种方式的优点是省去手动编写MAVLink收发逻辑,Simulink和PX4通过标准接口交互变量,开发速度快很多。
但FMU方式有个绕不开的缺陷:导出的PX4模型本质上还是跑在x86处理器上,硬件时序、中断优先级这些"真实感"全丢了。它更适合在SITL阶段快速尝试控制算法、做早期参数扫掠,不适合做"上真机前最后一道验证"。如果你的目标是验证控制律本身,而不是验证飞控硬件和软件链路,选这条路线效率更高。我自己会在HIL之前,先通过FMU把外环控制器的结构和参数空间缩小,避免一上来就在HIL环境下漫无目的地调参。
2.3 路线C:模型代码生成后嵌入PX4,严格说不是HIL但常见
另一种常见做法是用Simulink设计控制器,通过Embedded Coder生成C代码,然后以自定义模块的形式集成到PX4固件中,替换掉原有的控制律模块。这种方式的最终形态是"模型驱动的嵌入式开发",在产品阶段价值很大,因为它把开发和部署统一到了同一条工具链上。
但在HIL仿真的语境里,这条路线的定位有些尴尬。代码集成后你测试的是"融合了自定义控制器的PX4"在硬件上的表现,没法把固件原版控制器当作基准来对照,排查问题时,固件、模型、硬件之间的问题会互相纠缠。如果你要做HIL是为了验证PX4整体的鲁棒性,建议不要选路线C;如果你本来就要交付一个产品化控制器,那路线C值得认真考虑。
2.4 选型建议对比表
| 对比维度 | 路线A(MAVLink直连HIL) | 路线B(FMU联合仿真) | 路线C(代码集成) |
|---|---|---|---|
| 对PX4固件的修改 | 无需修改 | 无,但模型非真机态 | 需集成自定义控制器 |
| 硬件时序真实性 | 完整保留 | 基本丢失 | 完整保留 |
| 传感器链路验证 | 精准 | 弱 | 取决于集成方式 |
| 开发工作量 | 中高(需码MAVLink) | 低 | 高 |
| 适用场景 | 上真机前最关键验证 | 控制算法探索 | 产品化控制器落地 |
我的建议很明确:如果你目标是验证PX4与真实硬件的可靠性,选A;如果你还在早期摸索控制架构,选B;如果你本来就要做产品级嵌入式控制器交付,选C。
3. PX4一侧的HITL模式准备:固件、参数和传感器静默
3.1 固件版本与SYS_HITL参数开启
PX4官方对HITL的支持在1.13及之后的版本都比较稳定,推荐直接选当前官方主线的稳定release版本,避免用太老的1.9、1.10。固件用标准版即可,不需要做任何编译期的特殊配置,HITL是一个运行时功能。
开启方法很简单:用Micro USB连上飞控和QGroundControl,在参数列表里搜索 SYS_HITL,把值从0改成1,然后重启飞控。QGC新版界面里右上角也有模拟模式入口,选择Hardware In The Loop后,会自动引导你设置对应参数。
设置完重启后,通过MAVLink Console输入:
param show SYS_HITL如果输出是1,说明HITL模式已经启用。这时飞控会打印类似"waiting for HIL sensor data"的提示,说明它已经准备好接收仿真器数据。
3.2 HITL模式下EKF的准备与传感器"静默"
这里有个很多人忽视的细节:HITL模式下,真实传感器数据源虽然被切走了,但EKF的初始化和校准流程并没有被豁免。第一次接上QGC进入HITL后,我强烈建议老老实实做一遍加速度计、陀螺仪、磁力计的标准校准,以及水平校准。虽然这些校准在HITL中不会真正影响传感器读数,但它会初始化EKF里的初始偏置估计,跳过这一步,EKF状态在启动后很容易因为初始偏置异常而长时间处于不健康状态。
磁力计校准要特别小心。电脑、调试器、电源线在地面测试时都会产生磁场干扰,如果仿真传感器模型给的是一个固定磁场方向的简单向量,校准过程会变得很奇怪——读数一直不变或者缓慢漂移。我的做法是:先不启用磁力计主导的航向估计,把EKF2_AID_MASK里的磁力计辅助项先关掉,只保留IMU主导的姿态估计,等HIL链路完全稳定后再打开磁力计项。
EKF2_AID_MASK的值按PX4文档配置即可。如果只做姿态控制,可以暂时去掉GPS辅助,减少对HIL_GPS消息的依赖;如果要做位置控制,GPS的辅助功能必须保留,并且要确保HIL_GPS消息质量足够高。
3.3 通信链路设计:串口波特率与消息通道
HIL模式下,Simulink和飞控之间的数据交换链路是整个系统最容易出问题的地方。常见方案是串口直连和UDP网络连接两种。
如果走串口,TELEM2口是最常用的HIL串口,波特率必须设成921600,不能沿用默认的115200。为什么?HIL_SENSOR消息大约70到80字节,以250Hz发送,每秒的数据量接近20KB。115200波特率的有效数据吞吐率只有约11.5KB每秒,连理论值都喂不满;921600才有足够余量。我第一次跑HIL时就因为手里只有115200的USB-TTL模块,眼睁睁看着PX4报传感器超时,换了高波特率模块后立刻恢复正常。
如果走UDP,飞控需要挂一个能接入网络的模块(比如通过UART连接的WiFi模块),或者直接把飞控USB连到电脑,通过MAVLink over UDP转发。UDP的好处是带宽大、不容易因为字节粒度的阻塞丢帧,而且Simulink天然支持UDP收发,代码写起来比串口简单。缺点是网络栈本身会引入抖动,同时你必须先确定防火墙没有拦截飞控与电脑之间的UDP包——这个坑我踩过,排查了半天才发现是Windows防火墙把MAVLink端口挡了,Simulink的发送模块压根没收到飞控的任何回包。
4. Simulink端动力学模型与MAVLink收发实现
4.1 模型顶层结构:从执行器输出到传感器数据
Simulink模型的顶层可以做得很规整,核心链路就一条:UDP/串口接收MAVLink消息,解出HIL_ACTUATOR_CONTROLS里的执行器指令,送入飞行器动力学模型,算完状态后再经过传感器模型生成陀螺、加速度计、磁力计、气压、GPS数据,最后通过MAVLink编码发回PX4。
我推荐用Aerospace Blockset里的6DoF(Quaternion)刚体动力学模块作为机体模型。它的输入是三轴力矩和总拉力,输出是位置、速度、姿态四元数和角速度。关键在于怎么把PX4输出的HIL_ACTUATOR_CONTROLS换算成力和力矩。
HIL_ACTUATOR_CONTROLS里前四个控制值分别对应横滚、俯仰、偏航、油门,取值范围在-1到1附近。对多旋翼而言,这四个值经过PX4的混控器语义对应到执行机构后,需要等比例映射到推力系数和力矩系数上。我通常的做法是:
- 油门通道映射到总拉力基值,注意要把重力配平的静推力考虑进去,悬停时的油门通常对应约1g的重力加速度补偿;
- 横滚和俯仰通道映射到绕机体X/Y轴的力矩,缩放系数参考电机的力臂长度和推力系数估算;
- 偏航通道映射到绕机体Z轴的反扭矩差。
这个映射不需要一开始就特别精确,因为你是闭环仿真,PX4的姿态控制器会主动修正静差。但如果模型偏得太离谱——比如把拉力方向搞反了,或者把横滚和俯仰弄混了——飞控再怎么调也无法收敛,这类低级错误往往也是新手最容易踩的坑。
4.2 必须打通的六条核心MAVLink消息
HIL链路的核心是MAVLink消息,以下是必须打通的六类消息,以及我实际使用的频率和字段要点:
| 消息名 | 消息ID | 方向 | 频率建议 | 关键字段说明 |
|---|---|---|---|---|
| HIL_SENSOR | 107 | Simulink -> PX4 | 250Hz | 陀螺、加速度、磁力、气压、空速、temperature、fields_updated |
| HIL_GPS | 113 | Simulink -> PX4 | 50Hz | 经纬度、高度、速度分量、fix_type、satellites_visible |
| HIL_RC_INPUTS_RAW | 92 | Simulink -> PX4 | 50Hz | 远程遥控通道值(PWM数值),解锁时需要有效RC信号 |
| SYSTEM_TIME | 2 | Simulink -> PX4 | 1Hz | 对齐PX4的time_boot_ms,确保时间基准一致 |
| HIL_ACTUATOR_CONTROLS | 93 | PX4 -> Simulink | 250Hz | 执行机构控制量,前4个是横滚/俯仰/偏航/油门 |
| HIL_STATE_QUATERNION | 115 | PX4 -> Simulink | 50Hz | PX4内部状态估计回传,用于监控和对比 |
编解码MAVLink最省力的方式是用现成的mavlink C库,通过Simulink的Legacy Code Tool包装成S-Function调用。如果为了快速验证,也可以在MATLAB Function里手写字节拼包,但一定要小心MAVLink的CRC校验和字节序——MAVLink 1使用X.25 CRC,MAVLink 2还有额外的校验字节,一旦CRC对不上,PX4会静默丢弃,不会给你任何报错。第一次联调时,我建议先用Wireshark或者tcpdump抓包,确认Simulink发出去的包确实是通过MAVLink协议编码的合法帧。
4.3 实时性讨论:外部模式、UDP与调度抖动
Simulink模型在普通Windows/Linux系统上跑,本质上不是一个硬实时系统。Windows的定时器分辨率在默认情况下大约是15.6ms,这意味着如果你直接让Simulink以250Hz的速率发送HIL_SENSOR,发送间隔会出现比较大的抖动。PX4的传感器模块对数据到达间隔有一定容忍度,但如果抖动过大或者某段时间内完全没数据,就会触发传感器超时。
几种缓解方案按效果排序:
- Simulink模型使用定步长离散求解器,步长建议1ms,然后用Rate Transition块把传感器数据降采样到250Hz发送;
- 使用UDP发送而非串口,UDP自带缓冲,允许短时间内的小抖动不丢包;
- 不要在这个场景里依赖Simulink外部模式来实时调参。外部模式本质上是宿主机和目标机的通信链路,它本身会增加额外的调度开销和延迟。如果模型就是HIL的数据源,外部模式会让抖动问题雪上加霜。更好的做法是让模型通过UDP周期输出状态日志,用Simulink Dashboard或MATLAB Analysis离线分析;
- 如果要做严格的真实时HIL,应该把Simulink模型生成C代码后部署到带实时扩展的工控机(比如Speedgoat或者装PREEMPT_RT补丁的Linux机器)。
我自己在非实时系统上跑HIL的经验是:250Hz的HIL_SENSOR配合1ms的模型步长,在普通Ubuntu 22.04主机上,发送间隔的标准差可以控制在0.5ms以内,这对PX4验证来说已经足够。再低频率——比如降到200Hz——就需要调低PX4的IMU预测频率,否则EKF会因为数据稀疏而表现变差。
5. 联调实录:从"连不上"到"解锁起飞"遇到的四个坎
5.1 坎一:PX4一直在报传感器超时,无法进入预飞行
现象:QGC面板一片红,报PREFLIGHT FAIL: SENSOR,MAVLink Console里连续输出sensor missing。
这次排错花了大半天。排查链路是这样的:先确认SYS_HITL=1并重启,没问题;然后确认Simulink确实在发HIL_SENSOR,用tcpdump抓包看UDP端口,能抓到107号消息,但内容是否合法不确定;再检查飞控侧,发现飞控压根没收到任何消息。
最终定位到两个叠加的问题。第一个是Windows防火墙拦截了同一网段下的UDP通信,Simulink发包没有报错,但数据根本没离开本机网卡;第二个是MAVLink的sysid/compid没配对,Simulink发出去的包系统ID是255,而飞控配置里MAV_SYS_ID是1,PX4对这种来源的消息直接不予理睬。
经验:联调的第一步永远是让两个端点之间有一个能互相确认消息的中间视图。先用QGC的MAVLink Console确认飞控能收到HEARTBEAT以外的任何MAVLink消息,再用抓包软件确认Simulink发出去的消息内容,两端对上了再谈控制。
5.2 坎二:EKF状态不健康,解锁被拒
现象:能连上飞控,消息也在跑,但QGC姿态仪表盘总有一半是红的,解锁键点了没反应,或者解锁后几秒钟自动跳回HOLD。
查了EKF2的状态日志,发现加速度计和磁力计的innovation值特别大。原因是仿真器发过来的传感器数据没有加噪声,也没有合理的初始偏置——EKF拿到这种"过于干净"的数据反而不容易收敛,因为真实世界里任何传感器都有噪声和偏置,EKF依赖这些统计特性去估计状态。
解决方法是给传感器模型加入合适的噪声模型。我在模型里为陀螺加了约0.003度每秒每根号赫兹的白噪声和常值偏置,加速度计加了约0.05米每二次方秒的噪声,磁力计加了中等强度的白噪声。加完噪声后,EKF立刻进入normal状态。
另外,如果一直不执行QGC的校准向导步骤,EKF在启动后长时间停留在unhealthy状态。即使传感器数据来自仿真器,也不要跳过这个初始化步骤。
5.3 坎三:时间戳不同步导致数据被丢弃
现象:MAVLink消息计数在涨,状态估计器却一动不动,日志里频繁出现timestamp error。
原因很隐蔽。HIL_SENSOR消息里的time_usec字段是微秒级时间戳,PX4会用这个时间戳做数据时序处理。我的Simulink模型一开始直接用电脑的系统时间填入,但这个时间和飞控上电后的time_boot_ms基准差了很远,PX4判定时间戳不连续,把大多数消息当成无效数据丢掉。
解决方法是Simulink端发一条SYSTEM_TIME消息给PX4,把基准时间对齐。更稳妥的做法是:模型启动时先读取飞控返回的HEARTBEAT消息里的time_boot_ms字段,以此为基准计算自己的相对时间,再填入HIL_SENSOR。这样一来,Simulink和PX4的时钟在同一个参考系下,PX4不会再因为时间戳跳变丢弃数据。
5.4 坎四:控制频率和模型步长的匹配
现象:解锁成功后,飞机悬停出现明显的高频抖动,频率大概十几赫兹,和电机转速并不相同。
排查后发现是模型步长和HIL_SENSOR发送频率不匹配。我最初把Simulink主步长设成了5ms(200Hz),然后以200Hz发送HIL_SENSOR,PX4的IMU控制循环默认是250Hz,拿到的传感器数据更新率低于自身控制频率,EKF和控制器的相位裕度受到严重影响。
把模型主步长降到1ms(1000Hz),用Rate Transition块把HIL_SENSOR的发送频率锁定在250Hz后,抖动基本消失。之后我又尝试把发送频率提到400Hz,效果更平滑,但串口链路已经开始接近带宽上限,所以如果没有特别需要,250Hz是一个性价比最高的选择。
6. 在HIL平台上还能做什么:故障注入、测试自动化与跨领域延伸
6.1 常见故障注入
HIL平台最大的优势是你可以随意"制造事端",这是真机测试没法做到的。实操中值得做的故障注入类型包括:
- GPS断链:突然停止发送HIL_GPS消息,观察PX4从定位模式回退到姿态模式的逻辑是否正常,位置估计是否漂移,切换时间是否符合预期;
- 磁力计干扰:在Simulink里给磁力计数据加上一个阶跃性偏置,模拟飞控附近突然出现强磁场源,观察EKF航向估计的漂移和恢复过程;
- 单电机饱和:把某个执行器输出手动限幅,模拟真实情况下电机堵转或电调损坏,看PX4的混控补偿机制如何处理;
- 传感器噪声放大:把陀螺仪白噪声方差临时放大5倍,模拟高温振动环境下IMU性能下降,验证姿态控制在传感器质量恶化时的鲁棒性。
每一类故障注入都应该和真机试飞的测试场景对应起来,跑HIL不是图新鲜,而是为了在零风险条件下积累飞控对不同故障的响应数据。把故障注入的时机和结果记录成时间线,后续对比真机表现时会很有价值。
6.2 从手调到自动扫参
HIL环境没有炸机风险,非常适合自动化参数扫掠。我的做法是:用一个MATLAB脚本驱动Simulink模型,上一轮跑完后,通过MAVLink PARAM_SET命令写一组新的PID参数到PX4,等EKF重新健康后再次解锁并执行标准试飞动作。重复这个循环,就能在一个下午内测试十几组参数组合。
要注意,自动扫参时必须把起飞位置、起飞准备时间、任务动作序列完全固定,否则仿真结果之间的差异无法归因于参数变化。日志方面,Simulink记录的时间序列需要和PX4的ULog按时间戳对齐,我一般用MAVLink的SYSTEM_TIME和HIL_STATE_QUATERNION作为对齐锚点,两边时间戳完全一致时才认为该轮数据有效。
Simulink的UDP收发天然适合这种自动化模式。脚本控制Simulink模型的启动和停止,通过MATLAB API读取模型里的记录模块数据,再结合PX4的ULog做对比分析。整个过程无需人工干预,效率比手动操作QGC高一个数量级。
6.3 车辆HIL、转向台架与Simulink生态的共性
这套"HIL模式+Simulink模型+MAVLink/总线消息注入"的方法论,在很多其他行业都能看到同构实现。汽车转向台架HIL就是一个典型例子:真实ECU或域控制器通过CAN总线连接到负载模拟器和转向机器人,Simulink配合CarSim或AMEsim解算整车动力学,再把方向盘扭矩、车速、轮速等信息转换成传感器信号喂给控制器。它的架构和PX4的HIL完全是一个套路,只是MAVLink换成了CAN/以太网,无人机动力学换成了整车模型。
如果你之后从飞控转向域控制器、VCU或者底盘HIL,方法论是高度可迁移的:Simulink既可以是被控对象仿真器,也可以作为测试管理器负责场景编排、数据回灌和自动判据。搜到的很多关于VCU控制策略Simulink建模、CAN报文故障诊断Simulink、CANoe联合仿真的工作,本质上都在复用这一套信号级仿真与故障注入的思想。
我现在养成的习惯是,任何一次在SITL里优化过的控制器参数,在提交真机试飞前都会先跑一遍HIL,至少让它完成起飞、悬停、位置控制、降落整套动作,并且刻意在中间注入一两个故障,看飞控怎么处理。很多问题不是算法本身,而是隐藏在链路里的时序和数据质量问题,只有把真实硬件挪进回路,这些问题才会现形。这套Simulink+PX4的HIL链路给了我极大的安全感,也希望这篇文章能帮你少走几个我走过的弯路。