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:
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user