mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
feat(cw): телеграф — FPGA-кейер, pitch, сайдтон, передача текста
CW не работал ни на приём, ни на передачу: фильтр стоял симметрично вокруг нуля (тона не было), понятия pitch не существовало, MOX в CWL отправлял в эфир голос 2.9 кГц (в WDSP TXA_CWL — ветка SSB), байт CW-опций DUC всегда был нулевым, поэтому кейер, сайдтон и break-in в прошивке молчали. Pitch сделан ОДНИМ сдвигом в движке. Кромки фильтра везде остаются относительно VFO (у CW — симметрично нулю), а гетеродин и полосу двигают три метода WDSPEngine: CWLOOffset / PushShift / PushPassband. Прямых вызовов SetRXAShiftFreq и RXASetPassband в проекте больше нет. Так таблица фильтров не перестраивается при смене pitch (в отличие от Thetis), полоска на спектре и маркер VFO верны без правок в UI, а слайсы получают свой сдвиг по своему режиму. Правила эфира: - голосовой TXA в CW не запускается вообще (гард по режиму), MOX = только PTT; несущую даёт кейер, бит CWX или тон TUN; - байт 5 собирается из настроек и перепосылается на каждой смене режима и TX-слайса — вне CW он обязан быть нулевым, иначе прошивка поднимет PTT на замыкание ключа посреди SSB; - TUN в CW: тон = pitch и встречный сдвиг DUC, несущая встаёт ровно на VFO; - приёмник на передаче в CW не глушится, иначе умолкает программный сайдтон и эфир между посылками. Программная передача текста (CWMorse): поток с абсолютными дедлайнами дёргает бит CWX в High Priority, элементы рисует прошивка. Касание манипулятора или снятие MOX обрывают передачу. Память сообщений — окно CW Messages и F1..F8 в главном окне. Оживлены CAT KS/KY/ZZKM/ZZKS/ZZKY. Настройки — вкладка Transmit, подвкладка Hardware, перед блоком FM/CTCSS. Программный сайдтон точен для передачи текста и прямого ключа; при иамбике таймингом владеет FPGA, поэтому там верен только аппаратный тон. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+100
-12
@@ -472,6 +472,7 @@ type
|
||||
FLastSpectrumW: Integer; // последняя известная ширина спектра (пикс)
|
||||
FDisplayResLimit: Integer; // потолок точек анализатора (настройка display_pixels)
|
||||
FShiftHz: Double; // NCO shift для CTUN
|
||||
FCWPitch: Integer; // Гц, тон телеграфа (см. CWLOOffset)
|
||||
FNRMode: Integer;
|
||||
FNBMode: Integer;
|
||||
FSNBEnabled: Boolean;
|
||||
@@ -505,6 +506,14 @@ type
|
||||
// Порт Thetis UpdateTXLowHighFilterForMode: единственный конвертер знака,
|
||||
// через который обязан идти ЛЮБОЙ пуш TX-bandpass, иначе LSB уходит в USB.
|
||||
procedure TXSignedEdges(Mode: Integer; out Lo, Hi: Double);
|
||||
// --- CW pitch -----------------------------------------------------------
|
||||
// Смещение гетеродина демодулятора относительно VFO в телеграфе: станция,
|
||||
// стоящая ТОЧНО на частоте VFO, обязана звучать тоном CWPitch, а не нулём.
|
||||
// CWL слушаем ниже (гетеродин выше на pitch), CWU — наоборот. Всё остальное
|
||||
// в проекте (кромки фильтра, полоска на спектре, маркер VFO) остаётся в
|
||||
// координатах «относительно VFO»: конвертер ровно один — эти три метода.
|
||||
procedure PushShift(Chan: Integer; ShiftHz: Double; Mode: Integer);
|
||||
procedure PushPassband(Chan, Lo, Hi, Mode, Rate: Integer);
|
||||
// Mode-dependent TX overlay. FM RAW uses the flat WFM modulator and
|
||||
// bypasses every speech processor without overwriting the saved TX setup.
|
||||
procedure ApplyTXModeSettings(Mode: Integer);
|
||||
@@ -641,6 +650,14 @@ type
|
||||
procedure SetSpectrumWidth(W: Integer); // обновляет FLastSpectrumW для корректных AGC линий
|
||||
procedure SetDisplayResLimit(V: Integer); // потолок точек анализатора (1024/2048/4096)
|
||||
procedure SetShift(ShiftHz: Double); // CTUN NCO сдвиг
|
||||
// Тон телеграфа. Меняет положение гетеродина и полосы во ВСЕХ CW-каналах —
|
||||
// применяется немедленно (главный + слайсы), персист делает контроллер.
|
||||
procedure SetCWPitch(Hz: Integer);
|
||||
function CWPitch: Integer;
|
||||
// Смещение гетеродина демодулятора относительно VFO в телеграфе (см.
|
||||
// комментарий у PushShift). Публично: тем же сдвигом контроллер двигает
|
||||
// DUC при настройке TUN, чтобы несущая легла ровно на VFO.
|
||||
function CWLOOffset(Mode: Integer): Double;
|
||||
procedure SetNRMode(Mode: Integer);
|
||||
procedure SetNR(Enable: Boolean);
|
||||
procedure SetNBMode(Mode: Integer);
|
||||
@@ -1231,6 +1248,35 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
function TWDSPEngine.CWLOOffset(Mode: Integer): Double;
|
||||
// Знак — как в Thetis (console.cs:31757 rx_freq += pitch для CWL) и pihpsdr
|
||||
// (new_protocol.c:773 rxFrequency -= pitch для CWU).
|
||||
begin
|
||||
case Mode of
|
||||
MODE_CWL: Result := FCWPitch;
|
||||
MODE_CWU: Result := -FCWPitch;
|
||||
else Result := 0.0;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.PushShift(Chan: Integer; ShiftHz: Double; Mode: Integer);
|
||||
// ShiftHz приходит в координатах «VFO минус центр DDC»; в эфир гетеродин
|
||||
// уезжает ещё на CWLOOffset, чтобы принимаемая посылка легла на pitch.
|
||||
begin
|
||||
SetRXAShiftFreq(Chan, ShiftHz + CWLOOffset(Mode));
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.PushPassband(Chan, Lo, Hi, Mode, Rate: Integer);
|
||||
// Кромки хранятся относительно VFO (у CW — симметрично нулю: «500 Гц вокруг
|
||||
// корреспондента»). В baseband демодулятора они уезжают навстречу гетеродину,
|
||||
// т.е. у CWU получается pitch±bw/2 — ровно таблица Thetis (console.cs:5338).
|
||||
var Off: Integer;
|
||||
begin
|
||||
Off := Round(CWLOOffset(Mode));
|
||||
RXASetPassband(Chan, ClampToNyquist(Lo - Off, Rate),
|
||||
ClampToNyquist(Hi - Off, Rate));
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.ApplyTXModeSettings(Mode: Integer);
|
||||
begin
|
||||
if not FInitialized then Exit;
|
||||
@@ -1323,6 +1369,7 @@ begin
|
||||
FFilterHigh := 2400;
|
||||
FAGCMode := agcMedium;
|
||||
FShiftHz := 0.0;
|
||||
FCWPitch := 600;
|
||||
FAGCTop := -90.0;
|
||||
FVolume := 0.7;
|
||||
FSMeter := -130.0;
|
||||
@@ -1932,8 +1979,8 @@ begin
|
||||
SetRXABandpassWindow(RXA_CHAN, 1);
|
||||
SetRXAMode(RXA_CHAN, ModeToWDSP(FMode));
|
||||
SetRXAShiftRun(RXA_CHAN, 1);
|
||||
SetRXAShiftFreq(RXA_CHAN, FShiftHz);
|
||||
RXASetPassband(RXA_CHAN, FFilterLow, FFilterHigh);
|
||||
PushShift(RXA_CHAN, FShiftHz, FMode);
|
||||
PushPassband(RXA_CHAN, FFilterLow, FFilterHigh, FMode, FRXDSPRate);
|
||||
SetAGC(FAGCMode, 50.0);
|
||||
SetRXAPanelGain1(RXA_CHAN, FVolume);
|
||||
SetRXAPanelSelect(RXA_CHAN, 3);
|
||||
@@ -2457,9 +2504,9 @@ begin
|
||||
SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode));
|
||||
ApplyRXModeSettings(Chan, FSlices[Idx].Mode);
|
||||
SetRXAShiftRun(Chan, 1);
|
||||
SetRXAShiftFreq(Chan, FSlices[Idx].ShiftHz);
|
||||
RXASetPassband(Chan, ClampToNyquist(FSlices[Idx].FilterLo, DSPRate),
|
||||
ClampToNyquist(FSlices[Idx].FilterHi, DSPRate));
|
||||
PushShift(Chan, FSlices[Idx].ShiftHz, FSlices[Idx].Mode);
|
||||
PushPassband(Chan, FSlices[Idx].FilterLo, FSlices[Idx].FilterHi,
|
||||
FSlices[Idx].Mode, DSPRate);
|
||||
ApplyAGCToChan(Chan, FSlices[Idx].AGCMode, 50.0);
|
||||
// NR/NB/SNB/ANF из сохранённого состояния слайса.
|
||||
ApplyNRToChan(Chan, FSlices[Idx].NRMode);
|
||||
@@ -2733,7 +2780,8 @@ begin
|
||||
try
|
||||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||||
FSlices[idx].ShiftHz := ShiftHz;
|
||||
if FSlices[idx].Opened then SetRXAShiftFreq(FSlices[idx].Chan, ShiftHz);
|
||||
if FSlices[idx].Opened then
|
||||
PushShift(FSlices[idx].Chan, ShiftHz, FSlices[idx].Mode);
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
@@ -2768,6 +2816,14 @@ begin
|
||||
// шумодава в не-FM режимах нет); вернулись в FM — восстановить выбор.
|
||||
ApplySliceFMSQ(idx);
|
||||
end;
|
||||
// Приход/уход телеграфа меняет CWLOOffset — гетеродин и полосу надо
|
||||
// переложить под новый режим, даже если канал не пересоздавали.
|
||||
if FSlices[idx].Opened then
|
||||
begin
|
||||
PushShift(FSlices[idx].Chan, FSlices[idx].ShiftHz, Mode);
|
||||
PushPassband(FSlices[idx].Chan, FSlices[idx].FilterLo, FSlices[idx].FilterHi,
|
||||
Mode, SliceDSPRateFor(Mode, SliceInRate(idx)));
|
||||
end;
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
@@ -2784,8 +2840,7 @@ begin
|
||||
if FSlices[idx].Opened then
|
||||
begin
|
||||
Rate := SliceDSPRateFor(FSlices[idx].Mode, SliceInRate(idx));
|
||||
RXASetPassband(FSlices[idx].Chan, ClampToNyquist(Low, Rate),
|
||||
ClampToNyquist(High, Rate));
|
||||
PushPassband(FSlices[idx].Chan, Low, High, FSlices[idx].Mode, Rate);
|
||||
end;
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
@@ -2965,6 +3020,9 @@ begin
|
||||
ApplyDSPRate(Mode);
|
||||
SetRXAMode(RXA_CHAN, ModeToWDSP(Mode));
|
||||
ApplyRXModeSettings(RXA_CHAN, Mode);
|
||||
// Вход/выход из телеграфа меняет CWLOOffset: гетеродин надо переложить сразу
|
||||
// (кромки приедут из UI следом, но сдвиг оттуда не приходит).
|
||||
PushShift(RXA_CHAN, FShiftHz, Mode);
|
||||
ApplyTXMode(FTXMode);
|
||||
// НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI
|
||||
end;
|
||||
@@ -2978,9 +3036,7 @@ begin
|
||||
// до ±100 кГц, а dsp_rate зависит от SampleRate. Клампим только то, что
|
||||
// уходит в WDSP — запрошенные значения храним как есть, иначе понижение
|
||||
// SampleRate необратимо «съело» бы полосу.
|
||||
// RXASetPassband — правильный unified API, пересчитывает фильтр целиком
|
||||
RXASetPassband(RXA_CHAN, ClampToNyquist(Low, FRXDSPRate),
|
||||
ClampToNyquist(High, FRXDSPRate));
|
||||
PushPassband(RXA_CHAN, Low, High, FMode, FRXDSPRate);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetAGC(Mode: TWDSPAGCMode; FixedGain: Double);
|
||||
@@ -2999,7 +3055,39 @@ procedure TWDSPEngine.SetShift(ShiftHz: Double);
|
||||
begin
|
||||
FShiftHz := ShiftHz;
|
||||
if not FInitialized then Exit;
|
||||
SetRXAShiftFreq(RXA_CHAN, ShiftHz);
|
||||
PushShift(RXA_CHAN, ShiftHz, FMode);
|
||||
end;
|
||||
|
||||
function TWDSPEngine.CWPitch: Integer;
|
||||
begin
|
||||
Result := FCWPitch;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetCWPitch(Hz: Integer);
|
||||
var i, Rate: Integer;
|
||||
begin
|
||||
if Hz = FCWPitch then Exit;
|
||||
FCWPitch := Hz;
|
||||
if not FInitialized then Exit;
|
||||
if FMode in [MODE_CWL, MODE_CWU] then
|
||||
begin
|
||||
PushShift(RXA_CHAN, FShiftHz, FMode);
|
||||
PushPassband(RXA_CHAN, FFilterLow, FFilterHigh, FMode, FRXDSPRate);
|
||||
end;
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
for i := 0 to MAX_SLICES - 1 do
|
||||
if FSlices[i].Active and FSlices[i].Opened and
|
||||
(FSlices[i].Mode in [MODE_CWL, MODE_CWU]) then
|
||||
begin
|
||||
Rate := SliceDSPRateFor(FSlices[i].Mode, SliceInRate(i));
|
||||
PushShift(FSlices[i].Chan, FSlices[i].ShiftHz, FSlices[i].Mode);
|
||||
PushPassband(FSlices[i].Chan, FSlices[i].FilterLo, FSlices[i].FilterHi,
|
||||
FSlices[i].Mode, Rate);
|
||||
end;
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetAGCTop(TopDBm: Double);
|
||||
|
||||
Reference in New Issue
Block a user