Initial commit: OFDM-радиолиния на двух Pluto+ (PHY/link, код на ARM)
- src: transmitter, receiver, ofdm_bench, common (libiio local: + liquid-dsp) - scripts: кросс-сборка зависимостей, деплой scp -O, e2e-тест с md5 - docs: бенчмарк CPU (выбор rate 1.92 MSPS), грабли FDD/регуляторика - все текстовые файлы нормализованы в LF (.gitattributes принудительно): CRLF в toolchain.env/makefile ломал source и make на Linux - .vscode: IntelliSense в режиме linux-gcc-arm, заголовки из .xarm/include Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
26
.gitattributes
vendored
Normal file
26
.gitattributes
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# pluto-link: весь код исполняется на Linux (RPi-хост и ARM Pluto).
|
||||
# Принудительный LF везде — CRLF в makefile/toolchain.env/скриптах ломает
|
||||
# make и source на плате ("bad interpreter", \r в переменных PLUTO_A/XARM).
|
||||
|
||||
* text=auto eol=lf
|
||||
|
||||
# Исходники и сборка
|
||||
*.c text eol=lf
|
||||
*.h text eol=lf
|
||||
*.sh text eol=lf
|
||||
makefile text eol=lf
|
||||
*.env text eol=lf
|
||||
*.cmake text eol=lf
|
||||
|
||||
# Документация
|
||||
*.md text eol=lf
|
||||
|
||||
# Конфиги IDE (json читает и Windows — LF безопасен)
|
||||
*.json text eol=lf
|
||||
|
||||
# Бинарные артефакты (если когда-то попадут в репо — не трогать)
|
||||
*.bin binary
|
||||
*.a binary
|
||||
*.so binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# --- артефакты сборки ---
|
||||
build/
|
||||
*.o
|
||||
*.a
|
||||
*.elf
|
||||
|
||||
# скомпилированные бинарники в корне (make из README §4.3 кладёт рядом)
|
||||
/transmitter
|
||||
/receiver
|
||||
/ofdm_bench
|
||||
|
||||
# --- локальная копия кросс-заголовков для IntelliSense (см. .vscode/) ---
|
||||
.xarm/
|
||||
|
||||
# --- артефакты тестов и логи ---
|
||||
*.log
|
||||
link_test.bin
|
||||
out.bin
|
||||
test.bin
|
||||
|
||||
# --- редакторы/ОС ---
|
||||
.vscode/*
|
||||
!.vscode/c_cpp_properties.json
|
||||
!.vscode/settings.json
|
||||
.idea/
|
||||
*.swp
|
||||
*~
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
19
.vscode/c_cpp_properties.json
vendored
Normal file
19
.vscode/c_cpp_properties.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "pluto-armhf",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/src",
|
||||
"${workspaceFolder}/.xarm/include"
|
||||
],
|
||||
"defines": [],
|
||||
"cStandard": "gnu11",
|
||||
"intelliSenseMode": "linux-gcc-arm",
|
||||
"compilerArgs": [
|
||||
"-mfpu=neon",
|
||||
"-mfloat-abi=hard"
|
||||
]
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"files.eol": "\n",
|
||||
"files.encoding": "utf8",
|
||||
"C_Cpp.default.cStandard": "gnu11",
|
||||
"C_Cpp.default.intelliSenseMode": "linux-gcc-arm"
|
||||
}
|
||||
56
CLAUDE.md
Normal file
56
CLAUDE.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# CLAUDE.md — конституция проекта pluto-link
|
||||
|
||||
Прочитай этот файл первым. Полная документация — README.md, docs/.
|
||||
|
||||
## Контекст
|
||||
Радиолиния точка-точка на двух Pluto+ (клон ADALM-Pluto: Zynq-7020,
|
||||
2 × Cortex-A9 @ 667 МГц, NEON, 1 ГБ RAM, прошивка v0.38).
|
||||
Весь сигнальный тракт исполняется НА ARM ВНУТРИ Pluto (libiio, контекст
|
||||
`local:`). Хост (Raspberry Pi, aarch64) — только кросс-сборка и деплой.
|
||||
PHY: OFDM через liquid-dsp 1.8.0 + FFTW/NEON, FEC RS(255,223).
|
||||
|
||||
## Жёсткие правила
|
||||
1. Язык — C. НИКОГДА не предлагай Python, GNU Radio или обработку на хосте.
|
||||
2. libiio только `local:` в тракте. `ip:` — только для хост-утилит отладки.
|
||||
3. Статическая линковка, флаги обязательны:
|
||||
`-O3 -mcpu=cortex-a9 -mfpu=neon -mfloat-abi=hard -ffast-math -static`
|
||||
4. Никаких тяжёлых зависимостей (FFmpeg, cJSON, curl). JSON — руками.
|
||||
5. Бюджет CPU: на DSP доступно ОДНО ядро 667 МГц (второе — IIO/FEC/IO).
|
||||
Замерено: ofdmflexframesync тянет 2.34 Msps на кадрах, 5.32 Msps на шуме.
|
||||
Поэтому sample rate = 1 920 000. НИКОГДА не поднимай до 3 840 000 —
|
||||
система развалится под нагрузкой (0.61× от realtime).
|
||||
6. Изменил PHY-параметры (M/CP/модуляция/rate) → сначала прогони
|
||||
src/ofdm_bench.c на плате, обнови docs/BENCHMARK.md в том же коммите.
|
||||
7. Порядок отладки: кабель+аттенюатор 30–40 дБ (TX gain −40) → антенны
|
||||
на столе → дистанция. Не отлаживать новый код сразу в эфире.
|
||||
8. Изменения показывай в виде diff с объяснением каждой правки.
|
||||
|
||||
## Параметры тракта (текущие)
|
||||
- rate 1 920 000, bw 1 500 000, OFDM M=64 CP=16 taper=4, QPSK, CRC32
|
||||
- FEC RS(255,223), payload ≤ 1024 Б исходных (≤ 5 блоков, 1275 Б в эфире)
|
||||
- Симплекс-тест: обе платы 915 МГц. FDD (этап 2): A TX915/RX868, B зеркально
|
||||
- RX gain 50 дБ manual; TX gain: кабель −40, антенны от −20
|
||||
|
||||
## Формат кадра (заголовок 12 Б)
|
||||
0-1: 0xF0 0xAA | 2-5: seq u32 BE | 6-7: len u16 BE | 8-9: nblocks |
|
||||
10: last_block_bytes | 11: резерв
|
||||
КРИТИЧНО: liquid по умолчанию передаёт 8 Б заголовка. Обязательно
|
||||
`ofdmflexframegen_set_header_len(fg,12)` и
|
||||
`ofdmflexframesync_set_header_len(fs,12)` на обеих сторонах.
|
||||
|
||||
## IIO-имена (первые грабли)
|
||||
PHY: `ad9361-phy`; RX DMA: `cf-ad9361-lpc`; TX DMA: `cf-ad9361-dds-core-lpc`.
|
||||
TX LO: канал `altvoltage1` (output); RX LO: `altvoltage0` (output).
|
||||
Каналы данных: voltage0/voltage1 (I/Q, int16, диапазон ±2047 — 12 бит).
|
||||
|
||||
## Известный техдолг (README §9)
|
||||
#1 header 12Б (см. выше) · #2 лимит nblocks ≤ 18, не 100 (стек recovered[4096])
|
||||
· #3 fec_decode() liquid не сообщает об ошибках — rs_saved фиктивен
|
||||
· #4 внешний NCO в receiver вреден, убрать · #5 TX пушит лишние сэмплы
|
||||
· #6 нет детектора потерь по seq · #7 reciever.c → receiver.c
|
||||
|
||||
## Команды
|
||||
Сборка: `make` · Деплой: `make deploy` (scp -O! sftp на плате нет)
|
||||
Бенчмарк: `make bench && scripts/deploy.sh bench && ssh root@<pluto> '/tmp/ofdm_bench 1.92'`
|
||||
E2E-тест: `scripts/test_file_link.sh`
|
||||
Пароль root на Pluto: `analog`. rootfs в RAM — /tmp очищается при ребуте.
|
||||
246
README.md
Normal file
246
README.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# pluto-link — OFDM-радиолиния между двумя PlutoSDR (весь код на ARM внутри Pluto)
|
||||
|
||||
Цифровая радиолиния точка-точка на базе двух ADALM-Pluto+ (клон, Zynq-7020).
|
||||
PHY: OFDM (liquid-dsp) + RS(255,223) FEC. Весь сигнальный тракт исполняется
|
||||
на ARM Cortex-A9 **внутри** Pluto; хост-ПК нужен только для сборки, заливки
|
||||
бинарей и снятия логов. Прототип в рамках ОКР «R-Link» (двухранговая
|
||||
самоорганизующаяся сеть); данный репозиторий покрывает уровень PHY/линка.
|
||||
|
||||
---
|
||||
|
||||
## 1. Аппаратная платформа (фактическая, проверено)
|
||||
|
||||
| Параметр | Значение | Как проверено |
|
||||
|---|---|---|
|
||||
| Плата | Pluto+ (клон, Zynq-**7020**, AD9361) | `cat /proc/cpuinfo` — 2 ядра |
|
||||
| CPU | 2 × Cortex-A9 @ 667 МГц, NEON | `Features: ... neon vfpv3` |
|
||||
| RAM | 1 ГБ (доступно ~1000 МБ) | `free` |
|
||||
| Прошивка | v0.38-4-g95aad-dirty | баннер SSH |
|
||||
| Доступ | SSH root@192.168.2.1 (пароль `analog`), USB-Ethernet | — |
|
||||
|
||||
⚠️ Стоковый ADALM-Pluto (7010, 512 МБ, 1 ядро) — **другая плата**; все
|
||||
бюджеты CPU в этом документе рассчитаны для Pluto+. Не путать.
|
||||
|
||||
⚠️ В прошивке v0.38 нет sftp-server → заливать файлы только `scp -O`
|
||||
(legacy-протокол).
|
||||
|
||||
## 2. Бюджет CPU — главное ограничение проекта
|
||||
|
||||
Замерено офлайн-бенчмарком `src/ofdm_bench.c` прямо на ARM Pluto
|
||||
(одно ядро, static, `-O3 -mcpu=cortex-a9 -mfpu=neon -mfloat-abi=hard
|
||||
-ffast-math`, liquid-dsp 1.8.0 **с FFTW/NEON** — проверено по
|
||||
`HAVE_LIBFFTW3F` в config.log):
|
||||
|
||||
| Режим ofdmflexframesync | Пропускная способность | Вывод |
|
||||
|---|---|---|
|
||||
| Поиск преамбулы (шум) | **5.32 Msps** | дежурный режим, не лимитирует |
|
||||
| Приём кадров (duty ~57%) | **2.34 Msps** | узкое место |
|
||||
|
||||
Следствия (зафиксированы как проектные решения):
|
||||
|
||||
- **Рабочий sample rate = 1.92 MSPS.** Запас 1.22× на ядре синхронизатора;
|
||||
второе ядро — под IIO, RS-декод, ввод-вывод.
|
||||
- **3.84 MSPS запрещён**: система работает на редких пакетах и лавинообразно
|
||||
разваливается под нагрузкой (0.61× от realtime). Это худший вид отказа —
|
||||
«на демо работало».
|
||||
- Полезная скорость ≈ **1.3–1.5 Мбит/с** (~160–190 КБ/с). Файл 10 МБ ≈ 1 мин.
|
||||
- Параметры замера: M=64, CP=16, taper=4, QPSK, CRC32, payload 1275 Б.
|
||||
Полный протокол — `docs/BENCHMARK.md`. При любом изменении PHY-параметров
|
||||
бенчмарк перегоняется **до** изменения кода тракта.
|
||||
|
||||
## 3. Структура репозитория
|
||||
|
||||
```
|
||||
pluto-link/
|
||||
├── README.md # этот файл
|
||||
├── CLAUDE.md # краткие правила для Claude Code
|
||||
├── makefile # цели: all, tx, rx, bench, deploy, clean
|
||||
├── toolchain.env # CROSS=arm-linux-gnueabihf-, SYSROOT=$HOME/xarm
|
||||
├── src/
|
||||
│ ├── common.h # параметры OFDM/RS, дефолты, прототипы
|
||||
│ ├── common.c # pluto_init (local:), pluto_configure
|
||||
│ ├── transmitter.c # stdin → OFDM → AD9361 TX
|
||||
│ ├── receiver.c # AD9361 RX → OFDM sync → stdout
|
||||
│ └── ofdm_bench.c # бенчмарк CPU (не удалять!)
|
||||
├── scripts/
|
||||
│ ├── build_deps.sh # кросс-сборка FFTW + liquid-dsp → $HOME/xarm
|
||||
│ ├── deploy.sh # scp -O бинарей в /tmp обеих плат
|
||||
│ └── test_file_link.sh # e2e: файл → эфир → md5-сверка
|
||||
└── docs/
|
||||
├── BENCHMARK.md # методика и результаты замеров CPU
|
||||
└── NOTES.md # FDD, регуляторика, известные грабли
|
||||
```
|
||||
|
||||
## 4. Среда разработки и сборка
|
||||
|
||||
### 4.1 Хост
|
||||
|
||||
Любой Linux; проверено на Raspberry Pi OS 64-bit (aarch64). Кросс-тулчейн:
|
||||
|
||||
```bash
|
||||
sudo apt install -y build-essential autoconf automake libtool git wget \
|
||||
gcc-arm-linux-gnueabihf
|
||||
```
|
||||
|
||||
### 4.2 Зависимости (один раз, ~15 мин)
|
||||
|
||||
`scripts/build_deps.sh` собирает в `$HOME/xarm` статические armhf-библиотеки:
|
||||
|
||||
1. **FFTW 3.3.10**: `--enable-single --enable-neon --enable-static`
|
||||
2. **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`
|
||||
|
||||
После сборки liquid **обязательно** проверить, что FFTW подхвачен:
|
||||
`grep HAVE_LIBFFTW3F config.log` → должно быть `1`. Без FFTW liquid молча
|
||||
падает на встроенный FFT и цифры бюджета CPU невалидны.
|
||||
|
||||
### 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 -lliquid -lfftw3f -lm
|
||||
```
|
||||
|
||||
### 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 дБ | + аттенюатор 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) | **чинить до передачи файлов** |
|
||||
| 2 | `recovered[4096]` в receiver при лимите nblocks≤100 (22 КБ) → переполнение стека на битом заголовке. Лимит должен быть `4096/RS_DATA = 18` | чинить |
|
||||
| 3 | `fec_decode()` liquid не сообщает о неисправимых RS-блоках → счётчик `rs_saved` фиктивен. Достоверный критерий — только CRC/md5 поверх данных | знать; добавить CRC после декода |
|
||||
| 4 | Внешний NCO-CFO в receiver: `set_phase(0)` на границах буферов рвёт фазу; ofdmflexframesync и так компенсирует CFO сам | убрать или обосновать замером |
|
||||
| 5 | TX пушит весь буфер 16384 сэмпла при кадре ~10300 → ~35% эфира впустую + паузы `-p` | оптимизация: push ровно idx сэмплов |
|
||||
| 6 | Нет ARQ: потерянный кадр = молчаливая дыра в файле. seq в заголовке есть, но RX его игнорирует | минимум: детектор пропусков seq с логом |
|
||||
| 7 | Файл `reciever.c` → переименовать в `receiver.c` (make его ждёт) | тривиально |
|
||||
|
||||
## 10. Дорожная карта
|
||||
|
||||
1. ✅ Бенчмарк CPU на ARM Pluto → выбран rate 1.92 MSPS
|
||||
2. Перенос TX/RX на Pluto (`local:`), фиксы #1, #2, #7
|
||||
3. Файл через кабель + аттенюатор, md5 на 1 и 10 МБ
|
||||
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) |
|
||||
|
||||
---
|
||||
*Замеры и решения актуальны на 2026-07-10, прошивка v0.38, liquid-dsp 1.8.0.*
|
||||
84
docs/BENCHMARK.md
Normal file
84
docs/BENCHMARK.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# BENCHMARK.md — протокол замеров CPU (ofdmflexframesync на ARM Pluto)
|
||||
|
||||
Этот документ — основание для выбора sample rate. При любом изменении
|
||||
PHY-параметров (M, CP, модуляция, payload) бенчмарк перегоняется и таблица
|
||||
обновляется в том же коммите (правило №6 CLAUDE.md).
|
||||
|
||||
## Зачем
|
||||
|
||||
`ofdmflexframesync` (liquid-dsp) — самый дорогой блок тракта. Если его
|
||||
пропускная способность на одном ядре ниже sample rate, приёмник отстаёт от
|
||||
эфира: IIO-буферы какое-то время амортизируют, затем переполняются и кадры
|
||||
теряются лавинообразно. Опасность в том, что на редких пакетах система
|
||||
*выглядит* работающей — отказ проявляется только под нагрузкой.
|
||||
|
||||
## Методика
|
||||
|
||||
Инструмент: `src/ofdm_bench.c`. Генерирует эталонный буфер
|
||||
(8 кадров + гэпы, duty cycle кадров ~57%), делает int16-версии
|
||||
«сигнал+шум» (SNR ~28 дБ) и «чистый шум», прогоняет каждый через
|
||||
синхронизатор ≥5 с. Конвертация int16→float входит в замер (как в
|
||||
receiver.c). Процесс пинится на ядро 0 (`sched_setaffinity`).
|
||||
|
||||
Меряются два режима, лимитирует худший:
|
||||
- **шум** — стоимость дежурного поиска преамбулы (>90% времени в линии);
|
||||
- **сигнал+кадры** — полный цикл демодуляции под нагрузкой.
|
||||
|
||||
Валидация замера: `CRC ok` должен равняться `8 × число проходов`
|
||||
(все сгенерированные кадры пойманы и декодированы). Иначе сборка кривая
|
||||
и цифрам верить нельзя.
|
||||
|
||||
## Окружение эталонного замера
|
||||
|
||||
| Параметр | Значение |
|
||||
|---|---|
|
||||
| Дата | 2026-07-10 |
|
||||
| Плата | Pluto+ (Zynq-7020), 2 × Cortex-A9 @ 667 МГц, NEON, 1 ГБ |
|
||||
| Прошивка | v0.38-4-g95aad-dirty |
|
||||
| liquid-dsp | 1.8.0, **с FFTW 3.3.10 (NEON)** — `HAVE_LIBFFTW3F 1` в config.log |
|
||||
| Сборка | `-O3 -mcpu=cortex-a9 -mfpu=neon -mfloat-abi=hard -ffast-math -static` |
|
||||
| Кадр | M=64, CP=16, taper=4, QPSK, CRC32, FEC none, payload 1275 Б, header 8 Б |
|
||||
| Ядро | одно (affinity CPU0) |
|
||||
|
||||
## Результаты
|
||||
|
||||
```
|
||||
буфер: 143360 сэмплов, 8 кадров на проход
|
||||
|
||||
шум (поиск): 5.318–5.324 Msps (186 проходов; кадров 0 — норма)
|
||||
сигнал+кадры: 2.338–2.339 Msps (82 прохода; 656/656 CRC ok — валидно)
|
||||
```
|
||||
|
||||
Разброс между тремя запусками < 0.1% — замер чистый.
|
||||
|
||||
## Выводы (проектные решения)
|
||||
|
||||
| Sample rate | Запас (кадры) | Вердикт |
|
||||
|---|---|---|
|
||||
| 3.84 MSPS | 0.61× | **ЗАПРЕЩЁН**: не realtime, отказ под нагрузкой |
|
||||
| 1.92 MSPS | 1.22× | **рабочий**: второе ядро свободно под IIO/RS/IO |
|
||||
| 1.00 MSPS | 2.34× | резерв, если 1.92 окажется впритык в реальном тракте |
|
||||
|
||||
- Полезная скорость при 1.92 MSPS: ~1.3–1.5 Мбит/с (≈160–190 КБ/с)
|
||||
после RS(255,223), преамбул и служебки.
|
||||
- Поиск на шуме (5.32 Msps) не лимитирует ни один из режимов.
|
||||
- RS-декод в замер не входит: его стоимость мала относительно
|
||||
синхронизатора и планируется на второе ядро.
|
||||
|
||||
## Неисследованные резервы (если понадобится > 1.92 MSPS)
|
||||
|
||||
1. **M=32/CP=8** — дешевле FFT и эквалайзер; цена: выше доля пилотов,
|
||||
чувствительнее к CFO. Прогнать бенчмарк с этими параметрами.
|
||||
2. **Двухпоточный конвейер** — IIO-refill + int16→float на ядре 1,
|
||||
синхронизатор на ядре 0: ~+10–15% и убирает джиттер refill.
|
||||
3. **Сравнение NEON vs VFP** — пересборка без `-mfpu=neon`
|
||||
(с `-mfpu=vfpv3`) даёт цифру вклада векторизации для отчёта.
|
||||
|
||||
## Как воспроизвести
|
||||
|
||||
```bash
|
||||
make bench
|
||||
scripts/deploy.sh bench <ip_платы>
|
||||
ssh root@<ip_платы> '/tmp/ofdm_bench 1.92'
|
||||
# критерий приёмки: "сигнал+кадры" ≥ 1.2× цели на одном ядре
|
||||
```
|
||||
107
docs/NOTES.md
Normal file
107
docs/NOTES.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# NOTES.md — грабли, ограничения и почему так
|
||||
|
||||
Здесь собрано всё, что не является кодом, но экономит дни отладки.
|
||||
|
||||
## 1. Плата: Pluto+ ≠ ADALM-Pluto
|
||||
|
||||
Наши платы — клоны «Pluto+» (Zynq-7020). Отличия от стокового ADALM-Pluto
|
||||
(7010), которые влияют на проект:
|
||||
|
||||
| | Pluto+ (у нас) | ADALM-Pluto сток |
|
||||
|---|---|---|
|
||||
| SoC | Zynq-7020 | Zynq-7010 |
|
||||
| Ядра ARM | 2 активных | 1 (второе — через хак) |
|
||||
| RAM | 1 ГБ | 512 МБ |
|
||||
| Ethernet | есть гигабитный | нет (только USB) |
|
||||
|
||||
Следствия: бюджет «одно ядро на DSP, второе на IO» валиден только на наших
|
||||
платах; на стоковом Pluto весь тракт не поместится. Все замеры
|
||||
(docs/BENCHMARK.md) сделаны на Pluto+.
|
||||
|
||||
Особенности прошивки v0.38:
|
||||
- **нет sftp-server** → только `scp -O` (legacy protocol);
|
||||
- **rootfs в RAM** → всё в /tmp и /root пропадает при ребуте; бинарники
|
||||
заливаются заново (`make deploy`), «установить навсегда» = пересборка
|
||||
прошивки, нам не нужно;
|
||||
- BogoMIPS 333 в /proc/cpuinfo — артефакт таймера, реальная частота 667 МГц;
|
||||
- пароль root: `analog`.
|
||||
|
||||
## 2. FDD и самоглушение (главный риск этапа «в воздух»)
|
||||
|
||||
План FDD: A TX 915 / RX 868; B TX 868 / RX 915 (разнос 47 МГц).
|
||||
Проблема: **дуплексеров нет**. Собственный передатчик платы в паре
|
||||
сантиметров от собственного приёмника:
|
||||
- широкополосный шум TX попадает в полосу RX даже при разносе 47 МГц;
|
||||
- сильный сигнал TX блокирует (десенсибилизирует) входной каскад RX.
|
||||
|
||||
Характерный симптом: симплекс и loopback работают идеально, а в дуплексе
|
||||
RX платы «глохнет», как только её TX активен. Это не баг кода.
|
||||
|
||||
Меры (по нарастанию):
|
||||
1. максимальный `tx_attenuation`, какой позволяет линия;
|
||||
2. физический разнос TX/RX антенн + кросс-поляризация;
|
||||
3. SAW-фильтр 868 МГц на RX-вход (копеечный, SRD-диапазон);
|
||||
4. если ничего не помогает — честный TDD на одной частоте.
|
||||
|
||||
Поэтому порядок работ: **сначала симплекс** (обе платы 915 МГц, полудуплекс
|
||||
по очереди), FDD — отдельным этапом с замерами деградации RX при активном TX.
|
||||
|
||||
## 3. Регуляторика (868/915 МГц в регионе ETSI)
|
||||
|
||||
- **868 МГц** — SRD-диапазон: ограничения на полосу канала (сотни кГц),
|
||||
ЭИИМ и duty cycle. Наш сигнал 1.5 МГц в них не вписывается.
|
||||
- **915 МГц** — в Европе это uplink GSM-900, не ISM (ISM 902–928 — регион США).
|
||||
|
||||
Практический вывод: разработка и демо — **кабель с аттенюатором** или
|
||||
антенны на столе с минимальной мощностью (TX gain от −40) и без внешних
|
||||
усилителей. Это ограничение прототипа фиксируется в отчёте проекта;
|
||||
у целевой системы по ТЗ предполагается лицензированный канал.
|
||||
|
||||
## 4. RF-безопасность
|
||||
|
||||
- Кабельный тест **только** через аттенюатор 30–40 дБ + `-g -40`.
|
||||
Прямое соединение TX→RX выжигает входной каскад AD9361.
|
||||
- Диапазон DAC/ADC — 12 бит (±2047 в int16). Клиппинг при амплитуде
|
||||
TX > ~0.5 виден как рост EVM на RX.
|
||||
|
||||
## 5. Особенности liquid-dsp, найденные в бою
|
||||
|
||||
- **Заголовок пользователя по умолчанию 8 байт.** Наш формат — 12 байт.
|
||||
Без `ofdmflexframegen_set_header_len(fg,12)` /
|
||||
`ofdmflexframesync_set_header_len(fs,12)` байты 8–11 (nblocks,
|
||||
last_block_bytes) в эфир не уходят, RX читает мусор за границей буфера.
|
||||
- **`fec_decode()` не сообщает об ошибках**: RS-декодер liquid всегда
|
||||
«успешен», неисправимый блок просто отдаёт мусор. Единственный надёжный
|
||||
критерий целостности — CRC/md5 поверх декодированных данных.
|
||||
- **Без FFTW liquid молча деградирует** на встроенный FFT. Проверка после
|
||||
сборки обязательна: `grep HAVE_LIBFFTW3F config.log`.
|
||||
- `ofdmflexframesync` сам оценивает и компенсирует CFO по преамбуле;
|
||||
внешний NCO-контур поверх него (как в раннем receiver.c) не нужен и
|
||||
с `set_phase(0)` на границах буферов — вреден.
|
||||
|
||||
## 6. Почему архитектурные решения именно такие
|
||||
|
||||
- **Статическая линковка**: независимость от libc прошивки, деплой одним
|
||||
файлом, нет возни с sysroot ADI. Цена ~2 МБ — ничто при 1 ГБ RAM.
|
||||
- **rate 1.92, а не 3.84 MSPS**: замер (BENCHMARK.md) — 2.34 Msps потолок
|
||||
синхронизатора на кадрах. 3.84 даёт отказ «работает на демо, падает под
|
||||
нагрузкой» — худший для радиолинии вид отказа.
|
||||
- **OFDM оставлен, несмотря на цену CPU**: код написан и проверен;
|
||||
single-carrier QPSK дал бы ту же полезную скорость дешевле, но потребовал
|
||||
бы написать и отладить синхронизацию заново. Решение можно пересмотреть,
|
||||
если понадобится rate > 1.92 MSPS (см. резервы в BENCHMARK.md).
|
||||
- **Симплекс раньше FDD**: изолирует проблемы кода от проблем RF (п. 2).
|
||||
|
||||
## 7. Быстрые команды-шпаргалки
|
||||
|
||||
```bash
|
||||
# что видит плата
|
||||
iio_info -u ip:192.168.2.1 | less
|
||||
iio_attr -u ip:192.168.2.1 -c ad9361-phy altvoltage1 frequency # TX LO
|
||||
|
||||
# загрузка CPU на плате во время линии (busybox top)
|
||||
ssh root@192.168.2.1 top
|
||||
|
||||
# логи TX/RX после test_file_link.sh
|
||||
ssh root@<ip> 'cat /tmp/tx.log /tmp/rx.log'
|
||||
```
|
||||
66
makefile
Normal file
66
makefile
Normal file
@@ -0,0 +1,66 @@
|
||||
# pluto-link — кросс-сборка под ARM Pluto (armhf, static)
|
||||
# Цели: all | tx | rx | bench | deploy | test | clean | help
|
||||
# Переменные можно переопределить: make PLUTO_A=ip PLUTO_B=ip
|
||||
|
||||
# --- тулчейн (согласовано с toolchain.env) ---
|
||||
CROSS ?= arm-linux-gnueabihf-
|
||||
CC := $(CROSS)gcc
|
||||
XARM ?= $(HOME)/xarm
|
||||
|
||||
CFLAGS := -O3 -mcpu=cortex-a9 -mfpu=neon -mfloat-abi=hard -ffast-math \
|
||||
-Wall -Wextra -static -I$(XARM)/include -Isrc
|
||||
LDFLAGS := -L$(XARM)/lib
|
||||
LDLIBS := -lliquid -lfftw3f -lm -lpthread
|
||||
|
||||
# --- платы ---
|
||||
PLUTO_A ?= 192.168.2.1
|
||||
PLUTO_B ?= 192.168.3.1
|
||||
|
||||
BIN := build
|
||||
SRC := src
|
||||
|
||||
all: $(BIN)/transmitter $(BIN)/receiver $(BIN)/ofdm_bench
|
||||
|
||||
$(BIN):
|
||||
mkdir -p $(BIN)
|
||||
|
||||
$(BIN)/transmitter: $(SRC)/transmitter.c $(SRC)/common.c $(SRC)/common.h | $(BIN)
|
||||
$(CC) $(CFLAGS) $(SRC)/transmitter.c $(SRC)/common.c -o $@ $(LDFLAGS) -liio $(LDLIBS)
|
||||
|
||||
$(BIN)/receiver: $(SRC)/receiver.c $(SRC)/common.c $(SRC)/common.h | $(BIN)
|
||||
$(CC) $(CFLAGS) $(SRC)/receiver.c $(SRC)/common.c -o $@ $(LDFLAGS) -liio $(LDLIBS)
|
||||
|
||||
$(BIN)/ofdm_bench: $(SRC)/ofdm_bench.c | $(BIN)
|
||||
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) $(LDLIBS)
|
||||
|
||||
tx: $(BIN)/transmitter
|
||||
rx: $(BIN)/receiver
|
||||
bench: $(BIN)/ofdm_bench
|
||||
|
||||
# --- деплой (scp -O: на прошивке v0.38 нет sftp-server) ---
|
||||
deploy: all
|
||||
scripts/deploy.sh $(PLUTO_A) $(PLUTO_B)
|
||||
|
||||
# --- e2e: файл через радио + md5 ---
|
||||
test: deploy
|
||||
scripts/test_file_link.sh $(PLUTO_A) $(PLUTO_B)
|
||||
|
||||
check-env:
|
||||
@which $(CC) >/dev/null || { echo "нет $(CC): sudo apt install gcc-arm-linux-gnueabihf"; exit 1; }
|
||||
@test -f $(XARM)/lib/libliquid.a || { echo "нет libliquid.a: запусти scripts/build_deps.sh"; exit 1; }
|
||||
@test -f $(XARM)/lib/libfftw3f.a || { echo "нет libfftw3f.a: запусти scripts/build_deps.sh"; exit 1; }
|
||||
@echo "OK: тулчейн и зависимости на месте"
|
||||
|
||||
clean:
|
||||
rm -rf $(BIN)
|
||||
|
||||
help:
|
||||
@echo "make [all] — собрать transmitter, receiver, ofdm_bench (static armhf)"
|
||||
@echo "make tx|rx|bench — собрать отдельную цель"
|
||||
@echo "make deploy — залить бинарники на обе платы (scp -O в /tmp)"
|
||||
@echo "make test — e2e-тест передачи файла с md5-сверкой"
|
||||
@echo "make check-env — проверить тулчейн и $(XARM)"
|
||||
@echo "make clean — удалить build/"
|
||||
@echo "переменные: PLUTO_A=$(PLUTO_A) PLUTO_B=$(PLUTO_B) XARM=$(XARM)"
|
||||
|
||||
.PHONY: all tx rx bench deploy test check-env clean help
|
||||
85
scripts/build_deps.sh
Normal file
85
scripts/build_deps.sh
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
# build_deps.sh — кросс-сборка статических armhf-зависимостей в $XARM
|
||||
# Собирает: FFTW 3.3.10 (single+NEON), liquid-dsp 1.8.0, libiio (local backend)
|
||||
# Запуск: scripts/build_deps.sh (один раз, ~15-20 мин на RPi4)
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/../toolchain.env"
|
||||
|
||||
WORK="$HOME/src"
|
||||
mkdir -p "$WORK" "$XARM"
|
||||
cd "$WORK"
|
||||
|
||||
echo "=== [0/4] Проверка тулчейна ==="
|
||||
which "${CC}" >/dev/null || {
|
||||
echo "Нет ${CC}. Установи: sudo apt install gcc-arm-linux-gnueabihf"; exit 1; }
|
||||
sudo apt-get install -y build-essential autoconf automake libtool git wget cmake >/dev/null
|
||||
|
||||
echo "=== [1/4] FFTW 3.3.10 (float + NEON, static) ==="
|
||||
if [ ! -f "$XARM/lib/libfftw3f.a" ]; then
|
||||
[ -d fftw-3.3.10 ] || {
|
||||
wget -nc http://fftw.org/fftw-3.3.10.tar.gz
|
||||
tar xf fftw-3.3.10.tar.gz
|
||||
}
|
||||
cd fftw-3.3.10
|
||||
./configure --host=arm-linux-gnueabihf --prefix="$XARM" \
|
||||
--enable-single --enable-neon --enable-static --disable-shared \
|
||||
CC="$CC" CFLAGS="$XCFLAGS"
|
||||
make -j"$(nproc)" && make install
|
||||
cd "$WORK"
|
||||
else
|
||||
echo "уже собран, пропускаю"
|
||||
fi
|
||||
|
||||
echo "=== [2/4] liquid-dsp 1.8.0 (static, с FFTW) ==="
|
||||
if [ ! -f "$XARM/lib/libliquid.a" ]; then
|
||||
[ -d liquid-dsp ] || git clone --depth 1 --branch v1.8.0 \
|
||||
https://github.com/jgaeddert/liquid-dsp.git
|
||||
cd liquid-dsp
|
||||
./bootstrap.sh
|
||||
./configure --host=arm-linux-gnueabihf --prefix="$XARM" \
|
||||
CC="$CC" CFLAGS="$XCFLAGS -ffast-math" \
|
||||
CPPFLAGS="-I$XARM/include" LDFLAGS="-L$XARM/lib" \
|
||||
ac_cv_func_malloc_0_nonnull=yes ac_cv_func_realloc_0_nonnull=yes
|
||||
# КРИТИЧНО: без FFTW liquid молча падает на медленный встроенный FFT,
|
||||
# и бюджет CPU из docs/BENCHMARK.md становится невалидным.
|
||||
grep -q "HAVE_LIBFFTW3F 1" config.log || {
|
||||
echo "ОШИБКА: liquid не нашёл кросс-FFTW — проверь \$XARM"; exit 1; }
|
||||
make -j"$(nproc)" && make install
|
||||
cd "$WORK"
|
||||
else
|
||||
echo "уже собран, пропускаю"
|
||||
fi
|
||||
|
||||
echo "=== [3/4] libiio v0.25 (static, только local backend) ==="
|
||||
if [ ! -f "$XARM/lib/libiio.a" ]; then
|
||||
[ -d libiio ] || git clone --depth 1 --branch v0.25 \
|
||||
https://github.com/analogdevicesinc/libiio.git
|
||||
cd libiio
|
||||
cat > toolchain-armhf.cmake <<EOF
|
||||
set(CMAKE_SYSTEM_NAME Linux)
|
||||
set(CMAKE_SYSTEM_PROCESSOR arm)
|
||||
set(CMAKE_C_COMPILER ${CROSS}gcc)
|
||||
set(CMAKE_C_FLAGS_INIT "$XCFLAGS")
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
EOF
|
||||
cmake -B build-armhf -DCMAKE_TOOLCHAIN_FILE=toolchain-armhf.cmake \
|
||||
-DCMAKE_INSTALL_PREFIX="$XARM" -DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SHARED_LIBS=OFF \
|
||||
-DWITH_LOCAL_BACKEND=ON -DWITH_NETWORK_BACKEND=OFF \
|
||||
-DWITH_USB_BACKEND=OFF -DWITH_SERIAL_BACKEND=OFF \
|
||||
-DWITH_XML_BACKEND=OFF -DHAVE_DNS_SD=OFF \
|
||||
-DWITH_TESTS=OFF -DWITH_EXAMPLES=OFF -DWITH_DOC=OFF \
|
||||
-DCSHARP_BINDINGS=OFF -DPYTHON_BINDINGS=OFF
|
||||
cmake --build build-armhf -j"$(nproc)"
|
||||
cmake --install build-armhf
|
||||
cd "$WORK"
|
||||
# Если cmake ругается на libxml2: XML backend в этой версии отключается
|
||||
# флагом выше; если версия libiio его требует — собери libxml2 static
|
||||
# (--without-python --without-zlib --without-lzma) в $XARM и повтори.
|
||||
else
|
||||
echo "уже собран, пропускаю"
|
||||
fi
|
||||
|
||||
echo "=== [4/4] Итог ==="
|
||||
ls -la "$XARM/lib/" | grep -E "libfftw3f|libliquid|libiio" || true
|
||||
echo "Готово. Дальше: make check-env && make"
|
||||
42
scripts/deploy.sh
Normal file
42
scripts/deploy.sh
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy.sh — заливка бинарей на Pluto (обе или одна плата)
|
||||
# Использование:
|
||||
# scripts/deploy.sh # обе платы из toolchain.env
|
||||
# scripts/deploy.sh 192.168.2.1 # только одна плата
|
||||
# scripts/deploy.sh <ipA> <ipB> # явные адреса
|
||||
# scripts/deploy.sh bench <ip> # только бенчмарк
|
||||
# Прошивка v0.38: sftp-server отсутствует → обязателен scp -O (legacy).
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/../toolchain.env"
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# sshpass, если установлен, избавляет от ввода пароля (пароль: analog)
|
||||
if command -v sshpass >/dev/null; then
|
||||
SCP=(sshpass -p "$PLUTO_PASS" scp -O -o StrictHostKeyChecking=no)
|
||||
else
|
||||
SCP=(scp -O)
|
||||
fi
|
||||
|
||||
FILES=(build/transmitter build/receiver build/ofdm_bench)
|
||||
TARGETS=()
|
||||
|
||||
case "${1:-}" in
|
||||
bench) FILES=(build/ofdm_bench); TARGETS=("${2:-$PLUTO_A}") ;;
|
||||
"") TARGETS=("$PLUTO_A" "$PLUTO_B") ;;
|
||||
*) TARGETS=("$@") ;;
|
||||
esac
|
||||
|
||||
for f in "${FILES[@]}"; do
|
||||
[ -f "$f" ] || { echo "нет $f — сначала make"; exit 1; }
|
||||
done
|
||||
|
||||
for ip in "${TARGETS[@]}"; do
|
||||
echo "=== деплой на root@$ip:/tmp/ ==="
|
||||
if ! ping -c1 -W2 "$ip" >/dev/null 2>&1; then
|
||||
echo " ! $ip не пингуется, пропускаю"; continue
|
||||
fi
|
||||
"${SCP[@]}" "${FILES[@]}" "root@$ip:/tmp/"
|
||||
echo " ok: ${FILES[*]}"
|
||||
done
|
||||
|
||||
echo "Напоминание: rootfs платы в RAM — после перезагрузки залить заново."
|
||||
68
scripts/test_file_link.sh
Normal file
68
scripts/test_file_link.sh
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# test_file_link.sh — e2e-тест: файл → Pluto A → эфир → Pluto B → md5
|
||||
# Использование: scripts/test_file_link.sh [ipA] [ipB] [размер_КБ]
|
||||
# По умолчанию: адреса из toolchain.env, файл 1024 КБ, симплекс 915 МГц.
|
||||
#
|
||||
# ВНИМАНИЕ: сначала кабель + аттенюатор 30–40 дБ (TX gain −40)!
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/../toolchain.env"
|
||||
|
||||
A="${1:-$PLUTO_A}" # передатчик
|
||||
B="${2:-$PLUTO_B}" # приёмник
|
||||
SIZE_KB="${3:-1024}"
|
||||
|
||||
FREQ=915000000
|
||||
RATE=1920000 # НЕ поднимать: бюджет CPU, docs/BENCHMARK.md
|
||||
BW=1500000
|
||||
TXGAIN=-40 # для кабеля; антенны: −20…0
|
||||
TIMEOUT=$(( SIZE_KB / 100 + 30 )) # ~160 КБ/с + запас
|
||||
|
||||
if command -v sshpass >/dev/null; then
|
||||
SSH=(sshpass -p "$PLUTO_PASS" ssh -o StrictHostKeyChecking=no)
|
||||
SCP=(sshpass -p "$PLUTO_PASS" scp -O -o StrictHostKeyChecking=no)
|
||||
else
|
||||
SSH=(ssh); SCP=(scp -O)
|
||||
echo "подсказка: apt install sshpass — и пароль вводить не придётся"
|
||||
fi
|
||||
|
||||
echo "=== [1/5] Тестовый файл ${SIZE_KB} КБ ==="
|
||||
dd if=/dev/urandom of=/tmp/link_test.bin bs=1024 count="$SIZE_KB" status=none
|
||||
MD5_SRC=$(md5sum /tmp/link_test.bin | cut -d' ' -f1)
|
||||
echo "md5: $MD5_SRC"
|
||||
|
||||
echo "=== [2/5] Заливка файла на Pluto A ($A) ==="
|
||||
"${SCP[@]}" /tmp/link_test.bin "root@$A:/tmp/"
|
||||
|
||||
echo "=== [3/5] Старт приёмника на Pluto B ($B), таймаут ${TIMEOUT}с ==="
|
||||
"${SSH[@]}" "root@$B" \
|
||||
"rm -f /tmp/out.bin; timeout $TIMEOUT /tmp/receiver -f $FREQ -r $RATE \
|
||||
-b $BW -c rs8 -u local: > /tmp/out.bin 2>/tmp/rx.log" &
|
||||
RX_PID=$!
|
||||
sleep 3 # дать RX прогреться до старта TX
|
||||
|
||||
echo "=== [4/5] Передача с Pluto A ==="
|
||||
"${SSH[@]}" "root@$A" \
|
||||
"cat /tmp/link_test.bin | /tmp/transmitter -f $FREQ -r $RATE -b $BW \
|
||||
-g $TXGAIN -p 0 -c rs8 -u local: 2>/tmp/tx.log"
|
||||
sleep 3 # дохлебать хвост эфира
|
||||
"${SSH[@]}" "root@$B" "killall receiver 2>/dev/null || true"
|
||||
wait "$RX_PID" 2>/dev/null || true
|
||||
|
||||
echo "=== [5/5] Сверка ==="
|
||||
MD5_DST=$("${SSH[@]}" "root@$B" "md5sum /tmp/out.bin 2>/dev/null | cut -d' ' -f1" || echo "нет_файла")
|
||||
SIZE_DST=$("${SSH[@]}" "root@$B" "wc -c < /tmp/out.bin 2>/dev/null" || echo 0)
|
||||
|
||||
echo "источник: $MD5_SRC ($((SIZE_KB*1024)) байт)"
|
||||
echo "приёмник: $MD5_DST ($SIZE_DST байт)"
|
||||
|
||||
if [ "$MD5_SRC" = "$MD5_DST" ]; then
|
||||
echo "РЕЗУЛЬТАТ: OK — файл передан без искажений"
|
||||
else
|
||||
echo "РЕЗУЛЬТАТ: FAIL"
|
||||
echo "Диагностика (README §11):"
|
||||
echo " - размер меньше исходного → потерянные кадры (нет ARQ, техдолг #6):"
|
||||
echo " смотри лог RX и разрывы seq"
|
||||
echo " - размер равен, md5 нет → техдолг #1 (header_len=12?) или #3"
|
||||
echo " - размер 0 → RX не поймал ничего: частота/усиление/кабель"
|
||||
"${SSH[@]}" "root@$B" "tail -5 /tmp/rx.log" || true
|
||||
fi
|
||||
106
src/common.c
Normal file
106
src/common.c
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* common.c — Реализация общих функций
|
||||
*/
|
||||
|
||||
#include "common.h"
|
||||
|
||||
volatile sig_atomic_t stop = 0;
|
||||
|
||||
static void sigint_handler(int s) {
|
||||
(void)s;
|
||||
stop = 1;
|
||||
}
|
||||
|
||||
void setup_signal_handlers(void) {
|
||||
signal(SIGINT, sigint_handler);
|
||||
signal(SIGTERM, sigint_handler);
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
}
|
||||
|
||||
void print_usage(const char *prog_name, const char *mode) {
|
||||
fprintf(stderr, "═══════════════════════════════════════════════════\n");
|
||||
fprintf(stderr, "OFDM %s — PlutoSDR Radio Link\n", mode);
|
||||
fprintf(stderr, "═══════════════════════════════════════════════════\n");
|
||||
fprintf(stderr, "Использование: %s [ОПЦИИ]\n\n", prog_name);
|
||||
fprintf(stderr, "ОБЩИЕ ОПЦИИ:\n");
|
||||
fprintf(stderr, " -f ЧАСТОТА Частота несущей в Гц (по умолчанию: %lld)\n", DEFAULT_FREQ);
|
||||
fprintf(stderr, " -r СКОРОСТЬ Частота дискретизации (по умолчанию: %lld)\n", DEFAULT_RATE);
|
||||
fprintf(stderr, " -b ПОЛОСА Полоса пропускания (по умолчанию: %lld)\n", DEFAULT_BW);
|
||||
fprintf(stderr, " -u URI URI PlutoSDR\n");
|
||||
fprintf(stderr, " -c РЕЖИМ FEC: rs8 или none (по умолчанию: rs8)\n");
|
||||
fprintf(stderr, " -h Показать эту справку\n");
|
||||
}
|
||||
|
||||
void print_config(const char *mode, long long freq, long long rate,
|
||||
long long bw, int fec_on) {
|
||||
fprintf(stderr, "═══════════════════════════════════════════════════\n");
|
||||
fprintf(stderr, "КОНФИГУРАЦИЯ %s\n", mode);
|
||||
fprintf(stderr, "Частота: %.1f МГц\n", freq / 1e6);
|
||||
fprintf(stderr, "Дискрет.: %.2f МГц\n", rate / 1e6);
|
||||
fprintf(stderr, "Полоса: %.2f МГц\n", bw / 1e6);
|
||||
fprintf(stderr, "OFDM: M=%d, CP=%d, Taper=%d\n", OFDM_M, OFDM_CP, OFDM_TAPER);
|
||||
fprintf(stderr, "FEC: %s\n", fec_on ? "RS(255,223)" : "ОТКЛЮЧЕН");
|
||||
}
|
||||
|
||||
int pluto_init(const char *uri, struct iio_context **ctx,
|
||||
struct iio_device **phy, struct iio_device **dev, int is_tx) {
|
||||
*ctx = iio_create_context_from_uri(uri);
|
||||
if (!*ctx) {
|
||||
LOG_ERROR("Не удалось подключиться к PlutoSDR по адресу %s", uri);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*phy = iio_context_find_device(*ctx, "ad9361-phy");
|
||||
*dev = iio_context_find_device(*ctx,
|
||||
is_tx ? "cf-ad9361-dds-core-lpc" : "cf-ad9361-lpc");
|
||||
|
||||
if (!*phy || !*dev) {
|
||||
LOG_ERROR("Радиоустройства не найдены в контексте");
|
||||
iio_context_destroy(*ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pluto_configure(struct iio_device *phy, long long freq, long long rate,
|
||||
long long bw, double gain, int is_tx) {
|
||||
if (is_tx) {
|
||||
// Настройка передатчика
|
||||
iio_channel_attr_write_longlong(
|
||||
iio_device_find_channel(phy, "altvoltage1", true),
|
||||
"frequency", freq);
|
||||
iio_channel_attr_write_longlong(
|
||||
iio_device_find_channel(phy, "voltage0", true),
|
||||
"sampling_frequency", rate);
|
||||
iio_channel_attr_write_longlong(
|
||||
iio_device_find_channel(phy, "voltage0", true),
|
||||
"rf_bandwidth", bw);
|
||||
iio_channel_attr_write(
|
||||
iio_device_find_channel(phy, "voltage0", true),
|
||||
"rf_port_select", "A");
|
||||
iio_channel_attr_write_double(
|
||||
iio_device_find_channel(phy, "voltage0", true),
|
||||
"hardwaregain", gain);
|
||||
} else {
|
||||
// Настройка приёмника
|
||||
iio_channel_attr_write_longlong(
|
||||
iio_device_find_channel(phy, "altvoltage0", true),
|
||||
"frequency", freq);
|
||||
iio_channel_attr_write_longlong(
|
||||
iio_device_find_channel(phy, "voltage0", false),
|
||||
"sampling_frequency", rate);
|
||||
iio_channel_attr_write_longlong(
|
||||
iio_device_find_channel(phy, "voltage0", false),
|
||||
"rf_bandwidth", bw);
|
||||
iio_channel_attr_write(
|
||||
iio_device_find_channel(phy, "voltage0", false),
|
||||
"gain_control_mode", "manual");
|
||||
iio_channel_attr_write_double(
|
||||
iio_device_find_channel(phy, "voltage0", false),
|
||||
"hardwaregain", gain);
|
||||
iio_channel_attr_write(
|
||||
iio_device_find_channel(phy, "voltage0", false),
|
||||
"rf_port_select", "A_BALANCED");
|
||||
}
|
||||
}
|
||||
61
src/common.h
Normal file
61
src/common.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* common.h — Общие определения и конфигурация для приёмника и передатчика
|
||||
* OFDM + RS(255,223) FEC + PlutoSDR
|
||||
*/
|
||||
|
||||
#ifndef COMMON_H
|
||||
#define COMMON_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <signal.h>
|
||||
#include <getopt.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <iio.h>
|
||||
#include <liquid/liquid.h>
|
||||
|
||||
// Параметры OFDM-модуляции
|
||||
#define OFDM_M 64
|
||||
#define OFDM_CP 16
|
||||
#define OFDM_TAPER 4
|
||||
|
||||
// Параметры Рида-Соломона
|
||||
#define RS_DATA 223
|
||||
#define RS_ENC 255
|
||||
|
||||
// Параметры радиотракта по умолчанию
|
||||
#define DEFAULT_FREQ 1265000000LL
|
||||
#define DEFAULT_RATE 3840000LL
|
||||
#define DEFAULT_BW 3000000LL
|
||||
|
||||
// Адреса PlutoSDR
|
||||
#define TX_URI_DEFAULT "ip:192.168.3.1" // адрес Pluto передатчика
|
||||
#define RX_URI_DEFAULT "ip:192.168.2.1" // адрес Pluto приемника
|
||||
|
||||
// Формат заголовка кадра
|
||||
#define HDR_SIGNATURE_0 0xF0
|
||||
#define HDR_SIGNATURE_1 0xAA
|
||||
#define HDR_SIZE 12
|
||||
|
||||
// Макросы логирования
|
||||
#define LOG_ERROR(fmt, ...) fprintf(stderr, "[ОШИБКА] " fmt "\n", ##__VA_ARGS__)
|
||||
#define LOG_INFO(fmt, ...) fprintf(stderr, "[INFO] " fmt "\n", ##__VA_ARGS__)
|
||||
|
||||
// Глобальный флаг завершения
|
||||
extern volatile sig_atomic_t stop;
|
||||
|
||||
// Прототипы общих функций
|
||||
void setup_signal_handlers(void);
|
||||
void print_usage(const char *prog_name, const char *mode);
|
||||
void print_config(const char *mode, long long freq, long long rate,
|
||||
long long bw, int fec_on);
|
||||
int pluto_init(const char *uri, struct iio_context **ctx,
|
||||
struct iio_device **phy, struct iio_device **dev, int is_tx);
|
||||
void pluto_configure(struct iio_device *phy, long long freq, long long rate,
|
||||
long long bw, double gain, int is_tx);
|
||||
|
||||
#endif
|
||||
168
src/ofdm_bench.c
Normal file
168
src/ofdm_bench.c
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* ofdm_bench.c — офлайн-бенчмарк ofdmflexframesync на ARM (Pluto, Cortex-A9)
|
||||
*
|
||||
* Повторяет тракт receiver.c: int16 IQ -> float -> ofdmflexframesync,
|
||||
* параметры кадра как в transmitter.c (M=64, CP=16, taper=4, QPSK, CRC32).
|
||||
* Меряет два режима: чистый шум (поиск преамбулы) и сигнал с кадрами.
|
||||
*
|
||||
* Сборка (на хосте, кросс):
|
||||
* arm-linux-gnueabihf-gcc -O3 -mcpu=cortex-a9 -mfpu=neon -mfloat-abi=hard \
|
||||
* -ffast-math -static ofdm_bench.c -o ofdm_bench \
|
||||
* -I$HOME/xarm/include -L$HOME/xarm/lib -lliquid -lfftw3f -lm
|
||||
*
|
||||
* Запуск на Pluto:
|
||||
* /tmp/ofdm_bench [целевой_rate_Msps] # по умолчанию 3.84
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <time.h>
|
||||
#include <sched.h>
|
||||
#include <stdint.h>
|
||||
#include <complex.h>
|
||||
#include <liquid/liquid.h>
|
||||
|
||||
#define OFDM_M 64 /* как в common.h */
|
||||
#define OFDM_CP 16
|
||||
#define OFDM_TAPER 4
|
||||
#define PAYLOAD_LEN 1275 /* 5 блоков RS(255,223): как при n=1024 в transmitter.c */
|
||||
#define N_FRAMES 8 /* кадров в эталонном буфере */
|
||||
#define GAP_SAMPLES 7680 /* пауза 2000 мкс @ 3.84 MSPS, как -p 2000 */
|
||||
#define AMP 0.30f
|
||||
#define NOISE_STD 0.012f /* ~28 дБ SNR, "хороший" эфир */
|
||||
#define BENCH_SEC 5.0
|
||||
|
||||
static unsigned det = 0, ok = 0;
|
||||
|
||||
static int cb(unsigned char *h, int hv, unsigned char *p, unsigned int pl,
|
||||
int pv, framesyncstats_s s, void *u)
|
||||
{
|
||||
(void)h; (void)p; (void)pl; (void)s; (void)u;
|
||||
det++;
|
||||
if (hv && pv) ok++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static double tnow(void)
|
||||
{
|
||||
struct timespec t;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t);
|
||||
return t.tv_sec + 1e-9 * t.tv_nsec;
|
||||
}
|
||||
|
||||
static double bench(ofdmflexframesync fs, const int16_t *iq, unsigned n,
|
||||
float complex *work, const char *name, double target_msps)
|
||||
{
|
||||
det = ok = 0;
|
||||
ofdmflexframesync_reset(fs);
|
||||
|
||||
unsigned long long total = 0;
|
||||
unsigned passes = 0;
|
||||
double t0 = tnow(), t1;
|
||||
|
||||
do {
|
||||
/* конвертация int16 -> float входит в замер, как в receiver.c */
|
||||
for (unsigned i = 0; i < n; i++)
|
||||
work[i] = (iq[2*i] / 2048.0f) + (iq[2*i+1] / 2048.0f) * _Complex_I;
|
||||
ofdmflexframesync_execute(fs, work, n);
|
||||
total += n;
|
||||
passes++;
|
||||
t1 = tnow();
|
||||
} while (t1 - t0 < BENCH_SEC);
|
||||
|
||||
double msps = (double)total / (t1 - t0) / 1e6;
|
||||
printf("%-16s %8.3f Msps x%.2f от цели кадров: %u найдено / %u CRC ok (%u проходов)\n",
|
||||
name, msps, msps / target_msps, det, ok, passes);
|
||||
return msps;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
double target = (argc > 1) ? atof(argv[1]) : 3.84;
|
||||
|
||||
/* пиним на ядро 0 — меряем стоимость на ОДНОМ ядре */
|
||||
cpu_set_t cs;
|
||||
CPU_ZERO(&cs);
|
||||
CPU_SET(0, &cs);
|
||||
sched_setaffinity(0, sizeof(cs), &cs);
|
||||
|
||||
srand(1);
|
||||
|
||||
/* --- генератор кадров: параметры как в transmitter.c --- */
|
||||
ofdmflexframegenprops_s p;
|
||||
ofdmflexframegenprops_init_default(&p);
|
||||
p.check = LIQUID_CRC_32;
|
||||
p.fec0 = LIQUID_FEC_NONE;
|
||||
p.fec1 = LIQUID_FEC_NONE;
|
||||
p.mod_scheme = LIQUID_MODEM_QPSK;
|
||||
ofdmflexframegen fg =
|
||||
ofdmflexframegen_create(OFDM_M, OFDM_CP, OFDM_TAPER, NULL, &p);
|
||||
|
||||
printf("liquid-dsp %s | M=%d CP=%d taper=%d | QPSK, CRC32, FEC none | payload %d B\n",
|
||||
liquid_version, OFDM_M, OFDM_CP, OFDM_TAPER, PAYLOAD_LEN);
|
||||
printf("цель: %.2f Msps, одно ядро (affinity CPU0)\n\n", target);
|
||||
|
||||
/* --- эталонный буфер: [гап | кадр] x N_FRAMES --- */
|
||||
const unsigned sym = OFDM_M + OFDM_CP;
|
||||
const unsigned cap = N_FRAMES * (GAP_SAMPLES + 220 * sym);
|
||||
float complex *sig = malloc(cap * sizeof(float complex));
|
||||
unsigned n = 0;
|
||||
|
||||
unsigned char hdr[8] = {0xF0, 0xAA, 0, 0, 0, 1, 0, 0};
|
||||
unsigned char pay[PAYLOAD_LEN];
|
||||
|
||||
for (unsigned f = 0; f < N_FRAMES; f++) {
|
||||
for (unsigned i = 0; i < GAP_SAMPLES && n < cap; i++)
|
||||
sig[n++] = 0;
|
||||
|
||||
for (unsigned i = 0; i < PAYLOAD_LEN; i++)
|
||||
pay[i] = rand() & 0xFF;
|
||||
|
||||
ofdmflexframegen_assemble(fg, hdr, pay, PAYLOAD_LEN);
|
||||
int done = 0;
|
||||
float complex buf[sym];
|
||||
while (!done && n + sym <= cap) {
|
||||
done = ofdmflexframegen_write(fg, buf, sym);
|
||||
for (unsigned i = 0; i < sym; i++)
|
||||
sig[n++] = buf[i] * AMP;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- int16-версии, как приходят из IIO: сигнал+шум и чистый шум --- */
|
||||
int16_t *iq_sig = malloc((size_t)n * 2 * sizeof(int16_t));
|
||||
int16_t *iq_noise = malloc((size_t)n * 2 * sizeof(int16_t));
|
||||
for (unsigned i = 0; i < n; i++) {
|
||||
float complex nz = NOISE_STD * (float)M_SQRT1_2 *
|
||||
(randnf() + randnf() * _Complex_I);
|
||||
float complex x = sig[i] + nz;
|
||||
iq_sig[2*i] = (int16_t)(crealf(x) * 2047);
|
||||
iq_sig[2*i+1] = (int16_t)(cimagf(x) * 2047);
|
||||
iq_noise[2*i] = (int16_t)(crealf(nz) * 2047);
|
||||
iq_noise[2*i+1] = (int16_t)(cimagf(nz) * 2047);
|
||||
}
|
||||
float complex *work = malloc((size_t)n * sizeof(float complex));
|
||||
|
||||
ofdmflexframesync fs =
|
||||
ofdmflexframesync_create(OFDM_M, OFDM_CP, OFDM_TAPER, NULL, cb, NULL);
|
||||
|
||||
printf("буфер: %u сэмплов, %u кадров на проход\n\n", n, N_FRAMES);
|
||||
|
||||
double m_noise = bench(fs, iq_noise, n, work, "шум (поиск):", target);
|
||||
double m_sig = bench(fs, iq_sig, n, work, "сигнал+кадры:", target);
|
||||
|
||||
double worst = (m_sig < m_noise) ? m_sig : m_noise;
|
||||
printf("\nУзкое место: %.3f Msps -> ", worst);
|
||||
if (worst > 2.0 * target)
|
||||
printf("OK: запас >2x, realtime на одном ядре реален\n");
|
||||
else if (worst > 1.2 * target)
|
||||
printf("ВПРИТЫК (1.2–2x): пойдёт, но без запаса — думать о снижении rate\n");
|
||||
else
|
||||
printf("НЕ ТЯНЕТ: снижать sample rate или уходить на single-carrier PHY\n");
|
||||
|
||||
free(sig); free(iq_sig); free(iq_noise); free(work);
|
||||
ofdmflexframegen_destroy(fg);
|
||||
ofdmflexframesync_destroy(fs);
|
||||
return 0;
|
||||
}
|
||||
205
src/receiver.c
Normal file
205
src/receiver.c
Normal file
@@ -0,0 +1,205 @@
|
||||
// receiver.c — OFDM-приёмник с RS(255,223) FEC и частотной коррекцией
|
||||
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#define BUF_SAMPLES 32768
|
||||
#define MAX_FRAME_SAMPLES (1 << 16)
|
||||
#define RX_GAIN 50.0
|
||||
#define CFO_FILTER_ALPHA 0.95f
|
||||
#define CFO_MAX_VALID 0.5f
|
||||
|
||||
extern volatile sig_atomic_t stop;
|
||||
|
||||
static uint64_t calls = 0, hdr_ok = 0, crc_raw = 0;
|
||||
static uint64_t rs_saved = 0, crc_final = 0, total_bytes = 0;
|
||||
static float rssi = -100, evm = 0;
|
||||
static float cfo_est = 0.0f;
|
||||
static nco_crcf nco = NULL;
|
||||
static fec decoder = NULL;
|
||||
static int fec_on = 0;
|
||||
static liquid_float_complex frame[MAX_FRAME_SAMPLES];
|
||||
|
||||
static int callback(unsigned char *hdr, int hdr_valid,
|
||||
unsigned char *pay, unsigned int pay_len,
|
||||
int pay_valid, framesyncstats_s stats,
|
||||
void *user) {
|
||||
(void)user;
|
||||
calls++;
|
||||
rssi = stats.rssi;
|
||||
evm = stats.evm;
|
||||
|
||||
// Обновляем CFO только если NCO инициализирован
|
||||
if (nco && fabsf(stats.cfo) < CFO_MAX_VALID) {
|
||||
cfo_est = CFO_FILTER_ALPHA * cfo_est + (1.0f - CFO_FILTER_ALPHA) * stats.cfo;
|
||||
nco_crcf_set_frequency(nco, cfo_est);
|
||||
}
|
||||
|
||||
if (!hdr_valid) return 0;
|
||||
hdr_ok++;
|
||||
if (pay_valid) crc_raw++;
|
||||
|
||||
uint16_t original_len = (hdr[6] << 8) | hdr[7];
|
||||
uint8_t last_block_bytes = hdr[10];
|
||||
|
||||
if (fec_on && decoder && pay_len >= RS_ENC) {
|
||||
size_t nblocks = pay_len / RS_ENC;
|
||||
if (nblocks == 0 || nblocks > 100) return 0;
|
||||
|
||||
uint8_t recovered[4096];
|
||||
int rs_ok = 1;
|
||||
for (size_t b = 0; b < nblocks; b++) {
|
||||
if (fec_decode(decoder, RS_DATA, pay + b * RS_ENC,
|
||||
recovered + b * RS_DATA) != 0) {
|
||||
rs_ok = 0; break;
|
||||
}
|
||||
}
|
||||
if (rs_ok) {
|
||||
if (!pay_valid) rs_saved++;
|
||||
size_t out_len = (nblocks - 1) * RS_DATA +
|
||||
(last_block_bytes > 0 ? last_block_bytes : RS_DATA);
|
||||
if (out_len > nblocks * RS_DATA) out_len = nblocks * RS_DATA;
|
||||
if (out_len > original_len) out_len = original_len;
|
||||
if (out_len > 0) {
|
||||
fwrite(recovered, 1, out_len, stdout);
|
||||
fflush(stdout);
|
||||
total_bytes += out_len;
|
||||
crc_final++;
|
||||
}
|
||||
}
|
||||
} else if (pay_valid && original_len > 0 && original_len <= pay_len) {
|
||||
fwrite(pay, 1, original_len, stdout);
|
||||
fflush(stdout);
|
||||
total_bytes += original_len;
|
||||
crc_final++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
long long freq = DEFAULT_FREQ, rate = DEFAULT_RATE, bw = DEFAULT_BW;
|
||||
const char *uri = RX_URI_DEFAULT;
|
||||
const char *fec_opt = "rs8";
|
||||
|
||||
int opt;
|
||||
while ((opt = getopt(argc, argv, "f:r:b:u:c:h")) != -1) {
|
||||
switch (opt) {
|
||||
case 'f': freq = atoll(optarg); break;
|
||||
case 'r': rate = atoll(optarg); break;
|
||||
case 'b': bw = atoll(optarg); break;
|
||||
case 'u': uri = optarg; break;
|
||||
case 'c': fec_opt = optarg; break;
|
||||
case 'h':
|
||||
print_usage(argv[0], "ПРИЁМНИК");
|
||||
fprintf(stderr, "\nСПЕЦИФИЧНЫЕ ОПЦИИ RX:\n");
|
||||
fprintf(stderr, " Усиление фиксированное: %.0f дБ\n", RX_GAIN);
|
||||
fprintf(stderr, " Частотная коррекция: адаптивная (α=%.2f)\n", CFO_FILTER_ALPHA);
|
||||
fprintf(stderr, "\nОПТИМАЛЬНЫЙ ЗАПУСК:\n");
|
||||
fprintf(stderr, " %s -f %lld -r %lld -b %lld -c rs8 > output.bin\n",
|
||||
argv[0], DEFAULT_FREQ, DEFAULT_RATE, DEFAULT_BW);
|
||||
return 0;
|
||||
default: return 1;
|
||||
}
|
||||
}
|
||||
|
||||
fec_on = !strcmp(fec_opt, "rs8");
|
||||
setup_signal_handlers();
|
||||
|
||||
print_config("ПРИЁМНИК", freq, rate, bw, fec_on);
|
||||
fprintf(stderr, "Усиление: %.0f дБ (ручное)\n", RX_GAIN);
|
||||
fprintf(stderr, "CFO фильтр: α=%.2f\n", CFO_FILTER_ALPHA);
|
||||
fprintf(stderr, "URI: %s\n", uri);
|
||||
fprintf(stderr, "═══════════════════════════════════════════════════\n");
|
||||
|
||||
struct iio_context *ctx = NULL;
|
||||
struct iio_device *phy = NULL, *rx = NULL;
|
||||
if (pluto_init(uri, &ctx, &phy, &rx, 0) != 0) return 1;
|
||||
|
||||
pluto_configure(phy, freq, rate, bw, RX_GAIN, 0);
|
||||
|
||||
iio_channel_enable(iio_device_find_channel(rx, "voltage0", false));
|
||||
iio_channel_enable(iio_device_find_channel(rx, "voltage1", false));
|
||||
|
||||
struct iio_buffer *buf = iio_device_create_buffer(rx, BUF_SAMPLES, false);
|
||||
if (!buf) {
|
||||
fprintf(stderr, "Ошибка создания буфера приёма\n");
|
||||
iio_context_destroy(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Инициализация FEC
|
||||
if (fec_on) {
|
||||
decoder = fec_create(LIQUID_FEC_RS_M8, NULL);
|
||||
if (!decoder) {
|
||||
fprintf(stderr, "Ошибка создания FEC-декодера\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация NCO для частотной коррекции
|
||||
nco = nco_crcf_create(LIQUID_NCO);
|
||||
if (!nco) {
|
||||
fprintf(stderr, "Ошибка создания NCO\n");
|
||||
} else {
|
||||
nco_crcf_set_frequency(nco, 0.0f);
|
||||
}
|
||||
|
||||
ofdmflexframesync fs = ofdmflexframesync_create(
|
||||
OFDM_M, OFDM_CP, OFDM_TAPER, NULL, callback, NULL);
|
||||
if (!fs) {
|
||||
fprintf(stderr, "Ошибка создания OFDM-синхронизатора\n");
|
||||
if (decoder) fec_destroy(decoder);
|
||||
if (nco) nco_crcf_destroy(nco);
|
||||
iio_buffer_destroy(buf);
|
||||
iio_context_destroy(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "Приёмник готов. Ожидание сигнала...\n\n");
|
||||
fprintf(stderr, "Кадры | Загол. | CRC | RS испр | Вывод | RSSI | EVM | CFO\n");
|
||||
fprintf(stderr, "--------+--------+------+---------+--------+-------+-------+----------\n");
|
||||
|
||||
time_t last = time(NULL);
|
||||
|
||||
while (!stop) {
|
||||
ssize_t n = iio_buffer_refill(buf);
|
||||
if (n < 0) { usleep(500); continue; }
|
||||
|
||||
int16_t *bb = (int16_t *)iio_buffer_start(buf);
|
||||
int ns = (iio_buffer_end(buf) - iio_buffer_start(buf)) / sizeof(int16_t) / 2;
|
||||
if (ns > MAX_FRAME_SAMPLES) ns = MAX_FRAME_SAMPLES;
|
||||
|
||||
for (int i = 0; i < ns; i++) {
|
||||
frame[i] = ((float)bb[2*i] / 2048.0f) +
|
||||
((float)bb[2*i+1] / 2048.0f) * _Complex_I;
|
||||
if (nco) {
|
||||
nco_crcf_mix_down(nco, frame[i], &frame[i]);
|
||||
nco_crcf_step(nco);
|
||||
}
|
||||
}
|
||||
if (nco) nco_crcf_set_phase(nco, 0.0f);
|
||||
|
||||
ofdmflexframesync_execute(fs, frame, ns);
|
||||
|
||||
if (time(NULL) > last) {
|
||||
fprintf(stderr, "\r%6llu | %5llu | %4llu | %6llu | %6llu | %+5.0f | %+5.1f | %+8.4f",
|
||||
(unsigned long long)calls, (unsigned long long)hdr_ok,
|
||||
(unsigned long long)crc_raw, (unsigned long long)rs_saved,
|
||||
(unsigned long long)crc_final, rssi, evm, cfo_est);
|
||||
fflush(stderr);
|
||||
last = time(NULL);
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "\n\nПРИЁМ ЗАВЕРШЁН\n");
|
||||
fprintf(stderr, "Кадров: %llu | Заголовков: %llu | CRC: %llu | RS испр: %llu | Вывод: %llu | Байт: %llu\n",
|
||||
(unsigned long long)calls, (unsigned long long)hdr_ok,
|
||||
(unsigned long long)crc_raw, (unsigned long long)rs_saved,
|
||||
(unsigned long long)crc_final, (unsigned long long)total_bytes);
|
||||
|
||||
if (decoder) fec_destroy(decoder);
|
||||
if (nco) nco_crcf_destroy(nco);
|
||||
ofdmflexframesync_destroy(fs);
|
||||
iio_buffer_destroy(buf);
|
||||
iio_context_destroy(ctx);
|
||||
return 0;
|
||||
}
|
||||
185
src/transmitter.c
Normal file
185
src/transmitter.c
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* transmitter.c — OFDM-передатчик с RS(255,223) FEC для PlutoSDR
|
||||
*
|
||||
* ОПТИМАЛЬНАЯ КОМАНДА ЗАПУСКА:
|
||||
* cat test.bin | ./transmitter -f 1265000000 -r 3840000 -b 2000000 -g -20 -a 0.3 -p 0 -c rs8
|
||||
*/
|
||||
|
||||
#include "common.h"
|
||||
|
||||
// Параметры передатчика по умолчанию
|
||||
#define DEFAULT_GAIN -30.0 // Усиление передатчика: -30 дБ
|
||||
#define DEFAULT_AMP 0.2f // Амплитуда сигнала
|
||||
#define DEFAULT_PAUSE 2000 // Пауза между кадрами: 2000 мкс
|
||||
#define BUF_SAMPLES 16384 // Размер буфера в отсчётах
|
||||
#define MAX_PAYLOAD 1024 // Максимальный размер полезной нагрузки
|
||||
|
||||
// Внешняя переменная флага завершения (определена в common.c)
|
||||
extern volatile sig_atomic_t stop;
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
// Инициализация параметров
|
||||
long long freq = DEFAULT_FREQ, rate = DEFAULT_RATE, bw = DEFAULT_BW;
|
||||
double gain = DEFAULT_GAIN;
|
||||
float amp = DEFAULT_AMP;
|
||||
const char *uri = TX_URI_DEFAULT;
|
||||
int pause_us = DEFAULT_PAUSE, use_fec = 1;
|
||||
|
||||
// Обработка аргументов командной строки
|
||||
int opt;
|
||||
while ((opt = getopt(argc, argv, "f:r:b:g:a:p:u:c:h")) != -1) {
|
||||
switch (opt) {
|
||||
case 'f': freq = atoll(optarg); break;
|
||||
case 'r': rate = atoll(optarg); break;
|
||||
case 'b': bw = atoll(optarg); break;
|
||||
case 'g': gain = atof(optarg); break;
|
||||
case 'a': amp = atof(optarg); break;
|
||||
case 'p': pause_us = atoi(optarg); break;
|
||||
case 'u': uri = optarg; break;
|
||||
case 'c': use_fec = strcmp(optarg, "none") ? 1 : 0; break;
|
||||
case 'h':
|
||||
print_usage(argv[0], "ПЕРЕДАТЧИК");
|
||||
fprintf(stderr, "\nСПЕЦИФИЧНЫЕ ОПЦИИ TX:\n");
|
||||
fprintf(stderr, " -g УСИЛЕНИЕ Усиление в дБ (по умолчанию: %.0f)\n", DEFAULT_GAIN);
|
||||
fprintf(stderr, " -a АМПЛИТ. Амплитуда сигнала (по умолчанию: %.2f)\n", DEFAULT_AMP);
|
||||
fprintf(stderr, " -p ПАУЗА Пауза между кадрами в мкс (по умолчанию: %d)\n", DEFAULT_PAUSE);
|
||||
fprintf(stderr, "\nОПТИМАЛЬНЫЙ ЗАПУСК:\n");
|
||||
fprintf(stderr, " cat test.bin | %s -f %lld -r %lld -b %lld -c rs8 > /dev/null\n",
|
||||
argv[0], DEFAULT_FREQ, DEFAULT_RATE, DEFAULT_BW);
|
||||
return 0;
|
||||
default: return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Настройка обработчиков сигналов
|
||||
setup_signal_handlers();
|
||||
|
||||
// Вывод конфигурации
|
||||
print_config("ПЕРЕДАТЧИК", freq, rate, bw, use_fec);
|
||||
fprintf(stderr, "Усиление: %.0f дБ\n", gain);
|
||||
fprintf(stderr, "Амплитуда: %.2f\n", amp);
|
||||
fprintf(stderr, "Пауза: %d мкс\n", pause_us);
|
||||
fprintf(stderr, "URI: %s\n", uri);
|
||||
fprintf(stderr, "═══════════════════════════════════════════════════\n");
|
||||
|
||||
// Инициализация PlutoSDR
|
||||
struct iio_context *ctx = NULL;
|
||||
struct iio_device *phy = NULL, *tx = NULL;
|
||||
if (pluto_init(uri, &ctx, &phy, &tx, 1) != 0) return 1;
|
||||
|
||||
// Настройка радиотракта
|
||||
pluto_configure(phy, freq, rate, bw, gain, 1);
|
||||
|
||||
// Включение каналов передатчика
|
||||
iio_channel_enable(iio_device_find_channel(tx, "voltage0", true));
|
||||
iio_channel_enable(iio_device_find_channel(tx, "voltage1", true));
|
||||
|
||||
// Создание буфера передачи
|
||||
struct iio_buffer *buf = iio_device_create_buffer(tx, BUF_SAMPLES, false);
|
||||
if (!buf) {
|
||||
LOG_ERROR("Ошибка создания буфера передачи");
|
||||
iio_context_destroy(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Инициализация FEC и OFDM
|
||||
fec enc = use_fec ? fec_create(LIQUID_FEC_RS_M8, NULL) : NULL;
|
||||
|
||||
ofdmflexframegenprops_s fgp;
|
||||
ofdmflexframegenprops_init_default(&fgp);
|
||||
fgp.check = LIQUID_CRC_32;
|
||||
ofdmflexframegen fg = ofdmflexframegen_create(OFDM_M, OFDM_CP, OFDM_TAPER, NULL, &fgp);
|
||||
|
||||
// Счётчики статистики
|
||||
uint64_t frames = 0, bytes_tx = 0;
|
||||
uint32_t seq = 0;
|
||||
time_t last = time(NULL);
|
||||
|
||||
fprintf(stderr, " Передатчик готов. Ожидание данных...\n\n");
|
||||
|
||||
// Основной цикл передачи
|
||||
while (!stop) {
|
||||
uint8_t raw[MAX_PAYLOAD];
|
||||
size_t n = fread(raw, 1, MAX_PAYLOAD, stdin);
|
||||
if (!n) {
|
||||
if (feof(stdin) || stop) break;
|
||||
usleep(10000);
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t *tx_payload = NULL;
|
||||
size_t tx_len = 0;
|
||||
uint8_t hdr[HDR_SIZE] = {0};
|
||||
|
||||
// Кодирование данных
|
||||
if (enc) {
|
||||
size_t total_blocks = (n + RS_DATA - 1) / RS_DATA;
|
||||
tx_len = total_blocks * RS_ENC;
|
||||
tx_payload = malloc(tx_len);
|
||||
|
||||
for (size_t b = 0; b < total_blocks; b++) {
|
||||
uint8_t blk[RS_DATA] = {0};
|
||||
size_t bytes = (b == total_blocks - 1) ? n - b * RS_DATA : RS_DATA;
|
||||
memcpy(blk, raw + b * RS_DATA, bytes);
|
||||
fec_encode(enc, RS_DATA, blk, tx_payload + b * RS_ENC);
|
||||
}
|
||||
|
||||
// Формирование заголовка
|
||||
hdr[0] = HDR_SIGNATURE_0; hdr[1] = HDR_SIGNATURE_1;
|
||||
hdr[2] = (seq >> 24) & 0xFF; hdr[3] = (seq >> 16) & 0xFF;
|
||||
hdr[4] = (seq >> 8) & 0xFF; hdr[5] = seq & 0xFF;
|
||||
hdr[6] = (n >> 8) & 0xFF; hdr[7] = n & 0xFF;
|
||||
hdr[8] = (total_blocks >> 8) & 0xFF; hdr[9] = total_blocks & 0xFF;
|
||||
hdr[10] = n % RS_DATA; hdr[11] = 0;
|
||||
} else {
|
||||
tx_len = n;
|
||||
tx_payload = malloc(tx_len);
|
||||
memcpy(tx_payload, raw, n);
|
||||
|
||||
hdr[0] = HDR_SIGNATURE_0; hdr[1] = HDR_SIGNATURE_1;
|
||||
hdr[2] = (seq >> 24) & 0xFF; hdr[3] = (seq >> 16) & 0xFF;
|
||||
hdr[4] = (seq >> 8) & 0xFF; hdr[5] = seq & 0xFF;
|
||||
hdr[6] = (n >> 8) & 0xFF; hdr[7] = n & 0xFF;
|
||||
}
|
||||
|
||||
// Сборка OFDM-кадра
|
||||
ofdmflexframegen_assemble(fg, hdr, tx_payload, tx_len);
|
||||
free(tx_payload);
|
||||
|
||||
// Формирование выходного сигнала
|
||||
int16_t *out = (int16_t *)iio_buffer_start(buf);
|
||||
memset(out, 0, BUF_SAMPLES * 2 * sizeof(int16_t));
|
||||
int idx = 0, done = 0;
|
||||
float complex symb[OFDM_M + OFDM_CP];
|
||||
|
||||
while (!done && (idx + OFDM_M + OFDM_CP) <= BUF_SAMPLES) {
|
||||
done = ofdmflexframegen_write(fg, symb, OFDM_M + OFDM_CP);
|
||||
for (int i = 0; i < OFDM_M + OFDM_CP; i++) {
|
||||
out[2 * idx] = (int16_t)(crealf(symb[i]) * 32767 * amp);
|
||||
out[2 * idx + 1] = (int16_t)(cimagf(symb[i]) * 32767 * amp);
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
if (idx > 0) iio_buffer_push(buf);
|
||||
|
||||
// Обновление статистики
|
||||
frames++; bytes_tx += n; seq++;
|
||||
if (time(NULL) > last) {
|
||||
fprintf(stderr, "\r📤 Передано: %llu кадров, %llu байт",
|
||||
(unsigned long long)frames, (unsigned long long)bytes_tx);
|
||||
fflush(stderr);
|
||||
last = time(NULL);
|
||||
}
|
||||
if (pause_us > 0) usleep(pause_us);
|
||||
}
|
||||
|
||||
fprintf(stderr, "\nПередача завершена: %llu кадров, %llu байт\n",
|
||||
(unsigned long long)frames, (unsigned long long)bytes_tx);
|
||||
|
||||
// Освобождение ресурсов
|
||||
if (enc) fec_destroy(enc);
|
||||
ofdmflexframegen_destroy(fg);
|
||||
iio_buffer_destroy(buf);
|
||||
iio_context_destroy(ctx);
|
||||
return 0;
|
||||
}
|
||||
20
toolchain.env
Normal file
20
toolchain.env
Normal file
@@ -0,0 +1,20 @@
|
||||
# toolchain.env — окружение кросс-сборки pluto-link
|
||||
# Использование в скриптах: source "$(dirname "$0")/../toolchain.env"
|
||||
# Значения согласованы с makefile (там задаются через ?=).
|
||||
|
||||
export CROSS=arm-linux-gnueabihf-
|
||||
export CC=${CROSS}gcc
|
||||
export XARM="$HOME/xarm" # префикс кросс-библиотек (static)
|
||||
|
||||
export XCFLAGS="-O3 -mcpu=cortex-a9 -mfpu=neon -mfloat-abi=hard -ffast-math"
|
||||
|
||||
# Платы (переопределяй здесь или переменными окружения)
|
||||
export PLUTO_A="${PLUTO_A:-192.168.2.1}" # передатчик
|
||||
export PLUTO_B="${PLUTO_B:-192.168.3.1}" # приёмник
|
||||
export PLUTO_PASS="analog" # дефолтный пароль root
|
||||
|
||||
# Примечания:
|
||||
# - прошивка v0.38: заливка только `scp -O` (sftp-server на плате нет)
|
||||
# - rootfs платы в RAM: /tmp очищается при перезагрузке
|
||||
# - для запуска ssh/scp без ввода пароля: sudo apt install sshpass,
|
||||
# тогда скрипты используют sshpass -p "$PLUTO_PASS" автоматически
|
||||
Reference in New Issue
Block a user