fix(pluto): телеметрия больше не блокирует UI-поток

ReadTelemetry делал 5-6 синхронных iio_channel_attr_read_* — каждое это USB
round-trip, вместе ~30 мс. Звался из MeterTimerTick, то есть прямо в главном
потоке, дважды в секунду (POLL_MS=500). Замер зонным профайлером: тик метра
занимал 5633 мкс настенного времени при пиках до 51 мс, тогда как CPU-времени
в нём было всего 140 мкс — чистое ожидание USB, ронявшее кадры.

Опрос переехал в TPlutoTeleThread: он раз в 500 мс читает те же атрибуты и
публикует снимок под FTeleLock. ReadTelemetry теперь только отдаёт снимок и
никогда не блокирует, поэтому RadioController.ServicePlutoTelemetry и
MainForm.MeterTimerTick не менялись — вся фильтрация температуры на месте.
Прежнее блокирующее тело стало PollTelemetryBlocking (только из потока).

Конкуренция за контекст libiio не выросла: UI-поток и раньше читал атрибуты
параллельно с iio_buffer_refill в RX-потоке. Сменился лишь тот, кто ждёт.

StopTelemetry идёт первой строкой Disconnect — до iio_context_destroy, иначе
поток продолжил бы читать атрибуты разрушенного контекста.

Результат: ui.meter_tick 5633 -> 110 мкс. Значение телеметрии теперь может
отставать до секунды (троттл контроллера + период потока) — на 10-герцовом
индикаторе температуры это незаметно.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 16:28:45 +03:00
co-authored by Claude Opus 4.8
parent 66c2c09656
commit 7a554eb04e
2 changed files with 142 additions and 3 deletions
+119 -3
View File
@@ -82,6 +82,21 @@ type
FTxFifoCount: Integer; // занято элементов FTxFifoCount: Integer; // занято элементов
FTxLock: TRTLCriticalSection; 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 // Низкоуровневые помощники применения атрибутов к ad9361-phy
procedure ApplyRxLO(Hz: Double); procedure ApplyRxLO(Hz: Double);
procedure ApplyTxLO(Hz: Double); procedure ApplyTxLO(Hz: Double);
@@ -168,6 +183,19 @@ type
constructor Create(ABk: TPlutoBackend); constructor Create(ABk: TPlutoBackend);
end; 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), пишет в // TX-поток: тянет int16-пары из FIFO бэкенда (или нули при underrun), пишет в
// iio-буфер и push'ит. iio_buffer_push блокирует до приёма DMA → пейсинг ровно // iio-буфер и push'ит. iio_buffer_push блокирует до приёма DMA → пейсинг ровно
// на device-rate. Работает с private-полями TPlutoBackend (одна единица компиляции). // на device-rate. Работает с private-полями TPlutoBackend (одна единица компиляции).
@@ -350,6 +378,9 @@ begin
FStreaming := False; FRxSeq := 0; FStreaming := False; FRxSeq := 0;
FTxBuf := nil; FTxChI := nil; FTxChQ := nil; FTxThread := nil; FTxBuf := nil; FTxChI := nil; FTxChQ := nil; FTxThread := nil;
FTxStreaming := False; FTxRatio := 8; FTxStreaming := False; FTxRatio := 8;
FTeleThread := nil; FTeleValid := False;
FTeleTemp := TELEMETRY_NONE; FTeleSoc := TELEMETRY_NONE; FTeleRssi := TELEMETRY_NONE;
InitCriticalSection(FTeleLock);
FTxAttDb := PLUTO_TX_OFF; // на старте TX выключен (макс. аттенюация) FTxAttDb := PLUTO_TX_OFF; // на старте TX выключен (макс. аттенюация)
// FIFO на ~4 TX-буфера запаса под джиттер продюсера/консьюмера. Окончательный // FIFO на ~4 TX-буфера запаса под джиттер продюсера/консьюмера. Окончательный
// размер выставляется в StartTX по фактическому device-rate (TxBufPairsForRate). // размер выставляется в StartTX по фактическому device-rate (TxBufPairsForRate).
@@ -363,7 +394,8 @@ end;
destructor TPlutoBackend.Destroy; destructor TPlutoBackend.Destroy;
begin begin
Disconnect; Disconnect; // остановит и телеметрию
DoneCriticalSection(FTeleLock);
DoneCriticalSection(FTxLock); DoneCriticalSection(FTxLock);
inherited Destroy; inherited Destroy;
end; end;
@@ -535,11 +567,13 @@ begin
ApplySampleRate(FSampleRate); ApplySampleRate(FSampleRate);
ApplyGain; ApplyGain;
ApplyTxAtten; // TX заглушён (макс. аттенюация) до явного PTT ApplyTxAtten; // TX заглушён (макс. аттенюация) до явного PTT
StartTelemetry;
Result := True; Result := True;
end; end;
procedure TPlutoBackend.Disconnect; procedure TPlutoBackend.Disconnect;
begin begin
StopTelemetry; // до iio_context_destroy: поток ещё читает атрибуты
StopTX; StopTX;
StopRX; StopRX;
if FCtx <> nil then if FCtx <> nil then
@@ -904,8 +938,8 @@ begin
LeaveCriticalSection(FTxLock); LeaveCriticalSection(FTxLock);
end; end;
function TPlutoBackend.ReadTelemetry(out TempC, SocTempC, RSSIdB: Double): Boolean; function TPlutoBackend.PollTelemetryBlocking(out TempC, SocTempC, RSSIdB: Double): Boolean;
// Опрос аппаратной телеметрии (вызывается контроллером, не из RX-потока): // БЛОКИРУЮЩИЙ опрос железа — только из FTeleThread (каждое чтение = USB round-trip):
// ad9361-phy temp0/input — температура трансивера в миллиградусах C (longlong); // ad9361-phy temp0/input — температура трансивера в миллиградусах C (longlong);
// xadc temp0 — температура Zynq SoC/FPGA; сырой raw + offset (longlong) // xadc temp0 — температура Zynq SoC/FPGA; сырой raw + offset (longlong)
// + scale (double), C = (raw + offset) * scale / 1000; // + scale (double), C = (raw + offset) * scale / 1000;
@@ -963,4 +997,86 @@ begin
end; end;
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. end.
+23
View File
@@ -26,6 +26,7 @@ interface
uses uses
Classes, SysUtils, Math, SyncObjs, Classes, SysUtils, Math, SyncObjs,
BeaconDecoder, BeaconDecoder,
PerfLog, // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры
WDSP; WDSP;
const const
@@ -1365,10 +1366,12 @@ procedure TWDSPEngine.PushIQItemToDSP(const Item: TIQQueueItem);
var var
i, Pos: Integer; i, Pos: Integer;
IR, QR: LongInt; IR, QR: LongInt;
T0: Int64; // PERF
const const
SCALE = 1.0 / 8388608.0; SCALE = 1.0 / 8388608.0;
begin begin
if FTXActive and not FKeepRXDuringTX then Exit; if FTXActive and not FKeepRXDuringTX then Exit;
T0 := PerfNow; // PERF
// Post-TX hold: дропаем входящие RX-IQ пакеты пока активно окно мьюта. // Post-TX hold: дропаем входящие RX-IQ пакеты пока активно окно мьюта.
// Это закрывает ОБА канала сразу — и водопад/спектр (анализатор не // Это закрывает ОБА канала сразу — и водопад/спектр (анализатор не
// кормится), и аудио (RXA не запускается, FOnAudio не вызывается). // кормится), и аудио (RXA не запускается, FOnAudio не вызывается).
@@ -1402,9 +1405,11 @@ begin
if FRXAccPos >= FBufSize then if FRXAccPos >= FBufSize then
ProcessRXBlock; ProcessRXBlock;
end; end;
PerfAdd(pzDspPushIQ, T0); // PERF: весь путь пакета через DSP-поток
end; end;
procedure TWDSPEngine.FeedDisplaySample(const I, Q: Double); procedure TWDSPEngine.FeedDisplaySample(const I, Q: Double);
var T0: Int64; // PERF
begin begin
if not FAnalyzerOpen then Exit; if not FAnalyzerOpen then Exit;
@@ -1414,7 +1419,9 @@ begin
if FDispPos >= DISPLAY_BLOCK_SIZE then if FDispPos >= DISPLAY_BLOCK_SIZE then
begin begin
T0 := PerfNow; // PERF
Spectrum0(1, RX_DISP_ID, 0, 0, @FDispBuf[0]); Spectrum0(1, RX_DISP_ID, 0, 0, @FDispBuf[0]);
PerfAdd(pzDspSpectrum0, T0); // PERF: скармливание блока анализатору
FDispPos := 0; FDispPos := 0;
end; end;
end; end;
@@ -1438,12 +1445,14 @@ procedure TWDSPEngine.ProcessRXBlock;
var var
i: Integer; i: Integer;
Err: Integer; Err: Integer;
T0: Int64; // PERF
// OutL/OutR теперь поля класса — не аллоцируем каждый раз // OutL/OutR теперь поля класса — не аллоцируем каждый раз
begin begin
FRXAccPos := 0; FRXAccPos := 0;
if not FInitialized then Exit; if not FInitialized then Exit;
try try
T0 := PerfNow; // PERF
// Упаковываем в interleaved буфер для fexchange0 // Упаковываем в interleaved буфер для fexchange0
for i := 0 to FBufSize - 1 do for i := 0 to FBufSize - 1 do
begin begin
@@ -1454,6 +1463,7 @@ begin
// DSP обработка // DSP обработка
Err := 0; Err := 0;
fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err); fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err);
PerfAdd(pzDspFexMain, T0); // PERF: главный RX-канал WDSP
// S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter. // S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter.
// GetRXAMeter сбрасывает аккумулятор после вызова, поэтому второй // GetRXAMeter сбрасывает аккумулятор после вызова, поэтому второй
@@ -1475,12 +1485,14 @@ begin
and (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX)) and (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX))
and (FPostTXMuteUntil = 0) then and (FPostTXMuteUntil = 0) then
begin begin
T0 := PerfNow; // PERF
for i := 0 to FAudioBufSize - 1 do for i := 0 to FAudioBufSize - 1 do
begin begin
FOutL[i] := FRXOut[i * 2] * FVolume; FOutL[i] := FRXOut[i * 2] * FVolume;
FOutR[i] := FRXOut[i * 2 + 1] * FVolume; FOutR[i] := FRXOut[i * 2 + 1] * FVolume;
end; end;
FOnAudio(FOutL, FOutR, FAudioBufSize); FOnAudio(FOutL, FOutR, FAudioBufSize);
PerfAdd(pzDspAudioCb, T0); // PERF: маршрут RX-аудио (web/радио/звуковуха)
end; end;
// Софтовые слайсы: fan-out того же IQ-блока на доп. WDSP-каналы. // Софтовые слайсы: fan-out того же IQ-блока на доп. WDSP-каналы.
@@ -1588,8 +1600,10 @@ procedure TWDSPEngine.ProcessSlices;
var var
s, i, k, Err: Integer; s, i, k, Err: Integer;
V: Double; V: Double;
T0, TS: Int64; // PERF
begin begin
if not Assigned(FOnSliceAudio) then Exit; if not Assigned(FOnSliceAudio) then Exit;
T0 := PerfNow; // PERF
FSliceLock.Enter; FSliceLock.Enter;
try try
for s := 0 to MAX_SLICES - 1 do for s := 0 to MAX_SLICES - 1 do
@@ -1603,7 +1617,9 @@ begin
FSliceIn[i * 2 + 1] := FRXAccQ[i]; FSliceIn[i * 2 + 1] := FRXAccQ[i];
end; end;
Err := 0; Err := 0;
TS := PerfNow; // PERF
fexchange0(FSlices[s].Chan, @FSliceIn[0], @FSliceOut[0], @Err); fexchange0(FSlices[s].Chan, @FSliceIn[0], @FSliceOut[0], @Err);
PerfAdd(pzDspSliceFex, TS); // PERF: fexchange0 одного слайса
// S-метр канала читаем здесь (DSP-поток) — GetRXAMeter сбрасывает аккумулятор. // S-метр канала читаем здесь (DSP-поток) — GetRXAMeter сбрасывает аккумулятор.
FSlices[s].SMeter := GetRXAMeter(FSlices[s].Chan, RXA_S_AV); FSlices[s].SMeter := GetRXAMeter(FSlices[s].Chan, RXA_S_AV);
V := FSlices[s].Volume; V := FSlices[s].Volume;
@@ -1617,6 +1633,7 @@ begin
finally finally
FSliceLock.Leave; FSliceLock.Leave;
end; end;
PerfAdd(pzDspSlices, T0); // PERF: fan-out всех слайсов целиком
end; end;
function TWDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi: Integer; function TWDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi: Integer;
@@ -2273,8 +2290,10 @@ var
i, tail: Integer; i, tail: Integer;
Err: Integer; Err: Integer;
Avail: Integer; Avail: Integer;
T0: Int64; // PERF
begin begin
if not FInitialized then Exit; if not FInitialized then Exit;
T0 := PerfNow; // PERF
Avail := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1); Avail := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1);
@@ -2314,6 +2333,7 @@ begin
// FTXOut содержит FTXOutBufSize IQ пар @ FTXSampleRate (interleaved double) // FTXOut содержит FTXOutBufSize IQ пар @ FTXSampleRate (interleaved double)
if Assigned(FOnTXIQ) then if Assigned(FOnTXIQ) then
FOnTXIQ(FTXOut, FTXOutBufSize); FOnTXIQ(FTXOut, FTXOutBufSize);
PerfAdd(pzDspTxBlock, T0); // PERF: TX-блок целиком (fexchange0 + analyzer + IQ out)
end; end;
procedure TWDSPEngine.PushTXMicSamples16(const Src: array of SmallInt; N: Integer); procedure TWDSPEngine.PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
@@ -2611,8 +2631,10 @@ var
Flag: Integer; Flag: Integer;
i, N: Integer; i, N: Integer;
DispID: Integer; DispID: Integer;
T0: Int64; // PERF
begin begin
if not FInitialized or not FAnalyzerOpen then Exit; if not FInitialized or not FAnalyzerOpen then Exit;
T0 := PerfNow; // PERF
N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS); N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);
DispID := FActiveDisplayID; // RX_DISP_ID или TX_DISP_ID, зависит от FTXActive DispID := FActiveDisplayID; // RX_DISP_ID или TX_DISP_ID, зависит от FTXActive
@@ -2635,6 +2657,7 @@ begin
if Assigned(FOnWaterfall) then if Assigned(FOnWaterfall) then
FOnWaterfall(FWaterfallPixels, N); FOnWaterfall(FWaterfallPixels, N);
end; end;
PerfAdd(pzDispGetPixels, T0); // PERF: GetPixels + колбэки спектр/водопад
end; end;
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single; procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;