From 46c56b057b9e1fc019658eb305f5fdd6cce5d794 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Fri, 3 Jul 2026 10:36:41 +0200 Subject: [PATCH] fix: RTL-SDR bug fixes and accuracy improvements --- kernel/src/Api/BuildNo.hpp | 2 +- kernel/src/Drivers/Radio/Sdr.cpp | 10 +- kernel/src/Drivers/USB/Radio/R820t.cpp | 134 +++++++++++++++++++----- kernel/src/Drivers/USB/Radio/RtlSdr.cpp | 69 +++++++++--- kernel/src/Drivers/USB/Xhci.cpp | 33 ++++++ 5 files changed, 201 insertions(+), 47 deletions(-) diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index b3a52e6..40ae102 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 8 +#define MONTAUK_BUILD_NUMBER 9 diff --git a/kernel/src/Drivers/Radio/Sdr.cpp b/kernel/src/Drivers/Radio/Sdr.cpp index 792b130..f8cb1c2 100644 --- a/kernel/src/Drivers/Radio/Sdr.cpp +++ b/kernel/src/Drivers/Radio/Sdr.cpp @@ -155,7 +155,12 @@ namespace Drivers::Radio::Sdr { uint32_t space = RING_BYTES - r.count; uint32_t n = len; uint32_t dropped = 0; - if (n > space) { dropped = n - space; n = space; } + if (n > space) { + // Truncate to a whole number of I/Q byte pairs: dropping an odd + // count would swap I and Q for the rest of the stream. + n = space & ~1u; + dropped = len - n; + } uint32_t first = RING_BYTES - r.head; if (first > n) first = n; @@ -241,6 +246,9 @@ namespace Drivers::Radio::Sdr { int Start(int handle) { Receiver* r = Lookup(handle, true); if (!r) return -1; + // Already streaming: a second Start must not re-arm the driver's + // transfer pool (it would double-queue every buffer). + if (r->streaming) return 0; r->lock.Acquire(); r->head = r->count = 0; // discard stale samples before (re)starting diff --git a/kernel/src/Drivers/USB/Radio/R820t.cpp b/kernel/src/Drivers/USB/Radio/R820t.cpp index f1e80e9..c1e5bc8 100644 --- a/kernel/src/Drivers/USB/Radio/R820t.cpp +++ b/kernel/src/Drivers/USB/Radio/R820t.cpp @@ -120,12 +120,16 @@ namespace Drivers::USB::Radio { 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. + // Read `len` bytes starting at register 0. The read pointer must be set + // to 0 with an address-only write first: a preceding register write leaves + // it pointing past the last register written, which would return the wrong + // registers here. The status registers are bit-reversed on the wire, so + // the PLL/VCO read paths un-reverse each byte to recover the logical value. static bool Read(R820tDev& d, uint8_t* out, uint8_t len) { uint8_t raw[16]; if (len > sizeof(raw)) len = sizeof(raw); + uint8_t ptr = 0x00; + if (!RtlI2cWrite(d.slotId, R820T_I2C_ADDR, &ptr, 1)) return false; if (!RtlI2cRead(d.slotId, R820T_I2C_ADDR, raw, len)) return false; for (uint8_t i = 0; i < len; i++) out[i] = BitRev(raw[i]); return true; @@ -135,6 +139,79 @@ namespace Drivers::USB::Radio { // Detection / init // ========================================================================= + static bool SetPll(R820tDev& d, uint64_t freqHz); // defined below + + // IF filter setup for the SDR receive path (the "BW < 6 MHz" digital-TV + // profile): calibrate the filter at a 56 MHz LO, then program the filter + // code, bandwidth / HP corner, image-rejection side and filter gain. + static bool ApplyIfFilterConfig(R820tDev& d) { + const uint8_t filtGain = 0x10; // +3 dB, 6 MHz on + const uint8_t imgR = 0x00; // image negative + const uint8_t filtQ = 0x10; // low Q + const uint8_t hpCor = 0x6b; // 1.7 MHz disable, +2 cap, 1.0 MHz corner + + bool ok = true; + ok &= WriteRegMask(d, 0x0c, 0x00, 0x0f); // init flag & xtal check result + ok &= WriteRegMask(d, 0x13, 49, 0x3f); // version number + ok &= WriteRegMask(d, 0x1d, 0x00, 0x38); // LT gain test + + // Filter calibration: park the PLL at 56 MHz, pulse the calibration + // trigger, read the resulting filter code back (status reg 4, low + // nibble). One retry; 0x0f means the calibration failed (use 0). + uint8_t calCode = 0; + for (int i = 0; i < 2; i++) { + ok &= WriteRegMask(d, 0x0b, hpCor, 0x60); // filt_cap + ok &= WriteRegMask(d, 0x0f, 0x04, 0x04); // calibration clock on + ok &= WriteRegMask(d, 0x10, 0x00, 0x03); // xtal cap 0 pF for PLL + SetPll(d, 56000000ull); // lock not required here + ok &= WriteRegMask(d, 0x0b, 0x10, 0x10); // start trigger + ok &= WriteRegMask(d, 0x0b, 0x00, 0x10); // stop trigger + ok &= WriteRegMask(d, 0x0f, 0x00, 0x04); // calibration clock off + + uint8_t data[5] = {0}; + if (!Read(d, data, sizeof(data))) return false; + calCode = (uint8_t)(data[4] & 0x0f); + if (calCode && calCode != 0x0f) break; + } + if (calCode == 0x0f) calCode = 0; + + ok &= WriteRegMask(d, 0x0a, (uint8_t)(filtQ | calCode), 0x1f); + ok &= WriteRegMask(d, 0x0b, hpCor, 0xef); // bandwidth, filter gain, HP corner + ok &= WriteRegMask(d, 0x07, imgR, 0x80); // image rejection side + ok &= WriteRegMask(d, 0x06, filtGain, 0x30);// filt_3dB + ok &= WriteRegMask(d, 0x1e, 0x60, 0x60); // channel filter extension @ LNA max-1 + ok &= WriteRegMask(d, 0x05, 0x01, 0x80); // loop-through + ok &= WriteRegMask(d, 0x1f, 0x00, 0x80); // loop-through attenuation enable + ok &= WriteRegMask(d, 0x0f, 0x00, 0x80); // filter extension widest: off + ok &= WriteRegMask(d, 0x19, 0x60, 0x60); // RF poly filter current: min + return ok; + } + + // Receive-path operating point for the digital/SDR profile: LNA and mixer + // detector top points and thresholds, input select, charge-pump and + // divider-buffer currents, AGC clock rate. + static bool ApplySysFreqConfig(R820tDev& d) { + bool ok = true; + ok &= WriteRegMask(d, 0x1d, 0xe5, 0xc7); // LNA top (detect bw 3, top 4) + ok &= WriteRegMask(d, 0x1c, 0x24, 0xf8); // mixer top 13, top-1, low-discharge + ok &= WriteReg(d, 0x0d, 0x53); // LNA vth 0.84 / vtl 0.64 + ok &= WriteReg(d, 0x0e, 0x75); // mixer vth 1.04 / vtl 0.84 + ok &= WriteRegMask(d, 0x05, 0x00, 0x60); // air-in input select + ok &= WriteRegMask(d, 0x06, 0x00, 0x08); // cable-2 input off + ok &= WriteRegMask(d, 0x11, 0x38, 0x38); // charge-pump current: auto + ok &= WriteRegMask(d, 0x17, 0x30, 0x30); // divider buffer current 150u + ok &= WriteRegMask(d, 0x0a, 0x40, 0x60); // filter current: low + ok &= WriteRegMask(d, 0x1d, 0x00, 0x38); // LNA top: lowest + ok &= WriteRegMask(d, 0x1c, 0x00, 0x04); // normal mode + ok &= WriteRegMask(d, 0x06, 0x00, 0x40); // pre-detect off + ok &= WriteRegMask(d, 0x1a, 0x30, 0x30); // AGC clock 250 Hz + ok &= WriteRegMask(d, 0x1d, 0x18, 0x38); // LNA top = 3 + ok &= WriteRegMask(d, 0x1c, 0x24, 0x04); // mixer top bit + ok &= WriteRegMask(d, 0x1e, 0x0e, 0x1f); // LNA discharge current 14 + ok &= WriteRegMask(d, 0x1a, 0x20, 0x30); // AGC clock 60 Hz + return ok; + } + bool R820tDetect(uint8_t slotId) { // Match the reference driver's chip-id probe: set the read pointer to // register 0, then read one byte *without* bit-reversal and compare it @@ -161,13 +238,21 @@ namespace Drivers::USB::Radio { 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. + // Load the init register block, then bring the IF filter and the + // receive-path operating point to the SDR profile (incl. the 56 MHz + // filter calibration). if (!Write(d, 0x05, kInitArray, sizeof(kInitArray))) { KernelLogStream(ERROR, "R820T") << "init register write failed"; return false; } + if (!ApplyIfFilterConfig(d)) { + KernelLogStream(ERROR, "R820T") << "IF filter configuration failed"; + return false; + } + if (!ApplySysFreqConfig(d)) { + KernelLogStream(ERROR, "R820T") << "receive path configuration failed"; + return false; + } d.inited = true; KernelLogStream(OK, "R820T") << "Tuner initialised (xtal=" @@ -208,9 +293,8 @@ namespace Drivers::USB::Radio { 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); + uint32_t pllRef = d.xtal; + uint32_t freqKhz = (uint32_t)((freqHz + 500) / 1000); bool ok = true; ok &= WriteRegMask(d, 0x10, 0x00, 0x10); // refdiv = /1 @@ -240,28 +324,21 @@ namespace Drivers::USB::Radio { 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); + + // Exact fractional-N split: vcoDiv = round(65536 * vcoFreq / (2*ref)). + // The top bits are the integer divider, the low 16 bits feed the + // sigma-delta modulator directly (no iterative approximation). + uint64_t vcoDiv = (pllRef + 65536ull * vcoFreq) / (2ull * pllRef); + uint32_t nint = (uint32_t)(vcoDiv >> 16); + uint16_t sdm = (uint16_t)(vcoDiv & 0xffff); if (nint < 13) nint = 13; // keep ni/si arithmetic well-defined uint8_t ni = (uint8_t)((nint - 13) / 4); uint8_t si = (uint8_t)(nint - 4 * ni - 13); ok &= WriteReg(d, 0x14, (uint8_t)(ni + (si << 6))); - // Sigma-delta fractional path. - 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; - } + // Sigma-delta fractional path (powered down when the fraction is 0). + ok &= WriteRegMask(d, 0x12, sdm ? 0x00 : 0x08, 0x08); ok &= WriteReg(d, 0x16, (uint8_t)(sdm >> 8)); ok &= WriteReg(d, 0x15, (uint8_t)(sdm & 0xff)); @@ -309,9 +386,10 @@ namespace Drivers::USB::Radio { 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); + // NOTE: the Air-in vs Cable-1 input switch at 345 MHz applies to the + // R828D only. The R820T/T2 uses the air input exclusively (selected + // during init); writing the Cable-1 select on an R820T disconnects the + // antenna input below 345 MHz. return true; } diff --git a/kernel/src/Drivers/USB/Radio/RtlSdr.cpp b/kernel/src/Drivers/USB/Radio/RtlSdr.cpp index e072bdc..f91714c 100644 --- a/kernel/src/Drivers/USB/Radio/RtlSdr.cpp +++ b/kernel/src/Drivers/USB/Radio/RtlSdr.cpp @@ -64,6 +64,8 @@ namespace Drivers::USB::Radio { static int g_ppm = 0; static int g_manual = 0; // tuner gain mode (0=auto) static int g_gain = 0; // tuner gain, tenths of dB + static int g_directSamp = 0; // 0=tuner path, 1=I ADC, 2=Q ADC + static uint64_t g_lastFreq = 0; // last successfully tuned freq (Hz) // Bulk-IN streaming geometry. We keep BULK_POOL_BUFS transfers of // BULK_XFER_LEN bytes outstanding at once (multi-URB), so the RTL2832U FIFO @@ -92,14 +94,26 @@ namespace Drivers::USB::Radio { g_ctlBuf, false) == Xhci::CC_SUCCESS; } + static uint8_t DemodRead(uint8_t page, uint16_t addr) { + if (!g_ctlBuf) return 0; + uint16_t raddr = (uint16_t)((addr << 8) | 0x20); + g_ctlBuf[0] = 0; + Xhci::ControlTransfer(g_slotId, CTRL_IN, 0, raddr, page, 1, g_ctlBuf, true); + return g_ctlBuf[0]; + } + static bool DemodWrite(uint8_t page, uint16_t addr, uint16_t val, uint8_t len) { if (!g_ctlBuf) return false; uint16_t waddr = (uint16_t)((addr << 8) | 0x20); uint16_t index = (uint16_t)(0x10 | page); g_ctlBuf[0] = (len == 1) ? (uint8_t)(val & 0xff) : (uint8_t)(val >> 8); g_ctlBuf[1] = (uint8_t)(val & 0xff); - return Xhci::ControlTransfer(g_slotId, CTRL_OUT, 0, waddr, index, len, - g_ctlBuf, false) == Xhci::CC_SUCCESS; + bool ok = Xhci::ControlTransfer(g_slotId, CTRL_OUT, 0, waddr, index, len, + g_ctlBuf, false) == Xhci::CC_SUCCESS; + // Dummy status read after every demod write (reference behaviour); + // acts as a write barrier so the register latches before the next op. + DemodRead(0x0a, 0x01); + return ok; } static void SetI2cRepeater(bool on) { @@ -246,9 +260,16 @@ namespace Drivers::USB::Radio { static int DoSetFreq(uint64_t hz) { if (!EnsureInit()) return -1; + if (g_directSamp) { + // Tuner is bypassed: tuning is the demod's digital downconverter. + SetIfFreq((uint32_t)hz); + g_lastFreq = hz; + return 0; + } SetI2cRepeater(true); bool ok = R820tSetFreq(g_tuner, hz); SetI2cRepeater(false); + if (ok) g_lastFreq = hz; return ok ? 0 : -1; } @@ -256,9 +277,12 @@ namespace Drivers::USB::Radio { if (!EnsureInit()) return -1; // The RTL2832 resampler does not cover 300k..900k. if (rate <= 225000 || rate > 3200000 || - (rate > 300000 && rate < 900000)) return -1; + (rate > 300000 && rate <= 900000)) return -1; - uint32_t ratio = (uint32_t)(((uint64_t)g_rtlXtal * TWO_POW22) / rate); + // The ratio uses the NOMINAL crystal frequency: ppm correction is + // applied by the demod's sample-frequency-offset registers below, so + // baking it into the ratio too would correct the rate twice. + uint32_t ratio = (uint32_t)(((uint64_t)RTL_XTAL * TWO_POW22) / rate); ratio &= 0x0ffffffc; DemodWrite(1, 0x9f, (uint16_t)((ratio >> 16) & 0xffff), 2); DemodWrite(1, 0xa1, (uint16_t)(ratio & 0xffff), 2); @@ -266,7 +290,7 @@ namespace Drivers::USB::Radio { ApplySampleFreqCorrection(); DemodWrite(1, 0x01, 0x14, 1); // soft reset DemodWrite(1, 0x01, 0x10, 1); - SetIfFreq(R82XX_IF); + SetIfFreq(g_directSamp ? (uint32_t)g_lastFreq : R82XX_IF); return 0; } @@ -295,6 +319,9 @@ namespace Drivers::USB::Radio { g_rtlXtal = (uint32_t)((int64_t)RTL_XTAL + (int64_t)RTL_XTAL * ppm / 1000000); g_tuner.xtal = g_rtlXtal; ApplySampleFreqCorrection(); + // The tuner PLL (and, in direct mode, the DDC) derive from the xtal; + // retune so the new correction actually takes effect. + if (g_lastFreq) return DoSetFreq(g_lastFreq); return 0; } @@ -310,18 +337,25 @@ namespace Drivers::USB::Radio { SetI2cRepeater(true); R820tStandby(g_tuner); SetI2cRepeater(false); - DemodWrite(1, 0xb1, 0x1b, 1); // zero-IF path - DemodWrite(0, 0x08, 0x4d, 1); + DemodWrite(1, 0xb1, 0x1a, 1); // disable zero-IF + DemodWrite(1, 0x15, 0x00, 1); // no spectrum inversion + DemodWrite(0, 0x08, 0x4d, 1); // In-phase ADC input DemodWrite(0, 0x06, (mode == 2) ? 0x90 : 0x80, 1); // Q vs I ADC - SetIfFreq(0); - DemodWrite(1, 0x15, 0x00, 1); + g_directSamp = mode; + // Tuning now happens in the DDC; carry the current frequency over. + SetIfFreq((uint32_t)g_lastFreq); } else { - // Restore the R820T2 low-IF receive path. - DemodWrite(1, 0xb1, 0x1a, 1); - DemodWrite(0, 0x08, 0x4d, 1); - DemodWrite(0, 0x06, 0x80, 1); + // Restore the R820T2 low-IF receive path. Standby powered the + // tuner down, so it needs a full re-initialisation. + SetI2cRepeater(true); + bool ok = R820tInit(g_tuner, g_slotId, g_rtlXtal, R82XX_IF); + SetI2cRepeater(false); + if (!ok) return -1; SetIfFreq(R82XX_IF); - DemodWrite(1, 0x15, 0x01, 1); + DemodWrite(1, 0x15, 0x01, 1); // enable spectrum inversion + DemodWrite(0, 0x06, 0x80, 1); // default ADC I/Q datapath + g_directSamp = 0; + if (g_lastFreq) return DoSetFreq(g_lastFreq); } return 0; } @@ -447,11 +481,10 @@ namespace Drivers::USB::Radio { bool IsRtlSdr(uint16_t vid, uint16_t pid) { if (vid != 0x0bda) return false; // Realtek Semiconductor switch (pid) { + // Only the two known RTL2832U ids. In particular 0x2831 is the + // RTL2831U, a DIFFERENT demod this driver cannot program. case 0x2832: // RTL2832U (generic) case 0x2838: // RTL2838 (most RTL-SDR.com dongles) - case 0x2831: // RTL2831U - case 0x2837: // RTL2832U variant - case 0x2834: // RTL2832U variant return true; default: return false; @@ -473,6 +506,8 @@ namespace Drivers::USB::Radio { g_rtlXtal = RTL_XTAL; g_manual = 0; g_gain = 0; + g_directSamp = 0; + g_lastFreq = 0; g_tuner = R820tDev{}; g_ctlBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed(); diff --git a/kernel/src/Drivers/USB/Xhci.cpp b/kernel/src/Drivers/USB/Xhci.cpp index 4d0f11e..e247401 100644 --- a/kernel/src/Drivers/USB/Xhci.cpp +++ b/kernel/src/Drivers/USB/Xhci.cpp @@ -1024,6 +1024,10 @@ namespace Drivers::USB::Xhci { if (slotId == 0 || slotId > MAX_SLOTS || !g_devices[slotId].Active) return; UsbDeviceInfo& dev = g_devices[slotId]; if (!dev.BulkInRing || dev.BulkInEpNum == 0) return; + // Already armed: priming again would put a second transfer on the ring + // for every pool buffer and desync the completion rotation (callbacks + // would be handed the wrong buffer). Callers must Stop first. + if (g_bulkInPoolCount[slotId] != 0) return; if (numBuffers < 1) numBuffers = 1; if (numBuffers > BULK_IN_POOL_MAX) numBuffers = BULK_IN_POOL_MAX; @@ -1060,7 +1064,36 @@ namespace Drivers::USB::Xhci { 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. + bool wasArmed = g_bulkInPoolCount[slotId] != 0; g_bulkInPoolCount[slotId] = 0; + if (!wasArmed) return; + + // Flush the up-to-PoolCount TRBs still pending on the ring: Stop + // Endpoint, then Set TR Dequeue to the enqueue pointer. Without this a + // later Start would stack a fresh pool behind the stale TRBs and + // overflow the 32-entry transfer ring (16 stale + 16 new > 31 usable), + // wrapping the enqueue pointer onto still-pending TRBs. + UsbDeviceInfo& dev = g_devices[slotId]; + if (!dev.Active || dev.BulkInEpNum == 0 || !dev.BulkInRing) return; + uint8_t dci = dev.BulkInEpNum * 2 + 1; + + TRB stopTrb = {}; + stopTrb.Control = (TRB_STOP_ENDPOINT << TRB_TYPE_SHIFT) + | ((uint32_t)slotId << 24) + | ((uint32_t)dci << 16); + SendCommand(stopTrb); + + 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); } // -------------------------------------------------------------------------