Tagged: librtlsdr

RTLSDR-NEXT: A Ground-Up Rust Rewrite of the RTL-SDR Driver

Thank you to Matthew Delashaw, who has written in and shared a guest post with us. Matthew has rewritten the 2013 librtlsdr library from the ground up in Rust. His motivations for doing so and the results are explained in the post below:


I actually started down this path as an "interest". There was a Ham radio Technical Interest Group I was planning on attending a meeting. I had already wanted to convert my Raspberry Pi into a fallback radio receiver for potential internet outages and listening to storm chasers on SKYWARN. Now I have the "v4" dongle, and a full end-to-end SDR solution. !Spoilers, I'm releasing a native smart phone client soon.

The RTL2832U chipset has powered affordable software-defined radio for over a decade. The reference driver, librtlsdr, was written in C around 2013 and follows the same architectural pattern it always has: a blocking callback loop, manual buffer management, and a programming model that predates modern async runtimes by years.

rtlsdr-next is a ground-up Rust rewrite. It exposes SDR data as a native Tokio Stream, ships a zero-allocation DSP pipeline, and has first-class support for the RTL-SDR Blog V4 — a newer hardware variant the upstream driver handles correctly but never cleanly abstracted. The result is faster, safer, and substantially easier to build applications on top of.

1.49 GiB/s IQ conversion on Pi 5  ·  ~45ms frequency switching (was ~270ms with 20 I2C toggles)  ·  0 allocations in the streaming hot path


Why rewrite it at all?

The C driver works. Millions of people run it daily via OpenWebRX, GQRX, SDR++, and friends. But its architecture creates friction at every layer: the callback-based stream makes backpressure impossible to reason about, the I2C bus is hammered with redundant open/close cycles, and the conversion routine uses a 256-entry lookup table whose cache pressure eats into throughput on modern out-of-order cores.

More practically: trying to integrate librtlsdr into a modern async Rust application means spawning a dedicated thread, wrapping callbacks in channels, and handling all the lifetime gymnastics manually. For every project that does this, someone reinvents the same boilerplate. There are plenty of Rust "wrappers" out there That exemplifies this.


The stream architecture

The primary interface is a standard async stream. A SampleStream wraps a background USB reader thread that feeds raw IQ bytes into a tokio::mpsc channel. The F32Stream layer sits on top and handles conversion, decimation, DC removal, and AGC — all in a single pipeline with no intermediate heap allocations.

let mut stream = driver.stream_f32(8)   // ÷8 → 256 kSPS
    .with_dc_removal(0.01)
    .with_agc(1.0, 0.01, 0.01);

while let Some(Ok(iq)) = stream.next().await {
    // interleaved f32 I/Q, ready to demodulate
}

The blocking USB read thread never touches the async runtime. Sample delivery to async consumers happens entirely through the channel, and the PooledBuffer type ensures the backing buffers are returned to the pool via Drop — no explicit lifecycle management needed at the call site.

  • SampleStream — Blocking USB thread → tokio::mpsc channel. Pre-allocated buffer pool. Flush-on-tune via broadcast::Sender.
  • F32Stream — Convert → decimate (FIR) → DC remove → AGC. Processes split I/Q in-place. No per-block allocation.
  • PooledBuffer — Returns buffer to pool on Drop. try_send with blocking fallback thread — the pool never silently starves.
  • BoardOrchestratorV4Orchestrator / GenericOrchestrator produce a TuningPlan. Board logic never leaks into chip drivers.

The I2C repeater optimization

Every register write to the R828D tuner chip goes through an I2C bridge in the RTL2832U. The bridge must be explicitly opened and closed around each transaction. In a naive implementation — which is what the reference driver does — every call to set_frequency independently opens and closes the repeater for each register write.

A full frequency switch involves setting the PLL, MUX, filter coefficients, and various control registers. That adds up to roughly 20 open/close cycles, and each one costs ~13ms of USB round-trip time.

The fix: a single with_repeater(|| { ... }) closure that holds the bridge open for the entire mux + PLL sequence. One open, one close, all the work done in between.

// Before: ~20 repeater toggles ≈ 270ms
self.set_mux(hz)?;   // 10 writes, each with open/close
self.set_pll(hz)?;   // 10 writes, each with open/close

// After: 1 repeater toggle ≈ 45ms
self.with_repeater(|| {
    self.set_mux_raw(hz)?;
    self.set_pll_raw(hz)?;
    Ok(())
})?;

The distinction between write_reg_mask (opens and closes the repeater itself) and write_reg_mask_raw (no repeater toggle, must be inside a bracket) is enforced by convention throughout the codebase. Any raw variant called outside a bracket is a bug that surfaces immediately as a timeout rather than silently returning stale data.


Converter throughput

librtlsdr converts raw IQ bytes to float via a static 256-entry lookup table. It is a reasonable approach from an era when float math was expensive and cache was plentiful. On the Cortex-A76 inside the Pi 5, the situation is inverted: the NEON FPU is underutilized and random-access table reads create cache pressure that limits throughput.

The arithmetic equivalent — (x as f32 - 127.5) / 127.5 — is computed in two instructions per sample and is trivially auto-vectorized by LLVM. The compiler emits NEON FMLA instructions without any manual intrinsics.

Operation librtlsdr (C) rtlsdr-next (Rust)
Standard conversion (256KB) 172.32 µs · 1.42 GiB/s 164.35 µs · 1.49 GiB/s
V4 inverted conversion 256.07 µs · 976 MiB/s 170.81 µs · 1.43 GiB/s
FIR decimation ÷8 N/A 615 µs · 426 MSa/s

The V4 inversion case is a particularly notable optimization. librtlsdr implements it as a two-pass operation: first a full LUT conversion, then a second pass to negate every Q sample. The Rust implementation folds both into a single pass, processing I and Q pairs together and avoiding a complete re-read of the output buffer.


RTL-SDR Blog V4 specifics

The V4 is a substantial hardware revision. It ships with an R828D tuner (not R820T), adds an HF upconverter and a GPIO-switched triplexer, and has several initialization quirks that librtlsdr discovered through usbmon traces and EEPROM string detection.

The board logic is isolated entirely in V4Orchestrator. Given a target frequency, it returns a TuningPlan — the actual tuner frequency, whether spectral inversion is needed, which triplexer path to select, and whether the frequency falls inside a notch band. The R828D chip driver never touches a GPIO.

Notable quirks baked into the driver: the R828D responds at I2C address 0x74 rather than the R820T's 0x34; frequencies below 28.8 MHz are upconverted by adding the crystal frequency, and the resulting spectrum is inverted (Q = –Q). Every demodulator register write must be followed by a dummy read of page 0x0a register 0x01 — the hardware requires this as a flush sync, and omitting it causes subsequent control transfers to stall with a pipe error.


Built-in DSP pipeline

The dsp module ships a complete demodulation stack. The decimator uses a windowed-sinc FIR with NEON acceleration on aarch64, with a scalar fallback that LLVM auto-vectorizes on x86_64. The FM demodulator is a quadrature discriminator with configurable de-emphasis. AM uses a two-stage DC-subtraction envelope detector. SSB uses the phasing method with a 65-tap Hilbert transformer windowed with Blackman-Harris for high sideband rejection.

All demodulators maintain state across block boundaries — the history overlap buffer in the decimator ensures the FIR convolution is correct at every chunk edge, which is essential for continuous streaming.


Standalone servers

Two installable binaries ship alongside the library. rtl_tcp implements the standard RTL-TCP protocol and is compatible with OpenWebRX+, GQRX, and SDR++. websdr is a self-contained WebSocket SDR server with a full spectrum and waterfall UI embedded as a compiled-in HTML file — no separate web server needed. Both support TLS. The WebSDR binary accepts --cert and --key flags for wss:// connections, which are required by iOS App Transport Security when using a public domain.

  • OpenWebRX+ — confirmed working
  • GQRX — confirmed working
  • SDR++ — confirmed working
  • Corona SDR (iOS) — confirmed working

Getting started

cargo install rtlsdr-next

# Smoke test — run this first
RUST_LOG=info cargo run --release --example hw_probe

# Start an rtl_tcp server
rtl_tcp --address 0.0.0.0 --port 1234

# Start the WebSDR UI
websdr --address 0.0.0.0 --port 8080

On Linux, set up a udev rule for persistent USB access without sudo. On Windows, Zadig is required to swap the DVB-T driver to WinUSB — build works without it, but the USB runtime requires it at runtime.


Source on GitHub at github.com/mattdelashaw/rtlsdr-next. Licensed Apache 2.0. Benchmarks measured on Raspberry Pi 5 (aarch64) and AMD Ryzen 7600X (x86_64) with cargo build --release, no target-cpu=native.

Keep and eye out for the smart phone app release here: Spectral Bands

rtlsdr-next running with GQRX
rtlsdr-next running with GQRX

New Updates to the librtlsdr RTL-SDR Driver Fork

Thank you to Hayati Ayguen for letting us know that he and others have submitted a slew of updates to the "librtlsdr" fork of the librtlsdr RTL-SDR drivers. The improvements made to the development branch are extensive and are pasted below, and Hayati also has also created some presentation slides about his improvements. Hayati also notes that there are several open issues being tracked, and he has labelled some as "help wanted" where help and testing would be appreciated.

If you have tested any of the new features of tools, please let us know how they work in the comments!

"Driver" Library Features

  • added support for special USB (vendor) VID 0x1209 (product) PID 0x2832: "Generic RTL2832U":
  • added support for using RTLSDR-Dongle from remote - see rtl_rpcd and README.rtlsdr_rpc
  • improvements for R820T/2 tuner also see https://codingspirit.de/librtlsdr-driver.pdf
    • added better bandwidth support
      • added smaller bandwidths, improving selectivity: 290, 375, 420, 470, 600, 860, 950, 1100, 1300, 1500, 1600, 1750, 1950 kHz. These are coarse measured values .. which might get adjusted in future.
      • bandwidth filters utilize tuner's low- and highpass filters at IF
    • added spectrum flipping (inside tuner) - and back in RTL2832
      • the band edges (low/high-pass) have different steepness; the steeper edge can be selected with the mixer sideband (rtlsdr_set_tuner_sideband()), to achieve better attenuation depending on signal scenario
    • added (automatic) control over VGA (variable gain amplifier)
      • VGA gain (besides LNA and Mixer) can be utilized and set to automatic, letting it controlled from RTL2832U. Having all automatic (AGC) including activation of digital AGC in RTL2832 (rtlsdr_set_agc_mode()), oversteering effects got reduced (a lot).
      • gain range now up to 100 dB
    • deactivated "Filter extension under weak signal" for a stable filter characteristic
    • added shifting of IF-center, to receive away from DC. See rtlsdr_set_tuner_band_center()
  • probably some more: it's highly probable, that this list is incomplete

"Driver" Library API

  • added rtlsdr_set_and_get_tuner_bandwidth(), which also delivers the bandwidth. [ with rtlsdr_set_tuner_bandwidth() does not deliver the bandwidth ]
  • added rtlsdr_set_tuner_band_center(), to set center of the filtered tuner band
  • added rtlsdr_set_tuner_sideband(), to set mixer sideband
  • added rtlsdr_set_tuner_gain_ext(), special for R820T/2 tuner
  • added rtlsdr_set_tuner_if_mode(), sets AGC modes in detail
  • added rtlsdr_set_ds_mode() including threshold frequency
  • added rtlsdr_ir_query()
  • added rtlsdr_set_opt_string() and rtlsdr_get_opt_help() for configuration of 'driver' - especially from command line
  • added rtlsdr_set_tuner_i2c_register(), rtlsdr_get_tuner_i2c_register() and rtlsdr_set_tuner_i2c_override() exposing hacking of tuner-specific I2C registers
  • added rtlsdr_get_ver_id(), to allow discrimination between osmocom library - or this fork
  • added rtlsdr_get_version()

Added Tools

  • added rtl_ir: display received IR signals.
    • requires the IR diode of an RTL-SDR - which might not exist!
  • added rtl_rpcd: a Remote Procedure Call server for RTL-SDR dongles.
    • for use, set environment variable "RTLSDR_RPC_IS_ENABLED"
    • optionally set environment varibales "RTLSDR_RPC_SERV_ADDR" and "RTLSDR_RPC_SERV_PORT". These default to "127.0.0.1" and "40000".
    • requires cmake option WITH_RPC
  • added rtl_raw2wav: save rtl_sdr or rtl_fm's output (pipe) into a wave file, including some meta information like timestamp and frequency
  • added rtl_udp: same as rtl_tcp - just using UDP instead of TCP
  • added rtl_wavestat: display wave file meta information
  • added rtl_wavestream: stream raw data (in specified format)

Improved Tools

  • rtl_fm:
    • added command file option '-C', which can trigger actions depending on signal. have a look at README.rtlfm_cmdfile.
    • added command line interface option '-E rdc', to enable dc blocking on raw I/Q data at capture rate
    • added CLI option '-E rtlagc', to enable rtl2832's digital agc
    • added CLI option '-E bclo', to use tuner bandwidths low corner as band center
    • added CLI option '-E bchi', to use tuner bandwidths high corner as band center
    • added CLI option '-O', to set RTL driver options seperated with ':', e.g. -O 'bc=30000:agc=0'
    • added CLI option '-R', to specify number of seconds to run
    • added CLI option '-H', to write wave Header to file, producing a wave file with meta information, compatible with several SDR programs
    • added CLI option '-o', to request oversampling (4 recommended) for processing gain
  • not just rtl_fm, but many tools have more options. compare all the details by starting with command line option '-h'.

"Driver" Library's UDP-Server - only on Windows

  • enabled by cmake option PROVIDE_UDP_SERVER for tests. OFF by default
  • activated by rtlsdr_set_opt_string(): "port=1" or "port=<udp_port>", default port number: 32323
  • purpose is to allow configuration at runtime with a simple text protocol, e.g. with netcat
  • for detailed protocol, see comment section in parse() of librtlsdr.c. or look for sections with '#ifdef WITH_UDP_SERVER'

RTL_TCP TCP-PROTOCOL

  • allows non-GPL programs, e.g. QIRX, to utilize the RTLSDR stuff in a license compliant way
  • added several control functions in rtl_tcp, not existing in osmocom release: UDP_ESTABLISH, UDP_TERMINATE, SET_I2C_TUNER_REGISTER, SET_I2C_TUNER_OVERRIDE, SET_TUNER_BW_IF_CENTER, SET_TUNER_IF_MODE, SET_SIDEBAND, REPORT_I2C_REGS
  • control functions documented in rtl_tcp.h
  • (by default) control port number 1234, configurable via command-line-interface (CLI)
  • response(s) at +1 of control port: 1235, configurable via CLI
  • protocol details in protocol_rtl_tcp.txt

TCP Enabled version of librtlsdr

Recently RTL-SDR.com reader Fabien wrote in to let us know that he has created a new version of the librtlsdr RTL-SDR drivers which have built in TCP support.

Fabien built a remote SDR using a BeagleBone Black mini embedded computer and put it outside for better reception and to be closer to the antenna. When trying to remotely access the dongle he discovered some problems. He writes:

I then access the dongle over TCP from an indoor PC. One issue is that some existing tools such as rtl_fm, rtlizer or rtlsdr_waterfall lacks TCP/IP connectivity.

To solve this problem, I added TCP support to the rtlsdr library. When a tool communicates with the now physically distant dongle, this new implementation transparently forwards the data using TCP instead of USB. It allows one to use existing tools without modifying them. Also, it allows a developer to use the same librtlsdr, no matter whether the dongle is local or distant.

The implementation is located here: https://github.com/texane/librtlsdr
branch: rpc

To use it, one must compile and install the library. Then, a server (called rtl_rpcd) must be run on the remote location (in my case, the beagle bone black at address 192.168.0.43): RTLSDR_RPC_SERV_ADDR=192.168.0.43 \ rtl_rpcd

Then, the existing tool (for instance rtlizer) can be run using:
RTLSDR_RPC_IS_ENABLED=1 RTLSDR_RPC_SERV_ADDR=192.168.0.43 \
rtlizer

Outdoor RTL-SDR Receiver Running on a BeagleBone Black
Outdoor RTL-SDR Receiver Running on a BeagleBone Black
Outdoor RTL-SDR Receiver Running on a BeagleBone Black
Outdoor RTL-SDR Receiver Running on a BeagleBone Black