引言
在之前的两篇文章中,我们分别剖析了 AirSim 的总体架构(五层分层设计)和 AirSim↔PX4 SITL 的双通道通信架构(锁步同步与 MAVLink 路由)。本文更进一步,聚焦四旋翼 SITL 仿真的完整闭环——从 AirSim 初始化一个四旋翼无人机、与 PX4 建立连接、传感器数据注入、到 PX4 解算控制指令并驱动旋翼旋转的端到端流程。所有分析基于 AirSim main 分支源码。
本文重点:不重复前两篇文章的内容。如果你还没读过,建议先看前两篇建立全局认知。
一、仿真闭环总览:一张图看懂数据流
在深入源码之前,先建立全局视图。下图展示了一次 SITL 仿真循环的完整数据链路——从 AirSim 物理引擎生成位姿,到 PX4 飞控解算电机指令,再回到旋翼驱动的闭环:

简化版 ASCII 流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| ┌─────────────────────────────────────────────────────────────────┐ │ AirSim (Unreal/Unity) │ │ │ │ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ │ │ │ PhysicsBody │────▶│ SensorCollection │────▶│ MavLinkMulti │ │ │ │ (位姿/速度) │ │ IMU/Baro/Mag/GPS │ │ rotorApi │ │ │ └──────────────┘ └──────────────────┘ └──────┬───────┘ │ │ │ │ │ ┌──────────────┐ ┌──────────────────┐ │ │ │ │ RotorActuator│◀────│ getActuation() │ │ HIL_SENSOR│ │ │ (拉力/扭矩) │ │ 读取 rotor_controls│ │ HIL_GPS │ │ └──────────────┘ └──────────────────┘ │ │ │ │ │ └────────────────────────────────────────────────────────┼──────────┘ │ MAVLink UDP 4560 │ ┌────────────────────────────────────────────────────────┼──────────┐ │ PX4 SITL (px4 进程) │ │ │ ▼ │ │ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ │ │ │ EKF2 估计器 │◀────│ sensors/vehicle_ │◀────│ simulator │ │ │ │ (状态融合) │ │ IMU │ │ mavlink 接收 │ │ │ └──────┬───────┘ └──────────────────┘ └──────────────┘ │ │ │ │ │ ┌──────▼───────┐ ┌──────────────────┐ ┌──────────────┐ │ │ │ mc_att_control│────▶│ mc_rate_control │────▶│ mixers │ │ │ │ (姿态控制) │ │ (角速度控制) │ │ (混控器) │ │ │ └──────────────┘ └──────────────────┘ └──────┬───────┘ │ │ │ │ │ HIL_ACTUATOR_CONTROLS │ │ 返回到 AirSim │ └───────────────────────────────────────────────────────────────────┘
|
核心闭环:AirSim 物理 → 传感器 → MAVLink → PX4 飞控 → 电机指令 → AirSim 旋翼 → 回到物理。
二、启动链路:从 settings.json 到四旋翼诞生
2.1 工厂模式创建无人机
AirSim 启动时,MultiRotorParamsFactory 根据 settings.json 中的 VehicleType 字段决定创建哪种无人机:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| static std::unique_ptr<MultiRotorParams> createConfig( const AirSimSettings::VehicleSetting* vehicle_setting, std::shared_ptr<const SensorFactory> sensor_factory) { if (vehicle_setting->vehicle_type == AirSimSettings::kVehicleTypePX4) { config.reset(new Px4MultiRotorParams()); } else if (vehicle_setting->vehicle_type == AirSimSettings::kVehicleTypeSimpleFlight) { config.reset(new SimpleFlightQuadXParams()); } }
|
当用户配置 "VehicleType": "PX4" 时,工厂创建 Px4MultiRotorParams。
2.2 Px4MultiRotorParams:连接信息 + 机架配置
Px4MultiRotorParams 做了两件事:
第一,从 settings.json 提取 MAVLink 连接信息(含锁步开关):
1 2 3 4 5 6 7 8
| Px4MultiRotorParams(const AirSimSettings::MavLinkVehicleSetting& vehicle_setting, std::shared_ptr<const SensorFactory> sensor_factory) : sensor_factory_(sensor_factory) { connection_info_ = getConnectionInfo(vehicle_setting); }
|
第二,根据 Model 字段选择机架类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| virtual void setupParams() override { if (connection_info_.model == "Blacksheep") { setupFrameBlacksheep(params); } else if (connection_info_.model == "Flamewheel") { setupFrameFlamewheel(params); } else if (connection_info_.model == "Hexacopter") { setupFrameGenericHex(params); } else setupFrameGenericQuad(params); }
|
然后创建 MavLinkMultirotorApi 实例:
1 2 3 4 5 6 7 8 9
| virtual std::unique_ptr<MultirotorApiBase> createMultirotorApi() override { unique_ptr<MultirotorApiBase> api(new MavLinkMultirotorApi()); auto api_ptr = static_cast<MavLinkMultirotorApi*>(api.get()); api_ptr->initialize(connection_info_, &getSensors(), true); return api; }
|
2.3 PX4 进程启动
AirSim 本身不启动 PX4。PX4 SITL 需要外部启动:
1 2 3 4
| export PX4_SIM_MODEL=iris $BIN_DIR/px4 -i $instance_num $BUILD_DIR \ -s "etc/init.d-posix/rcS" -t $TEST_DATA
|
PX4_SIM_MODEL=iris 告诉 PX4 使用 Iris 四旋翼的机架参数。PX4 启动后会创建一个 UDP 端口等待 AirSim 连接。
三、传感器注入链路:从物理真值到 MAVLink 消息
这是仿真闭环的第一半——AirSim 如何把物理仿真结果「喂」给 PX4。
3.1 仿真 tick 入口
每一帧,MultiRotorPhysicsBody::updateKinematics() 被调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| virtual void updateKinematics(const Kinematics::State& kinematics) override { PhysicsBody::updateKinematics(kinematics); updateSensorsAndController(); }
void updateSensorsAndController() { updateSensors(*params_, getKinematics(), getEnvironment());
vehicle_api_->update();
for (uint rotor_index = 0; rotor_index < rotors_.size(); ++rotor_index) { rotors_.at(rotor_index).setControlSignal( vehicle_api_->getActuation(rotor_index)); } }
|
3.2 MavLinkMultirotorApi::update() — HIL 传感器发送
这是最核心的传感器注入函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| virtual void update() override { if (lock_step_active_) { if (last_update_time_ + 1000000 < now) { lock_step_active_ = false; lock_step_resets_++; } else if (!received_actuator_controls_) { return; } }
const auto& imu_output = getImuData(""); const auto& mag_output = getMagnetometerData(""); const auto& baro_output = getBarometerData("");
sendHILSensor( imu_output.linear_acceleration, imu_output.angular_velocity, mag_output.magnetic_field_body, baro_output.pressure * 0.01f, baro_output.altitude );
sendSystemTime();
const auto& gps_output = getGpsData(""); if (gps_output.is_valid && gps_output.gnss.time_utc > last_gps_time_) { sendHILGps( gps_output.gnss.geo_point, gps_velocity, gps_velocity_xy.norm(), gps_cog, gps_output.gnss.eph, gps_output.gnss.epv, gps_output.gnss.fix_type, 10 ); } }
|
3.3 sendHILSensor 详解——锁步同步的关键
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| void sendHILSensor(const Vector3r& acceleration, const Vector3r& gyro, const Vector3r& mag, float abs_pressure, float pressure_alt) { MavLinkHilSensor hil_sensor; hil_sensor.time_usec = last_hil_sensor_time_ = getSimTime();
hil_sensor.xacc = acceleration.x(); hil_sensor.yacc = acceleration.y(); hil_sensor.zacc = acceleration.z(); hil_sensor.fields_updated = 0b111;
hil_sensor.xgyro = gyro.x(); hil_sensor.ygyro = gyro.y(); hil_sensor.zgyro = gyro.z(); hil_sensor.fields_updated |= 0b111000;
hil_sensor.xmag = mag.x(); hil_sensor.ymag = mag.y(); hil_sensor.zmag = mag.z(); hil_sensor.fields_updated |= 0b111000000;
hil_sensor.abs_pressure = abs_pressure; hil_sensor.pressure_alt = pressure_alt; hil_sensor.fields_updated |= 0b1101000000000;
if (hil_node_ != nullptr) { hil_node_->sendMessage(hil_sensor); received_actuator_controls_ = false;
if (lock_step_active_ && world_ != nullptr) { world_->pauseForTime(1); } } }
|
关键设计:fields_updated 字段是一个位掩码,告诉 PX4 哪些传感器数据是有效的。复位时最高位(bit 31)置 1,PX4 会重新初始化 EKF。
四、锁步同步机制:为什么需要以及如何实现
4.1 问题背景
默认情况下,AirSim 和 PX4 以各自的速率异步运行:
- AirSim 以渲染帧率(~60-120 Hz)更新物理
- PX4 以固定的 250 Hz 运行飞控循环
这导致一个问题:AirSim 的时间可能比 PX4 快。如果 AirSim 跑完 1 秒物理仿真只需要 0.1 秒真实时间,PX4 只接收到了 25 个传感器包而非 250 个——飞控逻辑的时基被打乱。
4.2 锁步实现
锁步的核心思想:AirSim 等待 PX4 的回复再推进仿真时间。
1 2 3 4 5 6 7 8 9 10 11 12 13
| void handleHilActuatorControls() { received_actuator_controls_ = true;
if (!lock_step_active_ && lock_step_enabled_) { if (last_hil_sensor_time_ == HilActuatorControlsMessage.time_usec) { lock_step_active_ = true; } } }
|
锁步激活的判断条件非常严格:PX4 返回的 HIL_ACTUATOR_CONTROLS.time_usec 必须等于 AirSim 发送的 HIL_SENSOR.time_usec。这保证了 PX4 确实是在响应这一帧的传感器数据。
4.3 时间管理
锁步模式下,getSimTime() 返回冻结时间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| unsigned long long getSimTime() { if (lock_step_active_) { if (sim_time_us_ == 0) { sim_time_us_ = clock()->nowNanos() / 1000; } return sim_time_us_; } else { return clock()->nowNanos() / 1000; } }
void advanceTime() { sim_time_us_ = clock()->nowNanos() / 1000; }
|
每次传感器发送后,advanceTime() 在收到 HIL_ACTUATOR_CONTROLS 后才被调用,确保仿真时间与 PX4 的计算进度严格同步。
4.4 锁步配置
在 settings.json 中启用:
1 2 3 4 5 6 7 8 9 10 11 12
| { "Vehicles": { "PX4": { "VehicleType": "PX4", "LockStep": true, "SitlIp": "127.0.0.1", "SitlPort": 4560, "ControlIp": "127.0.0.1", "ControlPortLocal": 14540 } } }
|
注意:LockStep 不是 settings.json 中直接读取的字段,而是通过 connection_info.lock_step 传递——这个值从 MavLinkConnectionInfo 结构体中读取,在 PX4 配置下默认为 true。
五、执行器回路:PX4 指令如何驱动旋翼
5.1 PX4 侧:从传感器到电机指令
PX4 内部的飞控管线(简化):
1 2 3 4 5 6 7
| HIL_SENSOR → sensors/vehicle_imu → EKF2 (状态估计) ↓ mc_pos_control (位置控制) → mc_vel_control (速度控制) ↓ mc_att_control (姿态控制) → mc_rate_control (角速度控制) ↓ mixer (混控器) → HIL_ACTUATOR_CONTROLS → 发回 AirSim
|
PX4 通过 HIL_ACTUATOR_CONTROLS 消息将 8 个通道的控制量发回 AirSim。对于四旋翼,前 4 个通道分别对应 4 个电机。
5.2 AirSim 侧:接收并转发到旋翼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| else if (msg.msgid == HilActuatorControlsMessage.msgid) { HilActuatorControlsMessage.decode(msg); bool isarmed = (HilActuatorControlsMessage.mode & 128) != 0;
for (int i = 0; i < RotorControlsCount; ++i) { if (isarmed) { rotor_controls_[i] = HilActuatorControlsMessage.controls[i]; } else { rotor_controls_[i] = 0; } }
if (!lock_step_active_ && lock_step_enabled_) { if (last_hil_sensor_time_ == HilActuatorControlsMessage.time_usec) { lock_step_active_ = true; } } }
|
5.3 旋翼执行器:从控制信号到力和力矩
RotorActuator 是 AirSim 中最底层的物理驱动单元:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| void setControlSignal(real_T control_signal) { control_signal_filter_.setInput( Utils::clip(control_signal, 0.0f, 1.0f)); }
static void setOutput(Output& output, const RotorParams& params, const FirstOrderFilter<real_T>& control_signal_filter, RotorTurningDirection turning_direction) { output.control_signal_input = control_signal_filter.getInput(); output.control_signal_filtered = control_signal_filter.getOutput();
output.speed = sqrt( output.control_signal_filtered * params.max_speed_square);
output.thrust = output.control_signal_filtered * params.max_thrust;
output.torque_scaler = output.control_signal_filtered * params.max_torque * static_cast<int>(turning_direction); }
|
力/力矩施加到物理体:
1 2 3 4 5 6 7 8
| virtual void setWrench(Wrench& wrench) override { Vector3r normal = getNormal(); wrench.force = normal * output_.thrust * air_density_ratio_; wrench.torque = normal * output_.torque_scaler * air_density_ratio_; }
|
5.4 完整的力/力矩计算链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| PX4 HIL_ACTUATOR_CONTROLS (0~1) ↓ rotor_controls_[i] (8 通道) ↓ setControlSignal(rotor_controls_[i]) ↓ 一阶低通滤波: control_signal_filter_.update() ↓ thrust = control_signal_filtered × max_thrust torque = control_signal_filtered × max_torque × direction speed = √(control_signal_filtered × max_speed²) ↓ wrench.force = normal × thrust × (ρ/ρ₀) ← 空气密度修正 wrench.torque = normal × torque × (ρ/ρ₀) ↓ PhysicsBody 积分 → 新的位置/姿态/速度
|
六、四旋翼 QuadX 机架物理参数
MultiRotorParams::setupFrameGenericQuad() 配置标准 X 型四旋翼:
| 参数 |
典型值 |
说明 |
| mass |
1.0 kg |
总质量 |
| rotor_count |
4 |
电机数量 |
| max_thrust |
24.5 N |
单电机最大推力(≈2.5 kg) |
| max_torque |
1.117 N·m |
单电机最大扭矩 |
| max_speed_square |
1.21e6 (rad/s)² |
最大转速平方 |
| arm_lengths[4] |
[0.2275, 0.2275, …] m |
各臂长 |
| rotor_z |
0.02 m |
旋翼平面与重心的 z 偏移 |
| linear_drag_coefficient |
0.325 |
线阻力系数 |
| angular_drag_coefficient |
0.325 |
角阻力系数 |
电机排列(QuadX,俯视图):
1 2 3 4 5 6 7
| 前(0°) ↑ CW[3]│CCW[0] │ ─────┼─────→ 右(90°) │ CCW[2]│CW[1]
|
- 电机 0:右前,CCW(逆时针)
- 电机 1:右后,CW(顺时针)
- 电机 2:左后,CCW
- 电机 3:左前,CW
这个排列确保相邻电机转向相反,偏航力矩自动抵消。
七、完整启动与仿真时序
总结一次完整的 SITL 启动和运行流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| Step 1: 启动 PX4 SITL $ ./run_airsim_sitl.sh → PX4 在 UDP 4560 监听
Step 2: 启动 AirSim (Unreal/Unity) → 读取 settings.json → MultiRotorParamsFactory::createConfig("PX4") → Px4MultiRotorParams::createMultirotorApi() → MavLinkMultirotorApi::initialize() → openAllConnections() → 连接到 UDP 4560 (数据通道) → 连接到 UDP 14580 (控制通道)
Step 3: 心跳握手 AirSim → HEARTBEAT → PX4 PX4 → HEARTBEAT → AirSim (got_first_heartbeat_ = true)
Step 4: 进入仿真循环(每帧) for each frame: ① PhysicsBody 更新位姿 ② 传感器从物理真值生成读数 ③ MavLinkMultirotorApi::update() - 发送 HIL_SENSOR (accel, gyro, mag, baro) - 发送 HIL_GPS (位置, 速度) - 发送 SYSTEM_TIME - wait for HIL_ACTUATOR_CONTROLS (锁步) ④ PX4 接收传感器 → EKF → 控制 → 混控 ⑤ PX4 发送 HIL_ACTUATOR_CONTROLS ⑥ rotor_controls_[0..3] 更新 ⑦ RotorActuator::setControlSignal() ⑧ 旋翼力/力矩施加到 PhysicsBody ⑨ 回到 ①
|
八、与 simple_flight 模式的对比
AirSim 还内置了一个轻量飞控 simple_flight:
| 特性 |
PX4 SITL |
simple_flight |
| 飞控来源 |
外部 PX4 进程 |
AirSim 内置 C++ |
| 控制算法 |
EKF + 级联 PID(生产级) |
基础角度/速度 PID |
| 通信方式 |
MAVLink UDP |
内存直连(无序列化) |
| 仿真速率 |
锁步同步可选 |
帧率同步 |
| 传感器噪声 |
AirSim 注入到 PX4 |
AirSim 内部闭环 |
| 适用场景 |
算法验证、真实飞控测试 |
快速原型、视觉算法测试 |
simple_flight 通过 SimpleFlightQuadXParams 创建,不需要外部 PX4,适合快速验证。
九、实践建议
9.1 调试传感器数据
启用 MAVLink 日志可记录所有通信:
1 2 3 4 5 6 7
| { "Vehicles": { "PX4": { "Logs": "~/Documents/AirSim/logs" } } }
|
日志会保存完整的 MAVLink 二进制流,可用 mavlogdump.py 解析。
9.2 修改机架参数
如需定制四旋翼(如更大的无人机),修改 settings.json 中的 Model 字段,或在 Px4MultiRotorParams 中添加新的机架函数。
9.3 锁步调试
若仿真出现时间异常,检查:
lock_step_resets_ 计数器——频繁重置说明 PX4 发送超时
- PX4 的
HIL_MODE 是否已设置为启用
HIL_ACTUATOR_CONTROLS.time_usec 是否匹配发送的 HIL_SENSOR.time_usec
小结
本文从 AirSim 源码出发,完整梳理了 PX4 SITL 四旋翼仿真的闭环链路:
- 启动:
settings.json → MultiRotorParamsFactory → Px4MultiRotorParams → MavLinkMultirotorApi
- 传感器注入:
MavLinkMultirotorApi::update() → sendHILSensor() / sendHILGps() → PX4
- 锁步同步:
lock_step_active_ 冻结仿真时间,等待 PX4 的 HIL_ACTUATOR_CONTROLS
- 执行器回路:PX4 返回电机控制量 →
rotor_controls_[] → RotorActuator → 物理力/力矩
- 闭环:物理仿真产生新的位姿,下一帧传感器读取,循环往复
理解这个闭环对于自定义仿真场景、调试飞控算法、以及评估 Sim-to-Real 差距至关重要。
参考: