feat: add Broadcom FullMAC Wi-Fi driver

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

No files matched your search

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