fix: move RTL-SDR to userspace

This commit is contained in:
2026-08-29 10:02:32 +02:00
parent b1f1cfe32b
commit 5cd5c2e6be
35 changed files with 1922 additions and 1592 deletions
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 151
#define MONTAUK_BUILD_NUMBER 155
-55
View File
@@ -1,55 +0,0 @@
/*
* 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);
}
}
+37 -22
View File
@@ -33,7 +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 "Usb.hpp" // generic process-owned USB interface access
#include "WifiSyscall.hpp" // SYS_WIFI_SCAN, SYS_WIFI_INFO, SYS_WIFI_CONNECT, SYS_WIFI_DISCONNECT
#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
@@ -420,26 +420,41 @@ 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_USB_LIST: {
if ((int64_t)frame->arg2 < 0) return USB_ERR_INVALID;
uint64_t maxCount = frame->arg2 > 16 ? 16 : frame->arg2;
if (!UserMemory::Range(frame->arg1,
maxCount * sizeof(UsbInterfaceInfo), true)) return USB_ERR_INVALID;
return Sys_UsbList((UsbInterfaceInfo*)frame->arg1, (int)maxCount);
}
case SYS_USB_CLAIM:
if (frame->arg1 == 0 || frame->arg1 > 16 || frame->arg2 > 255)
return USB_ERR_INVALID;
return Sys_UsbClaim((uint8_t)frame->arg1, (uint8_t)frame->arg2);
case SYS_USB_CLOSE:
return Sys_UsbClose((int)frame->arg1);
case SYS_USB_CONTROL: {
if (!UserMemory::Readable<UsbControlRequest>(frame->arg2)) return USB_ERR_INVALID;
UsbControlRequest request = *(const UsbControlRequest*)frame->arg2;
if (frame->arg4 > 4096 || frame->arg4 != request.length) return USB_ERR_INVALID;
bool deviceToHost = (request.requestType & 0x80) != 0;
if (frame->arg4 != 0 &&
!UserMemory::Range(frame->arg3, frame->arg4, deviceToHost)) return USB_ERR_INVALID;
return Sys_UsbControl((int)frame->arg1, &request,
(void*)frame->arg3, (uint32_t)frame->arg4);
}
case SYS_USB_BULK_IN_START:
if (frame->arg2 > 0xffffffffULL || frame->arg3 > 0xffffffffULL)
return USB_ERR_INVALID;
return Sys_UsbBulkInStart((int)frame->arg1, (uint32_t)frame->arg2,
(uint32_t)frame->arg3);
case SYS_USB_BULK_IN_STOP:
return Sys_UsbBulkInStop((int)frame->arg1);
case SYS_USB_BULK_IN_READ:
if (frame->arg3 > 0xffffffffULL) return USB_ERR_INVALID;
if (!UserMemory::Range(frame->arg2, frame->arg3, true)) return USB_ERR_INVALID;
return Sys_UsbBulkInRead((int)frame->arg1, (uint8_t*)frame->arg2,
(uint32_t)frame->arg3);
case SYS_POWERINFO:
if (!UserMemory::Writable<PowerInfo>(frame->arg1)) return -1;
return Sys_PowerInfo((PowerInfo*)frame->arg1);
@@ -655,7 +670,7 @@ namespace montauk::abi {
Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR="
<< kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", "
<< (SYS_TERMINAL_ATTACHED + 1) << " syscall slots)";
<< (SYS_USB_BULK_IN_READ + 1) << " syscall slots)";
}
}
+63 -37
View File
@@ -275,16 +275,16 @@ 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
/* Reserved: former SDR API. Kept unavailable to preserve ABI numbering. */
static constexpr uint64_t SYS_RESERVED_140 = 140;
static constexpr uint64_t SYS_RESERVED_141 = 141;
static constexpr uint64_t SYS_RESERVED_142 = 142;
static constexpr uint64_t SYS_RESERVED_143 = 143;
static constexpr uint64_t SYS_RESERVED_144 = 144;
static constexpr uint64_t SYS_RESERVED_145 = 145;
static constexpr uint64_t SYS_RESERVED_146 = 146;
static constexpr uint64_t SYS_RESERVED_147 = 147;
static constexpr uint64_t SYS_RESERVED_148 = 148;
/* Power.hpp -- CPU power/thermal status */
static constexpr uint64_t SYS_POWERINFO = 149; // (PowerInfo*) -> 0, -1 unsupported
@@ -327,17 +327,26 @@ namespace montauk::abi {
static constexpr uint64_t SYS_SETENVIRON = 172;
static constexpr uint64_t SYS_SPAWN_ENV = 173;
// 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
/* Generic userspace USB interface access */
static constexpr uint64_t SYS_USB_LIST = 178; // (UsbInterfaceInfo*, max) -> count
static constexpr uint64_t SYS_USB_CLAIM = 179; // (slot, interface) -> owned handle
static constexpr uint64_t SYS_USB_CLOSE = 180; // (handle)
static constexpr uint64_t SYS_USB_CONTROL = 181; // (handle, UsbControlRequest*, data, len)
static constexpr uint64_t SYS_USB_BULK_IN_START = 182; // (handle, transferBytes, buffers)
static constexpr uint64_t SYS_USB_BULK_IN_STOP = 183; // (handle)
static constexpr uint64_t SYS_USB_BULK_IN_READ = 184; // (handle, data, len) -> bytes
// Sample formats reported in SdrDeviceInfo.sampleFormat.
static constexpr uint8_t SDR_FORMAT_CU8 = 0; // 8-bit unsigned interleaved I/Q
// Generic USB errors. Claims are restricted to interfaces without a
// bound in-kernel class driver and are owned by the claiming process.
static constexpr int USB_ERR_INVALID = -1;
static constexpr int USB_ERR_BUSY = -2;
static constexpr int USB_ERR_DISCONNECTED = -3;
static constexpr int USB_ERR_UNSUPPORTED = -4;
static constexpr int USB_ERR_IO = -5;
static constexpr int USB_ERR_NO_RESOURCES = -6;
static constexpr int USB_ERR_NOT_FOUND = -7;
static constexpr int USB_ERR_KERNEL_BOUND = -8;
// Graceful power-off request actions (SYS_POWER_REQUEST). The desktop posts
// a pending action and exits; login.elf reads it, runs the shutdown stages,
@@ -658,23 +667,40 @@ 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;
};
// One USB interface currently represented by the xHCI device table. A
// nonzero kernelDriverBound interface cannot be claimed by userspace.
struct UsbInterfaceInfo {
uint8_t slotId;
uint8_t portId;
uint8_t speed; // xHCI speed ID
uint8_t interfaceNumber;
uint16_t vendorId;
uint16_t productId;
uint8_t deviceClass;
uint8_t interfaceClass;
uint8_t interfaceSubClass;
uint8_t interfaceProtocol;
uint8_t bulkInEndpoint; // USB address, including direction bit
uint8_t bulkOutEndpoint;
uint16_t bulkInMaxPacket;
uint16_t bulkOutMaxPacket;
uint8_t kernelDriverBound;
uint8_t claimed;
uint8_t _reserved[4];
} __attribute__((packed));
// Standard USB setup packet fields. requestType bit 7 determines the data
// direction. length must match the data length passed to SYS_USB_CONTROL.
struct UsbControlRequest {
uint8_t requestType;
uint8_t request;
uint16_t value;
uint16_t index;
uint16_t length;
} __attribute__((packed));
static_assert(sizeof(UsbInterfaceInfo) == 24);
static_assert(sizeof(UsbControlRequest) == 8);
// Wi-Fi security suites reported in WifiNetwork.security.
static constexpr uint8_t WIFI_SEC_OPEN = 0;
+44
View File
@@ -0,0 +1,44 @@
/*
* Usb.hpp
* Generic userspace USB interface syscall layer.
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <Drivers/USB/UserUsb.hpp>
namespace montauk::abi {
static int64_t Sys_UsbList(UsbInterfaceInfo* out, int maxCount) {
return Drivers::USB::UserUsb::List(out, maxCount);
}
static int64_t Sys_UsbClaim(uint8_t slotId, uint8_t interfaceNumber) {
return Drivers::USB::UserUsb::Claim(slotId, interfaceNumber);
}
static int64_t Sys_UsbClose(int handle) {
return Drivers::USB::UserUsb::Close(handle);
}
static int64_t Sys_UsbControl(int handle, const UsbControlRequest* request,
void* data, uint32_t dataLen) {
if (!request) return USB_ERR_INVALID;
return Drivers::USB::UserUsb::Control(handle, *request, data, dataLen);
}
static int64_t Sys_UsbBulkInStart(int handle, uint32_t transferBytes,
uint32_t bufferCount) {
return Drivers::USB::UserUsb::StartBulkIn(handle, transferBytes, bufferCount);
}
static int64_t Sys_UsbBulkInStop(int handle) {
return Drivers::USB::UserUsb::StopBulkIn(handle);
}
static int64_t Sys_UsbBulkInRead(int handle, uint8_t* out, uint32_t maxLen) {
return Drivers::USB::UserUsb::ReadBulkIn(handle, out, maxLen);
}
}
-359
View File
@@ -1,359 +0,0 @@
/*
* 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) {
// Truncate to a whole number of I/Q byte pairs: dropping an odd
// count would swap I and Q for the rest of the stream.
n = space & ~1u;
dropped = len - n;
}
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;
// Already streaming: a second Start must not re-arm the driver's
// transfer pool (it would double-queue every buffer).
if (r->streaming) return 0;
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
@@ -1,119 +0,0 @@
/*
* 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);
}
-448
View File
@@ -1,448 +0,0 @@
/*
* 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 starting at register 0. The read pointer must be set
// to 0 with an address-only write first: a preceding register write leaves
// it pointing past the last register written, which would return the wrong
// registers here. 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);
uint8_t ptr = 0x00;
if (!RtlI2cWrite(d.slotId, R820T_I2C_ADDR, &ptr, 1)) return false;
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
// =========================================================================
static bool SetPll(R820tDev& d, uint64_t freqHz); // defined below
// IF filter setup for the SDR receive path (the "BW < 6 MHz" digital-TV
// profile): calibrate the filter at a 56 MHz LO, then program the filter
// code, bandwidth / HP corner, image-rejection side and filter gain.
static bool ApplyIfFilterConfig(R820tDev& d) {
const uint8_t filtGain = 0x10; // +3 dB, 6 MHz on
const uint8_t imgR = 0x00; // image negative
const uint8_t filtQ = 0x10; // low Q
const uint8_t hpCor = 0x6b; // 1.7 MHz disable, +2 cap, 1.0 MHz corner
bool ok = true;
ok &= WriteRegMask(d, 0x0c, 0x00, 0x0f); // init flag & xtal check result
ok &= WriteRegMask(d, 0x13, 49, 0x3f); // version number
ok &= WriteRegMask(d, 0x1d, 0x00, 0x38); // LT gain test
// Filter calibration: park the PLL at 56 MHz, pulse the calibration
// trigger, read the resulting filter code back (status reg 4, low
// nibble). One retry; 0x0f means the calibration failed (use 0).
uint8_t calCode = 0;
for (int i = 0; i < 2; i++) {
ok &= WriteRegMask(d, 0x0b, hpCor, 0x60); // filt_cap
ok &= WriteRegMask(d, 0x0f, 0x04, 0x04); // calibration clock on
ok &= WriteRegMask(d, 0x10, 0x00, 0x03); // xtal cap 0 pF for PLL
SetPll(d, 56000000ull); // lock not required here
ok &= WriteRegMask(d, 0x0b, 0x10, 0x10); // start trigger
ok &= WriteRegMask(d, 0x0b, 0x00, 0x10); // stop trigger
ok &= WriteRegMask(d, 0x0f, 0x00, 0x04); // calibration clock off
uint8_t data[5] = {0};
if (!Read(d, data, sizeof(data))) return false;
calCode = (uint8_t)(data[4] & 0x0f);
if (calCode && calCode != 0x0f) break;
}
if (calCode == 0x0f) calCode = 0;
ok &= WriteRegMask(d, 0x0a, (uint8_t)(filtQ | calCode), 0x1f);
ok &= WriteRegMask(d, 0x0b, hpCor, 0xef); // bandwidth, filter gain, HP corner
ok &= WriteRegMask(d, 0x07, imgR, 0x80); // image rejection side
ok &= WriteRegMask(d, 0x06, filtGain, 0x30);// filt_3dB
ok &= WriteRegMask(d, 0x1e, 0x60, 0x60); // channel filter extension @ LNA max-1
ok &= WriteRegMask(d, 0x05, 0x01, 0x80); // loop-through
ok &= WriteRegMask(d, 0x1f, 0x00, 0x80); // loop-through attenuation enable
ok &= WriteRegMask(d, 0x0f, 0x00, 0x80); // filter extension widest: off
ok &= WriteRegMask(d, 0x19, 0x60, 0x60); // RF poly filter current: min
return ok;
}
// Receive-path operating point for the digital/SDR profile: LNA and mixer
// detector top points and thresholds, input select, charge-pump and
// divider-buffer currents, AGC clock rate.
static bool ApplySysFreqConfig(R820tDev& d) {
bool ok = true;
ok &= WriteRegMask(d, 0x1d, 0xe5, 0xc7); // LNA top (detect bw 3, top 4)
ok &= WriteRegMask(d, 0x1c, 0x24, 0xf8); // mixer top 13, top-1, low-discharge
ok &= WriteReg(d, 0x0d, 0x53); // LNA vth 0.84 / vtl 0.64
ok &= WriteReg(d, 0x0e, 0x75); // mixer vth 1.04 / vtl 0.84
ok &= WriteRegMask(d, 0x05, 0x00, 0x60); // air-in input select
ok &= WriteRegMask(d, 0x06, 0x00, 0x08); // cable-2 input off
ok &= WriteRegMask(d, 0x11, 0x38, 0x38); // charge-pump current: auto
ok &= WriteRegMask(d, 0x17, 0x30, 0x30); // divider buffer current 150u
ok &= WriteRegMask(d, 0x0a, 0x40, 0x60); // filter current: low
ok &= WriteRegMask(d, 0x1d, 0x00, 0x38); // LNA top: lowest
ok &= WriteRegMask(d, 0x1c, 0x00, 0x04); // normal mode
ok &= WriteRegMask(d, 0x06, 0x00, 0x40); // pre-detect off
ok &= WriteRegMask(d, 0x1a, 0x30, 0x30); // AGC clock 250 Hz
ok &= WriteRegMask(d, 0x1d, 0x18, 0x38); // LNA top = 3
ok &= WriteRegMask(d, 0x1c, 0x24, 0x04); // mixer top bit
ok &= WriteRegMask(d, 0x1e, 0x0e, 0x1f); // LNA discharge current 14
ok &= WriteRegMask(d, 0x1a, 0x20, 0x30); // AGC clock 60 Hz
return ok;
}
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, then bring the IF filter and the
// receive-path operating point to the SDR profile (incl. the 56 MHz
// filter calibration).
if (!Write(d, 0x05, kInitArray, sizeof(kInitArray))) {
KernelLogStream(ERROR, "R820T") << "init register write failed";
return false;
}
if (!ApplyIfFilterConfig(d)) {
KernelLogStream(ERROR, "R820T") << "IF filter configuration failed";
return false;
}
if (!ApplySysFreqConfig(d)) {
KernelLogStream(ERROR, "R820T") << "receive path configuration 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 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;
// Exact fractional-N split: vcoDiv = round(65536 * vcoFreq / (2*ref)).
// The top bits are the integer divider, the low 16 bits feed the
// sigma-delta modulator directly (no iterative approximation).
uint64_t vcoDiv = (pllRef + 65536ull * vcoFreq) / (2ull * pllRef);
uint32_t nint = (uint32_t)(vcoDiv >> 16);
uint16_t sdm = (uint16_t)(vcoDiv & 0xffff);
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 (powered down when the fraction is 0).
ok &= WriteRegMask(d, 0x12, sdm ? 0x00 : 0x08, 0x08);
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;
}
// NOTE: the Air-in vs Cable-1 input switch at 345 MHz applies to the
// R828D only. The R820T/T2 uses the air input exclusively (selected
// during init); writing the Cable-1 select on an R820T disconnects the
// antenna input below 345 MHz.
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
@@ -1,61 +0,0 @@
/*
* 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);
}
-575
View File
@@ -1,575 +0,0 @@
/*
* 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
static int g_directSamp = 0; // 0=tuner path, 1=I ADC, 2=Q ADC
static uint64_t g_lastFreq = 0; // last successfully tuned freq (Hz)
// 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 uint8_t DemodRead(uint8_t page, uint16_t addr) {
if (!g_ctlBuf) return 0;
uint16_t raddr = (uint16_t)((addr << 8) | 0x20);
g_ctlBuf[0] = 0;
Xhci::ControlTransfer(g_slotId, CTRL_IN, 0, raddr, page, 1, g_ctlBuf, true);
return g_ctlBuf[0];
}
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);
bool ok = Xhci::ControlTransfer(g_slotId, CTRL_OUT, 0, waddr, index, len,
g_ctlBuf, false) == Xhci::CC_SUCCESS;
// Dummy status read after every demod write (reference behaviour);
// acts as a write barrier so the register latches before the next op.
DemodRead(0x0a, 0x01);
return ok;
}
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;
if (g_directSamp) {
// Tuner is bypassed: tuning is the demod's digital downconverter.
SetIfFreq((uint32_t)hz);
g_lastFreq = hz;
return 0;
}
SetI2cRepeater(true);
bool ok = R820tSetFreq(g_tuner, hz);
SetI2cRepeater(false);
if (ok) g_lastFreq = hz;
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;
// The ratio uses the NOMINAL crystal frequency: ppm correction is
// applied by the demod's sample-frequency-offset registers below, so
// baking it into the ratio too would correct the rate twice.
uint32_t ratio = (uint32_t)(((uint64_t)RTL_XTAL * 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(g_directSamp ? (uint32_t)g_lastFreq : 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();
// The tuner PLL (and, in direct mode, the DDC) derive from the xtal;
// retune so the new correction actually takes effect.
if (g_lastFreq) return DoSetFreq(g_lastFreq);
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, 0x1a, 1); // disable zero-IF
DemodWrite(1, 0x15, 0x00, 1); // no spectrum inversion
DemodWrite(0, 0x08, 0x4d, 1); // In-phase ADC input
DemodWrite(0, 0x06, (mode == 2) ? 0x90 : 0x80, 1); // Q vs I ADC
g_directSamp = mode;
// Tuning now happens in the DDC; carry the current frequency over.
SetIfFreq((uint32_t)g_lastFreq);
} else {
// Restore the R820T2 low-IF receive path. Standby powered the
// tuner down, so it needs a full re-initialisation.
SetI2cRepeater(true);
bool ok = R820tInit(g_tuner, g_slotId, g_rtlXtal, R82XX_IF);
SetI2cRepeater(false);
if (!ok) return -1;
SetIfFreq(R82XX_IF);
DemodWrite(1, 0x15, 0x01, 1); // enable spectrum inversion
DemodWrite(0, 0x06, 0x80, 1); // default ADC I/Q datapath
g_directSamp = 0;
if (g_lastFreq) return DoSetFreq(g_lastFreq);
}
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) {
// Only the two known RTL2832U ids. In particular 0x2831 is the
// RTL2831U, a DIFFERENT demod this driver cannot program.
case 0x2832: // RTL2832U (generic)
case 0x2838: // RTL2838 (most RTL-SDR.com dongles)
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_directSamp = 0;
g_lastFreq = 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
@@ -1,37 +0,0 @@
/*
* 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);
}
+58 -16
View File
@@ -10,7 +10,6 @@
#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>
@@ -351,11 +350,6 @@ 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];
@@ -423,8 +417,8 @@ namespace Drivers::USB::UsbDevice {
foundEp = true;
}
// Bluetooth, Mass Storage and RTL-SDR bulk endpoints
if (foundBt || currentMsc || foundRadio) {
// Bulk endpoints needed by in-kernel Bluetooth and storage drivers.
if (foundBt || currentMsc) {
if (isIn && xferType == EP_XFER_INTERRUPT && !foundEp) {
// HCI event pipe (interrupt IN)
dev->InterruptEpNum = ep->bEndpointAddress & 0x0F;
@@ -457,6 +451,51 @@ namespace Drivers::USB::UsbDevice {
foundBt = true;
}
// No in-kernel class driver recognized this device. Preserve the first
// interface and its bulk endpoints so a userspace driver can claim it.
// The xHCI slot model currently stores one interface; a future model
// can retain every alternate/interface without changing the userspace
// claim ABI, which already names the interface number explicitly.
bool knownInterface = foundBt || foundMsc ||
dev->InterfaceClass == CLASS_HID;
if (!knownInterface) {
bool inFirstInterface = false;
bool haveFirstInterface = false;
offset = 0;
while (offset + 2 <= totalLen) {
uint8_t len = cfgBuf[offset];
uint8_t type = cfgBuf[offset + 1];
if (len == 0 || offset + len > totalLen) break;
if (type == DESC_INTERFACE &&
offset + sizeof(InterfaceDescriptor) <= totalLen) {
if (haveFirstInterface) break;
auto* iface = (InterfaceDescriptor*)&cfgBuf[offset];
dev->InterfaceClass = iface->bInterfaceClass;
dev->InterfaceSubClass = iface->bInterfaceSubClass;
dev->InterfaceProtocol = iface->bInterfaceProtocol;
dev->InterfaceNumber = iface->bInterfaceNumber;
haveFirstInterface = true;
inFirstInterface = true;
} else if (inFirstInterface && type == DESC_ENDPOINT &&
offset + sizeof(EndpointDescriptor) <= totalLen) {
auto* ep = (EndpointDescriptor*)&cfgBuf[offset];
uint8_t xferType = ep->bmAttributes & EP_XFER_TYPE_MASK;
bool isIn = (ep->bEndpointAddress & EP_DIR_IN) != 0;
if (xferType == EP_XFER_BULK && isIn && !foundBulkIn) {
dev->BulkInEpNum = ep->bEndpointAddress & 0x0F;
dev->BulkInMaxPacket = ep->wMaxPacketSize & 0x7FF;
foundBulkIn = true;
} else if (xferType == EP_XFER_BULK && !isIn && !foundBulkOut) {
dev->BulkOutEpNum = ep->bEndpointAddress & 0x0F;
dev->BulkOutMaxPacket = ep->wMaxPacketSize & 0x7FF;
foundBulkOut = true;
}
}
offset += len;
}
}
// -----------------------------------------------------------------
// Step 8: SET_CONFIGURATION
// -----------------------------------------------------------------
@@ -657,15 +696,20 @@ namespace Drivers::USB::UsbDevice {
// -----------------------------------------------------------------
// Step 13: Register with the appropriate class driver
// -----------------------------------------------------------------
if (dev->InterfaceClass == CLASS_HID && dev->InterfaceProtocol == PROTOCOL_KEYBOARD) {
if (foundEp && dev->InterfaceClass == CLASS_HID &&
dev->InterfaceProtocol == PROTOCOL_KEYBOARD) {
dev->KernelDriverBound = true;
HidKeyboard::RegisterDevice(slotId);
KernelLogStream(OK, "USB") << "Slot " << (uint64_t)slotId << ": HID Boot Keyboard";
} else if (dev->InterfaceClass == CLASS_HID && dev->InterfaceProtocol == PROTOCOL_MOUSE) {
} else if (foundEp && dev->InterfaceClass == CLASS_HID &&
dev->InterfaceProtocol == PROTOCOL_MOUSE) {
dev->KernelDriverBound = true;
HidMouse::RegisterDevice(slotId);
KernelLogStream(OK, "USB") << "Slot " << (uint64_t)slotId << ": HID Boot Mouse";
} else if (dev->InterfaceClass == CLASS_WIRELESS &&
dev->InterfaceSubClass == SUBCLASS_RF &&
dev->InterfaceProtocol == PROTOCOL_BLUETOOTH) {
dev->KernelDriverBound = true;
Bluetooth::RegisterAdapter(slotId);
KernelLogStream(OK, "USB") << "Slot " << (uint64_t)slotId << ": Bluetooth Adapter"
<< " VID:" << base::hex << (uint64_t)dev->VendorId
@@ -674,15 +718,10 @@ namespace Drivers::USB::UsbDevice {
dev->InterfaceSubClass == SUBCLASS_SCSI &&
dev->InterfaceProtocol == PROTOCOL_BULK_ONLY &&
foundMsc && foundBulkIn && foundBulkOut) {
dev->KernelDriverBound = true;
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
@@ -692,6 +731,9 @@ namespace Drivers::USB::UsbDevice {
<< ": Non-HID device, class=" << (uint64_t)devDesc.bDeviceClass;
}
// Publish to userspace only after endpoint configuration and kernel
// class-driver binding decisions are complete.
dev->Ready = true;
return slotId;
}
+381
View File
@@ -0,0 +1,381 @@
/*
* UserUsb.cpp
* Process-owned access to unbound USB interfaces.
* Copyright (c) 2026 Daniel Hammer
*/
#include "UserUsb.hpp"
#include "Xhci.hpp"
#include <Sched/Scheduler.hpp>
#include <Memory/Heap.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Libraries/Memory.hpp>
#include <CppLib/Spinlock.hpp>
namespace Drivers::USB::UserUsb {
static constexpr int MaxClaims = Xhci::MAX_SLOTS;
static constexpr uint32_t RingBytes = 256 * 1024;
static constexpr uint32_t MaxControlBytes = 4096;
struct ClaimState {
bool active;
bool connected;
bool streaming;
uint8_t slotId;
uint8_t interfaceNumber;
uint16_t generation;
int ownerPid;
uint8_t* ring;
uint32_t head;
uint32_t count;
uint64_t droppedBytes;
uint32_t lastCompletionCode;
kcp::Spinlock ringLock;
};
static ClaimState g_claims[MaxClaims];
// Serializes process-context operations and prevents a sibling thread from
// closing a claim while another syscall is using its backing state.
static kcp::Mutex g_claimsLock;
static int MakeHandle(int index, uint16_t generation) {
return ((int)generation << 8) | index;
}
static ClaimState* LookupLocked(int handle, bool requireConnected = true) {
int index = handle & 0xff;
uint16_t generation = (uint16_t)((uint32_t)handle >> 8);
if (index < 0 || index >= MaxClaims || generation == 0) return nullptr;
ClaimState& claim = g_claims[index];
if (!claim.active || claim.generation != generation) return nullptr;
if (claim.ownerPid != Sched::GetCurrentPid()) return nullptr;
if (requireConnected && !claim.connected) return nullptr;
return &claim;
}
static bool SlotClaimedLocked(uint8_t slotId) {
for (int i = 0; i < MaxClaims; i++) {
if (g_claims[i].active && g_claims[i].connected &&
g_claims[i].slotId == slotId) return true;
}
return false;
}
static void CopyInterfaceInfo(uint8_t slotId, const Xhci::UsbDeviceInfo& dev,
montauk::abi::UsbInterfaceInfo& out) {
memset(&out, 0, sizeof(out));
out.slotId = slotId;
out.portId = dev.PortId;
out.speed = (uint8_t)dev.Speed;
out.interfaceNumber = dev.InterfaceNumber;
out.vendorId = dev.VendorId;
out.productId = dev.ProductId;
out.deviceClass = dev.DeviceClass;
out.interfaceClass = dev.InterfaceClass;
out.interfaceSubClass = dev.InterfaceSubClass;
out.interfaceProtocol = dev.InterfaceProtocol;
out.bulkInEndpoint = dev.BulkInEpNum ? (uint8_t)(0x80 | dev.BulkInEpNum) : 0;
out.bulkOutEndpoint = dev.BulkOutEpNum;
out.bulkInMaxPacket = dev.BulkInMaxPacket;
out.bulkOutMaxPacket = dev.BulkOutMaxPacket;
out.kernelDriverBound = dev.KernelDriverBound ? 1 : 0;
out.claimed = SlotClaimedLocked(slotId) ? 1 : 0;
}
int List(montauk::abi::UsbInterfaceInfo* out, int maxCount) {
if (!out || maxCount <= 0) return 0;
g_claimsLock.Acquire();
int count = 0;
for (uint8_t slot = 1; slot <= Xhci::MAX_SLOTS && count < maxCount; slot++) {
Xhci::UsbDeviceInfo* dev = Xhci::GetDevice(slot);
if (!dev || !dev->Active || !dev->Ready) continue;
CopyInterfaceInfo(slot, *dev, out[count++]);
}
g_claimsLock.Release();
return count;
}
int Claim(uint8_t slotId, uint8_t interfaceNumber) {
int ownerPid = Sched::GetCurrentPid();
if (ownerPid < 0 || slotId == 0 || slotId > Xhci::MAX_SLOTS) return montauk::abi::USB_ERR_INVALID;
g_claimsLock.Acquire();
Xhci::UsbDeviceInfo* dev = Xhci::GetDevice(slotId);
if (!dev || !dev->Active || !dev->Ready ||
dev->InterfaceNumber != interfaceNumber) {
g_claimsLock.Release();
return montauk::abi::USB_ERR_NOT_FOUND;
}
if (dev->KernelDriverBound) {
g_claimsLock.Release();
return montauk::abi::USB_ERR_KERNEL_BOUND;
}
if (SlotClaimedLocked(slotId)) {
g_claimsLock.Release();
return montauk::abi::USB_ERR_BUSY;
}
for (int i = 0; i < MaxClaims; i++) {
ClaimState& claim = g_claims[i];
if (claim.active) continue;
uint16_t generation = (uint16_t)(claim.generation + 1);
// Keep the encoded int handle positive so conventional `h < 0`
// error checks remain valid in userspace.
if (generation == 0 || generation > 0x7fff) generation = 1;
claim.active = true;
claim.connected = true;
claim.streaming = false;
claim.slotId = slotId;
claim.interfaceNumber = interfaceNumber;
claim.generation = generation;
claim.ownerPid = ownerPid;
claim.ring = nullptr;
claim.head = 0;
claim.count = 0;
claim.droppedBytes = 0;
claim.lastCompletionCode = Xhci::CC_SUCCESS;
int handle = MakeHandle(i, generation);
g_claimsLock.Release();
return handle;
}
g_claimsLock.Release();
return montauk::abi::USB_ERR_NO_RESOURCES;
}
static void TransferCallback(uint8_t slotId, uint8_t epDci,
const uint8_t* data, uint32_t length,
uint32_t completionCode) {
for (int i = 0; i < MaxClaims; i++) {
ClaimState& claim = g_claims[i];
claim.ringLock.Acquire();
if (!claim.active || !claim.connected || !claim.streaming ||
claim.slotId != slotId) {
claim.ringLock.Release();
continue;
}
Xhci::UsbDeviceInfo* dev = Xhci::GetDevice(slotId);
uint8_t expectedDci = (dev && dev->BulkInEpNum)
? (uint8_t)(dev->BulkInEpNum * 2 + 1) : 0;
claim.lastCompletionCode = completionCode;
if (epDci != expectedDci || !data || length == 0 || !claim.ring) {
claim.ringLock.Release();
return;
}
uint32_t space = RingBytes - claim.count;
uint32_t copied = length < space ? length : space;
uint32_t first = RingBytes - claim.head;
if (first > copied) first = copied;
memcpy(claim.ring + claim.head, data, first);
if (copied > first) memcpy(claim.ring, data + first, copied - first);
claim.head = (claim.head + copied) % RingBytes;
claim.count += copied;
claim.droppedBytes += length - copied;
claim.ringLock.Release();
return;
}
}
static void CloseLocked(ClaimState& claim) {
claim.ringLock.Acquire();
bool connected = claim.connected;
bool wasStreaming = claim.streaming;
uint8_t slotId = claim.slotId;
claim.streaming = false;
claim.active = false;
claim.connected = false;
claim.ownerPid = -1;
uint8_t* ring = claim.ring;
claim.ring = nullptr;
claim.head = claim.count = 0;
claim.ringLock.Release();
// Mark the claim inactive before stopping: PollEvents may dispatch a
// late completion from StopBulkInStream, and the callback must drop it.
if (connected && wasStreaming) Xhci::StopBulkInStream(slotId);
if (connected) Xhci::RegisterTransferCallback(slotId, nullptr);
if (ring) Memory::g_heap->Free(ring);
}
int Close(int handle) {
g_claimsLock.Acquire();
ClaimState* claim = LookupLocked(handle, false);
if (!claim) {
g_claimsLock.Release();
return montauk::abi::USB_ERR_INVALID;
}
CloseLocked(*claim);
g_claimsLock.Release();
return 0;
}
int Control(int handle, const montauk::abi::UsbControlRequest& request,
void* data, uint32_t dataLen) {
if (dataLen != request.length || dataLen > MaxControlBytes ||
(dataLen != 0 && data == nullptr)) return montauk::abi::USB_ERR_INVALID;
g_claimsLock.Acquire();
ClaimState* claim = LookupLocked(handle);
if (!claim) {
g_claimsLock.Release();
return montauk::abi::USB_ERR_DISCONNECTED;
}
void* dma = nullptr;
if (dataLen != 0) {
dma = Memory::g_pfa->AllocateZeroed();
if (!dma) {
g_claimsLock.Release();
return montauk::abi::USB_ERR_NO_RESOURCES;
}
if ((request.requestType & 0x80) == 0) memcpy(dma, data, dataLen);
}
uint32_t cc = Xhci::ControlTransfer(claim->slotId, request.requestType,
request.request, request.value, request.index, request.length,
dma, (request.requestType & 0x80) != 0);
if ((cc == Xhci::CC_SUCCESS || cc == Xhci::CC_SHORT_PACKET) &&
dataLen != 0 && (request.requestType & 0x80) != 0) {
memcpy(data, dma, dataLen);
}
if (dma) Memory::g_pfa->Free(dma);
g_claimsLock.Release();
return (cc == Xhci::CC_SUCCESS || cc == Xhci::CC_SHORT_PACKET)
? 0 : montauk::abi::USB_ERR_IO;
}
int StartBulkIn(int handle, uint32_t transferBytes, uint32_t bufferCount) {
if (transferBytes == 0 || transferBytes > 4096 ||
bufferCount == 0 || bufferCount > 16) return montauk::abi::USB_ERR_INVALID;
g_claimsLock.Acquire();
ClaimState* claim = LookupLocked(handle);
if (!claim) {
g_claimsLock.Release();
return montauk::abi::USB_ERR_DISCONNECTED;
}
Xhci::UsbDeviceInfo* dev = Xhci::GetDevice(claim->slotId);
if (!dev || !dev->BulkInEpNum || !dev->BulkInRing) {
g_claimsLock.Release();
return montauk::abi::USB_ERR_UNSUPPORTED;
}
if (claim->streaming) {
g_claimsLock.Release();
return 0;
}
if (!claim->ring) claim->ring = (uint8_t*)Memory::g_heap->Request(RingBytes);
if (!claim->ring) {
g_claimsLock.Release();
return montauk::abi::USB_ERR_NO_RESOURCES;
}
claim->ringLock.Acquire();
claim->head = claim->count = 0;
claim->droppedBytes = 0;
claim->lastCompletionCode = Xhci::CC_SUCCESS;
claim->streaming = true;
claim->ringLock.Release();
Xhci::RegisterTransferCallback(claim->slotId, TransferCallback);
Xhci::StartBulkInStream(claim->slotId, transferBytes, bufferCount);
if (!Xhci::IsBulkInStreamActive(claim->slotId)) {
claim->ringLock.Acquire();
claim->streaming = false;
claim->ringLock.Release();
Xhci::RegisterTransferCallback(claim->slotId, nullptr);
g_claimsLock.Release();
return montauk::abi::USB_ERR_NO_RESOURCES;
}
g_claimsLock.Release();
return 0;
}
int StopBulkIn(int handle) {
g_claimsLock.Acquire();
ClaimState* claim = LookupLocked(handle, false);
if (!claim) {
g_claimsLock.Release();
return montauk::abi::USB_ERR_INVALID;
}
claim->ringLock.Acquire();
bool stop = claim->connected && claim->streaming;
claim->streaming = false;
claim->ringLock.Release();
if (stop) Xhci::StopBulkInStream(claim->slotId);
g_claimsLock.Release();
return claim->connected ? 0 : montauk::abi::USB_ERR_DISCONNECTED;
}
int ReadBulkIn(int handle, uint8_t* out, uint32_t maxLen) {
if (!out && maxLen != 0) return montauk::abi::USB_ERR_INVALID;
g_claimsLock.Acquire();
ClaimState* claim = LookupLocked(handle, false);
if (!claim) {
g_claimsLock.Release();
return montauk::abi::USB_ERR_INVALID;
}
if (!claim->connected && claim->count == 0) {
g_claimsLock.Release();
return montauk::abi::USB_ERR_DISCONNECTED;
}
// Failed completions halt the endpoint and cannot be repaired from the
// xHCI event callback. Recover in process context so userspace drivers
// do not need a host-controller-specific reset API.
claim->ringLock.Acquire();
bool recover = claim->connected && claim->streaming &&
claim->lastCompletionCode != Xhci::CC_SUCCESS &&
claim->lastCompletionCode != Xhci::CC_SHORT_PACKET;
if (recover) claim->lastCompletionCode = Xhci::CC_SUCCESS;
claim->ringLock.Release();
if (recover) {
Xhci::ResetBulkInEndpoint(claim->slotId);
Xhci::PrimeBulkInStream(claim->slotId);
}
if (!claim->ring || maxLen == 0) {
g_claimsLock.Release();
return 0;
}
claim->ringLock.Acquire();
uint32_t copied = claim->count < maxLen ? claim->count : maxLen;
uint32_t tail = (claim->head + RingBytes - claim->count) % RingBytes;
uint32_t first = RingBytes - tail;
if (first > copied) first = copied;
memcpy(out, claim->ring + tail, first);
if (copied > first) memcpy(out + first, claim->ring, copied - first);
claim->count -= copied;
claim->ringLock.Release();
g_claimsLock.Release();
return (int)copied;
}
void ReleaseAllForPid(int pid) {
if (pid < 0) return;
g_claimsLock.Acquire();
for (int i = 0; i < MaxClaims; i++) {
if (g_claims[i].active && g_claims[i].ownerPid == pid) CloseLocked(g_claims[i]);
}
g_claimsLock.Release();
}
void DeviceDisconnected(uint8_t slotId) {
// Hot-unplug runs in deferred kernel context. Do not take the
// process-operation mutex: a control syscall may be polling the same
// event queue. The per-claim spinlock is enough to make callbacks and
// reads observe the disconnect atomically.
for (int i = 0; i < MaxClaims; i++) {
ClaimState& claim = g_claims[i];
claim.ringLock.Acquire();
if (claim.active && claim.slotId == slotId) {
claim.connected = false;
claim.streaming = false;
}
claim.ringLock.Release();
}
}
}
+38
View File
@@ -0,0 +1,38 @@
/*
* UserUsb.hpp
* Process-owned access to unbound USB interfaces.
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Api/Syscall.hpp>
namespace Drivers::USB::UserUsb {
// Enumerate the interfaces represented by the xHCI device table.
int List(montauk::abi::UsbInterfaceInfo* out, int maxCount);
// Exclusively claim an interface that has no in-kernel class driver.
// The returned handle is generation checked and belongs to the calling
// process. The current xHCI device model represents one interface per
// slot, so a claim is presently exclusive for the whole device slot.
int Claim(uint8_t slotId, uint8_t interfaceNumber);
int Close(int handle);
// Issue an EP0 control request. bmRequestType supplies the direction;
// dataLen is limited to one DMA page.
int Control(int handle, const montauk::abi::UsbControlRequest& request,
void* data, uint32_t dataLen);
// Continuous bulk-IN streaming into a kernel ring. Read is non-blocking.
int StartBulkIn(int handle, uint32_t transferBytes, uint32_t bufferCount);
int StopBulkIn(int handle);
int ReadBulkIn(int handle, uint8_t* out, uint32_t maxLen);
// Lifetime hooks used by process teardown and USB hot-unplug.
void ReleaseAllForPid(int pid);
void DeviceDisconnected(uint8_t slotId);
}
+37 -17
View File
@@ -10,7 +10,7 @@
#include "HidKeyboard.hpp"
#include "HidMouse.hpp"
#include "MassStorage.hpp"
#include "Radio/RtlSdr.hpp"
#include "UserUsb.hpp"
#include <Pci/Pci.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
@@ -167,8 +167,7 @@ namespace Drivers::USB::Xhci {
// 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).
// (observed as corrupted USB control reads while another core was polling).
static std::atomic<int> g_pollOwnerCpu{-1};
// Serialises non-nested (waiting) control transfers so only one EP0
@@ -198,7 +197,7 @@ namespace Drivers::USB::Xhci {
// 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
// under sustained high-rate input. PoolCount==0 selects 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] = {};
@@ -206,6 +205,7 @@ namespace Drivers::USB::Xhci {
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
static kcp::Spinlock g_bulkInPoolLocks[MAX_SLOTS + 1];
// Transfer callbacks for non-HID class drivers (per slot)
static TransferCallback g_transferCallbacks[MAX_SLOTS + 1] = {};
@@ -590,15 +590,19 @@ namespace Drivers::USB::Xhci {
// 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];
g_bulkInPoolLocks[slotId].Acquire();
uint32_t poolCount = g_bulkInPoolCount[slotId];
if (poolCount > 0) {
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) % poolCount;
}
g_bulkInPoolLocks[slotId].Release();
} else if (epDci == bulkInDci && g_transferCallbacks[slotId]) {
// Bulk IN — dispatch via registered callback.
// len = actually-transferred bytes (requested -
@@ -1149,13 +1153,18 @@ namespace Drivers::USB::Xhci {
// single-buffer start relies on.
void PrimeBulkInStream(uint8_t slotId) {
if (slotId == 0 || slotId > MAX_SLOTS || !g_devices[slotId].Active) return;
g_bulkInPoolLocks[slotId].Acquire();
uint32_t n = g_bulkInPoolCount[slotId];
if (n == 0) return;
if (n == 0) {
g_bulkInPoolLocks[slotId].Release();
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);
g_bulkInPoolLocks[slotId].Release();
}
void StartBulkInStream(uint8_t slotId, uint32_t xferLen, uint32_t numBuffers) {
@@ -1195,15 +1204,27 @@ namespace Drivers::USB::Xhci {
for (uint32_t i = 0; i < numBuffers; i++)
QueueBulkInTransfer(slotId, g_bulkInPool[slotId][i],
g_bulkInPoolPhys[slotId][i], xferLen);
g_bulkInPoolLocks[slotId].Acquire();
g_bulkInPoolCount[slotId] = numBuffers;
g_bulkInPoolLocks[slotId].Release();
}
bool IsBulkInStreamActive(uint8_t slotId) {
if (slotId == 0 || slotId > MAX_SLOTS) return false;
g_bulkInPoolLocks[slotId].Acquire();
bool active = g_bulkInPoolCount[slotId] != 0;
g_bulkInPoolLocks[slotId].Release();
return active;
}
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_bulkInPoolLocks[slotId].Acquire();
bool wasArmed = g_bulkInPoolCount[slotId] != 0;
g_bulkInPoolCount[slotId] = 0;
g_bulkInPoolLocks[slotId].Release();
if (!wasArmed) return;
// Flush the up-to-PoolCount TRBs still pending on the ring: Stop
@@ -1437,9 +1458,7 @@ namespace Drivers::USB::Xhci {
}
static void UnregisterClassDriver(uint8_t slotId, const UsbDeviceInfo& dev) {
if (Radio::IsRtlSdr(dev.VendorId, dev.ProductId)) {
Radio::UnregisterDevice(slotId);
} else if (dev.InterfaceClass == UsbDevice::CLASS_MASS_STORAGE) {
if (dev.InterfaceClass == UsbDevice::CLASS_MASS_STORAGE) {
MassStorage::UnregisterDevice(slotId);
} else if (dev.InterfaceClass == UsbDevice::CLASS_HID &&
dev.InterfaceProtocol == UsbDevice::PROTOCOL_KEYBOARD) {
@@ -1559,6 +1578,7 @@ namespace Drivers::USB::Xhci {
// Device disconnected — deactivate its slot
for (uint8_t s = 1; s <= MAX_SLOTS; s++) {
if (g_devices[s].Active && g_devices[s].PortId == port + 1) {
UserUsb::DeviceDisconnected(s);
UnregisterClassDriver(s, g_devices[s]);
g_devices[s].Active = false;
g_transferCallbacks[s] = nullptr;
+5 -2
View File
@@ -233,6 +233,7 @@ namespace Drivers::USB::Xhci {
struct UsbDeviceInfo {
bool Active;
bool Ready; // descriptors/endpoints and binding are complete
uint8_t PortId;
uint32_t Speed;
uint16_t VendorId;
@@ -242,6 +243,7 @@ namespace Drivers::USB::Xhci {
uint8_t InterfaceProtocol;
uint8_t InterfaceNumber;
uint8_t DeviceClass; // bDeviceClass from device descriptor
bool KernelDriverBound; // unavailable to a userspace interface claim
// Interrupt IN endpoint
uint8_t InterruptEpNum; // Endpoint number (1-15)
@@ -329,7 +331,7 @@ namespace Drivers::USB::Xhci {
// 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).
// Used by generic process-owned bulk streams after a transfer stall.
void ResetBulkInEndpoint(uint8_t slotId);
// Clear a halted bulk OUT endpoint and discard the errored/queued TRBs.
@@ -352,10 +354,11 @@ namespace Drivers::USB::Xhci {
// 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
// sustained high-rate sources. 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);
bool IsBulkInStreamActive(uint8_t slotId);
void PrimeBulkInStream(uint8_t slotId);
void StopBulkInStream(uint8_t slotId);
+6
View File
@@ -27,6 +27,7 @@
#include <Drivers/Audio/Mixer.hpp>
#include <Drivers/Graphics/IntelGPU.hpp>
#include <Ipc/Ipc.hpp>
#include <Drivers/USB/UserUsb.hpp>
// Assembly: context switch with CR3 and FPU state parameters
extern "C" void SchedContextSwitch(uint64_t* oldRsp, uint64_t newRsp, uint64_t newCR3,
@@ -1607,6 +1608,11 @@ namespace Sched {
// never stranded on the invisible buffer (no-op for non-owners).
Drivers::Graphics::IntelGPU::OnProcessExit(exitingPid);
// USB interface claims are process-owned capabilities. Closing them
// here stops DMA streaming and releases exclusivity even when an app
// exits without calling usb_close().
Drivers::USB::UserUsb::ReleaseAllForPid(exitingPid);
// Release process-scoped IPC handles/mappings before tearing down the address space.
Ipc::CleanupProcessSlot(slot, exitingPid, proc.pml4Phys);
montauk::abi::CleanupHeapForSlot(slot, proc.pml4Phys);