XTAL crystal-cap CFO lever + closed-loop CFO tracking (#217)#222
XTAL crystal-cap CFO lever + closed-loop CFO tracking (#217)#222josephnef wants to merge 7 commits into
Conversation
…sue #217) Expose the Realtek AFE crystal load-capacitance trim as a runtime lever: IRtlDevice::SetXtalCap(cap) / GetXtalCap(), env DEVOURER_XTAL_CAP, GetAdapterCaps().xtal_cap_max / xtal_cap_default, and the adapter.caps event fields. Applied at end of bring-up; sticky (an AFE register the RF retune never touches). Per-generation register (hal_set_crystal_cap / phydm_set_crystal_cap_reg): Jaguar1 0x2C, 6-bit, cap|(cap<<6), per-die field (8812/8811 [30:19], 8814A [26:15], 8821A [23:12]); efuse default 0xB9 Jaguar2 0x24[30:25] + 0x28[6:1], 6-bit; efuse default 0xB9 Jaguar3 0x1040[23:10], 7-bit, cap|(cap<<7) Also adds a live-step primitive (DEVOURER_XTAL_STEP="c1,c2,.." in the CW hold) — the mechanism a closed-loop CFO controller drives, and what makes a clean single-chip measurement possible. Bench-measured authority (tests/xtal_cfo_sweep.sh sdr — CW tone, cap stepped live on one warmed c812, SDR LO peak): monotonic ~51 Hz/code at 5.2 GHz (12042 -> 15317 Hz over caps 0x10..0x50), ~0.6 ppm mid-range, ~1.25 ppm full range — the right order for a marginal 5 MHz/5 GHz pair (1-2 ppm). 20 MHz stays locked under an aggressive trim (12400 hits). ctest 13/13. Open-loop caveat (documented): cold-start crystal warm-up drifts ~3 ppm, larger and faster than the trim, so a fixed trim is a per-link qualification lever; the production fix is a closed loop trimming toward the RX-measured CFO (live-step primitive in place, loop not wired). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…et (#217) The production half of #217: a fixed crystal-cap trim is a one-shot qualification lever, but the crystal drifts ~3 ppm during warm-up (the root of the 5 MHz/5 GHz bimodality), so it needs to track. This adds the closed loop on Jaguar3 (DEVOURER_CFO_TRACK). - RX phy-status: extract the per-path CFO tail (phy_sts_rpt_jgr3_type1 DW5, byte 20; the earlier evm@16/snr@24 offsets confirm the struct). - src/CfoTracker.h: bang-bang integral controller with hysteresis, ported from phydm phydm_cfo_tracking — steps the crystal cap +/-1 per tick to drive the CFO into a deadband. Auto-detects the cap->LO polarity (flips per silicon: if a step makes |CFO| worse, invert once). - RtlJaguar3Device: feed the per-frame CFO to the tracker, tick it on a ~2 s cadence from the RX loop (the controller runs on the RECEIVER, which is the side that measures the offset). The cap register (0x1040) is written READ-FREE from a base cached before the RX flood — a control-transfer read races the async bulk-IN and throws under load. Bench (c811 10M TX -> c812 RX, DEVOURER_CFO_TRACK): the loop measures the CFO, steps the cap in the correct auto-detected direction, and the CFO falls monotonically toward its minimum while the link stays decoding (104900 hits). Opt-in; normal RX unaffected (20M regression 4500). ctest 13/13. Honest scope: the loop needs a marginally-decoding link to bootstrap (no frames -> no error signal); where a pair's absolute offset exceeds the ~1 ppm crystal-cap authority it converges to the rail rather than the deadband; and the cfo_tail->kHz scale is the coarse vendor macro (~10x high for narrowband per SDR cross-check) — the loop uses the CFO sign + deadband, not a calibrated kHz. Jaguar1/2 not wired (Jaguar3 is the narrowband testbed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update: closed-loop CFO tracking added (commit 62a2274)Beyond the manual trim lever, this branch now includes the closed loop — the production answer to the warm-up drift that makes a one-shot trim insufficient.
Bench (c811 10 MHz TX → c812 RX): the loop measures the CFO, steps the cap in the correct auto-detected direction, and the CFO falls monotonically toward its minimum while the link stays decoding (104900 hits). Opt-in; 20 MHz RX unaffected; ctest 13/13. Honest scope: needs a marginally-decoding link to bootstrap (no frames → no error signal); where a pair's absolute offset exceeds the ~1 ppm crystal-cap authority it converges to the rail rather than the deadband; and the |
Extend the closed-loop CFO controller to Jaguar2 — highest-value target, since the 8821C (c811) is the other half of the #217 bimodal pair. The Jaguar2 phy-status uses the same jgr2 type1 layout as Jaguar3 (evm@16, snr@24), so the per-path CFO tail is at the same byte 20; FrameParserJaguar2 now fills rx_pkt_attrib::cfo_tail. The reusable CfoTracker + RX-loop tick mirror the Jaguar3 wiring; the cap is written read-free from cached bases (0x24[30:25] + 0x28[6:1], the two 6-bit AFE legs) to avoid the control-transfer-read-under-bulk-IN crash. Bench (c812 10M TX -> c811 RX detuned to 0x30, DEVOURER_CFO_TRACK): the loop auto-detects the cap->LO polarity (opposite to the c812's — steps UP here) and drives the measured CFO down (52 -> 45) while the link stays decoding (66300 hits). Opt-in; ctest 13/13. Jaguar1 remains the documented follow-on (same port; needs its 11AC phy-status CFO offset found + verified). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closed loop extended to Jaguar2 (commit 0eee52e)Ported the CFO controller to Jaguar2 (8822B/8821C) — the high-value target, since the 8821C (c811) is the other half of the #217 bimodal pair. The Jaguar2 phy-status is the same jgr2 type1 layout as Jaguar3 (evm@16 / snr@24), so the CFO tail is at the same byte 20; the reusable Bench (c812 10 MHz TX → c811 RX detuned to 0x30): the loop auto-detects the cap→LO polarity (opposite to the c812's — it steps up here) and drives the measured CFO down (52 → 45) while the link stays decoding (66300 hits). Opt-in; ctest 13/13. Coverage now: Jaguar2 + Jaguar3 for the closed loop; the manual |
…nerations Port the crystal-cap CFO controller (#217) to Jaguar1 so the closed loop now covers every generation. The 8812 11AC phy-status carries the tail as the named cfotail[0] field of _phy_status_rpt_8812 (DW2 byte 1) — the same unified report the 8814A uses (its 8814-specific field meanings are annotated on neighbouring members), so a single FrameParser assignment feeds the tracker on 8812/8811/ 8814A alike. The RX-loop tick writes the per-die 0x2C crystal-cap field read-free from a base cached before the RX flood (mirroring the Jaguar2/3 race-avoidance), gated on DEVOURER_CFO_TRACK. Add a DVR_DEBUG per-tick line to all three generations' cfo_tick so the controller's measurement is observable on a hold, not only on a step — compiled out in production builds. Hardware: verified on a plugged RTL8814AU — the tick fires on the ~2 s cadence reading the die's efuse crystal-cap, and holds at cfo~0 against calibrated ambient APs (correct no-op). tests/jaguar1_cfo_track_smoke.sh drives it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The closed-loop lead sentence claimed the CFO tail lives at "DW5" while asserting all three generations — but DW5 is only the Jaguar2/Jaguar3 offset (Jaguar1 carries it at DW2 byte 1). Make the lead generation-neutral and let the per-generation breakdown give the offsets. Add tests/xtal_cfo_sweep.sh and tests/jaguar1_cfo_track_smoke.sh to the script inventory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds tests/jaguar1_cfo_convergence.sh — an 8812AU transmits the canonical narrowband beacon while an 8814AU receives with the CFO loop armed. Confirms Jaguar1 is at full parity with Jaguar2/Jaguar3, not just plumbing: on a real 10 MHz/5 GHz link the receiver decodes the beacon (50k hits), the loop crosses the deadband on the pair's inter-crystal offset, auto-detects the cap->LO polarity (flips after a step grows |CFO|), and walks toward the CFO-reducing rail. This pair's offset exceeds the ~1.25 ppm cap authority, so it rails rather than nulling into the deadband — the same documented regime as J2/J3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What
Closes #217 — and the closed-loop follow-on the issue scoped as optional. Two layers:
SetXtalCap): the manual lever that pulls the chip's reference oscillator a few ppm, so a marginal TX/RX crystal pair can be aligned. The payoff is 5 MHz at 5 GHz, the one mode whose CFO budget is tight enough that per-adapter crystal offset matters.DEVOURER_CFO_TRACK): the receiver reads the per-frame CFO tail from the OFDM phy-status and a bang-bang controller (src/CfoTracker.h, ported from phydmphydm_cfo_tracking) steps the crystal cap on a ~2 s cadence to drive the offset toward zero — tracking the warm-up drift a one-shot trim can't. Wired and hardware-validated on all three generations.API
IRtlDevice::SetXtalCap(cap)/GetXtalCap()— raw trim code,cap < 0reverts to the efuse default. Applied at end of bring-up, sticky (an AFE register the RF retune never touches).GetAdapterCaps().xtal_cap_max/xtal_cap_default+ theadapter.capsevent fields.DEVOURER_XTAL_CAP=0xNN(manual trim) /DEVOURER_CFO_TRACK=1(closed loop) env.DEVOURER_XTAL_STEP="c1,c2,.."— live cap-stepping in the CW hold (the trim primitive the loop drives).Per-generation register (from
hal_set_crystal_cap/phydm_set_crystal_cap_reg):0x2C(per-die field)_phy_status_rpt_8812.cfotail[0](DW2 byte 1)0x24[30:25]+0x28[6:1]0x1040[23:10]Bench validation
Trim authority (
tests/xtal_cfo_sweep.sh sdr) — CW tone, cap stepped live on one warmed c812, B210 peak-freq per step (differential across cold bring-ups is useless: warm-up drift ~3 ppm swamps it):Smooth monotonic ~51 Hz/code at 5.2 GHz → ~0.6 ppm mid-range, ~1.25 ppm full range — the right order for a marginal 5 MHz/5 GHz pair (1–2 ppm). 20 MHz stays locked under an aggressive trim (12400 hits); ctest 13/13.
Closed loop — validated engaging on real links across all three generations. Jaguar1 two-adapter demo (
tests/jaguar1_cfo_convergence.sh, 8812AU TX ↔ 8814AU RX, 10 MHz/5 GHz): RX decodes the narrowband beacon (50k hits), the loop crosses the deadband on the pair's inter-crystal offset, auto-detects the cap→LO polarity (flips after a step grows |CFO|), and walks toward the CFO-reducing cap.tests/jaguar1_cfo_track_smoke.shconfirms the tick holds correctly against calibrated ambient APs.Honest scope
Only 5 MHz/5 GHz needs this; every other mode's CFO budget is wide enough that the factory efuse default works for any pair. The closed loop's limits, documented in
docs/narrowband.md: it needs a marginally-decoding link to bootstrap (no frames → no error signal), runs on the receiver, and where a pair's absolute offset exceeds the ~1.25 ppm crystal-cap authority it converges to the rail (the best it can do) rather than the deadband. Thecfo_tail→kHzscale is the coarse vendor macro — the loop uses the CFO sign + deadband, not a calibrated kHz.The one item from the issue's validation plan not literally reproduced is the specific 8812CU↔8811CU "bimodal → stable" cell — that bimodality is intermittent/thermal and would not reproduce on demand; the mechanism was validated instead by the measured LO authority above and the loop demonstrably reducing/railing a real inter-crystal CFO.
🤖 Generated with Claude Code