Замеры расширенным ofdm_bench (плата B, ядро 0) документированы в BENCHMARK.md, диагноз — в README §12.5: - sync-only на сплошных кадрах 1.65 Msps < 1.92 → -p 0 недостижим; «запас 1.22×» был артефактом duty 0.571 - конвертация ~бесплатна (85 Msps) → двухпоточный вынос дал ~2% - RS-декод 275 мкс/блок = 26% эфира кадра (было «мало» — опровергнуто) - контеншн L2/DDR 0.2% (гипотеза кэша отвергнута) Вывод: узкое место sync+RS на ядре 0; фикс — вынос RS на ядро 1 (3-стадия), p_min ~3.4→~1.3 мс. Исправлены устаревшие выводы §2 и рычаг 2 в §12.3. PHY не менялся.
11 KiB
BENCHMARK.md — протокол замеров CPU (ofdmflexframesync на ARM Pluto)
Этот документ — основание для выбора sample rate. При любом изменении PHY-параметров (M, CP, модуляция, payload) бенчмарк перегоняется и таблица обновляется в том же коммите (правило №6 CLAUDE.md).
Зачем
ofdmflexframesync (liquid-dsp) — самый дорогой блок тракта. Если его
пропускная способность на одном ядре ниже sample rate, приёмник отстаёт от
эфира: IIO-буферы какое-то время амортизируют, затем переполняются и кадры
теряются лавинообразно. Опасность в том, что на редких пакетах система
выглядит работающей — отказ проявляется только под нагрузкой.
Методика
Инструмент: src/ofdm_bench.c. Генерирует эталонный буфер
(8 кадров + гэпы, duty cycle кадров ~57%), делает int16-версии
«сигнал+шум» (SNR ~28 дБ) и «чистый шум», прогоняет каждый через
синхронизатор ≥5 с. Конвертация int16→float входит в замер (как в
receiver.c). Процесс пинится на ядро 0 (sched_setaffinity).
Меряются два режима, лимитирует худший:
- шум — стоимость дежурного поиска преамбулы (>90% времени в линии);
- сигнал+кадры — полный цикл демодуляции под нагрузкой.
Валидация замера: CRC ok должен равняться 8 × число проходов
(все сгенерированные кадры пойманы и декодированы). Иначе сборка кривая
и цифрам верить нельзя.
Окружение эталонного замера
| Параметр | Значение |
|---|---|
| Дата | 2026-07-10 |
| Плата | Pluto+ (Zynq-7020), 2 × Cortex-A9 @ 667 МГц, NEON, 1 ГБ |
| Прошивка | v0.38-4-g95aad-dirty |
| liquid-dsp | 1.8.0, с FFTW 3.3.10 (NEON) — HAVE_LIBFFTW3F 1 в config.log |
| Сборка | -O3 -mcpu=cortex-a9 -mfpu=neon -mfloat-abi=hard -ffast-math -static |
| Кадр | M=64, CP=16, taper=4, QPSK, CRC32, FEC none, payload 1275 Б, header 8 Б |
| Ядро | одно (affinity CPU0) |
Результаты
буфер: 143360 сэмплов, 8 кадров на проход
шум (поиск): 5.318–5.324 Msps (186 проходов; кадров 0 — норма)
сигнал+кадры: 2.338–2.339 Msps (82 прохода; 656/656 CRC ok — валидно)
Разброс между тремя запусками < 0.1% — замер чистый.
Выводы (проектные решения)
| Sample rate | Запас (кадры) | Вердикт |
|---|---|---|
| 3.84 MSPS | 0.61× | ЗАПРЕЩЁН: не realtime, отказ под нагрузкой |
| 1.92 MSPS | 1.22× | рабочий: второе ядро свободно под IIO/RS/IO |
| 1.00 MSPS | 2.34× | резерв, если 1.92 окажется впритык в реальном тракте |
⚠️ ПЕРЕСМОТРЕНО 2026-07-14 (см. «Разбивка стоимости по стадиям»). «Запас 1.22×» измерен при duty кадров 0.571 (гэп 7680). На СПЛОШНОМ потоке кадров (duty→1, как при
-p 0) синхронизатор тянет лишь 1.65 Msps = 0.86× от 1.92 — запаса нет. Строка «1.92 MSPS 1.22×» верна только для редких пакетов; под непрерывной нагрузкой линия требует TX-паузы.
- Полезная скорость при 1.92 MSPS: ~1.3–1.5 Мбит/с (≈160–190 КБ/с) после RS(255,223), преамбул и служебки.
- Поиск на шуме (5.32 Msps) не лимитирует ни один из режимов.
RS-декод в замер не входит: его стоимость малаПЕРЕСМОТРЕНО 2026-07-14: стоимость RS НЕ мала — 275 мкс/блок = 1.37 мс/кадр = 26% эфира кадра. Это крупнейшая переносимая стоимость на потоке синхронизатора; выносится на второе ядро отдельной стадией.
Разбивка стоимости по стадиям (2026-07-14) — диагностика TX-паузы
Двухпоточный RX (поток A: refill+convert на ядре 1; поток B: sync+RS на
ядре 0) НЕ убрал паузу: на 1 МБ чисто только при -p 8000 (хуже базы §12.4).
ofdm_bench расширен (-g гэп, -P payload, -H hog-контеншн, разбивка
conv/sync/RS) и вскрыл причину. Плата B (RX), ядро 0, liquid-dsp 1.8.0.
Артефакт прежней методики: «2.34 Msps» — это duty 0.571
«2.34» — смесь быстрого поиска по шуму (5.6 Msps) и медленной демодуляции.
На сплошных кадрах (-g 0, duty 1.0 — что видит RX на -p 0):
| Режим | duty 0.571 (-g 7680) |
duty 1.000 (-g 0) |
|---|---|---|
| шум sync-only | 5.61 Msps | 5.48 Msps |
| сигнал conv+sync | 2.33 Msps | 1.62 Msps |
| сигнал sync-only | 2.40 Msps | 1.65 Msps |
| только конвертация | 81 Msps | 85 Msps |
Синхронизатор сам тянет 1.65 Msps на сплошных кадрах = 0.86× от 1.92.
При duty→1 он в принципе не realtime — -p 0 недостижим на этом PHY/CPU
при любой архитектуре потоков. (-P 1024, геометрия FEC none: то же, 1.67.)
Стоимость RS-декода — то, что уносится на ядро 1
fec_decode RS(255,223), искусственные байт-ошибки:
| Ошибок/блок | мкс/блок | мс/кадр (5 бл) | % эфира кадра (5.33 мс) |
|---|---|---|---|
| 0 (чистый) | 275 | 1.37 | 25.8% |
| 4 | 303 | 1.52 | 28.4% |
| 16 (предел RS) | 387 | 1.93 | 36.2% |
При EVM −33 дБ почти все блоки чистые → рабочая цифра ~26% эфира.
Конвертация int16→float — практически бесплатна
85 Msps (44× запас). conv+sync (1.62) vs sync-only (1.65) → разница ~2%. Вынос конвертации на ядро 1 (что и сделал двухпоток) экономит потоку B лишь ~2% — вот почему он не помог и слегка навредил (оверхед потоков + drop-newest рвёт lock на выброшенных кусках → попутные битые кадры).
Контеншн общего L2/DDR — пренебрежимо
-H (hog-поток конвертации на соседнем ядре, ~35 МБ/с через общий L2):
sync-only 1.649 vs 1.652 без него = 0.2%. Гипотеза кэш-контеншна от
потока A ОТВЕРГНУТА — у ядра 1 достаточно ресурса под RS.
Модель p_min (realtime @ 1.92 Msps)
Условие на период кадра: sync(кадр) + sync(шум в паузе) + RS ≤ эфир(кадр+пауза).
| Что на ядре 0 (поток B) | p_min | подтверждение |
|---|---|---|
| sync + RS + conv (однопоток, было) | ~5–6 мс | старая база §12.3: -p 6000 чисто |
| sync + RS (текущий двухпоток) | ~3.4 мс | колено: -p 4000 ещё Overrun, -p 8000 чисто |
| sync only (цель 3-стадии) | ~1.33 мс | вынос RS на ядро 1 |
Вывод: -p 0 — вне досягаемости этого PHY. Реальная цель — 3-стадийный
конвейер (ядро 0: только sync; ядро 1: refill+conv+RS+запись), p_min ≈
1.3–2 мс с запасом → ~4–5× throughput против нынешних 8 мс. Абсолютный
лосслесс на потоке требует либо дешевле PHY (M=32/CP=8, резерв п.1), либо ARQ
(этап дуплекса) — вне текущего этапа.
Неисследованные резервы (если понадобится > 1.92 MSPS)
- M=32/CP=8 — дешевле FFT и эквалайзер; цена: выше доля пилотов,
чувствительнее к CFO. Прогнать бенчмарк с этими параметрами. Единственный
рычаг поднять сам синхронизатор выше 1.65 Msps (для реального
-p 0). Двухпоточный конвейер — вынос конвертации на ядро 1ИЗМЕРЕНО 2026-07-14: конвертация почти бесплатна (85 Msps), вынос дал ~2%, паузу не убрал. Рабочий вариант — 3-стадийный конвейер с выносом RS-декода (26% эфира) на ядро 1: p_min ~1.3–2 мс вместо ~8 мс. См. «Разбивка по стадиям».- Сравнение NEON vs VFP — пересборка без
-mfpu=neon(с-mfpu=vfpv3) даёт цифру вклада векторизации для отчёта.
Как воспроизвести
make bench
scripts/deploy.sh bench <ip_платы>
ssh root@<ip_платы> '/tmp/ofdm_bench 1.92' # штатный duty 0.571 (репро базы)
ssh root@<ip_платы> '/tmp/ofdm_bench -g 0 1.92' # duty 1.0: реальный потолок RX
ssh root@<ip_платы> '/tmp/ofdm_bench -g 0 -H 1.92' # + контеншн второго ядра
# критерий приёмки realtime: "сигнал sync-only" при -g 0 ≥ 1.92 Msps.
# Сейчас 1.65 < 1.92 → realtime без TX-паузы недостижим (см. разбивку выше).
Опции: -g гэп (сэмплов; 0 = сплошные кадры), -P payload (1275=rs8, 1024=none),
-H hog-поток контеншна на соседнем ядре, -c ядро замера, позиц. — цель Msps.