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

Лабораторная 11.47 — Плата принимает собственную пачку

Идея

Лабораторным с двумя платами нужны две платы и второй передатчик. Одна плата тоже может отправить пачку QPSK своего модема в ПЛИС через TX1 и принять её на RX1 по кабелю. Тогда TX и RX работают от одного опорного генератора: нет сдвига частоты несущей, нет блуждания генераторов, и весь модем можно проверить одной платой.

На стенде курса у платы B это сначала не получалось: кадр находился реже чем в одном приёме из десяти, и с большим числом ошибок. Причина оказалась не в модеме, а в том, как битстрим курса подаёт данные на ЦАП. Ей и посвящена эта лабораторная.

Стенд

плата B TX1 -> аттенюаторы (не меньше 30 dB в сумме) -> плата B RX1
  • Никогда не соединяйте TX1 с RX1 напрямую. При TX 0 dB и 30 dB в тракте на вход приёмника приходит около −30 dBm; предел входа AD9361 +2,5 dBm.
  • На стенде курса стояли переменный аттенюатор (0 dB) -> 30 dB -> сплиттер на 3 выхода; на остальные выходы были подключены RX1 платы A и RTL-SDR как независимые свидетели.
  • У платы B нет вентилятора. Сценарий читает температуру Zynq с XADC до и после и не начинает работу выше 80 °C; на стенде она держалась в пределах 61–70 °C.

Чем особенна плата B

На плате B стоит образ курса, в дереве устройств которого нет драйвера ЦАП (нет cf-ad9361-dds-core-lpc). Два следствия:

  1. После загрузки все каналы ЦАП берут данные со встроенного DDS, масштаб которого равен 0. Мост подмешивает пачку модема в FIFO ЦАП на входе DMA (course_dac_fifo_source_mux.v), поэтому, пока каналы на него не переключены (devmem 0x79024418 32 2 и так же для 0x...58, 0x...98, 0x...D8), пачка вообще не передаётся.
  2. Вендорский TX DMA никто не запускает. Об этом следующий раздел.

Дополнение, в тот же день: дерево устройств платы B отличалось от дерева платы A ровно этим одним узлом (cf-ad9361-dds-core-lpc@79024000). После установки на B дерева платы A (старое сохранено на карте как devicetree.dtb.course_nodds) у B появилось IIO-устройство ЦАП, и циклический буфер нулей через iio_writedev поддерживает вендорский поток так же, как на плате A. Сценарий использует этот путь, если устройство есть, а иначе переходит на dac_zero_stream.

ЦАП, который держит последний отсчёт

Когда каналы стоят на входе DMA, пачка уходит, но RTL-SDR на сплиттере показывает мощную непрерывную линию на частоте гетеродина. Её уровень над шумом RTL-SDR в разных состояниях:

состояние платы B линия на 915 МГц
гетеродин TX выключен 12,9 dB
источник ЦАП ZERO 13,5 dB
источник ЦАП DDS (по умолчанию после загрузки) 14,5 dB
источник ЦАП DMA, после нескольких пачек 87,7 dB

Значит, это не аналоговая утечка гетеродина. Мультиплексор пишет FIFO ЦАП по стробу вендорского потока и подставляет пачку, только пока она передаётся. На плате A всегда работает циклический буфер DMA (iio_writedev), поэтому строб не останавливается. На плате B TX DMA никто не запускает, FIFO пишется только во время пачки, и между пачками ЦАП продолжает выдавать последнее записанное слово, ненулевой отсчёт пачки. Постоянное число на ЦАП означает несущую на частоте гетеродина. Плата A показала, что во время пачки несущая всего на 3 dB слабее её и не пропадает никогда. На собственном приёмнике платы B она превращается в постоянную составляющую такой же силы, как сигнал.

Исправление, используемое здесь

blocks/block_11_integrated_sdr_project/sw/dac_zero_stream.c поддерживает вендорский поток нулями. Программа закрепляет страницу памяти, берёт её физический адрес из /proc/self/pagemap и гоняет по ней по кругу ADI axi_dmac по адресу 0x7c420000 (память -> поток на ЦАП). Перед запуском бездействующий драйвер ядра этого DMA отвязывается. По сигналу SIGTERM программа останавливает DMA, поэтому останавливать её нужно обычным kill. После kill -9 DMA продолжил бы читать страницу, которую ядро может отдать кому-то другому, и передавать её в эфир.

Сценарий ниже собирает программу компилятором arm-linux-gnueabihf-gcc -static из Vitis (как на этапе 3 Лабораторной 8.14), запускает её, переключает каналы ЦАП, прогоняет кадры и снова её останавливает.

Запуск

cd blocks/block_11_integrated_sdr_project/python
python lab_11_47_single_board_selfota.py --frames 60
python lab_11_47_single_board_selfota.py --frames 60 --no-zero-stream

Образ платы B 3d492301 (#66), TX 0 dB, RX 60 dB, дифференциальный режим Лабораторной 11.45, 915 МГц:

кадров захвачено чистых ошибок в полезной нагрузке
с потоком нулей 60 52 52 0 / 13312
без потока нулей (отсчёт держится) 60 5 0 220 / 1280

С потоком нулей каждый захваченный кадр чистый; без него связь практически теряется.

Упражнения

  1. Запустите с --offsets 0 1 2 3 4 5 6 7. Почему на одной плате некоторые значения start_offset никогда не дают захвата, а на стенде из двух плат работают все? Сравните с развёрткой фазы выборки в Лабораторной 5.8.
  2. Запустите с ключом --no-dc-blocker и без него. До исправления issue #66 DC-блокер стоил на этой связи кадров; сколько он стоит теперь и почему DC-блокер не мог убрать удерживаемый отсчёт?
  3. Во время разбора в тракт предлагали поставить пассивный коаксиальный DC block. Почему он ничего бы не изменил?
  4. Комментарий в мультиплексоре объясняет, почему FIFO нужно писать с жёстким шагом. Что пошло бы не так, если бы RTL просто писал нули на своём такте всякий раз, когда пачка не выбрана?
Ответы
  1. На одной плате задержка от TX модема до его RX постоянна, поэтому кадр всегда приходит с одной и той же фазой внутри символа. Каждое значение start_offset ставит выборку на одну фазу; два из восьми попадают на границу символа, где глаз закрыт. На двух платах момент старта относительно принимаемого потока случаен, и каждое значение чаще всего попадает на хорошую фазу.
  2. На стенде оба варианта захватили одни и те же кадры без ошибок (по 75 из 100 при восьми смещениях). Удерживаемый отсчёт создаёт несущую на частоте гетеродина, и в приёмник она приходит как постоянная составляющая, но она есть с самого начала и сравнима с сигналом; со сбросом на каждом кадре старого DC-блокера приёмник не захватил ни одного кадра. DC-блокер может убрать смещение только медленно и приближённо; устранить причину на передатчике лучше.
  3. На кабеле несущая находится на 915 МГц. DC block представляет собой последовательный конденсатор: он задерживает постоянное напряжение и пропускает радиосигнал. «Постоянной» несущая становится только после того, как приёмник перенёс её на нулевую частоту.
  4. FIFO переводит данные из тактового домена ПЛИС в такт ЦАП. Если число записей на одно чтение ЦАП зависит от того, что передаётся, заполнение FIFO, а вместе с ним задержка пачки до антенны, меняется от пачки к пачке. Комментарий в мультиплексоре фиксирует ровно это: кадр приходил с разной фазой внутри символа по повторяющемуся шаблону. Шаг записи должен следовать скорости чтения ЦАП.

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

  • board-b-dac-held-carrier-20261014.md: таблица RTL-SDR, взгляд платы A и сравнение DC-блокера на одной плате;
  • issue #67: поведение мультиплексора и варианты постоянного исправления (дерево устройств платы B или RTL).

Что лабораторная показывает и чего не показывает

Она показывает связь на одной плате, в которой каждый захваченный кадр принимается без ошибок, как только FIFO ЦАП получает данные правильно, и то, как деталь инфраструктуры далеко от модема может выглядеть как проблема модема. Мультиплексор она не исправляет: поток нулей остаётся обходным решением, которое должно работать, пока плата B передаёт.