QCW DRSSTC controller on ESP32: hardware ramp generator, interrupter, live ramp display and a home-etchable board (CC BY 4.0)
posted by HVfan · in Dual Resonant (DRSSTC) ·
- #1

Hi all. This whole thing started when I saw the QCW controller @merethihighvoltage shows on TikTok - the box with the knobs and the little screen drawing the ramp. I liked it a lot and tried to copy it, but the code was never shared, so in the end I designed and wrote everything myself from scratch: the ramp generator, the display, the board. I'll attach a photo of the original below so you can see where the idea comes from; none of the code or the schematic is his, only the inspiration. License is CC BY 4.0 - do what you want with it, just mention where it came from.
Background
The first version ran on an Arduino Nano. Timer1 made the PWM for the ramp, the interrupter was a pin toggled from the main loop, the display talked over bit-banged I2C. It worked, honestly, but it had a few problems that kept bugging me. The ramp only had a few hundred steps and everything else the CPU did leaked into the timing. The E-STOP was just a pin the code looked at from time to time. And when I turned a knob during AUTO, the display simply stopped updating because the link never found a free moment between pulses. At some point I decided to move the whole thing to an ESP32 and do it properly instead of patching the Nano code yet again.What I wanted from v5.9:
the ramp has to come out of a hardware peripheral, not out of a loop;
E-STOP has to kill the outputs even if the firmware is hung;
knobs and three buttons, no menus, no touch screen fiddling with gloves on;
I want to see the ramp before I fire it, with real numbers, not guess it from the arc;
optical fiber between the box and the driver, nothing else;
a board I can make on the kitchen table with toner transfer.
How the box is put together
Nothing exotic. An ESP32 DevKit V1 in socket headers, two TC4420 drivers that push about 60 mA into HFBR-14xx fiber transmitters, and a row of pin headers: seven pots, FIRE, AUTO, the E-STOP switch (two contact groups, more on that below), a five-wire cable to the display, and a screw terminal for 5 V from a small buck converter hanging off a 12 V battery.On the other end of the two fibers sits my driver board with HFBR-2412 receivers set up so that light means "enabled" - if a fiber breaks, the coil just stops. The buck modulator there is an FF200R12KE4 brick with 15 ohm gate resistors, an 800 uH choke, 650 V on the bus (325 V while I'm commissioning something) and an isolated +15/-9 V DC-DC for the gates. The bridge and the coil itself are a different story, I'll write that up separately.
The ramp
This is the part I'm actually happy with. The ramp is PWM on GPIO19 from the ESP32's MCPWM block. The timer runs at 16 MHz with a period of PWM_TOP+1 = 641 ticks, which gives 25 kHz and 640 possible levels:f = 16 000 000 / (PWM_TOP + 1)
On every period the timer fires a "counter reached zero" callback, the callback calls calculate_duty() for the current moment inside the pulse and loads the next compare value. That's it. Forty microseconds per step, and nothing in the main loop can delay it, because during a pulse the main loop isn't even running - the firmware sits in a busy wait and only the carrier interrupt and the fault logic exist.
The shape itself is what you'd expect from a QCW box. There's a wick - a starting step so the buck output is already up when the bridge comes on. I set it in volts (40 V on my coil), the code converts it to duty from the bus voltage. Then a rise to a knee point, and the knee can be moved in time and in power with two knobs. Then the peak, with a correction for the bus sagging under load (about +21 % on my capacitor bank, it's a constant you tune once with a scope). Then the fall, which the FALL knob makes anywhere from an abrupt cut-off to a fairly gentle slope. Pulse width goes from 5 to 25 ms, repetition from 0.5 to 15 pulses per second in AUTO, or one pulse per press of FIRE.
One thing I didn't appreciate at first: the pulse isn't just "ramp on, ramp off". The ramp starts on the wick level two milliseconds before the interrupter goes high, so the LC filter of the buck settles. The rise begins a millisecond after the enable. And after the ramp ends, the interrupter stays high for another two milliseconds so the coil itself drains the buck output capacitor. I learned the hard way what happens when it doesn't: the next pulse switches the bridge straight onto a charged capacitor. Those three times are constants at the top of the file, and every time I touch something on the buck I check the capacitor voltage before the next pulse with a scope.
Knobs and the quiet zone
The pots go straight to the ADC pins, no RC filters, the 3.3 V and ground rails are just daisy-chained from pot to pot. Each knob is read with 16 samples and a trimmed mean, and then there's a hysteresis sized to half of one output step. Without that, one count of ADC jitter recomputed the preview, pushed four frames to the display and made the shot counter blink for no reason. Polling and all the display traffic happen only between pulses. In AUTO at high BPS that window gets short, so the link sends up to four frames per pass but re-checks the window before every one of them, so it can never run into a pulse.A small but nasty bug from the Nano days that I fixed here: the first poll after power-up is now always accepted. Before, a knob sitting at its end stop kept its default value until you touched it - so the amplitude knob turned all the way down still meant 50 % on the first shot. Not a nice surprise.
Safety
The E-STOP is a mushroom switch with two contact groups. Group A (normally open) pulls GPIO23 to ground, and that pin is also wired into the MCPWM fault input. The peripheral forces both outputs low by itself - no interrupt, no code involved. Group B is a normally closed contact in series with the interrupter driver input, so the bridge enable is broken physically as well.On top of that there's a task watchdog, a pulse watchdog that checks the ramp interrupt is alive, a check of the CPU frequency at start (the busy waits count cycles, so a wrong clock would stretch everything by 1.5), a boot self-test of the fault path, and a check that not all knobs read the end stop, which is what a broken panel ground looks like. If any of that fails, the box refuses to arm and tells you why on the serial port. And if a fault fires inside a pulse, it latches SAFE and reminds you that the buck capacitor may still be charged.
The display
The display is one of those cheap ESP32-2432S028R boards ("Cheap Yellow Display"). It's an I2C slave, the generator is the master at 250 kHz. There's a 6-byte STATUS every 100 ms as a heartbeat, a 22-byte META with all the parameters the curve was computed from, and three chunks with 80 curve points. META and chunks carry a 3-bit generation number, because at some point a META lost to a CRC error mixed a new curve with an old caption and I spent an evening chasing a "bug" in the ramp math.The display redraws each screen area only when its content hash changed, so nothing flickers, and it re-initialises the LCD controller every 20 minutes because sitting next to a Tesla coil the SPI bus catches enough garbage to leave the picture shifted or inverted. There's also a little voltmeter reading the 5 V rail through a divider, which turns yellow and red when the battery starts to sag. The protocol is written up in both sketches, so if you want a different display, it's an evening of work.
The board
85 x 72 mm, single-sided, copper only on the bottom, 0.8 mm tracks, 0.6 mm clearance, a ground pour with thermal reliefs, holes 1.0 to 1.6 mm, so it etches and drills fine at home. Everything is through-hole on top except five 1206 capacitors that sit on the copper side. Where tracks have to cross there are 30 short insulated jumper wires through 29 holes; each hole is labelled W1...W11 and both ends of one wire carry the same label, so you can't get them wrong. Every connector pin has its function printed next to it, and the README has the full wiring: which pot goes to which pin, the display cable, the transmitters, the E-STOP groups, the grounds.Before you build one
All the numbers that depend on your hardware sit in one BUILD CONFIGURATION block at the top of the generator sketch: bus voltage, carrier frequency, ramp ceiling (100 % duty is only OK with an isolated gate supply - with a bootstrap you want about 60 %), the alpha correction, wick voltage and margins, pulse width and BPS limits, the buck stage timings, the knob end stops, the link speed. Everything else is derived from those and checked by static_asserts.The frequency table and the timing figures in the comments are from my parts, don't trust them for yours. A higher carrier does make the ramp smoother, but every extra kilohertz is switching loss in the brick, gate charge the DC-DC has to deliver, and heat in the driver; 25 kHz was the sweet spot for me.
Check three things with a scope on any new build:
the wick step at the buck output;
the capacitor voltage before the next pulse;
the knob end-stop counts in the serial log.



