|
| 1 | +# CFO Phase Hardening Plan (Pre-Demod Correction Path) |
| 2 | + |
| 3 | +Date: 2026-02-12 |
| 4 | +Status: Completed (Phase 1, Phase 2, and Phase 3 implemented and validated) |
| 5 | +Owner: DSP/Modem path |
| 6 | + |
| 7 | +## Objective |
| 8 | + |
| 9 | +Harden CFO correction for connected OFDM flows so chirp/LTS-derived CFO is applied with the correct phase reference before demodulation, and maintain consistency when residual CFO triggers LTS reprocessing. |
| 10 | + |
| 11 | +Primary target: reduce sync-induced decode failures and control-path ACK loss driven by CFO/phase mismatch on fading channels. |
| 12 | + |
| 13 | +## Current Findings (Code Reality) |
| 14 | + |
| 15 | +1. CFO is already corrected before demod in OFDM paths: |
| 16 | + - `OFDMChirpWaveform::process()` calls `setFrequencyOffsetWithPhase()` before `processPresynced()`. |
| 17 | + - `OFDMDemodulator::Impl::toBaseband()` applies per-sample phase rotation using `freq_offset_hz` and `freq_correction_phase`. |
| 18 | + |
| 19 | +2. The absolute training-position hook exists but is not wired: |
| 20 | + - `IWaveform::setAbsoluteTrainingPosition(size_t)` exists. |
| 21 | + - `StreamingDecoder` never calls it. |
| 22 | + - OFDM waveforms currently compute initial phase from buffer-relative training offsets. |
| 23 | + |
| 24 | +3. Residual CFO reprocess path resets phase origin to zero: |
| 25 | + - In `estimateChannelFromLTS()`, if residual CFO is detected, code reprocesses training after `mixer.reset(); freq_correction_phase = 0.0f;` |
| 26 | + - This can lose the initial phase baseline for the current frame. |
| 27 | + |
| 28 | +## Implementation Plan |
| 29 | + |
| 30 | +### Phase 1: Absolute Training Position Plumbing (Implement now) |
| 31 | + |
| 32 | +1. `StreamingDecoder`: |
| 33 | + - On successful sync, compute absolute sample index for `sync_position_`. |
| 34 | + - Call `waveform_->setAbsoluteTrainingPosition(abs_sync_training_start)`. |
| 35 | + - Keep this per-frame and independent from ring-buffer wrap. |
| 36 | + |
| 37 | +2. `OFDMChirpWaveform` and `OFDMNvisWaveform`: |
| 38 | + - Override `setAbsoluteTrainingPosition(size_t)`. |
| 39 | + - Store absolute training start for phase initialization. |
| 40 | + - In `process()`, use absolute training sample for initial CFO phase. |
| 41 | + - Fallback to legacy relative offset only if absolute position is not provided. |
| 42 | + |
| 43 | +Acceptance: |
| 44 | +- Build succeeds. |
| 45 | +- Logs show absolute phase reference being used in OFDM process path. |
| 46 | +Implementation note (2026-02-12): |
| 47 | +- Done. `StreamingDecoder` now maps ring index -> absolute sample index at sync and calls `waveform_->setAbsoluteTrainingPosition(...)`. |
| 48 | +- Done. `OFDMChirpWaveform` and `OFDMNvisWaveform` now consume absolute training position for initial CFO phase. |
| 49 | + |
| 50 | +### Phase 2: Residual CFO Reprocess Phase Baseline (Implement now) |
| 51 | + |
| 52 | +1. `channel_equalizer.cpp`: |
| 53 | + - Capture `freq_correction_phase` at training-start before first LTS pass. |
| 54 | + - If residual CFO correction triggers reprocess, reset mixer and restore that captured phase baseline (not zero). |
| 55 | + |
| 56 | +Acceptance: |
| 57 | +- Build succeeds. |
| 58 | +- Residual CFO correction path preserves phase continuity assumptions. |
| 59 | +Implementation note (2026-02-12): |
| 60 | +- Done. LTS residual CFO reprocess now restores the captured training-start phase baseline instead of forcing phase=0. |
| 61 | + |
| 62 | +### Phase 3: Connected LTS Sync Metric Upgrade (Next step) |
| 63 | + |
| 64 | +1. Replace/augment current real-only training autocorrelation in `detectDataSync()` with CFO-aware metric (complex/phase-compensated), rather than cosine-only normalization. |
| 65 | +2. Validate detection robustness under ±20..30 Hz CFO and fading. |
| 66 | + |
| 67 | +Acceptance: |
| 68 | +- Lower sync reject streaks on fading + nonzero CFO. |
| 69 | +- No regression in clean AWGN runs. |
| 70 | +Implementation note (2026-02-12): |
| 71 | +- Done. `OFDMChirpWaveform::detectDataSync()` now uses Hilbert/analytic complex correlation for LTS detection instead of real-only autocorrelation. |
| 72 | +- Done. Burst-marker sign detection now uses CFO-phase-compensated complex correlation (`best_p * exp(-j*phi_cfo)`), improving robustness when CFO is non-zero. |
| 73 | +- Quick sanity validation (5 seeds, SNR=20, good fading, R2/3): 5/5 PASS. |
| 74 | + |
| 75 | +## Validation Matrix (Post-Implementation) |
| 76 | + |
| 77 | +1. Simulator (connected OFDM-CHIRP): |
| 78 | + - SNR 20, fading good/moderate, rate DQPSK R2/3, seeds 30. |
| 79 | + - Add nonzero CFO scenario (fixed and random, if exposed in harness). |
| 80 | + |
| 81 | +2. Metrics to compare: |
| 82 | + - First-attempt frame success. |
| 83 | + - ACK reception success. |
| 84 | + - Retransmissions/timeouts. |
| 85 | + - Sync reject streak count. |
| 86 | + - Logged CFO evolution (chirp -> residual -> pilot corrected). |
| 87 | + |
| 88 | +3. OTA sanity check: |
| 89 | + - Confirm `[MODE]` / logs do not show unstable CFO jumps frame-to-frame under stable channel. |
| 90 | + |
| 91 | +## Risks / Notes |
| 92 | + |
| 93 | +1. Do not apply a second external pre-rotation in front of OFDM demod unless carefully gated; OFDM demod already performs CFO correction internally. |
| 94 | +2. MC-DPSK path is separate and should not be changed by this phase plan. |
| 95 | +3. Keep behavior backward-compatible for paths that do not provide absolute training position. |
| 96 | + |
| 97 | +## Completion Evidence (2026-02-12) |
| 98 | + |
| 99 | +1. Deterministic chain verification: |
| 100 | + - `./tests/verify_cfo_chain.sh --cfo 50 --channel awgn --snr 20 --seed 42` |
| 101 | + - Result: pass, with applied correction matching expected `-CFO` within tolerance across all dumps. |
| 102 | +2. Regression sanity: |
| 103 | + - `./build/cli_simulator --snr 20 --fading good --rate r1_2 --seed 42 --test` |
| 104 | + - Result: pass (no regression from hardening changes). |
| 105 | +3. Internal proof path: |
| 106 | + - `ULTRA_DUMP_CFO_PREFIX` + `ULTRA_DUMP_CFO_CALLS` dump pre/post corrected samples from `toBaseband()` for direct offline validation. |
0 commit comments