In this guide
Sensor Fusion Basics: Combining IMU Readings the Right Way
Your self-balancing robot stands for ten seconds, then slowly leans over and face-plants. You check the code: the tilt angle comes from the accelerometer. It looked fine on the desk. But the moment the motors spin, vibration shakes the reading, the angle jumps around, the controller overcorrects, and down it goes.
So you switch to the gyroscope. Beautifully smooth — for about thirty seconds. Then the "level" angle has drifted five degrees to the left, the robot compensates for a tilt that isn't there, and down it goes again.
Neither sensor is broken. The accelerometer measures the direction of gravity but gets confused by any other acceleration (vibration, motion, bumps). The gyroscope measures rotation speed beautifully, but you have to integrate it to get an angle — and integration accumulates every tiny bias error into drift. Each sensor is excellent at what the other is bad at. Sensor fusion is the art of combining them so the weaknesses cancel: trust the gyroscope for fast changes, trust the accelerometer for the long-term average, and get an angle that is both smooth and stable.
This guide builds that understanding from the physics up: what each IMU sensor actually measures, the two-line complementary filter that solves 90% of student projects, the Kalman filter for when you need the last 10%, and the practical mistakes — calibration, timing, mounting — that break filters that are mathematically perfect.
What each sensor actually measures
An IMU (inertial measurement unit) like the MPU6050 bundles three sensors. Understanding what each one truly senses is the whole game — most fusion bugs are really misunderstandings of the raw data.
Accelerometer: which way is down (plus everything else)
An accelerometer measures proper acceleration: the acceleration of the sensor relative to free fall. Sitting still on your desk, it reads about 1g pointing up (the desk pushing back against gravity — the sensor feels the support force, not gravity itself). Tilt the sensor and that 1g vector rotates across the axes, which is why you can compute a tilt angle from it:
tilt = atan2(ax, sqrt(ay^2 + az^2))
The problem: it measures all acceleration, not just gravity. Motor vibration, the robot accelerating forward, a bump on the floor — all of it adds to the reading. On a vibrating robot, the "tilt" from the accelerometer alone can swing ±10 degrees around the truth. It's a great long-term reference (gravity is always there, always pointing down) and a terrible short-term one.
Gyroscope: how fast you're rotating (then you do the maths)
A gyroscope measures angular velocity — degrees per second around each axis. It's beautifully immune to vibration and linear motion: shake the robot without rotating it and the gyro barely notices. To get an angle, you integrate:
angle = angle + gyro_rate * dt
Two problems creep in. First, bias: the gyro reads a small non-zero rate even when perfectly still (a degree or two per second is typical for cheap MEMS). Integration turns that constant bias into a ramp — 2°/s of bias becomes 120° of error after one minute. Second, dt must be accurate: if your loop timing jitters, the integration is wrong in a way that looks like noise. The gyro is a great short-term sensor and a hopeless long-term one — the exact mirror of the accelerometer.
Magnetometer: which way is north (when metal behaves)
A magnetometer measures the Earth's magnetic field direction — an absolute heading reference, like the accelerometer's gravity reference but for yaw (rotation around the vertical axis), which neither accel nor gyro can stabilise. The catch: any nearby metal, motors, or current-carrying wires distort the local field (hard-iron and soft-iron distortion), and calibrating that out is its own project. For tilt-only applications (balancing robots, camera gimbals' pitch/roll), you don't need it. For heading-hold on a drone or rover, you do — and you calibrate it.
The core idea: trust each sensor where it's strong
Plot the two error profiles side by side:
- Accelerometer angle: correct on average, noisy moment-to-moment (high-frequency noise).
- Gyroscope angle: smooth moment-to-moment, wrong on average over time (low-frequency drift).
The errors live in different frequency bands, so a filter can keep the good part of each: take the high-frequency content from the gyro (fast, smooth response) and the low-frequency content from the accelerometer (long-term truth). That is literally all a complementary filter is — a high-pass filter on the gyro plus a low-pass filter on the accelerometer, with the two cutoffs matched so they sum to the true signal.
The complementary filter: five lines that fix most projects
import math, time
ALPHA = 0.96 # how much we trust the gyro
def tilt_from_accel(ax, ay, az):
return math.degrees(math.atan2(ax, math.sqrt(ay*ay + az*az)))
angle = 0.0
last = time.monotonic()
while True:
ax, ay, az, gx = read_imu() # gx in degrees/second
now = time.monotonic()
dt = now - last
last = now
gyro_angle = angle + gx * dt
accel_angle = tilt_from_accel(ax, ay, az)
angle = ALPHA * gyro_angle + (1 - ALPHA) * accel_angle
Each loop: advance the angle with the gyro (smooth, responsive), then pull it a little toward the accelerometer's reading (stable long-term). With ALPHA = 0.96, 96% of the estimate comes from the gyro's integration and 4% from the accelerometer's correction.
Tuning ALPHA: it sets the crossover frequency — how quickly the filter trusts new accelerometer information. The time constant is tau = ALPHA * dt / (1 - ALPHA). With dt = 0.01 s (100 Hz loop) and ALPHA = 0.96, tau ≈ 0.24 s: disturbances faster than ~0.24 s are handled by the gyro, slower drift gets corrected by the accelerometer. Raise ALPHA (0.97–0.98) if vibration is heavy and you want the gyro trusted longer; lower it (0.90–0.94) if the gyro bias is large and you want faster correction. There's no magic value — but anything in 0.94–0.98 works for most balancing robots, which tells you the filter is forgiving.
A worked sanity check. Gyro bias 1.5°/s, uncorrected: after 60 s the angle is off by 90°. With the complementary filter at tau ≈ 0.5 s, the steady-state error from bias is roughly bias * tau ≈ 0.75° — the drift is bounded, not eliminated, and bounded at under a degree. That is the difference between a robot that falls over in a minute and one that balances indefinitely.
In C++ on Arduino/ESP32 the same filter is identical in structure — read the sensor, compute dt from micros(), apply the same blend. Keep the loop timing as steady as you can (a timer interrupt or a fixed-rate task beats a free-running loop with jittery dt).
The Kalman filter: when optimal is worth the complexity
The complementary filter uses one fixed ALPHA — one fixed opinion about how much to trust each sensor. The Kalman filter adjusts that trust continuously based on how noisy each sensor currently is. It maintains not just an angle estimate but an uncertainty (covariance) for it, and blends the prediction and measurement in proportion to their uncertainties.
For a 1D tilt filter, the two steps per loop are:
Predict (using the gyro):
angle_pred = angle + (gyro_rate - bias) * dt
P_pred = P + Q * dt # uncertainty grows: integration is guessing
Update (using the accelerometer):
K = P_pred / (P_pred + R) # Kalman gain: trust measurement this much
angle = angle_pred + K * (accel_angle - angle_pred)
bias = bias + K * (accel_angle - angle_pred) * dt * bias_learning_rate
P = (1 - K) * P_pred # uncertainty shrinks: measurement helped
Two tuning knobs, and they have physical meaning:
- Q (process noise): how much you distrust the gyro model — set it from the gyro's measured noise/bias instability. Larger Q means "the gyro drifts fast, lean on the accelerometer more."
- R (measurement noise): how much you distrust the accelerometer — set it from the observed variance of the tilt reading at rest plus your vibration environment. Larger R means "the accelerometer is noisy, lean on the gyro more."
Notice the Kalman gain K plays exactly the role of (1 − ALPHA) in the complementary filter — but it adapts. When the accelerometer suddenly gets noisy (motors spin up), a well-tuned Kalman filter automatically trusts it less. The complementary filter can't do that; its ALPHA is frozen.
When is it worth it? Honestly assess: for a self-balancing robot, a line-follower with tilt sensing, or a gesture controller, the complementary filter is enough and far easier to debug. Reach for the Kalman filter when the noise environment changes during operation (a drone that hovers quietly then punches the throttle), when you need the uncertainty estimate itself (probabilistic localisation), or when you're fusing more than two sources with different rates and delays. For 9-axis absolute orientation (pitch, roll and yaw), look at the Madgwick or Mahony filters — quaternion-based, computationally lighter than a full Kalman, and the standard choice for DIY AHRS projects.
One warning: a Kalman filter with badly chosen Q and R performs worse than a complementary filter with a sensible ALPHA. "Optimal" means optimal for the noise model you gave it — garbage model, garbage fusion. Measure your sensors' actual noise before tuning, don't copy Q/R values from a blog.
The unglamorous half: calibration, timing, mounting
Filters get the glory, but in student projects the failure is usually here:
Calibrate the gyro bias at startup. Keep the robot perfectly still for 2 seconds at power-on, average a few hundred gyro readings, subtract that average from every later reading. This single step removes most of the drift a complementary filter has to fight. Re-do it if the temperature changes a lot — MEMS bias drifts with temperature.
Calibrate the accelerometer roughly. At rest it should read 1g total magnitude (sqrt(ax^2 + ay^2 + az^2) ≈ 1g). If it reads 1.1g, your scale is off 10% and every tilt angle is slightly wrong. The sensor calibration guide covers the full procedure.
Keep dt honest. The filter math assumes you know the time between samples. time.monotonic() (Python) or micros() (Arduino) per loop is fine; assuming a constant dt while your loop jitters is not. At 100 Hz, even 2 ms of jitter is 20% timing error on the integration.
Mount the IMU rigidly, near the centre of rotation. A sensor on a long flexible stalk measures the stalk's wobble, not the robot's tilt. Mount it solidly, close to the pivot axis, with the axes aligned to the robot's frame — a sensor rotated 90° from what the code assumes produces beautifully filtered wrong angles.
Isolate vibration, don't amplify it. Foam tape between the IMU board and the chassis helps; a loose sensor rattling in a 3D-printed bracket does not. And remember: no filter removes vibration that aliases into your signal band — a mechanical fix (tighter mounting, balanced props) beats any amount of ALPHA tuning.
Common mistakes
- Integrating acceleration to get position. Angle from gyro works because drift is bounded by the accelerometer. Position from double-integrating acceleration has no absolute reference — errors grow quadratically, and a hobby IMU's position is garbage within seconds. Don't build "indoor GPS from an IMU" for your project; fuse with wheel encoders, optical flow, or beacons instead.
- Expecting yaw from accel + gyro. Gravity gives you pitch and roll references, but rotating around the vertical axis changes nothing in either sensor. Yaw drifts forever without a magnetometer (or another heading source). If your rover "drives straight" in a curve, this is why.
- Trusting the MPU6050's onboard DMP blindly. The digital motion processor does its own fusion — convenient, but its firmware is undocumented, its outputs are hard to debug, and it hides exactly the understanding examiners ask about. Implement the filter yourself for a project; use the DMP only if you've validated its output.
- Copying filter constants from the internet. ALPHA, Q, and R depend on your sensor, your loop rate, and your vibration environment. The values here are starting points — verify with a simple test: hold the device still (angle should sit rock-steady) and rotate it briskly (angle should track without lag or overshoot).
- Filtering at 10 Hz and wondering why it's laggy. Fusion needs a fast loop — 100 Hz minimum for balancing, faster for drones. A slow loop adds phase lag that no filter tuning removes, and your PID controller pays for it.
- Forgetting the accelerometer measures proper acceleration. Calibrating "tilt" while the device accelerates (in a moving vehicle, on a shaky table) bakes motion into your reference.
FAQ
Complementary or Kalman — which should I actually implement?
Start with the complementary filter. It's five lines, one knob, and audibly debuggable. Move to Kalman only if you can point to a specific failure of the complementary filter (changing noise conditions, need for uncertainty estimates) — not because it sounds more impressive.
What loop rate do I need?
100 Hz is the practical minimum for anything that balances or flies; 200–500 Hz is better for multirotors. Below ~50 Hz, phase lag from the sample rate dominates and filter choice barely matters.
Can I fuse at different rates — gyro at 500 Hz, accelerometer at 100 Hz?
Yes, and the Kalman formulation handles it naturally (predict on every gyro sample, update when the accelerometer sample arrives). With a complementary filter, run the gyro integration every sample and apply the accelerometer correction whenever a fresh reading exists.
Do I need the magnetometer?
Only for absolute yaw/heading. Pitch and roll are fully solved by accel + gyro. Adding an uncalibrated magnetometer near motors makes heading worse, not better — calibrate it properly or leave it out.
Why does my angle look perfect on the desk but terrible on the robot?
Vibration. The desk has none; the robot is a vibration generator. The accelerometer correction gets noisy, and if your ALPHA trusts it too much (too low), the noise feeds straight into the angle. Raise ALPHA, fix the mounting, and re-test.
Should I low-pass the accelerometer before the filter?
A light low-pass (or just averaging a few samples) on the accelerometer helps in high-vibration builds, since the filter only needs its slow, long-term information. Don't overdo it — heavy pre-filtering adds lag to the very correction signal you need.
Limitations
- Fusion can't fix a bad mechanical setup: flexing mounts, misaligned axes, and severe vibration defeat any filter.
- MEMS gyros drift with temperature; a startup calibration goes stale as the chip warms up. High-precision applications need temperature compensation or better sensors.
- Without an absolute reference, yaw always drifts (gyro-only) — plan a heading source if your project needs it.
- Double-integrated position from consumer IMUs diverges in seconds; this guide's techniques solve orientation, not position.
- Magnetic distortion near motors and steel structures corrupts magnetometer-based yaw; calibration helps but doesn't make it immune.
- Very cheap IMU clones have inconsistent bias and noise between units — validate each board, don't assume the datasheet.
Where to go from here
- MPU6050 Component Guide — wiring and raw readings from the IMU this guide fuses.
- Sensor Calibration for IoT Projects — the bias and scale calibration this filter depends on.
- PID Control Tuning for Student Projects — the controller that consumes your fused angle.
- Quadrature Rotary Encoders with ESP32 — odometry to pair with orientation for full pose.
- More embedded builds in the IoT & Embedded branch hub.