feat: add RTL-SDR, R820t drivers, radio APIs, and sdr demo tool
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user