mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
Replaced linear interpolation upsampling (192k→device-rate) with a polyphase FIR interpolator ×L. Prototype: windowed-sinc lowpass (Blackman, ~-58 dB sidelobes), TAPS*L long, cutoff TX_FIR_CUT=40k, normalized to unity per-phase DC gain (loudness unchanged). Images at multiples of 192k are now suppressed ~-58 dB instead of only sinc², cleaning up the uplink spectrum. BuildTxResampler designs coefficients per FTxRatio in StartTX; FTxDlI/Q delay line persists across SendDUCIQ calls, reset on StartTX/ClearDUCIQQueue. Removed FTxPrevI/Q. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
925 lines
36 KiB
ObjectPascal
925 lines
36 KiB
ObjectPascal
unit PlutoBackend;
|
||
|
||
{
|
||
Бэкенд ADALM-PLUTO (libiio / AD936x), реализует TRadioBackend.
|
||
|
||
Фаза 1 (текущая): discovery через iio_scan_context + Caps + открытие/закрытие
|
||
контекста (Connect/Disconnect) + геттеры состояния. RX/TX-потоки и реальная
|
||
подача IQ — фазы 2/4 (методы-заглушки сохраняют состояние).
|
||
|
||
Карта устройств/каналов Pluto (для фаз 2–4):
|
||
ad9361-phy:
|
||
RX LO — channel 'altvoltage0' (output), attr 'frequency'
|
||
TX LO — channel 'altvoltage1' (output), attr 'frequency'
|
||
RX rate/bw— channel 'voltage0' (input), attr 'sampling_frequency','rf_bandwidth'
|
||
RX gain — channel 'voltage0' (input), attr 'hardwaregain','gain_control_mode'
|
||
TX atten — channel 'voltage0' (output), attr 'hardwaregain' (отриц. дБ)
|
||
cf-ad9361-lpc — RX-стрим: 'voltage0'(I),'voltage1'(Q) input
|
||
cf-ad9361-dds-core-lpc — TX-стрим: 'voltage0'(I),'voltage1'(Q) output
|
||
}
|
||
|
||
{$IFDEF FPC}
|
||
{$MODE Delphi}
|
||
{$ENDIF}
|
||
|
||
interface
|
||
|
||
uses
|
||
Classes, SysUtils, ctypes,
|
||
RadioBackend, IIOBindings, HPSDRProtocol, Settings;
|
||
|
||
type
|
||
{ TPlutoBackend }
|
||
TPlutoBackend = class(TRadioBackend)
|
||
private
|
||
FCtx: Piio_context;
|
||
FPhy: Piio_device; // ad9361-phy (control)
|
||
FRxDev: Piio_device; // cf-ad9361-lpc (RX stream)
|
||
FTxDev: Piio_device; // cf-ad9361-dds-core-lpc (TX stream)
|
||
FConnected: Boolean;
|
||
FRunning: Boolean;
|
||
FDevice: TRadioDevice;
|
||
FLastError: string;
|
||
|
||
// Текущее состояние (применяется к железу в фазах 2/4)
|
||
FRXFreq: Double;
|
||
FTXFreq: Double;
|
||
FSampleRate: Integer;
|
||
FTransmitting: Boolean;
|
||
FGainMode: string; // gain_control_mode (manual/slow_attack/...)
|
||
FGainDb: Integer; // hardwaregain при ручном режиме
|
||
|
||
FTxAttDb: Double; // hardwaregain voltage0(out), дБ (<0); off на приёме
|
||
|
||
// RX-стрим (cf-ad9361-lpc): I/Q-каналы, буфер, поток refill.
|
||
FRxBuf: Piio_buffer;
|
||
FRxChI: Piio_channel;
|
||
FRxChQ: Piio_channel;
|
||
FRxThread: TThread;
|
||
FStreaming: Boolean;
|
||
FRxSeq: LongWord;
|
||
|
||
// TX-стрим (cf-ad9361-dds-core-lpc): I/Q output-каналы, буфер, поток push.
|
||
// Full-duplex: стрим живёт всё время Run; на приёме гоним нули (мощности нет
|
||
// за счёт макс. аттенюации). IQ от WDSP (192k) апсемплятся ×FTxRatio до рейта.
|
||
FTxBuf: Piio_buffer;
|
||
FTxChI: Piio_channel;
|
||
FTxChQ: Piio_channel;
|
||
FTxThread: TThread;
|
||
FTxStreaming: Boolean;
|
||
FTxRatio: Integer; // FSampleRate div 192000 (целочисленный апсемпл ×L)
|
||
FTxBufPairs: Integer; // размер TX-буфера в IQ-парах (по sample-rate, см. TxBufPairsForRate)
|
||
// Полифазный FIR-интерполятор: FTxCoef[p*TAPS + k] = h[p + k*L] (фаза p, отвод k).
|
||
// Линия задержки FTxDlI/Q хранит последние TAPS входных сэмплов между вызовами.
|
||
FTxCoef: array of Double;
|
||
FTxDlI: array of Double;
|
||
FTxDlQ: array of Double;
|
||
// FIFO int16-пар @ device-rate (кольцо): продюсер — SendDUCIQ, консьюмер — push-поток.
|
||
FTxFifo: array of SmallInt; // интерливнутые I,Q
|
||
FTxFifoCap: Integer; // ёмкость в SmallInt-элементах (пар*2)
|
||
FTxFifoHead: Integer; // индекс чтения (консьюмер)
|
||
FTxFifoTail: Integer; // индекс записи (продюсер)
|
||
FTxFifoCount: Integer; // занято элементов
|
||
FTxLock: TRTLCriticalSection;
|
||
|
||
// Низкоуровневые помощники применения атрибутов к ad9361-phy
|
||
procedure ApplyRxLO(Hz: Double);
|
||
procedure ApplyTxLO(Hz: Double);
|
||
procedure ApplySampleRate(Hz: Integer);
|
||
procedure BuildTxResampler; // (пере)строить полифазные коэффициенты под FTxRatio
|
||
procedure ApplyGain;
|
||
procedure ApplyTxAtten;
|
||
procedure StartRX;
|
||
procedure StopRX;
|
||
procedure StartTX;
|
||
procedure StopTX;
|
||
protected
|
||
function GetConnected: Boolean; override;
|
||
function GetRunning: Boolean; override;
|
||
function GetDevice: TRadioDevice; override;
|
||
function GetLastError: string; override;
|
||
public
|
||
constructor Create;
|
||
destructor Destroy; override;
|
||
|
||
function Caps: TBackendCaps; override;
|
||
|
||
function Discover(TimeoutMs: Integer = 3000): TRadioDeviceArray; override;
|
||
function Connect(const Dev: TRadioDevice): Boolean; override;
|
||
procedure Disconnect; override;
|
||
|
||
// Прямой probe конкретного URI (например 'ip:172.16.2.199' или 'usb:1.5.5').
|
||
// Открывает контекст, проверяет наличие ad9361-phy, читает hw_serial/hw_model,
|
||
// заполняет Dev и (если задан) дёргает OnDeviceFound. True — это AD936x-SDR.
|
||
// Нужен для сетевых устройств (без mDNS scan их иначе не найти).
|
||
function ProbeURI(const AURI: string; out Dev: TRadioDevice): Boolean;
|
||
|
||
procedure ConfigureDDCs(NumDDCs: Byte; SampleRate: Word; ADCSource: Byte = 0;
|
||
DitherEnabled: Boolean = True;
|
||
RandomEnabled: Boolean = True); override;
|
||
procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double;
|
||
DriveLevel: Byte = 100); override;
|
||
procedure UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte;
|
||
Transmitting, PAEnabled, AlexEnabled: Boolean); override;
|
||
procedure SendFullHP; override;
|
||
procedure SendPTT(Active: Boolean); override;
|
||
procedure SetRxGain(ModeIdx: Integer; GainDb: Integer); override;
|
||
procedure SetTxAtten(Db: Double); override;
|
||
procedure SendDUCIQ(const IData, QData: array of Integer); override;
|
||
procedure ClearDUCIQQueue; override;
|
||
function ReadTelemetry(out TempC, RSSIdB: Double): Boolean; override;
|
||
end;
|
||
|
||
// Стабильный 6-байтовый ключ (synthetic MAC) из serial/URI — для per-device
|
||
// настроек (Settings ключует по MAC). FNV-1a свёртка строки в 6 байт.
|
||
function PlutoSyntheticMAC(const S: string): TBytes;
|
||
|
||
implementation
|
||
|
||
const
|
||
PLUTO_MIN_SR = 521000; // нижний предел AD936x sampling_frequency
|
||
PLUTO_MAX_SR = 61440000;
|
||
PLUTO_MIN_HZ = 46875000; // AD9361 нативно (LibreSDR/AntSDR); AD9363 — 325 МГц
|
||
PLUTO_MAX_HZ = 6000000000.0;
|
||
PLUTO_DEF_SR = 1536000; // дефолтный rate если контроллер задал невалидный
|
||
RX_BUF_SAMPLES = 8192; // IQ-пар за один refill (~5.3 мс @ 1.536 MS/s)
|
||
PAIRS_PER_PKT = 238; // как DDC IQ HPSDR (238*6 = 1428 байт IQData)
|
||
TX_SRC_RATE = 192000; // фиксированный TX-рейт WDSP (TX_SAMPLE_RATE)
|
||
// Размер TX-буфера масштабируется от sample-rate (как SoapyPlutoSDR):
|
||
// round(rate/60) → следующая степень 2. Фикс 4096 «недостаточно для full-duplex
|
||
// выше 1 Msps — сэмплы дропаются» (комментарий драйвера). Минимум — TX_BUF_MIN.
|
||
TX_BUF_MIN = 4096; // нижняя граница (~7 мс @ 576k)
|
||
TX_BUF_MAX = 65536; // верхняя граница (защита от гигантских буферов)
|
||
// Полифазный анти-имидж интерполятор 192k→device-rate (вместо линейной интерполяции,
|
||
// которая оставляла образы на кратных 192k с подавлением только sinc²).
|
||
TX_FIR_TAPS = 16; // отводов на фазу (групп-задержка TAPS/2 входных сэмплов)
|
||
TX_FIR_CUT = 40000; // частота среза анти-имидж ФНЧ, Гц (покрывает SSB/NFM/AM/data)
|
||
PLUTO_TX_OFF = -89.75; // макс. аттенюация AD9361 — TX практически выключен
|
||
|
||
type
|
||
// RX-поток: блокирующий refill libiio → конверсия int16→24bit BE → OnDDCIQ.
|
||
// Работает с private-полями TPlutoBackend (одна единица компиляции).
|
||
TPlutoRXThread = class(TThread)
|
||
private
|
||
FBk: TPlutoBackend;
|
||
protected
|
||
procedure Execute; override;
|
||
public
|
||
constructor Create(ABk: TPlutoBackend);
|
||
end;
|
||
|
||
// TX-поток: тянет int16-пары из FIFO бэкенда (или нули при underrun), пишет в
|
||
// iio-буфер и push'ит. iio_buffer_push блокирует до приёма DMA → пейсинг ровно
|
||
// на device-rate. Работает с private-полями TPlutoBackend (одна единица компиляции).
|
||
TPlutoTXThread = class(TThread)
|
||
private
|
||
FBk: TPlutoBackend;
|
||
protected
|
||
procedure Execute; override;
|
||
public
|
||
constructor Create(ABk: TPlutoBackend);
|
||
end;
|
||
|
||
constructor TPlutoRXThread.Create(ABk: TPlutoBackend);
|
||
begin
|
||
FBk := ABk;
|
||
FreeOnTerminate := False;
|
||
inherited Create(False);
|
||
end;
|
||
|
||
procedure TPlutoRXThread.Execute;
|
||
var
|
||
n, step: ptrint;
|
||
pFirst, pEnd, p: PByte;
|
||
pkt: TDDCIQPacket;
|
||
outc: Integer;
|
||
vi, vq: LongInt;
|
||
u: LongWord;
|
||
|
||
procedure FlushPkt;
|
||
begin
|
||
Inc(FBk.FRxSeq);
|
||
pkt.Seq[0] := (FBk.FRxSeq shr 24) and $FF;
|
||
pkt.Seq[1] := (FBk.FRxSeq shr 16) and $FF;
|
||
pkt.Seq[2] := (FBk.FRxSeq shr 8) and $FF;
|
||
pkt.Seq[3] := FBk.FRxSeq and $FF;
|
||
pkt.SamplesPerFrame[0] := (outc shr 8) and $FF;
|
||
pkt.SamplesPerFrame[1] := outc and $FF;
|
||
if Assigned(FBk.FOnDDCIQ) then FBk.FOnDDCIQ(0, pkt);
|
||
outc := 0;
|
||
end;
|
||
|
||
begin
|
||
FillChar(pkt, SizeOf(pkt), 0);
|
||
pkt.BitsPerSample[1] := 24;
|
||
outc := 0;
|
||
while not Terminated do
|
||
begin
|
||
if FBk.FRxBuf = nil then begin Sleep(5); Continue; end;
|
||
n := iio_buffer_refill(FBk.FRxBuf);
|
||
if Terminated then Break;
|
||
if n <= 0 then begin Sleep(2); Continue; end;
|
||
|
||
step := iio_buffer_step(FBk.FRxBuf);
|
||
pFirst := iio_buffer_first(FBk.FRxBuf, FBk.FRxChI);
|
||
pEnd := iio_buffer_end(FBk.FRxBuf);
|
||
p := pFirst;
|
||
while PtrUInt(p) < PtrUInt(pEnd) do
|
||
begin
|
||
// I в смещении 0, Q в смещении 2 (два int16-канала, step=4).
|
||
// Q инвертируем (сопряжение): IQ-конвенция AD936x зеркальна ожидаемой WDSP,
|
||
// иначе спектр перевёрнут (USB↔LSB, перестройка вправо идёт влево).
|
||
vi := LongInt(PSmallInt(p)^) * 4096; // 12-bit (±2048) → 24-bit
|
||
vq := -LongInt(PSmallInt(p + 2)^) * 4096;
|
||
if vi > 8388607 then vi := 8388607 else if vi < -8388608 then vi := -8388608;
|
||
if vq > 8388607 then vq := 8388607 else if vq < -8388608 then vq := -8388608;
|
||
u := LongWord(vi) and $FFFFFF;
|
||
pkt.IQData[outc*6+0] := (u shr 16) and $FF;
|
||
pkt.IQData[outc*6+1] := (u shr 8) and $FF;
|
||
pkt.IQData[outc*6+2] := u and $FF;
|
||
u := LongWord(vq) and $FFFFFF;
|
||
pkt.IQData[outc*6+3] := (u shr 16) and $FF;
|
||
pkt.IQData[outc*6+4] := (u shr 8) and $FF;
|
||
pkt.IQData[outc*6+5] := u and $FF;
|
||
Inc(outc);
|
||
if outc >= PAIRS_PER_PKT then FlushPkt;
|
||
Inc(p, step);
|
||
end;
|
||
if outc > 0 then FlushPkt; // хвост блока
|
||
end;
|
||
end;
|
||
|
||
constructor TPlutoTXThread.Create(ABk: TPlutoBackend);
|
||
begin
|
||
FBk := ABk;
|
||
FreeOnTerminate := False;
|
||
inherited Create(False);
|
||
end;
|
||
|
||
procedure TPlutoTXThread.Execute;
|
||
var
|
||
step: ptrint;
|
||
pFirst, pEnd, p: PByte;
|
||
h: Integer;
|
||
n: ptrint;
|
||
begin
|
||
while not Terminated do
|
||
begin
|
||
if FBk.FTxBuf = nil then begin Sleep(5); Continue; end;
|
||
step := iio_buffer_step(FBk.FTxBuf);
|
||
pFirst := iio_buffer_first(FBk.FTxBuf, FBk.FTxChI);
|
||
pEnd := iio_buffer_end(FBk.FTxBuf);
|
||
|
||
// Наполняем буфер из FIFO; при underrun добиваем нулями (full-duplex idle).
|
||
EnterCriticalSection(FBk.FTxLock);
|
||
p := pFirst;
|
||
while PtrUInt(p) < PtrUInt(pEnd) do
|
||
begin
|
||
if FBk.FTxFifoCount > 0 then
|
||
begin
|
||
h := FBk.FTxFifoHead;
|
||
PSmallInt(p)^ := FBk.FTxFifo[h];
|
||
PSmallInt(p + 2)^ := FBk.FTxFifo[h + 1];
|
||
FBk.FTxFifoHead := (h + 2) mod FBk.FTxFifoCap;
|
||
Dec(FBk.FTxFifoCount, 2);
|
||
end
|
||
else
|
||
begin
|
||
PSmallInt(p)^ := 0;
|
||
PSmallInt(p + 2)^ := 0;
|
||
end;
|
||
Inc(p, step);
|
||
end;
|
||
LeaveCriticalSection(FBk.FTxLock);
|
||
|
||
if Terminated then Break;
|
||
n := iio_buffer_push(FBk.FTxBuf); // блокирует → пейсинг на device-rate
|
||
if n < 0 then Sleep(2);
|
||
end;
|
||
end;
|
||
|
||
function TxBufPairsForRate(Rate: Integer): Integer;
|
||
// Размер TX-буфера в IQ-парах под device-rate (как SoapyPlutoSDR): round(rate/60)
|
||
// округлённое вверх до степени 2, с клампом [TX_BUF_MIN..TX_BUF_MAX].
|
||
// 576k→16384, 1536k→32768, 3072k→65536.
|
||
var
|
||
Target, P: Integer;
|
||
begin
|
||
Target := (Rate + 30) div 60; // round(rate/60)
|
||
P := TX_BUF_MIN;
|
||
while P < Target do P := P shl 1; // следующая степень 2 ≥ Target
|
||
if P < TX_BUF_MIN then P := TX_BUF_MIN;
|
||
if P > TX_BUF_MAX then P := TX_BUF_MAX;
|
||
Result := P;
|
||
end;
|
||
|
||
function PlutoSyntheticMAC(const S: string): TBytes;
|
||
var
|
||
H: QWord;
|
||
I: Integer;
|
||
begin
|
||
// FNV-1a 64-bit, затем берём младшие 6 байт; первый байт делаем locally-
|
||
// administered/unicast (bit1=1, bit0=0), чтобы не коллизировать с реальными MAC.
|
||
H := QWord(14695981039346656037);
|
||
for I := 1 to Length(S) do
|
||
begin
|
||
H := H xor QWord(Ord(S[I]));
|
||
H := H * QWord(1099511628211);
|
||
end;
|
||
SetLength(Result, 6);
|
||
Result[0] := (Byte(H shr 40) and $FC) or $02;
|
||
Result[1] := Byte(H shr 32);
|
||
Result[2] := Byte(H shr 24);
|
||
Result[3] := Byte(H shr 16);
|
||
Result[4] := Byte(H shr 8);
|
||
Result[5] := Byte(H);
|
||
end;
|
||
|
||
{ TPlutoBackend }
|
||
|
||
constructor TPlutoBackend.Create;
|
||
begin
|
||
inherited Create;
|
||
FCtx := nil; FPhy := nil; FRxDev := nil; FTxDev := nil;
|
||
FConnected := False;
|
||
FRunning := False;
|
||
FSampleRate := 1536000;
|
||
FGainMode := 'slow_attack'; // аппаратный AGC по умолчанию
|
||
FGainDb := 40;
|
||
FRxBuf := nil; FRxChI := nil; FRxChQ := nil; FRxThread := nil;
|
||
FStreaming := False; FRxSeq := 0;
|
||
FTxBuf := nil; FTxChI := nil; FTxChQ := nil; FTxThread := nil;
|
||
FTxStreaming := False; FTxRatio := 8;
|
||
FTxAttDb := PLUTO_TX_OFF; // на старте TX выключен (макс. аттенюация)
|
||
// FIFO на ~4 TX-буфера запаса под джиттер продюсера/консьюмера. Окончательный
|
||
// размер выставляется в StartTX по фактическому device-rate (TxBufPairsForRate).
|
||
FTxBufPairs := TxBufPairsForRate(FSampleRate);
|
||
FTxFifoCap := FTxBufPairs * 2 * 4;
|
||
SetLength(FTxFifo, FTxFifoCap);
|
||
FTxFifoHead := 0; FTxFifoTail := 0; FTxFifoCount := 0;
|
||
InitCriticalSection(FTxLock);
|
||
FillChar(FDevice, SizeOf(FDevice), 0);
|
||
end;
|
||
|
||
destructor TPlutoBackend.Destroy;
|
||
begin
|
||
Disconnect;
|
||
DoneCriticalSection(FTxLock);
|
||
inherited Destroy;
|
||
end;
|
||
|
||
function TPlutoBackend.GetConnected: Boolean; begin Result := FConnected; end;
|
||
function TPlutoBackend.GetRunning: Boolean; begin Result := FRunning; end;
|
||
function TPlutoBackend.GetDevice: TRadioDevice; begin Result := FDevice; end;
|
||
function TPlutoBackend.GetLastError: string; begin Result := FLastError; end;
|
||
|
||
function TPlutoBackend.Caps: TBackendCaps;
|
||
begin
|
||
FillChar(Result, SizeOf(Result), 0);
|
||
Result.Kind := bkPluto;
|
||
Result.HasTX := True;
|
||
Result.HasPA := False; // нет PA → нет fwd/SWR/supply
|
||
Result.HasAlex := False;
|
||
Result.HasWideband := False;
|
||
Result.HasDitherRandom := False;
|
||
Result.HasHWMic := False;
|
||
Result.HasPLLStatus := False;
|
||
Result.HasHWGain := True; // manual gain / hw-AGC
|
||
Result.HasRFBandwidth := True;
|
||
Result.HasFullDuplex := True;
|
||
Result.MinSampleRate := PLUTO_MIN_SR;
|
||
Result.MaxSampleRate := PLUTO_MAX_SR;
|
||
Result.SampleRateMode := srmContinuous;
|
||
// Пресеты для SampleRateOverlay (все > 520 ksps, минимум AD936x).
|
||
SetLength(Result.RatePresets, 8);
|
||
Result.RatePresets[0] := 576000;
|
||
Result.RatePresets[1] := 768000;
|
||
Result.RatePresets[2] := 960000;
|
||
Result.RatePresets[3] := 1536000;
|
||
Result.RatePresets[4] := 2304000;
|
||
Result.RatePresets[5] := 3072000;
|
||
Result.RatePresets[6] := 3840000;
|
||
Result.RatePresets[7] := 5760000;
|
||
Result.MinFreqHz := PLUTO_MIN_HZ;
|
||
Result.MaxFreqHz := PLUTO_MAX_HZ;
|
||
end;
|
||
|
||
function TPlutoBackend.ProbeURI(const AURI: string; out Dev: TRadioDevice): Boolean;
|
||
// Открываем контекст по URI, проверяем что это AD936x-SDR (есть ad9361-phy),
|
||
// читаем hw_serial/hw_model. Контекст закрываем (это лишь probe, не Connect).
|
||
var
|
||
Ctx: Piio_context;
|
||
Ser, Model, IPStr: string;
|
||
PV: PAnsiChar;
|
||
MAC: TBytes;
|
||
begin
|
||
Result := False;
|
||
FillChar(Dev, SizeOf(Dev), 0);
|
||
if not IIOLoad then Exit;
|
||
|
||
Ctx := iio_create_context_from_uri(PAnsiChar(AnsiString(AURI)));
|
||
if Ctx = nil then Exit;
|
||
try
|
||
// Признак нашего железа — наличие управляющего устройства ad9361-phy.
|
||
if iio_context_find_device(Ctx, 'ad9361-phy') = nil then Exit;
|
||
|
||
Ser := '';
|
||
PV := iio_context_get_attr_value(Ctx, 'hw_serial');
|
||
if PV <> nil then Ser := string(PV);
|
||
if Ser = '' then Ser := AURI; // fallback: URI как идентификатор
|
||
|
||
Model := 'AD936x SDR';
|
||
PV := iio_context_get_attr_value(Ctx, 'hw_model');
|
||
if PV <> nil then Model := string(PV);
|
||
|
||
IPStr := AURI;
|
||
PV := iio_context_get_attr_value(Ctx, 'ip,ip-addr');
|
||
if PV <> nil then IPStr := string(PV);
|
||
|
||
MAC := PlutoSyntheticMAC(Ser);
|
||
Dev.Kind := bkPluto;
|
||
Dev.URI := AURI;
|
||
Dev.Serial := Ser;
|
||
Dev.Model := Model;
|
||
Dev.IPAddress := IPStr;
|
||
Dev.BoardType := 0;
|
||
Dev.Valid := True;
|
||
Move(MAC[0], Dev.MAC[0], 6);
|
||
Result := True;
|
||
finally
|
||
iio_context_destroy(Ctx);
|
||
end;
|
||
|
||
if Result and Assigned(FOnDeviceFound) then
|
||
FOnDeviceFound(Dev);
|
||
end;
|
||
|
||
function TPlutoBackend.Discover(TimeoutMs: Integer): TRadioDeviceArray;
|
||
var
|
||
Scan: Piio_scan_context;
|
||
Info: PPiio_context_info;
|
||
Cnt, I: ptrint;
|
||
Item: Piio_context_info;
|
||
URI: string;
|
||
Dev: TRadioDevice;
|
||
begin
|
||
SetLength(Result, 0);
|
||
if not IIOLoad then Exit;
|
||
|
||
// Сканируем backend 'usb' — надёжно и без зависимости от avahi/dnssd (её тянет
|
||
// backend=nil/'ip'). Для каждого найденного контекста делаем ProbeURI: детект по
|
||
// наличию ad9361-phy (а не по строке описания — клоны вроде LibreSDR не содержат
|
||
// 'pluto'). Сетевые устройства (ip:) находим отдельным ProbeURI по сохранённым/
|
||
// введённым адресам (см. контроллер).
|
||
Scan := iio_create_scan_context('usb', 0);
|
||
if Scan = nil then Exit;
|
||
try
|
||
Info := nil;
|
||
Cnt := iio_scan_context_get_info_list(Scan, Info);
|
||
if Cnt <= 0 then Exit;
|
||
try
|
||
I := 0;
|
||
while I < Cnt do
|
||
begin
|
||
Item := PPiio_context_info(PByte(Info) + I * SizeOf(Pointer))^;
|
||
URI := string(iio_context_info_get_uri(Item));
|
||
Inc(I);
|
||
if ProbeURI(URI, Dev) then
|
||
begin
|
||
SetLength(Result, Length(Result) + 1);
|
||
Result[High(Result)] := Dev;
|
||
end;
|
||
end;
|
||
finally
|
||
iio_context_info_list_free(Info);
|
||
end;
|
||
finally
|
||
iio_scan_context_destroy(Scan);
|
||
end;
|
||
end;
|
||
|
||
function TPlutoBackend.Connect(const Dev: TRadioDevice): Boolean;
|
||
var
|
||
M: TBytes;
|
||
begin
|
||
Result := False;
|
||
if not IIOLoad then begin FLastError := 'libiio not available'; Exit; end;
|
||
if FConnected then Disconnect;
|
||
|
||
FDevice := Dev;
|
||
// Гарантируем стабильный synthetic-MAC из serial (ключ per-device настроек),
|
||
// даже если запись пришла без MAC (сохранённое Pluto-устройство).
|
||
if FDevice.Serial <> '' then
|
||
begin
|
||
M := PlutoSyntheticMAC(FDevice.Serial);
|
||
Move(M[0], FDevice.MAC[0], 6);
|
||
end;
|
||
FCtx := iio_create_context_from_uri(PAnsiChar(AnsiString(Dev.URI)));
|
||
if FCtx = nil then
|
||
begin
|
||
FLastError := 'Cannot open Pluto context: ' + Dev.URI;
|
||
Exit;
|
||
end;
|
||
|
||
FPhy := iio_context_find_device(FCtx, 'ad9361-phy');
|
||
FRxDev := iio_context_find_device(FCtx, 'cf-ad9361-lpc');
|
||
FTxDev := iio_context_find_device(FCtx, 'cf-ad9361-dds-core-lpc');
|
||
if FPhy = nil then
|
||
begin
|
||
FLastError := 'ad9361-phy not found in context';
|
||
iio_context_destroy(FCtx); FCtx := nil;
|
||
Exit;
|
||
end;
|
||
|
||
FConnected := True;
|
||
// Приводим железо в известное состояние: валидный rate + усиление.
|
||
if FSampleRate < PLUTO_MIN_SR then FSampleRate := PLUTO_DEF_SR;
|
||
ApplySampleRate(FSampleRate);
|
||
ApplyGain;
|
||
ApplyTxAtten; // TX заглушён (макс. аттенюация) до явного PTT
|
||
Result := True;
|
||
end;
|
||
|
||
procedure TPlutoBackend.Disconnect;
|
||
begin
|
||
StopTX;
|
||
StopRX;
|
||
if FCtx <> nil then
|
||
begin
|
||
iio_context_destroy(FCtx);
|
||
FCtx := nil;
|
||
end;
|
||
FPhy := nil; FRxDev := nil; FTxDev := nil;
|
||
FConnected := False;
|
||
FRunning := False;
|
||
end;
|
||
|
||
// ---- Применение атрибутов к ad9361-phy ----
|
||
|
||
procedure TPlutoBackend.ApplyRxLO(Hz: Double);
|
||
var Ch: Piio_channel;
|
||
begin
|
||
if (FPhy = nil) or (Hz <= 0) then Exit;
|
||
Ch := iio_device_find_channel(FPhy, 'altvoltage0', 1);
|
||
if Ch <> nil then
|
||
iio_channel_attr_write_longlong(Ch, 'frequency', Round(Hz));
|
||
end;
|
||
|
||
procedure TPlutoBackend.ApplyTxLO(Hz: Double);
|
||
var Ch: Piio_channel;
|
||
begin
|
||
if (FPhy = nil) or (Hz <= 0) then Exit;
|
||
Ch := iio_device_find_channel(FPhy, 'altvoltage1', 1);
|
||
if Ch <> nil then
|
||
iio_channel_attr_write_longlong(Ch, 'frequency', Round(Hz));
|
||
end;
|
||
|
||
procedure TPlutoBackend.ApplySampleRate(Hz: Integer);
|
||
var Ch: Piio_channel; WasStreaming, WasTxStreaming: Boolean;
|
||
begin
|
||
if (FPhy = nil) or (Hz < PLUTO_MIN_SR) then Exit;
|
||
WasStreaming := FStreaming;
|
||
WasTxStreaming := FTxStreaming;
|
||
if WasTxStreaming then StopTX; // device-rate общий для RX/TX — пересоздать оба
|
||
if WasStreaming then StopRX; // менять rate с открытым буфером нельзя
|
||
|
||
// Предпочтительно ad9361_set_bb_rate: настраивает BBPLL И загружает FIR-таблицу
|
||
// децимации под частоту. Без этого на холодную (битый/дефолтный FIR) спектр —
|
||
// широкая палка по центру, не реагирует на перестройку. Fallback — прямая
|
||
// запись sampling_frequency (работает, если FIR уже корректен).
|
||
if Assigned(ad9361_set_bb_rate) then
|
||
ad9361_set_bb_rate(FPhy, culong(Hz))
|
||
else
|
||
begin
|
||
Ch := iio_device_find_channel(FPhy, 'voltage0', 0);
|
||
if Ch <> nil then
|
||
iio_channel_attr_write_longlong(Ch, 'sampling_frequency', Hz);
|
||
end;
|
||
|
||
// RF-полоса отдельно (set_bb_rate её не трогает).
|
||
Ch := iio_device_find_channel(FPhy, 'voltage0', 0);
|
||
if Ch <> nil then
|
||
iio_channel_attr_write_longlong(Ch, 'rf_bandwidth', Hz);
|
||
|
||
if WasStreaming then StartRX;
|
||
if WasTxStreaming then StartTX; // пересоздаём TX с новым FTxRatio
|
||
end;
|
||
|
||
procedure TPlutoBackend.BuildTxResampler;
|
||
// Полифазный FIR-интерполятор ×L (L=FTxRatio): прототип — оконный sinc-ФНЧ длиной
|
||
// TAPS*L с частотой среза TX_FIR_CUT, нормированный так, что DC-усиление любой фазы
|
||
// ≈ 1 (полная сумма = L). Раскладка в фазы: coef[p*TAPS+k] = proto[p + k*L].
|
||
var
|
||
L, M, n, p, k: Integer;
|
||
fc, x, s, w, sum, scale: Double;
|
||
proto: array of Double;
|
||
begin
|
||
L := FTxRatio;
|
||
if L < 1 then L := 1;
|
||
M := TX_FIR_TAPS * L;
|
||
SetLength(proto, M);
|
||
fc := TX_FIR_CUT / FSampleRate; // нормированный срез (циклов/сэмпл на выходе)
|
||
if fc > 0.49 then fc := 0.49;
|
||
sum := 0;
|
||
for n := 0 to M - 1 do
|
||
begin
|
||
x := n - (M - 1) / 2.0;
|
||
if Abs(x) < 1e-9 then s := 2.0 * fc
|
||
else s := Sin(2.0 * Pi * fc * x) / (Pi * x);
|
||
// окно Блэкмана (~ −58 дБ боковые)
|
||
w := 0.42 - 0.5 * Cos(2.0 * Pi * n / (M - 1)) + 0.08 * Cos(4.0 * Pi * n / (M - 1));
|
||
proto[n] := s * w;
|
||
sum := sum + proto[n];
|
||
end;
|
||
if sum = 0 then sum := 1;
|
||
scale := L / sum; // DC-усиление на фазу ≈ 1
|
||
SetLength(FTxCoef, M);
|
||
for p := 0 to L - 1 do
|
||
for k := 0 to TX_FIR_TAPS - 1 do
|
||
FTxCoef[p * TX_FIR_TAPS + k] := proto[p + k * L] * scale;
|
||
// линия задержки на TAPS входных сэмплов (сброс)
|
||
SetLength(FTxDlI, TX_FIR_TAPS);
|
||
SetLength(FTxDlQ, TX_FIR_TAPS);
|
||
for k := 0 to TX_FIR_TAPS - 1 do begin FTxDlI[k] := 0; FTxDlQ[k] := 0; end;
|
||
end;
|
||
|
||
procedure TPlutoBackend.ApplyGain;
|
||
var Ch: Piio_channel;
|
||
begin
|
||
if FPhy = nil then Exit;
|
||
Ch := iio_device_find_channel(FPhy, 'voltage0', 0); // RX gain — input voltage0
|
||
if Ch = nil then Exit;
|
||
iio_channel_attr_write(Ch, 'gain_control_mode', PAnsiChar(AnsiString(FGainMode)));
|
||
if FGainMode = 'manual' then
|
||
iio_channel_attr_write_longlong(Ch, 'hardwaregain', FGainDb);
|
||
end;
|
||
|
||
procedure TPlutoBackend.ApplyTxAtten;
|
||
var Ch: Piio_channel;
|
||
begin
|
||
if FPhy = nil then Exit;
|
||
Ch := iio_device_find_channel(FPhy, 'voltage0', 1); // TX att — output voltage0
|
||
if Ch = nil then Exit;
|
||
// hardwaregain выходного тракта — отрицательные дБ, шаг 0.25 (0=макс. мощность).
|
||
if Assigned(iio_channel_attr_write_double) then
|
||
iio_channel_attr_write_double(Ch, 'hardwaregain', FTxAttDb);
|
||
end;
|
||
|
||
procedure TPlutoBackend.StartRX;
|
||
begin
|
||
if FStreaming or not FConnected or (FRxDev = nil) then Exit;
|
||
FRxChI := iio_device_find_channel(FRxDev, 'voltage0', 0);
|
||
FRxChQ := iio_device_find_channel(FRxDev, 'voltage1', 0);
|
||
if (FRxChI = nil) or (FRxChQ = nil) then
|
||
begin FLastError := 'RX I/Q channels not found'; Exit; end;
|
||
iio_channel_enable(FRxChI);
|
||
iio_channel_enable(FRxChQ);
|
||
if Assigned(iio_device_set_kernel_buffers_count) then
|
||
iio_device_set_kernel_buffers_count(FRxDev, 4);
|
||
FRxBuf := iio_device_create_buffer(FRxDev, RX_BUF_SAMPLES, False);
|
||
if FRxBuf = nil then begin FLastError := 'RX create_buffer failed'; Exit; end;
|
||
FStreaming := True;
|
||
FRxThread := TPlutoRXThread.Create(Self);
|
||
end;
|
||
|
||
procedure TPlutoBackend.StopRX;
|
||
begin
|
||
if FRxThread <> nil then
|
||
begin
|
||
FRxThread.Terminate;
|
||
if (FRxBuf <> nil) and Assigned(iio_buffer_cancel) then
|
||
iio_buffer_cancel(FRxBuf); // разблокировать refill немедленно
|
||
FRxThread.WaitFor;
|
||
FreeAndNil(FRxThread);
|
||
end;
|
||
FStreaming := False;
|
||
if FRxBuf <> nil then
|
||
begin
|
||
iio_buffer_destroy(FRxBuf);
|
||
FRxBuf := nil;
|
||
end;
|
||
end;
|
||
|
||
procedure TPlutoBackend.StartTX;
|
||
begin
|
||
if FTxStreaming or not FConnected or (FTxDev = nil) then Exit;
|
||
FTxChI := iio_device_find_channel(FTxDev, 'voltage0', 1); // output I
|
||
FTxChQ := iio_device_find_channel(FTxDev, 'voltage1', 1); // output Q
|
||
if (FTxChI = nil) or (FTxChQ = nil) then
|
||
begin FLastError := 'TX I/Q channels not found'; Exit; end;
|
||
iio_channel_enable(FTxChI);
|
||
iio_channel_enable(FTxChQ);
|
||
// Апсемпл-ратио под текущий device-rate (рейты-пресеты кратны 192k → целое).
|
||
FTxRatio := FSampleRate div TX_SRC_RATE;
|
||
if FTxRatio < 1 then FTxRatio := 1;
|
||
BuildTxResampler; // полифазные коэффициенты + сброс линии задержки под FTxRatio
|
||
// Размер push-буфера под текущий rate + FIFO на ~4 таких буфера запаса.
|
||
FTxBufPairs := TxBufPairsForRate(FSampleRate);
|
||
EnterCriticalSection(FTxLock);
|
||
FTxFifoCap := FTxBufPairs * 2 * 4;
|
||
if Length(FTxFifo) <> FTxFifoCap then SetLength(FTxFifo, FTxFifoCap);
|
||
FTxFifoHead := 0; FTxFifoTail := 0; FTxFifoCount := 0;
|
||
LeaveCriticalSection(FTxLock);
|
||
FTxBuf := iio_device_create_buffer(FTxDev, FTxBufPairs, False);
|
||
if FTxBuf = nil then begin FLastError := 'TX create_buffer failed'; Exit; end;
|
||
ApplyTxAtten; // стартуем с текущей аттенюации (по умолчанию OFF до PTT)
|
||
FTxStreaming := True;
|
||
FTxThread := TPlutoTXThread.Create(Self);
|
||
end;
|
||
|
||
procedure TPlutoBackend.StopTX;
|
||
begin
|
||
if FTxThread <> nil then
|
||
begin
|
||
FTxThread.Terminate;
|
||
if (FTxBuf <> nil) and Assigned(iio_buffer_cancel) then
|
||
iio_buffer_cancel(FTxBuf); // разблокировать push немедленно
|
||
FTxThread.WaitFor;
|
||
FreeAndNil(FTxThread);
|
||
end;
|
||
FTxStreaming := False;
|
||
if FTxBuf <> nil then
|
||
begin
|
||
iio_buffer_destroy(FTxBuf);
|
||
FTxBuf := nil;
|
||
end;
|
||
end;
|
||
|
||
// ---- Команды контроллера (фаза 1: сохраняем состояние + базовое применение) ----
|
||
|
||
procedure TPlutoBackend.ConfigureDDCs(NumDDCs: Byte; SampleRate: Word;
|
||
ADCSource: Byte; DitherEnabled, RandomEnabled: Boolean);
|
||
begin
|
||
// SampleRate приходит в ksps (как у HPSDR ConfigureDDCs). Pluto — в Гц.
|
||
// Контроллер может прислать HPSDR-наследие (<520k) — клампим к дефолту.
|
||
FSampleRate := Integer(SampleRate) * 1000;
|
||
if FSampleRate < PLUTO_MIN_SR then FSampleRate := PLUTO_DEF_SR;
|
||
if FConnected then ApplySampleRate(FSampleRate); // перезапустит RX при смене
|
||
end;
|
||
|
||
procedure TPlutoBackend.SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double;
|
||
DriveLevel: Byte);
|
||
begin
|
||
FRXFreq := DDC0FreqHz;
|
||
FTXFreq := DUCFreqHz;
|
||
FRunning := Run;
|
||
if FConnected then
|
||
begin
|
||
ApplyRxLO(FRXFreq);
|
||
ApplyTxLO(FTXFreq);
|
||
// Full-duplex: оба стрима живут весь Run. TX по умолчанию заглушён макс.
|
||
// аттенюацией (FTxAttDb=OFF) — реальная мощность включается из контроллера
|
||
// через SetTxAtten на PTT.
|
||
if Run then begin StartRX; StartTX; end
|
||
else begin StopTX; StopRX; end;
|
||
end;
|
||
end;
|
||
|
||
procedure TPlutoBackend.UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte;
|
||
Transmitting, PAEnabled, AlexEnabled: Boolean);
|
||
begin
|
||
FRXFreq := RXFreqHz;
|
||
FTXFreq := TXFreqHz;
|
||
FTransmitting := Transmitting;
|
||
if FConnected then
|
||
begin
|
||
ApplyRxLO(FRXFreq);
|
||
ApplyTxLO(FTXFreq);
|
||
end;
|
||
end;
|
||
|
||
procedure TPlutoBackend.SendFullHP;
|
||
begin
|
||
// У Pluto нет HP-пакета: частоты/усиление применяются напрямую в UpdateState/
|
||
// SetRunAndFreq. Здесь — no-op (состояние уже применено).
|
||
end;
|
||
|
||
procedure TPlutoBackend.SendPTT(Active: Boolean);
|
||
begin
|
||
FTransmitting := Active;
|
||
// Реальный RX↔TX (вкл/выкл стримов) — фаза 4.
|
||
end;
|
||
|
||
procedure TPlutoBackend.SetRxGain(ModeIdx: Integer; GainDb: Integer);
|
||
begin
|
||
case ModeIdx of
|
||
1: FGainMode := 'fast_attack';
|
||
2: FGainMode := 'slow_attack';
|
||
3: FGainMode := 'hybrid';
|
||
else FGainMode := 'manual';
|
||
end;
|
||
FGainDb := GainDb;
|
||
if FConnected then ApplyGain;
|
||
end;
|
||
|
||
procedure TPlutoBackend.SetTxAtten(Db: Double);
|
||
begin
|
||
if Db > 0 then Db := 0;
|
||
if Db < PLUTO_TX_OFF then Db := PLUTO_TX_OFF;
|
||
FTxAttDb := Db;
|
||
if FConnected then ApplyTxAtten;
|
||
end;
|
||
|
||
procedure TPlutoBackend.SendDUCIQ(const IData, QData: array of Integer);
|
||
// IData/QData — 24-bit пары @ TX_SRC_RATE (192k) от WDSP через контроллер.
|
||
// Полифазный FIR-интерполятор ×FTxRatio (анти-имидж ФНЧ, см. BuildTxResampler):
|
||
// на каждый входной сэмпл выдаём L выходных, конвертим 24→int16 (>>8, full-scale
|
||
// MSB-aligned), Q инвертируем (та же конвенция, что и на RX) и кладём в FIFO.
|
||
// Underrun в push-потоке добивается нулями, overflow — отбрасываем старое.
|
||
function Clamp16(V: Int64): SmallInt; inline;
|
||
begin
|
||
if V > 32767 then Result := 32767
|
||
else if V < -32768 then Result := -32768
|
||
else Result := SmallInt(V);
|
||
end;
|
||
var
|
||
i, p, k, t, L, taps, base, n: Integer;
|
||
ai, aq: Double;
|
||
begin
|
||
if (not FTxStreaming) or (FTxRatio < 1) or (Length(FTxCoef) = 0) then Exit;
|
||
n := Length(IData);
|
||
if Length(QData) < n then n := Length(QData);
|
||
L := FTxRatio;
|
||
taps := TX_FIR_TAPS;
|
||
EnterCriticalSection(FTxLock);
|
||
try
|
||
for i := 0 to n - 1 do
|
||
begin
|
||
// сдвигаем линию задержки (FTxDl[0] = новейший входной сэмпл)
|
||
for k := taps - 1 downto 1 do
|
||
begin FTxDlI[k] := FTxDlI[k - 1]; FTxDlQ[k] := FTxDlQ[k - 1]; end;
|
||
FTxDlI[0] := IData[i]; FTxDlQ[0] := QData[i];
|
||
// L выходных сэмплов — фазы p = 0..L-1, каждая = свёртка с подфильтром
|
||
for p := 0 to L - 1 do
|
||
begin
|
||
ai := 0; aq := 0;
|
||
base := p * taps;
|
||
for k := 0 to taps - 1 do
|
||
begin
|
||
ai := ai + FTxCoef[base + k] * FTxDlI[k];
|
||
aq := aq + FTxCoef[base + k] * FTxDlQ[k];
|
||
end;
|
||
if FTxFifoCount + 2 > FTxFifoCap then
|
||
begin
|
||
// overflow: сбрасываем самые старые пары, держим хвост (минимум латентности)
|
||
FTxFifoHead := (FTxFifoHead + 2) mod FTxFifoCap;
|
||
Dec(FTxFifoCount, 2);
|
||
end;
|
||
t := FTxFifoTail;
|
||
FTxFifo[t] := Clamp16(Round(ai / 256.0)); // 24→16 бит
|
||
FTxFifo[t + 1] := Clamp16(-Round(aq / 256.0)); // Q инверсия (как RX)
|
||
FTxFifoTail := (t + 2) mod FTxFifoCap;
|
||
Inc(FTxFifoCount, 2);
|
||
end;
|
||
end;
|
||
finally
|
||
LeaveCriticalSection(FTxLock);
|
||
end;
|
||
end;
|
||
|
||
procedure TPlutoBackend.ClearDUCIQQueue;
|
||
var k: Integer;
|
||
begin
|
||
EnterCriticalSection(FTxLock);
|
||
FTxFifoHead := 0; FTxFifoTail := 0; FTxFifoCount := 0;
|
||
for k := 0 to Length(FTxDlI) - 1 do begin FTxDlI[k] := 0; FTxDlQ[k] := 0; end;
|
||
LeaveCriticalSection(FTxLock);
|
||
end;
|
||
|
||
function TPlutoBackend.ReadTelemetry(out TempC, RSSIdB: Double): Boolean;
|
||
// Опрос аппаратной телеметрии ad9361-phy (вызывается контроллером, не из RX-потока):
|
||
// temp0/input — температура чипа в миллиградусах C (longlong);
|
||
// voltage0/rssi — RSSI входного тракта в дБ (double; строка вида '12.34 dB',
|
||
// libiio парсит ведущее число). Валиден только во время приёма.
|
||
var
|
||
Ch: Piio_channel;
|
||
raw: clonglong;
|
||
d: cdouble;
|
||
begin
|
||
TempC := TELEMETRY_NONE;
|
||
RSSIdB := TELEMETRY_NONE;
|
||
Result := False;
|
||
if (not FConnected) or (FPhy = nil) or (not IIOLoaded) then Exit;
|
||
|
||
Ch := iio_device_find_channel(FPhy, 'temp0', 0);
|
||
if (Ch <> nil) and (iio_channel_attr_read_longlong(Ch, 'input', raw) = 0) then
|
||
begin
|
||
TempC := raw / 1000.0;
|
||
Result := True;
|
||
end;
|
||
|
||
// RSSI имеет смысл только при активном RX-стриме (иначе чтение даёт ошибку/мусор).
|
||
if FStreaming then
|
||
begin
|
||
Ch := iio_device_find_channel(FPhy, 'voltage0', 0);
|
||
if (Ch <> nil) and Assigned(iio_channel_attr_read_double)
|
||
and (iio_channel_attr_read_double(Ch, 'rssi', d) = 0) then
|
||
begin
|
||
RSSIdB := d;
|
||
Result := True;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
end.
|