From efdd96b0c6feaeaac91f0a5b9733c9e29a5dbf5f Mon Sep 17 00:00:00 2001 From: snokvist Date: Wed, 16 Sep 2026 22:05:28 +0200 Subject: [PATCH 01/11] mt7612u: read the TSF coherently across a low-word wrap, and fail loudly mt7612u_read_tsf read DW0 then DW1 with the unchecked mt_rr. The halves are not latched: forcing a read across the 2^32 us low-word wrap (71.6 min after bring-up, which restarts the counter) tore it by +2^32 us on both units, and the reverse order by -2^32 us. A failed transfer joined 0xffffffff into a plausible wrong clock. - Mt7612uTsfRead.h: a pure high/low/high read that re-reads the low word when the high word moved - the Realtek REG_TSFTR discipline. On the same forced straddle it retried and landed within 0.75 ms of an independent read on both units. - mt7612u_read_tsf_chk (new C entry point): 0 or -1, *out untouched on failure. mt7612u_read_tsf rides it and returns 0 on failure. - Mt7612uRadio::ReadTsf throws std::ios_base::failure on a failed read, as the Realtek backends do, instead of returning a guess. - bringup: the beacon gate's liveness check and the caps gate's word-order measurement read checked, so a failed transfer cannot pass for a live timer or a word order. - mt7612u_tsf_read ctest cell: a scripted counter swept across the wrap at every access gap, every failure position, an all-ones low word as a value, and the pre-fix order as a negative control that must tear. Mutation-tested three ways. Co-Authored-By: Claude Opus 5 (1M context) --- CMakeLists.txt | 12 +++ docs/mt7612u.md | 40 +++++-- src/IRadio.h | 7 +- src/mt7612u/Mt7612uRadio.cpp | 12 ++- src/mt7612u/Mt7612uTsfRead.h | 63 +++++++++++ src/mt7612u/caps.cpp | 26 ++++- src/mt7612u/include/mt7612u/mt7612u.h | 17 ++- src/mt7612u/internal.h | 5 + src/mt7612u/tests/api_link.c | 1 + src/mt7612u/tools/bringup.cpp | 71 ++++++++----- tests/mt7612u_tsf_read_selftest.cpp | 146 ++++++++++++++++++++++++++ 11 files changed, 359 insertions(+), 41 deletions(-) create mode 100644 src/mt7612u/Mt7612uTsfRead.h create mode 100644 tests/mt7612u_tsf_read_selftest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3bbe9e37..132eb05f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -927,6 +927,18 @@ target_link_libraries(Mt7612uRxQueueSelftest PRIVATE devourer Threads::Threads) add_test(NAME mt7612u_rx_queue COMMAND Mt7612uRxQueueSelftest) +# Headless guard for the MT7612U coherent TSF read. The two register halves are +# not latched, so the read order is the whole correctness argument, and on a +# bench a wrong order only shows for a few hundred microseconds every 71.6 min. +# The cell sweeps a scripted counter across the low-word wrap and requires the +# pre-fix order to tear in the same sweep. Header-only; no libusb. +add_executable(Mt7612uTsfReadSelftest + tests/mt7612u_tsf_read_selftest.cpp +) +target_link_libraries(Mt7612uTsfReadSelftest PRIVATE devourer) + +add_test(NAME mt7612u_tsf_read COMMAND Mt7612uTsfReadSelftest) + # Headless guard for the TX quiesce seam (ITransport::quiesce_tx via # RtlAdapter): the explicit "stop TX and wait it out" call every device makes # before anything is released. UsbTransport's cancel/drain is validated on diff --git a/docs/mt7612u.md b/docs/mt7612u.md index d9dcea1d..271d8503 100644 --- a/docs/mt7612u.md +++ b/docs/mt7612u.md @@ -252,6 +252,32 @@ saturation. A-MPDU, not USB parallelism, is what lifted 34 → 44.55 Mbit/s. `AdapterCaps::tsf_write_ok = false` and does not override `WriteTsf`: the `IRadio` default returns false and issues no register write at all, rather than pretending one landed. +- **The two TSF halves are not latched.** Bring-up restarts the counter near 0 + (~0.35–0.40 s on both units), so the low word first wraps 71.6 min later, and + a read that straddles that instant tears. Forced on two units + (`40:a5:ef:50:27:a1`, `40:a5:ef:5a:32:f8`) by placing each half of a read on + either side of the predicted wrap, ±20–30 ms away, straddle confirmed by + reads either side. Each value is compared with a coherent read taken just + after the straddle, so a correct read lands slightly below it: + + | read | 8-1 | 5-1 | + |---|---|---| + | DW0 then DW1 (the library before this) | +4294907051 µs | +4294915049 µs | + | DW1 then DW0 | −4294972557 µs | −4294969171 µs | + | DW1, DW0, DW1, retry on a high-word change | retried; −131 µs | retried; −750 µs | + + A DW0 read does not freeze DW1 — the first row would read correctly if it + did. Both library readers (`mt7612u_read_tsf_chk`, `mt7612u_read_tsf`) now use + the third discipline (`Mt7612uTsfRead.h`), which costs a third control + transfer (a fourth across a wrap). How often the old read tore depends on + cadence: about once per wrap for a back-to-back poller, and roughly + (read time ÷ poll interval) per wrap otherwise. The 72 min around each forced + straddle (13.5 M and 1.6 M iterations of all three reads) produced no + anomaly; that is a negative control only, since the forced read occupied + the wrap instant itself. A failed transfer now fails the read: `_chk` returns + -1, `mt7612u_read_tsf` returns 0, and `Mt7612uRadio::ReadTsf` throws + `std::ios_base::failure` as the Realtek backends do, where it used to join + a `0xffffffff` failure word into a plausible wrong clock. - **Register-stream equivalence**: our EP0 write stream during bring-up was diffed against a `usbmon` capture of the kernel driver's own probe. 522 kernel writes vs 521 ours, 376 common addresses, one final-value mismatch @@ -330,7 +356,7 @@ and nothing was ever queued on the healthy path. ## Offline tests -`ctest` runs five MediaTek cells. The first three are C++ and need neither +`ctest` runs six MediaTek cells. The first four are C++ and need neither hardware nor the `DEVOURER_MT7612U` option — the code they cover is header-only. The last two are Python and need the `reference/mt76` submodule: @@ -339,6 +365,7 @@ header-only. The last two are Python and need the `reference/mt76` submodule: | `mt7612u_usb_ids` | that the 16 MediaTek pairs never claim a device one of the 91 Realtek ids owns. The gate runs *before* the Realtek `SYS_CFG2` read, so an id we wrongly own is refused with no second chance | | `mt7612u_mapping` | the descriptor translations — RSSI bias, per-chain signal, rate codes, TID offset, widths. Every one of these has been wrong once | | `mt7612u_rx_queue` | the RX hand-off queue's two load-bearing properties: a full ring drops the **newest** frame and counts it, and a popped slot outlives the queue lock. Broken, the first reorders frames or wedges the part and the second is a use-after-free — and both look like a healthy link until a packet processor falls behind | +| `mt7612u_tsf_read` | that the TSF read stays coherent when the low word wraps between any two of its register accesses: a scripted counter swept across the wrap one microsecond at a time, every failure position, and an all-ones low word as a value. The pre-fix DW0,DW1 order runs against the same sweep and must tear, or the cell cannot see the bug. Mutation-tested three ways — no retry, a retry that keeps the first high word, a retry that skips the low-word re-read — each fails | | `mt7612u_usb_ids_vs_mt76` | that the id table above really is the complete `mt76x2u_device_table` from the pinned reference, byte-checked. An id we *forgot* is invisible to `mt7612u_usb_ids` — the adapter just falls through to the Realtek path — and the first draft of that header had 11 of the 16, taken from the host's kernel tree | | `mt7612u_initvals_generated` | that the checked-in `initvals.h` still matches what `tools/extract_mt7612u_tables.py` produces from the pinned reference | @@ -361,7 +388,7 @@ nothing. Each fails the cell, and each names the property it broke. | test | what it holds | |---|---| -| `api_link` | takes the address of all 30 public entry points while including only the public header, so a declaration that loses its definition is a link error. Still compiled as C, which is what keeps the `extern "C"` guard honest now the library itself is C++ | +| `api_link` | takes the address of all 31 public entry points while including only the public header, so a declaration that loses its definition is a link error. Still compiled as C, which is what keeps the `extern "C"` guard honest now the library itself is C++ | | `frame_shape` | `mt_hdrlen_from_fc()` over management, all eight control subtypes and the five data shapes; the RX L2-pad fold on a synthetic QoS frame, with a negative control that redoes the old fixed-24 fold and asserts the QoS Control really is destroyed; the radiotap VHT bandwidth mapping over all eleven codes the part can express | | `field_macros` | `MT_CTZ` against `__builtin_ctz` over all 32 single-bit and all 528 contiguous masks, plus a `FIELD_PREP`/`FIELD_GET` round-trip, plus a static initialiser that fails to compile if the macro stops being constant-foldable | | `log_sink` | that `mt7612u_set_log_sink()` **diverts** rather than copies — stderr must stay silent while a sink is installed — that the sink gets the bare message with no prefix to double up, that every level letter arrives, and that NULL restores the built-in sink instead of silencing the library | @@ -521,13 +548,14 @@ Stated because the numbers above are uniformly favourable. firmware-running bit. - **The 48 ms fast retune is our implementation, not the floor.** The floor is unmeasured. -- **Three ctest cells run in CI, two more only on a bench, and the subtree's own +- **Four ctest cells run in CI, two more only on a bench, and the subtree's own four still run nowhere automatically.** With `DEVOURER_MT7612U=ON` the whole platform matrix (gcc, clang, MSVC, mingw, macOS) builds the subtree and the sanitizer job links it, so a portability or lifetime regression is caught. - `mt7612u_usb_ids`, `mt7612u_mapping` and `mt7612u_rx_queue` run on every - configuration. That last one is the first cell to cover backend *behaviour* - rather than a lookup — but it covers the queue and nothing else. Bring-up, + `mt7612u_usb_ids`, `mt7612u_mapping`, `mt7612u_rx_queue` and + `mt7612u_tsf_read` run on every configuration. The last two cover backend + *behaviour* rather than a lookup — the queue and the TSF read, and nothing + else. Bring-up, the teardown ordering, the 1 Hz tick and TX still have no automated coverage, and the two `reference/`-comparing cells SKIP in CI because the submodule is not fetched there. Every hardware claim in this document is hand-run except diff --git a/src/IRadio.h b/src/IRadio.h index e363b8da..addaf6db 100644 --- a/src/IRadio.h +++ b/src/IRadio.h @@ -354,9 +354,10 @@ class IRadio { * chip's crystal and is latched into every RX descriptor at receive * (rx_pkt_attrib::tsfl, the low 32 bits), so it is a precise, host-jitter-free * timing reference for multi-radio sync / TDOA / scheduled bursts. Returns 0 - * where unsupported (default). NB: a register read is a control transfer — - * calling it concurrently with a heavy RX bulk-IN load can race (catch the - * exception). */ + * where unsupported (default). A failed read throws std::ios_base::failure + * on the Realtek backends and the MT7612U alike; it never returns a guess. NB: + * a register read is a control transfer — calling it concurrently with a + * heavy RX bulk-IN load can race (catch the exception). */ virtual uint64_t ReadTsf() { return 0; } /* Write the 64-bit MAC TSF (REG_TSFTR). Sets the free-running microsecond clock diff --git a/src/mt7612u/Mt7612uRadio.cpp b/src/mt7612u/Mt7612uRadio.cpp index 9254ac2d..adc502ab 100644 --- a/src/mt7612u/Mt7612uRadio.cpp +++ b/src/mt7612u/Mt7612uRadio.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -813,7 +814,16 @@ bool Mt7612uRadio::GetPermanentMacAddress(uint8_t out[6]) { uint64_t Mt7612uRadio::ReadTsf() { std::lock_guard lock(_mu); - return _dev ? mt7612u_read_tsf(_dev) : 0; + if (!_dev) + return 0; + /* A failed read throws, as it does on every Realtek backend (a failed + * rtw_read throws std::ios_base::failure) and as IRadio::ReadTsf documents. + * Returning 0 would claim "unsupported"; returning the joined words would + * hand a timing consumer a plausible wrong clock. */ + uint64_t tsf; + if (mt7612u_read_tsf_chk(_dev, &tsf)) + throw std::ios_base::failure("mt7612u: TSF read failed"); + return tsf; } /* Busy airtime from the MAC channel timers — the MediaTek half of the neutral diff --git a/src/mt7612u/Mt7612uTsfRead.h b/src/mt7612u/Mt7612uTsfRead.h new file mode 100644 index 00000000..acdfccb2 --- /dev/null +++ b/src/mt7612u/Mt7612uTsfRead.h @@ -0,0 +1,63 @@ +#ifndef MT7612U_TSF_READ_H +#define MT7612U_TSF_READ_H + +#include + +#include "regs.h" + +namespace mt7612u { + +/* + * A coherent read of the 64-bit TSF from its two 32-bit halves, + * MT_TSF_TIMER_DW0 (low) and MT_TSF_TIMER_DW1 (high). + * + * WHY NOT TWO READS. Nothing latches the pair. Measured on two units by forcing + * a read across the 2^32 µs low-word wrap (71.6 min after bring-up, which + * resets the counter): DW0 read just before the wrap and DW1 just after tore + * the value by +2^32 µs, and the reverse order by -2^32 µs, on both. A DW0 read + * does not freeze DW1. docs/mt7612u.md has the numbers. + * + * THE DISCIPLINE. High, low, high again; if the high word moved, the low word + * wrapped somewhere in between, so read the low word once more and pair it with + * the second high word. The retry cannot itself tear: that would take a second + * wrap, 71.6 min later, inside two control transfers. On the same forced + * straddle this read retried and landed within 0.75 ms of an independent read. + * It is the Realtek generations' REG_TSFTR discipline too. + * + * FAILURE. `rd(addr, &val)` returns 0 on success and nonzero on a failed + * transfer; any failure fails the whole read, and *out is left untouched. + * 0xffffffff is a legitimate word here (the low word passes through it once a + * wrap), so no value can double as a failure sentinel - hence the separate + * return. + * + * Pure: no device, no libusb, no lock (the caller serializes), so a headless + * cell can drive it with a scripted register sequence + * (tests/mt7612u_tsf_read_selftest.cpp). + * + * Returns 0 and fills *out, or -1. `*retried`, when given, reports whether the + * wrap retry ran - a hardware soak needs to know the path was exercised, not + * merely that nothing went wrong. + */ +template +int tsf_read(Rd &&rd, uint64_t *out, bool *retried = nullptr) { + uint32_t hi, lo, hi2; + + if (retried) + *retried = false; + if (rd(MT_TSF_TIMER_DW1, &hi) || rd(MT_TSF_TIMER_DW0, &lo) || + rd(MT_TSF_TIMER_DW1, &hi2)) + return -1; + if (hi2 != hi) { + if (rd(MT_TSF_TIMER_DW0, &lo)) + return -1; + hi = hi2; + if (retried) + *retried = true; + } + *out = (static_cast(hi) << 32) | lo; + return 0; +} + +} // namespace mt7612u + +#endif /* MT7612U_TSF_READ_H */ diff --git a/src/mt7612u/caps.cpp b/src/mt7612u/caps.cpp index 65a006e0..2213726d 100644 --- a/src/mt7612u/caps.cpp +++ b/src/mt7612u/caps.cpp @@ -2,6 +2,7 @@ /* TSF access and the static capability descriptor. */ #include #include "internal.h" +#include "Mt7612uTsfRead.h" /* * DW0 is the LOW word. @@ -20,15 +21,34 @@ * (DW1 << 32) | DW0 gives 200159 us over a 200000 us sleep. The `caps` gate * prints both orders against a known sleep so the claim is re-checkable on * any sample. + * + * The two halves are not latched, so the read order matters as much as the + * join: see mt7612u::tsf_read (Mt7612uTsfRead.h) for the wrap retry and the + * measurement behind it. */ +int mt7612u_read_tsf_chk(struct mt7612u_dev *d, uint64_t *out) +{ + bool retried = false; + + if (!d || !out) + return -1; + if (mt7612u::tsf_read([d](uint32_t addr, uint32_t *v) { return mt_rr_chk(d, addr, v); }, + out, &retried)) + return -1; + if (retried) + d->tsf_retries++; + return 0; +} + uint64_t mt7612u_read_tsf(struct mt7612u_dev *d) { - uint32_t dw0 = mt_rr(d, MT_TSF_TIMER_DW0); - uint32_t dw1 = mt_rr(d, MT_TSF_TIMER_DW1); + uint64_t tsf; - return ((uint64_t)dw1 << 32) | dw0; + return mt7612u_read_tsf_chk(d, &tsf) ? 0 : tsf; } +unsigned mt_tsf_retries(struct mt7612u_dev *d) { return d->tsf_retries; } + void mt7612u_get_caps(const struct mt7612u_dev *d, struct mt7612u_caps *c) { memset(c, 0, sizeof *c); diff --git a/src/mt7612u/include/mt7612u/mt7612u.h b/src/mt7612u/include/mt7612u/mt7612u.h index 7c0eabd2..d0e1d43c 100644 --- a/src/mt7612u/include/mt7612u/mt7612u.h +++ b/src/mt7612u/include/mt7612u/mt7612u.h @@ -451,8 +451,21 @@ int mt7612u_ch_time(struct mt7612u_dev *dev, uint32_t *busy, uint32_t *idle, int mt7612u_phy_tick(struct mt7612u_dev *dev); -/* TSF, the hardware microsecond clock. Read only: there is no load path - * (measured, docs/mt7612u.md). */ +/* + * TSF, the hardware microsecond clock. Read only: there is no load path + * (measured, docs/mt7612u.md). Bring-up restarts it near 0, so its low + * word first wraps 71.6 min later - and the two halves are not latched, so a + * plain two-register read tears there by 2^32 us. Both functions read high, + * low, high and retry across a wrap (three or four control transfers). + * + * mt7612u_read_tsf_chk: 0 and fills *out, or -1 on a failed transfer (or a + * NULL argument), leaving *out untouched. + * + * mt7612u_read_tsf: the same read with no error channel - 0 on failure. A + * running counter never reads 0 after bring-up, but 0 cannot say why; use + * _chk wherever a failure has to be told apart. + */ +int mt7612u_read_tsf_chk(struct mt7612u_dev *dev, uint64_t *out); uint64_t mt7612u_read_tsf(struct mt7612u_dev *dev); /* What this adapter can do, so a caller need not assume. */ diff --git a/src/mt7612u/internal.h b/src/mt7612u/internal.h index 02649301..1dd8740c 100644 --- a/src/mt7612u/internal.h +++ b/src/mt7612u/internal.h @@ -207,6 +207,7 @@ struct mt7612u_dev { struct mt7612u_cal cal; unsigned io_err; /* EP0 transfers that exhausted their retries */ + unsigned tsf_retries; /* coherent TSF reads that took the wrap retry */ int transfers_stranded; /* libusb still owns a cancelled ring */ uint16_t max_mpdu_rx; /* from MT_MAX_LEN_CFG at init, less the FCS */ uint64_t stats_last_us; /* previous mt7612u_link_stats() mark */ @@ -278,6 +279,10 @@ void mt_io_clear(struct mt7612u_dev *d); /* Restore a previously sampled accumulator; see the note in usb.c. */ void mt_io_restore(struct mt7612u_dev *d, unsigned v); unsigned mt_io_errors(struct mt7612u_dev *d); +/* How many mt7612u_read_tsf_chk() reads took the low-word-wrap retry. A soak + * reads this to show the retry path actually ran, not merely that nothing + * went wrong. */ +unsigned mt_tsf_retries(struct mt7612u_dev *d); #define mt_set(d, a, v) mt_rmw(d, a, v, v) #define mt_clear(d, a, v) mt_rmw(d, a, v, 0) /* Poll until (rr(addr) & mask) == val. Returns 1 on success, 0 on timeout. */ diff --git a/src/mt7612u/tests/api_link.c b/src/mt7612u/tests/api_link.c index d51029f1..87ff52cb 100644 --- a/src/mt7612u/tests/api_link.c +++ b/src/mt7612u/tests/api_link.c @@ -40,6 +40,7 @@ static void *const api[] = { (void *)mt7612u_get_stats, (void *)mt7612u_link_stats_start, (void *)mt7612u_link_stats, + (void *)mt7612u_read_tsf_chk, (void *)mt7612u_read_tsf, (void *)mt7612u_get_caps, (void *)mt7612u_asic_version, diff --git a/src/mt7612u/tools/bringup.cpp b/src/mt7612u/tools/bringup.cpp index 065afa0d..e75d66f4 100644 --- a/src/mt7612u/tools/bringup.cpp +++ b/src/mt7612u/tools/bringup.cpp @@ -634,24 +634,29 @@ static int gate_beacon(uint8_t chan, int secs) printf("witness: run rxdemo on the 8812AU and grep the BSSID; " "or `iw dev scan | grep MT7612U-AP`\n"); - /* Watch the TSF advance - proof the beacon timer is running. DW0 is the - * low word on this silicon (mt76's debug read has it backwards). */ + /* Watch the TSF advance - proof the beacon timer is running. The checked + * read matters here: an unchecked one returns all-ones on a failed + * transfer, which is "greater than the previous sample" and would count a + * dead transport as a live timer. A failed read breaks the chain instead. */ { - uint64_t prev = 0; + uint64_t prev = 0, tsf; + bool have = false; int good = 0; for (int s = 0; s < secs && !g_stop; s++) { - uint32_t lo = mt_rr(&dev, MT_TSF_TIMER_DW0); - uint32_t hi = mt_rr(&dev, MT_TSF_TIMER_DW1); - uint64_t tsf = ((uint64_t)hi << 32) | lo; - - if (s) - printf(" t=%ds TSF=%llu (+%llu us)\n", s, - (unsigned long long)tsf, - (unsigned long long)(tsf - prev)); - if (s && tsf > prev) - good++; - prev = tsf; + if (mt7612u_read_tsf_chk(&dev, &tsf)) { + printf(" t=%ds TSF read failed\n", s); + have = false; + } else { + if (have) + printf(" t=%ds TSF=%llu (+%llu us)\n", s, + (unsigned long long)tsf, + (unsigned long long)(tsf - prev)); + if (have && tsf > prev) + good++; + prev = tsf; + have = true; + } if (!wait_ms(1000)) break; } @@ -1813,16 +1818,24 @@ static int gate_caps(uint8_t chan) /* TSF: the register names suggest DW0 is the low word but mt76 reads * DW0 as the high one. Rather than trust either reading, sleep a known - * 200 ms and require the clock to have advanced by that much. */ + * 200 ms and require the clock to have advanced by that much. The raw + * words stay raw on purpose - this is the measurement of the word order - + * but they are checked: an all-ones failure would otherwise pose as a + * word-order answer. */ { - uint32_t a0 = mt_rr(&dev, MT_TSF_TIMER_DW0); - uint32_t a1 = mt_rr(&dev, MT_TSF_TIMER_DW1); - uint32_t b0, b1; + uint32_t a0 = 0, a1 = 0, b0 = 0, b1 = 0; int64_t d_hi0, d_lo0; + bool raw_ok; + raw_ok = !mt_rr_chk(&dev, MT_TSF_TIMER_DW0, &a0) && + !mt_rr_chk(&dev, MT_TSF_TIMER_DW1, &a1); mt_usleep(200000); - b0 = mt_rr(&dev, MT_TSF_TIMER_DW0); - b1 = mt_rr(&dev, MT_TSF_TIMER_DW1); + raw_ok = raw_ok && !mt_rr_chk(&dev, MT_TSF_TIMER_DW0, &b0) && + !mt_rr_chk(&dev, MT_TSF_TIMER_DW1, &b1); + if (!raw_ok) { + printf("\nTSF raw: read failed - no word-order verdict\n"); + bad++; + } d_hi0 = (int64_t)((((uint64_t)b0 << 32) | b1) - (((uint64_t)a0 << 32) | a1)); d_lo0 = (int64_t)((((uint64_t)b1 << 32) | b0) - (((uint64_t)a1 << 32) | a0)); @@ -1833,13 +1846,19 @@ static int gate_caps(uint8_t chan) printf(" over a 200000 us sleep -> DW%d is the low word\n", (d_lo0 > 150000 && d_lo0 < 400000) ? 0 : 1); - t1 = mt7612u_read_tsf(&dev); + int rc = mt7612u_read_tsf_chk(&dev, &t1); + mt_usleep(200000); - t2 = mt7612u_read_tsf(&dev); - delta = (int64_t)(t2 - t1); - printf(" mt7612u_read_tsf(): delta %lld us %s\n", (long long)delta, - (delta > 150000 && delta < 400000) ? "OK" : "*** WRONG ORDER ***"); - if (delta < 150000 || delta > 400000) bad++; + rc |= mt7612u_read_tsf_chk(&dev, &t2); + if (rc) { + printf(" mt7612u_read_tsf_chk(): read failed\n"); + bad++; + } else { + delta = (int64_t)(t2 - t1); + printf(" mt7612u_read_tsf_chk(): delta %lld us %s\n", (long long)delta, + (delta > 150000 && delta < 400000) ? "OK" : "*** WRONG ORDER ***"); + if (delta < 150000 || delta > 400000) bad++; + } } /* 40 MHz */ diff --git a/tests/mt7612u_tsf_read_selftest.cpp b/tests/mt7612u_tsf_read_selftest.cpp new file mode 100644 index 00000000..93385350 --- /dev/null +++ b/tests/mt7612u_tsf_read_selftest.cpp @@ -0,0 +1,146 @@ +/* Headless guard for the MT7612U coherent TSF read + * (src/mt7612u/Mt7612uTsfRead.h). + * + * The two TSF halves are not latched, so the read order is the whole + * correctness argument - and on a bench it only matters once every 71.6 min, + * for the few hundred microseconds around a low-word wrap. A broken order looks + * perfect for an hour. This cell scripts the register sequence instead: a + * simulated counter that advances a fixed step per register access, started + * just below the wrap at every phase, so each read-to-read gap gets the wrap + * in turn. + * + * Negative control: the order the library used before this cell existed (DW0 + * then DW1, no retry) runs against the same scripts and MUST tear, or the rig + * cannot see the bug it exists for. + * + * What this does NOT cover: that the hardware behaves like the model (no latch + * between the halves). That is a forced-straddle soak on two units, recorded + * in docs/mt7612u.md. */ +#include "mt7612u/Mt7612uTsfRead.h" + +#include +#include + +namespace { + +int fails; + +void expect(const char *what, bool ok) { + if (!ok) { + std::fprintf(stderr, "mt7612u_tsf_read: FAIL %s\n", what); + fails++; + } +} + +/* A counter that advances `step` µs on every register access, reporting the + * half the address names. `fail_at` makes that access (0-based) fail. */ +struct Sim { + uint64_t now; + uint64_t step; + int accesses = 0; + int fail_at = -1; + + int operator()(uint32_t addr, uint32_t *v) { + const int n = accesses++; + now += step; + if (n == fail_at) + return -1; + *v = addr == MT_TSF_TIMER_DW1 ? static_cast(now >> 32) + : static_cast(now); + return 0; + } +}; + +/* The pre-fix reader, kept only as the negative control. */ +template uint64_t old_read(Rd &&rd) { + uint32_t lo = 0, hi = 0; + rd(MT_TSF_TIMER_DW0, &lo); + rd(MT_TSF_TIMER_DW1, &hi); + return (static_cast(hi) << 32) | lo; +} + +/* A coherent value lies between the counter before the first access and after + * the last one. */ +bool coherent(uint64_t v, uint64_t before, uint64_t after) { + return v >= before && v <= after; +} + +} // namespace + +int main() { + const uint64_t step = 300; /* ~one control transfer */ + const uint64_t wrap = 1ull << 32; + int retries = 0, old_tears = 0; + + /* Sweep the start so the wrap falls before, inside and after every gap of + * the read, one microsecond at a time across five steps. */ + for (uint64_t off = 0; off <= 5 * step; off++) { + const uint64_t start = wrap - 2 * step + off - 150; + + Sim s{start, step}; + uint64_t v = 0; + bool retried = false; + expect("read succeeds", mt7612u::tsf_read(s, &v, &retried) == 0); + if (!coherent(v, start, s.now)) { + std::fprintf(stderr, " off=%llu value=%llu window=[%llu,%llu]\n", + (unsigned long long)off, (unsigned long long)v, + (unsigned long long)start, (unsigned long long)s.now); + expect("value is coherent across the wrap", false); + } + expect("3 accesses, or 4 with the retry", + s.accesses == (retried ? 4 : 3)); + retries += retried; + + Sim o{start, step}; + uint64_t ov = old_read(o); + old_tears += !coherent(ov, start, o.now); + } + expect("the sweep exercised the retry path", retries > 0); + expect("negative control: the old DW0,DW1 order tears in the same sweep", + old_tears > 0); + + /* Far from a wrap: no retry, exact join. */ + { + Sim s{0x12345678'9abcdef0ull, 0}; + uint64_t v = 0; + bool retried = true; + expect("plain read", mt7612u::tsf_read(s, &v, &retried) == 0); + expect("plain read joins high:low", v == 0x12345678'9abcdef0ull); + expect("plain read does not retry", !retried && s.accesses == 3); + } + + /* 0xffffffff is a word, not a failure: a low word of all-ones reads back. */ + { + Sim s{0x00000002'ffffffffull, 0}; + uint64_t v = 0; + expect("all-ones low word is a value", + mt7612u::tsf_read(s, &v) == 0 && v == 0x00000002'ffffffffull); + } + + /* A failure at any access fails the read and leaves *out untouched. Four + * positions, with the start placed so the retry (the fourth access) runs. */ + for (int at = 0; at < 4; at++) { + Sim s{wrap - 400, 300}; + s.fail_at = at; + uint64_t v = 0xdeadbeefull; + bool retried = false; + char what[64]; + std::snprintf(what, sizeof what, "failure at access %d fails the read", at); + expect(what, mt7612u::tsf_read(s, &v, &retried) == -1); + std::snprintf(what, sizeof what, "failure at access %d leaves *out", at); + expect(what, v == 0xdeadbeefull); + } + /* ...and the retry position really is reached in that script. */ + { + Sim s{wrap - 400, 300}; + uint64_t v = 0; + bool retried = false; + expect("the failure script reaches the retry", + mt7612u::tsf_read(s, &v, &retried) == 0 && retried && s.accesses == 4); + } + + if (fails == 0) + std::printf("mt7612u_tsf_read: PASS (%d retries, old order tore %d times)\n", + retries, old_tears); + return fails == 0 ? 0 : 1; +} From 7b045084244df13ec9d64ef4cd7bfa2eece251dd Mon Sep 17 00:00:00 2001 From: snokvist Date: Wed, 16 Sep 2026 22:12:58 +0200 Subject: [PATCH 02/11] mt7612u tsf read: fail the beacon gate on any failed read, catch in the demos Review round: - bringup beacon gate: a failed TSF read now fails the gate. Clearing the chain alone still let two good samples between failures report a live timer on a failing transport. - timesync, tdma, chanmig: ReadTsf throws on a failed read (the IRadio contract, and now on the MT7612U as on Realtek). The two markers skip the frame rather than stamp a wrong time; chanmig's informational stamp falls back to its existing 0. - CMake lists Mt7612uTsfRead.h for IDEs; the doc says the mutations were hand-run and that the cell holds the discipline, not the wiring. Co-Authored-By: Claude Opus 5 (1M context) --- CMakeLists.txt | 1 + docs/mt7612u.md | 7 ++++++- examples/chanmig/main.cpp | 9 ++++++++- examples/tdma/main.cpp | 15 +++++++++++---- examples/timesync/main.cpp | 18 +++++++++++++++--- src/mt7612u/tools/bringup.cpp | 8 +++++++- 6 files changed, 48 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 132eb05f..29edd044 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -392,6 +392,7 @@ if(DEVOURER_MT7612U) src/mt7612u/Mt7612uRadio.cpp src/mt7612u/Mt7612uRadio.h src/mt7612u/Mt7612uMapping.h src/mt7612u/Mt7612uRxQueue.h + src/mt7612u/Mt7612uTsfRead.h src/mt7612u/internal.h src/mt7612u/regs.h src/mt7612u/initvals.h diff --git a/docs/mt7612u.md b/docs/mt7612u.md index 271d8503..124cc89e 100644 --- a/docs/mt7612u.md +++ b/docs/mt7612u.md @@ -365,7 +365,7 @@ header-only. The last two are Python and need the `reference/mt76` submodule: | `mt7612u_usb_ids` | that the 16 MediaTek pairs never claim a device one of the 91 Realtek ids owns. The gate runs *before* the Realtek `SYS_CFG2` read, so an id we wrongly own is refused with no second chance | | `mt7612u_mapping` | the descriptor translations — RSSI bias, per-chain signal, rate codes, TID offset, widths. Every one of these has been wrong once | | `mt7612u_rx_queue` | the RX hand-off queue's two load-bearing properties: a full ring drops the **newest** frame and counts it, and a popped slot outlives the queue lock. Broken, the first reorders frames or wedges the part and the second is a use-after-free — and both look like a healthy link until a packet processor falls behind | -| `mt7612u_tsf_read` | that the TSF read stays coherent when the low word wraps between any two of its register accesses: a scripted counter swept across the wrap one microsecond at a time, every failure position, and an all-ones low word as a value. The pre-fix DW0,DW1 order runs against the same sweep and must tear, or the cell cannot see the bug. Mutation-tested three ways — no retry, a retry that keeps the first high word, a retry that skips the low-word re-read — each fails | +| `mt7612u_tsf_read` | that the TSF read stays coherent when the low word wraps between any two of its register accesses: a scripted counter swept across the wrap one microsecond at a time, every failure position, and an all-ones low word as a value. The pre-fix DW0,DW1 order runs against the same sweep and must tear, or the cell cannot see the bug. Mutation-tested by hand three ways — no retry, a retry that keeps the first high word, a retry that skips the low-word re-read — each fails | | `mt7612u_usb_ids_vs_mt76` | that the id table above really is the complete `mt76x2u_device_table` from the pinned reference, byte-checked. An id we *forgot* is invisible to `mt7612u_usb_ids` — the adapter just falls through to the Realtek path — and the first draft of that header had 11 of the 16, taken from the host's kernel tree | | `mt7612u_initvals_generated` | that the checked-in `initvals.h` still matches what `tools/extract_mt7612u_tables.py` produces from the pinned reference | @@ -374,6 +374,11 @@ The last two SKIP when `reference/mt76` is absent, which is the case in CI — are also registered only `if(Python3_Interpreter_FOUND)`, so on a host without Python they are *absent* rather than skipped, and `ctest` says nothing at all. +`mt7612u_tsf_read` has the same limit: it holds the read discipline, not the +wiring. Reverting `mt7612u_read_tsf_chk` to a plain two-register join, or +`Mt7612uRadio::ReadTsf` to the non-throwing wrapper, passes it; those are held +by the hardware soak recorded under *Other measured items*. + What still has no cell is the *wiring*: `Mt7612uRadio`'s calls into the queue — the reset before the ring is armed, the commit after the processor returns — are covered by nothing headless. Deleting the `pop_commit()` compiles clean and diff --git a/examples/chanmig/main.cpp b/examples/chanmig/main.cpp index 2c044734..7712ff4e 100644 --- a/examples/chanmig/main.cpp +++ b/examples/chanmig/main.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -323,7 +324,13 @@ static Drone *g_drone = nullptr; static uint64_t read_tsf() { std::lock_guard lk(g_dev_mu); - return g_dev ? g_dev->ReadTsf() : 0; + /* The stamp is informational; 0 already means "no TSF". A failed read + * throws (IRadio contract) and must not take the control plane down. */ + try { + return g_dev ? g_dev->ReadTsf() : 0; + } catch (const std::exception &) { + return 0; + } } static void drone_do(const std::vector &acts) { diff --git a/examples/tdma/main.cpp b/examples/tdma/main.cpp index 9973ef6f..b3673c09 100644 --- a/examples/tdma/main.cpp +++ b/examples/tdma/main.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -127,10 +128,16 @@ static void run_tx(IRadio* dev, const tdma::Config& c) { last_marker_burst = a.burst; // Stamp the marker with the TX's hardware TSF (works TX-side — no RX // flood starving the control read); the TSF-sync RX uses it for drift. - uint64_t tx_tsf = dev->ReadTsf(); - auto f = tdma::build_frame(rt_marker, tdma::Class::Marker, seq[0]++, - (uint32_t)a.burst, tx_tsf); - dev->send_packet(f.data(), f.size()); + // A failed read throws (IRadio contract): skip this burst's marker + // rather than hand the drift fit a wrong stamp. + try { + uint64_t tx_tsf = dev->ReadTsf(); + auto f = tdma::build_frame(rt_marker, tdma::Class::Marker, seq[0]++, + (uint32_t)a.burst, tx_tsf); + dev->send_packet(f.data(), f.size()); + } catch (const std::exception &e) { + fprintf(stderr, "tdma: TSF read failed (%s), marker skipped\n", e.what()); + } } tdma::Class cls = a.phase == tdma::Phase::NB ? tdma::Class::Critical : tdma::Class::Bulk; diff --git a/examples/timesync/main.cpp b/examples/timesync/main.cpp index 16bba5c8..eee08c72 100644 --- a/examples/timesync/main.cpp +++ b/examples/timesync/main.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -157,9 +158,20 @@ static void run_master(IRadio* dev, const timesync::Config& c) { 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()); + // A failed read throws (IRadio contract); skip this marker rather than + // stamp it with a time the slave would fit as real. + uint64_t tsf = 0; + bool stamped = true; + try { + tsf = dev->ReadTsf(); + } catch (const std::exception &e) { + stamped = false; + fprintf(stderr, "timesync master: TSF read failed (%s), marker skipped\n", e.what()); + } + if (stamped) { + 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); diff --git a/src/mt7612u/tools/bringup.cpp b/src/mt7612u/tools/bringup.cpp index e75d66f4..5df3e3fb 100644 --- a/src/mt7612u/tools/bringup.cpp +++ b/src/mt7612u/tools/bringup.cpp @@ -641,11 +641,12 @@ static int gate_beacon(uint8_t chan, int secs) { uint64_t prev = 0, tsf; bool have = false; - int good = 0; + int good = 0, failed = 0; for (int s = 0; s < secs && !g_stop; s++) { if (mt7612u_read_tsf_chk(&dev, &tsf)) { printf(" t=%ds TSF read failed\n", s); + failed++; have = false; } else { if (have) @@ -662,6 +663,11 @@ static int gate_beacon(uint8_t chan, int secs) } /* A running TSF is necessary, not sufficient - the witness is the * real gate - but a frozen TSF means no beacons are being sent. */ + if (failed) { + printf("GATE A: FAIL - %d TSF read(s) failed; no timer verdict on a failing transport\n", + failed); + goto out; + } if (good == 0) { printf("GATE A: FAIL - TSF did not advance; beacon timer is dead\n"); goto out; From e8bc617139823fa76498a949c84c2ddf747bf001 Mon Sep 17 00:00:00 2001 From: snokvist Date: Wed, 16 Sep 2026 23:29:53 +0200 Subject: [PATCH 03/11] mt7612u tsf read: record the hardware validation, drop the unused counter - docs: the shipped read against a host-clock model at a forced wrap in each gap (+29 / +149 us, pre-fix order off by 2^32 in the same session), 36 M continuous reads with 1.93 M checked around the wraps (worst 346 us), and a bus-level disconnect under a 100 Hz poll (every call threw, no value returned). Also what it does not show: the exported function's own retry never fired on hardware. - The tsf_retries counter and mt_tsf_retries accessor had no in-tree reader and the soak design could not make them informative; removed. Co-Authored-By: Claude Opus 5 (1M context) --- docs/mt7612u.md | 31 +++++++++++++++++++++++++++---- src/mt7612u/Mt7612uTsfRead.h | 4 ++-- src/mt7612u/caps.cpp | 12 ++---------- src/mt7612u/internal.h | 5 ----- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/docs/mt7612u.md b/docs/mt7612u.md index 124cc89e..004844d9 100644 --- a/docs/mt7612u.md +++ b/docs/mt7612u.md @@ -274,10 +274,33 @@ saturation. A-MPDU, not USB parallelism, is what lifted 34 → 44.55 Mbit/s. (read time ÷ poll interval) per wrap otherwise. The 72 min around each forced straddle (13.5 M and 1.6 M iterations of all three reads) produced no anomaly; that is a negative control only, since the forced read occupied - the wrap instant itself. A failed transfer now fails the read: `_chk` returns - -1, `mt7612u_read_tsf` returns 0, and `Mt7612uRadio::ReadTsf` throws - `std::ios_base::failure` as the Realtek backends do, where it used to join - a `0xffffffff` failure word into a plausible wrong clock. + the wrap instant itself. + + The shipped read was then checked the same way against a truth that is not + the algorithm: a least-squares host-clock model of the TSF fitted over the + preceding 60 s (a coherent read agrees with it to ~50 µs; a torn one is off + by 2^32 µs). The `Mt7612uTsfRead.h` template, driven through a reader that + sleeps between accesses, took the wrap in its first gap on one unit and its + second on the other; both retried and landed +29 µs and +149 µs from the + model. The pre-fix order interleaved in the same session missed by + 2^32 + 48 µs and 2^32 + 10 ms. Around those, `mt7612u_read_tsf_chk` itself + ran continuously — 32.1 M and 3.9 M reads, of which the 1.72 M and 210 k + within ±120 s of the wrap were checked against the model: worst 346 µs and + 344 µs, no backwards step, no failed read. **What that does not show:** the + exported function never took its retry on hardware, because the forced read + occupied the wrap instant; its retry path is held by the `mt7612u_tsf_read` + cell and by the forced template read, which is the same code. + + A failed transfer now fails the read: `_chk` returns -1, `mt7612u_read_tsf` + returns 0, and `Mt7612uRadio::ReadTsf` throws `std::ios_base::failure` as the + Realtek backends do, where it used to join a `0xffffffff` failure word into a + plausible wrong clock. Checked by disconnecting the adapter under a 100 Hz + `ReadTsf` poll: 795 plausible reads, then every call threw (200 of 200 over + 2 s), no value returned after the disconnect, clean teardown. The disconnect + was the whole bus de-authorised (`usbN/authorized`), not a cable pull. A trap + on the way there: de-authorising only the *device* (`8-1/authorized`) leaves + it enumerated with EP0 working — the TSF kept reading, correctly, for 40 s + while the MCU bulk endpoint failed — so it does not simulate an unplug. - **Register-stream equivalence**: our EP0 write stream during bring-up was diffed against a `usbmon` capture of the kernel driver's own probe. 522 kernel writes vs 521 ours, 376 common addresses, one final-value mismatch diff --git a/src/mt7612u/Mt7612uTsfRead.h b/src/mt7612u/Mt7612uTsfRead.h index acdfccb2..0f75227e 100644 --- a/src/mt7612u/Mt7612uTsfRead.h +++ b/src/mt7612u/Mt7612uTsfRead.h @@ -35,8 +35,8 @@ namespace mt7612u { * (tests/mt7612u_tsf_read_selftest.cpp). * * Returns 0 and fills *out, or -1. `*retried`, when given, reports whether the - * wrap retry ran - a hardware soak needs to know the path was exercised, not - * merely that nothing went wrong. + * wrap retry ran, so a test can show the path was exercised rather than merely + * that nothing went wrong. */ template int tsf_read(Rd &&rd, uint64_t *out, bool *retried = nullptr) { diff --git a/src/mt7612u/caps.cpp b/src/mt7612u/caps.cpp index 2213726d..4aefa33d 100644 --- a/src/mt7612u/caps.cpp +++ b/src/mt7612u/caps.cpp @@ -28,16 +28,10 @@ */ int mt7612u_read_tsf_chk(struct mt7612u_dev *d, uint64_t *out) { - bool retried = false; - if (!d || !out) return -1; - if (mt7612u::tsf_read([d](uint32_t addr, uint32_t *v) { return mt_rr_chk(d, addr, v); }, - out, &retried)) - return -1; - if (retried) - d->tsf_retries++; - return 0; + return mt7612u::tsf_read( + [d](uint32_t addr, uint32_t *v) { return mt_rr_chk(d, addr, v); }, out); } uint64_t mt7612u_read_tsf(struct mt7612u_dev *d) @@ -47,8 +41,6 @@ uint64_t mt7612u_read_tsf(struct mt7612u_dev *d) return mt7612u_read_tsf_chk(d, &tsf) ? 0 : tsf; } -unsigned mt_tsf_retries(struct mt7612u_dev *d) { return d->tsf_retries; } - void mt7612u_get_caps(const struct mt7612u_dev *d, struct mt7612u_caps *c) { memset(c, 0, sizeof *c); diff --git a/src/mt7612u/internal.h b/src/mt7612u/internal.h index 1dd8740c..02649301 100644 --- a/src/mt7612u/internal.h +++ b/src/mt7612u/internal.h @@ -207,7 +207,6 @@ struct mt7612u_dev { struct mt7612u_cal cal; unsigned io_err; /* EP0 transfers that exhausted their retries */ - unsigned tsf_retries; /* coherent TSF reads that took the wrap retry */ int transfers_stranded; /* libusb still owns a cancelled ring */ uint16_t max_mpdu_rx; /* from MT_MAX_LEN_CFG at init, less the FCS */ uint64_t stats_last_us; /* previous mt7612u_link_stats() mark */ @@ -279,10 +278,6 @@ void mt_io_clear(struct mt7612u_dev *d); /* Restore a previously sampled accumulator; see the note in usb.c. */ void mt_io_restore(struct mt7612u_dev *d, unsigned v); unsigned mt_io_errors(struct mt7612u_dev *d); -/* How many mt7612u_read_tsf_chk() reads took the low-word-wrap retry. A soak - * reads this to show the retry path actually ran, not merely that nothing - * went wrong. */ -unsigned mt_tsf_retries(struct mt7612u_dev *d); #define mt_set(d, a, v) mt_rmw(d, a, v, v) #define mt_clear(d, a, v) mt_rmw(d, a, v, 0) /* Poll until (rr(addr) & mask) == val. Returns 1 on success, 0 on timeout. */ From efaf9786e26c7ddc27df1b7b8a93f9c875de1869 Mon Sep 17 00:00:00 2001 From: snokvist Date: Thu, 17 Sep 2026 19:29:03 +0200 Subject: [PATCH 04/11] mt7612u bringup: the tsfwrite gate reads the TSF through the library #430 gave the tsfwrite gate a private checked, wrap-safe reader because the library's was neither. Now the library's is both, so the gate calls mt7612u_read_tsf_chk and there is one copy of the discipline instead of two. Re-run on hardware after the rebase: tsfwrite PASS on 8-1 (ch 6) and 5-1 (ch 149), caps PASS, beacon gate timer live. Co-Authored-By: Claude Opus 5 (1M context) --- src/mt7612u/tools/bringup.cpp | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/mt7612u/tools/bringup.cpp b/src/mt7612u/tools/bringup.cpp index 5df3e3fb..f4e2133d 100644 --- a/src/mt7612u/tools/bringup.cpp +++ b/src/mt7612u/tools/bringup.cpp @@ -2928,26 +2928,12 @@ static bool tsf_live(int64_t delta) return delta > kTsfLiveMinUs && delta < kTsfLiveMaxUs; } -/* A checked TSF read: DW1, DW0, DW1 again with one retry if the low word - * wrapped in between, every word through mt_rr_chk. mt7612u_read_tsf() goes - * through mt_rr, which reports a failed transfer as all-ones - two of those - * read as a stopped clock, and a flaky cable would be reported as a dead - * timer. False means a transfer failed and *out is not a TSF. */ +/* The library's checked, wrap-safe TSF read. A failed transfer must stay + * distinguishable from a stopped clock here, or a flaky cable is reported as + * a dead timer. False means a transfer failed and *out is not a TSF. */ static bool tsf_read_chk(uint64_t *out) { - uint32_t hi, lo, hi2; - - if (mt_rr_chk(&dev, MT_TSF_TIMER_DW1, &hi) || - mt_rr_chk(&dev, MT_TSF_TIMER_DW0, &lo) || - mt_rr_chk(&dev, MT_TSF_TIMER_DW1, &hi2)) - return false; - if (hi2 != hi) { - hi = hi2; - if (mt_rr_chk(&dev, MT_TSF_TIMER_DW0, &lo)) - return false; - } - *out = ((uint64_t)hi << 32) | lo; - return true; + return mt7612u_read_tsf_chk(&dev, out) == 0; } /* A fresh base for one arm, clear of a low-word wrap. Each arm judges a take From 3f7e89b00ec73953038dd43485fd92b2e5c80458 Mon Sep 17 00:00:00 2001 From: snokvist Date: Thu, 17 Sep 2026 19:53:10 +0200 Subject: [PATCH 05/11] mt7612u tsf read: an in-tree wrap gate, the C tsf_write cap, api_link complete - bringup tsfwrap [gap] [wrap_bits] [max_min]: the TSF read across the low-word wrap against a host-clock model, so the hardware evidence is reproducible from the tree rather than a scratch probe. Each read is judged over the host interval that bracketed its transfer (one transfer can take 10 ms on a busy USB 2.0 bus). PASS needs the forced read to take the retry in the chosen gap and land on the model, the interleaved DW0,DW1 control to tear by 2^32, and every continuous mt7612u_read_tsf_chk read near the wrap on the model. wrap_bits < 32 is a smoke mode that reports SMOKE, never PASS. - struct mt7612u_caps gains tsf_write (0 here), so the C ABI says what AdapterCaps::tsf_write_ok says (#434 item 5). - api_link: add mt7612u_ch_time and mt7612u_phy_tick, public and never checked; the header's 33 entry points all resolve. - caps gate: no word-order verdict printed from failed raw reads. - IRadio::ReadTsf: the throw-on-failure statement is scoped to USB and to the backends that implement it, with the PCIe hedge RtlTsf.h keeps. - Mt7612uTsfRead.h points at the docs for numbers and no longer claims to be the Realtek sequence (same idea; read_tsftr re-reads both words). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JP51Yp3WSbfiMDuDHJByHW --- src/IRadio.h | 11 +- src/mt7612u/Mt7612uTsfRead.h | 14 +- src/mt7612u/README.md | 1 + src/mt7612u/caps.cpp | 1 + src/mt7612u/include/mt7612u/mt7612u.h | 11 +- src/mt7612u/tests/api_link.c | 2 + src/mt7612u/tools/bringup.cpp | 326 +++++++++++++++++++++++++- 7 files changed, 342 insertions(+), 24 deletions(-) diff --git a/src/IRadio.h b/src/IRadio.h index addaf6db..40f5a736 100644 --- a/src/IRadio.h +++ b/src/IRadio.h @@ -354,10 +354,13 @@ class IRadio { * chip's crystal and is latched into every RX descriptor at receive * (rx_pkt_attrib::tsfl, the low 32 bits), so it is a precise, host-jitter-free * timing reference for multi-radio sync / TDOA / scheduled bursts. Returns 0 - * where unsupported (default). A failed read throws std::ios_base::failure - * on the Realtek backends and the MT7612U alike; it never returns a guess. NB: - * a register read is a control transfer — calling it concurrently with a - * heavy RX bulk-IN load can race (catch the exception). */ + * where unsupported (default, and the RTL8733B). Over USB a failed read + * throws std::ios_base::failure on every backend that implements it + * (Jaguar1/2/3, Kestrel, MT7612U). Over PCIe a register read has no + * completion to fail: a device that has left the bus typically reads + * all-ones (platform behaviour, not measured here), and over vfio the load can + * fault instead. NB: a register read is a control transfer — calling it + * concurrently with a heavy RX bulk-IN load can race (catch the exception). */ virtual uint64_t ReadTsf() { return 0; } /* Write the 64-bit MAC TSF (REG_TSFTR). Sets the free-running microsecond clock diff --git a/src/mt7612u/Mt7612uTsfRead.h b/src/mt7612u/Mt7612uTsfRead.h index 0f75227e..b3eb6825 100644 --- a/src/mt7612u/Mt7612uTsfRead.h +++ b/src/mt7612u/Mt7612uTsfRead.h @@ -11,18 +11,16 @@ namespace mt7612u { * A coherent read of the 64-bit TSF from its two 32-bit halves, * MT_TSF_TIMER_DW0 (low) and MT_TSF_TIMER_DW1 (high). * - * WHY NOT TWO READS. Nothing latches the pair. Measured on two units by forcing - * a read across the 2^32 µs low-word wrap (71.6 min after bring-up, which - * resets the counter): DW0 read just before the wrap and DW1 just after tore - * the value by +2^32 µs, and the reverse order by -2^32 µs, on both. A DW0 read - * does not freeze DW1. docs/mt7612u.md has the numbers. + * WHY NOT TWO READS. Nothing latches the pair: a DW0 read does not freeze DW1, + * so a read whose halves straddle the 2^32 µs low-word wrap tears by 2^32 µs. + * Bring-up restarts the counter, so that is 71.6 min in. Measured on hardware + * by the bringup `tsfwrap` gate; docs/mt7612u.md has the numbers. * * THE DISCIPLINE. High, low, high again; if the high word moved, the low word * wrapped somewhere in between, so read the low word once more and pair it with * the second high word. The retry cannot itself tear: that would take a second - * wrap, 71.6 min later, inside two control transfers. On the same forced - * straddle this read retried and landed within 0.75 ms of an independent read. - * It is the Realtek generations' REG_TSFTR discipline too. + * wrap, 71.6 min later, inside two control transfers. Same idea as the Realtek + * read_tsftr (src/RtlTsf.h), which re-reads both words instead. * * FAILURE. `rd(addr, &val)` returns 0 on success and nonzero on a failed * transfer; any failure fails the whole read, and *out is left untouched. diff --git a/src/mt7612u/README.md b/src/mt7612u/README.md index 201d0053..c6767f9d 100644 --- a/src/mt7612u/README.md +++ b/src/mt7612u/README.md @@ -128,6 +128,7 @@ pwr TX power vs the kernel's values ampdu aggregation A/B gateg per-frame rate control ack ACK responder (needs a stimulus) rtap send_packet / send_packets hop channel-switch cost tsfwrite no TSF load path (positive control + every write sequence) +tsfwrap the TSF read across the low-word wrap (~72 min; host-clock model + control) ``` `make` here builds it as `./bringup`, which is what the hardware notes use. diff --git a/src/mt7612u/caps.cpp b/src/mt7612u/caps.cpp index 4aefa33d..806a0cb5 100644 --- a/src/mt7612u/caps.cpp +++ b/src/mt7612u/caps.cpp @@ -62,6 +62,7 @@ void mt7612u_get_caps(const struct mt7612u_dev *d, struct mt7612u_caps *c) c->per_chain_rssi = 1; c->narrowband = 0; /* MT_RATE_BW has no 5/10 MHz encoding */ c->fast_retune = 0; /* measured 48 ms even with calibration skipped */ + c->tsf_write = 0; /* DW0/DW1 do not load: the bringup tsfwrite gate */ } /* diff --git a/src/mt7612u/include/mt7612u/mt7612u.h b/src/mt7612u/include/mt7612u/mt7612u.h index d0e1d43c..f31258e7 100644 --- a/src/mt7612u/include/mt7612u/mt7612u.h +++ b/src/mt7612u/include/mt7612u/mt7612u.h @@ -453,10 +453,11 @@ int mt7612u_phy_tick(struct mt7612u_dev *dev); /* * TSF, the hardware microsecond clock. Read only: there is no load path - * (measured, docs/mt7612u.md). Bring-up restarts it near 0, so its low - * word first wraps 71.6 min later - and the two halves are not latched, so a - * plain two-register read tears there by 2^32 us. Both functions read high, - * low, high and retry across a wrap (three or four control transfers). + * (measured, docs/mt7612u.md). Bring-up restarts it near 0 (measured on two + * units), so its low word first wraps 71.6 min later - and the two halves are + * not latched, so a plain two-register read tears there by 2^32 us. Both + * functions read high, low, high and retry across a wrap (three or four control + * transfers). * * mt7612u_read_tsf_chk: 0 and fills *out, or -1 on a failed transfer (or a * NULL argument), leaving *out untouched. @@ -500,6 +501,8 @@ struct mt7612u_caps { unsigned per_chain_rssi : 1; unsigned narrowband : 1; /* 5/10 MHz - not available on this part */ unsigned fast_retune : 1; /* sub-ms channel change - not on this part */ + unsigned tsf_write : 1; /* a TSF load path - none on this part (measured, + * docs/mt7612u.md); AdapterCaps::tsf_write_ok */ }; void mt7612u_get_caps(const struct mt7612u_dev *dev, struct mt7612u_caps *caps); diff --git a/src/mt7612u/tests/api_link.c b/src/mt7612u/tests/api_link.c index 87ff52cb..2f3d17e8 100644 --- a/src/mt7612u/tests/api_link.c +++ b/src/mt7612u/tests/api_link.c @@ -40,6 +40,8 @@ static void *const api[] = { (void *)mt7612u_get_stats, (void *)mt7612u_link_stats_start, (void *)mt7612u_link_stats, + (void *)mt7612u_ch_time, + (void *)mt7612u_phy_tick, (void *)mt7612u_read_tsf_chk, (void *)mt7612u_read_tsf, (void *)mt7612u_get_caps, diff --git a/src/mt7612u/tools/bringup.cpp b/src/mt7612u/tools/bringup.cpp index f4e2133d..45439b24 100644 --- a/src/mt7612u/tools/bringup.cpp +++ b/src/mt7612u/tools/bringup.cpp @@ -4,6 +4,7 @@ * stage is independently runnable on hardware. */ #include +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include "../internal.h" +#include "../Mt7612uTsfRead.h" static struct mt7612u_dev dev; @@ -1801,8 +1803,8 @@ static int gate_caps(uint8_t chan) printf(" 5 GHz %u-%u MHz, 2.4 GHz %u-%u MHz\n", c.band_5g_min_mhz, c.band_5g_max_mhz, c.band_2g_min_mhz, c.band_2g_max_mhz); - printf(" ampdu_tx=%u per_chain_rssi=%u narrowband=%u fast_retune=%u\n", - c.ampdu_tx, c.per_chain_rssi, c.narrowband, c.fast_retune); + printf(" ampdu_tx=%u per_chain_rssi=%u narrowband=%u fast_retune=%u tsf_write=%u\n", + c.ampdu_tx, c.per_chain_rssi, c.narrowband, c.fast_retune, c.tsf_write); printf(" max MPDU: tx %u rx %u (rx is MT_MAX_LEN_CFG 0x%03x on air,\n" " less the 4-byte FCS)\n", c.max_mpdu_tx, c.max_mpdu_rx, @@ -1846,11 +1848,13 @@ static int gate_caps(uint8_t chan) d_hi0 = (int64_t)((((uint64_t)b0 << 32) | b1) - (((uint64_t)a0 << 32) | a1)); d_lo0 = (int64_t)((((uint64_t)b1 << 32) | b0) - (((uint64_t)a1 << 32) | a0)); - printf("\nTSF raw: DW0 %08x -> %08x DW1 %08x -> %08x\n", a0, b0, a1, b1); - printf(" as (DW0<<32)|DW1 : delta %lld us\n", (long long)d_hi0); - printf(" as (DW1<<32)|DW0 : delta %lld us\n", (long long)d_lo0); - printf(" over a 200000 us sleep -> DW%d is the low word\n", - (d_lo0 > 150000 && d_lo0 < 400000) ? 0 : 1); + if (raw_ok) { + printf("\nTSF raw: DW0 %08x -> %08x DW1 %08x -> %08x\n", a0, b0, a1, b1); + printf(" as (DW0<<32)|DW1 : delta %lld us\n", (long long)d_hi0); + printf(" as (DW1<<32)|DW0 : delta %lld us\n", (long long)d_lo0); + printf(" over a 200000 us sleep -> DW%d is the low word\n", + (d_lo0 > 150000 && d_lo0 < 400000) ? 0 : 1); + } int rc = mt7612u_read_tsf_chk(&dev, &t1); @@ -3195,6 +3199,307 @@ static int gate_tsfwrite(uint8_t chan) return any ? 1 : 0; } +/* Gate TSF-WRAP: the TSF read across the 2^32 us low-word wrap, judged + * against a truth that is not the read under test. + * + * WHY A GATE. The two TSF halves are not latched, so a read is only wrong for + * the few hundred microseconds around a low-word wrap, and bring-up restarts + * the counter near 0, so the first wrap is 71.6 min in. The headless + * mt7612u_tsf_read cell holds the read discipline against a scripted counter; + * this holds it against the part. A PASS takes ~72 min. + * + * TRUTH. A least-squares host-clock model, tsf = t0 + a + b * (host - h0), + * fitted over one read per 100 ms for the preceding 60 s. A read latches at an + * unknown instant inside its control transfer, and one transfer can take 10 ms + * on a busy USB 2.0 bus, so a read is judged against the model over the host + * interval that bracketed it (+-kTsfWrapTolUs), never at one timestamp. A torn + * read misses by 2^32 us. + * + * THREE PARTS. + * 1. Continuous: mt7612u_read_tsf_chk in a tight loop for the whole run. Any + * failed read or backwards step fails the gate, and every read within + * kTsfWrapWindowUs of the wrap must sit within kTsfWrapTolUs of the model. + * 2. Forced: ~3 s before the predicted wrap, the mt7612u::tsf_read template + * the library compiles runs with a reader that sleeps to a schedule, so the + * wrap lands in the chosen gap of the read (gap 1: between the first high + * and the low read; gap 2: between the low and the second high). It must + * take the retry and land within kTsfWrapTolUs of the model. + * 3. Positive control, interleaved with part 2: a plain DW0-then-DW1 read with + * the wrap between its halves. It must miss the model by ~2^32 us, or the + * rig cannot see the tear it exists to catch - and a DW0 read that froze + * DW1 would make it read correctly. + * + * WHAT IT DOES NOT SHOW. Part 2 occupies the wrap instant, so the exported + * function in part 1 never itself takes the retry across it; the forced read + * is the same template, driven through a different reader. + * + * Smoke mode: wrap_bits < 32 treats the carry out of that bit of the low word + * as the "wrap" (16.7 s at 24). That checks the schedule and the model in + * seconds but cannot tear a read, so it reports SMOKE, never PASS. */ +static const int64_t kTsfWrapTolUs = 5000; +static const int64_t kTsfWrapWindowUs = 120000000; +static const int kTsfWrapModelPts = 600; + +static int64_t mono_us(void) +{ + struct timespec t; + + clock_gettime(CLOCK_MONOTONIC, &t); + return (int64_t)t.tv_sec * 1000000 + t.tv_nsec / 1000; +} + +static void sleep_until_us(int64_t at) +{ + struct timespec t; + + t.tv_sec = at / 1000000; + t.tv_nsec = (long)(at % 1000000) * 1000; + while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &t, NULL) != 0 && !g_stop) + ; +} + +/* A ring of (host, tsf) points and the line through them. */ +struct tsf_model { + double h[kTsfWrapModelPts], t[kTsfWrapModelPts]; + int n, head; + double h0, t0, a, b; + bool ok; +}; + +static void tsf_model_add(struct tsf_model *m, double h, double t) +{ + m->h[m->head] = h; + m->t[m->head] = t; + m->head = (m->head + 1) % kTsfWrapModelPts; + if (m->n < kTsfWrapModelPts) + m->n++; +} + +static void tsf_model_fit(struct tsf_model *m) +{ + const int first = m->n < kTsfWrapModelPts ? 0 : m->head; + double sx = 0, sy = 0, sxx = 0, sxy = 0, den; + + m->ok = false; + if (m->n < 50) + return; + m->h0 = m->h[first]; + m->t0 = m->t[first]; + for (int i = 0; i < m->n; i++) { + int k = (first + i) % kTsfWrapModelPts; + double x = m->h[k] - m->h0, y = m->t[k] - m->t0; + + sx += x; sy += y; sxx += x * x; sxy += x * y; + } + den = m->n * sxx - sx * sx; + if (den == 0) + return; + m->b = (m->n * sxy - sx * sy) / den; + m->a = (sy - m->b * sx) / m->n; + m->ok = true; +} + +static double tsf_model_at(const struct tsf_model *m, double h) +{ + return m->t0 + m->a + m->b * (h - m->h0); +} + +/* Whether a value read between host instants h_start and h_end is on the + * model: between the model at the start and at the end, give or take the + * tolerance. *err gets the miss against the interval's midpoint, for printing. */ +static bool tsf_model_holds(const struct tsf_model *m, double v, double h_start, + double h_end, double *err) +{ + *err = v - tsf_model_at(m, (h_start + h_end) / 2); + return v >= tsf_model_at(m, h_start) - kTsfWrapTolUs && + v <= tsf_model_at(m, h_end) + kTsfWrapTolUs; +} + +static int gate_tsfwrap(int gap, int wrap_bits, double max_min) +{ + static struct tsf_model model; + const uint64_t period = 1ull << wrap_bits, mask = period - 1; + const int64_t t_start = mono_us(); + int64_t deadline = t_start + (int64_t)(max_min * 60e6); + int64_t last_pt = 0, last_status = 0, wrap_host = 0; + uint64_t prev = 0, reads = 0, fails = 0, backwards = 0, checked = 0, off_model = 0; + double worst = 0; + bool have = false, forced = false, f_retried = false, f_held = false, c_held = false; + int f_rc = -1; + double f_err = 0, c_err = 0; + + if (gap != 1 && gap != 2) { + printf("GATE TSF-WRAP: FAIL - gap must be 1 or 2\n"); + return 1; + } + if (wrap_bits < 20 || wrap_bits > 32) { + printf("GATE TSF-WRAP: FAIL - wrap_bits must be 20..32\n"); + return 1; + } + if (mt_eeprom_init(&dev) || mt_init_hardware(&dev, NULL) || + mt_set_channel(&dev, 6, MT7612U_BW_20)) { + printf("GATE TSF-WRAP: FAIL - bring-up failed\n"); + return 1; + } + + while (!g_stop && mono_us() < deadline) { + uint64_t v; + const int64_t h0 = mono_us(); + + if (mt7612u_read_tsf_chk(&dev, &v)) { + fails++; + have = false; + continue; + } + const int64_t h1 = mono_us(); + const double hm = (h0 + h1) / 2.0; + + reads++; + if (have && (int64_t)(v - prev) < 0) { + backwards++; + printf(" backwards: %llu -> %llu\n", (unsigned long long)prev, + (unsigned long long)v); + } + prev = v; + have = true; + + const bool near = wrap_host + ? h1 < wrap_host + kTsfWrapWindowUs + : (v & mask) > mask - ((uint64_t)kTsfWrapWindowUs % period); + double e = 0; + bool holds = true; + + tsf_model_fit(&model); + if (model.ok) + holds = tsf_model_holds(&model, (double)v, h0, h1, &e); + if (model.ok && near) { + checked++; + if (fabs(e) > worst) + worst = fabs(e); + if (!holds && ++off_model <= 5) + printf(" off model: tsf=%llu err=%+.0f us (read took %lld us)\n", + (unsigned long long)v, e, (long long)(h1 - h0)); + } + /* Feed the model only fast reads it agrees with, once it exists. */ + if (h1 - last_pt > 100000 && h1 - h0 <= kTsfWrapTolUs && holds) { + last_pt = h1; + tsf_model_add(&model, hm, (double)v); + } + if (h1 - last_status > 60000000) { + last_status = h1; + printf(" t=%4.0fs tsf=%llu reads=%llu fails=%llu backwards=%llu checked=%llu off_model=%llu worst=%.0f us\n", + (h1 - t_start) / 1e6, (unsigned long long)v, + (unsigned long long)reads, (unsigned long long)fails, + (unsigned long long)backwards, (unsigned long long)checked, + (unsigned long long)off_model, worst); + fflush(stdout); + } + + if (forced || !model.ok || (v & mask) <= mask - (3000000ull % period)) + continue; + + /* Parts 2 and 3, once. */ + forced = true; + const int64_t w = (int64_t)(hm + (double)(period - (v & mask)) / model.b); + const int64_t sched_gap1[4] = { w - 20000, w + 20000, w + 25000, w + 30000 }; + const int64_t sched_gap2[4] = { w - 25000, w - 20000, w + 20000, w + 25000 }; + const int64_t *sched = gap == 1 ? sched_gap1 : sched_gap2; + const int first_post = gap == 1 ? 1 : 2; + int64_t lo_start = 0, lo_end = 0, c_start, c_end, at_ms[4] = { 0 }; + uint32_t c_lo = 0, c_hi = 0; + bool c_ok = true; + int k = 0; + uint64_t fv = 0; + + wrap_host = w; + sleep_until_us(w - 30000); + c_start = mono_us(); + c_ok = !mt_rr_chk(&dev, MT_TSF_TIMER_DW0, &c_lo); + c_end = mono_us(); + + auto rd = [&](uint32_t addr, uint32_t *val) { + int64_t start; + int r; + + if (k < 4) { + if (k == first_post) { + sleep_until_us(w + 15000); + c_ok = c_ok && !mt_rr_chk(&dev, MT_TSF_TIMER_DW1, &c_hi); + } + sleep_until_us(sched[k]); + } + start = mono_us(); + r = mt_rr_chk(&dev, addr, val); + if (k < 4) + at_ms[k] = start; + if (addr == MT_TSF_TIMER_DW0) { + lo_start = start; + lo_end = mono_us(); + } + k++; + return r; + }; + f_rc = mt7612u::tsf_read(rd, &fv, &f_retried); + if (!c_ok) + fails++; + /* A coherent read carries the instant its (last) low word latched. */ + f_held = tsf_model_holds(&model, (double)fv, (double)lo_start, (double)lo_end, &f_err); + /* The control's low word latched in its own transfer; a coherent value + * would sit there, a torn one 2^32 above. */ + c_held = tsf_model_holds(&model, (double)(((uint64_t)c_hi << 32) | c_lo), + (double)c_start, (double)c_end, &c_err); + printf(" forced read (gap %d, wrap_bits %d): rc=%d retried=%d err=%+.0f us accesses at", + gap, wrap_bits, f_rc, f_retried ? 1 : 0, f_err); + for (int i = 0; i < k && i < 4; i++) + printf(" %+.2f", (at_ms[i] - w) / 1e3); + printf(" ms\n control DW0,DW1 across the wrap: err=%+.0f us\n", c_err); + fflush(stdout); + have = false; /* the sleeps are a hole in part 1 */ + deadline = mono_us() + kTsfWrapWindowUs; + } + + printf(" reads=%llu fails=%llu backwards=%llu checked=%llu off_model=%llu worst=%.0f us\n", + (unsigned long long)reads, (unsigned long long)fails, + (unsigned long long)backwards, (unsigned long long)checked, + (unsigned long long)off_model, worst); + + if (fails || backwards || off_model) { + printf("\nGATE TSF-WRAP: FAIL - %llu failed read(s), %llu backwards step(s), %llu read(s) off the model\n", + (unsigned long long)fails, (unsigned long long)backwards, + (unsigned long long)off_model); + return 1; + } + if (!forced) { + printf("\nGATE TSF-WRAP: FAIL - no wrap reached before the deadline (%.0f min)%s\n", + max_min, g_stop ? ", interrupted" : ""); + return 1; + } + if (f_rc || !f_held) { + printf("\nGATE TSF-WRAP: FAIL - the forced read %s\n", + f_rc ? "failed" : "missed the model"); + return 1; + } + if (wrap_bits < 32) { + printf("\nGATE TSF-WRAP: SMOKE - schedule and model check out (control err %+.0f us); no wrap verdict below 32 bits\n", + c_err); + return c_held ? 0 : 1; + } + if (!f_retried || checked == 0) { + printf("\nGATE TSF-WRAP: FAIL - %s\n", + !f_retried ? "the forced read did not take the retry (wrap not in the gap)" + : "no continuous read was checked near the wrap"); + return 1; + } + if (fabs(c_err) < 2147483648.0) { + printf("\nGATE TSF-WRAP: FAIL - the control did not tear (err %+.0f us); the rig cannot see a tear\n", + c_err); + return 1; + } + printf("\nGATE TSF-WRAP: PASS - retried across the wrap in gap %d, %+.0f us off the model; control tore by %+.0f us\n", + gap, f_err, c_err); + return 0; +} + int main(int argc, char **argv) { const char *err = NULL, *cmd = argc > 1 ? argv[1] : "regs"; @@ -3256,6 +3561,10 @@ int main(int argc, char **argv) rc = gate_caps(argc > 2 ? (uint8_t)atoi(argv[2]) : 149); } else if (!strcmp(cmd, "tsfwrite")) { rc = gate_tsfwrite(argc > 2 ? (uint8_t)atoi(argv[2]) : 149); + } else if (!strcmp(cmd, "tsfwrap")) { + rc = gate_tsfwrap(argc > 2 ? atoi(argv[2]) : 1, + argc > 3 ? atoi(argv[3]) : 32, + argc > 4 ? atof(argv[4]) : 80.0); } else if (!strcmp(cmd, "rxbytes")) { rc = gate_rxbytes(argc > 2 ? (uint8_t)atoi(argv[2]) : 1, argc > 3 ? atoi(argv[3]) : 15); @@ -3330,9 +3639,10 @@ int main(int argc, char **argv) rc = gate_fw(argc > 2 ? argv[2] : NULL); } else { fprintf(stderr, "unknown subcommand '%s'\n", cmd); - fprintf(stderr, "usage: bringup [regs|fw|init|chan|tx|rx|hop|gateg|tsfwrite] [chan] [count] [phy 0=CCK 1=OFDM 2=HT 4=VHT] [mcs]\n"); + fprintf(stderr, "usage: bringup [regs|fw|init|chan|tx|rx|hop|gateg|tsfwrite|tsfwrap] [chan] [count] [phy 0=CCK 1=OFDM 2=HT 4=VHT] [mcs]\n"); fprintf(stderr, " bringup adopt (the mt_adopt path a libusb-owning consumer uses)\n"); fprintf(stderr, " bringup tsfwrite [chan] (confirm this part has no TSF load path)\n"); + fprintf(stderr, " bringup tsfwrap [gap 1|2] [wrap_bits] [max_min] (TSF read across the low-word wrap, ~72 min)\n"); fprintf(stderr, " bringup beacon [chan] [secs] (Stage A: static AP beacon on air)\n"); fprintf(stderr, " bringup ap [chan] [secs] (Stage B: beacon + RX, probe/auth/assoc)\n"); fprintf(stderr, " bringup [sweep|coding|vht] [chan] [count] [bw 0=20 1=40 2=80]\n"); From 6e7d5770ad565250cf75f2fc19b1c6ee322f0817 Mon Sep 17 00:00:00 2001 From: snokvist Date: Thu, 17 Sep 2026 20:03:05 +0200 Subject: [PATCH 06/11] mt7612u tsfwrap: stop on a gone adapter, and record the wrap evidence - The gate spun on failed reads when the adapter disappeared mid-run: an interrupted run reached 192 million failed reads before it was stopped. A hundred consecutive failures now end the run with "the adapter is gone". Its sleep also retries only on EINTR, never on another error. - docs/mt7612u.md states the TSF read as a current fact, with the per-unit wrap measurements in one place, what they do not show, and that the gate's own run is not yet recorded (the rows are the scratch probe the gate reproduces). - chanmig warns once when a TSF read fails rather than stamping 0 in silence. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JP51Yp3WSbfiMDuDHJByHW --- docs/mt7612u.md | 95 +++++++++++++++++------------------ examples/chanmig/main.cpp | 11 +++- src/mt7612u/tools/bringup.cpp | 20 +++++++- 3 files changed, 73 insertions(+), 53 deletions(-) diff --git a/docs/mt7612u.md b/docs/mt7612u.md index 004844d9..72fd0fac 100644 --- a/docs/mt7612u.md +++ b/docs/mt7612u.md @@ -252,55 +252,50 @@ saturation. A-MPDU, not USB parallelism, is what lifted 34 → 44.55 Mbit/s. `AdapterCaps::tsf_write_ok = false` and does not override `WriteTsf`: the `IRadio` default returns false and issues no register write at all, rather than pretending one landed. -- **The two TSF halves are not latched.** Bring-up restarts the counter near 0 - (~0.35–0.40 s on both units), so the low word first wraps 71.6 min later, and - a read that straddles that instant tears. Forced on two units - (`40:a5:ef:50:27:a1`, `40:a5:ef:5a:32:f8`) by placing each half of a read on - either side of the predicted wrap, ±20–30 ms away, straddle confirmed by - reads either side. Each value is compared with a coherent read taken just - after the straddle, so a correct read lands slightly below it: - - | read | 8-1 | 5-1 | +- **The two TSF halves are not latched.** A DW0 read does not freeze DW1, so a + read whose halves straddle a low-word wrap tears by 2^32 µs. Bring-up + restarts the counter near 0 (~0.35–0.40 s on both units), so the first wrap + is 71.6 min in. A plain DW0-then-DW1 read hits it about once per wrap when + polled back to back, and roughly (read time ÷ poll interval) per wrap + otherwise. `mt7612u_read_tsf_chk` and `mt7612u_read_tsf` read high, low, + high, and re-read the low word when the high word moved + (`Mt7612uTsfRead.h`) — a third control transfer per read, a fourth across a + wrap. + + `bringup tsfwrap [gap]` holds that against the part, judging every read + against a host-clock model fitted over the preceding minute rather than + against the read under test. Around the wrap it forces the library's read + template to take the wrap in the chosen gap, interleaves a plain DW0,DW1 read + across the same wrap as a positive control, and checks every continuous + `mt7612u_read_tsf_chk` read near the wrap. Measured once per unit, one wrap + each: + + | | unit A (`40:a5:ef:50:27:a1`), gap 1 | unit B (`40:a5:ef:5a:32:f8`), gap 2 | |---|---|---| - | DW0 then DW1 (the library before this) | +4294907051 µs | +4294915049 µs | - | DW1 then DW0 | −4294972557 µs | −4294969171 µs | - | DW1, DW0, DW1, retry on a high-word change | retried; −131 µs | retried; −750 µs | - - A DW0 read does not freeze DW1 — the first row would read correctly if it - did. Both library readers (`mt7612u_read_tsf_chk`, `mt7612u_read_tsf`) now use - the third discipline (`Mt7612uTsfRead.h`), which costs a third control - transfer (a fourth across a wrap). How often the old read tore depends on - cadence: about once per wrap for a back-to-back poller, and roughly - (read time ÷ poll interval) per wrap otherwise. The 72 min around each forced - straddle (13.5 M and 1.6 M iterations of all three reads) produced no - anomaly; that is a negative control only, since the forced read occupied - the wrap instant itself. - - The shipped read was then checked the same way against a truth that is not - the algorithm: a least-squares host-clock model of the TSF fitted over the - preceding 60 s (a coherent read agrees with it to ~50 µs; a torn one is off - by 2^32 µs). The `Mt7612uTsfRead.h` template, driven through a reader that - sleeps between accesses, took the wrap in its first gap on one unit and its - second on the other; both retried and landed +29 µs and +149 µs from the - model. The pre-fix order interleaved in the same session missed by - 2^32 + 48 µs and 2^32 + 10 ms. Around those, `mt7612u_read_tsf_chk` itself - ran continuously — 32.1 M and 3.9 M reads, of which the 1.72 M and 210 k - within ±120 s of the wrap were checked against the model: worst 346 µs and - 344 µs, no backwards step, no failed read. **What that does not show:** the - exported function never took its retry on hardware, because the forced read - occupied the wrap instant; its retry path is held by the `mt7612u_tsf_read` - cell and by the forced template read, which is the same code. - - A failed transfer now fails the read: `_chk` returns -1, `mt7612u_read_tsf` - returns 0, and `Mt7612uRadio::ReadTsf` throws `std::ios_base::failure` as the - Realtek backends do, where it used to join a `0xffffffff` failure word into a - plausible wrong clock. Checked by disconnecting the adapter under a 100 Hz - `ReadTsf` poll: 795 plausible reads, then every call threw (200 of 200 over - 2 s), no value returned after the disconnect, clean teardown. The disconnect - was the whole bus de-authorised (`usbN/authorized`), not a cable pull. A trap - on the way there: de-authorising only the *device* (`8-1/authorized`) leaves - it enumerated with EP0 working — the TSF kept reading, correctly, for 40 s - while the MCU bulk endpoint failed — so it does not simulate an unplug. + | forced read | retried, +29 µs off the model | retried, +149 µs | + | DW0,DW1 control across the wrap | +2^32 + 48 µs | +2^32 + 10 ms | + | continuous reads near the wrap | 1.72 M checked of 32.1 M, worst 346 µs | 210 k of 3.9 M, worst 344 µs | + | failed or backwards reads | 0 | 0 | + + Those rows were taken with a scratch probe running the same three parts; the + `tsfwrap` gate is that probe made re-runnable, and its own run is not yet + recorded here. The gate's smoke mode (`wrap_bits` 24) is green on both units. + + **What that does not show.** The forced read occupies the wrap instant, so + the exported function never takes its own retry across a wrap in that run; + the retry path is the forced template read (the same code through another + reader) and the `mt7612u_tsf_read` cell. It is one wrap per unit, and the + gate's `wrap_bits` smoke mode cannot tear a read at all. + + A failed transfer fails the read: `_chk` returns -1, `mt7612u_read_tsf` + returns 0, and `Mt7612uRadio::ReadTsf` throws `std::ios_base::failure`, as + the Realtek USB backends do. Checked once by hand, not by a gate: under a + 100 Hz `ReadTsf` poll, de-authorising the adapter's whole bus + (`usbN/authorized` — a logical disconnect, not a cable pull) gave 795 + plausible reads and then 200 of 200 calls threw, with no value returned + after the disconnect and a clean teardown. De-authorising only the device + (its own `authorized`) is not a disconnect on this part: EP0 kept answering and + the TSF kept reading correctly for 40 s while the MCU bulk endpoint failed. - **Register-stream equivalence**: our EP0 write stream during bring-up was diffed against a `usbmon` capture of the kernel driver's own probe. 522 kernel writes vs 521 ours, 376 common addresses, one final-value mismatch @@ -400,7 +395,7 @@ Python they are *absent* rather than skipped, and `ctest` says nothing at all. `mt7612u_tsf_read` has the same limit: it holds the read discipline, not the wiring. Reverting `mt7612u_read_tsf_chk` to a plain two-register join, or `Mt7612uRadio::ReadTsf` to the non-throwing wrapper, passes it; those are held -by the hardware soak recorded under *Other measured items*. +by the `bringup tsfwrap` gate recorded under *Other measured items*. What still has no cell is the *wiring*: `Mt7612uRadio`'s calls into the queue — the reset before the ring is armed, the commit after the processor returns — @@ -416,7 +411,7 @@ nothing. Each fails the cell, and each names the property it broke. | test | what it holds | |---|---| -| `api_link` | takes the address of all 31 public entry points while including only the public header, so a declaration that loses its definition is a link error. Still compiled as C, which is what keeps the `extern "C"` guard honest now the library itself is C++ | +| `api_link` | takes the address of all 33 public entry points while including only the public header, so a declaration that loses its definition is a link error. Still compiled as C, which is what keeps the `extern "C"` guard honest now the library itself is C++ | | `frame_shape` | `mt_hdrlen_from_fc()` over management, all eight control subtypes and the five data shapes; the RX L2-pad fold on a synthetic QoS frame, with a negative control that redoes the old fixed-24 fold and asserts the QoS Control really is destroyed; the radiotap VHT bandwidth mapping over all eleven codes the part can express | | `field_macros` | `MT_CTZ` against `__builtin_ctz` over all 32 single-bit and all 528 contiguous masks, plus a `FIELD_PREP`/`FIELD_GET` round-trip, plus a static initialiser that fails to compile if the macro stops being constant-foldable | | `log_sink` | that `mt7612u_set_log_sink()` **diverts** rather than copies — stderr must stay silent while a sink is installed — that the sink gets the bare message with no prefix to double up, that every level letter arrives, and that NULL restores the built-in sink instead of silencing the library | diff --git a/examples/chanmig/main.cpp b/examples/chanmig/main.cpp index 7712ff4e..5e9419d2 100644 --- a/examples/chanmig/main.cpp +++ b/examples/chanmig/main.cpp @@ -325,10 +325,17 @@ static Drone *g_drone = nullptr; static uint64_t read_tsf() { std::lock_guard lk(g_dev_mu); /* The stamp is informational; 0 already means "no TSF". A failed read - * throws (IRadio contract) and must not take the control plane down. */ + * throws (IRadio contract) and must not take the control plane down, but it + * is said once rather than swallowed. */ try { return g_dev ? g_dev->ReadTsf() : 0; - } catch (const std::exception &) { + } catch (const std::exception &e) { + static bool warned = false; + if (!warned) { + warned = true; + fprintf(stderr, "chanmig: TSF read failed (%s); stamping 0 from here on failure\n", + e.what()); + } return 0; } } diff --git a/src/mt7612u/tools/bringup.cpp b/src/mt7612u/tools/bringup.cpp index 45439b24..66f4bb51 100644 --- a/src/mt7612u/tools/bringup.cpp +++ b/src/mt7612u/tools/bringup.cpp @@ -4,6 +4,7 @@ * stage is independently runnable on hardware. */ #include +#include #include #include #include @@ -3237,6 +3238,9 @@ static int gate_tsfwrite(uint8_t chan) * as the "wrap" (16.7 s at 24). That checks the schedule and the model in * seconds but cannot tear a read, so it reports SMOKE, never PASS. */ static const int64_t kTsfWrapTolUs = 5000; +/* A gone device fails every read at loop speed; stop rather than spin for the + * rest of the run (an interrupted run reached 192 million failed reads). */ +static const uint64_t kTsfWrapMaxConsecFails = 100; static const int64_t kTsfWrapWindowUs = 120000000; static const int kTsfWrapModelPts = 600; @@ -3254,7 +3258,9 @@ static void sleep_until_us(int64_t at) t.tv_sec = at / 1000000; t.tv_nsec = (long)(at % 1000000) * 1000; - while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &t, NULL) != 0 && !g_stop) + /* clock_nanosleep returns the error number itself; only an interrupt is + * worth resuming, and never past a stop request. */ + while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &t, NULL) == EINTR && !g_stop) ; } @@ -3323,6 +3329,8 @@ static int gate_tsfwrap(int gap, int wrap_bits, double max_min) int64_t deadline = t_start + (int64_t)(max_min * 60e6); int64_t last_pt = 0, last_status = 0, wrap_host = 0; uint64_t prev = 0, reads = 0, fails = 0, backwards = 0, checked = 0, off_model = 0; + uint64_t consec_fails = 0; + bool gone = false; double worst = 0; bool have = false, forced = false, f_retried = false, f_held = false, c_held = false; int f_rc = -1; @@ -3349,8 +3357,13 @@ static int gate_tsfwrap(int gap, int wrap_bits, double max_min) if (mt7612u_read_tsf_chk(&dev, &v)) { fails++; have = false; + if (++consec_fails >= kTsfWrapMaxConsecFails) { + gone = true; + break; + } continue; } + consec_fails = 0; const int64_t h1 = mono_us(); const double hm = (h0 + h1) / 2.0; @@ -3463,6 +3476,11 @@ static int gate_tsfwrap(int gap, int wrap_bits, double max_min) (unsigned long long)backwards, (unsigned long long)checked, (unsigned long long)off_model, worst); + if (gone) { + printf("\nGATE TSF-WRAP: FAIL - %llu reads in a row failed; the adapter is gone\n", + (unsigned long long)consec_fails); + return 1; + } if (fails || backwards || off_model) { printf("\nGATE TSF-WRAP: FAIL - %llu failed read(s), %llu backwards step(s), %llu read(s) off the model\n", (unsigned long long)fails, (unsigned long long)backwards, From be5f0724915221990e2e6f8027d6f382dbaf1e38 Mon Sep 17 00:00:00 2001 From: snokvist Date: Thu, 17 Sep 2026 22:26:42 +0200 Subject: [PATCH 07/11] mt7612u tsfwrap: verdicts that can only be reached honestly Review round on the gate itself: - A fit outside 0.9..1.1 x the host clock is a frozen or wedged counter, not a clock to predict a wrap from, and a predicted wrap past the deadline is refused: the gate can no longer sleep for hours past max_min on a degenerate counter. - The forced accesses sit 40 ms either side of the wrap, not 20: the gate's own rationale says one control transfer can take 10 ms, so the old margin could move a latch across the wrap and fail a correct read. A coherent forced read that took no retry is now INCONCLUSIVE (rc 2), not FAIL: the wrap missed the gap, which jitter can do. - The control check is sign-aware. Its low word is read before the wrap and its high word after, so a tear is one high-word step ABOVE the truth; magnitude alone would also accept a read that is systematically 2^32 low, which the model would have absorbed. - Model points come from reads up to 20 ms, decoupled from the 5 ms judging tolerance: on a busy bus the old threshold could starve the model and fail a run with a perfectly good read. - SIGINT reports INTERRUPTED (rc 2) instead of looking like a defect. Also: - mt7612u_tsf_api (ctest, needs DEVOURER_MT7612U): the C entry points' failure contract - _chk refuses NULL with -1 and does not write through, read_tsf answers 0, caps carry tsf_write. The NULL refusals are all it can reach without the part, and it says so. - tdma leaves a burst unmarked when the read throws, so the next pass retries it. - IRadio::ReadTsf keeps the PCIe statement to one clause and points at RtlTsf.h for the detail. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JP51Yp3WSbfiMDuDHJByHW --- CMakeLists.txt | 15 +++++++ docs/mt7612u.md | 16 ++++--- examples/tdma/main.cpp | 5 ++- src/IRadio.h | 9 ++-- src/mt7612u/tools/bringup.cpp | 67 +++++++++++++++++++++++------- tests/mt7612u_tsf_api_selftest.cpp | 61 +++++++++++++++++++++++++++ 6 files changed, 146 insertions(+), 27 deletions(-) create mode 100644 tests/mt7612u_tsf_api_selftest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 29edd044..348b79ca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -940,6 +940,21 @@ target_link_libraries(Mt7612uTsfReadSelftest PRIVATE devourer) add_test(NAME mt7612u_tsf_read COMMAND Mt7612uTsfReadSelftest) +# The C entry points' failure contract, which the cell above cannot reach: a +# failed read must be distinguishable from a value, because 0xffffffff is a +# legitimate register word on this part. Needs the subtree's symbols, so it is +# gated on DEVOURER_MT7612U (CI's MediaTek jobs turn it on). +if(DEVOURER_MT7612U) + add_executable(Mt7612uTsfApiSelftest + tests/mt7612u_tsf_api_selftest.cpp + ) + target_link_libraries(Mt7612uTsfApiSelftest PRIVATE devourer) + target_include_directories(Mt7612uTsfApiSelftest PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/mt7612u/include) + + add_test(NAME mt7612u_tsf_api COMMAND Mt7612uTsfApiSelftest) +endif() + # Headless guard for the TX quiesce seam (ITransport::quiesce_tx via # RtlAdapter): the explicit "stop TX and wait it out" call every device makes # before anything is released. UsbTransport's cancel/drain is validated on diff --git a/docs/mt7612u.md b/docs/mt7612u.md index 72fd0fac..d74b4397 100644 --- a/docs/mt7612u.md +++ b/docs/mt7612u.md @@ -374,9 +374,11 @@ and nothing was ever queued on the healthy path. ## Offline tests -`ctest` runs six MediaTek cells. The first four are C++ and need neither +`ctest` runs seven MediaTek cells. The first four are C++ and need neither hardware nor the `DEVOURER_MT7612U` option — the code they cover is -header-only. The last two are Python and need the `reference/mt76` submodule: +header-only. The fifth is C++ and needs the option, because it calls the +subtree's own symbols. The last two are Python and need the `reference/mt76` +submodule: | cell | what it holds | |---|---| @@ -384,6 +386,7 @@ header-only. The last two are Python and need the `reference/mt76` submodule: | `mt7612u_mapping` | the descriptor translations — RSSI bias, per-chain signal, rate codes, TID offset, widths. Every one of these has been wrong once | | `mt7612u_rx_queue` | the RX hand-off queue's two load-bearing properties: a full ring drops the **newest** frame and counts it, and a popped slot outlives the queue lock. Broken, the first reorders frames or wedges the part and the second is a use-after-free — and both look like a healthy link until a packet processor falls behind | | `mt7612u_tsf_read` | that the TSF read stays coherent when the low word wraps between any two of its register accesses: a scripted counter swept across the wrap one microsecond at a time, every failure position, and an all-ones low word as a value. The pre-fix DW0,DW1 order runs against the same sweep and must tear, or the cell cannot see the bug. Mutation-tested by hand three ways — no retry, a retry that keeps the first high word, a retry that skips the low-word re-read — each fails | +| `mt7612u_tsf_api` | that a failed TSF read stays distinguishable from a value at the C entry points: `mt7612u_read_tsf_chk` refuses a NULL device or output with -1 and does not write through the pointer, `mt7612u_read_tsf` answers 0, and `struct mt7612u_caps` carries the `tsf_write` bit. `0xffffffff` is a legitimate register word here, so only the return code can carry a failure. Reaches the NULL refusals only; a failed transfer on a live device and the `Mt7612uRadio::ReadTsf` throw need the part | | `mt7612u_usb_ids_vs_mt76` | that the id table above really is the complete `mt76x2u_device_table` from the pinned reference, byte-checked. An id we *forgot* is invisible to `mt7612u_usb_ids` — the adapter just falls through to the Realtek path — and the first draft of that header had 11 of the 16, taken from the host's kernel tree | | `mt7612u_initvals_generated` | that the checked-in `initvals.h` still matches what `tools/extract_mt7612u_tables.py` produces from the pinned reference | @@ -571,14 +574,15 @@ Stated because the numbers above are uniformly favourable. firmware-running bit. - **The 48 ms fast retune is our implementation, not the floor.** The floor is unmeasured. -- **Four ctest cells run in CI, two more only on a bench, and the subtree's own +- **Five ctest cells run in CI, two more only on a bench, and the subtree's own four still run nowhere automatically.** With `DEVOURER_MT7612U=ON` the whole platform matrix (gcc, clang, MSVC, mingw, macOS) builds the subtree and the sanitizer job links it, so a portability or lifetime regression is caught. `mt7612u_usb_ids`, `mt7612u_mapping`, `mt7612u_rx_queue` and - `mt7612u_tsf_read` run on every configuration. The last two cover backend - *behaviour* rather than a lookup — the queue and the TSF read, and nothing - else. Bring-up, + `mt7612u_tsf_read` run on every configuration, and `mt7612u_tsf_api` + wherever the option is on, which the MediaTek CI jobs set. The TSF pair and + the queue cover backend *behaviour* rather than a lookup, and nothing else + does. Bring-up, the teardown ordering, the 1 Hz tick and TX still have no automated coverage, and the two `reference/`-comparing cells SKIP in CI because the submodule is not fetched there. Every hardware claim in this document is hand-run except diff --git a/examples/tdma/main.cpp b/examples/tdma/main.cpp index b3673c09..794f7a50 100644 --- a/examples/tdma/main.cpp +++ b/examples/tdma/main.cpp @@ -125,16 +125,17 @@ static void run_tx(IRadio* dev, const tdma::Config& c) { if (w != cur_w) { dev->FastSetBandwidth(w); cur_w = w; } if (a.phase == tdma::Phase::NB && a.burst != last_marker_burst) { - last_marker_burst = a.burst; // Stamp the marker with the TX's hardware TSF (works TX-side — no RX // flood starving the control read); the TSF-sync RX uses it for drift. // A failed read throws (IRadio contract): skip this burst's marker - // rather than hand the drift fit a wrong stamp. + // rather than hand the drift fit a wrong stamp, and leave the burst + // unmarked so the next pass through it tries again. try { uint64_t tx_tsf = dev->ReadTsf(); auto f = tdma::build_frame(rt_marker, tdma::Class::Marker, seq[0]++, (uint32_t)a.burst, tx_tsf); dev->send_packet(f.data(), f.size()); + last_marker_burst = a.burst; } catch (const std::exception &e) { fprintf(stderr, "tdma: TSF read failed (%s), marker skipped\n", e.what()); } diff --git a/src/IRadio.h b/src/IRadio.h index 40f5a736..95580de4 100644 --- a/src/IRadio.h +++ b/src/IRadio.h @@ -356,11 +356,10 @@ class IRadio { * timing reference for multi-radio sync / TDOA / scheduled bursts. Returns 0 * where unsupported (default, and the RTL8733B). Over USB a failed read * throws std::ios_base::failure on every backend that implements it - * (Jaguar1/2/3, Kestrel, MT7612U). Over PCIe a register read has no - * completion to fail: a device that has left the bus typically reads - * all-ones (platform behaviour, not measured here), and over vfio the load can - * fault instead. NB: a register read is a control transfer — calling it - * concurrently with a heavy RX bulk-IN load can race (catch the exception). */ + * (Jaguar1/2/3, Kestrel, MT7612U); a PCIe register read cannot report failure + * at all (src/RtlTsf.h). NB: a register read is a control transfer — calling + * it concurrently with a heavy RX bulk-IN load can race (catch the + * exception). */ virtual uint64_t ReadTsf() { return 0; } /* Write the 64-bit MAC TSF (REG_TSFTR). Sets the free-running microsecond clock diff --git a/src/mt7612u/tools/bringup.cpp b/src/mt7612u/tools/bringup.cpp index 66f4bb51..a67adfed 100644 --- a/src/mt7612u/tools/bringup.cpp +++ b/src/mt7612u/tools/bringup.cpp @@ -3241,6 +3241,19 @@ static const int64_t kTsfWrapTolUs = 5000; /* A gone device fails every read at loop speed; stop rather than spin for the * rest of the run (an interrupted run reached 192 million failed reads). */ static const uint64_t kTsfWrapMaxConsecFails = 100; +/* A read this slow still judges (it is judged over its own interval), but it is + * too coarse to fit the model from. Decoupled from the tolerance: on a busy + * USB 2.0 bus a 3-transfer read can take longer than the tolerance, and + * refusing to fit from those would leave the model empty. */ +static const int64_t kTsfWrapFeedMaxUs = 20000; +/* The counter runs at the host's rate to within a crystal's error. A fit + * outside this band is a frozen or wedged counter, not a clock to predict a + * wrap from. */ +static const double kTsfWrapRateMin = 0.9, kTsfWrapRateMax = 1.1; +/* How far either side of the wrap the forced read places its accesses. Wide + * enough that one slow (~10 ms) control transfer cannot move a latch across + * the wrap. */ +static const int64_t kTsfWrapMarginUs = 40000; static const int64_t kTsfWrapWindowUs = 120000000; static const int kTsfWrapModelPts = 600; @@ -3344,6 +3357,8 @@ static int gate_tsfwrap(int gap, int wrap_bits, double max_min) printf("GATE TSF-WRAP: FAIL - wrap_bits must be 20..32\n"); return 1; } + /* Register reads only: the MAC stays as mt_init_hardware left it (stopped), + * so there is nothing for an early return to unwind. */ if (mt_eeprom_init(&dev) || mt_init_hardware(&dev, NULL) || mt_set_channel(&dev, 6, MT7612U_BW_20)) { printf("GATE TSF-WRAP: FAIL - bring-up failed\n"); @@ -3394,7 +3409,7 @@ static int gate_tsfwrap(int gap, int wrap_bits, double max_min) (unsigned long long)v, e, (long long)(h1 - h0)); } /* Feed the model only fast reads it agrees with, once it exists. */ - if (h1 - last_pt > 100000 && h1 - h0 <= kTsfWrapTolUs && holds) { + if (h1 - last_pt > 100000 && h1 - h0 <= kTsfWrapFeedMaxUs && holds) { last_pt = h1; tsf_model_add(&model, hm, (double)v); } @@ -3412,10 +3427,16 @@ static int gate_tsfwrap(int gap, int wrap_bits, double max_min) continue; /* Parts 2 and 3, once. */ + if (model.b < kTsfWrapRateMin || model.b > kTsfWrapRateMax) { + printf("\nGATE TSF-WRAP: FAIL - the counter runs at %.6f x the host clock; not a clock to time a wrap from\n", + model.b); + return 1; + } forced = true; const int64_t w = (int64_t)(hm + (double)(period - (v & mask)) / model.b); - const int64_t sched_gap1[4] = { w - 20000, w + 20000, w + 25000, w + 30000 }; - const int64_t sched_gap2[4] = { w - 25000, w - 20000, w + 20000, w + 25000 }; + const int64_t m1 = kTsfWrapMarginUs, m2 = m1 + 5000, m3 = m1 + 10000; + const int64_t sched_gap1[4] = { w - m1, w + m1, w + m2, w + m3 }; + const int64_t sched_gap2[4] = { w - m2, w - m1, w + m1, w + m2 }; const int64_t *sched = gap == 1 ? sched_gap1 : sched_gap2; const int first_post = gap == 1 ? 1 : 2; int64_t lo_start = 0, lo_end = 0, c_start, c_end, at_ms[4] = { 0 }; @@ -3425,7 +3446,12 @@ static int gate_tsfwrap(int gap, int wrap_bits, double max_min) uint64_t fv = 0; wrap_host = w; - sleep_until_us(w - 30000); + if (w - m3 > deadline) { + printf("\nGATE TSF-WRAP: FAIL - the predicted wrap (%.1f min away) is past the deadline\n", + (w - mono_us()) / 60e6); + return 1; + } + sleep_until_us(w - m3); c_start = mono_us(); c_ok = !mt_rr_chk(&dev, MT_TSF_TIMER_DW0, &c_lo); c_end = mono_us(); @@ -3436,7 +3462,7 @@ static int gate_tsfwrap(int gap, int wrap_bits, double max_min) if (k < 4) { if (k == first_post) { - sleep_until_us(w + 15000); + sleep_until_us(w + m1 / 2); c_ok = c_ok && !mt_rr_chk(&dev, MT_TSF_TIMER_DW1, &c_hi); } sleep_until_us(sched[k]); @@ -3487,9 +3513,13 @@ static int gate_tsfwrap(int gap, int wrap_bits, double max_min) (unsigned long long)off_model); return 1; } + if (g_stop) { + printf("\nGATE TSF-WRAP: INTERRUPTED - no verdict\n"); + return 2; + } if (!forced) { - printf("\nGATE TSF-WRAP: FAIL - no wrap reached before the deadline (%.0f min)%s\n", - max_min, g_stop ? ", interrupted" : ""); + printf("\nGATE TSF-WRAP: FAIL - no wrap reached before the deadline (%.0f min)\n", + max_min); return 1; } if (f_rc || !f_held) { @@ -3502,14 +3532,23 @@ static int gate_tsfwrap(int gap, int wrap_bits, double max_min) c_err); return c_held ? 0 : 1; } - if (!f_retried || checked == 0) { - printf("\nGATE TSF-WRAP: FAIL - %s\n", - !f_retried ? "the forced read did not take the retry (wrap not in the gap)" - : "no continuous read was checked near the wrap"); + if (checked == 0) { + printf("\nGATE TSF-WRAP: FAIL - no continuous read was checked near the wrap\n"); return 1; } - if (fabs(c_err) < 2147483648.0) { - printf("\nGATE TSF-WRAP: FAIL - the control did not tear (err %+.0f us); the rig cannot see a tear\n", + if (!f_retried) { + /* The read is coherent (checked above); the wrap just did not land in + * the gap, which transfer jitter can do. Not a defect - re-run. */ + printf("\nGATE TSF-WRAP: INCONCLUSIVE - the forced read holds but took no retry; the wrap missed gap %d. Re-run.\n", + gap); + return 2; + } + /* The control reads the low word before the wrap and the high word after, + * so a torn value is one whole high-word step ABOVE the truth. The sign + * matters: a read that is systematically 2^32 low would also fail on + * magnitude alone, and the model would have absorbed it. */ + if (c_err < 2147483648.0 || c_err > 6442450944.0) { + printf("\nGATE TSF-WRAP: FAIL - the control is %+.0f us off, not the +2^32 us a torn read gives; the rig cannot see a tear\n", c_err); return 1; } @@ -3660,7 +3699,7 @@ int main(int argc, char **argv) fprintf(stderr, "usage: bringup [regs|fw|init|chan|tx|rx|hop|gateg|tsfwrite|tsfwrap] [chan] [count] [phy 0=CCK 1=OFDM 2=HT 4=VHT] [mcs]\n"); fprintf(stderr, " bringup adopt (the mt_adopt path a libusb-owning consumer uses)\n"); fprintf(stderr, " bringup tsfwrite [chan] (confirm this part has no TSF load path)\n"); - fprintf(stderr, " bringup tsfwrap [gap 1|2] [wrap_bits] [max_min] (TSF read across the low-word wrap, ~72 min)\n"); + fprintf(stderr, " bringup tsfwrap [gap 1|2] [wrap_bits] [max_min] (TSF read across the low-word wrap, ~72 min; rc 2 = inconclusive)\n"); fprintf(stderr, " bringup beacon [chan] [secs] (Stage A: static AP beacon on air)\n"); fprintf(stderr, " bringup ap [chan] [secs] (Stage B: beacon + RX, probe/auth/assoc)\n"); fprintf(stderr, " bringup [sweep|coding|vht] [chan] [count] [bw 0=20 1=40 2=80]\n"); diff --git a/tests/mt7612u_tsf_api_selftest.cpp b/tests/mt7612u_tsf_api_selftest.cpp new file mode 100644 index 00000000..e1c45373 --- /dev/null +++ b/tests/mt7612u_tsf_api_selftest.cpp @@ -0,0 +1,61 @@ +/* Headless guard for the C TSF read's failure contract + * (src/mt7612u/caps.cpp, declared in mt7612u/mt7612u.h). + * + * The read discipline itself is mt7612u_tsf_read, against a scripted counter. + * What that cell cannot reach is the C entry points' contract, and the whole + * point of this pair is that a failure is distinguishable from a value: + * `0xffffffff` is a legitimate register word here, so only the return code can + * carry a failed transfer. A refusal that scribbled on *out, or one that + * returned 0 from _chk, would leave a caller unable to tell a failure from a + * TSF - the silent-garbage shape this whole path exists to remove. + * + * A NULL device is the only failure this can reach without hardware, so that + * is what it pins. What it does NOT cover: a failed transfer on a live device, + * and Mt7612uRadio::ReadTsf throwing. Those need the part - the + * `bringup tsfwrap` gate and the bus-disconnect run in docs/mt7612u.md. */ +#include "mt7612u/mt7612u.h" + +#include +#include + +namespace { + +int fails; + +void expect(const char *what, bool ok) { + if (!ok) { + std::fprintf(stderr, "mt7612u_tsf_api: FAIL %s\n", what); + fails++; + } +} + +} // namespace + +int main() { + /* _chk refuses a NULL device, and says so in the return code. */ + { + uint64_t out = 0xdeadbeefcafef00dull; + expect("_chk(NULL dev) returns -1", mt7612u_read_tsf_chk(nullptr, &out) == -1); + expect("_chk(NULL dev) leaves *out", out == 0xdeadbeefcafef00dull); + } + /* ...and a NULL out, rather than writing through it. */ + expect("_chk(NULL out) returns -1", mt7612u_read_tsf_chk(nullptr, nullptr) == -1); + + /* The no-error-channel form answers 0, never a value built from a failure. */ + expect("read_tsf(NULL) is 0", mt7612u_read_tsf(nullptr) == 0); + + /* The caps bit says this part has no TSF load path, so a C caller need not + * discover it from a missing symbol. */ + { + struct mt7612u_caps c; + + /* No device to fill it: the field must exist and be addressable, which is + * what the C ABI half of this is. */ + c.tsf_write = 0; + expect("caps carry tsf_write", c.tsf_write == 0); + } + + if (fails == 0) + std::printf("mt7612u_tsf_api: PASS\n"); + return fails == 0 ? 0 : 1; +} From 6b0fa445abd9a303e1eb78202da19ab836fb429a Mon Sep 17 00:00:00 2001 From: snokvist Date: Thu, 17 Sep 2026 23:41:39 +0200 Subject: [PATCH 08/11] docs/mt7612u: the wrap evidence is the gate's own run Both units, one wrap each, `bringup tsfwrap`: the forced read retried across the wrap and landed -3 us and -10 us off the host-clock model, the DW0,DW1 control tore by +2^32 in the same wrap on both, and every continuous mt7612u_read_tsf_chk read near the wrap held (1.70 M and 211 k checked, worst 348 us and 74 us, no failed read and no backwards step). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JP51Yp3WSbfiMDuDHJByHW --- docs/mt7612u.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/mt7612u.md b/docs/mt7612u.md index d74b4397..f81d32bc 100644 --- a/docs/mt7612u.md +++ b/docs/mt7612u.md @@ -270,16 +270,17 @@ saturation. A-MPDU, not USB parallelism, is what lifted 34 → 44.55 Mbit/s. `mt7612u_read_tsf_chk` read near the wrap. Measured once per unit, one wrap each: - | | unit A (`40:a5:ef:50:27:a1`), gap 1 | unit B (`40:a5:ef:5a:32:f8`), gap 2 | + | | `40:a5:ef:50:27:a1`, gap 1 | `40:a5:ef:5a:32:f8`, gap 2 | |---|---|---| - | forced read | retried, +29 µs off the model | retried, +149 µs | - | DW0,DW1 control across the wrap | +2^32 + 48 µs | +2^32 + 10 ms | - | continuous reads near the wrap | 1.72 M checked of 32.1 M, worst 346 µs | 210 k of 3.9 M, worst 344 µs | - | failed or backwards reads | 0 | 0 | - - Those rows were taken with a scratch probe running the same three parts; the - `tsfwrap` gate is that probe made re-runnable, and its own run is not yet - recorded here. The gate's smoke mode (`wrap_bits` 24) is green on both units. + | forced read | retried, −3 µs off the model | retried, −10 µs | + | DW0,DW1 control across the same wrap | +2^32 − 6 µs | +2^32 + 4955 µs | + | continuous reads checked near the wrap | 1.70 M of 31.7 M, worst 348 µs | 211 k of 3.9 M, worst 74 µs | + | failed reads, backwards steps, reads off the model | 0, 0, 0 | 0, 0, 0 | + + The second unit sits on a USB 2.0 bus where a control transfer can take + ~10 ms, which is where its control's extra 4955 µs comes from — the read is + judged over the interval that bracketed its transfer, so that is slack in the + measurement, not in the counter. **What that does not show.** The forced read occupies the wrap instant, so the exported function never takes its own retry across a wrap in that run; From abee0f412f2660be96cbb805b37def61a6b14c3c Mon Sep 17 00:00:00 2001 From: snokvist Date: Thu, 17 Sep 2026 23:51:55 +0200 Subject: [PATCH 09/11] mt7612u: one owner for the tsf_write fact, and gate verdicts in the right order - Mt7612uRadio::GetAdapterCaps takes tsf_write_ok from the C caps bit instead of restating it, so the two cannot drift. Confirmed on the part: `bringup caps` prints tsf_write=0 and the rxdemo adapter.caps event carries tsf_write 0 through the C++ path. - mt7612u_tsf_api drops its caps assertion: the cell had set the field itself, so it held nothing. Filling it needs a device, and the cell and the docs now say which parts of the contract need the part. - tsfwrap: the control's tear check runs before the retry verdict, so a run that misses the gap still reports whether the rig can see a tear at all; the control's post-wrap read moved out to the forced read's own margin, so a late wrap cannot leave the control coherent and fail a healthy read; the checked==0 guard says why it is kept; the header no longer says the forced read runs 3 s before the wrap (that is the trigger, not the schedule). - tdma's try wraps only ReadTsf, so a send failure is not reported as a read failure, and both timing demos say it once rather than at marker rate. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JP51Yp3WSbfiMDuDHJByHW --- docs/mt7612u.md | 2 +- examples/tdma/main.cpp | 17 ++++++++++++++--- examples/timesync/main.cpp | 8 +++++++- src/mt7612u/Mt7612uRadio.cpp | 7 +++++-- src/mt7612u/tools/bringup.cpp | 28 ++++++++++++++++++---------- tests/mt7612u_tsf_api_selftest.cpp | 20 ++++++++------------ 6 files changed, 53 insertions(+), 29 deletions(-) diff --git a/docs/mt7612u.md b/docs/mt7612u.md index f81d32bc..310f6b5d 100644 --- a/docs/mt7612u.md +++ b/docs/mt7612u.md @@ -387,7 +387,7 @@ submodule: | `mt7612u_mapping` | the descriptor translations — RSSI bias, per-chain signal, rate codes, TID offset, widths. Every one of these has been wrong once | | `mt7612u_rx_queue` | the RX hand-off queue's two load-bearing properties: a full ring drops the **newest** frame and counts it, and a popped slot outlives the queue lock. Broken, the first reorders frames or wedges the part and the second is a use-after-free — and both look like a healthy link until a packet processor falls behind | | `mt7612u_tsf_read` | that the TSF read stays coherent when the low word wraps between any two of its register accesses: a scripted counter swept across the wrap one microsecond at a time, every failure position, and an all-ones low word as a value. The pre-fix DW0,DW1 order runs against the same sweep and must tear, or the cell cannot see the bug. Mutation-tested by hand three ways — no retry, a retry that keeps the first high word, a retry that skips the low-word re-read — each fails | -| `mt7612u_tsf_api` | that a failed TSF read stays distinguishable from a value at the C entry points: `mt7612u_read_tsf_chk` refuses a NULL device or output with -1 and does not write through the pointer, `mt7612u_read_tsf` answers 0, and `struct mt7612u_caps` carries the `tsf_write` bit. `0xffffffff` is a legitimate register word here, so only the return code can carry a failure. Reaches the NULL refusals only; a failed transfer on a live device and the `Mt7612uRadio::ReadTsf` throw need the part | +| `mt7612u_tsf_api` | that a failed TSF read stays distinguishable from a value at the C entry points: `mt7612u_read_tsf_chk` refuses a NULL device or output with -1 and does not write through the pointer, and `mt7612u_read_tsf` answers 0. `0xffffffff` is a legitimate register word here, so only the return code can carry a failure. Reaches the NULL refusals only; a failed transfer on a live device, the `Mt7612uRadio::ReadTsf` throw and the `tsf_write` caps bit need the part (`bringup caps` prints it, and `Mt7612uRadio::GetAdapterCaps` takes `tsf_write_ok` from it rather than restating it) | | `mt7612u_usb_ids_vs_mt76` | that the id table above really is the complete `mt76x2u_device_table` from the pinned reference, byte-checked. An id we *forgot* is invisible to `mt7612u_usb_ids` — the adapter just falls through to the Realtek path — and the first draft of that header had 11 of the 16, taken from the host's kernel tree | | `mt7612u_initvals_generated` | that the checked-in `initvals.h` still matches what `tools/extract_mt7612u_tables.py` produces from the pinned reference | diff --git a/examples/tdma/main.cpp b/examples/tdma/main.cpp index 794f7a50..a72c8041 100644 --- a/examples/tdma/main.cpp +++ b/examples/tdma/main.cpp @@ -130,14 +130,25 @@ static void run_tx(IRadio* dev, const tdma::Config& c) { // A failed read throws (IRadio contract): skip this burst's marker // rather than hand the drift fit a wrong stamp, and leave the burst // unmarked so the next pass through it tries again. + uint64_t tx_tsf = 0; + bool stamped = true; try { - uint64_t tx_tsf = dev->ReadTsf(); + tx_tsf = dev->ReadTsf(); + } catch (const std::exception &e) { + static bool warned = false; + stamped = false; + if (!warned) { + warned = true; + fprintf(stderr, "tdma: TSF read failed (%s), marker skipped " + "(said once; markers keep being skipped while it fails)\n", + e.what()); + } + } + if (stamped) { auto f = tdma::build_frame(rt_marker, tdma::Class::Marker, seq[0]++, (uint32_t)a.burst, tx_tsf); dev->send_packet(f.data(), f.size()); last_marker_burst = a.burst; - } catch (const std::exception &e) { - fprintf(stderr, "tdma: TSF read failed (%s), marker skipped\n", e.what()); } } tdma::Class cls = diff --git a/examples/timesync/main.cpp b/examples/timesync/main.cpp index eee08c72..564280a9 100644 --- a/examples/timesync/main.cpp +++ b/examples/timesync/main.cpp @@ -165,8 +165,14 @@ static void run_master(IRadio* dev, const timesync::Config& c) { try { tsf = dev->ReadTsf(); } catch (const std::exception &e) { + static bool warned = false; stamped = false; - fprintf(stderr, "timesync master: TSF read failed (%s), marker skipped\n", e.what()); + if (!warned) { + warned = true; + fprintf(stderr, "timesync master: TSF read failed (%s), marker skipped " + "(said once; markers keep being skipped while it fails)\n", + e.what()); + } } if (stamped) { auto f = tdma::build_frame(rt, tdma::Class::Marker, seq++, 0, tsf); diff --git a/src/mt7612u/Mt7612uRadio.cpp b/src/mt7612u/Mt7612uRadio.cpp index adc502ab..2cdba3e5 100644 --- a/src/mt7612u/Mt7612uRadio.cpp +++ b/src/mt7612u/Mt7612uRadio.cpp @@ -1112,8 +1112,11 @@ devourer::AdapterCaps Mt7612uRadio::GetAdapterCaps() { * describes sat three hundred lines above. */ c.hw_beacon_txtsf = true; /* No TSF load path: every write sequence the bringup `tsfwrite` gate tries - * is ignored (docs/mt7612u.md), so WriteTsf stays on the IRadio default. */ - c.tsf_write_ok = false; + * is ignored (docs/mt7612u.md), so WriteTsf stays on the IRadio default. + * Taken from the C caps rather than restated, so the two cannot drift; the + * C library owns the fact (mt7612u_get_caps), and `hw` is zeroed when there + * is no device, which is the same answer. */ + c.tsf_write_ok = hw.tsf_write; /* Measured on air: 0 frames at the stimulus radio unarmed, 3500+ armed. */ c.ack_responder_ok = true; /* Unmeasured, so false rather than optimistic - nothing here drives the diff --git a/src/mt7612u/tools/bringup.cpp b/src/mt7612u/tools/bringup.cpp index a67adfed..2d1854d1 100644 --- a/src/mt7612u/tools/bringup.cpp +++ b/src/mt7612u/tools/bringup.cpp @@ -3220,7 +3220,7 @@ static int gate_tsfwrite(uint8_t chan) * 1. Continuous: mt7612u_read_tsf_chk in a tight loop for the whole run. Any * failed read or backwards step fails the gate, and every read within * kTsfWrapWindowUs of the wrap must sit within kTsfWrapTolUs of the model. - * 2. Forced: ~3 s before the predicted wrap, the mt7612u::tsf_read template + * 2. Forced: once the counter is within 3 s of the wrap, the mt7612u::tsf_read template * the library compiles runs with a reader that sleeps to a schedule, so the * wrap lands in the chosen gap of the read (gap 1: between the first high * and the low read; gap 2: between the low and the second high). It must @@ -3462,7 +3462,9 @@ static int gate_tsfwrap(int gap, int wrap_bits, double max_min) if (k < 4) { if (k == first_post) { - sleep_until_us(w + m1 / 2); + /* After the wrap, and before the forced read's own first + * post-wrap access, with room for a slow transfer. */ + sleep_until_us(w + m1 - 5000); c_ok = c_ok && !mt_rr_chk(&dev, MT_TSF_TIMER_DW1, &c_hi); } sleep_until_us(sched[k]); @@ -3532,26 +3534,32 @@ static int gate_tsfwrap(int gap, int wrap_bits, double max_min) c_err); return c_held ? 0 : 1; } + /* Defensive: the forced read only runs on a fitted model, and the model + * needs reads from inside the same 120 s window, so this cannot be 0 today. + * It is the one thing a PASS silently rests on, so it is checked. */ if (checked == 0) { printf("\nGATE TSF-WRAP: FAIL - no continuous read was checked near the wrap\n"); return 1; } - if (!f_retried) { - /* The read is coherent (checked above); the wrap just did not land in - * the gap, which transfer jitter can do. Not a defect - re-run. */ - printf("\nGATE TSF-WRAP: INCONCLUSIVE - the forced read holds but took no retry; the wrap missed gap %d. Re-run.\n", - gap); - return 2; - } /* The control reads the low word before the wrap and the high word after, * so a torn value is one whole high-word step ABOVE the truth. The sign * matters: a read that is systematically 2^32 low would also fail on - * magnitude alone, and the model would have absorbed it. */ + * magnitude alone, and the model would have absorbed it. Checked before the + * retry verdict, so a run that misses the gap still reports whether the rig + * can see a tear at all. */ if (c_err < 2147483648.0 || c_err > 6442450944.0) { printf("\nGATE TSF-WRAP: FAIL - the control is %+.0f us off, not the +2^32 us a torn read gives; the rig cannot see a tear\n", c_err); return 1; } + if (!f_retried) { + /* The read holds against the model (checked above) and the control did + * tear, so the wrap simply did not land in the gap - transfer jitter + * can do that. Not a defect: re-run. */ + printf("\nGATE TSF-WRAP: INCONCLUSIVE - the forced read holds and the control tore, but the read took no retry; the wrap missed gap %d. Re-run.\n", + gap); + return 2; + } printf("\nGATE TSF-WRAP: PASS - retried across the wrap in gap %d, %+.0f us off the model; control tore by %+.0f us\n", gap, f_err, c_err); return 0; diff --git a/tests/mt7612u_tsf_api_selftest.cpp b/tests/mt7612u_tsf_api_selftest.cpp index e1c45373..37d2485c 100644 --- a/tests/mt7612u_tsf_api_selftest.cpp +++ b/tests/mt7612u_tsf_api_selftest.cpp @@ -11,8 +11,9 @@ * * A NULL device is the only failure this can reach without hardware, so that * is what it pins. What it does NOT cover: a failed transfer on a live device, - * and Mt7612uRadio::ReadTsf throwing. Those need the part - the - * `bringup tsfwrap` gate and the bus-disconnect run in docs/mt7612u.md. */ + * Mt7612uRadio::ReadTsf throwing, and the caps bit (filling it needs a device). + * Those need the part - the `bringup tsfwrap` and `caps` gates, and the + * bus-disconnect run in docs/mt7612u.md. */ #include "mt7612u/mt7612u.h" #include @@ -44,16 +45,11 @@ int main() { /* The no-error-channel form answers 0, never a value built from a failure. */ expect("read_tsf(NULL) is 0", mt7612u_read_tsf(nullptr) == 0); - /* The caps bit says this part has no TSF load path, so a C caller need not - * discover it from a missing symbol. */ - { - struct mt7612u_caps c; - - /* No device to fill it: the field must exist and be addressable, which is - * what the C ABI half of this is. */ - c.tsf_write = 0; - expect("caps carry tsf_write", c.tsf_write == 0); - } + /* The caps bit (struct mt7612u_caps::tsf_write) is NOT checked here: filling + * it needs mt7612u_get_caps on a live device, and asserting on a field this + * cell set itself would hold nothing. Mt7612uRadio::GetAdapterCaps takes + * tsf_write_ok from it rather than restating it, so the two cannot drift, + * and `bringup caps` prints it from the part. */ if (fails == 0) std::printf("mt7612u_tsf_api: PASS\n"); From 889073c8830068c1af8b5ea09c04ac1e2b2eb108 Mon Sep 17 00:00:00 2001 From: snokvist Date: Fri, 18 Sep 2026 07:08:40 +0200 Subject: [PATCH 10/11] mt7612u tsfwrap: refuse bad arguments before the arithmetic, and name the gap it proved Qodo on #436, plus the objections a reviewer would raise next: - gate_tsfwrap computed 1ull << wrap_bits and the int64 deadline in its initialiser list, before the checks that reject them: an out-of-range shift and a non-finite max_min were undefined before the gate could refuse. The checks come first now, and the refusals use rc 2, which is what the rest of this tool returns for a bad invocation. - Because rc 2 is taken, "no verdict" (interrupted, or the wrap missed the gap) is rc 3. A wrapper re-runs a 3; a 1 is a defect. - A retry says the wrap fell inside the read, not that it fell where it was aimed. The first low word says which gap it really landed in, and a run that covered the other one reports INCONCLUSIVE instead of crediting a gap it never exercised. - docs/mt7612u.md hands the return and throw semantics back to the declarations that own them (mt7612u.h, IRadio.h, mt7612u::tsf_read) and keeps the measurements; the public _chk doc says why a return code carries the failure - 0xffffffff is a legitimate word here. - tests/mt7612u_tsf_wrap.sh wraps the gate: the invocation, the ~72 min per wrap, one gap per run, one adapter being enough, and the rc-3 re-run rule, none of which should be folk knowledge. Also listed in tests/README.md. - mt7612u_tsf_api records the mutation it catches (deleting the NULL guard segfaults it), so "asserts only NULL refusals" is a scope, not a vacuum. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JP51Yp3WSbfiMDuDHJByHW --- docs/mt7612u.md | 30 +++---- src/IRadio.h | 5 +- src/mt7612u/include/mt7612u/mt7612u.h | 4 +- src/mt7612u/tools/bringup.cpp | 52 ++++++++++-- tests/README.md | 15 ++++ tests/mt7612u_tsf_api_selftest.cpp | 3 +- tests/mt7612u_tsf_wrap.sh | 111 ++++++++++++++++++++++++++ 7 files changed, 192 insertions(+), 28 deletions(-) create mode 100755 tests/mt7612u_tsf_wrap.sh diff --git a/docs/mt7612u.md b/docs/mt7612u.md index 310f6b5d..102b634f 100644 --- a/docs/mt7612u.md +++ b/docs/mt7612u.md @@ -230,8 +230,9 @@ saturation. A-MPDU, not USB parallelism, is what lifted 34 → 44.55 Mbit/s. `(dw0 << 32) | dw1`, which is backwards, but that value only feeds a `dev_dbg()` print upstream so the bug is never exercised. Ported faithfully it produced a clock advancing 8.6e14 "µs" per 200 ms. -- **No tested sequence loads the TSF.** `WriteTsf` is unsupported on this part: - the DW0/DW1 registers hold the counter, they do not load it. Every sequence +- **No tested sequence loads the TSF** (the contract is `mt7612u_caps::tsf_write` + and `IRadio::WriteTsf`): the DW0/DW1 registers hold the counter, they do not + load it. Every sequence the gate tries was measured ignored on two units (`40:a5:ef:50:27:a1` and `40:a5:ef:5a:32:f8`, channels 6 and 149 each) — `DW0` then `DW1` and the reverse, each word alone (with a high word that actually differs), a write @@ -248,21 +249,18 @@ saturation. A-MPDU, not USB parallelism, is what lifted 34 → 44.55 Mbit/s. Mainline mt76 registers no `.set_tsf` for the mt76x02 family, so the absence is a property of the part rather than of this port. The `bringup tsfwrite` gate runs that control and sweep and PASSes only while the clock is alive and - every sequence is confirmed ignored. The backend therefore reports - `AdapterCaps::tsf_write_ok = false` and does not override `WriteTsf`: the - `IRadio` default returns false and issues no register write at all, rather - than pretending one landed. + every sequence is confirmed ignored. That result is what sets + `mt7612u_caps::tsf_write`, which `AdapterCaps::tsf_write_ok` is taken from. - **The two TSF halves are not latched.** A DW0 read does not freeze DW1, so a read whose halves straddle a low-word wrap tears by 2^32 µs. Bring-up restarts the counter near 0 (~0.35–0.40 s on both units), so the first wrap is 71.6 min in. A plain DW0-then-DW1 read hits it about once per wrap when polled back to back, and roughly (read time ÷ poll interval) per wrap - otherwise. `mt7612u_read_tsf_chk` and `mt7612u_read_tsf` read high, low, - high, and re-read the low word when the high word moved - (`Mt7612uTsfRead.h`) — a third control transfer per read, a fourth across a - wrap. + otherwise. The read that survives it, and what it costs, is the contract at + `mt7612u::tsf_read` (`Mt7612uTsfRead.h`) and the declarations it serves. - `bringup tsfwrap [gap]` holds that against the part, judging every read + `bringup tsfwrap [gap]` (wrapped by `tests/mt7612u_tsf_wrap.sh`, which owns + the runtime and the re-run rule) holds that against the part, judging every read against a host-clock model fitted over the preceding minute rather than against the read under test. Around the wrap it forces the library's read template to take the wrap in the chosen gap, interleaves a plain DW0,DW1 read @@ -288,9 +286,11 @@ saturation. A-MPDU, not USB parallelism, is what lifted 34 → 44.55 Mbit/s. reader) and the `mt7612u_tsf_read` cell. It is one wrap per unit, and the gate's `wrap_bits` smoke mode cannot tear a read at all. - A failed transfer fails the read: `_chk` returns -1, `mt7612u_read_tsf` - returns 0, and `Mt7612uRadio::ReadTsf` throws `std::ios_base::failure`, as - the Realtek USB backends do. Checked once by hand, not by a gate: under a + A failed transfer fails the read rather than joining a `0xffffffff` word + into a plausible clock; the return and throw semantics are the declarations' + (`mt7612u_read_tsf_chk` / `mt7612u_read_tsf` in `include/mt7612u/mt7612u.h`, + `IRadio::ReadTsf`). What is measured here is that the failure is reachable + and reaches the caller. Checked once by hand, not by a gate: under a 100 Hz `ReadTsf` poll, de-authorising the adapter's whole bus (`usbN/authorized` — a logical disconnect, not a cable pull) gave 795 plausible reads and then 200 of 200 calls threw, with no value returned @@ -387,7 +387,7 @@ submodule: | `mt7612u_mapping` | the descriptor translations — RSSI bias, per-chain signal, rate codes, TID offset, widths. Every one of these has been wrong once | | `mt7612u_rx_queue` | the RX hand-off queue's two load-bearing properties: a full ring drops the **newest** frame and counts it, and a popped slot outlives the queue lock. Broken, the first reorders frames or wedges the part and the second is a use-after-free — and both look like a healthy link until a packet processor falls behind | | `mt7612u_tsf_read` | that the TSF read stays coherent when the low word wraps between any two of its register accesses: a scripted counter swept across the wrap one microsecond at a time, every failure position, and an all-ones low word as a value. The pre-fix DW0,DW1 order runs against the same sweep and must tear, or the cell cannot see the bug. Mutation-tested by hand three ways — no retry, a retry that keeps the first high word, a retry that skips the low-word re-read — each fails | -| `mt7612u_tsf_api` | that a failed TSF read stays distinguishable from a value at the C entry points: `mt7612u_read_tsf_chk` refuses a NULL device or output with -1 and does not write through the pointer, and `mt7612u_read_tsf` answers 0. `0xffffffff` is a legitimate register word here, so only the return code can carry a failure. Reaches the NULL refusals only; a failed transfer on a live device, the `Mt7612uRadio::ReadTsf` throw and the `tsf_write` caps bit need the part (`bringup caps` prints it, and `Mt7612uRadio::GetAdapterCaps` takes `tsf_write_ok` from it rather than restating it) | +| `mt7612u_tsf_api` | that a failed TSF read stays distinguishable from a value at the C entry points: `mt7612u_read_tsf_chk` refuses a NULL device or output with -1 and does not write through the pointer, and `mt7612u_read_tsf` answers 0. `0xffffffff` is a legitimate register word here, so only the return code can carry a failure. Not free: deleting the guard in `mt7612u_read_tsf_chk` segfaults the cell (hand-run). Reaches the NULL refusals only; a failed transfer on a live device, the `Mt7612uRadio::ReadTsf` throw and the `tsf_write` caps bit need the part (`bringup caps` prints it, and `Mt7612uRadio::GetAdapterCaps` takes `tsf_write_ok` from it rather than restating it) | | `mt7612u_usb_ids_vs_mt76` | that the id table above really is the complete `mt76x2u_device_table` from the pinned reference, byte-checked. An id we *forgot* is invisible to `mt7612u_usb_ids` — the adapter just falls through to the Realtek path — and the first draft of that header had 11 of the 16, taken from the host's kernel tree | | `mt7612u_initvals_generated` | that the checked-in `initvals.h` still matches what `tools/extract_mt7612u_tables.py` produces from the pinned reference | diff --git a/src/IRadio.h b/src/IRadio.h index 95580de4..d252e97b 100644 --- a/src/IRadio.h +++ b/src/IRadio.h @@ -355,9 +355,8 @@ class IRadio { * (rx_pkt_attrib::tsfl, the low 32 bits), so it is a precise, host-jitter-free * timing reference for multi-radio sync / TDOA / scheduled bursts. Returns 0 * where unsupported (default, and the RTL8733B). Over USB a failed read - * throws std::ios_base::failure on every backend that implements it - * (Jaguar1/2/3, Kestrel, MT7612U); a PCIe register read cannot report failure - * at all (src/RtlTsf.h). NB: a register read is a control transfer — calling + * throws std::ios_base::failure on every backend that implements it; a PCIe + * register read cannot report failure at all (src/RtlTsf.h). NB: a register read is a control transfer — calling * it concurrently with a heavy RX bulk-IN load can race (catch the * exception). */ virtual uint64_t ReadTsf() { return 0; } diff --git a/src/mt7612u/include/mt7612u/mt7612u.h b/src/mt7612u/include/mt7612u/mt7612u.h index f31258e7..e3bacb86 100644 --- a/src/mt7612u/include/mt7612u/mt7612u.h +++ b/src/mt7612u/include/mt7612u/mt7612u.h @@ -460,7 +460,9 @@ int mt7612u_phy_tick(struct mt7612u_dev *dev); * transfers). * * mt7612u_read_tsf_chk: 0 and fills *out, or -1 on a failed transfer (or a - * NULL argument), leaving *out untouched. + * NULL argument), leaving *out untouched. A separate return is what carries + * the failure because no value can: 0xffffffff is a legitimate word here (the + * low word passes through it once a wrap), so a sentinel would be a reading. * * mt7612u_read_tsf: the same read with no error channel - 0 on failure. A * running counter never reads 0 after bring-up, but 0 cannot say why; use diff --git a/src/mt7612u/tools/bringup.cpp b/src/mt7612u/tools/bringup.cpp index 2d1854d1..373f0b01 100644 --- a/src/mt7612u/tools/bringup.cpp +++ b/src/mt7612u/tools/bringup.cpp @@ -3337,26 +3337,38 @@ static bool tsf_model_holds(const struct tsf_model *m, double v, double h_start, static int gate_tsfwrap(int gap, int wrap_bits, double max_min) { static struct tsf_model model; - const uint64_t period = 1ull << wrap_bits, mask = period - 1; - const int64_t t_start = mono_us(); - int64_t deadline = t_start + (int64_t)(max_min * 60e6); int64_t last_pt = 0, last_status = 0, wrap_host = 0; uint64_t prev = 0, reads = 0, fails = 0, backwards = 0, checked = 0, off_model = 0; uint64_t consec_fails = 0; bool gone = false; double worst = 0; bool have = false, forced = false, f_retried = false, f_held = false, c_held = false; + /* The forced read's first low word, for the gap check at the verdict. */ + uint32_t lo_first = 0; + bool have_lo_first = false; int f_rc = -1; double f_err = 0, c_err = 0; + /* Argument checks come before the arithmetic they feed: 1ull << wrap_bits + * is undefined for a wrap_bits outside the word, and a non-finite or huge + * max_min has no int64 to convert to. atof() gives 0 for a non-number, + * which the positive check below refuses. */ if (gap != 1 && gap != 2) { printf("GATE TSF-WRAP: FAIL - gap must be 1 or 2\n"); - return 1; + return 2; } if (wrap_bits < 20 || wrap_bits > 32) { printf("GATE TSF-WRAP: FAIL - wrap_bits must be 20..32\n"); - return 1; + return 2; + } + if (!isfinite(max_min) || max_min <= 0 || max_min > 24 * 60) { + printf("GATE TSF-WRAP: FAIL - max_min must be a positive number of minutes, at most a day\n"); + return 2; } + + const uint64_t period = 1ull << wrap_bits, mask = period - 1; + const int64_t t_start = mono_us(); + int64_t deadline = t_start + (int64_t)(max_min * 60e6); /* Register reads only: the MAC stays as mt_init_hardware left it (stopped), * so there is nothing for an early return to unwind. */ if (mt_eeprom_init(&dev) || mt_init_hardware(&dev, NULL) || @@ -3476,6 +3488,13 @@ static int gate_tsfwrap(int gap, int wrap_bits, double max_min) if (addr == MT_TSF_TIMER_DW0) { lo_start = start; lo_end = mono_us(); + /* Which side of the wrap the FIRST low read landed on is what + * says which gap the wrap actually fell in - the requested one + * is only where it was aimed. */ + if (!have_lo_first && !r) { + lo_first = *val; + have_lo_first = true; + } } k++; return r; @@ -3515,9 +3534,12 @@ static int gate_tsfwrap(int gap, int wrap_bits, double max_min) (unsigned long long)off_model); return 1; } + /* 0 PASS, 1 FAIL, 2 bad invocation (as everywhere else in this tool), + * 3 no verdict: interrupted, or the wrap missed the gap. A wrapper reruns + * a 3; a 1 is a defect and a 2 is the operator's. */ if (g_stop) { printf("\nGATE TSF-WRAP: INTERRUPTED - no verdict\n"); - return 2; + return 3; } if (!forced) { printf("\nGATE TSF-WRAP: FAIL - no wrap reached before the deadline (%.0f min)\n", @@ -3552,13 +3574,27 @@ static int gate_tsfwrap(int gap, int wrap_bits, double max_min) c_err); return 1; } + /* A retry says the wrap fell somewhere inside the read, not that it fell + * where it was aimed: a mistimed wrap lands in the other gap and still + * retries. The first low word says which - post-wrap it reads small, + * pre-wrap it reads just under the mask. Crediting the wrong gap would + * report a gap as covered when it never was. */ + if (f_retried && have_lo_first) { + const int actual = ((uint64_t)lo_first & mask) < period / 2 ? 1 : 2; + + if (actual != gap) { + printf("\nGATE TSF-WRAP: INCONCLUSIVE - the wrap landed in gap %d, not the requested gap %d (first low word 0x%08x). Re-run.\n", + actual, gap, lo_first); + return 3; + } + } if (!f_retried) { /* The read holds against the model (checked above) and the control did * tear, so the wrap simply did not land in the gap - transfer jitter * can do that. Not a defect: re-run. */ printf("\nGATE TSF-WRAP: INCONCLUSIVE - the forced read holds and the control tore, but the read took no retry; the wrap missed gap %d. Re-run.\n", gap); - return 2; + return 3; } printf("\nGATE TSF-WRAP: PASS - retried across the wrap in gap %d, %+.0f us off the model; control tore by %+.0f us\n", gap, f_err, c_err); @@ -3707,7 +3743,7 @@ int main(int argc, char **argv) fprintf(stderr, "usage: bringup [regs|fw|init|chan|tx|rx|hop|gateg|tsfwrite|tsfwrap] [chan] [count] [phy 0=CCK 1=OFDM 2=HT 4=VHT] [mcs]\n"); fprintf(stderr, " bringup adopt (the mt_adopt path a libusb-owning consumer uses)\n"); fprintf(stderr, " bringup tsfwrite [chan] (confirm this part has no TSF load path)\n"); - fprintf(stderr, " bringup tsfwrap [gap 1|2] [wrap_bits] [max_min] (TSF read across the low-word wrap, ~72 min; rc 2 = inconclusive)\n"); + fprintf(stderr, " bringup tsfwrap [gap 1|2] [wrap_bits] [max_min] (TSF read across the low-word wrap, ~72 min; rc 3 = no verdict, re-run)\n"); fprintf(stderr, " bringup beacon [chan] [secs] (Stage A: static AP beacon on air)\n"); fprintf(stderr, " bringup ap [chan] [secs] (Stage B: beacon + RX, probe/auth/assoc)\n"); fprintf(stderr, " bringup [sweep|coding|vht] [chan] [count] [bw 0=20 1=40 2=80]\n"); diff --git a/tests/README.md b/tests/README.md index 33ced777..915da4b5 100644 --- a/tests/README.md +++ b/tests/README.md @@ -577,6 +577,21 @@ from `DEVOURER_HOP_BW`, not from the `/40` in `DEVOURER_TX_RATE` (that only fills the descriptor field), and getting that wrong zeroes a cell for reasons that have nothing to do with the DUT. +### `mt7612u_tsf_wrap.sh`: the MT7612U TSF read across the low-word wrap + +Wraps `bringup tsfwrap`, which is where the "two TSF halves are not latched" +table in `docs/mt7612u.md` comes from. The two register halves are not latched, +so a read is only wrong for the few hundred microseconds around a low-word +wrap, and bring-up restarts the counter: one run is ~72 min and covers one gap +of the read. Both gaps cost ~2.4 h on one adapter, or ~72 min on two in +parallel (`DEVS="6-1 7-1"`). `SMOKE=1` checks the schedule, the host-clock +model and the plumbing against a 16.7 s carry in ~2.5 min, and says SMOKE +rather than PASS because a carry inside the low word cannot tear a read. + +One adapter is enough to verify the claim; the second only buys another unit +and the wall-clock. A run can end with no verdict (rc 3) when the wrap lands in +the other gap — that is a re-run, which the script does once, not a defect. + ## Supported DUTs Listed in `SUPPORTED_DUTS` at the top of `regress.py`. Extend the dict diff --git a/tests/mt7612u_tsf_api_selftest.cpp b/tests/mt7612u_tsf_api_selftest.cpp index 37d2485c..4bdce9cf 100644 --- a/tests/mt7612u_tsf_api_selftest.cpp +++ b/tests/mt7612u_tsf_api_selftest.cpp @@ -10,7 +10,8 @@ * TSF - the silent-garbage shape this whole path exists to remove. * * A NULL device is the only failure this can reach without hardware, so that - * is what it pins. What it does NOT cover: a failed transfer on a live device, + * is what it pins, and it is not free: deleting the `!d || !out` guard in + * mt7612u_read_tsf_chk segfaults this cell (hand-run). What it does NOT cover: a failed transfer on a live device, * Mt7612uRadio::ReadTsf throwing, and the caps bit (filling it needs a device). * Those need the part - the `bringup tsfwrap` and `caps` gates, and the * bus-disconnect run in docs/mt7612u.md. */ diff --git a/tests/mt7612u_tsf_wrap.sh b/tests/mt7612u_tsf_wrap.sh new file mode 100755 index 00000000..ad065bac --- /dev/null +++ b/tests/mt7612u_tsf_wrap.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# mt7612u_tsf_wrap.sh — the MT7612U TSF read across the low-word wrap. +# +# Every number in the "two TSF halves are not latched" table in +# docs/mt7612u.md comes from `bringup tsfwrap`, which this wraps. The gate +# itself is the measurement; this exists so the invocation, the runtime and +# the re-run rule are not folk knowledge: +# +# - The wrap is 71.6 min after bring-up, which restarts the counter, so ONE +# run is ~72 min and covers ONE gap of the read. Both gaps therefore cost +# ~2.4 h on a single adapter, or ~72 min on two in parallel. +# - The gate can end with no verdict (rc 3): the wrap landed in the other +# gap, or someone interrupted it. That is a re-run, not a defect. This +# script re-runs a gap once for that, and never for rc 1. +# - One adapter is enough to verify the claim. Two only buys the second +# unit and the wall-clock. +# +# Nothing here power-cycles a port, unbinds a driver, or touches an adapter +# other than the one named: the gate is register reads on the device devourer +# has claimed. +# +# tests/mt7612u_tsf_wrap.sh # first MT7612U, both gaps +# MT7612U_DEV=6-1 tests/mt7612u_tsf_wrap.sh # that adapter, both gaps +# DEVS="6-1 7-1" tests/mt7612u_tsf_wrap.sh # one gap each, in parallel +# GAPS=1 tests/mt7612u_tsf_wrap.sh # just gap 1 +# SMOKE=1 tests/mt7612u_tsf_wrap.sh # ~2.5 min, no wrap verdict +# +# Exit: 0 every run PASSed (or SMOKEd), 1 any run FAILed, 3 a run ended +# without a verdict twice, 2 bad invocation. +set -uo pipefail + +here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +bringup=${BRINGUP:-$here/../src/mt7612u/bringup} +# The C library resolves its firmware relative to the working directory, so +# every run happens in the tool's own directory - the same place `make -C +# src/mt7612u bringup` builds it and the hardware notes invoke it from. +bringup_dir=$(cd -- "$(dirname -- "$bringup")" && pwd 2>/dev/null) || bringup_dir='' +bringup_exe=./$(basename -- "$bringup") +devs=${DEVS:-${MT7612U_DEV:-}} +gaps=${GAPS:-"1 2"} +smoke=${SMOKE:-0} +bits=32 +max_min=${MAX_MIN:-80} + +if [ ! -x "$bringup" ]; then + echo "no bringup at $bringup — build it with: make -C src/mt7612u bringup" >&2 + exit 2 +fi +if [ ! -r "$bringup_dir/firmware/mt7662.bin" ] || + [ ! -r "$bringup_dir/firmware/mt7662_rom_patch.bin" ]; then + echo "no firmware in $bringup_dir/firmware (mt7662.bin + mt7662_rom_patch.bin, from linux-firmware)" >&2 + exit 2 +fi +if [ "$smoke" != 0 ]; then + # The carry out of bit 24 comes every 16.7 s. It checks the schedule, the + # host-clock model and the plumbing; it cannot tear a read, and the gate + # says SMOKE rather than PASS for exactly that reason. + bits=24 + max_min=5 +fi + +# One run. Re-runs once on "no verdict", which is the gate asking for it. +run_gap() { + local dev=$1 gap=$2 attempt rc + local label="gap $gap${dev:+ on $dev}" + + for attempt in 1 2; do + echo "== $label (attempt $attempt, up to $max_min min)" + if [ -n "$dev" ]; then + ( cd "$bringup_dir" && MT7612U_DEV=$dev "$bringup_exe" tsfwrap "$gap" "$bits" "$max_min" ) + else + ( cd "$bringup_dir" && "$bringup_exe" tsfwrap "$gap" "$bits" "$max_min" ) + fi + rc=$? + case $rc in + 0) return 0 ;; + 3) echo "== $label: no verdict (rc 3); the gate asks for a re-run" ;; + *) return "$rc" ;; + esac + done + echo "== $label: no verdict twice — the wrap keeps missing the gap" >&2 + return 3 +} + +rc_all=0 +if [ "$(echo "$devs" | wc -w)" -gt 1 ]; then + # One gap per adapter, in parallel: the wrap is per-adapter, so this is + # the only way to cover both gaps in one wrap's worth of wall-clock. + i=0 + pids=() + for dev in $devs; do + i=$((i + 1)) + gap=$(echo "$gaps" | cut -d' ' -f$(( (i - 1) % $(echo "$gaps" | wc -w) + 1 ))) + run_gap "$dev" "$gap" & + pids+=($!) + done + for pid in "${pids[@]}"; do + wait "$pid" || rc_all=$? + done +else + for gap in $gaps; do + run_gap "$devs" "$gap" || rc_all=$? + done +fi + +if [ "$rc_all" = 0 ]; then + echo "TSF-WRAP: every run returned a verdict and it was PASS" +else + echo "TSF-WRAP: rc $rc_all — see the per-run verdicts above" >&2 +fi +exit "$rc_all" From 9fd5faa444d501dbd0234a6500f5f5a82f1063a4 Mon Sep 17 00:00:00 2001 From: snokvist Date: Fri, 18 Sep 2026 07:18:20 +0200 Subject: [PATCH 11/11] mt7612u tsfwrap: a sleep that exists on macOS too CI caught it: bringup.cpp is built by CMake as mt7612uprobe on every UNIX platform, not only by the subtree Makefile, and macOS has neither clock_nanosleep nor TIMER_ABSTIME. The gate's schedule now sleeps the remaining delta against mono_us() and re-checks, which keeps mono_us() the only clock in the measurement - reaching for a second clock source would put an epoch difference between the schedule and every timestamp around it. Same placement on the part: the forced accesses land within 0.12 ms of their targets (-39.88 / +40.07 / +45.06 ms against -40 / +40 / +45), and both units still SMOKE green. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JP51Yp3WSbfiMDuDHJByHW --- src/mt7612u/tools/bringup.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/mt7612u/tools/bringup.cpp b/src/mt7612u/tools/bringup.cpp index 373f0b01..933c1ddd 100644 --- a/src/mt7612u/tools/bringup.cpp +++ b/src/mt7612u/tools/bringup.cpp @@ -4,7 +4,8 @@ * stage is independently runnable on hardware. */ #include -#include +#include +#include #include #include #include @@ -3265,16 +3266,20 @@ static int64_t mono_us(void) return (int64_t)t.tv_sec * 1000000 + t.tv_nsec / 1000; } +/* Sleep until a mono_us() instant. Not clock_nanosleep(TIMER_ABSTIME): macOS + * has neither, and reaching for a second clock source would put an epoch + * difference between the schedule and every timestamp around it. Sleeping the + * remaining delta and re-checking keeps mono_us() the only clock; an + * interrupted or short sleep just goes round again. */ static void sleep_until_us(int64_t at) { - struct timespec t; + for (;;) { + const int64_t left = at - mono_us(); - t.tv_sec = at / 1000000; - t.tv_nsec = (long)(at % 1000000) * 1000; - /* clock_nanosleep returns the error number itself; only an interrupt is - * worth resuming, and never past a stop request. */ - while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &t, NULL) == EINTR && !g_stop) - ; + if (left <= 0 || g_stop) + return; + std::this_thread::sleep_for(std::chrono::microseconds(left)); + } } /* A ring of (host, tsf) points and the line through them. */