Skip to content

mattdelashaw/rtlsdr-next

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

126 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

rtlsdr-next πŸ“‘

Descriptive alt text for the image

Rust CI

A high-performance, asynchronous, and safety-first Rust driver for RTL2832U-based Software Defined Radios (SDR).

Designed for the modern era (2026+), this driver moves away from the legacy C callback model toward a Tokio-native Stream architecture, with specific optimizations for high-bandwidth ARM hosts like the Raspberry Pi 5.

Caution

This a shameless "vibe-code" project. I wanted to dig into Rust and this seemed like something fun to explore when I realized the gold version C code drivers were circa 2013. Since starting, I realized there is a Rust implementation that has a more human touch here: https://github.com/ccostes/rtl-sdr-rs

βœ… Confirmed Working Hardware

Dongle Tuner Host Clients Tested
RTL-SDR Blog V4 R828D Raspberry Pi 5 (Bookworm) OpenWebRX+, GQRX, SDR++
RTL-SDR Blog V4 R828D Windows 11 x86_64 (AMD Ryzen 7600X) Corona SDR (iOS)
RTL-SDR Blog V4 R828D Raspberry Pi 5 (Bookworm) SpectralBands (iOS, WebSDR protocol)

Other RTL2832U dongles with R820T/R820T2/E4000 tuners should work β€” hardware verification welcome.

πŸš€ Key Features

  • Async-First Architecture: Built on Tokio. SDR data is a standard Stream with backpressure and graceful shutdown.
  • Full RTL-SDR Blog V4 Support: Correct R828D initialization sequence reverse-engineered from usbmon traces against librtlsdr.
  • Zero-Allocation Pipeline: Custom buffer pooling eliminates memory allocations in the hot path.
  • Auto-Vectorized DSP: LLVM auto-vectorizes to NEON on aarch64 and AVX-512 on Zen 4 x86_64 β€” no manual intrinsics needed.
  • Automatic Tuner Probing: I2C presence detection for Rafael Micro (R820T/R828D), Elonics (E4000), and Fitipower (FC0012/13).
  • Multi-Client Daemon: A single rtlsdr-daemon process serves rtl_tcp, WebSDR, and Unix socket clients simultaneously from one hardware handle. No resource contention.
  • Independent Virtual VFOs: Each WebSDR browser client can tune independently within the hardware's 1.536 MHz capture window using a per-client NCO β€” no hardware retune needed for in-band frequency changes.
  • TOML Configuration: File-based config with CLI override support. Set gain, frequency, PPM, TLS, and server addresses once in /etc/rtlsdr-next/config.toml.
  • Precision Frequency Correction: Integrated PPM correction for both tuner PLL and RTL2832U resampler.
  • Standalone Tools: Individual rtl_tcp and websdr binaries remain available for single-protocol deployments.

πŸ›  Hardware: The V4 Deep Dive

The RTL-SDR Blog V4 required several initialization steps discovered during reverse engineering via usbmon traces:

  1. GPIO Reset Pulse (non-V4 only): Standard RTL-SDR dongles receive a GPIO 4 reset pulse during init. The V4 skips this entirely β€” librtlsdr detects the V4 by EEPROM string match and branches before the GPIO code. This driver mirrors that behavior exactly.

  2. R828D I2C Address: The R828D responds at 0x74, not 0x34 (which is the R820T address). Probing must try both.

  3. Low-IF Mode: The R828D uses low-IF (not Zero-IF). After tuner detection, Zero-IF mode must be explicitly disabled (page1 reg 0xb1 = 0x1a) and the In-phase ADC input enabled (page0 reg 0x08 = 0x4d).

  4. I2C Chunk Size: The RTL2832U I2C bridge has a maximum transfer size of 8 bytes (7 data + 1 register byte). The 27-byte tuner init array must be written in chunks or the USB endpoint stalls with a pipe error.

  5. Demod Register Sync: Every demodulator register write must be followed by a dummy read of page0 reg 0x01 β€” this is a hardware flush/sync requirement. Omitting it causes subsequent control transfers to stall.

  6. EEPROM Recovery: If the dongle's EEPROM is corrupted (reverts to generic strings), restore it with the RTL-SDR Blog fork of rtl_eeprom:

    ~/rtl-sdr-blog/build/src/rtl_eeprom -m "RTLSDRBlog" -p "Blog V4" -s "00000001"

πŸš€ Performance

Operation Pi 5 aarch64 x86_64 (Ryzen 7600X)
u8 β†’ f32 converter 1.49 GiB/s 12.67 GiB/s (AVX-512)
FIR decimator Γ·8 426 MSa/s 590 MSa/s
Full pipeline Γ·8 328 MiB/s β€”

Both results use cargo build --release without target-cpu=native. On x86_64, LLVM auto-vectorizes the converter to AVX-512 on supported CPUs. Setting target-cpu=native on Zen 4 can cause AVX-512 frequency throttling that hurts the decimator β€” the default build is better balanced.

Pi 5 is memory-bandwidth-bound on the converter. x86_64 desktop has enough DDR5 bandwidth that the ALU keeps up.

πŸŽ› Performance Tuning & Latency

Reducing Frequency Switching Lag

This driver implements a "Flush-on-Tune" mechanism. When you change frequency (e.g., click a bookmark), all stale data currently in the driver's buffers is immediately dropped. This makes tuning feel much snappier than standard librtlsdr.

However, you may still experience some lag in applications like Gqrx. This is because Gqrx maintains its own large internal audio and DSP buffers which we cannot flush.

For Gqrx Users:

  • Set Bandwidth to 0 (Auto) to avoid unnecessary I2C filter commands.
  • Reduce Audio Buffer size in Gqrx settings if audio lags behind the waterfall.
  • If you need ultra-low latency, you can programmatically reduce the driver's buffer count via StreamConfig.

For OpenWebRX Users:

  • OpenWebRX generally feels snappier because it manages its own pipeline more aggressively and benefits directly from our driver's flush mechanism.

Advanced Configuration (StreamConfig)

You can tune the trade-off between latency and stability by modifying the StreamConfig struct when creating a stream:

// Adjust configuration on the driver instance
driver.stream_config = rtlsdr::StreamConfig {
    num_buffers: 8,          // Default: 16. Lower = less latency, higher risk of drops.
    buffer_size: 256 * 1024, // Default: 256KB.
};

// Create the stream with the new settings
let stream = driver.stream();

πŸ“‹ Prerequisites

  • Rust toolchain (stable)
  • libusb development headers
  • USB access (see platform setup below)
# Ubuntu/Debian
sudo apt-get install libusb-1.0-0-dev

# macOS
brew install libusb

# Windows β€” no libusb headers needed, but see USB driver setup below

πŸ”Œ USB Permissions

The recommended approach is a persistent udev rule rather than chmod:

# Create udev rule
echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="2838", MODE="0666", GROUP="plugdev"' \
  | sudo tee /etc/udev/rules.d/99-rtlsdr.rules

# Reload and trigger
sudo udevadm control --reload-rules
sudo udevadm trigger

# Add yourself to plugdev if needed
sudo usermod -aG plugdev $USER

For quick testing only: sudo chmod 666 /dev/bus/usb/$(lsusb | grep RTL | awk '{print $2"/"$4}' | tr -d ':')

Windows

Windows requires a one-time driver swap via Zadig β€” the dongle enumerates as a DVB-T TV tuner by default and must be switched to WinUSB before libusb can claim it:

  1. Download and open Zadig
  2. Options β†’ List All Devices
  3. Select the RTL-SDR entry (look for "Bulk-In, Interface 0")
  4. Select WinUSB in the driver dropdown
  5. Click "Replace Driver"

This survives reboots but may need to be repeated if you plug into a different USB port. The Unix socket sharing server (SharingServer) is not available on Windows β€” use the rtl_tcp server for local sharing instead.

Environment Variables (Windows)

CMD: set RUST_LOG=info then run the command separately. PowerShell: $env:RUST_LOG = "info"; cargo run --release --example rtl_tcp

πŸ— Building & Installation

git clone https://github.com/mattdelashaw/rtlsdr-next
cd rtlsdr-next
# Install the library and binaries globally
cargo install --path .

For maximum Pi 5 performance, set the target CPU explicitly:

RUSTFLAGS="-C target-cpu=native" cargo build --release

On x86_64, target-cpu=native is not recommended β€” LLVM already auto-vectorizes well, and on Zen 4 CPUs AVX-512 activation causes frequency throttling that hurts sustained DSP throughput.

▢️ Usage

Unified Daemon (recommended)

rtlsdr-daemon runs a single hardware driver instance and serves any combination of protocols simultaneously. This is the recommended deployment for anything beyond a single-client use case.

# Minimal β€” rtl_tcp only (SDR++, GQRX, OpenWebRX+)
rtlsdr-daemon --rtl-tcp 0.0.0.0:1234

# Both servers simultaneously
rtlsdr-daemon --rtl-tcp 0.0.0.0:1234 --websdr 0.0.0.0:8080

# With TLS on the WebSDR server (wss://)
rtlsdr-daemon --websdr 0.0.0.0:8080 \
  --cert /etc/letsencrypt/live/yourdomain.com/fullchain.pem \
  --key  /etc/letsencrypt/live/yourdomain.com/privkey.pem

# From a config file (see Configuration section below)
rtlsdr-daemon -c /etc/rtlsdr-next/config.toml

# Config file with a CLI override (--gain overrides the file value)
rtlsdr-daemon -c /etc/rtlsdr-next/config.toml --gain 25.0

Full CLI reference:

Options:
  -c, --config <PATH>          TOML config file
      --device <INDEX>         USB device index [default: 0]
      --sample-rate <HZ>       Hardware sample rate [default: 1536000]
      --freq <HZ>              Initial center frequency [default: 101100000]
      --gain <DB>              Initial RF gain in dB [default: 30.0]
      --ppm <PPM>              Crystal PPM correction [default: 0]
      --bias-t                 Enable bias tee on startup
      --buffers <N>            USB transfer buffer count [default: 16]
      --buffer-size <BYTES>    USB transfer buffer size [default: 262144]
      --rtl-tcp <ADDR>         Enable rtl_tcp server
      --websdr <ADDR>          Enable WebSDR server
      --unix <PATH>            Enable Unix socket server (Unix only)
      --cert <PATH>            TLS certificate PEM (enables wss://)
      --key <PATH>             TLS private key PEM
      --log-level <LEVEL>      error|warn|info|debug|trace [default: info]
  -h, --help
  -V, --version

πŸ“» Independent Virtual VFOs

When multiple browser clients connect to the WebSDR server, each gets its own DSP pipeline with a dedicated Numerically Controlled Oscillator (NCO). Clients can tune independently to any frequency within the hardware's 1.536 MHz capture window without triggering a hardware retune β€” the NCO shifts the spectrum in software for each client independently.

If a client tunes outside the current hardware window, the daemon automatically recenters the hardware and all clients recompute their NCO offsets. Clients already listening within the new window hear no interruption.

βš™οΈ Configuration File

Copy config/default.toml from the repository as a starting point:

[hardware]
device_index  = 0
sample_rate   = 1_536_000   # Hz β€” must divide evenly to 48000 for WebSDR
initial_freq  = 101_100_000 # Hz β€” 101.1 MHz
initial_gain  = 30.0        # dB
ppm           = 0
bias_t        = false

[stream]
num_buffers  = 16       # increase if you hear dropouts
buffer_size  = 262144   # bytes, must be multiple of 512

[servers]
rtl_tcp     = "0.0.0.0:1234"
websdr      = "0.0.0.0:8080"
# unix_socket = "/tmp/rtlsdr.sock"  # uncomment to enable (Unix only)

[tls]
# cert = "/etc/letsencrypt/live/yourdomain.com/fullchain.pem"
# key  = "/etc/letsencrypt/live/yourdomain.com/privkey.pem"

Place it at /etc/rtlsdr-next/config.toml (system-wide) or ~/.config/rtlsdr-next/config.toml (per-user) and run:

rtlsdr-daemon -c /etc/rtlsdr-next/config.toml

πŸ”„ Running as a systemd Service (Pi 5 / Linux)

# /etc/systemd/system/rtlsdr-daemon.service
[Unit]
Description=rtlsdr-next unified daemon
After=network.target

[Service]
ExecStart=/usr/local/bin/rtlsdr-daemon -c /etc/rtlsdr-next/config.toml
Restart=on-failure
RestartSec=5
User=pi

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now rtlsdr-daemon
sudo journalctl -u rtlsdr-daemon -f

Standalone Tools

The individual binaries remain available for single-protocol deployments or when you don't need the daemon's multi-client capability.

Command Description
rtl_tcp Standard RTL-SDR TCP server. Compatible with OpenWebRX+, GQRX, SDR++. Supports -a/--address and -p/--port.
websdr WebSocket SDR server. Streams decoded audio (48kHz PCM) and waterfall data. Supports -a/--address, -p/--port, and TLS/SSL (wss://).
# Start rtl_tcp server on all interfaces, port 1234
rtl_tcp --address 0.0.0.0 --port 1234

# Start WebSDR server (standard ws://)
websdr --address 0.0.0.0 --port 8080

# Start WebSDR server with TLS (wss://)
websdr --address 0.0.0.0 --port 8080 --cert cert.pem --key key.pem

πŸ”’ Secure WebSDR (wss://)

To support secure connections (required by many modern browsers and iOS App Transport Security when using public domains), you can provide a PEM-encoded certificate and private key.

Using Let's Encrypt: If you have a domain pointing to your host, you can use certbot to generate a certificate and point websdr to the generated files:

websdr --port 8080 --cert /etc/letsencrypt/live/yourdomain.com/fullchain.pem --key /etc/letsencrypt/live/yourdomain.com/privkey.pem

Using Self-Signed (for testing):

openssl req -x509 -newkey rsa:4048 -keyout key.pem -out cert.pem -days 365 -nodes

Note: iOS clients will require you to manually trust the root certificate if using self-signed.

Development Examples

These demonstrate library usage and provide quick functional tests. Run them with cargo run --example.

Example Description
hw_probe Start here. Full driver smoke test β€” init, tune, stream 1s, report throughput. Clear PASS/FAIL output.
fm_radio FM receiver with built-in demodulator. Outputs audio via the system audio device.
monitor Continuous stream monitor β€” logs average signal magnitude and throughput every 10 blocks.
# Smoke test β€” run this first
RUST_LOG=info cargo run --release --example hw_probe

# FM radio
RUST_LOG=info cargo run --release --example fm_radio -- --freq 97.1e6

Diagnostic Tools

These are raw USB tools used during driver development. They bypass the driver entirely and speak directly to the hardware via libusb. Run them when you need to determine whether a problem is in the driver or the hardware.

Example Description
diag_write Scans all 8 USB control blocks for register responses. Use when debugging Pipe errors.
diag_i2c Probes demod read/write encoding patterns and validates the dummy-read flush requirement.
diag_demod Re-acquisition pulse β€” tries up to 5 times to reclaim a busy/crashed USB interface.
diag_sys Dumps registers from USB/SYS/DEMOD blocks using both encoding patterns.
diag_raw_clone Replays the exact V4 init sequence raw. The definitive "hardware vs driver" test.
# Is it the hardware or the driver?
RUST_LOG=debug cargo run --release --example diag_raw_clone

# Device stuck as busy after a crash?
cargo run --release --example diag_demod

# Pipe errors on demod writes?
cargo run --release --example diag_i2c

πŸ§ͺ Testing

# Unit tests (no hardware required)
cargo test

# Release mode (also runs NEON/scalar agreement tests on aarch64)
cargo test --release

Hardware-in-the-loop tests require a connected dongle and are run manually via the examples.

βš™οΈ Environment Variables

Variable Default Description
RUST_LOG warn Log level: error, warn, info, debug, trace
RTLSDR_DEVICE_INDEX 0 USB device index if multiple dongles are connected

Baseline Comparisons

Measurements taken on an ARM64 host comparing the rtlsdr-next Rust implementation against the librtlsdr (v4 branch) C baseline using 256KB blocks:

Benchmark Task librtlsdr (C) rtlsdr-next (Rust)
Standard Converter (256KB) 172.32 Β΅s 164.35 Β΅s
Throughput 1.4168 GiB/s 1.4855 GiB/s
Range [min ... max] [172.29 Β΅s ... 172.36 Β΅s] [164.13 Β΅s ... 164.78 Β΅s]
V4 Inverted Converter 256.07 Β΅s 170.81 Β΅s
Throughput 976.30 MiB/s 1.4293 GiB/s
Range [min ... max] [256.02 Β΅s ... 256.14 Β΅s] [170.78 Β΅s ... 170.84 Β΅s]
Decimation (FIR/4) N/A 778.40 Β΅s
Throughput N/A 336.77 Melem/s
Decimation (FIR/8) N/A 615.37 Β΅s
Throughput N/A 425.99 Melem/s
Decimation (FIR/16) N/A 534.04 Β΅s
Throughput N/A 490.87 Melem/s
Full Pipeline (FIR/4) N/A 925.26 Β΅s
Throughput N/A 270.19 MiB/s
Full Pipeline (FIR/8) N/A 761.17 Β΅s
Throughput N/A 328.44 MiB/s
Full Pipeline (FIR/16) N/A 678.84 Β΅s
Throughput N/A 368.27 MiB/s

Note: The performance gain in conversion is primarily due to moving from cache-latency-bound lookup tables to instruction-parallel arithmetic, which better utilizes modern out-of-order CPU pipelines.

πŸ—Ί Roadmap - Phaseshifting

  • Phase 1: Hardware Bridge β€” USB vendor requests, I2C bridge, control transfer encoding
  • Phase 2: R828D / V4 Support β€” Full tuner initialization, PLL, gain tables
  • Phase 3: Async Stream β€” Tokio-native stream with backpressure and graceful shutdown
  • Phase 4: DSP Pipeline β€” NEON SIMD FIR decimation, FM demodulator, AGC, DC removal
  • Phase 5: Device Sharing β€” Zero-copy Arc broadcasting, Unix socket server
  • Phase 6: Auto Probing β€” I2C handshake-based tuner detection
  • Phase 7: Zero-Allocation β€” Buffer pooling, in-place processing
  • Phase 8: rtl_tcp Server β€” Compatible with OpenWebRX+, GQRX, SDR#
  • Phase 9: Elonics E4000 β€” Full Zero-IF driver with manual gain control (theoretically)
  • Phase 10: Fitipower Tuners β€” FC0012/FC0013 register maps
  • Phase 11: Cross-Platform β€” Windows confirmed working via Zadig/WinUSB; x86_64 LLVM auto-vectorization benchmarked, no manual SIMD needed
  • Phase 12: Multi-Client Daemon Architecture β€” Single hardware handle, rtl_tcp + WebSDR + Unix socket simultaneously; per-client NCO DDC for independent virtual VFOs; TOML config + clap CLI
  • Phase 13: Configurables β€” Runtime buffer sizes, gain modes, bias-T persistence

πŸ“ Reference Material

Tuner Datasheets and Drivers

Another Rust Implementation

  • RTL-SDR-RS - probably human community driven project

Rafael Micro R820T / R820T2 / R828D

Elonics E4000 (legacy, up to 2.2 GHz, gap ~1.1 GHz)

Fitipower FC0012 / FC0013 (budget nano dongles)

RTL2832U

πŸ“œ License

Licensed under the Apache License, Version 2.0.

About

High-performance async Rust driver for RTL-SDR(RTL2832U) with NEON optimizations for the Pi 5.

Topics

Resources

License

Stars

36 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors