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

Лабораторная 11.48 — Приёмник, который ждёт пакет

Идея

Во всех двухплатных лабораторных до сих пор плата A передавала без перерыва: циклический буфер кадров, снова и снова. Плата B запускала приёмник в любой момент, и кадр всегда был в эфире. Настоящее радио так не работает. Сообщение уходит один раз, а до и после него в эфире только шум.

Когда плата A впервые передала один кадр и после него тишину, плата B ловила его ровно настолько часто, насколько долго кадр был в эфире: при тишине длиной в девятнадцать кадров после каждого кадра в 2-3 приёмах из 100. Чтобы принимать такой пакет, пришлось изменить три вещи. Одна в приёмнике, две в самом пакете, и ни одна из них не та, о которой думаешь в первую очередь.

Стенд

плата A TX1 -> аттенюаторы (30 дБ) -> сплиттер на 3 -> плата B RX1
                                                    -> плата A RX1, RTL-SDR (необязательно)
  • 915 МГц, по кабелю. На плате A заводской образ, передача через iio_writedev.
  • На плате B нужен образ с режимом прослушивания: issue68-qpsk-listen-mode в репозитории артефактов (sha256 3406e253...). Это образ #66 плюс один управляющий бит, gp_ctrl[18]. Пока бит равен 0, приёмник ведёт себя в точности как во всех прежних лабораторных.
  • У платы B нет вентилятора. В длинных прогонах на стенде курса она держалась на 67-72 °C; выше 80 °C работу надо остановить.

Приёмник работает в режиме Лабораторной 11.45 (0x2D630: дифференциальная QPSK, DC-блокер, Costas, тайминг Gardner). Буфер платы A строит two_board_burst.burst_iq: один кадр на 152 символа, потом пауза, длина которой считается в кадрах (--gaps 19 значит, что кадр в эфире одну двадцатую времени).

Шаг 1. Приёмник не ждёт

cd blocks/block_11_integrated_sdr_project/python
python two_board_burst.py --gaps 0 1 4 19 99 --json-out burst_fixed.json

По умолчанию скрипт проходит TX -30, -10 и -50 дБ, плату B на ручных 71 дБ и с AGC slow_attack, по 100 попыток. На стенде курса (образ до #68, все шесть настроек вместе, 600 попыток на строку):

тишина после кадра доля времени с сигналом поймано кадров
0 (непрерывно) 1 99 %
1 кадр 1/2 27 %
4 кадра 1/5 15 %
19 кадров 1/20 2,3 %
99 кадров 1/100 0,7 %

Приёмник проектировался для лабораторных self-OTA, где он сам передаёт кадр и знает, что тот вернётся через несколько сотен символов. После запуска его сэмплер выдаёт длину кадра плюс 256 символов (0,85 мс) и останавливается. Если в это окно кадр не попал, сторож простоя завершает попытку, и регистр состояния показывает 0x81C (готово и таймаут) при нуле принятых символов. В непрерывном потоке кадр в окно попадает всегда, поэтому ни одна прежняя лабораторная этого не заметила.

Шаг 2. Ждать значит жить с шумом

Очевидное решение: окно подлиннее. Счётчик сэмплера 16-битный, он может работать 65535 символов (136 мс). В симуляции одного этого оказалось мало: кадр находился каждый раз, но половина кадров приходила с грубыми ошибками. Причина в том, что приёмник делает, пока ждёт.

Между пакетами приёмник видит шум, а его петли продолжают на нём работать. Детектор тайминга Gardner выдаёт только знак ошибки, поэтому на шуме он на каждом символе добавляет к интегратору случайные +1 или -1. Это случайное блуждание, и назад его ничто не тянет. Через несколько тысяч символов частота символов уходила на 1-2 %. Интегратор частоты Costas блуждал так же, примерно на ±2 кГц. Когда пакет наконец приходил, обе петли были уже где-то не там.

В цепочке для этого уже был гейт: Costas замирает, пока |I|+|Q| символа ниже фиксированного порога 8. На плате B при 71 дБ этот гейт бесполезен. Один только её шум даёт 16 отсчётов АЦП rms, и этого с запасом хватает, чтобы гейт был открыт почти на каждом символе. (Первая версия отчёта о пакетном режиме исходила из того, что в тишине гейт закрыт; замер шума показал обратное.) Фиксированный порог не работает, когда уровень шума зависит от усиления.

Поэтому в режим прослушивания добавлен детектор пакета, qpsk_packet_detect.v. Он ведёт два скользящих средних |I|+|Q|: короткое, примерно по 8 символам, и длинное, примерно по 4096. Длинное и есть уровень шума, потому что пакеты по сравнению с ним короткие. Пакет считается присутствующим, пока короткое среднее хотя бы в 1,5 раза больше длинного. Длинное среднее сохраняется от приёма к приёму, как оценка постоянной составляющей в Лабораторной 11.41.

С детектором приёмник в режиме прослушивания делает две вещи:

  • петли Gardner и Costas не обновляются, пока пакета нет;
  • решения доходят до коррелятора синхронизации кадра только при наличии пакета. Они задержаны на 16 символов, чтобы не потерять первые символы пакета, пришедшие раньше, чем детектор решил, и проходят ещё 64 символа после того, как детектор отпустил.

Второй пункт не косметика. Коррелятор захватывает кадр, когда 24-битное окно совпадает с преамбулой не более чем с одной ошибкой, при четырёх гипотезах поворота. На случайных битах это случается с вероятностью 4 * 25 / 2^24, около 6e-6 на бит. Ожидание в 30 мс это примерно 30 000 бит, так что без гейта примерно каждое пятое ожидание заканчивалось бы «кадром» из шума.

Шаг 3. Первому символу не с чем сравниться

С включённым прослушиванием плата B ловила пакет почти в каждом приёме. Но в симуляции она часто пропускала один-два пакета, прежде чем поймать следующий, и причина была в пакете, а не в приёмнике.

Дифференциальная QPSK несёт информацию в изменении фазы от символа к символу. Кодер платы A начинает с фазового состояния 0. В непрерывном потоке перед кадром стоит последний символ предыдущего кадра, приёмник его знает, и первый символ кадра декодируется правильно. После тишины перед кадром стоит шум. Первый символ кадра декодируется в случайный дибит, неверный в трёх случаях из четырёх, а синхронизации нужны 23 из первых 24 бит. Один символ с фазовым состоянием 0 перед кадром это исправляет (--ref): ровно его кодер и подразумевал.

В симуляции (шум как на плате, случайная фаза тайминга) среднее ожидание пойманного пакета сократилось с 45 длин кадра без опорного символа до 16 с ним. С фиксированным окном на плате тот же символ поднял долю пойманных кадров при тишине в один кадр с 27 % до 35-49 %, в зависимости от настройки. Для сообщения, которое передаётся только один раз, это разница между «обычно доходит» и «обычно теряется».

Шаг 4. Дать петлям на чём сойтись

С прослушиванием и опорным символом длинный прогон на плате дал вот что:

cd blocks/block_11_integrated_sdr_project/python
python two_board_burst.py --gaps 19 --ref --listen --tx-levels -30 --rx manual:71 --frames 10000 \
    --json-out soak_ref.json

10 000 кадров захвачено, 9510 чистых, 435 грубых: у 4,35 % кадров неверны больше 8 из 256 бит полезной нагрузки. Непрерывный поток со старым приёмником на том же стенде даёт 0,32 %.

two_board_burst_errors.py записывает для каждого кадра число ошибок и их положение (приёмник сообщает ошибки по четвертям полезной нагрузки и индексы первой и последней ошибки). У грубых кадров ошибки шли с самых первых бит нагрузки, и у нескольких картина ошибок совпадала полностью, чего шум не делает. Симуляция со случайной фазой тайминга для каждого пакета их воспроизвела, а симуляция с фиксированной фазой не показывала ни одного.

Объяснение такое. После тишины петли тайминга и несущей стартуют с того места, где их заморозили, и им нужно несколько символов, чтобы сойтись. Если перед кадром ничего нет, они сходятся на преамбуле, то есть ровно на тех битах, от которых зависит синхронизация. Естественным решением казалось увеличить усиление петли тайминга на первые 32 символа пакета. В симуляции стало хуже (11 грубых кадров из 30 вместо 8), и от этого отказались. Помогло другое: дать петлям, на чём сойтись. 32 обучающих символа перед опорным, попеременно в фазовых состояниях 0 и 2 (--lead 32). Каждый символ это скачок на 180 градусов, так что детектор тайминга получает переход на каждом символе.

cd blocks/block_11_integrated_sdr_project/python
python two_board_burst_errors.py --gap 19 --lead 32 --frames 1000 --json-out errors_lead32.json
python two_board_burst.py --gaps 19 --lead 32 --listen --tx-levels -30 --rx manual:71 --frames 10000 \
    --json-out soak_lead32.json

Первая команда печатает грубые кадры прогона с положением ошибок, если они есть. На стенде курса она напечатала locked 1000/1000, clean 1000, gross 0. Длинный прогон дал 10 000 захвачено, 9986 чистых, 9 грубых (0,09 %).

Шаг 5. Сообщение с консоли, отправленное один раз

До сих пор всё делалось на циклическом буфере платы A с тишиной внутри. Инструмент консолей из Лабораторной 11.46 теперь умеет по-настоящему: zmsg send --once выпускает в эфир один пакет с вводной частью и опорным символом впереди, а zmsg receive --listen раз за разом перевзводит слушающий приёмник.

board-b$ ./zmsg receive --listen --timeout 60 --rx-gain 71
board-a$ ./zmsg stop
transmitter stopped
board-a$ ./zmsg send "Hello from board A" --seq 17 --once --tx-gain -30
TX once sequence=17 bytes=18 payload="Hello from board A" lead=32
board-b:  RX sequence=17 bytes=18 crc=OK payload="Hello from board A"
board-b:  receptions 30, locked 1, delivered 1, crc failures 0, timeouts 29

Перед первым --once выполните на плате A zmsg stop, если передатчиком пользовался любой другой инструмент. Передатчик, убитый через kill -9, оставляет свой циклический буфер играть в ЦАП, и слушающий приёмник услышит это старое сообщение, как только гетеродин снова включится. На стенде так и вышло, и это была одна из четырёх проблем инструмента, которые проявились только на одиночных сообщениях; все четыре описаны в отчёте.

cd blocks/block_11_integrated_sdr_project/python
python two_board_zmsg_once.py --messages 500 --json-out zmsg_once.json

Скрипт запускает слушающий приёмник на плате B, по одному отправляет сообщения с платы A и сравнивает пришедшее с отправленным. На стенде курса (TX -30 дБ, плата B на 71 дБ):

приёмник отправлено доставлено принято чужих сообщений
прослушивание 500 494 0
фиксированное окно (--no-listen) 100 6 0

При TX -50 дБ дошли 159 из 200. Ни разу не было принято сообщение, которое не отправлялось: каждый кадр, захваченный на чём-то другом, не прошёл проверку CRC.

Картина целиком

Плата B с образом 3406e253, TX -30 дБ, 100 попыток на точку:

тишина после кадра фиксированное окно, опорный символ прослушивание, опорный символ прослушивание, вводная часть 32
4 кадра поймано 18-20 97-99 чистых 100 чистых
19 кадров поймано 4-8 94-97 чистых 100 чистых
99 кадров поймано 1 92-94 чистых 100 чистых

Прослушивание не стоит чувствительности. С вводной частью 32 и тишиной в 19 кадров чистыми выходят 100 из 100 кадров от TX -20 до -40 дБ и 87-89 на -50 дБ, а обрыв приходится на -55...-60 дБ. Непрерывный поток в условиях Лабораторной 11.45 обрывается там же.

Без оборудования

python tools/run_qpsk_listen_sim.py --json-out listen_sim.json

Скрипт гоняет RTL приёмника в Icarus Verilog на потоках, построенных по измеренным на плате B уровням (шум 16 отсчётов rms, пик кадра около 590 отсчётов для TX -30 дБ, случайная фаза тайминга у каждого пакета), и печатает одну таблицу: фиксированное окно, прослушивание без опорного символа, с ним и с вводной частью 32, слабый сигнал и поток из одного шума. Последний случай проверяет, что попытка прослушивания без пакета сама заканчивается, когда выбрано окно в 65535 символов. С параллельным запуском случаев на компьютере курса это заняло около получаса.

Упражнения

  1. Запустите шаг 4 с --gaps 1 (кадр в эфире половину времени). На стенде курса в режиме прослушивания 18-59 из 100 кадров вышли грубыми. Почему при меньшей тишине прослушивание работает хуже?
  2. Сколько ложных захватов на шуме можно ожидать за всё окно 136 мс без гейта решений из шага 2? Возьмите 480 000 символов в секунду и вероятность на бит из шага 2.
  3. Опорный символ это фазовое состояние 0, а не какое-нибудь другое. Что будет с состоянием 1?
  4. Попробуйте --lead 8 и --lead 16. Что нужно измерить, чтобы решить, какой длины должна быть вводная часть, и почему ответ должен зависеть от уровня сигнала?
  5. Первая версия zmsg receive теряла примерно один импульс старта из семи и после этого печатала предыдущее сообщение ещё раз. Python-инструменты, делавшие те же записи в регистры через devmem, не теряли ни одного. Почему?
Ответы
  1. Детектор сравнивает короткое среднее с длинным, а длинное должно быть уровнем шума. Когда кадр в эфире половину времени, длинное среднее наполовину состоит из сигнала, порог оказывается рядом с уровнем сигнала, и детектор срабатывает и отпускает посреди пакета. Петли тогда замирают посреди кадра. По той же причине непрерывный поток в режиме прослушивания в основном вообще не захватывается. Режим прослушивания рассчитан на пакеты с тишиной между ними (на стенде работал один кадр сигнала на пять и реже); для непрерывного потока его надо выключать.
  2. 136 мс это около 65 000 символов, 130 000 бит. При 6e-6 на бит получается 0,8 ожидаемого ложного захвата на окно: большинство долгих ожиданий заканчивались бы мусором.
  3. Кодер начинает с состояния 0, поэтому первый символ кадра равен 0 + приращение. Опорный символ в состоянии 1 заставит приёмник декодировать первый символ как приращение - 1. Дибиты закодированы кодом Грея, так что это один неверный бит в каждом кадре: синхронизация по-прежнему захватывает, но единственная допустимая ошибка уже израсходована, и любая шумовая ошибка в тех же 24 битах теряет кадр. Опорный символ в состоянии 2 стоил бы двух бит и терял бы каждый кадр.
  4. Считать грубые кадры на прогоне, достаточно длинном, чтобы увидеть эффект в 0,1 % (тысячи кадров), при нескольких уровнях TX. Число символов, за которое сходятся петли, зависит от отношения сигнал/шум, так что вводной части, которой хватает на -30 дБ, может не хватить на -50 дБ. Важна и длина тишины: после 0,3 с тишины при 32 символах испорчены 3 кадра из 1003, при 64 и 128 ни один из 1000, поэтому инструменты теперь используют 64 (отчёт single-message-corruption-20261015.md). Длины меньше 32 не проверялись.
  5. Бит старта лежит в регистре, который пишется в тактовом домене процессора, а читается модемом в его собственном домене через двухтриггерный синхронизатор. Две записи через mmap подряд (бит в 1, бит в 0) разделяют самое большее несколько сотен наносекунд, примерно период такта модема, и синхронизатор иногда вообще не видит единицу. devmem запускает процесс на каждое обращение, так что его импульс длится миллисекунды. Теперь zmsg держит бит 20 мкс и считает приём состоявшимся только по биту «готово», потому что счётчик принятых символов хранит значение от предыдущего кадра.

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

  • two-board-burst-20261014.md: шаг 1, фиксированное окно на плате;
  • qpsk-listen-mode-20261015.md: режим прослушивания, опорный символ и вводная часть на плате и в симуляции, одиночные сообщения zmsg и четыре исправления инструмента, со всеми прогонами;
  • two-board-message-20261014.md: циклическая кампания сообщений Лабораторной 11.46 и её перемер после исправлений zmsg;
  • задача #68.

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

Она показывает приёмник, который до 136 мс ждёт пакет, переданный один раз, и декодирует его так же надёжно, как непрерывный поток; то, что формат пачки важен не меньше приёмника; и текстовое сообщение, отправленное один раз с консоли одной платы и принятое на другой. Она не показывает передатчик в ПЛИС, выпускающий такую пачку (вводную часть строят только хостовые инструменты и zmsg), и mailbox PS/PL из Лабораторной 5.12, передающий пакет модему. Все числа получены на одной паре плат, на одной несущей, по кабелю.