Author SHA1 Message Date
awe 5826c4ff67 deploy examples 2026-10-22 12:57:33 +03:00
awe 09a2aa7932 merge: свести документацию (взять версию с разделом про прошивку STM)
# Conflicts:
#	device_firmware/cart_firmware/docs/protocol.md
2026-10-19 16:16:56 +03:00
awe 75185c7e36 fix 2026-10-19 16:12:36 +03:00
aweandClaude Opus 4.8 5b70a9a4ce cart: документация сетевого моста в protocol.md
Разделы про роли и идентификацию устройства, прошивку STM (сборка/заливка
через ST-Link по SWD, подключения по ролям, проверка роли), кадр USB CDC,
сетевой слой (UDP, seq/новая сессия, HMAC, WireGuard), многоуровневый
failsafe и запуск моста.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-19 13:19:09 +03:00
aweandClaude Opus 4.8 7334f89491 cart: документация сетевого моста в protocol.md
Разделы про роли и идентификацию устройства, кадр USB CDC, сетевой слой
(UDP, seq/новая сессия, HMAC, WireGuard), многоуровневый failsafe и
запуск моста.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-19 13:13:24 +03:00
aweandClaude Opus 4.8 50f07caa64 cart: симметричный ПК-мост через сеть + зеркало протокола
Переносит управление с STM-передатчика на STM-приёмник через локальную
сеть/интернет двумя экземплярами одной программы.

- tools/link_proto.py: зеркало link_protocol.h на Python (кадрирование,
  CRC16-CCITT, (де)сериализация CONTROL/IDENT, инкрементальный парсер с
  ресинхронизацией) + --selftest с тест-векторами.
- tools/bridge.py: роль tx (USB->UDP) и rx (UDP->USB) из одного флага;
  поиск порта по product string и handshake HELLO/IDENT; UDP с seq и
  anti-replay (распознавание новой сессии при рестарте источника);
  сетевой failsafe при тишине; опциональная HMAC-аутентификация пакетов
  и фильтр источника.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-19 13:13:24 +03:00
aweandClaude Opus 4.8 8704db2de7 cart: роли прошивки TX и RX, выбор на этапе сборки
Прошивка получает две роли, выбираемые compile-time env; общий код
собирается в оба env, специфичный — через build_src_filter, без мёртвого
кода неактивной роли. Роль публикуется в USB product string
(CART-TX/CART-RX), по которой ПК находит нужный порт.

- app_tx.cpp: как раньше формирует SBUS на локальный провод и
  дополнительно шлёт те же ch[16] по USB кадром CONTROL; отвечает на
  HELLO кадром IDENT. Старый проводной функционал не изменён.
- app_rx.cpp: принимает CONTROL по USB и гонит SBUS на провод строго
  каждые 50 мс из последнего состояния. FAILSAFE: нет свежего CONTROL
  дольше 150 мс -> нейтраль с флагом failsafe (срабатывает и при обрыве
  USB/зависании ПК); сетевой флаг failsafe транслируется в бит SBUS.
- platformio.ini: env cart_tx/cart_rx, макросы роли и USB_PRODUCT_STRING.
- удалён монолитный main.cpp (логика разнесена по модулям).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-19 13:13:01 +03:00
aweandClaude Opus 4.8 bd9395fc3d cart: общий бинарный протокол связи поверх USB CDC
Единый слой связи пульт<->ПК<->тележка, одинаковый на обеих ролях и
зеркалимый на Python.

- link_protocol.h: единственный источник истины — кадрирование
  (SYNC/VER/TYPE/LEN/PAYLOAD/CRC16-CCITT), типы CONTROL/IDENT/HELLO/LOG,
  layout полезных нагрузок, роли, тайминги failsafe.
- usb_link.{h,cpp}: CRC16, сериализация CONTROL/IDENT, отправка кадров и
  стейт-машина приёма с ресинхронизацией по синхрослову и проверкой CRC.
- device_id.h: чтение 96-битного UID чипа для IDENT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-19 13:12:48 +03:00
aweandClaude Opus 4.8 6f1eaf19e4 cart: вынести SBUS и логику пульта в отдельные модули
Подготовка к ролям TX/RX: выделяю из монолитного main.cpp
переиспользуемые части без изменения поведения.

- sbus.{h,cpp}: упаковка 25-байтового SBUS-кадра, инициализация и
  вывод инвертированного SBUS на USART1 (PA9) — единственный владелец
  провода к драйверу, общий для будущих ролей.
- stick.{h,cpp}: калибровка стика, нормализация осей и профиль
  газа/поворота, формирование ch[16]. Неиспользованные в прежнем loop
  expo()/toSbus() и мёртвые константы не переносятся.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-19 13:12:07 +03:00
aweandClaude Fable 5 7329750c0c sar3d: fixed default scene window X [-2,2], Y [-0.5,2], Z [0,8] m
The grid bounds are now explicit config (sar3d_x/y/z_min/max_m) instead of
being derived from the antenna span and the depth gate. The depth mask
still zeroes voxels outside [min_depth, max_depth], so the wider Z window
just shows the masked wedge. The bound fields are double: the parity tool
recomputes the notebook-derived bounds (notebook_grid_bounds) and passes
them through the live config, and a float roundtrip shifted the axes by
~1e-7 m, flipping depth-mask knife-edge voxels against the reference npz.
Parity on frames 0034-0036 unchanged (1.3e-6, axes bit-match).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-25 17:12:18 +03:00
aweandClaude Fable 5 60f6c9e567 sar3d: chunk the aperture kernel over voxels to survive low free VRAM
Full-volume acc/cnt device buffers (~115 MB for the 9.6M-voxel grid) plus
the CUDA context did not fit next to a desktop session that had taken the
T600's 4 GB down to ~190 MB free, so every frame died with cuMemAlloc OOM
and the GUI showed no sar3d picture. The kernel now processes
[gid_begin, gid_end) ranges and the host launches it in 2M-voxel chunks
reusing one ~24 MB buffer pair. Parity vs the reference volumes is
unchanged (max diff 1.3e-6).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-25 17:01:22 +03:00
aweandClaude Fable 5 e1dfbec712 sar3d: python plumbing for the new parameters and real replay timing
- replay_raw_producer carries the RECORDED per-trace capture windows
  from each snapshot's meta.json (re-anchored by the same shift as the
  frame time) instead of one synthetic window for all traces — without
  this the segmented timing model degenerates in replay;
- GUI/profile/orchestration swap the removed window knobs
  (center_depth, half_width_*, grid_n*) for the notebook parameters
  (ground depth, pitch override, voxel pitch, Y range, X margin, norm
  edge margin, smooth sigma, comp power, tx per segment).

Headless replay smoke on the 20260922_mine_fast_true dataset passes:
51 sar3d result collections, transparent preprocessor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-25 16:42:14 +03:00
aweandClaude Fable 5 b8cb572bc4 sar3d: port the Multi_antenna_3D_clean_v1 notebook algorithm
The old sar3d core was a port of the superseded Horns_3D notebook and
produced wrong volumes. The compute core now follows the reference
notebook exactly:

- segmented_interleaved frame timing rebuilt from per-trace capture
  stamps (uniform tx_per_segment split as the no-stamps fallback), with
  the coarse inter-segment dtau and the intra-sweep dtau applied as one
  phase ramp before the IFFT;
- ground scene coordinates with the ground_depth_m lift (scene Y=0 is
  the ground), full-region grid instead of a window around a center
  depth, geo_ref compensation anchored on the ground plane, power 0.3;
- notebook finalization in C++: |acc/cnt|, depth mask, normalization
  without the Y-edge planes, separable Gaussian over (Z, X), level;
- CUDA kernel computes geometry in double: float32 loses ~3e-4 of an
  A-scan sample in tau and drifts ~1e-4 from the float64 reference.

Live-config keys follow: the center_depth/half_width/grid_n window knobs
are replaced by ground/voxel/y-range/margin/sigma/power/tx_per_segment.

Parity against the notebook npz exports (frames 0034-0036 of the
20260922_mine_fast_true dataset): max|diff| <= 1.6e-6 off the depth-mask
knife edge, identical argmax, level rel diff <= 7e-7. Verified via
make sar3d_parity --dump-dir + scripts/sar3d_volume_compare.py; the
executable spec lives in scripts/sar3d_reference_check.py (bit-tight
against the npz, including the animation shifted-grid arithmetic).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-25 16:42:01 +03:00
aweandClaude Fable 5 e9a0b43ed3 revert gpr heatmap threshold, keep it for sar3d only
The display threshold from 'add treshhold' (e113ef6) was meant for the
sar3d projections; the GPR heatmap goes back to full-range levels and
the constant moves into sar3d_plot_mixin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-25 14:52:20 +03:00
awe ebf0462d85 Revert "move to new version"
This reverts commit e202f57ef8.
2026-09-25 14:51:04 +03:00
awe 243963063c make gpr update 2026-09-21 13:39:04 +03:00
awe f0ad4e6185 jetson compile fix 2026-09-18 16:34:44 +03:00
awe fa9da949df jetsom compile 2026-09-17 19:26:22 +03:00
awe e202f57ef8 move to new version 2026-09-17 17:58:36 +03:00
awe e113ef60ab add treshhold 2026-09-17 16:43:22 +03:00
awe 16ccafff69 test new version 2026-09-16 13:22:21 +03:00
awe b3339d101c projector version 2026-09-15 19:06:05 +03:00
Ayzen 62c0266a6b per-point switching: hardware bring-up, profiles, protocol doc
Verified on the bench that the modified master bitstream excites in every stage
and the RF switch routes: 16 traces per sweep, 62 dB between the two switch
positions of one master port, 9.9k measurements/s. Adds the built master image,
two run profiles, a verification script and librevna_protocol.md, and drops the
stale prototype driver copy that the firmware tree still carried.
2026-09-15 14:41:54 +03:00
Ayzen 3f6cfc9108 switch controller: separate pin per switch, gap histogram
PB2 now drives the switch on master port 1 and PB6 the one on port 2, both
updated in a single BSRR write so they move in the same cycle. The CDC report
gained a gap histogram, which showed the point boundary sits at 70-79 us against
62 us between stages, so the resync threshold only fires on the longer
inter-sweep pauses; the misleading "stages per point" counter is renamed to
match what it actually counts.
2026-09-15 14:41:21 +03:00
AyzenandClaude Opus 5 caac6561c2 fpga: pin placer cost table 2, add the built master bitstream
Ограничение MCU_MOSI OFFSET hold маргинально уже в стоке: запас +0.458 нс.
Поэтому любое изменение нетлиста переставляет размещение и может его опрокинуть.
A/B в ISE: сток - Timing Score 0, версия на маске при cost table 1 - 5480.

Cost table 2 даёт Timing Score 0, причём запас по hold на MCU_MOSI больше
стокового: +0.642 против +0.458 нс. Значение зафиксировано в VNA.xise одной
строкой, иначе следующая сборка снова словит нарушение.

Ресурсы финальной сборки: 2302 LUT против 2189 у стока (40% против 38% ёмкости
xc6slx9). Промежуточные 2182, которые я приводил раньше, были из сборки с
cost table 1 - её перебор потом переделал, и упаковка map изменилась.

Битстрим положен как top_master_per_point_switching.bin, а не поверх
top_master.bin: последний - одна из двух копий невосстановимого битстрима, из
которого собран текущий combined.vnafw.

Документ приведён в согласованное состояние: таблица контракта и описание
Sweep.vhd отставали от версии на маске.

Собрано и проверено на машине сборки: ISE 14.7 в виртуалке, xtclsh в батч-режиме.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 14:18:20 +03:00
AyzenandClaude Opus 5 59c9d4f255 fpga: replace variable shift with a compare mask in port gating
Контрольная сборка стока в ISE показала, что первая версия правки внесла
нарушение тайминга: сток даёт Timing Score 0, а она — 1880 (hold на входе
MCU_MOSI). Причина — shift_right с переменным сдвигом синтезируется в barrel
shifter, а вызывался он четырежды: +142 LUT при +3 регистрах, из-за чего
поехало размещение и пострадала SPI-шина, по которой MCU пишет регистры FPGA.
Сбой там означал бы неверную конфигурацию развёртки.

Теперь младшие биты счётчика стадий просто маскируются, а маска декодируется
из SUBSTAGE_BITS тремя вентилями. Итог: 2182 LUT против 2189 у стока, то есть
логика стала дешевле стоковой. При SUBSTAGE_BITS=0 маска все единицы и
выражение вырождается в стоковое stage_cnt = port_stage буквально.

Заодно контракт стал однозначнее: хост присылает ПЕРВУЮ стадию блока порта, а
не индекс порта. Без под-стадий это то же число, поэтому стоковая совместимость
сохраняется, а зарезервированное "7 = порт выключен" больше не может совпасть с
настоящей стадией (при четырёх под-стадиях маскированная семёрка совпадала с
блоком второго порта).

Из VHDL убраны не-ASCII символы: GHDL отвергает их даже в комментариях.

Проверено: тестбенч проходит все четыре сценария и падает под мутацией
(8 сработавших проверок), 305 python-тестов зелёные, ISE собирает битстрим.
Остаточное нарушение hold на MCU_MOSI ловится подбором cost table размещателя —
ограничение маргинально уже в стоке (запас +0.458 нс).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 14:02:30 +03:00
AyzenandClaude Opus 5 dfb285ab93 add windows install/build scripts for per-point switching
Скрипты, которыми правки ставятся в дерево LibreVNA на сборочной машине и
собирается прошивка MCU мастера её штатным тулчейном (STM32CubeIDE 1.17.0,
gcc 12.3.1). Оригиналы шести изменённых файлов сохраняются в backup_original,
так что откат — обратный xcopy.

Проверено на месте: все 7 файлов легли побайтово (сверил sha256 каждого),
прошивка мастера собралась, эталон предыдущей прошивки остался цел в
extracted/master_stm32.bin.

Битстрим FPGA этими скриптами не собирается — нужна ISE 14.7 в виртуалке,
процедура описана в README.txt и per_point_switching.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 12:48:52 +03:00
AyzenandClaude Opus 5 2412aedc58 switch controller: shorten the gap-minimum window to adapt to dwell changes
Окно минимума было 4096 отсчётов, и это давало опасную асимметрию: при
УМЕНЬШЕНИИ межстадийной паузы минимум падает мгновенно, а при УВЕЛИЧЕНИИ
(подняли dwell_us или задержку PLL прибора) устаревшее маленькое значение живёт
тысячи стадий. Порог оказывался ниже реальной межстадийной паузы, детектор
границы срабатывал на каждой стадии и залипал ключ в положении A на пару
развёрток.

Окно сокращено до 16 отсчётов. Длинное окно не давало точности: пауза задаётся
счётчиком в FPGA и детерминирована, а межстадийная — самая частая (три из
четырёх при четырёх стадиях), так что минимум по десятку отсчётов уже равен ей.
Зато теперь смена настроек подхватывается за считанные точки, и случайная
короткая пауза от помехи вымывается так же быстро.

IFBW на порог не влияет и влиять не может: он меняет длительность оцифровки
(фронт-спад), а меряется пауза (спад-фронт). Плюс ключ переключается по спаду,
когда оцифровка закончена, а детектор границы во время оцифровки неактивен —
boundaryPending_ сбрасывается на фронте. Разобрано в README.

Прошито и проверено: после сброса ядро в loop, Verified OK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 12:37:31 +03:00
AyzenandClaude Opus 5 c1f8654677 review fixes: shared mock capture window, 2-position controller caveat
Мок-бэкенд ставил каждому трейсу свой capture_end_ns, потому что метка бралась
в момент создания трейса. На реальном железе все комбинации коллекции выходят из
одного синхронизированного цикла и делят одно окно — мок теперь ведёт себя так
же, иначе он вводит в заблуждение при чтении записанных меток.

В README контроллера добавлена оговорка: прошивка рассчитана ровно на два
положения ключа. Хост допускает и 4, но тогда положение перестаёт быть
чередованием одного бита и нужен контроллер на две управляющие линии.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 22:15:50 +03:00
AyzenandClaude Opus 5 7d1879ad79 per-point switching: FPGA and LibreVNA MCU changes
FPGA мастера: порт излучает не в одной стадии, а в блоке подряд идущих —
младшие SUBSTAGE_BITS бит счётчика стадий стали положением выходного ключа,
старшие выбирают порт. Вынесено в функцию port_excites в Sweep.vhd, новое поле
приезжает из свободных битов 8:6 регистра SweepSetup через SPIConfig и top.vhd.

Совместимость со стоком строгая: при SUBSTAGE_BITS=0 сдвиг нулевой, а
добавленная проверка "порт-стадия выше последней" ничего не меняет, потому что
счётчик стадий и так пробегает только 0..STAGES. Ручной режим и анализатор
спектра, где STAGES=0, ведут себя ровно как раньше.

MCU: у SetupSweep добавлен параметр substage_bits со значением по умолчанию 0,
поэтому вызовы из Manual.cpp и SpectrumAnalyzer.cpp не тронуты. VNA.cpp берёт
ширину под-стадии из port3Stage — порта 3 у этого железа нет, поле свободно, и
формат пакета не меняется.

Проверено: GHDL анализирует все три VHDL-файла чисто; новый самопроверяющийся
Test_Sweep.vhd прогоняет четыре сценария (сток, два положения, приёмник без
излучения, четыре положения) и проверен мутацией — с возвращённым стоковым
сравнением сценарии с под-стадиями падают. Обе сборки MCU собираются без ошибок.
Битовые позиции сверены сквозь всю цепочку: хост шлёт port3Stage=1 в слове
0x243, MCU пишет регистр 0x7041, FPGA читает SUBSTAGE_BITS=1.

Слейвы перепрошивать не нужно: им шлётся порт-стадия 7, которая со счётчиком
0..3 не совпадает и в стоковом сравнении.

Исходники теперь опережают лежащие рядом бинарники — предупреждение добавлено в
начало reproduce/README.md, хеш README в SHA256SUMS обновлён. Порядок ввода в
строй расписан в per_point_switching.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 22:06:23 +03:00
AyzenandClaude Opus 5 0db3dbe295 switch controller firmware + per-point switching spec
Прошивка STM32 (WeAct G431CB, тот же тулчейн, что у пульта тележки), которая
стоит в разрыве триггерной петли между мастером и первым слейвом и переключает
выходной ключ внутри частотной точки.

Положение ключа — младший бит номера стадии, а период 2 нацело делит 4 стадии,
поэтому чередование не рвётся на границе точки и контроллеру достаточно
инвертировать ногу на каждый цикл триггера. Переключение по СПАДУ: спад означает
«оцифровка закончилась», дальше идёт окно успокоения следующей стадии (~62 мкс
при dwell_us=2), которого ключу хватает с запасом, и кольцо удерживать не надо.

Один потерянный фронт инвертировал бы чётность навсегда и молча поменял антенны
местами, поэтому контроллер распознаёт границу точки по длине паузы (между
точками к успокоению добавляется перезагрузка регистров PLL) и принудительно
ставит положение A — чётность чинится на каждой точке, а не раз на развёртку.
Порог берётся как минимум паузы по скользящему окну с запасом, то есть
подстраивается сам; отдельный провод-маркер начала развёртки не нужен.

Задержку зеркалирования проверил по Sweep.vhd: мастер стартует оцифровку по
своему TRIGGER_IN с конца кольца, то есть тоже после контроллера. Задержка
общая для всех приборов и сокращается в отношении «приёмник/опорник» — фазовой
ошибки не вносит, только чуть удлиняет развёртку.

Собирается: RAM 13.9%, Flash 20.0%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 21:45:50 +03:00
AyzenandClaude Opus 5 6693ece0aa per-point stimulus switching: host side
Матрица 4x4 за одну развёртку: каждый порт мастера измеряется на каждой частоте
дважды, с двумя положениями выходного ключа. Ключ дёргает контроллер в
триггерной петле, поэтому расбери переключателями больше не управляет —
SwitchedMatrixRadarService уходит из тракта, все 16 комбинаций снимаются в
пределах одной точки вместо разброса между тремя развёртками.

Выходы нумеруются позиция-мажорно, стадии порт-мажорно (старшие биты счётчика
стадий выбирают порт, младший — положение ключа). Из этого следует, что
положение ключа = младший бит номера стадии, а период 2 нацело делит 4 стадии —
поэтому контроллеру достаточно инвертировать ногу на каждый цикл триггера, без
абсолютного счётчика. Выходы 0 и 1 при этом сохраняют прежний смысл.

Приёмники слейвов пронумерованы от числа выходов + 1, а не от фиксированного 3:
иначе при 4 выходах прямой член «приёмник 3 <- выход 3» назывался бы так же, как
отражение выхода 3. При двух выходах даёт прежние порты 3..6.

Слейвам порт-стадии выставляются в 7: стоковый приём «одна за последней стадией»
перестаёт работать, когда порт выбирается старшими битами.

Всё за флагом multi_device.stimulus_switch_positions (1 = прежнее поведение,
байт в байт). Спецификация правок FPGA/STM в device_firmware/per_point_switching.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 21:26:56 +03:00
AyzenandClaude Opus 5 d2338ff10c added librevna firmware reproduce kit
Полное дерево LibreVNA v1.6.5 с доработками master/slave, эталонные битстримы
и прошивки STM32, упаковщик .vnafw. Проверено: build.sh пересобирает обе
прошивки побайтово идентично лежащим рядом .vnafw.

Мастер-битстрим (top_master.bin / extracted/master_fpga.bin) больше нигде не
существует — place&route в ISE недетерминирован, из исходников он не
восстанавливается.

Положено в multi_libreVNA_firmware/reproduce, а не в device_firmware/reproduce
как сказано в README комплекта: SHA256SUMS и build.sh ссылаются на
../combined.vnafw, а .vnafw лежат здесь.

Корневые правила .gitignore build*/build/ выбрасывали 482 файла, включая сам
build.sh и эталонные прошивки STM32 из SHA256SUMS — дописано разыгноривание.
Оставшиеся 7 файлов перебивал вложенный апстримный .gitignore (более глубокий
имеет приоритет), они добавлены force-add'ом. Промежуточные .o/.d/.su из
build-каталогов убраны, .bin/.elf/.hex/.map оставлены.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 21:08:02 +03:00
Ayzen 7997abe2d9 added capture time for every sweep 2026-09-03 17:30:19 +03:00
Guriy 8a52431bd3 cart_firmware_settings_changed 2026-08-27 13:53:33 +03:00
Ayzen cc6d189d52 cart_firmware_added 2026-08-26 15:40:10 +03:00
Ayzen 6ada811c2f some fixes 2026-08-26 15:02:21 +03:00
BogatskiyG 74723bb635 Merge branch 'laser-temp-monitoring' into feature/switched-matrix-radar 2026-08-04 14:57:03 +03:00
aweandClaude Opus 4.8 e219f6ec02 Add laser temperature monitoring for current-variation runs
Independent monitor + checker for CHANGE_CURRENT_LD1 variation, where the
board sweeps LD1 current autonomously while both laser temperatures must stay
on their static setpoints. Nothing previously verified that the setpoints were
actually reached, so a stale temperature silently corrupted measurements.

New package python_app/hardware_full/laser_control/monitoring/:
- session.py: LaserVariationSession snapshot (targets + tolerance frozen at start)
- readings_channel.py: JSONL append/tail channel for per-sweep readings
- monitor.py: LaserTemperatureMonitor polls the board once per sweep, publishes
- checker.py: LaserTemperatureChecker validates temp1/temp2 vs targets (0.03 C),
  warns to console per laser with anti-spam state

CLI entry points scripts/laser_temp_monitor.py and scripts/laser_temp_checker.py
run as independent processes communicating via IPC files.

Also: add temp_tolerance_c to LaserVariationModeModel (schema + codec round-trip)
and write the session snapshot from apply_kamil_adc_laser_control's variation path
so the checker works with pipeline-started runs too.

Tests in tests/test_laser_temp_monitoring.py (17 cases); existing laser-control
protocol and config-codec suites remain green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 21:41:14 +03:00
awe 52c1218bf7 Independent monitor + checker for CHANGE_CURRENT_LD1 variation 2026-07-27 16:24:58 +03:00
Ayzen 3efe968dd1 some kamil_adc fixes 2026-07-02 17:44:24 +03:00
Ayzen 42532c9868 added new filtration and fixed processing parameters 2026-06-23 21:55:40 +03:00
Ayzen 4ca4b27246 new libreVNA config 2026-06-23 19:11:59 +03:00
1388 changed files with 660905 additions and 643 deletions
+52 -5
View File
@@ -3,11 +3,14 @@ __pycache__/
*.py[codz]
*$py.class
build*
# build* иначе прячет скрипт сборки вендоренного gpr_cpp (его build/ остаётся игнорируемым)
!data_acq_and_processing/gpr_cpp/build_cmake.sh
.codex
.DS_Store
# C extensions
python_app/data*
*.so
*.ipynb
*.npy
snapshots/
test_results*
@@ -20,8 +23,8 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
/lib/
/lib64/
parts/
sdist/
var/
@@ -85,7 +88,7 @@ target/
# Jupyter Notebook
.ipynb_checkpoints
202609144
# IPython
profile_default/
ipython_config.py
@@ -165,6 +168,25 @@ ENV/
env.bak/
venv.bak/
# Локальные инструменты, распакованные не из пакетного менеджера (GHDL для
# анализа и симуляции VHDL, clangd). Ставятся скриптом, в репозиторий не идут.
.tools/
# База компиляции для clangd: абсолютные пути этой машины.
# Генерируется .claude/tools/gen_compile_commands.sh из Makefile.
compile_commands.json
# Граф знаний graphify: 22 МБ, пересобирается из кода за минуту.
# Список исключений для него — в .graphifyignore, он тоже не версионируется
# (см. ниже, вместе с остальной обвязкой Claude Code).
graphify-out/
# Payload комплекта установки на Windows: копия файлов из дерева LibreVNA.
# Генерируется install_windows/make_files.sh; в git не идёт, чтобы не разойтись
# с деревом (уже расходился).
device_firmware/multi_libreVNA_firmware/reproduce/install_windows/files/
device_firmware/multi_libreVNA_firmware/reproduce/install_windows/backup_original/
# Spyder project settings
.spyderproject
.spyproject
@@ -225,6 +247,31 @@ __marimo__/
python_app/runtime
SHARE_INTERNET_TO_PI.md
# Локальная обвязка Claude Code: инструкции, хуки, скилы, конфиги инструментов.
# В репозиторий не идёт.
CLAUDE.md
docs/
test_end_2/
.claude/
.graphifyignore
pyrightconfig.json
/docs/
test_end_2/
# --- device_firmware: PlatformIO / STM32G431 cart remote ---
# build output (regenerated by `pio run`)
device_firmware/cart_firmware/.pio/
device_firmware/switch_controller/.pio/
# scope captures: raw .bin + .npz + preview .png, local-only
device_firmware/cart_firmware/captures/
# machine-specific, regenerated by the PlatformIO extension
device_firmware/cart_firmware/.vscode/c_cpp_properties.json
device_firmware/cart_firmware/.vscode/launch.json
device_firmware/cart_firmware/.vscode/ipch/
device_firmware/cart_firmware/.vscode/.browse.c_cpp.db*
# --- device_firmware: комплект воспроизведения прошивок LibreVNA ---
# Корневые `build*` / `build/` иначе молча выбрасывают сам build.sh и эталонные
# прошивки STM32, на которые ссылается reproduce/SHA256SUMS. Каталог сборки
# приходится разыгнорить целиком: git не умеет возвращать файл, у которого
# исключён родительский каталог.
!device_firmware/multi_libreVNA_firmware/reproduce/build.sh
!device_firmware/multi_libreVNA_firmware/reproduce/LibreVNA/Software/VNA_embedded/build/
!device_firmware/multi_libreVNA_firmware/reproduce/LibreVNA/Software/VNA_embedded/build_external_lo_slave/
+3
View File
@@ -0,0 +1,3 @@
{
"cmake.sourceDirectory": "/home/awe/Documents/radar_system/data_acq_and_processing/gpr_cpp"
}
+95 -12
View File
@@ -8,7 +8,43 @@ VISA_LDFLAGS := $(LDFLAGS) -lvisa
BUILD_DIR := build
BIN_DIR := $(BUILD_DIR)/bin
# --- gpr_cpp: vendored C++/CUDA 3D SAR library (originally from VNA_AlGo).
# Built automatically by the $(GPR_CPP_LIB) rule below via its own CMake
# (build_cmake.sh). CUDA headers and libnvrtc come from the pip nvidia-*
# packages inside this repo's venv (same detection as build_cmake.sh); when
# those are absent (e.g. Jetson, where CUDA comes with JetPack) the system
# toolkit at /usr/local/cuda is used. libcuda is always the system driver.
GPR_CPP_DIR ?= data_acq_and_processing/gpr_cpp
GPR_CPP_LIB ?= $(GPR_CPP_DIR)/build/libgpr_core_cpp.a
# Absolute so the derived -rpath (libnvrtc dir) survives running the binary
# from outside the repo root.
GPR_VENV ?= $(CURDIR)/.venv
GPR_CUDA_INC_DIR ?= $(dir $(firstword $(wildcard $(GPR_VENV)/lib/python*/site-packages/nvidia/*/include/nvrtc.h)))
# cuda.h can live in a DIFFERENT pip package than nvrtc.h (Jetson: cuda_runtime
# vs cuda_nvrtc); the unified cu13 package on dev machines has both in one dir.
GPR_CUDA_RT_INC_DIR ?= $(dir $(firstword $(wildcard $(GPR_VENV)/lib/python*/site-packages/nvidia/*/include/cuda.h)))
GPR_NVRTC_LIB ?= $(firstword $(wildcard $(GPR_VENV)/lib/python*/site-packages/nvidia/*/lib/libnvrtc.so*))
# System-CUDA fallback. An empty GPR_CUDA_INC_DIR would also corrupt the compile
# command: a bare `-I` swallows the next argument.
ifeq ($(strip $(GPR_CUDA_INC_DIR)),)
GPR_CUDA_INC_DIR := /usr/local/cuda/include
endif
ifeq ($(strip $(GPR_CUDA_RT_INC_DIR)),)
GPR_CUDA_RT_INC_DIR := /usr/local/cuda/include
endif
ifeq ($(strip $(GPR_NVRTC_LIB)),)
GPR_NVRTC_LIB := $(firstword $(wildcard /usr/local/cuda/lib64/libnvrtc.so*))
endif
# Deduplicated CUDA include flags (a single dir on dev machines).
GPR_CUDA_INC_FLAGS := $(addprefix -I,$(sort $(GPR_CUDA_INC_DIR) $(GPR_CUDA_RT_INC_DIR)))
GPR_CUDA_LIB_DIR := $(dir $(GPR_NVRTC_LIB))
# libnvrtc from pip ships only as libnvrtc.so.N (no .so symlink), so link the
# full path; the rpath makes the binary find it without LD_LIBRARY_PATH.
DATA_PROCESSOR_LDFLAGS := $(LDFLAGS) $(GPR_CPP_LIB) $(GPR_NVRTC_LIB) -lcuda -Wl,-rpath,$(GPR_CUDA_LIB_DIR)
INCLUDES := \
-I$(GPR_CPP_DIR)/include \
$(GPR_CUDA_INC_FLAGS) \
-Idata_acq_and_processing/common_cpp/ipc/include \
-Idata_acq_and_processing/common_cpp/config/include \
-Idata_acq_and_processing/third_party \
@@ -55,6 +91,7 @@ PROCESSOR_SOURCES := \
data_acq_and_processing/processing/processors/src/bscan_processor.cpp \
data_acq_and_processing/processing/processors/src/gpr_processor.cpp \
data_acq_and_processing/processing/processors/src/passthrough_processor.cpp \
data_acq_and_processing/processing/processors/src/sar3d_processor.cpp \
data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp \
data_acq_and_processing/processing/data_processor/src/data_processor.cpp \
data_acq_and_processing/processing/data_processor/src/main.cpp \
@@ -65,17 +102,36 @@ PROCESSOR_SOURCES := \
# from the vendored headers alone and only needs those libraries present at run
# time. Built on demand (not part of `all`) for radar.model == kamil_adc.
KAMIL_COLLECTOR_DIR := data_acq_and_processing/kamil_adc_collector
KAMIL_COLLECTOR_INCLUDES := -I$(KAMIL_COLLECTOR_DIR)/include -I$(KAMIL_COLLECTOR_DIR)/vendor/lcard
# The collector now also drives the RF switches itself (switch-aware mode), so it
# reuses the orchestrator's switch drivers and the shared run_config loader, which
# pull in the common config/ipc/locator headers and the vendored nlohmann/json.
KAMIL_COLLECTOR_INCLUDES := \
-I$(KAMIL_COLLECTOR_DIR)/include \
-I$(KAMIL_COLLECTOR_DIR)/vendor/lcard \
-Idata_acq_and_processing/common_cpp/ipc/include \
-Idata_acq_and_processing/common_cpp/config/include \
-Idata_acq_and_processing/sweep_orchestrator/device_drivers/interfaces \
-Idata_acq_and_processing/sweep_orchestrator/device_drivers/switches \
-Idata_acq_and_processing/processing/locator/include \
-Idata_acq_and_processing/third_party
KAMIL_COLLECTOR_LDFLAGS := -pthread -ldl -lutil
# Collector sources, including the shared drivers/config loader it reuses. These
# are compiled into a private build/kamil/ tree (see rule below) so they never
# collide with the orchestrator's objects, which use different compile flags.
KAMIL_COLLECTOR_SOURCES := \
$(KAMIL_COLLECTOR_DIR)/src/main.cpp \
$(KAMIL_COLLECTOR_DIR)/src/tty_protocol_writer.cpp \
$(KAMIL_COLLECTOR_DIR)/src/capture_file_writer.cpp
$(KAMIL_COLLECTOR_DIR)/src/capture_file_writer.cpp \
data_acq_and_processing/common_cpp/config/src/run_config.cpp \
data_acq_and_processing/common_cpp/ipc/src/shared_types.cpp \
data_acq_and_processing/sweep_orchestrator/device_drivers/switches/h7992_minimal_driver.cpp \
data_acq_and_processing/sweep_orchestrator/device_drivers/switches/hmc349a_minimal_driver.cpp
SWEEP_ORCH_OBJS := $(addprefix $(BUILD_DIR)/,$(COMMON_SOURCES:.cpp=.o) $(ORCH_SOURCES:.cpp=.o))
PREPROCESSOR_OBJS := $(addprefix $(BUILD_DIR)/,$(COMMON_SOURCES:.cpp=.o) $(PREPROC_SOURCES:.cpp=.o))
DATA_PROCESSOR_OBJS := $(addprefix $(BUILD_DIR)/,$(COMMON_SOURCES:.cpp=.o) $(PROCESSOR_SOURCES:.cpp=.o))
KAMIL_COLLECTOR_OBJS := $(addprefix $(BUILD_DIR)/,$(KAMIL_COLLECTOR_SOURCES:.cpp=.o))
KAMIL_BUILD_DIR := $(BUILD_DIR)/kamil
KAMIL_COLLECTOR_OBJS := $(addprefix $(KAMIL_BUILD_DIR)/,$(notdir $(KAMIL_COLLECTOR_SOURCES:.cpp=.o)))
DEPFILES := $(sort $(SWEEP_ORCH_OBJS:.o=.d) $(PREPROCESSOR_OBJS:.o=.d) $(DATA_PROCESSOR_OBJS:.o=.d) $(KAMIL_COLLECTOR_OBJS:.o=.d))
TARGETS := \
@@ -83,12 +139,27 @@ TARGETS := \
$(BIN_DIR)/data_preprocessor \
$(BIN_DIR)/data_processor
.PHONY: all clean kamil_adc_collector
.PHONY: all clean kamil_adc_collector sar3d_parity
all: $(TARGETS)
kamil_adc_collector: $(BIN_DIR)/kamil_adc_collector
# On-demand parity check of the sar3d processor against the reference notebook
# volumes (scripts/sar3d_volume_compare.py diffs the dumped volume against a
# *_volume.npz export). Not part of `all`.
SAR3D_PARITY_OBJS := $(addprefix $(BUILD_DIR)/, \
data_acq_and_processing/processing/tools/sar3d_parity_main.o \
data_acq_and_processing/processing/processors/src/sar3d_processor.o \
data_acq_and_processing/common_cpp/config/src/run_config.o \
data_acq_and_processing/common_cpp/ipc/src/shared_types.o)
sar3d_parity: $(BIN_DIR)/sar3d_parity
$(BIN_DIR)/sar3d_parity: $(SAR3D_PARITY_OBJS) $(GPR_CPP_LIB)
@mkdir -p $(BIN_DIR)
$(CXX) $(SAR3D_PARITY_OBJS) -o $@ $(DATA_PROCESSOR_LDFLAGS)
$(BIN_DIR)/sweep_orchestrator: $(SWEEP_ORCH_OBJS)
@mkdir -p $(BIN_DIR)
$(CXX) $(SWEEP_ORCH_OBJS) -o $@ $(ORCH_LDFLAGS)
@@ -97,15 +168,27 @@ $(BIN_DIR)/data_preprocessor: $(PREPROCESSOR_OBJS)
@mkdir -p $(BIN_DIR)
$(CXX) $(PREPROCESSOR_OBJS) -o $@ $(LDFLAGS)
$(BIN_DIR)/data_processor: $(DATA_PROCESSOR_OBJS)
$(BIN_DIR)/data_processor: $(DATA_PROCESSOR_OBJS) $(GPR_CPP_LIB)
@mkdir -p $(BIN_DIR)
$(CXX) $(DATA_PROCESSOR_OBJS) -o $@ $(LDFLAGS)
$(CXX) $(DATA_PROCESSOR_OBJS) -o $@ $(DATA_PROCESSOR_LDFLAGS)
# The collector is self-contained: compile its objects with only the vendored
# L-Card headers (no project/VISA includes) by overriding the generic rule's
# variables for these objects, then link with dlopen/openpty support.
$(KAMIL_COLLECTOR_OBJS): INCLUDES := $(KAMIL_COLLECTOR_INCLUDES)
$(KAMIL_COLLECTOR_OBJS): VISA_CXXFLAGS :=
# Vendored gpr_cpp static lib: delegate to its CMake build (also produces the
# standalone_bp example used by sar3d_parity cross-checks).
GPR_CPP_SRC_FILES := $(wildcard $(GPR_CPP_DIR)/src/*) $(wildcard $(GPR_CPP_DIR)/include/gpr/*) \
$(wildcard $(GPR_CPP_DIR)/examples/*) $(GPR_CPP_DIR)/CMakeLists.txt $(GPR_CPP_DIR)/build_cmake.sh
$(GPR_CPP_LIB): $(GPR_CPP_SRC_FILES)
GPR_BUILD_PYTHON=OFF VENV=$(abspath $(GPR_VENV)) bash $(GPR_CPP_DIR)/build_cmake.sh
# The collector compiles into its own build/kamil/ tree with collector-only
# includes (no VISA), so the shared driver/config sources it reuses never collide
# with the orchestrator's objects of the same name. Basenames are unique, so a
# vpath lets one pattern rule find every source.
vpath %.cpp $(sort $(dir $(KAMIL_COLLECTOR_SOURCES)))
$(KAMIL_BUILD_DIR)/%.o: %.cpp
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) $(KAMIL_COLLECTOR_INCLUDES) -c $< -o $@
$(BIN_DIR)/kamil_adc_collector: $(KAMIL_COLLECTOR_OBJS)
@mkdir -p $(BIN_DIR)
@@ -118,4 +201,4 @@ $(BUILD_DIR)/%.o: %.cpp
-include $(DEPFILES)
clean:
rm -rf $(BUILD_DIR)
rm -rf $(BUILD_DIR) $(GPR_CPP_DIR)/build
+90 -3
View File
@@ -60,7 +60,7 @@ controls plus a pixel-identical live plot through an embedded web UI.
| Path | Contents |
| --- | --- |
| `data_acq_and_processing/` | C++ pipeline: `sweep_orchestrator/`, `preprocessing/`, `processing/` (including `locator/`), shared `common_cpp/` (config, IPC rings), and vendored `third_party/` (Eigen, nlohmann/json). |
| `data_acq_and_processing/` | C++ pipeline: `sweep_orchestrator/`, `preprocessing/`, `processing/` (including `locator/`), shared `common_cpp/` (config, IPC rings), the vendored `gpr_cpp/` C++/CUDA 3D SAR library, and vendored `third_party/` (Eigen, nlohmann/json). |
| `python_app/` | Python application: `gui/`, `orchestration/` (process supervisor, shared-memory readers, config), `hardware_full/` (device drivers and services), `webui/`, `storage/`, `models/`, `workflows/`, `scripts/` (producers and tools), and `tests/`. |
| `run_config_examples/` | Ready-made configuration profiles, one per hardware setup. |
| `deploy/` | systemd daemon installer for Raspberry Pi appliances. |
@@ -69,9 +69,18 @@ controls plus a pixel-identical live plot through an embedded web UI.
## Requirements
- Linux (POSIX shared memory; GPIO on Raspberry Pi).
- Linux (POSIX shared memory; GPIO on Raspberry Pi). The deployment target is a
**Raspberry Pi 5**; hardware PWM gating for the generator sweep is Pi 5
specific (GPIO12/13/18/19, `dtoverlay=pwm-2chan`).
- A C++20 compiler and `make` — Eigen and nlohmann/json are vendored, so no extra C++ dependencies are needed.
- Python 3.12 with a virtual environment.
- **Python 3.10 or newer**, in a virtual environment.
- For the `sar3d` processor (3D SAR): an NVIDIA GPU with the driver installed
(`libcuda`) and `cmake` (the vendored `gpr_cpp` library at
`data_acq_and_processing/gpr_cpp/` builds via its own CMake, invoked
automatically by `make`). CUDA headers/libnvrtc are taken from the pip
`nvidia-*` packages in this repo's venv (installed by `requirements.txt` on
x86_64), otherwise from the system toolkit at `/usr/local/cuda`
(Jetson/JetPack, apt `cuda-toolkit`).
## Build and run
@@ -84,6 +93,21 @@ python3 -m venv .venv
make # build the C++ binaries into build/bin/
```
`make` also builds the vendored 3D SAR library
(`data_acq_and_processing/gpr_cpp/`, CMake + NVRTC — needs `cmake`; CUDA
headers come from the pip `nvidia-*` packages in `.venv`, or `/usr/local/cuda`
as a fallback).
To verify the sar3d processor against the gpr_cpp reference on the recorded
dataset, build and run the on-demand parity tool:
```bash
make sar3d_parity
./build/bin/sar3d_parity <preprocessed_dataset_dir> <center_snapshot_name>
# compare the printed peak/argmax with
# ./data_acq_and_processing/gpr_cpp/build/standalone_bp on the same arguments
```
Run the desktop GUI:
```bash
@@ -97,6 +121,69 @@ Run the desktop GUI:
`./start.sh --help`. The acquisition device (LibreVNA, Kamil ADC, …) is detected
automatically from the active config's `radar.model` — there is no per-device flag.
## Jetson (aarch64) bring-up
Ubuntu 22.04 on Jetson has glibc 2.35, and PyQt6 6.8+ ships only
`manylinux_2_39` aarch64 wheels — `requirements.txt` therefore pins PyQt6 to
`<6.8` on aarch64 (6.7.1 is the last release with a compatible wheel). With
that pin the stock `./start.sh` provisioning works; the extra steps are only
for the CUDA-backed `sar3d` processor:
```bash
# 1. copy the replay dataset (for dataset-replay profiles) and point
# radar.replay.dataset_dir in the profile at the local copy — the path in
# run_config_examples/*.json is machine-specific.
# 2. normal launch; the vendored gpr_cpp builds against the JetPack system CUDA
# (requirements.txt installs no pip nvidia-* packages on aarch64, and
# build_cmake.sh falls back to /usr/local/cuda automatically; override with
# CUDA_INC=... CUDA_LIB=... if the toolkit lives elsewhere):
./start.sh --profile run_config_examples/run_config_replay_vna.example.json
```
If linking fails with `cannot find -lcuda`, the driver stub lives at
`/usr/local/cuda/lib64/stubs/libcuda.so`; on JetPack the real driver is
`/usr/lib/aarch64-linux-gnu/nvidia/libcuda.so.1` (present after boot).
## Running without hardware (emulation)
Two ways to run the full pipeline with no radar attached:
**Mock producer (synthetic data).** The default `run_config.json` already sets
`radar.driver_mode: "mock"`, so the producer generates synthetic S-parameters.
The preprocessor still requires S21 calibration/reference sets; create them once:
```bash
.venv/bin/python -m python_app.scripts.manual_smoke_run --duration 1.0 # creates smoke_cal/smoke_ref
./start.sh # then in the GUI: Preprocessing Panel -> pick the sets -> Start
```
(Alternatively use the GUI's "Create neutral sets" button, which makes
identity calibration + zero reference so preprocessing is a pass-through.)
**Dataset replay (recorded scene, meaningful sar3d picture).** When
`radar.replay.dataset_dir` is set in the profile, the supervisor launches
`python_app.scripts.replay_raw_producer` instead of the normal producer: it
replays recorded snapshots into the raw ring at the recorded cadence, looping
forever with a monotonic timeline. Pair it with NEUTRAL preprocess sets so the
already-preprocessed recording passes through the preprocessor bit-exactly.
A ready profile for the VNA_AlGo tank005 dataset is provided:
```bash
# one-time: neutral sets for this profile (or use the GUI button)
.venv/bin/python -m python_app.scripts.create_neutral_preprocess_sets \
--config run_config_examples/run_config_replay_vna.example.json
# interactive GUI (mode sar3d is pre-selected by the profile's gui section):
./start.sh --profile run_config_examples/run_config_replay_vna.example.json
# or a fully headless end-to-end check:
.venv/bin/python -m python_app.scripts.replay_smoke_run --duration 30
```
Note: the sar3d processor allocates GPU memory; if the desktop (e.g. a browser)
has filled the GPU, sar3d logs `out of memory` and publishes empty results until
memory frees. Reduce `sar3d_grid_nx/ny/nz` in the processing panel to shrink
the footprint.
The web interface is served in-process at `http://<host>:8080`. It mirrors the live
plot and offers Start / Stop / Capture controls and the active processor settings.
@@ -38,6 +38,13 @@ struct SweepTraceBlock {
std::vector<Complex32> s11{};
// Complex S21 samples for matching frequency points.
std::vector<Complex32> s21{};
// Monotonic window in which THIS trace's sweep was measured, excluding the
// switch drive and settling that preceded it. In a switched matrix the
// collection is assembled combo by combo over many milliseconds, so the
// collection-level window says nothing about when an individual combo was
// measured. Zero on both is a valid "unmeasured" sentinel.
std::uint64_t capture_start_ns = 0;
std::uint64_t capture_end_ns = 0;
};
struct RawSweepCollection {
@@ -172,6 +172,10 @@ void require_count_fits(std::uint32_t count, std::size_t min_bytes_each, BinaryR
return trace;
}
// The capture windows live in a TRAILER after the trace blocks rather than inside
// them, so a reader built before they existed still decodes every trace and simply
// stops early. The trailer grows the same way: collection window first, then the
// per-trace window table (one pair per trace, in trace order).
void write_trace_collection(BinaryWriter& writer, std::uint32_t magic, const RawSweepCollection& collection) {
writer.write(magic);
writer.write(collection.collection_id);
@@ -184,6 +188,12 @@ void write_trace_collection(BinaryWriter& writer, std::uint32_t magic, const Raw
writer.write(collection.capture_start_ns);
writer.write(collection.capture_end_ns);
writer.write(checked_count_to_u32(collection.traces.size(), "Trace capture window count"));
for (const auto& trace : collection.traces) {
writer.write(trace.capture_start_ns);
writer.write(trace.capture_end_ns);
}
}
[[nodiscard]] auto read_trace_collection(BinaryReader& reader, std::uint32_t expected_magic) -> RawSweepCollection {
@@ -204,16 +214,31 @@ void write_trace_collection(BinaryWriter& writer, std::uint32_t magic, const Raw
collection.traces.push_back(read_trace_block(reader));
}
// Each trailer stage is optional: a payload from an older producer stops after
// the trace blocks (or after the collection window) and leaves the rest zeroed.
if (reader.remaining_bytes() == 0U) {
return collection;
}
if (reader.remaining_bytes() != (sizeof(std::uint64_t) * 2U)) {
throw std::runtime_error("Unexpected trailing bytes in trace collection");
if (reader.remaining_bytes() < (sizeof(std::uint64_t) * 2U)) {
throw std::runtime_error("Truncated capture window in trace collection");
}
collection.capture_start_ns = reader.read<std::uint64_t>();
collection.capture_end_ns = reader.read<std::uint64_t>();
if (reader.remaining_bytes() == 0U) {
return collection;
}
const auto trace_time_count = reader.read<std::uint32_t>();
if (trace_time_count != collection.traces.size()) {
throw std::runtime_error("Per-trace capture window count does not match trace count");
}
for (auto& trace : collection.traces) {
trace.capture_start_ns = reader.read<std::uint64_t>();
trace.capture_end_ns = reader.read<std::uint64_t>();
}
return collection;
}
@@ -188,7 +188,7 @@ auto ShmRing::open_or_create(
const bool geometry_ok = header->capacity == capacity && header->slot_size_bytes == slot_size_bytes;
if (magic_ok && version_ok && geometry_ok) {
// Fix #50: the requested mapped_size matched the header geometry, but the
// the requested mapped_size matched the header geometry, but the
// backing file may have been created undersized by another process. Confirm
// st_size covers the geometry before trusting the mapping.
struct stat info {};
@@ -257,7 +257,7 @@ auto ShmRing::open_existing(const std::string& name) -> ShmRing {
if (header->version != kRingVersion) {
throw std::runtime_error("Shared memory ring version mismatch for " + name);
}
// Fix #50: ensure the mapping actually spans every slot the header describes.
// ensure the mapping actually spans every slot the header describes.
validate_geometry(*header, mapped_size, name);
ShmRing ring{};
@@ -411,7 +411,7 @@ void ShmRing::validate_name(const std::string& name) {
}
void ShmRing::validate_geometry(const Header& header, std::size_t mapped_size, const std::string& name) {
// Fix #50: derive the expected size from the header's own geometry fields and
// derive the expected size from the header's own geometry fields and
// require the real mapping to cover it. A bogus capacity/slot_size or a truncated
// mapping would otherwise yield out-of-bounds slot offsets and a SIGSEGV.
const std::uint32_t capacity = header.capacity;
@@ -0,0 +1,78 @@
# 3.22 = apt cmake на Ubuntu 22.04 (Jetson/JetPack); фич новее здесь нет.
cmake_minimum_required(VERSION 3.22)
project(gpr_cpp LANGUAGES CXX) # CXX-only: CUDA компилируется в рантайме через NVRTC
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
set(CMAKE_CXX_FLAGS_RELEASE "-O3")
# bindings/pymodule.cpp не вендорен в radar_system — включение потребует вернуть его из VNA_AlGo
option(GPR_BUILD_PYTHON "Build pybind11 module" OFF)
option(GPR_BUILD_TESTS "Build C++ parity tests" OFF)
option(GPR_BUILD_EXAMPLES "Build standalone C++ example" ON)
# Python окружение (venv) — источник pybind11 и pip CUDA-библиотек (nvrtc, cudart).
if(NOT DEFINED PYTHON_EXECUTABLE)
set(PYTHON_EXECUTABLE python3)
endif()
# CUDA include/lib из pip-пакетов nvidia-*; задаются -DGPR_CUDA_INC / -DGPR_CUDA_LIB
# (детект делает build_cmake.sh). Заголовки: nvrtc.h, cuda.h (Driver API).
# GPR_CUDA_INC может быть СПИСКОМ каталогов (через ';'): в раздельных pip-пакетах
# (Jetson: cuda_nvrtc + cuda_runtime) nvrtc.h и cuda.h лежат в разных include/.
if(NOT DEFINED GPR_CUDA_INC OR NOT DEFINED GPR_CUDA_LIB)
message(FATAL_ERROR "Задайте -DGPR_CUDA_INC=<dir(ы) с nvrtc.h,cuda.h; ';'-список> и -DGPR_CUDA_LIB=<dir с libnvrtc.so*>")
endif()
# libcuda (драйвер) — системный
find_library(CUDA_DRIVER_LIB cuda PATHS /usr/lib/x86_64-linux-gnu /usr/lib/aarch64-linux-gnu
/usr/lib REQUIRED)
# libnvrtc из pip
find_library(NVRTC_LIB NAMES nvrtc libnvrtc.so.13 libnvrtc.so.12 nvrtc.so
PATHS ${GPR_CUDA_LIB} REQUIRED NO_DEFAULT_PATH)
# PocketFFT (header-only, vendored)
add_library(pocketfft INTERFACE)
target_include_directories(pocketfft INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/third_party/pocketfft)
# --- ядро библиотеки (usable без Python) ---
add_library(gpr_core_cpp STATIC
src/geometry.cpp
src/ascan.cpp
src/device.cpp
src/nvrtc_kernel.cpp
src/sar3d.cpp)
target_include_directories(gpr_core_cpp PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${GPR_CUDA_INC})
target_link_libraries(gpr_core_cpp PUBLIC pocketfft ${NVRTC_LIB} ${CUDA_DRIVER_LIB})
# rpath на каталог pip libnvrtc — чтобы бинарник/модуль грузились без LD_LIBRARY_PATH
set_target_properties(gpr_core_cpp PROPERTIES POSITION_INDEPENDENT_CODE ON
BUILD_RPATH "${GPR_CUDA_LIB}" INSTALL_RPATH "${GPR_CUDA_LIB}")
if(GPR_BUILD_PYTHON)
execute_process(COMMAND ${PYTHON_EXECUTABLE} -m pybind11 --cmakedir
OUTPUT_VARIABLE PB11_DIR OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE PB11_RC)
if(PB11_RC EQUAL 0)
find_package(pybind11 CONFIG REQUIRED HINTS ${PB11_DIR})
else()
find_package(pybind11 CONFIG REQUIRED)
endif()
pybind11_add_module(_gpr_cpp bindings/pymodule.cpp)
target_link_libraries(_gpr_cpp PRIVATE gpr_core_cpp)
set_target_properties(_gpr_cpp PROPERTIES BUILD_RPATH "${GPR_CUDA_LIB}" INSTALL_RPATH "${GPR_CUDA_LIB}")
endif()
if(GPR_BUILD_TESTS)
add_executable(gpr_test_parity tests/cpp/test_parity.cpp)
target_link_libraries(gpr_test_parity PRIVATE gpr_core_cpp)
endif()
if(GPR_BUILD_EXAMPLES)
add_executable(standalone_bp examples/standalone_bp.cpp)
target_link_libraries(standalone_bp PRIVATE gpr_core_cpp)
set_target_properties(standalone_bp PROPERTIES BUILD_RPATH "${GPR_CUDA_LIB}" INSTALL_RPATH "${GPR_CUDA_LIB}")
endif()
+85
View File
@@ -0,0 +1,85 @@
# gpr_cpp — C++/CUDA счёт-ядро MIMO GPR 3D SAR
> Инфраструктура (сборка, NVRTC, PocketFFT, npy) вендорена из репозитория
> VNA_AlGo (`gpr_cpp/`, коммит `544dc59`) без python-биндингов
> (`GPR_BUILD_PYTHON=OFF`). АЛГОРИТМ с сентября 2026 переписан: это порт
> эталонного ноутбука `Multi_antenna_3D_clean_v1.ipynb` (ячейки 0 и 4), а не
> старого `gpr_core`/Horns_3D. Паритет с ноутбуком проверяется инструментом
> `sar3d_parity` + `python_app/scripts/sar3d_volume_compare.py` по
> референсным `*_volume.npz`. Собирается из корневого Makefile radar_system
> (цель `$(GPR_CPP_LIB)` вызывает `build_cmake.sh`).
## Что внутри
**В C++ (`libgpr_core_cpp`):** сегментированный тайминг кадра по per-trace
меткам захвата (`compute_pair_timestamps`), препроцессинг A-сканов (bg-subtract,
полная фазовая коррекция движения intra+pair до IFFT, k0-вставка + Ханн + IFFT
через PocketFFT, нормировка пар), полнообластная ground-сетка
(`build_full_region_grid`), 3D SAR апертурный объём (одно CUDA-ядро на всю
апертуру; геометрия в double, A-сканы float32) и финализация
(`finish_volume`: |acc/cnt| → маска глубины → нормировка без Y-краёв → гаусс
по (Z,X) → нормировка; возвращает объём [0,1] + level).
## Ключевое решение: CUDA без nvcc (NVRTC + Driver API)
На целевых машинах (x86+T600, Jetson) нет полного CUDA toolkit — только runtime +
**NVRTC** (как у cupy). Поэтому ядро **не компилируется nvcc'ом**: его исходник
встроен строкой ([src/kernel_src.hpp](src/kernel_src.hpp)), компилируется в рантайме
через NVRTC в **cubin под точную арх устройства** (запрос compute capability →
`sm_XY`, без JIT PTX), грузится и запускается через **CUDA Driver API**. Хост-часть
собирается обычным g++. Плюс: без toolkit и без привязки к SM — один код для
x86 (sm_75) и Jetson (sm_87).
## Сборка
```bash
bash build_cmake.sh # авто-детект pip CUDA-путей; собирает всё
# артефакты в build/: libgpr_core_cpp.a, _gpr_cpp*.so, standalone_bp
```
Требуется: g++ ≥13, cmake ≥3.24, pybind11 (`pip install pybind11`), pip-пакеты
`nvidia-*` (nvrtc+cudart, идут с cupy), системный `libcuda`. rpath на каталог
libnvrtc прописан — бинарники/модуль грузятся без `LD_LIBRARY_PATH`.
## Публичный C++ API ([include/gpr/](include/gpr/))
```cpp
#include "gpr/config.hpp" // GeometryConfig/BPConfig/MotionConfig/SARConfig (дефолты = ноутбук)
#include "gpr/ascan.hpp" // compute_pair_timestamps, preprocess_snapshot(...) -> AScanSet
#include "gpr/sar3d.hpp" // SarEngine::aperture_volume -> VolumeRaw; finish_volume -> FinishedVolume
#include "gpr/geometry.hpp" // frame_offsets, frame_weights, build_full_region_grid
gpr::SarEngine engine(geom, bp, mot, sar); // компилит ядро под устройство
auto raw = engine.aperture_volume(frames, grid); // один запуск ядра на апертуру
auto fin = gpr::finish_volume(raw, grid, mot.pitch_deg, bp, sar); // объём [0,1] + level
```
Пример без Python: [examples/standalone_bp.cpp](examples/standalone_bp.cpp) — грузит
S21 из `.npy`, строит объём, печатает максимум.
```bash
./build/standalone_bp <preprocessed_dir> 0038_id39_ns11361438137486
```
## Python-модуль
```python
import _gpr_cpp # из gpr_cpp/build
eng = _gpr_cpp.SarEngine(_gpr_cpp.GeometryConfig(), _gpr_cpp.BPConfig(),
_gpr_cpp.MotionConfig(), _gpr_cpp.SARConfig())
a = _gpr_cpp.preprocess_snapshot(s21, freq, background, mot, geom, bp, True) # -> AScanSet
acc, cnt = eng.aperture_volume([(a, dx, dy, dz, w), ...], gx, gy, gz) # numpy
```
## Паритет с ноутбуком (проверено на x86 T600, датасет 20260922_mine_fast_true)
Кадры 0034/0035/0036 против референсных `*_volume.npz` ноутбука
(`make sar3d_parity` + `python_app/scripts/sar3d_volume_compare.py`):
max|Δ| ≤ 1.6e-6 вне ножевой границы маски глубины, argmax и level совпадают
(level rel ≤ 7e-7). Python-эталон той же цепочки:
`python_app/scripts/sar3d_reference_check.py`.
## Статус
- [x] x86 (T600, sm_75, CUDA 13): собрано, паритет, standalone работает.
- [ ] Jetson (Orin NX, sm_87, CUDA 12.6): порт тривиален (NVRTC сам берёт sm_87);
собрать `build_cmake.sh` на Jetson с его venv.
- Опц.: ветка `impl='cpp'` в `gpr_core/sar3d.py` (адаптер `gpr_cpp_backend.py`),
кэш device-A-сканов (сейчас каждый вызов грузит стек на GPU).
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# CMake-сборка libgpr_core_cpp + standalone (без nvcc; NVRTC/driver).
#
# Источник CUDA (nvrtc.h / libnvrtc), по убыванию приоритета:
# 1. env CUDA_INC / CUDA_LIB — заданы явно;
# 2. pip-пакеты nvidia-* в venv ($VENV, по умолчанию ../../.venv — venv
# radar_system) — дев-машина;
# 3. системный toolkit /usr/local/cuda — Jetson/JetPack, apt cuda-toolkit.
#
# GPR_BUILD_PYTHON=ON вернул бы pybind-модуль, но bindings/ не вендорен в
# radar_system (см. README) — по умолчанию OFF.
set -eu
HERE="$(cd "$(dirname "$0")" && pwd)"
VENV="${VENV:-$HERE/../../.venv}"
PY="$VENV/bin/python"
GPR_BUILD_PYTHON="${GPR_BUILD_PYTHON:-OFF}"
CUDA_INC="${CUDA_INC:-}"
CUDA_LIB="${CUDA_LIB:-}"
if [ -z "$CUDA_INC" ] || [ -z "$CUDA_LIB" ]; then
if [ -x "$PY" ]; then
# Печатается только когда найдены nvrtc.h, cuda.h И библиотека — частичная
# детекция не может подсунуть мусорный путь. nvrtc.h и cuda.h могут лежать
# в РАЗНЫХ каталогах (раздельные pip-пакеты cuda_nvrtc/cuda_runtime на
# Jetson) — тогда CUDA_INC становится ';'-списком для CMake.
PIP_CUDA=$("$PY" - <<'P' 2>/dev/null || true
import glob, os, site
sp = site.getsitepackages()[0]
nvrtc_hdr = glob.glob(os.path.join(sp, 'nvidia', '**', 'include', 'nvrtc.h'), recursive=True)
cuda_hdr = glob.glob(os.path.join(sp, 'nvidia', '**', 'include', 'cuda.h'), recursive=True)
lib = sorted(glob.glob(os.path.join(sp, 'nvidia', '**', 'lib', 'libnvrtc.so*'), recursive=True))
if nvrtc_hdr and cuda_hdr and lib:
inc_dirs = []
for hdr in (nvrtc_hdr[0], cuda_hdr[0]):
d = os.path.dirname(hdr)
if d not in inc_dirs:
inc_dirs.append(d)
print(';'.join(inc_dirs))
print(os.path.dirname(lib[0]))
P
)
if [ -n "$PIP_CUDA" ]; then
CUDA_INC=$(printf '%s\n' "$PIP_CUDA" | sed -n 1p)
CUDA_LIB=$(printf '%s\n' "$PIP_CUDA" | sed -n 2p)
fi
fi
fi
if [ -z "$CUDA_INC" ] || [ -z "$CUDA_LIB" ]; then
if [ -e /usr/local/cuda/include/nvrtc.h ]; then
CUDA_INC=/usr/local/cuda/include
CUDA_LIB=/usr/local/cuda/lib64
echo "[build_cmake] pip nvidia-* не найдены; использую системный CUDA: $CUDA_INC"
else
echo "[build_cmake] CUDA не найден: нет ни pip nvidia-* в $VENV, ни /usr/local/cuda." >&2
echo "[build_cmake] Задайте CUDA_INC=<dir с nvrtc.h,cuda.h> и CUDA_LIB=<dir с libnvrtc.so*>." >&2
exit 1
fi
fi
PYTHON_ARG=()
if [ "$GPR_BUILD_PYTHON" = "ON" ] && [ -x "$PY" ]; then
PYTHON_ARG=(-DPYTHON_EXECUTABLE="$PY")
fi
# cmake: сначала из venv (pip install cmake — свежий без sudo), потом системный.
CMAKE="$VENV/bin/cmake"
if [ ! -x "$CMAKE" ]; then
CMAKE=$(command -v cmake || true)
fi
if [ -z "$CMAKE" ]; then
echo "[build_cmake] cmake не найден. Поставьте его в venv: \"$VENV/bin/pip\" install cmake" >&2
echo "[build_cmake] (или системно: sudo apt install cmake)" >&2
exit 1
fi
"$CMAKE" -S "$HERE" -B "$HERE/build" \
${PYTHON_ARG[@]+"${PYTHON_ARG[@]}"} \
-DGPR_BUILD_PYTHON="$GPR_BUILD_PYTHON" \
-DGPR_CUDA_INC="$CUDA_INC" \
-DGPR_CUDA_LIB="$CUDA_LIB" \
-DCMAKE_BUILD_TYPE=Release "$@" 2>&1 | tail -8
"$CMAKE" --build "$HERE/build" -j4 2>&1 | tail -15
echo "--- артефакты ---"
ls -la "$HERE/build/libgpr_core_cpp.a" "$HERE/build/standalone_bp" "$HERE"/build/_gpr_cpp*.so 2>/dev/null | awk '{print $5,$9}'
echo "CUDA_LIB=$CUDA_LIB (для LD_LIBRARY_PATH при запуске)"
@@ -0,0 +1,159 @@
// Автономный пример: C++ без Python. Грузит S21 снапшота из .npy,
// строит A-сканы и 3D SAR-объём апертуры, печатает максимум |acc/cnt|.
//
// Использование:
// standalone_bp <preprocessed_dir> <center_snapshot_name>
// где preprocessed_dir содержит папки снапшотов с i{rx}_o{tx}_s21.npy и _freq.npy.
#include <algorithm>
#include <cmath>
#include <complex>
#include <cstdint>
#include <cstdio>
#include <dirent.h>
#include <fstream>
#include <map>
#include <string>
#include <vector>
#include "gpr/ascan.hpp"
#include "gpr/config.hpp"
#include "gpr/geometry.hpp"
#include "gpr/sar3d.hpp"
using namespace gpr;
// --- минимальный загрузчик numpy .npy (v1.0, 1-D complex128 или float64) ---
struct NpyArray { std::vector<double> f64; std::vector<c128> c128v; bool is_complex = false; };
static NpyArray load_npy(const std::string& path) {
std::ifstream in(path, std::ios::binary);
if (!in) throw std::runtime_error("не открыть " + path);
char magic[6]; in.read(magic, 6);
uint8_t ver[2]; in.read((char*)ver, 2);
uint16_t hlen; in.read((char*)&hlen, 2);
std::string hdr(hlen, '\0'); in.read(&hdr[0], hlen);
auto sp = hdr.find("'shape':");
auto lp = hdr.find('(', sp), rp = hdr.find(')', sp);
long n = std::stol(hdr.substr(lp + 1, rp - lp - 1));
NpyArray a;
auto has = [&](const char* s) { return hdr.find(s) != std::string::npos; };
if (has("c8")) { // complex64 → c128
a.is_complex = true; a.c128v.resize(n);
std::vector<std::complex<float>> tmp(n);
in.read((char*)tmp.data(), n * sizeof(std::complex<float>));
for (long i = 0; i < n; ++i) a.c128v[i] = c128(tmp[i].real(), tmp[i].imag());
} else if (has("c16")) { // complex128
a.is_complex = true; a.c128v.resize(n);
in.read((char*)a.c128v.data(), n * sizeof(c128));
} else if (has("f4")) { // float32 → double
a.f64.resize(n);
std::vector<float> tmp(n); in.read((char*)tmp.data(), n * sizeof(float));
for (long i = 0; i < n; ++i) a.f64[i] = tmp[i];
} else { // float64
a.f64.resize(n); in.read((char*)a.f64.data(), n * sizeof(double));
}
return a;
}
int main(int argc, char** argv) {
if (argc < 3) { printf("usage: %s <preprocessed_dir> <center_snapshot>\n", argv[0]); return 1; }
std::string root = argv[1], center = argv[2];
GeometryConfig geom; geom.finalize();
BPConfig bp; MotionConfig mot; SARConfig sar;
auto pairs = geom.pair_keys();
auto tx = geom.output_idx, rx = geom.input_idx;
std::sort(tx.begin(), tx.end()); std::sort(rx.begin(), rx.end());
// список снапшотов, выбор апертуры вокруг center
std::vector<std::string> snaps;
if (DIR* d = opendir(root.c_str())) {
for (dirent* e; (e = readdir(d));) {
std::string n = e->d_name;
if (!n.empty() && isdigit(n[0])) snaps.push_back(n);
}
closedir(d);
}
std::sort(snaps.begin(), snaps.end());
int ci = -1; for (int i = 0; i < (int)snaps.size(); ++i) if (snaps[i] == center) ci = i;
if (ci < 0) { printf("центр %s не найден\n", center.c_str()); return 1; }
int lo = std::max(0, ci - sar.frame_radius), hi = std::min((int)snaps.size(), ci + sar.frame_radius + 1);
std::vector<std::string> aperture(snaps.begin() + lo, snaps.begin() + hi);
printf("апертура: %d кадров вокруг %s\n", (int)aperture.size(), center.c_str());
auto load_snapshot = [&](const std::string& snap,
std::map<PairKey, std::vector<c128>>& s21,
std::map<PairKey, std::vector<double>>& freq) {
for (int p = 0; p < (int)pairs.size(); ++p) {
int i_tx = pairs[p].first, i_rx = pairs[p].second;
int phys_tx = tx[i_tx], phys_rx = rx[i_rx];
char fn[512];
snprintf(fn, sizeof(fn), "%s/%s/i%d_o%d_s21.npy", root.c_str(), snap.c_str(), phys_rx, phys_tx);
s21[{i_tx, i_rx}] = load_npy(fn).c128v;
snprintf(fn, sizeof(fn), "%s/%s/i%d_o%d_freq.npy", root.c_str(), snap.c_str(), phys_rx, phys_tx);
freq[{i_tx, i_rx}] = load_npy(fn).f64;
}
};
// Демо без чтения meta.json: равномерные сегменты по интервалу между
// соседними снапшотами (реальные per-trace окна читает sar3d_parity).
double acq_s = 0.0;
{
std::vector<double> stamps;
for (auto& s : snaps) {
auto pos = s.rfind("_ns");
if (pos != std::string::npos) stamps.push_back(std::stod(s.substr(pos + 3)) * 1e-9);
}
std::sort(stamps.begin(), stamps.end());
if (stamps.size() >= 2) acq_s = stamps[1] - stamps[0];
}
auto windows = uniform_tx_windows(geom.output_idx, acq_s, mot.tx_per_segment);
auto ts = compute_pair_timestamps(windows, acq_s, geom.output_idx, geom, mot);
SarEngine engine(geom, bp, mot, sar);
// держим AScanSet живыми
std::vector<std::shared_ptr<AScanSet>> keep;
std::vector<ApertureFrame> frames;
std::vector<double> frame_times;
for (auto& snap : aperture) {
// время из суффикса _nsNNNN
auto pos = snap.rfind("_ns");
double t = (pos != std::string::npos) ? std::stod(snap.substr(pos + 3)) * 1e-9 : 0.0;
frame_times.push_back(t);
}
double t_ref = frame_times[ci - lo];
auto offs = frame_offsets(frame_times, t_ref, mot);
auto wts = frame_weights(offs, sar.edge_frame_weight);
for (int f = 0; f < (int)aperture.size(); ++f) {
std::map<PairKey, std::vector<c128>> s21;
std::map<PairKey, std::vector<double>> freq;
load_snapshot(aperture[f], s21, freq);
auto a = std::make_shared<AScanSet>(
preprocess_snapshot(s21, freq, nullptr, mot, geom, bp, ts, true));
keep.push_back(a);
frames.push_back(ApertureFrame{a.get(), offs[f], wts[f]});
}
// ноутбучные границы сцены (размах антенн ±2 м, полоса глубин) вместо
// фиксированного окна по умолчанию
notebook_grid_bounds(geom, bp, sar, mot.pitch_deg, 2.0,
sar.x_min_m, sar.x_max_m, sar.z_min_m, sar.z_max_m);
Grid grid = build_full_region_grid(sar);
printf("сетка %dx%dx%d, счёт объёма...\n", grid.NX(), grid.NY(), grid.NZ());
VolumeRaw vol = engine.aperture_volume(frames, grid);
FinishedVolume fin = finish_volume(vol, grid, mot.pitch_deg, bp, sar);
double vmax = 0; size_t amax = 0;
for (size_t i = 0; i < fin.volume.size(); ++i)
if (fin.volume[i] > vmax) { vmax = fin.volume[i]; amax = i; }
int NX = vol.NX, NZ = vol.NZ;
int ix = amax % NX, iz = (amax / NX) % NZ, iy = amax / ((size_t)NX * NZ);
printf("МАКСИМУМ объёма: %.4f (level %.4e) в вокселе (ix=%d,iy=%d,iz=%d)\n",
vmax, fin.level, ix, iy, iz);
printf(" X=%.1f см, Y=%.1f см, Z=%.1f см\n",
grid.x[ix] * 100, grid.y[iy] * 100, grid.z[iz] * 100);
printf("OK: автономный C++ путь отработал без Python.\n");
return 0;
}
@@ -0,0 +1,81 @@
// Препроцессинг S21 → комплексные A-сканы (host, float64).
// Порт ячейки 0 Multi_antenna_3D_clean_v1.ipynb (segmented_interleaved тайминг,
// frequency-domain коррекция движения, k0-IFFT, нормировка пар).
#pragma once
#include <map>
#include <string>
#include <utility>
#include <vector>
#include "gpr/config.hpp"
#include "gpr/types.hpp"
namespace gpr {
using PairKey = std::pair<int, int>;
// Тайминг пары в модели segmented_interleaved: пара живёт в сегменте своего Tx
// (окно [segment_start, segment_start+segment_time] от начала акквизиции),
// внутри сегмента передатчики чередуются по частоте (slot из n_slots).
struct PairTiming {
int tx_interleave_slot = 0;
int tx_interleave_slots = 1;
double segment_start_s = 0.0;
double segment_time_s = 0.0;
double t_center_s = 0.0; // центр окна сегмента
// Грубый межсегментный сдвиг: dtau центра сегмента относительно центра
// акквизиции кадра (2*dz/v, dz = dir*speed*dt*cos(look)).
double dtau_pair_s = 0.0;
};
using PairTimestamps = std::map<PairKey, PairTiming>;
// Окна захвата по Tx (output_pos → [start,end] сек от начала акквизиции).
using TxWindows = std::map<int, std::pair<double, double>>;
// Fallback без per-trace меток: сегменты по tx_per_segment штук делят кадр
// [0, acq_s] поровну (медианная модель ноутбука compute_pair_timestamps).
TxWindows uniform_tx_windows(const std::vector<int>& tx_order, double acq_s, int tx_per_segment);
// Порт compute_pair_timestamps_for_snapshot: Tx с одинаковым окном — один
// сегмент (сегменты по возрастанию окна), слот — порядок в tx_order (полный
// аппаратный порядок из run.combos; выбранные каналы его не меняют).
PairTimestamps compute_pair_timestamps(const TxWindows& tx_windows_s, double acq_s,
const std::vector<int>& tx_order,
const GeometryConfig&, const MotionConfig&);
// A-сканы одного снапшота, общая ось времени у всех пар (assert).
struct AScanSet {
int n_pairs = 0;
int n_fft = 0;
double t0 = 0.0, dt = 0.0; // общая ось: t_k = t0 + k*dt
std::vector<PairKey> pairs; // порядок как в geom.pair_keys()
std::vector<c128> H; // [n_pairs * n_fft] float64
const c128* pair_ptr(int p) const { return H.data() + (size_t)p * n_fft; }
};
// Порт sar_pair_ascans: bg-subtract → phase correction → k0-IFFT → норма.
// s21/freq/background — по ключам (i_tx,i_rx); background может быть nullptr.
AScanSet preprocess_snapshot(
const std::map<PairKey, std::vector<c128>>& s21,
const std::map<PairKey, std::vector<double>>& freq,
const std::map<PairKey, std::vector<c128>>* background,
const MotionConfig&, const GeometryConfig&, const BPConfig&,
const PairTimestamps&, bool normalize = true);
// Отдельные шаги (для тестов/переиспользования):
// Полная фазовая модель движения до IFFT: dtau_intra(f) + dtau_pair одной
// фазой exp(sign*j*2*pi*f*dtau_total) (порт apply_intra_sweep_phase_correction).
std::vector<c128> apply_intra_sweep_phase_correction(
const std::vector<c128>& s21, const std::vector<double>& freq_full,
const PairTiming&, const MotionConfig&, double wave_speed,
double f_start, double f_stop);
// Возвращает t (ось), h (комплексный A-скан). n_fft через out-параметр.
void compute_ascan_bp(const std::vector<c128>& s21, const std::vector<double>& freq,
double f_start, double f_stop, bool window, int oversample,
std::vector<double>& t_out, std::vector<c128>& h_out);
double pair_norm_scale(const std::vector<c128>& h, const std::vector<double>& t,
double wave_speed, const BPConfig&);
} // namespace gpr
@@ -0,0 +1,114 @@
// Конфигурация счёт-ядра. Дефолты 1:1 повторяют Multi_antenna_3D_clean_v1.ipynb
// (ячейки 0 и 4) — эталонный ноутбук 3D SAR.
#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <utility>
#include <vector>
namespace gpr {
struct GeometryConfig {
std::vector<int> input_idx{0, 1, 2, 3};
std::vector<int> output_idx{0, 1, 2, 3};
// (x,y,z) метры; порядок соответствует input_idx/output_idx
std::vector<std::array<double, 3>> tx_positions{
{-0.88, 0.0, 0.0}, {0.89, 0.0, 0.0}, {-0.33, 0.445, 0.0}, {0.33, 0.445, 0.0}};
std::vector<std::array<double, 3>> rx_positions{
{0.23, 0.0, 0.0}, {0.54, 0.15, 0.0}, {-0.55, 0.3, 0.0}, {-0.23, 0.0, 0.0}};
double eps_r = 1.0;
// --- производные (заполняются finalize) ---
std::vector<double> x_tx, y_tx, z_tx; // отсортированы по phys-индексу
std::vector<double> x_rx, y_rx, z_rx;
int n_tx = 0, n_rx = 0;
double v = 0.0;
void finalize() {
auto sort_xyz = [](std::vector<int> idx, const std::vector<std::array<double, 3>>& pos,
std::vector<double>& X, std::vector<double>& Y, std::vector<double>& Z) {
std::vector<int> order(idx.size());
for (size_t i = 0; i < idx.size(); ++i) order[i] = (int)i;
std::sort(order.begin(), order.end(), [&](int a, int b) { return idx[a] < idx[b]; });
X.clear(); Y.clear(); Z.clear();
for (int o : order) { X.push_back(pos[o][0]); Y.push_back(pos[o][1]); Z.push_back(pos[o][2]); }
};
sort_xyz(output_idx, tx_positions, x_tx, y_tx, z_tx);
sort_xyz(input_idx, rx_positions, x_rx, y_rx, z_rx);
n_tx = (int)output_idx.size();
n_rx = (int)input_idx.size();
v = 3e8 / std::sqrt(eps_r);
}
std::vector<std::pair<int, int>> pair_keys() const {
std::vector<std::pair<int, int>> p;
for (int i = 0; i < n_tx; ++i)
for (int j = 0; j < n_rx; ++j) p.emplace_back(i, j);
return p;
}
};
struct BPConfig {
double f_start = 2.0e9;
double f_stop = 6.0e9;
double min_depth = 2.5;
double max_depth = 8.0;
int oversample = 8;
bool window = true; // окно Ханна на полосе
bool pair_normalize = true;
double pair_norm_percentile = 50.0;
double pair_norm_depth_min = 2.5;
double pair_norm_depth_max = 8.0;
double pair_norm_eps = 1e-15;
// Компенсация геометрического расхождения 1/(Rtx*Rrx), нормированная на
// точку под виртуальным центром пары на глубине comp_ref_depth НА ПЛОСКОСТИ
// ЗЕМЛИ (высота (-ground_depth + d*sin p)/cos p).
double comp_range_power = 0.3;
double comp_weight_max = 5.0;
double comp_ref_depth = 5.0;
};
struct MotionConfig {
double speed_m_s = 1.45;
double look_angle_deg = 17.5;
// Наклон рамы для ground-координат и смещений кадров; в ноутбуке
// BP_GROUND_PITCH_DEG=None означает «равен look_angle».
double pitch_deg = 17.5;
double direction_sign = +1.0;
// Внутрисвиповая (по частоте) фазовая коррекция движения.
bool apply_freq_phase_correction = true;
// Грубая межсегментная коррекция (dtau сегмента относительно центра кадра),
// вносится той же фазовой моделью до IFFT (режим 'frequency_domain').
bool apply_pair_motion_correction = true;
double motion_phase_sign = -1.0;
// Fallback-разбиение кадра на сегменты, когда per-trace меток захвата нет.
int tx_per_segment = 2;
};
struct SARConfig {
int frame_radius = 0; // 0 = один снимок (физическая вертикальная база)
double edge_frame_weight = 0.8; // вес крайнего кадра апертуры
bool coord_mode_ground = true; // 'ground' vs 'radar_local'
bool use_compensation = true;
// Высота НИЖНЕГО ряда антенн над землёй; Y=0 сцены — поверхность земли.
double ground_depth_m = 1.25;
// Явные границы сцены [м]: X — поперёк, Y — высота над землёй, Z — вдаль.
// Воксели с радарной глубиной вне [min_depth, max_depth] всё равно
// обнуляются маской, так что Z-окно шире полосы глубин безопасно.
double x_min_m = -2.0;
double x_max_m = 2.0;
double y_min_m = -0.5;
double y_max_m = 2.0;
double z_min_m = 0.0;
double z_max_m = 8.0;
std::array<double, 3> voxel_m{0.025, 0.02, 0.02}; // шаг (X, Y, Z)
// Финализация объёма: нормировка без верхних/нижних norm_edge_margin_m по Y
// (там ложный гребень постоянной дальности), сглаживание по (Z,X) в каждом
// Y-плане.
double norm_edge_margin_m = 0.3;
double smooth_sigma = 1.5;
};
} // namespace gpr
@@ -0,0 +1,52 @@
// CUDA Driver API RAII: контекст, поток, буфер устройства.
#pragma once
#include <cuda.h>
#include <cstddef>
#include <stdexcept>
#include <string>
namespace gpr {
void cu_check(CUresult r, const char* what);
// Инициализация драйвера + primary context на device 0. Возвращает (major,minor) CC.
struct CudaContext {
CUdevice dev = 0;
CUcontext ctx = nullptr;
int cc_major = 0, cc_minor = 0;
CudaContext();
~CudaContext();
std::string arch_flag() const; // "--gpu-architecture=sm_XY"
};
template <class T>
class DeviceBuffer {
CUdeviceptr ptr_ = 0;
size_t n_ = 0;
public:
DeviceBuffer() = default;
explicit DeviceBuffer(size_t n) { alloc(n); }
~DeviceBuffer() { free(); }
DeviceBuffer(const DeviceBuffer&) = delete;
DeviceBuffer& operator=(const DeviceBuffer&) = delete;
DeviceBuffer(DeviceBuffer&& o) noexcept : ptr_(o.ptr_), n_(o.n_) { o.ptr_ = 0; o.n_ = 0; }
void alloc(size_t n) {
free();
if (n) cu_check(cuMemAlloc(&ptr_, n * sizeof(T)), "cuMemAlloc");
n_ = n;
}
void free() {
if (ptr_) cuMemFree(ptr_);
ptr_ = 0; n_ = 0;
}
void upload(const T* host, size_t n) { cu_check(cuMemcpyHtoD(ptr_, host, n * sizeof(T)), "HtoD"); }
void download(T* host, size_t n) const { cu_check(cuMemcpyDtoH(host, ptr_, n * sizeof(T)), "DtoH"); }
CUdeviceptr ptr() const { return ptr_; }
CUdeviceptr* addr() { return &ptr_; }
size_t size() const { return n_; }
};
} // namespace gpr
@@ -0,0 +1,41 @@
// Геометрия: опорное расхождение пары, смещения/веса кадров, построение сетки.
#pragma once
#include <vector>
#include "gpr/config.hpp"
#include "gpr/types.hpp"
namespace gpr {
// 1/geo_ref пары (порт pair_geo_ref, скаляр float64).
double pair_geo_ref(const GeometryConfig&, const BPConfig&, int i_tx, int i_rx, double yy);
// Смещения кадров относительно опорного (порт sar_frame_offsets).
// frame_times[k] — время кадра (сек, от ns-таймстемпа); t_ref — время опорного.
std::vector<FrameOffset> frame_offsets(const std::vector<double>& frame_times, double t_ref,
const MotionConfig&);
// Косинусные веса апертуры (порт sar_frame_weights); dt берётся относительно центра.
std::vector<double> frame_weights(const std::vector<FrameOffset>& offsets, double edge_weight);
// Высота плоскости земли (радарные оси) на радарной глубине depth:
// y = (-ground_depth + depth*sin(pitch)) / cos(pitch) (порт bp_plane_y).
double ground_plane_y(double depth, double ground_depth_m, double pitch_rad);
// Сетка сцены по явным границам SARConfig (x/y/z_min..max, шаг voxel_m);
// оси — linspace c числом точек round(span/step)+1, как в ноутбуке.
Grid build_full_region_grid(const SARConfig&);
// Ноутбучные границы (sar_build_full_region_grid): X — размах антенн ±margin,
// Z — полоса, покрывающая радарные глубины [min_depth, max_depth] на всех
// высотах Y. Используются parity-инструментом для сверки с эталонными npz.
void notebook_grid_bounds(const GeometryConfig&, const BPConfig&, const SARConfig&,
double pitch_deg, double x_margin_m,
double& x_min, double& x_max, double& z_min, double& z_max);
// point_to_scene: оси радара → сцена (поворот на pitch + подъём на ground_depth,
// если ground; Y=0 сцены — земля).
std::array<double, 3> point_to_scene(double x, double y, double z, const MotionConfig&,
const SARConfig&);
} // namespace gpr
@@ -0,0 +1,25 @@
// Компиляция строки CUDA-ядра через NVRTC → cubin под арх устройства → CUfunction.
#pragma once
#include <cuda.h>
#include <string>
namespace gpr {
class NvrtcKernel {
CUmodule module_ = nullptr;
CUfunction func_ = nullptr;
public:
NvrtcKernel() = default;
~NvrtcKernel();
NvrtcKernel(const NvrtcKernel&) = delete;
NvrtcKernel& operator=(const NvrtcKernel&) = delete;
// Компилирует src в cubin под arch_flag ("--gpu-architecture=sm_XY"),
// загружает модуль и берёт функцию func_name.
void compile(const char* src, const std::string& arch_flag, const char* func_name);
CUfunction func() const { return func_; }
};
} // namespace gpr
@@ -0,0 +1,55 @@
// 3D SAR: сборка взвешенного объёма апертуры за один запуск ядра.
#pragma once
#include <memory>
#include <vector>
#include "gpr/ascan.hpp"
#include "gpr/config.hpp"
#include "gpr/device.hpp"
#include "gpr/geometry.hpp"
#include "gpr/nvrtc_kernel.hpp"
#include "gpr/types.hpp"
namespace gpr {
// Один кадр апертуры: A-сканы + смещение + вес.
struct ApertureFrame {
const AScanSet* ascans;
FrameOffset offset;
double weight;
};
// Сырой результат объёма (до finish_volume).
struct VolumeRaw {
std::vector<c64> acc; // [NY*NZ*NX]
std::vector<float> cnt; // [NY*NZ*NX]
int NX = 0, NY = 0, NZ = 0;
};
// Финальный объём (порт sar_finish_volume): |acc/cnt| → маска глубины →
// нормировка по Y-планам без краёв → гаусс по (Z,X) → нормировка → clip.
struct FinishedVolume {
std::vector<float> volume; // [NY*NZ*NX], нормирован на ~[0,1]
double level = 0.0; // масштаб, на который поделён объём
int NX = 0, NY = 0, NZ = 0;
};
FinishedVolume finish_volume(const VolumeRaw&, const Grid&, double pitch_deg,
const BPConfig&, const SARConfig&);
class SarEngine {
public:
SarEngine(const GeometryConfig&, const BPConfig&, const MotionConfig&, const SARConfig&);
// Весь объём апертуры за один запуск ядра; возвращает сырые acc/cnt (host).
VolumeRaw aperture_volume(const std::vector<ApertureFrame>& frames, const Grid&);
private:
GeometryConfig geom_;
BPConfig bp_;
MotionConfig mot_;
SARConfig sar_;
std::unique_ptr<CudaContext> ctx_;
std::unique_ptr<NvrtcKernel> kernel_;
};
} // namespace gpr
@@ -0,0 +1,25 @@
// Базовые типы счёт-ядра.
#pragma once
#include <complex>
#include <cstdint>
#include <vector>
namespace gpr {
using c64 = std::complex<float>; // complex64, layout == cuComplex (2×float)
using c128 = std::complex<double>; // float64 для препроцессинга/IFFT
// Ground-scene сетка (порт dict из geometry.py: x,y,z оси в метрах).
struct Grid {
std::vector<double> x, y, z;
int NX() const { return (int)x.size(); }
int NY() const { return (int)y.size(); }
int NZ() const { return (int)z.size(); }
};
// Смещение антенн кадра апертуры (порт sar_frame_offsets).
struct FrameOffset {
double dx_m = 0.0, dy_m = 0.0, dz_m = 0.0, dt_s = 0.0, horizontal_m = 0.0;
};
} // namespace gpr
@@ -0,0 +1,250 @@
#include "gpr/ascan.hpp"
#include <algorithm>
#include <cmath>
#include <numeric>
#include <stdexcept>
#include "pocketfft_hdronly.h"
namespace gpr {
TxWindows uniform_tx_windows(const std::vector<int>& tx_order, double acq_s, int tx_per_segment) {
TxWindows windows;
if (tx_order.empty()) return windows;
int per_seg = std::max(1, tx_per_segment);
int n_seg = std::max(1, (int)((tx_order.size() + per_seg - 1) / per_seg));
double t_seg = acq_s / n_seg;
for (size_t k = 0; k < tx_order.size(); ++k) {
int seg = (int)k / per_seg;
windows[tx_order[k]] = {seg * t_seg, seg * t_seg + t_seg};
}
return windows;
}
PairTimestamps compute_pair_timestamps(const TxWindows& tx_windows_s, double acq_s,
const std::vector<int>& tx_order,
const GeometryConfig& geom, const MotionConfig& mot) {
// Аппаратный порядок: сперва tx_order (run.combos), затем прочие ключи окон.
std::vector<int> hw_order;
for (int tx : tx_order)
if (tx_windows_s.count(tx) && std::find(hw_order.begin(), hw_order.end(), tx) == hw_order.end())
hw_order.push_back(tx);
for (auto& [tx, _] : tx_windows_s)
if (std::find(hw_order.begin(), hw_order.end(), tx) == hw_order.end())
hw_order.push_back(tx);
// Tx с одинаковым окном — один сегмент; сегменты по возрастанию окна,
// слот — порядок появления в hw_order (порт группировки ноутбука).
std::vector<std::pair<std::pair<double, double>, std::vector<int>>> groups;
for (int tx : hw_order) {
auto win = tx_windows_s.at(tx);
auto found = std::find_if(groups.begin(), groups.end(),
[&](const auto& g) { return g.first == win; });
if (found == groups.end())
groups.push_back({win, {tx}});
else
found->second.push_back(tx);
}
std::sort(groups.begin(), groups.end(),
[](const auto& a, const auto& b) { return a.first < b.first; });
struct SegSlot { double t0, t1; int slot, n_slots; };
std::map<int, SegSlot> seg_of;
for (auto& [win, txs] : groups)
for (size_t slot = 0; slot < txs.size(); ++slot)
seg_of[txs[slot]] = {win.first, win.second, (int)slot, (int)txs.size()};
double cos_theta = std::cos(mot.look_angle_deg * M_PI / 180.0);
double t_ref = 0.5 * acq_s;
PairTimestamps ts;
for (int i = 0; i < geom.n_tx; ++i) {
auto it = seg_of.find(geom.output_idx[i]);
if (it == seg_of.end())
throw std::runtime_error("pair timestamps: нет окна для Tx");
const SegSlot& s = it->second;
PairTiming timing;
timing.tx_interleave_slot = s.slot;
timing.tx_interleave_slots = s.n_slots;
timing.segment_start_s = s.t0;
timing.segment_time_s = s.t1 - s.t0;
timing.t_center_s = 0.5 * (s.t0 + s.t1);
double dz = mot.direction_sign * mot.speed_m_s * (timing.t_center_s - t_ref) * cos_theta;
timing.dtau_pair_s = 2.0 * dz / geom.v;
for (int j = 0; j < geom.n_rx; ++j) ts[{i, j}] = timing;
}
return ts;
}
static double median_of_diff(const std::vector<double>& f) {
std::vector<double> d(f.size() - 1);
for (size_t i = 0; i + 1 < f.size(); ++i) d[i] = f[i + 1] - f[i];
std::sort(d.begin(), d.end());
size_t n = d.size();
return (n % 2) ? d[n / 2] : 0.5 * (d[n / 2 - 1] + d[n / 2]);
}
std::vector<c128> apply_intra_sweep_phase_correction(
const std::vector<c128>& s21, const std::vector<double>& freq_full,
const PairTiming& info, const MotionConfig& cfg, double wave_speed,
double f_start, double f_stop) {
std::vector<c128> out = s21;
int n_full = (int)freq_full.size();
// индексы в полосе
std::vector<int> idx;
for (int i = 0; i < n_full; ++i)
if (freq_full[i] >= f_start && freq_full[i] <= f_stop) idx.push_back(i);
if (idx.empty()) throw std::runtime_error("phase corr: пустая полоса");
if (n_full < 2) return out;
// Сегмент проходит все n_full частот за своё окно; на каждой частоте по
// очереди работают его n_slots передатчиков, поэтому событие частоты i для
// слота slot имеет номер (n_slots*i + slot) из (n_slots*n_full).
int n_slots = std::max(1, info.tx_interleave_slots);
int slot = info.tx_interleave_slot;
double dt_event = info.segment_time_s / ((double)n_slots * n_full);
double cos_theta = std::cos(cfg.look_angle_deg * M_PI / 180.0);
double pair_dtau = cfg.apply_pair_motion_correction ? info.dtau_pair_s : 0.0;
if (!cfg.apply_freq_phase_correction && pair_dtau == 0.0) return out;
for (int i : idx) {
double dtau_intra = 0.0;
if (cfg.apply_freq_phase_correction) {
double t_abs = info.segment_start_s + (double)(n_slots * i + slot) * dt_event;
double dt_intra = t_abs - info.t_center_s;
dtau_intra = 2.0 * cfg.direction_sign * cfg.speed_m_s * cos_theta * dt_intra / wave_speed;
}
double phi = 2.0 * M_PI * freq_full[i] * (dtau_intra + pair_dtau);
double ang = cfg.motion_phase_sign * phi;
out[i] *= c128(std::cos(ang), std::sin(ang));
}
return out;
}
static int next_pow2(int x) {
int p = 1;
while (p < x) p <<= 1;
return p;
}
void compute_ascan_bp(const std::vector<c128>& s21, const std::vector<double>& freq,
double f_start, double f_stop, bool window, int oversample,
std::vector<double>& t_out, std::vector<c128>& h_out) {
std::vector<double> freq_cut;
std::vector<c128> s_cut;
for (size_t i = 0; i < freq.size(); ++i)
if (freq[i] >= f_start && freq[i] <= f_stop) {
freq_cut.push_back(freq[i]);
s_cut.push_back(s21[i]);
}
if (freq_cut.size() < 2) throw std::runtime_error("ascan: <2 точек в полосе");
double df = median_of_diff(freq_cut);
int n = (int)freq_cut.size();
int k0 = (int)std::nearbyint(freq_cut[0] / df); // round-half-to-even как python round
int min_len = 2 * (k0 + n - 1);
int n_fft_base = next_pow2(min_len);
int n_fft = n_fft_base * oversample;
double dt = 1.0 / ((double)n_fft * df);
// окно Ханна (numpy np.hanning: 0.5 - 0.5*cos(2*pi*i/(N-1)))
std::vector<c128> H(n_fft, c128(0.0, 0.0));
for (int i = 0; i < n; ++i) {
c128 v = s_cut[i];
if (window) {
double w = 0.5 - 0.5 * std::cos(2.0 * M_PI * i / (n - 1));
v *= w;
}
H[k0 + i] = v;
}
// IFFT через pocketfft (backward + 1/n_fft == numpy ifft)
h_out.assign(n_fft, c128(0.0, 0.0));
pocketfft::shape_t shape{(size_t)n_fft};
pocketfft::stride_t stride{(ptrdiff_t)sizeof(c128)};
pocketfft::c2c(shape, stride, stride, {0}, pocketfft::BACKWARD,
H.data(), h_out.data(), (double)(1.0 / n_fft));
t_out.resize(n_fft);
for (int i = 0; i < n_fft; ++i) t_out[i] = i * dt;
}
double pair_norm_scale(const std::vector<c128>& h, const std::vector<double>& t,
double wave_speed, const BPConfig& bp) {
std::vector<double> vals;
vals.reserve(h.size());
for (size_t i = 0; i < h.size(); ++i) {
double z = t[i] * wave_speed / 2.0;
if (z >= bp.pair_norm_depth_min && z <= bp.pair_norm_depth_max)
vals.push_back(std::abs(h[i]));
}
if (vals.empty())
for (auto& c : h) vals.push_back(std::abs(c));
std::sort(vals.begin(), vals.end());
// numpy.percentile linear interpolation
double q = bp.pair_norm_percentile / 100.0;
double pos = q * (vals.size() - 1);
int lo = (int)std::floor(pos);
int hi = std::min(lo + 1, (int)vals.size() - 1);
double scale = vals[lo] + (pos - lo) * (vals[hi] - vals[lo]);
if (!std::isfinite(scale) || scale <= bp.pair_norm_eps) scale = 1.0;
return scale;
}
AScanSet preprocess_snapshot(
const std::map<PairKey, std::vector<c128>>& s21,
const std::map<PairKey, std::vector<double>>& freq,
const std::map<PairKey, std::vector<c128>>* background,
const MotionConfig& mot, const GeometryConfig& geom, const BPConfig& bp,
const PairTimestamps& ts, bool normalize) {
AScanSet out;
out.pairs = geom.pair_keys();
out.n_pairs = (int)out.pairs.size();
std::vector<std::vector<c128>> Hs;
std::vector<std::vector<double>> Ts;
for (auto& key : out.pairs) {
auto its = s21.find(key);
if (its == s21.end()) throw std::runtime_error("preprocess: нет пары в s21");
std::vector<c128> s = its->second; // копия
if (background) {
auto ib = background->find(key);
if (ib != background->end())
for (size_t i = 0; i < s.size(); ++i) s[i] -= ib->second[i];
}
const std::vector<double>& fr = freq.at(key);
auto itt = ts.find(key);
if (itt != ts.end())
s = apply_intra_sweep_phase_correction(s, fr, itt->second, mot, geom.v,
bp.f_start, bp.f_stop);
std::vector<double> t;
std::vector<c128> h;
compute_ascan_bp(s, fr, bp.f_start, bp.f_stop, bp.window, bp.oversample, t, h);
Hs.push_back(std::move(h));
Ts.push_back(std::move(t));
}
// общая ось времени — проверка
int n_fft = (int)Ts[0].size();
double t0 = Ts[0][0];
double dt = (Ts[0].back() - t0) / (n_fft - 1);
for (auto& t : Ts)
if ((int)t.size() != n_fft || std::abs(t[0] - t0) > 1e-18)
throw std::runtime_error("A-сканы: разная ось времени у пар");
if (normalize) {
for (int p = 0; p < out.n_pairs; ++p) {
double scale = pair_norm_scale(Hs[p], Ts[p], geom.v, bp);
for (auto& c : Hs[p]) c /= (scale + bp.pair_norm_eps);
}
}
out.n_fft = n_fft;
out.t0 = t0;
out.dt = dt;
out.H.resize((size_t)out.n_pairs * n_fft);
for (int p = 0; p < out.n_pairs; ++p)
std::copy(Hs[p].begin(), Hs[p].end(), out.H.begin() + (size_t)p * n_fft);
return out;
}
} // namespace gpr
@@ -0,0 +1,34 @@
#include "gpr/device.hpp"
#include <cstdio>
namespace gpr {
void cu_check(CUresult r, const char* what) {
if (r != CUDA_SUCCESS) {
const char* s = nullptr;
cuGetErrorString(r, &s);
throw std::runtime_error(std::string("CUDA error in ") + what + ": " + (s ? s : "?"));
}
}
CudaContext::CudaContext() {
cu_check(cuInit(0), "cuInit");
cu_check(cuDeviceGet(&dev, 0), "cuDeviceGet");
cu_check(cuDeviceGetAttribute(&cc_major, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR, dev),
"cc_major");
cu_check(cuDeviceGetAttribute(&cc_minor, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR, dev),
"cc_minor");
cu_check(cuDevicePrimaryCtxRetain(&ctx, dev), "primaryCtxRetain");
cu_check(cuCtxSetCurrent(ctx), "ctxSetCurrent");
}
CudaContext::~CudaContext() {
if (ctx) cuDevicePrimaryCtxRelease(dev);
}
std::string CudaContext::arch_flag() const {
return "--gpu-architecture=sm_" + std::to_string(cc_major) + std::to_string(cc_minor);
}
} // namespace gpr
@@ -0,0 +1,104 @@
#include "gpr/geometry.hpp"
#include <algorithm>
#include <cmath>
namespace gpr {
double pair_geo_ref(const GeometryConfig& g, const BPConfig& bp, int i_tx, int i_rx, double yy) {
double xc = (g.x_tx[i_tx] + g.x_rx[i_rx]) / 2.0;
double Rtx = std::sqrt(std::pow(xc - g.x_tx[i_tx], 2) + std::pow(yy - g.y_tx[i_tx], 2) +
std::pow(bp.comp_ref_depth - g.z_tx[i_tx], 2));
double Rrx = std::sqrt(std::pow(xc - g.x_rx[i_rx], 2) + std::pow(yy - g.y_rx[i_rx], 2) +
std::pow(bp.comp_ref_depth - g.z_rx[i_rx], 2));
return 1.0 / (Rtx * Rrx + 1e-12) + 1e-30;
}
std::vector<FrameOffset> frame_offsets(const std::vector<double>& frame_times, double t_ref,
const MotionConfig& cfg) {
std::vector<FrameOffset> out(frame_times.size());
double pitch = cfg.pitch_deg * M_PI / 180.0;
for (size_t k = 0; k < frame_times.size(); ++k) {
double t = frame_times[k] - t_ref;
double h = cfg.direction_sign * cfg.speed_m_s * t;
out[k].dx_m = 0.0;
out[k].dy_m = h * std::sin(pitch);
out[k].dz_m = h * std::cos(pitch);
out[k].dt_s = t;
out[k].horizontal_m = h;
}
return out;
}
std::vector<double> frame_weights(const std::vector<FrameOffset>& offs, double edge_weight) {
size_t n = offs.size();
std::vector<double> w(n, 1.0);
double edge = std::min(std::max(edge_weight, 0.0), 1.0);
if (n == 0 || edge >= 1.0) return w;
double dmax = 0.0;
for (auto& o : offs) dmax = std::max(dmax, std::abs(o.dt_s));
if (dmax <= 1e-12) return w;
for (size_t i = 0; i < n; ++i) {
double r = std::min(std::abs(offs[i].dt_s) / dmax, 1.0);
w[i] = edge + (1.0 - edge) * 0.5 * (1.0 + std::cos(M_PI * r));
}
return w;
}
std::array<double, 3> point_to_scene(double x, double y, double z, const MotionConfig& mot,
const SARConfig& sar) {
if (!sar.coord_mode_ground) return {x, y, z};
double p = mot.pitch_deg * M_PI / 180.0;
double ys = y * std::cos(p) - z * std::sin(p) + sar.ground_depth_m;
double zs = y * std::sin(p) + z * std::cos(p);
return {x, ys, zs};
}
double ground_plane_y(double depth, double ground_depth_m, double pitch_rad) {
return (-ground_depth_m + depth * std::sin(pitch_rad)) / std::cos(pitch_rad);
}
Grid build_full_region_grid(const SARConfig& sar) {
// numpy: linspace(lo, hi, max(round(span/step)+1, 2)); nearbyint = банковское
// округление python round()
auto axis = [](double lo, double hi, double step, std::vector<double>& out) {
int n = std::max((int)std::nearbyint((hi - lo) / step) + 1, 2);
out.resize(n);
for (int i = 0; i < n; ++i) out[i] = lo + (hi - lo) * i / (double)(n - 1);
};
Grid g;
axis(sar.x_min_m, sar.x_max_m, sar.voxel_m[0], g.x);
axis(sar.y_min_m, sar.y_max_m, sar.voxel_m[1], g.y);
axis(sar.z_min_m, sar.z_max_m, sar.voxel_m[2], g.z);
return g;
}
void notebook_grid_bounds(const GeometryConfig& geom, const BPConfig& bp, const SARConfig& sar,
double pitch_deg, double x_margin_m,
double& x_min, double& x_max, double& z_min, double& z_max) {
double x_lo = geom.x_tx[0], x_hi = geom.x_tx[0];
for (double x : geom.x_tx) { x_lo = std::min(x_lo, x); x_hi = std::max(x_hi, x); }
for (double x : geom.x_rx) { x_lo = std::min(x_lo, x); x_hi = std::max(x_hi, x); }
x_min = x_lo - x_margin_m;
x_max = x_hi + x_margin_m;
// Радарная z линейна по Z сцены при фиксированной высоте:
// z_rad(Y, Z=0) = -(Y - ground_depth)*sin(p), z = (depth - z_rad)/cos(p).
double p = pitch_deg * M_PI / 180.0;
double cos_p = std::cos(p), sin_p = std::sin(p);
double z_lo = 0.0, z_hi = 0.0;
bool first = true;
for (double y : {sar.y_min_m, sar.y_max_m}) {
for (double depth : {bp.min_depth, bp.max_depth}) {
double z_rad0 = -(y - sar.ground_depth_m) * sin_p;
double z = (depth - z_rad0) / cos_p;
if (first) { z_lo = z_hi = z; first = false; }
z_lo = std::min(z_lo, z);
z_hi = std::max(z_hi, z);
}
}
z_min = z_lo;
z_max = z_hi;
}
} // namespace gpr
@@ -0,0 +1,83 @@
// Исходник CUDA-ядра для NVRTC (порт sar_frame_contribution из
// Multi_antenna_3D_clean_v1.ipynb). Self-contained complex<float>.
//
// Геометрия (координаты, дальности, tau, вес компенсации) считается в double:
// эталон ноутбука — float64, а в float32 позиция выборки A-скана (тысячи
// отсчётов) теряет ~3e-4 отсчёта и расходится с эталоном на ~1e-4. A-сканы
// остаются float32 — их значения O(1) и линейная интерполяция не теряет.
#pragma once
namespace gpr {
inline const char* APERTURE_KERNEL_SRC = R"CUDA(
struct cf { float x, y; };
// Взвешенный объём апертуры: один поток на воксель, внутренний цикл по
// n_contrib = n_frames*n_pairs. Обрабатывается диапазон вокселей
// [gid_begin, gid_end) — хост запускает ядро чанками, переиспользуя небольшие
// acc/cnt-буферы (полный объём в VRAM не обязан помещаться: на десктопе с
// занятой другими процессами картой свободных может быть <200 МБ).
extern "C" __global__ void bp_aperture_contribution(
const cf* __restrict__ H,
const double t0, const double inv_dt, const int n_fft, const double inv_v,
const double* __restrict__ xt, const double* __restrict__ yt, const double* __restrict__ zt,
const double* __restrict__ xr, const double* __restrict__ yr, const double* __restrict__ zr,
const double* __restrict__ inv_geo_ref, const float* __restrict__ wgt,
const double* __restrict__ gx, const double* __restrict__ gy, const double* __restrict__ gz,
const int NX, const int NY, const int NZ, const int n_contrib,
const long gid_begin, const long gid_end,
const double cos_p, const double sin_p, const double ground_depth,
const double min_depth, const double max_depth,
const double comp_pow, const double w_max, const int use_comp,
cf* __restrict__ acc, float* __restrict__ cnt)
{
for (long gid = gid_begin + blockIdx.x * blockDim.x + threadIdx.x;
gid < gid_end; gid += (long)gridDim.x * blockDim.x) {
int ix = gid % NX;
int iz = (gid / NX) % NZ;
int iy = gid / ((long)NX * NZ);
double x = gx[ix];
double zs = gz[iz];
// Сцена → оси радара: Y=0 сцены — земля, ground_depth ниже нижнего ряда
// антенн (порт sar_scene_to_radar_yz).
double y0 = gy[iy] - ground_depth;
double y_rad = y0 * cos_p + zs * sin_p;
double z_rad = -y0 * sin_p + zs * cos_p;
bool depth_ok = (z_rad >= min_depth) && (z_rad <= max_depth);
double ax = 0.0, ay = 0.0;
float c = 0.f;
for (int i = 0; i < n_contrib; ++i) {
double dxt = x - xt[i], dyt = y_rad - yt[i], dzt = z_rad - zt[i];
double dxr = x - xr[i], dyr = y_rad - yr[i], dzr = z_rad - zr[i];
double Rt = sqrt(dxt*dxt + dyt*dyt + dzt*dzt);
double Rr = sqrt(dxr*dxr + dyr*dyr + dzr*dzr);
double pos = ((Rt + Rr) * inv_v - t0) * inv_dt;
bool inside = (pos >= 0.0) && (pos <= (double)(n_fft - 1));
if (!inside || !depth_ok) continue;
int k = (int)floor(pos);
if (k < 0) k = 0;
if (k > n_fft - 2) k = n_fft - 2;
float f = (float)(pos - (double)k);
const cf* h = H + (long)i * n_fft;
float hx = h[k].x + f * (h[k+1].x - h[k].x);
float hy = h[k].y + f * (h[k+1].y - h[k].y);
double w = (double)wgt[i];
if (use_comp) {
double ratio = (1.0/(Rt*Rr + 1e-12) + 1e-30) * inv_geo_ref[i];
double ww = 1.0/(pow(ratio, comp_pow) + 1e-12);
ww = fmin(fmax(ww, 0.0), w_max);
w *= ww;
}
ax += w * (double)hx;
ay += w * (double)hy;
c += wgt[i];
}
acc[gid - gid_begin].x = (float)ax;
acc[gid - gid_begin].y = (float)ay;
cnt[gid - gid_begin] = c;
}
}
)CUDA";
} // namespace gpr
@@ -0,0 +1,48 @@
#include "gpr/nvrtc_kernel.hpp"
#include <nvrtc.h>
#include <stdexcept>
#include <string>
#include <vector>
#include "gpr/device.hpp"
namespace gpr {
static void nvrtc_check(nvrtcResult r, const char* what) {
if (r != NVRTC_SUCCESS)
throw std::runtime_error(std::string("NVRTC error in ") + what + ": " +
nvrtcGetErrorString(r));
}
NvrtcKernel::~NvrtcKernel() {
if (module_) cuModuleUnload(module_);
}
void NvrtcKernel::compile(const char* src, const std::string& arch_flag, const char* func_name) {
nvrtcProgram prog;
nvrtc_check(nvrtcCreateProgram(&prog, src, "gpr_kernel.cu", 0, nullptr, nullptr),
"createProgram");
std::vector<const char*> opts{arch_flag.c_str()};
nvrtcResult cr = nvrtcCompileProgram(prog, (int)opts.size(), opts.data());
if (cr != NVRTC_SUCCESS) {
size_t logsz = 0;
nvrtcGetProgramLogSize(prog, &logsz);
std::string log(logsz, '\0');
if (logsz) nvrtcGetProgramLog(prog, &log[0]);
nvrtcDestroyProgram(&prog);
throw std::runtime_error("NVRTC compile failed:\n" + log);
}
// cubin под точную арх (без JIT PTX — драйвер грузит SASS напрямую)
size_t cubsz = 0;
nvrtc_check(nvrtcGetCUBINSize(prog, &cubsz), "getCUBINSize");
std::string cubin(cubsz, '\0');
nvrtc_check(nvrtcGetCUBIN(prog, &cubin[0]), "getCUBIN");
nvrtcDestroyProgram(&prog);
cu_check(cuModuleLoadDataEx(&module_, cubin.data(), 0, nullptr, nullptr), "moduleLoad");
cu_check(cuModuleGetFunction(&func_, module_, func_name), "getFunction");
}
} // namespace gpr
@@ -0,0 +1,233 @@
#include "gpr/sar3d.hpp"
#include <algorithm>
#include <cmath>
#include <stdexcept>
#include "kernel_src.hpp"
namespace gpr {
SarEngine::SarEngine(const GeometryConfig& g, const BPConfig& bp, const MotionConfig& mot,
const SARConfig& sar)
: geom_(g), bp_(bp), mot_(mot), sar_(sar) {
geom_.finalize();
ctx_ = std::make_unique<CudaContext>();
kernel_ = std::make_unique<NvrtcKernel>();
kernel_->compile(APERTURE_KERNEL_SRC, ctx_->arch_flag(), "bp_aperture_contribution");
}
VolumeRaw SarEngine::aperture_volume(const std::vector<ApertureFrame>& frames, const Grid& grid) {
if (frames.empty()) throw std::runtime_error("aperture_volume: нет кадров");
auto pairs = geom_.pair_keys();
int n_pairs = (int)pairs.size();
int n_frames = (int)frames.size();
int n_contrib = n_frames * n_pairs;
const AScanSet& a0 = *frames[0].ascans;
int n_fft = a0.n_fft;
double t0 = a0.t0, dt = a0.dt;
int NX = grid.NX(), NY = grid.NY(), NZ = grid.NZ();
size_t nvox = (size_t)NX * NY * NZ;
double pitch = mot_.pitch_deg * M_PI / 180.0;
// Опорное расхождение пары — на плоскости земли на глубине comp_ref_depth.
double geo_ref_y = sar_.coord_mode_ground
? ground_plane_y(bp_.comp_ref_depth, sar_.ground_depth_m, pitch)
: 0.0;
// Плоские массивы по (кадр, пара)
std::vector<c64> H_stack((size_t)n_contrib * n_fft);
std::vector<double> xt(n_contrib), yt(n_contrib), zt(n_contrib);
std::vector<double> xr(n_contrib), yr(n_contrib), zr(n_contrib);
std::vector<double> igr(n_contrib);
std::vector<float> wgt(n_contrib);
int idx = 0;
for (int f = 0; f < n_frames; ++f) {
const AScanSet& a = *frames[f].ascans;
if (a.n_fft != n_fft || std::abs(a.t0 - t0) > 1e-18)
throw std::runtime_error("кадры апертуры: разная ось времени");
double dx = frames[f].offset.dx_m, dy = frames[f].offset.dy_m, dz = frames[f].offset.dz_m;
float w = (float)frames[f].weight;
for (int p = 0; p < n_pairs; ++p) {
int i = pairs[p].first, j = pairs[p].second;
// A-скан пары p → в стек (каст c128→c64)
const c128* src = a.pair_ptr(p);
c64* dst = H_stack.data() + (size_t)idx * n_fft;
for (int k = 0; k < n_fft; ++k) dst[k] = c64((float)src[k].real(), (float)src[k].imag());
xt[idx] = geom_.x_tx[i] + dx; yt[idx] = geom_.y_tx[i] + dy;
zt[idx] = geom_.z_tx[i] + dz;
xr[idx] = geom_.x_rx[j] + dx; yr[idx] = geom_.y_rx[j] + dy;
zr[idx] = geom_.z_rx[j] + dz;
igr[idx] = 1.0 / pair_geo_ref(geom_, bp_, i, j, geo_ref_y);
wgt[idx] = w;
++idx;
}
}
double cos_p = 1.0, sin_p = 0.0, ground_depth = 0.0;
if (sar_.coord_mode_ground) {
cos_p = std::cos(pitch);
sin_p = std::sin(pitch);
ground_depth = sar_.ground_depth_m;
}
// Заливка на устройство
DeviceBuffer<c64> dH(H_stack.size()); dH.upload(H_stack.data(), H_stack.size());
DeviceBuffer<double> dxt(n_contrib), dyt(n_contrib), dzt(n_contrib);
DeviceBuffer<double> dxr(n_contrib), dyr(n_contrib), dzr(n_contrib);
DeviceBuffer<double> digr(n_contrib);
DeviceBuffer<float> dwgt(n_contrib);
dxt.upload(xt.data(), n_contrib); dyt.upload(yt.data(), n_contrib); dzt.upload(zt.data(), n_contrib);
dxr.upload(xr.data(), n_contrib); dyr.upload(yr.data(), n_contrib); dzr.upload(zr.data(), n_contrib);
digr.upload(igr.data(), n_contrib); dwgt.upload(wgt.data(), n_contrib);
DeviceBuffer<double> dgx(NX), dgy(NY), dgz(NZ);
dgx.upload(grid.x.data(), NX); dgy.upload(grid.y.data(), NY); dgz.upload(grid.z.data(), NZ);
// Объём считается чанками с переиспользуемыми acc/cnt-буферами: полные
// буферы (~115 МБ на сетку 9.6 млн вокселей) могут не влезать в остаток
// VRAM, когда карту делят десктоп-процессы.
const size_t chunk_vox = std::min<size_t>(nvox, 2'000'000);
DeviceBuffer<c64> dacc(chunk_vox);
DeviceBuffer<float> dcnt(chunk_vox);
double inv_dt = 1.0 / dt, inv_v = 1.0 / geom_.v;
double min_d = bp_.min_depth, max_d = bp_.max_depth;
double comp_pow = bp_.comp_range_power, w_max = bp_.comp_weight_max;
int use_comp = sar_.use_compensation ? 1 : 0;
VolumeRaw out;
out.NX = NX; out.NY = NY; out.NZ = NZ;
out.acc.resize(nvox); out.cnt.resize(nvox);
int block = 256;
for (size_t begin = 0; begin < nvox; begin += chunk_vox) {
const size_t end = std::min(nvox, begin + chunk_vox);
long gid_begin = (long)begin, gid_end = (long)end;
void* args[] = {
dH.addr(), &t0, &inv_dt, &n_fft, &inv_v,
dxt.addr(), dyt.addr(), dzt.addr(), dxr.addr(), dyr.addr(), dzr.addr(),
digr.addr(), dwgt.addr(), dgx.addr(), dgy.addr(), dgz.addr(),
&NX, &NY, &NZ, &n_contrib, &gid_begin, &gid_end,
&cos_p, &sin_p, &ground_depth, &min_d, &max_d,
&comp_pow, &w_max, &use_comp, dacc.addr(), dcnt.addr()};
int gridDim = (int)std::min<long>(65535, ((long)(end - begin) + block - 1) / block);
cu_check(cuLaunchKernel(kernel_->func(), gridDim, 1, 1, block, 1, 1, 0, 0, args, nullptr),
"launch aperture");
cu_check(cuCtxSynchronize(), "sync");
dacc.download(out.acc.data() + begin, end - begin);
dcnt.download(out.cnt.data() + begin, end - begin);
}
return out;
}
namespace {
// scipy.ndimage.gaussian_filter: ядро радиуса int(truncate*sigma+0.5), reflect.
std::vector<double> gaussian_kernel_1d(double sigma, int radius) {
std::vector<double> w(2 * radius + 1);
double sum = 0.0;
for (int i = -radius; i <= radius; ++i) {
w[i + radius] = std::exp(-0.5 * i * i / (sigma * sigma));
sum += w[i + radius];
}
for (auto& v : w) v /= sum;
return w;
}
// Корреляция вдоль оси с reflect-границами ('d c b a | a b c d').
inline int reflect_index(int i, int n) {
while (i < 0 || i >= n) {
if (i < 0) i = -i - 1;
if (i >= n) i = 2 * n - i - 1;
}
return i;
}
} // namespace
FinishedVolume finish_volume(const VolumeRaw& raw, const Grid& grid, double pitch_deg,
const BPConfig& bp, const SARConfig& sar) {
int NX = raw.NX, NY = raw.NY, NZ = raw.NZ;
size_t nvox = (size_t)NX * NY * NZ;
FinishedVolume out;
out.NX = NX; out.NY = NY; out.NZ = NZ;
std::vector<double> vol(nvox);
for (size_t i = 0; i < nvox; ++i)
vol[i] = std::abs(c128(raw.acc[i].real(), raw.acc[i].imag())) / ((double)raw.cnt[i] + 1e-12);
// Маска глубины в радарных осях (в ядре невалидные вкладов не получали,
// но |0/(0+eps)| уже 0; обнуление здесь повторяет ноутбук буквально).
if (sar.coord_mode_ground) {
double p = pitch_deg * M_PI / 180.0;
double cos_p = std::cos(p), sin_p = std::sin(p);
for (int iy = 0; iy < NY; ++iy) {
double y0 = grid.y[iy] - sar.ground_depth_m;
for (int iz = 0; iz < NZ; ++iz) {
double z_rad = -y0 * sin_p + grid.z[iz] * cos_p;
if (z_rad >= bp.min_depth && z_rad <= bp.max_depth) continue;
double* row = vol.data() + ((size_t)iy * NZ + iz) * NX;
std::fill(row, row + NX, 0.0);
}
}
}
// Плоскости Y для нормировки: без верхних/нижних norm_edge_margin_m.
std::vector<bool> ref_plane(NY, true);
double margin = sar.norm_edge_margin_m;
double y_span = grid.y.back() - grid.y.front();
if (margin > 0.0 && y_span > 2.5 * margin) {
for (int iy = 0; iy < NY; ++iy)
ref_plane[iy] = grid.y[iy] >= grid.y.front() + margin &&
grid.y[iy] <= grid.y.back() - margin;
}
auto ref_max = [&](const std::vector<double>& v) {
double m = 0.0;
for (int iy = 0; iy < NY; ++iy) {
if (!ref_plane[iy]) continue;
const double* plane = v.data() + (size_t)iy * NZ * NX;
for (size_t i = 0; i < (size_t)NZ * NX; ++i) m = std::max(m, plane[i]);
}
return m + 1e-12;
};
double level_raw = ref_max(vol);
for (auto& v : vol) v /= level_raw;
// Гаусс sigma=(0, s, s): сглаживание по Z и X внутри каждого Y-плана
// (порядок осей как у scipy: сначала ось 1, затем ось 2).
if (sar.smooth_sigma > 0.0) {
int radius = (int)(4.0 * sar.smooth_sigma + 0.5);
const auto kernel = gaussian_kernel_1d(sar.smooth_sigma, radius);
std::vector<double> tmp((size_t)NZ * NX);
for (int iy = 0; iy < NY; ++iy) {
double* plane = vol.data() + (size_t)iy * NZ * NX;
// вдоль Z (ось 1)
for (int iz = 0; iz < NZ; ++iz)
for (int ix = 0; ix < NX; ++ix) {
double s = 0.0;
for (int k = -radius; k <= radius; ++k)
s += kernel[k + radius] * plane[(size_t)reflect_index(iz + k, NZ) * NX + ix];
tmp[(size_t)iz * NX + ix] = s;
}
// вдоль X (ось 2)
for (int iz = 0; iz < NZ; ++iz)
for (int ix = 0; ix < NX; ++ix) {
double s = 0.0;
for (int k = -radius; k <= radius; ++k)
s += kernel[k + radius] * tmp[(size_t)iz * NX + reflect_index(ix + k, NX)];
plane[(size_t)iz * NX + ix] = s;
}
}
}
double level_smooth = ref_max(vol);
out.volume.resize(nvox);
for (size_t i = 0; i < nvox; ++i)
out.volume[i] = (float)std::max(vol[i] / level_smooth, 0.0);
out.level = level_raw * level_smooth;
return out;
}
} // namespace gpr
File diff suppressed because it is too large Load Diff
@@ -13,12 +13,18 @@ counterpart to the C++ `sweep_orchestrator` used by LibreVNA devices.
| Path | Contents |
|------|----------|
| `src/` | Collector implementation (`main.cpp`) and the PTY / capture-file writers. |
| `include/` | Headers for the writers. |
| `include/` | Headers for the writers plus `switch_sequencer.h`. |
| `vendor/lcard/` | L-Card X502/E502 SDK headers (`x502api.h`, `e502api.h`, `lcard_pstdint.h`), vendored verbatim so the collector builds with no external include path. |
The sources under `src/` and `include/` are imported verbatim from the original
standalone tool and are kept byte-for-byte identical — the wire format and
acquisition behaviour are unchanged; only their home moved into this repo.
The sources started as a verbatim import of the original standalone tool, but are
**no longer identical to it**: the collector now drives the RF switches itself
(switch-aware mode, `SwitchSequencer`), stepping to the next combination inside
the hardware idle gap between sweeps and flagging a sweep *dirty* when a readout
backlog means the toggle could not have landed in that gap. It therefore reuses
the orchestrator's switch drivers and the shared `run_config` loader, which is why
its build has its own include set and its own `build/kamil/` object tree.
The wire format towards the Python producer is unchanged.
## Build
@@ -0,0 +1,147 @@
#pragma once
#include <cstddef>
#include <memory>
#include <utility>
#include <vector>
#include "shared_types.hpp"
#include "switch_driver.hpp"
namespace radar::kamil {
/**
* @brief Drives the input/output RF switches in lock-step with sweep boundaries.
*
* The E-502 stream is free-running: sweeps are delimited by DI_SYN2 edges with a
* hardware idle gap between them. On every completed sweep we step to the next
* switch combination *during that gap*, so the next sweep starts already settled
* in the new state and no samples are lost.
*
* The one failure mode of doing this purely in software is the readout backlog:
* if, at the moment we observe a sweep's end, we are running so far behind real
* time that the toggle could not have landed within the gap, the upcoming sweep
* straddles the transition. We flag that sweep *dirty*; the consumer drops it and
* we re-take the same combination on the following sweep (which is then clean,
* because no transition happens during its gap). A clean sweep costs nothing; a
* miss costs exactly one extra sweep period for that one combination.
*
* The sequencer owns the two switch drivers and exposes only what the collector
* needs at a sweep boundary: which combination the upcoming sweep belongs to and
* whether it is expected to be clean. All timing policy lives here so the recv
* loop stays readable.
*/
class SwitchSequencer {
public:
struct Settings {
/// Combinations to cycle through, one sweep each, in order. Empty disables
/// the sequencer entirely (the collector then streams a single passthrough
/// channel exactly as before).
std::vector<radar::ipc::ComboKey> combos{};
/// Guaranteed minimum inter-sweep idle window, in milliseconds. This is a
/// hardware property of the sweep generator; treat it as a lower bound.
double gap_ms = 10.0;
/// RF settle time required after a position change, in milliseconds.
double settle_ms = 1.0;
};
SwitchSequencer(
Settings settings,
std::unique_ptr<drivers::SwitchDriver> input_switch,
std::unique_ptr<drivers::SwitchDriver> output_switch
)
: settings_(std::move(settings)),
input_switch_(std::move(input_switch)),
output_switch_(std::move(output_switch)) {}
[[nodiscard]] auto enabled() const -> bool { return !settings_.combos.empty(); }
/// Open both switch drivers and park at the first combination.
void open() {
if (!enabled()) {
return;
}
output_switch_->open();
input_switch_->open();
index_ = 0;
applied_ = settings_.combos.front();
apply(applied_);
upcoming_dirty_ = false; // the first sweep is captured in a settled state
}
void close() {
if (!enabled()) {
return;
}
// Best-effort: teardown must never throw out of the collector's stop path.
try {
output_switch_->close();
} catch (...) {
}
try {
input_switch_->close();
} catch (...) {
}
}
/// Combination the in-progress / upcoming sweep is captured under.
[[nodiscard]] auto current_combo() const -> radar::ipc::ComboKey {
return settings_.combos[index_];
}
/// Whether the in-progress / upcoming sweep is expected to straddle a switch
/// transition and must be dropped by the consumer.
[[nodiscard]] auto current_dirty() const -> bool { return upcoming_dirty_; }
/**
* @brief Advance the sequence at a completed sweep's end (start of the gap).
*
* @param backlog_ms Estimated readout lag at this instant — how far behind
* real time we are, i.e. how late the toggle we issue now will land.
*
* If the sweep that just ended was clean we step to the next combination and
* toggle the switches now, inside the gap. If it was dirty we re-take the same
* combination (the switches are already there and long settled). The upcoming
* sweep is marked dirty only when a real transition happens *and* the backlog
* leaves no room for it to settle before the next sweep starts.
*/
void on_sweep_end(double backlog_ms) {
if (!enabled()) {
return;
}
if (upcoming_dirty_) {
// The sweep that just ended straddled a transition: re-take the same
// combination. The switches are already applied and settled, so the
// next sweep is clean without touching the hardware again.
upcoming_dirty_ = false;
return;
}
index_ = (index_ + 1U) % settings_.combos.size();
const radar::ipc::ComboKey next = settings_.combos[index_];
const bool position_changed = !(next == applied_);
if (position_changed) {
apply(next);
applied_ = next;
}
upcoming_dirty_ = position_changed && ((backlog_ms + settings_.settle_ms) > settings_.gap_ms);
}
private:
void apply(const radar::ipc::ComboKey& combo) {
// Output first, then input — same order the sweep orchestrator uses, so
// both acquisition paths drive an identical hardware sequence.
output_switch_->switch_to(combo.output_pos);
input_switch_->switch_to(combo.input_pos);
}
Settings settings_;
std::unique_ptr<drivers::SwitchDriver> input_switch_;
std::unique_ptr<drivers::SwitchDriver> output_switch_;
std::size_t index_ = 0;
radar::ipc::ComboKey applied_{};
bool upcoming_dirty_ = false;
};
} // namespace radar::kamil
@@ -17,6 +17,11 @@
#include "capture_file_writer.h"
#include "tty_protocol_writer.h"
#include "switch_sequencer.h"
#include "run_config.hpp"
#include "h7992_minimal_driver.hpp"
#include "hmc349a_minimal_driver.hpp"
#include <algorithm>
#include <array>
#include <atomic>
@@ -113,6 +118,12 @@ struct Config {
std::string live_html_path = "live_plot.html";
std::string live_json_path = "live_plot.json";
std::optional<std::string> tty_path;
// When set, the collector reads switch and combo configuration from this
// run_config.json and drives the RF switches itself, one combination per
// sweep, tagging each emitted sweep with its combo (see SwitchSequencer).
std::optional<std::string> run_config_path;
double switch_gap_ms = 10.0;
double switch_settle_ms = 1.0;
bool di1_group_average = false;
bool do1_toggle_per_frame = false;
bool do1_noise_subtract = false;
@@ -479,6 +490,7 @@ void print_help(const char* exe_name) {
<< " [di1:zero|trace|ignore]\n"
<< " [duration_ms:100] [packet_limit:0] [csv:capture.csv] [svg:capture.svg]\n"
<< " [live_html:live_plot.html] [live_json:live_plot.json] [tty:/tmp/ttyADC_data] [di1_group_avg]\n"
<< " [config:run_config.json] [switch_gap_ms:10] [switch_settle_ms:1]\n"
<< " [do1_toggle_per_frame] [do1_noise_subtract] [do1_raw_tty_marked] [do1_pair_subtract_avg] [noise_avg_steps:N]\n"
<< " [do8_freq_ref] [do8_cycle_period:10] [do8_threshold:X]\n"
<< " [recv_block:32768] [stats_period_ms:1000] [live_update_period_ms:1000] [svg_history_packets:50] [start_wait_ms:10000]\n"
@@ -514,6 +526,13 @@ void print_help(const char* exe_name) {
<< " step_words:32768 -> input stream transfer step in 32-bit words\n"
<< " live_html/live_json -> live graph files updated as packets arrive outside tty fast stream-only modes\n"
<< " tty:/tmp/ttyADC_data -> write a continuous legacy 4-word CH1/CH2 stream; with channels:1, CH2 is 0\n"
<< " config:run_config.json -> switch-aware mode: read the input/output switch and combo\n"
<< " configuration from this run_config.json and drive the RF switches\n"
<< " in lock-step with sweeps; each sweep is tagged with its combo via a\n"
<< " 0x00C0 frame (input_pos, output_pos, dirty). Omit for standalone mode.\n"
<< " switch_gap_ms:10 -> guaranteed minimum inter-sweep idle window; lower bound used to decide\n"
<< " whether an in-gap switch lands before the next sweep starts\n"
<< " switch_settle_ms:1 -> RF settle time required after a switch position change\n"
<< " di1_group_avg -> with tty + di1:trace, emit one averaged 4-word step per constant DI1 run\n"
<< " do1_toggle_per_frame -> hardware cyclic DO1 pattern in module memory:\n"
<< " DO1 outputs 00110011... continuously (toggle every 2 ADC ticks)\n"
@@ -793,6 +812,18 @@ Config parse_args(int argc, char** argv) {
cfg.tty_path = arg.substr(4);
continue;
}
if (starts_with(arg, "config:")) {
cfg.run_config_path = arg.substr(7);
continue;
}
if (starts_with(arg, "switch_gap_ms:")) {
cfg.switch_gap_ms = parse_double(arg.substr(14), "switch_gap_ms");
continue;
}
if (starts_with(arg, "switch_settle_ms:")) {
cfg.switch_settle_ms = parse_double(arg.substr(17), "switch_settle_ms");
continue;
}
fail("Unknown argument: " + arg);
}
@@ -1203,6 +1234,10 @@ constexpr uint32_t kDo1TogglePeriodTicks = 2U;
constexpr uint32_t kDo1CyclePatternWords = kDo1TogglePeriodTicks * 2U;
constexpr uint32_t kDo8HighTicks = 2U;
constexpr uint16_t kTtyMarkerDi8High = 0x00A8U;
// Combo tag frame: [0x00C0, input_pos, output_pos, dirty]. Emitted right after a
// sweep-boundary frame to tell the consumer which switch combination the upcoming
// sweep belongs to, and whether it straddled a switch transition (dirty != 0).
constexpr uint16_t kTtyMarkerComboTag = 0x00C0U;
constexpr uint32_t kStreamInputAdcFlag = 0x80000000U;
constexpr uint32_t kStreamInputCalibratedAdcFlag = 0x40000000U;
@@ -1802,6 +1837,55 @@ void print_device_info(const t_x502_info& info) {
<< "MCU firmware: " << info.mcu_firmware_ver << "\n";
}
// Build one switch driver (native GPIO or mock) from a parsed switch config,
// reusing the exact driver implementations the sweep orchestrator uses so both
// acquisition paths drive identical hardware.
std::unique_ptr<radar::drivers::SwitchDriver> make_switch_driver(const radar::config::SwitchConfig& sc) {
if (sc.driver_kind == radar::config::SwitchDriverKind::HMC349A) {
return std::make_unique<radar::drivers::HMC349AMinimalDriver>(
radar::drivers::HMC349AMinimalDriverSettings{
.name = sc.name,
.mode = sc.driver_mode,
.positions = sc.positions,
.default_position = sc.default_position,
.gpio_chip = sc.gpio_chip,
.pin_a = sc.pin_a,
.invert_logic = sc.invert_logic,
});
}
return std::make_unique<radar::drivers::H7992MinimalDriver>(
radar::drivers::H7992MinimalDriverSettings{
.name = sc.name,
.mode = sc.driver_mode,
.positions = sc.positions,
.default_position = sc.default_position,
.gpio_chip = sc.gpio_chip,
.pin_a = sc.pin_a,
.pin_b = sc.pin_b,
});
}
// Construct the switch sequencer from the run_config given via `config:<path>`.
// Returns nullptr (switching disabled, single-channel passthrough) when no config
// was supplied or it declares no combinations.
std::unique_ptr<radar::kamil::SwitchSequencer> make_switch_sequencer(const Config& cfg) {
if (!cfg.run_config_path.has_value()) {
return nullptr;
}
const radar::config::RunConfig rc = radar::config::load_run_config(*cfg.run_config_path);
if (rc.run_combos.empty()) {
return nullptr;
}
radar::kamil::SwitchSequencer::Settings settings;
settings.combos = rc.run_combos;
settings.gap_ms = cfg.switch_gap_ms;
settings.settle_ms = cfg.switch_settle_ms;
return std::make_unique<radar::kamil::SwitchSequencer>(
std::move(settings),
make_switch_driver(rc.input_switch),
make_switch_driver(rc.output_switch));
}
int run(const Config& cfg) {
Api api;
DeviceHandle device(api);
@@ -2029,6 +2113,21 @@ int run(const Config& cfg) {
tty_writer->emit_packet_start(tty_packet_start_marker);
}
}
// Switch sequencer (optional): drives the RF switches in lock-step with sweep
// boundaries and tags each sweep with its combination. Disabled (nullptr) when
// no run_config was passed, so the standalone/calibration use stays unchanged.
std::unique_ptr<radar::kamil::SwitchSequencer> sequencer = make_switch_sequencer(cfg);
if (sequencer && sequencer->enabled()) {
sequencer->open();
std::cout << "Switch sequencer enabled: cycling switch combinations per sweep "
<< "(gap_ms=" << cfg.switch_gap_ms << ", settle_ms=" << cfg.switch_settle_ms << ")\n";
} else {
sequencer.reset(); // normalize to nullptr so call sites can test the pointer
}
// Estimated readout backlog (ms) at the current loop iteration; how far behind
// real time we are, used to decide whether an in-gap switch lands in time.
double current_backlog_ms = 0.0;
std::unique_ptr<CaptureFileWriter> writer;
if (!fast_tty_avg_stream_mode) {
writer = std::make_unique<CaptureFileWriter>(cfg.csv_path, cfg.svg_path, cfg.live_html_path, cfg.live_json_path);
@@ -2317,6 +2416,15 @@ int run(const Config& cfg) {
auto append_tty_packet_start = [&]() {
append_tty_frame(tty_packet_start_marker, 0xFFFF, 0xFFFF, 0xFFFF);
// Right after the boundary, tag the upcoming sweep with its switch combo so
// the consumer can group sweeps and drop the dirty ones.
if (sequencer) {
const radar::ipc::ComboKey combo = sequencer->current_combo();
append_tty_frame(kTtyMarkerComboTag,
static_cast<uint16_t>(combo.input_pos),
static_cast<uint16_t>(combo.output_pos),
static_cast<uint16_t>(sequencer->current_dirty() ? 1U : 0U));
}
};
auto append_tty_group_step = [&]() {
@@ -2714,6 +2822,12 @@ int run(const Config& cfg) {
std::cout << "\n";
}
// A real sweep just completed: step the switch sequence into the gap. A
// user-stop teardown is not a sweep boundary, so skip it.
if (sequencer && (frames != 0U) && (reason != PacketCloseReason::UserStop)) {
sequencer->on_sweep_end(current_backlog_ms);
}
packet_active = false;
packet_avg_steps = 0;
fast_packet_frames = 0;
@@ -2768,6 +2882,11 @@ int run(const Config& cfg) {
recv_request_words = std::min<uint32_t>(ready_words, read_capacity_words);
recv_timeout_ms = 0;
}
// Backlog already queued in the driver = how far behind real time we are.
// Drives the switch sequencer's in-gap clean/dirty decision.
if ((ready_err == X502_ERR_OK) && (combined_input_rate_hz > 0.0)) {
current_backlog_ms = 1000.0 * static_cast<double>(ready_words) / combined_input_rate_hz;
}
const int32_t recvd = api.Recv(device.hnd, raw.data(), recv_request_words, recv_timeout_ms);
if (recvd < 0) {
@@ -3170,6 +3289,11 @@ int run(const Config& cfg) {
expect_ok(api, api.StreamsStop(device.hnd), "Stop streams");
device.streams_started = false;
// Park the switches in their safe default and release the GPIO lines.
if (sequencer) {
sequencer->close();
}
if (cfg.do1_toggle_per_frame) {
const uint32_t clear_mask = kE502Do1Mask | kE502Do2Mask | (cfg.do8_freq_ref ? kE502Do8Mask : 0U);
expect_ok(api,
@@ -34,6 +34,9 @@ auto CalibrationMaster::apply_to_trace(const ipc::SweepTraceBlock& measured_trac
output.frequency_hz = measured_trace.frequency_hz;
output.s21 = apply_s21(measured_trace.combo, measured_trace.frequency_hz, measured_trace.s21);
output.s11 = apply_s11(measured_trace.combo, measured_trace.frequency_hz, measured_trace.s11);
// Calibration reshapes the samples, not when they were measured.
output.capture_start_ns = measured_trace.capture_start_ns;
output.capture_end_ns = measured_trace.capture_end_ns;
return output;
}
@@ -132,6 +132,9 @@ auto ReferenceMaster::apply_to_trace(const ipc::SweepTraceBlock& calibrated_trac
output.frequency_hz = calibrated_trace.frequency_hz;
output.s21 = apply_s21(calibrated_trace.combo, calibrated_trace.frequency_hz, calibrated_trace.s21);
output.s11 = apply_s11(calibrated_trace.combo, calibrated_trace.frequency_hz, calibrated_trace.s11);
// Reference subtraction reshapes the samples, not when they were measured.
output.capture_start_ns = calibrated_trace.capture_start_ns;
output.capture_end_ns = calibrated_trace.capture_end_ns;
return output;
}
@@ -45,8 +45,10 @@ struct ProcessingLiveConfig {
float gpr_min_depth_m = 2.0F;
float gpr_max_depth_m = 14.0F;
float gpr_range_comp_power = 0.1F;
float gpr_angle_comp_power = 0.0F;
float gpr_comp_power = 0.2F;
// BP object-detection stop level, as a fraction of the global peak (Python
// Horns_motion_3libre.py BP_OBJECT_MIN_FRAC); peaks below it are not objects.
float gpr_object_min_frac = 0.7F;
std::string gpr_score_mode = "combined";
// Backprojection intra-sweep speed-correction mode: "int_minus" (full
// correction) or "int_focus" (focusing residual only). Mirrors the Python
@@ -73,19 +75,66 @@ struct ProcessingLiveConfig {
// BP image is computed in the y=imaging_plane_y_m slice of the 3D grid.
// Default 0 keeps legacy 1D antenna layouts imaging in the antenna plane.
float gpr_imaging_plane_y_m = 0.0F;
// Locator filter parameters. Mode-dependent threshold (legacy_gpr uses
// `legacy_gpr_min_visible_pair_count`, everything else uses
// `gpr_min_visible_score`). Draw limits apply only to non-legacy modes.
float gpr_min_visible_score = 0.0F;
// Coherent BP object visibility (window + the draw limits below) is applied in
// the processor itself, matching Horns_motion_3libre.py — there is NO score
// threshold for it. Only legacy GPR still thresholds, on a pair count.
float legacy_gpr_min_visible_pair_count = 0.0F;
std::uint32_t gpr_max_detected_objects_to_draw = 0;
std::uint32_t gpr_draw_top_m_objects = 0;
// Cross-frame approach filter: an object is shown only once it persists as a
// motion-consistent track over this many consecutive frames (<= 1 disables it).
std::uint32_t gpr_object_approach_min_frames = 3;
// Visible X/Z window (metres). The locator clips broadcast objects to this
// window so the socket emits only what the desktop plot actually shows.
float gpr_visible_x_min_m = -2.0F;
float gpr_visible_x_max_m = 2.0F;
float gpr_visible_z_min_m = 0.0F;
float gpr_visible_z_max_m = 14.0F;
// 3D SAR (sar3d processor) parameters; defaults mirror the reference
// notebook Multi_antenna_3D_clean_v1.ipynb (cells 0/4). Frequency band,
// depth gate, antenna selection and motion (speed, look angle, direction,
// phase correction) are shared with the gpr_* fields.
std::uint32_t sar3d_frame_radius = 0U;
float sar3d_edge_frame_weight = 0.8F;
// Frame pitch for ground coordinates and inter-frame offsets. Override
// disabled -> the pitch follows gpr_look_angle_deg (notebook
// BP_GROUND_PITCH_DEG = None).
bool sar3d_pitch_override_enabled = false;
float sar3d_pitch_deg = 17.5F;
// Height of the LOWER antenna row above the ground; scene Y=0 is the ground.
float sar3d_ground_depth_m = 1.25F;
// Scene grid bounds (metres): X across, Y height above the ground, Z along
// the ground away from the radar. Voxels whose radar depth falls outside
// [gpr_min_depth, gpr_max_depth] are zeroed by the mask regardless.
// Doubles, not floats: the parity tool feeds the notebook-derived bounds
// through here and a float roundtrip shifts the grid by ~1e-7 m, flipping
// knife-edge voxels of the depth mask against the reference volumes.
double sar3d_x_min_m = -2.0;
double sar3d_x_max_m = 2.0;
double sar3d_y_min_m = -0.5;
double sar3d_y_max_m = 2.0;
double sar3d_z_min_m = 0.0;
double sar3d_z_max_m = 8.0;
float sar3d_voxel_x_m = 0.025F;
float sar3d_voxel_y_m = 0.02F;
float sar3d_voxel_z_m = 0.02F;
// Volume finalization: normalization ignores this much of the grid's Y
// edges (constant-range ridge), then Gaussian smoothing over (Z, X).
float sar3d_norm_edge_margin_m = 0.3F;
float sar3d_smooth_sigma = 1.5F;
// Geometric spreading compensation 1/(Rtx*Rrx)^power, capped.
float sar3d_comp_range_power = 0.3F;
// Fallback frame segmentation when per-trace capture stamps are absent.
std::uint32_t sar3d_tx_per_segment = 2U;
bool sar3d_use_compensation = true;
bool sar3d_normalize_pairs = true;
// Debug: when non-empty, the processor dumps every finished volume to this
// directory (raw float32 + json header). Used by the parity tool only.
std::string sar3d_volume_dump_dir{};
// Bottom-view projection angle around the vertical Y axis, degrees:
// 0 = front view (X horizontal), 90 = side view (Z horizontal),
// intermediate values project along the rotated horizontal direction.
float sar3d_bottom_view_deg = 0.0F;
// When true, the data_processor ignores socket-supplied `vlc` updates and
// keeps using `gpr_speed_m_s` from this file. Mirrored from the GUI's
// "ignore socket speed" checkbox.
@@ -13,6 +13,7 @@
#include "bscan_processor.hpp"
#include "gpr_processor.hpp"
#include "passthrough_processor.hpp"
#include "sar3d_processor.hpp"
namespace radar::processing {
namespace {
@@ -65,7 +66,7 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
std::uint64_t last_applied_history_command_seq = 0;
std::uint64_t error_count = 0;
std::uint64_t consecutive_errors = 0;
// Fix #55: track the socket-fed speed used for the last reprocess so a change
// track the socket-fed speed used for the last reprocess so a change
// arriving without a live-config revision bump still triggers a reprocess of
// the current result (gated below by reprocess_current_result).
std::optional<double> last_reprocessed_socket_speed = std::nullopt;
@@ -124,7 +125,7 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
publish_locator(replay_result, live_config);
}
last_replayed_revision = live_revision;
// Fix #55: record the speed we just reprocessed with so an
//record the speed we just reprocessed with so an
// unchanged socket value does not retrigger every iteration.
last_reprocessed_socket_speed = current_socket_speed;
}
@@ -232,29 +233,23 @@ auto DataProcessor::build_locator_filter(const ProcessingLiveConfig& live_config
live_config.processor_mode.empty() ? default_processor_mode_ : live_config.processor_mode;
radar::locator::FilterParams filter{};
// Clip broadcast objects to the same visible X/Z window the desktop plot uses,
// so the socket emits only the objects the operator actually sees.
filter.visible_bounds = radar::locator::VisibleBounds{
.x_min = live_config.gpr_visible_x_min_m,
.x_max = live_config.gpr_visible_x_max_m,
.z_min = live_config.gpr_visible_z_min_m,
.z_max = live_config.gpr_visible_z_max_m,
};
if (requested_mode == "legacy_gpr") {
// Legacy GPR emits its objects unfiltered, so the socket applies the legacy
// rule here: clip to the visible window and threshold on the pair count. The
// GUI disables "draw top N" for legacy, so we skip it on the wire to match.
filter.visible_bounds = radar::locator::VisibleBounds{
.x_min = live_config.gpr_visible_x_min_m,
.x_max = live_config.gpr_visible_x_max_m,
.z_min = live_config.gpr_visible_z_min_m,
.z_max = live_config.gpr_visible_z_max_m,
};
filter.min_score = live_config.legacy_gpr_min_visible_pair_count;
// The GUI deliberately disables the "draw top N" capping for legacy
// GPR, so we also skip it on the wire to match observation semantics.
filter.draw_limits.reset();
} else {
filter.min_score = live_config.gpr_min_visible_score;
if (live_config.gpr_max_detected_objects_to_draw > 0U
&& live_config.gpr_draw_top_m_objects > 0U) {
filter.draw_limits = radar::locator::DrawLimits{
.max_detected_objects = live_config.gpr_max_detected_objects_to_draw,
.draw_top_objects = live_config.gpr_draw_top_m_objects,
};
}
}
// Coherent BP already emits the FINAL visible object set from the processor
// (window + N/M, no score threshold — matching Horns_motion_3libre.py), so the
// socket forwards it verbatim. The default FilterParams passes everything through
// (it only drops non-finite rows), keeping the filtering logic in one place.
return filter;
}
@@ -286,6 +281,10 @@ auto create_default_processors() -> ProcessorRegistry {
auto processor = std::make_unique<LegacyGprProcessor>();
processors.emplace(processor->name(), std::move(processor));
}
{
auto processor = std::make_unique<Sar3dProcessor>();
processors.emplace(processor->name(), std::move(processor));
}
return processors;
}
@@ -255,11 +255,11 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s
}
config.gpr_range_comp_power = static_cast<float>(found->get<double>());
}
if (const auto found = root.find("gpr_angle_comp_power"); found != root.end()) {
if (const auto found = root.find("gpr_object_min_frac"); found != root.end()) {
if (!found->is_number()) {
throw std::runtime_error("processing.gpr_angle_comp_power must be number");
throw std::runtime_error("processing.gpr_object_min_frac must be number");
}
config.gpr_angle_comp_power = static_cast<float>(found->get<double>());
config.gpr_object_min_frac = static_cast<float>(found->get<double>());
}
if (const auto found = root.find("gpr_comp_power"); found != root.end()) {
if (!found->is_number()) {
@@ -355,12 +355,6 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s
}
config.gpr_imaging_plane_y_m = static_cast<float>(found->get<double>());
}
if (const auto found = root.find("gpr_min_visible_score"); found != root.end()) {
if (!found->is_number()) {
throw std::runtime_error("processing.gpr_min_visible_score must be number");
}
config.gpr_min_visible_score = static_cast<float>(found->get<double>());
}
for (const auto& [key, target] : {
std::pair{"gpr_visible_x_min_m", &config.gpr_visible_x_min_m},
std::pair{"gpr_visible_x_max_m", &config.gpr_visible_x_max_m},
@@ -388,6 +382,65 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s
config.gpr_draw_top_m_objects =
parse_u32_number(*found, "processing.gpr_draw_top_m_objects");
}
if (const auto found = root.find("gpr_object_approach_min_frames"); found != root.end()) {
config.gpr_object_approach_min_frames =
parse_u32_number(*found, "processing.gpr_object_approach_min_frames");
}
if (const auto found = root.find("sar3d_frame_radius"); found != root.end()) {
config.sar3d_frame_radius = parse_u32_number(*found, "processing.sar3d_frame_radius");
}
for (const auto& [key, target] : {
std::pair{"sar3d_edge_frame_weight", &config.sar3d_edge_frame_weight},
std::pair{"sar3d_pitch_deg", &config.sar3d_pitch_deg},
std::pair{"sar3d_ground_depth_m", &config.sar3d_ground_depth_m},
std::pair{"sar3d_voxel_x_m", &config.sar3d_voxel_x_m},
std::pair{"sar3d_voxel_y_m", &config.sar3d_voxel_y_m},
std::pair{"sar3d_voxel_z_m", &config.sar3d_voxel_z_m},
std::pair{"sar3d_norm_edge_margin_m", &config.sar3d_norm_edge_margin_m},
std::pair{"sar3d_smooth_sigma", &config.sar3d_smooth_sigma},
std::pair{"sar3d_comp_range_power", &config.sar3d_comp_range_power},
std::pair{"sar3d_bottom_view_deg", &config.sar3d_bottom_view_deg},
}) {
if (const auto found = root.find(key); found != root.end()) {
if (!found->is_number()) {
throw std::runtime_error(std::string("processing.") + key + " must be number");
}
*target = static_cast<float>(found->get<double>());
}
}
for (const auto& [key, target] : {
std::pair{"sar3d_x_min_m", &config.sar3d_x_min_m},
std::pair{"sar3d_x_max_m", &config.sar3d_x_max_m},
std::pair{"sar3d_y_min_m", &config.sar3d_y_min_m},
std::pair{"sar3d_y_max_m", &config.sar3d_y_max_m},
std::pair{"sar3d_z_min_m", &config.sar3d_z_min_m},
std::pair{"sar3d_z_max_m", &config.sar3d_z_max_m},
}) {
if (const auto found = root.find(key); found != root.end()) {
if (!found->is_number()) {
throw std::runtime_error(std::string("processing.") + key + " must be number");
}
*target = found->get<double>();
}
}
if (const auto found = root.find("sar3d_tx_per_segment"); found != root.end()) {
config.sar3d_tx_per_segment = parse_u32_number(*found, "processing.sar3d_tx_per_segment");
if (config.sar3d_tx_per_segment == 0U) {
throw std::runtime_error("processing.sar3d_tx_per_segment must be >= 1");
}
}
for (const auto& [key, target] : {
std::pair{"sar3d_pitch_override_enabled", &config.sar3d_pitch_override_enabled},
std::pair{"sar3d_use_compensation", &config.sar3d_use_compensation},
std::pair{"sar3d_normalize_pairs", &config.sar3d_normalize_pairs},
}) {
if (const auto found = root.find(key); found != root.end()) {
if (!found->is_boolean()) {
throw std::runtime_error(std::string("processing.") + key + " must be bool");
}
*target = found->get<bool>();
}
}
if (const auto found = root.find("ignore_socket_speed"); found != root.end()) {
if (!found->is_boolean()) {
throw std::runtime_error("processing.ignore_socket_speed must be bool");
@@ -1,5 +1,6 @@
#pragma once
#include "object_approach_filter.hpp"
#include "processor_interface.hpp"
namespace radar::processing {
@@ -13,6 +14,11 @@ class GprProcessor final : public ProcessorInterface {
std::span<const ipc::PreprocessedCollection> previous_collections,
const ProcessingLiveConfig& live_config
) -> ipc::ResultCollection override;
private:
// Cross-frame "approach" track filter. Persists across collections because the
// owning processor instance is long-lived (one per data_processor run).
ObjectApproachFilter approach_filter_{};
};
class LegacyGprProcessor final : public ProcessorInterface {
@@ -0,0 +1,172 @@
#pragma once
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <limits>
#include <vector>
namespace radar::processing {
// Temporal "approach" filter for coherent-BP detections — a port of the objects-only
// filter in Horns_motion_3libre's demonstrate notebook.
//
// It keeps only objects that persist as a *motion-consistent track* across at least
// `min_frames` consecutive frames: as the radar moves, a real target reappears at a
// predictable, shifting (x, z), whereas a one-frame noise spike forms no track and is
// dropped. The expected per-frame change in range is `speed * dt * cos(look_angle)`,
// where `dt` is the real interval between consecutive frames (so dropped frames and a
// varying frame period are handled naturally).
//
// CAUSAL: unlike the offline notebook (which can look forward over the whole sequence),
// this confirms a track by looking *backward* — an object is kept once it ends a track
// of `min_frames` frames seen so far. A target therefore first appears after it has
// persisted `min_frames` frames; the early frames of its track are not shown
// retroactively.
//
// Stateless across pipeline restarts is approximated by breaking tracks across a large
// inter-frame gap (`kMaxFrameGapSeconds`), so a stale history from a previous run cannot
// spuriously confirm objects.
//
// IDEMPOTENT under reprocessing: the data_processor re-runs the last collection (or
// replays the whole window) whenever live settings or the socket speed change, with no
// new sweep. The history is therefore keyed by `frame_id` (the strictly increasing
// collection id): the same id replaces its entry (reprocess of the current frame), a
// smaller id rebuilds from scratch (a replay restart), so repeated reprocessing never
// duplicates frames or falsely confirms a track.
class ObjectApproachFilter {
public:
struct Point {
double x_m;
double z_m;
};
// Record `objects` as frame `frame_id` and return, per object, whether it is
// confirmed (ends a >= `min_frames` motion-consistent track). `frame_id` is the
// collection id (identity/order, survives reprocessing); `frame_time_seconds` is the
// frame's wall-clock timestamp (drives the inter-frame interval); `speed_m_s`/
// `look_angle_deg` are the live motion estimate. `min_frames <= 1` disables filtering.
[[nodiscard]] auto confirm(
const std::vector<Point>& objects,
std::uint64_t frame_id,
double frame_time_seconds,
double speed_m_s,
double look_angle_deg,
std::size_t min_frames
) -> std::vector<bool> {
record_frame(Frame{frame_id, frame_time_seconds, objects});
const std::size_t history_depth = std::max<std::size_t>(min_frames, 1U);
while (history_.size() > history_depth) {
history_.pop_front();
}
std::vector<bool> confirmed(objects.size(), min_frames <= 1U);
if (min_frames <= 1U || history_.size() < min_frames) {
return confirmed; // disabled, or not enough history yet to confirm anything
}
const double range_step_per_second = std::abs(speed_m_s) * std::cos(to_radians(look_angle_deg));
for (std::size_t object_index = 0U; object_index < objects.size(); ++object_index) {
confirmed[object_index] = has_backward_track(objects[object_index], min_frames, range_step_per_second);
}
return confirmed;
}
void reset() { history_.clear(); }
private:
struct Frame {
std::uint64_t id;
double time_seconds;
std::vector<Point> objects;
};
// Append a genuinely new frame, replace the current one on reprocessing (same id),
// or rebuild from scratch when the id steps backward (a replay restart). This keeps
// the history one entry per distinct collection no matter how often settings change.
void record_frame(Frame frame) {
if (history_.empty() || frame.id > history_.back().id) {
history_.push_back(std::move(frame));
} else if (frame.id == history_.back().id) {
history_.back() = std::move(frame);
} else {
history_.clear();
history_.push_back(std::move(frame));
}
}
// Fixed matching tolerances (Horns_motion notebook 0.2 block). Range decreases as the
// radar approaches the target, hence the negative Z sign.
static constexpr double kXToleranceM = 0.45;
static constexpr double kRangeToleranceFraction = 0.85;
static constexpr double kRangeToleranceFloorM = 0.12;
static constexpr double kRangeSign = -1.0;
static constexpr double kMaxFrameGapSeconds = 2.0;
[[nodiscard]] static auto to_radians(double degrees) -> double {
return degrees * (M_PI / 180.0);
}
// Walk back from the current object through the history, matching a motion-consistent
// predecessor in each earlier frame. Confirmed iff a full chain of `min_frames` frames
// (the current one plus `min_frames - 1` predecessors) is found.
[[nodiscard]] auto has_backward_track(
const Point& object,
std::size_t min_frames,
double range_step_per_second
) const -> bool {
const std::size_t newest = history_.size() - 1U;
Point current = object;
for (std::size_t step = 1U; step < min_frames; ++step) {
const std::size_t earlier_index = newest - step;
const Frame& earlier = history_[earlier_index];
const double dt = history_[earlier_index + 1U].time_seconds - earlier.time_seconds;
if (!(dt > 0.0) || dt > kMaxFrameGapSeconds) {
return false; // non-monotonic time, or a gap that breaks the track
}
const double expected_range_shift = range_step_per_second * dt;
const Point* predecessor = match_predecessor(current, earlier.objects, expected_range_shift);
if (predecessor == nullptr) {
return false;
}
current = *predecessor;
}
return true;
}
// The best earlier-frame object consistent with `object` having moved by one frame:
// its range was larger by `expected_range_shift` (radar since approached), within the
// cross-range and range tolerances. Returns nullptr when nothing matches.
[[nodiscard]] static auto match_predecessor(
const Point& object,
const std::vector<Point>& candidates,
double expected_range_shift
) -> const Point* {
const double target_z = object.z_m - (kRangeSign * expected_range_shift);
const double range_tolerance =
std::max(kRangeToleranceFloorM, kRangeToleranceFraction * expected_range_shift);
const Point* best = nullptr;
double best_cost = std::numeric_limits<double>::infinity();
for (const Point& candidate : candidates) {
const double dx = std::abs(candidate.x_m - object.x_m);
const double dz = std::abs(candidate.z_m - target_z);
if (dx > kXToleranceM || dz > range_tolerance) {
continue;
}
const double cost = (dx / kXToleranceM) * (dx / kXToleranceM)
+ (dz / range_tolerance) * (dz / range_tolerance);
if (cost < best_cost) {
best = &candidate;
best_cost = cost;
}
}
return best;
}
std::deque<Frame> history_{}; // recent frames, newest at the back; capped to min_frames
};
} // namespace radar::processing
@@ -0,0 +1,34 @@
#pragma once
#include <memory>
#include <span>
#include <string>
#include "processor_interface.hpp"
namespace radar::processing {
// 3D SAR over a synthesized aperture, computed by the external gpr_cpp
// CUDA/NVRTC library (VNA_AlGo). Emits 2D max-projections of the volume
// (side XZ and top XY views) as ImageF32 collection payloads.
class Sar3dProcessor final : public ProcessorInterface {
public:
Sar3dProcessor();
~Sar3dProcessor() override;
[[nodiscard]] auto name() const -> std::string override;
[[nodiscard]] auto process_collection(
const config::RunConfig& run_config,
const ipc::PreprocessedCollection& collection,
std::span<const ipc::PreprocessedCollection> previous_collections,
const ProcessingLiveConfig& live_config
) -> ipc::ResultCollection override;
private:
// Pimpl keeps gpr/CUDA headers (cuda.h, nvrtc.h) out of the shared
// processor include path; only sar3d_processor.cpp needs them.
struct State;
std::unique_ptr<State> state_;
};
} // namespace radar::processing
@@ -21,7 +21,6 @@ constexpr double kSmoothSigma = 1.5;
// same default 'reflect' (half-sample symmetric) extension — see reflect_index.
constexpr double kGaussianTruncate = 4.0;
constexpr std::size_t kMaxObjects = 10U;
constexpr double kObjectMinFrac = 0.7;
constexpr double kRegionThresholdFrac = 0.75;
constexpr double kSuppressThresholdFrac = 0.20;
constexpr double kSuppressRadiusXM = 0.80;
@@ -1405,7 +1404,8 @@ void apply_depth_gate(
[[nodiscard]] auto find_bp_objects(
const std::vector<double>& bp_image,
const GridDefinition& grid
const GridDefinition& grid,
double min_frac
) -> std::vector<ObjectRecord> {
std::vector<ObjectRecord> objects{};
if (bp_image.empty() || grid.x_grid.size() < 2U || grid.z_grid.size() < 2U) {
@@ -1414,7 +1414,7 @@ void apply_depth_gate(
std::vector<double> work = bp_image;
const double global_peak = max_value(work);
const double stop_level = kObjectMinFrac * global_peak;
const double stop_level = min_frac * global_peak;
if (!(global_peak > 0.0)) {
return objects;
}
@@ -1740,7 +1740,15 @@ void add_bp_score_metrics(
}
}
[[nodiscard]] auto output_objects_sorted(
// Select the FINAL visible objects exactly as Horns_motion_3libre.py does, so the
// processor is the single source of truth: the GUI plot and the locator socket both
// consume this set verbatim (no second, duplicated filter). Steps, in order:
// 1. drop sidelobe candidates (when enabled), then sort by score (peak tie-break);
// 2. clip to the visible X/Z window (display window doubles as an object gate);
// 3. apply the N/M draw rule (BP_MAX_DETECTED_OBJECTS_TO_DRAW / BP_DRAW_TOP_M_OBJECTS):
// if more than N survive, show none; otherwise keep the top M.
// There is deliberately NO score threshold (the Python reference has none).
[[nodiscard]] auto select_visible_objects(
const std::vector<ObjectRecord>& objects,
const ProcessingLiveConfig& live_config
) -> std::vector<const ObjectRecord*> {
@@ -1750,6 +1758,12 @@ void add_bp_score_metrics(
if (live_config.gpr_remove_sidelobe_objects_enabled && object.sidelobe_candidate) {
continue;
}
if (object.x_m < live_config.gpr_visible_x_min_m
|| object.x_m > live_config.gpr_visible_x_max_m
|| object.z_m < live_config.gpr_visible_z_min_m
|| object.z_m > live_config.gpr_visible_z_max_m) {
continue;
}
visible.push_back(&object);
}
@@ -1759,6 +1773,17 @@ void add_bp_score_metrics(
}
return left->selected_score > right->selected_score;
});
// N/M draw rule (0 on either disables limiting, mirroring the GUI/locator default).
const auto max_detected = live_config.gpr_max_detected_objects_to_draw;
const auto draw_top = live_config.gpr_draw_top_m_objects;
if (max_detected > 0U && draw_top > 0U) {
if (visible.size() > max_detected) {
visible.clear();
} else if (visible.size() > draw_top) {
visible.resize(draw_top);
}
}
return visible;
}
@@ -1815,7 +1840,8 @@ void add_bp_score_metrics(
const config::RunConfig& run_config,
const ipc::PreprocessedCollection& collection,
std::span<const ipc::PreprocessedCollection> previous_collections,
const ProcessingLiveConfig& live_config
const ProcessingLiveConfig& live_config,
ObjectApproachFilter& approach_filter
) -> ipc::ResultCollection {
ipc::ResultCollection results{};
results.collection_id = collection.collection_id;
@@ -1833,8 +1859,10 @@ void add_bp_score_metrics(
return results;
}
const double velocity_mps =
kSpeedOfLightMetersPerSec / std::sqrt(std::max(1e-6, static_cast<double>(run_config.gpr.relative_permittivity)));
// Coherent BP fixes the medium to vacuum/air (eps_r = 1), matching the Python
// reference Horns_motion_3libre.py (its 0.3 block hardcodes eps_r = 1.0). The
// configurable relative_permittivity stays a legacy-GPR-only knob.
const double velocity_mps = kSpeedOfLightMetersPerSec;
const double start_hz = static_cast<double>(live_config.gpr_start_freq_mhz) * 1'000'000.0;
const double stop_hz = static_cast<double>(live_config.gpr_stop_freq_mhz) * 1'000'000.0;
const double min_depth_m = static_cast<double>(live_config.gpr_min_depth_m);
@@ -1920,7 +1948,7 @@ void add_bp_score_metrics(
min_depth_m,
max_depth_m,
std::max(0.0, static_cast<double>(live_config.gpr_range_comp_power)),
std::max(0.0, static_cast<double>(live_config.gpr_angle_comp_power))
0.0 // angle compensation fixed off (Python Horns_motion_3libre.py COMP_ANGLE_POWER = 0.0)
);
if (bp.image.empty()) {
return results;
@@ -1930,7 +1958,7 @@ void add_bp_score_metrics(
const auto incoherent_display_map =
normalize_bp_map(bp.incoherent, grid, min_depth_m, max_depth_m, kSmoothSigma);
auto objects = find_bp_objects(display_map, grid);
auto objects = find_bp_objects(display_map, grid, static_cast<double>(live_config.gpr_object_min_frac));
add_local_prominence_metrics(objects, display_map, grid, min_depth_m, max_depth_m);
add_incoherent_support_metrics(objects, incoherent_display_map, bp.coherence_factor);
mark_sidelobe_candidates(objects, selected_traces, selection, imaging_plane_y_m);
@@ -1951,14 +1979,34 @@ void add_bp_score_metrics(
results.collection_payloads.push_back(build_image_payload("gpr_accumulator", grid.x_grid, grid.z_grid, display_map));
// Final visible set (window + N/M), then the cross-frame approach filter: keep only
// objects confirmed as a >= min_frames motion-consistent track (Horns_motion notebook).
const auto visible = select_visible_objects(objects, live_config);
std::vector<ObjectApproachFilter::Point> visible_points{};
visible_points.reserve(visible.size());
for (const auto* object : visible) {
visible_points.push_back({object->x_m, object->z_m});
}
const auto confirmed = approach_filter.confirm(
visible_points,
collection.collection_id,
static_cast<double>(collection.monotonic_ns) * 1e-9,
static_cast<double>(live_config.gpr_speed_m_s),
static_cast<double>(live_config.gpr_look_angle_deg),
static_cast<std::size_t>(live_config.gpr_object_approach_min_frames)
);
std::vector<std::vector<float>> point_rows{};
point_rows.reserve(objects.size());
for (const auto* object : output_objects_sorted(objects, live_config)) {
point_rows.reserve(visible.size());
for (std::size_t index = 0U; index < visible.size(); ++index) {
if (!confirmed[index]) {
continue;
}
point_rows.push_back(
{
static_cast<float>(object->x_m),
static_cast<float>(object->z_m),
static_cast<float>(object->selected_score),
static_cast<float>(visible[index]->x_m),
static_cast<float>(visible[index]->z_m),
static_cast<float>(visible[index]->selected_score),
}
);
}
@@ -36,7 +36,7 @@ auto GprProcessor::process_collection(
std::span<const ipc::PreprocessedCollection> previous_collections,
const ProcessingLiveConfig& live_config
) -> ipc::ResultCollection {
return process_backprojection_gpr(run_config, collection, previous_collections, live_config);
return process_backprojection_gpr(run_config, collection, previous_collections, live_config, approach_filter_);
}
auto LegacyGprProcessor::name() const -> std::string {
@@ -0,0 +1,677 @@
#include "sar3d_processor.hpp"
#include <algorithm>
#include <bit>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <fstream>
#include <iostream>
#include <limits>
#include <map>
#include <optional>
#include <stdexcept>
#include <vector>
#include "gpr/ascan.hpp"
#include "gpr/config.hpp"
#include "gpr/geometry.hpp"
#include "gpr/sar3d.hpp"
namespace radar::processing {
namespace {
constexpr std::size_t kAscanCacheLimit = 64U;
void hash_mix(std::size_t& seed, std::uint64_t value) {
seed ^= value + 0x9e3779b97f4a7c15ULL + (seed << 6U) + (seed >> 2U);
}
void hash_mix(std::size_t& seed, double value) {
hash_mix(seed, std::bit_cast<std::uint64_t>(value));
}
[[nodiscard]] auto contains_u32(const std::vector<std::uint32_t>& values, std::uint32_t value) -> bool {
return std::find(values.begin(), values.end(), value) != values.end();
}
// Requested positions filtered to the available set; empty request selects all.
// Mirrors the coherent-BP processor's selection semantics.
[[nodiscard]] auto selected_positions(
const std::vector<std::uint32_t>& requested,
std::vector<std::uint32_t> available
) -> std::vector<std::uint32_t> {
std::sort(available.begin(), available.end());
available.erase(std::unique(available.begin(), available.end()), available.end());
if (requested.empty()) {
return available;
}
std::vector<std::uint32_t> result{};
for (const auto value : requested) {
if (contains_u32(available, value) && !contains_u32(result, value)) {
result.push_back(value);
}
}
std::sort(result.begin(), result.end());
return result;
}
struct Selection {
// Sorted ascending switch positions; the logical pair key (i_tx, i_rx) is
// the index of a position within these lists.
std::vector<std::uint32_t> inputs{};
std::vector<std::uint32_t> outputs{};
};
[[nodiscard]] auto build_selection(
const config::RunConfig& run_config,
const ProcessingLiveConfig& live_config
) -> Selection {
std::vector<std::uint32_t> available_inputs{};
available_inputs.reserve(run_config.gpr.rx_geometry.size());
for (const auto& entry : run_config.gpr.rx_geometry) {
available_inputs.push_back(entry.input_pos);
}
std::vector<std::uint32_t> available_outputs{};
available_outputs.reserve(run_config.gpr.tx_geometry.size());
for (const auto& entry : run_config.gpr.tx_geometry) {
available_outputs.push_back(entry.output_pos);
}
Selection selection{};
selection.inputs = selected_positions(live_config.gpr_input_positions, std::move(available_inputs));
selection.outputs = selected_positions(live_config.gpr_output_positions, std::move(available_outputs));
return selection;
}
[[nodiscard]] auto build_geometry(
const config::RunConfig& run_config,
const Selection& selection
) -> gpr::GeometryConfig {
gpr::GeometryConfig geom{};
geom.input_idx.clear();
geom.output_idx.clear();
geom.rx_positions.clear();
geom.tx_positions.clear();
for (const auto position : selection.inputs) {
for (const auto& entry : run_config.gpr.rx_geometry) {
if (entry.input_pos == position) {
geom.input_idx.push_back(static_cast<int>(position));
geom.rx_positions.push_back({
static_cast<double>(entry.x_m),
static_cast<double>(entry.y_m),
static_cast<double>(entry.z_m),
});
break;
}
}
}
for (const auto position : selection.outputs) {
for (const auto& entry : run_config.gpr.tx_geometry) {
if (entry.output_pos == position) {
geom.output_idx.push_back(static_cast<int>(position));
geom.tx_positions.push_back({
static_cast<double>(entry.x_m),
static_cast<double>(entry.y_m),
static_cast<double>(entry.z_m),
});
break;
}
}
}
geom.eps_r = static_cast<double>(run_config.gpr.relative_permittivity);
geom.finalize();
return geom;
}
[[nodiscard]] auto build_bp_config(const ProcessingLiveConfig& live_config) -> gpr::BPConfig {
gpr::BPConfig bp{};
bp.f_start = static_cast<double>(live_config.gpr_start_freq_mhz) * 1e6;
bp.f_stop = static_cast<double>(live_config.gpr_stop_freq_mhz) * 1e6;
bp.min_depth = static_cast<double>(live_config.gpr_min_depth_m);
bp.max_depth = static_cast<double>(live_config.gpr_max_depth_m);
bp.pair_normalize = live_config.sar3d_normalize_pairs;
bp.pair_norm_depth_min = bp.min_depth;
bp.pair_norm_depth_max = bp.max_depth;
bp.comp_range_power = static_cast<double>(live_config.sar3d_comp_range_power);
return bp;
}
[[nodiscard]] auto build_motion_config(const ProcessingLiveConfig& live_config) -> gpr::MotionConfig {
gpr::MotionConfig motion{};
motion.speed_m_s = static_cast<double>(live_config.gpr_speed_m_s);
motion.look_angle_deg = static_cast<double>(live_config.gpr_look_angle_deg);
// Notebook BP_GROUND_PITCH_DEG = None -> the frame pitch follows the look angle.
motion.pitch_deg = live_config.sar3d_pitch_override_enabled
? static_cast<double>(live_config.sar3d_pitch_deg)
: static_cast<double>(live_config.gpr_look_angle_deg);
motion.direction_sign = static_cast<double>(live_config.gpr_direction_sign);
motion.apply_freq_phase_correction = live_config.gpr_apply_freq_phase_correction;
motion.tx_per_segment = static_cast<int>(std::max<std::uint32_t>(1U, live_config.sar3d_tx_per_segment));
return motion;
}
[[nodiscard]] auto build_sar_config(const ProcessingLiveConfig& live_config) -> gpr::SARConfig {
gpr::SARConfig sar{};
sar.frame_radius = static_cast<int>(std::max<std::uint32_t>(0U, live_config.sar3d_frame_radius));
sar.edge_frame_weight = static_cast<double>(live_config.sar3d_edge_frame_weight);
sar.ground_depth_m = static_cast<double>(live_config.sar3d_ground_depth_m);
sar.x_min_m = static_cast<double>(live_config.sar3d_x_min_m);
sar.x_max_m = static_cast<double>(live_config.sar3d_x_max_m);
sar.y_min_m = static_cast<double>(live_config.sar3d_y_min_m);
sar.y_max_m = static_cast<double>(live_config.sar3d_y_max_m);
sar.z_min_m = static_cast<double>(live_config.sar3d_z_min_m);
sar.z_max_m = static_cast<double>(live_config.sar3d_z_max_m);
sar.voxel_m = {
static_cast<double>(live_config.sar3d_voxel_x_m),
static_cast<double>(live_config.sar3d_voxel_y_m),
static_cast<double>(live_config.sar3d_voxel_z_m),
};
sar.norm_edge_margin_m = static_cast<double>(live_config.sar3d_norm_edge_margin_m);
sar.smooth_sigma = static_cast<double>(live_config.sar3d_smooth_sigma);
sar.use_compensation = live_config.sar3d_use_compensation;
return sar;
}
// Hardware Tx order: first occurrence of each output in run_combos. Deliberately
// ignores the live Tx selection so segment/slot assignment stays a hardware
// property (notebook tx_order_full).
[[nodiscard]] auto tx_order_full(const config::RunConfig& run_config) -> std::vector<int> {
std::vector<int> order{};
for (const auto& combo : run_config.run_combos) {
const int output = static_cast<int>(combo.output_pos);
if (std::find(order.begin(), order.end(), output) == order.end()) {
order.push_back(output);
}
}
return order;
}
[[nodiscard]] auto ascan_fingerprint(
const Selection& selection,
const gpr::GeometryConfig& geom,
const gpr::BPConfig& bp,
const gpr::MotionConfig& motion
) -> std::size_t {
std::size_t seed = 0U;
for (const auto position : selection.inputs) {
hash_mix(seed, static_cast<std::uint64_t>(position));
}
for (const auto position : selection.outputs) {
hash_mix(seed, static_cast<std::uint64_t>(static_cast<std::uint64_t>(position) | (1ULL << 40U)));
}
hash_mix(seed, geom.eps_r);
hash_mix(seed, bp.f_start);
hash_mix(seed, bp.f_stop);
hash_mix(seed, bp.pair_norm_depth_min);
hash_mix(seed, bp.pair_norm_depth_max);
hash_mix(seed, static_cast<std::uint64_t>(bp.pair_normalize ? 1U : 0U));
hash_mix(seed, motion.speed_m_s);
hash_mix(seed, motion.look_angle_deg);
hash_mix(seed, motion.direction_sign);
hash_mix(seed, static_cast<std::uint64_t>(motion.apply_freq_phase_correction ? 1U : 0U));
hash_mix(seed, static_cast<std::uint64_t>(motion.tx_per_segment));
return seed;
}
[[nodiscard]] auto engine_fingerprint(
const gpr::GeometryConfig& geom,
const gpr::BPConfig& bp,
const gpr::MotionConfig& motion,
const gpr::SARConfig& sar
) -> std::size_t {
std::size_t seed = 1U;
for (std::size_t index = 0U; index < geom.tx_positions.size(); ++index) {
hash_mix(seed, static_cast<std::uint64_t>(geom.output_idx[index]));
hash_mix(seed, geom.tx_positions[index][0]);
hash_mix(seed, geom.tx_positions[index][1]);
hash_mix(seed, geom.tx_positions[index][2]);
}
for (std::size_t index = 0U; index < geom.rx_positions.size(); ++index) {
hash_mix(seed, static_cast<std::uint64_t>(geom.input_idx[index]));
hash_mix(seed, geom.rx_positions[index][0]);
hash_mix(seed, geom.rx_positions[index][1]);
hash_mix(seed, geom.rx_positions[index][2]);
}
hash_mix(seed, geom.eps_r);
hash_mix(seed, bp.f_start);
hash_mix(seed, bp.f_stop);
hash_mix(seed, bp.min_depth);
hash_mix(seed, bp.max_depth);
hash_mix(seed, bp.comp_range_power);
hash_mix(seed, bp.comp_weight_max);
hash_mix(seed, bp.comp_ref_depth);
hash_mix(seed, motion.pitch_deg);
hash_mix(seed, sar.ground_depth_m);
hash_mix(seed, static_cast<std::uint64_t>(sar.use_compensation ? 1U : 0U));
return seed;
}
// Midpoint of the collection's capture window; monotonic_ns is the fallback for
// producers that never stamped capture times.
[[nodiscard]] auto collection_time_s(const ipc::PreprocessedCollection& collection) -> double {
if (collection.capture_end_ns > collection.capture_start_ns) {
return 0.5 * (static_cast<double>(collection.capture_start_ns) + static_cast<double>(collection.capture_end_ns)) * 1e-9;
}
return static_cast<double>(collection.monotonic_ns) * 1e-9;
}
// Real frame timing from the per-trace capture stamps: traces measured in one
// segment share one window on both producers, so the first window seen per Tx
// reconstructs the segments exactly (notebook snapshot_acquisition_timing).
// Invalid when any trace carries the zero "unmeasured" sentinel.
struct SnapshotTiming {
gpr::TxWindows windows{}; // seconds relative to the first trace start
double acq_s = 0.0;
double center_abs_s = 0.0; // absolute acquisition centre (frame time)
bool valid = false;
};
[[nodiscard]] auto snapshot_timing(const ipc::PreprocessedCollection& collection) -> SnapshotTiming {
SnapshotTiming timing{};
if (collection.traces.empty()) {
return timing;
}
std::map<int, std::pair<std::uint64_t, std::uint64_t>> by_tx{};
std::uint64_t first = std::numeric_limits<std::uint64_t>::max();
std::uint64_t last = 0U;
for (const auto& trace : collection.traces) {
if (trace.capture_end_ns <= trace.capture_start_ns) {
return timing;
}
by_tx.try_emplace(
static_cast<int>(trace.combo.output_pos),
std::pair{trace.capture_start_ns, trace.capture_end_ns}
);
first = std::min(first, trace.capture_start_ns);
last = std::max(last, trace.capture_end_ns);
}
for (const auto& [tx, window] : by_tx) {
timing.windows[tx] = {
static_cast<double>(window.first - first) * 1e-9,
static_cast<double>(window.second - first) * 1e-9,
};
}
timing.acq_s = static_cast<double>(last - first) * 1e-9;
timing.center_abs_s = 0.5 * (static_cast<double>(first) + static_cast<double>(last)) * 1e-9;
timing.valid = true;
return timing;
}
// Debug dump for the parity tool: raw float32 volume [iy][iz][ix] plus a json
// header with the axes and level.
void dump_volume(
const std::string& dir,
std::uint64_t collection_id,
const gpr::FinishedVolume& finished,
const gpr::Grid& grid
) {
char name[64];
std::snprintf(name, sizeof(name), "/sar3d_volume_%llu",
static_cast<unsigned long long>(collection_id));
const std::string base = dir + name;
std::ofstream bin(base + ".f32", std::ios::binary | std::ios::trunc);
bin.write(
reinterpret_cast<const char*>(finished.volume.data()),
static_cast<std::streamsize>(finished.volume.size() * sizeof(float))
);
std::ofstream meta(base + ".json", std::ios::trunc);
meta.precision(17);
meta << "{\"nx\": " << finished.NX << ", \"ny\": " << finished.NY
<< ", \"nz\": " << finished.NZ << ", \"level\": " << finished.level;
const auto write_axis = [&meta](const char* key, const std::vector<double>& axis) {
meta << ", \"" << key << "\": [";
for (std::size_t index = 0U; index < axis.size(); ++index) {
meta << (index > 0U ? "," : "") << axis[index];
}
meta << "]";
};
write_axis("x_m", grid.x);
write_axis("y_m", grid.y);
write_axis("z_m", grid.z);
meta << "}\n";
}
[[nodiscard]] auto build_image_payload(
const std::string& processing_name,
const std::vector<double>& x_axis,
const std::vector<double>& y_axis,
std::vector<float> values
) -> ipc::ResultPayload {
ipc::ResultPayload payload{};
payload.processing_name = processing_name;
payload.kind = ipc::ResultKind::ImageF32;
payload.image_x_axis.reserve(x_axis.size());
for (const auto value : x_axis) {
payload.image_x_axis.push_back(static_cast<float>(value));
}
payload.image_y_axis.reserve(y_axis.size());
for (const auto value : y_axis) {
payload.image_y_axis.push_back(static_cast<float>(value));
}
payload.image_values = std::move(values);
return payload;
}
} // namespace
struct Sar3dProcessor::State {
struct CacheEntry {
double time_s = 0.0;
// Null when the collection could not be preprocessed (e.g. missing
// pairs); cached so a bad collection is not retried every frame.
std::shared_ptr<gpr::AScanSet> ascans{};
};
// Keyed by collection_id (monotonically increasing), so evicting from the
// front drops the oldest collections.
std::map<std::uint64_t, CacheEntry> ascan_cache{};
std::size_t ascan_fp = 0U;
std::unique_ptr<gpr::SarEngine> engine{};
std::size_t engine_fp = 0U;
std::string last_error{};
std::uint64_t suppressed_errors = 0U;
auto get_or_compute_ascans(
const ipc::PreprocessedCollection& collection,
const Selection& selection,
const gpr::GeometryConfig& geom,
const gpr::BPConfig& bp,
const gpr::MotionConfig& motion,
const std::vector<int>& tx_order
) -> const CacheEntry& {
if (const auto found = ascan_cache.find(collection.collection_id); found != ascan_cache.end()) {
return found->second;
}
CacheEntry entry{};
auto timing = snapshot_timing(collection);
// The segmented model needs a window for every selected Tx; fall back to
// the uniform split otherwise (notebook median model).
if (timing.valid) {
for (const auto output : geom.output_idx) {
if (timing.windows.find(output) == timing.windows.end()) {
timing.valid = false;
break;
}
}
}
entry.time_s = timing.valid ? timing.center_abs_s : collection_time_s(collection);
std::map<gpr::PairKey, std::vector<gpr::c128>> s21{};
std::map<gpr::PairKey, std::vector<double>> freq{};
for (const auto& trace : collection.traces) {
const auto input_it = std::find(selection.inputs.begin(), selection.inputs.end(), trace.combo.input_pos);
const auto output_it = std::find(selection.outputs.begin(), selection.outputs.end(), trace.combo.output_pos);
if (input_it == selection.inputs.end() || output_it == selection.outputs.end()) {
continue;
}
const gpr::PairKey key{
static_cast<int>(output_it - selection.outputs.begin()),
static_cast<int>(input_it - selection.inputs.begin()),
};
auto& samples = s21[key];
samples.clear();
samples.reserve(trace.s21.size());
for (const auto& sample : trace.s21) {
samples.emplace_back(static_cast<double>(sample.re), static_cast<double>(sample.im));
}
auto& axis = freq[key];
axis.assign(trace.frequency_hz.begin(), trace.frequency_hz.end());
}
if (s21.size() == selection.inputs.size() * selection.outputs.size()) {
gpr::TxWindows windows{};
double acq_s = 0.0;
if (timing.valid) {
windows = std::move(timing.windows);
acq_s = timing.acq_s;
} else {
if (collection.capture_end_ns > collection.capture_start_ns) {
acq_s = static_cast<double>(collection.capture_end_ns - collection.capture_start_ns) * 1e-9;
}
auto order = tx_order;
for (const auto output : geom.output_idx) {
if (std::find(order.begin(), order.end(), output) == order.end()) {
order.push_back(output);
}
}
windows = gpr::uniform_tx_windows(order, acq_s, motion.tx_per_segment);
}
try {
const auto timestamps = gpr::compute_pair_timestamps(windows, acq_s, tx_order, geom, motion);
entry.ascans = std::make_shared<gpr::AScanSet>(gpr::preprocess_snapshot(
s21, freq, /*background=*/nullptr, motion, geom, bp, timestamps, bp.pair_normalize
));
} catch (const std::exception& exc) {
// A malformed collection (e.g. empty band after a sweep change)
// is cached as a skipped frame instead of failing the aperture.
report_error(std::string{"preprocess_snapshot: "} + exc.what());
entry.ascans = nullptr;
}
}
auto [inserted, _] = ascan_cache.emplace(collection.collection_id, std::move(entry));
while (ascan_cache.size() > kAscanCacheLimit) {
ascan_cache.erase(ascan_cache.begin());
}
return inserted->second;
}
void report_error(const std::string& message) {
if (message == last_error) {
++suppressed_errors;
return;
}
if (suppressed_errors > 0U) {
std::cerr << "sar3d: previous error repeated " << suppressed_errors << " times\n";
}
std::cerr << "sar3d: " << message << '\n';
last_error = message;
suppressed_errors = 0U;
}
};
Sar3dProcessor::Sar3dProcessor() : state_(std::make_unique<State>()) {}
Sar3dProcessor::~Sar3dProcessor() = default;
auto Sar3dProcessor::name() const -> std::string {
return "sar3d";
}
auto Sar3dProcessor::process_collection(
const config::RunConfig& run_config,
const ipc::PreprocessedCollection& collection,
std::span<const ipc::PreprocessedCollection> previous_collections,
const ProcessingLiveConfig& live_config
) -> ipc::ResultCollection {
ipc::ResultCollection results{};
results.collection_id = collection.collection_id;
results.monotonic_ns = collection.monotonic_ns;
try {
const auto selection = build_selection(run_config, live_config);
if (selection.inputs.empty() || selection.outputs.empty()) {
return results;
}
const auto geom = build_geometry(run_config, selection);
const auto bp = build_bp_config(live_config);
const auto motion = build_motion_config(live_config);
const auto sar = build_sar_config(live_config);
if (!(bp.max_depth > bp.min_depth) || !(bp.f_stop > bp.f_start)) {
return results;
}
const auto ascan_fp = ascan_fingerprint(selection, geom, bp, motion);
if (ascan_fp != state_->ascan_fp) {
state_->ascan_cache.clear();
state_->ascan_fp = ascan_fp;
}
// Aperture window: the newest 2r+1 collections, current one last.
const std::size_t window_size = 2U * static_cast<std::size_t>(sar.frame_radius) + 1U;
std::vector<const ipc::PreprocessedCollection*> window{};
window.reserve(window_size);
const std::size_t history_take = std::min(previous_collections.size(), window_size - 1U);
for (std::size_t index = previous_collections.size() - history_take; index < previous_collections.size(); ++index) {
window.push_back(&previous_collections[index]);
}
window.push_back(&collection);
const auto tx_order = tx_order_full(run_config);
std::vector<std::shared_ptr<gpr::AScanSet>> ascan_sets{};
std::vector<double> frame_times{};
ascan_sets.reserve(window.size());
frame_times.reserve(window.size());
for (const auto* entry_collection : window) {
const auto& entry =
state_->get_or_compute_ascans(*entry_collection, selection, geom, bp, motion, tx_order);
if (entry.ascans == nullptr) {
continue;
}
ascan_sets.push_back(entry.ascans);
frame_times.push_back(entry.time_s);
}
if (ascan_sets.empty()) {
return results;
}
// aperture_volume requires one shared time axis; drop frames whose axis
// differs from the current collection's (e.g. after a sweep-grid change).
const auto& reference = *ascan_sets.back();
for (std::size_t index = ascan_sets.size(); index-- > 0U;) {
const auto& candidate = *ascan_sets[index];
if (candidate.n_fft != reference.n_fft || std::abs(candidate.t0 - reference.t0) > 1e-18 ||
candidate.n_pairs != reference.n_pairs) {
ascan_sets.erase(ascan_sets.begin() + static_cast<std::ptrdiff_t>(index));
frame_times.erase(frame_times.begin() + static_cast<std::ptrdiff_t>(index));
}
}
if (ascan_sets.empty()) {
return results;
}
// Symmetric aperture: the reference frame sits `radius` frames before the
// newest one, so the published volume lags the input by `radius` frames.
const std::size_t center_index =
ascan_sets.size() > static_cast<std::size_t>(sar.frame_radius)
? ascan_sets.size() - 1U - static_cast<std::size_t>(sar.frame_radius)
: 0U;
const auto offsets = gpr::frame_offsets(frame_times, frame_times[center_index], motion);
const auto weights = gpr::frame_weights(offsets, sar.edge_frame_weight);
std::vector<gpr::ApertureFrame> frames{};
frames.reserve(ascan_sets.size());
for (std::size_t index = 0U; index < ascan_sets.size(); ++index) {
frames.push_back(gpr::ApertureFrame{ascan_sets[index].get(), offsets[index], weights[index]});
}
const auto engine_fp = engine_fingerprint(geom, bp, motion, sar);
if (state_->engine == nullptr || state_->engine_fp != engine_fp) {
state_->engine = std::make_unique<gpr::SarEngine>(geom, bp, motion, sar);
state_->engine_fp = engine_fp;
}
const gpr::Grid grid = gpr::build_full_region_grid(sar);
const gpr::VolumeRaw raw = state_->engine->aperture_volume(frames, grid);
// Full notebook finalization: |acc/cnt|, depth gate, normalization
// without the Y-edge planes, Gaussian smoothing over (Z, X), clip.
const gpr::FinishedVolume finished =
gpr::finish_volume(raw, grid, motion.pitch_deg, bp, sar);
if (!live_config.sar3d_volume_dump_dir.empty()) {
dump_volume(live_config.sar3d_volume_dump_dir, collection.collection_id, finished, grid);
}
// Two projections match the legacy render conventions (X across,
// Z range/"вдаль", Y up):
// - top view `sar3d_proj_xz`: max over Y, axes X (horizontal) / Z;
// - bottom view `sar3d_proj_bottom`: vertical axis is always Y, the
// horizontal axis u is the ground direction rotated by
// `sar3d_bottom_view_deg` around Y: 0° = front view (u = X, max
// along Z), 90° = side view (u = Z, max along X), intermediate
// angles project along the rotated perpendicular.
const int nx = finished.NX;
const int ny = finished.NY;
const int nz = finished.NZ;
const double theta_deg = std::clamp(static_cast<double>(live_config.sar3d_bottom_view_deg), 0.0, 90.0);
const double theta_rad = theta_deg * M_PI / 180.0;
const double cos_t = std::cos(theta_rad);
const double sin_t = std::sin(theta_rad);
// Exact-axis fast paths keep the front/side views on the native grid.
const bool front_view = theta_deg <= 1e-9;
const bool side_view = theta_deg >= 90.0 - 1e-9;
std::vector<double> u_axis{};
if (front_view) {
u_axis = grid.x;
} else if (side_view) {
u_axis = grid.z;
} else {
const double u_a = grid.x.front() * cos_t + grid.z.front() * sin_t;
const double u_b = grid.x.front() * cos_t + grid.z.back() * sin_t;
const double u_c = grid.x.back() * cos_t + grid.z.front() * sin_t;
const double u_d = grid.x.back() * cos_t + grid.z.back() * sin_t;
const double u_min = std::min(std::min(u_a, u_b), std::min(u_c, u_d));
const double u_max = std::max(std::max(u_a, u_b), std::max(u_c, u_d));
u_axis.resize(static_cast<std::size_t>(nx));
for (int iu = 0; iu < nx; ++iu) {
u_axis[static_cast<std::size_t>(iu)] = u_min + (u_max - u_min) * iu / static_cast<double>(nx - 1);
}
}
const int nu = static_cast<int>(u_axis.size());
const double u_start = u_axis.front();
const double inv_du = nu > 1 ? static_cast<double>(nu - 1) / (u_axis.back() - u_start) : 0.0;
// The volume is already normalized to ~[0, 1] by finish_volume, so the
// projections are published as-is; the scalar payload carries `level`
// (the absolute scale the volume was divided by).
std::vector<float> projection_xz(static_cast<std::size_t>(nz) * nx, 0.0F);
std::vector<float> projection_bottom(static_cast<std::size_t>(ny) * nu, 0.0F);
for (int iy = 0; iy < ny; ++iy) {
for (int iz = 0; iz < nz; ++iz) {
const std::size_t row = (static_cast<std::size_t>(iy) * nz + iz) * nx;
const std::size_t bottom_row = static_cast<std::size_t>(iy) * nu;
const double z_sin_t = grid.z[iz] * sin_t;
for (int ix = 0; ix < nx; ++ix) {
const float value = finished.volume[row + static_cast<std::size_t>(ix)];
auto& xz_cell = projection_xz[static_cast<std::size_t>(iz) * nx + ix];
xz_cell = std::max(xz_cell, value);
int bin = ix;
if (side_view) {
bin = iz;
} else if (!front_view) {
const double u = grid.x[ix] * cos_t + z_sin_t;
bin = static_cast<int>(std::lround((u - u_start) * inv_du));
bin = std::clamp(bin, 0, nu - 1);
}
auto& bottom_cell = projection_bottom[bottom_row + static_cast<std::size_t>(bin)];
bottom_cell = std::max(bottom_cell, value);
}
}
}
results.collection_payloads.push_back(
build_image_payload("sar3d_proj_xz", grid.x, grid.z, std::move(projection_xz))
);
results.collection_payloads.push_back(
build_image_payload("sar3d_proj_bottom", u_axis, grid.y, std::move(projection_bottom))
);
ipc::ResultPayload peak_payload{};
peak_payload.processing_name = "sar3d_peak";
peak_payload.kind = ipc::ResultKind::ScalarF32;
peak_payload.scalar_value = static_cast<float>(finished.level);
results.collection_payloads.push_back(std::move(peak_payload));
} catch (const std::exception& exc) {
// A CUDA/NVRTC or data error must not kill the data_processor loop:
// publish an empty result and keep going. Repeated identical errors are
// logged once with a suppression count.
state_->report_error(exc.what());
results.collection_payloads.clear();
}
return results;
}
} // namespace radar::processing
@@ -0,0 +1,349 @@
// Parity check of the sar3d processor against the reference notebook
// Multi_antenna_3D_clean_v1.ipynb.
//
// Feeds a preprocessed snapshot of an extracted replay dataset through
// Sar3dProcessor exactly the way the live pipeline would (geometry and combo
// order from the dataset's config_profile.json, per-trace capture windows from
// the snapshot's meta.json) and dumps the finished volume for comparison with
// the notebook's *_volume.npz export:
//
// sar3d_parity <dataset_root> <center_snapshot> [--dump-dir <dir>]
// python_app/scripts/sar3d_volume_compare.py <dir>/sar3d_volume_<id> <ref.npz>
//
// <dataset_root> contains config_profile.json and preprocessed/<snapshot>/.
// Defaults mirror the parameters the reference volumes were built with
// (f 2..6 GHz, depth 2.5..8 m, speed 1.45 m/s, look angle 17.5 deg).
#include <algorithm>
#include <cctype>
#include <complex>
#include <cstdint>
#include <cstdio>
#include <dirent.h>
#include <fstream>
#include <span>
#include <stdexcept>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#include "gpr/config.hpp"
#include "gpr/geometry.hpp"
#include "processing_live_config.hpp"
#include "run_config.hpp"
#include "sar3d_processor.hpp"
#include "shared_types.hpp"
namespace {
// Minimal numpy .npy loader (v1.0, 1-D complex128/complex64/float64/float32).
struct NpyArray {
std::vector<double> f64{};
std::vector<std::complex<double>> c128{};
};
NpyArray load_npy(const std::string& path) {
std::ifstream in(path, std::ios::binary);
if (!in) {
throw std::runtime_error("cannot open " + path);
}
char magic[6];
in.read(magic, 6);
std::uint8_t version[2];
in.read(reinterpret_cast<char*>(version), 2);
std::uint16_t header_len = 0;
in.read(reinterpret_cast<char*>(&header_len), 2);
std::string header(header_len, '\0');
in.read(header.data(), header_len);
const auto shape_pos = header.find("'shape':");
const auto left = header.find('(', shape_pos);
const auto right = header.find(')', shape_pos);
const long count = std::stol(header.substr(left + 1, right - left - 1));
NpyArray array{};
const auto has = [&header](const char* token) { return header.find(token) != std::string::npos; };
if (has("c8")) {
array.c128.resize(count);
std::vector<std::complex<float>> tmp(count);
in.read(reinterpret_cast<char*>(tmp.data()), count * sizeof(std::complex<float>));
for (long i = 0; i < count; ++i) {
array.c128[i] = std::complex<double>(tmp[i].real(), tmp[i].imag());
}
} else if (has("c16")) {
array.c128.resize(count);
in.read(reinterpret_cast<char*>(array.c128.data()), count * sizeof(std::complex<double>));
} else if (has("f4")) {
array.f64.resize(count);
std::vector<float> tmp(count);
in.read(reinterpret_cast<char*>(tmp.data()), count * sizeof(float));
for (long i = 0; i < count; ++i) {
array.f64[i] = tmp[i];
}
} else {
array.f64.resize(count);
in.read(reinterpret_cast<char*>(array.f64.data()), count * sizeof(double));
}
return array;
}
// Live config matching the reference notebook run (npz meta parameters).
// The default program grid is a fixed window (X -2..2, Z 0..8), while the
// notebook derives its bounds from the antenna span and the depth gate — so
// the notebook bounds are recomputed here and passed explicitly.
auto build_live_config(const radar::config::RunConfig& run_config)
-> radar::processing::ProcessingLiveConfig {
radar::processing::ProcessingLiveConfig live{};
live.processor_mode = "sar3d";
live.gpr_start_freq_mhz = 2000.0F;
live.gpr_stop_freq_mhz = 6000.0F;
live.gpr_min_depth_m = 2.5F;
live.gpr_max_depth_m = 8.0F;
live.gpr_speed_m_s = 1.45F;
live.gpr_look_angle_deg = 17.5F;
live.gpr_direction_sign = 1.0F;
live.gpr_apply_freq_phase_correction = true;
gpr::GeometryConfig geom{};
geom.input_idx.clear();
geom.output_idx.clear();
geom.rx_positions.clear();
geom.tx_positions.clear();
for (const auto& entry : run_config.gpr.rx_geometry) {
geom.input_idx.push_back(static_cast<int>(entry.input_pos));
geom.rx_positions.push_back({entry.x_m, entry.y_m, entry.z_m});
}
for (const auto& entry : run_config.gpr.tx_geometry) {
geom.output_idx.push_back(static_cast<int>(entry.output_pos));
geom.tx_positions.push_back({entry.x_m, entry.y_m, entry.z_m});
}
geom.finalize();
gpr::BPConfig bp{};
bp.min_depth = live.gpr_min_depth_m;
bp.max_depth = live.gpr_max_depth_m;
gpr::SARConfig sar{};
double x_min = 0.0, x_max = 0.0, z_min = 0.0, z_max = 0.0;
gpr::notebook_grid_bounds(geom, bp, sar, live.gpr_look_angle_deg, /*x_margin_m=*/2.0,
x_min, x_max, z_min, z_max);
live.sar3d_x_min_m = x_min;
live.sar3d_x_max_m = x_max;
live.sar3d_z_min_m = z_min;
live.sar3d_z_max_m = z_max;
return live;
}
auto load_collection(
const radar::config::RunConfig& run_config,
const std::string& root,
const std::string& snapshot,
std::uint64_t collection_id
) -> radar::ipc::PreprocessedCollection {
radar::ipc::PreprocessedCollection collection{};
collection.collection_id = collection_id;
// Per-trace capture windows from the snapshot's meta.json; the segmented
// timing model is reconstructed from them exactly like in the notebook.
std::map<std::pair<std::uint32_t, std::uint32_t>, std::pair<std::uint64_t, std::uint64_t>> windows{};
{
std::ifstream in(root + "/preprocessed/" + snapshot + "/meta.json");
if (in) {
nlohmann::json meta{};
in >> meta;
for (const auto& trace : meta.value("traces", nlohmann::json::array())) {
if (!trace.contains("capture_start_ns") || !trace.contains("capture_end_ns")) {
continue;
}
windows[{trace.at("input").get<std::uint32_t>(), trace.at("output").get<std::uint32_t>()}] = {
trace.at("capture_start_ns").get<std::uint64_t>(),
trace.at("capture_end_ns").get<std::uint64_t>(),
};
}
}
}
std::uint64_t first = 0U;
std::uint64_t last = 0U;
for (const auto& [key, window] : windows) {
if (first == 0U || window.first < first) {
first = window.first;
}
last = std::max(last, window.second);
}
if (first == 0U) {
// No meta labels: fall back to the `_ns<...>` folder suffix.
const auto ns_pos = snapshot.rfind("_ns");
if (ns_pos == std::string::npos) {
throw std::runtime_error("snapshot has no meta.json and no _ns suffix: " + snapshot);
}
last = static_cast<std::uint64_t>(std::stoull(snapshot.substr(ns_pos + 3)));
first = last;
}
collection.capture_start_ns = first;
collection.capture_end_ns = last;
collection.monotonic_ns = (first + last) / 2U;
for (const auto& combo : run_config.run_combos) {
char path[512];
std::snprintf(path, sizeof(path), "%s/preprocessed/%s/i%u_o%u_s21.npy",
root.c_str(), snapshot.c_str(), combo.input_pos, combo.output_pos);
const auto s21 = load_npy(path);
std::snprintf(path, sizeof(path), "%s/preprocessed/%s/i%u_o%u_freq.npy",
root.c_str(), snapshot.c_str(), combo.input_pos, combo.output_pos);
const auto freq = load_npy(path);
radar::ipc::SweepTraceBlock trace{};
trace.combo = combo;
trace.frequency_hz.assign(freq.f64.begin(), freq.f64.end());
trace.s21.reserve(s21.c128.size());
for (const auto& sample : s21.c128) {
trace.s21.push_back(radar::ipc::Complex32{
.re = static_cast<float>(sample.real()),
.im = static_cast<float>(sample.imag()),
});
}
trace.s11.resize(trace.s21.size());
if (const auto found = windows.find({combo.input_pos, combo.output_pos}); found != windows.end()) {
trace.capture_start_ns = found->second.first;
trace.capture_end_ns = found->second.second;
}
collection.traces.push_back(std::move(trace));
}
return collection;
}
void print_projection_argmax(
const radar::ipc::ResultPayload& payload,
const char* col_axis_name,
const char* row_axis_name
) {
std::size_t argmax = 0U;
float best = -1.0F;
for (std::size_t index = 0U; index < payload.image_values.size(); ++index) {
if (payload.image_values[index] > best) {
best = payload.image_values[index];
argmax = index;
}
}
const auto nx = payload.image_x_axis.size();
const auto col = argmax % nx;
const auto row = argmax / nx;
std::printf(
"%s: max=%.6f at %s=%.1f cm, %s=%.1f cm\n",
payload.processing_name.c_str(),
static_cast<double>(best),
col_axis_name,
static_cast<double>(payload.image_x_axis[col]) * 100.0,
row_axis_name,
static_cast<double>(payload.image_y_axis[row]) * 100.0
);
}
} // namespace
int main(int argc, char** argv) {
if (argc < 3) {
std::printf("usage: %s <dataset_root> <center_snapshot> [--dump-dir <dir>]\n", argv[0]);
return 1;
}
const std::string root = argv[1];
const std::string center = argv[2];
std::string dump_dir{};
for (int index = 3; index + 1 < argc; ++index) {
if (std::string{argv[index]} == "--dump-dir") {
dump_dir = argv[index + 1];
}
}
const std::string preprocessed = root + "/preprocessed";
std::vector<std::string> snapshots{};
if (DIR* dir = opendir(preprocessed.c_str())) {
for (dirent* entry = nullptr; (entry = readdir(dir)) != nullptr;) {
const std::string name = entry->d_name;
if (!name.empty() && (std::isdigit(name[0]) != 0)) {
snapshots.push_back(name);
}
}
closedir(dir);
}
std::sort(snapshots.begin(), snapshots.end());
const auto center_it = std::find(snapshots.begin(), snapshots.end(), center);
if (center_it == snapshots.end()) {
std::printf("center %s not found in %s\n", center.c_str(), preprocessed.c_str());
return 1;
}
const auto center_index = static_cast<std::size_t>(center_it - snapshots.begin());
const auto run_config = radar::config::load_run_config(root + "/config_profile.json");
if (run_config.gpr.tx_geometry.empty() || run_config.gpr.rx_geometry.empty()) {
std::printf("FAIL: config_profile.json has no gpr geometry\n");
return 1;
}
auto live = build_live_config(run_config);
live.sar3d_volume_dump_dir = dump_dir;
const auto radius = static_cast<std::size_t>(live.sar3d_frame_radius);
if (center_index < radius || center_index + radius >= snapshots.size()) {
std::printf("not enough snapshots around center for radius %zu\n", radius);
return 1;
}
// The processor centers the aperture `radius` frames before the newest
// collection, so feeding [center-r .. center+r] in order reproduces the
// symmetric aperture around `center`.
std::vector<radar::ipc::PreprocessedCollection> history{};
for (std::size_t index = center_index - radius; index <= center_index + radius; ++index) {
history.push_back(load_collection(run_config, root, snapshots[index], index));
}
std::printf("aperture: %zu frames around %s\n", history.size(), center.c_str());
radar::processing::Sar3dProcessor processor{};
radar::ipc::ResultCollection result{};
for (std::size_t index = 0U; index < history.size(); ++index) {
result = processor.process_collection(
run_config,
history[index],
std::span<const radar::ipc::PreprocessedCollection>(history.data(), index),
live
);
}
if (result.collection_payloads.empty()) {
std::printf("FAIL: sar3d produced no payloads\n");
return 1;
}
for (const auto& payload : result.collection_payloads) {
if (payload.kind == radar::ipc::ResultKind::ScalarF32) {
std::printf("%s (volume level): %.9e\n",
payload.processing_name.c_str(), static_cast<double>(payload.scalar_value));
}
}
for (const auto& payload : result.collection_payloads) {
if (payload.kind == radar::ipc::ResultKind::ImageF32 && payload.processing_name == "sar3d_proj_xz") {
print_projection_argmax(payload, "X", "Z");
}
}
// Bottom view across the selectable angles: the target's u coordinate must
// move smoothly from its X (front, 0 deg) to its Z (side, 90 deg).
const auto last_previous =
std::span<const radar::ipc::PreprocessedCollection>(history.data(), history.size() - 1U);
for (const float angle_deg : {0.0F, 30.0F, 60.0F, 90.0F}) {
auto live_angle = live;
live_angle.sar3d_volume_dump_dir.clear();
live_angle.sar3d_bottom_view_deg = angle_deg;
const auto angle_result =
processor.process_collection(run_config, history.back(), last_previous, live_angle);
for (const auto& payload : angle_result.collection_payloads) {
if (payload.kind == radar::ipc::ResultKind::ImageF32 && payload.processing_name == "sar3d_proj_bottom") {
std::printf("bottom @ %2.0f deg -> ", static_cast<double>(angle_deg));
print_projection_argmax(payload, "u", "Y");
}
}
}
if (!dump_dir.empty()) {
std::printf("volume dumped to %s/sar3d_volume_%llu.{f32,json}\n",
dump_dir.c_str(),
static_cast<unsigned long long>(history.back().collection_id));
}
std::printf("OK: sar3d processor path completed\n");
return 0;
}
@@ -147,14 +147,18 @@ class DriverLifecycleGuard {
// Worst-case serialized size of a collection given the configured combo count and sweep
// point count, using the trace wire format (see ipc::write_trace_collection/write_trace_block):
// collection header: magic(4) + collection_id(8) + monotonic_ns(8) + trace_count(4)
// + capture_start_ns(8) + capture_end_ns(8) = 40 bytes
// + capture_start_ns(8) + capture_end_ns(8)
// + trace capture window count(4) = 44 bytes
// per trace block: input_pos(4) + output_pos(4) + point_count(4) = 12 bytes
// + per point: frequency(4) + s11(8) + s21(8) = 20 bytes
// + trailer: capture_start_ns(8) + capture_end_ns(8) = 16 bytes
[[nodiscard]] auto worst_case_serialized_bytes(std::size_t combo_count, std::uint32_t sweep_points) -> std::size_t {
constexpr std::size_t kCollectionHeaderBytes = 40U;
constexpr std::size_t kCollectionHeaderBytes = 44U;
constexpr std::size_t kTraceHeaderBytes = 12U;
constexpr std::size_t kBytesPerPoint = 20U;
const std::size_t per_trace = kTraceHeaderBytes + (static_cast<std::size_t>(sweep_points) * kBytesPerPoint);
constexpr std::size_t kTraceTrailerBytes = 16U;
const std::size_t per_trace =
kTraceHeaderBytes + (static_cast<std::size_t>(sweep_points) * kBytesPerPoint) + kTraceTrailerBytes;
return kCollectionHeaderBytes + (combo_count * per_trace);
}
@@ -207,7 +211,7 @@ SweepOrchestrator::SweepOrchestrator(
void SweepOrchestrator::run(const std::atomic<bool>& stop_requested) {
// Fail fast on a config error: a slot that is too small for the worst-case payload can
// never carry a full collection, so report it clearly at startup instead of dropping
// every collection at runtime (fix #27).
// every collection at runtime.
const auto worst_case_bytes = worst_case_serialized_bytes(config_.run_combos.size(), config_.radar.sweep.points);
if (worst_case_bytes > raw_ring_.slot_size_bytes()) {
throw std::runtime_error(
@@ -219,7 +223,7 @@ void SweepOrchestrator::run(const std::atomic<bool>& stop_requested) {
}
DriverLifecycleGuard lifecycle_guard(radar_driver_, input_switch_driver_, output_switch_driver_);
// Wait for the devices to become available before starting (fix #8/#9): an absent device
// Wait for the devices to become available before starting: an absent device
// makes the orchestrator wait, not exit.
if (!lifecycle_guard.open_all_with_retry(stop_requested)) {
return; // stop requested before any device became available
@@ -290,7 +294,12 @@ auto SweepOrchestrator::acquire_one_collection(
// Production drivers ignore this; mock drivers use it to give every
// (input, output) pair its own synthetic response.
radar_driver_.set_active_combo(combo);
// Stamp around the sweep only: the switch drive and settling above belong to
// neither the previous combo nor this one, so excluding them keeps the window
// an honest "when was this combo actually measured".
const auto sweep_start_ns = ipc::current_monotonic_ns();
auto sweep = radar_driver_.acquire_sweep();
const auto sweep_end_ns = ipc::current_monotonic_ns();
validate_sweep(sweep);
ipc::SweepTraceBlock trace{};
@@ -298,6 +307,8 @@ auto SweepOrchestrator::acquire_one_collection(
trace.frequency_hz = std::move(sweep.frequency_hz);
trace.s11 = std::move(sweep.s11);
trace.s21 = std::move(sweep.s21);
trace.capture_start_ns = sweep_start_ns;
trace.capture_end_ns = sweep_end_ns;
collection.traces.push_back(std::move(trace));
}
+5
View File
@@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
+11
View File
@@ -0,0 +1,11 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack",
"pioarduino.pioarduino-ide"
]
}
@@ -0,0 +1,53 @@
# systemd-службы сетевого моста
Автозапуск `tools/bridge.py` как служб: `cart-bridge-tx` (отправитель, рядом с
пультом) и `cart-bridge-rx` (приёмник, рядом с тележкой). На каждой машине
включается ровно одна роль.
## Установка
На машине-отправителе:
```sh
sudo ./install-bridge.sh tx
```
На машине-приёмнике:
```sh
sudo ./install-bridge.sh rx
```
Установщик подставляет в юнит реальные пути (python из `.venv` проекта, если
есть, иначе системный `python3`; `bridge.py`; рабочий каталог) и пользователя
(того, кто вызвал `sudo`). Юниты кладутся в `/etc/systemd/system/`, конфиги —
в `/etc/cart-bridge/{tx,rx}.env` (создаются из примеров, существующие не
перезаписываются). Без аргумента (`none`) — только установка, без включения.
Интерпретатор можно задать явно: `sudo PYTHON=/usr/bin/python3 ./install-bridge.sh rx`.
В выбранном python должен быть `pyserial`.
## Настройка
Отредактировать конфиг под свою сеть и ключ:
```sh
sudoedit /etc/cart-bridge/tx.env # peer = адрес приёмника, общий ключ
sudoedit /etc/cart-bridge/rx.env # bind, тот же ключ
sudo systemctl restart cart-bridge-tx # применить
```
Ключ `CART_BRIDGE_KEY` на обеих сторонах должен совпадать (HMAC-аутентификация
пакетов; обязателен для интернета).
## Эксплуатация
```sh
systemctl status cart-bridge-tx
journalctl -u cart-bridge-tx -f # логи; на отправителе видно «приёмник отвечает/НЕ отвечает»
sudo systemctl restart cart-bridge-tx
sudo systemctl disable --now cart-bridge-tx
```
Служба перезапускается при падении (`Restart=always`). Доступ к USB-порту STM
даётся группами `dialout`/`plugdev` (прописаны в юните).
@@ -0,0 +1,19 @@
# Конфигурация службы приёмника (cart-bridge-rx). Копируется в
# /etc/cart-bridge/rx.env установщиком. Переменные читает tools/bridge.py.
# Локальный UDP-адрес для приёма HOST:PORT.
CART_BRIDGE_BIND=0.0.0.0:5005
# Общий ключ HMAC (ОБЯЗАТЕЛЕН для интернета). Должен совпадать с отправителем.
CART_BRIDGE_KEY=change-me-shared-secret
# Необязательно: принимать пакеты только с этого адреса отправителя (фильтр по
# хосту). Для интернета за NAT обычно оставляют пустым и полагаются на HMAC.
#CART_BRIDGE_PEER=10.0.0.1
# Необязательно: путь к порту или подстрока product/serial. По умолчанию порт
# ищется по USB product string CART-RX.
#CART_BRIDGE_DEVICE=CART-RX
# Необязательно: уровень логов (DEBUG/INFO/WARNING).
#CART_BRIDGE_LOG_LEVEL=INFO
@@ -0,0 +1,18 @@
[Unit]
Description=Cart bridge receiver (role rx): сеть -> USB тележки
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=0
[Service]
Type=simple
User=@USER@
SupplementaryGroups=dialout plugdev
EnvironmentFile=/etc/cart-bridge/rx.env
WorkingDirectory=@WORKDIR@
ExecStart=@PYTHON@ @BRIDGE@ --role rx
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,15 @@
# Конфигурация службы отправителя (cart-bridge-tx). Копируется в
# /etc/cart-bridge/tx.env установщиком. Переменные читает tools/bridge.py.
# Адрес приёмника HOST:PORT — куда слать управление.
CART_BRIDGE_PEER=10.0.0.2:5005
# Общий ключ HMAC (ОБЯЗАТЕЛЕН для интернета). Должен совпадать с приёмником.
CART_BRIDGE_KEY=change-me-shared-secret
# Необязательно: путь к порту или подстрока product/serial. По умолчанию порт
# ищется по USB product string CART-TX.
#CART_BRIDGE_DEVICE=CART-TX
# Необязательно: уровень логов (DEBUG/INFO/WARNING).
#CART_BRIDGE_LOG_LEVEL=INFO
@@ -0,0 +1,20 @@
[Unit]
Description=Cart bridge sender (role tx): пульт USB -> сеть
After=network-online.target
Wants=network-online.target
# не упираться в лимит перезапусков при флапе сети/USB
StartLimitIntervalSec=0
[Service]
Type=simple
User=@USER@
# доступ к последовательному порту STM (dialout) и к USB-дескрипторам (plugdev)
SupplementaryGroups=dialout plugdev
EnvironmentFile=/etc/cart-bridge/tx.env
WorkingDirectory=@WORKDIR@
ExecStart=@PYTHON@ @BRIDGE@ --role tx
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# Установка systemd-служб сетевого моста тележки.
#
# sudo ./install-bridge.sh [tx|rx|both|none]
#
# Аргумент выбирает, какую службу включить и запустить на ЭТОЙ машине
# (tx — рядом с пультом, rx — рядом с тележкой). Без аргумента юниты только
# ставятся, но не включаются. Файлы юнитов и шаблоны конфигов берутся из этого
# каталога; пути к python и bridge.py подставляются автоматически.
set -euo pipefail
SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TOOLS="$(cd "$SELF/../tools" && pwd)"
REPO="$(cd "$SELF/../../.." && pwd)"
BRIDGE="$TOOLS/bridge.py"
WORKDIR="$TOOLS"
ENABLE="${1:-none}"
case "$ENABLE" in tx|rx|both|none) ;; *) echo "аргумент: tx|rx|both|none" >&2; exit 2;; esac
if [ "$(id -u)" -ne 0 ]; then
echo "нужны права root — перезапускаюсь через sudo"
exec sudo -E PYTHON="${PYTHON:-}" bash "$0" "$ENABLE"
fi
TARGET_USER="${SUDO_USER:-root}"
# интерпретатор: venv проекта, если есть, иначе системный python3
PYTHON="${PYTHON:-}"
if [ -z "$PYTHON" ]; then
if [ -x "$REPO/.venv/bin/python" ]; then
PYTHON="$REPO/.venv/bin/python"
else
PYTHON="$(command -v python3)"
fi
fi
echo "python : $PYTHON"
echo "bridge : $BRIDGE"
echo "user : $TARGET_USER"
if ! "$PYTHON" -c 'import serial' 2>/dev/null; then
echo "ВНИМАНИЕ: в $PYTHON нет pyserial — поставь: $PYTHON -m pip install pyserial" >&2
fi
gen_unit() { # <role>
local role="$1"
sed -e "s|@PYTHON@|$PYTHON|g" -e "s|@BRIDGE@|$BRIDGE|g" \
-e "s|@WORKDIR@|$WORKDIR|g" -e "s|@USER@|$TARGET_USER|g" \
"$SELF/cart-bridge-$role.service" > "/etc/systemd/system/cart-bridge-$role.service"
echo "установлен юнит /etc/systemd/system/cart-bridge-$role.service"
}
install -d -m 755 /etc/cart-bridge
for role in tx rx; do
gen_unit "$role"
conf="/etc/cart-bridge/$role.env"
if [ ! -e "$conf" ]; then
install -m 600 "$SELF/cart-bridge-$role.env.example" "$conf"
echo "создан конфиг $conf (ОТРЕДАКТИРУЙ: peer/bind и ключ)"
else
echo "конфиг $conf уже есть — не трогаю"
fi
done
systemctl daemon-reload
enable_one() { # <role>
systemctl enable --now "cart-bridge-$1.service"
echo "включена и запущена cart-bridge-$1.service"
}
case "$ENABLE" in
tx) enable_one tx ;;
rx) enable_one rx ;;
both) enable_one tx; enable_one rx ;;
none) echo "юниты установлены, не включены. Включить: systemctl enable --now cart-bridge-tx (или -rx)";;
esac
echo "готово. Логи: journalctl -u cart-bridge-tx -f (или -rx)"
@@ -0,0 +1,241 @@
# Протокол старого пульта тележки (реверс-инжиниринг)
Снято 2026-08-20 осциллографом Hantek DPO7204C с сигнального провода пульта
(канал CH3). Инструменты: `tools/capture.py` (захват), `tools/decode.py`
(декодер), сырые данные и картинки — в `captures/`.
## Физический уровень
- Один сигнальный провод, логика **3.3 В**.
- **Инвертированный UART** (стандартная полярность SBUS): в покое линия
**низкая** (~0 В), импульсы вверх до ~3.3 В.
- Скорость **100 000 бод**, формат **8E2** (8 бит данных, чётность even,
2 стоп-бита), биты LSB-first. Длительность бита 10 мкс.
## Кадровый уровень — SBUS
Стандартный кадр Futaba SBUS, 25 байт (3 мс на линии):
| Смещение | Размер | Содержимое |
|---|---|---|
| 0 | 1 | Заголовок `0x0F` |
| 1 | 22 | 16 каналов × 11 бит, упакованы подряд LSB-first |
| 23 | 1 | Флаги: bit0=CH17, bit1=CH18, bit2=frame_lost, bit3=failsafe |
| 24 | 1 | Футер `0x00` |
Распаковка каналов: 22 байта складываются в 176-битное число LSB-first,
канал N (N=0..15) = биты [11·N .. 11·N+10], диапазон значений 0–2047.
- Кадры отправляются каждые **50 мс** (20 Гц). Это медленнее стандартного
SBUS (7/14 мс) — ответная часть с этим темпом работает.
- Флаги во всех наблюдениях = `0x00`.
## Карта каналов (нумерация с 1)
| Управление | Канал | Мин | Нейтраль | Макс |
|---|---|---|---|---|
| Стик вперёд/назад | **2** | 433 (назад) | 1024 | 1643 (вперёд) |
| Стик влево/вправо | **4** | 446 (влево) | 1024 | 1654 (вправо) |
| Остальные 14 | — | всегда 1024 | | |
Диапазон осей ~±600 от нейтрали (не полная шкала SBUS). Других органов
управления на пульте нет.
Эталонный кадр нейтрали (hex):
```
0F 00 04 20 00 01 08 40 00 02 10 80 00 04 20 00 01 08 40 00 02 10 80 00 00
```
## Воспроизведение на STM32G431
- USART: 100000 бод, 8 бит + чётность even (в терминах STM32: M=1, 9-bit
с PCE=1), 2 стоп-бита, **TXINV=1** (аппаратная инверсия TX) — бит-бэнг
не нужен.
- Отправлять 25-байтовый кадр по таймеру каждые 50 мс.
- Нейтраль: все каналы 1024; управление — каналы 2 и 4 в измеренных
диапазонах.
# Сетевой мост «пульт → тележка»
Та же команда управления может идти не по проводу рядом, а через локальную
сеть/интернет, не ломая прежний проводной режим. Цепочка:
```
[пульт] ADC→ STM(CART-TX) ─USB─► bridge --role tx ─UDP─► bridge --role rx ─USB─► STM(CART-RX) ─SBUS─► драйвер
```
STM-TX продолжает гнать SBUS на свой локальный провод как раньше и
дополнительно отдаёт те же значения каналов по USB. STM-RX принимает их по USB
и выдаёт на свой провод так, будто сигнал пришёл с пульта.
## Роли и идентификация устройства
Роль выбирается на сборке (`platformio.ini`): `pio run -e cart_tx` или
`-e cart_rx`. Общий код (`sbus`, `usb_link`) собирается в оба env, специфичный —
через `build_src_filter`, мёртвого кода неактивной роли нет.
Каждая прошивка публикует USB **product string**: `CART-TX` / `CART-RX`. По ней
`tools/bridge.py` находит нужный порт (`serial.tools.list_ports`). После открытия
порта ПК шлёт `HELLO` и ждёт `IDENT` — подтверждение роли, версии протокола и
96-битного UID чипа (на случай неверно прошитого устройства, рассинхрона версий
или чужого CDC с тем же VID/PID). Serial-дескриптор (UID) различает экземпляры.
## Прошивка STM
Плата — WeAct STM32G431CB. Нужен PlatformIO Core (`pio`); если не установлен:
`pip install platformio` (в репозитории он лежит в `~/.platformio/penv/bin/pio`).
Все команды выполняются из каталога прошивки `device_firmware/cart_firmware`.
Две роли — две отдельные прошивки на две платы. По умолчанию загрузчик — ST-Link
по SWD (`upload_protocol = stlink` в `platformio.ini`).
**1. Подключить ST-Link к плате по SWD** (4 провода):
| ST-Link | WeAct G431CB |
|---|---|
| SWDIO | PA13 (SWDIO) |
| SWCLK | PA14 (SWCLK) |
| GND | GND |
| 3V3 | 3V3 |
**2. Собрать и прошить роль** (прошивать по одной плате за раз, один ST-Link):
```
# плата-передатчик (к ней подключён пульт)
pio run -e cart_tx -t upload
# плата-приёмник (к ней подключён провод драйвера колёс)
pio run -e cart_rx -t upload
```
- Только собрать без заливки: `pio run -e cart_tx`. Артефакты —
`.pio/build/<env>/firmware.{elf,bin}`.
- Если подключено несколько ST-Link, указать конкретный:
`pio run -e cart_tx -t upload --upload-port <serial-ST-Link>`.
- Сменить роль платы — просто залить другой env, флеш перезаписывается целиком.
**3. Физические подключения по ролям** (распиновка общая, см. выше по документу):
| | CART-TX | CART-RX |
|---|---|---|
| Стик X / Y | PA0 / PA1 (АЦП) | — не используется |
| Выход SBUS | PA9 → локальный драйвер (как раньше) | PA9 → провод драйвера колёс |
| USB | → ПК (bridge --role tx) | → ПК (bridge --role rx) |
**4. Проверить роль после прошивки.** Подключить плату по USB к ПК и прочитать
product string (роль зашита в неё):
```
python3 -c "from serial.tools import list_ports; \
[print(p.device, p.product, p.serial_number) for p in list_ports.comports()]"
# ожидаемо: /dev/ttyACM0 CART-TX <UID> либо CART-RX
```
Либо просто запустить `tools/bridge.py` соответствующей роли — на старте он
делает handshake и логирует подтверждённые роль, версию прошивки и UID.
> USB CDC теперь несёт **бинарный** протокол, а не текстовый лог. `pio device
> monitor` покажет «мусор» — это нормально. Отладочный текст идёт кадрами `LOG`,
> которые печатает `bridge.py`.
## Кадр поверх USB CDC
Единственный источник истины — `src/link_protocol.h`, зеркало на Python —
`tools/link_proto.py` (сверка: `python3 tools/link_proto.py --selftest`).
```
[0xA5][0x5A][VER=1][TYPE][LEN][PAYLOAD..LEN][CRC16_LO][CRC16_HI]
```
- CRC16-CCITT (poly `0x1021`, init `0xFFFF`) по байтам `[VER..конец PAYLOAD]`, LE.
Контрольный вектор: `crc16("123456789") = 0x29B1`.
- Приёмник (STM и Python) — стейт-машина: ищет синхрослово, читает по `LEN`,
проверяет CRC; при сбое сдвиг на байт и ресинхронизация (устойчивость к мусору
и потере байта).
- Типы: `0x01 CONTROL`, `0x02 IDENT`, `0x03 HELLO`, `0x04 LOG`. Отладочный текст
идёт только кадром `LOG` — «голых» `Serial.print`, ломающих бинарный поток, нет.
Payload `CONTROL` (LE): `seq:u32`, `t_ms:u32`, `flags:u8`, `ch[16]:u16` = 41 байт.
`ch[]` — значения SBUS-каналов 0..2047, ровно те, что TX кладёт на свой провод.
Это общая «валюта» всей цепочки: ПК перекладывает payload в UDP и обратно, RX
распаковывает и пакует в SBUS. `flags` bit0 = failsafe.
## Сетевой слой (между экземплярами bridge)
- Транспорт — **UDP**: для управления 20 Гц свежий кадр ценнее
доставленного; TCP при потере встал бы в head-of-line blocking и копил лаг.
- Полезная нагрузка UDP — тот же `CONTROL` payload. `seq` даёт отбрасывание
устаревших/повторных пакетов (защита от переупорядочивания и дубликатов UDP)
с корректной арифметикой переполнения uint32. Перезапуск источника (seq снова
с 0) распознаётся по пачке подряд «несвежих» пакетов и трактуется как новая
сессия — иначе поток завис бы в failsafe навсегда. Это best-effort защита от
повтора старых команд, не криптографическая: аутентичность даёт HMAC, против
целевого replay на недоверенной сети используй WireGuard.
- Приём из сети и выдача SBUS на провод — **разные таймеры**: джиттер сети не
попадает на провод, STM-RX гонит SBUS строго каждые 50 мс из последнего
состояния.
- **Безопасность.** Для интернета обязателен общий ключ (`--key`): каждый UDP-
пакет аутентифицируется усечённым HMAC-SHA256 (8 байт), RX отбрасывает
неаутентичные и принимает только с ожидаемого адреса (`--peer`). Рекомендуемый
способ наружу — WireGuard-туннель, внутри которого идёт простой UDP. «Голый»
неаутентифицированный UDP допустим только в доверенной локальной сети.
- **Обратный heartbeat.** RX шлёт на адрес источника последнего CONTROL
маленький служебный пакет (~5 раз/с): magic `CB 01`, флаг собственного
failsafe, последний принятый seq; при включённом ключе — тоже под HMAC. TX
принимает их на тот же сокет (сквозь NAT проходит, т.к. он сам инициировал
поток) и показывает в логе, **отвечает ли приёмник** и не ушёл ли тот в
failsafe. Это чисто служебный PC-трафик, STM в нём не участвует, поэтому он не
в зеркале протокола, а в самом `bridge.py`. На выдачу SBUS и на основной поток
управления heartbeat не влияет.
- **Индикация для оператора.** Если запустить tx в терминале, он показывает одну
живую строку, обновляемую на месте: зелёное `● СВЯЗЬ ЕСТЬ`, жёлтое
`● СВЯЗЬ ЕСТЬ (приёмник в failsafe)` или красное `● СВЯЗИ НЕТ`. Под systemd
(вывод не в терминал) вместо строки идут события в журнал по смене состояния.
## FAILSAFE
Потеря связи никогда не оставляет тележку на последней команде — выход всегда
нейтраль (все каналы 1024), а не удержание.
- **Главный предохранитель — на самом STM-RX.** Если валидный `CONTROL` по USB
не приходил дольше `RX_USB_TIMEOUT_MS = 150` мс (3 кадра), STM-RX сам выдаёт на
провод нейтраль с флагом failsafe (SBUS bit3). Срабатывает при обрыве USB,
зависании или падении ПК-процесса. SBUS на провод идёт непрерывно 20 Гц в любом
случае — драйвер не видит пропажи сигнала.
- **Сетевой failsafe на ПК-RX.** При тишине из сети дольше 100 мс bridge активно
шлёт на STM-RX нейтральные `CONTROL` с флагом `CTRL_FLAG_FAILSAFE` в темпе
20 Гц, чтобы причина была явной. STM-RX транслирует этот флаг в бит failsafe
SBUS-кадра (byte 23 bit3). Восстановление — по первому свежему валидному пакету.
## Запуск
```
# сторона пульта
.venv/bin/python tools/bridge.py --role tx --peer 10.0.0.2:5005 --key SECRET
# сторона тележки
.venv/bin/python tools/bridge.py --role rx --bind 0.0.0.0:5005 --peer 10.0.0.1 --key SECRET
```
Локальная проверка без сети: оба экземпляра на одной машине с `--peer 127.0.0.1`
и двумя STM по USB; SBUS с RX снять `tools/decode.py` и сверить с осями стика TX.
Параметры можно задавать через env (`CART_BRIDGE_ROLE/PEER/BIND/DEVICE/KEY/
LOG_LEVEL`), CLI их переопределяет.
## Автозапуск: systemd-службы
Для постоянной работы мост ставится службами: `cart-bridge-tx` (отправитель) и
`cart-bridge-rx` (приёмник), по одной роли на машину. Юниты, примеры конфигов и
установщик — в каталоге `deploy/`:
```sh
cd deploy
sudo ./install-bridge.sh tx # на машине у пульта
sudo ./install-bridge.sh rx # на машине у тележки
```
Установщик подставляет реальные пути (python из `.venv`, `bridge.py`, рабочий
каталог) и пользователя, кладёт конфиги в `/etc/cart-bridge/{tx,rx}.env`. Там
задаются `CART_BRIDGE_PEER`/`BIND` и общий `CART_BRIDGE_KEY` (должен совпадать на
обеих сторонах). Логи и индикация «отвечает ли приёмник» — `journalctl -u
cart-bridge-tx -f`. Подробности — `deploy/README.md`.
@@ -0,0 +1,32 @@
; Две роли одной прошивки тележки, выбираются на этапе сборки:
; pio run -e cart_tx — пульт -> локальный SBUS + отправка по USB на ПК
; pio run -e cart_rx — приём по USB с ПК -> SBUS на драйвер колёс
; Роль публикуется в USB product string (CART-TX / CART-RX), по которой ПК-сервер
; (tools/bridge.py) находит нужный порт. Общий код (sbus, usb_link) собирается в
; оба env; специфичный — через build_src_filter, без мёртвого кода неактивной роли.
[env]
platform = ststm32
board = genericSTM32G431CB
framework = arduino
upload_protocol = stlink
debug_tool = stlink
monitor_speed = 115200
build_flags =
-DUSBCON
-DUSBD_USE_CDC
build_src_filter = -<*> +<sbus.cpp> +<usb_link.cpp>
[env:cart_tx]
build_flags =
${env.build_flags}
-DCART_ROLE_TX
-DUSB_PRODUCT_STRING="\"CART-TX\""
build_src_filter = ${env.build_src_filter} +<stick.cpp> +<app_tx.cpp>
[env:cart_rx]
build_flags =
${env.build_flags}
-DCART_ROLE_RX
-DUSB_PRODUCT_STRING="\"CART-RX\""
build_src_filter = ${env.build_src_filter} +<app_rx.cpp>
@@ -0,0 +1,70 @@
// Роль RX: принимает CONTROL по USB от ПК и гонит SBUS на провод драйвера так
// же, как если бы сигнал шёл с пульта. SBUS отправляется строго каждые 50 мс
// из последнего известного состояния — джиттер сети/USB не попадает на провод.
// FAILSAFE: если валидный CONTROL не приходил дольше RX_USB_TIMEOUT_MS, провод
// получает нейтраль (все каналы 1024) с флагом failsafe. Главный предохранитель
// живёт на самом STM и срабатывает даже при зависшем ПК или оборванном USB.
#ifdef CART_ROLE_RX
#include <Arduino.h>
#include "sbus.h"
#include "usb_link.h"
#include "device_id.h"
static uint16_t s_ch[16]; // последнее принятое состояние каналов
static uint8_t s_ctrlFlags = 0; // флаги последнего CONTROL (CTRL_FLAG_*)
static bool s_haveData = false;
static uint32_t s_lastGoodMs = 0;
static void replyIdent() {
LinkIdent id;
id.role = ROLE_RX;
id.proto_ver = LINK_VERSION;
id.fw_ver = CART_FW_VERSION;
cartReadUid(id.uid);
usbSendIdent(&id);
}
void setup() {
usbLinkInit();
sbusUartInit();
sbusNeutral(s_ch);
}
void loop() {
// приём кадров из USB
LinkRxEvent ev;
do {
usbLinkPoll(&ev);
if (ev.type == LINK_HELLO) {
replyIdent();
} else if (ev.type == LINK_CONTROL) {
memcpy(s_ch, ev.control.ch, sizeof(s_ch));
s_ctrlFlags = ev.control.flags;
s_lastGoodMs = millis();
s_haveData = true;
}
} while (ev.type != 0);
// выдача SBUS на провод строго по таймеру
static uint32_t next = 0;
uint32_t now = millis();
if (now < next)
return;
next = now + FRAME_PERIOD_MS;
bool stale = !s_haveData || (now - s_lastGoodMs > RX_USB_TIMEOUT_MS);
uint8_t frame[25];
if (stale) {
// нет свежих данных по USB -> нейтраль + явный failsafe на провод
uint16_t neutral[16];
sbusNeutral(neutral);
sbusPack(frame, neutral, SBUS_FLAG_FAILSAFE | SBUS_FLAG_FRAME_LOST);
} else {
// транслируем сетевой failsafe (CTRL_FLAG_FAILSAFE от ПК) в бит SBUS
uint8_t flags = (s_ctrlFlags & CTRL_FLAG_FAILSAFE) ? SBUS_FLAG_FAILSAFE : 0;
sbusPack(frame, s_ch, flags);
}
sbusSendFrame(frame);
}
#endif // CART_ROLE_RX
@@ -0,0 +1,59 @@
// Роль TX: к STM подключён пульт. Как и раньше, формирует SBUS на локальный
// провод (старый функционал не изменён), и ДОПОЛНИТЕЛЬНО шлёт те же значения
// каналов по USB на ПК кадром CONTROL. Отвечает на HELLO кадром IDENT.
#ifdef CART_ROLE_TX
#include <Arduino.h>
#include "sbus.h"
#include "stick.h"
#include "usb_link.h"
#include "device_id.h"
static void replyIdent() {
LinkIdent id;
id.role = ROLE_TX;
id.proto_ver = LINK_VERSION;
id.fw_ver = CART_FW_VERSION;
cartReadUid(id.uid);
usbSendIdent(&id);
}
void setup() {
usbLinkInit();
stickInit();
sbusUartInit();
}
void loop() {
// обслуживаем входящие кадры (ждём только HELLO от ПК)
LinkRxEvent ev;
do {
usbLinkPoll(&ev);
if (ev.type == LINK_HELLO)
replyIdent();
} while (ev.type != 0);
static uint32_t next = 0;
static uint32_t seq = 0;
uint32_t now = millis();
if (now < next)
return;
next = now + FRAME_PERIOD_MS;
uint16_t ch[16];
float fwd, right;
stickComputeChannels(ch, &fwd, &right);
// 1) локальный провод к драйверу — как раньше
uint8_t frame[25];
sbusPack(frame, ch);
sbusSendFrame(frame);
// 2) те же каналы по USB на ПК
LinkControl c;
c.seq = seq++;
c.t_ms = now;
c.flags = 0;
memcpy(c.ch, ch, sizeof(c.ch));
usbSendControl(&c);
}
#endif // CART_ROLE_TX
@@ -0,0 +1,9 @@
// 96-битный уникальный идентификатор чипа STM32G431 (UID base 0x1FFF7590).
// Используется в IDENT, чтобы ПК отличал конкретные экземпляры устройств.
#pragma once
#include <stdint.h>
#include <string.h>
static inline void cartReadUid(uint8_t uid[12]) {
memcpy(uid, (const void *)0x1FFF7590UL, 12);
}
@@ -0,0 +1,59 @@
// Единый источник истины для бинарного протокола связи пульт<->ПК<->тележка.
// Эти же числа зеркалятся в tools/link_proto.py — при любой правке меняй ОБА
// файла и сверяйся тест-векторами (tools/link_proto.py --selftest).
//
// Кадр поверх потока (USB CDC и полезная нагрузка UDP используют общий payload):
// [SYNC0][SYNC1][VER][TYPE][LEN][PAYLOAD..LEN][CRC16_LO][CRC16_HI]
// CRC16-CCITT (poly 0x1021, init 0xFFFF) по байтам [VER..конец PAYLOAD], LE.
#pragma once
#include <stdint.h>
// --- кадрирование ---
static const uint8_t LINK_SYNC0 = 0xA5;
static const uint8_t LINK_SYNC1 = 0x5A;
static const uint8_t LINK_VERSION = 1; // версия протокола, проверяется в handshake
static const uint8_t LINK_MAX_PAYLOAD = 64;
// --- типы кадров ---
enum LinkType : uint8_t {
LINK_HELLO = 0x03, // ПК -> STM: запрос идентификации (payload пуст)
LINK_IDENT = 0x02, // STM -> ПК: ответ на HELLO (LinkIdent)
LINK_CONTROL = 0x01, // поток управления в обе стороны (LinkControl)
LINK_LOG = 0x04, // STM -> ПК: отладочный ASCII-текст (вместо голого Serial)
};
// --- роли устройства (совпадают с продуктовой строкой USB) ---
enum LinkRole : uint8_t {
ROLE_TX = 'T', // к STM подключён пульт
ROLE_RX = 'R', // STM гонит SBUS на драйвер колёс
};
// --- флаги в LinkControl.flags ---
static const uint8_t CTRL_FLAG_FAILSAFE = 0x01; // источник объявил безопасный стоп
// Полезная нагрузка CONTROL (LE на проводе). Это общая «валюта» всей цепочки:
// TX кладёт сюда ровно те ch[], что уходят на его локальный провод, ПК
// перекладывает payload в UDP и обратно, RX распаковывает и пакует в SBUS.
// Пакуется/распаковывается вручную (без struct-паддинга) в link_pack.*.
struct LinkControl {
uint32_t seq; // монотонный счётчик кадров источника
uint32_t t_ms; // millis() на источнике (оценка задержки/джиттера)
uint8_t flags; // CTRL_FLAG_*
uint16_t ch[16]; // значения SBUS-каналов 0..2047
};
static const uint8_t LINK_CONTROL_LEN = 4 + 4 + 1 + 2 * 16; // 41 байт
// Полезная нагрузка IDENT.
struct LinkIdent {
uint8_t role; // LinkRole
uint8_t proto_ver; // LINK_VERSION на устройстве
uint16_t fw_ver; // версия прошивки
uint8_t uid[12]; // 96-битный UID чипа
};
static const uint8_t LINK_IDENT_LEN = 1 + 1 + 2 + 12; // 16 байт
static const uint16_t CART_FW_VERSION = 0x0100; // 1.00
// --- тайминги (мс) ---
static const uint32_t FRAME_PERIOD_MS = 50; // 20 Гц, темп старого пульта
static const uint32_t RX_USB_TIMEOUT_MS = 450; // нет CONTROL дольше -> нейтраль на проводе
@@ -0,0 +1,55 @@
#include "sbus.h"
#include <Arduino.h>
#include <string.h>
static UART_HandleTypeDef s_sbusUart;
void sbusNeutral(uint16_t ch[16]) {
for (int i = 0; i < 16; i++)
ch[i] = SBUS_MID;
}
void sbusPack(uint8_t out[25], const uint16_t ch[16], uint8_t flags) {
out[0] = 0x0F;
memset(out + 1, 0, 22);
uint32_t bitpos = 0;
for (int n = 0; n < 16; n++) {
for (int b = 0; b < 11; b++) {
if (ch[n] & (1u << b))
out[1 + (bitpos >> 3)] |= 1u << (bitpos & 7);
bitpos++;
}
}
out[23] = flags; // флаги: bit2=frame_lost, bit3=failsafe
out[24] = 0x00; // футер
}
// USART1 TX = PA9 (AF7), 100000 бод, 8E2, TX инвертирован
void sbusUartInit() {
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_USART1_CLK_ENABLE();
GPIO_InitTypeDef gpio = {};
gpio.Pin = GPIO_PIN_9;
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_LOW;
gpio.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &gpio);
s_sbusUart.Instance = USART1;
s_sbusUart.Init.BaudRate = 100000;
s_sbusUart.Init.WordLength = UART_WORDLENGTH_9B; // 8 данных + чётность
s_sbusUart.Init.StopBits = UART_STOPBITS_2;
s_sbusUart.Init.Parity = UART_PARITY_EVEN;
s_sbusUart.Init.Mode = UART_MODE_TX;
s_sbusUart.Init.HwFlowCtl = UART_HWCONTROL_NONE;
s_sbusUart.Init.OverSampling = UART_OVERSAMPLING_16;
s_sbusUart.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_TXINVERT_INIT;
s_sbusUart.AdvancedInit.TxPinLevelInvert = UART_ADVFEATURE_TXINV_ENABLE;
HAL_UART_Init(&s_sbusUart);
}
void sbusSendFrame(const uint8_t frame[25]) {
HAL_UART_Transmit(&s_sbusUart, (uint8_t *)frame, 25, 20);
}
+25
View File
@@ -0,0 +1,25 @@
// Общий SBUS-слой: упаковка кадра и вывод инвертированного SBUS на провод
// (USART1 TX = PA9, 100000 бод, 8E2, TXINV). Единственный владелец всего, что
// идёт на драйвер колёс — используется и TX (локальный провод), и RX.
// См. docs/protocol.md.
#pragma once
#include <stdint.h>
static const uint16_t SBUS_MID = 1024; // нейтраль всех каналов
// Флаги в байте 23 SBUS-кадра (см. docs/protocol.md).
static const uint8_t SBUS_FLAG_FRAME_LOST = 0x04; // bit2
static const uint8_t SBUS_FLAG_FAILSAFE = 0x08; // bit3
// Заполнить ch[16] нейтралью (все каналы 1024).
void sbusNeutral(uint16_t ch[16]);
// Упаковать 16 каналов (0..2047) в стандартный 25-байтовый SBUS-кадр.
// flags идёт в байт 23 (bit2=frame_lost, bit3=failsafe).
void sbusPack(uint8_t out[25], const uint16_t ch[16], uint8_t flags = 0);
// Инициализация USART1 для вывода инвертированного SBUS.
void sbusUartInit();
// Передать готовый 25-байтовый кадр на провод (блокирующе, таймаут 20 мс).
void sbusSendFrame(const uint8_t frame[25]);
@@ -0,0 +1,80 @@
#include "stick.h"
#include "sbus.h"
#include <Arduino.h>
// ---- калибровка стика (АЦП 12 бит, замерено 2026-08-20) ----
static const int X_FWD = 650, X_MID = 2014, X_BACK = 3378; // PA0 (плечи равны: 2014-650 = 3378-2014 = 1364)
static const int Y_RIGHT = 479, Y_MID = 1981, Y_LEFT = 3586; // PA1
static const int DEADZONE = 15; // отсечка дребезга вокруг нейтрали
// симметричные плечи: вперёд и назад дают одинаковый максимум
static const uint16_t CH2_SPAN = 591; // min(1643-1024, 1024-433)
static const uint16_t CH4_SPAN = 578; // min(1654-1024, 1024-446)
// ---- профиль газа/поворота ----
static const float MOVE_START = 0.15f; // старт движения, доля хода стика
static const float POWER_FWD = 0.60f; // потолок «вперёд»
static const float POWER_BACK = 0.50f; // потолок «назад»
static const float POWER_TURN = 0.70f; // потолок поворота
static const uint16_t CH2_DB = 221; // мёртвая зона приёмника тележки
static const uint16_t CH4_DB = 221;
static const uint32_t PIN_X = PA0;
static const uint32_t PIN_Y = PA1;
// нормализация одной оси в [-1..1] с мёртвой зоной и асимметричными плечами
static float axisNorm(int adc, int lowEnd, int mid, int highEnd) {
float x;
if (adc < mid - DEADZONE) {
x = (float)(mid - adc) / (float)(mid - lowEnd); // к lowEnd -> +1
} else if (adc > mid + DEADZONE) {
x = -(float)(adc - mid) / (float)(highEnd - mid); // к highEnd -> -1
} else {
return 0.0f;
}
return constrain(x, -1.0f, 1.0f);
}
// стик [-1..1] -> SBUS-значение канала: ниже MOVE_START — нейтраль, выше —
// линейно от порога срабатывания приёмника (db) до POWER_FRAC*span на полном стике
static uint16_t driveToSbus(float x, uint16_t span, uint16_t db,
float powerPos, float powerNeg) {
float mag = fabsf(x);
if (mag <= MOVE_START)
return SBUS_MID;
float outMax = (x > 0.0f ? powerPos : powerNeg) * span;
float t = (mag - MOVE_START) / (1.0f - MOVE_START);
long delta = lroundf(db + t * (outMax - db));
return (x > 0.0f) ? (uint16_t)(SBUS_MID + delta)
: (uint16_t)(SBUS_MID - delta);
}
static int readAvg(uint32_t pin) {
uint32_t acc = 0;
for (int i = 0; i < 16; i++)
acc += analogRead(pin);
return acc / 16;
}
void stickInit() {
analogReadResolution(12);
pinMode(PIN_X, INPUT_ANALOG);
pinMode(PIN_Y, INPUT_ANALOG);
}
void stickComputeChannels(uint16_t ch[16], float *fwdOut, float *rightOut) {
int adcX = readAvg(PIN_X);
int adcY = readAvg(PIN_Y);
// вперёд = adcX к X_FWD (вниз) -> +1; вправо = adcY к Y_RIGHT -> +1
float fwd = axisNorm(adcX, X_FWD, X_MID, X_BACK);
float right = axisNorm(adcY, Y_RIGHT, Y_MID, Y_LEFT);
sbusNeutral(ch);
ch[1] = driveToSbus(fwd, CH2_SPAN, CH2_DB, POWER_FWD, POWER_BACK); // канал 2 — газ
ch[3] = driveToSbus(right, CH4_SPAN, CH4_DB, POWER_TURN, POWER_TURN); // канал 4 — поворот
if (fwdOut)
*fwdOut = fwd;
if (rightOut)
*rightOut = right;
}
+12
View File
@@ -0,0 +1,12 @@
// Пульт: чтение стика (АЦП PA0/PA1), калибровка, профиль газа/поворота и
// формирование значений SBUS-каналов. Вся «логика пульта» живёт здесь.
// Компилируется только для роли TX (см. build_src_filter в platformio.ini).
#pragma once
#include <stdint.h>
// Инициализация АЦП-входов стика.
void stickInit();
// Прочитать стик и заполнить ch[16] значениями SBUS-каналов (нейтраль + ch2/ch4).
// Возвращает по ссылкам нормализованные оси для отладки.
void stickComputeChannels(uint16_t ch[16], float *fwdOut, float *rightOut);
@@ -0,0 +1,148 @@
#include "usb_link.h"
#include <Arduino.h>
#include <string.h>
uint16_t linkCrc16(const uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= (uint16_t)data[i] << 8;
for (int b = 0; b < 8; b++)
crc = (crc & 0x8000) ? (uint16_t)((crc << 1) ^ 0x1021) : (uint16_t)(crc << 1);
}
return crc;
}
// ---- сериализация payload (little-endian, без struct-паддинга) ----
static void put_u16(uint8_t *p, uint16_t v) { p[0] = v & 0xFF; p[1] = v >> 8; }
static void put_u32(uint8_t *p, uint32_t v) {
p[0] = v & 0xFF; p[1] = (v >> 8) & 0xFF; p[2] = (v >> 16) & 0xFF; p[3] = (v >> 24) & 0xFF;
}
static uint16_t get_u16(const uint8_t *p) { return (uint16_t)p[0] | ((uint16_t)p[1] << 8); }
static uint32_t get_u32(const uint8_t *p) {
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
static uint8_t packControl(const LinkControl *c, uint8_t *out) {
put_u32(out + 0, c->seq);
put_u32(out + 4, c->t_ms);
out[8] = c->flags;
for (int i = 0; i < 16; i++)
put_u16(out + 9 + 2 * i, c->ch[i]);
return LINK_CONTROL_LEN;
}
static void unpackControl(const uint8_t *in, LinkControl *c) {
c->seq = get_u32(in + 0);
c->t_ms = get_u32(in + 4);
c->flags = in[8];
for (int i = 0; i < 16; i++)
c->ch[i] = get_u16(in + 9 + 2 * i);
}
// ---- отправка кадра ----
static void sendFrame(uint8_t type, const uint8_t *payload, uint8_t len) {
uint8_t buf[3 + LINK_MAX_PAYLOAD + 2];
buf[0] = LINK_VERSION;
buf[1] = type;
buf[2] = len;
if (len)
memcpy(buf + 3, payload, len);
uint16_t crc = linkCrc16(buf, 3 + len);
uint8_t hdr[2] = {LINK_SYNC0, LINK_SYNC1};
Serial.write(hdr, 2);
Serial.write(buf, 3 + len);
uint8_t crcb[2] = {(uint8_t)(crc & 0xFF), (uint8_t)(crc >> 8)};
Serial.write(crcb, 2);
}
void usbSendControl(const LinkControl *c) {
uint8_t pl[LINK_CONTROL_LEN];
uint8_t n = packControl(c, pl);
sendFrame(LINK_CONTROL, pl, n);
}
void usbSendIdent(const LinkIdent *id) {
uint8_t pl[LINK_IDENT_LEN];
pl[0] = id->role;
pl[1] = id->proto_ver;
put_u16(pl + 2, id->fw_ver);
memcpy(pl + 4, id->uid, 12);
sendFrame(LINK_IDENT, pl, LINK_IDENT_LEN);
}
void usbSendLog(const char *msg) {
uint8_t len = (uint8_t)strnlen(msg, LINK_MAX_PAYLOAD);
sendFrame(LINK_LOG, (const uint8_t *)msg, len);
}
void usbLinkInit() {
Serial.begin(115200);
}
// ---- стейт-машина приёма ----
enum RxState { WAIT_S0, WAIT_S1, READ_HDR, READ_PAYLOAD, READ_CRC };
static RxState s_state = WAIT_S0;
static uint8_t s_hdr[3]; // VER, TYPE, LEN
static uint8_t s_hdrGot;
static uint8_t s_payload[LINK_MAX_PAYLOAD];
static uint8_t s_payGot;
static uint8_t s_crc[2];
static uint8_t s_crcGot;
static void resync() { s_state = WAIT_S0; s_hdrGot = s_payGot = s_crcGot = 0; }
void usbLinkPoll(LinkRxEvent *ev) {
ev->type = 0;
while (Serial.available() > 0) {
uint8_t b = (uint8_t)Serial.read();
switch (s_state) {
case WAIT_S0:
if (b == LINK_SYNC0) s_state = WAIT_S1;
break;
case WAIT_S1:
s_state = (b == LINK_SYNC1) ? READ_HDR : WAIT_S0;
s_hdrGot = 0;
break;
case READ_HDR:
s_hdr[s_hdrGot++] = b;
if (s_hdrGot == 3) {
// s_hdr: VER, TYPE, LEN
if (s_hdr[0] != LINK_VERSION || s_hdr[2] > LINK_MAX_PAYLOAD) {
resync(); // неверная версия/длина — сдвиг и поиск заново
break;
}
s_payGot = 0;
s_state = (s_hdr[2] == 0) ? READ_CRC : READ_PAYLOAD;
}
break;
case READ_PAYLOAD:
s_payload[s_payGot++] = b;
if (s_payGot == s_hdr[2])
s_state = READ_CRC;
break;
case READ_CRC:
s_crc[s_crcGot++] = b;
if (s_crcGot == 2) {
uint8_t chk[3 + LINK_MAX_PAYLOAD];
chk[0] = s_hdr[0]; chk[1] = s_hdr[1]; chk[2] = s_hdr[2];
memcpy(chk + 3, s_payload, s_hdr[2]);
uint16_t want = (uint16_t)s_crc[0] | ((uint16_t)s_crc[1] << 8);
uint16_t got = linkCrc16(chk, 3 + s_hdr[2]);
uint8_t type = s_hdr[1];
uint8_t len = s_hdr[2];
resync();
if (want == got) {
if (type == LINK_CONTROL && len == LINK_CONTROL_LEN) {
unpackControl(s_payload, &ev->control);
ev->type = LINK_CONTROL;
return;
} else if (type == LINK_HELLO || type == LINK_IDENT || type == LINK_LOG) {
ev->type = type;
return;
}
}
// битый CRC или неизвестный тип — молча ищем следующий кадр
}
break;
}
}
}
@@ -0,0 +1,23 @@
// Бинарный протокол поверх USB CDC (Serial): кадрирование, CRC16, сериализация
// CONTROL/IDENT/LOG и стейт-машина приёма с ресинхронизацией. Общий для TX и RX.
#pragma once
#include "link_protocol.h"
// Результат опроса входящего потока.
struct LinkRxEvent {
uint8_t type; // 0 = в этот вызов целого кадра нет; иначе LinkType
LinkControl control; // валидно при type == LINK_CONTROL
};
void usbLinkInit();
// Разобрать доступные байты. Возвращает максимум один полный валидный кадр за
// вызов (type != 0). Вызывать в цикле, пока event.type != 0, затем дальше.
void usbLinkPoll(LinkRxEvent *ev);
void usbSendControl(const LinkControl *c);
void usbSendIdent(const LinkIdent *id);
void usbSendLog(const char *msg);
// Общий CRC16-CCITT (poly 0x1021, init 0xFFFF). Экспортирован для тестов.
uint16_t linkCrc16(const uint8_t *data, uint16_t len);
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""Analyze a captured frame: extract pulse timing structure.
Usage: .venv/bin/python tools/analyze.py captures/<name>.npz
"""
import sys
import numpy as np
path = sys.argv[1]
d = np.load(path)
volts = d["volts"]
srate = float(d["srate"])
dt_us = 1e6 / srate
# threshold midway between the two dominant plateaus
hi_level = np.median(volts) # idle dominates the frame -> median = idle (high)
lo_level = np.percentile(volts, 10)
thr = (hi_level + lo_level) / 2
bits = (volts > thr).astype(np.int8)
print(f"levels: high~{hi_level:.2f} low~{lo_level:.2f} thr={thr:.2f} (raw units)")
# run-length encode
edges = np.flatnonzero(np.diff(bits)) + 1
starts = np.concatenate(([0], edges))
ends = np.concatenate((edges, [len(bits)]))
levels = bits[starts]
dur_us = (ends - starts) * dt_us
print(f"{len(levels)} runs total")
print("\nidx level dur_us (first/last runs are idle padding)")
for i, (lv, du) in enumerate(zip(levels, dur_us)):
tag = "H" if lv else "L"
print(f"{i:4d} {tag} {du:10.2f}")
@@ -0,0 +1,379 @@
#!/usr/bin/env python3
"""Сетевой мост «пульт -> тележка»: один симметричный сервер, роль задаётся
флагом. Два экземпляра переносят управление с STM-передатчика на STM-приёмник
через локальную сеть/интернет.
[пульт] ADC-> STM(CART-TX) --USB--> bridge --role tx --UDP--> bridge --role rx --USB--> STM(CART-RX) --SBUS-> драйвер
Роль tx: читает кадры CONTROL из USB от STM-TX и шлёт их по UDP на peer.
Роль rx: принимает UDP, пишет CONTROL в USB на STM-RX; при тишине из сети шлёт
явную нейтраль с флагом failsafe (главный предохранитель — на самом STM).
Обратный канал: rx шлёт tx heartbeat, поэтому на стороне отправителя видно,
отвечает ли приёмник и не ушёл ли он сам в failsafe.
Устройство выбирается по USB product string (CART-TX / CART-RX) и подтверждается
handshake-кадром IDENT (роль + версия протокола). Для интернета используй общий
ключ (--key) — каждый UDP-пакет аутентифицируется HMAC-SHA256; без ключа трафик
идёт в открытом виде (только для доверенной локальной сети).
Параметры можно задавать и через env (CART_BRIDGE_ROLE/PEER/BIND/DEVICE/KEY/
LOG_LEVEL) — так настраиваются systemd-службы (см. deploy/).
Запуск (из каталога прошивки):
.venv/bin/python tools/bridge.py --role tx --peer 10.0.0.2:5005
.venv/bin/python tools/bridge.py --role rx --bind 0.0.0.0:5005
"""
import argparse
import hmac
import logging
import os
import select
import socket
import struct
import sys
import time
from hashlib import sha256
import serial
from serial.tools import list_ports
import link_proto as lp
HMAC_LEN = 8 # усечённый HMAC-SHA256 на пакет
NET_TIMEOUT_S = 0.50 # нет UDP дольше -> rx начинает слать failsafe
HELLO_TIMEOUT_S = 1.0
SEQ_RESET_AFTER = 20 # столько подряд «несвежих» пакетов = перезапуск источника
# Обратный канал «жив ли приёмник»: rx шлёт heartbeat в сторону tx (на адрес
# источника последнего CONTROL), tx по ним показывает, отвечает ли приёмник и
# не ушёл ли он сам в failsafe. Это PC-only служебный трафик (к STM отношения не
# имеет), поэтому живёт здесь, а не в зеркале протокола link_proto.
HEARTBEAT_MAGIC = b"\xCB\x01"
HEARTBEAT_PERIOD_S = 0.2 # как часто rx шлёт heartbeat
HEARTBEAT_TIMEOUT_S = 1.0 # нет heartbeat дольше -> tx считает приёмник недоступным
HEARTBEAT_STATUS_S = 5.0 # период печати статуса приёмника на tx
HB_FLAG_FAILSAFE = 0x01
log = logging.getLogger("bridge")
def pack_hb(failsafe: bool, last_seq: int) -> bytes:
flags = HB_FLAG_FAILSAFE if failsafe else 0
return HEARTBEAT_MAGIC + struct.pack("<BI", flags, last_seq & 0xFFFFFFFF)
def parse_hb(payload: bytes):
"""-> dict(failsafe, last_seq) либо None, если это не heartbeat."""
if len(payload) != len(HEARTBEAT_MAGIC) + 5 or payload[:2] != HEARTBEAT_MAGIC:
return None
flags, last_seq = struct.unpack_from("<BI", payload, 2)
return {"failsafe": bool(flags & HB_FLAG_FAILSAFE), "last_seq": last_seq}
_ANSI = {"green": "\033[1;32m", "red": "\033[1;31m", "yellow": "\033[1;33m", "reset": "\033[0m"}
def render_link_status(alive: bool, failsafe: bool, color: bool):
"""Живая одна строка для оператора: зелёная «СВЯЗЬ ЕСТЬ» / красная «СВЯЗИ
НЕТ», обновляется на месте (возврат каретки)."""
if not alive:
text, c = "СВЯЗИ НЕТ", "red"
elif failsafe:
text, c = "СВЯЗЬ ЕСТЬ (приёмник в failsafe)", "yellow"
else:
text, c = "СВЯЗЬ ЕСТЬ", "green"
body = "● " + text
if color:
body = _ANSI[c] + body + _ANSI["reset"]
sys.stdout.write("\r\033[2K" + body) # \033[2K — очистить строку
sys.stdout.flush()
# ---------------- выбор и handshake USB-устройства ----------------
def find_port(role: str, selector: str | None) -> str:
"""Найти последовательный порт по роли (product string) или явному
селектору (путь /dev/..., подстрока product или серийник)."""
if selector and ("/" in selector or selector.startswith("COM")):
return selector # явный путь к устройству
want_product = {"tx": "CART-TX", "rx": "CART-RX"}[role]
matches = []
for p in list_ports.comports():
hay = " ".join(filter(None, (p.product, p.serial_number, p.description))).upper()
if selector:
if selector.upper() in hay:
matches.append(p)
elif want_product in hay:
matches.append(p)
if not matches:
raise RuntimeError(f"USB-устройство роли {role} ({want_product}) не найдено")
if len(matches) > 1:
names = ", ".join(f"{m.device}({m.product})" for m in matches)
raise RuntimeError(f"найдено несколько кандидатов роли {role}: {names}; уточни --device")
return matches[0].device
def handshake(ser: serial.Serial, role: str):
"""Послать HELLO, дождаться IDENT, проверить роль и версию протокола."""
want_role = {"tx": lp.ROLE_TX, "rx": lp.ROLE_RX}[role]
parser = lp.FrameParser()
ser.reset_input_buffer()
ser.write(lp.build_frame(lp.HELLO, b""))
deadline = time.monotonic() + HELLO_TIMEOUT_S
while time.monotonic() < deadline:
for ftype, payload in parser.feed(ser.read(256)):
if ftype != lp.IDENT:
continue
ident = lp.unpack_ident(payload)
if ident["proto_ver"] != lp.VERSION:
raise RuntimeError(
f"версия протокола устройства {ident['proto_ver']} != {lp.VERSION}")
if ident["role"] != want_role:
raise RuntimeError(
f"устройство представилось ролью {lp.role_name(ident['role'])}, ожидалась {role}")
log.info("handshake OK: роль %s, fw 0x%04x, uid %s",
role, ident["fw_ver"], ident["uid"].hex())
return
raise RuntimeError("нет ответа IDENT (не та прошивка/порт?)")
def open_device(role: str, selector: str | None) -> serial.Serial:
port = find_port(role, selector)
ser = serial.Serial(port, baudrate=115200, timeout=0.05)
handshake(ser, role)
return ser
# ---------------- аутентификация пакетов ----------------
def seal(payload: bytes, key: bytes | None) -> bytes:
if not key:
return payload
return payload + hmac.new(key, payload, sha256).digest()[:HMAC_LEN]
def unseal(datagram: bytes, key: bytes | None) -> bytes | None:
"""Проверить HMAC и вернуть payload, либо None если пакет невалиден."""
if not key:
return datagram
if len(datagram) < HMAC_LEN:
return None
payload, tag = datagram[:-HMAC_LEN], datagram[-HMAC_LEN:]
want = hmac.new(key, payload, sha256).digest()[:HMAC_LEN]
return payload if hmac.compare_digest(tag, want) else None
def seq_newer(a: int, b: int) -> bool:
"""Арифметика последовательных номеров с переполнением uint32."""
return 0 < ((a - b) & 0xFFFFFFFF) < 0x80000000
# ---------------- роль TX: USB -> сеть ----------------
def run_tx(args, key):
peer = parse_hostport(args.peer, "peer")
ser = open_device("tx", args.device)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
parser = lp.FrameParser()
log.info("tx: %s -> UDP %s:%d", ser.port, *peer)
sent = 0
rx_alive = False # получаем ли heartbeat от приёмника
rx_failsafe = False # сообщил ли приёмник, что сам в failsafe
last_hb = 0.0
last_status = 0.0
# В терминале — живая цветная строка; под systemd (journald, не TTY) — обычные
# события по изменению состояния.
tty = sys.stdout.isatty()
color = tty and os.environ.get("NO_COLOR") is None
while True:
# read(1) блокирует до первого байта, затем забираем всё доступное —
# кадр уходит в сеть сразу, без ожидания таймаута чтения (иначе каждый
# кадр задерживался бы до таймаута порта).
data = ser.read(1)
if ser.in_waiting:
data += ser.read(ser.in_waiting)
for ftype, payload in parser.feed(data):
if ftype == lp.CONTROL:
sock.sendto(seal(payload, key), peer)
sent += 1
elif ftype == lp.LOG:
log.info("STM-TX: %s", payload.decode("ascii", "replace"))
now = time.monotonic()
prev_alive, prev_fs = rx_alive, rx_failsafe
# обратный канал: heartbeat от приёмника (приходит на тот же сокет)
while select.select([sock], [], [], 0)[0]:
try:
datagram, _ = sock.recvfrom(2048)
except BlockingIOError:
break
hb = parse_hb(unseal(datagram, key) or b"")
if hb is None:
continue
last_hb = now
rx_alive = True
rx_failsafe = hb["failsafe"]
if rx_alive and now - last_hb > HEARTBEAT_TIMEOUT_S:
rx_alive, rx_failsafe = False, False
if tty:
render_link_status(rx_alive, rx_failsafe, color) # живая строка
else:
if rx_alive != prev_alive:
(log.info if rx_alive else log.warning)(
"приёмник отвечает" if rx_alive else "приёмник НЕ отвечает")
elif rx_alive and rx_failsafe != prev_fs:
(log.warning if rx_failsafe else log.info)(
"приёмник в failsafe: наш поток не доходит" if rx_failsafe
else "приёмник принимает поток")
if now - last_status >= HEARTBEAT_STATUS_S:
last_status = now
log.info("приёмник: %s (отправлено %d)",
"отвечает" if rx_alive else "НЕ отвечает", sent)
# ---------------- роль RX: сеть -> USB ----------------
def run_rx(args, key):
bind = parse_hostport(args.bind, "bind")
# для rx --peer — фильтр источника по хосту (порт источника эфемерный)
allowed_host = args.peer.split(":")[0] if args.peer else None
ser = open_device("rx", args.device)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(bind)
sock.setblocking(False)
parser = lp.FrameParser()
log.info("rx: UDP %s:%d -> %s", bind[0], bind[1], ser.port)
last_seq = None
last_net = time.monotonic()
last_fs = 0.0
last_hb_sent = 0.0
fs_seq = 0
in_failsafe = False
rejects = 0 # подряд отброшенных по seq -> признак перезапуска источника
tx_addr = None # куда слать heartbeat (источник последнего CONTROL)
def accept(datagram, addr):
"""Проверить пакет и обновить состояние; вернуть payload принятого
свежего кадра либо None."""
nonlocal last_seq, last_net, in_failsafe, rejects, tx_addr
if allowed_host and addr[0] != allowed_host:
log.warning("пакет с неожиданного адреса %s отброшен", addr[0])
return None
payload = unseal(datagram, key)
if not payload or len(payload) != lp.CONTROL_LEN:
log.warning("битый/неаутентичный пакет отброшен")
return None
tx_addr = addr # аутентичный CONTROL -> знаем обратный адрес для heartbeat
c = lp.unpack_control(payload)
# Принять свежий seq ИЛИ новую сессию: источник (STM-TX) после ребута
# начинает seq заново с 0, и без сброса last_seq поток завис бы в
# failsafe навсегда. Новую сессию распознаём по пачке подряд «несвежих»
# пакетов (устойчиво к одиночным reorder/dup).
fresh = last_seq is None or seq_newer(c["seq"], last_seq)
if not fresh:
rejects += 1
if rejects >= SEQ_RESET_AFTER:
log.warning("seq откатился (%d -> %d): новая сессия", last_seq, c["seq"])
fresh = True
if not fresh:
return None # устаревший/повторный — anti-replay
rejects = 0
last_seq = c["seq"]
last_net = time.monotonic()
if in_failsafe:
log.info("связь восстановлена")
in_failsafe = False
return payload
while True:
# Ждём пакет до 20 мс (пауза цикла), затем БЕЗ блокировки выгребаем всю
# накопившуюся очередь сокета и шлём на STM только самый свежий кадр
# (latest-wins): так задержка не растёт, даже если пакеты пришли пачкой.
newest = None
ready = select.select([sock], [], [], lp.FRAME_PERIOD_MS / 2000.0)[0]
while ready:
try:
datagram, addr = sock.recvfrom(2048)
except BlockingIOError:
break
p = accept(datagram, addr)
if p is not None:
newest = p
ready = select.select([sock], [], [], 0)[0]
if newest is not None:
ser.write(lp.build_frame(lp.CONTROL, newest))
# сетевой failsafe: тишина -> активная нейтраль на STM в темпе 20 Гц
now = time.monotonic()
if now - last_net >= NET_TIMEOUT_S and now - last_fs >= lp.FRAME_PERIOD_MS / 1000.0:
if not in_failsafe:
log.warning("потеря сети -> failsafe (нейтраль)")
in_failsafe = True
last_seq = None # рестарт источника при простое примем сразу
ser.write(lp.build_frame(lp.CONTROL, lp.neutral_control(fs_seq, failsafe=True)))
fs_seq = (fs_seq + 1) & 0xFFFFFFFF
last_fs = now
# обратный heartbeat к tx: «приёмник жив», с признаком собственного failsafe
if tx_addr is not None and now - last_hb_sent >= HEARTBEAT_PERIOD_S:
sock.sendto(seal(pack_hb(in_failsafe, last_seq or 0), key), tx_addr)
last_hb_sent = now
# неблокирующий дренаж входящих LOG от STM-RX
if ser.in_waiting:
for ftype, payload in parser.feed(ser.read(ser.in_waiting)):
if ftype == lp.LOG:
log.info("STM-RX: %s", payload.decode("ascii", "replace"))
# ---------------- общее ----------------
def parse_hostport(s: str, what: str):
if ":" not in s:
raise SystemExit(f"--{what} ожидает HOST:PORT, получено {s!r}")
host, port = s.rsplit(":", 1)
return (host, int(port))
def main():
# Значения по умолчанию берутся из env (CART_BRIDGE_*), чтобы systemd-юнит
# мог задавать всё через EnvironmentFile; аргументы CLI их переопределяют.
env = os.environ.get
ap = argparse.ArgumentParser(description="мост пульт<->тележка через сеть")
ap.add_argument("--role", default=env("CART_BRIDGE_ROLE"), choices=("tx", "rx"))
ap.add_argument("--peer", default=env("CART_BRIDGE_PEER"),
help="tx: куда слать HOST:PORT; rx: разрешённый источник (опц.)")
ap.add_argument("--bind", default=env("CART_BRIDGE_BIND", "0.0.0.0:5005"),
help="rx: локальный UDP HOST:PORT")
ap.add_argument("--device", default=env("CART_BRIDGE_DEVICE"),
help="путь к порту или подстрока product/serial (иначе по роли)")
ap.add_argument("--key", default=env("CART_BRIDGE_KEY"),
help="общий ключ для HMAC-аутентификации пакетов (для интернета)")
ap.add_argument("--log-level", default=env("CART_BRIDGE_LOG_LEVEL", "INFO"))
args = ap.parse_args()
logging.basicConfig(level=args.log_level.upper(),
format="%(asctime)s %(levelname)s %(message)s")
if args.role not in ("tx", "rx"):
raise SystemExit("нужна роль: --role tx|rx (или CART_BRIDGE_ROLE)")
key = args.key.encode() if args.key else None
if args.role == "tx" and not args.peer:
raise SystemExit("--peer обязателен для роли tx")
if not key:
log.warning("без --key: трафик не аутентифицирован, только для доверенной сети")
try:
(run_tx if args.role == "tx" else run_rx)(args, key)
except KeyboardInterrupt:
log.info("остановлено")
finally:
if sys.stdout.isatty():
sys.stdout.write("\n") # увести живую строку от приглашения shell
if __name__ == "__main__":
main()
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Capture one single-shot CH3 frame from Hantek DPO7204C, save raw + PNG.
Flow: query settings -> :SINGle -> poll :TRIGger:STATus? until STOP ->
WAVeform:DATA:ALL? CHANnel3 (drained completely, multi-packet aware).
Scope is left in STOP so the on-screen frame matches the saved data.
Usage: .venv/bin/python tools/capture.py <name> [device]
Saves captures/<name>.bin, captures/<name>.npz, captures/<name>.png
"""
import os
import sys
import time
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
DEV = sys.argv[2] if len(sys.argv) > 2 else "/dev/usbtmc2"
NAME = sys.argv[1] if len(sys.argv) > 1 else "capture"
OUTDIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "captures")
os.makedirs(OUTDIR, exist_ok=True)
FIRST_HDR = 11 + 117 # '#9'+9-digit len, then 18 bytes counters + 99 bytes info
NEXT_HDR = 11 + 18 # follow-up packets: counters only
def read_exact(fd, n):
buf = b""
while len(buf) < n:
chunk = os.read(fd, min(1 << 20, n - len(buf)))
if not chunk:
raise IOError("short read from scope")
buf += chunk
return buf
def read_packet(fd):
head = read_exact(fd, 11)
assert head[:2] == b"#9", f"bad packet start: {head!r}"
pkt_len = int(head[2:11])
body = read_exact(fd, pkt_len)
return head + body
def query(fd, cmd):
os.write(fd, cmd.encode() + b"\n")
return os.read(fd, 256).decode(errors="replace").strip()
fd = os.open(DEV, os.O_RDWR)
print("IDN:", query(fd, "*IDN?"))
tdiv = float(query(fd, ":TIMebase:SCALe?"))
vdiv = float(query(fd, ":CHANnel3:SCALe?"))
voff = float(query(fd, ":CHANnel3:OFFSet?"))
print(f"tdiv={tdiv} s/div vdiv={vdiv} V/div offset={voff} V")
os.write(fd, b":SINGle\n")
for _ in range(100):
time.sleep(0.1)
st = query(fd, ":TRIGger:STATus?")
if st == "STOP":
break
else:
sys.exit(f"scope did not reach STOP (last status: {st})")
print("status: STOP (frame captured)")
srate = float(query(fd, ":ACQuire:SRATe?"))
print(f"srate={srate:.3e} Sa/s")
os.write(fd, b"WAVeform:DATA:ALL? CHANnel3\n")
pkt = read_packet(fd)
total_len = int(pkt[11:20])
info_hdr = pkt[29:FIRST_HDR]
data = pkt[FIRST_HDR:]
raw_all = pkt
while len(data) < total_len:
os.write(fd, b"WAVeform:DATA:ALL? CHANnel3\n")
p = read_packet(fd)
raw_all += p
data += p[NEXT_HDR:]
print(f"received {len(data)} samples (declared {total_len})")
print("info header hex:", info_hdr.hex(" "))
os.close(fd)
raw = np.frombuffer(data[:total_len], dtype=np.uint8).astype(np.float32)
# unsigned 8-bit, 25.6 levels/div, mid-screen = code 128, offset shifts zero
volts = (raw - 128.0) / 25.6 * vdiv - voff
t = np.arange(len(volts)) / srate * 1e3 # ms
base = os.path.join(OUTDIR, NAME)
with open(base + ".bin", "wb") as f:
f.write(raw_all)
np.savez(base + ".npz", volts=volts, srate=srate, vdiv=vdiv, voff=voff, tdiv=tdiv)
fig, axes = plt.subplots(2, 1, figsize=(16, 8))
axes[0].plot(t, volts, lw=0.5)
axes[0].set_title(f"{NAME} — full frame ({srate:.0e} Sa/s, {vdiv} V/div, off {voff} V)")
# zoom on activity: region where signal deviates from its median
dev_idx = np.where(np.abs(volts - np.median(volts)) > 0.5)[0]
if len(dev_idx):
lo = max(0, dev_idx[0] - int(0.05 * (dev_idx[-1] - dev_idx[0] + 1)) - 100)
hi = min(len(volts), dev_idx[-1] + int(0.05 * (dev_idx[-1] - dev_idx[0] + 1)) + 100)
axes[1].plot(t[lo:hi], volts[lo:hi], lw=0.7)
axes[1].set_title("zoom on activity")
for ax in axes:
ax.set_xlabel("t, ms")
ax.set_ylabel("U, V")
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig(base + ".png", dpi=110)
print("saved:", base + ".png")
print(f"range: min={volts.min():.3f} V max={volts.max():.3f} V median={np.median(volts):.3f} V")
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Rigorous comparison of two SBUS captures (old remote vs our firmware).
Usage: .venv/bin/python tools/compare.py captures/a.npz captures/b.npz
"""
import sys
import numpy as np
def extract(path):
d = np.load(path)
v = d["volts"]
srate = float(d["srate"])
idle = np.median(v)
p10, p90 = np.percentile(v, [10, 90])
active = p10 if abs(p10 - idle) > abs(p90 - idle) else p90
thr = (idle + active) / 2
phys_hi = v > thr # physical high (pulse)
# plateau levels: median of samples well inside each state
lvl_hi = np.median(v[phys_hi])
lvl_lo = np.median(v[~phys_hi])
# runs
sig = phys_hi.astype(np.int8)
edges = np.flatnonzero(np.diff(sig)) + 1
starts = np.concatenate(([0], edges))
ends = np.concatenate((edges, [len(sig)]))
levels = sig[starts]
dur_us = (ends - starts) * 1e6 / srate
# burst envelope: first to last physical-high sample
hi_idx = np.flatnonzero(phys_hi)
envelope_us = (hi_idx[-1] - hi_idx[0] + 1) * 1e6 / srate
# inner runs (drop leading/trailing idle)
runs = [(int(l), float(du)) for l, du in zip(levels[1:-1], dur_us[1:-1])]
# UART logic: logic1 == idle state; idle here is physical low
stream = []
for l, du in runs:
n = max(1, round(du / 10.0))
stream.extend([1 - l] * n) # physical high -> logic 0
stream.extend([1] * 24)
frames = []
i = 0
while i + 12 <= len(stream):
if stream[i] == 1:
i += 1
continue
byte = sum(b << k for k, b in enumerate(stream[i + 1 : i + 9]))
frames.append(byte)
i += 12
# bit clock estimate: envelope should be 299 bits (last stop bits merge w/ idle)
n_units = round(envelope_us / 10.0)
bit_us = envelope_us / n_units
return {
"lvl_hi": lvl_hi,
"lvl_lo": lvl_lo,
"runs": runs,
"frames": frames,
"envelope_us": envelope_us,
"bit_us": bit_us,
"vmin": float(v.min()),
"vmax": float(v.max()),
}
a_path, b_path = sys.argv[1], sys.argv[2]
A, B = extract(a_path), extract(b_path)
print(f"{'':24s} {'A: ' + a_path:>28s} {'B: ' + b_path:>28s}")
print(f"{'bytes decoded':24s} {len(A['frames']):>28d} {len(B['frames']):>28d}")
ha = " ".join(f"{x:02X}" for x in A["frames"])
hb = " ".join(f"{x:02X}" for x in B["frames"])
print(f"frames identical: {A['frames'] == B['frames']}")
print(" A:", ha)
print(" B:", hb)
print(f"{'run count':24s} {len(A['runs']):>28d} {len(B['runs']):>28d}")
qa = [round(du / 10) for _, du in A["runs"]]
qb = [round(du / 10) for _, du in B["runs"]]
la = [l for l, _ in A["runs"]]
lb = [l for l, _ in B["runs"]]
print(f"quantized run pattern identical: {qa == qb and la == lb}")
print(f"{'envelope, us':24s} {A['envelope_us']:>28.2f} {B['envelope_us']:>28.2f}")
print(f"{'bit time, us':24s} {A['bit_us']:>28.4f} {B['bit_us']:>28.4f}")
print(f"{'-> baud':24s} {1e6/A['bit_us']:>28.1f} {1e6/B['bit_us']:>28.1f}")
print(f"{'high plateau, V':24s} {A['lvl_hi']:>28.3f} {B['lvl_hi']:>28.3f}")
print(f"{'low plateau, V':24s} {A['lvl_lo']:>28.3f} {B['lvl_lo']:>28.3f}")
print(f"{'abs min/max, V':24s} {A['vmin']:>14.2f}/{A['vmax']:>12.2f} {B['vmin']:>14.2f}/{B['vmax']:>12.2f}")
# worst run deviation from ideal 10us grid
da = max(abs(du - 10 * round(du / 10)) for _, du in A["runs"])
db = max(abs(du - 10 * round(du / 10)) for _, du in B["runs"])
print(f"{'worst grid dev, us':24s} {da:>28.2f} {db:>28.2f}")
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Decode a captured frame as SBUS: UART 100 kbit/s 8E2, 25-byte frame,
16 channels x 11 bits.
Usage: .venv/bin/python tools/decode.py captures/<name>.npz
"""
import sys
import numpy as np
BIT_US = 10.0
path = sys.argv[1]
d = np.load(path)
volts = d["volts"]
srate = float(d["srate"])
dt_us = 1e6 / srate
# idle level dominates the record; UART logic 1 == idle regardless of
# physical polarity (this line is standard inverted SBUS: idle low)
idle = np.median(volts)
p10, p90 = np.percentile(volts, [10, 90])
active = p10 if abs(p10 - idle) > abs(p90 - idle) else p90
thr = (idle + active) / 2
if active > idle:
sig = (volts < thr).astype(np.int8) # pulses up -> logic 0
else:
sig = (volts > thr).astype(np.int8)
edges = np.flatnonzero(np.diff(sig)) + 1
starts = np.concatenate(([0], edges))
ends = np.concatenate((edges, [len(sig)]))
levels = sig[starts]
dur_us = (ends - starts) * dt_us
stream = []
for lv, du in zip(levels[1:-1], dur_us[1:-1]):
stream.extend([int(lv)] * max(1, round(du / BIT_US)))
# trailing idle of last stop bits is trimmed by run cut; pad with idle-high
stream.extend([1] * 24)
# deframe 8E2: start=0, 8 data LSB-first, even parity, 2 stop=1
i = 0
frames = []
errors = []
while i + 12 <= len(stream):
if stream[i] == 1:
i += 1
continue
data = stream[i + 1 : i + 9]
par = stream[i + 9]
stops = stream[i + 10 : i + 12]
byte = sum(b << k for k, b in enumerate(data))
if par != (sum(data) & 1):
errors.append((len(frames), "parity"))
if stops != [1, 1]:
errors.append((len(frames), f"stop={stops}"))
frames.append(byte)
i += 12
print(f"decoded {len(frames)} bytes, errors: {errors if errors else 'none'}")
print("hex:", " ".join(f"{b:02X}" for b in frames))
if len(frames) >= 25 and frames[0] == 0x0F:
payload = frames[1:23]
flags = frames[23]
footer = frames[24]
bits = 0
for k, b in enumerate(payload):
bits |= b << (8 * k)
ch = [(bits >> (11 * n)) & 0x7FF for n in range(16)]
print("\nSBUS frame OK" if footer == 0x00 else f"\nfooter unexpected: {footer:02X}")
print("channels:", ch)
print(f"flags: 0x{flags:02X} (bit0=ch17 bit1=ch18 bit2=frame_lost bit3=failsafe)")
else:
print("not a valid SBUS frame (no 0x0F header)")
@@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""Зеркало бинарного протокола из src/link_protocol.h (единственный источник
истины на стороне Python). Кадрирование поверх потока, CRC16-CCITT, упаковка и
разбор CONTROL/IDENT/HELLO/LOG, инкрементальный парсер с ресинхронизацией.
При правке src/link_protocol.h меняй ЭТОТ файл и сверяйся:
python3 tools/link_proto.py --selftest
"""
import struct
import sys
SYNC0 = 0xA5
SYNC1 = 0x5A
VERSION = 1
MAX_PAYLOAD = 64
# типы кадров
HELLO = 0x03
IDENT = 0x02
CONTROL = 0x01
LOG = 0x04
# роли
ROLE_TX = ord("T")
ROLE_RX = ord("R")
# флаги CONTROL
CTRL_FLAG_FAILSAFE = 0x01
CONTROL_LEN = 4 + 4 + 1 + 2 * 16 # 41
IDENT_LEN = 1 + 1 + 2 + 12 # 16
SBUS_MID = 1024
# тайминги (мс), зеркало link_protocol.h
FRAME_PERIOD_MS = 50
RX_USB_TIMEOUT_MS = 400
def crc16(data: bytes) -> int:
"""CRC16-CCITT, poly 0x1021, init 0xFFFF — как linkCrc16 в прошивке."""
crc = 0xFFFF
for byte in data:
crc ^= byte << 8
for _ in range(8):
crc = ((crc << 1) ^ 0x1021) & 0xFFFF if (crc & 0x8000) else (crc << 1) & 0xFFFF
return crc
def build_frame(frame_type: int, payload: bytes) -> bytes:
"""Собрать кадр: SYNC + VER + TYPE + LEN + PAYLOAD + CRC16(LE)."""
if len(payload) > MAX_PAYLOAD:
raise ValueError("payload too long")
body = bytes((VERSION, frame_type, len(payload))) + payload
crc = crc16(body)
return bytes((SYNC0, SYNC1)) + body + struct.pack("<H", crc)
def pack_control(seq: int, t_ms: int, flags: int, ch) -> bytes:
"""Полезная нагрузка CONTROL. ch — 16 значений 0..2047."""
if len(ch) != 16:
raise ValueError("need 16 channels")
return struct.pack("<IIB16H", seq & 0xFFFFFFFF, t_ms & 0xFFFFFFFF, flags & 0xFF, *ch)
def unpack_control(payload: bytes):
"""-> dict(seq, t_ms, flags, ch[16])."""
if len(payload) != CONTROL_LEN:
raise ValueError("bad CONTROL length")
seq, t_ms, flags = struct.unpack_from("<IIB", payload, 0)
ch = list(struct.unpack_from("<16H", payload, 9))
return {"seq": seq, "t_ms": t_ms, "flags": flags, "ch": ch}
def unpack_ident(payload: bytes):
"""-> dict(role, proto_ver, fw_ver, uid)."""
if len(payload) != IDENT_LEN:
raise ValueError("bad IDENT length")
role, proto_ver, fw_ver = struct.unpack_from("<BBH", payload, 0)
uid = payload[4:16]
return {"role": role, "proto_ver": proto_ver, "fw_ver": fw_ver, "uid": uid}
def role_name(role: int) -> str:
return {ROLE_TX: "tx", ROLE_RX: "rx"}.get(role, "?")
def neutral_control(seq: int = 0, t_ms: int = 0, failsafe: bool = True) -> bytes:
"""Нейтральный CONTROL (все каналы 1024) — для сетевого failsafe на RX."""
flags = CTRL_FLAG_FAILSAFE if failsafe else 0
return pack_control(seq, t_ms, flags, [SBUS_MID] * 16)
class FrameParser:
"""Инкрементальный разбор потока. Скорми feed() любые байты — вернёт список
(type, payload) полных валидных кадров. Устойчив к мусору и потере байт:
при сбое CRC/версии сдвигается на байт и ищет следующий SYNC."""
def __init__(self):
self._buf = bytearray()
def feed(self, data: bytes):
self._buf.extend(data)
out = []
while True:
frame = self._try_one()
if frame is None:
break
out.append(frame)
return out
def _try_one(self):
buf = self._buf
while True:
# найти синхрослово
while len(buf) >= 2 and not (buf[0] == SYNC0 and buf[1] == SYNC1):
del buf[0]
if len(buf) < 5: # SYNC(2)+VER+TYPE+LEN
return None
ver, ftype, length = buf[2], buf[3], buf[4]
if ver != VERSION or length > MAX_PAYLOAD:
del buf[0] # ложный синк — сдвиг и поиск заново в этом же вызове
continue
total = 5 + length + 2
if len(buf) < total:
return None
body = bytes(buf[2:5 + length])
want = struct.unpack_from("<H", buf, 5 + length)[0]
if crc16(body) != want:
del buf[0] # битый кадр — ресинхронизация
continue
payload = bytes(buf[5:5 + length])
del buf[:total]
return (ftype, payload)
def _selftest():
# тест-вектор CONTROL: совпадает с packControl в прошивке
ch = [SBUS_MID] * 16
ch[1] = 1500
ch[3] = 600
payload = pack_control(0x01020304, 0x0A0B0C0D, CTRL_FLAG_FAILSAFE, ch)
assert len(payload) == CONTROL_LEN, len(payload)
back = unpack_control(payload)
assert back["seq"] == 0x01020304 and back["ch"][1] == 1500 and back["flags"] == 1, back
# кадрирование туда-обратно через парсер, c мусором спереди
frame = build_frame(CONTROL, payload)
p = FrameParser()
frames = p.feed(b"\x00\xffgarbage" + frame)
assert len(frames) == 1 and frames[0][0] == CONTROL, frames
assert frames[0][1] == payload
# битый CRC отбрасывается, следующий кадр ловится
bad = bytearray(frame)
bad[-1] ^= 0xFF
p2 = FrameParser()
assert p2.feed(bytes(bad)) == []
assert len(p2.feed(frame)) == 1
# битый + валидный в ОДНОМ feed() -> валидный извлекается сразу (ресинхрон
# не прерывает разбор остатка буфера)
p3 = FrameParser()
got = p3.feed(bytes(bad) + frame)
assert len(got) == 1 and got[0][1] == payload, got
# известный CRC-вектор для сверки с прошивкой
assert crc16(b"123456789") == 0x29B1, hex(crc16(b"123456789"))
print("link_proto selftest OK")
if __name__ == "__main__":
if "--selftest" in sys.argv:
_selftest()
else:
print(__doc__)
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Minimal SCPI helper for Hantek DPO7204C over /dev/usbtmc2."""
import os
import sys
import time
DEV = "/dev/usbtmc2"
class Scope:
def __init__(self, dev=DEV):
self.fd = os.open(dev, os.O_RDWR)
def write(self, cmd: str):
os.write(self.fd, cmd.encode() + b"\n")
def read(self, n=1 << 20, timeout=3.0) -> bytes:
# usbtmc read returns one transfer chunk; loop until short read
chunks = []
end = time.time() + timeout
while time.time() < end:
try:
data = os.read(self.fd, n)
except OSError:
break
chunks.append(data)
if not data or len(data) < n:
break
return b"".join(chunks)
def query(self, cmd: str, timeout=3.0) -> str:
self.write(cmd)
return self.read(timeout=timeout).decode(errors="replace").strip()
def query_raw(self, cmd: str, timeout=5.0) -> bytes:
self.write(cmd)
return self.read(timeout=timeout)
def close(self):
os.close(self.fd)
if __name__ == "__main__":
s = Scope()
for cmd in sys.argv[1:]:
if cmd.endswith("?"):
print(f"{cmd:40s} -> {s.query(cmd)}")
else:
s.write(cmd)
print(f"{cmd:40s} [sent]")
s.close()
+267
View File
@@ -0,0 +1,267 @@
# Бинарный протокол LibreVNA: что шлёт и парсит наш драйвер
Разбор USB-протокола LibreVNA и того, какое его подмножество использует
`python_app/hardware_full/librevna_multi_device_driver`. Про переключение
стимула внутри частотной точки — отдельный документ,
[per_point_switching.md](per_point_switching.md).
Прошивка в репозитории лежит под
`device_firmware/multi_libreVNA_firmware/reproduce/LibreVNA/Software/VNA_embedded/Application/`,
дальше он сокращён до `<FW>`. Версия протокола, заявленная прошивкой —
`Protocol::Version = 14` (`<FW>/Communication/Protocol.hpp:13`).
## 1. Транспорт
Поверх USB у LibreVNA свой бинарный протокол, а не tty/serial. Endpoints:
| Endpoint | Направление |
| --- | --- |
| `0x01` | хост -> устройство |
| `0x81` | устройство -> хост |
| `0x82` | отладочные ASCII-логи |
Наш драйвер шлёт в `0x01`, читает из `0x81`, `0x82` не использует. Приёмный
поток и очередь пакетов — в `librevna_multi_device_driver/transport.py`.
Очередь принятых пакетов ограничена и работает по принципу drop-oldest:
блокировать USB-поток чтения нельзя, а устаревшие пакеты всё равно
выбрасываются предсборочным дренажом (`transport.py:109`).
## 2. Структура кадра
Одна и та же почти для всех типов пакетов
(`<FW>/Communication/PacketConstants.h`):
| Поле | Длина | Значение |
| --- | --- | --- |
| Header | 1 | всегда `0x5A` |
| Length | 2 | длина **всего** кадра, little-endian |
| Type | 1 | номер типа пакета |
| Payload | переменная | |
| CRC32 | 4 | по всему кадру, включая header |
Итого накладных расходов 8 байт: `длина кадра = 8 + len(payload)`.
Кодирование и разбор на хосте — в общем модуле одиночного драйвера, а не в
multi-device: `python_app/hardware_full/librevna_driver/protocol/frame.py`,
функции `encode_frame` (строка 25) и `decode_frame` (строка 49). Поток байт
разбирает `FrameScanner`, который умеет ресинхронизироваться после битого кадра,
не роняя устройство.
### CRC: у `VNADatapoint` особый случай
Прошивка **не считает** CRC для `VNADatapoint` и пишет туда ноль, потому что
расчёт занимает около 18 мкс — это основная часть времени кодирования и
передачи точки (`<FW>/Communication/Protocol.cpp:149`). На приёме она,
наоборот, **требует** ровно нуля: непустой CRC у datapoint считается ошибкой
(`Protocol.cpp:69` и `Protocol.cpp:80`).
Наш хост повторяет это поведение точно: `decode_frame` проверяет CRC32 для всех
типов, а для `VNADatapoint` требует ноль (`frame.py:20` — список исключений).
Дополнительно multi-device парсер отбраковывает пакет, у которого тело не
кратно 9 байтам на значение: раз CRC у datapoint нет, это единственная защита
от ресинхронизации на `0x5A`, случайно оказавшийся внутри payload
(`librevna_multi_device_driver/protocol.py:137`).
## 3. Типы пакетов
Полный enum — `<FW>/Communication/Protocol.hpp:573`, там 30+ типов. Наш драйвер
использует шесть (`librevna_multi_device_driver/protocol.py:17`):
| Тип | Номер | Зачем |
| --- | --- | --- |
| `SweepSettings` | 2 | настройка развёртки, она же её запуск |
| `Ack` | 7 | подтверждение команды |
| `Nack` | 10 | отказ |
| `Reference` | 11 | выбор источника опорной частоты |
| `SetIdle` | 20 | остановить развёртку, перевести в idle |
| `VNADatapoint` | 27 | поток измерений |
То есть это узкий VNA-sweep-драйвер, а не полная реализация всех режимов
прибора. Не используются `RequestDeviceInfo`, проверка версии протокола,
`SetTrigger`/`ClearTrigger`, генератор, анализатор спектра, калибровочные
пакеты.
## 4. Пакеты, которые шлём
### `Reference` (11)
Структура — `Protocol.hpp:186`: `uint32 ExtRefOuputFreq` плюс битовое поле
`AutomaticSwitch` и `UseExternalRef`.
Шлём `output_frequency_hz = 0` и флаг внешней опоры: выход `Ref Out` не
включаем, а устройству принудительно указываем использовать внешние 10 МГц.
Строит `build_reference_settings_payload` (`protocol.py:48`), отправляет
`controller.py`.
### `SweepSettings` (2)
Структура — `Protocol.hpp:155`:
```c
uint64 f_start, f_stop;
uint16 points;
uint32 if_bandwidth;
int16 cdbm_excitation_start;
uint8 standby:1, syncMaster:1, suppressPeaks:1, fixedPowerSetting:1,
logSweep:1, syncMode:2, unused1:1;
uint16 stages:3, port1Stage:3, port2Stage:3, port3Stage:3, port4Stage:3, unused2:1;
int16 cdbm_excitation_stop;
uint16 dwell_time; // в микросекундах
```
Что кладём (`build_sweep_settings_payload`, `protocol.py:58`):
- `f_start`/`f_stop`/`points`/`if_bandwidth` — из `SweepConfiguration`;
- мощность старта и стопа одинаковая, `fixedPowerSetting = 1` — аттенюатор и
мощность PLL не меняются по развёртке;
- `suppressPeaks = 1`, `logSweep = 0`, `standby = 0`;
- `syncMode = 3` (триггерная синхронизация), если синхронизация включена;
- `syncMaster = 1` только у мастера;
- `stages` — номер последней стадии, не их количество;
- порт-стадии — см. ниже.
Про `syncMode = 3`: в стоковой прошивке рядом с полем стоит комментарий
«Trigger synchronization (not supported yet by hardware)». В нашем форке этот
режим и используется — устройство переводит обе триггерные линии FPGA в Hi-Z.
**Порт-стадии — не то, что было в первой версии драйвера.** Раньше мастер
получал `port1Stage = 0`, а слейв `1`. Сейчас:
- мастеру отправляется **первая стадия блока** порта (`index * substage_count`);
без под-стадий это просто индекс порта, то есть стоковая раскладка;
- приёмным устройствам обе порт-стадии выставляются в
`DISABLED_PORT_STAGE = 7` (`protocol.py:36`) — «порт выключен»;
- ширина блока под-стадий передаётся как `log2` в неиспользуемое поле
`port3Stage`.
Почему именно так и почему семёрка безопасна — в
[per_point_switching.md](per_point_switching.md).
### `SetIdle` (20)
Без payload. Выключает активный режим и останавливает развёртку. Используется
как служебная команда: остановить непрерывную развёртку, убраться при ошибке,
`close()`.
### `Ack` (7) / `Nack` (10)
Ответы устройства на команды хоста. `wait_for_acknowledgement`
(`transport.py:96`) читает входящие пакеты, на `Ack` считает команду
применённой, на `Nack` бросает исключение, прочие асинхронные пакеты
пропускает.
## 5. `VNADatapoint` (27) — поток измерений
Шаблон `VNADatapoint<s>` — `Protocol.hpp:25`; у прошивки `s = 32`, то есть до 32
значений в точке (три на стадию при восьми стадиях).
Payload:
| Поле | Тип | Длина |
| --- | --- | --- |
| `frequency` | uint64 | 8 |
| `cdBm` | int16 | 2 |
| `pointNum` | uint16 | 2 |
| `real_values` | n × float32 | 4n |
| `imag_values` | n × float32 | 4n |
| `descr_values` | n × uint8 | n |
Фиксированная часть — 12 байт, дальше по 9 байт на значение, причём **не
чередуясь**: сначала все реальные части, потом все мнимые, потом все
дескрипторы. Отсюда `n = (len(payload) - 12) / 9`.
Разбирает `parse_vna_datapoint_payload` (`protocol.py:137`).
### Байт дескриптора
Смещения битов — `<FW>/Communication/PacketConstants.h:42`:
| Биты | Значение |
| --- | --- |
| 0 | порт 1 |
| 1 | порт 2 |
| 2 | порт 3 |
| 3 | порт 4 |
| 4 | опорный приёмник (Reference) |
| 5..7 | номер стадии |
То же отражено в `enum class Source` (`Protocol.hpp:17`): `Port1 = 0x01`,
`Port2 = 0x02`, `Port3 = 0x04`, `Port4 = 0x08`, `Reference = 0x10`.
Прошивка собирает байт как `stage << 5 | sourceMask`.
На каждую стадию приходит ровно три значения (`<FW>/VNA.cpp:407`): порт 1, порт
2 и опорный приёмник. **Ловушка:** у опорного значения маска не `0x10`, а
`Port1 | Port2 | Reference = 0x13` — то есть биты портов у него тоже стоят.
Поэтому отбирать сигнальное значение только по биту порта нельзя, надо ещё
требовать сброшенный бит 4; именно так и сделано в `find_receiver_value`.
Ищет значение по этому байту `find_receiver_value` (`protocol.py:174`) —
фильтрует по стадии, по признаку опорного приёмника и, если задан, по порту.
Например: «стадия 0 + ref» — опорный приёмник мастера; «стадия 0 + порт 1, не
ref» — сигнал на первом приёмнике.
## 6. Как из этого получаются S-параметры
Устройство S-параметры **не считает** — оно отдаёт только сырые комплексные
отсчёты приёмников. Считает хост, в
`librevna_multi_device_driver/cycle_collection.py`.
Нормировка (`calculate_last_cycle_s_parameters`, строка 783): каждый канал
делится на опорный приёмник **мастера**, а не на локальный опорный приёмник
слейва — потому что излучает именно мастер. Опорные измерения хранятся по
номеру выходного канала (`master_reference_measurements_by_output`), так как с
переключением стимула выходов больше, чем портов мастера.
Имена S-параметров: отражение мастера — `master_reflection_s_parameter_name`
(строка 760), прямые каналы — `build_forward_s_parameter_names` (строка 765).
Нумерация приёмных портов слейвов начинается **после** всех выходов, чтобы
`S33`/`S44` отражения не столкнулись с прямыми каналами.
## 7. Почему поле мощности читается, но не используется
В парсере стоит:
```python
frequency_hz, _power_level_centidecibels_milliwatt, point_index = struct.unpack_from(
"<QhH", payload, 0
)
```
Поле мощности в payload реально существует и стоит между частотой и номером
точки, поэтому прочитать его необходимо, чтобы правильно достать `point_index`.
Подчёркивание в имени — обычный питоновский признак «прочитали намеренно, но не
используем».
По смыслу оно не нужно: для сбора циклов достаточно частоты, индекса точки и
массива отсчётов, а S-параметры считаются как отношение сигнала к опоре.
К тому же развёртка идёт с фиксированной мощностью (`cdbm_excitation_start ==
cdbm_excitation_stop`, `fixedPowerSetting = 1`), так что поле лишь дублирует
уже известное из настройки.
Понадобилось бы оно при переменной по частоте мощности, при логировании
фактической мощности каждой точки или при проверке, что устройство
действительно выдаёт ожидаемый профиль.
## 8. Чего в драйвере сознательно нет
- запроса `DeviceInfo` и проверки версии протокола устройства;
- отладочного endpoint `0x82`;
- пакетов калибровки, генератора, анализатора спектра;
- `SetTrigger`/`ClearTrigger` — синхронизация настраивается полем `syncMode` в
`SweepSettings`, а сама петля триггера аппаратная.
CRC входящих пакетов, в отличие от первой версии драйвера, **проверяется** —
см. раздел 2.
## 9. Карта модулей хоста
| Файл | Роль |
| --- | --- |
| `librevna_driver/protocol/frame.py` | кадр: кодирование, разбор, CRC, ресинхронизация |
| `librevna_multi_device_driver/protocol.py` | номера пакетов, сборка payload, парсер datapoint, поиск по дескриптору |
| `librevna_multi_device_driver/transport.py` | USB bulk, приёмный поток, очередь, ожидание `Ack` |
| `librevna_multi_device_driver/controller.py` | высокоуровневые команды: настроить развёртку, собрать циклы, остановить |
| `librevna_multi_device_driver/cycle_collection.py` | сбор точек в полные циклы, нормировка в S-параметры, имена трасс |
| `librevna_multi_device_driver/models.py` | `SweepConfiguration`, выходные каналы, ограничения стадий |
@@ -0,0 +1,15 @@
*.s#*
*.b#*
*.log
*.net
*.op.raw
*.raw
*.vnafw
*.aux
*.log
*.synctex.gz
*.xcf
*.out
*.toc
*.pyc
Hardware/Renderings
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
import argparse
import binascii
from pathlib import Path
DEFAULT_FPGA_BITSTREAM = "FPGA/VNA/top.bin"
DEFAULT_OUTPUT = "combined.vnafw"
DEFAULT_MCU_CANDIDATES = [
"Software/VNA_embedded/Debug/VNA_embedded.bin",
"Software/VNA_embedded/Release/VNA_embedded.bin",
"Software/VNA_embedded/build/VNA_embedded.bin",
]
HEADER_SIZE = 24
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Assemble a combined LibreVNA firmware image.",
)
parser.add_argument(
"--bitstream",
default=DEFAULT_FPGA_BITSTREAM,
help=f"FPGA bitstream to package (default: {DEFAULT_FPGA_BITSTREAM})",
)
parser.add_argument(
"--output",
default=DEFAULT_OUTPUT,
help=f"Output firmware image path (default: {DEFAULT_OUTPUT})",
)
parser.add_argument(
"--mcu",
help="Explicit MCU firmware .bin path. If omitted, the newest known build is used.",
)
return parser.parse_args()
def select_mcu_firmware(explicit_path: str | None) -> Path:
if explicit_path is not None:
path = Path(explicit_path)
if not path.is_file():
raise FileNotFoundError(f"MCU firmware not found: {path}")
return path
latest_modification = 0.0
newest_mcu: Path | None = None
for candidate in DEFAULT_MCU_CANDIDATES:
path = Path(candidate)
if not path.is_file():
continue
modified_time = path.stat().st_mtime
if modified_time >= latest_modification:
latest_modification = modified_time
newest_mcu = path
if newest_mcu is None:
raise FileNotFoundError("Couldn't find MCU firmware file")
return newest_mcu
def crc32_from_file(filename: Path, initial_crc: int) -> int:
buffer = filename.read_bytes()
return binascii.crc32(buffer, initial_crc) & 0xFFFFFFFF
def main() -> int:
args = parse_args()
bitstream_path = Path(args.bitstream)
if not bitstream_path.is_file():
raise FileNotFoundError(f"FPGA bitstream not found: {bitstream_path}")
mcu_firmware_path = select_mcu_firmware(args.mcu)
output_path = Path(args.output)
print(f"Using {mcu_firmware_path} as MCU firmware")
size_fpga = bitstream_path.stat().st_size
size_mcu = mcu_firmware_path.stat().st_size
print(f"Got FPGA bitstream of size {size_fpga}")
print(f"Got MCU firmware of size {size_mcu}")
crc = crc32_from_file(bitstream_path, 0xFFFFFFFF)
crc = crc32_from_file(mcu_firmware_path, crc)
print(f"Calculating CRC...:{hex(crc)}")
output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open("wb") as firmware_image:
firmware_image.write(b"VNA!")
firmware_image.write((HEADER_SIZE).to_bytes(4, byteorder="little"))
firmware_image.write(size_fpga.to_bytes(4, byteorder="little"))
firmware_image.write((HEADER_SIZE + size_fpga).to_bytes(4, byteorder="little"))
firmware_image.write(size_mcu.to_bytes(4, byteorder="little"))
firmware_image.write(crc.to_bytes(4, byteorder="little"))
if firmware_image.tell() != HEADER_SIZE:
raise RuntimeError(
"Incorrect header size "
f"(defined as {HEADER_SIZE} but actual header is {firmware_image.tell()})"
)
firmware_image.write(bitstream_path.read_bytes())
firmware_image.write(mcu_firmware_path.read_bytes())
if firmware_image.tell() % 256 != 0:
padding = 256 - firmware_image.tell() % 256
firmware_image.write(bytearray(padding))
final_size = firmware_image.tell()
print(f"Created combined firmware file at {output_path} with size {final_size}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,12 @@
*
!.gitignore
!*/
!*.vhd
!*.ucf
!*.ipf
!*.gise
!*.xise
!*.py
!*.bin
!VNA/*.dat
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import math
# Adapt these constants to the step attenuator
BITS_OF_ATTENUATION = 7
LSB_ATTENUATOR_DB = 0.25
NUMBER_LINEAR_ATTENUATION_STEPS = 128
file = open("AMdepth.dat", "w")
def bindigits(n, bits):
s = bin(n & int("1"*bits, 2))[2:]
return ("{0:0>%s}" % (bits)).format(s)
for i in range(NUMBER_LINEAR_ATTENUATION_STEPS):
# calculate percentage of attenuation
percent = float(i) / (NUMBER_LINEAR_ATTENUATION_STEPS - 1)
# calculate attenuation in dB
if percent < 1:
required_dB = -20*math.log10(1 - percent)
else:
required_dB = 999
# round to attenuator steps
attenuator_dB = round(required_dB / LSB_ATTENUATOR_DB) * LSB_ATTENUATOR_DB
# convert to digital attenuator value
attenuator_step = int(attenuator_dB / LSB_ATTENUATOR_DB)
# constrain
if attenuator_step < 0:
attenuator_step = 0
elif attenuator_step >= pow(2, BITS_OF_ATTENUATION):
attenuator_step = pow(2, BITS_OF_ATTENUATION) - 1
output = bindigits(attenuator_step, BITS_OF_ATTENUATION)
file.write(output+"\n")
# Calculate actual attenuation
actual_dB = attenuator_step * LSB_ATTENUATOR_DB
actual_percent = 1 - pow(10, -actual_dB/20)
percent_desired = round(percent*100, 2)
percent_actual = round(actual_percent*100, 2)
print(f'Target modulation: {percent_desired:.2f}, target dB: {required_dB:.2f}, attenuator setting: {attenuator_step}, achieved dB: {actual_dB}, achieved modulation: {percent_actual:.2f}')
file.close()
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<generated_project xmlns="http://www.xilinx.com/XMLSchema" xmlns:xil_pn="http://www.xilinx.com/XMLSchema">
<!-- -->
<!-- For tool use only. Do not edit. -->
<!-- -->
<!-- ProjectNavigator created generated project file. -->
<!-- For use in tracking generated file and other information -->
<!-- allowing preservation of process status. -->
<!-- -->
<!-- Copyright (c) 1995-2013 Xilinx, Inc. All rights reserved. -->
<version xmlns="http://www.xilinx.com/XMLSchema">11.1</version>
<sourceproject xmlns="http://www.xilinx.com/XMLSchema" xil_pn:fileType="FILE_XISE" xil_pn:name="AMMult.xise"/>
<files xmlns="http://www.xilinx.com/XMLSchema">
<file xil_pn:fileType="FILE_ASY" xil_pn:name="AMMult.asy" xil_pn:origination="imported"/>
<file xil_pn:fileType="FILE_SYMBOL" xil_pn:name="AMMult.sym" xil_pn:origination="imported"/>
<file xil_pn:fileType="FILE_VHO" xil_pn:name="AMMult.vho" xil_pn:origination="imported"/>
</files>
<transforms xmlns="http://www.xilinx.com/XMLSchema">
<transform xil_pn:end_ts="1654646662" xil_pn:name="TRAN_copyInitialToXSTAbstractSynthesis" xil_pn:start_ts="1654646662">
<status xil_pn:value="SuccessfullyRun"/>
<status xil_pn:value="ReadyToRun"/>
</transform>
<transform xil_pn:end_ts="1654895235" xil_pn:name="TRAN_schematicsToHdl" xil_pn:prop_ck="-4873039598653988690" xil_pn:start_ts="1654895235">
<status xil_pn:value="SuccessfullyRun"/>
<status xil_pn:value="ReadyToRun"/>
</transform>
<transform xil_pn:end_ts="1654895235" xil_pn:name="TRAN_regenerateCores" xil_pn:prop_ck="1388655879939993469" xil_pn:start_ts="1654895235">
<status xil_pn:value="SuccessfullyRun"/>
<status xil_pn:value="ReadyToRun"/>
</transform>
<transform xil_pn:end_ts="1654895235" xil_pn:name="TRAN_SubProjectAbstractToPreProxy" xil_pn:start_ts="1654895235">
<status xil_pn:value="SuccessfullyRun"/>
<status xil_pn:value="ReadyToRun"/>
</transform>
<transform xil_pn:end_ts="1654895235" xil_pn:name="TRAN_xawsTohdl" xil_pn:prop_ck="-8697384035118631440" xil_pn:start_ts="1654895235">
<status xil_pn:value="SuccessfullyRun"/>
<status xil_pn:value="ReadyToRun"/>
</transform>
</transforms>
</generated_project>
@@ -0,0 +1,102 @@
--------------------------------------------------------------------------------
-- This file is owned and controlled by Xilinx and must be used solely --
-- for design, simulation, implementation and creation of design files --
-- limited to Xilinx devices or technologies. Use with non-Xilinx --
-- devices or technologies is expressly prohibited and immediately --
-- terminates your license. --
-- --
-- XILINX IS PROVIDING THIS DESIGN, CODE, OR INFORMATION "AS IS" SOLELY --
-- FOR USE IN DEVELOPING PROGRAMS AND SOLUTIONS FOR XILINX DEVICES. BY --
-- PROVIDING THIS DESIGN, CODE, OR INFORMATION AS ONE POSSIBLE --
-- IMPLEMENTATION OF THIS FEATURE, APPLICATION OR STANDARD, XILINX IS --
-- MAKING NO REPRESENTATION THAT THIS IMPLEMENTATION IS FREE FROM ANY --
-- CLAIMS OF INFRINGEMENT, AND YOU ARE RESPONSIBLE FOR OBTAINING ANY --
-- RIGHTS YOU MAY REQUIRE FOR YOUR IMPLEMENTATION. XILINX EXPRESSLY --
-- DISCLAIMS ANY WARRANTY WHATSOEVER WITH RESPECT TO THE ADEQUACY OF THE --
-- IMPLEMENTATION, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OR --
-- REPRESENTATIONS THAT THIS IMPLEMENTATION IS FREE FROM CLAIMS OF --
-- INFRINGEMENT, IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A --
-- PARTICULAR PURPOSE. --
-- --
-- Xilinx products are not intended for use in life support appliances, --
-- devices, or systems. Use in such applications are expressly --
-- prohibited. --
-- --
-- (c) Copyright 1995-2022 Xilinx, Inc. --
-- All rights reserved. --
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- You must compile the wrapper file AMMult.vhd when simulating
-- the core, AMMult. When compiling the wrapper file, be sure to
-- reference the XilinxCoreLib VHDL simulation library. For detailed
-- instructions, please refer to the "CORE Generator Help".
-- The synthesis directives "translate_off/translate_on" specified
-- below are supported by Xilinx, Mentor Graphics and Synplicity
-- synthesis tools. Ensure they are correct for your synthesis tool(s).
LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
-- synthesis translate_off
LIBRARY XilinxCoreLib;
-- synthesis translate_on
ENTITY AMMult IS
PORT (
clk : IN STD_LOGIC;
a : IN STD_LOGIC_VECTOR(7 DOWNTO 0);
b : IN STD_LOGIC_VECTOR(6 DOWNTO 0);
ce : IN STD_LOGIC;
p : OUT STD_LOGIC_VECTOR(14 DOWNTO 0)
);
END AMMult;
ARCHITECTURE AMMult_a OF AMMult IS
-- synthesis translate_off
COMPONENT wrapped_AMMult
PORT (
clk : IN STD_LOGIC;
a : IN STD_LOGIC_VECTOR(7 DOWNTO 0);
b : IN STD_LOGIC_VECTOR(6 DOWNTO 0);
ce : IN STD_LOGIC;
p : OUT STD_LOGIC_VECTOR(14 DOWNTO 0)
);
END COMPONENT;
-- Configuration specification
FOR ALL : wrapped_AMMult USE ENTITY XilinxCoreLib.mult_gen_v11_2(behavioral)
GENERIC MAP (
c_a_type => 1,
c_a_width => 8,
c_b_type => 1,
c_b_value => "10000001",
c_b_width => 7,
c_ccm_imp => 0,
c_ce_overrides_sclr => 0,
c_has_ce => 1,
c_has_sclr => 0,
c_has_zero_detect => 0,
c_latency => 1,
c_model_type => 0,
c_mult_type => 1,
c_optimize_goal => 1,
c_out_high => 14,
c_out_low => 0,
c_round_output => 0,
c_round_pt => 0,
c_verbosity => 0,
c_xdevicefamily => "spartan6"
);
-- synthesis translate_on
BEGIN
-- synthesis translate_off
U0 : wrapped_AMMult
PORT MAP (
clk => clk,
a => a,
b => b,
ce => ce,
p => p
);
-- synthesis translate_on
END AMMult_a;
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<generated_project xmlns="http://www.xilinx.com/XMLSchema" xmlns:xil_pn="http://www.xilinx.com/XMLSchema">
<!-- -->
<!-- For tool use only. Do not edit. -->
<!-- -->
<!-- ProjectNavigator created generated project file. -->
<!-- For use in tracking generated file and other information -->
<!-- allowing preservation of process status. -->
<!-- -->
<!-- Copyright (c) 1995-2013 Xilinx, Inc. All rights reserved. -->
<version xmlns="http://www.xilinx.com/XMLSchema">11.1</version>
<sourceproject xmlns="http://www.xilinx.com/XMLSchema" xil_pn:fileType="FILE_XISE" xil_pn:name="DFT_CLK.xise"/>
<files xmlns="http://www.xilinx.com/XMLSchema">
<file xil_pn:fileType="FILE_ASY" xil_pn:name="DFT_CLK.asy" xil_pn:origination="imported"/>
<file xil_pn:fileType="FILE_VHO" xil_pn:name="DFT_CLK.vho" xil_pn:origination="imported"/>
</files>
<transforms xmlns="http://www.xilinx.com/XMLSchema"/>
</generated_project>
@@ -0,0 +1,59 @@
# file: DFT_CLK.ucf
#
# (c) Copyright 2008 - 2011 Xilinx, Inc. All rights reserved.
#
# This file contains confidential and proprietary information
# of Xilinx, Inc. and is protected under U.S. and
# international copyright and other intellectual property
# laws.
#
# DISCLAIMER
# This disclaimer is not a license and does not grant any
# rights to the materials distributed herewith. Except as
# otherwise provided in a valid license issued to you by
# Xilinx, and to the maximum extent permitted by applicable
# law: (1) THESE MATERIALS ARE MADE AVAILABLE "AS IS" AND
# WITH ALL FAULTS, AND XILINX HEREBY DISCLAIMS ALL WARRANTIES
# AND CONDITIONS, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
# BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-
# INFRINGEMENT, OR FITNESS FOR ANY PARTICULAR PURPOSE; and
# (2) Xilinx shall not be liable (whether in contract or tort,
# including negligence, or under any other theory of
# liability) for any loss or damage of any kind or nature
# related to, arising under or in connection with these
# materials, including for any direct, or any indirect,
# special, incidental, or consequential loss or damage
# (including loss of data, profits, goodwill, or any type of
# loss or damage suffered as a result of any action brought
# by a third party) even if such damage or loss was
# reasonably foreseeable or Xilinx had been advised of the
# possibility of the same.
#
# CRITICAL APPLICATIONS
# Xilinx products are not designed or intended to be fail-
# safe, or for use in any application requiring fail-safe
# performance, such as life-support or safety devices or
# systems, Class III medical devices, nuclear facilities,
# applications related to the deployment of airbags, or any
# other applications that could lead to death, personal
# injury, or severe property or environmental damage
# (individually and collectively, "Critical
# Applications"). Customer assumes the sole risk and
# liability of any use of Xilinx products in Critical
# Applications, subject only to applicable laws and
# regulations governing limitations on product liability.
#
# THIS COPYRIGHT NOTICE AND DISCLAIMER MUST BE RETAINED AS
# PART OF THIS FILE AT ALL TIMES.
#
# Input clock periods. These duplicate the values entered for the
# input clocks. You can use these to time your system
#----------------------------------------------------------------
NET "CLK_IN1" TNM_NET = "CLK_IN1";
TIMESPEC "TS_CLK_IN1" = PERIOD "CLK_IN1" 62.5 ns HIGH 50% INPUT_JITTER 625.0ps;
# FALSE PATH constraints
PIN "RESET" TIG;
@@ -0,0 +1,169 @@
-- file: DFT_CLK.vhd
--
-- (c) Copyright 2008 - 2011 Xilinx, Inc. All rights reserved.
--
-- This file contains confidential and proprietary information
-- of Xilinx, Inc. and is protected under U.S. and
-- international copyright and other intellectual property
-- laws.
--
-- DISCLAIMER
-- This disclaimer is not a license and does not grant any
-- rights to the materials distributed herewith. Except as
-- otherwise provided in a valid license issued to you by
-- Xilinx, and to the maximum extent permitted by applicable
-- law: (1) THESE MATERIALS ARE MADE AVAILABLE "AS IS" AND
-- WITH ALL FAULTS, AND XILINX HEREBY DISCLAIMS ALL WARRANTIES
-- AND CONDITIONS, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
-- BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-
-- INFRINGEMENT, OR FITNESS FOR ANY PARTICULAR PURPOSE; and
-- (2) Xilinx shall not be liable (whether in contract or tort,
-- including negligence, or under any other theory of
-- liability) for any loss or damage of any kind or nature
-- related to, arising under or in connection with these
-- materials, including for any direct, or any indirect,
-- special, incidental, or consequential loss or damage
-- (including loss of data, profits, goodwill, or any type of
-- loss or damage suffered as a result of any action brought
-- by a third party) even if such damage or loss was
-- reasonably foreseeable or Xilinx had been advised of the
-- possibility of the same.
--
-- CRITICAL APPLICATIONS
-- Xilinx products are not designed or intended to be fail-
-- safe, or for use in any application requiring fail-safe
-- performance, such as life-support or safety devices or
-- systems, Class III medical devices, nuclear facilities,
-- applications related to the deployment of airbags, or any
-- other applications that could lead to death, personal
-- injury, or severe property or environmental damage
-- (individually and collectively, "Critical
-- Applications"). Customer assumes the sole risk and
-- liability of any use of Xilinx products in Critical
-- Applications, subject only to applicable laws and
-- regulations governing limitations on product liability.
--
-- THIS COPYRIGHT NOTICE AND DISCLAIMER MUST BE RETAINED AS
-- PART OF THIS FILE AT ALL TIMES.
--
------------------------------------------------------------------------------
-- User entered comments
------------------------------------------------------------------------------
-- None
--
------------------------------------------------------------------------------
-- "Output Output Phase Duty Pk-to-Pk Phase"
-- "Clock Freq (MHz) (degrees) Cycle (%) Jitter (ps) Error (ps)"
------------------------------------------------------------------------------
-- CLK_OUT1___160.000______0.000______50.0______336.927____150.000
--
------------------------------------------------------------------------------
-- "Input Clock Freq (MHz) Input Jitter (UI)"
------------------------------------------------------------------------------
-- __primary______________16____________0.010
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;
use ieee.std_logic_arith.all;
use ieee.numeric_std.all;
library unisim;
use unisim.vcomponents.all;
entity DFT_CLK is
port
(-- Clock in ports
CLK_IN1 : in std_logic;
-- Clock out ports
CLK_OUT1 : out std_logic;
-- Status and control signals
RESET : in std_logic;
LOCKED : out std_logic
);
end DFT_CLK;
architecture xilinx of DFT_CLK is
attribute CORE_GENERATION_INFO : string;
attribute CORE_GENERATION_INFO of xilinx : architecture is "DFT_CLK,clk_wiz_v3_6,{component_name=DFT_CLK,use_phase_alignment=true,use_min_o_jitter=false,use_max_i_jitter=false,use_dyn_phase_shift=false,use_inclk_switchover=false,use_dyn_reconfig=false,feedback_source=FDBK_AUTO,primtype_sel=DCM_SP,num_out_clk=1,clkin1_period=62.5,clkin2_period=62.5,use_power_down=false,use_reset=true,use_locked=true,use_inclk_stopped=false,use_status=false,use_freeze=false,use_clk_valid=false,feedback_type=SINGLE,clock_mgr_type=AUTO,manual_override=false}";
-- Input clock buffering / unused connectors
signal clkin1 : std_logic;
-- Output clock buffering
signal clkfb : std_logic;
signal clk0 : std_logic;
signal clkfx : std_logic;
signal clkfbout : std_logic;
signal locked_internal : std_logic;
signal status_internal : std_logic_vector(7 downto 0);
begin
-- Input buffering
--------------------------------------
clkin1 <= CLK_IN1;
-- Clocking primitive
--------------------------------------
-- Instantiation of the DCM primitive
-- * Unused inputs are tied off
-- * Unused outputs are labeled unused
dcm_sp_inst: DCM_SP
generic map
(CLKDV_DIVIDE => 2.000,
CLKFX_DIVIDE => 1,
CLKFX_MULTIPLY => 10,
CLKIN_DIVIDE_BY_2 => FALSE,
CLKIN_PERIOD => 62.5,
CLKOUT_PHASE_SHIFT => "NONE",
CLK_FEEDBACK => "1X",
DESKEW_ADJUST => "SYSTEM_SYNCHRONOUS",
PHASE_SHIFT => 0,
STARTUP_WAIT => FALSE)
port map
-- Input clock
(CLKIN => clkin1,
CLKFB => clkfb,
-- Output clocks
CLK0 => clk0,
CLK90 => open,
CLK180 => open,
CLK270 => open,
CLK2X => open,
CLK2X180 => open,
CLKFX => clkfx,
CLKFX180 => open,
CLKDV => open,
-- Ports for dynamic phase shift
PSCLK => '0',
PSEN => '0',
PSINCDEC => '0',
PSDONE => open,
-- Other control and status signals
LOCKED => locked_internal,
STATUS => status_internal,
RST => RESET,
-- Unused pin, tie low
DSSEN => '0');
LOCKED <= locked_internal;
-- Output buffering
-------------------------------------
clkf_buf : BUFG
port map
(O => clkfb,
I => clk0);
clkout1_buf : BUFG
port map
(O => CLK_OUT1,
I => clkfx);
end xilinx;
@@ -0,0 +1,60 @@
# file: DFT_CLK_exdes.ucf
#
# (c) Copyright 2008 - 2011 Xilinx, Inc. All rights reserved.
#
# This file contains confidential and proprietary information
# of Xilinx, Inc. and is protected under U.S. and
# international copyright and other intellectual property
# laws.
#
# DISCLAIMER
# This disclaimer is not a license and does not grant any
# rights to the materials distributed herewith. Except as
# otherwise provided in a valid license issued to you by
# Xilinx, and to the maximum extent permitted by applicable
# law: (1) THESE MATERIALS ARE MADE AVAILABLE "AS IS" AND
# WITH ALL FAULTS, AND XILINX HEREBY DISCLAIMS ALL WARRANTIES
# AND CONDITIONS, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
# BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-
# INFRINGEMENT, OR FITNESS FOR ANY PARTICULAR PURPOSE; and
# (2) Xilinx shall not be liable (whether in contract or tort,
# including negligence, or under any other theory of
# liability) for any loss or damage of any kind or nature
# related to, arising under or in connection with these
# materials, including for any direct, or any indirect,
# special, incidental, or consequential loss or damage
# (including loss of data, profits, goodwill, or any type of
# loss or damage suffered as a result of any action brought
# by a third party) even if such damage or loss was
# reasonably foreseeable or Xilinx had been advised of the
# possibility of the same.
#
# CRITICAL APPLICATIONS
# Xilinx products are not designed or intended to be fail-
# safe, or for use in any application requiring fail-safe
# performance, such as life-support or safety devices or
# systems, Class III medical devices, nuclear facilities,
# applications related to the deployment of airbags, or any
# other applications that could lead to death, personal
# injury, or severe property or environmental damage
# (individually and collectively, "Critical
# Applications"). Customer assumes the sole risk and
# liability of any use of Xilinx products in Critical
# Applications, subject only to applicable laws and
# regulations governing limitations on product liability.
#
# THIS COPYRIGHT NOTICE AND DISCLAIMER MUST BE RETAINED AS
# PART OF THIS FILE AT ALL TIMES.
#
# Input clock periods. These duplicate the values entered for the
# input clocks. You can use these to time your system
#----------------------------------------------------------------
NET "CLK_IN1" TNM_NET = "CLK_IN1";
TIMESPEC "TS_CLK_IN1" = PERIOD "CLK_IN1" 62.5 ns HIGH 50% INPUT_JITTER 625.0ps;
# FALSE PATH constraints
PIN "COUNTER_RESET" TIG;
PIN "RESET" TIG;
@@ -0,0 +1,192 @@
-- file: DFT_CLK_exdes.vhd
--
-- (c) Copyright 2008 - 2011 Xilinx, Inc. All rights reserved.
--
-- This file contains confidential and proprietary information
-- of Xilinx, Inc. and is protected under U.S. and
-- international copyright and other intellectual property
-- laws.
--
-- DISCLAIMER
-- This disclaimer is not a license and does not grant any
-- rights to the materials distributed herewith. Except as
-- otherwise provided in a valid license issued to you by
-- Xilinx, and to the maximum extent permitted by applicable
-- law: (1) THESE MATERIALS ARE MADE AVAILABLE "AS IS" AND
-- WITH ALL FAULTS, AND XILINX HEREBY DISCLAIMS ALL WARRANTIES
-- AND CONDITIONS, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
-- BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-
-- INFRINGEMENT, OR FITNESS FOR ANY PARTICULAR PURPOSE; and
-- (2) Xilinx shall not be liable (whether in contract or tort,
-- including negligence, or under any other theory of
-- liability) for any loss or damage of any kind or nature
-- related to, arising under or in connection with these
-- materials, including for any direct, or any indirect,
-- special, incidental, or consequential loss or damage
-- (including loss of data, profits, goodwill, or any type of
-- loss or damage suffered as a result of any action brought
-- by a third party) even if such damage or loss was
-- reasonably foreseeable or Xilinx had been advised of the
-- possibility of the same.
--
-- CRITICAL APPLICATIONS
-- Xilinx products are not designed or intended to be fail-
-- safe, or for use in any application requiring fail-safe
-- performance, such as life-support or safety devices or
-- systems, Class III medical devices, nuclear facilities,
-- applications related to the deployment of airbags, or any
-- other applications that could lead to death, personal
-- injury, or severe property or environmental damage
-- (individually and collectively, "Critical
-- Applications"). Customer assumes the sole risk and
-- liability of any use of Xilinx products in Critical
-- Applications, subject only to applicable laws and
-- regulations governing limitations on product liability.
--
-- THIS COPYRIGHT NOTICE AND DISCLAIMER MUST BE RETAINED AS
-- PART OF THIS FILE AT ALL TIMES.
--
------------------------------------------------------------------------------
-- Clocking wizard example design
------------------------------------------------------------------------------
-- This example design instantiates the created clocking network, where each
-- output clock drives a counter. The high bit of each counter is ported.
------------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;
use ieee.std_logic_arith.all;
use ieee.numeric_std.all;
library unisim;
use unisim.vcomponents.all;
entity DFT_CLK_exdes is
generic (
TCQ : in time := 100 ps);
port
(-- Clock in ports
CLK_IN1 : in std_logic;
-- Reset that only drives logic in example design
COUNTER_RESET : in std_logic;
CLK_OUT : out std_logic_vector(1 downto 1) ;
-- High bits of counters driven by clocks
COUNT : out std_logic;
-- Status and control signals
RESET : in std_logic;
LOCKED : out std_logic
);
end DFT_CLK_exdes;
architecture xilinx of DFT_CLK_exdes is
-- Parameters for the counters
---------------------------------
-- Counter width
constant C_W : integer := 16;
-- When the clock goes out of lock, reset the counters
signal locked_int : std_logic;
signal reset_int : std_logic := '0';
-- Declare the clocks and counter
signal clk : std_logic;
signal clk_int : std_logic;
signal clk_n : std_logic;
signal counter : std_logic_vector(C_W-1 downto 0) := (others => '0');
-- Need to buffer input clocks that aren't already buffered
signal clk_in1_buf : std_logic;
signal rst_sync : std_logic;
signal rst_sync_int : std_logic;
signal rst_sync_int1 : std_logic;
signal rst_sync_int2 : std_logic;
component DFT_CLK is
port
(-- Clock in ports
CLK_IN1 : in std_logic;
-- Clock out ports
CLK_OUT1 : out std_logic;
-- Status and control signals
RESET : in std_logic;
LOCKED : out std_logic
);
end component;
begin
-- Alias output to internally used signal
LOCKED <= locked_int;
-- When the clock goes out of lock, reset the counters
reset_int <= (not locked_int) or RESET or COUNTER_RESET;
process (clk, reset_int) begin
if (reset_int = '1') then
rst_sync <= '1';
rst_sync_int <= '1';
rst_sync_int1 <= '1';
rst_sync_int2 <= '1';
elsif (clk 'event and clk='1') then
rst_sync <= '0';
rst_sync_int <= rst_sync;
rst_sync_int1 <= rst_sync_int;
rst_sync_int2 <= rst_sync_int1;
end if;
end process;
-- Insert BUFGs on all input clocks that don't already have them
----------------------------------------------------------------
clkin1_buf : BUFG
port map
(O => clk_in1_buf,
I => CLK_IN1);
-- Instantiation of the clocking network
----------------------------------------
clknetwork : DFT_CLK
port map
(-- Clock in ports
CLK_IN1 => clk_in1_buf,
-- Clock out ports
CLK_OUT1 => clk_int,
-- Status and control signals
RESET => RESET,
LOCKED => locked_int);
clk_n <= not clk;
clkout_oddr : ODDR2
port map
(Q => CLK_OUT(1),
C0 => clk,
C1 => clk_n,
CE => '1',
D0 => '1',
D1 => '0',
R => '0',
S => '0');
-- Connect the output clocks to the design
-------------------------------------------
clk <= clk_int;
-- Output clock sampling
-------------------------------------
process (clk, rst_sync_int2) begin
if (rst_sync_int2 = '1') then
counter <= (others => '0') after TCQ;
elsif (rising_edge(clk)) then
counter <= counter + 1 after TCQ;
end if;
end process;
-- alias the high bit to the output
COUNT <= counter(C_W-1);
end xilinx;
@@ -0,0 +1,197 @@
-- file: DFT_CLK_tb.vhd
--
-- (c) Copyright 2008 - 2011 Xilinx, Inc. All rights reserved.
--
-- This file contains confidential and proprietary information
-- of Xilinx, Inc. and is protected under U.S. and
-- international copyright and other intellectual property
-- laws.
--
-- DISCLAIMER
-- This disclaimer is not a license and does not grant any
-- rights to the materials distributed herewith. Except as
-- otherwise provided in a valid license issued to you by
-- Xilinx, and to the maximum extent permitted by applicable
-- law: (1) THESE MATERIALS ARE MADE AVAILABLE "AS IS" AND
-- WITH ALL FAULTS, AND XILINX HEREBY DISCLAIMS ALL WARRANTIES
-- AND CONDITIONS, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
-- BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-
-- INFRINGEMENT, OR FITNESS FOR ANY PARTICULAR PURPOSE; and
-- (2) Xilinx shall not be liable (whether in contract or tort,
-- including negligence, or under any other theory of
-- liability) for any loss or damage of any kind or nature
-- related to, arising under or in connection with these
-- materials, including for any direct, or any indirect,
-- special, incidental, or consequential loss or damage
-- (including loss of data, profits, goodwill, or any type of
-- loss or damage suffered as a result of any action brought
-- by a third party) even if such damage or loss was
-- reasonably foreseeable or Xilinx had been advised of the
-- possibility of the same.
--
-- CRITICAL APPLICATIONS
-- Xilinx products are not designed or intended to be fail-
-- safe, or for use in any application requiring fail-safe
-- performance, such as life-support or safety devices or
-- systems, Class III medical devices, nuclear facilities,
-- applications related to the deployment of airbags, or any
-- other applications that could lead to death, personal
-- injury, or severe property or environmental damage
-- (individually and collectively, "Critical
-- Applications"). Customer assumes the sole risk and
-- liability of any use of Xilinx products in Critical
-- Applications, subject only to applicable laws and
-- regulations governing limitations on product liability.
--
-- THIS COPYRIGHT NOTICE AND DISCLAIMER MUST BE RETAINED AS
-- PART OF THIS FILE AT ALL TIMES.
--
------------------------------------------------------------------------------
-- Clocking wizard demonstration testbench
------------------------------------------------------------------------------
-- This demonstration testbench instantiates the example design for the
-- clocking wizard. Input clocks are toggled, which cause the clocking
-- network to lock and the counters to increment.
------------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;
use ieee.std_logic_arith.all;
use ieee.numeric_std.all;
use ieee.std_logic_textio.all;
library std;
use std.textio.all;
library work;
use work.all;
entity DFT_CLK_tb is
end DFT_CLK_tb;
architecture test of DFT_CLK_tb is
-- Clock to Q delay of 100 ps
constant TCQ : time := 100 ps;
-- timescale is 1ps
constant ONE_NS : time := 1 ns;
-- how many cycles to run
constant COUNT_PHASE : integer := 1024 + 1;
-- we'll be using the period in many locations
constant PER1 : time := 62.5 ns;
-- Declare the input clock signals
signal CLK_IN1 : std_logic := '1';
-- The high bit of the sampling counter
signal COUNT : std_logic;
-- Status and control signals
signal RESET : std_logic := '0';
signal LOCKED : std_logic;
signal COUNTER_RESET : std_logic := '0';
-- signal defined to stop mti simulation without severity failure in the report
signal end_of_sim : std_logic := '0';
signal CLK_OUT : std_logic_vector(1 downto 1);
--Freq Check using the M & D values setting and actual Frequency generated
component DFT_CLK_exdes
generic (
TCQ : in time := 100 ps);
port
(-- Clock in ports
CLK_IN1 : in std_logic;
-- Reset that only drives logic in example design
COUNTER_RESET : in std_logic;
CLK_OUT : out std_logic_vector(1 downto 1) ;
-- High bits of counters driven by clocks
COUNT : out std_logic;
-- Status and control signals
RESET : in std_logic;
LOCKED : out std_logic
);
end component;
begin
-- Input clock generation
--------------------------------------
process begin
CLK_IN1 <= not CLK_IN1; wait for (PER1/2);
end process;
-- Test sequence
process
procedure simtimeprint is
variable outline : line;
begin
write(outline, string'("## SYSTEM_CYCLE_COUNTER "));
write(outline, NOW/PER1);
write(outline, string'(" ns"));
writeline(output,outline);
end simtimeprint;
procedure simfreqprint (period : time; clk_num : integer) is
variable outputline : LINE;
variable str1 : string(1 to 16);
variable str2 : integer;
variable str3 : string(1 to 2);
variable str4 : integer;
variable str5 : string(1 to 4);
begin
str1 := "Freq of CLK_OUT(";
str2 := clk_num;
str3 := ") ";
str4 := 1000000 ps/period ;
str5 := " MHz" ;
write(outputline, str1 );
write(outputline, str2);
write(outputline, str3);
write(outputline, str4);
write(outputline, str5);
writeline(output, outputline);
end simfreqprint;
begin
RESET <= '1';
wait for (PER1*6);
RESET <= '0';
wait until LOCKED = '1';
COUNTER_RESET <= '1';
wait for (PER1*20);
COUNTER_RESET <= '0';
wait for (PER1*COUNT_PHASE);
simtimeprint;
end_of_sim <= '1';
wait for 1 ps;
report "Simulation Stopped." severity failure;
wait;
end process;
-- Instantiation of the example design containing the clock
-- network and sampling counters
-----------------------------------------------------------
dut : DFT_CLK_exdes
generic map (
TCQ => TCQ)
port map
(-- Clock in ports
CLK_IN1 => CLK_IN1,
-- Reset for logic in example design
COUNTER_RESET => COUNTER_RESET,
CLK_OUT => CLK_OUT,
-- High bits of the counters
COUNT => COUNT,
-- Status and control signals
RESET => RESET,
LOCKED => LOCKED);
-- Freq Check
end test;
@@ -0,0 +1,220 @@
-- file: DFT_CLK_tb.vhd
--
-- (c) Copyright 2008 - 2011 Xilinx, Inc. All rights reserved.
--
-- This file contains confidential and proprietary information
-- of Xilinx, Inc. and is protected under U.S. and
-- international copyright and other intellectual property
-- laws.
--
-- DISCLAIMER
-- This disclaimer is not a license and does not grant any
-- rights to the materials distributed herewith. Except as
-- otherwise provided in a valid license issued to you by
-- Xilinx, and to the maximum extent permitted by applicable
-- law: (1) THESE MATERIALS ARE MADE AVAILABLE "AS IS" AND
-- WITH ALL FAULTS, AND XILINX HEREBY DISCLAIMS ALL WARRANTIES
-- AND CONDITIONS, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
-- BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-
-- INFRINGEMENT, OR FITNESS FOR ANY PARTICULAR PURPOSE; and
-- (2) Xilinx shall not be liable (whether in contract or tort,
-- including negligence, or under any other theory of
-- liability) for any loss or damage of any kind or nature
-- related to, arising under or in connection with these
-- materials, including for any direct, or any indirect,
-- special, incidental, or consequential loss or damage
-- (including loss of data, profits, goodwill, or any type of
-- loss or damage suffered as a result of any action brought
-- by a third party) even if such damage or loss was
-- reasonably foreseeable or Xilinx had been advised of the
-- possibility of the same.
--
-- CRITICAL APPLICATIONS
-- Xilinx products are not designed or intended to be fail-
-- safe, or for use in any application requiring fail-safe
-- performance, such as life-support or safety devices or
-- systems, Class III medical devices, nuclear facilities,
-- applications related to the deployment of airbags, or any
-- other applications that could lead to death, personal
-- injury, or severe property or environmental damage
-- (individually and collectively, "Critical
-- Applications"). Customer assumes the sole risk and
-- liability of any use of Xilinx products in Critical
-- Applications, subject only to applicable laws and
-- regulations governing limitations on product liability.
--
-- THIS COPYRIGHT NOTICE AND DISCLAIMER MUST BE RETAINED AS
-- PART OF THIS FILE AT ALL TIMES.
--
------------------------------------------------------------------------------
-- Clocking wizard demonstration testbench
------------------------------------------------------------------------------
-- This demonstration testbench instantiates the example design for the
-- clocking wizard. Input clocks are toggled, which cause the clocking
-- network to lock and the counters to increment.
------------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;
use ieee.std_logic_arith.all;
use ieee.numeric_std.all;
use ieee.std_logic_textio.all;
library std;
use std.textio.all;
library work;
use work.all;
entity DFT_CLK_tb is
end DFT_CLK_tb;
architecture test of DFT_CLK_tb is
-- Clock to Q delay of 100 ps
constant TCQ : time := 100 ps;
-- timescale is 1ps
constant ONE_NS : time := 1 ns;
-- how many cycles to run
constant COUNT_PHASE : integer := 1024 + 1;
-- we'll be using the period in many locations
constant PER1 : time := 62.5 ns;
-- Declare the input clock signals
signal CLK_IN1 : std_logic := '1';
-- The high bit of the sampling counter
signal COUNT : std_logic;
-- Status and control signals
signal RESET : std_logic := '0';
signal LOCKED : std_logic;
signal COUNTER_RESET : std_logic := '0';
signal timeout_counter : std_logic_vector (13 downto 0) := (others => '0');
-- signal defined to stop mti simulation without severity failure in the report
signal end_of_sim : std_logic := '0';
signal CLK_OUT : std_logic_vector(1 downto 1);
--Freq Check using the M & D values setting and actual Frequency generated
component DFT_CLK_exdes
port
(-- Clock in ports
CLK_IN1 : in std_logic;
-- Reset that only drives logic in example design
COUNTER_RESET : in std_logic;
CLK_OUT : out std_logic_vector(1 downto 1) ;
-- High bits of counters driven by clocks
COUNT : out std_logic;
-- Status and control signals
RESET : in std_logic;
LOCKED : out std_logic
);
end component;
begin
-- Input clock generation
--------------------------------------
process begin
CLK_IN1 <= not CLK_IN1; wait for (PER1/2);
end process;
-- Test sequence
process
procedure simtimeprint is
variable outline : line;
begin
write(outline, string'("## SYSTEM_CYCLE_COUNTER "));
write(outline, NOW/PER1);
write(outline, string'(" ns"));
writeline(output,outline);
end simtimeprint;
procedure simfreqprint (period : time; clk_num : integer) is
variable outputline : LINE;
variable str1 : string(1 to 16);
variable str2 : integer;
variable str3 : string(1 to 2);
variable str4 : integer;
variable str5 : string(1 to 4);
begin
str1 := "Freq of CLK_OUT(";
str2 := clk_num;
str3 := ") ";
str4 := 1000000 ps/period ;
str5 := " MHz" ;
write(outputline, str1 );
write(outputline, str2);
write(outputline, str3);
write(outputline, str4);
write(outputline, str5);
writeline(output, outputline);
end simfreqprint;
begin
report "Timing checks are not valid" severity note;
RESET <= '1';
wait for (PER1*6);
RESET <= '0';
wait until LOCKED = '1';
wait for (PER1*20);
COUNTER_RESET <= '1';
wait for (PER1*19.5);
COUNTER_RESET <= '0';
wait for (PER1*1);
report "Timing checks are valid" severity note;
wait for (PER1*COUNT_PHASE);
simtimeprint;
end_of_sim <= '1';
wait for 1 ps;
report "Simulation Stopped." severity failure;
wait;
end process;
process (CLK_IN1)
procedure simtimeprint is
variable outline : line;
begin
write(outline, string'("## SYSTEM_CYCLE_COUNTER "));
write(outline, NOW/PER1);
write(outline, string'(" ns"));
writeline(output,outline);
end simtimeprint;
begin
if (CLK_IN1'event and CLK_IN1='1') then
timeout_counter <= timeout_counter + '1';
if (timeout_counter = "10000000000000") then
if (LOCKED /= '1') then
simtimeprint;
report "NO LOCK signal" severity failure;
end if;
end if;
end if;
end process;
-- Instantiation of the example design containing the clock
-- network and sampling counters
-----------------------------------------------------------
dut : DFT_CLK_exdes
port map
(-- Clock in ports
CLK_IN1 => CLK_IN1,
-- Reset for logic in example design
COUNTER_RESET => COUNTER_RESET,
CLK_OUT => CLK_OUT,
-- High bits of the counters
COUNT => COUNT,
-- Status and control signals
RESET => RESET,
LOCKED => LOCKED);
-- Freq Check
end test;
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<generated_project xmlns="http://www.xilinx.com/XMLSchema" xmlns:xil_pn="http://www.xilinx.com/XMLSchema">
<!-- -->
<!-- For tool use only. Do not edit. -->
<!-- -->
<!-- ProjectNavigator created generated project file. -->
<!-- For use in tracking generated file and other information -->
<!-- allowing preservation of process status. -->
<!-- -->
<!-- Copyright (c) 1995-2013 Xilinx, Inc. All rights reserved. -->
<version xmlns="http://www.xilinx.com/XMLSchema">11.1</version>
<sourceproject xmlns="http://www.xilinx.com/XMLSchema" xil_pn:fileType="FILE_XISE" xil_pn:name="DSP48.xise"/>
<files xmlns="http://www.xilinx.com/XMLSchema">
<file xil_pn:fileType="FILE_ASY" xil_pn:name="DSP48.asy" xil_pn:origination="imported"/>
<file xil_pn:fileType="FILE_SYMBOL" xil_pn:name="DSP48.sym" xil_pn:origination="imported"/>
<file xil_pn:fileType="FILE_VHO" xil_pn:name="DSP48.vho" xil_pn:origination="imported"/>
</files>
<transforms xmlns="http://www.xilinx.com/XMLSchema"/>
</generated_project>
@@ -0,0 +1,139 @@
--------------------------------------------------------------------------------
-- This file is owned and controlled by Xilinx and must be used solely --
-- for design, simulation, implementation and creation of design files --
-- limited to Xilinx devices or technologies. Use with non-Xilinx --
-- devices or technologies is expressly prohibited and immediately --
-- terminates your license. --
-- --
-- XILINX IS PROVIDING THIS DESIGN, CODE, OR INFORMATION "AS IS" SOLELY --
-- FOR USE IN DEVELOPING PROGRAMS AND SOLUTIONS FOR XILINX DEVICES. BY --
-- PROVIDING THIS DESIGN, CODE, OR INFORMATION AS ONE POSSIBLE --
-- IMPLEMENTATION OF THIS FEATURE, APPLICATION OR STANDARD, XILINX IS --
-- MAKING NO REPRESENTATION THAT THIS IMPLEMENTATION IS FREE FROM ANY --
-- CLAIMS OF INFRINGEMENT, AND YOU ARE RESPONSIBLE FOR OBTAINING ANY --
-- RIGHTS YOU MAY REQUIRE FOR YOUR IMPLEMENTATION. XILINX EXPRESSLY --
-- DISCLAIMS ANY WARRANTY WHATSOEVER WITH RESPECT TO THE ADEQUACY OF THE --
-- IMPLEMENTATION, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OR --
-- REPRESENTATIONS THAT THIS IMPLEMENTATION IS FREE FROM CLAIMS OF --
-- INFRINGEMENT, IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A --
-- PARTICULAR PURPOSE. --
-- --
-- Xilinx products are not intended for use in life support appliances, --
-- devices, or systems. Use in such applications are expressly --
-- prohibited. --
-- --
-- (c) Copyright 1995-2020 Xilinx, Inc. --
-- All rights reserved. --
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- You must compile the wrapper file DSP48.vhd when simulating
-- the core, DSP48. When compiling the wrapper file, be sure to
-- reference the XilinxCoreLib VHDL simulation library. For detailed
-- instructions, please refer to the "CORE Generator Help".
-- The synthesis directives "translate_off/translate_on" specified
-- below are supported by Xilinx, Mentor Graphics and Synplicity
-- synthesis tools. Ensure they are correct for your synthesis tool(s).
LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
-- synthesis translate_off
LIBRARY XilinxCoreLib;
-- synthesis translate_on
ENTITY DSP48 IS
PORT (
clk : IN STD_LOGIC;
ce : IN STD_LOGIC;
sel : IN STD_LOGIC_VECTOR(0 DOWNTO 0);
a : IN STD_LOGIC_VECTOR(17 DOWNTO 0);
b : IN STD_LOGIC_VECTOR(17 DOWNTO 0);
c : IN STD_LOGIC_VECTOR(47 DOWNTO 0);
p : OUT STD_LOGIC_VECTOR(47 DOWNTO 0)
);
END DSP48;
ARCHITECTURE DSP48_a OF DSP48 IS
-- synthesis translate_off
COMPONENT wrapped_DSP48
PORT (
clk : IN STD_LOGIC;
ce : IN STD_LOGIC;
sel : IN STD_LOGIC_VECTOR(0 DOWNTO 0);
a : IN STD_LOGIC_VECTOR(17 DOWNTO 0);
b : IN STD_LOGIC_VECTOR(17 DOWNTO 0);
c : IN STD_LOGIC_VECTOR(47 DOWNTO 0);
p : OUT STD_LOGIC_VECTOR(47 DOWNTO 0)
);
END COMPONENT;
-- Configuration specification
FOR ALL : wrapped_DSP48 USE ENTITY XilinxCoreLib.xbip_dsp48_macro_v2_1(behavioral)
GENERIC MAP (
c_a_width => 18,
c_b_width => 18,
c_c_width => 48,
c_concat_width => 48,
c_constant_1 => 1,
c_d_width => 18,
c_has_a => 1,
c_has_acin => 0,
c_has_acout => 0,
c_has_b => 1,
c_has_bcin => 0,
c_has_bcout => 0,
c_has_c => 1,
c_has_carrycascin => 0,
c_has_carrycascout => 0,
c_has_carryin => 0,
c_has_carryout => 0,
c_has_ce => 1,
c_has_cea => 0,
c_has_ceb => 0,
c_has_cec => 0,
c_has_ceconcat => 0,
c_has_ced => 0,
c_has_cem => 0,
c_has_cep => 0,
c_has_cesel => 0,
c_has_concat => 0,
c_has_d => 0,
c_has_indep_ce => 0,
c_has_indep_sclr => 0,
c_has_pcin => 0,
c_has_pcout => 0,
c_has_sclr => 0,
c_has_sclra => 0,
c_has_sclrb => 0,
c_has_sclrc => 0,
c_has_sclrconcat => 0,
c_has_sclrd => 0,
c_has_sclrm => 0,
c_has_sclrp => 0,
c_has_sclrsel => 0,
c_latency => -1,
c_model_type => 0,
c_opmodes => "0000000000010000000,0000001100010000000",
c_p_lsb => 0,
c_p_msb => 47,
c_reg_config => "00000000000011100111100111100100",
c_sel_width => 1,
c_test_core => 0,
c_verbosity => 0,
c_xdevicefamily => "spartan6"
);
-- synthesis translate_on
BEGIN
-- synthesis translate_off
U0 : wrapped_DSP48
PORT MAP (
clk => clk,
ce => ce,
sel => sel,
a => a,
b => b,
c => c,
p => p
);
-- synthesis translate_on
END DSP48_a;
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<generated_project xmlns="http://www.xilinx.com/XMLSchema" xmlns:xil_pn="http://www.xilinx.com/XMLSchema">
<!-- -->
<!-- For tool use only. Do not edit. -->
<!-- -->
<!-- ProjectNavigator created generated project file. -->
<!-- For use in tracking generated file and other information -->
<!-- allowing preservation of process status. -->
<!-- -->
<!-- Copyright (c) 1995-2013 Xilinx, Inc. All rights reserved. -->
<version xmlns="http://www.xilinx.com/XMLSchema">11.1</version>
<sourceproject xmlns="http://www.xilinx.com/XMLSchema" xil_pn:fileType="FILE_XISE" xil_pn:name="DSP_SLICE.xise"/>
<files xmlns="http://www.xilinx.com/XMLSchema">
<file xil_pn:fileType="FILE_ASY" xil_pn:name="DSP_SLICE.asy" xil_pn:origination="imported"/>
<file xil_pn:fileType="FILE_SYMBOL" xil_pn:name="DSP_SLICE.sym" xil_pn:origination="imported"/>
<file xil_pn:fileType="FILE_VHO" xil_pn:name="DSP_SLICE.vho" xil_pn:origination="imported"/>
</files>
<transforms xmlns="http://www.xilinx.com/XMLSchema"/>
</generated_project>
@@ -0,0 +1,139 @@
--------------------------------------------------------------------------------
-- This file is owned and controlled by Xilinx and must be used solely --
-- for design, simulation, implementation and creation of design files --
-- limited to Xilinx devices or technologies. Use with non-Xilinx --
-- devices or technologies is expressly prohibited and immediately --
-- terminates your license. --
-- --
-- XILINX IS PROVIDING THIS DESIGN, CODE, OR INFORMATION "AS IS" SOLELY --
-- FOR USE IN DEVELOPING PROGRAMS AND SOLUTIONS FOR XILINX DEVICES. BY --
-- PROVIDING THIS DESIGN, CODE, OR INFORMATION AS ONE POSSIBLE --
-- IMPLEMENTATION OF THIS FEATURE, APPLICATION OR STANDARD, XILINX IS --
-- MAKING NO REPRESENTATION THAT THIS IMPLEMENTATION IS FREE FROM ANY --
-- CLAIMS OF INFRINGEMENT, AND YOU ARE RESPONSIBLE FOR OBTAINING ANY --
-- RIGHTS YOU MAY REQUIRE FOR YOUR IMPLEMENTATION. XILINX EXPRESSLY --
-- DISCLAIMS ANY WARRANTY WHATSOEVER WITH RESPECT TO THE ADEQUACY OF THE --
-- IMPLEMENTATION, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OR --
-- REPRESENTATIONS THAT THIS IMPLEMENTATION IS FREE FROM CLAIMS OF --
-- INFRINGEMENT, IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A --
-- PARTICULAR PURPOSE. --
-- --
-- Xilinx products are not intended for use in life support appliances, --
-- devices, or systems. Use in such applications are expressly --
-- prohibited. --
-- --
-- (c) Copyright 1995-2020 Xilinx, Inc. --
-- All rights reserved. --
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- You must compile the wrapper file DSP_SLICE.vhd when simulating
-- the core, DSP_SLICE. When compiling the wrapper file, be sure to
-- reference the XilinxCoreLib VHDL simulation library. For detailed
-- instructions, please refer to the "CORE Generator Help".
-- The synthesis directives "translate_off/translate_on" specified
-- below are supported by Xilinx, Mentor Graphics and Synplicity
-- synthesis tools. Ensure they are correct for your synthesis tool(s).
LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
-- synthesis translate_off
LIBRARY XilinxCoreLib;
-- synthesis translate_on
ENTITY DSP_SLICE IS
PORT (
clk : IN STD_LOGIC;
ce : IN STD_LOGIC;
sel : IN STD_LOGIC_VECTOR(0 DOWNTO 0);
a : IN STD_LOGIC_VECTOR(17 DOWNTO 0);
b : IN STD_LOGIC_VECTOR(17 DOWNTO 0);
c : IN STD_LOGIC_VECTOR(47 DOWNTO 0);
p : OUT STD_LOGIC_VECTOR(47 DOWNTO 0)
);
END DSP_SLICE;
ARCHITECTURE DSP_SLICE_a OF DSP_SLICE IS
-- synthesis translate_off
COMPONENT wrapped_DSP_SLICE
PORT (
clk : IN STD_LOGIC;
ce : IN STD_LOGIC;
sel : IN STD_LOGIC_VECTOR(0 DOWNTO 0);
a : IN STD_LOGIC_VECTOR(17 DOWNTO 0);
b : IN STD_LOGIC_VECTOR(17 DOWNTO 0);
c : IN STD_LOGIC_VECTOR(47 DOWNTO 0);
p : OUT STD_LOGIC_VECTOR(47 DOWNTO 0)
);
END COMPONENT;
-- Configuration specification
FOR ALL : wrapped_DSP_SLICE USE ENTITY XilinxCoreLib.xbip_dsp48_macro_v2_1(behavioral)
GENERIC MAP (
c_a_width => 18,
c_b_width => 18,
c_c_width => 48,
c_concat_width => 48,
c_constant_1 => 1,
c_d_width => 18,
c_has_a => 1,
c_has_acin => 0,
c_has_acout => 0,
c_has_b => 1,
c_has_bcin => 0,
c_has_bcout => 0,
c_has_c => 1,
c_has_carrycascin => 0,
c_has_carrycascout => 0,
c_has_carryin => 0,
c_has_carryout => 0,
c_has_ce => 1,
c_has_cea => 0,
c_has_ceb => 0,
c_has_cec => 0,
c_has_ceconcat => 0,
c_has_ced => 0,
c_has_cem => 0,
c_has_cep => 0,
c_has_cesel => 0,
c_has_concat => 0,
c_has_d => 0,
c_has_indep_ce => 0,
c_has_indep_sclr => 0,
c_has_pcin => 0,
c_has_pcout => 0,
c_has_sclr => 0,
c_has_sclra => 0,
c_has_sclrb => 0,
c_has_sclrc => 0,
c_has_sclrconcat => 0,
c_has_sclrd => 0,
c_has_sclrm => 0,
c_has_sclrp => 0,
c_has_sclrsel => 0,
c_latency => -1,
c_model_type => 0,
c_opmodes => "0000000000010000000,0000001100010000000",
c_p_lsb => 0,
c_p_msb => 47,
c_reg_config => "00000000000011100111100111100100",
c_sel_width => 1,
c_test_core => 0,
c_verbosity => 0,
c_xdevicefamily => "spartan6"
);
-- synthesis translate_on
BEGIN
-- synthesis translate_off
U0 : wrapped_DSP_SLICE
PORT MAP (
clk => clk,
ce => ce,
sel => sel,
a => a,
b => b,
c => c,
p => p
);
-- synthesis translate_on
END DSP_SLICE_a;
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<generated_project xmlns="http://www.xilinx.com/XMLSchema" xmlns:xil_pn="http://www.xilinx.com/XMLSchema">
<!-- -->
<!-- For tool use only. Do not edit. -->
<!-- -->
<!-- ProjectNavigator created generated project file. -->
<!-- For use in tracking generated file and other information -->
<!-- allowing preservation of process status. -->
<!-- -->
<!-- Copyright (c) 1995-2013 Xilinx, Inc. All rights reserved. -->
<version xmlns="http://www.xilinx.com/XMLSchema">11.1</version>
<sourceproject xmlns="http://www.xilinx.com/XMLSchema" xil_pn:fileType="FILE_XISE" xil_pn:name="ModulationMemory.xise"/>
<files xmlns="http://www.xilinx.com/XMLSchema">
<file xil_pn:fileType="FILE_ASY" xil_pn:name="ModulationMemory.asy" xil_pn:origination="imported"/>
<file xil_pn:fileType="FILE_SYMBOL" xil_pn:name="ModulationMemory.sym" xil_pn:origination="imported"/>
<file xil_pn:fileType="FILE_VHO" xil_pn:name="ModulationMemory.vho" xil_pn:origination="imported"/>
</files>
<transforms xmlns="http://www.xilinx.com/XMLSchema">
<transform xil_pn:end_ts="1654895235" xil_pn:name="TRAN_copyInitialToXSTAbstractSynthesis" xil_pn:start_ts="1654895235">
<status xil_pn:value="SuccessfullyRun"/>
<status xil_pn:value="ReadyToRun"/>
</transform>
<transform xil_pn:end_ts="1654895235" xil_pn:name="TRAN_schematicsToHdl" xil_pn:prop_ck="1122694780993201560" xil_pn:start_ts="1654895235">
<status xil_pn:value="SuccessfullyRun"/>
<status xil_pn:value="ReadyToRun"/>
</transform>
<transform xil_pn:end_ts="1654895235" xil_pn:name="TRAN_regenerateCores" xil_pn:prop_ck="5643155787033519143" xil_pn:start_ts="1654895235">
<status xil_pn:value="SuccessfullyRun"/>
<status xil_pn:value="ReadyToRun"/>
</transform>
<transform xil_pn:end_ts="1654895235" xil_pn:name="TRAN_SubProjectAbstractToPreProxy" xil_pn:start_ts="1654895235">
<status xil_pn:value="SuccessfullyRun"/>
<status xil_pn:value="ReadyToRun"/>
</transform>
<transform xil_pn:end_ts="1654895235" xil_pn:name="TRAN_xawsTohdl" xil_pn:prop_ck="-8675282148687179814" xil_pn:start_ts="1654895235">
<status xil_pn:value="SuccessfullyRun"/>
<status xil_pn:value="ReadyToRun"/>
</transform>
</transforms>
</generated_project>
@@ -0,0 +1,149 @@
--------------------------------------------------------------------------------
-- This file is owned and controlled by Xilinx and must be used solely --
-- for design, simulation, implementation and creation of design files --
-- limited to Xilinx devices or technologies. Use with non-Xilinx --
-- devices or technologies is expressly prohibited and immediately --
-- terminates your license. --
-- --
-- XILINX IS PROVIDING THIS DESIGN, CODE, OR INFORMATION "AS IS" SOLELY --
-- FOR USE IN DEVELOPING PROGRAMS AND SOLUTIONS FOR XILINX DEVICES. BY --
-- PROVIDING THIS DESIGN, CODE, OR INFORMATION AS ONE POSSIBLE --
-- IMPLEMENTATION OF THIS FEATURE, APPLICATION OR STANDARD, XILINX IS --
-- MAKING NO REPRESENTATION THAT THIS IMPLEMENTATION IS FREE FROM ANY --
-- CLAIMS OF INFRINGEMENT, AND YOU ARE RESPONSIBLE FOR OBTAINING ANY --
-- RIGHTS YOU MAY REQUIRE FOR YOUR IMPLEMENTATION. XILINX EXPRESSLY --
-- DISCLAIMS ANY WARRANTY WHATSOEVER WITH RESPECT TO THE ADEQUACY OF THE --
-- IMPLEMENTATION, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OR --
-- REPRESENTATIONS THAT THIS IMPLEMENTATION IS FREE FROM CLAIMS OF --
-- INFRINGEMENT, IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A --
-- PARTICULAR PURPOSE. --
-- --
-- Xilinx products are not intended for use in life support appliances, --
-- devices, or systems. Use in such applications are expressly --
-- prohibited. --
-- --
-- (c) Copyright 1995-2022 Xilinx, Inc. --
-- All rights reserved. --
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- You must compile the wrapper file ModulationMemory.vhd when simulating
-- the core, ModulationMemory. When compiling the wrapper file, be sure to
-- reference the XilinxCoreLib VHDL simulation library. For detailed
-- instructions, please refer to the "CORE Generator Help".
-- The synthesis directives "translate_off/translate_on" specified
-- below are supported by Xilinx, Mentor Graphics and Synplicity
-- synthesis tools. Ensure they are correct for your synthesis tool(s).
LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
-- synthesis translate_off
LIBRARY XilinxCoreLib;
-- synthesis translate_on
ENTITY ModulationMemory IS
PORT (
clka : IN STD_LOGIC;
wea : IN STD_LOGIC_VECTOR(0 DOWNTO 0);
addra : IN STD_LOGIC_VECTOR(7 DOWNTO 0);
dina : IN STD_LOGIC_VECTOR(143 DOWNTO 0);
clkb : IN STD_LOGIC;
addrb : IN STD_LOGIC_VECTOR(7 DOWNTO 0);
doutb : OUT STD_LOGIC_VECTOR(143 DOWNTO 0)
);
END ModulationMemory;
ARCHITECTURE ModulationMemory_a OF ModulationMemory IS
-- synthesis translate_off
COMPONENT wrapped_ModulationMemory
PORT (
clka : IN STD_LOGIC;
wea : IN STD_LOGIC_VECTOR(0 DOWNTO 0);
addra : IN STD_LOGIC_VECTOR(7 DOWNTO 0);
dina : IN STD_LOGIC_VECTOR(143 DOWNTO 0);
clkb : IN STD_LOGIC;
addrb : IN STD_LOGIC_VECTOR(7 DOWNTO 0);
doutb : OUT STD_LOGIC_VECTOR(143 DOWNTO 0)
);
END COMPONENT;
-- Configuration specification
FOR ALL : wrapped_ModulationMemory USE ENTITY XilinxCoreLib.blk_mem_gen_v7_3(behavioral)
GENERIC MAP (
c_addra_width => 8,
c_addrb_width => 8,
c_algorithm => 1,
c_axi_id_width => 4,
c_axi_slave_type => 0,
c_axi_type => 1,
c_byte_size => 9,
c_common_clk => 0,
c_default_data => "0",
c_disable_warn_bhv_coll => 0,
c_disable_warn_bhv_range => 0,
c_enable_32bit_address => 0,
c_family => "spartan6",
c_has_axi_id => 0,
c_has_ena => 0,
c_has_enb => 0,
c_has_injecterr => 0,
c_has_mem_output_regs_a => 0,
c_has_mem_output_regs_b => 0,
c_has_mux_output_regs_a => 0,
c_has_mux_output_regs_b => 0,
c_has_regcea => 0,
c_has_regceb => 0,
c_has_rsta => 0,
c_has_rstb => 0,
c_has_softecc_input_regs_a => 0,
c_has_softecc_output_regs_b => 0,
c_init_file => "BlankString",
c_init_file_name => "no_coe_file_loaded",
c_inita_val => "0",
c_initb_val => "0",
c_interface_type => 0,
c_load_init_file => 0,
c_mem_type => 1,
c_mux_pipeline_stages => 0,
c_prim_type => 1,
c_read_depth_a => 256,
c_read_depth_b => 256,
c_read_width_a => 144,
c_read_width_b => 144,
c_rst_priority_a => "CE",
c_rst_priority_b => "CE",
c_rst_type => "SYNC",
c_rstram_a => 0,
c_rstram_b => 0,
c_sim_collision_check => "ALL",
c_use_bram_block => 0,
c_use_byte_wea => 0,
c_use_byte_web => 0,
c_use_default_data => 0,
c_use_ecc => 0,
c_use_softecc => 0,
c_wea_width => 1,
c_web_width => 1,
c_write_depth_a => 256,
c_write_depth_b => 256,
c_write_mode_a => "WRITE_FIRST",
c_write_mode_b => "WRITE_FIRST",
c_write_width_a => 144,
c_write_width_b => 144,
c_xdevicefamily => "spartan6"
);
-- synthesis translate_on
BEGIN
-- synthesis translate_off
U0 : wrapped_ModulationMemory
PORT MAP (
clka => clka,
wea => wea,
addra => addra,
dina => dina,
clkb => clkb,
addrb => addrb,
doutb => doutb
);
-- synthesis translate_on
END ModulationMemory_a;
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<generated_project xmlns="http://www.xilinx.com/XMLSchema" xmlns:xil_pn="http://www.xilinx.com/XMLSchema">
<!-- -->
<!-- For tool use only. Do not edit. -->
<!-- -->
<!-- ProjectNavigator created generated project file. -->
<!-- For use in tracking generated file and other information -->
<!-- allowing preservation of process status. -->
<!-- -->
<!-- Copyright (c) 1995-2013 Xilinx, Inc. All rights reserved. -->
<version xmlns="http://www.xilinx.com/XMLSchema">11.1</version>
<sourceproject xmlns="http://www.xilinx.com/XMLSchema" xil_pn:fileType="FILE_XISE" xil_pn:name="PLL.xise"/>
<files xmlns="http://www.xilinx.com/XMLSchema">
<file xil_pn:fileType="FILE_ASY" xil_pn:name="PLL.asy" xil_pn:origination="imported"/>
<file xil_pn:fileType="FILE_VHO" xil_pn:name="PLL.vho" xil_pn:origination="imported"/>
</files>
<transforms xmlns="http://www.xilinx.com/XMLSchema">
<transform xil_pn:end_ts="1588801396" xil_pn:name="TRAN_copyInitialToXSTAbstractSynthesis" xil_pn:start_ts="1588801396">
<status xil_pn:value="SuccessfullyRun"/>
<status xil_pn:value="ReadyToRun"/>
</transform>
<transform xil_pn:end_ts="1654895235" xil_pn:name="TRAN_schematicsToHdl" xil_pn:prop_ck="-7485999881756126958" xil_pn:start_ts="1654895235">
<status xil_pn:value="SuccessfullyRun"/>
<status xil_pn:value="ReadyToRun"/>
</transform>
<transform xil_pn:end_ts="1654895235" xil_pn:name="TRAN_regenerateCores" xil_pn:prop_ck="5187593455580750305" xil_pn:start_ts="1654895235">
<status xil_pn:value="SuccessfullyRun"/>
<status xil_pn:value="ReadyToRun"/>
</transform>
<transform xil_pn:end_ts="1654895235" xil_pn:name="TRAN_SubProjectAbstractToPreProxy" xil_pn:start_ts="1654895235">
<status xil_pn:value="SuccessfullyRun"/>
<status xil_pn:value="ReadyToRun"/>
</transform>
<transform xil_pn:end_ts="1654895235" xil_pn:name="TRAN_xawsTohdl" xil_pn:prop_ck="-8519729393005554412" xil_pn:start_ts="1654895235">
<status xil_pn:value="SuccessfullyRun"/>
<status xil_pn:value="ReadyToRun"/>
</transform>
</transforms>
</generated_project>
@@ -0,0 +1,59 @@
# file: PLL.ucf
#
# (c) Copyright 2008 - 2011 Xilinx, Inc. All rights reserved.
#
# This file contains confidential and proprietary information
# of Xilinx, Inc. and is protected under U.S. and
# international copyright and other intellectual property
# laws.
#
# DISCLAIMER
# This disclaimer is not a license and does not grant any
# rights to the materials distributed herewith. Except as
# otherwise provided in a valid license issued to you by
# Xilinx, and to the maximum extent permitted by applicable
# law: (1) THESE MATERIALS ARE MADE AVAILABLE "AS IS" AND
# WITH ALL FAULTS, AND XILINX HEREBY DISCLAIMS ALL WARRANTIES
# AND CONDITIONS, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
# BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-
# INFRINGEMENT, OR FITNESS FOR ANY PARTICULAR PURPOSE; and
# (2) Xilinx shall not be liable (whether in contract or tort,
# including negligence, or under any other theory of
# liability) for any loss or damage of any kind or nature
# related to, arising under or in connection with these
# materials, including for any direct, or any indirect,
# special, incidental, or consequential loss or damage
# (including loss of data, profits, goodwill, or any type of
# loss or damage suffered as a result of any action brought
# by a third party) even if such damage or loss was
# reasonably foreseeable or Xilinx had been advised of the
# possibility of the same.
#
# CRITICAL APPLICATIONS
# Xilinx products are not designed or intended to be fail-
# safe, or for use in any application requiring fail-safe
# performance, such as life-support or safety devices or
# systems, Class III medical devices, nuclear facilities,
# applications related to the deployment of airbags, or any
# other applications that could lead to death, personal
# injury, or severe property or environmental damage
# (individually and collectively, "Critical
# Applications"). Customer assumes the sole risk and
# liability of any use of Xilinx products in Critical
# Applications, subject only to applicable laws and
# regulations governing limitations on product liability.
#
# THIS COPYRIGHT NOTICE AND DISCLAIMER MUST BE RETAINED AS
# PART OF THIS FILE AT ALL TIMES.
#
# Input clock periods. These duplicate the values entered for the
# input clocks. You can use these to time your system
#----------------------------------------------------------------
NET "CLK_IN1" TNM_NET = "CLK_IN1";
TIMESPEC "TS_CLK_IN1" = PERIOD "CLK_IN1" 62.5 ns HIGH 50% INPUT_JITTER 625.0ps;
# FALSE PATH constraints
PIN "RESET" TIG;
@@ -0,0 +1,172 @@
-- file: PLL.vhd
--
-- (c) Copyright 2008 - 2011 Xilinx, Inc. All rights reserved.
--
-- This file contains confidential and proprietary information
-- of Xilinx, Inc. and is protected under U.S. and
-- international copyright and other intellectual property
-- laws.
--
-- DISCLAIMER
-- This disclaimer is not a license and does not grant any
-- rights to the materials distributed herewith. Except as
-- otherwise provided in a valid license issued to you by
-- Xilinx, and to the maximum extent permitted by applicable
-- law: (1) THESE MATERIALS ARE MADE AVAILABLE "AS IS" AND
-- WITH ALL FAULTS, AND XILINX HEREBY DISCLAIMS ALL WARRANTIES
-- AND CONDITIONS, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
-- BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-
-- INFRINGEMENT, OR FITNESS FOR ANY PARTICULAR PURPOSE; and
-- (2) Xilinx shall not be liable (whether in contract or tort,
-- including negligence, or under any other theory of
-- liability) for any loss or damage of any kind or nature
-- related to, arising under or in connection with these
-- materials, including for any direct, or any indirect,
-- special, incidental, or consequential loss or damage
-- (including loss of data, profits, goodwill, or any type of
-- loss or damage suffered as a result of any action brought
-- by a third party) even if such damage or loss was
-- reasonably foreseeable or Xilinx had been advised of the
-- possibility of the same.
--
-- CRITICAL APPLICATIONS
-- Xilinx products are not designed or intended to be fail-
-- safe, or for use in any application requiring fail-safe
-- performance, such as life-support or safety devices or
-- systems, Class III medical devices, nuclear facilities,
-- applications related to the deployment of airbags, or any
-- other applications that could lead to death, personal
-- injury, or severe property or environmental damage
-- (individually and collectively, "Critical
-- Applications"). Customer assumes the sole risk and
-- liability of any use of Xilinx products in Critical
-- Applications, subject only to applicable laws and
-- regulations governing limitations on product liability.
--
-- THIS COPYRIGHT NOTICE AND DISCLAIMER MUST BE RETAINED AS
-- PART OF THIS FILE AT ALL TIMES.
--
------------------------------------------------------------------------------
-- User entered comments
------------------------------------------------------------------------------
-- None
--
------------------------------------------------------------------------------
-- "Output Output Phase Duty Pk-to-Pk Phase"
-- "Clock Freq (MHz) (degrees) Cycle (%) Jitter (ps) Error (ps)"
------------------------------------------------------------------------------
-- CLK_OUT1___102.400______0.000______50.0_____1274.405____150.000
--
------------------------------------------------------------------------------
-- "Input Clock Freq (MHz) Input Jitter (UI)"
------------------------------------------------------------------------------
-- __primary______________16____________0.010
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;
use ieee.std_logic_arith.all;
use ieee.numeric_std.all;
library unisim;
use unisim.vcomponents.all;
entity PLL is
port
(-- Clock in ports
CLK_IN1 : in std_logic;
-- Clock out ports
CLK_OUT1 : out std_logic;
-- Status and control signals
RESET : in std_logic;
LOCKED : out std_logic
);
end PLL;
architecture xilinx of PLL is
attribute CORE_GENERATION_INFO : string;
attribute CORE_GENERATION_INFO of xilinx : architecture is "PLL,clk_wiz_v3_6,{component_name=PLL,use_phase_alignment=true,use_min_o_jitter=false,use_max_i_jitter=false,use_dyn_phase_shift=false,use_inclk_switchover=false,use_dyn_reconfig=false,feedback_source=FDBK_AUTO,primtype_sel=DCM_SP,num_out_clk=1,clkin1_period=62.5,clkin2_period=62.5,use_power_down=false,use_reset=true,use_locked=true,use_inclk_stopped=false,use_status=false,use_freeze=false,use_clk_valid=false,feedback_type=SINGLE,clock_mgr_type=AUTO,manual_override=false}";
-- Input clock buffering / unused connectors
signal clkin1 : std_logic;
-- Output clock buffering
signal clkfb : std_logic;
signal clk0 : std_logic;
signal clkfx : std_logic;
signal clkfbout : std_logic;
signal locked_internal : std_logic;
signal status_internal : std_logic_vector(7 downto 0);
begin
-- Input buffering
--------------------------------------
clkin1_buf : IBUFG
port map
(O => clkin1,
I => CLK_IN1);
-- Clocking primitive
--------------------------------------
-- Instantiation of the DCM primitive
-- * Unused inputs are tied off
-- * Unused outputs are labeled unused
dcm_sp_inst: DCM_SP
generic map
(CLKDV_DIVIDE => 2.000,
CLKFX_DIVIDE => 5,
CLKFX_MULTIPLY => 32,
CLKIN_DIVIDE_BY_2 => FALSE,
CLKIN_PERIOD => 62.5,
CLKOUT_PHASE_SHIFT => "NONE",
CLK_FEEDBACK => "1X",
DESKEW_ADJUST => "SYSTEM_SYNCHRONOUS",
PHASE_SHIFT => 0,
STARTUP_WAIT => FALSE)
port map
-- Input clock
(CLKIN => clkin1,
CLKFB => clkfb,
-- Output clocks
CLK0 => clk0,
CLK90 => open,
CLK180 => open,
CLK270 => open,
CLK2X => open,
CLK2X180 => open,
CLKFX => clkfx,
CLKFX180 => open,
CLKDV => open,
-- Ports for dynamic phase shift
PSCLK => '0',
PSEN => '0',
PSINCDEC => '0',
PSDONE => open,
-- Other control and status signals
LOCKED => locked_internal,
STATUS => status_internal,
RST => RESET,
-- Unused pin, tie low
DSSEN => '0');
LOCKED <= locked_internal;
-- Output buffering
-------------------------------------
clkf_buf : BUFG
port map
(O => clkfb,
I => clk0);
clkout1_buf : BUFG
port map
(O => CLK_OUT1,
I => clkfx);
end xilinx;
@@ -0,0 +1,60 @@
# file: PLL_exdes.ucf
#
# (c) Copyright 2008 - 2011 Xilinx, Inc. All rights reserved.
#
# This file contains confidential and proprietary information
# of Xilinx, Inc. and is protected under U.S. and
# international copyright and other intellectual property
# laws.
#
# DISCLAIMER
# This disclaimer is not a license and does not grant any
# rights to the materials distributed herewith. Except as
# otherwise provided in a valid license issued to you by
# Xilinx, and to the maximum extent permitted by applicable
# law: (1) THESE MATERIALS ARE MADE AVAILABLE "AS IS" AND
# WITH ALL FAULTS, AND XILINX HEREBY DISCLAIMS ALL WARRANTIES
# AND CONDITIONS, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
# BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-
# INFRINGEMENT, OR FITNESS FOR ANY PARTICULAR PURPOSE; and
# (2) Xilinx shall not be liable (whether in contract or tort,
# including negligence, or under any other theory of
# liability) for any loss or damage of any kind or nature
# related to, arising under or in connection with these
# materials, including for any direct, or any indirect,
# special, incidental, or consequential loss or damage
# (including loss of data, profits, goodwill, or any type of
# loss or damage suffered as a result of any action brought
# by a third party) even if such damage or loss was
# reasonably foreseeable or Xilinx had been advised of the
# possibility of the same.
#
# CRITICAL APPLICATIONS
# Xilinx products are not designed or intended to be fail-
# safe, or for use in any application requiring fail-safe
# performance, such as life-support or safety devices or
# systems, Class III medical devices, nuclear facilities,
# applications related to the deployment of airbags, or any
# other applications that could lead to death, personal
# injury, or severe property or environmental damage
# (individually and collectively, "Critical
# Applications"). Customer assumes the sole risk and
# liability of any use of Xilinx products in Critical
# Applications, subject only to applicable laws and
# regulations governing limitations on product liability.
#
# THIS COPYRIGHT NOTICE AND DISCLAIMER MUST BE RETAINED AS
# PART OF THIS FILE AT ALL TIMES.
#
# Input clock periods. These duplicate the values entered for the
# input clocks. You can use these to time your system
#----------------------------------------------------------------
NET "CLK_IN1" TNM_NET = "CLK_IN1";
TIMESPEC "TS_CLK_IN1" = PERIOD "CLK_IN1" 62.5 ns HIGH 50% INPUT_JITTER 625.0ps;
# FALSE PATH constraints
PIN "COUNTER_RESET" TIG;
PIN "RESET" TIG;
@@ -0,0 +1,182 @@
-- file: PLL_exdes.vhd
--
-- (c) Copyright 2008 - 2011 Xilinx, Inc. All rights reserved.
--
-- This file contains confidential and proprietary information
-- of Xilinx, Inc. and is protected under U.S. and
-- international copyright and other intellectual property
-- laws.
--
-- DISCLAIMER
-- This disclaimer is not a license and does not grant any
-- rights to the materials distributed herewith. Except as
-- otherwise provided in a valid license issued to you by
-- Xilinx, and to the maximum extent permitted by applicable
-- law: (1) THESE MATERIALS ARE MADE AVAILABLE "AS IS" AND
-- WITH ALL FAULTS, AND XILINX HEREBY DISCLAIMS ALL WARRANTIES
-- AND CONDITIONS, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
-- BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-
-- INFRINGEMENT, OR FITNESS FOR ANY PARTICULAR PURPOSE; and
-- (2) Xilinx shall not be liable (whether in contract or tort,
-- including negligence, or under any other theory of
-- liability) for any loss or damage of any kind or nature
-- related to, arising under or in connection with these
-- materials, including for any direct, or any indirect,
-- special, incidental, or consequential loss or damage
-- (including loss of data, profits, goodwill, or any type of
-- loss or damage suffered as a result of any action brought
-- by a third party) even if such damage or loss was
-- reasonably foreseeable or Xilinx had been advised of the
-- possibility of the same.
--
-- CRITICAL APPLICATIONS
-- Xilinx products are not designed or intended to be fail-
-- safe, or for use in any application requiring fail-safe
-- performance, such as life-support or safety devices or
-- systems, Class III medical devices, nuclear facilities,
-- applications related to the deployment of airbags, or any
-- other applications that could lead to death, personal
-- injury, or severe property or environmental damage
-- (individually and collectively, "Critical
-- Applications"). Customer assumes the sole risk and
-- liability of any use of Xilinx products in Critical
-- Applications, subject only to applicable laws and
-- regulations governing limitations on product liability.
--
-- THIS COPYRIGHT NOTICE AND DISCLAIMER MUST BE RETAINED AS
-- PART OF THIS FILE AT ALL TIMES.
--
------------------------------------------------------------------------------
-- Clocking wizard example design
------------------------------------------------------------------------------
-- This example design instantiates the created clocking network, where each
-- output clock drives a counter. The high bit of each counter is ported.
------------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;
use ieee.std_logic_arith.all;
use ieee.numeric_std.all;
library unisim;
use unisim.vcomponents.all;
entity PLL_exdes is
generic (
TCQ : in time := 100 ps);
port
(-- Clock in ports
CLK_IN1 : in std_logic;
-- Reset that only drives logic in example design
COUNTER_RESET : in std_logic;
CLK_OUT : out std_logic_vector(1 downto 1) ;
-- High bits of counters driven by clocks
COUNT : out std_logic;
-- Status and control signals
RESET : in std_logic;
LOCKED : out std_logic
);
end PLL_exdes;
architecture xilinx of PLL_exdes is
-- Parameters for the counters
---------------------------------
-- Counter width
constant C_W : integer := 16;
-- When the clock goes out of lock, reset the counters
signal locked_int : std_logic;
signal reset_int : std_logic := '0';
-- Declare the clocks and counter
signal clk : std_logic;
signal clk_int : std_logic;
signal clk_n : std_logic;
signal counter : std_logic_vector(C_W-1 downto 0) := (others => '0');
signal rst_sync : std_logic;
signal rst_sync_int : std_logic;
signal rst_sync_int1 : std_logic;
signal rst_sync_int2 : std_logic;
component PLL is
port
(-- Clock in ports
CLK_IN1 : in std_logic;
-- Clock out ports
CLK_OUT1 : out std_logic;
-- Status and control signals
RESET : in std_logic;
LOCKED : out std_logic
);
end component;
begin
-- Alias output to internally used signal
LOCKED <= locked_int;
-- When the clock goes out of lock, reset the counters
reset_int <= (not locked_int) or RESET or COUNTER_RESET;
process (clk, reset_int) begin
if (reset_int = '1') then
rst_sync <= '1';
rst_sync_int <= '1';
rst_sync_int1 <= '1';
rst_sync_int2 <= '1';
elsif (clk 'event and clk='1') then
rst_sync <= '0';
rst_sync_int <= rst_sync;
rst_sync_int1 <= rst_sync_int;
rst_sync_int2 <= rst_sync_int1;
end if;
end process;
-- Instantiation of the clocking network
----------------------------------------
clknetwork : PLL
port map
(-- Clock in ports
CLK_IN1 => CLK_IN1,
-- Clock out ports
CLK_OUT1 => clk_int,
-- Status and control signals
RESET => RESET,
LOCKED => locked_int);
clk_n <= not clk;
clkout_oddr : ODDR2
port map
(Q => CLK_OUT(1),
C0 => clk,
C1 => clk_n,
CE => '1',
D0 => '1',
D1 => '0',
R => '0',
S => '0');
-- Connect the output clocks to the design
-------------------------------------------
clk <= clk_int;
-- Output clock sampling
-------------------------------------
process (clk, rst_sync_int2) begin
if (rst_sync_int2 = '1') then
counter <= (others => '0') after TCQ;
elsif (rising_edge(clk)) then
counter <= counter + 1 after TCQ;
end if;
end process;
-- alias the high bit to the output
COUNT <= counter(C_W-1);
end xilinx;
@@ -0,0 +1,197 @@
-- file: PLL_tb.vhd
--
-- (c) Copyright 2008 - 2011 Xilinx, Inc. All rights reserved.
--
-- This file contains confidential and proprietary information
-- of Xilinx, Inc. and is protected under U.S. and
-- international copyright and other intellectual property
-- laws.
--
-- DISCLAIMER
-- This disclaimer is not a license and does not grant any
-- rights to the materials distributed herewith. Except as
-- otherwise provided in a valid license issued to you by
-- Xilinx, and to the maximum extent permitted by applicable
-- law: (1) THESE MATERIALS ARE MADE AVAILABLE "AS IS" AND
-- WITH ALL FAULTS, AND XILINX HEREBY DISCLAIMS ALL WARRANTIES
-- AND CONDITIONS, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
-- BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-
-- INFRINGEMENT, OR FITNESS FOR ANY PARTICULAR PURPOSE; and
-- (2) Xilinx shall not be liable (whether in contract or tort,
-- including negligence, or under any other theory of
-- liability) for any loss or damage of any kind or nature
-- related to, arising under or in connection with these
-- materials, including for any direct, or any indirect,
-- special, incidental, or consequential loss or damage
-- (including loss of data, profits, goodwill, or any type of
-- loss or damage suffered as a result of any action brought
-- by a third party) even if such damage or loss was
-- reasonably foreseeable or Xilinx had been advised of the
-- possibility of the same.
--
-- CRITICAL APPLICATIONS
-- Xilinx products are not designed or intended to be fail-
-- safe, or for use in any application requiring fail-safe
-- performance, such as life-support or safety devices or
-- systems, Class III medical devices, nuclear facilities,
-- applications related to the deployment of airbags, or any
-- other applications that could lead to death, personal
-- injury, or severe property or environmental damage
-- (individually and collectively, "Critical
-- Applications"). Customer assumes the sole risk and
-- liability of any use of Xilinx products in Critical
-- Applications, subject only to applicable laws and
-- regulations governing limitations on product liability.
--
-- THIS COPYRIGHT NOTICE AND DISCLAIMER MUST BE RETAINED AS
-- PART OF THIS FILE AT ALL TIMES.
--
------------------------------------------------------------------------------
-- Clocking wizard demonstration testbench
------------------------------------------------------------------------------
-- This demonstration testbench instantiates the example design for the
-- clocking wizard. Input clocks are toggled, which cause the clocking
-- network to lock and the counters to increment.
------------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;
use ieee.std_logic_arith.all;
use ieee.numeric_std.all;
use ieee.std_logic_textio.all;
library std;
use std.textio.all;
library work;
use work.all;
entity PLL_tb is
end PLL_tb;
architecture test of PLL_tb is
-- Clock to Q delay of 100 ps
constant TCQ : time := 100 ps;
-- timescale is 1ps
constant ONE_NS : time := 1 ns;
-- how many cycles to run
constant COUNT_PHASE : integer := 1024 + 1;
-- we'll be using the period in many locations
constant PER1 : time := 62.5 ns;
-- Declare the input clock signals
signal CLK_IN1 : std_logic := '1';
-- The high bit of the sampling counter
signal COUNT : std_logic;
-- Status and control signals
signal RESET : std_logic := '0';
signal LOCKED : std_logic;
signal COUNTER_RESET : std_logic := '0';
-- signal defined to stop mti simulation without severity failure in the report
signal end_of_sim : std_logic := '0';
signal CLK_OUT : std_logic_vector(1 downto 1);
--Freq Check using the M & D values setting and actual Frequency generated
component PLL_exdes
generic (
TCQ : in time := 100 ps);
port
(-- Clock in ports
CLK_IN1 : in std_logic;
-- Reset that only drives logic in example design
COUNTER_RESET : in std_logic;
CLK_OUT : out std_logic_vector(1 downto 1) ;
-- High bits of counters driven by clocks
COUNT : out std_logic;
-- Status and control signals
RESET : in std_logic;
LOCKED : out std_logic
);
end component;
begin
-- Input clock generation
--------------------------------------
process begin
CLK_IN1 <= not CLK_IN1; wait for (PER1/2);
end process;
-- Test sequence
process
procedure simtimeprint is
variable outline : line;
begin
write(outline, string'("## SYSTEM_CYCLE_COUNTER "));
write(outline, NOW/PER1);
write(outline, string'(" ns"));
writeline(output,outline);
end simtimeprint;
procedure simfreqprint (period : time; clk_num : integer) is
variable outputline : LINE;
variable str1 : string(1 to 16);
variable str2 : integer;
variable str3 : string(1 to 2);
variable str4 : integer;
variable str5 : string(1 to 4);
begin
str1 := "Freq of CLK_OUT(";
str2 := clk_num;
str3 := ") ";
str4 := 1000000 ps/period ;
str5 := " MHz" ;
write(outputline, str1 );
write(outputline, str2);
write(outputline, str3);
write(outputline, str4);
write(outputline, str5);
writeline(output, outputline);
end simfreqprint;
begin
RESET <= '1';
wait for (PER1*6);
RESET <= '0';
wait until LOCKED = '1';
COUNTER_RESET <= '1';
wait for (PER1*20);
COUNTER_RESET <= '0';
wait for (PER1*COUNT_PHASE);
simtimeprint;
end_of_sim <= '1';
wait for 1 ps;
report "Simulation Stopped." severity failure;
wait;
end process;
-- Instantiation of the example design containing the clock
-- network and sampling counters
-----------------------------------------------------------
dut : PLL_exdes
generic map (
TCQ => TCQ)
port map
(-- Clock in ports
CLK_IN1 => CLK_IN1,
-- Reset for logic in example design
COUNTER_RESET => COUNTER_RESET,
CLK_OUT => CLK_OUT,
-- High bits of the counters
COUNT => COUNT,
-- Status and control signals
RESET => RESET,
LOCKED => LOCKED);
-- Freq Check
end test;
@@ -0,0 +1,220 @@
-- file: PLL_tb.vhd
--
-- (c) Copyright 2008 - 2011 Xilinx, Inc. All rights reserved.
--
-- This file contains confidential and proprietary information
-- of Xilinx, Inc. and is protected under U.S. and
-- international copyright and other intellectual property
-- laws.
--
-- DISCLAIMER
-- This disclaimer is not a license and does not grant any
-- rights to the materials distributed herewith. Except as
-- otherwise provided in a valid license issued to you by
-- Xilinx, and to the maximum extent permitted by applicable
-- law: (1) THESE MATERIALS ARE MADE AVAILABLE "AS IS" AND
-- WITH ALL FAULTS, AND XILINX HEREBY DISCLAIMS ALL WARRANTIES
-- AND CONDITIONS, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
-- BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-
-- INFRINGEMENT, OR FITNESS FOR ANY PARTICULAR PURPOSE; and
-- (2) Xilinx shall not be liable (whether in contract or tort,
-- including negligence, or under any other theory of
-- liability) for any loss or damage of any kind or nature
-- related to, arising under or in connection with these
-- materials, including for any direct, or any indirect,
-- special, incidental, or consequential loss or damage
-- (including loss of data, profits, goodwill, or any type of
-- loss or damage suffered as a result of any action brought
-- by a third party) even if such damage or loss was
-- reasonably foreseeable or Xilinx had been advised of the
-- possibility of the same.
--
-- CRITICAL APPLICATIONS
-- Xilinx products are not designed or intended to be fail-
-- safe, or for use in any application requiring fail-safe
-- performance, such as life-support or safety devices or
-- systems, Class III medical devices, nuclear facilities,
-- applications related to the deployment of airbags, or any
-- other applications that could lead to death, personal
-- injury, or severe property or environmental damage
-- (individually and collectively, "Critical
-- Applications"). Customer assumes the sole risk and
-- liability of any use of Xilinx products in Critical
-- Applications, subject only to applicable laws and
-- regulations governing limitations on product liability.
--
-- THIS COPYRIGHT NOTICE AND DISCLAIMER MUST BE RETAINED AS
-- PART OF THIS FILE AT ALL TIMES.
--
------------------------------------------------------------------------------
-- Clocking wizard demonstration testbench
------------------------------------------------------------------------------
-- This demonstration testbench instantiates the example design for the
-- clocking wizard. Input clocks are toggled, which cause the clocking
-- network to lock and the counters to increment.
------------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;
use ieee.std_logic_arith.all;
use ieee.numeric_std.all;
use ieee.std_logic_textio.all;
library std;
use std.textio.all;
library work;
use work.all;
entity PLL_tb is
end PLL_tb;
architecture test of PLL_tb is
-- Clock to Q delay of 100 ps
constant TCQ : time := 100 ps;
-- timescale is 1ps
constant ONE_NS : time := 1 ns;
-- how many cycles to run
constant COUNT_PHASE : integer := 1024 + 1;
-- we'll be using the period in many locations
constant PER1 : time := 62.5 ns;
-- Declare the input clock signals
signal CLK_IN1 : std_logic := '1';
-- The high bit of the sampling counter
signal COUNT : std_logic;
-- Status and control signals
signal RESET : std_logic := '0';
signal LOCKED : std_logic;
signal COUNTER_RESET : std_logic := '0';
signal timeout_counter : std_logic_vector (13 downto 0) := (others => '0');
-- signal defined to stop mti simulation without severity failure in the report
signal end_of_sim : std_logic := '0';
signal CLK_OUT : std_logic_vector(1 downto 1);
--Freq Check using the M & D values setting and actual Frequency generated
component PLL_exdes
port
(-- Clock in ports
CLK_IN1 : in std_logic;
-- Reset that only drives logic in example design
COUNTER_RESET : in std_logic;
CLK_OUT : out std_logic_vector(1 downto 1) ;
-- High bits of counters driven by clocks
COUNT : out std_logic;
-- Status and control signals
RESET : in std_logic;
LOCKED : out std_logic
);
end component;
begin
-- Input clock generation
--------------------------------------
process begin
CLK_IN1 <= not CLK_IN1; wait for (PER1/2);
end process;
-- Test sequence
process
procedure simtimeprint is
variable outline : line;
begin
write(outline, string'("## SYSTEM_CYCLE_COUNTER "));
write(outline, NOW/PER1);
write(outline, string'(" ns"));
writeline(output,outline);
end simtimeprint;
procedure simfreqprint (period : time; clk_num : integer) is
variable outputline : LINE;
variable str1 : string(1 to 16);
variable str2 : integer;
variable str3 : string(1 to 2);
variable str4 : integer;
variable str5 : string(1 to 4);
begin
str1 := "Freq of CLK_OUT(";
str2 := clk_num;
str3 := ") ";
str4 := 1000000 ps/period ;
str5 := " MHz" ;
write(outputline, str1 );
write(outputline, str2);
write(outputline, str3);
write(outputline, str4);
write(outputline, str5);
writeline(output, outputline);
end simfreqprint;
begin
report "Timing checks are not valid" severity note;
RESET <= '1';
wait for (PER1*6);
RESET <= '0';
wait until LOCKED = '1';
wait for (PER1*20);
COUNTER_RESET <= '1';
wait for (PER1*19.5);
COUNTER_RESET <= '0';
wait for (PER1*1);
report "Timing checks are valid" severity note;
wait for (PER1*COUNT_PHASE);
simtimeprint;
end_of_sim <= '1';
wait for 1 ps;
report "Simulation Stopped." severity failure;
wait;
end process;
process (CLK_IN1)
procedure simtimeprint is
variable outline : line;
begin
write(outline, string'("## SYSTEM_CYCLE_COUNTER "));
write(outline, NOW/PER1);
write(outline, string'(" ns"));
writeline(output,outline);
end simtimeprint;
begin
if (CLK_IN1'event and CLK_IN1='1') then
timeout_counter <= timeout_counter + '1';
if (timeout_counter = "10000000000000") then
if (LOCKED /= '1') then
simtimeprint;
report "NO LOCK signal" severity failure;
end if;
end if;
end if;
end process;
-- Instantiation of the example design containing the clock
-- network and sampling counters
-----------------------------------------------------------
dut : PLL_exdes
port map
(-- Clock in ports
CLK_IN1 => CLK_IN1,
-- Reset for logic in example design
COUNTER_RESET => COUNTER_RESET,
CLK_OUT => CLK_OUT,
-- High bits of the counters
COUNT => COUNT,
-- Status and control signals
RESET => RESET,
LOCKED => LOCKED);
-- Freq Check
end test;

Some files were not shown because too many files have changed in this diff Show More