695 lines
54 KiB
Markdown
695 lines
54 KiB
Markdown
# 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](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). Кросс-тулчейн:
|
||
|
||
```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 \
|
||
-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 Деплой
|
||
|
||
```bash
|
||
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. Правила кода (конституция)
|
||
|
||
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@<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)
|
||
|
||
```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 его игнорирует | ✅ исправлено на FDD (§12.9, §10 п.7): детектор пропусков seq → NACK в STATUS → селективный ретрансмит из окна TX 2048 кадров + кольцо переупорядочивания на RX + хендшейк END/COMPLETE (хвостовые потери). 10 МБ байт-в-байт, A возвращает 0 только по подтверждённому COMPLETE. **На симплексе (без `-F`) ARQ невозможен** — там остаются детектор `[ПОТЕРЯ]` и групповой FEC 16+2 (≤2 потери/группу, §12.7, п.6) |
|
||
|
||
## 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** (§12.8, §12.9) — техдолг #6 закрыт.
|
||
Этап 1: две несущие, A TX915/RX868, B зеркально. Этап 2: обратный канал
|
||
STATUS (кумулятивный снимок RX: highest_seq + NACK-список), gate пройден —
|
||
PER форварда не деградирует от обратного TX. Этап 3: EOF-хендшейк END/COMPLETE
|
||
(хвостовые потери молчаливы — END несёт итог передачи) + селективный
|
||
ретрансмит по NACK (окно TX 2048 кадров, повтор не чаще 400 мс) + кольцо
|
||
переупорядочивания на RX (1024 кадра). **Побайтовый лосслесс на 10 МБ:
|
||
44 потери вытянуты 45 ретрансмитами, md5 ==.** ARQ и group-FEC работают
|
||
вместе: паритет гасит одиночные потери без задержки, ARQ добирает остальное.
|
||
8. ✅ **UDP-туннель поверх линка** (§12.10) — `udp_gw` инкапсулирует UDP-датаграммы
|
||
в самосинхронизирующиеся рекорды поверх байтового потока transmitter/receiver,
|
||
без единой правки ядра тракта. Радио-смоук на антеннах пройден дважды подряд:
|
||
md5 == байт-в-байт, 0 дропов/ресинков/дыр. Web-настройка (libmicrohttpd) —
|
||
осталась нереализованной, следующий шаг п.8
|
||
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.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» нет; формат
|
||
кадра с резервом под неё.
|
||
|
||
### 12.8 FDD 868/915 + обратный канал STATUS — gate-замер (§10 п.7, этапы 1–2) — 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-хендшейк.
|
||
|
||
### 12.9 ARQ: END/COMPLETE + селективный ретрансмит (§10 п.7, этап 3) — 2026-07-16
|
||
|
||
Закрытие техдолга #6. Три подэтапа, каждый принят на железе отдельно.
|
||
|
||
**Механизм.** (а) **END** (кадр `F0 E7`, payload 12 Б: total_seq + total_bytes):
|
||
хвостовые потери молчаливы — у последних кадров нет следующего seq, по которому
|
||
детектор увидел бы дыру. A шлёт END каждые 100 мс после EOF, пока не услышит
|
||
COMPLETE (таймаут 10 с → **exit 2**). (б) **Окно TX** `txwin[2048]` на A хранит
|
||
сырой payload: `phy_tx_frame` детерминирован, повтор уходит байт-в-байт.
|
||
(в) **NACK** едут в кумулятивном STATUS (50 старейших seq); ретрансмит
|
||
per-seq не чаще **400 мс** — расчёт петли потеря→STATUS→повтор→подтверждение при
|
||
50% доставки STATUS (§12.8). (г) **Кольцо переупорядочивания** `rb[1024]` на B:
|
||
ретрансмит приезжает вне очереди, в stdout уходит только непрерывный префикс
|
||
(курсор `next_out`). COMPLETE ⇔ `next_out == total_seq`.
|
||
|
||
**Результаты** (кабель+аттенюатор, `TXGAIN −30`, `FBGAIN −15`, `-p 4000`):
|
||
|
||
| Прогон | Потери seq | Ретрансмитов | Итог |
|
||
|---|---|---|---|
|
||
| 1 МБ, GROUPFEC=0 | 4 / 1024 | 4 | md5 ==, COMPLETE за 88 мс, END × 1 |
|
||
| 10 МБ, GROUPFEC=0 | 44 / 10240 | 45 | md5 ==, `NACK вне окна: 0`, STATUS 93% |
|
||
| 1 МБ, GROUPFEC=1 | 3 / 1153 (GF восст.) | **0** | md5 ==, COMPLETE за 88 мс |
|
||
| 10 МБ, GROUPFEC=1 | 51 / 11520 (45 GF восст.) | **6** | md5 ==, COMPLETE за 185 мс |
|
||
| негатив, FBGAIN −89 | — | — | A честно **exit 2** за 10 с (раньше рапортовал успех) |
|
||
|
||
Окно TX на 10 МБ провернулось ~5 раз без единого «NACK вне окна», кольцо RX не
|
||
переполнялось ни разу — оба расчётных риска закрыты замером. Доставка STATUS в
|
||
END-фазе растёт до ~93% (эфир пуст, самоглушение A прекращается) — плато §12.8
|
||
на хендшейк не влияет, как и предполагалось.
|
||
|
||
**Гибрид закрывает почти всё паритетом.** На GROUPFEC=1 паритет гасит 45 из 51
|
||
потери на 10 МБ — ретрансмитов нужно всего **6** (против 55 на том же прогоне
|
||
без group-FEC), то есть на порядок меньше эфирного времени на дозакрытие тех же
|
||
потерь. Это и есть цель гибрида: GF закрывает большинство дыр немедленно, ARQ —
|
||
только то, что паритету не по силам (>2 стираний/группу). Полная приёмочная
|
||
матрица 3c (7 команд из плана, включая обе регрессии симплекса) пройдена
|
||
2026-07-16 — все результаты совпали с ожиданием, включая штатный FAIL симплекса
|
||
без group-FEC (нет ARQ на симплексе, техдолг #6 закрыт только на FDD).
|
||
|
||
**Клифф канала.** Стресс `TXGAIN −38/−40/−45` бесполезен: канала там нет вовсе —
|
||
`Заголовков 3322, CRC 0` (заголовок BPSK живёт, payload QPSK+RS падает первым).
|
||
Переход −30 (PER 0.4%) → −38 (100%) резкий, промежуточной точки на кабеле почти
|
||
нет. Поведение в обрыве корректное: A упёрся в расчётный потолок rate-limit
|
||
(2246 ретрансмитов ≈ 50 × 18 с / 0.4 с — эфир не залит дублями), B не поднял
|
||
COMPLETE, A вышел с 2. Настоящий PER-стресс — свипом в §12.4, не здесь.
|
||
|
||
**Симплекс идёт мимо кольца.** Без `-F` ретрансмитов нет, seq только растёт —
|
||
переупорядочивать нечего, а дыру закрыть некому: кольцо лишь задержало бы вывод
|
||
до EOF. Отсюда ветка `g_fb_fg ? rb : stdout` в `process_frame`/`group_flush`.
|
||
|
||
### 12.10 UDP-туннель — радио-смоук (§10 п.8) — 2026-07-17
|
||
|
||
**Реализация.** `udp_gw` — самостоятельный бинарь без зависимостей от
|
||
iio/liquid/fec, два режима: `-l <port>` (UDP → рекорды в stdout) и
|
||
`-d ip:port` (рекорды из stdin → UDP). Рекорд — 8 Б оверхеда (magic `D5 5D`,
|
||
len, id, flags, hcrc8) поверх уже существующего байтового потока
|
||
transmitter/receiver — ядро тракта не тронуто ни строкой. Парсер
|
||
самосинхронизируется по magic+hcrc8 после дыры (симплекс рвёт границы
|
||
рекордов произвольно). `stdout` шлюза в режиме `-l` — `O_NONBLOCK`: вход
|
||
быстрее линка дропается с счётчиком, а не блокирует приём (семантика UDP).
|
||
`udp_probe` (send/recv по UDP, свой мини-инструмент — busybox без `nc`/`socat`,
|
||
правило №1 запрещает Python и на хосте) закрывает бринг-ап без внешних
|
||
зависимостей. Пиннинг `udp_gw` на отдельное ядро не сделан — по факту
|
||
замера не нужен (Overrun/FQdrp = 0 при работающем шлюзе, ниже).
|
||
|
||
**Loopback на одной плате** (без радио, сокет↔сокет через localhost):
|
||
66/66 датаграмм, md5 ==, все счётчики (дропы/ресинк/bad_crc/дыры) — 0.
|
||
|
||
**Радио-смоук** (антенны на столе, симплекс 915 МГц, `TXGAIN −20`, `-G -p 3000`,
|
||
64 КБ / 66 датаграмм по 1000 Б, темп генератора `PACE_MS=15` ≈65 КБ/с — с
|
||
запасом под потолок линка ~107 КБ/с): два прогона подряд — md5 == байт-в-байт,
|
||
`отправлено 66 | дропов 0` на входе, `доставлено 66 | ресинк 0 Б | bad_crc 0 |
|
||
дыр(id) 0` на выходе, `Потери seq 0` на радио, EVM −18.5…−26.2 дБ.
|
||
|
||
Правило №7 (кабель+аттенюатор до антенн) сознательно не применялось: `udp_gw`
|
||
не меняет PHY ни на байт (слой поверх уже принятого байтового потока), его
|
||
собственная логика парсера закрыта юнит-тестом (`make gwtest`) и loopback-ом
|
||
на плате — радио-прогон здесь равносилен штатной приёмке линка.
|
||
|
||
**Отладка теста, не туннеля.** Первый радио-прогон (ручные команды в терминале)
|
||
дал пустой файл на приёме: у `udp_probe recv` сторожевой таймер простоя тикает
|
||
с момента ЗАПУСКА процесса, а не с первой датаграммы — интерактивная вставка
|
||
команд заняла больше отведённых 10 с, и приёмная цепочка на B самоликвидировалась
|
||
до начала передачи на A. После автоматизации в `scripts/test_udp_tunnel.sh`
|
||
всплыли ещё два эффекта хореографии: (а) единовременный `killall` терял хвост
|
||
последней group-FEC-группы, потому что `receiver` флашит его только в момент
|
||
собственной смерти (`receiver.c:798`, после основного цикла) — гашение
|
||
переписано на строгий порядок «receiver → пауза на прохождение хвоста через
|
||
`udp_gw -d` → `udp_probe»; (б) старт генератора без ожидания готовности
|
||
transmitter ронял 1–2 первые датаграммы в EAGAIN, пока iio ещё поднимался —
|
||
заменено на ожидание строки «Ожидание данных» в логе TX. Оба фикса —
|
||
в `scripts/test_udp_tunnel.sh`, радиотракт и `udp_gw` не менялись.
|
||
|
||
**Гибрид с group-FEC.** Паритет гасит одиночные потери без задержки (RX снимает
|
||
восстановленные seq с NACK — ретрансмит не нужен), ARQ добирает то, что паритету
|
||
не по силам (>2 стираний/группу). `grp_failed > 0` при целом md5 — норма: группа
|
||
не собралась, но ARQ её дозакрыл.
|
||
|
||
---
|
||
*Ввод в строй: 14–15-07-2026. Замеры CPU и базовые решения — 10-07-2026.
|
||
Прошивка v0.38, liquid-dsp 1.8.0.* |