diff --git a/RadioController.pas b/RadioController.pas index 8cd5e47..ea65100 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -324,6 +324,7 @@ type FBeaconTrackHz: Double; // измеренная АБСОЛЮТНАЯ display-частота маяка // (наведение + остаток декодера) — для оранж. маркера FBeaconSNRdB: Double; // SNR из последнего снимка декодера (индикация лока) + FBeaconBal: Double; // EMA доли меньшинства знаков I (BPSK-ность) FBeaconLocked: Boolean; // декодер солидно залочен (гистерезис по SNR) FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка ретюна LO) FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков) @@ -3300,6 +3301,17 @@ const // --- метрика лока (берём из снимка декодера: CarrierLock + SNR) --- BEACON_LOCK_SNR_DB = 3.0; // солидный лок: декодер несёт несущую и SNR выше этого BEACON_UNLOCK_SNR_DB = 0.0; // гистерезис: лок гаснет ниже этого + // --- «это точно BPSK, а не голая несущая» --- + // CarrierLock+SNR подтверждают лишь то, что в полосе есть узкая линия и Costas + // держит фазу. Ровная несущая (телеграф, пораженка, остаток несущей у цифры) даёт + // ровно это, причём с ОТЛИЧНЫМ SNR: захваченная Costas'ом, она лежит целиком на + // оси I, Q пустой. Контур тогда честно уводил LOError, подтягивая чужой сигнал к + // опорной частоте маяка. Различаем по констелляции: у настоящего BPSK точки ходят + // между двумя сгустками ±I (данные после свёрточного кодера равновероятны), у + // несущей все точки в одном. Меряем долю меньшинства по знаку I. + BEACON_BAL_MIN = 0.15; // ниже — на BPSK не похоже, лок не даём + BEACON_BAL_DROP = 0.08; // гистерезис: уже взятый лок гаснет ниже этого + BEACON_BAL_MIN_PTS = 64; // меньше точек в кольце — судить рано procedure TRadioController.SetBeaconRefHz(Hz: Double); begin @@ -3319,6 +3331,8 @@ begin FBeaconDec.SetOffsetHz(DisplayHz - FCenterFreq); FBeaconDec.Reseed; // одноразовый грубый захват частоты заново end; + FBeaconBal := 0.0; // новое наведение — BPSK-ность доказывается заново + FBeaconLocked := False; FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать декодеру захватиться до 1-й коррекции Changed(rfBeaconLock); end; @@ -3335,6 +3349,7 @@ begin FBeaconCorrCooldown := 0; FBeaconSaveCooldown := 0; FBeaconLocked := False; + FBeaconBal := 0.0; FBeaconSNRdB := -99.0; FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError несёт калибровку) FBeaconDecHz := 0.0; // ещё не наведён — ждём клик по маяку @@ -3354,6 +3369,20 @@ begin Changed(rfBeaconLock); end; +function BeaconIQBalance(const S: TBeaconScope): Double; +// Доля меньшинства среди знаков I в кольце констелляции: 0.5 — идеально +// сбалансированный BPSK, 0 — все точки в одном сгустке (немодулированная +// несущая). -1 = точек мало, судить рано. +var + i, Neg: Integer; +begin + if S.Count < BEACON_BAL_MIN_PTS then Exit(-1.0); + Neg := 0; + for i := 0 to S.Count - 1 do + if S.PtI[i] < 0 then Inc(Neg); + Result := Min(Neg, S.Count - Neg) / S.Count; +end; + procedure TRadioController.ServiceBeaconLock; // Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Эталон частоты — ДЕКОДЕР: // он точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload), и его @@ -3365,7 +3394,7 @@ procedure TRadioController.ServiceBeaconLock; // baseband-смещение ≈ 0: маяк сдвинулся на −dLock железом, наведение тоже на −dLock). var S: TBeaconScope; - residHz, measured, e, dLock: Double; + residHz, measured, e, dLock, Bal: Double; begin if not FBeaconLockOn then Exit; if not Assigned(FBeaconDec) then Exit; @@ -3382,9 +3411,17 @@ begin FBeaconDec.GetScope(S); FBeaconSNRdB := FBeaconSNRdB + 0.2 * (S.SNRdB - FBeaconSNRdB); - // Солидный лок: декодер несёт несущую И SNR выше порога (гистерезис против мерцания). - if S.CarrierLock and (FBeaconSNRdB >= BEACON_LOCK_SNR_DB) then FBeaconLocked := True - else if FBeaconSNRdB < BEACON_UNLOCK_SNR_DB then FBeaconLocked := False; + // Доля меньшинства по знаку I: у BPSK ≈0.5, у голой несущей ≈0. Кольцо в 256 + // точек при 400 Bd набирается за ~0.6 с, EMA добавляет ещё столько же — то есть + // чужой сигнал отваливается примерно за секунду, а не за десяток (кадр FEC). + Bal := BeaconIQBalance(S); + if Bal >= 0 then FBeaconBal := FBeaconBal + 0.2 * (Bal - FBeaconBal); + // Солидный лок: декодер несёт несущую, SNR выше порога И картинка похожа на + // BPSK (гистерезис по обоим — против мерцания). + if S.CarrierLock and (FBeaconSNRdB >= BEACON_LOCK_SNR_DB) + and (FBeaconBal >= BEACON_BAL_MIN) then FBeaconLocked := True + else if (FBeaconSNRdB < BEACON_UNLOCK_SNR_DB) + or (FBeaconBal < BEACON_BAL_DROP) then FBeaconLocked := False; // Остаток несущей декодера (Гц, относительно наведения). invert выравнивает ось // декодера с дисплеем → display_маяка = наведение + остаток (знак BEACON_RESID_SIGN).