feat: Pluto TX path — attenuation-based power, full-duplex IQ stream

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 12:27:49 +03:00
co-authored by Claude Opus 4.8
parent 1321e415f7
commit d08fc17056
7 changed files with 376 additions and 19 deletions
+9
View File
@@ -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,
+229 -4
View File
@@ -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);
+9
View File
@@ -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.
+46
View File
@@ -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
+4
View File
@@ -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
+46
View File
@@ -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;
+33 -15
View File
@@ -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 (073 дБ) / режим 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 как зависимость** — динамическая загрузка, поведение при отсутствии.