很多刚接触多机编队的飞手或算法同学,容易一上来就直接搞集中式控制,即一台地面站或者主控计算机挂多个 MAVLink 连接,计算完所有航路点后再分别下发。在三到五架机、通信干净的小场地上,这种集中式跑起来很漂亮,但一旦扩展到十架以上,或者进入强干扰、弱信号环境,地面站单点通信打满、丢包延迟拉大,跟着就是炸机连锁反应。
最近带团队做一套基于去中心化/分布式一致性(Consensus-based)算法的多机编队系统,硬件上板前,我们在 Gazebo 环境下做了完整的 SITL 闭环验证。这里把搭建流程、控制逻辑和实调踩坑记录整理出来,供大家参考。
一、 仿真系统架构设计
分布式控制的核心在于:每架飞机只与拓扑网络内邻近的少数节点交换状态(位置、速度),独立在自身机载计算机上解算控制律,不依赖中央节点。
仿真选型配置如下:
- 飞控固件:PX4 Autopilot (v1.14+) SITL
- 中间件:ROS 2 (Humble) + Micro XRCE-DDS
- 物理仿真引擎:Gazebo Harmonic (或经典版 Gazebo 11)
- 通信拓扑:固定无向图(环形拓扑或连通图),节点间通过 ROS 2 Topic 模拟自组网单跳广播
[PX4 SITL #1] <--> [Micro XRCE-DDS #1] <--> [ROS 2 Node (Agent 1)]
^
| (Ad-hoc P2P / MAVLink)
v
[PX4 SITL #2] <--> [Micro XRCE-DDS #2] <--> [ROS 2 Node (Agent 2)]
每架飞机拥有独立的机载命名空间(如 /px4_1、/px4_2),各自订阅自身飞控状态,并与邻居节点通信。
二、 多实例 SITL 环境启动配置
在多机仿真中,端口和 SYS_AUTOSTART、MAV_SYS_ID 必须严格隔离开,否则会导致 MAVLink 路由混乱。
PX4 官方提供了基于 Shell 的多实例生成机制。以三架多旋翼为例,核心脚本配置逻辑如下:
#!/bin/bash
# 启动 3 架 x500 仿真多旋翼
NUM_DRONES=3
for i in $(seq 1 $NUM_DRONES); do
export PX4_SYS_ID=$i
export PX4_SIM_MODEL="x500"
# 设置初始摆放坐标,防止重叠炸机
X_POS=$(( (i - 1) * 2 ))
Y_POS=0
echo "Starting Drone $i at X: $X_POS, Y: $Y_POS"
# 独立实例端口与启动
gnome-terminal --tab --title="Drone $i" -- bash -c \
"px4-autopilot/build/px4_sitl_default/bin/px4 -i $i \
-d px4-autopilot/ROMFS/px4fmu_common \
-w sitl_instance_$i \
-s etc/init.d-posix/rcS \
-t $X_POS,$Y_POS,0,0,0,0"
done
在 ROS 2 端,启动多个 Micro XRCE-DDS Agent,分别对应 UDP 端口(8888、8889、8890 等),并在 DDS 层面映射各自的 Topic 命名空间。
三、 分布式协同控制律实现
分布式编队常用的经典方法是一阶/二阶一致性协议(Consensus Protocol)。设节点 $i$ 的状态为位置 $p_i$ 和速度 $v_i$,邻居集合为 $N_i$,编队相对期望偏移量为 $\Delta_{ij}$:
$$u_i = - \sum_{j \in N_i} a_{ij} \left( (p_i - p_j - \Delta_{ij}) + \gamma (v_i - v_j) \right)$$
以下为基于 Python 的 ROS 2 分布式编队控制节点核心简化片段:
import rclpy
from rclpy.node import Node
from px4_msgs.msg import TrajectorySetpoint, VehicleLocalPosition
import numpy as np
class DistributedFormationAgent(Node):
def __init__(self, agent_id, neighbors):
super().__init__(f'formation_agent_{agent_id}')
self.agent_id = agent_id
self.neighbors = neighbors # 邻居 ID 列表,例如 [2] 或 [1, 3]
self.pos = np.zeros(3)
self.vel = np.zeros(3)
self.neighbor_pos = {nid: np.zeros(3) for nid in neighbors}
self.neighbor_vel = {nid: np.zeros(3) for nid in neighbors}
# 编队相对几何偏移 (NED 坐标系: [North, East, Down])
self.offsets = {
1: np.array([0.0, 0.0, -5.0]),
2: np.array([-2.0, 2.0, -5.0]),
3: np.array([-2.0, -2.0, -5.0]),
}
# 订阅自身位置 (Micro XRCE-DDS 转换后的 Topic)
self.create_subscription(
VehicleLocalPosition,
f'/px4_{agent_id}/fmu/out/vehicle_local_position',
self.self_pos_callback,
10
)
# 订阅邻居位置广播
for nid in neighbors:
self.create_subscription(
VehicleLocalPosition,
f'/px4_{nid}/fmu/out/vehicle_local_position',
lambda msg, nid=nid: self.neighbor_callback(msg, nid),
10
)
# 发布自身期望速度/位置指令
self.setpoint_pub = self.create_publisher(
TrajectorySetpoint,
f'/px4_{agent_id}/fmu/in/trajectory_setpoint',
10
)
self.timer = self.create_timer(0.05, self.control_loop) # 20 Hz 控制率
def self_pos_callback(self, msg):
self.pos = np.array([msg.x, msg.y, msg.z])
self.vel = np.array([msg.vx, msg.vy, msg.vz])
def neighbor_callback(self, msg, nid):
self.neighbor_pos[nid] = np.array([msg.x, msg.y, msg.z])
self.neighbor_vel[nid] = np.array([msg.vx, msg.vy, msg.vz])
def control_loop(self):
kp = 0.8
kv = 0.4
vel_cmd = np.zeros(3)
# 计算拉普拉斯图一致性控制量
for nid in self.neighbors:
target_offset = self.offsets[self.agent_id] - self.offsets[nid]
pos_error = (self.pos - self.neighbor_pos[nid]) - target_offset
vel_error = self.vel - self.neighbor_vel[nid]
# 分布式二阶一致性核心公式
vel_cmd -= kp * pos_error + kv * vel_error
# 限速滤波与安全饱和处理 (水平最大 3 m/s, 垂直最大 1.5 m/s)
vel_cmd[0:2] = np.clip(vel_cmd[0:2], -3.0, 3.0)
vel_cmd[2] = np.clip(vel_cmd[2], -1.5, 1.5)
# 发布 Offboard 速度控制
msg = TrajectorySetpoint()
msg.velocity = [float(vel_cmd[0]), float(vel_cmd[1]), float(vel_cmd[2])]
msg.yaw = 0.0 # 锁定正北或朝向速度方向
self.setpoint_pub.publish(msg)
四、 避障与死锁处理
单纯的拓扑一致性控制只能在开阔空间维持队形,一旦引入障碍物或队形变换交叉,极易发生机间气流互扰或物理碰撞。
我们在仿真中引入了 人工势场(APF)结合一致性算法:
- 斥力场激活阈值:设定安全距离 $R_{safe} = 1.5 \text{ m}$,警报距离 $R_{warn} = 3.0 \text{ m}$。
- 当 $|p_i - p_j| < R_{warn}$ 时,斥力向量直接叠加入速度控制环,斥力权重高于队形约束权重。
- 高度分层保护:在动态规避期间,若水平距离逼近 $R_{safe}$,则强制引入 Z 轴阶跃分流(如奇数 ID 爬升 1 m,偶数 ID 下降 1 m),防止平面死锁。
五、 踩坑复盘与实机落差
在纯仿真里跑得很顺的算法,直接刷进实机往往会撞得惨烈。以下是实机复盘记录的几个关键边界点:
1. 通信延迟与时钟不同步
Gazebo 仿真中 ROS 2 节点跑在同一台 CPU 上,共享系统时钟,延迟几乎为零。实机上依赖数传自组网模块(如 Wi-Fi Mesh 或专用 COFDM 模组),节点之间会发生丢包和 50~200 ms 的抖动。
- 仿真复现方法:在 Linux 下使用
tc(Traffic Control)工具对虚拟网卡人为注入延迟与丢包率:bashsudo tc qdisc add dev lo root netem delay 80ms 20ms loss 5% - 解决对策:一致性项必须加入时间戳滤波,丢包超 500 ms 的邻居节点直接在拓扑图矩阵中临时剔除,退化为局部相对定位保持。
2. 下洗气流(Downwash)影响
多旋翼在上下重叠时,下方机体进入上方机体的强下洗气流区,会导致下方飞机高度估算下沉,甚至由于升力骤降直接侧翻炸机。
- 实飞经验:机间水平间距必须大于 4 倍旋翼直径;队形变换如果需要穿越,必须遵循“低机先行”或“错层平移”原则,杜绝从友机正上方掠过。
3. 坐标系基准漂移
仿真里所有飞机的坐标系原点都在 Gazebo 世界绝对坐标(0,0,0)。实机在野外依靠 RTK 或 GPS,各机虽然都在 WGS-84 坐标系,但飞控上电初始化的 Local NED 原点各不相同。
- 必须规范:在起飞前由地面站广播统一的全局参考点(Global Home Point,经纬高),所有飞机通过自身局部坐标转换到统一的本地切平面(ENU / NED)后再进入编队控制,否则一致性算法计算的相对距离从初始状态就是错的。
六、 总结
多机编队的核心在于架构的弹性。从开发路径来看:
- 先跑通单机 Offboard 速度与位置跟踪响应;
- 在 Gazebo 里搭建物理多实例,引入通信延迟与丢包测试分布式一致性算法;
- 实机拉开距离,从两架机定距跟随开始逐步验证拓扑扩展。
仿真把逻辑跑透,能省下大量修桨、买电调的成本。下一步我们计划把视觉 VIO 相对定位融合进分布式拓扑中,在无 GPS 弱网环境下跑一跑集群避障,届时再整理相关实飞数据。
License: CC BY-NC 4.0
Updated 13 hours ago
Was this article helpful? Give it a like.
0 comments


