From ef15e9839c04a78dc77288c3558ae04979694332 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Sun, 16 Aug 2026 00:19:43 +0200 Subject: [PATCH] Fix Si4684 FM/DAB tune commands: ARG offset bug caused total RF blackout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit writeCommand() always prepends a fixed ARG1=0x00 byte before the payload; tuneFm()/tuneDab() built their argument arrays starting at what the author believed was ARG1, so every byte landed one slot right of its real AN649 position and an extra unused byte was appended. The chip never received the requested frequency. This is the root cause of the months-long total RF blackout (RSQ frozen at all-zero on every frequency/ANTCAP value) previously attributed to a QFN exposed-pad hardware defect — that hypothesis is now overturned, confirmed live: RSSI/SNR now read real, frequency-dependent values with zero STC timeouts after the fix. Also make BT1035 boot failure non-fatal in HardwareBootstrap::boot() so a companion-chip fault no longer halts the whole device (Si4684 tuning, web UI, Wi-Fi already isolate BT1035 readiness via CompanionChipStatus). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR --- .../drivers/si4684/src/Si4684Driver.cpp | 11 +- .../docs/si4684-rf-investigation-report.md | 180 ++++++++++++++++++ Software/main/hardware_bootstrap.cpp | 21 +- 3 files changed, 200 insertions(+), 12 deletions(-) diff --git a/Software/components/drivers/si4684/src/Si4684Driver.cpp b/Software/components/drivers/si4684/src/Si4684Driver.cpp index 36efbc4..70c1ca3 100644 --- a/Software/components/drivers/si4684/src/Si4684Driver.cpp +++ b/Software/components/drivers/si4684/src/Si4684Driver.cpp @@ -742,8 +742,12 @@ std::expected Si4684Driver::tuneFm( return cleared; } const std::uint16_t chipFreq = kHzToChipFmFreq(frequency.value()); + // writeCommand() always prepends a fixed ARG1=0x00 (DIR_TUNE=0, + // TUNE_MODE=0, INJECTION=0), so this array starts at ARG2 (AN649 + // Command 0x30 table: ARG2=FREQ[7:0], ARG3=FREQ[15:8], ARG4=ANTCAP[7:0], + // ARG5=ANTCAP[15:8], ARG6=PROG_ID). Do not add a leading/trailing byte + // here or every field shifts into the wrong ARG slot. const std::uint8_t args[] = { - 0x00U, static_cast(chipFreq & 0xFFU), static_cast(chipFreq >> 8), antCap, // ANTCAP[7:0] -- 0 = auto (FE_VARM/VARB), else forced value @@ -1005,7 +1009,10 @@ std::expected Si4684Driver::tuneDab(std::uint8_t freqIndex) if (freqIndex >= kDefaultDabFrequencyKhz.size()) { return std::unexpected(Si4684Error::TuneFailed); } - const std::uint8_t args[] = {0x00U, freqIndex, 0x00U, 0x00U, 0x00U}; + // writeCommand() always prepends a fixed ARG1=0x00 (INJECTION=0), so + // this array starts at ARG2 (AN649 Command 0xB0 table: ARG2=FREQ_INDEX, + // ARG3=0x00 fixed, ARG4=ANTCAP[7:0], ARG5=ANTCAP[15:8]). + const std::uint8_t args[] = {freqIndex, 0x00U, 0x00U, 0x00U}; if (auto cmd = writeCommand(Command::DabTuneFreq, args, sizeof(args)); !cmd) { return std::unexpected(Si4684Error::TuneFailed); diff --git a/Software/docs/si4684-rf-investigation-report.md b/Software/docs/si4684-rf-investigation-report.md index f9096f2..0bbcf0b 100644 --- a/Software/docs/si4684-rf-investigation-report.md +++ b/Software/docs/si4684-rf-investigation-report.md @@ -261,3 +261,183 @@ after a soldering session, deterministic-then-intermittent) turned out to be 100% software. Worth remembering as a caution against over-attributing intermittent symptoms to hardware without exhausting the code-path diff against a known-good commit first. + +## 2026-08-15 update: front-end network mismatch quantified — does not explain the total blackout + +Follow-up on the "Front-end network component mismatch" section above (board +network `C13 33pF, L1 18nH, C14 2.7pF shunt, L3 120nH shunt, L2 22nH` vs +AN851's reference `C1 33pF, L1 56nH, L2‖L3 120nH‖120nH`). The mismatch was +flagged as a plausible contributor but never quantified. Real component +coordinates were pulled directly from `DigiRadio.kicad_pcb` (RF1 at +104.064,92.281; C13 111.811,92.281; L1 112.319,89.868; C14 114.097,91.519; L3 +115.621,89.868; L2 115.621,91.9; U6 121.717,90.122 — confirming the network's +physical path and component identity), then modeled as a two-port ABCD chain +(series C13 → series L1 → shunt bank C14‖L3‖L2 at the VHFI node), 50 Ω +reference on both ports. This is a lumped-element approximation: it ignores +PCB trace parasitics, the chip's real complex input impedance at VHFI, and +antenna radiation — good for an order-of-magnitude comparison against the +AN851 reference network, not an absolute number. + +(A pure EM/gerber-based simulation via `gerber2ems`/openEMS, initially +considered, was ruled out for this specific question: per its own +documentation, `gerber2ems` does not model discrete capacitors/inductors — +"capacitors are not simulated... they can be approximated by shorting them +using a trace" — which would misrepresent a network that is almost entirely +discrete L/C components.) + +**Result** (S21 = insertion loss, S11 = return loss, board network vs AN851 +reference): + +| Band | Board S21 | Reference S21 | Board S11 | Reference S11 | +|---|---|---|---|---| +| FM 87.5–108 MHz | −7.1 to −9.8 dB | −1.2 to −1.5 dB | −0.5 to −0.9 dB | −5.4 to −6.3 dB | +| DAB 174–240 MHz | −2.4 to −3.4 dB | −2.0 to −3.0 dB | −2.7 to −3.7 dB | −3.1 to −4.4 dB | + +**FM**: the board network carries a real 6–9 dB insertion-loss penalty over +the reference network — worth correcting, but not by itself the kind of loss +that silences a strong local FM station on a working receiver (10 dB of +front-end loss is routinely tolerated). + +**DAB**: the board network is within ~0.3–1.4 dB of the reference network — +essentially the same insertion loss. The mismatch is not a meaningful factor +at DAB frequencies at all. + +**Conclusion**: since DAB shows the identical total-blackout signature as FM +(RSSI/SNR/VALID all zero, unmoved by ~100 ANTCAP sweep values) despite the +front-end mismatch being nearly irrelevant in that band, the network mismatch +cannot be the primary cause of the observed failure on its own. This is a +quantitative point in favor of the existing QFN exposed-pad hypothesis (§ +"Leading hypothesis" above), not a competing explanation — it narrows, rather +than replaces, the open items in that section. + +## 2026-08-16 update: hot-air reflow attempted — no change to RF symptom + +The manual hot-air rework of U6 (re-melt only, no added solder/paste) flagged +as "action pending" in the Leading hypothesis section was carried out: 100°C +for 1 minute, then 220°C for 1.5 minutes, low airflow. + +Post-rework, on a fresh build/flash of the current firmware, FM tuning was +retested at three frequencies (100.9, 95.0, 87.9 MHz) via `POST +/api/tuner/tune`. Result: **byte-for-byte identical to every pre-rework +capture in this report.** + +``` +Si4684: STC timeout: last poll spi_err=0 status=12 c0 00 00 c0 INTB=1 +Si4684: FM tune STC timeout at 95000 kHz — settling 150 ms +Si4684: FM RSQ raw: 00 80 00 00 c0 00 00 00 00 00 00 00 +Si4684: FM tuned 95000 kHz antcap=0 rssi=0 dBuV snr=0 dB valid=0 readfreq=0 +``` + +Same at 100.9 and 87.9 MHz. RSSI/SNR/VALID all zero, `locked=false`, no +variation from the reflow. + +**Item 2/3 (rework outcome) in "Open items" above is now closed: attempted, +no effect.** This does not rule out the QFN exposed-pad hypothesis — a +re-melt without added paste/flux does not reliably resolve a voiding defect +under an exposed pad (only adds heat to already-present solder, doesn't add +volume where a void is) — but it does mean the easy, low-risk fix attempt is +exhausted. Remaining paths are the non-destructive diagnostics proposed this +session (mechanical flex test with live RSSI monitoring, controlled thermal +stress test with live RSSI monitoring, NanoVNA S11 sweep at RF1 chip-on vs +chip-off) or escalating to X-ray/full chip removal, neither attempted yet. + +## 2026-08-16 update: root cause found — FM_TUNE_FREQ/DAB_TUNE_FREQ argument-offset bug, not hardware + +**This overturns the QFN exposed-pad hypothesis above.** The actual cause of +the months-long "total RF blackout" was a software bug in +`Si4684Driver::tuneFm()`/`tuneDab()`, found by diffing our command +construction against the official AN649 Command 0x30 (FM_TUNE_FREQ) and +Command 0xB0 (DAB_TUNE_FREQ) argument tables directly (page-level read of +`Hardware/DATASHEET/AN649.pdf`, not driver comments), prompted by cross- +referencing against the independent `hitech95/si468x_dab_receiver` Linux +driver. + +`Si4684Driver::writeCommand()` always prepends a fixed `ARG1 = 0x00` byte +before whatever payload array is passed to it: + +```cpp +buffer[0] = static_cast(cmd); +buffer[1] = 0x00U; // ARG1, always +std::memcpy(buffer.data() + 2U, payload, length); // ARG2 onward +``` + +`POWER_UP` and `HOST_LOAD` callers already accounted for this correctly +(their arrays are written starting at ARG2). **`tuneFm()` and `tuneDab()` +did not** — both built their argument arrays starting at what the author +believed was ARG1, so every byte actually landed one slot to the right of +where it belongs, with an extra unused byte tacked on the end: + +- **FM_TUNE_FREQ** (AN649 Command 0x30): real layout is ARG2=FREQ[7:0], + ARG3=FREQ[15:8], ARG4=ANTCAP[7:0], ARG5=ANTCAP[15:8], ARG6=PROG_ID. Our + code sent FREQ's low byte into ARG3 (should be the high byte), the actual + frequency low byte was always sent as a fixed `0x00`, and the ANTCAP value + landed in ARG5 (the *high* byte of a 0–128-range field) instead of ARG4. + **The chip was never told the requested frequency** — it received a + garbage FREQ value derived from shifted bytes, and the ANTCAP sweep + documented earlier in this report (~100 values, byte-identical results) + was sweeping the wrong byte entirely, which is exactly why it never + produced any variation. +- **DAB_TUNE_FREQ** (AN649 Command 0xB0): same shift. `FREQ_INDEX` (real + ARG2) was always sent as `0x00`; the actual requested index landed in + ARG3, which the spec requires to be a fixed `0x00`. + +Fixed in `components/drivers/si4684/src/Si4684Driver.cpp`, `tuneFm()` and +`tuneDab()`: removed the extra leading byte and the extra trailing byte so +the arrays start at the real ARG2. + +**Result, live on hardware immediately after the fix** (`POST +/api/tuner/tune`, no other change — same antenna, same board, no rework +involved in this result): + +``` +Si4684: FM RSQ raw: 00 81 80 00 c0 00 02 2e 22 8d fb fd +Si4684: FM tuned 87500 kHz antcap=0 rssi=-5 dBuV snr=-3 dB valid=0 readfreq=87500 +``` + +RSSI/SNR now read real, varying, frequency-dependent values (e.g. −13 to +4 +dBuV across a 10-point FM sweep, peaking near a plausible local station at +98.5 MHz) instead of the fixed `00 80 00 00 c0 00 00 00 00 00 00 00` / +all-zero pattern seen in every capture in this report until now. **No STC +timeout occurred in any tune or seek attempt after the fix** — every prior +capture in this document logged one on every single attempt. + +`locked`/`valid` is still `false` in this test — expected with the +board's improvised antenna and not yet investigated further; that is now an +ordinary sensitivity/antenna question, not a "chip never responds to RF" +question. DAB was retested at freq_index=10 with no station found +(`fic_quality=0`, `cnr_db=0`) but also with no STC timeout — most likely no +active multiplex at that index/location, to be swept properly with a real +antenna as a follow-up, not evidence against the fix (which addresses the +identical byte-shift bug in both commands). + +**What this means for the rest of the investigation**: the QFN exposed-pad +hypothesis, the front-end network mismatch analysis, the hot-air reflow, and +the mechanical flex test were all investigating a symptom that had a +software cause. None of that work was wasted — the empirical rigor (ANTCAP +sweep producing zero variation, DAB and FM failing identically) is exactly +what made this bug's fingerprint recognizable once the actual command bytes +were checked against the primary spec instead of trusted from driver +comments. The lesson: `writeCommand()`'s implicit ARG1 prepend is an easy +trap for future commands — any new caller must remember its array starts at +ARG2, not ARG1. + +**Follow-up**: get a proper antenna connected and confirm an actual station +lock (`valid=1`) on both FM and DAB; audit other `writeCommand()` call sites +in `Si4684Driver.cpp` for the same off-by-one pattern (POWER_UP and +HOST_LOAD were checked and are correct; FM_SEEK, property writes, and RSQ/ +DIGRAD status reads have not yet been re-verified against AN649 page text). + +**Unrelated finding from the same session, logged for completeness**: BT1035 +began failing boot deterministically (`no spontaneous UART bytes after +hardware reset`, then `AT init failed`) starting from this session, on both +the firmware build that predates and the one that includes the boot-sequence +fix from `fd9d4ae` — ruling out that fix's absence as the cause. Extending +the diagnostic listen window from 3.5 s to 12 s (temporary, reverted) +produced zero bytes either way, confirming this is not the previously-fixed +"banner arrives late" timing issue but a harder, total UART silence. The +BT1035 module was not physically touched during the U6 rework. Cause not +yet identified; unrelated to the Si4684 investigation (separate chip), but +`HardwareBootstrap::boot()` was changed (`main/hardware_bootstrap.cpp`) to +treat BT1035 boot failure as non-fatal rather than halting the whole device, +so the rest of the system (Si4684 tuning, web UI, Wi-Fi) remains usable +while this is investigated separately. diff --git a/Software/main/hardware_bootstrap.cpp b/Software/main/hardware_bootstrap.cpp index cd65852..2a94568 100644 --- a/Software/main/hardware_bootstrap.cpp +++ b/Software/main/hardware_bootstrap.cpp @@ -127,17 +127,18 @@ std::expected HardwareBootstrap::boot() } if (auto btResult = gBt1035.boot(); !btResult) { - ESP_LOGE(kTag, "BT1035 boot failed"); - return std::unexpected(HardwareBootError::Bt1035BootFailed); - } + ESP_LOGE(kTag, "BT1035 boot failed — continuing without Bluetooth"); + } else { + if (auto nameResult = + gBt1035.setDeviceName(gDeviceIdentity.bluetoothName()); + !nameResult) { + ESP_LOGW(kTag, "BT1035 device name set failed"); + } - if (auto nameResult = - gBt1035.setDeviceName(gDeviceIdentity.bluetoothName()); !nameResult) { - ESP_LOGW(kTag, "BT1035 device name set failed"); - } - - if (auto reconnectResult = gBt1035.setAutoReconnect(3U); !reconnectResult) { - ESP_LOGW(kTag, "BT1035 auto-reconnect set failed"); + if (auto reconnectResult = gBt1035.setAutoReconnect(3U); + !reconnectResult) { + ESP_LOGW(kTag, "BT1035 auto-reconnect set failed"); + } } gReady = true;