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; // Второй антенный вход (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; FTeleValid: Boolean; // был ли хоть один успешный опрос procedure StartTelemetry; procedure StopTelemetry; // Блокирующий опрос железа: только из FTeleThread. function PollTelemetryBlocking(out TempC, SocTempC, RSSIdB: 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; 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 BoardHasSecondRxPort(const Model: string): Boolean; forward; 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; 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-мультиплексор AD9361. Гейт по МОДЕЛИ — у стокового Pluto // второй вход никуда не разведён (кремний двухканальный, а SMA один), так что // селектор там показывать нельзя: он дал бы «мёртвую» антенну. FRxMuxAvail := (not FRx2Avail) and BoardHasSecondRxPort(FDevice.Model) 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 BoardHasSecondRxPort(const Model: string): Boolean; // Есть ли у платы физический разъём RX2. Кремний AD9361/AD9363 двухканальный на // ЛЮБОЙ плате, включая стоковый Pluto, — но у Pluto второй канал никуда не выведен, // поэтому переключать на него нельзя (антенны там нет). Список плат с двумя RX SMA. const BOARDS: array[0..4] of string = ('libresdr', 'antsdr', 'pluto+', 'plutoplus', 'e200'); var M: string; i: Integer; begin Result := False; M := LowerCase(StringReplace(Model, ' ', '', [rfReplaceAll])); for i := Low(BOARDS) to High(BOARDS) do if Pos(BOARDS[i], M) > 0 then Exit(True); 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: 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; 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; 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: Double; ok: Boolean; Waited: Integer; begin while not Terminated do begin if FBk.FConnected then begin ok := FBk.PollTelemetryBlocking(t, s, r); if ok then begin EnterCriticalSection(FBk.FTeleLock); try FBk.FTeleTemp := t; FBk.FTeleSoc := s; FBk.FTeleRssi := r; 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.