mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
feat: QO-100 beacon AO-40 FEC decode end-to-end (Milestone 2)
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>
This commit is contained in:
+238
-47
@@ -27,10 +27,11 @@ unit BeaconDecoder;
|
||||
interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, SyncObjs, Math;
|
||||
Classes, SysUtils, SyncObjs, Math, BeaconFEC;
|
||||
|
||||
const
|
||||
BCN_BAUD = 400.0; // символьная скорость маяка
|
||||
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 в символах (полу-)
|
||||
@@ -60,6 +61,11 @@ type
|
||||
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
|
||||
@@ -97,6 +103,8 @@ type
|
||||
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;
|
||||
|
||||
@@ -117,6 +125,16 @@ type
|
||||
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; // холдовер-счётчик лока (анти-мерцание)
|
||||
|
||||
@@ -135,6 +153,28 @@ type
|
||||
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;
|
||||
@@ -146,6 +186,9 @@ type
|
||||
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;
|
||||
@@ -163,6 +206,7 @@ type
|
||||
procedure Feed(const I, Q: Double); inline;
|
||||
|
||||
procedure GetScope(out S: TBeaconScope);
|
||||
function GetLastFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
|
||||
end;
|
||||
|
||||
implementation
|
||||
@@ -171,6 +215,10 @@ 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;
|
||||
@@ -179,6 +227,8 @@ end;
|
||||
|
||||
destructor TBeaconDecoder.Destroy;
|
||||
begin
|
||||
if FDbgStream <> nil then FreeAndNil(FDbgStream);
|
||||
FFEC.Free;
|
||||
FLock.Free;
|
||||
inherited Destroy;
|
||||
end;
|
||||
@@ -295,13 +345,21 @@ begin
|
||||
end;
|
||||
|
||||
procedure TBeaconDecoder.RunAcquisition;
|
||||
// Индикатор наличия несущей маяка: вычитаем DC (offset Pluto), возводим в квадрат
|
||||
// (снимаем BPSK), FFT → ищем самую сильную линию вне зоны DC. prominence (pk/среднее)
|
||||
// = «несущая видна» (FCarPresent). Частоту НЕ оцениваем — снос целиком ведёт
|
||||
// Costas + offload (см. ProcessSymbol); squaring-FFT давала неверный знак.
|
||||
// Грубый захват несущей маяка: вычитаем 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: Integer;
|
||||
mr, mi, re, im, mag, pk, sumMag, meanMag: Double;
|
||||
i, n, loBin, pkBin: Integer;
|
||||
mr, mi, re, im, mag, pk, sumMag, meanMag, fHz, fcHz, avgHz: Double;
|
||||
begin
|
||||
n := BCN_NFFT;
|
||||
// среднее (DC) для подавления offset Pluto
|
||||
@@ -320,17 +378,44 @@ begin
|
||||
|
||||
// самая сильная линия, исключая ±loBin вокруг DC (там сидит остаток DC при Δf≈0)
|
||||
loBin := 2;
|
||||
pk := 0; sumMag := 0;
|
||||
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 pk := 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;
|
||||
@@ -344,6 +429,7 @@ begin
|
||||
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;
|
||||
@@ -351,8 +437,14 @@ begin
|
||||
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);
|
||||
@@ -366,7 +458,7 @@ begin
|
||||
FFs := SampleRateHz;
|
||||
FDecim := Max(1, Round(FFs / BCN_TARGET_FS));
|
||||
FFdec := FFs / FDecim;
|
||||
FSps := FFdec / BCN_BAUD;
|
||||
FSps := FFdec / BCN_CHIP_BAUD; // отсчётов на ЧИП (Manchester), таймер на 800
|
||||
|
||||
FNcoInc := 2.0 * Pi * FOffsetHz / FFs;
|
||||
|
||||
@@ -472,7 +564,8 @@ procedure TBeaconDecoder.ProcessDecimated(const xi, xq: Double);
|
||||
// снимок констелляции защищён в GetScope собственным копированием полей-массивов.
|
||||
var
|
||||
k, idx: Integer;
|
||||
yi, yq, frac, e, step, cr, sr, ri, ii: Double;
|
||||
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);
|
||||
@@ -485,6 +578,17 @@ begin
|
||||
// Диагностика тракта (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.
|
||||
@@ -526,44 +630,39 @@ begin
|
||||
end;
|
||||
FSymMag := FRrcAmp; // диагностика силы сигнала (сырой уровень до AGC)
|
||||
|
||||
// линия задержки RRC-выхода для линейной интерполяции
|
||||
// линия задержки 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;
|
||||
|
||||
// Gardner: полу-символьные стробы. FMuAcc убывает на 1/отсчёт; при переходе
|
||||
// через 0 интерполируем и планируем следующий строб через ~FSps/2 отсчётов.
|
||||
// Таймер 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] между Yd1 и Yd0
|
||||
frac := FMuAcc + 1.0; // позиция в (0..1]
|
||||
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;
|
||||
// центр (между 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;
|
||||
|
||||
@@ -596,10 +695,16 @@ begin
|
||||
// Консервативно: сколько добавили в FResidInc, столько убрали из FCarFreq (без
|
||||
// рывка). Утечка медленная (TC~100 символов >> группозадержки RRC ~4 симв) — стабильно.
|
||||
// Итог: Costas держит лишь мелкий быстрый остаток, весь медленный снос — в FResidInc.
|
||||
leak := 0.01 * FCarFreq;
|
||||
FResidInc := FResidInc + leak / FSps; // рад/символ → рад/децим-отсчёт
|
||||
// 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); // для лога/диагностики
|
||||
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;
|
||||
@@ -631,6 +736,71 @@ begin
|
||||
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);
|
||||
@@ -652,8 +822,14 @@ begin
|
||||
S.SymMag := FSymMag;
|
||||
S.TimErr := FTimErrEMA;
|
||||
S.Wsym := FWsym;
|
||||
S.CarFreqHz := FCarFreq * BCN_BAUD / (2.0 * Pi); // рад/символ → Гц
|
||||
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
|
||||
@@ -686,4 +862,19 @@ begin
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user