feature(calibration): частотная калибровка опорника (ppm) + TO Error в слоте QO-100

Пункт 3 плана. Ручные подгонки остаются: ppm работает ПОД ними, а не вместо —
уход тракта где-то больше, где-то меньше.

- Settings: TCalibration.FreqCalPPM (JSON cal_freq_ppm, +-100 ppm, per-device).
  Смысл знака: «опора выше номинала на N ppm», положительное значение
  поднимает показываемые частоты.
- RadioBackend: виртуальный SetFreqCalPPM.
- HPSDRNetwork: CalFreq(Hz) = Hz / (1 + ppm) в частотном слове — все четыре
  точки (главный DDC, DDC панов, feedback-DDC PureSignal, DUC передачи).
  Тактовый генератор радио менять нечем, поэтому правим запрос.
- PlutoBackend: штатный xo_correction в ad9361-phy (device-attr биндинги
  добавлены в IIOBindings). Номинал XO снимается при коннекте с округлением
  до 100 кГц, чтобы наша же коррекция с прошлой сессии не накапливалась;
  после записи LO переписываются, иначе правка ждала бы следующей перестройки.
  Одна цифра лечит RX и TX на всех диапазонах.
- QO-100: TXvtrEntry.TxLOError (JSON tx_lo_error) + поле «TO Error (Hz)» на
  странице QO-100. Сам транспондер-offset держим стандартным (8089.5 МГц),
  уход тракта правим поправкой; ActiveTXFreqHz учитывает её, подсказка внизу
  страницы считает TX-частоту уже с ней.
- UI-фиксы вкладки Calibration: длинные подсказки переведены на несколько
  строк (WordWrap — TLabel по умолчанию режется по краю группы), заголовки
  таблиц укорочены и не заезжают под кнопку «= S-meter»; RelayoutCalTab
  сбрасывает прокрутку страницы перед перекладкой (SetBounds в TScrollBox
  задаёт координаты относительно прокрученного клиента).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 12:35:46 +03:00
co-authored by Claude Opus 4.8
parent a95292d2c0
commit d51f0fb614
7 changed files with 169 additions and 12 deletions
+40
View File
@@ -46,6 +46,8 @@ type
FTXFreq: Double;
FSampleRate: Integer;
FTransmitting: Boolean;
FFreqCalPPM: Double; // частотная калибровка опорника, ppm (xo_correction)
FXoNomHz: Int64; // номинал XO, Гц (снимается при коннекте, 0 = не знаем)
FGainMode: string; // gain_control_mode (manual/slow_attack/...)
FGainDb: Integer; // hardwaregain при ручном режиме
@@ -118,6 +120,7 @@ type
GainDb: Double): Boolean;
// Низкоуровневые помощники применения атрибутов к ad9361-phy
procedure ApplyFreqCal;
procedure ApplyRxLO(Hz: Double);
procedure ApplyTxLO(Hz: Double);
procedure SetTxLOPowerdown(PoweredOn: Boolean); // TX LO вкл/выкл (по PTT)
@@ -177,6 +180,7 @@ type
procedure ClearDUCIQQueue; override;
function ReadTelemetry(out TempC, SocTempC, RSSIdB: Double): Boolean; override;
function RxGainNowDb: Double; override;
procedure SetFreqCalPPM(P: Double); override;
end;
// Стабильный 6-байтовый ключ (synthetic MAC) из serial/URI — для per-device
@@ -410,6 +414,7 @@ begin
FSampleRate := 1536000;
FGainMode := 'slow_attack'; // аппаратный AGC по умолчанию
FGainDb := 40;
FFreqCalPPM := 0.0; FXoNomHz := 0;
FRxBuf := nil; FRxChI := nil; FRxChQ := nil; FRxThread := nil;
FStreaming := False; FRxSeq := 0;
FTxBuf := nil; FTxChI := nil; FTxChQ := nil; FTxThread := nil;
@@ -635,6 +640,7 @@ begin
ApplyTxMux;
ApplyGain(True);
ApplyTxAtten; // TX заглушён (макс. аттенюация) до явного PTT
ApplyFreqCal; // частотная калибровка опорника (xo_correction)
StartTelemetry;
Result := True;
end;
@@ -656,6 +662,40 @@ end;
// ---- Применение атрибутов к ad9361-phy ----
procedure TPlutoBackend.ApplyFreqCal;
// xo_correction — частота опорного XO в Гц, какой её считает драйвер. Правка
// разъезжается на ВСЁ: RX LO, TX LO, sample rate — один параметр вместо
// подгонки offset'ов по трансвертерам.
// Номинал снимаем округлением прочитанного до 100 кГц: в железе могла
// остаться наша же коррекция с прошлой сессии (она сбрасывается только по
// перезагрузке платы), а ±100 ppm от 40 МГц — это ±4 кГц, округление их
// съедает и компаундинга не будет.
var
V: clonglong;
begin
if (FPhy = nil) or (not IIOLoaded)
or (not Assigned(iio_device_attr_read_longlong))
or (not Assigned(iio_device_attr_write_longlong)) then Exit;
if FXoNomHz = 0 then
begin
if iio_device_attr_read_longlong(FPhy, 'xo_correction', V) <> 0 then Exit;
if V <= 0 then Exit;
FXoNomHz := (Int64(V) + 50000) div 100000 * 100000;
end;
iio_device_attr_write_longlong(FPhy, 'xo_correction',
Round(FXoNomHz * (1.0 + FFreqCalPPM * 1.0E-6)));
// Синтезаторы считают делители в момент записи частоты — переписываем LO,
// иначе правка подхватится только на следующей перестройке.
if FRXFreq > 0 then ApplyRxLO(FRXFreq);
if FTXFreq > 0 then ApplyTxLO(FTXFreq);
end;
procedure TPlutoBackend.SetFreqCalPPM(P: Double);
begin
FFreqCalPPM := P;
ApplyFreqCal;
end;
procedure TPlutoBackend.ApplyRxLO(Hz: Double);
var Ch: Piio_channel;
begin