Перейти к содержанию

Lab 11.42 — The last few percent: a frame-sync false lock

Goal

With the DC blocker fixed (Lab 11.41) the two-board Gardner link is ~92% clean. Find what the remaining ~6–8% of frames are, and close it — or prove it cannot be closed without an RTL change.

The failures are bimodal

The first move is not to tune anything; it is to look at what the dirty frames actually are. Splitting the locked frames by payload-error count:

  • ~92% decode perfectly;
  • ~2–3% carry a single wrong bit — ordinary channel noise, scattered across random indices;
  • ~2–3% carry ~117 of 256 bits wrong — 46%.

46% is not marginal SNR. A frame that is genuinely near the noise floor loses a handful of bits, not half of them. Half the bits wrong is a discrete event: the whole burst decoded in the wrong constellation rotation. The two error populations need separate explanations; averaging them into one "BER" hides the one that matters.

Where the gross errors sit

A 90° rotation and a mid-frame carrier slip both produce ~50% BER, so the error count cannot tell them apart. Their position can, and the gp_ctrl[15] telemetry reports it — which quarter fails first, and the index of the first error. Measured over the gross frames:

  • quarter 0 is clean in only ~1 of 17 gross frames;
  • the first error is in payload bits 0–31 in ~16 of 17.

The corruption starts at bit 0 and spans the whole frame. That rules out a mid-frame slip (which would leave an early clean region) and fixes the mechanism as a whole-burst wrong rotation from the very first symbol.

Every runtime lever, falsified

The receiver already resolves the QPSK 90° ambiguity with a four-rotation frame sync, so a wrong rotation means that resolution mis-fired. Before touching RTL, each runtime hypothesis was tested and killed by data:

Hypothesis Lever Result
inter-board CFO confuses acquisition coarse CFO gp_ctrl[13] worse — gross 5% → 16%, lock 100% → 84%
per-burst re-acquisition from reset costas_hold_phase gp_ctrl[11] no effect (3.0% → 3.3%)
a bad sampling phase sweep start_offset 0–7 flat; the earlier apparent correlation was noise
not enough SNR TX −38…−22 dBm U-shaped, minimum ~3% at −30 (noise below, compression above)
RX operating point RX gain 30–70 U-shaped, minimum ~3% at gain 30–50

Both power axes are U-shaped with a floor at the optimum, and no gate removes it. The floor is not a channel condition and not a tuning; it is structural.

The cause is arithmetic

The frame-sync arbitration (qpsk_ber_counter) is first-past-the-post over a sliding 24-bit correlation: the first branch to find a window whose preamble mismatch is ≤ LOCK_ERR_TOL owns the burst, and branch A has priority. The module comment assumed a wrong quadrant "mismatches ~12 of the 24 preamble bits, far outside LOCK_ERR_TOL, so exactly one branch ever acquires." That is true at the true frame position. It ignores the sliding search over the rest of the burst.

A wrong quadrant matches each preamble bit with probability ~½, so at any one of the ~140 sliding positions the chance of a window with ≤ LOCK_ERR_TOL mismatches is C(24, ≤tol) / 2²⁴. Over the burst:

python lab_11_42_ber_floor_lock_tolerance.py --predict
   LOCK_ERR_TOL=3: per-position 1.4e-4 -> per-burst 1.9e-2
   LOCK_ERR_TOL=2: per-position 1.8e-5 -> per-burst 2.5e-3
   LOCK_ERR_TOL=1: per-position 1.5e-6 -> per-burst 2.1e-4
   LOCK_ERR_TOL=0: per-position 6.0e-8 -> per-burst 8.3e-6

At the shipped LOCK_ERR_TOL=3 that is ~2% per burst — a false lock, and it matches the measured floor. The gross failures are branch A sliding over the noisy part of the burst and locking on a chance window before the true frame arrives.

The fix, and why it does not cost lock rate

Tighten LOCK_ERR_TOL to 1: the chance window drops ~100× to ~0.02% per burst. The obvious worry is the other side of the trade — a true preamble carrying a couple of noise errors would now fail to lock and the burst would be lost. Two things make that safe here:

  • the true preamble matches near-perfectly (the clean bursts have zero preamble errors), so it locks with room to spare at tol=1;
  • verified in simulation on the real self-OTA captures — both tb_qpsk_rx_costas and, crucially, tb_qpsk_costas_stress (which prepends 3000 noise samples before the frame, exactly the false-lock scenario) decode at BER 0/280 with LOCK_ERR_TOL=1. The tightened lock rejects the noise and still acquires the true frame.

Full block-5 suite: 35/35. The change is one parameter in the bridge instantiation, but it is backed by the arithmetic above and by the stress bench, not by a guess.

Hardware validation

Rebuilt at LOCK_ERR_TOL=1, redeployed on a cold boot, and re-run over A TX1 → 30 dB → B RX1 (1200 bursts across two runs):

tol=3 (baseline) tol=1 (this build)
clean frames ~92% 98.9% (1182/1195)
gross (whole-burst rotation) ~3% 0.75% (9/1195)
single-bit noise ~2% ~0.3%
lock rate ~100% 99.6% (5 lost of 1200)

The floor fell ~3–4× and the clean rate rose from ~92% to ~99%. The five lost frames are the intended trade: a burst whose true preamble carried ≥2 noise errors is now rejected rather than mis-decoded, and a lost frame contributes zero errors where a gross frame contributed ~117.

But the arithmetic promised ~100×, and hardware delivered ~4× — so the model was incomplete, and it is worth saying why rather than rounding it away. The ~2% spurious sliding-lock component did drop as predicted (to ~0.02%); what remains is a different wrong-rotation mechanism that LOCK_ERR_TOL cannot touch. Two candidates were checked:

  • preamble self-similarity under rotation — refuted offline: the 24-bit preamble mismatches its 90° and 270° rotations by exactly 12/24 and its 180° by 24/24, i.e. no easier-than-random wrong lock;
  • carrier-marginal bursts — the residual rate wandered 0.3%→1.0% between runs, tracking the slow inter-board CFO phase drift, which points at bursts where Costas settles near a 45° boundary and the preamble locks on the correct rotation but the payload rotates. That is a genuine acquisition event, not a false lock, and tightening the correlation does nothing to it.

So the honest result is a solid, well-understood ~4× reduction to a ~1% floor, not the 100× a single-mechanism model suggested. Pushing below ~1% would need a different tool — differential encoding or a payload-consistency gate — not another turn of the lock tolerance. Result: docs/assets/lab1142_ber_floor_live.json.

What this lab is really about

The DC-blocker bug was one block trusting a measurement taken in the wrong place. This one is one comment trusting an assumption at a single point — "the wrong branch can't lock" — that held at the true frame position and failed everywhere else the correlator looked. Both were closed the same way: stop averaging, split the failure population, and follow the one that carries the signal. Here the arithmetic of a sliding correlator predicted the exact floor before a single knob was turned.