From 5363ca52650f5e830c20d534876cdcb4cc3396d4 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Mon, 22 Jun 2026 11:49:16 +0200 Subject: [PATCH] feat: add RTL-SDR, R820t drivers, radio APIs, and sdr demo tool --- kernel/src/Api/BuildNo.hpp | 2 +- kernel/src/Api/Sdr.hpp | 55 ++ kernel/src/Api/Syscall.cpp | 21 + kernel/src/Api/Syscall.hpp | 41 ++ kernel/src/Drivers/Radio/Sdr.cpp | 351 ++++++++++++ kernel/src/Drivers/Radio/Sdr.hpp | 119 ++++ kernel/src/Drivers/USB/Radio/R820t.cpp | 370 ++++++++++++ kernel/src/Drivers/USB/Radio/R820t.hpp | 61 ++ kernel/src/Drivers/USB/Radio/RtlSdr.cpp | 540 ++++++++++++++++++ kernel/src/Drivers/USB/Radio/RtlSdr.hpp | 37 ++ kernel/src/Drivers/USB/UsbDevice.cpp | 16 +- kernel/src/Drivers/USB/Xhci.cpp | 185 +++++- kernel/src/Drivers/USB/Xhci.hpp | 19 + programs/include/Api/Syscall.hpp | 41 ++ programs/include/libc/montauk.h | 9 + programs/include/montauk/syscall.h | 57 ++ .../obj/src/libprintersapplet.o | Bin 18648 -> 19664 bytes .../obj/src/libtimezonesapplet.o | Bin 18792 -> 19808 bytes programs/src/sdr/main.cpp | 260 +++++++++ template/sysroot/include/Api/Syscall.hpp | 41 ++ 20 files changed, 2213 insertions(+), 12 deletions(-) create mode 100644 kernel/src/Api/Sdr.hpp create mode 100644 kernel/src/Drivers/Radio/Sdr.cpp create mode 100644 kernel/src/Drivers/Radio/Sdr.hpp create mode 100644 kernel/src/Drivers/USB/Radio/R820t.cpp create mode 100644 kernel/src/Drivers/USB/Radio/R820t.hpp create mode 100644 kernel/src/Drivers/USB/Radio/RtlSdr.cpp create mode 100644 kernel/src/Drivers/USB/Radio/RtlSdr.hpp create mode 100644 programs/src/sdr/main.cpp 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 7c7439226bfb2d7f8c011a35d5b00c7c584bb698..a497a88f266312442054ed06200adbf403da052b 100644 GIT binary patch literal 19664 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 V(u~^>n9~!rzlr_dB^rpT`cJ3(4uSvx literal 18648 zcmb_j33yz^k?yC_NV3M3CCkQ^FG;?zF&bI&iSM~&jXlx~bJ&&%o{XfCG`2Jnqmi*q zz~ROwCV>rCmYWY2Nbr(uF4zyg5EeEhfh_DofL)WoCQeMQ4>sgnlC7$)o_Q+E`S$A1 zs`vN*s;jH3tNR`Ed?XNRE3qtPilsKIV$HBh&GHoSR)Skqy_&Duk2|sU+=}(hsgaS< z=8;?`n@{J8z?~lKBYiCrU!M-3{pgQ9@j%OuV(rK0A>V%V2ce^V`dDs^oH zug?J%VD(HqHd>|C6{nEET1_P?XgsZQH{b^1YYE>s3w27TR}SC_OF18cY8`S7r86s5 z14+uWs?Z&c>j;+;Kf7iaR4NG1AzV#(F5y|q`7>;$szRWJe^YS`^@=0pM2gN~?{w}6 zoGZa5lVU*4Zm4!XYE@(XKBy9S%$kC@Lf1ZTP5m1@Q(XwCc3=*+aemoy(R{Be zTV=Ukg<56BCf5l#DoU7gm+PHLN=-?1XH@z0`vFq-RrS}Q1;SGaYqzQimk?h=*g<$2 z;Znl2gq?(^6D}h>gK#F%2uOz&f@G8Pf2(Ko5 zF5xwVml9q}cp2e!gwG><0pSk9>j`%f-at4^cq8HEgf|gBpYUeFO@y})b`#!8xS6nr z@Cw3S!Yzb-gj)&w39lp^AiRoj8{yT2gM`-*-cEQe;T?q65e^Z)fN%%l^@KYKZy+2d zoFW_{ypiyQgtroo67~>|5%v;}6ZR8M5Wa%&Zo-=gcM;x9cn{$%gfAi-AiS5bk8p}` z8{uxk+X(j%ZYSJJI7m26cst=f!aE4}6AlsHM>s-wKj8}r4-$?N9wHngJVH25_!7bi z!a2e_36B!qML17*H{nYOcM(2Jcn{&r2wz0_3c?>Fe1!1Dgg;4mFX1Z*CkbCg_~V4H zCfrT<8p1t`S<2;Wcm6NDcid^zC<2_GT+FyT)UeuVIqgg;OCD#BkNd^O?63136_ z1mSB5f06KYguhJqdct2Ld;{U92p=W}cRJqO4T2#4RS4!Zv z0ta=FZW8cj>l<(_FT>jt5~_htpG&uHe!yjTCsh=119%#Atq)OP{}x_gExf8(i&5a- zY~e;`;cjE$HejVtVDej-aTZRwbu)^)Q9O#`D=3~t@e+zRQT!Oi`zSs_G06d928u=$ zO(-rv5kPSvii=TXP>i9t62);8ccOR*#TQXLjp9WVuc3Gc#V=6&9)%O`&8!*}^H3~9 zu^NR3MF)y56loNfptu~xQ53hLxF5yiD4s;|Jc?IPoJ4UN#fK;?xVN>Ypr}K!7)1+; zO(=pW5-3tAhEN#_h3R$UF=<(smUfpymhQ&X}A;Zhx3!OIkg10oyn;d zwNistwH3j+xpZEYEUobahQ~hg-=Uv;93`}+IZE|GKl?eVwMsRl3lwXW+6G;gtWu7u z^^N5lTo<^4wHxc^x(4A{&ap_jT$^1E@St=GDU@o3b+Catm#84LEOXMK+^Je>OJ;t} z3lEYN=a8;L>q0oDDpz9#Dpyvh^)94mLB(bIblUrz?1Trh^^GMP8qY-<9o+iDvc z9oV`S`hgLb7$>LKSc|J_U5@%W^@*7^7CsQq;NiopodZr(Y1fsSSql}HQ&-W}EOLMb z7OY(d@vS2bS5aejt>tooXTX4y*(z)`hr&ceuoliKHJ679DyT58*eO=$Q@pqrn4&VA zqI$QrrOvpPt-`rO-G(!1SYvhrgLNhF1i!$p7p$$Zrj>7K7H5cnbDKT}st_D^WUsY3YMj;$C=)H;Y8g zuPkf?MGFg6 z;7V3%m6f`xN~g|;vt0fgtX`9#uh%LzT&gC)R)Ae(pk`UtAFHO|0rv;^15iKSb8I9^ ziB(@R6}A#;!GZ=&@ddwNsiPoQX&q6 zs8U;6;;eR@gbUuJTIY1fi6aPVOF^~FF=D~VNsj6UtU5+^>)LKz+sNn2epY4yvjux{ z?1!$JHQwNaJE#)qG}aYM?h1DLq5*h&r?BR0_Xne@XCReZ*_zBV6%6@<;pDDpkd?Lt z;_*Fn&Xa&MzEC)(TYKZNK-}Z^N9p8_P8YgU0s<$4A-9bC7jiE7#5rd+zW>F{ti4d;4>hI`Y4n?{FHgM$rC{S8gw)`li{ zowTVxJ4U9O`ucL|^k_bn?agH2d>^!jTF?Hbk=$@T-IE{A0jZgufppLQP1#`}B0-anmT6V6HcnPZjEFEIXR%&!&4D@bw~wu+0`u2yP@A zhy^<1IuM~iAd>8eQN)8(O?D*AR7E|p z_GB~=2}iYcZ`>E|>IlOe#^Sg{ZIO-yAQ@=s`PIPiXkHBsXY;AC{nVsr+7}9Vv>82ek2erf_})D^ zlFAQ|vPnI53%i6_^|f<5N(zb?o*^qRDLP@i@wRXjLZxq6X0~j6vJsS?zMSwZ$+g@G{_sW zClm_n_>Z?on+~eN@PeJ3s@^zElG$SMq!*Wg%~_05VQZqLiS8(v=6F{mkZkkBSGJM^ zCUfAoI;ulb;rdSK8G`|t4cQy_hH)ZOx&BeoglI;>p^zFqG};4~!dAU6^$w4952kJD zhcTfG#rR7|B&>UAi@~z(fEh^c>0HbE#_G|L)TP-#WJgb|C8?mFWLwCyE!ma`g<_D1 zuzEA2$yD!9M$Z)}Bswvm<9pyM0!0}%Q}Q|1o{0Oyutq4L0SGAU=%WL}xqRW83I28j zLy(IBh%bz{XcTAAwv4@TN?6K6lY(adr4+?=+mjlB)Edm>Gie>QHkc{s6Z?l{7U=Tq zgo_i+6kci9nZ=`59j(Ig4)990`2h}lqT6&dP5=8#nJm1UX$X!h~g`6rdLM< zw+vVbxWn6~V4a!Oz|hc;i56TpydJ*4@+TtZVh2NkDKHt0_ZWIVHCZ1GbcA>6tZ)sguyzRmnB-?E<*!o~y7pKk}Px7)Va=0>V^T@jq86c&? zbqaTywkTX?%<_fXAH8}bo~X`398sG;5DM@eSx2A)`{OkRYewN!D+w3dzD$4ND#ME( zcQ(bhjx+|nBF=~+gb9L-Yf&zX!kr+@emKOFjeB~M24O?3BOdm|L2KLI`kJ6 z<4)lx9G0pqa18~7kN1VwEjAYU!Yc>C)9Zq999A4Q`}UI#HQ8P*V2853{VcF(XR57M zgbw}wvq&&JIrym)LlF zHZ|FJ?DtA+1Z9;zkbUGYW5)x6znNZe!7qV=#~(Pbbm>FE&flYBP(Dsl`;YdIbH8c9 zpVIFS)un>>V_>0tLLUnDcXLp05_~)Ndyn9cv;Bt!KfwB57W{)e-fswAe^sW`cLkr~ zalIjUH^=Aaf?vt~{$B7$IO&``ZdAj#eV^^r2!0>OVV>aiZ}v(p6a4RZL4*argWs_3 z7yP5_&t-y-v7KuKKbQGi1dl(pVtG*TEo}cW!9T}a#@`A)!*>2z@Go({?+Lzw7r}1? zzn%Nlzqc3jbCC7)H(f>k7WPMfTUF%$gY7I~zs!8i;QZ0w5f%0Onb+Up6nPiNUw=PS zM21-}(gaWji^+H)=EL3c-Jq^YfVCy=?zB!Lu3lfZ%`5ex4Bg#XK)h3H}7<&kKS- z#CiCt;GbfD-WL2?_WwP>FEjhU;72$<=6Y`C{o|ZhRXh&EPiFnug8w_NFA@BGoDZ#n z|0CBo3%-s0+%EVq^E(B97w2cU;CFDpS;4n5e^~I>bNxEOAL9ACMey@^zU~qH7LMCv zf`5_c@vDOWHQV{7;49hBmj%Cx6Yed+>+jZ-IwkmLc)mXn{8b#cKMDTZJl;wkhne^D z*w1qWe}eOEf#BaY`@i6S$o;Js{AP|%o8a+z3QJV*7xB3E3jRi}?-Trb&YuH<|1I0O zO7QsXf#oK_e}TvKIl({8@qAeDpXKp>N${0y=NZ9oXMese_-i>nuM2)3+kaQ^`rBWn zeku50aKC>L{F_`a<9Rmo{yo;8Ciwf=AF~b$`kc2WUku-)!>QH?JJ)l*c?Dm>ap)9$ zAJ5|+!KZj%>JxlD>*oZ2H;?NI!9U6V9230${{u?hCirr$KOp$8aXihwZsLF+L}GbL z=#TJtUl9Ch)_+y-*Kqx9!8fy=_XOX_{BH!mhv%_`*Rk<)J?B-G;4k6%oh|s+Iscak zKF|HN3jQ*-zgh4{*w5{PAK`fJ6nsdBQtP*3jSUmSGVBt6HzQ# z!Q*GoSPl!`yDKih2j97@vPEA+p`d7H8EXu`bj8xlNktLng6>>sh|W5|sDjW!A&Fs z$J+tz54@F=rdh9Yej9`xt`^?IOaB+1MgM}c@Me9;e$D%EY2U0roIaeokFnh)d^Egy ze{JT=@DH$_Tj+nC>#c(S3D?&M-n@5SFZgD37|LdSpg5zS9_HHxpJTp5@Ha4T^2GRm z5A(Z({v*trco_Y!FrO0o-(tQ`@UJkxU+^cH9}ztMuOlq>JS@=^$-5b&nms=om`7c^ zzWF@`IlI1cj~*Jzr@G;o&*|fULHYE-ymI5`rOMqs3P1kl(t{~CvTpI)R^baycWxN< z<$3;E1N&%kxBLY|=CclCXIula3_id~_uw#mZ{$AP&vN8wZ@PP|AHL7%8-_s;PIY(Z z(g(PEO@W=k%qae{ZEKUtrLz6-t&W|`W&6)m9L!|X#yGfYAks;y_YA`q1@IxRwv--7 z_Q9tFX`ZA1%V7>Q#<@59jRUa1ybczi)7( z20ogDtUnAVCi1_X{Wo#M_~V`_{eKHM;|st-`5~jb`ToJ2kh-tIiHZ9E4EO&@{(&0% z$9rAb|0&?4O`z1}_@g_NefG^0>QWc4Qf--9nW+Dta{u=E!y(E37o4U4r@8+g@}ekY|MORqI*$e}wxt*KzD0zfO_k_i_I#urpg}nyxFO z<^hY(nf}eO^DOP}YtRkeH;QLzzxOQdZ(g7qa2U9CF;|RP{5=`5NOScFx8Gf8seu{4 zq5GlnMDsVo4}K;t7{A%@XWGwV0~7U4rxt33Cr|;(Jov-CQ?|#)a=CW_rJh)XQmJlz XC`dL}sAEn~)c!v9|A1&9s_MT0Ocirp diff --git a/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o b/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o index 03236a05ca02d73add0967a5483afee4deb7ebaa..ad5af58cb4f4beea5c53f6dc59a71b999cc1ceec 100644 GIT binary patch literal 19808 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 U`G+gt)Nvb5Q& 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 Sv^iP(H`xDc(Lq}gRrQ|-1cZVB 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