feat: add Broadcom FullMAC Wi-Fi driver

This commit is contained in:
daniel committed 2026-10-27 07:47:57 +01:00
1 parent 8b4a938164
commit 51a2b3a932
34 files changed
+4719 -95

No files matched your search

+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 109
#define MONTAUK_BUILD_NUMBER 123
+3
View File
@@ -36,6 +36,9 @@ namespace kcp {
Arch::Pause();
}
}
bool TryAcquire() {
return !flag.test_and_set(std::memory_order_acquire);
}
void Release() {
flag.clear(std::memory_order_release);
}
+32
View File
@@ -449,11 +449,43 @@ namespace Drivers {
Net::Genet::ProbePlatform(base, length, gsi, level, activeLow, coherent, phyMode, burst);
}
// The Raspberry Pi 4/400's Wi-Fi: the CYW4345x's SDIO half on the
// BCM2711's older Arasan controller (BCM2847, GPIO 34-39). Only while the
// card slot is on EMMC2 (BRCME88C): with the firmware's "SD on Arasan"
// option the Arasan runs the slot instead, and is not the radio's.
static void ProbePlatformBroadcomWifi() {
Hal::AcpiDevices::DeviceInfo emmc2{};
if (!Hal::AcpiDevices::FindDeviceByHid("BRCME88C", emmc2)) return;
Hal::AML::ResourceList resources;
uint64_t base = 0, length = 0;
Hal::AcpiDevices::DeviceInfo info{};
const char* name = "BCM2847";
if (Hal::AcpiDevices::FindDeviceByHid("BCM2847", info) &&
Hal::AcpiDevices::EvaluateCrs(info.NodeIndex, resources) &&
resources.GetMemory(base, length) && base != 0) {
name = info.Path;
} else {
// Not described: the Arasan sits 256 KiB below EMMC2 on the
// BCM2711 (0x7E300000 / 0x7E340000 bus addresses).
Hal::AML::ResourceList emmc2Resources;
uint64_t emmc2Base = 0;
if (!Hal::AcpiDevices::EvaluateCrs(emmc2.NodeIndex, emmc2Resources) ||
!emmc2Resources.GetMemory(emmc2Base, length) || emmc2Base < 0x40000) {
return;
}
base = emmc2Base - 0x40000;
name = "the Arasan controller (inferred from EMMC2)";
}
Net::Wifi::ProbeBroadcomSdio(base, name);
}
void ProbeNormal() {
Pci::ProbeAll(g_driverTable, g_driverTableCount, Pci::ProbePhase::Normal);
ProbePlatformXhci();
ProbePlatformSdhci();
ProbePlatformGenet();
ProbePlatformBroadcomWifi();
Bluetooth::ProbePlatformAdapter();
}
+218
View File
@@ -0,0 +1,218 @@
/*
* Brcmf.hpp
* Broadcom/Cypress FullMAC Wi-Fi over SDIO (CYW43455 / CYW43456 on the
* Raspberry Pi 4 / 400): driver-internal interface
* Derived in part from Linux's brcmfmac, Copyright (c) 2010-2014 Broadcom
* Corporation, ISC license (notice in programs/data/licenses/NOTICES.txt).
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <CppLib/Spinlock.hpp>
// A FullMAC radio runs the 802.11 MLME itself: the host asks for scans and
// joins through ioctls and sees Ethernet frames. Layout, after Linux's
// brcmfmac:
//
// BrcmfSdio.cpp the SD controller as an SDIO host (CMD52/CMD53)
// BrcmfChip.cpp backplane access, core enumeration (EROM), clocks,
// firmware + NVRAM download, F2 bring-up, firmware console
// BrcmfProto.cpp SDPCM framing, CDC ioctls, BDC data, the RX pump,
// firmware events
// BrcmfConnect.cpp preinit, CLM, escan, join, keys, the facade entry points
//
// The handshake runs on the host (Wpa.cpp), exactly as for the Intel
// adapter: EAPOL frames arrive as Ethernet frames and the keys go back to the
// firmware with "wsec_key".
//
// Threading: everything on the bus holds g_brcmf.BusLock for the length of one
// SDIO exchange. The RX pump (BrcmfPoll) has a reentrancy guard and releases
// the bus lock before dispatching, so the network stack may transmit from
// inside a delivered frame. Dispatch must never issue an ioctl -- its answer
// is read by the very pump it would be waiting inside. Work needing one is
// queued for BrcmfConnectService().
namespace Drivers::Net::Wifi {
// Facade-visible adapter state; values are the WIFI_STATE_* ABI ones.
enum class BrcmfState : uint8_t {
Absent = 0,
Detected,
Booting,
Running,
Error,
};
struct BrcmfCore {
uint16_t Id = 0;
uint8_t Rev = 0;
uint32_t Base = 0;
uint32_t Wrap = 0;
};
struct BrcmfDevice {
BrcmfState State = BrcmfState::Absent;
uint64_t ArasanMmio = 0;
// Chip
uint16_t ChipId = 0;
uint8_t ChipRev = 0;
const char* FwName = nullptr; // "43455" / "43456"
BrcmfCore Cores[16];
uint32_t CoreCount = 0;
uint32_t RamBase = 0;
uint32_t RamSize = 0;
uint32_t SdioCoreBase = 0;
uint32_t PmuBase = 0;
bool SrEnabled = false;
uint32_t SharedAddr = 0; // sdpcm_shared, 0 = not read yet
uint32_t ConsoleIdx = 0; // console bytes already printed
// Bus
kcp::Mutex BusLock;
uint32_t Window = 0xFFFFFFFF; // backplane window, 0xFFFFFFFF = unknown
uint32_t HostIntMask = 0;
// SDPCM
uint8_t TxSeq = 0;
uint8_t TxMax = 1;
bool RxPending = false;
// Firmware
uint8_t Mac[6] = {};
char FwVersion[32] = {};
uint16_t ChannelCount = 0;
bool Band24 = true;
bool Band5 = false;
// Health
uint32_t FwErrors = 0;
uint32_t CmdFailBucket = 0; // leaky bucket of unanswered ioctls
uint64_t RxPackets = 0;
uint64_t TxPackets = 0;
uint64_t LastPollMs = 0;
};
extern BrcmfDevice g_brcmf;
// =========================================================================
// BrcmfChip.cpp
// =========================================================================
bool BrcmfBpRead32(uint32_t addr, uint32_t& value);
bool BrcmfBpWrite32(uint32_t addr, uint32_t value);
bool BrcmfBpReadMem(uint32_t addr, uint8_t* buffer, uint32_t len);
bool BrcmfBpWriteMem(uint32_t addr, const uint8_t* buffer, uint32_t len);
// Point the backplane window at `addr` (function 2 transfers want it on
// ChipCommon, as Linux keeps it).
bool BrcmfBpSelectWindow(uint32_t addr);
// Function-1 SDIO-core registers in the 0x1000x space.
bool BrcmfF1Read8(uint32_t reg, uint8_t& value);
bool BrcmfF1Write8(uint32_t reg, uint8_t value);
// Power the radio, enumerate the SDIO card, identify the chip, download
// firmware + NVRAM, start the ARM and bring up function 2. Leaves
// g_brcmf ready for SDPCM traffic.
bool BrcmfChipBringUp();
// Hold the radio in reset (WL_ON low).
void BrcmfChipPowerOff();
// Print what the firmware wrote to its console since the last call, and
// a trap/assert summary if it has one. Takes the bus lock.
void BrcmfDumpConsole();
// Load 0:/os/firmware/brcm/brcmfmac<chip>-sdio.<ext>. The caller frees.
uint8_t* BrcmfLoadFile(const char* ext, uint32_t& size, bool required);
// =========================================================================
// BrcmfProto.cpp
// =========================================================================
void BrcmfProtoReset();
// Drain the device: interrupt status, mailbox, every pending frame, and
// queued transmits. Reentrancy guarded; returns at once if another core
// is already pumping.
void BrcmfPoll();
// Whether a poll is due (card interrupt, queued TX, the periodic sweep).
bool BrcmfPollDue(uint64_t now, uint64_t intervalMs);
// Synchronous firmware command. `buf` carries the request (for a query,
// its first bytes, e.g. an iovar name) and receives up to `len` bytes of
// answer. Returns false on a transport failure, a timeout, or a negative
// firmware status (*fwStatus, when given, receives it).
bool BrcmfIoctl(uint32_t cmd, bool set, void* buf, uint32_t len, int32_t* fwStatus = nullptr);
bool BrcmfSetInt(uint32_t cmd, uint32_t value);
bool BrcmfGetInt(uint32_t cmd, uint32_t& value);
bool BrcmfIovarSet(const char* name, const void* data, uint32_t len, bool quiet = false);
bool BrcmfIovarGet(const char* name, void* out, uint32_t len, bool quiet = false);
bool BrcmfIovarSetInt(const char* name, uint32_t value, bool quiet = false);
bool BrcmfIovarGetInt(const char* name, uint32_t& value, bool quiet = false);
// Queue an Ethernet frame for transmission (802.3, 14-byte header).
bool BrcmfTxEthernet(const uint8_t* frame, uint32_t len);
bool BrcmfTxPending();
// =========================================================================
// BrcmfConnect.cpp -- called from the protocol layer
// =========================================================================
// A firmware event. Runs inside the pump: no ioctls.
void BrcmfOnEvent(uint32_t type, uint32_t status, uint32_t reason, uint16_t flags,
const uint8_t* addr, const uint8_t* data, uint32_t len);
// A received Ethernet frame. Runs inside the pump.
void BrcmfOnRxEthernet(const uint8_t* frame, uint32_t len);
// The firmware halted or stopped answering.
void BrcmfOnFirmwareFailure(const char* why);
// =========================================================================
// BrcmfConnect.cpp -- the facade's backend (Wifi.cpp)
// =========================================================================
bool BrcmfProbe(uint64_t arasanMmio);
bool BrcmfCompleteInit();
void BrcmfShutdown(); // for recovery: power the radio off
BrcmfState BrcmfGetState();
bool BrcmfHasWork(uint64_t now);
void BrcmfService(); // pump + connection service
bool BrcmfStartScan();
void BrcmfAbortScan();
bool BrcmfScanActive();
bool BrcmfConnectStart(const uint8_t* bssid, uint8_t channel, const char* ssid,
const char* password, const uint8_t* rsnIe, uint8_t rsnIeLen);
void BrcmfConnectAbort();
int BrcmfConnectState(); // IwxConnStateId values
bool BrcmfConnectRefusedForSecurity();
bool BrcmfJoinFailedOnAuth();
const char* BrcmfConnectSsid();
const uint8_t* BrcmfConnectBssid();
uint8_t BrcmfConnectChannel();
bool BrcmfLinkUp();
bool BrcmfSendEthernet(const uint8_t* frame, uint32_t len);
bool BrcmfWpaTxEapol(const uint8_t* body, uint32_t len);
bool BrcmfWpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher);
bool BrcmfWpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
uint8_t cipher, const uint8_t* rsc);
// =========================================================================
// Wifi.cpp -- the facade, called by the backend
// =========================================================================
// One BSS from a scan: its information elements, as in a beacon body
// after the fixed fields.
void WifiRxBssInfo(const uint8_t* bssid, uint16_t beaconInterval, uint16_t capability,
const uint8_t* ies, uint32_t iesLen, uint8_t channel, int8_t rssiDbm);
void WifiRxEthernet(const uint8_t* frame, uint32_t len);
}
+849
View File
@@ -0,0 +1,849 @@
/*
* BrcmfChip.cpp
* Broadcom FullMAC over SDIO: backplane access, core enumeration, clocks,
* firmware and NVRAM download, function 2 bring-up, firmware console
* Derived in part from Linux's brcmfmac, Copyright (c) 2010-2014 Broadcom
* Corporation, ISC license (notice in programs/data/licenses/NOTICES.txt).
* Copyright (c) 2026 Daniel Hammer
*/
#include "Brcmf.hpp"
#include "BrcmfSdio.hpp"
#include <Arch/Cpu.hpp>
#include <CppLib/Stream.hpp>
#include <Drivers/Platform/RpiFirmware.hpp>
#include <Drivers/Platform/RpiGpio.hpp>
#include <Fs/Vfs.hpp>
#include <Libraries/Memory.hpp>
#include <Memory/Heap.hpp>
#include <Terminal/Terminal.hpp>
#include <Timekeeping/Timer.hpp>
// The radio is a small SoC of its own (a Broadcom "AI" interconnect): an
// ARM Cortex-R4 with its tightly coupled RAM, the 802.11 MAC/PHY core (D11),
// ChipCommon (with the PMU) and an SDIO device core. Function 1 of the SDIO
// card is a window onto that backplane: three window registers select the
// 32 KiB page, and CMD53 at offset | 0x8000 reaches it.
//
// Bring-up follows Linux's brcmfmac (sdio.c, chip.c, firmware.c):
// 1. route GPIO 34-39 to the Arasan controller, pulse WL_ON (line 1 of the
// VideoCore's GPIO expander), enumerate the card;
// 2. request the ALP clock, read the chip ID, walk the enumeration ROM for
// the cores;
// 3. halt the ARM and hold the D11 core in reset, size the RAM from the
// CR4's bank registers;
// 4. write the firmware at the RAM base and the NVRAM at its top (with the
// length token in the last word), write the reset vector to address 0
// and release the ARM;
// 5. wait for the HT clock the firmware brings up, tell the SDIO core the
// protocol version, enable function 2 (frames) and its interrupts.
using namespace Kt;
namespace Drivers::Net::Wifi {
namespace RpiFirmware = Drivers::Platform::RpiFirmware;
namespace RpiGpio = Drivers::Platform::RpiGpio;
BrcmfDevice g_brcmf;
// =========================================================================
// Registers
// =========================================================================
// Function 1, SDIO core local space
static constexpr uint32_t F1_WATERMARK = 0x10008;
static constexpr uint32_t F1_DEVICE_CTL = 0x10009;
static constexpr uint32_t F1_SBADDRLOW = 0x1000A;
static constexpr uint32_t F1_SBADDRMID = 0x1000B;
static constexpr uint32_t F1_SBADDRHIGH = 0x1000C;
static constexpr uint32_t F1_CHIPCLKCSR = 0x1000E;
static constexpr uint32_t F1_SDIOPULLUP = 0x1000F;
static constexpr uint32_t F1_MESBUSYCTRL = 0x1001D;
static constexpr uint32_t F1_WAKEUPCTRL = 0x1001E;
static constexpr uint8_t CLK_FORCE_ALP = 0x01;
static constexpr uint8_t CLK_FORCE_HT = 0x02;
static constexpr uint8_t CLK_ALP_AVAIL_REQ = 0x08;
static constexpr uint8_t CLK_HT_AVAIL_REQ = 0x10;
static constexpr uint8_t CLK_FORCE_HW_REQ_OFF= 0x20;
static constexpr uint8_t CLK_ALP_AVAIL = 0x40;
static constexpr uint8_t CLK_HT_AVAIL = 0x80;
static constexpr uint8_t DEVCTL_F2WM_ENAB = 0x10;
static constexpr uint8_t MESBUSY_ENAB = 0x80;
static constexpr uint8_t WAKEUP_HTWAIT = 0x02;
// CYW43455 values (Linux CY_43455_*): a deeper F2 watermark and
// MES busy signalling keep the card's FIFO from overrunning.
static constexpr uint8_t F2_WATERMARK_43455 = 0x60;
static constexpr uint8_t MES_WATERMARK_43455 = 0x50;
// Broadcom vendor CCCR
static constexpr uint32_t CCCR_BRCM_CARDCAP = 0xF0;
static constexpr uint8_t CARDCAP_CMD14 = 0x02 | 0x04; // support | ext
static constexpr uint32_t CCCR_BRCM_CARDCTRL = 0xF1;
static constexpr uint8_t CARDCTRL_WLANRESET = 0x02;
// Backplane
static constexpr uint32_t SI_ENUM_BASE = 0x18000000; // ChipCommon
static constexpr uint32_t CC_CHIPID = 0x000;
static constexpr uint32_t CC_CAPS_EXT = 0x0AC;
static constexpr uint32_t CAPS_EXT_AOB = 0x40; // PMU in its own "always on" core
static constexpr uint32_t CC_EROMPTR = 0x0FC;
static constexpr uint32_t CC_PMUCONTROL = 0x600;
static constexpr uint32_t CC_CHIPCONTROL_ADDR = 0x650;
static constexpr uint32_t CC_CHIPCONTROL_DATA = 0x654;
static constexpr uint32_t PMU_CTL_RES_RELOAD = 2u << 13;
// SDIO device core
static constexpr uint32_t SD_INTSTATUS = 0x20;
static constexpr uint32_t SD_HOSTINTMASK = 0x24;
static constexpr uint32_t SD_TOSBMAILBOXDATA = 0x48;
static constexpr uint32_t SDPCM_PROT_VERSION = 4;
static constexpr uint32_t HOSTINTMASK = 0x000000F0 | (1u << 29); // HMB SW | CHIPACTIVE
// Core wrapper (AI)
static constexpr uint32_t WRAP_IOCTL = 0x408;
static constexpr uint32_t WRAP_RESET_CTL = 0x800;
static constexpr uint32_t IOCTL_CLK = 0x0001;
static constexpr uint32_t IOCTL_FGC = 0x0002;
static constexpr uint32_t RESET_CTL_RESET = 0x0001;
static constexpr uint32_t CR4_IOCTL_CPUHALT = 0x0020;
static constexpr uint32_t D11_IOCTL_PHYCLKEN = 0x0004;
static constexpr uint32_t D11_IOCTL_PHYRESET = 0x0008;
// ARM CR4 core
static constexpr uint32_t CR4_CAP = 0x04;
static constexpr uint32_t CR4_BANKIDX = 0x40;
static constexpr uint32_t CR4_BANKINFO = 0x44;
// Core IDs
static constexpr uint16_t CORE_CHIPCOMMON = 0x800;
static constexpr uint16_t CORE_80211 = 0x812;
static constexpr uint16_t CORE_PMU = 0x827;
static constexpr uint16_t CORE_SDIO_DEV = 0x829;
static constexpr uint16_t CORE_ARM_CR4 = 0x83E;
static constexpr uint16_t CORE_GCI = 0x840;
static constexpr uint16_t CHIP_4345 = 0x4345;
static constexpr uint32_t RAMBASE_4345 = 0x198000;
// =========================================================================
// Function 1 and the backplane
// =========================================================================
bool BrcmfF1Read8(uint32_t reg, uint8_t& value) { return Sdio::Read8(Sdio::FN1, reg, value); }
bool BrcmfF1Write8(uint32_t reg, uint8_t value) { return Sdio::Write8(Sdio::FN1, reg, value); }
static bool SetWindow(uint32_t addr) {
uint32_t window = addr & ~0x7FFFu;
if (window == g_brcmf.Window) return true;
bool ok = BrcmfF1Write8(F1_SBADDRLOW, (uint8_t)((window >> 8) & 0x80))
&& BrcmfF1Write8(F1_SBADDRMID, (uint8_t)(window >> 16))
&& BrcmfF1Write8(F1_SBADDRHIGH, (uint8_t)(window >> 24));
g_brcmf.Window = ok ? window : 0xFFFFFFFF;
return ok;
}
bool BrcmfBpSelectWindow(uint32_t addr) { return SetWindow(addr); }
// 0x8000 in the function 1 address selects a 4-byte backplane access.
static constexpr uint32_t SB_ACCESS_4B = 0x8000;
bool BrcmfBpRead32(uint32_t addr, uint32_t& value) {
uint32_t word = 0;
if (!SetWindow(addr)) return false;
if (!Sdio::ReadBlock(Sdio::FN1, (addr & 0x7FFF) | SB_ACCESS_4B, true, (uint8_t*)&word, 4))
return false;
value = word;
return true;
}
bool BrcmfBpWrite32(uint32_t addr, uint32_t value) {
if (!SetWindow(addr)) return false;
return Sdio::WriteBlock(Sdio::FN1, (addr & 0x7FFF) | SB_ACCESS_4B, true, (const uint8_t*)&value, 4);
}
static bool BpMem(bool write, uint32_t addr, uint8_t* buffer, uint32_t len) {
while (len > 0) {
uint32_t offset = addr & 0x7FFF;
uint32_t run = 0x8000 - offset;
if (run > len) run = len;
if (!SetWindow(addr)) return false;
bool ok = write ? Sdio::WriteBlock(Sdio::FN1, offset, true, buffer, run)
: Sdio::ReadBlock(Sdio::FN1, offset, true, buffer, run);
if (!ok) return false;
addr += run;
buffer += run;
len -= run;
}
return true;
}
bool BrcmfBpReadMem(uint32_t addr, uint8_t* buffer, uint32_t len) {
return BpMem(false, addr, buffer, len);
}
bool BrcmfBpWriteMem(uint32_t addr, const uint8_t* buffer, uint32_t len) {
return BpMem(true, addr, (uint8_t*)buffer, len);
}
static uint32_t Rd(uint32_t addr) {
uint32_t v = 0;
BrcmfBpRead32(addr, v);
return v;
}
// =========================================================================
// Cores
// =========================================================================
static const BrcmfCore* FindCore(uint16_t id) {
for (uint32_t i = 0; i < g_brcmf.CoreCount; i++)
if (g_brcmf.Cores[i].Id == id) return &g_brcmf.Cores[i];
return nullptr;
}
// Enumeration ROM (Linux brcmf_chip_dmp_erom_scan).
static constexpr uint32_t DESC_TYPE_MSK = 0x0000000F;
static constexpr uint32_t DESC_VALID = 0x00000001;
static constexpr uint32_t DESC_COMPONENT = 0x00000001;
static constexpr uint32_t DESC_MASTER_PORT = 0x00000003;
static constexpr uint32_t DESC_ADDRESS = 0x00000005;
static constexpr uint32_t DESC_ADDRSIZE_GT32 = 0x00000008;
static constexpr uint32_t DESC_EOT = 0x0000000F;
static constexpr uint32_t COMP_PARTNUM_MASK = 0x000FFF00, COMP_PARTNUM_SHIFT = 8;
static constexpr uint32_t COMP_REV_MASK = 0xFF000000, COMP_REV_SHIFT = 24;
static constexpr uint32_t COMP_NSW_MASK = 0x00F80000, COMP_NSW_SHIFT = 19;
static constexpr uint32_t COMP_NMW_MASK = 0x0007C000, COMP_NMW_SHIFT = 14;
static constexpr uint32_t SLAVE_ADDR_BASE = 0xFFFFF000;
static constexpr uint32_t SLAVE_TYPE_MASK = 0x000000C0, SLAVE_TYPE_SHIFT = 6;
static constexpr uint32_t SLAVE_TYPE_SLAVE = 0;
static constexpr uint32_t SLAVE_TYPE_SWRAP = 2;
static constexpr uint32_t SLAVE_TYPE_MWRAP = 3;
static constexpr uint32_t SLAVE_SIZE_MASK = 0x00000030, SLAVE_SIZE_SHIFT = 4;
static constexpr uint32_t SLAVE_SIZE_4K = 0;
static constexpr uint32_t SLAVE_SIZE_8K = 1;
static constexpr uint32_t SLAVE_SIZE_DESC = 3;
static uint32_t EromNext(uint32_t& erom, uint32_t* type) {
uint32_t v = Rd(erom);
erom += 4;
uint32_t t = v & DESC_TYPE_MSK;
if ((t & ~DESC_ADDRSIZE_GT32) == DESC_ADDRESS) t = DESC_ADDRESS;
if (type) *type = t;
return v;
}
static bool EromRegAddr(uint32_t& erom, uint32_t& regBase, uint32_t& wrapBase) {
regBase = 0;
wrapBase = 0;
uint32_t desc = 0, val = 0;
uint32_t wrapType;
// One descriptor decides it, as in Linux: a master port means the
// core's wrapper is a master wrapper (any further master ports are
// skipped by the address search below); an address straight away
// means a slave wrapper. Cores with master ports -- the CR4, D11,
// the SDIO core, and ChipCommon on these chips -- must not be
// looked up as slave-wrapped: their wrapper is never found, and the
// halt/release writes go nowhere.
val = EromNext(erom, &desc);
if (desc == DESC_MASTER_PORT) {
wrapType = SLAVE_TYPE_MWRAP;
} else if (desc == DESC_ADDRESS) {
erom -= 4;
wrapType = SLAVE_TYPE_SWRAP;
} else {
erom -= 4;
return false;
}
for (int guard = 0; guard < 64; guard++) {
do {
val = EromNext(erom, &desc);
if (desc == DESC_EOT) { erom -= 4; return false; }
} while (desc != DESC_ADDRESS && desc != DESC_COMPONENT);
if (desc == DESC_COMPONENT) { erom -= 4; return true; }
if (val & DESC_ADDRSIZE_GT32) EromNext(erom, nullptr);
uint32_t sizeType = (val & SLAVE_SIZE_MASK) >> SLAVE_SIZE_SHIFT;
if (sizeType == SLAVE_SIZE_DESC) {
uint32_t size = EromNext(erom, nullptr);
if (size & DESC_ADDRSIZE_GT32) EromNext(erom, nullptr);
}
if (sizeType != SLAVE_SIZE_4K && sizeType != SLAVE_SIZE_8K) continue;
uint32_t slaveType = (val & SLAVE_TYPE_MASK) >> SLAVE_TYPE_SHIFT;
if (regBase == 0 && slaveType == SLAVE_TYPE_SLAVE) regBase = val & SLAVE_ADDR_BASE;
if (wrapBase == 0 && slaveType == wrapType) wrapBase = val & SLAVE_ADDR_BASE;
if (regBase && wrapBase) return true;
}
return false;
}
static bool ScanCores() {
g_brcmf.CoreCount = 0;
uint32_t erom = Rd(SI_ENUM_BASE + CC_EROMPTR);
if (erom == 0 || erom == 0xFFFFFFFF) return false;
uint32_t desc = 0;
for (int guard = 0; guard < 512 && desc != DESC_EOT; guard++) {
uint32_t val = EromNext(erom, &desc);
if (!(val & DESC_VALID) || desc != DESC_COMPONENT) continue;
uint16_t id = (uint16_t)((val & COMP_PARTNUM_MASK) >> COMP_PARTNUM_SHIFT);
val = EromNext(erom, &desc);
if (desc != DESC_COMPONENT) return false;
uint32_t nmw = (val & COMP_NMW_MASK) >> COMP_NMW_SHIFT;
uint32_t nsw = (val & COMP_NSW_MASK) >> COMP_NSW_SHIFT;
uint8_t rev = (uint8_t)((val & COMP_REV_MASK) >> COMP_REV_SHIFT);
if (nmw + nsw == 0 && id != CORE_PMU && id != CORE_GCI) continue;
uint32_t base = 0, wrap = 0;
if (!EromRegAddr(erom, base, wrap)) continue;
if (g_brcmf.CoreCount < sizeof(g_brcmf.Cores) / sizeof(g_brcmf.Cores[0])) {
g_brcmf.Cores[g_brcmf.CoreCount++] = { id, rev, base, wrap };
}
}
const BrcmfCore* cr4 = FindCore(CORE_ARM_CR4);
return FindCore(CORE_CHIPCOMMON) && FindCore(CORE_SDIO_DEV) && cr4 && cr4->Wrap != 0;
}
static void CoreDisable(const BrcmfCore* core, uint32_t prereset, uint32_t reset) {
if (!(Rd(core->Wrap + WRAP_RESET_CTL) & RESET_CTL_RESET)) {
BrcmfBpWrite32(core->Wrap + WRAP_IOCTL, prereset | IOCTL_FGC | IOCTL_CLK);
Rd(core->Wrap + WRAP_IOCTL);
BrcmfBpWrite32(core->Wrap + WRAP_RESET_CTL, RESET_CTL_RESET);
Timekeeping::BusyWaitUs(20);
for (int i = 0; i < 30 && Rd(core->Wrap + WRAP_RESET_CTL) != RESET_CTL_RESET; i++)
Timekeeping::BusyWaitUs(10);
}
BrcmfBpWrite32(core->Wrap + WRAP_IOCTL, reset | IOCTL_FGC | IOCTL_CLK);
Rd(core->Wrap + WRAP_IOCTL);
}
static void CoreReset(const BrcmfCore* core, uint32_t prereset, uint32_t reset, uint32_t postreset) {
CoreDisable(core, prereset, reset);
for (int i = 0; i < 50 && (Rd(core->Wrap + WRAP_RESET_CTL) & RESET_CTL_RESET); i++) {
BrcmfBpWrite32(core->Wrap + WRAP_RESET_CTL, 0);
Timekeeping::BusyWaitUs(50);
}
BrcmfBpWrite32(core->Wrap + WRAP_IOCTL, postreset | IOCTL_CLK);
Rd(core->Wrap + WRAP_IOCTL);
}
// The CR4's TCM: ATCM and BTCM bank counts in CAP, each bank's size in
// 8 KiB units in BANKINFO.
static uint32_t Cr4RamSize(const BrcmfCore* cr4) {
uint32_t cap = Rd(cr4->Base + CR4_CAP);
uint32_t banks = (cap & 0xF) + ((cap >> 4) & 0xF);
uint32_t size = 0;
for (uint32_t i = 0; i < banks; i++) {
BrcmfBpWrite32(cr4->Base + CR4_BANKIDX, i);
uint32_t info = Rd(cr4->Base + CR4_BANKINFO);
size += ((info & 0x3F) + 1) * 8192;
}
return size;
}
// =========================================================================
// Firmware files
// =========================================================================
uint8_t* BrcmfLoadFile(const char* ext, uint32_t& size, bool required) {
char path[80];
int n = 0;
auto add = [&](const char* s) { while (*s && n < 78) path[n++] = *s++; };
add("0:/os/firmware/brcm/brcmfmac");
add(g_brcmf.FwName);
add("-sdio.");
add(ext);
path[n] = '\0';
size = 0;
Fs::Vfs::BackendFile file;
if (Fs::Vfs::OpenBackendFile(path, file) < 0) {
if (required) KernelLogStream(ERROR, "WiFi") << "Firmware file not found: " << path;
return nullptr;
}
uint64_t fileSize = Fs::Vfs::GetBackendFileSize(file);
if (fileSize == 0 || fileSize > 4 * 1024 * 1024) {
Fs::Vfs::CloseBackendFile(file);
KernelLogStream(ERROR, "WiFi") << "Unusable firmware file " << path;
return nullptr;
}
// Rounded up and zero padded: everything goes over the bus in dwords.
uint32_t padded = ((uint32_t)fileSize + 3) & ~3u;
uint8_t* data = (uint8_t*)Memory::g_heap->Request(padded);
if (!data) {
Fs::Vfs::CloseBackendFile(file);
return nullptr;
}
memset(data, 0, padded);
int got = Fs::Vfs::ReadBackendFile(file, data, 0, fileSize);
Fs::Vfs::CloseBackendFile(file);
if (got != (int)fileSize) {
Memory::g_heap->Free(data);
KernelLogStream(ERROR, "WiFi") << "Short read of " << path;
return nullptr;
}
size = (uint32_t)fileSize;
KernelLogStream(INFO, "WiFi") << "Loaded " << path << " (" << (uint64_t)size << " bytes)";
return data;
}
// The NVRAM text ("key=value" lines, '#' comments) becomes the binary
// form the firmware reads: NUL-terminated pairs, zero padded to a dword
// (at least one extra NUL), then a token word holding the padded length
// in dwords and its complement (Linux brcmf_fw_nvram_strip).
static uint8_t* BuildNvram(const uint8_t* text, uint32_t len, uint32_t& outLen) {
uint8_t* out = (uint8_t*)Memory::g_heap->Request(len + 8);
if (!out) return nullptr;
uint32_t n = 0;
uint32_t i = 0;
auto printable = [](uint8_t c) { return c > 0x20 && c < 0x7F && c != '#'; };
while (i < len) {
while (i < len && (text[i] == ' ' || text[i] == '\t' || text[i] == '\r' || text[i] == '\n')) i++;
if (i >= len) break;
if (text[i] == '#') {
while (i < len && text[i] != '\n') i++;
continue;
}
uint32_t start = i;
bool sawEquals = false;
while (i < len && printable(text[i])) {
if (text[i] == '=') sawEquals = true;
i++;
}
if (sawEquals && i > start) {
memcpy(out + n, text + start, i - start);
n += i - start;
out[n++] = '\0';
}
while (i < len && text[i] != '\n') i++;
}
uint32_t padded = (n + 1 + 3) & ~3u;
while (n < padded) out[n++] = 0;
uint32_t words = padded / 4;
uint32_t token = (~words << 16) | (words & 0xFFFF);
memcpy(out + n, &token, 4);
outLen = n + 4;
return out;
}
static bool DownloadAndVerify(uint32_t addr, const uint8_t* data, uint32_t len) {
if (!BrcmfBpWriteMem(addr, data, len)) return false;
// Read the image back: a marginal SDIO bus shows up here as a clean
// error instead of as a firmware that never starts.
uint8_t* check = (uint8_t*)Memory::g_heap->Request(4096);
if (!check) return true;
bool ok = true;
for (uint32_t off = 0; off < len && ok; off += 4096) {
uint32_t run = len - off < 4096 ? len - off : 4096;
if (!BrcmfBpReadMem(addr + off, check, run) || memcmp(check, data + off, run) != 0) {
KernelLogStream(ERROR, "WiFi") << "Firmware verify failed at offset 0x"
<< base::hex << (uint64_t)off << base::dec;
ok = false;
}
}
Memory::g_heap->Free(check);
return ok;
}
// =========================================================================
// Bring-up
// =========================================================================
static bool WaitClock(uint8_t bit, uint64_t timeoutMs) {
uint64_t start = Timekeeping::GetMilliseconds();
for (;;) {
uint8_t v = 0;
if (BrcmfF1Read8(F1_CHIPCLKCSR, v) && (v & bit)) return true;
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
Timekeeping::BusyWaitUs(200);
}
}
void BrcmfChipPowerOff() {
RpiFirmware::SetGpioOutput(RpiFirmware::EXPGPIO_WL_ON, false);
}
static bool PowerOnAndEnumerate() {
// GPIO 34-39: CLK, CMD, DAT0-3 on ALT3 (the Arasan), pulled up but
// for the clock, as Linux's sdio_pins.
for (uint32_t pin = 34; pin <= 39; pin++) {
RpiGpio::SetFunction(pin, RpiGpio::Function::Alt3);
RpiGpio::SetPull(pin, pin == 34 ? RpiGpio::Pull::None : RpiGpio::Pull::Up);
}
if (!RpiFirmware::SetGpioOutput(RpiFirmware::EXPGPIO_WL_ON, false)) {
KernelLogStream(ERROR, "WiFi") << "Cannot switch the radio's power (WL_ON)";
return false;
}
Timekeeping::BusyWaitMs(20);
RpiFirmware::SetGpioOutput(RpiFirmware::EXPGPIO_WL_ON, true);
Timekeeping::BusyWaitMs(250);
// The legacy EMMC clock feeds the Arasan; nothing on the boot path
// uses that controller, so make sure the clock is not gated.
if (!RpiFirmware::SetClockState(RpiFirmware::CLOCK_EMMC, true))
KernelLogStream(WARNING, "WiFi") << "Could not switch the EMMC clock on";
uint32_t clockHz = 0;
if (!RpiFirmware::GetClockRate(RpiFirmware::CLOCK_EMMC, clockHz)) clockHz = 0;
if (!Sdio::HostInit(g_brcmf.ArasanMmio, clockHz)) return false;
return Sdio::CardInit();
}
static bool AttachChip() {
g_brcmf.Window = 0xFFFFFFFF;
// ALP first: the backplane runs from it until the firmware has set
// up the PLL.
uint8_t req = CLK_FORCE_HW_REQ_OFF | CLK_ALP_AVAIL_REQ;
uint8_t v = 0;
if (!BrcmfF1Write8(F1_CHIPCLKCSR, req) || !BrcmfF1Read8(F1_CHIPCLKCSR, v) ||
(v & ~(CLK_ALP_AVAIL | CLK_HT_AVAIL)) != req) {
KernelLogStream(ERROR, "WiFi") << "ChipClkCSR: wrote 0x" << base::hex << (uint64_t)req
<< ", read 0x" << (uint64_t)v << base::dec;
return false;
}
if (!WaitClock(CLK_ALP_AVAIL, 100)) {
KernelLogStream(ERROR, "WiFi") << "ALP clock never became available";
return false;
}
BrcmfF1Write8(F1_CHIPCLKCSR, CLK_FORCE_HW_REQ_OFF | CLK_FORCE_ALP);
Timekeeping::BusyWaitUs(65);
BrcmfF1Write8(F1_SDIOPULLUP, 0);
uint32_t chipId = 0;
if (!BrcmfBpRead32(SI_ENUM_BASE + CC_CHIPID, chipId)) {
KernelLogStream(ERROR, "WiFi") << "Backplane read failed";
return false;
}
g_brcmf.ChipId = (uint16_t)(chipId & 0xFFFF);
g_brcmf.ChipRev = (uint8_t)((chipId >> 16) & 0xF);
uint32_t socType = chipId >> 28;
KernelLogStream(INFO, "WiFi") << "Broadcom chip " << base::hex << (uint64_t)g_brcmf.ChipId
<< base::dec << " rev " << (uint64_t)g_brcmf.ChipRev;
if (g_brcmf.ChipId != CHIP_4345 || socType != 1) {
KernelLogStream(ERROR, "WiFi") << "Unsupported chip (only the BCM4345 family: "
"CYW43455 / CYW43456)";
return false;
}
// Linux's table: revision 9 is the CYW43456 (Pi 400), the others
// the CYW43455 (Pi 4B).
g_brcmf.FwName = g_brcmf.ChipRev == 9 ? "43456" : "43455";
if (!ScanCores()) {
KernelLogStream(ERROR, "WiFi") << "Enumeration ROM scan found no usable cores";
return false;
}
// Linux brcmf_chip_get_pmu: the PMU registers live in a core of their
// own only when ChipCommon (rev 35+) says the always-on block exists;
// otherwise they are ChipCommon's, at the same offsets.
const BrcmfCore* cc = FindCore(CORE_CHIPCOMMON);
const BrcmfCore* pmu = FindCore(CORE_PMU);
uint32_t capsExt = cc->Rev >= 35 ? Rd(cc->Base + CC_CAPS_EXT) : 0;
g_brcmf.PmuBase = (pmu && (capsExt & CAPS_EXT_AOB)) ? pmu->Base : cc->Base;
g_brcmf.SdioCoreBase = FindCore(CORE_SDIO_DEV)->Base;
for (uint32_t i = 0; i < g_brcmf.CoreCount; i++) {
const BrcmfCore& c = g_brcmf.Cores[i];
KernelLogStream(INFO, "WiFi") << " core 0x" << base::hex << (uint64_t)c.Id << " rev "
<< base::dec << (uint64_t)c.Rev << " at 0x" << base::hex << (uint64_t)c.Base
<< ", wrapper 0x" << (uint64_t)c.Wrap << base::dec;
}
KernelLogStream(INFO, "WiFi") << "ChipCommon rev " << (uint64_t)cc->Rev << ", caps_ext 0x"
<< base::hex << (uint64_t)capsExt << ", PMU registers at 0x" << (uint64_t)g_brcmf.PmuBase
<< base::dec;
// Passive: ARM halted, D11 held in reset with its PHY clock on.
const BrcmfCore* cr4 = FindCore(CORE_ARM_CR4);
CoreReset(cr4, CR4_IOCTL_CPUHALT, CR4_IOCTL_CPUHALT, CR4_IOCTL_CPUHALT);
if (const BrcmfCore* d11 = FindCore(CORE_80211)) {
CoreReset(d11, D11_IOCTL_PHYRESET | D11_IOCTL_PHYCLKEN, D11_IOCTL_PHYCLKEN, D11_IOCTL_PHYCLKEN);
}
uint32_t haltIoctl = Rd(cr4->Wrap + WRAP_IOCTL);
uint32_t haltReset = Rd(cr4->Wrap + WRAP_RESET_CTL);
if (haltIoctl != (CR4_IOCTL_CPUHALT | IOCTL_CLK) || (haltReset & RESET_CTL_RESET)) {
KernelLogStream(ERROR, "WiFi") << "The CR4 did not take the halt (wrapper 0x" << base::hex
<< (uint64_t)cr4->Wrap << ": ioctl 0x" << (uint64_t)haltIoctl << " reset 0x"
<< (uint64_t)haltReset << base::dec << ", expected 0x21 / 0x0)";
return false;
}
g_brcmf.RamBase = RAMBASE_4345;
g_brcmf.RamSize = Cr4RamSize(cr4);
if (g_brcmf.RamSize == 0 || g_brcmf.RamSize > 4 * 1024 * 1024) {
KernelLogStream(ERROR, "WiFi") << "Implausible RAM size " << (uint64_t)g_brcmf.RamSize;
return false;
}
// An SDIO card reset also resets the WLAN backplane, and a backplane
// reset reloads the PMU state.
uint8_t cardCtrl = 0;
if (Sdio::Read8(Sdio::FN0, CCCR_BRCM_CARDCTRL, cardCtrl))
Sdio::Write8(Sdio::FN0, CCCR_BRCM_CARDCTRL, cardCtrl | CARDCTRL_WLANRESET);
BrcmfBpWrite32(g_brcmf.PmuBase + CC_PMUCONTROL,
Rd(g_brcmf.PmuBase + CC_PMUCONTROL) | PMU_CTL_RES_RELOAD);
KernelLogStream(INFO, "WiFi") << "CYW" << g_brcmf.FwName << ": " << (uint64_t)g_brcmf.CoreCount
<< " cores, " << (uint64_t)(g_brcmf.RamSize / 1024) << " KiB RAM at 0x" << base::hex
<< (uint64_t)g_brcmf.RamBase << base::dec;
return true;
}
static bool DownloadFirmware() {
uint32_t fwLen = 0, nvLen = 0;
uint8_t* fw = BrcmfLoadFile("bin", fwLen, true);
if (!fw) return false;
uint8_t* nvText = BrcmfLoadFile("txt", nvLen, true);
if (!nvText) {
Memory::g_heap->Free(fw);
return false;
}
uint32_t nvramLen = 0;
uint8_t* nvram = BuildNvram(nvText, nvLen, nvramLen);
Memory::g_heap->Free(nvText);
uint32_t fwPadded = (fwLen + 3) & ~3u;
bool ok = nvram && fwPadded + nvramLen < g_brcmf.RamSize;
if (!ok) KernelLogStream(ERROR, "WiFi") << "Firmware and NVRAM do not fit in the radio's RAM";
uint32_t rstvec = 0;
memcpy(&rstvec, fw, 4);
// Download under a plain ALP request, as Linux does (alp_only):
// the probe's FORCE_HW_REQ_OFF | FORCE_ALP must be gone before the
// ARM starts, or the firmware's own clock requests -- the HT clock
// its PLL setup asks for above all -- are blocked.
if (ok) {
BrcmfF1Write8(F1_CHIPCLKCSR, CLK_ALP_AVAIL_REQ);
if (!WaitClock(CLK_ALP_AVAIL, 100))
KernelLogStream(WARNING, "WiFi") << "ALP clock lost on its plain request";
}
uint64_t start = Timekeeping::GetMilliseconds();
if (ok && !DownloadAndVerify(g_brcmf.RamBase, fw, fwPadded)) {
KernelLogStream(ERROR, "WiFi") << "Firmware download failed";
ok = false;
}
if (ok && !DownloadAndVerify(g_brcmf.RamBase + g_brcmf.RamSize - nvramLen, nvram, nvramLen)) {
KernelLogStream(ERROR, "WiFi") << "NVRAM download failed";
ok = false;
}
if (ok) {
KernelLogStream(INFO, "WiFi") << "Firmware and NVRAM written in "
<< (Timekeeping::GetMilliseconds() - start) << " ms";
}
Memory::g_heap->Free(fw);
if (nvram) Memory::g_heap->Free(nvram);
if (!ok) return false;
// Start the ARM: clear the SDIO core's interrupts, put the reset
// vector at address 0 and take the CPU out of halt.
BrcmfBpWrite32(g_brcmf.SdioCoreBase + SD_INTSTATUS, 0xFFFFFFFF);
uint32_t vecBack = 0;
bool vecOk = BrcmfBpWriteMem(0, (const uint8_t*)&rstvec, 4)
&& BrcmfBpReadMem(0, (uint8_t*)&vecBack, 4) && vecBack == rstvec;
const BrcmfCore* cr4 = FindCore(CORE_ARM_CR4);
CoreReset(cr4, CR4_IOCTL_CPUHALT, 0, 0);
if (!vecOk)
KernelLogStream(ERROR, "WiFi") << "Reset vector did not stick: address 0 reads 0x"
<< base::hex << (uint64_t)vecBack << base::dec;
KernelLogStream(INFO, "WiFi") << "Reset vector 0x" << base::hex << (uint64_t)rstvec
<< "; CR4 released: ioctl 0x" << (uint64_t)Rd(cr4->Wrap + WRAP_IOCTL) << " reset 0x"
<< (uint64_t)Rd(cr4->Wrap + WRAP_RESET_CTL) << base::dec << " (expect 0x1 / 0x0)";
return true;
}
// Save/restore: the firmware can power its domains down between
// frames; the host then has to allow for the HT clock coming back.
static bool SrCapable() {
BrcmfBpWrite32(g_brcmf.PmuBase + CC_CHIPCONTROL_ADDR, 3);
return (Rd(g_brcmf.PmuBase + CC_CHIPCONTROL_DATA) & (1u << 2)) != 0;
}
static bool StartFrames() {
// Clear the ALP force, then ask for HT: the running firmware brings
// up the PLL.
BrcmfF1Write8(F1_CHIPCLKCSR, 0);
BrcmfF1Write8(F1_CHIPCLKCSR, CLK_HT_AVAIL_REQ);
bool ht = WaitClock(CLK_HT_AVAIL, 20);
if (!ht) {
// Not yet: a snapshot 20 ms in tells a CPU that never ran from
// one that ran and was reset.
const BrcmfCore* cr4 = FindCore(CORE_ARM_CR4);
uint8_t csr = 0;
BrcmfF1Read8(F1_CHIPCLKCSR, csr);
KernelLogStream(INFO, "WiFi") << "20 ms after release: ChipClkCSR 0x" << base::hex
<< (uint64_t)csr << ", CR4 ioctl 0x" << (uint64_t)Rd(cr4->Wrap + WRAP_IOCTL) << " reset 0x"
<< (uint64_t)Rd(cr4->Wrap + WRAP_RESET_CTL) << ", RAM top 0x"
<< (uint64_t)Rd(g_brcmf.RamBase + g_brcmf.RamSize - 4) << base::dec;
ht = WaitClock(CLK_HT_AVAIL, 2000);
}
if (!ht) {
// What the hardware was doing, for the next attempt: the clock
// register (or whether function 1 still answers at all), the
// CPU's wrapper state, and whether the firmware ever replaced
// the NVRAM token in the last word of RAM with its shared area.
uint8_t csr = 0;
bool csrOk = BrcmfF1Read8(F1_CHIPCLKCSR, csr);
const BrcmfCore* cr4 = FindCore(CORE_ARM_CR4);
uint32_t ioctl = Rd(cr4->Wrap + WRAP_IOCTL);
uint32_t reset = Rd(cr4->Wrap + WRAP_RESET_CTL);
uint32_t top = Rd(g_brcmf.RamBase + g_brcmf.RamSize - 4);
KernelLogStream(ERROR, "WiFi") << "HT clock never became available (firmware did not start)";
KernelLogStream(ERROR, "WiFi") << "ChipClkCSR " << (csrOk ? "0x" : "unreadable")
<< base::hex << (uint64_t)(csrOk ? csr : 0) << ", CR4 ioctl 0x" << (uint64_t)ioctl
<< " reset 0x" << (uint64_t)reset << ", RAM top word 0x" << (uint64_t)top
<< base::dec << (((~top >> 16) & 0xFFFF) == (top & 0xFFFF)
? " (still the NVRAM token)" : " (written by the firmware)");
return false;
}
uint8_t saveClk = 0;
BrcmfF1Read8(F1_CHIPCLKCSR, saveClk);
BrcmfF1Write8(F1_CHIPCLKCSR, saveClk | CLK_FORCE_HT);
BrcmfBpWrite32(g_brcmf.SdioCoreBase + SD_TOSBMAILBOXDATA, SDPCM_PROT_VERSION << 16);
if (!Sdio::EnableFunction(Sdio::FN2, true, 3000)) {
KernelLogStream(ERROR, "WiFi") << "Function 2 did not become ready";
Sdio::EnableFunction(Sdio::FN2, false, 0);
return false;
}
g_brcmf.HostIntMask = HOSTINTMASK;
BrcmfBpWrite32(g_brcmf.SdioCoreBase + SD_HOSTINTMASK, HOSTINTMASK);
BrcmfF1Write8(F1_WATERMARK, F2_WATERMARK_43455);
uint8_t devCtl = 0;
if (BrcmfF1Read8(F1_DEVICE_CTL, devCtl))
BrcmfF1Write8(F1_DEVICE_CTL, devCtl | DEVCTL_F2WM_ENAB);
BrcmfF1Write8(F1_MESBUSYCTRL, MES_WATERMARK_43455 | MESBUSY_ENAB);
g_brcmf.SrEnabled = SrCapable();
if (g_brcmf.SrEnabled) {
uint8_t wake = 0;
if (BrcmfF1Read8(F1_WAKEUPCTRL, wake)) BrcmfF1Write8(F1_WAKEUPCTRL, wake | WAKEUP_HTWAIT);
Sdio::Write8(Sdio::FN0, CCCR_BRCM_CARDCAP, CARDCAP_CMD14);
BrcmfF1Write8(F1_CHIPCLKCSR, CLK_FORCE_HT);
} else {
BrcmfF1Write8(F1_CHIPCLKCSR, saveClk);
}
// Master, function 1 and function 2.
Sdio::EnableCardInterrupts(0x07);
return true;
}
bool BrcmfChipBringUp() {
g_brcmf.BusLock.Acquire();
g_brcmf.SharedAddr = 0;
g_brcmf.ConsoleIdx = 0;
g_brcmf.RamSize = 0;
bool ok = PowerOnAndEnumerate() && AttachChip() && DownloadFirmware() && StartFrames();
g_brcmf.BusLock.Release();
if (!ok) {
// A firmware that started and then trapped has said why on its
// console (no-op when it never got that far).
BrcmfDumpConsole();
BrcmfChipPowerOff();
}
return ok;
}
// =========================================================================
// Firmware console and trap report
// =========================================================================
static bool InRam(uint32_t addr, uint32_t len) {
return addr >= g_brcmf.RamBase && addr + len <= g_brcmf.RamBase + g_brcmf.RamSize
&& addr + len >= addr;
}
static bool ReadShared(uint32_t sh[16]) {
if (!g_brcmf.SharedAddr) {
uint32_t addr = Rd(g_brcmf.RamBase + g_brcmf.RamSize - 4);
// Still the NVRAM token: the firmware has not published it.
if (((~addr >> 16) & 0xFFFF) == (addr & 0xFFFF) || !InRam(addr, 64)) return false;
g_brcmf.SharedAddr = addr;
}
return BrcmfBpReadMem(g_brcmf.SharedAddr, (uint8_t*)sh, 64);
}
static void EmitConsole(const char* buf, uint32_t size, uint32_t from, uint32_t to) {
char line[161];
uint32_t n = 0;
for (uint32_t i = from; i != to; i = (i + 1) % size) {
char c = buf[i];
if (c == '\r') continue;
if (c == '\n' || n == sizeof(line) - 1) {
line[n] = '\0';
if (n) KernelLogStream(INFO, "WiFi-FW") << line;
n = 0;
if (c == '\n') continue;
}
line[n++] = (c >= 0x20 && c < 0x7F) ? c : '.';
}
line[n] = '\0';
if (n) KernelLogStream(INFO, "WiFi-FW") << line;
}
void BrcmfDumpConsole() {
if (g_brcmf.RamSize == 0) return;
g_brcmf.BusLock.Acquire();
// sdpcm_shared: flags, trap_addr, assert_exp_addr, assert_file_addr,
// assert_line, console_addr, msgtrace_addr, tag[32], brpt_addr
uint32_t sh[16] = {};
if (!ReadShared(sh)) {
g_brcmf.BusLock.Release();
return;
}
uint32_t flags = sh[0];
// rte_console: vcons_in, vcons_out, then the log: buf, buf_size,
// idx, out_idx.
uint32_t log[4] = {};
if (InRam(sh[5], 24) && BrcmfBpReadMem(sh[5] + 8, (uint8_t*)log, 16)
&& log[1] > 0 && log[1] <= 16384 && InRam(log[0], log[1]) && log[2] < log[1]) {
uint32_t size = log[1];
char* buf = (char*)Memory::g_heap->Request((size + 3) & ~3u);
if (buf && BrcmfBpReadMem(log[0], (uint8_t*)buf, (size + 3) & ~3u)) {
if (g_brcmf.ConsoleIdx >= size) g_brcmf.ConsoleIdx = 0;
EmitConsole(buf, size, g_brcmf.ConsoleIdx, log[2]);
g_brcmf.ConsoleIdx = log[2];
}
if (buf) Memory::g_heap->Free(buf);
}
if ((flags & 0x0400) && InRam(sh[1], 72)) {
// brcmf_trap_info: type, epc, cpsr, spsr, r0-r12, sp, lr, pc
uint32_t trap[20] = {};
if (BrcmfBpReadMem(sh[1], (uint8_t*)trap, sizeof(trap))) {
KernelLogStream(ERROR, "WiFi-FW") << "Firmware trap: type 0x" << base::hex
<< (uint64_t)trap[0] << ", epc 0x" << (uint64_t)trap[1] << ", lr 0x"
<< (uint64_t)trap[18] << ", pc 0x" << (uint64_t)trap[19] << base::dec;
}
}
if (flags & 0x0200) {
char file[64] = {};
if (InRam(sh[3], 60)) BrcmfBpReadMem(sh[3], (uint8_t*)file, 60);
file[63] = '\0';
KernelLogStream(ERROR, "WiFi-FW") << "Firmware assert at " << file << ":"
<< (uint64_t)sh[4];
}
g_brcmf.BusLock.Release();
}
}
@@ -0,0 +1,902 @@
/*
* BrcmfConnect.cpp
* Broadcom FullMAC over SDIO: firmware preinit (CLM, events), escan,
* join, key installation, link state, and the entry points the Wi-Fi
* facade calls
* Derived in part from Linux's brcmfmac, Copyright (c) 2010-2014 Broadcom
* Corporation, ISC license (notice in programs/data/licenses/NOTICES.txt).
* Copyright (c) 2026 Daniel Hammer
*/
#include "Brcmf.hpp"
#include "BrcmfSdio.hpp"
#include "Ieee80211.hpp"
#include "Wpa.hpp"
#include <Api/Syscall.hpp>
#include <Arch/Cpu.hpp>
#include <CppLib/Stream.hpp>
#include <Drivers/Platform/RpiFirmware.hpp>
#include <Libraries/Crypto.hpp>
#include <Libraries/Memory.hpp>
#include <Memory/Heap.hpp>
#include <Terminal/Terminal.hpp>
#include <Timekeeping/Timer.hpp>
// Joining follows brcmf_cfg80211_connect with the supplicant on the host:
// "wpaie" the RSN element our supplicant built (the firmware puts it in
// the association request, and it must match message 2's copy)
// "wpa_auth" WPA2-PSK (0x80) or PSK-SHA256 (0x8000), 0 for open
// "auth" open system
// "wsec" AES (4) for CCMP, 0 for open
// "join" SSID, BSSID and the chanspec escan reported
// The firmware answers with E_SET_SSID (status 0 = associated) and E_LINK;
// the access point then starts the 4-way handshake, whose EAPOL frames come
// and go as ordinary Ethernet frames. The keys it produces go to the
// firmware with "wsec_key". The radio does AES-CCMP only: GCMP networks are
// refused.
using namespace Kt;
using namespace montauk::abi;
namespace Drivers::Net::Wifi {
// WLC ioctls
static constexpr uint32_t WLC_UP = 2;
static constexpr uint32_t WLC_SET_INFRA = 20;
static constexpr uint32_t WLC_SET_SSID = 26;
static constexpr uint32_t WLC_SCAN = 50;
static constexpr uint32_t WLC_DISASSOC = 52;
static constexpr uint32_t WLC_SET_PM = 86;
static constexpr uint32_t WLC_GET_VALID_CHANNELS = 217;
// Events
static constexpr uint32_t E_SET_SSID = 0;
static constexpr uint32_t E_AUTH = 3;
static constexpr uint32_t E_DEAUTH = 5;
static constexpr uint32_t E_DEAUTH_IND = 6;
static constexpr uint32_t E_ASSOC = 7;
static constexpr uint32_t E_REASSOC = 9;
static constexpr uint32_t E_DISASSOC = 11;
static constexpr uint32_t E_DISASSOC_IND = 12;
static constexpr uint32_t E_LINK = 16;
static constexpr uint32_t E_ROAM = 19;
static constexpr uint32_t E_PSK_SUP = 46;
static constexpr uint32_t E_ESCAN_RESULT = 69;
static constexpr uint32_t STATUS_SUCCESS = 0;
static constexpr uint32_t STATUS_PARTIAL = 8;
static constexpr uint16_t EVENT_FLAG_LINK = 0x01;
static constexpr uint32_t WPA_AUTH_DISABLED = 0;
static constexpr uint32_t WPA2_AUTH_PSK = 0x80;
static constexpr uint32_t WPA2_AUTH_PSK_SHA256= 0x8000;
static constexpr uint32_t AES_ENABLED = 4;
static constexpr uint32_t CRYPTO_ALGO_AES_CCM = 4;
static constexpr uint32_t KEY_FLAG_PRIMARY = 1u << 1;
static constexpr uint64_t JOIN_TIMEOUT_MS = 12000;
static constexpr uint64_t SCAN_TIMEOUT_MS = 25000;
static constexpr uint64_t POLL_ACTIVE_MS = 10;
static constexpr uint64_t POLL_IDLE_MS = 250;
// The NVRAM files ship one address for every board; the radio's own
// OTP normally overrides it.
static constexpr uint8_t NVRAM_PLACEHOLDER_MAC[6] = { 0xb8, 0x27, 0xeb, 0x74, 0xf2, 0x6c };
// =========================================================================
// State
// =========================================================================
static volatile uint32_t g_conn = WIFI_CONN_IDLE;
static uint8_t g_bssid[6];
static char g_ssid[33];
static uint8_t g_ssidLen = 0;
static uint8_t g_channel = 0;
static bool g_secure = false;
static bool g_refusedSecurity = false;
static bool g_authFailed = false;
static bool g_sawEapol = false;
static uint64_t g_joinStartMs = 0;
static uint64_t g_lastServiceMs = 0;
static WpaConfig g_wpaCfg = {};
// Raised by events inside the pump, consumed by ConnectService().
static volatile bool g_evAssoc = false;
static volatile bool g_evJoinFailed = false;
static volatile bool g_evLinkDown = false;
static volatile uint32_t g_evStatus = 0;
static volatile uint32_t g_evReason = 0;
static volatile bool g_scanActive = false;
static uint64_t g_scanStartMs = 0;
static uint16_t g_syncId = 0;
static uint32_t g_scanSeen = 0;
static volatile bool g_dumpConsole = false;
static bool g_failureLogged = false;
// The chanspec escan reported for each BSS, in the firmware's own
// encoding, so the join names the channel exactly.
struct BssChan { uint8_t Bssid[6]; uint16_t Chanspec; bool Used; };
static BssChan g_chans[64];
static kcp::Spinlock g_chanLock;
struct PendingEapol { uint32_t Len; uint8_t Data[512]; };
static constexpr uint32_t EAPOL_DEPTH = 4;
static PendingEapol g_eapolQ[EAPOL_DEPTH];
static uint32_t g_eapolHead = 0, g_eapolTail = 0;
static kcp::Spinlock g_eapolLock;
static inline uint16_t Le16(const uint8_t* p) { return (uint16_t)(p[0] | p[1] << 8); }
static inline uint32_t Le32(const uint8_t* p) {
return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
}
static inline void Put32(uint8_t* p, uint32_t v) { memcpy(p, &v, 4); }
static inline void Put16(uint8_t* p, uint16_t v) { memcpy(p, &v, 2); }
static bool Elapsed(uint64_t now, uint64_t since, uint64_t ms) {
return now >= since && now - since >= ms;
}
static void PutMac(KernelLogStream& s, const uint8_t* m) {
static const char hex[] = "0123456789abcdef";
char buf[18];
for (int i = 0; i < 6; i++) {
buf[i * 3] = hex[m[i] >> 4];
buf[i * 3 + 1] = hex[m[i] & 0xF];
buf[i * 3 + 2] = i < 5 ? ':' : '\0';
}
s << buf;
}
// =========================================================================
// Events (inside the pump: record, never send)
// =========================================================================
static void RememberChanspec(const uint8_t* bssid, uint16_t chanspec) {
g_chanLock.Acquire();
BssChan* slot = nullptr;
for (auto& c : g_chans) {
if (c.Used && memcmp(c.Bssid, bssid, 6) == 0) { slot = &c; break; }
if (!c.Used && !slot) slot = &c;
}
if (slot) {
memcpy(slot->Bssid, bssid, 6);
slot->Chanspec = chanspec;
slot->Used = true;
}
g_chanLock.Release();
}
static uint16_t LookupChanspec(const uint8_t* bssid, uint8_t channel) {
uint16_t found = 0;
g_chanLock.Acquire();
for (auto& c : g_chans) {
if (c.Used && memcmp(c.Bssid, bssid, 6) == 0) { found = c.Chanspec; break; }
}
g_chanLock.Release();
if (found) return found;
// 802.11ac encoding: channel, 20 MHz, band.
return (uint16_t)(channel | 0x1000 | (channel > 14 ? 0xC000 : 0x0000));
}
// brcmf_escan_result_le: buflen, version, sync_id, bss_count, then
// brcmf_bss_info_le records (128 bytes fixed, IEs at ie_offset).
static void HandleEscanResult(const uint8_t* data, uint32_t len) {
if (len < 12) return;
uint16_t count = Le16(data + 10);
uint32_t off = 12;
for (uint16_t i = 0; i < count; i++) {
if (off + 128 > len) break;
const uint8_t* bi = data + off;
uint32_t biLen = Le32(bi + 4);
if (biLen < 128 || biLen > len - off) break;
const uint8_t* bssid = bi + 8;
uint16_t beacon = Le16(bi + 14);
uint16_t caps = Le16(bi + 16);
uint16_t chanspec = Le16(bi + 72);
int16_t rssi = (int16_t)Le16(bi + 78);
uint8_t ctlCh = bi[88];
uint16_t ieOff = Le16(bi + 116);
uint32_t ieLen = Le32(bi + 120);
if (ieOff > biLen || ieLen > biLen - ieOff) ieLen = 0;
uint8_t channel = ctlCh ? ctlCh : (uint8_t)(chanspec & 0xFF);
if (rssi < -127) rssi = -127;
if (rssi > 0) rssi = 0;
RememberChanspec(bssid, chanspec);
WifiRxBssInfo(bssid, beacon, caps, ieLen ? bi + ieOff : nullptr, ieLen,
channel, (int8_t)rssi);
g_scanSeen++;
off += biLen;
}
}
void BrcmfOnEvent(uint32_t type, uint32_t status, uint32_t reason, uint16_t flags,
const uint8_t* addr, const uint8_t* data, uint32_t len) {
(void)addr;
switch (type) {
case E_ESCAN_RESULT:
if (status == STATUS_PARTIAL) {
HandleEscanResult(data, len);
} else if (g_scanActive) {
g_scanActive = false;
KernelLogStream(INFO, "WiFi") << "Scan " << (status == STATUS_SUCCESS ? "complete" : "ended")
<< ": " << (uint64_t)g_scanSeen << " results";
}
break;
case E_SET_SSID:
if (g_conn != WIFI_CONN_ASSOCIATING) break;
if (status == STATUS_SUCCESS) {
g_evAssoc = true;
} else {
g_evStatus = status;
g_evJoinFailed = true;
}
break;
case E_LINK:
if (flags & EVENT_FLAG_LINK) {
if (g_conn == WIFI_CONN_ASSOCIATING) g_evAssoc = true;
} else if (g_conn >= WIFI_CONN_ASSOCIATED && g_conn != WIFI_CONN_FAILED) {
g_evReason = reason;
g_evLinkDown = true;
}
break;
case E_DEAUTH:
case E_DEAUTH_IND:
case E_DISASSOC_IND:
if (g_conn >= WIFI_CONN_ASSOCIATED && g_conn != WIFI_CONN_FAILED) {
g_evReason = reason;
g_evLinkDown = true;
}
break;
case E_AUTH:
if (status != STATUS_SUCCESS && g_conn == WIFI_CONN_ASSOCIATING)
KernelLogStream(WARNING, "WiFi") << "Authentication rejected (status "
<< (uint64_t)status << ", reason " << (uint64_t)reason << ")";
break;
case E_ASSOC:
case E_REASSOC:
if (status != STATUS_SUCCESS && g_conn == WIFI_CONN_ASSOCIATING)
KernelLogStream(WARNING, "WiFi") << "Association rejected (status "
<< (uint64_t)status << ", reason " << (uint64_t)reason << ")";
break;
default:
break;
}
}
static void QueueEapol(const uint8_t* body, uint32_t len) {
if (len > sizeof(g_eapolQ[0].Data)) return;
g_eapolLock.Acquire();
uint32_t next = (g_eapolHead + 1) % EAPOL_DEPTH;
if (next == g_eapolTail) g_eapolTail = (g_eapolTail + 1) % EAPOL_DEPTH;
memcpy(g_eapolQ[g_eapolHead].Data, body, len);
g_eapolQ[g_eapolHead].Len = len;
g_eapolHead = next;
g_eapolLock.Release();
}
static bool EapolPending() {
g_eapolLock.Acquire();
bool pending = g_eapolHead != g_eapolTail;
g_eapolLock.Release();
return pending;
}
void BrcmfOnRxEthernet(const uint8_t* frame, uint32_t len) {
uint16_t etherType = (uint16_t)(frame[12] << 8 | frame[13]);
if (etherType == ETHERTYPE_EAPOL) {
if (g_secure && g_conn >= WIFI_CONN_ASSOCIATING && g_conn != WIFI_CONN_FAILED) {
QueueEapol(frame + 14, len - 14);
}
return;
}
if (BrcmfLinkUp()) WifiRxEthernet(frame, len);
}
void BrcmfOnFirmwareFailure(const char* why) {
if (g_brcmf.State != BrcmfState::Running) return;
g_brcmf.State = BrcmfState::Error;
g_brcmf.FwErrors++;
g_scanActive = false;
g_dumpConsole = true;
if (!g_failureLogged) {
g_failureLogged = true;
KernelLogStream(ERROR, "WiFi") << "Radio failed: " << why;
}
}
// =========================================================================
// Bring-up
// =========================================================================
// CLM blob: the regulatory database the firmware ships without
// ("clm_min"). Sent in 1400-byte chunks behind brcmf_dload_data_le:
// flag (handler version 1 << 12 | begin 2 | end 4), type (2 = CLM),
// length, CRC (unused), data.
static bool LoadClm() {
uint32_t size = 0;
uint8_t* blob = BrcmfLoadFile("clm_blob", size, false);
if (!blob) {
KernelLogStream(WARNING, "WiFi") << "No CLM blob; the firmware's own regulatory data is used";
return true;
}
constexpr uint32_t CHUNK = 1400;
constexpr uint32_t HDR = 12;
uint8_t* chunk = (uint8_t*)Memory::g_heap->Request(HDR + CHUNK + 4);
bool ok = chunk != nullptr;
uint32_t off = 0;
uint16_t flag = 1u << 12 | 0x0002;
while (ok && off < size) {
uint32_t n = size - off > CHUNK ? CHUNK : size - off;
if (off + n == size) flag |= 0x0004;
memset(chunk, 0, HDR + CHUNK + 4);
Put16(chunk, flag);
Put16(chunk + 2, 2);
Put32(chunk + 4, n);
Put32(chunk + 8, 0);
memcpy(chunk + HDR, blob + off, n);
// Linux sends sizeof(struct) + len - 1: three bytes of padding.
ok = BrcmfIovarSet("clmload", chunk, HDR + n + 3);
flag &= (uint16_t)~0x0002;
off += n;
}
if (chunk) Memory::g_heap->Free(chunk);
Memory::g_heap->Free(blob);
uint32_t status = 0;
if (ok && BrcmfIovarGetInt("clmload_status", status, true) && status != 0) {
KernelLogStream(ERROR, "WiFi") << "The firmware rejected the CLM blob (status "
<< (uint64_t)status << ")";
ok = false;
}
if (ok) KernelLogStream(INFO, "WiFi") << "CLM blob loaded (" << (uint64_t)size << " bytes)";
return ok;
}
static void SetEventMask() {
uint8_t mask[16] = {};
const uint32_t events[] = {
E_SET_SSID, E_AUTH, E_DEAUTH, E_DEAUTH_IND, E_ASSOC, E_REASSOC, E_DISASSOC,
E_DISASSOC_IND, E_LINK, E_ROAM, E_PSK_SUP, E_ESCAN_RESULT,
};
for (uint32_t e : events) mask[e / 8] |= (uint8_t)(1u << (e % 8));
BrcmfIovarSet("event_msgs", mask, sizeof(mask));
}
// "Version: 7.45.265 (28bca26 CY) CRC: ..." -> "7.45.265"
static void ParseVersion(const char* ver) {
const char* p = ver;
const char* key = "version ";
while (*p) {
const char* a = p;
const char* b = key;
while (*a && *b && (*a | 0x20) == *b) { a++; b++; }
if (!*b) { p = a; break; }
p++;
}
if (!*p) p = ver;
int n = 0;
while (p[n] && p[n] != ' ' && p[n] != '\n' && n < 31) {
g_brcmf.FwVersion[n] = p[n];
n++;
}
g_brcmf.FwVersion[n] = '\0';
}
static void ReadChannels() {
uint32_t list[65] = {};
list[0] = 64;
g_brcmf.ChannelCount = 0;
if (!BrcmfIoctl(WLC_GET_VALID_CHANNELS, false, list, sizeof(list))) return;
uint32_t count = list[0] > 64 ? 64 : list[0];
bool b24 = false, b5 = false;
for (uint32_t i = 0; i < count; i++) {
if (list[1 + i] > 14) b5 = true;
else if (list[1 + i] > 0) b24 = true;
}
g_brcmf.ChannelCount = (uint16_t)count;
g_brcmf.Band24 = b24;
g_brcmf.Band5 = b5;
}
static bool Preinit() {
uint8_t mac[6] = {};
if (!BrcmfIovarGet("cur_etheraddr", mac, 6)) return false;
if (memcmp(mac, NVRAM_PLACEHOLDER_MAC, 6) == 0) {
// Raspberry Pi convention: wlan0 is the board (Ethernet) address
// plus one.
uint8_t board[6];
if (Drivers::Platform::RpiFirmware::GetBoardMacAddress(board)) {
memcpy(mac, board, 6);
for (int i = 5; i >= 3; i--) if (++mac[i] != 0) break;
BrcmfIovarSet("cur_etheraddr", mac, 6);
}
}
memcpy(g_brcmf.Mac, mac, 6);
char ver[256] = {};
if (BrcmfIovarGet("ver", ver, sizeof(ver) - 1)) {
ver[sizeof(ver) - 1] = '\0';
for (char& c : ver) if (c == '\n' || c == '\r') c = ' ';
KernelLogStream(INFO, "WiFi") << "Firmware: " << ver;
ParseVersion(ver);
}
if (!LoadClm()) return false;
// No glomming in either direction: one SDPCM frame per transfer.
BrcmfIovarSetInt("bus:txglom", 0, true);
BrcmfIovarSetInt("bus:rxglom", 0, true);
BrcmfSetInt(WLC_SET_INFRA, 1);
// Roaming would re-key without the host's supplicant; ARP/ND offload
// would answer for an address the firmware was never told.
BrcmfIovarSetInt("roam_off", 1, true);
BrcmfIovarSetInt("arpoe", 0, true);
BrcmfIovarSetInt("ndoe", 0, true);
// Power save off: the radio misses broadcasts (ARP, DHCP offers)
// while dozing, and the Pi is not on a battery.
BrcmfSetInt(WLC_SET_PM, 0);
SetEventMask();
if (!BrcmfSetInt(WLC_UP, 1)) {
KernelLogStream(ERROR, "WiFi") << "The firmware would not bring the interface up";
return false;
}
ReadChannels();
return true;
}
static void ResetConnection() {
g_conn = WIFI_CONN_IDLE;
g_evAssoc = false;
g_evJoinFailed = false;
g_evLinkDown = false;
g_secure = false;
g_sawEapol = false;
g_eapolLock.Acquire();
g_eapolHead = g_eapolTail = 0;
g_eapolLock.Release();
WpaReset();
}
bool BrcmfProbe(uint64_t arasanMmio) {
if (g_brcmf.State != BrcmfState::Absent) return false;
g_brcmf.ArasanMmio = arasanMmio;
g_brcmf.State = BrcmfState::Detected;
return true;
}
bool BrcmfCompleteInit() {
g_brcmf.State = BrcmfState::Booting;
g_failureLogged = false;
g_dumpConsole = false;
g_scanActive = false;
g_brcmf.CmdFailBucket = 0;
ResetConnection();
BrcmfProtoReset();
// One retry from power-off: the Bluetooth half of the same chip may
// be powering up or taking its patch download at the same moment.
bool up = BrcmfChipBringUp();
if (!up) {
KernelLogStream(WARNING, "WiFi") << "Bring-up failed; power-cycling the radio and trying once more";
Timekeeping::BusyWaitMs(100);
up = BrcmfChipBringUp();
}
if (!up) {
g_brcmf.State = BrcmfState::Error;
return false;
}
// Running from here: the preinit below is the first SDPCM traffic.
g_brcmf.LastPollMs = Timekeeping::GetMilliseconds();
g_brcmf.State = BrcmfState::Running;
BrcmfDumpConsole();
if (!Preinit()) {
KernelLogStream(ERROR, "WiFi") << "Firmware initialization failed";
BrcmfDumpConsole();
g_brcmf.State = BrcmfState::Error;
BrcmfChipPowerOff();
return false;
}
BrcmfDumpConsole();
auto log = KernelLogStream(OK, "WiFi");
log << "CYW" << g_brcmf.FwName << " ready: firmware " << g_brcmf.FwVersion << ", "
<< (uint64_t)g_brcmf.ChannelCount << " channels, MAC ";
PutMac(log, g_brcmf.Mac);
return true;
}
void BrcmfShutdown() {
g_scanActive = false;
ResetConnection();
BrcmfChipPowerOff();
g_brcmf.State = BrcmfState::Detected;
}
BrcmfState BrcmfGetState() { return g_brcmf.State; }
// =========================================================================
// Scanning
// =========================================================================
bool BrcmfScanActive() { return g_scanActive; }
bool BrcmfStartScan() {
if (g_brcmf.State != BrcmfState::Running) return false;
if (g_scanActive) return true;
// brcmf_escan_params_le: version 1, action 1 (start), sync id, then
// brcmf_scan_params_le: SSID (any), BSSID (broadcast), BSS type any,
// active scan, firmware defaults for every dwell, all channels.
uint8_t p[72] = {};
Put32(p, 1);
Put16(p + 4, 1);
Put16(p + 6, ++g_syncId);
memset(p + 44, 0xFF, 6);
p[50] = 2;
p[51] = 0;
Put32(p + 52, 0xFFFFFFFF);
Put32(p + 56, 0xFFFFFFFF);
Put32(p + 60, 0xFFFFFFFF);
Put32(p + 64, 0xFFFFFFFF);
Put32(p + 68, 0);
g_scanSeen = 0;
g_scanStartMs = Timekeeping::GetMilliseconds();
g_scanActive = true; // results may arrive while the ioctl is pumping
if (!BrcmfIovarSet("escan", p, sizeof(p))) {
g_scanActive = false;
return false;
}
return true;
}
void BrcmfAbortScan() {
if (!g_scanActive) return;
// A WLC_SCAN naming channel -1 cancels the scan in progress.
uint8_t p[68] = {};
memset(p + 36, 0xFF, 6);
p[42] = 2;
Put32(p + 44, 0xFFFFFFFF);
Put32(p + 48, 0xFFFFFFFF);
Put32(p + 52, 0xFFFFFFFF);
Put32(p + 56, 0xFFFFFFFF);
Put32(p + 60, 1);
Put16(p + 64, 0xFFFF);
BrcmfIoctl(WLC_SCAN, true, p, sizeof(p));
g_scanActive = false;
}
// =========================================================================
// Joining
// =========================================================================
int BrcmfConnectState() { return (int)g_conn; }
bool BrcmfConnectRefusedForSecurity() { return g_refusedSecurity; }
bool BrcmfJoinFailedOnAuth() { return g_authFailed || WpaGetState() == WpaState::Failed; }
const char* BrcmfConnectSsid() { return g_ssid; }
const uint8_t* BrcmfConnectBssid() { return g_bssid; }
uint8_t BrcmfConnectChannel() { return g_channel; }
bool BrcmfLinkUp() { return g_brcmf.State == BrcmfState::Running && g_conn == WIFI_CONN_CONNECTED; }
static void Disassociate() {
uint8_t scb[12] = {};
Put32(scb, 3); // deauthenticated: leaving
memcpy(scb + 4, g_bssid, 6);
BrcmfIoctl(WLC_DISASSOC, true, scb, sizeof(scb));
}
void BrcmfConnectAbort() {
if (g_conn != WIFI_CONN_IDLE && g_brcmf.State == BrcmfState::Running) Disassociate();
ResetConnection();
}
bool BrcmfConnectStart(const uint8_t* bssid, uint8_t channel, const char* ssid,
const char* password, const uint8_t* rsnIe, uint8_t rsnIeLen) {
if (g_brcmf.State != BrcmfState::Running) return false;
if (g_conn != WIFI_CONN_IDLE) BrcmfConnectAbort();
g_refusedSecurity = false;
g_authFailed = false;
memcpy(g_bssid, bssid, 6);
g_channel = channel;
g_ssidLen = 0;
while (ssid && g_ssidLen < 32 && ssid[g_ssidLen]) {
g_ssid[g_ssidLen] = ssid[g_ssidLen];
g_ssidLen++;
}
g_ssid[g_ssidLen] = '\0';
bool secure = rsnIe && rsnIeLen > 0;
uint8_t ie[64];
uint32_t ieLen = 0;
if (secure) {
memset(&g_wpaCfg, 0, sizeof(g_wpaCfg));
if (!WpaParseApRsn(rsnIe, rsnIeLen, g_wpaCfg)) {
g_refusedSecurity = true;
return false;
}
if (g_wpaCfg.PairwiseCipher != RSN_CIPHER_CCMP || g_wpaCfg.GroupCipher != RSN_CIPHER_CCMP) {
KernelLogStream(WARNING, "WiFi") << "This radio does AES-CCMP only; the network uses GCMP";
g_refusedSecurity = true;
return false;
}
uint32_t pl = 0;
if (password) while (pl < sizeof(g_wpaCfg.Passphrase) - 1 && password[pl]) pl++;
if (pl == 0) {
KernelLogStream(WARNING, "WiFi") << "This network is encrypted and needs a passphrase";
g_refusedSecurity = true;
return false;
}
memcpy(g_wpaCfg.Passphrase, password, pl);
g_wpaCfg.Passphrase[pl] = '\0';
g_wpaCfg.PassLen = (uint8_t)pl;
memcpy(g_wpaCfg.OwnMac, g_brcmf.Mac, 6);
memcpy(g_wpaCfg.Bssid, g_bssid, 6);
memcpy(g_wpaCfg.Ssid, g_ssid, g_ssidLen);
g_wpaCfg.SsidLen = g_ssidLen;
if (!WpaStart(g_wpaCfg)) {
g_refusedSecurity = true;
return false;
}
ieLen = WpaBuildRsnIe(ie, sizeof(ie));
if (ieLen == 0) return false;
}
if (g_scanActive) BrcmfAbortScan();
bool sha256 = secure && g_wpaCfg.Akm == RSN_AKM_PSK_SHA256;
if (secure) BrcmfIovarSet("wpaie", ie, ieLen);
else BrcmfIovarSet("wpaie", nullptr, 0, true);
bool ok = BrcmfIovarSetInt("wpa_auth", secure ? (sha256 ? WPA2_AUTH_PSK_SHA256 : WPA2_AUTH_PSK)
: WPA_AUTH_DISABLED)
&& BrcmfIovarSetInt("auth", 0)
&& BrcmfIovarSetInt("wsec", secure ? AES_ENABLED : 0);
BrcmfIovarSetInt("mfp", 0, true);
BrcmfIovarSetInt("sup_wpa", 0, true);
if (!ok) {
WpaReset();
return false;
}
uint16_t chanspec = LookupChanspec(g_bssid, channel);
g_secure = secure;
g_evAssoc = false;
g_evJoinFailed = false;
g_evLinkDown = false;
g_joinStartMs = Timekeeping::GetMilliseconds();
g_conn = WIFI_CONN_ASSOCIATING; // before "join": its events may land while it pumps
// brcmf_ext_join_params_le: SSID; join scan (type default, probes
// and dwells as Linux uses for a known channel); BSSID, one chanspec.
uint8_t jp[72] = {};
Put32(jp, g_ssidLen);
memcpy(jp + 4, g_ssid, g_ssidLen);
jp[36] = 0xFF;
Put32(jp + 40, 16);
Put32(jp + 44, 320);
Put32(jp + 48, 400);
Put32(jp + 52, 0xFFFFFFFF);
memcpy(jp + 56, g_bssid, 6);
Put32(jp + 64, 1);
Put16(jp + 68, chanspec);
if (!BrcmfIovarSet("join", jp, sizeof(jp), true)) {
// Older firmware: WLC_SET_SSID with brcmf_join_params_le.
uint8_t sp[52] = {};
Put32(sp, g_ssidLen);
memcpy(sp + 4, g_ssid, g_ssidLen);
memcpy(sp + 36, g_bssid, 6);
Put32(sp + 44, 1);
Put16(sp + 48, chanspec);
if (!BrcmfIoctl(WLC_SET_SSID, true, sp, sizeof(sp))) {
KernelLogStream(ERROR, "WiFi") << "The firmware refused the join request";
ResetConnection();
return false;
}
}
auto log = KernelLogStream(INFO, "WiFi");
log << "Joining \"" << g_ssid << "\" (";
PutMac(log, g_bssid);
log << ", channel " << (uint64_t)channel << ", chanspec 0x" << base::hex << (uint64_t)chanspec
<< base::dec << (secure ? ", WPA2-PSK)" : ", open)");
return true;
}
// =========================================================================
// Keys and EAPOL (the supplicant's hooks)
// =========================================================================
bool BrcmfWpaTxEapol(const uint8_t* body, uint32_t len) {
uint8_t frame[14 + 512];
if (len > 512) return false;
memcpy(frame, g_bssid, 6);
memcpy(frame + 6, g_brcmf.Mac, 6);
frame[12] = ETHERTYPE_EAPOL >> 8;
frame[13] = ETHERTYPE_EAPOL & 0xFF;
memcpy(frame + 14, body, len);
return BrcmfTxEthernet(frame, 14 + len);
}
// brcmf_wsec_key_le: index, len, data[32], pad[18], algo, flags, pad[3],
// iv_initialized, pad, rxiv {hi, lo}, pad[2], ea[6] -- 164 bytes.
static bool SendKey(uint32_t index, const uint8_t* key, uint32_t keyLen, uint32_t flags,
const uint8_t* ea, const uint8_t* rsc) {
if (keyLen > 32) return false;
uint8_t k[164] = {};
Put32(k, index);
Put32(k + 4, keyLen);
memcpy(k + 8, key, keyLen);
Put32(k + 112, CRYPTO_ALGO_AES_CCM);
Put32(k + 116, flags);
if (rsc) {
Put32(k + 132, 1);
Put32(k + 140, (uint32_t)rsc[2] | (uint32_t)rsc[3] << 8 | (uint32_t)rsc[4] << 16
| (uint32_t)rsc[5] << 24);
Put16(k + 144, (uint16_t)(rsc[0] | rsc[1] << 8));
}
if (ea) memcpy(k + 156, ea, 6);
bool ok = BrcmfIovarSet("wsec_key", k, sizeof(k));
Crypto::SecureZero(k, sizeof(k));
return ok;
}
// Message 4 is still in the transmit queue when the supplicant installs
// the pairwise key; it has to reach the radio first or it goes out
// encrypted and the access point never sees it (Linux waits for its
// pending 802.1X frames the same way).
static void DrainTx() {
uint64_t start = Timekeeping::GetMilliseconds();
while (BrcmfTxPending() && Timekeeping::GetMilliseconds() - start < 200) {
BrcmfPoll();
Timekeeping::BusyWaitUs(200);
}
Timekeeping::BusyWaitMs(10);
}
bool BrcmfWpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
if (cipher != RSN_CIPHER_CCMP) return false;
DrainTx();
return SendKey(0, tk, tkLen, 0, g_bssid, nullptr);
}
bool BrcmfWpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
uint8_t cipher, const uint8_t* rsc) {
if (cipher != RSN_CIPHER_CCMP) return false;
DrainTx();
return SendKey(keyIdx, gtk, gtkLen, KEY_FLAG_PRIMARY, nullptr, rsc);
}
// =========================================================================
// Service (idle loop / syscall waits: may send ioctls)
// =========================================================================
static void ConnectService() {
static volatile bool inService = false;
if (__atomic_test_and_set(&inService, __ATOMIC_ACQUIRE)) return;
struct Guard { ~Guard() { __atomic_clear(&inService, __ATOMIC_RELEASE); } } guard;
uint64_t now = Timekeeping::GetMilliseconds();
g_lastServiceMs = now;
if (g_scanActive && Elapsed(now, g_scanStartMs, SCAN_TIMEOUT_MS)) {
KernelLogStream(WARNING, "WiFi") << "Scan never finished; abandoning it";
BrcmfAbortScan();
}
if (g_conn == WIFI_CONN_ASSOCIATING) {
if (g_evJoinFailed) {
g_evJoinFailed = false;
KernelLogStream(WARNING, "WiFi") << "Could not join \"" << g_ssid << "\" (status "
<< (uint64_t)g_evStatus << (g_evStatus == 3 ? ": network not found)" : ")");
g_conn = WIFI_CONN_FAILED;
} else if (g_evAssoc) {
g_evAssoc = false;
if (g_secure) {
KernelLogStream(OK, "WiFi") << "Associated with \"" << g_ssid << "\"; starting the key handshake";
g_conn = WIFI_CONN_HANDSHAKING;
} else {
KernelLogStream(OK, "WiFi") << "Connected to \"" << g_ssid << "\" (open network)";
g_conn = WIFI_CONN_CONNECTED;
}
} else if (Elapsed(Timekeeping::GetMilliseconds(), g_joinStartMs, JOIN_TIMEOUT_MS)) {
KernelLogStream(WARNING, "WiFi") << "Timed out joining \"" << g_ssid << "\"";
g_conn = WIFI_CONN_FAILED;
}
}
if (g_evLinkDown && g_conn >= WIFI_CONN_ASSOCIATED && g_conn != WIFI_CONN_FAILED) {
g_evLinkDown = false;
if (g_conn == WIFI_CONN_HANDSHAKING) {
// The access point gives up on a handshake whose MICs do not
// verify: a wrong passphrase, nearly always.
KernelLogStream(WARNING, "WiFi") << "The access point dropped the connection during the key "
"handshake (reason " << (uint64_t)g_evReason << "); check the passphrase";
g_authFailed = g_sawEapol;
g_conn = WIFI_CONN_FAILED;
} else {
KernelLogStream(WARNING, "WiFi") << "Disconnected from \"" << g_ssid << "\" (reason "
<< (uint64_t)g_evReason << ")";
ResetConnection();
}
}
if (g_conn == WIFI_CONN_HANDSHAKING || g_conn == WIFI_CONN_CONNECTED) {
uint8_t body[512];
for (;;) {
uint32_t len = 0;
g_eapolLock.Acquire();
if (g_eapolHead != g_eapolTail) {
len = g_eapolQ[g_eapolTail].Len;
memcpy(body, g_eapolQ[g_eapolTail].Data, len);
g_eapolTail = (g_eapolTail + 1) % EAPOL_DEPTH;
}
g_eapolLock.Release();
if (!len) break;
g_sawEapol = true;
WpaOnEapol(body, len);
}
}
if (g_conn == WIFI_CONN_HANDSHAKING) {
WpaService(Timekeeping::GetMilliseconds());
if (WpaIsComplete()) {
KernelLogStream(OK, "WiFi") << "Connected to \"" << g_ssid << "\"";
g_conn = WIFI_CONN_CONNECTED;
} else if (WpaGetState() == WpaState::Failed) {
g_authFailed = true;
g_conn = WIFI_CONN_FAILED;
}
}
}
bool BrcmfHasWork(uint64_t now) {
if (g_brcmf.State == BrcmfState::Error) return g_dumpConsole;
if (g_brcmf.State != BrcmfState::Running) return false;
bool active = g_scanActive || (g_conn != WIFI_CONN_IDLE && g_conn != WIFI_CONN_FAILED);
if (BrcmfPollDue(now, active ? POLL_ACTIVE_MS : POLL_IDLE_MS)) return true;
if (g_evAssoc || g_evJoinFailed || g_evLinkDown || EapolPending()) return true;
// Deadlines and the supplicant's retransmissions, even with the air quiet.
if ((g_conn == WIFI_CONN_ASSOCIATING || g_conn == WIFI_CONN_HANDSHAKING || g_scanActive)
&& Elapsed(now, g_lastServiceMs, 50))
return true;
return false;
}
void BrcmfService() {
if (g_brcmf.State == BrcmfState::Error) {
if (g_dumpConsole) {
g_dumpConsole = false;
BrcmfDumpConsole();
}
return;
}
if (g_brcmf.State != BrcmfState::Running) return;
BrcmfPoll();
ConnectService();
}
bool BrcmfSendEthernet(const uint8_t* frame, uint32_t len) {
if (!BrcmfLinkUp()) return false;
return BrcmfTxEthernet(frame, len);
}
}
+592
View File
@@ -0,0 +1,592 @@
/*
* BrcmfProto.cpp
* Broadcom FullMAC over SDIO: SDPCM framing, CDC ioctls, BDC data frames,
* firmware events, the RX pump and the transmit queue
* Derived in part from Linux's brcmfmac, Copyright (c) 2010-2014 Broadcom
* Corporation, ISC license (notice in programs/data/licenses/NOTICES.txt).
* Copyright (c) 2026 Daniel Hammer
*/
#include "Brcmf.hpp"
#include "BrcmfSdio.hpp"
#include <Arch/Cpu.hpp>
#include <CppLib/Stream.hpp>
#include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp>
#include <Timekeeping/Timer.hpp>
#include <atomic>
// Every frame on function 2 starts with the SDPCM header (Linux sdio.c):
//
// hardware u16 len (the whole frame), u16 ~len
// software u8 seq, u8 channel (low nibble; bit 7 = glom descriptor),
// u8 next length, u8 data offset (from the frame start),
// u8 flow control bitmap, u8 max seq (TX credit, inbound),
// u16 reserved
//
// Channel 0 carries ioctls and their answers behind a 16-byte CDC header
// (cmd, len, flags: id [31:16] | interface [15:12] | set [1] | error [0],
// status). Channels 1 (events) and 2 (data) carry Ethernet frames behind a
// 4-byte BDC header (flags: version 2 in [7:4]; priority; flags2: interface;
// data offset in dwords to skip). Events are Ethernet frames of type 0x886C
// with Broadcom's OUI.
//
// Transmit credit: the device's max seq bounds what may be sent, every frame
// (ioctls included) takes the next sequence number.
using namespace Kt;
namespace Drivers::Net::Wifi {
static constexpr uint32_t SDPCM_HDRLEN = 12;
static constexpr uint8_t CHAN_CONTROL = 0;
static constexpr uint8_t CHAN_EVENT = 1;
static constexpr uint8_t CHAN_DATA = 2;
static constexpr uint8_t CHAN_GLOM = 3;
static constexpr uint8_t GLOM_DESC = 0x80;
static constexpr uint32_t CDC_HDRLEN = 16;
static constexpr uint32_t CDCF_ERROR = 0x01;
static constexpr uint32_t CDCF_SET = 0x02;
static constexpr uint32_t BDC_HDRLEN = 4;
static constexpr uint8_t BDC_VERSION = 2;
static constexpr uint16_t ETHERTYPE_BRCM = 0x886C;
// SDIO device core
static constexpr uint32_t SD_INTSTATUS = 0x20;
static constexpr uint32_t SD_TOSBMAILBOX = 0x40;
static constexpr uint32_t SD_TOHOSTMAILBOXDATA = 0x4C;
static constexpr uint32_t I_HMB_FRAME_IND = 1u << 6;
static constexpr uint32_t I_HMB_HOST_INT = 1u << 7;
static constexpr uint32_t SMB_INT_ACK = 1u << 1;
static constexpr uint32_t HMB_DATA_FWHALT = 0x10;
static constexpr uint32_t F1_FRAMECTRL = 0x1000D;
static constexpr uint32_t F1_RFRAMEBCLO = 0x1001B;
static constexpr uint32_t F1_RFRAMEBCHI = 0x1001C;
static constexpr uint8_t SFC_RF_TERM = 0x01;
// Function 2's FIFO; the address is ChipCommon's window | 4-byte access.
static constexpr uint32_t F2_ADDR = 0x8000;
static constexpr uint32_t SI_ENUM_BASE = 0x18000000;
static constexpr uint32_t RX_MAX = 8192;
static constexpr uint32_t CTRL_MAX = 2048; // ioctl payload
static constexpr uint32_t TX_SLOT = 2048; // padded data frame
static constexpr uint32_t TXQ_DEPTH = 32;
static constexpr uint64_t CREDIT_TIMEOUT_MS = 1000;
static constexpr uint64_t IOCTL_TIMEOUT_MS = 2000;
static constexpr uint32_t FAIL_BUCKET_LIMIT = 3;
static constexpr uint32_t BUS_ERROR_LIMIT = 16;
static uint32_t g_rxBuf[RX_MAX / 4 + 16];
static volatile bool g_pumping = false;
static uint32_t g_busErrors = 0;
static bool g_glomWarned = false;
struct TxSlot {
uint32_t Len; // bytes on the bus (padded)
uint32_t Data[TX_SLOT / 4];
};
static TxSlot g_txq[TXQ_DEPTH];
static uint32_t g_txHead = 0, g_txTail = 0;
static kcp::Spinlock g_txqLock;
// The outstanding ioctl, if any. The pump on whichever core finds the
// answer copies it here under g_respLock; the caller copies it out.
static kcp::Mutex g_ctrlLock;
static kcp::Spinlock g_respLock;
// Room for the largest ioctl padded to whole blocks.
static uint32_t g_ctrlTx[(SDPCM_HDRLEN + CDC_HDRLEN + CTRL_MAX + Sdio::FN2_BLOCK - 1)
/ Sdio::FN2_BLOCK * Sdio::FN2_BLOCK / 4];
static uint8_t g_ctrlResp[CTRL_MAX];
static uint32_t g_ctrlRespLen = 0;
static uint16_t g_reqId = 0;
static uint16_t g_waitId = 0; // 0 = nobody waiting
static int32_t g_ctrlStatus = 0;
static uint32_t g_ctrlFlags = 0;
static std::atomic<bool> g_ctrlDone{false};
static inline uint16_t Be16(const uint8_t* p) { return (uint16_t)(p[0] << 8 | p[1]); }
static inline uint32_t Be32(const uint8_t* p) {
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3];
}
static inline void Le32(uint8_t* p, uint32_t v) { memcpy(p, &v, 4); }
void BrcmfProtoReset() {
g_brcmf.TxSeq = 0;
g_brcmf.TxMax = 1;
g_brcmf.RxPending = false;
g_txqLock.Acquire();
g_txHead = g_txTail = 0;
g_txqLock.Release();
g_respLock.Acquire();
g_waitId = 0;
g_respLock.Release();
g_busErrors = 0;
g_glomWarned = false;
}
// =========================================================================
// Bus transfers (bus lock held)
// =========================================================================
static bool CanSend() {
uint8_t room = (uint8_t)(g_brcmf.TxMax - g_brcmf.TxSeq);
return room != 0 && (room & 0x80) == 0;
}
static void NoteBusError() {
if (++g_busErrors == BUS_ERROR_LIMIT) BrcmfOnFirmwareFailure("SDIO bus errors");
}
static bool SendFrameLocked(uint8_t* frame, uint32_t busLen) {
frame[4] = g_brcmf.TxSeq;
if (!BrcmfBpSelectWindow(SI_ENUM_BASE) ||
!Sdio::WriteBlock(Sdio::FN2, F2_ADDR, false, frame, busLen)) {
NoteBusError();
return false;
}
g_busErrors = 0;
g_brcmf.TxSeq++;
return true;
}
// Bytes on the bus for a frame: dwords, and whole blocks once it is
// longer than one (the device takes the length from the header).
static uint32_t BusLength(uint32_t len) {
if (len > Sdio::FN2_BLOCK)
return (len + Sdio::FN2_BLOCK - 1) / Sdio::FN2_BLOCK * Sdio::FN2_BLOCK;
return (len + 3) & ~3u;
}
static void PutSdpcm(uint8_t* f, uint32_t len, uint8_t channel) {
uint16_t l = (uint16_t)len;
f[0] = (uint8_t)l; f[1] = (uint8_t)(l >> 8);
f[2] = (uint8_t)~l; f[3] = (uint8_t)(~l >> 8);
f[4] = 0; // seq, filled in at send time
f[5] = channel;
f[6] = 0;
f[7] = SDPCM_HDRLEN;
f[8] = f[9] = f[10] = f[11] = 0;
}
// Abandon a frame half read: abort function 2, have the device drop the
// rest of it, and wait for its read FIFO to empty (Linux rxfail).
static void RxFailLocked() {
Sdio::Abort(Sdio::FN2);
BrcmfF1Write8(F1_FRAMECTRL, SFC_RF_TERM);
for (int i = 0; i < 100; i++) {
uint8_t hi = 0, lo = 0;
if (!BrcmfF1Read8(F1_RFRAMEBCHI, hi) || !BrcmfF1Read8(F1_RFRAMEBCLO, lo)) break;
if (hi == 0 && lo == 0) break;
}
NoteBusError();
}
// Read one frame into g_rxBuf. 1 = a frame, 0 = none waiting, -1 = error.
static int ReadFrameLocked(uint32_t& len) {
uint8_t* buf = (uint8_t*)g_rxBuf;
if (!BrcmfBpSelectWindow(SI_ENUM_BASE) ||
!Sdio::ReadBlock(Sdio::FN2, F2_ADDR, false, buf, 4)) {
RxFailLocked();
return -1;
}
uint16_t l = (uint16_t)(buf[0] | buf[1] << 8);
uint16_t check = (uint16_t)(buf[2] | buf[3] << 8);
if ((l | check) == 0) return 0;
if ((uint16_t)(l ^ check) != 0xFFFF || l < SDPCM_HDRLEN || l > RX_MAX) {
KernelLogStream(WARNING, "WiFi") << "Bad SDPCM header (len 0x" << base::hex
<< (uint64_t)l << ", check 0x" << (uint64_t)check << base::dec << ")";
RxFailLocked();
return -1;
}
uint32_t rest = ((l + 3u) & ~3u) - 4;
if (rest && !Sdio::ReadBlock(Sdio::FN2, F2_ADDR, false, buf + 4, rest)) {
RxFailLocked();
return -1;
}
g_busErrors = 0;
len = l;
return 1;
}
static void FlushTxLocked() {
for (;;) {
g_txqLock.Acquire();
bool empty = g_txHead == g_txTail;
g_txqLock.Release();
if (empty || !CanSend()) return;
TxSlot& slot = g_txq[g_txTail % TXQ_DEPTH];
if (SendFrameLocked((uint8_t*)slot.Data, slot.Len)) g_brcmf.TxPackets++;
g_txqLock.Acquire();
g_txTail++;
g_txqLock.Release();
}
}
// =========================================================================
// Receive dispatch (bus lock released)
// =========================================================================
static void HandleControl(const uint8_t* p, uint32_t len) {
if (len < CDC_HDRLEN) return;
uint32_t cmd, dlen, flags, status;
memcpy(&cmd, p, 4);
memcpy(&dlen, p + 4, 4);
memcpy(&flags, p + 8, 4);
memcpy(&status, p + 12, 4);
(void)cmd;
(void)dlen;
uint16_t id = (uint16_t)(flags >> 16);
g_respLock.Acquire();
if (g_waitId != 0 && id == g_waitId) {
uint32_t n = len - CDC_HDRLEN;
if (n > CTRL_MAX) n = CTRL_MAX;
memcpy(g_ctrlResp, p + CDC_HDRLEN, n);
g_ctrlRespLen = n;
g_ctrlStatus = (int32_t)status;
g_ctrlFlags = flags;
g_waitId = 0;
g_ctrlDone.store(true, std::memory_order_release);
}
g_respLock.Release();
}
static void HandleEvent(const uint8_t* eth, uint32_t len) {
// Ethernet (14), bcm_ethhdr (10): subtype 0x8001, length, version 1,
// OUI 00:10:18, user subtype 1; then the 48-byte event message.
constexpr uint32_t MSG = 24;
constexpr uint32_t DATA = MSG + 48;
if (len < DATA) return;
const uint8_t* h = eth + 14;
if (Be16(h) != 0x8001 || h[5] != 0x00 || h[6] != 0x10 || h[7] != 0x18 || Be16(h + 8) != 1)
return;
const uint8_t* m = eth + MSG;
uint16_t flags = Be16(m + 2);
uint32_t type = Be32(m + 4);
uint32_t status = Be32(m + 8);
uint32_t reason = Be32(m + 12);
uint32_t dataLen = Be32(m + 20);
const uint8_t* addr = m + 24;
uint8_t ifidx = m[46];
if (ifidx != 0) return;
if (dataLen > len - DATA) dataLen = len - DATA;
BrcmfOnEvent(type, status, reason, flags, addr, eth + DATA, dataLen);
}
static void Dispatch(const uint8_t* f, uint32_t len) {
const uint8_t* sw = f + 4;
uint8_t channel = sw[1] & 0x0F;
uint8_t offset = sw[3];
// Credit: the device's max seq, sanity checked as Linux does.
uint8_t max = sw[5];
if ((uint8_t)(max - g_brcmf.TxSeq) > 0x40) max = (uint8_t)(g_brcmf.TxSeq + 2);
g_brcmf.TxMax = max;
if ((sw[1] & GLOM_DESC) || channel == CHAN_GLOM) {
if (!g_glomWarned) {
g_glomWarned = true;
KernelLogStream(WARNING, "WiFi") << "Dropping a glommed superframe (rx glom is off)";
}
return;
}
if (offset < SDPCM_HDRLEN || offset > len) return;
const uint8_t* p = f + offset;
uint32_t plen = len - offset;
if (plen == 0) return; // credit update only
if (channel == CHAN_CONTROL) {
HandleControl(p, plen);
return;
}
if (channel != CHAN_EVENT && channel != CHAN_DATA) return;
if (plen < BDC_HDRLEN || (p[0] >> 4) != BDC_VERSION) return;
if ((p[2] & 0x0F) != 0) return; // another interface
uint32_t skip = BDC_HDRLEN + (uint32_t)p[3] * 4;
if (skip + 14 > plen) return;
const uint8_t* eth = p + skip;
uint32_t elen = plen - skip;
if (Be16(eth + 12) == ETHERTYPE_BRCM) {
HandleEvent(eth, elen);
return;
}
if (channel == CHAN_DATA) {
g_brcmf.RxPackets++;
BrcmfOnRxEthernet(eth, elen);
}
}
// =========================================================================
// The pump
// =========================================================================
bool BrcmfTxPending() {
g_txqLock.Acquire();
bool pending = g_txHead != g_txTail;
g_txqLock.Release();
return pending;
}
bool BrcmfPollDue(uint64_t now, uint64_t intervalMs) {
if (g_brcmf.State != BrcmfState::Running) return false;
if (g_brcmf.RxPending || BrcmfTxPending()) return true;
if (Sdio::CardInterruptPending()) return true;
uint64_t last = g_brcmf.LastPollMs;
return now < last || now - last >= intervalMs;
}
void BrcmfPoll() {
if (g_brcmf.State != BrcmfState::Running) return;
if (__atomic_test_and_set(&g_pumping, __ATOMIC_ACQUIRE)) return;
struct Guard { ~Guard() { __atomic_clear(&g_pumping, __ATOMIC_RELEASE); } } guard;
const uint32_t sdio = g_brcmf.SdioCoreBase;
for (int round = 0; round < 8 && g_brcmf.State == BrcmfState::Running; round++) {
bool frames = g_brcmf.RxPending;
uint32_t intStatus = 0;
bool halted = false;
g_brcmf.BusLock.Acquire();
if (BrcmfBpRead32(sdio + SD_INTSTATUS, intStatus)) {
intStatus &= g_brcmf.HostIntMask;
if (intStatus) BrcmfBpWrite32(sdio + SD_INTSTATUS, intStatus);
if (intStatus & I_HMB_HOST_INT) {
uint32_t mbox = 0;
BrcmfBpRead32(sdio + SD_TOHOSTMAILBOXDATA, mbox);
BrcmfBpWrite32(sdio + SD_TOSBMAILBOX, SMB_INT_ACK);
halted = (mbox & HMB_DATA_FWHALT) != 0;
}
if (intStatus & I_HMB_FRAME_IND) frames = true;
} else {
NoteBusError();
}
g_brcmf.BusLock.Release();
g_brcmf.LastPollMs = Timekeeping::GetMilliseconds();
if (halted) {
BrcmfOnFirmwareFailure("firmware halted");
return;
}
for (int n = 0; frames && n < 32; n++) {
uint32_t len = 0;
g_brcmf.BusLock.Acquire();
int r = ReadFrameLocked(len);
g_brcmf.BusLock.Release();
if (r <= 0) {
frames = false;
break;
}
Dispatch((const uint8_t*)g_rxBuf, len);
}
g_brcmf.RxPending = frames;
if (BrcmfTxPending()) {
g_brcmf.BusLock.Acquire();
FlushTxLocked();
g_brcmf.BusLock.Release();
}
if (!frames && intStatus == 0) break;
}
}
// =========================================================================
// Transmit
// =========================================================================
bool BrcmfTxEthernet(const uint8_t* frame, uint32_t len) {
if (g_brcmf.State != BrcmfState::Running || len < 14 || len > 1518) return false;
uint32_t total = SDPCM_HDRLEN + BDC_HDRLEN + len;
uint32_t busLen = BusLength(total);
g_txqLock.Acquire();
if (g_txHead - g_txTail >= TXQ_DEPTH) {
g_txqLock.Release();
return false;
}
TxSlot& slot = g_txq[g_txHead % TXQ_DEPTH];
uint8_t* f = (uint8_t*)slot.Data;
PutSdpcm(f, total, CHAN_DATA);
f[12] = BDC_VERSION << 4;
f[13] = 0; // priority: best effort
f[14] = 0; // interface 0
f[15] = 0; // no signalling TLVs
memcpy(f + 16, frame, len);
memset(f + total, 0, busLen - total);
slot.Len = busLen;
g_txHead++;
g_txqLock.Release();
// Never wait for the bus here: the caller may be the network stack
// inside a frame the pump is delivering, or a process that just
// preempted the bus lock's holder. Whoever holds it, or the next
// pump, sends what is queued.
if (g_brcmf.BusLock.TryAcquire()) {
FlushTxLocked();
g_brcmf.BusLock.Release();
}
return true;
}
// =========================================================================
// Control
// =========================================================================
bool BrcmfIoctl(uint32_t cmd, bool set, void* buf, uint32_t len, int32_t* fwStatus) {
if (fwStatus) *fwStatus = 0;
if (g_brcmf.State != BrcmfState::Running || len > CTRL_MAX) return false;
g_ctrlLock.Acquire();
uint16_t id = ++g_reqId;
if (id == 0) id = ++g_reqId;
uint8_t* f = (uint8_t*)g_ctrlTx;
uint32_t total = SDPCM_HDRLEN + CDC_HDRLEN + len;
PutSdpcm(f, total, CHAN_CONTROL);
uint8_t* cdc = f + SDPCM_HDRLEN;
Le32(cdc, cmd);
Le32(cdc + 4, len);
Le32(cdc + 8, (uint32_t)id << 16 | (set ? CDCF_SET : 0));
Le32(cdc + 12, 0);
if (len) memcpy(cdc + CDC_HDRLEN, buf, len);
uint32_t busLen = BusLength(total);
memset(f + total, 0, busLen - total);
g_respLock.Acquire();
g_waitId = id;
g_ctrlDone.store(false, std::memory_order_relaxed);
g_respLock.Release();
// Wait for transmit credit, pumping meanwhile: credit only ever
// arrives in the header of a received frame.
bool sent = false;
uint64_t start = Timekeeping::GetMilliseconds();
while (!sent && g_brcmf.State == BrcmfState::Running) {
g_brcmf.BusLock.Acquire();
if (CanSend()) {
sent = SendFrameLocked(f, busLen);
if (!sent) {
g_brcmf.BusLock.Release();
break;
}
}
g_brcmf.BusLock.Release();
if (sent) break;
if (Timekeeping::GetMilliseconds() - start > CREDIT_TIMEOUT_MS) break;
BrcmfPoll();
Timekeeping::BusyWaitUs(100);
}
bool done = false;
if (sent) {
start = Timekeeping::GetMilliseconds();
while (g_brcmf.State == BrcmfState::Running) {
if (g_ctrlDone.load(std::memory_order_acquire)) { done = true; break; }
if (Timekeeping::GetMilliseconds() - start > IOCTL_TIMEOUT_MS) break;
BrcmfPoll();
if (g_ctrlDone.load(std::memory_order_acquire)) { done = true; break; }
Timekeeping::BusyWaitUs(50);
}
}
g_respLock.Acquire();
g_waitId = 0;
bool ok = false;
if (done) {
int32_t status = g_ctrlStatus;
if (fwStatus) *fwStatus = status;
ok = !(g_ctrlFlags & CDCF_ERROR) && status >= 0;
if (!set && len) {
uint32_t n = g_ctrlRespLen < len ? g_ctrlRespLen : len;
memcpy(buf, g_ctrlResp, n);
}
}
g_respLock.Release();
if (done) {
if (g_brcmf.CmdFailBucket) g_brcmf.CmdFailBucket--;
} else if (g_brcmf.State == BrcmfState::Running) {
KernelLogStream(WARNING, "WiFi") << "Firmware command " << (uint64_t)cmd
<< (sent ? " went unanswered" : " could not be sent (no transmit credit)");
if (++g_brcmf.CmdFailBucket >= FAIL_BUCKET_LIMIT)
BrcmfOnFirmwareFailure("firmware stopped answering commands");
}
g_ctrlLock.Release();
return ok;
}
static constexpr uint32_t WLC_GET_VAR = 262;
static constexpr uint32_t WLC_SET_VAR = 263;
static kcp::Mutex g_iovarLock;
static uint8_t g_iovarBuf[CTRL_MAX];
static uint32_t NameLen(const char* name) {
uint32_t n = 0;
while (name[n]) n++;
return n + 1;
}
bool BrcmfIovarSet(const char* name, const void* data, uint32_t len, bool quiet) {
uint32_t nameLen = NameLen(name);
if (nameLen + len > CTRL_MAX) return false;
g_iovarLock.Acquire();
memcpy(g_iovarBuf, name, nameLen);
if (len) memcpy(g_iovarBuf + nameLen, data, len);
int32_t status = 0;
bool ok = BrcmfIoctl(WLC_SET_VAR, true, g_iovarBuf, nameLen + len, &status);
g_iovarLock.Release();
if (!ok && !quiet)
KernelLogStream(WARNING, "WiFi") << "Setting \"" << name << "\" failed (status "
<< (int)status << ")";
return ok;
}
bool BrcmfIovarGet(const char* name, void* out, uint32_t len, bool quiet) {
uint32_t nameLen = NameLen(name);
uint32_t bufLen = nameLen > len ? nameLen : len;
if (bufLen > CTRL_MAX) return false;
g_iovarLock.Acquire();
memset(g_iovarBuf, 0, bufLen);
memcpy(g_iovarBuf, name, nameLen);
int32_t status = 0;
bool ok = BrcmfIoctl(WLC_GET_VAR, false, g_iovarBuf, bufLen, &status);
if (ok) memcpy(out, g_iovarBuf, len);
g_iovarLock.Release();
if (!ok && !quiet)
KernelLogStream(WARNING, "WiFi") << "Reading \"" << name << "\" failed (status "
<< (int)status << ")";
return ok;
}
bool BrcmfIovarSetInt(const char* name, uint32_t value, bool quiet) {
return BrcmfIovarSet(name, &value, 4, quiet);
}
bool BrcmfIovarGetInt(const char* name, uint32_t& value, bool quiet) {
return BrcmfIovarGet(name, &value, 4, quiet);
}
bool BrcmfSetInt(uint32_t cmd, uint32_t value) {
return BrcmfIoctl(cmd, true, &value, 4);
}
bool BrcmfGetInt(uint32_t cmd, uint32_t& value) {
uint32_t v = 0;
if (!BrcmfIoctl(cmd, false, &v, 4)) return false;
value = v;
return true;
}
}
+436
View File
@@ -0,0 +1,436 @@
/*
* BrcmfSdio.cpp
* SDIO host on the Raspberry Pi 4/400's Arasan SD controller
* Copyright (c) 2026 Daniel Hammer
*/
#include "BrcmfSdio.hpp"
#include <Arch/Cpu.hpp>
#include <CppLib/Stream.hpp>
#include <Libraries/Memory.hpp>
#include <Memory/HHDM.hpp>
#include <Memory/Paging.hpp>
#include <Terminal/Terminal.hpp>
#include <Timekeeping/Timer.hpp>
// Register access follows Drivers/Storage/Sdhci: the Broadcom/Arasan
// controller only takes 32-bit accesses, so 8/16-bit fields are written by
// read-modify-write of their dword and the transfer-mode/command pair in
// the one write that issues the command. Linux (sdhci-iproc, bcm2835 data)
// also waits four SD clocks after every write while the bus runs at 400 kHz
// or below; so does this.
//
// SDIO commands (SDIO Simplified Specification 3.00):
// CMD5 IO_SEND_OP_COND, R4: OCR [23:0], ready [31]
// CMD52 IO_RW_DIRECT, R5: flags [15:8], data [7:0]
// arg: R/W [31], function [30:28], RAW [27], address [25:9], data [7:0]
// CMD53 IO_RW_EXTENDED, R5
// arg: R/W [31], function [30:28], block mode [27], incrementing
// address [26], address [25:9], count [8:0] (0 = 512)
using namespace Kt;
namespace Drivers::Net::Wifi::Sdio {
static constexpr uint32_t REG_BLOCK_SIZE_COUNT = 0x04;
static constexpr uint32_t REG_ARGUMENT = 0x08;
static constexpr uint32_t REG_XFER_MODE_CMD = 0x0C;
static constexpr uint32_t REG_RESPONSE = 0x10;
static constexpr uint32_t REG_BUFFER_DATA = 0x20;
static constexpr uint32_t REG_PRESENT_STATE = 0x24;
static constexpr uint32_t REG_HOST_CONTROL = 0x28;
static constexpr uint32_t REG_CLOCK_CONTROL = 0x2C;
static constexpr uint32_t REG_INT_STATUS = 0x30;
static constexpr uint32_t REG_INT_ENABLE = 0x34;
static constexpr uint32_t REG_INT_SIGNAL = 0x38;
static constexpr uint32_t REG_CAPABILITIES = 0x40;
static constexpr uint32_t REG_VERSION = 0xFC;
static constexpr uint32_t PS_CMD_INHIBIT = 1u << 0;
static constexpr uint32_t PS_DAT_INHIBIT = 1u << 1;
static constexpr uint32_t INT_CMD_COMPLETE = 1u << 0;
static constexpr uint32_t INT_XFER_COMPLETE = 1u << 1;
static constexpr uint32_t INT_WRITE_READY = 1u << 4;
static constexpr uint32_t INT_READ_READY = 1u << 5;
static constexpr uint32_t INT_CARD = 1u << 8;
static constexpr uint32_t INT_ERROR = 1u << 15;
static constexpr uint32_t INT_ERROR_MASK = 0xFFFF0000u;
// Status enables: the completions, both buffer-ready bits, the card
// interrupt and every error. Card insert/remove stay off: nothing is
// ever inserted, and the detect line is not wired to the radio.
static constexpr uint32_t INT_ENABLES = 0x03FF0000u | INT_CMD_COMPLETE | INT_XFER_COMPLETE
| INT_WRITE_READY | INT_READ_READY | INT_CARD;
static constexpr uint32_t HC_4BIT = 1u << 1;
static constexpr uint32_t PWR_ON = 1u << 8;
static constexpr uint32_t PWR_330 = 7u << 9;
static constexpr uint32_t CLK_INT_EN = 1u << 0;
static constexpr uint32_t CLK_INT_STABLE = 1u << 1;
static constexpr uint32_t CLK_SD_EN = 1u << 2;
static constexpr uint32_t TIMEOUT_MAX = 0xEu << 16;
static constexpr uint32_t RESET_ALL = 1u << 24;
static constexpr uint32_t RESET_CMD = 1u << 25;
static constexpr uint32_t RESET_DAT = 1u << 26;
static constexpr uint32_t XM_BLOCK_COUNT_EN = 1u << 1;
static constexpr uint32_t XM_READ = 1u << 4;
static constexpr uint32_t XM_MULTI_BLOCK = 1u << 5;
enum Response : uint32_t {
RespNone = 0,
Resp48 = 2 | 3u << 3, // R1, R5, R6: CRC and index checked
Resp48NoCrc = 2, // R4
};
static constexpr uint32_t CMD_DATA = 1u << 5;
// R5 flags: COM_CRC_ERROR, ILLEGAL_COMMAND, ERROR, FUNCTION_NUMBER,
// OUT_OF_RANGE.
static constexpr uint32_t R5_ERRORS = 0xCB00;
// CCCR
static constexpr uint32_t CCCR_IOEN = 0x02;
static constexpr uint32_t CCCR_IORDY = 0x03;
static constexpr uint32_t CCCR_INTEN = 0x04;
static constexpr uint32_t CCCR_ABORT = 0x06;
static constexpr uint32_t CCCR_BUSIF = 0x07;
static constexpr uint32_t BUSIF_4BIT = 0x02;
static constexpr uint32_t BUSIF_CD_DIS = 0x80;
static constexpr uint32_t FBR_BLKSIZE = 0x10; // in function n's FBR at n * 0x100
static constexpr uint64_t CommandTimeoutMs = 100;
static constexpr uint64_t DataTimeoutMs = 500;
// CMD53's count field is nine bits.
static constexpr uint32_t MaxBlocks = 511;
// The legacy EMMC clock the firmware sets up; only used when neither the
// mailbox nor the capabilities name one. Assuming it high errs towards a
// slower bus, never a faster one.
static constexpr uint32_t FallbackBaseClockHz = 250000000;
static volatile uint8_t* g_regs = nullptr;
static uint32_t g_version = 0;
static uint32_t g_baseClockHz = 0;
static uint32_t g_clockHz = 0;
static uint32_t g_rca = 0;
static bool g_initialized = false;
static inline uint32_t Read(uint32_t reg) {
return *(volatile uint32_t*)(g_regs + reg);
}
static inline void Write(uint32_t reg, uint32_t value) {
*(volatile uint32_t*)(g_regs + reg) = value;
if (g_clockHz <= 400000) Timekeeping::BusyWaitUs(10);
}
static bool WaitClear(uint32_t reg, uint32_t mask, uint64_t timeoutMs) {
uint64_t start = Timekeeping::GetMilliseconds();
while (Read(reg) & mask) {
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
Arch::Pause();
}
return true;
}
static bool Reset(uint32_t which) {
Write(REG_CLOCK_CONTROL, Read(REG_CLOCK_CONTROL) | which);
return WaitClear(REG_CLOCK_CONTROL, which, 100);
}
static bool WaitStatus(uint32_t bits, uint64_t timeoutMs) {
uint64_t start = Timekeeping::GetMilliseconds();
for (;;) {
uint32_t status = Read(REG_INT_STATUS);
if (status & (INT_ERROR | INT_ERROR_MASK)) {
Write(REG_INT_STATUS, status & ~INT_CARD);
Reset(RESET_CMD | RESET_DAT);
return false;
}
if (status & bits) {
Write(REG_INT_STATUS, status & bits);
return true;
}
if (Timekeeping::GetMilliseconds() - start > timeoutMs) {
Reset(RESET_CMD | RESET_DAT);
return false;
}
Arch::Pause();
}
}
static bool SendCommand(uint32_t index, uint32_t argument, uint32_t response,
uint32_t transferMode = 0, bool data = false) {
uint32_t inhibit = PS_CMD_INHIBIT | (data ? PS_DAT_INHIBIT : 0);
if (!WaitClear(REG_PRESENT_STATE, inhibit, CommandTimeoutMs)) {
Reset(RESET_CMD | RESET_DAT);
return false;
}
Write(REG_INT_STATUS, ~INT_CARD);
Write(REG_ARGUMENT, argument);
uint32_t command = index << 8 | response | (data ? CMD_DATA : 0);
Write(REG_XFER_MODE_CMD, command << 16 | transferMode);
return WaitStatus(INT_CMD_COMPLETE, CommandTimeoutMs);
}
static bool SetClock(uint32_t hz) {
Write(REG_CLOCK_CONTROL, Read(REG_CLOCK_CONTROL) & ~0xFFFFu);
uint32_t divider = 0; // base / (2 * divider); 0 = undivided
if (hz < g_baseClockHz) {
divider = (g_baseClockHz + 2 * hz - 1) / (2 * hz);
uint32_t max = g_version >= 2 ? 0x3FF : 0x80;
if (divider > max) divider = max;
if (g_version < 2) {
uint32_t p = 1;
while (p < divider) p <<= 1;
divider = p;
}
}
uint32_t actual = divider ? g_baseClockHz / (2 * divider) : g_baseClockHz;
uint32_t field = (divider & 0xFF) << 8 | ((divider >> 8) & 0x3) << 6;
Write(REG_CLOCK_CONTROL, (Read(REG_CLOCK_CONTROL) & ~0xFFFFu) | field | CLK_INT_EN);
uint64_t start = Timekeeping::GetMilliseconds();
while (!(Read(REG_CLOCK_CONTROL) & CLK_INT_STABLE)) {
if (Timekeeping::GetMilliseconds() - start > 20) return false;
Arch::Pause();
}
Write(REG_CLOCK_CONTROL, Read(REG_CLOCK_CONTROL) | CLK_SD_EN);
g_clockHz = actual;
return true;
}
static bool Cmd52(bool write, uint32_t fn, uint32_t addr, uint8_t in, uint8_t* out) {
uint32_t arg = (write ? 1u << 31 : 0) | (fn & 7) << 28 | (addr & 0x1FFFF) << 9 | in;
if (!SendCommand(52, arg, Resp48)) return false;
uint32_t r = Read(REG_RESPONSE);
if (r & R5_ERRORS) return false;
if (out) *out = (uint8_t)r;
return true;
}
bool Read8(uint32_t fn, uint32_t addr, uint8_t& value) {
return Cmd52(false, fn, addr, 0, &value);
}
bool Write8(uint32_t fn, uint32_t addr, uint8_t value) {
return Cmd52(true, fn, addr, value, nullptr);
}
// One CMD53: `count` blocks of `blockSize` bytes, or (byte mode) one run
// of `blockSize` bytes. `words` is 4-byte aligned.
static bool Cmd53(bool write, uint32_t fn, uint32_t addr, bool incr, uint32_t* words,
bool blockMode, uint32_t count, uint32_t blockSize) {
uint32_t arg = (write ? 1u << 31 : 0) | (fn & 7) << 28 | (blockMode ? 1u << 27 : 0)
| (incr ? 1u << 26 : 0) | (addr & 0x1FFFF) << 9
| ((blockMode ? count : blockSize) & 0x1FF);
uint32_t blocks = blockMode ? count : 1;
Write(REG_BLOCK_SIZE_COUNT, blocks << 16 | blockSize);
uint32_t mode = write ? 0 : XM_READ;
if (blockMode) mode |= XM_BLOCK_COUNT_EN | (blocks > 1 ? XM_MULTI_BLOCK : 0);
if (!SendCommand(53, arg, Resp48, mode, true)) return false;
if (Read(REG_RESPONSE) & R5_ERRORS) {
Abort(fn);
Reset(RESET_CMD | RESET_DAT);
return false;
}
uint32_t perBlock = (blockSize + 3) / 4;
for (uint32_t b = 0; b < blocks; b++) {
if (!WaitStatus(write ? INT_WRITE_READY : INT_READ_READY, DataTimeoutMs)) {
Abort(fn);
return false;
}
for (uint32_t i = 0; i < perBlock; i++) {
if (write) *(volatile uint32_t*)(g_regs + REG_BUFFER_DATA) = *words++;
else *words++ = Read(REG_BUFFER_DATA);
}
}
if (!WaitStatus(INT_XFER_COMPLETE, DataTimeoutMs)) {
Abort(fn);
return false;
}
return true;
}
static bool Transfer(bool write, uint32_t fn, uint32_t addr, bool incr,
uint8_t* buffer, uint32_t len) {
if (!g_initialized || (len & 3) != 0) return false;
uint32_t blockSize = fn == FN2 ? FN2_BLOCK : FN1_BLOCK;
// The data port moves dwords; callers' buffers are only byte aligned
// in general.
uint32_t bounce[FN2_BLOCK / 4];
while (len > 0) {
bool blockMode = len >= blockSize;
uint32_t count = blockMode ? len / blockSize : 0;
if (count > MaxBlocks) count = MaxBlocks;
uint32_t run = blockMode ? count * blockSize : len;
if (!blockMode && run > FN2_BLOCK) run = FN2_BLOCK;
bool aligned = ((uintptr_t)buffer & 3) == 0;
if (aligned) {
if (!Cmd53(write, fn, addr, incr, (uint32_t*)buffer, blockMode, count,
blockMode ? blockSize : run)) return false;
} else {
// Unaligned: one block (or the byte-mode run) at a time
// through the bounce buffer.
if (blockMode) { count = 1; run = blockSize; }
if (write) memcpy(bounce, buffer, run);
if (!Cmd53(write, fn, addr, incr, bounce, blockMode, count,
blockMode ? blockSize : run)) return false;
if (!write) memcpy(buffer, bounce, run);
}
buffer += run;
if (incr) addr += run;
len -= run;
}
return true;
}
bool ReadBlock(uint32_t fn, uint32_t addr, bool incr, uint8_t* buffer, uint32_t len) {
return Transfer(false, fn, addr, incr, buffer, len);
}
bool WriteBlock(uint32_t fn, uint32_t addr, bool incr, const uint8_t* buffer, uint32_t len) {
return Transfer(true, fn, addr, incr, (uint8_t*)buffer, len);
}
void Abort(uint32_t fn) {
Cmd52(true, FN0, CCCR_ABORT, (uint8_t)fn, nullptr);
}
bool EnableFunction(uint32_t fn, bool enable, uint64_t timeoutMs) {
uint8_t ioen = 0;
if (!Read8(FN0, CCCR_IOEN, ioen)) return false;
uint8_t bit = (uint8_t)(1u << fn);
ioen = enable ? (ioen | bit) : (ioen & ~bit);
if (!Write8(FN0, CCCR_IOEN, ioen)) return false;
if (!enable) return true;
uint64_t start = Timekeeping::GetMilliseconds();
for (;;) {
uint8_t ready = 0;
if (Read8(FN0, CCCR_IORDY, ready) && (ready & bit)) return true;
if (Timekeeping::GetMilliseconds() - start > timeoutMs) return false;
Timekeeping::BusyWaitUs(500);
}
}
bool EnableCardInterrupts(uint8_t mask) {
return Write8(FN0, CCCR_INTEN, mask);
}
bool CardInterruptPending() {
return g_initialized && (Read(REG_INT_STATUS) & INT_CARD) != 0;
}
bool IsInitialized() { return g_initialized; }
bool HostInit(uint64_t mmioPhys, uint32_t baseClockHz) {
g_initialized = false;
if (!g_regs) {
Memory::VMM::g_paging->MapMMIO(mmioPhys & ~0xFFFULL, Memory::HHDM(mmioPhys & ~0xFFFULL));
g_regs = (volatile uint8_t*)Memory::HHDM(mmioPhys);
}
g_clockHz = 0;
if (!Reset(RESET_ALL)) {
KernelLogStream(ERROR, "WiFi-SDIO") << "Controller reset timed out";
return false;
}
g_version = (Read(REG_VERSION) >> 16) & 0xFF;
uint32_t caps = Read(REG_CAPABILITIES);
uint32_t capsMhz = g_version >= 2 ? (caps >> 8) & 0xFF : (caps >> 8) & 0x3F;
g_baseClockHz = baseClockHz ? baseClockHz
: capsMhz ? capsMhz * 1000000 : FallbackBaseClockHz;
// The radio's 3.3 V supply is not switched by the controller, but the
// bus power bit gates the controller's own drivers.
Write(REG_HOST_CONTROL, (Read(REG_HOST_CONTROL) & ~0xFF00u) | PWR_330 | PWR_ON);
Write(REG_INT_ENABLE, INT_ENABLES);
Write(REG_INT_SIGNAL, 0);
Write(REG_CLOCK_CONTROL, (Read(REG_CLOCK_CONTROL) & ~0xFF0000u) | TIMEOUT_MAX);
if (!SetClock(400000)) {
KernelLogStream(ERROR, "WiFi-SDIO") << "Clock did not stabilize";
return false;
}
// 74 clocks before the first command.
Timekeeping::BusyWaitMs(2);
KernelLogStream(INFO, "WiFi-SDIO") << "Arasan controller at 0x" << base::hex << mmioPhys
<< base::dec << ", version " << (uint64_t)(g_version + 1) << ".00, base clock "
<< (uint64_t)(g_baseClockHz / 1000000) << " MHz";
return true;
}
bool CardInit() {
// Allow Transfer()/Cmd52 during bring-up; cleared again on failure.
g_initialized = true;
auto fail = [](const char* what) {
KernelLogStream(WARNING, "WiFi-SDIO") << what;
g_initialized = false;
return false;
};
SendCommand(0, 0, RespNone);
Timekeeping::BusyWaitMs(2);
// CMD5 with no voltage first to read the OCR; the radio may still be
// coming out of reset, so give it a few tries.
uint32_t ocr = 0;
bool answered = false;
for (int i = 0; i < 10 && !answered; i++) {
answered = SendCommand(5, 0, Resp48NoCrc);
if (!answered) Timekeeping::BusyWaitMs(10);
}
if (!answered) return fail("No SDIO card answers CMD5");
ocr = Read(REG_RESPONSE) & 0x00FFFFFF;
uint32_t want = ocr & 0x00300000; // 3.2-3.4 V
if (!want) want = ocr & 0x00FF8000;
uint64_t start = Timekeeping::GetMilliseconds();
for (;;) {
if (!SendCommand(5, want, Resp48NoCrc)) return fail("CMD5 failed");
if (Read(REG_RESPONSE) & (1u << 31)) break;
if (Timekeeping::GetMilliseconds() - start > 1000) return fail("Card stayed busy (CMD5)");
Timekeeping::BusyWaitMs(1);
}
if (!SendCommand(3, 0, Resp48)) return fail("CMD3 failed");
g_rca = Read(REG_RESPONSE) >> 16;
if (!SendCommand(7, g_rca << 16, Resp48)) return fail("CMD7 failed");
// 4-bit bus; disconnect the card-detect pull-up on DAT3.
uint8_t busif = 0;
if (!Read8(FN0, CCCR_BUSIF, busif)) return fail("CCCR read failed");
busif = (uint8_t)((busif & ~0x03) | BUSIF_4BIT | BUSIF_CD_DIS);
if (!Write8(FN0, CCCR_BUSIF, busif)) return fail("Could not select the 4-bit bus");
Write(REG_HOST_CONTROL, Read(REG_HOST_CONTROL) | HC_4BIT);
if (!SetClock(25000000)) return fail("Clock did not stabilize at 25 MHz");
// Function block sizes (little endian in the FBR).
bool ok = Write8(FN0, 1 * 0x100 + FBR_BLKSIZE, (uint8_t)(FN1_BLOCK & 0xFF))
&& Write8(FN0, 1 * 0x100 + FBR_BLKSIZE + 1, (uint8_t)(FN1_BLOCK >> 8))
&& Write8(FN0, 2 * 0x100 + FBR_BLKSIZE, (uint8_t)(FN2_BLOCK & 0xFF))
&& Write8(FN0, 2 * 0x100 + FBR_BLKSIZE + 1, (uint8_t)(FN2_BLOCK >> 8));
if (!ok) return fail("Could not set the function block sizes");
if (!EnableFunction(FN1, true, 1000)) return fail("Function 1 did not become ready");
KernelLogStream(OK, "WiFi-SDIO") << "SDIO card ready (RCA 0x" << base::hex << (uint64_t)g_rca
<< base::dec << ", OCR 0x" << base::hex << (uint64_t)ocr << base::dec << ", "
<< (uint64_t)(g_clockHz / 1000) << " kHz, 4-bit)";
return true;
}
}
+63
View File
@@ -0,0 +1,63 @@
/*
* BrcmfSdio.hpp
* SDIO host on the Raspberry Pi 4/400's Arasan SD controller, for the
* CYW4345x Wi-Fi radio wired to it
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
// The BCM2711 has two SD controllers: EMMC2 runs the card slot
// (Drivers/Storage/Sdhci) and the older Arasan-derived EMMC, routed to GPIO
// 34-39, runs the radio's SDIO interface. This is a minimal SDIO host for
// the latter: one card, polled, programmed I/O, CMD52 and CMD53 only.
//
// None of these calls lock; the Broadcom driver above serializes them.
namespace Drivers::Net::Wifi::Sdio {
// SDIO functions on the radio.
constexpr uint32_t FN0 = 0; // CCCR / FBR
constexpr uint32_t FN1 = 1; // backplane
constexpr uint32_t FN2 = 2; // WLAN frames
// Block sizes the card is configured with.
constexpr uint32_t FN1_BLOCK = 64;
constexpr uint32_t FN2_BLOCK = 512;
// Map the controller's registers and reset it. `baseClockHz` is the
// controller's input clock (0 = take it from the capabilities).
bool HostInit(uint64_t mmioPhys, uint32_t baseClockHz);
// Enumerate the card: CMD0/5/3/7, 4-bit bus at 25 MHz, function 1/2
// block sizes, function 1 enabled. Returns false when nothing answers.
bool CardInit();
// CMD52: one byte.
bool Read8(uint32_t fn, uint32_t addr, uint8_t& value);
bool Write8(uint32_t fn, uint32_t addr, uint8_t value);
// CMD53: `len` bytes, a multiple of 4. `incr` selects an incrementing
// address (backplane memory) over a fixed one (the function 2 FIFO).
// Longer transfers are split into block-mode runs plus a byte-mode tail.
bool ReadBlock(uint32_t fn, uint32_t addr, bool incr, uint8_t* buffer, uint32_t len);
bool WriteBlock(uint32_t fn, uint32_t addr, bool incr, const uint8_t* buffer, uint32_t len);
// Enable or disable function `fn`'s I/O; waits up to `timeoutMs` for the
// card to report it ready.
bool EnableFunction(uint32_t fn, bool enable, uint64_t timeoutMs);
// CCCR interrupt enables (master | functions).
bool EnableCardInterrupts(uint8_t mask);
// Abort a function's transfer in progress (CCCR I/O Abort).
void Abort(uint32_t fn);
// The card is signalling an interrupt on DAT1. One register read, safe
// from any context.
bool CardInterruptPending();
bool IsInitialized();
}
+6
View File
@@ -443,6 +443,12 @@ namespace Drivers::Net::Wifi {
// Send an Ethernet frame over the air (called by the netif).
bool IwxConnectSendEthernet(const uint8_t* frame, uint32_t len);
// The supplicant's hooks (Wpa.hpp) for this MLME; Wifi.cpp routes them.
bool IwxWpaTxEapol(const uint8_t* body, uint32_t len);
bool IwxWpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher);
bool IwxWpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
uint8_t cipher, const uint8_t* rsc);
// Sink for decapsulated Ethernet frames, implemented by Wifi.cpp.
void WifiRxEthernet(const uint8_t* frame, uint32_t len);
}
+4 -4
View File
@@ -535,7 +535,7 @@ namespace Drivers::Net::Wifi {
// The supplicant transmits EAPOL frames unencrypted: they are what
// establishes the key in the first place.
bool WpaTxEapol(const uint8_t* body, uint32_t len) {
bool IwxWpaTxEapol(const uint8_t* body, uint32_t len) {
return SendDataFrame(g_bssid, ETHERTYPE_EAPOL, body, len, false);
}
@@ -545,14 +545,14 @@ namespace Drivers::Net::Wifi {
static InstalledKey g_ptkKey = {};
static InstalledKey g_gtkKey = {};
bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
bool IwxWpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
if (!IwxSetKey(tk, tkLen, 0, true, cipher, nullptr)) return false;
g_ptkKey = { true, 0, cipher, (uint8_t)tkLen };
return true;
}
bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
uint8_t cipher, const uint8_t* rsc) {
bool IwxWpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
uint8_t cipher, const uint8_t* rsc) {
if (gtkLen != 16 && gtkLen != 32) return false;
if (!IwxSetKey(gtk, gtkLen, keyIdx, false, cipher, rsc)) return false;
g_gtkKey = { true, keyIdx, cipher, (uint8_t)gtkLen };
+279 -82
View File
@@ -14,6 +14,7 @@
#include "Wifi.hpp"
#include <Arch/Cpu.hpp>
#include "Iwx.hpp"
#include "Brcmf.hpp"
#include "Wpa.hpp"
#include <Fs/Vfs.hpp>
#include <Libraries/Memory.hpp>
@@ -34,6 +35,72 @@ namespace Drivers::Net::Wifi {
static std::atomic<bool> g_initPending{false};
static bool g_initialized = false;
// =========================================================================
// Backend
//
// Two kinds of adapter sit behind this facade: the Intel AX210 family on
// PCIe (Iwx*: the firmware is a SoftMAC, the MLME is ours) and the
// Raspberry Pi 4/400's Broadcom/Cypress radio on SDIO (Brcmf*: FullMAC,
// the firmware joins by itself). A machine has at most one; whichever
// probes first owns the facade. Both use the same scan table, the same
// supplicant and the same connection-state numbering (WIFI_CONN_*, which
// IwxConnStateId mirrors).
// =========================================================================
enum class Backend : uint8_t { None, Iwx, Brcmf };
static Backend g_backend = Backend::None;
static bool IsBrcmf() { return g_backend == Backend::Brcmf; }
static bool FwRunning() {
return IsBrcmf() ? BrcmfGetState() == BrcmfState::Running
: g_iwx.State == IwxFwState::Running;
}
static bool FwFailed() {
return IsBrcmf() ? BrcmfGetState() == BrcmfState::Error
: g_iwx.State == IwxFwState::Error;
}
static bool ScanActive() { return IsBrcmf() ? BrcmfScanActive() : g_iwx.ScanActive; }
static bool BeginScan() { return IsBrcmf() ? BrcmfStartScan() : IwxStartScan(nullptr); }
static void AbortScan() {
if (IsBrcmf()) BrcmfAbortScan();
else IwxAbortScan();
}
// What a blocking wait pumps: the receive path, and for a join the
// state machine behind it.
static void PumpEvents() {
if (IsBrcmf()) BrcmfService();
else IwxProcessEvents();
}
static void PumpConnection() {
if (IsBrcmf()) {
BrcmfService();
} else {
IwxProcessEvents();
IwxConnectService();
}
}
static int ConnState() { return IsBrcmf() ? BrcmfConnectState() : IwxConnectState(); }
static void ConnAbort() {
if (IsBrcmf()) BrcmfConnectAbort();
else IwxConnectAbort();
}
// A handshake that got as far as exchanging EAPOL frames and then failed
// is almost always a wrong passphrase.
static bool JoinFailedOnAuth() {
return IsBrcmf() ? BrcmfJoinFailedOnAuth() : WpaGetState() == WpaState::Failed;
}
static bool LinkUp() { return IsBrcmf() ? BrcmfLinkUp() : IwxLinkUp(); }
// =========================================================================
// Scan result table
// =========================================================================
@@ -141,22 +208,12 @@ namespace Drivers::Net::Wifi {
uint8_t RsnLen = 0;
};
static bool ParseBeacon(const uint8_t* frame, uint32_t len, ParsedBeacon* out) {
// 24-byte MAC header, then timestamp(8) + beacon interval(2) + caps(2).
constexpr uint32_t HDR = 24;
constexpr uint32_t FIXED = 12;
if (len < HDR + FIXED) return false;
const uint8_t* fixed = frame + HDR;
out->BeaconInterval = (uint16_t)(fixed[8] | (fixed[9] << 8));
uint16_t caps = (uint16_t)(fixed[10] | (fixed[11] << 8));
bool privacy = (caps & 0x0010) != 0;
// The information elements of a beacon or probe response; `privacy` is
// the capability field's Privacy bit.
static void ParseIes(const uint8_t* ie, const uint8_t* end, bool privacy, ParsedBeacon* out) {
bool haveRsn = false, haveWpa = false;
const uint8_t* ie = frame + HDR + FIXED;
const uint8_t* end = frame + len;
while (ie + 2 <= end) {
while (ie && ie + 2 <= end) {
uint8_t id = ie[0];
uint8_t ielen = ie[1];
if (ie + 2 + ielen > end) break;
@@ -200,9 +257,24 @@ namespace Drivers::Net::Wifi {
else if (privacy) out->Security = WIFI_SEC_WEP;
else out->Security = WIFI_SEC_OPEN;
}
}
static bool ParseBeacon(const uint8_t* frame, uint32_t len, ParsedBeacon* out) {
// 24-byte MAC header, then timestamp(8) + beacon interval(2) + caps(2).
constexpr uint32_t HDR = 24;
constexpr uint32_t FIXED = 12;
if (len < HDR + FIXED) return false;
const uint8_t* fixed = frame + HDR;
out->BeaconInterval = (uint16_t)(fixed[8] | (fixed[9] << 8));
uint16_t caps = (uint16_t)(fixed[10] | (fixed[11] << 8));
ParseIes(frame + HDR + FIXED, frame + len, (caps & 0x0010) != 0, out);
return true;
}
static void StoreScanResult(const uint8_t* bssid, const ParsedBeacon& pb, uint8_t ch,
int8_t rssiDbm);
// Called by the MVM RX path for every received management frame.
void WifiRxMgmtFrame(const uint8_t* frame, uint32_t len, uint8_t channel,
int8_t rssiDbm) {
@@ -224,9 +296,21 @@ namespace Drivers::Net::Wifi {
ParsedBeacon pb;
if (!ParseBeacon(frame, len, &pb)) return;
const uint8_t* bssid = frame + 16; // addr3 of a beacon
uint8_t ch = pb.Channel ? pb.Channel : channel;
// addr3 of a beacon
StoreScanResult(frame + 16, pb, pb.Channel ? pb.Channel : channel, rssiDbm);
}
// One BSS a FullMAC firmware reported from its own scan.
void WifiRxBssInfo(const uint8_t* bssid, uint16_t beaconInterval, uint16_t capability,
const uint8_t* ies, uint32_t iesLen, uint8_t channel, int8_t rssiDbm) {
ParsedBeacon pb;
pb.BeaconInterval = beaconInterval;
ParseIes(ies, ies ? ies + iesLen : nullptr, (capability & 0x0010) != 0, &pb);
StoreScanResult(bssid, pb, pb.Channel ? pb.Channel : channel, rssiDbm);
}
static void StoreScanResult(const uint8_t* bssid, const ParsedBeacon& pb, uint8_t ch,
int8_t rssiDbm) {
g_resultLock.Acquire();
ScanEntry* slot = nullptr;
@@ -275,7 +359,9 @@ namespace Drivers::Net::Wifi {
// =========================================================================
bool Probe(const Pci::PciDevice& dev) {
if (g_backend != Backend::None) return false;
if (!IwxProbe(dev)) return false;
g_backend = Backend::Iwx;
// Firmware comes off the ramdisk, which is not mounted during the PCI
// scan; hand the rest of the bring-up to the idle loop.
@@ -283,7 +369,23 @@ namespace Drivers::Net::Wifi {
return true;
}
bool ProbeBroadcomSdio(uint64_t mmioPhys, const char* name) {
if (g_backend != Backend::None) return false;
if (!BrcmfProbe(mmioPhys)) return false;
g_backend = Backend::Brcmf;
KernelLogStream(INFO, "WiFi") << "Broadcom SDIO radio on " << name
<< "; firmware loads once the root filesystem is up";
// Same reason as the PCI path: the firmware is on the root volume.
g_initPending.store(true, std::memory_order_release);
return true;
}
static void CompleteInit() {
if (IsBrcmf()) {
g_initialized = BrcmfCompleteInit();
return;
}
if (!IwxReadFirmware()) {
g_iwx.State = IwxFwState::Error;
return;
@@ -348,7 +450,7 @@ namespace Drivers::Net::Wifi {
static constexpr uint64_t RECOVERY_BACKOFF_MS = 5000;
static void ServiceRecovery() {
if (g_iwx.State != IwxFwState::Error || g_recoveryGaveUp) return;
if (!FwFailed() || g_recoveryGaveUp) return;
// An adapter that never finished its first bring-up is ServiceDeferredInit's
// problem, not this one.
if (!g_initialized) return;
@@ -366,7 +468,7 @@ namespace Drivers::Net::Wifi {
// Re-check under the gate: the winner of the race may have just
// finished a reset that fixed things.
if (g_iwx.State != IwxFwState::Error) return;
if (!FwFailed()) return;
uint64_t now = Timekeeping::GetMilliseconds();
if (g_lastRecoveryMs && now - g_lastRecoveryMs < RECOVERY_BACKOFF_MS) return;
@@ -384,6 +486,24 @@ namespace Drivers::Net::Wifi {
<< " (attempt " << (uint64_t)g_recoveryAttempts << " of "
<< (uint64_t)MAX_RECOVERY_ATTEMPTS << ")";
if (IsBrcmf()) {
// A FullMAC radio is reset by power: WL_ON low, then the whole
// bring-up (firmware download included) again.
g_initialized = false;
g_asyncConnect = false;
g_scanDeadline = 0;
g_lastError = WIFI_ERR_NO_ADAPTER;
ClearResults();
BrcmfShutdown();
CompleteInit();
if (g_initialized) {
KernelLogStream(OK, "WiFi") << "Adapter recovered";
g_recoveryAttempts = 0;
g_lastError = 0;
}
return;
}
// Nothing may be mid-command while the device is torn down. Taking
// CmdLock is enough: it is what serializes every sender, and the RX
// pump has its own reentrancy guard.
@@ -517,8 +637,24 @@ namespace Drivers::Net::Wifi {
return false;
}
// The facade's own deadlines (async scan and join) and a due reset.
static bool FacadeWorkDue(uint64_t now) {
if (g_scanDeadline != 0 && (now >= g_scanDeadline || !ScanActive())) return true;
if (g_asyncConnect) return true;
return FwFailed() && g_initialized && !g_recoveryGaveUp &&
(g_lastRecoveryMs == 0 || now - g_lastRecoveryMs >= RECOVERY_BACKOFF_MS);
}
bool HasPolledWork() {
return IsBrcmf() && BrcmfHasWork(Timekeeping::GetMilliseconds());
}
bool HasDeferredWork() {
if (InitDue()) return true;
if (IsBrcmf()) {
uint64_t now = Timekeeping::GetMilliseconds();
return BrcmfHasWork(now) || FacadeWorkDue(now);
}
if (!g_iwx.Mmio) return false;
if (g_iwx.WorkPending) return true;
@@ -527,7 +663,29 @@ namespace Drivers::Net::Wifi {
return HasBlockingWork(now);
}
// The Broadcom radio has no interrupt in use: everything on its bus is
// polled, and everything on its bus waits (SDIO transfers, ioctls), so
// all of it goes through the blocking gate.
static void ServiceBrcmf() {
uint64_t now = Timekeeping::GetMilliseconds();
bool radioWork = BrcmfHasWork(now);
if (!radioWork && !FacadeWorkDue(now)) return;
if (LeaveToAp()) return;
if (__atomic_test_and_set(&g_blockingBusy, __ATOMIC_ACQUIRE)) return;
struct Guard {
~Guard() { __atomic_clear(&g_blockingBusy, __ATOMIC_RELEASE); }
} guard;
if (radioWork) BrcmfService();
ServiceAsync();
ServiceRecovery();
}
void ServiceEvents() {
if (IsBrcmf()) {
ServiceBrcmf();
return;
}
if (!g_iwx.Mmio) return;
// The non-blocking half: any core, the BSP included.
@@ -573,7 +731,11 @@ namespace Drivers::Net::Wifi {
}
bool IsInitialized() { return g_initialized; }
bool IsPresent() { return g_iwx.State != IwxFwState::Absent; }
bool IsPresent() {
return IsBrcmf() ? BrcmfGetState() != BrcmfState::Absent
: g_iwx.State != IwxFwState::Absent;
}
// =========================================================================
// Public operations
@@ -611,26 +773,26 @@ namespace Drivers::Net::Wifi {
int Scan(WifiNetwork* out, int maxCount, uint32_t timeoutMs) {
if (!out || maxCount <= 0) return -1;
if (!g_initialized) return -1;
if (g_iwx.State != IwxFwState::Running) return -1;
if (!FwRunning()) return -1;
if (timeoutMs < 1000) timeoutMs = 1000;
if (timeoutMs > 20000) timeoutMs = 20000;
ClearResults();
if (!IwxStartScan(nullptr)) return -1;
if (!BeginScan()) return -1;
uint64_t start = Timekeeping::GetMilliseconds();
while (g_iwx.ScanActive && Timekeeping::GetMilliseconds() - start < timeoutMs) {
IwxProcessEvents();
while (ScanActive() && Timekeeping::GetMilliseconds() - start < timeoutMs) {
PumpEvents();
for (int i = 0; i < 200; i++) Arch::Pause();
}
if (g_iwx.ScanActive) {
IwxAbortScan();
if (ScanActive()) {
AbortScan();
// Drain whatever the firmware queued before the abort landed.
uint64_t t0 = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - t0 < 200) IwxProcessEvents();
while (Timekeeping::GetMilliseconds() - t0 < 200) PumpEvents();
}
g_scanGeneration++;
@@ -639,14 +801,14 @@ namespace Drivers::Net::Wifi {
int StartScan(uint32_t timeoutMs) {
if (!g_initialized) return -1;
if (g_iwx.State != IwxFwState::Running) return -1;
if (g_iwx.ScanActive) return 1; // already sweeping
if (!FwRunning()) return -1;
if (ScanActive()) return 1; // already sweeping
if (timeoutMs < 1000) timeoutMs = 1000;
if (timeoutMs > 20000) timeoutMs = 20000;
ClearResults();
if (!IwxStartScan(nullptr)) return -1;
if (!BeginScan()) return -1;
g_scanDeadline = Timekeeping::GetMilliseconds() + timeoutMs;
return 0;
@@ -657,33 +819,47 @@ namespace Drivers::Net::Wifi {
memset(out, 0, sizeof(*out));
out->present = IsPresent() ? 1 : 0;
out->state = (uint8_t)g_iwx.State;
if (!IsPresent()) return -1;
memcpy(out->mac, g_iwx.Nvm.HwAddr, 6);
out->scanning = g_iwx.ScanActive ? 1 : 0;
out->bands = (uint8_t)((g_iwx.Nvm.Sku24GHz ? 1 : 0)
| (g_iwx.Nvm.Sku52GHz ? 2 : 0));
out->channels = (uint16_t)g_iwx.ChannelCount;
out->rxPackets = g_iwx.RxPackets;
out->txPackets = g_iwx.TxPackets;
out->fwErrors = (uint32_t)g_iwx.FwErrors;
out->connState = (uint32_t)IwxConnectState();
out->connected = IwxLinkUp() ? 1 : 0;
const char* fwVersion;
if (IsBrcmf()) {
out->state = (uint8_t)BrcmfGetState();
memcpy(out->mac, g_brcmf.Mac, 6);
out->bands = (uint8_t)((g_brcmf.Band24 ? 1 : 0) | (g_brcmf.Band5 ? 2 : 0));
out->channels = g_brcmf.ChannelCount;
out->rxPackets = g_brcmf.RxPackets;
out->txPackets = g_brcmf.TxPackets;
out->fwErrors = g_brcmf.FwErrors;
fwVersion = g_brcmf.FwVersion;
} else {
out->state = (uint8_t)g_iwx.State;
memcpy(out->mac, g_iwx.Nvm.HwAddr, 6);
out->bands = (uint8_t)((g_iwx.Nvm.Sku24GHz ? 1 : 0)
| (g_iwx.Nvm.Sku52GHz ? 2 : 0));
out->channels = (uint16_t)g_iwx.ChannelCount;
out->rxPackets = g_iwx.RxPackets;
out->txPackets = g_iwx.TxPackets;
out->fwErrors = (uint32_t)g_iwx.FwErrors;
fwVersion = g_iwx.Fw.Version;
}
out->scanning = ScanActive() ? 1 : 0;
out->connState = (uint32_t)ConnState();
out->connected = LinkUp() ? 1 : 0;
out->lastError = (int32_t)g_lastError;
out->scanGeneration = g_scanGeneration;
out->joining = g_asyncConnect ? 1 : 0;
if (IwxConnectState() != (int)IwxConnStateId::Idle) {
const char* ssid = IwxConnectSsid();
if (ConnState() != (int)IwxConnStateId::Idle) {
const char* ssid = IsBrcmf() ? BrcmfConnectSsid() : IwxConnectSsid();
int k = 0;
for (; k < 32 && ssid[k]; k++) out->ssid[k] = ssid[k];
out->ssid[k] = '\0';
memcpy(out->bssid, IwxConnectBssid(), 6);
memcpy(out->bssid, IsBrcmf() ? BrcmfConnectBssid() : IwxConnectBssid(), 6);
if (IsBrcmf()) out->channel = BrcmfConnectChannel();
}
int i = 0;
for (; i < 31 && g_iwx.Fw.Version[i]; i++) out->fwVersion[i] = g_iwx.Fw.Version[i];
for (; i < 31 && fwVersion[i]; i++) out->fwVersion[i] = fwVersion[i];
out->fwVersion[i] = '\0';
return 0;
}
@@ -695,24 +871,20 @@ namespace Drivers::Net::Wifi {
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
IwxProcessEvents();
IwxConnectService();
PumpConnection();
auto state = (IwxConnStateId)IwxConnectState();
auto state = (IwxConnStateId)ConnState();
if (state == IwxConnStateId::Connected) return 0;
if (state == IwxConnStateId::Failed) {
// A handshake that got as far as exchanging EAPOL frames and
// then failed is almost always a wrong passphrase.
int rc = WpaGetState() == WpaState::Failed
? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
int rc = JoinFailedOnAuth() ? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
// A rejection seen on the RX path only marks the state; the
// firmware contexts are still up and have to come back down.
IwxConnectAbort();
ConnAbort();
return rc;
}
if (state == IwxConnStateId::Idle) return WIFI_ERR_FAILED;
if (g_iwx.State == IwxFwState::Error) {
IwxConnectAbort();
if (FwFailed()) {
ConnAbort();
return WIFI_ERR_FAILED;
}
@@ -721,7 +893,7 @@ namespace Drivers::Net::Wifi {
Sched::Schedule();
}
IwxConnectAbort();
ConnAbort();
return WIFI_ERR_TIMEOUT;
}
@@ -732,7 +904,7 @@ namespace Drivers::Net::Wifi {
// scan table is worth a (blocking) sweep to look for it.
static int StartJoin(const char* ssid, const char* password, bool rescan) {
if (!g_initialized || !ssid) return WIFI_ERR_NO_ADAPTER;
if (g_iwx.State != IwxFwState::Running) return WIFI_ERR_NO_ADAPTER;
if (!FwRunning()) return WIFI_ERR_NO_ADAPTER;
// Locate the network in the most recent scan results: the firmware
// contexts need its BSSID and channel, and the connect path needs its
@@ -809,17 +981,24 @@ namespace Drivers::Net::Wifi {
if (!havePassword) return WIFI_ERR_NEED_KEY;
}
if (!IwxConnectStart(bssid, channel, is5, ssid,
havePassword ? password : nullptr,
security == WIFI_SEC_OPEN ? nullptr : rsnIe,
security == WIFI_SEC_OPEN ? 0 : rsnIeLen,
beaconInterval, dtimPeriod)) {
// IwxConnectStart already logged the specific reason. Only report
bool started = IsBrcmf()
? BrcmfConnectStart(bssid, channel, ssid,
havePassword ? password : nullptr,
security == WIFI_SEC_OPEN ? nullptr : rsnIe,
security == WIFI_SEC_OPEN ? 0 : rsnIeLen)
: IwxConnectStart(bssid, channel, is5, ssid,
havePassword ? password : nullptr,
security == WIFI_SEC_OPEN ? nullptr : rsnIe,
security == WIFI_SEC_OPEN ? 0 : rsnIeLen,
beaconInterval, dtimPeriod);
if (!started) {
// The backend already logged the specific reason. Only report
// a security problem when that is genuinely what happened; a radio
// or firmware failure needs completely different advice.
bool security_ = IwxConnectRefusedForSecurity();
auto state = (IwxConnStateId)IwxConnectState();
if (state != IwxConnStateId::Idle) IwxConnectAbort();
bool security_ = IsBrcmf() ? BrcmfConnectRefusedForSecurity()
: IwxConnectRefusedForSecurity();
auto state = (IwxConnStateId)ConnState();
if (state != IwxConnStateId::Idle) ConnAbort();
return security_ ? WIFI_ERR_UNSUPPORTED : WIFI_ERR_FAILED;
}
@@ -835,8 +1014,8 @@ namespace Drivers::Net::Wifi {
int ConnectAsync(const char* ssid, const char* password) {
// A join already in flight owns the firmware contexts; tear it down
// rather than stacking a second one on top.
if (g_asyncConnect || (IwxConnectState() != (int)IwxConnStateId::Idle))
IwxConnectAbort();
if (g_asyncConnect || (ConnState() != (int)IwxConnStateId::Idle))
ConnAbort();
g_asyncConnect = false;
g_lastError = 0;
@@ -858,7 +1037,7 @@ namespace Drivers::Net::Wifi {
if (!g_initialized) return -1;
g_asyncConnect = false;
g_lastError = 0;
IwxConnectAbort();
ConnAbort();
return 0;
}
@@ -881,11 +1060,11 @@ namespace Drivers::Net::Wifi {
uint64_t now = Timekeeping::GetMilliseconds();
if (g_scanDeadline) {
if (!g_iwx.ScanActive) {
if (!ScanActive()) {
g_scanDeadline = 0;
g_scanGeneration++;
} else if (now >= g_scanDeadline) {
IwxAbortScan();
AbortScan();
g_scanDeadline = 0;
g_scanGeneration++;
}
@@ -893,31 +1072,29 @@ namespace Drivers::Net::Wifi {
if (!g_asyncConnect) return;
auto state = (IwxConnStateId)IwxConnectState();
auto state = (IwxConnStateId)ConnState();
if (state == IwxConnStateId::Connected) {
g_asyncConnect = false;
g_lastError = 0;
return;
}
if (state == IwxConnStateId::Failed) {
// Same reading as the blocking path: a handshake that exchanged
// EAPOL frames and then failed is almost always a wrong passphrase.
g_lastError = WpaGetState() == WpaState::Failed
? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
// Same reading as the blocking path.
g_lastError = JoinFailedOnAuth() ? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
g_asyncConnect = false;
IwxConnectAbort();
ConnAbort();
return;
}
if (state == IwxConnStateId::Idle || g_iwx.State == IwxFwState::Error) {
if (state == IwxConnStateId::Idle || FwFailed()) {
g_lastError = WIFI_ERR_FAILED;
g_asyncConnect = false;
if (state != IwxConnStateId::Idle) IwxConnectAbort();
if (state != IwxConnStateId::Idle) ConnAbort();
return;
}
if (now >= g_asyncConnectDeadline) {
g_lastError = WIFI_ERR_TIMEOUT;
g_asyncConnect = false;
IwxConnectAbort();
ConnAbort();
}
}
@@ -927,13 +1104,33 @@ namespace Drivers::Net::Wifi {
static RxCallback g_rxCallback = nullptr;
const uint8_t* GetMacAddress() { return g_iwx.Nvm.HwAddr; }
const uint8_t* GetMacAddress() { return IsBrcmf() ? g_brcmf.Mac : g_iwx.Nvm.HwAddr; }
bool IsLinkUp() { return g_initialized && IwxLinkUp(); }
bool IsLinkUp() { return g_initialized && LinkUp(); }
bool SendPacket(const uint8_t* data, uint16_t length) {
if (!IsLinkUp()) return false;
return IwxConnectSendEthernet(data, length);
return IsBrcmf() ? BrcmfSendEthernet(data, length)
: IwxConnectSendEthernet(data, length);
}
// =========================================================================
// Supplicant hooks (Wpa.hpp): to whichever backend is running the join
// =========================================================================
bool WpaTxEapol(const uint8_t* body, uint32_t len) {
return IsBrcmf() ? BrcmfWpaTxEapol(body, len) : IwxWpaTxEapol(body, len);
}
bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
return IsBrcmf() ? BrcmfWpaInstallPtk(tk, tkLen, cipher)
: IwxWpaInstallPtk(tk, tkLen, cipher);
}
bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
uint8_t cipher, const uint8_t* rsc) {
return IsBrcmf() ? BrcmfWpaInstallGtk(gtk, gtkLen, keyIdx, cipher, rsc)
: IwxWpaInstallGtk(gtk, gtkLen, keyIdx, cipher, rsc);
}
void SetRxCallback(RxCallback callback) { g_rxCallback = callback; }
+10
View File
@@ -14,6 +14,11 @@ namespace Drivers::Net::Wifi {
// PCI driver-table probe entry point.
bool Probe(const Pci::PciDevice& dev);
// The Raspberry Pi 4/400's Broadcom/Cypress radio, on the SDIO
// controller (Arasan) at `mmioPhys`. Claims it; the bring-up waits for
// the root filesystem like the PCI path.
bool ProbeBroadcomSdio(uint64_t mmioPhys, const char* name);
// Complete the firmware-dependent bring-up once the ramdisk is mounted.
// Cheap no-op unless a device is waiting. Called from the idle loop.
void ServiceDeferredInit();
@@ -25,6 +30,11 @@ namespace Drivers::Net::Wifi {
// deadline, or a due recovery attempt needs idle-context servicing.
bool HasDeferredWork();
// A polled adapter (the Broadcom radio has no interrupt in use) is due a
// sweep. Cheap and safe from the timer interrupt: the tick uses it to
// force a bottom-half pass when the cores never go idle.
bool HasPolledWork();
bool IsInitialized();
bool IsPresent();
+2 -1
View File
@@ -66,7 +66,8 @@ namespace Drivers::Net::Wifi {
bool WpaParseApRsn(const uint8_t* ie, uint32_t len, WpaConfig& cfg);
// -------------------------------------------------------------------------
// Hooks implemented by the MLME (IwxConnect.cpp)
// Hooks into the driver running the join (Wifi.cpp routes them to
// IwxConnect.cpp or BrcmfConnect.cpp)
// -------------------------------------------------------------------------
// Transmit an EAPOL frame body (802.1X header included) to the AP.
@@ -45,6 +45,7 @@ namespace Drivers::Platform::RpiFirmware {
static constexpr uint32_t TAG_RESPONSE = 0x80000000;
static constexpr uint32_t TAG_GET_BOARD_MAC = 0x00010003;
static constexpr uint32_t TAG_SET_CLOCK_STATE = 0x00038001;
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;
@@ -233,6 +234,14 @@ namespace Drivers::Platform::RpiFirmware {
return hz != 0;
}
// State bit 0 = on; the answer echoes the clock ID and its new state
// (bit 1 set: no such clock).
bool SetClockState(uint32_t clockId, bool on) {
uint32_t v[2] = { clockId, on ? 1u : 0u };
if (PropertyCall(TAG_SET_CLOCK_STATE, v, 2) < 8 || v[0] != clockId) return false;
return (v[1] & 2) == 0 && ((v[1] & 1) != 0) == on;
}
// 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) {
@@ -27,11 +27,15 @@ namespace Drivers::Platform::RpiFirmware {
bool IsPresent();
// Clock identifiers (the firmware's numbering).
constexpr uint32_t CLOCK_EMMC = 1; // legacy EMMC (Arasan), runs the Wi-Fi SDIO bus
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);
// Gate a firmware clock on or off.
bool SetClockState(uint32_t clockId, bool on);
// 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)
+2 -1
View File
@@ -67,7 +67,8 @@ namespace Timekeeping {
bool audioWork = Drivers::Audio::IntelHda::HasDeferredWork();
bool netWork = Drivers::Net::E1000::HasDeferredWork() ||
Drivers::Net::E1000E::HasDeferredWork() ||
Drivers::Net::Genet::HasDeferredWork();
Drivers::Net::Genet::HasDeferredWork() ||
Drivers::Net::Wifi::HasPolledWork();
// A forced trip into the idle bottom-half context is safe only when
// this timer interrupted ring 3. If it interrupted a syscall, that
// suspended kernel stack may own one of the process-context mutexes