mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
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:
@@ -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
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 как зависимость** — динамическая загрузка, поведение при отсутствии.
|
||||
|
||||
Reference in New Issue
Block a user