Compare commits
10 Commits
b5bf0c7b18
...
c4933e60c5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4933e60c5 | ||
|
|
ab1e87a17e | ||
|
|
6294a472cd | ||
|
|
3b6a56e54c | ||
|
|
b9846fcbdc | ||
|
|
4ee9274f09 | ||
|
|
3b39f6cfc1 | ||
|
|
42c9970b84 | ||
|
|
fc836f2af1 | ||
|
|
341a5555ab |
@@ -42,6 +42,8 @@ PHY: OFDM через liquid-dsp 1.8.0 + FFTW/NEON, FEC RS(255,223).
|
||||
bit5 GF_Q (Q иначе P), bits4-0 idx кадра в группе (0..17). 0 = legacy-кадр.
|
||||
Group-FEC 16+2: 16 кадров данных + P(⊕) + Q(Σgⁱ) над GF(256), закрывает ≤2
|
||||
стёртых кадра/группу без обратного канала (src/group_fec.c). RX автодетект.
|
||||
Сигнатура F0 E7 — END (этап 3): payload 12 Б, total_seq u32 + total_bytes u64,
|
||||
маркер конца передачи для хендшейка END/COMPLETE (feedback.h, §12.9).
|
||||
КРИТИЧНО: liquid по умолчанию передаёт 8 Б заголовка. Обязательно
|
||||
`ofdmflexframegen_set_header_len(fg,12)` и
|
||||
`ofdmflexframesync_set_header_len(fs,12)` на обеих сторонах.
|
||||
@@ -55,7 +57,8 @@ TX LO: канал `altvoltage1` (output); RX LO: `altvoltage0` (output).
|
||||
#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
|
||||
· #6 ✅ закрыт на FDD: ARQ (NACK→ретрансмит) + END/COMPLETE, §12.9. На симплексе
|
||||
ARQ невозможен — там детектор [ПОТЕРЯ] + group-FEC · #7 reciever.c → receiver.c
|
||||
|
||||
## Команды
|
||||
Сборка: `make` · Деплой: `make deploy` (scp -O! sftp на плате нет)
|
||||
|
||||
101
README.md
101
README.md
@@ -277,7 +277,7 @@ ssh root@192.168.2.1 '/tmp/ofdm_bench 1.92'
|
||||
| 3 | `fec_decode()` liquid не сообщает о неисправимых RS-блоках → счётчик `rs_saved` фиктивен. Достоверный критерий — только CRC/md5 поверх данных | ✅ исправлено: CRC32 поверх данных до RS на TX, сверка после декода на RX; `rs_saved`/`rs_fail` достоверны (подтверждено loopback'ом, §12.2) |
|
||||
| 4 | Внешний NCO-CFO в receiver: `set_phase(0)` на границах буферов рвёт фазу; ofdmflexframesync и так компенсирует CFO сам | ✅ исправлено: внешний NCO удалён, CFO оставлен только как телеметрия |
|
||||
| 5 | TX пушит весь буфер 16384 сэмпла при кадре ~10300 → ~35% эфира впустую + паузы `-p` | ✅ исправлено: `iio_buffer_push_partial(idx)` + дренаж хвоста DMA перед закрытием |
|
||||
| 6 | Нет ARQ: потерянный кадр = молчаливая дыра в файле. seq в заголовке есть, но RX его игнорирует | ⚙️ частично: детектор пропусков seq (лог `[ПОТЕРЯ]` + счётчик) + **групповой стирающий FEC 16+2 закрывает ≤2 потери/группу на симплексе без обратного канала (§12.7, п.6)**; полный ARQ — на дуплексе (п.7) |
|
||||
| 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. Дорожная карта
|
||||
|
||||
@@ -303,7 +303,15 @@ ssh root@192.168.2.1 '/tmp/ofdm_bench 1.92'
|
||||
байт-в-байт на рабочей точке `-p 4000`. Защита информации (лёгкий AEAD,
|
||||
+16 Б/кадр, <1% CPU) — отложена, требования ОКР «R-Link» нет; формат
|
||||
с резервом под неё.
|
||||
7. FDD 868/915 двусторонняя + ARQ с обратным каналом + меры из docs/NOTES.md
|
||||
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-туннель поверх линка; затем web-настройка (libmicrohttpd)
|
||||
9. Видео (raw UDP, пакеты ≤1472 Б) — при устойчивом PER
|
||||
|
||||
@@ -546,6 +554,95 @@ GF-математика самопроверена офлайн: `make gftest`
|
||||
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`.
|
||||
|
||||
**Гибрид с 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.*
|
||||
@@ -145,6 +145,20 @@ ARQ или дешевле PHY (M=32/CP=8, резерв п.1).
|
||||
> (~60 Overrun, потери 2.7%) — рабочая точка §12.6 снята на коротких прогонах.
|
||||
> Для длинных передач / группового FEC — **`-p 4000`** (Overrun 0, ~95 КБ/с).
|
||||
|
||||
## Стоимость framegen короткого кадра (16-07-2026) — обратный канал STATUS
|
||||
|
||||
Перед включением обратного канала (README §10 п.7, этап 2) замерена сборка+
|
||||
модуляция короткого кадра 255 Б (STATUS = 1 RS-блок) — `bench_framegen`
|
||||
в составе штатного прогона `ofdm_bench` (плата A, ядро 0):
|
||||
|
||||
```
|
||||
framegen кадр 255Б 570.6 мкс/кадр 1753 кадров/с (STATUS 10 Гц ≈ 0.571% ядра)
|
||||
```
|
||||
|
||||
STATUS 10 Гц на потоке C приёмника стоит ~0.6% ядра 1 — пренебрежимо.
|
||||
Геометрия PHY не меняется (та же сетка M=64/CP=16, тот же rate) → таблицы
|
||||
выше в силе, правило №6 CLAUDE.md соблюдено.
|
||||
|
||||
## Неисследованные резервы (если понадобится > 1.92 MSPS)
|
||||
|
||||
1. **M=32/CP=8** — дешевле FFT и эквалайзер; цена: выше доля пилотов,
|
||||
|
||||
@@ -33,7 +33,7 @@ Sinosun MIMOMesh построен на **AD936x + Zynq (7Z020/7Z030/7Z035/7Z100)
|
||||
| Топология | точка-точка, **симплекс** | MANET, 256+ узлов, multi-hop | MANET, до 559 узлов |
|
||||
| Дуплекс | нет (FDD 868/915 — roadmap §10 п.7) | TDD | TDD, single-frequency |
|
||||
| Надёжность/FEC | RS(255,223) внутрикадрово + **group-FEC 16+2** (эрейжер, GF(256), overhead 12.5%), измеренный PER 0.26–0.36%, 10 МБ соук байт-в-байт (§12.7) | адаптивная модуляция под канал (заявлено) | адаптивная модуляция под канал (заявлено) |
|
||||
| ARQ | нет — закрыто частично group-FEC без обратного канала; полный ARQ на дуплексе (§10 п.7, техдолг #6) | есть (MAC-уровень) | есть (MAC-уровень) |
|
||||
| ARQ | ✅ есть на FDD (селективный ретрансмит по NACK + END/COMPLETE, §12.9, техдолг #6 закрыт); на симплексе — group-FEC без обратного канала, ARQ там недоступен принципиально | есть (MAC-уровень) | есть (MAC-уровень) |
|
||||
| Латентность | не нормируется отдельно; эфир кадра 5.33 мс + пауза TX 3–4 мс (§12.6/12.7) | ~6 мс/хоп (заявлено) | ~7 мс/хоп (заявлено) |
|
||||
| TX-мощность | AD9361 без внешнего УМ, рабочая точка gain −20 дБ (единицы мВт на разъёме, §12.4) | 2×2 / 4×2 / 5×2 Вт | до 10 Вт (20 Вт эфф. с beamforming) |
|
||||
| Диапазон | 915 МГц (тест); AD9361 tunable 70 МГц–6 ГГц, но без фильтров/УМ под конкретный банд | заказные банды 225 МГц – 5.85 ГГц | dual-band, конкретные частоты не раскрыты |
|
||||
@@ -134,8 +134,13 @@ group-FEC слоя, а не переписывание протокола с н
|
||||
|
||||
## 7. Приоритеты (рекомендация)
|
||||
|
||||
1. Завершить **§10 п.7** (FDD + ARQ) — даёт максимальный прирост надёжности
|
||||
при чисто софтовых изменениях, закрывает последний открытый техдолг (#6).
|
||||
1. ✅ **Завершить §10 п.7** (FDD + ARQ) — **выполнено 2026-07-16** (README §12.9,
|
||||
техдолг #6 закрыт на FDD). Полная приёмочная матрица пройдена на железе:
|
||||
гибрид group-FEC+ARQ на 10 МБ вытягивает 51/51 потерю всего 6 ретрансмитами
|
||||
(паритет закрывает 45 из них немедленно, без обратного канала) — то самое
|
||||
сочетание «стирающий FEC + ARQ», которого по отдельности нет ни у одного
|
||||
конкурента в §2. На симплексе (без обратного канала) техдолг закрыт
|
||||
group-FEC-ом, ARQ там принципиально недоступен — так и задумано.
|
||||
2. Адаптивная модуляция (QPSK↔16QAM) — измеримый шаг к паритету по
|
||||
«auto link adaptation» с минимальными изменениями кода.
|
||||
3. **§10 п.8** (UDP-туннель) — переводит pluto-link из «демо передачи файла»
|
||||
|
||||
101
docs/NOTES.md
101
docs/NOTES.md
@@ -46,6 +46,107 @@ RX платы «глохнет», как только её TX активен. Э
|
||||
Поэтому порядок работ: **сначала симплекс** (обе платы 915 МГц, полудуплекс
|
||||
по очереди), FDD — отдельным этапом с замерами деградации RX при активном TX.
|
||||
|
||||
### 2.1. Предварительные замеры (этап 1, §10 п.7 — FDD-водопровод)
|
||||
|
||||
Числа сняты на **сконфигурированном**, но молчащем обратном тракте (LO 868
|
||||
поднят, `hardwaregain` выставлен, но DMA обратного TX ничего не пушит).
|
||||
Полноценный gate с реальным STATUS-трафиком — этап 2, числа уйдут в §2.2.
|
||||
|
||||
- **Самоглушения от «просто включённого» обратного тракта не видно.** Форвард
|
||||
A→B под FDD (1 МБ, `-p 4000`, group-FEC) байт-в-байт и при fb-TX `-g -89`
|
||||
(≈выкл), и при `-g -20` (штатное). Активный обратный LO 868 в 47 МГц от
|
||||
RX 915 приёмник данных не глушит — на кабеле+аттенюаторе. Это ответ на
|
||||
вопрос PER₁ (десенс от включённого тракта), но НЕ на PER₂ (десенс от
|
||||
реального излучения STATUS) — тот снимается в этапе 2.
|
||||
- **Диагностика: пустой файл на выходе RX ≠ самоглушение.** Симптом «0 кадров,
|
||||
RSSI −100» — это НЕ глушение, а мёртвый вход: остаточный процесс держит
|
||||
RX-DMA (`cf-ad9361-lpc`) от неубитого прошлого запуска. Глушение выглядит
|
||||
ИНАЧЕ — сильный RSSI (−48) при разрушенном EVM (−2.5). **Различать по RSSI.**
|
||||
Лечение: `killall -9 receiver transmitter` перед каждым стартом (уже в
|
||||
`scripts/test_fdd_fwd.sh`).
|
||||
- **Ридбэк LO округляется PLL.** AD9361 — дробный-N синтезатор с шагом ~2 Гц:
|
||||
`868000000` читается как `867999998`. Это норма, не ошибка; сверка ридбэка
|
||||
идёт с допуском `FDD_LO_TOL_HZ`=100 Гц (common.c).
|
||||
- **Канал деградировал против этапа 0.** Эталон регрессии сдвинут `-p 3000` →
|
||||
`-p 4000` (на 3000/3500 теперь Overrun+потери). Направление **B→A** флапает
|
||||
даже на `-p 4000` при СИЛЬНОМ RSSI (−33…−35) — вероятна компрессия входа RX
|
||||
(усиление 50 дБ избыточно). Проверить снижением RX/TX gain до свипа частот.
|
||||
|
||||
### 2.2. Gate-замер самоглушения (этап 2, 2026-07-16) — вердикт: FDD принят
|
||||
|
||||
Условия: кабель+аттенюатор, данные A→B 915 (`TXGAIN −30`, `-p 4000`,
|
||||
GROUPFEC=0 — сырой PER без маскировки), STATUS B→A 868 (10 Гц),
|
||||
1 МБ = 1024 кадра на прогон. Полные числа — README §12.8.
|
||||
|
||||
- **PER₀** (симплекс, 3×): потерь 3/5/14 → 0.29–1.37%, среднее 0.72%
|
||||
(канал флапает, см. выше).
|
||||
- **PER₂** (FDD, STATUS в эфире, FBGAIN −80…0, 8 прогонов): потерь 1–11,
|
||||
среднее 0.51%. **PER₂−PER₀ ≈ −0.2 п.п. — излучение STATUS форвард-линк
|
||||
не трогает вообще, ни при каком усилении.** PER₁≈PER₀ снят ранее (§2.1).
|
||||
- **Доставка STATUS B→A**: 0% при FBGAIN ≤ −40 (не пробивает аттенюатор);
|
||||
**плато ~50% при −20…0** (47/52/52/50/51%) — от усиления НЕ зависит.
|
||||
- FBGAIN=0 дал первые «RS битых: 3» на RX данных платы B → лёгкий самодесенс
|
||||
B на максимуме fb-TX. Рабочее окно fb-TX на кабеле: **−20…−10**.
|
||||
|
||||
**Плато 50% — структурное, не RF.** Гипотеза: fb-RX платы A десенсится её же
|
||||
TX-бёрстом (механизм §2, но на стороне A). Duty данных при `-p 4000` = 57%
|
||||
(кадр 5.33 мс + пауза 4 мс); STATUS ~1.4 мс выживает только попав в паузу —
|
||||
доля доставки ≈ доле тихого времени и потому не зависит от FBGAIN. Проверка
|
||||
при случае: `PAUSE=8000` должен поднять доставку. Приятное следствие: в фазе
|
||||
ретрансмитов и END-хендшейка ARQ плата A передаёт мало → доставка растёт
|
||||
именно там, где обратная связь критична.
|
||||
|
||||
**Вердикт gate.** Исходный критерий «≥99% доставки» был перестрахован и
|
||||
пересмотрен: STATUS кумулятивен по построению (потеря безвредна — следующий
|
||||
несёт то же состояние), 50% доставки = эффективные ~5 Гц фидбэка, для ARQ
|
||||
с rate-limit 2× периода и TX-окном ~19 с этого с большим запасом достаточно.
|
||||
Рабочий критерий: доставка стабильна (≥30%) И PER₂−PER₀ ≤ +0.3 п.п. И
|
||||
PER₁≈PER₀ — **выполнен, этап 5 (fallback TDD, цена ~8% throughput) не нужен**.
|
||||
|
||||
Замечание к методике: у приёмника НЕТ флага усиления RX данных (константа
|
||||
`RX_GAIN=50` в коде; `-g` приёмника — усиление fb-TX). Переменная окружения
|
||||
`RX_GAIN` скриптами не читается — прогоны «RX_GAIN=40/30/20» были репликами
|
||||
FBGAIN=0 (они и дали три точки плато). Гипотеза компрессии входа при RSSI
|
||||
−33…−35 (см. §2.1) остаётся непроверенной, но gate она не блокирует.
|
||||
|
||||
### 2.3. Находки этапа 3 (ARQ, 2026-07-16)
|
||||
|
||||
**Клифф канала на кабеле — свипу PER здесь не место.** `TXGAIN −30` даёт
|
||||
PER 0.4%, `−38` — уже `CRC: 0` при живых `Заголовках: 3322`. Порог заголовка и
|
||||
порога payload разнесены: заголовок ofdmflexframe идёт BPSK с собственным FEC,
|
||||
payload — QPSK+RS(255,223), и падает он первым. Практический смысл: **живой
|
||||
счётчик «Заголовков» при нулевом CRC — это не «плохой канал», а его отсутствие**;
|
||||
диагностику по такому прогону строить нельзя, ARQ там нечего вытягивать.
|
||||
Аттенюатор даёт слишком грубый шаг — карта PER снята свипом в README §12.4.
|
||||
|
||||
**Направления несимметричны.** На одном и том же кабеле A→B даёт `RS битых: 0`,
|
||||
B→A — `RS битых: 20…28` на 1024 кадрах. Разброс устойчив между прогонами, то
|
||||
есть это не статистика, а свойство пары плат/тракта. Для ARQ безразлично
|
||||
(ретрансмит закроет), но при выборе направления для чувствительного трафика и
|
||||
при трактовке PER-замеров это надо помнить: цифры одного направления не
|
||||
переносятся на другое.
|
||||
|
||||
**Ловушка «битый кадр выпадает из NACK».** `miss_ack(seq)` по факту приёма кадра
|
||||
(до декода) — тихо ломает ARQ: кадр, не прошедший RS/CRC, снимался с NACK
|
||||
навсегда, A его не досылал, а RX считал файл собранным → **A рапортовал успех
|
||||
с битым md5**. Учёт NACK обязан идти ПОСЛЕ декода: `ok → miss_ack`,
|
||||
`!ok → miss_add_sorted`. Битый кадр — такая же дыра, как непришедший, просто
|
||||
обнаруженная не детектором seq. Не выстрелило на приёмке 3a лишь потому, что
|
||||
там случились `RS битых: 0`.
|
||||
|
||||
**Ловушка «COMPLETE съеден снимком».** STATUS на A забирается снимком
|
||||
(latest-wins, `fb_take` гасит флаг свежести). Если бы COMPLETE читался из
|
||||
снимка, `arq_service` съел бы тот самый STATUS, в котором он приехал, и A ушёл
|
||||
бы в 10-секундный таймаут на ЦЕЛОМ файле. COMPLETE поэтому — отдельная
|
||||
монотонная защёлка, взводимая прямо в `fb_callback`.
|
||||
|
||||
**Переполнение MISS_CAP — тупик без пересборки.** `miss_add` при переполнении
|
||||
молча теряет seq, и вернуть его в NACK уже некому: детектор дыр по нему второй
|
||||
раз не сработает (`highest_seq` ушёл вперёд). В тяжёлом канале это заклинивало
|
||||
приём насмерть. Лечится тем, что кольцо reorder знает точно, каких кадров нет:
|
||||
`miss_rebuild()` пересобирает NACK из кольца на каждом END (A шлёт их 10 Гц до
|
||||
самого COMPLETE, скан ≤1024 — сотые доли ядра).
|
||||
|
||||
## 3. Регуляторика (868/915 МГц в регионе ETSI)
|
||||
|
||||
- **868 МГц** — SRD-диапазон: ограничения на полосу канала (сотни кГц),
|
||||
|
||||
8
makefile
8
makefile
@@ -25,11 +25,11 @@ all: $(BIN)/transmitter $(BIN)/receiver $(BIN)/ofdm_bench
|
||||
$(BIN):
|
||||
mkdir -p $(BIN)
|
||||
|
||||
$(BIN)/transmitter: $(SRC)/transmitter.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/frame_tx.c $(SRC)/common.h $(SRC)/group_fec.h $(SRC)/frame_tx.h $(DEPS_LIBS) | $(BIN)
|
||||
$(CC) $(CFLAGS) $(SRC)/transmitter.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/frame_tx.c -o $@ $(LDFLAGS) -Wl,--start-group -lad9361 -liio $(LDLIBS) -Wl,--end-group
|
||||
$(BIN)/transmitter: $(SRC)/transmitter.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/frame_tx.c $(SRC)/feedback.c $(SRC)/common.h $(SRC)/group_fec.h $(SRC)/frame_tx.h $(SRC)/feedback.h $(DEPS_LIBS) | $(BIN)
|
||||
$(CC) $(CFLAGS) $(SRC)/transmitter.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/frame_tx.c $(SRC)/feedback.c -o $@ $(LDFLAGS) -Wl,--start-group -lad9361 -liio $(LDLIBS) -Wl,--end-group
|
||||
|
||||
$(BIN)/receiver: $(SRC)/receiver.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/common.h $(SRC)/group_fec.h $(DEPS_LIBS) | $(BIN)
|
||||
$(CC) $(CFLAGS) $(SRC)/receiver.c $(SRC)/common.c $(SRC)/group_fec.c -o $@ $(LDFLAGS) -Wl,--start-group -lad9361 -liio $(LDLIBS) -Wl,--end-group
|
||||
$(BIN)/receiver: $(SRC)/receiver.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/frame_tx.c $(SRC)/feedback.c $(SRC)/common.h $(SRC)/group_fec.h $(SRC)/frame_tx.h $(SRC)/feedback.h $(DEPS_LIBS) | $(BIN)
|
||||
$(CC) $(CFLAGS) $(SRC)/receiver.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/frame_tx.c $(SRC)/feedback.c -o $@ $(LDFLAGS) -Wl,--start-group -lad9361 -liio $(LDLIBS) -Wl,--end-group
|
||||
|
||||
$(BIN)/ofdm_bench: $(SRC)/ofdm_bench.c | $(BIN)
|
||||
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) $(LDLIBS)
|
||||
|
||||
146
scripts/test_arq.sh
Executable file
146
scripts/test_arq.sh
Executable file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
# test_arq.sh — e2e-тест ARQ и EOF-хендшейка (этап 3, §10 п.7, техдолг #6).
|
||||
#
|
||||
# Отличие от test_fdd_fwd.sh: там успех = md5 совпал, а завершение обеих сторон
|
||||
# организует скрипт (killall + сторожевой kill). Здесь проверяется сам ПРОТОКОЛ:
|
||||
# • B, собрав файл целиком, поднимает FB_FLAG_COMPLETE в STATUS и выходит САМ;
|
||||
# • A ждёт этот COMPLETE и возвращает 0; не дождался за 10 с → exit 2.
|
||||
# Поэтому сторожевой kill приёмника = провал (B не смог завершиться сам), а
|
||||
# ненулевой код A — не аварийная ситуация, а штатный отчёт «файл не подтверждён».
|
||||
#
|
||||
# Использование: scripts/test_arq.sh [ipA] [ipB] [размер_КБ]
|
||||
# SIZE_KB=10240 — длинный прогон (или третьим позиционным аргументом)
|
||||
# TXGAIN=-33 — стресс: высокий PER, ARQ обязан вытянуть md5. Клифф на
|
||||
# кабеле резкий: −30 ≈ PER 0.4%, −38 и глубже — CRC 0
|
||||
# (канала нет вовсе, проверять там нечего)
|
||||
# NEGATIVE=1 FBGAIN=-89 — обратный канал заглушён: успех = A НЕ подтвердил
|
||||
# FBGAIN (деф. −15) — середина рабочего окна −20…−10 (README §12.8)
|
||||
# FBFREQ, PAUSE, TXGAIN, FEC, GROUPFEC — как в test_fdd_fwd.sh
|
||||
# ВНИМАНИЕ: сначала кабель + аттенюатор 30–40 дБ!
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/../toolchain.env"
|
||||
|
||||
A="${1:-$PLUTO_A}" # передатчик данных (TX 915 / fb-RX 868)
|
||||
B="${2:-$PLUTO_B}" # приёмник данных (RX 915 / fb-TX 868)
|
||||
SIZE_KB="${3:-${SIZE_KB:-1024}}"
|
||||
|
||||
FREQ=915000000
|
||||
FBFREQ="${FBFREQ:-868000000}"
|
||||
RATE=1920000 # НЕ поднимать: бюджет CPU, docs/BENCHMARK.md
|
||||
BW=1500000
|
||||
TXGAIN="${TXGAIN:--30}"
|
||||
FBGAIN="${FBGAIN:--15}" # штатный обратный канал; −89 ≈ выкл (NEGATIVE)
|
||||
FEC="${FEC:-rs8}"
|
||||
PAUSE="${PAUSE:-4000}" # §12.7: соук/маргинальный канал — только 4000, не 3000
|
||||
GROUPFEC="${GROUPFEC:-1}"
|
||||
NEGATIVE="${NEGATIVE:-0}"
|
||||
[ "$GROUPFEC" = 1 ] && GFLAG="-G" || GFLAG=""
|
||||
# +40 (против +20 у test_fdd_fwd): END-фаза до 10 с плюс запас на ретрансмиты.
|
||||
TIMEOUT=$(( SIZE_KB / 85 + 40 ))
|
||||
|
||||
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 "=== ARQ e2e: A=$A (TX $((FREQ/1000000))/fb-RX $((FBFREQ/1000000))) → B=$B ==="
|
||||
echo " ${SIZE_KB} КБ TXGAIN=$TXGAIN FBGAIN=$FBGAIN PAUSE=$PAUSE GROUPFEC=$GROUPFEC NEGATIVE=$NEGATIVE"
|
||||
|
||||
echo "=== [1/6] Тестовый файл ${SIZE_KB} КБ ==="
|
||||
dd if=/dev/urandom of=/tmp/arq_test.bin bs=1024 count="$SIZE_KB" status=none
|
||||
MD5_SRC=$(md5sum /tmp/arq_test.bin | cut -d' ' -f1)
|
||||
echo "md5: $MD5_SRC"
|
||||
|
||||
echo "=== [2/6] Зачистка хвостов на обеих платах ==="
|
||||
# Остаточный процесс держит RX-DMA → пустой out.bin (грабля из README §12.6).
|
||||
"${SSH[@]}" "root@$A" "killall -9 transmitter receiver 2>/dev/null || true"
|
||||
"${SSH[@]}" "root@$B" "killall -9 transmitter receiver 2>/dev/null || true"
|
||||
|
||||
echo "=== [3/6] Заливка файла на Pluto A ($A) ==="
|
||||
"${SCP[@]}" /tmp/arq_test.bin "root@$A:/tmp/"
|
||||
|
||||
echo "=== [4/6] Старт приёмника на Pluto B ($B), сторож ${TIMEOUT}с ==="
|
||||
# busybox без `timeout` → сторожевой sleep+kill на плате. Штатно B выходит САМ по
|
||||
# COMPLETE задолго до сторожа; сторож снимается сразу после выхода B, иначе ssh
|
||||
# висел бы до конца sleep и скрывал реальное время завершения.
|
||||
"${SSH[@]}" "root@$B" \
|
||||
"rm -f /tmp/out.bin; /tmp/receiver -f $FREQ -r $RATE -b $BW -c $FEC \
|
||||
-F $FBFREQ -g $FBGAIN -u local: > /tmp/out.bin 2>/tmp/rx_arq.log & \
|
||||
RXP=\$!; (sleep $TIMEOUT; kill \$RXP 2>/dev/null) & WDP=\$!; \
|
||||
wait \$RXP; RC=\$?; kill \$WDP 2>/dev/null; exit \$RC" &
|
||||
RX_PID=$!
|
||||
sleep 4 # дать RX прогреться (FDD-инициализация двух трактов дольше)
|
||||
|
||||
echo "=== [5/6] Передача с Pluto A + EOF-хендшейк ==="
|
||||
TX_RC=0
|
||||
"${SSH[@]}" "root@$A" \
|
||||
"cat /tmp/arq_test.bin | /tmp/transmitter -f $FREQ -r $RATE -b $BW \
|
||||
-g $TXGAIN -p $PAUSE -c $FEC $GFLAG -F $FBFREQ -u local: 2>/tmp/tx_arq.log" \
|
||||
|| TX_RC=$?
|
||||
echo "код возврата передатчика: $TX_RC (0 = COMPLETE подтверждён, 2 = таймаут)"
|
||||
|
||||
wait "$RX_PID" 2>/dev/null || true # B выходит сам либо снимается сторожем
|
||||
"${SSH[@]}" "root@$B" "killall receiver 2>/dev/null || true" # страховка
|
||||
|
||||
echo "=== [6/6] Сверка ==="
|
||||
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 байт)"
|
||||
|
||||
# Самозавершение B: маркер печатается ТОЛЬКО когда COMPLETE ушёл в эфир и B
|
||||
# закрывается штатно. Нет маркера → B досидел до сторожевого kill = провал 3a.
|
||||
SELF_DONE=0
|
||||
"${SSH[@]}" "root@$B" "grep -q 'завершаю приём' /tmp/rx_arq.log 2>/dev/null" && SELF_DONE=1 || true
|
||||
|
||||
echo "--- Хендшейк ---"
|
||||
"${SSH[@]}" "root@$B" "grep ARQ /tmp/rx_arq.log 2>/dev/null | sed 's/^/B: /'" || true
|
||||
"${SSH[@]}" "root@$A" "grep ARQ /tmp/tx_arq.log 2>/dev/null | sed 's/^/A: /'" || true
|
||||
echo "B самозавершился: $([ "$SELF_DONE" = 1 ] && echo да || echo 'НЕТ (снят сторожем)')"
|
||||
|
||||
echo "--- Обратный канал STATUS (B→A) ---"
|
||||
SENT=$("${SSH[@]}" "root@$B" "grep -o 'отправлено STATUS: [0-9]*' /tmp/rx_arq.log 2>/dev/null | grep -o '[0-9]*'" || echo "")
|
||||
RECV=$("${SSH[@]}" "root@$A" "grep -o 'принято STATUS: [0-9]*' /tmp/tx_arq.log 2>/dev/null | grep -o '[0-9]*'" || echo "")
|
||||
SENT=${SENT:-0}; RECV=${RECV:-0}
|
||||
if [ "$SENT" -gt 0 ]; then
|
||||
echo "B отправил: $SENT | A принял: $RECV | доставка ≈ $(( RECV * 100 / SENT ))%"
|
||||
else
|
||||
echo "B отправил: $SENT | A принял: $RECV"
|
||||
fi
|
||||
|
||||
echo "--- RX-статистика (PER) ---"
|
||||
"${SSH[@]}" "root@$B" "grep '^Кадров:' /tmp/rx_arq.log 2>/dev/null | tail -1" || true
|
||||
# Ретрансмиты — главная метрика 3b: сырой PER остаётся, но md5 обязан сойтись.
|
||||
"${SSH[@]}" "root@$A" "grep 'ретрансмитов' /tmp/tx_arq.log 2>/dev/null | tail -1 | sed 's/^/A: /'" || true
|
||||
|
||||
echo "=== РЕЗУЛЬТАТ ==="
|
||||
if [ "$NEGATIVE" = 1 ]; then
|
||||
# Обратный канал заглушён: B файл соберёт и выйдет сам, но подтвердить A
|
||||
# некому. Проверяем именно честность отказа — раньше A рапортовал успех.
|
||||
if [ "$TX_RC" != 0 ]; then
|
||||
echo "OK (негатив) — A не получил COMPLETE и честно вышел с кодом $TX_RC"
|
||||
else
|
||||
echo "FAIL (негатив) — A вернул 0 без обратного канала: хендшейк фиктивен"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if [ "$MD5_SRC" = "$MD5_DST" ] && [ "$SELF_DONE" = 1 ] && [ "$TX_RC" = 0 ]; then
|
||||
echo "OK — md5 совпал, B завершился по COMPLETE, A подтвердил"
|
||||
else
|
||||
echo "FAIL: md5 $([ "$MD5_SRC" = "$MD5_DST" ] && echo ок || echo РАСХОЖДЕНИЕ) |" \
|
||||
"самозавершение $([ "$SELF_DONE" = 1 ] && echo ок || echo НЕТ) | код A $TX_RC"
|
||||
echo "Диагностика:"
|
||||
echo " - md5 разошёлся при коде A=0 → COMPLETE поднят рано (проверь miss/next_out)."
|
||||
echo " - код A=2 при целом md5 → COMPLETE не долетел: подними FBGAIN к −10."
|
||||
echo " - размер 0 → RX не декодит: остаточный процесс на B или самоглушение."
|
||||
echo " - CRC: 0 при живых 'Заголовках' → канал за клиффом (TXGAIN ≤ −38 на кабеле):"
|
||||
echo " заголовок BPSK ещё держится, payload QPSK — нет. Это не стресс ARQ, а обрыв."
|
||||
echo " - 'NACK вне окна' > 0 на B → дыра пережила TX-окно (2048 кадров ≈ 21 с)."
|
||||
echo "--- хвост rx_arq.log (B) ---"
|
||||
"${SSH[@]}" "root@$B" "tail -8 /tmp/rx_arq.log" || true
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -83,6 +83,23 @@ echo "--- FDD-ридбэк ---"
|
||||
"${SSH[@]}" "root@$A" "grep 'FDD ридбэк' /tmp/tx_fdd.log 2>/dev/null | sed 's/^/A: /'" || true
|
||||
"${SSH[@]}" "root@$B" "grep 'FDD ридбэк' /tmp/rx_fdd.log 2>/dev/null | sed 's/^/B: /'" || true
|
||||
|
||||
# Обратный канал (этап 2): сколько STATUS B отправил и сколько A декодировал.
|
||||
# Доля доставки — метрика gate самоглушения (§10 п.7).
|
||||
echo "--- Обратный канал STATUS (B→A) ---"
|
||||
SENT=$("${SSH[@]}" "root@$B" "grep -o 'отправлено STATUS: [0-9]*' /tmp/rx_fdd.log 2>/dev/null | grep -o '[0-9]*'" || echo "")
|
||||
RECV=$("${SSH[@]}" "root@$A" "grep -o 'принято STATUS: [0-9]*' /tmp/tx_fdd.log 2>/dev/null | grep -o '[0-9]*'" || echo "")
|
||||
SENT=${SENT:-0}; RECV=${RECV:-0}
|
||||
if [ "$SENT" -gt 0 ]; then
|
||||
echo "B отправил: $SENT | A принял: $RECV | доставка ≈ $(( RECV * 100 / SENT ))%"
|
||||
else
|
||||
echo "B отправил: $SENT | A принял: $RECV"
|
||||
fi
|
||||
|
||||
# Итоговая RX-статистика — источник PER (Потери seq / Байт), нужна каждый прогон,
|
||||
# особенно при GROUPFEC=0 (сырой PER без маскировки групповым FEC).
|
||||
echo "--- RX-статистика (PER) ---"
|
||||
"${SSH[@]}" "root@$B" "grep '^Кадров:' /tmp/rx_fdd.log 2>/dev/null | tail -1" || true
|
||||
|
||||
if [ "$MD5_SRC" = "$MD5_DST" ]; then
|
||||
echo "РЕЗУЛЬТАТ: OK — форвард-линк под FDD цел (FBGAIN=$FBGAIN)"
|
||||
else
|
||||
|
||||
@@ -46,9 +46,12 @@
|
||||
#define TX_URI_DEFAULT "local:"
|
||||
#define RX_URI_DEFAULT "local:"
|
||||
|
||||
// Формат заголовка кадра
|
||||
// Формат заголовка кадра. hdr[0..1] различает тип: данные/паритет GF — F0 AA,
|
||||
// END (маркер конца передачи, этап 3) — F0 E7, STATUS обратного канала — F0 55
|
||||
// (FB_SIG_* в feedback.h). Байт 0 общий у всех типов.
|
||||
#define HDR_SIGNATURE_0 0xF0
|
||||
#define HDR_SIGNATURE_1 0xAA
|
||||
#define HDR_SIG_END_1 0xE7
|
||||
#define HDR_SIZE 12
|
||||
|
||||
// Макросы логирования
|
||||
|
||||
70
src/feedback.c
Normal file
70
src/feedback.c
Normal file
@@ -0,0 +1,70 @@
|
||||
// feedback.c — упаковка/распаковка STATUS обратного канала (этап 2, §10 п.7).
|
||||
// Чистая логика без зависимостей от liquid/iio → пригодна к офлайн-юнит-тесту.
|
||||
|
||||
#include "feedback.h"
|
||||
#include <string.h>
|
||||
|
||||
static void put_u32(uint8_t *p, uint32_t v) {
|
||||
p[0] = (v >> 24) & 0xFF; p[1] = (v >> 16) & 0xFF;
|
||||
p[2] = (v >> 8) & 0xFF; p[3] = v & 0xFF;
|
||||
}
|
||||
|
||||
static uint32_t get_u32(const uint8_t *p) {
|
||||
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
|
||||
((uint32_t)p[2] << 8) | (uint32_t)p[3];
|
||||
}
|
||||
|
||||
static void put_u64(uint8_t *p, uint64_t v) {
|
||||
put_u32(p, (uint32_t)(v >> 32));
|
||||
put_u32(p + 4, (uint32_t)(v & 0xFFFFFFFFu));
|
||||
}
|
||||
|
||||
static uint64_t get_u64(const uint8_t *p) {
|
||||
return ((uint64_t)get_u32(p) << 32) | (uint64_t)get_u32(p + 4);
|
||||
}
|
||||
|
||||
size_t fb_status_pack(const fb_status_t *s, uint8_t *out) {
|
||||
uint8_t cnt = s->nack_cnt > FB_MAX_NACK ? FB_MAX_NACK : s->nack_cnt;
|
||||
out[0] = FB_TYPE_STATUS;
|
||||
out[1] = s->flags;
|
||||
out[2] = cnt;
|
||||
out[3] = 0; // резерв
|
||||
put_u32(out + 4, s->highest_seq);
|
||||
put_u32(out + 8, s->rx_frames);
|
||||
put_u32(out + 12, s->overruns);
|
||||
for (uint8_t i = 0; i < cnt; i++)
|
||||
put_u32(out + FB_STATUS_HDR + (size_t)i * 4, s->nack[i]);
|
||||
return (size_t)FB_STATUS_HDR + (size_t)cnt * 4;
|
||||
}
|
||||
|
||||
int fb_status_unpack(const uint8_t *in, size_t len, fb_status_t *s) {
|
||||
if (len < FB_STATUS_HDR) return -1;
|
||||
if (in[0] != FB_TYPE_STATUS) return -1;
|
||||
uint8_t cnt = in[2];
|
||||
if (cnt > FB_MAX_NACK) return -1;
|
||||
if (len < (size_t)FB_STATUS_HDR + (size_t)cnt * 4) return -1;
|
||||
|
||||
memset(s, 0, sizeof(*s));
|
||||
s->type = in[0];
|
||||
s->flags = in[1];
|
||||
s->nack_cnt = cnt;
|
||||
s->highest_seq = get_u32(in + 4);
|
||||
s->rx_frames = get_u32(in + 8);
|
||||
s->overruns = get_u32(in + 12);
|
||||
for (uint8_t i = 0; i < cnt; i++)
|
||||
s->nack[i] = get_u32(in + FB_STATUS_HDR + (size_t)i * 4);
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t fb_end_pack(const fb_end_t *e, uint8_t *out) {
|
||||
put_u32(out, e->total_seq);
|
||||
put_u64(out + 4, e->total_bytes);
|
||||
return FB_END_LEN;
|
||||
}
|
||||
|
||||
int fb_end_unpack(const uint8_t *in, size_t len, fb_end_t *e) {
|
||||
if (len < FB_END_LEN) return -1;
|
||||
e->total_seq = get_u32(in);
|
||||
e->total_bytes = get_u64(in + 4);
|
||||
return 0;
|
||||
}
|
||||
63
src/feedback.h
Normal file
63
src/feedback.h
Normal file
@@ -0,0 +1,63 @@
|
||||
// feedback.h — обратный канал STATUS (roadmap §10 п.7, этап 2).
|
||||
// STATUS — кумулятивный СНИМОК состояния RX, а не поток событий: потеря одного
|
||||
// STATUS безвредна, следующий несёт то же состояние (ARQ-над-ARQ не нужен).
|
||||
// Идёт B→A на частоте обратного канала (868); сигнатура кадра F0 55 в hdr[0..1]
|
||||
// (данные — F0 AA, END — F0 E7). Payload RS(255,223)+CRC32, как у данных.
|
||||
|
||||
#ifndef FEEDBACK_H
|
||||
#define FEEDBACK_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define FB_TYPE_STATUS 0x01
|
||||
#define FB_MAX_NACK 50 // потолок NACK в одном STATUS (влезает в 1 RS-блок)
|
||||
#define FB_FLAG_END_SEEN 0x01 // RX видел END-кадр (этап 3)
|
||||
#define FB_FLAG_COMPLETE 0x02 // все кадры собраны, файл цел (этап 3)
|
||||
|
||||
// Сигнатура STATUS-кадра в hdr[0..1].
|
||||
#define FB_SIG_0 0xF0
|
||||
#define FB_SIG_1 0x55
|
||||
|
||||
// Снимок состояния приёмника. nack[] — отсортированные по возрастанию seq
|
||||
// отсутствующих кадров данных (старейшие первыми), не более FB_MAX_NACK.
|
||||
typedef struct {
|
||||
uint8_t type; // FB_TYPE_STATUS
|
||||
uint8_t flags; // FB_FLAG_END_SEEN | FB_FLAG_COMPLETE
|
||||
uint8_t nack_cnt; // число валидных элементов nack[] (≤ FB_MAX_NACK)
|
||||
uint32_t highest_seq; // макс. принятый seq (0xFFFFFFFF = ничего)
|
||||
uint32_t rx_frames; // кадров данных записано в stdout
|
||||
uint32_t overruns; // Overrun ядра захвата (диагностика линии)
|
||||
uint32_t nack[FB_MAX_NACK];
|
||||
} fb_status_t;
|
||||
|
||||
// Раскладка payload (сетевой порядок, BE):
|
||||
// 0 type | 1 flags | 2 nack_cnt | 3 rsv | 4..7 highest | 8..11 rx_frames
|
||||
// 12..15 overruns | 16.. nack[i] u32
|
||||
#define FB_STATUS_HDR 16 // фикс. часть
|
||||
#define FB_STATUS_MAXLEN (FB_STATUS_HDR + FB_MAX_NACK * 4) // 216 ≤ 219 → 1 RS-блок
|
||||
|
||||
// Упаковка → out (≥ FB_STATUS_MAXLEN). Возврат — длина payload в байтах.
|
||||
size_t fb_status_pack(const fb_status_t *s, uint8_t *out);
|
||||
// Распаковка. Возврат 0 при успехе, -1 при кривой длине/типе.
|
||||
int fb_status_unpack(const uint8_t *in, size_t len, fb_status_t *s);
|
||||
|
||||
// --- END-кадр A→B (этап 3, сигнатура F0 E7 в hdr[0..1]) -----------------
|
||||
// Хвостовые потери молчаливы: у последних кадров нет следующего seq, по которому
|
||||
// видна дыра. END несёт итог передачи — RX добивает недостающие seq в NACK и
|
||||
// понимает, когда файл собран целиком (COMPLETE). Идёт тем же путём RS+CRC32,
|
||||
// что и данные (1 RS-блок). Тип не кодируется в payload — его задаёт сигнатура.
|
||||
typedef struct {
|
||||
uint32_t total_seq; // отправлено кадров: валидные seq = 0..total_seq-1
|
||||
uint64_t total_bytes; // исходных байт всего (сверка длины файла)
|
||||
} fb_end_t;
|
||||
|
||||
// Раскладка payload (BE): 0..3 total_seq | 4..11 total_bytes
|
||||
#define FB_END_LEN 12
|
||||
|
||||
// Упаковка → out (≥ FB_END_LEN). Возврат — длина payload.
|
||||
size_t fb_end_pack(const fb_end_t *e, uint8_t *out);
|
||||
// Распаковка. Возврат 0 при успехе, -1 при кривой длине.
|
||||
int fb_end_unpack(const uint8_t *in, size_t len, fb_end_t *e);
|
||||
|
||||
#endif
|
||||
@@ -68,3 +68,33 @@ void phy_tx_frame(ofdmflexframegen fg, struct iio_buffer *buf, int buf_samples,
|
||||
if (idx > 0) iio_buffer_push_partial(buf, idx);
|
||||
if (pause_us > 0) usleep(pause_us);
|
||||
}
|
||||
|
||||
int phy_rx_decode(const uint8_t *hdr, const uint8_t *pay, unsigned pay_len,
|
||||
fec dec, uint8_t *out, size_t out_cap, size_t *out_len) {
|
||||
if (!dec || pay_len < RS_ENC) return 0;
|
||||
size_t nblocks = pay_len / RS_ENC;
|
||||
if (nblocks == 0 || nblocks * RS_DATA > out_cap) return 0;
|
||||
|
||||
for (size_t b = 0; b < nblocks; b++)
|
||||
// fec_decode() liquid не const-correct и «всегда успешен» — код возврата
|
||||
// бесполезен, целостность проверяем CRC ниже (техдолг #3).
|
||||
fec_decode(dec, RS_DATA, (unsigned char *)pay + b * RS_ENC,
|
||||
out + b * RS_DATA);
|
||||
|
||||
uint16_t original_len = (uint16_t)((hdr[6] << 8) | hdr[7]);
|
||||
uint8_t last_block_bytes = hdr[10];
|
||||
size_t framed_len = (nblocks - 1) * RS_DATA +
|
||||
(last_block_bytes > 0 ? last_block_bytes : RS_DATA);
|
||||
if (framed_len > nblocks * RS_DATA) framed_len = nblocks * RS_DATA;
|
||||
if (original_len == 0 || (size_t)original_len + 4 > framed_len) return 0;
|
||||
|
||||
unsigned int rx_crc = crc_generate_key(LIQUID_CRC_32, out, original_len);
|
||||
unsigned int tx_crc = ((uint32_t)out[original_len] << 24) |
|
||||
((uint32_t)out[original_len + 1] << 16) |
|
||||
((uint32_t)out[original_len + 2] << 8) |
|
||||
(uint32_t)out[original_len + 3];
|
||||
if (rx_crc != tx_crc) return 0;
|
||||
|
||||
*out_len = original_len;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -17,4 +17,14 @@ void phy_tx_frame(ofdmflexframegen fg, struct iio_buffer *buf, int buf_samples,
|
||||
fec enc, const uint8_t sig[2], uint32_t seq, uint8_t hdr11,
|
||||
const uint8_t *pl, size_t n, float amp, int pause_us);
|
||||
|
||||
// Обратная операция к phy_tx_frame для кадра, собранного с enc: RS-декод всех
|
||||
// блоков payload + проверка CRC32-поверх-RS (единственный надёжный критерий,
|
||||
// техдолг #3). hdr — 12 Б заголовка (нужны байты 6-7 original_len и 10
|
||||
// last_block_bytes); pay/pay_len — сырой RS-payload из ofdmflexframesync; dec —
|
||||
// декодер RS(255,223); out (≥ out_cap) принимает декодированные данные, out_len
|
||||
// — исходная длина. Возврат 1 при пройденном CRC, 0 иначе. Используется обратным
|
||||
// каналом (fb_rx) на TX-стороне; RX-данные пока декодит свой инлайн в receiver.c.
|
||||
int phy_rx_decode(const uint8_t *hdr, const uint8_t *pay, unsigned pay_len,
|
||||
fec dec, uint8_t *out, size_t out_cap, size_t *out_len);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -165,6 +165,35 @@ static void bench_rs(int nerr, double frame_us)
|
||||
fec_destroy(enc); fec_destroy(dec);
|
||||
}
|
||||
|
||||
/* Стоимость генерации короткого кадра обратного канала (STATUS: RS-payload ≈ 1
|
||||
* блок = 255 Б). Этап 2 §10 п.7: модуляция фидбэка идёт на ядре 1 приёмника
|
||||
* (поток C) — убедиться, что assemble+write дёшевы и 10 Гц STATUS не грузят ядро.
|
||||
* Меряем полную генерацию кадра (assemble + write всех символов). */
|
||||
static void bench_framegen(ofdmflexframegen fg, unsigned pay_bytes)
|
||||
{
|
||||
const unsigned sym = OFDM_M + OFDM_CP;
|
||||
unsigned char hdr[12] = {0xF0, 0x55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
unsigned char pay[512];
|
||||
if (pay_bytes > sizeof(pay)) pay_bytes = sizeof(pay);
|
||||
for (unsigned i = 0; i < pay_bytes; i++) pay[i] = (unsigned char)(i & 0xFF);
|
||||
float complex buf[OFDM_M + OFDM_CP];
|
||||
|
||||
unsigned long long frames = 0;
|
||||
double t0 = tnow(), t1;
|
||||
do {
|
||||
ofdmflexframegen_assemble(fg, hdr, pay, pay_bytes);
|
||||
int done = 0;
|
||||
while (!done) done = ofdmflexframegen_write(fg, buf, sym);
|
||||
frames++;
|
||||
t1 = tnow();
|
||||
} while (t1 - t0 < 2.0);
|
||||
|
||||
double us = (t1 - t0) * 1e6 / (double)frames;
|
||||
/* STATUS 10 Гц → доля ядра = 10·us/1e6, в процентах = us·1e-3 */
|
||||
printf("framegen кадр %uБ %7.1f мкс/кадр %8.0f кадров/с (STATUS 10 Гц ≈ %.3f%% ядра)\n",
|
||||
pay_bytes, us, (double)frames / (t1 - t0), us * 10.0 / 1e6 * 100.0);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
int main_cpu = 0, use_hog = 0, opt;
|
||||
@@ -271,6 +300,10 @@ int main(int argc, char **argv)
|
||||
bench_rs(4, frame_us);
|
||||
bench_rs(16, frame_us);
|
||||
|
||||
/* обратный канал: генерация короткого STATUS-кадра (§10 п.7, этап 2) */
|
||||
printf("\n");
|
||||
bench_framegen(fg, 255); /* RS-payload STATUS ≈ 1 блок */
|
||||
|
||||
/* p_min: минимальная TX-пауза для realtime @ 1.92 при чистой sync-only цепочке
|
||||
* (без RS, без контеншна если без -H). d/S + (1-d)/N = 1/1.92; d = frm/(frm+p) */
|
||||
printf("\nМодель realtime @ %.2f Msps (sync-only%s):\n",
|
||||
|
||||
452
src/receiver.c
452
src/receiver.c
@@ -9,6 +9,8 @@
|
||||
|
||||
#include "common.h" // _GNU_SOURCE/pthread/sched — уже подключены здесь
|
||||
#include "group_fec.h"
|
||||
#include "frame_tx.h" // phy_tx_frame — отправка STATUS обратного канала
|
||||
#include "feedback.h" // fb_status_t / pack — телеметрия B→A (этап 2, §10 п.7)
|
||||
|
||||
#define BUF_SAMPLES 32768
|
||||
#define RING_SLOTS 16 // 16 × 17 мс = 273 мс поглощения всплесков
|
||||
@@ -73,6 +75,9 @@ static uint64_t grp_recovered = 0, grp_failed = 0; // group-FEC: восст.
|
||||
static uint64_t sig_bad = 0; // hdr[0..1] не совпал с известной сигнатурой
|
||||
static uint32_t highest_seq = 0; // максимальный увиденный seq (не откатывается назад)
|
||||
static int seq_init = 0;
|
||||
static uint32_t next_out = 0; // курсор вывода: следующий seq в stdout (этап 3b)
|
||||
static uint64_t rb_over = 0; // кадров отброшено переполнением окна reorder
|
||||
static uint64_t miss_rebuilt = 0; // пересборок NACK из кольца (этап 3c)
|
||||
// Разделяемых на запись счётчиков нет. main читает их раз в секунду для
|
||||
// телеметрии без синхронизации: на 32-бит ARM возможен косметический «рваный»
|
||||
// показ 64-бит значения — на итоговую строку (печатается после join) не влияет.
|
||||
@@ -80,6 +85,136 @@ static int seq_init = 0;
|
||||
static fec decoder = NULL;
|
||||
static int fec_on = 0;
|
||||
|
||||
// --- Missing-tracker обратного канала (этап 2, §10 п.7) -----------------
|
||||
// Отсортированный по возрастанию набор seq отсутствующих кадров данных. Пишется
|
||||
// ТОЛЬКО потоком C (в process_frame и maybe_send_status) — синхронизации нет,
|
||||
// как у прочего C-состояния. Дыры seq → miss_add (по возрастанию), приход кадра
|
||||
// (в т.ч. будущий ретрансмит) → miss_ack. В STATUS уходят FB_MAX_NACK старейших.
|
||||
#define MISS_CAP 512
|
||||
static uint32_t miss[MISS_CAP];
|
||||
static int miss_cnt = 0;
|
||||
|
||||
static uint64_t miss_over = 0; // дыр не влезло в MISS_CAP
|
||||
|
||||
static void miss_add(uint32_t seq) {
|
||||
if (miss_cnt >= MISS_CAP) { miss_over++; return; } // старейшие дыры важнее
|
||||
miss[miss_cnt++] = seq; // highest_seq только растёт → массив сортирован
|
||||
}
|
||||
|
||||
// Вставка с сохранением порядка и дедупом — для seq, который МЕНЬШЕ фронта:
|
||||
// битый кадр (rs_fail) пришёл, дырой в seq не выглядит, но дослать его надо.
|
||||
// miss_add для такого seq сломал бы сортировку (в STATUS уходят FB_MAX_NACK
|
||||
// первых элементов — они обязаны быть старейшими).
|
||||
static void miss_add_sorted(uint32_t seq) {
|
||||
int i;
|
||||
for (i = 0; i < miss_cnt; i++) {
|
||||
if (miss[i] == seq) return; // уже в списке
|
||||
if (miss[i] > seq) break; // точка вставки
|
||||
}
|
||||
if (miss_cnt >= MISS_CAP) { miss_over++; return; }
|
||||
memmove(&miss[i + 1], &miss[i], (size_t)(miss_cnt - i) * sizeof(miss[0]));
|
||||
miss[i] = seq;
|
||||
miss_cnt++;
|
||||
}
|
||||
|
||||
static void miss_ack(uint32_t seq) {
|
||||
for (int i = 0; i < miss_cnt; i++) {
|
||||
if (miss[i] == seq) {
|
||||
memmove(&miss[i], &miss[i + 1], (size_t)(miss_cnt - i - 1) * sizeof(miss[0]));
|
||||
miss_cnt--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- EOF-хендшейк (этап 3, поток C) -------------------------------------
|
||||
// END (F0 E7) несёт итог передачи: сколько кадров и байт ушло в эфир. Без него
|
||||
// хвостовые потери молчаливы — у последних кадров нет следующего seq, по которому
|
||||
// детектор видит дыру. Приняв END, RX добивает недостающие seq в NACK, а собрав
|
||||
// всё — поднимает FB_FLAG_COMPLETE в STATUS и завершается сам.
|
||||
#define ARQ_COMPLETE_HOLD_MS 600 // держать COMPLETE ≥6 периодов STATUS: в
|
||||
// тихом эфире END-фазы доставка ~100%, но
|
||||
// повтор дешевле, чем таймаут A на 10 с
|
||||
|
||||
static int end_seen = 0;
|
||||
static uint32_t end_total_seq = 0;
|
||||
static uint64_t end_total_bytes = 0;
|
||||
static uint64_t end_rx = 0; // принято валидных END (диагностика)
|
||||
static int64_t complete_since_ms = 0; // 0 = файл ещё не собран
|
||||
|
||||
// --- Отправитель STATUS на feedback-TX (только при -F, поток C) ----------
|
||||
// Свой framegen + буфер на txfb (868), свой RS-энкодер. Кадр STATUS шлётся не
|
||||
// чаще FB_PERIOD_MS. Данные RX и обратный TX — независимые cf-девайсы (FDD),
|
||||
// поэтому push сюда не конфликтует с приёмом.
|
||||
#define FB_BUF_SAMPLES 16384
|
||||
#define FB_PERIOD_MS 100
|
||||
#define FB_AMP 0.2f
|
||||
|
||||
static ofdmflexframegen g_fb_fg = NULL;
|
||||
static struct iio_buffer *g_fb_buf = NULL;
|
||||
static fec g_fb_enc = NULL;
|
||||
static uint32_t g_fb_seq = 0;
|
||||
static int64_t g_fb_last_ms = 0;
|
||||
|
||||
static const uint8_t SIG_STATUS[2] = { FB_SIG_0, FB_SIG_1 };
|
||||
|
||||
static int64_t now_ms(void) {
|
||||
struct timespec t;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t);
|
||||
return (int64_t)t.tv_sec * 1000 + t.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
// Файл собран? Критерий один для обоих режимов (этап 3c): курсор вывода дошёл до
|
||||
// конца. Это сильнее, чем «дыр нет»: next_out двигается только по непрерывному
|
||||
// префиксу реально записанных кадров. Паритетные кадры group-FEC занимают слот
|
||||
// нулевой длины — они тратят seq, но в файл не идут, и курсор их проходит.
|
||||
// Держим флаг ARQ_COMPLETE_HOLD_MS, чтобы A успел услышать хотя бы один STATUS,
|
||||
// затем завершаемся штатным путём через stop (main → join A→B→C).
|
||||
static void arq_check_complete(void) {
|
||||
if (!g_fb_fg || !end_seen) return; // симплекс либо передача ещё идёт
|
||||
int done = (end_total_seq == 0) ||
|
||||
(miss_cnt == 0 && next_out == end_total_seq);
|
||||
if (!done) { complete_since_ms = 0; return; }
|
||||
|
||||
int64_t t = now_ms();
|
||||
if (!complete_since_ms) {
|
||||
complete_since_ms = t; // с этого момента STATUS несёт COMPLETE
|
||||
fprintf(stderr, "\n[ARQ] COMPLETE: %u кадров, %llu байт — держу флаг %d мс\n",
|
||||
end_total_seq, (unsigned long long)end_total_bytes,
|
||||
ARQ_COMPLETE_HOLD_MS);
|
||||
}
|
||||
if (t - complete_since_ms >= ARQ_COMPLETE_HOLD_MS) {
|
||||
fprintf(stderr, "[ARQ] COMPLETE отправлен — завершаю приём\n");
|
||||
stop = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void maybe_send_status(void) {
|
||||
if (!g_fb_fg) return; // симплекс — обратного канала нет
|
||||
arq_check_complete(); // до гейта периода: решение о завершении
|
||||
// не должно ждать своего слота STATUS
|
||||
int64_t t = now_ms();
|
||||
if (g_fb_last_ms && t - g_fb_last_ms < FB_PERIOD_MS) return;
|
||||
g_fb_last_ms = t;
|
||||
|
||||
fb_status_t st;
|
||||
memset(&st, 0, sizeof(st));
|
||||
st.type = FB_TYPE_STATUS;
|
||||
st.flags = (end_seen ? FB_FLAG_END_SEEN : 0) |
|
||||
(complete_since_ms ? FB_FLAG_COMPLETE : 0);
|
||||
st.highest_seq = seq_init ? highest_seq : 0xFFFFFFFFu;
|
||||
st.rx_frames = (uint32_t)crc_final;
|
||||
st.overruns = (uint32_t)ring.overruns; // телеметрия, гонка косметическая
|
||||
int cnt = miss_cnt < FB_MAX_NACK ? miss_cnt : FB_MAX_NACK;
|
||||
st.nack_cnt = (uint8_t)cnt;
|
||||
for (int i = 0; i < cnt; i++) st.nack[i] = miss[i];
|
||||
|
||||
uint8_t pay[FB_STATUS_MAXLEN];
|
||||
size_t n = fb_status_pack(&st, pay);
|
||||
phy_tx_frame(g_fb_fg, g_fb_buf, FB_BUF_SAMPLES, g_fb_enc,
|
||||
SIG_STATUS, g_fb_seq++, 0, pay, n, FB_AMP, 0);
|
||||
}
|
||||
|
||||
// callback потока B — лёгкий: телеметрия + копия сырого кадра в очередь C.
|
||||
static int callback(unsigned char *hdr, int hdr_valid,
|
||||
unsigned char *pay, unsigned int pay_len,
|
||||
@@ -122,12 +257,119 @@ static int callback(unsigned char *hdr, int hdr_valid,
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- Кольцо переупорядочивания вывода (поток C, этап 3b) ----------------
|
||||
// До ARQ кадры приходили строго по возрастанию seq и писались в stdout сразу.
|
||||
// Ретрансмит ломает это: кадр 5 приедет после кадра 40 — прямая запись сдвинула
|
||||
// бы данные и убила md5. Кольцо по seq % RB_CAP + курсор next_out: в stdout
|
||||
// уходит только непрерывный префикс. Дыра держит вывод, пока ARQ её не закроет.
|
||||
// len == 0 — «слот занят, но в файл ничего»: паритетный кадр GF тратит seq, но
|
||||
// данных не несёт (нужно для этапа 3c, чтобы next_out доходил до total_seq).
|
||||
#define RB_CAP 1024 // ~4 с эфира при 9.3 мс/кадр; TXWIN у A вдвое
|
||||
// больше — досылка для любого seq в окне есть
|
||||
typedef struct {
|
||||
uint32_t seq;
|
||||
uint16_t len;
|
||||
uint8_t filled;
|
||||
uint8_t data[GF_CHUNK];
|
||||
} rbslot_t;
|
||||
|
||||
static rbslot_t rb[RB_CAP]; // ~1 МБ .bss (next_out/rb_over — выше, к ним
|
||||
// обращается arq_check_complete до этой секции)
|
||||
|
||||
// Выдать в stdout непрерывный префикс, начиная с next_out.
|
||||
static void rb_drain(void) {
|
||||
int wrote = 0;
|
||||
for (;;) {
|
||||
rbslot_t *s = &rb[next_out % RB_CAP];
|
||||
if (!s->filled || s->seq != next_out) break; // дыра — дальше нельзя
|
||||
if (s->len) {
|
||||
fwrite(s->data, 1, s->len, stdout);
|
||||
total_bytes += s->len;
|
||||
crc_final++;
|
||||
}
|
||||
s->filled = 0;
|
||||
next_out++;
|
||||
wrote = 1;
|
||||
}
|
||||
if (wrote) fflush(stdout);
|
||||
}
|
||||
|
||||
// Принудительно продвинуть курсор до seq `to`, схлопывая дыры: то, что есть —
|
||||
// в stdout, чего нет — пропало навсегда (прежняя семантика «потеря = сдвиг»).
|
||||
static void rb_force_to(uint32_t to) {
|
||||
while (next_out < to) {
|
||||
rbslot_t *s = &rb[next_out % RB_CAP];
|
||||
if (s->filled && s->seq == next_out && s->len) {
|
||||
fwrite(s->data, 1, s->len, stdout);
|
||||
total_bytes += s->len;
|
||||
crc_final++;
|
||||
}
|
||||
s->filled = 0;
|
||||
next_out++;
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
// Вызывается ТОЛЬКО на FDD-пути (симплекс пишет в stdout напрямую — там нечего
|
||||
// переупорядочивать), поэтому досылка по NACK всегда доступна.
|
||||
static void rb_insert(uint32_t seq, const uint8_t *d, size_t len) {
|
||||
if (len > GF_CHUNK) len = GF_CHUNK; // защита в глубину: len берётся из
|
||||
// hdr, слот кольца — ровно GF_CHUNK
|
||||
if (seq < next_out) return; // уже выведен: дубль ретрансмита
|
||||
if (seq - next_out >= RB_CAP) {
|
||||
// Дыра в голове старше окна: дропаем новый кадр и возвращаем его seq в
|
||||
// NACK — A дошлёт, когда голова разъедется. Схлопнуть окно нельзя: это
|
||||
// молча потеряло бы кадр, который ARQ ещё способен закрыть.
|
||||
rb_over++;
|
||||
miss_add_sorted(seq);
|
||||
return;
|
||||
}
|
||||
rbslot_t *s = &rb[seq % RB_CAP];
|
||||
s->seq = seq;
|
||||
s->len = (uint16_t)len;
|
||||
s->filled = 1;
|
||||
if (len) memcpy(s->data, d, len);
|
||||
}
|
||||
|
||||
// Досдать всё, что осталось в кольце (поток C на выходе): приём кончился, ARQ
|
||||
// уже не поможет — дыры схлопываются, хвост уходит в файл.
|
||||
static void rb_flush_all(void) {
|
||||
uint32_t last = next_out;
|
||||
for (int i = 0; i < RB_CAP; i++)
|
||||
if (rb[i].filled && rb[i].seq >= last) last = rb[i].seq + 1;
|
||||
rb_force_to(last);
|
||||
}
|
||||
|
||||
// Пересобрать NACK-набор из кольца (этап 3c, только FDD, только по END).
|
||||
// Зачем: miss_add при переполнении MISS_CAP молча теряет seq, и вернуть его туда
|
||||
// уже некому — детектор дыр по нему не сработает второй раз (highest_seq ушёл
|
||||
// вперёд). В тяжёлом канале это тупик: A не знает, что дослать, next_out стоит,
|
||||
// COMPLETE не наступает, B досиживает до сторожа. Кольцо же знает точно, каких
|
||||
// кадров нет: слот либо занят своим seq, либо это дыра.
|
||||
// Скан ограничен окном кольца: seq дальше next_out+RB_CAP всё равно принять
|
||||
// некуда (rb_insert их отбросит), просить их у A рано.
|
||||
static void miss_rebuild(void) {
|
||||
if (!g_fb_fg || !end_seen) return;
|
||||
if (end_total_seq <= next_out) { miss_cnt = 0; return; } // всё выведено; заодно
|
||||
// страховка от переполнения вычитания ниже
|
||||
uint32_t to = end_total_seq;
|
||||
if (to - next_out > RB_CAP) to = next_out + RB_CAP;
|
||||
miss_cnt = 0;
|
||||
for (uint32_t s = next_out; s < to && miss_cnt < MISS_CAP; s++) {
|
||||
rbslot_t *sl = &rb[s % RB_CAP];
|
||||
if (sl->filled && sl->seq == s) continue; // кадр уже принят
|
||||
miss[miss_cnt++] = s; // возрастает → сортировка цела
|
||||
}
|
||||
miss_rebuilt++;
|
||||
}
|
||||
|
||||
// --- Групповой стирающий FEC (поток C, README §12.7) --------------------
|
||||
// Собирает группу из 16 кадров данных + 2 паритетных (P,Q). Пропавший на эфире
|
||||
// или битый по CRC кадр = стирание; ≤2 стираний на группу закрываются gf_recover.
|
||||
// Всё состояние — только поток C (как остальные C-счётчики), синхронизации нет.
|
||||
static struct {
|
||||
int active;
|
||||
int seen; // хоть одна группа была: g.base осмыслен (этап 3c)
|
||||
uint32_t base; // seq первого кадра группы (= seq − idx)
|
||||
int k; // число кадров данных (из меты паритета)
|
||||
int k_known; // k получен из паритета
|
||||
@@ -135,6 +377,7 @@ static struct {
|
||||
uint8_t data[GF_K][GF_CHUNK];
|
||||
uint16_t dlen[GF_K]; // фактическая длина каждого кадра данных
|
||||
uint8_t have[GF_K]; // 1 = кадр idx получен и валиден
|
||||
uint8_t fixed[GF_K]; // 1 = кадр восстановлен паритетом, а не принят
|
||||
uint8_t P[GF_CHUNK], Q[GF_CHUNK];
|
||||
int haveP, haveQ;
|
||||
uint16_t tail_len; // длина последнего кадра данных (из меты)
|
||||
@@ -142,8 +385,9 @@ static struct {
|
||||
|
||||
static void group_reset(uint32_t base) {
|
||||
memset(g.have, 0, sizeof(g.have));
|
||||
memset(g.fixed, 0, sizeof(g.fixed));
|
||||
memset(g.dlen, 0, sizeof(g.dlen));
|
||||
g.active = 1; g.base = base; g.k = 0; g.k_known = 0;
|
||||
g.active = 1; g.seen = 1; g.base = base; g.k = 0; g.k_known = 0;
|
||||
g.maxidx = -1; g.haveP = g.haveQ = 0; g.tail_len = 0;
|
||||
}
|
||||
|
||||
@@ -163,20 +407,38 @@ static void group_flush(void) {
|
||||
// прочие кадры полные 1024, последний — tail_len
|
||||
g.dlen[i] = (i == k - 1 && g.tail_len) ? g.tail_len : GF_CHUNK;
|
||||
g.have[i] = 1;
|
||||
g.fixed[i] = 1; // закрыт паритетом — досылать по ARQ не нужно
|
||||
}
|
||||
} else {
|
||||
grp_failed++; // >2 стираний либо не хватило паритета
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < k; i++) {
|
||||
if (!g.have[i]) continue; // невосстановимая дыра
|
||||
uint16_t len = g.dlen[i] > GF_CHUNK ? GF_CHUNK : g.dlen[i];
|
||||
fwrite(g.data[i], 1, len, stdout);
|
||||
total_bytes += len;
|
||||
crc_final++;
|
||||
if (g_fb_fg) {
|
||||
// FDD (гибрид GF+ARQ, этап 3c): порядок держит кольцо, и принятые кадры
|
||||
// уже легли в него на приёме (group_route). Отсюда идут ТОЛЬКО кадры,
|
||||
// поднятые паритетом, — и их seq снимаются с NACK: GF закрыл дыру,
|
||||
// ретрансмит был бы лишним трафиком. Невосстановимые остаются в miss —
|
||||
// их дошлёт ARQ, для того он здесь и включён.
|
||||
for (int i = 0; i < k; i++) {
|
||||
if (!g.fixed[i]) continue;
|
||||
rb_insert(g.base + i, g.data[i], g.dlen[i]);
|
||||
miss_ack(g.base + i);
|
||||
}
|
||||
rb_drain();
|
||||
} else {
|
||||
// Симплекс: кольца нет (ретрансмитов не будет — переупорядочивать нечего),
|
||||
// группа сама себе буфер. Пишем её целиком в порядке idx, восстановленные
|
||||
// встают на свои места. Невосстановимая дыра = сдвиг, прежняя семантика.
|
||||
for (int i = 0; i < k; i++) {
|
||||
if (!g.have[i]) continue;
|
||||
uint16_t len = g.dlen[i] > GF_CHUNK ? GF_CHUNK : g.dlen[i];
|
||||
fwrite(g.data[i], 1, len, stdout);
|
||||
total_bytes += len;
|
||||
crc_final++;
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
fflush(stdout);
|
||||
g.active = 0;
|
||||
}
|
||||
|
||||
@@ -187,6 +449,24 @@ static void group_route(uint8_t flags, uint32_t seq,
|
||||
int is_par = flags & GF_FLAG_PAR;
|
||||
uint32_t base = seq - idx; // idx=k для P, k+1 для Q → та же база
|
||||
|
||||
// FDD: кольцо — единственный источник порядка, и оно живёт независимо от
|
||||
// сборки групп. Кладём кадр в слот ДО всякого bypass: даже ретрансмит давно
|
||||
// закрытой группы обязан занять своё место, иначе next_out встанет на нём
|
||||
// навсегда и COMPLETE не наступит. Паритет тратит seq, но данных не несёт —
|
||||
// ему слот нулевой длины, курсор его проходит насквозь.
|
||||
if (ok && g_fb_fg) {
|
||||
rb_insert(seq, is_par ? NULL : dec, is_par ? 0 : dec_len);
|
||||
rb_drain();
|
||||
}
|
||||
|
||||
// Ретрансмит кадра уже закрытой группы (этап 3c). Пересобирать нечего: в FDD
|
||||
// кадр только что лёг в кольцо, а группа нужна лишь ради паритета. Ловим два
|
||||
// случая: старая группа (base < g.base) — иначе она преждевременно flush-нула
|
||||
// бы активную и сбросила её стирания; и текущая, уже финализированная по END
|
||||
// (base == g.base && !g.active) — иначе кадр «воскресил» бы группу с одним
|
||||
// элементом, наврав в grp_failed.
|
||||
if (g.seen && (base < g.base || (base == g.base && !g.active))) return;
|
||||
|
||||
if (!g.active || base != g.base) { // новая группа → финализировать прежнюю
|
||||
group_flush();
|
||||
group_reset(base);
|
||||
@@ -211,6 +491,62 @@ static void group_route(uint8_t flags, uint32_t seq,
|
||||
}
|
||||
}
|
||||
|
||||
// Разбор END-кадра (поток C, этап 3). Зафиксировать итог передачи и добить
|
||||
// хвостовые дыры в NACK. Повторы END (A шлёт их каждые 100 мс до COMPLETE)
|
||||
// идемпотентны: состояние берём с первого валидного.
|
||||
static void process_end(const uint8_t *hdr, uint8_t *pay, unsigned pay_len,
|
||||
int pay_valid) {
|
||||
static uint8_t edec[RS_ENC]; // END = 12 Б → ровно 1 RS-блок (223 Б)
|
||||
fb_end_t e;
|
||||
|
||||
if (fec_on && decoder) {
|
||||
size_t elen = 0;
|
||||
if (!phy_rx_decode(hdr, pay, pay_len, decoder, edec, sizeof(edec), &elen))
|
||||
return; // RS/CRC не прошли — ждём следующий END
|
||||
if (fb_end_unpack(edec, elen, &e) != 0) return;
|
||||
} else { // -c none: payload как есть
|
||||
uint16_t olen = (uint16_t)((hdr[6] << 8) | hdr[7]);
|
||||
if (!pay_valid || olen < FB_END_LEN || olen > pay_len) return;
|
||||
if (fb_end_unpack(pay, olen, &e) != 0) return;
|
||||
}
|
||||
|
||||
end_rx++;
|
||||
if (!end_seen) {
|
||||
end_seen = 1;
|
||||
end_total_seq = e.total_seq;
|
||||
end_total_bytes = e.total_bytes;
|
||||
|
||||
// Хвостовые потери молчаливы: у последних кадров нет следующего seq, по
|
||||
// которому детектор увидел бы разрыв. Двигаем фронт на конец файла —
|
||||
// дальше приходить могут только ретрансмиты (seq ≤ highest_seq), ложных
|
||||
// дыр они не породят.
|
||||
if (e.total_seq > 0 && (!seq_init || highest_seq < e.total_seq - 1)) {
|
||||
lost += e.total_seq - (seq_init ? highest_seq + 1 : 0);
|
||||
seq_init = 1;
|
||||
highest_seq = e.total_seq - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Досрочно финализировать последнюю группу (group-FEC + FDD). Штатно группа
|
||||
// закрывается приходом первого кадра СЛЕДУЮЩЕЙ группы — а для последней его
|
||||
// не будет, и без этого её восстановление паритетом случилось бы только при
|
||||
// выходе потока C, то есть уже после COMPLETE. Дыры пришлось бы закрывать
|
||||
// ретрансмитом вместо готового паритета. END значит «новых данных не будет»
|
||||
// (дальше только повторы), поэтому финализировать безопасно; повторные END
|
||||
// попадают в no-op (g.active уже 0).
|
||||
group_flush();
|
||||
|
||||
// NACK пересобираем на КАЖДОМ END, а не только на первом (этап 3c). A шлёт
|
||||
// END каждые 100 мс до самого COMPLETE, так что это ещё и штатный способ
|
||||
// вернуть в запрос seq, выпавшие из miss[] по MISS_CAP: пока хвост пуст,
|
||||
// дыры видит только кольцо. Скан ≤1024 при 10 Гц — сотые доли ядра.
|
||||
miss_rebuild();
|
||||
|
||||
if (end_rx == 1)
|
||||
fprintf(stderr, "\n[ARQ] END принят: всего %u кадров, %llu байт; не хватает %d\n",
|
||||
e.total_seq, (unsigned long long)e.total_bytes, miss_cnt);
|
||||
}
|
||||
|
||||
// Разбор одного кадра в потоке C: seq-детектор + RS + CRC-поверх-RS, затем
|
||||
// маршрутизация (прямая запись для legacy, сборка группы для group-FEC).
|
||||
// Единственный писатель в stdout — этот поток, порядок сохранён (fq FIFO).
|
||||
@@ -220,10 +556,16 @@ static void process_frame(fq_slot_t *fs) {
|
||||
unsigned pay_len = fs->pay_len;
|
||||
int pay_valid = fs->pay_valid;
|
||||
|
||||
// END (F0 E7) — маркер конца передачи. Ветвимся ДО seq-детектора: у END свой
|
||||
// счётчик seq, в пространстве seq данных его быть не должно.
|
||||
if (hdr[0] == HDR_SIGNATURE_0 && hdr[1] == HDR_SIG_END_1) {
|
||||
process_end(hdr, pay, pay_len, pay_valid);
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверка сигнатуры hdr[0..1]: liquid-CRC (hdr_valid) страхует только
|
||||
// целостность байт, но не то, что это наш протокол. Пока в эфире один тип
|
||||
// кадра (данные/паритет GF, F0 AA) — это no-op; готовит почву под доп.
|
||||
// типы кадров обратного канала (roadmap §10 п.7: STATUS/END на др. частоте).
|
||||
// целостность байт, но не то, что это наш протокол. Отсеивает чужие типы
|
||||
// кадров (roadmap §10 п.7) и мусор, прошедший CRC заголовка.
|
||||
if (hdr[0] != HDR_SIGNATURE_0 || hdr[1] != HDR_SIGNATURE_1) {
|
||||
sig_bad++;
|
||||
return;
|
||||
@@ -241,7 +583,15 @@ static void process_frame(fq_slot_t *fs) {
|
||||
if (!seq_init) {
|
||||
seq_init = 1;
|
||||
highest_seq = seq;
|
||||
if (seq > 0) { // приём начался не с нуля: кадры до
|
||||
for (uint32_t m = 0; m < seq; m++) miss_add(m); // первого пойманного
|
||||
lost += seq; // потеряны, а дырой не выглядят —
|
||||
fprintf(stderr, "\n[ПОТЕРЯ] старт с seq %u, дыра %u кадров в начале\n",
|
||||
seq, seq); // им не предшествовал никакой seq
|
||||
}
|
||||
} else if (seq > highest_seq + 1) {
|
||||
// Пропавшие seq → NACK-набор обратного канала (этап 2) + счётчик потерь.
|
||||
for (uint32_t m = highest_seq + 1; m < seq; m++) miss_add(m);
|
||||
lost += seq - highest_seq - 1;
|
||||
fprintf(stderr, "\n[ПОТЕРЯ] seq %u→%u, дыра %u кадров\n",
|
||||
highest_seq, seq, seq - highest_seq - 1);
|
||||
@@ -250,6 +600,8 @@ static void process_frame(fq_slot_t *fs) {
|
||||
highest_seq = seq;
|
||||
}
|
||||
// seq ≤ highest_seq: старый/повторный кадр — highest_seq не трогаем.
|
||||
// Снятие seq с NACK — НЕ здесь, а после декода (см. метку route): факт
|
||||
// приёма кадра ещё не значит, что он целый.
|
||||
|
||||
uint16_t original_len = (hdr[6] << 8) | hdr[7];
|
||||
uint8_t last_block_bytes = hdr[10];
|
||||
@@ -300,11 +652,24 @@ static void process_frame(fq_slot_t *fs) {
|
||||
}
|
||||
|
||||
route:
|
||||
// Учёт NACK — ТОЛЬКО после декода. Раньше seq снимался с NACK по факту
|
||||
// приёма кадра, и битый кадр (rs_fail) выпадал из списка навсегда: ARQ его
|
||||
// уже не дослал бы, а RX считал бы файл собранным. Битый кадр — такая же
|
||||
// дыра, как непришедший, просто обнаруженная не детектором seq.
|
||||
if (ok) miss_ack(seq);
|
||||
else miss_add_sorted(seq);
|
||||
|
||||
if (flags & GF_FLAG_MODE) {
|
||||
group_route(flags, seq, dec, dec_len, ok);
|
||||
} else {
|
||||
} else if (ok) {
|
||||
if (g.active) group_flush(); // TX не мешает режимы, но подстрахуемся
|
||||
if (ok) {
|
||||
if (g_fb_fg) {
|
||||
rb_insert(seq, dec, dec_len); // FDD: ретрансмит приходит вне
|
||||
rb_drain(); // очереди — порядок держит кольцо
|
||||
} else {
|
||||
// Симплекс: ретрансмитов нет, seq всегда растёт — переупорядочивать
|
||||
// нечего. Кольцо здесь только задержало бы вывод до EOF (дыра стопорит
|
||||
// next_out, а закрыть её некому). Пишем сразу, как до этапа 3b.
|
||||
fwrite(dec, 1, dec_len, stdout);
|
||||
fflush(stdout);
|
||||
total_bytes += dec_len;
|
||||
@@ -396,13 +761,28 @@ static void *dsp_thread(void *arg) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Поток C (ядро 1): RS-декод + CRC + запись из очереди кадров.
|
||||
// Поток C (ядро 1): RS-декод + CRC + запись из очереди кадров + периодический
|
||||
// STATUS обратного канала. Ждём кадр таймингованно (50 мс), чтобы STATUS уходил
|
||||
// и в простое (именно тогда TX и нужно знать про дыры). maybe_send_status —
|
||||
// no-op в симплексе (g_fb_fg==NULL), поэтому поведение без -F не меняется.
|
||||
static void *fec_thread(void *arg) {
|
||||
(void)arg;
|
||||
pin_to_cpu(1);
|
||||
for (;;) {
|
||||
if (!wait_slot(&fq.mtx, &fq.not_empty, &fq.count, &fq.eof))
|
||||
break;
|
||||
pthread_mutex_lock(&fq.mtx);
|
||||
while (fq.count == 0 && !fq.eof && !stop) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_nsec += 50000000L; // 50 мс
|
||||
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
|
||||
pthread_cond_timedwait(&fq.not_empty, &fq.mtx, &ts);
|
||||
if (fq.count == 0 && !fq.eof && !stop) { // всё ещё пусто → STATUS без кадра
|
||||
pthread_mutex_unlock(&fq.mtx);
|
||||
maybe_send_status();
|
||||
pthread_mutex_lock(&fq.mtx);
|
||||
}
|
||||
}
|
||||
if (fq.count == 0) { pthread_mutex_unlock(&fq.mtx); break; } // eof/stop
|
||||
fq_slot_t *fs = &fq_slots[fq.head];
|
||||
pthread_mutex_unlock(&fq.mtx);
|
||||
|
||||
@@ -412,9 +792,12 @@ static void *fec_thread(void *arg) {
|
||||
fq.head = (fq.head + 1) % FQ_SLOTS;
|
||||
fq.count--;
|
||||
pthread_mutex_unlock(&fq.mtx);
|
||||
|
||||
maybe_send_status(); // под нагрузкой — период гейтит частоту
|
||||
}
|
||||
group_flush(); // дописать последнюю группу файла (нет кадра-триггера)
|
||||
return NULL;
|
||||
rb_flush_all(); // и хвост кольца: приём кончился, дыры схлопываем —
|
||||
return NULL; // досылать их уже некому
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
@@ -457,7 +840,7 @@ int main(int argc, char *argv[]) {
|
||||
fprintf(stderr, "Усиление: %.0f дБ (ручное)\n", RX_GAIN);
|
||||
fprintf(stderr, "CFO фильтр: α=%.2f\n", CFO_FILTER_ALPHA);
|
||||
fprintf(stderr, "Дуплекс: %s\n", fb_freq
|
||||
? "FDD (обратный TX настроен, трафик — этап 2)" : "симплекс");
|
||||
? "FDD (обратный канал: передача STATUS на 868)" : "симплекс");
|
||||
if (fb_freq) fprintf(stderr, "Обр. TX LO: %.1f МГц, усил. %.0f дБ\n", fb_freq / 1e6, fb_gain);
|
||||
fprintf(stderr, "URI: %s\n", uri);
|
||||
fprintf(stderr, "Конвейер: 3 стадии (A захват / B sync / C FEC+IO)\n");
|
||||
@@ -519,6 +902,27 @@ int main(int argc, char *argv[]) {
|
||||
// Тот же 12-байтовый заголовок, что и на TX (иначе байты 8..11 — мусор)
|
||||
ofdmflexframesync_set_header_len(fs, HDR_SIZE);
|
||||
|
||||
// Обратный канал (этап 2, §10 п.7): framegen + буфер на feedback-TX (868).
|
||||
// Кадры STATUS (F0 55) шлёт поток C через maybe_send_status. set_header_len
|
||||
// 12 обязателен и здесь — иначе TX-сторона прочтёт заголовок как мусор.
|
||||
if (fb_freq && txfb) {
|
||||
iio_channel_enable(iio_device_find_channel(txfb, "voltage0", true));
|
||||
iio_channel_enable(iio_device_find_channel(txfb, "voltage1", true));
|
||||
g_fb_buf = iio_device_create_buffer(txfb, FB_BUF_SAMPLES, false);
|
||||
if (!g_fb_buf) {
|
||||
fprintf(stderr, "[warn] буфер обратного канала не создан — STATUS отключён\n");
|
||||
} else {
|
||||
ofdmflexframegenprops_s fp;
|
||||
ofdmflexframegenprops_init_default(&fp);
|
||||
fp.check = LIQUID_CRC_32;
|
||||
g_fb_fg = ofdmflexframegen_create(OFDM_M, OFDM_CP, OFDM_TAPER, NULL, &fp);
|
||||
ofdmflexframegen_set_header_len(g_fb_fg, HDR_SIZE);
|
||||
g_fb_enc = fec_create(LIQUID_FEC_RS_M8, NULL);
|
||||
fprintf(stderr, "Обратный канал: STATUS каждые %d мс (F0 55, RS+CRC32)\n",
|
||||
FB_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "Приёмник готов. Ожидание сигнала...\n\n");
|
||||
fprintf(stderr, "Кадры | Загол. | CRC | RS испр | RS бит | SigBd | Потери | Overr | FQdrp | Rec | GrpF | Вывод | RSSI | EVM | CFO\n");
|
||||
fprintf(stderr, "--------+--------+------+---------+--------+-------+--------+--------+--------+--------+--------+--------+-------+-------+----------\n");
|
||||
@@ -565,10 +969,22 @@ int main(int argc, char *argv[]) {
|
||||
(unsigned long long)ring.overruns, (unsigned long long)fq.drops,
|
||||
(unsigned long long)grp_recovered, (unsigned long long)grp_failed,
|
||||
(unsigned long long)crc_final, (unsigned long long)total_bytes);
|
||||
if (fb_freq) {
|
||||
fprintf(stderr, "Обратный канал: отправлено STATUS: %u\n", g_fb_seq);
|
||||
fprintf(stderr, "ARQ: END принято: %llu | ожидалось кадров: %u | выведено seq: %u | не хватает: %d | COMPLETE: %s\n",
|
||||
(unsigned long long)end_rx, end_total_seq, next_out, miss_cnt,
|
||||
complete_since_ms ? "да" : "НЕТ");
|
||||
fprintf(stderr, "ARQ: окно reorder переполнено: %llu | NACK переполнен: %llu | пересборок NACK: %llu\n",
|
||||
(unsigned long long)rb_over, (unsigned long long)miss_over,
|
||||
(unsigned long long)miss_rebuilt);
|
||||
}
|
||||
|
||||
if (decoder) fec_destroy(decoder);
|
||||
ofdmflexframesync_destroy(fs);
|
||||
iio_buffer_destroy(buf);
|
||||
if (g_fb_fg) ofdmflexframegen_destroy(g_fb_fg);
|
||||
if (g_fb_enc) fec_destroy(g_fb_enc);
|
||||
if (g_fb_buf) iio_buffer_destroy(g_fb_buf);
|
||||
iio_context_destroy(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "common.h"
|
||||
#include "group_fec.h"
|
||||
#include "frame_tx.h"
|
||||
#include "feedback.h" // приём STATUS обратного канала (этап 2, §10 п.7)
|
||||
|
||||
// Параметры передатчика по умолчанию
|
||||
#define DEFAULT_GAIN -30.0 // Усиление передатчика: -30 дБ
|
||||
@@ -23,6 +24,219 @@ extern volatile sig_atomic_t stop;
|
||||
// Сигнатура кадров данных/паритета GF (см. frame_tx.h — тип кадра различает
|
||||
// hdr[0..1]; END/STATUS обратного канала используют другие сигнатуры, §10 п.7).
|
||||
static const uint8_t SIG_DATA[2] = { HDR_SIGNATURE_0, HDR_SIGNATURE_1 };
|
||||
static const uint8_t SIG_END[2] = { HDR_SIGNATURE_0, HDR_SIG_END_1 };
|
||||
|
||||
// Параметры EOF-хендшейка (этап 3). После EOF A шлёт END каждые END_PERIOD_MS и
|
||||
// ждёт STATUS с флагом COMPLETE. В этой фазе эфир 915 почти пуст (END ≈ 1.4 мс
|
||||
// на 100 мс), поэтому fb-RX платы A больше не глохнет в собственные бёрсты и
|
||||
// слышит практически все STATUS (плато 50% из §12.8 — эффект duty 57%).
|
||||
#define END_PERIOD_MS 100
|
||||
#define END_TIMEOUT_MS 10000 // 100 попыток; молчание = честный отказ (exit 2)
|
||||
|
||||
static int64_t now_ms(void) {
|
||||
struct timespec t;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t);
|
||||
return (int64_t)t.tv_sec * 1000 + t.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
// --- Обратный канал: приём STATUS на 868 (этап 2, §10 п.7) --------------
|
||||
// Отдельный поток на ядре 1 (данные TX — ядро 0). Свой синхронизатор с
|
||||
// header_len 12 и свой RS-декодер. Слушает rxfb — второй cf-девайс, открытый
|
||||
// pluto_init_fdd. Декодированный STATUS публикуется снимком для main: пока его
|
||||
// читает только END-фаза (этап 3a), ретрансмит по NACK — этап 3b.
|
||||
#define FB_RX_BUF_SAMPLES 16384
|
||||
|
||||
static struct iio_buffer *g_fbrx_buf = NULL;
|
||||
static ofdmflexframesync g_fbrx_fs = NULL;
|
||||
static fec g_fbrx_dec = NULL;
|
||||
static volatile sig_atomic_t fb_stop = 0;
|
||||
static uint64_t g_fb_status_rx = 0; // сколько STATUS декодировано (для итога)
|
||||
|
||||
// Снимок последнего STATUS для main (этап 3). STATUS кумулятивен — это состояние
|
||||
// RX целиком, а не событие, поэтому очередь не нужна: latest-wins. Пишет поток
|
||||
// fb_rx, читает main; TX-буфером по-прежнему владеет ТОЛЬКО main (передавать из
|
||||
// callback нельзя — это ядро 1 и чужой буфер).
|
||||
static pthread_mutex_t g_fb_mtx = PTHREAD_MUTEX_INITIALIZER;
|
||||
static fb_status_t g_fb_st;
|
||||
static int g_fb_fresh = 0; // 1 = снимок не прочитан main'ом
|
||||
|
||||
// COMPLETE — отдельная защёлка, а не поле снимка: снимок «съедает» arq_service,
|
||||
// и подтверждение, пришедшее в том же STATUS, потерялось бы. Флаг монотонный —
|
||||
// once complete, always complete.
|
||||
static volatile sig_atomic_t g_fb_complete = 0;
|
||||
|
||||
// Забрать свежий снимок. Возврат 1 — *out заполнен, 0 — нового STATUS не было.
|
||||
static int fb_take(fb_status_t *out) {
|
||||
int fresh;
|
||||
pthread_mutex_lock(&g_fb_mtx);
|
||||
fresh = g_fb_fresh;
|
||||
if (fresh) { *out = g_fb_st; g_fb_fresh = 0; }
|
||||
pthread_mutex_unlock(&g_fb_mtx);
|
||||
return fresh;
|
||||
}
|
||||
|
||||
// callback синхронизатора обратного канала: отсев по сигнатуре F0 55, RS+CRC
|
||||
// декод, распаковка, лог и публикация снимка для main.
|
||||
static int fb_callback(unsigned char *hdr, int hdr_valid,
|
||||
unsigned char *pay, unsigned int pay_len,
|
||||
int pay_valid, framesyncstats_s stats, void *user) {
|
||||
(void)pay_valid; (void)stats; (void)user;
|
||||
if (!hdr_valid) return 0;
|
||||
if (hdr[0] != FB_SIG_0 || hdr[1] != FB_SIG_1) return 0; // не STATUS
|
||||
|
||||
static uint8_t dec[RS_ENC * 2]; // STATUS ≤ 1 RS-блок, запас на будущее
|
||||
size_t dlen = 0;
|
||||
if (!phy_rx_decode(hdr, pay, pay_len, g_fbrx_dec, dec, sizeof(dec), &dlen))
|
||||
return 0; // RS/CRC не прошли — молча дропаем
|
||||
fb_status_t st;
|
||||
if (fb_status_unpack(dec, dlen, &st) != 0) return 0;
|
||||
|
||||
g_fb_status_rx++;
|
||||
if (st.flags & FB_FLAG_COMPLETE) g_fb_complete = 1;
|
||||
pthread_mutex_lock(&g_fb_mtx);
|
||||
g_fb_st = st;
|
||||
g_fb_fresh = 1;
|
||||
pthread_mutex_unlock(&g_fb_mtx);
|
||||
|
||||
fprintf(stderr, "\n[FB] highest=%u rx_frames=%u overruns=%u%s%s nack=%u{",
|
||||
st.highest_seq, st.rx_frames, st.overruns,
|
||||
(st.flags & FB_FLAG_END_SEEN) ? " END" : "",
|
||||
(st.flags & FB_FLAG_COMPLETE) ? " COMPLETE" : "",
|
||||
st.nack_cnt);
|
||||
for (int i = 0; i < st.nack_cnt && i < 8; i++)
|
||||
fprintf(stderr, "%s%u", i ? "," : "", st.nack[i]);
|
||||
if (st.nack_cnt > 8) fprintf(stderr, ",…");
|
||||
fprintf(stderr, "}\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void *fb_rx_thread(void *arg) {
|
||||
(void)arg;
|
||||
pin_to_cpu(1);
|
||||
static liquid_float_complex conv[FB_RX_BUF_SAMPLES];
|
||||
while (!fb_stop && !stop) {
|
||||
ssize_t nb = iio_buffer_refill(g_fbrx_buf);
|
||||
if (nb < 0) { if (fb_stop || stop) break; usleep(500); continue; }
|
||||
int16_t *bb = (int16_t *)iio_buffer_start(g_fbrx_buf);
|
||||
int ns = (iio_buffer_end(g_fbrx_buf) - iio_buffer_start(g_fbrx_buf))
|
||||
/ (int)sizeof(int16_t) / 2;
|
||||
if (ns > FB_RX_BUF_SAMPLES) ns = FB_RX_BUF_SAMPLES;
|
||||
for (int i = 0; i < ns; i++)
|
||||
conv[i] = ((float)bb[2*i] / 2048.0f) +
|
||||
((float)bb[2*i+1] / 2048.0f) * _Complex_I;
|
||||
ofdmflexframesync_execute(g_fbrx_fs, conv, ns);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// --- TX-окно и ARQ (этап 3b) --------------------------------------------
|
||||
// Копия каждого переданного кадра: без неё дослать нечего — stdin уже прочитан
|
||||
// и не перематывается. Кольцо по seq % TXWIN, хранит СЫРОЙ payload (до RS/OFDM):
|
||||
// phy_tx_frame детерминирован, поэтому повтор уходит в эфир байт-в-байт.
|
||||
// TXWIN(2048) > RB_CAP(1024) приёмника — кадр, который B ещё может принять в
|
||||
// своё окно переупорядочивания, гарантированно есть у нас в окне.
|
||||
#define TXWIN 2048
|
||||
#define ARQ_RETX_MS 400 // per-seq пауза между повторами одного seq.
|
||||
// 400 = петля «потеря → детект (~10 мс) → слот STATUS (~50 мс) → доставка при
|
||||
// p≈0.5 (~100–300 мс) → ретрансмит (~30 мс) → подтверждение (~150–250 мс)».
|
||||
// Меньше — дубли жгут эфир (кадр 9.3 мс); больше — дыра дольше держит окно B.
|
||||
|
||||
typedef struct {
|
||||
uint32_t seq;
|
||||
int64_t last_ms; // когда seq последний раз уходил в эфир
|
||||
uint16_t len;
|
||||
uint8_t hdr11; // флаги group-FEC (0 = legacy)
|
||||
uint8_t valid;
|
||||
uint8_t pay[GF_PAR_PAY]; // 1026: паритетный кадр GF — самый большой
|
||||
} txslot_t;
|
||||
|
||||
static txslot_t txwin[TXWIN]; // ~2.1 МБ .bss (из 1 ГБ платы)
|
||||
static int g_arq_on = 0; // ARQ включён (требует -F: без обратного канала
|
||||
// NACK взять неоткуда). Работает и с group-FEC.
|
||||
static uint64_t g_retx = 0; // отправлено ретрансмитов
|
||||
static uint64_t g_retx_old = 0; // NACK на seq, вышедший из окна (≈21 с эфира)
|
||||
|
||||
// Последний известный список дыр B. Живёт между снимками: пока не пришёл новый
|
||||
// STATUS, дожимаем прежний — от дублей защищает ARQ_RETX_MS, а не свежесть.
|
||||
static uint32_t g_nack[FB_MAX_NACK];
|
||||
static int g_nack_cnt = 0;
|
||||
|
||||
// Передать НОВЫЙ кадр данных/паритета, попутно сохранив его в окне.
|
||||
static void tx_send_data(ofdmflexframegen fg, struct iio_buffer *buf, fec enc,
|
||||
uint32_t seq, uint8_t hdr11, const uint8_t *pl, size_t n,
|
||||
float amp, int pause_us) {
|
||||
txslot_t *s = &txwin[seq % TXWIN];
|
||||
s->seq = seq; s->hdr11 = hdr11; s->len = (uint16_t)n; s->valid = 1;
|
||||
memcpy(s->pay, pl, n);
|
||||
phy_tx_frame(fg, buf, BUF_SAMPLES, enc, SIG_DATA, seq, hdr11, pl, n, amp, pause_us);
|
||||
s->last_ms = now_ms(); // после отправки: паузу считаем от конца кадра
|
||||
}
|
||||
|
||||
// Досылка по NACK. budget — потолок ретрансмитов за один вызов: очередь дыр
|
||||
// не должна вытеснять новые данные (голодание фронта). Вызывается ТОЛЬКО из
|
||||
// main — TX-буфер один и владеет им этот поток.
|
||||
static void arq_service(ofdmflexframegen fg, struct iio_buffer *buf, fec enc,
|
||||
float amp, int pause_us, int budget) {
|
||||
if (!g_arq_on) return;
|
||||
fb_status_t st;
|
||||
if (fb_take(&st)) { // свежий снимок → обновить список дыр
|
||||
g_nack_cnt = st.nack_cnt < FB_MAX_NACK ? st.nack_cnt : FB_MAX_NACK;
|
||||
memcpy(g_nack, st.nack, (size_t)g_nack_cnt * sizeof(g_nack[0]));
|
||||
}
|
||||
int64_t t = now_ms();
|
||||
for (int i = 0; i < g_nack_cnt && budget > 0; i++) {
|
||||
uint32_t seq = g_nack[i];
|
||||
txslot_t *s = &txwin[seq % TXWIN];
|
||||
if (!s->valid || s->seq != seq) { // окно провернулось — дослать нечем
|
||||
g_retx_old++; // → END-таймаут, честный отказ
|
||||
continue;
|
||||
}
|
||||
if (t - s->last_ms < ARQ_RETX_MS) continue; // повтор ещё в полёте
|
||||
phy_tx_frame(fg, buf, BUF_SAMPLES, enc, SIG_DATA, seq, s->hdr11,
|
||||
s->pay, s->len, amp, pause_us);
|
||||
t = now_ms();
|
||||
s->last_ms = t;
|
||||
g_retx++;
|
||||
budget--;
|
||||
}
|
||||
}
|
||||
|
||||
// EOF-хендшейк (этап 3, ядро 0/main): шлём END с итогом передачи, пока B не
|
||||
// подтвердит COMPLETE флагом в STATUS. Возврат 0 — подтверждено, 2 — таймаут или
|
||||
// прерывание: на той стороне файл неполон. Честный отказ вместо тихой дыры
|
||||
// (техдолг #6) — раньше A просто закрывал буфер и рапортовал успех.
|
||||
static int end_phase(ofdmflexframegen fg, struct iio_buffer *buf, fec enc,
|
||||
float amp, int pause_us, uint32_t total_seq, uint64_t total_bytes) {
|
||||
fb_end_t e = { .total_seq = total_seq, .total_bytes = total_bytes };
|
||||
uint8_t pay[FB_END_LEN];
|
||||
size_t n = fb_end_pack(&e, pay);
|
||||
uint32_t eseq = 0; // свой счётчик: RX ветвится по сигнатуре ДО
|
||||
// seq-детектора данных → дыр от END не будет
|
||||
int64_t t0 = now_ms();
|
||||
int sent = 0;
|
||||
|
||||
fprintf(stderr, "[ARQ] END: кадров %u, байт %llu — жду COMPLETE (таймаут %d мс)\n",
|
||||
total_seq, (unsigned long long)total_bytes, END_TIMEOUT_MS);
|
||||
|
||||
while (!stop && !g_fb_complete && now_ms() - t0 < END_TIMEOUT_MS) {
|
||||
phy_tx_frame(fg, buf, BUF_SAMPLES, enc, SIG_END, eseq++, 0, pay, n, amp, 0);
|
||||
sent++;
|
||||
// Бюджет выше, чем в data-фазе: новых данных больше нет, эфир свободен,
|
||||
// и закрыть остаток дыр надо именно сейчас — таймаут тикает.
|
||||
arq_service(fg, buf, enc, amp, pause_us, 4);
|
||||
int64_t w = now_ms();
|
||||
while (!stop && !g_fb_complete && now_ms() - w < END_PERIOD_MS)
|
||||
usleep(5000); // пауза между END: опрос защёлки, не sleep
|
||||
}
|
||||
if (g_fb_complete) {
|
||||
fprintf(stderr, "[ARQ] COMPLETE подтверждён: END × %d, ретрансмитов %llu, %lld мс\n",
|
||||
sent, (unsigned long long)g_retx, (long long)(now_ms() - t0));
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, "[ARQ] таймаут COMPLETE: END × %d за %lld мс — приём НЕ подтверждён\n",
|
||||
sent, (long long)(now_ms() - t0));
|
||||
return 2;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
// Инициализация параметров
|
||||
@@ -74,7 +288,7 @@ int main(int argc, char *argv[]) {
|
||||
fprintf(stderr, "Пауза: %d мкс\n", pause_us);
|
||||
fprintf(stderr, "Group-FEC: %s\n", group_fec ? "16+2 (§10 п.6)" : "выкл");
|
||||
fprintf(stderr, "Дуплекс: %s\n", fb_freq
|
||||
? "FDD (обратный RX настроен, трафик — этап 2)" : "симплекс");
|
||||
? "FDD (обратный канал: приём STATUS на 868)" : "симплекс");
|
||||
if (fb_freq) fprintf(stderr, "Обр. RX LO: %.1f МГц\n", fb_freq / 1e6);
|
||||
fprintf(stderr, "URI: %s\n", uri);
|
||||
fprintf(stderr, "═══════════════════════════════════════════════════\n");
|
||||
@@ -122,6 +336,37 @@ int main(int argc, char *argv[]) {
|
||||
// last_block_bytes) без этого в эфир не уходят. RX выставляет то же.
|
||||
ofdmflexframegen_set_header_len(fg, HDR_SIZE);
|
||||
|
||||
// Обратный канал (этап 2): поток fb_rx слушает STATUS на 868 (rxfb). Пока
|
||||
// только логирует. Данные TX уводим на ядро 0, fb_rx — на ядро 1 (§affinity).
|
||||
pthread_t fb_tid;
|
||||
int fb_started = 0;
|
||||
if (fb_freq) {
|
||||
iio_channel_enable(iio_device_find_channel(rxfb, "voltage0", false));
|
||||
iio_channel_enable(iio_device_find_channel(rxfb, "voltage1", false));
|
||||
g_fbrx_buf = iio_device_create_buffer(rxfb, FB_RX_BUF_SAMPLES, false);
|
||||
if (!g_fbrx_buf) {
|
||||
LOG_ERROR("Буфер обратного канала не создан — STATUS не принимаются");
|
||||
} else {
|
||||
g_fbrx_fs = ofdmflexframesync_create(OFDM_M, OFDM_CP, OFDM_TAPER,
|
||||
NULL, fb_callback, NULL);
|
||||
ofdmflexframesync_set_header_len(g_fbrx_fs, HDR_SIZE);
|
||||
g_fbrx_dec = fec_create(LIQUID_FEC_RS_M8, NULL);
|
||||
pin_to_cpu(0); // TX-данные — ядро 0
|
||||
pthread_create(&fb_tid, NULL, fb_rx_thread, NULL);
|
||||
fb_started = 1;
|
||||
// Этап 3c: ARQ работает и с group-FEC. Ретрансмит кадра старой группы
|
||||
// больше не ломает сборку на RX (правило base < g.base в group_route),
|
||||
// а дыры, закрытые паритетом, RX снимает с NACK сам — досылать их не
|
||||
// придётся. GF и ARQ дополняют друг друга: GF гасит одиночные потери
|
||||
// без задержки, ARQ добирает то, что паритету не по силам.
|
||||
g_arq_on = 1;
|
||||
fprintf(stderr, "Обратный канал: слушаю STATUS на %.1f МГц (ядро 1)\n",
|
||||
fb_freq / 1e6);
|
||||
fprintf(stderr, "ARQ: вкл (окно %d кадров, повтор не чаще %d мс)\n",
|
||||
TXWIN, ARQ_RETX_MS);
|
||||
}
|
||||
}
|
||||
|
||||
// Счётчики статистики
|
||||
uint64_t frames = 0, bytes_tx = 0;
|
||||
uint32_t seq = 0;
|
||||
@@ -140,8 +385,12 @@ int main(int argc, char *argv[]) {
|
||||
usleep(10000);
|
||||
continue;
|
||||
}
|
||||
phy_tx_frame(fg, buf, BUF_SAMPLES, enc, SIG_DATA, seq++, 0, raw, n, amp, pause_us);
|
||||
tx_send_data(fg, buf, enc, seq++, 0, raw, n, amp, pause_us);
|
||||
frames++; bytes_tx += n;
|
||||
// Досылка вперемешку с новыми данными: дыра закрывается, пока B ещё
|
||||
// держит её в окне переупорядочивания. Бюджет 2 — потолок, чтобы
|
||||
// очередь NACK не вытеснила фронт передачи.
|
||||
arq_service(fg, buf, enc, amp, pause_us, 2);
|
||||
if (time(NULL) > last) {
|
||||
fprintf(stderr, "\r📤 Передано: %llu кадров, %llu байт",
|
||||
(unsigned long long)frames, (unsigned long long)bytes_tx);
|
||||
@@ -168,9 +417,10 @@ int main(int argc, char *argv[]) {
|
||||
if (k == GF_K || (done_reading && k > 0)) {
|
||||
// k кадров данных: idx=0..k-1
|
||||
for (int i = 0; i < k; i++) {
|
||||
phy_tx_frame(fg, buf, BUF_SAMPLES, enc, SIG_DATA, seq++, GF_FLAG_MODE | i,
|
||||
gdata[i], glen[i], amp, pause_us);
|
||||
tx_send_data(fg, buf, enc, seq++, GF_FLAG_MODE | i,
|
||||
gdata[i], glen[i], amp, pause_us);
|
||||
frames++; bytes_tx += glen[i];
|
||||
arq_service(fg, buf, enc, amp, pause_us, 2); // как в legacy
|
||||
}
|
||||
// паритеты P (idx=k) и Q (idx=k+1); мета = k′ + длина хвоста
|
||||
uint8_t P[GF_CHUNK], Q[GF_CHUNK], par[GF_PAR_PAY];
|
||||
@@ -178,12 +428,13 @@ int main(int argc, char *argv[]) {
|
||||
uint16_t meta = GF_META_PACK(k, glen[k - 1]);
|
||||
par[0] = meta >> 8; par[1] = meta & 0xFF;
|
||||
memcpy(par + GF_META, P, GF_CHUNK);
|
||||
phy_tx_frame(fg, buf, BUF_SAMPLES, enc, SIG_DATA, seq++, GF_FLAG_MODE | GF_FLAG_PAR | k,
|
||||
par, GF_PAR_PAY, amp, pause_us);
|
||||
tx_send_data(fg, buf, enc, seq++, GF_FLAG_MODE | GF_FLAG_PAR | k,
|
||||
par, GF_PAR_PAY, amp, pause_us);
|
||||
memcpy(par + GF_META, Q, GF_CHUNK);
|
||||
phy_tx_frame(fg, buf, BUF_SAMPLES, enc, SIG_DATA, seq++, GF_FLAG_MODE | GF_FLAG_PAR | GF_FLAG_Q | (k + 1),
|
||||
par, GF_PAR_PAY, amp, pause_us);
|
||||
tx_send_data(fg, buf, enc, seq++, GF_FLAG_MODE | GF_FLAG_PAR | GF_FLAG_Q | (k + 1),
|
||||
par, GF_PAR_PAY, amp, pause_us);
|
||||
frames += 2;
|
||||
arq_service(fg, buf, enc, amp, pause_us, 2);
|
||||
k = 0;
|
||||
if (time(NULL) > last) {
|
||||
fprintf(stderr, "\r📤 Передано: %llu кадров, %llu байт (group-FEC)",
|
||||
@@ -198,6 +449,13 @@ int main(int argc, char *argv[]) {
|
||||
fprintf(stderr, "\nПередача завершена: %llu кадров, %llu байт\n",
|
||||
(unsigned long long)frames, (unsigned long long)bytes_tx);
|
||||
|
||||
// EOF-хендшейк (этап 3). Только при живом обратном канале: без него COMPLETE
|
||||
// услышать нечем и END-фаза всегда упиралась бы в таймаут. Симплекс идёт
|
||||
// прежним путём — молча в дренаж, exit 0 (регрессия §12 не меняется).
|
||||
int rc = 0;
|
||||
if (fb_started)
|
||||
rc = end_phase(fg, buf, enc, amp, pause_us, seq, bytes_tx);
|
||||
|
||||
// Дренаж: выпихнуть последний кадр из kernel-буферов DMA перед закрытием.
|
||||
// Без паузы iio_buffer_destroy() срезает хвост файла в эфире (техдолг #5).
|
||||
{
|
||||
@@ -207,10 +465,24 @@ int main(int argc, char *argv[]) {
|
||||
usleep(100000); // ~4 kernel-буфера при 1.92 MSPS ≈ 35 мс, с запасом
|
||||
}
|
||||
|
||||
// Остановить обратный канал: fb_rx выходит по fb_stop в течение одного
|
||||
// refill (~8.5 мс на 16384 сэмпл @ 1.92 Msps), затем join.
|
||||
if (fb_started) {
|
||||
fb_stop = 1;
|
||||
pthread_join(fb_tid, NULL);
|
||||
fprintf(stderr, "Обратный канал: принято STATUS: %llu\n",
|
||||
(unsigned long long)g_fb_status_rx);
|
||||
fprintf(stderr, "ARQ: ретрансмитов: %llu | NACK вне окна: %llu\n",
|
||||
(unsigned long long)g_retx, (unsigned long long)g_retx_old);
|
||||
}
|
||||
|
||||
// Освобождение ресурсов
|
||||
if (enc) fec_destroy(enc);
|
||||
ofdmflexframegen_destroy(fg);
|
||||
iio_buffer_destroy(buf);
|
||||
if (g_fbrx_fs) ofdmflexframesync_destroy(g_fbrx_fs);
|
||||
if (g_fbrx_dec) fec_destroy(g_fbrx_dec);
|
||||
if (g_fbrx_buf) iio_buffer_destroy(g_fbrx_buf);
|
||||
iio_context_destroy(ctx);
|
||||
return 0;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user