feat: Raspberry Pi 4/400 Bluetooth over mini UART (H4, Broadcom patchram, firmware mailbox BT_ON); move BT stack to Drivers/Bluetooth

This commit is contained in:
daniel committed 2026-10-25 15:59:33 +01:00
1 parent 808e1d66fd
commit ffee9002a6
38 files changed
+2176 -141

No files matched your search

+6 -6
View File
@@ -11,8 +11,8 @@
#include <Drivers/Audio/IntelHda.hpp>
#include <Drivers/Audio/Mixer.hpp>
#include <Terminal/Terminal.hpp>
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
#include <Drivers/USB/Bluetooth/A2dp.hpp>
#include <Drivers/Bluetooth/Bluetooth.hpp>
#include <Drivers/Bluetooth/A2dp.hpp>
#include <Sched/Scheduler.hpp>
#include "Syscall.hpp"
@@ -55,10 +55,10 @@ namespace montauk::abi {
return Drivers::Audio::Mixer::SetOutput(
(Drivers::Audio::Mixer::Output)value);
if (cmd == AUDIO_CTL_BT_STATUS) {
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
if (Drivers::USB::Bluetooth::A2dp::IsReady()) return 2;
return Drivers::USB::Bluetooth::A2dp::GetState()
== Drivers::USB::Bluetooth::A2dp::State::Idle
if (!Drivers::Bluetooth::IsInitialized()) return 0;
if (Drivers::Bluetooth::A2dp::IsReady()) return 2;
return Drivers::Bluetooth::A2dp::GetState()
== Drivers::Bluetooth::A2dp::State::Idle
? 0 : 1;
}
+22 -22
View File
@@ -6,8 +6,8 @@
#pragma once
#include <cstdint>
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
#include <Drivers/USB/Bluetooth/Hci.hpp>
#include <Drivers/Bluetooth/Bluetooth.hpp>
#include <Drivers/Bluetooth/Hci.hpp>
#include <Libraries/Memory.hpp>
#include <Sched/Scheduler.hpp>
#include <Terminal/Terminal.hpp>
@@ -19,14 +19,14 @@ namespace montauk::abi {
static int64_t Sys_BtScan(BtScanResult* buf, int maxCount, uint32_t timeoutMs) {
if (!buf || maxCount <= 0) return InvalidArgument;
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
// Use a stack buffer for kernel-side results (max 16)
Drivers::USB::Bluetooth::Hci::InquiryDevice tmpBuf[16];
Drivers::Bluetooth::Hci::InquiryDevice tmpBuf[16];
int kMax = maxCount;
if (kMax > 16) kMax = 16;
int found = Drivers::USB::Bluetooth::Scan(tmpBuf, kMax, timeoutMs);
int found = Drivers::Bluetooth::Scan(tmpBuf, kMax, timeoutMs);
if (found < 0) return found;
// Convert kernel InquiryDevice to userspace BtScanResult
@@ -47,38 +47,38 @@ namespace montauk::abi {
static int64_t Sys_BtConnect(const uint8_t* bdAddr) {
if (!bdAddr) return InvalidArgument;
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
return (int64_t)Drivers::USB::Bluetooth::Connect(bdAddr);
return (int64_t)Drivers::Bluetooth::Connect(bdAddr);
}
static int64_t Sys_BtDisconnect(const uint8_t* bdAddr) {
if (!bdAddr) return InvalidArgument;
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
Kt::KernelLogStream(Kt::INFO, "Power") << "pid " << kcp::dec
<< (uint64_t)Sched::GetCurrentPid()
<< " entered Bluetooth disconnect stage";
return (int64_t)Drivers::USB::Bluetooth::Disconnect(bdAddr);
return (int64_t)Drivers::Bluetooth::Disconnect(bdAddr);
}
static int64_t Sys_BtSetAddr(const uint8_t* bdAddr) {
if (!bdAddr) return InvalidArgument;
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
// bdAddr is a 6-byte buffer, addr[0] = least-significant octet.
return Drivers::USB::Bluetooth::SetAddress(bdAddr) ? Success : GenericFailure;
return Drivers::Bluetooth::SetAddress(bdAddr) ? Success : GenericFailure;
}
static int64_t Sys_BtBonds(BtBondInfo* buf, int maxCount) {
if (!buf || maxCount <= 0) return 0;
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
if (!Drivers::Bluetooth::IsInitialized()) return 0;
Drivers::USB::Bluetooth::Hci::BondInfo tmpBuf[8];
Drivers::Bluetooth::Hci::BondInfo tmpBuf[8];
int kMax = maxCount;
if (kMax > 8) kMax = 8;
int count = Drivers::USB::Bluetooth::ListBonded(tmpBuf, kMax);
int count = Drivers::Bluetooth::ListBonded(tmpBuf, kMax);
for (int i = 0; i < count; i++) {
memcpy(buf[i].bdAddr, tmpBuf[i].Addr, 6);
@@ -91,20 +91,20 @@ namespace montauk::abi {
static int64_t Sys_BtForget(const uint8_t* bdAddr) {
if (!bdAddr) return InvalidArgument;
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
return (int64_t)Drivers::USB::Bluetooth::ForgetDevice(bdAddr);
return (int64_t)Drivers::Bluetooth::ForgetDevice(bdAddr);
}
static int64_t Sys_BtList(BtDevInfo* buf, int maxCount) {
if (!buf || maxCount <= 0) return 0;
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
if (!Drivers::Bluetooth::IsInitialized()) return 0;
Drivers::USB::Bluetooth::Hci::ConnectionInfo tmpBuf[4];
Drivers::Bluetooth::Hci::ConnectionInfo tmpBuf[4];
int kMax = maxCount;
if (kMax > 4) kMax = 4;
int count = Drivers::USB::Bluetooth::ListConnected(tmpBuf, kMax);
int count = Drivers::Bluetooth::ListConnected(tmpBuf, kMax);
for (int i = 0; i < count; i++) {
memcpy(buf[i].bdAddr, tmpBuf[i].BdAddr, 6);
@@ -123,14 +123,14 @@ namespace montauk::abi {
memset(buf, 0, sizeof(BtAdapterInfo));
if (!Drivers::USB::Bluetooth::IsInitialized()) {
if (!Drivers::Bluetooth::IsInitialized()) {
buf->initialized = 0;
return NoDevice;
}
buf->initialized = 1;
memcpy(buf->bdAddr, Drivers::USB::Bluetooth::GetBdAddr(), 6);
buf->scanning = Drivers::USB::Bluetooth::Hci::IsInquiryActive() ? 1 : 0;
memcpy(buf->bdAddr, Drivers::Bluetooth::GetBdAddr(), 6);
buf->scanning = Drivers::Bluetooth::Hci::IsInquiryActive() ? 1 : 0;
// Copy adapter name
const char* name = "MontaukOS";
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 73
#define MONTAUK_BUILD_NUMBER 80
+8 -2
View File
@@ -8,7 +8,7 @@
#include <cstdint>
#include <Drivers/USB/Xhci.hpp>
#include <Drivers/USB/UsbDevice.hpp>
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
#include <Drivers/Bluetooth/Bluetooth.hpp>
#include <Drivers/Net/E1000.hpp>
#include <Drivers/Net/E1000E.hpp>
#include <Drivers/Net/Wifi/Wifi.hpp>
@@ -127,6 +127,12 @@ namespace montauk::abi {
? "802.11 wireless (connected)"
: "802.11 wireless");
}
// An on-board UART Bluetooth controller (Raspberry Pi) is no USB
// device, so it is listed here; USB adapters appear above.
if (Drivers::Bluetooth::IsInitialized() &&
Drivers::Bluetooth::Hci::GetTransport() == Drivers::Bluetooth::Hci::Transport::Uart) {
add(5, "Bluetooth Adapter", "Broadcom, HCI over UART");
}
// Display (category 6)
if (Drivers::Graphics::IntelGPU::IsInitialized()) {
@@ -148,7 +154,7 @@ namespace montauk::abi {
add(9, "Intel HDA", detail);
}
if (Drivers::USB::Bluetooth::IsInitialized()) {
if (Drivers::Bluetooth::IsInitialized()) {
add(9, "Bluetooth Audio", "A2DP (SBC)");
}
+20 -20
View File
@@ -7,9 +7,9 @@
#include "Mixer.hpp"
#include "IntelHda.hpp"
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
#include <Drivers/USB/Bluetooth/A2dp.hpp>
#include <Drivers/USB/Bluetooth/Avrcp.hpp>
#include <Drivers/Bluetooth/Bluetooth.hpp>
#include <Drivers/Bluetooth/A2dp.hpp>
#include <Drivers/Bluetooth/Avrcp.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Memory/HHDM.hpp>
#include <Sched/Scheduler.hpp>
@@ -146,8 +146,8 @@ namespace Drivers::Audio::Mixer {
}
static bool BluetoothReady() {
if (!Drivers::USB::Bluetooth::IsInitialized()) return false;
return Drivers::USB::Bluetooth::A2dp::IsReady();
if (!Drivers::Bluetooth::IsInitialized()) return false;
return Drivers::Bluetooth::A2dp::IsReady();
}
static void SyncHdaMasterMute() {
@@ -236,7 +236,7 @@ namespace Drivers::Audio::Mixer {
static uint32_t BackendWriteSpace() {
if (g_output == Output::Bluetooth) {
return g_btOpened
? Drivers::USB::Bluetooth::A2dp::GetWriteSpace() : 0;
? Drivers::Bluetooth::A2dp::GetWriteSpace() : 0;
}
return g_hdaOpened ? IntelHda::GetWriteSpace(g_hdaHandle) : 0;
}
@@ -244,7 +244,7 @@ namespace Drivers::Audio::Mixer {
static int BackendWrite(const uint8_t* data, uint32_t size) {
if (g_output == Output::Bluetooth) {
return g_btOpened
? Drivers::USB::Bluetooth::A2dp::WriteAudio(data, size) : -1;
? Drivers::Bluetooth::A2dp::WriteAudio(data, size) : -1;
}
return g_hdaOpened ? IntelHda::Write(g_hdaHandle, data, size) : -1;
}
@@ -403,14 +403,14 @@ namespace Drivers::Audio::Mixer {
if (g_output == Output::Bluetooth && !g_btOpened && BluetoothReady()) {
g_switchingOutput = true;
g_lock.Release();
bool btOk = Drivers::USB::Bluetooth::A2dp::ConfigureStream(
bool btOk = Drivers::Bluetooth::A2dp::ConfigureStream(
MIX_RATE, MIX_CHANNELS, MIX_BITS) &&
Drivers::USB::Bluetooth::A2dp::StartStream();
Drivers::Bluetooth::A2dp::StartStream();
g_lock.Acquire();
g_btOpened = btOk;
if (!btOk) g_output = Output::Hda;
if (btOk)
Drivers::USB::Bluetooth::A2dp::SetMuted(
Drivers::Bluetooth::A2dp::SetMuted(
g_masterMute.load(std::memory_order_acquire));
g_switchingOutput = false;
wakeSwitchWaiters = true;
@@ -515,7 +515,7 @@ namespace Drivers::Audio::Mixer {
if (closeBt) g_btOpened = false;
g_lock.Release();
if (closeHda) IntelHda::Close(hdaHandle);
if (closeBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
if (closeBt) Drivers::Bluetooth::A2dp::StopStream(true);
if (ringToFree) FreeRing(ringToFree);
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
}
@@ -535,7 +535,7 @@ namespace Drivers::Audio::Mixer {
bool serviceBluetooth = g_output == Output::Bluetooth;
g_lock.Release();
if (serviceBluetooth)
Drivers::USB::Bluetooth::A2dp::ServiceMedia();
Drivers::Bluetooth::A2dp::ServiceMedia();
return (int)written;
}
@@ -651,7 +651,7 @@ namespace Drivers::Audio::Mixer {
if (changed) BumpSerialLocked();
g_lock.Release();
if (closeHda) IntelHda::Close(hdaHandle);
if (closeBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
if (closeBt) Drivers::Bluetooth::A2dp::StopStream(true);
for (int i = 0; i < ringCount; i++) FreeRing(ringsToFree[i]);
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
}
@@ -668,7 +668,7 @@ namespace Drivers::Audio::Mixer {
g_lock.Release();
if (changed)
Drivers::USB::Bluetooth::Avrcp::NotifyVolumeChanged(percent);
Drivers::Bluetooth::Avrcp::NotifyVolumeChanged(percent);
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
}
@@ -688,7 +688,7 @@ namespace Drivers::Audio::Mixer {
if (changed) SyncHdaMasterMute();
if (changed)
Drivers::USB::Bluetooth::A2dp::SetMuted(muted);
Drivers::Bluetooth::A2dp::SetMuted(muted);
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
}
@@ -733,11 +733,11 @@ namespace Drivers::Audio::Mixer {
}
} else if (needBackend && output == Output::Bluetooth) {
prepared = BluetoothReady() &&
Drivers::USB::Bluetooth::A2dp::ConfigureStream(
Drivers::Bluetooth::A2dp::ConfigureStream(
MIX_RATE, MIX_CHANNELS, MIX_BITS) &&
Drivers::USB::Bluetooth::A2dp::StartStream();
Drivers::Bluetooth::A2dp::StartStream();
if (prepared)
Drivers::USB::Bluetooth::A2dp::SetMuted(
Drivers::Bluetooth::A2dp::SetMuted(
g_masterMute.load(std::memory_order_acquire));
} else if (output == Output::Hda) {
prepared = IntelHda::IsInitialized();
@@ -779,7 +779,7 @@ namespace Drivers::Audio::Mixer {
Sched::WakeObjectWaiters((void*)&g_switchingOutput);
if (closeHda) IntelHda::Close(oldHdaHandle);
if (closeBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
if (closeBt) Drivers::Bluetooth::A2dp::StopStream(true);
Sched::WakeObjectWaiters((void*)&g_serial);
return 0;
}
@@ -815,7 +815,7 @@ namespace Drivers::Audio::Mixer {
wake = true;
}
g_lock.Release();
if (stopBt) Drivers::USB::Bluetooth::A2dp::StopStream(true);
if (stopBt) Drivers::Bluetooth::A2dp::StopStream(true);
if (wake) Sched::WakeObjectWaiters((void*)&g_serial);
}
@@ -9,7 +9,6 @@
#include "Sbc.hpp"
#include "L2cap.hpp"
#include "Hci.hpp"
#include <Drivers/USB/Xhci.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Libraries/Memory.hpp>
@@ -18,7 +17,7 @@
using namespace Kt;
namespace Drivers::USB::Bluetooth::A2dp {
namespace Drivers::Bluetooth::A2dp {
// =========================================================================
// AVDTP constants
@@ -222,7 +221,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
Xhci::PollEvents();
Hci::Pump();
Hci::DrainEvents(); // AVDTP responses arrive as ACL data
if (g_avdtpResponseReady.load(std::memory_order_acquire)) {
// Freeze this mailbox before returning to the caller. A late
@@ -557,7 +556,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
void OnChannelReady(uint16_t l2capCid) {
// Invoked from L2CAP inside the ACL receive path -- i.e. NESTED under
// Xhci::PollEvents. PollEvents is non-reentrant (re-entry guard), so we
// Hci::DrainEvents. That is non-reentrant (re-entry guard), so we
// must NOT run the blocking AVDTP signaling chain here; a nested poll
// would stall and every WaitAvdtpResponse() would time out. Just record
// the channel; StartSource() drives the handshakes from top-level
@@ -1161,7 +1160,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
bool answered = false;
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
Xhci::PollEvents();
Hci::Pump();
Hci::DrainEvents();
if (g_sdpRspReady.load(std::memory_order_acquire)) {
answered = true;
@@ -1196,7 +1195,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
static bool WaitEncrypted(uint32_t timeoutMs) {
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
Xhci::PollEvents();
Hci::Pump();
Hci::DrainEvents();
Hci::ProcessPendingCommands();
auto* c = Hci::GetConnection(g_aclHandle.load(std::memory_order_acquire));
@@ -1235,7 +1234,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
uint64_t waitStart = Timekeeping::GetMilliseconds();
while (g_sourceSetupActive.load(std::memory_order_acquire)
&& Timekeeping::GetMilliseconds() - waitStart < timeoutMs) {
Xhci::PollEvents();
Hci::Pump();
Hci::DrainEvents();
for (int j = 0; j < 100; j++) Arch::Pause();
}
@@ -1265,7 +1264,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
if (g_sigCid.load(std::memory_order_acquire) == 0) {
uint64_t lStart = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - lStart < 1000) {
Xhci::PollEvents();
Hci::Pump();
Hci::DrainEvents();
Hci::ProcessPendingCommands();
if (g_sigCid.load(std::memory_order_acquire) != 0) break;
@@ -1319,7 +1318,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
uint32_t attemptWaitMs = timeoutMs < 1500 ? timeoutMs : 1500;
uint32_t waitBudgetMs = attemptWaitMs;
while (Timekeeping::GetMilliseconds() - sigStart < waitBudgetMs) {
Xhci::PollEvents();
Hci::Pump();
Hci::DrainEvents();
if (g_sigCid.load(std::memory_order_acquire) != 0) break;
// PENDING is explicitly non-final. A peer performing service
@@ -1363,7 +1362,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
if (sig) L2cap::FreeChannel(sig);
uint64_t st = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - st < 400) {
Xhci::PollEvents();
Hci::Pump();
Hci::DrainEvents();
for (int j = 0; j < 200; j++) Arch::Pause();
}
@@ -1384,7 +1383,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
if (g_peerConfigured.load(std::memory_order_acquire)) {
uint64_t peerStart = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - peerStart < timeoutMs) {
Xhci::PollEvents();
Hci::Pump();
Hci::DrainEvents();
State s = g_state.load(std::memory_order_acquire);
if (s == State::Open || s == State::Streaming) break;
@@ -1483,7 +1482,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
uint64_t mStart = Timekeeping::GetMilliseconds();
while (g_mediaCid.load(std::memory_order_acquire) == 0
&& Timekeeping::GetMilliseconds() - mStart < kMediaWaitMs) {
Xhci::PollEvents();
Hci::Pump();
Hci::DrainEvents();
auto* ch = media ? L2cap::GetChannel(media) : nullptr;
if (ch && ch->Configured) {
@@ -2136,7 +2135,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
void ServiceMedia() {
if (!AcquireMediaService()) return;
Xhci::PollEvents();
Hci::Pump();
Hci::DrainEvents();
PumpMedia();
g_serviceActive.store(false, std::memory_order_release);
@@ -8,7 +8,7 @@
#pragma once
#include <cstdint>
namespace Drivers::USB::Bluetooth::A2dp {
namespace Drivers::Bluetooth::A2dp {
// =========================================================================
// A2DP stream states
@@ -15,7 +15,7 @@
using namespace Kt;
namespace Drivers::USB::Bluetooth::Avrcp {
namespace Drivers::Bluetooth::Avrcp {
// =========================================================================
// Constants
@@ -7,7 +7,7 @@
#pragma once
#include <cstdint>
namespace Drivers::USB::Bluetooth::Avrcp {
namespace Drivers::Bluetooth::Avrcp {
// Process an AVCTP packet arriving on an AVRCP control channel (PSM 0x0017).
// Implements a minimal AVRCP Target: unit/subunit info, honest rejection of
@@ -1,6 +1,6 @@
/*
* Bluetooth.cpp
* Top-level Bluetooth subsystem — adapter registration and Intel BT initialization
* Top-level Bluetooth subsystem — adapter registration and controller bring-up
* Copyright (c) 2026 Daniel Hammer
*/
@@ -9,6 +9,7 @@
#include "Hci.hpp"
#include "A2dp.hpp"
#include "IntelFirmware.hpp"
#include "Broadcom.hpp"
#include <Drivers/Audio/Mixer.hpp>
#include <Drivers/USB/Xhci.hpp>
#include <Drivers/USB/UsbDevice.hpp>
@@ -22,7 +23,9 @@
using namespace Kt;
namespace Drivers::USB::Bluetooth {
namespace Drivers::Bluetooth {
namespace Xhci = Drivers::USB::Xhci;
// =========================================================================
// State
@@ -205,7 +208,9 @@ namespace Drivers::USB::Bluetooth {
// =========================================================================
void RegisterAdapter(uint8_t slotId) {
if (g_initialized) {
// One adapter at a time, whichever came first (an on-board UART
// controller may still be finishing its deferred init).
if (g_initialized || Hci::GetTransport() != Hci::Transport::None) {
KernelLogStream(WARNING, "BT") << "Bluetooth adapter already registered";
return;
}
@@ -249,17 +254,47 @@ namespace Drivers::USB::Bluetooth {
CompleteInit();
}
// =========================================================================
// ProbePlatformAdapter — an on-board UART controller ACPI describes
// =========================================================================
bool ProbePlatformAdapter() {
if (g_initialized || Hci::GetTransport() != Hci::Transport::None) return false;
if (!Broadcom::ProbePlatform()) return false;
// The firmware patch lives on the root filesystem, which the boot
// driver probe precedes; finish from the idle loop like USB adapters.
if (!Fs::Vfs::IsDriveRegistered(0)) {
g_initPending.store(true, std::memory_order_release);
KernelLogStream(INFO, "BT") << "Transport up; deferring init until the root is mounted";
return true;
}
CompleteInit();
return true;
}
// =========================================================================
// CompleteInit — firmware-dependent HCI bring-up (needs VFS/ramdisk)
// =========================================================================
static void CompleteInit() {
auto* dev = Xhci::GetDevice(g_slotId);
if (!dev) return;
bool didReset = false;
if (Hci::GetTransport() == Hci::Transport::Uart) {
// Power cycle, firmware patch and operating baud; ends with a reset.
if (!Broadcom::Initialize()) {
KernelLogStream(ERROR, "BT") << "Broadcom BT initialization failed";
return;
}
didReset = true;
}
auto* dev = Hci::GetTransport() == Hci::Transport::Usb ? Xhci::GetDevice(g_slotId) : nullptr;
if (Hci::GetTransport() == Hci::Transport::Usb && !dev) return;
// Intel-specific initialization (firmware download + HCI Reset)
bool didReset = false;
if (IsIntelBt(dev->VendorId, dev->ProductId)) {
if (dev && IsIntelBt(dev->VendorId, dev->ProductId)) {
Hci::SetVendor(Hci::Vendor::Intel);
if (InitIntelBluetooth()) {
didReset = true; // InitIntelBluetooth already sent HCI Reset
} else {
@@ -280,9 +315,11 @@ namespace Drivers::USB::Bluetooth {
}
}
// Apply a persisted BD_ADDR override (0:/config/bluetooth.toml) now,
// after the last reset and before the address is read back, so the
// whole bring-up below uses the overridden address.
// A Broadcom part still on its chip-wide default address gets the
// board's, then a persisted BD_ADDR override (0:/config/bluetooth.toml)
// wins over both. Both after the last reset and before the address
// is read back, so the whole bring-up below uses the final address.
if (Hci::GetVendor() == Hci::Vendor::Broadcom) Broadcom::FixDefaultAddress();
ApplyConfiguredAddress();
// Read BD_ADDR
@@ -387,7 +424,7 @@ namespace Drivers::USB::Bluetooth {
void ServiceDeferredInit() {
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
if (Hci::InPumpContext()) return; // never nest under the transport pump
// Claim the pending init (this runs from the idle loop; make sure only
// one pass performs the bring-up). The firmware download inside takes
@@ -425,12 +462,12 @@ namespace Drivers::USB::Bluetooth {
!g_initialized && Fs::Vfs::IsDriveRegistered(0)) {
return true;
}
return g_initialized && Hci::HasPendingCommands();
return g_initialized && (Hci::HasPendingCommands() || Hci::TransportHasRxData());
}
void ServiceEvents() {
if (!g_initialized) return;
if (Xhci::InPollContext()) return; // never nest under PollEvents
if (Hci::InPumpContext()) return; // never nest under the transport pump
A2dp::ServiceMedia(); // reap events and feed queued media
// DrainEvents may just have queued an accept/auth/encryption reply.
// Send it in this same service pass instead of adding a scheduler-turn
@@ -492,7 +529,7 @@ namespace Drivers::USB::Bluetooth {
uint64_t t0 = Timekeeping::GetMilliseconds();
bool anyActive = true;
while (anyActive && Timekeeping::GetMilliseconds() - t0 < 2000) {
Xhci::PollEvents();
Hci::Pump();
Hci::DrainEvents();
anyActive = false;
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
@@ -548,7 +585,7 @@ namespace Drivers::USB::Bluetooth {
// Poll until inquiry completes or timeout
uint64_t start = Timekeeping::GetMilliseconds();
while (Hci::IsInquiryActive() && (Timekeeping::GetMilliseconds() - start < timeoutMs)) {
Xhci::PollEvents();
Hci::Pump();
Hci::DrainEvents();
for (int j = 0; j < 200; j++) {
@@ -566,7 +603,7 @@ namespace Drivers::USB::Bluetooth {
uint64_t cancelStart = Timekeeping::GetMilliseconds();
while (Hci::IsInquiryActive()
&& Timekeeping::GetMilliseconds() - cancelStart < 250) {
Xhci::PollEvents();
Hci::Pump();
Hci::DrainEvents();
for (int j = 0; j < 100; j++)
Arch::Pause();
@@ -612,7 +649,7 @@ namespace Drivers::USB::Bluetooth {
// Wait for Connection Complete event
uint64_t start = Timekeeping::GetMilliseconds();
while (!target && Timekeeping::GetMilliseconds() - start < timeoutMs) {
Xhci::PollEvents();
Hci::Pump();
Hci::DrainEvents();
target = FindAclConnection(bdAddr);
for (int j = 0; j < 200; j++) {
@@ -631,7 +668,7 @@ namespace Drivers::USB::Bluetooth {
if (reusedAcl) Hci::AuthenticateLink(target->Handle);
uint64_t t0 = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - t0 < 5000) {
Xhci::PollEvents();
Hci::Pump();
Hci::DrainEvents();
Hci::ProcessPendingCommands();
if (!target->Active || !SameAddress(target->BdAddr, bdAddr)) break;
@@ -8,28 +8,35 @@
#include <cstdint>
#include "Hci.hpp"
namespace Drivers::USB::Bluetooth {
namespace Drivers::Bluetooth {
// Called by USB enumeration when a Bluetooth adapter is detected
void RegisterAdapter(uint8_t slotId);
// Called by the boot driver probe: bring up an on-board UART controller
// ACPI describes (the Raspberry Pi 4/400's Broadcom radio). Returns true
// if one was found; its firmware-dependent init is deferred like USB's.
bool ProbePlatformAdapter();
// Complete any firmware-dependent bring-up that was deferred because the
// adapter enumerated during the boot port scan, before the ramdisk (which
// holds the firmware images) was mounted. Safe to call unconditionally
// once the boot filesystems are up; a no-op if nothing is pending.
void ServiceDeferredInit();
// Steady-state event service: drains the xHCI event ring + Bluetooth ACL
// RX ring and flushes queued HCI replies. Called from the idle loop so
// Steady-state event service: drains the transport (xHCI event ring or
// UART receive ring) + Bluetooth ACL RX ring and flushes queued HCI
// replies. Called from the idle loop so
// inbound traffic (the headset's SDP/AVRCP queries, AVDTP commands, ACL
// credits) is processed even when no syscall happens to be pumping a BT
// wait loop. Cheap no-op when the adapter is down; safe from any core
// (PollEvents/DrainEvents/ProcessPendingCommands all self-serialize).
// (Pump/DrainEvents/ProcessPendingCommands all self-serialize).
void ServiceEvents();
// True when boot-deferred initialization or queued HCI control work needs
// an idle-context service pass. USB receive events are signaled separately
// by xHCI and cause the dispatcher to service Bluetooth in the same pass.
// True when boot-deferred initialization, queued HCI control work or
// received UART bytes need an idle-context service pass. USB receive
// events are signaled separately by xHCI and cause the dispatcher to
// service Bluetooth in the same pass.
bool HasDeferredWork();
// Query adapter state
@@ -40,8 +47,8 @@ namespace Drivers::USB::Bluetooth {
const uint8_t* GetBdAddr();
// Change the adapter's BD_ADDR at runtime. Drops any active links (their
// pairing/crypto was bound to the old address), issues the Intel vendor
// write (0xFC31), re-reads the controller's address into the cache and
// pairing/crypto was bound to the old address), issues the vendor write
// (Intel 0xFC31, Broadcom 0xFC01), re-reads the controller's address into the cache and
// re-asserts scan. Returns true only if the controller reports the new
// address. Persisting the choice to 0:/config/bluetooth.toml is the
// caller's job (userspace); the override is re-applied on boot by
+493
View File
@@ -0,0 +1,493 @@
/*
* Broadcom.cpp
* Broadcom/Cypress UART Bluetooth controller bring-up (Raspberry Pi 4/400)
* Copyright (c) 2026 Daniel Hammer
*/
#include "Broadcom.hpp"
#include "Hci.hpp"
#include <ACPI/AcpiDevices.hpp>
#include <ACPI/AML/AmlInterpreter.hpp>
#include <Arch/Cpu.hpp>
#include <CppLib/Stream.hpp>
#include <Drivers/Platform/RpiFirmware.hpp>
#include <Drivers/Platform/RpiGpio.hpp>
#include <Drivers/Serial/BtUart.hpp>
#include <Drivers/Serial/Uart.hpp>
#include <Fs/Vfs.hpp>
#include <Libraries/Memory.hpp>
#include <Memory/Heap.hpp>
#include <Terminal/Terminal.hpp>
#include <Timekeeping/Timer.hpp>
// The Raspberry Pi 4B (CYW43455) and 400 (CYW43456) carry a Cypress radio
// whose Bluetooth half speaks HCI over a UART (H4) on GPIO 30-33: CTS, RTS,
// TXD, RXD. Its power switch, BT_ON, is line 0 of the VideoCore firmware's
// GPIO expander.
//
// Bring-up follows Linux's hci_bcm + btbcm:
// 1. power on, wait ~100 ms, HCI Reset at the controller's 115200 baud;
// 2. Read Local Version: the LMP subversion names the chip and so the
// patch file (BCM4345C0.hcd, BCM4345C5.hcd);
// 3. raise both ends to the operating speed (vendor 0xFC18, answered at
// the old speed);
// 4. Download Minidriver (0xFC2E), wait 50 ms, then send every record of
// the .hcd file -- each one is a complete HCI command, Write RAM
// (0xFC4C) and finally Launch RAM (0xFC4E) -- and wait 250 ms;
// 5. the patched firmware restarts at 115200: HCI Reset there, and raise
// the speed again.
//
// Raspberry Pi OS's btuart runs the controller at 460800 without flow
// control on the mini UART and at 3 Mbaud with it on the PL011. The mini
// UART's FIFO is 8 bytes deep, so here its hardware RTS is used whenever
// the controller is still reachable with it on, which allows 921600.
using namespace Kt;
namespace Drivers::Bluetooth::Broadcom {
namespace BtUart = Drivers::Serial::BtUart;
namespace RpiFirmware = Drivers::Platform::RpiFirmware;
namespace RpiGpio = Drivers::Platform::RpiGpio;
// GPIO 30-33 carry CTS, RTS, TXD, RXD: alternate function 3 routes them
// to the PL011, 5 to the mini UART. Pulls as Linux's bcm2711 pinctrl
// sets them (uart0_ctsrts_gpio30, uart0_gpio32).
static constexpr uint32_t PIN_CTS = 30;
static constexpr uint32_t PIN_RTS = 31;
static constexpr uint32_t PIN_TXD = 32;
static constexpr uint32_t PIN_RXD = 33;
static constexpr uint32_t InitialBaud = 115200;
// LMP subversions of the ROM firmware -> patch file (Linux btbcm's
// bcm_uart_subver_table).
struct ChipName {
uint16_t subversion;
const char* name;
};
static constexpr ChipName g_chips[] = {
{ 0x6119, "BCM4345C0" }, // CYW43455: Raspberry Pi 3B+, 4B
{ 0x6606, "BCM4345C5" }, // CYW43456: Raspberry Pi 400, CM4
};
static bool g_flowControl = false;
static bool g_powerSwitch = true;
// =========================================================================
// Platform probe
// =========================================================================
static bool EndsWith(const char* s, const char* suffix) {
int sl = 0, xl = 0;
while (s[sl]) sl++;
while (suffix[xl]) xl++;
if (xl > sl) return false;
for (int i = 0; i < xl; i++) {
if (s[sl - xl + i] != suffix[i]) return false;
}
return true;
}
// The controller's UartSerialBusV2 descriptor (ACPI 6.4, 6.4.3.8.2.3)
// names the UART it hangs off in its ResourceSource string.
static bool ReadUartSource(int32_t node, char* source, int maxLength) {
Hal::AML::Object crs;
if (!Hal::AML::GetInterpreter().EvaluateChild(node, "_CRS", nullptr, 0, crs) ||
crs.Type != Hal::AML::ObjectType::Buffer) {
return false;
}
const uint8_t* data = crs.Buffer.Data;
uint32_t length = crs.Buffer.Length;
uint32_t pos = 0;
while (pos < length) {
uint8_t tag = data[pos];
if (!(tag & 0x80)) {
if ((tag & 0x78) == 0x78) break; // end tag
pos += 1 + (tag & 0x07);
continue;
}
if (pos + 3 > length) break;
uint32_t descLength = (uint32_t)data[pos + 1] | ((uint32_t)data[pos + 2] << 8);
uint32_t end = pos + 3 + descLength;
if (end > length) break;
static constexpr uint8_t SerialBusTag = 0x8E, SerialBusUart = 3;
if (tag == SerialBusTag && descLength >= 9 && data[pos + 5] == SerialBusUart) {
uint32_t typeDataLength = (uint32_t)data[pos + 10] | ((uint32_t)data[pos + 11] << 8);
uint32_t s = pos + 12 + typeDataLength;
int n = 0;
while (s < end && data[s] != 0 && n < maxLength - 1) source[n++] = (char)data[s++];
source[n] = '\0';
return n > 0;
}
pos = end;
}
return false;
}
static bool ReadInteger(int32_t node, const char* name, uint64_t& value) {
Hal::AML::Object obj;
if (!Hal::AML::GetInterpreter().EvaluateChild(node, name, nullptr, 0, obj) ||
obj.Type != Hal::AML::ObjectType::Integer) {
return false;
}
value = obj.Integer;
return true;
}
bool ProbePlatform() {
Hal::AcpiDevices::DeviceInfo bt{};
if (!Hal::AcpiDevices::FindDeviceByHid("BCM2EA6", bt)) return false;
// Which UART: the descriptor's source, else the firmware's "UART in
// use" flag (0: the PL011 is the console, Bluetooth has the mini UART).
BtUart::Kind kind = BtUart::Kind::MiniUart;
char source[64] = {};
uint64_t uartInUse = 0;
if (ReadUartSource(bt.NodeIndex, source, sizeof(source))) {
kind = EndsWith(source, "URT0") ? BtUart::Kind::Pl011 : BtUart::Kind::MiniUart;
} else if (ReadInteger(bt.NodeIndex, "URIU", uartInUse)) {
kind = uartInUse == 0 ? BtUart::Kind::MiniUart : BtUart::Kind::Pl011;
}
bool mini = kind == BtUart::Kind::MiniUart;
Hal::AcpiDevices::DeviceInfo uart{};
if (!Hal::AcpiDevices::FindDeviceByHid(mini ? "BCM2836" : "BCM2837", uart)) {
KernelLogStream(WARNING, "BT-BCM") << bt.Path << ": its UART "
<< (source[0] ? source : (mini ? "(mini UART)" : "(PL011)")) << " is not in ACPI";
return false;
}
Hal::AML::ResourceList resources;
uint64_t base = 0, length = 0;
if (!Hal::AcpiDevices::EvaluateCrs(uart.NodeIndex, resources) ||
!resources.GetMemory(base, length) || base == 0) {
KernelLogStream(WARNING, "BT-BCM") << uart.Path << ": no register block in _CRS";
return false;
}
uint64_t console = Drivers::Serial::ConsoleAddress();
if (console != 0 && (console & ~0xFFFULL) == (base & ~0xFFFULL)) {
KernelLogStream(WARNING, "BT-BCM") << "Bluetooth's UART " << uart.Path
<< " is the serial console; leaving Bluetooth off";
return false;
}
BtUart::Config config{};
config.kind = kind;
config.mmioPhys = base;
config.levelTriggered = true;
if (auto* irq = resources.FindFirst(Hal::AML::ResourceType::ExtendedIrq)) {
config.gsi = irq->ExtendedIrq.Interrupt;
config.levelTriggered = (irq->ExtendedIrq.Flags & 1) == 0;
config.activeLow = (irq->ExtendedIrq.Flags & 2) != 0;
}
// The mini UART's baud generator runs off the VPU core clock: ask the
// firmware for its current rate, else take the rate the firmware
// patched into the table (MUCR; the placeholder 0x1000000 means it
// did not). The PL011's reference clock is fixed (CLCK, 48 MHz).
uint64_t tableClock = 0;
uint32_t clockHz = 0;
if (mini) {
if (!RpiFirmware::GetClockRate(RpiFirmware::CLOCK_CORE, clockHz)) {
if (ReadInteger(uart.NodeIndex, "MUCR", tableClock) && tableClock > 0x1000000)
clockHz = (uint32_t)tableClock;
else
clockHz = 500000000;
}
} else {
clockHz = ReadInteger(uart.NodeIndex, "CLCK", tableClock) && tableClock != 0
? (uint32_t)tableClock : 48000000;
}
config.clockHz = clockHz;
RpiGpio::Function function = mini ? RpiGpio::Function::Alt5 : RpiGpio::Function::Alt3;
bool pinsOk = RpiGpio::SetFunction(PIN_CTS, function)
&& RpiGpio::SetFunction(PIN_RTS, function)
&& RpiGpio::SetFunction(PIN_TXD, function)
&& RpiGpio::SetFunction(PIN_RXD, function)
&& RpiGpio::SetPull(PIN_CTS, RpiGpio::Pull::Up)
&& RpiGpio::SetPull(PIN_RTS, RpiGpio::Pull::None)
&& RpiGpio::SetPull(PIN_TXD, RpiGpio::Pull::None)
&& RpiGpio::SetPull(PIN_RXD, RpiGpio::Pull::Up);
if (!pinsOk) {
KernelLogStream(WARNING, "BT-BCM")
<< "No GPIO controller (BCM2845): UART pins left as the firmware set them";
}
if (!BtUart::Initialize(config)) return false;
Hci::InitializeUart();
Hci::SetVendor(Hci::Vendor::Broadcom);
KernelLogStream(OK, "BT-BCM") << "Bluetooth controller " << bt.Path << " on "
<< uart.Path << " (GSI " << (uint64_t)config.gsi << ")";
return true;
}
// =========================================================================
// Controller bring-up
// =========================================================================
static void PowerCycle() {
if (g_powerSwitch) {
if (RpiFirmware::SetGpioState(RpiFirmware::EXPGPIO_BT_ON, false)) {
Timekeeping::BusyWaitMs(20);
RpiFirmware::SetGpioState(RpiFirmware::EXPGPIO_BT_ON, true);
// hci_bcm allows 100-120 ms to power on and leave reset.
Timekeeping::BusyWaitMs(150);
} else {
g_powerSwitch = false;
KernelLogStream(WARNING, "BT-BCM")
<< "Cannot switch BT_ON through the firmware; assuming the controller is powered";
}
}
Hci::ResetUartReceiver();
}
static bool ResetController(uint32_t timeoutMs) {
if (!Hci::SendCommand(Hci::OP_RESET, nullptr, 0)) return false;
return Hci::WaitCommandComplete(Hci::OP_RESET, nullptr, 0, timeoutMs);
}
// Reach the controller at its power-on 115200 baud. Hardware RTS is tried
// first: if the RTS line is wired the wrong way round for it, the
// controller sees RTS deasserted and never answers, and the retry without
// it tells the two cases apart.
static bool SyncAtInitialBaud() {
BtUart::SetBaud(InitialBaud);
static constexpr bool attempts[] = { true, true, false, false };
for (bool flow : attempts) {
BtUart::SetFlowControl(flow);
Hci::ResetUartReceiver();
if (ResetController(1000)) {
g_flowControl = flow;
return true;
}
}
return false;
}
static uint32_t PickOperatingBaud() {
uint32_t wanted;
if (BtUart::GetKind() == BtUart::Kind::Pl011) {
wanted = g_flowControl ? 3000000 : 921600;
} else if (!BtUart::HasInterrupt()) {
// Polled 8-byte FIFO: stay where nothing overruns.
return InitialBaud;
} else {
wanted = g_flowControl ? 921600 : 460800;
}
static constexpr uint32_t rates[] = { 3000000, 2000000, 921600, 460800, 230400 };
for (uint32_t rate : rates) {
if (rate <= wanted && BtUart::BaudReachable(rate)) return rate;
}
return InitialBaud;
}
// Both ends to `baud`. The controller answers 0xFC18 at the old speed,
// then switches. False leaves the host at its old speed; the controller
// may or may not have switched (the caller power-cycles to be sure).
static bool SwitchBaud(uint32_t baud) {
uint32_t oldBaud = BtUart::GetBaud();
uint8_t params[6] = {
0x00, 0x00,
(uint8_t)baud, (uint8_t)(baud >> 8), (uint8_t)(baud >> 16), (uint8_t)(baud >> 24),
};
if (!Hci::SendCommand(Hci::OP_BCM_UPDATE_BAUD_RATE, params, sizeof(params)) ||
!Hci::WaitCommandComplete(Hci::OP_BCM_UPDATE_BAUD_RATE, nullptr, 0, 1000)) {
KernelLogStream(WARNING, "BT-BCM") << "Controller refused " << (uint64_t)baud << " baud";
return false;
}
if (!BtUart::SetBaud(baud)) return false;
Timekeeping::BusyWaitMs(10);
Hci::ResetUartReceiver();
Hci::LocalVersion ver{};
if (Hci::ReadLocalVersion(&ver)) return true;
KernelLogStream(WARNING, "BT-BCM") << "No answer at " << (uint64_t)baud << " baud";
BtUart::SetBaud(oldBaud);
Hci::ResetUartReceiver();
return false;
}
static const char* ChipFor(uint16_t subversion) {
for (const auto& chip : g_chips) {
if (chip.subversion == subversion) return chip.name;
}
return nullptr;
}
// Load 0:/os/firmware/brcm/<chip>.hcd. Returns nullptr (and logs) when
// the file is missing; the caller frees the image.
static uint8_t* LoadPatch(const char* chip, uint64_t& size) {
char path[64];
const char* prefix = "0:/os/firmware/brcm/";
int n = 0;
for (const char* p = prefix; *p; p++) path[n++] = *p;
for (const char* p = chip; *p && n < 56; p++) path[n++] = *p;
for (const char* p = ".hcd"; *p; p++) path[n++] = *p;
path[n] = '\0';
Fs::Vfs::BackendFile file;
if (Fs::Vfs::OpenBackendFile(path, file) < 0) {
KernelLogStream(WARNING, "BT-BCM") << "Firmware patch not found: " << path;
return nullptr;
}
size = Fs::Vfs::GetBackendFileSize(file);
uint8_t* image = size ? (uint8_t*)Memory::g_heap->Request(size) : nullptr;
if (image == nullptr) {
Fs::Vfs::CloseBackendFile(file);
return nullptr;
}
int got = Fs::Vfs::ReadBackendFile(file, image, 0, size);
Fs::Vfs::CloseBackendFile(file);
if (got != (int)size) {
Memory::g_heap->Free(image);
KernelLogStream(ERROR, "BT-BCM") << "Short read of " << path;
return nullptr;
}
KernelLogStream(INFO, "BT-BCM") << "Loading " << path << " (" << size << " bytes)";
return image;
}
static bool DownloadPatch(const uint8_t* image, uint64_t size) {
if (!Hci::SendCommand(Hci::OP_BCM_DOWNLOAD_MINIDRV, nullptr, 0) ||
!Hci::WaitCommandComplete(Hci::OP_BCM_DOWNLOAD_MINIDRV, nullptr, 0, 2000)) {
KernelLogStream(ERROR, "BT-BCM") << "Download Minidriver failed";
return false;
}
Timekeeping::BusyWaitMs(50);
uint64_t start = Timekeeping::GetMilliseconds();
uint64_t off = 0;
uint32_t records = 0;
while (off + 3 <= size) {
uint16_t opcode = (uint16_t)image[off] | ((uint16_t)image[off + 1] << 8);
uint8_t paramLength = image[off + 2];
off += 3;
if (off + paramLength > size) {
KernelLogStream(ERROR, "BT-BCM") << "Truncated patch record " << (uint64_t)records;
return false;
}
if (!Hci::SendCommand(opcode, image + off, paramLength)) return false;
bool acked = Hci::WaitCommandComplete(opcode, nullptr, 0, 2000);
if (!acked && opcode != Hci::OP_BCM_LAUNCH_RAM) {
KernelLogStream(ERROR, "BT-BCM") << "Patch record " << (uint64_t)records
<< " (opcode " << base::hex << (uint64_t)opcode << base::dec << ") failed";
return false;
}
off += paramLength;
records++;
}
// The patched firmware starts after Launch RAM; btbcm waits 250 ms.
Timekeeping::BusyWaitMs(250);
KernelLogStream(OK, "BT-BCM") << "Patch downloaded: " << (uint64_t)records << " records in "
<< (Timekeeping::GetMilliseconds() - start) << " ms at "
<< (uint64_t)BtUart::GetBaud() << " baud";
return true;
}
static void LogVersion(const char* chip, const Hci::LocalVersion& ver) {
uint16_t sub = ver.LmpSubversion;
KernelLogStream(OK, "BT-BCM") << (chip ? chip : "Broadcom controller") << " ("
<< (uint64_t)((sub & 0xE000) >> 13) << "." << (uint64_t)((sub & 0x1F00) >> 8)
<< "." << (uint64_t)(sub & 0x00FF) << ") build " << (uint64_t)(ver.HciRevision & 0x0FFF)
<< ", " << (uint64_t)BtUart::GetBaud() << " baud, "
<< (g_flowControl ? "RTS flow control" : "no flow control");
}
// One complete bring-up. `fast` raises the speed for the download and
// afterwards; without it everything stays at 115200.
static bool Attempt(bool fast) {
PowerCycle();
if (!SyncAtInitialBaud()) {
KernelLogStream(ERROR, "BT-BCM") << "Controller does not answer HCI Reset"
<< (g_powerSwitch ? "" : " (BT_ON was never switched on: it is likely unpowered)");
return false;
}
Hci::LocalVersion rom{};
if (!Hci::ReadLocalVersion(&rom)) return false;
const char* chip = ChipFor(rom.LmpSubversion);
KernelLogStream(INFO, "BT-BCM") << "ROM firmware: LMP subversion " << base::hex
<< (uint64_t)rom.LmpSubversion << ", HCI revision " << (uint64_t)rom.HciRevision
<< base::dec << (chip ? " -> " : " (unknown chip)") << (chip ? chip : "");
uint32_t operating = fast ? PickOperatingBaud() : InitialBaud;
if (operating != InitialBaud && !SwitchBaud(operating)) return false;
uint64_t size = 0;
uint8_t* image = chip ? LoadPatch(chip, size) : nullptr;
if (image != nullptr) {
bool ok = DownloadPatch(image, size);
Memory::g_heap->Free(image);
if (!ok) return false;
// The patched firmware comes up at 115200 again.
BtUart::SetBaud(InitialBaud);
Hci::ResetUartReceiver();
bool reset = false;
for (int i = 0; i < 3 && !reset; i++) reset = ResetController(1000);
if (!reset) {
KernelLogStream(ERROR, "BT-BCM") << "Patched firmware does not answer HCI Reset";
return false;
}
if (operating != InitialBaud && !SwitchBaud(operating)) return false;
} else {
KernelLogStream(WARNING, "BT-BCM") << "Running the controller's ROM firmware";
}
Hci::LocalVersion ver{};
if (Hci::ReadLocalVersion(&ver)) LogVersion(chip, ver);
return true;
}
bool Initialize() {
if (Hci::GetTransport() != Hci::Transport::Uart) return false;
if (Attempt(true)) return true;
KernelLogStream(WARNING, "BT-BCM") << "Retrying bring-up at 115200 baud";
return Attempt(false);
}
// =========================================================================
// BD_ADDR
// =========================================================================
// Addresses are in HCI order (addr[0] = least significant octet).
static bool IsDefaultAddress(const uint8_t* addr) {
bool allZero = true, allOnes = true;
for (int i = 0; i < 6; i++) {
if (addr[i] != 0x00) allZero = false;
if (addr[i] != 0xFF) allOnes = false;
}
if (allZero || allOnes) return true;
// The chip-wide defaults Linux's btbcm lists for these parts (the
// BCM4345C5's is 43:45:C5:00:1F:AC) start with 0x43, which has the
// IEEE group bit set: no assigned device address does.
return (addr[5] & 0x01) != 0;
}
void FixDefaultAddress() {
uint8_t addr[6];
if (!Hci::ReadBdAddr(addr) || !IsDefaultAddress(addr)) return;
uint8_t mac[6];
if (!RpiFirmware::GetBoardMacAddress(mac)) {
KernelLogStream(WARNING, "BT-BCM")
<< "Controller has its chip-wide default BD_ADDR and no board MAC is available";
return;
}
// The board's Ethernet MAC is unique to it and in the Raspberry Pi's
// own address block.
uint8_t board[6];
for (int i = 0; i < 6; i++) board[i] = mac[5 - i];
if (Hci::SetBdAddr(board)) {
KernelLogStream(OK, "BT-BCM") << "Replaced the chip-wide default BD_ADDR with the board's";
} else {
KernelLogStream(WARNING, "BT-BCM") << "Controller rejected the board BD_ADDR";
}
}
}
+27
View File
@@ -0,0 +1,27 @@
/*
* Broadcom.hpp
* Broadcom/Cypress UART Bluetooth controller bring-up (Raspberry Pi 4/400)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
namespace Drivers::Bluetooth::Broadcom {
// Find the on-board controller ACPI describes (BCM2EA6, \_SB.GDV0.BTH0),
// route its UART to GPIO 30-33 and start the UART and the H4 HCI
// transport. Needs no firmware files, so it runs during the boot driver
// probe. Returns false when there is no such controller.
bool ProbePlatform();
// Power-cycle the controller, load its firmware patch from
// 0:/os/firmware/brcm/ and raise the UART to its operating speed.
// Leaves the controller freshly reset and ready for standard HCI.
bool Initialize();
// After the last HCI Reset: give a controller still on its chip-wide
// default BD_ADDR (the CYW43456 ships with one) a board-unique address
// from the Pi's Ethernet MAC.
void FixDefaultAddress();
}
@@ -1,6 +1,6 @@
/*
* Hci.cpp
* Bluetooth HCI transport over USB
* Bluetooth HCI transport over USB or a UART (H4)
* Copyright (c) 2026 Daniel Hammer
*/
@@ -10,6 +10,7 @@
#include "A2dp.hpp"
#include <atomic>
#include <Fs/Vfs.hpp>
#include <Drivers/Serial/BtUart.hpp>
#include <Drivers/USB/Xhci.hpp>
#include <Drivers/USB/UsbDevice.hpp>
#include <Terminal/Terminal.hpp>
@@ -22,12 +23,17 @@
using namespace Kt;
namespace Drivers::USB::Bluetooth::Hci {
namespace Drivers::Bluetooth::Hci {
namespace Xhci = Drivers::USB::Xhci;
namespace BtUart = Drivers::Serial::BtUart;
// =========================================================================
// State
// =========================================================================
static Transport g_transport = Transport::None;
static Vendor g_vendor = Vendor::Unknown;
static uint8_t g_slotId = 0;
static bool g_initialized = false;
@@ -541,6 +547,157 @@ namespace Drivers::USB::Bluetooth::Hci {
}
}
// =========================================================================
// UART (H4) receive deframing
// =========================================================================
// The UART delivers one byte stream: [type][header][payload]... Pump()
// feeds it here in process context, so complete packets go straight to
// the same command mailbox / async event ring / ACL ring the USB
// callback fills. A byte that cannot start a packet is dropped, which
// resynchronizes after line noise (e.g. while the controller powers up).
enum class H4State : uint8_t { Type, Header, Payload, Discard };
static H4State g_h4State = H4State::Type;
static uint8_t g_h4Type = 0;
static uint8_t g_h4Packet[ACL_RX_SLOT_SIZE] = {};
static uint16_t g_h4Len = 0; // bytes in g_h4Packet
static uint16_t g_h4HeaderLen = 0;
static uint16_t g_h4Expected = 0; // header + payload
static uint32_t g_h4Remaining = 0; // Discard: bytes left to skip
static uint32_t g_h4Dropped = 0; // bytes that could not start a packet
// The UART receive ring and the deframer have one consumer at a time:
// whoever holds this (Pump, or a flush).
static std::atomic<bool> g_uartConsumer{false};
void ResetUartReceiver() {
bool expected = false;
while (!g_uartConsumer.compare_exchange_weak(expected, true,
std::memory_order_acquire)) {
expected = false;
Arch::Pause();
}
BtUart::FlushRx();
g_h4State = H4State::Type;
g_h4Len = 0;
g_h4Remaining = 0;
g_uartConsumer.store(false, std::memory_order_release);
}
static void CompleteH4Packet() {
if (g_h4Type == H4_EVENT) {
QueueCompleteHciEvent(g_h4Packet, g_h4Len);
} else if (g_h4Type == H4_ACL) {
QueueCompleteAclPacket(g_h4Packet, g_h4Len);
}
}
static void FeedH4Bytes(const uint8_t* data, uint32_t length) {
uint32_t off = 0;
while (off < length) {
switch (g_h4State) {
case H4State::Type: {
uint8_t type = data[off++];
g_h4Type = type;
g_h4Len = 0;
if (type == H4_EVENT) g_h4HeaderLen = sizeof(EventHeader);
else if (type == H4_ACL) g_h4HeaderLen = sizeof(AclHeader);
else if (type == H4_SCO) g_h4HeaderLen = 3;
else { g_h4Dropped++; break; }
g_h4State = H4State::Header;
break;
}
case H4State::Header: {
g_h4Packet[g_h4Len++] = data[off++];
if (g_h4Len < g_h4HeaderLen) break;
uint32_t payload;
if (g_h4Type == H4_EVENT) payload = g_h4Packet[1];
else if (g_h4Type == H4_ACL) payload = (uint32_t)g_h4Packet[2]
| ((uint32_t)g_h4Packet[3] << 8);
else payload = g_h4Packet[2];
// SCO is not used; an ACL packet beyond the ring slot
// cannot be a valid one from a controller that reported
// its buffer size. Skip either without losing framing.
if (g_h4Type == H4_SCO
|| g_h4HeaderLen + payload > sizeof(g_h4Packet)) {
g_h4Remaining = payload;
g_h4State = payload ? H4State::Discard : H4State::Type;
break;
}
g_h4Expected = (uint16_t)(g_h4HeaderLen + payload);
if (payload == 0) {
CompleteH4Packet();
g_h4State = H4State::Type;
} else {
g_h4State = H4State::Payload;
}
break;
}
case H4State::Payload: {
uint32_t take = g_h4Expected - g_h4Len;
if (take > length - off) take = length - off;
memcpy(&g_h4Packet[g_h4Len], &data[off], take);
g_h4Len = (uint16_t)(g_h4Len + take);
off += take;
if (g_h4Len == g_h4Expected) {
CompleteH4Packet();
g_h4State = H4State::Type;
}
break;
}
case H4State::Discard: {
uint32_t take = length - off;
if (take > g_h4Remaining) take = g_h4Remaining;
off += take;
g_h4Remaining -= take;
if (g_h4Remaining == 0) g_h4State = H4State::Type;
break;
}
}
}
}
// =========================================================================
// Transport pump
// =========================================================================
void Pump() {
if (g_transport == Transport::Usb) {
Xhci::PollEvents();
return;
}
if (g_transport != Transport::Uart) return;
bool expected = false;
if (!g_uartConsumer.compare_exchange_strong(expected, true,
std::memory_order_acquire)) return;
// Also moves bytes by hand, in case the UART interrupt never fires.
BtUart::Service();
uint8_t chunk[256];
int n;
while ((n = BtUart::Read(chunk, sizeof(chunk))) > 0) {
FeedH4Bytes(chunk, (uint32_t)n);
}
g_uartConsumer.store(false, std::memory_order_release);
}
bool InPumpContext() {
// UART packets are only queued by Pump, never handled inside it.
return g_transport == Transport::Usb && Xhci::InPollContext();
}
bool TransportHasRxData() {
return g_transport == Transport::Uart && BtUart::HasRxData();
}
Transport GetTransport() { return g_transport; }
void SetVendor(Vendor vendor) { g_vendor = vendor; }
Vendor GetVendor() { return g_vendor; }
// =========================================================================
// USB transfer callback
// =========================================================================
@@ -641,7 +798,7 @@ namespace Drivers::USB::Bluetooth::Hci {
static void PollWait(uint32_t ms) {
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < ms) {
Xhci::PollEvents();
Pump();
DrainEvents();
for (int j = 0; j < 100; j++) {
Arch::CompilerBarrier();
@@ -672,10 +829,20 @@ namespace Drivers::USB::Bluetooth::Hci {
// the configured USB function its settling delay. StartEventPipe then
// arms both receive endpoints before the first bootloader command.
g_transport = Transport::Usb;
g_initialized = true;
KernelLogStream(OK, "BT-HCI") << "HCI transport initialized on slot " << (uint64_t)slotId;
}
void InitializeUart() {
// No DMA buffers: SendCommand/SendAcl copy each H4 packet straight
// into the UART's transmit ring.
ResetUartReceiver();
g_transport = Transport::Uart;
g_initialized = true;
KernelLogStream(OK, "BT-HCI") << "HCI transport initialized on UART";
}
// Start receiving HCI events and ACL data — call after HCI init sequence.
// Arms BOTH the interrupt IN (events) and the bulk IN (ACL). The bulk IN
// MUST stay armed across the firmware download: on this controller the device
@@ -712,7 +879,8 @@ namespace Drivers::USB::Bluetooth::Hci {
// =========================================================================
bool SendCommand(uint16_t opcode, const uint8_t* params, uint8_t paramLen) {
if (!g_initialized || !g_cmdDmaBuf) return false;
if (!g_initialized) return false;
if (g_transport == Transport::Usb && !g_cmdDmaBuf) return false;
uint16_t expected = 0;
if (!g_commandOpcode.compare_exchange_strong(expected, opcode,
@@ -745,6 +913,27 @@ namespace Drivers::USB::Bluetooth::Hci {
<< base::hex << (uint64_t)opcode << base::dec;
}
if (g_transport == Transport::Uart) {
// H4: [0x01][opcode][paramLen][params]. The transmit ring is
// shared with ACL traffic; wait briefly for room behind it.
uint8_t header[4] = {
H4_COMMAND, (uint8_t)(opcode & 0xFF), (uint8_t)(opcode >> 8), paramLen
};
uint64_t start = Timekeeping::GetMilliseconds();
while (!BtUart::Write(header, sizeof(header), params, paramLen)) {
if (Timekeeping::GetMilliseconds() - start > 1000) {
FinishCommand(opcode);
KernelLogStream(WARNING, "BT-HCI") << "SendCommand: UART transmit stalled, opcode="
<< base::hex << (uint64_t)opcode << base::dec;
return false;
}
BtUart::Service();
Arch::Pause();
}
return true;
}
// Use DMA-allocated buffer (not stack) for the command data.
// xHCI reads from this buffer via DMA for OUT transfers.
memset(g_cmdDmaBuf, 0, 512);
@@ -791,7 +980,7 @@ namespace Drivers::USB::Bluetooth::Hci {
// cannot wait -- a nested PollEvents is a no-op, so the Command Complete
// is reaped by the active PollEvents after we return. The command was
// already submitted (fire-and-forget); report success.
if (Xhci::InPollContext()) {
if (InPumpContext()) {
FinishCommand(opcode);
return true;
}
@@ -799,7 +988,7 @@ namespace Drivers::USB::Bluetooth::Hci {
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
Xhci::PollEvents();
Pump();
DrainEvents();
uint8_t event[sizeof(g_eventBuf)];
@@ -852,7 +1041,7 @@ namespace Drivers::USB::Bluetooth::Hci {
bool WaitCommandStatus(uint16_t opcode, uint32_t timeoutMs) {
// See WaitCommandComplete: cannot wait when nested under PollEvents.
if (Xhci::InPollContext()) {
if (InPumpContext()) {
FinishCommand(opcode);
return true;
}
@@ -860,7 +1049,7 @@ namespace Drivers::USB::Bluetooth::Hci {
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
Xhci::PollEvents();
Pump();
DrainEvents();
uint8_t event[sizeof(g_eventBuf)];
@@ -897,10 +1086,40 @@ namespace Drivers::USB::Bluetooth::Hci {
// SendAcl — via USB bulk OUT
// =========================================================================
// Room the UART transmit ring must keep for one maximum-size ACL packet.
static uint32_t UartAclSlotBytes() {
return 1u + sizeof(AclHeader) + (g_aclMaxLen ? g_aclMaxLen : 1021);
}
bool SendAcl(uint16_t handle, uint16_t pbFlag, const uint8_t* data, uint16_t len) {
if (!g_initialized || !g_aclTxRing[0] || !GetConnection(handle)) return false;
if (!g_initialized || !GetConnection(handle)) return false;
if (len + sizeof(AclHeader) > 4096) return false; // Single page DMA buffer
if (g_transport == Transport::Uart) {
g_aclTxLock.Acquire();
if (g_aclMaxNum != 0 && AclPendingCount() >= g_aclMaxNum) {
g_aclTxLock.Release();
return false;
}
uint16_t handleFlags = (uint16_t)((handle & 0x0FFF) | pbFlag);
uint8_t header[1 + sizeof(AclHeader)] = {
H4_ACL,
(uint8_t)handleFlags, (uint8_t)(handleFlags >> 8),
(uint8_t)len, (uint8_t)(len >> 8)
};
// All or nothing: a partial H4 packet would desynchronize the
// controller's deframer. A full ring drops the packet, as a
// full xHCI TX ring does.
bool queued = BtUart::Write(header, sizeof(header), data, len);
if (queued) {
g_aclPendingCount.fetch_add(1, std::memory_order_relaxed);
g_aclTxCount.fetch_add(1, std::memory_order_relaxed);
}
g_aclTxLock.Release();
return queued;
}
if (!g_aclTxRing[0]) return false;
g_aclTxLock.Acquire();
// HCI host flow control is a hard limit, not merely a media-pacing
@@ -951,6 +1170,11 @@ namespace Drivers::USB::Bluetooth::Hci {
uint16_t AclMaxPackets() { return g_aclMaxNum; }
uint32_t AclTxInFlight() {
if (g_transport == Transport::Uart) {
uint32_t pending = BtUart::TxPending();
uint32_t slot = UartAclSlotBytes();
return (pending + slot - 1) / slot;
}
uint32_t tx = g_aclTxCount.load(std::memory_order_relaxed);
uint32_t done = g_aclTxDoneCount.load(std::memory_order_relaxed);
return (tx >= done) ? (tx - done) : 0;
@@ -966,6 +1190,10 @@ namespace Drivers::USB::Bluetooth::Hci {
uint16_t maxOut = g_aclMaxNum ? g_aclMaxNum : 4;
uint16_t mediaLimit = maxOut > 1 ? (uint16_t)(maxOut - 1) : maxOut;
if (AclPendingCount() >= mediaLimit) return false;
if (g_transport == Transport::Uart) {
// Room for this packet plus a signaling reply behind it.
return BtUart::TxFree() >= 2 * UartAclSlotBytes();
}
if (AclTxInFlight() >= (uint32_t)(ACL_TX_SLOTS - 1)) return false;
return true;
}
@@ -1420,6 +1648,7 @@ namespace Drivers::USB::Bluetooth::Hci {
}
case EVT_VENDOR_SPECIFIC: {
if (g_vendor != Vendor::Intel) break;
// Intel vendor events carry a sub-opcode in the first byte.
// 0x02 = "bootup" (operational firmware booted after 0xFC01)
// 0x06 = "secure send result": result(1) opcode(2) status(1)
@@ -1625,7 +1854,7 @@ namespace Drivers::USB::Bluetooth::Hci {
// copying past what the controller delivered.
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < 2000) {
Xhci::PollEvents();
Pump();
DrainEvents();
uint8_t event[sizeof(g_eventBuf)];
@@ -1676,7 +1905,7 @@ namespace Drivers::USB::Bluetooth::Hci {
void ProcessPendingCommands() {
// Top-level only: drains queued pairing replies with real, confirmed
// control transfers. Must NOT be called from inside PollEvents.
if (Xhci::InPollContext()) return;
if (InPumpContext()) return;
// One consumer at a time: this runs from the Connect wait loop AND the
// idle-core service pump, potentially on different cores; two consumers
// on the lockless queue would race the tail and interleave commands.
@@ -1728,7 +1957,7 @@ namespace Drivers::USB::Bluetooth::Hci {
bool WaitSecureSendResult(uint32_t timeoutMs, uint8_t* outResult, uint8_t* outStatus) {
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
Xhci::PollEvents();
Pump();
DrainEvents();
if (g_secureResultValid) {
if (outResult) *outResult = g_secureResult;
@@ -1763,7 +1992,7 @@ namespace Drivers::USB::Bluetooth::Hci {
<< " byte " << g_ssBytesSent;
return false;
}
Xhci::PollEvents();
Pump();
DrainEvents();
g_ssBytesSent += frag;
@@ -1784,7 +2013,7 @@ namespace Drivers::USB::Bluetooth::Hci {
// header is deliberately absent, and the NOCP credit counter is NOT
// touched: these are commands, not ACL data).
bool IntelSecureSendBulk(uint8_t fragmentType, const uint8_t* data, uint32_t len) {
if (!g_initialized || !g_aclTxRing[0]) return false;
if (!g_initialized || g_transport != Transport::Usb || !g_aclTxRing[0]) return false;
if (g_aclTxTransportError.load(std::memory_order_acquire)) return false;
uint32_t off = 0;
@@ -1796,7 +2025,7 @@ namespace Drivers::USB::Bluetooth::Hci {
// controller from freezing the boot path.
uint64_t start = Timekeeping::GetMilliseconds();
while (AclTxInFlight() >= (uint32_t)(ACL_TX_SLOTS - 1)) {
Xhci::PollEvents();
Pump();
DrainEvents();
if (g_aclTxTransportError.load(std::memory_order_acquire)) {
KernelLogStream(ERROR, "BT-HCI")
@@ -1843,7 +2072,7 @@ namespace Drivers::USB::Bluetooth::Hci {
bool DrainBulkTx(uint32_t timeoutMs) {
uint64_t start = Timekeeping::GetMilliseconds();
while (AclTxInFlight() > 0) {
Xhci::PollEvents();
Pump();
DrainEvents();
if (g_aclTxTransportError.load(std::memory_order_acquire)) return false;
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
@@ -1872,7 +2101,7 @@ namespace Drivers::USB::Bluetooth::Hci {
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
Xhci::PollEvents();
Pump();
DrainEvents();
if (g_intelBootup) return true;
for (int j = 0; j < 100; j++) Arch::CompilerBarrier();
@@ -1953,9 +2182,14 @@ namespace Drivers::USB::Bluetooth::Hci {
}
bool SetBdAddr(const uint8_t* addr) {
// Intel 0xFC31: 6-byte BD_ADDR, little-endian (same order as ReadBdAddr).
if (!SendCommand(OP_INTEL_WRITE_BD_ADDR, addr, 6)) return false;
return WaitCommandComplete(OP_INTEL_WRITE_BD_ADDR, nullptr, 0, 2000);
// Intel 0xFC31 / Broadcom 0xFC01: 6-byte BD_ADDR, little-endian (same
// order as ReadBdAddr).
uint16_t opcode;
if (g_vendor == Vendor::Intel) opcode = OP_INTEL_WRITE_BD_ADDR;
else if (g_vendor == Vendor::Broadcom) opcode = OP_BCM_WRITE_BD_ADDR;
else return false;
if (!SendCommand(opcode, addr, 6)) return false;
return WaitCommandComplete(opcode, nullptr, 0, 2000);
}
bool Disconnect(uint16_t handle, uint8_t reason) {
@@ -1,23 +1,35 @@
/*
* Hci.hpp
* Bluetooth HCI (Host Controller Interface) layer
* HCI transport over USB bulk/interrupt/control endpoints
* HCI transport over USB (bulk/interrupt/control endpoints) or a UART (H4)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Drivers::USB::Bluetooth::Hci {
namespace Drivers::Bluetooth::Hci {
// =========================================================================
// HCI packet types (for USB transport)
// HCI transports
// =========================================================================
// USB transport uses different endpoints for each packet type:
// Commands -> Control EP0 (class request)
// ACL data -> Bulk OUT / Bulk IN
// Events -> Interrupt IN
//
// The UART transport (H4, Bluetooth Core spec Vol 4 Part A) carries all
// of them on one byte stream, each packet preceded by its type.
constexpr uint8_t H4_COMMAND = 0x01;
constexpr uint8_t H4_ACL = 0x02;
constexpr uint8_t H4_SCO = 0x03;
constexpr uint8_t H4_EVENT = 0x04;
enum class Transport : uint8_t { None, Usb, Uart };
// Controller vendor, for the vendor-specific commands (BD_ADDR write).
enum class Vendor : uint8_t { Unknown, Intel, Broadcom };
// =========================================================================
// HCI command opcodes (OGF << 10 | OCF)
@@ -61,6 +73,13 @@ namespace Drivers::USB::Bluetooth::Hci {
constexpr uint16_t OP_READ_LOCAL_FEATURES = 0x1003;
constexpr uint16_t OP_READ_BUFFER_SIZE = 0x1005;
// Broadcom vendor commands (OGF 0x3F), from Linux btbcm/hci_bcm
constexpr uint16_t OP_BCM_WRITE_BD_ADDR = 0xFC01;
constexpr uint16_t OP_BCM_UPDATE_BAUD_RATE = 0xFC18;
constexpr uint16_t OP_BCM_DOWNLOAD_MINIDRV = 0xFC2E;
constexpr uint16_t OP_BCM_WRITE_RAM = 0xFC4C;
constexpr uint16_t OP_BCM_LAUNCH_RAM = 0xFC4E;
// Intel vendor commands (OGF 0x3F)
constexpr uint16_t OP_INTEL_READ_VERSION = 0xFC05;
constexpr uint16_t OP_INTEL_RESET = 0xFC01;
@@ -175,6 +194,32 @@ namespace Drivers::USB::Bluetooth::Hci {
// Initialize HCI transport over USB for the given slot
void Initialize(uint8_t slotId);
// Initialize HCI transport over the Bluetooth UART (Serial::BtUart,
// already initialized by the caller).
void InitializeUart();
Transport GetTransport();
void SetVendor(Vendor vendor);
Vendor GetVendor();
// Move received bytes from the transport into the HCI queues: the xHCI
// event ring for USB, the UART receive ring (H4 deframing) for UART.
// Every wait loop calls this before DrainEvents. Non-reentrant; a
// nested or concurrent call returns at once.
void Pump();
// True while running nested under Pump (USB transfer callbacks), where
// a command cannot be waited for.
bool InPumpContext();
// Received transport bytes are waiting for Pump (UART only; USB signals
// through the xHCI deferred-work flag).
bool TransportHasRxData();
// Discard all received UART input, queued bytes and any partial packet
// (after a power cycle or a baud-rate change).
void ResetUartReceiver();
// Start receiving HCI events and ACL data (call after HCI init sequence).
// Arms both the interrupt IN and the bulk IN; the bulk IN must stay armed
// through the firmware download (it absorbs the device's ~635 KB cc=4 glitch
@@ -186,7 +231,7 @@ namespace Drivers::USB::Bluetooth::Hci {
// are intentionally ignored and any endpoint error is recovered here.
void EnableAclDataReception();
// Send an HCI command via USB control transfer (EP0)
// Send an HCI command (USB: control transfer on EP0; UART: H4 packet)
bool SendCommand(uint16_t opcode, const uint8_t* params, uint8_t paramLen);
// Wait for a Command Complete event matching the given opcode
@@ -197,7 +242,7 @@ namespace Drivers::USB::Bluetooth::Hci {
// Wait for a Command Status event matching the given opcode
bool WaitCommandStatus(uint16_t opcode, uint32_t timeoutMs = 2000);
// Send ACL data via USB bulk OUT
// Send ACL data (USB: bulk OUT; UART: H4 packet)
bool SendAcl(uint16_t handle, uint16_t pbFlag, const uint8_t* data, uint16_t len);
// Process an HCI event received on the interrupt IN endpoint
@@ -323,8 +368,10 @@ namespace Drivers::USB::Bluetooth::Hci {
// Drives Link Key Request -> pairing; needed for bonded-device reconnects.
bool AuthenticateLink(uint16_t handle);
// Set the adapter's BD_ADDR (Intel vendor command 0xFC31). Used to dodge a
// remote that holds a stale, un-clearable bond to our real address.
// Set the adapter's BD_ADDR (vendor command: Intel 0xFC31, Broadcom
// 0xFC01). Used to dodge a remote that holds a stale, un-clearable bond
// to our real address, and to give Broadcom parts without a programmed
// address a unique one.
bool SetBdAddr(const uint8_t* addr);
// Send any queued pairing replies (IO-cap / user-confirm / link-key) with
@@ -340,7 +387,8 @@ namespace Drivers::USB::Bluetooth::Hci {
uint16_t AclMaxPackets();
// ACL packets handed to the xHCI whose bulk OUT completion has not been
// reaped yet (each one still owns a TX DMA ring slot).
// reaped yet (each one still owns a TX DMA ring slot). On UART, the
// transmit ring's backlog in maximum-size packets (0 once drained).
uint32_t AclTxInFlight();
// True when SendAcl can be called without overrunning either the
@@ -23,7 +23,7 @@
using namespace Kt;
namespace Drivers::USB::Bluetooth {
namespace Drivers::Bluetooth {
// =========================================================================
// .sfi / TLV format constants (from drivers/bluetooth/btintel.{c,h})
@@ -7,7 +7,7 @@
#pragma once
#include <cstdint>
namespace Drivers::USB::Bluetooth {
namespace Drivers::Bluetooth {
// Drive the Intel bootloader firmware download sequence for the currently
// registered adapter:
@@ -9,7 +9,6 @@
#include "Hci.hpp"
#include "A2dp.hpp"
#include "Avrcp.hpp"
#include <Drivers/USB/Xhci.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Libraries/Memory.hpp>
@@ -17,7 +16,7 @@
using namespace Kt;
namespace Drivers::USB::Bluetooth::L2cap {
namespace Drivers::Bluetooth::L2cap {
// =========================================================================
// State
@@ -548,7 +547,7 @@ namespace Drivers::USB::Bluetooth::L2cap {
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
Xhci::PollEvents();
Hci::Pump();
Hci::DrainEvents(); // process incoming ACL (the Connection Response!)
auto* ch = GetChannel(localCid);
@@ -7,7 +7,7 @@
#pragma once
#include <cstdint>
namespace Drivers::USB::Bluetooth::L2cap {
namespace Drivers::Bluetooth::L2cap {
// =========================================================================
// L2CAP CIDs (Channel Identifiers)
@@ -9,7 +9,7 @@
#include "Sbc.hpp"
#include <Libraries/Memory.hpp>
namespace Drivers::USB::Bluetooth::Sbc {
namespace Drivers::Bluetooth::Sbc {
// =========================================================================
// Analysis filterbank tables
@@ -8,7 +8,7 @@
#pragma once
#include <cstdint>
namespace Drivers::USB::Bluetooth::Sbc {
namespace Drivers::Bluetooth::Sbc {
// =========================================================================
// SBC configuration
+5 -2
View File
@@ -17,6 +17,7 @@
#include <Drivers/Storage/Sdhci.hpp>
#include <Drivers/Storage/Gpt.hpp>
#include <Drivers/Audio/IntelHda.hpp>
#include <Drivers/Bluetooth/Bluetooth.hpp>
#include <Graphics/Framebuffer.hpp>
#include <Net/Net.hpp>
#include <Terminal/Terminal.hpp>
@@ -359,6 +360,7 @@ namespace Drivers {
Pci::ProbeAll(g_driverTable, g_driverTableCount, Pci::ProbePhase::Normal);
ProbePlatformXhci();
ProbePlatformSdhci();
Bluetooth::ProbePlatformAdapter();
}
void InitializeNetwork() {
@@ -373,8 +375,9 @@ namespace Drivers {
void InitializeAudio() {
// HDA driver initializes during ProbeNormal().
// Bluetooth audio (A2DP) initializes automatically during USB enumeration
// when a Bluetooth adapter is detected by the xHCI driver.
// Bluetooth audio (A2DP) initializes with its adapter: during USB
// enumeration, or for an on-board UART controller (Raspberry Pi) from
// the platform probe in ProbeNormal().
}
}
+250
View File
@@ -0,0 +1,250 @@
/*
* RpiFirmware.cpp
* Raspberry Pi VideoCore firmware mailbox (property interface)
* Copyright (c) 2026 Daniel Hammer
*/
#include "RpiFirmware.hpp"
#include <ACPI/AcpiDevices.hpp>
#include <Arch/Cpu.hpp>
#include <CppLib/Spinlock.hpp>
#include <CppLib/Stream.hpp>
#include <Libraries/Memory.hpp>
#include <Boot/Boot.hpp>
#include <Memory/Memmap.hpp>
#include <Memory/HHDM.hpp>
#include <Memory/Paging.hpp>
#include <Terminal/Terminal.hpp>
#include <Timekeeping/Timer.hpp>
// Property interface (raspberrypi/firmware wiki, "Mailbox property
// interface"): the ARM writes a message buffer's bus address, tagged with
// channel 8, to mailbox 1 and reads the same address back from mailbox 0
// once the firmware has written its responses into the buffer.
//
// u32 size, u32 code (0 = request; 0x80000000 = success),
// tags: { u32 id, u32 value buffer size, u32 req/resp (bit 31 + length),
// value buffer ... }, u32 0 (end tag)
using namespace Kt;
namespace Drivers::Platform::RpiFirmware {
// Mailbox registers (offsets from the block ACPI describes, 0x7E00B880)
static constexpr uint32_t MBOX_READ = 0x00; // mailbox 0: VC -> ARM
static constexpr uint32_t MBOX0_STATUS = 0x18;
static constexpr uint32_t MBOX_WRITE = 0x20; // mailbox 1: ARM -> VC
static constexpr uint32_t MBOX1_STATUS = 0x38;
static constexpr uint32_t STATUS_FULL = 1u << 31;
static constexpr uint32_t STATUS_EMPTY = 1u << 30;
static constexpr uint32_t CHANNEL_PROPERTY = 8;
static constexpr uint32_t CODE_REQUEST = 0x00000000;
static constexpr uint32_t CODE_SUCCESS = 0x80000000;
static constexpr uint32_t TAG_RESPONSE = 0x80000000;
static constexpr uint32_t TAG_GET_BOARD_MAC = 0x00010003;
static constexpr uint32_t TAG_GET_CLOCK_RATE = 0x00030002;
static constexpr uint32_t TAG_GET_GPIO_STATE = 0x00030041;
static constexpr uint32_t TAG_SET_GPIO_STATE = 0x00038041;
// The VideoCore sees the first GiB of SDRAM at bus address 0xC0000000
// (the uncached alias), and nothing above it, so the message buffer
// must sit below 1 GiB.
static constexpr uint64_t VC_BUS_ALIAS = 0xC0000000;
static constexpr uint64_t VC_DMA_LIMIT = 0x40000000;
static constexpr uint64_t TimeoutMs = 200;
static volatile uint8_t* g_mmio = nullptr;
static volatile uint32_t* g_buffer = nullptr; // uncached, below 1 GiB
static uint64_t g_bufferPhys = 0;
static bool g_probed = false;
static kcp::Mutex g_lock;
static inline uint32_t Read32(uint32_t offset) {
return *(volatile uint32_t*)(g_mmio + offset);
}
static inline void Write32(uint32_t offset, uint32_t value) {
*(volatile uint32_t*)(g_mmio + offset) = value;
}
// A page the VideoCore can reach. The page-frame allocator only manages
// the largest run of usable RAM, which on a Pi 4 with the 3 GB limit is
// 0x40000000 and up, so it never has one. The usable regions below 1 GiB
// are otherwise untouched: take the last page of the highest one.
static void* AllocateLowPage(uint64_t& phys) {
const auto& map = montauk::boot::Info().memoryMap;
Memory::LargestSection pool = Memory::Scan(map);
uint64_t best = 0;
for (uint64_t i = 0; i < map.count; i++) {
const auto& r = map.regions[i];
if (r.kind != montauk::boot::MemoryKind::Usable || r.base == 0) continue;
if (r.base == pool.address) continue; // the allocator's
uint64_t end = r.base + r.length;
if (end > VC_DMA_LIMIT) end = VC_DMA_LIMIT;
if (end < r.base + 0x1000) continue;
uint64_t page = (end - 0x1000) & ~0xFFFULL;
if (page >= r.base && page > best) best = page;
}
if (best == 0) return nullptr;
// Zero it through its cacheable HHDM alias, push the lines out, then
// make the alias uncached so neither side needs cache maintenance.
void* virt = (void*)Memory::HHDM(best);
memset(virt, 0, 0x1000);
Arch::InvalidateDataCache(virt, 0x1000);
Memory::VMM::g_paging->MapWC(best, (uint64_t)virt);
phys = best;
return virt;
}
bool Initialize() {
if (g_probed) return g_buffer != nullptr;
g_probed = true;
Hal::AcpiDevices::DeviceInfo info{};
if (!Hal::AcpiDevices::FindDeviceByHid("BCM2849", info)) return false;
Hal::AML::ResourceList resources;
uint64_t base = 0, length = 0;
if (!Hal::AcpiDevices::EvaluateCrs(info.NodeIndex, resources) ||
!resources.GetMemory(base, length) || base == 0) {
KernelLogStream(WARNING, "RpiFw") << "Mailbox " << info.Path << ": no register block in _CRS";
return false;
}
uint64_t page = base & ~0xFFFULL;
Memory::VMM::g_paging->MapMMIO(page, Memory::HHDM(page));
if (((base + 0x40) & ~0xFFFULL) != page) {
Memory::VMM::g_paging->MapMMIO(page + 0x1000, Memory::HHDM(page + 0x1000));
}
g_mmio = (volatile uint8_t*)Memory::HHDM(base);
uint64_t phys = 0;
void* buffer = AllocateLowPage(phys);
if (buffer == nullptr) {
KernelLogStream(ERROR, "RpiFw") << "No message buffer below 1 GiB";
g_mmio = nullptr;
return false;
}
g_buffer = (volatile uint32_t*)buffer;
g_bufferPhys = phys;
KernelLogStream(OK, "RpiFw") << "VideoCore mailbox at 0x" << base::hex << base
<< ", buffer 0x" << phys << base::dec;
return true;
}
bool IsPresent() {
return Initialize();
}
// Send the message in g_buffer and wait for the firmware's reply.
// Caller holds g_lock.
static bool Call() {
uint32_t message = (uint32_t)((g_bufferPhys | VC_BUS_ALIAS) & ~0xFULL) | CHANNEL_PROPERTY;
// Drop any stale replies (a previous call that timed out).
for (int i = 0; i < 16 && !(Read32(MBOX0_STATUS) & STATUS_EMPTY); i++) {
(void)Read32(MBOX_READ);
}
uint64_t start = Timekeeping::GetMilliseconds();
while (Read32(MBOX1_STATUS) & STATUS_FULL) {
if (Timekeeping::GetMilliseconds() - start > TimeoutMs) return false;
Arch::Pause();
}
// The buffer is uncached; the fence orders its stores before the
// doorbell write the firmware reacts to.
Arch::MemoryFence();
Write32(MBOX_WRITE, message);
start = Timekeeping::GetMilliseconds();
for (;;) {
while (Read32(MBOX0_STATUS) & STATUS_EMPTY) {
if (Timekeeping::GetMilliseconds() - start > TimeoutMs) {
KernelLogStream(WARNING, "RpiFw") << "Mailbox call timed out";
return false;
}
Arch::Pause();
}
uint32_t reply = Read32(MBOX_READ);
if (reply == message) break;
}
Arch::MemoryFence();
return g_buffer[1] == CODE_SUCCESS;
}
// One-tag property call. `values` holds valueWords request words and
// receives up to valueWords response words. Returns the response length
// in bytes, or -1 on failure.
static int PropertyCall(uint32_t tag, uint32_t* values, uint32_t valueWords) {
if (!Initialize()) return -1;
if (valueWords > 16) return -1;
g_lock.Acquire();
uint32_t words = 0;
g_buffer[words++] = 0; // size, filled in below
g_buffer[words++] = CODE_REQUEST;
g_buffer[words++] = tag;
g_buffer[words++] = valueWords * 4; // value buffer size
g_buffer[words++] = 0; // request
uint32_t valueStart = words;
for (uint32_t i = 0; i < valueWords; i++) g_buffer[words++] = values[i];
g_buffer[words++] = 0; // end tag
g_buffer[0] = words * 4;
int result = -1;
if (Call()) {
uint32_t status = g_buffer[valueStart - 1];
if (status & TAG_RESPONSE) {
uint32_t length = status & ~TAG_RESPONSE;
uint32_t copy = (length + 3) / 4;
if (copy > valueWords) copy = valueWords;
for (uint32_t i = 0; i < copy; i++) values[i] = g_buffer[valueStart + i];
result = (int)length;
}
}
g_lock.Release();
return result;
}
bool GetClockRate(uint32_t clockId, uint32_t& hz) {
uint32_t v[2] = { clockId, 0 };
if (PropertyCall(TAG_GET_CLOCK_RATE, v, 2) < 8 || v[0] != clockId) return false;
hz = v[1];
return hz != 0;
}
// The GPIO tags answer in place of the GPIO number with a status word,
// 0 for success (as Linux's raspberrypi-exp-gpio driver checks).
bool SetGpioState(uint32_t gpio, bool high) {
uint32_t v[2] = { gpio, high ? 1u : 0u };
return PropertyCall(TAG_SET_GPIO_STATE, v, 2) >= 4 && v[0] == 0;
}
bool GetGpioState(uint32_t gpio, bool& high) {
uint32_t v[2] = { gpio, 0 };
if (PropertyCall(TAG_GET_GPIO_STATE, v, 2) < 8 || v[0] != 0) return false;
high = v[1] != 0;
return true;
}
bool GetBoardMacAddress(uint8_t mac[6]) {
uint32_t v[2] = {};
if (PropertyCall(TAG_GET_BOARD_MAC, v, 2) < 6) return false;
const uint8_t* bytes = (const uint8_t*)v;
bool allZero = true;
for (int i = 0; i < 6; i++) {
mac[i] = bytes[i];
if (bytes[i] != 0) allZero = false;
}
return !allZero;
}
}
@@ -0,0 +1,47 @@
/*
* RpiFirmware.hpp
* Raspberry Pi VideoCore firmware mailbox (property interface)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
// The VideoCore firmware owns parts of the board the ARM cores cannot reach
// directly: the GPIO expander that powers the Wi-Fi/Bluetooth radio, the
// clock tree, the board's OTP (serial number, MAC address). The ARM side asks
// for them through the property channel of the VideoCore mailbox, which
// ACPI describes as BCM2849 (\_SB.GDV0.RPIQ) on the Pi Firmware Task Force
// UEFI firmware.
//
// All calls are synchronous, take a process-context lock, and must not be
// made from interrupt context. They return false when there is no mailbox
// (any machine but a Pi) or the firmware rejects the request.
namespace Drivers::Platform::RpiFirmware {
// Find the mailbox through ACPI and allocate its message buffer.
// Idempotent; the other calls initialize on first use.
bool Initialize();
bool IsPresent();
// Clock identifiers (the firmware's numbering).
constexpr uint32_t CLOCK_CORE = 4; // VPU core clock, feeds the mini UART
// Current rate of a firmware clock, in Hz.
bool GetClockRate(uint32_t clockId, uint32_t& hz);
// The firmware's GPIO expander lines are numbered from 128.
constexpr uint32_t EXPGPIO_BASE = 128;
constexpr uint32_t EXPGPIO_BT_ON = EXPGPIO_BASE + 0; // Bluetooth power (high = on)
constexpr uint32_t EXPGPIO_WL_ON = EXPGPIO_BASE + 1; // Wi-Fi power (high = on)
bool SetGpioState(uint32_t gpio, bool high);
bool GetGpioState(uint32_t gpio, bool& high);
// The board's Ethernet MAC address from OTP, in transmission order
// (mac[0] is the first octet of the OUI).
bool GetBoardMacAddress(uint8_t mac[6]);
}
+69
View File
@@ -0,0 +1,69 @@
/*
* RpiGpio.cpp
* BCM2711 GPIO pin function and pull control
* Copyright (c) 2026 Daniel Hammer
*/
#include "RpiGpio.hpp"
#include <ACPI/AcpiDevices.hpp>
#include <CppLib/Spinlock.hpp>
#include <CppLib/Stream.hpp>
#include <Memory/HHDM.hpp>
#include <Memory/Paging.hpp>
#include <Terminal/Terminal.hpp>
using namespace Kt;
namespace Drivers::Platform::RpiGpio {
// BCM2711 ARM Peripherals, chapter 5
static constexpr uint32_t REG_GPFSEL0 = 0x00; // 10 pins per register, 3 bits each
static constexpr uint32_t REG_PUP_PDN_CNTRL0 = 0xE4; // 16 pins per register, 2 bits each
static constexpr uint32_t PinCount = 58;
static volatile uint8_t* g_mmio = nullptr;
static bool g_probed = false;
static kcp::Spinlock g_lock;
static bool Initialize() {
if (g_probed) return g_mmio != nullptr;
g_probed = true;
Hal::AcpiDevices::DeviceInfo info{};
if (!Hal::AcpiDevices::FindDeviceByHid("BCM2845", info)) return false;
Hal::AML::ResourceList resources;
uint64_t base = 0, length = 0;
if (!Hal::AcpiDevices::EvaluateCrs(info.NodeIndex, resources) ||
!resources.GetMemory(base, length) || base == 0) {
return false;
}
uint64_t page = base & ~0xFFFULL;
Memory::VMM::g_paging->MapMMIO(page, Memory::HHDM(page));
g_mmio = (volatile uint8_t*)Memory::HHDM(base);
return true;
}
static void Modify(uint32_t offset, uint32_t mask, uint32_t value) {
g_lock.Acquire();
volatile uint32_t* reg = (volatile uint32_t*)(g_mmio + offset);
*reg = (*reg & ~mask) | (value & mask);
g_lock.Release();
}
bool SetFunction(uint32_t pin, Function function) {
if (pin >= PinCount || !Initialize()) return false;
uint32_t shift = (pin % 10) * 3;
Modify(REG_GPFSEL0 + (pin / 10) * 4, 7u << shift, (uint32_t)function << shift);
return true;
}
bool SetPull(uint32_t pin, Pull pull) {
if (pin >= PinCount || !Initialize()) return false;
uint32_t shift = (pin % 16) * 2;
Modify(REG_PUP_PDN_CNTRL0 + (pin / 16) * 4, 3u << shift, (uint32_t)pull << shift);
return true;
}
}
+37
View File
@@ -0,0 +1,37 @@
/*
* RpiGpio.hpp
* BCM2711 GPIO pin function and pull control
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
// The SoC's own GPIO block (ACPI BCM2845, \_SB.GDV0.GPI0), used to route
// pins to the peripheral that drives them. On Linux the device tree's
// pinctrl does this; booted through ACPI, nothing does it for us. Only the
// BCM2711 (Raspberry Pi 4/400) pull-control layout is implemented.
namespace Drivers::Platform::RpiGpio {
enum class Function : uint8_t {
Input = 0b000,
Output = 0b001,
Alt0 = 0b100,
Alt1 = 0b101,
Alt2 = 0b110,
Alt3 = 0b111,
Alt4 = 0b011,
Alt5 = 0b010,
};
enum class Pull : uint8_t {
None = 0b00,
Up = 0b01,
Down = 0b10,
};
bool SetFunction(uint32_t pin, Function function);
bool SetPull(uint32_t pin, Pull pull);
}
+466
View File
@@ -0,0 +1,466 @@
/*
* BtUart.cpp
* Interrupt-driven UART to an on-board Bluetooth controller
* Copyright (c) 2026 Daniel Hammer
*/
#include "BtUart.hpp"
#include <ACPI/GsiRouting.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/Interrupts.hpp>
#include <CppLib/Spinlock.hpp>
#include <CppLib/Stream.hpp>
#include <Memory/HHDM.hpp>
#include <Memory/Paging.hpp>
#include <Sched/Scheduler.hpp>
#include <Terminal/Terminal.hpp>
#include <Timekeeping/Timer.hpp>
#include <atomic>
using namespace Kt;
namespace Drivers::Serial::BtUart {
// =========================================================================
// Registers
// =========================================================================
// Mini UART, offsets from the AUX block (BCM2711 ARM Peripherals 2.2,
// with the elinux.org BCM2835 datasheet errata: IER bit 0 enables the
// receive interrupt and bit 1 the transmit one, IER bits 3:2 must be set
// for interrupts to be raised, LCR needs 0b11 for 8 data bits).
static constexpr uint32_t AUX_ENABLES = 0x04;
static constexpr uint32_t MU_IO = 0x40;
static constexpr uint32_t MU_IER = 0x44;
static constexpr uint32_t MU_IIR = 0x48;
static constexpr uint32_t MU_LCR = 0x4C;
static constexpr uint32_t MU_MCR = 0x50;
static constexpr uint32_t MU_LSR = 0x54;
static constexpr uint32_t MU_CNTL = 0x60;
static constexpr uint32_t MU_BAUD = 0x68;
static constexpr uint32_t AUX_ENABLE_MU = 1u << 0;
static constexpr uint32_t MU_IER_RX = 1u << 0;
static constexpr uint32_t MU_IER_TX = 1u << 1;
static constexpr uint32_t MU_IER_REQUIRED = 3u << 2;
static constexpr uint32_t MU_IIR_FLUSH = 0xC6; // clear both FIFOs, FIFOs on
static constexpr uint32_t MU_LCR_8BIT = 0x03;
static constexpr uint32_t MU_MCR_RTS = 1u << 1; // RTS pin low (asserted)
static constexpr uint32_t MU_LSR_DATA_READY = 1u << 0;
static constexpr uint32_t MU_LSR_OVERRUN = 1u << 1;
static constexpr uint32_t MU_LSR_TX_SPACE = 1u << 5; // FIFO accepts a byte
static constexpr uint32_t MU_LSR_TX_IDLE = 1u << 6; // FIFO empty, shifter idle
static constexpr uint32_t MU_CNTL_RXEN = 1u << 0;
static constexpr uint32_t MU_CNTL_TXEN = 1u << 1;
// Auto RTS at the default level (deassert with 3 FIFO spaces left) and
// the default, active-low polarity (CNTL bit 6 would invert it).
static constexpr uint32_t MU_CNTL_AUTORTS = 1u << 2;
// PL011 (ARM PrimeCell UART TRM)
static constexpr uint32_t PL_DR = 0x00;
static constexpr uint32_t PL_FR = 0x18;
static constexpr uint32_t PL_IBRD = 0x24;
static constexpr uint32_t PL_FBRD = 0x28;
static constexpr uint32_t PL_LCRH = 0x2C;
static constexpr uint32_t PL_CR = 0x30;
static constexpr uint32_t PL_IFLS = 0x34;
static constexpr uint32_t PL_IMSC = 0x38;
static constexpr uint32_t PL_ICR = 0x44;
static constexpr uint32_t PL_DR_OE = 1u << 11;
static constexpr uint32_t PL_FR_BUSY = 1u << 3;
static constexpr uint32_t PL_FR_RXFE = 1u << 4;
static constexpr uint32_t PL_FR_TXFF = 1u << 5;
static constexpr uint32_t PL_FR_TXFE = 1u << 7;
static constexpr uint32_t PL_LCRH_FEN = 1u << 4;
static constexpr uint32_t PL_LCRH_8BIT = 3u << 5;
static constexpr uint32_t PL_CR_UARTEN = 1u << 0;
static constexpr uint32_t PL_CR_TXE = 1u << 8;
static constexpr uint32_t PL_CR_RXE = 1u << 9;
static constexpr uint32_t PL_CR_RTS = 1u << 11;
static constexpr uint32_t PL_CR_RTSEN = 1u << 14;
static constexpr uint32_t PL_CR_CTSEN = 1u << 15;
static constexpr uint32_t PL_INT_RX = 1u << 4;
static constexpr uint32_t PL_INT_TX = 1u << 5;
static constexpr uint32_t PL_INT_RT = 1u << 6; // receive timeout
static constexpr uint32_t PL_INT_OE = 1u << 10;
static constexpr uint32_t PL_IFLS_RX_HALF = 2u << 3;
static constexpr uint32_t PL_IFLS_TX_EIGHTH = 0;
// =========================================================================
// State
// =========================================================================
static constexpr uint32_t RX_RING_SIZE = 16384; // powers of two
static constexpr uint32_t TX_RING_SIZE = 16384;
static Kind g_kind = Kind::MiniUart;
static volatile uint8_t* g_mmio = nullptr;
static uint32_t g_clockHz = 0;
static uint32_t g_baud = 0;
static bool g_flowControl = false;
static bool g_interrupt = false;
static bool g_initialized = false;
// Guards the hardware and the transmit ring: taken by the IRQ handler,
// Service() and every producer, with interrupts off.
static kcp::Spinlock g_hwLock;
// Receive ring: produced under g_hwLock, consumed by Read() (one reader).
static uint8_t g_rxRing[RX_RING_SIZE];
static std::atomic<uint32_t> g_rxHead{0};
static std::atomic<uint32_t> g_rxTail{0};
// Transmit ring: entirely under g_hwLock. g_txCount mirrors the fill for
// lock-free readers.
static uint8_t g_txRing[TX_RING_SIZE];
static uint32_t g_txHead = 0;
static uint32_t g_txTail = 0;
static std::atomic<uint32_t> g_txCount{0};
static bool g_txIrqEnabled = false;
static Stats g_stats = {};
static inline uint32_t Read32(uint32_t offset) {
return *(volatile uint32_t*)(g_mmio + offset);
}
static inline void Write32(uint32_t offset, uint32_t value) {
*(volatile uint32_t*)(g_mmio + offset) = value;
}
// =========================================================================
// FIFO service (caller holds g_hwLock)
// =========================================================================
static inline void PushRx(uint8_t byte) {
uint32_t head = g_rxHead.load(std::memory_order_relaxed);
if (head - g_rxTail.load(std::memory_order_acquire) >= RX_RING_SIZE) {
g_stats.rxDropped++;
return;
}
g_rxRing[head & (RX_RING_SIZE - 1)] = byte;
g_rxHead.store(head + 1, std::memory_order_release);
g_stats.rxBytes++;
}
static bool DrainRxLocked() {
bool any = false;
if (g_kind == Kind::MiniUart) {
for (;;) {
uint32_t lsr = Read32(MU_LSR);
if (lsr & MU_LSR_OVERRUN) g_stats.overruns++;
if (!(lsr & MU_LSR_DATA_READY)) break;
PushRx((uint8_t)Read32(MU_IO));
any = true;
}
} else {
while (!(Read32(PL_FR) & PL_FR_RXFE)) {
uint32_t data = Read32(PL_DR);
if (data & PL_DR_OE) g_stats.overruns++;
PushRx((uint8_t)data);
any = true;
}
Write32(PL_ICR, PL_INT_RX | PL_INT_RT | PL_INT_OE);
}
return any;
}
static void SetTxIrqLocked(bool on) {
if (on == g_txIrqEnabled || !g_interrupt) return;
g_txIrqEnabled = on;
if (g_kind == Kind::MiniUart) {
Write32(MU_IER, MU_IER_RX | MU_IER_REQUIRED | (on ? MU_IER_TX : 0));
} else {
Write32(PL_IMSC, PL_INT_RX | PL_INT_RT | PL_INT_OE | (on ? PL_INT_TX : 0));
}
}
// Move queued bytes into the transmit FIFO. The transmit interrupt stays
// enabled exactly while bytes remain queued: the PL011's only fires on
// crossing its FIFO threshold, so the FIFO must be refilled here first.
static void FillTxLocked() {
while (g_txHead != g_txTail) {
bool space = (g_kind == Kind::MiniUart)
? (Read32(MU_LSR) & MU_LSR_TX_SPACE) != 0
: (Read32(PL_FR) & PL_FR_TXFF) == 0;
if (!space) break;
uint8_t byte = g_txRing[g_txTail & (TX_RING_SIZE - 1)];
g_txTail++;
Write32(g_kind == Kind::MiniUart ? MU_IO : PL_DR, byte);
g_stats.txBytes++;
}
g_txCount.store(g_txHead - g_txTail, std::memory_order_release);
if (g_kind == Kind::Pl011) Write32(PL_ICR, PL_INT_TX);
SetTxIrqLocked(g_txHead != g_txTail);
}
static bool TransmitterIdleLocked() {
if (g_txHead != g_txTail) return false;
if (g_kind == Kind::MiniUart) return (Read32(MU_LSR) & MU_LSR_TX_IDLE) != 0;
uint32_t fr = Read32(PL_FR);
return (fr & PL_FR_TXFE) && !(fr & PL_FR_BUSY);
}
static void HandleInterrupt(uint8_t, bool fromUser) {
g_hwLock.Acquire();
g_stats.interrupts++;
bool received = DrainRxLocked();
FillTxLocked();
g_hwLock.Release();
// Received bytes are parsed in process context. When this interrupt
// landed in user mode, ask for an idle-context pass at once (as the
// timer does for USB) instead of waiting for the core to go idle.
if (received && fromUser) Sched::RequestKernelWork();
}
// =========================================================================
// Baud rate (caller holds g_hwLock, transmitter idle)
// =========================================================================
static bool WithinTolerance(uint64_t actual, uint32_t wanted) {
uint64_t diff = actual > wanted ? actual - wanted : wanted - actual;
return diff * 40 <= wanted; // 2.5%
}
// The divisor for `baud`: mini UART, baud = clock / (8 * divisor) (the
// register holds divisor - 1); PL011, clock / (16 * baud) in 16.6 fixed
// point (IBRD = divisor >> 6, FBRD = divisor & 63).
static bool ComputeDivisor(uint32_t baud, uint64_t& divisor) {
if (baud == 0 || g_clockHz == 0) return false;
if (g_kind == Kind::MiniUart) {
divisor = ((uint64_t)g_clockHz + 4ull * baud) / (8ull * baud);
if (divisor < 1 || divisor > 0x10000) return false;
return WithinTolerance((uint64_t)g_clockHz / (8 * divisor), baud);
}
divisor = ((uint64_t)g_clockHz * 8 / baud + 1) / 2;
if ((divisor >> 6) < 1 || (divisor >> 6) > 0xFFFF) return false;
return WithinTolerance((uint64_t)g_clockHz * 4 / divisor, baud);
}
static bool ProgramBaudLocked(uint32_t baud) {
uint64_t divisor = 0;
if (!ComputeDivisor(baud, divisor)) return false;
if (g_kind == Kind::MiniUart) {
uint32_t cntl = Read32(MU_CNTL);
Write32(MU_CNTL, 0);
Write32(MU_BAUD, (uint32_t)(divisor - 1));
Write32(MU_CNTL, cntl);
} else {
uint32_t ibrd = (uint32_t)(divisor >> 6);
uint32_t fbrd = (uint32_t)(divisor & 63);
uint32_t cr = Read32(PL_CR);
Write32(PL_CR, 0);
for (int i = 0; i < 100000 && (Read32(PL_FR) & PL_FR_BUSY); i++) Arch::Pause();
Write32(PL_LCRH, 0); // flush the FIFOs
Write32(PL_IBRD, ibrd);
Write32(PL_FBRD, fbrd);
Write32(PL_LCRH, PL_LCRH_8BIT | PL_LCRH_FEN); // latches IBRD/FBRD
Write32(PL_CR, cr);
}
g_baud = baud;
return true;
}
// =========================================================================
// Public API
// =========================================================================
bool Initialize(const Config& config) {
if (g_initialized) return true;
if (config.mmioPhys == 0 || config.clockHz == 0) return false;
g_kind = config.kind;
g_clockHz = config.clockHz;
// The mini UART lives 0x40 into the AUX block; accept either base.
uint64_t base = config.mmioPhys;
if (g_kind == Kind::MiniUart && (base & 0xFF) == MU_IO) base -= MU_IO;
uint64_t page = base & ~0xFFFULL;
Memory::VMM::g_paging->MapMMIO(page, Memory::HHDM(page));
g_mmio = (volatile uint8_t*)Memory::HHDM(base);
g_hwLock.Acquire();
if (g_kind == Kind::MiniUart) {
// AUX_ENABLES also gates SPI1/SPI2; leave their bits alone.
Write32(AUX_ENABLES, Read32(AUX_ENABLES) | AUX_ENABLE_MU);
Write32(MU_CNTL, 0);
Write32(MU_IER, 0);
Write32(MU_LCR, MU_LCR_8BIT);
Write32(MU_MCR, MU_MCR_RTS);
Write32(MU_IIR, MU_IIR_FLUSH);
Write32(MU_CNTL, MU_CNTL_RXEN | MU_CNTL_TXEN);
} else {
Write32(PL_CR, 0);
Write32(PL_IMSC, 0);
Write32(PL_ICR, 0x7FF);
Write32(PL_IFLS, PL_IFLS_RX_HALF | PL_IFLS_TX_EIGHTH);
Write32(PL_LCRH, PL_LCRH_8BIT | PL_LCRH_FEN);
Write32(PL_CR, PL_CR_UARTEN | PL_CR_TXE | PL_CR_RXE | PL_CR_RTS);
}
bool baudOk = ProgramBaudLocked(115200);
while (DrainRxLocked()) {}
g_rxTail.store(g_rxHead.load(std::memory_order_relaxed), std::memory_order_release);
g_hwLock.Release();
if (!baudOk) {
KernelLogStream(ERROR, "BtUart") << "115200 baud unreachable from a "
<< (uint64_t)g_clockHz << " Hz clock";
return false;
}
if (config.gsi != 0) {
uint8_t line = Hal::GsiRouting::LineForGsi(config.gsi, config.levelTriggered,
config.activeLow);
if (line != 0) {
Arch::RegisterIrqHandler(line, HandleInterrupt);
g_interrupt = true;
g_hwLock.Acquire();
if (g_kind == Kind::MiniUart) {
Write32(MU_IER, MU_IER_RX | MU_IER_REQUIRED);
} else {
Write32(PL_IMSC, PL_INT_RX | PL_INT_RT | PL_INT_OE);
}
g_hwLock.Release();
Arch::EnableLegacyIrq(line);
}
}
g_initialized = true;
KernelLogStream(OK, "BtUart") << (g_kind == Kind::MiniUart ? "Mini UART" : "PL011")
<< " at 0x" << base::hex << base << base::dec << ", " << (uint64_t)g_clockHz
<< " Hz clock, " << (g_interrupt ? "interrupt driven" : "polled (no interrupt)");
return true;
}
bool IsInitialized() { return g_initialized; }
bool HasInterrupt() { return g_interrupt; }
uint32_t GetBaud() { return g_baud; }
Kind GetKind() { return g_kind; }
bool FlowControl() { return g_flowControl; }
bool BaudReachable(uint32_t baud) {
uint64_t divisor = 0;
return g_initialized && ComputeDivisor(baud, divisor);
}
bool SetBaud(uint32_t baud) {
if (!g_initialized) return false;
WaitTxIdle(1000);
g_hwLock.Acquire();
bool ok = ProgramBaudLocked(baud);
g_hwLock.Release();
return ok;
}
void SetFlowControl(bool on) {
if (!g_initialized) return;
g_hwLock.Acquire();
if (g_kind == Kind::MiniUart) {
uint32_t cntl = MU_CNTL_RXEN | MU_CNTL_TXEN | (on ? MU_CNTL_AUTORTS : 0);
Write32(MU_CNTL, cntl);
// With auto RTS off, hold RTS asserted by hand.
Write32(MU_MCR, MU_MCR_RTS);
} else {
uint32_t cr = Read32(PL_CR) & ~(PL_CR_RTSEN | PL_CR_CTSEN);
if (on) cr |= PL_CR_RTSEN | PL_CR_CTSEN;
Write32(PL_CR, cr | PL_CR_RTS);
}
g_flowControl = on;
g_hwLock.Release();
}
int Read(uint8_t* buffer, int maxLength) {
if (buffer == nullptr || maxLength <= 0) return 0;
uint32_t tail = g_rxTail.load(std::memory_order_relaxed);
uint32_t head = g_rxHead.load(std::memory_order_acquire);
uint32_t available = head - tail;
uint32_t n = available < (uint32_t)maxLength ? available : (uint32_t)maxLength;
for (uint32_t i = 0; i < n; i++) {
buffer[i] = g_rxRing[(tail + i) & (RX_RING_SIZE - 1)];
}
g_rxTail.store(tail + n, std::memory_order_release);
return (int)n;
}
bool HasRxData() {
if (!g_initialized) return false;
if (g_rxHead.load(std::memory_order_acquire) != g_rxTail.load(std::memory_order_relaxed))
return true;
// Polled: nothing fills the ring until Service() runs, so look at
// the FIFO itself.
if (g_interrupt) return false;
return g_kind == Kind::MiniUart ? (Read32(MU_LSR) & MU_LSR_DATA_READY) != 0
: (Read32(PL_FR) & PL_FR_RXFE) == 0;
}
void FlushRx() {
if (!g_initialized) return;
g_hwLock.Acquire();
while (DrainRxLocked()) {}
g_rxTail.store(g_rxHead.load(std::memory_order_relaxed), std::memory_order_release);
g_hwLock.Release();
}
bool Write(const uint8_t* head, uint32_t headLength,
const uint8_t* body, uint32_t bodyLength) {
if (body == nullptr) bodyLength = 0;
uint32_t length = headLength + bodyLength;
if (!g_initialized || length == 0) return length == 0;
if (length > TX_RING_SIZE) return false;
g_hwLock.Acquire();
if (TX_RING_SIZE - (g_txHead - g_txTail) < length) {
g_hwLock.Release();
return false;
}
for (uint32_t i = 0; i < headLength; i++) {
g_txRing[(g_txHead + i) & (TX_RING_SIZE - 1)] = head[i];
}
for (uint32_t i = 0; i < bodyLength; i++) {
g_txRing[(g_txHead + headLength + i) & (TX_RING_SIZE - 1)] = body[i];
}
g_txHead += length;
FillTxLocked();
g_hwLock.Release();
return true;
}
uint32_t TxFree() {
return TX_RING_SIZE - g_txCount.load(std::memory_order_acquire);
}
uint32_t TxPending() {
return g_txCount.load(std::memory_order_acquire);
}
bool WaitTxIdle(uint32_t timeoutMs) {
if (!g_initialized) return false;
uint64_t start = Timekeeping::GetMilliseconds();
for (;;) {
g_hwLock.Acquire();
FillTxLocked();
bool idle = TransmitterIdleLocked();
g_hwLock.Release();
if (idle) return true;
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
Arch::Pause();
}
}
void Service() {
if (!g_initialized) return;
g_hwLock.Acquire();
DrainRxLocked();
FillTxLocked();
g_hwLock.Release();
}
void GetStats(Stats& out) {
g_hwLock.Acquire();
out = g_stats;
g_hwLock.Release();
}
}
+91
View File
@@ -0,0 +1,91 @@
/*
* BtUart.hpp
* Interrupt-driven UART to an on-board Bluetooth controller
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
// A byte pipe to a peripheral wired to one of the Broadcom SoC's UARTs --
// on the Raspberry Pi 4/400 the Bluetooth half of the CYW4345x radio.
// Unlike the console (Uart.cpp: polled, transmit only) this one receives,
// so it is interrupt driven: the hard IRQ moves bytes between the FIFOs and
// two software rings, and everything else runs in process context.
//
// Two UARTs can carry it:
// - the mini UART (BCM2835 AUX, 8-byte FIFOs, baud from the VPU core
// clock), which is where Bluetooth sits with the miniuart-bt overlay
// the Pi firmware task force's UEFI ships;
// - the PL011 (32-byte FIFOs, fixed 48 MHz reference clock).
// Pin routing (GPIO 30-33) is the caller's job.
namespace Drivers::Serial::BtUart {
enum class Kind : uint8_t {
MiniUart,
Pl011,
};
struct Config {
Kind kind;
uint64_t mmioPhys; // the block ACPI describes (mini UART: the AUX block)
uint32_t clockHz; // UART reference clock
uint32_t gsi; // 0 = no interrupt, poll from Service()
bool levelTriggered;
bool activeLow;
};
// Program the UART to 115200 8N1 with RTS asserted and start receiving.
bool Initialize(const Config& config);
bool IsInitialized();
// Whether the receive path is interrupt driven. Without it, bytes only
// move when Service() runs, so a caller should keep the baud rate low.
bool HasInterrupt();
// Change the baud rate. Waits for queued output to drain first, since
// the peer is about to switch too. Returns false if the rate is
// unreachable from the reference clock (more than 2.5% off).
bool SetBaud(uint32_t baud);
bool BaudReachable(uint32_t baud);
uint32_t GetBaud();
Kind GetKind();
// Hardware RTS: deassert RTS while the receive FIFO is nearly full so
// the peer pauses instead of overrunning it. On the PL011 this also
// enables CTS (the peer's RTS gating our transmitter).
void SetFlowControl(bool on);
bool FlowControl();
// Receive ring (single consumer). Read returns the number of bytes copied.
int Read(uint8_t* buffer, int maxLength);
bool HasRxData();
void FlushRx();
// Queue bytes for transmission: all of them, or none (false) when the
// transmit ring lacks room. Safe from several producers at once; each
// call's bytes (head, then body) stay contiguous on the wire.
bool Write(const uint8_t* head, uint32_t headLength,
const uint8_t* body = nullptr, uint32_t bodyLength = 0);
uint32_t TxFree();
uint32_t TxPending();
// Wait until everything queued has left the transmitter.
bool WaitTxIdle(uint32_t timeoutMs);
// Move bytes between the FIFOs and the rings from process context: the
// whole receive path when there is no interrupt, and a safety net when
// there is one.
void Service();
struct Stats {
uint64_t rxBytes;
uint64_t txBytes;
uint64_t rxDropped; // receive ring full
uint64_t overruns; // receive FIFO overflowed in hardware
uint64_t interrupts;
};
void GetStats(Stats& out);
}
+6
View File
@@ -64,6 +64,7 @@ namespace Drivers::Serial {
static volatile uint8_t* g_mmio = nullptr;
static uint16_t g_port = 0;
static uint32_t g_stride = 1;
static uint64_t g_consoleAddress = 0;
static inline uint32_t Read32(uint64_t offset) {
return *(volatile uint32_t*)(g_mmio + offset);
@@ -173,9 +174,14 @@ namespace Drivers::Serial {
return;
}
g_consoleAddress = reg.Address;
AttachSerialConsole(sink);
KernelLogStream(OK, "Serial") << "Console on " << name << " at " << (isIo ? "port " : "")
<< "0x" << base::hex << reg.Address;
}
uint64_t ConsoleAddress() {
return g_consoleAddress;
}
}
+4
View File
@@ -15,4 +15,8 @@ namespace Drivers::Serial {
// tables (the registers are mapped uncached through the HHDM).
void InitializeConsole(Hal::ACPI::CommonSDTHeader* xsdt);
// Physical address of the console UART's registers, or 0 when there is
// none (so another driver does not take over the console's UART).
uint64_t ConsoleAddress();
}
+1 -1
View File
@@ -11,7 +11,7 @@
#include "HidMouse.hpp"
#include "MassStorage.hpp"
#include "Hub.hpp"
#include "Bluetooth/Bluetooth.hpp"
#include <Drivers/Bluetooth/Bluetooth.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Memory/HHDM.hpp>
+9 -5
View File
@@ -19,7 +19,8 @@
#include <Drivers/Net/Wifi/Wifi.hpp>
#include <Drivers/Audio/IntelHda.hpp>
#include <Drivers/USB/Xhci.hpp>
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
#include <Drivers/Bluetooth/Bluetooth.hpp>
#include <Drivers/Serial/BtUart.hpp>
#include <Drivers/USB/HidKeyboard.hpp>
using namespace Kt;
@@ -58,7 +59,10 @@ namespace Timekeeping {
// desktop may never enter idle. Force one bounded idle-context
// pass for USB immediately, and for network RX at most every
// 10 ms (important for E1000E's polling fallback).
bool usbWork = Drivers::USB::Xhci::HasDeferredWork();
// UART Bluetooth (Raspberry Pi) has no xHCI behind it: its
// received bytes wait in the UART ring for the same pass.
bool usbWork = Drivers::USB::Xhci::HasDeferredWork() ||
Drivers::Serial::BtUart::HasRxData();
bool audioWork = Drivers::Audio::IntelHda::HasDeferredWork();
bool netWork = Drivers::Net::E1000::HasDeferredWork() ||
Drivers::Net::E1000E::HasDeferredWork();
@@ -185,9 +189,9 @@ namespace Timekeeping {
// Cheap no-op unless an adapter is waiting; self-claiming, and safe
// to preempt (the scheduler saves/resumes the idle context).
bool bluetoothWork = usbWork ||
Drivers::USB::Bluetooth::HasDeferredWork();
Drivers::Bluetooth::HasDeferredWork();
if (bluetoothWork)
Drivers::USB::Bluetooth::ServiceDeferredInit();
Drivers::Bluetooth::ServiceDeferredInit();
// Service Bluetooth inbound traffic (the headset's SDP/AVRCP queries,
// AVDTP commands, ACL flow-control credits) whenever a core idles.
@@ -197,7 +201,7 @@ namespace Timekeeping {
// queries never answered). Self-serializing and a cheap no-op when
// the adapter is down.
if (bluetoothWork)
Drivers::USB::Bluetooth::ServiceEvents();
Drivers::Bluetooth::ServiceEvents();
// Wi-Fi mirrors the Bluetooth split: the firmware load needs the
// ramdisk, and the RX/notification ring must be drained outside hard