Skip to content

Hardware time distribution: beacon TX-egress TSF + TsfSync, and the measured PTP reality#230

Merged
josephnef merged 2 commits into
masterfrom
timing-accuracy-tsf-sync
Jul 10, 2026
Merged

Hardware time distribution: beacon TX-egress TSF + TsfSync, and the measured PTP reality#230
josephnef merged 2 commits into
masterfrom
timing-accuracy-tsf-sync

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

What

The 802.11 MAC inserts its live TSF into every beacon at the instant of transmission — a genuine sub-µs hardware TX-egress timestamp. Bench-measured at 0.289 µs RMS against an independent receiver, versus 117–473 µs for any host-side "read the clock after send" shortcut. Paired with the per-frame hardware RX timestamp (RxAtrib.tsfl), it is the primitive for one-way hardware time distribution — the only sub-µs option on Wi-Fi silicon that exposes no host-reported TX-egress timestamp.

This lands the library support for that primitive, plus the measured writeup of how devourer's TSF sync compares to NTP/PTP over Wi-Fi.

Library

  • Packet::TxEgressTsf() — parse the sender's hardware egress TSF from a received beacon / probe response (any AP, not just a devourer transmitter), returned alongside tsfl.
  • TsfSync (src/TsfSync.h) — a running least-squares fit over {remote egress, local arrival} pairs → SkewPpm(), OffsetUs(), and LocalForRemote() / RemoteForLocal() clock translation. Feed it a Packet and beacons are picked automatically. Header-only, chip-neutral.
  • AdapterCaps hw_rx_timestamp / hw_beacon_txtsf flags (surfaced in the adapter.caps event), hardware-verified per generation (J2/J3 stamp beacons; J1 has no StartBeaconhw_beacon_txtsf=0).
  • rxdemo emits tx_tsf on beacons; docs/logging.md documents it.
  • Headless selftest tsf_sync_math (ctest): skew/offset recovery within 1 ppm, clock translation round-trip, the 32-bit TSF wrap, and the beacon pick.

Docs

  • docs/timing-accuracy.md — measured NTP-vs-TSF (~0.76 ms vs ~0.25 µs, ~3000×), why hardware PTP can't run on Realtek Wi-Fi (stock driver exposes no HW timestamps; TX egress is firmware-blocked), the lone ath9k exception, a real-PTP-hardware cross-check against the on-board Intel I226 (TSF tracks it to ~290 ns — its quantization floor — at +42 ppm), and the TX-egress-shortcut benchmark. Includes source links so the state can be re-checked if it changes.
  • tests/txegress_{witness.cpp,analyze.py,bench.sh} — the single-witness benchmark behind the 0.289 µs / 117 µs / 473 µs figures.

Test

ctest 16/16 green (adds tsf_sync_math). No chip HAL behavior change beyond the two additive caps flags; OFF-config builds unaffected.

🤖 Generated with Claude Code

josephnef and others added 2 commits July 10, 2026 15:24
… and the measured PTP reality

The 802.11 MAC inserts its live TSF into every beacon at the instant of
transmission — a genuine sub-µs hardware TX-egress timestamp (bench-measured
0.289 µs RMS against an independent receiver, vs 117-473 µs for any host-side
"read the clock after send" shortcut). Paired with the per-frame hardware RX
timestamp (RxAtrib.tsfl) it is the primitive for one-way hardware time
distribution — the only sub-µs option on Wi-Fi silicon that exposes no
host-reported TX-egress timestamp.

Library:
- Packet::TxEgressTsf(): parse the sender's hardware egress TSF from a received
  beacon / probe response (works against any AP), returned alongside tsfl.
- TsfSync (src/TsfSync.h): running least-squares over {remote egress, local
  arrival} pairs -> skew (ppm), offset, and clock translation; feed it a Packet
  and beacons are picked automatically. Header-only, chip-neutral.
- AdapterCaps hw_rx_timestamp / hw_beacon_txtsf flags (surfaced in adapter.caps),
  hardware-verified per generation (J2/J3 stamp beacons; J1 has no StartBeacon).
- rxdemo emits tx_tsf on beacons; docs/logging.md documents it.
- Headless selftest tsf_sync_math (ctest): skew/offset recovery, clock
  translation, the 32-bit TSF wrap, and the beacon pick.

Docs:
- docs/timing-accuracy.md: the measured NTP-vs-TSF comparison, why hardware PTP
  can't run on Realtek Wi-Fi (no exposed HW timestamps; TX egress firmware-
  blocked), the ath9k exception, the real-PTP-hardware (Intel I226) cross-check,
  and the TX-egress-shortcut benchmark, with source links.
- tests/txegress_{witness.cpp,analyze.py,bench.sh}: the single-witness bench
  behind the 0.289 us / 117 us / 473 us figures.

ctest 16/16 green; OFF-config builds unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mingw job builds an explicit target list because ctest runs every registered
test and a missing binary is a "Not Run" failure. Add the new TsfSyncSelftest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@josephnef josephnef merged commit c991805 into master Jul 10, 2026
15 checks passed
@josephnef josephnef deleted the timing-accuracy-tsf-sync branch July 10, 2026 12:36
josephnef added a commit that referenced this pull request Jul 10, 2026
…aintaining the mingw list) (#231)

## Problem

The `build-mingw` job can't build the whole tree — `rxdemo` / `txdemo` /
`precoder` use POSIX-only APIs (e.g. `fork()`), so it built a
**hand-maintained list** of targets. But `ctest` runs *every* registered
test, and any self-test binary missing from that list fails as **"Not
Run"**.

So adding a self-test meant also remembering to edit the workflow — and
forgetting broke CI the same way, repeatedly (most recently
`TsfSyncSelftest` in #230).

## Fix

Add a `selftests` aggregate target that depends on **every `*Selftest`
executable**, collected automatically by naming convention via
`BUILDSYSTEM_TARGETS` in `CMakeLists.txt`:

```cmake
add_custom_target(selftests)
get_directory_property(_dvr_targets BUILDSYSTEM_TARGETS)
foreach(_dvr_t IN LISTS _dvr_targets)
    if(_dvr_t MATCHES "Selftest$")
        add_dependencies(selftests ${_dvr_t})
    endif()
endforeach()
```

The mingw job now builds `devourer streamtx duplex selftests` instead of
the explicit list. A newly-added self-test is picked up with **no
workflow edit** and can never be silently skipped into a "Not Run".

## Verified

`cmake --build build --target devourer streamtx duplex selftests` builds
all 15 `*Selftest` binaries (including the Jaguar2-conditional
`TxPktPwrSelftest` and `StreamStdinSelftest`); `ctest` 16/16 green.
Naming convention matches every existing self-test target.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant