The reflow oven on a cutting mat: a Hamilton Beach toaster oven with a black 3D-printed face plate holding the touchscreen, two LEDs and a switch
Projects / Embedded

Reflow Oven

My brother and I turned a $50 toaster oven into a reflow oven. In spring 2026, I went back and designed its temperature controller properly: modeled from measured data, verified in Simulink, then tuned on the real oven.

RolePCB, Firmware & GUI
Timeline2024 · 2026
Key partsESP32 · PT100 · SSR
Read— min

Overview

We are going to be building a lot of SMD PCBs, so we wanted a reflow oven. However, consumer reflow ovens are very expensive and require a lot of mods to work well, so my brother and I turned a $50 Hamilton Beach toaster oven into our own. It worked, but the temperature controller was tuned by trial and error: change a gain, run a cycle, wait for the oven to cool down, and try again. It was good enough to solder boards, but I couldn't really explain why the gains were what they were.

In spring 2026, I took a controls class (EECE 5580), and for the final project, I decided to go back and do it properly. This time, I modeled the oven from measured data, designed the controller with the math I learnt in class, simulated it in Simulink, and then ran it on the real oven.

Loading a board, running a reflow cycle, and the finished board.

Background

To solder SMD components with solder paste, the board has to follow a specific temperature profile. I use lead-free paste, and its profile has four stages:

Lead-free reflow profile: 25°C to 150°C by 90 s, 175°C at 180 s, 217°C at 210 s, peak 249°C at 240 s, then cooling
Lead-free reflow profile.

What makes this hard is that the oven is slow and delayed. When the heater turns on, the temperature sensor doesn't see anything for a while because the heat has to travel through the air and metal in the oven first. That delay is called dead time, and it's the main reason my old trial-and-error tuning was so painful.

Hardware

Before getting into the controller, here's what it's actually driving. When we first built the oven, we wanted it to fit a medium-sized board and look clean, with no wires dangling around. Here's a quick look at the build:

Put together, the control loop is simple: the RTD measures the oven temperature, the ESP32 computes a duty cycle, and the solid-state relay turns the heater on and off to match it. Everything in the rest of this project lives in the ESP32's firmware.

PCB design

I designed a custom PCB to make sure there are no loose wires lying around. This PCB was designed in KiCad and fabricated using JLCPCB.

KiCad schematic of the reflow oven board: 3.3 V power, ESP32, display connector, MOSFET drivers and buzzer, thermocouple amplifierSchematic
KiCad layout of the reflow oven board with terminal blocks around the edges and the ESP32 footprint in the middleLayout
The board in KiCad.
The bare black reflow oven PCB held in a hand
The fabricated board from JLCPCB.

Face plate

The face plate was designed in Fusion 360 and it was fabricated using a 3D printer.

Fusion 360 model of the face plate with cut-outs for the display, two LEDs, a switch and a USB port
Face plate in Fusion 360.

Firmware

The code was developed in Arduino using Platform.io for the ESP32. It was structured as a state machine, with state transitions triggered by touch commands to ensure efficient and organized control flow.

The start of main.cpp: includes, pin definitions, PID variables and the Reflow_Mode enum
The top of main.cpp, ending in the Reflow_Mode states.

GUI

The GUI was designed in Adobe Illustrator, with the pages exported and converted into a .c file for deployment on the ESP32. The live graph was rendered using the drawPixel function from the TFT_eSPI library.

GUI home page with Profiles and Bake buttons and the oven temperature
GUI profiles page listing the CQ NC191SNL50 solder paste, with a Home button
GUI page for the CQ NC191SNL50 profile with Abort and Start buttons
GUI bake page with plus and minus buttons for temperature and time, and a Start button
GUI open-oven warning page: caution, hot content, with an Okay button
GUI live graph of the reflow profile with time, temperature and mode readouts and an Abort button
GUI pages for the 240 x 320 display.

Parts list

Design goals

Modeling the oven

Thermal systems like this are usually modeled as a first-order plus dead-time (FOPDT) system:

$$ P(s) = \frac{K}{\tau s + 1}\, e^{-\theta s} $$

\(K\) is the gain (how hot the oven eventually gets for a given heater duty cycle), \(\tau\) is the time constant (how fast it gets there), and \(\theta\) is the dead time. To find these values, I ran bump tests: starting from room temperature, I set the heater to a fixed duty cycle and logged the temperature every second until it stopped rising. I did this at 4 different duty cycles and fit the FOPDT model to each run using a least-squares curve fit in Python (I wrote the script with some help from Claude). Below are the results.

Four bump tests at 10, 25, 40 and 100 percent duty, each with measured temperature and the FOPDT fit overlaid
Bump tests at 26, 64, 102 and 255 out of 255 duty, with the FOPDT fit on top.

The model fits all 4 tests really well (R² above 0.999 for every one), so the FOPDT model is a good description of the oven.

Duty (of 255)Normalized inputK (°C)τ (s)θ (s)
260.10913.0585.752.7
640.25760.0482.940.0
1020.40666.5395.329.6
2551.00404.8193.719.5

The first thing I noticed was that the numbers change a lot depending on the duty cycle. \(K\) and \(\theta\) both drop by about half from the lowest to the highest duty cycle, so the oven isn't really a linear system. I picked the 40% duty model because it sits near the middle of what the reflow profile demands on average:

$$ G(s) = \frac{666.5}{395.3\, s + 1}\, e^{-29.6 s} $$

Since the numbers vary this much, whatever controller I designed would have to handle a model that isn't exactly right.

Controller design

Why PI?

Most of the reflow profile is a ramp, not a constant setpoint. The oven model has no integrator in it, so a proportional-only controller (or the PD controller I had before) would fall further and further behind a ramp. Adding an integrator (a PI controller) fixes that, and the steady-state error to a ramp becomes a finite value:

$$ e_{ss} = \frac{R}{K K_i} $$

\(R\) is the ramp rate. So the plan was: use a PI controller and choose the gains so this error is less than 5°C.

Handwritten derivation of the steady-state ramp error for a PI controller on the FOPDT plant
Handwritten steady-state error derivation.

SIMC tuning

To pick the gains, I used the SIMC tuning rules by Sigurd Skogestad. The idea is to pick the closed-loop response you want and solve for the controller that gives you that response. I chose a first-order response with the same dead time as the oven, since you can't get rid of the dead time:

$$ \begin{gathered} T_{cl}(s) = \frac{e^{-\theta s}}{\tau_c s + 1} \\[10pt] C(s) = \frac{T_{cl}}{P(s)\,\bigl(1 - T_{cl}\bigr)} \end{gathered} $$

Approximating the dead time with a first-order Taylor expansion (\(e^{-\theta s} \approx 1 - \theta s\)) and simplifying gives the PI gains:

$$ K_p = \frac{\tau}{K(\tau_c + \theta)}, \qquad K_i = \frac{1}{K(\tau_c + \theta)} $$

Here, \(\tau_c\) is the closed-loop time constant. It's the one knob you get to turn: smaller is faster and more aggressive, larger is slower and safer.

Handwritten SIMC derivation of the PI gains
Handwritten SIMC derivation.

PI alone isn't enough

The steepest part of the profile is the preheat ramp, which goes from 25°C to 150°C in 90 seconds:

$$ R = \frac{150 - 25}{90} \approx \frac{25}{18}\ {}^\circ\mathrm{C/s} $$

Plugging the SIMC gains into the steady-state error equation and requiring it to be less than 5°C gives:

$$ \begin{gathered} R(\tau_c + \theta) < 5 \;\;\Rightarrow\;\; \tau_c < \frac{18}{5} - \theta \\[4pt] \tau_c < 3.6 - 29.6 = -26\ \mathrm{s} \end{gathered} $$

The closed-loop time constant would have to be negative, which is impossible. The dead time alone (29.6 s) is already way bigger than the 3.6 s budget. No matter how I tuned it, a PI controller by itself could not track this ramp within 5°C. This was the point where I realized why my old controller needed so much trial and error.

Adding feedforward

The fix is to stop making the feedback controller do all the work. Since I have a model of the oven, I can use it to calculate ahead of time how much heater power is needed to follow the ramp. Inverting the plant at low frequency gives:

$$ u_{ff}(t) = \frac{\tau R + r(t)}{K} $$

\(r(t)\) is the current reference temperature and \(R\) is the current ramp rate. The \(r(t)/K\) part is the power needed to hold the oven at that temperature, and the \(\tau R/K\) part is the extra power needed to keep climbing at rate \(R\). On top of that, I advance the reference by \(\theta\) seconds, so by the time the heat actually reaches the sensor, it lines up with the profile.

The PI controller stays in the loop, but now it only has to clean up whatever small error is left over from the model not being perfect.

Handwritten system parameters and feedforward calculation
Handwritten system parameters and feedforward calculation.
Full handwritten calculations (PDF)

Simulation

I built the whole system in Simulink: the reflow profile and its derivative (for the ramp rate \(R\)), the feedforward block, the PI controller, a saturation block (since the heater duty cycle can only go from 0 to 100%), and the FOPDT oven model with the dead time.

Simulink model: reflow profile and its derivative, feedforward block, PID block, saturation, FOPDT plant with transport delay, and scopes
Simulink model of the system.

With the PI + feedforward controller, the simulated oven temperature sits right on top of the reference for the entire profile. The tracking error is less than a degree.

Simulated reference and actual temperature overlapping for the whole profileTemperature (°C)
Simulated tracking error, within about plus or minus 1.2 degreesError (°C)
Simulation with PI + feedforward. Time in seconds.

At this point, I was feeling pretty good about it. Then I ran it on the oven.

Testing on the oven

First hardware run

I implemented the controller in the oven's firmware on the ESP32 and ran a full reflow cycle. The error went past ±15°C, about three times my spec. The oven ran way ahead of the setpoint on the preheat ramp and again at the start of the reflow stage.

First hardware run: actual temperature running ahead of the setpointTemperature
First hardware run: tracking error reaching about 15 degreesError
First hardware run.

What went wrong

After looking at the data, I found three issues:

Hardware run with no dead-time advance after preheat: temperatureTemperature
Hardware run with no dead-time advance after preheat: errorError
Hardware run with no dead-time advance after preheat.

Redesign

Instead of jumping to something complicated like gain scheduling, I went back to the simulation and changed the design dead time from 29.6 s to 5 s to match what the oven does once it's warm. I kept the same SIMC structure and treated the proportional gain as a free parameter to sweep on the hardware.

Before flashing it, I checked it in simulation with the plant gain multiplied by 1.1 to fake a 10% model mismatch. The error still stayed within ±5°C, so it was worth trying on the oven.

Simulink model with an extra gain block scaling the plant gain
Simulink model with the plant gain scaled.
Simulation with 1.1 times plant gain: temperatureTemperature (°C)
Simulation with 1.1 times plant gain: errorError (°C)
Simulation with 1.1× plant gain.

Tuning Kp on the oven

I tried 3 values of \(K_p\) on the real oven: 1×, 3×, and 5× the SIMC value. Below are the results.

Hardware run at 1 times Kp: temperature
Hardware run at 3 times Kp: temperature
Hardware run at 5 times Kp: temperature
Hardware run at 1 times Kp: error1× Kp
Hardware run at 3 times Kp: error3× Kp
Hardware run at 5 times Kp: error5× Kp
Hardware runs at 1×, 3× and 5× the SIMC \(K_p\). Top: temperature. Bottom: error.
KpResult
1×Much better than the first run, but the error still drifts outside ±5°C on the steeper ramps.
3×Error band gets tighter, but small oscillations start showing up in the soak stage.
5×Error stays within ±5°C for the entire profile. There are some small oscillations in the soak, but it meets the spec.

This is the classic trade-off: the more aggressive the controller, the tighter the tracking, but the more it oscillates. At 5× \(K_p\), the oscillations in the soak are small and harmless, so I went with that.

Robustness check

I wanted to make sure 5× \(K_p\) wasn't just a lucky tune that only works with this exact model. So I changed the model gain in the controller to \(K = 759.9\) (the value from the 25% duty bump test). This is a 14% mismatch from the design value of 666.4. The controller stayed stable and the error stayed within ±5°C.

Robustness test with K = 759.9 and 5 times Kp: temperatureTemperature
Robustness test with K = 759.9 and 5 times Kp: error within plus or minus 5 degreesError
Robustness test (\(K = 759.9\), 5× \(K_p\)).

Limitations and future work

What I learnt

Conclusion

In the end, the oven now tracks the lead-free reflow profile within ±5°C and stays stable even with a 14% model mismatch. It still took a bit of tuning at the end (5× \(K_p\)), but this time, it was tuning with a reason behind it, and I can actually explain why the controller works.

KiCadESP32PlatformIOFusion 360SimulinkControl systems