mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
feat: Pluto TX polyphase FIR interpolator (anti-imaging)
Replaced linear interpolation upsampling (192k→device-rate) with a polyphase FIR interpolator ×L. Prototype: windowed-sinc lowpass (Blackman, ~-58 dB sidelobes), TAPS*L long, cutoff TX_FIR_CUT=40k, normalized to unity per-phase DC gain (loudness unchanged). Images at multiples of 192k are now suppressed ~-58 dB instead of only sinc², cleaning up the uplink spectrum. BuildTxResampler designs coefficients per FTxRatio in StartTX; FTxDlI/Q delay line persists across SendDUCIQ calls, reset on StartTX/ClearDUCIQQueue. Removed FTxPrevI/Q. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+77
-21
@@ -67,10 +67,13 @@ type
|
||||
FTxChQ: Piio_channel;
|
||||
FTxThread: TThread;
|
||||
FTxStreaming: Boolean;
|
||||
FTxRatio: Integer; // FSampleRate div 192000 (целочисленный апсемпл)
|
||||
FTxRatio: Integer; // FSampleRate div 192000 (целочисленный апсемпл ×L)
|
||||
FTxBufPairs: Integer; // размер TX-буфера в IQ-парах (по sample-rate, см. TxBufPairsForRate)
|
||||
FTxPrevI: LongInt; // последний 24-бит сэмпл (линейная интерполяция между вызовами)
|
||||
FTxPrevQ: LongInt;
|
||||
// Полифазный FIR-интерполятор: FTxCoef[p*TAPS + k] = h[p + k*L] (фаза p, отвод k).
|
||||
// Линия задержки FTxDlI/Q хранит последние TAPS входных сэмплов между вызовами.
|
||||
FTxCoef: array of Double;
|
||||
FTxDlI: array of Double;
|
||||
FTxDlQ: array of Double;
|
||||
// FIFO int16-пар @ device-rate (кольцо): продюсер — SendDUCIQ, консьюмер — push-поток.
|
||||
FTxFifo: array of SmallInt; // интерливнутые I,Q
|
||||
FTxFifoCap: Integer; // ёмкость в SmallInt-элементах (пар*2)
|
||||
@@ -83,6 +86,7 @@ type
|
||||
procedure ApplyRxLO(Hz: Double);
|
||||
procedure ApplyTxLO(Hz: Double);
|
||||
procedure ApplySampleRate(Hz: Integer);
|
||||
procedure BuildTxResampler; // (пере)строить полифазные коэффициенты под FTxRatio
|
||||
procedure ApplyGain;
|
||||
procedure ApplyTxAtten;
|
||||
procedure StartRX;
|
||||
@@ -146,6 +150,10 @@ const
|
||||
// выше 1 Msps — сэмплы дропаются» (комментарий драйвера). Минимум — TX_BUF_MIN.
|
||||
TX_BUF_MIN = 4096; // нижняя граница (~7 мс @ 576k)
|
||||
TX_BUF_MAX = 65536; // верхняя граница (защита от гигантских буферов)
|
||||
// Полифазный анти-имидж интерполятор 192k→device-rate (вместо линейной интерполяции,
|
||||
// которая оставляла образы на кратных 192k с подавлением только sinc²).
|
||||
TX_FIR_TAPS = 16; // отводов на фазу (групп-задержка TAPS/2 входных сэмплов)
|
||||
TX_FIR_CUT = 40000; // частота среза анти-имидж ФНЧ, Гц (покрывает SSB/NFM/AM/data)
|
||||
PLUTO_TX_OFF = -89.75; // макс. аттенюация AD9361 — TX практически выключен
|
||||
|
||||
type
|
||||
@@ -341,7 +349,7 @@ begin
|
||||
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;
|
||||
FTxStreaming := False; FTxRatio := 8;
|
||||
FTxAttDb := PLUTO_TX_OFF; // на старте TX выключен (макс. аттенюация)
|
||||
// FIFO на ~4 TX-буфера запаса под джиттер продюсера/консьюмера. Окончательный
|
||||
// размер выставляется в StartTX по фактическому device-rate (TxBufPairsForRate).
|
||||
@@ -597,6 +605,44 @@ begin
|
||||
if WasTxStreaming then StartTX; // пересоздаём TX с новым FTxRatio
|
||||
end;
|
||||
|
||||
procedure TPlutoBackend.BuildTxResampler;
|
||||
// Полифазный FIR-интерполятор ×L (L=FTxRatio): прототип — оконный sinc-ФНЧ длиной
|
||||
// TAPS*L с частотой среза TX_FIR_CUT, нормированный так, что DC-усиление любой фазы
|
||||
// ≈ 1 (полная сумма = L). Раскладка в фазы: coef[p*TAPS+k] = proto[p + k*L].
|
||||
var
|
||||
L, M, n, p, k: Integer;
|
||||
fc, x, s, w, sum, scale: Double;
|
||||
proto: array of Double;
|
||||
begin
|
||||
L := FTxRatio;
|
||||
if L < 1 then L := 1;
|
||||
M := TX_FIR_TAPS * L;
|
||||
SetLength(proto, M);
|
||||
fc := TX_FIR_CUT / FSampleRate; // нормированный срез (циклов/сэмпл на выходе)
|
||||
if fc > 0.49 then fc := 0.49;
|
||||
sum := 0;
|
||||
for n := 0 to M - 1 do
|
||||
begin
|
||||
x := n - (M - 1) / 2.0;
|
||||
if Abs(x) < 1e-9 then s := 2.0 * fc
|
||||
else s := Sin(2.0 * Pi * fc * x) / (Pi * x);
|
||||
// окно Блэкмана (~ −58 дБ боковые)
|
||||
w := 0.42 - 0.5 * Cos(2.0 * Pi * n / (M - 1)) + 0.08 * Cos(4.0 * Pi * n / (M - 1));
|
||||
proto[n] := s * w;
|
||||
sum := sum + proto[n];
|
||||
end;
|
||||
if sum = 0 then sum := 1;
|
||||
scale := L / sum; // DC-усиление на фазу ≈ 1
|
||||
SetLength(FTxCoef, M);
|
||||
for p := 0 to L - 1 do
|
||||
for k := 0 to TX_FIR_TAPS - 1 do
|
||||
FTxCoef[p * TX_FIR_TAPS + k] := proto[p + k * L] * scale;
|
||||
// линия задержки на TAPS входных сэмплов (сброс)
|
||||
SetLength(FTxDlI, TX_FIR_TAPS);
|
||||
SetLength(FTxDlQ, TX_FIR_TAPS);
|
||||
for k := 0 to TX_FIR_TAPS - 1 do begin FTxDlI[k] := 0; FTxDlQ[k] := 0; end;
|
||||
end;
|
||||
|
||||
procedure TPlutoBackend.ApplyGain;
|
||||
var Ch: Piio_channel;
|
||||
begin
|
||||
@@ -666,7 +712,7 @@ begin
|
||||
// Апсемпл-ратио под текущий device-rate (рейты-пресеты кратны 192k → целое).
|
||||
FTxRatio := FSampleRate div TX_SRC_RATE;
|
||||
if FTxRatio < 1 then FTxRatio := 1;
|
||||
FTxPrevI := 0; FTxPrevQ := 0;
|
||||
BuildTxResampler; // полифазные коэффициенты + сброс линии задержки под FTxRatio
|
||||
// Размер push-буфера под текущий rate + FIFO на ~4 таких буфера запаса.
|
||||
FTxBufPairs := TxBufPairsForRate(FSampleRate);
|
||||
EnterCriticalSection(FTxLock);
|
||||
@@ -776,9 +822,10 @@ 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 (продюсер опередил) — отбрасываем старое.
|
||||
// Полифазный FIR-интерполятор ×FTxRatio (анти-имидж ФНЧ, см. BuildTxResampler):
|
||||
// на каждый входной сэмпл выдаём L выходных, конвертим 24→int16 (>>8, full-scale
|
||||
// MSB-aligned), Q инвертируем (та же конвенция, что и на RX) и кладём в FIFO.
|
||||
// Underrun в push-потоке добивается нулями, overflow — отбрасываем старое.
|
||||
function Clamp16(V: Int64): SmallInt; inline;
|
||||
begin
|
||||
if V > 32767 then Result := 32767
|
||||
@@ -786,23 +833,32 @@ procedure TPlutoBackend.SendDUCIQ(const IData, QData: array of Integer);
|
||||
else Result := SmallInt(V);
|
||||
end;
|
||||
var
|
||||
i, j, t: Integer;
|
||||
ci, cq, oi, oq: Int64;
|
||||
n: Integer;
|
||||
i, p, k, t, L, taps, base, n: Integer;
|
||||
ai, aq: Double;
|
||||
begin
|
||||
if (not FTxStreaming) or (FTxRatio < 1) then Exit;
|
||||
if (not FTxStreaming) or (FTxRatio < 1) or (Length(FTxCoef) = 0) then Exit;
|
||||
n := Length(IData);
|
||||
if Length(QData) < n then n := Length(QData);
|
||||
L := FTxRatio;
|
||||
taps := TX_FIR_TAPS;
|
||||
EnterCriticalSection(FTxLock);
|
||||
try
|
||||
for i := 0 to n - 1 do
|
||||
begin
|
||||
ci := IData[i]; cq := QData[i];
|
||||
for j := 1 to FTxRatio do
|
||||
// сдвигаем линию задержки (FTxDl[0] = новейший входной сэмпл)
|
||||
for k := taps - 1 downto 1 do
|
||||
begin FTxDlI[k] := FTxDlI[k - 1]; FTxDlQ[k] := FTxDlQ[k - 1]; end;
|
||||
FTxDlI[0] := IData[i]; FTxDlQ[0] := QData[i];
|
||||
// L выходных сэмплов — фазы p = 0..L-1, каждая = свёртка с подфильтром
|
||||
for p := 0 to L - 1 do
|
||||
begin
|
||||
// линейная интерполяция prev→cur по FTxRatio точкам (последняя = cur)
|
||||
oi := FTxPrevI + (ci - FTxPrevI) * j div FTxRatio;
|
||||
oq := FTxPrevQ + (cq - FTxPrevQ) * j div FTxRatio;
|
||||
ai := 0; aq := 0;
|
||||
base := p * taps;
|
||||
for k := 0 to taps - 1 do
|
||||
begin
|
||||
ai := ai + FTxCoef[base + k] * FTxDlI[k];
|
||||
aq := aq + FTxCoef[base + k] * FTxDlQ[k];
|
||||
end;
|
||||
if FTxFifoCount + 2 > FTxFifoCap then
|
||||
begin
|
||||
// overflow: сбрасываем самые старые пары, держим хвост (минимум латентности)
|
||||
@@ -810,12 +866,11 @@ begin
|
||||
Dec(FTxFifoCount, 2);
|
||||
end;
|
||||
t := FTxFifoTail;
|
||||
FTxFifo[t] := Clamp16(oi div 256); // 24→16 бит
|
||||
FTxFifo[t + 1] := Clamp16(-(oq div 256)); // Q инверсия (как RX)
|
||||
FTxFifo[t] := Clamp16(Round(ai / 256.0)); // 24→16 бит
|
||||
FTxFifo[t + 1] := Clamp16(-Round(aq / 256.0)); // Q инверсия (как RX)
|
||||
FTxFifoTail := (t + 2) mod FTxFifoCap;
|
||||
Inc(FTxFifoCount, 2);
|
||||
end;
|
||||
FTxPrevI := ci; FTxPrevQ := cq;
|
||||
end;
|
||||
finally
|
||||
LeaveCriticalSection(FTxLock);
|
||||
@@ -823,10 +878,11 @@ begin
|
||||
end;
|
||||
|
||||
procedure TPlutoBackend.ClearDUCIQQueue;
|
||||
var k: Integer;
|
||||
begin
|
||||
EnterCriticalSection(FTxLock);
|
||||
FTxFifoHead := 0; FTxFifoTail := 0; FTxFifoCount := 0;
|
||||
FTxPrevI := 0; FTxPrevQ := 0;
|
||||
for k := 0 to Length(FTxDlI) - 1 do begin FTxDlI[k] := 0; FTxDlQ[k] := 0; end;
|
||||
LeaveCriticalSection(FTxLock);
|
||||
end;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user