# pluto-link — OFDM-радиолиния между двумя PlutoSDR (весь код на ARM внутри Pluto) Цифровая радиолиния точка-точка на базе двух ADALM-Pluto+ (клон, Zynq-7020). PHY: OFDM (liquid-dsp) + RS(255,223) FEC. Весь сигнальный тракт исполняется на ARM Cortex-A9 **внутри** Pluto; хост-ПК нужен только для сборки, заливки бинарей и снятия логов. Прототип в рамках ОКР «R-Link» (двухранговая самоорганизующаяся сеть); данный репозиторий покрывает уровень PHY/линка. --- ## 1. Аппаратная платформа (фактическая, проверено) | Параметр | Значение | Как проверено | |---|---|---| | Плата | Pluto+ (клон, Zynq-**7020**, AD9361) | `cat /proc/cpuinfo` — 2 ядра | | CPU | 2 × Cortex-A9 @ 667 МГц, NEON | `Features: ... neon vfpv3` | | RAM | 1 ГБ (доступно ~1000 МБ) | `free` | | Прошивка | v0.38-4-g95aad-dirty | баннер SSH | | Доступ | SSH root@192.168.2.1 (пароль `analog`), USB-Ethernet | — | ⚠️ Стоковый ADALM-Pluto (7010, 512 МБ, 1 ядро) — **другая плата**; все бюджеты CPU в этом документе рассчитаны для Pluto+. Не путать. ⚠️ В прошивке v0.38 нет sftp-server → заливать файлы только `scp -O` (legacy-протокол). ## 2. Бюджет CPU — главное ограничение проекта Замерено офлайн-бенчмарком `src/ofdm_bench.c` прямо на ARM Pluto (одно ядро, static, `-O3 -mcpu=cortex-a9 -mfpu=neon -mfloat-abi=hard -ffast-math`, liquid-dsp 1.8.0 **с FFTW/NEON** — проверено по `HAVE_LIBFFTW3F` в config.log): | Режим ofdmflexframesync | Пропускная способность | Вывод | |---|---|---| | Поиск преамбулы (шум) | **5.32 Msps** | дежурный режим, не лимитирует | | Приём кадров (duty ~57%) | **2.34 Msps** | узкое место | Следствия (зафиксированы как проектные решения): - **Рабочий sample rate = 1.92 MSPS.** Запас 1.22× на ядре синхронизатора; второе ядро — под IIO, RS-декод, ввод-вывод. - **3.84 MSPS запрещён**: система работает на редких пакетах и лавинообразно разваливается под нагрузкой (0.61× от realtime). Это худший вид отказа — «на демо работало». - Полезная скорость ≈ **1.3–1.5 Мбит/с** (~160–190 КБ/с). Файл 10 МБ ≈ 1 мин. - Параметры замера: M=64, CP=16, taper=4, QPSK, CRC32, payload 1275 Б. Полный протокол — `docs/BENCHMARK.md`. При любом изменении PHY-параметров бенчмарк перегоняется **до** изменения кода тракта. ## 3. Структура репозитория ``` pluto-link/ ├── README.md # этот файл ├── CLAUDE.md # краткие правила для Claude Code ├── makefile # цели: all, tx, rx, bench, deploy, clean ├── toolchain.env # CROSS=arm-linux-gnueabihf-, SYSROOT=$HOME/xarm ├── src/ │ ├── common.h # параметры OFDM/RS, дефолты, прототипы │ ├── common.c # pluto_init (local:), pluto_configure │ ├── transmitter.c # stdin → OFDM → AD9361 TX │ ├── receiver.c # AD9361 RX → OFDM sync → stdout │ └── ofdm_bench.c # бенчмарк CPU (не удалять!) ├── scripts/ │ ├── build_deps.sh # кросс-сборка FFTW + liquid-dsp → $HOME/xarm │ ├── deploy.sh # scp -O бинарей в /tmp обеих плат │ └── test_file_link.sh # e2e: файл → эфир → md5-сверка └── docs/ ├── BENCHMARK.md # методика и результаты замеров CPU └── NOTES.md # FDD, регуляторика, известные грабли ``` ## 4. Среда разработки и сборка ### 4.1 Хост Любой Linux; проверено на Raspberry Pi OS 64-bit (aarch64). Кросс-тулчейн: ```bash sudo apt install -y build-essential autoconf automake libtool git wget \ gcc-arm-linux-gnueabihf ``` ### 4.2 Зависимости (один раз, ~20 мин) `scripts/build_deps.sh` собирает в `$HOME/xarm` статические armhf-библиотеки. **Порядок важен**: liquid зависит от FFTW и libfec, поэтому они собираются до него. 1. **FFTW 3.3.10**: `--enable-single --enable-neon --enable-static` 2. **libfec** (jgaeddert): Reed-Solomon для liquid. liquid **не реализует RS сам** — оборачивает libfec. Без неё `fec_create(LIQUID_FEC_RS_M8)` вернёт NULL и `-c rs8` молча пойдёт БЕЗ FEC. Собирать строго ДО liquid. 3. **liquid-dsp 1.8.0**: с `CPPFLAGS=-I$HOME/xarm/include LDFLAGS=-L$HOME/xarm/lib` и обходом autoconf-кросс-проблемы: `ac_cv_func_malloc_0_nonnull=yes ac_cv_func_realloc_0_nonnull=yes` 4. **libiio v0.25**: только local backend (network/usb/xml/iiod отключены). 5. **libad9361-iio**: FIR-фильтр для sample rate < 2.083 MSPS (`ad9361_set_bb_rate`). Без него AD9361 отвергает 1.92 MSPS (EINVAL). После сборки liquid **обязательно** проверить, что подхвачены и FFTW, и libfec: `grep -E "HAVE_LIBFFTW3F 1|HAVE_LIBFEC 1" config.log` → обе строки. Без FFTW liquid молча падает на встроенный FFT (бюджет CPU невалиден); без libfec нет RS. ⚠️ При обновлении зависимостей liquid надо пересобирать через `make clean`: его makefile не отслеживает зависимость от `config.h`, и `configure`+`make` без очистки лишь переустановят старую `libliquid.a` (без FEC). Проверка перед деплоем: `arm-linux-gnueabihf-nm $XARM/lib/libliquid.a | grep -c init_rs_char` → ненулевое = RS-символы libfec действительно связаны. ### 4.3 Ключевые решения по сборке - **Только статическая линковка** (`-static`). Причина: не зависим от libc прошивки Pluto, не таскаем .so, деплой = один файл. Цена +2 МБ — ничто при 1 ГБ RAM. - **Флаги обязательны**: `-O3 -mcpu=cortex-a9 -mfpu=neon -mfloat-abi=hard -ffast-math`. Без NEON бюджет CPU из раздела 2 не выполняется. - Одной командой: ```bash arm-linux-gnueabihf-gcc -O3 -mcpu=cortex-a9 -mfpu=neon -mfloat-abi=hard \ -ffast-math -static src/receiver.c src/common.c -o receiver \ -I$HOME/xarm/include -L$HOME/xarm/lib \ -lad9361 -liio -lliquid -lfec -lfftw3f -lm ``` Порядок библиотек критичен при статической линковке: зависимый идёт раньше (`-lad9361` перед `-liio`, `-lliquid` перед `-lfec`). ### 4.4 Деплой ```bash scp -O transmitter receiver root@192.168.2.1:/tmp/ # Pluto A scp -O transmitter receiver root@:/tmp/ # Pluto B ``` rootfs Pluto живёт в RAM: `/tmp` очищается при ребуте — это нормально, `deploy.sh` заливает заново. ## 5. Правила кода (конституция) 1. Язык — **C**. Python/GNU Radio в тракте данных запрещены. 2. Железо — только **libiio, контекст `local:`** (код исполняется на Pluto). Сетевые контексты `ip:` — только для отладочных утилит с хоста. 3. Бюджет: считай, что у тебя **одно ядро 667 МГц** на DSP; второе занято IIO/FEC/IO. Любая новая нагрузка в тракте — сначала через ofdm_bench. 4. Тяжёлые зависимости (FFmpeg, cJSON и т.п.) — запрещены. JSON руками, видео — raw UDP. 5. Порядок отладки: **кабель+аттенюатор → антенны на столе → дистанция**. Никогда не отлаживать новый код сразу «в воздухе». 6. Изменил PHY-параметры (M, CP, модуляция, rate) — перегони бенчмарк и обнови `docs/BENCHMARK.md` в том же коммите. ## 6. Параметры радиотракта | Параметр | Значение | Примечание | |---|---|---| | Sample rate | **1 920 000** | см. раздел 2; 3.84 MSPS запрещён | | Полоса | 1 500 000 | ~0.78 × rate | | Частота (симплекс-тест) | 915 МГц | обе платы на одной частоте | | FDD (этап 2) | A: TX 915/RX 868; B: TX 868/RX 915 | разнос 47 МГц | | TX gain (кабель) | −40 дБ | + аттенюатор 30–40 дБ обязателен | | TX gain (антенны) | −20…0 дБ | начинать с минимума | | RX gain | 50 дБ, manual | фиксированный | | OFDM | M=64, CP=16, taper=4, QPSK | liquid defaults + CRC32 | | FEC | RS(255,223), payload ≤ 1024 Б | 5 блоков на кадр | ⚠️ **FDD без дуплексеров**: собственный TX 915 МГц глушит свой RX 868 МГц широкополосным шумом. Симптом: loopback работает, дуплекс в эфире — нет. Меры: max tx_attenuation, разнос/кросс-поляризация антенн, SAW-фильтр 868 МГц на RX. Подробнее — `docs/NOTES.md`. ⚠️ **Регуляторика**: 868 МГц — SRD-диапазон (ограничения полосы/мощности/ duty cycle), 915 МГц в регионе ETSI занят GSM-900 uplink. Работа — кабель или минимальная мощность на столе, без внешних усилителей. ## 7. Формат кадра ``` OFDM-кадр liquid: [преамбула][заголовок 12 Б][payload ≤ 1275 Б][CRC32] Заголовок (12 байт): 0-1 0xF0 0xAA сигнатура 2-5 seq (uint32 BE) порядковый номер кадра 6-7 len (uint16 BE) длина исходных данных до FEC 8-9 nblocks число RS-блоков 10 last_block_bytes хвост последнего блока (0 = полный) 11 резерв ``` ⚠️ **Критично**: у liquid заголовок пользователя по умолчанию **8 байт**. Байты 8–11 передаются только после явного вызова на обеих сторонах: ```c ofdmflexframegen_set_header_len(fg, 12); // TX ofdmflexframesync_set_header_len(fs, 12); // RX ``` Без этого RX читает `hdr[10]` за пределами буфера (маскируется обрезкой по `original_len`, но это мина). ## 8. Запуск ### 8.1 Передача файла Pluto→Pluto ```bash scp -O test.bin root@192.168.2.1:/tmp/ # Pluto B — приёмник ssh root@ '/tmp/receiver -f 915000000 -r 1920000 -b 1500000 -c rs8 \ > /tmp/out.bin' # Pluto A — передатчик (кабель: -g -40 + аттенюатор!) ssh root@192.168.2.1 'cat /tmp/test.bin | /tmp/transmitter -f 915000000 \ -r 1920000 -b 1500000 -g -40 -p 0 -c rs8' # сверка md5sum test.bin && ssh root@ 'md5sum /tmp/out.bin' ``` ### 8.2 Бенчмарк CPU (перед любым изменением PHY) ```bash scp -O ofdm_bench root@192.168.2.1:/tmp/ ssh root@192.168.2.1 '/tmp/ofdm_bench 1.92' # критерий: "сигнал+кадры" ≥ 1.2× цели на одном ядре ``` ## 9. Известные проблемы и техдолг | # | Проблема | Статус | |---|---|---| | 1 | Заголовок 12 Б vs 8 Б по умолчанию у liquid (раздел 7) | ✅ исправлено: `set_header_len(12)` на обеих сторонах | | 2 | `recovered[4096]` в receiver при лимите nblocks≤100 (22 КБ) → переполнение стека на битом заголовке. Лимит должен быть `4096/RS_DATA = 18` | ✅ исправлено: лимит `nblocks ≤ 4096/RS_DATA = 18` | | 3 | `fec_decode()` liquid не сообщает о неисправимых RS-блоках → счётчик `rs_saved` фиктивен. Достоверный критерий — только CRC/md5 поверх данных | ✅ исправлено: CRC32 поверх данных до RS на TX, сверка после декода на RX; `rs_saved`/`rs_fail` достоверны (подтверждено loopback'ом, §12.2) | | 4 | Внешний NCO-CFO в receiver: `set_phase(0)` на границах буферов рвёт фазу; ofdmflexframesync и так компенсирует CFO сам | ✅ исправлено: внешний NCO удалён, CFO оставлен только как телеметрия | | 5 | TX пушит весь буфер 16384 сэмпла при кадре ~10300 → ~35% эфира впустую + паузы `-p` | ✅ исправлено: `iio_buffer_push_partial(idx)` + дренаж хвоста DMA перед закрытием | | 6 | Нет ARQ: потерянный кадр = молчаливая дыра в файле. seq в заголовке есть, но RX его игнорирует | ⚙️ частично: добавлен детектор пропусков seq (лог `[ПОТЕРЯ]` + счётчик «Потери seq»); ARQ ещё нет | | 7 | Файл `reciever.c` → переименовать в `receiver.c` (make его ждёт) | ✅ исправлено | ## 10. Дорожная карта 1. ✅ Бенчмарк CPU на ARM Pluto → выбран rate 1.92 MSPS 2. ✅ Перенос TX/RX на Pluto (`local:`), фиксы техдолга #1–#7 (см. §9) 3. ⚙️ **текущий этап** — файл через эфир (OTA), md5 на 1 и 10 МБ. Ввод в строй ведётся строго OTA; кабельный этап осознанно пропущен. ✅ 2026-07-14: цифровой BIST-loopback подтвердил DSP-тракт (§12.2) ✅ 2026-07-14: первый OTA-приём — 10 КБ байт-в-байт, EVM −22.7 дБ (§12.3); под RS/нагрузкой нужен троттлинг `-p` или двухпоточный RX 4. Антенны на столе: замер скорости, PER, счётчик потерянных seq (#6) 5. FDD 868/915 двусторонняя + меры из docs/NOTES.md 6. UDP-туннель поверх линка; затем web-настройка (libmicrohttpd) 7. Видео (raw UDP, пакеты ≤1472 Б) — при устойчивом PER ## 11. Диагностика (шпаргалка) | Симптом | Первое, что проверить | |---|---| | scp: `sftp-server not found` | использовать `scp -O` | | RX молчит | частоты TX/RX, `-g` TX, антенны/кабель, RX gain | | CRC бьётся, кадры находятся | SNR: снизить полосу, амплитуду TX (`-a 0.15`) | | md5 не сходится, CRC ок | потерянные кадры → смотреть разрывы seq (#6) | | Работает на малом трафике, падает под нагрузкой | CPU-лимит: rate > 1.92 MSPS? второй процесс на ядре 0? | | Loopback ок, дуплекс в эфире нет | самоглушение FDD (раздел 6) | ## 12. Журнал ввода в строй (commissioning) Ввод в строй ведётся строго **OTA** (антенны с самого начала). Кабельный этап регламента отладки (§5.5) осознанно пропущен — фиксируется как отклонение. ### 12.1 Программные фиксы перед первым включением (2026-07-14) Перед первой передачей закрыты DSP-техдолги, способные испортить приём даже при идеальном канале (детали и статусы — §9): удалён вредный внешний NCO-CFO-контур (#4), добавлена достоверная проверка целостности CRC32 поверх RS (#3), детектор пропусков seq (#6); TX переведён на partial-push + дренаж хвоста DMA (#5). PHY-параметры не менялись → бенчмарк §2 остаётся в силе. ### 12.2 Цифровой BIST-loopback (2026-07-14) — DSP-тракт без эфира Цель: изолировать код/протокол от RF **до** первого выхода в эфир. Внутренний цифровой шлейф AD9361 (`/sys/kernel/debug/iio/iio:device0/loopback = 1`), TX и RX на одной плате (192.168.2.1), файл 100 КБ случайных данных, режим `-c rs8`. | Пауза TX (`-p`) | Поймано/послано | RS испр | RS битых | Потери seq | md5 | EVM | |---|---|---|---|---|---|---| | 2 000 мкс | 76/100 | 2 | 17 | 24 | ✗ (дыры) | −56 дБ | | 20 000 мкс | 100/100 | 0 | 0 | 0 | ✅ совпал | −60 дБ | **Вывод: DSP-пайплайн полностью исправен** — при разгруженном RX 100% кадров приняты байт-в-байт, md5 совпал. Потери на паузе 2 мс — не баг фрейминга, а **конкуренция за CPU: TX+RX+iiod на одной 2-ядерной плате**; RX не успевает за эфиром и kernel роняет сэмплы (согласуется с бюджетом 1.22× из §2). В штатном двухплатном OTA у RX выделенное ядро — ожидается кратное снижение потерь. Проверка CRC-поверх-RS подтверждена в бою: 17 неисправимых кадров корректно отброшены (не ушли мусором в файл), 2 кадра реально восстановлены RS — счётчики `rs_saved`/`rs_fail` теперь достоверны (техдолг #3 закрыт по факту). ### 12.3 Первый двухплатный OTA (2026-07-14) — приём по воздуху Антенны, симплекс 915 МГц, дистанция «на столе». RX на плате B (192.168.3.1), TX на A (192.168.2.1), TX gain −30 дБ. Опорники плат сведены достаточно: |CFO| ≈ 0.0005, синхронизатор ловит **без** ручного свипа частоты. | Тест | Размер | FEC | `-p` | Поймано | RS испр | RS бит | Потери | Вывод/послано | md5 | RSSI | EVM | |---|---|---|---|---|---|---|---|---|---|---|---| | A | 10 КБ | none | 5000 мкс | 10 | — | — | 0 | 10/10 | ✅ **совпал** | −31 дБ | −22.7 дБ | | B | 100 КБ | rs8 | 2000 мкс | 77 | 4 | 13 | 26 | 61/100 | ✗ (дыры) | −31 дБ | −25.0 дБ | **Главное: первый файл прошёл по воздуху байт-в-байт (тест A)** — OTA-линия работоспособна. Радиоканал качественный: EVM −22…−25 дБ, RSSI −31 дБ, CFO ≈ 0, пойманные кадры почти все декодируются. **Ограничение (тест B): под нагрузкой с RS и паузой 2 мс теряется ~39% кадров** — причём на двух платах с выделенными ядрами, значит это **не** CPU-contention из §12.2. Причина конструктивная: **receiver однопоточный** — RS-декод выполняется inline в callback синхронизатора и отнимает его пропускную способность. Запас 1.22× (§2) не покрывает добавленную стоимость RS при сплошном потоке: RX отстаёт от эфира, kernel роняет сэмплы (отсюда и не пойманные кадры, и `RS битых` от частично побитых). «Второе ядро под RS» из архитектуры пока **не реализовано**. Рычаги к устойчивой передаче: (1) пауза `-p` — троттлинг TX под возможности RX (немедленный обход); (2) двухпоточный конвейер RX (refill+конвертация на ядре 1, синк на ядре 0 — резерв BENCHMARK §«резервы» п.2) — настоящее решение с приростом throughput. Следующий шаг: подобрать `-p` с нулевой потерей, затем md5 на 1 и 10 МБ; в `test_file_link.sh` дефолт `-p 0` заменён на параметр `PAUSE`. --- *Ввод в строй: 2026-07-14. Замеры CPU и базовые решения — 2026-07-10. Прошивка v0.38, liquid-dsp 1.8.0.*