diff --git a/PlutoBackend.pas b/PlutoBackend.pas index 6222a22..c08cb89 100644 --- a/PlutoBackend.pas +++ b/PlutoBackend.pas @@ -82,6 +82,21 @@ type FTxFifoCount: Integer; // занято элементов FTxLock: TRTLCriticalSection; + // Телеметрия. Каждый iio_channel_attr_read_* — USB round-trip (~5 мс), а их + // 5-6 за опрос: ~30 мс. Раньше это делал UI-поток (MeterTimerTick, 10 Гц) и + // замирал на весь опрос. Теперь опрашивает свой поток, а ReadTelemetry лишь + // отдаёт последний снимок и никогда не блокирует. + FTeleThread: TThread; + FTeleLock: TRTLCriticalSection; + FTeleTemp: Double; // TELEMETRY_NONE, пока не прочитано + FTeleSoc: Double; + FTeleRssi: Double; + FTeleValid: Boolean; // был ли хоть один успешный опрос + procedure StartTelemetry; + procedure StopTelemetry; + // Блокирующий опрос железа: только из FTeleThread. + function PollTelemetryBlocking(out TempC, SocTempC, RSSIdB: Double): Boolean; + // Низкоуровневые помощники применения атрибутов к ad9361-phy procedure ApplyRxLO(Hz: Double); procedure ApplyTxLO(Hz: Double); @@ -168,6 +183,19 @@ type constructor Create(ABk: TPlutoBackend); end; + // Телеметрия: опрашивает железо в своём темпе и публикует снимок. Держит UI + // подальше от блокирующих USB-транзакций. Доступ к контексту libiio здесь не + // более конкурентен, чем раньше: UI-поток дёргал те же атрибуты параллельно с + // refill в RX-потоке — сменился только поток, который ждёт. + TPlutoTeleThread = class(TThread) + private + FBk: TPlutoBackend; + protected + procedure Execute; override; + public + constructor Create(ABk: TPlutoBackend); + end; + // TX-поток: тянет int16-пары из FIFO бэкенда (или нули при underrun), пишет в // iio-буфер и push'ит. iio_buffer_push блокирует до приёма DMA → пейсинг ровно // на device-rate. Работает с private-полями TPlutoBackend (одна единица компиляции). @@ -350,6 +378,9 @@ begin FStreaming := False; FRxSeq := 0; FTxBuf := nil; FTxChI := nil; FTxChQ := nil; FTxThread := nil; FTxStreaming := False; FTxRatio := 8; + FTeleThread := nil; FTeleValid := False; + FTeleTemp := TELEMETRY_NONE; FTeleSoc := TELEMETRY_NONE; FTeleRssi := TELEMETRY_NONE; + InitCriticalSection(FTeleLock); FTxAttDb := PLUTO_TX_OFF; // на старте TX выключен (макс. аттенюация) // FIFO на ~4 TX-буфера запаса под джиттер продюсера/консьюмера. Окончательный // размер выставляется в StartTX по фактическому device-rate (TxBufPairsForRate). @@ -363,7 +394,8 @@ end; destructor TPlutoBackend.Destroy; begin - Disconnect; + Disconnect; // остановит и телеметрию + DoneCriticalSection(FTeleLock); DoneCriticalSection(FTxLock); inherited Destroy; end; @@ -535,11 +567,13 @@ begin ApplySampleRate(FSampleRate); ApplyGain; ApplyTxAtten; // TX заглушён (макс. аттенюация) до явного PTT + StartTelemetry; Result := True; end; procedure TPlutoBackend.Disconnect; begin + StopTelemetry; // до iio_context_destroy: поток ещё читает атрибуты StopTX; StopRX; if FCtx <> nil then @@ -904,8 +938,8 @@ begin LeaveCriticalSection(FTxLock); end; -function TPlutoBackend.ReadTelemetry(out TempC, SocTempC, RSSIdB: Double): Boolean; -// Опрос аппаратной телеметрии (вызывается контроллером, не из RX-потока): +function TPlutoBackend.PollTelemetryBlocking(out TempC, SocTempC, RSSIdB: Double): Boolean; +// БЛОКИРУЮЩИЙ опрос железа — только из FTeleThread (каждое чтение = USB round-trip): // ad9361-phy temp0/input — температура трансивера в миллиградусах C (longlong); // xadc temp0 — температура Zynq SoC/FPGA; сырой raw + offset (longlong) // + scale (double), C = (raw + offset) * scale / 1000; @@ -963,4 +997,86 @@ begin end; end; +function TPlutoBackend.ReadTelemetry(out TempC, SocTempC, RSSIdB: Double): Boolean; +// Неблокирующий: отдаёт последний снимок, снятый FTeleThread. Безопасно звать из UI. +begin + EnterCriticalSection(FTeleLock); + try + TempC := FTeleTemp; + SocTempC := FTeleSoc; + RSSIdB := FTeleRssi; + Result := FTeleValid; + finally + LeaveCriticalSection(FTeleLock); + end; +end; + +procedure TPlutoBackend.StartTelemetry; +begin + if FTeleThread = nil then + FTeleThread := TPlutoTeleThread.Create(Self); +end; + +procedure TPlutoBackend.StopTelemetry; +begin + if FTeleThread = nil then Exit; + FTeleThread.Terminate; + FTeleThread.WaitFor; // обязателен до разрушения контекста libiio + FreeAndNil(FTeleThread); + EnterCriticalSection(FTeleLock); + try + FTeleValid := False; + FTeleTemp := TELEMETRY_NONE; + FTeleSoc := TELEMETRY_NONE; + FTeleRssi := TELEMETRY_NONE; + finally + LeaveCriticalSection(FTeleLock); + end; +end; + +{ TPlutoTeleThread } + +constructor TPlutoTeleThread.Create(ABk: TPlutoBackend); +begin + FBk := ABk; + FreeOnTerminate := False; + inherited Create(False); +end; + +procedure TPlutoTeleThread.Execute; +const + POLL_MS = 500; // ~2 Гц: чаще незачем, каждый опрос ~30 мс USB-транзакций + STEP_MS = 25; // мелкий шаг сна — чтобы Terminate отрабатывал быстро +var + t, s, r: Double; + ok: Boolean; + Waited: Integer; +begin + while not Terminated do + begin + if FBk.FConnected then + begin + ok := FBk.PollTelemetryBlocking(t, s, r); + if ok then + begin + EnterCriticalSection(FBk.FTeleLock); + try + FBk.FTeleTemp := t; + FBk.FTeleSoc := s; + FBk.FTeleRssi := r; + FBk.FTeleValid := True; + finally + LeaveCriticalSection(FBk.FTeleLock); + end; + end; + end; + Waited := 0; + while (Waited < POLL_MS) and not Terminated do + begin + Sleep(STEP_MS); + Inc(Waited, STEP_MS); + end; + end; +end; + end. diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 7f4d22b..f58d295 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -26,6 +26,7 @@ interface uses Classes, SysUtils, Math, SyncObjs, BeaconDecoder, + PerfLog, // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры WDSP; const @@ -1365,10 +1366,12 @@ procedure TWDSPEngine.PushIQItemToDSP(const Item: TIQQueueItem); var i, Pos: Integer; IR, QR: LongInt; + T0: Int64; // PERF const SCALE = 1.0 / 8388608.0; begin if FTXActive and not FKeepRXDuringTX then Exit; + T0 := PerfNow; // PERF // Post-TX hold: дропаем входящие RX-IQ пакеты пока активно окно мьюта. // Это закрывает ОБА канала сразу — и водопад/спектр (анализатор не // кормится), и аудио (RXA не запускается, FOnAudio не вызывается). @@ -1402,9 +1405,11 @@ begin if FRXAccPos >= FBufSize then ProcessRXBlock; end; + PerfAdd(pzDspPushIQ, T0); // PERF: весь путь пакета через DSP-поток end; procedure TWDSPEngine.FeedDisplaySample(const I, Q: Double); +var T0: Int64; // PERF begin if not FAnalyzerOpen then Exit; @@ -1414,7 +1419,9 @@ begin if FDispPos >= DISPLAY_BLOCK_SIZE then begin + T0 := PerfNow; // PERF Spectrum0(1, RX_DISP_ID, 0, 0, @FDispBuf[0]); + PerfAdd(pzDspSpectrum0, T0); // PERF: скармливание блока анализатору FDispPos := 0; end; end; @@ -1438,12 +1445,14 @@ procedure TWDSPEngine.ProcessRXBlock; var i: Integer; Err: Integer; + T0: Int64; // PERF // OutL/OutR теперь поля класса — не аллоцируем каждый раз begin FRXAccPos := 0; if not FInitialized then Exit; try + T0 := PerfNow; // PERF // Упаковываем в interleaved буфер для fexchange0 for i := 0 to FBufSize - 1 do begin @@ -1454,6 +1463,7 @@ begin // DSP обработка Err := 0; fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err); + PerfAdd(pzDspFexMain, T0); // PERF: главный RX-канал WDSP // S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter. // GetRXAMeter сбрасывает аккумулятор после вызова, поэтому второй @@ -1475,12 +1485,14 @@ begin and (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX)) and (FPostTXMuteUntil = 0) then begin + T0 := PerfNow; // PERF for i := 0 to FAudioBufSize - 1 do begin FOutL[i] := FRXOut[i * 2] * FVolume; FOutR[i] := FRXOut[i * 2 + 1] * FVolume; end; FOnAudio(FOutL, FOutR, FAudioBufSize); + PerfAdd(pzDspAudioCb, T0); // PERF: маршрут RX-аудио (web/радио/звуковуха) end; // Софтовые слайсы: fan-out того же IQ-блока на доп. WDSP-каналы. @@ -1588,8 +1600,10 @@ procedure TWDSPEngine.ProcessSlices; var s, i, k, Err: Integer; V: Double; + T0, TS: Int64; // PERF begin if not Assigned(FOnSliceAudio) then Exit; + T0 := PerfNow; // PERF FSliceLock.Enter; try for s := 0 to MAX_SLICES - 1 do @@ -1603,7 +1617,9 @@ begin FSliceIn[i * 2 + 1] := FRXAccQ[i]; end; Err := 0; + TS := PerfNow; // PERF fexchange0(FSlices[s].Chan, @FSliceIn[0], @FSliceOut[0], @Err); + PerfAdd(pzDspSliceFex, TS); // PERF: fexchange0 одного слайса // S-метр канала читаем здесь (DSP-поток) — GetRXAMeter сбрасывает аккумулятор. FSlices[s].SMeter := GetRXAMeter(FSlices[s].Chan, RXA_S_AV); V := FSlices[s].Volume; @@ -1617,6 +1633,7 @@ begin finally FSliceLock.Leave; end; + PerfAdd(pzDspSlices, T0); // PERF: fan-out всех слайсов целиком end; function TWDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi: Integer; @@ -2273,8 +2290,10 @@ var i, tail: Integer; Err: Integer; Avail: Integer; + T0: Int64; // PERF begin if not FInitialized then Exit; + T0 := PerfNow; // PERF Avail := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1); @@ -2314,6 +2333,7 @@ begin // FTXOut содержит FTXOutBufSize IQ пар @ FTXSampleRate (interleaved double) if Assigned(FOnTXIQ) then FOnTXIQ(FTXOut, FTXOutBufSize); + PerfAdd(pzDspTxBlock, T0); // PERF: TX-блок целиком (fexchange0 + analyzer + IQ out) end; procedure TWDSPEngine.PushTXMicSamples16(const Src: array of SmallInt; N: Integer); @@ -2611,8 +2631,10 @@ var Flag: Integer; i, N: Integer; DispID: Integer; + T0: Int64; // PERF begin if not FInitialized or not FAnalyzerOpen then Exit; + T0 := PerfNow; // PERF N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS); DispID := FActiveDisplayID; // RX_DISP_ID или TX_DISP_ID, зависит от FTXActive @@ -2635,6 +2657,7 @@ begin if Assigned(FOnWaterfall) then FOnWaterfall(FWaterfallPixels, N); end; + PerfAdd(pzDispGetPixels, T0); // PERF: GetPixels + колбэки спектр/водопад end; procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;