{"id":51595220,"url":"https://github.com/hyperized/rtl2832u","last_synced_at":"2026-07-11T18:01:47.649Z","repository":{"id":355711774,"uuid":"1229268656","full_name":"hyperized/rtl2832u","owner":"hyperized","description":"Pure Go driver for the Realtek RTL2832U SDR demodulator + Rafael Micro R820T/R860 tuner. No CGo, no librtlsdr, no libusb.","archived":false,"fork":false,"pushed_at":"2026-05-21T11:04:42.000Z","size":589,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-21T19:34:44.993Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hyperized.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-04T21:49:35.000Z","updated_at":"2026-05-21T11:03:38.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hyperized/rtl2832u","commit_stats":null,"previous_names":["hyperized/rtl2832u"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/hyperized/rtl2832u","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyperized%2Frtl2832u","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyperized%2Frtl2832u/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyperized%2Frtl2832u/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyperized%2Frtl2832u/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hyperized","download_url":"https://codeload.github.com/hyperized/rtl2832u/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyperized%2Frtl2832u/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35370428,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-11T02:00:05.354Z","response_time":104,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-11T18:01:46.975Z","updated_at":"2026-07-11T18:01:47.642Z","avatar_url":"https://github.com/hyperized.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# rtl2832u\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/hyperized/rtl2832u.svg)](https://pkg.go.dev/github.com/hyperized/rtl2832u)\n\nA pure-Go driver for the Realtek RTL2832U SDR demodulator paired with a Rafael Micro R820T / R860 tuner. The driver speaks Linux usbfs directly through `golang.org/x/sys/unix` ioctls — no CGo on the deploy target, no `libusb`, no `librtlsdr`. One module, drop it into any application that needs RTL-SDR IQ samples.\n\n## Why pure Go?\n\n- **Container-friendly**: the deploy artefact is a single static binary. No `apt-get install librtlsdr0` step in your Dockerfile, no shared-library version drift between dev and prod, no glibc surprises in distroless images.\n- **Cross-compilable from anywhere**: `GOOS=linux GOARCH=arm64 go build` produces a uConsole-ready binary from a Mac without configuring a toolchain.\n- **Auditable**: every register write, every USB transfer, every gain table is visible Go code with citations to the RTL2832U / R860 datasheets and (where transliterated) `osmocom/rtl-sdr` (BSD-2). No black box.\n\n## Status\n\nHardware-validated end-to-end on a Realtek RTL2838 + R820T2 dongle on a Raspberry Pi CM4 (uConsole, Linux/arm64). The flow that already works on real silicon:\n\n```\nsysfs enumerate → claim usbfs interface → RTL2832U baseband init →\nconfigure demod for R820T real-IF (I-only ADC, spectrum inversion) →\nR860 detect (chip-ID 0x96 per datasheet §6) → seed register table over I²C →\nrate-driven IF filter (Tuner.InitializeForSampleRate) →\nalign demod DDC with tuner IF → sample-rate divider → PLL lock →\nper-stage gain → IF channel-filter overrides → optional bias-tee →\noptional auto-gain search → URB-ring bulk read → IQ samples on the wire\n```\n\nCoverage: ≥ 98 % of statements across the library and the `rtl-probe` operator tool (`make cover`); lint clean against `golangci-lint v2.12` with `default: all` and `revive` enable-all-rules at line-length 120.\n\n## Quickstart\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/hyperized/rtl2832u\"\n)\n\nfunc main() {\n\trcv, err := rtl2832u.Open(\n\t\trtl2832u.WithCenterFreq(1_090_000_000), // 1090 MHz Mode S (the default; shown for clarity)\n\t\trtl2832u.WithSampleRate(2_400_000),     // 2.4 MS/s, the FlightAware dump1090 default\n\t\trtl2832u.WithAutoGain(),                // closed-loop gain search at Open time\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer rcv.Close()\n\n\tbuf := make([]byte, 32*1024)\n\tn, err := rcv.Read(context.Background(), buf)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// buf[:n] is interleaved unsigned-8-bit IQ: I, Q, I, Q, ...\n\t// (DC offset at 127). Feed it to your demodulator of choice.\n\tfmt.Printf(\"got %d IQ bytes\\n\", n)\n}\n```\n\n## Public API tour\n\nThe driver exposes a single `Receiver` opened through `Open` with functional options. Once open, `Read` blocks until the requested IQ chunk is available and `Close` releases the device.\n\n### Open / Read / Close\n\n| | |\n|---|---|\n| `Open(opts ...Option) (*Receiver, error)` | Enumerates dongles via sysfs, claims interface 0, runs the chip + tuner init dance. Returns `ErrNoDevice` if nothing matched, `ErrUnsupportedPlatform` on non-Linux. |\n| `Receiver.Read(ctx, p) (int, error)` | Returns up to `len(p)` bytes of interleaved `I, Q, I, Q, ...` from the next completed URB. One Read maps to one URB; wrap with `io.ReadFull` for fill-to-buffer semantics. Buffer sizes between 16 KiB and 256 KiB match the URB sizes used by `librtlsdr` / `dump1090`. Cancel via `ctx`. |\n| `Receiver.Close() error` | Releases the USB interface and closes the device handle. Idempotent. |\n\n### Tuning options\n\n| Option | Default | Notes |\n|---|---|---|\n| `WithCenterFreq(hz uint32)` | `DefaultCenterFreqHz` (1090 MHz) | Override for non-ADS-B targets within the tuner's 24 MHz – 1.766 GHz range. |\n| `WithSampleRate(hz uint32)` | `DefaultSampleRateHz` (2.4 MS/s) | Valid sub-ranges: (225 kHz, 300 kHz] ∪ (900 kHz, 3.2 MHz]. The chip's gap between 300 kHz and 900 kHz produces nonsense; the validator rejects with a hint. |\n| `WithDevice(index int)` | `0` (first enumerated) | Stable per boot, not across reboots — pin by serial number once an EEPROM reader lands. |\n| `WithFrequencyCorrection(ppm int)` | `0` | Trims the chip's effective reference crystal so both `rsamp_ratio` and the R860 PLL compensate for a drifty TCXO. Clamped to ±`FrequencyCorrectionPPMMax` (1000 ppm), matching `librtlsdr`'s `rtlsdr_set_freq_correction` range. |\n\n### Gain control\n\nThree stages on the R820T / R860 tuner: LNA, post-mixer amp, VGA. Pin individually, hand to the chip's AGC, or let auto-tune pick.\n\n| Option | Notes |\n|---|---|\n| `WithGain(tenthsDB int)` | librtlsdr-compatible single-knob ladder. Walks the chip's empirically-calibrated LNA + Mixer step pairs to land closest to the requested target; pins VGA at the librtlsdr default (+16 dB). Pass `GainAGC` to hand all three stages back to AGC. |\n| `WithLNAGain(stage GainStage)` | Per-stage escape hatch for callers already holding a `GainStage` (e.g. from `VGAStepForCentiDB`). `AutoGain` for AGC, `ManualGainStep(0..15)` to pin. Last write wins. |\n| `WithMixerGain(stage GainStage)` | Same shape; controls the post-mixer amplifier. |\n| `WithVGAGain(stage GainStage)` | VGA scale is documented (3.5 dB/step from -12.0 dB per R860 datasheet table 6-3). Use `VGAStepForCentiDB(centi)` if a centi-dB target reads better than a step index. |\n| `WithManualLNAGain(step uint8)` | User-input-friendly variant: pins the LNA to a 4-bit code (0..15) and surfaces a `Warn`-level log entry through `WithLogger` if `step` exceeds 15 (`ManualGainStep` clamps silently — debug-hostile). Same for `WithManualMixerGain` / `WithManualVGAGain`. |\n| `WithAutoGain()` | Closed-loop search at Open time: pin Mixer + VGA at max, walk LNA from step 15 downward until the chip's `if_agc_val` mean (RTL2832U §8.1.5) climbs above the over-gained threshold. Converges in 1–3 iterations on most chains; logs the result via the configured `slog.Logger`. |\n\n`Receiver.AutoTuneGain(ctx, opts)` runs the same algorithm at runtime — useful after a band change or thermal event.\n\n### IF channel filter (R860)\n\n| Option | Register | Notes |\n|---|---|---|\n| `WithIFBandwidth(coarse, fine uint8)` | FILT_BW (0..3) + FILT_CODE (0..15) | Defaults to the chip's init seed (`FILT_BW=3 narrow, FILT_CODE=6 mid` — librtlsdr's). Lower numbers = wider, higher = narrower. |\n| `WithIFHighPass(code uint8)` | R11 HPF[3:0] | Use the package's `R860HPF*` constants for the documented (corner, attenuation) tuples per code. |\n| `WithFilterExt(enable bool)` | R30[6] | Datasheet labels this \"filter extension for weak signal conditions\"; the mechanism is undocumented. Toggle empirically. |\n\n### Diagnostics + auxiliary\n\n| | |\n|---|---|\n| `Receiver.ReadSampleStats(ctx, targetSamples) (SampleStats, error)` | Reads `targetSamples` I/Q pairs and returns host-side magnitude statistics: RMS, peak, saturation fraction, DC offsets, and a 64-bucket magnitude histogram. Primary chain-quality probe. Works on all tuners — computed from the bulk endpoint, independent of the chip's AGC state. |\n| `ComputeSampleStats(raw []byte) SampleStats` | Pure-function variant: when the caller already has the raw I/Q bytes in hand (replay mode, shared buffers feeding an FFT, etc.) and wants the stats without going through a Read pass. |\n| `Receiver.SignalStats() (SignalStats, error)` | Point-in-time read of the chip's `if_agc_val` / `rf_agc_val` / `aagc_lock` (RTL2832U §8.1.5). Note: on the R820T2 path the demod-side RF/IF AGC loops are intentionally disabled to avoid a feedback fight with the tuner's VGA, so these register values are effectively static — use `ReadSampleStats` instead for diagnostics on that tuner. |\n| `Receiver.AutoTuneGain(ctx, opts) (AutoTuneResult, error)` | Closed-loop gain search at Open time. Pins Mixer + VGA at max, walks LNA down until `SampleStats.SaturationFrac` drops below the threshold (default 2 %, sized to ADS-B's burst statistics). |\n| `Receiver.DroppedSampleChunks() uint64` | Cumulative count of sample chunks the URB ring had to discard because the consumer fell behind. A non-zero value over a long-running session means the demod is slower than the configured sample rate. |\n| `WithBiasTee(enable bool)` | Toggles GPIO0 to switch the dongle's 4.5 V bias-tee output. Powers external active LNAs and SAW filters from the antenna coax on V3-class dongles. |\n| `WithBiasTeeGPIO(gpio uint8, enable bool)` | Escape hatch for clones with non-standard bias-tee wiring. |\n\n## Architecture\n\nThe package splits naturally along the two physical chips:\n\n```\n                      Receiver (sdr.go)                        ← public surface\n                       │\n                       ▼\n                openBackend(cfg)                                ← platform-conditional\n                       │\n        ┌──────────────┴──────────────┐\n        │                             │\n   linuxBackend (usbfs_linux.go)   stub_other.go\n        │                             │\n   ┌────┴────┐                  returns ErrUnsupportedPlatform\n   │         │\n   ▼         ▼\n chip      tuner\n (rtl2832u.go,                           (tuner_r860*.go)\n  init_chip.go,                          ─ chip-ID gate (0x96)\n  sample_rate.go,                        ─ seed register table (chunked I²C)\n  signal_stats.go,                       ─ PLL synthesis (integer-N + 16-bit Σ-Δ)\n  bias_tee.go,                           ─ per-band setMux\n  i2c.go,                                ─ IF channel filter\n  center_freq.go)                        ─ per-stage gain (LNA / Mixer / VGA)\n        │                                        ▲\n        └────────────── Tuner interface ─────────┘  ← swap-in for E4000, FC0012, …\n```\n\nThe chip driver (`rtl2832u`) only knows about baseband. Any front-end mixer that implements `Tuner` — `SetFreq`, the three `SetXGain` methods, the IF-filter trio — slots in without changes elsewhere. Today only R820T / R860 ships; the abstraction makes adding more straightforward.\n\nThe chip exposes its I²C bus to the tuner through a `repeater` register, encapsulated as `i2cTransport`. Tuners are written against that interface so they can be unit-tested with a small mock instead of booting the whole stack.\n\n## Operator tool: `rtl-probe`\n\n`cmd/rtl-probe` is a small CLI that ships alongside the library. Three modes, all routed through the same opener so the flag plumbing is shared:\n\n```sh\n# one-shot stats line (legacy, fast)\nrtl-probe --probe-bytes 65536\n\n# IQ capture (rtl_sdr / dump1090 --ifile compatible)\nrtl-probe --capture cap.iq --capture-bytes $((2*1024*1024))\n\n# interactive TUI: live histogram + strip chart + spectrum\nrtl-probe --tui\n```\n\n`--tui` opens an in-process terminal UI for interactive gain stepping, live signal-quality diagnostics, and a 3D LNA × Mix × VGA sweep. Useful for bringing up a new dongle, diagnosing a yield drop after an antenna change, characterising a SAW filter or external LNA, or eyeballing a sweep before baking the winning gain config into a daemon.\n\n![rtl-probe --tui showing the live magnitude histogram, RMS / saturation / DC strip chart, Welch-averaged FFT spectrum, and the gain / auto-tune / sweep keybind footer](rtl2832u.png)\n\nThe TUI is driven by a sampling goroutine that pulls raw I/Q chunks (~5 Hz) and derives both `SampleStats` and an FFT spectrum from the same buffer:\n\n```\n┌─ header: live samples / RMS / peak / sat % / DC values ─────────────────┐\n├─ status: smoothed GOOD / MARGINAL / BAD with offender labels ───────────┤\n├─ advice: actionable hints (\"reduce gain\", \"DC offset large\", …) ────────┤\n├──────────────────────────────────┬──────────────────────────────────────┤\n│ ┌─ magnitude histogram ────────┐ │ ┌─ strip chart (last ~30 s) ───────┐ │\n│ │ colour by X-axis position:   │ │ │ rms / sat / peak / dcI / dcQ as  │ │\n│ │  red \u003c 12 mag (under-gained) │ │ │ block-char traces; each cell     │ │\n│ │  green 25–130 (healthy)      │ │ │ colour-coded by per-series grade │ │\n│ │  red \u003e 160 mag (clipping)    │ │ └──────────────────────────────────┘ │\n│ │ y-axis: % of max bucket      │ │ ┌─ spectrum (~54 ms Welch average) ┐ │\n│ │ x-axis: 0..181 |I+jQ|        │ │ │ row-coloured (VU-meter style),   │ │\n│ └──────────────────────────────┘ │ │ baseline ╌╌ at long-term floor,  │ │\n│                                  │ │ │ at carrier, ▲ on the axis     │ │\n│                                  │ └──────────────────────────────────┘ │\n├─────────────────────────────────────────────────────────────────────────┤\n└─ footer: LNA=N Mix=N VGA=N bias=on/off · keybinds · errors / auto-tune ─┘\n```\n\nLive controls (footer keybinds):\n\n- **`l` / `L`** — step LNA up / down\n- **`m` / `M`** — step Mixer up / down\n- **`v` / `V`** — step VGA up / down\n- **`b`** — toggle bias-tee\n- **`a`** — run TUI-driven gain auto-tune (pins Mixer + VGA at 15, walks LNA down until `SaturationFrac` ≤ 5 % or LNA hits 0). Press `a` again while it's running to cancel. The footer shows live progress (`probing LNA=N (step k/16)`) and, after a run completes, a green summary (`auto-tune: LNA=N sat=X.XX% in Y steps`) that stays visible so you can compare against manual exploration.\n- **`s`** — run TUI-driven 3D sweep across LNA × Mixer × VGA on a stride-3 grid (`{0,3,6,9,12,15}` per axis = 216 cells, ~1.5–2.5 min). Picks the cell with the highest RMS where `SaturationFrac` ≤ 5 %; falls back to the lowest-saturation cell if none meet the threshold. Footer shows `cell N/216 · probing LNA=L Mix=M VGA=V · best LNA=A Mix=B VGA=C (sat=X.XX% rms=Y.Y)` live; on completion the walker applies the winning cell and the footer shows the sticky summary. Press `s` again to cancel. Mutually exclusive with `a`.\n- **`q`** / **`Esc`** — quit\n\nManual gain keys are suppressed while either walker is running so the operator's keystrokes don't race the walker's `Set*` calls. The footer's completion summary always shows whichever of auto-tune / sweep finished most recently (`completedAt` timestamp comparison).\n\nKey design choices:\n\n- **Welch averaging in the spectrum**: each frame runs ~256 short FFTs over the 128 KiB sample window (50 % overlap, Hann window) and averages in the power domain before converting to dB. Stable noise floor; bursts don't dominate single frames.\n- **Slow-decay scale tracker**: the spectrum's Y-axis top snaps up to new peaks immediately but decays at 5 dB/s — the chart doesn't constantly rescale on transient signals.\n- **Long-term baseline tracker**: a 30 s EMA of the 25th-percentile bin, drawn as a horizontal dashed line. Anything sticking up above the line is signal worth attention.\n- **Carrier marker `│`**: a vertical guide through the chart at the tuned-frequency column, plus a `▲` on the X-axis. Visual anchor for \"is the peak where I tuned?\".\n- **Status / advice debounce**: both banners read from a 20-frame trailing average so they don't flicker on bursty traffic.\n- **TUI-side auto-tune**: the walker drives `Receiver.SetLNAGain` directly and reads `SaturationFrac` off the live sampler rather than calling `Receiver.AutoTuneGain` — the library's variant issues its own `Read` calls, and the receiver is single-producer on the bulk endpoint. This way the strip chart visibly updates as auto-tune steps so the operator can see the convergence.\n\nThe TUI is built on `github.com/rivo/tview` + `github.com/gdamore/tcell` — pulled in only by `cmd/rtl-probe`, so library consumers (e.g. downstream demodulators) don't transitively depend on them.\n\n## Hardware target\n\nTested on:\n\n- **Realtek RTL2838DUB** (USB ID `0x0bda:0x2838`) with a Rafael Micro R820T2 (datasheet-equivalent to R860).\n- **HackerGadgets All-In-One** board on a uConsole + Raspberry Pi CM4 (Linux/arm64).\n- 28.8 MHz reference crystal — the value baked into `referenceClockHz`. Boards with a different TCXO need either `WithFrequencyCorrection(ppm)` (small drift) or, eventually, an EEPROM reader for boards with a 16 MHz reference.\n\nThe chip-ID gate is strict against the datasheet's fixed value (R0 must read `0x96` post-bitrev per R860 §6 Read Mode); dongles with anything else return `ErrTunerNotPresent` so the failure mode is \"wrong tuner / dead silicon\" instead of a cascade of register-write errors.\n\n## Build \u0026 test\n\n```sh\nmake                  # fmt + vet + test (race + cover)\nmake lint             # golangci-lint run ./...\nmake cover            # produces coverage.html\nmake test-integration # go test -race -tags=integration ./...\n```\n\nCross-compile to the deploy target:\n\n```sh\nGOOS=linux GOARCH=arm64 go build ./...\n```\n\n`go test ./...` runs the offline test suite (controller mocks; no USB needed) on every supported platform. Integration tests that touch real hardware live behind the `integration` build tag and are not run by default; see `integration_linux_test.go` and use `make test-integration` on a host with a dongle attached.\n\n## Roadmap\n\n- **Push the remaining tuner-specific demod writes behind the `Tuner` interface.**\n  The rate-driven IF-filter selection is already there — every `Tuner`\n  implements `InitializeForSampleRate(rateHz) (intFreqHz, error)` and\n  the orchestrator pipes the returned IF into the demod's DDC. What is\n  *not* yet behind the interface: `configureForR820T` (the real-IF /\n  I-only-ADC / spectrum-inversion register dance) is still called\n  unconditionally in the bring-up path because R820T is the only\n  silicon that ships today. The next tuner (R828D, future Rafael parts)\n  would want this generalised — likely an `Init(chip) error` method on\n  the `Tuner` interface so each silicon owns its demod-side prerequisites.\n\n## License\n\nBusiness Source License 1.1. See `LICENSE`. Free for non-commercial use; commercial integration requires a paid license. Converts to Apache-2.0 on the change date (2036-05-04).\n\nFor commercial licensing, contact the licensor (see `LICENSE` for details).\n\n## References\n\n- [RTL2832U datasheet](https://www.realtek.com/) (Realtek; sections cited inline as `§N.N` or `R\u003caddr\u003e`).\n- [R860 datasheet](https://www.rafaelmicro.com/) (Rafael Micro; \"table 6-3\", etc.).\n- [`osmocom/rtl-sdr`](https://github.com/osmocom/rtl-sdr) — BSD-2 reference implementation; transliterated tables (gain ladder, FIR coefficients) are attributed inline.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhyperized%2Frtl2832u","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhyperized%2Frtl2832u","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhyperized%2Frtl2832u/lists"}