PID Controller Tuning on a Thermal System: Manual vs Ziegler-Nichols vs Autotuner
Thermal systems have large time constants and pure dead time that make Ziegler-Nichols overshoot badly. What actually worked and how I validated it without an industrial process tool.

Thermal systems combine large time constants (30–120 s) with pure dead time from sensor placement (5–15 s)—Ziegler-Nichols tuned on this plant overshot 12 °C and oscillated for an hour. What worked: manual bump test for FOPDT model, conservative PI with derivative on PV only, and a 30-minute soak validation protocol instead of trusting autotuner defaults from a $200 PID module.
Plant under test
- Heater: 500 W cartridge, SSR driven PWM 1 Hz (time-proportional)
- Sensor: PT100 → MAX31865, 10 cm from heater, 40 cm from load chamber center
- Setpoint: 85 °C operating point, ambient 22 °C
- Controller: STM32G431 @ 170 MHz, control loop 1 Hz
- Disturbance: door open 10 s releases ~15 °C
Goal: ±1 °C at setpoint within 20 min of cold start, no sustained oscillation.
Ziegler-Nichols (ultimate gain method) — failed
Procedure: disable I and D, increase Kp until sustained oscillation (Pu, Ku).
Result: Ku occurred near heater duty 78% with Pu ≈ 180 s—uncomfortably close to SSR max cycling. ZN table (classic PID) gave Kp=0.6Ku, Ti=Pu/2, Td=Pu/8.
Step response: 12 °C overshoot, slow decay over 40 min. Thermal mass absorbed energy; D term on error amplified sensor noise from 50 Hz mains pickup (see ADC noise floor on STM32—we filtered PT100 before PID).
ZN assumes linear first-order plant. Our dead time / time constant ratio θ/τ ≈ 0.35—borderline where ZN overshoots badly per literature.
Autotuner (commercial PID module) — mixed
Autotuner in a $189 Eurotherm-style module identified model and suggested PI. Faster than manual—15 min relay test. Still overshot 6 °C because autotuner optimized for step response time, not our overshoot constraint.
Useful data: estimated τ=95 s, θ=12 s. We kept estimates, discarded gains.
Manual FOPDT + conservative PI — winner
Bump test: 40% → 60% duty step, log PV, fit FOPDT with Python script (scipy.optimize, not on-device).
Initial PI from IMC tuning (λ = 3θ):
Kp = τ / (K * (λ + θ))
Ti = τ
With K=0.42 °C/%, τ=95 s, θ=12 s, λ=36 s → Kp≈4.8, Ti=95 s.
Implementation (velocity form, anti-windup):
typedef struct {
float kp, ti, td;
float integral, prev_pv;
float out_min, out_max;
} pid_t;
float pid_step(pid_t *p, float sp, float pv, float dt) {
float err = sp - pv;
p->integral += err * dt;
p->integral = clampf(p->integral, p->out_min, p->out_max);
float d_pv = (pv - p->prev_pv) / dt;
p->prev_pv = pv;
float out = p->kp * (err + p->integral / p->ti - p->td * d_pv);
return clampf(out, p->out_min, p->out_max);
}
Derivative on process variable only—avoids setpoint kick when SP steps.
Final gains after one conservative tweak: Kp=3.2, Ti=110 s, Td=0 (no D needed after PT100 filtering).
Results: 2.1 °C overshoot, settle within ±1 °C in 18 min, door disturbance recovery 4 min.
Validation without industrial tools
- CSV log:
{t, sp, pv, duty, integral}at 1 Hz to UART → Python matplotlib - Criteria checklist signed before ship: max overshoot, settling time, IAE metric
- 3 cold starts, 3 hot restarts, 1 door event—minimum sample
Bench PSU logging correlated heater power with reported duty—bench PSU autobiography part 3 style sanity check.
Comparison summary
| Method | Overshoot | Time to ±1 °C | Engineer time |
|---|---|---|---|
| Ziegler-Nichols | 12 °C | 35 min | 2 h |
| Module autotune | 6 °C | 22 min | 30 min |
| Manual FOPDT PI | 2.1 °C | 18 min | 4 h |
Manual took longest calendar time but met spec without hardware cycle limit.
What I'd do next
- Feed-forward from known door state (magnetic reed)—trim integral windup during known disturbances.
- Gain schedule for setpoints above 120 °C where radiation losses nonlinear.
- Log tuning constants in NVM with version field—support can diff field units.
Integrator windup on saturations
Heater saturated at 100% duty during ramp—integral term accumulated assuming output could go higher. Anti-windup clamped integral but conditional integration (pause I when saturated) reduced overshoot another 0.8 °C without retuning Kp/Ti.
Sensor filter interaction
PT100 PT1 filter with τ=2 s added effective delay—PID saw stale PV. Either account delay in θ estimate or filter less aggressively; we reduced filter to τ=0.5 s after identifying phase lag in Bode plot from step test data.
Relay tuning vs PID
Door open disturbance dominated loop—not fixable by PID alone without feed-forward. Magnetic reed on door triggered temporary output cap reduction; manual tweak beat autotuner because disturbance was discrete event, not step response metric.
Logging rate for tuning sessions
UART CSV at 1 Hz filled flash during week-long soak—switched to log only step changes >0.5 °C or duty delta >5%. Enough for post-mortem, not drowning in points.
For thermal plants, skip textbook ZN unless you enjoy oscillation. Model dead time, tune PI conservatively, validate with soak tests longer than your QA shift.
Manish Bookreader
Electronics enthusiast, Embedded Systems Expert, Linux/Networking programmer, and Software Engineer passionate about AI, electronics, books, and cooking.

