feat: add Broadcom FullMAC Wi-Fi driver
This commit is contained in:
1 parent
8b4a938164
commit
51a2b3a932
34 files changed
+4719
-95
No files matched your search
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 109
|
||||
#define MONTAUK_BUILD_NUMBER 123
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -535,7 +535,7 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
// The supplicant transmits EAPOL frames unencrypted: they are what
|
||||
// establishes the key in the first place.
|
||||
bool WpaTxEapol(const uint8_t* body, uint32_t len) {
|
||||
bool IwxWpaTxEapol(const uint8_t* body, uint32_t len) {
|
||||
return SendDataFrame(g_bssid, ETHERTYPE_EAPOL, body, len, false);
|
||||
}
|
||||
|
||||
@@ -545,14 +545,14 @@ namespace Drivers::Net::Wifi {
|
||||
static InstalledKey g_ptkKey = {};
|
||||
static InstalledKey g_gtkKey = {};
|
||||
|
||||
bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
|
||||
bool IwxWpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
|
||||
if (!IwxSetKey(tk, tkLen, 0, true, cipher, nullptr)) return false;
|
||||
g_ptkKey = { true, 0, cipher, (uint8_t)tkLen };
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||
uint8_t cipher, const uint8_t* rsc) {
|
||||
bool IwxWpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||
uint8_t cipher, const uint8_t* rsc) {
|
||||
if (gtkLen != 16 && gtkLen != 32) return false;
|
||||
if (!IwxSetKey(gtk, gtkLen, keyIdx, false, cipher, rsc)) return false;
|
||||
g_gtkKey = { true, keyIdx, cipher, (uint8_t)gtkLen };
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "Wifi.hpp"
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include "Iwx.hpp"
|
||||
#include "Brcmf.hpp"
|
||||
#include "Wpa.hpp"
|
||||
#include <Fs/Vfs.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
@@ -34,6 +35,72 @@ namespace Drivers::Net::Wifi {
|
||||
static std::atomic<bool> g_initPending{false};
|
||||
static bool g_initialized = false;
|
||||
|
||||
// =========================================================================
|
||||
// Backend
|
||||
//
|
||||
// Two kinds of adapter sit behind this facade: the Intel AX210 family on
|
||||
// PCIe (Iwx*: the firmware is a SoftMAC, the MLME is ours) and the
|
||||
// Raspberry Pi 4/400's Broadcom/Cypress radio on SDIO (Brcmf*: FullMAC,
|
||||
// the firmware joins by itself). A machine has at most one; whichever
|
||||
// probes first owns the facade. Both use the same scan table, the same
|
||||
// supplicant and the same connection-state numbering (WIFI_CONN_*, which
|
||||
// IwxConnStateId mirrors).
|
||||
// =========================================================================
|
||||
|
||||
enum class Backend : uint8_t { None, Iwx, Brcmf };
|
||||
static Backend g_backend = Backend::None;
|
||||
|
||||
static bool IsBrcmf() { return g_backend == Backend::Brcmf; }
|
||||
|
||||
static bool FwRunning() {
|
||||
return IsBrcmf() ? BrcmfGetState() == BrcmfState::Running
|
||||
: g_iwx.State == IwxFwState::Running;
|
||||
}
|
||||
|
||||
static bool FwFailed() {
|
||||
return IsBrcmf() ? BrcmfGetState() == BrcmfState::Error
|
||||
: g_iwx.State == IwxFwState::Error;
|
||||
}
|
||||
|
||||
static bool ScanActive() { return IsBrcmf() ? BrcmfScanActive() : g_iwx.ScanActive; }
|
||||
static bool BeginScan() { return IsBrcmf() ? BrcmfStartScan() : IwxStartScan(nullptr); }
|
||||
|
||||
static void AbortScan() {
|
||||
if (IsBrcmf()) BrcmfAbortScan();
|
||||
else IwxAbortScan();
|
||||
}
|
||||
|
||||
// What a blocking wait pumps: the receive path, and for a join the
|
||||
// state machine behind it.
|
||||
static void PumpEvents() {
|
||||
if (IsBrcmf()) BrcmfService();
|
||||
else IwxProcessEvents();
|
||||
}
|
||||
|
||||
static void PumpConnection() {
|
||||
if (IsBrcmf()) {
|
||||
BrcmfService();
|
||||
} else {
|
||||
IwxProcessEvents();
|
||||
IwxConnectService();
|
||||
}
|
||||
}
|
||||
|
||||
static int ConnState() { return IsBrcmf() ? BrcmfConnectState() : IwxConnectState(); }
|
||||
|
||||
static void ConnAbort() {
|
||||
if (IsBrcmf()) BrcmfConnectAbort();
|
||||
else IwxConnectAbort();
|
||||
}
|
||||
|
||||
// A handshake that got as far as exchanging EAPOL frames and then failed
|
||||
// is almost always a wrong passphrase.
|
||||
static bool JoinFailedOnAuth() {
|
||||
return IsBrcmf() ? BrcmfJoinFailedOnAuth() : WpaGetState() == WpaState::Failed;
|
||||
}
|
||||
|
||||
static bool LinkUp() { return IsBrcmf() ? BrcmfLinkUp() : IwxLinkUp(); }
|
||||
|
||||
// =========================================================================
|
||||
// Scan result table
|
||||
// =========================================================================
|
||||
@@ -141,22 +208,12 @@ namespace Drivers::Net::Wifi {
|
||||
uint8_t RsnLen = 0;
|
||||
};
|
||||
|
||||
static bool ParseBeacon(const uint8_t* frame, uint32_t len, ParsedBeacon* out) {
|
||||
// 24-byte MAC header, then timestamp(8) + beacon interval(2) + caps(2).
|
||||
constexpr uint32_t HDR = 24;
|
||||
constexpr uint32_t FIXED = 12;
|
||||
if (len < HDR + FIXED) return false;
|
||||
|
||||
const uint8_t* fixed = frame + HDR;
|
||||
out->BeaconInterval = (uint16_t)(fixed[8] | (fixed[9] << 8));
|
||||
uint16_t caps = (uint16_t)(fixed[10] | (fixed[11] << 8));
|
||||
bool privacy = (caps & 0x0010) != 0;
|
||||
|
||||
// The information elements of a beacon or probe response; `privacy` is
|
||||
// the capability field's Privacy bit.
|
||||
static void ParseIes(const uint8_t* ie, const uint8_t* end, bool privacy, ParsedBeacon* out) {
|
||||
bool haveRsn = false, haveWpa = false;
|
||||
|
||||
const uint8_t* ie = frame + HDR + FIXED;
|
||||
const uint8_t* end = frame + len;
|
||||
while (ie + 2 <= end) {
|
||||
while (ie && ie + 2 <= end) {
|
||||
uint8_t id = ie[0];
|
||||
uint8_t ielen = ie[1];
|
||||
if (ie + 2 + ielen > end) break;
|
||||
@@ -200,9 +257,24 @@ namespace Drivers::Net::Wifi {
|
||||
else if (privacy) out->Security = WIFI_SEC_WEP;
|
||||
else out->Security = WIFI_SEC_OPEN;
|
||||
}
|
||||
}
|
||||
|
||||
static bool ParseBeacon(const uint8_t* frame, uint32_t len, ParsedBeacon* out) {
|
||||
// 24-byte MAC header, then timestamp(8) + beacon interval(2) + caps(2).
|
||||
constexpr uint32_t HDR = 24;
|
||||
constexpr uint32_t FIXED = 12;
|
||||
if (len < HDR + FIXED) return false;
|
||||
|
||||
const uint8_t* fixed = frame + HDR;
|
||||
out->BeaconInterval = (uint16_t)(fixed[8] | (fixed[9] << 8));
|
||||
uint16_t caps = (uint16_t)(fixed[10] | (fixed[11] << 8));
|
||||
ParseIes(frame + HDR + FIXED, frame + len, (caps & 0x0010) != 0, out);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void StoreScanResult(const uint8_t* bssid, const ParsedBeacon& pb, uint8_t ch,
|
||||
int8_t rssiDbm);
|
||||
|
||||
// Called by the MVM RX path for every received management frame.
|
||||
void WifiRxMgmtFrame(const uint8_t* frame, uint32_t len, uint8_t channel,
|
||||
int8_t rssiDbm) {
|
||||
@@ -224,9 +296,21 @@ namespace Drivers::Net::Wifi {
|
||||
ParsedBeacon pb;
|
||||
if (!ParseBeacon(frame, len, &pb)) return;
|
||||
|
||||
const uint8_t* bssid = frame + 16; // addr3 of a beacon
|
||||
uint8_t ch = pb.Channel ? pb.Channel : channel;
|
||||
// addr3 of a beacon
|
||||
StoreScanResult(frame + 16, pb, pb.Channel ? pb.Channel : channel, rssiDbm);
|
||||
}
|
||||
|
||||
// One BSS a FullMAC firmware reported from its own scan.
|
||||
void WifiRxBssInfo(const uint8_t* bssid, uint16_t beaconInterval, uint16_t capability,
|
||||
const uint8_t* ies, uint32_t iesLen, uint8_t channel, int8_t rssiDbm) {
|
||||
ParsedBeacon pb;
|
||||
pb.BeaconInterval = beaconInterval;
|
||||
ParseIes(ies, ies ? ies + iesLen : nullptr, (capability & 0x0010) != 0, &pb);
|
||||
StoreScanResult(bssid, pb, pb.Channel ? pb.Channel : channel, rssiDbm);
|
||||
}
|
||||
|
||||
static void StoreScanResult(const uint8_t* bssid, const ParsedBeacon& pb, uint8_t ch,
|
||||
int8_t rssiDbm) {
|
||||
g_resultLock.Acquire();
|
||||
|
||||
ScanEntry* slot = nullptr;
|
||||
@@ -275,7 +359,9 @@ namespace Drivers::Net::Wifi {
|
||||
// =========================================================================
|
||||
|
||||
bool Probe(const Pci::PciDevice& dev) {
|
||||
if (g_backend != Backend::None) return false;
|
||||
if (!IwxProbe(dev)) return false;
|
||||
g_backend = Backend::Iwx;
|
||||
|
||||
// Firmware comes off the ramdisk, which is not mounted during the PCI
|
||||
// scan; hand the rest of the bring-up to the idle loop.
|
||||
@@ -283,7 +369,23 @@ namespace Drivers::Net::Wifi {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProbeBroadcomSdio(uint64_t mmioPhys, const char* name) {
|
||||
if (g_backend != Backend::None) return false;
|
||||
if (!BrcmfProbe(mmioPhys)) return false;
|
||||
g_backend = Backend::Brcmf;
|
||||
KernelLogStream(INFO, "WiFi") << "Broadcom SDIO radio on " << name
|
||||
<< "; firmware loads once the root filesystem is up";
|
||||
// Same reason as the PCI path: the firmware is on the root volume.
|
||||
g_initPending.store(true, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void CompleteInit() {
|
||||
if (IsBrcmf()) {
|
||||
g_initialized = BrcmfCompleteInit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IwxReadFirmware()) {
|
||||
g_iwx.State = IwxFwState::Error;
|
||||
return;
|
||||
@@ -348,7 +450,7 @@ namespace Drivers::Net::Wifi {
|
||||
static constexpr uint64_t RECOVERY_BACKOFF_MS = 5000;
|
||||
|
||||
static void ServiceRecovery() {
|
||||
if (g_iwx.State != IwxFwState::Error || g_recoveryGaveUp) return;
|
||||
if (!FwFailed() || g_recoveryGaveUp) return;
|
||||
// An adapter that never finished its first bring-up is ServiceDeferredInit's
|
||||
// problem, not this one.
|
||||
if (!g_initialized) return;
|
||||
@@ -366,7 +468,7 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
// Re-check under the gate: the winner of the race may have just
|
||||
// finished a reset that fixed things.
|
||||
if (g_iwx.State != IwxFwState::Error) return;
|
||||
if (!FwFailed()) return;
|
||||
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (g_lastRecoveryMs && now - g_lastRecoveryMs < RECOVERY_BACKOFF_MS) return;
|
||||
@@ -384,6 +486,24 @@ namespace Drivers::Net::Wifi {
|
||||
<< " (attempt " << (uint64_t)g_recoveryAttempts << " of "
|
||||
<< (uint64_t)MAX_RECOVERY_ATTEMPTS << ")";
|
||||
|
||||
if (IsBrcmf()) {
|
||||
// A FullMAC radio is reset by power: WL_ON low, then the whole
|
||||
// bring-up (firmware download included) again.
|
||||
g_initialized = false;
|
||||
g_asyncConnect = false;
|
||||
g_scanDeadline = 0;
|
||||
g_lastError = WIFI_ERR_NO_ADAPTER;
|
||||
ClearResults();
|
||||
BrcmfShutdown();
|
||||
CompleteInit();
|
||||
if (g_initialized) {
|
||||
KernelLogStream(OK, "WiFi") << "Adapter recovered";
|
||||
g_recoveryAttempts = 0;
|
||||
g_lastError = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Nothing may be mid-command while the device is torn down. Taking
|
||||
// CmdLock is enough: it is what serializes every sender, and the RX
|
||||
// pump has its own reentrancy guard.
|
||||
@@ -517,8 +637,24 @@ namespace Drivers::Net::Wifi {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The facade's own deadlines (async scan and join) and a due reset.
|
||||
static bool FacadeWorkDue(uint64_t now) {
|
||||
if (g_scanDeadline != 0 && (now >= g_scanDeadline || !ScanActive())) return true;
|
||||
if (g_asyncConnect) return true;
|
||||
return FwFailed() && g_initialized && !g_recoveryGaveUp &&
|
||||
(g_lastRecoveryMs == 0 || now - g_lastRecoveryMs >= RECOVERY_BACKOFF_MS);
|
||||
}
|
||||
|
||||
bool HasPolledWork() {
|
||||
return IsBrcmf() && BrcmfHasWork(Timekeeping::GetMilliseconds());
|
||||
}
|
||||
|
||||
bool HasDeferredWork() {
|
||||
if (InitDue()) return true;
|
||||
if (IsBrcmf()) {
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
return BrcmfHasWork(now) || FacadeWorkDue(now);
|
||||
}
|
||||
if (!g_iwx.Mmio) return false;
|
||||
if (g_iwx.WorkPending) return true;
|
||||
|
||||
@@ -527,7 +663,29 @@ namespace Drivers::Net::Wifi {
|
||||
return HasBlockingWork(now);
|
||||
}
|
||||
|
||||
// The Broadcom radio has no interrupt in use: everything on its bus is
|
||||
// polled, and everything on its bus waits (SDIO transfers, ioctls), so
|
||||
// all of it goes through the blocking gate.
|
||||
static void ServiceBrcmf() {
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
bool radioWork = BrcmfHasWork(now);
|
||||
if (!radioWork && !FacadeWorkDue(now)) return;
|
||||
if (LeaveToAp()) return;
|
||||
if (__atomic_test_and_set(&g_blockingBusy, __ATOMIC_ACQUIRE)) return;
|
||||
struct Guard {
|
||||
~Guard() { __atomic_clear(&g_blockingBusy, __ATOMIC_RELEASE); }
|
||||
} guard;
|
||||
|
||||
if (radioWork) BrcmfService();
|
||||
ServiceAsync();
|
||||
ServiceRecovery();
|
||||
}
|
||||
|
||||
void ServiceEvents() {
|
||||
if (IsBrcmf()) {
|
||||
ServiceBrcmf();
|
||||
return;
|
||||
}
|
||||
if (!g_iwx.Mmio) return;
|
||||
|
||||
// The non-blocking half: any core, the BSP included.
|
||||
@@ -573,7 +731,11 @@ namespace Drivers::Net::Wifi {
|
||||
}
|
||||
|
||||
bool IsInitialized() { return g_initialized; }
|
||||
bool IsPresent() { return g_iwx.State != IwxFwState::Absent; }
|
||||
|
||||
bool IsPresent() {
|
||||
return IsBrcmf() ? BrcmfGetState() != BrcmfState::Absent
|
||||
: g_iwx.State != IwxFwState::Absent;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Public operations
|
||||
@@ -611,26 +773,26 @@ namespace Drivers::Net::Wifi {
|
||||
int Scan(WifiNetwork* out, int maxCount, uint32_t timeoutMs) {
|
||||
if (!out || maxCount <= 0) return -1;
|
||||
if (!g_initialized) return -1;
|
||||
if (g_iwx.State != IwxFwState::Running) return -1;
|
||||
if (!FwRunning()) return -1;
|
||||
|
||||
if (timeoutMs < 1000) timeoutMs = 1000;
|
||||
if (timeoutMs > 20000) timeoutMs = 20000;
|
||||
|
||||
ClearResults();
|
||||
|
||||
if (!IwxStartScan(nullptr)) return -1;
|
||||
if (!BeginScan()) return -1;
|
||||
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (g_iwx.ScanActive && Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
IwxProcessEvents();
|
||||
while (ScanActive() && Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
PumpEvents();
|
||||
for (int i = 0; i < 200; i++) Arch::Pause();
|
||||
}
|
||||
|
||||
if (g_iwx.ScanActive) {
|
||||
IwxAbortScan();
|
||||
if (ScanActive()) {
|
||||
AbortScan();
|
||||
// Drain whatever the firmware queued before the abort landed.
|
||||
uint64_t t0 = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - t0 < 200) IwxProcessEvents();
|
||||
while (Timekeeping::GetMilliseconds() - t0 < 200) PumpEvents();
|
||||
}
|
||||
|
||||
g_scanGeneration++;
|
||||
@@ -639,14 +801,14 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
int StartScan(uint32_t timeoutMs) {
|
||||
if (!g_initialized) return -1;
|
||||
if (g_iwx.State != IwxFwState::Running) return -1;
|
||||
if (g_iwx.ScanActive) return 1; // already sweeping
|
||||
if (!FwRunning()) return -1;
|
||||
if (ScanActive()) return 1; // already sweeping
|
||||
|
||||
if (timeoutMs < 1000) timeoutMs = 1000;
|
||||
if (timeoutMs > 20000) timeoutMs = 20000;
|
||||
|
||||
ClearResults();
|
||||
if (!IwxStartScan(nullptr)) return -1;
|
||||
if (!BeginScan()) return -1;
|
||||
|
||||
g_scanDeadline = Timekeeping::GetMilliseconds() + timeoutMs;
|
||||
return 0;
|
||||
@@ -657,33 +819,47 @@ namespace Drivers::Net::Wifi {
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
out->present = IsPresent() ? 1 : 0;
|
||||
out->state = (uint8_t)g_iwx.State;
|
||||
if (!IsPresent()) return -1;
|
||||
|
||||
memcpy(out->mac, g_iwx.Nvm.HwAddr, 6);
|
||||
out->scanning = g_iwx.ScanActive ? 1 : 0;
|
||||
out->bands = (uint8_t)((g_iwx.Nvm.Sku24GHz ? 1 : 0)
|
||||
| (g_iwx.Nvm.Sku52GHz ? 2 : 0));
|
||||
out->channels = (uint16_t)g_iwx.ChannelCount;
|
||||
out->rxPackets = g_iwx.RxPackets;
|
||||
out->txPackets = g_iwx.TxPackets;
|
||||
out->fwErrors = (uint32_t)g_iwx.FwErrors;
|
||||
out->connState = (uint32_t)IwxConnectState();
|
||||
out->connected = IwxLinkUp() ? 1 : 0;
|
||||
const char* fwVersion;
|
||||
if (IsBrcmf()) {
|
||||
out->state = (uint8_t)BrcmfGetState();
|
||||
memcpy(out->mac, g_brcmf.Mac, 6);
|
||||
out->bands = (uint8_t)((g_brcmf.Band24 ? 1 : 0) | (g_brcmf.Band5 ? 2 : 0));
|
||||
out->channels = g_brcmf.ChannelCount;
|
||||
out->rxPackets = g_brcmf.RxPackets;
|
||||
out->txPackets = g_brcmf.TxPackets;
|
||||
out->fwErrors = g_brcmf.FwErrors;
|
||||
fwVersion = g_brcmf.FwVersion;
|
||||
} else {
|
||||
out->state = (uint8_t)g_iwx.State;
|
||||
memcpy(out->mac, g_iwx.Nvm.HwAddr, 6);
|
||||
out->bands = (uint8_t)((g_iwx.Nvm.Sku24GHz ? 1 : 0)
|
||||
| (g_iwx.Nvm.Sku52GHz ? 2 : 0));
|
||||
out->channels = (uint16_t)g_iwx.ChannelCount;
|
||||
out->rxPackets = g_iwx.RxPackets;
|
||||
out->txPackets = g_iwx.TxPackets;
|
||||
out->fwErrors = (uint32_t)g_iwx.FwErrors;
|
||||
fwVersion = g_iwx.Fw.Version;
|
||||
}
|
||||
out->scanning = ScanActive() ? 1 : 0;
|
||||
out->connState = (uint32_t)ConnState();
|
||||
out->connected = LinkUp() ? 1 : 0;
|
||||
out->lastError = (int32_t)g_lastError;
|
||||
out->scanGeneration = g_scanGeneration;
|
||||
out->joining = g_asyncConnect ? 1 : 0;
|
||||
|
||||
if (IwxConnectState() != (int)IwxConnStateId::Idle) {
|
||||
const char* ssid = IwxConnectSsid();
|
||||
if (ConnState() != (int)IwxConnStateId::Idle) {
|
||||
const char* ssid = IsBrcmf() ? BrcmfConnectSsid() : IwxConnectSsid();
|
||||
int k = 0;
|
||||
for (; k < 32 && ssid[k]; k++) out->ssid[k] = ssid[k];
|
||||
out->ssid[k] = '\0';
|
||||
memcpy(out->bssid, IwxConnectBssid(), 6);
|
||||
memcpy(out->bssid, IsBrcmf() ? BrcmfConnectBssid() : IwxConnectBssid(), 6);
|
||||
if (IsBrcmf()) out->channel = BrcmfConnectChannel();
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
for (; i < 31 && g_iwx.Fw.Version[i]; i++) out->fwVersion[i] = g_iwx.Fw.Version[i];
|
||||
for (; i < 31 && fwVersion[i]; i++) out->fwVersion[i] = fwVersion[i];
|
||||
out->fwVersion[i] = '\0';
|
||||
return 0;
|
||||
}
|
||||
@@ -695,24 +871,20 @@ namespace Drivers::Net::Wifi {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
|
||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
IwxProcessEvents();
|
||||
IwxConnectService();
|
||||
PumpConnection();
|
||||
|
||||
auto state = (IwxConnStateId)IwxConnectState();
|
||||
auto state = (IwxConnStateId)ConnState();
|
||||
if (state == IwxConnStateId::Connected) return 0;
|
||||
if (state == IwxConnStateId::Failed) {
|
||||
// A handshake that got as far as exchanging EAPOL frames and
|
||||
// then failed is almost always a wrong passphrase.
|
||||
int rc = WpaGetState() == WpaState::Failed
|
||||
? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
||||
int rc = JoinFailedOnAuth() ? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
||||
// A rejection seen on the RX path only marks the state; the
|
||||
// firmware contexts are still up and have to come back down.
|
||||
IwxConnectAbort();
|
||||
ConnAbort();
|
||||
return rc;
|
||||
}
|
||||
if (state == IwxConnStateId::Idle) return WIFI_ERR_FAILED;
|
||||
if (g_iwx.State == IwxFwState::Error) {
|
||||
IwxConnectAbort();
|
||||
if (FwFailed()) {
|
||||
ConnAbort();
|
||||
return WIFI_ERR_FAILED;
|
||||
}
|
||||
|
||||
@@ -721,7 +893,7 @@ namespace Drivers::Net::Wifi {
|
||||
Sched::Schedule();
|
||||
}
|
||||
|
||||
IwxConnectAbort();
|
||||
ConnAbort();
|
||||
return WIFI_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
@@ -732,7 +904,7 @@ namespace Drivers::Net::Wifi {
|
||||
// scan table is worth a (blocking) sweep to look for it.
|
||||
static int StartJoin(const char* ssid, const char* password, bool rescan) {
|
||||
if (!g_initialized || !ssid) return WIFI_ERR_NO_ADAPTER;
|
||||
if (g_iwx.State != IwxFwState::Running) return WIFI_ERR_NO_ADAPTER;
|
||||
if (!FwRunning()) return WIFI_ERR_NO_ADAPTER;
|
||||
|
||||
// Locate the network in the most recent scan results: the firmware
|
||||
// contexts need its BSSID and channel, and the connect path needs its
|
||||
@@ -809,17 +981,24 @@ namespace Drivers::Net::Wifi {
|
||||
if (!havePassword) return WIFI_ERR_NEED_KEY;
|
||||
}
|
||||
|
||||
if (!IwxConnectStart(bssid, channel, is5, ssid,
|
||||
havePassword ? password : nullptr,
|
||||
security == WIFI_SEC_OPEN ? nullptr : rsnIe,
|
||||
security == WIFI_SEC_OPEN ? 0 : rsnIeLen,
|
||||
beaconInterval, dtimPeriod)) {
|
||||
// IwxConnectStart already logged the specific reason. Only report
|
||||
bool started = IsBrcmf()
|
||||
? BrcmfConnectStart(bssid, channel, ssid,
|
||||
havePassword ? password : nullptr,
|
||||
security == WIFI_SEC_OPEN ? nullptr : rsnIe,
|
||||
security == WIFI_SEC_OPEN ? 0 : rsnIeLen)
|
||||
: IwxConnectStart(bssid, channel, is5, ssid,
|
||||
havePassword ? password : nullptr,
|
||||
security == WIFI_SEC_OPEN ? nullptr : rsnIe,
|
||||
security == WIFI_SEC_OPEN ? 0 : rsnIeLen,
|
||||
beaconInterval, dtimPeriod);
|
||||
if (!started) {
|
||||
// The backend already logged the specific reason. Only report
|
||||
// a security problem when that is genuinely what happened; a radio
|
||||
// or firmware failure needs completely different advice.
|
||||
bool security_ = IwxConnectRefusedForSecurity();
|
||||
auto state = (IwxConnStateId)IwxConnectState();
|
||||
if (state != IwxConnStateId::Idle) IwxConnectAbort();
|
||||
bool security_ = IsBrcmf() ? BrcmfConnectRefusedForSecurity()
|
||||
: IwxConnectRefusedForSecurity();
|
||||
auto state = (IwxConnStateId)ConnState();
|
||||
if (state != IwxConnStateId::Idle) ConnAbort();
|
||||
return security_ ? WIFI_ERR_UNSUPPORTED : WIFI_ERR_FAILED;
|
||||
}
|
||||
|
||||
@@ -835,8 +1014,8 @@ namespace Drivers::Net::Wifi {
|
||||
int ConnectAsync(const char* ssid, const char* password) {
|
||||
// A join already in flight owns the firmware contexts; tear it down
|
||||
// rather than stacking a second one on top.
|
||||
if (g_asyncConnect || (IwxConnectState() != (int)IwxConnStateId::Idle))
|
||||
IwxConnectAbort();
|
||||
if (g_asyncConnect || (ConnState() != (int)IwxConnStateId::Idle))
|
||||
ConnAbort();
|
||||
|
||||
g_asyncConnect = false;
|
||||
g_lastError = 0;
|
||||
@@ -858,7 +1037,7 @@ namespace Drivers::Net::Wifi {
|
||||
if (!g_initialized) return -1;
|
||||
g_asyncConnect = false;
|
||||
g_lastError = 0;
|
||||
IwxConnectAbort();
|
||||
ConnAbort();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -881,11 +1060,11 @@ namespace Drivers::Net::Wifi {
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
|
||||
if (g_scanDeadline) {
|
||||
if (!g_iwx.ScanActive) {
|
||||
if (!ScanActive()) {
|
||||
g_scanDeadline = 0;
|
||||
g_scanGeneration++;
|
||||
} else if (now >= g_scanDeadline) {
|
||||
IwxAbortScan();
|
||||
AbortScan();
|
||||
g_scanDeadline = 0;
|
||||
g_scanGeneration++;
|
||||
}
|
||||
@@ -893,31 +1072,29 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
if (!g_asyncConnect) return;
|
||||
|
||||
auto state = (IwxConnStateId)IwxConnectState();
|
||||
auto state = (IwxConnStateId)ConnState();
|
||||
if (state == IwxConnStateId::Connected) {
|
||||
g_asyncConnect = false;
|
||||
g_lastError = 0;
|
||||
return;
|
||||
}
|
||||
if (state == IwxConnStateId::Failed) {
|
||||
// Same reading as the blocking path: a handshake that exchanged
|
||||
// EAPOL frames and then failed is almost always a wrong passphrase.
|
||||
g_lastError = WpaGetState() == WpaState::Failed
|
||||
? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
||||
// Same reading as the blocking path.
|
||||
g_lastError = JoinFailedOnAuth() ? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
||||
g_asyncConnect = false;
|
||||
IwxConnectAbort();
|
||||
ConnAbort();
|
||||
return;
|
||||
}
|
||||
if (state == IwxConnStateId::Idle || g_iwx.State == IwxFwState::Error) {
|
||||
if (state == IwxConnStateId::Idle || FwFailed()) {
|
||||
g_lastError = WIFI_ERR_FAILED;
|
||||
g_asyncConnect = false;
|
||||
if (state != IwxConnStateId::Idle) IwxConnectAbort();
|
||||
if (state != IwxConnStateId::Idle) ConnAbort();
|
||||
return;
|
||||
}
|
||||
if (now >= g_asyncConnectDeadline) {
|
||||
g_lastError = WIFI_ERR_TIMEOUT;
|
||||
g_asyncConnect = false;
|
||||
IwxConnectAbort();
|
||||
ConnAbort();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -927,13 +1104,33 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
static RxCallback g_rxCallback = nullptr;
|
||||
|
||||
const uint8_t* GetMacAddress() { return g_iwx.Nvm.HwAddr; }
|
||||
const uint8_t* GetMacAddress() { return IsBrcmf() ? g_brcmf.Mac : g_iwx.Nvm.HwAddr; }
|
||||
|
||||
bool IsLinkUp() { return g_initialized && IwxLinkUp(); }
|
||||
bool IsLinkUp() { return g_initialized && LinkUp(); }
|
||||
|
||||
bool SendPacket(const uint8_t* data, uint16_t length) {
|
||||
if (!IsLinkUp()) return false;
|
||||
return IwxConnectSendEthernet(data, length);
|
||||
return IsBrcmf() ? BrcmfSendEthernet(data, length)
|
||||
: IwxConnectSendEthernet(data, length);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Supplicant hooks (Wpa.hpp): to whichever backend is running the join
|
||||
// =========================================================================
|
||||
|
||||
bool WpaTxEapol(const uint8_t* body, uint32_t len) {
|
||||
return IsBrcmf() ? BrcmfWpaTxEapol(body, len) : IwxWpaTxEapol(body, len);
|
||||
}
|
||||
|
||||
bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
|
||||
return IsBrcmf() ? BrcmfWpaInstallPtk(tk, tkLen, cipher)
|
||||
: IwxWpaInstallPtk(tk, tkLen, cipher);
|
||||
}
|
||||
|
||||
bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||
uint8_t cipher, const uint8_t* rsc) {
|
||||
return IsBrcmf() ? BrcmfWpaInstallGtk(gtk, gtkLen, keyIdx, cipher, rsc)
|
||||
: IwxWpaInstallGtk(gtk, gtkLen, keyIdx, cipher, rsc);
|
||||
}
|
||||
|
||||
void SetRxCallback(RxCallback callback) { g_rxCallback = callback; }
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user