diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index e25e10d..190b865 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 46 +#define MONTAUK_BUILD_NUMBER 53 diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index 3b1d8e6..05798b4 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -34,6 +34,7 @@ #include "Audio.hpp" // SYS_AUDIOOPEN, SYS_AUDIOCLOSE, SYS_AUDIOWRITE, SYS_AUDIOCTL #include "BluetoothSyscall.hpp" // SYS_BTSCAN, SYS_BTCONNECT, SYS_BTDISCONNECT, SYS_BTLIST, SYS_BTINFO #include "Sdr.hpp" // SYS_SDR_COUNT, SYS_SDR_INFO, SYS_SDR_OPEN, SYS_SDR_CLOSE, SYS_SDR_START, SYS_SDR_STOP, SYS_SDR_READ, SYS_SDR_SETPARAM, SYS_SDR_GETPARAM +#include "WifiSyscall.hpp" // SYS_WIFI_SCAN, SYS_WIFI_INFO, SYS_WIFI_CONNECT, SYS_WIFI_DISCONNECT #include "IpcSyscall.hpp" // SYS_DUPHANDLE, SYS_WAIT_HANDLE, SYS_STREAM_CREATE, SYS_STREAM_READ, SYS_STREAM_WRITE, SYS_MAILBOX_CREATE, SYS_MAILBOX_SEND, SYS_MAILBOX_RECV, SYS_WAITSET_CREATE, SYS_WAITSET_ADD, SYS_WAITSET_REMOVE, SYS_WAITSET_WAIT, SYS_PROC_OPEN, SYS_SURFACE_CREATE, SYS_SURFACE_MAP, SYS_SURFACE_RESIZE #include "LibSyscall.hpp" // SYS_LOAD_LIB, SYS_UNLOAD_LIB, SYS_DLSYM #include "CrashReportSyscall.hpp" // SYS_CRASH_REPORT @@ -443,6 +444,19 @@ namespace montauk::abi { case SYS_BTINFO: if (!UserMemory::Writable(frame->arg1)) return -1; return Sys_BtInfo((BtAdapterInfo*)frame->arg1); + case SYS_WIFI_SCAN: + if ((int64_t)frame->arg2 < 0) return -1; + if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(WifiNetwork), true)) return -1; + return Sys_WifiScan((WifiNetwork*)frame->arg1, (int)frame->arg2, (uint32_t)frame->arg3); + case SYS_WIFI_INFO: + if (!UserMemory::Writable(frame->arg1)) return -1; + return Sys_WifiInfo((WifiInfo*)frame->arg1); + case SYS_WIFI_CONNECT: + if (!UserMemory::String(frame->arg1, 64)) return -1; + if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, 128)) return -1; + return Sys_WifiConnect((const char*)frame->arg1, (const char*)frame->arg2); + case SYS_WIFI_DISCONNECT: + return Sys_WifiDisconnect(); case SYS_SUSPEND: return Sys_Suspend(); case SYS_SETTZ: diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 6753427..c35329c 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -297,6 +297,12 @@ namespace montauk::abi { static constexpr uint64_t SYS_DISPLAYSETMODE = 156; static constexpr uint64_t SYS_DISPLAYBRIGHTNESS = 157; + /* Wifi.hpp -- Wi-Fi adapter control */ + static constexpr uint64_t SYS_WIFI_SCAN = 158; // (WifiNetwork*, maxCount, timeoutMs) -> count + static constexpr uint64_t SYS_WIFI_INFO = 159; // (WifiInfo*) -> 0, -1 if absent + static constexpr uint64_t SYS_WIFI_CONNECT = 160; // (ssid, password) -> 0, <0 on error + static constexpr uint64_t SYS_WIFI_DISCONNECT = 161; // () -> 0 + // Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM). static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz static constexpr int SDR_PARAM_SAMPLE_RATE = 1; // sample rate, Hz @@ -639,6 +645,46 @@ namespace montauk::abi { uint32_t _pad2; }; + // Wi-Fi security suites reported in WifiNetwork.security. + static constexpr uint8_t WIFI_SEC_OPEN = 0; + static constexpr uint8_t WIFI_SEC_WEP = 1; + static constexpr uint8_t WIFI_SEC_WPA = 2; + static constexpr uint8_t WIFI_SEC_WPA2 = 3; + static constexpr uint8_t WIFI_SEC_WPA3 = 4; + + // Adapter states reported in WifiInfo.state. + static constexpr uint8_t WIFI_STATE_ABSENT = 0; // no device + static constexpr uint8_t WIFI_STATE_DETECTED = 1; // waiting for firmware load + static constexpr uint8_t WIFI_STATE_BOOTING = 2; + static constexpr uint8_t WIFI_STATE_RUNNING = 3; + static constexpr uint8_t WIFI_STATE_ERROR = 4; + static constexpr uint8_t WIFI_STATE_RFKILL = 5; // radio disabled in hardware + + // One scanned network (returned by SYS_WIFI_SCAN). + struct WifiNetwork { + char ssid[36]; // NUL-terminated; empty for hidden networks + uint8_t bssid[6]; + uint8_t channel; + int8_t rssi; // dBm + uint8_t band; // 0 = 2.4 GHz, 1 = 5 GHz + uint8_t security; // WIFI_SEC_* + uint16_t beaconInterval; // TU + }; + + // Adapter status (returned by SYS_WIFI_INFO). + struct WifiInfo { + uint8_t mac[6]; + uint8_t present; // 1 if a supported device was found + uint8_t state; // WIFI_STATE_* + uint8_t scanning; + uint8_t bands; // bit0 = 2.4 GHz, bit1 = 5 GHz + uint16_t channels; // usable channels after regulatory filtering + char fwVersion[32]; + uint64_t rxPackets; + uint32_t fwErrors; + uint32_t connState; // 0 idle, >0 connection setup in progress + }; + struct ThermalInfo { char name[32]; // short zone name (e.g. "THRM", "TZ00") int32_t temperature; // tenths of degrees Celsius, or -1 if unavailable diff --git a/kernel/src/Api/WifiSyscall.hpp b/kernel/src/Api/WifiSyscall.hpp new file mode 100644 index 0000000..c69389d --- /dev/null +++ b/kernel/src/Api/WifiSyscall.hpp @@ -0,0 +1,35 @@ +/* + * WifiSyscall.hpp + * SYS_WIFI_SCAN, SYS_WIFI_INFO, SYS_WIFI_CONNECT, SYS_WIFI_DISCONNECT + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include +#include + +#include "Syscall.hpp" + +namespace montauk::abi { + + static int64_t Sys_WifiScan(WifiNetwork* buf, int maxCount, uint32_t timeoutMs) { + if (!buf || maxCount <= 0) return -1; + if (maxCount > 64) maxCount = 64; + return (int64_t)Drivers::Net::Wifi::Scan(buf, maxCount, timeoutMs); + } + + static int64_t Sys_WifiInfo(WifiInfo* buf) { + if (!buf) return -1; + return (int64_t)Drivers::Net::Wifi::GetInfo(buf); + } + + static int64_t Sys_WifiConnect(const char* ssid, const char* password) { + if (!ssid) return -1; + return (int64_t)Drivers::Net::Wifi::Connect(ssid, password); + } + + static int64_t Sys_WifiDisconnect() { + return (int64_t)Drivers::Net::Wifi::Disconnect(); + } + +} diff --git a/kernel/src/Drivers/Init.cpp b/kernel/src/Drivers/Init.cpp index 9541f08..5eb7192 100644 --- a/kernel/src/Drivers/Init.cpp +++ b/kernel/src/Drivers/Init.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,18 @@ namespace Drivers { 0x1A1F, }; + // Intel AX210-family Wi-Fi (CNVi and discrete). The driver additionally + // checks CSR_HW_RF_ID at probe time and only claims RF type GF (AX211), + // which is the firmware image shipped on the ramdisk. + static constexpr uint16_t g_intelWifiIds[] = { + 0x2725, // AX210 (discrete) + 0x2726, // AX211 (discrete, GF2) + 0x51f0, 0x51f1, 0x54f0, // AlderLake CNVi + 0x7a70, 0x7af0, // RaptorLake / AlderLake-P CNVi + 0x7e40, // MeteorLake CNVi + 0x7f70, // RaptorLake-S CNVi + }; + // ------------------------------------------------------------------------- // Probe wrappers (adapt namespace::Probe to PciProbeFunc signature) // ------------------------------------------------------------------------- @@ -63,6 +76,10 @@ namespace Drivers { return Net::E1000E::Probe(dev); } + static bool ProbeWifi(const Pci::PciDevice& dev) { + return Net::Wifi::Probe(dev); + } + static bool ProbeAhci(const Pci::PciDevice& dev) { return Storage::Ahci::Probe(dev); } @@ -124,7 +141,20 @@ namespace Drivers { Pci::ProbePhase::Normal, ProbeE1000E, }, - // Order 5: AHCI — Normal phase, match class=0x01/0x06/0x01 (SATA AHCI) + // Order 5: Intel Wi-Fi — Normal phase, vendor=0x8086 + deviceIds list. + // These are the AX210-family (CNVi "So") parts; the probe + // rejects anything whose RF type is not GF, since only the + // AX211 firmware image is bundled. + { + "IntelWiFi", + 0x8086, + 0xFF, 0xFF, 0xFF, + g_intelWifiIds, + sizeof(g_intelWifiIds) / sizeof(g_intelWifiIds[0]), + Pci::ProbePhase::Normal, + ProbeWifi, + }, + // Order 6: AHCI — Normal phase, match class=0x01/0x06/0x01 (SATA AHCI) { "AHCI", 0, // VendorId (any) @@ -136,7 +166,7 @@ namespace Drivers { Pci::ProbePhase::Normal, ProbeAhci, }, - // Order 6: NVMe — Normal phase, match class=0x01/0x08/0x02 (NVM Express) + // Order 7: NVMe — Normal phase, match class=0x01/0x08/0x02 (NVM Express) { "NVMe", 0, // VendorId (any) @@ -148,7 +178,7 @@ namespace Drivers { Pci::ProbePhase::Normal, ProbeNvme, }, - // Order 7: Intel HDA — Normal phase, match vendor=0x8086 + class=0x04 (Multimedia) + // Order 8: Intel HDA — Normal phase, match vendor=0x8086 + class=0x04 (Multimedia) // SubClass 0x03 = "Audio device" (plain HD Audio controller). // SubClass 0x01 = "Multimedia audio controller": on modern // laptops this is the SAME HDA controller enumerated with the diff --git a/kernel/src/Drivers/Net/Wifi/Iwx.hpp b/kernel/src/Drivers/Net/Wifi/Iwx.hpp new file mode 100644 index 0000000..84acb96 --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/Iwx.hpp @@ -0,0 +1,314 @@ +/* + * Iwx.hpp + * Intel AX210/AX211 Wi-Fi driver core - shared state and internal API. + * + * The driver is split into: + * IwxTrans.cpp - PCIe transport: MMIO, MSI-X, DMA rings, firmware boot + * (context info gen3), host commands, RX processing + * IwxFw.cpp - .ucode / .pnvm TLV file parsing + * IwxMvm.cpp - post-ALIVE firmware init, NVM, UMAC scan + * IwxConnect.cpp - auth/assoc groundwork (untested scaffolding) + * Wifi.cpp - public subsystem facade (probe, deferred init, syscalls) + * + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include +#include +#include +#include "IwxReg.hpp" + +namespace Drivers::Net::Wifi { + + // ========================================================================= + // DMA helpers (contiguous physical allocations from the PFA) + // ========================================================================= + + struct IwxDma { + void* Virt = nullptr; + uint64_t Phys = 0; + uint32_t Pages = 0; + }; + + bool IwxDmaAlloc(IwxDma& dma, uint64_t bytes); // zeroed, page-granular + void IwxDmaFree(IwxDma& dma); + + // ========================================================================= + // Firmware image (parsed .ucode file) + // ========================================================================= + + struct IwxFwSection { + uint32_t DevOff = 0; // device offset / separator marker + const uint8_t* Data = nullptr; + uint32_t Len = 0; + }; + + // AX211 firmware 89 has 60 sections (15 LMAC + 17 UMAC + 26 paging, plus + // two separators); leave headroom for newer images. + constexpr int IWX_MAX_FW_SECTIONS = 96; + constexpr int IWX_MAX_FW_CMD_VERSIONS = 384; + + struct IwxFwInfo { + uint8_t* Raw = nullptr; // whole .ucode file (kernel heap) + uint64_t RawSize = 0; + + IwxFwSection Sections[IWX_MAX_FW_SECTIONS]; + int SectionCount = 0; + + const uint8_t* Iml = nullptr; // image loader (points into Raw) + uint32_t ImlLen = 0; + + // PNVM TLV blob: either embedded in the .ucode (points into Raw) or + // read from the separate .pnvm file, in which case PnvmOwned holds the + // heap allocation that has to be released. + const uint8_t* PnvmData = nullptr; + uint32_t PnvmLen = 0; + uint8_t* PnvmOwned = nullptr; + + uint8_t ApiFlags[IWX_NUM_UCODE_TLV_API / 8] = {}; + uint8_t Capa[IWX_NUM_UCODE_TLV_CAPA / 8] = {}; + + IwxFwCmdVersion CmdVersions[IWX_MAX_FW_CMD_VERSIONS]; + int NumCmdVersions = 0; + + uint32_t PhyConfig = 0; // PHY_SKU + uint32_t NumScanChannels = IWX_DEFAULT_SCAN_CHANNELS; + char Version[48] = {}; + }; + + inline bool IwxBitSet(const uint8_t* map, uint32_t bit) { + return (map[bit / 8] & (1 << (bit % 8))) != 0; + } + + // ========================================================================= + // Rings + // ========================================================================= + + struct IwxTxRing { + int Qid = 0; + IwxDma Desc; // IwxTfhTfd[IWX_TX_RING_COUNT] + IwxDma Cmd; // IwxDeviceCmd[IWX_TX_RING_COUNT] + IwxDma BcTbl; // byte-count table + IwxDma Bounce; // one-page bounce for oversized commands + uint32_t Cur = 0; // ring slot (0..count-1) + uint32_t CurHw = 0; // hardware index (0..65535) + uint32_t Queued = 0; + }; + + struct IwxRxRing { + IwxDma FreeDescs; // IwxRxTransferDesc[IWX_RX_MQ_RING_COUNT] + IwxDma Stat; // uint16_t used-ring write index + IwxDma UsedDescs; // IwxRxCompletionDesc[IWX_RX_MQ_RING_COUNT] + uint8_t* Buf[IWX_RX_MQ_RING_COUNT] = {}; + uint64_t BufPhys[IWX_RX_MQ_RING_COUNT] = {}; + uint32_t Cur = 0; + }; + + // ========================================================================= + // NVM data (from NVM_GET_INFO) + // ========================================================================= + + struct IwxNvmData { + uint8_t HwAddr[6] = {}; + bool Sku24GHz = false; + bool Sku52GHz = false; + bool Sku11n = false; + bool Sku11ac = false; + bool Sku11ax = false; + bool LarEnabled = false; + uint8_t ValidTxAnt = 0; + uint8_t ValidRxAnt = 0; + uint16_t NvmVersion = 0; + }; + + // Per-channel scan availability derived from the NVM/MCC channel profile. + struct IwxChannel { + uint8_t ChannelNum = 0; // IEEE channel number + bool Is5GHz = false; + bool Valid = false; + bool ActiveAllowed = false; // active (probe) scanning allowed + }; + + constexpr int IWX_MAX_CHANNELS_TRACKED = IWX_NUM_2GHZ_CHANNELS + IWX_NUM_5GHZ_CHANNELS; + + // ========================================================================= + // Driver state + // ========================================================================= + + enum class IwxFwState : uint8_t { + Absent = 0, // no device found + Detected, // PCI device claimed, waiting for VFS (firmware file) + Booting, // firmware load in progress + Running, // operational firmware alive and initialized + Error, // fatal error; device stopped + RfKill, // radio disabled by hardware switch + }; + + struct IwxState { + // PCI location + MMIO + uint8_t Bus = 0, Dev = 0, Func = 0; + volatile uint8_t* Mmio = nullptr; + uint32_t HwRev = 0; + uint32_t HwRfId = 0; + + IwxFwState State = IwxFwState::Absent; + bool MsixProgrammed = false; + int NicLockCount = 0; + + // Interrupt-to-idle-loop deferral + volatile bool WorkPending = false; + + IwxFwInfo Fw; + + // Boot-time DMA + IwxDma CtxtInfo; // IwxContextInfoGen3 + IwxDma PrphScratch; // IwxPrphScratch + IwxDma PrphInfo; // one page (incl. dummy TR/CR tails) + IwxDma ImlDma; + // LMAC/UMAC section copies: released once the firmware is alive. + IwxDma FwSecDma[IWX_MAX_FW_SECTIONS]; + int FwSecDmaCount = 0; + // Paging sections: the firmware keeps reading these while it runs, so + // they are only released when the device is stopped. + IwxDma PagingDma[IWX_MAX_DRAM_ENTRY]; + int PagingCount = 0; + IwxDma PnvmDma; // PNVM payload (or fragment table) + IwxDma PnvmSegDma[IWX_MAX_DRAM_ENTRY]; + int PnvmSegs = 0; + uint32_t PnvmSize = 0; + uint32_t PnvmVersion = 0; + + IwxRxRing RxQ; + IwxTxRing CmdQ; // queue 0: host commands + IwxTxRing MgmtQ; // queue 1: management frames (connect path) + + // ALIVE / init-complete tracking (set from notification processing) + volatile bool AliveIntr = false; + volatile bool AliveOk = false; + volatile uint32_t InitComplete = 0; // bit0 INIT, bit1 PNVM + IwxSkuId SkuId = {}; + + // Firmware error-log pointers reported by ALIVE, and the last command + // we sent -- together these identify what the firmware choked on. + uint32_t UmacErrorTable = 0; + uint32_t LmacErrorTable = 0; + uint32_t LastCmdId = 0; + bool LtrEnabled = false; // PCIe LTR capability advertised + + // Synchronous-command bookkeeping (commands are fully serialized) + kcp::Spinlock CmdLock; // serializes SendCmd callers + volatile bool CmdDone = false; + volatile bool CmdWantResp = false; + uint8_t CmdRespBuf[4096]; + volatile uint32_t CmdRespLen = 0; + uint32_t CmdIdx = 0; // ring slot of in-flight command + + // Reentrancy guard for ProcessEvents + volatile bool InProcessEvents = false; + + IwxNvmData Nvm; + IwxChannel Channels[IWX_MAX_CHANNELS_TRACKED]; + int ChannelCount = 0; + + // Scan state + volatile bool ScanActive = false; + volatile bool ScanCompleted = false; + + // Statistics/diagnostics + uint64_t RxPackets = 0; + uint64_t FwErrors = 0; + }; + + extern IwxState g_iwx; + + // ========================================================================= + // Transport (IwxTrans.cpp) + // ========================================================================= + + bool IwxProbe(const Pci::PciDevice& dev); // claim device, map BAR, MSI-X + bool IwxStartHw(); // prepare + reset + apm init + void IwxStopDevice(); + bool IwxStartFirmware(); // context-info boot, wait ALIVE + bool IwxLoadPnvm(); // after ALIVE + + uint32_t IwxRead32(uint32_t reg); + void IwxWrite32(uint32_t reg, uint32_t val); + void IwxWrite8(uint32_t reg, uint8_t val); + void IwxSetBits(uint32_t reg, uint32_t bits); + void IwxClearBits(uint32_t reg, uint32_t bits); + bool IwxNicLock(); + void IwxNicUnlock(); + uint32_t IwxReadPrph(uint32_t addr); // caller holds nic lock + void IwxWritePrph(uint32_t addr, uint32_t val); + uint32_t IwxReadUmacPrph(uint32_t addr); + void IwxWriteUmacPrph(uint32_t addr, uint32_t val); + bool IwxPollBit(uint32_t reg, uint32_t bits, uint32_t mask, int timeoutUs); + + void IwxDelayUs(uint32_t us); + void IwxDelayMs(uint32_t ms); + + // Host commands. Payload is copied; for WANT_RESP the response packet is + // copied into g_iwx.CmdRespBuf. Synchronous variants pump ProcessEvents. + struct IwxHostCmd { + uint32_t Id = 0; // opcode or IWX_WIDE_ID(group, opcode) + const void* Data = nullptr; + uint32_t Len = 0; + bool WantResp = false; + }; + bool IwxSendCmd(IwxHostCmd& cmd); // sync, 1s timeout + bool IwxSendCmdPdu(uint32_t id, const void* data, uint32_t len); + bool IwxSendCmdStatus(uint32_t id, const void* data, uint32_t len, + uint32_t* statusOut); + + // Poll interrupt causes + drain the RX/notification ring. Safe to call + // from any process/idle context; self-guarded against reentry. + void IwxProcessEvents(); + + // Look up the firmware-advertised version of a command/notification. + int IwxLookupCmdVer(uint8_t group, uint8_t cmd); + int IwxLookupNotifVer(uint8_t group, uint8_t cmd); + + bool IwxCheckRfKill(); + + // TX queue management (used by connect path) + bool IwxEnableTxq(int staId, int qid, int tid); + + // ========================================================================= + // Firmware file parsing (IwxFw.cpp) + // ========================================================================= + + bool IwxReadFirmware(); // load + parse .ucode from VFS + void IwxFreeFirmware(); + // Parse PNVM data (embedded TLV or .pnvm file) and stage DMA for our SKU. + bool IwxPnvmParse(const uint8_t* data, uint64_t len); + + // ========================================================================= + // MVM op-mode (IwxMvm.cpp) + // ========================================================================= + + bool IwxRunInitUcode(); // boot + NVM + init complete + bool IwxInitHw(); // full init: ant/bt/soc/ltr/scan cfg + bool IwxStartScan(const char* directSsid); // directSsid may be null + bool IwxAbortScan(); + + // Notification dispatch, called from RX processing for every fw packet. + void IwxHandleNotification(const IwxRxPacket* pkt, const uint8_t* rxBuf, + uint32_t bufLen); + + // Scan results sink, implemented by Wifi.cpp: raw 802.11 beacon/probe-resp. + void WifiRxMgmtFrame(const uint8_t* frame, uint32_t len, uint8_t channel, + int8_t rssiDbm); + + // ========================================================================= + // Connect groundwork (IwxConnect.cpp) - UNTESTED scaffolding + // ========================================================================= + + bool IwxConnectStart(const uint8_t* bssid, uint8_t channel, bool is5GHz, + const char* ssid); + void IwxConnectAbort(); + void IwxConnectRxMgmt(const uint8_t* frame, uint32_t len); + // Apply state changes the RX path queued (it cannot send commands itself). + void IwxConnectService(); + int IwxConnectState(); +} diff --git a/kernel/src/Drivers/Net/Wifi/IwxConnect.cpp b/kernel/src/Drivers/Net/Wifi/IwxConnect.cpp new file mode 100644 index 0000000..e104c1d --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/IwxConnect.cpp @@ -0,0 +1,391 @@ +/* + * IwxConnect.cpp + * Association groundwork: PHY/MAC context setup, station add, and the + * open-system authentication + association exchange. + * + * STATUS: this path is scaffolding. It builds the firmware contexts the + * same way iwlwifi does and drives the 802.11 state machine far enough to + * authenticate and associate with an open network, but it has never been + * exercised on hardware, and the WPA2 key exchange is deliberately not + * implemented (see WifiConnectSecurity notes in Wifi.cpp). Scanning is the + * supported operation today. + * + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "Iwx.hpp" +#include +#include +#include +#include + +using namespace Kt; + +namespace Drivers::Net::Wifi { + + // Connection state machine. + enum class ConnState : int { + Idle = 0, + ContextsUp, // PHY/MAC/binding/STA programmed, ready to authenticate + Authenticating, + Authenticated, + Associating, + Associated, + Failed, + }; + + static ConnState g_state = ConnState::Idle; + static uint8_t g_bssid[6] = {}; + static uint8_t g_channel = 0; + static bool g_is5GHz = false; + static char g_ssid[33] = {}; + static uint8_t g_ssidLen = 0; + static uint16_t g_aid = 0; + + static constexpr uint32_t MAC_ID = 0; + static constexpr uint32_t MAC_COLOR = 0; + static constexpr uint32_t PHY_ID = 0; + static constexpr uint32_t PHY_COLOR = 0; + + static bool g_phyActive = false; + static bool g_macActive = false; + static bool g_bindingActive = false; + static bool g_staActive = false; + static bool g_mgmtQueueUp = false; + + // Work discovered while parsing an inbound frame, applied later from the + // idle loop: RX processing must not send commands (it would re-enter the + // event pump that its own completion depends on). + static volatile bool g_postAssocPending = false; + static volatile bool g_teardownPending = false; + + static uint8_t FwValidRxAntConn() { + uint8_t ant = (uint8_t)((g_iwx.Fw.PhyConfig & IWX_FW_PHY_CFG_RX_CHAIN) + >> IWX_FW_PHY_CFG_RX_CHAIN_POS); + if (g_iwx.Nvm.ValidRxAnt) ant &= g_iwx.Nvm.ValidRxAnt; + return ant; + } + + // ========================================================================= + // Firmware contexts + // ========================================================================= + + static bool PhyCtxtCmd(uint32_t action) { + IwxPhyContextCmd cmd = {}; + cmd.id_and_color = IWX_FW_CMD_ID_AND_COLOR(PHY_ID, PHY_COLOR); + cmd.action = action; + cmd.lmac_id = (!g_is5GHz + || !IwxBitSet(g_iwx.Fw.Capa, IWX_UCODE_TLV_CAPA_CDB_SUPPORT)) + ? IWX_LMAC_24G_INDEX : IWX_LMAC_5G_INDEX; + cmd.ci.band = g_is5GHz ? IWX_PHY_BAND_5 : IWX_PHY_BAND_24; + cmd.ci.channel = g_channel; + cmd.ci.width = IWX_PHY_VHT_CHANNEL_MODE20; + cmd.ci.ctrl_pos = IWX_PHY_VHT_CTRL_POS_1_BELOW; + + // From RLC_CONFIG v2 on, the chain configuration moved out of this + // command into its own RLC command. + if (IwxLookupCmdVer(IWX_DATA_PATH_GROUP, IWX_RLC_CONFIG_CMD) != 2) { + cmd.rxchain_info = (uint32_t)FwValidRxAntConn() << IWX_PHY_RX_CHAIN_VALID_POS; + cmd.rxchain_info |= 1u << IWX_PHY_RX_CHAIN_CNT_POS; + cmd.rxchain_info |= 1u << IWX_PHY_RX_CHAIN_MIMO_CNT_POS; + } + + return IwxSendCmdPdu(IWX_PHY_CONTEXT_CMD, &cmd, sizeof(cmd)); + } + + static bool PhySendRlc() { + if (IwxLookupCmdVer(IWX_DATA_PATH_GROUP, IWX_RLC_CONFIG_CMD) != 2) + return true; + + IwxRlcConfigCmd cmd = {}; + cmd.phy_id = PHY_ID; + cmd.rlc.rx_chain_info = (uint32_t)FwValidRxAntConn() << IWX_PHY_RX_CHAIN_VALID_POS; + cmd.rlc.rx_chain_info |= 1u << IWX_PHY_RX_CHAIN_CNT_POS; + cmd.rlc.rx_chain_info |= 1u << IWX_PHY_RX_CHAIN_MIMO_CNT_POS; + return IwxSendCmdPdu(IWX_WIDE_ID(IWX_DATA_PATH_GROUP, IWX_RLC_CONFIG_CMD), + &cmd, sizeof(cmd)); + } + + static bool MacCtxtCmd(uint32_t action, bool assoc) { + IwxMacCtxCmd cmd = {}; + cmd.id_and_color = IWX_FW_CMD_ID_AND_COLOR(MAC_ID, MAC_COLOR); + cmd.action = action; + cmd.mac_type = IWX_FW_MAC_TYPE_BSS_STA; + cmd.tsf_id = IWX_TSF_ID_A; + memcpy(cmd.node_addr, g_iwx.Nvm.HwAddr, 6); + memcpy(cmd.bssid_addr, g_bssid, 6); + + // Basic rate masks: CCK 1/2/5.5/11 on 2.4 GHz, OFDM 6/12/24 everywhere. + // The firmware indexes these bitmaps against its own rate tables. + cmd.cck_rates = g_is5GHz ? 0 : 0x0f; + cmd.ofdm_rates = 0x15; + cmd.cck_short_preamble = 0; + cmd.short_slot = 0; + cmd.filter_flags = IWX_MAC_FILTER_ACCEPT_GRP | IWX_MAC_FILTER_IN_BEACON; + cmd.qos_flags = IWX_MAC_QOS_FLG_UPDATE_EDCA; + + // Default EDCA parameters, one entry per access category. + for (uint32_t i = 0; i < IWX_AC_NUM; i++) { + cmd.ac[i].cw_min = 15; + cmd.ac[i].cw_max = 1023; + cmd.ac[i].aifsn = 2; + cmd.ac[i].fifos_mask = (uint8_t)(1 << i); + cmd.ac[i].edca_txop = 0; + } + + cmd.sta.is_assoc = assoc ? 1 : 0; + cmd.sta.bi = 100; + cmd.sta.dtim_interval = 100 * 3; + cmd.sta.listen_interval = 10; + cmd.sta.assoc_id = g_aid; + + return IwxSendCmdPdu(IWX_MAC_CONTEXT_CMD, &cmd, sizeof(cmd)); + } + + static bool BindingCmd(uint32_t action) { + IwxBindingCmd cmd = {}; + cmd.id_and_color = IWX_FW_CMD_ID_AND_COLOR(PHY_ID, PHY_COLOR); + cmd.action = action; + cmd.phy = IWX_FW_CMD_ID_AND_COLOR(PHY_ID, PHY_COLOR); + cmd.macs[0] = IWX_FW_CMD_ID_AND_COLOR(MAC_ID, MAC_COLOR); + for (uint32_t i = 1; i < IWX_MAX_MACS_IN_BINDING; i++) + cmd.macs[i] = IWX_FW_CTXT_INVALID; + cmd.lmac_id = (!g_is5GHz + || !IwxBitSet(g_iwx.Fw.Capa, IWX_UCODE_TLV_CAPA_CDB_SUPPORT)) + ? IWX_LMAC_24G_INDEX : IWX_LMAC_5G_INDEX; + + uint32_t status = 0; + if (!IwxSendCmdStatus(IWX_BINDING_CONTEXT_CMD, &cmd, sizeof(cmd), &status)) + return false; + return status == 0; + } + + static bool AddStaCmd(bool update) { + IwxAddStaCmd cmd = {}; + cmd.add_modify = update ? 1 : 0; + cmd.mac_id_n_color = IWX_FW_CMD_ID_AND_COLOR(MAC_ID, MAC_COLOR); + cmd.sta_id = IWX_STATION_ID; + cmd.station_type = IWX_STA_TYPE_LINK; + memcpy(cmd.addr, g_bssid, 6); + cmd.tid_disable_tx = 0xffff; // aggregation disabled for now + + uint32_t status = 0; + if (!IwxSendCmdStatus(IWX_ADD_STA, &cmd, sizeof(cmd), &status)) + return false; + return (status & IWX_ADD_STA_STATUS_MASK) == IWX_ADD_STA_SUCCESS; + } + + static bool RemoveStaCmd() { + struct { uint8_t sta_id; uint8_t reserved[3]; } __attribute__((packed)) cmd = {}; + cmd.sta_id = IWX_STATION_ID; + return IwxSendCmdPdu(IWX_REMOVE_STA, &cmd, sizeof(cmd)); + } + + // Keep the firmware on our channel for the duration of the exchange. + static bool ScheduleSessionProtection(uint32_t durationTu) { + if (!IwxBitSet(g_iwx.Fw.Capa, IWX_UCODE_TLV_CAPA_SESSION_PROT_CMD)) + return true; + + IwxSessionProtCmd cmd = {}; + cmd.id_and_color = IWX_FW_CMD_ID_AND_COLOR(MAC_ID, MAC_COLOR); + cmd.action = IWX_FW_CTXT_ACTION_ADD; + cmd.conf_id = IWX_SESSION_PROTECT_CONF_ASSOC; + cmd.duration_tu = durationTu; + return IwxSendCmdPdu(IWX_WIDE_ID(IWX_MAC_CONF_GROUP, IWX_SESSION_PROTECTION_CMD), + &cmd, sizeof(cmd)); + } + + // ========================================================================= + // Teardown + // ========================================================================= + + static void TearDown() { + if (g_staActive) { RemoveStaCmd(); g_staActive = false; } + if (g_bindingActive) { + BindingCmd(IWX_FW_CTXT_ACTION_REMOVE); + g_bindingActive = false; + } + if (g_macActive) { + MacCtxtCmd(IWX_FW_CTXT_ACTION_REMOVE, false); + g_macActive = false; + } + if (g_phyActive) { + PhyCtxtCmd(IWX_FW_CTXT_ACTION_REMOVE); + g_phyActive = false; + } + g_mgmtQueueUp = false; + g_aid = 0; + } + + // ========================================================================= + // Public entry points + // ========================================================================= + + bool IwxConnectStart(const uint8_t* bssid, uint8_t channel, bool is5GHz, + const char* ssid) { + if (g_iwx.State != IwxFwState::Running) return false; + if (g_state != ConnState::Idle) IwxConnectAbort(); + + memcpy(g_bssid, bssid, 6); + g_channel = channel; + g_is5GHz = is5GHz; + g_aid = 0; + g_ssidLen = 0; + if (ssid) { + while (g_ssidLen < 32 && ssid[g_ssidLen]) { + g_ssid[g_ssidLen] = ssid[g_ssidLen]; + g_ssidLen++; + } + } + g_ssid[g_ssidLen] = '\0'; + + if (g_iwx.ScanActive) IwxAbortScan(); + + if (!PhyCtxtCmd(IWX_FW_CTXT_ACTION_ADD)) { + KernelLogStream(ERROR, "WiFi") << "Could not add PHY context"; + return false; + } + g_phyActive = true; + + if (!PhySendRlc()) { + KernelLogStream(ERROR, "WiFi") << "Could not configure RLC for PHY"; + TearDown(); + return false; + } + + if (!MacCtxtCmd(IWX_FW_CTXT_ACTION_ADD, false)) { + KernelLogStream(ERROR, "WiFi") << "Could not add MAC context"; + TearDown(); + return false; + } + g_macActive = true; + + if (!BindingCmd(IWX_FW_CTXT_ACTION_ADD)) { + KernelLogStream(ERROR, "WiFi") << "Could not add binding"; + TearDown(); + return false; + } + g_bindingActive = true; + + if (!AddStaCmd(false)) { + KernelLogStream(ERROR, "WiFi") << "Could not add station"; + TearDown(); + return false; + } + g_staActive = true; + + // Non-QoS management frames go out on the MGMT TID/queue. + if (!IwxEnableTxq(IWX_STATION_ID, IWX_DQA_MGMT_QUEUE, IWX_MGMT_TID)) { + KernelLogStream(WARNING, "WiFi") + << "Management TX queue unavailable; cannot transmit auth frames"; + TearDown(); + return false; + } + g_mgmtQueueUp = true; + + // Beacon interval 100 TU * 9 is what iwlwifi reserves for the whole + // authenticate + associate exchange. + ScheduleSessionProtection(900); + + g_state = ConnState::ContextsUp; + KernelLogStream(INFO, "WiFi") + << "Firmware contexts up for BSSID " << base::hex + << (uint64_t)g_bssid[0] << ":" << (uint64_t)g_bssid[1] << ":" + << (uint64_t)g_bssid[2] << ":" << (uint64_t)g_bssid[3] << ":" + << (uint64_t)g_bssid[4] << ":" << (uint64_t)g_bssid[5] << base::dec + << " on channel " << (uint64_t)g_channel; + + // Transmitting the authentication frame itself requires the TX data + // path (TFD assembly, rate selection, TX status handling), which this + // driver does not implement yet. Everything above is the firmware-side + // state the exchange needs; the 802.11 handshake is the remaining work. + KernelLogStream(WARNING, "WiFi") + << "Authentication frame TX is not implemented; stopping after context setup"; + return true; + } + + void IwxConnectAbort() { + if (g_state == ConnState::Idle) return; + TearDown(); + g_state = ConnState::Idle; + } + + // Inbound management frames for the connection state machine. Beacons and + // probe responses are consumed by the scan path instead. + void IwxConnectRxMgmt(const uint8_t* frame, uint32_t len) { + if (g_state == ConnState::Idle || len < 24) return; + + uint8_t subtype = (uint8_t)(frame[0] & 0xf0); + constexpr uint8_t SUBTYPE_AUTH = 0xb0; + constexpr uint8_t SUBTYPE_ASSOC_RESP = 0x10; + constexpr uint8_t SUBTYPE_DEAUTH = 0xc0; + constexpr uint8_t SUBTYPE_DISASSOC = 0xa0; + + // Only frames from the BSS we are joining are interesting here. + for (int i = 0; i < 6; i++) + if (frame[10 + i] != g_bssid[i]) return; + + switch (subtype) { + case SUBTYPE_AUTH: { + if (len < 30) return; + uint16_t status = (uint16_t)(frame[28] | (frame[29] << 8)); + if (status == 0) { + g_state = ConnState::Authenticated; + KernelLogStream(OK, "WiFi") << "Authenticated"; + } else { + g_state = ConnState::Failed; + KernelLogStream(WARNING, "WiFi") + << "Authentication rejected, status " << (uint64_t)status; + } + break; + } + case SUBTYPE_ASSOC_RESP: { + if (len < 30) return; + uint16_t status = (uint16_t)(frame[26] | (frame[27] << 8)); + if (status == 0) { + g_aid = (uint16_t)((frame[28] | (frame[29] << 8)) & 0x3fff); + g_state = ConnState::Associated; + // This runs inside RX processing, which is guarded against + // reentry; sending the context updates from here would + // deadlock their own completion wait. Defer to the idle + // loop (IwxConnectService). + g_postAssocPending = true; + KernelLogStream(OK, "WiFi") << "Associated, AID " + << (uint64_t)g_aid; + } else { + g_state = ConnState::Failed; + KernelLogStream(WARNING, "WiFi") + << "Association rejected, status " << (uint64_t)status; + } + break; + } + case SUBTYPE_DEAUTH: + case SUBTYPE_DISASSOC: + KernelLogStream(INFO, "WiFi") << "Link torn down by AP"; + g_teardownPending = true; // see IwxConnectService + break; + default: + break; + } + } + + // Apply work queued by the RX path. Called from the idle loop, where + // sending firmware commands (and pumping their completions) is safe. + void IwxConnectService() { + if (g_teardownPending) { + g_teardownPending = false; + TearDown(); + g_state = ConnState::Idle; + return; + } + if (g_postAssocPending) { + g_postAssocPending = false; + MacCtxtCmd(IWX_FW_CTXT_ACTION_MODIFY, true); + AddStaCmd(true); + } + } + + int IwxConnectState() { + return (int)g_state; + } +} diff --git a/kernel/src/Drivers/Net/Wifi/IwxFw.cpp b/kernel/src/Drivers/Net/Wifi/IwxFw.cpp new file mode 100644 index 0000000..8612cbf --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/IwxFw.cpp @@ -0,0 +1,437 @@ +/* + * IwxFw.cpp + * Intel Wi-Fi firmware file handling: .ucode TLV parsing and PNVM staging. + * + * Files live on the ramdisk at 0:/os/firmware/intel/ (staged there by the + * userspace build, same as the Bluetooth .sfi images): + * + * iwlwifi-so-a0-gf-a0-.ucode operational firmware (AX211 / RF GF) + * iwlwifi-so-a0-gf-a0.pnvm platform NVM (regulatory data) + * + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "Iwx.hpp" +#include +#include +#include +#include +#include + +using namespace Kt; + +namespace Drivers::Net::Wifi { + + static constexpr const char* FW_DIR = "0:/os/firmware/intel/"; + + // Firmware API revisions to try, newest first. Intel ships one file per + // API level and the driver simply uses the newest one present. + static constexpr int FW_API_TRY[] = { 89, 86, 83, 81, 79, 78, 77, 74, 73, 72 }; + + // ========================================================================= + // Small helpers + // ========================================================================= + + static uint32_t Rd32(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 uint16_t Rd16(const uint8_t* p) { + return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); + } + + static void SetBit(uint8_t* map, uint32_t bit, uint32_t mapBits) { + if (bit >= mapBits) return; + map[bit / 8] = (uint8_t)(map[bit / 8] | (1 << (bit % 8))); + } + + static char* AppendStr(char* p, const char* s) { + while (*s) *p++ = *s++; + return p; + } + + static char* AppendU32(char* p, uint32_t v) { + char tmp[12]; + int n = 0; + if (v == 0) tmp[n++] = '0'; + while (v) { tmp[n++] = (char)('0' + (v % 10)); v /= 10; } + while (n) *p++ = tmp[--n]; + return p; + } + + // ========================================================================= + // .ucode TLV parsing + // ========================================================================= + + static bool ParseUcode(uint8_t* raw, uint64_t size) { + IwxFwInfo& fw = g_iwx.Fw; + + if (size < sizeof(IwxTlvUcodeHeader)) { + KernelLogStream(ERROR, "WiFi-FW") << "Firmware file too small"; + return false; + } + + auto* uhdr = (IwxTlvUcodeHeader*)raw; + if (uhdr->zero != 0 || uhdr->magic != IWX_TLV_UCODE_MAGIC) { + KernelLogStream(ERROR, "WiFi-FW") << "Not a TLV firmware image"; + return false; + } + + const uint8_t* data = raw + sizeof(IwxTlvUcodeHeader); + uint64_t len = size - sizeof(IwxTlvUcodeHeader); + + // Default version string from the header; a FW_VERSION TLV overrides it. + char* vp = fw.Version; + vp = AppendU32(vp, IWX_UCODE_MAJOR(uhdr->ver)); + *vp++ = '.'; + vp = AppendU32(vp, IWX_UCODE_MINOR(uhdr->ver)); + *vp++ = '.'; + vp = AppendU32(vp, IWX_UCODE_API(uhdr->ver)); + *vp = '\0'; + + while (len >= sizeof(IwxUcodeTlv)) { + uint32_t type = Rd32(data); + uint32_t tlvLen = Rd32(data + 4); + len -= sizeof(IwxUcodeTlv); + data += sizeof(IwxUcodeTlv); + if (tlvLen > len) { + KernelLogStream(ERROR, "WiFi-FW") << "Truncated firmware TLV"; + return false; + } + + switch (type) { + case IWX_UCODE_TLV_SEC_RT: { + // [devoff][payload]. Separator markers carry no payload. + if (tlvLen < 4) break; + if (fw.SectionCount >= IWX_MAX_FW_SECTIONS) { + KernelLogStream(ERROR, "WiFi-FW") + << "Firmware has more sections than supported"; + return false; + } + IwxFwSection& sec = fw.Sections[fw.SectionCount++]; + sec.DevOff = Rd32(data); + sec.Data = data + 4; + sec.Len = tlvLen - 4; + break; + } + case IWX_UCODE_TLV_IML: + fw.Iml = data; + fw.ImlLen = tlvLen; + break; + case IWX_UCODE_TLV_PNVM_DATA: + fw.PnvmData = data; + fw.PnvmLen = tlvLen; + break; + case IWX_UCODE_TLV_PHY_SKU: + if (tlvLen >= 4) fw.PhyConfig = Rd32(data); + break; + case IWX_UCODE_TLV_N_SCAN_CHANNELS: + if (tlvLen >= 4) { + fw.NumScanChannels = Rd32(data); + if (fw.NumScanChannels > IWX_MAX_SCAN_CHANNELS) + fw.NumScanChannels = IWX_MAX_SCAN_CHANNELS; + } + break; + case IWX_UCODE_TLV_FW_VERSION: + if (tlvLen >= 12) { + char* p = fw.Version; + p = AppendU32(p, Rd32(data)); + *p++ = '.'; + p = AppendU32(p, Rd32(data + 4)); + *p++ = '.'; + p = AppendU32(p, Rd32(data + 8)); + *p = '\0'; + } + break; + case IWX_UCODE_TLV_API_CHANGES_SET: { + if (tlvLen < sizeof(IwxUcodeApiCapa)) break; + uint32_t idx = Rd32(data); + uint32_t flags = Rd32(data + 4); + for (int i = 0; i < 32; i++) + if (flags & (1u << i)) + SetBit(fw.ApiFlags, i + 32 * idx, IWX_NUM_UCODE_TLV_API); + break; + } + case IWX_UCODE_TLV_ENABLED_CAPABILITIES: { + if (tlvLen < sizeof(IwxUcodeApiCapa)) break; + uint32_t idx = Rd32(data); + uint32_t flags = Rd32(data + 4); + for (int i = 0; i < 32; i++) + if (flags & (1u << i)) + SetBit(fw.Capa, i + 32 * idx, IWX_NUM_UCODE_TLV_CAPA); + break; + } + case IWX_UCODE_TLV_CMD_VERSIONS: { + uint32_t n = tlvLen / sizeof(IwxFwCmdVersion); + if (n > IWX_MAX_FW_CMD_VERSIONS) n = IWX_MAX_FW_CMD_VERSIONS; + memcpy(fw.CmdVersions, data, n * sizeof(IwxFwCmdVersion)); + fw.NumCmdVersions = (int)n; + break; + } + default: + // Debug/monitor/calibration TLVs are not used here. + break; + } + + uint64_t adv = (tlvLen + 3) & ~3ull; + if (adv > len) break; // trailing padding + len -= adv; + data += adv; + } + + if (fw.SectionCount == 0) { + KernelLogStream(ERROR, "WiFi-FW") << "Firmware image has no sections"; + return false; + } + return true; + } + + // ========================================================================= + // PNVM parsing + // ========================================================================= + + // Fragmented PNVM: the firmware reads a table of segment addresses whose + // base is handed over in prph_scratch.pnvm_cfg. Non-fragmented firmware + // instead wants one flat buffer. + static bool PnvmSetup(uint8_t* const* segs, const uint32_t* sizes, int count) { + bool fragmented = IwxBitSet(g_iwx.Fw.Capa, + IWX_UCODE_TLV_CAPA_FRAGMENTED_PNVM_IMG); + + if (fragmented) { + if (!IwxDmaAlloc(g_iwx.PnvmDma, sizeof(IwxPnvmInfoDram))) return false; + auto* info = (IwxPnvmInfoDram*)g_iwx.PnvmDma.Virt; + for (int i = 0; i < count && i < (int)IWX_MAX_DRAM_ENTRY; i++) { + if (!IwxDmaAlloc(g_iwx.PnvmSegDma[i], sizes[i])) return false; + memcpy(g_iwx.PnvmSegDma[i].Virt, segs[i], sizes[i]); + info->pnvm_img[i] = g_iwx.PnvmSegDma[i].Phys; + g_iwx.PnvmSize += sizes[i]; + g_iwx.PnvmSegs = i + 1; + } + return g_iwx.PnvmSegs > 0; + } + + uint32_t total = 0; + for (int i = 0; i < count; i++) total += sizes[i]; + if (total == 0) return false; + if (!IwxDmaAlloc(g_iwx.PnvmDma, total)) return false; + + uint8_t* dst = (uint8_t*)g_iwx.PnvmDma.Virt; + for (int i = 0; i < count; i++) { + memcpy(dst, segs[i], sizes[i]); + dst += sizes[i]; + } + g_iwx.PnvmSize = total; + return true; + } + + // Parse the section that follows a matching PNVM_SKU entry: verify the + // hardware type, then collect the SEC_RT payloads into DMA. + static bool PnvmHandleSection(const uint8_t* data, uint64_t len) { + uint8_t* segs[IWX_MAX_DRAM_ENTRY]; + uint32_t sizes[IWX_MAX_DRAM_ENTRY]; + int count = 0; + bool hwMatch = false; + uint32_t sha1 = 0; + uint32_t total = 0; + bool ok = false; + + uint16_t ourMac = (uint16_t)IWX_CSR_HW_REV_TYPE(g_iwx.HwRev); + uint16_t ourRf = (uint16_t)IWX_CSR_HW_RFID_TYPE(g_iwx.HwRfId); + + while (len >= sizeof(IwxUcodeTlv)) { + uint32_t type = Rd32(data); + uint32_t tlvLen = Rd32(data + 4); + len -= sizeof(IwxUcodeTlv); + data += sizeof(IwxUcodeTlv); + if (tlvLen > len) break; + + if (type == IWX_UCODE_TLV_PNVM_VERSION) { + if (tlvLen >= 4) sha1 = Rd32(data); + } else if (type == IWX_UCODE_TLV_HW_TYPE) { + if (tlvLen >= 4 && !hwMatch) { + uint16_t macType = Rd16(data); + uint16_t rfId = Rd16(data + 2); + if (macType == ourMac && rfId == ourRf) hwMatch = true; + } + } else if (type == IWX_UCODE_TLV_SEC_RT) { + // struct iwx_pnvm_section { uint32_t offset; uint8_t data[]; } + if (tlvLen <= 4) { /* nothing to copy */ } + else if (Rd32(data) == 0xddddeeee) { /* deprecated separator */ } + else if (count < (int)IWX_MAX_DRAM_ENTRY) { + uint32_t dataLen = tlvLen - 4; + auto* buf = (uint8_t*)Memory::g_heap->Request(dataLen); + if (!buf) goto out; + memcpy(buf, data + 4, dataLen); + segs[count] = buf; + sizes[count] = dataLen; + count++; + total += dataLen; + } + } else if (type == IWX_UCODE_TLV_PNVM_SKU) { + break; // next SKU section starts here + } + + uint64_t adv = (tlvLen + 3) & ~3ull; + if (adv > len) break; + len -= adv; + data += adv; + } + + if (!hwMatch || total == 0) goto out; + + if (!PnvmSetup(segs, sizes, count)) { + KernelLogStream(ERROR, "WiFi-FW") << "Could not stage PNVM in DMA memory"; + goto out; + } + g_iwx.PnvmVersion = sha1; + ok = true; + + out: + for (int i = 0; i < count; i++) Memory::g_heap->Free(segs[i]); + return ok; + } + + bool IwxPnvmParse(const uint8_t* data, uint64_t len) { + while (len >= sizeof(IwxUcodeTlv)) { + uint32_t type = Rd32(data); + uint32_t tlvLen = Rd32(data + 4); + uint64_t adv = (tlvLen + 3) & ~3ull; + if (tlvLen > len - sizeof(IwxUcodeTlv)) return false; + + if (type == IWX_UCODE_TLV_PNVM_SKU && tlvLen >= 12) { + const uint8_t* sku = data + sizeof(IwxUcodeTlv); + uint32_t s0 = Rd32(sku), s1 = Rd32(sku + 4), s2 = Rd32(sku + 8); + + const uint8_t* next = data + sizeof(IwxUcodeTlv) + adv; + uint64_t remain = len - sizeof(IwxUcodeTlv) - adv; + + if (s0 == g_iwx.SkuId.data[0] && s1 == g_iwx.SkuId.data[1] + && s2 == g_iwx.SkuId.data[2] + && PnvmHandleSection(next, remain)) + return true; + + data = next; + len = remain; + } else { + data += sizeof(IwxUcodeTlv) + adv; + len -= sizeof(IwxUcodeTlv) + adv; + } + } + return false; + } + + // ========================================================================= + // File loading + // ========================================================================= + + static uint8_t* LoadFile(const char* path, uint64_t* outSize) { + Fs::Vfs::BackendFile file; + if (Fs::Vfs::OpenBackendFile(path, file) < 0) return nullptr; + + uint64_t size = Fs::Vfs::GetBackendFileSize(file); + if (size == 0 || size > 8u * 1024 * 1024) { + Fs::Vfs::CloseBackendFile(file); + return nullptr; + } + + auto* buf = (uint8_t*)Memory::g_heap->Request(size); + if (!buf) { + Fs::Vfs::CloseBackendFile(file); + return nullptr; + } + Fs::Vfs::ReadBackendFile(file, buf, 0, size); + Fs::Vfs::CloseBackendFile(file); + asm volatile("" ::: "memory"); + + *outSize = size; + return buf; + } + + void IwxFreeFirmware() { + if (g_iwx.Fw.Raw) { + Memory::g_heap->Free(g_iwx.Fw.Raw); + g_iwx.Fw.Raw = nullptr; + } + // Only the separately-loaded .pnvm file is ours to free; an embedded + // PNVM TLV points into Raw and has just gone away with it. + if (g_iwx.Fw.PnvmOwned) { + Memory::g_heap->Free(g_iwx.Fw.PnvmOwned); + g_iwx.Fw.PnvmOwned = nullptr; + } + g_iwx.Fw.RawSize = 0; + g_iwx.Fw.SectionCount = 0; + g_iwx.Fw.Iml = nullptr; + g_iwx.Fw.ImlLen = 0; + g_iwx.Fw.PnvmData = nullptr; + g_iwx.Fw.PnvmLen = 0; + g_iwx.Fw.NumCmdVersions = 0; + } + + bool IwxReadFirmware() { + if (g_iwx.Fw.Raw) return true; // already loaded and parsed + + char path[96]; + uint8_t* raw = nullptr; + uint64_t size = 0; + + for (int api : FW_API_TRY) { + char* p = AppendStr(path, FW_DIR); + p = AppendStr(p, "iwlwifi-so-a0-gf-a0-"); + p = AppendU32(p, (uint32_t)api); + p = AppendStr(p, ".ucode"); + *p = '\0'; + + raw = LoadFile(path, &size); + if (raw) { + KernelLogStream(INFO, "WiFi-FW") << "Loading " << path + << " (" << size << " bytes)"; + break; + } + } + + if (!raw) { + KernelLogStream(ERROR, "WiFi-FW") + << "No iwlwifi-so-a0-gf-a0-*.ucode found under " << FW_DIR; + return false; + } + + g_iwx.Fw.Raw = raw; + g_iwx.Fw.RawSize = size; + + if (!ParseUcode(raw, size)) { + IwxFreeFirmware(); + return false; + } + + KernelLogStream(OK, "WiFi-FW") << "Firmware " << g_iwx.Fw.Version + << ": " << (uint64_t)g_iwx.Fw.SectionCount << " sections, IML " + << (uint64_t)g_iwx.Fw.ImlLen << " bytes, " + << (uint64_t)g_iwx.Fw.NumCmdVersions << " command versions"; + + // The PNVM is usually a separate file; when the .ucode carries an + // embedded copy, IwxLoadPnvm() prefers that and this read is skipped. + if (!g_iwx.Fw.PnvmData) { + char* p = AppendStr(path, FW_DIR); + p = AppendStr(p, "iwlwifi-so-a0-gf-a0.pnvm"); + *p = '\0'; + + uint64_t pnvmSize = 0; + uint8_t* pnvm = LoadFile(path, &pnvmSize); + if (pnvm) { + // Keep the buffer alive for the driver's lifetime: PnvmParse + // runs later, after ALIVE has reported the SKU id. + g_iwx.Fw.PnvmData = pnvm; + g_iwx.Fw.PnvmOwned = pnvm; + g_iwx.Fw.PnvmLen = (uint32_t)pnvmSize; + KernelLogStream(INFO, "WiFi-FW") << "Loaded PNVM (" + << pnvmSize << " bytes)"; + } else { + KernelLogStream(WARNING, "WiFi-FW") + << "No PNVM file; firmware will use built-in regulatory defaults"; + } + } + + return true; + } +} diff --git a/kernel/src/Drivers/Net/Wifi/IwxMvm.cpp b/kernel/src/Drivers/Net/Wifi/IwxMvm.cpp new file mode 100644 index 0000000..2ed3ac9 --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/IwxMvm.cpp @@ -0,0 +1,684 @@ +/* + * IwxMvm.cpp + * Intel Wi-Fi "MVM" op-mode: post-ALIVE firmware configuration, NVM/channel + * map retrieval, and UMAC scanning. + * + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "Iwx.hpp" +#include +#include +#include +#include + +using namespace Kt; + +namespace Drivers::Net::Wifi { + + // IEEE channel numbers in NVM channel-profile order (AX210 with UHB + // support uses the 6 GHz-extended table; we track the 2.4/5 GHz prefix, + // which is identical in both). + static constexpr uint8_t NVM_CHANNELS[] = { + // 2.4 GHz + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + // 5 GHz + 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, + 96, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, + 149, 153, 157, 161, 165, 169, 173, 177, 181 + }; + static constexpr int NVM_CHANNEL_COUNT = + (int)(sizeof(NVM_CHANNELS) / sizeof(NVM_CHANNELS[0])); + + // Supported rates in 500 kbps units. + static constexpr uint8_t RATES_11G[] = { 2, 4, 11, 22, 12, 18, 24, 36 }; + static constexpr uint8_t RATES_11G_EXT[] = { 48, 72, 96, 108 }; + static constexpr uint8_t RATES_11A[] = { 12, 18, 24, 36, 48, 72, 96, 108 }; + + // Directed-scan SSID staged by IwxStartScan for the next command build. + static char g_directSsid[33] = {}; + static uint8_t g_directSsidLen = 0; + + static uint8_t FwValidTxAnt() { + uint8_t ant = (uint8_t)((g_iwx.Fw.PhyConfig & IWX_FW_PHY_CFG_TX_CHAIN) + >> IWX_FW_PHY_CFG_TX_CHAIN_POS); + if (g_iwx.Nvm.ValidTxAnt) ant &= g_iwx.Nvm.ValidTxAnt; + return ant; + } + + static uint8_t FwValidRxAnt() { + uint8_t ant = (uint8_t)((g_iwx.Fw.PhyConfig & IWX_FW_PHY_CFG_RX_CHAIN) + >> IWX_FW_PHY_CFG_RX_CHAIN_POS); + if (g_iwx.Nvm.ValidRxAnt) ant &= g_iwx.Nvm.ValidRxAnt; + return ant; + } + + // ========================================================================= + // Channel map + // ========================================================================= + + static void IwxInitChannelMap(const uint16_t* profileV3, + const uint32_t* profileV4, int nProfile) { + g_iwx.ChannelCount = 0; + + for (int i = 0; i < NVM_CHANNEL_COUNT && i < nProfile + && g_iwx.ChannelCount < IWX_MAX_CHANNELS_TRACKED; i++) { + uint32_t flags = profileV4 ? profileV4[i] : (uint32_t)profileV3[i]; + bool is5 = i >= (int)IWX_NUM_2GHZ_CHANNELS; + + if (is5 && !g_iwx.Nvm.Sku52GHz) continue; + if (!(flags & IWX_NVM_CHANNEL_VALID)) continue; + + IwxChannel& ch = g_iwx.Channels[g_iwx.ChannelCount++]; + ch.ChannelNum = NVM_CHANNELS[i]; + ch.Is5GHz = is5; + ch.Valid = true; + ch.ActiveAllowed = (flags & IWX_NVM_CHANNEL_ACTIVE) != 0; + } + + KernelLogStream(INFO, "WiFi") << "Regulatory: " << (uint64_t)g_iwx.ChannelCount + << " usable channels"; + } + + // ========================================================================= + // NVM + // ========================================================================= + + static void SetMacAddrFromCsr() { + if (!IwxNicLock()) return; + + uint32_t a0 = IwxRead32(IWX_CSR_MAC_ADDR0_STRAP); + uint32_t a1 = IwxRead32(IWX_CSR_MAC_ADDR1_STRAP); + + // The hardware stores the address byte-swapped within each dword. + auto flip = [](uint32_t m0, uint32_t m1, uint8_t* dst) { + const uint8_t* p = (const uint8_t*)&m0; + dst[0] = p[3]; dst[1] = p[2]; dst[2] = p[1]; dst[3] = p[0]; + p = (const uint8_t*)&m1; + dst[4] = p[1]; dst[5] = p[0]; + }; + flip(a0, a1, g_iwx.Nvm.HwAddr); + + auto valid = [](const uint8_t* a) { + static const uint8_t reserved[6] = { 0x02, 0xcc, 0xaa, 0xff, 0xee, 0x00 }; + bool allZero = true, allOnes = true, isReserved = true; + for (int i = 0; i < 6; i++) { + if (a[i] != 0) allZero = false; + if (a[i] != 0xff) allOnes = false; + if (a[i] != reserved[i]) isReserved = false; + } + return !allZero && !allOnes && !isReserved && !(a[0] & 1); + }; + + // The OEM strap wins when fused; otherwise fall back to OTP. + if (!valid(g_iwx.Nvm.HwAddr)) { + a0 = IwxRead32(IWX_CSR_MAC_ADDR0_OTP); + a1 = IwxRead32(IWX_CSR_MAC_ADDR1_OTP); + flip(a0, a1, g_iwx.Nvm.HwAddr); + } + + IwxNicUnlock(); + } + + static bool IwxNvmGet() { + IwxNvmGetInfo cmd = {}; + IwxHostCmd hcmd; + hcmd.Id = IWX_WIDE_ID(IWX_REGULATORY_AND_NVM_GROUP, IWX_NVM_GET_INFO); + hcmd.Data = &cmd; + hcmd.Len = sizeof(cmd); + hcmd.WantResp = true; + + if (!IwxSendCmd(hcmd)) return false; + + bool v4 = IwxBitSet(g_iwx.Fw.ApiFlags, IWX_UCODE_TLV_API_REGULATORY_NVM_INFO); + uint32_t want = v4 ? sizeof(IwxNvmGetInfoRsp) : sizeof(IwxNvmGetInfoRspV3); + + if (g_iwx.CmdRespLen < sizeof(IwxRxPacket) + want) { + KernelLogStream(ERROR, "WiFi") << "Short NVM_GET_INFO response (" + << (uint64_t)g_iwx.CmdRespLen << " bytes)"; + return false; + } + + auto* pkt = (IwxRxPacket*)g_iwx.CmdRespBuf; + auto* rsp = (IwxNvmGetInfoRsp*)pkt->data; + + SetMacAddrFromCsr(); + + g_iwx.Nvm.NvmVersion = rsp->general.nvm_version; + + uint32_t mf = rsp->mac_sku.mac_sku_flags; + g_iwx.Nvm.Sku24GHz = (mf & IWX_NVM_MAC_SKU_FLAGS_BAND_2_4_ENABLED) != 0; + g_iwx.Nvm.Sku52GHz = (mf & IWX_NVM_MAC_SKU_FLAGS_BAND_5_2_ENABLED) != 0; + g_iwx.Nvm.Sku11n = (mf & IWX_NVM_MAC_SKU_FLAGS_802_11N_ENABLED) != 0; + g_iwx.Nvm.Sku11ac = (mf & IWX_NVM_MAC_SKU_FLAGS_802_11AC_ENABLED) != 0; + g_iwx.Nvm.Sku11ax = (mf & IWX_NVM_MAC_SKU_FLAGS_802_11AX_ENABLED) != 0; + + g_iwx.Nvm.ValidTxAnt = (uint8_t)rsp->phy_sku.tx_chains; + g_iwx.Nvm.ValidRxAnt = (uint8_t)rsp->phy_sku.rx_chains; + + if (v4) { + g_iwx.Nvm.LarEnabled = rsp->regulatory.lar_enabled != 0 + && IwxBitSet(g_iwx.Fw.Capa, IWX_UCODE_TLV_CAPA_LAR_SUPPORT); + IwxInitChannelMap(nullptr, rsp->regulatory.channel_profile, + IWX_NUM_CHANNELS); + } else { + auto* v3 = (IwxNvmGetInfoRspV3*)pkt->data; + g_iwx.Nvm.LarEnabled = v3->regulatory.lar_enabled != 0 + && IwxBitSet(g_iwx.Fw.Capa, IWX_UCODE_TLV_CAPA_LAR_SUPPORT); + IwxInitChannelMap(v3->regulatory.channel_profile, nullptr, + IWX_NUM_CHANNELS_V1); + } + + const uint8_t* m = g_iwx.Nvm.HwAddr; + KernelLogStream(OK, "WiFi") << "MAC address " << base::hex + << (uint64_t)m[0] << ":" << (uint64_t)m[1] << ":" << (uint64_t)m[2] + << ":" << (uint64_t)m[3] << ":" << (uint64_t)m[4] << ":" + << (uint64_t)m[5] << base::dec + << " (bands:" << (g_iwx.Nvm.Sku24GHz ? " 2.4GHz" : "") + << (g_iwx.Nvm.Sku52GHz ? " 5GHz" : "") << ")"; + return true; + } + + // ========================================================================= + // Init sequence + // ========================================================================= + + static bool SendTxAntCfg() { + IwxTxAntCfgCmd cmd = {}; + cmd.valid = FwValidTxAnt(); + return IwxSendCmdPdu(IWX_TX_ANT_CONFIGURATION_CMD, &cmd, sizeof(cmd)); + } + + static bool SendBtInitConf() { + IwxBtCoexCmd cmd = {}; + cmd.mode = IWX_BT_COEX_WIFI; + cmd.enabled_modules = 0; + return IwxSendCmdPdu(IWX_BT_CONFIG, &cmd, sizeof(cmd)); + } + + static bool SendSocConf() { + IwxSocConfigurationCmd cmd = {}; + // The AX211 here is an integrated (CNVi) part with a low-latency + // crystal, matching iwlwifi's "so with low latency xtal" profile. + uint32_t flags = IWX_SOC_FLAGS_LTR_APPLY_DELAY_2500 & 0xc; + int scanVer = IwxLookupCmdVer(IWX_LONG_GROUP, IWX_SCAN_REQ_UMAC); + if (scanVer >= 2) flags |= IWX_SOC_CONFIG_CMD_FLAGS_LOW_LATENCY; + cmd.flags = flags; + cmd.latency = 12000; + return IwxSendCmdPdu(IWX_WIDE_ID(IWX_SYSTEM_GROUP, IWX_SOC_CONFIGURATION_CMD), + &cmd, sizeof(cmd)); + } + + static bool SendLtrConfig() { + // Only meaningful when the PCIe link advertises LTR; upstream skips + // the command entirely otherwise. + if (!g_iwx.LtrEnabled) return true; + IwxLtrConfigCmd cmd = {}; + cmd.flags = IWX_LTR_CFG_FLAG_FEATURE_ENABLE; + return IwxSendCmdPdu(IWX_LTR_CONFIG, &cmd, sizeof(cmd)); + } + + static bool SendTempReportThs() { + IwxTempReportThsCmd cmd = {}; + return IwxSendCmdPdu( + IWX_WIDE_ID(IWX_PHY_OPS_GROUP, IWX_TEMP_REPORTING_THRESHOLDS_CMD), + &cmd, sizeof(cmd)); + } + + static bool DisableBeaconFilter() { + IwxBeaconFilterCmd cmd = {}; + return IwxSendCmdPdu(IWX_REPLY_BEACON_FILTERING_CMD, &cmd, sizeof(cmd)); + } + + static bool SendUpdateMcc(const char* alpha2) { + IwxMccUpdateCmd cmd = {}; + cmd.mcc = (uint16_t)((alpha2[0] << 8) | alpha2[1]); + if (IwxBitSet(g_iwx.Fw.ApiFlags, IWX_UCODE_TLV_API_WIFI_MCC_UPDATE) + || IwxBitSet(g_iwx.Fw.Capa, IWX_UCODE_TLV_CAPA_LAR_MULTI_MCC)) + cmd.source_id = IWX_MCC_SOURCE_GET_CURRENT; + else + cmd.source_id = IWX_MCC_SOURCE_OLD_FW; + + IwxHostCmd hcmd; + hcmd.Id = IWX_MCC_UPDATE_CMD; + hcmd.Data = &cmd; + hcmd.Len = sizeof(cmd); + hcmd.WantResp = true; + if (!IwxSendCmd(hcmd)) return false; + + if (g_iwx.CmdRespLen < sizeof(IwxRxPacket) + sizeof(IwxMccUpdateRespV4)) + return false; + + auto* pkt = (IwxRxPacket*)g_iwx.CmdRespBuf; + auto* rsp = (IwxMccUpdateRespV4*)pkt->data; + + // Refresh the channel map from the regulatory profile the firmware + // just applied; the response is variable length, so bound the channel + // count by what actually arrived. + uint32_t payload = IwxRxPacketPayloadLen(pkt); + uint32_t maxCh = (payload - sizeof(IwxMccUpdateRespV4)) / sizeof(uint32_t); + uint32_t n = rsp->n_channels < maxCh ? rsp->n_channels : maxCh; + if (n > 0) IwxInitChannelMap(nullptr, rsp->channels, (int)n); + return true; + } + + // Reduced scan configuration (SCAN_CFG_CMD v5+): only antenna masks. + static bool ConfigUmacScan() { + if (!IwxBitSet(g_iwx.Fw.ApiFlags, IWX_UCODE_TLV_API_REDUCED_SCAN_CONFIG)) { + KernelLogStream(WARNING, "WiFi") + << "Firmware lacks reduced scan config support"; + return false; + } + + IwxScanConfig cfg = {}; + int cmdVer = IwxLookupCmdVer(IWX_LONG_GROUP, IWX_SCAN_CFG_CMD); + if (cmdVer < 5) cfg.bcast_sta_id = 0xff; // deprecated from v5 on + cfg.tx_chains = FwValidTxAnt(); + cfg.rx_chains = FwValidRxAnt(); + + return IwxSendCmdPdu(IWX_WIDE_ID(IWX_LONG_GROUP, IWX_SCAN_CFG_CMD), + &cfg, sizeof(cfg)); + } + + bool IwxRunInitUcode() { + g_iwx.InitComplete = 0; + + if (!IwxStartFirmware()) return false; + if (!IwxLoadPnvm()) { + // Not fatal: without PNVM the firmware falls back to conservative + // built-in regulatory limits, which still allows scanning. + KernelLogStream(WARNING, "WiFi") << "Continuing without PNVM data"; + } + + IwxInitExtendedCfgCmd initCfg = {}; + initCfg.init_flags = IWX_INIT_NVM; + if (!IwxSendCmdPdu(IWX_WIDE_ID(IWX_SYSTEM_GROUP, IWX_INIT_EXTENDED_CFG_CMD), + &initCfg, sizeof(initCfg))) { + KernelLogStream(ERROR, "WiFi") << "INIT_EXTENDED_CFG failed"; + return false; + } + + IwxNvmAccessCompleteCmd nvmDone = {}; + if (!IwxSendCmdPdu(IWX_WIDE_ID(IWX_REGULATORY_AND_NVM_GROUP, + IWX_NVM_ACCESS_COMPLETE), + &nvmDone, sizeof(nvmDone))) { + KernelLogStream(ERROR, "WiFi") << "NVM_ACCESS_COMPLETE failed"; + return false; + } + + uint64_t start = Timekeeping::GetMilliseconds(); + while (!(g_iwx.InitComplete & 0x1) + && Timekeeping::GetMilliseconds() - start < 2000) { + IwxProcessEvents(); + IwxDelayUs(200); + } + if (!(g_iwx.InitComplete & 0x1)) { + KernelLogStream(ERROR, "WiFi") << "No INIT_COMPLETE notification"; + return false; + } + + if (!IwxNvmGet()) { + KernelLogStream(ERROR, "WiFi") << "Failed to read NVM"; + return false; + } + return true; + } + + // Name each configuration step in the log so a firmware assert can be + // attributed to the command that preceded it without a rebuild. + static void InitStep(const char* what) { + KernelLogStream(INFO, "WiFi") << " init: " << what; + } + + bool IwxInitHw() { + InitStep("tx antenna config"); + if (!SendTxAntCfg()) { + KernelLogStream(ERROR, "WiFi") << "TX antenna config failed"; + return false; + } + InitStep("bt coex"); + if (!SendBtInitConf()) + KernelLogStream(WARNING, "WiFi") << "BT coex config failed"; + InitStep("soc config"); + if (!SendSocConf()) + KernelLogStream(WARNING, "WiFi") << "SoC config failed"; + InitStep("pcie ltr"); + if (!SendLtrConfig()) + KernelLogStream(WARNING, "WiFi") << "PCIe LTR config failed"; + InitStep("temp thresholds"); + if (IwxBitSet(g_iwx.Fw.Capa, IWX_UCODE_TLV_CAPA_CT_KILL_BY_FW)) { + if (!SendTempReportThs()) + KernelLogStream(WARNING, "WiFi") << "Temperature threshold config failed"; + } + InitStep("regulatory (mcc)"); + if (g_iwx.Nvm.LarEnabled) { + // "ZZ" selects the NVM's own default regulatory profile. + if (!SendUpdateMcc("ZZ")) + KernelLogStream(WARNING, "WiFi") << "Regulatory (MCC) update failed"; + } + InitStep("scan config"); + if (!ConfigUmacScan()) { + KernelLogStream(ERROR, "WiFi") << "Scan configuration failed"; + return false; + } + InitStep("beacon filter"); + if (!DisableBeaconFilter()) + KernelLogStream(WARNING, "WiFi") << "Could not disable beacon filter"; + + return true; + } + + // ========================================================================= + // Probe request template + // ========================================================================= + + static uint8_t* AddRates(uint8_t* frm, const uint8_t* rates, int n, uint8_t id) { + *frm++ = id; + *frm++ = (uint8_t)n; + for (int i = 0; i < n; i++) *frm++ = rates[i]; + return frm; + } + + // Build the probe-request template the firmware transmits during active + // scanning. The firmware inserts the SSID itself (directed scan) and fills + // in duration/sequence, so the SSID element is left empty here. + static void FillProbeReq(IwxScanProbeReq* preq) { + memset(preq, 0, sizeof(*preq)); + + uint8_t* buf = preq->buf; + uint8_t* frm = buf; + + // 802.11 management header: probe request, to broadcast. + *frm++ = 0x40; // frame control: mgmt / probe request + *frm++ = 0x00; + *frm++ = 0x00; *frm++ = 0x00; // duration (hw) + for (int i = 0; i < 6; i++) *frm++ = 0xff; // addr1 broadcast + for (int i = 0; i < 6; i++) *frm++ = g_iwx.Nvm.HwAddr[i]; // addr2 + for (int i = 0; i < 6; i++) *frm++ = 0xff; // addr3 broadcast + *frm++ = 0x00; *frm++ = 0x00; // seq ctl (hw) + + // Empty SSID element; hardware substitutes the directed SSID. + *frm++ = 0x00; // element id: SSID + *frm++ = 0x00; // length 0 + + preq->mac_header.offset = 0; + preq->mac_header.len = (uint16_t)(frm - buf); + + // 2.4 GHz band IEs: supported + extended rates, DS parameter set. + uint8_t* pos = frm; + preq->band_data[0].offset = (uint16_t)(frm - buf); + frm = AddRates(frm, RATES_11G, (int)sizeof(RATES_11G), 0x01); + frm = AddRates(frm, RATES_11G_EXT, (int)sizeof(RATES_11G_EXT), 0x32); + if (IwxBitSet(g_iwx.Fw.Capa, IWX_UCODE_TLV_CAPA_DS_PARAM_SET_IE_SUPPORT)) { + *frm++ = 0x03; // element id: DS parameter set + *frm++ = 0x01; + *frm++ = 0x00; // channel filled in by firmware + } + preq->band_data[0].len = (uint16_t)(frm - pos); + + // 5 GHz band IEs. + if (g_iwx.Nvm.Sku52GHz) { + pos = frm; + preq->band_data[1].offset = (uint16_t)(frm - buf); + frm = AddRates(frm, RATES_11A, (int)sizeof(RATES_11A), 0x01); + preq->band_data[1].len = (uint16_t)(frm - pos); + } + + // Common (both bands) trailer: nothing extra is advertised, HT/VHT + // capabilities are only needed once we associate. + preq->common_data.offset = (uint16_t)(frm - buf); + preq->common_data.len = 0; + } + + // ========================================================================= + // Scan + // ========================================================================= + + static uint16_t ScanFlagsV2(bool haveSsid) { + uint16_t flags = IWX_UMAC_SCAN_GEN_FLAGS_V2_PASS_ALL + | IWX_UMAC_SCAN_GEN_FLAGS_V2_NTFY_ITER_COMPLETE + | IWX_UMAC_SCAN_GEN_FLAGS_V2_ADAPTIVE_DWELL; + // Without a target SSID there is nothing to put in a probe request, so + // a passive sweep (listening for beacons) is what the firmware runs. + if (!haveSsid) flags |= IWX_UMAC_SCAN_GEN_FLAGS_V2_FORCE_PASSIVE; + return flags; + } + + static void FillGeneralParams(IwxScanGeneralParamsV10* gp, uint16_t flags) { + gp->flags = flags; + gp->scan_start_mac_id = 0; + gp->adwell_default_social_chn = IWX_SCAN_ADWELL_DEFAULT_N_APS_SOCIAL; + gp->adwell_default_2g = IWX_SCAN_ADWELL_DEFAULT_LB_N_APS; + gp->adwell_default_5g = IWX_SCAN_ADWELL_DEFAULT_HB_N_APS; + gp->adwell_max_budget = IWX_SCAN_ADWELL_MAX_BUDGET_FULL_SCAN; + gp->scan_priority = IWX_SCAN_PRIORITY_EXT_6; + gp->max_out_of_time[IWX_SCAN_LB_LMAC_IDX] = 0; + gp->suspend_time[IWX_SCAN_LB_LMAC_IDX] = 0; + gp->max_out_of_time[IWX_SCAN_HB_LMAC_IDX] = 0; + gp->suspend_time[IWX_SCAN_HB_LMAC_IDX] = 0; + gp->active_dwell[IWX_SCAN_LB_LMAC_IDX] = IWX_SCAN_DWELL_ACTIVE; + gp->passive_dwell[IWX_SCAN_LB_LMAC_IDX] = IWX_SCAN_DWELL_PASSIVE; + gp->active_dwell[IWX_SCAN_HB_LMAC_IDX] = IWX_SCAN_DWELL_ACTIVE; + gp->passive_dwell[IWX_SCAN_HB_LMAC_IDX] = IWX_SCAN_DWELL_PASSIVE; + } + + static uint8_t FillChannelsV5(IwxScanChannelCfgUmacV5* chans, uint32_t cfgFlags) { + uint8_t n = 0; + for (int i = 0; i < g_iwx.ChannelCount + && n < g_iwx.Fw.NumScanChannels + && n < IWX_MAX_SCAN_CHANNELS; i++) { + const IwxChannel& ch = g_iwx.Channels[i]; + if (!ch.Valid) continue; + chans[n].channel_num = ch.ChannelNum; + chans[n].psd_20 = 0x80; // -128: "unknown" power spectral density + chans[n].iter_count = 1; + chans[n].iter_interval = 0; + uint32_t band = ch.Is5GHz ? IWX_PHY_BAND_5 : IWX_PHY_BAND_24; + chans[n].flags = cfgFlags | (band << IWX_CHAN_CFG_FLAGS_BAND_POS); + n++; + } + return n; + } + + static uint8_t FillChannels(IwxScanChannelCfgUmac* chans, uint32_t cfgFlags) { + uint8_t n = 0; + for (int i = 0; i < g_iwx.ChannelCount + && n < g_iwx.Fw.NumScanChannels + && n < IWX_MAX_SCAN_CHANNELS; i++) { + const IwxChannel& ch = g_iwx.Channels[i]; + if (!ch.Valid) continue; + chans[n].channel_num = ch.ChannelNum; + chans[n].band = ch.Is5GHz ? IWX_PHY_BAND_5 : IWX_PHY_BAND_24; + chans[n].iter_count = 1; + chans[n].iter_interval = 0; + chans[n].flags = cfgFlags; + n++; + } + return n; + } + + // Build and submit the scan request. The command is ~2 KB, well past the + // per-slot command area, so it is staged through the command ring's bounce + // page by IwxSendCmd. + static bool UmacScanV17() { + auto* cmd = (IwxScanReqUmacV17*)Memory::g_heap->Request(sizeof(IwxScanReqUmacV17)); + if (!cmd) return false; + memset(cmd, 0, sizeof(*cmd)); + + cmd->ooc_priority = IWX_SCAN_PRIORITY_EXT_6; + cmd->uid = 0; + + uint32_t bitmapSsid = 0; + FillGeneralParams(&cmd->general_params, ScanFlagsV2(g_directSsidLen != 0)); + + cmd->periodic_params.schedule[0].interval = 0; + cmd->periodic_params.schedule[0].iter_count = 1; + + FillProbeReq(&cmd->probe_params.preq); + + if (g_directSsidLen) { + cmd->probe_params.direct_scan[0].id = 0x00; // SSID element + cmd->probe_params.direct_scan[0].len = g_directSsidLen; + memcpy(cmd->probe_params.direct_scan[0].ssid, g_directSsid, + g_directSsidLen); + bitmapSsid |= (1 << 0); + } + + cmd->channel_params.flags = IWX_SCAN_CHANNEL_FLAG_ENABLE_CHAN_ORDER; + cmd->channel_params.count = FillChannelsV5(cmd->channel_params.channel_config, + bitmapSsid); + cmd->channel_params.n_aps_override[0] = IWX_SCAN_ADWELL_N_APS_GO_FRIENDLY; + cmd->channel_params.n_aps_override[1] = IWX_SCAN_ADWELL_N_APS_SOCIAL_CHS; + + bool ok = IwxSendCmdPdu(IWX_WIDE_ID(IWX_LONG_GROUP, IWX_SCAN_REQ_UMAC), + cmd, sizeof(*cmd)); + Memory::g_heap->Free(cmd); + return ok; + } + + static bool UmacScanV14() { + auto* cmd = (IwxScanReqUmacV14*)Memory::g_heap->Request(sizeof(IwxScanReqUmacV14)); + if (!cmd) return false; + memset(cmd, 0, sizeof(*cmd)); + + cmd->ooc_priority = IWX_SCAN_PRIORITY_EXT_6; + cmd->uid = 0; + + uint32_t bitmapSsid = 0; + FillGeneralParams(&cmd->general_params, ScanFlagsV2(g_directSsidLen != 0)); + + cmd->periodic_params.schedule[0].interval = 0; + cmd->periodic_params.schedule[0].iter_count = 1; + + FillProbeReq(&cmd->probe_params.preq); + + if (g_directSsidLen) { + cmd->probe_params.direct_scan[0].id = 0x00; + cmd->probe_params.direct_scan[0].len = g_directSsidLen; + memcpy(cmd->probe_params.direct_scan[0].ssid, g_directSsid, + g_directSsidLen); + bitmapSsid |= (1 << 0); + } + + cmd->channel_params.flags = IWX_SCAN_CHANNEL_FLAG_ENABLE_CHAN_ORDER; + cmd->channel_params.count = FillChannels(cmd->channel_params.channel_config, + bitmapSsid); + cmd->channel_params.n_aps_override[0] = IWX_SCAN_ADWELL_N_APS_GO_FRIENDLY; + cmd->channel_params.n_aps_override[1] = IWX_SCAN_ADWELL_N_APS_SOCIAL_CHS; + + bool ok = IwxSendCmdPdu(IWX_WIDE_ID(IWX_LONG_GROUP, IWX_SCAN_REQ_UMAC), + cmd, sizeof(*cmd)); + Memory::g_heap->Free(cmd); + return ok; + } + + bool IwxStartScan(const char* directSsid) { + if (g_iwx.State != IwxFwState::Running) return false; + if (g_iwx.ScanActive) return false; + + g_directSsidLen = 0; + g_directSsid[0] = '\0'; + if (directSsid) { + while (g_directSsidLen < 32 && directSsid[g_directSsidLen]) { + g_directSsid[g_directSsidLen] = directSsid[g_directSsidLen]; + g_directSsidLen++; + } + g_directSsid[g_directSsidLen] = '\0'; + } + + g_iwx.ScanCompleted = false; + g_iwx.ScanActive = true; + + int ver = IwxLookupCmdVer(IWX_LONG_GROUP, IWX_SCAN_REQ_UMAC); + bool ok = (ver >= 17) ? UmacScanV17() : UmacScanV14(); + + if (!ok) { + g_iwx.ScanActive = false; + KernelLogStream(ERROR, "WiFi") << "Could not initiate scan"; + } + return ok; + } + + bool IwxAbortScan() { + if (!g_iwx.ScanActive) return true; + IwxUmacScanAbort cmd = {}; + bool ok = IwxSendCmdPdu(IWX_WIDE_ID(IWX_LONG_GROUP, IWX_SCAN_ABORT_UMAC), + &cmd, sizeof(cmd)); + if (ok) g_iwx.ScanActive = false; + return ok; + } + + // ========================================================================= + // Notification dispatch + // ========================================================================= + + static void HandleRxMpdu(const IwxRxPacket* pkt, uint32_t bufLen) { + uint32_t payload = IwxRxPacketPayloadLen(pkt); + if (payload < sizeof(IwxRxMpduDesc)) return; + + auto* desc = (const IwxRxMpduDesc*)pkt->data; + + if (!(desc->status & IWX_RX_MPDU_RES_STATUS_CRC_OK) + || !(desc->status & IWX_RX_MPDU_RES_STATUS_OVERRUN_OK)) + return; + + uint32_t len = desc->mpdu_len; + if (len < 24) return; // shorter than a MAC header + + const uint8_t* frame = pkt->data + sizeof(IwxRxMpduDesc); + uint32_t offset = (uint32_t)(frame - (const uint8_t*)pkt); + if (offset + len > bufLen) return; + + // The firmware pads the header to a 4-byte boundary when the flag is + // set; the payload then starts two bytes later. + if (desc->mac_flags2 & IWX_RX_MPDU_MFLG2_PAD) { + if (len < 2) return; + frame += 2; + if (offset + 2 + len > bufLen) return; + } + + int energyA = desc->v3.energy_a ? -(int)desc->v3.energy_a : -256; + int energyB = desc->v3.energy_b ? -(int)desc->v3.energy_b : -256; + int rssi = energyA > energyB ? energyA : energyB; + if (rssi < -128) rssi = -128; + + WifiRxMgmtFrame(frame, len, desc->v3.channel, (int8_t)rssi); + } + + void IwxHandleNotification(const IwxRxPacket* pkt, const uint8_t* rxBuf, + uint32_t bufLen) { + (void)rxBuf; + uint32_t code = ((uint32_t)pkt->hdr.flags << 8) | pkt->hdr.code; + if (IwxCmdGroupId(code) == IWX_LONG_GROUP) + code = IwxCmdOpcode(code); + + switch (code) { + case IWX_REPLY_RX_MPDU_CMD: + HandleRxMpdu(pkt, bufLen); + break; + + case IWX_SCAN_COMPLETE_UMAC: + case IWX_SCAN_ITERATION_COMPLETE_UMAC: + g_iwx.ScanActive = false; + g_iwx.ScanCompleted = true; + break; + + case IWX_MCC_CHUB_UPDATE_CMD: + // Firmware-initiated regulatory change; the channel list is + // refreshed on the next explicit MCC update. + break; + + case IWX_WIDE_ID(IWX_PHY_OPS_GROUP, IWX_CT_KILL_NOTIFICATION): + KernelLogStream(ERROR, "WiFi") + << "Device at critical temperature; stopping radio"; + g_iwx.State = IwxFwState::Error; + break; + + default: + // Command acks, statistics and everything else the driver does + // not act on. The transport still uses them to complete the + // in-flight synchronous command. + break; + } + } +} diff --git a/kernel/src/Drivers/Net/Wifi/IwxReg.hpp b/kernel/src/Drivers/Net/Wifi/IwxReg.hpp new file mode 100644 index 0000000..c522179 --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/IwxReg.hpp @@ -0,0 +1,1221 @@ +/* + * IwxReg.hpp + * Intel Wi-Fi (AX210/AX211 "So" family) register and firmware API definitions. + * + * Register offsets, firmware command IDs and wire-format structures follow + * Intel's dual BSD/GPLv2-licensed iwlwifi API headers, as consolidated in + * OpenBSD's if_iwxreg.h (ISC/BSD). Only the subset used by this driver is + * kept. See 0:/os/licenses/NOTICES.txt for the full attribution. + * + * Copyright (c) 2026 Daniel Hammer + * Portions Copyright (c) 2017 Intel Deutschland GmbH + * Portions Copyright (c) 2018-2019 Intel Corporation (BSD license) +*/ + +#pragma once +#include + +namespace Drivers::Net::Wifi { + +// ============================================================================= +// CSR (control and status registers, BAR0-mapped) +// ============================================================================= + +constexpr uint32_t IWX_CSR_HW_IF_CONFIG_REG = 0x000; +constexpr uint32_t IWX_CSR_INT_COALESCING = 0x004; +constexpr uint32_t IWX_CSR_INT = 0x008; +constexpr uint32_t IWX_CSR_INT_MASK = 0x00c; +constexpr uint32_t IWX_CSR_FH_INT_STATUS = 0x010; +constexpr uint32_t IWX_CSR_RESET = 0x020; +constexpr uint32_t IWX_CSR_GP_CNTRL = 0x024; +constexpr uint32_t IWX_CSR_HW_REV = 0x028; +constexpr uint32_t IWX_CSR_GIO_REG = 0x03C; +constexpr uint32_t IWX_CSR_UCODE_DRV_GP1_CLR = 0x05c; +constexpr uint32_t IWX_CSR_MBOX_SET_REG = 0x088; +constexpr uint32_t IWX_CSR_HW_RF_ID = 0x09c; +constexpr uint32_t IWX_CSR_MAC_SHADOW_REG_CTRL = 0x0A8; +constexpr uint32_t IWX_CSR_LTR_LONG_VAL_AD = 0x0d4; +constexpr uint32_t IWX_CSR_GIO_CHICKEN_BITS = 0x100; +constexpr uint32_t IWX_CSR_CTXT_INFO_BOOT_CTRL = 0x0; +constexpr uint32_t IWX_CSR_CTXT_INFO_ADDR = 0x118; +constexpr uint32_t IWX_CSR_IML_DATA_ADDR = 0x120; +constexpr uint32_t IWX_CSR_IML_SIZE_ADDR = 0x128; +constexpr uint32_t IWX_CSR_DBG_HPET_MEM_REG = 0x240; +constexpr uint32_t IWX_CSR_DBG_LINK_PWR_MGMT_REG = 0x250; + +constexpr uint32_t IWX_CSR_MBOX_SET_REG_OS_ALIVE = 0x20; +constexpr uint32_t IWX_CSR_AUTO_FUNC_BOOT_ENA = 1u << 1; + +// IWX_CSR_HW_IF_CONFIG_REG bits +constexpr uint32_t IWX_CSR_HW_IF_CONFIG_REG_BIT_HAP_WAKE_L1A = 0x00080000; +constexpr uint32_t IWX_CSR_HW_IF_CONFIG_REG_BIT_NIC_READY = 0x00400000; +constexpr uint32_t IWX_CSR_HW_IF_CONFIG_REG_PREPARE = 0x08000000; +constexpr uint32_t IWX_CSR_HW_IF_CONFIG_REG_ENABLE_PME = 0x10000000; + +// IWX_CSR_RESET bits +constexpr uint32_t IWX_CSR_RESET_REG_FLAG_SW_RESET = 0x00000080; +constexpr uint32_t IWX_CSR_RESET_REG_FLAG_MASTER_DISABLED = 0x00000100; +constexpr uint32_t IWX_CSR_RESET_REG_FLAG_STOP_MASTER = 0x00000200; +constexpr uint32_t IWX_CSR_RESET_LINK_PWR_MGMT_DISABLED = 0x80000000; + +// IWX_CSR_GP_CNTRL bits +constexpr uint32_t IWX_CSR_GP_CNTRL_REG_FLAG_MAC_CLOCK_READY = 0x00000001; +constexpr uint32_t IWX_CSR_GP_CNTRL_REG_FLAG_INIT_DONE = 0x00000004; +constexpr uint32_t IWX_CSR_GP_CNTRL_REG_FLAG_MAC_ACCESS_REQ = 0x00000008; +constexpr uint32_t IWX_CSR_GP_CNTRL_REG_FLAG_GOING_TO_SLEEP = 0x00000010; +constexpr uint32_t IWX_CSR_GP_CNTRL_REG_FLAG_RFKILL_WAKE_L1A_EN = 0x04000000; +constexpr uint32_t IWX_CSR_GP_CNTRL_REG_FLAG_HW_RF_KILL_SW = 0x08000000; + +// HW REV / RF ID decoding +constexpr uint32_t IWX_CSR_HW_REV_TYPE(uint32_t v) { return (v & 0x000FFF0) >> 4; } +constexpr uint32_t IWX_CSR_HW_RFID_TYPE(uint32_t v) { return (v & 0x0FFF000) >> 12; } +constexpr uint32_t IWX_CSR_HW_RFID_IS_CDB(uint32_t v){ return (v & 0x10000000) >> 28; } +constexpr uint32_t IWX_CFG_RF_TYPE_GF = 0x10D; +constexpr uint32_t IWX_CFG_RF_TYPE_HR2 = 0x10A; +constexpr uint32_t IWX_CSR_HW_REV_TYPE_SO = 0x0000037; // after TYPE() extraction + +constexpr uint32_t IWX_CSR_GIO_REG_VAL_L0S_DISABLED = 0x00000002; +constexpr uint32_t IWX_CSR_GIO_CHICKEN_BITS_REG_BIT_L1A_NO_L0S_RX = 0x00800000; +constexpr uint32_t IWX_CSR_DBG_HPET_MEM_REG_VAL = 0xFFFF0000; +constexpr uint32_t IWX_CSR_UCODE_SW_BIT_RFKILL = 0x00000002; +constexpr uint32_t IWX_CSR_UCODE_DRV_GP1_BIT_CMD_BLOCKED = 0x00000004; + +// LTR +constexpr uint32_t IWX_CSR_LTR_LONG_VAL_AD_NO_SNOOP_REQ = 0x80000000; +constexpr uint32_t IWX_CSR_LTR_LONG_VAL_AD_NO_SNOOP_SCALE_MASK = 0x1c000000; +constexpr uint32_t IWX_CSR_LTR_LONG_VAL_AD_NO_SNOOP_SCALE_SHIFT = 24; +constexpr uint32_t IWX_CSR_LTR_LONG_VAL_AD_NO_SNOOP_VAL_MASK = 0x03ff0000; +constexpr uint32_t IWX_CSR_LTR_LONG_VAL_AD_NO_SNOOP_VAL_SHIFT = 16; +constexpr uint32_t IWX_CSR_LTR_LONG_VAL_AD_SNOOP_REQ = 0x00008000; +constexpr uint32_t IWX_CSR_LTR_LONG_VAL_AD_SNOOP_SCALE_MASK = 0x00001c00; +constexpr uint32_t IWX_CSR_LTR_LONG_VAL_AD_SNOOP_SCALE_SHIFT = 8; +constexpr uint32_t IWX_CSR_LTR_LONG_VAL_AD_SNOOP_VAL = 0x000003ff; +constexpr uint32_t IWX_CSR_LTR_LONG_VAL_AD_SCALE_USEC = 2; + +// MAC address straps (AX210: base 0x380) +constexpr uint32_t IWX_CSR_MAC_ADDR_BASE = 0x380; +constexpr uint32_t IWX_CSR_MAC_ADDR0_OTP = IWX_CSR_MAC_ADDR_BASE + 0x00; +constexpr uint32_t IWX_CSR_MAC_ADDR1_OTP = IWX_CSR_MAC_ADDR_BASE + 0x04; +constexpr uint32_t IWX_CSR_MAC_ADDR0_STRAP = IWX_CSR_MAC_ADDR_BASE + 0x08; +constexpr uint32_t IWX_CSR_MAC_ADDR1_STRAP = IWX_CSR_MAC_ADDR_BASE + 0x0c; + +// Host interrupt coalescing default (32-usec units) +constexpr uint8_t IWX_HOST_INT_TIMEOUT_DEF = 0x40; + +// ============================================================================= +// HBUS (indirect access to device-internal registers/memory) +// ============================================================================= + +constexpr uint32_t IWX_HBUS_BASE = 0x400; +// Device-internal SRAM window: write the address, then read data words back +// (the address auto-increments). Used to pull the firmware's error tables. +constexpr uint32_t IWX_HBUS_TARG_MEM_RADDR = IWX_HBUS_BASE + 0x00c; +constexpr uint32_t IWX_HBUS_TARG_MEM_RDAT = IWX_HBUS_BASE + 0x01c; +constexpr uint32_t IWX_HBUS_TARG_PRPH_WADDR = IWX_HBUS_BASE + 0x044; +constexpr uint32_t IWX_HBUS_TARG_PRPH_RADDR = IWX_HBUS_BASE + 0x048; +constexpr uint32_t IWX_HBUS_TARG_PRPH_WDAT = IWX_HBUS_BASE + 0x04c; +constexpr uint32_t IWX_HBUS_TARG_PRPH_RDAT = IWX_HBUS_BASE + 0x050; +constexpr uint32_t IWX_HBUS_TARG_WRPTR = IWX_HBUS_BASE + 0x060; + +// RX free-ring write index shadow register (AX210, direct CSR) +constexpr uint32_t IWX_RFH_Q0_FRBDCB_WIDX_TRG = 0x1C80; + +// ============================================================================= +// PRPH / UMAC-PRPH registers (via HBUS indirect access) +// ============================================================================= + +// AX210: UMAC PRPH registers are offset by this amount. +constexpr uint32_t IWX_UMAC_PRPH_OFFSET = 0x300000; + +constexpr uint32_t IWX_UREG_CHICK = 0xa05c00; +constexpr uint32_t IWX_UREG_CHICK_MSI_ENABLE = 1u << 24; +constexpr uint32_t IWX_UREG_CHICK_MSIX_ENABLE = 1u << 25; +constexpr uint32_t IWX_UREG_CPU_INIT_RUN = 0xa05c44; +constexpr uint32_t IWX_UREG_DOORBELL_TO_ISR6 = 0xa05c04; +constexpr uint32_t IWX_UREG_DOORBELL_TO_ISR6_PNVM = 1u << 20; +constexpr uint32_t IWX_WFPM_CTRL_REG = 0xa03030; +constexpr uint32_t IWX_WFPM_AUX_CTL_AUX_IF_MAC_OWNER_MSK = 0x08000000; +constexpr uint32_t IWX_SD_REG_VER_GEN2 = 0xa2b800; +constexpr uint32_t IWX_CNVI_AUX_MISC_CHIP = 0xA200B0; +constexpr uint32_t IWX_RFH_RXF_DMA_CFG_GEN3 = 0xA07880; +constexpr uint32_t IWX_RFH_GEN_STATUS_GEN3 = 0xA07824; +constexpr uint32_t IWX_RXF_DMA_IDLE = 1u << 31; + +// ============================================================================= +// MSI-X cause handling registers +// ============================================================================= + +constexpr uint32_t IWX_CSR_MSIX_BASE = 0x2000; +constexpr uint32_t IWX_CSR_MSIX_FH_INT_CAUSES_AD = IWX_CSR_MSIX_BASE + 0x800; +constexpr uint32_t IWX_CSR_MSIX_FH_INT_MASK_AD = IWX_CSR_MSIX_BASE + 0x804; +constexpr uint32_t IWX_CSR_MSIX_HW_INT_CAUSES_AD = IWX_CSR_MSIX_BASE + 0x808; +constexpr uint32_t IWX_CSR_MSIX_HW_INT_MASK_AD = IWX_CSR_MSIX_BASE + 0x80C; +constexpr uint32_t IWX_CSR_MSIX_AUTOMASK_ST_AD = IWX_CSR_MSIX_BASE + 0x810; +constexpr uint32_t IWX_CSR_MSIX_RX_IVAR_AD_REG = IWX_CSR_MSIX_BASE + 0x880; +constexpr uint32_t IWX_CSR_MSIX_IVAR_AD_REG = IWX_CSR_MSIX_BASE + 0x890; + +constexpr uint32_t IWX_CSR_MSIX_RX_IVAR(uint32_t cause) { return IWX_CSR_MSIX_RX_IVAR_AD_REG + cause; } +constexpr uint32_t IWX_CSR_MSIX_IVAR(uint32_t cause) { return IWX_CSR_MSIX_IVAR_AD_REG + cause; } + +// FH cause bits +constexpr uint32_t IWX_MSIX_FH_INT_CAUSES_Q0 = 1u << 0; +constexpr uint32_t IWX_MSIX_FH_INT_CAUSES_Q1 = 1u << 1; +constexpr uint32_t IWX_MSIX_FH_INT_CAUSES_D2S_CH0_NUM = 1u << 16; +constexpr uint32_t IWX_MSIX_FH_INT_CAUSES_D2S_CH1_NUM = 1u << 17; +constexpr uint32_t IWX_MSIX_FH_INT_CAUSES_S2D = 1u << 19; +constexpr uint32_t IWX_MSIX_FH_INT_CAUSES_FH_ERR = 1u << 21; + +// HW cause bits +constexpr uint32_t IWX_MSIX_HW_INT_CAUSES_REG_ALIVE = 1u << 0; +constexpr uint32_t IWX_MSIX_HW_INT_CAUSES_REG_WAKEUP = 1u << 1; +constexpr uint32_t IWX_MSIX_HW_INT_CAUSES_REG_RESET_DONE = 1u << 2; +constexpr uint32_t IWX_MSIX_HW_INT_CAUSES_REG_TOP_FATAL_ERR = 1u << 3; +constexpr uint32_t IWX_MSIX_HW_INT_CAUSES_REG_SW_ERR_V2 = 1u << 5; +constexpr uint32_t IWX_MSIX_HW_INT_CAUSES_REG_CT_KILL = 1u << 6; +constexpr uint32_t IWX_MSIX_HW_INT_CAUSES_REG_RF_KILL = 1u << 7; +constexpr uint32_t IWX_MSIX_HW_INT_CAUSES_REG_PERIODIC = 1u << 8; +constexpr uint32_t IWX_MSIX_HW_INT_CAUSES_REG_SW_ERR = 1u << 25; +constexpr uint32_t IWX_MSIX_HW_INT_CAUSES_REG_SCD = 1u << 26; +constexpr uint32_t IWX_MSIX_HW_INT_CAUSES_REG_FH_TX = 1u << 27; +constexpr uint32_t IWX_MSIX_HW_INT_CAUSES_REG_HW_ERR = 1u << 29; +constexpr uint32_t IWX_MSIX_HW_INT_CAUSES_REG_HAP = 1u << 30; + +// IVAR cause indices +constexpr uint32_t IWX_MSIX_IVAR_CAUSE_D2S_CH0_NUM = 0x0; +constexpr uint32_t IWX_MSIX_IVAR_CAUSE_D2S_CH1_NUM = 0x1; +constexpr uint32_t IWX_MSIX_IVAR_CAUSE_S2D = 0x3; +constexpr uint32_t IWX_MSIX_IVAR_CAUSE_FH_ERR = 0x5; +constexpr uint32_t IWX_MSIX_IVAR_CAUSE_REG_ALIVE = 0x10; +constexpr uint32_t IWX_MSIX_IVAR_CAUSE_REG_WAKEUP = 0x11; +constexpr uint32_t IWX_MSIX_IVAR_CAUSE_REG_RESET_DONE = 0x12; +constexpr uint32_t IWX_MSIX_IVAR_CAUSE_REG_SW_ERR_V2 = 0x15; +constexpr uint32_t IWX_MSIX_IVAR_CAUSE_REG_CT_KILL = 0x16; +constexpr uint32_t IWX_MSIX_IVAR_CAUSE_REG_RF_KILL = 0x17; +constexpr uint32_t IWX_MSIX_IVAR_CAUSE_REG_PERIODIC = 0x18; +constexpr uint32_t IWX_MSIX_IVAR_CAUSE_REG_SW_ERR = 0x29; +constexpr uint32_t IWX_MSIX_IVAR_CAUSE_REG_SCD = 0x2a; +constexpr uint32_t IWX_MSIX_IVAR_CAUSE_REG_FH_TX = 0x2b; +constexpr uint32_t IWX_MSIX_IVAR_CAUSE_REG_HW_ERR = 0x2d; +constexpr uint32_t IWX_MSIX_IVAR_CAUSE_REG_HAP = 0x2e; + +constexpr uint8_t IWX_MSIX_NON_AUTO_CLEAR_CAUSE = 1 << 7; + +// ============================================================================= +// Rings and DMA structures (AX210 / context-info gen3) +// ============================================================================= + +constexpr uint32_t IWX_RX_MQ_RING_COUNT = 512; +constexpr uint32_t IWX_RBUF_SIZE = 4096; +constexpr uint32_t IWX_TX_RING_COUNT = 256; +constexpr uint32_t IWX_TFD_QUEUE_SIZE_MAX_GEN3 = 65536; +constexpr uint32_t IWX_TFD_QUEUE_BC_SIZE_GEN3_AX210 = 1024; +constexpr uint32_t IWX_TFH_NUM_TBS = 25; +constexpr uint32_t IWX_MAX_DRAM_ENTRY = 64; + +// log2 helper for ring size encodings (RX cb size = exponent, TFD = exponent-3) +constexpr uint32_t IwxLog2(uint32_t x) { + uint32_t r = 0; + while (x > 1) { x >>= 1; r++; } + return r; +} +constexpr uint32_t IWX_RX_QUEUE_CB_SIZE(uint32_t x) { return IwxLog2(x); } +constexpr uint32_t IWX_TFD_QUEUE_CB_SIZE(uint32_t x) { return IwxLog2(x) - 3; } + +struct IwxRxTransferDesc { // RX free-ring entry (AX210) + uint16_t rbid; + uint16_t reserved[3]; + uint64_t addr; +} __attribute__((packed)); + +struct IwxRxCompletionDesc { // RX used-ring entry (AX210) + uint32_t reserved1; + uint16_t rbid; + uint8_t flags; + uint8_t reserved2[25]; +} __attribute__((packed)); + +struct IwxTfhTb { + uint16_t tb_len; + uint64_t addr; +} __attribute__((packed)); + +struct IwxTfhTfd { // TX frame descriptor (gen2/gen3) + uint16_t num_tbs; + IwxTfhTb tbs[IWX_TFH_NUM_TBS]; + uint32_t pad; +} __attribute__((packed)); + +struct IwxGen3BcTblEntry { // TX byte-count table entry + uint16_t tfd_offset; // 0-13 byte count, 14-15 num 64B chunks +} __attribute__((packed)); + +// ============================================================================= +// Context info gen3 (firmware boot descriptors, AX210) +// ============================================================================= + +struct IwxContextInfoDram { + uint64_t umac_img[IWX_MAX_DRAM_ENTRY]; + uint64_t lmac_img[IWX_MAX_DRAM_ENTRY]; + uint64_t virtual_img[IWX_MAX_DRAM_ENTRY]; +} __attribute__((packed)); + +struct IwxPrphScratchVersion { + uint16_t mac_id; + uint16_t version; + uint16_t size; + uint16_t reserved; +} __attribute__((packed)); + +struct IwxPrphScratchControl { + uint32_t control_flags; + uint32_t reserved; +} __attribute__((packed)); + +constexpr uint32_t IWX_PRPH_SCRATCH_IMR_DEBUG_EN = 1u << 1; +constexpr uint32_t IWX_PRPH_SCRATCH_RB_SIZE_4K = 1u << 16; +constexpr uint32_t IWX_PRPH_SCRATCH_MTR_MODE = 1u << 17; +constexpr uint32_t IWX_PRPH_SCRATCH_MTR_FORMAT = (1u << 18) | (1u << 19); +constexpr uint32_t IWX_PRPH_MTR_FORMAT_256B = 0xC0000; + +struct IwxPrphScratchPnvmCfg { + uint64_t pnvm_base_addr; + uint32_t pnvm_size; + uint32_t reserved; +} __attribute__((packed)); + +struct IwxPrphScratchHwmCfg { + uint64_t hwm_base_addr; + uint32_t hwm_size; + uint32_t debug_token_config; +} __attribute__((packed)); + +struct IwxPrphScratchRbdCfg { + uint64_t free_rbd_addr; + uint32_t reserved; +} __attribute__((packed)); + +struct IwxPrphScratchUefiCfg { + uint64_t base_addr; + uint32_t size; + uint32_t reserved; +} __attribute__((packed)); + +struct IwxPrphScratchCtrlCfg { + IwxPrphScratchVersion version; + IwxPrphScratchControl control; + IwxPrphScratchPnvmCfg pnvm_cfg; + IwxPrphScratchHwmCfg hwm_cfg; + IwxPrphScratchRbdCfg rbd_cfg; + IwxPrphScratchUefiCfg reduce_power_cfg; +} __attribute__((packed)); + +struct IwxPrphScratch { + IwxPrphScratchCtrlCfg ctrl_cfg; + uint32_t reserved[12]; + IwxContextInfoDram dram; +} __attribute__((packed)); + +struct IwxPrphInfo { + uint32_t boot_stage_mirror; + uint32_t ipc_status_mirror; + uint32_t sleep_notif; + uint32_t reserved; +} __attribute__((packed)); + +struct IwxContextInfoGen3 { + uint16_t version; + uint16_t size; + uint32_t config; + uint64_t prph_info_base_addr; + uint64_t cr_head_idx_arr_base_addr; + uint64_t tr_tail_idx_arr_base_addr; + uint64_t cr_tail_idx_arr_base_addr; + uint64_t tr_head_idx_arr_base_addr; + uint16_t cr_idx_arr_size; + uint16_t tr_idx_arr_size; + uint64_t mtr_base_addr; + uint64_t mcr_base_addr; + uint16_t mtr_size; + uint16_t mcr_size; + uint16_t mtr_doorbell_vec; + uint16_t mcr_doorbell_vec; + uint16_t mtr_msi_vec; + uint16_t mcr_msi_vec; + uint8_t mtr_opt_header_size; + uint8_t mtr_opt_footer_size; + uint8_t mcr_opt_header_size; + uint8_t mcr_opt_footer_size; + uint16_t msg_rings_ctrl_flags; + uint16_t prph_info_msi_vec; + uint64_t prph_scratch_base_addr; + uint32_t prph_scratch_size; + uint32_t reserved; +} __attribute__((packed)); + +struct IwxPnvmInfoDram { + uint64_t pnvm_img[IWX_MAX_DRAM_ENTRY]; +} __attribute__((packed)); + +// ============================================================================= +// Firmware file (.ucode) TLV format +// ============================================================================= + +constexpr uint32_t IWX_TLV_UCODE_MAGIC = 0x0a4c5749; + +struct IwxTlvUcodeHeader { + uint32_t zero; + uint32_t magic; + uint8_t human_readable[64]; + uint32_t ver; + uint32_t build; + uint64_t ignore; + uint8_t data[]; +} __attribute__((packed)); + +struct IwxUcodeTlv { + uint32_t type; + uint32_t length; + uint8_t data[]; +} __attribute__((packed)); + +// TLV types used by this driver +constexpr uint32_t IWX_UCODE_TLV_PROBE_MAX_LEN = 6; +constexpr uint32_t IWX_UCODE_TLV_FLAGS = 18; +constexpr uint32_t IWX_UCODE_TLV_SEC_RT = 19; +constexpr uint32_t IWX_UCODE_TLV_PHY_SKU = 23; +constexpr uint32_t IWX_UCODE_TLV_API_CHANGES_SET = 29; +constexpr uint32_t IWX_UCODE_TLV_ENABLED_CAPABILITIES = 30; +constexpr uint32_t IWX_UCODE_TLV_N_SCAN_CHANNELS = 31; +constexpr uint32_t IWX_UCODE_TLV_FW_VERSION = 36; +constexpr uint32_t IWX_UCODE_TLV_CMD_VERSIONS = 48; +constexpr uint32_t IWX_UCODE_TLV_IML = 52; +constexpr uint32_t IWX_UCODE_TLV_PNVM_DATA = 74; +constexpr uint32_t IWX_UCODE_TLV_PNVM_VERSION = 62; +constexpr uint32_t IWX_UCODE_TLV_PNVM_SKU = 64; +constexpr uint32_t IWX_UCODE_TLV_HW_TYPE = 58; + +// Section separators inside SEC_RT stream +constexpr uint32_t IWX_CPU1_CPU2_SEPARATOR_SECTION = 0xFFFFCCCC; +constexpr uint32_t IWX_PAGING_SEPARATOR_SECTION = 0xAAAABBBB; + +constexpr uint32_t IWX_UCODE_MAJOR(uint32_t ver) { return (ver & 0xFF000000) >> 24; } +constexpr uint32_t IWX_UCODE_MINOR(uint32_t ver) { return (ver & 0x00FF0000) >> 16; } +constexpr uint32_t IWX_UCODE_API(uint32_t ver) { return (ver & 0x0000FF00) >> 8; } + +struct IwxUcodeApiCapa { // API_CHANGES_SET / ENABLED_CAPABILITIES + uint32_t api_index; + uint32_t api_flags; +} __attribute__((packed)); + +struct IwxFwCmdVersion { + uint8_t cmd; + uint8_t group; + uint8_t cmd_ver; + uint8_t notif_ver; +} __attribute__((packed)); + +// ucode API/capability bits used here +constexpr uint32_t IWX_UCODE_TLV_API_REGULATORY_NVM_INFO = 48; +constexpr uint32_t IWX_UCODE_TLV_API_REDUCED_SCAN_CONFIG = 56; +constexpr uint32_t IWX_UCODE_TLV_API_SCAN_EXT_CHAN_VER = 58; +constexpr uint32_t IWX_UCODE_TLV_CAPA_LAR_SUPPORT = 1; +constexpr uint32_t IWX_UCODE_TLV_CAPA_DS_PARAM_SET_IE_SUPPORT = 9; +constexpr uint32_t IWX_UCODE_TLV_CAPA_DQA_SUPPORT = 12; +constexpr uint32_t IWX_UCODE_TLV_CAPA_LAR_MULTI_MCC = 29; +constexpr uint32_t IWX_UCODE_TLV_CAPA_FRAGMENTED_PNVM_IMG = 32; +constexpr uint32_t IWX_UCODE_TLV_CAPA_CDB_SUPPORT = 40; +constexpr uint32_t IWX_UCODE_TLV_CAPA_ULTRA_HB_CHANNELS = 48; +constexpr uint32_t IWX_UCODE_TLV_CAPA_SESSION_PROT_CMD = 54; +constexpr uint32_t IWX_UCODE_TLV_CAPA_CT_KILL_BY_FW = 74; +constexpr uint32_t IWX_UCODE_TLV_CAPA_MCC_UPDATE_11AX_SUPPORT = 89; +constexpr uint32_t IWX_UCODE_TLV_API_WIFI_MCC_UPDATE = 9; +constexpr uint32_t IWX_NUM_UCODE_TLV_API = 128; +constexpr uint32_t IWX_NUM_UCODE_TLV_CAPA = 160; + +// PHY_SKU decoding +constexpr uint32_t IWX_FW_PHY_CFG_TX_CHAIN_POS = 16; +constexpr uint32_t IWX_FW_PHY_CFG_TX_CHAIN = 0xfu << IWX_FW_PHY_CFG_TX_CHAIN_POS; +constexpr uint32_t IWX_FW_PHY_CFG_RX_CHAIN_POS = 20; +constexpr uint32_t IWX_FW_PHY_CFG_RX_CHAIN = 0xfu << IWX_FW_PHY_CFG_RX_CHAIN_POS; + +// ============================================================================= +// Host commands +// ============================================================================= + +// Command groups +constexpr uint8_t IWX_LEGACY_GROUP = 0x0; +constexpr uint8_t IWX_LONG_GROUP = 0x1; +constexpr uint8_t IWX_SYSTEM_GROUP = 0x2; +constexpr uint8_t IWX_MAC_CONF_GROUP = 0x3; +constexpr uint8_t IWX_PHY_OPS_GROUP = 0x4; +constexpr uint8_t IWX_DATA_PATH_GROUP = 0x5; +constexpr uint8_t IWX_REGULATORY_AND_NVM_GROUP = 0xc; +constexpr uint8_t IWX_STATISTICS_GROUP = 0x10; + +constexpr uint32_t IWX_WIDE_ID(uint32_t grp, uint32_t opcode) { return (grp << 8) | opcode; } +constexpr uint8_t IwxCmdOpcode(uint32_t id) { return (uint8_t)(id & 0xff); } +constexpr uint8_t IwxCmdGroupId(uint32_t id) { return (uint8_t)((id >> 8) & 0xff); } + +// Legacy-group command / notification IDs +constexpr uint8_t IWX_ALIVE = 0x1; +constexpr uint8_t IWX_REPLY_ERROR = 0x2; +constexpr uint8_t IWX_INIT_COMPLETE_NOTIF = 0x4; +constexpr uint8_t IWX_PHY_CONTEXT_CMD = 0x8; +constexpr uint8_t IWX_SCAN_ITERATION_COMPLETE_UMAC = 0xb5; +constexpr uint8_t IWX_SCAN_CFG_CMD = 0xc; +constexpr uint8_t IWX_SCAN_REQ_UMAC = 0xd; +constexpr uint8_t IWX_SCAN_ABORT_UMAC = 0xe; +constexpr uint8_t IWX_SCAN_COMPLETE_UMAC = 0xf; +constexpr uint8_t IWX_ADD_STA_KEY = 0x17; +constexpr uint8_t IWX_ADD_STA = 0x18; +constexpr uint8_t IWX_REMOVE_STA = 0x19; +constexpr uint8_t IWX_TX_CMD = 0x1c; +constexpr uint8_t IWX_SCD_QUEUE_CFG = 0x1d; +constexpr uint8_t IWX_MAC_CONTEXT_CMD = 0x28; +constexpr uint8_t IWX_TIME_EVENT_CMD = 0x29; +constexpr uint8_t IWX_TIME_EVENT_NOTIFICATION = 0x2a; +constexpr uint8_t IWX_BINDING_CONTEXT_CMD = 0x2b; +constexpr uint8_t IWX_PHY_CONFIGURATION_CMD = 0x6a; +constexpr uint8_t IWX_POWER_TABLE_CMD = 0x77; +constexpr uint8_t IWX_LTR_CONFIG = 0xee; +constexpr uint8_t IWX_TX_ANT_CONFIGURATION_CMD = 0x98; +constexpr uint8_t IWX_BT_CONFIG = 0x9b; +constexpr uint8_t IWX_STATISTICS_NOTIFICATION = 0x9d; +constexpr uint8_t IWX_MISSED_BEACONS_NOTIFICATION = 0xa2; +constexpr uint8_t IWX_MFUART_LOAD_NOTIFICATION = 0xb1; +constexpr uint8_t IWX_MAC_PM_POWER_TABLE = 0xa9; +constexpr uint8_t IWX_REPLY_RX_PHY_CMD = 0xc0; +constexpr uint8_t IWX_REPLY_RX_MPDU_CMD = 0xc1; +constexpr uint8_t IWX_BA_NOTIF = 0xc5; +constexpr uint8_t IWX_MCC_UPDATE_CMD = 0xc8; +constexpr uint8_t IWX_MCC_CHUB_UPDATE_CMD = 0xc9; +constexpr uint8_t IWX_REPLY_BEACON_FILTERING_CMD = 0xd2; +constexpr uint8_t IWX_DEBUG_LOG_MSG = 0xf7; + +// SYSTEM group subcommands +constexpr uint8_t IWX_SOC_CONFIGURATION_CMD = 0x01; +constexpr uint8_t IWX_INIT_EXTENDED_CFG_CMD = 0x03; +constexpr uint8_t IWX_FSEQ_VER_MISMATCH_NOTIFICATION = 0xff; + +// MAC_CONF group subcommands +constexpr uint8_t IWX_SESSION_PROTECTION_CMD = 0x05; +constexpr uint8_t IWX_SESSION_PROTECTION_NOTIF = 0xfb; + +// PHY_OPS group subcommands +constexpr uint8_t IWX_TEMP_REPORTING_THRESHOLDS_CMD = 0x04; +constexpr uint8_t IWX_CT_KILL_NOTIFICATION = 0xFE; +constexpr uint8_t IWX_DTS_MEASUREMENT_NOTIF_WIDE = 0xFF; + +// DATA_PATH group subcommands +constexpr uint8_t IWX_DQA_ENABLE_CMD = 0x00; +constexpr uint8_t IWX_RLC_CONFIG_CMD = 0x08; +constexpr uint8_t IWX_TLC_MNG_CONFIG_CMD = 0x0f; +constexpr uint8_t IWX_SCD_QUEUE_CONFIG_CMD = 0x17; +constexpr uint8_t IWX_RX_NO_DATA_NOTIF = 0xf5; +constexpr uint8_t IWX_THERMAL_DUAL_CHAIN_REQUEST = 0xf6; +constexpr uint8_t IWX_TLC_MNG_UPDATE_NOTIF = 0xf7; + +// REGULATORY_AND_NVM group subcommands +constexpr uint8_t IWX_NVM_ACCESS_COMPLETE = 0x00; +constexpr uint8_t IWX_NVM_GET_INFO = 0x02; +constexpr uint8_t IWX_PNVM_INIT_COMPLETE = 0xfe; + +// Command delivery structures +struct IwxCmdHeader { + uint8_t code; + uint8_t flags; + uint8_t idx; + uint8_t qid; +} __attribute__((packed)); + +struct IwxCmdHeaderWide { + uint8_t opcode; + uint8_t group_id; + uint8_t idx; + uint8_t qid; + uint16_t length; + uint8_t reserved; + uint8_t version; +} __attribute__((packed)); + +constexpr uint32_t IWX_DEF_CMD_PAYLOAD_SIZE = 320; +constexpr uint32_t IWX_MAX_CMD_PAYLOAD_SIZE = 4096 - sizeof(IwxCmdHeaderWide); +constexpr uint32_t IWX_FIRST_TB_SIZE = 20; +constexpr uint32_t IWX_FIRST_TB_SIZE_ALIGN = (IWX_FIRST_TB_SIZE + 63) & ~63u; +constexpr uint8_t IWX_CMD_FAILED_MSK = 0x40; + +struct IwxDeviceCmd { + IwxCmdHeaderWide hdr_wide; + uint8_t data_wide[IWX_DEF_CMD_PAYLOAD_SIZE - sizeof(IwxCmdHeaderWide) + + sizeof(IwxCmdHeader)]; +} __attribute__((packed)); + +struct IwxRxPacket { + uint32_t len_n_flags; + IwxCmdHeader hdr; + uint8_t data[]; +} __attribute__((packed)); + +constexpr uint32_t IWX_FH_RSCSR_FRAME_SIZE_MSK = 0x00003fff; +constexpr uint32_t IWX_FH_RSCSR_FRAME_INVALID = 0x55550000; +constexpr uint32_t IWX_FH_RSCSR_FRAME_ALIGN = 0x40; + +inline uint32_t IwxRxPacketLen(const IwxRxPacket* pkt) { + return pkt->len_n_flags & IWX_FH_RSCSR_FRAME_SIZE_MSK; +} +inline uint32_t IwxRxPacketPayloadLen(const IwxRxPacket* pkt) { + return IwxRxPacketLen(pkt) - sizeof(IwxCmdHeader); +} + +// DQA queue assignment +constexpr int IWX_DQA_CMD_QUEUE = 0; +constexpr int IWX_DQA_MGMT_QUEUE = 1; +constexpr int IWX_MGMT_TID = 15; +constexpr int IWX_STATION_ID = 0; + +// ============================================================================= +// Simple command payloads +// ============================================================================= + +struct IwxDqaEnableCmd { uint32_t cmd_queue; } __attribute__((packed)); +struct IwxCmdResponse { uint32_t status; } __attribute__((packed)); +struct IwxTxAntCfgCmd { uint32_t valid; } __attribute__((packed)); +struct IwxCalibCtrl { uint32_t flow_trigger; uint32_t event_trigger; } __attribute__((packed)); +struct IwxPhyCfgCmd { uint32_t phy_cfg; IwxCalibCtrl calib_control; } __attribute__((packed)); +struct IwxBtCoexCmd { uint32_t mode; uint32_t enabled_modules; } __attribute__((packed)); +constexpr uint32_t IWX_BT_COEX_WIFI = 0x3; +struct IwxSocConfigurationCmd { uint32_t flags; uint32_t latency; } __attribute__((packed)); +constexpr uint32_t IWX_SOC_CONFIG_CMD_FLAGS_LOW_LATENCY = 1u << 1; +constexpr uint32_t IWX_SOC_FLAGS_LTR_APPLY_DELAY_2500 = 2; +struct IwxNvmAccessCompleteCmd { uint32_t reserved; } __attribute__((packed)); +struct IwxNvmGetInfo { uint32_t reserved; } __attribute__((packed)); +struct IwxInitExtendedCfgCmd { uint32_t init_flags; } __attribute__((packed)); +constexpr uint32_t IWX_INIT_NVM = 1u << 1; + +constexpr uint32_t IWX_LTR_CFG_FLAG_FEATURE_ENABLE = 0x00000001; +constexpr uint32_t IWX_LTR_VALID_STATES_NUM = 4; +struct IwxLtrConfigCmd { + uint32_t flags; + uint32_t static_long; + uint32_t static_short; + uint32_t ltr_cfg_values[IWX_LTR_VALID_STATES_NUM]; + uint32_t ltr_short_idle_timeout; +} __attribute__((packed)); + +constexpr uint32_t IWX_MAX_DTS_TRIPS = 8; +struct IwxTempReportThsCmd { + uint32_t num_temps; + uint16_t thresholds[IWX_MAX_DTS_TRIPS]; +} __attribute__((packed)); + +// BEACON_FILTER_CONFIG_API_S_VER_4. The two trailing threshold arrays are +// part of the command: firmware that advertises v4 expects all 60 bytes and +// asserts on a short payload. +struct IwxBeaconFilterCmd { + uint32_t bf_energy_delta; + uint32_t bf_roaming_energy_delta; + uint32_t bf_roaming_state; + uint32_t bf_temp_threshold; + uint32_t bf_temp_fast_filter; + uint32_t bf_temp_slow_filter; + uint32_t bf_enable_beacon_filter; + uint32_t bf_debug_flag; + uint32_t bf_escape_timer; + uint32_t ba_escape_timer; + uint32_t ba_enable_beacon_abort; + uint32_t bf_threshold_absolute_low[2]; + uint32_t bf_threshold_absolute_high[2]; +} __attribute__((packed)); + +// ============================================================================= +// ALIVE notification +// ============================================================================= + +constexpr uint16_t IWX_ALIVE_STATUS_OK = 0xCAFE; + +struct IwxLmacDebugAddrs { + uint32_t error_event_table_ptr; + uint32_t log_event_table_ptr; + uint32_t cpu_register_ptr; + uint32_t dbgm_config_ptr; + uint32_t alive_counter_ptr; + uint32_t scd_base_ptr; + uint32_t st_fwrd_addr; + uint32_t st_fwrd_size; +} __attribute__((packed)); + +struct IwxLmacAlive { + uint32_t ucode_major; + uint32_t ucode_minor; + uint8_t ver_subtype; + uint8_t ver_type; + uint8_t mac; + uint8_t opt; + uint32_t timestamp; + IwxLmacDebugAddrs dbg_ptrs; +} __attribute__((packed)); + +struct IwxUmacDebugAddrs { + uint32_t error_info_addr; + uint32_t dbg_print_buff_addr; +} __attribute__((packed)); + +struct IwxUmacAlive { + uint32_t umac_major; + uint32_t umac_minor; + IwxUmacDebugAddrs dbg_ptrs; +} __attribute__((packed)); + +struct IwxSkuId { uint32_t data[3]; } __attribute__((packed)); + +struct IwxAliveRespV5 { + uint16_t status; + uint16_t flags; + IwxLmacAlive lmac_data[2]; + IwxUmacAlive umac_data; + IwxSkuId sku_id; +} __attribute__((packed)); + +struct IwxImrAliveInfo { + uint64_t base_addr; + uint32_t size; + uint32_t enabled; +} __attribute__((packed)); + +struct IwxAliveRespV6 { + uint16_t status; + uint16_t flags; + IwxLmacAlive lmac_data[2]; + IwxUmacAlive umac_data; + IwxSkuId sku_id; + IwxImrAliveInfo imr; +} __attribute__((packed)); + +// ============================================================================= +// NVM (regulatory / MAC data via NVM_GET_INFO) +// ============================================================================= + +constexpr uint32_t IWX_NUM_CHANNELS_V1 = 51; +constexpr uint32_t IWX_NUM_CHANNELS = 110; +constexpr uint32_t IWX_NUM_2GHZ_CHANNELS = 14; +constexpr uint32_t IWX_NUM_5GHZ_CHANNELS = 37; + +struct IwxNvmGetInfoGeneral { + uint32_t flags; + uint16_t nvm_version; + uint8_t board_type; + uint8_t n_hw_addrs; +} __attribute__((packed)); + +struct IwxNvmGetInfoSku { uint32_t mac_sku_flags; } __attribute__((packed)); +struct IwxNvmGetInfoPhy { uint32_t tx_chains; uint32_t rx_chains; } __attribute__((packed)); + +constexpr uint32_t IWX_NVM_MAC_SKU_FLAGS_BAND_2_4_ENABLED = 1u << 0; +constexpr uint32_t IWX_NVM_MAC_SKU_FLAGS_BAND_5_2_ENABLED = 1u << 1; +constexpr uint32_t IWX_NVM_MAC_SKU_FLAGS_802_11N_ENABLED = 1u << 2; +constexpr uint32_t IWX_NVM_MAC_SKU_FLAGS_802_11AC_ENABLED = 1u << 3; +constexpr uint32_t IWX_NVM_MAC_SKU_FLAGS_802_11AX_ENABLED = 1u << 4; + +struct IwxNvmGetInfoRegulatoryV1 { + uint32_t lar_enabled; + uint16_t channel_profile[IWX_NUM_CHANNELS_V1]; + uint16_t reserved; +} __attribute__((packed)); + +struct IwxNvmGetInfoRegulatory { + uint32_t lar_enabled; + uint32_t n_channels; + uint32_t channel_profile[IWX_NUM_CHANNELS]; +} __attribute__((packed)); + +struct IwxNvmGetInfoRspV3 { + IwxNvmGetInfoGeneral general; + IwxNvmGetInfoSku mac_sku; + IwxNvmGetInfoPhy phy_sku; + IwxNvmGetInfoRegulatoryV1 regulatory; +} __attribute__((packed)); + +struct IwxNvmGetInfoRsp { + IwxNvmGetInfoGeneral general; + IwxNvmGetInfoSku mac_sku; + IwxNvmGetInfoPhy phy_sku; + IwxNvmGetInfoRegulatory regulatory; +} __attribute__((packed)); + +// NVM channel flags +constexpr uint32_t IWX_NVM_CHANNEL_VALID = 1u << 0; +constexpr uint32_t IWX_NVM_CHANNEL_ACTIVE = 1u << 3; +constexpr uint32_t IWX_NVM_CHANNEL_40MHZ = 1u << 9; +constexpr uint32_t IWX_NVM_CHANNEL_80MHZ = 1u << 10; +constexpr uint32_t IWX_NVM_CHANNEL_160MHZ = 1u << 11; + +// ============================================================================= +// MCC (regulatory country) update +// ============================================================================= + +struct IwxMccUpdateCmd { + uint16_t mcc; + uint8_t source_id; + uint8_t reserved; + uint32_t key; + uint32_t reserved2[5]; +} __attribute__((packed)); + +constexpr uint8_t IWX_MCC_SOURCE_OLD_FW = 0; +constexpr uint8_t IWX_MCC_SOURCE_GET_CURRENT = 0x10; + +struct IwxMccUpdateRespV4 { + uint32_t status; + uint16_t mcc; + uint16_t cap; + uint16_t time; + uint16_t geo_info; + uint8_t source_id; + uint8_t reserved[3]; + uint32_t n_channels; + uint32_t channels[]; +} __attribute__((packed)); + +struct IwxMccChubNotif { + uint16_t mcc; + uint8_t source_id; + uint8_t reserved1; +} __attribute__((packed)); + +// ============================================================================= +// UMAC scan +// ============================================================================= + +constexpr uint32_t IWX_MAX_SCAN_CHANNELS = 67; // API channel_config[] size +constexpr uint32_t IWX_DEFAULT_SCAN_CHANNELS = 40; +constexpr uint32_t IWX_SCAN_TWO_LMACS = 2; +constexpr uint32_t IWX_SCAN_LB_LMAC_IDX = 0; +constexpr uint32_t IWX_SCAN_HB_LMAC_IDX = 1; +constexpr uint32_t IWX_SCAN_PRIORITY_EXT_6 = 6; +constexpr uint8_t IWX_PHY_BAND_5 = 0; +constexpr uint8_t IWX_PHY_BAND_24 = 1; +constexpr uint32_t IWX_CHAN_CFG_FLAGS_BAND_POS = 30; +constexpr uint32_t IWX_MAX_SCHED_SCAN_PLANS = 2; +constexpr uint32_t IWX_PROBE_OPTION_MAX = 20; +constexpr uint32_t IWX_SCAN_OFFLOAD_PROBE_REQ_SIZE = 512; +constexpr uint32_t IWX_SCAN_SHORT_SSID_MAX_SIZE = 8; +constexpr uint32_t IWX_SCAN_BSSID_MAX_SIZE = 16; +constexpr uint8_t IWX_SCAN_CHANNEL_FLAG_ENABLE_CHAN_ORDER = 1 << 5; + +constexpr uint16_t IWX_UMAC_SCAN_GEN_FLAGS_V2_PASS_ALL = 1 << 1; +constexpr uint16_t IWX_UMAC_SCAN_GEN_FLAGS_V2_NTFY_ITER_COMPLETE = 1 << 2; +constexpr uint16_t IWX_UMAC_SCAN_GEN_FLAGS_V2_ADAPTIVE_DWELL = 1 << 7; +constexpr uint16_t IWX_UMAC_SCAN_GEN_FLAGS_V2_FORCE_PASSIVE = 1 << 11; + +struct IwxScanConfig { // SCAN_CONFIG_DB_CMD_API_S_5 (reduced) + uint8_t enable_cam_mode; + uint8_t enable_promiscuous_mode; + uint8_t bcast_sta_id; + uint8_t reserved; + uint32_t tx_chains; + uint32_t rx_chains; +} __attribute__((packed)); + +struct IwxSsidIe { + uint8_t id; + uint8_t len; + uint8_t ssid[32]; +} __attribute__((packed)); + +struct IwxScanProbeSegment { uint16_t offset; uint16_t len; } __attribute__((packed)); + +struct IwxScanProbeReq { // PROBE_REQUEST_FRAME_API_S_VER_v2 + IwxScanProbeSegment mac_header; + IwxScanProbeSegment band_data[3]; + IwxScanProbeSegment common_data; + uint8_t buf[IWX_SCAN_OFFLOAD_PROBE_REQ_SIZE]; +} __attribute__((packed)); + +struct IwxScanChannelCfgUmac { // SCAN_CHANNEL_CFG_S_VER_2 layout + uint32_t flags; + uint8_t channel_num; + uint8_t band; + uint8_t iter_count; + uint8_t iter_interval; +} __attribute__((packed)); + +struct IwxScanChannelCfgUmacV5 { // SCAN_CHANNEL_CONFIG_API_S_VER_5 + uint32_t flags; + uint8_t channel_num; + uint8_t psd_20; + uint8_t iter_count; + uint8_t iter_interval; +} __attribute__((packed)); + +struct IwxScanUmacSchedule { + uint16_t interval; + uint8_t iter_count; + uint8_t reserved; +} __attribute__((packed)); + +struct IwxScanGeneralParamsV10 { + uint16_t flags; + uint8_t reserved; + uint8_t scan_start_mac_id; + uint8_t active_dwell[IWX_SCAN_TWO_LMACS]; + uint8_t adwell_default_2g; + uint8_t adwell_default_5g; + uint8_t adwell_default_social_chn; + uint8_t reserved1; + uint16_t adwell_max_budget; + uint32_t max_out_of_time[IWX_SCAN_TWO_LMACS]; + uint32_t suspend_time[IWX_SCAN_TWO_LMACS]; + uint32_t scan_priority; + uint8_t passive_dwell[IWX_SCAN_TWO_LMACS]; + uint8_t num_of_fragments[IWX_SCAN_TWO_LMACS]; +} __attribute__((packed)); + +// V11/V12 layout matches V10 except reserved1 is flags2. +using IwxScanGeneralParamsV11 = IwxScanGeneralParamsV10; + +struct IwxScanChannelParamsV6 { + uint8_t flags; + uint8_t count; + uint8_t n_aps_override[2]; + IwxScanChannelCfgUmac channel_config[IWX_MAX_SCAN_CHANNELS]; +} __attribute__((packed)); + +struct IwxScanChannelParamsV7 { + uint8_t flags; + uint8_t count; + uint8_t n_aps_override[2]; + IwxScanChannelCfgUmacV5 channel_config[IWX_MAX_SCAN_CHANNELS]; +} __attribute__((packed)); + +struct IwxScanPeriodicParmsV1 { + IwxScanUmacSchedule schedule[IWX_MAX_SCHED_SCAN_PLANS]; + uint16_t delay; + uint16_t reserved; +} __attribute__((packed)); + +struct IwxScanProbeParamsV4 { + IwxScanProbeReq preq; + uint8_t short_ssid_num; + uint8_t bssid_num; + uint16_t reserved; + IwxSsidIe direct_scan[IWX_PROBE_OPTION_MAX]; + uint32_t short_ssid[IWX_SCAN_SHORT_SSID_MAX_SIZE]; + uint8_t bssid_array[IWX_SCAN_BSSID_MAX_SIZE][6]; +} __attribute__((packed)); + +struct IwxScanReqUmacV14 { + uint32_t uid; + uint32_t ooc_priority; + IwxScanGeneralParamsV10 general_params; + IwxScanChannelParamsV6 channel_params; + IwxScanPeriodicParmsV1 periodic_params; + IwxScanProbeParamsV4 probe_params; +} __attribute__((packed)); + +struct IwxScanReqUmacV17 { + uint32_t uid; + uint32_t ooc_priority; + IwxScanGeneralParamsV11 general_params; + IwxScanChannelParamsV7 channel_params; + IwxScanPeriodicParmsV1 periodic_params; + IwxScanProbeParamsV4 probe_params; +} __attribute__((packed)); + +struct IwxUmacScanAbort { uint32_t uid; uint32_t flags; } __attribute__((packed)); + +struct IwxUmacScanComplete { + uint32_t uid; + uint8_t last_schedule; + uint8_t last_iter; + uint8_t status; + uint8_t ebs_status; + uint32_t time_from_last_iter; + uint32_t reserved; +} __attribute__((packed)); + +// Scan dwell defaults (TU) +constexpr uint8_t IWX_SCAN_DWELL_ACTIVE = 10; +constexpr uint8_t IWX_SCAN_DWELL_PASSIVE = 110; +constexpr uint16_t IWX_SCAN_ADWELL_MAX_BUDGET_FULL_SCAN = 300; +constexpr uint8_t IWX_SCAN_ADWELL_DEFAULT_HB_N_APS = 8; +constexpr uint8_t IWX_SCAN_ADWELL_DEFAULT_LB_N_APS = 2; +constexpr uint8_t IWX_SCAN_ADWELL_DEFAULT_N_APS_SOCIAL = 10; +constexpr uint8_t IWX_SCAN_ADWELL_N_APS_GO_FRIENDLY = 10; +constexpr uint8_t IWX_SCAN_ADWELL_N_APS_SOCIAL_CHS = 2; + +// ============================================================================= +// RX MPDU descriptor (AX210) +// ============================================================================= + +constexpr uint32_t IWX_RX_MPDU_RES_STATUS_CRC_OK = 1u << 0; +constexpr uint32_t IWX_RX_MPDU_RES_STATUS_OVERRUN_OK = 1u << 1; +constexpr uint8_t IWX_RX_MPDU_MFLG2_PAD = 0x20; +constexpr uint8_t IWX_RX_MPDU_MFLG2_AMSDU = 0x40; + +struct IwxRxMpduDescV3 { + uint32_t filter_match; + uint32_t rss_hash; + uint32_t partial_hash; + uint16_t raw_xsum; + uint16_t reserved_xsum; + uint32_t rate_n_flags; + uint8_t energy_a; + uint8_t energy_b; + uint8_t channel; + uint8_t mac_context; + uint32_t gp2_on_air_rise; + uint32_t tsf_on_air_rise0; + uint32_t tsf_on_air_rise1; + uint32_t reserved[2]; +} __attribute__((packed)); + +struct IwxRxMpduDesc { + uint16_t mpdu_len; + uint8_t mac_flags1; + uint8_t mac_flags2; + uint8_t amsdu_info; + uint16_t phy_info; + uint8_t mac_phy_idx; + uint16_t raw_csum; + uint16_t l3l4_flags; + uint32_t status; + uint32_t reorder_data; + IwxRxMpduDescV3 v3; +} __attribute__((packed)); + +// ============================================================================= +// Firmware context management (auth/assoc groundwork) +// ============================================================================= + +constexpr uint32_t IWX_FW_CTXT_ID_POS = 0; +constexpr uint32_t IWX_FW_CTXT_COLOR_POS = 8; +constexpr uint32_t IWX_FW_CTXT_INVALID = 0xffffffff; +constexpr uint32_t IWX_FW_CMD_ID_AND_COLOR(uint32_t id, uint32_t color) { + return (id << IWX_FW_CTXT_ID_POS) | (color << IWX_FW_CTXT_COLOR_POS); +} +constexpr uint32_t IWX_FW_CTXT_ACTION_ADD = 1; +constexpr uint32_t IWX_FW_CTXT_ACTION_MODIFY = 2; +constexpr uint32_t IWX_FW_CTXT_ACTION_REMOVE = 3; + +constexpr uint32_t IWX_LMAC_24G_INDEX = 0; +constexpr uint32_t IWX_LMAC_5G_INDEX = 1; + +// PHY context +struct IwxFwChannelInfoV1 { + uint8_t band; + uint8_t channel; + uint8_t width; + uint8_t ctrl_pos; +} __attribute__((packed)); + +constexpr uint8_t IWX_PHY_VHT_CHANNEL_MODE20 = 0x0; +constexpr uint8_t IWX_PHY_VHT_CTRL_POS_1_BELOW = 0x0; +constexpr uint32_t IWX_PHY_RX_CHAIN_VALID_POS = 1; +constexpr uint32_t IWX_PHY_RX_CHAIN_CNT_POS = 10; +constexpr uint32_t IWX_PHY_RX_CHAIN_MIMO_CNT_POS = 12; + +struct IwxPhyContextCmd { // PHY_CONTEXT_CMD_API_VER_3/4 (non-UHB) + uint32_t id_and_color; + uint32_t action; + IwxFwChannelInfoV1 ci; + uint32_t lmac_id; + uint32_t rxchain_info; + uint32_t dsp_cfg_flags; + uint32_t reserved; +} __attribute__((packed)); + +// MAC context +constexpr uint32_t IWX_FW_MAC_TYPE_BSS_STA = 5; +constexpr uint32_t IWX_TSF_ID_A = 0; +constexpr uint32_t IWX_AC_NUM = 4; +constexpr uint32_t IWX_MAC_QOS_FLG_UPDATE_EDCA = 1u << 0; +constexpr uint32_t IWX_MAC_FILTER_IN_CONTROL_AND_MGMT = 1u << 1; +constexpr uint32_t IWX_MAC_FILTER_ACCEPT_GRP = 1u << 2; +constexpr uint32_t IWX_MAC_FILTER_IN_BEACON = 1u << 6; +constexpr uint32_t IWX_MAC_FILTER_IN_PROBE_REQUEST = 1u << 12; + +struct IwxAcQos { + uint16_t cw_min; + uint16_t cw_max; + uint8_t aifsn; + uint8_t fifos_mask; + uint16_t edca_txop; +} __attribute__((packed)); + +struct IwxMacDataSta { + uint32_t is_assoc; + uint32_t dtim_time; + uint64_t dtim_tsf; + uint32_t bi; + uint32_t reserved1; + uint32_t dtim_interval; + uint32_t data_policy; + uint32_t listen_interval; + uint32_t assoc_id; + uint32_t assoc_beacon_arrive_time; +} __attribute__((packed)); + +struct IwxMacCtxCmd { // IWX_MAC_CONTEXT_CMD_API_S_VER_1 (sta) + uint32_t id_and_color; + uint32_t action; + uint32_t mac_type; + uint32_t tsf_id; + uint8_t node_addr[6]; + uint16_t reserved_for_node_addr; + uint8_t bssid_addr[6]; + uint16_t reserved_for_bssid_addr; + uint32_t cck_rates; + uint32_t ofdm_rates; + uint32_t protection_flags; + uint32_t cck_short_preamble; + uint32_t short_slot; + uint32_t filter_flags; + uint32_t qos_flags; + IwxAcQos ac[IWX_AC_NUM + 1]; + IwxMacDataSta sta; +} __attribute__((packed)); + +// Binding context +constexpr uint32_t IWX_MAX_MACS_IN_BINDING = 3; +struct IwxBindingCmd { + uint32_t id_and_color; + uint32_t action; + uint32_t macs[IWX_MAX_MACS_IN_BINDING]; + uint32_t phy; + uint32_t lmac_id; +} __attribute__((packed)); + +// ADD_STA +constexpr uint8_t IWX_STA_MODE_ADD = 0; // add_modify: 0 add, 1 modify +struct IwxAddStaCmd { // ADD_STA_CMD_API_S_VER_12 prefix + uint8_t add_modify; + uint8_t awake_acs; + uint16_t tid_disable_tx; + uint32_t mac_id_n_color; + uint8_t addr[6]; + uint16_t reserved2; + uint8_t sta_id; + uint8_t modify_mask; + uint16_t reserved3; + uint32_t station_flags; + uint32_t station_flags_msk; + uint8_t add_immediate_ba_tid; + uint8_t remove_immediate_ba_tid; + uint16_t add_immediate_ba_ssn; + uint16_t sleep_tx_count; + uint8_t sleep_state_flags; + uint8_t station_type; + uint16_t assoc_id; + uint16_t beamform_flags; + uint32_t tfd_queue_msk; + uint16_t rx_ba_window; + uint8_t sp_length; + uint8_t uapsd_acs; +} __attribute__((packed)); +constexpr uint8_t IWX_STA_TYPE_LINK = 1; // iwl_sta_type: 1 = STA (link) +constexpr uint32_t IWX_ADD_STA_SUCCESS = 0x1; +constexpr uint32_t IWX_ADD_STA_STATUS_MASK = 0xFF; + +// SCD_QUEUE_CFG (v0, TX_QUEUE_CFG_CMD_API_S_VER_2) +constexpr uint16_t IWX_TX_QUEUE_CFG_ENABLE_QUEUE = 1 << 0; +struct IwxTxQueueCfgCmd { + uint8_t sta_id; + uint8_t tid; + uint16_t flags; + uint32_t cb_size; + uint64_t byte_cnt_addr; + uint64_t tfdq_addr; +} __attribute__((packed)); + +struct IwxTxQueueCfgRsp { + uint16_t queue_number; + uint16_t flags; + uint16_t write_pointer; + uint16_t reserved; +} __attribute__((packed)); + +// SCD_QUEUE_CONFIG_CMD v3 (DATA_PATH group) - what AX211 firmware 89 uses. +constexpr uint32_t IWX_SCD_QUEUE_ADD = 0; +constexpr uint32_t IWX_SCD_QUEUE_REMOVE = 1; + +struct IwxScdQueueCfgCmd { + uint32_t operation; + union { + struct { + uint32_t sta_mask; + uint8_t tid; + uint8_t reserved[3]; + uint32_t flags; + uint32_t cb_size; + uint64_t bc_dram_addr; + uint64_t tfdq_dram_addr; + } __attribute__((packed)) add; + struct { + uint32_t sta_mask; + uint32_t tid; + } __attribute__((packed)) remove; + } __attribute__((packed)) u; +} __attribute__((packed)); + +// RLC_CONFIG_CMD v2 (DATA_PATH group): RX chain configuration for a PHY ctxt. +struct IwxRlcProperties { + uint32_t rx_chain_info; + uint32_t reserved; +} __attribute__((packed)); + +struct IwxSadProperties { + uint32_t chain_a_sad_mode; + uint32_t chain_b_sad_mode; + uint32_t mac_id; + uint32_t reserved; +} __attribute__((packed)); + +struct IwxRlcConfigCmd { + uint32_t phy_id; + IwxRlcProperties rlc; + IwxSadProperties sad; + uint8_t flags; + uint8_t reserved[3]; +} __attribute__((packed)); + +// Session protection +constexpr uint32_t IWX_SESSION_PROTECT_CONF_ASSOC = 1; +struct IwxSessionProtCmd { + uint32_t id_and_color; + uint32_t action; + uint32_t conf_id; + uint32_t duration_tu; + uint32_t repetition_count; + uint32_t interval; +} __attribute__((packed)); + +struct IwxSessionProtNotif { + uint32_t mac_id; + uint32_t status; + uint32_t start; + uint32_t conf_id; +} __attribute__((packed)); + +// ============================================================================= +// Firmware error log (read from device SRAM after an assert) +// ============================================================================= + +struct IwxUmacErrorEventTable { + uint32_t valid; // nonzero when the log holds an entry + uint32_t error_id; // assert / error type + uint32_t blink1; + uint32_t blink2; + uint32_t ilink1; + uint32_t ilink2; + uint32_t data1; + uint32_t data2; + uint32_t data3; + uint32_t umac_major; + uint32_t umac_minor; + uint32_t frame_pointer; + uint32_t stack_pointer; + uint32_t cmd_header; // last host command the UMAC accepted + uint32_t nic_isr_pref; +} __attribute__((packed)); + +} diff --git a/kernel/src/Drivers/Net/Wifi/IwxTrans.cpp b/kernel/src/Drivers/Net/Wifi/IwxTrans.cpp new file mode 100644 index 0000000..10048b2 --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/IwxTrans.cpp @@ -0,0 +1,1455 @@ +/* + * IwxTrans.cpp + * Intel AX210/AX211 PCIe transport: MMIO access, MSI-X, DMA rings, + * context-info-gen3 firmware boot, host commands and RX processing. + * + * The bring-up sequence follows Intel's iwlwifi/OpenBSD iwx flow: + * prepare card -> sw reset -> APM init -> MSI-X config -> rfkill check + * -> context info gen3 (firmware self-load) -> ALIVE -> PNVM -> init cmds + * + * Everything here runs in process/idle context. Hard interrupts only latch + * a "work pending" flag; all ring processing happens in IwxProcessEvents(), + * which the idle loop and the command wait loops pump. This mirrors the + * Bluetooth/E1000E split in this kernel: the network and IPC layers take + * process-context mutexes that must never be entered from an IRQ. + * + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "Iwx.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Kt; + +namespace Drivers::Net::Wifi { + + IwxState g_iwx; + + // MSI-X/MSI interrupt slot. Slots in use elsewhere: 24 (E1000E), + // 27 (Intel HDA), 28 (GPU vblank), 29 (xHCI). + static constexpr uint8_t MSI_IRQ = 25; + static constexpr uint32_t MSI_VECTOR = 57; // IRQ_VECTOR_BASE + MSI_IRQ + static constexpr uint32_t MSI_ADDR_BASE = 0xFEE00000; + + // Cached MSI-X masks (a cause is ENABLED when its mask bit is clear). + static uint32_t g_fhInitMask = 0; + static uint32_t g_hwInitMask = 0; + static uint32_t g_fhMask = 0; + static uint32_t g_hwMask = 0; + static bool g_msix = false; + + // ========================================================================= + // Register access + // ========================================================================= + + uint32_t IwxRead32(uint32_t reg) { + return *(volatile uint32_t*)(g_iwx.Mmio + reg); + } + + void IwxWrite32(uint32_t reg, uint32_t val) { + *(volatile uint32_t*)(g_iwx.Mmio + reg) = val; + } + + void IwxWrite8(uint32_t reg, uint8_t val) { + *(volatile uint8_t*)(g_iwx.Mmio + reg) = val; + } + + void IwxSetBits(uint32_t reg, uint32_t bits) { + IwxWrite32(reg, IwxRead32(reg) | bits); + } + + void IwxClearBits(uint32_t reg, uint32_t bits) { + IwxWrite32(reg, IwxRead32(reg) & ~bits); + } + + // Approximate busy-wait. The kernel has no sub-millisecond clock source + // exposed here; the loop count is calibrated the same way IntelHda does it. + void IwxDelayUs(uint32_t us) { + for (volatile uint32_t i = 0; i < us * 100u; i++) { + asm volatile("pause" ::: "memory"); + } + } + + void IwxDelayMs(uint32_t ms) { + uint64_t start = Timekeeping::GetMilliseconds(); + while (Timekeeping::GetMilliseconds() - start < ms) { + asm volatile("pause" ::: "memory"); + } + } + + bool IwxPollBit(uint32_t reg, uint32_t bits, uint32_t mask, int timeoutUs) { + for (int t = 0; t < timeoutUs; t += 10) { + if ((IwxRead32(reg) & mask) == bits) return true; + IwxDelayUs(10); + } + return false; + } + + // ========================================================================= + // NIC lock (MAC wakeup for PRPH / internal register access) + // ========================================================================= + + bool IwxNicLock() { + if (g_iwx.NicLockCount > 0) { + g_iwx.NicLockCount++; + return true; + } + + IwxSetBits(IWX_CSR_GP_CNTRL, IWX_CSR_GP_CNTRL_REG_FLAG_MAC_ACCESS_REQ); + IwxDelayUs(2); + + if (IwxPollBit(IWX_CSR_GP_CNTRL, + IWX_CSR_GP_CNTRL_REG_FLAG_MAC_CLOCK_READY, + IWX_CSR_GP_CNTRL_REG_FLAG_MAC_CLOCK_READY + | IWX_CSR_GP_CNTRL_REG_FLAG_GOING_TO_SLEEP, 150000)) { + g_iwx.NicLockCount = 1; + return true; + } + + KernelLogStream(WARNING, "WiFi") << "Timeout waiting for NIC access"; + return false; + } + + void IwxNicUnlock() { + if (g_iwx.NicLockCount > 0) { + if (--g_iwx.NicLockCount == 0) + IwxClearBits(IWX_CSR_GP_CNTRL, + IWX_CSR_GP_CNTRL_REG_FLAG_MAC_ACCESS_REQ); + } + } + + // PRPH accessors. Bits 24-25 of the address register encode "number of + // bytes - 1"; the driver only ever does dword accesses (3). + // + // The address field is 24 bits wide on the AX210 family (20 bits on older + // parts). This matters: UMAC PRPH registers are reached by adding + // IWX_UMAC_PRPH_OFFSET (0x300000), so e.g. UREG_CPU_INIT_RUN becomes + // 0xd05c44 -- a 20-bit mask truncates that to 0x005c44 and the access + // silently lands on an unrelated register. With the "kick firmware + // self-load" write going nowhere, the firmware never boots and ALIVE + // never arrives. + static uint32_t PrphAddr(uint32_t addr) { + return ((addr & 0x00FFFFFF) | (3 << 24)); + } + + uint32_t IwxReadPrph(uint32_t addr) { + IwxWrite32(IWX_HBUS_TARG_PRPH_RADDR, PrphAddr(addr)); + asm volatile("" ::: "memory"); + return IwxRead32(IWX_HBUS_TARG_PRPH_RDAT); + } + + void IwxWritePrph(uint32_t addr, uint32_t val) { + IwxWrite32(IWX_HBUS_TARG_PRPH_WADDR, PrphAddr(addr)); + asm volatile("" ::: "memory"); + IwxWrite32(IWX_HBUS_TARG_PRPH_WDAT, val); + } + + uint32_t IwxReadUmacPrph(uint32_t addr) { + return IwxReadPrph(addr + IWX_UMAC_PRPH_OFFSET); + } + + void IwxWriteUmacPrph(uint32_t addr, uint32_t val) { + IwxWritePrph(addr + IWX_UMAC_PRPH_OFFSET, val); + } + + // ========================================================================= + // DMA allocation + // ========================================================================= + + bool IwxDmaAlloc(IwxDma& dma, uint64_t bytes) { + if (bytes == 0) return false; + uint32_t pages = (uint32_t)((bytes + 0xFFF) / 0x1000); + + void* base = Memory::g_pfa->Allocate(); + if (!base) return false; + if (pages > 1) { + base = Memory::g_pfa->ReallocConsecutive(base, (int)pages); + if (!base) return false; + } + memset(base, 0, (size_t)pages * 0x1000); + + dma.Virt = base; + dma.Phys = Memory::SubHHDM(base); + dma.Pages = pages; + return true; + } + + void IwxDmaFree(IwxDma& dma) { + if (!dma.Virt) return; + if (dma.Pages > 1) Memory::g_pfa->Free(dma.Virt, (int)dma.Pages); + else Memory::g_pfa->Free(dma.Virt); + dma.Virt = nullptr; + dma.Phys = 0; + dma.Pages = 0; + } + + // ========================================================================= + // Interrupt configuration + // ========================================================================= + + static void IwxDisableInterrupts() { + if (!g_msix) { + IwxWrite32(IWX_CSR_INT_MASK, 0); + IwxWrite32(IWX_CSR_INT, ~0u); + IwxWrite32(IWX_CSR_FH_INT_STATUS, ~0u); + } else { + IwxWrite32(IWX_CSR_MSIX_FH_INT_MASK_AD, g_fhInitMask); + IwxWrite32(IWX_CSR_MSIX_HW_INT_MASK_AD, g_hwInitMask); + } + } + + static void IwxEnableInterrupts() { + if (!g_msix) { + // Non-MSI-X path: enable the causes we act on. + constexpr uint32_t INT_BIT_FH_RX = 1u << 31; + constexpr uint32_t INT_BIT_HW_ERR = 1u << 29; + constexpr uint32_t INT_BIT_FH_TX = 1u << 27; + constexpr uint32_t INT_BIT_SW_ERR = 1u << 25; + constexpr uint32_t INT_BIT_RF_KILL = 1u << 7; + constexpr uint32_t INT_BIT_SW_RX = 1u << 3; + constexpr uint32_t INT_BIT_ALIVE = 1u << 0; + IwxWrite32(IWX_CSR_INT_MASK, INT_BIT_FH_RX | INT_BIT_HW_ERR + | INT_BIT_FH_TX | INT_BIT_SW_ERR | INT_BIT_RF_KILL + | INT_BIT_SW_RX | INT_BIT_ALIVE); + } else { + g_hwMask = g_hwInitMask; + g_fhMask = g_fhInitMask; + IwxWrite32(IWX_CSR_MSIX_FH_INT_MASK_AD, ~g_fhMask); + IwxWrite32(IWX_CSR_MSIX_HW_INT_MASK_AD, ~g_hwMask); + } + } + + // Map every cause to vector 0 and unmask the ones we handle. `stopped` + // skips the UREG_CHICK write, which needs a live MAC. + static void IwxConfMsixHw(bool stopped) { + constexpr uint8_t vector = 0; + + if (!g_msix) { + if (!stopped && IwxNicLock()) { + IwxWriteUmacPrph(IWX_UREG_CHICK, IWX_UREG_CHICK_MSI_ENABLE); + IwxNicUnlock(); + } + return; + } + + if (!stopped && IwxNicLock()) { + IwxWriteUmacPrph(IWX_UREG_CHICK, IWX_UREG_CHICK_MSIX_ENABLE); + IwxNicUnlock(); + } + + IwxWrite32(IWX_CSR_MSIX_FH_INT_MASK_AD, ~0u); + IwxWrite32(IWX_CSR_MSIX_HW_INT_MASK_AD, ~0u); + + // RX queues 0 (command/fallback) and 1 (RSS data) -> our vector. + IwxWrite8(IWX_CSR_MSIX_RX_IVAR(0), vector | IWX_MSIX_NON_AUTO_CLEAR_CAUSE); + IwxWrite8(IWX_CSR_MSIX_RX_IVAR(1), vector | IWX_MSIX_NON_AUTO_CLEAR_CAUSE); + + static const uint32_t causes[] = { + IWX_MSIX_IVAR_CAUSE_D2S_CH0_NUM, IWX_MSIX_IVAR_CAUSE_D2S_CH1_NUM, + IWX_MSIX_IVAR_CAUSE_S2D, IWX_MSIX_IVAR_CAUSE_FH_ERR, + IWX_MSIX_IVAR_CAUSE_REG_ALIVE, IWX_MSIX_IVAR_CAUSE_REG_WAKEUP, + IWX_MSIX_IVAR_CAUSE_REG_RESET_DONE, IWX_MSIX_IVAR_CAUSE_REG_CT_KILL, + IWX_MSIX_IVAR_CAUSE_REG_RF_KILL, IWX_MSIX_IVAR_CAUSE_REG_PERIODIC, + IWX_MSIX_IVAR_CAUSE_REG_SW_ERR, IWX_MSIX_IVAR_CAUSE_REG_SW_ERR_V2, + IWX_MSIX_IVAR_CAUSE_REG_SCD, IWX_MSIX_IVAR_CAUSE_REG_FH_TX, + IWX_MSIX_IVAR_CAUSE_REG_HW_ERR, IWX_MSIX_IVAR_CAUSE_REG_HAP, + }; + for (uint32_t c : causes) + IwxWrite8(IWX_CSR_MSIX_IVAR(c), vector | IWX_MSIX_NON_AUTO_CLEAR_CAUSE); + + IwxClearBits(IWX_CSR_MSIX_FH_INT_MASK_AD, + IWX_MSIX_FH_INT_CAUSES_Q0 | IWX_MSIX_FH_INT_CAUSES_Q1 + | IWX_MSIX_FH_INT_CAUSES_D2S_CH0_NUM + | IWX_MSIX_FH_INT_CAUSES_D2S_CH1_NUM + | IWX_MSIX_FH_INT_CAUSES_S2D + | IWX_MSIX_FH_INT_CAUSES_FH_ERR); + IwxClearBits(IWX_CSR_MSIX_HW_INT_MASK_AD, + IWX_MSIX_HW_INT_CAUSES_REG_ALIVE + | IWX_MSIX_HW_INT_CAUSES_REG_WAKEUP + | IWX_MSIX_HW_INT_CAUSES_REG_RESET_DONE + | IWX_MSIX_HW_INT_CAUSES_REG_CT_KILL + | IWX_MSIX_HW_INT_CAUSES_REG_RF_KILL + | IWX_MSIX_HW_INT_CAUSES_REG_PERIODIC + | IWX_MSIX_HW_INT_CAUSES_REG_SW_ERR + | IWX_MSIX_HW_INT_CAUSES_REG_SW_ERR_V2 + | IWX_MSIX_HW_INT_CAUSES_REG_SCD + | IWX_MSIX_HW_INT_CAUSES_REG_FH_TX + | IWX_MSIX_HW_INT_CAUSES_REG_HW_ERR + | IWX_MSIX_HW_INT_CAUSES_REG_HAP); + } + + static void IwxInitMsixHw() { + IwxConfMsixHw(false); + if (!g_msix) return; + g_fhInitMask = ~IwxRead32(IWX_CSR_MSIX_FH_INT_MASK_AD); + g_fhMask = g_fhInitMask; + g_hwInitMask = ~IwxRead32(IWX_CSR_MSIX_HW_INT_MASK_AD); + g_hwMask = g_hwInitMask; + } + + static void IwxEnableRfkillInt() { + if (!g_msix) { + IwxWrite32(IWX_CSR_INT_MASK, 1u << 7); // IWX_CSR_INT_BIT_RF_KILL + } else { + IwxWrite32(IWX_CSR_MSIX_FH_INT_MASK_AD, g_fhInitMask); + IwxWrite32(IWX_CSR_MSIX_HW_INT_MASK_AD, + ~IWX_MSIX_HW_INT_CAUSES_REG_RF_KILL); + g_hwMask = IWX_MSIX_HW_INT_CAUSES_REG_RF_KILL; + } + IwxSetBits(IWX_CSR_GP_CNTRL, IWX_CSR_GP_CNTRL_REG_FLAG_RFKILL_WAKE_L1A_EN); + } + + bool IwxCheckRfKill() { + // Documented as "state of the hardware RF-Kill switch", but the bit + // reads 0 when the radio is DISABLED (Intel's own comment says the + // documentation is unhelpful here). + uint32_t v = IwxRead32(IWX_CSR_GP_CNTRL); + return (v & IWX_CSR_GP_CNTRL_REG_FLAG_HW_RF_KILL_SW) == 0; + } + + // The hard interrupt only latches work. All ring/notification handling + // takes heap and (indirectly) IPC locks, so it runs from the idle loop. + static void HandleInterrupt(uint8_t, bool) { + g_iwx.WorkPending = true; + } + + // ========================================================================= + // APM (adapter power management) bring-up + // ========================================================================= + + static void IwxApmConfig() { + // L0s is unstable on these parts and unsupported on newer ones; the + // Linux and OpenBSD drivers both disable it unconditionally. + IwxSetBits(IWX_CSR_GIO_REG, IWX_CSR_GIO_REG_VAL_L0S_DISABLED); + } + + static bool IwxApmInit() { + IwxSetBits(IWX_CSR_GIO_CHICKEN_BITS, + IWX_CSR_GIO_CHICKEN_BITS_REG_BIT_L1A_NO_L0S_RX); + IwxSetBits(IWX_CSR_DBG_HPET_MEM_REG, IWX_CSR_DBG_HPET_MEM_REG_VAL); + IwxSetBits(IWX_CSR_HW_IF_CONFIG_REG, + IWX_CSR_HW_IF_CONFIG_REG_BIT_HAP_WAKE_L1A); + IwxApmConfig(); + + // D0U* -> D0A*: "initialization complete". + IwxSetBits(IWX_CSR_GP_CNTRL, IWX_CSR_GP_CNTRL_REG_FLAG_INIT_DONE); + + if (!IwxPollBit(IWX_CSR_GP_CNTRL, + IWX_CSR_GP_CNTRL_REG_FLAG_MAC_CLOCK_READY, + IWX_CSR_GP_CNTRL_REG_FLAG_MAC_CLOCK_READY, 25000)) { + KernelLogStream(ERROR, "WiFi") << "Timeout waiting for clock stabilization"; + return false; + } + return true; + } + + static void IwxApmStop() { + IwxSetBits(IWX_CSR_DBG_LINK_PWR_MGMT_REG, + IWX_CSR_RESET_LINK_PWR_MGMT_DISABLED); + IwxSetBits(IWX_CSR_HW_IF_CONFIG_REG, + IWX_CSR_HW_IF_CONFIG_REG_PREPARE + | IWX_CSR_HW_IF_CONFIG_REG_ENABLE_PME); + IwxDelayUs(1000); + IwxClearBits(IWX_CSR_DBG_LINK_PWR_MGMT_REG, + IWX_CSR_RESET_LINK_PWR_MGMT_DISABLED); + IwxDelayUs(5000); + + IwxSetBits(IWX_CSR_RESET, IWX_CSR_RESET_REG_FLAG_STOP_MASTER); + if (!IwxPollBit(IWX_CSR_RESET, IWX_CSR_RESET_REG_FLAG_MASTER_DISABLED, + IWX_CSR_RESET_REG_FLAG_MASTER_DISABLED, 100)) + KernelLogStream(WARNING, "WiFi") << "Timeout waiting for bus master"; + + IwxClearBits(IWX_CSR_GP_CNTRL, IWX_CSR_GP_CNTRL_REG_FLAG_INIT_DONE); + } + + static void IwxSwReset() { + IwxSetBits(IWX_CSR_RESET, IWX_CSR_RESET_REG_FLAG_SW_RESET); + IwxDelayUs(5000); + } + + static bool IwxSetHwReady() { + IwxSetBits(IWX_CSR_HW_IF_CONFIG_REG, + IWX_CSR_HW_IF_CONFIG_REG_BIT_NIC_READY); + bool ready = IwxPollBit(IWX_CSR_HW_IF_CONFIG_REG, + IWX_CSR_HW_IF_CONFIG_REG_BIT_NIC_READY, + IWX_CSR_HW_IF_CONFIG_REG_BIT_NIC_READY, 50); + if (ready) + IwxSetBits(IWX_CSR_MBOX_SET_REG, IWX_CSR_MBOX_SET_REG_OS_ALIVE); + return ready; + } + + static bool IwxPrepareCardHw() { + if (IwxSetHwReady()) return true; + + IwxSetBits(IWX_CSR_DBG_LINK_PWR_MGMT_REG, + IWX_CSR_RESET_LINK_PWR_MGMT_DISABLED); + IwxDelayUs(1000); + + for (int tries = 0; tries < 10; tries++) { + IwxSetBits(IWX_CSR_HW_IF_CONFIG_REG, + IWX_CSR_HW_IF_CONFIG_REG_PREPARE); + int t = 0; + do { + if (IwxSetHwReady()) return true; + IwxDelayUs(200); + t += 200; + } while (t < 150000); + IwxDelayUs(25000); + } + return false; + } + + // ========================================================================= + // Ring allocation + // ========================================================================= + + static bool IwxAllocRxRing() { + IwxRxRing& ring = g_iwx.RxQ; + ring.Cur = 0; + + if (!IwxDmaAlloc(ring.FreeDescs, + sizeof(IwxRxTransferDesc) * IWX_RX_MQ_RING_COUNT)) + return false; + if (!IwxDmaAlloc(ring.Stat, 64)) + return false; + if (!IwxDmaAlloc(ring.UsedDescs, + sizeof(IwxRxCompletionDesc) * IWX_RX_MQ_RING_COUNT)) + return false; + + // One 4 KiB page per RX buffer (IWX_RBUF_SIZE == PAGE_SIZE). + for (uint32_t i = 0; i < IWX_RX_MQ_RING_COUNT; i++) { + void* p = Memory::g_pfa->AllocateZeroed(); + if (!p) return false; + ring.Buf[i] = (uint8_t*)p; + ring.BufPhys[i] = Memory::SubHHDM(p); + } + return true; + } + + static void IwxFreeRxRing() { + IwxRxRing& ring = g_iwx.RxQ; + IwxDmaFree(ring.FreeDescs); + IwxDmaFree(ring.Stat); + IwxDmaFree(ring.UsedDescs); + for (uint32_t i = 0; i < IWX_RX_MQ_RING_COUNT; i++) { + if (ring.Buf[i]) { + Memory::g_pfa->Free(ring.Buf[i]); + ring.Buf[i] = nullptr; + } + } + } + + // Publish every RX buffer into the free ring. The firmware configures the + // RFH itself and only accepts these once it signals ALIVE. + static void IwxUpdateRxDescs() { + auto* descs = (IwxRxTransferDesc*)g_iwx.RxQ.FreeDescs.Virt; + for (uint32_t i = 0; i < IWX_RX_MQ_RING_COUNT; i++) { + descs[i].rbid = (uint16_t)(i & 0xffff); + descs[i].addr = g_iwx.RxQ.BufPhys[i]; + } + asm volatile("" ::: "memory"); + } + + static bool IwxAllocTxRing(IwxTxRing& ring, int qid) { + ring.Qid = qid; + ring.Cur = 0; + ring.CurHw = 0; + ring.Queued = 0; + + if (!IwxDmaAlloc(ring.Desc, sizeof(IwxTfhTfd) * IWX_TX_RING_COUNT)) + return false; + if (!IwxDmaAlloc(ring.BcTbl, + sizeof(IwxGen3BcTblEntry) * IWX_TFD_QUEUE_BC_SIZE_GEN3_AX210)) + return false; + if (!IwxDmaAlloc(ring.Cmd, sizeof(IwxDeviceCmd) * IWX_TX_RING_COUNT)) + return false; + // Commands larger than IWX_DEF_CMD_PAYLOAD_SIZE (e.g. SCAN_REQ_UMAC) + // are staged in this page instead of the per-slot command area. + if (!IwxDmaAlloc(ring.Bounce, 4096)) + return false; + return true; + } + + static void IwxFreeTxRing(IwxTxRing& ring) { + IwxDmaFree(ring.Desc); + IwxDmaFree(ring.BcTbl); + IwxDmaFree(ring.Cmd); + IwxDmaFree(ring.Bounce); + } + + static void IwxResetTxRing(IwxTxRing& ring) { + if (ring.BcTbl.Virt) memset(ring.BcTbl.Virt, 0, ring.BcTbl.Pages * 0x1000); + if (ring.Desc.Virt) memset(ring.Desc.Virt, 0, ring.Desc.Pages * 0x1000); + ring.Cur = 0; + ring.CurHw = 0; + ring.Queued = 0; + } + + // ========================================================================= + // Firmware section staging + context info (gen3) + // ========================================================================= + + static void IwxFreeFwSecDma() { + for (int i = 0; i < g_iwx.FwSecDmaCount; i++) IwxDmaFree(g_iwx.FwSecDma[i]); + g_iwx.FwSecDmaCount = 0; + } + + static void IwxFreePagingDma() { + for (int i = 0; i < g_iwx.PagingCount; i++) IwxDmaFree(g_iwx.PagingDma[i]); + g_iwx.PagingCount = 0; + } + + // Count sections until the next separator marker. + static int IwxNumSections(int start) { + int n = 0; + while (start < g_iwx.Fw.SectionCount + && g_iwx.Fw.Sections[start].DevOff != IWX_CPU1_CPU2_SEPARATOR_SECTION + && g_iwx.Fw.Sections[start].DevOff != IWX_PAGING_SEPARATOR_SECTION) { + start++; + n++; + } + return n; + } + + // Copy LMAC/UMAC/paging sections into DMA and record their addresses in the + // firmware's DRAM map. The image is laid out as + // [lmac...] SEP [umac...] SEP [paging...]. + static bool IwxInitFwSec(IwxContextInfoDram* dram) { + int lmacCnt = IwxNumSections(0); + int umacCnt = IwxNumSections(lmacCnt + 1); + int pagingCnt = IwxNumSections(lmacCnt + umacCnt + 2); + + if (lmacCnt <= 0 || umacCnt <= 0) { + KernelLogStream(ERROR, "WiFi") << "Firmware image has no LMAC/UMAC sections"; + return false; + } + if (lmacCnt + umacCnt + pagingCnt > IWX_MAX_FW_SECTIONS) return false; + + int fwCnt = 0; + for (int i = 0; i < lmacCnt; i++) { + IwxDma& d = g_iwx.FwSecDma[fwCnt]; + if (!IwxDmaAlloc(d, g_iwx.Fw.Sections[i].Len)) return false; + memcpy(d.Virt, g_iwx.Fw.Sections[i].Data, g_iwx.Fw.Sections[i].Len); + dram->lmac_img[i] = d.Phys; + g_iwx.FwSecDmaCount = ++fwCnt; + } + for (int i = 0; i < umacCnt; i++) { + IwxDma& d = g_iwx.FwSecDma[fwCnt]; + const IwxFwSection& sec = g_iwx.Fw.Sections[fwCnt + 1]; // skip separator + if (!IwxDmaAlloc(d, sec.Len)) return false; + memcpy(d.Virt, sec.Data, sec.Len); + dram->umac_img[i] = d.Phys; + g_iwx.FwSecDmaCount = ++fwCnt; + } + // Paging sections are tracked separately: unlike the LMAC/UMAC images, + // the firmware keeps demand-paging from them for as long as it runs, so + // they must survive past ALIVE. + if (pagingCnt > (int)IWX_MAX_DRAM_ENTRY) pagingCnt = IWX_MAX_DRAM_ENTRY; + IwxFreePagingDma(); + for (int i = 0; i < pagingCnt; i++) { + IwxDma& d = g_iwx.PagingDma[i]; + const IwxFwSection& sec = g_iwx.Fw.Sections[fwCnt + i + 2]; + if (!IwxDmaAlloc(d, sec.Len)) return false; + memcpy(d.Virt, sec.Data, sec.Len); + dram->virtual_img[i] = d.Phys; + g_iwx.PagingCount = i + 1; + } + return true; + } + + static void IwxSetLtr() { + uint32_t ltrVal = + IWX_CSR_LTR_LONG_VAL_AD_NO_SNOOP_REQ + | ((IWX_CSR_LTR_LONG_VAL_AD_SCALE_USEC + << IWX_CSR_LTR_LONG_VAL_AD_NO_SNOOP_SCALE_SHIFT) + & IWX_CSR_LTR_LONG_VAL_AD_NO_SNOOP_SCALE_MASK) + | ((250u << IWX_CSR_LTR_LONG_VAL_AD_NO_SNOOP_VAL_SHIFT) + & IWX_CSR_LTR_LONG_VAL_AD_NO_SNOOP_VAL_MASK) + | IWX_CSR_LTR_LONG_VAL_AD_SNOOP_REQ + | ((IWX_CSR_LTR_LONG_VAL_AD_SCALE_USEC + << IWX_CSR_LTR_LONG_VAL_AD_SNOOP_SCALE_SHIFT) + & IWX_CSR_LTR_LONG_VAL_AD_SNOOP_SCALE_MASK) + | (250u & IWX_CSR_LTR_LONG_VAL_AD_SNOOP_VAL); + + // The AX211 in this laptop is a CNVi (integrated) part of the AX210 + // family; only discrete parts take the CSR write here (the Qu-only + // HPM_*_LTR path does not apply to AX210). + (void)ltrVal; + } + + // Fill the gen3 context info and kick the firmware's self-load. + static bool IwxCtxtInfoGen3Init() { + if (!g_iwx.Fw.Iml || g_iwx.Fw.ImlLen == 0) { + KernelLogStream(ERROR, "WiFi") << "Firmware has no image loader (IML)"; + return false; + } + + if (!IwxDmaAlloc(g_iwx.ImlDma, g_iwx.Fw.ImlLen)) return false; + + auto* scratch = (IwxPrphScratch*)g_iwx.PrphScratch.Virt; + memset(scratch, 0, sizeof(*scratch)); + auto* ctrl = &scratch->ctrl_cfg; + ctrl->version.version = 0; + ctrl->version.mac_id = (uint16_t)IwxRead32(IWX_CSR_HW_REV); + ctrl->version.size = (uint16_t)(sizeof(*scratch) / 4); + ctrl->control.control_flags = IWX_PRPH_SCRATCH_RB_SIZE_4K + | IWX_PRPH_SCRATCH_MTR_MODE + | (IWX_PRPH_MTR_FORMAT_256B + & IWX_PRPH_SCRATCH_MTR_FORMAT); + ctrl->rbd_cfg.free_rbd_addr = g_iwx.RxQ.FreeDescs.Phys; + + if (!IwxInitFwSec(&scratch->dram)) { + IwxDmaFree(g_iwx.ImlDma); + IwxFreeFwSecDma(); + IwxFreePagingDma(); + return false; + } + + auto* ci = (IwxContextInfoGen3*)g_iwx.CtxtInfo.Virt; + memset(ci, 0, sizeof(*ci)); + ci->prph_info_base_addr = g_iwx.PrphInfo.Phys; + ci->prph_scratch_base_addr = g_iwx.PrphScratch.Phys; + ci->prph_scratch_size = sizeof(IwxPrphScratch); + ci->cr_head_idx_arr_base_addr = g_iwx.RxQ.Stat.Phys; + // The driver does not use the TR/CR tail arrays, but the device still + // writes there; point them at the unused half of the prph-info page. + ci->tr_tail_idx_arr_base_addr = g_iwx.PrphInfo.Phys + 0x1000 / 2; + ci->cr_tail_idx_arr_base_addr = g_iwx.PrphInfo.Phys + 3 * 0x1000 / 4; + ci->mtr_base_addr = g_iwx.CmdQ.Desc.Phys; + ci->mcr_base_addr = g_iwx.RxQ.UsedDescs.Phys; + ci->mtr_size = (uint16_t)IWX_TFD_QUEUE_CB_SIZE(IWX_TX_RING_COUNT); + ci->mcr_size = (uint16_t)IWX_RX_QUEUE_CB_SIZE(IWX_RX_MQ_RING_COUNT); + + memcpy(g_iwx.ImlDma.Virt, g_iwx.Fw.Iml, g_iwx.Fw.ImlLen); + asm volatile("" ::: "memory"); + + uint64_t paddr = g_iwx.CtxtInfo.Phys; + IwxWrite32(IWX_CSR_CTXT_INFO_ADDR, (uint32_t)(paddr & 0xffffffff)); + IwxWrite32(IWX_CSR_CTXT_INFO_ADDR + 4, (uint32_t)(paddr >> 32)); + + paddr = g_iwx.ImlDma.Phys; + IwxWrite32(IWX_CSR_IML_DATA_ADDR, (uint32_t)(paddr & 0xffffffff)); + IwxWrite32(IWX_CSR_IML_DATA_ADDR + 4, (uint32_t)(paddr >> 32)); + IwxWrite32(IWX_CSR_IML_SIZE_ADDR, g_iwx.Fw.ImlLen); + + IwxSetBits(IWX_CSR_CTXT_INFO_BOOT_CTRL, IWX_CSR_AUTO_FUNC_BOOT_ENA); + + if (!IwxNicLock()) { + IwxDmaFree(g_iwx.ImlDma); + IwxFreeFwSecDma(); + IwxFreePagingDma(); + return false; + } + IwxSetLtr(); + IwxWriteUmacPrph(IWX_UREG_CPU_INIT_RUN, 1); + IwxNicUnlock(); + return true; + } + + // ========================================================================= + // NIC init + firmware start + // ========================================================================= + + static bool IwxNicInit() { + if (!IwxApmInit()) return false; + // AX210 firmware configures the RFH itself; only interrupt coalescing + // and the shadow-register control need programming here. + IwxWrite8(IWX_CSR_INT_COALESCING, IWX_HOST_INT_TIMEOUT_DEF); + IwxSetBits(IWX_CSR_MAC_SHADOW_REG_CTRL, 0x800fffff); + return true; + } + + bool IwxStartHw() { + if (!IwxPrepareCardHw()) { + KernelLogStream(ERROR, "WiFi") << "Could not prepare card hardware"; + return false; + } + + IwxSwReset(); + + if (!IwxApmInit()) return false; + + IwxInitMsixHw(); + IwxEnableRfkillInt(); + + if (IwxCheckRfKill()) { + KernelLogStream(WARNING, "WiFi") << "Radio is disabled by hardware switch"; + g_iwx.State = IwxFwState::RfKill; + } + return true; + } + + void IwxStopDevice() { + IwxDisableInterrupts(); + + if (IwxNicLock()) { + IwxWriteUmacPrph(IWX_RFH_RXF_DMA_CFG_GEN3, 0); + for (int i = 0; i < 1000; i++) { + if (IwxReadUmacPrph(IWX_RFH_GEN_STATUS_GEN3) & IWX_RXF_DMA_IDLE) + break; + IwxDelayUs(10); + } + IwxNicUnlock(); + } + + IwxResetTxRing(g_iwx.CmdQ); + IwxResetTxRing(g_iwx.MgmtQ); + g_iwx.RxQ.Cur = 0; + if (g_iwx.RxQ.Stat.Virt) *(volatile uint16_t*)g_iwx.RxQ.Stat.Virt = 0; + + IwxClearBits(IWX_CSR_GP_CNTRL, IWX_CSR_GP_CNTRL_REG_FLAG_MAC_ACCESS_REQ); + g_iwx.NicLockCount = 0; + + IwxApmStop(); + IwxSwReset(); + + // The IVAR table is wiped by the reset; reprogram it so an RF-kill + // interrupt still lands on our vector. + IwxConfMsixHw(true); + IwxDisableInterrupts(); + IwxEnableRfkillInt(); + IwxCheckRfKill(); + IwxPrepareCardHw(); + + IwxFreeFwSecDma(); + IwxFreePagingDma(); + IwxDmaFree(g_iwx.PnvmDma); + for (int i = 0; i < g_iwx.PnvmSegs; i++) IwxDmaFree(g_iwx.PnvmSegDma[i]); + g_iwx.PnvmSegs = 0; + g_iwx.PnvmSize = 0; + } + + bool IwxStartFirmware() { + g_iwx.AliveIntr = false; + g_iwx.AliveOk = false; + g_iwx.InitComplete = 0; + + IwxWrite32(IWX_CSR_INT, ~0u); + IwxDisableInterrupts(); + + // Clear the rfkill handshake bits left over from a previous run. + IwxWrite32(IWX_CSR_UCODE_DRV_GP1_CLR, IWX_CSR_UCODE_SW_BIT_RFKILL); + IwxWrite32(IWX_CSR_UCODE_DRV_GP1_CLR, IWX_CSR_UCODE_DRV_GP1_BIT_CMD_BLOCKED); + IwxWrite32(IWX_CSR_INT, ~0u); + + if (!IwxNicInit()) { + KernelLogStream(ERROR, "WiFi") << "Unable to init NIC"; + return false; + } + + IwxEnableInterrupts(); + + if (!IwxCtxtInfoGen3Init()) return false; + + // Wait for the ALIVE notification (firmware self-load takes ~100 ms). + uint64_t start = Timekeeping::GetMilliseconds(); + while (!g_iwx.AliveIntr && Timekeeping::GetMilliseconds() - start < 2000) { + IwxProcessEvents(); + IwxDelayUs(200); + } + + IwxDmaFree(g_iwx.ImlDma); + IwxFreeFwSecDma(); + + if (!g_iwx.AliveOk) { + KernelLogStream(ERROR, "WiFi") << "Firmware did not come alive" + << (g_iwx.AliveIntr ? " (bad ALIVE response)" : " (timeout)"); + + // The firmware mirrors its boot progress into the prph-info page, + // so a stalled self-load can be told apart from one that ran but + // whose notification never reached us. + auto* info = (IwxPrphInfo*)g_iwx.PrphInfo.Virt; + KernelLogStream(INFO, "WiFi") << " boot_stage=" << base::hex + << (uint64_t)info->boot_stage_mirror + << " ipc_status=" << (uint64_t)info->ipc_status_mirror + << " sleep_notif=" << (uint64_t)info->sleep_notif << base::dec; + KernelLogStream(INFO, "WiFi") << " fh_causes=" << base::hex + << (uint64_t)IwxRead32(IWX_CSR_MSIX_FH_INT_CAUSES_AD) + << " hw_causes=" << (uint64_t)IwxRead32(IWX_CSR_MSIX_HW_INT_CAUSES_AD) + << " csr_int=" << (uint64_t)IwxRead32(IWX_CSR_INT) + << " gp_cntrl=" << (uint64_t)IwxRead32(IWX_CSR_GP_CNTRL) + << base::dec << (g_msix ? " [MSI-X]" : " [MSI]"); + KernelLogStream(INFO, "WiFi") << " rx_used_widx=" + << (uint64_t)(*(volatile uint16_t*)g_iwx.RxQ.Stat.Virt & 0xfff) + << " rx_cur=" << (uint64_t)g_iwx.RxQ.Cur; + return false; + } + + KernelLogStream(OK, "WiFi") << "Firmware alive: " << g_iwx.Fw.Version; + return true; + } + + // Point the firmware at the staged PNVM image and wait for its ack. + bool IwxLoadPnvm() { + if (g_iwx.SkuId.data[0] == 0 && g_iwx.SkuId.data[1] == 0 + && g_iwx.SkuId.data[2] == 0) + return true; // no PNVM needed + + if (g_iwx.PnvmDma.Virt == nullptr && g_iwx.Fw.PnvmData) { + // Parse the PNVM TLV blob for our SKU; a missing match is not + // fatal, the firmware then runs with built-in defaults. + IwxPnvmParse(g_iwx.Fw.PnvmData, g_iwx.Fw.PnvmLen); + } + + if (g_iwx.PnvmDma.Virt) { + auto* scratch = (IwxPrphScratch*)g_iwx.PrphScratch.Virt; + scratch->ctrl_cfg.pnvm_cfg.pnvm_base_addr = g_iwx.PnvmDma.Phys; + scratch->ctrl_cfg.pnvm_cfg.pnvm_size = g_iwx.PnvmSize; + asm volatile("" ::: "memory"); + } + + if (!IwxNicLock()) return false; + IwxWriteUmacPrph(IWX_UREG_DOORBELL_TO_ISR6, IWX_UREG_DOORBELL_TO_ISR6_PNVM); + IwxNicUnlock(); + + uint64_t start = Timekeeping::GetMilliseconds(); + while (!(g_iwx.InitComplete & 0x2) + && Timekeeping::GetMilliseconds() - start < 2000) { + IwxProcessEvents(); + IwxDelayUs(200); + } + if (!(g_iwx.InitComplete & 0x2)) { + KernelLogStream(WARNING, "WiFi") << "No PNVM complete notification"; + return false; + } + return true; + } + + // ========================================================================= + // Host command submission + // ========================================================================= + + int IwxLookupCmdVer(uint8_t group, uint8_t cmd) { + for (int i = 0; i < g_iwx.Fw.NumCmdVersions; i++) { + if (g_iwx.Fw.CmdVersions[i].group == group + && g_iwx.Fw.CmdVersions[i].cmd == cmd) + return g_iwx.Fw.CmdVersions[i].cmd_ver; + } + return -1; + } + + int IwxLookupNotifVer(uint8_t group, uint8_t cmd) { + for (int i = 0; i < g_iwx.Fw.NumCmdVersions; i++) { + if (g_iwx.Fw.CmdVersions[i].group == group + && g_iwx.Fw.CmdVersions[i].cmd == cmd) + return g_iwx.Fw.CmdVersions[i].notif_ver; + } + return -1; + } + + bool IwxSendCmd(IwxHostCmd& hcmd) { + if (g_iwx.State == IwxFwState::Error) return false; + + IwxTxRing& ring = g_iwx.CmdQ; + + g_iwx.CmdLock.Acquire(); + + uint32_t idx = ring.Cur; + uint32_t code = hcmd.Id; + + // Firmware API >= 50 rejects group-0 commands; the legacy opcodes must + // be re-tagged into LONG_GROUP. Responses come back tagged the same + // way, so notification dispatch strips the group again. + if (IwxCmdGroupId(code) == 0) + code = IWX_WIDE_ID(IWX_LONG_GROUP, code); + + uint32_t hdrLen = sizeof(IwxCmdHeaderWide); + uint32_t datasz = sizeof(IwxDeviceCmd) - hdrLen; + + if (hcmd.Len > IWX_MAX_CMD_PAYLOAD_SIZE) { + g_iwx.CmdLock.Release(); + KernelLogStream(ERROR, "WiFi") << "Firmware command too long (" + << (uint64_t)hcmd.Len << " bytes)"; + return false; + } + + uint8_t* cmdBuf; + uint64_t cmdPhys; + if (hcmd.Len > datasz) { + cmdBuf = (uint8_t*)ring.Bounce.Virt; + cmdPhys = ring.Bounce.Phys; + } else { + cmdBuf = (uint8_t*)ring.Cmd.Virt + (uint64_t)idx * sizeof(IwxDeviceCmd); + cmdPhys = ring.Cmd.Phys + (uint64_t)idx * sizeof(IwxDeviceCmd); + } + + memset(cmdBuf, 0, hdrLen + hcmd.Len); + auto* hdr = (IwxCmdHeaderWide*)cmdBuf; + hdr->opcode = IwxCmdOpcode(code); + hdr->group_id = IwxCmdGroupId(code); + hdr->qid = (uint8_t)ring.Qid; + hdr->idx = (uint8_t)idx; + hdr->length = (uint16_t)hcmd.Len; + hdr->version = 0; + if (hcmd.Len && hcmd.Data) memcpy(cmdBuf + hdrLen, hcmd.Data, hcmd.Len); + + auto* desc = &((IwxTfhTfd*)ring.Desc.Virt)[idx]; + memset(desc, 0, sizeof(*desc)); + uint32_t total = hdrLen + hcmd.Len; + uint16_t firstLen = (uint16_t)(total < IWX_FIRST_TB_SIZE ? total : IWX_FIRST_TB_SIZE); + desc->tbs[0].tb_len = firstLen; + desc->tbs[0].addr = cmdPhys; + if (total > IWX_FIRST_TB_SIZE) { + desc->tbs[1].tb_len = (uint16_t)(total - IWX_FIRST_TB_SIZE); + desc->tbs[1].addr = cmdPhys + IWX_FIRST_TB_SIZE; + desc->num_tbs = 2; + } else { + desc->num_tbs = 1; + } + asm volatile("" ::: "memory"); + + g_iwx.CmdDone = false; + g_iwx.LastCmdId = code; + g_iwx.CmdWantResp = hcmd.WantResp; + g_iwx.CmdRespLen = 0; + g_iwx.CmdIdx = idx; + + ring.Queued++; + ring.Cur = (ring.Cur + 1) % IWX_TX_RING_COUNT; + ring.CurHw = (ring.CurHw + 1) % IWX_TFD_QUEUE_SIZE_MAX_GEN3; + IwxWrite32(IWX_HBUS_TARG_WRPTR, ((uint32_t)ring.Qid << 16) | ring.CurHw); + + // Wait for the firmware's response/ack. Commands are serialized by + // CmdLock, so exactly one can be in flight and the completion is + // unambiguous. + bool ok = false; + uint64_t start = Timekeeping::GetMilliseconds(); + while (Timekeeping::GetMilliseconds() - start < 1000) { + IwxProcessEvents(); + if (g_iwx.CmdDone) { ok = true; break; } + IwxDelayUs(100); + } + + if (!ok) { + KernelLogStream(WARNING, "WiFi") << "Command 0x" << base::hex + << (uint64_t)code << base::dec << " timed out"; + if (ring.Queued > 0) ring.Queued--; + } + + g_iwx.CmdWantResp = false; + g_iwx.CmdLock.Release(); + return ok; + } + + bool IwxSendCmdPdu(uint32_t id, const void* data, uint32_t len) { + IwxHostCmd cmd; + cmd.Id = id; + cmd.Data = data; + cmd.Len = len; + return IwxSendCmd(cmd); + } + + bool IwxSendCmdStatus(uint32_t id, const void* data, uint32_t len, + uint32_t* statusOut) { + IwxHostCmd cmd; + cmd.Id = id; + cmd.Data = data; + cmd.Len = len; + cmd.WantResp = true; + if (!IwxSendCmd(cmd)) return false; + + if (g_iwx.CmdRespLen < sizeof(IwxRxPacket) + sizeof(IwxCmdResponse)) + return false; + auto* pkt = (IwxRxPacket*)g_iwx.CmdRespBuf; + auto* resp = (IwxCmdResponse*)pkt->data; + if (statusOut) *statusOut = resp->status; + return true; + } + + // ========================================================================= + // TX queue configuration (used by the connect path) + // ========================================================================= + + bool IwxEnableTxq(int staId, int qid, int tid) { + IwxTxRing& ring = g_iwx.MgmtQ; + IwxResetTxRing(ring); + ring.Qid = qid; + + int cmdVer = IwxLookupCmdVer(IWX_DATA_PATH_GROUP, IWX_SCD_QUEUE_CONFIG_CMD); + + IwxHostCmd hcmd; + hcmd.WantResp = true; + + // AX211 firmware 89 advertises v3 (DATA_PATH group). Older images + // expose no version at all and take the legacy SCD_QUEUE_CFG command. + IwxTxQueueCfgCmd cmdV0 = {}; + IwxScdQueueCfgCmd cmdV3 = {}; + if (cmdVer == 3) { + cmdV3.operation = IWX_SCD_QUEUE_ADD; + cmdV3.u.add.tfdq_dram_addr = ring.Desc.Phys; + cmdV3.u.add.bc_dram_addr = ring.BcTbl.Phys; + cmdV3.u.add.cb_size = IWX_TFD_QUEUE_CB_SIZE(IWX_TX_RING_COUNT); + cmdV3.u.add.flags = 0; + cmdV3.u.add.sta_mask = 1u << staId; + cmdV3.u.add.tid = (uint8_t)tid; + hcmd.Id = IWX_WIDE_ID(IWX_DATA_PATH_GROUP, IWX_SCD_QUEUE_CONFIG_CMD); + hcmd.Data = &cmdV3; + hcmd.Len = sizeof(cmdV3); + } else if (cmdVer <= 0) { + cmdV0.sta_id = (uint8_t)staId; + cmdV0.tid = (uint8_t)tid; + cmdV0.flags = IWX_TX_QUEUE_CFG_ENABLE_QUEUE; + cmdV0.cb_size = IWX_TFD_QUEUE_CB_SIZE(IWX_TX_RING_COUNT); + cmdV0.byte_cnt_addr = ring.BcTbl.Phys; + cmdV0.tfdq_addr = ring.Desc.Phys; + hcmd.Id = IWX_SCD_QUEUE_CFG; + hcmd.Data = &cmdV0; + hcmd.Len = sizeof(cmdV0); + } else { + KernelLogStream(WARNING, "WiFi") + << "Unsupported SCD_QUEUE_CONFIG version " << (uint64_t)cmdVer; + return false; + } + + if (!IwxSendCmd(hcmd)) return false; + + if (g_iwx.CmdRespLen < sizeof(IwxRxPacket) + sizeof(IwxTxQueueCfgRsp)) + return false; + auto* pkt = (IwxRxPacket*)g_iwx.CmdRespBuf; + auto* resp = (IwxTxQueueCfgRsp*)pkt->data; + if (resp->queue_number != qid) { + KernelLogStream(WARNING, "WiFi") << "Firmware assigned queue " + << (uint64_t)resp->queue_number << ", expected " << (uint64_t)qid; + return false; + } + return true; + } + + // ========================================================================= + // RX / notification processing + // ========================================================================= + + static bool IwxRxPacketValid(const IwxRxPacket* pkt) { + int qid = pkt->hdr.qid & ~0x80; + int idx = pkt->hdr.idx; + uint32_t code = ((uint32_t)pkt->hdr.flags << 8) | pkt->hdr.code; + return !(qid == 0 && idx == 0 && code == 0) + && pkt->len_n_flags != IWX_FH_RSCSR_FRAME_INVALID; + } + + // Capture a command response for the in-flight synchronous command. + static void IwxCaptureCmdResp(const IwxRxPacket* pkt) { + uint32_t len = sizeof(pkt->len_n_flags) + IwxRxPacketLen(pkt); + if (len > sizeof(g_iwx.CmdRespBuf)) len = sizeof(g_iwx.CmdRespBuf); + memcpy(g_iwx.CmdRespBuf, pkt, len); + g_iwx.CmdRespLen = len; + } + + static void IwxHandleAlive(const IwxRxPacket* pkt) { + uint32_t payload = IwxRxPacketPayloadLen(pkt); + g_iwx.AliveOk = false; + + if (payload >= sizeof(IwxAliveRespV6)) { + auto* r = (const IwxAliveRespV6*)pkt->data; + g_iwx.SkuId = r->sku_id; + g_iwx.UmacErrorTable = r->umac_data.dbg_ptrs.error_info_addr; + g_iwx.LmacErrorTable = r->lmac_data[0].dbg_ptrs.error_event_table_ptr; + if (r->status == IWX_ALIVE_STATUS_OK) g_iwx.AliveOk = true; + } else if (payload >= sizeof(IwxAliveRespV5)) { + auto* r = (const IwxAliveRespV5*)pkt->data; + g_iwx.SkuId = r->sku_id; + g_iwx.UmacErrorTable = r->umac_data.dbg_ptrs.error_info_addr; + g_iwx.LmacErrorTable = r->lmac_data[0].dbg_ptrs.error_event_table_ptr; + if (r->status == IWX_ALIVE_STATUS_OK) g_iwx.AliveOk = true; + } else { + KernelLogStream(WARNING, "WiFi") << "Unexpected ALIVE payload size " + << (uint64_t)payload; + } + g_iwx.AliveIntr = true; + } + + // Read dwords out of device-internal SRAM (needs the MAC awake). + static bool IwxReadMem(uint32_t addr, uint32_t* out, int dwords) { + if (!IwxNicLock()) return false; + IwxWrite32(IWX_HBUS_TARG_MEM_RADDR, addr); + asm volatile("" ::: "memory"); + for (int i = 0; i < dwords; i++) + out[i] = IwxRead32(IWX_HBUS_TARG_MEM_RDAT); + IwxNicUnlock(); + return true; + } + + // Dump the firmware's own error log after an assert. error_id identifies + // the assert and cmd_header names the last command the UMAC accepted -- + // between them they pin down which host command the firmware rejected. + void IwxDumpFwError() { + KernelLogStream(ERROR, "WiFi-FW") << "Firmware assert; last command sent: 0x" + << base::hex << (uint64_t)g_iwx.LastCmdId << base::dec; + + uint32_t base_ = g_iwx.UmacErrorTable; + if (base_ < 0x400000) { + KernelLogStream(WARNING, "WiFi-FW") << " no valid UMAC error log pointer (0x" + << base::hex << (uint64_t)base_ << base::dec << ")"; + return; + } + + IwxUmacErrorEventTable t = {}; + if (!IwxReadMem(base_, (uint32_t*)&t, sizeof(t) / sizeof(uint32_t))) { + KernelLogStream(WARNING, "WiFi-FW") << " could not read the error log"; + return; + } + + KernelLogStream(ERROR, "WiFi-FW") << " UMAC error 0x" << base::hex + << (uint64_t)t.error_id << " last host cmd 0x" << (uint64_t)t.cmd_header + << base::dec << " (valid=" << (uint64_t)t.valid << ")"; + KernelLogStream(INFO, "WiFi-FW") << " data 0x" << base::hex + << (uint64_t)t.data1 << " 0x" << (uint64_t)t.data2 + << " 0x" << (uint64_t)t.data3 + << " pc 0x" << (uint64_t)t.frame_pointer + << " sp 0x" << (uint64_t)t.stack_pointer << base::dec; + KernelLogStream(INFO, "WiFi-FW") << " umac ver " << (uint64_t)t.umac_major + << "." << (uint64_t)t.umac_minor << " blink 0x" << base::hex + << (uint64_t)t.blink1 << "/0x" << (uint64_t)t.blink2 + << " ilink 0x" << (uint64_t)t.ilink1 << "/0x" << (uint64_t)t.ilink2 + << base::dec; + } + + // Process one firmware packet out of an RX buffer. + static void IwxRxPkt(uint8_t* buf) { + const size_t minsz = sizeof(uint32_t) + sizeof(IwxCmdHeader); + auto* pkt = (IwxRxPacket*)buf; + + if (!IwxRxPacketValid(pkt)) return; + + uint32_t len = sizeof(pkt->len_n_flags) + IwxRxPacketLen(pkt); + if (len < minsz || len > IWX_RBUF_SIZE) return; + + int qid = pkt->hdr.qid; + uint32_t code = ((uint32_t)pkt->hdr.flags << 8) | pkt->hdr.code; + + // Legacy commands were re-tagged into LONG_GROUP on the way out; undo + // that so the dispatcher sees the original opcode. + if (IwxCmdGroupId(code) == IWX_LONG_GROUP) + code = IwxCmdOpcode(code); + + switch (code) { + case IWX_ALIVE: + IwxHandleAlive(pkt); + break; + case IWX_INIT_COMPLETE_NOTIF: + g_iwx.InitComplete |= 0x1; + break; + case IWX_WIDE_ID(IWX_REGULATORY_AND_NVM_GROUP, IWX_PNVM_INIT_COMPLETE): + g_iwx.InitComplete |= 0x2; + break; + case IWX_REPLY_ERROR: { + if (IwxRxPacketPayloadLen(pkt) >= 8) { + uint32_t errType = *(const uint32_t*)pkt->data; + uint8_t cmdId = pkt->data[4]; + KernelLogStream(ERROR, "WiFi") << "Firmware error 0x" + << base::hex << (uint64_t)errType << ", cmd 0x" + << (uint64_t)cmdId << base::dec; + } + g_iwx.FwErrors++; + break; + } + default: + // Everything else (scan results, NVM/MCC responses, per-command + // acks, statistics) is handled by the MVM layer. + IwxHandleNotification(pkt, buf, IWX_RBUF_SIZE); + break; + } + + // Capture the payload for a pending WANT_RESP command before signalling + // completion. Bit 7 of qid marks firmware-originated notifications, + // which never complete a command, and only the command queue carries + // command acks at all -- a data-queue packet with a colliding index + // must not release the waiter. + if (!(qid & 0x80) && (qid & ~0x80) == IWX_DQA_CMD_QUEUE) { + if (g_iwx.CmdWantResp && pkt->hdr.idx == g_iwx.CmdIdx + && !(pkt->hdr.flags & IWX_CMD_FAILED_MSK)) + IwxCaptureCmdResp(pkt); + if (pkt->hdr.idx == g_iwx.CmdIdx) { + if (g_iwx.CmdQ.Queued > 0) g_iwx.CmdQ.Queued--; + g_iwx.CmdDone = true; + } + } + } + + // Drain the used (completion) ring: for each completed RBD, process the + // firmware packet it points at and hand the buffer back. + static void IwxNotifIntr() { + if (!g_iwx.RxQ.Stat.Virt) return; + + uint16_t hw = *(volatile uint16_t*)g_iwx.RxQ.Stat.Virt & 0xfff; + hw &= (IWX_RX_MQ_RING_COUNT - 1); + + auto* used = (IwxRxCompletionDesc*)g_iwx.RxQ.UsedDescs.Virt; + + uint32_t processed = 0; + while (g_iwx.RxQ.Cur != hw && processed < IWX_RX_MQ_RING_COUNT) { + uint16_t rbid = used[g_iwx.RxQ.Cur].rbid & (IWX_RX_MQ_RING_COUNT - 1); + uint8_t* buf = g_iwx.RxQ.Buf[rbid]; + if (buf) { + IwxRxPkt(buf); + g_iwx.RxPackets++; + } + g_iwx.RxQ.Cur = (g_iwx.RxQ.Cur + 1) % IWX_RX_MQ_RING_COUNT; + processed++; + } + + if (processed == 0) return; + + // Tell the firmware how far we have consumed. The write index must be + // 8-aligned; the hardware misbehaves otherwise (documented quirk). + uint16_t widx = (hw == 0) ? (uint16_t)(IWX_RX_MQ_RING_COUNT - 1) + : (uint16_t)(hw - 1); + IwxWrite32(IWX_RFH_Q0_FRBDCB_WIDX_TRG, widx & ~7u); + } + + void IwxProcessEvents() { + if (!g_iwx.Mmio) return; + if (g_iwx.InProcessEvents) return; // never nest + g_iwx.InProcessEvents = true; + + if (g_msix) { + uint32_t fh = IwxRead32(IWX_CSR_MSIX_FH_INT_CAUSES_AD); + uint32_t hwc = IwxRead32(IWX_CSR_MSIX_HW_INT_CAUSES_AD); + IwxWrite32(IWX_CSR_MSIX_FH_INT_CAUSES_AD, fh); + IwxWrite32(IWX_CSR_MSIX_HW_INT_CAUSES_AD, hwc); + fh &= g_fhMask; + hwc &= g_hwMask; + + if (hwc & IWX_MSIX_HW_INT_CAUSES_REG_ALIVE) { + // The firmware has configured the RFH; publish RX buffers. + IwxUpdateRxDescs(); + IwxWrite32(IWX_RFH_Q0_FRBDCB_WIDX_TRG, 8); + } + + if (fh & (IWX_MSIX_FH_INT_CAUSES_Q0 | IWX_MSIX_FH_INT_CAUSES_Q1)) + IwxNotifIntr(); + + if ((fh & IWX_MSIX_FH_INT_CAUSES_FH_ERR) + || (hwc & IWX_MSIX_HW_INT_CAUSES_REG_SW_ERR) + || (hwc & IWX_MSIX_HW_INT_CAUSES_REG_SW_ERR_V2)) { + KernelLogStream(ERROR, "WiFi") << "Fatal firmware error"; + g_iwx.State = IwxFwState::Error; + g_iwx.FwErrors++; + IwxDumpFwError(); + } + if (hwc & IWX_MSIX_HW_INT_CAUSES_REG_HW_ERR) { + KernelLogStream(ERROR, "WiFi") << "Hardware error"; + g_iwx.State = IwxFwState::Error; + g_iwx.FwErrors++; + } + if (hwc & IWX_MSIX_HW_INT_CAUSES_REG_RF_KILL) { + if (IwxCheckRfKill()) { + KernelLogStream(WARNING, "WiFi") << "Radio disabled by RF-kill"; + g_iwx.State = IwxFwState::RfKill; + } + } + + IwxWrite32(IWX_CSR_MSIX_AUTOMASK_ST_AD, 1u << 0); + } else { + uint32_t r1 = IwxRead32(IWX_CSR_INT); + if (r1 != 0 && r1 != 0xffffffff && (r1 & 0xfffffff0) != 0xa5a5a5a0) { + uint32_t r2 = IwxRead32(IWX_CSR_FH_INT_STATUS); + IwxWrite32(IWX_CSR_INT, r1); + IwxWrite32(IWX_CSR_FH_INT_STATUS, r2); + + if (r1 & (1u << 0)) { // ALIVE + IwxUpdateRxDescs(); + IwxWrite32(IWX_RFH_Q0_FRBDCB_WIDX_TRG, 8); + } + if (r1 & (1u << 25)) { // SW_ERR + KernelLogStream(ERROR, "WiFi") << "Fatal firmware error"; + g_iwx.State = IwxFwState::Error; + g_iwx.FwErrors++; + IwxDumpFwError(); + } + if (r1 & ((1u << 31) | (1u << 3) | (1u << 28))) // FH_RX/SW_RX/periodic + IwxNotifIntr(); + } + } + + // The status word is updated by DMA and the interrupt may already have + // been consumed by another poller, so always sweep the ring. + IwxNotifIntr(); + + g_iwx.WorkPending = false; + g_iwx.InProcessEvents = false; + } + + // ========================================================================= + // MSI-X / MSI setup + // ========================================================================= + + static bool SetupMsix(uint8_t bus, uint8_t dev, uint8_t func) { + uint8_t cap = Pci::FindCapability(bus, dev, func, Pci::PCI_CAP_MSIX); + if (cap == 0) return false; + + Pci::DisableInterruptDelivery(bus, dev, func); + + uint16_t msgCtrl = Pci::LegacyRead16(bus, dev, func, cap + 2); + uint32_t tableOff = Pci::LegacyRead32(bus, dev, func, cap + 4); + uint8_t tableBir = (uint8_t)(tableOff & 0x7); + tableOff &= ~0x7u; + + if (tableBir != 0) { + KernelLogStream(INFO, "WiFi") + << "MSI-X table lives in BAR " << (uint64_t)tableBir + << " (only BAR0 supported); falling back to MSI"; + return false; + } + + // Program entry 0: all causes are mapped to vector 0 by IwxConfMsixHw. + volatile uint32_t* entry = (volatile uint32_t*)(g_iwx.Mmio + tableOff); + entry[0] = MSI_ADDR_BASE; // message address (BSP, physical, fixed) + entry[1] = 0; // upper address + entry[2] = MSI_VECTOR; // message data + entry[3] = 0; // vector control: unmasked + + msgCtrl |= (1 << 15); // MSI-X enable + msgCtrl &= ~(1 << 14); // function not masked + Pci::LegacyWrite16(bus, dev, func, cap + 2, msgCtrl); + + uint16_t pciCmd = Pci::LegacyRead16(bus, dev, func, (uint8_t)Pci::PCI_REG_COMMAND); + pciCmd |= Pci::PCI_CMD_INTX_DISABLE; + Pci::LegacyWrite16(bus, dev, func, (uint8_t)Pci::PCI_REG_COMMAND, pciCmd); + + Hal::RegisterIrqHandler(MSI_IRQ, HandleInterrupt); + KernelLogStream(OK, "WiFi") << "MSI-X enabled: vector " + << (uint64_t)MSI_VECTOR << " (IRQ slot " << (uint64_t)MSI_IRQ << ")"; + return true; + } + + static bool SetupMsi(uint8_t bus, uint8_t dev, uint8_t func) { + uint8_t cap = Pci::FindCapability(bus, dev, func, Pci::PCI_CAP_MSI); + if (cap == 0) return false; + + Pci::DisableInterruptDelivery(bus, dev, func); + + uint16_t msgCtrl = Pci::LegacyRead16(bus, dev, func, cap + 2); + bool is64 = (msgCtrl & (1 << 7)) != 0; + + Pci::LegacyWrite32(bus, dev, func, cap + 4, MSI_ADDR_BASE); + if (is64) { + Pci::LegacyWrite32(bus, dev, func, cap + 8, 0); + Pci::LegacyWrite16(bus, dev, func, cap + 12, MSI_VECTOR); + } else { + Pci::LegacyWrite16(bus, dev, func, cap + 8, MSI_VECTOR); + } + + msgCtrl &= ~0x70; // single message + msgCtrl |= (1 << 0); // MSI enable + Pci::LegacyWrite16(bus, dev, func, cap + 2, msgCtrl); + + uint16_t pciCmd = Pci::LegacyRead16(bus, dev, func, (uint8_t)Pci::PCI_REG_COMMAND); + pciCmd |= Pci::PCI_CMD_INTX_DISABLE; + Pci::LegacyWrite16(bus, dev, func, (uint8_t)Pci::PCI_REG_COMMAND, pciCmd); + + Hal::RegisterIrqHandler(MSI_IRQ, HandleInterrupt); + KernelLogStream(OK, "WiFi") << "MSI enabled: vector " + << (uint64_t)MSI_VECTOR << " (IRQ slot " << (uint64_t)MSI_IRQ << ")"; + return true; + } + + // ========================================================================= + // Probe + // ========================================================================= + + bool IwxProbe(const Pci::PciDevice& dev) { + if (g_iwx.State != IwxFwState::Absent) return false; + + uint64_t mmioPhys = Pci::ReadBar0(dev.Bus, dev.Device, dev.Function); + if (mmioPhys == 0) { + KernelLogStream(WARNING, "WiFi") << "Device has no memory BAR0"; + return false; + } + + // The CSR/MSI-X window is 16 KiB on these parts; map a bit more so the + // MSI-X table (which lives inside BAR0) is always covered. + constexpr uint64_t MmioSize = 0x8000; + for (uint64_t off = 0; off < MmioSize; off += 0x1000) + Memory::VMM::g_paging->MapMMIO(mmioPhys + off, Memory::HHDM(mmioPhys + off)); + + g_iwx.Mmio = (volatile uint8_t*)Memory::HHDM(mmioPhys); + g_iwx.Bus = dev.Bus; + g_iwx.Dev = dev.Device; + g_iwx.Func = dev.Function; + + Pci::EnableBusMaster(dev.Bus, dev.Device, dev.Function); + + // LTR_CONFIG may only be sent when the PCIe link actually advertises + // Latency Tolerance Reporting (Device Control 2, bit 10); firmware + // rejects it otherwise. + constexpr uint8_t PCI_CAP_PCIE = 0x10; + constexpr uint8_t PCIE_DCSR2 = 0x28; + uint8_t pcieCap = Pci::FindCapability(dev.Bus, dev.Device, dev.Function, + PCI_CAP_PCIE); + if (pcieCap) { + uint32_t dcsr2 = Pci::LegacyRead32(dev.Bus, dev.Device, dev.Function, + (uint8_t)(pcieCap + PCIE_DCSR2)); + g_iwx.LtrEnabled = (dcsr2 & (1u << 10)) != 0; + } + + // Disable the PCI retry timeout (0x41) so Tx retries cannot interfere + // with deep CPU C-states. + uint32_t reg40 = Pci::LegacyRead32(dev.Bus, dev.Device, dev.Function, 0x40); + Pci::LegacyWrite32(dev.Bus, dev.Device, dev.Function, 0x40, reg40 & ~0xff00u); + + IwxWrite32(IWX_CSR_INT_MASK, 0); + IwxWrite32(IWX_CSR_INT, ~0u); + IwxWrite32(IWX_CSR_FH_INT_STATUS, ~0u); + + g_iwx.HwRev = IwxRead32(IWX_CSR_HW_REV); + g_iwx.HwRfId = IwxRead32(IWX_CSR_HW_RF_ID); + + uint32_t macType = IWX_CSR_HW_REV_TYPE(g_iwx.HwRev); + uint32_t rfType = IWX_CSR_HW_RFID_TYPE(g_iwx.HwRfId); + + KernelLogStream(OK, "WiFi") << "Intel Wi-Fi device " << base::hex + << (uint64_t)dev.DeviceId << " at PCI " << (uint64_t)dev.Bus << ":" + << (uint64_t)dev.Device << "." << (uint64_t)dev.Function + << " hw_rev=" << (uint64_t)g_iwx.HwRev + << " mac_type=" << (uint64_t)macType + << " rf_type=" << (uint64_t)rfType << base::dec; + + if (rfType != IWX_CFG_RF_TYPE_GF) { + KernelLogStream(WARNING, "WiFi") + << "Unsupported RF type (only GF / AX211 firmware is bundled)"; + g_iwx.Mmio = nullptr; + return false; + } + + g_msix = SetupMsix(dev.Bus, dev.Device, dev.Function); + if (!g_msix && !SetupMsi(dev.Bus, dev.Device, dev.Function)) { + KernelLogStream(WARNING, "WiFi") + << "No MSI-X/MSI available; the device cannot be driven by polling alone"; + g_iwx.Mmio = nullptr; + return false; + } + + // Boot-time DMA that persists for the device's lifetime. + if (!IwxDmaAlloc(g_iwx.CtxtInfo, sizeof(IwxContextInfoGen3)) + || !IwxDmaAlloc(g_iwx.PrphScratch, sizeof(IwxPrphScratch)) + || !IwxDmaAlloc(g_iwx.PrphInfo, 4096) + || !IwxAllocRxRing() + || !IwxAllocTxRing(g_iwx.CmdQ, IWX_DQA_CMD_QUEUE) + || !IwxAllocTxRing(g_iwx.MgmtQ, IWX_DQA_MGMT_QUEUE)) { + KernelLogStream(ERROR, "WiFi") << "Could not allocate device DMA memory"; + IwxFreeRxRing(); + IwxFreeTxRing(g_iwx.CmdQ); + IwxFreeTxRing(g_iwx.MgmtQ); + IwxDmaFree(g_iwx.CtxtInfo); + IwxDmaFree(g_iwx.PrphScratch); + IwxDmaFree(g_iwx.PrphInfo); + g_iwx.Mmio = nullptr; + return false; + } + + g_iwx.State = IwxFwState::Detected; + KernelLogStream(INFO, "WiFi") + << "Transport ready; deferring firmware load until the ramdisk is mounted"; + return true; + } +} diff --git a/kernel/src/Drivers/Net/Wifi/Wifi.cpp b/kernel/src/Drivers/Net/Wifi/Wifi.cpp new file mode 100644 index 0000000..d0df900 --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/Wifi.cpp @@ -0,0 +1,439 @@ +/* + * Wifi.cpp + * Wi-Fi subsystem facade: device probe, boot-deferred firmware bring-up, + * beacon parsing and the scan-result table exposed to userspace. + * + * Firmware bring-up is deferred exactly like Bluetooth: the PCI probe runs + * before the boot filesystems are mounted, but the .ucode image lives on the + * ramdisk, so the transport is claimed early and the multi-second firmware + * load is picked up from the idle loop once VFS is up. + * + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "Wifi.hpp" +#include "Iwx.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Kt; +using namespace montauk::abi; + +namespace Drivers::Net::Wifi { + + static std::atomic g_initPending{false}; + static bool g_initialized = false; + + // ========================================================================= + // Scan result table + // ========================================================================= + + static constexpr int MAX_SCAN_RESULTS = 64; + + struct ScanEntry { + uint8_t Bssid[6]; + char Ssid[33]; + uint8_t SsidLen; + uint8_t Channel; + int8_t Rssi; + uint8_t Band; // 0 = 2.4 GHz, 1 = 5 GHz + uint8_t Security; // WifiSecurity value + uint16_t BeaconInterval; + bool Used; + }; + + static ScanEntry g_results[MAX_SCAN_RESULTS]; + static int g_resultCount = 0; + static kcp::Spinlock g_resultLock; + + static void ClearResults() { + g_resultLock.Acquire(); + for (int i = 0; i < MAX_SCAN_RESULTS; i++) g_results[i].Used = false; + g_resultCount = 0; + g_resultLock.Release(); + } + + static bool SameAddr(const uint8_t* a, const uint8_t* b) { + for (int i = 0; i < 6; i++) if (a[i] != b[i]) return false; + return true; + } + + // ========================================================================= + // Beacon / probe-response parsing + // ========================================================================= + + // Element IDs used here. + static constexpr uint8_t ELEMID_SSID = 0; + static constexpr uint8_t ELEMID_DSPARMS = 3; + static constexpr uint8_t ELEMID_RSN = 48; + static constexpr uint8_t ELEMID_VENDOR = 221; + + // Classify the network's security from the capability field and the RSN / + // WPA information elements. + // + // RSN layout: version(2) group cipher(4) pairwise count(2) suites(4*n) + // akm count(2) suites(4*n) [rsn capabilities(2)] + // A SAE (00-0F-AC:8) or FT-SAE (:9) AKM means WPA3. + static uint8_t ClassifyRsn(const uint8_t* ie, uint8_t len) { + if (len < 2) return WIFI_SEC_WPA2; + uint32_t off = 2; + if (off + 4 > len) return WIFI_SEC_WPA2; + off += 4; // group cipher suite + if (off + 2 > len) return WIFI_SEC_WPA2; + uint16_t pairwiseCount = (uint16_t)(ie[off] | (ie[off + 1] << 8)); + off += 2; + off += (uint32_t)pairwiseCount * 4; + if (off + 2 > len) return WIFI_SEC_WPA2; + uint16_t akmCount = (uint16_t)(ie[off] | (ie[off + 1] << 8)); + off += 2; + + bool sae = false; + for (uint16_t i = 0; i < akmCount && off + 4 <= len; i++, off += 4) { + if (ie[off] == 0x00 && ie[off + 1] == 0x0f && ie[off + 2] == 0xac + && (ie[off + 3] == 8 || ie[off + 3] == 9)) + sae = true; + } + return sae ? WIFI_SEC_WPA3 : WIFI_SEC_WPA2; + } + + struct ParsedBeacon { + const uint8_t* Ssid = nullptr; + uint8_t SsidLen = 0; + uint8_t Channel = 0; + uint8_t Security = WIFI_SEC_OPEN; + uint16_t BeaconInterval = 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; + + bool haveRsn = false, haveWpa = false; + + const uint8_t* ie = frame + HDR + FIXED; + const uint8_t* end = frame + len; + while (ie + 2 <= end) { + uint8_t id = ie[0]; + uint8_t ielen = ie[1]; + if (ie + 2 + ielen > end) break; + const uint8_t* body = ie + 2; + + switch (id) { + case ELEMID_SSID: + out->Ssid = body; + out->SsidLen = ielen > 32 ? 32 : ielen; + break; + case ELEMID_DSPARMS: + if (ielen >= 1) out->Channel = body[0]; + break; + case ELEMID_RSN: + haveRsn = true; + out->Security = ClassifyRsn(body, ielen); + break; + case ELEMID_VENDOR: + // WPA1: Microsoft OUI 00:50:F2, type 1. + if (ielen >= 4 && body[0] == 0x00 && body[1] == 0x50 + && body[2] == 0xf2 && body[3] == 0x01) + haveWpa = true; + break; + default: + break; + } + ie += 2 + ielen; + } + + if (!haveRsn) { + if (haveWpa) out->Security = WIFI_SEC_WPA; + else if (privacy) out->Security = WIFI_SEC_WEP; + else out->Security = WIFI_SEC_OPEN; + } + return true; + } + + // 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) { + if (len < 24) return; + + uint8_t type = (uint8_t)(frame[0] & 0x0c); + uint8_t subtype = (uint8_t)(frame[0] & 0xf0); + if (type != 0x00) return; // management frames only + + constexpr uint8_t SUBTYPE_BEACON = 0x80; + constexpr uint8_t SUBTYPE_PROBE_RESP = 0x50; + + if (subtype != SUBTYPE_BEACON && subtype != SUBTYPE_PROBE_RESP) { + // Authentication/association responses belong to the connect path. + IwxConnectRxMgmt(frame, len); + return; + } + + 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; + + g_resultLock.Acquire(); + + ScanEntry* slot = nullptr; + for (int i = 0; i < MAX_SCAN_RESULTS; i++) { + if (g_results[i].Used && SameAddr(g_results[i].Bssid, bssid)) { + slot = &g_results[i]; + break; + } + } + if (!slot) { + for (int i = 0; i < MAX_SCAN_RESULTS; i++) { + if (!g_results[i].Used) { + slot = &g_results[i]; + slot->Used = true; + g_resultCount++; + break; + } + } + } + if (!slot) { // table full + g_resultLock.Release(); + return; + } + + memcpy(slot->Bssid, bssid, 6); + slot->SsidLen = pb.SsidLen; + for (uint8_t i = 0; i < pb.SsidLen; i++) slot->Ssid[i] = (char)pb.Ssid[i]; + slot->Ssid[pb.SsidLen] = '\0'; + slot->Channel = ch; + slot->Rssi = rssiDbm; + slot->Band = ch > 14 ? 1 : 0; + slot->Security = pb.Security; + slot->BeaconInterval = pb.BeaconInterval; + + g_resultLock.Release(); + } + + // ========================================================================= + // Bring-up + // ========================================================================= + + bool Probe(const Pci::PciDevice& dev) { + if (!IwxProbe(dev)) return false; + + // Firmware comes off the ramdisk, which is not mounted during the PCI + // scan; hand the rest of the bring-up to the idle loop. + g_initPending.store(true, std::memory_order_release); + return true; + } + + static void CompleteInit() { + if (!IwxReadFirmware()) { + g_iwx.State = IwxFwState::Error; + return; + } + + if (!IwxStartHw()) { + g_iwx.State = IwxFwState::Error; + return; + } + if (g_iwx.State == IwxFwState::RfKill) { + KernelLogStream(WARNING, "WiFi") << "Radio is off; skipping firmware load"; + return; + } + + g_iwx.State = IwxFwState::Booting; + + if (!IwxRunInitUcode()) { + KernelLogStream(ERROR, "WiFi") << "Firmware initialization failed"; + IwxStopDevice(); + g_iwx.State = IwxFwState::Error; + return; + } + + // Init and runtime share one image on AX210; the device is live now. + g_iwx.State = IwxFwState::Running; + + if (!IwxInitHw()) { + KernelLogStream(ERROR, "WiFi") << "Hardware configuration failed"; + IwxStopDevice(); + g_iwx.State = IwxFwState::Error; + return; + } + + g_initialized = true; + KernelLogStream(OK, "WiFi") << "Wi-Fi adapter initialized (" + << (uint64_t)g_iwx.ChannelCount << " channels, firmware " + << g_iwx.Fw.Version << ")"; + } + + void ServiceDeferredInit() { + if (!g_initPending.load(std::memory_order_relaxed) || g_initialized) return; + if (!Fs::Vfs::IsDriveRegistered(0)) return; // ramdisk not mounted yet + + bool expected = true; + if (!g_initPending.compare_exchange_strong(expected, false, + std::memory_order_acquire)) return; + + KernelLogStream(INFO, "WiFi") << "Completing deferred Wi-Fi init in background"; + + // Reserve this CPU for the bring-up: the firmware handshakes below use + // wall-clock timeouts, and being descheduled mid-way expires them with + // almost no polling done (the same reason Bluetooth reserves here). + auto* cpu = Smp::GetCurrentCpuData(); + bool wasReserved = cpu && cpu->reservedForKernelWork; + if (cpu) cpu->reservedForKernelWork = true; + CompleteInit(); + if (cpu) cpu->reservedForKernelWork = wasReserved; + } + + void ServiceEvents() { + if (!g_iwx.Mmio) return; + if (g_iwx.WorkPending) IwxProcessEvents(); + // Firmware commands the RX path deferred (it runs under the event + // pump's reentrancy guard and cannot wait for a completion itself). + IwxConnectService(); + } + + bool IsInitialized() { return g_initialized; } + bool IsPresent() { return g_iwx.State != IwxFwState::Absent; } + + // ========================================================================= + // Public operations + // ========================================================================= + + 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 (timeoutMs < 1000) timeoutMs = 1000; + if (timeoutMs > 20000) timeoutMs = 20000; + + ClearResults(); + + if (!IwxStartScan(nullptr)) return -1; + + uint64_t start = Timekeeping::GetMilliseconds(); + while (g_iwx.ScanActive && Timekeeping::GetMilliseconds() - start < timeoutMs) { + IwxProcessEvents(); + for (int i = 0; i < 200; i++) asm volatile("pause" ::: "memory"); + } + + if (g_iwx.ScanActive) { + IwxAbortScan(); + // Drain whatever the firmware queued before the abort landed. + uint64_t t0 = Timekeeping::GetMilliseconds(); + while (Timekeeping::GetMilliseconds() - t0 < 200) IwxProcessEvents(); + } + + g_resultLock.Acquire(); + int n = 0; + for (int i = 0; i < MAX_SCAN_RESULTS && n < maxCount; i++) { + if (!g_results[i].Used) continue; + const ScanEntry& e = g_results[i]; + WifiNetwork& w = out[n]; + memset(&w, 0, sizeof(w)); + for (int k = 0; k < 32 && e.Ssid[k]; k++) w.ssid[k] = e.Ssid[k]; + memcpy(w.bssid, e.Bssid, 6); + w.channel = e.Channel; + w.rssi = e.Rssi; + w.band = e.Band; + w.security = e.Security; + w.beaconInterval = e.BeaconInterval; + n++; + } + g_resultLock.Release(); + return n; + } + + int GetInfo(WifiInfo* out) { + if (!out) return -1; + 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->fwErrors = (uint32_t)g_iwx.FwErrors; + out->connState = (uint32_t)IwxConnectState(); + + int i = 0; + for (; i < 31 && g_iwx.Fw.Version[i]; i++) out->fwVersion[i] = g_iwx.Fw.Version[i]; + out->fwVersion[i] = '\0'; + return 0; + } + + int Connect(const char* ssid, const char* password) { + if (!g_initialized || !ssid) return -1; + + // Locate the network in the most recent scan results: the firmware + // contexts need its BSSID and channel. + uint8_t bssid[6]; + uint8_t channel = 0; + bool is5 = false; + uint8_t security = WIFI_SEC_OPEN; + bool found = false; + + g_resultLock.Acquire(); + for (int i = 0; i < MAX_SCAN_RESULTS; i++) { + if (!g_results[i].Used) continue; + const ScanEntry& e = g_results[i]; + bool match = true; + for (int k = 0; k < 32; k++) { + char a = e.Ssid[k], b = ssid[k]; + if (a != b) { match = false; break; } + if (a == '\0') break; + } + if (!match) continue; + memcpy(bssid, e.Bssid, 6); + channel = e.Channel; + is5 = e.Band == 1; + security = e.Security; + found = true; + break; + } + g_resultLock.Release(); + + if (!found) { + KernelLogStream(WARNING, "WiFi") + << "Network not in scan results; run a scan first"; + return -1; + } + + // Encryption is checked before the passphrase: the WPA2/WPA3 key + // exchange (PMK derivation, EAPOL 4-way, HW key install) is not + // implemented at all, so a passphrase would not help and reporting + // "needs a passphrase" would be misleading. + if (security != WIFI_SEC_OPEN) { + KernelLogStream(WARNING, "WiFi") + << "Encrypted networks are not supported yet (open only)"; + return -2; + } + (void)password; + + return IwxConnectStart(bssid, channel, is5, ssid) ? 0 : -1; + } + + int Disconnect() { + if (!g_initialized) return -1; + IwxConnectAbort(); + return 0; + } +} diff --git a/kernel/src/Drivers/Net/Wifi/Wifi.hpp b/kernel/src/Drivers/Net/Wifi/Wifi.hpp new file mode 100644 index 0000000..c176fe3 --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/Wifi.hpp @@ -0,0 +1,40 @@ +/* + * Wifi.hpp + * Wi-Fi subsystem facade: PCI probe hook, deferred bring-up, scanning. + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include +#include +#include + +namespace Drivers::Net::Wifi { + + // PCI driver-table probe entry point. + bool Probe(const Pci::PciDevice& dev); + + // 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(); + + // Steady-state event pump (RX ring, notifications). Idle-loop callback. + void ServiceEvents(); + + bool IsInitialized(); + bool IsPresent(); + + // Run a scan and return up to maxCount networks found. Blocks (pumping + // firmware events) until the scan completes or timeoutMs elapses. + // Returns the number of entries written, or -1 on error. + int Scan(montauk::abi::WifiNetwork* out, int maxCount, uint32_t timeoutMs); + + // Fill in adapter/firmware status. + int GetInfo(montauk::abi::WifiInfo* out); + + // Association groundwork. Returns 0 when the firmware contexts came up, + // negative on failure. See IwxConnect.cpp: the 802.11 handshake itself is + // not implemented yet, so this cannot establish a usable link. + int Connect(const char* ssid, const char* password); + int Disconnect(); +} diff --git a/kernel/src/Timekeeping/ApicTimer.cpp b/kernel/src/Timekeeping/ApicTimer.cpp index ea7094f..f097e5f 100644 --- a/kernel/src/Timekeeping/ApicTimer.cpp +++ b/kernel/src/Timekeeping/ApicTimer.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -270,6 +271,12 @@ namespace Timekeeping { // the adapter is down. Drivers::USB::Bluetooth::ServiceEvents(); + // Wi-Fi mirrors the Bluetooth split: the firmware load needs the + // ramdisk, and the RX/notification ring must be drained outside hard + // interrupt context (the MSI handler only latches a flag). + Drivers::Net::Wifi::ServiceDeferredInit(); + Drivers::Net::Wifi::ServiceEvents(); + // Thermal policy records transitions during BSP maintenance; print // them from this explicitly non-interrupt idle path. Hal::CpuPower::ServiceDeferredDiagnostics(); diff --git a/programs/data/firmware/intel/iwlwifi-so-a0-gf-a0-89.ucode b/programs/data/firmware/intel/iwlwifi-so-a0-gf-a0-89.ucode new file mode 100644 index 0000000..3a3d5f3 Binary files /dev/null and b/programs/data/firmware/intel/iwlwifi-so-a0-gf-a0-89.ucode differ diff --git a/programs/data/firmware/intel/iwlwifi-so-a0-gf-a0.pnvm b/programs/data/firmware/intel/iwlwifi-so-a0-gf-a0.pnvm new file mode 100644 index 0000000..4d53d6f Binary files /dev/null and b/programs/data/firmware/intel/iwlwifi-so-a0-gf-a0.pnvm differ diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 8382aa4..69dd86c 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -219,6 +219,12 @@ namespace montauk::abi { static constexpr uint64_t SYS_DISPLAYSETMODE = 156; static constexpr uint64_t SYS_DISPLAYBRIGHTNESS = 157; + // Wi-Fi adapter control + static constexpr uint64_t SYS_WIFI_SCAN = 158; // (WifiNetwork*, maxCount, timeoutMs) -> count + static constexpr uint64_t SYS_WIFI_INFO = 159; // (WifiInfo*) -> 0, -1 if absent + static constexpr uint64_t SYS_WIFI_CONNECT = 160; // (ssid, password) -> 0, <0 on error + static constexpr uint64_t SYS_WIFI_DISCONNECT = 161; // () -> 0 + // Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM). static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz static constexpr int SDR_PARAM_SAMPLE_RATE = 1; // sample rate, Hz @@ -564,6 +570,46 @@ namespace montauk::abi { uint32_t _pad2; }; + // Wi-Fi security suites reported in WifiNetwork.security. + static constexpr uint8_t WIFI_SEC_OPEN = 0; + static constexpr uint8_t WIFI_SEC_WEP = 1; + static constexpr uint8_t WIFI_SEC_WPA = 2; + static constexpr uint8_t WIFI_SEC_WPA2 = 3; + static constexpr uint8_t WIFI_SEC_WPA3 = 4; + + // Adapter states reported in WifiInfo.state. + static constexpr uint8_t WIFI_STATE_ABSENT = 0; // no device + static constexpr uint8_t WIFI_STATE_DETECTED = 1; // waiting for firmware load + static constexpr uint8_t WIFI_STATE_BOOTING = 2; + static constexpr uint8_t WIFI_STATE_RUNNING = 3; + static constexpr uint8_t WIFI_STATE_ERROR = 4; + static constexpr uint8_t WIFI_STATE_RFKILL = 5; // radio disabled in hardware + + // One scanned network (returned by SYS_WIFI_SCAN). + struct WifiNetwork { + char ssid[36]; // NUL-terminated; empty for hidden networks + uint8_t bssid[6]; + uint8_t channel; + int8_t rssi; // dBm + uint8_t band; // 0 = 2.4 GHz, 1 = 5 GHz + uint8_t security; // WIFI_SEC_* + uint16_t beaconInterval; // TU + }; + + // Adapter status (returned by SYS_WIFI_INFO). + struct WifiInfo { + uint8_t mac[6]; + uint8_t present; // 1 if a supported device was found + uint8_t state; // WIFI_STATE_* + uint8_t scanning; + uint8_t bands; // bit0 = 2.4 GHz, bit1 = 5 GHz + uint16_t channels; // usable channels after regulatory filtering + char fwVersion[32]; + uint64_t rxPackets; + uint32_t fwErrors; + uint32_t connState; // 0 idle, >0 connection setup in progress + }; + struct ThermalInfo { char name[32]; // short zone name (e.g. "THRM", "TZ00") int32_t temperature; // tenths of degrees Celsius, or -1 if unavailable diff --git a/programs/include/libc/montauk.h b/programs/include/libc/montauk.h index 54186df..dafaec9 100644 --- a/programs/include/libc/montauk.h +++ b/programs/include/libc/montauk.h @@ -181,6 +181,10 @@ extern "C" { #define MTK_SYS_DISPLAYMODES 155 #define MTK_SYS_DISPLAYSETMODE 156 #define MTK_SYS_DISPLAYBRIGHTNESS 157 +#define MTK_SYS_WIFI_SCAN 158 +#define MTK_SYS_WIFI_INFO 159 +#define MTK_SYS_WIFI_CONNECT 160 +#define MTK_SYS_WIFI_DISCONNECT 161 /* @SYSCALLS-END */ #define MTK_SOCK_TCP 1 diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index d19b054..9cce5c1 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -596,6 +596,27 @@ namespace montauk { return (int)syscall1(montauk::abi::SYS_BTINFO, (uint64_t)buf); } + // Wi-Fi. scan() runs a full channel sweep and blocks until it finishes or + // timeoutMs elapses, then fills buf with the networks seen; it returns the + // number of entries written, or -1 when no adapter is ready. + inline int wifi_scan(montauk::abi::WifiNetwork* buf, int maxCount, + uint32_t timeoutMs) { + return (int)syscall3(montauk::abi::SYS_WIFI_SCAN, (uint64_t)buf, + (uint64_t)maxCount, (uint64_t)timeoutMs); + } + inline int wifi_info(montauk::abi::WifiInfo* out) { + return (int)syscall1(montauk::abi::SYS_WIFI_INFO, (uint64_t)out); + } + // Association is groundwork only: open networks bring the firmware + // contexts up, encrypted ones are rejected with -2. + inline int wifi_connect(const char* ssid, const char* password) { + return (int)syscall2(montauk::abi::SYS_WIFI_CONNECT, (uint64_t)ssid, + (uint64_t)password); + } + inline int wifi_disconnect() { + return (int)syscall0(montauk::abi::SYS_WIFI_DISCONNECT); + } + // Software-defined radio (Rx). Receivers are identified by index [0, count); // open() returns a handle used by the rest of the calls. Samples are read // as interleaved 8-bit unsigned I/Q (CU8) from the device's ring buffer. diff --git a/programs/man/wifi.1 b/programs/man/wifi.1 new file mode 100644 index 0000000..ea7bcfc --- /dev/null +++ b/programs/man/wifi.1 @@ -0,0 +1,74 @@ +.TH WIFI 1 +.SH NAME + wifi - scan for wireless networks and inspect the Wi-Fi adapter + +.SH SYNOPSIS + wifi [scan [seconds]] + wifi info + wifi debug + wifi connect [passphrase] + wifi disconnect + +.SH DESCRIPTION + Drives the Intel Wi-Fi adapter. With no arguments, runs a five + second scan and lists the networks found, strongest signal first. + + The driver supports Intel AX210-family devices whose RF type is + GF (marketed as AX211), which is what the bundled firmware image + covers. Other Intel wireless parts are left unclaimed. + +.SH COMMANDS + scan [seconds] + Sweep every regulatory-permitted channel and report the + networks seen. The duration is clamped to 1-20 seconds and + defaults to 5. + + info + Adapter status: MAC address, firmware version, supported + bands, usable channel count and whether a scan is running. + + debug + Everything info prints, plus RX/error counters, a scan with + per-BSS detail (BSSID, channel, RSSI, security) and band and + security tallies. Use this when a scan finds nothing. + + connect [passphrase] + Brings the firmware contexts (PHY, MAC, binding, station) up + for a network from the last scan. The 802.11 authentication + and association exchange is not implemented, so this does not + produce a usable link; encrypted networks are refused outright. + + disconnect + Tears those contexts back down. + +.SH OUTPUT + SSID SIGNAL RSSI CH BAND SECURITY + ------------------------------------------------------------------- + homenet [####] -42 6 2.4GHz WPA2 + homenet-5g [###.] -61 44 5GHz WPA3 + guest [##..] -74 11 2.4GHz open + +.SH FIRMWARE + The adapter needs firmware from the ramdisk: + + 0:/os/firmware/intel/iwlwifi-so-a0-gf-a0-.ucode + 0:/os/firmware/intel/iwlwifi-so-a0-gf-a0.pnvm + + Loading happens shortly after boot, off the boot-critical path, + so "wifi info" may briefly report "detected (firmware not loaded + yet)". The PNVM file carries regulatory data; without it the + firmware falls back to conservative built-in limits. + +.SH DIAGNOSTICS + no supported Wi-Fi adapter found + No matching device, or its RF type is not GF. + + radio disabled (RF-kill) + The hardware kill switch is off. No scanning is possible. + + error + Firmware load or configuration failed; klog(1) has the + detail under the WiFi and WiFi-FW tags. + +.SH SEE ALSO + ifconfig(1), klog(1), dhcp(1), syscalls(2) diff --git a/programs/src/wifi/main.cpp b/programs/src/wifi/main.cpp new file mode 100644 index 0000000..4559cb5 --- /dev/null +++ b/programs/src/wifi/main.cpp @@ -0,0 +1,387 @@ +/* + * main.cpp + * wifi - Wi-Fi scanning and adapter diagnostics. + * + * Usage: + * wifi scan and list nearby networks + * wifi scan [seconds] scan for a specific duration (default 5, max 20) + * wifi info adapter, firmware and radio status + * wifi debug info plus raw counters, repeated scan detail + * wifi connect bring up the firmware contexts for an open network + * wifi disconnect tear those contexts back down + * + * Copyright (c) 2026 Daniel Hammer +*/ + +#include +#include + +using namespace montauk; + +// --------------------------------------------------------------------------- +// Formatting helpers (freestanding; no libc printf here) +// --------------------------------------------------------------------------- + +static void put_u64(uint64_t n) { + char buf[24]; + int i = 0; + if (n == 0) { putchar('0'); return; } + while (n) { buf[i++] = (char)('0' + (n % 10)); n /= 10; } + while (i) putchar(buf[--i]); +} + +static void put_i64(int64_t n) { + if (n < 0) { putchar('-'); n = -n; } + put_u64((uint64_t)n); +} + +// Right-align an unsigned value in a field of `width` characters. +static void put_u64_pad(uint64_t n, int width) { + int digits = 1; + for (uint64_t t = n; t >= 10; t /= 10) digits++; + for (int i = digits; i < width; i++) putchar(' '); + put_u64(n); +} + +static void put_hex2(uint8_t v) { + const char* hex = "0123456789abcdef"; + putchar(hex[(v >> 4) & 0xF]); + putchar(hex[v & 0xF]); +} + +static void put_mac(const uint8_t* m) { + for (int i = 0; i < 6; i++) { + if (i) putchar(':'); + put_hex2(m[i]); + } +} + +static void put_padded_str(const char* s, int width) { + int n = 0; + while (s[n] && n < width) { putchar(s[n]); n++; } + for (; n < width; n++) putchar(' '); +} + +static const char* security_name(uint8_t sec) { + switch (sec) { + case abi::WIFI_SEC_OPEN: return "open"; + case abi::WIFI_SEC_WEP: return "WEP"; + case abi::WIFI_SEC_WPA: return "WPA"; + case abi::WIFI_SEC_WPA2: return "WPA2"; + case abi::WIFI_SEC_WPA3: return "WPA3"; + default: return "?"; + } +} + +static const char* state_name(uint8_t state) { + switch (state) { + case abi::WIFI_STATE_ABSENT: return "no adapter"; + case abi::WIFI_STATE_DETECTED: return "detected (firmware not loaded yet)"; + case abi::WIFI_STATE_BOOTING: return "loading firmware"; + case abi::WIFI_STATE_RUNNING: return "ready"; + case abi::WIFI_STATE_ERROR: return "error"; + case abi::WIFI_STATE_RFKILL: return "radio disabled (RF-kill)"; + default: return "unknown"; + } +} + +// A rough signal-quality bar from the RSSI: -50 dBm and up is excellent, +// -90 dBm and below is unusable. +static void put_signal_bar(int8_t rssi) { + int bars = 0; + if (rssi >= -55) bars = 4; + else if (rssi >= -67) bars = 3; + else if (rssi >= -78) bars = 2; + else if (rssi >= -88) bars = 1; + + putchar('['); + for (int i = 0; i < 4; i++) putchar(i < bars ? '#' : '.'); + putchar(']'); +} + +// --------------------------------------------------------------------------- +// Argument helpers +// --------------------------------------------------------------------------- + +static uint64_t parse_u64(const char* s) { + uint64_t v = 0; + while (*s >= '0' && *s <= '9') { v = v * 10 + (uint64_t)(*s - '0'); s++; } + return v; +} + +static bool next_token(const char** rest, char* tok, int cap) { + const char* s = skip_spaces(*rest); + if (*s == '\0') { *rest = s; return false; } + int i = 0; + while (*s && *s != ' ' && i < cap - 1) tok[i++] = *s++; + tok[i] = '\0'; + *rest = s; + return true; +} + +// --------------------------------------------------------------------------- +// Commands +// --------------------------------------------------------------------------- + +static abi::WifiNetwork g_nets[64]; + +static bool require_adapter(abi::WifiInfo& info) { + if (wifi_info(&info) != 0 || !info.present) { + print("wifi: no supported Wi-Fi adapter found.\n"); + print(" Only Intel AX210-family devices with an RF type of GF " + "(AX211) are supported.\n"); + return false; + } + if (info.state != abi::WIFI_STATE_RUNNING) { + print("wifi: adapter is not ready ("); print(state_name(info.state)); + print(").\n"); + if (info.state == abi::WIFI_STATE_DETECTED) + print(" Firmware loads shortly after boot; try again in a moment.\n"); + else if (info.state == abi::WIFI_STATE_ERROR) + print(" Check the kernel log (klog) for firmware errors.\n"); + return false; + } + return true; +} + +static void print_info(const abi::WifiInfo& info, bool verbose) { + print("Wi-Fi adapter\n"); + print(" status : "); print(state_name(info.state)); print("\n"); + print(" MAC : "); put_mac(info.mac); print("\n"); + print(" firmware : "); + print(info.fwVersion[0] ? info.fwVersion : "(not loaded)"); + print("\n"); + print(" bands : "); + if (info.bands & 1) print("2.4GHz "); + if (info.bands & 2) print("5GHz"); + if (!info.bands) print("(unknown)"); + print("\n"); + print(" channels : "); put_u64(info.channels); + print(" usable after regulatory filtering\n"); + print(" scanning : "); print(info.scanning ? "yes" : "no"); print("\n"); + + if (verbose) { + print(" rx packets : "); put_u64(info.rxPackets); print("\n"); + print(" fw errors : "); put_u64(info.fwErrors); print("\n"); + print(" conn state : "); put_u64(info.connState); + print(" (0 = idle)\n"); + } +} + +static int cmd_scan(uint32_t seconds, bool verbose) { + abi::WifiInfo info; + if (!require_adapter(info)) return 1; + + uint32_t timeoutMs = seconds * 1000; + if (timeoutMs < 1000) timeoutMs = 5000; + if (timeoutMs > 20000) timeoutMs = 20000; + + print("Scanning for up to "); + put_u64(timeoutMs / 1000); + print(" seconds...\n"); + + int n = wifi_scan(g_nets, 64, timeoutMs); + if (n < 0) { + print("wifi: scan failed.\n"); + return 1; + } + if (n == 0) { + print("No networks found.\n"); + return 0; + } + + // Strongest signal first (small n, so a simple selection sort is fine). + for (int i = 0; i < n - 1; i++) { + int best = i; + for (int j = i + 1; j < n; j++) + if (g_nets[j].rssi > g_nets[best].rssi) best = j; + if (best != i) { + abi::WifiNetwork tmp = g_nets[i]; + g_nets[i] = g_nets[best]; + g_nets[best] = tmp; + } + } + + print("\n"); + print("SSID SIGNAL RSSI CH BAND SECURITY\n"); + print("-------------------------------------------------------------------\n"); + + for (int i = 0; i < n; i++) { + const abi::WifiNetwork& w = g_nets[i]; + put_padded_str(w.ssid[0] ? w.ssid : "(hidden)", 32); + putchar(' '); + put_signal_bar(w.rssi); + print(" "); + put_i64(w.rssi); + print(w.rssi > -100 ? " " : " "); + put_u64_pad(w.channel, 3); + print(" "); + print(w.band ? "5GHz " : "2.4GHz"); + print(" "); + print(security_name(w.security)); + print("\n"); + + if (verbose) { + print(" bssid "); put_mac(w.bssid); + print(" beacon interval "); put_u64(w.beaconInterval); + print(" TU\n"); + } + } + + print("\n"); + put_u64((uint64_t)n); + print(n == 1 ? " network found.\n" : " networks found.\n"); + return 0; +} + +static int cmd_info(bool verbose) { + abi::WifiInfo info; + if (wifi_info(&info) != 0 && !info.present) { + print("wifi: no supported Wi-Fi adapter found.\n"); + return 1; + } + print_info(info, verbose); + return 0; +} + +static int cmd_debug() { + abi::WifiInfo info; + if (wifi_info(&info) != 0 && !info.present) { + print("wifi: no supported Wi-Fi adapter found.\n"); + return 1; + } + + print("=== Wi-Fi diagnostics ===\n\n"); + print_info(info, true); + + if (info.state != abi::WIFI_STATE_RUNNING) { + print("\nAdapter is not ready; skipping the scan test.\n"); + print("Kernel-side detail is in the klog (look for the WiFi/WiFi-FW tags).\n"); + return 0; + } + + print("\nRunning a 5 second scan and reporting per-BSS detail...\n"); + int n = wifi_scan(g_nets, 64, 5000); + if (n < 0) { + print("scan failed\n"); + return 1; + } + + int open = 0, wpa2 = 0, wpa3 = 0, other = 0, band24 = 0, band5 = 0; + for (int i = 0; i < n; i++) { + switch (g_nets[i].security) { + case abi::WIFI_SEC_OPEN: open++; break; + case abi::WIFI_SEC_WPA2: wpa2++; break; + case abi::WIFI_SEC_WPA3: wpa3++; break; + default: other++; break; + } + if (g_nets[i].band) band5++; else band24++; + } + + print("\nBSS count : "); put_u64((uint64_t)n); print("\n"); + print(" 2.4 GHz : "); put_u64((uint64_t)band24); print("\n"); + print(" 5 GHz : "); put_u64((uint64_t)band5); print("\n"); + print(" open : "); put_u64((uint64_t)open); print("\n"); + print(" WPA2 : "); put_u64((uint64_t)wpa2); print("\n"); + print(" WPA3 : "); put_u64((uint64_t)wpa3); print("\n"); + print(" other : "); put_u64((uint64_t)other); print("\n"); + + if (n > 0) { + print("\nPer-BSS detail:\n"); + for (int i = 0; i < n; i++) { + const abi::WifiNetwork& w = g_nets[i]; + print(" "); put_mac(w.bssid); + print(" ch "); put_u64_pad(w.channel, 3); + print(" "); put_i64(w.rssi); print(" dBm "); + print(security_name(w.security)); + print(" "); + print(w.ssid[0] ? w.ssid : "(hidden)"); + print("\n"); + } + } + + // Re-read the counters so the caller can see whether frames actually moved. + abi::WifiInfo after; + if (wifi_info(&after) == 0) { + print("\nRX packets: "); put_u64(info.rxPackets); + print(" -> "); put_u64(after.rxPackets); + print(" firmware errors: "); put_u64(after.fwErrors); + print("\n"); + } + return 0; +} + +static int cmd_connect(const char* ssid, const char* password) { + abi::WifiInfo info; + if (!require_adapter(info)) return 1; + + print("Connecting to \""); print(ssid); print("\"...\n"); + int rc = wifi_connect(ssid, password); + if (rc == 0) { + print("Firmware contexts are up.\n"); + print("NOTE: the 802.11 authentication exchange is not implemented yet,\n"); + print(" so this does not establish a usable link.\n"); + return 0; + } + if (rc == -2) { + print("wifi: \""); print(ssid); print("\" is encrypted (WPA2/WPA3).\n"); + print(" Only open networks can be joined so far - the key exchange\n"); + print(" is not implemented yet. Scanning is unaffected.\n"); + return 1; + } + print("wifi: \""); print(ssid); print("\" was not found in the last scan.\n"); + print(" Run \"wifi scan\" first, and check the SSID spelling.\n"); + return 1; +} + +static void usage() { + print("usage: wifi [command]\n\n"); + print(" scan [seconds] scan and list nearby networks (default 5)\n"); + print(" info adapter, firmware and radio status\n"); + print(" debug diagnostics plus per-BSS scan detail\n"); + print(" connect bring up firmware contexts (open networks)\n"); + print(" disconnect tear those contexts down\n\n"); + print("With no command, runs a 5 second scan.\n"); +} + +extern "C" void _start() { + char args[192]; + int alen = getargs(args, sizeof(args)); + const char* rest = (alen > 0) ? args : ""; + + char cmd[32]; + if (!next_token(&rest, cmd, sizeof(cmd))) { + exit(cmd_scan(5, false)); + } + + if (streq(cmd, "scan")) { + char tok[16]; + uint32_t secs = 5; + if (next_token(&rest, tok, sizeof(tok))) secs = (uint32_t)parse_u64(tok); + exit(cmd_scan(secs, false)); + } else if (streq(cmd, "info")) { + exit(cmd_info(false)); + } else if (streq(cmd, "debug")) { + exit(cmd_debug()); + } else if (streq(cmd, "connect")) { + char ssid[40], pass[80]; + if (!next_token(&rest, ssid, sizeof(ssid))) { + print("wifi: connect needs an SSID\n"); + exit(1); + } + pass[0] = '\0'; + next_token(&rest, pass, sizeof(pass)); + exit(cmd_connect(ssid, pass)); + } else if (streq(cmd, "disconnect")) { + wifi_disconnect(); + print("Disconnected.\n"); + exit(0); + } else if (streq(cmd, "help") || streq(cmd, "-h") || streq(cmd, "--help")) { + usage(); + exit(0); + } + + print("wifi: unknown command \""); print(cmd); print("\"\n\n"); + usage(); + exit(1); +}