Диагностика узкого места RX (поток B sync+RS насыщает ядро 0). Добавлены CLI-режимы для ofdm_bench: - -g гэп (умолч. 7680 = -p 4000 @ 1.92; -g 0 = duty 1) - -P payload (1275=rs8 / 1024=none геометрия) - -H hog-поток на соседнем ядре (контеншн L2/DDR как от потока A) - -c ядро замера Замер разделён на conv+sync / sync-only / только конвертация, добавлен bench_rs (мкс/блок fec_decode RS(255,223), nerr 0/4/16), печать duty и расчёт p_min для realtime @ 1.92 Msps. Вскрывает артефакт прежней методики: "2.34 Msps" — смесь при duty 57%, чистая демодуляция ~1.65 Msps. Никаких изменений PHY.
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 Зависимости (один раз, ~20 мин)
scripts/build_deps.sh собирает в $HOME/xarm статические armhf-библиотеки.
Порядок важен: liquid зависит от FFTW и libfec, поэтому они собираются до него.
- FFTW 3.3.10:
--enable-single --enable-neon --enable-static - libfec (jgaeddert): Reed-Solomon для liquid. liquid не реализует RS
сам — оборачивает libfec. Без неё
fec_create(LIQUID_FEC_RS_M8)вернёт NULL и-c rs8молча пойдёт БЕЗ FEC. Собирать строго ДО liquid. - 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 - libiio v0.25: только local backend (network/usb/xml/iiod отключены).
- 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 не выполняется. - Одной командой:
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 Деплой
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) | ✅ исправлено: 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. Дорожная карта
- ✅ Бенчмарк CPU на ARM Pluto → выбран rate 1.92 MSPS
- ✅ Перенос TX/RX на Pluto (
local:), фиксы техдолга #1–#7 (см. §9) - ⚙️ текущий этап — файл через эфир (OTA). Ввод в строй строго OTA;
кабельный этап осознанно пропущен.
✅ 2026-07-14: цифровой BIST-loopback подтвердил DSP-тракт (§12.2)
✅ 2026-07-14: первый OTA-приём — 10 КБ байт-в-байт, EVM −22.7 дБ (§12.3)
✅ 2026-07-14: пауза
-p 6000убрала throughput-потери (100 КБ — 100/100); на 1 МБ остаточный PER ≈ 0.2% (2 кадра). Побайтовый md5 на объёме отложен на ARQ/дуплекс (п.5) — решение §12.3; веха симплекса = PHY-линия + PER - ✅ 2026-07-14: антенны на столе — PER 10 МБ ≈ 0.36%, свип TX gain (§12.4), рабочая точка −20 дБ; линия SNR-ограничена, целостность 100%
- 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) |
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 дБ |
| B′ | 100 КБ | rs8 | 6000 мкс | 100 | 0 | 0 | 0 | 100/100 | ✅ совпал | −34 дБ | −16.0 дБ |
| C | 1 МБ | rs8 | 6000 мкс | 1022 | 0 | 0 | 2 | 1022/1024 | ✗ (2 кадра) | −30 дБ | −26.8 дБ |
| D | 10 МБ | rs8 | 6000 мкс | 10240 | 2 | 0 | 37 | 10203/10240 | ✗ (37 кадров) | −30 дБ | −25.9 дБ |
Главное: первый файл прошёл по воздуху байт-в-байт (тест A) — OTA-линия работоспособна. Радиоканал качественный: EVM −16…−27 дБ, RSSI −30…−34 дБ, CFO ≈ 0, пойманные кадры почти все декодируются.
Пропускная способность (тесты B→B′): пауза 2 мс теряет ~39% кадров, 6 мс — 0%.
Причём на двух платах с выделенными ядрами, значит это не CPU-contention из
§12.2. Причина конструктивная: receiver однопоточный — RS-декод выполняется
inline в callback синхронизатора и отнимает его пропускную способность. Запас
1.22× (§2) не покрывает добавленную стоимость RS при сплошном потоке; троттлинг
TX паузой -p 6000 возвращает систематическую потерю в ноль (100 КБ — 100/100).
Остаточный PER (тесты C, D): 0.2% на 1 МБ (2/1024) и 0.36% на 10 МБ (37/10240).
Это уже не throughput (кадры не накапливаются, RS битых: 0 на всех объёмах), а
спорадические одиночные пропуски. RS битых: 0 на 10 МБ — важно: проверка CRC-
поверх-RS ни разу не пропустила мусор в файл, а RS дважды реально исправил ошибки
(тест D). При симплексе без ARQ пропущенный кадр — молчаливая дыра (техдолг #6),
поэтому md5 на объёме не сходится, хотя PHY-линия по факту надёжна (PER ≈ 0.3%,
целостность 100%). CPU RX справляется: PER не растёт с объёмом → отставание не
накапливается.
Дальнейшие рычаги: (1) устойчивая передача файла требует избыточности —
на симплексе это forward-redundancy (TX гонит файл N раз, RX пишет кадры по
seq в нужное смещение, дыры заполняются за проходы), полноценный ARQ — на этапе
дуплекса (§10 п.5); (2) двухпоточный конвейер RX (BENCHMARK §«резервы» п.2) —
поднимет throughput и позволит убрать паузу. test_file_link.sh теперь с
параметрами PAUSE/FEC/TXGAIN.
Решение (2026-07-14): побайтовый лосслесс на объёме отложен на этап дуплекса — там ставится полноценный ARQ с обратным каналом (§10 п.5). На текущем симплекс- этапе forward-redundancy не внедряем; веха этапа — работоспособная PHY-линия и измеренный PER.
12.4 Свип TX gain — карта PER/EVM (2026-07-14)
1 МБ, rs8, -p 6000, дистанция «на столе», симплекс 915 МГц.
| TX gain | RSSI | EVM | Потери seq | RS испр | RS бит | PER (потери) | Примечание |
|---|---|---|---|---|---|---|---|
| −40 дБ | −45 дБ | −15.0 дБ | 33 | 27 | 41 | ~3.2% (+41 неиспр) | SNR-floor: RS активно спасает, но 41 блок неисправим |
| −30 дБ | −31 дБ | −24.8 дБ | 5 | 0 | 1 | 0.49% | рабочая зона |
| −20 дБ | −22 дБ | −32.9 дБ | 3 | 0 | 0 | 0.29% | рабочая точка (запас в обе стороны) |
| −10 дБ | −11 дБ | −39.7 дБ | 1 | 0 | 0 | 0.10% | лучший PER; вход RX ещё не насыщен |
Выводы:
- Линия SNR-ограничена, а не насыщением: EVM монотонно улучшается −15 → −40 дБ с ростом мощности; насыщения входа RX (усиление 50 дБ фикс.) не видно даже при RSSI −11 дБ. Потолок по gain на этой геометрии не достигнут.
- PER падает с мощностью: 3.2% (−40) → 0.1% (−10). Плато −20…−10 дБ даёт PER 0.1–0.3% при EVM −33…−40 дБ, RS даже не требуется (0 неисправимых).
- RS реально работает под нагрузкой: на −40 дБ спас 27 кадров, забракованных liquid-CRC. До фикса #3 они ушли бы мусором или фиктивно «спасёнными» — это боевое подтверждение всей FEC-цепочки (libfec + CRC-поверх-RS).
- Рабочая точка симплекса: TX gain −20 дБ (PER 0.29%, EVM −33 дБ, запас по SNR вниз и до насыщения вверх); для максимальной надёжности на этой дистанции −10 дБ.
Итог симплекс-этапа: PHY OTA-линия работает и охарактеризована по мощности, целостность 100% (мусор в файл не попадает), PER ≈ 0.1–0.3% в рабочей зоне. Остаток к «надёжной побайтовой» передаче = ARQ на этапе дуплекса (§10 п.5).
Ввод в строй: 2026-07-14. Замеры CPU и базовые решения — 2026-07-10. Прошивка v0.38, liquid-dsp 1.8.0.