Skip to content

Qualify the receiver before believing a delivery number (#348 post-mortem; corrects two #349 measurements) - #351

Merged
josephnef merged 8 commits into
masterfrom
jaguar1-latched-state-investigation
Jul 28, 2026
Merged

Qualify the receiver before believing a delivery number (#348 post-mortem; corrects two #349 measurements)#351
josephnef merged 8 commits into
masterfrom
jaguar1-latched-state-investigation

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

Closes #348 (as unreproducible — see below). Corrects two measurements published
in #349.

What #348 turned out to be

The reported bug — an RTL8812AU whose 64-QAM rates die across sessions while
robust rates stay perfect, recovering only on a power cycle — was a
measurement artifact
. The ground station was a TP-Link Archer T3U, an
RTL8822BU with internal antennas, whose modulation cliff sits inside the
rates under test.

Two RTL8822BU adapters. Identical silicon, identical code path, same
transmitter, same channel, minutes apart. Only the antennas differ:

ground station MCS1 MCS3 MCS4 MCS5 MCS6 MCS7
Archer T3U — internal 98.1 97.8 98.3 79.1 49.0 2.9
Comfast CF-924AC V2 — two external 95.8 95.4 95.8 95.7 94.4 95.9

A receiver on its cliff swings between fine and zero on a few dB of ambient
while the rates below stay pinned — indistinguishable from a transmitter that
degrades and recovers. In one session that same T3U gave MCS7 68%, later 0.6%;
the cliff never moved, the ambient did.

Four successive root causes were wrong, all from this one instrument:
latched chip state (refuted by reading the registers — the BB swing is rewritten
every init), thermal (refuted by an off-time sweep varying only cooling time,
and by a within-session probe where the meter is pinned while delivery drifts),
a band-switching TX defect (a real 32-point collapse over the devourer link —
but an AR9271 sniffer showed the same stressor moving decoded MCS7 from 3721 to
3915, i.e. not at all), and a degraded adapter (the vendor driver failed
identically, which a link-budget shortfall predicts just as well).

Corrections to #349

Both were measured through the T3U and are wrong:

  • "2SS above MCS2 reads zero — near-field spatial decorrelation, rig not
    chip"
    → wrong. On a qualified receiver: VHT2SS MCS0 95.3%, MCS4
    90.8%, MCS7 15.5%, all modulation-verified. That was the receiver's cliff.
  • "VHT1SS_MCS8 (256-QAM) delivered 295/4675" → understated 12×. Actually
    77.1%, 37/37 frames decoded as vht1ss_mcs8.

So the 256-QAM-on-2.4 GHz result is materially stronger than published and the
2SS ceiling is MCS7, not MCS2.

What did not change: DEVOURER_BW=40 still delivers zero on a qualified
receiver, including the most robust 40 MHz rate. That gap survives the better
instrument, so it is real and now properly evidenced rather than merely repeated.

The guard, so this cannot recur

tests/ground_station_qualify.sh sweeps the ladder on a candidate ground
station and refuses the pairing (exit 1) when the test rate is off the flat
part. Self-tested against the T3U: NOT QUALIFIED for MCS7/20, 2.8% vs 98.1%
best on its own ladder.

Plus the harnesses each wrong answer needed to kill it:

script question it answers
probe_repeatability.sh how big must an effect be to be real? (sd 1.8, 5.7-pt spread → <4 pts is not a finding)
rx_vendor_ab.sh bad receiver: our code, or the hardware? (vendor driver on the receive side — the control never run)
band_stressor_sniffer.sh does a stressor hurt the transmitter, judged independently?
ground_rx_degradation.sh which end of the link is losing?
severe_form_hunt.sh which stressor reproduces the collapse? (now cold-cycles both ends per arm + a drift canary that fails the run if the reference moved)
thermal_offtime_sweep.sh cooling, or state reset?
thermal_causation_probe.sh does delivery track temperature with nothing else changing?

Code changes

  • examples/chipstate — reads a chip's registers with no USB reset and no
    bring-up, the only way to inspect what a session left behind. It forces its
    own teardown_power_down=0 or it destroys the state it just read (verified:
    two consecutive dumps byte-identical). Canary set extended with the TX IQ
    correction (0xcc4/0xcc8/0xccc/0xcd4 + 0xec*, on BB page C1 — a plain read
    returns the wrong bank) and RF 0x58 LOK; neither is rewritten by a bring-up
    and neither was visible before.
  • Jaguar1 teardown power-downStop()/destructor run
    HalModule::rtw_hal_deinit() (halt MAC engines, then the die's card-disable
    sequence, using tables that sat unused in hal/Hal88*PwrSeq.c since the port
    began). Jaguar1 was the only generation leaving the chip in ACT with RF live
    after process exit. No performance claim attached — the delivery recovery
    an earlier revision cited came from the unqualified T3U. It also makes a
    stray hardware beacon stop at session end (a documented wart).
  • tuning.teardown_power_down (DEVOURER_TEARDOWN_POWER_DOWN=0|1) — a
    powered-down chip reads back the CARDEMU fill, so post-mortems need it off.
  • PowerOff() reads back 0x0005/REG_CR instead of trusting a return code.

Behaviour change

A beacon in the MAC reserved page now stops when a Jaguar1 session ends instead
of airing until re-enumeration. Matches Jaguar3 and the vendor driver.

Testing

ctest 48/48. teardown_gen_sanity.sh 10/10. tx_teardown_asan.sh 6/6 under
ASan including max-duty kills (teardown now writes registers with URBs in
flight). 8812AU/8814AU/8821AU each re-init cleanly across consecutive sessions
after the power-down.

On closing #348

Whether the original field report describes something real is unknown and
deliberately left open in the doc. Nothing measured is evidence either way — it
was all taken through an instrument that could not support the conclusion. A
re-report is worth taking seriously from a rig where the ground station passes
ground_station_qualify.sh, a second receiver corroborates, and the noise floor
is established first.

🤖 Generated with Claude Code

josephnef and others added 8 commits July 28, 2026 11:49
A Jaguar1 session left the chip in the ACT power state with its RF front end
live, indefinitely, long after the process that brought it up had exited.
Nothing else in the tree does that: Jaguar2 resets at init, Jaguar3 has run a
full card-disable at teardown for a long time.

The cost was a transmitter that quietly lost its dense constellations. Across
back-to-back sessions on an 8812AU at ch6/20 MHz, constant TX config, the chip
thermal meter climbed 43 -> 53 and MCS7 (64-QAM 5/6) delivery fell 83% -> 71%
while the MCS1 control held at 98% and RSSI never moved — no power is lost,
only signal quality. Nothing logs an error, and the signature is
indistinguishable from a link too weak to carry the rate.

Idling did not help, because a chip held in ACT keeps dissipating whether or
not it is transmitting: 120 s of idle bought 4 thermal units where a 12 s VBUS
cycle bought 9. That is why only pulling power appeared to recover it.

Stop() and the destructor now run HalModule::rtw_hal_deinit(): halt the MAC
engines (REG_CR, REG_RCR) then the die's card-disable power sequence, through
the same HalPwrSeqCmdParsing and the same three-way chip dispatch InitPowerOn
already uses. The sequence tables have been sitting in hal/Hal88*PwrSeq.c since
the port began with nothing calling them. Both call sites are best-effort; a
chip already off the bus makes the writes fail, which is fine at teardown.

Measured effect, same repro: 60 s of idle with no power cycle went from
thermal 48 / MCS7 73.0% to thermal 40 / MCS7 85.8%. An ordinary gap between
sessions now restores the part completely without touching the hardware.

Deliberately not included. There is no in-flight recovery API: the mechanism is
heat, recovery needs the radio to stop transmitting for around a minute, and a
call that restores the rate by killing the link for a minute is not a recovery
for an airborne link. An init-side power-off was also rejected — it would run
immediately before power-on and buys no cooling time, while adding risk to a
bring-up path that works. Jaguar2 keeps its current teardown: its thermal meter
did not move across the same run and the link available was too marginal at
MCS7 to resolve a small effect, so there is no measurement to justify changing
it.

Validated on hardware: 8812AU, 8814AU and 8821AU all re-init cleanly across
three consecutive sessions after the power-down; teardown_gen_sanity 10/10;
tx_teardown_asan 6/6 including max-duty kills, which is the case that matters
now that teardown writes registers while URBs may be in flight. The 8814AU
barely heats at this duty (34 -> 37) and shows no degradation to fix.

Also flips a documented wart: a beacon loaded into the MAC reserved page now
stops when the session ends instead of airing until re-enumeration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… refuted it

The previous commit asserted that warm-session TX degradation is caused by chip
temperature. That was correlation across sessions presented as causation, and
two experiments added here argue against it:

thermal_offtime_sweep.sh degrades the part, then powers it down for 5/15/30/60/
120 s before an otherwise identical probe. The reset is bit-identical in every
arm; only cooling time varies — which is the one way to separate "the die
cooled" from "the state was reset", since every recovery observed does both at
once. Temperature fell cleanly with off-time (47 -> 40) while delivery scattered
63-83% with no relation to off-time or temperature: the best result came from
the shortest off-time and the hottest chip.

thermal_causation_probe.sh measures delivery inside a single uninterrupted
session — one bring-up, no re-init, no state change, so only temperature can
move. The thermal meter stayed pinned for 240 s of max-duty TX while MCS7
delivery still drifted ~10% and the control held flat.

probe_repeatability.sh is the measurement that should have come first: ten
identical back-to-back probes, nothing changed between them, give MCS7/20
sd 1.8 and a 5.7-point spread (MCS1 control 98.0 +/- 0.2). A single probe is
worth about +/-3 points, so effects under ~4 points are not findings — and
several plausible-looking decay curves in this investigation sat inside that
band. Run it before believing a delivery difference.

What survives: the fix itself. A 60 s idle recovers MCS7 from 73.0% to 85.8%
only once the teardown power-down exists — ~13 points, about 7 sd, comfortably
real — where before it idling recovered nothing. And the change is right on its
own terms regardless of mechanism: leaving a radio powered and
transmitting-capable after its owning process has exited is wrong, and is what
every other generation already avoids.

What does not: any claim about why. Heat is a real correlate of delivery within
one probe sequence (thermal 39 -> 45 costing ~5 points, control flat, RSSI
unchanged) and demonstrably not the driver in the two experiments above. The
mechanism is open, and the docs now say so instead of asserting it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…invisible ones

Investigating #348 needs to answer "what state did the last session leave this
chip in?", and until now nothing could: every entry point reconfigures the chip
on the way in. claim_interface_then_reset re-enumerates it, Init/InitWrite re-run
the power sequence, reload the BB/RF/MAC tables and re-calibrate. The act of
looking destroyed the evidence.

examples/chipstate opens, claims, does NOT reset, constructs the device (chip
identity resolves from SYS_CFG2 at construction, no bring-up needed), dumps and
exits. It also forces tuning.teardown_power_down=0 for itself — otherwise the
device destructor would card-disable the chip on the way out and the tool would
destroy the state it just read, one look and gone. Verified: two consecutive
dumps of the same chip are byte-identical. --init runs a normal bring-up first,
for a healthy reference on a freshly power-cycled adapter.

Two register groups join the canary set because they are exactly the state that
could explain the bug and exactly the state nothing could see:

  - TX IQ correction (0xcc4/0xcc8/0xccc/0xcd4 and the 0xec* mirrors), written by
    IQK via FillTxIqc. Nothing in array_mp_8812a_phy_reg rewrites them, so unlike
    everything else in the dump they are NOT re-established by a bring-up —
    whatever the last calibration left is what transmits.
  - RF 0x58, the LOK the IQK loads from RF 0x08[19:10]. Not in the RF table, not
    in IQK's own backup/restore.

Both damage TX EVM when wrong, which kills the dense constellations while the
robust rates ride through — the exact signature in #348.

The TX IQC registers live on BB page C1 (0x82c[31]=1), so a plain read returns
whichever bank happens to be selected; the first version of this dump read them
as zero for that reason. They are now read deliberately with the bank snapshotted
and restored, and reported under their own BBC1 kind so a diff cannot silently
compare across banks. On a live 8812AU that turns 0x000000 into path A
X=0x1FC/Y=0x04, path B X=0x203/Y=0x0F — converged calibration values, not the
0x200/0x0 the failure path writes.

tuning.teardown_power_down (DEVOURER_TEARDOWN_POWER_DOWN=0|1, default on) gates
the card-disable added earlier. A powered-down chip answers every register read
with the CARDEMU fill, so post-mortem inspection needs it off — and it is the A/B
lever for measuring what that power-down is actually worth, which is still
unestablished.

PowerOff() now reads back 0x0005 and REG_CR after the sequence and logs them
rather than trusting the return value, so the teardown path is provably not
inert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three experiments, run in this order, each answering the one the previous made
unavoidable.

severe_form_hunt.sh adds exactly one stressor per arm on top of an identical
session loop — band switch, bandwidth switch, ch13/14 spur, power extremes,
failed 5 GHz tunes — so a collapse names its cause instead of just happening.
Measured over a devourer->devourer link it reported the #348 severe form at last:
the band arm took MCS7 from 32.2% to 0.0% with the MCS1 control at 97.5%.

That HIT did not survive contact with a second receiver. The vendor A/B, judged
by an AR9271 that shares no silicon or code with either side, showed the same
"dead" transmitter delivering MCS7 at 62.5% against the vendor driver's 69.8% —
minutes after the hunt scored it at zero.

band_stressor_sniffer.sh therefore re-runs the winning stressor with the sniffer
as judge, so the verdict is about the transmitter alone. Eight band-switching
sessions moved the sniffer's MCS7 count from 3721 to 3915. No degradation. The
transmitter is not what breaks.

ground_rx_degradation.sh isolates the other end by recovering the receiver in
steps while leaving the transmitter alone: fresh ground, aged ground, RX process
restart, RX power cycle, and finally a TX power cycle as the control. On the
8812AU->8822BU pair MCS7 sits at ~0.1-0.4% at every step while MCS1 holds at
95-98%, and none of the recoveries move it — including power-cycling both ends.

The conclusion this forces is uncomfortable and is the point: delivery is a
property of a link, and every measurement in #348 so far — including the original
report — inferred a transmitter defect from a single devourer<->devourer link.
Judged independently the transmitter is healthy under every stressor tried. What
is actually broken lives on the receive side of that link, and remains
unidentified; ambient co-channel traffic on ch6 also rose from ~19 to ~58
frames/3s across the session, which hurts 64-QAM far more than it hurts BPSK, so
an environmental explanation is not yet excluded either.

No mechanism is claimed here. These scripts exist so the next attempt starts from
measurements that can distinguish the two ends.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…driver too

The band-switch arm's 32-point collapse was not a transmitter defect and not a
devourer defect. It was the ground station's receiver dying mid-run.

tests/rx_vendor_ab.sh holds the transmitter constant and changes only the
RECEIVER's driver on the same dongle. On the 8822BU that had been the ground
station for nearly every measurement in this investigation:

  MCS7/20   devourer 34/5862 = 0.6%    vendor 94/5845 = 1.6%
  MCS1/20   devourer 5834/5936 = 98.3% vendor 5152/5922 = 87.0%

Neither driver decodes 64-QAM on it; both decode BPSK fine. At the same time the
same transmitter is decoded at MCS7 by an 8814AU ground at 82.9% and by an AR9271
sniffer at 62%, and matches the vendor driver on the transmit side. So the
adapter's receive path is degraded, not our code — the one conclusion that had
never been tested because the vendor driver had only ever been run as a
TRANSMITTER control.

That single dying adapter is behind a long trail of wrong answers in this issue:
a "warm-session TX degradation", a thermal correlation, and finally a
band-switch stressor that looked like a clean root cause. All of them were
measured through it.

So severe_form_hunt.sh gains the two things that would have caught it:

  - both ends are recovered before every arm, and the ground process restarted
    with them. Cycling only the DUT let receive-side decay accumulate across the
    whole run and surface as a per-arm delta.
  - a drift canary: re-measure the first arm's cold baseline at the very end,
    with both ends recovered exactly as at the start. Nothing in the run should
    move it. If it moved by more than the noise floor the reference shifted
    underneath every arm, the run exits non-zero, and the report says so instead
    of presenting differences between two different rigs as stressor effects.
    On the run that produced the "hit" the arms' own cold baselines had slid
    62 -> 32 -> 1.2 -> 1.8 -> 1.9 -> 0.7.

Also fixes rx_vendor_ab.sh's transmit helper to sleep-then-SIGTERM rather than
letting `timeout` kill txdemo: the final tx.stats carries the frames-sent
denominator and is only emitted on a clean signal, so a slow bring-up silently
turned a measured cell into a divide-by-zero.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retracting "the 8822BU's receive path is degraded". It is not. The TP-Link
Archer T3U has INTERNAL antennas; the 8814AU it was compared against has two
external ones. That is several dB of receive gain, and it puts the T3U's
modulation cliff in the middle of the rates under test. Same transmitter, same
channel, minutes apart:

  T3U (8822BU, internal)   MCS3 97.8  MCS4 98.3  MCS5 79.1  MCS6 49.0  MCS7  2.9
  RTL8814AU (external x2)  MCS3 80.6  MCS4 80.2  MCS5 80.8  MCS6 80.6  MCS7 80.4

A textbook waterfall rolling off through the 64-QAM rates, against a receiver
that is flat with margin to spare. Both adapters are healthy. The vendor driver
failing identically on the T3U was never evidence of damage — a link-budget
shortfall predicts exactly that, and so does hardware failure. Raising TX power
does not rescue it either: at RSSI ~67 the front end is already near-field
saturated, so no offset from -5 to +10 dB moved MCS7 while MCS1 held at 98%.

This is the mechanism behind the whole investigation. A receiver perched on its
cliff swings between fine and zero on a few dB of ambient while the robust rates
stay pinned — indistinguishable from a transmitter that degrades and recovers,
which is precisely the shape of the original report. Earlier in the same session,
with lower ambient, that same T3U delivered MCS7 at 68%. The cliff never moved;
the operating point did.

tests/ground_station_qualify.sh sweeps the ladder on a candidate ground station
and refuses the pairing (exit 1) when the test rate is off the flat part, with
the reason spelled out. Self-tested against the known-marginal T3U: NOT
QUALIFIED for MCS7/20, 2.8% against 98.1% best on its own ladder.

Recorded in CLAUDE.md next to the noise-floor rule, because both are about the
same thing: knowing whether a measurement can support the claim before making
the claim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The numbers in this doc were taken through a TP-Link Archer T3U, whose internal
antennas put its modulation cliff in the middle of the rates under test. A
Comfast CF-924AC V2 — the SAME RTL8822BU silicon, same code path, but with two
external antennas — is flat at 94-96% across MCS1..MCS7 where the T3U rolls off
to 2.9%. Re-measured against it, two of this doc's claims were wrong:

  "2 spatial streams above MCS2 read zero on this bench, two dongles in near
  field give the streams too little spatial decorrelation"
    -> wrong. VHT2SS MCS0 95.3%, MCS4 90.8%, MCS7 15.5%, all modulation-verified.
       That was the receiver's cliff, not spatial decorrelation.

  "VHT1SS_MCS8 (256-QAM) delivered 295/4675"
    -> understated 12x. 77.1% on a qualified receiver, 37/37 frames decoded as
       vht1ss_mcs8.

So the 256-QAM result is considerably stronger than published, and the 2SS
ceiling is MCS7 rather than MCS2.

What did NOT change is the 40 MHz gap: rxdemo with DEVOURER_BW=40 still delivers
zero on a qualified receiver, including the most robust 40 MHz rate. That
survives the better instrument, so it is a real receive-path gap rather than a
margin artifact — the claim is now properly evidenced instead of merely repeated.

The remaining gap to the 2x2 headline is 256-QAM on two streams (VHT2SS_MCS8,
0.6% where 2SS MCS7 reaches 15.5%) — a ladder rolling off, i.e. link margin,
not a property of the mode.

Doc now opens with the receiver-qualification requirement and the two-adapter
comparison that motivates it, because every figure in it depends on that.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… post-mortem

The teardown power-down's only quantitative justification — a 60 s idle
recovering MCS7 delivery from 73.0% to 85.8% — was measured through the ground
station that turns out to have been the source of every wrong answer in this
investigation. It cannot support the claim, so the claim is gone from the code
comment, the subtree CLAUDE.md and the doc.

The change itself stands on architecture alone, which was always the stronger
argument: Jaguar1 was the only generation leaving the chip in ACT with its RF
front end live indefinitely after the owning process exited, and an
autonomously-airing beacon kept transmitting.

docs/warm-tx-degradation.md is rewritten as what it actually is — a post-mortem
of four successive wrong root causes (latched chip state, thermal, band
switching, degraded adapter) that all had one origin: a receiver whose
modulation cliff sat inside the rates under test. Two RTL8822BU adapters,
identical silicon and code, differing only in antennas:

  Archer T3U (internal)     MCS5 79.1  MCS6 49.0  MCS7  2.9
  CF-924AC V2 (external x2) MCS5 95.7  MCS6 94.4  MCS7 95.9

The root CLAUDE.md thermal bullet is narrowed to the part that survived: the
correlation is real, it is a trap, and a power cycle changes chip state and
temperature together so it can never separate them.

Whether the original field report describes something real is left explicitly
open. Nothing measured here is evidence either way, because it was all taken
through an instrument that could not support the conclusion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@josephnef
josephnef merged commit 09819b6 into master Jul 28, 2026
19 checks passed
@josephnef
josephnef deleted the jaguar1-latched-state-investigation branch July 28, 2026 13:01
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.

TX degrades across warm sessions: high-order constellations die, only a VBUS power cycle recovers (a drone cannot power-cycle in flight)

1 participant