Practical Control Methods
for engineers who implement, not just derive
Twelve techniques that separate production control code from textbook control code. Every topic has a live simulation you can drag parameters on — the plant integrates in real time, the controller computes in real time, and the effects speak for themselves. Background, math, implementation pseudocode, and rules of thumb follow each demo.
The Smith Predictor
When transport delay dominates, feedback is blindfolded. The Smith predictor hands it a working model of the plant so the controller can act on what will happen, not what already has.
The problem it solves
Consider a first-order plant with pure transport delay: $G(s) = \dfrac{K_p}{\tau s + 1}\, e^{-L s}$. A classical PI controller designed aggressively on $K_p / (\tau s + 1)$ will oscillate badly once $L$ is re-introduced, because each corrective action takes $L$ seconds to reach the output, and the controller keeps piling on more action in the meantime. The loop gain at the delay frequency must stay below 1, which typically means detuning Kp aggressively — sluggish response.
Smith's 1957 idea: give the controller a prediction of the delay-free output $\hat y_0$, let it close the loop on that, and then correct using the difference between the real delayed plant and the modeled delayed plant. If the model matches, the controller sees no delay and can be tuned accordingly.
The signal fed back to the controller is $\hat y_0 + (y - \hat y_L)$. When the model matches perfectly and there are no disturbances, $\hat y_L = y$ and the controller sees only $\hat y_0$ — the delay-free prediction.
L̂/L away from 1 — this is the predictor's Achilles heel. Under 30% mismatch the compensation already starts losing effectiveness; past 50% it can make things worse than plain PI.
Key terminology
| Term | Meaning |
|---|---|
| Dead time L | Pure transport delay, usually from fluid flow, conveyor, computation, or sampling. Measured from an input step to first output motion. |
| Dead-time dominance | Ratio $L/\tau$. $L/\tau > 1$ means delay dominates dynamics — this is where Smith pays off. |
| Internal model | $\hat G(s)$ running in parallel on the DCS/ECU. Must be executed every sample. |
| Primary controller | The PI/PID wrapped around the Smith structure. Tune it as if the plant had no delay. |
Implementation notes
// Discrete Smith predictor, sample time Ts, delay N = round(L/Ts) samples
struct SmithState { float y_model; float buf[N_MAX]; int idx; float I; float u_prev; };
float smith_step(SmithState* s, float r, float y_meas) {
// 1. Run the (delay-free) internal model one step
float a = expf(-Ts / TAU_HAT);
s->y_model = a*s->y_model + K_HAT*(1-a)*s->u_prev;
// 2. Pull delayed-model output from ring buffer
float y_model_delayed = s->buf[s->idx];
s->buf[s->idx] = s->y_model;
s->idx = (s->idx + 1) % N;
// 3. Feedback = undelayed model + correction from real plant
float y_fb = s->y_model + (y_meas - y_model_delayed);
// 4. Standard PI on delay-free feedback
float e = r - y_fb;
s->I += Ki * e * Ts;
float u = Kp*e + s->I;
s->u_prev = u;
return u;
}
When it shines
Paper-machine basis-weight loops, sheet extrusion, long-pipe heating, sensor-downstream processes — anywhere $L/\tau$ exceeds unity by a lot and the plant is well-identified.
When it betrays you
Plants with uncertain or time-varying delay (variable flow rate changes transport time). Plants with low $L/\tau$ — overhead of maintaining the model isn't worth the gain. Disturbance rejection is not improved the same way setpoint tracking is; the correction signal still carries delay $L$.
Rules of thumb
- Cross the threshold $L/\tau > 0.5$ before reaching for Smith. Below that, a well-tuned PID is usually enough.
- Keep $\hat L$ within $\pm 20\%$ of $L$. Above that, stability margin erodes fast. If delay varies with operating point, schedule $\hat L$.
- Re-tune the primary PI assuming $L = 0$. You can typically raise $K_p$ by 2–4× over the non-Smith loop.
- Filter the measurement before the correction term. The path $(y - \hat y_L)$ amplifies measurement noise; a 1st-order LPF at $5/L$ rad/s is a safe default.
- Observe the model output as a diagnostic signal. If $\hat y_L$ drifts from $y$ during steady operation, your plant parameters have moved — retune or adapt.
Two-Degree-of-Freedom PID
One PID cannot simultaneously give aggressive disturbance rejection and smooth setpoint tracking — the gains fight each other. 2-DOF splits the two problems into independent design knobs.
The core insight
For a standard PID, the same $K_p$, $K_i$, $K_d$ act on the reference-error $e = r - y$. Increase $K_p$ and you get snappier disturbance rejection but also huge proportional kick at the moment a setpoint steps. Increase $K_d$ and you get more damping for load disturbances but also a giant derivative spike on setpoint steps. The 2-DOF structure introduces setpoint weights $b$ and $c$:
Integral acts on the full error (so steady-state is zero). Proportional and derivative act on weighted combinations. Pick $b = c = 1$ → standard PID. Pick $b = 0, c = 0$ → "I-PD" form; only integral sees the reference. Typical industrial setting: $b = 0.4{-}0.7$, $c = 0$.
b=1: the amber response overshoots noticeably on the setpoint step. Drop b to 0.4 — the overshoot vanishes while the disturbance bump at t=10s recovers just as fast. That's the point: you got both behaviours with one structure. Now push $K_p$ to 4.5 and watch disturbance rejection sharpen without blowing up the setpoint step, because the higher $K_p$ sees a muted $b \cdot r$.
Equivalent realizations
Several industrial controllers offer 2-DOF under different names:
- Reference weighting form — what we use above: $(b, c)$ weights, same $(K_p, K_i, K_d)$.
- I-PD form — $b = 0$, $c = 0$. Only the integrator sees the setpoint. Smoothest possible setpoint response, but slowest to follow a reference ramp.
- PI-D form — $b = 1$, $c = 0$. Derivative on measurement only; P and I act on error. Very common default; eliminates derivative kick with no tracking penalty.
- Two-filter form — $F(s)\,r$ feedforward path plus a separate feedback controller $C_{fb}(s)$ on $-y$. Equivalent to reference weighting but makes the setpoint prefilter explicit.
Implementation notes
// Discrete 2-DOF PID, parallel form, with filtered derivative
// u = Kp(b*r - y) + I + Kd*d(c*r - y)/dt (D-filter with N)
float pid_2dof(PidState* s, float r, float y) {
float e = r - y;
float ep = b*r - y; // weighted P error
float ed_ref = c*r - y; // weighted D error
// Integral (uses full error — keep for zero steady-state)
s->I += Ki * e * Ts;
// Filtered derivative on ed_ref (backward-Euler on first-order filter)
// D(s) = Kd*N*s / (s + N/Td) with Td = Kd/Kp
float Tf = Kd / (Kp * N);
s->D = (Tf/(Tf+Ts))*s->D + (Kd/(Tf+Ts))*(ed_ref - s->ed_prev);
s->ed_prev = ed_ref;
return Kp*ep + s->I + s->D;
}
Rules of thumb
- Start with $b = 0.4, c = 0$. Matches most industrial defaults (ABB, Siemens, Yokogawa ship this). Moves the closed-loop zero out of a damaging region.
- Use $b$ to shape overshoot, $K_p$ to shape disturbance rejection. They are now decoupled.
- If your reference is smooth (splines, planner output), you can push $b$ closer to 1. 2-DOF matters most for step-like references.
- Never put derivative on the error unless you have a strong reason. $c = 0$ is the default. Let the reference move freely without derivative-kick control spikes.
- Beware of setpoint bumping when changing $b$ online. The control output jumps by $K_p(1-b)r$ at the moment of change — bumpless switching is needed if $b$ is scheduled.
Anti-Windup
The moment the actuator saturates, your integrator is running an open-loop race. Three strategies keep it honest: conditional integration, back-calculation, and the clamping mindset.
Why windup happens
The integrator term $K_i\!\int e\,dt$ accumulates error whenever the output is off target. Normally, once the plant catches up, $e$ changes sign and the integrator discharges. But if the actuator is saturated — rail voltage hit, valve full-open, motor torque clipped — the integrator keeps integrating a large $e$ that the controller cannot act on. When the setpoint is eventually reached, the integrator is now far above its correct value. Result: massive overshoot, long recovery, and often limit-cycle oscillation.
The three main schemes
| Scheme | Idea | Pros & Cons |
|---|---|---|
| Conditional integration ("clamping") | Stop integrating whenever $u_{\text{cmd}}$ is outside the limits and integrating further would make it worse. | + Simple, one if statement.− Can lock the integrator at a stale value → slow recovery. |
| Back-calculation ("tracking") | Feed $(u_{\text{sat}} - u_{\text{cmd}})$ back through gain $K_t$ into the integrator; it pulls the integral toward a value consistent with the saturated output. | + Smooth, tunable recovery. + Industry favorite. − Requires picking $K_t$; too large → noisy, too small → no effect. |
| Integrator clamping with sign test | Only integrate if the sign of $e$ would move $u$ away from the saturated rail. | + Slightly faster recovery than pure conditional. − Still abrupt; bumps on exit. |
| Observer-based (incremental) | Express the controller in velocity (incremental) form: $\Delta u = K_p \Delta e + K_i e \, T_s$; when you clip $u$, no hidden state grows. | + Elegant; windup is structurally impossible. − Requires care at startup; no explicit integral state to inspect. |
Back-calculation in detail
The modified integrator update is:
$$\dot I = K_i\, e + K_t\,(u_{\text{sat}} - u_{\text{cmd}})$$When unsaturated, $u_{\text{sat}} = u_{\text{cmd}}$ and the correction term is zero. When saturated, the term is negative (for positive saturation) and drives $I$ downward until $u_{\text{cmd}}$ equals $u_{\text{sat}}$. The time constant of this decay is approximately $1/K_t$.
// Back-calculation anti-windup, parallel PID
float pid_backcalc(PidState* s, float r, float y, float Ts) {
float e = r - y;
float P = Kp * e;
float D = filtered_derivative(s, y);
float u_cmd = P + s->I + D;
// Apply saturation
float u_sat = clamp(u_cmd, u_min, u_max);
// Integrator with back-calculation correction
s->I += Ts * (Ki*e + Kt*(u_sat - u_cmd));
return u_sat;
}
Rules of thumb
- Anti-windup is not optional on any loop with an actuator that can rail. If $K_i > 0$ and saturation is physically possible, you will see windup eventually — no exceptions.
- Back-calculation is the default for textbook loops; incremental form is the default for embedded loops where state memory is expensive or where you're already computing $\Delta u$ anyway (cascaded current loops in a motor drive).
- Apply anti-windup at the final saturation, not an intermediate one. If a motor drive clips at voltage and torque and current, feed back from the actual clipped signal — otherwise the integrator chases a phantom.
- Anti-windup must also handle rate limits, not just magnitude. A slewing actuator behaves like a saturating one in the short term.
- Test it. Force the actuator into hard saturation with a big reference step. You should see the integrator settle smoothly, not overshoot.
Feedforward Control
Feedback is reactive; feedforward is predictive. If you can measure or model a disturbance (or a reference trajectory), reject it directly rather than waiting for the feedback loop to see its effect on the output.
Disturbance vs reference feedforward
Two distinct flavors:
- Disturbance feedforward: measure the disturbance $d$ and inject $u_{\mathrm{ff}} = -\hat G_d / \hat G\, d$ at the plant input. Perfect cancellation requires perfect plant inversion — usually impossible for physical plants (they're strictly proper), so we approximate in the frequency range where the disturbance lives.
- Reference feedforward: compute the steady-state or dynamic $u$ that would produce the desired reference trajectory, and inject that directly. The feedback controller only fixes the residual error. In motor control, this is the $u_q = L_q \cdot di_q/dt + R \cdot i_q + \omega \cdot L_d \cdot i_d + \omega \cdot \psi_f$ decoupling term.
Kd to 2 (strong disturbance). The PI-only response deviates significantly; the PI+FF combination barely moves. Now introduce model error by shifting K̂d to 0.5 — the FF still helps, but the remaining 50% of the disturbance is handled by feedback. Feedforward reduces disturbance effect rather than eliminating it in practice; feedback covers the residual.
Model-based reference feedforward
For a plant modeled as $G(s)$ and a reference trajectory $r(t)$, ideal reference feedforward is $u_{\mathrm{ff}} = \hat G^{-1}(s)\, r$. Exact inversion is non-causal/non-proper for most plants, so we either:
- Use a static inverse: $u_{\mathrm{ff}} = r / K_{\mathrm{DC}}$ (just the DC gain). Handles steady-state tracking only.
- Use a filtered inverse: $u_{\mathrm{ff}} = \hat G^{-1}(s) \, F(s) \, r$, where $F(s)$ is a low-pass filter to make the product proper. Cutoff at the desired bandwidth.
- Use ZPETC (zero-phase error tracking) for discrete systems with non-minimum-phase zeros.
- Use trajectory-derived feedforward: if $r(t)$ and its derivatives are computed by a planner, substitute directly: $u_{\mathrm{ff}} = J\,\ddot r + b\,\dot r + k\,r$ for a mechanical system.
Implementation notes
// Disturbance feedforward with low-pass filter on FF signal
// (limits high-frequency amplification from differentiation or noise)
float ff_filter_step(FFState* s, float d_meas, float Ts) {
float alpha = Ts / (1.0/OMEGA_F + Ts);
s->d_filtered = alpha*d_meas + (1-alpha)*s->d_filtered;
return -KD_HAT / K_HAT * s->d_filtered; // static plant inverse
}
float control_step(..., float d_meas) {
float u_ff = ff_filter_step(&s->ff, d_meas, Ts);
float u_fb = pi_step(&s->pi, r, y, Ts);
return u_fb + u_ff;
}
Rules of thumb
- Feedforward handles the disturbance you can measure; feedback handles everything else. They work best as a team — never remove feedback thinking FF is enough.
- Don't invert the plant beyond its bandwidth. Cap the FF signal's bandwidth below the plant's sensible frequency range, or you'll amplify noise and excite unmodeled modes.
- Static FF recovers 60–90% of the benefit with 10% of the complexity. Only go to dynamic inversion when you truly need it (high-BW servos, aerospace, web tension control).
- Apply a limit and a slew-rate cap to $u_{\mathrm{ff}}$. A bad sensor reading shouldn't crash the actuator.
- Decoupling terms in motor control are feedforward in disguise. The cross-coupling $\omega L_d i_d$ in the $q$-axis voltage equation is compensated by an FF term, not by a smarter PI.
- A model 50% wrong is still useful. Even a poorly identified plant inverse beats no FF at all, because feedback picks up the residual — but it does degrade with mismatch, just like Smith.
Derivative Kick & Filtering
A naive D term on a discrete error produces a unit-step-sized impulse on every setpoint change and amplifies every bit of sensor noise. Three fixes: D on measurement, filtered D, and bandwidth-limited D.
Problem 1: Derivative on a stepped reference
If $D = K_d \cdot de/dt$ and the reference $r$ changes by $\Delta r$ instantaneously, then $e$ jumps by $\Delta r$ and the derivative is $K_d \cdot \Delta r / T_s$ — a massive actuator spike, potentially saturating the actuator for one sample. Fix: take derivative of $-y$ only (or of $cr - y$ with $c = 0$ in the 2-DOF form).
Problem 2: Derivative of noisy measurements
If $y$ has white measurement noise, $dy/dt$ has unbounded power (power spectral density rises as $\omega^2$). A proper derivative must always be implemented as a filtered derivative, equivalent to a lead compensator:
$$D(s) = \frac{K_d\, s}{1 + (K_d/K_p N)\, s} = K_d\,\frac{N\,s}{s + N/T_d}$$The filter coefficient $N$ (typically 5–20) sets how much high-frequency noise is rejected. $N = \infty$ recovers the pure derivative; finite $N$ puts the filter pole at $N/T_d$ in rad/s.
Practical equivalence: velocity form
Many embedded implementations use the incremental/velocity form which naturally avoids derivative kick on reference changes, because it differences $\Delta e$ in a way that cancels the jump under certain formulations. However, derivative-on-measurement is cleaner and easier to reason about — prefer it.
// Discretized filtered-derivative, derivative on measurement
// Using Tustin (bilinear) for good frequency matching
float d_filtered_on_meas(DState* s, float y, float Ts) {
float Tf = Kd / (Kp * N);
float a = (2*Tf - Ts) / (2*Tf + Ts);
float b = 2*Kd / (2*Tf + Ts);
float D_new = a*s->D_prev + b*(-(y - s->y_prev));
s->D_prev = D_new;
s->y_prev = y;
return D_new;
}
Rules of thumb
- D always on measurement, never on error. This single rule eliminates derivative kick entirely.
- Always filter D. Pick $N \in [5, 20]$. Larger $N$ = less noise rejection, more derivative authority; smaller $N$ = more rejection, slower response.
- If you can't measure $y$ cleanly, consider an observer instead of filtered D. An observer estimates $\dot y$ from the plant model plus measurement, with better noise characteristics than a lead compensator.
- For motion systems, use a dedicated velocity sensor (tach, encoder differential) rather than differentiating position. Most of the time, this is cheaper and cleaner than noise-filtered D.
- Check the D-filter pole location $N/T_d$ against your Nyquist frequency. Filter poles above $\pi/T_s$ alias and behave badly.
Cascade Control
Put a fast loop inside a slow one. The inner loop crushes fast disturbances at their source; the outer loop only sees a cleaner, faster plant. Every production motor drive is cascade: current → speed → position.
The structure
The outer controller's output is the reference of the inner controller. The inner loop runs fast (often 5–20× faster than the outer) and rejects disturbances that enter the inner plant before they reach the outer plant.
When cascade is the right answer
- The inner loop has measurable state (current sensor, flow meter, inner encoder).
- The inner plant is significantly faster than the outer — typically by a factor of 5–10.
- The major disturbances enter the inner loop, not the outer. (Torque ripple hits the current loop; wind gust hits the attitude loop inside position.)
- The inner loop has nonlinearities or constraints you want to handle locally (current limits in a motor, valve dynamics in a flow loop).
// Classic cascade: outer speed loop produces current reference
// Inner current loop produces voltage; plant is a DC motor
void cascade_step(State* s, float omega_ref, float omega_meas, float i_meas) {
// Outer: speed loop at slower rate (e.g. 1 kHz)
if (s->outer_tick) {
float e_om = omega_ref - omega_meas;
s->I_out += Ki_out * e_om * Ts_outer;
s->i_ref = clamp(Kp_out*e_om + s->I_out, -I_MAX, +I_MAX);
}
// Inner: current loop at fast rate (e.g. 10 kHz)
float e_i = s->i_ref - i_meas;
s->I_in += Ki_in * e_i * Ts_inner;
float v = Kp_in*e_i + s->I_in;
write_pwm(v);
}
Rules of thumb
- Inner bandwidth ≥ 5× outer bandwidth — this separation is what makes the design decouple. If they're close, use a single loop or an MIMO design instead.
- Limit the inner reference at the outer controller's output — this is where actuator limits belong in a cascade, not at the motor voltage.
- Anti-windup lives on the outer loop, keyed off the inner reference saturation (not the actuator voltage).
- Current loops in motor drives should have a bandwidth of ~1/10 of the PWM frequency and ~10× the mechanical loop. Typical values: 10 kHz PWM → 1 kHz current loop → 100 Hz speed loop → 10 Hz position loop.
- Always tune the inner loop first. Outer tuning on a poorly-tuned inner loop is a waste of time.
Gain Scheduling
Your plant isn't linear — its gain, inertia, or time constant depends on operating point. One fixed PID will be sluggish in some regions and unstable in others. Schedule the controller gains on a measurable scheduling variable.
The classic pattern
Pick a scheduling variable $\sigma$ (altitude, speed, temperature, load). Linearize the plant at a grid of $\sigma$-points $\{\sigma_1, \ldots, \sigma_n\}$, design a controller for each linearization, and interpolate the controller gains online:
$$K_p(\sigma) = \sum_i \alpha_i(\sigma)\, K_{p,i}, \quad \text{similarly for } K_i, K_d$$The weights $\alpha_i(\sigma)$ come from a 1-D or 2-D lookup with linear/spline interpolation. The same idea applies to state-feedback gains, observer gains, feedforward constants, and filter cutoffs.
gain variation amplitude to 4 and watch the fixed controller ring while the scheduled one stays tight.
Nuances that trip people up
| Issue | What to do |
|---|---|
| Bumpless transfer | Gains change continuously → no bump. But if you swap whole controllers, use bumpless switching (topic 11). |
| Hidden coupling | Gain scheduling is a quasi-static design: it assumes $\sigma$ varies slowly compared to closed-loop dynamics. If $\sigma$ moves fast, you may see instability that no local linearization predicts. Rule: $\dot\sigma$ should be slow relative to closed-loop bandwidth. |
| Scheduling on output | Scheduling on a signal that depends on $u$ creates a hidden nonlinear feedback loop. If possible, schedule on an exogenous signal (setpoint, measured disturbance) rather than a plant state. |
| LPV design | For rigorous guarantees across the envelope, use Linear Parameter-Varying synthesis ($H_\infty$ with parameter-dependent Lyapunov). This is overkill for most problems but essential for aerospace / safety-critical work. |
Motor-control flavor: MTPA scheduling
In a PMSM field-oriented control drive, the plant gain from $u_q$ to torque varies with magnetic saturation and operating point. Production drives schedule everything: current-loop gains on operating current, the MTPA $i_d$-reference on $|i_s|$, feedforward decoupling on $\omega$ and $i_d$. The scheduling table is usually the output of offline identification at each operating point, and online you interpolate.
// 2D gain schedule on (speed, current) with bilinear interpolation
const float Kp_table[N_W][N_I] = { ... };
const float Ki_table[N_W][N_I] = { ... };
void update_pid_gains(float omega, float i_mag) {
float iw = idx_fractional(omega, W_MIN, W_MAX, N_W);
float ii = idx_fractional(i_mag, I_MIN, I_MAX, N_I);
Kp = bilinear(Kp_table, iw, ii);
Ki = bilinear(Ki_table, iw, ii);
// Bumpless: rescale the integrator state when Ki changes
// I_new = I_old * (Ki_old / Ki_new)
}
Rules of thumb
- Pick a scheduling variable that is measurable, slowly varying, and explains most of the plant's nonlinearity. More variables = exponentially more tuning.
- Design at the boundary points first. If the controller works at the extremes, interior points usually interpolate fine. Sample more densely where plant changes fast.
- Rescale the integrator on schedule changes to avoid output bumps (see code above). Not doing this is a common source of small transients that people mistake for measurement noise.
- Verify the linearization families are consistent — no sign flips in controller zeros between grid points, no mode hopping. Discontinuities at grid boundaries break real-time behavior.
- Gain scheduling can mask, not solve, missing feedforward. Often what looks like a gain-scheduling problem is better addressed by adding a $\sigma$-dependent feedforward term.
IMC / λ-Tuning
Internal Model Control collapses the PID tuning problem to a single parameter: the desired closed-loop time constant λ. Given a plant model, the PID gains come out of algebra — no iteration, no Ziegler-Nichols dance.
The structure
IMC splits the plant model $\hat G(s)$ into invertible and non-invertible parts: $\hat G = \hat G_+ \hat G_-$, where $\hat G_+$ contains right-half-plane zeros and time delay (cannot be inverted stably). The IMC controller is
$$Q(s) = \hat G_-^{-1}(s)\, F(s), \quad F(s) = \frac{1}{(\lambda s + 1)^r}$$where $r$ is chosen high enough to make $Q$ proper. Translating IMC back to classical feedback form:
$$C(s) = \frac{Q(s)}{1 - \hat G(s) Q(s)}$$For a first-order plus dead-time plant $\hat G = \dfrac{K}{\tau s + 1} e^{-Ls}$, this simplifies to a PI controller with tuning:
$$K_p = \frac{\tau}{K(\lambda + L)}, \qquad K_i = \frac{K_p}{\tau} = \frac{1}{K(\lambda + L)}$$Only one dial — $\lambda$ — controls the speed/robustness tradeoff. Small $\lambda$ → fast but sensitive to model error. Large $\lambda$ → slow but robust. Typical choice: $\lambda \approx 0.5\tau$ to $2\tau$.
IMC for other plant types
| Plant | Resulting controller |
|---|---|
| $K/(\tau s+1)\,e^{-Ls}$ | PI, $K_p = \tau/(K(\lambda+L))$, $T_i = \tau$ |
| $K/((\tau_1 s+1)(\tau_2 s+1))$ | PID, $K_p = (\tau_1+\tau_2)/(K\lambda)$, $T_i = \tau_1+\tau_2$, $T_d = \tau_1\tau_2/(\tau_1+\tau_2)$ |
| $K/s\,e^{-Ls}$ (integrating) | PI, $K_p = 2(\lambda+L)/K(\lambda+L)^2$, $T_i = 2(\lambda+L)$ |
| $K(-\beta s+1)/(\tau s+1)\,e^{-Ls}$ (RHP zero) | PI, $K_p = \tau/(K(\lambda+\beta+L))$, $T_i = \tau$ — note the RHP zero steals bandwidth into $\lambda$ |
Why IMC tuning is underrated
Ziegler-Nichols produces aggressive controllers designed for 1/4 decay ratio — intentionally oscillatory. Cohen-Coon is similar. Both are tuned for a specific closed-loop behavior that almost nobody wants. IMC tuning instead gives you a deliberately specified closed-loop response, and the knob directly maps to engineering specs: "settle in 3 seconds" → $\lambda \approx 1$ s.
// Compute PI gains from FOPDT model and desired λ
void imc_pi_from_fopdt(float K, float tau, float L, float lambda,
float* Kp_out, float* Ki_out) {
*Kp_out = tau / (K * (lambda + L));
*Ki_out = 1.0f / (K * (lambda + L));
// Integral time Ti = tau ⇒ pole cancellation at the plant's real pole
}
Rules of thumb
- Start with $\lambda = \tau$. It's boring and it works. Speed up only if the application demands it.
- Don't pick $\lambda < L$ — the controller will chase dead-time phase and oscillate.
- IMC gains assume pole cancellation — if your plant has unmodeled dynamics near the canceled pole, you'll see slow drift. Filter your measurement and keep $\lambda$ realistic.
- Skogestad's SIMC rules are an excellent refinement of IMC for practical use; they bound $T_i$ to avoid the over-cancellation problem ($T_i = \min(\tau, 4(\lambda+L))$).
- For integrating plants ($\tau \to \infty$, like liquid-level tanks), the SIMC refinement is essential — classical IMC gives infinite $T_i$, which in practice means "no integral action", which in practice means steady-state error for load disturbances.
Disturbance Observer (DOB)
When you can't measure the disturbance, estimate it. A DOB treats the lump of unknown forces on the plant as a fictitious input, observes it from the input/output data, and cancels it at the actuator.
The idea
Treat the plant as $y = \hat G(s)\,(u + d)$, where $d$ collects every unknown torque, unmodeled dynamics residual, and disturbance. Given $u$ and $y$, a nominal inverse model gives $\hat d = \hat G^{-1}(s)\, y - u$. In practice, $\hat G^{-1}$ is improper and amplifies noise, so a Q-filter is added:
$$\hat d = Q(s)\,\big[\hat G^{-1}(s)\,y - u\big]$$The Q-filter is a low-pass that rolls off above the DOB's useful bandwidth (where model accuracy and noise trade off). The control law is
$$u = u_{\text{fb}} - \hat d$$making the inner loop behave like $\hat G(s)$ regardless of the true plant and disturbances, within the Q-filter bandwidth. This is extremely powerful for motor drives and servos.
Stability and bandwidth constraints
Robustness of a DOB hinges on $\|(1 - Q(j\omega))\,\Delta(j\omega)\| < 1$ where $\Delta$ is the relative model error. The Q-filter cutoff must be placed below the frequency where the model starts to disagree with reality. Aggressive Q (high cutoff) is what makes DOBs dangerous on real hardware — push $\omega_Q$ too high and unmodeled resonance will destabilize the loop.
// DOB for a plant modeled as Gn(s) = 1/(Js + b) (mechanical)
// Implementation: Q(s)/Gn(s) applied to y, minus Q(s) applied to u
// Using Q(s) = 1/(tau_q s + 1)
float dob_step(DobState* s, float u, float y, float Ts) {
// Q/Gn filter applied to y: produces Q·(Js + b)·y
// Discretize (Js + b)/(tau_q s + 1) using Tustin
float a0 = (2*TAU_Q + Ts);
float b_yn = (2*J_HAT + B_HAT*Ts) / a0;
float b_yp = (-2*J_HAT + B_HAT*Ts) / a0;
float a_p = (2*TAU_Q - Ts) / a0;
float Q_over_Gn_y = b_yn*y + b_yp*s->y_prev + a_p*s->QGn_prev;
// Q applied to u: first-order LPF
float alpha = Ts / (TAU_Q + Ts);
s->Q_u = alpha*u + (1-alpha)*s->Q_u;
float d_hat = Q_over_Gn_y - s->Q_u;
s->QGn_prev = Q_over_Gn_y;
s->y_prev = y;
return d_hat;
}
// Apply: u_cmd = u_fb - d_hat (then saturate)
Rules of thumb
- Q-filter cutoff $\omega_Q \le$ 0.3 × (frequency of highest unmodeled dynamics). If you know a resonance sits at 200 Hz, don't put $\omega_Q$ above 60 Hz — DOBs destabilize resonances aggressively.
- The relative degree of $Q$ must be at least the relative degree of $\hat G$. Otherwise $Q \hat G^{-1}$ is improper and won't realize.
- Use the simplest nominal model $\hat G$ that captures what you need to cancel. A rigid-body $1/(Js+b)$ model is often enough for a motor — adding flexibility models invites instability from mismatch.
- DOBs are equivalent to adding an integrator (and more) inside the loop — no standalone integral term is strictly needed. In practice, keeping a light $K_i$ in the outer controller catches steady offsets that slip through DOB mismatch.
- Beware noise amplification from $\hat G^{-1}$. If $\hat G$ has low DC gain, its inverse has high DC gain and sensor bias becomes a disturbance estimate. Use high-pass-ish $Q$ only if noise is a concern and you don't need DC rejection — usually this is a bad tradeoff; fix the sensor instead.
Notch Filters for Resonance Suppression
Mechanical systems have resonances; sensors have pickup frequencies; grids have 50/60 Hz. A notch filter removes a single frequency with minimal phase damage to the rest of the bandwidth — but you have to aim it right.
The transfer function
A second-order notch has the form:
$$N(s) = \frac{s^2 + 2\zeta_z \omega_n s + \omega_n^2}{s^2 + 2\zeta_p \omega_n s + \omega_n^2}$$Both poles and zeros at $\omega_n$; the zero damping $\zeta_z$ sets depth (small $\zeta_z$ → deep notch), the pole damping $\zeta_p$ sets width (small $\zeta_p$ → narrow notch). Setting $\zeta_z = 0$ gives an infinitely deep notch at $\omega_n$; setting $\zeta_z = \zeta_p$ gives a pure all-pass.
Design rules
| Quantity | Choice |
|---|---|
| Center frequency $\omega_n$ | Match the known resonance. Identify via frequency sweep, impact-hammer test, or FRF data. |
| Zero damping $\zeta_z$ | 0.02–0.05 for strong rejection; 0.1+ if you want moderate attenuation with less phase impact. |
| Pole damping $\zeta_p$ | 0.3–0.7 typical. Lower = narrower notch. |
| Notch depth (dB) | $\approx 20\log_{10}(\zeta_z/\zeta_p)$. So $\zeta_z/\zeta_p = 0.1$ → 20 dB notch, $= 0.01$ → 40 dB. |
| Phase at $\omega_n$ | 0° by construction (symmetric zeros/poles). Phase distortion is confined to $\omega_n(1 \pm \zeta_p)$. |
Implementation in the signal chain
Notch placement matters:
- On the reference — removes frequencies the commanded signal is trying to excite. Useful for rejecting known command-generated resonances.
- On the measurement — removes sensor pickup (e.g., grid harmonics, power-supply hum). Does not stabilize a resonance; only hides it from the controller.
- In the forward path — between controller output and actuator. Attenuates the excitation of a mechanical resonance. The usual place for rejecting a structural mode.
// Discrete notch filter via Tustin transform
// H(s) = (s^2 + 2*zz*wn*s + wn^2) / (s^2 + 2*zp*wn*s + wn^2)
void notch_init(NotchCoef* c, float wn, float zz, float zp, float Ts) {
float K = 2.0f/Ts;
float a0 = K*K + 2*zp*wn*K + wn*wn;
c->b0 = (K*K + 2*zz*wn*K + wn*wn) / a0;
c->b1 = (2*wn*wn - 2*K*K) / a0;
c->b2 = (K*K - 2*zz*wn*K + wn*wn) / a0;
c->a1 = (2*wn*wn - 2*K*K) / a0;
c->a2 = (K*K - 2*zp*wn*K + wn*wn) / a0;
}
float notch_step(NotchState* s, const NotchCoef* c, float x) {
float y = c->b0*x + c->b1*s->x1 + c->b2*s->x2 - c->a1*s->y1 - c->a2*s->y2;
s->x2 = s->x1; s->x1 = x; s->y2 = s->y1; s->y1 = y;
return y;
}
Rules of thumb
- Never notch a resonance you haven't measured. "Trust-me" notches based on CAD frequencies almost always miss by 10–30%.
- A too-deep, too-narrow notch is worse than no notch. If the plant resonance moves, the filter does nothing and you've added unnecessary phase lag nearby.
- Check the phase added at your loop crossover. A notch at $\omega_n$ adds phase lag at frequencies up to $\omega_n(1+\zeta_p)$ — if crossover is too close, stability margin drops.
- Notches in series are multiplicative. Two 20 dB notches at slightly different frequencies → a wider 20 dB band rather than deeper rejection at a single frequency.
- Consider adaptive notches (LMS, Goertzel-based) when the resonance drifts. Adapt $\omega_n$ by locking to the detected peak; this is how HCI-type harmonic cancellation works in PMSM drives.
Bumpless Transfer
Switching between manual and auto, or swapping one controller for another, wants to be invisible to the plant. The integrator must pre-track its new value before the switch; otherwise you get a violent transient at the moment of change.
Where bumps come from
Imagine an operator has held a valve at $u_{\text{man}} = 60\%$ for minutes while manual-tuning. They switch to auto; the setpoint is 5, the output is 4.5. The auto PI computes $u = K_p e + I = 0.5\cdot 0.5 + I_0$ — but $I_0 = 0$, so $u$ suddenly drops to 0.25%. The valve slams shut. Same story on auto-to-manual, cold-start of the controller, or any time a scheduled gain changes abruptly.
Handover patterns
| Scenario | Mechanism |
|---|---|
| Manual → Auto | Pre-track: integrator mirrors manual command. On switch, auto takes over continuously. |
| Auto → Manual | Initialize manual command to current $u_{\text{auto}}$. Operator then moves from that value. |
| Controller A → Controller B (gain swap) | Set $I_B = u_{\text{old}} - P_B - D_B$ at switch moment. Or run B in tracking mode concurrently. Most robust for scheduled gain changes. |
| Setpoint-weight change (b, c) | Changing $b$ creates a step $K_p(1-b_{\text{new}})r - K_p(1-b_{\text{old}})r$ in $u$. Rebalance $I$ by the negative of that jump. |
| Cold start with known $y(0)$ | Initialize $I = u_{\text{bias}}$ where $u_{\text{bias}}$ is the feedforward guess of steady-state control. Often $u_{\text{bias}} = r / K_{\text{DC}}$. |
| External SP tracking | In cascade: when outer is in manual, inner SP is set manually; outer integrator tracks to match. When outer goes to auto, no inner-loop reference discontinuity. |
// Bumpless PI with manual/auto modes and tracking
typedef struct { float I; float u_out; int mode; } BumplessPI;
float bumpless_step(BumplessPI* s, float r, float y, float u_man, float Ts) {
float e = r - y;
float u_auto = Kp*e + s->I;
if (s->mode == AUTO) {
s->I += Ki * e * Ts;
s->u_out = u_auto;
} else { /* MANUAL */
s->u_out = u_man;
// Pre-track: drive integrator so u_auto would equal u_man
s->I += KT_TRACK * (u_man - u_auto) * Ts;
}
return s->u_out;
}
Rules of thumb
- Every real controller needs bumpless logic. Even if you don't expose mode switching to the operator, the startup transient is a one-shot bumpless problem: the controller goes from "off" to "active".
- Tracking time constant $1/K_t \approx T_i / 3$ is a safe default — fast enough to catch up within the integral time, slow enough to not fight manual adjustments.
- Display both $u_{\text{auto}}$ and $u_{\text{man}}$ to the operator. They should know how large the bump would be if tracking weren't active — it's a useful diagnostic of controller health.
- On cold start, pre-set the integrator to the expected steady-state value. Zero is almost never the right choice.
- Gain changes from scheduling: rescale the integrator so that $I_{\text{new}} \cdot K_{i,\text{new}} = I_{\text{old}} \cdot K_{i,\text{old}}$. Otherwise the effective integral action jumps.
Reference Shaping & Input Shaping
If your plant has lightly-damped modes you can't mask with feedback, don't hit them hard. Filter the reference so its spectrum has zeros where the plant has resonances, or use discrete impulse sequences to cancel oscillation at its source.
Two approaches
- Reference prefilter — a low-pass or polynomial-trajectory filter that limits the reference's bandwidth. Simple and general. Used in motion-planner systems (S-curves, splines).
- Input shaping — convolve the reference with a specific sequence of impulses so that the plant's residual oscillation from each impulse cancels the previous one. The classic is the Zero-Vibration (ZV) shaper: two impulses $A_1 = 1/(1+K)$, $A_2 = K/(1+K)$ with $K = e^{-\zeta\pi/\sqrt{1-\zeta^2}}$, separated by $\Delta t = \pi/\omega_d$. Leaves zero residual vibration when plant parameters match.
Shaper family
| Shaper | Impulses | Δt | Robustness (freq tolerance) |
|---|---|---|---|
| ZV (Zero Vibration) | 2 | π/ω_d | Low (~5% before noticeable residual) |
| ZVD (Zero Vib & Derivative) | 3 | 2π/ω_d total | Medium (~15%) |
| EI (Extra Insensitive) | 3 | 2π/ω_d total | High (~25%, with tunable Vtol) |
| Multi-mode ZV | N×2 | convolution of single-mode ZVs | Covers multiple resonances simultaneously |
Implementation notes
// ZV input shaper: output is a weighted sum of current and delayed reference
// K = exp(-ζ·π / sqrt(1 - ζ²)), Δt = π / (ω_n · sqrt(1 - ζ²))
// A1 = 1/(1+K), A2 = K/(1+K)
float zv_shaper_step(ZVState* s, float r) {
// buffer length = round(Δt / Ts); push-pop FIFO
s->buf[s->idx] = r;
int j = (s->idx - s->delay_samples + BUF_LEN) % BUF_LEN;
float r_delayed = s->buf[j];
s->idx = (s->idx + 1) % BUF_LEN;
return s->A1 * r + s->A2 * r_delayed;
}
Reference prefilter vs trajectory planner
For motion systems, the industry-standard reference shaping is the jerk-limited trajectory planner (S-curve): it outputs $r(t)$ whose first three derivatives are continuous, bounded by user-specified limits. This is equivalent to a 3rd- or 4th-order prefilter with explicit constraints, and it naturally avoids exciting structural modes and plays well with feedforward ($u_{\text{ff}} = J\ddot r + b\dot r$ uses the planner's derivatives directly).
Rules of thumb
- Reference shaping is a feedforward tool. It doesn't help with disturbances or measurement-driven oscillations — only with reference-excited resonances.
- Use a prefilter when you want simplicity and don't know the plant mode precisely. Use a shaper when you know the mode and want zero residual.
- Shaper + feedback is additive: the shaper handles reference-excited modes, feedback (or a notch) handles disturbance-excited ones.
- Shapers add delay. A ZV shaper delays reference action by $\Delta t / 2$; multi-mode shapers even more. If latency matters, this is the cost.
- Use EI or adaptive shapers when plant frequency varies (crane payload mass, robot arm configuration, liquid-filled tank level). A fixed ZV will fail as soon as the mode drifts 10%.
- For cascaded systems, shape the outermost reference. Shaping inner references post-hoc defeats the purpose if the outer loop ends up commanding a step anyway.
Glossary of Terms
The working vocabulary. Every term here comes up across several topics above.
| Term | Definition |
|---|---|
| Bandwidth (BW) | Frequency at which closed-loop magnitude $|T(j\omega)|$ drops to $-3$ dB. Coarse measure of responsiveness: rise time $\approx 0.35/\text{BW}$. |
| Phase margin (PM) | $180° + \angle L(j\omega_c)$ where $\omega_c$ is the gain crossover. Desired: 45°–60° for robust, well-damped response. |
| Gain margin (GM) | $-20\log|L(j\omega_{180})|$ where $\omega_{180}$ is phase crossover. Desired: 6–12 dB. |
| Sensitivity function $S(s)$ | $S = 1/(1+L)$. Maps disturbance at output to output. Small $|S|$ at low freq = good disturbance rejection. $\|S\|_\infty \le 2$ is a standard robustness target. |
| Complementary sensitivity $T(s)$ | $T = L/(1+L) = 1-S$. Maps reference to output, also maps measurement noise to output. $\|T\|_\infty$ bounds noise amplification. |
| Crossover frequency $\omega_c$ | Where $|L(j\omega_c)| = 1$. Roughly the closed-loop bandwidth for well-damped systems. |
| Settling time | Time for output to stay within $\pm 2\%$ (or $\pm 5\%$) of final value after a reference step. $\approx 4/(\zeta \omega_n)$ for a 2nd-order system. |
| IAE / ISE / ITAE | Integral Absolute / Squared / Time-weighted Absolute Error. Objective functions for auto-tuning. ITAE strongly penalizes late errors, producing well-damped responses. |
| Tustin (bilinear) transform | $s \to (2/T_s)\cdot(z-1)/(z+1)$. Preserves stability and gives good frequency matching up to ~$1/(3 T_s)$. Industry default for discretizing continuous-time designs. |
| ZOH / Euler / Backward Euler | Alternate discretization methods. ZOH preserves DC gain exactly. Forward Euler is simple but can destabilize fast poles. Backward Euler is always stable but distorts frequency. |
| Relative degree | Excess of poles over zeros. Cannot invert a plant of relative degree $r$ with a causal controller — the controller inverse is improper. Limits feedforward design. |
| Observer / estimator | A parallel plant model driven by $u$ and corrected by $y - \hat y$. Produces estimates of unmeasured states. Foundation of DOB, Kalman filter, state feedback. |
| Stiffness (loop) | Informal term for how much the closed-loop resists disturbance. High stiffness = fast disturbance rejection, usually buys you less stability margin. |
| NMP (non-minimum phase) | Plant has a right-half-plane zero or time delay. Limits achievable bandwidth: $\omega_c < \omega_{z}/2$ roughly for RHP zero at $\omega_z$. |
| Robust stability / performance | Stability / performance guaranteed across a specified family of plant models. The Q-filter bandwidth in DOB and the $\lambda$ in IMC are both robustness dials. |
| Waterbed effect | Trading sensitivity in one frequency range costs sensitivity in another: $\int \log|S|\,d\omega = 0$ (Bode integral). You cannot reduce $|S|$ everywhere; you can only move where it's low. |
Rules of Thumb
The meta-heuristics that show up across every control project. Commit these to muscle memory.
Tuning order
- Identify before you tune. A 5-minute step test giving $(K, \tau, L)$ is worth more than a day of Ziegler-Nichols. Save the step-response data for later re-tuning.
- Tune the inner loop first in cascades. Inner loops are usually smaller, faster, and less dependent on outer-loop behavior.
- Tune for disturbance rejection, then check setpoint tracking. If they conflict, use 2-DOF to separate them — don't compromise.
- Set $K_i$ before $K_p$ when possible. Use IMC/λ-tuning: $K_i = 1/[K(\lambda+L)]$, then $K_p$ is determined by $T_i = \tau$. This gives you one knob (λ) instead of two.
- Add derivative last, only if you need it. Most processes run fine on PI. D buys you extra phase margin at the cost of noise sensitivity — only worth it for fast servos and stiff mechanical systems.
Architecture checklist (do before you start coding)
- What is the dominant disturbance and where does it enter? (chooses between FF, DOB, and cascade)
- Is there significant dead time? (answers: Smith predictor or λ ≥ L)
- Can I measure the disturbance? (FF is cheap; DOB is the fallback)
- Are there actuator limits that will be hit? (anti-windup is mandatory)
- Are there mode switches in operation? (bumpless transfer)
- Is the plant linear across the operating envelope? (gain scheduling or LPV)
- Are there lightly damped resonances? (notch filter in forward path, or reference shaping)
- Is setpoint or disturbance rejection primary? (drives 2-DOF weight selection)
Robustness guardrails
- Phase margin ≥ 45°, gain margin ≥ 6 dB, $\|S\|_\infty \le 2$ ($M_S$ ≤ 6 dB), $\|T\|_\infty \le 1.25$ ($M_T$ ≤ 2 dB). If you have to violate these, document why.
- Bandwidth $\le 1/3$ of Nyquist. Discrete-time aliasing will eat you if you push closer.
- Crossover well below any unmodeled resonance. Typically factor of 3 to 10.
- Never rely on a pole-zero cancellation deeper than your model is trusted — RHP cancellations are always unstable.
- Keep the integral action bounded with explicit limits on $I$, not just on $u$. An integrator that can grow past the actuator range is a latent bug.
Implementation hygiene
- All discretizations Tustin unless you have a specific reason otherwise. Exception: a pure integrator is usually backward Euler to preserve exact DC gain.
- Log everything during development. Internal states (integrator, derivative filter, DOB d̂, manual/auto mode), all references and measurements, all saturations. You cannot debug what you cannot see.
- Test saturation, mode switches, and parameter-boundary cases. Most field failures are transient — they live at the edges.
- Unit-test the controller in isolation. Mock the plant, feed known inputs, check known outputs. This is cheap and catches almost every arithmetic bug.
- Run the controller at a fixed, deterministic sample rate — jitter changes the effective gains and can hide bugs until a production load.
- Match fixed-point scaling to the physical range. $Q15$ for normalized signals is not always right; a torque command in Nm needs scaling tied to the drive's torque range, not to $\pm 1$.
When to stop tuning and change the structure
- If raising $K_p$ or $K_i$ causes oscillation before hitting your performance target → you need feedforward, a faster inner loop, or a DOB. Tuning won't save you.
- If setpoint tracking and disturbance rejection demand conflicting gains → add 2-DOF.
- If the controller works at one operating point but not another → gain schedule or adapt.
- If a notch is wider than 0.3 decades → reconsider; the resonance may actually be a modeling error.
- If your response looks good in simulation but oscillates in hardware → check for sampling delay, actuator rate limit, or sensor filter not captured in simulation.
- If you're tuning four or more loops to talk to each other — stop, draw the block diagram, and reconsider the architecture. More loops ≠ better control; the right loops do.