4、SITL仿真基础:工作原理、启动脚本、时间与日志

这一章,我们正式进入SITL仿真的核心地带。说实话,很多初学者把SITL当成一个黑盒子,敲个命令飞起来就完事了。但作为工程师,你得知道它背后怎么转的。我在项目中遇到过好几次,仿真跑得挺好,一上真机就炸,最后发现是SITL的时间模型没搞对。所以,咱们得把底层的逻辑理清楚。

4.1 SITL到底是怎么工作的?

SITL的全称是Software In The Loop。说白了,就是把飞控的代码,直接在电脑上编译成一个可执行程序。它不依赖任何真实的硬件。

你想想看,真实的飞控板上有STM32芯片,有传感器(IMU、GPS、气压计),有PWM输出。SITL干了什么事?它用你电脑的CPU模拟了那个STM32,用操作系统的时间模拟了传感器数据流,用UDP网络包模拟了PWM信号和遥控器输入。

核心流程其实就三步:

  • 编译成PC程序:把原本跑在ARM架构上的PX4/ArduPilot代码,用本地编译器(gcc/clang)编译成x86架构的可执行文件。
  • 模拟传感器驱动:代码里原本读取I2C总线获取IMU数据的地方,被替换成了从UDP端口读取仿真数据。
  • 对接仿真引擎:通过MAVLink协议,把飞控算出来的电机控制量(比如油门值)发给Gazebo或JSBSim,然后仿真引擎把新的姿态和位置反馈回来。

一句话总结:SITL就是一个“硬件抽象层”。它让飞控代码以为自己在操作真实的传感器和电机,实际上是在跟操作系统里的网络端口和文件描述符打交道。

下面这张图,是我自己画的一个SITL内部数据流,你看一眼就明白了。

SITL 仿真内部数据流图 飞控代码 (姿态控制/导航/日志) 硬件抽象层 (HAL) (UDP模拟I2C/SPI) 操作系统 & 网络 (UDP端口/时钟/文件) 传感器请求 网络数据包 仿真引擎 (Gazebo/JSBSim) 电机控制量 传感器反馈 地面站 (QGC/MP) MAVLink遥测

4.2 启动脚本分析:到底执行了什么?

我们平时用make px4_sitl gazebo或者直接运行./ArduCopter.elf,背后其实有一整套脚本在干活。我建议你花10分钟读一下启动日志,里面信息量很大。

启动脚本主要干了这几件事:

  1. 设置环境变量:比如PX4_SIM_MODEL定义了用哪款无人机模型(iris、plane、rover)。
  2. 创建虚拟传感器设备:在/dev下创建一些虚拟文件,或者直接通过UDP端口模拟。
  3. 初始化参数系统:加载默认的.param文件,把飞控参数设置好。
  4. 启动MAVLink通道:默认在UDP端口14550上监听地面站连接。
  5. 等待仿真引擎握手:飞控会一直尝试连接仿真引擎的UDP端口,直到收到第一个传感器数据包。

我的小技巧:如果你用sitl_multiple_run.sh启动多机仿真,记得检查每个实例的-i参数。我曾经因为两个无人机用了同一个实例ID,结果遥控器信号串了,两架飞机一起抽风。

下面是一个典型的启动日志片段,我加了注释:

# 启动命令
cd ~/PX4-Autopilot
make px4_sitl gazebo

# 日志输出(关键部分)
[INFO] [px4] Startup script: /home/user/PX4-Autopilot/ROMFS/px4fmu_common/init.d/rcS
[INFO] [px4] Setting simulator: gazebo
[INFO] [px4] Sensor model: iris (四旋翼标准模型)
[INFO] [px4] Waiting for simulator to connect on UDP port 14560...
[INFO] [px4] Simulator connected! (收到第一个IMU数据包)
[INFO] [px4] EKF starting in GPS fusion mode
[INFO] [px4] MAVLink: port 14550 (QGC) is now open

嗯,这里要注意。如果卡在Waiting for simulator这一步,99%的情况是Gazebo没启动,或者端口被占用了。我一般先killall gzserver清一下场。

4.3 仿真时间 vs 真实时间

这是SITL里最容易踩坑的地方。真实世界里,1秒就是1秒。但在仿真里,时间是可以被“操纵”的。

SITL支持两种时间模式:

模式 描述 典型场景 注意事项
实时模式 仿真时间 = 真实时间 硬件在环测试、飞手训练 CPU负载过高时会变慢
加速模式 仿真时间 > 真实时间 长时间航线测试、参数调优 日志时间戳会压缩

为什么会这样?因为SITL的时钟源是CLOCK_MONOTONIC,它完全由CPU的时钟周期驱动。如果你在代码里调用了hrt_absolute_time(),拿到的其实是仿真时间,不是墙上时间。

避坑指南:我曾经在写一个长时间悬停测试脚本时,用了sleep(10)来控制仿真步长。结果因为CPU跑得太快,仿真里的10秒实际只过了2秒。最后日志分析出来的电池消耗曲线完全不对。正确的做法是用hrt_absolute_time()做时间同步。

如果你想在SITL里加速时间,可以设置环境变量:

# 设置仿真时间倍率为2倍
export PX4_SIM_SPEED_FACTOR=2
make px4_sitl gazebo

但要注意,加速后地面站的显示会变快,而且EKF的收敛速度也会受影响。我一般只在做统计性测试(比如100次起飞降落)时才开加速。

4.4 日志系统:你的飞行黑匣子

做仿真,不会看日志等于白做。SITL的日志系统继承了真实飞控的架构,但多了很多仿真特有的字段。

日志主要分两类:

  • ULog(二进制日志):存储在~/PX4-Autopilot/log/目录下,用ulog2csv可以转成CSV。
  • SDLog(文本日志):实时打印在终端上,或者重定向到文件。

我个人习惯在仿真时同时开三个窗口:一个跑SITL,一个跑地面站,一个用tail -f盯着SDLog。这样能第一时间发现异常。

下面是一个典型的ULog分析流程:

  1. 运行仿真,执行你的测试动作(比如一个自动航线)。
  2. 仿真结束后,在log/目录下找到最新的.ulg文件。
  3. 用ulog2csv转换:ulog2csv xxx.ulg。
  4. 用Python或Excel打开生成的CSV,分析vehicle_attitude、vehicle_gps_position等话题。

关键日志话题:

  • sensor_combined:原始IMU数据,检查是否有异常跳变。
  • vehicle_local_position:本地坐标系下的位置和速度。
  • actuator_outputs:电机输出值,看是否饱和。
  • estimator_status:EKF的健康状态,看是否发散。

嗯,最后说一句。SITL的日志和真机日志是通用的。你在仿真里调好的日志分析脚本,直接拿到真机上也能用。这就是全栈仿真的价值——一次调试,到处复用。