Files
ewsdr/BeaconDecoder.pas
T
ew8bakandClaude Opus 4.8 c063d7a197 feat: QO-100 central beacon BPSK decoder (Milestone 1 — stable demod)
Декодер центрального маяка QO-100 (10489.750, AO-40 BPSK 400 бод): фронтенд
до мягких символов + констелляция, с устойчивым захватом несущей/символа.

Цепочка (BeaconDecoder.pas): NCO-снос смещения → boxcar-децим до 9600 →
RRC матч-фильтр → AGC-нормировка → Gardner timing → Costas BPSK с frequency
offload в пред-RRC NCO. squaring-FFT оставлена индикатором несущей (prom).

Ключевые решения по итогам эфирной отладки:
- invert=True для тракта QO-100 (LNB+трансвертер зеркалит спектр).
- Декодер развязан от beacon-lock (тот ретюнил железо, рвал IQ).
- AGC-нормировка петель: сигнал ~-60 dBFS замораживал Costas/Gardner.
- Frequency offload: накопл. снос Costas стекает в FResidInc (без потолка
  ±π/символ) → держит ошибку наведения (1пкс≈300Гц) + дрейф LNB.

Интеграция: тап IQ в WDSPEngine.PushIQItemToDSP; RadioController владеет
декодером, клик-наведение (ПКМ BEACON + ЛКМ/Shift+ЛКМ по спектру); узкий
фильтр-маркер в SpectrumView/GL; окно констелляции BeaconScopeForm; файловый
лог HOME/ewsdr_beacon.log для диагностики.

Milestone 2 (далее): FEC — Viterbi k=7 + RS(160,128) + интерливер + дескремблер.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 18:15:00 +03:00

690 lines
27 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit BeaconDecoder;
{
QO-100 central beacon — DSP front-end (Milestone 1).
Средний маяк QO-100 (10489.750) = AMSAT AO-40 FEC телеметрия: BPSK 400 бод.
Этот юнит делает ТОЛЬКО демодуляцию до мягких символов и снимает констелляцию
для визуализации/диагностики. FEC-бэкенд (Viterbi k=7 + RS(160,128) + интерливер
+ дескремблер) подключается отдельно (libfec Карна) на следующем этапе.
Цепочка (вход — комплексный baseband IQ на FSampleRate из DDC, после beacon-lock):
NCO-снос смещения маяка к DC
→ boxcar-дециматор до ~9600 Гц (целое D)
→ RRC матч-фильтр (beta 0.5)
→ Gardner TED + линейный интерполятор (символьная синхра 400 бод)
→ Costas (BPSK, 2-й порядок) (фаза/несущая)
→ мягкий символ sign(I), констелляция, метрики (SNR, lock).
Feed() зовётся из DSP-потока (WDSPEngine.PushIQItemToDSP), снимок читается из UI —
обмен под критической секцией, как в прежнем BeaconLock.
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, SyncObjs, Math;
const
BCN_BAUD = 400.0; // символьная скорость маяка
BCN_TARGET_FS = 9600.0; // целевой rate после децимации
BCN_RRC_BETA = 0.5; // roll-off матч-фильтра
BCN_RRC_SPAN = 8; // длина RRC в символах (полу-)
BCN_SCOPE_PTS = 256; // точек констелляции в снимке
BCN_NFFT = 4096; // FFT грубой carrier-acquisition (~0.43 c @ 9600)
BCN_NFFT_LOG2 = 12;
type
// Снимок состояния для UI (констелляция + метрики). Копируется под CS.
TBeaconScope = record
Enabled: Boolean;
Count: Integer; // заполнено точек [0..BCN_SCOPE_PTS]
PtI, PtQ: array[0..BCN_SCOPE_PTS-1] of Single; // нормированные символы
SNRdB: Single;
SymRate: Single; // измеренная скорость, ~400
CarrierLock: Boolean;
SymbolLock: Boolean;
OffsetHz: Double; // текущий снос NCO
ResidHz: Single; // найденный остаточный снос несущей
CarProm: Single; // prominence линии несущей (squaring-FFT)
FsHz: Single; // rate IQ-тапа (диагностика)
FdecHz: Single; // rate после децимации
Sps: Single; // отсчётов на символ
DecRMS: Single; // RMS децимированного сигнала (до RRC)
SymMag: Single; // |символ| после RRC (lock-независимо)
TimErr: Single; // EMA |ошибки Gardner| (таймер-петля)
Wsym: Single; // дробная поправка периода символа
CarFreqHz: Single; // частота Costas (Гц, остаток несущей)
LockHold: Integer; // холдовер-счётчик лока
end;
TBeaconDecoder = class
private
FLock: TCriticalSection;
FEnabled: Boolean;
// --- параметры rate ---
FFs: Double; // вход (DDC) Гц
FDecim: Integer; // целая децимация
FFdec: Double; // rate после децимации
FSps: Double; // отсчётов на символ после децимации
// --- NCO сноса ---
FOffsetHz: Double;
FNcoPh: Double;
FNcoInc: Double;
FInvert: Boolean; // зеркальный baseband (QO-100/LNB): сопряжение IQ
// --- boxcar дециматор ---
FAccI, FAccQ: Double;
FAccN: Integer;
// --- RRC матч-фильтр ---
FRRC: array of Double;
FHistI: array of Double; // кольцевой буфер decimated I
FHistQ: array of Double;
FHistPos: Integer;
FNtaps: Integer;
// --- Gardner timing ---
FMuAcc: Double; // обратный отсчёт до следующего полу-символьного строба
FHalf: Boolean; // текущий строб = середина (true) или центр символа
FMidI, FMidQ: Double; // отсчёт середины (для Gardner)
FPrevI, FPrevQ: Double; // прошлый центр символа
FYd0I, FYd0Q: Double; // линия задержки RRC-выхода (новейший)
FYd1I, FYd1Q: Double; // предыдущий
FWsym: Double; // дробная коррекция периода (интегратор)
FTimErrEMA: Double;
// --- Costas (BPSK) ---
FCarPh: Double;
FCarFreq: Double;
FCarAlpha: Double; // proportional
FCarBeta: Double; // integral
// --- грубая carrier-acquisition (squaring-FFT) ---
// BPSK² снимает модуляцию → линия на 2·Δf. Находит подавленную несущую
// независимо от знака сноса (алиасинг сворачивает её в полосу), игнорируя
// DC-offset Pluto (вычитаем среднее перед возведением в квадрат).
FAcqRe, FAcqIm: array of Double; // буфер декадированных отсчётов (NFFT)
FAcqPos: Integer;
FResidDf: Double; // накопленный остаточный снос несущей (Гц)
FResidPh: Double;
FResidInc: Double;
FCarProm: Double; // prominence линии (EMA)
FCarPresent: Boolean; // линия несущей видна
FFreqLocked: Boolean; // Costas держит фазу (для индикации/symbol-lock)
FLockHold: Integer; // холдовер-счётчик лока (анти-мерцание)
// --- FLL (частотная петля Costas, decision-directed) ---
FFllGain: Double;
FPrevRre, FPrevRim: Double; // прошлый символ с убранной модуляцией
FBitRev: array of Integer; // бит-реверс FFT
FTwRe, FTwIm: array of Double;// твидлы
FFre, FFim: array of Double; // рабочие буферы FFT
// --- метрики ---
FSigAmp: Double; // EMA |I|
FQVar: Double; // EMA Q^2
FConstScale: Double;
FDecRMS: Double; // EMA RMS децимированного сигнала (диагностика тракта)
FSymMag: Double; // EMA |символ| после RRC (диагностика тракта)
FRrcAmp: Double; // AGC: EMA амплитуды выхода RRC (нормировка петель)
// --- констелляция (кольцо) ---
FRingI, FRingQ: array[0..BCN_SCOPE_PTS-1] of Single;
FRingPos: Integer;
FRingCnt: Integer;
procedure BuildRRC;
procedure BuildFFT;
procedure FFTInplace;
procedure RunAcquisition;
procedure ProcessDecimated(const xi, xq: Double);
procedure ProcessSymbol(const zi, zq: Double);
procedure ResetDSP;
public
constructor Create;
destructor Destroy; override;
procedure Configure(SampleRateHz: Double); // при смене rate
procedure SetOffsetHz(Hz: Double); // снос маяка к DC (display center)
procedure SetInvert(On_: Boolean); // зеркальный baseband (инверсия спектра)
function Inverted: Boolean;
procedure Reseed; // переинициализация петель (новое наведение)
procedure SetEnabled(On_: Boolean);
property Enabled: Boolean read FEnabled;
// Из DSP-потока, по одному комплексному отсчёту на FSampleRate.
procedure Feed(const I, Q: Double); inline;
procedure GetScope(out S: TBeaconScope);
end;
implementation
constructor TBeaconDecoder.Create;
begin
inherited Create;
FLock := TCriticalSection.Create;
FFs := 0;
FEnabled := False;
FConstScale := 1.0;
Configure(192000);
end;
destructor TBeaconDecoder.Destroy;
begin
FLock.Free;
inherited Destroy;
end;
procedure TBeaconDecoder.BuildRRC;
// Root-raised-cosine матч-фильтр, длина BCN_RRC_SPAN символов, нормировка по энергии.
var
i, mid: Integer;
t, beta, num, den, h, ssum: Double;
begin
beta := BCN_RRC_BETA;
FNtaps := Round(BCN_RRC_SPAN * FSps);
if FNtaps < 8 then FNtaps := 8;
if (FNtaps and 1) = 0 then Inc(FNtaps); // нечётное → симметрично
SetLength(FRRC, FNtaps);
mid := FNtaps div 2;
ssum := 0;
for i := 0 to FNtaps - 1 do
begin
t := (i - mid) / FSps;
if Abs(t) < 1e-9 then
h := 1.0 - beta + 4.0 * beta / Pi
else if Abs(Abs(4.0 * beta * t) - 1.0) < 1e-6 then
h := (beta / Sqrt(2.0)) *
((1.0 + 2.0 / Pi) * Sin(Pi / (4.0 * beta)) +
(1.0 - 2.0 / Pi) * Cos(Pi / (4.0 * beta)))
else
begin
num := Sin(Pi * t * (1.0 - beta)) +
4.0 * beta * t * Cos(Pi * t * (1.0 + beta));
den := Pi * t * (1.0 - Sqr(4.0 * beta * t));
h := num / den;
end;
FRRC[i] := h;
ssum := ssum + h * h;
end;
if ssum > 0 then
for i := 0 to FNtaps - 1 do
FRRC[i] := FRRC[i] / Sqrt(ssum);
SetLength(FHistI, FNtaps);
SetLength(FHistQ, FNtaps);
end;
procedure TBeaconDecoder.BuildFFT;
// Таблицы бит-реверса и твидлов для радикс-2 FFT длины BCN_NFFT.
var
i, j, bit, n: Integer;
begin
n := BCN_NFFT;
SetLength(FBitRev, n);
for i := 0 to n - 1 do
begin
j := 0;
for bit := 0 to BCN_NFFT_LOG2 - 1 do
if (i and (1 shl bit)) <> 0 then
j := j or (1 shl (BCN_NFFT_LOG2 - 1 - bit));
FBitRev[i] := j;
end;
SetLength(FTwRe, n div 2);
SetLength(FTwIm, n div 2);
for i := 0 to (n div 2) - 1 do
begin
FTwRe[i] := Cos(-2.0 * Pi * i / n);
FTwIm[i] := Sin(-2.0 * Pi * i / n);
end;
SetLength(FFre, n);
SetLength(FFim, n);
SetLength(FAcqRe, n);
SetLength(FAcqIm, n);
end;
procedure TBeaconDecoder.FFTInplace;
// Итеративный радикс-2 DIT FFT над FFre/FFim (длина BCN_NFFT), in-place.
var
n, half, step, i, j, k, twi: Integer;
ur, ui, tr, ti: Double;
begin
n := BCN_NFFT;
// бит-реверс перестановка
for i := 0 to n - 1 do
begin
j := FBitRev[i];
if j > i then
begin
tr := FFre[i]; FFre[i] := FFre[j]; FFre[j] := tr;
ti := FFim[i]; FFim[i] := FFim[j]; FFim[j] := ti;
end;
end;
half := 1;
while half < n do
begin
step := half * 2;
i := 0;
while i < n do
begin
k := 0;
while k < half do
begin
twi := (k * (n div step));
ur := FTwRe[twi]; ui := FTwIm[twi];
tr := ur * FFre[i + k + half] - ui * FFim[i + k + half];
ti := ur * FFim[i + k + half] + ui * FFre[i + k + half];
FFre[i + k + half] := FFre[i + k] - tr;
FFim[i + k + half] := FFim[i + k] - ti;
FFre[i + k] := FFre[i + k] + tr;
FFim[i + k] := FFim[i + k] + ti;
Inc(k);
end;
Inc(i, step);
end;
half := step;
end;
end;
procedure TBeaconDecoder.RunAcquisition;
// Индикатор наличия несущей маяка: вычитаем DC (offset Pluto), возводим в квадрат
// (снимаем BPSK), FFT → ищем самую сильную линию вне зоны DC. prominence (pk/среднее)
// = «несущая видна» (FCarPresent). Частоту НЕ оцениваем — снос целиком ведёт
// Costas + offload (см. ProcessSymbol); squaring-FFT давала неверный знак.
var
i, n, loBin: Integer;
mr, mi, re, im, mag, pk, sumMag, meanMag: Double;
begin
n := BCN_NFFT;
// среднее (DC) для подавления offset Pluto
mr := 0; mi := 0;
for i := 0 to n - 1 do begin mr := mr + FAcqRe[i]; mi := mi + FAcqIm[i]; end;
mr := mr / n; mi := mi / n;
// z² с вычтенным DC
for i := 0 to n - 1 do
begin
re := FAcqRe[i] - mr; im := FAcqIm[i] - mi;
FFre[i] := (re * re - im * im);
FFim[i] := (2.0 * re * im);
end;
FFTInplace;
// самая сильная линия, исключая ±loBin вокруг DC (там сидит остаток DC при Δf≈0)
loBin := 2;
pk := 0; sumMag := 0;
for i := 0 to n - 1 do
begin
mag := FFre[i] * FFre[i] + FFim[i] * FFim[i];
sumMag := sumMag + mag;
if (i >= loBin) and (i <= n - loBin) and (mag > pk) then pk := mag;
end;
meanMag := sumMag / n;
if meanMag > 1e-30 then
FCarProm := FCarProm + 0.4 * (pk / meanMag - FCarProm);
FCarPresent := FCarProm > 20.0; // линия выше среднего → несущая в полосе
end;
procedure TBeaconDecoder.ResetDSP;
var i: Integer;
begin
FNcoPh := 0;
FAccI := 0; FAccQ := 0; FAccN := 0;
for i := 0 to High(FHistI) do begin FHistI[i] := 0; FHistQ[i] := 0; end;
FHistPos := 0;
FMuAcc := FSps / 2.0;
FHalf := False;
FMidI := 0; FMidQ := 0; FPrevI := 0; FPrevQ := 0;
FYd0I := 0; FYd0Q := 0; FYd1I := 0; FYd1Q := 0;
FWsym := 0; FTimErrEMA := 0;
FCarPh := 0; FCarFreq := 0;
FSigAmp := 0; FQVar := 0;
FDecRMS := 0; FSymMag := 0; FRrcAmp := 0;
FRingPos := 0; FRingCnt := 0;
FAcqPos := 0; FResidDf := 0; FResidPh := 0; FResidInc := 0;
FCarProm := 0; FCarPresent := False; FFreqLocked := False;
FLockHold := 0;
FPrevRre := 0; FPrevRim := 0;
end;
procedure TBeaconDecoder.Configure(SampleRateHz: Double);
var
bn, zeta, theta, d: Double;
begin
if SampleRateHz < 1000 then Exit;
FLock.Enter;
try
if SameValue(SampleRateHz, FFs) and (FNtaps > 0) then Exit;
FFs := SampleRateHz;
FDecim := Max(1, Round(FFs / BCN_TARGET_FS));
FFdec := FFs / FDecim;
FSps := FFdec / BCN_BAUD;
FNcoInc := 2.0 * Pi * FOffsetHz / FFs;
BuildRRC;
if Length(FBitRev) <> BCN_NFFT then BuildFFT;
// Costas: 2-й порядок, нормированный на символ. Bn ~ 1.5% символьной.
bn := 0.02;
zeta := 0.707;
theta := bn / (zeta + 1.0 / (4.0 * zeta));
d := 1.0 + 2.0 * zeta * theta + theta * theta;
FCarAlpha := (4.0 * zeta * theta) / d;
FCarBeta := (4.0 * theta * theta) / d;
FFllGain := 0.002; // частотная петля (decision-directed), мягкая
ResetDSP;
finally
FLock.Leave;
end;
end;
procedure TBeaconDecoder.SetOffsetHz(Hz: Double);
begin
FLock.Enter;
try
FOffsetHz := Hz;
if FFs > 0 then FNcoInc := 2.0 * Pi * FOffsetHz / FFs;
finally
FLock.Leave;
end;
end;
procedure TBeaconDecoder.SetInvert(On_: Boolean);
begin
FLock.Enter;
try
if On_ = FInvert then Exit;
FInvert := On_;
ResetDSP; // смена ориентации → перезахват
finally
FLock.Leave;
end;
end;
function TBeaconDecoder.Inverted: Boolean;
begin
Result := FInvert;
end;
procedure TBeaconDecoder.Reseed;
begin
FLock.Enter;
try
ResetDSP;
finally
FLock.Leave;
end;
end;
procedure TBeaconDecoder.SetEnabled(On_: Boolean);
begin
FLock.Enter;
try
if On_ = FEnabled then Exit;
FEnabled := On_;
if On_ then ResetDSP;
finally
FLock.Leave;
end;
end;
procedure TBeaconDecoder.Feed(const I, Q: Double);
// Один комплексный отсчёт @ FFs. Снос маяка к DC + boxcar-децимация.
var
c, s, mi, mq, qq: Double;
begin
if not FEnabled then Exit;
// Зеркальный baseband (QO-100/LNB): сопряжение IQ переворачивает ось частот,
// тогда снос совпадает с осью дисплея.
if FInvert then qq := -Q else qq := Q;
// exp(-j*ph): сносим +OffsetHz к DC.
c := Cos(FNcoPh); s := Sin(FNcoPh);
mi := I * c + qq * s;
mq := qq * c - I * s;
FNcoPh := FNcoPh + FNcoInc;
if FNcoPh > Pi then FNcoPh := FNcoPh - 2.0 * Pi
else if FNcoPh < -Pi then FNcoPh := FNcoPh + 2.0 * Pi;
FAccI := FAccI + mi;
FAccQ := FAccQ + mq;
Inc(FAccN);
if FAccN >= FDecim then
begin
ProcessDecimated(FAccI / FDecim, FAccQ / FDecim);
FAccI := 0; FAccQ := 0; FAccN := 0;
end;
end;
procedure TBeaconDecoder.ProcessDecimated(const xi, xq: Double);
// RRC матч-фильтр + Gardner-таймер @ FFdec. Без CS (зовётся из Feed, под потоком DSP);
// снимок констелляции защищён в GetScope собственным копированием полей-массивов.
var
k, idx: Integer;
yi, yq, frac, e, step, cr, sr, ri, ii: Double;
begin
// остаточный снос несущей (по squaring-FFT acquisition) — поверх главного NCO
cr := Cos(FResidPh); sr := Sin(FResidPh);
ri := xi * cr + xq * sr;
ii := xq * cr - xi * sr;
FResidPh := FResidPh + FResidInc;
if FResidPh > Pi then FResidPh := FResidPh - 2.0 * Pi
else if FResidPh < -Pi then FResidPh := FResidPh + 2.0 * Pi;
// Диагностика тракта (lock-независимо): RMS децимированного сигнала на входе RRC.
FDecRMS := FDecRMS + 0.001 * (Sqrt(ri * ri + ii * ii) - FDecRMS);
// Буфер для грубой carrier-acquisition: кладём сигнал ПОСЛЕ главного NCO, но
// ДО остаточного — тогда squaring-FFT меряет АБСОЛЮТНЫЙ остаток (не зависит от
// FResidDf) → коррекция = прямое присвоение, без накопления и runaway.
FAcqRe[FAcqPos] := xi; FAcqIm[FAcqPos] := xq;
Inc(FAcqPos);
if FAcqPos >= BCN_NFFT then
begin
RunAcquisition;
FAcqPos := 0;
end;
// вставляем в кольцо истории (уже с остаточной коррекцией)
FHistI[FHistPos] := ri;
FHistQ[FHistPos] := ii;
// свёртка RRC
yi := 0; yq := 0;
idx := FHistPos;
for k := 0 to FNtaps - 1 do
begin
yi := yi + FRRC[k] * FHistI[idx];
yq := yq + FRRC[k] * FHistQ[idx];
Dec(idx); if idx < 0 then idx := FNtaps - 1;
end;
FHistPos := FHistPos + 1; if FHistPos >= FNtaps then FHistPos := 0;
// AGC: нормируем выход RRC к ~1. Без этого при слабом сигнале (~−60 dBFS) ошибки
// петель (Costas ∝ амплитуде, Gardner ∝ амплитуде²) на 3 порядка меньше калибровки
// усилений → петли практически заморожены, лока нет. Нормировка делает динамику
// петель независимой от абсолютного уровня. FRrcAmp медленный — это и метрика силы.
if FRrcAmp <= 0 then
FRrcAmp := Sqrt(yi * yi + yq * yq) + 1e-12 // сид на первом отсчёте (без скачка)
else
FRrcAmp := FRrcAmp + 0.0005 * (Sqrt(yi * yi + yq * yq) - FRrcAmp);
if FRrcAmp > 1e-12 then
begin
yi := yi / FRrcAmp;
yq := yq / FRrcAmp;
end;
FSymMag := FRrcAmp; // диагностика силы сигнала (сырой уровень до AGC)
// линия задержки RRC-выхода для линейной интерполяции
FYd1I := FYd0I; FYd1Q := FYd0Q;
FYd0I := yi; FYd0Q := yq;
// Gardner: полу-символьные стробы. FMuAcc убывает на 1/отсчёт; при переходе
// через 0 интерполируем и планируем следующий строб через ~FSps/2 отсчётов.
FMuAcc := FMuAcc - 1.0;
if FMuAcc <= 0.0 then
begin
frac := FMuAcc + 1.0; // позиция в (0..1] между Yd1 и Yd0
if frac < 0 then frac := 0;
yi := FYd1I + frac * (FYd0I - FYd1I);
yq := FYd1Q + frac * (FYd0Q - FYd1Q);
step := (FSps / 2.0) * (1.0 + FWsym); // базовый интервал до след. полу-символа
if FHalf then
begin
// середина символа — запоминаем для Gardner
FMidI := yi; FMidQ := yq;
end
else
begin
// центр символа → ошибка Gardner (вращательно-инвариантна):
// e = Re{ conj(mid) * (curr - prev) } по компонентам
e := FMidI * (yi - FPrevI) + FMidQ * (yq - FPrevQ);
FTimErrEMA := FTimErrEMA + 0.01 * (Abs(e) - FTimErrEMA);
// После AGC вход ~единичный, ошибка Gardner шумная (по логам wsym бился об
// ±0.05). Снижаем усиления интегратора/пропорц. и поджимаем кламп: маяк 400 бод
// стабилен (доли %), большой диапазон не нужен, а шум перестаёт расшатывать.
FWsym := FWsym + 0.0003 * e; // интегратор периода (медленнее)
if FWsym > 0.01 then FWsym := 0.01;
if FWsym < -0.01 then FWsym := -0.01;
FPrevI := yi; FPrevQ := yq;
ProcessSymbol(yi, yq); // символ → Costas (вход уже нормирован AGC)
step := step + 0.003 * e; // пропорциональная правка таймера (мягче)
end;
FMuAcc := FMuAcc + step;
FHalf := not FHalf;
end;
end;
procedure TBeaconDecoder.ProcessSymbol(const zi, zq: Double);
// Costas BPSK: дероворот по FCarPh, решающе-направленный детектор фазы, обновление
// контура; нормированный символ → констелляция + метрики.
var
c, s, dr, di, ce, na, absI, ef, leak: Double;
begin
c := Cos(FCarPh); s := Sin(FCarPh);
dr := zi * c + zq * s; // умножение на exp(-j FCarPh)
di := zq * c - zi * s;
if dr >= 0 then ce := di else ce := -di; // Costas: фазовый детектор sign(I)*Q
absI := Abs(dr);
// FLL (decision-directed): убираем модуляцию (rk = |I| + j·ce), частотная ошибка
// = Im(rk·conj(rk-1)). Расширяет диапазон захвата и держит частоту устойчиво
// (отрицательная ОС) — в отличие от open-loop squaring-FFT, не «убегает».
ef := ce * FPrevRre - absI * FPrevRim;
FPrevRre := absI; FPrevRim := ce;
FCarFreq := FCarFreq + FCarBeta * ce + FFllGain * ef;
if FCarFreq > 3.0 then FCarFreq := 3.0;
if FCarFreq < -3.0 then FCarFreq := -3.0;
// OFFLOAD частоты: медленно «стекаем» накопленный снос Costas в пред-RRC NCO
// (FResidInc), у которого НЕТ предела ±π/символ. По логам carHz упирался в кламп
// (±127/±191) из-за ошибки наведения (1 пиксель≈300Гц) + дрейфа LNB и срывал лок.
// Консервативно: сколько добавили в FResidInc, столько убрали из FCarFreq (без
// рывка). Утечка медленная (TC~100 символов >> группозадержки RRC ~4 симв) — стабильно.
// Итог: Costas держит лишь мелкий быстрый остаток, весь медленный снос — в FResidInc.
leak := 0.01 * FCarFreq;
FResidInc := FResidInc + leak / FSps; // рад/символ → рад/децим-отсчёт
FResidDf := FResidInc * FFdec / (2.0 * Pi); // для лога/диагностики
FCarFreq := FCarFreq - leak;
FCarPh := FCarPh + FCarFreq + FCarAlpha * ce;
if FCarPh > Pi then FCarPh := FCarPh - 2.0 * Pi
else if FCarPh < -Pi then FCarPh := FCarPh + 2.0 * Pi;
// метрики — медленные EMA (~0.8с): SNR по логам скачет −8…+6 за секунду, на быстрой
// EMA порог лока дребезжит. Замедляем, чтобы оценка была устойчивой.
FSigAmp := FSigAmp + 0.003 * (Abs(dr) - FSigAmp);
FQVar := FQVar + 0.003 * (di * di - FQVar);
// Холдовер-лок (анти-мерцание): копим счётчик при хорошем SNR, тратим при плохом.
// Лок держится сквозь кратковременные провалы (~секунды), не отваливаясь на каждом.
if FQVar > 1e-12 then
begin
if (FSigAmp * FSigAmp) / FQVar > 2.0 then
begin
if FLockHold < 2400 then Inc(FLockHold, 3); // захват быстрее, чем потеря
end
else
if FLockHold > 0 then Dec(FLockHold);
end;
if FLockHold >= 600 then FFreqLocked := True
else if FLockHold <= 150 then FFreqLocked := False;
// нормировка констелляции к ±1 по амплитуде сигнала
if FSigAmp > 1e-9 then FConstScale := 1.0 / FSigAmp;
na := FConstScale;
FRingI[FRingPos] := dr * na;
FRingQ[FRingPos] := di * na;
FRingPos := FRingPos + 1; if FRingPos >= BCN_SCOPE_PTS then FRingPos := 0;
if FRingCnt < BCN_SCOPE_PTS then Inc(FRingCnt);
end;
procedure TBeaconDecoder.GetScope(out S: TBeaconScope);
var
i, idx: Integer;
snr: Double;
begin
FLock.Enter;
try
S.Enabled := FEnabled;
S.OffsetHz := FOffsetHz;
S.SymRate := BCN_BAUD * (1.0 + FWsym);
S.ResidHz := FResidDf;
S.CarProm := FCarProm;
S.FsHz := FFs;
S.FdecHz := FFdec;
S.Sps := FSps;
S.DecRMS := FDecRMS;
S.SymMag := FSymMag;
S.TimErr := FTimErrEMA;
S.Wsym := FWsym;
S.CarFreqHz := FCarFreq * BCN_BAUD / (2.0 * Pi); // рад/символ → Гц
S.LockHold := FLockHold;
// SNR ≈ (E|I|)^2 / E[Q^2] (мощность сигнала по I против шума по Q)
if FQVar > 1e-12 then
snr := (FSigAmp * FSigAmp) / FQVar
else
snr := 0;
if snr > 1e-6 then S.SNRdB := 10.0 * Log10(snr) else S.SNRdB := -99;
// CARRIER = линия несущей видна (squaring-FFT) И Costas держит фазу.
S.CarrierLock := FCarPresent and FFreqLocked;
S.SymbolLock := FFreqLocked and
(FTimErrEMA < 0.25 * Max(FSigAmp, 1e-9)) and (FRingCnt >= BCN_SCOPE_PTS);
S.Count := FRingCnt;
// выгружаем кольцо в хронологическом порядке (старейший → новейший)
for i := 0 to FRingCnt - 1 do
begin
if FRingCnt < BCN_SCOPE_PTS then
idx := i
else
begin
idx := FRingPos + i;
if idx >= BCN_SCOPE_PTS then Dec(idx, BCN_SCOPE_PTS);
end;
S.PtI[i] := FRingI[idx];
S.PtQ[i] := FRingQ[idx];
end;
finally
FLock.Leave;
end;
end;
end.