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
@@ -49,15 +49,19 @@ void sigma_studio_unlock(void);
/**
* @brief Write a contiguous register/data block to the ADAU1701.
*
* Retries each I2C chunk up to 3 times on NACK, matching the reliability
* already applied to the runtime safeload path (see sigma_i2c_write).
*
* @param devAddress SigmaStudio device address (0x68 write addr).
* @param address 16-bit target address in DSP memory map.
* @param length Payload length in bytes (may exceed 255).
* @param pData Payload bytes.
* @return 0 on success, non-zero if any chunk failed after retries.
*/
void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress,
unsigned int address,
unsigned int length,
ADI_REG_TYPE* pData);
int SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress,
unsigned int address,
unsigned int length,
ADI_REG_TYPE* pData);
/**
* @brief Read a contiguous register/data block from the ADAU1701.
@@ -69,6 +73,22 @@ void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress,
*/
int sigma_i2c_read(unsigned int reg, unsigned char* data, unsigned int length);
/**
* @brief Read back a previously written block and compare it byte-for-byte.
*
* Diagnostic aid for boot-time DSP program replay: a chunk that ACKed but
* still landed wrong (or a register that doesn't hold the value it was
* given) shows up here even though SIGMA_WRITE_REGISTER_BLOCK reported
* success. Read-only — never aborts anything by itself, callers decide.
*
* @param address 16-bit address the block was written to.
* @param expected Bytes that were written.
* @param length Payload length in bytes (may exceed 255).
* @return 0 if the read-back matches, non-zero on mismatch or read failure.
*/
int sigma_verify_block(unsigned int address, const ADI_REG_TYPE* expected,
unsigned int length);
/** Safeload data register base (0x0810..0x0814). */
#define ADAU1701_SAFELOAD_DATA_BASE 0x0810U
/** Safeload address register base (0x0815..0x0819). */