mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
Full chain now decodes the QO-100 central beacon to 256-byte AO-40 frames on-air (frames>0, RS errors=0). Builds on M1 (stable BPSK demod). FEC backend (BeaconFEC.pas, TBeaconFEC): - AO-40 FEC: distributed sync (65 bit, step 80) -> 80x65 deinterleave -> Viterbi r=1/2 k=7 -> CCSDS descramble -> 2x RS(160,128). Links system libfec (Karn) load-time (libfec.so/.dll/.dylib). Viterbi AO-40 [0x4F,-0x6D] remapped to libfec [0x6D,0x4F]. Validated against gr-satellites reference vectors (fectest2: exact 256-byte frame, 0 mismatches). DBPSK Manchester frontend (BeaconDecoder.pas): - Chip-rate (800) processing: RRC matched filter -> ML signal-times-slope timing recovery -> Costas -> Manchester combine (block phase select) -> differential decode -> soft symbols to FEC. - Carrier acquisition: squaring-FFT estimates the residual (carrier at +-pi/chip defeats decision-directed Costas), one-shot pulls it to DC via residual NCO. - Costas frequency offload into pre-RRC NCO (with deadband) tracks LNB drift without hitting the +-pi Costas clamp. - ML TED replaces Gardner, which degenerates on the Manchester chip stream. Temporary on-air diagnostics retained (IQ dump + STATE log) pending wider signal validation; to be removed before final cleanup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
881 lines
37 KiB
ObjectPascal
881 lines
37 KiB
ObjectPascal
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;
|
||
|
||
// --- Gardner timing ---
|
||
FMuAcc: Double; // обратный отсчёт до следующего полу-символьного строба
|
||
FHalf: Boolean; // текущий строб = середина (true) или центр символа
|
||
FMidI, FMidQ: Double; // отсчёт середины (для Gardner)
|
||
FPrevI, FPrevQ: 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(c0−c1)≈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;
|
||
|
||
// --- ВРЕМЕННЫЙ ДЕБАГ: дамп сырого baseband 9600 Гц (I,Q float32) ---
|
||
FDbgStream: TFileStream;
|
||
FDbgLeft: 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 DbgWrite2(const a, b: Double); // ВРЕМЕННЫЙ дамп I,Q float32
|
||
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;
|
||
FDbgStream := nil;
|
||
FDbgLeft := 336000; // ВРЕМЕННЫЙ: ~35 c @ 9600 (2-3 кадра маяка) для оффлайн
|
||
FFs := 0;
|
||
FEnabled := False;
|
||
FConstScale := 1.0;
|
||
Configure(192000);
|
||
end;
|
||
|
||
destructor TBeaconDecoder.Destroy;
|
||
begin
|
||
if FDbgStream <> nil then FreeAndNil(FDbgStream);
|
||
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;
|
||
FHalf := False;
|
||
FMidI := 0; FMidQ := 0; FPrevI := 0; FPrevQ := 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);
|
||
|
||
// ВРЕМЕННЫЙ ДЕБАГ: дамп сырого baseband (после сноса частоты, до RRC) в файл
|
||
// для оффлайн-анализа тайминга чипов и Manchester-структуры.
|
||
if (FDbgLeft > 0) and FFreqLocked then
|
||
begin
|
||
if FDbgStream = nil then
|
||
FDbgStream := TFileStream.Create(GetEnvironmentVariable('HOME') + '/ewsdr_beacon_iq.f32', fmCreate);
|
||
DbgWrite2(ri, ii);
|
||
Dec(FDbgLeft);
|
||
if FDbgLeft = 0 then FreeAndNil(FDbgStream);
|
||
end;
|
||
|
||
// Буфер для грубой 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_{n−1})) (знак −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_{n−1}))
|
||
FFEC.PushSoftSymbol(soft);
|
||
end;
|
||
FSymPrevI := di_; FSymPrevQ := dq_; FSymHave := True;
|
||
end;
|
||
end;
|
||
FChipPrevI := ci; FChipPrevQ := cq; FChipHave := True;
|
||
Inc(FChipCount);
|
||
end;
|
||
|
||
procedure TBeaconDecoder.DbgWrite2(const a, b: Double);
|
||
var fa, fb: Single;
|
||
begin
|
||
fa := a; fb := b;
|
||
FDbgStream.WriteBuffer(fa, 4);
|
||
FDbgStream.WriteBuffer(fb, 4);
|
||
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.
|