Files
ewsdr/PlutoBackend.pas
T
ew8bakandClaude Opus 4.8 6375092741 feat: pluto telemetry — show both temperatures (transceiver + Zynq SoC)
Pluto exposes two temperature sensors: the AD9361 transceiver
(ad9361-phy temp0/input, milli-°C) and the Zynq SoC/FPGA (xadc temp0,
raw+offset+scale → C = (raw+offset)*scale/1000). Only the former was
read. Now both are read and shown as "Temp 37/53C" (chip/SoC) in the
desktop status bar and web UI.

- ReadTelemetry gains an out SocTempC param (base + Pluto override);
  Pluto reads xadc via the IIO context.
- Controller stores FLastPlutoSocTempC; SoC temp validated with a
  plausibility window (no slew-limit — stable local sensor, unlike the
  AD9361 sensor that spikes during mid-conversion).
- Status formatting handles one or both temps in MainForm and WebAdapter.
- Widen desktop status-bar field 3 (150→190) so both temps + RSSI fit;
  web layout unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 19:10:07 +03:00

967 lines
38 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit PlutoBackend;
{
Бэкенд ADALM-PLUTO (libiio / AD936x), реализует TRadioBackend.
Фаза 1 (текущая): discovery через iio_scan_context + Caps + открытие/закрытие
контекста (Connect/Disconnect) + геттеры состояния. RX/TX-потоки и реальная
подача IQ — фазы 2/4 (методы-заглушки сохраняют состояние).
Карта устройств/каналов Pluto (для фаз 24):
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 SetTxLOPowerdown(PoweredOn: Boolean); // TX LO вкл/выкл (по PTT)
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, SocTempC, 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_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;
// Пресеты для 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
begin
iio_channel_attr_write(Ch, 'powerdown', '0'); // вернуть LO, если внешнее ПО его погасило
iio_channel_attr_write_longlong(Ch, 'frequency', Round(Hz));
end;
end;
procedure TPlutoBackend.SetTxLOPowerdown(PoweredOn: Boolean);
// TX LO (altvoltage1) вкл/выкл. Гасим на приёме → нет утечки несущей TX на частоте
// VFO в RX-спектре; включаем на PTT. Также возвращает LO, если стороннее ПО его
// погасило. RX LO (altvoltage0) не трогаем — приём непрерывен.
var Ch: Piio_channel;
begin
if FPhy = nil then Exit;
Ch := iio_device_find_channel(FPhy, 'altvoltage1', 1);
if Ch = nil then Exit;
if PoweredOn then iio_channel_attr_write(Ch, 'powerdown', '0')
else iio_channel_attr_write(Ch, 'powerdown', '1');
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));
// powerdown TX LO по состоянию передачи (на приёме гасим — убирает спайк-утечку).
SetTxLOPowerdown(FTransmitting);
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;
// TX LO включаем на передаче, гасим на приёме (нет утечки несущей в RX-спектр).
// TX-стрим живёт непрерывно (full-duplex); реальная мощность — через аттенюацию.
if FConnected then SetTxLOPowerdown(Active);
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, SocTempC, RSSIdB: Double): Boolean;
// Опрос аппаратной телеметрии (вызывается контроллером, не из RX-потока):
// ad9361-phy temp0/input — температура трансивера в миллиградусах C (longlong);
// xadc temp0 — температура Zynq SoC/FPGA; сырой raw + offset (longlong)
// + scale (double), C = (raw + offset) * scale / 1000;
// ad9361-phy voltage0/rssi — RSSI входного тракта в дБ (double; строка '12.34 dB',
// libiio парсит ведущее число). Валиден только во время приёма.
var
Ch, ChX: Piio_channel;
XDev: Piio_device;
raw, offs: clonglong;
scale, d: cdouble;
begin
TempC := TELEMETRY_NONE;
SocTempC := 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;
// Температура Zynq SoC/FPGA — отдельное IIO-устройство 'xadc' (всегда присутствует).
// Канал отдаёт сырой код + offset + scale (в отличие от ad9361, дающего милли-°C):
// T(°C) = (raw + offset) * scale / 1000.
if FCtx <> nil then
begin
XDev := iio_context_find_device(FCtx, 'xadc');
if XDev <> nil then
begin
ChX := iio_device_find_channel(XDev, 'temp0', 0);
if (ChX <> nil) and Assigned(iio_channel_attr_read_double)
and (iio_channel_attr_read_longlong(ChX, 'raw', raw) = 0)
and (iio_channel_attr_read_longlong(ChX, 'offset', offs) = 0)
and (iio_channel_attr_read_double(ChX, 'scale', scale) = 0) then
begin
SocTempC := (raw + offs) * scale / 1000.0;
Result := True;
end;
end;
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.