chore: refine project documentation

This commit is contained in:
2026-08-25 22:44:36 +03:00
parent 8237f03930
commit 98d90e1e5a
45 changed files with 457 additions and 516 deletions
+40 -44
View File
@@ -196,7 +196,7 @@ const
TX_MIC_RING = 8192;
// TX/DUC sample rate. Фиксирован независимо от FSampleRate (RX).
// Thetis/piHPSDR держат TX rate постоянным (192k для Hermes/Saturn DUC0),
// TX rate постоянен: 192k для Hermes/Saturn DUC0,
// меняя только RX rate. Это гарантирует, что DUC IQ всегда подаётся в том
// темпе, который ожидает железо (DUC0RateHi/Lo в DUC Specific = 192).
TX_SAMPLE_RATE = 192000;
@@ -323,7 +323,7 @@ type
// Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает)
FSpecBuf: array of Double;
// Отдельный мелкий feed для display-анализатора, как в Thetis spec_blocksize.
// Отдельный небольшой feed для display-анализатора.
FDispBuf: array of Double;
FDispPos: Integer;
// То же самое для TX-analyzer: TX_DISP_ID получает сэмплы чанками
@@ -372,7 +372,7 @@ type
FWfAvgMode: Integer;
FWfAvgTimeMS: Double;
// Zoom/pan анализатора (span-clip в SetAnalyzer). Конвенция WDSP/Thetis:
// Zoom/pan анализатора (span-clip в SetAnalyzer). Конвенция WDSP:
// FZoomFactor 0.0..1.0, где 0.0 = весь span (без зума), 1.0 = максимум.
// FPanSlider 0..1 (положение окна по span). FViewLowHz/FViewHighHz —
// границы видимого окна в Гц относительно DDC-центра (low отрицательный),
@@ -531,7 +531,7 @@ type
FFilterLow: Integer;
FFilterHigh: Integer;
FAGCMode: TWDSPAGCMode;
FAGCTop: Double; // gain (dB) = agc_gain в piHPSDR
FAGCTop: Double; // AGC gain in dB
FAGCSlope: Integer; // наклон АРУ в дБ (default 0)
FAGCHangThreshold: Integer; // порог hang 0..100
FAGCHangLevel: Double; // читается из WDSP (GetRXAAGCHangLevel)
@@ -580,7 +580,7 @@ type
// главного RXA и для слайсов — иначе дублирование по трём местам.
procedure ApplyRXModeSettings(Chan, Mode: Integer);
// Магнитуда TX-полосы (FTXFilter*) → знаковые края bp0 по боковой режима.
// Порт Thetis UpdateTXLowHighFilterForMode: единственный конвертер знака,
// Единственный конвертер знака краёв TX-фильтра для выбранного режима,
// через который обязан идти ЛЮБОЙ пуш TX-bandpass, иначе LSB уходит в USB.
procedure TXSignedEdges(Mode: Integer; out Lo, Hi: Double);
// --- CW pitch -----------------------------------------------------------
@@ -783,7 +783,7 @@ type
procedure SetTXTone(Enabled: Boolean; FreqHz, Mag: Double);
// 2TON: PostGen two-tone (IMD-тест / калибровка PureSignal).
// Тона Freq1/Freq2 Гц (для LSB/CWL/DIGL знак инвертируется), амплитуды
// по 0.49 (сумма чуть ниже полной шкалы, как Thetis/pihpsdr).
// по 0.49, чтобы сумма оставалась чуть ниже полной шкалы.
procedure SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double);
// Protocol 2: компенсирующий FIR перед FPGA CIC-интерполятором TX.
// Для openHPSDR P2 всегда включён; для остальных бэкендов выключен.
@@ -795,7 +795,7 @@ type
procedure SetPureSignalArmed(Armed: Boolean);
// Feedback-пакет синхронизированного DDC0 (interleaved: пара DDC0 =
// RX-feedback, пара DDC1 = TX DAC, 24-bit BE). Вызывается из СЕТЕВОГО
// потока (как Thetis/pihpsdr — pscc потокобезопасен относительно
// потока; pscc потокобезопасен относительно
// fexchange0 TX-потока). TotalPairs — всего IQ-пар в пакете (оба потока).
procedure PushPSFeedback(const Buf: array of Byte; DataOffset: Integer;
TotalPairs: Integer);
@@ -958,7 +958,7 @@ implementation
// ===========================================================================
// TX DSP поток — читает mic ring buffer, вызывает fexchange0(TXA_CHAN),
// сигнализирует о готовых IQ данных через FOnTXIQ callback.
// Аналог tx_thread в piHPSDR/transmitter.c
// Поток обработки TX-аудио и формирования IQ.
// ===========================================================================
type
TDisplayThread = class(TThread)
@@ -981,7 +981,7 @@ type
function UsePanNormOneHz(Detector: Integer): Integer;
begin
// В Thetis нормализация к 1 Гц включается только для panadapter
// Нормализация к 1 Гц включается только для panadapter
// и только для detector modes 2/3/4.
if Detector in [2, 3, 4] then
Result := 1
@@ -1029,8 +1029,7 @@ begin
end;
procedure TTXDSPThread.Execute;
// Аналог tx_thread в piHPSDR/transmitter.c:
// Тикает с периодом одного mic-буфера (FAudioBufSize/FAudioRate).
// Поток работает с периодом одного mic-буфера (FAudioBufSize/FAudioRate).
// Если mic-данные накопились — обрабатывает их; если нет — обрабатывает тишину.
// Это обеспечивает непрерывный поток DUC IQ к железу и TX-сигнал на спектре/водопаде.
var
@@ -1052,7 +1051,7 @@ begin
Dec(AccUs, PeriodMs * 1000);
// Ждём сигнала от mic-данных ИЛИ истечения периода —
// в piHPSDR timing идёт от receive thread; здесь — от таймаута
// Здесь timing задаётся таймаутом, синхронизированным с размером буфера.
RTLEventWaitFor(FEngine.FTXMicSem, PeriodMs);
if Terminated then Break;
if not FEngine.FTXActive then Continue;
@@ -1085,7 +1084,7 @@ begin
if Terminated then Break;
// Silence-инъекция УБРАНА (модель dl1ycf pihpsdr). Раньше при пустом ring
// Silence-инъекция УБРАНА. Раньше при пустом ring
// (>6 тиков) сюда впрыскивался блок тишины «чтобы DUC IQ не прерывался».
// Но это фабрикация данных не на радиоклоке: для TUN такой «silence» несёт
// PostGen-тон, и лишний блок ломает темп DUC IQ (боковые/тр-р-р); плюс он
@@ -1098,9 +1097,8 @@ begin
end;
// ===========================================================================
// DSP поток обрабатывает IQ пакеты из очереди
// Сетевой поток только кладёт пакеты, этот поток занимается DSP
// Аналог iq_thread в piHPSDR/new_protocol.c
// DSP поток обрабатывает IQ-пакеты из очереди.
// Сетевой поток только кладёт пакеты, этот поток занимается DSP.
// ===========================================================================
type
TDSPThread = class(TThread)
@@ -1125,7 +1123,7 @@ var
begin
while not Terminated do
begin
// Ждём сигнала от сетевого потока (как sem_wait в piHPSDR)
// Ждём сигнала от сетевого потока.
RTLEventWaitFor(FEngine.FQueueSem, 100);
if Terminated then Break;
@@ -1339,7 +1337,7 @@ begin
end;
procedure TWDSPEngine.TXSignedEdges(Mode: Integer; out Lo, Hi: Double);
// Порт Thetis UpdateTXLowHighFilterForMode. Вход — магнитуда TX-аудио-полосы
// Вход — магнитуда TX-аудио-полосы
// (FTXFilterLow/High, всегда положительная); выход — знаковые края bp0 под
// боковую режима. Нижняя боковая зеркалит полосу в отрицательные частоты.
var
@@ -1376,8 +1374,7 @@ begin
end;
function TWDSPEngine.CWLOOffset(Mode: Integer): Double;
// Знак — как в Thetis (console.cs:31757 rx_freq += pitch для CWL) и pihpsdr
// (new_protocol.c:773 rxFrequency -= pitch для CWU).
// Для CWL смещение положительное, для CWU — отрицательное.
begin
case Mode of
MODE_CWL: Result := FCWPitch;
@@ -1396,7 +1393,7 @@ end;
procedure TWDSPEngine.PushPassband(Chan, Lo, Hi, Mode, Rate: Integer);
// Кромки хранятся относительно VFO (у CW — симметрично нулю: «500 Гц вокруг
// корреспондента»). В baseband демодулятора они уезжают навстречу гетеродину,
// т.е. у CWU получается pitch±bw/2 — ровно таблица Thetis (console.cs:5338).
// т.е. у CWU получается pitch±bw/2.
var Off: Integer;
begin
Off := Round(CWLOOffset(Mode));
@@ -1428,7 +1425,7 @@ begin
begin
// Restore the user's normal voice/data chain after leaving FM RAW.
// bp1 (aux bandpass) НЕ трогаем вручную — им управляет компрессор через
// TXASetupBPFilters (как в Thetis). bp0 всегда активен сам.
// bp0 всегда активен сам; здесь обновляем остальную цепочку TX.
ApplyTXChainSettings;
if Mode = MODE_WFM then ApplyTXWFMSettings;
end;
@@ -1478,9 +1475,9 @@ begin
inherited Create;
FSampleRate := SampleRate;
FAudioRate := AudioRate;
// BufSize = dsp_size @ AudioRate (внутренний DSP буфер, piHPSDR buffer_size = 1024)
// BufSize = dsp_size @ AudioRate; внутренний DSP buffer_size = 1024.
// FBufSize = in_size @ SampleRate = BufSize * (SampleRate/AudioRate)
// piHPSDR: in_size = buffer_size * sample_rate/48000 = 1024*4 = 4096 @ 192kHz
// in_size = buffer_size * sample_rate/48000 = 1024*4 = 4096 @ 192kHz.
FAudioBufSize := BufSize; // dsp_size = out_size @ AudioRate
FBufSize := BufSize * SampleRate div AudioRate; // in_size @ SampleRate = 4096
// TX rate фиксирован — не зависит от RX. FTXOutBufSize = out_size TXA @ FTXSampleRate.
@@ -1579,7 +1576,7 @@ begin
FTXToneOn := False;
FTXTTOn := False;
FKeepRXDuringTX := False;
// PureSignal — дефолты как Thetis PSForm / clsHardwareSpecific (P2)
// PureSignal — значения по умолчанию для протокола 2.
FPSArmed := False;
FPSMoxDelay := 0.2; // с — задержка после MOX до сбора данных
FPSLoopDelay := 0.0; // с — пауза между итерациями калибровки
@@ -2210,7 +2207,7 @@ begin
end;
try
// Анализатор ПЕРВЫМ — до OpenChannel (порядок как в piHPSDR)
// Анализатор открывается перед DSP-каналом.
OpenAnalyzer;
if not FAnalyzerOpen then
begin
@@ -2237,7 +2234,7 @@ begin
0,
0.010, 0.010, 0.010, 0.010,
1 // bfo=1: fexchange0 БЛОКИРУЕТСЯ до готовности выходного блока (как
// pihpsdr). bfo=0 давал async-чтение кольца r2 со сдвигом outidx →
// bfo=0 давал async-чтение кольца r2 со сдвигом outidx →
// изредка фазовый разрыв PostGen-тона (всплеск на водопаде + щелчок).
);
@@ -2277,7 +2274,7 @@ begin
Result := True;
// Запускаем DSP поток после успешной инициализации WDSP
// Аналог iq_thread_id = g_thread_new("iq thread", ...) в piHPSDR
// Запускаем отдельный поток обработки IQ.
FDSPThread := TDSPThread.Create(Self);
FDSPThread.Priority := tpHighest; // DSP критический поток
@@ -2531,7 +2528,7 @@ begin
Item^.PanId := 0;
FQueueHead := NextHead;
RTLEventSetEvent(FQueueSem); // будим DSP поток (как sem_post в piHPSDR)
RTLEventSetEvent(FQueueSem); // будим DSP-поток
end;
procedure TWDSPEngine.PushPanIQ(PanId: Integer; const Buf: array of Byte;
@@ -2815,7 +2812,7 @@ begin
FSMeter := GetRXAMeter(RXA_CHAN, RXA_S_AV);
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1).
// При TX RX-аудио глушим (как в Thetis/piHPSDR при не-duplex MOX),
// При TX RX-аудио глушим в режиме non-duplex MOX,
// иначе оператор слышит свой эфир через RX-цепь и получает feedback.
// Дополнительный гейт FPostTXMuteUntil закрывает «хвост» TX leakage
// на Windows (radio FPGA TX-buffer + PA slew-down). Cheap-path: проверка
@@ -2824,7 +2821,7 @@ begin
FPostTXMuteUntil := 0;
// Full-duplex self-monitor (QO-100): при FMonitorRXDuringTX не глушим RX-аудио
// на TX — слышим свой downlink со спутника. Иначе (по умолчанию) глушим, как
// Thetis/piHPSDR при не-duplex MOX. FPostTXMuteUntil-хвост уважаем в любом случае.
// FPostTXMuteUntil-хвост уважаем и при non-duplex MOX.
if (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX))
and (FPostTXMuteUntil = 0) then
begin
@@ -3523,7 +3520,7 @@ begin
FAGCMode := Mode;
if not FInitialized then Exit;
// Точно по piHPSDR receiver.c: set_agc()
// Настройка параметров AGC приёмного канала.
ApplyAGCToChan(RXA_CHAN, Mode, FixedGain);
// Читаем обратно hang level и thresh для линии на спектре
@@ -3976,7 +3973,7 @@ end;
procedure TWDSPEngine.SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double);
// 2TON: двухтональник PostGen (mode 1) для IMD-теста и проверки PureSignal.
// Амплитуды 0.49+0.49 сумма чуть ниже полной шкалы (как Thetis/pihpsdr).
// Амплитуды 0.49+0.49 дают сумму чуть ниже полной шкалы.
var
F1, F2: Double;
begin
@@ -4026,9 +4023,8 @@ end;
procedure TWDSPEngine.ProcessTXBlock;
// Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов
// через WDSP TXA, выдаёт FTXOutBufSize IQ пар @ FTXSampleRate в FOnTXIQ callback.
// Аналог tx_full_buffer() в piHPSDR/transmitter.c:
// если mic ring buffer содержит достаточно данных — берём их,
// иначе обрабатываем тишину (нули), как piHPSDR при mic_sample=0.0.
// Если mic ring buffer содержит достаточно данных — берём их,
// иначе обрабатываем тишину (нули).
var
i, tail: Integer;
Err: Integer;
@@ -4055,7 +4051,7 @@ begin
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
end else
begin
// Нет mic-данных — тишина (аналог piHPSDR: tx->mic_input_buffer заполняется нулями)
// Нет mic-данных — заполняем входной TX-буфер нулями.
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
end;
@@ -4258,7 +4254,7 @@ end;
procedure TWDSPEngine.PushPSFeedback(const Buf: array of Byte;
DataOffset: Integer; TotalPairs: Integer);
// Сетевой поток: interleaved 24-bit BE пары [DDC0 I,Q][DDC1 I,Q]... →
// деинтерливим и кормим WDSP pscc (как pihpsdr process_ps_iq_data):
// Деинтерливим feedback-потоки и передаём их в WDSP pscc:
// DDC0 = RX-feedback (искажённый сигнал после PA), DDC1 = TX DAC (эталон).
var
i, n, Pos: Integer;
@@ -4329,7 +4325,7 @@ procedure TWDSPEngine.TXFilterEdgesHz(out Lo, Hi: Integer);
// WFM (20..15000), и полоса на спектре уезжала вверх от несущей — визуально
// «переключалась на USB». Для ЧМ считаем по Карсону: ±(девиация + верхний срез
// аудио), симметрично несущей. У MODE_FM TXSignedEdges отдаёт ровно эту
// величину и без нас (порт Thetis), но пусть формула будет в одном месте.
// величину и без нас, но формула остаётся в одном месте.
var L, H, Half: Double;
begin
Half := 0.0;
@@ -4423,7 +4419,7 @@ end;
procedure TWDSPEngine.PushTXEQProfile;
// Единственная точка, отдающая кривую EQ в WDSP.
// F[1..n] — УЗЛЫ ломаной АЧХ (eq.c/eq_impulse), между ними интерполяция по
// F[1..n] — узлы ломаной АЧХ, между ними интерполяция по
// децибелам, а за крайними узлами при ctfmode=0 (так создаётся eqp в TXA.c)
// идёт кумулятивный скат (f/f0)^4 на бин — фактически обрыв. Поэтому:
// • 10 полос — узлы берём из профиля как есть (за краями обязан быть запас,
@@ -4694,7 +4690,7 @@ end;
function TWDSPEngine.NormalizeFFTSize(FFTSize: Integer): Integer;
begin
// Thetis-style display FFT range.
// Диапазон display FFT.
if FFTSize < 4096 then FFTSize := 4096;
Result := 4096;
while Result < FFTSize do
@@ -4705,7 +4701,7 @@ end;
function TWDSPEngine.CalcDisplayFFTSize: Integer;
begin
// FFT Size теперь снова управляет display-analyzer как в Thetis,
// FFT Size управляет display-analyzer,
// но не влияет на аудио DSP цепочку.
Result := FFFTSize;
end;
@@ -4716,7 +4712,7 @@ var
TargetFPS: Double;
begin
TargetFPS := EnsureRange(FDisplayFPS, 5, 100);
// Thetis/piHPSDR-style timing:
// Тайминг обновления дисплея:
// целимся в пользовательский FPS и разрешаем большой overlap даже на крупных FFT.
Overlap := Max(0, Ceil(AnalyzerFFT - (FSampleRate / TargetFPS)));
EffectiveFPS := TargetFPS;
@@ -4729,7 +4725,7 @@ begin
if ForWaterfall then
begin
// Peak hold на водопаде даёт грязную/залипающую картинку и не похож на Thetis.
// Peak hold на водопаде даёт грязную и залипающую картинку.
if AvgMode < 0 then AvgMode := 1;
end
else