Add VHF XVTR PA calibration; fix TX drive formula and stale FDriveLevel

PA calibration per band (HF and VHF):
- Add VHFBandCal[0..7] in TGlobalSettings (saved as vhf_cal_0..7 in JSON)
- Default HF cal updated to Thetis HERMES defaults (38.8-41.3 dB per band)
- Default VHF cal = 56.2 dB (Thetis HERMES XVTR default)
- SettingsForm: second group in PA tab "VHF Calibration (Transverters)" with
  8 spinboxes VHF0..VHF7; labels dynamically show XVTR slot ButtonText
  when LoadXvtrSettings is called (e.g. "VHF0 (2m):")

TX drive formula (CalcDriveByte) rewritten to match Thetis exactly:
  drive_byte = min(sqrt(10^((10*log10(pwr_W*1000) - Cal)/10) * 0.05) / 0.8, 1.0) * 1.02 * 255
  Cal = PA gain in dB (not a percentage); matches Thetis verified output
  (Cal=63.1 + slider=100% + PAMax=100W → drive_byte=16, verified against Thetis)
  Old formula Pos/100*Cal/100*255 was wrong: treated dB gain as linear percent

Fix stale FDriveLevel not updated on band/XVTR switch:
- Startup: FCurrentBand must be set before CalcDriveByte (was reversed)
- RestoreBand: recalculate FDriveLevel before ApplyVfoA network update
- ActivateXvtrBand: recalculate after FCurrentXvtr changes
- ApplyVfoA: recalculate when VFO crosses HF band boundary
- FreqDispBChanged: same for VFO-B band changes
- ApplyMOX(True): safety recalculate at PTT engage time

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 11:49:12 +03:00
co-authored by Claude Sonnet 4.6
parent 203d9d0f05
commit d823106366
4 changed files with 238 additions and 71 deletions
+68 -33
View File
@@ -209,6 +209,7 @@ type
FDriveLevel: Byte;
FPAMaxPower: Double;
FPABandCal: array[0..BAND_COUNT-1] of Double; // калибровка 38.8..100.0 на диапазон
FVHFBandCal: array[0..CFG_XVTR_COUNT-1] of Double; // калибровка VHF/XVTR слотов
FRunning: Boolean;
FTransmitting: Boolean;
FSplitTxB: Boolean;
@@ -658,6 +659,7 @@ type
procedure ApplyMOX(Active: Boolean);
procedure OnTXIQReady(const Buf: array of Double; Count: Integer);
procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double);
procedure OnVHFCalSettingsChange(const VHFCal: array of Double);
procedure OnTXSettingsChange(const T: TTXSettings);
procedure BtnAGCModeClick(Sender: TObject);
procedure TrkAGCChange(Sender: TObject);
@@ -947,6 +949,8 @@ begin
Result.PAMaxPower := FPAMaxPower;
for i := 0 to BAND_COUNT - 1 do
Result.PABandCal[i] := FPABandCal[i];
for i := 0 to CFG_XVTR_COUNT - 1 do
Result.VHFBandCal[i] := FVHFBandCal[i];
Result.ActiveVfo := FActiveVfo;
Result.NRMode := BtnNR.Tag;
Result.NBMode := BtnNB.Tag;
@@ -1118,6 +1122,8 @@ begin
FVfoB := B.VfoB;
FreqDispB.Frequency := Round(FVfoB);
// Пересчитываем drive byte для нового диапазона перед отправкой в сеть
FDriveLevel := CalcDriveByte;
// ApplyVfoA обновит сеть, FreqDisp, shift, перерисует
ApplyVfoA(Round(B.VfoA));
end;
@@ -1157,7 +1163,8 @@ begin
FSpecDrag := False;
FDriveLevel := 0;
FPAMaxPower := 100.0;
for i := 0 to BAND_COUNT - 1 do FPABandCal[i] := 100.0;
for i := 0 to BAND_COUNT - 1 do FPABandCal[i] := CFG_PA_BAND_CAL_DEFAULT[i];
for i := 0 to CFG_XVTR_COUNT - 1 do FVHFBandCal[i] := CFG_VHF_CAL_DEFAULT;
FRunning := False;
FTransmitting := False;
FSplitTxB := False;
@@ -3731,10 +3738,12 @@ begin
FPAMaxPower := G_Settings.PAMaxPower;
for i := 0 to BAND_COUNT - 1 do
FPABandCal[i] := G_Settings.PABandCal[i];
for i := 0 to CFG_XVTR_COUNT - 1 do
FVHFBandCal[i] := G_Settings.VHFBandCal[i];
// Slider position (0..100) and calibrated drive byte
TrkDrive.Position := EnsureRange(G_Settings.DriveLevel, 0, 100);
FDriveLevel := CalcDriveByte;
FCurrentBand := G_Settings.LastBand;
FDriveLevel := CalcDriveByte;
// SampleRate — глобальный, загружаем до RestoreBand
if G_Settings.SampleRate > 0 then
begin
@@ -4098,6 +4107,7 @@ begin
StyleButton(BtnBand[FCurrentBand], False);
FCurrentBand := BandIdx;
StyleButton(BtnBand[FCurrentBand], True);
FDriveLevel := CalcDriveByte;
end;
FSpecView.InvalidateRulerCache;
SyncSpecViewFreq;
@@ -4297,6 +4307,7 @@ begin
StyleButton(BtnBand[FCurrentBand], False);
FCurrentBand := BandIdx;
StyleButton(BtnBand[FCurrentBand], True);
FDriveLevel := CalcDriveByte;
end;
end;
@@ -5002,6 +5013,7 @@ begin
Exit;
WasTransmitting := FTransmitting;
FTransmitting := Active;
if Active then FDriveLevel := CalcDriveByte;
FDUCPendingCount := 0;
if FTransmitting then
begin
@@ -5907,31 +5919,33 @@ begin
end;
function TMainForm.CalcDriveByte: Byte;
// Thetis-compatible formula:
// drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 )
// where:
// target_volts = sqrt( 10^((target_dBm)/10) * 0.05 ) [E=sqrt(P*R), R=50, P in mW]
// target_dBm = 10*log10(pwr_W * 1000) - Cal
// pwr_W = Pos/100 * FPAMaxPower
// Cal = per-band PA gain in dB (HF: ~38.8-41.3, VHF default: ~56.2)
var
Cal: Double;
Pos: Integer;
XvtrActive: Boolean;
Cal: Double;
Pos: Integer;
XvtrActive: Boolean;
pwr_W: Double;
target_dBm: Double;
target_volts: Double;
audio_vol: Double;
begin
if (FCurrentBand >= 0) and (FCurrentBand < BAND_COUNT) and (FPABandCal[FCurrentBand] > 0.0) then
Cal := FPABandCal[FCurrentBand]
else
Cal := 100.0;
XvtrActive := (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
FXvtrSettings.Entries[FCurrentXvtr].Enabled;
// Pos — итоговый процент драйва 0..100 ДО per-band калибровки.
//
// Логика (как в Thetis):
// • Регулярный TX:
// HF → Pos = TrkDrive.Position
// XVTR → Pos = TrkDrive.Position × (XVTR.TXPower / 100)
// (TXPower работает как верхний предел драйва на трансверторе)
// • TUN (кнопка):
// HF → Pos = TUNLevel (TTXSettings.TUNLevel)
// XVTR, UseXVTRTunePower=False → Pos = TUNLevel (поведение как HF)
// XVTR, UseXVTRTunePower=True → Pos = XVTR.TXPower
// (галка ЗАМЕНЯЕТ TUNLevel на XVTR power — Thetis-семантика)
if XvtrActive then
Cal := FVHFBandCal[FCurrentXvtr]
else if (FCurrentBand >= 0) and (FCurrentBand < BAND_COUNT) then
Cal := FPABandCal[FCurrentBand]
else
Cal := 38.8;
// Pos — итоговый процент драйва 0..100
if FTuning then
begin
if XvtrActive and FXvtrSettings.UseXVTRTunePower then
@@ -5942,13 +5956,18 @@ begin
else
begin
if XvtrActive then
Pos := Round(TrkDrive.Position
* FXvtrSettings.Entries[FCurrentXvtr].TXPower / 100.0)
Pos := Round(TrkDrive.Position * FXvtrSettings.Entries[FCurrentXvtr].TXPower / 100.0)
else
Pos := TrkDrive.Position;
end;
Result := Round(Pos / 100.0 * Cal / 100.0 * 255.0);
if Pos <= 0 then begin Result := 0; Exit; end;
pwr_W := Pos / 100.0 * FPAMaxPower;
target_dBm := 10.0 * Log10(pwr_W * 1000.0) - Cal;
target_volts := Sqrt(Power(10.0, target_dBm * 0.1) * 0.05);
audio_vol := Min(target_volts / 0.8, 1.0);
Result := Min(Round(audio_vol * 1.02 * 255.0), 255);
end;
procedure TMainForm.OnTXIQReady(const Buf: array of Double; Count: Integer);
@@ -6002,6 +6021,21 @@ begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
end;
procedure TMainForm.OnVHFCalSettingsChange(const VHFCal: array of Double);
var i: Integer;
begin
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
if i <= High(VHFCal) then
FVHFBandCal[i] := VHFCal[i]
else
FVHFBandCal[i] := CFG_VHF_CAL_DEFAULT;
end;
FDriveLevel := CalcDriveByte;
if FNetwork.Connected and FNetwork.Running then
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True);
end;
procedure TMainForm.OnTXSettingsChange(const T: TTXSettings);
// Любая правка в Settings→Transmit летит сюда: обновляем FTXSettings,
// перешиваем WDSP TX-цепь и (если поменялись mic-биты) перепосылаем DUC Specific.
@@ -6097,11 +6131,12 @@ begin
end;
// Передаёт XVTR-режим в сетевой слой. Вызывается при смене XVTR или его
// настроек. Управляет XVTR Enable bit, DDC IN routing, T/R relay suppression.
// настроек. Управляет XVTR Enable bit, T/R relay suppression.
// XVTR DDC In (Alex bits 8+11) управляется через галочку XVTR в Antenna/Alex
// tab для IF-диапазона (Alex.RxOnly[IF_band]=3) — аналогично Thetis.
procedure TMainForm.ApplyXvtrToNetwork;
var
En: Boolean;
UseDDCIn: Boolean;
DisablePA: Boolean;
RxAnt: Byte;
begin
@@ -6109,16 +6144,15 @@ begin
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
begin
En := True;
UseDDCIn := True;
DisablePA := FXvtrSettings.Entries[FCurrentXvtr].DisablePA;
RxAnt := FXvtrSettings.Entries[FCurrentXvtr].RXAntenna;
end
else
begin
En := False; UseDDCIn := False; DisablePA := False; RxAnt := 0;
En := False; DisablePA := False; RxAnt := 0;
end;
if FNetwork.Connected then
FNetwork.SetXvtrMode(En, UseDDCIn, DisablePA, RxAnt);
FNetwork.SetXvtrMode(En, DisablePA, RxAnt);
end;
// Перестраивает динамические XVTR-кнопки в PanelBands. Удаляет старые,
@@ -6289,9 +6323,8 @@ begin
FCenterFreq := Vis;
FreqDispA.Frequency := Round(FVfoA);
if FWDSPReady then FDSPEngine.SetShift(0.0);
// Применяем драйв (если есть кастомный TX power для этого XVTR)
// Сделано на стороне ActiveTXFreqHz / TX path — здесь только обновляем дисплей.
// Отправляем в сеть с трансляцией
// Пересчитываем drive byte для нового XVTR слота и отправляем в сеть
FDriveLevel := CalcDriveByte;
if FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
@@ -6683,6 +6716,7 @@ begin
SF.OnVisibilityChange := ApplyVisibility;
SF.OnFPSChange := ApplyFPS;
SF.OnPAChange := OnPASettingsChange;
SF.OnVHFCalChange := OnVHFCalSettingsChange;
SF.OnCATChange := OnCATSettingsChange;
SF.OnThemeChange := SetLightTheme;
SF.OnFreqMhzDigitsChange := ApplyFreqMhzDigits;
@@ -6699,6 +6733,7 @@ begin
IfThen(FNetwork.Connected, FNetwork.Device.BoardType, FPendingBoardType));
SF.LoadXvtrSettings(FXvtrSettings);
SF.LoadPASettings(FPAMaxPower, FPABandCal);
SF.LoadVHFCalSettings(FVHFBandCal);
SF.LoadCATSettings(
FCATLastGlobal.CATSerialEnabled,
FCATLastGlobal.CATSerialPort,