← SHEET 02 · ASSEMBLIES EL-003
PID Temperature Controller
| PART NO | EL-003 |
|---|---|
| MATL / SYSTEM | ARDUINO NANO · NTC 100K · SSR |
| TOOLS | Arduino IDE · Wokwi · C++ |
Same control problem as the Arduino PID motor rig and the RL-control series — closed-loop feedback with saturation and noise to manage — but swapped from position to temperature, and written as embedded firmware instead of a Python/Gymnasium sim. No PID library: the controller, the anti-windup, and the derivative filtering are all hand-rolled so the gain structure is fully inspectable, the same discipline as the motor rig’s discrete PID.
OVERVIEW & MOTIVATION
A bench temperature loop: hold a setpoint against a slow, saturating, noisy plant. Temperature control differs from the motor rig’s position loop in three ways that shape the firmware — the actuator (SSR) can’t be finely modulated so it needs a time-proportional (slow-PWM) drive instead of true PWM; the sensor is a nonlinear NTC thermistor needing a Steinhart-Hart-style linearization; and getting it wrong is a fire risk, not just an overshoot, so the cutoff logic is as much a design requirement as the control loop itself. The target plant is a small resistive heater plate (hotplate / preheat-bed class), driven from an Arduino Nano, with a rotary encoder for setpoint entry and an OLED for live status.
COMPONENTS & BOM
| REF | COMPONENT | SPEC | ROLE |
|---|---|---|---|
| U1 | Arduino Nano | ATmega328, 5 V logic | runs the PID loop, drives the SSR window |
| RT1 | NTC thermistor | 100 kΩ @ 25 °C, beta ≈ 3950 | temperature sensing element |
| R1 | Reference resistor | 100 kΩ, 1% | forms the sense divider with RT1 |
| K1 | Solid-state relay (SSR) | zero-cross, rated above heater current | switches heater power from a logic-level input |
| DISP1 | SSD1306 OLED | 128×64, I2C, addr 0x3C | shows setpoint / actual / output % / fault |
| ENC1 | Rotary encoder | KY-040 module (CLK/DT/SW) | setpoint entry + step-size toggle + fault-reset |
| LED1 | Fault indicator LED | 5 mm + 220 Ω series R | lit while a safety cutoff is latched |
| — | Heater element | resistive, matches SSR/supply rating | the actual plant (real build only) |
WIRING
<text x="412" y="53">SDA</text><text x="365" y="153">A4</text>
<text x="412" y="65">SCL</text><text x="365" y="165">A5</text>
<text x="412" y="77">VCC</text><text x="365" y="177">5V</text>
<text x="412" y="89">GND</text><text x="365" y="189">GND</text>
<text x="412" y="249">CLK</text><text x="365" y="201">D2</text>
<text x="412" y="260">DT</text><text x="365" y="213">D4</text>
<text x="412" y="271">SW</text><text x="365" y="225">D5</text>
<text x="412" y="282">VCC</text><text x="365" y="237">5V</text>
<text x="412" y="293">GND</text><text x="365" y="249">GND</text>
<text x="326" y="292">D6</text>
<text x="112" y="296">COM/NO</text>
| MCU PIN | NET | PERIPHERAL PIN |
|---|---|---|
| A0 | THERM_SENSE | NTC divider OUT |
| A4 | I2C_SDA | OLED SDA |
| A5 | I2C_SCL | OLED SCL |
| D2 | ENC_CLK | Encoder CLK (INT0) |
| D3 | SSR_DRIVE | SSR / relay IN |
| D4 | ENC_DT | Encoder DT |
| D5 | ENC_SW | Encoder push button |
| D6 | FAULT_LED | Fault LED (220 Ω to GND) |
| 5V | +5V | NTC VCC, OLED VCC, encoder VCC, SSR/relay VCC |
| GND | GND | common return |
CONTROL DESIGN
Three deliberate choices, none from a library:
Anti-windup by conditional integration. The integral only accumulates while the unclamped output isn’t already saturated in the same direction the error is pushing it — cheaper than back-calculation and sufficient for a slow thermal plant with a hard 0–100% output range.
double unclamped = pTerm + integralTerm + dTerm;
bool satHigh = unclamped > 100.0, satLow = unclamped < 0.0;
bool wouldGrowSaturation = (satHigh && error > 0.0) || (satLow && error < 0.0);
if (!wouldGrowSaturation) integralTerm += Ki * error * dt;
Derivative-on-measurement, not on error — the D term reacts to the plant’s rate of change, not to a setpoint step, so turning the knob doesn’t spike the output:
double dMeasurement = (measurement - prevMeasurement) / dt;
double dTerm = -Kd * dMeasurement; // note: -Kd, since d(error)/dt = -d(measurement)/dt
Time-proportional output. An SSR can’t be PWM’d at kHz like a MOSFET gate without wearing out the contactor equivalent inside it, so the 0–100% PID output is converted into an on-time within a fixed 1 s window instead — the classic slow-PWM pattern for SSR-driven heaters:
unsigned long onTimeMs = (unsigned long)(outputPct / 100.0 * WINDOW_MS);
bool ssrOn = (now - windowStartMs) < onTimeMs;
SIMULATION
Wokwi project: Arduino Nano + wokwi-ntc-temperature-sensor + board-ssd1306 + wokwi-ky-040 +
wokwi-relay-module (SSR stand-in) + a fault LED. Load sketch.ino and diagram.json from
sources/electronics/pid-hotplate/ into a new Wokwi Arduino Nano project (add Adafruit SSD1306
and Adafruit GFX Library via the library manager), then run:
- Click the NTC part and drag its temperature slider — this is the plant disturbance the loop reacts to; the firmware reads it through the same divider math a real 100k NTC would use.
- Turn the encoder to move the setpoint; short-press toggles the step size (1 °C / 5 °C); long-press (≥1.5 s) clears a latched fault once the reading is sane again.
- Watch the OLED’s
SP/PV/OUT %line and the relay LED to see the time-proportional window open and close as the loop converges. - Drag the slider past
TEMP_MAX_C(150 °C) to trip the overtemp cutoff, or unplug the NTC wire to see the sensor-fault cutoff latch the fault LED (D6).
Substitutions, noted honestly: the relay module stands in for an SSR (no SSR part exists in
Wokwi’s library) — its IN pin is the same logic-level control signal a real SSR would take.
Wokwi’s NTC module is a fixed 10 kΩ/10 kΩ divider internally; the firmware’s constants are set so
the ratio still matches a real 100 kΩ/100 kΩ build (see the README for the derivation). There is
no thermal link from the relay’s switched side back to the NTC in the simulator — closing the
relay doesn’t heat the simulated sensor, only the temperature slider does.
STATUS
Design and simulation stage. The firmware, the Wokwi circuit, and the safety-cutoff logic are
written and run in-simulator; no bench hardware has been built and no gains have been tuned
against a real thermal plant. Kp/Ki/Kd in sketch.ino are reasonable starting points for a
small resistive plate, not measured. Ahead: source the SSR and NTC, build the mains-isolated
heater side, mount the sensor in thermal contact with the plate, and re-tune from a real step
response.
USE CASES & APPLICATIONS
Time-proportional SSR-driven PID is the standard pattern for slow, saturating thermal loops: reflow/preheat plates, 3D-printer heated beds, lab incubators, and small industrial oven/kiln zones all use the same three-term structure with anti-windup and a slow-PWM output stage — the mains-side details differ, the control math doesn’t.
FILES
sources/electronics/pid-hotplate/sketch.ino— complete firmware; pinout and safety notes in the header comment.sources/electronics/pid-hotplate/diagram.json— Wokwi circuit, importable directly.sources/electronics/pid-hotplate/README.md— Wokwi run steps, sim-vs-real substitution table, and real-build notes (mains safety warning up front).