"The outer loop dreams of waypoints; the inner loop keeps the aircraft from arguing with gravity."
A Field-Tested Control Loop
Picture a 500-gram quadrotor carrying a neural policy that has learned to dodge obstacles in simulation. The moment it tilts 15 degrees to accelerate, gravity is no longer pointing straight down relative to its body, and every force the policy learned to ignore now matters. Modern aerial AI is failing in exactly this gap: learned planners issue commands that look smooth on a trajectory plot but ask physics for something impossible. The rotor allocation matrix, the cascaded attitude loop, the integrator wind-up at saturation boundaries, these are not firmware details to skim past. They are the constraints that determine whether a policy's output is a flight maneuver or a crash report. Reading quadrotor equations of motion, tracing a setpoint through the full cascaded control hierarchy, and predicting exactly where a poorly interfaced AI command gets silently clipped before it reaches a motor: these are the skills this section builds.
Figure 47.6A shows the core intuition before any math: the vehicle holds against wind because fast inner loops stabilize attitude and body rates while the outer autonomy stack requests only motion the airframe can produce.
This section assumes familiarity with underactuated system concepts from section 6.1 and the cascaded control framework introduced in section 7.1; the nonlinear MPC variant discussed in section 7.5 maps directly onto the constraint formulation used here. The dynamics and control interfaces developed in this section are the foundation for section 47.7, which builds feasible trajectory generation on top of exactly these quadrotor limits, and for section 47.8, which shows how to validate the same control stack on real hardware through SITL (software-in-the-loop, running the real flight-control software against a physics simulator instead of a physical airframe) and flight-test logs.
Quadrotor State And Forces
Before writing any equations, consider what a quadrotor can and cannot do. It has four rotors and must produce thrust, roll torque, pitch torque, and yaw torque from those four inputs. That controls four degrees of freedom, but the vehicle moves through six degrees of freedom in space. It is underactuated. We call this the tilt-thrust coupling constraint: you cannot independently command horizontal acceleration and attitude at the same instant. When an AI decision ignores this coupling, the flight controller silently saturates or rejects the resulting commands. Mastering the state and force equations lets a planner know which motions are feasible before it asks for them.
A quadrotor state can be written as position \(p\), velocity \(v\), attitude \(R\), body angular velocity \(\omega\), and rotor speeds \(\Omega_i\). The translational and rotational dynamics are commonly summarized as:
$$m\dot v = mg e_3 - f R e_3 + d, \qquad J\dot\omega = \tau - \omega \times J\omega.$$
The total thrust \(f\) and body torque \(\tau\) come from rotor thrusts through an allocation matrix. Together the thrust and the three torque components are called the wrench, the combined force-and-moment command a controller asks the rotors to produce. For a plus or x quadrotor, the command vector \([f,\tau_x,\tau_y,\tau_z]^T\) is generated by squared rotor speeds, so the high-level planner is really choosing targets that must remain inside a polytope (the multi-dimensional region of feasible thrust-and-torque combinations, bounded on every side by a rotor's saturation limit) defined by rotor saturation, battery voltage, and motor dynamics. If the planner asks for a force or moment outside that envelope, the mission is already infeasible even if the waypoint list looks smooth.
Checkpoint
So far: a quadrotor is underactuated (four rotor inputs, six degrees of freedom), its state and dynamics are governed by the equations above, and every command the flight controller can execute must be expressed as a wrench (thrust plus torque) that stays inside a feasibility polytope set by rotor saturation.
The allocation matrix matters for embodied AI because it is where a learned policy's abstract force-torque command collides with physical motor limits. A policy trained in simulation with unlimited actuators routinely requests combinations of thrust and yaw torque that no real motor configuration can satisfy at once. ETH Zurich's agile-flight group added rotor saturation constraints inside the simulator reward. In their reported experiments, that change reduced the real-world fine-tuning needed by roughly two orders of magnitude, because the policy had already learned not to ask for the impossible; exact episode counts vary by task and airframe, so treat the specific figures as illustrative of the effect size rather than a guaranteed reduction for any given policy. When the firmware clips those commands, the vehicle follows a different trajectory than the policy intended, and the break in that closed loop is what learning relied on. A policy that no motor can execute is not a flight policy; it is a wish list.
From rotor speeds to the allocation matrix
Mechanically, each rotor \(i\) produces thrust \(T_i = k_T \Omega_i^2\) and a reaction torque \(Q_i = k_Q \Omega_i^2\) proportional to squared speed. The allocation matrix \(A\) stacks these contributions so that \([f, \tau_x, \tau_y, \tau_z]^T = A [\Omega_1^2, \Omega_2^2, \Omega_3^2, \Omega_4^2]^T\). Inverting \(A\) converts a desired wrench into target rotor speeds, but the solution is only valid when every resulting \(\Omega_i^2\) stays within the motor's operating range.
Step-Through: Inverting the allocation matrix for a hover-with-yaw command
Trace a desired wrench through the allocation map with actual numbers. Take an x-quadrotor with thrust coefficient \(k_T = 1.0 \times 10^{-5}\) N per (rad/s)\(^2\), torque coefficient \(k_Q = 1.6 \times 10^{-7}\) N·m per (rad/s)\(^2\), and arm length \(\ell = 0.15\) m. A 0.5 kg vehicle hovering needs total thrust \(f = mg = 0.5 \times 9.81 = 4.905\) N, plus a small yaw torque \(\tau_z = +0.02\) N·m to rotate, with \(\tau_x = \tau_y = 0\).
Step 1, total squared speed: \(\sum \Omega_i^2 = f / k_T = 4.905 / 10^{-5} = 4.905 \times 10^5\). Split evenly across four rotors when roll and pitch torques are zero, so each rotor's baseline is \(1.226 \times 10^5\), giving \(\Omega_i \approx 350\) rad/s.
Step 2, yaw split: yaw torque comes from the reaction-torque difference between clockwise and counter-clockwise pairs, \(\tau_z = k_Q(\Omega_1^2 + \Omega_3^2 - \Omega_2^2 - \Omega_4^2)\). To get \(+0.02\) N·m we need a pair difference of \(0.02 / (1.6 \times 10^{-7}) = 1.25 \times 10^5\), so each CW rotor gains \(+3.1 \times 10^4\) and each CCW rotor loses \(3.1 \times 10^4\) in squared speed.
Step 3, per-rotor result: CW rotors land at \(1.226\times10^5 + 3.1\times10^4 = 1.54\times10^5\) (\(\Omega \approx 392\) rad/s); CCW rotors at \(1.226\times10^5 - 3.1\times10^4 = 9.2\times10^4\) (\(\Omega \approx 303\) rad/s).
Step 4, feasibility check: if the motor's max is 400 rad/s, the 392 rad/s CW rotors sit at 98% of saturation. Ask for \(\tau_z = +0.04\) N·m instead and the CW rotors demand 432 rad/s, which the firmware clips, so the vehicle yaws slower than commanded while quietly losing the hover thrust budget. That clip is the exact moment a learned policy's wish becomes infeasible.
Think of the allocation matrix like a recipe for a four-burner stove where you must produce a specific total heat AND a specific heat gradient across the cooktop at the same time. Each burner has a maximum setting, and some combinations of total output and gradient simply cannot be achieved without exceeding at least one burner's limit. The allocation matrix is that recipe: given what the dish demands (a desired thrust and torque), it tells you exactly how to set each burner (rotor speed), and the moment any one burner would need to exceed its maximum, the entire recipe fails and something on the cooktop gets less heat than intended.
A common assumption is that a quadrotor can accelerate horizontally while holding a level attitude. That assumption is wrong. A quadrotor is underactuated: horizontal acceleration requires tilting the airframe so the rotor thrust vector gains a horizontal component. A learned policy that commands "move forward, stay level" requests something physics forbids. The flight controller resolves the conflict by prioritizing attitude stability and discarding part of the position command. Horizontal acceleration and attitude are mechanically coupled. To go forward, the vehicle must pitch forward. Any policy interface that treats them as separable produces tracking errors that look like controller bugs but are actually constraint violations.
A learned planner may choose waypoints or velocities, but the vehicle survives because fast attitude and rate loops stabilize the body. High-level AI should respect the timing and authority of the low-level flight controller.
Cascaded Flight Control
Those fast inner loops that keep the body stable are not a single controller but a nested stack, and seeing how a waypoint descends through that stack is what reveals where a learned command actually lands. Most practical quadrotor stacks use cascaded control: position control produces velocity or acceleration targets, velocity control produces attitude and thrust targets, attitude control produces body-rate targets, and rate control produces motor commands. The outer loops can be slower. The inner loops must be fast, stable, and well tuned.
PX4 makes this hierarchy visible in its controller diagrams: multicopter position, attitude, and rate controllers are separate contracts with clear setpoints and saturation points. For a learning system, those boundaries are not implementation trivia. They decide whether the policy should command a waypoint, velocity, attitude, body rate, or direct actuator target. Figure 47.6B diagrams this setpoint flow from position through attitude and rate to motor allocation, including the saturation feedback path that an outer loop cannot see directly. The Drone Control Stack table below summarizes each loop's typical input, output, and the failure it should log, so you can see at a glance which layer a learned command targets and what evidence to capture when it goes wrong.
| Loop | Typical Input | Typical Output | Failure To Log |
|---|---|---|---|
| Mission | inspection goal, geofence (a virtual perimeter the vehicle is not allowed to cross), battery budget | waypoints or coverage path | route infeasible |
| Position | pose estimate, waypoint | desired velocity or acceleration | tracking drift |
| Attitude | desired thrust direction | body-rate target | tilt or saturation |
| Rate | body-rate target | motor commands | oscillation or actuator limit |
| Safety | geofence, failsafe, health state | land, hold, return, abort | late intervention |
Consider a concrete case: a 500 g quadrotor is 0.3 m east of its waypoint. The position controller (running at ~50 Hz) converts that 0.3 m error into a desired northward velocity of roughly 0.6 m/s. The velocity controller (also ~50 Hz) converts that into a forward tilt command of about 6 degrees. The attitude controller (250 Hz) drives body rates to achieve that tilt in under 40 ms. The rate controller (1 kHz on PX4) converts those body-rate targets into differential motor PWM signals within a single millisecond. If any loop in the chain saturates, for example the attitude loop clips tilt at 30 degrees, the position loop never sees why its velocity target was not achieved and will keep integrating error. A position controller typically carries an accumulating (integral) term that sums past error over time specifically to erase steady-state offsets; when the attitude loop is saturated and cannot deliver any more tilt, that accumulated sum keeps growing anyway, so once saturation finally clears, the controller overshoots trying to discharge an error total that no longer reflects the real position error. This is why position-loop integrator wind-up is a cascaded-control failure, not a position-controller bug.
In PX4, position-loop integrator wind-up during attitude saturation is controlled by the parameter MPC_XY_VEL_MAX combined with the velocity-controller anti-windup flag MPC_VEL_MANUAL. When you observe persistent drift after a recovery from tilt saturation, reduce MPC_XY_I_GAIN before blaming the trajectory or the sensor: the integrator accumulated error that the attitude loop never had authority to discharge. Log vehicle_local_position_setpoint alongside actuator_controls in every flight to see the saturation event and the wind-up onset in the same timeline rather than inferring one from the other.
Geometric Control And MPC
Geometric control and MPC are not alternatives to the cascade just described, they are candidate implementations of its attitude and position loops respectively, so every saturation and wind-up behavior from the previous section still applies underneath them. Once the cascaded loops and their saturation-driven wind-up are understood, the next question is which controller fills each loop, and two modern choices move beyond hand-tuned PID. Geometric control treats attitude directly on \(SO(3)\) (the group of all 3D rotation matrices, i.e. the full space of orientations a rigid body can have), avoiding Euler-angle singularities. Concretely, it defines the attitude error as a rotation matrix difference rather than an Euler-angle difference, then drives that error and the body-rate error to zero with gains applied directly on the rotation group; this is what lets a quadrotor track large, fast attitude changes, such as recovering from a near-inversion gust, without the controller's own math blowing up near the poles where roll and yaw angles become degenerate. Nonlinear MPC adds constraints for thrust, tilt, obstacle clearance, geofences, and energy. The practical question is not which controller sounds more advanced, but which one meets the update deadline while keeping enough safety margin.
- Define mission goals, geofence, wind envelope, sensing mode, and emergency behaviors.
- Run a transparent cascaded PID (Proportional-Integral-Derivative) or LQR (Linear Quadratic Regulator) baseline.
- Add a geometric or MPC controller on the same waypoint panel.
- Inject wind, latency, battery drop, sensor dropout, and obstacle perturbations.
- Compare tracking error, control saturation, near-collision margin, energy, and failsafe events.
# Drone control evidence record.
# Roll out one flight-test metric packet after a wind-perturbed run.
# The fields mirror what a PX4 or simulator experiment should save.
from dataclasses import dataclass, asdict
@dataclass
class DroneMissionMetric:
controller: str
wind_mps: float
mean_tracking_error_m: float
max_tilt_deg: float
failsafe_events: int
def as_row(self) -> dict[str, object]:
return asdict(self)
metric = DroneMissionMetric("geometric_controller", 6.0, 0.21, 24.0, 0)
print(metric)
DroneMissionMetric dataclass captures wind speed, mean tracking error, maximum tilt, and failsafe count for one flight-test run, the exact quantities that separate a controller that is merely stable from one that is flight-ready. The printed row makes the wind condition, tracking quality, attitude excursion, and emergency behavior visible in one line so that a later comparison against PID or MPC stays construct-matched.Expected output: the printed record should show a controller name, a perturbation level, and at least one safety-relevant consequence such as tilt excursion or failsafe count. If your experiment logs only average tracking error, it hides the distinction between graceful recovery and a controller that briefly leaves the safe flight envelope.
Use PX4 for the flight stack, ROS 2 and MAVLink for companion-computer integration, uXRCE-DDS for current PX4 to ROS 2 middleware routing, gym-pybullet-drones and safe-control-gym for controlled learning tests, and Aerial Gym or Isaac Sim when parallel aerial simulation matters.
A high-level policy can command a trajectory that looks smooth but exceeds thrust, tilt, body-rate, or battery limits. Check actuator saturation before blaming the learning algorithm.
For a wind-disturbed inspection route, compare position error, body-rate saturation, return-to-land triggers, estimator innovation spikes (sudden jumps in the difference between the state estimator's prediction and its sensor measurement, a signal of sensor or model trouble), and geofence margin for each controller on the same waypoint panel. That comparison is where geometric control, gain-scheduled PID, and constrained MPC stop sounding like style choices and start revealing their actual safety margin.
Real-World Application: Skydio autonomous drones
Skydio's X2 and X10 drones run a cascaded flight controller exactly like the one in this section, but with the outer position loop fed by a learned obstacle-avoidance planner that issues velocity setpoints rather than raw attitude commands. Because the planner targets the velocity layer and never the rate loop directly, Skydio can swap or retrain the navigation policy without retuning the inner attitude and rate controllers that keep the airframe stable. This separation is what lets the drone weave through dense forest at 16 m/s while the safety-critical inner loops stay frozen and certified.
The tilt-thrust coupling constraint is the reason a DJI Matrice 300 cannot accelerate north without simultaneously pitching nose-down: the same four rotors that produce upward thrust must tilt the entire body to redirect thrust horizontally. Memorize the cascaded loop frequencies (position at 50 Hz, attitude at 250 Hz, rate at 1 kHz on PX4) and the constraint falls into place: a learned velocity command arriving at 10 Hz is orders of magnitude slower than the attitude loop that executes it, which is exactly why the outer loop can treat the inner loops as instantaneous black boxes without losing stability.
Neural-inertial odometry and GPS-denied agile flight (2024-2026). Controllers that fuse IMU-only neural state estimation with geometric control now sustain agile maneuvers in GPS-denied tunnels and forests. The RAPID lab at CMU demonstrated sub-meter drift over 400 m indoor flights by coupling a learned IMU integrator directly to the attitude loop without GPS (published 2025). An open problem: quantifying when the neural estimator's covariance output is calibrated enough to hand off authority from a conservative failsafe to an aggressive learned policy, since overconfident estimates cause crashes that conservative ones avoid by staying in hover.
Whole-body aerobatic policy learning with actuator-aware simulation (2024-2025). Groups at ETH Zurich (Scaramuzza lab, "Learning Agile Flight in the Wild," 2024) and at Berkeley (BAIR) train end-to-end policies that output body-rate setpoints rather than position waypoints, keeping the inner rate loop as the sole hardware interface and enforcing rotor saturation inside the simulation reward. This avoids the tilt-command clipping failure described above. The remaining frontier is that these policies degrade under motor wear: a 20% thrust loss on one rotor shifts the allocation matrix, but the policy was never trained to detect or compensate that asymmetry online.
Constraint-conditioned diffusion planners for aerial manipulation (2025-2026). Diffusion-based trajectory generators conditioned on real-time rotor feasibility polytopes (replacing MPC solve times with a forward pass) are being explored at MIT CSAIL and in the DeepMind robotics group for pick-and-place with aerial arms. Early results show feasibility-aware samples that stay inside the thrust-tilt envelope without requiring an MPC solver at runtime. Open problem for a PhD student: designing a sample-rejection criterion that operates faster than the attitude loop (under 4 ms) while remaining differentiable, so the diffusion score network can be fine-tuned online from flight logs without destabilizing the inner loop.
Project Ideas
Beginner (weekend): Build a PID vs. geometric controller comparison in gym-pybullet-drones (Gymnasium interface) on a square hover-and-translate task; log mean position error, max tilt, and motor saturation events for each controller using the DroneMissionMetric pattern from this section. The key challenge is wiring the geometric controller's SO(3) attitude error into the gym-pybullet-drones actuator interface without accidentally bypassing the rotor allocation matrix.
Intermediate (1 to 2 weeks): Implement a cascaded position-velocity-attitude controller in MuJoCo (using the mjx backend for speed) and train a PPO (Proximal Policy Optimization) policy in Gymnasium to issue velocity setpoints to the outer loop; compare tracking error and saturation frequency when the policy commands at 10 Hz vs. 50 Hz. The key challenge is keeping the inner-loop PD gains stable across the range of vehicle states the RL policy explores, particularly near tilt saturation where integrator wind-up degrades position tracking.
Advanced reference (ROS2 integration): Connect a ROS2 node to PX4 via uXRCE-DDS, publish velocity setpoints from a learned policy, and log vehicle_local_position_setpoint alongside actuator_controls in SITL to detect and visualize the exact cascade layer where the policy's commands are clipped. The key challenge is aligning the ROS2 publisher rate with PX4 estimator output so that timestamp mismatches do not introduce phantom tracking errors before any controller saturation is reached.
Can you state which layer receives the learned command: waypoint, velocity, attitude, body rate, or actuator target?
Compare cascaded PID, geometric control, and MPC on the same square inspection route with wind, latency, and thrust saturation. Report tracking error, energy, saturation, and failsafe interventions.
Lab: Watching the position integrator wind up at tilt saturation
Goal: empirically observe how an inner-loop saturation in a cascaded controller corrupts the outer-loop integrator, reproducing the wind-up failure described in the "Cascade In Numbers" insight.
Tools needed: Python 3.11+, gym-pybullet-drones (pip install from the utiasDSL repo), NumPy, and Matplotlib. Runs entirely on CPU in roughly 20 minutes.
Procedure: Use the built-in DSLPIDControl cascaded controller and command a step setpoint 3 m to the side so the vehicle must tilt hard to chase it. Add an artificial tilt clamp by capping the commanded attitude (clip the roll/pitch setpoint at, say, 15 degrees) inside the controller's compute step.
What to vary: sweep the tilt clamp from 30 degrees (effectively unrestricted) down to 10 degrees, and separately vary the position-loop integral gain across roughly 3 values.
What to observe: log the position error, the commanded vs. achieved tilt, and the position-integral accumulator each timestep. Plot all three on one time axis. You should see that at the tighter clamps the integral term keeps climbing while the achieved tilt is pinned at the clamp, then overshoots badly once the vehicle finally reaches the setpoint, exactly the wind-up signature. Confirm that lowering the integral gain reduces the overshoot but slows steady-state convergence, making the tradeoff visible in your own data.
Drone autonomy is credible when the mission planner, trajectory generator, flight controller, estimator, and failsafe behavior are evaluated as one physical stack.
Section References
Lee, Leok, and McClamroch. "Geometric Tracking Control of a Quadrotor UAV on SE(3)." https://arxiv.org/abs/1003.2005
Foundational reference for geometric quadrotor control on SE(3).
PX4 Autopilot documentation. https://docs.px4.io/main/en/
Official open flight-stack documentation.
PX4 ROS 2 guide. https://docs.px4.io/main/en/ros2/
Official guide for ROS 2 integration with PX4.
gym-pybullet-drones. https://github.com/utiasDSL/gym-pybullet-drones
Gymnasium-style quadrotor learning environment.
PX4 controller diagrams. https://docs.px4.io/main/en/flight_stack/controller_diagrams
Official PX4 reference for multicopter controller interfaces and loop structure.