fix: move RTL-SDR to userspace
This commit is contained in:
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 151
|
||||
#define MONTAUK_BUILD_NUMBER 155
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user