mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +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:
+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);
|
||||
|
||||
Reference in New Issue
Block a user