/* * 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. 460800 is // too slow for A2DP here: 8N1 carries 46 KB/s and SBC at bitpool 53 needs // ~46 KB/s with its headers, so music broke up whenever anything else used // the link. The mini UART's auto RTS/CTS allows 921600, so it is tried // first; without flow control (460800) is the fallback, and A2DP lowers its // bitpool to fit (Hci::TransportBytesPerSecond). // // Our RTS must be deasserted while the controller powers on: with it // asserted the controller boots into autobaud mode (see PowerCycle). 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; const char* clockSource = "firmware"; if (mini) { if (!RpiFirmware::GetClockRate(RpiFirmware::CLOCK_CORE, clockHz)) { if (ReadInteger(uart.NodeIndex, "MUCR", tableClock) && tableClock > 0x1000000) { clockHz = (uint32_t)tableClock; clockSource = "ACPI MUCR"; } else { clockHz = 500000000; clockSource = "assumed"; } } } else { clockSource = "PL011 reference"; 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 << ", clock " << clockSource << ")"; return true; } // ========================================================================= // Controller bring-up // ========================================================================= // BT_ON through the firmware's expander. The line has to be an output // first (hci_bcm's GPIOD_OUT_LOW request does that on Linux); plain // SET_GPIO_STATE is the fallback for firmware without the config tag. static bool SetBtOn(bool on) { return RpiFirmware::SetGpioOutput(RpiFirmware::EXPGPIO_BT_ON, on) || RpiFirmware::SetGpioState(RpiFirmware::EXPGPIO_BT_ON, on); } // The controller samples its CTS (our RTS) as it leaves reset. Asserted, // it boots into autobaud mode, where it answers HCI Reset and silently // drops nearly everything else (on a Pi 400: Read Local Version, Read // PIN Type, Write PIN Type, at any baud rate). hci_bcm deasserts it // around power-on for the same reason; SyncAtInitialBaud asserts it // again once the controller is up. static void PowerCycle() { if (g_powerSwitch) { BtUart::DeassertRts(); if (SetBtOn(false)) { Timekeeping::BusyWaitMs(20); bool on = SetBtOn(true); bool state = false; bool readBack = RpiFirmware::GetGpioState(RpiFirmware::EXPGPIO_BT_ON, state); KernelLogStream(on ? INFO : WARNING, "BT-BCM") << "BT_ON " << (on ? "switched on" : "did not switch on") << " (reads back " << (readBack ? (state ? "high" : "low") : "unknown") << ")"; // 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(); } // Whether received bytes move by interrupt and whether any were lost: // the first thing to know when the controller seems not to answer. static void LogUartStats(const char* when) { BtUart::Stats stats{}; BtUart::GetStats(stats); KernelLogStream(INFO, "BT-BCM") << when << ": UART rx " << stats.rxBytes << " bytes, tx " << stats.txBytes << ", " << stats.interrupts << " interrupts, " << stats.overruns << " overruns, " << stats.rxDropped << " dropped, " << (uint64_t)BtUart::GetBaud() << " baud, " << (BtUart::FlowControl() ? "RTS flow control" : "no flow control"); } static bool ResetController(uint32_t timeoutMs) { if (!Hci::SendCommand(Hci::OP_RESET, nullptr, 0)) return false; if (!Hci::WaitCommandComplete(Hci::OP_RESET, nullptr, 0, timeoutMs)) return false; // The controller acknowledges the reset before it has finished it // and ignores commands until then; btbcm_reset waits 100 ms. Timekeeping::BusyWaitMs(100); return true; } // Reach the controller at its power-on 115200 baud and read its ROM // version. A flow-control mode counts only once a second command has // been answered too: a controller in autobaud mode answers HCI Reset // alone, and a flow-control polarity that holds either side back fails // here rather than later. static bool SyncAtInitialBaud(Hci::LocalVersion& rom) { BtUart::SetBaud(InitialBaud); static constexpr bool attempts[] = { true, true, false, false }; for (bool flow : attempts) { BtUart::SetFlowControl(flow); // Events the controller held back while it saw RTS deasserted // pour out once it is asserted: let them arrive and drop them, // so they neither overrun the FIFO under the next reply nor // leave the deframer mid-packet. Timekeeping::BusyWaitMs(50); Hci::ResetUartReceiver(); if (!ResetController(1000)) { LogUartStats(flow ? "No reset reply with flow control" : "No reset reply"); continue; } // A controller slow to finish its reset gets a second chance. bool answered = Hci::ReadLocalVersion(&rom); if (!answered) { Timekeeping::BusyWaitMs(200); answered = Hci::ReadLocalVersion(&rom); } if (answered) { g_flowControl = flow; return true; } LogUartStats(flow ? "Reset answered but not Read Local Version, with flow control" : "Reset answered but not Read Local Version"); } 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(); Hci::LocalVersion rom{}; if (!SyncAtInitialBaud(rom)) { KernelLogStream(ERROR, "BT-BCM") << "Controller does not answer HCI commands" << (g_powerSwitch ? "" : " (BT_ON was never switched on: it is likely unpowered)"); 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"; } } }