mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +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
|
interface
|
||||||
|
|
||||||
uses
|
uses
|
||||||
Classes, SysUtils, SyncObjs, Math;
|
Classes, SysUtils, SyncObjs, Math, BeaconFEC;
|
||||||
|
|
||||||
const
|
const
|
||||||
BCN_BAUD = 400.0; // символьная скорость маяка
|
BCN_BAUD = 400.0; // скорость ДАННЫХ маяка (после Manchester)
|
||||||
|
BCN_CHIP_BAUD = 800.0; // чип-рейт (Manchester = 2× данных) — на нём таймер
|
||||||
BCN_TARGET_FS = 9600.0; // целевой rate после децимации
|
BCN_TARGET_FS = 9600.0; // целевой rate после децимации
|
||||||
BCN_RRC_BETA = 0.5; // roll-off матч-фильтра
|
BCN_RRC_BETA = 0.5; // roll-off матч-фильтра
|
||||||
BCN_RRC_SPAN = 8; // длина RRC в символах (полу-)
|
BCN_RRC_SPAN = 8; // длина RRC в символах (полу-)
|
||||||
@@ -60,6 +61,11 @@ type
|
|||||||
Wsym: Single; // дробная поправка периода символа
|
Wsym: Single; // дробная поправка периода символа
|
||||||
CarFreqHz: Single; // частота Costas (Гц, остаток несущей)
|
CarFreqHz: Single; // частота Costas (Гц, остаток несущей)
|
||||||
LockHold: Integer; // холдовер-счётчик лока
|
LockHold: Integer; // холдовер-счётчик лока
|
||||||
|
ManPhase: Integer; // выбранная фаза Manchester (0/1)
|
||||||
|
ManM0, ManM1: Single; // EMA энергии двух фаз (диагностика)
|
||||||
|
Frames: Int64; // декодировано кадров AO-40 FEC
|
||||||
|
HasFrame: Boolean; // есть хотя бы один кадр
|
||||||
|
LastRSErr: Integer; // ошибок RS в последнем кадре
|
||||||
end;
|
end;
|
||||||
|
|
||||||
TBeaconDecoder = class
|
TBeaconDecoder = class
|
||||||
@@ -97,6 +103,8 @@ type
|
|||||||
FPrevI, FPrevQ: Double; // прошлый центр символа
|
FPrevI, FPrevQ: Double; // прошлый центр символа
|
||||||
FYd0I, FYd0Q: Double; // линия задержки RRC-выхода (новейший)
|
FYd0I, FYd0Q: Double; // линия задержки RRC-выхода (новейший)
|
||||||
FYd1I, FYd1Q: Double; // предыдущий
|
FYd1I, FYd1Q: Double; // предыдущий
|
||||||
|
FYd2I, FYd2Q: Double; // −2 (для ML-TED центр/early/late + задержка «late»)
|
||||||
|
FYd3I, FYd3Q: Double; // −3
|
||||||
FWsym: Double; // дробная коррекция периода (интегратор)
|
FWsym: Double; // дробная коррекция периода (интегратор)
|
||||||
FTimErrEMA: Double;
|
FTimErrEMA: Double;
|
||||||
|
|
||||||
@@ -117,6 +125,16 @@ type
|
|||||||
FResidInc: Double;
|
FResidInc: Double;
|
||||||
FCarProm: Double; // prominence линии (EMA)
|
FCarProm: Double; // prominence линии (EMA)
|
||||||
FCarPresent: Boolean; // линия несущей видна
|
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)
|
FFreqLocked: Boolean; // Costas держит фазу (для индикации/symbol-lock)
|
||||||
FLockHold: Integer; // холдовер-счётчик лока (анти-мерцание)
|
FLockHold: Integer; // холдовер-счётчик лока (анти-мерцание)
|
||||||
|
|
||||||
@@ -135,6 +153,28 @@ type
|
|||||||
FSymMag: Double; // EMA |символ| после RRC (диагностика тракта)
|
FSymMag: Double; // EMA |символ| после RRC (диагностика тракта)
|
||||||
FRrcAmp: Double; // AGC: 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;
|
FRingI, FRingQ: array[0..BCN_SCOPE_PTS-1] of Single;
|
||||||
FRingPos: Integer;
|
FRingPos: Integer;
|
||||||
@@ -146,6 +186,9 @@ type
|
|||||||
procedure RunAcquisition;
|
procedure RunAcquisition;
|
||||||
procedure ProcessDecimated(const xi, xq: Double);
|
procedure ProcessDecimated(const xi, xq: Double);
|
||||||
procedure ProcessSymbol(const zi, zq: 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;
|
procedure ResetDSP;
|
||||||
public
|
public
|
||||||
constructor Create;
|
constructor Create;
|
||||||
@@ -163,6 +206,7 @@ type
|
|||||||
procedure Feed(const I, Q: Double); inline;
|
procedure Feed(const I, Q: Double); inline;
|
||||||
|
|
||||||
procedure GetScope(out S: TBeaconScope);
|
procedure GetScope(out S: TBeaconScope);
|
||||||
|
function GetLastFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
@@ -171,6 +215,10 @@ constructor TBeaconDecoder.Create;
|
|||||||
begin
|
begin
|
||||||
inherited Create;
|
inherited Create;
|
||||||
FLock := TCriticalSection.Create;
|
FLock := TCriticalSection.Create;
|
||||||
|
FFEC := TBeaconFEC.Create;
|
||||||
|
FFEC.OnFrame := FECFrame;
|
||||||
|
FDbgStream := nil;
|
||||||
|
FDbgLeft := 336000; // ВРЕМЕННЫЙ: ~35 c @ 9600 (2-3 кадра маяка) для оффлайн
|
||||||
FFs := 0;
|
FFs := 0;
|
||||||
FEnabled := False;
|
FEnabled := False;
|
||||||
FConstScale := 1.0;
|
FConstScale := 1.0;
|
||||||
@@ -179,6 +227,8 @@ end;
|
|||||||
|
|
||||||
destructor TBeaconDecoder.Destroy;
|
destructor TBeaconDecoder.Destroy;
|
||||||
begin
|
begin
|
||||||
|
if FDbgStream <> nil then FreeAndNil(FDbgStream);
|
||||||
|
FFEC.Free;
|
||||||
FLock.Free;
|
FLock.Free;
|
||||||
inherited Destroy;
|
inherited Destroy;
|
||||||
end;
|
end;
|
||||||
@@ -295,13 +345,21 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TBeaconDecoder.RunAcquisition;
|
procedure TBeaconDecoder.RunAcquisition;
|
||||||
// Индикатор наличия несущей маяка: вычитаем DC (offset Pluto), возводим в квадрат
|
// Грубый захват несущей маяка: вычитаем DC (offset Pluto), возводим в квадрат
|
||||||
// (снимаем BPSK), FFT → ищем самую сильную линию вне зоны DC. prominence (pk/среднее)
|
// (z² снимает BPSK → линия на 2·fc), FFT → самая сильная линия вне зоны DC.
|
||||||
// = «несущая видна» (FCarPresent). Частоту НЕ оцениваем — снос целиком ведёт
|
// prominence (pk/среднее) = «несущая видна» (FCarPresent).
|
||||||
// Costas + offload (см. ProcessSymbol); squaring-FFT давала неверный знак.
|
// ЧАСТОТУ ОЦЕНИВАЕМ: бин пика → 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
|
var
|
||||||
i, n, loBin: Integer;
|
i, n, loBin, pkBin: Integer;
|
||||||
mr, mi, re, im, mag, pk, sumMag, meanMag: Double;
|
mr, mi, re, im, mag, pk, sumMag, meanMag, fHz, fcHz, avgHz: Double;
|
||||||
begin
|
begin
|
||||||
n := BCN_NFFT;
|
n := BCN_NFFT;
|
||||||
// среднее (DC) для подавления offset Pluto
|
// среднее (DC) для подавления offset Pluto
|
||||||
@@ -320,17 +378,44 @@ begin
|
|||||||
|
|
||||||
// самая сильная линия, исключая ±loBin вокруг DC (там сидит остаток DC при Δf≈0)
|
// самая сильная линия, исключая ±loBin вокруг DC (там сидит остаток DC при Δf≈0)
|
||||||
loBin := 2;
|
loBin := 2;
|
||||||
pk := 0; sumMag := 0;
|
pk := 0; pkBin := 0; sumMag := 0;
|
||||||
for i := 0 to n - 1 do
|
for i := 0 to n - 1 do
|
||||||
begin
|
begin
|
||||||
mag := FFre[i] * FFre[i] + FFim[i] * FFim[i];
|
mag := FFre[i] * FFre[i] + FFim[i] * FFim[i];
|
||||||
sumMag := sumMag + mag;
|
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;
|
end;
|
||||||
meanMag := sumMag / n;
|
meanMag := sumMag / n;
|
||||||
if meanMag > 1e-30 then
|
if meanMag > 1e-30 then
|
||||||
FCarProm := FCarProm + 0.4 * (pk / meanMag - FCarProm);
|
FCarProm := FCarProm + 0.4 * (pk / meanMag - FCarProm);
|
||||||
FCarPresent := FCarProm > 20.0; // линия выше среднего → несущая в полосе
|
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;
|
end;
|
||||||
|
|
||||||
procedure TBeaconDecoder.ResetDSP;
|
procedure TBeaconDecoder.ResetDSP;
|
||||||
@@ -344,6 +429,7 @@ begin
|
|||||||
FHalf := False;
|
FHalf := False;
|
||||||
FMidI := 0; FMidQ := 0; FPrevI := 0; FPrevQ := 0;
|
FMidI := 0; FMidQ := 0; FPrevI := 0; FPrevQ := 0;
|
||||||
FYd0I := 0; FYd0Q := 0; FYd1I := 0; FYd1Q := 0;
|
FYd0I := 0; FYd0Q := 0; FYd1I := 0; FYd1Q := 0;
|
||||||
|
FYd2I := 0; FYd2Q := 0; FYd3I := 0; FYd3Q := 0;
|
||||||
FWsym := 0; FTimErrEMA := 0;
|
FWsym := 0; FTimErrEMA := 0;
|
||||||
FCarPh := 0; FCarFreq := 0;
|
FCarPh := 0; FCarFreq := 0;
|
||||||
FSigAmp := 0; FQVar := 0;
|
FSigAmp := 0; FQVar := 0;
|
||||||
@@ -351,8 +437,14 @@ begin
|
|||||||
FRingPos := 0; FRingCnt := 0;
|
FRingPos := 0; FRingCnt := 0;
|
||||||
FAcqPos := 0; FResidDf := 0; FResidPh := 0; FResidInc := 0;
|
FAcqPos := 0; FResidDf := 0; FResidPh := 0; FResidInc := 0;
|
||||||
FCarProm := 0; FCarPresent := False; FFreqLocked := False;
|
FCarProm := 0; FCarPresent := False; FFreqLocked := False;
|
||||||
|
FAcqArmed := True; FAcqCount := 0; FAcqSum := 0; // взводим грубый захват частоты
|
||||||
FLockHold := 0;
|
FLockHold := 0;
|
||||||
FPrevRre := 0; FPrevRim := 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;
|
end;
|
||||||
|
|
||||||
procedure TBeaconDecoder.Configure(SampleRateHz: Double);
|
procedure TBeaconDecoder.Configure(SampleRateHz: Double);
|
||||||
@@ -366,7 +458,7 @@ begin
|
|||||||
FFs := SampleRateHz;
|
FFs := SampleRateHz;
|
||||||
FDecim := Max(1, Round(FFs / BCN_TARGET_FS));
|
FDecim := Max(1, Round(FFs / BCN_TARGET_FS));
|
||||||
FFdec := FFs / FDecim;
|
FFdec := FFs / FDecim;
|
||||||
FSps := FFdec / BCN_BAUD;
|
FSps := FFdec / BCN_CHIP_BAUD; // отсчётов на ЧИП (Manchester), таймер на 800
|
||||||
|
|
||||||
FNcoInc := 2.0 * Pi * FOffsetHz / FFs;
|
FNcoInc := 2.0 * Pi * FOffsetHz / FFs;
|
||||||
|
|
||||||
@@ -472,7 +564,8 @@ procedure TBeaconDecoder.ProcessDecimated(const xi, xq: Double);
|
|||||||
// снимок констелляции защищён в GetScope собственным копированием полей-массивов.
|
// снимок констелляции защищён в GetScope собственным копированием полей-массивов.
|
||||||
var
|
var
|
||||||
k, idx: Integer;
|
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
|
begin
|
||||||
// остаточный снос несущей (по squaring-FFT acquisition) — поверх главного NCO
|
// остаточный снос несущей (по squaring-FFT acquisition) — поверх главного NCO
|
||||||
cr := Cos(FResidPh); sr := Sin(FResidPh);
|
cr := Cos(FResidPh); sr := Sin(FResidPh);
|
||||||
@@ -485,6 +578,17 @@ begin
|
|||||||
// Диагностика тракта (lock-независимо): RMS децимированного сигнала на входе RRC.
|
// Диагностика тракта (lock-независимо): RMS децимированного сигнала на входе RRC.
|
||||||
FDecRMS := FDecRMS + 0.001 * (Sqrt(ri * ri + ii * ii) - FDecRMS);
|
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, но
|
// Буфер для грубой carrier-acquisition: кладём сигнал ПОСЛЕ главного NCO, но
|
||||||
// ДО остаточного — тогда squaring-FFT меряет АБСОЛЮТНЫЙ остаток (не зависит от
|
// ДО остаточного — тогда squaring-FFT меряет АБСОЛЮТНЫЙ остаток (не зависит от
|
||||||
// FResidDf) → коррекция = прямое присвоение, без накопления и runaway.
|
// FResidDf) → коррекция = прямое присвоение, без накопления и runaway.
|
||||||
@@ -526,44 +630,39 @@ begin
|
|||||||
end;
|
end;
|
||||||
FSymMag := FRrcAmp; // диагностика силы сигнала (сырой уровень до AGC)
|
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;
|
FYd1I := FYd0I; FYd1Q := FYd0Q;
|
||||||
FYd0I := yi; FYd0Q := yq;
|
FYd0I := yi; FYd0Q := yq;
|
||||||
|
|
||||||
// Gardner: полу-символьные стробы. FMuAcc убывает на 1/отсчёт; при переходе
|
// Таймер ML signal×slope (как gr symbol_sync_cc, TED_SIGNAL_TIMES_SLOPE_ML):
|
||||||
// через 0 интерполируем и планируем следующий строб через ~FSps/2 отсчётов.
|
// лочит ЦЕНТР чипа по максимуму |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;
|
FMuAcc := FMuAcc - 1.0;
|
||||||
if FMuAcc <= 0.0 then
|
if FMuAcc <= 0.0 then
|
||||||
begin
|
begin
|
||||||
frac := FMuAcc + 1.0; // позиция в (0..1] между Yd1 и Yd0
|
frac := FMuAcc + 1.0; // позиция в (0..1]
|
||||||
if frac < 0 then frac := 0;
|
if frac < 0 then frac := 0;
|
||||||
yi := FYd1I + frac * (FYd0I - FYd1I);
|
// центр (между FYd2 и FYd1), early (FYd3/FYd2), late (FYd1/FYd0)
|
||||||
yq := FYd1Q + frac * (FYd0Q - FYd1Q);
|
ci := FYd2I + frac * (FYd1I - FYd2I);
|
||||||
|
cq := FYd2Q + frac * (FYd1Q - FYd2Q);
|
||||||
step := (FSps / 2.0) * (1.0 + FWsym); // базовый интервал до след. полу-символа
|
eai := FYd3I + frac * (FYd2I - FYd3I);
|
||||||
if FHalf then
|
eaq := FYd3Q + frac * (FYd2Q - FYd3Q);
|
||||||
begin
|
lai := FYd1I + frac * (FYd0I - FYd1I);
|
||||||
// середина символа — запоминаем для Gardner
|
laq := FYd1Q + frac * (FYd0Q - FYd1Q);
|
||||||
FMidI := yi; FMidQ := yq;
|
// производная dx ≈ late − early; ошибка = Re{conj(центр)·dx} (вращ.-инвариантна)
|
||||||
end
|
dri := lai - eai; drq := laq - eaq;
|
||||||
else
|
e := ci * dri + cq * drq;
|
||||||
begin
|
FTimErrEMA := FTimErrEMA + 0.01 * (Abs(e) - FTimErrEMA);
|
||||||
// центр символа → ошибка Gardner (вращательно-инвариантна):
|
FWsym := FWsym + 0.0005 * e; // интегратор периода
|
||||||
// e = Re{ conj(mid) * (curr - prev) } по компонентам
|
if FWsym > 0.02 then FWsym := 0.02;
|
||||||
e := FMidI * (yi - FPrevI) + FMidQ * (yq - FPrevQ);
|
if FWsym < -0.02 then FWsym := -0.02;
|
||||||
FTimErrEMA := FTimErrEMA + 0.01 * (Abs(e) - FTimErrEMA);
|
ProcessSymbol(ci, cq); // чип-центр → Costas → Manchester
|
||||||
// После AGC вход ~единичный, ошибка Gardner шумная (по логам wsym бился об
|
FMuAcc := FMuAcc + FSps * (1.0 + FWsym) + 0.005 * e; // период + проп. правка
|
||||||
// ±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;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -596,10 +695,16 @@ begin
|
|||||||
// Консервативно: сколько добавили в FResidInc, столько убрали из FCarFreq (без
|
// Консервативно: сколько добавили в FResidInc, столько убрали из FCarFreq (без
|
||||||
// рывка). Утечка медленная (TC~100 символов >> группозадержки RRC ~4 симв) — стабильно.
|
// рывка). Утечка медленная (TC~100 символов >> группозадержки RRC ~4 симв) — стабильно.
|
||||||
// Итог: Costas держит лишь мелкий быстрый остаток, весь медленный снос — в FResidInc.
|
// Итог: Costas держит лишь мелкий быстрый остаток, весь медленный снос — в FResidInc.
|
||||||
leak := 0.01 * FCarFreq;
|
// DEADBAND ~2.5 Гц: без него мелкий постоянный bias Costas (carHz~0.7) непрерывно
|
||||||
FResidInc := FResidInc + leak / FSps; // рад/символ → рад/децим-отсчёт
|
// интегрировался в 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); // для лога/диагностики
|
FResidDf := FResidInc * FFdec / (2.0 * Pi); // для лога/диагностики
|
||||||
FCarFreq := FCarFreq - leak;
|
|
||||||
FCarPh := FCarPh + FCarFreq + FCarAlpha * ce;
|
FCarPh := FCarPh + FCarFreq + FCarAlpha * ce;
|
||||||
if FCarPh > Pi then FCarPh := FCarPh - 2.0 * Pi
|
if FCarPh > Pi then FCarPh := FCarPh - 2.0 * Pi
|
||||||
else 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;
|
FRingQ[FRingPos] := di * na;
|
||||||
FRingPos := FRingPos + 1; if FRingPos >= BCN_SCOPE_PTS then FRingPos := 0;
|
FRingPos := FRingPos + 1; if FRingPos >= BCN_SCOPE_PTS then FRingPos := 0;
|
||||||
if FRingCnt < BCN_SCOPE_PTS then Inc(FRingCnt);
|
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;
|
end;
|
||||||
|
|
||||||
procedure TBeaconDecoder.GetScope(out S: TBeaconScope);
|
procedure TBeaconDecoder.GetScope(out S: TBeaconScope);
|
||||||
@@ -652,8 +822,14 @@ begin
|
|||||||
S.SymMag := FSymMag;
|
S.SymMag := FSymMag;
|
||||||
S.TimErr := FTimErrEMA;
|
S.TimErr := FTimErrEMA;
|
||||||
S.Wsym := FWsym;
|
S.Wsym := FWsym;
|
||||||
S.CarFreqHz := FCarFreq * BCN_BAUD / (2.0 * Pi); // рад/символ → Гц
|
S.CarFreqHz := FCarFreq * BCN_CHIP_BAUD / (2.0 * Pi); // рад/чип → Гц
|
||||||
S.LockHold := FLockHold;
|
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)
|
// SNR ≈ (E|I|)^2 / E[Q^2] (мощность сигнала по I против шума по Q)
|
||||||
if FQVar > 1e-12 then
|
if FQVar > 1e-12 then
|
||||||
@@ -686,4 +862,19 @@ begin
|
|||||||
end;
|
end;
|
||||||
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.
|
end.
|
||||||
|
|||||||
+341
@@ -0,0 +1,341 @@
|
|||||||
|
unit BeaconFEC;
|
||||||
|
|
||||||
|
{
|
||||||
|
QO-100 центральный маяк — FEC-бэкенд (Milestone 2).
|
||||||
|
|
||||||
|
Декодирует AO-40 FEC поверх потока мягких символов (400/с, после Manchester+
|
||||||
|
дифф-декода фронтенда). Цепочка (ровно как gr-satellites ao40_fec_deframer):
|
||||||
|
|
||||||
|
мягкие символы 400/с
|
||||||
|
→ distributed sync (65 бит, шаг 80, окно 5200, порог 8, обе полярности)
|
||||||
|
→ matrix deinterleave 80×65: out[i]=data[80*(i%65)+i/65], skip 65, take 5132
|
||||||
|
→ Viterbi r=1/2 k=7, полиномы [0x4F, -0x6D] (CCSDS), terminated → 2560 бит/320 байт
|
||||||
|
→ CCSDS additive descramble (LFSR mask 0xA9, seed 0xFF, len 7, XOR по битам)
|
||||||
|
→ 2× RS(255,223) укороч. pad=95 → (160,128), conventional basis, 2-way интерлив
|
||||||
|
→ 256 байт кадра → OnFrame.
|
||||||
|
|
||||||
|
Viterbi и RS — через libfec Карна (разделяемая libfec.so/.dll/.dylib, load-time
|
||||||
|
линковка как WDSP/IIO). libfec ставится отдельно на каждой ОС.
|
||||||
|
}
|
||||||
|
|
||||||
|
{$IFDEF FPC}{$MODE Delphi}{$ENDIF}
|
||||||
|
{$PACKRECORDS C}
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
Classes, SysUtils;
|
||||||
|
|
||||||
|
const
|
||||||
|
AO40_SYNC_LEN = 65;
|
||||||
|
AO40_SYNC_STEP = 80;
|
||||||
|
AO40_FRAME_SYMS = AO40_SYNC_LEN * AO40_SYNC_STEP; // 5200
|
||||||
|
AO40_IL_ROWS = 80;
|
||||||
|
AO40_IL_COLS = 65;
|
||||||
|
AO40_VITERBI_SYM = 5132; // после skip 65
|
||||||
|
AO40_DATA_BITS = 2560; // выход Viterbi (без 6 tail)
|
||||||
|
AO40_CONV_BYTES = AO40_DATA_BITS div 8; // 320
|
||||||
|
AO40_RS_NROOTS = 32;
|
||||||
|
AO40_RS_PAD = 95; // 255-160
|
||||||
|
AO40_RS_NN = 160; // укороченная длина
|
||||||
|
AO40_RS_KK = 128;
|
||||||
|
AO40_INTERLEAVE = 2;
|
||||||
|
AO40_FRAME_BYTES = AO40_RS_KK * AO40_INTERLEAVE; // 256
|
||||||
|
AO40_SYNC_THRESH = 8; // допуск ошибок синхры
|
||||||
|
|
||||||
|
type
|
||||||
|
TBeaconFrame = array[0..AO40_FRAME_BYTES-1] of Byte;
|
||||||
|
TBeaconFrameEvent = procedure(const Frame: TBeaconFrame; RSErrors: Integer) of object;
|
||||||
|
|
||||||
|
TBeaconFEC = class
|
||||||
|
private
|
||||||
|
FSync: array[0..AO40_SYNC_LEN-1] of Byte;
|
||||||
|
FRing: array[0..AO40_FRAME_SYMS-1] of Single;
|
||||||
|
FRingPos: Integer; // позиция следующей записи = старейший элемент
|
||||||
|
FRingCnt: Integer;
|
||||||
|
FCooldown: Integer; // не перепроверять сразу после успешного кадра
|
||||||
|
FVp: Pointer; // viterbi27 instance
|
||||||
|
FFrames: Int64;
|
||||||
|
FOnFrame: TBeaconFrameEvent;
|
||||||
|
|
||||||
|
function CheckSyncAt: Integer; // -1 нет; 0 норм. полярность; 1 инверсия
|
||||||
|
procedure DecodeCurrentFrame(Invert: Boolean);
|
||||||
|
procedure Descramble(var Buf: array of Byte; Count: Integer);
|
||||||
|
public
|
||||||
|
constructor Create;
|
||||||
|
destructor Destroy; override;
|
||||||
|
procedure Reset;
|
||||||
|
procedure PushSoftSymbol(const S: Single);
|
||||||
|
function SelfTest(out Msg: string): Boolean;
|
||||||
|
property Frames: Int64 read FFrames;
|
||||||
|
property OnFrame: TBeaconFrameEvent read FOnFrame write FOnFrame;
|
||||||
|
end;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
// ---------------- libfec bindings (Karn) ----------------
|
||||||
|
// Динамическая (load-time) линковка против разделяемой libfec — как WDSP/IIO.
|
||||||
|
// libfec ставится отдельно на каждой ОС (Linux: make install из исходников;
|
||||||
|
// Windows/macOS — отдельная сборка). НЕ статический {$LINKLIB}: на Arch системный
|
||||||
|
// libfec.a собран с LTO (slim-объекты без машинного кода) и не линкуется обычным ld.
|
||||||
|
const
|
||||||
|
{$IFDEF WINDOWS}
|
||||||
|
FEC_LIB = 'libfec.dll';
|
||||||
|
{$ENDIF}
|
||||||
|
{$IFDEF LINUX}
|
||||||
|
FEC_LIB = 'libfec.so';
|
||||||
|
{$ENDIF}
|
||||||
|
{$IFDEF DARWIN}
|
||||||
|
FEC_LIB = 'libfec.dylib';
|
||||||
|
{$ENDIF}
|
||||||
|
|
||||||
|
{$IFDEF UNIX}{$LINKLIB m}{$ENDIF} // libfec использует log() из libm
|
||||||
|
|
||||||
|
function create_viterbi27(len: LongInt): Pointer; cdecl; external FEC_LIB;
|
||||||
|
function init_viterbi27(vp: Pointer; starting_state: LongInt): LongInt; cdecl; external FEC_LIB;
|
||||||
|
function update_viterbi27_blk(vp: Pointer; syms: PByte; npairs: LongInt): LongInt; cdecl; external FEC_LIB;
|
||||||
|
function chainback_viterbi27(vp: Pointer; data: PByte; nbits: LongWord; endstate: LongWord): LongInt; cdecl; external FEC_LIB;
|
||||||
|
procedure delete_viterbi27(vp: Pointer); cdecl; external FEC_LIB;
|
||||||
|
|
||||||
|
function decode_rs_8(data: PByte; eras_pos: PLongInt; no_eras: LongInt; pad: LongInt): LongInt; cdecl; external FEC_LIB;
|
||||||
|
procedure encode_rs_8(data: PByte; parity: PByte; pad: LongInt); cdecl; external FEC_LIB;
|
||||||
|
|
||||||
|
// ---------------- helpers ----------------
|
||||||
|
|
||||||
|
function ParityB(x: LongWord): Byte; inline;
|
||||||
|
// чётность числа единичных битов: 1=нечётно, 0=чётно
|
||||||
|
begin
|
||||||
|
x := x xor (x shr 16);
|
||||||
|
x := x xor (x shr 8);
|
||||||
|
x := x xor (x shr 4);
|
||||||
|
x := x xor (x shr 2);
|
||||||
|
x := x xor (x shr 1);
|
||||||
|
Result := x and 1;
|
||||||
|
end;
|
||||||
|
|
||||||
|
const
|
||||||
|
// 65-битная распределённая синхра AO-40 (gr-satellites _syncword)
|
||||||
|
AO40_SYNCWORD: array[0..AO40_SYNC_LEN-1] of Byte = (
|
||||||
|
1,1,1,1,1,1,1,0,0,0,0,1,1,1,0,1,1,1,1,0,0,1,0,1,1,0,0,1,0,0,1,0,
|
||||||
|
0,0,0,0,0,1,0,0,0,1,0,0,1,1,0,0,0,1,0,1,1,1,0,1,0,1,1,0,1,1,0,0,0);
|
||||||
|
|
||||||
|
// ---------------- TBeaconFEC ----------------
|
||||||
|
|
||||||
|
constructor TBeaconFEC.Create;
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
for i := 0 to AO40_SYNC_LEN-1 do FSync[i] := AO40_SYNCWORD[i];
|
||||||
|
FVp := create_viterbi27(AO40_DATA_BITS);
|
||||||
|
Reset;
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TBeaconFEC.Destroy;
|
||||||
|
begin
|
||||||
|
if FVp <> nil then delete_viterbi27(FVp);
|
||||||
|
inherited Destroy;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TBeaconFEC.Reset;
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
for i := 0 to AO40_FRAME_SYMS-1 do FRing[i] := 0;
|
||||||
|
FRingPos := 0; FRingCnt := 0; FCooldown := 0;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TBeaconFEC.PushSoftSymbol(const S: Single);
|
||||||
|
var r: Integer;
|
||||||
|
begin
|
||||||
|
FRing[FRingPos] := S;
|
||||||
|
FRingPos := FRingPos + 1;
|
||||||
|
if FRingPos >= AO40_FRAME_SYMS then FRingPos := 0;
|
||||||
|
if FRingCnt < AO40_FRAME_SYMS then Inc(FRingCnt);
|
||||||
|
|
||||||
|
if FCooldown > 0 then begin Dec(FCooldown); Exit; end;
|
||||||
|
if FRingCnt < AO40_FRAME_SYMS then Exit;
|
||||||
|
|
||||||
|
r := CheckSyncAt;
|
||||||
|
if r >= 0 then
|
||||||
|
begin
|
||||||
|
DecodeCurrentFrame(r = 1);
|
||||||
|
FCooldown := AO40_FRAME_SYMS; // следующий кадр — через полный фрейм
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TBeaconFEC.CheckSyncAt: Integer;
|
||||||
|
// Кандидат-кадр = текущее содержимое кольца (старейший→новейший), старт = FRingPos.
|
||||||
|
// Синхра на относительных позициях j*step. Возвращает полярность или -1.
|
||||||
|
var
|
||||||
|
j, idx, hard, matches: Integer;
|
||||||
|
begin
|
||||||
|
matches := 0;
|
||||||
|
for j := 0 to AO40_SYNC_LEN-1 do
|
||||||
|
begin
|
||||||
|
idx := FRingPos + j * AO40_SYNC_STEP;
|
||||||
|
if idx >= AO40_FRAME_SYMS then Dec(idx, AO40_FRAME_SYMS);
|
||||||
|
if FRing[idx] > 0.0 then hard := 1 else hard := 0; // gr-конвенция: +soft = «1»
|
||||||
|
if hard = FSync[j] then Inc(matches);
|
||||||
|
end;
|
||||||
|
if matches >= AO40_SYNC_LEN - AO40_SYNC_THRESH then Exit(0); // норм. полярность
|
||||||
|
if matches <= AO40_SYNC_THRESH then Exit(1); // инверсия
|
||||||
|
Result := -1;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TBeaconFEC.Descramble(var Buf: array of Byte; Count: Integer);
|
||||||
|
// CCSDS additive descrambler = GNU Radio additive_scrambler_bb(0xA9, 0xFF, 7).
|
||||||
|
// Fibonacci LFSR, seed 0xFF, XOR с каждым битом (MSB-first), reset на старте кадра.
|
||||||
|
var
|
||||||
|
state: Byte;
|
||||||
|
i, b, outp, newbit, bit: Integer;
|
||||||
|
v: Byte;
|
||||||
|
begin
|
||||||
|
state := $FF;
|
||||||
|
for i := 0 to Count-1 do
|
||||||
|
begin
|
||||||
|
v := Buf[i];
|
||||||
|
for b := 7 downto 0 do
|
||||||
|
begin
|
||||||
|
outp := state and 1;
|
||||||
|
newbit := ParityB(state and $A9);
|
||||||
|
state := (state shr 1) or (newbit shl 7);
|
||||||
|
bit := (v shr b) and 1;
|
||||||
|
bit := bit xor outp;
|
||||||
|
if bit <> 0 then v := v or (1 shl b) else v := v and not (1 shl b);
|
||||||
|
end;
|
||||||
|
Buf[i] := v;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TBeaconFEC.DecodeCurrentFrame(Invert: Boolean);
|
||||||
|
var
|
||||||
|
win: array[0..AO40_FRAME_SYMS-1] of Single;
|
||||||
|
deint: array[0..AO40_FRAME_SYMS-1] of Single;
|
||||||
|
syms: array[0..AO40_VITERBI_SYM-1] of Byte;
|
||||||
|
conv: array[0..AO40_CONV_BYTES-1] of Byte;
|
||||||
|
cw: array[0..AO40_RS_NN-1] of Byte;
|
||||||
|
frame: TBeaconFrame;
|
||||||
|
i, idx, k, j, sgn, totErr, res: Integer;
|
||||||
|
v: Double;
|
||||||
|
begin
|
||||||
|
// 1. снимок кольца в хронологическом порядке (+ инверсия полярности)
|
||||||
|
if Invert then sgn := -1 else sgn := 1;
|
||||||
|
for i := 0 to AO40_FRAME_SYMS-1 do
|
||||||
|
begin
|
||||||
|
idx := FRingPos + i;
|
||||||
|
if idx >= AO40_FRAME_SYMS then Dec(idx, AO40_FRAME_SYMS);
|
||||||
|
win[i] := sgn * FRing[idx];
|
||||||
|
end;
|
||||||
|
|
||||||
|
// 2. матричный деинтерливер 80×65, out[i]=data[80*(i%65)+i/65]
|
||||||
|
for i := 0 to AO40_FRAME_SYMS-1 do
|
||||||
|
deint[i] := win[AO40_IL_ROWS * (i mod AO40_IL_COLS) + (i div AO40_IL_COLS)];
|
||||||
|
|
||||||
|
// 3. skip 65 (синхра) → 5132 мягких символа → байты для libfec (255=«1»).
|
||||||
|
// РЕМАП КОНВЕНЦИИ Viterbi: AO-40/gr-satellites кодирует полиномами [0x4F, -0x6D]
|
||||||
|
// (cc_decoder), а libfec viterbi27 ЗАШИВАЕТ [0x6D, 0x4F] БЕЗ инверсии. Эмпирически
|
||||||
|
// (брут-форс против gr post_viterbi_reference, см. историю) точное соответствие:
|
||||||
|
// libfec[2p] = -soft[2p+1] (своп пары + инверсия первого символа)
|
||||||
|
// libfec[2p+1] = soft[2p]
|
||||||
|
// Без ремапа Viterbi выдаёт мусор → RS не корректируется.
|
||||||
|
for i := 0 to (AO40_VITERBI_SYM div 2) - 1 do
|
||||||
|
begin
|
||||||
|
v := 128.0 - deint[AO40_SYNC_LEN + 2*i + 1] * 100.0; // -soft[2p+1]
|
||||||
|
if v > 255.0 then v := 255.0 else if v < 0.0 then v := 0.0;
|
||||||
|
syms[2*i] := Round(v);
|
||||||
|
v := 128.0 + deint[AO40_SYNC_LEN + 2*i] * 100.0; // soft[2p]
|
||||||
|
if v > 255.0 then v := 255.0 else if v < 0.0 then v := 0.0;
|
||||||
|
syms[2*i+1] := Round(v);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// 4. Viterbi r=1/2 k=7 terminated → 2560 бит = 320 байт (MSB-first)
|
||||||
|
init_viterbi27(FVp, 0);
|
||||||
|
update_viterbi27_blk(FVp, @syms[0], (AO40_DATA_BITS + 6)); // 2566 пар
|
||||||
|
chainback_viterbi27(FVp, @conv[0], AO40_DATA_BITS, 0);
|
||||||
|
|
||||||
|
// 5. CCSDS дескремблер
|
||||||
|
Descramble(conv, AO40_CONV_BYTES);
|
||||||
|
|
||||||
|
// 6. RS: 2-way байт-интерлив, decode_rs_8 укороч. pad=95
|
||||||
|
totErr := 0;
|
||||||
|
for j := 0 to AO40_INTERLEAVE-1 do
|
||||||
|
begin
|
||||||
|
for k := 0 to AO40_RS_NN-1 do
|
||||||
|
cw[k] := conv[j + k * AO40_INTERLEAVE];
|
||||||
|
res := decode_rs_8(@cw[0], nil, 0, AO40_RS_PAD);
|
||||||
|
if res < 0 then Exit; // некорректируемо → кадр невалиден
|
||||||
|
Inc(totErr, res);
|
||||||
|
for k := 0 to AO40_RS_KK-1 do
|
||||||
|
frame[j + k * AO40_INTERLEAVE] := cw[k];
|
||||||
|
end;
|
||||||
|
|
||||||
|
Inc(FFrames);
|
||||||
|
if Assigned(FOnFrame) then FOnFrame(frame, totErr);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// ---------------- self-test ----------------
|
||||||
|
|
||||||
|
function TBeaconFEC.SelfTest(out Msg: string): Boolean;
|
||||||
|
// Проверяет линковку libfec и базовую корректность: RS encode/decode round-trip
|
||||||
|
// с внесёнными ошибками (укорочение pad=95) + Viterbi encode/decode round-trip.
|
||||||
|
var
|
||||||
|
data: array[0..AO40_RS_KK-1] of Byte;
|
||||||
|
par: array[0..AO40_RS_NROOTS-1] of Byte;
|
||||||
|
cw: array[0..AO40_RS_NN-1] of Byte;
|
||||||
|
i, res, encst, sym0, sym1, errs: Integer;
|
||||||
|
bits: array[0..AO40_DATA_BITS-1] of Byte;
|
||||||
|
vsyms: array[0..AO40_VITERBI_SYM-1] of Byte;
|
||||||
|
outb: array[0..AO40_CONV_BYTES-1] of Byte;
|
||||||
|
b: Integer;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
|
||||||
|
// --- RS round-trip ---
|
||||||
|
for i := 0 to AO40_RS_KK-1 do data[i] := (i * 37 + 11) and $FF;
|
||||||
|
encode_rs_8(@data[0], @par[0], AO40_RS_PAD);
|
||||||
|
for i := 0 to AO40_RS_KK-1 do cw[i] := data[i];
|
||||||
|
for i := 0 to AO40_RS_NROOTS-1 do cw[AO40_RS_KK + i] := par[i];
|
||||||
|
// вносим 16 байтовых ошибок (= предел исправления для nroots=32)
|
||||||
|
for i := 0 to 15 do cw[i * 9] := cw[i * 9] xor $A5;
|
||||||
|
res := decode_rs_8(@cw[0], nil, 0, AO40_RS_PAD);
|
||||||
|
if res < 0 then begin Msg := 'RS decode FAILED (link/params?)'; Exit; end;
|
||||||
|
for i := 0 to AO40_RS_KK-1 do
|
||||||
|
if cw[i] <> data[i] then begin Msg := Format('RS mismatch at %d', [i]); Exit; end;
|
||||||
|
|
||||||
|
// --- Viterbi round-trip ---
|
||||||
|
// libfec viterbi27 зашивает полиномы V27POLYA=$6D, V27POLYB=$4F (CCSDS) БЕЗ
|
||||||
|
// инверсии и без API смены. Энкодер строго как у Карна (vtest27.c):
|
||||||
|
// sr=(sr shl 1)|bit; sym0=parity(sr & $6D); sym1=parity(sr & $4F).
|
||||||
|
// Мягкий «1» = 255, «0» = 0. + 6 хвостовых нулей (terminated).
|
||||||
|
Randomize;
|
||||||
|
for i := 0 to AO40_DATA_BITS-1 do bits[i] := Random(2);
|
||||||
|
encst := 0;
|
||||||
|
for i := 0 to AO40_DATA_BITS + 6 - 1 do
|
||||||
|
begin
|
||||||
|
if i < AO40_DATA_BITS then b := bits[i] else b := 0;
|
||||||
|
encst := ((encst shl 1) or b) and $7F;
|
||||||
|
sym0 := ParityB(encst and $6D);
|
||||||
|
sym1 := ParityB(encst and $4F);
|
||||||
|
vsyms[2*i] := sym0 * 255;
|
||||||
|
vsyms[2*i+1] := sym1 * 255;
|
||||||
|
end;
|
||||||
|
init_viterbi27(FVp, 0);
|
||||||
|
update_viterbi27_blk(FVp, @vsyms[0], AO40_DATA_BITS + 6);
|
||||||
|
chainback_viterbi27(FVp, @outb[0], AO40_DATA_BITS, 0);
|
||||||
|
errs := 0;
|
||||||
|
for i := 0 to AO40_DATA_BITS-1 do
|
||||||
|
begin
|
||||||
|
b := (outb[i div 8] shr (7 - (i mod 8))) and 1; // MSB-first
|
||||||
|
if b <> bits[i] then Inc(errs);
|
||||||
|
end;
|
||||||
|
if errs <> 0 then
|
||||||
|
begin
|
||||||
|
Msg := Format('Viterbi round-trip: %d/%d bit errors (convention mismatch)',
|
||||||
|
[errs, AO40_DATA_BITS]);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Msg := Format('OK: RS corrected 16 errs; Viterbi 0/%d bit errors', [AO40_DATA_BITS]);
|
||||||
|
Result := True;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
+7
-1
@@ -44,7 +44,7 @@ begin
|
|||||||
Caption := 'QO-100 Beacon — Constellation';
|
Caption := 'QO-100 Beacon — Constellation';
|
||||||
BorderStyle := bsSizeable;
|
BorderStyle := bsSizeable;
|
||||||
Width := 360;
|
Width := 360;
|
||||||
Height := 510;
|
Height := 560;
|
||||||
Position := poScreenCenter;
|
Position := poScreenCenter;
|
||||||
|
|
||||||
FBox := TPaintBox.Create(Self);
|
FBox := TPaintBox.Create(Self);
|
||||||
@@ -160,6 +160,12 @@ begin
|
|||||||
C.TextOut(14, py + 90, 'INVERT ' + IfThen(FCtl.BeaconDecodeInvert, 'ON', 'OFF'));
|
C.TextOut(14, py + 90, 'INVERT ' + IfThen(FCtl.BeaconDecodeInvert, 'ON', 'OFF'));
|
||||||
C.TextOut(14, py + 108, Format('RATE Fs %.0f Fdec %.0f sps %.1f',
|
C.TextOut(14, py + 108, Format('RATE Fs %.0f Fdec %.0f sps %.1f',
|
||||||
[S.FsHz, S.FdecHz, S.Sps]));
|
[S.FsHz, S.FdecHz, S.Sps]));
|
||||||
|
|
||||||
|
// ----- FEC (AO-40) -----
|
||||||
|
if S.HasFrame then C.Font.Color := FTheme.MeterOn else C.Font.Color := FTheme.TextDim;
|
||||||
|
C.TextOut(14, py + 132, Format('FRAMES %d (RS err %d)', [S.Frames, S.LastRSErr]));
|
||||||
|
C.Font.Color := FTheme.TextDim;
|
||||||
|
C.TextOut(14, py + 150, Format('MANCHESTER phase %d', [S.ManPhase]));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|||||||
+5
-2
@@ -1261,7 +1261,8 @@ begin
|
|||||||
BeaconLog(Format(
|
BeaconLog(Format(
|
||||||
'STATE decHz=%.1f center=%.1f off=%.1f | Fs=%.0f Fdec=%.0f sps=%.1f | ' +
|
'STATE decHz=%.1f center=%.1f off=%.1f | Fs=%.0f Fdec=%.0f sps=%.1f | ' +
|
||||||
'resid=%.1f prom=%.1f snr=%.1f baud=%.1f decRMS=%.5f symMag=%.5f ' +
|
'resid=%.1f prom=%.1f snr=%.1f baud=%.1f decRMS=%.5f symMag=%.5f ' +
|
||||||
'timErr=%.5f wsym=%.4f carHz=%.1f hold=%d | CAR=%d SYM=%d invert=%d lockOn=%d locked=%d',
|
'timErr=%.5f wsym=%.4f carHz=%.1f hold=%d | CAR=%d SYM=%d invert=%d lockOn=%d locked=%d ' +
|
||||||
|
'| manPh=%d m0=%.5f m1=%.5f frames=%d rsErr=%d',
|
||||||
[FBeaconDecHz, FCenterFreq, FBeaconLogScope.OffsetHz,
|
[FBeaconDecHz, FCenterFreq, FBeaconLogScope.OffsetHz,
|
||||||
FBeaconLogScope.FsHz, FBeaconLogScope.FdecHz, FBeaconLogScope.Sps,
|
FBeaconLogScope.FsHz, FBeaconLogScope.FdecHz, FBeaconLogScope.Sps,
|
||||||
FBeaconLogScope.ResidHz, FBeaconLogScope.CarProm, FBeaconLogScope.SNRdB,
|
FBeaconLogScope.ResidHz, FBeaconLogScope.CarProm, FBeaconLogScope.SNRdB,
|
||||||
@@ -1269,7 +1270,9 @@ begin
|
|||||||
FBeaconLogScope.TimErr, FBeaconLogScope.Wsym, FBeaconLogScope.CarFreqHz,
|
FBeaconLogScope.TimErr, FBeaconLogScope.Wsym, FBeaconLogScope.CarFreqHz,
|
||||||
FBeaconLogScope.LockHold,
|
FBeaconLogScope.LockHold,
|
||||||
Ord(FBeaconLogScope.CarrierLock), Ord(FBeaconLogScope.SymbolLock),
|
Ord(FBeaconLogScope.CarrierLock), Ord(FBeaconLogScope.SymbolLock),
|
||||||
Ord(FBeaconDecInvert), Ord(FBeaconLockOn), Ord(FBeaconLocked)]));
|
Ord(FBeaconDecInvert), Ord(FBeaconLockOn), Ord(FBeaconLocked),
|
||||||
|
FBeaconLogScope.ManPhase, FBeaconLogScope.ManM0, FBeaconLogScope.ManM1,
|
||||||
|
FBeaconLogScope.Frames, FBeaconLogScope.LastRSErr]));
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,10 @@
|
|||||||
<Filename Value="BeaconDecoder.pas"/>
|
<Filename Value="BeaconDecoder.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
</Unit>
|
</Unit>
|
||||||
|
<Unit>
|
||||||
|
<Filename Value="BeaconFEC.pas"/>
|
||||||
|
<IsPartOfProject Value="True"/>
|
||||||
|
</Unit>
|
||||||
<Unit>
|
<Unit>
|
||||||
<Filename Value="BeaconScopeForm.pas"/>
|
<Filename Value="BeaconScopeForm.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
program fectest;
|
||||||
|
{$MODE Delphi}
|
||||||
|
uses SysUtils, BeaconFEC;
|
||||||
|
var
|
||||||
|
fec: TBeaconFEC;
|
||||||
|
msg: string;
|
||||||
|
ok: Boolean;
|
||||||
|
begin
|
||||||
|
fec := TBeaconFEC.Create;
|
||||||
|
try
|
||||||
|
ok := fec.SelfTest(msg);
|
||||||
|
WriteLn('SelfTest: ', msg);
|
||||||
|
if ok then WriteLn('==> PASS') else WriteLn('==> FAIL');
|
||||||
|
finally
|
||||||
|
fec.Free;
|
||||||
|
end;
|
||||||
|
if not ok then Halt(1);
|
||||||
|
end.
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
program fectest2;
|
||||||
|
{$MODE Delphi}
|
||||||
|
{
|
||||||
|
Валидация TBeaconFEC против эталонных тест-векторов gr-satellites
|
||||||
|
(python/components/deframers/qa_ao40_fec_deframer_*). Гоним поток мягких
|
||||||
|
символов через PushSoftSymbol и сверяем выданный 256-байтовый кадр с эталоном.
|
||||||
|
Решает вопрос конвенции Viterbi (libfec [0x6D,0x4F] vs AO-40 [0x4F,-0x6D]).
|
||||||
|
}
|
||||||
|
uses SysUtils, Classes, BeaconFEC;
|
||||||
|
|
||||||
|
const
|
||||||
|
GR = '/home/vladimir/Документы/source/gr-satellites/python/components/deframers/';
|
||||||
|
SymPath = GR + 'qa_ao40_fec_deframer_symbols.f32';
|
||||||
|
|
||||||
|
// qa_ao40_fec_deframer.py: финальный кадр (256 байт)
|
||||||
|
FrameRef: array[0..255] of Byte = (
|
||||||
|
137, 0, 0, 0, 0, 0, 0, 0, 0, 31, 204, 0, 206, 2, 209, 0, 0,
|
||||||
|
7, 8, 9, 9, 0, 0, 5, 1, 1, 0, 64, 19, 47, 200, 242, 92,
|
||||||
|
143, 52, 35, 243, 186, 11, 93, 98, 116, 81, 199, 234, 250,
|
||||||
|
105, 74, 154, 159, 0, 9, 239, 160, 31, 244, 167, 234, 74,
|
||||||
|
198, 143, 17, 64, 17, 30, 16, 247, 1, 62, 32, 100, 0, 215,
|
||||||
|
139, 248, 215, 148, 200, 147, 168, 42, 218, 82, 166, 14, 88,
|
||||||
|
14, 200, 15, 78, 1, 29, 32, 90, 0, 219, 148, 168, 170, 138,
|
||||||
|
152, 19, 172, 105, 10, 166, 168, 16, 230, 16, 146, 15, 184,
|
||||||
|
1, 80, 32, 100, 0, 215, 150, 168, 193, 139, 72, 37, 171, 169,
|
||||||
|
202, 206, 157, 16, 118, 15, 201, 16, 85, 1, 58, 32, 90, 0, 215,
|
||||||
|
151, 41, 8, 140, 72, 79, 169, 106, 90, 242, 164, 16, 57, 15,
|
||||||
|
123, 15, 134, 1, 73, 32, 100, 0, 215, 148, 8, 208, 138, 216,
|
||||||
|
42, 173, 106, 90, 126, 180, 14, 83, 14, 155, 14, 183, 1, 9, 32,
|
||||||
|
90, 0, 219, 153, 168, 242, 143, 232, 56, 175, 170, 138, 194,
|
||||||
|
158, 14, 222, 15, 72, 14, 49, 1, 49, 32, 90, 0, 206, 155, 200,
|
||||||
|
255, 136, 104, 27, 178, 106, 90, 202, 167, 15, 195, 14, 116, 14,
|
||||||
|
88, 1, 52, 32, 90, 0, 215, 155, 57, 27, 151, 184, 197, 176, 43,
|
||||||
|
58, 214, 181, 1, 107, 0, 106, 2, 158, 0, 3, 32, 19, 0);
|
||||||
|
|
||||||
|
type
|
||||||
|
TFrameChecker = class
|
||||||
|
Got: Boolean;
|
||||||
|
Match: Boolean;
|
||||||
|
NErr: Integer;
|
||||||
|
RSErr: Integer;
|
||||||
|
procedure OnFrame(const Frame: TBeaconFrame; RSErrors: Integer);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TFrameChecker.OnFrame(const Frame: TBeaconFrame; RSErrors: Integer);
|
||||||
|
var i, e: Integer;
|
||||||
|
begin
|
||||||
|
if Got then Exit; // берём первый кадр
|
||||||
|
Got := True;
|
||||||
|
RSErr := RSErrors;
|
||||||
|
e := 0;
|
||||||
|
for i := 0 to 255 do
|
||||||
|
if Frame[i] <> FrameRef[i] then Inc(e);
|
||||||
|
NErr := e;
|
||||||
|
Match := (e = 0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
var
|
||||||
|
fec: TBeaconFEC;
|
||||||
|
chk: TFrameChecker;
|
||||||
|
fs: TFileStream;
|
||||||
|
n, i: Integer;
|
||||||
|
buf: array of Single;
|
||||||
|
begin
|
||||||
|
if not FileExists(SymPath) then
|
||||||
|
begin
|
||||||
|
WriteLn('symbols file not found: ', SymPath); Halt(2);
|
||||||
|
end;
|
||||||
|
fs := TFileStream.Create(SymPath, fmOpenRead);
|
||||||
|
try
|
||||||
|
n := fs.Size div 4;
|
||||||
|
SetLength(buf, n);
|
||||||
|
fs.ReadBuffer(buf[0], n * 4);
|
||||||
|
finally
|
||||||
|
fs.Free;
|
||||||
|
end;
|
||||||
|
WriteLn('loaded ', n, ' soft symbols');
|
||||||
|
|
||||||
|
chk := TFrameChecker.Create;
|
||||||
|
fec := TBeaconFEC.Create;
|
||||||
|
try
|
||||||
|
fec.OnFrame := chk.OnFrame;
|
||||||
|
for i := 0 to n - 1 do
|
||||||
|
fec.PushSoftSymbol(buf[i]);
|
||||||
|
|
||||||
|
WriteLn('frames decoded: ', fec.Frames);
|
||||||
|
if not chk.Got then
|
||||||
|
begin
|
||||||
|
WriteLn('==> FAIL: no frame decoded (sync not found / RS uncorrectable)');
|
||||||
|
Halt(1);
|
||||||
|
end;
|
||||||
|
WriteLn(Format('frame: %d/256 byte mismatches, RS errors=%d', [chk.NErr, chk.RSErr]));
|
||||||
|
if chk.Match then
|
||||||
|
WriteLn('==> PASS: frame matches gr-satellites reference')
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
WriteLn('==> FAIL: frame does not match reference');
|
||||||
|
Halt(1);
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
fec.Free;
|
||||||
|
chk.Free;
|
||||||
|
end;
|
||||||
|
end.
|
||||||
Reference in New Issue
Block a user