Files
ewsdr/BeaconDecoder.pas
T
ew8bakandClaude Opus 4.8 37eb8065d6 chore: remove QO-100 beacon decoder debug instrumentation
After successful on-air validation, drop the temporary diagnostics:
- IQ baseband dump to ~/ewsdr_beacon_iq.f32 (FDbgStream/DbgWrite2).
- BeaconLog file logger (~/ewsdr_beacon.log) and per-second STATE / AIM lines.
- Dead Gardner timing fields (FMidI/FMidQ/FHalf/FPrevI/FPrevQ), superseded by
  the ML signal×slope TED.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:45:52 +03:00

849 lines
36 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, BeaconFEC;
const
BCN_BAUD = 400.0; // скорость ДАННЫХ маяка (после Manchester)
BCN_CHIP_BAUD = 800.0; // чип-рейт (Manchester = 2× данных) — на нём таймер
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; // холдовер-счётчик лока
ManPhase: Integer; // выбранная фаза Manchester (0/1)
ManM0, ManM1: Single; // EMA энергии двух фаз (диагностика)
Frames: Int64; // декодировано кадров AO-40 FEC
HasFrame: Boolean; // есть хотя бы один кадр
LastRSErr: Integer; // ошибок RS в последнем кадре
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;
// --- ML signal×slope symbol timing ---
FMuAcc: Double; // обратный отсчёт до следующего строба чипа
FYd0I, FYd0Q: Double; // линия задержки RRC-выхода (новейший)
FYd1I, FYd1Q: Double; // предыдущий
FYd2I, FYd2Q: Double; // 2 (для ML-TED центр/early/late + задержка «late»)
FYd3I, FYd3Q: Double; // 3
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; // линия несущей видна
// --- one-shot захват частоты (squaring-FFT → FResidInc) ---
// На чип-рейте 800 бод снос ~400 Гц = π/чип: decision-directed Costas его НЕ
// тянет, а несущая в π/чип вырождает Manchester (chips одинаковы → 0.5(c0c1)≈0).
// Поэтому грубый снос убираем NCO по оценке частоты из squaring-FFT (z² снимает
// BPSK → линия на 2·fc, знак однозначен при большом сносе). Усредняем по
// FAcqNeed блокам, ставим FResidInc один раз, разоружаемся. Дрейф LNB дальше
// ведёт непрерывный Costas+offload (отриц. ОС, не убегает).
FAcqArmed: Boolean; // ждём захвата (взводится на Reseed/новое наведение)
FAcqCount: Integer; // сколько блоков усреднено
FAcqSum: Double; // сумма оценок fc (Гц) для усреднения
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 (нормировка петель)
// --- Manchester-свёртка (чипы 800 → символы 400) ---
FChipPrevI, FChipPrevQ: Double; // предыдущий чип
FChipHave: Boolean;
FChipCount: Integer; // глобальный счётчик чипов (для чётности фазы)
FManMetric0, FManMetric1: Double; // EMA энергии |0.5(cPrev−c)| по двум фазам
FManPhase: Integer; // выбранная фаза (0: пары с чётного чипа)
// --- дифференциальный декодер (DBPSK) ---
FSymPrevI, FSymPrevQ: Double; // предыдущий Manchester-символ
FSymHave: Boolean;
// --- FEC-бэкенд (AO-40) ---
FFEC: TBeaconFEC;
FFrames: Int64;
FHasFrame: Boolean;
FLastFrame: TBeaconFrame;
FLastRSErr: Integer;
// --- констелляция (кольцо) ---
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 ProcessChip(const ci, cq: Double); // Manchester + дифф-декод → FEC
procedure FECFrame(const Frame: TBeaconFrame; RSErrors: Integer);
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);
function GetLastFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
end;
implementation
constructor TBeaconDecoder.Create;
begin
inherited Create;
FLock := TCriticalSection.Create;
FFEC := TBeaconFEC.Create;
FFEC.OnFrame := FECFrame;
FFs := 0;
FEnabled := False;
FConstScale := 1.0;
Configure(192000);
end;
destructor TBeaconDecoder.Destroy;
begin
FFEC.Free;
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), возводим в квадрат
// (z² снимает BPSK → линия на 2·fc), FFT → самая сильная линия вне зоны DC.
// prominence (pk/среднее) = «несущая видна» (FCarPresent).
// ЧАСТОТУ ОЦЕНИВАЕМ: бин пика → 2·fc → fc; усредняем по FAcqNeed блокам пока
// взведено (FAcqArmed) и несущая видна и Costas ещё не залочен, затем ставим
// FResidInc один раз (стягиваем несущую к DC) и разоружаемся. Знак z² однозначен
// при большом сносе (~±400 Гц), а буфер FAcqRe/Im берётся ДО остаточного NCO —
// значит меряем АБСОЛЮТНЫЙ снос, присваиваем FResidInc прямо (без накопления).
const
FAcqNeed = 4; // блоков усреднения (~1.7 c @ 9600) до применения
AcqMinHz = 15.0; // ниже — Costas вытянет сам, NCO не дёргаем
AcqMaxHz = 3000.0; // санитарный предел
var
i, n, loBin, pkBin: Integer;
mr, mi, re, im, mag, pk, sumMag, meanMag, fHz, fcHz, avgHz: 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; pkBin := 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 begin pk := mag; pkBin := i; end;
end;
meanMag := sumMag / n;
if meanMag > 1e-30 then
FCarProm := FCarProm + 0.4 * (pk / meanMag - FCarProm);
FCarPresent := FCarProm > 20.0; // линия выше среднего → несущая в полосе
// --- one-shot оценка частоты ---
// Гейтим ТОЛЬКО по «несущая видна» + взведено (одноразово на наведение). НЕ
// гейтим по FFreqLocked: Costas на чип-рейте может ложно «залочиться» на состоянии
// +400 (π/чип) за ~3 c — раньше, чем накопится усреднение; тогда условие
// not FFreqLocked заблокировало бы захват (гонка). Применение и так защищено
// порогом |avgHz|>=AcqMinHz: если несущая реально у DC (Costas справился сам),
// оценка ~0 → коррекцию не трогаем.
if not (FAcqArmed and FCarPresent) then Exit;
// бин пика → частота линии z² (= 2·fc), со знаком; затем fc = /2
if pkBin <= n div 2 then fHz := pkBin * FFdec / n
else fHz := (pkBin - n) * FFdec / n;
fcHz := fHz / 2.0;
FAcqSum := FAcqSum + fcHz;
Inc(FAcqCount);
if FAcqCount < FAcqNeed then Exit;
avgHz := FAcqSum / FAcqCount;
// применяем: residual NCO (exp(-j FResidPh)) с FResidInc убирает +fc к DC
if (Abs(avgHz) >= AcqMinHz) and (Abs(avgHz) <= AcqMaxHz) then
begin
FResidInc := 2.0 * Pi * avgHz / FFdec;
FResidDf := avgHz;
end;
FAcqArmed := False; // разоружаемся; дальше дрейф ведёт Costas+offload
FAcqCount := 0; FAcqSum := 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;
FYd0I := 0; FYd0Q := 0; FYd1I := 0; FYd1Q := 0;
FYd2I := 0; FYd2Q := 0; FYd3I := 0; FYd3Q := 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;
FAcqArmed := True; FAcqCount := 0; FAcqSum := 0; // взводим грубый захват частоты
FLockHold := 0;
FPrevRre := 0; FPrevRim := 0;
// Manchester / дифф-декод
FChipPrevI := 0; FChipPrevQ := 0; FChipHave := False; FChipCount := 0;
FManMetric0 := 0; FManMetric1 := 0; FManPhase := 0;
FSymPrevI := 0; FSymPrevQ := 0; FSymHave := False;
if FFEC <> nil then FFEC.Reset;
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_CHIP_BAUD; // отсчётов на ЧИП (Manchester), таймер на 800
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, cr, sr, ri, ii: Double;
ci, cq, eai, eaq, lai, laq, dri, drq: Double; // ML-TED центр/early/late/произв.
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-выхода (4 отсчёта) для ML-TED: центр/early/late + задержка
// на 1 отсчёт, чтобы «late» (FYd0) был доступен в момент строба.
FYd3I := FYd2I; FYd3Q := FYd2Q;
FYd2I := FYd1I; FYd2Q := FYd1Q;
FYd1I := FYd0I; FYd1Q := FYd0Q;
FYd0I := yi; FYd0Q := yq;
// Таймер ML signal×slope (как gr symbol_sync_cc, TED_SIGNAL_TIMES_SLOPE_ML):
// лочит ЦЕНТР чипа по максимуму |x|, ошибка e = Re{conj(x)·dx/dt}. Gardner на
// Manchester чип-рейте ВЫРОЖДАЛСЯ (всегда-переход в паре → S-кривая degenerate →
// таймер вращался, timErr~1.5, m0≈m1, frames=0). ML сходится стабильно (оффлайн на
// дампе 35c: timErr 0.25 vs Gardner 1.49 → sync 65/65). Один строб на чип.
FMuAcc := FMuAcc - 1.0;
if FMuAcc <= 0.0 then
begin
frac := FMuAcc + 1.0; // позиция в (0..1]
if frac < 0 then frac := 0;
// центр (между FYd2 и FYd1), early (FYd3/FYd2), late (FYd1/FYd0)
ci := FYd2I + frac * (FYd1I - FYd2I);
cq := FYd2Q + frac * (FYd1Q - FYd2Q);
eai := FYd3I + frac * (FYd2I - FYd3I);
eaq := FYd3Q + frac * (FYd2Q - FYd3Q);
lai := FYd1I + frac * (FYd0I - FYd1I);
laq := FYd1Q + frac * (FYd0Q - FYd1Q);
// производная dx ≈ late − early; ошибка = Re{conj(центр)·dx} (вращ.-инвариантна)
dri := lai - eai; drq := laq - eaq;
e := ci * dri + cq * drq;
FTimErrEMA := FTimErrEMA + 0.01 * (Abs(e) - FTimErrEMA);
FWsym := FWsym + 0.0005 * e; // интегратор периода
if FWsym > 0.02 then FWsym := 0.02;
if FWsym < -0.02 then FWsym := -0.02;
ProcessSymbol(ci, cq); // чип-центр → Costas → Manchester
FMuAcc := FMuAcc + FSps * (1.0 + FWsym) + 0.005 * e; // период + проп. правка
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.
// DEADBAND ~2.5 Гц: без него мелкий постоянный bias Costas (carHz~0.7) непрерывно
// интегрировался в FResidInc → resid полз ~5.5 Гц/с (спур, не дрейф). Стекаем только
// КРУПНЫЙ снос (ошибка наведения/дрейф LNB), мелкий остаток держит сам Costas.
if Abs(FCarFreq) > 0.02 then // 0.02 рад/чип ≈ 2.5 Гц @800
begin
leak := 0.01 * FCarFreq;
FResidInc := FResidInc + leak / FSps; // рад/символ → рад/децим-отсчёт
FCarFreq := FCarFreq - leak;
end;
FResidDf := FResidInc * FFdec / (2.0 * Pi); // для лога/диагностики
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);
// скорректированный (Costas) чип → Manchester-свёртка + дифф-декод → FEC
ProcessChip(dr, di);
end;
procedure TBeaconDecoder.ProcessChip(const ci, cq: Double);
// Вход — комплексный чип @ 800 (после таймера/Costas). Делает то же, что
// gr-satellites manchester_sync_cc + дифф-декод:
// 1) Manchester: символ-кандидат s = 0.5·(cPrev − c) для пары соседних чипов.
// Какая из 2 фаз спаривания верна (пары с чётного или нечётного чипа) — решаем
// по бо́льшей сумме |s| (EMA): на верной фазе чипы внутри бита противофазны →
// |s| максимально. Эмитим символ только когда чётность пары = выбранной фазе.
// 2) дифф-декод DBPSK: soft = Re(s_n · conj(s_{n1})) (знак 1 как в gr для Manchester).
// 3) soft 400/с → TBeaconFEC.
var
di_, dq_, mag, soft: Double;
parity: Integer;
begin
if FChipHave then
begin
di_ := 0.5 * (FChipPrevI - ci);
dq_ := 0.5 * (FChipPrevQ - cq);
mag := Sqrt(di_ * di_ + dq_ * dq_);
parity := (FChipCount - 1) and 1; // чётность первого чипа пары
if parity = 0 then
FManMetric0 := FManMetric0 + 0.01 * (mag - FManMetric0)
else
FManMetric1 := FManMetric1 + 0.01 * (mag - FManMetric1);
if FManMetric0 >= FManMetric1 then FManPhase := 0 else FManPhase := 1;
if parity = FManPhase then
begin
// это символ данных (400/с)
if FSymHave then
begin
soft := -(di_ * FSymPrevI + dq_ * FSymPrevQ); // Re(s_n·conj(s_{n1}))
FFEC.PushSoftSymbol(soft);
end;
FSymPrevI := di_; FSymPrevQ := dq_; FSymHave := True;
end;
end;
FChipPrevI := ci; FChipPrevQ := cq; FChipHave := True;
Inc(FChipCount);
end;
procedure TBeaconDecoder.FECFrame(const Frame: TBeaconFrame; RSErrors: Integer);
// Колбэк из TBeaconFEC (DSP-поток). Сохраняем кадр под CS для UI.
begin
FLock.Enter;
try
FLastFrame := Frame;
FLastRSErr := RSErrors;
FHasFrame := True;
Inc(FFrames);
finally
FLock.Leave;
end;
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_CHIP_BAUD / (2.0 * Pi); // рад/чип → Гц
S.LockHold := FLockHold;
S.ManPhase := FManPhase;
S.ManM0 := FManMetric0;
S.ManM1 := FManMetric1;
S.Frames := FFrames;
S.HasFrame := FHasFrame;
S.LastRSErr := FLastRSErr;
// 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;
function TBeaconDecoder.GetLastFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
begin
FLock.Enter;
try
Result := FHasFrame;
if Result then
begin
Frame := FLastFrame;
RSErrors := FLastRSErr;
end;
finally
FLock.Leave;
end;
end;
end.