mt7612u: read the TSF coherently across a low-word wrap, and fail loudly - #436
Conversation
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) <noreply@anthropic.com>
…he 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) <noreply@anthropic.com>
…nter - 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) <noreply@anthropic.com>
OpenIPC#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) <noreply@anthropic.com>
… 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 (OpenIPC#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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JP51Yp3WSbfiMDuDHJByHW
- 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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JP51Yp3WSbfiMDuDHJByHW
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JP51Yp3WSbfiMDuDHJByHW
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JP51Yp3WSbfiMDuDHJByHW
…ight 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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JP51Yp3WSbfiMDuDHJByHW
PR Summary by QodoMake MT7612U TSF reads wrap-safe and failure-aware
AI Description
Diagram
High-Level Assessment
Files changed (15)
|
Code Review by Qodo
1.
|
… the gap it proved Qodo on OpenIPC#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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JP51Yp3WSbfiMDuDHJByHW
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JP51Yp3WSbfiMDuDHJByHW
josephnef
left a comment
There was a problem hiding this comment.
Reviewed at 9fd5faa (11 commits, up to date with master). Approving.
Checked on the branch here (headless; no MT7612U on this bench or in the inventory, so the tsfwrap gate itself was not re-run — the two-unit evidence is the author's run, which the gate makes reproducible):
cmake -DDEVOURER_MT7612U=ONbuilds warning-free for the new code;ctest68/68 (the tworeference/mt76cells skip, as documented).mt7612u_tsf_read: PASS (450 retries, old order tore 300 times),mt7612u_tsf_api: PASS.make -C src/mt7612u check:api_link: 33 public entry points resolved(master resolves 30).- Mutation: disabling the
hi2 != hiretry inMt7612uTsfRead.hfails the cell on the coherence sweep, the access-3 failure positions and the "reaches the retry" pin. The negative control is real. - Read discipline: hi, lo, hi, retry lo paired with the second hi — coherent for a wrap in either gap, and the retry can only tear on a second wrap 71.6 min later. Same shape as
read_tsftr(src/RtlTsf.h) andRtlKestrelDevice::ReadTsf; theIRadio.hnote ("throws over USB on every backend that implements it, PCIe cannot report failure, RTL8733B returns 0") matches what those backends actually do. gate_tsfwrap: schedule/gap arithmetic, the control's sign check (+2^32 only), the "which gap did the wrap really land in" credit from the first low word, and the rc 0/1/2/3 split all read correctly. The argument checks now precede the shift and the int64 conversion (qodo's thread).Mt7612uRadio::GetAdapterCapstakestsf_write_okfrom the zeroed-then-filled C caps, so the bit has one owner.
Nits, none blocking:
src/IRadio.h~L359: the new sentence makes one ~120-col line inside an otherwise wrapped comment. Wrap it.examples/tdma/main.cpp: leavinglast_marker_burstunset on a failed read means the NB phase retries the 3-transfer read on every loop pass until it succeeds, not once per burst as before. That is the right call for the transient RX-flood race theIRadionote describes, but on a dead transport it is a tight loop of failing control transfers. One sentence in the comment saying that is deliberate would do; a per-burst backoff is the alternative.tests/mt7612u_tsf_wrap.shparallel mode: notrapon INT/TERM to kill${pids[@]}. A tty Ctrl-C reaches the children via the process group andbringuphandles it, but a dropped SSH session or akillof the wrapper leaves two 72-minbringupruns holding the adapters. Cheap to add.- The
mt7612u_read_tsf"0 on failure" contract collides withIRadio's "0 = unsupported" meaning if a C consumer ever forwards it; the header already says to use_chkwherever a failure has to be told apart, so no change — noting it for the record.
Splitting mt7612u_ch_time / mt7612u_phy_tick out of api_link is not needed; they belong with the "covers all entry points" claim this PR rewrites.
mt7612u_read_tsfreadMT_TSF_TIMER_DW0thenDW1with the uncheckedmt_rrand joined them. The two halves are not latched. A read whose halves straddle
the 2^32 µs low-word wrap therefore returns a value 2^32 µs (71.6 min) off. The
first wrap comes 71.6 min after bring-up, which restarts the counter. A failed
transfer joined
0xffffffffinto a plausible wrong clock, andMt7612uRadio::ReadTsfpassed both through as a TSF.
The read tears on hardware, and the fix does not
bringup tsfwrap [gap], new in this PR and reproducible from the tree, judgesevery read against a least-squares host-clock model fitted over the preceding
minute, not against the read under test. The gate does three things:
template through a reader that sleeps to a schedule, so the wrap lands in a
chosen gap of the read.
wrap.
mt7612u_read_tsf_chkreadnear the wrap.
Each value is judged over the host interval bracketing its control transfer. One
transfer took ~10 ms on the slower unit's bus, which a single timestamp would
have misjudged by that much.
One run per unit, both at the real 32-bit wrap:
40:a5:ef:50:27:a1, gap 140:a5:ef:5a:32:f8, gap 2Both runs
PASS. The second unit is on a USB 2.0 bus where a control transfercan take ~10 ms, which is where its control's extra 4955 µs comes from: a read
is judged over the interval that bracketed its transfer, so that is slack in the
measurement rather than in the counter.
The control tearing by 2^32 is also the latch answer: a DW0 read does not freeze
DW1.
What that does not show.
occupies the wrap instant, so
mt7612u_read_tsf_chknever takes its retryacross the wrap in that run. The retry path is covered by the forced template
read, which is the same code through another reader, and by the headless cell
below.
wrap_bits< 32 reportsSMOKE,never
PASS.Failure path, checked once by hand, not a gate. Under a 100 Hz
ReadTsfpoll I de-authorised the adapter's whole bus (
usbN/authorized, a logicaldisconnect, not a cable pull). That gave 795 plausible reads, then 200 of 200
calls threw, with no value returned after the disconnect and a clean teardown.
De-authorising only the device (
8-1/authorized) is not a disconnect on thispart: EP0 kept answering and the TSF kept reading correctly for 40 s. The first
attempt would have "passed" vacuously on that.
Structure
src/mt7612u/Mt7612uTsfRead.his a pure template. It reads high, low,high; if the high word moved, it re-reads the low word and pairs it with the
second high word.
*outuntouched.0xffffffffis a legitimate word, which is why a return code carriesfailure rather than a sentinel value.
read_tsftrinsrc/RtlTsf.h, which re-reads bothwords instead.
mt7612u_read_tsf_chk(dev, &out)is a new C entry point returning 0 or-1.
mt7612u_read_tsfuses the same read and returns 0 on failure.Mt7612uRadio::ReadTsfthrowsstd::ios_base::failureon a failed read,as the Realtek USB backends already do. The
IRadio::ReadTsfnote now saysthat for USB, keeps the PCIe hedge
RtlTsf.hcarries, and names the RTL8733Bas returning 0.
struct mt7612u_capsgainstsf_write : 1(0 here), so the C ABI sayswhat
AdapterCaps::tsf_write_oksays.bringup:tsfwritegate calls the library reader, so there is one copy of theread discipline instead of two.
only the chain, so a failing transport could still report a live timer.
capsgate prints no word-order verdict from failed raw reads.timesync,tdma,chanmig: the two timing demos skip that markerrather than stamp a wrong time;
chanmigstamps its existing "no TSF" 0 andwarns once.
api_linkgainsmt7612u_read_tsf_chkplus the two public functions itwas already missing,
mt7612u_ch_timeandmt7612u_phy_tick. All 33 nowresolve. The two omissions predate this branch and would normally be their
own change; they ride here because this PR rewrites the doc line that claims
the cell covers all public entry points, and that claim was false. Say the
word and I will split them out.
tests/mt7612u_tsf_wrap.shwraps the gate so the invocation, the ~72 minper wrap, one gap per run, one adapter being enough, and the re-run rule are
not folk knowledge.
SMOKE=1checks the schedule, the model and the plumbingin ~2.5 min against a 16.7 s carry, and reports
SMOKE, neverPASS.Caller-visible behaviour changes
Mt7612uRadio::ReadTsfthrows on a failed read, where it returned agarbage value. Any MT7612U consumer that does not catch exceptions will now
exit on a transport failure instead of carrying a wrong clock. That matches
what every Realtek USB backend already does.
mt7612u_read_tsfreturns 0 on failure instead of a value built from0xffffffffwords.ReadTsfis caller-cadence, never on the send path.timesyncandtdmamasters skip a marker when the TSF read fails,and each says so once rather than at marker rate.
struct mt7612u_capsgainstsf_write. It lands in spare padding of anexisting bitfield, so
sizeofis unchanged (32 B on x86-64; no 32-bittoolchain here to check that layout), and
mt7612u_get_capszeroes the structfirst, so a caller built against the new header and linked to an older library
reads 0 — which is the right answer for this part.
bringup tsfwrapexit codes: 0 PASS, 1 the part failed the measurement,2 bad invocation (as everywhere else in the tool), 3 no verdict — interrupted,
or the wrap landed in the other gap, which is a re-run rather than a defect.
Out of scope, stated
The
ReadTsfcallers that still do not catch (kestrelprobe,beacon_steer_check,dl_departure_tx,pcie_ptp_beacon,pcie_txegress_tx,tsf_probe's first loop) are Kestrel-, Realtek- orPCIe-specific bench tools. Their behaviour is unchanged by this PR: those
backends already throw.
Headless coverage
mt7612u_tsf_api(ctest, needsDEVOURER_MT7612U, which CI's MediaTek jobsset) pins the C entry points' failure contract:
_chkrefuses a NULL device oroutput with -1 and does not write through the pointer,
mt7612u_read_tsfanswers 0, and
struct mt7612u_capscarriestsf_write.0xffffffffis alegitimate register word here, so only the return code can carry a failure. It
reaches the NULL refusals only — a failed transfer on a live device, and the
Mt7612uRadio::ReadTsfthrow, need the part.mt7612u_tsf_read(ctest, runs withDEVOURER_MT7612U=OFFtoo) sweeps ascripted counter across the wrap one microsecond at a time, so the wrap falls in
every gap of the read. It also covers 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 fails. I also hand-mutated the template three ways (no
retry; a retry that keeps the first high word; a retry that skips the low-word
re-read), and each fails the cell. That mutation run is not in the tree. The
cell covers the read discipline, not the wiring (
docs/mt7612u.mdsays so); thewiring is what the gate covers.
Test plan
🤖 Generated with Claude Code
https://claude.ai/code/session_01JP51Yp3WSbfiMDuDHJByHW