Files
Pluto-SDR/README.md
Maxim fc836f2af1 Этап 2
- README §12.8: таблица gate (PER0 3/5/14, PER2 1-11 при FBGAIN -80..0,
  доставка STATUS 0% при <=-40, плато ~50% при -20..0), вердикт
- NOTES §2.2: разбор плато 50% (структурное: fb-RX платы A глохнет в её же
  TX-бёрсты, duty 57% при -p 4000), окно fb-TX -20..-10, пересмотр критерия
  доставки (STATUS кумулятивен, 5 Гц эффективных достаточно), TDD не нужен
- BENCHMARK.md: framegen 570.6 мкс/кадр 255 Б -> STATUS 10 Гц ~0.6% ядра
2026-07-16 11:42:08 +03:00

43 KiB
Raw Blame History

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.31.5 Мбит/с (~160190 КБ/с). Файл 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, поэтому они собираются до него.

  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 не выполняется.
  • Одной командой:
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.

Зелёные 0104 подтверждают компилируемость и статические инварианты. Они не подтверждают передачу данных по эфиру — это по-прежнему ручной чек-лист: scripts/test_file_link.sh, деплой, бенчмарк на плате (§8.2).

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 дБ + аттенюатор 3040 дБ обязателен
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 байт. Байты 811 передаются только после явного вызова на обеих сторонах:

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. Дорожная карта

  1. Бенчмарк CPU на ARM Pluto → выбран rate 1.92 MSPS
  2. Перенос TX/RX на Pluto (local:), фиксы техдолга #1#7 (см. §9)
  3. ⚙️ текущий этап — файл через эфир (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
  4. антенны на столе — PER 10 МБ ≈ 0.36%, свип TX gain (§12.4), рабочая точка 20 дБ; линия SNR-ограничена, целостность 100%
  5. 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).
  6. Групповой стирающий FEC 16+2 (§12.7): восстановление молчаливых потерь кадров БЕЗ обратного канала (RAID-6-стиль P+Q над GF(256): 16 кадров данных + 2 паритета, закрывает ≤2 стёртых кадра/группу). Соук 10 МБ байт-в-байт на рабочей точке -p 4000. Защита информации (лёгкий AEAD, +16 Б/кадр, <1% CPU) — отложена, требования ОКР «R-Link» нет; формат с резервом под неё.
  7. FDD 868/915 двусторонняя + ARQ с обратным каналом + меры из docs/NOTES.md
  8. UDP-туннель поверх линка; затем web-настройка (libmicrohttpd)
  9. Видео (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.10.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.10.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 (не ×45 — эфир кадра 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 gftest774400 кейсов (все 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» нет; формат кадра с резервом под неё.

12.8 FDD 868/915 + обратный канал STATUS — gate-замер (§10 п.7, этапы 12) — 2026-07-16

Подготовка к ARQ (техдолг #6). Обе платы в FDD (ENSM fdd, оба cf-девайса, LO по ридбэку с допуском 100 Гц — PLL округляет к сетке ~2 Гц): данные A→B на 915, приёмник B шлёт кумулятивный STATUS (кадр F0 55, 255 Б = 1 RS-блок, 10 Гц, поток C) на 868; передатчик A слушает 868 потоком fb_rx (ядро 1). Стоимость framegen STATUS — 570.6 мкс/кадр ≈ 0.6% ядра (BENCHMARK.md).

Gate самоглушения (кабель+аттенюатор, TXGAIN 30, -p 4000, GROUPFEC=0 — сырой PER, 1 МБ = 1024 кадра на прогон):

Режим FBGAIN, дБ Потери seq /1024 Доставка STATUS B→A
симплекс (PER₀, 3×) 3 / 5 / 14
FDD + STATUS 80 / 60 / 40 5 / 5 / 5 0%
FDD + STATUS 20 4 47%
FDD + STATUS 10 1 52%
FDD + STATUS 0 (3×) 4* / 11 / 7 52 / 50 / 51%

* плюс первые «RS битых: 3» — лёгкий самодесенс RX платы B на максимуме её fb-TX; рабочее окно fb-TX на кабеле 20…10.

Выводы. (1) PER₂ ≈ PER₀ при ЛЮБОМ усилении обратного канала — излучение STATUS форвард-линк не деградирует (среднее 0.51% против 0.72% базы; PER₁≈PER₀ снято этапом 1). (2) Доставка STATUS упирается в плато ~50% независимо от усиления — структурный эффект: fb-RX платы A глохнет на время её собственных TX-бёрстов (duty 57% при -p 4000), STATUS выживает в паузах. Для ARQ это приемлемо: STATUS кумулятивен, 50% потерь = эффективные ~5 Гц обратной связи; в фазах ретрансмита/END духота эфира падает и доставка растёт там, где нужна.

Gate пройден (критерий уточнён: доставка стабильна И PER₂PER₀ ≤ +0.3 п.п. И PER₁≈PER₀), fallback TDD не требуется. Разбор — NOTES §2.2. Дальше — этап 3: селективный ретрансмит по NACK + END/COMPLETE-хендшейк.


Ввод в строй: 1415-07-2026. Замеры CPU и базовые решения — 10-07-2026. Прошивка v0.38, liquid-dsp 1.8.0.