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
+24 -4
View File
@@ -156,6 +156,9 @@ type
FMainDither: Boolean;
FMainRandom: Boolean;
// Частотная калибровка опорника радио (ppm) — вносится в частотное слово
// (тактовый генератор радио менять нечем, поэтому правим запрос).
FFreqCalPPM: Double;
// Текущее состояние для построения HP пакетов
FCurrentRXFreq: Double;
FCurrentTXFreq: Double;
@@ -260,6 +263,9 @@ type
procedure UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte;
Transmitting, PAEnabled, AlexEnabled: Boolean); override;
procedure SetStepAtten(dB: Byte); override;
procedure SetFreqCalPPM(P: Double); override;
// Частота с поправкой опорника — для частотного слова DDC/DUC.
function CalFreq(Hz: Double): Double;
procedure SetAlexConfig(const A: TAlexSettings); override;
// Включение/настройка XVTR-режима. Все параметры применяются мгновенно
// (отправляется HP-пакет). Когда AEnable=False, XVTR-bit гасится,
@@ -736,6 +742,7 @@ begin
FWBUpdateRateMS := 70;
FWBFrameCount := 0;
FWBSeqValid := False;
FFreqCalPPM := 0.0;
FCurrentRXFreq := 7100000;
FCurrentTXFreq := 7100000;
FCurrentDrive := 0;
@@ -1693,6 +1700,19 @@ begin
FAlexEnabled := AlexEnabled;
end;
function THPSDRNetwork.CalFreq(Hz: Double): Double;
// Опора радио выше номинала на FFreqCalPPM ⇒ DDC/DUC уходят вверх на столько
// же. Компенсируем запрошенной частотой: просим ниже ровно на ppm.
begin
if FFreqCalPPM = 0 then Exit(Hz);
Result := Hz / (1.0 + FFreqCalPPM * 1.0E-6);
end;
procedure THPSDRNetwork.SetFreqCalPPM(P: Double);
begin
FFreqCalPPM := P;
end;
procedure THPSDRNetwork.SetStepAtten(dB: Byte);
begin
if dB > 31 then dB := 31;
@@ -1737,7 +1757,7 @@ begin
DDCBase := DDCBaseIndex;
// DDC RX frequency (bytes 9 + DDC*4)
Ph := FreqToPhaseWord(FCurrentRXFreq);
Ph := FreqToPhaseWord(CalFreq(FCurrentRXFreq));
Buf[9 + DDCBase*4] := (Ph shr 24) and $FF;
Buf[10 + DDCBase*4] := (Ph shr 16) and $FF;
Buf[11 + DDCBase*4] := (Ph shr 8) and $FF;
@@ -1748,7 +1768,7 @@ begin
for ddc := 0 to MAX_DDCS - 1 do
if FPanDDCEnabled[ddc] then
begin
Ph := FreqToPhaseWord(FPanDDCFreqHz[ddc]);
Ph := FreqToPhaseWord(CalFreq(FPanDDCFreqHz[ddc]));
Buf[9 + ddc*4] := (Ph shr 24) and $FF;
Buf[10 + ddc*4] := (Ph shr 16) and $FF;
Buf[11 + ddc*4] := (Ph shr 8) and $FF;
@@ -1759,7 +1779,7 @@ begin
// DDCBase=2 ⇒ с частотным словом главного DDC (bytes 17..20) не пересекаемся.
if FPSTXActive then
begin
Ph := FreqToPhaseWord(FCurrentTXFreq);
Ph := FreqToPhaseWord(CalFreq(FCurrentTXFreq));
Buf[9] := (Ph shr 24) and $FF;
Buf[10] := (Ph shr 16) and $FF;
Buf[11] := (Ph shr 8) and $FF;
@@ -1771,7 +1791,7 @@ begin
end;
// DUC TX frequency (bytes 329-332)
Ph := FreqToPhaseWord(FCurrentTXFreq);
Ph := FreqToPhaseWord(CalFreq(FCurrentTXFreq));
Buf[329] := (Ph shr 24) and $FF;
Buf[330] := (Ph shr 16) and $FF;
Buf[331] := (Ph shr 8) and $FF;