Si4684 RF investigation report; fix ADAU1701 I2C reliability under long safeload bursts

- Add docs/si4684-rf-investigation-report.md: full record of the FM/DAB no-lock
  investigation (crystal, front-end matching, ANTCAP sweep, continuity, leading
  EP solder-defect hypothesis, PCBWay report sent) plus the separate audio
  profile NVS bug found and partially fixed this session.
- Fix sigma_i2c_write() (SigmaStudioFW.c): no retry on I2C failure meant a single
  transient NACK anywhere in a long safeload burst (EQ apply = ~55 sequential
  transactions) aborted the whole sequence. Added a 3-attempt retry.
- Add granular failure logging (AudioService::applyProfileToDsp/persistProfile,
  Adau1701Driver::applyMixer/applyEq, NvsAudioProfileStore::saveProfile error
  codes) to isolate the remaining NVS-side audio profile save failure.
- Si4684Driver::tuneFm gains an optional ANTCAP argument (default 0 = unchanged
  auto-tune behavior) used during this session's front-end matching sweep.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 22:21:04 +02:00
co-authored by Claude Sonnet 5
parent 6f7b6dd12c
commit bee8cfa86f
7 changed files with 193 additions and 13 deletions
@@ -241,12 +241,14 @@ namespace adau1701
mixer.si4684Right);
!result)
{
ESP_LOGW(kTag, "applyMixer: si4684 input safeload failed");
return result;
}
if (auto result = setInputVolume(core::MixSource::Esp32, mixer.esp32Left,
mixer.esp32Right);
!result)
{
ESP_LOGW(kTag, "applyMixer: esp32 input safeload failed");
return result;
}
if (auto result =
@@ -254,10 +256,17 @@ namespace adau1701
mixer.mixLeft);
!result)
{
ESP_LOGW(kTag, "applyMixer: st0 safeload failed");
return result;
}
return safeloadGain(static_cast<unsigned>(ADDR_STMIXER1_ST1_VOLUME),
mixer.mixRight);
if (auto result = safeloadGain(
static_cast<unsigned>(ADDR_STMIXER1_ST1_VOLUME), mixer.mixRight);
!result)
{
ESP_LOGW(kTag, "applyMixer: st1 safeload failed");
return result;
}
return {};
}
std::expected<void, Adau1701Error> Adau1701Driver::setEqBand(
@@ -344,6 +353,8 @@ namespace adau1701
if (auto result = setEqBand(*index, band.gain, band.center, band.q);
!result)
{
ESP_LOGW(kTag, "applyEq: band %u safeload failed",
static_cast<unsigned>(i));
return result;
}
}
@@ -16,6 +16,7 @@
#include "driver/i2c_master.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include <string.h>
@@ -135,9 +136,22 @@ static int sigma_i2c_write(unsigned int reg, const unsigned char* data,
buf[0] = (unsigned char)((reg >> 8) & 0xFFU);
buf[1] = (unsigned char)(reg & 0xFFU);
memcpy(buf + 2U, data, length);
const esp_err_t err =
i2c_master_transmit(s_dev, buf, (size_t)(2U + length), 1000);
return err == ESP_OK ? 0 : -1;
// Long safeload sequences (e.g. one EQ band = 5 params x 2 writes +
// trigger) chain dozens of back-to-back transactions; a single
// transient NACK anywhere in that chain used to abort the whole
// sequence. Retry a few times before giving up -- cheap and matches
// how every other bus driver in this codebase tolerates bus noise.
static const int kMaxAttempts = 3;
esp_err_t err = ESP_FAIL;
for (int attempt = 0; attempt < kMaxAttempts; ++attempt) {
err = i2c_master_transmit(s_dev, buf, (size_t)(2U + length), 1000);
if (err == ESP_OK) {
return 0;
}
vTaskDelay(pdMS_TO_TICKS(2));
}
return -1;
}
/*
@@ -203,6 +203,11 @@ public:
*
* @dname tuneFm
* @param frequency FM centre frequency in kHz.
* @param antCap FM_TUNE_FREQ ARG4/5 ANTCAP (AN649 Command 0x30).
* 0 = automatic (varactor set from FE_VARM/VARB
* properties); 1-128 bypasses auto-tune and forces
* the on-chip antenna varactor directly (AN851
* Appendix A calibration procedure).
* @return Ok on success, or Si4684Error::WrongBand / TuneFailed.
* @pubstate sends FM_TUNE_FREQ and waits for STC (AN649).
*
@@ -210,7 +215,7 @@ public:
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Si4684Error> tuneFm(
core::FrequencyKHz frequency);
core::FrequencyKHz frequency, std::uint8_t antCap = 0U);
/**
* @brief seekFm — seek FM in the given direction.
@@ -690,7 +690,7 @@ Si4684Band Si4684Driver::loadedBand() const noexcept
}
std::expected<void, Si4684Error> Si4684Driver::tuneFm(
core::FrequencyKHz frequency)
core::FrequencyKHz frequency, std::uint8_t antCap)
{
if (auto band = ensureBand(Si4684Band::Fm); !band) {
return band;
@@ -703,8 +703,8 @@ std::expected<void, Si4684Error> Si4684Driver::tuneFm(
0x00U,
static_cast<std::uint8_t>(chipFreq & 0xFFU),
static_cast<std::uint8_t>(chipFreq >> 8),
0x00U,
0x00U,
antCap, // ANTCAP[7:0] -- 0 = auto (FE_VARM/VARB), else forced value
0x00U, // ANTCAP[15:8] -- range is 0-128, high byte always 0
0x00U, // PROG_ID (AN649 ARG6; ignored when DIR_TUNE=0)
};
if (auto cmd = writeCommand(Command::FmTuneFreq, args, sizeof(args));
@@ -728,8 +728,10 @@ std::expected<void, Si4684Error> Si4684Driver::tuneFm(
static_cast<unsigned>(readKhz));
}
ESP_LOGI(kTag,
"FM tuned %u kHz rssi=%d dBuV snr=%d dB valid=%d readfreq=%u",
"FM tuned %u kHz antcap=%u rssi=%d dBuV snr=%d dB valid=%d "
"readfreq=%u",
static_cast<unsigned>(frequency.value()),
static_cast<unsigned>(antCap),
static_cast<int>(rsq->rssiDbuV),
static_cast<int>(rsq->snrDb),
static_cast<int>(rsq->valid),
@@ -15,6 +15,7 @@
#include "core/AudioProfileJson.hpp"
#include "esp_log.h"
#include "nvs.h"
#include <string>
@@ -23,6 +24,7 @@
namespace secure_store {
namespace {
constexpr char kTag[] = "NvsAudioProfileStore";
constexpr char kNamespace[] = "digiradio";
constexpr char kProfileKey[] = "audio_profile_json";
} // namespace
@@ -46,13 +48,23 @@ std::expected<void, core::StoreError> NvsAudioProfileStore::saveProfile(
const std::string json = core::serializeAudioProfileJson(profile);
nvs_handle_t handle = 0;
if (nvs_open(kNamespace, NVS_READWRITE, &handle) != ESP_OK) {
esp_err_t openErr = nvs_open(kNamespace, NVS_READWRITE, &handle);
if (openErr != ESP_OK) {
ESP_LOGW(kTag, "nvs_open failed (0x%x)", static_cast<unsigned>(openErr));
return std::unexpected(core::StoreError::IoFailed);
}
esp_err_t err = nvs_set_str(handle, kProfileKey, json.c_str());
if (err == ESP_OK) {
if (err != ESP_OK) {
ESP_LOGW(kTag, "nvs_set_str failed (0x%x) json_len=%u",
static_cast<unsigned>(err),
static_cast<unsigned>(json.size()));
} else {
err = nvs_commit(handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "nvs_commit failed (0x%x)",
static_cast<unsigned>(err));
}
}
nvs_close(handle);
@@ -13,10 +13,14 @@
#include "audio/AudioService.hpp"
#include "esp_log.h"
namespace audio {
namespace {
constexpr char kTag[] = "AudioService";
[[nodiscard]] core::AudioProfile profileForHardware(
const core::AudioProfile& profile) noexcept
{
@@ -59,7 +63,11 @@ std::expected<void, core::StoreError> AudioService::persistProfile() const
if (store_ == nullptr) {
return {};
}
return store_->saveProfile(profile_);
auto saved = store_->saveProfile(profile_);
if (!saved) {
ESP_LOGW(kTag, "persistProfile: NVS save failed");
}
return saved;
}
std::expected<void, core::StoreError> AudioService::applyProfileToDsp(
@@ -67,6 +75,7 @@ std::expected<void, core::StoreError> AudioService::applyProfileToDsp(
{
const core::AudioProfile hardware = profileForHardware(profile);
if (auto applied = dsp_.applyProfile(hardware); !applied) {
ESP_LOGW(kTag, "applyProfileToDsp: DSP safeload failed");
return std::unexpected(core::StoreError::IoFailed);
}
return {};
@@ -0,0 +1,127 @@
# Si4684 RF no-lock investigation — status report
**Date**: 2026-08-13
**Board**: PCBWay order W96157ASH49, U6 = Si4684-A10 (confirmed genuine, top marking `4684A10-2112AD254YZ-2112AD-E3`)
## Symptom
Si4684 (U6) boots, loads the ROM patch and FM/DAB application images, and answers
every SPI command correctly (CTS, boot sequence, property reads/writes all succeed).
FM and DAB tuning never completes: the STCINT status bit never sets, RSQ/DIGRAD
metrics stay at zero (RSSI=0, SNR=0, VALID=0, FIC quality=0, empty service list),
on both bands, at every frequency tried.
## What has been verified correct (do not re-litigate)
All cross-checked byte-by-byte against the official Skyworks documents
(`Hardware/DATASHEET/AN649.pdf`, `AN851_Schematics_Layout.pdf`):
- **Boot sequence**: RSTB# pulse, ROM patch stream, image stream, `BOOT` — matches
the AN649 flowchart exactly, on every boot, both bands.
- **Crystal / POWER_UP**: `XTAL_FREQ` = 19,200,000 exact (bytes `00 F8 24 01`,
little-endian `0x0124F800`), `CLK_MODE` = crystal mode (`0x17` → bits 5:4 = `01`).
Matches the physical ABM8-19.200MHZ-10-1-U-T crystal (U7) and BOM/schematic.
- **I2S**: Si4684 configured as I2S slave (`DIGITAL_IO_OUTPUT_SELECT = 0x0000`),
48 kHz; ADAU1701 is bus master. Confirmed via GPIO clock probe (LRCLK ≈ 48000 Hz,
BCLK ≈ 3.07 MHz) at every boot.
- **Front-end matching properties**: `FM/DAB_TUNE_FE_VARM` (0x1710),
`FM/DAB_TUNE_FE_VARB` (0x1711), and `FM/DAB_TUNE_FE_CFG`/VHFSW switch (0x1712,
value `0x0001` = closed) all match AN851's "Silicon Labs Recommended Front End
Network" table exactly (FM: `0xEDB5`/`0x01E3`; DAB: `0xF8A9`/`0x01C6`; switch
closed on both).
- **FM_TUNE_FREQ command**: all six ARG bytes decoded bit-by-bit against the AN649
command table (DIR_TUNE, TUNE_MODE, INJECTION, FREQ, ANTCAP, PROG_ID) — correct.
- **INT_CTL_ENABLE / INT_CTL_REPEAT** (STCIEN/STCREP, properties 0x0000/0x0001):
correct bit positions; confirmed these only gate the physical INTB pin, not the
STATUS0 STCINT bit our driver polls directly over SPI.
- **STC polling mechanism**: the same raw SPI byte (`pollRx[1]`) that reliably
reports CTS=1 (bit 7) across hundreds of successful commands also reports
STCINT=0 (bit 0) — the read path itself is proven reliable by the CTS side, so
the "never sets" result is a real hardware/firmware-image observation, not a
polling bug.
## Front-end network component mismatch (found this session, not the cause by itself)
The board's actual front-end network (`RF1 → C13(33pF) → L1(18nH) → C14(2.7pF
shunt) → L3(120nH shunt) → VHFI`, `L2(22nH)` bridging VHFI↔VHFSW) differs from
Silicon Labs' AN851 reference network the VARM/VARB constants were derived from
(`C1=33pF, L1=56nH, L2=120nH‖L3=120nH`). This was flagged as a plausible
contributor, then tested directly and ruled out as the *sole* cause (see below).
## Empirical sweeps (all negative — zero variation)
- **IBIAS/CTUN** (crystal startup calibration, POWER_UP ARG3/ARG8): 8 candidates
across the practical range, full reboot between each. No change.
- **ANTCAP** (FM_TUNE_FREQ ARG4/5, bypasses FE_VARM/VARB auto-tune entirely and
forces the on-chip antenna varactor directly, per AN851 Appendix A): ~100 of 128
possible values swept, across three antenna conditions (disconnected, loose
contact, directly soldered 70 cm — correct quarter-wave for FM). **Every single
attempt returned byte-identical RSQ raw data**
(`00 80 00 00 c0 00 00 00 00 00 00 00`, RSSI=0/SNR=0/VALID=0). If the RF path
were electrically functional, at least one of ~100 forced varactor values across
the full physical range should have produced resonance. None did.
- **Reset type**: software RSTB# vs. full USB power-cycle (15 s cold) — no change.
- **PCB continuity**: RF1 (antenna connector) → C13 → L1 → U6 pin 10 (VHFI)
confirmed intact with a multimeter (tested each leg separately to work around
C13's DC block). No broken trace/via.
## Leading hypothesis: QFN-48 exposed pad (EP) solder defect
U6 is a 7×7 mm QFN-48 with an exposed thermal/ground pad (pin 49, tied to GND).
Insufficient solder or voiding under this pad during reflow is a well-documented
QFN assembly failure mode that produces exactly this symptom: digital I/O
(peripheral pins, less ground-sensitive) works perfectly, while the RF/analog
front end (which references the exposed pad for a clean ground) fails entirely.
ESD was considered and set aside — the antenna input has ESD clamp protection
(D3, BAV99) ahead of the RF path, and no digital-side symptom consistent with ESD
damage (SPI glitches, crystal instability) has ever appeared.
**Action taken**: a technical report was sent to PCBWay (order W96157ASH49)
requesting an assembly quality review of U6's solder joints, specifically the
exposed pad, laying out the same evidence above (firmware ruled out by the
ANTCAP-bypasses-firmware argument).
**Action pending**: a manual hot-air reflow of U6 (re-melt only, no added
solder/paste) was planned as a lower-risk first attempt before considering full
chip removal and re-paste. Outcome not yet recorded in this document as of this
report's writing — update this section once attempted.
## Separate finding this session: audio profile save was broken, now partially fixed
Independent of the Si4684 investigation, while testing the internet radio
streaming feature (`components/services/webradio`), audio profile changes via
`PUT /api/audio/profile` and `POST /api/audio/reset` were found to fail
(`store_failed`) — which is why the ESP32 mixer channel (needed to hear the web
radio stream through the DSP mixer, muted at -96 dB by the default "radio-first"
mix) could not be un-muted via the API.
Root-caused and fixed: `sigma_i2c_write()` (`components/drivers/adau1701/src/SigmaStudioFW.c`)
had no retry on I2C transaction failure. A full EQ apply chains ~55 sequential
I2C transactions (5 bands × safeload block); a single transient NACK anywhere in
that burst aborted the whole sequence, while short bursts (e.g. the 2-write beep
toggle) reliably succeeded. Added a 3-attempt retry with a 2 ms backoff.
After the fix, DSP-side writes (mixer, EQ) succeed. A **second, separate**
failure remains: `NvsAudioProfileStore::saveProfile()` still fails, now isolated
to the NVS write step itself (not the DSP). Diagnostic logging was added
(`nvs_open`/`nvs_set_str`/`nvs_commit` error codes) to pin down the exact
`esp_err_t`; the leading suspicion is NVS partition space/fragmentation (the
`nvs` partition is only 24 KB, and this session alone did many repeated writes
across streaming config, beep toggles, Wi-Fi, and station data). **Not yet
confirmed with the actual error code — re-run the diagnostic build and capture
the log line to close this out.**
## Open items
1. Confirm the exact NVS error code for the audio-profile save failure and fix
accordingly (likely: erase/compact the `audio_profile_json` key, or address
partition fragmentation — do **not** perform a full NVS erase without explicit
confirmation, it would wipe Wi-Fi credentials, stations, and the saved BT
speaker pairing).
2. Record the hot-air rework outcome (RSSI response test) once attempted.
3. If rework doesn't change the symptom: escalate to full chip removal +
re-paste, or treat the PCBWay claim as the primary path forward.
4. Blob/firmware-image integrity check (in progress, separate task): verify
`GET_FUNC_INFO`/`GET_PART_INFO` revision strings and blob byte counts/hashes
for the FM and DAB images actually loaded, to rule out a corrupt or wrong
image as an alternative explanation to the EP hardware hypothesis.