From d08fc170560f71a304fc34aa02f77eac217af394 Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Fri, 19 Jun 2026 12:27:49 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20Pluto=20TX=20path=20=E2=80=94=20attenua?= =?UTF-8?q?tion-based=20power,=20full-duplex=20IQ=20stream?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TX path for Pluto/AD936x (RX was already working): - PlutoBackend: continuous full-duplex TX stream (cf-ad9361-dds-core-lpc) with FIFO + push thread; SendDUCIQ upsamples WDSP 192k -> device rate (integer, linear interp), 24->16 bit, Q inverted; zeros on underrun. - Power via output attenuator (ADI-recommended): SetTxAtten writes hardwaregain on voltage0(out). Controller maps drive% -> dB (PlutoTxAttForDrive/ApplyPlutoTxAtten); -89.75 dB on RX (~off), ceiling PlutoTxMaxAttDb on 100% drive. - Configurable max-att (default -10 dB, ADI-linear) for external amp: persisted in global settings, UI spin on PA page, routed through controller SetPlutoTxMaxAtt (UI stays decoupled from logic). - RadioBackend: virtual SetTxAtten (no-op for HPSDR). - Update doc/PLUTO_INTEGRATION_PLAN.md: phases 3/4/5 + risks. Builds clean (headless + qt6 GUI). Not yet verified on hardware. Co-Authored-By: Claude Opus 4.8 --- MainForm.pas | 9 ++ PlutoBackend.pas | 233 +++++++++++++++++++++++++++++++++- RadioBackend.pas | 9 ++ RadioController.pas | 46 +++++++ Settings.pas | 4 + SettingsForm.pas | 46 +++++++ doc/PLUTO_INTEGRATION_PLAN.md | 48 ++++--- 7 files changed, 376 insertions(+), 19 deletions(-) diff --git a/MainForm.pas b/MainForm.pas index 4a76fde..5ce65e0 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -498,6 +498,7 @@ type function DefaultMicSource: TTXMicSource; procedure ApplyMOX(Active: Boolean); procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double); + procedure OnPlutoTxMaxAttChange(MaxAttDb: Double); procedure OnVHFCalSettingsChange(const VHFCal: array of Double); procedure OnTXSettingsChange(const T: TTXSettings); procedure BtnAGCModeClick(Sender: TObject); @@ -5053,6 +5054,12 @@ begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); end; +procedure TMainForm.OnPlutoTxMaxAttChange(MaxAttDb: Double); +begin + // Логика (clamp/apply/persist) — в контроллере; UI лишь передаёт значение. + FController.SetPlutoTxMaxAtt(MaxAttDb); +end; + procedure TMainForm.OnVHFCalSettingsChange(const VHFCal: array of Double); var i: Integer; begin @@ -5708,6 +5715,7 @@ begin SF.OnVisibilityChange := ApplyVisibility; SF.OnFPSChange := ApplyFPS; SF.OnPAChange := OnPASettingsChange; + SF.OnPlutoTxChange := OnPlutoTxMaxAttChange; SF.OnVHFCalChange := OnVHFCalSettingsChange; SF.OnCATChange := OnCATSettingsChange; SF.OnThemeChange := SetLightTheme; @@ -5726,6 +5734,7 @@ begin IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType)); SF.LoadXvtrSettings(FController.FXvtrSettings); SF.LoadPASettings(FController.FPAMaxPower, FController.FPABandCal); + SF.LoadPlutoTxMaxAtt(FController.FPlutoTxMaxAttDb); SF.LoadVHFCalSettings(FController.FVHFBandCal); SF.LoadCATSettings( FCATLastGlobal.CATSerialEnabled, diff --git a/PlutoBackend.pas b/PlutoBackend.pas index 020397f..191d4c3 100644 --- a/PlutoBackend.pas +++ b/PlutoBackend.pas @@ -49,6 +49,8 @@ type 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; @@ -57,13 +59,35 @@ type 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 (целочисленный апсемпл) + FTxPrevI: LongInt; // последний 24-бит сэмпл (линейная интерполяция между вызовами) + FTxPrevQ: LongInt; + // FIFO int16-пар @ device-rate (кольцо): продюсер — SendDUCIQ, консьюмер — push-поток. + FTxFifo: array of SmallInt; // интерливнутые I,Q + FTxFifoCap: Integer; // ёмкость в SmallInt-элементах (пар*2) + FTxFifoHead: Integer; // индекс чтения (консьюмер) + FTxFifoTail: Integer; // индекс записи (продюсер) + FTxFifoCount: Integer; // занято элементов + FTxLock: TRTLCriticalSection; + // Низкоуровневые помощники применения атрибутов к ad9361-phy procedure ApplyRxLO(Hz: Double); procedure ApplyTxLO(Hz: Double); procedure ApplySampleRate(Hz: Integer); procedure ApplyGain; + procedure ApplyTxAtten; procedure StartRX; procedure StopRX; + procedure StartTX; + procedure StopTX; protected function GetConnected: Boolean; override; function GetRunning: Boolean; override; @@ -95,6 +119,9 @@ type procedure SendFullHP; override; procedure SendPTT(Active: Boolean); override; procedure SetRxGain(ModeIdx: Integer; GainDb: Integer); override; + procedure SetTxAtten(Db: Double); override; + procedure SendDUCIQ(const IData, QData: array of Integer); override; + procedure ClearDUCIQQueue; override; function ReadTelemetry(out TempC, RSSIdB: Double): Boolean; override; end; @@ -112,6 +139,9 @@ const 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_BUF_PAIRS = 4096; // IQ-пар в одном TX-буфере push (~2.7 мс @ 1.536 MS/s) + PLUTO_TX_OFF = -89.75; // макс. аттенюация AD9361 — TX практически выключен type // RX-поток: блокирующий refill libiio → конверсия int16→24bit BE → OnDDCIQ. @@ -125,6 +155,18 @@ type 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; @@ -194,6 +236,55 @@ begin 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 PlutoSyntheticMAC(const S: string): TBytes; var H: QWord; @@ -229,12 +320,21 @@ begin 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; FTxPrevI := 0; FTxPrevQ := 0; + FTxAttDb := PLUTO_TX_OFF; // на старте TX выключен (макс. аттенюация) + // FIFO на ~4 TX-буфера запаса под джиттер продюсера/консьюмера. + FTxFifoCap := TX_BUF_PAIRS * 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; @@ -406,11 +506,13 @@ begin 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 @@ -443,11 +545,13 @@ begin end; procedure TPlutoBackend.ApplySampleRate(Hz: Integer); -var Ch: Piio_channel; WasStreaming: Boolean; +var Ch: Piio_channel; WasStreaming, WasTxStreaming: Boolean; begin if (FPhy = nil) or (Hz < PLUTO_MIN_SR) then Exit; - WasStreaming := FStreaming; - if WasStreaming then StopRX; // менять rate с открытым буфером нельзя + WasStreaming := FStreaming; + WasTxStreaming := FTxStreaming; + if WasTxStreaming then StopTX; // device-rate общий для RX/TX — пересоздать оба + if WasStreaming then StopRX; // менять rate с открытым буфером нельзя // Предпочтительно ad9361_set_bb_rate: настраивает BBPLL И загружает FIR-таблицу // децимации под частоту. Без этого на холодную (битый/дефолтный FIR) спектр — @@ -468,6 +572,7 @@ begin iio_channel_attr_write_longlong(Ch, 'rf_bandwidth', Hz); if WasStreaming then StartRX; + if WasTxStreaming then StartTX; // пересоздаём TX с новым FTxRatio end; procedure TPlutoBackend.ApplyGain; @@ -481,6 +586,17 @@ begin 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; @@ -516,6 +632,47 @@ begin 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; + FTxPrevI := 0; FTxPrevQ := 0; + EnterCriticalSection(FTxLock); + FTxFifoHead := 0; FTxFifoTail := 0; FTxFifoCount := 0; + LeaveCriticalSection(FTxLock); + FTxBuf := iio_device_create_buffer(FTxDev, TX_BUF_PAIRS, 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; @@ -538,7 +695,11 @@ begin begin ApplyRxLO(FRXFreq); ApplyTxLO(FTXFreq); - if Run then StartRX else StopRX; // RX-стрим; TX-стрим — фаза 4 + // Full-duplex: оба стрима живут весь Run. TX по умолчанию заглушён макс. + // аттенюацией (FTxAttDb=OFF) — реальная мощность включается из контроллера + // через SetTxAtten на PTT. + if Run then begin StartRX; StartTX; end + else begin StopTX; StopRX; end; end; end; @@ -579,6 +740,70 @@ begin 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 через контроллер. +// Линейно апсемплим ×FTxRatio, конвертим 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, j, t: Integer; + ci, cq, oi, oq: Int64; + n: Integer; +begin + if (not FTxStreaming) or (FTxRatio < 1) then Exit; + n := Length(IData); + if Length(QData) < n then n := Length(QData); + EnterCriticalSection(FTxLock); + try + for i := 0 to n - 1 do + begin + ci := IData[i]; cq := QData[i]; + for j := 1 to FTxRatio do + begin + // линейная интерполяция prev→cur по FTxRatio точкам (последняя = cur) + oi := FTxPrevI + (ci - FTxPrevI) * j div FTxRatio; + oq := FTxPrevQ + (cq - FTxPrevQ) * j div FTxRatio; + if FTxFifoCount + 2 > FTxFifoCap then + begin + // overflow: сбрасываем самые старые пары, держим хвост (минимум латентности) + FTxFifoHead := (FTxFifoHead + 2) mod FTxFifoCap; + Dec(FTxFifoCount, 2); + end; + t := FTxFifoTail; + FTxFifo[t] := Clamp16(oi div 256); // 24→16 бит + FTxFifo[t + 1] := Clamp16(-(oq div 256)); // Q инверсия (как RX) + FTxFifoTail := (t + 2) mod FTxFifoCap; + Inc(FTxFifoCount, 2); + end; + FTxPrevI := ci; FTxPrevQ := cq; + end; + finally + LeaveCriticalSection(FTxLock); + end; +end; + +procedure TPlutoBackend.ClearDUCIQQueue; +begin + EnterCriticalSection(FTxLock); + FTxFifoHead := 0; FTxFifoTail := 0; FTxFifoCount := 0; + FTxPrevI := 0; FTxPrevQ := 0; + LeaveCriticalSection(FTxLock); +end; + function TPlutoBackend.ReadTelemetry(out TempC, RSSIdB: Double): Boolean; // Опрос аппаратной телеметрии ad9361-phy (вызывается контроллером, не из RX-потока): // temp0/input — температура чипа в миллиградусах C (longlong); diff --git a/RadioBackend.pas b/RadioBackend.pas index 2c98203..c1ca5a7 100644 --- a/RadioBackend.pas +++ b/RadioBackend.pas @@ -144,6 +144,11 @@ type // RX-усиление (Pluto/AD936x). ModeIdx: 0=manual, 1=fast_attack, 2=slow_attack, // 3=hybrid. GainDb применяется только в manual. HPSDR — no-op. procedure SetRxGain(ModeIdx: Integer; GainDb: Integer); virtual; + // TX-аттенюация выходного тракта (Pluto/AD936x): hardwaregain на voltage0 + // (output), дБ в диапазоне ~ -89.75..0 (0 = макс. мощность). Контроллер ставит + // максимум на приёме (≈выкл) и значение от drive на передаче. HPSDR — no-op + // (мощность там через DriveLevel/DUC). Db < 0. + procedure SetTxAtten(Db: Double); virtual; procedure SetAlexConfig(const A: TAlexSettings); virtual; procedure SetXvtrMode(AEnable, ADisablePA: Boolean; ARxAnt: Byte); virtual; procedure SendDDCAudio(const LeftRight: array of SmallInt); virtual; @@ -250,4 +255,8 @@ begin Result := False; end; +procedure TRadioBackend.SetTxAtten(Db: Double); +begin +end; + end. diff --git a/RadioController.pas b/RadioController.pas index 4a97ddd..3a0d00c 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -205,6 +205,7 @@ type FLastVfoScrolled: Boolean; // последний SetVfoA прокрутил центр (для водопада) FAtten: Integer; FPAMaxPower: Double; + FPlutoTxMaxAttDb: Double; // Pluto: TX-аттенюация на drive=100% (потолок, дБ<0) FRunning: Boolean; FTransmitting: Boolean; FTuning: Boolean; @@ -453,6 +454,12 @@ type procedure DriveBy(Delta: Integer); procedure SetAtten(Idx: Integer); + // Pluto TX-мощность через аттенюацию: drive% → дБ (потолок FPlutoTxMaxAttDb на + // 100%, выкл на приёме). Логика/маппинг здесь; бэкенд лишь пишет hardwaregain. + function PlutoTxAttForDrive: Double; // дБ<0 под текущий FDrivePercent + procedure ApplyPlutoTxAtten; // ставит att по FTransmitting/FTuning + procedure SetPlutoTxMaxAtt(Db: Double); // потолок на drive=100% (внешний усилитель) + // Диапазоны / трансвертеры / спан procedure SetBand(Idx: Integer); procedure BandUp; @@ -560,6 +567,7 @@ begin FCurrentBand := 5; FCurrentXvtr := -1; FSpectrumBufCount := 1024; FWaterfallBufCount := 1024; FPAMaxPower := 100.0; + FPlutoTxMaxAttDb := -10.0; // ADI-дефолт: линейный режим выходных каскадов FWfAGCEnabled := True; FDitherEnabled := True; FRandomEnabled := True; FFMStepOn := True; FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1; @@ -1485,6 +1493,7 @@ begin Result.Volume := FVolume; Result.DriveLevel := FDrivePercent; Result.PAMaxPower := FPAMaxPower; + Result.PlutoTxMaxAttDb := FPlutoTxMaxAttDb; for i := 0 to CFG_BAND_COUNT - 1 do Result.PABandCal[i] := FPABandCal[i]; for i := 0 to CFG_XVTR_COUNT - 1 do Result.VHFBandCal[i] := FVHFBandCal[i]; Result.ActiveVfo := FActiveVfo; @@ -2076,6 +2085,7 @@ begin FDrivePercent := V; FDriveLevel := CalcDriveByte; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetDriveLevel(FDrivePercent / 100.0); + ApplyPlutoTxAtten; // Pluto: drive живьём меняет TX-аттенюацию во время передачи PushNetworkState; // протолкнуть новый drive в радио (если подключено+Running) Changed(rfDrive); end; @@ -2083,6 +2093,39 @@ end; procedure TRadioController.DriveBy(Delta: Integer); begin SetDrive(FDrivePercent + Delta); end; +function TRadioController.PlutoTxAttForDrive: Double; +const + TX_OFF = -89.75; // макс. аттенюация AD9361 (0% → практически выкл) +var + Pct: Integer; +begin + // На тюне (TUN) уровень берём из TX-настроек (TUNLevel), иначе из drive%. + if FTuning then Pct := FTXSettings.TUNLevel else Pct := FDrivePercent; + if Pct < 0 then Pct := 0 else if Pct > 100 then Pct := 100; + // dB-линейно: 0% → TX_OFF, 100% → FPlutoTxMaxAttDb (потолок под внешний тракт). + Result := TX_OFF + (Pct / 100.0) * (FPlutoTxMaxAttDb - TX_OFF); + if Result > FPlutoTxMaxAttDb then Result := FPlutoTxMaxAttDb; + if Result < TX_OFF then Result := TX_OFF; +end; + +procedure TRadioController.ApplyPlutoTxAtten; +const + TX_OFF = -89.75; +begin + if (FNetwork = nil) or not IsPluto then Exit; + // На передаче (MOX/TUN) — мощность от drive; на приёме — макс. аттенюация (выкл). + if FTransmitting or FTuning then FNetwork.SetTxAtten(PlutoTxAttForDrive) + else FNetwork.SetTxAtten(TX_OFF); +end; + +procedure TRadioController.SetPlutoTxMaxAtt(Db: Double); +begin + if Db > 0 then Db := 0 else if Db < -50 then Db := -50; + FPlutoTxMaxAttDb := Db; + ApplyPlutoTxAtten; // применить живьём, если сейчас передаём + SaveGlobalSettings; // персист потолка (per-device) +end; + procedure TRadioController.SetAtten(Idx: Integer); begin if Idx < 0 then Idx := 0 else if Idx > 2 then Idx := 2; @@ -2510,6 +2553,7 @@ begin FNetwork.ClearDUCIQQueue; FNetwork.SendFullHP; // 2й пакет: Alex bit27=0 (T/R relay RX) end; + ApplyPlutoTxAtten; // Pluto: TX-мощность по drive на передаче, OFF на приёме Changed(rfTransmitting); end; @@ -2549,6 +2593,7 @@ begin FNetwork.SendFullHP; end; end; + ApplyPlutoTxAtten; // финальное состояние att по FTuning/FTransmitting (Pluto) Changed(rfTuning); end; @@ -2780,6 +2825,7 @@ begin FVolume := FLoadedGlobal.Volume; FActiveVfo := EnsureRange(FLoadedGlobal.ActiveVfo, 0, 1); // битый конфиг не должен дать невалидный VFO FPAMaxPower := FLoadedGlobal.PAMaxPower; + FPlutoTxMaxAttDb := EnsureRange(FLoadedGlobal.PlutoTxMaxAttDb, -50.0, 0.0); for i := 0 to CFG_BAND_COUNT - 1 do FPABandCal[i] := FLoadedGlobal.PABandCal[i]; for i := 0 to CFG_XVTR_COUNT - 1 do diff --git a/Settings.pas b/Settings.pas index c732b25..60665eb 100644 --- a/Settings.pas +++ b/Settings.pas @@ -261,6 +261,7 @@ type FreqMhzDigits: Integer; // minimum MHz digit count: 3=999MHz, 4=9.999GHz, 5=99.999GHz // --- PA (Power Amplifier) settings --- PAMaxPower: Double; // максимальная выходная мощность, Вт (5..200) + PlutoTxMaxAttDb: Double; // Pluto: TX-аттенюация на drive=100%, дБ<0 (дефолт -10) PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0 VHFBandCal: array[0..CFG_XVTR_COUNT-1] of Double; // калибровка для XVTR слотов 38.8..100.0 // --- ADC settings --- @@ -410,6 +411,7 @@ begin G.SampleRate := 192000; G.RxGainMode := 2; G.RxGainDb := 40; G.PAMaxPower := 100.0; + G.PlutoTxMaxAttDb := -10.0; for i := 0 to CFG_BAND_COUNT-1 do G.PABandCal[i] := CFG_PA_BAND_CAL_DEFAULT[i]; for i := 0 to CFG_XVTR_COUNT-1 do G.VHFBandCal[i] := CFG_VHF_CAL_DEFAULT; // Display defaults @@ -665,6 +667,7 @@ begin G.FreqMhzDigits := EnsureRange(JI(GObj,'freq_mhz_digits',3), 3, 5); // PA settings G.PAMaxPower := JD(GObj,'pa_max_power',100.0); + G.PlutoTxMaxAttDb := JD(GObj,'pluto_tx_max_att',-10.0); for i := 0 to CFG_BAND_COUNT-1 do G.PABandCal[i] := JD(GObj,'pa_band_cal_'+IntToStr(i), CFG_PA_BAND_CAL_DEFAULT[i]); for i := 0 to CFG_XVTR_COUNT-1 do @@ -757,6 +760,7 @@ begin JW(O,'freq_mhz_digits',G.FreqMhzDigits); // PA settings JW(O,'pa_max_power',G.PAMaxPower); + JW(O,'pluto_tx_max_att',G.PlutoTxMaxAttDb); for i := 0 to CFG_BAND_COUNT-1 do JW(O,'pa_band_cal_'+IntToStr(i),G.PABandCal[i]); for i := 0 to CFG_XVTR_COUNT-1 do diff --git a/SettingsForm.pas b/SettingsForm.pas index 80aea30..516ef05 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -35,6 +35,8 @@ type // PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон TOnPASettingsChange = procedure(MaxPower: Double; const BandCal: array of Double) of object; + // Pluto TX: аттенюация выходного тракта на drive=100% (потолок), дБ<0 + TOnPlutoTxChange = procedure(MaxAttDb: Double) of object; // VHF (XVTR) PA calibration: 8 slots, 38.8..100.0 TOnVHFCalChange = procedure(const VHFCal: array of Double) of object; // TX-настройки целиком (любая правка → один callback с актуальной структурой) @@ -132,6 +134,7 @@ type // ---- PA Settings tab controls ---- FEdMaxPower: TFlatSpinEdit; + FEdPlutoTxMaxAtt: TFlatFloatSpinEdit; // Pluto TX att на drive=100% FEdBandCal: array[0..10] of TFlatFloatSpinEdit; FEdVHFCal: array[0..CFG_XVTR_COUNT-1] of TFlatFloatSpinEdit; FVHFCalLabels: array[0..CFG_XVTR_COUNT-1] of TLabel; @@ -265,6 +268,7 @@ type // ---- Callbacks ---- FOnCATChange: TOnCATChange; FOnPAChange: TOnPASettingsChange; + FOnPlutoTxChange: TOnPlutoTxChange; FOnVHFCalChange: TOnVHFCalChange; FOnADCChange: TOnADCChange; FOnWebSettingsChange: TOnWebSettingsChange; @@ -344,6 +348,7 @@ type procedure OnLightThemeChkChange(Sender: TObject); procedure OnFreqMhzDigitsCmbChange(Sender: TObject); procedure OnMaxPowerChange(Sender: TObject); + procedure OnPlutoTxMaxAttChange(Sender: TObject); procedure OnBandCalChange(Sender: TObject); procedure OnVHFCalEdit(Sender: TObject); procedure OnCATSerialChange(Sender: TObject); @@ -386,6 +391,7 @@ type procedure LoadLightTheme(ALight: Boolean); procedure LoadFreqMhzDigits(Digits: Integer); procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double); + procedure LoadPlutoTxMaxAtt(MaxAttDb: Double); procedure LoadVHFCalSettings(const VHFCal: array of Double); procedure LoadCATSettings( const SerEnabled: array of Boolean; @@ -404,6 +410,7 @@ type property OnAlexChange: TOnAlexChange read FOnAlexChange write FOnAlexChange; property OnXvtrChange: TOnXvtrChange read FOnXvtrChange write FOnXvtrChange; property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange; + property OnPlutoTxChange: TOnPlutoTxChange read FOnPlutoTxChange write FOnPlutoTxChange; property OnVHFCalChange: TOnVHFCalChange read FOnVHFCalChange write FOnVHFCalChange; property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange; property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange; @@ -1662,6 +1669,29 @@ begin Spin.OnChange := OnMaxPowerChange; FEdMaxPower := Spin; + // Pluto TX max attenuation @ drive 100% (потолок под внешний усилитель), дБ<0 + Lbl := TLabel.Create(Self); + Lbl.Parent := Grp; + Lbl.Caption := 'Pluto TX att @100% (dB)'; + Lbl.SetBounds(GRP_PAD + 320, R1 + 6, 180, 18); + Lbl.Font.Color := CLR_TEXTDIM; + Lbl.Font.Name := UI_FONT; + Lbl.Font.Size := 9; + + FEdPlutoTxMaxAtt := TFlatFloatSpinEdit.Create(Self); + FEdPlutoTxMaxAtt.Parent := Grp; + FEdPlutoTxMaxAtt.SetBounds(GRP_PAD + 510, R1, ED_W, BTN_H + 2); + FEdPlutoTxMaxAtt.Color := CLR_INPUT; + FEdPlutoTxMaxAtt.Font.Color := CLR_INPUT_TEXT; + FEdPlutoTxMaxAtt.Font.Name := UI_FONT; + FEdPlutoTxMaxAtt.Font.Size := 9; + FEdPlutoTxMaxAtt.DecimalPlaces := 2; + FEdPlutoTxMaxAtt.MinValue := -50; + FEdPlutoTxMaxAtt.MaxValue := 0; + FEdPlutoTxMaxAtt.Increment := 0.25; + FEdPlutoTxMaxAtt.Value := -10; + FEdPlutoTxMaxAtt.OnChange := OnPlutoTxMaxAttChange; + // Calibration label Lbl := TLabel.Create(Self); Lbl.Parent := Grp; @@ -2486,6 +2516,16 @@ begin end; end; +procedure TSettingsForm.LoadPlutoTxMaxAtt(MaxAttDb: Double); +begin + FLoading := True; + try + FEdPlutoTxMaxAtt.Value := EnsureRange(MaxAttDb, -50.0, 0.0); + finally + FLoading := False; + end; +end; + procedure TSettingsForm.OnMaxPowerChange(Sender: TObject); var BandCal: array[0..10] of Double; @@ -2498,6 +2538,12 @@ begin FOnPAChange(FEdMaxPower.Value, BandCal); end; +procedure TSettingsForm.OnPlutoTxMaxAttChange(Sender: TObject); +begin + if FLoading then Exit; + if Assigned(FOnPlutoTxChange) then FOnPlutoTxChange(FEdPlutoTxMaxAtt.Value); +end; + procedure TSettingsForm.OnBandCalChange(Sender: TObject); var BandCal: array[0..10] of Double; diff --git a/doc/PLUTO_INTEGRATION_PLAN.md b/doc/PLUTO_INTEGRATION_PLAN.md index a4454a1..c976418 100644 --- a/doc/PLUTO_INTEGRATION_PLAN.md +++ b/doc/PLUTO_INTEGRATION_PLAN.md @@ -239,16 +239,23 @@ end; - **RX:** `TPlutoBackend` читает int16 из `iio_buffer`, конвертирует 16→24-бит, упаковывает в `TDDCIQPacket` (seq синтезируется), вызывает `OnDDCIQ`. -- **TX:** `SendDUCIQ` конвертирует 24→16-бит, пишет в tx-буфер libiio с пейсингом - (модель FIFO-cushion, см. memory `project_txiq_pacing`). ⚠️ TX-rate Pluto - ≥ 521 ksps, WDSP TXA отдаёт 192k — нужен ресэмпл 192k→PlutoTX или смена TX-rate. +- **TX:** ✅ `SendDUCIQ` апсемплит 192k→device-rate (целочисленно, линейная + интерполяция; пресеты рейта кратны 192k), конвертирует 24→16-бит (`>>8`, + full-scale MSB-aligned), инвертирует Q (симметрично RX) и кладёт в кольцевой + FIFO. Отдельный `TPlutoTXThread` льёт пары в tx-буфер libiio и `iio_buffer_push` + (блокирующий push = пейсинг на device-rate); при underrun — нули (непрерывный + full-duplex). Решение rate-mismatch (риск §6.2) — апсемпл в host (RX/TX делят + один `sampling_frequency`). TODO: полифазный FIR вместо линейной интерполяции + для лучшего подавления образов на аплинке. ### 4.7 Усиление / телеметрия - RX gain: контрол manual gain (0–73 дБ) / режим hw-AGC при `Caps.HasHWGain` (можно расширить UI step-atten). WDSP AGC поверх. - Телеметрия: S-meter (WDSP) — для обоих. fwd/SWR/supply/PLL/overload — скрыть при - `not Caps.HasPA`. Опц.: RSSI/температура Pluto. + `not Caps.HasPA`. ✅ RSSI/температура Pluto: `ReadTelemetry` (backend) → + `ServicePlutoTelemetry` (контроллер, опрос ~2 Гц с фильтром выбросов) → поле 3 + статус-бара (Temp/RSSI вместо Supply, т.к. у Pluto нет PA). --- @@ -270,16 +277,26 @@ end; - Гейтинг UI по caps. ✅ приём на Pluto (спектр/водопад/аудио на 2м/70см/23см/13см). **Фаза 3 — Управление/калибровка** -- RX gain / hw-AGC, XO ppm, транспорт (USB/IP), buffer/timeout, температура/RSSI. -- Persist per-device по synthetic-MAC из serial. +- ✅ RX gain / hw-AGC, транспорт (USB/IP), persist per-device по synthetic-MAC. +- ✅ Имя платы (Pluto/LibreSDR + serial) и телеметрия (температура/RSSI) в статус-бар. +- TODO: XO ppm-калибровка, buffer/timeout-контролы. -**Фаза 4 — TX-тракт Pluto** -- `SendDUCIQ` 24→16 + пейсинг; ресэмпл/смена TX-rate (§4.6); TX atten вместо - drive/PA; гейтинг PA/SWR/Alex/keyer. ✅ передача (SSB/CW/FM). +**Фаза 4 — TX-тракт Pluto** ✅ +- ✅ `SendDUCIQ` 24→16 + апсемпл 192k→device-rate + пейсинг (FIFO + push-поток, §4.6). +- ✅ Управление мощностью через TX-аттенюацию (ADI-модель): непрерывный full-duplex + стрим, drive% → `hardwaregain` (потолок `PlutoTxMaxAttDb`, дефолт −10 дБ, + настраиваемый под внешний усилитель); на приёме −89.75 дБ (≈выкл). Маппинг/ + политика — в контроллере (`PlutoTxAttForDrive`/`ApplyPlutoTxAtten`), бэкенд — + тонкий `SetTxAtten`. Без PA → нет drive-байта/SWR/Alex/keyer. +- TODO: проверка на железе (ориентация SSB по Q-инверсии, уровни); полифазный + ресэмпл (см. §4.6); опц. зануление IQ на приёме. **Фаза 5 — Режим QO-100** -- `TQO100Settings` + раздельные RX/TX LO (§3.2), дисплей в downlink-терминах, - калибровка LNB по маяку, **full-duplex** (довести `SetDuplex`). +- ✅ Калибровка LNB по маяку (beacon lock) — стабилизация дрейфа. +- ✅ Hardware full-duplex: TX-стрим живёт всё время Run (Фаза 4), RX+TX + одновременно. Остаётся display-duplex (`SetDuplex` — водопад RX во время TX). +- TODO: `TQO100Settings` + раздельные RX/TX LO (§3.2), дисплей в downlink-терминах, + довести `SetDuplex`. - UI: вход/выход в режим QO-100 (как XVTR-слот), поля LNB LO / ppm / TX atten. - ✅ полноценная работа через спутник. @@ -293,10 +310,11 @@ end; 1. **2 м / 70 см ниже 325 МГц** → требуется freq-range мод прошивки Pluto. Документировать; возможно гейтить эти кнопки при стоковой прошивке. -2. **TX sample-rate mismatch** (192k WDSP vs ≥521k Pluto) — ресэмпл или смена - TX-rate WDSP. -3. **QO-100 full-duplex** — довести `SetDuplex` (сейчас TODO) до реального - одновременного RX+TX; проверить латентность/буферы libiio. +2. ✅ **TX sample-rate mismatch** (192k WDSP vs ≥521k Pluto) — решено host-апсемплом + 192k→device-rate в `SendDUCIQ` (целочисленно, пресеты кратны 192k). Качество + образов — линейная интерполяция; апгрейд до полифазного FIR в бэклоге. +3. **QO-100 display-duplex** — hardware full-duplex есть (Фаза 4), остаётся довести + `SetDuplex` (водопад RX во время TX); проверить латентность/буферы libiio. 4. **LNB дрейф** — калибровка `LnbPpm` по маяку (ручная/полуавтомат). 5. **Ключ настроек** — synthetic-MAC из serial (быстро) vs обобщение до строки. 6. **libiio как зависимость** — динамическая загрузка, поведение при отсутствии.