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