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-коэффициент за всё расследование так и не был покручен. У детерминированного дефекта есть причина, а не настройка.