Visual Navigation for Autonomous Vehicles · MIT 16.485 · Lectures 6–7

Control: PID, LQR & Geometric Control

A quadrotor must hover at a setpoint despite wind gusts and its own unstable dynamics. The state estimator tells you where the drone IS. The planner says where it should GO. But nothing moves until you compute the rotor commands that bridge the gap — without oscillating, drifting, or crashing. This lesson derives the full control stack from state-space fundamentals through PID and LQR, arriving at the geometric controller that respects SO(3) geometry. Worked numbers at every step. MIT 16.485 by Luca Carlone, Lectures 6–7.

Prerequisites: VNAV L1 (rotation matrices, SO(3)). VNAV L2 (Lie groups, exp map). Basic calculus and linear algebra. No control background needed.
10
Chapters
5
Live Canvases
Derived
From First Principles

Chapter 0: The Hovering Problem

Imagine your quadrotor is hovering 2 meters above the ground. A gust of wind pushes it 0.5 m to the left. The state estimator — doing its Kalman filter job — reports: "position error: −0.5 m in x." Now what? Someone has to translate that error number into four rotor speeds. Get it wrong and the drone either drifts away indefinitely, oscillates like a pendulum until it crashes, or snaps so aggressively it flips.

This is the control problem. It has nothing to do with estimation (knowing where you are) or planning (knowing where to go). Control is the bridge: given an error signal, compute actuator commands that drive the error to zero, quickly and smoothly.

The three questions every controller must answer. (1) How hard do I push back? (2) How do I handle errors that persist for seconds? (3) How do I avoid overshoot and oscillation? The three terms of a PID controller answer exactly these three questions — P, I, and D respectively.

Why naive solutions fail

The simplest idea: set rotor thrust proportional to position error. If you're 0.5 m too low, add thrust. This is the P (proportional) term — and it works, sort of. But a constant P gain causes the system to oscillate around the setpoint: thrust too much, overshoot, reverse, undershoot, repeat. You need damping.

Add damping via the velocity: reduce thrust as you approach the target. Now it settles, but there's a new problem: a small constant disturbance (like a steady breeze) creates a permanent position offset. The P term only pushes back proportional to error; a small steady error produces a small steady counterforce that exactly balances the disturbance — but never eliminates it. You need memory.

The integral term accumulates past errors. Over time, the integral grows until the integral action is strong enough to overcome the steady disturbance. Now you have P (stiffness) + I (memory) + D (damping) = a PID controller. Each term exists to solve a specific failure mode of the others.

Open-loop vs. closed-loop: watch the drift

A disturbance (wind gust) hits at t=2 s. Open-loop has no feedback — the drone drifts away. Closed-loop reads the error and corrects. Toggle between modes and vary the disturbance magnitude.

Disturbance 1.0
A quadrotor hovers with a constant proportional (P) controller. A steady 1 N wind force pushes it left. What happens in the long run?

Chapter 1: State-Space & Stability

Before designing controllers, we need a precise language for describing how systems evolve. The language is state-space form. Rather than writing out Newton's equations from scratch each time, we pack everything into two compact equations:

ẋ = f(x, u)     (dynamics)
y = g(x, u)     (output)

Here x ∈ ℝn is the state vector — everything you need to predict the future. u ∈ ℝm is the control input — the actuator commands. y is the output you observe. The dot notation ẋ means dx/dt — the time derivative of the state.

Quadrotor state vector

For a quadrotor, the state is: position p ∈ ℝ³, velocity v ∈ ℝ³, rotation R ∈ SO(3), angular velocity ω ∈ ℝ³. We can list this as x = [p, v, R, ω]. The full state has 12 real-valued degrees of freedom (3+3+3+3, since SO(3) is a 3-DOF manifold even though R is a 3×3 matrix). The control inputs are the four rotor speeds w = [w₁, w₂, w₃, w₄].

Linear state-space: ẋ = Ax + Bu

When f is linear in x and u, the dynamics simplify to:

ẋ = Ax + Bu

where A ∈ ℝn×n is the system matrix (captures how the state drives its own derivative) and B ∈ ℝn×m is the input matrix (captures how control inputs affect the derivative). This linear form is not always exact, but near an equilibrium point, any nonlinear system can be linearized to this form via a Taylor expansion.

Stability: eigenvalues decide everything

The long-term behavior of ẋ = Ax (with u=0) is entirely determined by the eigenvalues of A. Let λ be an eigenvalue. If Re(λ) < 0 for all eigenvalues, perturbations decay exponentially — the system is asymptotically stable. If any Re(λ) > 0, perturbations grow exponentially — the system is unstable.

Eigenvalue locationRe(λ) signBehaviorExample
Left half-plane< 0Decays to zeroPendulum with friction
Imaginary axis= 0Oscillates foreverUndamped spring
Right half-plane> 0Grows without boundInverted pendulum
Complex pair left< 0Damped oscillationPendulum with light friction

A hovering quadrotor without control is unstable — its linearized A matrix has eigenvalues in the right half-plane. Every position perturbation grows. Control must place all closed-loop eigenvalues in the left half-plane.

Worked numbers: a simple 1D system

Consider a mass on a spring: ẍ = −kx/m − bẋ/m + u/m. In state-space form with x = [position, velocity]:

ẋ = [ẋ₁, ẋ₂]ᵀ = [[0, 1],[ −k/m, −b/m]] · [x₁, x₂]ᵀ + [[0],[1/m]] · u

With m=1 kg, k=4 N/m, b=0: A = [[0,1],[−4,0]]. Eigenvalues: λ = ±2j (purely imaginary — undamped oscillation). Add b=4: eigenvalues λ = −2±0j (both real negative — overdamped, stable, no oscillation). Drag b is the key to stability here.

Pole-plane stability visualizer — drag the poles

The complex plane shows two conjugate poles. Drag them left (stable) or right (unstable). The right panel shows the resulting time response. Poles in the left half = decay. Right half = explosion. On imaginary axis = oscillation.

A 2D system ẋ = Ax has A = [[−1, 2],[0, −3]]. The eigenvalues are −1 and −3 (both negative real). What is the system's long-term behavior from any initial condition?

Chapter 2: Open vs Closed Loop

There are two fundamentally different ways to control a system. In open-loop control, you compute your commands from the desired trajectory alone, ignoring what the system actually does. In closed-loop (feedback) control, you continuously measure the actual state, compare it to the desired state, and compute commands from the error.

Reference r(t)
Desired setpoint or trajectory
Controller C
Computes command u from error e = r − y
Plant P
The physical system (quadrotor, motor, arm)
↻ measured output y fed back to compute e

Why open-loop fails in robotics

Open-loop control works beautifully if your model is perfect and there are no disturbances. In a real robot, neither is true. Motors have friction that varies with temperature. Wind applies unknown forces. Sensor calibration drifts. An open-loop controller that was tuned on a calm day will fail on a windy day — it never "knows" it's drifting.

Feedback is error-driven actuation. The key insight: a feedback controller doesn't need a perfect model. It just needs to know the error. Even with a rough model, feedback will push the system toward zero error. This robustness to model mismatch is why virtually all real-world controllers use feedback.

The feedback loop equation

For a linear system, the closed-loop dynamics look like this. Without control: ẋ = Ax. With a linear feedback law u = −Kx (we'll derive K later):

ẋ = Ax + B(−Kx) = (A − BK)x = Aclx

The closed-loop system matrix is Acl = A − BK. The controller K reshapes the eigenvalues of A into whatever we want. An unstable A (right-half eigenvalues) can become a stable Acl with the right K. This is the fundamental promise of feedback control: you can move the poles to stable locations by choosing K.

A concrete 1D example

Scalar system: ẋ = ax + bu. Without control (u=0), if a > 0, x(t) = x₀eat → ∞. Now apply u = −kx. Then ẋ = ax − bkx = (a−bk)x. If we choose k > a/b, the exponent (a−bk) becomes negative and x decays to zero. Worked numbers: a=2, b=1. For stability we need k > 2. Take k=5: closed-loop pole = 2−5 = −3. Time constant τ = 1/3 ≈ 0.33 s. Starting at x₀=1, after 1 s: x = e−3 ≈ 0.05. The feedback gain k=5 stabilized an unstable system (a=+2) in under a second.

A linear system has A = 3 (scalar, unstable). You apply feedback u = −kx with b = 1. What is the minimum k needed to make the closed-loop system stable?

Chapter 3: PID: Derive Each Term

PID stands for Proportional-Integral-Derivative. Each term responds to a different aspect of the error signal e(t) = r(t) − y(t), where r is the reference (setpoint) and y is the measured output. The control law is:

u(t) = KP·e(t) + KI·∫e(τ)dτ + KD·(de/dt)

Let's derive why each term is necessary, and what happens if you remove any one of them.

P term: stiffness (push proportional to displacement)

The proportional term uP = KP·e is the simplest feedback law. Think of it as a spring: the further you are from the setpoint, the harder you push back. It gives the system stiffness. Increasing KP makes the response faster but also more prone to overshoot and oscillation. For the drone 0.5 m below setpoint, KP=4 gives uP = 4×0.5 = 2 N of upward thrust correction.

D term: damping (resist the velocity of change)

The derivative term uD = KD·ė = KD·(de/dt) acts like a damper. It opposes the rate of change of the error. When the drone is falling toward the setpoint fast (error decreasing rapidly), ė < 0, so uD < 0 — it brakes the approach. This prevents overshoot. Without D, a highly-tuned P gain causes oscillation; the D term is the "anti-oscillation" term. If the drone is 0.5 m below and approaching at 0.3 m/s: ė = −0.3 m/s (error decreasing). KD=2 gives uD = 2×(−0.3) = −0.6 N (braking).

I term: memory (eliminate steady-state error)

The integral term uI = KI·∫e(τ)dτ accumulates all past errors. If there is a constant disturbance (say, a steady 1 N wind), the P term alone settles at a steady-state error ess = disturbance/KP. No matter how long you wait, this offset persists. The integral builds up over time until KI·∫e·dt = 1 N, fully canceling the disturbance. The I term gives the controller infinite DC gain — it will keep pushing until the error is exactly zero.

Worked numbers: step response at t=0

Initial conditions: error e₀=1 m, ėe₀=0, ∫e₀=0. Gains: KP=4, KD=2, KI=1.

uP(0) = 4 × 1.0 = 4.0 N
uD(0) = 2 × 0.0 = 0.0 N   (ė=0 at start)
uI(0) = 1 × 0.0 = 0.0 N   (no history yet)
u(0) = 4.0 + 0.0 + 0.0 = 4.0 N

At t=0.5 s (suppose error has dropped to 0.4 m, ė = −1.2 m/s, ∫e=0.3 m·s):

uP(0.5) = 4 × 0.4 = 1.6 N
uD(0.5) = 2 × (−1.2) = −2.4 N   (braking!)
uI(0.5) = 1 × 0.3 = 0.3 N
u(0.5) = 1.6 − 2.4 + 0.3 = −0.5 N

The D term has gone negative (braking) because the system is approaching the setpoint fast. Without D, u(0.5) = 1.9 N, still pushing forward — causing overshoot.

A PID controller has KP=5, KI=0, KD=0 (P-only). The system reaches a steady state with error ess=0.2 m against a constant wind disturbance. What happens if you add KI=1?

Chapter 4: PID Tuning & Anti-Windup

Having a PID controller and having a working PID controller are two different things. The gains KP, KI, KD must be tuned. Too low: sluggish. Too high: oscillation or instability. The interaction between the three terms makes tuning non-trivial.

Tuning intuition: the Ziegler-Nichols mental model

Start with KI=0, KD=0. Increase KP until the system oscillates continuously — this is the ultimate gain Ku at the ultimate period Tu. Ziegler-Nichols suggests KP=0.6Ku, KI=1.2Ku/Tu, KD=0.075KuTu. This is a starting point, not a final answer — always retune on the real hardware.

More P gain is not always better. Increasing KP makes the system faster but also more oscillatory. The D term damps oscillation, but too much D amplifies sensor noise. There is always a tradeoff: high bandwidth (fast response) vs. noise sensitivity. Real tuning is balancing these tradeoffs for the specific task.

Integrator windup

The I term has a dangerous failure mode called integrator windup. Suppose the drone is trying to lift off from the ground but is held down (saturated actuator). The error is large and constant: e = +2 m. The integral keeps accumulating: ∫e grows to 10, 100, 1000 m·s. When the constraint is released, the massive integral term causes the drone to shoot up far beyond the setpoint — severe overshoot or even a crash.

The fix is anti-windup: when the actuator saturates (command clips to max or min), stop integrating. Concretely: don't accumulate the integral when u is at its limit.

python
import numpy as np

class PIDController:
    def __init__(self, Kp, Ki, Kd, u_min, u_max, dt):
        self.Kp, self.Ki, self.Kd = Kp, Ki, Kd
        self.u_min, self.u_max = u_min, u_max
        self.dt = dt
        self.integral = 0.0
        self.prev_e   = 0.0

    def step(self, e):
        # Proportional
        u_p = self.Kp * e
        # Derivative (backward difference)
        u_d = self.Kd * (e - self.prev_e) / self.dt
        # Unclamped output (without I)
        u_raw = u_p + self.Kd * (e - self.prev_e) / self.dt

        # Anti-windup: only integrate when NOT saturated
        u_test = u_p + self.Ki * self.integral * self.dt + u_d
        if self.u_min < u_test < self.u_max:
            self.integral += e * self.dt   # accumulate

        u_i = self.Ki * self.integral
        u = np.clip(u_p + u_i + u_d, self.u_min, self.u_max)
        self.prev_e = e
        return u
PID step-response tuning playground

Unit step: reference jumps from 0 to 1 at t=0. Tune KP, KI, KD and watch the step response. Observe overshoot, settling time, and steady-state error in real time. The system is a double integrator (mass with friction — like a quadrotor altitude axis).

KP 6.0
KI 1.0
KD 2.0
Integrator windup is most dangerous when:

Chapter 5: LQR: Optimal Control

PID works well when you can tune three numbers by hand. But a quadrotor has 12 state dimensions and 4 inputs. Tuning 12-dimensional feedback gains manually is impractical. Linear Quadratic Regulator (LQR) automates the gain design by solving an optimization problem: find the feedback law u = −Kx that minimizes a weighted cost over all future time.

The cost function

LQR minimizes the infinite-horizon quadratic cost:

J = ∫0 (xTQx + uTRu) dt

where Q ∈ ℝn×n (positive semidefinite) penalizes state error and R ∈ ℝm×m (positive definite) penalizes control effort. The term xTQx says "I care about states xi being near zero, with weight Qii." The term uTRu says "I care about keeping inputs small — don't waste actuator authority."

Q/R tradeoff intuition

The ratio Q/R is the fundamental design knob. Large Q/R: the optimizer is told "state errors are very expensive, control effort is cheap" — it pushes the state hard toward zero, using lots of control. Small Q/R: "control effort is expensive, state error is acceptable" — gentle corrections, slower convergence, smaller actuator commands. This is directly analogous to the PID gain: high KP ↔ large Q, low KD ↔ small R.

Q penalizes where you are; R penalizes how hard you push. Diagonal Q means each state is penalized independently. Q₁₁=100 means "keep x₁ near zero very tightly." R=0.01 means "control effort is cheap — use the actuators freely." The solution automatically balances these competing objectives.

The optimal feedback law

The remarkable result of LQR: the optimal control is always a linear state feedback law:

u* = −Kx    where    K = R−1BTP

Here P ∈ ℝn×n is the unique positive definite solution to the Algebraic Riccati Equation (ARE):

ATP + PA − PBR−1BTP + Q = 0

You do not need to solve the ARE by hand. In practice, scipy.linalg.solve_continuous_are(A, B, Q, R) computes P in milliseconds. The resulting K is the globally optimal linear feedback gain for the given Q and R.

Worked numbers: LQR cost evaluation for two K choices

Scalar system: A=0, B=1, Q=1, R=1. The ARE simplifies to: −P² + 1 = 0, so P=1, K=P=1. Optimal gain K=1. Compare two controllers from x₀=1:

ControllerKClosed-loop polex(1 s)Integral cost J (1 s)
Under-gain0.5−0.5e−0.5=0.61∫(x²+0.25x²)dt=1.25∫e−tdt≈1.25
Optimal LQR1.0−1.0e−1=0.37∫(e−2t+e−2t)dt=1.0 ✓
Over-gain3.0−3.0e−3=0.05∫(e−6t+9e−6t)dt≈1.67

The optimal K=1 achieves the minimum cost J=1.0. The over-gain (K=3) drives the state to zero faster but pays a high control cost (9u²), giving a higher total J. The under-gain settles slowly, paying a high state cost. LQR finds the sweet spot automatically.

python
import numpy as np
from scipy.linalg import solve_continuous_are

# System: quadrotor altitude axis (double integrator + drag)
# State x = [z, z_dot], input u = thrust deviation from hover
m = 1.0   # kg
b = 0.1   # drag coefficient
A = np.array([[0, 1],
              [0, -b/m]])
B = np.array([[0],
              [1/m]])

# LQR weights: penalize altitude error 10x more than thrust effort
Q = np.diag([10.0, 1.0])   # [z, z_dot] weights
R = np.array([[1.0]])         # thrust cost

# Solve Riccati equation for P
P = solve_continuous_are(A, B, Q, R)
# Optimal gain: K = R^{-1} B^T P
K = np.linalg.inv(R) @ B.T @ P

# K = [Kz, Kzdot] — feedback on altitude error and velocity
# Control law: u = -K @ (x - x_desired)
print(f"K = {K.flatten()}")  # e.g., K = [3.16, 4.63]

# LQR cost evaluator: simulate and compute J
def lqr_cost(K, A, B, Q, R, x0, T=5.0, dt=0.01):
    x = x0.copy(); J = 0.0
    for _ in np.arange(0, T, dt):
        u = -K @ x
        J += (x @ Q @ x + u @ R @ u) * dt
        x = x + (A @ x + B.flatten() * u[0]) * dt
    return J
LQR Q/R tradeoff — trajectory and cost live

Slider sets log(Q/R). Left = gentle control (low Q/R). Right = aggressive control (high Q/R). Watch the trajectory change and the cost breakdown: state cost vs control cost.

log(Q/R) 0.5
In LQR, if you double Q (the state-error penalty) while keeping R fixed, what happens to the optimal gain K and the resulting closed-loop behavior?

Chapter 6: Quadrotor Dynamics

Everything so far has been generic control theory. Now we apply it to the quadrotor — the canonical VNAV platform. The quadrotor's dynamics are richer and stranger than a simple mass-spring because it is underactuated: four scalar inputs (rotor speeds) must control six degrees of freedom.

The Newton-Euler equations

The quadrotor obeys Newton's second law in both translation and rotation. In compact form:

w = −mge3 + RwB fB
Jω̇B = −ωB × JωB + τB

The translational equation says: mass × acceleration = gravity (downward) + thrust force rotated to world frame. The rotational equation says: inertia × angular acceleration = Euler term (gyroscopic coupling) + applied torques. Here RwB is the rotation from body to world frame, fB is force in body frame (only the z component is nonzero for a standard quadrotor), τB is torque in body frame, and J is the 3×3 inertia matrix.

Rotor physics: thrust and torque from spin speed

Each rotor i spinning at angular velocity wi produces:

Thrust: Ti = cf wi|wi|   (thrust coefficient cf)
Drag torque: τdrag,i = (−1)i+1 cd wi|wi|   (alternating sign: CW/CCW rotors)

The total thrust is the sum of all four: fBz = cf(w₁|w₁| + w₂|w₂| + w₃|w₃| + w₄|w₄|). The torques come from both the drag and from the geometry: off-center thrust at arm position ρBi creates a moment ρBi × Tie₃.

The mixing matrix: rotor speeds to thrust/torque

We can pack the input mapping into a 4×4 matrix F̄ (the "mixing matrix") that maps the signed-square rotor speeds w = [w₁|w₁|, w₂|w₂|, w₃|w₃|, w₄|w₄|]T to [fBz, τBx, τBy, τBz]:

[fBz, τBx, τBy, τBz]T = F̄ · w

For a + configuration quad with arm length L and rotor positions at [±L,0] and [0,±L]:

F̄ = [[cf, cf, cf, cf],
     [0, −cfL, 0, cfL],
     [cfL, 0, −cfL, 0],
     [−cd, cd, −cd, cd]]

Because F̄ is invertible, you can always go from desired [fBz, τB] to required rotor speeds via w = F̄−1[fBz, τB]. This inverse mapping is called the thrust-torque mixer or control allocation.

Worked thrust-torque mixing example

Parameters: m=1 kg, g=9.81 m/s², cf=1e−5 N/(rad/s)², cd=1e−6 Nm/(rad/s)², L=0.25 m. At hover: total thrust = mg = 9.81 N, all torques = 0. Each rotor: Ti = 9.81/4 = 2.4525 N. Rotor speed: wi = √(Ti/cf) = √(2.4525/1e−5) ≈ 495 rad/s ≈ 4730 rpm. Now apply a roll torque τx=0.5 Nm. From F̄ row 2: −cfL(w₂−w₄) = 0.5 → Δw²=200 → w₂ decreases, w₄ increases by √200 ≈ 14 rad/s each.

python
import numpy as np

# Quadrotor thrust-torque mixer
def mixer(cf=1e-5, cd=1e-6, L=0.25):
    """Returns 4x4 mixing matrix F_bar and its inverse."""
    F = np.array([
        [cf,      cf,      cf,      cf     ],
        [0,      -cf*L,   0,       cf*L  ],
        [cf*L,    0,      -cf*L,   0     ],
        [-cd,     cd,     -cd,      cd     ]
    ])
    return F, np.linalg.inv(F)

F, Finv = mixer()

# Hover: total thrust = mg = 9.81 N, zero torques
m, g = 1.0, 9.81
desired = np.array([m*g, 0.0, 0.0, 0.0])  # [fz, tx, ty, tz]
w_sq = Finv @ desired   # signed-square rotor speeds
w_rpm = np.sqrt(np.abs(w_sq)) / (2*np.pi/60)
print(f"Hover RPM: {w_rpm}")  # ~4730 rpm each

# Roll maneuver: add 0.5 Nm roll torque
desired_roll = np.array([m*g, 0.5, 0.0, 0.0])
w_sq_roll = Finv @ desired_roll
print(f"Roll w_sq: {w_sq_roll}")   # rotor 2 decreases, rotor 4 increases
The quadrotor is underactuated. It has 6 DOF (3 position + 3 orientation) but only 4 independent inputs. This means it CANNOT independently control all 6 DOF simultaneously. Specifically: it cannot move sideways without tilting. To go left, it must roll left (tilt), redirecting the total thrust vector. Position and attitude are coupled by physics. This is why position control must compute a desired attitude and then attitude control tracks that attitude — the two cannot be decoupled.
A quadrotor wants to accelerate to the left (+y direction in the world frame). Assuming it can only thrust along its body z-axis, which physical maneuver is required?

Chapter 7: Cascaded Control Loops & Geometric Control

A quadrotor's full controller isn't one monolithic block — it's a hierarchy of nested loops. The outer loop handles position; the inner loop handles attitude. This cascaded control structure exploits the timescale separation: attitude dynamics (rotating the drone) are much faster than position dynamics (moving the drone through space).

Position Reference pd, ψd
Desired 3D position + yaw angle
Position Controller
Computes desired thrust magnitude fz and desired body z-axis zdw
Attitude Reference Rd ∈ SO(3)
Desired rotation matrix from position loop output
Attitude Controller
Computes torques τ to track Rd, operating on SO(3)
Mixer F̄−1
Converts [fz, τ] to individual rotor speeds

The position loop: from error to desired attitude

The position controller receives the position error ep = pw − pwd and velocity error ev = vw − vwd. It computes an "ideal force" (what force, in any direction, would PD-correct the position):

fideal = −kpep − kvev + mge3 + mp̈wd

The gravity term mge₃ and feedforward term mp̈d pre-cancel gravity and track the desired acceleration. But the quadrotor can only thrust along its own body z-axis. So the controller: (1) sets the desired body z direction zdw = fideal/‖fideal‖, pointing the drone to produce the ideal force; (2) sets the thrust magnitude fz = fideal · RwBe₃ (projected onto actual body z).

Geometric control: why SO(3) matters for attitude

The attitude controller must track the desired rotation Rd ∈ SO(3). A naive approach would represent attitude as Euler angles and apply PID to each angle. This fails at singularities (gimbal lock at pitch=±90°) and doesn't respect the geometry of SO(3).

Geometric control (Lee, Leok, McClamroch 2010) works directly on SO(3) by defining the rotation error using the Lie group structure (exactly as in L2). The rotation error vector is:

eR = ½(RdTRB − RBTRd)

This is the vee (∨) of the skew-symmetric part of RdTRB — essentially the axis-angle error between current and desired rotation, expressed in the body frame. The angular velocity error is eω = ωB − RBTRdωd.

The geometric control law

The full geometric controller torque:

τB = −kReR − kωeω + ωB × JωB − J([ωB]×RBTRdωd − RBTRdω̇d)

The first two terms are PD on the rotation error (in SO(3)). The remaining terms cancel the gyroscopic coupling (ω×Jω) and feedforward the desired angular acceleration. This controller is proven to be almost globally stable on SO(3) — it converges from almost any initial orientation, with only the "upside-down" configuration excluded (a measure-zero set).

Geometric control is PD control on the rotation manifold. The kReR term is the proportional part: proportional to how far the current rotation is from desired. The kωeω term is the derivative part: proportional to relative angular velocity error. The Lie group structure ensures the error metric respects the non-Euclidean geometry of SO(3) — the same insight from L2.
Why does the geometric controller define the rotation error as eR = ½(RdTRB − RBTRd) instead of simply subtracting Euler angles φcurrent − φdesired?

Chapter 8: Showcase: Quadrotor Hover Simulator

This chapter is the payoff. A 2D quadrotor (two rotors, planar motion) uses a PID position controller feeding into a PD attitude controller. Click anywhere to set a new target. Use sliders to tune gains and add wind disturbance. Watch how the drone tilts to move, corrects attitude, and hovers at the target.

This is the cascaded control stack in action. Position error → desired tilt angle → attitude error → differential rotor thrust → motion. The tilt is not commanded directly — it emerges from the position controller demanding a sideways acceleration.
2D quadrotor hover — click to set target

Click on the canvas to set the target position (orange star). The quadrotor uses cascaded PID+PD control. Increase wind to stress the I term. Reduce KP to see sluggishness; increase to see overshoot. The tilt angle is produced by the position controller, not set manually.

KP pos 3.0
KD pos 2.0
Wind (N) 0.0

Chapter 9: Connections & Cheat Sheet

You have now built the complete control stack: from state-space fundamentals through PID and LQR to the geometric controller running on SO(3). Here's how everything connects.

Control cheat sheet

ConceptKey formulaKey intuition
State-spaceẋ = Ax + BuEigenvalues of A determine stability
P termuP = KP·eStiffness: push back proportionally
I termuI = KI·∫e dtMemory: eliminates steady-state error
D termuD = KD·ėDamping: prevents overshoot
LQR costJ = ∫(xTQx + uTRu)dtQ penalizes error, R penalizes effort
LQR gainK = R−1BTPP from Riccati equation (automated)
Quad thrust[fz,τ] = F̄·wInvertible mixer: desired torques → rotor speeds
Cascadepos loop → attitude ref → att loop → mixerOuter loop (slow) → inner loop (fast)
Geometric eReR = ½(RdTRB−RBTRd)Axis-angle error on SO(3) manifold

What comes next: Trajectory Optimization (L4)

The control stack assumes a desired position pd(t) and velocity vd(t) are given at each instant. Where do those come from? That's the job of the trajectory optimizer (Lectures 9–11), which computes smooth, dynamically feasible paths from start to goal. The interface is clean: trajectory optimizer outputs [pd(t), vd(t), p̈d(t), ψd(t)] → geometric controller turns it into rotor commands → drone flies the path.

Connection to Lie groups (L2)

The geometric controller is the application of L2 ideas. The rotation error eR uses the vee map (∨) to extract the axis-angle vector from the relative rotation matrix RdTRB — this is exactly the log map from L2 (at small angles). The proof of almost-global stability relies on the Lyapunov function constructed on the SO(3) manifold, not on a linearized Euler-angle representation.

Related Gleams

"What I cannot create, I do not understand." You can now: write state-space equations for a quadrotor; implement a PID controller with anti-windup; set up an LQR problem and interpret Q and R; understand why a quadrotor must tilt to translate; derive the rotation error in geometric control; and trace an error signal from sensor reading to rotor command through the full cascaded loop.
In the cascaded control architecture, why does the position controller output a desired rotation matrix Rd rather than directly outputting torque commands?