Hand-soldering fine-pitch SMD components is slow and inconsistent. Commercial reflow ovens are expensive; a converted toaster oven needs a proper temperature controller to follow a solder paste profile.
Approach
Converted a toaster oven with a solid-state relay and thermocouple, then wrote a PID controller on a microcontroller to execute a configurable reflow profile (preheat → soak → reflow → cooling).
Outcome
Successfully reflowed boards with components down to X mm pitch. Used on all subsequent personal PCB projects.
Fig. 1 — Converted reflow oven with thermocouple probe and live serial monitor.
Overview
Reflowing SMD boards by hand is tedious and inconsistent — a proper oven lets you run a controlled thermal profile and get repeatable results. Rather than buying a commercial unit, I converted a toaster oven by adding two solid-state relays (one for the top element, one for the bottom), a Type K thermocouple via a MAX31855 breakout, and an Arduino to close the control loop.
The firmware executes a five-stage profile — Preheat, Soak, Transition, Reflow, and Cooling — advancing automatically based on elapsed time or temperature, whichever comes first. Temperature and timestamps are streamed over serial in CSV format each loop cycle, which made it easy to log real trials and plot measured vs. target curves.
Design decisions
The biggest firmware challenge was keeping the temperature tracking smooth without overshoot. I used a dual-gain PID strategy: aggressive tuning (Kp=300, Ki=30, Kd=15) when the oven is far from the setpoint, switching to conservative tuning (Kp=150, Ki=10, Kd=5) within 2 °C. The PID output drives a 5-second time-proportional window rather than PWM, since SSRs are rated for limited switching frequency.
The top and bottom elements are controlled independently — the top element runs at 75% of the bottom's output to compensate for the natural heat distribution of a toaster oven, which tends to be bottom-heavy. On any MAX31855 fault (open thermocouple, short to GND or VCC), both relays cut immediately and the loop exits — no silent failures.
Each stage linearly ramps the setpoint from the temperature at stage entry toward the target, which avoids the step-change overshoot you'd get from instantly jumping the setpoint to the stage maximum.
Reflow profile
Stage
Duration
Target temp
Notes
Preheat
90 s
90 °C
Linear ramp from ambient
Soak
90 s
130 °C
Flux activation
Transition
30 s
138 °C
Approach liquidus
Reflow
30 s
165 °C
Peak — above liquidus
Cooling
30 s
≤ 138 °C
Both relays off; natural cool
Fig. 2 — Measured thermocouple temperature across 5 trial runs. Dashed grey lines mark stage target temperatures (90 / 130 / 138 / 165 °C).
Trial progression
Trial 1 was an early heating test with no stage gating — the oven climbed continuously past every target temperature, reaching nearly 200 °C before being stopped manually. It confirmed the SSR and thermocouple were working, but exposed that the PID alone, without stage boundaries enforcing a setpoint ramp, would happily overshoot indefinitely.
Trial 2 aborted at 59 °C over a 241 s window. The oven sat at ambient for the first 30 s then heated very slowly, suggesting an SSR connection issue or a cold-start condition that prevented the relays from driving at full authority in the early stages.
Trial 3 was the first run with the full five-stage profile and dual-gain PID enabled. The controller tracked the preheat and soak stages reasonably well but stalled around 133 °C — the initial aggressive Kp was too low to push the oven past the soak plateau and into the reflow zone before the stage timer expired. The oven ran the full 270 s cycle but never reached liquidus.
Raising the aggressive gains to Kp=300, Ki=30, Kd=15 gave the controller enough authority to push through. Trials 4 and 5 both tracked the datasheet profile closely — hitting 90 °C near the 90 s preheat mark, clearing 130 °C just after the 180 s soak boundary, and peaking at roughly 170 °C within 10 s of the 240 s reflow target. The slight overshoot above 165 °C is a known artefact of the 5 s proportional window: thermal energy already committed in the last window cycle continues heating the oven slightly after the controller backs off.
Fig. 3 — Reflowed PCB with SMD components, fresh out of the oven.
Technical details
Parameter
Value
Notes
MCU
Arduino (SPI)
Pins 3/4/5 software SPI, 8/9 relay outputs
Thermocouple
Type K
Adafruit MAX31855 breakout
Heating elements
Top + Bottom SSR
Top runs at 75% of bottom output
Control method
Time-proportional PID
5 s window; dual aggressive/conservative gains
PID — aggressive
Kp=300, Ki=30, Kd=15
Gap > 2 °C from setpoint
PID — conservative
Kp=150, Ki=10, Kd=5
Gap ≤ 2 °C from setpoint
Peak temperature
165 °C
Reflow stage target
Total cycle time
~270 s
90 + 90 + 30 + 30 + 30 s
Data logging
Serial CSV
Temperature, timestamp per loop cycle
Solder paste
ChipQuick NC191LT10
No-clean, low-temp Bi-Sn; liquidus ~138 °C
Fault protection
MAX31855 fault flags
Open, short-to-GND, short-to-VCC → both relays cut
Firmware — key snippets
Dual-gain PID switch
double gap = abs(setpoint - currentTemp);
if (gap < 2) {
// close to target — conservative tuning
bottomPID.SetTunings(consKp, consKi, consKd);
} else {
// far from target — aggressive tuning
bottomPID.SetTunings(aggKp, aggKi, aggKd);
}
Time-proportional window + dual SSR
// windowSize = 5000 ms
if (now - windowStartTime > windowSize) windowStartTime += windowSize;
// bottom element
if (bottomOutput > now - windowStartTime) digitalWrite(BOTTOM_RELAY, HIGH);
else digitalWrite(BOTTOM_RELAY, LOW);
// top element runs at 75% of bottom
double topOutput = bottomOutput * 0.75;
if (topOutput > now - windowStartTime) digitalWrite(TOP_RELAY, HIGH);
else digitalWrite(TOP_RELAY, LOW);
MAX31855 fault detection
uint8_t fault = thermocouple.readError();
if (fault) {
digitalWrite(TOP_RELAY, LOW);
digitalWrite(BOTTOM_RELAY, LOW);
if (fault & MAX31855_FAULT_OPEN) Serial.println("Fault: open circuit");
if (fault & MAX31855_FAULT_SHORT_GND) Serial.println("Fault: short to GND");
if (fault & MAX31855_FAULT_SHORT_VCC) Serial.println("Fault: short to VCC");
return; // abort current cycle
}