Designing an ESP32-S3 Servo-Driven Antenna Tuner: The Fuchs ATU V3.0
One servo, one air variable capacitor, and a 430-CNY bill of materials. The interesting part is where the SWR sensor does not live — and what that decision buys you in the field.
An End-Fed Half-Wave antenna is wonderfully simple to hang and wonderfully annoying to tune. Move the frequency across a band and the feed-point impedance wanders far from 50 Ω; dial across three bands and you'll be turning a knurled knob and re-reading an SWR meter for a while. The EFHW Fuchs ATU V3.0 is an automatic tuner built to end that: an ESP32-S3 turns a single servo-driven air variable capacitor until the SWR comes down — with the SWR bridge living entirely off the tuner.
This article walks through the hardware, the matching network, and the tuning algorithm that makes a ~430 CNY device cover 40 m through 10 m including every WARC band.
Hardware platform
The core is an ESP32-S3-WROOM-1 — dual-core Xtensa LX7 at 240 MHz, 512 KB SRAM, 16 MB flash — running ESP-IDF v5.x native C with FreeRTOS. No Arduino layer, no Linux: just a real-time kernel and three tasks doing precisely defined jobs.
| Component | Detail |
|---|---|
| MCU | ESP32-S3-WROOM-1, LX7 dual-core 240 MHz, 512 KB SRAM, 16 MB flash |
| Servo | MG996R, 6 V, 10 kg·cm, 0.17 s/60° |
| Servo PWM | LEDC on GPIO1, 50 Hz, 500–2500 µs ↔ 0–180° |
| Servo power | GPIO2 → 2N2222A → IRF9540 P-MOSFET switches the 6 V rail |
| Bias-T monitor | GPIO5, ADC1_CH4, 12-bit, 47 k+10 k divider (5.7:1) |
| Status LED / buzzer | GPIO6 / GPIO7 |
The power chain is worth reading top to bottom, because it solves a classic remote-antenna problem: power over the coax. A bias-T injects 13.8 V DC onto the feed line; the tuner splits it: LM2940CT-12 to a 12 V rail, a DC-DC stepdown for the 6 V servo rail, and an AMS1117-3.3 for the ESP32. One coax run, no second battery, no solar panel — the tuner is a weatherproof box with nothing but the antenna and the coax attached.
Servo power is gated by a MOSFET because a powered but idle servo is an unnecessary 300 mA drain at a remote site. After a tune completes, the firmware cuts the 6 V rail entirely; the capacitor holds its position mechanically, so the antenna stays matched with zero quiescent servo current.
The matching network
The tuner uses a single T200-6 carbonyl-iron toroid as an impedance transformer plus a servo-turned air variable capacitor:
| Parameter | Value |
|---|---|
| Toroid | T200-6 (µ=8), OD 50.8 mm, ID 31.8 mm, h 14 mm, AL ≈ 10.5 nH/N² |
| Turns | 2 T primary : 14 T secondary |
| Impedance ratio | (14/2)² = 49:1 → 50 Ω in, ~2450 Ω out |
| Capacitor | Transmitter-grade air variable, 10–500 pF, ≥5 kV working (plate gap ≥1.5 mm) |
| Servo coupling | MG996R through a 3:1–6:1 gear reduction |
| Tuning range | f_max 35.1 MHz @ 10 pF → f_min 4.96 MHz @ 500 pF |
Two engineering details here matter. First, the air variable capacitor is the only tuned component — no switched inductor bank, no relay clacking. That's what keeps the BOM at 430 CNY and the failure modes down to "one moving part." Second, the transformer is massively under-run: peak flux at 100 W on 7.1 MHz is about 12.7 mT against a 600 mT saturation limit — a 47× margin, so the core never heats meaningfully even on a contest afternoon.
Where the SWR sensing actually lives
Here's the decision that makes this design interesting: the tuner has no on-board SWR bridge. No tandem match detector, no BAT41 diode pair, no FT37-43 toroid, no calibration pot. Instead, SWR is measured by a completely separate instrument and delivered over a WebSocket:
ATR-1000 → MRRC server → WebSocket (JSON) → ESP32-S3 → tune_engine_feed_swr()
This is an explicit architecture decision (AD-003 in the SDD): removing the bridge from the tuner's bill of materials eliminates a whole class of calibration and component-tolerance problems from the device that lives up a pole. The cost is honest and important: the tuner cannot tune when the network is down. For a permanently installed station whose MRRC server is always on, that's a fine trade; for a portable field setup it's a constraint you should know about.
The tuning algorithm
The tuning engine (tune_engine.c) is a four-phase state machine:
Phase 1 — cache lookup (<1 s)
Every completed tune writes the winning capacitor position to NVS flash, keyed by frequency ("f" + freq_khz, e.g. f7040 for 7.040 MHz). A fuzzy lookup accepts a stored position within ±50 kHz of the target. Hit rate is high because 40 m–10 m at 50 kHz steps fits in ~2000 entries across a 24 KB partition. On a hit, the servo drives straight to the cached angle, reports tune_done, and powers off — the whole thing completes in under a second.
Phase 2 — coarse sweep
Cache miss? The servo sweeps 0–180° in 5° steps (36 steps). At each step the firmware reports tune_progress, then waits for the WebSocket to deliver an swr_update with the ATR-1000's measurement. It tracks best_swr and best_pos throughout, and exits the loop early the moment SWR drops below 1.05:1.
Phase 3 — fine sweep (only if needed)
If the best coarse SWR is still above 1.5:1, the engine scans ±15° around the best position in 1° steps (30 steps) using the same measure-and-track loop.
Phase 4 — lock and persist
The servo moves to the best position, the result is written to NVS only if the final SWR is below 2.0:1, the servo MOSFET is switched off, and tune_done {cap_pct, swr_final, elapsed_ms} goes out over the WebSocket.
Timing: each step costs about 80 ms of servo travel plus ~100 ms of SWR network round-trip — roughly 180 ms per step. A full coarse sweep is ~6.5 s; a worst-case coarse-plus-fine sweep is ~12 s (the SDD targets under 10 s). For a band change from the couch, that's plenty fast.
Safety and fault detection
Transmitting into a badly-tuned matching network can damage a finals stage, so the engine treats every SWR sample as a safety gate. Every feed_swr() call checks forward power and aborts with a specific error code:
- Forward power > 15 W → abort
TUNE_ERR_OVERPOWER - Forward power < 0.5 W → abort
TUNE_ERR_NORF(no RF reaching the tuner) - No match found after sweep (SWR ≥ 2.0) →
no_match
Firmware also monitors bias-T voltage (10–15 V window), ESP32 die temperature (>80 °C alerts), WiFi (auto-reconnect every 10 s), WebSocket (reconnect every 3 s), NVS corruption (auto-erase and rebuild), and a 5-second task watchdog. Equally important is what it cannot detect with no extra sensors: servo mechanical stall, a stripped gear or coupler slip, a failed MOSFET, capacitor arc-over, core overheating, or water ingress. The field notes below are where those lessons belong.
FreeRTOS architecture
Three tasks, three priorities, three stacks:
ws_client_task pri 3 · 8192 B stack WiFi STA + WebSocket client + JSON dispatch
tune_engine_task pri 2 · 4096 B stack tuning state machine
health_mon_task pri 1 · 3072 B stack bias-V ADC, die temp, health FSM
The two directions of communication are deliberately asymmetric. Inbound, the WebSocket task calls tune_engine_feed_swr() directly — a synchronous function call, no queueing, because SWR samples must never be dropped. Outbound, the engine calls a ws_client_send() callback that pushes into a FreeRTOS Queue (depth 16, 512-byte messages), so the tuner never blocks on the network.
The JSON protocol
Five inbound commands and five outbound events (cJSON, protocol.c):
- Commands:
tune_start {freq_hz, swr, fwd_pwr_w}·swr_update {swr, fwd_pwr_w}·tune_abort·set_bypass·get_status - Events:
tune_progress {cap_pct, servo_pos, state}·tune_done {cap_pct, swr_final, elapsed_ms}·tune_error {code, message}·status_report {pos, cache_hits, health, uptime}·health_alert {code, value, message}
Because the SWR source is remote, the MRRC server drives the whole session: it reads the ATR-1000, decides when to call tune_start, and streams swr_update at whatever cadence the tuner asks for. The tuner stays a dumb, reliable actuator — which is exactly what you want at the far end of a WebSocket across a field.
For the deeper engineering write-up — the fault-detection boundary, the bench test matrix, and the full SDD — see the project's documentation. The EFHW product page covers the buying and building story, and the deep-dive research lives under MRRC's EFHW research section.