feat: add RTL-SDR, R820t drivers, radio APIs, and sdr demo tool

This commit is contained in:
2026-06-22 11:49:16 +02:00
parent 121f3f2081
commit 108538f60c
20 changed files with 2213 additions and 12 deletions
+176 -9
View File
@@ -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) {