feat: add RTL-SDR, R820t drivers, radio APIs, and sdr demo tool

This commit is contained in:
2026-06-22 11:49:16 +02:00
parent c8d8ed6ebe
commit 5363ca5265
20 changed files with 2213 additions and 12 deletions
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 1
#define MONTAUK_BUILD_NUMBER 8
+55
View File
@@ -0,0 +1,55 @@
/*
* Sdr.hpp
* Software-defined radio receive syscalls.
* SYS_SDR_COUNT / INFO / OPEN / CLOSE / START / STOP / READ / SETPARAM / GETPARAM
* Thin syscall layer over the generic SDR subsystem (Drivers::Radio::Sdr).
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Drivers/Radio/Sdr.hpp>
#include "Syscall.hpp"
namespace montauk::abi {
static int64_t Sys_SdrCount() {
return (int64_t)Drivers::Radio::Sdr::Count();
}
static int64_t Sys_SdrInfo(int index, SdrDeviceInfo* out) {
if (!out) return -1;
return Drivers::Radio::Sdr::GetInfo(index, out) ? 0 : -1;
}
static int64_t Sys_SdrOpen(int index) {
return (int64_t)Drivers::Radio::Sdr::Open(index);
}
static int64_t Sys_SdrClose(int handle) {
return (int64_t)Drivers::Radio::Sdr::Close(handle);
}
static int64_t Sys_SdrStart(int handle) {
return (int64_t)Drivers::Radio::Sdr::Start(handle);
}
static int64_t Sys_SdrStop(int handle) {
return (int64_t)Drivers::Radio::Sdr::Stop(handle);
}
static int64_t Sys_SdrRead(int handle, uint8_t* buf, uint32_t len) {
if (!buf) return -1;
return (int64_t)Drivers::Radio::Sdr::Read(handle, buf, len);
}
static int64_t Sys_SdrSetParam(int handle, int param, uint64_t value) {
return (int64_t)Drivers::Radio::Sdr::SetParam(handle, param, value);
}
static int64_t Sys_SdrGetParam(int handle, int param) {
return (int64_t)Drivers::Radio::Sdr::GetParam(handle, param);
}
}
+21
View File
@@ -33,6 +33,7 @@
#include "Window.hpp" // SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL, SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE, SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE
#include "Audio.hpp" // SYS_AUDIOOPEN, SYS_AUDIOCLOSE, SYS_AUDIOWRITE, SYS_AUDIOCTL
#include "BluetoothSyscall.hpp" // SYS_BTSCAN, SYS_BTCONNECT, SYS_BTDISCONNECT, SYS_BTLIST, SYS_BTINFO
#include "Sdr.hpp" // SYS_SDR_COUNT, SYS_SDR_INFO, SYS_SDR_OPEN, SYS_SDR_CLOSE, SYS_SDR_START, SYS_SDR_STOP, SYS_SDR_READ, SYS_SDR_SETPARAM, SYS_SDR_GETPARAM
#include "IpcSyscall.hpp" // SYS_DUPHANDLE, SYS_WAIT_HANDLE, SYS_STREAM_CREATE, SYS_STREAM_READ, SYS_STREAM_WRITE, SYS_MAILBOX_CREATE, SYS_MAILBOX_SEND, SYS_MAILBOX_RECV, SYS_WAITSET_CREATE, SYS_WAITSET_ADD, SYS_WAITSET_REMOVE, SYS_WAITSET_WAIT, SYS_PROC_OPEN, SYS_SURFACE_CREATE, SYS_SURFACE_MAP, SYS_SURFACE_RESIZE
#include "LibSyscall.hpp" // SYS_LOAD_LIB, SYS_UNLOAD_LIB, SYS_DLSYM
#include "CrashReportSyscall.hpp" // SYS_CRASH_REPORT
@@ -357,6 +358,26 @@ namespace montauk::abi {
return Sys_AudioList((AudioStreamInfo*)frame->arg1, (int)frame->arg2);
case SYS_AUDIOWAIT:
return Sys_AudioWait(frame->arg1, frame->arg2);
case SYS_SDR_COUNT:
return Sys_SdrCount();
case SYS_SDR_INFO:
if (!UserMemory::Writable<SdrDeviceInfo>(frame->arg2)) return -1;
return Sys_SdrInfo((int)frame->arg1, (SdrDeviceInfo*)frame->arg2);
case SYS_SDR_OPEN:
return Sys_SdrOpen((int)frame->arg1);
case SYS_SDR_CLOSE:
return Sys_SdrClose((int)frame->arg1);
case SYS_SDR_START:
return Sys_SdrStart((int)frame->arg1);
case SYS_SDR_STOP:
return Sys_SdrStop((int)frame->arg1);
case SYS_SDR_READ:
if (!UserMemory::Range(frame->arg2, frame->arg3, true)) return -1;
return Sys_SdrRead((int)frame->arg1, (uint8_t*)frame->arg2, (uint32_t)frame->arg3);
case SYS_SDR_SETPARAM:
return Sys_SdrSetParam((int)frame->arg1, (int)frame->arg2, frame->arg3);
case SYS_SDR_GETPARAM:
return Sys_SdrGetParam((int)frame->arg1, (int)frame->arg2);
case SYS_THREAD_SPAWN:
return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3);
case SYS_THREAD_EXIT:
+41
View File
@@ -265,6 +265,29 @@ namespace montauk::abi {
static constexpr uint64_t SYS_BTBONDS = 138;
static constexpr uint64_t SYS_BTFORGET = 139;
/* Sdr.hpp -- software-defined radio receive API */
static constexpr uint64_t SYS_SDR_COUNT = 140; // number of receivers
static constexpr uint64_t SYS_SDR_INFO = 141; // (index, SdrDeviceInfo*)
static constexpr uint64_t SYS_SDR_OPEN = 142; // (index) -> handle
static constexpr uint64_t SYS_SDR_CLOSE = 143; // (handle)
static constexpr uint64_t SYS_SDR_START = 144; // (handle) begin streaming
static constexpr uint64_t SYS_SDR_STOP = 145; // (handle) stop streaming
static constexpr uint64_t SYS_SDR_READ = 146; // (handle, buf, len) -> bytes
static constexpr uint64_t SYS_SDR_SETPARAM = 147; // (handle, param, value)
static constexpr uint64_t SYS_SDR_GETPARAM = 148; // (handle, param) -> value
// Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM).
static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz
static constexpr int SDR_PARAM_SAMPLE_RATE = 1; // sample rate, Hz
static constexpr int SDR_PARAM_GAIN_MODE = 2; // 0 = auto/AGC, 1 = manual
static constexpr int SDR_PARAM_GAIN = 3; // tuner gain, tenths of dB
static constexpr int SDR_PARAM_FREQ_CORR = 4; // frequency correction, ppm
static constexpr int SDR_PARAM_AGC = 5; // demod digital AGC, 0/1
static constexpr int SDR_PARAM_DIRECT_SAMP = 6; // direct sampling: 0=off,1=I,2=Q
// Sample formats reported in SdrDeviceInfo.sampleFormat.
static constexpr uint8_t SDR_FORMAT_CU8 = 0; // 8-bit unsigned interleaved I/Q
// Graceful power-off request actions (SYS_POWER_REQUEST). The desktop posts
// a pending action and exits; login.elf reads it, runs the shutdown stages,
// then issues the matching SYS_SHUTDOWN / SYS_RESET.
@@ -521,6 +544,24 @@ namespace montauk::abi {
uint8_t _pad[2];
};
// Software-defined radio receiver description (returned by SYS_SDR_INFO).
struct SdrDeviceInfo {
char name[64]; // e.g. "Realtek RTL2832U"
char tuner[32]; // e.g. "Rafael Micro R820T2"
char serial[32]; // device serial / bus location
uint64_t freqMin; // minimum tunable center frequency, Hz
uint64_t freqMax; // maximum tunable center frequency, Hz
uint32_t sampleRateMin; // minimum sample rate, Hz
uint32_t sampleRateMax; // maximum sample rate, Hz
uint32_t numGains; // number of discrete tuner gain steps
int32_t gains[32]; // available gains, tenths of dB
uint8_t sampleFormat; // SDR_FORMAT_*
uint8_t present; // 1 if the underlying hardware is connected
uint8_t streaming; // 1 if currently delivering samples
uint8_t _pad;
uint32_t _pad2;
};
struct ThermalInfo {
char name[32]; // short zone name (e.g. "THRM", "TZ00")
int32_t temperature; // tenths of degrees Celsius, or -1 if unavailable
+351
View File
@@ -0,0 +1,351 @@
/*
* Sdr.cpp
* Generic software-defined radio receive subsystem.
* Copyright (c) 2026 Daniel Hammer
*/
#include "Sdr.hpp"
#include <Memory/Heap.hpp>
#include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
using namespace Kt;
namespace Drivers::Radio::Sdr {
// I/Q ring size per receiver. 256 KiB is ~62 ms of jitter buffer at
// 2.048 Msps (2 bytes/sample), which comfortably absorbs scheduling gaps
// between a userspace reader's polls.
static constexpr uint32_t RING_BYTES = 256 * 1024;
struct Receiver {
bool used;
bool opened;
bool streaming;
char name[64];
char tuner[32];
char serial[32];
uint64_t freqMin, freqMax;
uint32_t sampleRateMin, sampleRateMax;
int gains[MAX_GAINS];
uint32_t numGains;
uint8_t format;
ReceiverOps ops;
void* ctx;
// Last-requested configuration (cached for GETPARAM readback).
uint64_t freq;
uint32_t sampleRate;
int gainMode; // 0 = auto, 1 = manual
int gain; // tenths of dB
int ppm;
int agc;
int directSamp;
// I/Q ring buffer (byte FIFO).
uint8_t* ring;
uint32_t head; // write position
uint32_t count; // bytes currently queued
uint64_t totalBytes; // lifetime sample bytes delivered
uint64_t droppedBytes; // bytes dropped on overflow
kcp::Spinlock lock;
};
static Receiver g_rx[MAX_RECEIVERS];
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
static void CopyStr(char* dst, uint32_t cap, const char* src) {
uint32_t i = 0;
if (src) {
for (; i < cap - 1 && src[i]; i++) dst[i] = src[i];
}
dst[i] = '\0';
}
static Receiver* Lookup(int handle, bool needOpen) {
if (handle < 0 || handle >= MAX_RECEIVERS) return nullptr;
Receiver& r = g_rx[handle];
if (!r.used) return nullptr;
if (needOpen && !r.opened) return nullptr;
return &r;
}
// -------------------------------------------------------------------------
// Driver-facing API
// -------------------------------------------------------------------------
int Register(const ReceiverDesc& desc) {
for (int i = 0; i < MAX_RECEIVERS; i++) {
if (g_rx[i].used) continue;
Receiver& r = g_rx[i];
// Reset everything except the (non-copyable) spinlock instance.
r.opened = false;
r.streaming = false;
CopyStr(r.name, sizeof(r.name), desc.name);
CopyStr(r.tuner, sizeof(r.tuner), desc.tuner);
CopyStr(r.serial, sizeof(r.serial), desc.serial);
r.freqMin = desc.freqMin;
r.freqMax = desc.freqMax;
r.sampleRateMin = desc.sampleRateMin;
r.sampleRateMax = desc.sampleRateMax;
r.numGains = desc.numGains > MAX_GAINS ? MAX_GAINS : desc.numGains;
for (uint32_t g = 0; g < r.numGains; g++) r.gains[g] = desc.gains[g];
r.format = desc.format;
r.ops = desc.ops;
r.ctx = desc.ctx;
r.freq = (desc.freqMin + desc.freqMax) / 2;
r.sampleRate = desc.sampleRateMax;
r.gainMode = 0;
r.gain = 0;
r.ppm = 0;
r.agc = 0;
r.directSamp = 0;
r.ring = nullptr;
r.head = r.count = 0;
r.totalBytes = r.droppedBytes = 0;
r.used = true; // publish last
KernelLogStream(OK, "SDR") << "Registered receiver " << (uint64_t)i
<< ": " << r.name << " / " << r.tuner;
return i;
}
KernelLogStream(WARNING, "SDR") << "No free receiver slot for " << desc.name;
return -1;
}
void Unregister(int idx) {
if (idx < 0 || idx >= MAX_RECEIVERS) return;
Receiver& r = g_rx[idx];
if (!r.used) return;
if (r.streaming && r.ops.Stop) r.ops.Stop(r.ctx);
r.lock.Acquire();
r.streaming = false;
r.opened = false;
r.used = false;
uint8_t* ring = r.ring;
r.ring = nullptr;
r.head = r.count = 0;
r.lock.Release();
if (ring) Memory::g_heap->Free(ring);
KernelLogStream(INFO, "SDR") << "Unregistered receiver " << (uint64_t)idx;
}
void PushSamples(int idx, const uint8_t* data, uint32_t len) {
if (idx < 0 || idx >= MAX_RECEIVERS || !data || len == 0) return;
Receiver& r = g_rx[idx];
r.lock.Acquire();
// Re-validate under the lock: Unregister() clears these and frees the
// ring while holding the same lock, so an in-flight USB completion can
// never write into a freed buffer.
if (!r.used || !r.ring) { r.lock.Release(); return; }
uint32_t space = RING_BYTES - r.count;
uint32_t n = len;
uint32_t dropped = 0;
if (n > space) { dropped = n - space; n = space; }
uint32_t first = RING_BYTES - r.head;
if (first > n) first = n;
memcpy(r.ring + r.head, data, first);
if (n > first) memcpy(r.ring, data + first, n - first);
r.head = (r.head + n) % RING_BYTES;
r.count += n;
r.totalBytes += n;
r.droppedBytes += dropped;
r.lock.Release();
}
bool IsStreaming(int idx) {
if (idx < 0 || idx >= MAX_RECEIVERS) return false;
return g_rx[idx].used && g_rx[idx].streaming;
}
// -------------------------------------------------------------------------
// Syscall-facing API
// -------------------------------------------------------------------------
int Count() {
int n = 0;
for (int i = 0; i < MAX_RECEIVERS; i++) if (g_rx[i].used) n++;
return n;
}
bool GetInfo(int idx, montauk::abi::SdrDeviceInfo* out) {
Receiver* r = Lookup(idx, false);
if (!r || !out) return false;
memset(out, 0, sizeof(*out));
CopyStr(out->name, sizeof(out->name), r->name);
CopyStr(out->tuner, sizeof(out->tuner), r->tuner);
CopyStr(out->serial, sizeof(out->serial), r->serial);
out->freqMin = r->freqMin;
out->freqMax = r->freqMax;
out->sampleRateMin = r->sampleRateMin;
out->sampleRateMax = r->sampleRateMax;
out->numGains = r->numGains;
for (uint32_t g = 0; g < r->numGains && g < 32; g++) out->gains[g] = r->gains[g];
out->sampleFormat = r->format;
out->present = 1;
out->streaming = r->streaming ? 1 : 0;
return true;
}
int Open(int idx) {
Receiver* r = Lookup(idx, false);
if (!r) return -1;
// Single-user OS: an Open always claims the device, reclaiming it from a
// previous owner that exited without closing.
if (r->streaming && r->ops.Stop) r->ops.Stop(r->ctx);
if (!r->ring) {
r->ring = (uint8_t*)Memory::g_heap->Request(RING_BYTES);
if (!r->ring) {
KernelLogStream(ERROR, "SDR") << "Ring alloc failed for receiver "
<< (uint64_t)idx;
return -1;
}
}
r->lock.Acquire();
r->head = r->count = 0;
r->lock.Release();
r->streaming = false;
r->opened = true;
return idx; // handle == index
}
int Close(int handle) {
Receiver* r = Lookup(handle, true);
if (!r) return -1;
if (r->streaming && r->ops.Stop) r->ops.Stop(r->ctx);
r->streaming = false;
r->opened = false;
return 0;
}
int Start(int handle) {
Receiver* r = Lookup(handle, true);
if (!r) return -1;
r->lock.Acquire();
r->head = r->count = 0; // discard stale samples before (re)starting
r->lock.Release();
int rc = r->ops.Start ? r->ops.Start(r->ctx) : -1;
if (rc == 0) r->streaming = true;
return rc;
}
int Stop(int handle) {
Receiver* r = Lookup(handle, true);
if (!r) return -1;
int rc = r->ops.Stop ? r->ops.Stop(r->ctx) : 0;
r->streaming = false;
return rc;
}
int Read(int handle, uint8_t* buf, uint32_t len) {
Receiver* r = Lookup(handle, true);
if (!r || !buf || !r->ring) return -1;
if (len == 0) return 0;
// Give the driver a process-context tick (e.g. USB stall recovery)
// before draining; do this outside the ring lock since it may issue
// blocking USB commands.
if (r->streaming && r->ops.Service) r->ops.Service(r->ctx);
r->lock.Acquire();
uint32_t n = r->count < len ? r->count : len;
uint32_t tail = (r->head + RING_BYTES - r->count) % RING_BYTES;
uint32_t first = RING_BYTES - tail;
if (first > n) first = n;
memcpy(buf, r->ring + tail, first);
if (n > first) memcpy(buf + first, r->ring, n - first);
r->count -= n;
r->lock.Release();
return (int)n;
}
uint32_t Available(int handle) {
Receiver* r = Lookup(handle, true);
if (!r) return 0;
return r->count;
}
int64_t SetParam(int handle, int param, uint64_t value) {
Receiver* r = Lookup(handle, true);
if (!r) return -1;
switch (param) {
case montauk::abi::SDR_PARAM_FREQ:
if (!r->ops.SetFreq) return -1;
if (r->ops.SetFreq(r->ctx, value) != 0) return -1;
r->freq = value;
return 0;
case montauk::abi::SDR_PARAM_SAMPLE_RATE:
if (!r->ops.SetSampleRate) return -1;
if (r->ops.SetSampleRate(r->ctx, (uint32_t)value) != 0) return -1;
r->sampleRate = (uint32_t)value;
return 0;
case montauk::abi::SDR_PARAM_GAIN_MODE:
if (!r->ops.SetGainMode) return -1;
if (r->ops.SetGainMode(r->ctx, (int)value) != 0) return -1;
r->gainMode = (int)value ? 1 : 0;
return 0;
case montauk::abi::SDR_PARAM_GAIN:
if (!r->ops.SetGain) return -1;
if (r->ops.SetGain(r->ctx, (int)(int64_t)value) != 0) return -1;
r->gain = (int)(int64_t)value;
return 0;
case montauk::abi::SDR_PARAM_FREQ_CORR:
if (!r->ops.SetFreqCorrection) return -1;
if (r->ops.SetFreqCorrection(r->ctx, (int)(int64_t)value) != 0) return -1;
r->ppm = (int)(int64_t)value;
return 0;
case montauk::abi::SDR_PARAM_AGC:
if (!r->ops.SetAgc) return -1;
if (r->ops.SetAgc(r->ctx, (int)value) != 0) return -1;
r->agc = (int)value ? 1 : 0;
return 0;
case montauk::abi::SDR_PARAM_DIRECT_SAMP:
if (!r->ops.SetDirectSampling) return -1;
if (r->ops.SetDirectSampling(r->ctx, (int)value) != 0) return -1;
r->directSamp = (int)value;
return 0;
default:
return -1;
}
}
int64_t GetParam(int handle, int param) {
Receiver* r = Lookup(handle, true);
if (!r) return -1;
switch (param) {
case montauk::abi::SDR_PARAM_FREQ: return (int64_t)r->freq;
case montauk::abi::SDR_PARAM_SAMPLE_RATE: return (int64_t)r->sampleRate;
case montauk::abi::SDR_PARAM_GAIN_MODE: return r->gainMode;
case montauk::abi::SDR_PARAM_GAIN: return r->gain;
case montauk::abi::SDR_PARAM_FREQ_CORR: return r->ppm;
case montauk::abi::SDR_PARAM_AGC: return r->agc;
case montauk::abi::SDR_PARAM_DIRECT_SAMP: return r->directSamp;
default: return -1;
}
}
}
+119
View File
@@ -0,0 +1,119 @@
/*
* Sdr.hpp
* Generic software-defined radio (SDR) receive subsystem.
*
* Hardware-agnostic registry of radio receivers. A concrete driver (e.g. the
* RTL-SDR USB driver) registers itself as a receiver by supplying an ops table
* and a private context pointer; it then pushes demodulated baseband I/Q
* samples into a per-receiver ring buffer via PushSamples(). Userspace reaches
* this layer through the SYS_SDR_* syscalls and drains the ring with Read().
*
* The native sample format is CU8 -- 8-bit unsigned interleaved I/Q -- which is
* what the RTL2832U produces; other formats can be advertised per receiver via
* SdrDeviceInfo.sampleFormat.
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <CppLib/Spinlock.hpp>
#include <Api/Syscall.hpp>
namespace Drivers::Radio::Sdr {
static constexpr int MAX_RECEIVERS = 4;
static constexpr int MAX_GAINS = 32;
// -------------------------------------------------------------------------
// Receiver ops table -- implemented by a concrete driver.
// All calls happen in process/syscall context (never from the sample
// callback), so they may block on USB control transfers. Each returns 0 on
// success, negative on error. ctx is the receiver's private pointer.
// -------------------------------------------------------------------------
struct ReceiverOps {
int (*SetFreq)(void* ctx, uint64_t hz);
int (*SetSampleRate)(void* ctx, uint32_t hz);
int (*SetGainMode)(void* ctx, int manual); // 0 = auto/AGC, 1 = manual
int (*SetGain)(void* ctx, int tenthsDb);
int (*SetFreqCorrection)(void* ctx, int ppm);
int (*SetAgc)(void* ctx, int on); // demod digital AGC
int (*SetDirectSampling)(void* ctx, int mode); // 0=off,1=I,2=Q
int (*Start)(void* ctx); // arm streaming
int (*Stop)(void* ctx); // halt streaming
// Optional: process-context housekeeping invoked from Read() while
// streaming (e.g. USB stall recovery that cannot run in the ISR). May
// be null.
void (*Service)(void* ctx);
};
// Static description a driver supplies at registration time.
struct ReceiverDesc {
const char* name; // e.g. "Realtek RTL2832U"
const char* tuner; // e.g. "Rafael Micro R820T2"
const char* serial; // bus location / serial string (may be null)
uint64_t freqMin; // Hz
uint64_t freqMax; // Hz
uint32_t sampleRateMin;
uint32_t sampleRateMax;
const int* gains; // table of tenths-of-dB gain steps (may be null)
uint32_t numGains;
uint8_t format; // montauk::abi::SDR_FORMAT_*
ReceiverOps ops;
void* ctx;
};
// =========================================================================
// Driver-facing API
// =========================================================================
// Register a receiver. Returns its index [0, MAX_RECEIVERS) or -1 if full.
int Register(const ReceiverDesc& desc);
// Remove a receiver (e.g. on USB unplug). Stops streaming and frees the
// ring. Safe to call with an out-of-range / already-removed index.
void Unregister(int idx);
// Push baseband sample bytes into a receiver's ring buffer. Called from the
// driver's USB completion callback (possibly interrupt context); never
// allocates or blocks. Bytes that do not fit are dropped (counted).
void PushSamples(int idx, const uint8_t* data, uint32_t len);
// True if the receiver is currently in the streaming state (used by drivers
// to decide whether to re-arm USB transfers).
bool IsStreaming(int idx);
// =========================================================================
// Syscall-facing API
// =========================================================================
// Number of registered receivers.
int Count();
// Fill out an info struct for receiver idx. Returns false if idx invalid.
bool GetInfo(int idx, montauk::abi::SdrDeviceInfo* out);
// Claim a receiver for use. Returns a handle (== idx) or -1 on failure.
int Open(int idx);
// Release a receiver (stops streaming). Returns 0 on success.
int Close(int handle);
// Begin / end sample delivery. Returns 0 on success, negative on error.
int Start(int handle);
int Stop(int handle);
// Copy up to len bytes of buffered I/Q out of the ring. Non-blocking;
// returns the number of bytes copied (0 when nothing is queued).
int Read(int handle, uint8_t* buf, uint32_t len);
// Number of sample bytes currently queued in the ring.
uint32_t Available(int handle);
// Set / get a tunable parameter (montauk::abi::SDR_PARAM_*). SetParam
// returns 0 on success; GetParam returns the cached value or negative on
// error.
int64_t SetParam(int handle, int param, uint64_t value);
int64_t GetParam(int handle, int param);
}
+370
View File
@@ -0,0 +1,370 @@
/*
* R820t.cpp
* Rafael Micro R820T / R820T2 silicon tuner driver.
* Copyright (c) 2026 Daniel Hammer
*/
#include "R820t.hpp"
#include "RtlSdr.hpp" // I2C facade (RtlI2cWrite / RtlI2cRead)
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
using namespace Kt;
namespace Drivers::USB::Radio {
// =========================================================================
// Tuner constant tables
// =========================================================================
// Initial register values for registers 0x05..0x1f (27 registers). Written
// verbatim at init; the shadow cache then tracks read-modify-writes.
static const uint8_t kInitArray[R820T_NUM_REGS] = {
0x83, 0x32, 0x75, // 0x05 - 0x07
0xc0, 0x40, 0xd6, 0x6c, // 0x08 - 0x0b
0xf5, 0x63, 0x75, 0x68, // 0x0c - 0x0f
0x6c, 0x83, 0x80, 0x00, // 0x10 - 0x13
0x0f, 0x00, 0xc0, 0x30, // 0x14 - 0x17
0x48, 0xcc, 0x60, 0x00, // 0x18 - 0x1b
0x54, 0xae, 0x4a, 0xc0 // 0x1c - 0x1f
};
// RF tracking-filter / mux band selection, keyed by LO frequency in MHz.
struct FreqRange {
uint32_t freqMhz;
uint8_t openD; // reg 0x17 bit 3 (open drain)
uint8_t rfMuxPoly; // reg 0x1a bits (RF mux + poly)
uint8_t tfC; // reg 0x1b (tracking filter band)
};
static const FreqRange kFreqRanges[] = {
{ 0, 0x08, 0x02, 0xdf },
{ 50, 0x08, 0x02, 0xbe },
{ 55, 0x08, 0x02, 0x8b },
{ 60, 0x08, 0x02, 0x7b },
{ 65, 0x08, 0x02, 0x69 },
{ 70, 0x08, 0x02, 0x58 },
{ 75, 0x00, 0x02, 0x44 },
{ 80, 0x00, 0x02, 0x44 },
{ 90, 0x00, 0x02, 0x34 },
{ 100, 0x00, 0x02, 0x34 },
{ 110, 0x00, 0x02, 0x24 },
{ 120, 0x00, 0x02, 0x24 },
{ 140, 0x00, 0x02, 0x14 },
{ 180, 0x00, 0x02, 0x13 },
{ 220, 0x00, 0x02, 0x13 },
{ 250, 0x00, 0x02, 0x11 },
{ 280, 0x00, 0x02, 0x00 },
{ 310, 0x00, 0x41, 0x00 },
{ 450, 0x00, 0x41, 0x00 },
{ 588, 0x00, 0x40, 0x00 },
{ 650, 0x00, 0x40, 0x00 },
};
// Composite gain steps (tenths of dB) reachable by walking the LNA + mixer
// gain stages together. Advertised to userspace as the discrete gain set.
static const int kGains[] = {
0, 9, 14, 27, 37, 77, 87, 125, 144, 157,
166, 197, 207, 229, 254, 280, 297, 328,
338, 364, 372, 386, 402, 421, 434, 439,
445, 480, 496,
};
// Per-index increments of the LNA and mixer gain stages (tenths of dB).
static const int kLnaGainSteps[] = { 0, 9, 13, 40, 38, 13, 31, 22,
26, 31, 26, 14, 19, 5, 35, 13 };
static const int kMixerGainSteps[] = { 0, 5, 10, 10, 19, 9, 10, 25,
17, 10, 8, 16, 13, 6, 3, -8 };
// =========================================================================
// Low-level register access (through the demod's I2C repeater)
// =========================================================================
static uint8_t BitRev(uint8_t b) {
static const uint8_t lut[16] = {
0x0, 0x8, 0x4, 0xc, 0x2, 0xa, 0x6, 0xe,
0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf,
};
return (uint8_t)((lut[b & 0xf] << 4) | lut[b >> 4]);
}
// Write `len` register values starting at `reg`. The RTL2832 I2C path is
// chunked to 7 values per transfer (8-byte message incl. the start reg).
static bool Write(R820tDev& d, uint8_t reg, const uint8_t* val, uint8_t len) {
uint8_t buf[8];
uint8_t pos = 0;
while (len > 0) {
uint8_t size = len > 7 ? 7 : len;
buf[0] = reg;
for (uint8_t i = 0; i < size; i++) buf[1 + i] = val[pos + i];
if (!RtlI2cWrite(d.slotId, R820T_I2C_ADDR, buf, (uint8_t)(size + 1)))
return false;
for (uint8_t i = 0; i < size; i++)
if ((reg + i) < 32) d.regs[reg + i] = val[pos + i];
pos += size;
reg = (uint8_t)(reg + size);
len = (uint8_t)(len - size);
}
return true;
}
static bool WriteReg(R820tDev& d, uint8_t reg, uint8_t val) {
return Write(d, reg, &val, 1);
}
// Read-modify-write using the register shadow as the source of truth for the
// bits outside `mask`.
static bool WriteRegMask(R820tDev& d, uint8_t reg, uint8_t val, uint8_t mask) {
uint8_t cur = (reg < 32) ? d.regs[reg] : 0;
uint8_t merged = (uint8_t)((cur & ~mask) | (val & mask));
return WriteReg(d, reg, merged);
}
// Read `len` bytes; the R820T always returns starting at register 0. The
// status registers are bit-reversed on the wire, so the PLL/VCO read paths
// un-reverse each byte to recover the logical value.
static bool Read(R820tDev& d, uint8_t* out, uint8_t len) {
uint8_t raw[16];
if (len > sizeof(raw)) len = sizeof(raw);
if (!RtlI2cRead(d.slotId, R820T_I2C_ADDR, raw, len)) return false;
for (uint8_t i = 0; i < len; i++) out[i] = BitRev(raw[i]);
return true;
}
// =========================================================================
// Detection / init
// =========================================================================
bool R820tDetect(uint8_t slotId) {
// Match the reference driver's chip-id probe: set the read pointer to
// register 0, then read one byte *without* bit-reversal and compare it
// to the raw R820T id. (The bit-reversal only applies to the status
// registers read during tuning, not to this id check.)
uint8_t ptr = 0x00;
RtlI2cWrite(slotId, R820T_I2C_ADDR, &ptr, 1);
uint8_t raw[1] = {0};
if (!RtlI2cRead(slotId, R820T_I2C_ADDR, raw, 1)) {
KernelLogStream(WARNING, "R820T") << "id read failed (I2C transfer error)";
return false;
}
KernelLogStream(INFO, "R820T") << "chip id = 0x" << base::hex
<< (uint64_t)raw[0] << base::dec
<< (raw[0] == R820T_CHECK_VAL ? " (R820T/R820T2)" : " (unrecognised)");
return raw[0] == R820T_CHECK_VAL;
}
bool R820tInit(R820tDev& d, uint8_t slotId, uint32_t xtal, uint32_t intFreq) {
d.slotId = slotId;
d.xtal = xtal;
d.intFreq = intFreq;
d.hasLock = false;
d.inited = false;
// Load the init register block. The IF low-pass filter is left at the
// init-array default (widest practical for the SDR receive path); a
// dedicated bandwidth calibration is not performed.
if (!Write(d, 0x05, kInitArray, sizeof(kInitArray))) {
KernelLogStream(ERROR, "R820T") << "init register write failed";
return false;
}
d.inited = true;
KernelLogStream(OK, "R820T") << "Tuner initialised (xtal="
<< (uint64_t)xtal << " IF=" << (uint64_t)intFreq << ")";
return true;
}
// =========================================================================
// RF mux / tracking filter band
// =========================================================================
static bool SetMux(R820tDev& d, uint64_t loHz) {
uint32_t loMhz = (uint32_t)(loHz / 1000000);
unsigned idx = 0;
const unsigned n = sizeof(kFreqRanges) / sizeof(kFreqRanges[0]);
for (; idx < n - 1; idx++) {
if (loMhz < kFreqRanges[idx + 1].freqMhz) break;
}
const FreqRange& r = kFreqRanges[idx];
bool ok = true;
ok &= WriteRegMask(d, 0x17, r.openD, 0x08); // open drain
ok &= WriteRegMask(d, 0x1a, r.rfMuxPoly, 0xc3); // RF mux + poly
ok &= WriteReg(d, 0x1b, r.tfC); // tracking-filter band
// Default XTAL cap (high-cap-0p selection) and the unused LNA/mixer
// top registers.
ok &= WriteRegMask(d, 0x10, 0x00, 0x0b);
ok &= WriteRegMask(d, 0x08, 0x00, 0x3f);
ok &= WriteRegMask(d, 0x09, 0x00, 0x3f);
return ok;
}
// =========================================================================
// PLL / VCO frequency synthesis
// =========================================================================
static bool SetPll(R820tDev& d, uint64_t freqHz) {
const uint32_t vcoMinKhz = 1770000; // 1.77 GHz
const uint32_t vcoMaxKhz = vcoMinKhz * 2; // 3.54 GHz
uint32_t pllRef = d.xtal;
uint32_t pllRefKhz = (d.xtal + 500) / 1000;
uint32_t freqKhz = (uint32_t)((freqHz + 500) / 1000);
bool ok = true;
ok &= WriteRegMask(d, 0x10, 0x00, 0x10); // refdiv = /1
ok &= WriteRegMask(d, 0x1a, 0x00, 0x0c); // pll autotune 128 kHz
ok &= WriteRegMask(d, 0x12, 0x80, 0xe0); // VCO current = 100
// Pick the smallest mixer divider that lands the VCO in range.
uint8_t mixDiv = 2;
uint8_t divNum = 0;
while (mixDiv <= 64) {
if ((uint64_t)freqKhz * mixDiv >= vcoMinKhz &&
(uint64_t)freqKhz * mixDiv < vcoMaxKhz) {
uint8_t divBuf = mixDiv;
while (divBuf > 2) { divBuf >>= 1; divNum++; }
break;
}
mixDiv <<= 1;
}
// VCO fine-tune feedback adjusts the divider selection.
uint8_t data[5] = {0};
if (!Read(d, data, sizeof(data))) return false;
uint8_t vcoPowerRef = 2;
uint8_t vcoFineTune = (uint8_t)((data[4] & 0x30) >> 4);
if (vcoFineTune > vcoPowerRef && divNum > 0) divNum--;
else if (vcoFineTune < vcoPowerRef) divNum++;
ok &= WriteRegMask(d, 0x10, (uint8_t)(divNum << 5), 0xe0);
uint64_t vcoFreq = freqHz * (uint64_t)mixDiv;
uint32_t nint = (uint32_t)(vcoFreq / (2u * pllRef));
uint32_t vcoFra = (uint32_t)((vcoFreq - 2ull * pllRef * nint) / 1000);
if (nint < 13) nint = 13; // keep ni/si arithmetic well-defined
uint8_t ni = (uint8_t)((nint - 13) / 4);
uint8_t si = (uint8_t)(nint - 4 * ni - 13);
ok &= WriteReg(d, 0x14, (uint8_t)(ni + (si << 6)));
// Sigma-delta fractional path.
ok &= WriteRegMask(d, 0x12, vcoFra ? 0x00 : 0x08, 0x08);
uint16_t nSdm = 2;
uint16_t sdm = 0;
while (vcoFra > 1) {
uint32_t step = 2 * pllRefKhz / nSdm;
if (vcoFra > step) {
sdm = (uint16_t)(sdm + 32768 / (nSdm / 2));
vcoFra -= step;
if (nSdm >= 0x8000) break;
}
nSdm <<= 1;
}
ok &= WriteReg(d, 0x16, (uint8_t)(sdm >> 8));
ok &= WriteReg(d, 0x15, (uint8_t)(sdm & 0xff));
// Confirm lock; bump the VCO current once if the first read is not
// locked.
d.hasLock = false;
uint8_t lk[3] = {0};
for (int i = 0; i < 2; i++) {
if (!Read(d, lk, sizeof(lk))) {
KernelLogStream(WARNING, "R820T") << "PLL status read failed";
return false;
}
if (lk[2] & 0x40) { d.hasLock = true; break; }
if (i == 0) WriteRegMask(d, 0x12, 0x60, 0xe0); // raise VCO current
}
KernelLogStream(INFO, "R820T") << "PLL nint=" << (uint64_t)nint
<< " sdm=0x" << base::hex << (uint64_t)sdm
<< " mixDiv=" << base::dec << (uint64_t)mixDiv
<< " status=0x" << base::hex << (uint64_t)lk[2] << base::dec
<< (d.hasLock ? " LOCKED" : " UNLOCKED");
ok &= WriteRegMask(d, 0x1a, 0x08, 0x08); // pll autotune 8 kHz
return ok && d.hasLock;
}
// =========================================================================
// Public tuner control
// =========================================================================
bool R820tSetFreq(R820tDev& d, uint64_t rfHz) {
if (!d.inited) {
KernelLogStream(WARNING, "R820T") << "SetFreq: tuner not inited";
return false;
}
uint64_t loHz = rfHz + d.intFreq;
if (!SetMux(d, loHz)) {
KernelLogStream(WARNING, "R820T") << "SetFreq: mux write failed";
return false;
}
if (!SetPll(d, loHz)) {
KernelLogStream(WARNING, "R820T") << "PLL not locked at "
<< (uint64_t)(rfHz / 1000) << " kHz";
return false;
}
// Air-in vs Cable-1 input select crosses over at 345 MHz.
uint8_t airCable1In = (rfHz > 345000000ull) ? 0x00 : 0x60;
WriteRegMask(d, 0x05, airCable1In, 0x60);
return true;
}
bool R820tSetGain(R820tDev& d, int manual, int tenthsDb) {
if (!d.inited) {
KernelLogStream(WARNING, "R820T") << "SetGain: tuner not inited";
return false;
}
bool ok = true;
if (manual) {
// LNA + mixer to manual; VGA to a fixed mid value.
ok &= WriteRegMask(d, 0x05, 0x10, 0x10); // LNA AGC off
ok &= WriteRegMask(d, 0x07, 0x00, 0x10); // mixer AGC off
ok &= WriteRegMask(d, 0x0c, 0x08, 0x9f); // VGA = 16.3 dB
int total = 0;
uint8_t lnaIndex = 0, mixIndex = 0;
for (int i = 0; i < 15; i++) {
if (total >= tenthsDb) break;
total += kLnaGainSteps[++lnaIndex];
if (total >= tenthsDb) break;
total += kMixerGainSteps[++mixIndex];
}
ok &= WriteRegMask(d, 0x05, lnaIndex, 0x0f);
ok &= WriteRegMask(d, 0x07, mixIndex, 0x0f);
} else {
ok &= WriteRegMask(d, 0x05, 0x00, 0x10); // LNA AGC on
ok &= WriteRegMask(d, 0x07, 0x10, 0x10); // mixer AGC on
ok &= WriteRegMask(d, 0x0c, 0x0b, 0x9f); // VGA = 26.5 dB
}
return ok;
}
void R820tStandby(R820tDev& d) {
if (!d.inited) return;
// Documented R820T standby register values (mute + power-down).
WriteReg(d, 0x06, 0xb1);
WriteReg(d, 0x05, 0xa0);
WriteReg(d, 0x07, 0x3a);
WriteReg(d, 0x08, 0x40);
WriteReg(d, 0x09, 0xc0);
WriteReg(d, 0x0a, 0x36);
WriteReg(d, 0x0c, 0x35);
WriteReg(d, 0x0f, 0x68);
WriteReg(d, 0x11, 0x03);
WriteReg(d, 0x17, 0xf4);
WriteReg(d, 0x19, 0x0c);
}
const int* R820tGainTable(int* count) {
if (count) *count = (int)(sizeof(kGains) / sizeof(kGains[0]));
return kGains;
}
}
+61
View File
@@ -0,0 +1,61 @@
/*
* R820t.hpp
* Rafael Micro R820T / R820T2 silicon tuner.
*
* The tuner sits on the RTL2832U's I2C bus; all register traffic is carried
* by the demod's I2C repeater (managed by the RtlSdr layer, which enables the
* repeater around every call here). This module owns the tuner-side logic:
* the init register array, the PLL/VCO frequency synthesis, the RF tracking
* filter / mux band selection, and the LNA/Mixer/VGA gain stages.
*
* Register/algorithm facts follow the publicly documented R820T programming
* model (as used by osmocom rtl-sdr); the implementation here is original.
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Drivers::USB::Radio {
// I2C bus address of the tuner on the RTL2832U (8-bit form).
static constexpr uint8_t R820T_I2C_ADDR = 0x34;
// Chip-id register (reg 0) reads back this value for an R820T/R820T2.
static constexpr uint8_t R820T_CHECK_VAL = 0x69;
// First writable register; the 27-entry shadow covers regs 0x05..0x1f.
static constexpr uint8_t R820T_REG_SHADOW_START = 5;
static constexpr uint8_t R820T_NUM_REGS = 27;
struct R820tDev {
uint8_t slotId;
uint32_t xtal; // reference crystal, Hz (28.8 MHz on RTL-SDR)
uint32_t intFreq; // IF the demod expects the signal at, Hz (3.57 MHz)
uint8_t regs[32]; // register shadow (index == register number)
bool hasLock; // PLL lock state after the last tune
bool inited;
};
// Detect an R820T/R820T2 on the demod I2C bus. The caller must have the
// demod's I2C repeater enabled. Returns true if the chip id matches.
bool R820tDetect(uint8_t slotId);
// Initialise the tuner (writes the init register array + base setup). The
// caller must have the I2C repeater enabled. Returns true on success.
bool R820tInit(R820tDev& d, uint8_t slotId, uint32_t xtal, uint32_t intFreq);
// Tune to an RF center frequency (Hz). Programs the RF mux band and the PLL
// for an LO of rfHz + intFreq. Updates d.hasLock. Repeater must be on.
bool R820tSetFreq(R820tDev& d, uint64_t rfHz);
// Configure gain. manual==0 puts LNA/mixer in AGC; manual!=0 selects the
// closest fixed gain to tenthsDb from the LNA+mixer step tables.
bool R820tSetGain(R820tDev& d, int manual, int tenthsDb);
// Put the tuner into standby (mute / power down).
void R820tStandby(R820tDev& d);
// The discrete gain table (tenths of dB), for advertising to userspace.
const int* R820tGainTable(int* count);
}
+540
View File
@@ -0,0 +1,540 @@
/*
* RtlSdr.cpp
* Realtek RTL2832U + R820T2 SDR receiver driver.
* Copyright (c) 2026 Daniel Hammer
*/
#include "RtlSdr.hpp"
#include "R820t.hpp"
#include <Drivers/Radio/Sdr.hpp>
#include <Drivers/USB/Xhci.hpp>
#include <Drivers/USB/UsbDevice.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Memory/HHDM.hpp>
#include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <CppLib/Spinlock.hpp>
#include <Api/Syscall.hpp>
#include <atomic>
using namespace Kt;
namespace Drivers::USB::Radio {
// =========================================================================
// Constants
// =========================================================================
// Vendor control-transfer request types (vendor, host<->device).
static constexpr uint8_t CTRL_OUT = 0x40; // host-to-device, vendor
static constexpr uint8_t CTRL_IN = 0xC0; // device-to-host, vendor
// RTL2832U register blocks (high byte of wIndex; OR 0x10 to write).
static constexpr uint8_t BLOCK_USB = 1;
static constexpr uint8_t BLOCK_SYS = 2;
static constexpr uint8_t BLOCK_IIC = 6;
// USB / system register addresses.
static constexpr uint16_t USB_EPA_CTL = 0x2148;
static constexpr uint16_t USB_EPA_MAXPKT = 0x2158;
static constexpr uint16_t USB_SYSCTL = 0x2000;
static constexpr uint16_t SYS_DEMOD_CTL = 0x3000;
static constexpr uint16_t SYS_DEMOD_CTL1 = 0x300b;
static constexpr uint32_t RTL_XTAL = 28800000; // 28.8 MHz reference
static constexpr uint32_t R82XX_IF = 3570000; // IF the demod expects
static constexpr uint32_t TWO_POW22 = 1u << 22;
// =========================================================================
// Driver state (single instance -- the common RTL-SDR case)
// =========================================================================
static bool g_present = false;
static bool g_hwInited = false;
static bool g_streaming = false;
static uint8_t g_slotId = 0;
static int g_rxIndex = -1;
static uint8_t* g_ctlBuf = nullptr; // HHDM page for control transfers
static kcp::Mutex g_ctlLock; // serialises register access
static R820tDev g_tuner{};
static uint32_t g_rtlXtal = RTL_XTAL; // adjusted by ppm correction
static int g_ppm = 0;
static int g_manual = 0; // tuner gain mode (0=auto)
static int g_gain = 0; // tuner gain, tenths of dB
// Bulk-IN streaming geometry. We keep BULK_POOL_BUFS transfers of
// BULK_XFER_LEN bytes outstanding at once (multi-URB), so the RTL2832U FIFO
// always has a TRB to DMA into and never overflows in the window between a
// completion and its re-arm -- the single-outstanding scheme dropped ~88% of
// samples at 2.048 Msps for exactly that reason. 4 KiB == one DMA page;
// 16 x 4 KiB == 64 KiB in flight, ~16 ms of slack at 4 MB/s.
static constexpr uint32_t BULK_XFER_LEN = 4096;
static constexpr uint32_t BULK_POOL_BUFS = 16;
// Set by the bulk-IN completion callback when the endpoint halts (cc=6
// STALL etc.). Recovery (Reset Endpoint) needs a command wait and so must
// run in process context -- serviced from Read() via OpService().
static std::atomic<bool> g_bulkStalled{false};
// =========================================================================
// Low-level register access (control transfers via EP0)
// =========================================================================
static bool RegWrite(uint8_t block, uint16_t addr, uint16_t val, uint8_t len) {
if (!g_ctlBuf) return false;
g_ctlBuf[0] = (len == 1) ? (uint8_t)(val & 0xff) : (uint8_t)(val >> 8);
g_ctlBuf[1] = (uint8_t)(val & 0xff);
uint16_t index = (uint16_t)((block << 8) | 0x10);
return Xhci::ControlTransfer(g_slotId, CTRL_OUT, 0, addr, index, len,
g_ctlBuf, false) == Xhci::CC_SUCCESS;
}
static bool DemodWrite(uint8_t page, uint16_t addr, uint16_t val, uint8_t len) {
if (!g_ctlBuf) return false;
uint16_t waddr = (uint16_t)((addr << 8) | 0x20);
uint16_t index = (uint16_t)(0x10 | page);
g_ctlBuf[0] = (len == 1) ? (uint8_t)(val & 0xff) : (uint8_t)(val >> 8);
g_ctlBuf[1] = (uint8_t)(val & 0xff);
return Xhci::ControlTransfer(g_slotId, CTRL_OUT, 0, waddr, index, len,
g_ctlBuf, false) == Xhci::CC_SUCCESS;
}
static void SetI2cRepeater(bool on) {
DemodWrite(1, 0x01, on ? 0x18 : 0x10, 1);
}
// =========================================================================
// I2C facade for the tuner module
// =========================================================================
bool RtlI2cWrite(uint8_t slotId, uint8_t i2cAddr, const uint8_t* buf, uint8_t len) {
if (!g_ctlBuf || len == 0 || len > 64) return false;
memcpy(g_ctlBuf, buf, len);
uint16_t index = (uint16_t)((BLOCK_IIC << 8) | 0x10);
uint32_t cc = Xhci::ControlTransfer(slotId, CTRL_OUT, 0, i2cAddr, index, len,
g_ctlBuf, false);
if (cc != Xhci::CC_SUCCESS)
KernelLogStream(WARNING, "RTL-SDR") << "I2C write cc=" << (uint64_t)cc
<< " reg=0x" << base::hex << (uint64_t)buf[0]
<< " len=" << base::dec << (uint64_t)len;
return cc == Xhci::CC_SUCCESS;
}
bool RtlI2cRead(uint8_t slotId, uint8_t i2cAddr, uint8_t* buf, uint8_t len) {
if (!g_ctlBuf || len == 0 || len > 64) return false;
uint16_t index = (uint16_t)(BLOCK_IIC << 8);
uint32_t cc = Xhci::ControlTransfer(slotId, CTRL_IN, 0, i2cAddr, index, len,
g_ctlBuf, true);
if (cc != Xhci::CC_SUCCESS) {
KernelLogStream(WARNING, "RTL-SDR") << "I2C read cc=" << (uint64_t)cc
<< " len=" << (uint64_t)len;
return false;
}
memcpy(buf, g_ctlBuf, len);
return true;
}
// =========================================================================
// Demodulator bring-up
// =========================================================================
// The 16-tap default FIR (8x int8 then 8x int12) used for the SDR/FM path.
static void SetFir() {
static const int fir[16] = {
-54, -36, -41, -40, -32, -14, 14, 53,
101, 156, 215, 273, 327, 372, 404, 421,
};
uint8_t buf[20];
for (int i = 0; i < 8; i++) buf[i] = (uint8_t)(fir[i] & 0xff);
for (int i = 0; i < 8; i += 2) {
int v0 = fir[8 + i];
int v1 = fir[8 + i + 1];
buf[8 + i * 3 / 2] = (uint8_t)((v0 >> 4) & 0xff);
buf[8 + i * 3 / 2 + 1] = (uint8_t)(((v0 << 4) | ((v1 >> 8) & 0x0f)) & 0xff);
buf[8 + i * 3 / 2 + 2] = (uint8_t)(v1 & 0xff);
}
for (int i = 0; i < 20; i++) DemodWrite(1, (uint16_t)(0x1c + i), buf[i], 1);
}
static bool BasebandInit() {
// USB FIFO / endpoint A setup.
RegWrite(BLOCK_USB, USB_SYSCTL, 0x09, 1);
RegWrite(BLOCK_USB, USB_EPA_MAXPKT, 0x0002, 2);
RegWrite(BLOCK_USB, USB_EPA_CTL, 0x1002, 2);
// Power on the demod.
RegWrite(BLOCK_SYS, SYS_DEMOD_CTL1, 0x22, 1);
RegWrite(BLOCK_SYS, SYS_DEMOD_CTL, 0xe8, 1);
// Soft-reset the demod state machine.
DemodWrite(1, 0x01, 0x14, 1);
DemodWrite(1, 0x01, 0x10, 1);
// Disable spectrum inversion + clear DDC shift / IF registers.
DemodWrite(1, 0x15, 0x00, 1);
DemodWrite(1, 0x16, 0x0000, 2);
for (int i = 0; i < 6; i++) DemodWrite(1, (uint16_t)(0x16 + i), 0x00, 1);
SetFir();
DemodWrite(0, 0x19, 0x05, 1); // enable SDR mode, disable DAGC
DemodWrite(1, 0x93, 0xf0, 1);
DemodWrite(1, 0x94, 0x0f, 1);
DemodWrite(1, 0x11, 0x00, 1); // disable AGC loop
DemodWrite(1, 0x04, 0x00, 1);
DemodWrite(0, 0x61, 0x60, 1); // disable PID filter
DemodWrite(0, 0x06, 0x80, 1); // default ADC I/Q datapath
DemodWrite(1, 0xb1, 0x1b, 1); // zero-IF + DC cancel + IQ comp/est
DemodWrite(0, 0x0d, 0x83, 1); // disable clock output on TP_CK0
return true;
}
// Set the digital downconversion IF frequency the demod searches at.
static void SetIfFreq(uint32_t freq) {
int32_t ifv = (int32_t)(-(int64_t)((uint64_t)freq * TWO_POW22 / g_rtlXtal));
DemodWrite(1, 0x19, (uint16_t)((ifv >> 16) & 0x3f), 1);
DemodWrite(1, 0x1a, (uint16_t)((ifv >> 8) & 0xff), 1);
DemodWrite(1, 0x1b, (uint16_t)(ifv & 0xff), 1);
}
static void ApplySampleFreqCorrection() {
int32_t offs = (int32_t)(-(int64_t)g_ppm * (1 << 24) / 1000000);
DemodWrite(1, 0x3f, (uint16_t)(offs & 0xff), 1);
DemodWrite(1, 0x3e, (uint16_t)((offs >> 8) & 0x3f), 1);
}
static bool TunerInit() {
SetI2cRepeater(true);
// Retry detection a few times: an I2C read can transiently come back
// wrong if it raced another core's USB activity around bring-up.
bool detected = false;
for (int attempt = 0; attempt < 4 && !detected; attempt++)
detected = R820tDetect(g_slotId);
bool ok = detected && R820tInit(g_tuner, g_slotId, g_rtlXtal, R82XX_IF);
SetI2cRepeater(false);
if (!detected) {
KernelLogStream(WARNING, "RTL-SDR") << "no R820T2 tuner found on I2C";
return false;
}
if (!ok) return false;
// Demod path for the R820T2 low-IF tuner.
DemodWrite(1, 0xb1, 0x1a, 1); // disable zero-IF mode
DemodWrite(0, 0x08, 0x4d, 1); // enable In-phase ADC input only
SetIfFreq(R82XX_IF);
DemodWrite(1, 0x15, 0x01, 1); // enable spectrum inversion
return true;
}
static bool EnsureInit() {
if (g_hwInited) return true;
if (!g_present || !g_ctlBuf) return false;
if (!BasebandInit()) return false;
if (!TunerInit()) return false;
g_hwInited = true;
KernelLogStream(OK, "RTL-SDR") << "Demod + tuner brought up on slot "
<< (uint64_t)g_slotId;
return true;
}
// =========================================================================
// Tuning / configuration (each holds g_ctlLock via the op wrappers)
// =========================================================================
static int DoSetFreq(uint64_t hz) {
if (!EnsureInit()) return -1;
SetI2cRepeater(true);
bool ok = R820tSetFreq(g_tuner, hz);
SetI2cRepeater(false);
return ok ? 0 : -1;
}
static int DoSetSampleRate(uint32_t rate) {
if (!EnsureInit()) return -1;
// The RTL2832 resampler does not cover 300k..900k.
if (rate <= 225000 || rate > 3200000 ||
(rate > 300000 && rate < 900000)) return -1;
uint32_t ratio = (uint32_t)(((uint64_t)g_rtlXtal * TWO_POW22) / rate);
ratio &= 0x0ffffffc;
DemodWrite(1, 0x9f, (uint16_t)((ratio >> 16) & 0xffff), 2);
DemodWrite(1, 0xa1, (uint16_t)(ratio & 0xffff), 2);
ApplySampleFreqCorrection();
DemodWrite(1, 0x01, 0x14, 1); // soft reset
DemodWrite(1, 0x01, 0x10, 1);
SetIfFreq(R82XX_IF);
return 0;
}
static int DoSetGainMode(int manual) {
if (!EnsureInit()) return -1;
g_manual = manual ? 1 : 0;
SetI2cRepeater(true);
bool ok = R820tSetGain(g_tuner, g_manual, g_gain);
SetI2cRepeater(false);
return ok ? 0 : -1;
}
static int DoSetGain(int tenths) {
if (!EnsureInit()) return -1;
g_gain = tenths;
g_manual = 1; // selecting an explicit gain implies manual mode
SetI2cRepeater(true);
bool ok = R820tSetGain(g_tuner, 1, g_gain);
SetI2cRepeater(false);
return ok ? 0 : -1;
}
static int DoSetFreqCorrection(int ppm) {
if (!EnsureInit()) return -1;
g_ppm = ppm;
g_rtlXtal = (uint32_t)((int64_t)RTL_XTAL + (int64_t)RTL_XTAL * ppm / 1000000);
g_tuner.xtal = g_rtlXtal;
ApplySampleFreqCorrection();
return 0;
}
static int DoSetAgc(int on) {
if (!EnsureInit()) return -1;
return DemodWrite(0, 0x19, on ? 0x25 : 0x05, 1) ? 0 : -1;
}
static int DoSetDirectSampling(int mode) {
if (!EnsureInit()) return -1;
if (mode) {
// Bypass the tuner and digitise the ADC input directly.
SetI2cRepeater(true);
R820tStandby(g_tuner);
SetI2cRepeater(false);
DemodWrite(1, 0xb1, 0x1b, 1); // zero-IF path
DemodWrite(0, 0x08, 0x4d, 1);
DemodWrite(0, 0x06, (mode == 2) ? 0x90 : 0x80, 1); // Q vs I ADC
SetIfFreq(0);
DemodWrite(1, 0x15, 0x00, 1);
} else {
// Restore the R820T2 low-IF receive path.
DemodWrite(1, 0xb1, 0x1a, 1);
DemodWrite(0, 0x08, 0x4d, 1);
DemodWrite(0, 0x06, 0x80, 1);
SetIfFreq(R82XX_IF);
DemodWrite(1, 0x15, 0x01, 1);
}
return 0;
}
// =========================================================================
// Streaming
// =========================================================================
static void TransferCallback(uint8_t slotId, uint8_t epDci,
const uint8_t* data, uint32_t length,
uint32_t /*completionCode*/) {
if (slotId != g_slotId) return;
auto* dev = Xhci::GetDevice(slotId);
if (!dev) return;
uint8_t bulkInDci = dev->BulkInEpNum ? (uint8_t)(dev->BulkInEpNum * 2 + 1) : 0;
if (epDci != bulkInDci || !g_streaming) return;
if (data) {
// Deliver samples only. The xHCI layer owns the multi-buffer pool
// (StartBulkInStream) and re-arms this very buffer automatically once
// we return; re-queuing here would double-arm the pool and lap the
// ring. PushSamples copies out synchronously, so the buffer is free
// to be re-armed the instant this returns.
if (length > 0)
Drivers::Radio::Sdr::PushSamples(g_rxIndex, data, length);
} else {
// Error (data==nullptr), e.g. cc=6 STALL: the endpoint is halted.
// Clearing it requires a Reset Endpoint command that waits on the
// event ring, which is unsafe here (we are inside PollEvents).
// Flag it for process-context recovery in OpService(), which resets
// the endpoint and re-primes the whole pool.
g_bulkStalled.store(true, std::memory_order_relaxed);
}
}
static int DoStart() {
if (!EnsureInit()) return -1;
// Reset endpoint-A FIFO so streaming starts on a clean boundary.
RegWrite(BLOCK_USB, USB_EPA_CTL, 0x1002, 2); // hold + reset
RegWrite(BLOCK_USB, USB_EPA_CTL, 0x0000, 2); // release
g_bulkStalled.store(false, std::memory_order_relaxed);
g_streaming = true;
Xhci::RegisterTransferCallback(g_slotId, TransferCallback);
auto* dev = Xhci::GetDevice(g_slotId);
if (dev && dev->BulkInEpNum)
Xhci::StartBulkInStream(g_slotId, BULK_XFER_LEN, BULK_POOL_BUFS);
return 0;
}
static int DoStop() {
g_streaming = false;
g_bulkStalled.store(false, std::memory_order_relaxed);
// Disarm the multi-buffer rotation so no further transfers re-arm, then
// hold/reset the FIFO so the device stops producing samples.
Xhci::StopBulkInStream(g_slotId);
if (g_ctlBuf) RegWrite(BLOCK_USB, USB_EPA_CTL, 0x1002, 2);
return 0;
}
// Process-context housekeeping called from Read(): recover a halted bulk-IN
// endpoint (Reset Endpoint + Set TR Dequeue) and re-arm reception.
static void DoService() {
if (!g_bulkStalled.load(std::memory_order_relaxed)) return;
g_ctlLock.Acquire();
if (g_streaming) {
Xhci::ResetBulkInEndpoint(g_slotId);
Xhci::PrimeBulkInStream(g_slotId);
}
g_bulkStalled.store(false, std::memory_order_relaxed);
g_ctlLock.Release();
static uint32_t recoveries = 0;
if (recoveries < 5) {
recoveries++;
KernelLogStream(INFO, "RTL-SDR") << "bulk IN stall recovered ("
<< (uint64_t)recoveries << ")";
}
}
// =========================================================================
// Ops table wrappers (lock the control path)
// =========================================================================
static int OpSetFreq(void*, uint64_t hz) {
g_ctlLock.Acquire(); int r = DoSetFreq(hz); g_ctlLock.Release(); return r;
}
static int OpSetSampleRate(void*, uint32_t hz) {
g_ctlLock.Acquire(); int r = DoSetSampleRate(hz); g_ctlLock.Release(); return r;
}
static int OpSetGainMode(void*, int manual) {
g_ctlLock.Acquire(); int r = DoSetGainMode(manual); g_ctlLock.Release(); return r;
}
static int OpSetGain(void*, int tenths) {
g_ctlLock.Acquire(); int r = DoSetGain(tenths); g_ctlLock.Release(); return r;
}
static int OpSetFreqCorrection(void*, int ppm) {
g_ctlLock.Acquire(); int r = DoSetFreqCorrection(ppm); g_ctlLock.Release(); return r;
}
static int OpSetAgc(void*, int on) {
g_ctlLock.Acquire(); int r = DoSetAgc(on); g_ctlLock.Release(); return r;
}
static int OpSetDirectSampling(void*, int mode) {
g_ctlLock.Acquire(); int r = DoSetDirectSampling(mode); g_ctlLock.Release(); return r;
}
static int OpStart(void*) {
g_ctlLock.Acquire(); int r = DoStart(); g_ctlLock.Release(); return r;
}
static int OpStop(void*) {
g_ctlLock.Acquire(); int r = DoStop(); g_ctlLock.Release(); return r;
}
// DoService does its own locking (around the reset), so OpService must not
// take g_ctlLock here.
static void OpService(void*) { DoService(); }
// =========================================================================
// USB enumeration hooks
// =========================================================================
bool IsRtlSdr(uint16_t vid, uint16_t pid) {
if (vid != 0x0bda) return false; // Realtek Semiconductor
switch (pid) {
case 0x2832: // RTL2832U (generic)
case 0x2838: // RTL2838 (most RTL-SDR.com dongles)
case 0x2831: // RTL2831U
case 0x2837: // RTL2832U variant
case 0x2834: // RTL2832U variant
return true;
default:
return false;
}
}
void RegisterDevice(uint8_t slotId) {
if (g_present) {
KernelLogStream(WARNING, "RTL-SDR")
<< "second RTL-SDR ignored (single instance), slot " << (uint64_t)slotId;
return;
}
g_slotId = slotId;
g_present = true;
g_hwInited = false;
g_streaming = false;
g_ppm = 0;
g_rtlXtal = RTL_XTAL;
g_manual = 0;
g_gain = 0;
g_tuner = R820tDev{};
g_ctlBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed();
if (!g_ctlBuf) {
KernelLogStream(ERROR, "RTL-SDR") << "control buffer alloc failed";
g_present = false;
return;
}
int gainCount = 0;
const int* gains = R820tGainTable(&gainCount);
Drivers::Radio::Sdr::ReceiverDesc desc{};
desc.name = "Realtek RTL2832U";
desc.tuner = "Rafael Micro R820T2";
desc.serial = "USB RTL-SDR";
desc.freqMin = 24000000ull;
desc.freqMax = 1766000000ull;
desc.sampleRateMin = 225001;
desc.sampleRateMax = 3200000;
desc.gains = gains;
desc.numGains = (uint32_t)gainCount;
desc.format = montauk::abi::SDR_FORMAT_CU8;
desc.ops.SetFreq = OpSetFreq;
desc.ops.SetSampleRate = OpSetSampleRate;
desc.ops.SetGainMode = OpSetGainMode;
desc.ops.SetGain = OpSetGain;
desc.ops.SetFreqCorrection = OpSetFreqCorrection;
desc.ops.SetAgc = OpSetAgc;
desc.ops.SetDirectSampling = OpSetDirectSampling;
desc.ops.Start = OpStart;
desc.ops.Stop = OpStop;
desc.ops.Service = OpService;
desc.ctx = nullptr;
g_rxIndex = Drivers::Radio::Sdr::Register(desc);
if (g_rxIndex < 0) {
KernelLogStream(ERROR, "RTL-SDR") << "SDR registration failed";
Memory::g_pfa->Free(g_ctlBuf);
g_ctlBuf = nullptr;
g_present = false;
return;
}
KernelLogStream(OK, "RTL-SDR") << "RTL-SDR on slot " << (uint64_t)slotId
<< " registered as receiver " << (uint64_t)g_rxIndex;
}
void UnregisterDevice(uint8_t slotId) {
if (!g_present || slotId != g_slotId) return;
g_streaming = false;
if (g_rxIndex >= 0) Drivers::Radio::Sdr::Unregister(g_rxIndex);
g_rxIndex = -1;
g_present = false;
g_hwInited = false;
if (g_ctlBuf) {
Memory::g_pfa->Free(g_ctlBuf);
g_ctlBuf = nullptr;
}
KernelLogStream(INFO, "RTL-SDR") << "RTL-SDR removed from slot " << (uint64_t)slotId;
}
}
+37
View File
@@ -0,0 +1,37 @@
/*
* RtlSdr.hpp
* Realtek RTL2832U + R820T2 software-defined-radio receiver (RTL-SDR).
*
* The RTL2832U is a DVB-T demodulator that, in raw mode, streams 8-bit
* unsigned I/Q samples over a USB bulk-IN endpoint. This driver brings up the
* demodulator + R820T2 tuner, configures the resampler / IF, and feeds the
* bulk-IN samples into the generic SDR receive subsystem (Drivers::Radio::Sdr),
* which userspace reaches through the SYS_SDR_* syscalls.
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Drivers::USB::Radio {
// True if a USB VID:PID identifies a supported RTL2832U-based SDR dongle.
bool IsRtlSdr(uint16_t vid, uint16_t pid);
// Called by USB enumeration once the bulk-IN endpoint has been configured.
// Registers a receiver with the SDR subsystem; the demod/tuner are brought
// up lazily on first use (in process context, never from the USB poll path).
void RegisterDevice(uint8_t slotId);
// Tear down on unplug.
void UnregisterDevice(uint8_t slotId);
// -------------------------------------------------------------------------
// I2C facade used by the R820T2 tuner module. These carry the tuner's
// register traffic over the demod's I2C block. The control-transfer mutex
// is held by the calling op wrapper, so these do not lock themselves.
// -------------------------------------------------------------------------
bool RtlI2cWrite(uint8_t slotId, uint8_t i2cAddr, const uint8_t* buf, uint8_t len);
bool RtlI2cRead(uint8_t slotId, uint8_t i2cAddr, uint8_t* buf, uint8_t len);
}
+14 -2
View File
@@ -10,6 +10,7 @@
#include "HidMouse.hpp"
#include "MassStorage.hpp"
#include "Bluetooth/Bluetooth.hpp"
#include "Radio/RtlSdr.hpp"
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Memory/HHDM.hpp>
@@ -350,6 +351,11 @@ namespace Drivers::USB::UsbDevice {
bool foundBulkOut = false;
uint16_t hidReportDescLen = 0;
// RTL-SDR dongles are vendor-class (0xFF) devices identified by VID:PID.
// They expose a single bulk-IN endpoint that streams raw I/Q samples;
// treat them like the other bulk-capable class drivers below.
bool foundRadio = Drivers::USB::Radio::IsRtlSdr(devDesc.idVendor, devDesc.idProduct);
while (offset + 2 <= totalLen) {
uint8_t len = cfgBuf[offset];
uint8_t type = cfgBuf[offset + 1];
@@ -417,8 +423,8 @@ namespace Drivers::USB::UsbDevice {
foundEp = true;
}
// Bluetooth and Mass Storage bulk endpoints
if (foundBt || currentMsc) {
// Bluetooth, Mass Storage and RTL-SDR bulk endpoints
if (foundBt || currentMsc || foundRadio) {
if (isIn && xferType == EP_XFER_INTERRUPT && !foundEp) {
// HCI event pipe (interrupt IN)
dev->InterruptEpNum = ep->bEndpointAddress & 0x0F;
@@ -671,6 +677,12 @@ namespace Drivers::USB::UsbDevice {
MassStorage::RegisterDevice(slotId);
KernelLogStream(OK, "USB") << "Slot " << (uint64_t)slotId
<< ": USB Mass Storage";
} else if (foundRadio && foundBulkIn) {
Drivers::USB::Radio::RegisterDevice(slotId);
KernelLogStream(OK, "USB") << "Slot " << (uint64_t)slotId
<< ": RTL-SDR receiver"
<< " VID:" << base::hex << (uint64_t)dev->VendorId
<< " PID:" << (uint64_t)dev->ProductId << base::dec;
} else if (foundEp) {
KernelLogStream(INFO, "USB") << "Slot " << (uint64_t)slotId
<< ": USB device, class=" << (uint64_t)dev->InterfaceClass
+176 -9
View File
@@ -10,6 +10,7 @@
#include "HidKeyboard.hpp"
#include "HidMouse.hpp"
#include "MassStorage.hpp"
#include "Radio/RtlSdr.hpp"
#include <Pci/Pci.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
@@ -18,7 +19,9 @@
#include <Memory/PageFrameAllocator.hpp>
#include <Libraries/Memory.hpp>
#include <Hal/Apic/Interrupts.hpp>
#include <Hal/SmpBoot.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <CppLib/Spinlock.hpp>
#include <atomic>
using namespace Kt;
@@ -125,6 +128,28 @@ namespace Drivers::USB::Xhci {
// started pumping from syscall context.
static std::atomic<bool> g_pollActive{false};
// CPU index of the core currently draining the event ring in PollEvents
// (-1 = none). ControlTransfer fire-and-forgets ONLY for true same-core
// nesting (a callback invoked from THIS core's PollEvents); a different
// core merely polling must not make a process-context transfer skip its
// wait -- that returned CC_SUCCESS before the device filled the buffer
// (observed as garbled RTL-SDR register reads while the BT firmware
// download was polling on another core).
static std::atomic<int> g_pollOwnerCpu{-1};
// Serialises non-nested (waiting) control transfers so only one EP0
// transfer is ever in flight; the completion signal (g_xferCompleted) is a
// single global, so two concurrent waiters would otherwise consume each
// other's completion. A Mutex (interrupts stay enabled) is correct here:
// ControlTransfer is process-context only and may hold this across its
// multi-millisecond poll/wait.
static kcp::Mutex g_controlXferLock;
static int CurrentCpuIndex() {
auto* cpu = Smp::GetCurrentCpuData();
return cpu ? cpu->cpuIndex : -1;
}
// Interrupt transfer data buffers (per slot)
static uint8_t* g_interruptDataBuf[MAX_SLOTS + 1] = {};
static uint64_t g_interruptDataBufPhys[MAX_SLOTS + 1] = {};
@@ -133,6 +158,21 @@ namespace Drivers::USB::Xhci {
static uint8_t* g_bulkInDataBuf[MAX_SLOTS + 1] = {};
static uint64_t g_bulkInDataBufPhys[MAX_SLOTS + 1] = {};
// Multi-buffer bulk-IN streaming pool (per slot). When PoolCount>0 the slot
// keeps that many bulk-IN transfers outstanding at all times: as each one
// completes the event handler hands its buffer to the callback and instantly
// re-arms the SAME buffer at the ring tail, so the endpoint is never without
// a place to DMA. This closes the gap that single-outstanding bulk IN leaves
// between completion and re-arm, during which the device FIFO overflows
// (the RTL-SDR ~88% sample-drop at 2.048 Msps). PoolCount==0 => the legacy
// single-buffer path above (used by Bluetooth ACL), unchanged.
static constexpr uint32_t BULK_IN_POOL_MAX = 16;
static uint8_t* g_bulkInPool[MAX_SLOTS + 1][BULK_IN_POOL_MAX] = {};
static uint64_t g_bulkInPoolPhys[MAX_SLOTS + 1][BULK_IN_POOL_MAX] = {};
static uint32_t g_bulkInPoolCount[MAX_SLOTS + 1] = {}; // outstanding URBs (0=off)
static uint32_t g_bulkInPoolHead[MAX_SLOTS + 1] = {}; // next buffer to complete
static uint32_t g_bulkInPoolXferLen[MAX_SLOTS + 1] = {}; // bytes per transfer
// Transfer callbacks for non-HID class drivers (per slot)
static TransferCallback g_transferCallbacks[MAX_SLOTS + 1] = {};
@@ -412,6 +452,7 @@ namespace Drivers::USB::Xhci {
std::memory_order_acquire)) {
return; // another context is draining; it will reap our events
}
g_pollOwnerCpu.store(CurrentCpuIndex(), std::memory_order_relaxed);
// Bound the work per call so a flooding/wedged device can never spin
// here forever; the outer wall-clock timeouts then fire instead of
@@ -489,7 +530,25 @@ namespace Drivers::USB::Xhci {
uint8_t bulkOutDci = dev.BulkOutEpNum ? (dev.BulkOutEpNum * 2) : 0;
uint8_t intDci = dev.InterruptEpNum ? (dev.InterruptEpNum * 2 + 1) : 0;
if (epDci == bulkInDci && g_transferCallbacks[slotId]) {
if (epDci == bulkInDci && g_transferCallbacks[slotId]
&& g_bulkInPoolCount[slotId] > 0) {
// Multi-buffer streaming: hand back the rotating
// pool buffer and immediately re-arm it. The
// callback consumes the data synchronously (copies
// it out) before returning, so re-queuing the same
// buffer is safe -- it will not be DMA'd into again
// until the other PoolCount-1 transfers ahead of it
// complete (~PoolCount ms of slack).
uint32_t i = g_bulkInPoolHead[slotId];
uint32_t reqLen = g_bulkInPoolXferLen[slotId];
uint32_t len = (residual < reqLen) ? (reqLen - residual) : 0;
g_transferCallbacks[slotId](slotId, epDci,
g_bulkInPool[slotId][i], len, completionCode);
QueueBulkInTransfer(slotId, g_bulkInPool[slotId][i],
g_bulkInPoolPhys[slotId][i], reqLen);
g_bulkInPoolHead[slotId] =
(i + 1) % g_bulkInPoolCount[slotId];
} else if (epDci == bulkInDci && g_transferCallbacks[slotId]) {
// Bulk IN — dispatch via registered callback.
// len = actually-transferred bytes (requested -
// residual). A 0-byte / ZLP completion has
@@ -580,6 +639,7 @@ namespace Drivers::USB::Xhci {
}
}
g_pollOwnerCpu.store(-1, std::memory_order_relaxed);
g_pollActive.store(false, std::memory_order_release);
}
@@ -667,6 +727,15 @@ namespace Drivers::USB::Xhci {
return 0xFF;
}
// True nesting = called from a callback inside THIS core's PollEvents;
// such a call must fire-and-forget (a nested PollEvents is a no-op, so
// it could never observe its own completion). A different core merely
// polling is NOT nesting: serialise behind g_controlXferLock and wait
// normally so the global completion signal is unambiguous.
bool trueNested = g_pollActive.load(std::memory_order_relaxed) &&
g_pollOwnerCpu.load(std::memory_order_relaxed) == CurrentCpuIndex();
if (!trueNested) g_controlXferLock.Acquire();
UsbDeviceInfo& dev = g_devices[slotId];
// --- Setup Stage TRB ---
@@ -734,13 +803,14 @@ namespace Drivers::USB::Xhci {
g_xferCompleted = false;
WriteDoorbell(slotId, 1);
// If we are nested inside PollEvents (e.g. an HCI reply sent from a
// Bluetooth event handler), we cannot wait for completion here: the
// reentrancy guard makes a nested PollEvents a no-op, so the completion
// would never be observed and the timeout+recovery path would corrupt
// the EP0 ring. The transfer is submitted (doorbell rung); let the
// active PollEvents reap its completion. Fire-and-forget.
if (g_pollActive.load(std::memory_order_relaxed)) {
// If we are nested inside THIS core's PollEvents (e.g. an HCI reply sent
// from a Bluetooth event handler), we cannot wait for completion here:
// the reentrancy guard makes a nested PollEvents a no-op, so the
// completion would never be observed and the timeout+recovery path would
// corrupt the EP0 ring. The transfer is submitted (doorbell rung); let
// the active PollEvents reap its completion. Fire-and-forget (no lock
// was taken for the nested case).
if (trueNested) {
return CC_SUCCESS;
}
@@ -752,6 +822,7 @@ namespace Drivers::USB::Xhci {
while (Timekeeping::GetMilliseconds() - xferStart < 2000) {
PollEvents();
if (g_xferCompleted) {
g_controlXferLock.Release();
return g_xferCompletionCode;
}
for (int j = 0; j < 100; j++) {
@@ -785,6 +856,7 @@ namespace Drivers::USB::Xhci {
| (1 << 16); // DCI=1 (EP0)
SendCommand(deqTrb);
g_controlXferLock.Release();
return 0xFF;
}
@@ -861,6 +933,36 @@ namespace Drivers::USB::Xhci {
QueueInterruptTransfer(slotId);
}
// -------------------------------------------------------------------------
// ResetBulkInEndpoint - clear a halted bulk IN endpoint (does not re-arm)
// -------------------------------------------------------------------------
void ResetBulkInEndpoint(uint8_t slotId) {
if (slotId == 0 || slotId > MAX_SLOTS || !g_devices[slotId].Active) return;
UsbDeviceInfo& dev = g_devices[slotId];
if (dev.BulkInEpNum == 0 || !dev.BulkInRing) return;
uint8_t dci = dev.BulkInEpNum * 2 + 1;
TRB resetTrb = {};
resetTrb.Control = (TRB_RESET_ENDPOINT << TRB_TYPE_SHIFT)
| ((uint32_t)slotId << 24)
| ((uint32_t)dci << 16);
SendCommand(resetTrb);
uint64_t newDeq = dev.BulkInRingPhys
+ (uint64_t)dev.BulkInRingEnqueue * sizeof(TRB);
if (dev.BulkInRingCCS) newDeq |= 1; // DCS bit
TRB deqTrb = {};
deqTrb.Parameter0 = (uint32_t)(newDeq & 0xFFFFFFFF);
deqTrb.Parameter1 = (uint32_t)(newDeq >> 32);
deqTrb.Control = (TRB_SET_TR_DEQUEUE << TRB_TYPE_SHIFT)
| ((uint32_t)slotId << 24)
| ((uint32_t)dci << 16);
SendCommand(deqTrb);
}
// -------------------------------------------------------------------------
// QueueBulkInTransfer
// -------------------------------------------------------------------------
@@ -898,6 +1000,69 @@ namespace Drivers::USB::Xhci {
WriteDoorbell(slotId, target);
}
// -------------------------------------------------------------------------
// Multi-buffer bulk-IN streaming (see g_bulkInPool* declarations)
// -------------------------------------------------------------------------
// (Re)queue one transfer per pool buffer; resets the completion rotation.
// Safe to call from process context: the prime loop (a few TRB writes)
// finishes in microseconds, far below the >=tens-of-us minimum USB transfer
// time, so no completion can race the loop -- the same timing the legacy
// single-buffer start relies on.
void PrimeBulkInStream(uint8_t slotId) {
if (slotId == 0 || slotId > MAX_SLOTS || !g_devices[slotId].Active) return;
uint32_t n = g_bulkInPoolCount[slotId];
if (n == 0) return;
g_bulkInPoolHead[slotId] = 0;
uint32_t len = g_bulkInPoolXferLen[slotId];
for (uint32_t i = 0; i < n; i++)
QueueBulkInTransfer(slotId, g_bulkInPool[slotId][i],
g_bulkInPoolPhys[slotId][i], len);
}
void StartBulkInStream(uint8_t slotId, uint32_t xferLen, uint32_t numBuffers) {
if (slotId == 0 || slotId > MAX_SLOTS || !g_devices[slotId].Active) return;
UsbDeviceInfo& dev = g_devices[slotId];
if (!dev.BulkInRing || dev.BulkInEpNum == 0) return;
if (numBuffers < 1) numBuffers = 1;
if (numBuffers > BULK_IN_POOL_MAX) numBuffers = BULK_IN_POOL_MAX;
// One page per buffer; a single TRB must not cross a 64 KiB boundary, so
// clamp the transfer to a page (AllocateDmaBuffer hands out one page).
if (xferLen == 0 || xferLen > 4096) xferLen = 4096;
// Lazily allocate the pool; buffers persist across stop/start (same
// keep-forever pattern as g_bulkInDataBuf).
for (uint32_t i = 0; i < numBuffers; i++) {
if (g_bulkInPool[slotId][i] == nullptr) {
g_bulkInPool[slotId][i] =
AllocateDmaBuffer(g_bulkInPoolPhys[slotId][i]);
if (g_bulkInPool[slotId][i] == nullptr) {
// Out of DMA pages: fall back to however many we got.
if (i == 0) return;
numBuffers = i;
break;
}
}
}
g_bulkInPoolXferLen[slotId] = xferLen;
// Prime the transfers BEFORE publishing PoolCount so the event handler
// only switches to the rotating path once every buffer is queued.
g_bulkInPoolHead[slotId] = 0;
for (uint32_t i = 0; i < numBuffers; i++)
QueueBulkInTransfer(slotId, g_bulkInPool[slotId][i],
g_bulkInPoolPhys[slotId][i], xferLen);
g_bulkInPoolCount[slotId] = numBuffers;
}
void StopBulkInStream(uint8_t slotId) {
if (slotId == 0 || slotId > MAX_SLOTS) return;
// Disarm the rotation; any late completion now takes the (no-op for SDR)
// legacy path and is not re-armed. Buffers are retained for reuse.
g_bulkInPoolCount[slotId] = 0;
}
// -------------------------------------------------------------------------
// QueueBulkOutTransfer
// -------------------------------------------------------------------------
@@ -1053,7 +1218,9 @@ namespace Drivers::USB::Xhci {
}
static void UnregisterClassDriver(uint8_t slotId, const UsbDeviceInfo& dev) {
if (dev.InterfaceClass == UsbDevice::CLASS_MASS_STORAGE) {
if (Radio::IsRtlSdr(dev.VendorId, dev.ProductId)) {
Radio::UnregisterDevice(slotId);
} else if (dev.InterfaceClass == UsbDevice::CLASS_MASS_STORAGE) {
MassStorage::UnregisterDevice(slotId);
} else if (dev.InterfaceClass == UsbDevice::CLASS_HID &&
dev.InterfaceProtocol == UsbDevice::PROTOCOL_KEYBOARD) {
+19
View File
@@ -320,6 +320,12 @@ namespace Drivers::USB::Xhci {
// pipe (e.g. during Bluetooth firmware download).
void ResetInterruptEndpoint(uint8_t slotId);
// Clear a halted bulk IN endpoint (Reset Endpoint + Set TR Dequeue) without
// re-arming. Must be called from process context (it issues commands that
// wait on the event ring); the caller re-arms with QueueBulkInTransfer.
// Used for SDR stream stall recovery (RTL2832 bulk IN can STALL on start).
void ResetBulkInEndpoint(uint8_t slotId);
// True while PollEvents() is draining the event ring. A command submitter
// reached from inside an event callback must fire-and-forget (not wait),
// since a nested PollEvents() is a no-op.
@@ -329,6 +335,19 @@ namespace Drivers::USB::Xhci {
void QueueBulkInTransfer(uint8_t slotId, uint8_t* data, uint64_t dataPhys, uint32_t length);
void QueueBulkOutTransfer(uint8_t slotId, uint8_t* data, uint64_t dataPhys, uint32_t length);
// Continuous multi-buffer bulk-IN streaming. StartBulkInStream allocates
// `numBuffers` (clamped to 16) DMA buffers of `xferLen` bytes (clamped to a
// 4 KiB page) and keeps that many transfers outstanding, auto-re-arming each
// as it completes. The slot's registered transfer callback receives every
// buffer's data but must NOT re-arm itself (the event handler does). This
// eliminates the FIFO-overflow gap of single-outstanding bulk IN. Use for
// sustained high-rate sources (RTL-SDR I/Q). PrimeBulkInStream re-queues the
// whole pool after a stall reset; StopBulkInStream disarms the rotation.
// All three are process-context calls.
void StartBulkInStream(uint8_t slotId, uint32_t xferLen, uint32_t numBuffers);
void PrimeBulkInStream(uint8_t slotId);
void StopBulkInStream(uint8_t slotId);
// Queue a bulk transfer and wait for its completion. Used by storage-style
// class drivers that need command/response ordering on bulk pipes.
uint32_t BulkTransfer(uint8_t slotId, bool dirIn, void* data, uint64_t dataPhys,