Introduction

Every drone, phone and self-balancing robot needs to answer one humble question many times per second: which way is down? Inside them sit two small sensors that each try to answer it, and each one fails in its own particular way.

A gyroscope measures how fast the device is rotating. Integrate that rate over time and you get an angle — updated instantly, buttery smooth, and reacting the moment you move. The catch: any tiny measurement error accumulates forever. Leave a gyro-only estimate running for a minute and it will confidently report an angle that has drifted far from reality.

An accelerometer measures the pull of gravity, so it can compute an absolute tilt angle that never drifts — the same reading in one second or one hour. The catch: it also measures every bump, vibration and sudden move, so on a fast-moving device its output is a jittery mess.

One sensor is smooth but wanders off; the other is noisy but honest in the long run. That complementary pair of flaws is exactly what gives this filter its name.

Try It

Below is a simulated sensor swinging back and forth. The accelerometer (orange) reads the true angle but shakes with noise; the gyroscope (blue) is silky smooth but slowly drifts away from the truth. Drag the weight slider to blend them and watch the black needle settle onto the real angle.

<p class="hint">{{hint_para}}</p>
<div class="gauge-wrap">
  <svg id="gauge" class="gauge" viewBox="0 0 200 120" aria-hidden="true">
    <path d="M10,110 A90,90 0 0 1 190,110" class="gauge-arc"/>
    <line id="needleAcc" class="needle needle-acc" x1="100" y1="110" x2="100" y2="30"/>
    <line id="needleGyro" class="needle needle-gyro" x1="100" y1="110" x2="100" y2="30"/>
    <line id="needleFused" class="needle needle-fused" x1="100" y1="110" x2="100" y2="30"/>
    <circle cx="100" cy="110" r="4" fill="#222"/>
  </svg>
  <div class="legend">
    <span class="tag acc">{{legend_acc}}</span>
    <span class="tag gyro">{{legend_gyro}}</span>
    <span class="tag fused">{{legend_fused}}</span>
  </div>
</div>
<label class="slider-label" for="alpha">{{alpha_label}}</label>
<input type="range" id="alpha" min="0" max="0.99" step="0.01" value="0.98">
<div class="status" id="status">{{status_init}}</div>
<div class="btns">
  <button id="toggle" type="button">{{btn_pause}}</button>
  <button id="reset" type="button" class="ghost">{{btn_reset}}</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.gauge-wrap { display: flex; flex-direction: column; align-items: center; gap: .4rem; }
.gauge { width: 220px; max-width: 80%; }
.gauge-arc { fill: none; stroke: #cdd9e3; stroke-width: 4; }
.needle { stroke-width: 3; stroke-linecap: round; transform-origin: 100px 110px; }
.needle-acc { stroke: #e07b1d; opacity: .85; }
.needle-gyro { stroke: #3572e0; opacity: .85; }
.needle-fused { stroke: #1a1a1a; stroke-width: 3.6; }
.legend { display: flex; gap: .6rem; flex-wrap: wrap; justify-content: center; }
.tag { font-size: .75rem; font-weight: 600; padding: .15rem .5rem; border-radius: 6px; }
.tag.acc { background: #fbe6d2; color: #a5540a; }
.tag.gyro { background: #dbe6fb; color: #1d47a5; }
.tag.fused { background: #e4e4e4; color: #1a1a1a; }
.slider-label { display: block; font-size: .85rem; font-weight: 600; margin: .8rem 0 .2rem; }
input[type="range"] { width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; color: #1a1a1a; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
// Code not found

The slider controls α\alpha, the fraction of fast trust given to the gyroscope versus the slow trust given to the accelerometer. Push it all the way to one side and you get pure drift or pure jitter back. Somewhere in the middle — commonly around α=0.98\alpha = 0.98 in real IMUs — the needle tracks fast motion instantly and never wanders off.

The Real Complexity

The trick sounds almost too simple to work, and yet it does. At every timestep the filter combines two angle estimates:

θt=α(θt1+ωtΔt)+(1α)θacc\theta_t = \alpha\,(\theta_{t-1} + \omega_t \,\Delta t) + (1-\alpha)\,\theta_{\text{acc}}

Here θt1+ωtΔt\theta_{t-1} + \omega_t \Delta t is the gyro's guess — last angle plus the measured rotation rate ωt\omega_t times the elapsed time Δt\Delta t — and θacc\theta_{\text{acc}} is the angle read straight off gravity's direction from the accelerometer. The weight α\alpha, typically close to 0.980.98, decides how much to trust each source.

Why does this fix both problems at once? Because of what each term does in the frequency domain:

  • The gyro term behaves like a high-pass filter: it tracks fast changes (real motion) faithfully, but any slow bias in the rate measurement integrates into a slow drift — exactly the low-frequency error the accelerometer term will correct.
  • The accelerometer term behaves like a low-pass filter: it is only trusted a little at a time (1α1-\alpha, e.g. 0.020.02), so fast noise gets averaged away, but its slow, correct average survives and continuously drags the estimate back to the truth.

Add a high-pass version of one signal to a low-pass version of the other and — if both filters share the same cutoff — their frequency responses sum to exactly 11 at every frequency. Nothing is lost, nothing is double-counted; the two are true complements of each other, which is where the filter gets its name.

This is not a coincidence dressed up as elegance — it is a lightweight, hand-tuned special case of the far more general Kalman filter, which does the same job optimally by explicitly modeling the statistics of each sensor's error instead of picking one fixed α\alpha.

Where It Matters

Sensor fusion sounds like a job for a supercomputer, but this particular fix runs happily on the cheapest microcontrollers:

  • Drone and quadcopter flight controllers: stabilizing an aircraft needs a tilt estimate hundreds of times per second; the complementary filter's near-zero cost leaves the processor free for everything else.
  • Smartphone orientation: the screen-rotation and step-counting logic in many phones leans on exactly this kind of gyro-plus-accelerometer blend.
  • Self-balancing robots and segways: keeping an inverted pendulum upright depends on knowing its tilt angle instantly and without drift, timestep after timestep.
  • Low-power wearables: anywhere a full Kalman filter is too expensive to run, the complementary filter gives most of the benefit for a tiny fraction of the computation.

Learn this one filter and you've learned the core idea behind almost every inertial measurement unit (IMU) on the market: never trust one flawed sensor when its flaws cancel a nearby sensor's flaws.

Conclusion

Neither sensor alone can tell a drone which way is down for more than a few seconds. The gyroscope is fast but forgetful, drifting into fiction; the accelerometer is honest but jumpy, drowning the truth in noise. The complementary filter does not fix either flaw — it simply notices that they live at opposite ends of the frequency spectrum and lets each sensor answer only for the timescale it is good at.

That single weighted average, tuned once and left alone, is why your phone knows which way is up and why a drone doesn't flip over in a gust of wind. It's a small, elegant reminder that combining two imperfect sources of information, wisely, can beat either one — the same intuition that scales all the way up to full Bayesian inference.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/complementary-filter/Content licensed under CC BY-NC 4.0.