Лабораторная 11.30 — Оценщик грубого CFO на реальном двухплатном канале¶
Цель¶
Оценщик грубого CFO Блока 05 (qpsk_coarse_cfo.v, моделируемый coarse_cfo_ref.py) был проверен только на синтетических сдвигах: модель с плавающей точкой, fixed-point модель, побитово с ней совпадающая, и RTL-testbench, питаемый сгенерированным захватом. У всех этих проверок общее допущение: что реальная пара AD9361 выглядит как модель.
Одна плата этого не проверит. TX_LO и RX_LO берутся от одного опорного генератора, поэтому CFO ≈ 0, и оценивать нечего. Эта работа использует две платы с независимыми опорными генераторами (единственную конфигурацию, где существует реальный сдвиг несущей) и заново прогоняет те же самые оценщики float и fixed-point по восстановленным символам. Fixed-point тракт это арифметика RTL, поэтому согласие здесь: свидетельство о поставляемом HDL, а не о симуляции.
Установка¶
плата A ── циклический QPSK из ЦАП через DMA (RRC курса, SPS=8, 480 кСимв/с, 3,84 МГц)
│
│ TX1 ──▶ аттенюатор 30 дБ ──▶ RX1 (закрытый кабель SMA, 915 МГц)
▼
плата B ── захват сырых IQ ──▶ SSH ──▶ хост: оценщик, EVM, SER
Плата B: вторая плата AD9361 в своей подсети, поэтому обе могут делить один коммутатор без конфликта адресов (ZYNQ_SSH_HOST_B, по умолчанию 192.168.20.1). Передаваемая форма сигнала строится из тех же коэффициентов RRC Q15, что использует передатчик PL, а RRC применяется циклически: DMA бесконечно воспроизводит буфер от конца к началу, и линейная свёртка оставила бы шов, размазывающий спектр.
Запуск¶
# ВСЕГДА сначала: цепочка анализа на сгенерированной форме сигнала, без радио в тракте.
python blocks/block_11_integrated_sdr_project/python/lab_11_30_two_board_cfo_validation.py --self-test
# затем реальный канал
python blocks/block_11_integrated_sdr_project/python/lab_11_30_two_board_cfo_validation.py
Полезные флаги: --carrier (по умолчанию 915e6), --tx-gain (по умолчанию −30 дБ, кондуктивно), --rx-gain, --symbols, --capture, --window, --host-a/--host-b.
Результат (2026-07-17, 915 МГц, TX −30 дБ на аттенюатор 30 дБ, усиление RX 50 дБ)¶
Дословно из скрипта:
board A transmitting: TX=-30.000000 dB at 915.000 MHz, DAC source=0x00000002 (DMA), LO_powerdown=0
receiver alive: capture rms = 253.5 counts
contiguity check: |autocorr| at the 32768-sample cyclic period = 0.9989
timing mu = 1.625 samples, |corr| = 35.6x
EVM = 6.62 %
CFO = -173.2 Hz (std 13.4 Hz) -> -0.189 ppm at 915 MHz
estimator float vs fixed(RTL): mean |diff| = 0.14 Hz, max 0.34 Hz
SER = 0 / 15360 = 0.000000
| Метрика | Значение |
|---|---|
Непрерывность \|autocorr\| на циклическом периоде |
0,9989 |
| Пик корреляции | в 35,6× выше среднего |
| EVM | 6,62 % |
| Межплатный CFO | −173,2 Гц, СКО 13,4 Гц (−0,189 ppm на 915 МГц) |
| Оценщик float против fixed (RTL) | среднее |diff| = 0,14 Гц, максимум 0,34 Гц |
| SER | 0 / 15 360 символов (30 720 бит) |
Два независимых TCXO оказываются на расстоянии ~0,2 ppm: пара сотен Гц на 915 МГц, стабильно с точностью ~13 Гц на протяжении записи. Оценщик однозначен в пределах ±60 кГц (±1/8 цикла на символ при 480 кСимв/с), так что железо лежит внутри проектного диапазона с запасом более двух порядков, а fixed-point арифметика, выполняемая RTL, следует за эталоном float с точностью до долей герца на живом сигнале. Более ранний интерактивный прогон той же последовательности дал EVM 6,59 %, CFO −208,0 Гц и float-против-fixed 0,09 Гц: те же числа в пределах дрейфа от прогона к прогону.
Сравнение оценщиков подаёт обеим моделям те же символы того же (не развёрнутого) сегмента. Если дать fixed-модели развёрнутый срез или иное число символов, измеряется собственная дисперсия двух оценщиков, а не арифметика до исправления это раздувало показатель с 0,14 Гц до 66 Гц.
Четыре ловушки, зашитые в эту работу¶
Каждая из них давала убедительный, но совершенно ложный «аппаратный дефект». Три из четырёх были ошибками измерения, а не радио.
1. iio_readdev по умолчанию использует буфер на 256 отсчётов¶
Без -b <N> захват склеивается из N/256 фрагментов с пропущенными отсчётами между ними. Каждый фрагмент верен, поэтому спектр и EVM выглядят вполне разумно, но запись не является непрерывным временным рядом. Симптомы: корреляция никогда не захватывается (5–8× вместо 20–70×), CFO как будто «гуляет» на десятки кГц, BER застревает около случайного.
Всегда задавайте размер буфера равным всему захвату и проверяйте:
|autocorr| на периоде циклического буфера: с разрывами ≈ 0,01 непрерывный ≈ 0,99
CFO эту проверку не ломает (он лишь добавляет постоянную фазу) поэтому это чистый ответ «да/нет» о непрерывности. Работа выполняет её автоматически и предупреждает.
2. Тон DDS не включает синтезатор TX¶
При adi,tx-lo-powerdown-managed-enable=1 драйвер держит out_altvoltage1_TX_LO_powerdown=1, пока не появится настоящий поток TX. Тон только от DDS потоком не является, поэтому синтезатор остаётся выключенным, и с разъёма SMA ничего не выходит независимо от hardwaregain. Признак безошибочен, когда его знаешь: изменение усиления TX на 40 дБ не сдвигает принятый уровень вообще это сломанный тракт, а не слабый. Не гоняйтесь за кабелями.
Буфер DMA (то, что использует эта работа) является таким потоком и сам поднимает LO. Чтобы разделить цифровую и аналоговую части, когда канал молчит, используйте собственный loopback AD9361 это атрибут debugfs называется loopback, а не bist_loopback:
echo 1 > /sys/kernel/debug/iio/iio:device0/loopback # цифровой TX→RX внутри микросхемы
Сильный тон там при тишине при loopback=0 относит неисправность в аналоговую область/LO и снимает подозрения с прошивки, ядра DDS и каждого кабеля.
3. Эталон для EVM нужно повернуть на +45°¶
Для QPSK sym**4 = −1 всегда, поэтому angle(mean(sym**4))/4 = π/4, и коррекция кладёт созвездие на оси, а не на 45°. Если сравнивать его с (±1±j)/√2, вы измерите постоянные ≈54 %, которые не сдвинет ни изменение усиления, ни тайминга, ни LO. Эта неподвижность и есть подсказка: физика сдвигается, а постоянная арифметическая ошибка это нет.
4. Знак корреляции¶
ifft(fft(a) * conj(fft(b))) имеет пик в off, означающем a[n] ≈ b[n−off], поэтому выровненный эталон: np.roll(tx, +off). Неверный знак даёт ровно 0,75 SER случайного угадывания при том что корреляция по-прежнему сообщает идеальный захват.
Правило, которому учат все четыре: прогоните всю цепочку анализа на известном сигнале без радио в тракте и потребуйте SER = 0 до каких-либо выводов о железе.
--self-testделает именно это за несколько секунд.
Повторяемость¶
Работа идёт подряд без перезагрузок между прогонами. Проверено 25-прогонной серией без присмотра (232 с), без сбоев:
| За 25 прогонов | |
|---|---|
| Декодировано | 768 000 бит, 0 ошибок → BER = 0 (0 / 384 000 символов) |
| Межплатный CFO | среднее −288,5 Гц, σ 11,0 Гц (мин −311,5, макс −262,0) |
| EVM | среднее 7,35 % (6,65…8,27 %) |
| Захват корреляции | ≥ 35,5× выше среднего; непрерывность \|autocorr\| ≥ 0,994 в каждом прогоне |
Это возможно благодаря двум вещам, и обе достались тяжело:
- Приёмник должен быть платой того же класса, что и передатчик (стоковый ADI/Pluto, Linux 5.15). Первый приёмник (устройство FishBall/ZynqSDR на замороженном Linux 4.0) имел драйвер, у которого RX DMA «зависал» примерно после одного захвата/переконфигурации за загрузку, игнорировал обычный
reboot, иногда игнорировал sysrq и порой требовал физического выключения питания. Перепрошивка тем же образом Pluto, что у передатчика, убрала всё это: его RX теперь возвращает чистые данные в каждом прогоне, аrebootработает. - Циклический DMA передатчика освобождается в начале каждого прогона функцией
reset_tx_dma(). Если оставить как есть,iio_writedev -cзастревает в неубиваемом состоянии (uninterruptible-DMA), удерживая буфер ЦАП, и следующий прогон падает сUnable to allocate buffer: Device or resource busy, аpkill -9,SIGTERMи переключение RSTN ядра DDS не помогают. Помогает unbind+rebind драйвераcf_axi_dds. Он заново регистрирует ядро DDS под новымiio:deviceN, поэтому работа обращается к передатчику и приёмнику по ИМЕНИ и читает источник ЦАП по физическому адресу: ничто не зависит от номера устройства DDS.
Рецепт: прошейте обе платы стендовым образом (см.
README-bench.txtдля каждой платы на её SD-карте), затем просто запускайте работу: многократно, без присмотра. Сначала запустите--self-test, если вы меняли анализ на стороне хоста.
По-прежнему держите -b неизменным в рамках сессии: захват с другим -b заставляет драйвер склеивать следующий из устаревших фрагментов (|autocorr| падает до ~0,02, то есть ловушка 1). Работа использует один размер буфера (--capture) для своего единственного чтения, так что это касается только случая, когда вы вручную опрашиваете приёмник между прогонами: не делайте этого.
Ловушка запуска, о которой стоит знать¶
ParamikoCommandRunner оборачивает каждую команду в sh -lc '...; rc=$?; printf ...' и читает канал до EOF. Фоновое задание, запущенное через него (даже nohup ... &) уничтожается вместе с обёрткой, молча и с пустым журналом. Измерено бок о бок:
запуск через ParamikoCommandRunner: DAC source = 0x0 (DDS), writedev исчез
запуск через raw exec_command: DAC source = 0x2 (DMA), writer жив
Поэтому передатчик запускается в сыром сеансе (start_detached). А поскольку работающий iio_writedev всё же не доказывает передачу, работа считывает выбор источника ЦАП обратно и требует 0x2; канал, оставленный на DDS с нулевым масштабом, иначе выглядит в точности как мёртвый канал: именно так это и обнаружили.
RF-безопасность¶
Это кондуктивная работа: TX1 платы A попадает на RX1 платы B через кабель и аттенюатор 30 дБ, и ничего не излучается. TX −30 дБ по умолчанию безопасен за этим аттенюатором. По воздуху предел это −50 дБ: не повышайте мощность, чтобы «увидеть сигнал».
Стоковый rc.user платы B при каждой загрузке заново включает передатчик 71 МГц на −10 дБ. Эта работа глушит его и принудительно выставляет −89,75 дБ до какого-либо иного действия и приглушает обе платы при выходе в том числе после сбоя.