wip: async BT firmware download attempt — DO NOT MERGE, breaks AX211 bring-up

Deferring the Intel BT firmware download off the boot path made the AX211
bootloader stop answering after the first FC05; even the final synchronous
revert freezes boot, so one of the 'neutral' fixes kept in this diff breaks
the bring-up on its own (candidates: BT-TRACE logging inside TransferCallback,
unconditional interrupt-IN re-queue after error completions on a halted EP,
xHCI interrupt-IN ZLP len fix interacting with HID, InPollContext owner
check).  Full history + next experiments in memory notes, 2026-07-05/06.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 15:46:06 +02:00
parent fcf8ae1e0b
commit f437601b76
9 changed files with 163 additions and 31 deletions
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 17
#define MONTAUK_BUILD_NUMBER 22
+36 -7
View File
@@ -15,6 +15,8 @@
#include <CppLib/Stream.hpp>
#include <Libraries/Memory.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <Hal/SmpBoot.hpp>
#include <atomic>
using namespace Kt;
@@ -31,8 +33,10 @@ namespace Drivers::USB::Bluetooth {
// True when the USB transport is up but the firmware-dependent HCI init is
// still waiting for the ramdisk (drive 0) to be mounted. Set when an
// adapter enumerates during the boot port scan, which runs before the boot
// filesystems are mounted; cleared by ServiceDeferredInit() once VFS is up.
static bool g_initPending = false;
// filesystems are mounted; claimed (atomically -- the pickup runs from the
// idle loop, concurrently with the rest of the system) by
// ServiceDeferredInit() once VFS is up.
static std::atomic<bool> g_initPending{false};
// Forward declaration: firmware-dependent HCI bring-up, run once VFS is up.
static void CompleteInit();
@@ -236,15 +240,19 @@ namespace Drivers::USB::Bluetooth {
// Start the event pipe BEFORE sending any HCI commands.
// HCI command responses arrive as events on the interrupt IN endpoint,
// so it must be queued to receive them.
// so it must be queued to receive them. Deliberately done HERE, at
// enumeration time, not in the deferred bring-up: this preserves the
// exact transport timing of the original synchronous boot path.
Hci::StartEventPipe();
// The firmware download path reads the .sfi/.ddc images from the
// ramdisk (drive 0). Adapters present at boot enumerate during the
// xHCI port scan, which runs before the boot filesystems are mounted,
// so defer the firmware-dependent bring-up until VFS is available.
// The idle loop (ServiceDeferredInit) picks it up after boot, keeping
// the multi-second firmware download off the boot-critical path.
if (!Fs::Vfs::IsDriveRegistered(0)) {
g_initPending = true;
g_initPending.store(true, std::memory_order_release);
KernelLogStream(INFO, "BT") << "Transport up; deferring init until ramdisk is mounted";
return;
}
@@ -362,12 +370,33 @@ namespace Drivers::USB::Bluetooth {
// =========================================================================
void ServiceDeferredInit() {
if (!g_initPending || g_initialized) return;
if (!g_initPending.load(std::memory_order_relaxed) || g_initialized) return;
if (!Fs::Vfs::IsDriveRegistered(0)) return; // ramdisk still not mounted
if (Xhci::InPollContext()) return; // never nest under PollEvents
g_initPending = false;
KernelLogStream(INFO, "BT") << "Ramdisk mounted; completing Bluetooth init";
// Claim the pending init (this runs from the idle loop; make sure only
// one pass performs the bring-up). The firmware download inside takes
// seconds -- running it here instead of on the boot path is what keeps
// boot fast.
bool expected = true;
if (!g_initPending.compare_exchange_strong(expected, false,
std::memory_order_acquire)) return;
KernelLogStream(INFO, "BT") << "Completing deferred Bluetooth init in background";
// Reserve this CPU for the duration. The bring-up overlaps desktop
// startup, and if the scheduler tick pulls this idle context away
// whenever a process is ready, the HCI waits' wall-clock timeouts
// expire with almost no polling done. Reserved, the bring-up runs
// uninterrupted here while processes use other CPUs; on a single-CPU
// system this briefly pauses userspace, matching the old synchronous
// behavior minus the boot-path stall.
auto* cpu = Smp::GetCurrentCpuData();
if (cpu) cpu->reservedForKernelWork = true;
Hci::SetFwTrace(true); // bounded per-completion event-pipe trace
CompleteInit();
Hci::SetFwTrace(false);
if (cpu) cpu->reservedForKernelWork = false;
}
// =========================================================================
+74 -18
View File
@@ -34,6 +34,19 @@ namespace Drivers::USB::Bluetooth::Hci {
static volatile uint32_t g_eventLen = 0;
static volatile bool g_eventReady = false;
// Firmware-phase diagnostics. g_fwTrace turns on a bounded per-completion
// trace of the interrupt IN pipe (SetFwTrace, driven by the bring-up);
// g_intInCompletions counts interrupt-IN completions since power-up so
// wait-loop timeouts can report whether the pipe was delivering at all.
static std::atomic<bool> g_fwTrace{false};
static std::atomic<uint32_t> g_fwTraceCount{0};
static std::atomic<uint32_t> g_intInCompletions{0};
void SetFwTrace(bool on) {
g_fwTrace.store(on, std::memory_order_relaxed);
if (on) g_fwTraceCount.store(0, std::memory_order_relaxed);
}
// ACL receive ring buffer. The bulk-IN callback (nested under PollEvents)
// only copies an incoming packet into a slot; DrainEvents() processes them
// at top level. A ring (not a single buffer) is required because the headset
@@ -276,26 +289,53 @@ namespace Drivers::USB::Bluetooth::Hci {
uint8_t intDci = dev->InterruptEpNum ? (dev->InterruptEpNum * 2 + 1) : 0;
uint8_t bulkInDci = dev->BulkInEpNum ? (dev->BulkInEpNum * 2 + 1) : 0;
if (epDci == intDci && data && length > 0) {
// HCI Event received on interrupt IN.
// Dispatch asynchronous events (inquiry results, connection events,
// etc.) immediately so they are never lost. Only buffer
// Command Complete / Command Status events — those are consumed
// by WaitCommandComplete / WaitCommandStatus.
uint8_t evtCode = (length >= 1) ? data[0] : 0;
if (epDci == intDci) {
g_intInCompletions.fetch_add(1, std::memory_order_relaxed);
if (evtCode == EVT_COMMAND_COMPLETE || evtCode == EVT_COMMAND_STATUS) {
uint32_t copyLen = length;
if (copyLen > sizeof(g_eventBuf)) copyLen = sizeof(g_eventBuf);
memcpy(g_eventBuf, data, copyLen);
g_eventLen = copyLen;
g_eventReady = true;
} else {
// Process immediately (inquiry results, connection events, etc.)
ProcessEvent(data, length);
// Bounded bring-up trace: one line per interrupt-IN completion
// while the firmware phase runs, so a hardware boot log shows
// exactly what the event pipe delivered (or didn't).
if (g_fwTrace.load(std::memory_order_relaxed)) {
uint32_t n = g_fwTraceCount.fetch_add(1, std::memory_order_relaxed);
if (n < 48) {
KernelLogStream(INFO, "BT-TRACE") << "int-in cc=" << (uint64_t)completionCode
<< " len=" << length
<< " b0=" << base::hex << (uint64_t)(data && length ? data[0] : 0xEE)
<< " b3b4=" << (uint64_t)(data && length >= 5
? ((uint32_t)data[3] | ((uint32_t)data[4] << 8)) : 0xEEEE)
// ncmd = HCI command-flow-control window in a Command
// Complete; 0 here would explain a controller ignoring
// all subsequent commands.
<< " ncmd=" << (uint64_t)(data && length >= 3 ? data[2] : 0xEE)
<< base::dec << (g_eventReady ? " [mailbox-full]" : "");
}
}
// Re-queue interrupt transfer for next event
if (data && length > 0) {
// HCI Event received on interrupt IN.
// Dispatch asynchronous events (inquiry results, connection
// events, etc.) immediately so they are never lost. Only
// buffer Command Complete / Command Status events — those are
// consumed by WaitCommandComplete / WaitCommandStatus.
uint8_t evtCode = data[0];
if (evtCode == EVT_COMMAND_COMPLETE || evtCode == EVT_COMMAND_STATUS) {
uint32_t copyLen = length;
if (copyLen > sizeof(g_eventBuf)) copyLen = sizeof(g_eventBuf);
memcpy(g_eventBuf, data, copyLen);
g_eventLen = copyLen;
g_eventReady = true;
} else {
// Process immediately (inquiry results, connection events, etc.)
ProcessEvent(data, length);
}
}
// ALWAYS re-queue, including error (data == nullptr) and 0-length
// (ZLP) completions. Re-arming only on data>0 meant a single such
// completion silently killed the event pipe for good -- every
// later command then "timed out" with no xHCI error in sight.
// Mirrors the bulk-IN rule below ("must keep cycling").
Xhci::QueueInterruptTransfer(slotId);
} else if (epDci == bulkInDci) {
// ACL data received on bulk IN -> copy into the ring; processed by
@@ -1035,6 +1075,17 @@ namespace Drivers::USB::Bluetooth::Hci {
if (!outBuf || maxLen <= 0) return -1;
uint8_t param = 0xFF;
uint32_t completionsBefore = g_intInCompletions.load(std::memory_order_relaxed);
// Up to 3 attempts, 150ms apart: a bootloader that dropped one FC05
// (busy, or still flushing a previous multi-packet response) gets a
// fresh chance instead of failing the whole bring-up.
for (int attempt = 0; attempt < 3; attempt++) {
if (attempt > 0) {
KernelLogStream(WARNING, "BT-HCI") << "Retrying Intel TLV version read (attempt "
<< (uint64_t)(attempt + 1) << ")";
PollWait(150);
}
if (!SendCommand(OP_INTEL_READ_VERSION, &param, 1)) return -1;
// Wait for the matching Command Complete and copy the *actually
@@ -1064,7 +1115,12 @@ namespace Drivers::USB::Bluetooth::Hci {
for (int j = 0; j < 100; j++) asm volatile("" ::: "memory");
}
KernelLogStream(WARNING, "BT-HCI") << "ReadIntelVersionTlv timeout";
// Distinguish "pipe dead" (0 completions) from "responses arriving but
// never matching" (mailbox scrambling) -- the two need different fixes.
KernelLogStream(WARNING, "BT-HCI") << "ReadIntelVersionTlv timeout ("
<< (uint64_t)(g_intInCompletions.load(std::memory_order_relaxed) - completionsBefore)
<< " int-in completions during wait)";
}
return -1;
}
+6
View File
@@ -265,6 +265,12 @@ namespace Drivers::USB::Bluetooth::Hci {
// Returns true if a matching bond was found and forgotten.
bool ForgetBond(const uint8_t* addr);
// Toggle the bounded firmware-phase interrupt-IN trace (one log line per
// completion, capped) plus mailbox-overwrite flags. Enabled by the
// deferred bring-up so a failing hardware boot log shows exactly what the
// event pipe delivered.
void SetFwTrace(bool on);
// Non-blocking peek at the most recent 0xFF/0x06 secure-send result without
// consuming it. Returns true if one has arrived since the last
// ClearSecureSendResult(). The payload loop uses this to catch a mid-stream
+18 -4
View File
@@ -428,7 +428,13 @@ namespace Drivers::USB::Xhci {
// sent from a Bluetooth event handler). Such callers must fire-and-forget,
// not wait, since a nested PollEvents is a no-op.
bool InPollContext() {
return g_pollActive.load(std::memory_order_relaxed);
// Same-core only: "nested under PollEvents" means an event callback on
// THIS core issued the query. A different core merely polling must
// not make callers (WaitCommandComplete & co.) skip their wait -- that
// reported success before the reply arrived (same cross-core bug
// ControlTransfer's trueNested check already guards against).
return g_pollActive.load(std::memory_order_relaxed) &&
g_pollOwnerCpu.load(std::memory_order_relaxed) == CurrentCpuIndex();
}
// -------------------------------------------------------------------------
@@ -568,9 +574,17 @@ namespace Drivers::USB::Xhci {
g_transferCallbacks[slotId](slotId, epDci,
nullptr, 0, completionCode);
} else if (epDci == intDci) {
// Interrupt IN — HID or callback dispatch
uint16_t len = dev.InterruptMaxPacket;
if (residual < len) len = dev.InterruptMaxPacket - (uint16_t)residual;
// Interrupt IN — HID or callback dispatch.
// len = actually-transferred bytes. A ZLP has
// residual == requested, so len MUST be 0 (same
// fix as bulk IN above): the old code left len at
// the full max-packet and handed the callback a
// slice of STALE DMA buffer -- for Bluetooth that
// can replay the PREVIOUS HCI event into the
// command-complete mailbox.
uint16_t reqLen = dev.InterruptMaxPacket;
uint16_t len = (residual < reqLen)
? (uint16_t)(reqLen - residual) : 0;
if (dev.InterfaceClass == UsbDevice::CLASS_HID) {
if (dev.InterfaceProtocol == UsbDevice::PROTOCOL_KEYBOARD) {
+8
View File
@@ -40,6 +40,14 @@ namespace Smp {
Hal::TSS64* tss; // pointer to this CPU's TSS
bool hasMwait; // CPU supports MONITOR/MWAIT
// When set, the scheduler tick must NOT pull this (idle) CPU away to
// run a ready process: its idle context is busy with long-running
// kernel work whose wall-clock-bounded waits would spuriously expire
// if the context only ran when nothing else is ready (observed: the
// deferred Bluetooth firmware download racing login-screen startup).
// Other CPUs schedule normally while this is held.
volatile bool reservedForKernelWork = false;
// Per-CPU GDT and TSS (APs use these; BSP uses globals)
Hal::BasicGDT cpuGdt __attribute__((aligned(16)));
Hal::TSS64 cpuTss __attribute__((aligned(16)));
+7
View File
@@ -189,6 +189,13 @@ extern "C" void kmain() {
// A Bluetooth adapter present at boot enumerates during the xHCI port scan,
// before the ramdisk is mounted. Now that drive 0 is up, finish any
// firmware-dependent bring-up that was deferred (loads ibt-*.sfi/.ddc).
//
// Deliberately synchronous, HERE, pre-Sched/pre-SMP/pre-MSI: deferring
// this to the post-boot idle loop made the AX211 bootloader stop
// answering after the first FC05 (2026-07-05, three attempts: CPU
// reservation, restored pipe timing, event-pipe fixes -- identical
// failure each time; suspected xHCI-MSI-context event processing, see
// memory notes). Boot pays the download cost until that is understood.
Drivers::USB::Bluetooth::ServiceDeferredInit();
Hal::LoadTSS();
+6 -1
View File
@@ -945,7 +945,12 @@ namespace Sched {
// scanning 256 process slots on every tick. On a 32-core
// system with 27 idle CPUs, this avoids ~7M cache-line
// reads/sec from the process table.
if (readyCount > 0) {
//
// A CPU whose idle context is reserved for long-running kernel
// work (e.g. the deferred Bluetooth firmware download) is left
// alone: stealing it starves that work of CPU time while its
// wall-clock timeouts keep running.
if (readyCount > 0 && !cpu->reservedForKernelWork) {
Schedule();
}
return;
+7
View File
@@ -212,6 +212,13 @@ namespace Timekeeping {
Drivers::USB::Xhci::ProcessDeferredWork();
}
// Complete any boot-deferred Bluetooth bring-up (Intel firmware
// download) here instead of on the boot path: the download takes
// seconds and used to stall kmain before the first process spawned.
// Cheap no-op unless an adapter is waiting; self-claiming, and safe
// to preempt (the scheduler saves/resumes the idle context).
Drivers::USB::Bluetooth::ServiceDeferredInit();
// Service Bluetooth inbound traffic (the headset's SDP/AVRCP queries,
// AVDTP commands, ACL flow-control credits) whenever a core idles.
// Without this, BT events are only processed while some syscall happens