From ffee9002a6c6b3b7a01bcd64968c7e90a5c85e40 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Sun, 25 Oct 2026 15:59:33 +0100 Subject: [PATCH] feat: Raspberry Pi 4/400 Bluetooth over mini UART (H4, Broadcom patchram, firmware mailbox BT_ON); move BT stack to Drivers/Bluetooth --- docs/raspberry-pi.md | 45 +- kernel/src/Api/Audio.hpp | 12 +- kernel/src/Api/BluetoothSyscall.hpp | 44 +- kernel/src/Api/BuildNo.hpp | 2 +- kernel/src/Api/Device.hpp | 10 +- kernel/src/Drivers/Audio/Mixer.cpp | 40 +- .../src/Drivers/{USB => }/Bluetooth/A2dp.cpp | 25 +- .../src/Drivers/{USB => }/Bluetooth/A2dp.hpp | 2 +- .../src/Drivers/{USB => }/Bluetooth/Avrcp.cpp | 2 +- .../src/Drivers/{USB => }/Bluetooth/Avrcp.hpp | 2 +- .../Drivers/{USB => }/Bluetooth/Bluetooth.cpp | 73 ++- .../Drivers/{USB => }/Bluetooth/Bluetooth.hpp | 25 +- kernel/src/Drivers/Bluetooth/Broadcom.cpp | 493 ++++++++++++++++++ kernel/src/Drivers/Bluetooth/Broadcom.hpp | 27 + .../src/Drivers/{USB => }/Bluetooth/Hci.cpp | 274 +++++++++- .../src/Drivers/{USB => }/Bluetooth/Hci.hpp | 64 ++- .../{USB => }/Bluetooth/IntelFirmware.cpp | 2 +- .../{USB => }/Bluetooth/IntelFirmware.hpp | 2 +- .../src/Drivers/{USB => }/Bluetooth/L2cap.cpp | 5 +- .../src/Drivers/{USB => }/Bluetooth/L2cap.hpp | 2 +- .../src/Drivers/{USB => }/Bluetooth/Sbc.cpp | 2 +- .../src/Drivers/{USB => }/Bluetooth/Sbc.hpp | 2 +- kernel/src/Drivers/Init.cpp | 7 +- kernel/src/Drivers/Platform/RpiFirmware.cpp | 250 +++++++++ kernel/src/Drivers/Platform/RpiFirmware.hpp | 47 ++ kernel/src/Drivers/Platform/RpiGpio.cpp | 69 +++ kernel/src/Drivers/Platform/RpiGpio.hpp | 37 ++ kernel/src/Drivers/Serial/BtUart.cpp | 466 +++++++++++++++++ kernel/src/Drivers/Serial/BtUart.hpp | 91 ++++ kernel/src/Drivers/Serial/Uart.cpp | 6 + kernel/src/Drivers/Serial/Uart.hpp | 4 + kernel/src/Drivers/USB/UsbDevice.cpp | 2 +- kernel/src/Timekeeping/Timer.cpp | 14 +- programs/data/firmware/brcm/BCM4345C0.hcd | Bin 0 -> 63806 bytes programs/data/firmware/brcm/BCM4345C5.hcd | Bin 0 -> 55676 bytes .../data/licenses/Cypress-bt-firmware.txt | 139 +++++ programs/data/licenses/NOTICES.txt | 20 + scripts/mk-rpi4-sd.sh | 10 +- 38 files changed, 2176 insertions(+), 141 deletions(-) rename kernel/src/Drivers/{USB => }/Bluetooth/A2dp.cpp (99%) rename kernel/src/Drivers/{USB => }/Bluetooth/A2dp.hpp (99%) rename kernel/src/Drivers/{USB => }/Bluetooth/Avrcp.cpp (99%) rename kernel/src/Drivers/{USB => }/Bluetooth/Avrcp.hpp (95%) rename kernel/src/Drivers/{USB => }/Bluetooth/Bluetooth.cpp (92%) rename kernel/src/Drivers/{USB => }/Bluetooth/Bluetooth.hpp (74%) create mode 100644 kernel/src/Drivers/Bluetooth/Broadcom.cpp create mode 100644 kernel/src/Drivers/Bluetooth/Broadcom.hpp rename kernel/src/Drivers/{USB => }/Bluetooth/Hci.cpp (90%) rename kernel/src/Drivers/{USB => }/Bluetooth/Hci.hpp (86%) rename kernel/src/Drivers/{USB => }/Bluetooth/IntelFirmware.cpp (99%) rename kernel/src/Drivers/{USB => }/Bluetooth/IntelFirmware.hpp (96%) rename kernel/src/Drivers/{USB => }/Bluetooth/L2cap.cpp (99%) rename kernel/src/Drivers/{USB => }/Bluetooth/L2cap.hpp (99%) rename kernel/src/Drivers/{USB => }/Bluetooth/Sbc.cpp (99%) rename kernel/src/Drivers/{USB => }/Bluetooth/Sbc.hpp (98%) create mode 100644 kernel/src/Drivers/Platform/RpiFirmware.cpp create mode 100644 kernel/src/Drivers/Platform/RpiFirmware.hpp create mode 100644 kernel/src/Drivers/Platform/RpiGpio.cpp create mode 100644 kernel/src/Drivers/Platform/RpiGpio.hpp create mode 100644 kernel/src/Drivers/Serial/BtUart.cpp create mode 100644 kernel/src/Drivers/Serial/BtUart.hpp create mode 100644 programs/data/firmware/brcm/BCM4345C0.hcd create mode 100644 programs/data/firmware/brcm/BCM4345C5.hcd create mode 100644 programs/data/licenses/Cypress-bt-firmware.txt 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 0000000000000000000000000000000000000000..007611e37b40542c8358c70c4023aef8cdceac07 GIT binary patch literal 63806 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 Nr5dq{MX`*ZzW@a~9L4|u literal 0 HcmV?d00001 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