Altitude Kalman filter¶
flowchart LR
IEKF["attitude IEKF<br/>quaternion q"] --> AZ["a_z = (e₃ᵀ R f / |f₀| − 1) g₀"]
ACC["accelerometer f<br/>(window mean, calibrated)"] --> AZ
AZ --> PRED["predict 240 Hz<br/>x = [h, v, b_b, b_a]"]
BARO["barometer 50 Hz<br/>p → ISA altitude"] --> UB["update<br/>z = h + b_b"]
TOF["ToF 30 Hz<br/>ρ cos θ − d_lens"] --> GATE{"3σ gate,<br/>re-anchor after 0.5 s"}
GATE --> UT["update<br/>z = h − g"]
PRED --> UB --> OUT["h, v → flight core,<br/>FLIGHT altRel / vz"]
PRED --> UT --> OUT
The attitude IEKF (Attitude IEKF, IEKF on the microcontroller) estimates only the attitude. Takeoff, landing, the hold
modes and the ceiling also need the height and the vertical speed. A
second, independent filter estimates them: App/filters/altitude_kf/alt_est.c.
Why a separate, linear filter¶
- Cascade on the attitude. The altitude filter takes the attitude from the IEKF as an input; it does not feed back into it. The IEKF stays exactly the filter validated against the MATLAB reference (Validation against the reference). Height errors cannot disturb the attitude, which is the quantity the drone needs most.
-
A linear Kalman filter, not an EKF. The non-linear parts sit outside the filter, where they cost nothing in accuracy:
- the pressure is converted into an altitude before the update;
- the ToF range is multiplied by \(\cos\) of the tilt before the update;
- the specific force is rotated into the world frame with the IEKF attitude before the prediction.
What enters the filter is linear in the state, so there are no Jacobians and no linearisation error. It would become an EKF only if the raw pressure, or the attitude itself, were part of the state or of the measurement model.
- Small. It has 4 states and scalar updates, about 150 multiply-adds per prediction, with nothing to invert.
State and process model¶
World frame NWU (\(z\) up), heights in metres:
| State | Meaning |
|---|---|
| \(h\) | height above the reference (the first baro samples, or the floor below the drone at the first ToF reading) |
| \(v\) | vertical speed, + up |
| \(b_b\) | barometer bias: what the baro reads in excess of \(h\) (weather, temperature drift, prop wash) |
| \(b_a\) | bias of the vertical acceleration (accelerometer offset along \(z\) and residual tilt error) |
Input: vertical acceleration. The accelerometer measures the specific force \(f = R^\top (a - g)\) in body axes, with \(g = (0, 0, -g_0)\). In the world frame,
where \(q = [w, x, y, z]\) is the IEKF quaternion. \(f\) is the calibrated,
rotation-compensated window mean acc_mean (Rotation-compensated accelerometer mean (accwin)), in g. \(\|f_0\|\) is the
magnitude measured at rest during the static calibration (nom_acc), so the
input is zero at rest even with a small residual scale error. The function is
alt_est_accel_up().
Continuous model:
Here \(w_a\) is the acceleration noise (sensor noise plus vibration), and \(b_b\) and \(b_a\) are random walks.
Discrete model with step \(\Delta t\) (one acc window, 1/240 s):
\(P\) is symmetrised after every step, in single precision as in the IEKF.
Measurements¶
All updates are scalar, \(z = H x + n\), with \(K = P H^\top / S\), \(S = H P H^\top + r\), \(x \leftarrow x + K (z - Hx)\) and \(P \leftarrow P - K H P\).
Barometer, 50 Hz. It is read through the IMU sensor hub and arrives in the same FIFO, so it shares the IMU time base.
This is the ISA altitude relative to \(p_0\), the mean of the first 25 samples
(0.5 s). At that moment \(b_b\) is set so that the baro agrees with the current
\(h\). Absolute altitude does not matter: FLIGHT altRel is measured from the
take-off point, captured at ARM.
ToF height, 30 Hz (VL53L1X, ACwing Flow). The range \(\rho\) is measured along the board normal, so the height of the lens above the floor is \(\rho \cos\theta\), with \(\cos\theta = R_{33}\) from the IEKF:
- \(d_{lens}\) = 11.3 mm is the lens height when landed (frame C2).
- \(g\) is the floor level in the \(h\) frame. It is a parameter, not a state: it is set at the first ToF reading and moved only by the re-anchoring below.
- Rejected outright: a reading out of range (\(\rho\) > 4 m), a range status that is not valid, or a tilt above 30°.
On the floor. The VL53L1X reads short or nothing below about 40 mm, and the lens is 11 mm above the floor when landed. A reading below \(\rho_{min}\) + 20 mm, or no return right after a height below 0.15 m, means "on the floor":
- the filter applies \(z_t = 0\) with \(\sigma\) = 3 cm;
tof_groundis reported, which the take-off and touchdown logic use (TAKEOFF, LAND, HOLD).
Gating and re-anchoring¶
A ToF reading is gated when its normalised innovation exceeds 3σ:
This happens when an object passes under the drone, or when the drone flies
over the edge of a table. After 15 consecutive rejections (0.5 s) the
step is taken as real and the floor is re-anchored, \(g \leftarrow h - z_t\).
The height above the new floor is then right at once, while \(h\) (and with it
altRel) stays continuous.
Observability¶
| Sensors | What is observable | What the filter does |
|---|---|---|
| baro only | \(h + b_b\) only: the two cannot be separated. \(b_a\) is observable from the second derivative of the baro | \(\sigma_{bb}\) is set to 0, so \(b_b\) is frozen (no process noise) and its variance does not grow without limit. \(h\) follows the baro, drift included: barometric weather drift, ~0.1 m/min, is not corrected |
| baro + ToF | everything: the ToF fixes \(h - g\), the baro difference gives \(b_b\) | \(\sigma_{bb}\) is enabled while a ToF sample was used in the last 1 s. The baro bridges ToF gaps (out of range, tilt, gating) with the bias it has just learned |
| ToF only | \(h\), \(v\), \(b_a\) | works the same way, but without a fallback during ToF gaps |
This is why both models use the same filter. With GNSS outdoors the baro is the reference. Indoors the ToF dominates near the floor, and the baro takes over above 4 m or over gaps.
Tuning (alt_est_default_cfg())¶
| Parameter | Value | Note |
|---|---|---|
| \(\sigma_a\) | 0.5 m/s² | vibration in flight dominates; to be tuned on the first flights from the vibration level in SYSTEM |
| \(\sigma_{ba}\) | 0.01 m/s²/√s | slow accelerometer drift |
| \(\sigma_{bb}\) | 0.05 m/√s | only with ToF |
| \(\sigma_b\) | 0.30 m | LPS22 noise is a few cm at 50 Hz with averaging; the margin covers prop wash |
| \(\sigma_t\) | 0.02 m + 1 % of the range | VL53L1X long mode |
| ToF range | 0.04–4 m, tilt ≤ 30° | |
| Gate, re-anchor | 3σ, 15 rejects | |
| \(P_0\) | diag(0.01, 0.01, 1, 0.04) |
Rates: prediction 240 Hz in the ImuTask, right after the IEKF accelerometer
update (on_state); baro 50 Hz (correct_events); ToF 30 Hz (take_ext, from
the ExtTask queue). Everything runs in the ImuTask, the only owner of the
state, so there are no locks.
Host tests (00 - FC_FW/Tests/host):
- a 60 s profile with 0.5 m/s² steps, accelerometer noise and bias, and a baro with 0.25 m noise and drift. The height RMS stays below 0.15 m with the baro alone and below 0.03 m with ToF, while the baro drifts at 0.1 m/s; the vertical speed RMS stays below 0.08 m/s;
- re-anchoring on a 0.7 m step;
tof_groundwith a short reading;- \(a_z\) = 0 at rest and at 30° of tilt.
Limits and next steps¶
- Not tuned on hardware yet. \(\sigma_a\), \(\sigma_b\) and the gate come from the datasheets and from simulation.
- Prop wash and ground effect bias the baro near the floor and during fast climbs. With ToF this ends up in \(b_b\); without ToF it is height error. A thrust-dependent baro correction is a phase-3 item.
- Vibration. The input is the 4-sample window mean at 240 Hz, so motor vibration above ~100 Hz aliases into \(a_z\). The FC damping mounts (isolation band ~90–120 Hz, mechanical handoff in the hardware repository) and the SYSTEM vibration figure on the first flights decide whether a dedicated low-pass is needed.
- Floor model. The ToF assumes a flat floor under the drone. Slopes and stairs show up as re-anchoring steps.
- The filter is linear on purpose. Should the thrust model of phase 3 enter the prediction (thrust divided by mass instead of the accelerometer, or both fused), the state stays the same and only \(B\) and \(Q\) change.