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(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; // --- констелляция (кольцо) --- 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_{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.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.