Fix Si4684 pitch distortion: uncalibrated crystal (CTUN/XTAL_FREQ)

Root cause of FM/DAB "stonata" audio: xtalCtun=31/nominal XTAL_FREQ
defaults were never measured against this board's actual crystal
(Abracon ABM8-19.200MHZ-10-1-U-T, CL=10pF, two external 15pF load
caps). Fixed via CTUN trim by ear (31->0) plus a new live calibration
loop that reads the Si4684's own FM_RSQ FREQOFF field (broadcast
carriers are GPS-locked, so it's a free precision frequency reference)
to trim XTAL_FREQ with no lab equipment. Converged to CTUN=0,
XTAL_FREQ=19199750 Hz, residual -3/-4 ppm cross-checked on two
stations.

New tools/infrastructure (kept, not one-off):
- Si4684Driver::recalibrateXtal() + POST /api/tuner/xtal-calibrate:
  live crystal re-trim without an ESP32 reflash.
- FM_RSQ FREQOFF exposed as "freqoff_ppm" in GET /api/tuner/status.
- tools/si4684_xtal_calibration.py: automates the trim loop.
- tools/si4684_antenna_calibration.py: AN851 Appendix A ANTCAP/VARM/VARB
  sweep tool (same session, separate calibration).
- CONFIG_ESP32_I2S_TEST_TONE (off by default): isolates Si4684-specific
  audio issues from shared-downstream ones by writing a tone directly
  over the I2S bus the Si4684 also uses.
- SIGMA_WRITE_REGISTER_BLOCK/sigma_safeload_block now retry on NACK and
  verify via read-back instead of firing I2C writes blind.
- FM de-emphasis set to European 50us (was left at the US 75us default).
- DAB_ACF_ENABLE restored to its previous 0x0000 with a citation
  explaining why (tested the datasheet default of 0x0003, made things
  audibly worse).

See docs/si4684-rf-investigation-report.md's 2026-08-23 entry for the
full elimination chain and docs/TODO.md for follow-up work (persisting
calibration results to EEPROM instead of requiring a firmware edit).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 20:30:42 +02:00
co-authored by Claude Sonnet 5
parent a91c63fc31
commit fae2305164
24 changed files with 1309 additions and 29 deletions
@@ -1111,3 +1111,123 @@ RTS=0 stability argues against pure floating-noise, and flow control may
not even be engaged by default per the datasheet. The single most
valuable next input is Feasycom's own answer, not another round of
timing-parameter permutation.
## 2026-08-23 update: FM/DAB pitch-distortion root-caused and fixed (uncalibrated Si4684 crystal reference); a separate downstream audio-quality issue found and left open
**Symptom**: user reported FM+DAB audio hiss/distortion, later sharpened to
"voce stonata" (mistuned/off-pitch voice), present on both bands.
**Elimination chain, each step verified on real hardware, not theory**:
RF signal strength (new antenna, RSSI/SNR excellent — see the ANTCAP
section above) → boot-time ADAU1701 DSP program load (added read-back
verification to `SIGMA_WRITE_REGISTER_BLOCK`/`sigma_safeload_block`,
confirmed clean on every boot and every runtime safeload) → ADAU1701 EQ
band 0 ("fixed high-pass," never touched at runtime) — initially
miscalculated as an unstable filter from a wrong fixed-point bit-width
assumption (8.23 vs the chip's actual 5.23/28-bit format), corrected and
confirmed stable via SigmaStudio's own live register capture connected to
the device → ADAU1701 mixer (all input knobs centered, confirmed) →
`DAB_ACF_ENABLE` (0xB500, found disabled with no citation; datasheet
default is 3; tried enabling it — made things audibly *worse*, likely
because COMF_NOISE_ENABLE literally injects synthetic noise on signal
dips; reverted to 0x0000, matching what PE5PVB's independent
SI4684-DAB-Receiver project also does deliberately — not the cause, but
no longer an unexplained magic number) → **decisive test**: a 440 Hz tone
generated by the ESP32 and written directly over the shared I2S bus
(`main/esp32_i2s_test_tone.{hpp,cpp}`, `CONFIG_ESP32_I2S_TEST_TONE`) came
through clean and perfectly in-tune, verified with a real tuner — isolated
the pitch problem to the Si4684 itself, ruling out ADAU1701/mixer/I2S
receiving/BT1035/speaker → TR_SIZE (0x7) and IBIAS (72 = 720µA) checked
against AN649 Figure 13 ("Safe Range of Operation for a 19.2 MHz
Crystal"), both comfortably inside the safe range for this crystal's ESR.
**Root cause**: `Si4684Driver::boot()`'s `xtalCtun=31`/`xtalIbias=72`
defaults had only a vague "already verified live" justification — no
actual measurement for *this* board's crystal (Abracon
ABM8-19.200MHZ-10-1-U-T, CL=10pF decoded from the part number's own
ordering-code table, two external 15pF load caps per the schematic) ever
existed.
**Fix, two stages**:
1. CTUN empirical trim by ear (AN649 §9.3 says trim by measurement; no
oscilloscope available, and a multimeter's frequency counter reads the
strong I2S LRCLK signal fine but returns 0 on the crystal pins
themselves — too weak/high-impedance for a general-purpose meter to
trigger on). Swept 31→5→0 (0 = the floor of the 0-63 range),
monotonic improvement each step, still "less bad, not fixed" at the
floor.
2. **XTAL_FREQ precision trim using the chip's own measurement, no lab
equipment needed**: real FM/DAB transmitters are GPS/rubidium-locked,
so FM_RSQ_STATUS's FREQOFF field (AN649 Command 0x32 RESP8, signed,
units of 2 PPM) directly reports the *receiver's* crystal error on any
locked station. Added `Si4684Driver::recalibrateXtal()` (forces a full
re-boot with new IBIAS/CTUN/XTAL_FREQ, no ESP32 restart), a new
endpoint `POST /api/tuner/xtal-calibrate`, exposed FREQOFF as
`"freqoff_ppm"` in `GET /api/tuner/status`, and
`tools/si4684_xtal_calibration.py` to automate the trim loop
(tune → average several FREQOFF samples → correct → repeat).
**Non-obvious gotcha, found only by watching the first attempt
diverge, not documented anywhere**: the correction sign is
`xtal_freq *= (1 - ppm/1e6)`, not `(1 + ppm/1e6)` — the "tell it the
truth" sign convention makes the error grow, not shrink (confirmed
live: ppm went 28→60→122→254/no-lock across 3 iterations before the
sign was flipped). Averaging + damping (0.6) were both needed for
smooth convergence; a single raw FREQOFF sample has enough
reception-noise jitter (~±20-35 ppm swings observed) to make an
undamped loop oscillate instead of settling.
**Final calibrated values** (now the firmware default,
`main/hardware_bootstrap.cpp`): `CTUN=0`, `XTAL_FREQ=19,199,750 Hz`
(≈-13 ppm off the 19.2 MHz nominal). Converged residual: **-3.8 ppm at
87.6 MHz, -3.0 ppm at 105.1 MHz** — consistent across two stations at
opposite ends of the FM band (the cross-check AN649 itself recommends),
confirming this really is the crystal reference and not something
frequency-dependent. Down from **+70 ppm** uncorrected at nominal
XTAL_FREQ. No physical hardware change (different load-cap values) ended
up being necessary — contrary to what seemed likely after CTUN alone.
**User-confirmed result**: "migliorato moltissimo" (improved a lot) after
this fix — the systematic pitch/tuning distortion is resolved.
### Still open: a separate downstream hiss/intelligibility issue, NOT the Si4684
After the crystal fix, the user still reported residual hiss and, more
seriously, words being unintelligible on both FM and DAB. Recordings sent
for spectral analysis showed no gross technical defects (no clipping, no
dropouts, no dominant isolated resonance) — inconclusive from the
recordings alone (phone-mic-through-air recordings are a poor tool for
this specific symptom; room acoustics and mic response confound the
signal). The user's direct listening judgement (confirmed repeatedly:
"si sente ancora fruscio e le parole sono incomprensibili") is the
ground truth here, not the recordings.
**Decisive test**: enabled `web_radio_stream` (internet radio via ESP32,
`POST /api/streaming`) with a direct HTTP MP3 stream
(`http://icecast.radiofrance.fr/franceinter-midfi.mp3`), routed through
the same shared ADAU1701 mixer/EQ/output/BT1035/Bluetooth-speaker chain
as FM/DAB but **never touching the Si4684 at all**. User confirmed:
**same symptom** (hiss + unintelligible). This is different from the
earlier synthetic-440Hz-tone test, which came through clean — the tone
test used a trivial, CPU-cheap sine generator with no decode/buffering
involved, so it never exercised whatever a real MP3-decode-under-WiFi-load
pipeline does.
**Conclusion**: the pitch/tuning problem (fixed) and this hiss/
intelligibility problem are two separate, independently-confirmed root
causes that happened to co-occur and get conflated as "one bug" for most
of this session. The Si4684/crystal is now cleared for *this* symptom —
next session should look at: (a) `web_radio_stream`'s MP3 decode/I2S
buffer-feed path for underrun/overrun under real WiFi jitter, since that
was the actual reproducer, and (b) whether the same class of issue could
independently affect the ADAU1701 mixer/EQ path under real dynamic
program content generally (the passing tone test doesn't rule this out
for FM/DAB specifically, only for a pure sine wave). Don't re-open the
Si4684/crystal-calibration question for this symptom without new
evidence — it's a different, still-unidentified mechanism.
**New permanent diagnostic tools from this session** (kept in the repo,
not removed): `GET /api/tuner/status` now reports `"freqoff_ppm"` for FM;
`POST /api/tuner/xtal-calibrate` for live Si4684 crystal re-trim without
reflashing; `CONFIG_ESP32_I2S_TEST_TONE` Kconfig option (off by default)
for isolating Si4684-specific vs. shared-downstream audio issues;
`tools/si4684_xtal_calibration.py` and `tools/si4684_antenna_calibration.py`.