feat: Pluto RX hw-gain control + AD9361 cold-init (FIR via libad9361)

- RX hw-gain (Pluto/AD936x): backend SetRxGain(ModeIdx, GainDb) — gain_control_mode
  (manual/fast_attack/slow_attack/hybrid) + hardwaregain. Controller FRxGainMode/
  FRxGainDb, commands SetRxGainMode/SetRxGain/RxGainBy, rfRxGain event, snapshot,
  applied in StartRunning. Persisted per-device (rx_gain_mode/rx_gain_db). HPSDR
  unaffected (backend no-op).
- UI: separate "RF AGC" panel below the (unchanged) WDSP AGC block, shown only for
  Pluto — FAST/SLOW/HYB/MAN mode buttons + manual GAIN slider. LayoutLeftPanel
  reflows lower panels via RelayoutBelowBands; no layout shift on HPSDR.
- Cold-init fix: direct sampling_frequency write doesn't load the AD9361 FIR
  decimation filter, so a cold-booted Pluto showed a wide centre spike that didn't
  track tuning (worked only after SDR Console pre-initialised it). Add optional
  libad9361 binding (dynamic) and call ad9361_set_bb_rate() in ApplySampleRate,
  which configures BBPLL + loads the FIR; falls back to direct write if absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 12:06:59 +03:00
co-authored by Claude Opus 4.8
parent a988849d3e
commit 24f74357cd
6 changed files with 228 additions and 3 deletions
+31
View File
@@ -84,6 +84,13 @@ var
// ---- Утилиты ----
iio_strerror: procedure(err: cint; dst: PAnsiChar; len: csize_t); cdecl;
// ---- libad9361 (опционально) ----
// ad9361_set_bb_rate: настраивает BBPLL + ЗАГРУЖАЕТ FIR-таблицу под частоту.
// Без него прямая запись sampling_frequency на холодную даёт мусор (битый FIR).
// nil если libad9361 не загружена.
ad9361_set_bb_rate: function(dev: Piio_device; rate: culong): cint; cdecl;
ad9361_set_trx_fir_enable: function(dev: Piio_device; enable: cint): cint; cdecl;
// Загружает libiio (один раз). True — готово к использованию. Идемпотентно.
function IIOLoad: Boolean;
// True, если libiio загружена и символы разрешены.
@@ -95,16 +102,20 @@ implementation
var
GLib: TLibHandle = NilHandle;
GLibAD: TLibHandle = NilHandle; // libad9361 (опционально)
GLoaded: Boolean = False;
const
{$IFDEF WINDOWS}
IIO_LIBS: array[0..1] of string = ('libiio.dll', 'iio.dll');
AD_LIBS: array[0..1] of string = ('libad9361.dll', 'ad9361.dll');
{$ELSE}
{$IFDEF DARWIN}
IIO_LIBS: array[0..1] of string = ('libiio.dylib', 'libiio.1.dylib');
AD_LIBS: array[0..1] of string = ('libad9361.dylib', 'libad9361.0.dylib');
{$ELSE}
IIO_LIBS: array[0..1] of string = ('libiio.so', 'libiio.so.0');
AD_LIBS: array[0..1] of string = ('libad9361.so', 'libad9361.so.0');
{$ENDIF}
{$ENDIF}
@@ -163,6 +174,21 @@ begin
iio_buffer_step := Sym('iio_buffer_step');
iio_buffer_end := Sym('iio_buffer_end');
iio_strerror := Sym('iio_strerror');
// libad9361 — опционально (ad9361_set_bb_rate грузит FIR). Отсутствие не
// фатально: ApplySampleRate откатится на прямую запись sampling_frequency.
if GLibAD = NilHandle then
for I := 0 to High(AD_LIBS) do
begin
GLibAD := LoadLibrary(AD_LIBS[I]);
if GLibAD <> NilHandle then Break;
end;
if GLibAD <> NilHandle then
begin
ad9361_set_bb_rate := GetProcedureAddress(GLibAD, 'ad9361_set_bb_rate');
ad9361_set_trx_fir_enable := GetProcedureAddress(GLibAD, 'ad9361_set_trx_fir_enable');
end;
GLoaded := True;
except
on E: Exception do
@@ -190,6 +216,11 @@ begin
end;
finalization
if GLibAD <> NilHandle then
begin
UnloadLibrary(GLibAD);
GLibAD := NilHandle;
end;
if GLib <> NilHandle then
begin
UnloadLibrary(GLib);
+107
View File
@@ -259,6 +259,11 @@ type
BtnAGCMode: array[0..4] of TFlatButton; // FAST MED SLOW LONG OFF
LblAGCTop: TLabel; // показывает значение уровня
TrkAGC: TFlatSlider;
// RX hw-gain (Pluto) — ОТДЕЛЬНЫЙ блок под AGC (WDSP). Виден только для Pluto.
PanelPlutoAGC: TPanel;
BtnPlutoGain: array[0..3] of TFlatButton; // FAST/SLOW/HYB/MAN (Tag=режим)
TrkRxGain: TFlatSlider; // manual gain 0..73 dB
LblRxGain: TLabel; // значение усиления
TrkVolume: TFlatSlider;
BtnNR: TFlatButton;
BtnNB: TFlatButton;
@@ -328,6 +333,9 @@ type
function ResolveDevice(const IP: string): THPSDRDevice;
procedure RelabelBands; // подписи band-кнопок под план бэкенда
procedure ApplyBackendCapsToUI; // sample-rate пресеты + гейтинг полей по Caps
procedure UpdateRxGainUI; // рендер RX hw-gain (Pluto)
procedure LayoutLeftPanel(Pluto: Boolean); // показать/скрыть Pluto-блок + reflow
procedure BtnPlutoGainClick(Sender: TObject);
procedure DoConnectDevice(const Dev: THPSDRDevice);
// Старт с уже выбранным устройством (preload rate + WDSP open + connect).
// Переиспользуется десктоп-диалогом и web-оверлеем — без модального диалога.
@@ -487,6 +495,8 @@ type
procedure OnTXSettingsChange(const T: TTXSettings);
procedure BtnAGCModeClick(Sender: TObject);
procedure TrkAGCChange(Sender: TObject);
procedure TrkRxGainChange(Sender: TObject);
// (BtnPlutoGainClick объявлен выше)
procedure MeterTimerTick(Sender: TObject);
procedure SpectrumTimerTick(Sender: TObject);
procedure RightPanelResize(Sender: TObject);
@@ -1225,6 +1235,8 @@ begin
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FController.FAGCMode);
rfVolume:
TrkVolume.Position := FController.FVolume;
rfRxGain:
UpdateRxGainUI;
rfDrive:
begin
TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive%
@@ -1397,6 +1409,7 @@ const
BTN_H = 26;
BTN_SM = 24;
LEFT_W = 232;
PLUTO_AGC_H = 64; // высота отдельного Pluto RF-AGC блока (под AGC)
TOP_VFO_W = 244;
TOP_VFO_FREQ_H = 47;
var
@@ -1759,6 +1772,47 @@ begin
Inc(Y, 74);
// --- Pluto RF AGC/gain — отдельный блок ПОД блоком AGC (WDSP). Виден только
// когда подключён Pluto-бэкенд; раскладку панелей ниже сдвигает LayoutLeftPanel.
PanelPlutoAGC := TPanel.Create(Self);
PanelPlutoAGC.Parent := PanelLeft;
PanelPlutoAGC.SetBounds(0, Y, LEFT_W, PLUTO_AGC_H);
PanelPlutoAGC.BevelOuter := bvNone;
PanelPlutoAGC.Visible := False;
MakeLbl(PanelPlutoAGC, 'RF AGC', 4, 2);
// Режимы AD936x: FAST/SLOW/HYB/MAN. Tag = индекс режима (1/2/3/0).
BtnPlutoGain[0] := MakeBtn(PanelPlutoAGC, 'FAST',
LeftPanelButtonLeft(LEFT_W, 4, 0), 16, LeftPanelButtonWidth(LEFT_W, 4, 0),
BTN_SM, BtnPlutoGainClick);
BtnPlutoGain[0].Tag := 1;
BtnPlutoGain[1] := MakeBtn(PanelPlutoAGC, 'SLOW',
LeftPanelButtonLeft(LEFT_W, 4, 1), 16, LeftPanelButtonWidth(LEFT_W, 4, 1),
BTN_SM, BtnPlutoGainClick);
BtnPlutoGain[1].Tag := 2;
BtnPlutoGain[2] := MakeBtn(PanelPlutoAGC, 'HYB',
LeftPanelButtonLeft(LEFT_W, 4, 2), 16, LeftPanelButtonWidth(LEFT_W, 4, 2),
BTN_SM, BtnPlutoGainClick);
BtnPlutoGain[2].Tag := 3;
BtnPlutoGain[3] := MakeBtn(PanelPlutoAGC, 'MAN',
LeftPanelButtonLeft(LEFT_W, 4, 3), 16, LeftPanelButtonWidth(LEFT_W, 4, 3),
BTN_SM, BtnPlutoGainClick);
BtnPlutoGain[3].Tag := 0;
// Manual gain slider.
MakeLbl(PanelPlutoAGC, 'GAIN', 4, 44);
TrkRxGain := TFlatSlider.Create(Self);
TrkRxGain.Parent := PanelPlutoAGC; TrkRxGain.Left := 56; TrkRxGain.Top := 40;
TrkRxGain.Width := LEFT_W - 96; TrkRxGain.Height := 16;
TrkRxGain.Min := 0; TrkRxGain.Max := 73; // AD936x hardwaregain dB
TrkRxGain.Position := FController.FRxGainDb;
TrkRxGain.OnChange := TrkRxGainChange;
LblRxGain := TLabel.Create(Self);
LblRxGain.Parent := PanelPlutoAGC;
LblRxGain.Left := LEFT_W - 38; LblRxGain.Top := 44;
LblRxGain.Caption := Format('%ddB', [FController.FRxGainDb]);
LblRxGain.Font.Color := CLR_TEXT; LblRxGain.Font.Name := 'Courier New';
LblRxGain.Font.Size := 7;
// НЕ Inc(Y): панель скрыта и наложена на позицию Bands; LayoutLeftPanel сдвинет.
// Band selector. Высота резервирует 2 дополнительных ряда под XVTR-кнопки
// (создаются динамически в RebuildXvtrButtons). В каждом ряду до 6 кнопок,
// итого до 12 XVTR-кнопок (наш CFG_XVTR_COUNT = 8 умещается с запасом).
@@ -3540,6 +3594,10 @@ begin
FSampleRateOverlay.SetRatePresetsDefault; // HPSDR 48k..1536k
FSampleRateOverlay.SetCurrentRate(FController.FSampleRate);
end;
// Отдельный Pluto RF-AGC блок под AGC: показываем и сдвигаем панели ниже.
LayoutLeftPanel(Caps.HasHWGain);
if Caps.HasHWGain then UpdateRxGainUI;
end;
procedure TMainForm.DoConnectDevice(const Dev: THPSDRDevice);
@@ -5252,6 +5310,55 @@ begin
FController.SetAGCTop(TrkAGC.Position);
end;
procedure TMainForm.TrkRxGainChange(Sender: TObject);
begin
if FSyncingFromController then Exit;
FController.SetRxGain(TrkRxGain.Position); // переводит в manual
end;
procedure TMainForm.BtnPlutoGainClick(Sender: TObject);
begin
if FSyncingFromController then Exit;
FController.SetRxGainMode((Sender as TFlatButton).Tag); // Tag = индекс режима
end;
procedure TMainForm.UpdateRxGainUI;
// Рендер Pluto RF-AGC блока: подсветка режима + слайдер/значение manual gain.
var i: Integer;
begin
if PanelPlutoAGC = nil then Exit;
FSyncingFromController := True;
try
for i := 0 to 3 do
if BtnPlutoGain[i] <> nil then
StyleButton(BtnPlutoGain[i], BtnPlutoGain[i].Tag = FController.FRxGainMode);
TrkRxGain.Position := FController.FRxGainDb;
TrkRxGain.Enabled := (FController.FRxGainMode = 0); // активен только в manual
if FController.FRxGainMode = 0 then
LblRxGain.Caption := Format('%ddB', [FController.FRxGainDb])
else
LblRxGain.Caption := 'AGC';
finally
FSyncingFromController := False;
end;
end;
procedure TMainForm.LayoutLeftPanel(Pluto: Boolean);
// Показывает/скрывает Pluto RF-AGC блок под AGC и сдвигает панели ниже
// (Bands и далее через RelayoutBelowBands).
var baseTop: Integer;
begin
if (PanelPlutoAGC = nil) or (PanelAGC = nil) or (PanelBands = nil) then Exit;
baseTop := PanelAGC.Top + PanelAGC.Height + 4;
PanelPlutoAGC.Top := baseTop;
PanelPlutoAGC.Visible := Pluto;
if Pluto then
PanelBands.Top := baseTop + PanelPlutoAGC.Height + 2
else
PanelBands.Top := baseTop;
RelayoutBelowBands;
end;
// ===========================================================================
// DSP/Audio callbacks — вызываются из рабочих потоков
// ===========================================================================
+28 -2
View File
@@ -94,6 +94,7 @@ type
Transmitting, PAEnabled, AlexEnabled: Boolean); override;
procedure SendFullHP; override;
procedure SendPTT(Active: Boolean); override;
procedure SetRxGain(ModeIdx: Integer; GainDb: Integer); override;
end;
// Стабильный 6-байтовый ключ (synthetic MAC) из serial/URI — для per-device
@@ -446,12 +447,25 @@ begin
if (FPhy = nil) or (Hz < PLUTO_MIN_SR) then Exit;
WasStreaming := FStreaming;
if WasStreaming then StopRX; // менять rate с открытым буфером нельзя
// Предпочтительно ad9361_set_bb_rate: настраивает BBPLL И загружает FIR-таблицу
// децимации под частоту. Без этого на холодную (битый/дефолтный FIR) спектр —
// широкая палка по центру, не реагирует на перестройку. Fallback — прямая
// запись sampling_frequency (работает, если FIR уже корректен).
if Assigned(ad9361_set_bb_rate) then
ad9361_set_bb_rate(FPhy, culong(Hz))
else
begin
Ch := iio_device_find_channel(FPhy, 'voltage0', 0);
if Ch <> nil then
begin
iio_channel_attr_write_longlong(Ch, 'sampling_frequency', Hz);
iio_channel_attr_write_longlong(Ch, 'rf_bandwidth', Hz);
end;
// RF-полоса отдельно (set_bb_rate её не трогает).
Ch := iio_device_find_channel(FPhy, 'voltage0', 0);
if Ch <> nil then
iio_channel_attr_write_longlong(Ch, 'rf_bandwidth', Hz);
if WasStreaming then StartRX;
end;
@@ -552,4 +566,16 @@ begin
// Реальный RX↔TX (вкл/выкл стримов) — фаза 4.
end;
procedure TPlutoBackend.SetRxGain(ModeIdx: Integer; GainDb: Integer);
begin
case ModeIdx of
1: FGainMode := 'fast_attack';
2: FGainMode := 'slow_attack';
3: FGainMode := 'hybrid';
else FGainMode := 'manual';
end;
FGainDb := GainDb;
if FConnected then ApplyGain;
end;
end.
+7
View File
@@ -134,6 +134,9 @@ type
procedure SendDUCSpecific(const Pkt: TDUCSpecificPacket); virtual;
procedure SendHighPriority(const Pkt: THighPriorityPacket); virtual;
procedure SetStepAtten(dB: Byte); virtual;
// 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;
procedure SetAlexConfig(const A: TAlexSettings); virtual;
procedure SetXvtrMode(AEnable, ADisablePA: Boolean; ARxAnt: Byte); virtual;
procedure SendDDCAudio(const LeftRight: array of SmallInt); virtual;
@@ -204,6 +207,10 @@ procedure TRadioBackend.SetStepAtten(dB: Byte);
begin
end;
procedure TRadioBackend.SetRxGain(ModeIdx: Integer; GainDb: Integer);
begin
end;
procedure TRadioBackend.SetAlexConfig(const A: TAlexSettings);
begin
end;
+47
View File
@@ -58,6 +58,7 @@ type
rfVfoA, rfVfoB, rfActiveVfo,
rfMode, rfFilter, rfFilterBW,
rfAGCMode, rfAGCTop,
rfRxGain, // Pluto: hw-gain (auto/manual dB)
rfVolume, rfMute, rfDrive, rfAtten,
rfBand, rfXvtr, rfBandRestore, // rfBandRestore: явная смена диапазона (сброс Wf-avg в UI)
rfCenterFreq, rfSpan, rfSampleRate,
@@ -95,6 +96,8 @@ type
ActiveVfo: Integer;
Mode, Filter, FilterBW: Integer;
AGCMode, AGCTop: Integer;
RxGainMode: Integer;
RxGainDb: Integer;
Volume, Drive, Atten: Integer;
Band, Xvtr: Integer;
Muted, CTun, Duplex: Boolean;
@@ -164,6 +167,8 @@ type
FFilterBW: Integer;
FAGCMode: Integer;
FAGCTop: Integer;
FRxGainMode: Integer; // Pluto hw-gain: 0=manual,1=fast,2=slow,3=hybrid
FRxGainDb: Integer; // Pluto manual gain (дБ, 0..73)
FCTun: Boolean;
FCenterFreq: Double;
FSpanHz: Double;
@@ -395,6 +400,13 @@ type
procedure SetAGCTop(DB: Integer);
procedure AGCTopBy(Delta: Integer);
// RX hw-gain (Pluto): режим (0=manual,1=fast,2=slow,3=hybrid) / ручной дБ.
// На HPSDR — no-op в бэкенде.
procedure SetRxGainMode(M: Integer);
procedure SetRxGain(Db: Integer);
procedure RxGainBy(Delta: Integer);
procedure ApplyRxGainToBackend; // FRxGain* → FNetwork.SetRxGain
// Громкость / mute / drive / аттенюатор
procedure SetVolume(V: Integer);
procedure VolumeBy(Delta: Integer);
@@ -486,6 +498,7 @@ begin
FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0;
FMode := 1; FFilter := 5; FFilterBW := 2700;
FAGCMode := 1; FAGCTop := 90;
FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию
FVolume := 70; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
FCurrentBand := 5; FCurrentXvtr := -1;
@@ -863,6 +876,7 @@ begin
Result.SampleRate := FSampleRate; Result.ActiveVfo := FActiveVfo;
Result.Mode := FMode; Result.Filter := FFilter; Result.FilterBW := FFilterBW;
Result.AGCMode := FAGCMode; Result.AGCTop := FAGCTop;
Result.RxGainMode := FRxGainMode; Result.RxGainDb := FRxGainDb;
Result.Volume := FVolume; Result.Drive := FDrivePercent; Result.Atten := FAtten;
Result.Band := FCurrentBand; Result.Xvtr := FCurrentXvtr;
Result.Muted := FMuted; Result.CTun := FCTun; Result.Duplex := FDisplayDuplex;
@@ -1121,6 +1135,8 @@ begin
Result.LastBand := FCurrentBand;
Result.LastXvtr := FCurrentXvtr;
Result.SampleRate := FSampleRate;
Result.RxGainMode := FRxGainMode;
Result.RxGainDb := FRxGainDb;
if FWDSPReady then
begin
Result.FFTSize := FDSPEngine.FFTSize;
@@ -1633,6 +1649,33 @@ end;
procedure TRadioController.AGCTopBy(Delta: Integer);
begin SetAGCTop(FAGCTop + Delta); end;
procedure TRadioController.ApplyRxGainToBackend;
begin
if Assigned(FNetwork) and FNetwork.Connected then
FNetwork.SetRxGain(FRxGainMode, FRxGainDb);
end;
procedure TRadioController.SetRxGainMode(M: Integer);
begin
if M < 0 then M := 0 else if M > 3 then M := 3;
FRxGainMode := M;
ApplyRxGainToBackend;
Changed(rfRxGain);
end;
procedure TRadioController.SetRxGain(Db: Integer);
begin
if Db < 0 then Db := 0 else if Db > 73 then Db := 73;
FRxGainDb := Db;
// Ручная установка усиления подразумевает manual-режим (0).
FRxGainMode := 0;
ApplyRxGainToBackend;
Changed(rfRxGain);
end;
procedure TRadioController.RxGainBy(Delta: Integer);
begin SetRxGain(FRxGainDb + Delta); end;
procedure TRadioController.SetVolume(V: Integer);
begin
if V < 0 then V := 0 else if V > 100 then V := 100;
@@ -2284,6 +2327,8 @@ begin
FNetwork.ConfigureDDCs(1, FSampleRate div 1000, 0, FDitherEnabled, FRandomEnabled);
// Применяем сохранённое состояние динамика/отправки аудио (мьют byte 1400 bit1).
FNetwork.SetSpeakerAudio(FSendAudioToRadio);
// RX hw-gain в бэкенд (Pluto). HPSDR — no-op.
ApplyRxGainToBackend;
SendDUCSpecificFromSettings;
// Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...).
ApplyTXSettingsToDSP;
@@ -2374,6 +2419,8 @@ begin
FSampleRate := 1536000;
FSpanHz := FSampleRate;
end;
FRxGainMode := EnsureRange(FLoadedGlobal.RxGainMode, 0, 3);
FRxGainDb := EnsureRange(FLoadedGlobal.RxGainDb, 0, 73);
FNRMode := EnsureRange(FLoadedGlobal.NRMode, 0, 4);
FNBMode := EnsureRange(FLoadedGlobal.NBMode, 0, 2);
FSNB := FLoadedGlobal.SNBEnabled;
+7
View File
@@ -218,6 +218,8 @@ type
LastBand: Integer;
LastXvtr: Integer; // -1 = HF, 0..CFG_XVTR_COUNT-1 = активный трансвертер при выходе
SampleRate: Integer; // глобальный — один для всех диапазонов
RxGainMode: Integer; // Pluto: 0=manual,1=fast,2=slow,3=hybrid
RxGainDb: Integer; // Pluto: manual gain дБ
WindowLeft: Integer;
WindowTop: Integer;
WindowWidth: Integer;
@@ -401,6 +403,7 @@ begin
G.WfAGCEnabled := True;
G.WfNFEnabled := False;
G.SampleRate := 192000;
G.RxGainMode := 2; G.RxGainDb := 40;
G.PAMaxPower := 100.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;
@@ -597,6 +600,8 @@ begin
G.LastBand := JI(GObj,'last_band',5);
G.LastXvtr := JI(GObj,'last_xvtr',-1);
G.SampleRate := JI(GObj,'sample_rate',192000);
G.RxGainMode := JI(GObj,'rx_gain_mode',2);
G.RxGainDb := JI(GObj,'rx_gain_db',40);
// Display settings
G.FFTSize := JI(GObj,'fft_size',131072);
G.WindowType := JI(GObj,'win_type',2);
@@ -712,6 +717,8 @@ begin
JW(O,'last_band',G.LastBand);
JW(O,'last_xvtr',G.LastXvtr);
JW(O,'sample_rate',G.SampleRate);
JW(O,'rx_gain_mode',G.RxGainMode);
JW(O,'rx_gain_db',G.RxGainDb);
// Display settings
JW(O,'fft_size',G.FFTSize);
JW(O,'win_type',G.WindowType);