diff --git a/HPSDRNetwork.pas b/HPSDRNetwork.pas index cc742c7..8de2900 100644 --- a/HPSDRNetwork.pas +++ b/HPSDRNetwork.pas @@ -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; diff --git a/IIOBindings.pas b/IIOBindings.pas index 78c5367..adb02c0 100644 --- a/IIOBindings.pas +++ b/IIOBindings.pas @@ -72,6 +72,14 @@ var iio_channel_attr_write_double: function(chn: Piio_channel; attr: PAnsiChar; val: cdouble): cint; cdecl; + // ---- Атрибуты устройства (не канальные) ---- + // ad9361-phy: 'xo_correction' — фактическая частота опорного XO в Гц, какой её + // считает драйвер. Правка = частотная калибровка всего чипа (RX и TX LO). + iio_device_attr_read_longlong: function(dev: Piio_device; attr: PAnsiChar; + out val: clonglong): cint; cdecl; + iio_device_attr_write_longlong: function(dev: Piio_device; attr: PAnsiChar; + val: clonglong): cint; cdecl; + // ---- Debug-атрибуты устройства (debugfs) ---- // Нужны для выбора антенного разъёма у AD936x в режиме 1R1T: // 'adi,1rx-1tx-mode-use-rx-num'/'-tx-num' (какой физический канал кормит тракт) @@ -185,6 +193,8 @@ begin iio_channel_attr_write := Sym('iio_channel_attr_write'); iio_channel_attr_write_bool := Sym('iio_channel_attr_write_bool'); iio_channel_attr_write_double := Sym('iio_channel_attr_write_double'); + iio_device_attr_read_longlong := Sym('iio_device_attr_read_longlong'); + iio_device_attr_write_longlong := Sym('iio_device_attr_write_longlong'); iio_device_debug_attr_read := Sym('iio_device_debug_attr_read'); iio_device_debug_attr_write := Sym('iio_device_debug_attr_write'); iio_device_debug_attr_write_longlong := Sym('iio_device_debug_attr_write_longlong'); diff --git a/PlutoBackend.pas b/PlutoBackend.pas index 040b949..681a24c 100644 --- a/PlutoBackend.pas +++ b/PlutoBackend.pas @@ -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 diff --git a/RadioBackend.pas b/RadioBackend.pas index 744a03b..57733cf 100644 --- a/RadioBackend.pas +++ b/RadioBackend.pas @@ -204,6 +204,9 @@ type // Нужно для абсолютной шкалы уровней — контроллер вычитает его из показаний. // HPSDR: усиления нет (есть аттенюатор), возвращает TELEMETRY_NONE. function RxGainNowDb: Double; virtual; + // Частотная калибровка опорника, ppm: «опора выше номинала на N ppm». + // HPSDR — масштаб частотного слова, AD936x — xo_correction в ad9361-phy. + procedure SetFreqCalPPM(P: Double); virtual; // ---- Свойства состояния (полиморфные через виртуальные геттеры) ---- property Connected: Boolean read GetConnected; @@ -310,6 +313,10 @@ begin Result := TELEMETRY_NONE; end; +procedure TRadioBackend.SetFreqCalPPM(P: Double); +begin +end; + procedure TRadioBackend.SetTxAtten(Db: Double); begin end; diff --git a/RadioController.pas b/RadioController.pas index 31da849..b91d18e 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -2295,7 +2295,9 @@ begin // QO-100 / split-LO: TX идёт на собственный uplink-LO (visible - TxLOOffset), // без LOError — дрейф LNB к передаче отношения не имеет. if FXvtrSettings.Entries[FCurrentXvtr].FullDuplexTx then + // TO Error: сам транспондер-offset держим стандартным, уход тракта — здесь. Result := VisibleHz - FXvtrSettings.Entries[FCurrentXvtr].TxLOOffset + + FXvtrSettings.Entries[FCurrentXvtr].TxLOError else // Обычный трансвертер: RX=TX LO (с калибровкой LOError) — как раньше. Result := VisibleHz @@ -3664,6 +3666,10 @@ begin // Санитизация: битый конфиг не должен дать деление на ноль/абсурдные ватты. FCal.PwrDetModel := EnsureRange(FCal.PwrDetModel, 0, 3); if FCal.AmpSensMVA <= 0 then FCal.AmpSensMVA := 120.0; + FCal.FreqCalPPM := EnsureRange(FCal.FreqCalPPM, -100.0, 100.0); + // Опорник: у HPSDR правим частотное слово, у AD936x — xo_correction. + // Ручные LOError/TO Error трансвертеров остаются сверху. + if Assigned(FNetwork) then FNetwork.SetFreqCalPPM(FCal.FreqCalPPM); // Офсет S-метра берётся из FCal на каждом чтении (SMeterCalFor) — применять // отдельно нечего: он зависит от диапазона/трансвертера, а не от движка. SaveGlobalSettings; // персист (гейт по FDevConnected внутри) diff --git a/Settings.pas b/Settings.pas index 74cb5fe..8b0ae1d 100644 --- a/Settings.pas +++ b/Settings.pas @@ -270,6 +270,10 @@ type // собственный у SDR). FullDuplexTx помечает слот как такой транспондер. FullDuplexTx: Boolean; // True — раздельный TX-LO (QO-100); авто display-duplex TxLOOffset: Double; // Hz, TX LO-offset (для QO-100 = транспондер-offset, ~8089.5 МГц) + // Hz, поправка к TX LO-offset (TO Error). Сам offset держим стандартным + // (8089.5 МГц у QO-100), а уход конкретного тракта правим здесь. + // Знак как у LOError: положительная — TX IF растёт. + TxLOError: Double; RXGain: Double; // dB, компенсация усиления RX тракта RXOnly: Boolean; // True — запрещает TX на этом трансверторе TXPower: Integer; // 0..100 % (drive level специально для XVTR) @@ -354,6 +358,12 @@ type // уровень отличается от показаний стрелки на пару дБ. DispBandDB: array[0..CFG_BAND_COUNT-1] of Double; DispXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double; + // Частотная калибровка опорника, ppm (Frequency Calibration в Thetis). + // Смысл: «опора выше номинала на N ppm» ⇒ до правки сигналы показываются + // ниже истинной частоты; положительное значение поднимает показания. + // Действует на всё железо разом; ручные LOError/TO Error трансвертеров + // остаются сверху — тракт тракту рознь. + FreqCalPPM: Double; end; TGlobalSettings = record @@ -922,6 +932,7 @@ begin for i := 0 to CFG_XVTR_COUNT-1 do G.Cal.DispXvtrDB[i] := EnsureRange(JD(GObj,'cal_disp_xvtr_'+IntToStr(i), 0.0), -30.0, 30.0); + G.Cal.FreqCalPPM := EnsureRange(JD(GObj,'cal_freq_ppm', 0.0), -100.0, 100.0); for i := 0 to CFG_BAND_COUNT-1 do begin @@ -1047,6 +1058,7 @@ begin JW(O,'cal_disp_band_'+IntToStr(i),G.Cal.DispBandDB[i]); for i := 0 to CFG_XVTR_COUNT-1 do JW(O,'cal_disp_xvtr_'+IntToStr(i),G.Cal.DispXvtrDB[i]); + JW(O,'cal_freq_ppm',G.Cal.FreqCalPPM); end; procedure TSettingsManager.SaveBand(const MAC: array of Byte; BandIdx: Integer; @@ -1598,6 +1610,7 @@ begin X.Entries[i].LOError := 0.0; X.Entries[i].FullDuplexTx := False; X.Entries[i].TxLOOffset := 0.0; + X.Entries[i].TxLOError := 0.0; X.Entries[i].RXGain := 0.0; X.Entries[i].RXOnly := False; X.Entries[i].TXPower := 100; @@ -1680,6 +1693,7 @@ begin X.Entries[i].LOError := JD(EObj, 'lo_err', X.Entries[i].LOError); X.Entries[i].FullDuplexTx := JB(EObj, 'full_dup', X.Entries[i].FullDuplexTx); X.Entries[i].TxLOOffset := JD(EObj, 'tx_lo_off', X.Entries[i].TxLOOffset); + X.Entries[i].TxLOError := JD(EObj, 'tx_lo_error', X.Entries[i].TxLOError); X.Entries[i].RXGain := JD(EObj, 'rx_gain', X.Entries[i].RXGain); X.Entries[i].RXOnly := JB(EObj, 'rx_only', X.Entries[i].RXOnly); X.Entries[i].TXPower := EnsureRange(JI(EObj, 'tx_pwr', X.Entries[i].TXPower), 0, 100); @@ -1781,6 +1795,7 @@ begin JW(EObj, 'lo_err', X.Entries[i].LOError); JW(EObj, 'full_dup', X.Entries[i].FullDuplexTx); JW(EObj, 'tx_lo_off', X.Entries[i].TxLOOffset); + JW(EObj, 'tx_lo_error', X.Entries[i].TxLOError); JW(EObj, 'rx_gain', X.Entries[i].RXGain); JW(EObj, 'rx_only', X.Entries[i].RXOnly); JW(EObj, 'tx_pwr', X.Entries[i].TXPower); diff --git a/SettingsForm.pas b/SettingsForm.pas index 747aa29..0a7f1cc 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -183,6 +183,8 @@ type FLblDispBandHdr: TLabel; FLblDispXvtrHdr: TLabel; FBtnDispFromSM: TFlatButton; // «= S-meter» — копия таблицы + FCalGrpFreq: TPanel; // Reference frequency + FEdFreqCalPPM: TFlatFloatSpinEdit; // калибровка опорника, ppm FCalIsPluto: Boolean; // активное железо — AD936x FCalXvtrOn: array[0..CFG_XVTR_COUNT-1] of Boolean; // слот трансвертера включён FCalXvtrName: array[0..CFG_XVTR_COUNT-1] of string; // подпись слота @@ -316,6 +318,7 @@ type FQoLnbLO: TFlatFloatSpinEdit; // МГц — LNB LO (= LOOffset, RX) FQoLoErr: TFlatFloatSpinEdit; // Гц — LO Error (= LOError, живая калибровка LNB / beacon-lock) FQoTxLO: TFlatFloatSpinEdit; // МГц — транспондер-offset (= TxLOOffset) + FQoTxLOErr: TFlatFloatSpinEdit; // Гц — поправка offset'а (TO Error) FQoBegin: TFlatFloatSpinEdit; // МГц — downlink begin FQoEnd: TFlatFloatSpinEdit; // МГц — downlink end FQoRXGain: TFlatFloatSpinEdit; // dB @@ -1946,6 +1949,17 @@ procedure TSettingsForm.BuildCalibrationTab; const GRP3_H0 = 200; // стартовая высота S-метра, уточняется в RelayoutCalTab + // Длинные подсказки не влезают в ширину группы — переводим на несколько + // строк (TLabel по умолчанию AutoSize=True и режется по краю). + procedure WrapLbl(L: TLabel; AW, AH: Integer); + begin + if L = nil then Exit; + L.AutoSize := False; + L.WordWrap := True; + L.Width := AW; + L.Height := AH; + end; + function MakeCalSpin(AParent: TWinControl; ALeft, ATop: Integer; AMin, AMax, AInc: Double; ADec: Integer; ADefault: Double): TFlatFloatSpinEdit; @@ -2021,7 +2035,8 @@ begin Lbl := MakeLbl(Grp, 'FWD/REV multiplier per band: 1.00 = no correction, >1 if meter under-reads ' + '(e.g. 88 W shown at real 100 W -> 1.14). SWR is not affected.', - CAL_PAD, CAL_R1 + CAL_ROW_H + 10, 650); + CAL_PAD, CAL_R1 + CAL_ROW_H + 8, 650); + WrapLbl(Lbl, 650, 34); for i := 0 to CFG_BAND_COUNT - 1 do begin @@ -2054,6 +2069,7 @@ begin Lbl := MakeLbl(Grp, 'Defaults 360 mV / 120 mV/A (ACS712-style, Orion MkII)', CAL_PAD + CAL_VA_LBL_W + 10 + CAL_ED_W + 14, CAL_R1 + CAL_ROW_H + 6, 320); + WrapLbl(Lbl, 320, 34); // ── S-meter ───────────────────────────────────────────────────────────── GTop := GTop + CAL_GRP2_H + 14; @@ -2063,7 +2079,8 @@ begin FLblSMBandHdr := MakeLbl(Grp, 'Offset (dB) added to S-meter readings — per band (+ = meter reads higher).', - CAL_PAD, 30, 650); + CAL_PAD, 30, 540); + WrapLbl(FLblSMBandHdr, 540, 18); for i := 0 to CFG_BAND_COUNT - 1 do begin @@ -2093,8 +2110,9 @@ begin Grp := FCalGrpDisp; FLblDispBandHdr := MakeLbl(Grp, - 'Offset (dB) added to spectrum/waterfall levels — per band (moves the whole ' - + 'trace incl. noise floor).', CAL_PAD, 30, 650); + 'Offset (dB) added to spectrum/waterfall — per band (moves the whole trace).', + CAL_PAD, 30, 540); + WrapLbl(FLblDispBandHdr, 540, 18); FBtnDispFromSM := TFlatButton.Create(Self); FBtnDispFromSM.Parent := Grp; @@ -2130,6 +2148,27 @@ begin FEdDispXvtr[i].Tag := i; end; + // ── Reference frequency ───────────────────────────────────────────────── + // Одна поправка опорника на всё железо (Frequency Calibration в Thetis): + // у openHPSDR правится частотное слово DDC/DUC, у AD936x — xo_correction. + // Ручные LO Error / TO Error трансвертеров остаются сверху: тракт тракту + // рознь, и где-то уход больше, где-то меньше. + FCalGrpFreq := MakeGroupPanel(FPageCalib, 'Reference frequency', + CAL_MARGIN, GTop, CAL_GRP_W, CAL_GRP2_H); + Grp := FCalGrpFreq; + + Lbl := MakeLbl(Grp, 'Oscillator error (ppm)', CAL_PAD, CAL_R1 + 6, CAL_VA_LBL_W); + FEdFreqCalPPM := MakeCalSpin(Grp, CAL_PAD + CAL_VA_LBL_W + 10, CAL_R1, + -100.0, 100.0, 0.01, 3, 0.0); + + Lbl := MakeLbl(Grp, + 'Positive value raises displayed frequencies. Measure against a known ' + + 'reference (beacon, WWV, GPSDO): if a 10 MHz signal reads 10 Hz low, ' + + 'that is +1.0 ppm. Applies to RX and TX on every band; per-transverter ' + + 'LO Error / TO Error still apply on top.', + CAL_PAD, CAL_R1 + CAL_ROW_H + 6, 650); + WrapLbl(Lbl, 650, 52); + RelayoutCalTab; // стартовая раскладка (HF-план, трансвертеры скрыты) FLoading := False; @@ -2146,7 +2185,16 @@ var begin // Гард: вкладка ещё строится (или LoadXvtrSettings пришёл раньше неё). if (FCalGrpDet = nil) or (FCalGrpVA = nil) or (FCalGrpSM = nil) - or (FCalGrpDisp = nil) or (FLblDispXvtrHdr = nil) then Exit; + or (FCalGrpDisp = nil) or (FLblDispXvtrHdr = nil) + or (FCalGrpFreq = nil) then Exit; + + // TScrollBox двигает детей при прокрутке, и SetBounds ниже задаёт координаты + // ОТНОСИТЕЛЬНО прокрученного клиента. Если страницу успели прокрутить (у + // openHPSDR она длиннее на две группы), после перекладки прокрутка + // схлопывается — и группы остаются смещёнными вниз ровно на прежний скролл. + // Поэтому сначала возвращаем страницу в ноль. + FPageCalib.VertScrollBar.Position := 0; + FPageCalib.HorzScrollBar.Position := 0; // ---- подписи/видимость строк Det. Cal. (своя сетка, уже с подсказкой) ---- for i := 0 to CFG_BAND_COUNT - 1 do @@ -2189,6 +2237,8 @@ begin FCalGrpSM.SetBounds(CAL_MARGIN, GTop, CAL_GRP_W, HSM); GTop := GTop + HSM + 14; FCalGrpDisp.SetBounds(CAL_MARGIN, GTop, CAL_GRP_W, HDisp); + GTop := GTop + HDisp + 14; + FCalGrpFreq.SetBounds(CAL_MARGIN, GTop, CAL_GRP_W, CAL_GRP2_H); end; function TSettingsForm.LayoutCalOffsets(const BandLbl: array of TLabel; @@ -2292,6 +2342,7 @@ begin if FEdSMXvtr[i] <> nil then FCalConfig.SMeterXvtrDB[i] := FEdSMXvtr[i].Value; if FEdDispXvtr[i] <> nil then FCalConfig.DispXvtrDB[i] := FEdDispXvtr[i].Value; end; + FCalConfig.FreqCalPPM := FEdFreqCalPPM.Value; FCalConfig.SupplyVoltCal := FEdSupplyVoltCal.Value; FCalConfig.AmpVoffMV := FEdAmpVoff.Value; FCalConfig.AmpSensMVA := FEdAmpSens.Value; @@ -2343,6 +2394,7 @@ begin if FEdSMXvtr[i] <> nil then FEdSMXvtr[i].Value := C.SMeterXvtrDB[i]; if FEdDispXvtr[i] <> nil then FEdDispXvtr[i].Value := C.DispXvtrDB[i]; end; + FEdFreqCalPPM.Value := EnsureRange(C.FreqCalPPM, -100.0, 100.0); FEdSupplyVoltCal.Value := C.SupplyVoltCal; FEdAmpVoff.Value := C.AmpVoffMV; FEdAmpSens.Value := C.AmpSensMVA; @@ -4355,7 +4407,7 @@ begin L.Font.Name := UI_FONT; L.Font.Size := 9; L.Font.Color := CLR_TEXTDIM; L.WordWrap := True; - Grp := MakeGroupPanel(FPageQO, 'Transponder', MARGIN, 100, 560, GRP_TITLE_H + 8 * 30 + 16); + Grp := MakeGroupPanel(FPageQO, 'Transponder', MARGIN, 100, 560, GRP_TITLE_H + 9 * 30 + 16); Y := GRP_TITLE_H + 8; FQoEn := TFlatCheckBox.Create(Self); @@ -4377,6 +4429,11 @@ begin FQoTxLO := QoFloat(Y, 0.0, 50000.0, 0.001, 6); FQoTxLO.Value := 8089.5; + // TO Error (Гц): сам транспондер-offset держим стандартным (8089.5 МГц), + // а уход конкретного тракта (TCXO uplink-SDR, наводка LNB) правим здесь. + Inc(Y, 30); QoLabel('TO Error (Hz)', Y); + FQoTxLOErr := QoFloat(Y, -999999.0, 999999.0, 1.0, 0); + Inc(Y, 30); QoLabel('Downlink begin (MHz)', Y); FQoBegin := QoFloat(Y, 0.0, 50000.0, 0.001, 3); FQoBegin.Value := 10489.5; @@ -4398,7 +4455,7 @@ begin FQoInfo := TLabel.Create(Self); FQoInfo.Parent := FPageQO; - FQoInfo.SetBounds(MARGIN, 100 + GRP_TITLE_H + 8 * 30 + 28, 700, 52); + FQoInfo.SetBounds(MARGIN, 100 + GRP_TITLE_H + 9 * 30 + 28, 700, 52); FQoInfo.Font.Color := CLR_TEXTDIM; FQoInfo.Font.Name := 'Courier New'; FQoInfo.Font.Size := 9; FQoInfo.WordWrap := True; finally @@ -4412,7 +4469,7 @@ begin if FQoInfo = nil then Exit; dlMid := (FQoBegin.Value + FQoEnd.Value) / 2.0; // МГц rxIf := dlMid - FQoLnbLO.Value; // RX IF в SDR - txF := dlMid - FQoTxLO.Value; // TX uplink + txF := dlMid - FQoTxLO.Value + FQoTxLOErr.Value / 1.0e6; // TX uplink + TO Error FQoInfo.Caption := Format( 'At downlink %.3f MHz: RX IF = %.3f MHz TX = %.3f MHz', [dlMid, rxIf, txF]); @@ -4427,6 +4484,7 @@ begin FXvtrConfig.Entries[QO100_SLOT].LOOffset := FQoLnbLO.Value * 1.0e6; FXvtrConfig.Entries[QO100_SLOT].LOError := FQoLoErr.Value; FXvtrConfig.Entries[QO100_SLOT].TxLOOffset := FQoTxLO.Value * 1.0e6; + FXvtrConfig.Entries[QO100_SLOT].TxLOError := FQoTxLOErr.Value; FXvtrConfig.Entries[QO100_SLOT].FreqBegin := FQoBegin.Value * 1.0e6; FXvtrConfig.Entries[QO100_SLOT].FreqEnd := FQoEnd.Value * 1.0e6; FXvtrConfig.Entries[QO100_SLOT].RXGain := FQoRXGain.Value; @@ -4442,6 +4500,7 @@ begin FQoLnbLO.Value := E.LOOffset / 1.0e6; FQoLoErr.Value := E.LOError; FQoTxLO.Value := E.TxLOOffset / 1.0e6; + FQoTxLOErr.Value := E.TxLOError; FQoBegin.Value := E.FreqBegin / 1.0e6; FQoEnd.Value := E.FreqEnd / 1.0e6; FQoRXGain.Value := E.RXGain;