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; // занято элементов
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.