diff --git a/examples/doctor/main.cpp b/examples/doctor/main.cpp index b81885f..728baca 100644 --- a/examples/doctor/main.cpp +++ b/examples/doctor/main.cpp @@ -292,6 +292,21 @@ int main(int argc, char **argv) { /* ---- report ---- */ std::printf("\n== adapter doctor ==\n"); std::printf("bring-up: %s\n", yn(in.init_completed)); + { + /* Per-unit identity. In the report so it can be checked against the netdev + * name the vendor driver would give the same dongle (`wlx`) — a + * one-line confirmation that the EFUSE offset is right on a chip nobody + * has measured yet. Attempted even after a failed bring-up: on Jaguar1 the + * EEPROM map may already be in by then, and false degrades to the + * unavailable line either way. */ + uint8_t mac[6]; + if (dev->GetPermanentMacAddress(mac)) + std::printf("efuse MAC: %02x:%02x:%02x:%02x:%02x:%02x\n", mac[0], + mac[1], mac[2], mac[3], mac[4], mac[5]); + else + std::printf("efuse MAC: unavailable (unsupported chip, or " + "unprogrammed)\n"); + } if (in.efuse.supported) { std::printf("efuse stability: %d reads, %d mismatched, %d bad-id " "(last id 0x%04x%s)\n", diff --git a/src/IRtlDevice.h b/src/IRtlDevice.h index c774c4d..0546ae2 100644 --- a/src/IRtlDevice.h +++ b/src/IRtlDevice.h @@ -329,6 +329,29 @@ class IRtlDevice { virtual SelectedChannel GetSelectedChannel() = 0; + /* Per-unit hardware identity: the MAC address burned in the adapter's EFUSE — + * the same value the vendor kernel driver programs into the netdev, and hence + * the thing udev's predictable name `wlx` is derived from. + * + * Why this is on the interface at all: a consumer that keeps per-adapter state + * (a measured TX-power curve, a calibration, anything tied to one specific + * dongle) has to answer "is this the same physical adapter I measured last + * time?" across a re-plug, a reboot and a port change. Neither key otherwise + * available can answer it. A USB bus path identifies a *port*, not a device. + * And the USB serial descriptor is not unique: on every RTL88x2 part measured + * it is the constant placeholder "123456", burned into the EFUSE a few bytes + * from the MAC itself. Keying on it is worse than useless — two adapters in + * one host would share state and silently apply each other's measurements. + * + * `out` receives the 6 bytes in wire order. Returns false where the chip is + * unsupported (the default), or the EFUSE value is unprogrammed/unreadable — + * callers must treat false as "no stable identity available" rather than + * substituting a weaker one silently. The default is a defined answer, not a + * silent no-op: Jaguar2 and Kestrel are expected follow-ups, not permanent + * gaps (HalMAC has the efuse APIs; EFUSE_USB_MAC_ADDR_8852B is already in + * kestrel/MacRegAx.h — each just needs a hardware-verified route here). */ + virtual bool GetPermanentMacAddress(uint8_t /*out*/[6]) { return false; } + /* Read the 64-bit hardware TSF (Timing Synchronization Function) timer — the * 802.11 MAC's free-running microsecond clock (REG_TSFTR). It runs off the * chip's crystal and is latched into every RX descriptor at receive diff --git a/src/jaguar1/RtlJaguarDevice.cpp b/src/jaguar1/RtlJaguarDevice.cpp index 70829fa..e82cf3b 100644 --- a/src/jaguar1/RtlJaguarDevice.cpp +++ b/src/jaguar1/RtlJaguarDevice.cpp @@ -385,6 +385,14 @@ void RtlJaguarDevice::measure_idle_noise_floor() { SelectedChannel RtlJaguarDevice::GetSelectedChannel() { return _channel; } +bool RtlJaguarDevice::GetPermanentMacAddress(uint8_t out[6]) { + /* The read, the per-chip offsets and the unprogrammed-value rejection all + * already existed in EepromManager; only the route to a caller was missing. */ + if (out == nullptr || !_eepromManager) + return false; + return _eepromManager->GetMacAddress(out); +} + uint64_t RtlJaguarDevice::ReadTsf() { /* REG_TSFTR (0x0560) = TSF low 32, 0x0564 = TSF high 32. Read hi, lo, hi * again and retry the pair once if the low word wrapped between the reads. */ diff --git a/src/jaguar1/RtlJaguarDevice.h b/src/jaguar1/RtlJaguarDevice.h index f59cbda..062d19d 100644 --- a/src/jaguar1/RtlJaguarDevice.h +++ b/src/jaguar1/RtlJaguarDevice.h @@ -270,6 +270,10 @@ class RtlJaguarDevice : public IRtlDevice { devourer::AmpduMode GetAmpduMode() override { return _ampdu; } devourer::TxStats GetTxStats() override { return _device.GetTxStats(); } SelectedChannel GetSelectedChannel() override; + /* EFUSE MAC via EepromManager (offsets from upstream hal_pg.h: 8812AU 0xD7, + * 8814AU 0xD8, 8821AU 0x107). The EEPROM map is already read during bring-up, + * so this is a lookup, not a chip access. */ + bool GetPermanentMacAddress(uint8_t out[6]) override; uint64_t ReadTsf() override; /* Hardware-timed beacon (IRtlDevice contract): download the beacon MPDU to diff --git a/src/jaguar3/HalJaguar3.cpp b/src/jaguar3/HalJaguar3.cpp index 0e76fb1..66abdca 100644 --- a/src/jaguar3/HalJaguar3.cpp +++ b/src/jaguar3/HalJaguar3.cpp @@ -1,5 +1,6 @@ #include "HalJaguar3.h" #include +#include #include #include @@ -730,13 +731,58 @@ void HalJaguar3::read_efuse_logical_map(uint8_t *map, size_t len) { } } +/* Unprogrammed EFUSE reads back all-0xFF; an all-zero result means the map was + * never populated. Neither is an identity. (Same rule EepromManager applies on + * Jaguar1.) */ +bool HalJaguar3::mac_programmed(const uint8_t m[6]) { + bool all_ff = true, all_zero = true; + for (int i = 0; i < 6; ++i) { + if (m[i] != 0xFF) all_ff = false; + if (m[i] != 0x00) all_zero = false; + } + return !all_ff && !all_zero; +} + void HalJaguar3::cache_efuse_8822e() { if (_variant != ChipVariant::C8822E) return; - read_efuse_logical_map(_efuse_cache, sizeof(_efuse_cache)); + /* One walk, decoded far enough to reach the MAC, then split: the low 0x100 + * is the existing cache, and the 6 bytes at 0x157 are the per-unit identity. + * Done here rather than on demand because the 8822E OTP is not reliably + * readable after TX/coex bring-up — the same constraint this cache exists + * for. */ + uint8_t map[kMacLogicalOff + 0x10] = {}; + read_efuse_logical_map(map, sizeof(map)); + std::memcpy(_efuse_cache, map, sizeof(_efuse_cache)); _efuse_cache_valid = true; + std::memcpy(_perm_mac, map + kMacLogicalOff, sizeof(_perm_mac)); + _perm_mac_valid = mac_programmed(_perm_mac); _logger->info("Jaguar3(8822e): efuse decoded (0x22={:x} 0x4c={:x} 0xca={:x})", _efuse_cache[0x22], _efuse_cache[0x4c], _efuse_cache[0xca]); + if (_perm_mac_valid) + _logger->info("Jaguar3(8822e): efuse MAC {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + _perm_mac[0], _perm_mac[1], _perm_mac[2], _perm_mac[3], + _perm_mac[4], _perm_mac[5]); +} + +bool HalJaguar3::perm_mac(uint8_t out[6]) { + if (out == nullptr) + return false; + if (!_perm_mac_valid && !_perm_mac_probed && _variant == ChipVariant::C8822C) { + /* 8822C OTP stays readable post-bring-up (it is why probe_efuse_map is + * 8822C-only), so decode on demand and keep the result — including a + * negative one: the map walk is real register I/O under the device lock, + * and an unprogrammed EFUSE stays unprogrammed, so one attempt is enough. */ + _perm_mac_probed = true; + uint8_t map[kMacLogicalOff + 0x10] = {}; + read_efuse_logical_map(map, sizeof(map)); + std::memcpy(_perm_mac, map + kMacLogicalOff, sizeof(_perm_mac)); + _perm_mac_valid = mac_programmed(_perm_mac); + } + if (!_perm_mac_valid) + return false; + std::memcpy(out, _perm_mac, sizeof(_perm_mac)); + return true; } uint8_t HalJaguar3::read_efuse_rfe_type() { diff --git a/src/jaguar3/HalJaguar3.h b/src/jaguar3/HalJaguar3.h index d1fa7a4..505a7b2 100644 --- a/src/jaguar3/HalJaguar3.h +++ b/src/jaguar3/HalJaguar3.h @@ -79,6 +79,15 @@ class HalJaguar3 { * stability probe there would flag healthy units. Returns false on 8822E. */ bool probe_efuse_map(uint8_t *map, size_t len); + /* Per-unit MAC at logical EFUSE offset 0x157 on this generation (why the MAC + * is the identity key at all: IRtlDevice::GetPermanentMacAddress). + * + * On 8822E this is served from the value captured during rtw_hal_init: the + * OTP is not reliably readable after TX/coex bring-up by design, the same + * reason _efuse_cache exists. On 8822C the map is read on demand, one + * attempt. false when unprogrammed (all-0xFF) or unread (all-0x00). */ + bool perm_mac(uint8_t out[6]); + /* Outcome of the fw download run by the last rtw_hal_init — forwarded from * the DLFW state machine's real hardware boundaries (checksum-ready bits vs * the 0xC078 boot handshake; see HalmacJaguar3Fw::boot_status). */ @@ -209,6 +218,16 @@ class HalJaguar3 { uint8_t _efuse_cache[0x100]; bool _efuse_cache_valid = false; + /* EFUSE MAC (see perm_mac). Logical 0x157 sits past _efuse_cache, so the + * 8822E capture decodes into a larger local buffer and copies both out — + * one OTP walk, and _efuse_cache keeps its size so the health probe's + * compare surface is unchanged. */ + static constexpr uint16_t kMacLogicalOff = 0x0157; + static bool mac_programmed(const uint8_t m[6]); + uint8_t _perm_mac[6] = {}; + bool _perm_mac_valid = false; + bool _perm_mac_probed = false; /* 8822C on-demand walk: one attempt only */ + RtlAdapter _device; devourer::DeviceConfig _cfg; /* skip_iqk + calibration forward */ Logger_t _logger; diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index e443ff5..e12fffa 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -2081,6 +2081,27 @@ size_t RtlJaguar3Device::build_tx_block(const uint8_t *packet, size_t length, SelectedChannel RtlJaguar3Device::GetSelectedChannel() { return _channel; } +bool RtlJaguar3Device::GetPermanentMacAddress(uint8_t out[6]) { + /* Serialize vs the coex tick, like every other entry point that can touch + * the EFUSE or registers: on 8822C perm_mac may run a fresh on-demand map + * decode, which is real register I/O. (On 8822E it is a cached lookup and + * the lock is uncontended.) The lock also makes the lazy fill of + * _perm_mac/_perm_mac_valid single-writer. */ + std::lock_guard lk(_reg_mu); + /* The 8822C walk is thousands of control-IN reads, any of which can throw on + * a USB glitch (rtw_read's std::ios_base::failure) — most likely exactly when + * a caller probes identity on a dead or unpowered adapter. The contract folds + * that into its false ("no stable identity available"), it must not escape as + * an exception from an accessor. The one-attempt latch has already been set by + * then, so a glitched walk is not silently retried on the next call either. */ + try { + return _hal.perm_mac(out); + } catch (const std::exception &e) { + _logger->warn("GetPermanentMacAddress: efuse walk failed ({})", e.what()); + return false; + } +} + uint64_t RtlJaguar3Device::ReadTsf() { /* REG_TSFTR 0x0560 (low) / 0x0564 (high); hi/lo/hi with a wrap retry. Under * _reg_mu (shared with the coex runtime thread). Starved to 0 under a heavy diff --git a/src/jaguar3/RtlJaguar3Device.h b/src/jaguar3/RtlJaguar3Device.h index 71fa82b..34482c5 100644 --- a/src/jaguar3/RtlJaguar3Device.h +++ b/src/jaguar3/RtlJaguar3Device.h @@ -83,6 +83,9 @@ class RtlJaguar3Device : public IRtlDevice { devourer::AmpduMode GetAmpduMode() override { return _ampdu; } devourer::TxStats GetTxStats() override { return _device.GetTxStats(); } SelectedChannel GetSelectedChannel() override; + /* EFUSE MAC at logical 0x157 — captured during rtw_hal_init on 8822E (the + * OTP is not reliably readable later), decoded on demand on 8822C. */ + bool GetPermanentMacAddress(uint8_t out[6]) override; uint64_t ReadTsf() override; void WriteTsf(uint64_t tsf) override; bool StartBeacon(const uint8_t *beacon, size_t len, int interval_tu) override;