40 KiB
Pluto-link — OFDM-радиолиния между двумя PlutoSDR
Цифровая радиолиния точка-точка на базе двух ADALM-Pluto+ (клон, Zynq-7020). PHY: OFDM (liquid-dsp) + RS(255,223) FEC. Весь сигнальный тракт исполняется на ARM Cortex-A9 внутри Pluto; хост-ПК нужен только для сборки, заливки бинарей и снятия логов. Прототип в рамках ОКР «R-Link» (двухранговая самоорганизующаяся сеть); данный репозиторий покрывает уровень PHY/линка.
Быстрый старт (clone → сборка → приём/передача) — docs/USAGE.md.
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, gftest, deploy, clean
├── toolchain.env # CROSS=arm-linux-gnueabihf-, SYSROOT=$HOME/xarm
├── .gitea/workflows/ # offline CI (см. §4.5, docs/CI.md)
│ ├── 01-smoke.yml # runner жив, что есть в образе
│ ├── 02-guardrails.yml # grep-страж жёстких правил CLAUDE.md
│ ├── 03-gftest.yml # юнит-тест group-FEC (774 400 кейсов)
│ └── 04-cross-build.yml # приёмка ARM-тулчейна + сборка tx/rx/bench
├── 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
│ ├── group_fec.c # GF(256) 16+2 erasure FEC + offline self-test
│ └── ofdm_bench.c # бенчмарк CPU (не удалять!)
├── scripts/
│ ├── build_deps.sh # кросс-сборка FFTW + liquid-dsp → $HOME/xarm
│ ├── deploy.sh # scp -O бинарей в /tmp обеих плат
│ ├── test_file_link.sh # e2e: файл → эфир → md5-сверка
│ └── ci_guardrails.sh # логика 02-guardrails.yml, гоняется и локально
└── docs/
├── BENCHMARK.md # методика и результаты замеров CPU
├── CI.md # два runner'а, что тестируется, нюансы
├── COMPARISON.md # сравнение с коммерческими MANET-радио (Silvus, Sinosun)
├── NOTES.md # FDD, регуляторика, известные грабли
└── USAGE.md # руководство пользователя pluto-link
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 \
-Wl,--start-group -lad9361 -liio -lliquid -lfec -lfftw3f -lm -lpthread -Wl,--end-group
Библиотеки завёрнуты в -Wl,--start-group ... -Wl,--end-group: у
libad9361.a/libiio.a есть циклическая зависимость символов друг на друга,
и однопроходный порядок слева направо её не всегда разрешает — компоновщику
нужно несколько проходов по кругу. Баг был не виден на бумаге и всплыл
только на реальной кросс-сборке в CI (04-cross-build.yml, см. §4.5); без
--start-group/--end-group make all изредка падает на undefined
reference в зависимости от версии тулчейна.
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 заливает заново.
4.5 CI / автотесты (offline Gitea Actions)
На каждый push гоняются 4 workflow на self-hosted-runner'ах сервера, без
доступа в интернет и без actions/checkout (Marketplace офлайн недоступен).
Два независимых runner'а: host (Alpine, только grep/bash-проверки) и
armhf (нативный x86-64 Debian-контейнер с ARM-кросс-тулчейном — не
эмуляция ARM, обычная кросс-компиляция).
| Workflow | Runner | Проверяет |
|---|---|---|
01-smoke |
host |
runner жив, какие инструменты есть в образе |
02-guardrails |
host |
grep-страж жёстких правил CLAUDE.md (§5): rate, header_len=12, local:, static-флаги |
03-gftest |
armhf |
make gftest — 774 400 кейсов GF(256) group-FEC |
04-cross-build |
armhf |
make check-env && make all — реальная кросс-сборка tx/rx/bench |
Локально без runner'а: scripts/ci_guardrails.sh, make gftest, make check-env && make all. Полное описание архитектуры двух runner'ов,
почему это не эмуляция ARM, и разбор реальных ловушек (musl vs glibc,
group-linking из §4.3, параллелизм между runner'ами) — docs/CI.md.
Зелёные 01–04 подтверждают компилируемость и статические инварианты. Они
не подтверждают передачу данных по эфиру — это по-прежнему ручной
чек-лист: scripts/test_file_link.sh, деплой, бенчмарк на плате (§8.2).
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 (лог [ПОТЕРЯ] + счётчик) + групповой стирающий FEC 16+2 закрывает ≤2 потери/группу на симплексе без обратного канала (§12.7, п.6); полный ARQ — на дуплексе (п.7) |
10. Дорожная карта
- ✅ Бенчмарк CPU на ARM Pluto → выбран rate 1.92 MSPS
- ✅ Перенос TX/RX на Pluto (
local:), фиксы техдолга #1–#7 (см. §9) - ⚙️ текущий этап — файл через эфир (OTA). Ввод в строй строго OTA;
кабельный этап осознанно пропущен.
✅ 14-07-2026: цифровой BIST-loopback подтвердил DSP-тракт (§12.2)
✅ первый OTA-приём — 10 КБ байт-в-байт, EVM −22.7 дБ (§12.3)
✅ пауза-p 6000убрала throughput-потери (100 КБ — 100/100); на 1 МБ остаточный PER ≈ 0.2% (2 кадра). Побайтовый md5 на объёме отложен на ARQ/дуплекс (п.7) — решение §12.3; веха симплекса = PHY-линия + PER - ✅ антенны на столе — PER 10 МБ ≈ 0.36%, свип TX gain (§12.4), рабочая точка −20 дБ; линия SNR-ограничена, целостность 100%
- ✅ 3-стадийный RX-конвейер (§12.6): ядро 0 — только
ofdmflexframesync_execute; ядро 1 — refill+конвертация и RS-декод. Рабочая пауза-p 6000→-p 3000, throughput 90 → 120 КБ/с (×1.36), FQ drops 0 (RS не узкое место).-p 0на этом PHY недостижим — осознанное ограничение; побайтовый лосслесс на симплексе — групповым FEC (п.6, §12.7). - ✅ Групповой стирающий FEC 16+2 (§12.7): восстановление молчаливых
потерь кадров БЕЗ обратного канала (RAID-6-стиль P+Q над GF(256): 16 кадров
данных + 2 паритета, закрывает ≤2 стёртых кадра/группу). Соук 10 МБ
байт-в-байт на рабочей точке
-p 4000. Защита информации (лёгкий AEAD, +16 Б/кадр, <1% CPU) — отложена, требования ОКР «R-Link» нет; формат с резервом под неё. - FDD 868/915 двусторонняя + ARQ с обратным каналом + меры из 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 п.7); (2) двухпоточный конвейер RX (BENCHMARK §«резервы» п.2) —
поднимет throughput и позволит убрать паузу. test_file_link.sh теперь с
параметрами PAUSE/FEC/TXGAIN. Рычаг (2) реализован и опровергнут —
см. §12.5.
Решение (2026-07-14): побайтовый лосслесс на объёме отложен на этап дуплекса — там ставится полноценный ARQ с обратным каналом (§10 п.7). На текущем симплекс- этапе 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% в рабочей зоне. Остаток к «надёжной побайтовой» передаче на симплексе закрыт групповым FEC (§12.7, §10 п.6); полный ARQ — на этапе дуплекса (§10 п.7).
12.5 Двухпоточный RX и диагностика паузы (2026-07-14)
Реализован двухпоточный RX (поток A: refill+convert на ядре 1; поток B:
sync+RS на ядре 0, кольцо 16×32768). Цель — убрать TX-паузу -p 6000 —
НЕ достигнута: на 1 МБ чисто только при -p 8000 (хуже базы §12.4),
на низких паузах кольцо переполняется (столбец Overrun).
Диагностика через расширенный ofdm_bench (разбивка стадий, см.
docs/BENCHMARK.md «Разбивка стоимости по стадиям»):
| Замер (плата B, ядро 0) | Результат | Вывод |
|---|---|---|
sync-only, сплошные кадры (-g 0) |
1.65 Msps < 1.92 | синхронизатор сам не realtime при duty→1; -p 0 недостижим |
| «запас 1.22×» из §2 | артефакт duty 0.571 | под сплошным потоком запаса нет |
| только конвертация | 85 Msps (~2% нагрузки B) | вынос конвертации (что и дал двухпоток) почти бесполезен |
| RS-декод | 275 мкс/блок = 26% эфира кадра | крупнейшая переносимая стоимость; «RS мал» (§2) опровергнуто |
контеншн L2/DDR (-H) |
0.2% | кэш-гипотеза отвергнута; ядро 1 свободно под RS |
Диагноз: узкое место — не захват (конвертация бесплатна) и не кэш, а
сам sync + RS на одном ядре. Двухпоток вынес не то (конвертацию, ~2%),
оставив RS (26%) на ядре синхронизатора.
План: 3-стадийный конвейер — ядро 0: только sync; ядро 1: refill+conv+RS
+запись. Модель p_min sync-only ~1.3 мс. Абсолютный -p 0 на этом PHY недостижим
(нужен M=32/CP=8 или ARQ). Реализовано и измерено в §12.6.
12.6 3-стадийный RX-конвейер — результат (2026-07-14)
Реализован (receiver.c): поток A (ядро 1) refill+convert → кольцо сэмплов;
поток B (ядро 0) только ofdmflexframesync_execute, callback кладёт сырой
кадр в очередь fq; поток C (ядро 1) из fq → seq + RS + CRC-поверх-RS + запись.
Статистику печатает main. Два новых счётчика: Overrun (кольцо A→B) и
FQ drops (очередь B→C).
Свип паузы, 1 МБ, TXGAIN −20:
-p |
Кадров | Потери seq | Overrun | FQ drops | Оценка |
|---|---|---|---|---|---|
| 6000 | 1024 | 6 | 0 | 0 | чисто (SNR-пол) |
| 3000 | 1024 | 3 | 0 | 0 | чисто — рабочая точка |
| 2500 | 1024 | 0 | 0 | 0 | чисто (измеренный край) |
| 2000 | 1006 | 20 | 7 | 0 | колено (sync не успевает) |
| 1000 | 971 | 56 | 15 | 0 | перегруз sync ядра 0 |
| 0 | 970 | 52 | 15 | 0 | перегруз sync ядра 0 |
Соук 10 МБ на -p 3000: Потери 27/10240 = PER 0.26%, Overrun 0, FQ drops 0 —
чисто по throughput, остаток чисто SNR (как §12.4).
Выводы:
FQ drops = 0на всех паузах, включая-p 0— поток C (RS на ядре 1) ни разу не отстал. Тезис «RS на второе ядро» подтверждён: RS больше не узкое место.- Единственный лимит — sync на ядре 0 (
Overrun): чисто до-p 2500, колено на-p 2000. Реальное колено ~2500 выше нуль-запасной модели p_min 1333 мкс из-за джиттера/маржи планировщика (запас ~2×). - Рабочая точка:
-p 3000(вдвое меньше прежних-p 6000), соук-подтверждена, запас ~1000 мкс до overrun. Throughput 1024 Б/(5.33+3 мс) ≈ 120 КБ/с против ~90 КБ/с на-p 6000= ×1.36 (не ×4–5 — эфир кадра 5.33 мс доминирует над паузой; прежняя оценка исправлена). -p 0по-прежнему недостижим (sync сам 0.86× realtime, §12.5) — осознанное ограничение PHY, лосслесс на объёме — групповым FEC (§12.7) или ARQ (§10 п.7).
12.7 Групповой стирающий FEC 16+2 (§10 п.6) — 2026-07-15
Симплекс закрывает молчаливые потери кадров БЕЗ обратного канала (ARQ требует дуплекса, п.7). Потери одиночные и с известной позицией (из seq) — это стирания, поэтому оптимально стирающее кодирование, а не дублирование.
Метод (по расчёту). Группа = 16 кадров данных + 2 паритета над GF(256) (полином 0x11d, генератор g=2, RAID-6-стиль): P = ⊕dᵢ, Q = ⊕gⁱ·dᵢ. Позиции стираний известны → 2 паритета закрывают ≤2 стёртых кадра на группу.
| Вариант | Overhead | Остаток на 10 МБ (PER 0.26%) |
|---|---|---|
| Дублирование 2× | 100% | ~0.07 кадра |
| XOR 16+1 | 6.25% | ~0.6 группы — НЕ лосслесс |
| 16+2 (выбрано) | 12.5% | ~0.009 группы → ~99% передач байт-в-байт |
Формат: hdr[11] (был резерв) = флаги [bit7 GF_MODE][bit6 GF_PARITY][bit5 GF_Q] [bits4-0 idx 0..17]; hdr[11]=0 = legacy (RX автодетект по первому кадру, старый
путь не тронут). Паритетный кадр несёт 1026 Б (2 Б меты k′/tail_len
- 1024 Б паритета) → те же 5 RS-блоков (1275 Б в эфире), что и данные →
геометрия PHY не меняется, бенчмарк §2 в силе (правило №6 не срабатывает).
GF-математика самопроверена офлайн:
make gftest— 774400 кейсов (всеk=1..16, все пары стираний, все комбинации наличия P/Q), 0 ошибок. Стоимость GF-кодека < 0.2% ядра, ложится в поток C (RS).
Прогоны OTA (2026-07-15, TXGAIN −20, симплекс 915 МГц):
| Тест | Размер | -p |
Потери seq | Overrun | GF восст | GF провал | md5 |
|---|---|---|---|---|---|---|---|
Регрессия legacy (GROUPFEC=0) |
1 МБ | 3000 | 0 | 0 | — | — | ✅ |
| Group | 1 МБ | 3000 | 4 | 0 | 4 | 0 | ✅ |
| Group стресс (−29 дБ) | 1 МБ | 3000 | 3 | 0 | 3 | 0 | ✅ |
| Соук | 10 МБ | 3000 | 208 | 62 | 38 | 44 | ✗ |
Диагностика legacy (GROUPFEC=0) |
10 МБ | 3000 | 213 | 63 | — | — | ✗ |
| Соук | 10 МБ | 4000 | 33 | 0 | 31 | 0 | ✅ байт-в-байт |
Ключевая находка — overrun рвёт стирающий FEC на длинной дистанции.
Соук на -p 3000 дал 44 провала группы против ~6 по независимой модели
(λ=0.42 стирания/группу, Пуассон) = ×7 сверх ожидания → потери
кластеризуются в пачки, а не рассыпаны. Источник — 62 overrun ядра 0:
каждый роняет подряд идущие seq = подряд идущие idx одной группы, пробивая
бюджет ≤2. Диагностика (GROUPFEC=0, тот же 10 МБ) дала те же 63 overrun и
2.7% потерь → срыв ядра 0 присущ sustained-соуку сам по себе, group-FEC ни
при чём; рабочая точка -p 3000 из §12.6 снята на коротких прогонах и на
10 МБ-соук не переносится (поток B не держит realtime на непрерывном потоке
дольше).
-p 4000 даёт ядру 0 запас → Overrun 0 → чистый floor (33 стирания на
640 групп = λ=0.05, независимые одиночные) → 16+2 закрыл всё с запасом
(GF провал 0), 10 МБ байт-в-байт. Интерливер (разнос кадров группы против
пачек) оказался не нужен: убрав overrun паузой, получили редкие
независимые стирания, где у 16+2 колоссальный margin.
Рабочая точка group-FEC / длинных передач: -p 4000 (~95 КБ/с; цена
против -p 3000 — ~12 КБ/с за Overrun 0 и лосслесс). Короткие передачи
(≤1 МБ) остаются рабочими на -p 3000 (§12.6). Защита информации (лёгкий
AEAD, +16 Б/кадр, <1% CPU) — отложена, требования ОКР «R-Link» нет; формат
кадра с резервом под неё.
Ввод в строй: 14–15-07-2026. Замеры CPU и базовые решения — 10-07-2026. Прошивка v0.38, liquid-dsp 1.8.0.