Over-the-air time distribution + infrastructure AP mode (hardware beacon)#227
Merged
Conversation
Idea-6 experiment: can the MAC auto-transmit a beacon at each TBTT (TSF-timed, host-jitter-free) from devourer's monitor/injection mode? Adds an experimental IRtlDevice::BeaconTbttSpike (Jaguar1) + tests/beacon_tbtt.cpp harness, and a behaviour-neutral _tx_qsel override on the J1 TX descriptor (default 0x12 mgmt, unchanged; 0x10 = beacon queue). RESULT: NO — the minimal path does not work. BeaconTbttSpike loads the frame via a QSEL_BEACON bulk-OUT and enables the beacon function (BCN_CTRL 0x10 -> 0x1a, EN_BCN_FUNCTION|DIS_BCNQ_SUB), but a second RX (8822C) saw ZERO beacons over 18 s. A plain QSEL_BEACON send does NOT load the persistent reserved-page buffer the TBTT engine re-transmits from. Root cause + full sequence (for future reference, from reference/rtl8812au): - _BeaconFunctionEnable = BCN_CTRL (DIS_TSF_UDT|EN_BCN_FUNCTION|DIS_BCNQ_SUB = 0x1a) + REG_RD_CTRL+1 = 0x6F. (My 0x1a matched; RD_CTRL was missing.) - The beacon must be DOWNLOADED to the reserved page via HalDownloadRSVDPage8812 with a BCN_VALID handshake (poll REG_DWBCN1_CTRL_8812+2) + beacon head-page setup — which devourer implements ONLY for the 8814 (3081/IDDMA), not the 8812. Conclusion: hardware supports TBTT TX, but it's a real reserved-page-download port, not a quick enable; the quick path is empirically ruled out, and monitor-mode RX coexistence remains unverified. Spike branch — NOT for merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the beacon-TBTT experiment to Jaguar2 (RtlJaguar2Device::BeaconTbttSpike). Unlike the 8812 (which lacks any reserved-page download), J2/J3 HAVE a real halmac rsvd-page download (HalmacJaguar2Fw::download_rsvd_page), so the beacon LOAD is reachable — the J1 blocker is gone. RESULT: still negative. The download returns OK and the beacon function enables (BCN_CTRL 0x1c->0x1e, EN_BCN_FUNCTION already set), but the 8822B TX airs ZERO beacons over 18 s (8822C RX). Root cause, from the MacInit comment: post-init the beacon boundary is set to rsvd_boundary(1938), which "makes the page-0 rsvd-page beacon download fail its bcn-valid latch" — so the beacon is never validly latched for the TBTT engine. Making it fire needs the beacon-boundary manipulation (init_trx_cfg's set_bcn_boundary path) + the correct beacon page, plus DRVERLYINT/TBTT_PROHIBIT (set during init, not here). Verdict across all generations: hardware-timed beacon-at-TBTT TX is a genuine reserved-page/beacon-queue feature build, NOT a quick enable — J1 blocked at the download, J2/J3 blocked at the boundary/latch. Monitor-mode RX coexistence still unverified. Spike branch — NOT for merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A worked demonstration of the hardware-TSF primitives (#225) as an LTE eNB→UE timebase: one MASTER broadcasts its hardware TSF periodically (a sync beacon, ReadTsf()-stamped — reliable TX-side), and any number of SLAVES lock their notion of the master clock to it from the beacons alone. No GPS at the slaves, no shared host clock — only the master holds a reference, exactly the eNB→UE model (master TSF = SFN, each slave a UE slaving to it). Each slave relates the master's broadcast TSF to its OWN per-frame hardware TSF (rx_pkt_attrib::tsfl) with a running least-squares fit (both clean MAC-latched µs clocks), PREDICTS each beacon before it arrives, and emits the prediction error. Two slaves predicting the same beacon (matched by seq) agree far tighter than either's absolute lock, because the master's software stamp→air jitter is common to both and cancels in the difference. Bench (master 8812AU, slaves 8822CU + 8822EU, ch36, 50 ms beacons, 30 s): per-slave absolute lock : ~94 us RMS (bounded by the master stamp jitter) inter-slave (inter-UE) : 4.71 us RMS, max 17.6 us, mean +1.70 us Two UEs agree on the eNB clock to <5 us over the air, no GPS. The inter-slave error tightens with beacon rate (17.8 us at 100 ms → 4.7 us at 50 ms) as the fits densify and balance. New: examples/timesync/{main.cpp,timesync.h} (master/slave roles, reusing the tdma TD frame tag), tests/timesync_selftest.cpp (headless LinFit ppm-recovery + inter-slave-agreement + TSF-wrap math, wired into ctest as timesync_math and the mingw target list), tests/timesync_demo.sh + timesync_analyze.py (the bench). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the LTE closed-loop half to the timesync example (DEVOURER_TSYNC_UPLINK=1, roles master + ue): a full-duplex master phase-measures each UE uplink against its own TSF slot grid (arrival tsfl mod slot — reliable per-frame, no ReadTsf the RX loop would starve) and integrates a timing advance it broadcasts back in the beacon; a full-duplex UE transmits one uplink per beacon, TA-corrected. Adds LinFit::inverse(), the uplink/TA frame-class codes (reusing the tdma TD tag), a headless TA-loop convergence test (timesync_math), and the bench harness (tests/timesync_ta_demo.sh + timesync_ta_analyze.py). STATUS — experimental, does NOT converge on the bench. The control math converges in the selftest and the full-duplex plumbing works on-air (with a FIXED advance, uplinks arrive tightly clustered, ~±0.2 ms). But a fixed-advance authority test shows the TA shifts the UE's send-CALL time (~44%) yet NOT the master-measured arrival phase. Root cause: under full-duplex, send_packet queues the frame and the chip airs it on its own schedule, so userspace call-timing has no sub-slot control over TX air departure — the loop has nothing fine to actuate. (This bench also has only one clean full-duplex Jaguar2/3 adapter; the 8822E desenses its RX in TX+RX.) The measurement/feedback/math are in place; closing the loop needs hardware-timed TX departure this path lacks. Kept behind the opt-in flag and clearly marked; the downlink distribution (default) is unaffected and validated. docs/time-distribution.md documents the working downlink (eNB→UE, the bench numbers) and the honest uplink-TA status. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…find, still no air Adds RtlJaguar3Device::BeaconTbttSpike (8822C/8822E) mirroring the J2 path, via a new HalmacJaguar3Fw::download_rsvd_page + rsvd_boundary() and a HalJaguar3:: download_beacon_page passthrough. Beacon is downloaded to the rsvd_boundary head page (where BCN_HEAD points after send_fw_page restores it). NEW FINDING: the TBTT engine only pulls the beacon queue when the port's network type is AP/Ad-hoc — monitor bring-up leaves REG_MSR (0x0102) port-0 at NoLink (read back 0x00), so a validly-latched beacon never airs no matter what BCN_CTRL says. Set port-0 = AP. Also dropped a bogus REG_TCR "TSF reset" write from the J2 spike (0x0604 bit0 is NOT the TSF reset — that's REG_DUAL_TSF_RST 0x0553; the old write likely disabled a TX function / froze state). RESULT: still negative on the 8822C. With the beacon loaded (bcn-valid latched), MSR=AP, BCN_HEAD at rsvd_boundary, EN_BCN_FUNCTION on (already set at init, BCN_CTRL 0x1c), and the interval set, ZERO beacons air (8822E RX sees only the ~12/s ambient rate, no added ~10/s at 100 TU; the RX path is proven — it hears the 8822C's normal txdemo at 22 hits). The remaining blocker is a deeper AP-beacon enable (beacon-queue scheduler / TBTT trigger) that needs a golden register-dump comparison against a working AP (the DEVOURER_REPLAY_WSEQ methodology) — beyond a blind register sweep. Spike branch, NOT for merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…chestrated)
Captured the kernel rtw88_8822c IBSS beacon-enable/refresh register stream on the
8822CU (usbmon, ch6), diffed against devourer's BeaconTbttSpike, and SDR-confirmed
(modulation-agnostic burst detector) the reference beacons on air at the ~102 ms
interval.
VERDICT: devourer's static "download beacon + set EN_BCN_FUNCTION, hardware
auto-transmits at TBTT" is the wrong model for this combo chip. rtw88 orchestrates
the beacon through the FW:
1. re-downloads the beacon EVERY beacon interval (~94 ms) — 53 downloads in ~5 s,
2. sends an H2C RSVD_PAGE command (REG_HMEBOX0 0x1d0=0x690c0100, cmd 0x00) telling
the FW the rsvd-page locations,
3. uses BCNQ_BDNY (0x0424)=0x9207 -> boundary page 0x207 (519), vs devourer's
computed rsvd_boundary=1938 (page-allocation mismatch),
4. cycles BCN_CTRL (0x0550) 0x10/0x14/0x18/0x1c across the download bracket (the
spike wrote a static 0x1e),
5. stages multiple rsvd pages (beacon+probe-rsp+null) at heads 0x207/0x287/...
So cracking beacon-TBTT here = porting rtw88's FW-orchestrated beacon path
(rsvd-page download to the right boundary + H2C RSVD_PAGE + periodic refresh), a
real feature build now precisely scoped by the dump — not a register tweak. The
capture/parse tooling (IBSS on the kernel driver + usbmon) and the SDR burst
detector are reproducible. tests/golden_dump_beacon.md has the distilled delta.
Spike branch, NOT for merge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… still no air
From the rtw88 source (~/rtw88_build/rtw88, fw.c/mac80211.c) the beacon-enable
path is: net_type=AP in REG_CR[17:16], BCN_CTRL=EN_BCN_FUNCTION|DIS_TSF_UDT
(0x18), BIT_EN_BCNQ_DL (BIT22) in REG_FWHW_TXQ_CTRL (BSS_CHANGED_BEACON_ENABLED),
the rsvd-page download bracket (matches devourer's send_fw_page: BIT_ENSWBCN =
CR+1|0x1, FIFOPAGE_CTRL_2 pg|valid, restore), and a per-interval beacon
re-download. Applied all of it here + a periodic-refresh thread.
RESULT: still negative. EN_BCNQ_DL was ALREADY set at devourer init (TXQ
0x00711f83, bit22=1), BCN_CTRL now 0x18, net_type=AP, refresh every interval —
the 8822E RX sees ZERO beacon-subtype frames on ch6 OR ch36. Ruled out: the
CCK-rate-at-5GHz theory (dead on ch6 too where 1M CCK is valid), EN_BCNQ_DL
missing, BCN_CTRL value, MSR-only. (SDR burst detection on ch36 is unreliable —
an ambient AP beacons at 100 TU there, false-positive.)
Remaining blocker is deeper than register setup: rtw88 builds the FULL rsvd-page
blob (beacon+probe-rsp+null+... as one buffer) then re-downloads JUST the beacon
to fix its TX descriptor ("correct tx_desc for the beacon"), sends H2C RSVD_PAGE
with the page offsets, and its fifo.rsvd_boundary (519) differs from devourer's
(1938). So the 8822C beacon is FW-orchestrated end-to-end — cracking it needs the
full rsvd-page/TX-descriptor/H2C port, not the register enables. Spike branch,
NOT for merge; genuine progress + precise remaining scope.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… — still no air Key correction: the earlier "kernel boundary 519 vs devourer 1938" was an ENDIANNESS MISREAD of usbmon's raw bytes — "9207" is bytes 92 07 = LE 0x0792 = 1938, the SAME as devourer. Beacon heads 9287/a287/ab87 decode to pages 1938/1954/1963. So devourer's beacon lands at the correct page; the fifo-layout reconciliation was a red herring (rtw88 rtw_set_trx_fifo_info: rsvd_boundary = txff_pg_num(2048) - rsvd_pg_num = 1938 with page_size 128 — matches HalmacJaguar3MacInit::priority_queue_cfg exactly). Added an H2C RSVD_PAGE probe (cmd 0x00, captured payload). Still negative — but the payload's probe/pspoll/null page offsets are for rtw88's FULL rsvd blob, which devourer doesn't build (beacon only), so it's approximate/inert. RULED OUT now: page layout, net_type/BCN_CTRL/EN_BCNQ_DL enables, CCK-rate, periodic refresh, approximate H2C. The one remaining untested area is the beacon TX DESCRIPTOR: devourer's send_fw_page builds a FW-DOWNLOAD descriptor (routes a bulk chunk to QSEL_BEACON for iddma), NOT a complete beacon-TX descriptor the TBTT engine transmits from — rtw88 explicitly re-downloads the beacon "to replace the TX desc header (correct tx_desc for the beacon)". Cracking it needs a proper beacon-TX-descriptor rsvd-page write path (reuse send_packet's descriptor logic, routed to the beacon queue) + the real rsvd-page blob + a correctly-offset H2C. That's a new code path, not a register tweak. Spike branch, NOT for merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…still no air send_fw_page gains a beacon_desc mode: for the beacon rsvd-page it now sets SET_TX_DESC_BMC/EN_HWSEQ/DISQSELSEQ (rtw_tx_rsvd_page_pkt_info_update RSVD_BEACON: bmc=true, en_hwseq=true, dis_qselseq=true) on top of QSEL_BEACON/USE_RATE/LS — the FW-download descriptor lacked all three. download_rsvd_page passes beacon_desc=true; FW-download chunks keep the original descriptor. STILL NEGATIVE on ch6. This exhausts the individual-piece hypotheses: page layout (correct — endianness fix), net_type=AP, BCN_CTRL=0x18, EN_BCNQ_DL, download bracket, CCK-rate, periodic refresh, H2C RSVD_PAGE (approx), and now the real beacon TX descriptor. Each matches rtw88 yet the beacon does not leave the queue. The blocker is now SYSTEMIC, not a single register/field: either devourer's pure-monitor-mode MAC bring-up (RCR/filters/no-port — a mode the vendor driver never combines with AP beaconing) leaves the beacon scheduler off, or the combo FW only enters a beacon-transmitting state via the full integrated rtw88 flow (build the complete rsvd-page blob + a correctly-offset H2C + the port/BSSID config, as one sequence). Cracking it now means an integrated port of rtw88's AP/beacon bring-up (or verifying monitor+beacon are incompatible on this chip), not more piecewise fixes. Spike branch, NOT for merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… at page 0 Stopped asserting "matches rtw88" and actually diffed devourer's USB register writes against the kernel rtw88_8822c IBSS beacon-enable (both on the 8822CU, usbmon). The diff found concrete mismatches the reasoning missed: 1. BEACON DOWNLOADED TO PAGE 0 (the real bug). HalmacJaguar3Fw::_rsvd_boundary is never assigned — MacInit computes 1938 and writes the REGISTERS but never propagates it to the FW class, so _fw.rsvd_boundary() returned 0. The beacon armed to FIFOPAGE_CTRL_2=0x8000 (head 0) while BCNQ_BDNY=1938 — beacon and TBTT-engine head in different places. Fix: MacInit exposes rsvd_boundary(), HalJaguar3 propagates it via new _fw.set_rsvd_boundary() after init_mac_cfg. Verified via usbmon: devourer now arms to 0x8792 (page 1938), matching kernel. 2. NO PORT IDENTITY. The kernel writes REG_MACID (0x0610) + REG_BSSID (0x0618); devourer's monitor bring-up writes NEITHER. Added both from the beacon's addr2/addr3 (rtw88 rtw_vif_port_config). 3. EN_BCNQ_DL cleared during download on USB. send_fw_page cleared BIT_EN_BCNQ_DL every download; rtw88 does that ONLY for PCIe. A refresh overlapping a TBTT would suppress the beacon. Kept set on the beacon path. STATUS: all three are real, usbmon-verified corrections (the page-0 bug matters for ANY beacon work), but the beacon still does not air. Remaining diff = ~a dozen registers the kernel's IBSS config writes that devourer's MONITOR bring-up omits (REG_MACID tail, RD_CTRL, INT_MIG, RRSR/rate, coex tables). The gap is now "monitor-mode MAC config != beaconing-station MAC config" — matchable, but many registers. Spike branch, NOT for merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… 8822C The final two bugs, both found by actually DIFFING devourer's usbmon writes against the kernel rtw88 IBSS (not by asserting a match): 1. RADIOTAP PREFIX (the last blocker). The harness passes [radiotap][802.11 beacon]; BeaconTbttSpike downloaded the WHOLE buffer into the rsvd page, so the on-air beacon was [radiotap][802.11] — malformed, and the readback showed REG_MACID/BSSID = garbage (0x80.../0xffff, i.e. radiotap+FC bytes). Strip the radiotap (it_len = bytes[2:3]) so the rsvd page holds the raw 802.11 MPDU, and read MACID/BSSID from the MPDU's addr2/addr3. Combined with the earlier diff-found fixes (beacon page 0 -> 1938 via _rsvd_boundary propagation; REG_MACID/BSSID port identity; EN_BCNQ_DL kept on the USB beacon path) plus net_type=AP, BCN_CTRL=0x18, EN_BCNQ_DL, interval: RESULT: the 8822C now AUTO-TRANSMITS the beacon at TBTT — ~200 canonical-SA beacons decoded by the 8822E over 24 s (~8.3/s ≈ 100 TU), fully hardware-timed (one download + periodic refresh, the MAC fires at each TBTT). This is the hardware-timed TX primitive that unlocks LTE-style uplink-TA and a sub-µs downlink beacon stamp. Lesson: diff the actual register traffic, don't reason about whether it matches. Spike branch — the crack; productization (strip the spike scaffolding, wire a clean API) is follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…w opt-in Bench-verified: ONE beacon download airs beacons at TBTT indefinitely on both bands (ch6 and ch36, no refresh) — the periodic re-download was never needed to keep it airing. rtw88 re-downloads each interval only to refresh the beacon CONTENT (TSF/TIM), not the airing, which is pure hardware TBTT. So the refresh thread is now opt-in (BEACON_REFRESH=1); the default is the clean single-download HW-TBTT path, which is truly kernel-like at the hardware level. Status: beacon-TBTT works on 2.4 + 5 GHz. Remaining to full parity: the 5 GHz decode count is lower than 2.4 (RX-side loss / beacon rate — to investigate), and productization (clean API, J2 same fixes, timesync uplink-TA integration). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verification (tests/beacon_ts_check.cpp): the 8822C's TBTT beacon body timestamp (MPDU bytes 24..31) steps by ~102364 us (= 100 TU) each beacon, and the receiver's arrival tsfl steps by the same — the MAC inserts the current hardware TSF into the beacon at TX time. So devourer's beacon is now a fully functional, kernel- equivalent 802.11 beacon: hardware-timed at TBTT, hardware-stamped with the live TSF, on both 2.4 and 5 GHz. Payoff for timesync: the master can use hardware-timed + hardware-stamped beacons. A slave reads the beacon's TSF (master clock) and its own arrival tsfl (slave clock) -> sub-µs downlink sync, dropping the ~94 us software-ReadTsf-stamp floor that bounded the current downlink. That's the sub-µs floor + the uplink-TA hardware actuator the timesync work was missing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retires the BeaconTbttSpike scaffolding into a proper IRtlDevice::StartBeacon (non-experimental): load a full 802.11 beacon MPDU into the beacon reserved-page and enable HW-TBTT auto-TX, hardware-timed + hardware-TSF-stamped. Renamed across IRtlDevice + all three generations + the harness. Jaguar1: now returns false (unsupported) — the 8812/8821 have no HalMAC reserved-page download (QSEL-beacon transmits once; bench-confirmed negative), the 8814 IDDMA path is unported. Removed the dead QSEL-send attempt. Jaguar2 (RtlJaguar2Device::StartBeacon): ported the two J3 bug-fixes verbatim — (1) download to the real _fw.rsvd_boundary() not page 0 (added the _fw.set_rsvd_boundary(_macinit.rsvd_boundary()) propagation after init_mac_cfg, which was missing exactly as on J3); (2) strip a leading radiotap header so the rsvd page holds the raw MPDU. Plus REG_MACID/BSSID port identity, net_type=AP, BCN_CTRL=0x18, EN_BCNQ_DL, and the send_fw_page beacon_desc mode (BMC/EN_HWSEQ/ DISQSELSEQ + keep EN_BCNQ_DL on the USB beacon path). By-construction identical to the validated J3 flow but UNTESTED (no 8822B/8821C on the bench). Jaguar3 (the validated impl): unchanged behavior, verified still airs post-rename (~100 beacons / 10 s on ch6). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…4ns path Wires the cracked StartBeacon (hardware-timed, hardware-TSF-stamped beacon) into the timesync master/slave, so the downlink runs on real MAC-hardware timestamps instead of the ~94 us software ReadTsf+send_packet stamp. - master: build_std_beacon (byte-identical to the validated beacon_tbtt beacon — an extended body broke the hardware TSF insertion) + StartBeacon, then idle. The MAC auto-transmits at TBTT and inserts the live 64-bit TSF into the beacon. - slave: reads the STANDARD 802.11 beacon timestamp (MPDU bytes 24..31 = master's hardware TSF) instead of the TD-tag tx_tsf, fit against its own arrival tsfl. Bench (c812 master, a81a slave, ch36, HW beacon): 169 beacons, slave locks, master-slave TSF offset stable to ~120 us on this shared channel (dominated by CSMA air-time jitter + predict-over-gap). A direct clean-stretch measurement (tests/beacon_ts_check) showed the floor is 0.254 us — vs the ~94 us software stamp. Both timestamps are pure hardware; the residual is now channel-condition- limited, not driver-limited. Integration branch (timesync-hwbeacon = timesync + beacon merged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…downlink The hardware-beacon downlink's residual was CSMA-limited: on a shared channel the TBTT beacon defers to carrier-sense (EDCCA) and airs after a variable backoff, so the master's TSF-stamp-at-scheduled-TBTT vs the delayed actual air time jitter by ~hundreds of us. In a time-distribution setup the master OWNS the channel, so the backoff is pure loss. Exposes SetCcaMode(bool) on IRtlDevice (J3 already implemented the vendor dis_cca recipe: MAC BIT_DIS_EDCCA 0x520[15] + EDCCA-mask 0x524[11]) and adds DEVOURER_TSYNC_NO_CSMA: the HW-beacon master disables EDCCA so the beacon airs exactly on the TBTT schedule. Bench (c812 master + a81a slave, HW beacon, CROWDED ch6, 146 beacons): without NO_CSMA: 472 us RMS (CSMA backoff jitter) with NO_CSMA: 0.31 us RMS, max 0.56 us <- sub-µs on a crowded channel The master-slave TSF offset goes rock-stable (moves only with the ~1.3 ppm crystal drift). ~1500x tighter than the ~470us CSMA case, ~300x below the ~94us software-ReadTsf-stamp floor. Full LTE-eNB-style sub-µs time distribution. (EDCCA-disable is a NULL-op for injected TX — which bypasses TX deferral — but the TBTT beacon goes through the MAC's normal deferral, so this is exactly where it bites. Idea + 88x2EU edcca_threshold_override recipe from the user.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… _CSMA=1) The master owns the channel in a time-distribution setup, so deferring the beacon to CSMA is pure loss. Flip the default: DEVOURER_TSYNC_HWBEACON now disables EDCCA unless DEVOURER_TSYNC_CSMA=1 is set. Default-on verified sub-µs (0.30 us RMS) on crowded ch6 with no env; the opt-out keeps CSMA for anyone who needs to coexist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… parity An RTL8812BU (2357:012d, Jaguar2) and RTL8811CU (0bda:c811) turned up on the bench, so the previously by-construction-only J2 port is now hardware-validated: - J2 StartBeacon: the 8812BU auto-transmits its beacon at TBTT, decoded by an 8822E RX (~100 beacons / 12 s) — the page-0 + radiotap fixes ported from J3 work. - Added RtlJaguar2Device::SetCcaMode (the EDCCA registers 0x520/0x524 are HalMAC-common with J3) so the no-CSMA path works on J2 too. - End-to-end timesync (8812BU master, HW beacon + default no-CSMA, 8822E slave, crowded ch6, 149 beacons): 0.39 us RMS downlink sync — matching J3. So hardware-timed, hardware-TSF-stamped, master-owns-channel sub-µs beacon time distribution now works on BOTH HalMAC generations (8822B/8812BU/8821C and 8822C/8822E). Dropped the "UNTESTED" caveat. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The doc still called the hardware-timed master beacon "a separate build" and capped the downlink at the ~94 us software stamp. Updates it to the shipped reality: StartBeacon (HW-timed + HW-TSF-stamped beacon at TBTT, both HalMAC gens), the slave reading the standard 802.11 timestamp, and no-CSMA (master-owns-channel, default on) giving 0.31 us (8822C) / 0.39 us (8812BU) on a crowded channel. Adds the DEVOURER_TSYNC_HWBEACON / _CSMA knobs, the minimal-beacon-body caveat, and notes the HW beacon is the sub-slot TX actuator the uplink-TA loop was missing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uator Adds IRtlDevice::WriteTsf(uint64_t) (REG_TSFTR 0x0560/0x0564) — the kernel-side primitive devourer was missing next to ReadTsf. Sets the free-running MAC µs clock, which continues from the written value. This is what a slave needs to ADOPT the master's clock (slew its TSF onto the beacon's), and the actuator the LTE-style uplink timing-advance needs (shift the port TSF so a TBTT-scheduled uplink lands in its slot — the sub-slot air-departure control send_packet can't give). Validated on hardware both gens: WriteTsf(0x50_0000_0000) then ReadTsf 200 ms later returns wrote + ~200692 us (8822C) — the counter took the value and ran on. Implemented on Jaguar2 (8822B/8812BU/8821C) + Jaguar3 (8822C/8822E); no-op base. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The uplink timing-advance loop needs a way to shift a hardware-timed TX in sub-slot increments. WriteTsf was the assumed lever, but it moves only the reported TSF (and the beacon-body timestamp) — the beacon TBTT air-time is driven by a separate per-port timer and is deaf to REG_TSFTR (bench-proven: writing +25 ms to the TSF left the observed beacon arrival phase rock-constant). The actuator that DOES steer the TBTT is a one-shot beacon-interval tweak: run one interval at (nominal ± Δ) TU via REG_BCN_INTERVAL (0x0554) then restore, and the next TBTT — and the cadence thereafter — advances/retards by Δ TU. This is the 802.11 IBSS/TSF-merge mechanism. Add IRtlDevice::AdjustBeaconTiming(us) on Jaguar2/3 (base no-op) built on the bare-0x0554 path (no beacon re-download); requires an active StartBeacon, blocks ~one interval, TU-quantized (1 TU = 1.024 ms). Bench-validated on 8822C (8822CU master + 8822EU observer, ch36): a single AdjustBeaconTiming(-20480) advanced the beacon TBTT by exactly 20 TU — the observer's arrival phase stepped 88018 -> 67565 µs at the tweak (predicted -20480 µs), and held. tests/beacon_interval_shift.sh <short_TU> characterizes it. Correct the WriteTsf doc-comment + docs/time-distribution.md accordingly (WriteTsf = TSF adoption for the downlink; AdjustBeaconTiming = the TBTT/uplink-TA actuator, coarse at 1-TU granularity). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The TU-quantized AdjustBeaconTiming (1.024 ms grain) is too coarse for a real uplink timing-advance. Root cause of why WriteTsf couldn't move the TBTT: the TBTT counter is latched while the beacon function runs — the vendor reset_tsf path clears EN_BCN_FUNCTION first. Add AdjustBeaconTimingFine, which toggles the beacon function off, shifts the port-0 TSF by the requested µs, and toggles it back on so the TBTT re-derives from the shifted TSF at MICROSECOND resolution. Bench-validated on 8822C (8822CU master + 8822EU observer, ch36), arrival-phase steps all sub-TU: -5000 -> -4479 µs, -10000 -> -8997 µs, +6000 -> +7172 µs (retard). Magnitude undershoots/overshoots by a sub-ms USB read->write latency (~0.5-1.2 ms, the real TSF advances during the register sequence) — a systematic offset a closed timing-advance loop absorbs; the resolution is µs. It also shifts this port's TSF + beacon-body timestamp, the intended UE-advances-its-own-timebase behaviour. Jaguar2 correction: TBTT steering is NOT viable on the 8822B. Bench-proven on the 8812BU, BOTH the beacon-function toggle AND the one-shot REG_BCN_INTERVAL tweak drop the beacon (the bcn-valid latch is lost, and J2 doesn't retain the beacon bytes to re-download). The prior commit shipped the J2 interval tweak as working (it was only J3-validated). Gate both J2 actuators to return 0 rather than silently kill the beacon; the downlink (StartBeacon + SetCcaMode) is unaffected. Steering is Jaguar3-only. tests/beacon_interval_shift.sh gains a FINE_US=<µs> mode (+ DEVOURER_VID support) to characterize the fine actuator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… was a misdiagnosis) docs/time-distribution.md claimed the beacon body had to stay minimal — an extended body (long SSID / extra IEs) was reported to break the hardware TSF insertion (the MAC writing a per-beacon counter instead). Kernel APs beacon with full bodies and the HW inserts the TSF fine, so the limit was suspect. Bench-tested on an 8822C: a well-formed 70-byte beacon body (10-char SSID + 8 supported rates + DS param + TIM + ERP IEs) inserts the live hardware TSF correctly — the observed timestamp field steps ~102400 µs per beacon exactly as the minimal body does (4608054 -> 4710436 -> 4812890 -> ... via a second-adapter tests/beacon_ts_check, isolating the fresh low-TSF source from a stale autonomous beacon). So there is NO minimal-body requirement; the earlier failure was a malformed test frame (bad IE lengths), not a hardware constraint. devourer beacons can carry full kernel-AP content with the live TSF. Add tests/beacon_fullbody.cpp (the full-body validation harness) and correct the doc + the timesync build_std_beacon comment. No code change — StartBeacon already accepts arbitrary beacon bytes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ve TSF, both gens) Confirm devourer's hardware beacon looks like a real kernel-AP beacon on the air, not just at the TSF level. tests/beacon_wire_check.cpp dumps, per canonical-SA beacon, the frame control, the 802.11 sequence-control field and the beacon-body timestamp. Bench-verified on both HalMAC generations: J3 8822C : FC=0x0080, seq 16,17,18,19,20,21..., ts steps ~102400 us J2 8812BU: FC=0x0080, seq 730,731,732,733..., ts steps ~102400 us So the hardware sequence numbering (EN_HWSEQ) increments the beacon seq by 1 per beacon on both gens — kernel-equivalent. (A frozen seq seen earlier was a J2 beacon left in the post-drop state after an AdjustBeaconTiming tweak, which J2 does not survive — a degraded engine, not normal behaviour.) Also reconfirms the full-body TSF insertion on J2 (last iteration validated it on J3 only). No code change — a verification tool + a docs note. StartBeacon already produces a fully kernel-equivalent beacon. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e steering The uplink-TA loop never converged because the UE's send_packet uplink has no sub-slot air-departure control (the chip airs the queued frame on its own schedule) — the doc's fixed-TA authority test showed TA shifts the send-CALL time but not the measured arrival (bench baseline: phase RMS ~5.4 ms, drifting across the whole slot). Close it: DEVOURER_TSYNC_HWBEACON=1 on the UE airs the uplink from the BEACON engine (StartBeacon stores a tdma Uplink frame in the rsvd page; the engine airs it verbatim at the UE's TBTT) and steers that TBTT with AdjustBeaconTimingFine per the master's TA — the sub-slot control send_packet lacked. The master's Uplink measurement path is unchanged (matches by class). The bench was NOT actually blocked: the master needs full-duplex but no fine steering, so it can be the rock-solid 8812AU (Jaguar1 does TX+RX) — only the UE must be a clean full-duplex J3 (8822CU). Bench-proven convergence (ch36, slot 20 ms, gain 0.30): arrival phase RMS drops from ~5.5 ms to a bounded ~1.4 ms steady-state (mean ~0) — a 4x improvement over the non-converging baseline. The residual is the fine actuator's USB read->write jitter (~0.5-1.2 ms/correction), the floor for a userspace-USB TSF write. tests/timesync_ta_demo.sh gains HWBEACON=1 (8812AU master + 8822CU UE, INT=100). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pts devourer's AP The strongest "behaves like a kernel" proof: a real Linux 802.11 station (rtw88 / mac80211) scans for devourer's beacon and lists it as a valid AP, parsing every element. Bench-proven — devourer airs a full-content beacon (8812BU, ch6), a second Realtek adapter bound to rtw88 (managed) runs `iw scan` and returns: BSS 57:42:75:05:d6:00 TSF <live> freq 2437 beacon interval 25 TUs capability: ESS SSID: devourerAP Supported rates: 1.0* 2.0* 5.5* 11.0* 18.0 24.0 36.0 54.0 DS Parameter set: channel 6 TIM: DTIM Count 0 Period 1 ERP: <no flags> The live hardware TSF, capability, SSID, all 8 rates (basic-rate flags), DS param, TIM/DTIM and ERP are all interpreted correctly by the kernel BSS parser — devourer's beacon is a genuine kernel-equivalent AP beacon, not just wire-correct bytes. Add tests/beacon_kernel_scan.sh (the interop harness) + a DEVOURER_BCN_TU interval knob on tests/beacon_fullbody.cpp (a dense interval is caught reliably by the scan dwell) + a docs note. No library change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The earlier ch36 miss was the sparse 100 TU interval, not a band issue: with a dense interval (25 TU) the rtw88 station lists devourer's beacon at ch36 (5180 MHz) — SSID/rates/DS-param parsed, same as ch6. Control: the station sees 8 ambient 5 GHz APs, so its 5 GHz scan works. External kernel-AP interop is now validated on both 2.4 GHz (ch6) and 5 GHz (ch36). Doc + harness note updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…be response) Open the AP-side handshake and de-risk it with the first step: devourer responds to active-scan probe requests fast enough that a real station discovers the AP via the PROBE RESPONSE, with NO beacon aired. Full-duplex responder (InitWrite + StartRxLoop): the RX callback matches a probe-request and queues the requester; the main thread send_packet's a probe-response addressed to it. (send_packet must run off the RX event thread — calling the synchronous bulk transfer from inside the RX callback returns libusb BUSY, rc=-6; queue + main-thread TX fixes it.) Bench-proven: devourer probe-responder on c812 (ch6, no beacon); a second Realtek adapter bound to rtw88 (wlp13s0u1u4) runs `iw scan freq 2437` and lists "devourerAP" in every scan — discovery purely via the probe response (4 reqs seen, 4 resps sent, 0 errors). This works because management-frame timing is tens of ms (the userspace RX->TX round-trip is a few ms), unlike the SIFS-timed ACK a data frame needs. It de-risks the full AP handshake (probe-resp -> auth -> assoc). tests/probe_responder.cpp + a docs note. No library change (RX callback + send_packet). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ICATING Extend the AP frontier from probe-response (tests/probe_responder.cpp) to the full open-network handshake: ap_responder beacons (StartBeacon) and answers probe / auth / (re)assoc requests from its RX callback (built in the callback, SENT from the main thread — send_packet from the RX event thread returns libusb BUSY). Bench findings: - With a DENSE beacon (DEVOURER_BCN_TU=25 — a fast channel-hopping scan misses a 100 TU beacon + the ~ms-delayed probe response), wpa_supplicant discovers devourerAP, selects it, and reaches "SME: Trying to authenticate". - Full association did NOT complete on this rig, and it is NOT a devourer-side gap: the test station's rtw88 (2357:0120) never puts the auth frame on air. Two independent promiscuous monitors (the AP's own RX + an 8812AU sniffer) saw ZERO auth-to-BSSID, and devourer's monitor RX is confirmed promiscuous (52 ambient unicast-to-other-MAC frames in 10 s), so a transmitted auth would be seen. The auth/assoc responder paths are built but unexercised end to end, pending a station that actually transmits auth. The AP side (beacon accepted by a real kernel scan on both bands + active-scan probe response + reaching wpa_supplicant AUTHENTICATING) behaves like a kernel AP. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t BSSID fix) FULL ASSOCIATION ACHIEVED — the ultimate "behaves like a kernel AP". A real Linux station (rtw88 8822cu) authenticates, associates, and stays Connected to devourer's AP: wpa_supplicant CTRL-EVENT-CONNECTED, `iw link` Connected, stable for 8 s+. The station's AUTH/ASSOC requests arrive with retry=0 — devourer HARDWARE-ACKs them (the ACK engine matches REG_MACID, which StartBeacon sets to the BSSID). So the auth/assoc responder paths (built last commit) are now exercised end to end. ROOT CAUSE of the multi-iteration association block: the BSSID was MULTICAST. The canonical test SA 57:42:75:05:d6:00 has the I/G bit set in 0x57 (bit 0 = 1 = group/multicast), which is invalid as a BSSID — a station cannot unicast-auth to a multicast address, so rtw88 silently dropped the auth before it hit the air. Two independent promiscuous monitors saw ZERO auth-to-BSSID with 0x57 across two station chips; flipping the first octet to 0x02 (locally-administered UNICAST) put the auth on air and the whole handshake completed. ap_responder's kBssid is now 0x02.. and the header/doc explain the requirement. Not a code change to the library — StartBeacon already sets MACID from the beacon addr2 and the hardware ACKs; the fix is using a valid unicast BSSID. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The capstone of the AP frontier: after a real Linux station associates (prior commit), it now exchanges IP traffic with devourer's AP over the air. ap_responder answers the post-association data plane — ARP requests for the AP IP -> ARP replies, ICMP echo requests -> echo replies, over 802.11 from-DS data frames. Bench-proven: an rtw88 8822cu station associates, gets a static IP (192.168.99.2), and `ping 192.168.99.1` returns 0% packet loss, ~1.8 ms RTT. So devourer is a real, associable, PINGABLE AP — a Linux station discovers it, authenticates, associates, and exchanges IP traffic with it, exactly like a kernel-driven AP. The station's data frames are hardware-ACKed (MACID = BSSID). tests/ap_ping_demo.sh runs the whole thing (associate + static IP + ping) and reports the loss/RTT. A full data path (DHCP, forwarding) is an AP-stack concern beyond the driver. No library change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… AP) Complete the "just works like a real AP" chain: ap_responder now runs a minimal DHCP server (DISCOVER->OFFER, REQUEST->ACK leasing 192.168.99.2, with subnet/ router/DNS options) alongside the ARP + ICMP data plane. So a Linux station associates and dhcpcd AUTOMATICALLY leases 192.168.99.2 from devourer — no manual static IP — then `ping 192.168.99.1` returns 0% loss, ~1.9 ms RTT. Bench-proven end to end via tests/ap_ping_demo.sh: beacon -> auth -> assoc -> DHCP (offered/leased) -> ARP/ICMP -> ping 6/6. devourer is a self-service infrastructure AP in one userspace process, like hostapd+dnsmasq. Beyond this (routing/NAT, WPA2) is AP-stack scope, not driver parity. No library change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…th devourer Take the AP to WPA2: tests/ap_wpa2.cpp advertises an RSN IE (WPA2-PSK-CCMP) and runs the 4-way handshake authenticator. A real Linux station completes it — wpa_supplicant: "WPA: Key negotiation completed ... [PTK=CCMP GTK=CCMP]" + CTRL-EVENT-CONNECTED; the AP logs msg2-MIC-verified -> msg3 -> msg4-OK. This is the deepest driver-protocol milestone (WPA2 key negotiation). The crypto is openssl in userspace: PMK = PBKDF2-HMAC-SHA1(psk, ssid), PTK = PRF, MIC = HMAC-SHA1(KCK), GTK AES-key-wrapped with the KEK. The 4-way handshake needs no CCMP (EAPOL-Key frames are cleartext, MIC-protected), so it works without the chip crypto engine — which matters because J3 has no HW-crypto TX/RX descriptor fields (only J1 does). Encrypted CCMP *data* is the follow-on (software AES-CCM on the data path); the station disconnects post-handshake since data isn't yet encrypted. Two details that mattered: msg3 key-data pad is 0xDD then 0x00s (a 2nd 0xDD mis-parses as a KDE -> station rejects msg3); a prior WRONG_KEY failure temp-disables the SSID in wpa_supplicant (cold-cycle the station for a clean run). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…2 (0% loss) Complete the WPA2 AP: after the 4-way handshake, ap_wpa2 encrypts/decrypts the data plane with software AES-CCM (openssl) using the negotiated TK (PTK[32:48]). It decrypts the station's CCMP data frames and answers ARP + ICMP echo encrypted, so a real Linux station pings the AP over WPA2/CCMP at 0% packet loss, ~2.5 ms RTT. The station runs hardware CCMP, devourer software CCMP — they interoperate. So devourer is a COMPLETE WPA2-PSK infrastructure AP: a real station discovers it, authenticates, associates, negotiates CCMP keys (4-way), and exchanges ENCRYPTED IP traffic with it — the full encrypted management + data plane, validated against the Linux WiFi stack. CCMP framing details: AAD masks FC subtype/retry/pm/moredata + sets protected, and masks the seq number (keeps frag); nonce = 0|A2|PN(6, big-endian); CCMP header carries the 48-bit PN + ext-IV keyid; from-DS data frames set the protected bit. (HW CCMP offload would need the J3 security TX/RX descriptor fields, absent in devourer; software CCMP sidesteps that. Static IP used; DHCP-over-CCMP is the same handle_plain extension.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pted lease) Route the DHCP server through the CCMP data path (handle_plain): the AP decrypts the station's DHCP DISCOVER/REQUEST, builds the OFFER/ACK, and encrypts it, so the station leases 192.168.99.2 over ENCRYPTED DHCP — no static IP. Validated: dhcpcd reports "leased 192.168.99.2 for 86400 seconds" over the WPA2/CCMP link (the decrypt log shows the DISCOVER=332B UDP -> OFFER, plus ARP + ICMP over CCMP). So devourer is a complete zero-config WPA2-PSK AP: a real Linux station discovers it, authenticates, associates, negotiates CCMP keys (4-way), auto-leases an IP over encrypted DHCP, and pings it encrypted at 0% loss — the full encrypted management + data plane like a hostapd+dnsmasq WPA2 AP, in one userspace process. (Each capability is individually bench-validated; a single clean end-to-end run of all of it is flaky after many cycles on this rig — the 8812BU AP is on an xhci root hub with no VBUS reset. IPv6 ND frames from the station are ignored.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…de in README The infrastructure-AP work (probe/auth/assoc, DHCP/ARP/ICMP, WPA2 4-way + software CCMP) had accreted inside docs/time-distribution.md, which is about time sync — incoherent. Move it to a standalone docs/ap-mode.md (a real Linux station associates → gets an IP → pings devourer, open or WPA2-PSK encrypted), leaving a cross-reference in the time-distribution doc. Add both docs to the README index — neither was listed. Docs only; no code change. All test harnesses (beacon_fullbody, beacon_wire_check, probe_responder, ap_responder, ap_wpa2, + the *.sh) compile clean, ctest 15/15. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pted DHCP + ping) The WPA2 flow had no orchestration script (open mode has ap_ping_demo.sh). Add the matching harness: builds ap_wpa2 (-lcrypto), brings devourer up as a WPA2-PSK AP, connects a real station, leases an IP over encrypted DHCP, and pings over WPA2/CCMP — reporting the 4-way, lease, and loss/RTT. Completes the AP harness set. Docs only / test harness; no library change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The library must read no environment (CLAUDE.md: construction knobs go in
DeviceConfig, mid-session knobs are IRtlDevice setters) — but StartBeacon gated an
experimental periodic beacon re-download thread on std::getenv("BEACON_REFRESH"),
the only getenv in src/. Worse, the thread re-downloaded the SAME static beacon
bytes every interval: there is no content-update path, so even when enabled it did
nothing useful (a single download airs the beacon at every TBTT indefinitely).
Remove it entirely — the getenv, the thread, and its only-there-for-it state
(_bcn_bytes, _bcn_interval_ms, _bcn_run, _bcn_thread) plus the destructor join. The
default path (no refresh) is unchanged, and _bcn_interval_tu (used by
AdjustBeaconTiming) is untouched. A real dynamic-beacon-content refresh would need
a content-update path behind a DeviceConfig knob, not env.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cn_interval_tu) Two more write-only leftovers from the beacon-TBTT work, same class as the BEACON_REFRESH getenv: - Jaguar1 `_tx_qsel`: the spike made the TX-desc QUEUE_SEL dynamic (0x10 for a beacon-TX experiment, 0x12 for mgmt), but the beacon path was abandoned (StartBeacon returns false on J1) and the `_tx_qsel = 0x10/0x12` assignments were dropped — freezing the member at 0x12, exactly the original hardcode. Inline `SET_TX_DESC_QUEUE_SEL_8812(usb_frame, 0x12)` and drop the member. - Jaguar2 `_bcn_interval_tu`: set in StartBeacon but never read — J2's AdjustBeaconTiming is gated (TBTT steering drops the 8822B beacon, returns 0), so the interval is unused. Remove the member and its assignment. J3's `_bcn_interval_tu` is untouched (its AdjustBeaconTiming/Fine genuinely read it — steering works there). No behaviour change; build clean, ctest 15/15. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
josephnef
added a commit
that referenced
this pull request
Jul 10, 2026
…t-latency bench (#228) ## Summary Answers the recurring question *"how accurate is the TSF time distribution vs NTP/PTP over Wi-Fi?"* with **measured** numbers from a two-node bench, and lands a reusable transport-latency microbench. Follow-up to #227 (over-the-air time distribution). Two small, self-contained additions — a benchmark tool and a docs writeup; no library change. ## `tests/reglat.cpp` — register round-trip latency (USB vs PCIe) One binary that times `RtlAdapter::rtw_read32` over whichever transport it opens — USB vendor-control by default, PCIe BAR2 MMIO when `DEVOURER_PCIE_BDF` is set (the PCIe path compiles only with `DEVOURER_PCIE`, via the `devourer` PUBLIC `DEVOURER_HAVE_PCIE` define). Added as a plain `add_executable` (USB path builds in all configs). Measured on the same binary: | Transport | mean/op | jitter (p99−p50) | |---|---|---| | USB (8822EU) | ~68 µs | ~64 µs | | PCIe 8821CE (MMIO) | ~2.2 µs | ~0.08 µs | → **~30× faster, ~600–900× lower jitter**. The `AdjustBeaconTimingFine` actuator chains ~5–7 register ops, so the ~0.5–1.2 ms USB jitter (and the ~1.3 ms closed-loop uplink-TA residual) is a **transport** floor — a PCIe MMIO path would collapse it toward µs. ## `docs/timing-accuracy.md` — NTP / PTP over Wi-Fi vs TSF Measured on two machines associated to the same AP: | Method | Precision over Wi-Fi | Timestamp source | |---|---|---| | NTP (`chrony -Q`, software TS) | ~0.76 ms RMS (quiet); tens of ms under load | software | | PTP (`ptp4l`) | **does not run** | — | | devourer TSF downlink | ~0.25 µs RMS | 802.11 MAC hardware | - **~3000× tighter than NTP**, because the MAC latches the TSF *below* the CSMA/queue/power-save layer whose RTT jitter (measured **1.6 → 110 ms** under load) bounds software-timestamped protocols. - **PTP can't run**: `ethtool -T` = `PTP Hardware Clock: none`; grepping the `reference/` vendor drivers for `SOF_TIMESTAMPING`/`SIOCSHWTSTAMP`/`skb_hwtstamps`/ `get_ts_info` finds nothing — neither vendor nor in-tree driver exposes the HW timestamp to the kernel API, though both read the TSF internally. devourer surfaces it (`RxPacket.tsfl`), which is what enables sub-µs sync. Caveats kept explicit in the doc: devourer's figure is one-way *relative* precision (TSF is 1 µs resolution, sub-µs via averaging), NTP's is the healthy-link daemon offset, and FTM ranging can beat a one-way TSF on *absolute* accuracy. ## Testing `ctest` green; multi-config build clean (`reglat` builds in USB-only and PCIe configs); PCIe path verified on a Radxa X4 (RTL8821CE) via vfio-pci. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Over-the-air time distribution (LTE-eNB style) built on a new hardware-timed
beacon primitive, plus an infrastructure AP mode that grows on the same
beacon. A Realtek adapter driven by devourer becomes a clock source and/or a real
access point that a stock Linux
rtw88/mac80211station discovers, associateswith, and exchanges traffic with — open or WPA2-PSK encrypted.
All of it rides small, generation-agnostic additions to
IRtlDevice; the bulk ofthe diff is the demo (
examples/timesync/), docs, and experimental bench harnessesunder
tests/. The library core is ~400 lines.Library API (
src/)New
IRtlDevicevirtuals (base no-ops; per-generation where the hardware allows):StartBeacon(mpdu, len, interval_tu)— load a beacon into the reserved page andenable the MAC beacon function, so the chip auto-transmits it at every TBTT,
hardware-TSF-stamped. Jaguar2/3 (HalMAC reserved-page download); Jaguar1 returns
false(no rsvd-page path).ReadTsf()/WriteTsf()— the 64-bit MAC TSF (adoption / uplink-TA timebase).SetCcaMode(disabled)— the EDCCA gate, so a TBTT beacon airs on scheduleinstead of after CSMA backoff (the master owns the channel).
AdjustBeaconTiming(µs)/AdjustBeaconTimingFine(µs)— steer the beacon TBTT(coarse one-shot
REG_BCN_INTERVALtweak; µs-fine via the beacon-function toggleTBTT re-latch, so J2 gates these to
0rather than silently kill the beacon.HAL plumbing: HalMAC
send_fw_pagegains a beacon TX-descriptor path (BMC/HWSEQ/DISQSELSEQ + bcn-valid latch) and the reserved-page boundary is propagated from
MacInit to the FW-download stage (J2 + J3).
Time distribution (
examples/timesync/,docs/time-distribution.md)Master → slave/UE clock distribution off the beacon TSF: sub-µs downlink, TSF
adoption, and a closed-loop uplink timing advance that converges to ~1.3 ms
(USB-actuator-jitter-limited) using the µs-fine steering.
AP mode (
tests/ap_*,docs/ap-mode.md)Experimental harnesses that make devourer a real AP: beacon (accepted by a kernel
iw scanon both bands) → probe/auth/assoc responses → DHCP/ARP/ICMP, so a Linuxstation associates, leases an IP, and pings it.
ap_wpa2adds the WPA2-PSK 4-wayhandshake (openssl) and a software CCMP data path — a real station connects
encrypted and pings at 0% loss.
Validation
Bench-tested against a second Realtek adapter running stock kernel
rtw88:beacon accepted by
iw scan(2.4 + 5 GHz), full open + WPA2 association, encryptedDHCP lease and ping. Headless
ctest(timesync selftest + existing math guards)green; multi-platform CI builds clean.
Scope / caveats
StartBeaconis Jaguar2/3; beacon-TBTT steering is Jaguar3-only (gated off onJ2). Jaguar1 has no reserved-page path.
hardware CCMP offload (J3 security TX/RX descriptor fields) is not ported.
tests/ap_*/beacon_*harnesses are experimental bench tools (manual-build,like
beacon_tbtt.cpp), not library API; only the timesync selftest is actest.getenvand dead state were removed).🤖 Generated with Claude Code