Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 34 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -514,8 +514,11 @@ MAC-latched `tsfl` RX timestamp — on all generations, µs-grade
(`docs/time-distribution.md`, measured vs NTP/PTP: `docs/timing-accuracy.md`).
`StartBeacon` loads a beacon into the MAC's reserved page and the chip
auto-transmits at each TBTT with the live TSF stamped at the TX instant — the
sub-µs downlink. The chip beacons **autonomously**: a session that ends
without a power-cycle must call `StopBeacon()` or the beacon keeps airing.
sub-µs downlink. The chip beacons **autonomously**, so `StopBeacon()` is what
stops it mid-session; a session that ends via `Stop()` or destruction powers the
chip down and takes the beacon with it (Jaguar1/Jaguar3 — Jaguar2 has no
teardown power-down yet, so there it keeps airing until the adapter is
re-enumerated).
`UpdateBeaconPayload` swaps the airing content in place (frame-atomic on air,
TBTT-quantized latency); `PinBeaconTbtt` steers the TBTT to an absolute TSF
instant without corrupting the clock (Jaguar1: offset 0 only — its TBTT is
Expand Down Expand Up @@ -608,14 +611,35 @@ generators, never the output files.
After detaching a kernel driver, expect a cold re-init; `DEVOURER_SKIP_RESET`
only helps when firmware state is intact.
- **The chip retains state across soft re-init** — cold-bisect hardware
problems with a VBUS power-cycle, not a re-run. This is not only a bring-up
concern: **high-order constellation TX degrades across warm re-inits**. After
a run of back-to-back sessions an 8812AU delivered 0% at MCS7 (64-QAM) while
16-QAM and below still worked, and a VBUS cycle restored it to 58% at the
same channel, power and gap. Nothing logs an error, RSSI and EVM look
healthy, and the result is indistinguishable from a link too weak to carry
the rate — so any rate-ceiling measurement must cold-cycle per cell or it
measures accumulated chip state. An `authorized` toggle does not clear it.
problems with a VBUS power-cycle, not a re-run.
- **Qualify the ground station before believing a delivery number.** Delivery is
a property of a *link*; a receiver whose modulation cliff sits at the rate
under test measures itself, not the transmitter, and swings between "fine" and
"zero" on a few dB of ambient while the robust rates stay pinned — which looks
exactly like a transmitter that degrades and recovers. Same TX, same channel,
minutes apart: a TP-Link Archer T3U (8822BU, **internal** antennas) ran
MCS3 97.8 / MCS4 98.3 / MCS5 79.1 / MCS6 49.0 / **MCS7 2.9**, while an
RTL8814AU (two **external** antennas) was flat at ~80% across the same ladder.
Both healthy — one just has less margin. `tests/ground_station_qualify.sh`
sweeps the ladder and refuses the pairing (exit 1) when the test rate is off
the flat part. Prefer external-antenna adapters as ground stations for
high-MCS work, and cross-check with a second, independent receiver.
- **A single delivery probe is worth ±3 points, so small effects are not
findings.** Ten identical back-to-back MCS7/20 probes on an 8812AU (nothing
changed between them) gave sd 1.8 and a 5.7-point spread; the MCS1 control
over the same run held 98.0 ± 0.2. Anything under ~4 points needs repetition
before it means anything — several plausible-looking "decay curves" have
turned out to sit inside that band. `tests/probe_repeatability.sh` measures
the floor for a given pair; run it before believing a delivery difference.
- **Do not attribute a rate ceiling to chip temperature without varying
temperature independently.** Delivery does correlate with the `thermal` meter
within a probe sequence, and that correlation is a trap: a sweep varying
*only* the power-off duration (identical reset every arm) found delivery
scattered 63–83% with no relation to off-time or temperature, and inside a
single uninterrupted session the meter stays pinned while delivery drifts.
Read the `thermal` event (`DEVOURER_THERMAL_POLL_MS`) beside a rate-ceiling
number by all means, but a power cycle changes chip state *and* temperature
together, so it can never separate them (`docs/warm-tx-degradation.md`).
- **MediaTek Android hosts cap bulk-IN reads at 16 KB**: some MTK xhci/usbfs
stacks (Dimensity 810, Helio G99, MT6765) never complete a larger bulk-IN
transfer — `LIBUSB_ERROR_TIMEOUT` forever, zero RX with a green init
Expand Down
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,16 @@ add_executable(doctor
target_include_directories(doctor PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/examples/common)
target_link_libraries(doctor PUBLIC devourer PRIVATE PkgConfig::libusb)

# chipstate — read a chip's registers without touching it: no USB reset, no
# bring-up, no calibration. The only way to inspect the state a previous
# session left behind, since every other entry point reconfigures the chip on
# the way in and destroys exactly that evidence.
add_executable(chipstate
examples/chipstate/main.cpp
)
target_include_directories(chipstate PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/examples/common)
target_link_libraries(chipstate PUBLIC devourer PRIVATE PkgConfig::libusb)

# Headless regression guard for the binary-stdin framing shared by the two
# stream demos above (examples/common/stream_stdin.h). No libusb, no radio — just the
# set_stdin_binary() + read_exact() path, so a text-mode regression (e.g. the
Expand Down
49 changes: 38 additions & 11 deletions docs/vht-on-2g4.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,24 @@ Nothing gates this. Rate resolution never reads the band, so a VHT rate on a
channel 6, or a radiotap VHT field on a per-packet basis. The receiver must be
able to decode it; a standards-only 802.11n station sees nothing at all.

## Read this first: cold-cycle or measure nothing
## Read this first: qualify the receiver, or measure the receiver

Delivery is a property of a *link*. A ground station whose modulation cliff sits
at the rate under test reports its own limits, not the transmitter's — and it
swings between "fine" and "zero" on a few dB of ambient while the robust rates
stay pinned. Two RTL8822BU adapters, identical silicon and identical code, same
transmitter and channel, differing only in antennas:

| ground (both 8822BU) | MCS1 | MCS4 | MCS5 | MCS6 | MCS7 |
| --- | --- | --- | --- | --- | --- |
| TP-Link Archer T3U — **internal** antennas | 98.1 | 97.1 | 79.1 | 49.0 | **2.9** |
| Comfast CF-924AC V2 — **two external** | 95.8 | 95.8 | 95.7 | 94.4 | **95.9** |

Run `tests/ground_station_qualify.sh` before trusting any number here; it refuses
the pairing when the test rate is off the flat part of the ladder. Every figure
below was taken through a qualified receiver.

## Cold-cycle too

**High-order constellation TX degrades across warm re-inits.** After a run of
back-to-back devourer sessions on the same adapter, 64-QAM and up stop decoding
Expand Down Expand Up @@ -47,16 +64,25 @@ frames, and every sampled `rx.txhit` decoded back to a rate so the receiver
confirms *which* modulation arrived. A frame commanded as VHT that decodes as
HT is a fallback, not a pass.

Measured from an RTL8812AU into a qualified CF-924AC (external antennas):

| rate | delivery | modulation verified |
| --- | --- | --- |
| VHT 1SS MCS8 — **256-QAM** | **77.1%** | 37/37 decoded as `vht1ss_mcs8` |
| VHT 2SS MCS0 | 95.3% | 54/54 |
| VHT 2SS MCS4 | 90.8% | 43/43 |
| VHT 2SS MCS7 | 15.5% | 7/7 |
| VHT 2SS MCS8 — 256-QAM, 2 streams | 0.6% | not reached |

| Transmitter | Generation | Peer | Confirmed on 2.4 GHz |
| --- | --- | --- | --- |
| RTL8812AU | Jaguar1 | RTL8822BU | VHT 1SS up to **MCS8 (256-QAM)**; HT MCS7 |
| RTL8812AU | Jaguar1 | CF-924AC (8822BU) | VHT 1SS to **MCS8 (256-QAM)**, 2SS to MCS7 |
| RTL8822BU | Jaguar2 | RTL8814AU | VHT 1SS MCS0, 2SS MCS0 |
| RTL8812CU | Jaguar3 | RTL8814AU | VHT 1SS MCS0, MCS4 |

`AdapterCaps::vht_2g4_ok` records the VHT format as a transmit claim for those
three dies. 256-QAM specifically is confirmed on the 8812A only — the other two
were measured before the cold-cycle requirement was understood, so their
ceiling is a floor on what they can do, not a limit.
three dies. The Jaguar2/Jaguar3 rows are floors, not limits — they were measured
through an unqualified receiver and have not been re-run.

**VHT MCS9 is not a legal rate at 20 MHz for 1 or 2 spatial streams.** Asking
for `VHT1SS_MCS9/20` gets an MCS8 PPDU: the run delivered 311 frames and every
Expand Down Expand Up @@ -93,15 +119,16 @@ Two traps that harness encodes, each of which produced a wrong answer first:

## What is still not measured

- **2 spatial streams above MCS2.** Every 2SS rate above the low ladder reads
zero on this bench even cold, on both directions tried. Two dongles in near
field give the two streams too little spatial decorrelation; this is a rig
property and is not evidence about the chip.
- **256-QAM on 2 spatial streams** (`VHT2SS_MCS8`) reads 0.6% where 2SS MCS7
reaches 15.5% — the ladder is rolling off, so this is the link running out of
margin rather than anything specific to the mode. It is the one genuine
remaining gap to the 2×2 headline.
- **40 MHz, hence the headline 400 Mbps rate.** `rxdemo` with
`DEVOURER_BW=40` delivers zero on both Jaguar1 and Jaguar2 — including for a
20 MHz transmitter on the primary channel, at either primary-channel offset,
in both role directions. Re-tested after the cold-cycle fix and unchanged, so
it is a real receiver-side gap, not warm state. Note the existing 40 MHz TX
in both role directions, and including the most robust 40 MHz rate (MCS0/40).
Re-tested on a **qualified** receiver and still zero, so this is a real
receiver-side gap and not a margin artifact. Note the existing 40 MHz TX
validation (`tests/jaguar2_tx_bw40.sh`) uses a *kernel* sniffer, so the
devourer 40 MHz receive path may never have been exercised.
- **Link asymmetry.** 8812AU → 8822BU reaches MCS7 at 68%; the reverse
Expand Down
114 changes: 114 additions & 0 deletions docs/warm-tx-degradation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# The "warm TX degradation" that wasn't — a measurement post-mortem

A field report described an RTL8812AU whose 64-QAM rates died across repeated
sessions while the robust rates stayed perfect, recovering only on a USB power
cycle. Investigating it produced, in order: a latched-chip-state root cause, a
thermal root cause, a band-switching root cause, and a degraded-adapter root
cause. **All four were wrong, and all four had the same origin.**

The ground station was a TP-Link Archer T3U — an RTL8822BU with *internal*
antennas. Its modulation cliff sits inside the 64-QAM rates, so it could not
measure what it was being asked to measure.

## The measurement that ends it

Two RTL8822BU adapters. Identical silicon, identical code path, same
transmitter, same channel, minutes apart. The only difference is antennas:

| 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** |

One is a textbook waterfall rolling off through 64-QAM. The other is flat with
margin to spare. Both adapters are healthy.

## Why a marginal receiver imitates a degrading transmitter

A receiver perched on its cliff swings between "fine" and "zero" on a few dB of
ambient, while the rates below the cliff stay pinned at ~98%. That is precisely
the reported symptom: high-order modulation dies, low rates unaffected,
apparently recovering at random.

Earlier in one session that same T3U delivered MCS7 at 68%; later, 0.6%. The
cliff never moved — the operating point did (co-channel ambient rose from ~19 to
~58 frames/3 s). Power-cycling appeared to "fix" it only because a cycle takes
long enough for conditions to drift back.

## What each wrong answer was, and what killed it

- **Latched chip state.** Refuted by reading the registers: the BB TX-swing is
rewritten unconditionally at every init by `phy_SetBBSwingByBand_8812A`.
- **Thermal.** Refuted by `tests/thermal_offtime_sweep.sh` — vary only the
power-off duration, keeping the reset identical, and delivery does not follow
temperature (best result came from the *shortest* off-time and the *hottest*
chip). Also by `tests/thermal_causation_probe.sh`: inside one uninterrupted
session the thermal meter is pinned while delivery still drifts.
- **A transmitter defect from band switching.** A stressor matrix did reproduce
a 32-point collapse — but judged by an AR9271 sniffer the same stressor moved
decoded MCS7 from 3721 to 3915 frames, i.e. not at all. The transmitter also
matched the vendor driver (62.5% vs 69.8%).
- **A degraded 8822BU.** The vendor driver failed on it identically, which
looked conclusive — but a link-budget shortfall predicts exactly that too. TX
power moved nothing (front end already near-field saturated at RSSI 67) and
both RX chains read healthy. Then the CF-924AC settled it.

## Rules this produced

1. **Qualify the receiver before believing a delivery number.**
`tests/ground_station_qualify.sh` sweeps the ladder and refuses the pairing
(exit 1) when the test rate is off the flat part. Prefer external-antenna
adapters as ground stations for high-MCS work.
2. **Measure the noise floor first.** `tests/probe_repeatability.sh` — a single
MCS7/20 probe here has sd 1.8 and a 5.7-point spread, so effects under ~4
points are not findings. Several convincing "decay curves" sat inside it.
3. **Corroborate with an independent receiver** before attributing a delivery
change to either end. An AR9271 and the `reference/` vendor drivers were on
this bench the whole time; either would have caught this on day one.
4. **A control that fails under two drivers does not prove hardware damage.**
Run the vendor driver on the *receive* side too (`tests/rx_vendor_ab.sh`),
and check the physical setup — antennas, placement, gain.
5. **Never attribute causation to an intervention that changes two variables.**
Removing power both resets chip state and cools the die; no amount of
power-cycling separates them.

## The one code change that survived

Jaguar1 was the only generation that neither reset the chip at init nor
de-initialised it at teardown, so a session left the chip in ACT with its RF
front end live indefinitely after the owning process exited — and an
autonomously-airing beacon kept transmitting.

`RtlJaguarDevice::Stop()` and the destructor now run
`HalModule::rtw_hal_deinit()`: halt the MAC engines, then the die's card-disable
power sequence, using tables that had sat in `hal/Hal88*PwrSeq.c` since the port
began with nothing calling them.

**It carries no performance claim.** The delivery recovery an earlier revision
cited was measured through the unqualified T3U. The change stands on the
architecture alone: every other generation already does this, and it makes a
stray beacon stop. `tuning.teardown_power_down=0`
(`DEVOURER_TEARDOWN_POWER_DOWN=0`) disables it, which post-mortems need — a
powered-down chip answers every register read with the CARDEMU fill.

## Is the original report real?

Unknown, and deliberately left open. Every measurement taken here was through an
instrument that could not support the conclusion, so nothing gathered so far is
evidence either way. A re-report is worth taking seriously if it comes from a rig
where the ground station passes rule 1, a second receiver corroborates, and the
noise floor is established.

## Harnesses

| script | question |
| --- | --- |
| `ground_station_qualify.sh` | can this receiver measure this rate at all? |
| `probe_repeatability.sh` | how big must an effect be to be real here? |
| `rx_vendor_ab.sh` | is a bad receiver our code, or the hardware? |
| `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, if any, reproduces the collapse? |
| `thermal_offtime_sweep.sh` | cooling, or state reset? |
| `thermal_causation_probe.sh` | does delivery track temperature with nothing else changing? |
| `examples/chipstate` | what state did the last session leave, read without disturbing it? |
Loading
Loading