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.