feat(pluto): выбор антенного разъёма RX/TX на диапазон (LibreSDR/AntSDR)

Второй разъём у AD936x-плат — это второй КАНАЛ чипа (не порт B первого), и в
1R1T-образе он выбирается мультиплексором AD9361: debug-параметры
adi,1rx-1tx-mode-use-rx-num/-tx-num + биты RX1/RX2, TX1/TX2 регистров 0x3/0x2
(как switch_rfinput.sh/switch_rfoutput.sh прошивки). rf_port_select тут ни при
чём — прошивка его к тому же блокирует.

- IIOBindings: debug-атрибуты + прямой доступ к регистрам.
- PlutoBackend: детект второго разъёма (2R2T-каналы либо мультиплексор + модель
  платы), SetAntPorts(RxPort,TxPort), ApplyRxMux/ApplyTxMux; Caps.HasAntSwitch.
  ⚠ApplyGain(Force): драйвер пропускает запись gain_control_mode, если считает
  режим неизменным, — а у нового канала AGC-регистры не запрограммированы (в
  железе крутится auto-AGC, hardwaregain отвергается EOPNOTSUPP). Форсируем
  перепрог через другой режим после смены разъёма и после ad9361_set_bb_rate.
- RadioBackend: TBackendCaps.HasAntSwitch + virtual SetAntPorts (HPSDR — no-op,
  там антеннами рулит Alex).
- Settings: секция ant936x (разъём RX/TX на диапазон и на слот трансвертера);
  hw-AGC (режим + manual gain) переехал в TBandSettings/TXvtrEntry — теперь он
  свой на каждом диапазоне и трансвертере.
- RadioController: FAnt936x + ApplyAntPorts (RestoreBand/ActivateXvtr/
  StartRunning); ApplyBandDSP восстанавливает hw-AGC диапазона.
- SettingsForm: вкладка Antenna — Alex-таблица (openHPSDR) либо таблица
  разъёмов AD936x по диапазонам/трансвертерам.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 21:26:18 +03:00
co-authored by Claude Opus 4.8
parent a2e403af3d
commit 9dd38e1dd3
8 changed files with 673 additions and 32 deletions
+226 -18
View File
@@ -58,6 +58,24 @@ type
FRxThread: TThread;
FStreaming: Boolean;
FRxSeq: LongWord;
// Второй антенный вход (RX2). Два разных механизма — какой доступен, зависит
// от прошивки/FPGA-образа платы:
// • 2R2T-образ: второй канал инстанцирован, в cf-ad9361-lpc есть voltage2/3 →
// выбор = пара стрим-каналов (FRx2Avail). Задел на второй пан (этап 3.6).
// • 1R1T-образ (напр. LibreSDR/tezuka): второй канал в FPGA не разведён, но
// внутри AD9361 есть мультиплексор — какой физический разъём кормит
// единственный тракт. Выбор = debug-атрибуты 'adi,1rx-1tx-mode-use-rx-num'/
// '-tx-num' (1|2) + 'initialize'=1 (драйвер заново прогоняет ad9361_setup
// из pdata). ⚠Сырые записи битов RX1/RX2 в регистры 0x2/0x3 (как в
// switch_rfinput.sh прошивки) НЕ годятся: тракт переключается, а учёт
// драйвера (gain_control_mode/agc_mode на канал) остаётся от старого —
// hardwaregain потом отвергается («Rx gain can be set in MGC mode only»),
// а в железе продолжает крутиться AGC. rf_port_select тут ни при чём:
// RX2-разъём разведён на второй КАНАЛ, а не на порт B первого.
FRx2Avail: Boolean; // 2R2T: в cf-ad9361-lpc есть voltage2/voltage3
FRxMuxAvail: Boolean; // 1R1T: доступен RX/TX-мультиплексор AD9361
FRxChanSel: Byte; // 0 = RX1, 1 = RX2
FTxChanSel: Byte; // 0 = TX1, 1 = TX2
// TX-стрим (cf-ad9361-dds-core-lpc): I/Q output-каналы, буфер, поток push.
// Full-duplex: стрим живёт всё время Run; на приёме гоним нули (мощности нет
@@ -103,8 +121,19 @@ type
procedure SetTxLOPowerdown(PoweredOn: Boolean); // TX LO вкл/выкл (по PTT)
procedure ApplySampleRate(Hz: Integer);
procedure BuildTxResampler; // (пере)строить полифазные коэффициенты под FTxRatio
procedure ApplyGain;
// Force=True — перед записью режима пишем ДРУГОЙ режим: драйвер пропускает
// перепрог AGC-регистров, если считает режим неизменным, а после смены
// антенного мультиплексора настройки AGC нового канала ещё не запрограммированы
// (в железе крутится auto-AGC, hardwaregain отвергается с EOPNOTSUPP).
procedure ApplyGain(Force: Boolean = False);
procedure ApplyTxAtten;
// Имя phy-канала управления выбранным RX-трактом (гейн/RSSI/rf_bandwidth):
// 'voltage0' = RX1, 'voltage1' = RX2 (только 2R2T; в 1R1T канал всегда один).
function RxPhyChanName: string;
// 1R1T: переключить мультиплексор AD9361 на выбранные разъёмы (см. FRxMuxAvail).
// Дёшево: debug-атрибут (маппинг каналов у драйвера) + биты регистра 0x3/0x2.
procedure ApplyRxMux;
procedure ApplyTxMux;
procedure StartRX;
procedure StopRX;
procedure StartTX;
@@ -139,6 +168,7 @@ type
Transmitting, PAEnabled, AlexEnabled: Boolean); override;
procedure SendFullHP; override;
procedure SendPTT(Active: Boolean); override;
procedure SetAntPorts(RxPort, TxPort: Byte); override;
procedure SetRxGain(ModeIdx: Integer; GainDb: Integer); override;
procedure SetTxAtten(Db: Double); override;
procedure SendDUCIQ(const IData, QData: array of Integer); override;
@@ -171,6 +201,10 @@ const
TX_FIR_CUT = 40000; // частота среза анти-имидж ФНЧ, Гц (покрывает SSB/NFM/AM/data)
PLUTO_TX_OFF = -89.75; // макс. аттенюация AD9361 — TX практически выключен
// Детект второго антенного входа (тела ниже — нужны до Connect).
function BoardHasSecondRxPort(const Model: string): Boolean; forward;
function DebugAttrExists(Dev: Piio_device; const Attr: string): Boolean; forward;
type
// RX-поток: блокирующий refill libiio → конверсия int16→24bit BE → OnDDCIQ.
// Работает с private-полями TPlutoBackend (одна единица компиляции).
@@ -432,11 +466,14 @@ begin
Result.RatePresets[7] := 5760000;
Result.MinFreqHz := PLUTO_MIN_HZ;
Result.MaxFreqHz := PLUTO_MAX_HZ;
// Один RX-тракт только главный пан. (Pluto+ 2r2t = MaxPans 2 с общим
// RX LO, IndependentPanFreq=False — этап 3.6.)
// Стримим ровно один тракт только главный пан. Одновременный приём с двух
// антенн (второй пан на RX2, общий RX LO) — этап 3.6, нужен 2R2T-образ.
Result.MaxPans := 1;
Result.IndependentPanFreq := False;
Result.NumADCs := 1;
// Пан один ⇒ ADC-селектор панов не при чём: у AD936x «антенна» — это выбор
// физических разъёмов (RX1/RX2, TX1/TX2) на диапазон, вкладка Antenna настроек.
Result.NumADCs := 1;
Result.HasAntSwitch := FRx2Avail or FRxMuxAvail;
end;
function TPlutoBackend.ProbeURI(const AURI: string; out Dev: TRadioDevice): Boolean;
@@ -536,6 +573,7 @@ end;
function TPlutoBackend.Connect(const Dev: TRadioDevice): Boolean;
var
M: TBytes;
PV: PAnsiChar;
begin
Result := False;
if not IIOLoad then begin FLastError := 'libiio not available'; Exit; end;
@@ -566,11 +604,33 @@ begin
Exit;
end;
// 2R2T-образ — по НАЛИЧИЮ каналов второго тракта (клоны не отличить по строкам
// описания). Управление RX2 (гейн/RSSI) — phy input voltage1, требуем и его.
FRx2Avail := (FRxDev <> nil) and
(iio_device_find_channel(FRxDev, 'voltage2', 0) <> nil) and
(iio_device_find_channel(FRxDev, 'voltage3', 0) <> nil) and
(iio_device_find_channel(FPhy, 'voltage1', 0) <> nil);
// Модель берём из ЖИВОГО контекста: в записи устройства она могла приехать из
// сохранёнки/фолбэка ProbeURI ('AD936x SDR') и не содержать имени платы.
PV := iio_context_get_attr_value(FCtx, 'hw_model');
if PV <> nil then FDevice.Model := string(PV);
// 1R1T-образ: RX-мультиплексор AD9361. Гейт по МОДЕЛИ — у стокового Pluto
// второй вход никуда не разведён (кремний двухканальный, а SMA один), так что
// селектор там показывать нельзя: он дал бы «мёртвую» антенну.
FRxMuxAvail := (not FRx2Avail) and BoardHasSecondRxPort(FDevice.Model) and
Assigned(iio_device_debug_attr_write) and
DebugAttrExists(FPhy, 'adi,1rx-1tx-mode-use-rx-num') and
DebugAttrExists(FPhy, 'adi,1rx-1tx-mode-use-tx-num') and
DebugAttrExists(FPhy, 'initialize');
if not (FRx2Avail or FRxMuxAvail) then FRxChanSel := 0;
FConnected := True;
// Приводим железо в известное состояние: валидный rate + усиление.
// Приводим железо в известное состояние: валидный rate + вход + усиление.
if FSampleRate < PLUTO_MIN_SR then FSampleRate := PLUTO_DEF_SR;
ApplySampleRate(FSampleRate);
ApplyGain;
ApplyRxMux; // разъёмы могли остаться от прошлой сессии (состояние в чипе)
ApplyTxMux;
ApplyGain(True);
ApplyTxAtten; // TX заглушён (макс. аттенюация) до явного PTT
StartTelemetry;
Result := True;
@@ -651,8 +711,9 @@ begin
iio_channel_attr_write_longlong(Ch, 'sampling_frequency', Hz);
end;
// RF-полоса отдельно (set_bb_rate её не трогает).
Ch := iio_device_find_channel(FPhy, 'voltage0', 0);
// RF-полоса отдельно (set_bb_rate её не трогает) — на phy-канале выбранного
// приёмного тракта (у 2R2T они независимы).
Ch := iio_device_find_channel(FPhy, PAnsiChar(AnsiString(RxPhyChanName)), 0);
if Ch <> nil then
iio_channel_attr_write_longlong(Ch, 'rf_bandwidth', Hz);
@@ -698,12 +759,129 @@ begin
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;
function BoardHasSecondRxPort(const Model: string): Boolean;
// Есть ли у платы физический разъём RX2. Кремний AD9361/AD9363 двухканальный на
// ЛЮБОЙ плате, включая стоковый Pluto, — но у Pluto второй канал никуда не выведен,
// поэтому переключать на него нельзя (антенны там нет). Список плат с двумя RX SMA.
const
BOARDS: array[0..4] of string = ('libresdr', 'antsdr', 'pluto+', 'plutoplus', 'e200');
var
M: string;
i: Integer;
begin
Result := False;
M := LowerCase(StringReplace(Model, ' ', '', [rfReplaceAll]));
for i := Low(BOARDS) to High(BOARDS) do
if Pos(BOARDS[i], M) > 0 then Exit(True);
end;
function DebugAttrExists(Dev: Piio_device; const Attr: string): Boolean;
var Buf: array[0..63] of AnsiChar;
begin
Result := Assigned(iio_device_debug_attr_read) and (Dev <> nil) and
(iio_device_debug_attr_read(Dev, PAnsiChar(AnsiString(Attr)),
@Buf[0], SizeOf(Buf)) >= 0);
end;
function TPlutoBackend.RxPhyChanName: string;
// Управляющий phy-канал приёмного тракта: RX1 = input voltage0, RX2 = voltage1.
// В 1R1T-образе канал ровно один (voltage0) — мультиплексор меняет вход ПОД ним,
// поэтому гейн/RSSI остаются на voltage0.
begin
if (FRxChanSel = 1) and FRx2Avail then Result := 'voltage1'
else Result := 'voltage0';
end;
procedure TPlutoBackend.ApplyRxMux;
// Приёмный разъём: debug-параметр (по нему драйвер маппит gain-атрибуты на канал)
// + биты RX1/RX2 (маска 0xC0) регистра 0x003 — как switch_rfinput.sh прошивки.
// ⚠Вызывающий обязан следом сделать ApplyGain(True): AGC-регистры нового канала
// ещё не запрограммированы (см. ApplyGain).
var
Val: cuint32;
RxNum: Integer;
begin
if not (FConnected and FRxMuxAvail and (FPhy <> nil)) then Exit;
if FRxChanSel = 1 then RxNum := 2 else RxNum := 1;
iio_device_debug_attr_write(FPhy, 'adi,1rx-1tx-mode-use-rx-num',
PAnsiChar(AnsiString(IntToStr(RxNum))));
Val := 0;
if iio_device_reg_read(FPhy, $3, Val) <> 0 then Exit;
Val := Val and $3F;
if FRxChanSel = 1 then Val := Val or $80 // RX2
else Val := Val or $40; // RX1
iio_device_reg_write(FPhy, $3, Val);
end;
procedure TPlutoBackend.ApplyTxMux;
// То же для передачи (switch_rfoutput.sh): '-tx-num' + биты TX1/TX2 регистра 0x002.
var
Val: cuint32;
TxNum: Integer;
begin
if not (FConnected and FRxMuxAvail and (FPhy <> nil)) then Exit;
if FTxChanSel = 1 then TxNum := 2 else TxNum := 1;
iio_device_debug_attr_write(FPhy, 'adi,1rx-1tx-mode-use-tx-num',
PAnsiChar(AnsiString(IntToStr(TxNum))));
Val := 0;
if iio_device_reg_read(FPhy, $2, Val) <> 0 then Exit;
Val := Val and $3F;
if FTxChanSel = 1 then Val := Val or $80 // TX2
else Val := Val or $40; // TX1
iio_device_reg_write(FPhy, $2, Val);
end;
procedure TPlutoBackend.SetAntPorts(RxPort, TxPort: Byte);
// Выбор физических разъёмов: 0 = RX1/TX1, 1 = RX2/TX2. Зовётся контроллером при
// смене диапазона/трансвертера (антенна — свойство диапазона, см. вкладку Antenna).
var
RxChanged, TxChanged: Boolean;
begin
if RxPort > 1 then RxPort := 0;
if TxPort > 1 then TxPort := 0;
if not (FRx2Avail or FRxMuxAvail) then Exit; // одна пара разъёмов — нечего выбирать
RxChanged := RxPort <> FRxChanSel;
TxChanged := TxPort <> FTxChanSel;
if not (RxChanged or TxChanged) then Exit;
FRxChanSel := RxPort;
FTxChanSel := TxPort;
if not FConnected then Exit;
if FRx2Avail then
begin
// 2R2T-образ: разъём = ДРУГАЯ пара стрим-каналов ⇒ пересоздать буферы, затем
// вернуть гейн/аттенюацию (у 2R2T они ещё и живут на phy-канале своего тракта).
if RxChanged and FStreaming then begin StopRX; StartRX; end;
if TxChanged and FTxStreaming then begin StopTX; StartTX; end;
ApplyGain;
ApplyTxAtten;
end
else
begin
// 1R1T: мультиплексор внутри чипа — стримы/LO/rate не трогаем (дёшево).
// Обе записи ДО восстановления усиления, и гейн — форсированный: у нового
// канала AGC-регистры ещё не запрограммированы.
if RxChanged then ApplyRxMux;
if TxChanged then ApplyTxMux;
ApplyGain(True);
ApplyTxAtten;
end;
end;
procedure TPlutoBackend.ApplyGain(Force: Boolean);
var Ch: Piio_channel; Other: string;
begin
if FPhy = nil then Exit;
Ch := iio_device_find_channel(FPhy, 'voltage0', 0); // RX gain — input voltage0
// Усиление/режим AGC — поканальные: пишем в тракт выбранной антенны.
Ch := iio_device_find_channel(FPhy, PAnsiChar(AnsiString(RxPhyChanName)), 0);
if Ch = nil then Exit;
if Force then
begin
// Прогоняем через другой режим — иначе драйвер сочтёт режим неизменным и не
// перепрограммирует AGC-регистры канала (после смены мультиплексора там
// остаётся auto-AGC, а hardwaregain отвергается: «Rx gain can be set in MGC only»).
if FGainMode = 'manual' then Other := 'slow_attack' else Other := 'manual';
iio_channel_attr_write(Ch, 'gain_control_mode', PAnsiChar(AnsiString(Other)));
end;
iio_channel_attr_write(Ch, 'gain_control_mode', PAnsiChar(AnsiString(FGainMode)));
if FGainMode = 'manual' then
iio_channel_attr_write_longlong(Ch, 'hardwaregain', FGainDb);
@@ -721,10 +899,17 @@ begin
end;
procedure TPlutoBackend.StartRX;
var Base: Integer; ChI, ChQ: string;
begin
if FStreaming or not FConnected or (FRxDev = nil) then Exit;
FRxChI := iio_device_find_channel(FRxDev, 'voltage0', 0);
FRxChQ := iio_device_find_channel(FRxDev, 'voltage1', 0);
// Пара стрим-каналов выбранного тракта: RX1 = voltage0/1, RX2 = voltage2/3.
// Включаем ТОЛЬКО её — второй тракт не стримим (иначе вдвое трафик/CPU).
Base := 0;
if (FRxChanSel = 1) and FRx2Avail then Base := 2;
ChI := 'voltage' + IntToStr(Base);
ChQ := 'voltage' + IntToStr(Base + 1);
FRxChI := iio_device_find_channel(FRxDev, PAnsiChar(AnsiString(ChI)), 0);
FRxChQ := iio_device_find_channel(FRxDev, PAnsiChar(AnsiString(ChQ)), 0);
if (FRxChI = nil) or (FRxChQ = nil) then
begin FLastError := 'RX I/Q channels not found'; Exit; end;
iio_channel_enable(FRxChI);
@@ -753,13 +938,26 @@ begin
iio_buffer_destroy(FRxBuf);
FRxBuf := nil;
end;
// Снять enable с пары каналов: иначе после смены антенного входа (2R2T) в
// новом буфере окажутся включёнными оба тракта — чередование I1,Q1,I2,Q2.
if FRxChI <> nil then iio_channel_disable(FRxChI);
if FRxChQ <> nil then iio_channel_disable(FRxChQ);
FRxChI := nil;
FRxChQ := nil;
end;
procedure TPlutoBackend.StartTX;
var Base: Integer; ChI, ChQ: string;
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
// 2R2T-образ: пара выходных каналов выбранного разъёма (TX1 = voltage0/1,
// TX2 = voltage2/3). В 1R1T пара всегда одна — разъём переключает ApplyTxMux.
Base := 0;
if (FTxChanSel = 1) and FRx2Avail then Base := 2;
ChI := 'voltage' + IntToStr(Base);
ChQ := 'voltage' + IntToStr(Base + 1);
FTxChI := iio_device_find_channel(FTxDev, PAnsiChar(AnsiString(ChI)), 1); // output I
FTxChQ := iio_device_find_channel(FTxDev, PAnsiChar(AnsiString(ChQ)), 1); // output Q
if (FTxChI = nil) or (FTxChQ = nil) then
begin FLastError := 'TX I/Q channels not found'; Exit; end;
iio_channel_enable(FTxChI);
@@ -805,11 +1003,20 @@ end;
procedure TPlutoBackend.ConfigureDDCs(NumDDCs: Byte; SampleRate: Word;
ADCSource: Byte; DitherEnabled, RandomEnabled: Boolean);
begin
// SampleRate приходит в ksps (как у HPSDR ConfigureDDCs). Pluto — в Гц.
// ADCSource (АЦП-источник HPSDR) у AD936x смысла не имеет — антенные разъёмы
// переключает SetAntPorts. SampleRate приходит в ksps (как у HPSDR), Pluto — в Гц.
// Контроллер может прислать HPSDR-наследие (<520k) — клампим к дефолту.
FSampleRate := Integer(SampleRate) * 1000;
if FSampleRate < PLUTO_MIN_SR then FSampleRate := PLUTO_DEF_SR;
if FConnected then ApplySampleRate(FSampleRate); // перезапустит RX при смене
if not FConnected then Exit;
ApplySampleRate(FSampleRate); // перезапустит RX при смене
// ad9361_set_bb_rate перепрошивает FIR/BBPLL и сбрасывает усиление/AGC к
// дефолтам — возвращаем разъёмы и форсированно гейн + TX-аттенюацию (иначе
// в железе остаётся auto-AGC: высокая шумовая полка).
ApplyRxMux;
ApplyTxMux;
ApplyGain(True);
ApplyTxAtten;
end;
procedure TPlutoBackend.SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double;
@@ -990,9 +1197,10 @@ begin
end;
// RSSI имеет смысл только при активном RX-стриме (иначе чтение даёт ошибку/мусор).
// Читаем с phy-канала выбранного приёмного тракта (RX1/RX2 — независимы).
if FStreaming then
begin
Ch := iio_device_find_channel(FPhy, 'voltage0', 0);
Ch := iio_device_find_channel(FPhy, PAnsiChar(AnsiString(RxPhyChanName)), 0);
if (Ch <> nil) and Assigned(iio_channel_attr_read_double)
and (iio_channel_attr_read_double(Ch, 'rssi', d) = 0) then
begin