EFHW

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.

By BG1SB  ·   ·  ~8 min read

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.

ComponentDetail
MCUESP32-S3-WROOM-1, LX7 dual-core 240 MHz, 512 KB SRAM, 16 MB flash
ServoMG996R, 6 V, 10 kg·cm, 0.17 s/60°
Servo PWMLEDC on GPIO1, 50 Hz, 500–2500 µs ↔ 0–180°
Servo powerGPIO2 → 2N2222A → IRF9540 P-MOSFET switches the 6 V rail
Bias-T monitorGPIO5, ADC1_CH4, 12-bit, 47 k+10 k divider (5.7:1)
Status LED / buzzerGPIO6 / 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:

ParameterValue
ToroidT200-6 (µ=8), OD 50.8 mm, ID 31.8 mm, h 14 mm, AL ≈ 10.5 nH/N²
Turns2 T primary : 14 T secondary
Impedance ratio(14/2)² = 49:1 → 50 Ω in, ~2450 Ω out
CapacitorTransmitter-grade air variable, 10–500 pF, ≥5 kV working (plate gap ≥1.5 mm)
Servo couplingMG996R through a 3:1–6:1 gear reduction
Tuning rangef_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:

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):

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.