Skip to content

Quadrotor Model Deployment

This guide is for integrators who use QuadrotorModel as the plant in a simulator, today racing-drone’s SITL, and later as an MPC prediction model. It covers what to set, how to call the model, where its results can be trusted, and what it does at the edges. The developer guide has the equations and the API.

QuadrotorParams<T> is a plain aggregate, and its fields map one-to-one onto racing-drone’s VehicleConfig. The defaults are RP-1’s (vehicles/rp1.yaml). quadrotor_params_valid() (I1) must hold before a model is built.

FieldUnitRP-1What it sets
masskg1Weight and translational inertia
inertiakg·m²(0.003, 0.003, 0.005)Diagonal J, body frame
arm_lengthm0.11Centre to rotor axis; the roll and pitch lever arm is arm/√2
thrust_coefficientN/(rad/s)²8.9e-7k_f in T = k_f·ω²
torque_coefficientm0.016k_m: yaw torque per newton of thrust
max_thrustN12Per rotor; sets ω_max = √(max_thrust / k_f)
motor_time_constants0.02τ of the first-order motor lag; also bounds dt
drag_linear, drag_quadraticN/(m/s), N/(m/s)²0Body-frame drag, per axis
gravitym/s²9.80665
ground_zm0Height of the ground plane, world frame
battery_capacityJ1e5Usable energy
battery_sag10Thrust fraction lost when the battery is empty, in [0, 1)
power_coefficientW/(rad/s)³0k_p in rotor power = k_p·ω³; 0 means no drain

With RP-1, battery sag is off: battery_sag and power_coefficient are 0. Enable both for a run where endurance or end-of-flight authority matters (battery sag below).

The parameters are not yet identified from flight data. Validation (BB-L4 in the V&V plan) will replace the RP-1 values with measured ones and will report the residuals.

  • Step size. step() requires 0 < dt ≤ min(10 ms, τ), which is dt ≤ 10 ms with RP-1. RK4 becomes unstable on the motor lag at about 2.78 τ, so the bound keeps a margin. Racing-drone runs at 1–2 kHz, which is well inside it.
  • Commands are held. The command passed to step() applies for the whole step (zero-order hold). Commands clamp to [0, ω_max] inside the model.
  • Cost. About 120–130 ns per step on x86 (GCC Release), so a 2 kHz loop spends well under 0.1 % of its time on the model. step() never allocates, throws, or makes system calls, and its operation count is fixed for each contact arm. On the flight MCU, measure the time per step there.
  • Determinism. step() is a pure function: the same inputs give the same bits within one build. Different compilers can differ in the last bits, because of FMA contraction (finding 5 in the V&V plan). Compare runs across builds with a tolerance, as npm run trace-diff does.

The model leaves out rotor drag and blade flapping, induced-velocity thrust loss in fast forward flight and climb, ground effect, motor dynamics beyond the lag and the speed clamp, contact dynamics, wind, and interference between rotors.

Until validation measures it, the expected envelope is:

QuantityExpected range
Airspeedbelow about 5 m/s
Body ratesbelow about 10 rad/s
Rotor speeds10 % to 95 % of ω_max

Outside the envelope the model still runs and keeps its invariants, but its results are unvalidated. A racing drone leaves this envelope in most of a race, so treat lap-time results from SITL as a comparison between controllers, not a prediction of the real vehicle.

The model clamps each command to [0, ω_max]. It does nothing else: it has no priority between thrust and torque. A command above ω_max on one side of the vehicle loses that side’s share of a torque command.

What happens to a roll command at full throttle? 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
Thrust arrows: 1 N = 4 cm · Grid: 0.5 m · Body axes: x red, y green, z blue
Rotor speeds [rad/s] 0 FR (= 3 RR) 1 RL (= 2 FL) dashed: command
Rotor speeds near ω_max (zoomed) [rad/s] 0 FR 1 RL ω_max dashed: command
Roll torque [N·m] τ_x
Roll angle [rad] roll
Speed [m/s] |v|
t = 0.000 s / 0.800 s

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

The vehicle hovers at 1 m. At 0.1 s every rotor is commanded to ω_max, about 3672 rad/s. The rotors reach 95 % of ω_max 50 ms later, which leaves the expected envelope, and the vehicle climbs at about 3.9 g. It passes 5 m/s at 0.26 s, out of the airspeed envelope too. At 0.5 s a roll command adds 20 rad/s to the left rotors and takes 20 rad/s from the right rotors. The left rotors are already at ω_max, and their commands are clamped, so they stay there; only the right rotors slow down. The zoomed plot shows it: the left command (dashed) sits 20 rad/s above ω_max, and the left rotor does not follow it. The vehicle rolls with half the torque the command asked for, about 0.02 N·m, and loses a little thrust as it does. By 0.8 s it has rolled 0.27 rad (15°) and climbed to 9.7 m.

  1. full throttle: every rotor commanded to ω_max
  2. rotor speeds above 95 % of ω_max: outside the expected envelope
  3. airspeed above 5 m/s: outside the expected envelope
  4. roll command ±20 rad/s: the left rotors are clamped, only the right respond

To keep attitude authority near full throttle, send rotor commands through control::Mixer::mix(). It lowers the collective thrust to make room for roll and pitch, and reports when it did. Commanding the model directly bypasses that.

With battery_sag > 0, thrust per rotor speed falls as the energy drawn, E, grows: s=1−sag⋅min⁡(E/capacity,1)s = 1 - \mathrm{sag}\cdot\min(E/\mathrm{capacity}, 1). Hovering then needs faster rotors, and the most thrust the vehicle can make falls with s.

How much headroom does a draining battery leave? 1× real time
Rotor speed for hover [rad/s] rotor 0 dashed: command
Thrust scale s s
Maximum thrust to weight 4·s·T_max / mg
Battery used E / capacity
t = 0.000 s / 12.000 s

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

The vehicle hovers at 1 m. The battery is shortened to 1.5 kJ, instead of RP-1's 100 kJ, so that it empties in seconds. With battery_sag 0.3 and power_coefficient 8e-9 W/(rad/s)³, hover draws about 150 W. Each step, the scenario commands the rotor speed whose thrust at the current scale carries the weight, as a perfect altitude controller would. The hover speed climbs from 1660 to 1984 rad/s as the thrust scale falls from 1 to 0.7. The battery is empty at 8.1 s, and from then on the scale stays at 0.7. The most thrust available falls from 4.9 to 3.4 times the weight. The rotors trail the rising command by their lag, so the vehicle sinks about 0.5 m over the run.

  1. battery empty: thrust per rotor speed is 70 % of full; it falls no further

At the end of a flight, an aggressive manoeuvre that fitted at take-off may no longer fit. Give the controller’s thrust limits the sagged value, or reserve headroom for it.

Contact is a stop. A grounded vehicle stays put while its vertical thrust is at most its weight, and a vehicle that reaches the ground stops at once and keeps its attitude, even inverted. There is no friction, bounce, or tip-over. This suits take-off and idle on the pad. It does not suit landing or crash studies. See the ground contact animation.

step() requires a valid state (I2–I5), and a Debug build asserts it. If your code writes the state between steps, for example to reset it from an estimator, check it with quadrotor_state_violation(). The developer guide shows how a caller bug produces a violation and how to find it.

PreconditionsValid parameters (I1); a valid state (I2–I5); finite commands; 0 < dt ≤ min(10 ms, τ)
PostconditionThe new state is valid (I2–I5)
PurityA function of (state, command, dt) and the parameters; no hidden state
ResourcesNo allocation, exceptions, or system calls; a fixed operation count per contact arm
Scalar typesAny type with the arithmetic, sqrt, abs, isfinite, and numeric_limits; tested in double, float, posit<16,1>, and bfloat16
Not smoothThe rotor clamp, the energy clamp, and ground contact. An MPC should linearize derivative() in free flight