first
This commit is contained in:
@@ -0,0 +1,531 @@
|
||||
// timesync — LTE-eNB-style over-the-air time distribution. One binary, one
|
||||
// adapter, one role (compose scenarios by running instances):
|
||||
//
|
||||
// DEVOURER_TSYNC_ROLE=master TX-only. Every DEVOURER_TSYNC_INTERVAL_MS,
|
||||
// broadcast a sync beacon stamped with the chip's
|
||||
// hardware TSF (ReadTsf(), reliable TX-side — no
|
||||
// RX flood starving the control read). This is the
|
||||
// eNB distributing its SFN.
|
||||
// DEVOURER_TSYNC_ROLE=slave RX-only. Lock a running fit of the master's
|
||||
// broadcast TSF against this slave's own per-frame
|
||||
// hardware TSF, PREDICT each beacon before it
|
||||
// arrives, and emit the prediction error — how
|
||||
// tightly this UE tracks the eNB. No host clock,
|
||||
// no GPS.
|
||||
//
|
||||
// Run a master + two slaves and join the slaves' {"ev":"timesync.lock"} streams
|
||||
// on `seq`: pred_master_A vs pred_master_B is the inter-UE sync error, measured
|
||||
// without either slave touching a wall clock (tests/timesync_demo.sh).
|
||||
//
|
||||
// UPLINK TIMING ADVANCE (DEVOURER_TSYNC_UPLINK=1, roles master + ue) is the LTE
|
||||
// closed-loop extension — a full-duplex master phase-measures each UE uplink
|
||||
// against its TSF slot grid and feeds back a timing advance. It is EXPERIMENTAL:
|
||||
// the control math converges in the headless selftest and the full-duplex
|
||||
// plumbing works on-air (arrivals cluster tightly, ~±0.2 ms with a FIXED TA),
|
||||
// but the closed loop does NOT converge on the bench — a fixed-TA authority test
|
||||
// shows the TA shifts the UE's send-CALL time 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 air departure (plus this bench has only one clean
|
||||
// full-duplex Jaguar2/3 adapter; the 8822E desenses its RX in TX+RX). See
|
||||
// docs and tests/timesync_ta_demo.sh.
|
||||
//
|
||||
// See timesync.h for the fit + env knobs. Metrics are JSONL on stdout.
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
#include <libusb.h>
|
||||
#elif defined(__MINGW32__) || defined(__MINGW64__)
|
||||
#include <libusb-1.0/libusb.h>
|
||||
#elif defined(__APPLE__) || defined(__ANDROID__)
|
||||
#include <libusb.h>
|
||||
#else
|
||||
#include <libusb-1.0/libusb.h>
|
||||
#endif
|
||||
|
||||
#include "DeviceSession.h"
|
||||
#include "RadiotapBuilder.h"
|
||||
#include "RxPacket.h"
|
||||
#include "SignalStop.h"
|
||||
#include "UsbOpen.h"
|
||||
#include "WiFiDriver.h"
|
||||
#include "env_config.h"
|
||||
#include "logger.h"
|
||||
#include "timesync.h"
|
||||
#if defined(DEVOURER_HAVE_PCIE)
|
||||
#include "PcieTransport.h"
|
||||
#endif
|
||||
|
||||
#define USB_VENDOR_ID 0x0bda
|
||||
static constexpr uint16_t kRealtekProductIds[] = {
|
||||
0x8812, 0x0811, 0xa811, 0xb811, 0x8813,
|
||||
};
|
||||
|
||||
static inline void sleep_ms(long ms) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
|
||||
}
|
||||
static void emit(const char* json) { std::fputs(json, stdout); std::fflush(stdout); }
|
||||
|
||||
// --- device open (mirrors examples/tdma/main.cpp) ---------------------------
|
||||
static libusb_device_handle* open_device(
|
||||
const std::shared_ptr<Logger>& logger, libusb_context** ctx,
|
||||
std::shared_ptr<devourer::UsbDeviceLock>& lock) {
|
||||
if (libusb_init(ctx) < 0) return nullptr;
|
||||
libusb_set_option(*ctx, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_WARNING);
|
||||
uint16_t vid = USB_VENDOR_ID, pid = 0;
|
||||
if (const char* v = std::getenv("DEVOURER_VID")) vid = (uint16_t)strtoul(v, 0, 0);
|
||||
if (const char* p = std::getenv("DEVOURER_PID")) pid = (uint16_t)strtoul(p, 0, 0);
|
||||
libusb_device_handle* h = nullptr;
|
||||
for (uint16_t p : kRealtekProductIds) {
|
||||
if (pid != 0 && p != pid) continue;
|
||||
h = libusb_open_device_with_vid_pid(*ctx, vid, p);
|
||||
if (h) break;
|
||||
}
|
||||
if (!h && pid != 0) h = libusb_open_device_with_vid_pid(*ctx, vid, pid);
|
||||
if (!h) { logger->error("no device {:04x}:{:04x}", vid, pid); return nullptr; }
|
||||
if (devourer::claim_interface_then_reset(h, devourer::find_wifi_interface(h), logger, std::getenv("DEVOURER_SKIP_RESET") == nullptr, lock) != 0) {
|
||||
logger->error("claim failed (busy?)");
|
||||
return nullptr;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
// --- MASTER (eNB): broadcast the hardware TSF -------------------------------
|
||||
// A compact raw 802.11 beacon MPDU (canonical SA/BSSID) matching the
|
||||
// bench-validated tests/beacon_tbtt.cpp beacon. The 8-byte timestamp is left
|
||||
// zero; the MAC inserts the hardware TSF at each TBTT. A full kernel-AP body
|
||||
// (SSID + rates + DS + TIM + ...) also inserts the TSF correctly
|
||||
// (tests/beacon_fullbody.cpp) — this stays compact only because the timesync
|
||||
// demo needs no extra IEs.
|
||||
static std::vector<uint8_t> build_std_beacon(int interval_tu) {
|
||||
return {
|
||||
0x80, 0x00, 0x00, 0x00, // FC beacon + dur
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // addr1 broadcast
|
||||
0x57, 0x42, 0x75, 0x05, 0xd6, 0x00, // addr2 = SA (canonical)
|
||||
0x57, 0x42, 0x75, 0x05, 0xd6, 0x00, // addr3 = BSSID
|
||||
0x00, 0x00, // seq
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // timestamp (HW fills)
|
||||
static_cast<uint8_t>(interval_tu & 0xff),
|
||||
static_cast<uint8_t>((interval_tu >> 8) & 0xff), // beacon interval
|
||||
0x00, 0x00, // capability
|
||||
0x00, 0x03, 'T', 'B', 'T', // SSID IE
|
||||
0x01, 0x01, 0x82}; // supported rates (1M)
|
||||
}
|
||||
|
||||
static void run_master(IRadio* dev, const timesync::Config& c) {
|
||||
dev->InitWrite(SelectedChannel{c.channel, 0, CHANNEL_WIDTH_20});
|
||||
sleep_ms(2000);
|
||||
if (c.hwbeacon) {
|
||||
// Hardware-timed, hardware-TSF-stamped beacon at TBTT — no software send loop,
|
||||
// no ReadTsf jitter. The MAC inserts the live TSF into the beacon at TX.
|
||||
if (c.no_csma) {
|
||||
// The master owns the channel: disable EDCCA so the beacon airs exactly on
|
||||
// the TBTT schedule (no CSMA backoff). Collapses the downlink residual from
|
||||
// ~hundreds of µs to sub-µs even on a crowded channel.
|
||||
dev->SetCcaMode(true);
|
||||
fprintf(stderr, "timesync master: CSMA/EDCCA disabled (master owns channel)\n");
|
||||
}
|
||||
auto b = build_std_beacon(c.interval_ms > 0 ? c.interval_ms * 1000 / 1024 : 100);
|
||||
bool ok = dev->StartBeacon(b.data(), b.size(),
|
||||
c.interval_ms > 0 ? c.interval_ms * 1000 / 1024 : 100);
|
||||
fprintf(stderr, "timesync master(HW beacon): StartBeacon -> %s, ch%d\n",
|
||||
ok ? "OK" : "UNSUPPORTED", c.channel);
|
||||
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(c.secs);
|
||||
while (!g_devourer_should_stop) {
|
||||
sleep_ms(200);
|
||||
if (c.secs && std::chrono::steady_clock::now() >= deadline) break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const auto rt = devourer::build_stream_radiotap(c.rate);
|
||||
fprintf(stderr, "timesync master: ch%d, sync beacon every %d ms\n", c.channel,
|
||||
c.interval_ms);
|
||||
|
||||
uint32_t seq = 0;
|
||||
auto next_stat = std::chrono::steady_clock::now() + std::chrono::seconds(1);
|
||||
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(c.secs);
|
||||
while (!g_devourer_should_stop) {
|
||||
// Stamp with the master's hardware TSF at send time. TX-side ReadTsf() is
|
||||
// reliable (no bulk-IN flood), unlike on a busy receiver.
|
||||
uint64_t tsf = dev->ReadTsf();
|
||||
auto f = tdma::build_frame(rt, tdma::Class::Marker, seq++, 0, tsf);
|
||||
dev->send_packet(f.data(), f.size());
|
||||
|
||||
if (std::chrono::steady_clock::now() >= next_stat) {
|
||||
next_stat += std::chrono::seconds(1);
|
||||
char buf[160];
|
||||
std::snprintf(buf, sizeof(buf),
|
||||
"{\"ev\":\"timesync.master\",\"beacons\":%u,\"tsf\":%llu}\n",
|
||||
seq, (unsigned long long)tsf);
|
||||
emit(buf);
|
||||
}
|
||||
if (c.secs && std::chrono::steady_clock::now() >= deadline) break;
|
||||
sleep_ms(c.interval_ms);
|
||||
}
|
||||
fprintf(stderr, "timesync master: %u beacons sent\n", seq);
|
||||
}
|
||||
|
||||
// --- SLAVE (UE): lock to the master, predict each beacon --------------------
|
||||
static timesync::Recon g_recon;
|
||||
static timesync::LinFit g_fit;
|
||||
static std::mutex g_mu;
|
||||
static uint64_t g_beacons = 0; // master frames heard
|
||||
static uint64_t g_predicted = 0; // frames predicted (fit was ready)
|
||||
static double g_resid_ss = 0; // Σ resid² (µs²), for RMS
|
||||
static double g_resid_max = 0;
|
||||
|
||||
static bool g_hwbeacon = false;
|
||||
|
||||
static void slave_cb(const Packet& p) {
|
||||
uint64_t master_tsf; uint32_t seq;
|
||||
if (g_hwbeacon) {
|
||||
// Standard 802.11 beacon: canonical SA at addr2, and the master's LIVE
|
||||
// hardware TSF is the 8-byte timestamp field (MPDU offset 24). No TD tag.
|
||||
static const uint8_t kSa[6] = {0x57, 0x42, 0x75, 0x05, 0xd6, 0x00};
|
||||
if (p.Data.size() < 32 || p.RxAtrib.crc_err) return;
|
||||
if ((p.Data[0] & 0xfc) != 0x80) return; // beacon subtype
|
||||
if (std::memcmp(p.Data.data() + 10, kSa, 6) != 0) return; // our master
|
||||
master_tsf = 0;
|
||||
for (int i = 0; i < 8; ++i) master_tsf |= (uint64_t)p.Data[24 + i] << (8 * i);
|
||||
seq = (uint32_t)(p.RxAtrib.seq_num);
|
||||
} else {
|
||||
auto pr = tdma::parse_frame(p.Data.data(), p.Data.size());
|
||||
if (!pr.ok || p.RxAtrib.crc_err) return;
|
||||
if (pr.cls != tdma::Class::Marker || pr.tx_tsf == 0) return; // sync beacons only
|
||||
master_tsf = pr.tx_tsf; seq = pr.seq;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
double local_us = (double)g_recon(p.RxAtrib.tsfl);
|
||||
double master_us = (double)master_tsf;
|
||||
++g_beacons;
|
||||
|
||||
// Predict this beacon's master TSF from the fit built on PRIOR beacons,
|
||||
// evaluated at this beacon's clean local hardware TSF. resid = lock error.
|
||||
if (g_fit.ready()) {
|
||||
double pred = g_fit.at(local_us);
|
||||
double resid = master_us - pred;
|
||||
++g_predicted;
|
||||
g_resid_ss += resid * resid;
|
||||
if (std::fabs(resid) > g_resid_max) g_resid_max = std::fabs(resid);
|
||||
char buf[256];
|
||||
std::snprintf(buf, sizeof(buf),
|
||||
"{\"ev\":\"timesync.lock\",\"seq\":%u,\"master_tsf\":%llu,"
|
||||
"\"local_tsf\":%llu,\"pred_master\":%.1f,\"resid_us\":%.2f,"
|
||||
"\"ppm\":%.2f}\n",
|
||||
seq, (unsigned long long)master_tsf,
|
||||
(unsigned long long)(int64_t)local_us, pred, resid, g_fit.ppm());
|
||||
emit(buf);
|
||||
}
|
||||
g_fit.add(local_us, master_us);
|
||||
}
|
||||
|
||||
static void run_slave(IRadio* dev, const timesync::Config& c) {
|
||||
std::thread rx([&] {
|
||||
dev->Init(slave_cb, SelectedChannel{c.channel, 0, CHANNEL_WIDTH_20});
|
||||
});
|
||||
fprintf(stderr, "timesync slave: ch%d, locking to master beacons\n", c.channel);
|
||||
|
||||
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(c.secs);
|
||||
while (!g_devourer_should_stop) {
|
||||
sleep_ms(100);
|
||||
if (c.secs && std::chrono::steady_clock::now() >= deadline) break;
|
||||
}
|
||||
dev->StopRxLoop();
|
||||
rx.join();
|
||||
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
double rms = g_predicted ? std::sqrt(g_resid_ss / (double)g_predicted) : 0;
|
||||
fprintf(stderr,
|
||||
"\n=== timesync slave summary ===\n"
|
||||
" beacons heard : %llu\n"
|
||||
" predicted : %llu\n"
|
||||
" lock error (RMS) : %.2f us max %.2f us\n"
|
||||
" master-vs-slave ppm: %.2f\n",
|
||||
(unsigned long long)g_beacons, (unsigned long long)g_predicted, rms,
|
||||
g_resid_max, g_fit.ppm());
|
||||
}
|
||||
|
||||
// --- UPLINK TIMING ADVANCE (LTE TA), full-duplex ---------------------------
|
||||
// The master broadcasts beacons AND phase-measures each UE uplink against its
|
||||
// own TSF slot grid (arrival tsfl mod slot_us — reliable per-frame, no ReadTsf
|
||||
// which the RX loop would starve), integrating a per-UE timing-advance it feeds
|
||||
// back. The UE schedules its uplinks off the beacon-arrival cadence (a seq↔host
|
||||
// fit — rate-locked to the master) minus the TA. The loop drives each uplink's
|
||||
// arrival onto the master's slot boundary; open-loop it drifts across the slot
|
||||
// at the crystal offset. Both nodes are full-duplex (InitWrite + StartRxLoop).
|
||||
static int64_t steady_us() {
|
||||
return std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
// Master TA state.
|
||||
static timesync::Recon g_m_recon;
|
||||
static std::atomic<double> g_ta_us{0};
|
||||
static std::mutex g_m_mu;
|
||||
static uint64_t g_uplinks = 0;
|
||||
static double g_phase_ss = 0, g_phase_max = 0;
|
||||
static uint64_t g_phase_n = 0;
|
||||
static double g_slot_us = 20000, g_ta_gain = 0.3;
|
||||
static bool g_ta_fixed = false; // DEVOURER_TSYNC_TA_FIXED: hold TA constant (authority test)
|
||||
|
||||
static void master_ta_cb(const Packet& p) {
|
||||
auto pr = tdma::parse_frame(p.Data.data(), p.Data.size());
|
||||
if (!pr.ok || p.RxAtrib.crc_err) return;
|
||||
if (static_cast<uint8_t>(pr.cls) != timesync::kClassUplink) return;
|
||||
std::lock_guard<std::mutex> lk(g_m_mu);
|
||||
double arrival = (double)g_m_recon(p.RxAtrib.tsfl);
|
||||
double phase = arrival - std::round(arrival / g_slot_us) * g_slot_us; // (-slot/2, slot/2]
|
||||
double ta = g_ta_us.load();
|
||||
if (!g_ta_fixed) {
|
||||
ta += g_ta_gain * phase; // late (phase>0) → more TA → UE earlier
|
||||
if (ta > g_slot_us) ta = g_slot_us; // clamp to one slot (no wrap — a hard
|
||||
if (ta < -g_slot_us) ta = -g_slot_us; // mod jump kicks the loop)
|
||||
g_ta_us.store(ta);
|
||||
}
|
||||
++g_uplinks; g_phase_ss += phase * phase; ++g_phase_n;
|
||||
if (std::fabs(phase) > g_phase_max) g_phase_max = std::fabs(phase);
|
||||
char buf[224];
|
||||
std::snprintf(buf, sizeof(buf),
|
||||
"{\"ev\":\"timesync.ta\",\"seq\":%u,\"n\":%llu,\"phase_us\":%.2f,"
|
||||
"\"ta_us\":%.2f}\n",
|
||||
pr.seq, (unsigned long long)g_uplinks, phase, ta);
|
||||
emit(buf);
|
||||
}
|
||||
|
||||
static void run_master_ta(IRadio* dev, const timesync::Config& c) {
|
||||
g_slot_us = c.slot_ms * 1000.0; g_ta_gain = c.ta_gain;
|
||||
if (const char* f = std::getenv("DEVOURER_TSYNC_TA_FIXED")) {
|
||||
g_ta_fixed = true; g_ta_us.store(std::atof(f)); // authority test: hold TA constant
|
||||
}
|
||||
dev->InitWrite(SelectedChannel{c.channel, 0, CHANNEL_WIDTH_20});
|
||||
std::thread rx([&] { dev->StartRxLoop(master_ta_cb); });
|
||||
sleep_ms(2000);
|
||||
const auto rt = devourer::build_stream_radiotap(c.rate);
|
||||
fprintf(stderr, "timesync master(TA): ch%d beacons=%dms slot=%dms gain=%.2f\n",
|
||||
c.channel, c.interval_ms, c.slot_ms, c.ta_gain);
|
||||
|
||||
uint32_t seq = 0;
|
||||
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(c.secs);
|
||||
while (!g_devourer_should_stop) {
|
||||
// The beacon carries the current TA in its tx_tsf field (unused for cadence),
|
||||
// so every beacon the UE decodes delivers the latest TA — one send per slot,
|
||||
// no separate frame to drop.
|
||||
int64_t ta_i = (int64_t)std::llround(g_ta_us.load()); // signed µs → 8-byte tag
|
||||
uint64_t ta_b; std::memcpy(&ta_b, &ta_i, 8);
|
||||
auto b = tdma::build_frame(rt, tdma::Class::Marker, seq++, 0, ta_b);
|
||||
dev->send_packet(b.data(), b.size());
|
||||
if (c.secs && std::chrono::steady_clock::now() >= deadline) break;
|
||||
sleep_ms(c.interval_ms);
|
||||
}
|
||||
dev->StopRxLoop(); rx.join();
|
||||
std::lock_guard<std::mutex> lk(g_m_mu);
|
||||
double rms = g_phase_n ? std::sqrt(g_phase_ss / (double)g_phase_n) : 0;
|
||||
fprintf(stderr,
|
||||
"\n=== timesync master(TA) summary ===\n"
|
||||
" uplinks measured : %llu\n"
|
||||
" arrival phase (all): RMS %.2f us max %.2f us\n"
|
||||
" final TA : %.2f us\n",
|
||||
(unsigned long long)g_uplinks, rms, g_phase_max, g_ta_us.load());
|
||||
}
|
||||
|
||||
// UE state. Event-driven off actual beacon arrivals (rate-locked to the master),
|
||||
// so the TA directly and unambiguously shifts the uplink's arrival phase.
|
||||
static std::atomic<int64_t> g_ue_beacon_us{0}; // steady_us of the last beacon
|
||||
static std::atomic<uint32_t> g_ue_beacon_seq{0};
|
||||
static std::atomic<bool> g_ue_have{false};
|
||||
static std::atomic<double> g_ue_ta_us{0};
|
||||
static std::atomic<uint64_t> g_ue_beacons{0}, g_ue_tx{0};
|
||||
|
||||
static void ue_cb(const Packet& p) {
|
||||
auto pr = tdma::parse_frame(p.Data.data(), p.Data.size());
|
||||
if (!pr.ok || p.RxAtrib.crc_err) return;
|
||||
if (pr.cls == tdma::Class::Marker) {
|
||||
g_ue_beacon_us.store(steady_us(), std::memory_order_relaxed);
|
||||
g_ue_beacon_seq.store(pr.seq, std::memory_order_relaxed);
|
||||
g_ue_have.store(true, std::memory_order_relaxed);
|
||||
g_ue_beacons.fetch_add(1, std::memory_order_relaxed);
|
||||
int64_t ta_i; uint64_t ta_b = pr.tx_tsf; std::memcpy(&ta_i, &ta_b, 8); // TA rides the beacon
|
||||
g_ue_ta_us.store((double)ta_i, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
static void run_ue(IRadio* dev, const timesync::Config& c) {
|
||||
dev->InitWrite(SelectedChannel{c.channel, 0, CHANNEL_WIDTH_20});
|
||||
std::thread rx([&] { dev->StartRxLoop(ue_cb); });
|
||||
sleep_ms(2000);
|
||||
const auto rt = devourer::build_stream_radiotap(c.rate);
|
||||
const int64_t slot_us = (int64_t)c.slot_ms * 1000;
|
||||
|
||||
// --- Hardware-beacon uplink (fine-steered) — DEVOURER_TSYNC_HWBEACON --------
|
||||
// The send_packet uplink below has no sub-slot air-departure control (the chip
|
||||
// airs the queued frame on its own schedule), so the TA loop cannot converge
|
||||
// (the doc's fixed-TA authority test: TA shifts the send-CALL time, not the
|
||||
// measured arrival). Instead air the uplink from the BEACON engine —
|
||||
// hardware-timed at the UE's TBTT — and steer that TBTT with
|
||||
// AdjustBeaconTimingFine per the master's TA. The uplink is a tdma Uplink frame
|
||||
// (not a beacon FC), which StartBeacon stores in the rsvd page and the engine
|
||||
// airs verbatim at each TBTT, so the master's existing Uplink measurement path
|
||||
// sees it unchanged. The UE owns its slot too, so drop EDCCA (like the master)
|
||||
// for a crisp TBTT departure. This is the closed-loop LTE timing advance.
|
||||
if (c.hwbeacon) {
|
||||
dev->SetCcaMode(true);
|
||||
int interval_tu = c.interval_ms * 1000 / 1024;
|
||||
if (interval_tu < 1) interval_tu = 1;
|
||||
auto up = tdma::build_frame(rt, static_cast<tdma::Class>(timesync::kClassUplink),
|
||||
0, 0, 0);
|
||||
bool ok = dev->StartBeacon(up.data(), up.size(), interval_tu);
|
||||
fprintf(stderr, "timesync ue(HW-beacon uplink): StartBeacon(%d TU) -> %s; "
|
||||
"steering via AdjustBeaconTimingFine\n",
|
||||
interval_tu, ok ? "OK" : "FAILED");
|
||||
if (!ok) { dev->StopRxLoop(); rx.join(); return; }
|
||||
double applied_ta = 0; // TA already actuated into the TBTT (cumulative)
|
||||
uint64_t steers = 0;
|
||||
auto nstat = std::chrono::steady_clock::now() + std::chrono::seconds(1);
|
||||
auto deadline2 = std::chrono::steady_clock::now() + std::chrono::seconds(c.secs);
|
||||
while (!g_devourer_should_stop) {
|
||||
// Apply the TA increment: more TA => UE transmits earlier => advance (<0 µs).
|
||||
double ta = g_ue_ta_us.load(std::memory_order_relaxed);
|
||||
double delta = ta - applied_ta;
|
||||
if (std::fabs(delta) >= 3.0) {
|
||||
dev->AdjustBeaconTimingFine(-(int32_t)std::llround(delta));
|
||||
applied_ta = ta; // integrating loop: master measures the real arrival
|
||||
++steers; g_ue_tx.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
if (std::chrono::steady_clock::now() >= nstat) {
|
||||
nstat += std::chrono::seconds(1);
|
||||
char buf[192];
|
||||
std::snprintf(buf, sizeof(buf),
|
||||
"{\"ev\":\"timesync.ue\",\"beacons\":%llu,\"steers\":%llu,"
|
||||
"\"ta_us\":%.1f}\n",
|
||||
(unsigned long long)g_ue_beacons.load(),
|
||||
(unsigned long long)steers, ta);
|
||||
emit(buf);
|
||||
}
|
||||
if (c.secs && std::chrono::steady_clock::now() >= deadline2) break;
|
||||
sleep_ms(50); // rate-limit fine steers (each toggles EN_BCN_FUNCTION)
|
||||
}
|
||||
dev->StopRxLoop(); rx.join();
|
||||
fprintf(stderr,
|
||||
"\n=== timesync ue(HW-beacon) summary ===\n"
|
||||
" beacons heard : %llu\n"
|
||||
" fine steers : %llu\n"
|
||||
" final TA : %.2f us\n",
|
||||
(unsigned long long)g_ue_beacons.load(), (unsigned long long)steers,
|
||||
g_ue_ta_us.load());
|
||||
return;
|
||||
}
|
||||
|
||||
fprintf(stderr, "timesync ue: ch%d, uplink one frame per beacon (TA-corrected)\n",
|
||||
c.channel);
|
||||
|
||||
uint32_t done = 0; // last beacon seq we've already answered with an uplink
|
||||
auto next_stat = std::chrono::steady_clock::now() + std::chrono::seconds(1);
|
||||
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(c.secs);
|
||||
while (!g_devourer_should_stop) {
|
||||
if (std::chrono::steady_clock::now() >= next_stat) {
|
||||
next_stat += std::chrono::seconds(1);
|
||||
char buf[160];
|
||||
std::snprintf(buf, sizeof(buf),
|
||||
"{\"ev\":\"timesync.ue\",\"beacons\":%llu,\"tx\":%llu,\"ta_us\":%.1f}\n",
|
||||
(unsigned long long)g_ue_beacons.load(),
|
||||
(unsigned long long)g_ue_tx.load(), g_ue_ta_us.load());
|
||||
emit(buf);
|
||||
}
|
||||
if (!g_ue_have.load(std::memory_order_relaxed)) { sleep_ms(5); continue; }
|
||||
uint32_t s = g_ue_beacon_seq.load(std::memory_order_relaxed);
|
||||
if (s == done) { sleep_ms(1); continue; } // wait for the next beacon (a slot tick)
|
||||
done = s;
|
||||
// Aim the uplink to ARRIVE one slot after this beacon (the next boundary),
|
||||
// advanced by the master's TA. TA authority is direct: earlier send → earlier
|
||||
// arrival → smaller measured phase.
|
||||
int64_t send_at = g_ue_beacon_us.load(std::memory_order_relaxed) + slot_us -
|
||||
(int64_t)g_ue_ta_us.load(std::memory_order_relaxed);
|
||||
int64_t wait = send_at - steady_us();
|
||||
if (wait > 0 && wait < 2 * slot_us)
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(wait));
|
||||
else if (wait >= 2 * slot_us)
|
||||
continue; // absurd — skip this slot
|
||||
auto f = tdma::build_frame(rt, static_cast<tdma::Class>(timesync::kClassUplink),
|
||||
s, 0, 0);
|
||||
dev->send_packet(f.data(), f.size());
|
||||
g_ue_tx.fetch_add(1, std::memory_order_relaxed);
|
||||
if (c.secs && std::chrono::steady_clock::now() >= deadline) break;
|
||||
}
|
||||
dev->StopRxLoop(); rx.join();
|
||||
fprintf(stderr,
|
||||
"\n=== timesync ue summary ===\n"
|
||||
" beacons heard : %llu\n"
|
||||
" uplinks sent : %llu\n"
|
||||
" final TA : %.2f us\n",
|
||||
(unsigned long long)g_ue_beacons.load(),
|
||||
(unsigned long long)g_ue_tx.load(), g_ue_ta_us.load());
|
||||
}
|
||||
|
||||
int main() {
|
||||
auto logger = std::make_shared<Logger>();
|
||||
apply_logging_env(*logger);
|
||||
install_devourer_signal_handlers();
|
||||
|
||||
// Owns the teardown order (device -> interface -> handle -> context; see
|
||||
// DeviceSession.h): the role loops below only return once their RX thread is
|
||||
// joined, so the adapter is released with nothing in flight. On the PCIe
|
||||
// path there is no handle to adopt — the transport dies with the device.
|
||||
devourer::DeviceSession session{logger};
|
||||
|
||||
timesync::Config c = timesync::config_from_env();
|
||||
g_hwbeacon = c.hwbeacon; // slave reads the standard 802.11 beacon timestamp
|
||||
|
||||
WiFiDriver wifi(logger);
|
||||
std::unique_ptr<IRadio> owned_device;
|
||||
libusb_context* ctx = nullptr;
|
||||
/* DEVOURER_PCIE_BDF=0000:01:00.0 — drive a PCIe adapter (RTL8821CE) through
|
||||
* the vfio transport instead of libusb (DEVOURER_PCIE builds; mirrors the
|
||||
* RX/TX demos). A PCIe master collapses the software-downlink stamp→air
|
||||
* floor from ~93 µs (USB submit path) to ~12 µs (the MAC TX pipeline). */
|
||||
const char* pcie_bdf = std::getenv("DEVOURER_PCIE_BDF");
|
||||
#if defined(DEVOURER_HAVE_PCIE)
|
||||
if (pcie_bdf) {
|
||||
auto transport = devourer::PcieTransport::Open(pcie_bdf, logger);
|
||||
if (!transport) return 1;
|
||||
owned_device =
|
||||
wifi.CreateRadioPcie(std::move(transport), devourer_config_from_env());
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
if (pcie_bdf) {
|
||||
logger->error("DEVOURER_PCIE_BDF set but this build has DEVOURER_PCIE=OFF");
|
||||
return 1;
|
||||
}
|
||||
std::shared_ptr<devourer::UsbDeviceLock> lock;
|
||||
auto* handle = open_device(logger, &ctx, lock);
|
||||
session.adopt_context(ctx);
|
||||
if (!handle) return 1;
|
||||
session.adopt_handle(handle, devourer::find_wifi_interface(handle));
|
||||
session.adopt_lock(lock);
|
||||
owned_device =
|
||||
wifi.CreateRadio(handle, ctx, lock, devourer_config_from_env());
|
||||
}
|
||||
if (!owned_device) { logger->error("no driver for this chip"); return 1; }
|
||||
// The session owns the device from here: it is what guarantees the device
|
||||
// (and its in-flight TX) dies before libusb does.
|
||||
session.adopt_device(std::move(owned_device));
|
||||
IRadio* const dev = session.device();
|
||||
|
||||
if (c.role == timesync::Role::Ue) run_ue(dev, c);
|
||||
else if (c.role == timesync::Role::Master && c.uplink) run_master_ta(dev, c);
|
||||
else if (c.role == timesync::Role::Master) run_master(dev, c);
|
||||
else run_slave(dev, c);
|
||||
session.close();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// timesync — LTE-eNB-style over-the-air time distribution: one MASTER broadcasts
|
||||
// its hardware TSF periodically (a "sync beacon"), and any number of SLAVES lock
|
||||
// their notion of the master clock to it from the beacons alone — no GPS at the
|
||||
// slaves, only the master holds a reference. This is the 802.11 analog of an eNB
|
||||
// distributing frame timing to UEs: the master's TSF is the SFN, each slave a UE
|
||||
// slaving to it.
|
||||
//
|
||||
// A 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 are clean
|
||||
// MAC-latched microsecond clocks, so the fit residual is the true lock quality
|
||||
// (the ~sub-µs floor of the dual-RX TSF correlation), NOT the ~1 ms host-callback
|
||||
// jitter. Each slave PREDICTS the next beacon's master TSF from its fit; the
|
||||
// prediction error is how tightly it tracks the eNB. Two slaves predicting the
|
||||
// SAME beacon (matched by seq) agree to within their combined residual — the
|
||||
// inter-UE sync error, measured without either slave reading a host clock.
|
||||
//
|
||||
// App-level only (no WiFiDriver core changes); reuses the TD frame tag + SA from
|
||||
// the tdma example (examples/tdma/tdma.h) so one marker layout serves both.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#include "RadiotapBuilder.h" // devourer::TxMode / parse_tx_mode_str
|
||||
#include "tdma.h" // tdma::build_frame / parse_frame / kSa / Class
|
||||
|
||||
namespace timesync {
|
||||
|
||||
// 32→64-bit TSF reconstruction (the MAC latches only the low 32 bits per frame;
|
||||
// it wraps every ~71 min). One per clock source.
|
||||
struct Recon {
|
||||
int64_t hi = 0;
|
||||
uint32_t plo = 0;
|
||||
bool init = false;
|
||||
int64_t operator()(uint32_t lo) {
|
||||
if (init && lo < plo) hi += (1LL << 32);
|
||||
plo = lo;
|
||||
init = true;
|
||||
return hi + lo;
|
||||
}
|
||||
};
|
||||
|
||||
// Incremental ordinary-least-squares fit y = a·x + b, offset-normalized to the
|
||||
// first sample so the sums stay well within double precision over a long run.
|
||||
// Here x = the slave's local TSF (µs), y = the master's broadcast TSF (µs).
|
||||
struct LinFit {
|
||||
bool init = false;
|
||||
double x0 = 0, y0 = 0;
|
||||
long long n = 0;
|
||||
double sx = 0, sy = 0, sxx = 0, sxy = 0;
|
||||
|
||||
void add(double x, double y) {
|
||||
if (!init) { x0 = x; y0 = y; init = true; }
|
||||
double xi = x - x0, yi = y - y0;
|
||||
++n; sx += xi; sy += yi; sxx += xi * xi; sxy += xi * yi;
|
||||
}
|
||||
bool ready() const { return n >= 16; }
|
||||
double slope() const {
|
||||
double den = (double)n * sxx - sx * sx;
|
||||
return den == 0 ? 1.0 : ((double)n * sxy - sx * sy) / den;
|
||||
}
|
||||
double intercept() const { return (sy - slope() * sx) / (double)n; }
|
||||
// Predicted y at x.
|
||||
double at(double x) const { return y0 + slope() * (x - x0) + intercept(); }
|
||||
// Inverse: the x that yields a given y (for scheduling in the fitted domain).
|
||||
double inverse(double y) const {
|
||||
double a = slope();
|
||||
return x0 + (a == 0 ? 0 : (y - y0 - intercept()) / a);
|
||||
}
|
||||
// Fitted crystal offset in ppm (slope = dy/dx).
|
||||
double ppm() const { return (slope() - 1.0) * 1e6; }
|
||||
};
|
||||
|
||||
// Uplink extends the downlink beacon with two more TD-tag class codes (reusing
|
||||
// tdma::build_frame; parse_frame round-trips any class value, so tdma.h is
|
||||
// untouched). Uplink = UE→master frame the master phase-measures; Ta = the
|
||||
// master→UE timing-advance correction.
|
||||
static constexpr uint8_t kClassUplink = 3;
|
||||
static constexpr uint8_t kClassTa = 4;
|
||||
|
||||
// --- Config (env) -----------------------------------------------------------
|
||||
enum class Role { Master, Slave, Ue };
|
||||
|
||||
struct Config {
|
||||
Role role = Role::Slave;
|
||||
int interval_ms = 100; // master: sync-beacon period (LTE beacon ≈ 100 ms)
|
||||
int secs = 0; // 0 = run until signalled
|
||||
uint8_t channel = 36;
|
||||
devourer::TxMode rate; // master beacon rate (default 6M — must be heard)
|
||||
// Uplink timing-advance (full-duplex, DEVOURER_TSYNC_UPLINK=1):
|
||||
bool uplink = false; // master: measure UE uplinks + feed back TA. ue: TX uplinks
|
||||
bool hwbeacon = false; // master: HW-TBTT beacon (StartBeacon); slave: read 802.11 TS
|
||||
bool no_csma = true; // master: disable EDCCA by DEFAULT (master owns the channel,
|
||||
// beacon airs exactly at TBTT -> sub-µs); DEVOURER_TSYNC_CSMA=1 keeps CSMA
|
||||
int slot_ms = 20; // uplink slot grid on the master TSF (a TDMA slot)
|
||||
double ta_gain = 0.3; // master TA integrator gain (0..1; error fraction/step)
|
||||
};
|
||||
|
||||
inline int env_int(const char* n, int dflt) {
|
||||
const char* e = std::getenv(n);
|
||||
return (e && *e) ? std::atoi(e) : dflt;
|
||||
}
|
||||
|
||||
inline Config config_from_env() {
|
||||
Config c;
|
||||
if (const char* r = std::getenv("DEVOURER_TSYNC_ROLE")) {
|
||||
std::string s(r);
|
||||
c.role = (s == "master") ? Role::Master : (s == "ue") ? Role::Ue : Role::Slave;
|
||||
}
|
||||
c.interval_ms = env_int("DEVOURER_TSYNC_INTERVAL_MS", 100);
|
||||
c.secs = env_int("DEVOURER_TSYNC_SECS", 0);
|
||||
c.channel = static_cast<uint8_t>(env_int("DEVOURER_CHANNEL", 36));
|
||||
c.uplink = std::getenv("DEVOURER_TSYNC_UPLINK") != nullptr;
|
||||
c.hwbeacon = std::getenv("DEVOURER_TSYNC_HWBEACON") != nullptr;
|
||||
c.no_csma = std::getenv("DEVOURER_TSYNC_CSMA") == nullptr; // on by default; opt out to keep CSMA
|
||||
c.slot_ms = env_int("DEVOURER_TSYNC_SLOT_MS", 20);
|
||||
if (const char* g = std::getenv("DEVOURER_TSYNC_TA_GAIN")) c.ta_gain = std::atof(g);
|
||||
const char* rt = std::getenv("DEVOURER_TSYNC_RATE");
|
||||
c.rate = devourer::parse_tx_mode_str(rt && *rt ? rt : "6M");
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace timesync
|
||||
Reference in New Issue
Block a user