Перейти к содержанию

Лабораторная 11.35 — парный timing A/B и фазоинвариантный Gardner TED

Lab 11.34 оставила неоднозначный результат: continuous Gardner принимал намного больше полных кадров, но в sweep из 288 попыток получил на один чистый кадр меньше fixed sampler. Здесь мы повышаем статистическую мощность, убираем медленный дрейф стенда из сравнения и по результату находим дефект первого детектора, зависящий от фазы несущей.

Стенд и правила безопасности

  • плата A: vendor cyclic-DMA transmitter, TX1;
  • плата B: course receiver, RX1;
  • тракт: A TX1 -> аттенюатор 30 дБ -> B RX1;
  • несущая 915 МГц, TX −30 дБ, RX +50 дБ;
  • оба timing-режима работают в одном bitstream и отличаются только gp_ctrl[14];
  • до и после запуска оба TX-канала принудительно переводятся в −89,75 дБ.

Это только кабельный эксперимент. Не запускайте его без аттенюатора и проверки направления портов.

Зачем нужно парное сравнение

Runner Lab 11.35 наблюдает fixed и Gardner попарно при одинаковых CFO и start offset. Порядок чередуется AB, BA, поэтому медленный RF- или тепловой дрейф не помогает постоянно режиму, который выполняется первым.

Gate объявлен до сбора данных. Для разницы Gardner минус fixed:

  • нижняя граница 95%-го CI clean-attempt должна быть не хуже −2 процентных пунктов;
  • нижняя граница 95%-го CI full-frame-lock должна быть не меньше +10 пунктов.

Все no-lock остаются в знаменателе. Интервал считается по discordant pairs — парам, в которых режимы дали разные исходы.

python blocks/block_11_integrated_sdr_project/python/lab_11_35_paired_timing_ab.py `
  --cfo-values 0,30000,55000 `
  --offsets 0,1,2,3,4,5,6,7 `
  --pairs-per-offset 50 `
  --json-out docs/assets/lab1135_paired_timing_ab_live_20260721.json `
  --png-out docs/assets/lab1135_paired_timing_ab_live_20260721.png

Runner сохраняет checkpoint полного per-attempt JSON и не разрешает продолжить его с несовместимыми параметрами. В evidence входят SHA-256 bitstream, revision репозитория, RF-настройки, порядок пары, телеметрия режимов и заранее заданные margins.

Парный результат axis-sign detector

Кампания содержит 1200 пар, то есть 2400 попыток приёмника:

Метрика Fixed Axis-sign Gardner Парная разница 95% CI
Full-frame lock 774/1200 (64,50%) 928/1200 (77,33%) +12,83 п.п. +9,27…+16,40 п.п.
BER=0 attempt 292/1200 (24,33%) 274/1200 (22,83%) −1,50 п.п. −4,97…+1,97 п.п.

Точечные оценки удовлетворяют обоим margins, но нижние границы интервалов — нет. Кандидат не продвигается. Это не доказательство, что Gardner хуже; честный вывод по объявленному gate — статистически неопределённый результат.

Общие числа скрывают важную подсказку:

Внесённый CFO Fixed BER=0 Axis-sign Gardner BER=0 Clean delta Lock delta
0 Гц 118/400 12/400 −26,50 п.п. +10,25 п.п.
+30 кГц 93/400 109/400 +4,00 п.п. +13,50 п.п.
+55 кГц 81/400 153/400 +18,00 п.п. +14,75 п.п.

При нулевом CFO контур часто сообщает полный кадр, но только 12 из 400 попыток чистые. При вращающейся несущей тот же контур непрерывно улучшается. Timing recovery не должен принимать лучше из-за residual carrier rotation, поэтому зависимость указывает на TED, а не на PI tuning.

Причина

Первая multiplier-free аппроксимация считала знаки I и Q независимо:

e_old = sign(sign(I_mid) * sign(I_now - I_prev)
           + sign(Q_mid) * sign(Q_now - Q_prev))

Она дешева, но не инвариантна к общему повороту созвездия. Статическая carrier phase меняет частоту ничьих между осями и effective detector gain. Residual CFO скрывает дефект, проводя созвездие через множество ориентаций.

Исправленный детектор берёт знак скалярного произведения комплексных векторов:

e_new = sign(I_mid * (I_now - I_prev)
           + Q_mid * (Q_now - Q_prev))

Для общего ортогонального поворота R выполняется (R a) · (R b) = a · b, поэтому timing decision не зависит от carrier phase. Цена — два DSP multiplier. Выход остаётся трёхуровневым, так что остальная арифметика PI и telemetry contract не меняются.

Проверка модели и RTL

Fixed-point модель сохраняет прежний TED как regression oracle. В статическом sweep carrier phase 0…90° старый detector даёт ошибки, новый остаётся на нуле.

Выбраны коэффициенты K1=256/65536 и K2=3/65536. Три случайных потока по 12 000 символов проходят без ошибок при ±100 ppm и ±200 ppm sample-clock mismatch для всех фаз с шагом 5°. Проходят также короткие stress cases ±0,75% SPS и сохранённый hardware capture +30 кГц.

Все 34 canonical HDL smoke tests зелёные, включая standalone bit-exact timing recovery, полную QPSK chain, runtime mux и retained-capture replay.

Физическая реализация

Первый route dot-product TED завершился при WNS=-7,749 нс, хотя hold был чистым. Полный отчёт показал требование 8 нс и endpoint ted_dot_*_psdsp/D: Vivado создал регистр на входе DSP во время placement, уже после того, как pre-opt XDC-hook перечислил обычные D/R/S pins timing-recovery.

Мы не увеличивали multicycle budget. В тот же setup/hold 4/3 добавлены A/B/C data pins DSP48 только внутри timing-recovery hierarchy, поэтому exception следует за преобразованием placement. Покрытие выросло с 364 до 1364 pins. Свежая реализация ExtraNetDelay_high / AggressiveExplore / NoTimingRelaxation закрылась при:

  • WNS=+0,004 нс, TNS=0,000 нс;
  • WHS=+0,017 нс, THS=0,000 нс;
  • 77 638/77 638 routable nets, routing errors 0;
  • bitgen без errors и critical warnings;
  • SHA-256 907a277cd0906ae228b09414930a6a7e8e56efbbe73e6e56953139f25bd1b80d.

Размещённый дизайн использует те же 192/220 DSP48E1: старые Verilog sign products также были выведены Vivado в два DSP, поэтому замена меняет функцию и timing, но не итоговое число DSP.

Плата B чисто загружается точным образом, инициализирует AD9361, показывает три IIO device и возвращает course core ID 0x4250534B.

Прицельный результат при нулевом CFO

Следующая заранее заданная проверка выполнялась в точке провала старого detector. Кампания из 400 пар дала:

Метрика Fixed Dot-product Gardner Парная разница 95% CI
Full-frame lock 247/400 (61,75%) 354/400 (88,50%) +26,75 п.п. +21,11…+32,39 п.п.
BER=0 attempt 108/400 (27,00%) 11/400 (2,75%) −24,25 п.п. −28,84…−19,66 п.п.
Aggregate BER по полным кадрам 0,130682 0,067645

Фазоинвариантный detector резко улучшает acquisition и вдвое уменьшает aggregate BER, но не убирает провал exact-zero frames. Carrier-phase diagnosis нашёл настоящий дефект модели, но не полную причину live-проблемы. Focused gate отвергает кандидат, поэтому трёхточечный CFO campaign уже не может изменить promotion decision и намеренно не запускается.

Распределение ошибок даёт следующую подсказку:

Максимум bit errors в полном кадре Fixed frames Dot-product Gardner frames
0 108 11
1 135 117
2 146 143
3 157 163
5 161 202
10 177 288

Исправленный loop часто не доходит до exact zero всего на один bit, но обгоняет fixed при пороге три ошибки и сильно выигрывает при пяти. Прежний QPSK register map выдавал payload errors как константный ноль, поэтому evidence не показывает, находятся ли одиночные ошибки в acquisition preamble или payload.

Lab 11.35 добавляет настоящую QPSK payload-error telemetry в уже существующую младшую половину error-count register. Отдельный RTL-тест вносит одну допустимую ошибку preamble и одну payload error и получает соответственно (total=1,payload=0) и (total=1,payload=1). Следующий короткий hardware run — эксперимент с инструментированием, а не переинтерпретация gate.

Live-локализация ошибок payload

Для telemetry image понадобился один ограниченный post-route шаг после того, как первая реализация остановилась на WNS=-0,049 нс. AggressiveExplore улучшил тот же routed checkpoint без изменения RTL и constraints. Прошитый в плату B образ закрывается при:

  • WNS=+0,009 нс, TNS=0,000 нс;
  • WHS=+0,028 нс, THS=0,000 нс;
  • 77 735/77 735 полностью разведённых routable nets, routing errors 0;
  • bitgen без errors и critical warnings;
  • SHA-256 29fb47e06a255ab6829af03d099d09dd29e569e62a9a46a37dfd35941c4527ca.

После clean boot снова получены core ID 0x4250534B, успешная инициализация AD9361 и три IIO device. Прицельный instrumentation run при CFO=0 содержит 160 чередующихся пар:

Метрика Fixed Dot-product Gardner Парная разница 95% CI
Full-frame lock 98/160 (61,25%) 146/160 (91,25%) +30,00 п.п. +21,00…+39,00 п.п.
BER=0 attempt 44/160 (27,50%) 4/160 (2,50%) −25,00 п.п. −32,57…−17,43 п.п.
Aggregate BER по полным кадрам 0,109803 0,068224
Aggregate payload BER 0,117985 0,073523

Разбиение отвечает на диагностический вопрос:

Класс ошибок полного кадра Fixed Dot-product Gardner
Dirty full frames 54 142
Только preamble 3 0
Только payload 27 122
Preamble и payload 24 20
Однобитовые dirty frames 15 55
Единственная ошибка в preamble 1 0
Единственная ошибка в payload 14 55

Все 55 однобитовых промахов Gardner находятся в payload. Значит, acquisition preamble не создаёт провал exact-zero. Сам payload длиннее preamble, поэтому одни raw counts были бы слабым аргументом; важнее, что Gardner payload BER равен 0,073523, а preamble BER — лишь 0,011701. Приёмник находит хороший старт кадра, после чего качество решений падает внутри кадра.

Следующая ограниченная диагностика — позиция ошибки внутри payload: например, четыре segment counter и индексы первой/последней ошибки. Скопление к концу укажет на остаточный timing/carrier drift, равномерное распределение — на decision margin или noise. Этот результат не обосновывает изменение acquisition и ещё один слепой sweep PI gains.

Позиционная телеметрия ошибок payload: реализована, timing-clean, live pending

Заранее объявленная диагностика теперь есть в RTL и host и имеет timing-clean образ. Но она ещё не запускалась на стенде — стенд на момент написания выключен, поэтому здесь честно разделены реализованное и симулированное от измеренного.

RTL и host — реализовано. При gp_ctrl[4]=1 и gp_ctrl[15]=1 существующий register map несёт позиционную телеметрию без изменения block design и адресной карты, поверх кадра 280 бит (24 бита preamble, 256 бит payload, четыре четверти payload по 64 бита):

  • gp_tx_valid_count = {quarter3, quarter2, quarter1, quarter0} — четыре насыщаемых 8-битных счётчика ошибок по четвертям payload;
  • gp_rx_valid_count = {last_payload_error_index[15:0], first_payload_error_index[15:0]}, где 0xFFFF означает «ошибок нет».

Отдельный bridge-тест вносит ошибки в известные позиции и проверяет счётчики четвертей и sentinel’ы first/last; host возвращает payload_error_position и агрегирует availability, четыре суммы по сегментам и статистику индексов first/last. FIXED_MODE и GARDNER_MODE включают gp_ctrl[15].

Симуляция и регрессия — зелёные. Canonical HDL smoke 34/34; targeted Python-тесты QPSK digital-loopback и paired-timing проходят. RRC transmit FIR был конвейеризован, а не ограничен constraint’ом: его реальный critical path (delay line → symmetric pair-add → materialized DSP input register) не допускает multicycle exception, потому что внутренний fabric loopback может подавать valid каждый такт, и ложный exception испортил бы непрерывный случай. Симметричные pre-adds и center tap теперь регистрируются до умножителей (FIR latency 9 вместо 8), а преобразование bit-exact на 2312 векторах.

Timing-clean образ — получен. С конвейеризованным FIR новый worst path находится вообще не в модеме, а в диагностическом capture peak detector (capture_nonzero_seen_* → capture_peak_abs_sample_reg[*]/CE) — route-heavy телеметрический путь. Свежая main-seed реализация закрылась на WNS=-0,145 нс; placement ExtraNetDelay_high из vendor post-opt snapshot довёл до WNS=-0,076 нс; один разрешённый post-route phys_opt_design -directive AggressiveExplore на том же routed checkpoint — без изменения RTL и constraints — закрыл timing:

  • WNS=+0,020 нс, TNS=0,000 нс (0 failing endpoints);
  • WHS=+0,039 нс, THS=0,000 нс (0 failing endpoints);
  • 80 546/80 546 полностью разведённых routable nets, routing errors 0;
  • bitstream записан; SHA-256: 2d7ed04d79a180f6d3a4e97fab5a7b52a5d028b7705c04f79ecdac3be0834296.

Маржа +0,020 нс тонкая и достигнута post-route оптимизацией на диагностическом пути, поэтому честная оговорка — стабильность именно этого закрытия по repeat-build/seed; конвейеризация peak detector остаётся вариантом, если нужна бо́льшая, независимая от placement маржа. Прежний pre-telemetry образ (SHA-256 29fb47e0…, источник 160-парного результата выше) теперь устарел для position experiment и не должен использоваться.

Live position experiment — измерен, и ни одна заранее объявленная гипотеза не подтвердилась.

Сначала пришлось добавить guard. gp_ctrl[15] мультиплексирует два регистра, которые образ без телеметрии по-прежнему гонит в старом смысле (счётчик TX-отсчётов и счётчик RX valid), а хост декодировал их безусловно. На старом образе чистый кадр (payload_errors=0) выдал segment_errors=[224,4,0,48], first=1843, last=0, а кадры со 121 и 2 ошибками payload — те же самые счётчики сегментов: кампания в таком состоянии дала бы уверенный мусор. decode_payload_error_position() теперь проверяет показания по собственному счётчику ошибок кадра (счётчики четвертей не насыщаются, поэтому сумма обязана точно равняться payload_errors, а индексы — удовлетворять 0 ≤ first ≤ last < 256 либо оба быть sentinel) и иначе возвращает None. Проверено на стенде: тот же зонд даёт None на старом образе и настоящие сентинелы ([0,0,0,0], оба индекса None) на новом.

Измеренный прогон — 160 чередующихся пар при CFO=0 на timing-clean образе (SHA-256 2d7ed04d…), восемь start offsets × 20 пар:

Метрика Fixed Dot-product Gardner Парная разница 95% CI
Full-frame lock 87/160 (54,38%) 133/160 (83,13%) +28,75 п.п. +20,17…+37,33 п.п.
BER=0 attempt 36/160 (22,50%) 3/160 (1,88%) −20,62 п.п. −27,38…−13,87 п.п.
Aggregate BER по полным кадрам 0,106568 0,068018
Aggregate payload BER 0,113820 0,073132

Счётчики четвертей не дали ни одной из предсказанных форм: ни монотонного роста к концу payload, ни равномерности:

Четверть payload Fixed Gardner
q0 (биты 0–63) 530 (20,9%) 492 (19,8%)
q1 (биты 64–127) 779 (30,7%) 529 (21,2%)
q2 (биты 128–191) 713 (28,1%) 798 (32,0%)
q3 (биты 192–255) 513 (20,2%) 671 (26,9%)

Резкий ответ — в однобитовых кадрах, где first и last совпадают с самой ошибкой:

Кадры с одной ошибкой payload Fixed Gardner
Количество 7 33
В q0 / q1 / q2 / q3 0 / 0 / 7 / 0 2 / 0 / 31 / 0
Индекс ошибки 173, затем 189 × 6 2, 2, затем 189 × 31

37 из 40 однобитовых промахов приходятся ровно на payload-бит 189, независимо у обоих семплеров. Этот индекс — кадровый бит 213, то есть Q-ось QPSK-символа 106, последнего символа в Q-серии длиной четыре перед сменой знака. Согласованно: агрегатный first_error_index ни у одного семплера не превышает 189, а диапазон ошибок покрывает бит 189 в 90% dirty-кадров fixed и 98% у Gardner. Кадры фактически бимодальны: либо полностью чистые, либо неверные на 189.

Значит систематический однобитовый промах — это не within-frame drift (нет монотонного роста) и не равномерная потеря decision margin (нет равномерности). Это один символ, решаемый на границе. Честная сила доказательств разная: локализация по однобитовым кадрам прямая, а 90/98% — это покрытие диапазоном (first ≤ 189 ≤ last) и потому слабее.

Очевидный механизм был проверен и опровергнут. Естественное объяснение — ISI от паттерна «серия, затем переход», оставляющий Q-отсчёт символа 106 около нуля, а момент Gardner сдвигает его за границу — вычисляется оффлайн, потому что и кадр, и курсовой RRC детерминированы. Согласованная пара RRC даёт Найквиста, поэтому в идеальный момент каждое решение имеет полный запас (проверено: все 280 бит верны, минимум |Q| 0,972 от номинала, символ 106 — 0,976 против медианы кадра 0,998, ничем не выделяется). ISI появляется только при сдвиге семплера, поэтому осмысленный рейтинг — какой бит переворачивается при наименьшем возмущении:

Возмущение Первые переворачивающиеся payload-биты Бит 189
Сдвиг тайминга 70, 22, 68, 135, 137 при 2,69–2,71 отсчёта (≈0,34 символа) не переворачивается в пределах ±0,5 символа (ранг 121 из 124 переворачивающихся)
Фаза несущей 74, 244, 24, 28, 251 при 43,60–43,85° 44,15° — ранг 14 из 256, то есть ничем не выделяется
Сетка timing × phase 89 (19 точек), 68 (17), 24, 88, 137 бывают единственным отказавшим битом 0 точек сетки

Фаза несущей не дискриминирует вообще: все 256 payload-битов переворачиваются в узкой полосе примерно 43,6–45° вокруг теоретической границы QPSK, поэтому фаза не выделяет ни один бит, и 189 лежит внутри этой полосы совершенно обычно. Тайминг дискриминирует — и оправдывает бит 189: он доживает до границы в полсимвола, тогда как биты 70, 22 и 68 падают на трети символа. Решающая же — комбинированная сетка: бит 189 бывает единственным отказавшим битом в нуле точек, а биты 89 и 68 — в 19 и 17. Живьём тот же бит забирает 37 из 40 однобитовых промахов. Значит переданный сигнал при ошибках тайминга и несущей это не объясняет, и гипотеза ISI снимается.

Расчёт лежит в blocks/block_11_integrated_sdr_project/python/frame_bit_timing_sensitivity.py и железа не требует.

Поиск ограничивают ещё два факта. Fabric loopback (тот же модем, тот же эталон, без RF) декодирует кадр с payload_errors=0 и чистыми сентинелами — значит ROM кадра, BER-счётчик и кодирование позиций исправны. А RF-тракт отличается от этого loopback ровно двумя вещами: передатчик — host-поток, а не fabric-модулятор, и сигнал проходит приёмный тракт AD9361 (DC block, coarse CFO, Costas, phase picker). Причина — в одном из этих двух, а не в данных кадра и не в геометрии канала.

Что на самом деле отдал AD9361

Мост уже пишет core_rx в BRAM на 4096 отсчётов на каждом бёрсте, и её можно вычитать через gpreg (gp_ctrl[7]=1, адрес в gp_start_offset, слово {rx_i, rx_q} из gp_capture_debug) — ровно тот крючок «увидеть, что отдаёт аналоговый тракт против заведомо верного TX». Две методические ловушки стоит записать, потому что обе сначала выдали уверенную чушь:

  • кадр лежит в буфере на плавающем смещении (измеренные старты 844, 917, 2649, 2876), поэтому короткое окно чтения его просто не застаёт: при окне 1536 отсчётов получались «выравнивания» с |corr| ≈ 2 и абсурдными многокилогерцовыми оценками частоты;
  • оценивать CFO на каждом кандидате до корреляции хуже, чем сначала коррелировать, а потом подгонять фазовую прямую: на 140 символах оценка 4-й степенью достаточно шумная, чтобы разрушить корреляцию в истинной точке, и тогда выигрывает неверный старт.

С чтением всего буфера и выравниванием «корреляция → подгонка» каждый экземпляр кадра садится чисто при |corr| ≈ 11,7 и CFO −300…−410 Гц, что совпадает с известным межплатным интринсиком. Для бёрста, чья телеметрия на кристалле сообщила ровно одну ошибку payload на бите 189:

Захват Экземпляр |corr| CFO Ошибки решений на хосте Q-запас символа 106
падающий бёрст старт 844 11,66 −305,9 Гц 0 +0,725 (ранг 94/140)
падающий бёрст старт 2876 11,38 −411,7 Гц 2 (обе в других символах) +0,741 (ранг 114/140)
чистый бёрст старт 917 11,66 −301,9 Гц 0 +0,674 (ранг 37/140)
чистый бёрст старт 2649 11,65 −299,1 Гц 0 +0,710 (ранг 75/140)

Q-решение символа 106 здорово во всех экземплярах — всегда положительное, от +0,67 до +1,16, в середине-верху рейтинга запасов кадра (медиана ≈ +0,71). Даже в том единственном экземпляре, где хост-декодер всё же делает две ошибки, символа 106 среди них нет. Значит в сигнале, который отдал AD9361, никакого пограничного решения на этом символе нет.

Честная оговорка: хост-декодер идеализирован — data-aided выравнивание плюс линейная подгонка фазы — и потому сильнее причинного Costas и фиксированного семплера на кристалле, так что чип вполне может ошибаться там, где этот декодер не ошибается. Но просто более слабый приёмник должен падать на самых слабых решениях, а это совсем другие символы. Здоровое решение из середины рейтинга, объявленное единственной ошибкой, указывает на собственную обработку приёмника или на способ приписывания индекса, а не на качество сигнала. Стоит отметить и то, что индекс позиции вообще проверялся реальной ошибкой только на RF-тракте: fabric loopback ошибок не даёт, поэтому аппаратная валидация самого индекса опирается на те же RF-числа, которые с его помощью и трактуются.

Отказ не сдвигается

Две дешёвые проверки инвариантности закрывают аналоговую ветку целиком. Выравнивание кадра его не двигает: в 160-парной кампании однобитовая ошибка сидит на индексе 189 на всех восьми start offsets (Gardner 0–7, fixed на 0, 5 и 7). Несущая — тоже: по 100 Gardner-бёрстов на каждой из трёх частот оставляют распределение нетронутым:

Несущая Locked Dirty Однобитовых кадров Индексы Доля бита 189
910 МГц 100/100 90 30 189 × 30 100%
915 МГц 100/100 89 27 189 × 26, 2 × 1 96%
920 МГц 99/100 87 31 189 × 30, 2 × 1 97%

Смена несущей меняет LO, смещение постоянной составляющей, характеристику фильтров и рабочую точку АРУ, оставляя данные кадра и индексацию битов нетронутыми, — и отказу это безразлично. Вместе с измерением по capture-tap (здоровый запас решения из середины рейтинга) исключены аналоговый тракт, канал и выравнивание кадра. Дефект привязан к данным или к индексу, и остаётся ровно одна развилка: либо декодер на кристалле действительно ошибается на бите 189 по детерминированной причине, либо сообщаемый индекс — не тот бит, который на самом деле упал.

Спрашиваем у декодера напрямую

Эта развилка была не «пока не отвечена», а неотвечаема: мост жёстко зануляет свой отладочный поток битов в режиме QPSK, то есть видимости декодированных битов у QPSK-тракта не было вовсе. Теперь их выдаёт gp_ctrl[16] — каждый восстановленный дибит сдвигается в 288-битный регистр, который замирает по завершении бёрста и читается девятью словами плюс счётчик (см. decoded_bit_readout.py и lab_11_40_resolve_decoded_bit_index.py).

Два решения здесь несущие. Сдвиговый регистр, а не адресуемая запись: адресный декодер в домене sample_clk не по карману при марже +0,020 нс. И заморозка в конце бёрста, а не захват с его начала: сэмплер выдаёт RX_SAMPLE_MARGIN=256 лишних символов, коррелятор находит кадр где-то внутри потока, и первые 288 бит — это предкадровый шум.

Сравнение тоже требует аккуратности. BER-счётчик сравнивает с ROM не сырые дибиты: он гоняет две ветки frame-sync (A отдаёт {d0,d1}, B — деротацию на 90° {d1,~d0}), и каждая сама разрешает свою 180-градусную неоднозначность, то есть возможны четыре поворота созвездия, тогда как readout снимает сырые дибиты. Сравнение только по тождественному повороту делало 7 бёрстов из 10 «наполовину неверными» (121–129 несовпадений — подпись неправильного сравнения, а не сломанного декодера); перебор всех четырёх превратил 3/10 в 10/10, и встретились все четыре (A ×3, A+invert ×2, B ×1, B+invert ×4).

Результат однозначен. В 10 случаях из 10, где телеметрия сообщила одну ошибку payload на индексе 189, декодированный кадр расходится с ROM ровно в одном бите — кадровом 213, то есть payload 189, — и ни в каком другом. Три чистых кадра не расходятся нигде.

Позиционная телеметрия верна, и приёмник действительно выдаёт неверный бит на Q-оси символа 106. Отдельно про базы индексов: выравнивание возвращает кадровые индексы, поэтому кадровый бит 213 — это и есть payload 189; прямое сравнение этих двух чисел однажды напечатало вывод, прямо обратный данным.

Это переосмысливает и прежнее решение о продвижении. Clean-frame gate, отвергающий Gardner, в значительной мере является прокси для одного бита: Gardner захватывает намного чаще (133 против 87) и несёт заметно меньший payload BER (0,0731 против 0,1138), но достигает BER=0 лишь 3 раза из 160, потому что почти всегда промахивается на бите 189. Gate остаётся объявленным gate, а gp_ctrl[14]=0 — значением по умолчанию, но «Gardner не умеет давать чистые кадры» точнее звучит как «Gardner не попадает в символ 106».

Evidence: docs/assets/lab1135_payload_position_live_20260723.json.png).

Вердикт

Dot-product TED корректен в модели, закрывает timing и проверен на железе, но решительно не проходит focused clean-frame gate. gp_ctrl[14]=0 остаётся runtime default, fixed sampler — принятым baseline.

Позиционная телеметрия ответила на вопрос, ради которого её строили, и ответ не совпал ни с одним из заранее объявленных вариантов. Систематические однобитовые промахи не размазаны по payload и не копятся к его концу: 37 из 40 однобитовых кадров падают ровно на payload-бите 189 — Q-оси QPSK-символа 106 — независимо у обоих семплеров, а кадры бимодальны между «полностью чистый» и «неверный на 189». И within-frame drift, и однородная потеря decision margin отвергнуты; цель — один символ, решаемый на границе.

Это уточняет и то, что мерил отвергнутый gate. Gardner захватывает намного чаще (133/160 против 87/160) и несёт меньший payload BER (0,0731 против 0,1138), но достигает BER=0 три раза из 160, потому что почти всегда промахивается по одному биту. Gate остаётся в силе, но вывод «Gardner не умеет давать чистые кадры» точнее звучит как «Gardner не попадает в символ 106».

Очевидный механизм уже проверен и снят: оффлайн, на детерминированных кадре и RRC, бит 189 не переворачивается в пределах ±0,5 символа сдвига тайминга (тогда как биты 70, 22 и 68 падают на трети символа) и является единственным отказавшим битом в нуле точек сетки timing × phase, где биты 89 и 68 отказывают в 19 и 17 точках. Фаза несущей не дискриминирует ничего — все 256 payload-битов переворачиваются в полосе ~43,6–45° вокруг границы QPSK. Переданный сигнал при ошибках тайминга и несущей живой результат не объясняет.

Оставшаяся развилка с тех пор закрыта чтением собственного выхода декодера (gp_ctrl[16]): в 10 случаях из 10 декодированный кадр расходится с ROM ровно на кадровом бите 213 — payload 189 — и нигде больше, а чистые кадры не расходятся вовсе. Позиционная телеметрия верна, и приёмник действительно выдаёт неверный бит на Q-оси символа 106.

Мишень стала узкой и воспроизводимой: детерминированно неверное решение на одном символе, у которого принятый запас здоров, при независимости от start_offset и несущей, и при полностью оправданных ROM кадра, BER-счётчике и кодировании индекса. Остался участок между хорошим отсчётом и плохим решением — Costas, семплер, решающее устройство. Sweep PI-коэффициентов по-прежнему не оправдан: у детерминированного дефекта есть причина, а не настройка.

Всё это несёт timing-clean образ SHA-256 2d7ed04d… (WNS=+0,020 нс); хост теперь отказывается сообщать позиции с образа, который их не реализует, поэтому подделать такой результат устаревшим битстримом больше нельзя.