Files
Pluto-SDR/README.md

330 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# pluto-link — OFDM-радиолиния между двумя PlutoSDR (весь код на ARM внутри Pluto)
Цифровая радиолиния точка-точка на базе двух ADALM-Pluto+ (клон, Zynq-7020).
PHY: OFDM (liquid-dsp) + RS(255,223) FEC. Весь сигнальный тракт исполняется
на ARM Cortex-A9 **внутри** Pluto; хост-ПК нужен только для сборки, заливки
бинарей и снятия логов. Прототип в рамках ОКР «R-Link» (двухранговая
самоорганизующаяся сеть); данный репозиторий покрывает уровень PHY/линка.
---
## 1. Аппаратная платформа (фактическая, проверено)
| Параметр | Значение | Как проверено |
|---|---|---|
| Плата | Pluto+ (клон, Zynq-**7020**, AD9361) | `cat /proc/cpuinfo` — 2 ядра |
| CPU | 2 × Cortex-A9 @ 667 МГц, NEON | `Features: ... neon vfpv3` |
| RAM | 1 ГБ (доступно ~1000 МБ) | `free` |
| Прошивка | v0.38-4-g95aad-dirty | баннер SSH |
| Доступ | SSH root@192.168.2.1 (пароль `analog`), USB-Ethernet | — |
⚠️ Стоковый ADALM-Pluto (7010, 512 МБ, 1 ядро) — **другая плата**; все
бюджеты CPU в этом документе рассчитаны для Pluto+. Не путать.
⚠️ В прошивке v0.38 нет sftp-server → заливать файлы только `scp -O`
(legacy-протокол).
## 2. Бюджет CPU — главное ограничение проекта
Замерено офлайн-бенчмарком `src/ofdm_bench.c` прямо на ARM Pluto
(одно ядро, static, `-O3 -mcpu=cortex-a9 -mfpu=neon -mfloat-abi=hard
-ffast-math`, liquid-dsp 1.8.0 **с FFTW/NEON** — проверено по
`HAVE_LIBFFTW3F` в config.log):
| Режим ofdmflexframesync | Пропускная способность | Вывод |
|---|---|---|
| Поиск преамбулы (шум) | **5.32 Msps** | дежурный режим, не лимитирует |
| Приём кадров (duty ~57%) | **2.34 Msps** | узкое место |
Следствия (зафиксированы как проектные решения):
- **Рабочий sample rate = 1.92 MSPS.** Запас 1.22× на ядре синхронизатора;
второе ядро — под IIO, RS-декод, ввод-вывод.
- **3.84 MSPS запрещён**: система работает на редких пакетах и лавинообразно
разваливается под нагрузкой (0.61× от realtime). Это худший вид отказа —
«на демо работало».
- Полезная скорость ≈ **1.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, deploy, clean
├── toolchain.env # CROSS=arm-linux-gnueabihf-, SYSROOT=$HOME/xarm
├── src/
│ ├── common.h # параметры OFDM/RS, дефолты, прототипы
│ ├── common.c # pluto_init (local:), pluto_configure
│ ├── transmitter.c # stdin → OFDM → AD9361 TX
│ ├── receiver.c # AD9361 RX → OFDM sync → stdout
│ └── ofdm_bench.c # бенчмарк CPU (не удалять!)
├── scripts/
│ ├── build_deps.sh # кросс-сборка FFTW + liquid-dsp → $HOME/xarm
│ ├── deploy.sh # scp -O бинарей в /tmp обеих плат
│ └── test_file_link.sh # e2e: файл → эфир → md5-сверка
└── docs/
├── BENCHMARK.md # методика и результаты замеров CPU
└── NOTES.md # FDD, регуляторика, известные грабли
```
## 4. Среда разработки и сборка
### 4.1 Хост
Любой Linux; проверено на Raspberry Pi OS 64-bit (aarch64). Кросс-тулчейн:
```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 \
-lad9361 -liio -lliquid -lfec -lfftw3f -lm
```
Порядок библиотек критичен при статической линковке: зависимый идёт раньше
(`-lad9361` перед `-liio`, `-lliquid` перед `-lfec`).
### 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` заливает заново.
## 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 передаются только после явного вызова на обеих сторонах:
```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 его игнорирует | ⚙️ частично: добавлен детектор пропусков seq (лог `[ПОТЕРЯ]` + счётчик «Потери seq»); ARQ ещё нет |
| 7 | Файл `reciever.c` → переименовать в `receiver.c` (make его ждёт) | ✅ исправлено |
## 10. Дорожная карта
1. ✅ Бенчмарк CPU на ARM Pluto → выбран rate 1.92 MSPS
2. ✅ Перенос TX/RX на Pluto (`local:`), фиксы техдолга #1#7 (см. §9)
3. ⚙️ **текущий этап** — файл через эфир (OTA), md5 на 1 и 10 МБ.
Ввод в строй ведётся строго OTA; кабельный этап осознанно пропущен.
✅ 2026-07-14: цифровой BIST-loopback подтвердил DSP-тракт (§12.2)
✅ 2026-07-14: первый OTA-приём — 10 КБ байт-в-байт, EVM 22.7 дБ (§12.3);
под RS/нагрузкой нужен троттлинг `-p` или двухпоточный RX
4. Антенны на столе: замер скорости, PER, счётчик потерянных seq (#6)
5. FDD 868/915 двусторонняя + меры из docs/NOTES.md
6. UDP-туннель поверх линка; затем web-настройка (libmicrohttpd)
7. Видео (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 дБ |
**Главное: первый файл прошёл по воздуху байт-в-байт (тест A)** — OTA-линия
работоспособна. Радиоканал качественный: EVM 22…25 дБ, RSSI 31 дБ, CFO ≈ 0,
пойманные кадры почти все декодируются.
**Ограничение (тест B): под нагрузкой с RS и паузой 2 мс теряется ~39% кадров** —
причём на двух платах с выделенными ядрами, значит это **не** CPU-contention из
§12.2. Причина конструктивная: **receiver однопоточный** — RS-декод выполняется
inline в callback синхронизатора и отнимает его пропускную способность. Запас
1.22× (§2) не покрывает добавленную стоимость RS при сплошном потоке: RX отстаёт
от эфира, kernel роняет сэмплы (отсюда и не пойманные кадры, и `RS битых` от
частично побитых). «Второе ядро под RS» из архитектуры пока **не реализовано**.
Рычаги к устойчивой передаче: (1) пауза `-p` — троттлинг TX под возможности RX
(немедленный обход); (2) двухпоточный конвейер RX (refill+конвертация на ядре 1,
синк на ядре 0 — резерв BENCHMARK §«резервы» п.2) — настоящее решение с приростом
throughput. Следующий шаг: подобрать `-p` с нулевой потерей, затем md5 на 1 и
10 МБ; в `test_file_link.sh` дефолт `-p 0` заменён на параметр `PAUSE`.
---
*Ввод в строй: 2026-07-14. Замеры CPU и базовые решения — 2026-07-10.
Прошивка v0.38, liquid-dsp 1.8.0.*