Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 34 additions & 11 deletions docs/time-distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,15 @@ and the cadence thereafter — by Δ TU. Bench-proven to the microsecond on an
(observer arrival phase stepped 88018 → 67565 µs at the tweak). This is the
802.11 IBSS/TSF-merge mechanism. Granularity is **1 TU = 1.024 ms** (the
`REG_BCN_INTERVAL` field is integer TU) — a coarse slot/guard-alignment lever.

**Microsecond-fine steering** (`AdjustBeaconTimingFine`, Jaguar3): the reason a
The interval register **latches at a TBTT**, so the actuator phase-aligns off
the TSF (write the tweak early in a beacon period, restore mid-way into the
first tweaked interval) — a fixed hold instead races with the beacon phase:
bench-caught on the 8821CE, an advance can silently no-op (write+restore inside
one period) or double-shift (two shortened TBTTs before the restore latches).
Consecutive coarse steers move the TBTT grid off `TSF % period == 0`; the
actuator tracks that offset (a fine steer or `StartBeacon` re-zeroes it).

**Microsecond-fine steering** (`AdjustBeaconTimingFine`): the reason a
bare `WriteTsf` doesn't move the TBTT is that the counter is latched while the
beacon function runs — the vendor `reset_tsf` path clears `EN_BCN_FUNCTION`
first. Toggling the beacon function off, shifting the port-0 TSF by the desired
Expand All @@ -173,16 +180,32 @@ resolution is what matters. It also shifts this port's TSF + beacon-body
timestamp by the same amount, which is the intended UE-advances-its-own-timebase
behaviour.

Beacon-TBTT steering is **Jaguar3-only**. The Jaguar2 8822B beacon engine drops
the beacon on *any* TBTT re-latch — bench-proven on the 8812BU, the beacon stops
airing after both the interval tweak and the beacon-function toggle (the
bcn-valid latch is lost, and J2 does not retain the beacon bytes to re-download).
So `AdjustBeaconTiming` / `AdjustBeaconTimingFine` refuse on J2 (return 0) rather
than silently kill the beacon; the downlink (`StartBeacon` + `SetCcaMode`) is
unaffected.
**Jaguar2 steer-then-re-download.** The Jaguar2 beacon engine loses its
bcn-valid latch on *any* TBTT re-latch — bench-proven on the 8812BU, the beacon
stops airing after both the interval tweak and the beacon-function toggle — and
the hardware does not retain the reserved-page bytes to re-arm it. The driver
does (`StartBeacon` keeps the MPDU), so the J2 actuators steer and then re-run
the reserved-page beacon download to re-assert the latch — at most one skipped
beacon per correction. Bench (fine steers, observer arrival phase, hardware seq
consecutive across every steer):

- **8812BU (USB)**: 0–1 beacon lost per steer, ~9 ms per fine steer; the fine
shift undershoots the request by a systematic ~2–3 ms (USB register latency,
larger than J3's ~0.5–1.2 ms) that a closed loop absorbs.
- **8821CE (PCIe MMIO)**: 0–1 beacon lost per steer, ~0–1 ms per fine steer;
fine accuracy **−4842…−5047 µs for a −5000 µs request** (systematic offset
~30 µs — the MMIO register path is µs-scale) and coarse −20 TU steers land
within ~±150 µs, including back-to-back. This is the actuator that closes
the AP-beacon ↔ network-PTP discipline loop on the Radxa X4's 8821CE.

At a realistic discipline cadence (~10 s between corrections against ~40 ppm
crystal drift and a ~500 µs guard) the cost is ~1 lost beacon per 100. Jaguar3
needs no re-download — its engine survives the re-latch.

Actuator characterization: `tests/beacon_interval_shift.sh <short_TU>` (TU tweak),
or `FINE_US=<µs> tests/beacon_interval_shift.sh` (µs-fine, Jaguar3).
or `FINE_US=<µs> tests/beacon_interval_shift.sh` (µs-fine). Steer *survival* +
repeated-steer accuracy (incl. a PCIe master via `MASTER_CMD='ssh …'`):
`tests/beacon_steer_survival.sh [n] [steer_us] [period_s]`.

Harness: `tests/timesync_ta_demo.sh` (+ `tests/timesync_ta_analyze.py`).

Expand All @@ -195,7 +218,7 @@ Harness: `tests/timesync_ta_demo.sh` (+ `tests/timesync_ta_analyze.py`).
- `DEVOURER_TSYNC_HWBEACON=1` — hardware-timed beacon downlink (StartBeacon) →
sub-µs; set on both master and slave. On the **UE** (role=ue) it instead
selects the hardware-beacon uplink fine-steered by `AdjustBeaconTimingFine` —
the converging closed loop (J3 UE required for µs steering)
the converging closed loop (Jaguar2/3 UE)
- `DEVOURER_TSYNC_CSMA=1` — keep CSMA on the HW-beacon master (default: EDCCA
off so the beacon airs exactly at TBTT — the master owns the channel)
- `DEVOURER_TSYNC_UPLINK=1` — enable the uplink timing-advance loop (experimental)
Expand Down
15 changes: 8 additions & 7 deletions src/IRtlDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,10 @@ class IRtlDevice {
* microsecond). Requires an active StartBeacon. BLOCKS the caller ~one beacon
* interval (the tweaked interval must latch and fire once before restore).
* Returns the actual applied shift in µs (TU-quantized); 0 if no active beacon
* or |microseconds| < 512. Jaguar3 only in practice: the Jaguar2 8822B beacon
* engine drops the beacon on any TBTT re-latch (bench-proven), so J2 refuses
* (returns 0) rather than silently kill it. Base is a no-op. */
* or |microseconds| < 512. Jaguar3 and Jaguar2: the J2 beacon engine loses its
* bcn-valid latch on any TBTT re-latch (bench-proven), so the J2 path follows
* the steer with a reserved-page re-download of the retained beacon — one
* skipped beacon per correction. Base is a no-op. */
virtual int32_t AdjustBeaconTiming(int32_t microseconds) {
(void)microseconds;
return 0;
Expand All @@ -324,10 +325,10 @@ class IRtlDevice {
* same amount, which is the intended behaviour for a UE advancing its own
* timebase. The USB read→write latency adds a sub-ms offset (~0.5–1.2 ms) that
* a closed timing-advance loop absorbs — the *resolution* is microseconds.
* Requires an active StartBeacon; returns the applied shift in µs. Jaguar3 only:
* the Jaguar2 beacon engine survives neither the beacon-function toggle nor the
* interval tweak (both drop the beacon), so J2 refuses (returns 0). Base is a
* no-op. */
* Requires an active StartBeacon; returns the applied shift in µs. Jaguar3 and
* Jaguar2: the J2 beacon engine drops its bcn-valid latch on the toggle, so the
* J2 path re-downloads the retained reserved-page beacon after the re-latch —
* one skipped beacon per correction. Base is a no-op. */
virtual int32_t AdjustBeaconTimingFine(int32_t microseconds) {
(void)microseconds;
return 0;
Expand Down
137 changes: 119 additions & 18 deletions src/jaguar2/RtlJaguar2Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1408,34 +1408,135 @@ bool RtlJaguar2Device::StartBeacon(const uint8_t *beacon, size_t len,
_device.rtw_write8(0x0550 /* REG_BCN_CTRL */, (1u << 3) | (1u << 4));
uint32_t txq = _device.rtw_read<uint32_t>(0x0420 /* REG_FWHW_TXQ_CTRL */);
_device.rtw_write<uint32_t>(0x0420, txq | (1u << 22) /* BIT_EN_BCNQ_DL */);
/* Retain the MPDU + interval for the TBTT-steer re-download
* (AdjustBeaconTiming*): the J2 engine loses the bcn-valid latch on any
* re-latch and the hardware does not keep the reserved-page bytes. */
_bcn_mpdu.assign(mpdu, mpdu + mpdu_len);
_bcn_interval_tu = interval_tu > 0 ? interval_tu : 100;
_tbtt_off_us = 0; // fresh beacon function: TBTT grid at TSF % period == 0
_logger->info("beacon(J2): beacon@rsvd_boundary, net_type->AP, BCN_CTRL=0x18, "
"EN_BCNQ_DL (interval {} TU)", interval_tu);
return true;
}

/* Re-download the retained reserved-page beacon to re-arm the bcn-valid latch.
* The J2 beacon engine loses the latch on ANY TBTT re-latch (bench-proven on
* the 8812BU: the beacon stops airing after both the one-shot interval tweak
* and the EN_BCN_FUNCTION toggle), and the hardware does not keep the
* reserved-page bytes — but the driver does (StartBeacon). So every steer is
* "re-latch, then re-download": the TBTT re-derives from the steered timebase
* and the fresh download re-asserts the valid latch (its poll is the success
* signal). Costs one skipped beacon per correction. Caller holds _reg_mu. */
bool RtlJaguar2Device::redownload_beacon_locked() {
if (_bcn_mpdu.empty())
return false;
if (!_fw.download_rsvd_page(_fw.rsvd_boundary(), _bcn_mpdu.data(),
static_cast<uint32_t>(_bcn_mpdu.size()))) {
_logger->error("beacon(J2): TBTT-steer rsvd-page re-download failed — "
"beacon may have stopped airing");
return false;
}
return true;
}

int32_t RtlJaguar2Device::AdjustBeaconTiming(int32_t microseconds) {
(void)microseconds;
/* Beacon-TBTT STEERING IS NOT SUPPORTED ON JAGUAR2. Both mechanisms that work
* on Jaguar3 drop the beacon on the 8822B engine (bench-proven on the 8812BU,
* the beacon stops airing after the tweak): the one-shot REG_BCN_INTERVAL
* (0x0554) tweak AND the EN_BCN_FUNCTION-toggle + TSF-shift both lose the
* bcn-valid latch, and J2 does not retain the beacon bytes to re-download and
* re-assert it. So refuse rather than silently kill the beacon. (A fix would
* store the beacon bytes in StartBeacon and re-download after the tweak — the
* Jaguar3 store-bytes path — but that needs its own hardware validation.) The
* downlink (StartBeacon + SetCcaMode) is unaffected; only the uplink-TA
* actuator is J3-only. */
static bool warned = false;
if (!warned) {
warned = true;
_logger->warn("beacon(J2): TBTT steering not supported (8822B beacon engine "
"drops the beacon on a TBTT re-latch) — uplink-TA is Jaguar3-only");
int nominal;
{
std::lock_guard<std::mutex> lk(_reg_mu);
nominal = _bcn_interval_tu;
}
if (nominal <= 0) return 0; // no active beacon
/* Round to whole TU (REG_BCN_INTERVAL is integer TU); sign follows the request
* (>0 = later/retard => longer one-shot interval, <0 = earlier/advance). */
int delta_tu = (microseconds >= 0 ? microseconds + 512 : microseconds - 512) / 1024;
if (delta_tu == 0) return 0; // below 1-TU resolution
int one = nominal + delta_tu;
if (one < 1) { // can't shorten below one TU
one = 1;
delta_tu = one - nominal;
}
/* Latch one interval at the tweaked length; after exactly one TBTT fires
* under it the phase has advanced/retarded by delta_tu TU, then restore
* nominal (the J3 mechanism). On J2 the tweak drops the bcn-valid latch, so
* follow the restore with the reserved-page re-download.
*
* The interval register latches at a TBTT, so the shift count = the number
* of TBTTs between the tweak-latch and the restore-latch — a fixed sleep
* races with the beacon phase (bench-caught on the 8821CE: an advance whose
* write+restore land inside one period is a silent no-op; one held past two
* shortened periods double-shifts). Phase-align off the TSF instead (TBTT
* fires at TSF % interval == 0): read the in-period position, write the
* tweak, then time the restore to land mid-way into the FIRST tweaked
* interval — the restore latches at that interval's closing TBTT, so exactly
* one fires under the tweak. Positions are measured against the TBTT grid
* (TSF % period == _tbtt_off_us — prior coarse steers move the grid off the
* TSF). Serialize register access on _reg_mu but release it across the
* waits so the coex/thermal tick isn't starved. */
const int64_t period_us = static_cast<int64_t>(nominal) * 1024;
auto grid_pos = [&]() { // in-period position vs the TBTT grid; under _reg_mu
uint32_t hi = _device.rtw_read<uint32_t>(0x0564);
uint32_t lo = _device.rtw_read<uint32_t>(0x0560);
int64_t p = static_cast<int64_t>(((static_cast<uint64_t>(hi) << 32) | lo) %
static_cast<uint64_t>(period_us));
return ((p - _tbtt_off_us) % period_us + period_us) % period_us;
};
int64_t pos;
{
std::lock_guard<std::mutex> lk(_reg_mu);
pos = grid_pos();
}
/* Keep the tweak write clear of the next TBTT (so the latching TBTT is
* unambiguous even against register/sleep jitter). */
if (pos > period_us - 20000)
std::this_thread::sleep_for(
std::chrono::microseconds(period_us - pos + 5000));
{
std::lock_guard<std::mutex> lk(_reg_mu);
pos = grid_pos();
_device.rtw_write16(0x0554 /* REG_BCN_INTERVAL */, static_cast<uint16_t>(one));
}
/* latch TBTT in (period - pos) µs; restore mid-first-tweaked-interval. */
std::this_thread::sleep_for(std::chrono::microseconds(
(period_us - pos) + static_cast<int64_t>(one) * 512));
{
std::lock_guard<std::mutex> lk(_reg_mu);
_device.rtw_write16(0x0554, static_cast<uint16_t>(nominal));
_tbtt_off_us = ((_tbtt_off_us + static_cast<int64_t>(delta_tu) * 1024) %
period_us + period_us) % period_us;
if (!redownload_beacon_locked())
return 0;
}
return 0;
_logger->info("beacon(J2): TBTT shift {} TU ({} us) via one-shot interval "
"{}->{}->{} TU + rsvd-page re-download",
delta_tu, delta_tu * 1024, nominal, one, nominal);
return delta_tu * 1024;
}

int32_t RtlJaguar2Device::AdjustBeaconTimingFine(int32_t microseconds) {
return AdjustBeaconTiming(microseconds); // both unsupported on J2 (beacon drops)
std::lock_guard<std::mutex> lk(_reg_mu);
if (_bcn_interval_tu <= 0) return 0; // no active beacon
/* Same mechanism as J3: a bare TSF write with the beacon function running
* leaves the TBTT latched, so toggle the beacon function off, shift the
* port-0 TSF, toggle on — the TBTT counter re-derives from the shifted TSF
* at microsecond resolution (TBTT fires at TSF % interval, so subtracting
* `microseconds` advances (<0) / retards (>0) the next boundary by that many
* µs). On J2 the toggle drops the bcn-valid latch, so re-download the
* reserved-page beacon afterwards to re-arm it. */
uint32_t hi = _device.rtw_read<uint32_t>(0x0564);
uint32_t lo = _device.rtw_read<uint32_t>(0x0560);
uint64_t tsf = (static_cast<uint64_t>(hi) << 32) | lo;
uint64_t nt = tsf - static_cast<uint64_t>(static_cast<int64_t>(microseconds));
uint8_t bc = _device.rtw_read8(0x0550 /* REG_BCN_CTRL */);
_device.rtw_write8(0x0550, static_cast<uint8_t>(bc & ~(1u << 3))); // clear EN_BCN_FUNCTION
_device.rtw_write<uint32_t>(0x0560, static_cast<uint32_t>(nt));
_device.rtw_write<uint32_t>(0x0564, static_cast<uint32_t>(nt >> 32));
_device.rtw_write8(0x0550, static_cast<uint8_t>(bc | (1u << 3))); // set EN_BCN_FUNCTION
_tbtt_off_us = 0; // the re-latch re-derives the TBTT grid from the TSF
if (!redownload_beacon_locked())
return 0;
_logger->info("beacon(J2): fine TBTT shift {} us (TSF toggle + rsvd-page "
"re-download)", microseconds);
return microseconds;
}

void RtlJaguar2Device::SetCcaMode(bool disabled) {
Expand Down
22 changes: 22 additions & 0 deletions src/jaguar2/RtlJaguar2Device.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <mutex>
#include <optional>
#include <thread>
#include <vector>

#include "logger.h"
#include "IRtlDevice.h"
Expand Down Expand Up @@ -87,6 +88,10 @@ class RtlJaguar2Device : public IRtlDevice {
/* Disable/restore the MAC EDCCA gate (BIT_DIS_EDCCA 0x520[15] + EDCCA-mask
* 0x524[11] — HalMAC-common with J3) so a TBTT beacon airs on schedule. */
void SetCcaMode(bool disabled) override;
/* Beacon-TBTT steering (IRtlDevice contract). The J2 engine loses the
* bcn-valid latch on ANY TBTT re-latch (bench-proven on the 8812BU), so both
* actuators steer then re-download the retained reserved-page beacon to
* re-arm the latch — one skipped beacon per correction. */
int32_t AdjustBeaconTiming(int32_t microseconds) override;
int32_t AdjustBeaconTimingFine(int32_t microseconds) override;
void Stop() override;
Expand Down Expand Up @@ -270,6 +275,23 @@ class RtlJaguar2Device : public IRtlDevice {
void start_pwrtrack();
void stop_pwrtrack();

/* Beacon retained for the TBTT-steer re-download (AdjustBeaconTiming*): the
* J2 engine loses the bcn-valid latch on any TBTT re-latch and does not keep
* the reserved-page bytes, so the steer path re-downloads this copy to
* re-arm it. Guarded by _reg_mu (written in StartBeacon, read in the
* steer actuators). _bcn_interval_tu = 0 means no active beacon. */
std::vector<uint8_t> _bcn_mpdu;
int _bcn_interval_tu = 0;
/* TBTT-grid offset vs the TSF, in µs: TBTT fires at TSF % period == this.
* 0 after StartBeacon and after every fine steer (the EN_BCN_FUNCTION
* re-latch re-derives the grid from the TSF); each coarse interval-tweak
* steer moves the grid by its applied shift without moving the TSF, so the
* coarse path's TBTT phase-alignment must subtract it. Guarded by _reg_mu. */
int64_t _tbtt_off_us = 0;
/* Re-download _bcn_mpdu to the reserved page (re-arms the bcn-valid latch
* after a TBTT re-latch). Caller holds _reg_mu. */
bool redownload_beacon_locked();

/* StartRxLoop stop request (StopRxLoop). volatile (not atomic) to match the
* signal-flag pattern used across the library (g_devourer_should_stop). */
volatile bool _rx_stop = false;
Expand Down
Loading
Loading