Distributed Control & Second-Order Consensus for UAV Swarms
ROS2 distributed control with second-order consensus for UAV swarms, LMI-based gain synthesis and PX4 velocity-layer integration
Introduction#
This article presents a distributed control framework for multi-UAV formation systems, built on the ROS2+PX4 simulation environment and Multi-Drone Offboard Control foundations. By integrating ROS2’s distributed communication architecture with PX4’s MAVLink protocol, the system enables real-time state sharing and cooperative control across multiple drones. The consensus-based control law allows each drone to rely solely on local neighbor information to achieve formation keeping and trajectory tracking, offering a highly reliable inter-drone communication method for large-scale swarm missions.
UAV formation control generally falls into two architectural paradigms: centralized and distributed. Their differences lie primarily in decision hierarchy, communication topology, and system robustness.
Centralized control relies on a single central node (e.g., a ground station or a lead UAV) for global decision-making. All drones execute commands issued by this central node. It offers high control precision and excellent formation consistency, making it suitable for complex missions. However, it suffers from single-point failure risk, high communication load on the central node, and poor scalability — especially critical when the central node fails or comes under attack, which can severely compromise formation safety.
Distributed control has no central node. Each drone makes autonomous decisions through local communication, interacting only with neighboring drones. This approach provides strong robustness, high scalability, and dynamic adaptability for large-scale formations, at the cost of higher coordination complexity and weaker global optimization capability.
This article is based on a formation of six UAVs. We first establish a first-order swarm cooperative control law, then extend it to a more general second-order model (acceleration input), solve for the feedback gain matrix via the LMI method, and finally validate the approach in Gazebo simulation. The final section provides a rigorous proof of the error dynamics under constant formation offsets.
Mathematical Preliminaries#
Extensive literature exists on the mathematical background of distributed cooperative control systems; this section only provides the necessary review. Readers are referred to the following for deeper understanding:
- Graph theory fundamentals: Multi-Agent System Control (1) — Graph Theory ↗ (Chinese)
- Properties of the Laplacian matrix: CSDN Blog ↗ (Chinese)
- Consensus control lemmas: CSDN Blog ↗ (Chinese), which can be used to prove the convergence of the control law
From a theoretical perspective, it can be shown that the swarm cooperative system constructed in this article is capable of achieving cooperative UAV formation tasks.
First-Order Consensus Control#
System Model#
Consider a formation of UAVs. The drone labeled is the leader, and the remaining drones labeled are followers.
Each drone uses a first-order kinematic model in which the state consists only of position information. Let denote the position of drone , and let the control input be a velocity command :
First-Order Consensus Protocol#
We adopt the classical linear consensus protocol, where the control input of drone is determined by the weighted sum of neighbor state deviations:
where is the neighbor set of drone , are communication weights, and is the position gain.
For a connected graph, it can be proved that the position states satisfy as , meaning all followers asymptotically converge to the leader’s position. This is the most fundamental application of distributed consensus in swarm formation — global position synchronization achieved using only local neighbor position deviations.
Second-Order Distributed Control#
Theoretical Modeling#
Single-UAV Dynamics#
We consider a formation of UAVs. Drone serves as the leader, chosen as a virtual leader (not physically flying). Drones labeled are followers.
Each drone is modeled with second-order dynamics (containing both position and velocity states). The state of drone is:
where is the position and is the velocity. The control input is a second-order acceleration command .
The linearized second-order model of each drone can be written in matrix form:
The matrices and are block matrices, obtained by simple derivation:
Linear Consensus Protocol#
We adopt the classical linear consensus protocol, where the input of each follower drone () can be expressed as the weighted sum of neighbor state deviations:
where is the state-feedback gain matrix.
Rewrite this in Laplacian matrix form. Define the weight matrix , degree matrix where , and Laplacian matrix whose entries satisfy , (), .
Expand equation (3):
Therefore:
Error System Definition#
Define the error of each follower relative to the leader (labeled ):
From the single-drone dynamics (equation (1)):
Substitute the control law (equation (4)):
For all , we have . Split into and :
Using the Laplacian’s zero row-sum property , we therefore have , so:
Substituting back yields the error dynamics for a single follower :
Stack all followers:
Writing the error dynamics in matrix form:
Lyapunov Function and LMI Condition#
Choose the Lyapunov function , where and is a symmetric positive-definite matrix:
The closed-loop system (homogeneous part) of the error dynamics is:
Compute the Lyapunov derivative:
To guarantee asymptotic stability, we require , i.e.:
Congruence Transformation Note: Let be a symmetric matrix and be an invertible matrix. Define . Then and . For any nonzero vector , , and since is invertible the sign is preserved.
Apply the congruence transformation to eliminate :
Then . Expanding :
Apply the variable substitution to eliminate bilinear coupling:
Thus the LMI condition is:
Control Law with Formation Offsets#
Assume the formation has desired relative positions. Define as the desired relative offset between follower and follower . The control law is modified to:
Redefine the error term. Let (with ), where is the desired formation position offset of follower relative to the leader. Define the new error as:
Differentiate the error, substituting the single-drone dynamics (1) and the control law (13):
Simplify Sum 1 (Laplacian term). For , substitute , and :
From the Laplacian zero row-sum property (), we obtain , hence:
Simplify Sum 2 (formation weighting term). Using , , and ():
Therefore:
Substitute into the error equation. Substitute (for ), along with (15) and (16), into (14). After expansion, note that the term and the term (the part with ) cancel each other, and means . The result is:
Constant offset case — LMI condition unchanged. If is a constant position offset (the most common case), i.e.:
The extra bias term vanishes, and the error dynamics reduce to the classic homogeneous form:
Stacking all followers:
This is identical to equation (7). Therefore, when formation-internal relative positions (constant offsets) are present, the Lyapunov analysis and LMI condition (12) remain unchanged.
Simulation Implementation#
Solving the LMI Inequality#
The above LMI condition is solved in MATLAB using the YALMIP toolbox. The chosen communication topology is: drone is the virtual leader, drone subscribes to drones , , and (relative formation position ), drone subscribes to drone (relative formation position ), and drone subscribes to drone (relative formation position ).
% ------------ Parameters ------------
N = 3; % Number of followers
% System matrices A (6x6) and B (6x3)
A = [zeros(3) eye(3); zeros(3) zeros(3)];
B = [zeros(3); eye(3)];
% Construct L_sub and Ltilde
L_sub = [ 3 -1 -1;
-1 1 0;
-1 0 1 ];
Ltilde = -L_sub;
% ------------- YALMIP Variables -------------
n = size(A,1); % =6
m = size(B,2); % =3
X = sdpvar(n,n,'symmetric'); % X ≻ 0
Y = sdpvar(m,n,'full'); % Y = K*X (m x n)
% Construct large matrix S = I_N ⊗ (A X + X A') + Ltilde ⊗ (B Y) + Ltilde' ⊗ (B Y)'
S = kron(eye(N), A*X + X*A') + kron(Ltilde, B*Y) + kron(Ltilde', (B*Y)');
% LMI: S < 0, X > 0
eps = 1e-6;
Constraints = [S <= -eps*eye(N*n), X >= eps*eye(n)];
Constraints = [Constraints, X == X'];
% ------------- Solve -------------
options = sdpsettings('verbose', 1, 'solver', 'sedumi');
Objective = [];
sol = optimize(Constraints, Objective, options);
if sol.problem == 0
Xopt = value(X);
Yopt = value(Y);
K = Yopt * (Xopt \ eye(n)); % K = Y * X^{-1}
disp('Found feasible K:');
disp(K);
else
disp('Solver failed; return information:');
yalmiperror(sol.problem)
endmatlabThe solution for the formation positions used in this article (readers should solve according to their own models):
Found feasible K:
0.6983 0.0000 0.0000 2.1929 0.0000 0.0000
-0.0000 0.6983 0.0000 -0.0000 2.1929 0.0000
0.0000 -0.0000 0.6983 -0.0000 -0.0000 2.1929plaintextHence , .
From Acceleration to Velocity Commands#
PX4 employs a typical cascaded PID control structure: position controller → velocity controller → acceleration controller → attitude controller → angular rate controller → motors.
If one were to directly use second-order acceleration input, gravity compensation and attitude resolution would be required to convert world-frame acceleration commands into body-frame attitude commands:
In Offboard mode, direct acceleration control is uncommon in engineering practice (for instance, does not produce hover; a gravity compensation empirical value must be provided).
Therefore, in this article the virtual acceleration command obtained from the linear consensus protocol is integrated to generate the desired velocity input:
The result is then smoothed through a first-order filter:
where is the filter factor, set to in this article.
Code example for the and directions:
float ax = position_gain_ * sum_dx + velocity_gain_ * sum_dvx;
float ay = position_gain_ * sum_dy + velocity_gain_ * sum_dvy;
float vx_integrated = desired_velocity_[0] + ax * dt;
float vy_integrated = desired_velocity_[1] + ay * dt;
desired_velocity_[0] = filter_alpha_ * vx_integrated
+ (1.0f - filter_alpha_) * desired_velocity_[0];
desired_velocity_[1] = filter_alpha_ * vy_integrated
+ (1.0f - filter_alpha_) * desired_velocity_[1];cppThis approach preserves the dynamic characteristics of second-order consensus control while being compatible with PX4’s velocity control layer, offering good engineering feasibility and stability.
Filter Discretization Derivation#
The continuous-time first-order low-pass filter equation is:
where is the filtered velocity signal, is the input signal, and is the time constant. Rewriting:
Under discrete conditions with sampling period :
Substituting:
Multiplying both sides by and rearranging:
Define , yielding:
This matches the form of equation (22).
ROS2 Coordinate Transformation Note#
In a ROS2 + PX4 launch, each drone instance by default starts with its own takeoff point as the origin . Therefore, each time a position is received in a subscription callback, one must manually subtract the takeoff point coordinates to obtain the global coordinates before substituting into the control law. A code example is provided in the next section.
Reference Code#
The following is the complete C++ code for UAV (which subscribes to the virtual leader as well as neighboring UAVs and ), corresponding to the model described above. The code for UAVs and is similar, requiring only modifications to the subscription topology and formation offsets.
#include <rclcpp/rclcpp.hpp>
#include <px4_msgs/msg/offboard_control_mode.hpp>
#include <px4_msgs/msg/trajectory_setpoint.hpp>
#include <px4_msgs/msg/vehicle_command.hpp>
#include <px4_msgs/msg/vehicle_odometry.hpp>
#include <vector>
#include <map>
#include <array>
#include <limits>
#include <chrono>
struct NeighborState {
std::array<float, 3> position = {0.0f, 0.0f, 0.0f};
std::array<float, 3> velocity = {0.0f, 0.0f, 0.0f};
bool valid = false;
};
class UAV1Controller : public rclcpp::Node {
public:
UAV1Controller() : Node("uav1_controller") {
rclcpp::QoS qos(10);
qos.reliability(RMW_QOS_POLICY_RELIABILITY_BEST_EFFORT);
qos.durability(RMW_QOS_POLICY_DURABILITY_TRANSIENT_LOCAL);
qos.history(RMW_QOS_POLICY_HISTORY_KEEP_LAST);
offboard_control_mode_publisher_ = create_publisher<
px4_msgs::msg::OffboardControlMode>(
"/px4_1/fmu/in/offboard_control_mode", qos);
trajectory_setpoint_publisher_ = create_publisher<
px4_msgs::msg::TrajectorySetpoint>(
"/px4_1/fmu/in/trajectory_setpoint", qos);
vehicle_command_publisher_ = create_publisher<
px4_msgs::msg::VehicleCommand>(
"/px4_1/fmu/in/vehicle_command", qos);
own_odometry_sub_ = create_subscription<
px4_msgs::msg::VehicleOdometry>(
"/px4_1/fmu/out/vehicle_odometry", qos,
[this](const px4_msgs::msg::VehicleOdometry::SharedPtr msg) {
own_position_ = {msg->position[0], msg->position[1],
msg->position[2]};
own_velocity_ = {msg->velocity[0], msg->velocity[1],
msg->velocity[2]};
own_state_valid_ = true;
});
uav2_sub_ = create_subscription<px4_msgs::msg::VehicleOdometry>(
"/px4_2/fmu/out/vehicle_odometry", qos,
[this](const px4_msgs::msg::VehicleOdometry::SharedPtr msg) {
neighbor_states_[2] = {
{msg->position[0] - 5.0f, msg->position[1] - 10.0f,
msg->position[2]},
{msg->velocity[0], msg->velocity[1], msg->velocity[2]},
true
};
});
uav3_sub_ = create_subscription<px4_msgs::msg::VehicleOdometry>(
"/px4_3/fmu/out/vehicle_odometry", qos,
[this](const px4_msgs::msg::VehicleOdometry::SharedPtr msg) {
neighbor_states_[3] = {
{msg->position[0] + 5.0f, msg->position[1] - 10.0f,
msg->position[2]},
{msg->velocity[0], msg->velocity[1], msg->velocity[2]},
true
};
});
position_gain_ = 0.6983f;
velocity_gain_ = 2.1929f;
filter_alpha_ = 0.5f;
start_time_ = this->now();
control_timer_ = create_wall_timer(std::chrono::milliseconds(20),
[this]() { this->controlLoop(); });
last_print_time_ = this->now();
last_control_time_ = this->now();
desired_velocity_ = {0.0f, 0.0f, 0.0f};
}
private:
NeighborState getVirtualLeaderState() {
auto current_time = this->now();
double t = (current_time - start_time_).seconds();
NeighborState leader_state;
leader_state.valid = true;
leader_state.position[2] = 0.0f;
leader_state.velocity[2] = 0.0f;
if (t <= 10.0) {
leader_state.velocity[0] = 3.0f;
leader_state.position[0] = 3.0f * t;
leader_state.velocity[1] = 0.3f * t;
leader_state.position[1] = 0.5f * 0.3f * t * t;
} else if (t <= 20.0) {
leader_state.velocity[0] = 0.0f;
leader_state.position[0] = 30.0f;
leader_state.velocity[1] = 3.0f;
leader_state.position[1] = 15.0f + 3.0f * (t - 10.0f);
} else if (t <= 30.0) {
float d = t - 20.0f;
leader_state.velocity[0] = 0.0f;
leader_state.position[0] = 30.0f;
leader_state.velocity[1] = 3.0f - 0.3f * d;
leader_state.position[1] = 45.0f + 3.0f * d
- 0.5f * 0.3f * d * d;
} else {
leader_state.velocity[0] = 0.0f;
leader_state.position[0] = 30.0f;
leader_state.velocity[1] = 0.0f;
leader_state.position[1] = 60.0f;
}
return leader_state;
}
void controlLoop() {
NeighborState leader_state = getVirtualLeaderState();
neighbor_states_[0] = leader_state;
if (!own_state_valid_ || !neighbor_states_[2].valid
|| !neighbor_states_[3].valid) return;
double dt = (this->now() - last_control_time_).seconds();
last_control_time_ = this->now();
px4_msgs::msg::OffboardControlMode offboard_msg;
offboard_msg.position = false;
offboard_msg.velocity = true;
offboard_msg.acceleration = false;
offboard_msg.timestamp = this->now().nanoseconds() / 1000;
offboard_control_mode_publisher_->publish(offboard_msg);
static int init_counter = 0;
if (init_counter < 20) {
px4_msgs::msg::TrajectorySetpoint setpoint{};
setpoint.timestamp = get_clock()->now().nanoseconds() / 1000;
setpoint.position = {std::numeric_limits<float>::quiet_NaN(),
std::numeric_limits<float>::quiet_NaN(),
std::numeric_limits<float>::quiet_NaN()};
setpoint.velocity = {0.0f, 0.0f, 0.0f};
setpoint.acceleration = {std::numeric_limits<float>::quiet_NaN(),
std::numeric_limits<float>::quiet_NaN(),
std::numeric_limits<float>::quiet_NaN()};
trajectory_setpoint_publisher_->publish(setpoint);
init_counter++;
if (init_counter == 20) {
px4_msgs::msg::VehicleCommand cmd;
cmd.command = px4_msgs::msg::VehicleCommand::VEHICLE_CMD_DO_SET_MODE;
cmd.param1 = 1.0f; cmd.param2 = 6.0f;
cmd.target_system = 2; cmd.target_component = 1;
cmd.source_system = 1; cmd.source_component = 1;
cmd.from_external = true;
cmd.timestamp = this->now().nanoseconds() / 1000;
vehicle_command_publisher_->publish(cmd);
}
return;
}
float sum_dx = (neighbor_states_[0].position[0] - own_position_[0])
+ (neighbor_states_[2].position[0] - own_position_[0]
+ 5.0f)
+ (neighbor_states_[3].position[0] - own_position_[0]
- 5.0f);
float sum_dy = (neighbor_states_[0].position[1] - own_position_[1])
+ (neighbor_states_[2].position[1] - own_position_[1]
+ 10.0f)
+ (neighbor_states_[3].position[1] - own_position_[1]
+ 10.0f);
float sum_dvx = (neighbor_states_[0].velocity[0] - own_velocity_[0])
+ (neighbor_states_[2].velocity[0] - own_velocity_[0])
+ (neighbor_states_[3].velocity[0] - own_velocity_[0]);
float sum_dvy = (neighbor_states_[0].velocity[1] - own_velocity_[1])
+ (neighbor_states_[2].velocity[1] - own_velocity_[1])
+ (neighbor_states_[3].velocity[1] - own_velocity_[1]);
float ax = position_gain_ * sum_dx + velocity_gain_ * sum_dvx;
float ay = position_gain_ * sum_dy + velocity_gain_ * sum_dvy;
float vx_integrated = desired_velocity_[0] + ax * dt;
float vy_integrated = desired_velocity_[1] + ay * dt;
desired_velocity_[0] = filter_alpha_ * vx_integrated
+ (1.0f - filter_alpha_) * desired_velocity_[0];
desired_velocity_[1] = filter_alpha_ * vy_integrated
+ (1.0f - filter_alpha_) * desired_velocity_[1];
desired_velocity_[2] = 0.0f;
px4_msgs::msg::TrajectorySetpoint setpoint;
setpoint.timestamp = this->now().nanoseconds() / 1000;
setpoint.velocity = {desired_velocity_[0], desired_velocity_[1], 0.0f};
setpoint.position = {NAN, NAN, NAN};
setpoint.acceleration = {NAN, NAN, NAN};
trajectory_setpoint_publisher_->publish(setpoint);
}
// Publishers
rclcpp::Publisher<px4_msgs::msg::OffboardControlMode>::SharedPtr
offboard_control_mode_publisher_;
rclcpp::Publisher<px4_msgs::msg::TrajectorySetpoint>::SharedPtr
trajectory_setpoint_publisher_;
rclcpp::Publisher<px4_msgs::msg::VehicleCommand>::SharedPtr
vehicle_command_publisher_;
// Subscribers
rclcpp::Subscription<px4_msgs::msg::VehicleOdometry>::SharedPtr
own_odometry_sub_;
rclcpp::Subscription<px4_msgs::msg::VehicleOdometry>::SharedPtr uav2_sub_;
rclcpp::Subscription<px4_msgs::msg::VehicleOdometry>::SharedPtr uav3_sub_;
rclcpp::TimerBase::SharedPtr control_timer_;
std::array<float, 3> own_position_ = {0.0f, 0.0f, 0.0f};
std::array<float, 3> own_velocity_ = {0.0f, 0.0f, 0.0f};
std::array<float, 3> desired_velocity_ = {0.0f, 0.0f, 0.0f};
bool own_state_valid_ = false;
std::map<int, NeighborState> neighbor_states_;
float position_gain_, velocity_gain_, filter_alpha_;
rclcpp::Time last_print_time_, last_control_time_, start_time_;
};
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
auto node = std::make_shared<UAV1Controller>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}cppSimulation Results#
At startup, all three drones are manually commanded to take off via commander takeoff and hover at altitude. The consensus controller then takes over in the -plane, driving the formation to follow the virtual leader’s trajectory.
Video 1: Second-order consensus control law formation simulation
The overall formation tracking performance is satisfactory. There is some overshoot but overall stability is maintained, confirming the effectiveness of the second-order control law.
Notably, the unique advantages of distributed communication enable further robustness testing: after the formation begins moving, killing UAV ‘s simulation (simulating a shoot-down scenario) leaves the remaining drones unaffected, continuing along their planned paths. This is precisely the core reliability benefit of distributed communication — using a connected communication topology for data exchange, the impact of a single-point failure on the overall formation can be effectively mitigated in practical applications.
Summary#
This article began with a first-order swarm system and progressively introduced second-order dynamics to build a distributed communication-based UAV formation control system. Starting from first-order position consensus, we derived the error dynamics and Lyapunov stability conditions for the second-order model, converted the stability conditions into a solvable LMI via congruence transformation (obtaining the feedback gain matrix through YALMIP in MATLAB), adapted acceleration-form commands to PX4-compatible velocity commands via integration and filtering, and provided a rigorous derivation for the case with constant formation offsets, verifying that the LMI condition remains invariant.
Compared to centralized control, the distributed approach offers superior robustness and scalability — advantages that are decisive in complex, dynamically changing real-world scenarios. In emerging fields such as low-altitude economy and swarm combat systems, this integrated communication-and-control architecture will play an increasingly important role.
This article presents only an initial control scheme, and many areas for improvement were identified during simulation — such as how to further suppress overshoot, how to adapt to non-connected (switching) communication topologies, and how to deploy the computation on real onboard computers. For deploying these algorithms on real hardware, see ROS2 Hardware: Flashing the Jetson and Controlling PX4 with MAVROS2 for the complete simulation-to-real workflow.
Finally, thank you for your patient reading and constructive feedback!