Skip to content

How the Cascade Works

A quadrotor’s rotors all push along one axis, the body’s z axis. To move sideways the vehicle has to tilt that axis, to tilt it has to turn, and to turn it needs a torque, which takes the motors a moment to deliver. So Cascade is four loops, nested, each asking the one inside it for what it needs:

position ──▶ velocity ──(tilt, thrust)──▶ attitude ──(body rates)──▶ rate ──(torque)──▶ mixer

Each loop can only be as fast as the one inside it lets it be, so each one is tuned slower than the next. This page shows the loops at work, from the inside out.

Every animation plays a trace recorded from the cascade’s own C++, flying the quadrotor model through the mixer, and the scenario is linked under each player. The two widgets with sliders rerun that C++, compiled to WebAssembly, in your browser. The vehicle is RP-1: 1 kg, motors with a 20 ms lag, and at most 48 N of thrust. The controller sees the true state, with no sensor noise or delay. The controls and keys are described on the Trace Player page.

LoopInputOutputRuns atSpeed of response
Ratebody-rate setpoint ω_sptorque, through an angular acceleration1 kHz50 /s
Attitudeattitude target q_spω_sp250 Hz10 /s for tilt, 5 /s for heading
Velocityvelocity setpoint v_spa tilt and a collective thrust50 Hz3.6 /s
Positionposition setpointv_sp50 Hz1.6 /s

The speeds come from the motors outwards. The motor lag τ = 20 ms sets the rate loop at 1/τ = 50 /s, and each loop outside it is several times slower: the attitude loop by 5, the velocity loop by 14, the position loop by 32. cascade_config_from() derives every gain this way from the vehicle.

The inner loops run more often than the outer ones, because they have more to do in less time. On every rate-loop tick (1 ms), update() runs the rate loop. On every 4th it runs the attitude loop first, and on every 20th the position loop before that. Between its runs each outer loop holds its last output.

Before you play: the setpoint sets off forward at 5 m/s, and the vehicle is still at rest. How often does each loop’s output change?

How often does each loop update its output? 1× real time
Position loop: velocity setpoint [m/s] v_sp,x
Velocity loop: tilt target [rad] tilt
Attitude loop: pitch-rate setpoint [rad/s] q_sp
Rate loop: pitch torque [N·m] τ_y
t = 0.000 s / 0.100 s

Keys (with the player focused): Space play/pause · ← → one sample (Shift: ten) · Home/End · [ ] speed

RP-1 hovers at 5 m in position mode. From 0 s the position setpoint moves forward at 5 m/s, with no feed-forward, and the plot shows every 1 ms tick of the first 100 ms. At 0 s the setpoint has not moved yet, so every loop outputs zero. On the 20 ms position tick the setpoint is 0.1 m ahead, and the position loop asks for 0.16 m/s and a tilt of 3.3° toward it. It holds that until 40 ms, then asks for 0.31 m/s and 6.5°, and so on: a staircase with 20 ms steps. The attitude loop turns the tilt error into a pitch-rate setpoint every 4 ms, a staircase of finer steps that start each time the tilt target moves. The rate loop's torque changes on every tick, falling as the measured rate catches up with its setpoint. Pass criterion: no output changes off its loop's schedule.

  1. the setpoint sets off at 5 m/s; all three loops run, with no error yet
  2. position loop: 0.1 m behind, so a 3.3° tilt target
  3. attitude loop, 4 ms later: the next rate setpoint toward that tilt
  • ✓ passBB-T1 updates_off_schedule = 0 (<= 0), computed by the scenario harness

Holding an output costs nothing, so running the outer loops slower spends the processor on the loop that needs it. It also gives each inner loop a steady setpoint to settle on before the next one arrives.

Start with the inner two loops alone, in attitude mode: the caller sets the attitude and the thrust, and the position loop does not run. The attitude loop turns the angle error into a roll-rate setpoint, K times the error. The rate loop turns the rate error into an angular acceleration, k_p times the error plus damping, and multiplies it by the inertia to get a torque.

Before you play: the attitude loop runs at K = 10 /s, a fifth of the rate loop’s 50 /s. What happens to the roll when the two come close?

How do the loop gains shape a roll step? 1× real time

Predict, then try it: the attitude loop runs at K = 10 /s, a fifth of the rate loop's 50 /s. Raise K toward the rate gain, or lower the rate gain toward K: what happens to the overshoot?

Showing the recorded run. Moving a slider reruns the same C++ in your browser (WebAssembly).
World: z up · Body: x forward, y left, z up
Rotors (PX4 Quad X): 0 FR, 1 RL spin CCW · 2 FL, 3 RR spin CW
Thrust arrows: 1 N = 4 cm · Grid: 0.5 m · Body axes: x red, y green, z blue
Roll angle [rad] roll target
Roll rate [rad/s] p setpoint
Roll torque [N·m] τ_x
Rotor speeds [rad/s] 0 FR (= 3 RR) 1 RL (= 2 FL)
t = 0.000 s / 1.000 s

Keys (with the player focused): Space play/pause · ← → one sample (Shift: ten) · Home/End · [ ] speed

The vehicle hovers at 2 m. At 0.1 s the cascade, in attitude mode, is given a target of 30° roll at hover thrust. The attitude loop turns the angle error into a roll-rate setpoint (dashed in the rate plot). The rate loop turns the rate error into a torque, and the mixer and the motors deliver it. With the default gains the roll reaches 90 % of the step in 0.16 s, without overshoot. The roll rate follows its setpoint closely, because the rate loop is five times faster than the attitude loop. The vehicle holds the roll at hover thrust, so it accelerates to the right and sinks: a held attitude is not a hover. Pass criterion: the roll overshoots the 30° step by at most 10 %, the closed-loop criterion of reflex#20.

  1. roll target 30°, at hover thrust
  • ✓ passBB-I2 roll_overshoot = 0 (<= 0.1), computed by the scenario harness

When the two loops’ speeds come close, the attitude loop asks for rates faster than the rate loop can deliver, and the roll overshoots. The default ratio of 5 leaves margin for slower motors, more inertia, and delay. The closed-loop tests require at most 25 % overshoot with the motors twice as slow, 30 % more inertia, and 4 ms of delay (the V&V plan has the full set).

The rate loop works in angular acceleration, not torque. Its gains have units of 1/s, whatever the vehicle weighs, and the inertia turns its output into a torque. A heavier vehicle with the same motors keeps the same gains.

The tilt points the thrust, so it decides where the vehicle goes. The heading does not move the vehicle at all. The attitude loop therefore splits each attitude error in two: the tilt that moves the thrust axis, and then a turn about it. It runs the tilt at 10 /s and the turn at 5 /s, capped at 4 rad/s, and neither part slows the other.

Before you play: two vehicles are given the same 30° roll. One of them (solid) is also asked to turn 90° about its thrust axis. Which one gets its tilt right first?

Does a large turn slow the tilt down? 1× real time
World: z up · Body: x forward, y left, z up
Rotors (PX4 Quad X): 0 FR, 1 RL spin CCW · 2 FL, 3 RR spin CW
Translucent: the vehicle that only tilts
Thrust arrows: 1 N = 4 cm · Grid: 0.5 m · Body axes: x red, y green, z blue
Tilt error [rad] tilt and turn tilt only
Heading still to turn [rad] tilt and turn
Rate setpoints, tilt and turn [rad/s] p_sp q_sp r_sp
t = 0.000 s / 1.500 s

Keys (with the player focused): Space play/pause · ← → one sample (Shift: ten) · Home/End · [ ] speed

Two RP-1 vehicles hover at 20 m, each with its own cascade in attitude mode at hover thrust. At 0.1 s both are given a 30° roll. The solid vehicle's target also turns it 90° about the tilted thrust axis, so both targets point the thrust the same way. The tilt errors fall together: 1.8° and 1.7° at 0.3 s, within 2° of the target 2 ms apart. With the same thrust direction the two vehicles drift along the same path, never more than 0.1 m apart. The solid vehicle's heading follows on its own: the yaw-rate setpoint sits at its 4 rad/s limit until about 0.4 s, when 46° are still to turn, and 3° at 1 s. Pass criterion: the turning vehicle's tilt is within 2° of its target no later than the other's, to one attitude-loop period (4 ms).

  1. target: roll 30°; the solid vehicle also turns 90°
  • ✓ passBB-I2 tilt_settle_delay_from_yaw = 0.002 s (<= 0.004), computed by the scenario harness

The roll-rate setpoint jumps to 5.2 rad/s at the step, 10 /s times the 30° (0.52 rad) error. The yaw-rate setpoint sits at its limit, 4 rad/s. A controller that worked on Euler angles would mix the two: the turn would change the roll and pitch errors as it went, and the tilt would wander. Here the tilt error depends on the tilt alone, so it falls exactly as it does without the turn.

In position mode all four loops run. The position loop turns the position error into a velocity setpoint, and the velocity loop turns the velocity error into an acceleration. To make an acceleration, the vehicle tilts its thrust toward it: the velocity loop hands the attitude loop a tilt and a collective thrust.

A loop that only reacts to errors needs an error before it does anything. So a setpoint can also carry the velocity and acceleration the path calls for, as feed-forward: the position loop adds the velocity to its output, and the velocity loop adds the acceleration. The loops then have only the difference to correct.

Before you play: the setpoint goes around a 10 m square at 5 m/s at mid-leg, stopping at each corner, with full feed-forward. The translucent vehicle is where the position loop wants the vehicle to be, with the attitude it asks for. How far behind it does the vehicle fall?

How closely does the cascade follow a moving setpoint? 1× real time

Predict, then try it: set the feed-forward to 0 %. Does the vehicle still follow the square, and how far behind the setpoint does it run? Then restore it and raise the peak speed to 8 m/s.

Showing the recorded run. Moving a slider reruns the same C++ in your browser (WebAssembly).
World: z up · Body: x forward, y left, z up
Rotors (PX4 Quad X): 0 FR, 1 RL spin CCW · 2 FL, 3 RR spin CW
Translucent: the setpoint, at the attitude target
Thrust arrows: 1 N = 4 cm · Grid: 0.5 m · Body axes: x red, y green, z blue
Distance behind the setpoint [m] vehicle to setpoint
Speed [m/s] profile flown
Tilt [rad] target flown
Distance from the square [m] cross-track
t = 0.000 s / 18.000 s

Keys (with the player focused): Space play/pause · ← → one sample (Shift: ten) · Home/End · [ ] speed

RP-1 hovers at 5 m at a corner of a 10 m square, in position mode. The setpoint runs around the square, each leg from rest at a corner to 5 m/s at mid-leg and back to rest at the next corner, 4 s per leg, with the profile's velocity and acceleration as feed-forward. The translucent vehicle is the setpoint, drawn with the attitude target the velocity loop sends. The vehicle stays within 0.11 m of the setpoint and within 0.07 m of the square, and its top speed is 5.1 m/s. To accelerate at 2.5 m/s² it tilts up to 20° into each leg, and the same the other way to stop. Pass criterion, from reflex#20: the RMS distance from the square at most 0.3 m, its maximum at most 0.6 m, and the top speed within 10 % of the 5 m/s peak, each taken as a fraction of its limit, the worst at most 1. It is 0.2, from the top speed; the RMS distance is 0.02 m.

  1. leg 1: the setpoint sets off along +x
  2. corner: the setpoint turns 90°
  3. corner: the setpoint turns 90°
  4. corner: the setpoint turns 90°
  5. back at the start: hover
  • ✓ passBB-I2 worst_criterion_fraction = 0.1981 (<= 1), computed by the scenario harness

Without feed-forward the position loop is the only thing that moves the vehicle, and it moves it only once it has fallen behind. At 5 m/s the vehicle trails the setpoint by up to 2.7 m and reaches only 4.5 m/s, just inside the criterion’s 10 %. It still stays on the square, within 0.09 m, because each leg is straight and ends at rest. Feed-forward fixes the timing, not the path.

At 8 m/s with full feed-forward the profile itself asks for 6.4 m/s², a 33° tilt, and the corrections on top reach the 40° tilt limit. The vehicle then cannot accelerate as asked. It runs 0.61 m wide of the corners, past the criterion’s 0.6 m, and the check fails, though the RMS error is still 0.22 m.

Each loop has limits. The velocity setpoint is capped (8 m/s horizontal, 3 m/s up, 2 m/s down), the tilt at 40°, and the collective between 10 % of hover and 80 % of the rotors’ full thrust. When a limit cuts, the loop inside cannot deliver what the loop outside asks. An integrator that kept integrating the error would wind up, and the vehicle would overshoot once the limit let go. So the position loop reports each cut, and on the next tick the velocity PID on that axis stops integrating in the direction of the cut. The mixer’s saturation report does the same for the rate loop (anti-windup).

Before you play: the collective is capped at 1.25 times the weight, and the setpoint jumps 10 m up. The velocity loop asks for twice the weight. What happens to the vertical integrator while the cap holds, and does the vehicle overshoot 15 m?

When the thrust runs out on a climb, does the controller wind up? 1× real time
Height [m] flown setpoint
Climb rate [m/s] flown setpoint
Collective thrust [N] velocity loop asks commanded cap
Vertical velocity integral [m/s²] integral
Height as a fraction of the step 10 m, capped 1 m, uncapped
t = 0.000 s / 15.000 s

Keys (with the player focused): Space play/pause · ← → one sample (Shift: ten) · Home/End · [ ] speed

RP-1 hovers at 5 m in position mode with the collective capped at 12.3 N, 1.25 times its weight, and the climb-rate limit raised to 10 m/s. At 0 s the setpoint moves to 15 m. The velocity setpoint jumps to 10 m/s, and the velocity loop asks for 19.6 N, its own limit of g of acceleration on top of the weight. The position loop cuts the force to the cap for the first 2.2 s, and reports the cut. The vertical integral stays at 0 through the cut. The vehicle accelerates at 2.4 m/s² to 5.6 m/s, then brakes, and reaches 14.9 m at 3.5 s without overshooting. It settles from below, 2 % short at 5 s, because the integral went negative while the vehicle braked and takes several seconds to unwind. In the last plot the uncapped 1 m step, scaled to the same height, overshoots by 3.7 %. Pass criterion: the capped climb overshoots by at most 5 points more than the uncapped step.

  1. setpoint 10 m up; the vertical force is cut to the cap
  • ✓ passBB-I2 overshoot_above_unsaturated = -0.03701 (<= 0.05), computed by the scenario harness

Two limits hold the integral here: the velocity PID’s own output limit, and the cut the position loop reports. Either is enough on its own. The report matters when the PID is inside its own limit but the force is not, for example when the tilt takes the room the collective needed.

  • The developer guide has the loops, the anti-windup wiring, the default gains, and the API.
  • The V&V plan lists what is verified, and how.
  • The deployment guide covers the configuration and RP-1’s values, the loop rates, the modes, the state the cascade needs, the limits, and cost.