mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
fix(tci): всплески на передаче — TX-аудио просилось общим тиком 20 мс
Посреди передачи из MSHV на водопаде появлялись всплески своего сигнала. Цепочка: очередь DUC пустеет дольше подушки отправителя (DUC_FIFO_THROTTLE = 2000 отсчётов = 10.4 мс) → FIFO радио сохнет → модуляция обрывается → в эфире остаётся голая несущая на частоте гетеродина DUC, в стороне от тона ровно на звуковой сдвиг. Доказано pcap-съёмом: шесть всплесков в дампе — ровно столько, сколько видел оператор, и каждый стоит за паузой 10.3-23.5 мс, а паузы 8 мс и короче не дали ни одного. Виноват не клиент и не блокировка UI, а зернистость НАШЕГО запроса. Слой первый: PushTxChrono жил на общем тике сервера 20 мс, а просил блок клиента целиком (2048 отсчётов = 42.7 мс) — маркер выходил через два или три тика, то есть через 40 или 60 мс. Слой второй: MSHV отвечает пачками по 4-5 блоков раз в ~44 мс (STREAM_C = 4096 при 96 кГц), и мелкий квант этого не лечит — нужен запас не меньше пачки. Сделано: * квант запроса = один блок TXA (512 отсчётов движка), а не блок клиента; * свой поток-планировщик TxTickLoop с АБСОЛЮТНЫМИ дедлайнами (опоздание одного пробуждения не сдвигает сетку); общий тик маркеров больше не шлёт; * бухгалтерия Owed/InFlight в кадрах на канал, гасится по k до интерполятора; потолок долга обязан быть выше окна в полёте (TCI_TX_OWED_HEADROOM_Q), иначе связывающим становится он и подача падает до 58% реального времени при полностью исправном клиенте; * SendBinNow: маркеры пишутся в сокет напрямую под FWriteLock, минуя очередь (та выпускается лишь на пробуждении потока клиента, TCI_POLL_MS = 20 мс — вдвое больше кванта, и подача снова рвалась); * FReapLock: планировщик TX — новый поток, а правило «клиента освобождает только тик-поток» держалось на том, что им же он и пользуется; * аванс под зернистость клиента: измеряется по ПЕРИОДУ между пачками (размер пачки зависит от того, сколько мы запросили ⇒ положительная обратная связь), переживает конец передачи, умеет уменьшаться по выдержке TCI_TX_LEAD_DOWN_MS, потолок TCI_TX_LEAD_MAX_MS; * старт передачи: KickTxTick будит планировщика на фронте PTT, TxPreWarm шлёт один маркер ДО SetMOX (41 мс раздумий клиента накладываются на нашу же подготовку тракта) под гейтом «передатчик свободен и чужого источника нет», PrimeDUCIQ для источника TCI растянут до Max(6096, аванс×4) — путь микрофона радио, CW и web не затронут; * посев аванса TCI_TX_LEAD_DEF_MS = 50 мс, пока про клиента ничего не известно: обучение к первому осушению физически не успевает. Монотонные часы одного источника для всех потоков — PlatformUtils.MonotonicUs (абсолютные дедлайны не терпят часов, способных прыгнуть от NTP). На железе: опасных осушений посреди передачи НОЛЬ (было 12 за 11 с), четыре передачи из пяти вообще без единого, включая старт; прогон 15:21 чист везде, в том числе на первой передаче после подключения. Всплесков оператор больше не видит. Приборы: TxTrace.pas (EWSDR_TXTRACE=1) и стенд test/hpsdr — кольцевой tcpdump capture.sh, разбор дампа pcap_tx_scan.py, разбор трассы txtrace_scan.py. Стенд test/tci: часть F «Пейсинг TX», 260 проверок, провалов нет; живой клиент с рампой, RTT и потерями — test/tci/tx_chrono_bench.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gmVQnna1i4EbSZ2VGm6KD
This commit is contained in:
+139
@@ -0,0 +1,139 @@
|
||||
unit TxTrace;
|
||||
|
||||
{
|
||||
TxTrace.pas — кольцевая трассировка TX-тракта.
|
||||
|
||||
★Временный диагностический юнит. Выключен по умолчанию, включается
|
||||
переменной окружения EWSDR_TXTRACE=1; при выключенной трассировке TR() —
|
||||
одна проверка Boolean. Никакого ввода-вывода в горячем пути: кольцо
|
||||
сбрасывается на диск в конце каждой передачи.
|
||||
|
||||
Зачем он снова здесь: всплески на передаче вернулись после того, как первая
|
||||
редакция зондов была удалена, и без них видно только то, что видит глаз на
|
||||
водопаде. Прибор смотрит ровно на подачу TX-аудио по TCI и на очередь DUC.
|
||||
}
|
||||
|
||||
{$IFDEF FPC}
|
||||
{$MODE Delphi}
|
||||
{$ENDIF}
|
||||
|
||||
interface
|
||||
|
||||
const
|
||||
trkPtt = 0; // A=1 старт передачи / 0 конец; B=источник модуляции
|
||||
trkChrono = 7; // A=запрошено кадров; B=непокрытый долг; C=пауза от прошлого маркера, мкс
|
||||
trkTxAudio = 8; // A=пауза от прошлого блока, мкс; B=length в шапке; C=кадров после интерполяции
|
||||
trkDucDry = 10; // A=виртуальный уровень FIFO радио; B=глубина очереди
|
||||
trkDucRun = 11; // A=длительность простоя очереди, мкс; B=уровень FIFO; C=глубина очереди
|
||||
trkNote = 12; // A=-1 прощение кредита, -2 смена аванса, -3 в полёте на снятии PTT, -4 предварительный маркер
|
||||
|
||||
var
|
||||
TraceOn: Boolean = False;
|
||||
|
||||
procedure TraceInit;
|
||||
procedure TR(Kind: Integer; A: Integer = 0; B: Integer = 0; C: Integer = 0);
|
||||
function TraceDump(const Reason: string): string;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
SysUtils, Math, PlatformUtils;
|
||||
|
||||
const
|
||||
TRACE_SIZE = 16384;
|
||||
TRACE_MASK = TRACE_SIZE - 1;
|
||||
|
||||
type
|
||||
TTraceRec = record
|
||||
T: Int64;
|
||||
Kind: Integer;
|
||||
A, B, C: Integer;
|
||||
end;
|
||||
|
||||
var
|
||||
Ring: array[0..TRACE_SIZE - 1] of TTraceRec;
|
||||
RingSeq: LongInt = 0;
|
||||
Epoch: Int64 = 0;
|
||||
|
||||
const
|
||||
TRACE_NAMES: array[0..12] of string = (
|
||||
'PTT', 'RECV-GAP', 'HANDLER', 'HP-SYNC', 'WIDEBAND', 'TX-TICK',
|
||||
'TX-PHASE', 'CHRONO', 'TX-AUDIO', 'PULL-MIC', 'DUC-DRY', 'DUC-RUN', 'NOTE');
|
||||
|
||||
procedure TraceInit;
|
||||
var
|
||||
S: string;
|
||||
begin
|
||||
S := LowerCase(Trim(GetEnvironmentVariable('EWSDR_TXTRACE')));
|
||||
TraceOn := (S = '1') or (S = 'true') or (S = 'yes') or (S = 'on');
|
||||
Epoch := MonotonicUs;
|
||||
end;
|
||||
|
||||
procedure TR(Kind: Integer; A: Integer = 0; B: Integer = 0; C: Integer = 0);
|
||||
var
|
||||
Idx: LongInt;
|
||||
P: ^TTraceRec;
|
||||
begin
|
||||
if not TraceOn then Exit;
|
||||
Idx := InterlockedIncrement(RingSeq) - 1;
|
||||
P := @Ring[Idx and TRACE_MASK];
|
||||
P^.T := MonotonicUs;
|
||||
P^.Kind := Kind;
|
||||
P^.A := A;
|
||||
P^.B := B;
|
||||
P^.C := C;
|
||||
end;
|
||||
|
||||
function Slug(const S: string): string;
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := '';
|
||||
for i := 1 to Length(S) do
|
||||
if S[i] in ['a'..'z', 'A'..'Z', '0'..'9', '-', '_'] then
|
||||
Result := Result + S[i];
|
||||
if Result <> '' then Result := '-' + Copy(Result, 1, 24);
|
||||
end;
|
||||
|
||||
function TraceDump(const Reason: string): string;
|
||||
var
|
||||
F: TextFile;
|
||||
i, First: LongInt;
|
||||
Total: LongInt;
|
||||
R: TTraceRec;
|
||||
Name: string;
|
||||
begin
|
||||
Result := '';
|
||||
if not TraceOn then Exit;
|
||||
Total := RingSeq;
|
||||
if Total <= 0 then Exit;
|
||||
|
||||
Result := GetAppCfgDir + 'txtrace-' +
|
||||
FormatDateTime('yyyymmdd-hhnnss', Now) + Slug(Reason) + '.txt';
|
||||
AssignFile(F, Result);
|
||||
try
|
||||
Rewrite(F);
|
||||
try
|
||||
WriteLn(F, '# ewsdr TX trace, повод: ', Reason);
|
||||
WriteLn(F, '# всего событий: ', Total, ', в кольце: ',
|
||||
Min(Total, TRACE_SIZE));
|
||||
WriteLn(F, '# t_ms событие A B C');
|
||||
First := Total - Min(Total, TRACE_SIZE);
|
||||
for i := First to Total - 1 do
|
||||
begin
|
||||
R := Ring[i and TRACE_MASK];
|
||||
if (R.Kind < Low(TRACE_NAMES)) or (R.Kind > High(TRACE_NAMES)) then
|
||||
Name := '?'
|
||||
else
|
||||
Name := TRACE_NAMES[R.Kind];
|
||||
WriteLn(F, Format('%12.3f %-9s %9d %9d %9d',
|
||||
[(R.T - Epoch) / 1000.0, Name, R.A, R.B, R.C]));
|
||||
end;
|
||||
finally
|
||||
CloseFile(F);
|
||||
end;
|
||||
except
|
||||
on E: Exception do Result := '';
|
||||
end;
|
||||
end;
|
||||
|
||||
end.
|
||||
Reference in New Issue
Block a user