Skip to content

Cascade Deployment

This guide is for integrators who put Cascade between a state estimator and the mixer. It covers what to set, what the cascade needs from the rest of the stack, and how to wire it. The developer guide has the loops and the API.

CascadeConfig<T> is a plain aggregate of the three loops’ configs and the two dividers. cascade_config_from(vehicle) derives all of it from the vehicle’s QuadrotorParams, and cascade_config_valid() (I1) must hold before a cascade is built. For RP-1 (1 kg, J = (0.003, 0.003, 0.005) kg·m², motor lag τ = 20 ms, 12 N per rotor):

FieldRP-1UnitFrom
rate.inertia0.003, 0.003, 0.005kg·m²the vehicle’s J
rate.axis[i].kp501/s1/τ
rate.axis[i].kd0.412·0.7 − 1
rate.axis[i].ki2501/s²k_p·ω_n/10
rate.axis[i].derivative_tau2msτ/10
rate.axis[i].output_max622, 622, 76.8rad/s²the full differential thrust over J
rate.axis[i].integral_max156, 156, 19.2rad/s²a quarter of the output limit
attitude.gain10, 10, 51/sω_n/5, and ω_n/10 for yaw
attitude.max_rate10, 10, 4rad/sfixed
position.position_gain1.561/sω_n/32
position.velocity[i].kp, .ki3.57, 1.281/s, 1/s²ω_n/14, k_v²/10
position.velocity[i].output_max, .integral_max9.81, 4.90m/s²g, g/2
position.max_speed_xy, _up, _down8, 3, 2m/sfixed
position.max_tilt0.7 (40°)radfixed
position.min_thrust, max_thrust0.98, 38.4N10 % of hover; 80 % of 4·T_max
attitude_divider, position_divider4, 20ticks250 Hz and 50 Hz at a 1 kHz rate loop

Every row is a derived default; set a field directly to override it. The motor lag and the inertia matter most: the rate loop’s gains come from τ, and its torque from J. Measure both on your vehicle (τ from a step on a thrust stand, J from a swing test or CAD) rather than keep RP-1’s.

The defaults assume a 1 kHz rate loop: call update() once per tick with dt = 1 ms. Another rate-loop period works as long as dt is what you pass: the PIDs integrate over dt, and the position loop over dt · position_divider. Keep the outer loops at about 250 Hz and 50 Hz when you change it, by changing the dividers: the gains assume each loop is sampled well above its bandwidth. position_divider must be a multiple of attitude_divider.

VehicleState is position and velocity in the world frame (z up, m and m/s), the attitude as a body-to-world unit quaternion, and the body rates in rad/s. It comes from your estimator. The cascade does no filtering of its own, beyond the rate loop’s derivative filter, and the closed-loop tests feed it the true state. So:

  • Body rates should be the gyro, filtered for the motors’ noise (see the RPM notch), with as little delay as you can manage. The rate loop is the fastest loop, and delay there costs phase margin first.
  • Delay. The tests show the loops stay stable, with at most 25 % overshoot, with 4 ms of command delay combined with slower motors and more inertia. Treat 4 ms end to end as the budget, and measure your stack’s.
  • Frames. A state in another convention (NED, FRD) must be converted before it reaches the cascade. A wrong sign on one axis turns its loop into positive feedback.
  • Position mode (update(x, PositionSetpoint, dt)) for autonomous flight. Give the setpoint the path’s velocity and acceleration as feed-forward when you have them: without it the vehicle trails the setpoint (2.7 m on the explainer’s square at 5 m/s).
  • Attitude mode (update(x, AttitudeTarget, dt)) for a pilot’s angle mode, or for an outer loop of your own. You supply the collective thrust.
  • Switching from attitude to position mode resets the position loop, so the vehicle flies toward the position setpoint from where it is, with fresh integrators. Set the setpoint to the current position at the switch if you want it to hold.
  • Arming. Call reset() when the vehicle arms. On the ground the loops’ integrators wind up against the ground’s reaction, and the mixer spins the rotors up whenever torque is asked for (airmode). Hold the cascade reset, and the motors at idle, until take-off; see the mixer’s arming and idle.

Once per rate tick, in this order:

const control::Wrench<double> w = cascade.update(state, setpoint, dt);
const control::MixerOutput<double> out = mixer.mix(w);
cascade.feedback(out); // the integrators see this tick's cuts next tick
const model::RotorCommand<double> u = mixer.to_rotor_command(out.thrusts);
esc.write(u);

Call feedback() with the result of mixing the wrench update() just returned, before the next update(). Without it the rate integrators wind up while the mixer cuts, and the vehicle overshoots once the cut ends.

What does the cascade fly from a setpoint with feed-forward? 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 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
Tilt [rad] target flown
t = 0.000 s / 18.000 s

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

The waypoint square of the explainer: a 10 m square at 5 m height, each leg from rest to 5 m/s and back in 4 s, with the profile's velocity and acceleration as feed-forward. The translucent vehicle is the setpoint, at the attitude target the velocity loop sends. The vehicle stays within 0.11 m of the setpoint and 0.07 m of the square. 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
LimitRP-1When it cutsWhat holds the integrators
Horizontal speed setpoint8 m/sa far position setpoint(a velocity setpoint, not integrated)
Climb and descent speed3, 2 m/sa far vertical setpoint(as above)
Collective thrust0.98 to 38.4 Na hard climb or descentthe cut, on the next position tick
Tilt40°a hard horizontal accelerationthe cut, on the next position tick
Velocity-loop acceleration±g per axisa large velocity errorthe PID’s own output limit
Body-rate setpoint10, 10, 4 rad/sa large attitude error(a rate setpoint, not integrated)
Angular acceleration622, 622, 76.8 rad/s²a large rate errorthe PID’s own output limit
Rotor thrust, in the mixer0 to 12 Nany of the above at the edgesthe mixer’s report, through feedback()

The collective limit leaves 20 % of the rotors’ thrust free for torque, so in position mode the mixer keeps room for the tilt even at the top of the collective. In attitude mode your collective goes straight to the mixer, so a full-throttle command leaves the mixer to give up yaw, and then the collective, to keep the tilt.

Change the gains when the vehicle differs from what cascade_config_from() assumes: a different motor lag, a different airframe stiffness, or a measured delay larger than 4 ms. Keep the ratios between the loops: each loop at most about a fifth as fast as the one inside it. The roll-step widget shows what happens when the attitude and rate gains come close. Check the result with cascade_config_valid(), and fly the closed-loop tests in tests/sdk/cascade_closed_loop.cpp against your vehicle’s QuadrotorParams before you fly.

A vehicle with a thrust-to-weight ratio of 1.25 or less cannot hover inside the derived collective limit, and cascade_config_from() returns an invalid config for it. Raise position.max_thrust only if you accept less torque authority at the top of the throttle.

About 16 ns per rate-loop tick on x86, for float and double alike (bench/cascade_bench, GCC 15.2 Release, i7-12700K), averaged over the schedule: a rate update costs about 8 ns, an attitude update 4 ns, and a position update 40 ns. The operation count of each tick kind is bounded, over generated states and setpoints (tests/sdk/cascade_resource.cpp): a rate-only tick runs 72 arithmetic operations, an attitude tick at most 122 and one square root, and a position tick at most 302 and ten calls (square roots, sine, and cosine). Measure on the flight processor before budgeting.

The unit tests and three of the closed-loop tests (hover hold, the 30° roll step, and the 1 m steps) run in double and float. The cascade uses + − × ÷, comparisons, sqrt, sin, cos, and isfinite.

Preconditionscascade_config_valid(cfg) at construction; a finite state with a unit attitude; a finite setpoint; dt > 0, else the last wrench is returned unchanged
PostconditionsA wrench whose thrust is the target’s; rate setpoints within their limits; in position mode, the target’s thrust within the collective limits and its tilt within the tilt limit
FaultsNone reported: a precondition violation is an assert in Debug builds, and dt ≤ 0 holds the output
StateEach loop’s PID integrators and held outputs, the tick counter, the mode, and the stored mixer report; reset() clears them
ResourcesNo allocation, no exceptions, no system calls; a bounded operation count per tick kind
Not providedSensor filtering or estimation, arming and idle, throttle-PID attenuation, I-term relax, gain scheduling, INDI, drag compensation, setpoint differentiation, staleness handling of an external rate command