Back to Home

Lab 7: Kalman Filter

Week 7 | March 2026

Collaborators: Robby Leslie

1. Objective

The goal of this lab was to build a Kalman Filter (KF) for the robot's position estimation and integrate it into the existing PID position controller from Lab 5. The KF replaces the linear extrapolation used previously, providing a physically-grounded state estimate that runs at the full controller loop rate (~204 Hz) rather than being limited to the ToF sensor update rate (~27 Hz).


2. Step 1: Estimating Drag and Momentum

Before building the KF, the system's physical parameters — drag d and momentum m — needed to be estimated from real robot data. This was done using an open-loop step response: the robot was driven toward a wall at a constant PWM=120 while logging ToF distance at ~27 Hz.

Three runs were attempted. Run 1 started ~4m from the wall and was unusable — the ToF sensor saturated at its long-mode ceiling (~3975mm) for the entire acceleration phase, so no valid speed data was captured. Run 2 (start ~2.7m) and Run 3 (start ~3.5m) both produced usable data, with Run 3 being cleaner since the robot had more room to accelerate toward steady state.

A key data processing issue was identified: the Arduino loop runs at ~100 Hz but the ToF sensor only updates at ~27 Hz, so ~75% of logged rows were duplicate distance values. Feeding these into np.gradient created large artificial speed spikes. The fix was to deduplicate — computing speed only between rows where distance actually changed — then applying a 5-point median filter and clipping outliers beyond 3×IQR from the median.

Step response Run 3 — 90% rise time annotation showing t90 ≈ 1.25s

Figure 1: Step response Run 3 with 90% rise time annotated (~1.25s). Used to extract steady-state speed and rise-time for drag/momentum estimation.

Speed vs time after 5-point median filter — spike removal visible

Figure 2: Computed speed after deduplication and 5-point median filter. Artificial spikes from duplicate ToF readings are suppressed.

Clean step response Run 3 — distance and speed vs time

Figure 3: Clean step response (Run 3). Distance (top) and filtered speed (bottom) vs time. Steady-state speed ≈ 2529 mm/s.

From Run 3, steady-state speed was measured as ~2529 mm/s and 90% rise time as ~1.25s. Averaged across Run 2 and Run 3:

Parameter Value
d (drag) 0.000200
m (momentum) 0.000101

3. Step 2: Building and Tuning the KF in Python

With d and m estimated, the state-space model was constructed and discretised at ΔT = 34ms (matching the ToF update period):

Delta_T = 0.034   # seconds

A = np.array([[0,      1   ],
              [0,  -d/m    ]])
B = np.array([[0    ],
              [1/m  ]])

Ad = np.eye(2) + Delta_T * A
Bd = Delta_T * B
C  = np.array([[-1, 0]])   # measure negative position (distance = -x[0])

The state vector is x = [position, velocity]ᵀ where position is the negative distance from the wall. C = [-1, 0] because the ToF measures positive distance, which equals -x[0].

Noise covariances were tuned iteratively. The critical discovery during tuning was a unit inconsistency: d and m were estimated with u = PWM/255 (absolute normalisation), but the KF was initially being fed u = PWM/STEP_PWM = 1.0, effectively doubling the modelled thrust and pushing the estimated terminal velocity to ~5000 mm/s. Correcting to u = PWM/255.0 immediately brought the velocity estimate in line with the measured ~2500 mm/s.

Final noise parameters:

sigma_1 = 25.0   # position process noise (mm)
sigma_2 = 5.0    # velocity process noise (mm/s)
sigma_3 = 30.0   # ToF sensor noise (mm)
KF Python tuning — position estimate vs raw ToF (zoomed)

Figure 4: KF Python tuning — KF distance estimate tracking raw ToF. Velocity estimate converges to ~1950 mm/s.

KF Python tuning — full run with velocity estimate overlay

Figure 5: Full KF tuning run. KF position (blue) tracks raw ToF cleanly; velocity estimate (red) is smooth and physically consistent.


4. Step 3: Integrating KF into the Arduino PID Controller

The KF was ported to the Artemis using the BasicLinearAlgebra library. The linear extrapolation from Lab 5 was removed entirely — the KF predict step handles inter-reading position estimation, which is more physically principled. The key architectural change is that the KF runs two modes every loop iteration:

  • Predict-only (~204 Hz): advances the state using the physics model and last motor command.
  • Predict + Update (~27 Hz): incorporates a fresh ToF reading when one is available.
// Predict every loop; update only when ToF fires
kf_step(u_norm, (float)tof_last, tof_updated);
float dist_kf = kf_distance();   // fed to PID instead of extrapolated distance

5. Step 4: PID Tuning with KF

Initial runs with the Lab 5 gains (Kp=0.08, Ki=0.002, Kd=1.5) caused the robot to hit the wall at ~3200 mm/s. The KF's smoother position estimate meant the D term was no longer damping sensor noise — it was reacting to real velocity, requiring retuning.

The braking zones were tightened to start acting earlier:

if (dist_kf < 2000) p_out = min(p_out, 150.0f);
if (dist_kf < 1200) p_out = min(p_out, 100.0f);
if (dist_kf <  700) p_out = min(p_out,  50.0f);
if (dist_kf <  400) p_out = min(p_out,  30.0f);

Final tuned gains: Kp=0.04, Ki=0.001, Kd=2.5, SP=304mm. The robot approaches at ~1550 mm/s, decelerates smoothly, and parks at the setpoint with no overshoot.

Final PID+KF run — distance and KF estimate vs time showing smooth deceleration to setpoint

Figure 6: Final PID+KF run. The KF estimate (blue) guides the controller smoothly to the 304mm setpoint with no overshoot.

Video 1: Robot driving to wall and parking at 304mm setpoint using Kalman Filter-guided PID.


6. Step 5: Demonstrating KF Runs Faster Than the Sensor

KF vs ToF rate plot — staircase ToF vs smooth KF estimate, zoomed 200ms window below

Figure 7: KF vs ToF rate comparison. Top: raw ToF (staircase) vs KF estimate (smooth continuous line). Bottom: 200ms zoom showing ~7 controller iterations per sensor update, with predict-only intervals shaded.

The plot above demonstrates the core KF advantage. The controller loop runs at 204.4 Hz while the ToF sensor updates at 28.3 Hz — a 7.2× difference. The top panel shows raw ToF as a staircase (flat between sensor firings) vs the KF estimate as a smooth continuous line. The bottom panel zooms into a 200ms window, showing ~7 controller iterations per sensor update with the predict-only intervals shaded. Without the KF, the PID would use stale position data for up to 37ms between updates. With the KF, it receives a fresh state estimate every ~5ms.


7. Results Summary

Parameter Value
Controller loop rate 204.4 Hz
ToF sensor rate 28.3 Hz
KF speedup factor 7.2×
Final gains Kp=0.04, Ki=0.001, Kd=2.5
Setpoint 304 mm
Peak approach speed ~1550 mm/s
Steady-state error ~0 mm

AI Usage: AI was used in this lab for cross-checking calculations, comparing CSV data across runs, and tuning the filter. Speed was essential.