https://github.com/mattdelashaw/rtlsdr-next
High-performance async Rust driver for RTL-SDR(RTL2832U) with NEON optimizations for the Pi 5.
https://github.com/mattdelashaw/rtlsdr-next
e4000 neon-simd r820d r820t raspberry-pi rtl-sdr rust sdr software-defined-radio tokio
Last synced: 3 months ago
JSON representation
High-performance async Rust driver for RTL-SDR(RTL2832U) with NEON optimizations for the Pi 5.
- Host: GitHub
- URL: https://github.com/mattdelashaw/rtlsdr-next
- Owner: mattdelashaw
- License: apache-2.0
- Created: 2026-03-07T00:48:33.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-03-14T03:27:57.000Z (4 months ago)
- Last Synced: 2026-03-14T13:49:12.238Z (4 months ago)
- Topics: e4000, neon-simd, r820d, r820t, raspberry-pi, rtl-sdr, rust, sdr, software-defined-radio, tokio
- Language: Rust
- Homepage:
- Size: 951 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# rtlsdr-next ๐ก

[](https://github.com/mattdelashaw/rtlsdr-next/actions/workflows/rust.yml)
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).
- **Zero-Copy Broadcasting:** Share a single hardware device across multiple local apps via `Arc`-based broadcasting.
- **Precision Frequency Correction:** Integrated PPM correction for both tuner PLL and RTL2832U resampler.
- **Standalone Tools:** Optimized `rtl_tcp` and `websdr` binaries for professional distribution.
## ๐ 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:
```bash
~/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:
```rust
// 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)
```bash
# 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`:
```bash
# 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](https://zadig.akeo.ie/) โ 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
```bash
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:
```bash
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
### Standalone Tools
Once installed, you can run the primary tools directly from your terminal:
| 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://`). |
```bash
# 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:
```bash
websdr --port 8080 --cert /etc/letsencrypt/live/yourdomain.com/fullchain.pem --key /etc/letsencrypt/live/yourdomain.com/privkey.pem
```
**Using Self-Signed (for testing):**
```bash
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. |
```bash
# 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. |
```bash
# 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
```bash
# 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
- [x] **Phase 1: Hardware Bridge** โ USB vendor requests, I2C bridge, control transfer encoding
- [x] **Phase 2: R828D / V4 Support** โ Full tuner initialization, PLL, gain tables
- [x] **Phase 3: Async Stream** โ Tokio-native stream with backpressure and graceful shutdown
- [x] **Phase 4: DSP Pipeline** โ NEON SIMD FIR decimation, FM demodulator, AGC, DC removal
- [x] **Phase 5: Device Sharing** โ Zero-copy Arc broadcasting, Unix socket server
- [x] **Phase 6: Auto Probing** โ I2C handshake-based tuner detection
- [x] **Phase 7: Zero-Allocation** โ Buffer pooling, in-place processing
- [x] **Phase 8: rtl_tcp Server** โ Compatible with OpenWebRX+, GQRX, SDR#
- [x] **Phase 9: Elonics E4000** โ Full Zero-IF driver with manual gain control (theoretically)
- [x] **Phase 10: Fitipower Tuners** โ FC0012/FC0013 register maps
- [x] **Phase 11: Cross-Platform** โ Windows confirmed working via Zadig/WinUSB; x86_64 LLVM auto-vectorization benchmarked, no manual SIMD needed
- [ ] **Phase 12: Configurables** โ Runtime buffer sizes, gain modes, bias-T persistence
## ๐ Reference Material
### Tuner Datasheets and Drivers
**Another Rust Implementation**
- [RTL-SDR-RS](https://github.com/ccostes/rtl-sdr-rs) - probably human community driven project
**Rafael Micro R820T / R820T2 / R828D**
- [R820T Datasheet (leaked draft)](https://github.com/josebury/R820T-Datasheet/blob/master/R820T_Datasheet-Non_Disclosure_Working_Draft.pdf) โ gold standard for register maps
- [RTL-SDR Blog V4 Guide](https://www.rtl-sdr.com/V4/) โ V4-specific hardware details
- [tuner_r82xx.c](https://github.com/rtlsdrblog/rtl-sdr-blog/blob/master/src/tuner_r82xx.c) โ reference C implementation
**Elonics E4000** (legacy, up to 2.2 GHz, gap ~1.1 GHz)
- [Osmocom E4000 Wiki](https://osmocom.org/projects/rtl-sdr/wiki/E4000)
- [tuner_e4k.c](https://github.com/osmocom/rtl-sdr/blob/master/src/tuner_e4k.c)
**Fitipower FC0012 / FC0013** (budget nano dongles)
- [tuner_fc0012.c](https://github.com/osmocom/rtl-sdr/blob/master/src/tuner_fc0012.c)
### RTL2832U
- [Osmocom RTL-SDR Wiki](https://osmocom.org/projects/rtl-sdr/wiki/Rtl-sdr) โ the definitive reference
- [librtlsdr.c](https://github.com/rtlsdrblog/rtl-sdr-blog/blob/master/src/librtlsdr.c) โ comments are essentially the RTL2832U register manual
## ๐ License
Licensed under the Apache License, Version 2.0.