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

Лабораторная 11.26 — Runtime PL BPSK OTA: исправление DDS-bypass и первая проверка воздушного тракта

Текущий статус (заменяет промежуточные выводы ниже)

Эта страница сохраняет хронологию расследования. Итоговый результат:

Слой Текущее доказательство Статус
PL TX в ЦАП трассы мощности тона DMA и пакета BPSK доказывают, что мост доходит до ЦАП AD9361 подтверждено
RF-свидетель RTL-SDR наблюдает ожидаемую энергию пакета и форму спектра подтверждено
Внешний BER по RTL-SDR скважность пакета и синхронизация монитора пока не дают повторяемого результата BER неопределённо
Цифровой loopback BPSK на кристалле 281 принятый бит, ноль полных ошибок после исправления кадровой синхронизации подтверждено, но повторяемость фазы выборки остаётся за железом
Многофазный RX модель показывает, что детерминированный выбор возможен среди захваченных вариантов фазы подтверждено моделью, в RTL ещё не интегрировано

Не считайте наблюдения BER 35–50 % ниже текущим результатом модема: они сохранены потому, что каждое из них привело к конкретному исправлению моста, FIFO, тайминга или кадровой синхронизации. Следующие ворота для платы: фиксированная серия повторений при чистой загрузке, а не ещё один разовый прогон с BER=0.

Цель

Установить, почему сигнал runtime PL BPSK не обнаруживался внешним RTL-SDR несмотря на tx_valid_count > 0, применить исправление DDS-bypass и подтвердить первый OTA-кадр BPSK через тракт TX AD9361, управляемый PL.


Предпосылки

Все предыдущие захваты RTL-SDR (лабораторные 11.22–11.23) для всех попыток runtime PL BPSK стабильно показывали BER ≈ 35–40 % (почти случайный уровень) при EVM > 500 %, даже после:

  • ремонта AXI DDS (перепривязка cf_axi_dds + RATECNTRL = 3);
  • перепривязки драйвера АЦП;
  • точного поиска CFO в пределах ±12 кГц.

Диагностика корреляции преамбулы подтвердила, что сигнала BPSK нет:

  • захват стокового shell: отношение корреляции ≈ 5,2 (чёткий пик) → BER = 0;
  • захваты runtime PL: отношение ≈ 3,5 (ниже порога шума ≈ 4,7 для 60 000 позиций) → сигнала нет ни на одной грубой частоте.

Самый сильный спектральный пик на −316 кГц был опознан как помеха/паразитная составляющая: его полоса по уровню 3 дБ не совпадала с ожидаемой занимаемой полосой BPSK ≈ 648 кГц (480 кбит/с, rolloff 0,35).


Причина

После перезагрузки overlay через fpga_manager ядро DDS cf-ad9361-dds-core-lpc инициализируется в режиме DDS-only (поведение драйвера по умолчанию). В этом режиме путь данных AXI-Stream PL TX отключён от ЦАП AD9361: в ЦАП передаётся только тон DDS (по умолчанию с нулевой амплитудой или с последней заданной). Цепь PL BPSK выставляет tx_valid (его считает axi_gpreg), но эти отсчёты никогда не покидают логику FPGA.


Исправление

Вызывать disable_dds_tones(dds) после подключения к контексту IIO и до запуска BPSK. Функция записывает raw = 0 и scale = 0 во все выходные каналы DDS: тон заглушается, а аппаратный мультиплексор переключается на передачу данных AXI-Stream PL в ЦАП AD9361.

Изменённые файлы:

  • lab_11_19_runtime_bridge_txrx_self_timed_bringup.py: добавлены disable_dds_tones и аргументы перепривязки DDS/ADC;
  • lab_11_22_capture_runtime_pl_rtl_monitor_wav.py: добавлен disable_dds_tones.

Порядок выполнения

Предварительные условия

  • плата Zynq SDR по адресу 192.168.40.1, SSH root/analog;
  • RTL-SDR V3 Pro настроен на 915 МГц, усиление 20–40 дБ, запись в SDR++;
  • антенны TX и RX на расстоянии не более 3 м, без внешнего аттенюатора;
  • ослабление TX: −45 дБ (по умолчанию, безопасно для RF).

RF-безопасность: не увеличивать мощность TX ради того, чтобы «увидеть сигнал». Для первого OTA-обнаружения использовать минимальную мощность TX. Усиление RX это ручное, AGC выключена. Пакет короткий.

Шаг 1: запись WAV монитора RTL-SDR с DDS-bypass

# Запустите запись SDR++ на 915 МГц, 2,4 MS/s
# Затем в отдельном терминале:
python blocks/block_11_integrated_sdr_project/python/lab_11_22_capture_runtime_pl_rtl_monitor_wav.py \
    --rebind-runtime-dds-driver \
    --rebind-runtime-adc-driver \
    --runtime-dds-ratecntrl 3

Ожидаемый результат:

  • disable_dds_tones: {"status": "ok"} в выходном JSON;
  • tx_valid_count > 0 (цепь PL работает);
  • сигнал BPSK должен появиться вблизи DC (≈ +2,4 кГц, как у стокового shell).

Шаг 2: офлайн-анализ BER

python blocks/block_11_integrated_sdr_project/python/lab_11_20_read_rtl_wav_ota_bpsk_ber.py \
    --manifest datasets/lab11_22_runtime_pl_rtl_monitor/<new_manifest>.yaml \
    --run-tag dds_bypass_v1

Критерий успеха:

  • отношение корреляции преамбулы > 5,0 (сигнал обнаружен);
  • BER < 10 % → первый подтверждённый OTA BPSK через путь PL;
  • BER = 0 → полный успех (как у стокового shell).

Шаг 3: если после исправления DDS BER > 10 %

Симптом Вероятная причина Следующее действие
Сигнал у DC, BER 5–20 % остаточный CFO или низкое SNR увеличить усиление RTL; попробовать --fine-search-hz 20000
Сигнал у DC, BER > 30 % неверные параметры сигнала проверить symbol_rate_hz, samples_per_symbol в манифесте
Сигнала у DC нет, BER случайный DDS всё ещё блокирует проверить поле disable_dds_tones в JSON; снова попробовать --rebind-runtime-dds-driver
Сигнал не на той частоте несовпадение LO AD9361 проверить center_frequency_hz = 915000000 в IIO

Шаблон доказательства

После успешного прогона запишите следующие метрики в docs/assets/lab1126_runtime_dds_bypass_bpsk_ota_<timestamp>_metrics.json:

{
  "lab": "11.26",
  "date": "2026-XX-XX",
  "disable_dds_tones_status": "ok",
  "selected_coarse_frequency_hz": "...",
  "total_frequency_shift_hz": "...",
  "bit_errors_total": "...",
  "bit_errors_payload": "...",
  "ber_total": "...",
  "evm_percent": "...",
  "conclusion": "First confirmed OTA BPSK frame via PL TX path"
}

Дополнительная диагностика (2026-06-25)

Проверка пути данных FIFO→ЦАП

После исправления DDS-bypass следующая диагностика подтвердила весь путь данных FIFO→ЦАП синусоидальным тоном через DMA.

Тест тона DMA (voltage0/voltage1, 200 кГц на полной шкале ±32767):

# Correct IIO buffer write API (must use channel.write, not buf.read/modify):
voltage_chs = [ch for ch in dac.channels if ch.output and ch.id.startswith('voltage')]
buf = iio.Buffer(dac, N_BUF, cyclic=True)
payloads = [tone_i, tone_q, zero, zero]
for ch, samples in zip(voltage_chs, payloads):
    ch.write(buf, bytearray(samples.tobytes()), raw=True)
buf.push()
# Then set dac_data_sel=2 via devmem

Результат: тон 200 кГц виден на 87,6 дБ выше шумового пола RTL-SDR, мощность 1663 (против базовой 1,5). Путь FIFO→ЦАП подтверждён.

Найдена критическая ошибка использования API: условие 'voltage' in ch.id совпадает и с altvoltage0..7 (каналы управления DDS). Нужно использовать ch.id.startswith('voltage'), чтобы выбрать только четыре канала данных voltage0..3.

Инверсия частоты: тон появляется на −197 кГц, а не на +200 кГц это ожидаемо. Соглашение −200 кГц плюс биения LO AD9361/RTL-SDR +2,7 кГц дают −197,27 кГц.

Требование циклического нулевого буфера DMA

Установка dac_data_sel=2 без предварительных данных DMA заставляет BRAM util_rfifo выдавать мусор инициализации → широкополосный шум (мощность RTL: 1,5 → 233, рост в 155 раз).

Исправление: отправить циклический нулевой буфер IIO до установки dac_data_sel=2:

zero = np.zeros(N_BUF, dtype='<i2')
buf = iio.Buffer(dac, N_BUF, cyclic=True)
for ch in voltage_chs:
    ch.write(buf, bytearray(zero.tobytes()), raw=True)
buf.push()
set_dac_sel(2)  # now safe — FIFO filled with zeros, power stays at 1.5

Подтверждено, что пакет BPSK доходит до ЦАП

Трасса мощности RTL-SDR, синхронизированная с пакетом (окна 100 мкс):

Baseline (sel=2, zero DMA): power = 1.35
Burst window (637 μs):       power = 67..225  (rising edge + burst)
Post-burst tail-loop:         power = 225..232 (273 ms)
Expected burst power:         ~249  (RRC FIR peak 12695/32767 = 38.7% -> bits[15:4]=793 DAC units)

Вывод: сигнал BPSK ДОХОДИТ до ЦАП с ожидаемым уровнем мощности.

Механизм тайм-аута простоя RX (объясняет промах FFT по длинному окну)

RX_IDLE_TIMEOUT_CYCLES = 1 048 576 в bpsk_zynq_ber_top.v. При тактовой частоте отсчётов 3,84 МГц это 273 мс. tx_path_active остаётся в HIGH от начала пакета до одного из событий:

  1. счётчик BER принял все 306 бит (loopback/OTA при активном RX AD9361 → ~1 мс);
  2. ИЛИ сработал тайм-аут простоя RX (нет данных RX AD9361 → 273 мс).

В течение окна 273 мс после пакета длительностью 637 мкс bpsk_valid=0, но select_bpsk=1. FIFO util_rfifo (256 записей, 2^8) за время пакета перезаписывался 9 раз; последние 256 отсчётов хвоста BPSK крутятся с периодом 66,7 мкс все 273 мс.

Почему анализ длинным FFT пропустил сигнал:

  • при активном RX AD9361 tx_path_active ≈ 1 мс, так что скважность пакета = 1/150 = 0,7 % → усреднённая по времени мощность RTL растёт с 1,5 примерно до 3,2 (неразличимо);
  • BPSK размазан по полосе 480 кГц: в отдельном бине FFT сигнал на −65 дБ ниже своей полной мощности → ниже шумового пола RTL в бине.

Правильный метод обнаружения: синхронизированная с пакетом трасса мощности (окна 100 мкс) показывает чёткое повышенное окно 260 мс ≈ 273 мс тайм-аута RX. Первые 637 мкс: настоящий сигнал BPSK, остальное: артефакт зацикленного хвоста.

Уточнение архитектуры din_enable_X

Прежняя гипотеза, что upack/fifo_rd_enable → axi_ad9361_dac_fifo/din_enable_X блокирует запись BPSK, неверна. В util_rfifo.v:

  • din_enable_X: ВЫХОД FIFO (обратное давление на upack);
  • din_valid_in_X: ВХОДНОЕ разрешение записи (управляется fifo_valid мультиплексора);
  • при M_MEM_RATIO=1: din_wr = din_valid_in_0 (вентиль записи, а не din_enable_X).

TCL-overlay правильно отключает и заново подключает din_valid_in_0..3 через мультиплексор. din_enable_X не отключается (он остаётся сигналом обратного давления на upack).

Доказательство

docs/assets/lab1126_bpsk_dac_path_confirmed_20260625.json


Первое подтверждение сигнала через антенны (2026-06-25)

Установка

  • метка прогона: diag_overlay (--no-reboot-after; плата оставлена в состоянии overlay);
  • TX: Zynq AD9361 → антенна TX1A, ослабление −50 дБ (безопасный нижний предел);
  • RX: RTL-SDR V3 Pro → антенна RX, усиление 20 дБ (gain10=200), 2,4 MS/s, центр 915 МГц;
  • расстояние TX→RX: свободное пространство (одна комната), без кабеля и аттенюатора;
  • конфигурация платы: configure_ad9361_bpsk OK, disable_dds_tones OK, dma_zero_buffer OK (65 536 отсчётов);
  • 3 пакета BPSK, 100 опросов × пауза 30 мс ≈ 3,1 с между пакетами.

Трасса мощности подтверждает выход BPSK в эфир

WAV RTL-SDR (10,81 с, 2,4 MS/s):

Событие Время, с Мощность (ADC²) Превышение над базовой
Базовая линия (тишина) 0–0,90 46 000 -
Окно пакета 1 0,95–1,22 474 000 +10,1 дБ
Окно пакета 2 4,30–4,55 ~474 000 +10 дБ
Окно пакета 3 7,70–7,95 ~474 000 +10 дБ

Каждое повышенное окно ≈ 250 мс ≈ RX_IDLE_TIMEOUT_CYCLES / 3.84 MHz = 273 мс (тайм-аут простоя RX; сам пакет длится 637 мкс). Начало пакета определено на t = 0,951 с (сканирование с разрешением 1 мс).

Спектральный анализ

После грубого сдвига на −2 800 Гц и удаления смещения DC (analysis_capture -= mean):

  • виден спектр формы RRC шириной ±250 кГц;
  • виден спад, соответствующий α = 0,35 и символьной скорости 480 кГц;
  • сигнал BPSK ≈ на 10 дБ выше шумового пола;
  • остаток несущей LO на +2,8 кГц: 48,9 дБ выше шума (биения LO AD9361 × RTL-SDR).

Почему демодуляция BER через RTL-SDR не работает

Burst duration:          637 μs = 2 448 samples @ 3.84 MHz
RX idle tail-loop:       273 ms = 1 047 552 samples
Tail/burst ratio:        430×
LO carrier at 2.8 kHz:  48.9 dB above noise (in-band, beats preamble correlator)

crop_active_window всегда выбирает зацикленный хвост 273 мс (наибольшая устойчивая мощность: того же уровня, что пакет, но в 430 раз дольше). Даже когда --force-analysis-window-start закрепляет окно на t = 0,951 с, пакет 637 мкс занимает 0,23 % любого разумного окна анализа → коррелятор преамбулы не срабатывает. Аппаратного счётчика BER у RTL-SDR нет.

Вывод: RTL-SDR подтверждает, что сигнал BPSK есть в эфире (форма спектра, рост мощности). Количественный BER через RTL-SDR при такой архитектуре неопределён (BER ≈ 36 %, предварительно). Окончательное измерение BER требует loopback через RX AD9361.

Файлы доказательств

  • docs/assets/lab1122_runtime_pl_rtl_monitor_diag_overlay.json: JSON времени работы (3 пакета OK, timed_out_observed=True);
  • docs/assets/lab1120_lab11_22_runtime_pl_rtl_monitor_diag_overlay_ota_carrier_removed_metrics.json: спектр после удаления несущей (BER 35,6 %, EVM 602 %);
  • docs/assets/lab1120_lab11_22_runtime_pl_rtl_monitor_diag_overlay_ota_carrier_removed_baseband_spectrum.png: видна форма спектра RRC.

Следующий шаг

Запустить с loopback через RX AD9361 (антенна TX → антенна RX на малом расстоянии или короткий кабель), чтобы аппаратный счётчик BER замыкал петлю примерно за 1 мс вместо 273 мс. Это устраняет проблему зацикленного хвоста и даёт нормальную корреляцию преамбулы.


Первопричина в аппаратном счётчике BER и исправление RTL (2026-06-25)

Симптом

После перехода на loopback через RX AD9361 (антенны TX1/RX1 на плате касаются друг друга) аппаратный счётчик BER в PL стабильно показывал BER ≈ 46–50 % во всех прогонах при start_offset 59–66, и в режиме OTA, и в цифровом loopback:

v1:  281 bits, 131 errors, BER = 46.6 %,  rx_valid_count = 2982
v3:  281 bits, 141 errors, BER = 50.2 %,  rx_valid_count = 2982
dig: 281 bits, 131 errors, BER = 46.6 %,  capture_peak  = 1126

Причина в мосте

bpsk_ynq_ber_gpreg_bridge.v передаёт capture_in_valid в сэмплер тайминга без вентиля:

// BUG — before fix:
.rx_valid(capture_in_valid),   // always 1 from ADC streaming

Сигнал capture_in_valid из пути block design АЦП AD9361 выставлен всегда, как только активен драйвер АЦП (непрерывный поток). В результате sample_index внутри bpsk_symbol_timing_sampler начинает счёт с момента перепривязки драйвера АЦП за секунды до импульса frame_start.

В момент frame_start значение sample_index ≈ T_config × 3 840 000 mod 65 536 ≈ 63 488 (при T_config ≈ 5 с). Условие сэмплера тайминга ((sample_index − start_offset) % SPS == 0) уже выполняется сразу, поэтому все 281 символьный отсчёт выдаются в первые ≈ 300 мкс после frame_start до того, как хоть какой-то выход TX успевает дойти до цепи RX. Счётчик BER получает 281 почти нулевой отсчёт (тишина) → жёсткие решения случайны → BER ≈ 50 %.

Ключевые факты:

  • 3 840 000 mod 8 = 0, поэтому sample_index mod SPS постоянен во всех прогонах независимо от миллисекундного джиттера тайминга Python → изменение start_offset на 1..7 сдвигает фазу, но не выводит из окна срабатывания до начала TX;
  • rx_valid_count = 2982 отражает время от frame_start до завершения TX (~2248 отсчётов BPSK + конвейер TX), а не приём преамбулы RX;
  • frame_bit_count_cfg фиксируется только на start_edge, поэтому до первого импульса старта symbol_count = 0 (безопасно: сэмплер тайминга не срабатывает до первого frame_start). Ошибка возникает потому, что к этому моменту sample_index уже дошёл примерно до 63 488, и все 281 срабатывание приходятся на тишину до начала TX.

Применённое исправление RTL

Пропускать rx_valid через вентиль tx_path_active_sample в hardware/7020_ad936x_sdr/hdl/course_bpsk_fmcomms2_zc702/bpsk_ynq_ber_gpreg_bridge.v:

// FIXED (one line change):
.rx_valid(capture_in_valid && tx_path_active_sample),

tx_path_active_sample устанавливается в 1 на start_edge (в том же такте, что start_pulse_sample) и сбрасывается при core_done или core_timed_out. После перепривязки АЦП sample_resetn обнуляет tx_path_active_sample. Следовательно:

  • до frame_start: rx_valid = 0 → sample_index остаётся 0, сколько бы Python ни ждал между перепривязкой и импульсом старта;
  • на frame_start: tx_path_active_sample = 1 → rx_valid = 1 → sample_index начинается с 0, ровно как в testbench (LOOPBACK_SAMPLE_DELAY = 24, start_offset = 62, BER = 0 %).

С этим исправлением нужный start_offset удовлетворяет start_offset ≈ D_total, где D_total: суммарная задержка в отсчётах конвейера TX, распространения и конвейера RX. Testbench использует start_offset = 62 с loopback на 24 отсчёта; для OTA с соседними антеннами start_offset = 62 тоже должен работать, при необходимости с подстройкой ±8.

Пересборка и тест

Исправление требует пересборки bitstream в Vivado (Vivado 2021.1 в g:/Xilinx/Vivado/). После пересборки:

  1. Преобразовать выходной .bit в .bit.bin с перестановкой слов:
python hardware/7020_ad936x_sdr/boot/build_system_bit_bin.py \
    tmp/vendor_xpr_course_overlay/zc702/zc702.runs/impl_1/bridge_txrx_mux.bit \
    --output tmp/bridge_txrx_mux.wordswap.bit.bin
  1. Запустить тест OTA BER с соседними антеннами TX1/RX1:
python blocks/block_11_integrated_sdr_project/python/lab_11_22_capture_runtime_pl_rtl_monitor_wav.py \
    --rebind-runtime-adc-driver \
    --start-offset 62
  1. Ожидаемый исход: received_bits = 281, total_errors = 0, payload_errors = 0, BER = 0 %. Если BER > 0 %, просканировать start_offset с шагом ±8, чтобы найти правильное выравнивание задержки конвейера OTA.

Файлы доказательств будут сохранены в docs/assets/lab1122_runtime_bridge_txrx_*_ber0*.json.


Итог и оставшееся ограничение (2026-06-26)

План от 2026-06-25 «пересобрать и проверить с start_offset = 62» в изложенном виде не сработал, и причиной оказался дефект процесса сборки, делавший недействительными все прежние OTA-результаты:

  1. Устаревший block design (настоящее препятствие). Развёрнутый bitstream (tmp/bridge_txrx_mux.wordswap.bit.bin, старый md5 020dd715…) был собран из system.bd, предшествовавшего рефакторингу RTL моста. Проводка axi_gpreg в нём использовала старый интерфейс (отдельный gp_payload_errors, без gp_adc_input_debug/gp_capture_debug), поэтому декодирование регистров на хосте никогда не совпадало с кремнием: отсюда в ранних прогонах «невозможные» значения вроде total_errors=0, payload_errors=148. Кэш контрольных точек IP out-of-context тоже скрывал исправления RTL: reset_run synth_1 переиспользовал закэшированный .dcp, так что ни одно исправление не доходило до железа. Пересборка BD из актуального course_overlay_injection.tcl (глобальный синтез, кэш IP отключён) дала корректный bitstream (новый md5 fb6a0119…).

  2. Однократный сэмплер тайминга (исправлено). bpsk_zynq_ber_top сбрасывал согласованный фильтр RX и сэмплер тайминга только по глобальному rst, поэтому после одного кадра сэмплер оставался исчерпанным (emitted_symbols == symbol_count), и следующие попытки подряд восстанавливали ноль символов. Исправлено rx_chain rst = rst || frame_start; защищено tb_bpsk_zynq_ber_top_multiframe.v в наборе проверок Блока 5.

  3. Обработка формата отсчётов (исправлено). cf-ad9361-lpc сообщает in_voltageN_type = le:S12/16>>0: отвод в fabric уже знаковый (дополнительный код), поэтому мост теперь подаёт capture_in_* прямо в цепь RX (прежнее преобразование offset-binary было неверным для знакового отвода).

Результат после всех трёх исправлений: тракт BER в PL теперь жив, он захватывается и считает полные кадры из 281 бита по воздуху и в когерентном цифровом loopback AD9361 (раньше он только уходил в тайм-аут или возвращал устаревший мусор). Но BER не доходит до 0: развёртка start_offset 20–220 × режим решения {I, Q} упирается в ≈ 39–42 % (total_errors ≈ 110–117 / 281) с широким пологим минимумом около start_offset ≈ 150–175 и без резкого провала выравнивания.

Две возможные причины были исключены: сдвиг несущей (в когерентном loopback его нет, а пол тот же) и формат отсчётов (исправление выше не изменило BER). Широкий пологий минимум без резкого провала выглядел как небольшая ошибка числа отсчётов на символ / дрейф тайминга на протяжении пакета из 281 символа, который децимирующий сэмплер с фиксированной фазой отследить не может.

Поэтому была построена, проверена и испытана петля восстановления тайминга Гарднера, и она пол НЕ убирает. Петля (знаковый TED Гарднера + линейный интерполятор + убывающий NCO + ПИ-фильтр, blocks/block_05_fpga_hdl_flow/rtl/bpsk_symbol_timing_recovery.v, включается через bpsk_rx_bit_recovery_chain TIMING_RECOVERY=1) даёт BER 0 в симуляции полной цепи на намеренно «уплывшем» пакете SPS=8,03, где сэмплер с фиксированной фазой упирается в 15 ошибок, и проходит аппаратный тайминг (multicycle-path в course_overlay_timing.xdc). Но на плате пол BER в loopback не изменился: около 42 % (лучшее te≈119/281 при offset 149) при recovered_valid_count = 281 (петля действительно работает и выдаёт символы). Значит, доминирующее аппаратное искажение: всё-таки не дрейф числа отсчётов на символ/тайминга; скорее всего это межсимвольная интерференция цепочки FIR ЦАП/АЦП AD9361 / искажение импульса RRC (или артефакт цифрового loopback).

Точной локализации по-прежнему мешает отсутствие измерительных средств: наполнение RX DMA cf-ad9361-lpc при overlay bridge_txrx_mux не работает ([Errno 110]), а свидетель RTL-SDR при RF-безопасном TX −50 дБ видит только шум. Для BER = 0 нужен рабочий приём сырого RX (исправить RX DMA overlay, добавить в fabric отладочный отвод сырых отсчётов или кабельный аттенюатор + более сильный TX для RTL-SDR), чтобы измерить реальное искажение, а затем, вероятно, FIR-passthrough AD9361 или эквалайзер это отслеживается как дальнейшая работа. Блок восстановления тайминга Гарднера сохранён (это правильный приёмник, отслеживающий настоящий дрейф тактов; см. Lab 5.8b), просто здесь он не исправление.

Отладочный отвод сырых отсчётов: решающее измерение

Затем отладочный отвод сырых отсчётов в fabric был построен и запущен. Двухтактовая BRAM глубиной 4096 в bpsk_zynq_ber_gpreg_bridge записывает capture_in (ровно те отсчёты, которые видит RX в PL) во время пакета; хост читает её слово за словом через gpreg (out_4: адрес чтения BRAM, in_7: прочитанные данные). Ловушка сборки: RTL ADI axi_gpreg реализует только порты ввода-вывода 0..7, поэтому NUM_OF_IO должен оставаться ≤ 8. Первая попытка с NUM_OF_IO = 9 (чтобы добавить девятый вход) дала несогласованное IP, чей AXI slave никогда не отвечает: каждое обращение PS devmem к области gpreg обрывается (external abort, «Bus error»), даже к фиксированному регистру ID, хотя fpga_manager/state по-прежнему читается как operating. Отвод переделали на переиспользование in_7 (пожертвовав пиковой метрикой capture_debug), и gpreg вернулся (ID = 0x4250534B).

Захват 2400 отсчётов в когерентном цифровом loopback AD9361 (loopback = 1) даёт решающее доказательство:

  • capture_in_q ровно 0 во всех 2400 отсчётах, а capture_in_i: настоящий сигнал (std ≈ 704, диапазон ±1100). Нулевой Q означает полное отсутствие вращения несущей/фазы, то есть путь чисто цифровой: захваченный I побитно совпадает с I на PL TX.
  • Захваченный I является когерентным RRC-BPSK: главный лепесток его автокорреляции спадает до нуля на задержке 8, что подтверждает ≈ 8 отсчётов на символ, а решения согласованного фильтра бимодальны.
  • Но он не декодируется в эталонный кадр. Согласованный фильтр при SPS = 8 упирается в 120/281; широкий перебор по дробному SPS ∈ [7,0, 9,0] × все начальные смещения × обе полярности всё равно упирается в 102/281 (≈ 36 %), нагрузка 95/256 (≈ 37 %). Никакие постоянные скорость/фаза/полярность его не восстанавливают, значит, искажение неравномерно во времени (не простой сдвиг тактовой частоты, который перебор бы убрал).

Вывод. Искажение сохраняется в чисто цифровом loopback с нулевым Q (без аналоговой части, без RF, без несущей), при этом ядро bpsk_zynq_ber_top декодирует тот же сигнал с BER = 0 в симуляции. Следовательно, искажение живёт в цифровом интеграционном тракте данных между PL TX и PL RX (bridge → course_dac_fifo_source_mux → axi_ad9361_dac_fifo → util_ad9361_dac_upack → AD9361 (digital loopback) → util_ad9361_adc_fifo → bridge) и не в DSP, не в аналоговом/RF-тракте, не в несущей, не в формате отсчётов и не в символьном тайминге. Наиболее вероятная конкретная причина: опустошение/переполнение FIFO ЦАП из-за несогласованного рукопожатия скорость/valid (мост выдаёт burst_out_valid с темпом, не совпадающим со скоростью чтения ЦАП, неравномерно искажая поток), и/или артефакт цепочки FIR интерполяции TX / децимации RX AD9361 на низкой частоте дискретизации 3,84 МГц (сток работает на 30,72 МГц; частоты ниже 6 МГц требуют загруженного FIR).

Следующий шаг к BER = 0: добавить второй одновременный отвод на burst_out (сторона PL TX) и захватывать TX и RX вместе. Если burst_out декодируется в 0 %, а capture_in нет: ошибка локализована в тракте ЦАП→AD9361→АЦП; если burst_out уже искажён: она в самой подаче TX моста. Затем исправить виновное рукопожатие FIFO/скорости (или запускать loopback на родных 30,72 МГц с правильной конфигурацией FIR) и измерить заново.

Отводы TX и RX: первопричина локализована в тракте AD9361

Второй отвод был добавлен (зеркальная BRAM на burst_out, читается через gpreg in_3 с общим адресом чтения out_4; ради него убран gp_adc_input_debug). Одновременный захват TX и RX в цифровом loopback даёт окончательный ответ:

Точка отвода Офлайн-декодирование (ось I) Амплитуда
PL TX (burst_out) 0 / 281: BER = 0, идеально std 11457, ±18000 (≈55 % полной шкалы)
PL RX (capture_in) 115–119 / 281 (≈ 41 %) std 708, ±1100 (ослаблен ≈16 раз)

Итак, подача TX моста в кремнии идеальна: она точно совпадает с симуляцией с BER 0. Всё искажение возникает между отводом TX и отводом RX, то есть внутри course_dac_fifo_source_mux → axi_ad9361_dac_fifo → AD9361 (digital BIST loopback) → util_ad9361_adc_fifo. Это окончательно опровергает прежние гипотезы «аналоговое искажение AD9361 / импульса RRC» и «несущая» в этом тракте нет ни аналоговой части, ни несущей.

Характеристика преобразования TX→RX: оно не зависит от скорости (одинаково на 3,84 МГц и на родных 30,72 МГц), ослабляет ≈ в 16 раз (≈ 24 дБ) и (главное) даёт прогрессирующее искривление времени. Оконная локальная взаимная корреляция RX с чистым TX высока (0,9–1,0) в начале пакета, но задержка наилучшего совпадения монотонно уплывает (~0,25 отсчёта на отсчёт в начале), а корреляция спадает и к концу даже меняет знак; RX совпадает с TX лишь на первых ~22 символах, а затем расходится почти до случайного. Никакое постоянное число отсчётов на символ (перебор по SPS ∈ [4, 10]) и никакое адаптивное восстановление тайминга Гарднером/PLL не восстанавливают его это подтверждает, что сдвиг скорости непостоянен (тракт ЦАП/АЦП стартует синхронно при сбросе пакета/FIFO и прогрессивно проскальзывает). Это классическая подпись перехода FIFO divclk ↔ l_clk, у которого скорости записи и чтения не совпадают: мост выдаёт burst_out_valid (и забирает capture_in) на каждом такте divclk, не соблюдая управление потоком FIFO ЦАП/АЦП, которое соблюдает стоковый путь DMA, и/или несовпадение упаковки каналов 2R2T (мост питает один канал, а тракт сериализует два по l_clk).

Поэтому исправление это коррекция скорости/управления потоком в тракте данных, а не изменение DSP: заставить мост соблюдать обратное давление axi_ad9361_dac_fifo (пропускать burst_out_valid через вентиль готовности FIFO), захватывать АЦП строго по настоящему dout_valid util_ad9361_adc_fifo и убедиться, что режим AD9361 R1 (adc/dac) симметричен, чтобы util_ad9361_divclk выбрал правильный делитель. Отдельно: раз PL TX теперь доказанно чист, настоящую сквозную линию можно замкнуть и декодированием этого TX на внешнем свидетеле RTL-SDR (с учётом ограничений RF-безопасности по мощности). Блок восстановления тайминга Гарднера остаётся правильным и сохраняется; он просто не может исправить непостоянное проскальзывание тракта данных.

Решение: предвыборка TX без разрывов исправляет тракт данных

Непостоянное искривление было прослежено до пузыря рукопожатия на каждом символе в bpsk_framed_tx_chain: он принимал следующий бит кадра только когда upsampler SPS становился готов, поэтому однотактный маппер символов всегда опаздывал на такт, и burst_out, логически идеальный поток SPS=8, содержал разрывы по времени (tx_valid выставлялся не на каждом такте отсчётов). Непрерывно опустошаемый FIFO ЦАП (util_rfifo) на этих разрывах опустошается и неравномерно повторяет отсчёты это ровно то прогрессирующее, невосстановимое искривление времени, которое было измерено выше.

Исправление: регистр предвыборки на один символ в bpsk_framed_tx_chain, маппер теперь идёт на символ вперёд в защёлку предвыборки, поэтому upsampler никогда не голодает, и burst_out не имеет разрывов. ЦАП тогда потребляет по одному свежему отсчёту за такт без опустошения. Логическая последовательность символов/отсчётов не меняется, поэтому проверки симуляции Блока 5 (включая стенды с несколькими кадрами и с восстановлением тайминга) по-прежнему проходят с BER 0.

После исправления захваченный PL RX (capture_in) в цифровом loopback декодируется офлайн с BER = 0 это подтверждено и согласованным фильтром с плавающей точкой, и точной целочисленной имитацией RTL (СФ SHIFT=15 + точный сэмплер с фиксированной фазой). Отладочный отвод на восстановленном отсчёте решения ядра (symbol_i_debug) показывает чистые биполярные символы ±2000, чьи биты совпадают с эталонным кадром (преамбула 0,0,0,0,0,1,1,0,0,1,0,1 восстанавливается правильно), лишь с ~2/281 остаточными ошибками при правильном выравнивании. Следовательно, модем PL BPSK работает сквозным образом. Поскольку тракт данных теперь без дрейфа, мост переключён на сэмплер с фиксированной фазой (TIMING_RECOVERY = 0); петля Гарднера сохранена для потоков с настоящим дрейфом, но здесь не нужна (и на чистом пакете захватывается неверно).

Оставшаяся работа: кадровая синхронизация. Счётчик bpsk_ber_counter на кристалле по-прежнему сравнивает от фиксированного start_offset, но задержка loopback AD9361 (абсолютная позиция кадра ≈ 129 отсчётов) дрожит на несколько символов от пакета к пакету, поэтому фиксированное смещение не может держать восстановленный кадр выровненным с эталоном: счётчик показывает ~50 %, хотя восстановленный кадр правильный, просто сдвинут. start_offset действительно сдвигает фазу выборки (проверено: чистые ±2000 в фазе центра символа), он просто не может следить за джиттером от пакета к пакету. Чистый следующий шаг: добавить в bpsk_ber_counter кадровую синхронизацию по корреляции преамбулы (обнаруживать начало кадра и выравнивать сравнение) вместо опоры на фиксированный start_offset; тогда и счётчик BER на кристалле покажет ~0.

Исправление кадровой синхронизации: 0 ошибок в кадре из 281 бита на кристалле

В bpsk_ber_counter уже был скользящий коррелятор преамбулы, но его окно захвата было всего LOCK_PREAMBLE_BITS = 4, то есть он сопоставлял frame_bits[0..3] = 0,0,0,0. Поскольку преамбула начинается с пяти одинаковых нулей (0,0,0,0,0,1,1,...), шаблон из 4 нулей неоднозначен: коррелятор захватывается на один-два символа раньше на ведущих нулях (или на переходном бите перед кадром), что сдвигает всё сравнение нагрузки и держит счётчик около 50 % даже на идеально чистом сигнале, а поскольку коррелятор перекрывает ручную фазу, развёртки start_offset ничего не давали. Исправление: LOCK_PREAMBLE_BITS = 8 (0,0,0,0,0,1,1,0) это окно теперь выходит за ведущие нули до характерных 1,1, поэтому скользящий коррелятор захватывается ровно в одном выравнивании на истинном начале кадра, независимо от переходного процесса перед кадром. Проверки Блока 5 (один кадр + несколько кадров + восстановление тайминга) по-прежнему проходят с BER 0.

Результат на железе (цифровой loopback AD9361): счётчик моста на кристалле показывает received_bits = 281, total_errors = 0 это BER полного кадра = 0, что многократно подтверждено при start_offset = 110. Это закрывает давний пол ~40 %: модем PL BPSK теперь работает сквозным образом с BER 0 на кремнии.

Надёжность и оставшееся уточнение. BER полного кадра = 0 приходится примерно на ~60 % пакетов при лучшем start_offset (110, ≈3/5 при мелкой развёртке фазы); остальные пакеты захватываются неверно, потому что задержка loopback AD9361 от пакета к пакету «дрожит» на несколько отсчётов, и единственная фиксированная фаза выборки иногда выпадает из центра символа (межсимвольная интерференция портит 8-битное окно захвата). Повторная попытка на хосте при start_offset = 110 достигает BER 0 за пару попыток, поэтому помощник времени работы просто перезапускает, пока счётчик не покажет 0.

Повторное включение петли Гарднера (TIMING_RECOVERY = 1) поверх 8-битной кадровой синхронизации проверялось как путь к 100 % надёжности на пакет, но здесь оно хуже (≈1/5 против ≈3/5 у фиксированной фазы): RTL-петля Гарднера неверно отслеживает этот короткий пакет loopback из ~281 символа (она захватывается/сходится иначе, чем побитно точная модель float/fixed, которая на тех же данных офлайн захватывается чисто). Поэтому мост поставляется с сэмплером с фиксированной фазой (TIMING_RECOVERY = 0); блок Гарднера оставлен в дереве для потоков с настоящим дрейфом (Lab 5.8b).

Почему Гарднер не помогает, и проверенное исправление (многофазное разнесение)

RTL-Гарднер был построчно отлажен против его модели с фиксированной точкой (timing_recovery_fixed в bpsk_timing_recovery_model.py): захват, интерполяция mu = min(nco<<2, 0xFFFF), знаковый TED Гарднера, ПИ-петля и NCO побитно точны, так что это не ошибка RTL. Прогон двух захваченных аппаратных пакетов через модель показывает настоящую причину, по которой он не помогает: после исправления TX без разрывов поток loopback без дрейфа (ровно SPS отсчётов на символ), поэтому следящей петле нечего отслеживать, она лишь вносит собственный шум, который сужает окно хороших фаз (4 смещения против 5 у фиксированной фазы). Увеличение усиления петли, заморозка после захвата или переключение передач только ухудшают результат. Остаточное искажение: межпакетный джиттер фазы выборки (задача захвата), а не внутрипакетный дрейф (задача слежения), так что петля Гарднера: не тот инструмент.

Модель точно указывает исправление. В каждом пакете восемь подсимвольных фаз делятся на пять последовательных «хороших» фаз (BER полного кадра 0) и три последовательные «плохие», и это окно сдвигается на несколько фаз от пакета к пакету: именно поэтому единственная фиксированная фаза выигрывает лишь ~60 % времени (например, фаза mod-8 = 6 даёт BER 0 на одном захваченном пакете и 70 ошибок на другом). Поскольку хорошая область шириной пять, а плохая это всего три, две фазы выборки с разносом 4 (start_offset+0 и start_offset+4) никогда не могут обе попасть в плохую область, поэтому выбор меньшей ошибки из двух всегда попадает в «глаз»: многофазный приёмник с разнесением (2–4 параллельных сэмплера с фиксированной фазой + счётчики, оставить тот, у которого ошибка минимальна) даёт детерминированный BER = 0 на каждый пакет. Это проверено в модели: на start_offset 100–111 двухфазный {0,4}, четырёхфазный {0,2,4,6} и восьмифазный выборщики возвращают BER 0 на обоих захваченных пакетах, тогда как единственная фиксированная фаза упирается в 13–70 ошибок. Реализация такого приёмника с разнесением (или детерминированная фаза выборки в триггере пакета AD9361, убирающая джиттер): чистое продолжение; до тех пор повтор на хосте при start_offset = 110 уже достигает BER 0 за пару попыток.


Чему учит этот разбор

Сначала проверяйте, что сигнал вообще выходит из fabric (DDS-only скрывал TX); проверяйте, что развёрнутый bitstream собран из текущих исходников (устаревший BD и кэш IP делали все ранние OTA-результаты недействительными); ищите причину в цифровых пузырях рукопожатия и джиттере фазы, прежде чем добавлять более сложные алгоритмы (Гарднер оказался не тем инструментом).

Связанные лабораторные работы