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

Лабораторная 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.

Второй шаг: хранить оценку между кадрами (issue #66)

Текущее среднее исправило бит 189, но оно по-прежнему начинается заново в начале каждого кадра. К преамбуле оно успевает увидеть всего несколько сотен отсчётов, а дифференциальная преамбула Лабораторной 11.45 повторяет один символ, поэтому заметная часть вычитаемой там «постоянной составляющей» на деле сама преамбула. Два измерения на стенде из двух плат в октябре 2026 года показали, чего это стоит (отчёт):

  • с включённым DC-блокером 5-6 % кадров не захватывались, и одиночных битовых ошибок было больше, чем без него;
  • после резкого спада уровня на 20 dB в середине кадра смещение, выученное на сильном сигнале, составляло большую часть слабого, и практически каждый такой кадр терялся (35 сбитых кадров из 279; без блокера 4).

Исправление хранит оценку от кадра к кадру вместо сброса: у qpsk_zynq_ber_top теперь DC_PERSIST = 1 и постоянная времени 2^13 отсчётов. Отсчёты приходят в приёмник только во время приёма, поэтому между приёмами оценка просто хранится, а одна преамбула теперь составляет малую часть среднего. dc_blocker.v не изменился; изменился только момент его сброса.

Сначала был испробован перезапуск среднего при обнаружении спада уровня. Он помогал одиночному приёму сразу после сброса, но когда оценка стала храниться между кадрами, он ухудшал результат (56 захваченных кадров из 80 вместо 62 в симуляции): каждый перезапуск возвращает короткое окно, которое снова учит следующую преамбулу.

На железе, по 300 кадров (отчёт): с включённым DC-блокером на постоянном уровне захватываются 300 кадров из 300 (было 284), спад на 20 dB стоит 7 сбитых кадров вместо 35, а дифференциальная радиолиния между платами приняла 200 кадров из 200 без ошибок. На этом проводном стенде из двух плат приёмник без блокера всё ещё немного лучше (2 сбитых кадра на спаде). Путь self-OTA с одной платой, ради которого блокер существует, на железе заново не измерялся.

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

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

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

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