diff --git a/CMakeLists.txt b/CMakeLists.txt index 3bbe9e37..348b79ca 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 @@ -927,6 +928,33 @@ 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) + +# 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 d9dcea1d..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,10 +249,54 @@ 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. 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]` (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 + 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: + + | | `40:a5:ef:50:27:a1`, gap 1 | `40:a5:ef:5a:32:f8`, gap 2 | + |---|---|---| + | 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; + 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 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 + 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 @@ -330,15 +375,19 @@ 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 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 | |---|---| | `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 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. 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 | @@ -347,6 +396,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 `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 — are covered by nothing headless. Deleting the `pop_commit()` compiles clean and @@ -361,7 +415,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 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 | @@ -521,13 +575,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. -- **Three 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` 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, 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/chanmig/main.cpp b/examples/chanmig/main.cpp index 2c044734..5e9419d2 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,20 @@ 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, but it + * is said once rather than swallowed. */ + try { + return g_dev ? g_dev->ReadTsf() : 0; + } 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; + } } static void drone_do(const std::vector &acts) { diff --git a/examples/tdma/main.cpp b/examples/tdma/main.cpp index 9973ef6f..a72c8041 100644 --- a/examples/tdma/main.cpp +++ b/examples/tdma/main.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -124,13 +125,31 @@ 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. - 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, and leave the burst + // unmarked so the next pass through it tries again. + uint64_t tx_tsf = 0; + bool stamped = true; + try { + 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; + } } 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..564280a9 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,26 @@ 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) { + static bool warned = false; + stamped = false; + 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); + 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/IRadio.h b/src/IRadio.h index e363b8da..d252e97b 100644 --- a/src/IRadio.h +++ b/src/IRadio.h @@ -354,8 +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 + * where unsupported (default, and the RTL8733B). Over USB a failed read + * 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/Mt7612uRadio.cpp b/src/mt7612u/Mt7612uRadio.cpp index 9254ac2d..2cdba3e5 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 @@ -1102,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/Mt7612uTsfRead.h b/src/mt7612u/Mt7612uTsfRead.h new file mode 100644 index 00000000..b3eb6825 --- /dev/null +++ b/src/mt7612u/Mt7612uTsfRead.h @@ -0,0 +1,61 @@ +#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: 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. 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. + * 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, 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) { + 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/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 65a006e0..806a0cb5 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,13 +21,24 @@ * (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) +{ + if (!d || !out) + return -1; + 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) { - 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; } void mt7612u_get_caps(const struct mt7612u_dev *d, struct mt7612u_caps *c) @@ -50,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 7c0eabd2..e3bacb86 100644 --- a/src/mt7612u/include/mt7612u/mt7612u.h +++ b/src/mt7612u/include/mt7612u/mt7612u.h @@ -451,8 +451,24 @@ 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 (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. 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 + * _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. */ @@ -487,6 +503,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 d51029f1..2f3d17e8 100644 --- a/src/mt7612u/tests/api_link.c +++ b/src/mt7612u/tests/api_link.c @@ -40,6 +40,9 @@ 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, (void *)mt7612u_asic_version, diff --git a/src/mt7612u/tools/bringup.cpp b/src/mt7612u/tools/bringup.cpp index 065afa0d..933c1ddd 100644 --- a/src/mt7612u/tools/bringup.cpp +++ b/src/mt7612u/tools/bringup.cpp @@ -4,6 +4,9 @@ * stage is independently runnable on hardware. */ #include +#include +#include +#include #include #include #include @@ -15,6 +18,7 @@ #include #include #include "../internal.h" +#include "../Mt7612uTsfRead.h" static struct mt7612u_dev dev; @@ -634,29 +638,40 @@ 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; - int good = 0; + uint64_t prev = 0, tsf; + bool have = false; + int good = 0, failed = 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); + failed++; + 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; } /* 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; @@ -1790,8 +1805,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, @@ -1813,33 +1828,49 @@ 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)); - 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); - t1 = mt7612u_read_tsf(&dev); 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 */ @@ -2903,26 +2934,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 @@ -3184,6 +3201,411 @@ 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: 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 + * 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; +/* 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; + +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; +} + +/* 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) +{ + for (;;) { + const int64_t left = at - mono_us(); + + 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. */ +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; + 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 2; + } + if (wrap_bits < 20 || wrap_bits > 32) { + printf("GATE TSF-WRAP: FAIL - wrap_bits must be 20..32\n"); + 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) || + 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; + if (++consec_fails >= kTsfWrapMaxConsecFails) { + gone = true; + break; + } + continue; + } + consec_fails = 0; + 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 <= kTsfWrapFeedMaxUs && 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. */ + 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 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 }; + uint32_t c_lo = 0, c_hi = 0; + bool c_ok = true; + int k = 0; + uint64_t fv = 0; + + wrap_host = w; + 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(); + + auto rd = [&](uint32_t addr, uint32_t *val) { + int64_t start; + int r; + + if (k < 4) { + if (k == first_post) { + /* 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]); + } + 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(); + /* 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; + }; + 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 (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, + (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 3; + } + if (!forced) { + printf("\nGATE TSF-WRAP: FAIL - no wrap reached before the deadline (%.0f min)\n", + max_min); + 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; + } + /* 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; + } + /* 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. 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; + } + /* 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 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); + return 0; +} + int main(int argc, char **argv) { const char *err = NULL, *cmd = argc > 1 ? argv[1] : "regs"; @@ -3245,6 +3667,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); @@ -3319,9 +3745,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; 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 new file mode 100644 index 00000000..4bdce9cf --- /dev/null +++ b/tests/mt7612u_tsf_api_selftest.cpp @@ -0,0 +1,58 @@ +/* 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, 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. */ +#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 (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"); + return fails == 0 ? 0 : 1; +} 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; +} 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"