mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +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:
+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.
|
||||
Reference in New Issue
Block a user