A working prototype of a charge-balanced, transformer-isolated taVNS stimulator built around an ESP32-S3, six discrete MOSFETs and a small audio transformer. Everything is here: schematic, as-built wiring map, firmware, a browser-based control panel, the bench bring-up procedure, and an honest engineering log of what broke and why.
Warning
This is not a medical device. It puts current through the skin of a human ear. It has no regulatory approval, no clinical validation, and no redundant protection. Read SAFETY.md before you build or power anything — especially the mains-isolation and charge-balance rules, which are the two that actually keep you safe. Anyone who builds this accepts full responsibility for the result.
Transcutaneous auricular vagus nerve stimulation (taVNS) delivers small biphasic current pulses to the cymba conchae of the outer ear, which is innervated by the auricular branch of the vagus nerve. The usual research parameters are 25 Hz, 100–500 µs pulses, current titrated to a clear-but-comfortable tingle, 30 minutes per session.
This device produces those pulses. It does not claim to treat anything — see docs/stimulation-protocol.md for the parameter rationale and a deliberately unenthusiastic summary of the evidence.
An H-bridge switches the low-voltage primary of a 1:4 audio transformer; the ear hangs off the floating secondary. That single decision solves most of the hard problems at once: all high-voltage nodes sit on a winding with no switches on it, so gate drive stays trivial (Vgs never exceeds the rail); the transformer cannot pass DC, so charge balance is structural rather than a firmware promise; and the same transformer is the galvanic barrier between the ear and everything else. A series capacitor on the secondary is the second, independent DC block, and a 10 kΩ series resistor caps the ear current at roughly 4.8 mA even into a dead short.
power bank 5 V ─┬─► ESP32-S3 (timing, UI, telemetry)
└─► XL6019 boost ─► Vprim 12–18 V
│
GPIO4..7 ──────► H-bridge Q1..Q4 (+ Q5/Q6 level shifters)
│ OUTA / OUTB
▼
T1 primary ≈600 Ω
─────║───── ← galvanic isolation barrier
T1 secondary ≈10 kΩ
│
R_lim 10k ─► Cb 4.7 µF ─► ear electrode A
~ ear tissue ~
───────────── Rb 1 MΩ ──► ear electrode B
The firmware runs the bridge as a phase machine on esp_timer: a positive phase where one
high-side switch is PWM-chopped and the opposite low-side switch is held on, a dead time,
the mirror-image negative phase, another dead time, then idle until the period is up.
Amplitude is the PWM duty of the chopped switch. Only one switch is ever chopped at a time
and its low-side neighbour is off, so PWM-off current freewheels through a body diode and
shoot-through inside a phase is structurally impossible. Details and the measured proof are
in docs/firmware.md.
Working prototype. Built, powered, measured, and used on a human ear.
| Area | State |
|---|---|
| Power stage (H-bridge on perfboard) | Built, continuity-checked, run under 12–18 V, clean |
| Bridge logic | Verified with a logic analyser: 0 µs shoot-through, diagonals match |
| Isolated output stage | Built; drives a dummy load and a real ear |
| Firmware v2.2 (ESP32-S3) | Flashed and in use |
| Firmware v2.3 (optional GC9A01 display) | Compiles, not yet flashed |
| ESP32-C3 port | Compiles, never run on hardware |
| Control panel (browser) | Working |
| Current sense (ADS1115) | Not connected — the umbilical carries no I²C. Charge figures are arithmetic, not measurement |
| Electrode attachment | Ear clips with ECG gel; the weakest part of the whole system |
Working point in actual use: 100 % amplitude, 500 µs, 25 Hz, Vprim 18 V.
Known limitation — the 250 µs target is not reachable yet. The nerve responds to charge
(current × pulse width), so halving the width means doubling the current. The firmware's
width command reports the exact shortfall from the device itself: ×2.0 for 250 µs and
×3.3 for 150 µs. That is arithmetic about the output impedance, not a fault. The single
unmeasured quantity in that chain is the ear/electrode impedance; measuring it is the next
step, and the fix is likely better electrode contact rather than a circuit change. See
docs/troubleshooting.md.
| Path | What's in it |
|---|---|
| SAFETY.md | Read first. Engineering rules, contraindications, the isolation test |
| hardware/schematic.svg | As-built schematic (regenerate with tools/gen_schematic.py) |
| hardware/bom.md | Bill of materials with substitution notes |
| hardware/photos/ | Build photos |
| docs/circuit.md | Why this topology, every component value, gate drive analysis |
| docs/board-map.md | As-built perfboard map: designator ↔ hole ↔ measured pin role |
| docs/build.md | Build order and the staged bring-up protocol |
| docs/firmware.md | Firmware architecture, serial command reference, flashing |
| docs/control-panel.md | Browser control panel: power knobs, threshold search, standalone mode |
| docs/troubleshooting.md | Symptom → measurement → diagnosis, with reference numbers |
| docs/stimulation-protocol.md | Stimulation parameters and what the evidence does and does not support |
| docs/engineering-log.md | What actually broke, in order, and what each failure taught |
| firmware/ | taVNS_fw (ESP32-S3, in use) and taVNS_fw_C3 (port, untested) |
| panel/ | panel.py — dependency-light control panel served in a browser |
| tools/ | Schematic generator |
Roughly a weekend of work if you can solder and own a multimeter.
- Read SAFETY.md. The isolation and charge-balance rules are not optional.
- Gather parts — hardware/bom.md. Nothing exotic: an ESP32-S3 board, an XL6019 boost module, 6 small-signal MOSFETs, a 600 Ω:10 kΩ audio transformer, a handful of resistors, two film capacitors, ear clip electrodes and conductive gel.
- Build the power stage following docs/build.md and docs/board-map.md. Determine every MOSFET pin role by diode-testing the part on the board; SOT-23 adapter pinouts vary and assuming the order has already destroyed one build here.
- Flash the firmware — docs/firmware.md. Arduino-ESP32 core ≥ 3.0.
- Bring it up on the bench, into a 4.7–10 kΩ dummy load, never into an ear. Confirm bridge timing, then confirm net DC ≈ 0 across the output with a multimeter.
- Confirm T1 primary↔secondary isolation reads open (megohms). This is the barrier the whole safety argument rests on.
- Only then: electrodes on your own ear, amplitude from zero, upward slowly.
# control panel (creates its own venv on first run, needs only pyserial)
cd panel && ./run.sh # then open the URL it prints- Put the step-up transformer between the switches and the patient, not a high-voltage H-bridge in front of them. A discrete HV bridge needs floating gate drive and risks Vgs breakdown; this topology needs neither.
- Make dead time a function of the PWM carrier, not a constant. The LEDC peripheral only
applies
duty = 0at a carrier-period boundary, so a high-side switch can stay on for up to one full carrier period after being told to stop. Dead time is computed ascarrier period + 15 µsand cannot be set lower — an operator cannot create shoot-through by changing the carrier frequency. - Check driver return codes. A silent
ledcAttachfailure at 40 kHz once left the bridge outputting nothing while the UI showed a perfectly healthy amplitude. Cost: one night. - One ground star, and treat it as a mechanical problem. The single wire from the boost converter's OUT− to the star came loose twice in two days; both times the symptom was "the processor is dead". It is now a screw terminal, not solder.
Archived on Zenodo under a concept DOI that always resolves to the newest version: 10.5281/zenodo.21628511. Machine-readable metadata lives in CITATION.cff.
Boliasov, N. (2026). taVNS-ESP32: a charge-balanced, transformer-isolated DIY transcutaneous auricular vagus nerve stimulator. Zenodo. https://doi.org/10.5281/zenodo.21628511
Code (firmware, panel, tools) — MIT, see LICENSE. Documentation, schematic and photos — CC BY-SA 4.0.