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

Lab 11.41 — DC-блокер следил за данными

Цель

Понять, почему приёмник детерминированно выдаёт неверный бит на payload-индексе 189, хотя все измерения доставленного сигнала на этом символе говорят, что он в порядке, — и починить.

Предыстория: Lab 11.35, где отказ был локализован и доказано, что сообщаемый индекс верен.

В чём была ошибка предыдущих пяти лаб

Состояние на конец Lab 11.35 было настоящим парадоксом. Чтение декодированных битов установило, что кристалл действительно ошибается на payload 189 — 10 кадров из 10 расходятся с ROM ровно на кадровом бите 213 и нигде больше. При этом capture-tap показывал здоровый запас решения на этом символе, оффлайн-модель говорила, что переданная волна не роняет этот бит первым, а сам отказ не зависел ни от start_offset, ни от несущей.

Разгадка не в этих измерениях. Она в том, что именно они измеряли.

Capture-tap пишет core_rxвход приёмной цепочки. DC-блокер стоит первым блоком после него. Любой результат вида «сигнал здесь в порядке» слеп к DC-блокеру по построению, а не случайно.

Инструмент был сделан, чтобы отвечать на вопрос что доставляет AD9361. А использовался пять лаб подряд, чтобы отвечать на вопрос что видит решающее устройство. Это разные вопросы, и разница между ними — ровно тот блок, который и портил приём.

Почему DC-блокер объясняет всё сразу

Как только подозреваемый назван, факты, у которых не было другого объяснения, выстраиваются разом:

Наблюдение Почему это следует из DC-блокера
независимость от фазы и частоты несущей (910/915/920 МГц одинаково) фильтр линеен и одинаков по I и Q, поэтому dcb(x·e^{jθ}) = dcb(x)·e^{jθ} — искажение поворачивается вместе с сигналом
fabric-loopback декодирует тот же кадр при BER 0 gp_ctrl[9] включает блокер только на RF-тракте; в петле он выключен
capture-tap показывает здоровый запас на символе 106 он там и правда здоровый — порча происходит дальше по цепочке
детерминированность, кадр за кадром искажение есть функция данных кадра, а они фиксированы
независимость от start_offset передатчик крутит кадр циклически, оценка в циклическом установившемся режиме

Механизм — постоянная времени. Блокер это «протекающий интегратор», аккумулятор которого хранит mean << K; исходное фиксированное K=6 даёт tau = 64 отсчёта = 8 символов при SPS=8. Этого достаточно, чтобы следить за модуляцией: серия одинаковых символов по одной оси утягивает оценку на себя, и больше всех теряет отсчёт в конце серии.

Символ 106 — последний в серии из четырёх одинаковых Q прямо перед сменой знака.

Измерение

dc_blocker_margin.py бит-точно повторяет RTL на реальном кадре и ранжирует все решения по запасу.

cd blocks/block_11_integrated_sdr_project/python
python dc_blocker_margin.py
Конфигурация запас на бите 189 ранг в кадре
без блокера, вход без DC (fabric-loopback и идеал) +0,976 17 из 256
фиксированное K=6 — конфигурация, которая отказывает +0,336 1 из 256
растущее среднее, K_MAX=10 — текущий модуль +0,913 23 из 256

Блокер съедает две трети запаса и превращает ничем не примечательное 17-е решение в самое слабое из всех 256 — 0,336 против медианы 0,99.

Проверка, которая делает это причиной, а не историей

Четыре предыдущих объяснения тоже были правдоподобны. Отличает их тест: предсказывает ли модель всю живую гистограмму, а не только бит 189. За кампанию одиночные ошибки легли на три индекса и ни на один другой:

payload-индекс живых кадров ранг по модели
189 37 1 из 256
2 2 3 из 256
173 1 23 из 256

Все три лежат в предсказанном слабом хвосте, и порядок совпадает с частотой отказов. Ничем другим в кадре эти три бита не выделяются.

Предупреждение всё это время стояло в комментарии самого модуля — «K=4 всё ещё отслеживает модуляцию и ошибается; K≥6 чисто». «Чисто» было проверено на одном захвате с хорошим SNR, где запаса 0,336 хватает на нулевую ошибку. У двухплатного канала запаса меньше. Комментарий описывал режим отказа абсолютно верно; неверным было допущение, что 6 — далеко от границы.

Первая правка неверна, и это стоит понять

Блокер сбрасывается на каждом кадре (qpsk_rx_bit_recovery_chain подаёт .rst(rst || frame_start)), то есть обязан сходиться с нуля внутри каждого бёрста. Это закрывает путь «просто увеличить K». Очевидный ответ — приём, который уже используется в qpsk_costas: быстрый захват, потом замедление, K_ACQ до схождения и дальше K_TRACK.

Синтетический стенд проходит, запас на бите 189 восстановлен. И при этом падает tb_qpsk_timing_recovery_retained — стенд на реальном двухплатном захвате.

Причину стоит запомнить. В том захвате настоящей DC почти нет: среднее по всем 2600 отсчётам — 4,5+1,1j при rms 377. Но среднее первых 192 отсчётов — 79+44j. На коротком окне доминирует локальный перекос самой модуляции, то есть окно захвата измеряет не ту величину, а последующее замедление замораживает эту ошибку в виде медленно спадающего смещения на весь кадр.

Быстрая неверная оценка исправляет себя сама. Медленная неверная — нет.

Попытка убрать пульсацию сделала бы её постоянной. Поймал это только стенд на реальных данных — ни модель, ни синтетический стимул не поймали.

Правка, которая работает

Оценка стала скользящим средним по всем отсчётам с момента сброса: K растёт как floor(log2 n) до K_MAX и только затем превращается в утечку с tau = 2^K_MAX. Пока K растёт, это в точности среднее всех n отсчётов — лучшая доступная оценка в каждый момент, причём модуляция усредняется тем сильнее, чем шире окно. Нет ни произвольного момента переключения, ни пульсации, которую можно заморозить.

Аккумулятор хранит mean << K, поэтому на каждом шаге его надо пересчитывать сдвигом влево на единицу. Иначе оценка будет ополовиниваться на каждом шаге.

Проверка

python tools/run_block5_hdl_smoke.py --test tb_dc_blocker_gear
python tools/run_block5_hdl_smoke.py          # весь пакет: 35/35

tb_dc_blocker_gear держит модуль против поведенческого двойника исходного алгоритма с фиксированным K, а не против идеала, — сравнение идёт с кодом, который реально работал. Три численные проверки:

  • впрыснутая DC по-прежнему снимается (остаток 0 при впрыснутых 5000);
  • слежение за данными в установившемся режиме минимум вчетверо меньше — измерено 15,8× при теоретических 2⁴ = 16×, и само это совпадение служит подтверждением, что механизм понят верно;
  • оценка лучше исходной и на первом кадре. Эта проверка существует специально ради того, чтобы регресс с переключением по счётчику не вернулся молча.

На сохранённом двухплатном захвате число чистых фаз семплера выросло с 1 из 8 до 3 из 8.

Один тест был ослаблен — осознанно

tb_qpsk_phase_picker требовал, чтобы два реальных захвата декодировались чисто на совпадающих наборах фаз семплера. После правки обе маски расширились, а краевые фазы добавились асимметрично: четыре у одного захвата, три у другого.

Стало лучше обоим; сломалось только равенство. Равенство масок было хрупким прокси — оно держалось, пока оба захвата сидят на одинаковом запасе. Реальный контракт picker'а — снять зависимость от фазы прихода, то есть оба должны быть чисты на общем наборе с центром на offset 0; это и проверяется теперь. Проверка «с выключенным picker'ом маски расходятся» осталась нетронутой — без неё стенд не доказывал бы ничего.

Живая проверка на железе

Модель дала одно фальсифицируемое предсказание: на двухплатном канале с фиксированным блокером бит 189 забирал 37 из 40 одиночных ошибок; с растущим средним он должен перестать доминировать. Если бы он по-прежнему забирал большинство — модель неверна, как бы хорошо она ни ложилась на старые данные. lab_11_41_dc_blocker_live_validation.py пересобрал, перезалил (холодная загрузка, чтобы AD9361 откалибровался с нуля) и повторил кампанию через A TX1 → 30 дБ → B RX1:

фикс. K=6 (baseline) растущее среднее (эта сборка)
доля бита 189 в одиночных ошибках 37/40 = 92% 0/27 = 0%
чистых кадров Gardner 4/133 = 3% 275/300 = 92%
payload BER Gardner 7,3×10⁻² 2,6×10⁻²

Бит 189 исчез из гистограммы одиночных ошибок полностью, а продакшн-путь Gardner вырос с 3% до 92% чистых кадров. Предсказание подтвердилось: причиной был DC-блокер.

Остаточные одиночные ошибки теперь разбросаны (Gardner: по одной на битах 1, 9, 28, две на бите 2), а не свалены на один индекс — обычный шум, а не детерминированный дефект. Фиксированный семплер даёт небольшой кластер на бите 88, но это непродакшн-путь с известным дрейфом тайминга SPS=8,06, ради которого и делался Gardner (Lab 11.34), а не эффект DC-блокера. Результат: docs/assets/lab1141_dc_blocker_live_validation.json.

О чём эта лаба на самом деле

Пять лаб ушло на то, чтобы измерять верную величину в неверном месте. Инструмент был исправен, показания верны, а вывод из них — «сигнал здоров, значит виновата логика решения» — неверен, потому что во фразе не хватало слова где.

Когда измерение и симптом противоречат друг другу, схема между щупом и симптомом — тоже подозреваемый.

Ни один PI-коэффициент за всё расследование так и не был покручен. У детерминированного дефекта есть причина, а не настройка.