diff --git a/Software/components/drivers/adau1701/src/Adau1701Driver.cpp b/Software/components/drivers/adau1701/src/Adau1701Driver.cpp index b049c16..e04977d 100644 --- a/Software/components/drivers/adau1701/src/Adau1701Driver.cpp +++ b/Software/components/drivers/adau1701/src/Adau1701Driver.cpp @@ -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(ADDR_STMIXER1_ST1_VOLUME), - mixer.mixRight); + if (auto result = safeloadGain( + static_cast(ADDR_STMIXER1_ST1_VOLUME), mixer.mixRight); + !result) + { + ESP_LOGW(kTag, "applyMixer: st1 safeload failed"); + return result; + } + return {}; } std::expected 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(i)); return result; } } diff --git a/Software/components/drivers/adau1701/src/SigmaStudioFW.c b/Software/components/drivers/adau1701/src/SigmaStudioFW.c index 7364376..cefd7c6 100644 --- a/Software/components/drivers/adau1701/src/SigmaStudioFW.c +++ b/Software/components/drivers/adau1701/src/SigmaStudioFW.c @@ -16,6 +16,7 @@ #include "driver/i2c_master.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" +#include "freertos/task.h" #include @@ -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; } /* diff --git a/Software/components/drivers/si4684/include/si4684/Si4684Driver.hpp b/Software/components/drivers/si4684/include/si4684/Si4684Driver.hpp index 9fab7f1..7f6dce1 100644 --- a/Software/components/drivers/si4684/include/si4684/Si4684Driver.hpp +++ b/Software/components/drivers/si4684/include/si4684/Si4684Driver.hpp @@ -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 tuneFm( - core::FrequencyKHz frequency); + core::FrequencyKHz frequency, std::uint8_t antCap = 0U); /** * @brief seekFm — seek FM in the given direction. diff --git a/Software/components/drivers/si4684/src/Si4684Driver.cpp b/Software/components/drivers/si4684/src/Si4684Driver.cpp index 3f65b7c..2abe566 100644 --- a/Software/components/drivers/si4684/src/Si4684Driver.cpp +++ b/Software/components/drivers/si4684/src/Si4684Driver.cpp @@ -690,7 +690,7 @@ Si4684Band Si4684Driver::loadedBand() const noexcept } std::expected 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 Si4684Driver::tuneFm( 0x00U, static_cast(chipFreq & 0xFFU), static_cast(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 Si4684Driver::tuneFm( static_cast(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(frequency.value()), + static_cast(antCap), static_cast(rsq->rssiDbuV), static_cast(rsq->snrDb), static_cast(rsq->valid), diff --git a/Software/components/secure_store/src/NvsAudioProfileStore.cpp b/Software/components/secure_store/src/NvsAudioProfileStore.cpp index 33aafd1..01c2b5a 100644 --- a/Software/components/secure_store/src/NvsAudioProfileStore.cpp +++ b/Software/components/secure_store/src/NvsAudioProfileStore.cpp @@ -15,6 +15,7 @@ #include "core/AudioProfileJson.hpp" +#include "esp_log.h" #include "nvs.h" #include @@ -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 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(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(err), + static_cast(json.size())); + } else { err = nvs_commit(handle); + if (err != ESP_OK) { + ESP_LOGW(kTag, "nvs_commit failed (0x%x)", + static_cast(err)); + } } nvs_close(handle); diff --git a/Software/components/services/audio/src/AudioService.cpp b/Software/components/services/audio/src/AudioService.cpp index c707543..b41fa3e 100644 --- a/Software/components/services/audio/src/AudioService.cpp +++ b/Software/components/services/audio/src/AudioService.cpp @@ -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 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 AudioService::applyProfileToDsp( @@ -67,6 +75,7 @@ std::expected 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 {}; diff --git a/Software/docs/si4684-rf-investigation-report.md b/Software/docs/si4684-rf-investigation-report.md new file mode 100644 index 0000000..0c73ec7 --- /dev/null +++ b/Software/docs/si4684-rf-investigation-report.md @@ -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.