ESP32-C5 Analog 5.8 GHz FPV Receiver
From live RF to real-time analog NTSC composite video with one Seeed Studio XIAO ESP32-C5 and a passive resistor DAC.
Important
Development notice
I'm taking a temporary step back from active C5VRX development. The project has grown into a lot of work and takes a significant amount of time. I recently graduated, and I'm currently spending a lot of time applying for jobs and focusing on that next step, so I simply don't have much time to keep developing C5VRX at the same pace.
C5VRX is not abandoned. Development will just be slower for a while. Contributions, testing, ideas, and discussion are still very welcome. Thanks for all the support and understanding!
— Twotoz
The current experimental range work measures semantic CVBS sync and the exact-adjacent winding loss hidden by the 50 ns endpoint discriminator. See docs/range-demod-quality-v2.md for the measurement model and hardware validation rules.
The production receive profile now uses ARC: it reconstructs the valid
ESP32-C5 vendor gain table at boot, starts at the highest RF stage with bounded
downstream gain, fits BB/fine gain to raw Q4 evidence during acquisition, and
performs zero PHY writes while clean video is locked. See
docs/arc-receive-chain.md for the recovered PHY ABI and state model.
Pre-Q4 receiver characterization is documented in docs/pre-q4-lab.md. The
lab can isolate TX/DAC self-noise, sweep the complete highest RF-stage portion
of the generated vendor gain table, and request a fresh vendor PHY calibration
on the next boot without promoting undocumented RXDC/IQ/filter writers into
production.
Hardware walk tests now show that useful generated gain spans almost the full
vendor table: roughly G14-G18 at extreme close range, G35-G56 through
close/medium conditions, and G77-G81 at the weakest tested range. The
gain-first ARC V3 EXP profile uses raw-Q4 occupancy/coherence, temporal
median filtering and asymmetric hysteresis to follow that changing operating
region without the old G62 starvation trap. In the latest close -> far -> close
test, the gain trajectory moved from about G16 to G81 and back toward G39, and
a location that previously represented the practical far limit produced good
video. These are empirical calibration anchors, not calibrated dB values; the
next step is a repeatable Q4-state -> gain search table, ideally validated with
known RF attenuation.
The current Range v2 work is documented in:
docs/range-v2.md— implementation and validation overview;docs/range-v2-knowledge.md— preserved control/demod engineering knowledge;docs/range-v2-research-notes.md— RF research hypotheses and hardware test plan.docs/trajectory-v2.md— two-bundle adjacent-trajectory demod, confidence model and PLL-lite validation plan.
Flash your Seeed Studio XIAO ESP32-C5 directly from your browser (Google Chrome, Microsoft Edge, Brave, Opera) with zero installation required:
The production web flasher is hosted entirely by GitHub Pages at twotoz.github.io/C5VRX. There is no VPS, application server, or separate production web host in the current deployment.
- Automatic Latest Firmware: Automatically selects the newest immutable semantic-version release.
- One-Click Flashing: Flashes the universal merged production image (
bootloader + partitions + appat0x0) over Web Serial. Firmware is mirrored into the GitHub Pages artifact and fetched same-origin, so browser CORS redirects are not part of the production flash path. - Selectable Releases: The Releases tab contains only semantic-version releases such as
v3.0.0,v3.0.1, and newer. - Separate PR Builds Tab: Same-repository pull requests publish a temporary
pr-<number>GitHub prerelease. Experimental builds appear only under PR Builds, never in the normal Releases list, and require an explicit warning confirmation before flashing. - Offline / Local Execution: You can also run the web flasher locally:
(Starts a local HTTP server at
python tools/open_webflasher.py
http://localhost:8080/web/index.htmland opens your browser.)
Every push or merged PR to main builds the production firmware and publishes a new immutable GitHub release with a semantic version tag.
BREAKING CHANGEor a conventional-commit!creates a major bump.feat:/feat(scope):creates a minor bump.fix:,chore:,docs:, and other changes create a patch bump.- If no stable release exists yet, the current
v3.0.0-rc1line is promoted tov3.0.0. - The resolved version is written to
version.txtbefore the ESP-IDF build, so the firmware metadata and GitHub release use the same version. - Release assets remain attached to their immutable version tag; the old mutable
mainrelease/tag is retired automatically after the first versioned release succeeds.
Firmware releases and website deployment are intentionally separate:
.github/workflows/build.ymlvalidates and builds firmware, then publishes a versioned release after a successful push tomain. For same-repository PRs it also maintains a clearly marked temporary prerelease (pr-<number>) and removes it when the PR closes..github/workflows/deploy-web.ymlis the only production website deployment. It always checks out trustedmain, then packages the web flasher plus a generated firmware mirror into one GitHub Pages artifact.tools/prepare_pages_site.shmirrors the newest semantic firmware releases and all activepr-<number>prereleases underfirmware/<tag>/, and generatesfirmware/releases.json.- The browser reads that Pages manifest and downloads binaries from the same
twotoz.github.ioorigin. This avoids GitHub Release storage CORS redirects entirely. - Successful Production CI triggers a Pages mirror refresh, so newly published/updated PR builds and releases become available without deploying unmerged PR web code.
- When a PR closes or merges, CI deletes its temporary prerelease/tag; the next successful cleanup-triggered Pages refresh removes it from the mirror.
Questions, build photos, feedback, or development discussion? Join the C5VRX Discord.
C5VRX-3 turns the Seeed Studio XIAO ESP32-C5 (ESP32-C5 RISC-V SoC) into a standalone 5.8 GHz analog video (FPV) receiver.
It captures raw Wi-Fi PHY I/Q samples directly from the 5 GHz RF front-end at 40 MS/s, demodulates Wideband FM (WBFM) in real-time hardware using the ESP32-C5 BitScrambler, and outputs analog NTSC composite video (CVBS) via PARLIO TX and a 6-bit passive resistor DAC ladder into standard 75-ohm FPV goggles or monitors.
5.8 GHz Analog FPV (48 Channels)
│
▼
ESP32-C5 RF / MODEM_DIAG Bus (40 MS/s Q4/I4)
│
▼
PARLIO RX @ 40 MS/s (POS sample edge, pure continuous hardware GDMA)
│
▼
Circular GDMA Ring (32 KiB in HP SRAM, Zero-EOF patched)
│
▼
Phase5 BitScrambler Demodulator (fm.bsasm: 50 ns discriminator, embedded LUT)
│
▼
PARLIO TX @ 40 MHz ([D,D] mode -> 20 MS/s unique CVBS output)
│
▼
6-bit Resistor DAC Ladder + 470 pF Filter -> 75-ohm Goggles
After startup, the CPU does not process pixels; the entire pipeline runs continuously in dedicated silicon peripherals (AHB GDMA
- The Problem: In continuous loop mode, the stock ESP-IDF PARLIO TX driver injected a GDMA EOF (
suc_eof = 1) on every cyclic ring wrap (confirmed in espressif/esp-idf#19091). This triggered periodic hardware stalls, causing a 1-second vertical sync drop and jagged horizontal line jitter ("kartels"). - The Solution: C5VRX-3 patches
dw0.suc_eof = 0across the descriptor ring in SRAM after driver initialization, paired with 64-byte aligned cache synchronization (sync_dma_c2m). - The Result: Truly gapless, infinite circular streaming with zero wrap bubbles, rock-solid vertical sync lock, and crystal-clear horizontal alignment.
- Eliminates both the erratic hunting of stock packet AGC and the "noise trap" of blind power measurement (where background thermal noise keeps measured power elevated even in deep fades).
- Computes real-time integer FM phase coherence:
$$Q_{\text{phase}} = \frac{\text{count}(P \ge 8 \land \text{Dot} > 0 \land |\text{Cross}| \le \text{Dot})}{255} \times 100%$$ - ARC Gain Adaptation: ARC separates the vendor RF stage from downstream BB/fine gain. Persistent loss selects the first entry of the highest RF stage; acquisition then fits Q4 utilization one valid vendor index at a time.
-
Fast Overload Safety Rem: Instant gain cut (
$\Delta G = -4 / -6$ ) if clipping occurs ($N_{\text{clip}} \ge 4$ and$P_{\text{median}} > 18$ ). -
Deadband Lock: Zero register writes when locked in the clean target zone (
$Q_{\text{phase}} \ge 70%, P_{\text{median}} \in [18, 30]$ ).
- BW40 is the production RF contract: C5VRX keeps the wide analog front-end selected with
phy_wifi_fbw_sel(1)during startup and after every channel retune. - No runtime BW20 gearbox: Earlier hardware testing showed the narrower setting rolls off part of the analog-FM video spectrum, reducing detail and causing chroma instability. The later theoretical "+3 dB survival" gearbox was therefore removed.
- No bandwidth-switch transient in flight: Weak-signal recovery is handled by the adaptive gain controller and demodulator/noise handling while RF bandwidth remains fixed.
- The
fm.bsasmBitScrambler program implements soft-noise squelching: phase deltas around$\pm 180^\circ$ (deltas$-16 \dots -12$ and$+13 \dots +15$ ) are mapped to blanking pedestal (DAC code 20) instead of sync tip (DAC code 0). - Eliminates false horizontal sync pulses and screen tearing during noise bursts and static.
Connect a 6-bit binary-weighted resistor DAC ladder to the XIAO pins, meeting at the VIDEO node:
| XIAO Pin | ESP32-C5 GPIO | Bit Weight | Series Resistor |
|---|---|---|---|
| D4 | GPIO 23 | Bit 0 (LSB) | 8.2 kΩ |
| D5 | GPIO 24 | Bit 1 | 3.9 kΩ |
| D6 | GPIO 11 | Bit 2 | 2.0 kΩ |
| D7 | GPIO 12 | Bit 3 | 1.0 kΩ |
| D8 | GPIO 8 | Bit 4 | 470 Ω |
| D9 | GPIO 9 | Bit 5 (MSB) | 240 Ω |
| GND | GND | Ground | Ground reference |
- Shunt Termination: 200 Ω resistor from
VIDEOtoGND. When connected to goggles with standard 75 Ω termination, this forms a matched 0–1.0 V standard CVBS level. - De-Emphasis Filter: A 470 pF ceramic capacitor placed in parallel across
VIDEOandGNDcreates a 10–14 dB high-frequency de-emphasis low-pass filter, dramatically reducing triangular FM noise and snow. - BOOT Button: The built-in BOOT button (GPIO 28) switches channels on short click. A long press opens the standalone PAL/NTSC menu; short presses move between pages and a long press applies the selected action. On the CHANNEL page, a long press scans all 48 channels and selects the strongest coherent carrier. If a persisted Safe Flight setting blocks normal menu entry, hold BOOT for three seconds to restore GOLDEN/6BIT@40/ARC and open the recovery menu.
- Persistent settings: Channel, RF bandwidth mode, AFC/output/video-standard modes, AGC/manual gain and the BOOT-menu preference are stored in NVS and restored after restart.
Connecting to the USB serial console (115200 baud) provides live telemetry and single-key controls:
| Key | Action |
|---|---|
c / C |
Cycle FPV channel / band (48 standard channels: RaceBand, Boscam A/B/E, FatShark, LowBand) |
+ / - |
Manual RF gain step (±2 index) |
a / s / m |
Switch AGC mode: Active (auto-adapting) / Shadow (dry-run) / Manual (fixed) |
f |
Cycle AFC Mode: Auto Centering (±1.5 MHz) / Hold / Off (0 kHz) |
, / . |
Fine-tune carrier frequency offset in ±50 kHz steps |
0 |
Reset frequency offset to 0 kHz |
e |
Toggle RX sample clock edge (POS / NEG) |
d |
Print real-time reception diagnostics summary |
- Option A (Docker - Recommended): Docker Desktop or Docker engine installed.
- Option B (Native ESP-IDF): ESP-IDF v6.0.x installed with Python 3.10+.
No ESP-IDF installation required on your host machine. Run from the repository root:
Linux / macOS / Git Bash:
docker run --rm -v "${PWD}:/workspace" -w /workspace espressif/idf:v6.0.2 idf.py buildWindows PowerShell:
docker run --rm -v "${PWD}:/workspace" -w /workspace espressif/idf:v6.0.2 idf.py buildIf you have ESP-IDF installed locally:
Linux / macOS:
. $IDF_PATH/export.sh
idf.py buildWindows (ESP-IDF PowerShell Environment):
export.ps1
idf.py buildThe build produces three critical binaries in build/:
build/bootloader/bootloader.bin(at flash offset0x2000)build/partition_table/partition-table.bin(at flash offset0x8000)build/c5vrx3.bin(at flash offset0x10000)
Before flashing, run the built-in validator to ensure zero DMA/BitScrambler constraint violations:
python tools/validate_build.py(All architectural checks must pass.)
Launch the web flasher directly in your browser (Google Chrome, Microsoft Edge, Brave): Open C5VRX Web Flasher on GitHub Pages
Run the auto-flash watcher:
python tools/auto_flash.pyPlug in or reset your Seeed Studio XIAO ESP32-C5 into download mode (hold BOOT while tapping RESET), and the watcher will detect the COM port, flash the firmware, and automatically trigger a watchdog reset into the application!
Specify your COM port (or omit to auto-detect):
python tools/flash.py COM10idf.py -p COM10 flashLaunch the dedicated low-latency serial monitor:
python tools/monitor.py COM10Use the interactive hotkeys (c to cycle channels, +/- for manual gain, a for active AGC, d for hardware diagnostics).
├── CMakeLists.txt # Production top-level ESP-IDF project
├── sdkconfig.defaults # Production build configuration (ESP32-C5 @ 240MHz)
├── partitions.csv # Custom minimal partition table
├── main/ # Standalone C5VRX-3 production firmware
│ ├── CMakeLists.txt # Component manifest & BitScrambler registration
│ ├── main.c # Application entry point
│ ├── rf.c / rf.h # Wi-Fi PHY RX-only frontend & frequency tuning
│ ├── video.c / video.h # Realtime PARLIO RX/TX, Zero-EOF GDMA & AGC engine
│ ├── fm.bsasm # Phase5 BitScrambler demodulator program
│ └── osd_font.h # 8x8 font tables for OSD
├── web/ # Zero-install Betaflight-style Web Flasher (Web Serial API)
│ ├── index.html # Flasher dashboard UI
│ ├── style.css # Dark slate theme with blue accents & white topbar
│ ├── app.js # Web Serial flasher & release management logic
│ └── esptool.js # Vendored esptool-js client with ESP32-C5 support
├── tools/ # Production validation & flashing utilities
│ ├── open_webflasher.py # Local offline Web Flasher launcher & HTTP server
│ ├── validate_build.py # Architectural constraint validator
│ ├── auto_flash.py # Auto-detecting flashing watcher
│ ├── flash.py # One-click direct flasher
│ ├── monitor.py # Low-latency interactive serial console
│ └── live_logger.py # Real-time CSV telemetry logger
├── docs/ # Architectural specs & mathematical proofs
└── legacy/
├── c5vrx1/ # Original proof-of-concept repository snapshot
└── c5vrx2/ # Complete historical C5VRX-2 firmware, research & tools
C5VRX is open-source software licensed under the GNU General Public License v3.0 only (GPL-3.0-only).
See LICENSE for full licensing terms.