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