Files
Pluto-SDR/docs/NOTES.md
Maxim ab1e87a17e Этап 3c — гибрид GF+ARQ, пересборка NACK, приёмка. Техдолг #6 закрыт
ARQ включён и для group-FEC. Паритет и ретрансмит перестали быть альтернативами:
GF гасит одиночные потери без задержки, ARQ добирает то, что паритету не по силам
(>2 стираний/группу). Дыру, закрытую паритетом, RX снимает с NACK сам — лишнего
трафика нет.

receiver.c:
- group_route: кадр ложится в кольцо ДО любого bypass. Кольцо — единственный
  источник порядка и живёт независимо от сборки групп; иначе ретрансмит закрытой
  группы не занял бы свой слот и next_out встал бы на нём навсегда. Паритет
  получает слот нулевой длины: тратит seq, но в файл не идёт.
- group_route: bypass для кадра уже закрытой группы (base < g.base, а также
  base == g.base при !active — группа финализирована по END). Без него ретрансмит
  преждевременно flush-ал активную группу, сбрасывая её стирания, либо «воскрешал»
  закрытую с одним элементом.
- group_flush: под FDD пишет в кольцо ТОЛЬКО восстановленные паритетом кадры
  (g.fixed) и снимает их seq с NACK — принятые уже в кольце. Симплекс сохраняет
  прямой вывод группы: ретрансмитов там нет, переупорядочивать нечего.
- process_end: досрочный group_flush последней группы. Штатно группа закрывается
  первым кадром следующей — для последней его не будет, и её восстановление
  случилось бы только при выходе потока C, то есть уже после COMPLETE.
- process_end/miss_rebuild: NACK пересобирается из кольца на каждом END. Находка
  стресса 3b: miss_add при переполнении MISS_CAP молча терял seq, и вернуть его
  было некому (детектор дыр по нему второй раз не срабатывает) — приём заклинивало
  насмерть. Кольцо знает точно, каких кадров нет.
- arq_check_complete: критерий единый (next_out == total_seq), временный gf_seen
  убран.

transmitter.c: g_arq_on = 1 безусловно при -F; arq_service в GF-цикле как в legacy.
2026-07-16 15:29:25 +03:00

16 KiB
Raw Permalink Blame History

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.

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.291.37%, среднее 0.72% (канал флапает, см. выше).
  • PER₂ (FDD, STATUS в эфире, FBGAIN 80…0, 8 прогонов): потерь 111, среднее 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-диапазон: ограничения на полосу канала (сотни кГц), ЭИИМ и duty cycle. Наш сигнал 1.5 МГц в них не вписывается.
  • 915 МГц — в Европе это uplink GSM-900, не ISM (ISM 902928 — регион США).

Практический вывод: разработка и демо — кабель с аттенюатором или антенны на столе с минимальной мощностью (TX gain от 40) и без внешних усилителей. Это ограничение прототипа фиксируется в отчёте проекта; у целевой системы по ТЗ предполагается лицензированный канал.

4. RF-безопасность

  • Кабельный тест только через аттенюатор 3040 дБ + -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) байты 811 (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. Быстрые команды-шпаргалки

# что видит плата
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'