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). Кросс-тулчейн:
sudo apt install -y build-essential autoconf automake libtool git wget \
gcc-arm-linux-gnueabihf
4.2 Зависимости (один раз, ~15 мин)
scripts/build_deps.sh собирает в $HOME/xarm статические armhf-библиотеки:
- FFTW 3.3.10:
--enable-single --enable-neon --enable-static - 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
После сборки liquid обязательно проверить, что FFTW подхвачен:
grep HAVE_LIBFFTW3F config.log → должно быть 1. Без FFTW liquid молча
падает на встроенный FFT и цифры бюджета CPU невалидны.
4.3 Ключевые решения по сборке
- Только статическая линковка (
-static). Причина: не зависим от libc прошивки Pluto, не таскаем .so, деплой = один файл. Цена +2 МБ — ничто при 1 ГБ RAM. - Флаги обязательны:
-O3 -mcpu=cortex-a9 -mfpu=neon -mfloat-abi=hard -ffast-math. Без NEON бюджет CPU из раздела 2 не выполняется. - Одной командой:
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 -lliquid -lfftw3f -lm
4.4 Деплой
scp -O transmitter receiver root@192.168.2.1:/tmp/ # Pluto A
scp -O transmitter receiver root@<pluto_B>:/tmp/ # Pluto B
rootfs Pluto живёт в RAM: /tmp очищается при ребуте — это нормально,
deploy.sh заливает заново.
5. Правила кода (конституция)
- Язык — C. Python/GNU Radio в тракте данных запрещены.
- Железо — только libiio, контекст
local:(код исполняется на Pluto). Сетевые контекстыip:— только для отладочных утилит с хоста. - Бюджет: считай, что у тебя одно ядро 667 МГц на DSP; второе занято IIO/FEC/IO. Любая новая нагрузка в тракте — сначала через ofdm_bench.
- Тяжёлые зависимости (FFmpeg, cJSON и т.п.) — запрещены. JSON руками, видео — raw UDP.
- Порядок отладки: кабель+аттенюатор → антенны на столе → дистанция. Никогда не отлаживать новый код сразу «в воздухе».
- Изменил 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 передаются только после явного вызова на обеих сторонах:
ofdmflexframegen_set_header_len(fg, 12); // TX
ofdmflexframesync_set_header_len(fs, 12); // RX
Без этого RX читает hdr[10] за пределами буфера (маскируется обрезкой
по original_len, но это мина).
8. Запуск
8.1 Передача файла Pluto→Pluto
scp -O test.bin root@192.168.2.1:/tmp/
# Pluto B — приёмник
ssh root@<pluto_B> '/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@<pluto_B> 'md5sum /tmp/out.bin'
8.2 Бенчмарк CPU (перед любым изменением PHY)
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) | чинить до передачи файлов |
| 2 | recovered[4096] в receiver при лимите nblocks≤100 (22 КБ) → переполнение стека на битом заголовке. Лимит должен быть 4096/RS_DATA = 18 |
чинить |
| 3 | fec_decode() liquid не сообщает о неисправимых RS-блоках → счётчик rs_saved фиктивен. Достоверный критерий — только CRC/md5 поверх данных |
знать; добавить CRC после декода |
| 4 | Внешний NCO-CFO в receiver: set_phase(0) на границах буферов рвёт фазу; ofdmflexframesync и так компенсирует CFO сам |
убрать или обосновать замером |
| 5 | TX пушит весь буфер 16384 сэмпла при кадре ~10300 → ~35% эфира впустую + паузы -p |
оптимизация: push ровно idx сэмплов |
| 6 | Нет ARQ: потерянный кадр = молчаливая дыра в файле. seq в заголовке есть, но RX его игнорирует | минимум: детектор пропусков seq с логом |
| 7 | Файл reciever.c → переименовать в receiver.c (make его ждёт) |
тривиально |
10. Дорожная карта
- ✅ Бенчмарк CPU на ARM Pluto → выбран rate 1.92 MSPS
- Перенос TX/RX на Pluto (
local:), фиксы #1, #2, #7 - Файл через кабель + аттенюатор, md5 на 1 и 10 МБ
- Антенны на столе: замер скорости, PER, счётчик потерянных seq (#6)
- FDD 868/915 двусторонняя + меры из docs/NOTES.md
- UDP-туннель поверх линка; затем web-настройка (libmicrohttpd)
- Видео (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) |
Замеры и решения актуальны на 2026-07-10, прошивка v0.38, liquid-dsp 1.8.0.