Files
ewsdr/PlutoBackend.pas
T
ew8bakandClaude Opus 4.8 a95292d2c0 feature(pluto): компенсация hardwaregain в уровнях + живое усиление в блоке RF AGC
На Pluto шкала dBm ничего не значила: усиление тракта 0..73 дБ входит в
показания напрямую, а в режимах AGC железо крутит его само, и картинка
«дышит» вместе с ним.

- RadioBackend: виртуальный RxGainNowDb (TELEMETRY_NONE = регулируемого
  усиления нет, как у openHPSDR с его аттенюатором).
- PlutoBackend: в MANUAL отдаёт FGainDb (точно, без опроса железа), в режимах
  AGC — hardwaregain из телеметрического потока (читается там же, где rssi,
  ~2 Гц, не блокируя UI).
- RadioController: AttenCalDB -> FrontEndCalDB, общее понятие «регулируемое
  усиление входа»: openHPSDR +FAtten (аттенюатор режет), AD936x -hardwaregain
  (усиление поднимает). Постоянная часть тракта (LNB, кабель, конвертер)
  остаётся за таблицами калибровки на диапазон/трансвертер.
- UI: в блоке RF AGC при режиме != MANUAL вместо надписи «AGC» — фактическое
  усиление («~38dB», тильда = измеренное), ползунок ездит сам. Видно, что
  цифра гуляет, а уровни на спектре стоят.

Ограничения: FAST AGC крутит усиление быстрее опроса (2 Гц), для стабильной
шкалы нужен MANUAL или SLOW; шумовая полка при смене усиления немного едет —
это физика (input-referred NF), компенсацию проверять по сигналу.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 11:50:28 +03:00

1309 lines
56 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;
// Второй антенный вход (RX2). Два разных механизма — какой доступен, зависит
// от прошивки/FPGA-образа платы:
// • 2R2T-образ: второй канал инстанцирован, в cf-ad9361-lpc есть voltage2/3 →
// выбор = пара стрим-каналов (FRx2Avail). Задел на второй пан (этап 3.6).
// • 1R1T-образ (напр. LibreSDR/tezuka): второй канал в FPGA не разведён, но
// внутри AD9361 есть мультиплексор — какой физический разъём кормит
// единственный тракт. Выбор = debug-атрибуты 'adi,1rx-1tx-mode-use-rx-num'/
// '-tx-num' (1|2) + 'initialize'=1 (драйвер заново прогоняет ad9361_setup
// из pdata). ⚠Сырые записи битов RX1/RX2 в регистры 0x2/0x3 (как в
// switch_rfinput.sh прошивки) НЕ годятся: тракт переключается, а учёт
// драйвера (gain_control_mode/agc_mode на канал) остаётся от старого —
// hardwaregain потом отвергается («Rx gain can be set in MGC mode only»),
// а в железе продолжает крутиться AGC. rf_port_select тут ни при чём:
// RX2-разъём разведён на второй КАНАЛ, а не на порт B первого.
FRx2Avail: Boolean; // 2R2T: в cf-ad9361-lpc есть voltage2/voltage3
FRxMuxAvail: Boolean; // 1R1T: доступен RX/TX-мультиплексор AD9361
FRxChanSel: Byte; // 0 = RX1, 1 = RX2
FTxChanSel: Byte; // 0 = TX1, 1 = TX2
// 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;
// Телеметрия. Каждый iio_channel_attr_read_* — USB round-trip (~5 мс), а их
// 5-6 за опрос: ~30 мс. Раньше это делал UI-поток (MeterTimerTick, 10 Гц) и
// замирал на весь опрос. Теперь опрашивает свой поток, а ReadTelemetry лишь
// отдаёт последний снимок и никогда не блокирует.
FTeleThread: TThread;
FTeleLock: TRTLCriticalSection;
FTeleTemp: Double; // TELEMETRY_NONE, пока не прочитано
FTeleSoc: Double;
FTeleRssi: Double;
FTeleGain: Double; // hardwaregain RX, дБ (в AGC-режимах крутит железо)
FTeleValid: Boolean; // был ли хоть один успешный опрос
procedure StartTelemetry;
procedure StopTelemetry;
// Блокирующий опрос железа: только из FTeleThread.
function PollTelemetryBlocking(out TempC, SocTempC, RSSIdB,
GainDb: Double): Boolean;
// Низкоуровневые помощники применения атрибутов к ad9361-phy
procedure ApplyRxLO(Hz: Double);
procedure ApplyTxLO(Hz: Double);
procedure SetTxLOPowerdown(PoweredOn: Boolean); // TX LO вкл/выкл (по PTT)
procedure ApplySampleRate(Hz: Integer);
procedure BuildTxResampler; // (пере)строить полифазные коэффициенты под FTxRatio
// Force=True — перед записью режима пишем ДРУГОЙ режим: драйвер пропускает
// перепрог AGC-регистров, если считает режим неизменным, а после смены
// антенного мультиплексора настройки AGC нового канала ещё не запрограммированы
// (в железе крутится auto-AGC, hardwaregain отвергается с EOPNOTSUPP).
procedure ApplyGain(Force: Boolean = False);
procedure ApplyTxAtten;
// Имя phy-канала управления выбранным RX-трактом (гейн/RSSI/rf_bandwidth):
// 'voltage0' = RX1, 'voltage1' = RX2 (только 2R2T; в 1R1T канал всегда один).
function RxPhyChanName: string;
// 1R1T: переключить мультиплексор AD9361 на выбранные разъёмы (см. FRxMuxAvail).
// Дёшево: debug-атрибут (маппинг каналов у драйвера) + биты регистра 0x3/0x2.
procedure ApplyRxMux;
procedure ApplyTxMux;
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 SetAntPorts(RxPort, TxPort: Byte); 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;
function RxGainNowDb: Double; 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 практически выключен
// Детект доступности антенного мультиплексора (тело ниже — нужно до Connect).
function DebugAttrExists(Dev: Piio_device; const Attr: string): Boolean; forward;
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;
// Телеметрия: опрашивает железо в своём темпе и публикует снимок. Держит UI
// подальше от блокирующих USB-транзакций. Доступ к контексту libiio здесь не
// более конкурентен, чем раньше: UI-поток дёргал те же атрибуты параллельно с
// refill в RX-потоке — сменился только поток, который ждёт.
TPlutoTeleThread = 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;
FTeleThread := nil; FTeleValid := False;
FTeleTemp := TELEMETRY_NONE; FTeleSoc := TELEMETRY_NONE; FTeleRssi := TELEMETRY_NONE;
FTeleGain := TELEMETRY_NONE;
InitCriticalSection(FTeleLock);
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(FTeleLock);
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;
// Стримим ровно один тракт ⇒ только главный пан. Одновременный приём с двух
// антенн (второй пан на RX2, общий RX LO) — этап 3.6, нужен 2R2T-образ.
Result.MaxPans := 1;
Result.IndependentPanFreq := False;
// Пан один ⇒ ADC-селектор панов не при чём: у AD936x «антенна» — это выбор
// физических разъёмов (RX1/RX2, TX1/TX2) на диапазон, вкладка Antenna настроек.
Result.NumADCs := 1;
Result.HasAntSwitch := FRx2Avail or FRxMuxAvail;
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;
PV: PAnsiChar;
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;
// 2R2T-образ — по НАЛИЧИЮ каналов второго тракта (клоны не отличить по строкам
// описания). Управление RX2 (гейн/RSSI) — phy input voltage1, требуем и его.
FRx2Avail := (FRxDev <> nil) and
(iio_device_find_channel(FRxDev, 'voltage2', 0) <> nil) and
(iio_device_find_channel(FRxDev, 'voltage3', 0) <> nil) and
(iio_device_find_channel(FPhy, 'voltage1', 0) <> nil);
// Модель берём из ЖИВОГО контекста: в записи устройства она могла приехать из
// сохранёнки/фолбэка ProbeURI ('AD936x SDR') и не содержать имени платы.
PV := iio_context_get_attr_value(FCtx, 'hw_model');
if PV <> nil then FDevice.Model := string(PV);
// 1R1T-образ: RX/TX-мультиплексор AD9361. Определяем по реально
// доступным debug-атрибутам, а не по hw_model: у обычного Pluto второй
// канал могли вывести аппаратно, не меняя строку модели.
FRxMuxAvail := (not FRx2Avail) and
Assigned(iio_device_debug_attr_write) and
DebugAttrExists(FPhy, 'adi,1rx-1tx-mode-use-rx-num') and
DebugAttrExists(FPhy, 'adi,1rx-1tx-mode-use-tx-num') and
DebugAttrExists(FPhy, 'initialize');
if not (FRx2Avail or FRxMuxAvail) then FRxChanSel := 0;
FConnected := True;
// Приводим железо в известное состояние: валидный rate + вход + усиление.
if FSampleRate < PLUTO_MIN_SR then FSampleRate := PLUTO_DEF_SR;
ApplySampleRate(FSampleRate);
ApplyRxMux; // разъёмы могли остаться от прошлой сессии (состояние в чипе)
ApplyTxMux;
ApplyGain(True);
ApplyTxAtten; // TX заглушён (макс. аттенюация) до явного PTT
StartTelemetry;
Result := True;
end;
procedure TPlutoBackend.Disconnect;
begin
StopTelemetry; // до iio_context_destroy: поток ещё читает атрибуты
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 её не трогает) — на phy-канале выбранного
// приёмного тракта (у 2R2T они независимы).
Ch := iio_device_find_channel(FPhy, PAnsiChar(AnsiString(RxPhyChanName)), 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;
function DebugAttrExists(Dev: Piio_device; const Attr: string): Boolean;
var Buf: array[0..63] of AnsiChar;
begin
Result := Assigned(iio_device_debug_attr_read) and (Dev <> nil) and
(iio_device_debug_attr_read(Dev, PAnsiChar(AnsiString(Attr)),
@Buf[0], SizeOf(Buf)) >= 0);
end;
function TPlutoBackend.RxPhyChanName: string;
// Управляющий phy-канал приёмного тракта: RX1 = input voltage0, RX2 = voltage1.
// В 1R1T-образе канал ровно один (voltage0) — мультиплексор меняет вход ПОД ним,
// поэтому гейн/RSSI остаются на voltage0.
begin
if (FRxChanSel = 1) and FRx2Avail then Result := 'voltage1'
else Result := 'voltage0';
end;
procedure TPlutoBackend.ApplyRxMux;
// Приёмный разъём: debug-параметр (по нему драйвер маппит gain-атрибуты на канал)
// + биты RX1/RX2 (маска 0xC0) регистра 0x003 — как switch_rfinput.sh прошивки.
// ⚠Вызывающий обязан следом сделать ApplyGain(True): AGC-регистры нового канала
// ещё не запрограммированы (см. ApplyGain).
var
Val: cuint32;
RxNum: Integer;
begin
if not (FConnected and FRxMuxAvail and (FPhy <> nil)) then Exit;
if FRxChanSel = 1 then RxNum := 2 else RxNum := 1;
iio_device_debug_attr_write(FPhy, 'adi,1rx-1tx-mode-use-rx-num',
PAnsiChar(AnsiString(IntToStr(RxNum))));
Val := 0;
if iio_device_reg_read(FPhy, $3, Val) <> 0 then Exit;
Val := Val and $3F;
if FRxChanSel = 1 then Val := Val or $80 // RX2
else Val := Val or $40; // RX1
iio_device_reg_write(FPhy, $3, Val);
end;
procedure TPlutoBackend.ApplyTxMux;
// То же для передачи (switch_rfoutput.sh): '-tx-num' + биты TX1/TX2 регистра 0x002.
var
Val: cuint32;
TxNum: Integer;
begin
if not (FConnected and FRxMuxAvail and (FPhy <> nil)) then Exit;
if FTxChanSel = 1 then TxNum := 2 else TxNum := 1;
iio_device_debug_attr_write(FPhy, 'adi,1rx-1tx-mode-use-tx-num',
PAnsiChar(AnsiString(IntToStr(TxNum))));
Val := 0;
if iio_device_reg_read(FPhy, $2, Val) <> 0 then Exit;
Val := Val and $3F;
if FTxChanSel = 1 then Val := Val or $80 // TX2
else Val := Val or $40; // TX1
iio_device_reg_write(FPhy, $2, Val);
end;
procedure TPlutoBackend.SetAntPorts(RxPort, TxPort: Byte);
// Выбор физических разъёмов: 0 = RX1/TX1, 1 = RX2/TX2. Зовётся контроллером при
// смене диапазона/трансвертера (антенна — свойство диапазона, см. вкладку Antenna).
var
RxChanged, TxChanged: Boolean;
begin
if RxPort > 1 then RxPort := 0;
if TxPort > 1 then TxPort := 0;
if not (FRx2Avail or FRxMuxAvail) then Exit; // одна пара разъёмов — нечего выбирать
RxChanged := RxPort <> FRxChanSel;
TxChanged := TxPort <> FTxChanSel;
if not (RxChanged or TxChanged) then Exit;
FRxChanSel := RxPort;
FTxChanSel := TxPort;
if not FConnected then Exit;
if FRx2Avail then
begin
// 2R2T-образ: разъём = ДРУГАЯ пара стрим-каналов ⇒ пересоздать буферы, затем
// вернуть гейн/аттенюацию (у 2R2T они ещё и живут на phy-канале своего тракта).
if RxChanged and FStreaming then begin StopRX; StartRX; end;
if TxChanged and FTxStreaming then begin StopTX; StartTX; end;
ApplyGain;
ApplyTxAtten;
end
else
begin
// 1R1T: мультиплексор внутри чипа — стримы/LO/rate не трогаем (дёшево).
// Обе записи ДО восстановления усиления, и гейн — форсированный: у нового
// канала AGC-регистры ещё не запрограммированы.
if RxChanged then ApplyRxMux;
if TxChanged then ApplyTxMux;
ApplyGain(True);
ApplyTxAtten;
end;
end;
procedure TPlutoBackend.ApplyGain(Force: Boolean);
var Ch: Piio_channel; Other: string;
begin
if FPhy = nil then Exit;
// Усиление/режим AGC — поканальные: пишем в тракт выбранной антенны.
Ch := iio_device_find_channel(FPhy, PAnsiChar(AnsiString(RxPhyChanName)), 0);
if Ch = nil then Exit;
if Force then
begin
// Прогоняем через другой режим — иначе драйвер сочтёт режим неизменным и не
// перепрограммирует AGC-регистры канала (после смены мультиплексора там
// остаётся auto-AGC, а hardwaregain отвергается: «Rx gain can be set in MGC only»).
if FGainMode = 'manual' then Other := 'slow_attack' else Other := 'manual';
iio_channel_attr_write(Ch, 'gain_control_mode', PAnsiChar(AnsiString(Other)));
end;
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;
var Base: Integer; ChI, ChQ: string;
begin
if FStreaming or not FConnected or (FRxDev = nil) then Exit;
// Пара стрим-каналов выбранного тракта: RX1 = voltage0/1, RX2 = voltage2/3.
// Включаем ТОЛЬКО её — второй тракт не стримим (иначе вдвое трафик/CPU).
Base := 0;
if (FRxChanSel = 1) and FRx2Avail then Base := 2;
ChI := 'voltage' + IntToStr(Base);
ChQ := 'voltage' + IntToStr(Base + 1);
FRxChI := iio_device_find_channel(FRxDev, PAnsiChar(AnsiString(ChI)), 0);
FRxChQ := iio_device_find_channel(FRxDev, PAnsiChar(AnsiString(ChQ)), 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;
// Снять enable с пары каналов: иначе после смены антенного входа (2R2T) в
// новом буфере окажутся включёнными оба тракта — чередование I1,Q1,I2,Q2.
if FRxChI <> nil then iio_channel_disable(FRxChI);
if FRxChQ <> nil then iio_channel_disable(FRxChQ);
FRxChI := nil;
FRxChQ := nil;
end;
procedure TPlutoBackend.StartTX;
var Base: Integer; ChI, ChQ: string;
begin
if FTxStreaming or not FConnected or (FTxDev = nil) then Exit;
// 2R2T-образ: пара выходных каналов выбранного разъёма (TX1 = voltage0/1,
// TX2 = voltage2/3). В 1R1T пара всегда одна — разъём переключает ApplyTxMux.
Base := 0;
if (FTxChanSel = 1) and FRx2Avail then Base := 2;
ChI := 'voltage' + IntToStr(Base);
ChQ := 'voltage' + IntToStr(Base + 1);
FTxChI := iio_device_find_channel(FTxDev, PAnsiChar(AnsiString(ChI)), 1); // output I
FTxChQ := iio_device_find_channel(FTxDev, PAnsiChar(AnsiString(ChQ)), 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
// ADCSource (АЦП-источник HPSDR) у AD936x смысла не имеет — антенные разъёмы
// переключает SetAntPorts. SampleRate приходит в ksps (как у HPSDR), Pluto — в Гц.
// Контроллер может прислать HPSDR-наследие (<520k) — клампим к дефолту.
FSampleRate := Integer(SampleRate) * 1000;
if FSampleRate < PLUTO_MIN_SR then FSampleRate := PLUTO_DEF_SR;
if not FConnected then Exit;
ApplySampleRate(FSampleRate); // перезапустит RX при смене
// ad9361_set_bb_rate перепрошивает FIR/BBPLL и сбрасывает усиление/AGC к
// дефолтам — возвращаем разъёмы и форсированно гейн + TX-аттенюацию (иначе
// в железе остаётся auto-AGC: высокая шумовая полка).
ApplyRxMux;
ApplyTxMux;
ApplyGain(True);
ApplyTxAtten;
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.PollTelemetryBlocking(out TempC, SocTempC, RSSIdB,
GainDb: Double): Boolean;
// БЛОКИРУЮЩИЙ опрос железа — только из FTeleThread (каждое чтение = USB round-trip):
// 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;
GainDb := 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-стриме (иначе чтение даёт ошибку/мусор).
// Читаем с phy-канала выбранного приёмного тракта (RX1/RX2 — независимы).
if FStreaming then
begin
Ch := iio_device_find_channel(FPhy, PAnsiChar(AnsiString(RxPhyChanName)), 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;
// hardwaregain: в режимах AGC железо крутит усиление само, и без него шкала
// dBm «дышит» вместе с ним (см. TRadioController.FrontEndCalDB).
if (Ch <> nil) and Assigned(iio_channel_attr_read_double)
and (iio_channel_attr_read_double(Ch, 'hardwaregain', d) = 0) then
begin
GainDb := d;
Result := True;
end;
end;
end;
function TPlutoBackend.RxGainNowDb: Double;
// В ручном режиме значение известно точно и без опроса железа; в AGC-режимах
// отдаём последний снимок телеметрии (обновляется ~2 Гц — быстрый AGC всё
// равно шевелится быстрее, для стабильной шкалы нужен MANUAL или SLOW).
begin
if FGainMode = 'manual' then Exit(FGainDb);
EnterCriticalSection(FTeleLock);
try
if FTeleValid and (FTeleGain > TELEMETRY_NONE) then Result := FTeleGain
else Result := FGainDb;
finally
LeaveCriticalSection(FTeleLock);
end;
end;
function TPlutoBackend.ReadTelemetry(out TempC, SocTempC, RSSIdB: Double): Boolean;
// Неблокирующий: отдаёт последний снимок, снятый FTeleThread. Безопасно звать из UI.
begin
EnterCriticalSection(FTeleLock);
try
TempC := FTeleTemp;
SocTempC := FTeleSoc;
RSSIdB := FTeleRssi;
Result := FTeleValid;
finally
LeaveCriticalSection(FTeleLock);
end;
end;
procedure TPlutoBackend.StartTelemetry;
begin
if FTeleThread = nil then
FTeleThread := TPlutoTeleThread.Create(Self);
end;
procedure TPlutoBackend.StopTelemetry;
begin
if FTeleThread = nil then Exit;
FTeleThread.Terminate;
FTeleThread.WaitFor; // обязателен до разрушения контекста libiio
FreeAndNil(FTeleThread);
EnterCriticalSection(FTeleLock);
try
FTeleValid := False;
FTeleTemp := TELEMETRY_NONE;
FTeleSoc := TELEMETRY_NONE;
FTeleRssi := TELEMETRY_NONE;
finally
LeaveCriticalSection(FTeleLock);
end;
end;
{ TPlutoTeleThread }
constructor TPlutoTeleThread.Create(ABk: TPlutoBackend);
begin
FBk := ABk;
FreeOnTerminate := False;
inherited Create(False);
end;
procedure TPlutoTeleThread.Execute;
const
POLL_MS = 500; // ~2 Гц: чаще незачем, каждый опрос ~30 мс USB-транзакций
STEP_MS = 25; // мелкий шаг сна — чтобы Terminate отрабатывал быстро
var
t, s, r, g: Double;
ok: Boolean;
Waited: Integer;
begin
while not Terminated do
begin
if FBk.FConnected then
begin
ok := FBk.PollTelemetryBlocking(t, s, r, g);
if ok then
begin
EnterCriticalSection(FBk.FTeleLock);
try
FBk.FTeleTemp := t;
FBk.FTeleSoc := s;
FBk.FTeleRssi := r;
FBk.FTeleGain := g;
FBk.FTeleValid := True;
finally
LeaveCriticalSection(FBk.FTeleLock);
end;
end;
end;
Waited := 0;
while (Waited < POLL_MS) and not Terminated do
begin
Sleep(STEP_MS);
Inc(Waited, STEP_MS);
end;
end;
end;
end.