diff --git a/docs/raspberry-pi.md b/docs/raspberry-pi.md index 22fde68..8f10577 100644 --- a/docs/raspberry-pi.md +++ b/docs/raspberry-pi.md @@ -157,6 +157,7 @@ PL011 on GPIO 14/15: header pins 8 (TX) and 10 (RX), ground on pin 6, 115200 | VideoCore firmware mailbox (radio power, clocks, board MAC) | `BCM2849` (`RPIQ`) | `Drivers/Platform/RpiFirmware.cpp` | | GPIO pin functions | `BCM2845` (`GPI0`) | `Drivers/Platform/RpiGpio.cpp` | | Ethernet (GENET v5 MAC, BCM54213PE PHY) | `BCM6E4E` (`\_SB.ETH0`) | `Drivers/Net/Genet.cpp` | +| Wi-Fi (the same CYW4345x, SDIO) | Arasan SD controller `BCM2847`, GPIO 34-39 | `Drivers/Net/Wifi/Brcmf*.cpp` | The VL805 sits behind the BCM2711's PCIe, whose config space is not ECAM (the root bridge's `_DSD` says `linux-ecam-quirk-id = "bcm2711"`), so the @@ -239,18 +240,100 @@ The MAC address is the board's, read from OTP through the firmware mailbox driver programmed is the fallback. `dhcp.elf` waits up to 10 s for a wired link, since autonegotiation takes about 3 s after the PHY reset. +## Wi-Fi + +The radio's WLAN half is an SDIO card on the BCM2711's older SD controller, +the Arasan-derived EMMC (`BCM2847`, `0x7E300000`); the card slot is on +EMMC2, so the two never share a bus. Its pins are GPIO 34-39 (ALT3; clock +without a pull, the rest pulled up, as Linux's `sdio_pins`) and its power +switch, `WL_ON`, is line 1 of the firmware's GPIO expander. The probe +claims the controller only while the slot is on EMMC2 (`BRCME88C` present); +if the tables do not describe the Arasan, its address is taken as EMMC2's +less 256 KiB. + +It is a FullMAC radio: the firmware does the 802.11 MLME itself and the host +sees Ethernet frames. The driver follows Linux's brcmfmac (ISC license, +Broadcom; its notice is in `NOTICES.txt`): + +| File | Part | +|---|---| +| `BrcmfSdio.cpp` | the Arasan as an SDIO host: CMD5/3/7, 4-bit at 25 MHz, CMD52/CMD53, polled PIO, 32-bit register access | +| `BrcmfChip.cpp` | backplane window (function 1), clocks, the enumeration ROM's core list, firmware + NVRAM download, function 2, the firmware console | +| `BrcmfProto.cpp` | SDPCM framing and transmit credit, CDC ioctls, BDC data, events, the receive pump, the transmit queue | +| `BrcmfConnect.cpp` | preinit and CLM, escan, join, keys, link state | + +Once the root is mounted the idle loop: + +1. routes the pins, switches the EMMC clock on, pulses `WL_ON`, enumerates + the card (function 1 blocks of 64 bytes, function 2 of 512); +2. requests the ALP clock, reads the chip ID: `0x4345` revision 9 is the + CYW43456 (Pi 400), any other revision the CYW43455 (Pi 4B), which picks + `0:/os/firmware/brcm/brcmfmac43456-sdio.*` or `brcmfmac43455-sdio.*` + (shipped unmodified from Raspberry Pi OS's firmware-nonfree); +3. halts the ARM Cortex-R4, holds the 802.11 core in reset, sizes the RAM + from the CR4's banks, writes the firmware at `0x198000` and the NVRAM + (stripped to `key=value\0` pairs and a length token) at the top of RAM, + reading both back to verify, puts the reset vector at address 0 and + releases the CPU; +4. waits for the HT clock, sends the SDPCM protocol version through the + mailbox, enables function 2, the F2 watermark and MES busy signalling of + Linux's CYW43455 settings, save/restore if the chip has it, and the card + interrupt; +5. reads `cur_etheraddr` (the NVRAM's shared placeholder address + `b8:27:eb:74:f2:6c` is replaced with the board address + 1, as on + Raspberry Pi OS), loads the CLM blob (`clmload`, 1400-byte chunks: these + firmware builds carry no regulatory data of their own), turns off glom, + roaming, ARP/ND offload and power save, enables the events it handles and + brings the interface up (`WLC_UP`). + +Nothing uses the controller's interrupt: the bus is swept from the idle +loop (and from the timer tick when the cores never idle), every 10 ms while +a scan or a connection is active and every 250 ms otherwise, and at once when +the controller shows the card interrupt. Every sweep reads the SDIO core's +interrupt status, then every frame waiting in function 2 (a 4-byte header +read, then the rest). Ioctls wait for transmit credit and their answer by +sweeping the bus themselves. Frames are delivered with the bus unlocked, and +transmitting never waits for the bus: data frames go into a 32-frame queue +that whoever holds the bus, or the next sweep, sends. + +Scans are escans (all channels, active, firmware dwell times); each result +event carries a `brcmf_bss_info_le` whose information elements go into the +same scan table the Intel driver fills from beacons. A join sets `wpaie` +(the RSN element our supplicant builds), `wpa_auth`, `auth` and `wsec`, then +`join` with the BSSID and the chanspec escan reported. The 4-way handshake +runs in the host's supplicant (`Wpa.cpp`) as for the Intel adapter: EAPOL +frames are ordinary Ethernet frames here, and the keys go to the firmware +with `wsec_key` once message 4 has left the transmit queue. The radio does +AES-CCMP only: GCMP networks are refused, as are WPA3-only (SAE), WEP and +TKIP ones. + +Unanswered ioctls (a leaky bucket of three), a halt reported through +the mailbox, or repeated bus errors mark the radio failed: the firmware's +console and any trap record are printed to the kernel log (`WiFi-FW`), and +the facade's recovery powers the radio off and repeats the bring-up (at +most three times). + +`tests/wifi/run.sh` drives `BrcmfProto.cpp` and `BrcmfConnect.cpp` with the +real supplicant against a simulated firmware: preinit and the CLM chunks, +escan parsing, open and WPA2 joins (an independent authenticator checks +messages 2 and 4 and the installed keys), the wrong passphrase, refusals, +join timeouts and a firmware that stops answering. + ## Not supported yet - SD high-speed mode: the driver transfers by polled PIO at 25 MHz on a 4-bit bus (multi-block CMD18/CMD25 with Auto CMD12), at most 12.5 MB/s; switching the card to 50 MHz (CMD6) would double that. -- Wi-Fi (the CYW4345x's SDIO half, on the Arasan controller). +- Wi-Fi beyond 802.11 station mode on one interface: no access point mode, + no SDIO high speed (50 MHz), no glom, no firmware supplicant. - Audio (HDMI via VideoCore, the 3.5 mm jack via PWM). ## Status Bluetooth works on a Pi 400 (CYW43456), including A2DP music to -headphones. Ethernet has not run yet: QEMU has no GENET model. It is untested on a Pi 4B (CYW43455), and QEMU has no model of +headphones. Ethernet has not run yet: QEMU has no GENET model. Wi-Fi has +not run on hardware yet either (QEMU has no model of the radio); only its +protocol and connection layers are tested, in the host harness. It is untested on a Pi 4B (CYW43455), and QEMU has no model of the radio. Everything else above is verified on QEMU only, including the Pi-specific path: diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 6ed5512..97624f7 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 109 +#define MONTAUK_BUILD_NUMBER 123 diff --git a/kernel/src/CppLib/Spinlock.hpp b/kernel/src/CppLib/Spinlock.hpp index 0760605..8402a9f 100644 --- a/kernel/src/CppLib/Spinlock.hpp +++ b/kernel/src/CppLib/Spinlock.hpp @@ -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); } diff --git a/kernel/src/Drivers/Init.cpp b/kernel/src/Drivers/Init.cpp index 7d1adb7..843fd9f 100644 --- a/kernel/src/Drivers/Init.cpp +++ b/kernel/src/Drivers/Init.cpp @@ -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(); } diff --git a/kernel/src/Drivers/Net/Wifi/Brcmf.hpp b/kernel/src/Drivers/Net/Wifi/Brcmf.hpp new file mode 100644 index 0000000..32c54aa --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/Brcmf.hpp @@ -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 +#include + +// 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-sdio.. 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); + +} diff --git a/kernel/src/Drivers/Net/Wifi/BrcmfChip.cpp b/kernel/src/Drivers/Net/Wifi/BrcmfChip.cpp new file mode 100644 index 0000000..e9862bd --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/BrcmfChip.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +// 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(); + } + +} diff --git a/kernel/src/Drivers/Net/Wifi/BrcmfConnect.cpp b/kernel/src/Drivers/Net/Wifi/BrcmfConnect.cpp new file mode 100644 index 0000000..e344841 --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/BrcmfConnect.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +// 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); + } + +} diff --git a/kernel/src/Drivers/Net/Wifi/BrcmfProto.cpp b/kernel/src/Drivers/Net/Wifi/BrcmfProto.cpp new file mode 100644 index 0000000..7c2e75e --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/BrcmfProto.cpp @@ -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 +#include +#include +#include +#include +#include + +// 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 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; + } + +} diff --git a/kernel/src/Drivers/Net/Wifi/BrcmfSdio.cpp b/kernel/src/Drivers/Net/Wifi/BrcmfSdio.cpp new file mode 100644 index 0000000..2d11e2c --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/BrcmfSdio.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +// 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; + } + +} diff --git a/kernel/src/Drivers/Net/Wifi/BrcmfSdio.hpp b/kernel/src/Drivers/Net/Wifi/BrcmfSdio.hpp new file mode 100644 index 0000000..a6612f4 --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/BrcmfSdio.hpp @@ -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 + +// 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(); + +} diff --git a/kernel/src/Drivers/Net/Wifi/Iwx.hpp b/kernel/src/Drivers/Net/Wifi/Iwx.hpp index 0b49e2d..c39402f 100644 --- a/kernel/src/Drivers/Net/Wifi/Iwx.hpp +++ b/kernel/src/Drivers/Net/Wifi/Iwx.hpp @@ -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); } diff --git a/kernel/src/Drivers/Net/Wifi/IwxConnect.cpp b/kernel/src/Drivers/Net/Wifi/IwxConnect.cpp index a32054d..cc4353c 100644 --- a/kernel/src/Drivers/Net/Wifi/IwxConnect.cpp +++ b/kernel/src/Drivers/Net/Wifi/IwxConnect.cpp @@ -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 }; diff --git a/kernel/src/Drivers/Net/Wifi/Wifi.cpp b/kernel/src/Drivers/Net/Wifi/Wifi.cpp index ed45c6e..8fd77d3 100644 --- a/kernel/src/Drivers/Net/Wifi/Wifi.cpp +++ b/kernel/src/Drivers/Net/Wifi/Wifi.cpp @@ -14,6 +14,7 @@ #include "Wifi.hpp" #include #include "Iwx.hpp" +#include "Brcmf.hpp" #include "Wpa.hpp" #include #include @@ -34,6 +35,72 @@ namespace Drivers::Net::Wifi { static std::atomic 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; } diff --git a/kernel/src/Drivers/Net/Wifi/Wifi.hpp b/kernel/src/Drivers/Net/Wifi/Wifi.hpp index b4f70c2..07b04a8 100644 --- a/kernel/src/Drivers/Net/Wifi/Wifi.hpp +++ b/kernel/src/Drivers/Net/Wifi/Wifi.hpp @@ -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(); diff --git a/kernel/src/Drivers/Net/Wifi/Wpa.hpp b/kernel/src/Drivers/Net/Wifi/Wpa.hpp index 48aefd2..67f2622 100644 --- a/kernel/src/Drivers/Net/Wifi/Wpa.hpp +++ b/kernel/src/Drivers/Net/Wifi/Wpa.hpp @@ -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. diff --git a/kernel/src/Drivers/Platform/RpiFirmware.cpp b/kernel/src/Drivers/Platform/RpiFirmware.cpp index 026b6fb..8bc2aa3 100644 --- a/kernel/src/Drivers/Platform/RpiFirmware.cpp +++ b/kernel/src/Drivers/Platform/RpiFirmware.cpp @@ -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) { diff --git a/kernel/src/Drivers/Platform/RpiFirmware.hpp b/kernel/src/Drivers/Platform/RpiFirmware.hpp index 2043939..c066b3b 100644 --- a/kernel/src/Drivers/Platform/RpiFirmware.hpp +++ b/kernel/src/Drivers/Platform/RpiFirmware.hpp @@ -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) diff --git a/kernel/src/Timekeeping/Timer.cpp b/kernel/src/Timekeeping/Timer.cpp index b47393d..c36c4f9 100644 --- a/kernel/src/Timekeeping/Timer.cpp +++ b/kernel/src/Timekeeping/Timer.cpp @@ -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 diff --git a/programs/data/firmware/brcm/brcmfmac43455-sdio.bin b/programs/data/firmware/brcm/brcmfmac43455-sdio.bin new file mode 100644 index 0000000..fd7a32d Binary files /dev/null and b/programs/data/firmware/brcm/brcmfmac43455-sdio.bin differ diff --git a/programs/data/firmware/brcm/brcmfmac43455-sdio.clm_blob b/programs/data/firmware/brcm/brcmfmac43455-sdio.clm_blob new file mode 100644 index 0000000..c905a50 Binary files /dev/null and b/programs/data/firmware/brcm/brcmfmac43455-sdio.clm_blob differ diff --git a/programs/data/firmware/brcm/brcmfmac43455-sdio.txt b/programs/data/firmware/brcm/brcmfmac43455-sdio.txt new file mode 100644 index 0000000..9a20cb7 --- /dev/null +++ b/programs/data/firmware/brcm/brcmfmac43455-sdio.txt @@ -0,0 +1,98 @@ +# Cloned from bcm94345wlpagb_p2xx.txt +NVRAMRev=$Rev: 498373 $ +sromrev=11 +vendid=0x14e4 +devid=0x43ab +manfid=0x2d0 +prodid=0x06e4 +#macaddr=00:90:4c:c5:12:38 +macaddr=b8:27:eb:74:f2:6c +nocrc=1 +boardtype=0x6e4 +boardrev=0x1304 + +#XTAL 37.4MHz +xtalfreq=37400 + +btc_mode=1 +#------------------------------------------------------ +#boardflags: 5GHz eTR switch by default +# 2.4GHz eTR switch by default +# bit1 for btcoex +boardflags=0x00480201 +boardflags2=0x40800000 +boardflags3=0x44200100 +phycal_tempdelta=15 +rxchain=1 +txchain=1 +aa2g=1 +aa5g=1 +tssipos5g=1 +tssipos2g=1 +femctrl=0 +AvVmid_c0=1,165,2,100,2,100,2,100,2,100 +pa2ga0=-129,6525,-718 +pa2ga1=-149,4408,-601 +pa5ga0=-185,6836,-815,-186,6838,-815,-184,6859,-815,-184,6882,-818 +pa5ga1=-202,4285,-574,-201,4312,-578,-196,4391,-586,-201,4294,-575 +itrsw=1 +pdoffsetcckma0=2 +pdoffset2gperchan=0,-2,1,0,1,0,1,1,1,0,0,-1,-1,0 +pdoffset2g40ma0=16 +pdoffset40ma0=0x8888 +pdoffset80ma0=0x8888 +extpagain5g=2 +extpagain2g=2 +tworangetssi2g=1 +tworangetssi5g=1 +# LTECX flags +# WCI2 +ltecxmux=0 +ltecxpadnum=0x0504 +ltecxfnsel=0x22 +ltecxgcigpio=0x32 + +maxp2ga0=80 +ofdmlrbw202gpo=0x0022 +dot11agofdmhrbw202gpo=0x4442 +mcsbw202gpo=0x98444422 +mcsbw402gpo=0x98444422 +maxp5ga0=82,82,82,82 +mcsbw205glpo=0xb9555000 +mcsbw205gmpo=0xb9555000 +mcsbw205ghpo=0xb9555000 +mcsbw405glpo=0xb9555000 +mcsbw405gmpo=0xb9555000 +mcsbw405ghpo=0xb9555000 +mcsbw805glpo=0xb9555000 +mcsbw805gmpo=0xb9555000 +mcsbw805ghpo=0xb9555000 + +swctrlmap_2g=0x00000000,0x00000000,0x00000000,0x010000,0x3ff +swctrlmap_5g=0x00100010,0x00200020,0x00200020,0x010000,0x3fe +swctrlmapext_5g=0x00000000,0x00000000,0x00000000,0x000000,0x3 +swctrlmapext_2g=0x00000000,0x00000000,0x00000000,0x000000,0x3 + +vcodivmode=1 +deadman_to=481500000 + +ed_thresh2g=-54 +ed_thresh5g=-54 +eu_edthresh2g=-54 +eu_edthresh5g=-54 +ldo1=4 +rawtempsense=0x1ff +cckPwrIdxCorr=3 +cckTssiDelay=150 +ofdmTssiDelay=150 +txpwr2gAdcScale=1 +txpwr5gAdcScale=1 +dot11b_opts=0x3aa85 +cbfilttype=1 +fdsslevel_ch11=6 + +# Improved Bluetooth coexistence parameters from Cypress +btc_mode=1 +btc_params8=0x4e20 +btc_params1=0x7530 +btc_params50=0x972c diff --git a/programs/data/firmware/brcm/brcmfmac43456-sdio.bin b/programs/data/firmware/brcm/brcmfmac43456-sdio.bin new file mode 100644 index 0000000..0a9df2a Binary files /dev/null and b/programs/data/firmware/brcm/brcmfmac43456-sdio.bin differ diff --git a/programs/data/firmware/brcm/brcmfmac43456-sdio.clm_blob b/programs/data/firmware/brcm/brcmfmac43456-sdio.clm_blob new file mode 100644 index 0000000..36b8652 Binary files /dev/null and b/programs/data/firmware/brcm/brcmfmac43456-sdio.clm_blob differ diff --git a/programs/data/firmware/brcm/brcmfmac43456-sdio.txt b/programs/data/firmware/brcm/brcmfmac43456-sdio.txt new file mode 100644 index 0000000..a34c691 --- /dev/null +++ b/programs/data/firmware/brcm/brcmfmac43456-sdio.txt @@ -0,0 +1,97 @@ +# Cloned from bcm94345wlpagb_p2xx.txt +NVRAMRev=$Rev: 498373 $ +sromrev=11 +vendid=0x14e4 +devid=0x43ab +manfid=0x2d0 +prodid=0x06e4 +#macaddr=00:90:4c:c5:12:38 +macaddr=b8:27:eb:74:f2:6c +nocrc=1 +boardtype=0x6e4 +boardrev=0x1304 + +#XTAL 37.4MHz +xtalfreq=37400 + +btc_mode=1 +#------------------------------------------------------ +#boardflags: 5GHz eTR switch by default +# 2.4GHz eTR switch by default +# bit1 for btcoex +boardflags=0x00480201 +boardflags2=0x40800000 +boardflags3=0x44200100 +phycal_tempdelta=15 +rxchain=1 +txchain=1 +aa2g=1 +aa5g=1 +tssipos5g=1 +tssipos2g=1 +femctrl=0 +AvVmid_c0=0,157,1,126,1,126,1,126,1,126 +pa2ga0=-170,5896,-680 +pa2ga1=-156,4032,-547 +pa5ga0=-150,5547,-628,-150,5583,-640,-158,5608,-649,-160,5757,-673 +pa5ga1=-109,3831,-519,-88,3821,-512,-125,3731,-528,-103,3829,-516 +itrsw=1 +pdoffsetcckma0=2 +pdoffset2gperchan=0,-2,1,0,1,0,1,1,1,0,0,-1,-1,0 +pdoffset2g40ma0=16 +pdoffset40ma0=0x8888 +pdoffset80ma0=0x8888 +extpagain5g=2 +extpagain2g=2 +tworangetssi2g=1 +tworangetssi5g=1 +# LTECX flags +# WCI2 +ltecxmux=0 +ltecxpadnum=0x0504 +ltecxfnsel=0x22 +ltecxgcigpio=0x32 + +maxp2ga0=80 +ofdmlrbw202gpo=0x0022 +dot11agofdmhrbw202gpo=0x4442 +mcsbw202gpo=0x98444422 +mcsbw402gpo=0x98444422 +maxp5ga0=82,82,82,82 +mcsbw205glpo=0xb9555000 +mcsbw205gmpo=0xb9555000 +mcsbw205ghpo=0xb9555000 +mcsbw405glpo=0xb9555000 +mcsbw405gmpo=0xb9555000 +mcsbw405ghpo=0xb9555000 +mcsbw805glpo=0xb9555000 +mcsbw805gmpo=0xb9555000 +mcsbw805ghpo=0xb9555000 + +swctrlmap_2g=0x00000000,0x00000000,0x00000000,0x010000,0x3ff +swctrlmap_5g=0x00100010,0x00200020,0x00200020,0x010000,0x3fe +swctrlmapext_5g=0x00000000,0x00000000,0x00000000,0x000000,0x3 +swctrlmapext_2g=0x00000000,0x00000000,0x00000000,0x000000,0x3 + +vcodivmode=1 +deadman_to=481500000 + +ed_thresh2g=-54 +ed_thresh5g=-54 +eu_edthresh2g=-54 +eu_edthresh5g=-54 +ldo1=4 +rawtempsense=0x1ff +cckPwrIdxCorr=3 +cckTssiDelay=150 +ofdmTssiDelay=150 +txpwr2gAdcScale=1 +txpwr5gAdcScale=1 +dot11b_opts=0x3aa85 +cbfilttype=1 +fdsslevel_ch11=6 + +# Improved Bluetooth coexistence parameters from Cypress +btc_mode=1 +btc_params8=0x4e20 +btc_params1=0x7530 diff --git a/programs/data/licenses/NOTICES.txt b/programs/data/licenses/NOTICES.txt index 191d2ae..1817235 100644 --- a/programs/data/licenses/NOTICES.txt +++ b/programs/data/licenses/NOTICES.txt @@ -675,6 +675,61 @@ License: Cypress Wireless Connectivity Devices Driver End User License use with Cypress integrated circuit products. The full license text is included at 0:/os/licenses/Cypress-bt-firmware.txt. +================================================================================ +24. Cypress / Synaptics (Broadcom) Wi-Fi firmware +================================================================================ +Shipped as binary firmware, CLM regulatory blobs and NVRAM files +(0:/os/firmware/brcm/brcmfmac43455-sdio.{bin,clm_blob,txt} and +brcmfmac43456-sdio.{bin,clm_blob,txt}), loaded at runtime into the CYW43455 / +CYW43456 Wi-Fi radio of the Raspberry Pi 4 and 400 by the MontaukOS Wi-Fi +driver. The files are taken unmodified from Raspberry Pi OS's firmware-nonfree +package (brcmfmac43455-sdio.bin and .clm_blob are its +cypress/cyfmac43455-sdio-standard.bin and cyfmac43455-sdio.clm_blob). + + Upstream: https://github.com/RPi-Distro/firmware-nonfree + (debian/config/brcm80211/) + + CYW43455 firmware, CLM blob and NVRAM: + Copyright (c) Cypress Semiconductor Corporation (an Infineon company) + License: Cypress Wireless Connectivity Devices Driver End User License + Agreement (the same text as for the Bluetooth firmware above); full + text at 0:/os/licenses/Cypress-bt-firmware.txt. Reproduction and + distribution in object code form are permitted solely for use with + Cypress integrated circuit products. + + CYW43456 firmware, CLM blob and NVRAM: + Copyright (c) Synaptics Incorporated + License: Synaptics Wireless Connectivity Devices Driver End User License + Agreement (binary distribution); full text at + 0:/os/licenses/Synaptics-wifi-firmware.txt. Distribution in object code + form is permitted solely for use with Synaptics integrated circuit + products. + +================================================================================ +25. Linux brcmfmac (Broadcom FullMAC Wi-Fi driver) +================================================================================ +The MontaukOS Broadcom Wi-Fi driver (kernel/src/Drivers/Net/Wifi/Brcmf.hpp, +BrcmfChip.cpp, BrcmfProto.cpp, BrcmfConnect.cpp) is derived in part from the +brcmfmac driver in the Linux kernel (drivers/net/wireless/broadcom/brcm80211/): +its chip enumeration, core reset, clock and firmware start-up sequences, NVRAM +format, and the firmware interface's constants and structure layouts. + +License: ISC License + + Copyright (c) 2010-2014 Broadcom Corporation + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + ================================================================================ For licensing inquiries regarding MontaukOS itself, contact: licensing@montaukos.org ================================================================================ diff --git a/programs/data/licenses/Synaptics-wifi-firmware.txt b/programs/data/licenses/Synaptics-wifi-firmware.txt new file mode 100644 index 0000000..bd7eb2d --- /dev/null +++ b/programs/data/licenses/Synaptics-wifi-firmware.txt @@ -0,0 +1,161 @@ +SYNAPTICS WIRELESS CONNECTIVITY DEVICES +DRIVER END USER LICENSE AGREEMENT (BINARY DISTRIBUTION) + +PLEASE READ THIS END USER LICENSE AGREEMENT ("Agreement") CAREFULLY BEFORE +DOWNLOADING, INSTALLING, OR USING THIS SOFTWARE, ANY ACCOMPANYING +DOCUMENTATION, OR ANY UPDATES PROVIDED BY SYNAPTICS ("Software"). BY +DOWNLOADING, INSTALLING, OR USING THE SOFTWARE, YOU ARE AGREEING TO BE BOUND +BY THIS AGREEMENT. IF YOU DO NOT AGREE TO ALL OF THE TERMS OF THIS +AGREEMENT, PROMPTLY RETURN AND DO NOT USE THE SOFTWARE. IF YOU HAVE +PURCHASED THE SOFTWARE, YOUR RIGHT TO RETURN THE SOFTWARE EXPIRES 30 DAYS +AFTER YOUR PURCHASE AND APPLIES ONLY TO THE ORIGINAL PURCHASER. + +Software Provided in Binary Code Form. This paragraph applies to any Software +provided in binary code form. Subject to the terms and conditions of this +Agreement, Synaptics Incorporated ("Synaptics") grants you a +non-exclusive, non-transferable license under its copyright rights in the +Software to reproduce and distribute the Software in object code form only, +solely for use in connection with Synaptics integrated circuit products +("Purpose"). + +Proprietary Rights. The Software, including all intellectual property rights +therein, is and will remain the sole and exclusive property of Synaptics or its +suppliers. Except as otherwise expressly provided in this Agreement, you may +not: (i) modify, adapt, or create derivative works based upon the Software; +(ii) copy the Software; (iii) except and only to the extent explicitly +permitted by applicable law despite this limitation, decompile, translate, +reverse engineer, disassemble or otherwise reduce the Software to +human-readable form; or (iv) use the Software other than for the Purpose. + +No Support. Synaptics may, but is not required to, provide technical support for +the Software. + +Term and Termination. This Agreement is effective until terminated. This +Agreement and Your license rights will terminate immediately without notice +from Synaptics if you fail to comply with any provision of this Agreement. Upon +termination, you must destroy all copies of Software in your possession or +control. Termination of this Agreement will not affect any licenses validly +granted as of the termination date to any end users of the Software. The +following paragraphs shall survive any termination of this Agreement: +"Proprietary Rights," "Compliance With Law," +"Disclaimer," "Limitation of Liability," and "General." + +Compliance With Law. Each party agrees to comply with all applicable laws, +rules and regulations in connection with its activities under this Agreement. +Without limiting the foregoing, the Software may be subject to export control +laws and regulations of the United States and other countries. You agree to +comply strictly with all such laws and regulations and acknowledge that you +have the responsibility to obtain licenses to export, re-export, or import +the Software. + +Disclaimer. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, SYNAPTICS MAKES +NO WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, WITH REGARD TO THE SOFTWARE, +INCLUDING, BUT NOT LIMITED TO, INFRINGEMENT AND THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. Synaptics reserves the +right to make changes to the Software without notice. Synaptics does not assume +any liability arising out of the application or use of Software or any +product or circuit described in the Software. Synaptics does not authorize its +products for use as critical components in life-support systems where a +malfunction or failure may reasonably be expected to result in significant +injury to the user. The inclusion of Synaptics product in a life-support +system or application implies that the manufacturer of such system or +application assumes all risk of such use and in doing so indemnifies Synaptics +against all charges. + +Limitation of Liability. IN NO EVENT WILL SYNAPTICS OR ITS SUPPLIERS, +RESELLERS, OR DISTRIBUTORS BE LIABLE FOR ANY LOST REVENUE, PROFIT, OR DATA, +OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL, INCIDENTAL, OR PUNITIVE DAMAGES +HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE +USE OF OR INABILITY TO USE THE SOFTWARE EVEN IF SYNAPTICS OR ITS SUPPLIERS, +RESELLERS, OR DISTRIBUTORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. IN NO EVENT SHALL SYNAPTICS' OR ITS SUPPLIERS' RESELLERS', OR +DISTRIBUTORS' TOTAL LIABILITY TO YOU, WHETHER IN CONTRACT, TORT (INCLUDING +NEGLIGENCE), OR OTHERWISE, EXCEED THE PRICE PAID BY YOU FOR THE SOFTWARE. +THE FOREGOING LIMITATIONS SHALL APPLY EVEN IF THE ABOVE-STATED WARRANTY FAILS +OF ITS ESSENTIAL PURPOSE. BECAUSE SOME STATES OR JURISDICTIONS DO NOT ALLOW +LIMITATION OR EXCLUSION OF CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE +LIMITATION MAY NOT APPLY TO YOU. + +Restricted Rights. The Software under this Agreement is commercial computer +software as that term is described in 48 C.F.R. 252.227-7014(a)(1). If +acquired by or on behalf of a civilian agency, the U.S. Government acquires +this commercial computer software and/or commercial computer software +documentation subject to the terms of this Agreement as specified in 48 +C.F.R. 12.212 (Computer Software) and 12.211 (Technical Data) of the Federal +Acquisition Regulations ("FAR") and its successors. If acquired by or on +behalf of any agency within the Department of Defense ("DOD"), the U.S. +Government acquires this commercial computer software and/or commercial +computer software documentation subject to the terms of this Agreement as +specified in 48 C.F.R. 227.7202-3 of the DOD FAR Supplement ("DFAR") and its +successors. + +General. This Agreement will bind and inure to the benefit of each party's +successors and assigns, provided that you may not assign or transfer this +Agreement, in whole or in part, without Synaptics' written consent. This +Agreement shall be governed by and construed in accordance with the laws of +the State of California, United States of America, as if performed wholly +within the state and without giving effect to the principles of conflict of +law. The parties consent to personal and exclusive jurisdiction of and venue +in, the state and federal courts within Santa Clara County, California; +provided however, that nothing in this Agreement will limit Synaptics' right to +bring legal action in any venue in order to protect or enforce its +intellectual property rights. No failure of either party to exercise or +enforce any of its rights under this Agreement will act as a waiver of such +rights. If any portion hereof is found to be void or unenforceable, the +remaining provisions of this Agreement shall remain in full force and +effect. This Agreement is the complete and exclusive agreement between the +parties with respect to the subject matter hereof, superseding and replacing +any and all prior agreements, communications, and understandings (both +written and oral) regarding such subject matter. Any notice to Synaptics will +application assumes all risk of such use and in doing so indemnifies Synaptics +against all charges. + +Limitation of Liability. IN NO EVENT WILL SYNAPTICS OR ITS SUPPLIERS, +RESELLERS, OR DISTRIBUTORS BE LIABLE FOR ANY LOST REVENUE, PROFIT, OR DATA, +OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL, INCIDENTAL, OR PUNITIVE DAMAGES +HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE +USE OF OR INABILITY TO USE THE SOFTWARE EVEN IF SYNAPTICS OR ITS SUPPLIERS, +RESELLERS, OR DISTRIBUTORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. IN NO EVENT SHALL SYNAPTICS' OR ITS SUPPLIERS' RESELLERS', OR +DISTRIBUTORS' TOTAL LIABILITY TO YOU, WHETHER IN CONTRACT, TORT (INCLUDING +NEGLIGENCE), OR OTHERWISE, EXCEED THE PRICE PAID BY YOU FOR THE SOFTWARE. +THE FOREGOING LIMITATIONS SHALL APPLY EVEN IF THE ABOVE-STATED WARRANTY FAILS +OF ITS ESSENTIAL PURPOSE. BECAUSE SOME STATES OR JURISDICTIONS DO NOT ALLOW +LIMITATION OR EXCLUSION OF CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE +LIMITATION MAY NOT APPLY TO YOU. + +Restricted Rights. The Software under this Agreement is commercial computer +software as that term is described in 48 C.F.R. 252.227-7014(a)(1). If +acquired by or on behalf of a civilian agency, the U.S. Government acquires +this commercial computer software and/or commercial computer software +documentation subject to the terms of this Agreement as specified in 48 +C.F.R. 12.212 (Computer Software) and 12.211 (Technical Data) of the Federal +Acquisition Regulations ("FAR") and its successors. If acquired by or on +behalf of any agency within the Department of Defense ("DOD"), the U.S. +Government acquires this commercial computer software and/or commercial +computer software documentation subject to the terms of this Agreement as +specified in 48 C.F.R. 227.7202-3 of the DOD FAR Supplement ("DFAR") and its +successors. + +General. This Agreement will bind and inure to the benefit of each party's +successors and assigns, provided that you may not assign or transfer this +Agreement, in whole or in part, without Synaptics' written consent. This +Agreement shall be governed by and construed in accordance with the laws of +the State of California, United States of America, as if performed wholly +within the state and without giving effect to the principles of conflict of +law. The parties consent to personal and exclusive jurisdiction of and venue +in, the state and federal courts within Santa Clara County, California; +provided however, that nothing in this Agreement will limit Synaptics' right to +bring legal action in any venue in order to protect or enforce its +intellectual property rights. No failure of either party to exercise or +enforce any of its rights under this Agreement will act as a waiver of such +rights. If any portion hereof is found to be void or unenforceable, the +remaining provisions of this Agreement shall remain in full force and +effect. This Agreement is the complete and exclusive agreement between the +parties with respect to the subject matter hereof, superseding and replacing +any and all prior agreements, communications, and understandings (both +written and oral) regarding such subject matter. Any notice to Synaptics will +be deemed effective when actually received and must be sent to Synaptics +Incorporated, ATTN: General Counsel, 1109 McKay Dr. +San Jose, CA 95131, USA + diff --git a/programs/src/wifi/main.cpp b/programs/src/wifi/main.cpp index 0579977..c2b3047 100644 --- a/programs/src/wifi/main.cpp +++ b/programs/src/wifi/main.cpp @@ -146,8 +146,8 @@ static abi::WifiNetwork g_nets[64]; static bool require_adapter(abi::WifiInfo& info) { if (wifi_info(&info) != 0 || !info.present) { print("wifi: no supported Wi-Fi adapter found.\n"); - print(" Only Intel AX210-family devices with an RF type of GF " - "(AX211) are supported.\n"); + print(" Supported: Intel AX210-family devices with an RF type of GF " + "(AX211), and the Raspberry Pi 4/400's on-board radio.\n"); return false; } if (info.state != abi::WIFI_STATE_RUNNING) { diff --git a/tests/wifi/brcmf_harness.cpp b/tests/wifi/brcmf_harness.cpp new file mode 100644 index 0000000..85db8e1 --- /dev/null +++ b/tests/wifi/brcmf_harness.cpp @@ -0,0 +1,769 @@ +/* + * brcmf_harness.cpp + * Host harness for the Broadcom FullMAC driver's protocol and connection + * layers (BrcmfProto.cpp, BrcmfConnect.cpp, with the real supplicant). + * + * The SDIO bus and the backplane are replaced by a simulated CYW4345x + * firmware: it decodes every SDPCM frame the driver writes to function 2 + * (CDC ioctls, BDC data), answers ioctls the way the firmware does, and + * feeds back events and data frames through the same framing. The access + * point side of the WPA2 handshake is written here independently of + * Wpa.cpp, on the crypto primitives crypto_vectors checks against the + * published vectors. + * + * What stays untested is everything that needs the chip: the SDIO host, + * the backplane, firmware download (BrcmfSdio.cpp, BrcmfChip.cpp), and + * the firmware's own opinion of the structures it is sent. + * + * Copyright (c) 2026 Daniel Hammer +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include "Drivers/Net/Wifi/Brcmf.hpp" +#include "Drivers/Net/Wifi/BrcmfSdio.hpp" +#include "Drivers/Net/Wifi/Ieee80211.hpp" +#include "Drivers/Net/Wifi/Wpa.hpp" +#include "Drivers/Platform/RpiFirmware.hpp" +#include "Libraries/Crypto.hpp" + +using namespace Drivers::Net::Wifi; +using Bytes = std::vector; + +namespace Timekeeping { + uint64_t g_ms = 1000; + uint64_t GetMilliseconds() { return g_ms; } + void BusyWaitMs(uint64_t ms) { g_ms += ms; } + void BusyWaitUs(uint64_t us) { g_ms += (us + 999) / 1000; } +} + +static int g_fails = 0; +static void check(bool cond, const char* what) { + printf(" %s %s\n", cond ? "PASS" : "FAIL", what); + if (!cond) g_fails++; +} + +static uint16_t be16(const uint8_t* p) { return (uint16_t)(p[0] << 8 | p[1]); } +static uint16_t le16(const uint8_t* p) { return (uint16_t)(p[0] | p[1] << 8); } +static uint32_t le32(const uint8_t* p) { uint32_t v; memcpy(&v, p, 4); return v; } +static void putle16(Bytes& b, uint16_t v) { b.push_back((uint8_t)v); b.push_back((uint8_t)(v >> 8)); } +static void putle32(Bytes& b, uint32_t v) { for (int i = 0; i < 4; i++) b.push_back((uint8_t)(v >> (8 * i))); } +static void putbe16(Bytes& b, uint16_t v) { b.push_back((uint8_t)(v >> 8)); b.push_back((uint8_t)v); } +static void putbe32(Bytes& b, uint32_t v) { for (int i = 3; i >= 0; i--) b.push_back((uint8_t)(v >> (8 * i))); } +static void append(Bytes& b, const uint8_t* p, size_t n) { b.insert(b.end(), p, p + n); } + +static const uint8_t PLACEHOLDER_MAC[6] = { 0xb8, 0x27, 0xeb, 0x74, 0xf2, 0x6c }; +static const uint8_t BOARD_MAC[6] = { 0xdc, 0xa6, 0x32, 0x01, 0x02, 0xff }; +static const uint8_t WLAN_MAC[6] = { 0xdc, 0xa6, 0x32, 0x01, 0x03, 0x00 }; // board + 1, with carry +static const uint8_t AP_OPEN[6] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x01 }; +static const uint8_t AP_WPA[6] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x02 }; + +// ============================================================================= +// Simulated firmware +// ============================================================================= + +struct Ioctl { + uint32_t Cmd; + bool Set; + std::string Name; // iovar name, for GET_VAR/SET_VAR + Bytes Data; // what follows the name (or the whole payload) + uint64_t Order; +}; + +struct Fw { + std::deque ToHost; // complete SDPCM frames + Bytes Cur; + size_t CurOff = 0; + uint8_t HostSeq = 0; // next sequence number expected + uint8_t Window = 8; + bool SeqError = false; + bool Mute = false; // stop answering ioctls + std::vector Ioctls; + std::vector> DataTx; // Ethernet frames, order + uint64_t Order = 0; + bool AutoJoin = true; + void (*OnEapol)(const Bytes& body) = nullptr; +}; +static Fw fw; + +static void QueueFrame(uint8_t channel, const Bytes& payload) { + Bytes f(12, 0); + uint16_t len = (uint16_t)(12 + payload.size()); + f[0] = (uint8_t)len; f[1] = (uint8_t)(len >> 8); + f[2] = (uint8_t)~len; f[3] = (uint8_t)(~len >> 8); + f[4] = 0; + f[5] = channel; + f[7] = 12; + f[9] = (uint8_t)(fw.HostSeq + fw.Window); // credit + append(f, payload.data(), payload.size()); + fw.ToHost.push_back(f); +} + +static Bytes Bdc(const Bytes& eth) { + Bytes b = { 0x20, 0, 0, 0 }; + append(b, eth.data(), eth.size()); + return b; +} + +static void QueueEvent(uint32_t type, uint32_t status, uint32_t reason, uint16_t flags, + const uint8_t* addr, const Bytes& data = {}) { + Bytes e; + append(e, WLAN_MAC, 6); + append(e, AP_WPA, 6); + putbe16(e, 0x886C); + putbe16(e, 0x8001); + putbe16(e, (uint16_t)(48 + data.size())); + e.push_back(1); + e.push_back(0x00); e.push_back(0x10); e.push_back(0x18); + putbe16(e, 1); + putbe16(e, 2); // version + putbe16(e, flags); + putbe32(e, type); + putbe32(e, status); + putbe32(e, reason); + putbe32(e, 0); // auth type + putbe32(e, (uint32_t)data.size()); + Bytes a(6, 0); + if (addr) memcpy(a.data(), addr, 6); + append(e, a.data(), 6); + Bytes ifname(16, 0); + memcpy(ifname.data(), "wlan0", 5); + append(e, ifname.data(), 16); + e.push_back(0); // ifidx + e.push_back(0); // bsscfgidx + append(e, data.data(), data.size()); + QueueFrame(1, Bdc(e)); +} + +static void QueueEthernet(const uint8_t* src, uint16_t type, const Bytes& body) { + Bytes e; + append(e, WLAN_MAC, 6); + append(e, src, 6); + putbe16(e, type); + append(e, body.data(), body.size()); + QueueFrame(2, Bdc(e)); +} + +// One brcmf_bss_info_le, laid out as Linux's struct. +static Bytes BssInfo(const uint8_t* bssid, const char* ssid, uint16_t chanspec, uint8_t ctl, + int16_t rssi, uint16_t caps, const Bytes& ies) { + Bytes b(128, 0); + memcpy(&b[0], "\x6d\x00\x00\x00", 4); // version 109 + uint32_t len = (uint32_t)(128 + ies.size()); + memcpy(&b[4], &len, 4); + memcpy(&b[8], bssid, 6); + uint16_t bcn = 100; + memcpy(&b[14], &bcn, 2); + memcpy(&b[16], &caps, 2); + b[18] = (uint8_t)strlen(ssid); + memcpy(&b[19], ssid, strlen(ssid)); + memcpy(&b[72], &chanspec, 2); + b[76] = 3; + memcpy(&b[78], &rssi, 2); + b[88] = ctl; + uint16_t ieOff = 128; + memcpy(&b[116], &ieOff, 2); + uint32_t ieLen = (uint32_t)ies.size(); + memcpy(&b[120], &ieLen, 4); + append(b, ies.data(), ies.size()); + return b; +} + +static const Bytes RSN_CCMP = { + 0x01, 0x00, 0x00, 0x0f, 0xac, 0x04, 0x01, 0x00, 0x00, 0x0f, 0xac, 0x04, + 0x01, 0x00, 0x00, 0x0f, 0xac, 0x02, 0x00, 0x00, +}; +static const Bytes RSN_GCMP = { + 0x01, 0x00, 0x00, 0x0f, 0xac, 0x08, 0x01, 0x00, 0x00, 0x0f, 0xac, 0x08, + 0x01, 0x00, 0x00, 0x0f, 0xac, 0x02, 0x00, 0x00, +}; + +static Bytes Ies(const char* ssid, uint8_t channel, const Bytes* rsn) { + Bytes ie; + ie.push_back(0); ie.push_back((uint8_t)strlen(ssid)); + append(ie, (const uint8_t*)ssid, strlen(ssid)); + ie.push_back(1); ie.push_back(4); ie.push_back(0x82); ie.push_back(0x84); ie.push_back(0x0b); ie.push_back(0x16); + ie.push_back(3); ie.push_back(1); ie.push_back(channel); + if (rsn) { + ie.push_back(48); ie.push_back((uint8_t)rsn->size()); + append(ie, rsn->data(), rsn->size()); + } + return ie; +} + +static void QueueEscan(uint16_t syncId) { + Bytes bss1 = BssInfo(AP_OPEN, "OpenNet", 0x1006, 6, -48, 0x0401, Ies("OpenNet", 6, nullptr)); + Bytes bss2 = BssInfo(AP_WPA, "SecureNet", 0xd024, 36, -61, 0x0411, Ies("SecureNet", 36, &RSN_CCMP)); + for (const Bytes* bss : { &bss1, &bss2 }) { + Bytes r; + putle32(r, (uint32_t)(12 + bss->size())); + putle32(r, 1); + putle16(r, syncId); + putle16(r, 1); + append(r, bss->data(), bss->size()); + QueueEvent(69, 8, 0, 0, nullptr, r); // PARTIAL + } + QueueEvent(69, 0, 0, 0, nullptr); // done +} + +static void FwControl(const uint8_t* p, size_t len) { + uint32_t cmd = le32(p), dlen = le32(p + 4), flags = le32(p + 8); + bool set = flags & 2; + const uint8_t* data = p + 16; + size_t avail = len - 16; + if (dlen > avail) dlen = (uint32_t)avail; + + Ioctl io{cmd, set, "", {}, fw.Order++}; + if (cmd == 262 || cmd == 263) { + size_t n = strnlen((const char*)data, dlen); + io.Name.assign((const char*)data, n); + if (n + 1 < dlen) io.Data.assign(data + n + 1, data + dlen); + } else { + io.Data.assign(data, data + dlen); + } + fw.Ioctls.push_back(io); + if (fw.Mute) return; + + int32_t status = 0; + Bytes resp(dlen, 0); + if (cmd == 262) { + if (io.Name == "cur_etheraddr") memcpy(resp.data(), PLACEHOLDER_MAC, 6); + else if (io.Name == "ver") { + const char* v = "wl0: Aug 29 2023 01:51:02 version 7.45.265 (28bca26 CY) FWID 01-b677b91b\n"; + memcpy(resp.data(), v, strlen(v) < dlen ? strlen(v) : dlen); + } else if (io.Name == "clmload_status") { + } else status = -23; // BCME_UNSUPPORTED + } else if (cmd == 263) { + if (io.Name == "escan") { + QueueEscan(le16(io.Data.data() + 6)); + } else if (io.Name == "join" && fw.AutoJoin) { + QueueEvent(0, 0, 0, 0, io.Data.data() + 56); + QueueEvent(16, 0, 0, 1, io.Data.data() + 56); + } else if (io.Name == "bus:rxglom" || io.Name == "mfp" || io.Name == "sup_wpa") { + status = -23; + } + } else if (cmd == 217) { // valid channels + uint32_t list[3] = { 2, 1, 36 }; + memcpy(resp.data(), list, sizeof(list) < dlen ? sizeof(list) : dlen); + } + + Bytes r; + putle32(r, cmd); + putle32(r, dlen); + putle32(r, (flags & 0xFFFF0002u) | (status < 0 ? 1 : 0)); + putle32(r, (uint32_t)status); + append(r, resp.data(), resp.size()); + QueueFrame(0, r); +} + +static void FwReceive(const uint8_t* f, size_t busLen) { + uint16_t len = le16(f), check = le16(f + 2); + if ((uint16_t)(len ^ check) != 0xFFFF || len > busLen) { fw.SeqError = true; return; } + if (f[4] != fw.HostSeq) fw.SeqError = true; + fw.HostSeq = (uint8_t)(f[4] + 1); + uint8_t ch = f[5] & 0x0F, off = f[7]; + const uint8_t* p = f + off; + size_t plen = len - off; + if (ch == 0) { + FwControl(p, plen); + } else if (ch == 2) { + if (plen < 4 || (p[0] >> 4) != 2 || p[3] != 0) { fw.SeqError = true; return; } + Bytes eth(p + 4, p + plen); + fw.DataTx.push_back({eth, fw.Order++}); + if (eth.size() >= 14 && be16(ð[12]) == 0x888E && fw.OnEapol) + fw.OnEapol(Bytes(eth.begin() + 14, eth.end())); + } +} + +// ============================================================================= +// What the driver links against instead of the hardware +// ============================================================================= + +namespace Drivers::Net::Wifi { + + BrcmfDevice g_brcmf; + + bool BrcmfBpRead32(uint32_t addr, uint32_t& value) { + value = 0; + if (addr == g_brcmf.SdioCoreBase + 0x20 && (!fw.ToHost.empty() || !fw.Cur.empty())) + value = 0x40; // I_HMB_FRAME_IND + return true; + } + bool BrcmfBpWrite32(uint32_t, uint32_t) { return true; } + bool BrcmfBpReadMem(uint32_t, uint8_t*, uint32_t) { return true; } + bool BrcmfBpWriteMem(uint32_t, const uint8_t*, uint32_t) { return true; } + bool BrcmfBpSelectWindow(uint32_t) { return true; } + bool BrcmfF1Read8(uint32_t, uint8_t& v) { v = 0; return true; } + bool BrcmfF1Write8(uint32_t, uint8_t) { return true; } + + bool BrcmfChipBringUp() { + g_brcmf.FwName = "43456"; + g_brcmf.SdioCoreBase = 0x18002000; + g_brcmf.HostIntMask = 0x200000F0; + return true; + } + void BrcmfChipPowerOff() {} + void BrcmfDumpConsole() {} + + uint8_t* BrcmfLoadFile(const char* ext, uint32_t& size, bool) { + if (strcmp(ext, "clm_blob") != 0) return nullptr; + size = 3000; + uint8_t* p = (uint8_t*)malloc(size); + for (uint32_t i = 0; i < size; i++) p[i] = (uint8_t)i; + return p; + } + + struct Seen { Bytes Bssid; Bytes Ies; uint16_t Beacon, Caps; uint8_t Channel; int8_t Rssi; }; + static std::vector g_seen; + static std::vector g_delivered; + + 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) { + g_seen.push_back({Bytes(bssid, bssid + 6), Bytes(ies, ies + iesLen), beaconInterval, + capability, channel, rssiDbm}); + } + void WifiRxEthernet(const uint8_t* frame, uint32_t len) { + g_delivered.push_back(Bytes(frame, frame + len)); + } + + // Routed by Wifi.cpp in the kernel. + bool WpaTxEapol(const uint8_t* body, uint32_t len) { return BrcmfWpaTxEapol(body, len); } + bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) { + return BrcmfWpaInstallPtk(tk, tkLen, cipher); + } + bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx, + uint8_t cipher, const uint8_t* rsc) { + return BrcmfWpaInstallGtk(gtk, gtkLen, keyIdx, cipher, rsc); + } +} + +namespace Drivers::Net::Wifi::Sdio { + bool HostInit(uint64_t, uint32_t) { return true; } + bool CardInit() { return true; } + bool Read8(uint32_t, uint32_t, uint8_t& v) { v = 0; return true; } + bool Write8(uint32_t, uint32_t, uint8_t) { return true; } + bool EnableFunction(uint32_t, bool, uint64_t) { return true; } + bool EnableCardInterrupts(uint8_t) { return true; } + void Abort(uint32_t) {} + bool CardInterruptPending() { return !fw.ToHost.empty(); } + bool IsInitialized() { return true; } + + bool ReadBlock(uint32_t fn, uint32_t, bool, uint8_t* buffer, uint32_t len) { + if (fn != FN2 || (len & 3)) return false; + Timekeeping::g_ms++; + if (fw.Cur.empty()) { + if (fw.ToHost.empty()) { memset(buffer, 0, len); return true; } + fw.Cur = fw.ToHost.front(); + fw.ToHost.pop_front(); + fw.CurOff = 0; + } + for (uint32_t i = 0; i < len; i++, fw.CurOff++) + buffer[i] = fw.CurOff < fw.Cur.size() ? fw.Cur[fw.CurOff] : 0; + if (fw.CurOff >= ((fw.Cur.size() + 3) & ~(size_t)3)) fw.Cur.clear(); + return true; + } + + bool WriteBlock(uint32_t fn, uint32_t, bool, const uint8_t* buffer, uint32_t len) { + if (fn != FN2 || (len & 3)) return false; + if (len > FN2_BLOCK && len % FN2_BLOCK) fw.SeqError = true; // padded to whole blocks + Timekeeping::g_ms++; + FwReceive(buffer, len); + return true; + } +} + +namespace Drivers::Platform::RpiFirmware { + bool GetBoardMacAddress(uint8_t mac[6]) { memcpy(mac, BOARD_MAC, 6); return true; } + bool SetGpioOutput(uint32_t, bool) { return true; } +} + +// ============================================================================= +// An independent WPA2-PSK authenticator +// ============================================================================= + +namespace Ap { + using namespace Kt::Crypto; + static const char* SSID = "SecureNet"; + static const char* PASS = "supersecret123"; + static uint8_t pmk[32], ptk[48], anonce[32], snonce[32], gtk[16]; + static uint64_t replay = 0; + static Bytes lastMsg2, lastMsg4, staRsn; + static bool msg2MicOk = false, msg4MicOk = false; + + static void Prf(const uint8_t* key, const char* label, const Bytes& data, uint8_t* out, size_t n) { + uint8_t counter = 0; + size_t done = 0; + while (done < n) { + Bytes m((const uint8_t*)label, (const uint8_t*)label + strlen(label) + 1); + append(m, data.data(), data.size()); + m.push_back(counter++); + uint8_t h[20]; + HmacSha1(key, 32, m.data(), m.size(), h); + size_t take = n - done < 20 ? n - done : 20; + memcpy(out + done, h, take); + done += take; + } + } + + static Bytes KeyFrame(uint16_t info, const Bytes& keyData, bool mic) { + Bytes k(99, 0); + k[0] = 2; k[1] = 3; + uint16_t bodyLen = (uint16_t)(95 + keyData.size()); + k[2] = (uint8_t)(bodyLen >> 8); k[3] = (uint8_t)bodyLen; + k[4] = 2; + k[5] = (uint8_t)(info >> 8); k[6] = (uint8_t)info; + k[8] = 16; + for (int i = 0; i < 8; i++) k[9 + i] = (uint8_t)(replay >> (56 - 8 * i)); + memcpy(&k[17], anonce, 32); + k[97] = (uint8_t)(keyData.size() >> 8); k[98] = (uint8_t)keyData.size(); + append(k, keyData.data(), keyData.size()); + if (mic) { + uint8_t h[20]; + HmacSha1(ptk, 16, k.data(), k.size(), h); + memcpy(&k[81], h, 16); + } + return k; + } + + static bool MicOk(Bytes frame) { + if (frame.size() < 99) return false; + uint8_t mic[16]; + memcpy(mic, &frame[81], 16); + memset(&frame[81], 0, 16); + uint8_t h[20]; + HmacSha1(ptk, 16, frame.data(), frame.size(), h); + return memcmp(h, mic, 16) == 0; + } + + static void Start(const char* pass) { + Pbkdf2Sha1(pass, strlen(pass), (const uint8_t*)SSID, strlen(SSID), 4096, pmk, 32); + for (int i = 0; i < 32; i++) anonce[i] = (uint8_t)(0xA0 + i); + for (int i = 0; i < 16; i++) gtk[i] = (uint8_t)(0x30 + i); + replay = 1; + lastMsg2.clear(); lastMsg4.clear(); staRsn.clear(); + msg2MicOk = msg4MicOk = false; + QueueEthernet(AP_WPA, 0x888E, KeyFrame(0x008A, {}, false)); // msg 1 + } + + static void OnEapol(const Bytes& f) { + if (f.size() < 99 || f[1] != 3) return; + uint16_t info = be16(&f[5]); + if (!(info & 0x0100)) return; + if (lastMsg2.empty()) { + lastMsg2 = f; + memcpy(snonce, &f[17], 32); + Bytes data; + const uint8_t* a = memcmp(AP_WPA, WLAN_MAC, 6) < 0 ? AP_WPA : WLAN_MAC; + const uint8_t* b = a == AP_WPA ? WLAN_MAC : AP_WPA; + append(data, a, 6); append(data, b, 6); + const uint8_t* n1 = memcmp(anonce, snonce, 32) < 0 ? anonce : snonce; + const uint8_t* n2 = n1 == anonce ? snonce : anonce; + append(data, n1, 32); append(data, n2, 32); + Prf(pmk, "Pairwise key expansion", data, ptk, 48); + msg2MicOk = MicOk(f); + uint16_t kdl = be16(&f[97]); + staRsn.assign(f.begin() + 99, f.begin() + 99 + kdl); + if (!msg2MicOk) { + // Wrong passphrase: the AP gives up and deauthenticates. + QueueEvent(6, 0, 15, 0, AP_WPA); + return; + } + // Message 3: our RSN IE and the GTK KDE, key-wrapped under the KEK. + Bytes plain = { 0x30, (uint8_t)RSN_CCMP.size() }; + append(plain, RSN_CCMP.data(), RSN_CCMP.size()); + Bytes kde = { 0xdd, 22, 0x00, 0x0f, 0xac, 0x01, 0x01, 0x00 }; + append(kde, gtk, 16); + append(plain, kde.data(), kde.size()); + plain.push_back(0xdd); + while (plain.size() % 8) plain.push_back(0); + Bytes wrapped(plain.size() + 8); + AesKeyWrap(ptk + 16, 16, plain.data(), plain.size(), wrapped.data()); + replay = 2; + QueueEthernet(AP_WPA, 0x888E, KeyFrame(0x13CA, wrapped, true)); + } else if (lastMsg4.empty()) { + lastMsg4 = f; + msg4MicOk = MicOk(f); + } + } +} + +// ============================================================================= +// Cases +// ============================================================================= + +static void Pump(int rounds = 20) { + for (int i = 0; i < rounds; i++) { + BrcmfService(); + Timekeeping::g_ms += 5; + } +} + +static const Ioctl* LastSet(const char* name) { + for (auto it = fw.Ioctls.rbegin(); it != fw.Ioctls.rend(); ++it) + if (it->Set && it->Name == name) return &*it; + return nullptr; +} + +static uint32_t SetInt(const char* name) { + const Ioctl* io = LastSet(name); + return io && io->Data.size() >= 4 ? le32(io->Data.data()) : 0xDEADBEEF; +} + +static const Ioctl* LastCmd(uint32_t cmd) { + for (auto it = fw.Ioctls.rbegin(); it != fw.Ioctls.rend(); ++it) + if (it->Cmd == cmd) return &*it; + return nullptr; +} + +static constexpr int CONN_IDLE = 0, CONN_ASSOCIATING = 4, CONN_HANDSHAKING = 6, + CONN_CONNECTED = 7, CONN_FAILED = 8; + +static void CaseInit() { + printf("== bring-up and preinit ==\n"); + check(BrcmfProbe(0xFE300000), "the Arasan controller is claimed"); + check(BrcmfCompleteInit(), "preinit completes against the firmware"); + check(!fw.SeqError, "every frame carries the next sequence number and a valid header"); + check(BrcmfGetState() == BrcmfState::Running, "the radio is running"); + check(memcmp(g_brcmf.Mac, WLAN_MAC, 6) == 0, "the NVRAM placeholder MAC becomes the board address + 1"); + const Ioctl* mac = LastSet("cur_etheraddr"); + check(mac && mac->Data.size() == 6 && memcmp(mac->Data.data(), WLAN_MAC, 6) == 0, + "and is programmed into the firmware"); + check(strcmp(g_brcmf.FwVersion, "7.45.265") == 0, "the firmware version is read from \"ver\""); + + std::vector clm; + for (auto& io : fw.Ioctls) if (io.Set && io.Name == "clmload") clm.push_back(&io); + check(clm.size() == 3, "a 3000-byte CLM blob goes down in three chunks"); + if (clm.size() == 3) { + check(le16(clm[0]->Data.data()) == 0x1002 && le16(clm[1]->Data.data()) == 0x1000 && + le16(clm[2]->Data.data()) == 0x1004, "flagged begin / middle / end, handler version 1"); + check(le16(clm[0]->Data.data() + 2) == 2, "of type CLM"); + check(le32(clm[0]->Data.data() + 4) == 1400 && le32(clm[2]->Data.data() + 4) == 200, + "with 1400-byte chunks and the remainder last"); + check(clm[1]->Data[12] == (uint8_t)1400 && clm[2]->Data[12 + 199] == (uint8_t)2999, + "carrying the blob's bytes in order"); + } + + const Ioctl* ev = LastSet("event_msgs"); + bool evOk = ev && ev->Data.size() == 16; + if (evOk) { + for (uint32_t e : { 0u, 5u, 6u, 12u, 16u, 69u }) + evOk = evOk && (ev->Data[e / 8] & (1u << (e % 8))); + } + check(evOk, "events for join, link, deauth/disassoc and escan are enabled"); + check(SetInt("roam_off") == 1, "firmware roaming is off"); + const Ioctl* up = LastCmd(2); + check(up && up->Set, "WLC_UP is sent"); + const Ioctl* pm = LastCmd(86); + check(pm && le32(pm->Data.data()) == 0, "power save is off"); + check(g_brcmf.ChannelCount == 2 && g_brcmf.Band24 && g_brcmf.Band5, + "the valid channel list gives the channel count and bands"); + + uint32_t v = 0; + check(!BrcmfIovarGetInt("no_such_iovar", v, true), "an iovar the firmware rejects reports failure"); + check(BrcmfGetState() == BrcmfState::Running, "without counting against the firmware's health"); +} + +static void CaseScan() { + printf("== escan ==\n"); + check(BrcmfStartScan(), "a scan starts"); + const Ioctl* es = LastSet("escan"); + check(es && es->Data.size() == 72, "escan carries the 72-byte v1 parameter block"); + if (es && es->Data.size() == 72) { + const uint8_t* p = es->Data.data(); + check(le32(p) == 1 && le16(p + 4) == 1, "version 1, action start"); + bool bcast = true; + for (int i = 0; i < 6; i++) bcast = bcast && p[44 + i] == 0xFF; + check(bcast && p[50] == 2 && p[51] == 0, "broadcast BSSID, any BSS type, active"); + check(le32(p + 52) == 0xFFFFFFFF && le32(p + 68) == 0, "default dwells, every channel"); + } + Pump(); + check(!BrcmfScanActive(), "the completion event ends the scan"); + check(g_seen.size() == 2, "both BSSs reach the scan table"); + if (g_seen.size() == 2) { + check(memcmp(g_seen[0].Bssid.data(), AP_OPEN, 6) == 0 && g_seen[0].Channel == 6 && + g_seen[0].Rssi == -48 && g_seen[0].Beacon == 100, "BSSID, channel, RSSI, beacon interval"); + check(g_seen[1].Channel == 36 && (g_seen[1].Caps & 0x10), "the 5 GHz network, privacy set"); + check(g_seen[1].Ies.size() == Ies("SecureNet", 36, &RSN_CCMP).size() && g_seen[1].Ies[0] == 0, + "with its information elements intact"); + } +} + +static void CaseOpenJoin() { + printf("== open network ==\n"); + check(BrcmfConnectStart(AP_OPEN, 6, "OpenNet", nullptr, nullptr, 0), "the join is accepted"); + check(SetInt("wpa_auth") == 0 && SetInt("wsec") == 0 && SetInt("auth") == 0, + "no WPA, no encryption, open system authentication"); + const Ioctl* j = LastSet("join"); + check(j && j->Data.size() == 72, "join carries brcmf_ext_join_params_le"); + if (j && j->Data.size() == 72) { + const uint8_t* p = j->Data.data(); + check(le32(p) == 7 && memcmp(p + 4, "OpenNet", 7) == 0, "the SSID"); + check(memcmp(p + 56, AP_OPEN, 6) == 0 && le32(p + 64) == 1 && le16(p + 68) == 0x1006, + "the BSSID and the chanspec escan reported"); + } + check(BrcmfConnectState() == CONN_ASSOCIATING, "associating until the firmware says otherwise"); + Pump(); + check(BrcmfConnectState() == CONN_CONNECTED && BrcmfLinkUp(), "E_SET_SSID + E_LINK bring the link up"); + + Bytes eth; + append(eth, AP_OPEN, 6); append(eth, WLAN_MAC, 6); putbe16(eth, 0x0800); + for (int i = 0; i < 100; i++) eth.push_back((uint8_t)i); + size_t before = fw.DataTx.size(); + check(BrcmfSendEthernet(eth.data(), (uint32_t)eth.size()), "an Ethernet frame is accepted"); + Pump(2); + check(fw.DataTx.size() == before + 1 && fw.DataTx.back().first == eth, + "and reaches the firmware unchanged behind a BDC header"); + + Bytes big = eth; + big.resize(1514, 0x5a); + check(BrcmfSendEthernet(big.data(), (uint32_t)big.size()), "a full-size frame is accepted"); + Pump(2); + check(fw.DataTx.back().first == big && !fw.SeqError, "and goes out padded to whole blocks"); + + g_delivered.clear(); + QueueEthernet(AP_OPEN, 0x0800, Bytes(60, 0x11)); + Pump(2); + check(g_delivered.size() == 1 && g_delivered[0].size() == 74 && be16(&g_delivered[0][12]) == 0x0800, + "a received data frame is delivered to the stack"); + + QueueEvent(6, 0, 3, 0, AP_OPEN); // DEAUTH_IND + Pump(); + check(BrcmfConnectState() == CONN_IDLE && !BrcmfLinkUp(), "a deauthentication takes the link down"); + check(!BrcmfSendEthernet(eth.data(), (uint32_t)eth.size()), "and nothing is sent while it is down"); +} + +static const Ioctl* FindKey(uint32_t index) { + for (auto it = fw.Ioctls.rbegin(); it != fw.Ioctls.rend(); ++it) + if (it->Set && it->Name == "wsec_key" && it->Data.size() == 164 && le32(it->Data.data()) == index) + return &*it; + return nullptr; +} + +static void CaseWpaJoin() { + printf("== WPA2-PSK ==\n"); + fw.OnEapol = Ap::OnEapol; + size_t ioctlsBefore = fw.Ioctls.size(); + check(BrcmfConnectStart(AP_WPA, 36, "SecureNet", Ap::PASS, RSN_CCMP.data(), (uint8_t)RSN_CCMP.size()), + "the join is accepted"); + check(SetInt("wpa_auth") == 0x80 && SetInt("wsec") == 4 && SetInt("auth") == 0, + "WPA2-PSK, AES, open system authentication"); + const Ioctl* ie = LastSet("wpaie"); + Bytes want = { 0x30, (uint8_t)RSN_CCMP.size() }; + append(want, RSN_CCMP.data(), RSN_CCMP.size()); + check(ie && ie->Data == want, "the supplicant's RSN element goes into the association request"); + const Ioctl* j = LastSet("join"); + check(j && le16(j->Data.data() + 68) == 0xd024, "the 5 GHz chanspec from the scan"); + + Pump(); + check(BrcmfConnectState() == CONN_HANDSHAKING && !BrcmfLinkUp(), + "association waits for the handshake before the link is usable"); + + Ap::Start(Ap::PASS); + Pump(40); + check(!Ap::lastMsg2.empty() && Ap::msg2MicOk, "message 2 carries a MIC the AP verifies"); + check(Ap::staRsn == want, "and the same RSN element"); + check(!Ap::lastMsg4.empty() && Ap::msg4MicOk, "message 4 follows message 3, MIC verified"); + check(BrcmfConnectState() == CONN_CONNECTED && BrcmfLinkUp(), "the link is up once keyed"); + + const Ioctl* ptk = FindKey(0); + const Ioctl* gtk = FindKey(1); + check(ptk && memcmp(ptk->Data.data() + 8, Ap::ptk + 32, 16) == 0 && le32(ptk->Data.data() + 4) == 16, + "the pairwise key is the TK the AP derived"); + if (ptk) { + const uint8_t* k = ptk->Data.data(); + check(le32(k + 112) == 4 && le32(k + 116) == 0 && memcmp(k + 156, AP_WPA, 6) == 0, + "AES-CCM, bound to the AP's address"); + } + check(gtk && memcmp(gtk->Data.data() + 8, Ap::gtk, 16) == 0, "the group key is the AP's GTK"); + if (gtk) { + const uint8_t* k = gtk->Data.data(); + bool noEa = true; + for (int i = 0; i < 6; i++) noEa = noEa && k[156 + i] == 0; + check(le32(k + 116) == 2 && noEa && le32(k + 132) == 1, "primary, broadcast, receive IV set"); + } + uint64_t msg4Order = 0; + for (auto& d : fw.DataTx) + if (d.first.size() > 14 && be16(&d.first[12]) == 0x888E) msg4Order = d.second; + check(ptk && msg4Order != 0 && msg4Order < ptk->Order, + "message 4 reaches the radio before the pairwise key is installed"); + (void)ioctlsBefore; + + QueueEvent(16, 0, 0, 0, AP_WPA); // link down + Pump(); + check(BrcmfConnectState() == CONN_IDLE, "losing the link returns to idle"); +} + +static void CaseWrongPassphrase() { + printf("== wrong passphrase ==\n"); + check(BrcmfConnectStart(AP_WPA, 36, "SecureNet", "not-the-password", RSN_CCMP.data(), + (uint8_t)RSN_CCMP.size()), "the join is accepted"); + Pump(); + Ap::Start(Ap::PASS); + Pump(40); + check(!Ap::lastMsg2.empty() && !Ap::msg2MicOk, "the AP rejects message 2's MIC"); + check(BrcmfConnectState() == CONN_FAILED, "the deauthentication fails the join"); + check(BrcmfJoinFailedOnAuth(), "and it is reported as an authentication failure"); + BrcmfConnectAbort(); + const Ioctl* d = LastCmd(52); + check(d && d->Set && memcmp(d->Data.data() + 4, AP_WPA, 6) == 0, "abort disassociates from the AP"); + check(BrcmfConnectState() == CONN_IDLE, "and leaves the connection idle"); +} + +static void CaseRefusals() { + printf("== refusals ==\n"); + check(!BrcmfConnectStart(AP_WPA, 36, "SecureNet", Ap::PASS, RSN_GCMP.data(), (uint8_t)RSN_GCMP.size()), + "a GCMP network is refused before anything is sent"); + check(BrcmfConnectRefusedForSecurity(), "as a security refusal"); + check(!BrcmfConnectStart(AP_WPA, 36, "SecureNet", nullptr, RSN_CCMP.data(), (uint8_t)RSN_CCMP.size()), + "an encrypted network without a passphrase is refused"); +} + +static void CaseJoinTimeout() { + printf("== join timeout ==\n"); + fw.AutoJoin = false; + check(BrcmfConnectStart(AP_OPEN, 6, "OpenNet", nullptr, nullptr, 0), "the join is accepted"); + Timekeeping::g_ms += 13000; + Pump(2); + check(BrcmfConnectState() == CONN_FAILED, "no answer from the firmware fails the join in bounded time"); + BrcmfConnectAbort(); + + check(BrcmfConnectStart(AP_OPEN, 6, "OpenNet", nullptr, nullptr, 0), "another join is accepted"); + QueueEvent(0, 3, 0, 0, AP_OPEN); // SET_SSID: no networks + Pump(); + check(BrcmfConnectState() == CONN_FAILED, "an E_SET_SSID failure fails it at once"); + BrcmfConnectAbort(); + fw.AutoJoin = true; +} + +static void CaseDeadFirmware() { + printf("== firmware stops answering ==\n"); + fw.Mute = true; + uint32_t v = 0; + for (int i = 0; i < 3; i++) BrcmfIovarGetInt("ver", v, true); + check(BrcmfGetState() == BrcmfState::Error, "three unanswered commands mark the radio failed"); + check(!BrcmfStartScan(), "and nothing more is attempted"); + fw.Mute = false; +} + +int main() { + CaseInit(); + CaseScan(); + CaseOpenJoin(); + CaseWpaJoin(); + CaseWrongPassphrase(); + CaseRefusals(); + CaseJoinTimeout(); + CaseDeadFirmware(); + if (g_fails) { + printf("\n%d Broadcom case(s) FAILED\n", g_fails); + return 1; + } + printf("\nAll Broadcom protocol and connection cases passed.\n"); + return 0; +} diff --git a/tests/wifi/mlme_harness.cpp b/tests/wifi/mlme_harness.cpp index bbcac40..dc099a2 100644 --- a/tests/wifi/mlme_harness.cpp +++ b/tests/wifi/mlme_harness.cpp @@ -135,6 +135,16 @@ namespace Drivers::Net::Wifi { void WifiRxEthernet(const uint8_t* frame, uint32_t len) { puthex("ETH", frame, len); } + + // Wifi.cpp routes the supplicant's hooks to the running backend. + bool WpaTxEapol(const uint8_t* body, uint32_t len) { return IwxWpaTxEapol(body, len); } + bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) { + return IwxWpaInstallPtk(tk, tkLen, cipher); + } + bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx, + uint8_t cipher, const uint8_t* rsc) { + return IwxWpaInstallGtk(gtk, gtkLen, keyIdx, cipher, rsc); + } } // ============================================================================= diff --git a/tests/wifi/run.sh b/tests/wifi/run.sh index 8fb77fa..10c6df9 100755 --- a/tests/wifi/run.sh +++ b/tests/wifi/run.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # -# Host-side tests for the Wi-Fi crypto and the WPA supplicant. +# Host-side tests for the Wi-Fi crypto, the WPA supplicant, the Intel MLME and +# the Broadcom FullMAC protocol layer. # # These compile the real kernel sources (Libraries/Crypto.cpp and # Drivers/Net/Wifi/Wpa.cpp) for the host against the small shim in shim/, so @@ -44,5 +45,15 @@ $CXX $CXXFLAGS -o "$out/mlme" \ python3 "$here/ap_mlme.py" "$out/mlme" +echo +echo "== Broadcom FullMAC protocol and join against a simulated firmware ==" +$CXX $CXXFLAGS -o "$out/brcmf" \ + "$here/brcmf_harness.cpp" \ + "$src/Drivers/Net/Wifi/BrcmfProto.cpp" \ + "$src/Drivers/Net/Wifi/BrcmfConnect.cpp" \ + "$src/Drivers/Net/Wifi/Wpa.cpp" "$src/Libraries/Crypto.cpp" + +"$out/brcmf" 2>/dev/null + echo echo "All Wi-Fi host tests passed." diff --git a/tests/wifi/shim/CppLib/Spinlock.hpp b/tests/wifi/shim/CppLib/Spinlock.hpp index df60a37..4a236f9 100644 --- a/tests/wifi/shim/CppLib/Spinlock.hpp +++ b/tests/wifi/shim/CppLib/Spinlock.hpp @@ -14,6 +14,7 @@ namespace kcp { std::atomic_flag f{ATOMIC_FLAG_INIT}; public: void Acquire() { while (f.test_and_set(std::memory_order_acquire)) {} } + bool TryAcquire() { return !f.test_and_set(std::memory_order_acquire); } void Release() { f.clear(std::memory_order_release); } }; } diff --git a/tests/wifi/shim/Memory/Heap.hpp b/tests/wifi/shim/Memory/Heap.hpp new file mode 100644 index 0000000..bcbe4ca --- /dev/null +++ b/tests/wifi/shim/Memory/Heap.hpp @@ -0,0 +1,10 @@ +#pragma once +#include +namespace Memory { + struct HostHeap { + void* Request(size_t n) { return malloc(n); } + void Free(void* p) { free(p); } + }; + inline HostHeap g_hostHeap; + inline HostHeap* g_heap = &g_hostHeap; +} diff --git a/tests/wifi/shim/Terminal/Terminal.hpp b/tests/wifi/shim/Terminal/Terminal.hpp index a510697..f974614 100644 --- a/tests/wifi/shim/Terminal/Terminal.hpp +++ b/tests/wifi/shim/Terminal/Terminal.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include namespace Kt { enum KernelLogLevel { INFO, OK, WARNING, ERROR }; class KernelLogStream { @@ -10,5 +11,6 @@ namespace Kt { KernelLogStream& operator<<(const char* s) { fprintf(stderr, "%s", s); return *this; } KernelLogStream& operator<<(uint64_t v) { fprintf(stderr, "%llu", (unsigned long long)v); return *this; } KernelLogStream& operator<<(int v) { fprintf(stderr, "%d", v); return *this; } + KernelLogStream& operator<<(base::Manip) { return *this; } }; } diff --git a/tests/wifi/shim/Timekeeping/Timer.hpp b/tests/wifi/shim/Timekeeping/Timer.hpp index 2f8c66e..6d4237a 100644 --- a/tests/wifi/shim/Timekeeping/Timer.hpp +++ b/tests/wifi/shim/Timekeeping/Timer.hpp @@ -1,3 +1,7 @@ #pragma once #include -namespace Timekeeping { uint64_t GetMilliseconds(); } +namespace Timekeeping { + uint64_t GetMilliseconds(); + void BusyWaitMs(uint64_t ms); + void BusyWaitUs(uint64_t us); +}