Deep dive ② · Analyze first
A purpose-built QX-250 6-DOF model in Simcenter Amesim, driven by a full cascade flight controller — position → velocity → attitude → body-rate, plus a Quad-X mixer. It holds position, tracks a step, rejects a gust and flies a waypoint mission — closing the drift the first pass exposed.
From demo to purpose-built
The first pass reparametrized a Siemens-shipped demo model and honestly reported its limit: the demo's controller was attitude-only, so a free hover held its angles but drifted in heading and position. That finding is exactly what "simulate early" is for — so the next step was to build a purpose-built QX-250 model with a proper cascade controller and re-run the battery. The drift is gone: the vehicle now holds position, tracks commands, rejects disturbances, and flies a mission. The controller validated here is the flight-control software.
The battery
Now native in Amesim
The reference design validated the control law; the next step was to make it a native Simcenter Amesim model, not just Python output. The full 6-DOF plant + cascade controller is now a hand-authored Amesim submodel — 12 states (position, velocity, Euler angles, body rates) with the setpoints as parameters — built in the Submodel Editor, compiled by Amesim, and run in the GUI. The images below are screenshots of Amesim's own result plots: the in-tool screens promised in the earlier pass.
Reduced-order model
The last step makes the model portable. The same validated dynamics are wrapped as a standards-compliant FMI 2.0 Co-Simulation FMU — qx250.fmu — so the QX-250 can drop into any FMI master (Simulink, PyFMI/FMPy, Simcenter, Teamcenter, a HIL rig) with no Amesim runtime needed. It's not a linearized surrogate: it's the full nonlinear 6-DOF plant + cascade controller, so it keeps the very saturation behavior the rigor note above is about. The setpoints are live inputs (drive it in the loop), and all 12 states come back as outputs.
# drive it from Python in three lines from fmpy import simulate_fmu r = simulate_fmu("qx250.fmu", start_values={"z_ref": 2.0, "x_ref": 3.0})
Fault tolerance
A vehicle that has to be trusted can't have a single point of failure. The QX-250 carries two battery packs and two flight-control channels, and the model earns that claim the only way that counts: inject the fault and watch what happens with the backup and without it. Both packs share a common bus; both controllers run the identical cascade law in hot standby, arbitrated by a health monitor. The electrical and flight sides are coupled — motor power is drawn from the bus, and bus sag scales the thrust available — so a power loss with no backup genuinely drops the vehicle out of the sky. These runs carry the real dual-pack all-up mass — 0.65 kg — since the second battery is exactly what buys the redundancy.
Re-verified at the as-built mass
Reliability redundancy and the airframe pushed the QX-250 from its 0.50 kg single-string design mass to a real 0.65 kg (2× 1100 mAh packs, dual FC, landing gear + aeroshell). That raised the honest question this page had flagged — do the flight dynamics still hold at the heavier as-built mass? — so the validated 6-DOF model was reparametrized to 0.65 kg with scaled inertia and re-flown. It does: hover holds, a 15° upset recovers, and the mission tracks and lands — with thrust-to-weight still 3.83, comfortably above the REQ-PERF-001 ≥ 2.0 floor.
Where this connects
The cascade control law proven here — outer position/heading loop → velocity → attitude → inner body-rate PID → Quad-X mixer — is the "what" half of the flight computer. It runs on the MCU; the FPGA owns the "when" (deterministic sampling, jitter-free motor output and a hardware failsafe). See the Flight Computer page for that partition.
def controller(state, ref):
pos, vel, eul, omega = state[0:3], state[3:6], state[6:9], state[9:12]
phi, th, psi = eul; pos_ref, yaw_ref = ref
# OUTER position error -> desired velocity -> desired acceleration
vel_des = KP_POS * (pos_ref - pos)
acc_des = KP_VEL * (vel_des - vel) - KD_VEL * vel
# the thrust + tilt that produce that acceleration
T_des = M * (acc_des[2] + G) / max(np.cos(phi)*np.cos(th), 0.5)
th_des = np.clip( ax_b/G, -TILT_MAX, TILT_MAX) # ax_b, ay_b:
phi_des = np.clip(-ay_b/G, -TILT_MAX, TILT_MAX) # accel, yaw-rotated
# INNER attitude error -> desired body rate -> moments (PD)
rate_des = KP_ATT * att_err
M_cmd = KP_RATE * (rate_des - omega) - KD_RATE * omega
return T_des, M_cmd
Every plot on this page is that function in the loop. It's the control law the onboard flight software implements — the tuned gains (KP_POS, KP_ATT, KP_RATE…) are what make the QX-250 hold position instead of drift.