Skip to content

IRadio: WriteTsf reports success, and the MT7612U reports false - #430

Merged
josephnef merged 4 commits into
OpenIPC:masterfrom
snokvist:fix/mt7612u-write-tsf
Sep 17, 2026
Merged

josephnef merged 4 commits into
OpenIPC:masterfrom
snokvist:fix/mt7612u-write-tsf

Conversation

@snokvist

@snokvist snokvist commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

This is a standalone change; it does not stack on the mt7612u series. It
touches the shared IRadio contract and adds an AdapterCaps field, so it is
best reviewed knowing it is also an (intentional) API change.

IRadio::WriteTsf was void with a silent no-op default, so a caller could not
tell a chip that moves its TSF from one that discards the write. The MT7612U was
that case: we wrote MT_TSF_TIMER_DW0/DW1 and the registers hold a
free-running counter that does not load.

This PR splits the question in two:

  • Static: whether a part has a standalone TSF write at all is
    AdapterCaps::tsf_write_ok, resolved at construction. It is true on
    Jaguar1/2/3 and false on the MT7612U (measured), Kestrel and the RTL8733B.
  • Per call: WriteTsf returns bool, whether the transport accepted this
    write. On PCIe, and on USB near a low-word wrap, that answer comes from a
    readback.

A bringup tsfwrite cell characterises the MT7612U's absence of a load path, so
a future firmware that enables loading trips it.

It is ignored, on hardware, in both bands, on two units

Units 8-1 (40:a5:ef:50:27:a1) and 5-1 (40:a5:ef:5a:32:f8), channels 6 and
149, firmware running, MT_BEACON_TIME_CFG free-running. All four runs PASS.
8-1, channel 149:

MT_BEACON_TIME_CFG=0x00070640 TIMER_EN=1 TBTT_EN=0 BEACON_TX=0 SYNC_MODE=3
  clock control                      t0=430488 t1=480952 delta=+50464  ALIVE
  MAC on, both words DW0,DW1         target=5481626 read=502027 err=-4979599 delta50ms=+50341  no-op
  MAC on, both words DW1,DW0         target=5552601 read=573019 err=-4979582 delta50ms=+50298  no-op
  MAC on, high word (DW1) only       target=4295590758 read=643763 err=-4294946995 delta50ms=+50286  no-op
  MAC on, low word (DW0) only        target=5694281 read=714614 err=-4979667 delta50ms=+50422  no-op
  MAC off, both words                target=5778837 read=799192 err=-4979645 delta50ms=+50378  no-op
                                     held=0 (read with TIMER_EN clear)
  MAC off, timer off, both words     target=5849794 read=20232 err=-5829562 delta50ms=+50366  no-op

GATE TSF-WRITE: PASS - confirmed: no sequence loads the TSF, so tsf_write_ok is false

How the gate avoids a false PASS:

  • Positive control. The gate only PASSes while the clock is confirmed alive,
    so a frozen counter cannot be mistaken for a successful load. Every arm also
    re-checks liveness.
  • Checked reads. Every TSF read is checked, so a failed transfer is reported
    as a transport fault, not a dead clock.
  • Wrap guard. Each arm takes its base clear of a low-word wrap, so the
    single-word arms can't misjudge a load across a carry.
  • Host-time bounds. A silently retried EP0 transfer can't turn a late
    readback of a real load into a "no-op": a slow arm is inconclusive and FAILs.
  • Non-vacuous high-word arm. Its target changes the high word
    (base + (1ull<<32)).
  • Timer-off arm. Restoring TIMER_EN restarts the counter from ~0, which
    would wipe a load before the normal readback. So this arm also reads the
    counter while the timer is still clear. It reads 0, not the target.

And the Realtek parts accept it. Every recorded readback of a +5 s write is
in docs/time-distribution.md: the source, the sleep, and error − sleep per row.
In summary:

die readbacks error − sleep
RTL8822B (incl. RTL8812BU, this PR's WriteTsf, host-timed) 45 +98 to +3419 µs (timed rows: +98 to +881 µs)
RTL8822C 2 +274 / +1013 µs
RTL8821AU (raw pair, no beacon) 2 +625 / +1000 µs

On the RTL8812BU, 40 host-timed writes (0/20/50 ms sleeps, plus backward −5 s
writes) all returned true and landed, and readback error minus the host time
from WriteTsf returning to ReadTsf returning was −485…+38 µs: the counter
loads the target and the residual is host time. The 8821AU cells are the maintainer's scratch probe on the register pair the J1
override writes; the override itself has not run on J1 hardware. The
8812AU/8814AU, 8821C (USB and CE) and 8822E share the pair and code path and are
not separately measured.

Structure

  • IRadio::WriteTsf becomes bool, and the default returns false.
    • AdapterCaps::tsf_write_ok (emitted as tsf_write in adapter.caps)
      answers "does this part have one".
    • On a tsf_write_ok part, false is a transport failure, and the counter may
      be half-updated: read back, and retry only while the device is present.
  • src/RtlTsf.h holds devourer::read_tsftr / write_tsftr, shared by
    Jaguar1/2/3. Both words are always attempted.
    • USB: true is two completed control transfers, except when the target's
      low word is within 100 ms of wrapping. There the pair can tear between the
      transfers, so it is read back.
    • PCIe: a register write is a posted MMIO store that always reports
      success, so every write is read back. A device that has left the bus
      typically reads all-ones and fails.
    • A throwing USB readback folds into false.
  • Jaguar1 writes the bare pair under _port0_mu, so it cannot interleave
    with a beacon steer. On J1 the TBTT is recorded as locked to the TSF grid, so
    expect an active beacon's TBTT to move with a write. That, and whether the
    beacon keeps airing without the steer's re-download, is unmeasured through
    WriteTsf.
  • Jaguar2/3 keep _reg_mu and route through the helper.
  • MT7612U reports tsf_write_ok = false and does not override WriteTsf,
    so the IRadio default refuses without touching a register.
    mt7612u_write_tsf (its declaration, definition and api_link entry) is
    deleted; a helper that only ever no-ops is a trap, not an API.
  • bringup tsfwrite [chan] is the characterisation cell, next to caps.

What this does not show

  • It does not move the beacon TBTT air-time on Jaguar2/3. WriteTsf moves
    the reported TSF and the beacon timestamp; the TBTT is a separate per-port
    timer there. PinBeaconTbtt / AdjustBeaconTimingFine are the TBTT levers.
  • Kestrel and the RTL8733B report false because their source has no TSF write
    at all. That is a source fact, not a bench measurement.
  • Not hardware-exercised:
    • the PCIe readback (no 8821CE here);
    • the J1 override on J1 silicon;
    • the J3 helper refactor (the bench 8812CU was busy; the register operations
      are unchanged). The J2 helper did run, on an RTL8812BU;
    • the gate's wrap-guard, time-bound and read-failure branches.

What review changed, because it is most of the value here

  • Adversarial rounds 1–2 and Qodo (commits dda2e719280d29):

    • clock positive control;
    • both word orders, and a non-vacuous high-word arm;
    • a post-sweep I/O guard;
    • per-arm liveness;
    • the J2/J3 lo_ok && hi_ok;
    • the checked MT_BEACON_TIME_CFG read in the timer-off arm;
    • mt_mac_stop on a failed start.
  • Maintainer review (45cf642):

    • Jaguar1's explicit false was measured wrong. The raw pair loads on an
      8821AU, so J1 now writes.
    • "true means both transfers landed" was false over PCIe, so the shared
      helper reads back.
    • false conflated "unsupported" with "transient", so the static half moved
      to AdapterCaps::tsf_write_ok.
    • Gate fixes: a low-word wrap could false-PASS the DW0-only arm, and the
      clock control used unchecked reads.
    • The redundant MT7612U override was dropped.
    • Nits: an enum plus an arm table instead of six copy-pasted blocks; one
      named liveness window; the readback claim now carries its sleep term and n.
  • A further adversarial pass on that response:

    • the table had dropped the worst 8822B cell (+53419 µs);
    • the "two units" claim had no log;
    • the timer-off arm could never see a take;
    • silent EP0 retries could stretch an arm past both the take window and the
      wrap guard;
    • the USB pair can tear across a low-word carry;
    • a readback that throws broke the bool contract.

    All six are fixed above.

API/surface breaks (intentional)

  • mt7612u_write_tsf removed from the public header, its definition, and
    the api_link check. This is the hard break: it is a C symbol, so any
    out-of-tree user fails to link. api_link is built only by
    src/mt7612u/Makefile; CI does not build it.
  • IRadio::WriteTsf: voidbool. Return types are not part of the
    Itanium mangled name, so this breaks source compatibility for out-of-tree
    callers, not the ABI.
  • AdapterCaps::tsf_write_ok added, and adapter.caps gains tsf_write.
    An AdapterCaps layout change breaks ABI for anyone compiled against the old
    struct, as every new caps flag does.

There are no in-tree callers of WriteTsf or mt7612u_write_tsf.

Test plan

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DDEVOURER_MT7612U=ON
cmake --build build -j
ctest --test-dir build                    # 63/63, radio_iface incl. tsf_write_ok default
make -C src/mt7612u check                 # api_link (30), frame_shape, field_macros, log_sink
# hardware (firmware from linux-firmware: mt7662_rom_patch.bin, mt7662.bin)
make -C src/mt7612u bringup
MT7612U_DEV=<bus-port> src/mt7612u/bringup tsfwrite 6     # expect GATE TSF-WRITE: PASS
MT7612U_DEV=<bus-port> src/mt7612u/bringup tsfwrite 149   # 5 GHz, same

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Report TSF write support and reject MT7612U no-op writes

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Make TSF writes report backend support instead of silently succeeding.
• Disable ineffective MT7612U register writes and remove their standalone C API.
• Add hardware characterization, contract tests, and backend-specific TSF documentation.
Diagram

classDiagram
class IRadio {
  +WriteTsf(tsf) bool
}
class Jaguar1 {
  +WriteTsf() false
}
class Jaguar2 {
  +WriteTsf() true
}
class Jaguar3 {
  +WriteTsf() true
}
class MT7612U {
  +WriteTsf() false
}
class DefaultBackends {
  +WriteTsf() false
}
class TsfWriteGate {
  +testClock()
  +sweepWrites()
}
IRadio <|-- Jaguar1
IRadio <|-- Jaguar2
IRadio <|-- Jaguar3
IRadio <|-- MT7612U
IRadio <|-- DefaultBackends
TsfWriteGate ..> MT7612U : verifies no load
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Separate capability query
  • ➕ Could preserve the existing void WriteTsf signature.
  • ➕ Makes support discoverable before attempting a write.
  • ➖ Requires callers to coordinate two operations instead of inspecting one result.
  • ➖ Preserves source compatibility with callers that currently assume silent success.
  • ➖ Cannot naturally report an attempted operation's outcome.
2. Return a status enum
  • ➕ Could distinguish unsupported hardware, transport failure, and accepted writes.
  • ➕ Provides room for future backend-specific failure states.
  • ➖ Adds complexity unsupported by the current binary backend behavior.
  • ➖ Would require backends to expose transport outcomes they do not currently report.
  • ➖ Creates a larger API migration for limited immediate benefit.

Recommendation: The boolean return is the best fit for the current contract because callers need one binary answer: whether this backend issued a meaningful standalone TSF write. The intentional compile-time break prevents silent assumptions, while a status enum should be considered later only if implementations can reliably expose transport or verification failures.

Files changed (16) +208 / -26

Enhancement (4) +12 / -6
RtlJaguar2Device.cppReport successful Jaguar2 TSF register writes +5/-2

Report successful Jaguar2 TSF register writes

• Updates the implementation to return true after writing the low and high TSF registers under the existing lock. Notes the bench-verified RTL8822B readback behavior.

src/jaguar2/RtlJaguar2Device.cpp

RtlJaguar2Device.hAdopt the boolean WriteTsf contract for Jaguar2 +1/-1

Adopt the boolean WriteTsf contract for Jaguar2

• Changes the Jaguar2 override declaration to return a boolean result.

src/jaguar2/RtlJaguar2Device.h

RtlJaguar3Device.cppReport successful Jaguar3 TSF register writes +5/-2

Report successful Jaguar3 TSF register writes

• Returns true after the serialized TSF register-pair write. Documents the bench-verified RTL8822C readback result.

src/jaguar3/RtlJaguar3Device.cpp

RtlJaguar3Device.hAdopt the boolean WriteTsf contract for Jaguar3 +1/-1

Adopt the boolean WriteTsf contract for Jaguar3

• Changes the Jaguar3 override declaration to return a boolean result.

src/jaguar3/RtlJaguar3Device.h

Bug fix (6) +30 / -15
IRadio.hChange WriteTsf to report whether a write is supported +13/-2

Change WriteTsf to report whether a write is supported

• Changes the shared virtual method from 'void' to 'bool' and makes the default return false. Expands the contract to distinguish accepted standalone writes from unsupported or unimplemented paths.

src/IRadio.h

RtlJaguarDevice.hExplicitly reject standalone Jaguar1 TSF writes +6/-0

Explicitly reject standalone Jaguar1 TSF writes

• Adds a false-returning override because Jaguar1 only moves TSF through its complete beacon-steering sequence. This prevents that behavior from being confused with a standalone write.

src/jaguar1/RtlJaguarDevice.h

Mt7612uRadio.cppReject ineffective MT7612U TSF writes +8/-4

Reject ineffective MT7612U TSF writes

• Replaces the register-writing implementation with an unconditional false result. The backend no longer touches DW0/DW1 because hardware characterization shows they cannot load the counter.

src/mt7612u/Mt7612uRadio.cpp

Mt7612uRadio.hAdopt the boolean WriteTsf contract for MT7612U +1/-1

Adopt the boolean WriteTsf contract for MT7612U

• Changes the MT7612U override declaration to return whether a TSF write was accepted.

src/mt7612u/Mt7612uRadio.h

caps.cppRemove the misleading MT7612U TSF writer +0/-6

Remove the misleading MT7612U TSF writer

• Removes the helper that wrote DW0/DW1 despite those registers not loading the hardware counter.

src/mt7612u/caps.cpp

mt7612u.hRemove the unsupported MT7612U write API +2/-2

Remove the unsupported MT7612U write API

• Deletes the public 'mt7612u_write_tsf' declaration and documents that only TSF reads are supported. This is an intentional public C API break.

src/mt7612u/include/mt7612u/mt7612u.h

Tests (3) +144 / -2
api_link.cRemove the deleted TSF writer from API linkage checks +0/-1

Remove the deleted TSF writer from API linkage checks

• Stops requiring the removed 'mt7612u_write_tsf' symbol in the MT7612U public API linkage test.

src/mt7612u/tests/api_link.c

bringup.cppAdd an MT7612U TSF write characterization gate +143/-1

Add an MT7612U TSF write characterization gate

• Adds 'bringup tsfwrite', which verifies the TSF clock is alive and tests both word orders, individual words, stopped-MAC writes, and stopped-timer writes. The gate fails when a write takes, setup fails, or USB errors make the conclusion unreliable.

src/mt7612u/tools/bringup.cpp

radio_iface_selftest.cppPin the default WriteTsf result to false +1/-0

Pin the default WriteTsf result to false

• Adds a headless interface assertion confirming unsupported radios reject TSF writes through the default implementation.

tests/radio_iface_selftest.cpp

Documentation (3) +22 / -3
mt7612u.mdDocument the missing MT7612U TSF load path +11/-0

Document the missing MT7612U TSF load path

• Records that DW0/DW1 are free-running counter registers rather than load registers. Describes the measured write sequences, positive clock control, and expected 'tsfwrite' gate result.

docs/mt7612u.md

time-distribution.mdDefine backend-specific standalone TSF write semantics +10/-3

Define backend-specific standalone TSF write semantics

• Documents which radio generations return true or false from 'WriteTsf' and why. Clarifies that TSF writes and beacon TBTT steering remain separate operations.

docs/time-distribution.md

README.mdList the TSF write characterization command +1/-0

List the TSF write characterization command

• Adds 'tsfwrite' to the MT7612U bring-up command summary.

src/mt7612u/README.md

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 14, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Failed writes still report success ✓ Resolved 📘 Rule violation ≡ Correctness
Description
RtlJaguar2Device::WriteTsf and RtlJaguar3Device::WriteTsf discard both boolean results from
rtw_write() and unconditionally return true. When either low- or high-word USB or memory-mapped
transfer is short or fails, callers are told the write was accepted even though the clock may be
unchanged or only half-written.
Code

src/jaguar2/RtlJaguar2Device.cpp[1971]

+  return true;
Evidence
Both changed implementations ignore the two register-write results and return true, even though
the new interface defines that value as an accepted TSF write. RtlAdapter::rtw_write() forwards
the underlying transport status, USB returns false for short or failed control transfers, and PCIe
forwards guarded-write failures, demonstrating that these are meaningful failure signals currently
masked by the API.

CLAUDE.md: Represent Unsupported Backend Features Honestly: CLAUDE.md: Represent Unsupported Backend Features Honestly: CLAUDE.md: Represent Unsupported Backend Features Honestly: CLAUDE.md: Represent Unsupported Backend Features Honestly: CLAUDE.md: Represent Unsupported Backend Features Honestly: CLAUDE.md: Represent Unsupported Backend Features Honestly: CLAUDE.md: Represent Unsupported Backend Features Honestly: CLAUDE.md: Represent Unsupported Backend Features Honestly: CLAUDE.md: Represent Unsupported Backend Features Honestly: CLAUDE.md: Represent Unsupported Backend Features Honestly: CLAUDE.md: Represent Unsupported Backend Features Honestly: CLAUDE.md: Represent Unsupported Backend Features Honestly: CLAUDE.md: Represent Unsupported Backend Features Honestly: CLAUDE.md: Represent Unsupported Backend Features Honestly: CLAUDE.md: Represent Unsupported Backend Features Honestly: CLAUDE.md: Represent Unsupported Backend Features Honestly
src/jaguar2/RtlJaguar2Device.cpp[1963-1971]
src/jaguar3/RtlJaguar3Device.cpp[2216-2224]
src/RtlAdapter.h[180-195]
src/UsbTransport.h[171-174]
src/IRadio.h[372-382]
src/UsbTransport.h[47-49]
src/UsbTransport.h[171-175]
src/UsbTransport.h[44-58]
src/PcieTransport.h[84-98]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Jaguar2 and Jaguar3 TSF writers return success regardless of whether either register transfer succeeds, masking failed or short transfers and contradicting the new success-reporting contract.
## Fix Focus Areas
- src/jaguar2/RtlJaguar2Device.cpp[1963-1971]
- src/jaguar3/RtlJaguar3Device.cpp[2216-2224]
## Recommended Fix
Capture the boolean result of each low- and high-word `rtw_write()` call, preserve execution of both writes so the attempted register sequence remains unchanged, and return `true` only when both transfers succeed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. A failed read can corrupt beacon state ✓ Resolved 🐞 Bug ☼ Reliability
Description
tsf_variant reads MT_BEACON_TIME_CFG with unchecked mt_rr and, for the timer-off arm, uses the
0xffffffff failure sentinel as configuration to disable and then restore. If that read fails while
subsequent writes succeed, the diagnostic writes 0xfffeffff and then 0xffffffff into the beacon
timer register before its final error check, leaving the adapter misconfigured.
Code

src/mt7612u/tools/bringup.cpp[R2897-2903]

+	uint32_t cfg = mt_rr(&dev, MT_BEACON_TIME_CFG);
+	uint64_t r1, r2;
+	int64_t err, rate;
+	bool took;
+
+	if (stop_timer)
+		mt_wr(&dev, MT_BEACON_TIME_CFG, cfg & ~MT_BEACON_TIME_CFG_TIMER_EN);
Evidence
The convenience reader explicitly returns all ones after a failed transfer, and the repository warns
that writing this sentinel back during a read-modify-write is destructive. The new gate nevertheless
masks and restores that value before checking the accumulated error count, while teardown performs
no corrective register reset.

src/mt7612u/tools/bringup.cpp[2894-2920]
src/mt7612u/tools/bringup.cpp[2990-2997]
src/mt7612u/usb.cpp[151-180]
src/mt7612u/usb.cpp[199-220]
src/mt7612u/usb.cpp[731-768]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The timer-off characterization arm treats a failed configuration read as a valid value and writes the failure sentinel back into hardware.
## Fix Focus Areas
- src/mt7612u/tools/bringup.cpp[2894-2904]
- src/mt7612u/tools/bringup.cpp[2919-2920]
## Recommended Fix
For the timer-off arm, read `MT_BEACON_TIME_CFG` with `mt_rr_chk` and skip all timer configuration writes when it fails. Preserve the accumulated I/O error so the existing sequence-boundary check returns failure.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Failed tests leave radio transmit enabled ✓ Resolved 🐞 Bug ☼ Reliability
Description
gate_tsfwrite returns directly when mt_mac_start fails instead of invoking mt_mac_stop.
mt_mac_start enables transmitter control before its DMA-idle poll and returns on that poll failure
without clearing it, while the eventual mt_close only releases USB resources and does not perform
register teardown.
Code

src/mt7612u/tools/bringup.cpp[R2970-2973]

+	if (mt_mac_start(&dev, MT_RX_DRAIN_NONE)) {
+		printf("\nGATE TSF-WRITE: FAIL - mt_mac_start failed\n");
+		return 1;
+	}
Evidence
The added early return bypasses the gate's normal mt_mac_stop call. A start failure can occur only
after the start routine has written the transmitter-enable bit, and neither that failure branch nor
the final close routine clears the MAC register state.

src/mt7612u/tools/bringup.cpp[2970-2988]
src/mt7612u/init.cpp[283-289]
src/mt7612u/init.cpp[318-346]
src/mt7612u/tools/bringup.cpp[3149-3151]
src/mt7612u/usb.cpp[731-767]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
Issue description
The TSF-write diagnostic returns after a failed MAC start without restoring the MAC state that the start operation may already have changed.
Fix Focus Areas
- src/mt7612u/tools/bringup.cpp[2970-2973]
Recommended Fix
Call `mt_mac_stop(&dev)` before returning from the `mt_mac_start` failure branch, or route this branch through a common cleanup path that always stops the MAC after hardware initialization. Keep the existing failure result and message.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can enable the Remediation agent and Qodo fixes findings in a dedicated fix PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/jaguar2/RtlJaguar2Device.cpp Outdated
Comment thread src/mt7612u/tools/bringup.cpp Outdated
Comment thread src/mt7612u/tools/bringup.cpp

@josephnef josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direction is right: an honest bool beats a silent no-op, and the MT7612U evidence (positive control + every load sequence, both bands) is solid. I built the head in a clean worktree and independently confirmed the Realtek true path on this bench: RTL8822B took=true err +50443 µs and RTL8822C took=true err +50274 µs after a +5 s write and a 50 ms sleep. radio_iface selftest passes; make -C src/mt7612u bringup and make check are clean. No MT7612U here, so the tsfwrite gate itself is not re-run.

Two blockers, both inline: the Jaguar1 false is measured wrong on an RTL8821AU (the bare REG_TSFTR write takes, both word orders, no beacon armed), and the new "true means both transfers landed" promise is false over PCIe. The gate findings and the contract note are non-blocking but worth folding in while you are there.

Nits not anchored inline:

  • tsf_variant takes an untyped which 0/1/2 plus a high_word_first bool that is dead unless which == 0, and gate_tsfwrite repeats the four-line read/arm/accumulate block six times. An enum for the four write orders plus a small run_arm() helper removes the unreachable combination and the copy-paste.
  • The liveness window (> 10000 && < 200000 over 50 ms) is written twice, in tsf_clock_alive and at the tail of tsf_variant, as bare literals. One helper plus a named window pair keeps the control and the per-arm check from ever disagreeing.
  • "Target + the control round trip" (IRadio.h, time-distribution.md, the J2 comment) drops the sleep term. The recorded J2 datum is +53 ms including the sleep, n=1, 8822B only, 8821C unmeasured. Quote the number or scope the claim ("moves to ~target; magnitude not characterised") — the same rule the PR body already applies to the MT7612U evidence.

Probe used for the measurements (a second RtlAdapter on the claimed handle after InitWrite, raw 0x0560/0x0564 writes, target = fresh read + 5 s, readback after 20 ms, liveness re-read 50 ms later) is a scratch tool; happy to share it if you want to repeat on an 8812AU.

Comment thread src/jaguar1/RtlJaguarDevice.h Outdated
Comment thread docs/time-distribution.md Outdated
Comment thread src/jaguar2/RtlJaguar2Device.cpp Outdated
Comment thread src/IRadio.h Outdated
Comment thread src/mt7612u/tools/bringup.cpp Outdated
Comment thread src/mt7612u/tools/bringup.cpp Outdated
Comment thread src/mt7612u/Mt7612uRadio.cpp Outdated
@snokvist

Copy link
Copy Markdown
Collaborator Author

Thanks for the independent Realtek numbers and the 8821AU cells; that measurement changed the design. I've pushed 45cf642 (one commit on top of 9280d29), with per-thread replies inline. Summary:

  • Blocker 1 (J1 false): J1 writes the bare pair under _port0_mu, and tsf_write_ok = true.
  • Blocker 2 (PCIe): the shared devourer::write_tsftr reads back on PCIe (and on USB near a low-word wrap, where the pair can tear).
  • Contract: AdapterCaps::tsf_write_ok is the static answer, and the bool is the per-call transport result.
  • Gate: checked reads, a wrap guard, host-time bounds per arm, and the override dropped.

The unanchored nits:

  • tsf_variant(which, high_word_first) is replaced by enum tsf_write_order (DW0→DW1, DW1→DW0, DW0 only, DW1 only), a struct tsf_arm table, and tsf_arm_run()/tsf_arms_run(). The six copy-pasted blocks and the unreachable combination are gone.
  • One liveness predicate, tsf_live(), over the named kTsfLiveSleepUs/kTsfLiveMinUs/kTsfLiveMaxUs, shared by the control and every arm. The take tolerance is its own kTsfTakeWindowUs.
  • The sleep term: time-distribution.md now has the full readback table (above). AdapterCaps.h and the J1/J2/J3 comments say which die each claim was measured on and name the unmeasured siblings. IRadio.h points at the table instead of restating a number.

Two things came out of a further adversarial pass, beyond your list:

  • The timer-off arm could never have seen a take. Restoring TIMER_EN restarts the counter from ~0, which wipes any load before the normal readback. The arm now also reads the counter while the timer is still clear: it reads 0, not the target.
  • The "two units" claim in the docs had no posted log. Both units now have one, below.

Verification:

tsfw_probe (scratch, this PR's WriteTsf) on RTL8812BU ch36  # 40/40 ret=true, took, clock live
cmake --build build && ctest --test-dir build      # 63/63 (radio_iface incl. tsf_write_ok default)
make -C src/mt7612u bringup check                  # api_link 30 resolved; frame_shape, field_macros, log_sink PASS
MT7612U_DEV=8-1 src/mt7612u/bringup tsfwrite 6|149 # PASS, PASS  (40:a5:ef:50:27:a1)
MT7612U_DEV=5-1 src/mt7612u/bringup tsfwrite 6|149 # PASS, PASS  (40:a5:ef:5a:32:f8)

8-1, channel 149 (raw, only the library [I] log lines removed):

MT_BEACON_TIME_CFG=0x00070640 TIMER_EN=1 TBTT_EN=0 BEACON_TX=0 SYNC_MODE=3
  clock control                      t0=430488 t1=480952 delta=+50464  ALIVE
  MAC on, both words DW0,DW1         target=5481626 read=502027 err=-4979599 delta50ms=+50341  no-op
  MAC on, both words DW1,DW0         target=5552601 read=573019 err=-4979582 delta50ms=+50298  no-op
  MAC on, high word (DW1) only       target=4295590758 read=643763 err=-4294946995 delta50ms=+50286  no-op
  MAC on, low word (DW0) only        target=5694281 read=714614 err=-4979667 delta50ms=+50422  no-op
  MAC off, both words                target=5778837 read=799192 err=-4979645 delta50ms=+50378  no-op
                                     held=0 (read with TIMER_EN clear)
  MAC off, timer off, both words     target=5849794 read=20232 err=-5829562 delta50ms=+50366  no-op

GATE TSF-WRITE: PASS - confirmed: no sequence loads the TSF, so tsf_write_ok is false

5-1, channel 6:

MT_BEACON_TIME_CFG=0x00070640 TIMER_EN=1 TBTT_EN=0 BEACON_TX=0 SYNC_MODE=3
  clock control                      t0=461407 t1=534331 delta=+72924  ALIVE
  MAC on, both words DW0,DW1         target=5536863 read=568734 err=-4968129 delta50ms=+61118  no-op
  MAC on, both words DW1,DW0         target=5630989 read=664736 err=-4966253 delta50ms=+73300  no-op
  MAC on, high word (DW1) only       target=4295706397 read=781481 err=-4294924916 delta50ms=+61453  no-op
  MAC on, low word (DW0) only        target=5854712 read=886989 err=-4967723 delta50ms=+61098  no-op
  MAC off, both words                target=5968220 read=1000464 err=-4967756 delta50ms=+62143  no-op
                                     held=0 (read with TIMER_EN clear)
  MAC off, timer off, both words     target=6063836 read=31058 err=-6032778 delta50ms=+73317  no-op

GATE TSF-WRITE: PASS - confirmed: no sequence loads the TSF, so tsf_write_ok is false

Not re-measured this round:

  • Jaguar1 hardware: none on this bench. The J1 true rests on your raw-pair cells.
  • Jaguar2: only the USB path, via the RTL8812BU run (this PR's helper, 40/40; numbers in the time-distribution.md thread). The 8821C and the 8821CE are not measured.
  • The J3 refactor on the RTL8812CU: the unit was held by another application, so it is build-verified only. The register operations are unchanged; they moved into the helper.
  • The PCIe readback: no 8821CE here.
  • The gate's wrap-guard, time-bound and read-failure branches: none triggered on hardware.

If an 8812AU comes back to the bench I'd take you up on the probe.

josephnef
josephnef previously approved these changes Sep 17, 2026

@josephnef josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both blockers are fixed and the round-2 head is measured here. I built 45cf642 in a clean worktree (63/63 ctest, make -C src/mt7612u bringup + make check clean) and ran the PR's own WriteTsf — the override, not the raw pair — on every Realtek die on this bench: 5 forward (+5 s) and 5 backward (−5 s) writes each, 20 ms settle, host clock bracketing every call, 50 ms liveness re-read after each readback.

die tsf_write_ok took / landed / bool agrees readback error − host time (WriteTsf return → ReadTsf return) 50 ms re-read
RTL8821AU (J1, Archer T2U Plus 2357:0120, ch36, no beacon) true 10 / 10 / 10 −300 … −15 µs +51.3 … +51.8 ms
RTL8822B (J2, 0bda:b812) true 10 / 10 / 10 −197 … +44 µs +50.3 … +50.8 ms
RTL8812BU (J2, 8822B die, 2357:012d) true 10 / 10 / 10 −226 … −8 µs +51.3 … +51.6 ms
RTL8822C (J3, 0bda:c812) true 10 / 10 / 10 −334 … +211 µs +50.7 … +51.1 ms

So the Jaguar1 override does load the counter through the library path, both directions, and the "override itself has not run on Jaguar1 hardware" caveat can come out (it is currently stated in three places — see the AdapterCaps.h comment). Same scope as before: one 8821AU unit; the 8812AU/8814AU were not on the bench. The PCIe readback and the USB near-wrap readback branches are not hardware-exercised here either.

The inline notes are non-blocking: one real race worth a one-line fix (J1 ReadTsf unlocked against the new locked pair write), one place where the new TBTT statement contradicts the same file's 8814A note, the roster duplication, and the false wording. Two smaller ones not anchored to a diff line:

  • struct mt7612u_caps (src/mt7612u/include/mt7612u/mt7612u.h) already carries negative bits (narrowband, fast_retune) so a C consumer need not assume; with mt7612u_write_tsf deleted, a tsf_write : 1 bit there would make the C ABI say what AdapterCaps::tsf_write_ok says instead of leaving a link error as the only signal.
  • Follow-up, not this PR: PinBeaconTbtt / AdjustBeaconTimingFine on all three Jaguar backends still hand-roll the same 0x0560/0x0564 pair (AdjustBeaconTimingFine without the wrap retry, and every steer write discards the bool). They all already hold the lock RtlTsf.h prescribes, so read_tsftr/write_tsftr are drop-in there too.

Comment thread src/jaguar1/RtlJaguarDevice.cpp
Comment thread src/jaguar1/RtlJaguarDevice.h
Comment thread src/AdapterCaps.h
Comment thread src/IRadio.h
snokvist and others added 4 commits September 17, 2026 16:09
IRadio::WriteTsf was a void method whose default was a silent no-op, so a
caller could not tell a chip that moved its TSF from one that ignored the
write. The MT7612U was exactly that case: we wrote MT_TSF_TIMER_DW0/DW1
and returned, while the registers hold a free-running counter that does
not load.

Swept on hardware (two MT7612U, ch6 and 5 GHz, firmware running,
MT_BEACON_TIME_CFG TIMER_EN set): twelve writes across six sequences -
both words in each order with the MAC running, each word alone, and the
MAC- and timer-stopped variants - every one ignored, with the clock still
advancing at wall rate and a positive control proving it was alive. No
sequence loads the TSF on this part, so there is no sequence to fix.

Make WriteTsf report whether the write is real. The default returns false
(the RTL8733B and Kestrel have no TSF write either); the RTL8822C
(jaguar3, readback-verified) and RTL8822B return true; the RTL8812AU
moves its TSF only as part of the beacon-steer sequence, not a standalone
write, so it returns false explicitly rather than by inheritance; and the
MT7612U returns false without touching a register. A part whose registers
exist but do not load and a part with no path at all both mean "no
standalone write here", which is what false now says.

`bringup tsfwrite [chan]` is the silicon-characterization cell: it
PASSes only while every load sequence is confirmed ignored and the clock
is confirmed alive, so a future firmware that enables loading trips it.
docs/mt7612u.md and docs/time-distribution.md record the absence and the
per-backend answers.

API note for reviewers: this changes the public IRadio signature from
void to bool and removes the standalone C symbol mt7612u_write_tsf
(declaration, definition and its api_link presence check). There are no
in-tree callers; out-of-tree callers must adapt, which is the intent - a
silent no-op is worse than a compile error.
The WriteTsf comment and docs/time-distribution.md hedged Jaguar2's true as
inferred from Jaguar3's register pair. It is now measured on the bench:
RTL8822B, write of target 6230820 us read back 6284239 (the target plus the
110 us control round trip and the sleep between reads), took true. Say so;
no behaviour change.
Review follow-ups on the WriteTsf contract change:

- Jaguar2/Jaguar3 now return the AND of the two rtw_write results. Both
  words are still written even if the first transfer fails (the pair is
  the unit; a half-updated counter is not something to leave behind), but
  a failed transfer is no longer dressed as success.
- The mt7612u tsfwrite gate reads MT_BEACON_TIME_CFG with mt_rr_chk and
  skips the timer-off arm when the read fails, instead of writing the
  0xffffffff failure sentinel back as configuration; every arm now also
  requires the clock to still be advancing at the wall rate, so a counter
  that stalls during the sweep cannot be read as "ignored" and PASS.
- mt_mac_stop runs before the mt_mac_start failure return, and the gate's
  own config read has a failure verdict.
- Jaguar1's false override says why: the standalone method deliberately
  does not perform the beacon-steer sequence's EN_BCN_FUNCTION toggle and
  TBTT re-download, which move the beacon grid rather than set the clock.
- docs: "no tested sequence loads the TSF" rather than an absolute, with
  the mainline mt76 absence of a mt76x02 .set_tsf beside it; the IRadio
  comment states the contract and points at time-distribution.md for the
  per-backend state instead of enumerating them; api_link's entry count
  corrected to 30.

Re-verified on hardware both bands: ch6 and ch149 each pass with the
clock control ALIVE and all six arms no-op; ctest 63/63; make -C
src/mt7612u check passes.
Review round from the maintainer:

- Jaguar1 implements WriteTsf. The bare REG_TSFTR pair was measured to
  load on an RTL8821AU (raw pair, both word orders, no beacon armed), so
  the explicit false was a fake in the other direction. Under _port0_mu
  so it cannot interleave with a beacon-steer sequence.
- The REG_TSFTR read/write moves into src/RtlTsf.h, shared by J1/J2/J3.
  A PCIe register write is a posted MMIO store that always reports
  success, so write_tsftr reads the counter back there; on USB it also
  reads back when the target's low word is close enough to wrap that the
  pair could tear between the two transfers. A throwing readback is false.
- AdapterCaps::tsf_write_ok is the static half of the contract (emitted
  as adapter.caps tsf_write); the WriteTsf bool is only the per-call
  transport result.
- The MT7612U drops its WriteTsf override (identical to the default) and
  records the measurement on tsf_write_ok = false instead.
- The readback claim becomes a table of every recorded cell, with the
  sleep term, the source of each row, and the unmeasured dies named.
  The TBTT notes are scoped to J2/J3, with the J1 caveat.
- bringup tsfwrite: every TSF read is checked (a failed read is a
  transport fault, not a dead clock); each arm waits out a low-word wrap
  before taking its base; an arm slower than its host-time bounds is
  inconclusive rather than "ignored"; the timer-off arm reads the counter
  while TIMER_EN is still clear; the arms are a table over a write-order
  enum sharing one named liveness window. PASS on two units, both bands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@josephnef

Copy link
Copy Markdown
Collaborator

Rebased onto master (cb98f10) to clear the conflict with #432 in src/mt7612u/Mt7612uRadio.cpp: master added GetChannelBusy next to the WriteTsf override this PR deletes. Resolution keeps GetChannelBusy and drops the override, as the PR intends; every other file's delta is byte-identical to 45cf642 (diff-of-diffs, only hunk offsets moved). Rebased head 0ecd680 builds with DEVOURER_MT7612U=ON, 66/66 ctest, make -C src/mt7612u check clean. Merging once CI is green.

@josephnef
josephnef merged commit bb64508 into OpenIPC:master Sep 17, 2026
23 checks passed
@josephnef

Copy link
Copy Markdown
Collaborator

Follow-ups from the review are tracked in #434.

josephnef pushed a commit that referenced this pull request Sep 18, 2026
…dly (#436)

> Standalone follow-up to #430, and #434 item 5 in full: the checked,
wrap-safe
> library `ReadTsf`, plus the `tsf_write` bit in the C caps. #434 items
1–4
> (Jaguar1 `ReadTsf` lock, the 8814A TBTT note, the roster copies, the
> `WriteTsf` false wording) are Realtek-side and are not touched here.

`mt7612u_read_tsf` read `MT_TSF_TIMER_DW0` then `DW1` with the unchecked
`mt_rr`
and 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 `0xffffffff` into a plausible wrong clock, and
`Mt7612uRadio::ReadTsf`
passed 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,
judges
every read against a least-squares host-clock model fitted over the
preceding
minute, not against the read under test. The gate does three things:

1. **Forced read.** Around the predicted wrap it drives the library's
read
template through a reader that sleeps to a schedule, so the wrap lands
in a
   chosen gap of the read.
2. **Positive control.** It interleaves a plain DW0,DW1 read across the
same
   wrap.
3. **Continuous check.** It checks every continuous
`mt7612u_read_tsf_chk` read
   near 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 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
|

Both runs `PASS`. The second unit is 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:
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.**
- **The exported function's own retry never ran on hardware.** The
forced read
occupies the wrap instant, so `mt7612u_read_tsf_chk` never takes its
retry
across 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.
- **n is one wrap per unit.**
- **The smoke mode cannot tear a read.** `wrap_bits` < 32 reports
`SMOKE`,
  never `PASS`.

**Failure path, checked once by hand, not a gate.** Under a 100 Hz
`ReadTsf`
poll I de-authorised the adapter's whole bus (`usbN/authorized`, a
logical
disconnect, 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 this
part: 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.h`** is 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.
  - Any failed access fails the read and leaves `*out` untouched.
- `0xffffffff` is a legitimate word, which is why a return code carries
    failure rather than a sentinel value.
- The approach matches `read_tsftr` in `src/RtlTsf.h`, which re-reads
both
    words instead.
- **`mt7612u_read_tsf_chk(dev, &out)`** is a new C entry point returning
0 or
  -1. `mt7612u_read_tsf` uses the same read and returns 0 on failure.
- **`Mt7612uRadio::ReadTsf`** throws `std::ios_base::failure` on a
failed read,
as the Realtek USB backends already do. The `IRadio::ReadTsf` note now
says
that for USB, keeps the PCIe hedge `RtlTsf.h` carries, and names the
RTL8733B
  as returning 0.
- **`struct mt7612u_caps`** gains `tsf_write : 1` (0 here), so the C ABI
says
  what `AdapterCaps::tsf_write_ok` says.
- **`bringup`**:
- The `tsfwrite` gate calls the library reader, so there is one copy of
the
    read discipline instead of two.
- The beacon gate fails on any failed TSF read. Before, a failed read
cleared
only the chain, so a failing transport could still report a live timer.
  - The `caps` gate prints no word-order verdict from failed raw reads.
- **`timesync`, `tdma`, `chanmig`**: the two timing demos skip that
marker
rather than stamp a wrong time; `chanmig` stamps its existing "no TSF" 0
and
  warns once.
- **`api_link`** gains `mt7612u_read_tsf_chk` plus the two public
functions it
was already missing, `mt7612u_ch_time` and `mt7612u_phy_tick`. All 33
now
resolve. 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.sh`** wraps the gate so the invocation, the
~72 min
per wrap, one gap per run, one adapter being enough, and the re-run rule
are
not folk knowledge. `SMOKE=1` checks the schedule, the model and the
plumbing
  in ~2.5 min against a 16.7 s carry, and reports `SMOKE`, never `PASS`.

## Caller-visible behaviour changes

- **`Mt7612uRadio::ReadTsf` throws** on a failed read, where it returned
a
garbage 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_tsf` returns 0 on failure** instead of a value built
from
  `0xffffffff` words.
- **Each TSF read costs 3 control transfers (4 across a wrap)** instead
of 2.
  `ReadTsf` is caller-cadence, never on the send path.
- **The `timesync` and `tdma` masters skip a marker** when the TSF read
fails,
  and each says so once rather than at marker rate.
- **`struct mt7612u_caps` gains `tsf_write`.** It lands in spare padding
of an
  existing bitfield, so `sizeof` is unchanged (32 B on x86-64; no 32-bit
toolchain here to check that layout), and `mt7612u_get_caps` zeroes the
struct
first, 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 tsfwrap` exit 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 `ReadTsf` callers 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- or
PCIe-specific bench tools. Their behaviour is unchanged by this PR:
those
backends already throw.

## Headless coverage

`mt7612u_tsf_api` (ctest, needs `DEVOURER_MT7612U`, which CI's MediaTek
jobs
set) pins the C entry points' failure contract: `_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 `tsf_write`. `0xffffffff`
is a
legitimate 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::ReadTsf` throw, need the part.

`mt7612u_tsf_read` (ctest, runs with `DEVOURER_MT7612U=OFF` too) sweeps
a
scripted 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.md` says
so); the
wiring is what the gate covers.

## Test plan

```sh
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DDEVOURER_MT7612U=ON
cmake --build build -j
ctest --test-dir build                        # 68/68, mt7612u_tsf_read + mt7612u_tsf_api
make -C src/mt7612u check                     # api_link: 33 resolved
# hardware (firmware from linux-firmware), ~72 min per run
SMOKE=1 MT7612U_DEV=<bus-port> tests/mt7612u_tsf_wrap.sh    # ~2.5 min, SMOKE
MT7612U_DEV=<bus-port> tests/mt7612u_tsf_wrap.sh            # both gaps, ~2.4 h
DEVS="<port-a> <port-b>" tests/mt7612u_tsf_wrap.sh          # one gap each, ~72 min
MT7612U_DEV=<bus-port> src/mt7612u/bringup tsfwrite 6       # still PASS through the library reader
MT7612U_DEV=<bus-port> src/mt7612u/bringup caps 149         # tsf_write=0, checked word-order read
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)


https://claude.ai/code/session_01JP51Yp3WSbfiMDuDHJByHW

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants