Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5826c4ff67 | ||
|
|
09a2aa7932 | ||
|
|
75185c7e36 | ||
|
|
5b70a9a4ce | ||
|
|
7334f89491 | ||
|
|
50f07caa64 | ||
|
|
8704db2de7 | ||
|
|
bd9395fc3d | ||
|
|
6f1eaf19e4 | ||
|
|
7329750c0c | ||
|
|
60f6c9e567 | ||
|
|
e1dfbec712 | ||
|
|
b8cb572bc4 | ||
|
|
e9a0b43ed3 | ||
|
|
ebf0462d85 | ||
|
|
243963063c | ||
|
|
f0ad4e6185 | ||
|
|
fa9da949df | ||
|
|
e202f57ef8 | ||
|
|
e113ef60ab | ||
|
|
16ccafff69 | ||
|
|
b3339d101c | ||
|
|
62c0266a6b | ||
|
|
3f6cfc9108 | ||
|
|
caac6561c2 | ||
|
|
59c9d4f255 | ||
|
|
dfb285ab93 | ||
|
|
2412aedc58 | ||
|
|
c1f8654677 | ||
|
|
7d1879ad79 | ||
|
|
0db3dbe295 | ||
|
|
6693ece0aa | ||
|
|
d2338ff10c | ||
|
|
7997abe2d9 | ||
|
|
8a52431bd3 | ||
|
|
cc6d189d52 | ||
|
|
6ada811c2f | ||
|
|
74723bb635 | ||
|
|
e219f6ec02 | ||
|
|
52c1218bf7 | ||
|
|
3efe968dd1 | ||
|
|
42532c9868 | ||
|
|
4ca4b27246 |
+52
-5
@@ -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/
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"cmake.sourceDirectory": "/home/awe/Documents/radar_system/data_acq_and_processing/gpr_cpp"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+54
-5
@@ -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
|
||||
+62
-14
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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`, выходные каналы, ограничения стадий |
|
||||
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
*.s#*
|
||||
*.b#*
|
||||
*.log
|
||||
*.net
|
||||
*.op.raw
|
||||
*.raw
|
||||
*.vnafw
|
||||
*.aux
|
||||
*.log
|
||||
*.synctex.gz
|
||||
*.xcf
|
||||
*.out
|
||||
*.toc
|
||||
*.pyc
|
||||
Hardware/Renderings
|
||||
+118
@@ -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())
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
*
|
||||
!.gitignore
|
||||
!*/
|
||||
!*.vhd
|
||||
!*.ucf
|
||||
!*.ipf
|
||||
!*.gise
|
||||
!*.xise
|
||||
!*.py
|
||||
!*.bin
|
||||
!VNA/*.dat
|
||||
|
||||
Executable
+45
@@ -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()
|
||||
+53
@@ -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>
|
||||
+102
@@ -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;
|
||||
+31
@@ -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>
|
||||
+59
@@ -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;
|
||||
|
||||
+169
@@ -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;
|
||||
+60
@@ -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;
|
||||
|
||||
+192
@@ -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;
|
||||
+197
@@ -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;
|
||||
+220
@@ -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;
|
||||
+32
@@ -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>
|
||||
+139
@@ -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;
|
||||
+32
@@ -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>
|
||||
+139
@@ -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;
|
||||
+53
@@ -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>
|
||||
+149
@@ -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;
|
||||
+52
@@ -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>
|
||||
+59
@@ -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;
|
||||
|
||||
+172
@@ -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;
|
||||
+60
@@ -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;
|
||||
|
||||
+182
@@ -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;
|
||||
+197
@@ -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;
|
||||
+220
@@ -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
Reference in New Issue
Block a user