diff --git a/docs/raspberry-pi.md b/docs/raspberry-pi.md index b4c1087..f9e99bd 100644 --- a/docs/raspberry-pi.md +++ b/docs/raspberry-pi.md @@ -95,6 +95,9 @@ PL011 on GPIO 14/15: header pins 8 (TX) and 10 (RX), ground on pin 6, 115200 | Keyboard (built in), USB ports | VL805 xHCI as ACPI `\_SB.SCB0.XHC0` (`PNP0D10`) | `Drivers/USB/Xhci.cpp` platform probe | | USB 2 devices (built-in keyboard too) | VIA hub on the VL805's USB 2 root port | `Drivers/USB/Hub.cpp` | | Reset, power-off | PSCI | `Arch::Reboot`, `Arch::PowerOff` | +| Bluetooth (CYW43455 on the 4B, CYW43456 on the 400) | `BCM2EA6` (`\_SB.GDV0.BTH0`) on the mini UART (`BCM2836`) | `Drivers/Bluetooth/Broadcom.cpp`, `Drivers/Serial/BtUart.cpp` | +| VideoCore firmware mailbox (radio power, clocks, board MAC) | `BCM2849` (`RPIQ`) | `Drivers/Platform/RpiFirmware.cpp` | +| GPIO pin functions | `BCM2845` (`GPI0`) | `Drivers/Platform/RpiGpio.cpp` | The VL805 sits behind the BCM2711's PCIe, whose config space is not ECAM (the root bridge's `_DSD` says `linux-ecam-quirk-id = "bcm2711"`), so the @@ -114,18 +117,56 @@ socket, and the Pi 400's internal keyboard (`04d9:0007`), hang off it: a keyboard or mouse is always a low/full-speed device behind a high-speed hub, reached through that hub's transaction translator. +## Bluetooth + +The radio's 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, reached through the mailbox. The firmware tables +describe it as `BTH0`, whose `UARTSerialBus` names the UART it uses: with +the `miniuart-bt` overlay the PL011 is the console, so Bluetooth has the mini +UART. The kernel refuses to take a UART that is the SPCR console. + +At boot the driver probe routes the pins (`ALT5` for the mini UART, `ALT3` +for the PL011), starts the UART (interrupt driven, 16 KiB receive and +transmit rings) and the H4 transport under the shared HCI layer. Once the +root is mounted the idle loop finishes, as for USB adapters: + +1. power-cycle the controller through `BT_ON`, HCI Reset at 115200 baud; +2. Read Local Version: the ROM's LMP subversion picks the patch file, + `0:/os/firmware/brcm/BCM4345C0.hcd` (0x6119, 4B) or `BCM4345C5.hcd` + (0x6606, 400), shipped unmodified from Raspberry Pi OS's bluez-firmware; +3. raise both ends to the operating speed (vendor `0xFC18`); +4. Download Minidriver (`0xFC2E`), then every record of the `.hcd` file + (Write RAM, then Launch RAM), as Linux's `btbcm` does; +5. the patched firmware restarts at 115200: reset, raise the speed again. + +The mini UART's FIFO is 8 bytes deep. Its hardware RTS is tried first; if +the controller answers with it on, the link runs at 921600 baud, otherwise +at 460800 without flow control (what Raspberry Pi OS uses), and at 115200 +if the UART has no interrupt. The baud rate derives from the VPU core +clock, so `mk-rpi4-sd.sh` pins it (`core_freq=500`, `core_freq_min=500` in +`config.txt`), as the overlay's documentation requires. A failed fast +bring-up is retried entirely at 115200. + +The CYW43456 reports a chip-wide default address (`43:45:C5:00:1F:AC`); +the driver replaces it with the board's Ethernet MAC from the firmware. +`0:/config/bluetooth.toml` still overrides both (Broadcom `0xFC01`). + ## Not supported yet - Ethernet (BCM54213 via GENET v5, `BCM6E4E`). - SD high-speed mode: the driver transfers by polled PIO at 25 MHz on a 4-bit bus (multi-block CMD18/CMD25 with Auto CMD12), at most 12.5 MB/s; switching the card to 50 MHz (CMD6) would double that. -- Wi-Fi and Bluetooth (CYW43455 over SDIO/UART). +- Wi-Fi (the CYW4345x's SDIO half, on the Arasan controller). - Audio (HDMI via VideoCore, the 3.5 mm jack via PWM). ## Status -Everything above is verified on QEMU only, including the Pi-specific path: +Bluetooth has not run yet: QEMU has no model of the radio, so it is +untested on any machine. + +Everything else above is verified on QEMU only, including the Pi-specific path: the firmware's own XHC0 table, retargeted at QEMU's xHCI and loaded with the `bcm2711` quirk on QEMU's root bridge, boots to the desktop with non-coherent DMA. Real hardware has not been tried yet. The ext2 root on the SD card boots on QEMU (`run-rpi4-sd`); the diff --git a/kernel/src/Api/Audio.hpp b/kernel/src/Api/Audio.hpp index 179b8bd..a40c2c7 100644 --- a/kernel/src/Api/Audio.hpp +++ b/kernel/src/Api/Audio.hpp @@ -11,8 +11,8 @@ #include #include #include -#include -#include +#include +#include #include #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; } diff --git a/kernel/src/Api/BluetoothSyscall.hpp b/kernel/src/Api/BluetoothSyscall.hpp index 5bd2403..ffcec3a 100644 --- a/kernel/src/Api/BluetoothSyscall.hpp +++ b/kernel/src/Api/BluetoothSyscall.hpp @@ -6,8 +6,8 @@ #pragma once #include -#include -#include +#include +#include #include #include #include @@ -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"; diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index e0a924c..555b528 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 73 +#define MONTAUK_BUILD_NUMBER 80 diff --git a/kernel/src/Api/Device.hpp b/kernel/src/Api/Device.hpp index 0697000..8d51c98 100644 --- a/kernel/src/Api/Device.hpp +++ b/kernel/src/Api/Device.hpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include #include #include @@ -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)"); } diff --git a/kernel/src/Drivers/Audio/Mixer.cpp b/kernel/src/Drivers/Audio/Mixer.cpp index 1222c47..6479712 100644 --- a/kernel/src/Drivers/Audio/Mixer.cpp +++ b/kernel/src/Drivers/Audio/Mixer.cpp @@ -7,9 +7,9 @@ #include "Mixer.hpp" #include "IntelHda.hpp" -#include -#include -#include +#include +#include +#include #include #include #include @@ -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); } diff --git a/kernel/src/Drivers/USB/Bluetooth/A2dp.cpp b/kernel/src/Drivers/Bluetooth/A2dp.cpp similarity index 99% rename from kernel/src/Drivers/USB/Bluetooth/A2dp.cpp rename to kernel/src/Drivers/Bluetooth/A2dp.cpp index e773236..6a114e9 100644 --- a/kernel/src/Drivers/USB/Bluetooth/A2dp.cpp +++ b/kernel/src/Drivers/Bluetooth/A2dp.cpp @@ -9,7 +9,6 @@ #include "Sbc.hpp" #include "L2cap.hpp" #include "Hci.hpp" -#include #include #include #include @@ -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); diff --git a/kernel/src/Drivers/USB/Bluetooth/A2dp.hpp b/kernel/src/Drivers/Bluetooth/A2dp.hpp similarity index 99% rename from kernel/src/Drivers/USB/Bluetooth/A2dp.hpp rename to kernel/src/Drivers/Bluetooth/A2dp.hpp index d153dff..d4866b9 100644 --- a/kernel/src/Drivers/USB/Bluetooth/A2dp.hpp +++ b/kernel/src/Drivers/Bluetooth/A2dp.hpp @@ -8,7 +8,7 @@ #pragma once #include -namespace Drivers::USB::Bluetooth::A2dp { +namespace Drivers::Bluetooth::A2dp { // ========================================================================= // A2DP stream states diff --git a/kernel/src/Drivers/USB/Bluetooth/Avrcp.cpp b/kernel/src/Drivers/Bluetooth/Avrcp.cpp similarity index 99% rename from kernel/src/Drivers/USB/Bluetooth/Avrcp.cpp rename to kernel/src/Drivers/Bluetooth/Avrcp.cpp index 6e54d0f..4718342 100644 --- a/kernel/src/Drivers/USB/Bluetooth/Avrcp.cpp +++ b/kernel/src/Drivers/Bluetooth/Avrcp.cpp @@ -15,7 +15,7 @@ using namespace Kt; -namespace Drivers::USB::Bluetooth::Avrcp { +namespace Drivers::Bluetooth::Avrcp { // ========================================================================= // Constants diff --git a/kernel/src/Drivers/USB/Bluetooth/Avrcp.hpp b/kernel/src/Drivers/Bluetooth/Avrcp.hpp similarity index 95% rename from kernel/src/Drivers/USB/Bluetooth/Avrcp.hpp rename to kernel/src/Drivers/Bluetooth/Avrcp.hpp index cb3e621..4c41b27 100644 --- a/kernel/src/Drivers/USB/Bluetooth/Avrcp.hpp +++ b/kernel/src/Drivers/Bluetooth/Avrcp.hpp @@ -7,7 +7,7 @@ #pragma once #include -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 diff --git a/kernel/src/Drivers/USB/Bluetooth/Bluetooth.cpp b/kernel/src/Drivers/Bluetooth/Bluetooth.cpp similarity index 92% rename from kernel/src/Drivers/USB/Bluetooth/Bluetooth.cpp rename to kernel/src/Drivers/Bluetooth/Bluetooth.cpp index 4917cde..dcf7aac 100644 --- a/kernel/src/Drivers/USB/Bluetooth/Bluetooth.cpp +++ b/kernel/src/Drivers/Bluetooth/Bluetooth.cpp @@ -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 #include #include @@ -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; diff --git a/kernel/src/Drivers/USB/Bluetooth/Bluetooth.hpp b/kernel/src/Drivers/Bluetooth/Bluetooth.hpp similarity index 74% rename from kernel/src/Drivers/USB/Bluetooth/Bluetooth.hpp rename to kernel/src/Drivers/Bluetooth/Bluetooth.hpp index 86e9601..0aa1273 100644 --- a/kernel/src/Drivers/USB/Bluetooth/Bluetooth.hpp +++ b/kernel/src/Drivers/Bluetooth/Bluetooth.hpp @@ -8,28 +8,35 @@ #include #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 diff --git a/kernel/src/Drivers/Bluetooth/Broadcom.cpp b/kernel/src/Drivers/Bluetooth/Broadcom.cpp new file mode 100644 index 0000000..0f779d2 --- /dev/null +++ b/kernel/src/Drivers/Bluetooth/Broadcom.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 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/.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"; + } + } + +} diff --git a/kernel/src/Drivers/Bluetooth/Broadcom.hpp b/kernel/src/Drivers/Bluetooth/Broadcom.hpp new file mode 100644 index 0000000..a3e591d --- /dev/null +++ b/kernel/src/Drivers/Bluetooth/Broadcom.hpp @@ -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(); + +} diff --git a/kernel/src/Drivers/USB/Bluetooth/Hci.cpp b/kernel/src/Drivers/Bluetooth/Hci.cpp similarity index 90% rename from kernel/src/Drivers/USB/Bluetooth/Hci.cpp rename to kernel/src/Drivers/Bluetooth/Hci.cpp index b4ae308..437c2ed 100644 --- a/kernel/src/Drivers/USB/Bluetooth/Hci.cpp +++ b/kernel/src/Drivers/Bluetooth/Hci.cpp @@ -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 #include +#include #include #include #include @@ -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 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) { diff --git a/kernel/src/Drivers/USB/Bluetooth/Hci.hpp b/kernel/src/Drivers/Bluetooth/Hci.hpp similarity index 86% rename from kernel/src/Drivers/USB/Bluetooth/Hci.hpp rename to kernel/src/Drivers/Bluetooth/Hci.hpp index a774d22..09c7974 100644 --- a/kernel/src/Drivers/USB/Bluetooth/Hci.hpp +++ b/kernel/src/Drivers/Bluetooth/Hci.hpp @@ -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 -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 diff --git a/kernel/src/Drivers/USB/Bluetooth/IntelFirmware.cpp b/kernel/src/Drivers/Bluetooth/IntelFirmware.cpp similarity index 99% rename from kernel/src/Drivers/USB/Bluetooth/IntelFirmware.cpp rename to kernel/src/Drivers/Bluetooth/IntelFirmware.cpp index 4c9e076..fbbd9e4 100644 --- a/kernel/src/Drivers/USB/Bluetooth/IntelFirmware.cpp +++ b/kernel/src/Drivers/Bluetooth/IntelFirmware.cpp @@ -23,7 +23,7 @@ using namespace Kt; -namespace Drivers::USB::Bluetooth { +namespace Drivers::Bluetooth { // ========================================================================= // .sfi / TLV format constants (from drivers/bluetooth/btintel.{c,h}) diff --git a/kernel/src/Drivers/USB/Bluetooth/IntelFirmware.hpp b/kernel/src/Drivers/Bluetooth/IntelFirmware.hpp similarity index 96% rename from kernel/src/Drivers/USB/Bluetooth/IntelFirmware.hpp rename to kernel/src/Drivers/Bluetooth/IntelFirmware.hpp index 39e0c87..d323197 100644 --- a/kernel/src/Drivers/USB/Bluetooth/IntelFirmware.hpp +++ b/kernel/src/Drivers/Bluetooth/IntelFirmware.hpp @@ -7,7 +7,7 @@ #pragma once #include -namespace Drivers::USB::Bluetooth { +namespace Drivers::Bluetooth { // Drive the Intel bootloader firmware download sequence for the currently // registered adapter: diff --git a/kernel/src/Drivers/USB/Bluetooth/L2cap.cpp b/kernel/src/Drivers/Bluetooth/L2cap.cpp similarity index 99% rename from kernel/src/Drivers/USB/Bluetooth/L2cap.cpp rename to kernel/src/Drivers/Bluetooth/L2cap.cpp index 348af65..1fde48c 100644 --- a/kernel/src/Drivers/USB/Bluetooth/L2cap.cpp +++ b/kernel/src/Drivers/Bluetooth/L2cap.cpp @@ -9,7 +9,6 @@ #include "Hci.hpp" #include "A2dp.hpp" #include "Avrcp.hpp" -#include #include #include #include @@ -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); diff --git a/kernel/src/Drivers/USB/Bluetooth/L2cap.hpp b/kernel/src/Drivers/Bluetooth/L2cap.hpp similarity index 99% rename from kernel/src/Drivers/USB/Bluetooth/L2cap.hpp rename to kernel/src/Drivers/Bluetooth/L2cap.hpp index 4448dfe..d03c57d 100644 --- a/kernel/src/Drivers/USB/Bluetooth/L2cap.hpp +++ b/kernel/src/Drivers/Bluetooth/L2cap.hpp @@ -7,7 +7,7 @@ #pragma once #include -namespace Drivers::USB::Bluetooth::L2cap { +namespace Drivers::Bluetooth::L2cap { // ========================================================================= // L2CAP CIDs (Channel Identifiers) diff --git a/kernel/src/Drivers/USB/Bluetooth/Sbc.cpp b/kernel/src/Drivers/Bluetooth/Sbc.cpp similarity index 99% rename from kernel/src/Drivers/USB/Bluetooth/Sbc.cpp rename to kernel/src/Drivers/Bluetooth/Sbc.cpp index da12b5e..039b422 100644 --- a/kernel/src/Drivers/USB/Bluetooth/Sbc.cpp +++ b/kernel/src/Drivers/Bluetooth/Sbc.cpp @@ -9,7 +9,7 @@ #include "Sbc.hpp" #include -namespace Drivers::USB::Bluetooth::Sbc { +namespace Drivers::Bluetooth::Sbc { // ========================================================================= // Analysis filterbank tables diff --git a/kernel/src/Drivers/USB/Bluetooth/Sbc.hpp b/kernel/src/Drivers/Bluetooth/Sbc.hpp similarity index 98% rename from kernel/src/Drivers/USB/Bluetooth/Sbc.hpp rename to kernel/src/Drivers/Bluetooth/Sbc.hpp index 84ae6a1..ca7ff8f 100644 --- a/kernel/src/Drivers/USB/Bluetooth/Sbc.hpp +++ b/kernel/src/Drivers/Bluetooth/Sbc.hpp @@ -8,7 +8,7 @@ #pragma once #include -namespace Drivers::USB::Bluetooth::Sbc { +namespace Drivers::Bluetooth::Sbc { // ========================================================================= // SBC configuration diff --git a/kernel/src/Drivers/Init.cpp b/kernel/src/Drivers/Init.cpp index a9a6b8d..561de27 100644 --- a/kernel/src/Drivers/Init.cpp +++ b/kernel/src/Drivers/Init.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -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(). } } diff --git a/kernel/src/Drivers/Platform/RpiFirmware.cpp b/kernel/src/Drivers/Platform/RpiFirmware.cpp new file mode 100644 index 0000000..2cdca99 --- /dev/null +++ b/kernel/src/Drivers/Platform/RpiFirmware.cpp @@ -0,0 +1,250 @@ +/* + * RpiFirmware.cpp + * Raspberry Pi VideoCore firmware mailbox (property interface) + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "RpiFirmware.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 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; + } + +} diff --git a/kernel/src/Drivers/Platform/RpiFirmware.hpp b/kernel/src/Drivers/Platform/RpiFirmware.hpp new file mode 100644 index 0000000..e88fa49 --- /dev/null +++ b/kernel/src/Drivers/Platform/RpiFirmware.hpp @@ -0,0 +1,47 @@ +/* + * RpiFirmware.hpp + * Raspberry Pi VideoCore firmware mailbox (property interface) + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +// 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]); + +} diff --git a/kernel/src/Drivers/Platform/RpiGpio.cpp b/kernel/src/Drivers/Platform/RpiGpio.cpp new file mode 100644 index 0000000..fad0a21 --- /dev/null +++ b/kernel/src/Drivers/Platform/RpiGpio.cpp @@ -0,0 +1,69 @@ +/* + * RpiGpio.cpp + * BCM2711 GPIO pin function and pull control + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "RpiGpio.hpp" +#include +#include +#include +#include +#include +#include + +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; + } + +} diff --git a/kernel/src/Drivers/Platform/RpiGpio.hpp b/kernel/src/Drivers/Platform/RpiGpio.hpp new file mode 100644 index 0000000..2f69e9c --- /dev/null +++ b/kernel/src/Drivers/Platform/RpiGpio.hpp @@ -0,0 +1,37 @@ +/* + * RpiGpio.hpp + * BCM2711 GPIO pin function and pull control + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +// 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); + +} diff --git a/kernel/src/Drivers/Serial/BtUart.cpp b/kernel/src/Drivers/Serial/BtUart.cpp new file mode 100644 index 0000000..6eba5a0 --- /dev/null +++ b/kernel/src/Drivers/Serial/BtUart.cpp @@ -0,0 +1,466 @@ +/* + * BtUart.cpp + * Interrupt-driven UART to an on-board Bluetooth controller + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "BtUart.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 g_rxHead{0}; + static std::atomic 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 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(); + } + +} diff --git a/kernel/src/Drivers/Serial/BtUart.hpp b/kernel/src/Drivers/Serial/BtUart.hpp new file mode 100644 index 0000000..2830c07 --- /dev/null +++ b/kernel/src/Drivers/Serial/BtUart.hpp @@ -0,0 +1,91 @@ +/* + * BtUart.hpp + * Interrupt-driven UART to an on-board Bluetooth controller + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +// 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); + +} diff --git a/kernel/src/Drivers/Serial/Uart.cpp b/kernel/src/Drivers/Serial/Uart.cpp index a80a342..f71600f 100644 --- a/kernel/src/Drivers/Serial/Uart.cpp +++ b/kernel/src/Drivers/Serial/Uart.cpp @@ -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; + } + } diff --git a/kernel/src/Drivers/Serial/Uart.hpp b/kernel/src/Drivers/Serial/Uart.hpp index 45c70f6..7be7006 100644 --- a/kernel/src/Drivers/Serial/Uart.hpp +++ b/kernel/src/Drivers/Serial/Uart.hpp @@ -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(); + } diff --git a/kernel/src/Drivers/USB/UsbDevice.cpp b/kernel/src/Drivers/USB/UsbDevice.cpp index cf6022b..3cf5291 100644 --- a/kernel/src/Drivers/USB/UsbDevice.cpp +++ b/kernel/src/Drivers/USB/UsbDevice.cpp @@ -11,7 +11,7 @@ #include "HidMouse.hpp" #include "MassStorage.hpp" #include "Hub.hpp" -#include "Bluetooth/Bluetooth.hpp" +#include #include #include #include diff --git a/kernel/src/Timekeeping/Timer.cpp b/kernel/src/Timekeeping/Timer.cpp index f7fc208..7e248aa 100644 --- a/kernel/src/Timekeeping/Timer.cpp +++ b/kernel/src/Timekeeping/Timer.cpp @@ -19,7 +19,8 @@ #include #include #include -#include +#include +#include #include 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 diff --git a/programs/data/firmware/brcm/BCM4345C0.hcd b/programs/data/firmware/brcm/BCM4345C0.hcd new file mode 100644 index 0000000..007611e Binary files /dev/null and b/programs/data/firmware/brcm/BCM4345C0.hcd differ diff --git a/programs/data/firmware/brcm/BCM4345C5.hcd b/programs/data/firmware/brcm/BCM4345C5.hcd new file mode 100644 index 0000000..762359d Binary files /dev/null and b/programs/data/firmware/brcm/BCM4345C5.hcd differ diff --git a/programs/data/licenses/Cypress-bt-firmware.txt b/programs/data/licenses/Cypress-bt-firmware.txt new file mode 100644 index 0000000..b5f5ec2 --- /dev/null +++ b/programs/data/licenses/Cypress-bt-firmware.txt @@ -0,0 +1,139 @@ +### CYPRESS WIRELESS CONNECTIVITY DEVICES +### DRIVER END USER LICENSE AGREEMENT (SOURCE AND BINARY DISTRIBUTION) + +PLEASE READ THIS END USER LICENSE AGREEMENT ("Agreement") CAREFULLY BEFORE +DOWNLOADING, INSTALLING, OR USING THIS SOFTWARE, ANY ACCOMPANYING +DOCUMENTATION, OR ANY UPDATES PROVIDED BY CYPRESS ("Software"). BY +DOWNLOADING, INSTALLING, OR USING THE SOFTWARE, YOU ARE AGREEING TO BE BOUND +BY THIS AGREEMENT. IF YOU DO NOT AGREE TO ALL OF THE TERMS OF THIS +AGREEMENT, PROMPTLY RETURN AND DO NOT USE THE SOFTWARE. IF YOU HAVE +PURCHASED THE SOFTWARE, YOUR RIGHT TO RETURN THE SOFTWARE EXPIRES 30 DAYS +AFTER YOUR PURCHASE AND APPLIES ONLY TO THE ORIGINAL PURCHASER. + +Software Provided in Binary Code Form. This paragraph applies to any Software +provided in binary code form. Subject to the terms and conditions of this +Agreement, Cypress Semiconductor Corporation ("Cypress") grants you a +non-exclusive, non-transferable license under its copyright rights in the +Software to reproduce and distribute the Software in object code form only, +solely for use in connection with Cypress integrated circuit products +("Purpose"). + +Software Provided in Source Code Form. This paragraph applies to any Software +provided in source code form ("Cypress Source Code"). Subject to the terms and +conditions of this Agreement, Cypress grants you a non-exclusive, +non-transferable license under its copyright rights in the Cypress Source Code +to reproduce, modify, compile, and distribute the Cypress Source Code (whether +in source code form or as compiled into binary code form) solely for the +Purpose. Cypress retains ownership of the Cypress Source Code and any compiled +version thereof. Subject to Cypress' ownership of the underlying Cypress +Source Code, you retain ownership of any modifications you make to the +Cypress Source Code. You agree not to remove any Cypress copyright or other +notices from the Cypress Source Code and any modifications thereof. Any +reproduction, modification, translation, compilation, or representation of +the Cypress Source Code except as permitted in this paragraph is prohibited +without the express written permission of Cypress. + +Free and Open Source Software. Portions of the Software may be licensed under +free and/or open source licenses such as the GNU General Public License +("FOSS"). FOSS is subject to the applicable license agreement and not this +Agreement. If you are entitled to receive the source code from Cypress for any +FOSS included with the Software, either the source code will be included with +the Software or you may obtain the source code at no charge from +. The applicable license terms will +accompany each source code package. To review the license terms applicable to +any FOSS for which Cypress is not required to provide you with source code, +please see the Software's installation directory on your computer. + +Proprietary Rights. The Software, including all intellectual property rights +therein, is and will remain the sole and exclusive property of Cypress or its +suppliers. Except as otherwise expressly provided in this Agreement, you may +not: (i) modify, adapt, or create derivative works based upon the Software; +(ii) copy the Software; (iii) except and only to the extent explicitly +permitted by applicable law despite this limitation, decompile, translate, +reverse engineer, disassemble or otherwise reduce the Software to +human-readable form; or (iv) use the Software other than for the Purpose. + +No Support. Cypress may, but is not required to, provide technical support for +the Software. + +Term and Termination. This Agreement is effective until terminated. This +Agreement and Your license rights will terminate immediately without notice +from Cypress if you fail to comply with any provision of this Agreement. Upon +termination, you must destroy all copies of Software in your possession or +control. Termination of this Agreement will not affect any licenses validly +granted as of the termination date to any end users of the Software. The +following paragraphs shall survive any termination of this Agreement: "Free and +Open Source Software," "Proprietary Rights," "Compliance With Law," +"Disclaimer," "Limitation of Liability," and "General." + +Compliance With Law. Each party agrees to comply with all applicable laws, +rules and regulations in connection with its activities under this Agreement. +Without limiting the foregoing, the Software may be subject to export control +laws and regulations of the United States and other countries. You agree to +comply strictly with all such laws and regulations and acknowledge that you +have the responsibility to obtain licenses to export, re-export, or import +the Software. + +Disclaimer. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, CYPRESS MAKES +NO WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, WITH REGARD TO THE SOFTWARE, +INCLUDING, BUT NOT LIMITED TO, INFRINGEMENT AND THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. Cypress reserves the +right to make changes to the Software without notice. Cypress does not assume +any liability arising out of the application or use of Software or any +product or circuit described in the Software. Cypress does not authorize its +products for use as critical components in life-support systems where a +malfunction or failure may reasonably be expected to result in significant +injury to the user. The inclusion of Cypress' product in a life-support +system or application implies that the manufacturer of such system or +application assumes all risk of such use and in doing so indemnifies Cypress +against all charges. + +Limitation of Liability. IN NO EVENT WILL CYPRESS OR ITS SUPPLIERS, +RESELLERS, OR DISTRIBUTORS BE LIABLE FOR ANY LOST REVENUE, PROFIT, OR DATA, +OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL, INCIDENTAL, OR PUNITIVE DAMAGES +HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE +USE OF OR INABILITY TO USE THE SOFTWARE EVEN IF CYPRESS OR ITS SUPPLIERS, +RESELLERS, OR DISTRIBUTORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. IN NO EVENT SHALL CYPRESS' OR ITS SUPPLIERS' RESELLERS', OR +DISTRIBUTORS' TOTAL LIABILITY TO YOU, WHETHER IN CONTRACT, TORT (INCLUDING +NEGLIGENCE), OR OTHERWISE, EXCEED THE PRICE PAID BY YOU FOR THE SOFTWARE. +THE FOREGOING LIMITATIONS SHALL APPLY EVEN IF THE ABOVE-STATED WARRANTY FAILS +OF ITS ESSENTIAL PURPOSE. BECAUSE SOME STATES OR JURISDICTIONS DO NOT ALLOW +LIMITATION OR EXCLUSION OF CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE +LIMITATION MAY NOT APPLY TO YOU. + +Restricted Rights. The Software under this Agreement is commercial computer +software as that term is described in 48 C.F.R. 252.227-7014(a)(1). If +acquired by or on behalf of a civilian agency, the U.S. Government acquires +this commercial computer software and/or commercial computer software +documentation subject to the terms of this Agreement as specified in 48 +C.F.R. 12.212 (Computer Software) and 12.211 (Technical Data) of the Federal +Acquisition Regulations ("FAR") and its successors. If acquired by or on +behalf of any agency within the Department of Defense ("DOD"), the U.S. +Government acquires this commercial computer software and/or commercial +computer software documentation subject to the terms of this Agreement as +specified in 48 C.F.R. 227.7202-3 of the DOD FAR Supplement ("DFAR") and its +successors. + +General. This Agreement will bind and inure to the benefit of each party's +successors and assigns, provided that you may not assign or transfer this +Agreement, in whole or in part, without Cypress' written consent. This +Agreement shall be governed by and construed in accordance with the laws of +the State of California, United States of America, as if performed wholly +within the state and without giving effect to the principles of conflict of +law. The parties consent to personal and exclusive jurisdiction of and venue +in, the state and federal courts within Santa Clara County, California; +provided however, that nothing in this Agreement will limit Cypress' right to +bring legal action in any venue in order to protect or enforce its +intellectual property rights. No failure of either party to exercise or +enforce any of its rights under this Agreement will act as a waiver of such +rights. If any portion hereof is found to be void or unenforceable, the +remaining provisions of this Agreement shall remain in full force and +effect. This Agreement is the complete and exclusive agreement between the +parties with respect to the subject matter hereof, superseding and replacing +any and all prior agreements, communications, and understandings (both +written and oral) regarding such subject matter. Any notice to Cypress will +be deemed effective when actually received and must be sent to Cypress +Semiconductor Corporation, ATTN: Chief Legal Officer, 198 Champion Court, San +Jose, CA 95134 USA. + diff --git a/programs/data/licenses/NOTICES.txt b/programs/data/licenses/NOTICES.txt index 85354b2..191d2ae 100644 --- a/programs/data/licenses/NOTICES.txt +++ b/programs/data/licenses/NOTICES.txt @@ -655,6 +655,26 @@ License: zlib License Jean-loup Gailly Mark Adler jloup@gzip.org madler@alumni.caltech.edu + +================================================================================ +23. Cypress (Broadcom) Bluetooth firmware +================================================================================ +Shipped as binary firmware patch files (0:/os/firmware/brcm/BCM4345C0.hcd and +BCM4345C5.hcd), loaded at runtime into the CYW43455 / CYW43456 Bluetooth +controller of the Raspberry Pi 4 and 400 by the MontaukOS Bluetooth driver. +The files are taken unmodified from Raspberry Pi OS's bluez-firmware package. + + Copyright (c) Cypress Semiconductor Corporation (an Infineon company) + Upstream: https://github.com/RPi-Distro/bluez-firmware + (debian/firmware/broadcom/) + +License: Cypress Wireless Connectivity Devices Driver End User License + Agreement (source and binary distribution) + + Reproduction and distribution in object code form are permitted solely for + use with Cypress integrated circuit products. The full license text is + included at 0:/os/licenses/Cypress-bt-firmware.txt. + ================================================================================ For licensing inquiries regarding MontaukOS itself, contact: licensing@montaukos.org ================================================================================ diff --git a/scripts/mk-rpi4-sd.sh b/scripts/mk-rpi4-sd.sh index eeccae3..6d4a2f3 100755 --- a/scripts/mk-rpi4-sd.sh +++ b/scripts/mk-rpi4-sd.sh @@ -165,10 +165,18 @@ IMG="$WORK/esp.fat" dd if=/dev/zero of="$IMG" bs=1M count=0 seek=$esp_mib status=none mformat -i "$IMG" -F -v MONTAUK :: -# Firmware: everything in the release except its readme. +# Firmware: everything in the release except its readme. Its config.txt +# gets the VPU core clock pinned: the miniuart-bt overlay leaves Bluetooth on +# the mini UART, whose baud rate is derived from that clock, and the +# overlay's documentation requires core_freq and core_freq_min to match or +# the mini UART breaks when the firmware scales the clock (500 MHz is the +# Pi 4/400 default). for f in "$FW_DIR"/*; do case "$(basename "$f")" in Readme.md) ;; + config.txt) + { cat "$f"; printf '\ncore_freq=500\ncore_freq_min=500\n'; } > "$WORK/config.txt" + mcopy -i "$IMG" "$WORK/config.txt" :: ;; *) mcopy -s -i "$IMG" "$f" :: ;; esac done