Compare commits

..

36 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] b54c04272f fix: correct .gitignore stub name stm32_stub → stm32_settings_stub
Agent-Logs-Url: https://github.com/NawfalMotii79/PLFM_RADAR/sessions/15d36e68-be17-4ee3-b6e0-1da7de544671

Co-authored-by: JJassonn69 <83615043+JJassonn69@users.noreply.github.com>
2026-04-13 15:21:06 +00:00
copilot-swe-agent[bot] ce61b71cf4 fix: stable target IDs, hardware.py null checks, remove unused crcmod
Agent-Logs-Url: https://github.com/NawfalMotii79/PLFM_RADAR/sessions/39ac635f-c79b-438f-8764-8db7361e4d50

Co-authored-by: JJassonn69 <83615043+JJassonn69@users.noreply.github.com>
2026-04-13 15:13:15 +00:00
copilot-swe-agent[bot] bbaf1e3436 fix: restore actionable error messages to stderr in uart_capture.py
Agent-Logs-Url: https://github.com/NawfalMotii79/PLFM_RADAR/sessions/3a9a3676-8353-4df6-96b3-0163bd25923f

Co-authored-by: JJassonn69 <83615043+JJassonn69@users.noreply.github.com>
2026-04-13 15:08:30 +00:00
Jason 4578621c75 fix: restore T20-stripped print() calls in cosim scripts; add 60 mem validation tests
- Restored print() output in 6 generator/cosim scripts that ruff T20
  had silently stripped, leaving dead 'for _var: pass' stubs and
  orphaned expressions. Files restored from pre-ruff commit and
  re-linted with T20/ERA/ARG/E501 per-file-ignores.
- Removed 5 dead/self-blessing scripts (compare.py, compare_doppler.py,
  compare_mf.py, validate_mem_files.py, LUT.py).
- Added test_mem_validation.py: 60 pytest tests validating .mem files
  against independently-derived ground truth (twiddle factors, chirp
  waveforms, memory addressing, segment padding).
- Updated CI cross-layer-tests job to include test_mem_validation.py.
- All 150 tests pass (61 GUI + 29 cross-layer + 60 mem validation).
2026-04-13 20:36:28 +05:45
copilot-swe-agent[bot] 8901894b6c fix: restore uart_capture.py terminal output; add T20 per-file ignore for CLI tool
Agent-Logs-Url: https://github.com/NawfalMotii79/PLFM_RADAR/sessions/671cf948-60b5-47c3-af69-7e1d26366728

Co-authored-by: JJassonn69 <83615043+JJassonn69@users.noreply.github.com>
2026-04-13 14:11:23 +00:00
copilot-swe-agent[bot] e6e2217b76 fix: enforce 1-32 range for Chirps Per Elevation (opcode 0x15); mojibake already fixed
Agent-Logs-Url: https://github.com/NawfalMotii79/PLFM_RADAR/sessions/9509b8cb-c385-479a-a7a6-a4a9307f2615

Co-authored-by: JJassonn69 <83615043+JJassonn69@users.noreply.github.com>
2026-04-12 19:15:58 +00:00
Jason cc9ab27d44 Update 9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/RadarSettings.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-12 22:10:27 +03:00
copilot-swe-agent[bot] 56d0ea2883 fix: use importlib for radar_protocol import; downgrade noisy log levels to DEBUG
Agent-Logs-Url: https://github.com/NawfalMotii79/PLFM_RADAR/sessions/8acb5f68-51fa-4632-a73b-0188b876bed1

Co-authored-by: JJassonn69 <83615043+JJassonn69@users.noreply.github.com>
2026-04-12 19:09:23 +00:00
copilot-swe-agent[bot] b394f6bc49 fix: widen per-file-ignores globs in pyproject.toml to use ** patterns
Agent-Logs-Url: https://github.com/NawfalMotii79/PLFM_RADAR/sessions/1aaab9fe-f41c-4e43-9391-99ce5a500686

Co-authored-by: JJassonn69 <83615043+JJassonn69@users.noreply.github.com>
2026-04-12 19:06:10 +00:00
Jason 23b2beee53 fix: resolve 3 cross-layer bugs (status_words truncation, mode readback, buffer overread)
Bug 1 (FPGA): status_words[0] was 37 bits (8+3+2+5+3+16), silently
truncated to 32. Restructured to {0xFF, mode[1:0], stream[2:0],
3'b000, threshold[15:0]} = 32 bits exactly. Fixed in both
usb_data_interface_ft2232h.v and usb_data_interface.v.

Bug 2 (Python): radar_mode extracted at bit 21 but was actually at
bit 24 after truncation — always returned 0. Updated shift/mask in
parse_status_packet() to match new layout (mode>>22, stream>>19).

Bug 3 (STM32): parseFromUSB() minimum size check was 74 bytes but
9 doubles + uint32 + markers = 82 bytes. Buffer overread on last
fields when 74-81 bytes passed.

All 166 tests pass (29 cross-layer, 92 GUI, 20 MCU, 25 FPGA).
2026-04-12 22:51:26 +05:45
Jason 0537b40dcc feat: add cross-layer contract tests (Python/Verilog/C) with CI job
Three-tier test orchestrator validates opcode maps, bit widths, packet
layouts, and round-trip correctness across FPGA RTL, Python GUI, and
STM32 firmware. Catches 3 real bugs:

- status_words[0] 37-bit truncation in both USB interfaces
- Python radar_mode readback at wrong bit position (bit 21 vs 24)
- RadarSettings.cpp buffer overread (min check 74 vs required 82)

29 tests: 24 pass, 5 xfail (documenting confirmed bugs).
4th CI job added: cross-layer-tests (Python + iverilog + cc).
2026-04-12 16:04:59 +05:45
Jason 2106e24952 fix: enforce strict ruff lint (17 rule sets) across entire repo
- Expand ruff config from E/F to 17 rule sets (B, RUF, SIM, PIE, T20,
  ARG, ERA, A, BLE, RET, ISC, TCH, UP, C4, PERF)
- Fix 907 lint errors across all Python files (GUI, FPGA cosim,
  schematics scripts, simulations, utilities, tools)
- Replace all blind except-Exception with specific exception types
- Remove commented-out dead code (ERA001) from cosim/simulation files
- Modernize typing: deprecated typing.List/Dict/Tuple to builtins
- Fix unused args/loop vars, ambiguous unicode, perf anti-patterns
- Delete legacy GUI files V1-V4
- Add V7 test suite, requirements files
- All CI jobs pass: ruff (0 errors), py_compile, pytest (92/92),
  MCU tests (20/20), FPGA regression (25/25)
2026-04-12 14:21:03 +05:45
Jason b6e8eda130 Merge pull request #55 from NawfalMotii79/main
keep develop upto date with the main
2026-04-11 23:24:24 +03:00
NawfalMotii79 eddc44076a Merge pull request #49 from joyshmitz/fix/readme-alpha-disclaimer
docs(readme): add Alpha disclaimer to Key Features
2026-04-10 00:56:22 +01:00
NawfalMotii79 8cd5464cf8 Ready for production
Added Via fencing and stop mask to ADTR1107 CPLR traces
2026-04-10 00:16:08 +01:00
NawfalMotii79 2bd52909d7 Delete 4_Schematics and Boards Layout/4_6_Schematics/MainBoard/RADAR_Main_Board.brd 2026-04-10 00:14:10 +01:00
NawfalMotii79 4b441a28d1 Final touch (added via fencing and stop mask to ADTR1107 CPLR traces 2026-04-10 00:12:41 +01:00
Serhii 96c1f68778 docs(readme): replace text disclaimer with WIP badge
Swap the verbose Alpha disclaimer paragraph for a simple
"Features: Work in Progress" badge next to the existing
Alpha badge, per review feedback from @JJassonn69.
Badge links to the issues page for current status.
2026-04-09 14:07:37 +03:00
Jason e39141df69 fix: align replay DC notch with dual sub-frame architecture
The replay _replay_dc_notch() was treating all 32 Doppler bins as a
single frame, only zeroing bins at the global edges ({0,1,31} for
width=2). The RTL uses dual 16-point sub-frames where each sub-frame
has its own DC, so the notch must use bin_within_sf = dbin & 0xF.

This fixes test_replay_packets_parseable which was seeing 5 detections
instead of the expected 4, due to a spurious hit at (range=2, doppler=15)
surviving CFAR.
2026-04-09 02:42:50 +03:00
Jason 519c95f452 fix: regenerate golden hex for dual-16pt Doppler and add real-data TBs to regression
Regenerate all real-data golden reference hex files against the current
dual 16-point FFT Doppler architecture (staggered-PRI sub-frames).
The old hex files were generated against the previous 32-point single-FFT
architecture and caused 2048/2048 mismatches in both strict real-data TBs.

Changes:
- Regenerate doppler_ref_i/q.hex, fullchain_doppler_ref_i/q.hex, and all
  downstream golden files (MTI, DC notch, CFAR) via golden_reference.py
- Add tb_doppler_realdata (exact-match, ADI CN0566 data) to regression
- Add tb_fullchain_realdata (exact-match, decim->Doppler chain) to regression
- Both TBs now pass: 2048/2048 bins exact match, MAX_ERROR=0
- Update CI comment: 23 -> 25 testbenches
- Fill in STALE_NOTICE.md with regeneration instructions

Regression: 25/25 pass, 0 fail, 0 skip. ruff check: 0 errors.
2026-04-09 02:36:14 +03:00
Jason 11aa590cf2 fix: full-repo ruff lint cleanup and CI migration to uv
Resolve all 374 ruff errors across 36 Python files (E501, E702, E722,
E741, F821, F841, invalid-syntax) bringing `ruff check .` to zero
errors repo-wide with line-length=100.

Rewrite CI workflow to use uv for dependency management, whole-repo
`ruff check .`, py_compile syntax gate, and merged python-tests job.
Add pyproject.toml with ruff config and uv dependency groups.

CI structure proposed by hcm444.
2026-04-09 02:05:34 +03:00
Jason 57de32b172 fix: resolve all ruff lint errors across V6+ GUIs, v7 module, and FPGA cosim scripts
Fixes 25 remaining manual lint errors after auto-fix pass (94 auto-fixed earlier):
- GUI_V6.py: noqa on availability imports, bare except, unused vars, F811 redefs
- GUI_V6_Demo.py: unused app variable
- v7/models.py: noqa F401 on 8 try/except availability-check imports
- FPGA cosim: unused header/status/span vars, ambiguous 'l' renamed to 'line',
  E701 while-on-one-line split, F841 padding vars annotated

Also adds v7/ module, GUI_PyQt_Map.py, and GUI_V7_PyQt.py to version control.
Expands CI lint job to cover all 21 maintained Python files (was 4).

All 58 Python tests pass. Zero ruff errors on all target files.
2026-04-08 19:11:40 +03:00
Jason 6a117dd324 fix: resolve ruff lint errors and add lint CI job
Remove unused imports (deque, sys, Opcode, struct, _REPLAY_ADJUSTABLE_OPCODES)
across 4 active Python files and refactor semicolons to separate statements
in radar_protocol.py. Add ruff lint job to CI workflow targeting only the
active files (excludes legacy GUI_V*.py and v7/).
2026-04-08 17:28:22 +03:00
Serhii 836243ab18 docs(readme): add Alpha disclaimer to Key Features
Core hardware works, but GPS/map integration, attitude correction,
and beam steering UI are not yet complete — callers deserve to know
before they build on those features.

Closes gap 10.5 from reality-check review.
2026-04-08 01:11:19 +03:00
Serhii 178484a72d Merge remote-tracking branch 'upstream/develop' 2026-04-08 01:10:48 +03:00
Jason 9df73fe994 Merge pull request #48 from NawfalMotii79/main
Sync the branches
2026-04-07 23:27:05 +03:00
Jason ce391f1ae6 ci: add GitHub Actions workflow for full regression suite
Three parallel jobs covering all AERIS-10 test infrastructure:
- Python dashboard tests (58): protocol, connection, replay, opcodes, e2e
- MCU firmware tests (20): bug regression (15) + Gap-3 safety (5)
- FPGA regression (23 TBs + lint): unit, integration, and system e2e

Triggers on push/PR to main and develop branches.
2026-04-07 23:09:24 +03:00
Jason c8fa961f33 Merge pull request #46 from NawfalMotii79/develop
Merge the develop branch into main with the fixed changes.
2026-04-07 21:45:17 +03:00
Jason e4db996db9 fix: remove server credentials from constraints README
Accidentally included SSH key path, hostname, port, and internal server
paths in the build quick-reference section. Replaced with generic
instructions.
2026-04-07 21:37:30 +03:00
Jason 75854a39ca docs: update constraints README with USB_MODE architecture and build guide
Add USB Interface Architecture section documenting the USB_MODE parameter,
generate block mechanism, per-target wrapper pattern, FT2232H pin map, and
build quick-reference. Update top modules table (50T now uses
radar_system_top_50t), bank voltage tables, and signal differences to
reflect the FT2232H/FT601 dual-interface design.
2026-04-07 21:34:38 +03:00
Jason 7c82d20306 refactor(host): remove FT601 references from radar_dashboard, smoke_test, and docs
Replace FT601Connection with FT2232HConnection in radar_dashboard.py and
smoke_test.py. Both files had broken imports after FT601Connection was
removed from radar_protocol.py. Also update requirements_dashboard.txt
(ftd3xx -> pyftdi) and GUI_versions.txt descriptions.
2026-04-07 21:26:09 +03:00
Jason c1d12c4130 test: update test_radar_dashboard for FT2232H-only protocol
Align test suite with FT601 removal from radar_protocol.py:
- Replace FT601Connection with FT2232HConnection throughout
- Rewrite _make_data_packet() to build 11-byte packets (was 35-byte)
- Update data packet roundtrip test for 11-byte format
- Fix truncation test threshold (20 -> 6 bytes, since packets are 11)
- Update ReplayConnection frame_len assertions (35 -> 11 per packet)

57 passed, 1 skipped (h5py), 0 failed.
2026-04-07 21:18:12 +03:00
Serhii ab17d19df2 Merge remote-tracking branch 'origin/main' 2026-04-07 21:16:41 +03:00
Jason 385a54d971 refactor(host): remove FT601 support from radar_protocol.py
Strip all FT601/ftd3xx references from the core protocol module:
- Remove FT601Connection class and ftd3xx import block
- Remove _build_packets_ft601() 35-byte packet builder
- Remove compact: bool parameter from RadarAcquisition
- Remove dual-path parsing logic (compact vs FT601)
- Rename parse_data_packet_compact -> parse_data_packet
- Unify DATA_PACKET_SIZE to single 11-byte constant

The 50T production board uses FT2232H exclusively.
FT601 remains in out-of-scope legacy files (GUI_V6, etc).
2026-04-07 21:10:30 +03:00
NawfalMotii79 f1126d6d80 Add files via upload 2026-04-07 18:17:41 +01:00
NawfalMotii79 4255eff56c Merge pull request #45 from JJassonn69/feat/usb2.0-support
feat(usb): add FT2232H USB 2.0 interface for 50T production board
2026-04-07 18:10:22 +01:00
125 changed files with 1449524 additions and 41122 deletions
+68 -24
View File
@@ -8,64 +8,108 @@ on:
jobs: jobs:
# =========================================================================== # ===========================================================================
# Job 1: Python Host Software Tests (58 tests) # Python: lint (ruff), syntax check (py_compile), unit tests (pytest)
# radar_protocol, radar_dashboard, FT2232H connection, replay, opcodes, e2e # CI structure proposed by hcm444 — uses uv for dependency management
# =========================================================================== # ===========================================================================
python-tests: python-tests:
name: Python Dashboard Tests (58) name: Python Lint + Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - uses: actions/checkout@v4
uses: actions/checkout@v4
- name: Set up Python 3.12 - uses: actions/setup-python@v5
uses: actions/setup-python@v5
with: with:
python-version: "3.12" python-version: "3.12"
- name: Install dependencies - uses: astral-sh/setup-uv@v5
run: |
python -m pip install --upgrade pip
pip install pytest numpy h5py
- name: Run test suite - name: Install dependencies
run: python -m pytest 9_Firmware/9_3_GUI/test_radar_dashboard.py -v --tb=short run: uv sync --group dev
- name: Ruff lint (whole repo)
run: uv run ruff check .
- name: Syntax check (py_compile)
run: |
uv run python - <<'PY'
import py_compile
from pathlib import Path
skip = {".git", "__pycache__", ".venv", "venv", "docs"}
for p in Path(".").rglob("*.py"):
if skip & set(p.parts):
continue
py_compile.compile(str(p), doraise=True)
PY
- name: Unit tests
run: >
uv run pytest
9_Firmware/9_3_GUI/test_radar_dashboard.py -v --tb=short
# =========================================================================== # ===========================================================================
# Job 2: MCU Firmware Unit Tests (20 tests) # MCU Firmware Unit Tests (20 tests)
# Bug regression (15) + Gap-3 safety tests (5) # Bug regression (15) + Gap-3 safety tests (5)
# =========================================================================== # ===========================================================================
mcu-tests: mcu-tests:
name: MCU Firmware Tests (20) name: MCU Firmware Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - uses: actions/checkout@v4
uses: actions/checkout@v4
- name: Install build tools - name: Install build tools
run: sudo apt-get update && sudo apt-get install -y build-essential run: sudo apt-get update && sudo apt-get install -y build-essential
- name: Build and run MCU tests - name: Build and run MCU tests
working-directory: 9_Firmware/9_1_Microcontroller/tests
run: make test run: make test
working-directory: 9_Firmware/9_1_Microcontroller/tests
# =========================================================================== # ===========================================================================
# Job 3: FPGA RTL Regression (23 testbenches + lint) # FPGA RTL Regression (25 testbenches + lint)
# Phase 0: Vivado-style lint, Phase 1-4: unit + integration + e2e
# =========================================================================== # ===========================================================================
fpga-regression: fpga-regression:
name: FPGA Regression (23 TBs + lint) name: FPGA Regression
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - uses: actions/checkout@v4
uses: actions/checkout@v4
- name: Install Icarus Verilog - name: Install Icarus Verilog
run: sudo apt-get update && sudo apt-get install -y iverilog run: sudo apt-get update && sudo apt-get install -y iverilog
- name: Run full FPGA regression - name: Run full FPGA regression
working-directory: 9_Firmware/9_2_FPGA
run: bash run_regression.sh run: bash run_regression.sh
working-directory: 9_Firmware/9_2_FPGA
# ===========================================================================
# Cross-Layer Contract Tests (Python ↔ Verilog ↔ C)
# Validates opcode maps, bit widths, packet layouts, and round-trip
# correctness across FPGA RTL, Python GUI, and STM32 firmware.
# ===========================================================================
cross-layer-tests:
name: Cross-Layer Contract Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@v5
- name: Install dependencies
run: uv sync --group dev
- name: Install Icarus Verilog
run: sudo apt-get update && sudo apt-get install -y iverilog
- name: Run cross-layer contract tests
run: >
uv run pytest
9_Firmware/tests/cross_layer/test_cross_layer_contract.py
9_Firmware/tests/cross_layer/test_mem_validation.py
-v --tb=short
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,10 @@
G75*
%MOIN*%
%OFA0B0*%
%FSLAX25Y25*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
%
M02*
@@ -0,0 +1,194 @@
G75*
%MOIN*%
%OFA0B0*%
%FSLAX25Y25*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
%
%ADD10C,0.00300*%
D10*
X0576543Y0546405D02*
X0576543Y0551109D01*
X0579679Y0551109D02*
X0576543Y0546405D01*
X0579679Y0546405D02*
X0579679Y0551109D01*
X0579679Y0570005D02*
X0579679Y0573141D01*
X0578111Y0574709D01*
X0576543Y0573141D01*
X0576543Y0570005D01*
X0576543Y0572357D02*
X0579679Y0572357D01*
X0582179Y0577205D02*
X0582179Y0580341D01*
X0582179Y0578773D02*
X0586883Y0578773D01*
X0585315Y0577205D01*
X0605779Y0577205D02*
X0605779Y0580341D01*
X0605779Y0578773D02*
X0610483Y0578773D01*
X0608915Y0577205D01*
X0609699Y0581809D02*
X0610483Y0582593D01*
X0610483Y0584161D01*
X0609699Y0584945D01*
X0608915Y0584945D01*
X0608131Y0584161D01*
X0607347Y0584945D01*
X0606563Y0584945D01*
X0605779Y0584161D01*
X0605779Y0582593D01*
X0606563Y0581809D01*
X0608131Y0583377D02*
X0608131Y0584161D01*
X0586095Y0835378D02*
X0586879Y0836162D01*
X0586879Y0837730D01*
X0586095Y0838514D01*
X0586879Y0839982D02*
X0586879Y0843118D01*
X0586879Y0841550D02*
X0582175Y0841550D01*
X0583743Y0843118D01*
X0582959Y0838514D02*
X0582175Y0837730D01*
X0582175Y0836162D01*
X0582959Y0835378D01*
X0583743Y0835378D01*
X0584527Y0836162D01*
X0585311Y0835378D01*
X0586095Y0835378D01*
X0584527Y0836162D02*
X0584527Y0836946D01*
X0605775Y0841550D02*
X0610479Y0841550D01*
X0610479Y0843118D02*
X0610479Y0839982D01*
X0607343Y0843118D02*
X0605775Y0841550D01*
X0612979Y0847182D02*
X0614547Y0845614D01*
X0616115Y0847182D01*
X0616115Y0850318D01*
X0616115Y0847966D02*
X0612979Y0847966D01*
X0612979Y0847182D02*
X0612979Y0850318D01*
X0612979Y0869214D02*
X0616115Y0873918D01*
X0616115Y0869214D01*
X0612979Y0869214D02*
X0612979Y0873918D01*
X0881387Y0841550D02*
X0886091Y0841550D01*
X0886091Y0843118D02*
X0886091Y0839982D01*
X0885307Y0838514D02*
X0886091Y0837730D01*
X0886091Y0836162D01*
X0885307Y0835378D01*
X0884523Y0835378D01*
X0883739Y0836162D01*
X0883739Y0836946D01*
X0883739Y0836162D02*
X0882955Y0835378D01*
X0882171Y0835378D01*
X0881387Y0836162D01*
X0881387Y0837730D01*
X0882171Y0838514D01*
X0881387Y0841550D02*
X0882955Y0843118D01*
X0904987Y0841550D02*
X0909691Y0841550D01*
X0909691Y0843118D02*
X0909691Y0839982D01*
X0906555Y0843118D02*
X0904987Y0841550D01*
X0912191Y0847182D02*
X0913759Y0845614D01*
X0915327Y0847182D01*
X0915327Y0850318D01*
X0915327Y0847966D02*
X0912191Y0847966D01*
X0912191Y0847182D02*
X0912191Y0850318D01*
X0912191Y0869214D02*
X0915327Y0873918D01*
X0915327Y0869214D01*
X0912191Y0869214D02*
X0912191Y0873918D01*
X0908911Y0584945D02*
X0908127Y0584945D01*
X0907343Y0584161D01*
X0906559Y0584945D01*
X0905775Y0584945D01*
X0904991Y0584161D01*
X0904991Y0582593D01*
X0905775Y0581809D01*
X0904991Y0580341D02*
X0904991Y0577205D01*
X0904991Y0578773D02*
X0909695Y0578773D01*
X0908127Y0577205D01*
X0908911Y0581809D02*
X0909695Y0582593D01*
X0909695Y0584161D01*
X0908911Y0584945D01*
X0907343Y0584161D02*
X0907343Y0583377D01*
X0886095Y0578773D02*
X0881391Y0578773D01*
X0881391Y0577205D02*
X0881391Y0580341D01*
X0884527Y0577205D02*
X0886095Y0578773D01*
X0878891Y0573141D02*
X0877323Y0574709D01*
X0875755Y0573141D01*
X0875755Y0570005D01*
X0875755Y0572357D02*
X0878891Y0572357D01*
X0878891Y0573141D02*
X0878891Y0570005D01*
X0878891Y0551109D02*
X0875755Y0546405D01*
X0875755Y0551109D01*
X0878891Y0551109D02*
X0878891Y0546405D01*
X0110335Y0716515D02*
X0110335Y0719651D01*
X0110335Y0717299D02*
X0107199Y0717299D01*
X0107199Y0716515D02*
X0108767Y0714947D01*
X0110335Y0716515D01*
X0107199Y0716515D02*
X0107199Y0719651D01*
X0107199Y0730747D02*
X0107199Y0735451D01*
X0110335Y0735451D01*
X0108767Y0733099D02*
X0107199Y0733099D01*
X0107199Y0730747D02*
X0110335Y0730747D01*
X0102799Y0706651D02*
X0102799Y0703515D01*
X0102799Y0705083D02*
X0098095Y0705083D01*
X0099663Y0706651D01*
X0086999Y0705867D02*
X0086215Y0706651D01*
X0086999Y0705867D02*
X0086999Y0704299D01*
X0086215Y0703515D01*
X0084647Y0703515D01*
X0083863Y0704299D01*
X0083863Y0705083D01*
X0084647Y0706651D01*
X0082295Y0706651D01*
X0082295Y0703515D01*
M02*
@@ -0,0 +1,50 @@
Generated by EAGLE CAM Processor 7.4.0
Drill Station Info File: C:/Users/dell/Desktop/CrowdSupply/RADAR_V6/4_Schematics and Boards Layout/4_6_Schematics/MainBoard_Test/RADAR_Main_Board.dri
Date : 06/04/2026 22:10
Drills : generated
Device : Excellon drill station, coordinate format 2.5 inch
Parameter settings:
Tolerance Drill + : 2.50 %
Tolerance Drill - : 2.50 %
Rotate : no
Mirror : no
Optimize : yes
Auto fit : yes
OffsetX : 0inch
OffsetY : 0inch
Layers : Drills Holes
Drill File Info:
Data Mode : Absolute
Units : 1/100000 Inch
Drills used:
Code Size used
T01 0.0059inch 1609
T02 0.0079inch 1892
T03 0.0100inch 18
T04 0.0118inch 355
T05 0.0138inch 113
T06 0.0197inch 21
T07 0.0236inch 7
T08 0.0252inch 4
T09 0.0331inch 8
T10 0.0354inch 4
T11 0.0394inch 132
T12 0.0400inch 115
T13 0.0470inch 148
T14 0.0472inch 4
T15 0.1260inch 8
Total number of drills: 4438
Plotfiles:
C:/Users/dell/Desktop/CrowdSupply/RADAR_V6/4_Schematics and Boards Layout/4_6_Schematics/MainBoard_Test/RADAR_Main_Board.drd
@@ -0,0 +1,14 @@
T01 0.006in
T02 0.008in
T03 0.010in
T04 0.012in
T05 0.014in
T06 0.020in
T07 0.024in
T08 0.025in
T09 0.033in
T10 0.035in
T11 0.039in
T12 0.040in
T13 0.047in
T14 0.126in
@@ -0,0 +1,37 @@
Generated by EAGLE CAM Processor 7.4.0
Photoplotter Info File: C:/Users/dell/Desktop/CrowdSupply/RADAR_V6/4_Schematics and Boards Layout/4_6_Schematics/MainBoard_Test/RADAR_Main_Board.gpi
Date : 06/04/2026 22:41
Plotfile : C:/Users/dell/Desktop/CrowdSupply/RADAR_V6/4_Schematics and Boards Layout/4_6_Schematics/MainBoard_Test/RADAR_Main_Board.bsk
Apertures : generated:
Device : Gerber RS-274-X photoplotter, coordinate format 2.5 inch
Parameter settings:
Emulate Apertures : no
Tolerance Draw + : 0.00 %
Tolerance Draw - : 0.00 %
Tolerance Flash + : 0.00 %
Tolerance Flash - : 0.00 %
Rotate : no
Mirror : no
Optimize : yes
Auto fit : yes
OffsetX : 0inch
OffsetY : 0inch
Plotfile Info:
Coordinate Format : 2.5
Coordinate Units : Inch
Data Mode : Absolute
Zero Suppression : None
End Of Block : *
Apertures used:
Code Shape Size used
D10 draw 0.0030inch 121
@@ -0,0 +1,710 @@
ADAR1_ 150.00 218.00 180 ADAR1000ACCZN CC-88-1_ADI
ADAR2_ 226.00 218.00 180 ADAR1000ACCZN CC-88-1_ADI
ADAR3_ 226.00 142.00 0 ADAR1000ACCZN CC-88-1_ADI
ADAR4_ 150.00 142.00 0 ADAR1000ACCZN CC-88-1_ADI
ADTR1107_1 141.00 209.00 225 ADTR1107ACCZ CC-24-8_ADI
ADTR1107_2 159.00 209.00 315 ADTR1107ACCZ CC-24-8_ADI
ADTR1107_3 141.00 227.00 135 ADTR1107ACCZ CC-24-8_ADI
ADTR1107_4 159.00 227.00 45 ADTR1107ACCZ CC-24-8_ADI
ADTR1107_5 217.00 209.00 225 ADTR1107ACCZ CC-24-8_ADI
ADTR1107_6 235.00 209.00 315 ADTR1107ACCZ CC-24-8_ADI
ADTR1107_7 217.00 227.00 135 ADTR1107ACCZ CC-24-8_ADI
ADTR1107_8 235.00 227.00 45 ADTR1107ACCZ CC-24-8_ADI
ADTR1107_9 217.00 133.00 225 ADTR1107ACCZ CC-24-8_ADI
ADTR1107_10 235.00 133.00 315 ADTR1107ACCZ CC-24-8_ADI
ADTR1107_11 217.00 151.00 135 ADTR1107ACCZ CC-24-8_ADI
ADTR1107_12 235.00 151.00 45 ADTR1107ACCZ CC-24-8_ADI
ADTR1107_13 141.00 133.00 225 ADTR1107ACCZ CC-24-8_ADI
ADTR1107_14 159.00 133.00 315 ADTR1107ACCZ CC-24-8_ADI
ADTR1107_15 141.00 151.00 135 ADTR1107ACCZ CC-24-8_ADI
ADTR1107_16 159.00 151.00 45 ADTR1107ACCZ CC-24-8_ADI
C1 64.52 156.43 0 0.1uF C0201
C2 64.52 157.08 0 0.1uF C0201
C3 58.61 152.59 180 2.7pF C0402
C4 55.87 153.47 90 0.1uF C0201
C5 56.47 153.82 90 0.1uF C0201
C6 57.05 153.82 90 0.1uF C0201
C7 57.62 153.82 90 0.1uF C0201
C8 59.40 153.77 90 0.1uF C0201
C9 59.87 153.77 90 0.1uF C0201
C10 60.40 153.77 90 0.1uF C0201
C11 61.35 153.77 90 0.1uF C0201
C12 61.90 153.77 90 0.1uF C0201
C13 63.82 155.47 90 0.1uF C0201
C14 53.00 156.48 0 0.1uF C0201
C15 58.60 163.86 0 0.1uF C0201
C16 53.28 158.23 270 0.1uF C0201
C17 59.11 192.79 135 47uF C0201
C18 92.83 95.88 270 2.7pF C0201
C19 92.82 98.42 90 2.7pF C0201
C20 92.73 103.23 270 4.3pF C0201
C21 92.67 105.77 90 4.3pF C0201
C22 62.08 94.57 180 2.2uF C0201
C23 66.17 114.42 90 2.2uF C0201
C24 86.77 109.52 0 100nF C0201
C25 87.32 96.00 0 100nF C0201
C26 87.09 101.26 270 100nF C0201
C27 87.10 108.24 90 100nF C0201
C28 82.41 115.04 0 100nF C0201
C29 75.95 115.04 0 100nF C0201
C30 70.91 115.03 0 100nF C0201
C31 65.66 115.26 90 47nF C0201
C32 61.34 105.71 90 100nF C0201
C33 61.41 100.24 90 100nF C0201
C34 61.35 93.74 90 100nF C0201
C35 71.41 89.46 180 100nF C0201
C36 64.53 158.25 270 0.1uF C0201
C37 53.98 214.81 0 10uF C0201
C38 58.50 214.07 90 0.1uF C0201
C39 63.55 214.17 90 0.1uF C0201
C40 65.48 215.62 180 10uF C0201
C41 66.25 214.48 0 0.1uF C0201
C42 65.48 216.07 180 0.1uF C0201
C43 58.60 142.08 180 0.2pF C0201
C44 57.81 148.10 90 0.1uF C0201
C45 59.31 148.08 90 0.1uF C0201
C46 58.57 148.28 90 0.1uF C0201
C47 56.22 145.73 0 0.1uF C0201
C48 62.55 143.34 90 0.1オF C0402
C49 54.62 143.39 270 0.1オF C0402
C50 17.63 182.50 90 100nF C0201
C51 58.39 139.26 90 0.1uF C0201
C52 59.06 139.29 90 0.1uF C0201
C53 58.40 141.13 0 0.1uF C0201
C54 58.75 133.08 180 0.6pF C0201
C55 39.63 193.43 225 47uF C0201
C56 56.35 136.71 0 0.1uF C0201
C57 62.34 134.95 90 0.1オF C0402
C58 55.10 134.97 270 0.1オF C0402
C59 80.14 213.74 270 32.8pF C0201
C60 76.72 145.26 90 103pF C0201
C61 75.56 144.93 0 7.8pF C0201
C62 75.55 145.56 0 7.8pF C0201
C63 73.84 145.23 90 103pF C0201
C64 72.61 144.89 0 25pF C0201
C65 72.62 145.56 0 25pF C0201
C66 135.36 232.49 45 100pF C0201
C67 134.91 232.95 45 0.1uF C0201
C68 133.70 230.97 225 100pF C0201
C69 133.16 231.41 225 0.1uF C0201
C70 137.58 231.90 225 1pF C0201
C71 134.39 228.76 225 1pF C0201
C72 138.16 228.51 315 1pF C0201
C73 164.39 232.64 315 100pF C0201
C74 164.95 233.09 315 0.1uF C0201
C75 41.98 37.44 270 10オF C0805
C76 44.18 37.77 270 100nF C0402
C77 107.38 38.09 270 10オF C0805
C78 109.99 38.54 270 100nF C0402
C79 112.95 180.56 90 100pF C0201
C80 113.60 180.56 90 0.1uF C0201
C81 112.97 178.52 270 100pF C0201
C82 113.66 178.52 270 0.1uF C0201
C83 115.91 182.17 270 1pF C0201
C84 115.93 177.02 270 1pF C0201
C85 183.83 179.76 0 1pF C0201
C86 226.37 223.00 90 1uF C0201
C87 225.92 222.99 90 1uF C0201
C88 151.49 222.99 90 1uF C0201
C89 150.97 222.98 90 1uF C0201
C90 149.79 222.43 90 1uF C0201
C91 150.27 222.42 90 1uF C0201
C92 225.02 222.49 90 1uF C0201
C93 225.47 222.49 90 1uF C0201
C94 225.69 137.67 270 1uF C0201
C95 226.19 137.65 270 1uF C0201
C96 226.24 136.03 270 1uF C0201
C97 225.71 136.03 270 1uF C0201
C98 149.99 137.72 270 1uF C0201
C99 150.51 137.69 270 1uF C0201
C100 151.06 136.31 270 1uF C0201
C101 150.59 136.31 270 1uF C0201
C102 139.83 212.93 45 10nF C0201
C103 144.78 228.09 315 10nF C0201
C104 160.12 223.26 225 10nF C0201
C105 155.17 207.91 135 10nF C0201
C106 215.85 212.89 45 10nF C0201
C107 220.90 228.14 315 10nF C0201
C108 60.02 193.71 135 4.7uF C0201
C109 59.55 193.23 135 4.7uF C0201
C110 60.48 194.16 135 0.47uF C0201
C111 60.94 194.62 135 0.47uF C0201
C112 61.41 195.11 135 0.47uF C0201
C113 61.87 195.57 135 0.47uF C0201
C114 231.14 207.91 135 10nF C0201
C115 236.08 223.28 225 10nF C0201
C116 231.22 131.83 135 10nF C0201
C117 236.24 147.14 225 10nF C0201
C118 220.65 151.98 315 10nF C0201
C119 215.68 137.01 45 10nF C0201
C120 155.14 131.87 135 10nF C0201
C121 160.32 147.13 225 10nF C0201
C122 144.88 152.19 315 10nF C0201
C123 139.70 136.91 45 10nF C0201
C124 162.87 234.00 135 100pF C0201
C125 163.41 234.54 135 0.1uF C0201
C126 136.19 205.50 315 1pF C0201
C127 77.23 213.72 270 32.8pF C0201
C128 139.27 202.41 315 1pF C0201
C129 139.47 206.09 45 1pF C0201
C130 164.79 203.51 225 100pF C0201
C131 165.28 203.04 225 0.1uF C0201
C132 166.32 204.80 45 100pF C0201
C133 166.83 204.37 45 0.1uF C0201
C134 162.50 204.12 45 1pF C0201
C135 165.61 207.23 45 1pF C0201
C136 161.93 207.42 135 1pF C0201
C137 135.49 203.23 135 100pF C0201
C138 135.02 202.74 135 0.1uF C0201
C139 137.02 201.86 315 100pF C0201
C140 136.39 201.25 315 0.1uF C0201
C141 78.67 213.72 270 106pF C0201
C142 163.82 230.39 135 1pF C0201
C143 160.73 233.50 135 1pF C0201
C144 160.57 229.92 225 1pF C0201
C145 211.16 232.54 45 100pF C0201
C146 210.66 233.00 45 0.1uF C0201
C147 209.83 230.95 225 100pF C0201
C148 209.33 231.39 225 0.1uF C0201
C149 213.54 231.86 225 1pF C0201
C150 210.40 228.70 225 1pF C0201
C151 213.92 228.73 315 1pF C0201
C152 165.66 157.62 315 100pF C0201
C153 38.73 194.33 225 4.7uF C0201
C154 39.19 193.87 225 4.7uF C0201
C155 38.28 194.79 225 0.47uF C0201
C156 37.83 195.23 225 0.47uF C0201
C157 37.37 195.68 225 0.47uF C0201
C158 36.92 196.16 225 0.47uF C0201
C159 33.88 222.14 180 0.1uF C0201
C160 165.89 126.45 225 0.1uF C0201
C161 164.23 159.09 135 100pF C0201
C162 167.22 127.96 45 0.1uF C0201
C163 211.87 205.34 315 1pF C0201
C164 95.14 107.47 90 2.2uF C0201
C165 214.94 202.25 315 1pF C0201
C166 215.17 205.82 45 1pF C0201
C167 166.18 158.10 315 100pF C0201
C168 134.12 125.84 135 0.1uF C0201
C169 164.71 159.51 135 100pF C0201
C170 136.19 124.85 315 0.1uF C0201
C171 239.14 203.45 45 1pF C0201
C172 242.24 206.51 45 1pF C0201
C173 238.08 207.28 135 1pF C0201
C174 165.38 126.90 225 100pF C0201
C175 134.60 126.26 135 0.1uF C0201
C176 62.65 174.87 0 47uF C0201
C177 62.65 173.59 0 4.7uF C0201
C178 62.65 174.26 0 4.7uF C0201
C179 62.64 172.94 0 0.47uF C0201
C180 62.62 172.24 0 0.47uF C0201
C181 62.62 171.67 0 0.47uF C0201
C182 62.63 171.06 0 0.47uF C0201
C183 166.73 128.41 45 100pF C0201
C184 27.95 210.80 90 12pF C0201
C185 29.08 207.55 90 12pF C0201
C186 24.64 201.69 90 4.7uF 35V EIA3528
C187 21.59 209.81 270 4.7uF 35V EIA3528
C188 27.39 203.95 90 0.1uF C0201
C189 24.26 207.63 270 0.1uF C0201
C190 39.09 211.26 0 0.1uF C0201
C191 40.23 211.06 180 3.3uF C0201
C192 33.89 198.90 90 0.1uF C0201
C193 40.08 199.47 180 0.1uF C0201
C194 42.05 205.81 180 0.1uF C0201
C195 36.41 210.93 270 0.1uF C0201
C196 135.71 124.34 315 0.1uF C0201
C197 240.25 230.89 135 1pF C0201
C198 237.12 234.00 135 1pF C0201
C199 236.82 230.17 225 1pF C0201
C200 210.13 157.53 45 100pF C0201
C201 241.14 127.07 225 0.1uF C0201
C202 208.66 156.06 225 100pF C0201
C203 242.67 128.54 45 0.1uF C0201
C204 238.99 127.57 45 1pF C0201
C205 242.06 130.64 45 1pF C0201
C206 238.08 131.28 135 1pF C0201
C207 209.65 157.95 45 100pF C0201
C208 241.62 126.65 225 0.1uF C0201
C209 208.14 156.54 225 100pF C0201
C210 243.19 128.06 45 0.1uF C0201
C211 240.32 154.95 135 1pF C0201
C212 237.24 158.04 135 1pF C0201
C213 236.87 154.22 225 1pF C0201
C214 240.97 157.23 315 100pF C0201
C215 210.73 126.20 135 0.1uF C0201
C216 239.52 158.68 135 100pF C0201
C217 211.88 124.29 315 0.1uF C0201
C218 212.42 156.97 225 1pF C0201
C219 209.28 153.87 225 1pF C0201
C220 213.67 152.93 315 1pF C0201
C221 241.49 157.71 315 100pF C0201
C222 210.31 125.82 135 0.1uF C0201
C223 240.00 159.20 135 100pF C0201
C224 212.30 124.77 315 0.1uF C0201
C225 211.39 128.46 315 1pF C0201
C226 214.47 125.38 315 1pF C0201
C227 215.02 129.62 45 1pF C0201
C228 241.86 202.40 225 100pF C0201
C229 211.30 203.17 135 0.1uF C0201
C230 212.71 201.65 315 100pF C0201
C231 212.21 201.22 315 0.1uF C0201
C232 163.08 127.47 45 1pF C0201
C233 166.20 130.57 45 1pF C0201
C234 162.33 131.03 135 1pF C0201
C235 241.34 202.88 225 100pF C0201
C236 210.78 202.69 135 0.1uF C0201
C237 243.27 203.83 45 100pF C0201
C238 242.80 204.33 45 0.1uF C0201
C239 164.99 155.33 135 1pF C0201
C240 161.87 158.44 135 1pF C0201
C241 161.08 154.32 225 1pF C0201
C242 241.34 233.62 315 100pF C0201
C243 133.65 158.07 45 0.1uF C0201
C244 239.95 235.09 135 100pF C0201
C245 132.14 156.52 225 0.1uF C0201
C246 136.45 156.82 225 1pF C0201
C247 133.36 153.73 225 1pF C0201
C248 137.48 153.08 315 1pF C0201
C249 240.86 233.10 315 100pF C0201
C250 134.17 157.59 45 0.1uF C0201
C251 239.43 234.61 135 100pF C0201
C252 132.62 156.10 225 0.1uF C0201
C253 135.31 128.57 315 1pF C0201
C254 138.43 125.45 315 1pF C0201
C255 139.17 129.72 45 1pF C0201
C256 45.26 22.42 90 100nF C0201
C257 45.46 51.50 90 100nF C0201
C258 112.49 30.06 270 100nF C0402
C259 102.49 30.17 270 100nF C0402
C260 99.60 45.35 90 100nF C0402
C261 118.23 43.06 180 4.7nF C0201
C262 89.70 45.27 90 100nF C0402
C263 108.13 43.21 180 4.7nF C0201
C264 139.40 45.00 90 100nF C0402
C265 98.08 43.16 180 4.7nF C0201
C266 162.79 29.91 270 100nF C0402
C267 88.33 43.11 180 4.7nF C0201
C268 78.40 144.95 0 25pF C0201
C269 83.67 32.34 0 4.7nF C0201
C270 78.39 145.57 0 25pF C0201
C271 93.77 32.09 0 4.7nF C0201
C272 82.50 216.35 90 18pF C0201
C273 103.97 32.04 0 4.7nF C0201
C274 82.74 142.36 270 18pF C0201
C275 113.62 32.04 0 4.7nF C0201
C276 104.58 39.82 90 1オF C0201
C277 104.52 35.60 0 0.1オF C0201
C278 104.52 36.28 0 0.1オF C0201
C279 76.38 89.35 180 100nF C0201
C280 168.48 43.01 180 4.7nF C0201
C281 82.91 89.41 0 100nF C0201
C282 158.18 43.16 180 4.7nF C0201
C283 8.10 24.20 0 22オF C1206
C284 148.18 43.06 180 4.7nF C0201
C285 38.10 177.47 0 47uF C0201
C286 138.38 43.06 180 4.7nF C0201
C287 38.10 176.25 0 4.7uF C0201
C288 133.72 32.04 0 4.7nF C0201
C289 38.10 176.88 0 4.7uF C0201
C290 143.72 31.99 0 4.7nF C0201
C291 38.10 175.60 0 0.47uF C0201
C292 153.87 32.09 0 4.7nF C0201
C293 65.85 157.90 180 0.1uF C0201
C294 164.77 32.04 0 4.7nF C0201
C295 154.62 35.48 0 0.1オF C0201
C296 154.30 39.72 90 1オF C0201
C297 154.62 36.18 0 0.1オF C0201
C298 198.75 54.77 90 100nF C0201
C299 198.98 30.63 270 0.1オF C0201
C300 203.17 31.10 0 1オF C0201
C301 199.58 30.63 270 0.1オF C0201
C302 204.35 54.72 90 100nF C0201
C303 209.45 54.77 90 100nF C0201
C304 214.65 54.82 90 100nF C0201
C305 188.35 55.12 90 100nF C0201
C306 193.60 55.02 90 100nF C0201
C307 219.75 54.82 90 100nF C0201
C308 75.73 213.20 270 22pF C0201
C309 75.69 214.26 90 22pF C0201
C310 183.30 55.27 90 100nF C0201
C311 11.73 156.14 270 22オF C1206
C312 23.23 156.49 270 10オF C0805
C313 25.44 156.93 270 100nF C0402
C314 27.15 156.93 270 1nF C0402
C315 191.92 286.66 90 22オF C1206
C316 171.43 289.19 270 10オF C0805
C317 169.19 289.75 270 100nF C0402
C318 167.38 289.73 270 1nF C0402
C319 204.83 289.54 270 10オF C0805
C320 207.24 290.12 270 100nF C0402
C321 209.13 290.13 270 1nF C0402
C322 182.98 77.69 270 22オF C1206
C323 203.08 78.24 270 10オF C0805
C324 205.59 78.64 270 100nF C0402
C325 207.63 78.68 270 1nF C0402
C326 169.03 77.74 270 10オF C0805
C327 166.69 78.11 270 100nF C0402
C328 164.93 78.08 270 1nF C0402
C329 9.78 12.64 270 22オF C1206
C330 101.58 226.24 270 10オF C0805
C331 99.50 226.65 270 100nF C0402
C332 97.79 226.65 270 1nF C0402
C333 100.98 136.04 270 10オF C0805
C334 99.19 136.60 270 100nF C0402
C335 97.93 136.58 270 1nF C0402
C336 37.48 37.39 270 10オF C0805
C337 39.69 37.72 270 100nF C0402
C338 157.08 38.14 270 10オF C0805
C339 159.68 38.57 270 100nF C0402
C340 193.92 32.97 180 10オF C0805
C341 194.23 30.73 180 100nF C0402
C342 92.22 30.14 270 100nF C0402
C343 82.02 30.26 270 100nF C0402
C344 119.96 45.24 90 100nF C0402
C345 109.54 45.34 90 100nF C0402
C346 142.58 30.01 270 100nF C0402
C347 169.98 45.21 90 100nF C0402
C348 149.66 45.02 90 100nF C0402
C349 152.62 29.92 270 100nF C0402
C350 131.86 30.06 270 100nF C0402
C351 159.82 45.11 90 100nF C0402
D2 73.53 118.48 90 Blue LED-0603
D3 71.88 118.47 90 Blue LED-0603
D4 75.13 118.46 90 Blue LED-0603
D5 76.68 118.45 90 Blue LED-0603
IC1 33.95 216.56 0 AT93C46A-10SQ-2.7 SOIC8
J1 67.00 185.90 0 142-0731-211 1420731211
J18 67.01 195.24 0 142-0731-211 1420731211
J20 52.00 223.05 0 142-0731-211 1420731211
J22 82.73 136.85 0 142-0731-211 1420731211
J23 82.50 221.81 0 142-0731-211 1420731211
J24 129.14 223.50 45 142-0731-211 1420731211
J25 142.75 237.02 45 142-0731-211 1420731211
J26 144.44 197.24 135 142-0731-211 1420731211
J27 131.04 210.69 225 142-0731-211 1420731211
J28 170.81 212.46 45 142-0731-211 1420731211
J29 157.34 198.89 45 142-0731-211 1420731211
J30 155.53 238.64 135 142-0731-211 1420731211
J31 169.04 225.14 45 142-0731-211 1420731211
J32 205.15 223.41 45 142-0731-211 1420731211
J33 218.65 237.01 45 142-0731-211 1420731211
J34 218.84 198.35 135 142-0731-211 1420731211
J35 206.75 210.56 45 142-0731-211 1420731211
J36 247.30 211.61 45 142-0731-211 1420731211
J37 233.94 198.22 45 142-0731-211 1420731211
J38 232.00 239.11 45 142-0731-211 1420731211
J39 245.55 225.61 45 142-0731-211 1420731211
J40 245.45 134.16 45 142-0731-211 1420731211
J41 234.31 122.86 45 142-0731-211 1420731211
J42 233.13 162.21 135 142-0731-211 1420731211
J43 245.46 150.02 45 142-0731-211 1420731211
J44 205.05 149.71 45 142-0731-211 1420731211
J45 216.95 161.51 45 142-0731-211 1420731211
J46 218.94 120.93 45 142-0731-211 1420731211
J47 206.52 133.32 45 142-0731-211 1420731211
J48 170.30 134.66 45 142-0731-211 1420731211
J49 158.25 122.61 45 142-0731-211 1420731211
J50 158.33 161.94 135 142-0731-211 1420731211
J51 169.59 150.89 45 142-0731-211 1420731211
J52 128.70 149.14 45 142-0731-211 1420731211
J53 141.42 161.79 45 142-0731-211 1420731211
J54 142.70 121.17 45 142-0731-211 1420731211
J55 130.17 133.59 45 142-0731-211 1420731211
L1 17.69 157.73 0 L5650M
L2 76.23 145.25 270 12nH L0201
L3 74.69 144.92 0 159nH L0201
L4 74.67 145.56 0 159nH L0201
L5 89.33 109.02 0 BLM15HB121SN1 0402
L6 67.44 215.23 90 BLM15HB121SN1 0402
L7 62.36 215.19 90 BLM15HB121SN1 0402
L8 73.31 145.22 270 12nH L0201
L9 71.66 144.89 0 50nH L0201
L10 71.66 145.55 0 50nH L0201
L11 177.85 289.22 180 L5650M
L12 198.36 289.23 0 L5650M
L13 196.97 79.23 0 L5650M
L14 175.52 79.82 180 L5650M
L15 107.54 226.15 180 L5650M
L16 107.10 137.02 180 L5650M
L17 31.01 37.48 0 L5650M
L18 120.12 6.48 0 L5650M
L19 18.00 207.89 180 BLM15HB121SN1 0402
L20 20.63 203.53 180 BLM15HB121SN1 0402
L21 114.77 65.02 180 L5650M
L22 77.96 214.02 0 107.3nH L0201
L23 126.84 64.98 0 L5650M
L24 77.50 144.94 0 50nH L0201
L25 77.97 213.41 0 107.3nH L0201
L26 79.40 214.04 0 107.3nH L0201
L27 79.42 213.43 0 107.3nH L0201
L28 77.49 145.57 0 50nH L0201
OPA_1 33.00 22.00 90 OPA4703EA/250 PW14
OPA_2 33.20 51.95 90 OPA4703EA/250 PW14
OPA_3 62.90 22.20 90 OPA4703EA/250 PW14
OPA_4 63.00 52.00 90 OPA4703EA/250 PW14
R1 59.23 150.71 270 24R R0402
R2 59.50 177.50 270 100R R0201
R3 57.56 173.11 180 100R R0201
R4 59.81 180.74 90 100R R0201
R5 53.16 172.94 180 100R R0201
R6 55.80 173.04 180 100R R0201
R7 51.85 172.99 180 100R R0201
R8 48.95 173.01 180 100R R0201
R9 50.79 172.96 180 100R R0201
R10 47.85 173.01 180 100R R0201
R11 46.78 172.53 180 100R R0201
R12 59.57 178.72 270 100R R0201
R13 57.79 150.70 270 24R R0402
R14 58.61 143.28 180 115R R0201
R15 58.61 142.62 180 4.3k R0201
R16 58.01 148.94 180 200R R0201
R17 59.07 148.92 180 200R R0201
R18 55.93 144.16 0 0R R0201
R19 61.32 144.15 0 0R R0201
R20 58.22 140.45 180 200R R0201
R21 59.16 140.45 180 200R R0201
R22 58.76 134.18 180 56R R0201
R23 52.14 201.13 90 22R R0201
R24 53.44 201.10 90 22R R0201
R25 54.69 201.12 90 22R R0201
R26 55.94 201.14 90 22R R0201
R27 57.24 201.16 90 22R R0201
R28 58.49 201.18 90 22R R0201
R29 59.76 201.19 90 22R R0201
R30 61.06 201.21 90 22R R0201
R31 51.18 213.99 0 50R R0201
R32 58.76 133.60 180 4.3k R0201
R33 64.84 214.45 90 3k2 R0201
R34 56.23 135.69 0 0R R0201
R35 61.21 135.68 0 0R R0201
R36 53.83 154.35 180 830R R0402
R37 53.81 152.99 0 1k R0402
R38 55.16 150.83 90 20k R0402
R39 50.14 18.20 270 10k R0201
R40 50.24 47.48 270 10k R0201
R41 48.94 53.66 270 1k R0201
R42 48.36 53.66 270 4.7k R0201
R43 48.59 24.74 270 1k R0201
R44 47.96 24.69 270 4.7k R0201
R45 151.96 213.75 270 4.7k R0201
R46 151.95 225.02 270 4.7k R0201
R47 152.50 225.03 270 4.7k R0201
R48 228.04 213.51 270 4.7k R0201
R49 8.32 180.66 180 22R R0201
R50 21.14 177.99 270 4.7k R0201
R51 26.18 180.49 0 22R R0201
R52 26.11 183.12 270 4.7k R0201
R53 17.51 184.25 270 4.7k R0201
R54 17.46 185.30 90 4.7k R0201
R55 227.41 222.59 90 1k R0201
R56 225.17 137.61 270 1k R0201
R57 147.87 137.67 270 1k R0201
R58 149.49 137.69 270 1k R0201
R59 31.74 46.95 270 1k R0201
R60 30.79 206.39 180 1k2_1% R0201
R61 29.92 202.31 0 1k R0201
R62 8.21 183.26 180 22R R0201
R63 8.12 185.78 180 22R R0201
R64 8.07 188.36 180 22R R0201
R65 8.34 187.59 270 4.7k R0201
R66 8.38 184.94 270 4.7k R0201
R67 8.49 182.47 270 4.7k R0201
R68 8.59 179.78 270 4.7k R0201
R69 60.07 182.71 0 4.7k R0201
R70 227.96 222.61 270 4.7k R0201
R71 228.51 222.62 270 4.7k R0201
R72 224.06 146.58 90 4.7k R0201
R73 223.99 131.89 90 4.7k R0201
R74 222.78 129.02 90 4.7k R0201
R75 148.12 146.64 90 4.7k R0201
R76 149.76 132.90 90 4.7k R0201
R77 148.42 132.20 90 4.7k R0201
R78 226.88 222.61 270 840R R0201
R79 224.60 137.59 90 840R R0201
R80 148.40 137.67 90 840R R0201
R81 148.95 137.67 90 840R R0201
R82 34.59 46.90 270 1k R0201
R83 38.25 213.89 0 10k R0201
R84 30.66 215.31 180 10k R0201
R85 61.59 47.14 270 1k R0201
R86 64.39 47.07 270 1k R0201
R87 64.39 57.05 270 1k R0201
R88 61.59 56.93 270 1k R0201
R89 61.33 47.83 180 2.443k R0201
R90 64.65 47.82 0 2.443k R0201
R91 64.62 56.23 180 2.443k R0201
R92 61.36 56.17 0 2.443k R0201
R93 34.59 57.06 270 1k R0201
R94 31.84 56.94 270 1k R0201
R95 31.51 47.78 180 2.443k R0201
R96 34.82 47.77 0 2.443k R0201
R97 34.85 56.23 180 2.443k R0201
R98 31.58 56.17 0 2.443k R0201
R99 61.54 17.22 270 1k R0201
R100 64.30 17.28 270 1k R0201
R101 64.30 27.13 270 1k R0201
R102 61.55 27.13 270 1k R0201
R103 61.29 18.03 180 2.443k R0201
R104 64.54 18.07 0 2.443k R0201
R105 64.53 26.33 180 2.443k R0201
R106 61.30 26.37 0 2.443k R0201
R107 31.65 17.18 270 1k R0201
R108 34.40 17.13 270 1k R0201
R109 34.40 27.03 270 1k R0201
R110 76.70 115.88 270 500R R0201
R111 95.14 108.56 90 10k R0201
R112 75.15 115.93 270 500R R0201
R113 73.75 115.93 270 500R R0201
R114 72.30 115.93 270 500R R0201
R115 76.36 213.18 90 50R R0201
R116 76.37 214.26 270 50R R0201
R117 69.78 212.87 180 4.7k R0201
R118 31.58 26.98 270 1k R0201
R119 31.37 17.93 180 2.443k R0201
R120 34.66 17.92 0 2.443k R0201
R121 34.61 26.18 180 2.443k R0201
R122 31.32 26.17 0 2.443k R0201
R123 49.20 18.43 270 10k R0201
R124 117.45 43.27 90 1k R0201
R125 107.40 43.47 90 1k R0201
R126 97.35 43.42 90 1k R0201
R127 87.50 43.37 90 1k R0201
R128 84.45 32.08 270 1k R0201
R129 94.60 31.88 270 1k R0201
R130 104.68 31.73 270 1k R0201
R131 114.43 31.78 270 1k R0201
R132 105.53 39.53 180 5R R0201
R133 167.67 43.27 90 1k R0201
R134 157.37 43.42 90 1k R0201
R135 147.32 43.37 90 1k R0201
R136 137.52 43.32 90 1k R0201
R137 134.48 31.78 270 1k R0201
R138 69.03 171.15 180 1k R0201
R139 69.03 170.50 180 1k R0201
R140 69.03 169.84 180 1k R0201
R141 69.03 169.17 180 4.7k R0201
R142 37.34 187.53 0 4.7k R0201
R143 60.07 183.41 0 4.7k R0201
R144 60.07 184.08 0 1k R0201
R145 31.18 211.95 180 10k R0201
R146 37.53 214.86 270 2.2k R0201
R147 144.63 31.73 270 1k R0201
R148 154.68 31.83 270 1k R0201
R149 165.58 31.73 270 1k R0201
R150 155.33 39.49 180 5R R0201
R151 154.47 37.43 0 10k R0201
R152 202.92 30.22 90 5R R0201
R153 200.33 30.48 270 10k R0201
R154 42.27 165.29 0 22.1k R0201
R155 35.92 165.29 0 22.1k R0201
R156 29.57 165.29 0 22.1k R0201
R157 23.22 165.29 0 22.1k R0201
R158 87.62 181.59 0 22.1k R0201
R159 87.62 182.49 0 22.1k R0201
R160 87.62 183.44 0 22.1k R0201
R161 87.62 184.44 0 22.1k R0201
R162 87.62 185.34 0 22.1k R0201
R163 87.62 186.24 0 22.1k R0201
R164 87.62 187.14 0 22.1k R0201
R165 63.47 133.96 270 25R R0201
R166 54.05 134.03 270 25R R0201
R167 61.82 97.48 0 1k R0201
R168 61.82 98.03 0 1k R0201
R169 79.72 90.07 90 1k R0201
R170 79.17 90.07 90 1k R0201
R171 86.58 98.07 180 1k R0201
R172 86.58 98.62 180 1k R0201
R173 63.50 156.75 90 100R R0201
RF_SW_1 137.90 204.14 315 M3SWA2-34DR+ 16_QFN
RF_SW_2 163.86 205.85 45 M3SWA2-34DR+ 16_QFN
RF_SW_3 136.15 230.16 225 M3SWA2-34DR+ 16_QFN
RF_SW_4 162.10 231.77 135 M3SWA2-34DR+ 16_QFN
RF_SW_5 213.60 203.94 315 M3SWA2-34DR+ 16_QFN
RF_SW_6 240.51 205.15 45 M3SWA2-34DR+ 16_QFN
RF_SW_7 212.14 230.10 225 M3SWA2-34DR+ 16_QFN
RF_SW_8 238.50 232.26 135 M3SWA2-34DR+ 16_QFN
RF_SW_9 213.10 127.09 315 M3SWA2-34DR+ 16_QFN
RF_SW_10 240.36 129.30 45 M3SWA2-34DR+ 16_QFN
RF_SW_11 211.04 155.25 225 M3SWA2-34DR+ 16_QFN
RF_SW_12 238.60 156.31 135 M3SWA2-34DR+ 16_QFN
RF_SW_13 137.05 127.19 315 M3SWA2-34DR+ 16_QFN
RF_SW_14 164.46 129.20 45 M3SWA2-34DR+ 16_QFN
RF_SW_15 135.09 155.10 225 M3SWA2-34DR+ 16_QFN
RF_SW_16 163.25 156.71 135 M3SWA2-34DR+ 16_QFN
S1 98.41 109.95 90 MOMENTARY-SWITCH-SPST-SMD-4.6X2.8MM TACTILE_SWITCH_SMD_4.6X2.8MM
SJ1 47.68 155.81 90 SJ_2
U$1 116.18 179.55 270 M3SWA2-34DR+ 16_QFN
U$2 91.65 211.91 180 BPF2 BPF2
U$3 91.93 147.07 0 BPF2 BPF2
U1 58.60 159.25 270 AD9484BCPZ-500 CP_56_5_ADI
U2 74.41 102.23 180 STM32F746ZGT7 LQFP-144_STM
U3 60.39 207.89 90 AD9708AR RW_28_ADI
U4 58.59 145.49 90 AD8352ACPZ-R7 CP_16_3_ADI
U5 82.49 214.01 0 LTC5552IUDBTRMPBF UDB_12_ADI
U6 36.17 205.07 0 FT2232HQ 64QFN_FT2232HQ_FTD
U7 48.66 50.54 0 DAC5578SRGET RGE24_2P7X2P7
U8 58.72 136.47 90 AD8352ACPZ-R7 CP_16_3_ADI
U9 22.04 183.82 180 MT25QL01GBBB8E12-0AUT BGA24_MT25QL_MRN
U10 100.04 37.25 0 ADS7830IPWR PW16
U11 115.00 45.14 0 INA241A3IDGKRDGK0008A-MFG DGK0008A-MFG
U13 82.76 144.55 180 LTC5552IUDBTRMPBF UDB_12_ADI
U14 110.85 176.62 270 SZMMSZ5232BT1G SOD-123_ONS
U15 110.85 182.37 270 SZMMSZ5232BT1G SOD-123_ONS
U16 187.75 180.00 180 EP4RKU+ DG1677-2_MNC
U17 128.49 232.25 225 SZMMSZ5232BT1G SOD-123_ONS
U37 133.83 237.34 225 SZMMSZ5232BT1G SOD-123_ONS
U38 164.30 238.84 135 SZMMSZ5232BT1G SOD-123_ONS
U39 169.16 234.38 135 SZMMSZ5232BT1G SOD-123_ONS
U40 171.01 203.72 45 SZMMSZ5232BT1G SOD-123_ONS
U41 166.66 199.06 45 SZMMSZ5232BT1G SOD-123_ONS
U42 49.68 183.28 0 XC7A50T-2FTG256I BGA256C100P16X16_1700X1700X155
U43 135.61 195.39 315 SZMMSZ5232BT1G SOD-123_ONS
U44 129.06 201.24 315 SZMMSZ5232BT1G SOD-123_ONS
U45 203.29 232.19 225 SZMMSZ5232BT1G SOD-123_ONS
U46 209.35 238.11 225 SZMMSZ5232BT1G SOD-123_ONS
U47 203.01 158.96 225 SZMMSZ5232BT1G SOD-123_ONS
U48 207.96 163.71 225 SZMMSZ5232BT1G SOD-123_ONS
U49 210.04 120.11 315 SZMMSZ5232BT1G SOD-123_ONS
U50 205.59 124.76 315 SZMMSZ5232BT1G SOD-123_ONS
U51 248.29 126.73 45 SZMMSZ5232BT1G SOD-123_ONS
U52 243.44 122.18 45 SZMMSZ5232BT1G SOD-123_ONS
U53 172.18 126.35 45 SZMMSZ5232BT1G SOD-123_ONS
U54 167.83 122.10 45 SZMMSZ5232BT1G SOD-123_ONS
U55 167.03 163.87 135 SZMMSZ5232BT1G SOD-123_ONS
U56 173.18 159.01 135 SZMMSZ5232BT1G SOD-123_ONS
U57 126.84 158.23 225 SZMMSZ5232BT1G SOD-123_ONS
U58 132.19 163.58 225 SZMMSZ5232BT1G SOD-123_ONS
U59 133.37 120.04 315 SZMMSZ5232BT1G SOD-123_ONS
U60 128.72 124.59 315 SZMMSZ5232BT1G SOD-123_ONS
U61 210.16 197.51 315 SZMMSZ5232BT1G SOD-123_ONS
U62 206.61 201.76 315 SZMMSZ5232BT1G SOD-123_ONS
U63 247.59 201.96 45 SZMMSZ5232BT1G SOD-123_ONS
U64 246.37 158.82 135 SZMMSZ5232BT1G SOD-123_ONS
U65 241.52 164.27 135 SZMMSZ5232BT1G SOD-123_ONS
U66 243.12 198.13 45 SZMMSZ5232BT1G SOD-123_ONS
U67 241.12 239.87 135 SZMMSZ5232BT1G SOD-123_ONS
U68 245.87 234.92 135 SZMMSZ5232BT1G SOD-123_ONS
U69 48.35 21.44 0 DAC5578SRGET RGE24_2P7X2P7
U73 105.00 45.24 0 INA241A3IDGKRDGK0008A-MFG DGK0008A-MFG
U74 94.97 45.22 0 INA241A3IDGKRDGK0008A-MFG DGK0008A-MFG
U75 85.02 45.18 0 INA241A3IDGKRDGK0008A-MFG DGK0008A-MFG
U76 86.84 30.28 180 INA241A3IDGKRDGK0008A-MFG DGK0008A-MFG
U77 96.86 30.03 180 INA241A3IDGKRDGK0008A-MFG DGK0008A-MFG
U78 107.22 30.01 180 INA241A3IDGKRDGK0008A-MFG DGK0008A-MFG
U79 116.87 29.97 180 INA241A3IDGKRDGK0008A-MFG DGK0008A-MFG
U80 164.99 45.18 0 INA241A3IDGKRDGK0008A-MFG DGK0008A-MFG
U81 154.84 45.21 0 INA241A3IDGKRDGK0008A-MFG DGK0008A-MFG
U82 144.83 45.10 0 INA241A3IDGKRDGK0008A-MFG DGK0008A-MFG
U83 134.88 45.06 0 INA241A3IDGKRDGK0008A-MFG DGK0008A-MFG
U84 137.03 30.02 180 INA241A3IDGKRDGK0008A-MFG DGK0008A-MFG
U85 147.02 30.05 180 INA241A3IDGKRDGK0008A-MFG DGK0008A-MFG
U86 157.06 30.07 180 INA241A3IDGKRDGK0008A-MFG DGK0008A-MFG
U87 167.98 29.93 180 INA241A3IDGKRDGK0008A-MFG DGK0008A-MFG
U88 149.99 37.12 0 ADS7830IPWR PW16
U89 200.60 35.26 270 ADS7830IPWR PW16
X2 5.30 205.16 0 MINI-USB-32005-201 32005-201
X53 5.33 107.56 0 MINI-USB-32005-201 32005-201
XTAL1 90.58 104.75 270 NX3225GD-8MHZ-STD-CRA-3 XTAL_NX3225GD-8MHZ-STD-CRA-3_N
XTAL3 90.68 97.21 270 NX3215SA-32.768KHz XTAL_NX3225GD-8MHZ-STD-CRA-3_N
Y1 27.42 208.60 60 ECS-120-10-36B2-JTN-TR CRYSTAL-SMD-2X2.5MM
@@ -0,0 +1,692 @@
G75*
%MOIN*%
%OFA0B0*%
%FSLAX25Y25*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
%
%ADD10C,0.12998*%
%ADD11C,0.07451*%
%ADD12C,0.04888*%
%ADD13C,0.05774*%
%ADD14OC8,0.06400*%
%ADD15C,0.06400*%
%ADD16C,0.03369*%
%ADD17C,0.06306*%
%ADD18C,0.03943*%
%ADD19C,0.03353*%
%ADD20C,0.06306*%
%ADD21C,0.07487*%
D10*
X0021526Y0017248D03*
X0462471Y0450319D03*
X0462471Y0985752D03*
X0021526Y1166854D03*
X1013652Y1166854D03*
X1013652Y0985752D03*
X1013652Y0450319D03*
X1013652Y0017248D03*
D11*
X0928258Y0471059D03*
X0942400Y0485201D03*
X0928258Y0499343D03*
X0914116Y0485201D03*
X0881888Y0477602D03*
X0867746Y0463460D03*
X0853604Y0477602D03*
X0867746Y0491744D03*
X0832991Y0526382D03*
X0818848Y0540524D03*
X0804706Y0526382D03*
X0818848Y0512240D03*
X0813061Y0576767D03*
X0798919Y0590909D03*
X0813061Y0605052D03*
X0827203Y0590909D03*
X0859911Y0623224D03*
X0845769Y0637366D03*
X0859911Y0651508D03*
X0874054Y0637366D03*
X0909470Y0640122D03*
X0923612Y0654264D03*
X0937754Y0640122D03*
X0923612Y0625980D03*
X0958013Y0592130D03*
X0972156Y0606272D03*
X0986298Y0592130D03*
X0972156Y0577988D03*
X0972116Y0543831D03*
X0957974Y0529689D03*
X0972116Y0515547D03*
X0986258Y0529689D03*
X0926801Y0767752D03*
X0912659Y0781894D03*
X0926801Y0796036D03*
X0940943Y0781894D03*
X0979400Y0820468D03*
X0993542Y0834610D03*
X0979400Y0848752D03*
X0965258Y0834610D03*
X0972510Y0875586D03*
X0986652Y0889728D03*
X0972510Y0903870D03*
X0958368Y0889728D03*
X0919163Y0928736D03*
X0905021Y0942878D03*
X0919163Y0957020D03*
X0933306Y0942878D03*
X0880747Y0934610D03*
X0866604Y0920468D03*
X0852462Y0934610D03*
X0866604Y0948752D03*
X0813455Y0895209D03*
X0827597Y0881067D03*
X0813455Y0866925D03*
X0799313Y0881067D03*
X0819754Y0844619D03*
X0833896Y0830476D03*
X0819754Y0816334D03*
X0805612Y0830476D03*
X0853210Y0782406D03*
X0867352Y0796548D03*
X0881495Y0782406D03*
X0867352Y0768263D03*
X0692400Y0837957D03*
X0678258Y0852099D03*
X0664116Y0837957D03*
X0678258Y0823815D03*
X0639369Y0784531D03*
X0625226Y0770389D03*
X0611084Y0784531D03*
X0625226Y0798674D03*
X0588581Y0778035D03*
X0574439Y0763893D03*
X0560297Y0778035D03*
X0574439Y0792178D03*
X0535825Y0830988D03*
X0521683Y0845130D03*
X0507541Y0830988D03*
X0521683Y0816846D03*
X0514208Y0867285D03*
X0528350Y0881428D03*
X0514208Y0895570D03*
X0500066Y0881428D03*
X0553644Y0934649D03*
X0567786Y0920507D03*
X0581928Y0934649D03*
X0567786Y0948791D03*
X0603958Y0941028D03*
X0618100Y0955170D03*
X0632243Y0941028D03*
X0618100Y0926885D03*
X0657147Y0887878D03*
X0671289Y0902020D03*
X0685432Y0887878D03*
X0671289Y0873736D03*
X0629124Y0653201D03*
X0614982Y0639059D03*
X0629124Y0624917D03*
X0643266Y0639059D03*
X0673455Y0609697D03*
X0687597Y0595555D03*
X0673455Y0581413D03*
X0659313Y0595555D03*
X0676250Y0545800D03*
X0662108Y0531657D03*
X0676250Y0517515D03*
X0690392Y0531657D03*
X0642951Y0484217D03*
X0628809Y0470074D03*
X0614667Y0484217D03*
X0628809Y0498359D03*
X0581731Y0478547D03*
X0567589Y0464405D03*
X0553447Y0478547D03*
X0567589Y0492689D03*
X0532400Y0527445D03*
X0518258Y0541587D03*
X0504116Y0527445D03*
X0518258Y0513303D03*
X0512471Y0574523D03*
X0526613Y0588665D03*
X0512471Y0602807D03*
X0498328Y0588665D03*
X0548407Y0638469D03*
X0562549Y0624326D03*
X0576691Y0638469D03*
X0562549Y0652611D03*
X0341486Y0550280D03*
X0321486Y0550280D03*
X0321486Y0530280D03*
X0341486Y0530280D03*
X0279557Y0723390D03*
X0279557Y0743390D03*
X0279597Y0760161D03*
X0279597Y0780161D03*
X0259597Y0780161D03*
X0259597Y0760161D03*
X0259557Y0743390D03*
X0259557Y0723390D03*
X0220502Y0869650D03*
X0220502Y0889650D03*
X0200502Y0889650D03*
X0200502Y0869650D03*
X0320581Y0864768D03*
X0320581Y0884768D03*
X0340581Y0884768D03*
X0340581Y0864768D03*
D12*
X0301841Y0711953D03*
X0301841Y0706953D03*
X0308691Y0621170D03*
X0308691Y0616170D03*
D13*
X0303691Y0613670D03*
X0313691Y0613670D03*
X0313691Y0623670D03*
X0303691Y0623670D03*
X0306841Y0704453D03*
X0306841Y0714453D03*
X0296841Y0714453D03*
X0296841Y0704453D03*
D14*
X0207746Y0503114D03*
X0207746Y0493114D03*
X0207746Y0483114D03*
X0207746Y0473114D03*
X0207746Y0463114D03*
X0207746Y0453114D03*
X0207746Y0443114D03*
X0207746Y0433114D03*
X0207746Y0423114D03*
X0207746Y0413114D03*
X0197746Y0413114D03*
X0197746Y0423114D03*
X0197746Y0433114D03*
X0197746Y0443114D03*
X0197746Y0453114D03*
X0197746Y0463114D03*
X0197746Y0473114D03*
X0197746Y0483114D03*
X0197746Y0493114D03*
X0197746Y0503114D03*
X0304833Y0320358D03*
X0304833Y0310358D03*
X0314833Y0310358D03*
X0324833Y0310358D03*
X0334833Y0310358D03*
X0344833Y0310358D03*
X0354833Y0310358D03*
X0364833Y0310358D03*
X0364833Y0320358D03*
X0354833Y0320358D03*
X0344833Y0320358D03*
X0334833Y0320358D03*
X0324833Y0320358D03*
X0314833Y0320358D03*
X0408730Y0365437D03*
X0408730Y0375437D03*
X0408730Y0385437D03*
X0408730Y0395437D03*
X0408730Y0405437D03*
X0408730Y0415437D03*
X0418730Y0415437D03*
X0418730Y0405437D03*
X0418730Y0395437D03*
X0418730Y0385437D03*
X0418730Y0375437D03*
X0418730Y0365437D03*
X0027589Y0712563D03*
X0027589Y0722563D03*
X0027589Y0732563D03*
X0027589Y0742563D03*
X0017589Y0742563D03*
X0017589Y0732563D03*
X0017589Y0722563D03*
X0017589Y0712563D03*
D15*
X0255423Y0498122D02*
X0255423Y0492122D01*
X0265423Y0492122D02*
X0265423Y0498122D01*
X0275423Y0498122D02*
X0275423Y0492122D01*
X0212116Y0375051D02*
X0212116Y0369051D01*
X0202116Y0369051D02*
X0202116Y0375051D01*
X0192116Y0375051D02*
X0192116Y0369051D01*
X0182116Y0369051D02*
X0182116Y0375051D01*
X0172116Y0375051D02*
X0172116Y0369051D01*
X0162116Y0369051D02*
X0162116Y0375051D01*
X0152116Y0375051D02*
X0152116Y0369051D01*
X0142116Y0369051D02*
X0142116Y0375051D01*
X0142471Y0345051D02*
X0142471Y0339051D01*
X0152471Y0339051D02*
X0152471Y0345051D01*
X0132471Y0345051D02*
X0132471Y0339051D01*
X0122471Y0339051D02*
X0122471Y0345051D01*
X0161762Y0322886D02*
X0161762Y0316886D01*
X0171762Y0316886D02*
X0171762Y0322886D01*
X0181762Y0322886D02*
X0181762Y0316886D01*
X0191762Y0316886D02*
X0191762Y0322886D01*
X0201762Y0322886D02*
X0201762Y0316886D01*
X0211762Y0316886D02*
X0211762Y0322886D01*
X0211526Y0341098D02*
X0211526Y0347098D01*
X0201526Y0347098D02*
X0201526Y0341098D01*
X0191526Y0341098D02*
X0191526Y0347098D01*
X0181526Y0347098D02*
X0181526Y0341098D01*
X0233959Y0320358D02*
X0239959Y0320358D01*
X0239959Y0310358D02*
X0233959Y0310358D01*
X0233959Y0300358D02*
X0239959Y0300358D01*
X0239959Y0290358D02*
X0233959Y0290358D01*
X0258368Y0300201D02*
X0264368Y0300201D01*
X0264368Y0310201D02*
X0258368Y0310201D01*
X0258368Y0320201D02*
X0264368Y0320201D01*
X0280219Y0319925D02*
X0286219Y0319925D01*
X0286219Y0309925D02*
X0280219Y0309925D01*
X0280219Y0299925D02*
X0286219Y0299925D01*
X0311833Y0750437D02*
X0317833Y0750437D01*
X0317833Y0760437D02*
X0311833Y0760437D01*
X0724510Y0209177D02*
X0730510Y0209177D01*
X0730510Y0199177D02*
X0724510Y0199177D01*
X0724510Y0189177D02*
X0730510Y0189177D01*
X0744982Y0189098D02*
X0750982Y0189098D01*
X0750982Y0199098D02*
X0744982Y0199098D01*
X0744982Y0209098D02*
X0750982Y0209098D01*
X0765848Y0208154D02*
X0771848Y0208154D01*
X0771848Y0198154D02*
X0765848Y0198154D01*
X0765848Y0188154D02*
X0771848Y0188154D01*
X0785730Y0187839D02*
X0791730Y0187839D01*
X0791730Y0197839D02*
X0785730Y0197839D01*
X0785730Y0207839D02*
X0791730Y0207839D01*
X0807384Y0208272D02*
X0813384Y0208272D01*
X0813384Y0198272D02*
X0807384Y0198272D01*
X0807384Y0188272D02*
X0813384Y0188272D01*
X0828447Y0188232D02*
X0834447Y0188232D01*
X0834447Y0198232D02*
X0828447Y0198232D01*
X0828447Y0208232D02*
X0834447Y0208232D01*
X0848919Y0208862D02*
X0854919Y0208862D01*
X0854919Y0198862D02*
X0848919Y0198862D01*
X0848919Y0188862D02*
X0854919Y0188862D01*
X0868408Y0188902D02*
X0874408Y0188902D01*
X0874408Y0198902D02*
X0868408Y0198902D01*
X0868408Y0208902D02*
X0874408Y0208902D01*
D16*
X0200452Y0203576D03*
X0197352Y0203576D03*
X0194252Y0203576D03*
X0194252Y0200476D03*
X0194252Y0197376D03*
X0197352Y0197376D03*
X0197352Y0200476D03*
X0200452Y0200476D03*
X0200452Y0197376D03*
X0199232Y0089009D03*
X0199232Y0085909D03*
X0199232Y0082809D03*
X0196132Y0082809D03*
X0193032Y0082809D03*
X0193032Y0085909D03*
X0196132Y0085909D03*
X0196132Y0089009D03*
X0193032Y0089009D03*
D17*
X0170226Y0114689D02*
X0170226Y0120594D01*
X0160226Y0120594D02*
X0160226Y0114689D01*
X0160226Y0061539D02*
X0160226Y0055634D01*
X0170226Y0055634D02*
X0170226Y0061539D01*
X0218888Y0062327D02*
X0218888Y0056421D01*
X0228888Y0056421D02*
X0228888Y0062327D01*
X0228888Y0115476D02*
X0228888Y0121382D01*
X0218888Y0121382D02*
X0218888Y0115476D01*
X0219282Y0173744D02*
X0219282Y0179650D01*
X0229282Y0179650D02*
X0229282Y0173744D01*
X0278337Y0173744D02*
X0278337Y0179650D01*
X0288337Y0179650D02*
X0288337Y0173744D01*
X0287943Y0121382D02*
X0287943Y0115476D01*
X0277943Y0115476D02*
X0277943Y0121382D01*
X0277943Y0062327D02*
X0277943Y0056421D01*
X0287943Y0056421D02*
X0287943Y0062327D01*
X0330954Y0077375D02*
X0330954Y0083281D01*
X0340954Y0083281D02*
X0340954Y0077375D01*
X0350954Y0077375D02*
X0350954Y0083281D01*
X0369728Y0082963D02*
X0369728Y0077058D01*
X0379728Y0077058D02*
X0379728Y0082963D01*
X0389728Y0082963D02*
X0389728Y0077058D01*
X0409412Y0076750D02*
X0409412Y0082655D01*
X0419412Y0082655D02*
X0419412Y0076750D01*
X0429412Y0076750D02*
X0429412Y0082655D01*
X0447944Y0082895D02*
X0447944Y0076990D01*
X0457944Y0076990D02*
X0457944Y0082895D01*
X0467944Y0082895D02*
X0467944Y0076990D01*
X0527441Y0077392D02*
X0527441Y0083297D01*
X0537441Y0083297D02*
X0537441Y0077392D01*
X0547441Y0077392D02*
X0547441Y0083297D01*
X0566712Y0083508D02*
X0566712Y0077602D01*
X0576712Y0077602D02*
X0576712Y0083508D01*
X0586712Y0083508D02*
X0586712Y0077602D01*
X0606046Y0076755D02*
X0606046Y0082661D01*
X0616046Y0082661D02*
X0616046Y0076755D01*
X0626046Y0076755D02*
X0626046Y0082661D01*
X0645054Y0083075D02*
X0645054Y0077169D01*
X0655054Y0077169D02*
X0655054Y0083075D01*
X0665054Y0083075D02*
X0665054Y0077169D01*
X0664969Y0214900D02*
X0664969Y0220805D01*
X0654969Y0220805D02*
X0654969Y0214900D01*
X0644969Y0214900D02*
X0644969Y0220805D01*
X0625663Y0220802D02*
X0625663Y0214896D01*
X0615663Y0214896D02*
X0615663Y0220802D01*
X0605663Y0220802D02*
X0605663Y0214896D01*
X0586764Y0214893D02*
X0586764Y0220798D01*
X0576764Y0220798D02*
X0576764Y0214893D01*
X0566764Y0214893D02*
X0566764Y0220798D01*
X0547034Y0220947D02*
X0547034Y0215041D01*
X0537034Y0215041D02*
X0537034Y0220947D01*
X0527034Y0220947D02*
X0527034Y0215041D01*
X0468617Y0215083D02*
X0468617Y0220989D01*
X0458617Y0220989D02*
X0458617Y0215083D01*
X0448617Y0215083D02*
X0448617Y0220989D01*
X0429181Y0221390D02*
X0429181Y0215484D01*
X0419181Y0215484D02*
X0419181Y0221390D01*
X0409181Y0221390D02*
X0409181Y0215484D01*
X0389141Y0215373D02*
X0389141Y0221279D01*
X0379141Y0221279D02*
X0379141Y0215373D01*
X0369141Y0215373D02*
X0369141Y0221279D01*
X0349931Y0221316D02*
X0349931Y0215410D01*
X0339931Y0215410D02*
X0339931Y0221316D01*
X0329931Y0221316D02*
X0329931Y0215410D01*
X0288337Y0232799D02*
X0288337Y0238705D01*
X0278337Y0238705D02*
X0278337Y0232799D01*
X0229282Y0232799D02*
X0229282Y0238705D01*
X0219282Y0238705D02*
X0219282Y0232799D01*
X0171014Y0232602D02*
X0171014Y0238508D01*
X0161014Y0238508D02*
X0161014Y0232602D01*
X0161014Y0179453D02*
X0161014Y0173547D01*
X0171014Y0173547D02*
X0171014Y0179453D01*
X0111959Y0179453D02*
X0111959Y0173547D01*
X0101959Y0173547D02*
X0101959Y0179453D01*
X0101959Y0232602D02*
X0101959Y0238508D01*
X0111959Y0238508D02*
X0111959Y0232602D01*
X0028219Y0228705D02*
X0022313Y0228705D01*
X0022313Y0238705D02*
X0028219Y0238705D01*
X0028297Y0192524D02*
X0022392Y0192524D01*
X0022392Y0182524D02*
X0028297Y0182524D01*
X0101171Y0120594D02*
X0101171Y0114689D01*
X0111171Y0114689D02*
X0111171Y0120594D01*
X0111171Y0061539D02*
X0111171Y0055634D01*
X0101171Y0055634D02*
X0101171Y0061539D01*
X0028219Y0063902D02*
X0022313Y0063902D01*
X0022313Y0073902D02*
X0028219Y0073902D01*
X0026526Y0338626D02*
X0020620Y0338626D01*
X0020620Y0348626D02*
X0026526Y0348626D01*
X0024557Y0553823D02*
X0030463Y0553823D01*
X0030463Y0563823D02*
X0024557Y0563823D01*
X0027313Y0611539D02*
X0021408Y0611539D01*
X0021408Y0621539D02*
X0027313Y0621539D01*
X0026762Y0646736D02*
X0020856Y0646736D01*
X0020856Y0656736D02*
X0026762Y0656736D01*
X0027313Y0675791D02*
X0021408Y0675791D01*
X0021408Y0685791D02*
X0027313Y0685791D01*
X0026526Y0867681D02*
X0032431Y0867681D01*
X0032431Y0877681D02*
X0026526Y0877681D01*
X0026919Y0995122D02*
X0021014Y0995122D01*
X0021014Y1005122D02*
X0026919Y1005122D01*
X0467313Y1096106D02*
X0467313Y1102012D01*
X0477313Y1102012D02*
X0477313Y1096106D01*
X0711014Y1093429D02*
X0711014Y1087524D01*
X0721014Y1087524D02*
X0721014Y1093429D01*
X0735187Y1123075D02*
X0735187Y1128980D01*
X0745187Y1128980D02*
X0745187Y1123075D01*
X0752352Y1093429D02*
X0752352Y1087524D01*
X0762352Y1087524D02*
X0762352Y1093429D01*
X0796289Y1095909D02*
X0796289Y1090004D01*
X0806289Y1090004D02*
X0806289Y1095909D01*
X0956171Y1093154D02*
X0956171Y1099059D01*
X0966171Y1099059D02*
X0966171Y1093154D01*
X0974793Y0429610D02*
X0980699Y0429610D01*
X0980699Y0419610D02*
X0974793Y0419610D01*
X0974478Y0398232D02*
X0980384Y0398232D01*
X0980384Y0388232D02*
X0974478Y0388232D01*
X0973219Y0312720D02*
X0979124Y0312720D01*
X0979124Y0302720D02*
X0973219Y0302720D01*
X0973415Y0249492D02*
X0979321Y0249492D01*
X0979321Y0239492D02*
X0973415Y0239492D01*
X0973415Y0179492D02*
X0979321Y0179492D01*
X0979321Y0169492D02*
X0973415Y0169492D01*
X0749045Y0308665D02*
X0749045Y0314571D01*
X0739045Y0314571D02*
X0739045Y0308665D01*
X0735463Y0356697D02*
X0735463Y0362602D01*
X0745463Y0362602D02*
X0745463Y0356697D01*
X0495226Y0360634D02*
X0495226Y0366539D01*
X0485226Y0366539D02*
X0485226Y0360634D01*
D18*
X0026762Y0416303D03*
X0026762Y0433626D03*
X0026644Y0800555D03*
X0026644Y0817878D03*
X0664636Y1135161D03*
X0671919Y1135358D03*
X0680778Y1130634D03*
X0760896Y1142642D03*
X0812274Y1132209D03*
X0821919Y1136539D03*
X0829400Y1136933D03*
D19*
X0749400Y0714650D03*
X0749203Y0710319D03*
X0744872Y0710122D03*
X0744872Y0714650D03*
X0740345Y0714846D03*
X0740345Y0710319D03*
X0740345Y0705594D03*
X0745069Y0705201D03*
X0749203Y0705594D03*
X0405482Y0880634D03*
X0397510Y0884669D03*
X0390915Y0884669D03*
X0390719Y0530535D03*
X0396132Y0530339D03*
X0403514Y0526008D03*
X0179597Y0141657D03*
X0171132Y0138508D03*
X0161880Y0140673D03*
X0153219Y0137327D03*
X0051250Y0096579D03*
X0043967Y0037720D03*
D20*
X0447411Y0539197D03*
X0348199Y0847760D03*
X0380778Y0891165D03*
X0348297Y0964098D03*
D21*
X0121992Y0620994D03*
X0429400Y0262524D03*
X0531368Y0259374D03*
X0558927Y0029059D03*
M02*
@@ -0,0 +1,105 @@
"Qty";"Value";"Device";"Package";"Parts";"Description";"AVAILABILITY";"CHECK_PRICES";"COPYRIGHT";"DATASHEET";"DESCRIPTION";"HEIGHT";"MANUFACTURER_NAME";"MANUFACTURER_PART_NUMBER";"MF";"MFR_NAME";"MOUSER_PART_NUMBER";"MOUSER_PRICE-STOCK";"MP";"MPN";"OC_FARNELL";"OC_NEWARK";"PACKAGE";"POPULARITY";"PRICE";"PROD_ID";"REFDES";"SNAPEDA_LINK";"SPICEMODEL";"SPICEPREFIX";"TYPE";"VALUE";
"11";"";"L-EUL5650M";"L5650M";"L1, L11, L12, L13, L14, L15, L16, L17, L18, L21, L23";"INDUCTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"0";"";"";"";"";"";"L";"";"";
"1";"";"MA10-2";"MA10-2";"SV1";"PIN HEADER";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"unknown";"unknown";"";"3";"";"";"";"";"";"";"";"";
"1";"";"PINHD-1X2";"1X02";"JP20";"PIN HEADER";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"98";"";"";"";"";"";"";"";"";
"11";"";"PINHD-1X3";"1X03";"JP4, JP5, JP6, JP10, JP11, JP12, JP14, JP15, JP16, JP17, JP19";"PIN HEADER";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"92";"";"";"";"";"";"";"";"";
"3";"";"PINHD-1X4";"1X04";"JP8, JP9, JP18";"PIN HEADER";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"91";"";"";"";"";"";"";"";"";
"1";"";"PINHD-1X6";"1X06";"JP2";"PIN HEADER";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"79";"";"";"";"";"";"";"";"";
"1";"";"PINHD-1X8";"1X08";"JP7";"PIN HEADER";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"67";"";"";"";"";"";"";"";"";
"1";"";"PINHD-2X4";"2X04";"JP3";"PIN HEADER";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"47";"";"";"";"";"";"";"";"";
"1";"";"PINHD-2X6";"2X06";"JP1";"PIN HEADER";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"8";"";"";"";"";"";"";"";"";
"1";"";"PINHD-2X7";"2X07";"JP13";"PIN HEADER";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"8";"";"";"";"";"";"";"";"";
"1";"";"SJ2W";"SJ_2";"SJ1";"SMD solder JUMPER";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"7";"";"";"";"";"";"";"";"";
"71";"0.1uF";"CC0201";"C0201";"C1, C2, C4, C5, C6, C7, C8, C9, C10, C11, C12, C13, C14, C15, C16, C36, C38, C39, C41, C42, C44, C45, C46, C47, C51, C52, C53, C56, C67, C69, C74, C80, C82, C125, C131, C133, C138, C140, C146, C148, C159, C160, C162, C168, C170, C175, C188, C189, C190, C192, C193, C194, C195, C196, C201, C203, C208, C210, C215, C217, C222, C224, C229, C231, C236, C238, C243, C245, C250, C252, C293";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"4";"0.1µF";"C-EUC0402";"C0402";"C48, C49, C57, C58";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"18";"";"";"";"";"";"C";"";"";
"6";"0.1µF";"CC0201";"C0201";"C277, C278, C295, C297, C299, C301";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"1";"0.2pF";"C-EUC0201";"C0201";"C43";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"0";"";"";"";"";"";"C";"";"";
"13";"0.47uF";"CC0201";"C0201";"C110, C111, C112, C113, C155, C156, C157, C158, C179, C180, C181, C182, C291";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"1";"0.6pF";"C-EUC0201";"C0201";"C54";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"0";"";"";"";"";"";"C";"";"";
"4";"0R";"RR0201";"R0201";"R18, R19, R34, R35";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"12";"100R";"RR0201";"R0201";"R2, R3, R4, R5, R6, R7, R8, R9, R10, R11, R12, R173";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"28";"100nF";"C-EUC0402";"C0402";"C76, C78, C258, C259, C260, C262, C264, C266, C313, C317, C320, C324, C327, C331, C334, C337, C339, C341, C342, C343, C344, C345, C346, C347, C348, C349, C350, C351";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"18";"";"";"";"";"";"C";"";"";
"24";"100nF";"CC0201";"C0201";"C24, C25, C26, C27, C28, C29, C30, C32, C33, C34, C35, C50, C256, C257, C279, C281, C298, C302, C303, C304, C305, C306, C307, C310";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"34";"100pF";"CC0201";"C0201";"C66, C68, C73, C79, C81, C124, C130, C132, C137, C139, C145, C147, C152, C161, C167, C169, C174, C183, C200, C202, C207, C209, C214, C216, C221, C223, C228, C230, C235, C237, C242, C244, C249, C251";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"2";"103pF";"CC0201";"C0201";"C60, C63";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"1";"106pF";"CC0201";"C0201";"C141";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"4";"107.3nH";"LL0201";"L0201";"L22, L25, L26, L27";"INDUCTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"L";"";"";
"9";"10k";"RR0201";"R0201";"R39, R40, R83, R84, R111, R123, R145, R151, R153";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"16";"10nF";"CC0201";"C0201";"C102, C103, C104, C105, C106, C107, C114, C115, C116, C117, C118, C119, C120, C121, C122, C123";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"2";"10uF";"CC0201";"C0201";"C37, C40";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"12";"10µF";"C-EUC0805";"C0805";"C75, C77, C312, C316, C319, C323, C326, C330, C333, C336, C338, C340";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"88";"";"";"";"";"";"C";"";"";
"1";"115R";"R-EU_R0201";"R0201";"R14";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"0";"";"";"";"";"";"R";"";"";
"2";"12nH";"LL0201";"L0201";"L2, L8";"INDUCTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"L";"";"";
"2";"12pF";"CC0201";"C0201";"C184, C185";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"37";"142-0731-211";"142-0731-211";"1420731211";"J1, J18, J20, J22, J23, J24, J25, J26, J27, J28, J29, J30, J31, J32, J33, J34, J35, J36, J37, J38, J39, J40, J41, J42, J43, J44, J45, J46, J47, J48, J49, J50, J51, J52, J53, J54, J55";"SMA Connector Jack, Female Socket 50 Ohms Through Hole Solder";"";"";"";"";"SMA Connector Jack, Female Socket 50 Ohms Through Hole Solder";"9.8852mm";"Cinch Connectivity Solutions";"142-0731-211";"";"";"530-142-0731-211";"https://www.mouser.co.uk/ProductDetail/Johnson-Cinch-Connectivity-Solutions/142-0731-211?qs=HFfMDpzxxd0OVzI3hm9tuA%3D%3D";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"2";"159nH";"LL0201";"L0201";"L3, L4";"INDUCTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"L";"";"";
"2";"18pF";"CC0201";"C0201";"C272, C274";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"1";"1k";"R-EU_R0402";"R0402";"R37";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"0";"";"";"";"";"";"R";"";"";
"49";"1k";"RR0201";"R0201";"R41, R43, R55, R56, R57, R58, R59, R61, R82, R85, R86, R87, R88, R93, R94, R99, R100, R101, R102, R107, R108, R109, R118, R124, R125, R126, R127, R128, R129, R130, R131, R133, R134, R135, R136, R137, R138, R139, R140, R144, R147, R148, R149, R167, R168, R169, R170, R171, R172";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"1";"1k2_1%";"RR0201";"R0201";"R60";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"7";"1nF";"C-EUC0402";"C0402";"C314, C318, C321, C325, C328, C332, C335";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"18";"";"";"";"";"";"C";"";"";
"51";"1pF";"CC0201";"C0201";"C70, C71, C72, C83, C84, C85, C126, C128, C129, C134, C135, C136, C142, C143, C144, C149, C150, C151, C163, C165, C166, C171, C172, C173, C197, C198, C199, C204, C205, C206, C211, C212, C213, C218, C219, C220, C225, C226, C227, C232, C233, C234, C239, C240, C241, C246, C247, C248, C253, C254, C255";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"16";"1uF";"CC0201";"C0201";"C86, C87, C88, C89, C90, C91, C92, C93, C94, C95, C96, C97, C98, C99, C100, C101";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"3";"1µF";"CC0201";"C0201";"C276, C296, C300";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"1";"2.2k";"RR0201";"R0201";"R146";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"3";"2.2uF";"CC0201";"C0201";"C22, C23, C164";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"16";"2.443k";"RR0201";"R0201";"R89, R90, R91, R92, R95, R96, R97, R98, R103, R104, R105, R106, R119, R120, R121, R122";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"1";"2.7pF";"C-EUC0402";"C0402";"C3";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"18";"";"";"";"";"";"C";"";"";
"2";"2.7pF";"CC0201";"C0201";"C18, C19";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"4";"200R";"RR0201";"R0201";"R16, R17, R20, R21";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"1";"20k";"R-EU_R0402";"R0402";"R38";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"0";"";"";"";"";"";"R";"";"";
"40";"22-23-2021";"22-23-2021";"22-23-2021";"X1, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16, X17, X18, X19, X20, X21, X22, X24, X54, X55, X56, X_1, X_2, X_3, X_4, X_5, X_6, X_7, X_8, X_9, X_10, X_11, X_12, X_13, X_14, X_15, X_16";".100" (2.54mm) Center Header - 2 Pin";"";"";"";"";"";"";"";"";"MOLEX";"";"";"";"";"22-23-2021";"1462926";"25C3832";"";"40";"";"";"";"";"";"";"";"";
"16";"22-23-2031";"22-23-2031";"22-23-2031";"X3, X38, X39, X40, X41, X42, X43, X44, X45, X46, X47, X48, X49, X50, X51, X52";".100" (2.54mm) Center Header - 3 Pin";"";"";"";"";"";"";"";"";"MOLEX";"";"";"";"";"22-23-2031";"1462950";"30C0862";"";"35";"";"";"";"";"";"";"";"";
"11";"22.1k";"RR0201";"R0201";"R154, R155, R156, R157, R158, R159, R160, R161, R162, R163, R164";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"13";"22R";"RR0201";"R0201";"R23, R24, R25, R26, R27, R28, R29, R30, R49, R51, R62, R63, R64";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"2";"22pF";"CC0201";"C0201";"C308, C309";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"5";"22µF";"C-EUC1206";"C1206";"C283, C311, C315, C322, C329";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"54";"";"";"";"";"";"C";"";"";
"2";"24R";"R-EU_R0402";"R0402";"R1, R13";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"0";"";"";"";"";"";"R";"";"";
"2";"25R";"RR0201";"R0201";"R165, R166";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"4";"25pF";"CC0201";"C0201";"C64, C65, C268, C270";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"1";"3.3uF";"CC0201";"C0201";"C191";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"2";"32.8pF";"CC0201";"C0201";"C59, C127";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"1";"3k2";"RR0201";"R0201";"R33";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"2";"4.3k";"R-EU_R0201";"R0201";"R15, R32";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"0";"";"";"";"";"";"R";"";"";
"2";"4.3pF";"CC0201";"C0201";"C20, C21";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"27";"4.7k";"RR0201";"R0201";"R42, R44, R45, R46, R47, R48, R50, R52, R53, R54, R65, R66, R67, R68, R69, R70, R71, R72, R73, R74, R75, R76, R77, R117, R141, R142, R143";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"16";"4.7nF";"CC0201";"C0201";"C261, C263, C265, C267, C269, C271, C273, C275, C280, C282, C284, C286, C288, C290, C292, C294";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"8";"4.7uF";"CC0201";"C0201";"C108, C109, C153, C154, C177, C178, C287, C289";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"2";"4.7uF 35V";"4.7UF-POLAR-EIA3528-35V-10%(TANT)";"EIA3528";"C186, C187";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"CAP-13916";"";"";"";"";"";"4.7uF 35V";
"1";"47nF";"CC0201";"C0201";"C31";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"4";"47uF";"CC0201";"C0201";"C17, C55, C176, C285";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"4";"500R";"RR0201";"R0201";"R110, R112, R113, R114";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"3";"50R";"RR0201";"R0201";"R31, R115, R116";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"4";"50nH";"LL0201";"L0201";"L9, L10, L24, L28";"INDUCTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"L";"";"";
"1";"56R";"R-EU_R0201";"R0201";"R22";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"0";"";"";"";"";"";"R";"";"";
"3";"5R";"RR0201";"R0201";"R132, R150, R152";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"2";"7.8pF";"CC0201";"C0201";"C61, C62";"CAPACITOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"C";"";"";
"1";"830R";"R-EU_R0402";"R0402";"R36";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"0";"";"";"";"";"";"R";"";"";
"4";"840R";"RR0201";"R0201";"R78, R79, R80, R81";"RESISTOR, European symbol";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"NONE";"R";"";"";
"2";"AD8352ACPZ-R7";"AD8352ACPZ-R7";"CP_16_3_ADI";"U4, U8";"";"";"";"Copyright (C) 2025 Ultra Librarian. All rights reserved.";"https://www.analog.com/media/en/technical-documentation/data-sheets/ad8352.pdf";"2 GHz Ultralow Distortion Differential RF/IF Amplifier";"";"Analog Devices Inc";"AD8352ACPZ-R7";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"1";"AD9484BCPZ-500";"AD9484BCPZ-500";"CP_56_5_ADI";"U1";"";"";"";"Copyright (C) 2025 Ultra Librarian. All rights reserved.";"https://www.analog.com/media/en/technical-documentation/data-sheets/AD9484.pdf";"8-Bit, 500 MSPS, 1.8 V Analog-to-Digital Converter";"";"Analog Devices Inc";"AD9484BCPZ-500";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"1";"AD9708AR";"AD9708AR";"RW_28_ADI";"U3";"";"";"";"Copyright (C) 2024 Ultra Librarian. All rights reserved.";"";"";"";"";"AD9708AR";"";"Analog Devices Inc";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"4";"ADAR1000ACCZN";"ADAR1000ACCZN";"CC-88-1_ADI";"ADAR1_, ADAR2_, ADAR3_, ADAR4_";"";"";"";"Copyright (C) 2024 Ultra Librarian. All rights reserved.";"";"";"";"";"ADAR1000ACCZN";"";"Analog Devices Inc";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"RF";"";
"3";"ADS7830IPWR";"ADS7830IPWR";"PW16";"U10, U88, U89";"";"";"";"Copyright (C) 2025 Ultra Librarian. All rights reserved.";"https://www.ti.com/lit/gpn/ads7830";"8-Bit, 8-Channel Sampling A/D Converter with I2C Interface 16-TSSOP -40 to 85";"";"Texas Instruments";"ADS7830IPWR";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"16";"ADTR1107ACCZ";"ADTR1107ACCZ";"CC-24-8_ADI";"ADTR1107_1, ADTR1107_2, ADTR1107_3, ADTR1107_4, ADTR1107_5, ADTR1107_6, ADTR1107_7, ADTR1107_8, ADTR1107_9, ADTR1107_10, ADTR1107_11, ADTR1107_12, ADTR1107_13, ADTR1107_14, ADTR1107_15, ADTR1107_16";"";"";"";"Copyright (C) 2024 Ultra Librarian. All rights reserved.";"";"";"";"";"ADTR1107ACCZ";"";"Analog Devices Inc";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"RF";"";
"1";"AT93C46A-10SQ-2.7";"AT93C46A-10SQ-2.7";"SOIC8";"IC1";"Three-wire Automotive Temperature Serial EEPROM 1K (64 x 16)";"";"";"";"";"";"";"";"";"";"";"";"";"";"AT93C46DN-SH-B";"1455086";"58M3879";"";"0";"";"";"";"";"";"";"";"";
"5";"BLM15HB121SN1";"BLM15HB121SN1";"0402";"L5, L6, L7, L19, L20";"EMIFIL (R) Chip Ferrite Bead for GHz Noise";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"0";"";"";"";"";"";"";"";"";
"2";"BPF2";"BPF2";"BPF2";"U$2, U$3";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"4";"Blue";"LED-BLUE0603";"LED-0603";"D2, D3, D4, D5";"Blue SMD LED";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"DIO-08575";"";"";"";"";"";"Blue";
"2";"CJT-T-P-HH-ST-TH1";"CJT-T-P-HH-ST-TH1";"CJTTPHHSTTH1";"J19, J21";"Conn Twinax F 0Hz to 4GHz 100Ohm Solder ST Thru-Hole Gold";"";"";"";"";"Conn Twinax F 0Hz to 4GHz 100Ohm Solder ST Thru-Hole Gold";"7.31mm";"SAMTEC";"CJT-T-P-HH-ST-TH1";"";"";"200-CJTTPHHSTTH1";"https://www.mouser.co.uk/ProductDetail/Samtec/CJT-T-P-HH-ST-TH1?qs=PB6%2FjmICvI3dfW8RDpxn0g%3D%3D";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"2";"DAC5578SRGET";"DAC5578SRGET";"RGE24_2P7X2P7";"U7, U69";"";"";"";"Copyright (C) 2025 Ultra Librarian. All rights reserved.";"https://www.ti.com/lit/gpn/dac5578";"8-bit, Octal Channel, Ultra-Low Glitch, Voltage Output, 2-Wire Interface DAC 24-VQFN -40 to 125";"";"Texas Instruments";"DAC5578SRGET";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"1";"ECS-120-10-36B2-JTN-TR";"CRYSTAL-12MHZ";"CRYSTAL-SMD-2X2.5MM";"Y1";"12.0MHz Crystal";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"XTAL-15540";"";"";"";"";"";"";
"1";"EP4RKU+";"EP4RKU+";"DG1677-2_MNC";"U16";"";"";"";"Copyright (C) 2024 Ultra Librarian. All rights reserved.";"";"";"";"";"EP4RKU+";"";"Mini Circuits";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"1";"FT2232HQ";"FT2232HQ";"64QFN_FT2232HQ_FTD";"U6";"";"";"";"Copyright (C) 2025 Ultra Librarian. All rights reserved.";"";"";"";"";"FT2232HQ";"";"FTDI, Future Technology Devices International Ltd";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"16";"INA241A3IDGKRDGK0008A-MFG";"INA241A3IDGKRDGK0008A-MFG";"DGK0008A-MFG";"U11, U73, U74, U75, U76, U77, U78, U79, U80, U81, U82, U83, U84, U85, U86, U87";"";"";"";"Copyright (C) 2025 Ultra Librarian. All rights reserved.";"";"-5-V to 110-V bidirectional ultraprecise current sense amplifier with enhanced PWM rejection 8-VSSOP -40 to 125";"";"Texas Instruments";"INA241A3IDGKR";"";"";"";"";"";"";"";"";"";"";"";"";"RefDes";"";"";"";"TYPE";"";
"2";"LTC5552IUDBTRMPBF";"LTC5552IUDBTRMPBF";"UDB_12_ADI";"U5, U13";"";"";"";"Copyright (C) 2024 Ultra Librarian. All rights reserved.";"";"";"";"";"LTC5552IUDB#TRMPBF";"";"Analog Devices Inc";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"17";"M3SWA2-34DR+";"M3SWA2-34DR+";"16_QFN";"RF_SW_1, RF_SW_2, RF_SW_3, RF_SW_4, RF_SW_5, RF_SW_6, RF_SW_7, RF_SW_8, RF_SW_9, RF_SW_10, RF_SW_11, RF_SW_12, RF_SW_13, RF_SW_14, RF_SW_15, RF_SW_16, U$1";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"2";"MINI-USB-32005-201";"MINI-USB-32005-201";"32005-201";"X2, X53";"MINI USB-B Conector";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"unknown";"unknown";"";"5";"";"";"";"";"";"";"";"";
"1";"MOMENTARY-SWITCH-SPST-SMD-4.6X2.8MM";"MOMENTARY-SWITCH-SPST-SMD-4.6X2.8MM";"TACTILE_SWITCH_SMD_4.6X2.8MM";"S1";"Momentary Switch (Pushbutton) - SPST";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"SWCH-15606";"";"";"";"";"";"";
"1";"MT25QL01GBBB8E12-0AUT";"MT25QL01GBBB8E12-0AUT";"BGA24_MT25QL_MRN";"U9";"";"";"";"Copyright (C) 2024 Ultra Librarian. All rights reserved.";"";"";"";"";"MT25QL01GBBB8E12-0AUT";"";"Micron";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"1";"NX3215SA-32.768KHz";"NX3225GD-8MHZ-STD-CRA-3";"XTAL_NX3225GD-8MHZ-STD-CRA-3_N";"XTAL3";"";"";"";"Copyright (C) 2024 Ultra Librarian. All rights reserved.";"";"";"";"";"NX3225GD-8MHZ-STD-CRA-3";"";"NDK";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"1";"NX3225GD-8MHZ-STD-CRA-3";"NX3225GD-8MHZ-STD-CRA-3";"XTAL_NX3225GD-8MHZ-STD-CRA-3_N";"XTAL1";"";"";"";"Copyright (C) 2024 Ultra Librarian. All rights reserved.";"";"";"";"";"NX3225GD-8MHZ-STD-CRA-3";"";"NDK";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"4";"OPA4703EA/250";"OPA4703EA/250";"PW14";"OPA_1, OPA_2, OPA_3, OPA_4";"";"";"";"Copyright (C) 2025 Ultra Librarian. All rights reserved.";"https://www.ti.com/lit/gpn/opa4703";"Quad, 12-V, 1-MHz, low-offset operational amplifier 14-TSSOP -40 to 85";"";"Texas Instruments";"OPA4703EA/250";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"1";"STM32F746ZGT7";"STM32F746ZGT7";"LQFP-144_STM";"U2";"";"";"";"Copyright (C) 2024 Ultra Librarian. All rights reserved.";"";"";"";"";"STM32F746ZGT7";"";"STMicroelectronics";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"34";"SZMMSZ5232BT1G";"SZMMSZ5232BT1G";"SOD-123_ONS";"U14, U15, U17, U37, U38, U39, U40, U41, U43, U44, U45, U46, U47, U48, U49, U50, U51, U52, U53, U54, U55, U56, U57, U58, U59, U60, U61, U62, U63, U64, U65, U66, U67, U68";"";"";"";"Copyright (C) 2024 Ultra Librarian. All rights reserved.";"";"";"";"";"SZMMSZ5232BT1G";"";"onsemi";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";
"1";"XC7A50T-2FTG256I";"XC7A50T-2FTG256I";"BGA256C100P16X16_1700X1700X155";"U42";"Artix-7 Field Programmable Gate Array (FPGA) IC 170 2764800 52160 256-LBGA Check availability";"In Stock";"https://www.snapeda.com/parts/XC7A50T-2FTG256I/Xilinx/view-part/?ref=eda";"";"";" Artix-7 Field Programmable Gate Array (FPGA) IC 170 2764800 52160 256-LBGA ";"";"";"";"Xilinx Inc.";"";"";"";"XC7A50T-2FTG256I";"";"";"";"LBGA-256 Xilinx Inc.";"";"None";"";"";"https://www.snapeda.com/parts/XC7A50T-2FTG256I/Xilinx/view-part/?ref=snap";"";"";"";"";
@@ -0,0 +1,86 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 5 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Symbol /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
6 0 obj
<<
/Contents 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 9 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/PageMode /UseNone /Pages 9 0 R /Type /Catalog
>>
endobj
8 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20250912164324+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20250912164324+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
9 0 obj
<<
/Count 1 /Kids [ 6 0 R ] /Type /Pages
>>
endobj
10 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2059
>>
stream
Gatm<95iQE&AIm?b[ggQ==YXc8To\KZq6bSU6!eY>+k@RO_J$Z,cW&R1jdm4^-h/tfnksI].j0B@qI^lc,U]!+S_])1J.[Scf+XN"j15e!m2MnoCq`dro1s+>-A>]&C@C4R7o!<""/nq.+U%j05\Z_+6G#p@R);1(1Th^kY,U"O@^X^n1Mu)1<^pC+/KaW?n?i]-gl<A2(V39p]H=>#s3b7GF6-u;49+tRQkK5B7+Q#7D.3OSLHemSi)nEp2YNRM7*(MLXAha_n"-&S>[V0hP@2_96O]s;sOMN?=HkjG/.c"+$qQ:3)!(T-J+tgN@1eoHi,Uga!)+X0&d-\R40.g)BErQ<4pN7r+9qj!j:S\34=cIX)Yi[Q%2itNEk&+)\dBsa@I`09&^MCM3$d\`5Ch`QY-jUTa=o+1qea@S'2/S!#d;0STUqqn]406d^1'64Aea4PBed<#:X>?B5I'8\AklQ%f^L"0_'EE\r*grD%KA!$Jbr4''_G\GE'4taV&77:u.Wac8*>kaS\VgOgDNiM\$C.neN$qENsqtR95;4+@8l>i&8kQ=XmMiZHSV()+n,Q"eIlfLMN$/:iD<:,eOr4+Ar9M&#gUo%n,\F/Cm/^,Lg@dmhuXgFu2/l(3#>OmPjFTE1?.q%g\+k7j2:5Em(24pf5C)PoVr7@'25+FH0`cDU\r1aO/*HK(iWtYI:aIZOm;pD#IsrN'oo;BqXO)W>p?B5uKjN+a<M3di*W'XS=:K9-VA">9\`X-J88t<P,sW[)b>M+A03\RFqFkn*dYcB,C@+Y46jR4c_f?\?]V?*[4f5SL)hIeiq-T"_%J4$2dZ&Auo0Q^?a=A+%);hH_JU\6_TC5>*q^3$R(Y)MYh9hVK#R+KliIgdllm?F#pAOn&43YR$=!0+X1)IQK0gH["=Poi:?7-fC0j,?@;f*O^^_a*%RH4$4&BRFbs7N(Ls=4ZHSFoSR*OX_?EjKgo[()R?[;&GgjQ.N_`I\E,&hX8uXSjFY'ks!DS"-"Oq4Qmg\K93+;IIIYXK[\YI7mggR#/[!si8(9!X2:_R0'%c/l=M]r=6\OLVTO28_RLX:dZ*j/>8#]kXlE!]q_T/N/E3f!G9s2B[5_9&@Vd-hS14,C8$a4eDHg?SG!W,;3B_0"rORelB^g/@Ag6O*l/ZQcf&JNpa<B81R9Z!C7%DXD3Gd5%ekkV0(a8Meq\,kWVnk.;AO-8fj>3eQ4iak(A7T:f+AR.eq,bF^GfT;L(=#o5`C(6J%-ju\1h7Yi&PC^H)g627$<\GTY_%I5^W/Cu<iHO4L^RH+-G\[s->X(LkS@0G^q!io2r*.]j03\G>OhjHR%5>)l<W96$r41RMaNDCo3BiVhAR:E@SV1L9lH?SYI?o7'o:-TP>?8lk/d[+=jTn-:IHUN5h(:ZN_LMU[>CK$ko6^kbjb4t=#.H@ac;T<>[Mk2J2beul7eqFY%(/1f)Ib)b`cKd%C6U_NTp1>cF&'`Oj/8_4</$[;9.VrcP4hQjBoI4WC>I>rYO@`8o&o2Wk&rFS&e$A1sYa8\*oslpPcreJr/W?@$N3?dUnesS:m*gVaV]^GQ=P)HMTdbe:WTi=RZ.4N@_k'(4gNtdifc!kfToM#Y4[<4*h@!3`;:@d6%\j.mEUi5LfQ<\P0?\.LQt?J*>8c7qY14WaW*q0q[[h3G5$8YODC\no6(-E;[1H?'\?n$;0#(_/\R8uS[).&)&B"oJ-'fojE#`;)AD1>rk[jA'oV@eHH4SPq`Ps-WmIM/8I"6\e@^iE8e.h&tdc5Ij/bPuh$DZUWfQ,G76m=PV1RaoTGRYg)T,c4_/\>lZ@O0E)KD9/![KPS)qR>%>(\6,o!HD/dm]C<8Z_2d9K-]_'qB5%HFpl?3JTn>DhDIh)<RV]X:Ja=L>]#8u/h?regcf)G3u&,s'?pEef1rShR\9Zth+)!SF^-B0D0o:+i6>2q],dRi\A#E]28O%b0EQ61O^i2h`\[&N<s/?UHG="n9Yh1T:3RbUZmaO;O[5qcf4CI[):`i3[oG4u)\]V=6;Uit=G>@_fN]2<-dM5@?T@%+D#~>endstream
endobj
xref
0 11
0000000000 65535 f
0000000073 00000 n
0000000134 00000 n
0000000241 00000 n
0000000353 00000 n
0000000430 00000 n
0000000545 00000 n
0000000749 00000 n
0000000817 00000 n
0000001100 00000 n
0000001159 00000 n
trailer
<<
/ID
[<b8f8c11bfd0a1bed1b3e2009136b9939><b8f8c11bfd0a1bed1b3e2009136b9939>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 8 0 R
/Root 7 0 R
/Size 11
>>
startxref
3310
%%EOF
+438
View File
@@ -0,0 +1,438 @@
#!/usr/bin/env python3
"""
AERIS-10 FMC Anti-Alias Filter — openEMS 3D EM Simulation
==========================================================
5th-order differential Butterworth LC LPF, fc ≈ 195 MHz
All components are 0402 (1.0 x 0.5 mm) on FR4 4-layer stackup.
Filter topology (each half of differential):
IN → R_series(49.9Ω) → L1(24nH) → C1(27pF)↓GND → L2(82nH) → C2(27pF)↓GND → L3(24nH) → OUT
Plus R_diff(100Ω) across input and output differential pairs.
PCB stackup:
L1: F.Cu (signal + components) — 35µm copper
Prepreg: 0.2104 mm
L2: In1.Cu (GND plane) — 35µm copper
Core: 1.0 mm
L3: In2.Cu (Power plane) — 35µm copper
Prepreg: 0.2104 mm
L4: B.Cu (signal) — 35µm copper
Total board thickness ≈ 1.6 mm
Differential trace: W=0.23mm, S=0.12mm gap → Zdiff≈100Ω
All 0402 pads: 0.5mm x 0.55mm with 0.5mm gap between pads
Simulation extracts 4-port S-parameters (differential in → differential out)
then converts to mixed-mode (Sdd11, Sdd21, Scc21) for analysis.
"""
import os
import sys
import numpy as np
sys.path.insert(0, '/Users/ganeshpanth/openEMS-Project/CSXCAD/python')
sys.path.insert(0, '/Users/ganeshpanth/openEMS-Project/openEMS/python')
os.environ['PATH'] = '/Users/ganeshpanth/opt/openEMS/bin:' + os.environ.get('PATH', '')
from CSXCAD import ContinuousStructure
from openEMS import openEMS
from openEMS.physical_constants import C0, EPS0
unit = 1e-3
f_start = 1e6
f_stop = 1e9
f_center = 150e6
f_IF_low = 120e6
f_IF_high = 180e6
max_res = C0 / f_stop / unit / 20
copper_t = 0.035
prepreg_t = 0.2104
core_t = 1.0
sub_er = 4.3
sub_tand = 0.02
cu_cond = 5.8e7
z_L4_bot = 0.0
z_L4_top = z_L4_bot + copper_t
z_pre2_top = z_L4_top + prepreg_t
z_L3_top = z_pre2_top + copper_t
z_core_top = z_L3_top + core_t
z_L2_top = z_core_top + copper_t
z_pre1_top = z_L2_top + prepreg_t
z_L1_bot = z_pre1_top
z_L1_top = z_L1_bot + copper_t
pad_w = 0.50
pad_l = 0.55
pad_gap = 0.50
comp_pitch = 1.5
trace_w = 0.23
trace_s = 0.12
pair_pitch = trace_w + trace_s
R_series = 49.9
R_diff_in = 100.0
R_diff_out = 100.0
L1_val = 24e-9
L2_val = 82e-9
L3_val = 24e-9
C1_val = 27e-12
C2_val = 27e-12
FDTD = openEMS(NrTS=50000, EndCriteria=1e-5)
FDTD.SetGaussExcite(0.5 * (f_start + f_stop), 0.5 * (f_stop - f_start))
FDTD.SetBoundaryCond(['PML_8'] * 6)
CSX = ContinuousStructure()
FDTD.SetCSX(CSX)
copper = CSX.AddMetal('copper')
gnd_metal = CSX.AddMetal('gnd_plane')
fr4_pre1 = CSX.AddMaterial(
'prepreg1', epsilon=sub_er, kappa=sub_tand * 2 * np.pi * f_center * EPS0 * sub_er
)
fr4_core = CSX.AddMaterial(
'core', epsilon=sub_er, kappa=sub_tand * 2 * np.pi * f_center * EPS0 * sub_er
)
fr4_pre2 = CSX.AddMaterial(
'prepreg2', epsilon=sub_er, kappa=sub_tand * 2 * np.pi * f_center * EPS0 * sub_er
)
y_P = +pair_pitch / 2
y_N = -pair_pitch / 2
x_port_in = -1.0
x_R_series = 0.0
x_L1 = x_R_series + comp_pitch
x_C1 = x_L1 + comp_pitch
x_L2 = x_C1 + comp_pitch
x_C2 = x_L2 + comp_pitch
x_L3 = x_C2 + comp_pitch
x_port_out = x_L3 + comp_pitch + 1.0
x_Rdiff_in = x_port_in - 0.5
x_Rdiff_out = x_port_out + 0.5
margin = 3.0
x_min = x_Rdiff_in - margin
x_max = x_Rdiff_out + margin
y_min = y_N - margin
y_max = y_P + margin
z_min = z_L4_bot - margin
z_max = z_L1_top + margin
fr4_pre1.AddBox([x_min, y_min, z_L2_top], [x_max, y_max, z_L1_bot], priority=1)
fr4_core.AddBox([x_min, y_min, z_L3_top], [x_max, y_max, z_core_top], priority=1)
fr4_pre2.AddBox([x_min, y_min, z_L4_top], [x_max, y_max, z_pre2_top], priority=1)
gnd_metal.AddBox(
[x_min + 0.5, y_min + 0.5, z_core_top], [x_max - 0.5, y_max - 0.5, z_L2_top], priority=10
)
def add_trace_segment(x_start, x_end, y_center, z_bot, z_top, w, metal, priority=20):
metal.AddBox(
[x_start, y_center - w / 2, z_bot], [x_end, y_center + w / 2, z_top], priority=priority
)
def add_0402_pads(x_center, y_center, z_bot, z_top, metal, priority=20):
x_left = x_center - pad_gap / 2 - pad_w / 2
metal.AddBox(
[x_left - pad_w / 2, y_center - pad_l / 2, z_bot],
[x_left + pad_w / 2, y_center + pad_l / 2, z_top],
priority=priority,
)
x_right = x_center + pad_gap / 2 + pad_w / 2
metal.AddBox(
[x_right - pad_w / 2, y_center - pad_l / 2, z_bot],
[x_right + pad_w / 2, y_center + pad_l / 2, z_top],
priority=priority,
)
return (x_left, x_right)
def add_lumped_element(
CSX, name, element_type, value, x_center, y_center, z_bot, z_top, direction='x'
):
x_left = x_center - pad_gap / 2 - pad_w / 2
x_right = x_center + pad_gap / 2 + pad_w / 2
if direction == 'x':
start = [x_left, y_center - pad_l / 4, z_bot]
stop = [x_right, y_center + pad_l / 4, z_top]
edir = 'x'
elif direction == 'y':
start = [x_center - pad_l / 4, y_center - pad_gap / 2 - pad_w / 2, z_bot]
stop = [x_center + pad_l / 4, y_center + pad_gap / 2 + pad_w / 2, z_top]
edir = 'y'
if element_type == 'R':
elem = CSX.AddLumpedElement(name, ny=edir, caps=True, R=value)
elif element_type == 'L':
elem = CSX.AddLumpedElement(name, ny=edir, caps=True, L=value)
elif element_type == 'C':
elem = CSX.AddLumpedElement(name, ny=edir, caps=True, C=value)
elem.AddBox(start, stop, priority=30)
return elem
def add_shunt_cap(
CSX, name, value, x_center, y_trace, _z_top_signal, _z_gnd_top, metal, priority=20,
):
metal.AddBox(
[x_center - pad_w / 2, y_trace - pad_l / 2, z_L1_bot],
[x_center + pad_w / 2, y_trace + pad_l / 2, z_L1_top],
priority=priority,
)
via_drill = 0.15
cap = CSX.AddLumpedElement(name, ny='z', caps=True, C=value)
cap.AddBox(
[x_center - via_drill, y_trace - via_drill, z_L2_top],
[x_center + via_drill, y_trace + via_drill, z_L1_bot],
priority=30,
)
via_metal = CSX.AddMetal(name + '_via')
via_metal.AddBox(
[x_center - via_drill, y_trace - via_drill, z_L2_top],
[x_center + via_drill, y_trace + via_drill, z_L1_bot],
priority=25,
)
add_trace_segment(
x_port_in, x_R_series - pad_gap / 2 - pad_w, y_P, z_L1_bot, z_L1_top, trace_w, copper
)
add_0402_pads(x_R_series, y_P, z_L1_bot, z_L1_top, copper)
add_lumped_element(CSX, 'R10', 'R', R_series, x_R_series, y_P, z_L1_bot, z_L1_top)
add_trace_segment(
x_R_series + pad_gap / 2 + pad_w,
x_L1 - pad_gap / 2 - pad_w,
y_P,
z_L1_bot,
z_L1_top,
trace_w,
copper,
)
add_0402_pads(x_L1, y_P, z_L1_bot, z_L1_top, copper)
add_lumped_element(CSX, 'L5', 'L', L1_val, x_L1, y_P, z_L1_bot, z_L1_top)
add_trace_segment(x_L1 + pad_gap / 2 + pad_w, x_C1, y_P, z_L1_bot, z_L1_top, trace_w, copper)
add_shunt_cap(CSX, 'C53', C1_val, x_C1, y_P, z_L1_top, z_L2_top, copper)
add_trace_segment(x_C1, x_L2 - pad_gap / 2 - pad_w, y_P, z_L1_bot, z_L1_top, trace_w, copper)
add_0402_pads(x_L2, y_P, z_L1_bot, z_L1_top, copper)
add_lumped_element(CSX, 'L8', 'L', L2_val, x_L2, y_P, z_L1_bot, z_L1_top)
add_trace_segment(x_L2 + pad_gap / 2 + pad_w, x_C2, y_P, z_L1_bot, z_L1_top, trace_w, copper)
add_shunt_cap(CSX, 'C55', C2_val, x_C2, y_P, z_L1_top, z_L2_top, copper)
add_trace_segment(x_C2, x_L3 - pad_gap / 2 - pad_w, y_P, z_L1_bot, z_L1_top, trace_w, copper)
add_0402_pads(x_L3, y_P, z_L1_bot, z_L1_top, copper)
add_lumped_element(CSX, 'L10', 'L', L3_val, x_L3, y_P, z_L1_bot, z_L1_top)
add_trace_segment(x_L3 + pad_gap / 2 + pad_w, x_port_out, y_P, z_L1_bot, z_L1_top, trace_w, copper)
add_trace_segment(
x_port_in, x_R_series - pad_gap / 2 - pad_w, y_N, z_L1_bot, z_L1_top, trace_w, copper
)
add_0402_pads(x_R_series, y_N, z_L1_bot, z_L1_top, copper)
add_lumped_element(CSX, 'R11', 'R', R_series, x_R_series, y_N, z_L1_bot, z_L1_top)
add_trace_segment(
x_R_series + pad_gap / 2 + pad_w,
x_L1 - pad_gap / 2 - pad_w,
y_N,
z_L1_bot,
z_L1_top,
trace_w,
copper,
)
add_0402_pads(x_L1, y_N, z_L1_bot, z_L1_top, copper)
add_lumped_element(CSX, 'L6', 'L', L1_val, x_L1, y_N, z_L1_bot, z_L1_top)
add_trace_segment(x_L1 + pad_gap / 2 + pad_w, x_C1, y_N, z_L1_bot, z_L1_top, trace_w, copper)
add_shunt_cap(CSX, 'C54', C1_val, x_C1, y_N, z_L1_top, z_L2_top, copper)
add_trace_segment(x_C1, x_L2 - pad_gap / 2 - pad_w, y_N, z_L1_bot, z_L1_top, trace_w, copper)
add_0402_pads(x_L2, y_N, z_L1_bot, z_L1_top, copper)
add_lumped_element(CSX, 'L7', 'L', L2_val, x_L2, y_N, z_L1_bot, z_L1_top)
add_trace_segment(x_L2 + pad_gap / 2 + pad_w, x_C2, y_N, z_L1_bot, z_L1_top, trace_w, copper)
add_shunt_cap(CSX, 'C56', C2_val, x_C2, y_N, z_L1_top, z_L2_top, copper)
add_trace_segment(x_C2, x_L3 - pad_gap / 2 - pad_w, y_N, z_L1_bot, z_L1_top, trace_w, copper)
add_0402_pads(x_L3, y_N, z_L1_bot, z_L1_top, copper)
add_lumped_element(CSX, 'L9', 'L', L3_val, x_L3, y_N, z_L1_bot, z_L1_top)
add_trace_segment(x_L3 + pad_gap / 2 + pad_w, x_port_out, y_N, z_L1_bot, z_L1_top, trace_w, copper)
R4_x = x_port_in - 0.3
copper.AddBox(
[R4_x - pad_l / 2, y_P - pad_w / 2, z_L1_bot],
[R4_x + pad_l / 2, y_P + pad_w / 2, z_L1_top],
priority=20,
)
copper.AddBox(
[R4_x - pad_l / 2, y_N - pad_w / 2, z_L1_bot],
[R4_x + pad_l / 2, y_N + pad_w / 2, z_L1_top],
priority=20,
)
R4_elem = CSX.AddLumpedElement('R4', ny='y', caps=True, R=R_diff_in)
R4_elem.AddBox([R4_x - pad_l / 4, y_N, z_L1_bot], [R4_x + pad_l / 4, y_P, z_L1_top], priority=30)
R18_x = x_port_out + 0.3
copper.AddBox(
[R18_x - pad_l / 2, y_P - pad_w / 2, z_L1_bot],
[R18_x + pad_l / 2, y_P + pad_w / 2, z_L1_top],
priority=20,
)
copper.AddBox(
[R18_x - pad_l / 2, y_N - pad_w / 2, z_L1_bot],
[R18_x + pad_l / 2, y_N + pad_w / 2, z_L1_top],
priority=20,
)
R18_elem = CSX.AddLumpedElement('R18', ny='y', caps=True, R=R_diff_out)
R18_elem.AddBox([R18_x - pad_l / 4, y_N, z_L1_bot], [R18_x + pad_l / 4, y_P, z_L1_top], priority=30)
port1 = FDTD.AddLumpedPort(
1,
50,
[x_port_in, y_P - trace_w / 2, z_L2_top],
[x_port_in, y_P + trace_w / 2, z_L1_bot],
'z',
excite=1.0,
)
port2 = FDTD.AddLumpedPort(
2,
50,
[x_port_in, y_N - trace_w / 2, z_L2_top],
[x_port_in, y_N + trace_w / 2, z_L1_bot],
'z',
excite=-1.0,
)
port3 = FDTD.AddLumpedPort(
3,
50,
[x_port_out, y_P - trace_w / 2, z_L2_top],
[x_port_out, y_P + trace_w / 2, z_L1_bot],
'z',
excite=0,
)
port4 = FDTD.AddLumpedPort(
4,
50,
[x_port_out, y_N - trace_w / 2, z_L2_top],
[x_port_out, y_N + trace_w / 2, z_L1_bot],
'z',
excite=0,
)
mesh = CSX.GetGrid()
mesh.SetDeltaUnit(unit)
mesh.AddLine('x', [x_min, x_max])
for x_comp in [x_R_series, x_L1, x_C1, x_L2, x_C2, x_L3]:
mesh.AddLine('x', np.linspace(x_comp - 1.0, x_comp + 1.0, 15))
mesh.AddLine('x', [x_port_in, x_port_out])
mesh.AddLine('x', [R4_x, R18_x])
mesh.AddLine('y', [y_min, y_max])
for y_trace in [y_P, y_N]:
mesh.AddLine('y', np.linspace(y_trace - 0.5, y_trace + 0.5, 10))
mesh.AddLine('z', [z_min, z_max])
mesh.AddLine('z', np.linspace(z_L4_bot - 0.1, z_L1_top + 0.1, 25))
mesh.SmoothMeshLines('x', max_res, ratio=1.4)
mesh.SmoothMeshLines('y', max_res, ratio=1.4)
mesh.SmoothMeshLines('z', max_res / 3, ratio=1.3)
sim_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'results')
if not os.path.exists(sim_path):
os.makedirs(sim_path)
CSX_file = os.path.join(sim_path, 'aaf_filter.xml')
CSX.Write2XML(CSX_file)
FDTD.Run(sim_path, cleanup=True, verbose=3)
freq = np.linspace(f_start, f_stop, 1001)
port1.CalcPort(sim_path, freq)
port2.CalcPort(sim_path, freq)
port3.CalcPort(sim_path, freq)
port4.CalcPort(sim_path, freq)
inc1 = port1.uf_inc
ref1 = port1.uf_ref
inc2 = port2.uf_inc
ref2 = port2.uf_ref
inc3 = port3.uf_inc
ref3 = port3.uf_ref
inc4 = port4.uf_inc
ref4 = port4.uf_ref
a_diff = (inc1 - inc2) / np.sqrt(2)
b_diff_in = (ref1 - ref2) / np.sqrt(2)
b_diff_out = (ref3 - ref4) / np.sqrt(2)
Sdd11 = b_diff_in / a_diff
Sdd21 = b_diff_out / a_diff
b_comm_out = (ref3 + ref4) / np.sqrt(2)
Scd21 = b_comm_out / a_diff
import matplotlib # noqa: E402
matplotlib.use('Agg')
import matplotlib.pyplot as plt # noqa: E402
fig, axes = plt.subplots(3, 1, figsize=(12, 14))
ax = axes[0]
Sdd21_dB = 20 * np.log10(np.abs(Sdd21) + 1e-15)
ax.plot(freq / 1e6, Sdd21_dB, 'b-', linewidth=2, label='|Sdd21| (Insertion Loss)')
ax.axvspan(
f_IF_low / 1e6, f_IF_high / 1e6, alpha=0.15, color='green', label='IF Band (120-180 MHz)'
)
ax.axhline(-3, color='r', linestyle='--', alpha=0.5, label='-3 dB')
ax.set_xlabel('Frequency (MHz)')
ax.set_ylabel('|Sdd21| (dB)')
ax.set_title('Anti-Alias Filter — Differential Insertion Loss')
ax.set_xlim([0, 1000])
ax.set_ylim([-60, 5])
ax.grid(True, alpha=0.3)
ax.legend()
ax = axes[1]
Sdd11_dB = 20 * np.log10(np.abs(Sdd11) + 1e-15)
ax.plot(freq / 1e6, Sdd11_dB, 'r-', linewidth=2, label='|Sdd11| (Return Loss)')
ax.axvspan(f_IF_low / 1e6, f_IF_high / 1e6, alpha=0.15, color='green', label='IF Band')
ax.axhline(-10, color='orange', linestyle='--', alpha=0.5, label='-10 dB')
ax.set_xlabel('Frequency (MHz)')
ax.set_ylabel('|Sdd11| (dB)')
ax.set_title('Anti-Alias Filter — Differential Return Loss')
ax.set_xlim([0, 1000])
ax.set_ylim([-40, 0])
ax.grid(True, alpha=0.3)
ax.legend()
ax = axes[2]
phase_Sdd21 = np.unwrap(np.angle(Sdd21))
group_delay = -np.diff(phase_Sdd21) / np.diff(2 * np.pi * freq) * 1e9
ax.plot(freq[1:] / 1e6, group_delay, 'g-', linewidth=2, label='Group Delay')
ax.axvspan(f_IF_low / 1e6, f_IF_high / 1e6, alpha=0.15, color='green', label='IF Band')
ax.set_xlabel('Frequency (MHz)')
ax.set_ylabel('Group Delay (ns)')
ax.set_title('Anti-Alias Filter — Group Delay')
ax.set_xlim([0, 500])
ax.grid(True, alpha=0.3)
ax.legend()
plt.tight_layout()
plot_file = os.path.join(sim_path, 'aaf_filter_response.png')
plt.savefig(plot_file, dpi=150)
idx_120 = np.argmin(np.abs(freq - f_IF_low))
idx_150 = np.argmin(np.abs(freq - f_center))
idx_180 = np.argmin(np.abs(freq - f_IF_high))
idx_200 = np.argmin(np.abs(freq - 200e6))
idx_400 = np.argmin(np.abs(freq - 400e6))
csv_file = os.path.join(sim_path, 'aaf_sparams.csv')
np.savetxt(
csv_file,
np.column_stack([freq / 1e6, Sdd21_dB, Sdd11_dB, 20 * np.log10(np.abs(Scd21) + 1e-15)]),
header='Freq_MHz, Sdd21_dB, Sdd11_dB, Scd21_dB',
delimiter=',', fmt='%.6f'
)
+27 -16
View File
@@ -91,9 +91,9 @@ z_edges = np.concatenate([z_centers - slot_L/2.0, z_centers + slot_L/2.0])
# ------------------------- # -------------------------
# Mesh lines — EXPLICIT (no GetLine calls) # Mesh lines — EXPLICIT (no GetLine calls)
# ------------------------- # -------------------------
x_lines = sorted(set([x_min, -t_metal, 0.0, a, a+t_metal, x_max] + list(x_edges))) x_lines = sorted({x_min, -t_metal, 0.0, a, a + t_metal, x_max, *list(x_edges)})
y_lines = [y_min, 0.0, b, b+t_metal, y_max] y_lines = [y_min, 0.0, b, b+t_metal, y_max]
z_lines = sorted(set([z_min, 0.0, L, z_max] + list(z_edges))) z_lines = sorted({z_min, 0.0, L, z_max, *list(z_edges)})
mesh.AddLine('x', x_lines) mesh.AddLine('x', x_lines)
mesh.AddLine('y', y_lines) mesh.AddLine('y', y_lines)
@@ -106,7 +106,8 @@ mesh.SmoothMeshLines('all', mesh_res, ratio=1.4)
# Materials # Materials
# ------------------------- # -------------------------
pec = CSX.AddMetal('PEC') pec = CSX.AddMetal('PEC')
quartz = CSX.AddMaterial('QUARTZ'); quartz.SetMaterialProperty(epsilon=er_quartz) quartz = CSX.AddMaterial('QUARTZ')
quartz.SetMaterialProperty(epsilon=er_quartz)
air = CSX.AddMaterial('AIR') # explicit for slot holes air = CSX.AddMaterial('AIR') # explicit for slot holes
# ------------------------- # -------------------------
@@ -122,7 +123,7 @@ pec.AddBox([-t_metal,-t_metal,0],[a+t_metal,0, L]) # bottom
pec.AddBox([-t_metal, b, 0], [a+t_metal,b+t_metal,L]) # top pec.AddBox([-t_metal, b, 0], [a+t_metal,b+t_metal,L]) # top
# Slots = AIR boxes overriding the top metal # Slots = AIR boxes overriding the top metal
for zc, xc in zip(z_centers, x_centers): for zc, xc in zip(z_centers, x_centers, strict=False):
x1, x2 = xc - slot_w/2.0, xc + slot_w/2.0 x1, x2 = xc - slot_w/2.0, xc + slot_w/2.0
z1, z2 = zc - slot_L/2.0, zc + slot_L/2.0 z1, z2 = zc - slot_L/2.0, zc + slot_L/2.0
prim = air.AddBox([x1, b, z1], [x2, b+t_metal, z2]) prim = air.AddBox([x1, b, z1], [x2, b+t_metal, z2])
@@ -180,7 +181,7 @@ if simulate:
# Post-processing: S-params & impedance # Post-processing: S-params & impedance
# ------------------------- # -------------------------
freq = np.linspace(f_start, f_stop, 401) freq = np.linspace(f_start, f_stop, 401)
ports = [p for p in FDTD.ports] # Port 1 & Port 2 in creation order ports = list(FDTD.ports) # Port 1 & Port 2 in creation order
for p in ports: for p in ports:
p.CalcPort(Sim_Path, freq) p.CalcPort(Sim_Path, freq)
@@ -191,13 +192,19 @@ Zin = ports[0].uf_tot / ports[0].if_tot
plt.figure(figsize=(7.6,4.6)) plt.figure(figsize=(7.6,4.6))
plt.plot(freq*1e-9, 20*np.log10(np.abs(S11)), lw=2, label='|S11|') plt.plot(freq*1e-9, 20*np.log10(np.abs(S11)), lw=2, label='|S11|')
plt.plot(freq*1e-9, 20*np.log10(np.abs(S21)), lw=2, ls='--', label='|S21|') plt.plot(freq*1e-9, 20*np.log10(np.abs(S21)), lw=2, ls='--', label='|S21|')
plt.grid(True); plt.legend(); plt.xlabel('Frequency (GHz)'); plt.ylabel('Magnitude (dB)') plt.grid(True)
plt.legend()
plt.xlabel('Frequency (GHz)')
plt.ylabel('Magnitude (dB)')
plt.title('S-Parameters: Slotted Quartz-Filled WG') plt.title('S-Parameters: Slotted Quartz-Filled WG')
plt.figure(figsize=(7.6,4.6)) plt.figure(figsize=(7.6,4.6))
plt.plot(freq*1e-9, np.real(Zin), lw=2, label='Re{Zin}') plt.plot(freq*1e-9, np.real(Zin), lw=2, label='Re{Zin}')
plt.plot(freq*1e-9, np.imag(Zin), lw=2, ls='--', label='Im{Zin}') plt.plot(freq*1e-9, np.imag(Zin), lw=2, ls='--', label='Im{Zin}')
plt.grid(True); plt.legend(); plt.xlabel('Frequency (GHz)'); plt.ylabel('Ohms') plt.grid(True)
plt.legend()
plt.xlabel('Frequency (GHz)')
plt.ylabel('Ohms')
plt.title('Input Impedance (Port 1)') plt.title('Input Impedance (Port 1)')
# ------------------------- # -------------------------
@@ -219,9 +226,6 @@ mismatch = 1.0 - np.abs(S11[idx_f0])**2 # (1 - |S11|^2)
Gmax_lin = Dmax_lin * float(mismatch) Gmax_lin = Dmax_lin * float(mismatch)
Gmax_dBi = 10*np.log10(Gmax_lin) Gmax_dBi = 10*np.log10(Gmax_lin)
print(f"Max directivity @ {f0/1e9:.3f} GHz: {10*np.log10(Dmax_lin):.2f} dBi")
print(f"Mismatch term (1-|S11|^2) : {float(mismatch):.3f}")
print(f"Estimated max realized gain : {Gmax_dBi:.2f} dBi")
# 3D normalized pattern # 3D normalized pattern
E = np.squeeze(res.E_norm) # shape [f, th, ph] -> [th, ph] E = np.squeeze(res.E_norm) # shape [f, th, ph] -> [th, ph]
@@ -237,19 +241,26 @@ ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(X, Y, Z, rstride=2, cstride=2, linewidth=0, antialiased=True, alpha=0.92) ax.plot_surface(X, Y, Z, rstride=2, cstride=2, linewidth=0, antialiased=True, alpha=0.92)
ax.set_title(f'Normalized 3D Pattern @ {f0/1e9:.2f} GHz\n(peak ≈ {Gmax_dBi:.1f} dBi)') ax.set_title(f'Normalized 3D Pattern @ {f0/1e9:.2f} GHz\n(peak ≈ {Gmax_dBi:.1f} dBi)')
ax.set_box_aspect((1,1,1)) ax.set_box_aspect((1,1,1))
ax.set_xlabel('x'); ax.set_ylabel('y'); ax.set_zlabel('z') ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.tight_layout() plt.tight_layout()
# Quick 2D geometry preview (top view at y=b) # Quick 2D geometry preview (top view at y=b)
plt.figure(figsize=(8.4,2.8)) plt.figure(figsize=(8.4,2.8))
plt.fill_between([0,a], [0,0], [L,L], color='#dddddd', alpha=0.5, step='pre', label='WG aperture (top)') plt.fill_between(
for zc, xc in zip(z_centers, x_centers): [0, a], [0, 0], [L, L], color='#dddddd', alpha=0.5, step='pre', label='WG aperture (top)'
)
for zc, xc in zip(z_centers, x_centers, strict=False):
plt.gca().add_patch(plt.Rectangle((xc - slot_w/2.0, zc - slot_L/2.0), plt.gca().add_patch(plt.Rectangle((xc - slot_w/2.0, zc - slot_L/2.0),
slot_w, slot_L, fc='#3355ff', ec='k')) slot_w, slot_L, fc='#3355ff', ec='k'))
plt.xlim(-2, a+2); plt.ylim(-5, L+5) plt.xlim(-2, a + 2)
plt.ylim(-5, L + 5)
plt.gca().invert_yaxis() plt.gca().invert_yaxis()
plt.xlabel('x (mm)'); plt.ylabel('z (mm)') plt.xlabel('x (mm)')
plt.ylabel('z (mm)')
plt.title('Top-view slot layout (y=b plane)') plt.title('Top-view slot layout (y=b plane)')
plt.grid(True); plt.legend() plt.grid(True)
plt.legend()
plt.show() plt.show()
@@ -1,6 +1,6 @@
# openems_quartz_slotted_wg_10p5GHz.py # openems_quartz_slotted_wg_10p5GHz.py
# Slotted rectangular waveguide (quartz-filled, εr=3.8) tuned to 10.5 GHz. # Slotted rectangular waveguide (quartz-filled, εr=3.8) tuned to 10.5 GHz.
# Builds geometry, meshes (no GetLine calls), sweeps S-params/impedance over 9.511.5 GHz, # Builds geometry, meshes (no GetLine calls), sweeps S-params/impedance over 9.5-11.5 GHz,
# computes 3D far-field, and reports estimated max realized gain. # computes 3D far-field, and reports estimated max realized gain.
import os import os
@@ -15,14 +15,14 @@ from openEMS.physical_constants import C0
try: try:
from CSXCAD import ContinuousStructure, AppCSXCAD_BIN from CSXCAD import ContinuousStructure, AppCSXCAD_BIN
HAVE_APP = True HAVE_APP = True
except Exception: except ImportError:
from CSXCAD import ContinuousStructure from CSXCAD import ContinuousStructure
AppCSXCAD_BIN = None AppCSXCAD_BIN = None
HAVE_APP = False HAVE_APP = False
#Set PROFILE to "sanity" first; run and check [mesh] cells: stays reasonable. #Set PROFILE to "sanity" first; run and check [mesh] cells: stays reasonable.
#If its small, move to "balanced"; once happy, go "full". #If it's small, move to "balanced"; once happy, go "full".
#Toggle VIEW_GEOM=True if you want the 3D viewer (requires AppCSXCAD_BIN available). #Toggle VIEW_GEOM=True if you want the 3D viewer (requires AppCSXCAD_BIN available).
@@ -123,9 +123,9 @@ x_edges = np.concatenate([x_centers - slot_w/2.0, x_centers + slot_w/2.0])
z_edges = np.concatenate([z_centers - slot_L/2.0, z_centers + slot_L/2.0]) z_edges = np.concatenate([z_centers - slot_L/2.0, z_centers + slot_L/2.0])
# Mesh lines: explicit (NO GetLine calls) # Mesh lines: explicit (NO GetLine calls)
x_lines = sorted(set([x_min, -t_metal, 0.0, a, a+t_metal, x_max] + list(x_edges))) x_lines = sorted({x_min, -t_metal, 0.0, a, a + t_metal, x_max, *list(x_edges)})
y_lines = [y_min, 0.0, b, b+t_metal, y_max] y_lines = [y_min, 0.0, b, b+t_metal, y_max]
z_lines = sorted(set([z_min, 0.0, guide_length_mm, z_max] + list(z_edges))) z_lines = sorted({z_min, 0.0, guide_length_mm, z_max, *list(z_edges)})
mesh.AddLine('x', x_lines) mesh.AddLine('x', x_lines)
mesh.AddLine('y', y_lines) mesh.AddLine('y', y_lines)
@@ -134,11 +134,10 @@ mesh.AddLine('z', z_lines)
# Print complexity and rough memory (to help stay inside 16 GB) # Print complexity and rough memory (to help stay inside 16 GB)
Nx, Ny, Nz = len(x_lines)-1, len(y_lines)-1, len(z_lines)-1 Nx, Ny, Nz = len(x_lines)-1, len(y_lines)-1, len(z_lines)-1
Ncells = Nx*Ny*Nz Ncells = Nx*Ny*Nz
print(f"[mesh] cells: {Nx} × {Ny} × {Nz} = {Ncells:,}")
mem_fields_bytes = Ncells * 6 * 8 # rough ~ (Ex,Ey,Ez,Hx,Hy,Hz) doubles mem_fields_bytes = Ncells * 6 * 8 # rough ~ (Ex,Ey,Ez,Hx,Hy,Hz) doubles
print(f"[mesh] rough field memory: ~{mem_fields_bytes/1e9:.2f} GB (solver overhead extra)") dx_min = min(np.diff(x_lines))
dx_min = min(np.diff(x_lines)); dy_min = min(np.diff(y_lines)); dz_min = min(np.diff(z_lines)) dy_min = min(np.diff(y_lines))
print(f"[mesh] min steps (mm): dx={dx_min:.3f}, dy={dy_min:.3f}, dz={dz_min:.3f}") dz_min = min(np.diff(z_lines))
# Optional smoothing to limit max cell size # Optional smoothing to limit max cell size
mesh.SmoothMeshLines('all', mesh_res, ratio=1.4) mesh.SmoothMeshLines('all', mesh_res, ratio=1.4)
@@ -147,7 +146,8 @@ mesh.SmoothMeshLines('all', mesh_res, ratio=1.4)
# MATERIALS & SOLIDS # MATERIALS & SOLIDS
# ================= # =================
pec = CSX.AddMetal('PEC') pec = CSX.AddMetal('PEC')
quartzM = CSX.AddMaterial('QUARTZ'); quartzM.SetMaterialProperty(epsilon=er_quartz) quartzM = CSX.AddMaterial('QUARTZ')
quartzM.SetMaterialProperty(epsilon=er_quartz)
airM = CSX.AddMaterial('AIR') airM = CSX.AddMaterial('AIR')
# Quartz full block # Quartz full block
@@ -157,10 +157,12 @@ quartzM.AddBox([0, 0, 0], [a, b, guide_length_mm])
pec.AddBox([-t_metal, 0, 0], [0, b, guide_length_mm]) # left pec.AddBox([-t_metal, 0, 0], [0, b, guide_length_mm]) # left
pec.AddBox([a, 0, 0], [a+t_metal,b, guide_length_mm]) # right pec.AddBox([a, 0, 0], [a+t_metal,b, guide_length_mm]) # right
pec.AddBox([-t_metal,-t_metal,0],[a+t_metal,0, guide_length_mm]) # bottom pec.AddBox([-t_metal,-t_metal,0],[a+t_metal,0, guide_length_mm]) # bottom
pec.AddBox([-t_metal, b, 0], [a+t_metal,b+t_metal,guide_length_mm]) # top (slots will pierce) pec.AddBox(
[-t_metal, b, 0], [a + t_metal, b + t_metal, guide_length_mm]
) # top (slots will pierce)
# Slots (AIR) overriding top metal # Slots (AIR) overriding top metal
for zc, xc in zip(z_centers, x_centers): for zc, xc in zip(z_centers, x_centers, strict=False):
x1, x2 = xc - slot_w/2.0, xc + slot_w/2.0 x1, x2 = xc - slot_w/2.0, xc + slot_w/2.0
z1, z2 = zc - slot_L/2.0, zc + slot_L/2.0 z1, z2 = zc - slot_L/2.0, zc + slot_L/2.0
prim = airM.AddBox([x1, b, z1], [x2, b+t_metal, z2]) prim = airM.AddBox([x1, b, z1], [x2, b+t_metal, z2])
@@ -210,23 +212,20 @@ if VIEW_GEOM and HAVE_APP and AppCSXCAD_BIN:
t0 = time.time() t0 = time.time()
FDTD.Run(Sim_Path, cleanup=True, verbose=2, numThreads=THREADS) FDTD.Run(Sim_Path, cleanup=True, verbose=2, numThreads=THREADS)
t1 = time.time() t1 = time.time()
print(f"[timing] FDTD solve elapsed: {t1 - t0:.2f} s")
# ... right before NF2FF (far-field): # ... right before NF2FF (far-field):
t2 = time.time() t2 = time.time()
try: try:
res = nf2ff.CalcNF2FF(Sim_Path, [f0], theta, phi) res = nf2ff.CalcNF2FF(Sim_Path, [f0], theta, phi) # noqa: F821
except AttributeError: except AttributeError:
res = FDTD.CalcNF2FF(nf2ff, Sim_Path, [f0], theta, phi) res = FDTD.CalcNF2FF(nf2ff, Sim_Path, [f0], theta, phi) # noqa: F821
t3 = time.time() t3 = time.time()
print(f"[timing] NF2FF (far-field) elapsed: {t3 - t2:.2f} s")
# ... S-parameters postproc timing (optional): # ... S-parameters postproc timing (optional):
t4 = time.time() t4 = time.time()
for p in ports: for p in ports: # noqa: F821
p.CalcPort(Sim_Path, freq) p.CalcPort(Sim_Path, freq) # noqa: F821
t5 = time.time() t5 = time.time()
print(f"[timing] Port/S-params postproc elapsed: {t5 - t4:.2f} s")
# ======= # =======
@@ -235,11 +234,8 @@ print(f"[timing] Port/S-params postproc elapsed: {t5 - t4:.2f} s")
if SIMULATE: if SIMULATE:
FDTD.Run(Sim_Path, cleanup=True, verbose=2, numThreads=THREADS) FDTD.Run(Sim_Path, cleanup=True, verbose=2, numThreads=THREADS)
# ========================== freq = np.linspace(f_start, f_stop, profiles[PROFILE]["freq_pts"])
# POST: S-PARAMS / IMPEDANCE ports = list(FDTD.ports) # Port 1 & 2 in creation order
# ==========================
freq = np.linspace(f_start, f_stop, profiles[PROFILE]["freq_pts"])
ports = [p for p in FDTD.ports] # Port 1 & 2 in creation order
for p in ports: for p in ports:
p.CalcPort(Sim_Path, freq) p.CalcPort(Sim_Path, freq)
@@ -250,13 +246,19 @@ Zin = ports[0].uf_tot / ports[0].if_tot
plt.figure(figsize=(7.6,4.6)) plt.figure(figsize=(7.6,4.6))
plt.plot(freq*1e-9, 20*np.log10(np.abs(S11)), lw=2, label='|S11|') plt.plot(freq*1e-9, 20*np.log10(np.abs(S11)), lw=2, label='|S11|')
plt.plot(freq*1e-9, 20*np.log10(np.abs(S21)), lw=2, ls='--', label='|S21|') plt.plot(freq*1e-9, 20*np.log10(np.abs(S21)), lw=2, ls='--', label='|S21|')
plt.grid(True); plt.legend(); plt.xlabel('Frequency (GHz)'); plt.ylabel('Magnitude (dB)') plt.grid(True)
plt.legend()
plt.xlabel('Frequency (GHz)')
plt.ylabel('Magnitude (dB)')
plt.title(f'S-Parameters (profile: {PROFILE})') plt.title(f'S-Parameters (profile: {PROFILE})')
plt.figure(figsize=(7.6,4.6)) plt.figure(figsize=(7.6,4.6))
plt.plot(freq*1e-9, np.real(Zin), lw=2, label='Re{Zin}') plt.plot(freq*1e-9, np.real(Zin), lw=2, label='Re{Zin}')
plt.plot(freq*1e-9, np.imag(Zin), lw=2, ls='--', label='Im{Zin}') plt.plot(freq*1e-9, np.imag(Zin), lw=2, ls='--', label='Im{Zin}')
plt.grid(True); plt.legend(); plt.xlabel('Frequency (GHz)'); plt.ylabel('Ohms') plt.grid(True)
plt.legend()
plt.xlabel('Frequency (GHz)')
plt.ylabel('Ohms')
plt.title('Input Impedance (Port 1)') plt.title('Input Impedance (Port 1)')
# ========================== # ==========================
@@ -277,9 +279,6 @@ mismatch = 1.0 - np.abs(S11[idx_f0])**2
Gmax_lin = Dmax_lin * float(mismatch) Gmax_lin = Dmax_lin * float(mismatch)
Gmax_dBi = 10*np.log10(Gmax_lin) Gmax_dBi = 10*np.log10(Gmax_lin)
print(f"[far-field] Dmax @ {f0/1e9:.3f} GHz: {10*np.log10(Dmax_lin):.2f} dBi")
print(f"[far-field] mismatch (1-|S11|^2): {float(mismatch):.3f}")
print(f"[far-field] est. max realized gain: {Gmax_dBi:.2f} dBi")
# Normalized 3D pattern # Normalized 3D pattern
E = np.squeeze(res.E_norm) # [th, ph] E = np.squeeze(res.E_norm) # [th, ph]
@@ -295,22 +294,35 @@ ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(X, Y, Z, rstride=2, cstride=2, linewidth=0, antialiased=True, alpha=0.92) ax.plot_surface(X, Y, Z, rstride=2, cstride=2, linewidth=0, antialiased=True, alpha=0.92)
ax.set_title(f'Normalized 3D Pattern @ {f0/1e9:.2f} GHz\n(peak ≈ {Gmax_dBi:.1f} dBi)') ax.set_title(f'Normalized 3D Pattern @ {f0/1e9:.2f} GHz\n(peak ≈ {Gmax_dBi:.1f} dBi)')
ax.set_box_aspect((1,1,1)) ax.set_box_aspect((1,1,1))
ax.set_xlabel('x'); ax.set_ylabel('y'); ax.set_zlabel('z') ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.tight_layout() plt.tight_layout()
# ========================== # ==========================
# QUICK 2D GEOMETRY PREVIEW # QUICK 2D GEOMETRY PREVIEW
# ========================== # ==========================
plt.figure(figsize=(8.4,2.8)) plt.figure(figsize=(8.4,2.8))
plt.fill_between([0,a], [0,0], [guide_length_mm, guide_length_mm], color='#dddddd', alpha=0.5, step='pre', label='WG top aperture') plt.fill_between(
for zc, xc in zip(z_centers, x_centers): [0, a],
[0, 0],
[guide_length_mm, guide_length_mm],
color='#dddddd',
alpha=0.5,
step='pre',
label='WG top aperture',
)
for zc, xc in zip(z_centers, x_centers, strict=False):
plt.gca().add_patch(plt.Rectangle((xc - slot_w/2.0, zc - slot_L/2.0), plt.gca().add_patch(plt.Rectangle((xc - slot_w/2.0, zc - slot_L/2.0),
slot_w, slot_L, fc='#3355ff', ec='k')) slot_w, slot_L, fc='#3355ff', ec='k'))
plt.xlim(-2, a+2); plt.ylim(-5, guide_length_mm+5) plt.xlim(-2, a + 2)
plt.ylim(-5, guide_length_mm + 5)
plt.gca().invert_yaxis() plt.gca().invert_yaxis()
plt.xlabel('x (mm)'); plt.ylabel('z (mm)') plt.xlabel('x (mm)')
plt.ylabel('z (mm)')
plt.title(f'Top-view slot layout (N={Nslots}, profile={PROFILE})') plt.title(f'Top-view slot layout (N={Nslots}, profile={PROFILE})')
plt.grid(True); plt.legend() plt.grid(True)
plt.legend()
@@ -68,11 +68,8 @@ def generate_multi_ramp_csv(Fs=125e6, Tb=1e-6, Tau=2e-6, fmax=30e6, fmin=10e6,
# --- Save CSV (no header) # --- Save CSV (no header)
df = pd.DataFrame({"time(s)": t_csv, "voltage(V)": y_csv}) df = pd.DataFrame({"time(s)": t_csv, "voltage(V)": y_csv})
df.to_csv(filename, index=False, header=False) df.to_csv(filename, index=False, header=False)
print(f"CSV saved: {filename}")
print(f"Total raw samples: {total_samples} | Ramps inserted: {ramps_inserted} | CSV points: {len(y_csv)}")
# --- Plot (staircase) if show_plot or save_plot_png:
if show_plot or save_plot_png:
# Choose plotting vectors (use raw DAC samples to keep lines crisp) # Choose plotting vectors (use raw DAC samples to keep lines crisp)
t_plot = t t_plot = t
y_plot = y y_plot = y
@@ -108,7 +105,6 @@ def generate_multi_ramp_csv(Fs=125e6, Tb=1e-6, Tau=2e-6, fmax=30e6, fmin=10e6,
if save_plot_png: if save_plot_png:
plt.savefig(save_plot_png, dpi=150) plt.savefig(save_plot_png, dpi=150)
print(f"Plot saved: {save_plot_png}")
if show_plot: if show_plot:
plt.show() plt.show()
else: else:
+13 -4
View File
@@ -1,7 +1,6 @@
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
# Dimensions (all in mm) line_width = 0.204
line_width = 0.204
substrate_height = 0.102 substrate_height = 0.102
via_drill = 0.20 via_drill = 0.20
via_pad_A = 0.20 # minimal pad case via_pad_A = 0.20 # minimal pad case
@@ -27,10 +26,20 @@ ax.axhline(polygon_y2, color="blue", linestyle="--")
via_positions = [2, 4, 6, 8] # x positions for visualization via_positions = [2, 4, 6, 8] # x positions for visualization
for x in via_positions: for x in via_positions:
# Case A # Case A
ax.add_patch(plt.Circle((x, polygon_y1), via_pad_A/2, facecolor="green", alpha=0.5, label="Via pad A" if x==2 else "")) ax.add_patch(
plt.Circle(
(x, polygon_y1), via_pad_A / 2, facecolor="green", alpha=0.5,
label="Via pad A" if x == 2 else ""
)
)
ax.add_patch(plt.Circle((x, polygon_y2), via_pad_A/2, facecolor="green", alpha=0.5)) ax.add_patch(plt.Circle((x, polygon_y2), via_pad_A/2, facecolor="green", alpha=0.5))
# Case B # Case B
ax.add_patch(plt.Circle((-x, polygon_y1), via_pad_B/2, facecolor="red", alpha=0.3, label="Via pad B" if x==2 else "")) ax.add_patch(
plt.Circle(
(-x, polygon_y1), via_pad_B / 2, facecolor="red", alpha=0.3,
label="Via pad B" if x == 2 else ""
)
)
ax.add_patch(plt.Circle((-x, polygon_y2), via_pad_B/2, facecolor="red", alpha=0.3)) ax.add_patch(plt.Circle((-x, polygon_y2), via_pad_B/2, facecolor="red", alpha=0.3))
# Add dimensions text # Add dimensions text
+18 -7
View File
@@ -1,7 +1,6 @@
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
# Dimensions (all in mm) line_width = 0.204
line_width = 0.204
via_pad_A = 0.20 via_pad_A = 0.20
via_pad_B = 0.45 via_pad_B = 0.45
polygon_offset = 0.30 polygon_offset = 0.30
@@ -26,10 +25,20 @@ ax.axhline(polygon_y2, color="blue", linestyle="--")
via_positions = [2, 2 + via_pitch] # two vias for showing spacing via_positions = [2, 2 + via_pitch] # two vias for showing spacing
for x in via_positions: for x in via_positions:
# Case A # Case A
ax.add_patch(plt.Circle((x, polygon_y1), via_pad_A/2, facecolor="green", alpha=0.5, label="Via pad A" if x==2 else "")) ax.add_patch(
plt.Circle(
(x, polygon_y1), via_pad_A / 2, facecolor="green", alpha=0.5,
label="Via pad A" if x == 2 else ""
)
)
ax.add_patch(plt.Circle((x, polygon_y2), via_pad_A/2, facecolor="green", alpha=0.5)) ax.add_patch(plt.Circle((x, polygon_y2), via_pad_A/2, facecolor="green", alpha=0.5))
# Case B # Case B
ax.add_patch(plt.Circle((-x, polygon_y1), via_pad_B/2, facecolor="red", alpha=0.3, label="Via pad B" if x==2 else "")) ax.add_patch(
plt.Circle(
(-x, polygon_y1), via_pad_B / 2, facecolor="red", alpha=0.3,
label="Via pad B" if x == 2 else ""
)
)
ax.add_patch(plt.Circle((-x, polygon_y2), via_pad_B/2, facecolor="red", alpha=0.3)) ax.add_patch(plt.Circle((-x, polygon_y2), via_pad_B/2, facecolor="red", alpha=0.3))
# Add text annotations # Add text annotations
@@ -40,15 +49,17 @@ ax.text(-2, polygon_y1 + 0.5, "Via B Ø0.45 mm pad", color="red")
# Add pitch dimension (horizontal between vias) # Add pitch dimension (horizontal between vias)
ax.annotate("", xy=(2, polygon_y1 + 0.2), xytext=(2 + via_pitch, polygon_y1 + 0.2), ax.annotate("", xy=(2, polygon_y1 + 0.2), xytext=(2 + via_pitch, polygon_y1 + 0.2),
arrowprops=dict(arrowstyle="<->", color="purple")) arrowprops={"arrowstyle": "<->", "color": "purple"})
ax.text(2 + via_pitch/2, polygon_y1 + 0.3, f"{via_pitch:.2f} mm pitch", color="purple", ha="center") ax.text(2 + via_pitch/2, polygon_y1 + 0.3, f"{via_pitch:.2f} mm pitch", color="purple", ha="center")
# Add distance from RF line edge to via center # Add distance from RF line edge to via center
line_edge_y = rf_line_y + line_width/2 line_edge_y = rf_line_y + line_width/2
via_center_y = polygon_y1 via_center_y = polygon_y1
ax.annotate("", xy=(2.4, line_edge_y), xytext=(2.4, via_center_y), ax.annotate("", xy=(2.4, line_edge_y), xytext=(2.4, via_center_y),
arrowprops=dict(arrowstyle="<->", color="brown")) arrowprops={"arrowstyle": "<->", "color": "brown"})
ax.text(2.5, (line_edge_y + via_center_y)/2, f"{via_center_offset:.2f} mm", color="brown", va="center") ax.text(
2.5, (line_edge_y + via_center_y) / 2, f"{via_center_offset:.2f} mm", color="brown", va="center"
)
# Formatting # Formatting
ax.set_xlim(-5, 5) ax.set_xlim(-5, 5)
@@ -27,7 +27,7 @@ n_idx = np.arange(N) - (N-1)/2
y_positions = m_idx * dy y_positions = m_idx * dy
z_positions = n_idx * dz z_positions = n_idx * dz
def element_factor(theta_rad, phi_rad): def element_factor(theta_rad, _phi_rad):
return np.abs(np.cos(theta_rad)) return np.abs(np.cos(theta_rad))
def array_factor(theta_rad, phi_rad, y_positions, z_positions, wy, wz, theta0_rad, phi0_rad): def array_factor(theta_rad, phi_rad, y_positions, z_positions, wy, wz, theta0_rad, phi0_rad):
@@ -105,5 +105,3 @@ plt.title('Array Pattern Heatmap (|AF·EF|, dB) — Kaiser ~-25 dB')
plt.tight_layout() plt.tight_layout()
plt.savefig('Heatmap_Kaiser25dB_like.png', bbox_inches='tight') plt.savefig('Heatmap_Kaiser25dB_like.png', bbox_inches='tight')
plt.show() plt.show()
print('Saved: E_plane_Kaiser25dB_like.png, H_plane_Kaiser25dB_like.png, Heatmap_Kaiser25dB_like.png')
+25 -35
View File
@@ -15,12 +15,20 @@ def generate_radar_csv(filename="pulse_compression_output.csv"):
timestamp_ns = 0 timestamp_ns = 0
# Target parameters # Target parameters
targets = [ targets = [
{'range': 3000, 'velocity': 25, 'snr': 30, 'azimuth': 10, 'elevation': 5}, # Fast moving target {
{'range': 5000, 'velocity': -15, 'snr': 25, 'azimuth': 20, 'elevation': 2}, # Approaching target 'range': 3000, 'velocity': 25, 'snr': 30, 'azimuth': 10, 'elevation': 5
{'range': 8000, 'velocity': 5, 'snr': 20, 'azimuth': 30, 'elevation': 8}, # Slow moving target }, # Fast moving target
{'range': 12000, 'velocity': -8, 'snr': 18, 'azimuth': 45, 'elevation': 3}, # Distant target {
] 'range': 5000, 'velocity': -15, 'snr': 25, 'azimuth': 20, 'elevation': 2
}, # Approaching target
{
'range': 8000, 'velocity': 5, 'snr': 20, 'azimuth': 30, 'elevation': 8
}, # Slow moving target
{
'range': 12000, 'velocity': -8, 'snr': 18, 'azimuth': 45, 'elevation': 3
}, # Distant target
]
# Noise parameters # Noise parameters
noise_std = 5 noise_std = 5
@@ -30,7 +38,6 @@ def generate_radar_csv(filename="pulse_compression_output.csv"):
chirp_number = 0 chirp_number = 0
# Generate Long Chirps (30µs duration equivalent) # Generate Long Chirps (30µs duration equivalent)
print("Generating Long Chirps...")
for chirp in range(num_long_chirps): for chirp in range(num_long_chirps):
for sample in range(samples_per_chirp): for sample in range(samples_per_chirp):
# Base noise # Base noise
@@ -38,7 +45,7 @@ def generate_radar_csv(filename="pulse_compression_output.csv"):
q_val = np.random.normal(0, noise_std) q_val = np.random.normal(0, noise_std)
# Add clutter (stationary targets) # Add clutter (stationary targets)
clutter_range = 2000 # Fixed clutter at 2km _clutter_range = 2000 # Fixed clutter at 2km
if sample < 100: # Simulate clutter in first 100 samples if sample < 100: # Simulate clutter in first 100 samples
i_val += np.random.normal(0, clutter_std) i_val += np.random.normal(0, clutter_std)
q_val += np.random.normal(0, clutter_std) q_val += np.random.normal(0, clutter_std)
@@ -47,7 +54,9 @@ def generate_radar_csv(filename="pulse_compression_output.csv"):
for target in targets: for target in targets:
# Calculate range bin (simplified) # Calculate range bin (simplified)
range_bin = int(target['range'] / 20) # ~20m per bin range_bin = int(target['range'] / 20) # ~20m per bin
doppler_phase = 2 * math.pi * target['velocity'] * chirp / 100 # Doppler phase shift doppler_phase = (
2 * math.pi * target['velocity'] * chirp / 100
) # Doppler phase shift
# Target appears around its range bin with some spread # Target appears around its range bin with some spread
if abs(sample - range_bin) < 10: if abs(sample - range_bin) < 10:
@@ -80,7 +89,6 @@ def generate_radar_csv(filename="pulse_compression_output.csv"):
timestamp_ns += 175400 # 175.4µs guard time timestamp_ns += 175400 # 175.4µs guard time
# Generate Short Chirps (0.5µs duration equivalent) # Generate Short Chirps (0.5µs duration equivalent)
print("Generating Short Chirps...")
for chirp in range(num_short_chirps): for chirp in range(num_short_chirps):
for sample in range(samples_per_chirp): for sample in range(samples_per_chirp):
# Base noise # Base noise
@@ -96,7 +104,9 @@ def generate_radar_csv(filename="pulse_compression_output.csv"):
for target in targets: for target in targets:
# Range bin calculation (different for short chirps) # Range bin calculation (different for short chirps)
range_bin = int(target['range'] / 40) # Different range resolution range_bin = int(target['range'] / 40) # Different range resolution
doppler_phase = 2 * math.pi * target['velocity'] * (chirp + 5) / 80 # Different Doppler doppler_phase = (
2 * math.pi * target['velocity'] * (chirp + 5) / 80
) # Different Doppler
# Target appears around its range bin # Target appears around its range bin
if abs(sample - range_bin) < 8: if abs(sample - range_bin) < 8:
@@ -130,11 +140,6 @@ def generate_radar_csv(filename="pulse_compression_output.csv"):
# Save to CSV # Save to CSV
df.to_csv(filename, index=False) df.to_csv(filename, index=False)
print(f"Generated CSV file: {filename}")
print(f"Total samples: {len(df)}")
print(f"Long chirps: {num_long_chirps}, Short chirps: {num_short_chirps}")
print(f"Samples per chirp: {samples_per_chirp}")
print(f"File size: {len(df) // 1000}K samples")
return df return df
@@ -142,15 +147,11 @@ def analyze_generated_data(df):
""" """
Analyze the generated data to verify target detection Analyze the generated data to verify target detection
""" """
print("\n=== Data Analysis ===")
# Basic statistics # Basic statistics
long_chirps = df[df['chirp_type'] == 'LONG'] df[df['chirp_type'] == 'LONG']
short_chirps = df[df['chirp_type'] == 'SHORT'] df[df['chirp_type'] == 'SHORT']
print(f"Long chirp samples: {len(long_chirps)}")
print(f"Short chirp samples: {len(short_chirps)}")
print(f"Unique chirp numbers: {df['chirp_number'].nunique()}")
# Calculate actual magnitude and phase for analysis # Calculate actual magnitude and phase for analysis
df['magnitude'] = np.sqrt(df['I_value']**2 + df['Q_value']**2) df['magnitude'] = np.sqrt(df['I_value']**2 + df['Q_value']**2)
@@ -160,15 +161,11 @@ def analyze_generated_data(df):
high_mag_threshold = df['magnitude'].quantile(0.95) # Top 5% high_mag_threshold = df['magnitude'].quantile(0.95) # Top 5%
targets_detected = df[df['magnitude'] > high_mag_threshold] targets_detected = df[df['magnitude'] > high_mag_threshold]
print(f"\nTarget detection threshold: {high_mag_threshold:.2f}")
print(f"High magnitude samples: {len(targets_detected)}")
# Group by chirp type # Group by chirp type
long_targets = targets_detected[targets_detected['chirp_type'] == 'LONG'] targets_detected[targets_detected['chirp_type'] == 'LONG']
short_targets = targets_detected[targets_detected['chirp_type'] == 'SHORT'] targets_detected[targets_detected['chirp_type'] == 'SHORT']
print(f"Targets in long chirps: {len(long_targets)}")
print(f"Targets in short chirps: {len(short_targets)}")
return df return df
@@ -179,10 +176,3 @@ if __name__ == "__main__":
# Analyze the generated data # Analyze the generated data
analyze_generated_data(df) analyze_generated_data(df)
print("\n=== CSV File Ready ===")
print("You can now test the Python GUI with this CSV file!")
print("The file contains:")
print("- 16 Long chirps + 16 Short chirps")
print("- 4 simulated targets at different ranges and velocities")
print("- Realistic noise and clutter")
print("- Proper I/Q data for Doppler processing")
-2
View File
@@ -90,8 +90,6 @@ def generate_small_radar_csv(filename="small_test_radar_data.csv"):
df = pd.DataFrame(data) df = pd.DataFrame(data)
df.to_csv(filename, index=False) df.to_csv(filename, index=False)
print(f"Generated small CSV: {filename}")
print(f"Total samples: {len(df)}")
return df return df
generate_small_radar_csv() generate_small_radar_csv()
+7 -4
View File
@@ -1,5 +1,5 @@
import numpy as np import numpy as np
from numpy.fft import fft, ifft from numpy.fft import fft
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
@@ -15,7 +15,10 @@ theta_n= 2*np.pi*(pow(N,2)*pow(Ts,2)*(fmax-fmin)/(2*Tb)+fmin*N*Ts) # instantaneo
y = 1 + np.sin(theta_n) # ramp signal in time domain y = 1 + np.sin(theta_n) # ramp signal in time domain
M = np.arange(n, 2*n, 1) M = np.arange(n, 2*n, 1)
theta_m= 2*np.pi*(pow(M,2)*pow(Ts,2)*(-fmax+fmin)/(2*Tb)+(-fmin+2*fmax)*M*Ts)-2*np.pi*((fmin-fmax)*Tb/2+(2*fmax-fmin)*Tb) # instantaneous phase theta_m= (
2*np.pi*(pow(M,2)*pow(Ts,2)*(-fmax+fmin)/(2*Tb)+(-fmin+2*fmax)*M*Ts)
- 2*np.pi*((fmin-fmax)*Tb/2+(2*fmax-fmin)*Tb)
) # instantaneous phase
z = 1 + np.sin(theta_m) # ramp signal in time domain z = 1 + np.sin(theta_m) # ramp signal in time domain
x = np.concatenate((y, z)) x = np.concatenate((y, z))
@@ -23,9 +26,9 @@ x = np.concatenate((y, z))
t = Ts*np.arange(0,2*n,1) t = Ts*np.arange(0,2*n,1)
X = fft(x) X = fft(x)
L =len(X) L =len(X)
l = np.arange(L) freq_indices = np.arange(L)
T = L*Ts T = L*Ts
freq = l/T freq = freq_indices/T
plt.figure(figsize = (12, 6)) plt.figure(figsize = (12, 6))
@@ -15,7 +15,10 @@ theta_n= 2*np.pi*(pow(N,2)*pow(Ts,2)*(fmax-fmin)/(2*Tb)+fmin*N*Ts) # instantaneo
y = 1 + np.sin(theta_n) # ramp signal in time domain y = 1 + np.sin(theta_n) # ramp signal in time domain
M = np.arange(n, 2*n, 1) M = np.arange(n, 2*n, 1)
theta_m= 2*np.pi*(pow(M,2)*pow(Ts,2)*(-fmax+fmin)/(2*Tb)+(-fmin+2*fmax)*M*Ts)-2*np.pi*((fmin-fmax)*Tb/2+(2*fmax-fmin)*Tb) # instantaneous phase theta_m= (
2*np.pi*(pow(M,2)*pow(Ts,2)*(-fmax+fmin)/(2*Tb)+(-fmin+2*fmax)*M*Ts)
- 2*np.pi*((fmin-fmax)*Tb/2+(2*fmax-fmin)*Tb)
) # instantaneous phase
z = 1 + np.sin(theta_m) # ramp signal in time domain z = 1 + np.sin(theta_m) # ramp signal in time domain
x = np.concatenate((y, z)) x = np.concatenate((y, z))
@@ -24,11 +27,10 @@ t = Ts*np.arange(0,2*n,1)
plt.plot(t, x) plt.plot(t, x)
X = fft(x) X = fft(x)
L =len(X) L =len(X)
l = np.arange(L) freq_indices = np.arange(L)
T = L*Ts T = L*Ts
freq = l/T freq = freq_indices/T
print("The Array is: ", x) #printing the array
plt.figure(figsize = (12, 6)) plt.figure(figsize = (12, 6))
plt.subplot(121) plt.subplot(121)
-24
View File
@@ -1,24 +0,0 @@
import numpy as np
# Define parameters
fs = 120e6 # Sampling frequency
Ts = 1 / fs # Sampling time
Tb = 1e-6 # Burst time
Tau = 30e-6 # Pulse repetition time
fmax = 15e6 # Maximum frequency on ramp
fmin = 1e6 # Minimum frequency on ramp
# Compute number of samples per ramp
n = int(Tb / Ts)
N = np.arange(0, n, 1)
# Compute instantaneous phase
theta_n = 2 * np.pi * ((N**2 * Ts**2 * (fmax - fmin) / (2 * Tb)) + fmin * N * Ts)
# Generate waveform and scale it to 8-bit unsigned values (0 to 255)
y = 1 + np.sin(theta_n) # Normalize from 0 to 2
y_scaled = np.round(y * 127.5).astype(int) # Scale to 8-bit range (0-255)
# Print values in Verilog-friendly format
for i in range(n):
print(f"waveform_LUT[{i}] = 8'h{y_scaled[i]:02X};")
+27 -21
View File
@@ -58,10 +58,10 @@ class RadarCalculatorGUI:
scrollbar = ttk.Scrollbar(self.input_frame, orient="vertical", command=canvas.yview) scrollbar = ttk.Scrollbar(self.input_frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas) scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind( scrollable_frame.bind(
"<Configure>", "<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all")) lambda _e: canvas.configure(scrollregion=canvas.bbox("all"))
) )
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set) canvas.configure(yscrollcommand=scrollbar.set)
@@ -83,7 +83,7 @@ class RadarCalculatorGUI:
self.entries = {} self.entries = {}
for i, (label, default) in enumerate(inputs): for _i, (label, default) in enumerate(inputs):
# Create a frame for each input row # Create a frame for each input row
row_frame = ttk.Frame(scrollable_frame) row_frame = ttk.Frame(scrollable_frame)
row_frame.pack(fill=tk.X, pady=5) row_frame.pack(fill=tk.X, pady=5)
@@ -119,8 +119,8 @@ class RadarCalculatorGUI:
calculate_btn.pack() calculate_btn.pack()
# Bind hover effect # Bind hover effect
calculate_btn.bind("<Enter>", lambda e: calculate_btn.config(bg='#45a049')) calculate_btn.bind("<Enter>", lambda _e: calculate_btn.config(bg='#45a049'))
calculate_btn.bind("<Leave>", lambda e: calculate_btn.config(bg='#4CAF50')) calculate_btn.bind("<Leave>", lambda _e: calculate_btn.config(bg='#4CAF50'))
def create_results_display(self): def create_results_display(self):
"""Create the results display area""" """Create the results display area"""
@@ -135,10 +135,10 @@ class RadarCalculatorGUI:
scrollbar = ttk.Scrollbar(self.results_frame, orient="vertical", command=canvas.yview) scrollbar = ttk.Scrollbar(self.results_frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas) scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind( scrollable_frame.bind(
"<Configure>", "<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all")) lambda _e: canvas.configure(scrollregion=canvas.bbox("all"))
) )
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set) canvas.configure(yscrollcommand=scrollbar.set)
@@ -158,7 +158,7 @@ class RadarCalculatorGUI:
self.results_labels = {} self.results_labels = {}
for i, (label, key) in enumerate(results): for _i, (label, key) in enumerate(results):
# Create a frame for each result row # Create a frame for each result row
row_frame = ttk.Frame(scrollable_frame) row_frame = ttk.Frame(scrollable_frame)
row_frame.pack(fill=tk.X, pady=10, padx=20) row_frame.pack(fill=tk.X, pady=10, padx=20)
@@ -180,10 +180,10 @@ class RadarCalculatorGUI:
note_text = """ note_text = """
NOTES: NOTES:
• Maximum detectable range is calculated using the radar equation • Maximum detectable range is calculated using the radar equation
• Range resolution = c × τ / 2, where τ is pulse duration • Range resolution = c x τ / 2, where τ is pulse duration
• Maximum unambiguous range = c / (2 × PRF) • Maximum unambiguous range = c / (2 x PRF)
• Maximum detectable speed = λ × PRF / 4 • Maximum detectable speed = λ x PRF / 4
• Speed resolution = λ × PRF / (2 × N) where N is number of pulses (assumed 1) • Speed resolution = λ x PRF / (2 x N) where N is number of pulses (assumed 1)
• λ (wavelength) = c / f • λ (wavelength) = c / f
""" """
@@ -221,7 +221,10 @@ class RadarCalculatorGUI:
temp = self.get_float_value(self.entries["Temperature (K):"]) temp = self.get_float_value(self.entries["Temperature (K):"])
# Validate inputs # Validate inputs
if None in [f_ghz, pulse_duration_us, prf, p_dbm, g_dbi, sens_dbm, rcs, losses_db, nf_db, temp]: if None in [
f_ghz, pulse_duration_us, prf, p_dbm, g_dbi,
sens_dbm, rcs, losses_db, nf_db, temp,
]:
messagebox.showerror("Error", "Please enter valid numeric values for all fields") messagebox.showerror("Error", "Please enter valid numeric values for all fields")
return return
@@ -235,7 +238,7 @@ class RadarCalculatorGUI:
g_linear = 10 ** (g_dbi / 10) g_linear = 10 ** (g_dbi / 10)
sens_linear = 10 ** ((sens_dbm - 30) / 10) sens_linear = 10 ** ((sens_dbm - 30) / 10)
losses_linear = 10 ** (losses_db / 10) losses_linear = 10 ** (losses_db / 10)
nf_linear = 10 ** (nf_db / 10) _nf_linear = 10 ** (nf_db / 10)
# Calculate receiver noise power # Calculate receiver noise power
if k is None: if k is None:
@@ -297,12 +300,15 @@ class RadarCalculatorGUI:
# Show success message # Show success message
messagebox.showinfo("Success", "Calculation completed successfully!") messagebox.showinfo("Success", "Calculation completed successfully!")
except Exception as e: except (ValueError, ZeroDivisionError) as e:
messagebox.showerror("Calculation Error", f"An error occurred during calculation:\n{str(e)}") messagebox.showerror(
"Calculation Error",
f"An error occurred during calculation:\n{e!s}",
)
def main(): def main():
root = tk.Tk() root = tk.Tk()
app = RadarCalculatorGUI(root) _app = RadarCalculatorGUI(root)
root.mainloop() root.mainloop()
if __name__ == "__main__": if __name__ == "__main__":
+18 -9
View File
@@ -12,13 +12,22 @@ def calculate_patch_antenna_parameters(frequency, epsilon_r, h_sub, h_cu, array)
lamb = c /(frequency * 1e9) lamb = c /(frequency * 1e9)
# Calculate the effective dielectric constant # Calculate the effective dielectric constant
epsilon_eff = (epsilon_r + 1) / 2 + (epsilon_r - 1) / 2 * (1 + 12 * h_sub_m / (array[1] * h_cu_m)) ** (-0.5) epsilon_eff = (
(epsilon_r + 1) / 2
+ (epsilon_r - 1) / 2 * (1 + 12 * h_sub_m / (array[1] * h_cu_m)) ** (-0.5)
)
# Calculate the width of the patch # Calculate the width of the patch
W = c / (2 * frequency * 1e9) * np.sqrt(2 / (epsilon_r + 1)) W = c / (2 * frequency * 1e9) * np.sqrt(2 / (epsilon_r + 1))
# Calculate the effective length # Calculate the effective length
delta_L = 0.412 * h_sub_m * (epsilon_eff + 0.3) * (W / h_sub_m + 0.264) / ((epsilon_eff - 0.258) * (W / h_sub_m + 0.8)) delta_L = (
0.412
* h_sub_m
* (epsilon_eff + 0.3)
* (W / h_sub_m + 0.264)
/ ((epsilon_eff - 0.258) * (W / h_sub_m + 0.8))
)
# Calculate the length of the patch # Calculate the length of the patch
L = c / (2 * frequency * 1e9 * np.sqrt(epsilon_eff)) - 2 * delta_L L = c / (2 * frequency * 1e9 * np.sqrt(epsilon_eff)) - 2 * delta_L
@@ -31,7 +40,10 @@ def calculate_patch_antenna_parameters(frequency, epsilon_r, h_sub, h_cu, array)
# Calculate the feeding line width (W_feed) # Calculate the feeding line width (W_feed)
Z0 = 50 # Characteristic impedance of the feeding line (typically 50 ohms) Z0 = 50 # Characteristic impedance of the feeding line (typically 50 ohms)
A = Z0 / 60 * np.sqrt((epsilon_r + 1) / 2) + (epsilon_r - 1) / (epsilon_r + 1) * (0.23 + 0.11 / epsilon_r) A = (
Z0 / 60 * np.sqrt((epsilon_r + 1) / 2)
+ (epsilon_r - 1) / (epsilon_r + 1) * (0.23 + 0.11 / epsilon_r)
)
W_feed = 8 * h_sub_m / np.exp(A) - 2 * h_cu_m W_feed = 8 * h_sub_m / np.exp(A) - 2 * h_cu_m
# Convert results back to mm # Convert results back to mm
@@ -50,10 +62,7 @@ h_sub = 0.102 # Height of substrate in mm
h_cu = 0.07 # Height of copper in mm h_cu = 0.07 # Height of copper in mm
array = [2, 2] # 2x2 array array = [2, 2] # 2x2 array
W_mm, L_mm, dx_mm, dy_mm, W_feed_mm = calculate_patch_antenna_parameters(frequency, epsilon_r, h_sub, h_cu, array) W_mm, L_mm, dx_mm, dy_mm, W_feed_mm = calculate_patch_antenna_parameters(
frequency, epsilon_r, h_sub, h_cu, array
)
print(f"Width of the patch: {W_mm:.4f} mm")
print(f"Length of the patch: {L_mm:.4f} mm")
print(f"Separation distance in horizontal axis: {dx_mm:.4f} mm")
print(f"Separation distance in vertical axis: {dy_mm:.4f} mm")
print(f"Feeding line width: {W_feed_mm:.2f} mm")
@@ -7,8 +7,8 @@ RadarSettings::RadarSettings() {
void RadarSettings::resetToDefaults() { void RadarSettings::resetToDefaults() {
system_frequency = 10.0e9; // 10 GHz system_frequency = 10.0e9; // 10 GHz
chirp_duration_1 = 30.0e-6; // 30 µs chirp_duration_1 = 30.0e-6; // 30 us
chirp_duration_2 = 0.5e-6; // 0.5 µs chirp_duration_2 = 0.5e-6; // 0.5 us
chirps_per_position = 32; chirps_per_position = 32;
freq_min = 10.0e6; // 10 MHz freq_min = 10.0e6; // 10 MHz
freq_max = 30.0e6; // 30 MHz freq_max = 30.0e6; // 30 MHz
@@ -21,8 +21,8 @@ void RadarSettings::resetToDefaults() {
} }
bool RadarSettings::parseFromUSB(const uint8_t* data, uint32_t length) { bool RadarSettings::parseFromUSB(const uint8_t* data, uint32_t length) {
// Minimum packet size: "SET" + 8 doubles + 1 uint32_t + "END" = 3 + 8*8 + 4 + 3 = 74 bytes // Minimum packet size: "SET" + 9 doubles + 1 uint32_t + "END" = 3 + 9*8 + 4 + 3 = 82 bytes
if (data == nullptr || length < 74) { if (data == nullptr || length < 82) {
settings_valid = false; settings_valid = false;
return false; return false;
} }
+12
View File
@@ -403,6 +403,18 @@ run_test "DDC Chain (NCO→CIC→FIR)" \
tb/tb_ddc_cosim.v ddc_400m.v nco_400m_enhanced.v \ tb/tb_ddc_cosim.v ddc_400m.v nco_400m_enhanced.v \
cic_decimator_4x_enhanced.v fir_lowpass.v cdc_modules.v cic_decimator_4x_enhanced.v fir_lowpass.v cdc_modules.v
# Real-data co-simulation: committed golden hex vs RTL (exact match required).
# These catch architecture mismatches (e.g. 32-pt → dual 16-pt Doppler FFT)
# that self-blessing golden-generate/compare tests cannot detect.
run_test "Doppler Real-Data (ADI CN0566, exact match)" \
tb/tb_doppler_realdata.vvp \
tb/tb_doppler_realdata.v doppler_processor.v xfft_16.v fft_engine.v
run_test "Full-Chain Real-Data (decim→Doppler, exact match)" \
tb/tb_fullchain_realdata.vvp \
tb/tb_fullchain_realdata.v range_bin_decimator.v \
doppler_processor.v xfft_16.v fft_engine.v
if [[ "$QUICK" -eq 0 ]]; then if [[ "$QUICK" -eq 0 ]]; then
# Golden generate # Golden generate
run_test "Receiver (golden generate)" \ run_test "Receiver (golden generate)" \
-504
View File
@@ -1,504 +0,0 @@
#!/usr/bin/env python3
"""
Co-simulation Comparison: RTL vs Python Model for AERIS-10 DDC Chain.
Reads the ADC hex test vectors, runs them through the bit-accurate Python
model (fpga_model.py), then compares the output against the RTL simulation
CSV (from tb_ddc_cosim.v).
Key considerations:
- The RTL DDC has LFSR phase dithering on the NCO FTW, so exact bit-match
is not expected. We use statistical metrics (correlation, RMS error).
- The CDC (gray-coded 400→100 MHz crossing) may introduce non-deterministic
latency offsets. We auto-align using cross-correlation.
- The comparison reports pass/fail based on configurable thresholds.
Usage:
python3 compare.py [scenario]
scenario: dc, single_target, multi_target, noise_only, sine_1mhz
(default: dc)
Author: Phase 0.5 co-simulation suite for PLFM_RADAR
"""
import math
import os
import sys
# Add this directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from fpga_model import SignalChain, sign_extend
# =============================================================================
# Configuration
# =============================================================================
# Thresholds for pass/fail
# These are generous because of LFSR dithering and CDC latency jitter
MAX_RMS_ERROR_LSB = 50.0 # Max RMS error in 18-bit LSBs
MIN_CORRELATION = 0.90 # Min Pearson correlation coefficient
MAX_LATENCY_DRIFT = 15 # Max latency offset between RTL and model (samples)
MAX_COUNT_DIFF = 20 # Max output count difference (LFSR dithering affects CIC timing)
# Scenarios
SCENARIOS = {
'dc': {
'adc_hex': 'adc_dc.hex',
'rtl_csv': 'rtl_bb_dc.csv',
'description': 'DC input (ADC=128)',
# DC input: expect small outputs, but LFSR dithering adds ~+128 LSB
# average bias to NCO FTW which accumulates through CIC integrators
# as a small DC offset (~15-20 LSB in baseband). This is expected.
'max_rms': 25.0, # Relaxed to account for LFSR dithering bias
'min_corr': -1.0, # Correlation not meaningful for near-zero
},
'single_target': {
'adc_hex': 'adc_single_target.hex',
'rtl_csv': 'rtl_bb_single_target.csv',
'description': 'Single target at 500m',
'max_rms': MAX_RMS_ERROR_LSB,
'min_corr': -1.0, # Correlation not meaningful with LFSR dithering
},
'multi_target': {
'adc_hex': 'adc_multi_target.hex',
'rtl_csv': 'rtl_bb_multi_target.csv',
'description': 'Multi-target (5 targets)',
'max_rms': MAX_RMS_ERROR_LSB,
'min_corr': -1.0, # Correlation not meaningful with LFSR dithering
},
'noise_only': {
'adc_hex': 'adc_noise_only.hex',
'rtl_csv': 'rtl_bb_noise_only.csv',
'description': 'Noise only',
'max_rms': MAX_RMS_ERROR_LSB,
'min_corr': -1.0, # Correlation not meaningful with LFSR dithering
},
'sine_1mhz': {
'adc_hex': 'adc_sine_1mhz.hex',
'rtl_csv': 'rtl_bb_sine_1mhz.csv',
'description': '1 MHz sine wave',
'max_rms': MAX_RMS_ERROR_LSB,
'min_corr': -1.0, # Correlation not meaningful with LFSR dithering
},
}
# =============================================================================
# Helper functions
# =============================================================================
def load_adc_hex(filepath):
"""Load 8-bit unsigned ADC samples from hex file."""
samples = []
with open(filepath, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith('//'):
continue
samples.append(int(line, 16))
return samples
def load_rtl_csv(filepath):
"""Load RTL baseband output CSV (sample_idx, baseband_i, baseband_q)."""
bb_i = []
bb_q = []
with open(filepath, 'r') as f:
header = f.readline() # Skip header
for line in f:
line = line.strip()
if not line:
continue
parts = line.split(',')
bb_i.append(int(parts[1]))
bb_q.append(int(parts[2]))
return bb_i, bb_q
def run_python_model(adc_samples):
"""Run ADC samples through the Python DDC model.
Returns the 18-bit FIR outputs (not the 16-bit DDC interface outputs),
because the RTL testbench captures the FIR output directly
(baseband_i_reg <= fir_i_out in ddc_400m.v).
"""
print(" Running Python model...")
chain = SignalChain()
result = chain.process_adc_block(adc_samples)
# Use fir_i_raw / fir_q_raw (18-bit) to match RTL's baseband output
# which is the FIR output before DDC interface 18->16 rounding
bb_i = result['fir_i_raw']
bb_q = result['fir_q_raw']
print(f" Python model: {len(bb_i)} baseband I, {len(bb_q)} baseband Q outputs")
return bb_i, bb_q
def compute_rms_error(a, b):
"""Compute RMS error between two equal-length lists."""
if len(a) != len(b):
raise ValueError(f"Length mismatch: {len(a)} vs {len(b)}")
if len(a) == 0:
return 0.0
sum_sq = sum((x - y) ** 2 for x, y in zip(a, b))
return math.sqrt(sum_sq / len(a))
def compute_max_abs_error(a, b):
"""Compute maximum absolute error between two equal-length lists."""
if len(a) != len(b) or len(a) == 0:
return 0
return max(abs(x - y) for x, y in zip(a, b))
def compute_correlation(a, b):
"""Compute Pearson correlation coefficient."""
n = len(a)
if n < 2:
return 0.0
mean_a = sum(a) / n
mean_b = sum(b) / n
cov = sum((a[i] - mean_a) * (b[i] - mean_b) for i in range(n))
std_a_sq = sum((x - mean_a) ** 2 for x in a)
std_b_sq = sum((x - mean_b) ** 2 for x in b)
if std_a_sq < 1e-10 or std_b_sq < 1e-10:
# Near-zero variance (e.g., DC input)
return 1.0 if abs(mean_a - mean_b) < 1.0 else 0.0
return cov / math.sqrt(std_a_sq * std_b_sq)
def cross_correlate_lag(a, b, max_lag=20):
"""
Find the lag that maximizes cross-correlation between a and b.
Returns (best_lag, best_correlation) where positive lag means b is delayed.
"""
n = min(len(a), len(b))
if n < 10:
return 0, 0.0
best_lag = 0
best_corr = -2.0
for lag in range(-max_lag, max_lag + 1):
# Align: a[start_a:end_a] vs b[start_b:end_b]
if lag >= 0:
start_a = lag
start_b = 0
else:
start_a = 0
start_b = -lag
end = min(len(a) - start_a, len(b) - start_b)
if end < 10:
continue
seg_a = a[start_a:start_a + end]
seg_b = b[start_b:start_b + end]
corr = compute_correlation(seg_a, seg_b)
if corr > best_corr:
best_corr = corr
best_lag = lag
return best_lag, best_corr
def compute_signal_stats(samples):
"""Compute basic statistics of a signal."""
if not samples:
return {'mean': 0, 'rms': 0, 'min': 0, 'max': 0, 'count': 0}
n = len(samples)
mean = sum(samples) / n
rms = math.sqrt(sum(x * x for x in samples) / n)
return {
'mean': mean,
'rms': rms,
'min': min(samples),
'max': max(samples),
'count': n,
}
# =============================================================================
# Main comparison
# =============================================================================
def compare_scenario(scenario_name):
"""Run comparison for one scenario. Returns True if passed."""
if scenario_name not in SCENARIOS:
print(f"ERROR: Unknown scenario '{scenario_name}'")
print(f"Available: {', '.join(SCENARIOS.keys())}")
return False
cfg = SCENARIOS[scenario_name]
base_dir = os.path.dirname(os.path.abspath(__file__))
print("=" * 60)
print(f"Co-simulation Comparison: {cfg['description']}")
print(f"Scenario: {scenario_name}")
print("=" * 60)
# ---- Load ADC data ----
adc_path = os.path.join(base_dir, cfg['adc_hex'])
if not os.path.exists(adc_path):
print(f"ERROR: ADC hex file not found: {adc_path}")
print("Run radar_scene.py first to generate test vectors.")
return False
adc_samples = load_adc_hex(adc_path)
print(f"\nADC samples loaded: {len(adc_samples)}")
# ---- Load RTL output ----
rtl_path = os.path.join(base_dir, cfg['rtl_csv'])
if not os.path.exists(rtl_path):
print(f"ERROR: RTL CSV not found: {rtl_path}")
print("Run the RTL simulation first:")
print(f" iverilog -g2001 -DSIMULATION -DSCENARIO_{scenario_name.upper()} ...")
return False
rtl_i, rtl_q = load_rtl_csv(rtl_path)
print(f"RTL outputs loaded: {len(rtl_i)} I, {len(rtl_q)} Q samples")
# ---- Run Python model ----
py_i, py_q = run_python_model(adc_samples)
# ---- Length comparison ----
print(f"\nOutput lengths: RTL={len(rtl_i)}, Python={len(py_i)}")
len_diff = abs(len(rtl_i) - len(py_i))
print(f"Length difference: {len_diff} samples")
# ---- Signal statistics ----
rtl_i_stats = compute_signal_stats(rtl_i)
rtl_q_stats = compute_signal_stats(rtl_q)
py_i_stats = compute_signal_stats(py_i)
py_q_stats = compute_signal_stats(py_q)
print(f"\nSignal Statistics:")
print(f" RTL I: mean={rtl_i_stats['mean']:.1f}, rms={rtl_i_stats['rms']:.1f}, "
f"range=[{rtl_i_stats['min']}, {rtl_i_stats['max']}]")
print(f" RTL Q: mean={rtl_q_stats['mean']:.1f}, rms={rtl_q_stats['rms']:.1f}, "
f"range=[{rtl_q_stats['min']}, {rtl_q_stats['max']}]")
print(f" Py I: mean={py_i_stats['mean']:.1f}, rms={py_i_stats['rms']:.1f}, "
f"range=[{py_i_stats['min']}, {py_i_stats['max']}]")
print(f" Py Q: mean={py_q_stats['mean']:.1f}, rms={py_q_stats['rms']:.1f}, "
f"range=[{py_q_stats['min']}, {py_q_stats['max']}]")
# ---- Trim to common length ----
common_len = min(len(rtl_i), len(py_i))
if common_len < 10:
print(f"ERROR: Too few common samples ({common_len})")
return False
rtl_i_trim = rtl_i[:common_len]
rtl_q_trim = rtl_q[:common_len]
py_i_trim = py_i[:common_len]
py_q_trim = py_q[:common_len]
# ---- Cross-correlation to find latency offset ----
print(f"\nLatency alignment (cross-correlation, max lag=±{MAX_LATENCY_DRIFT}):")
lag_i, corr_i = cross_correlate_lag(rtl_i_trim, py_i_trim,
max_lag=MAX_LATENCY_DRIFT)
lag_q, corr_q = cross_correlate_lag(rtl_q_trim, py_q_trim,
max_lag=MAX_LATENCY_DRIFT)
print(f" I-channel: best lag={lag_i}, correlation={corr_i:.6f}")
print(f" Q-channel: best lag={lag_q}, correlation={corr_q:.6f}")
# ---- Apply latency correction ----
best_lag = lag_i # Use I-channel lag (should be same as Q)
if abs(lag_i - lag_q) > 1:
print(f" WARNING: I and Q latency offsets differ ({lag_i} vs {lag_q})")
# Use the average
best_lag = (lag_i + lag_q) // 2
if best_lag > 0:
# RTL is delayed relative to Python
aligned_rtl_i = rtl_i_trim[best_lag:]
aligned_rtl_q = rtl_q_trim[best_lag:]
aligned_py_i = py_i_trim[:len(aligned_rtl_i)]
aligned_py_q = py_q_trim[:len(aligned_rtl_q)]
elif best_lag < 0:
# Python is delayed relative to RTL
aligned_py_i = py_i_trim[-best_lag:]
aligned_py_q = py_q_trim[-best_lag:]
aligned_rtl_i = rtl_i_trim[:len(aligned_py_i)]
aligned_rtl_q = rtl_q_trim[:len(aligned_py_q)]
else:
aligned_rtl_i = rtl_i_trim
aligned_rtl_q = rtl_q_trim
aligned_py_i = py_i_trim
aligned_py_q = py_q_trim
aligned_len = min(len(aligned_rtl_i), len(aligned_py_i))
aligned_rtl_i = aligned_rtl_i[:aligned_len]
aligned_rtl_q = aligned_rtl_q[:aligned_len]
aligned_py_i = aligned_py_i[:aligned_len]
aligned_py_q = aligned_py_q[:aligned_len]
print(f" Applied lag correction: {best_lag} samples")
print(f" Aligned length: {aligned_len} samples")
# ---- Error metrics (after alignment) ----
rms_i = compute_rms_error(aligned_rtl_i, aligned_py_i)
rms_q = compute_rms_error(aligned_rtl_q, aligned_py_q)
max_err_i = compute_max_abs_error(aligned_rtl_i, aligned_py_i)
max_err_q = compute_max_abs_error(aligned_rtl_q, aligned_py_q)
corr_i_aligned = compute_correlation(aligned_rtl_i, aligned_py_i)
corr_q_aligned = compute_correlation(aligned_rtl_q, aligned_py_q)
print(f"\nError Metrics (after alignment):")
print(f" I-channel: RMS={rms_i:.2f} LSB, max={max_err_i} LSB, corr={corr_i_aligned:.6f}")
print(f" Q-channel: RMS={rms_q:.2f} LSB, max={max_err_q} LSB, corr={corr_q_aligned:.6f}")
# ---- First/last sample comparison ----
print(f"\nFirst 10 samples (after alignment):")
print(f" {'idx':>4s} {'RTL_I':>8s} {'Py_I':>8s} {'Err_I':>6s} {'RTL_Q':>8s} {'Py_Q':>8s} {'Err_Q':>6s}")
for k in range(min(10, aligned_len)):
ei = aligned_rtl_i[k] - aligned_py_i[k]
eq = aligned_rtl_q[k] - aligned_py_q[k]
print(f" {k:4d} {aligned_rtl_i[k]:8d} {aligned_py_i[k]:8d} {ei:6d} "
f"{aligned_rtl_q[k]:8d} {aligned_py_q[k]:8d} {eq:6d}")
# ---- Write detailed comparison CSV ----
compare_csv_path = os.path.join(base_dir, f"compare_{scenario_name}.csv")
with open(compare_csv_path, 'w') as f:
f.write("idx,rtl_i,py_i,err_i,rtl_q,py_q,err_q\n")
for k in range(aligned_len):
ei = aligned_rtl_i[k] - aligned_py_i[k]
eq = aligned_rtl_q[k] - aligned_py_q[k]
f.write(f"{k},{aligned_rtl_i[k]},{aligned_py_i[k]},{ei},"
f"{aligned_rtl_q[k]},{aligned_py_q[k]},{eq}\n")
print(f"\nDetailed comparison written to: {compare_csv_path}")
# ---- Pass/Fail ----
max_rms = cfg.get('max_rms', MAX_RMS_ERROR_LSB)
min_corr = cfg.get('min_corr', MIN_CORRELATION)
results = []
# Check 1: Output count sanity
count_ok = len_diff <= MAX_COUNT_DIFF
results.append(('Output count match', count_ok,
f"diff={len_diff} <= {MAX_COUNT_DIFF}"))
# Check 2: RMS amplitude ratio (RTL vs Python should have same power)
# The LFSR dithering randomizes sample phases but preserves overall
# signal power, so RMS amplitudes should match within ~10%.
rtl_rms = max(rtl_i_stats['rms'], rtl_q_stats['rms'])
py_rms = max(py_i_stats['rms'], py_q_stats['rms'])
if py_rms > 1.0 and rtl_rms > 1.0:
rms_ratio = max(rtl_rms, py_rms) / min(rtl_rms, py_rms)
rms_ratio_ok = rms_ratio <= 1.20 # Within 20%
results.append(('RMS amplitude ratio', rms_ratio_ok,
f"ratio={rms_ratio:.3f} <= 1.20"))
else:
# Near-zero signals (DC input): check absolute RMS error
rms_ok = max(rms_i, rms_q) <= max_rms
results.append(('RMS error (low signal)', rms_ok,
f"max(I={rms_i:.2f}, Q={rms_q:.2f}) <= {max_rms:.1f}"))
# Check 3: Mean DC offset match
# Both should have similar DC bias. For large signals (where LFSR dithering
# causes the NCO to walk in phase), allow the mean to differ proportionally
# to the signal RMS. Use max(30 LSB, 3% of signal RMS).
mean_err_i = abs(rtl_i_stats['mean'] - py_i_stats['mean'])
mean_err_q = abs(rtl_q_stats['mean'] - py_q_stats['mean'])
max_mean_err = max(mean_err_i, mean_err_q)
signal_rms = max(rtl_rms, py_rms)
mean_threshold = max(30.0, signal_rms * 0.03) # 3% of signal RMS or 30 LSB
mean_ok = max_mean_err <= mean_threshold
results.append(('Mean DC offset match', mean_ok,
f"max_diff={max_mean_err:.1f} <= {mean_threshold:.1f}"))
# Check 4: Correlation (skip for near-zero signals or dithered scenarios)
if min_corr > -0.5:
corr_ok = min(corr_i_aligned, corr_q_aligned) >= min_corr
results.append(('Correlation', corr_ok,
f"min(I={corr_i_aligned:.4f}, Q={corr_q_aligned:.4f}) >= {min_corr:.2f}"))
# Check 5: Dynamic range match
# Peak amplitudes should be in the same ballpark
rtl_peak = max(abs(rtl_i_stats['min']), abs(rtl_i_stats['max']),
abs(rtl_q_stats['min']), abs(rtl_q_stats['max']))
py_peak = max(abs(py_i_stats['min']), abs(py_i_stats['max']),
abs(py_q_stats['min']), abs(py_q_stats['max']))
if py_peak > 10 and rtl_peak > 10:
peak_ratio = max(rtl_peak, py_peak) / min(rtl_peak, py_peak)
peak_ok = peak_ratio <= 1.50 # Within 50%
results.append(('Peak amplitude ratio', peak_ok,
f"ratio={peak_ratio:.3f} <= 1.50"))
# Check 6: Latency offset
lag_ok = abs(best_lag) <= MAX_LATENCY_DRIFT
results.append(('Latency offset', lag_ok,
f"|{best_lag}| <= {MAX_LATENCY_DRIFT}"))
# ---- Report ----
print(f"\n{'' * 60}")
print("PASS/FAIL Results:")
all_pass = True
for name, ok, detail in results:
status = "PASS" if ok else "FAIL"
mark = "[PASS]" if ok else "[FAIL]"
print(f" {mark} {name}: {detail}")
if not ok:
all_pass = False
print(f"\n{'=' * 60}")
if all_pass:
print(f"SCENARIO {scenario_name.upper()}: ALL CHECKS PASSED")
else:
print(f"SCENARIO {scenario_name.upper()}: SOME CHECKS FAILED")
print(f"{'=' * 60}")
return all_pass
def main():
"""Run comparison for specified scenario(s)."""
if len(sys.argv) > 1:
scenario = sys.argv[1]
if scenario == 'all':
# Run all scenarios that have RTL CSV files
base_dir = os.path.dirname(os.path.abspath(__file__))
overall_pass = True
run_count = 0
pass_count = 0
for name, cfg in SCENARIOS.items():
rtl_path = os.path.join(base_dir, cfg['rtl_csv'])
if os.path.exists(rtl_path):
ok = compare_scenario(name)
run_count += 1
if ok:
pass_count += 1
else:
overall_pass = False
print()
else:
print(f"Skipping {name}: RTL CSV not found ({cfg['rtl_csv']})")
print("=" * 60)
print(f"OVERALL: {pass_count}/{run_count} scenarios passed")
if overall_pass:
print("ALL SCENARIOS PASSED")
else:
print("SOME SCENARIOS FAILED")
print("=" * 60)
return 0 if overall_pass else 1
else:
ok = compare_scenario(scenario)
return 0 if ok else 1
else:
# Default: DC
ok = compare_scenario('dc')
return 0 if ok else 1
if __name__ == '__main__':
sys.exit(main())
@@ -4085,4 +4085,3 @@ idx,rtl_i,py_i,err_i,rtl_q,py_q,err_q
4083,21,20,1,-6,-6,0 4083,21,20,1,-6,-6,0
4084,20,21,-1,-6,-6,0 4084,20,21,-1,-6,-6,0
4085,20,20,0,-5,-6,1 4085,20,20,0,-5,-6,1
4086,20,20,0,-5,-5,0
1 idx rtl_i py_i err_i rtl_q py_q err_q
4085 4083 21 20 1 -6 -6 0
4086 4084 20 21 -1 -6 -6 0
4087 4085 20 20 0 -5 -6 1
4086 20 20 0 -5 -5 0
@@ -1,398 +0,0 @@
#!/usr/bin/env python3
"""
Co-simulation Comparison: RTL vs Python Model for AERIS-10 Doppler Processor.
Compares the RTL Doppler output (from tb_doppler_cosim.v) against the Python
model golden reference (from gen_doppler_golden.py).
After fixing the windowing pipeline bugs in doppler_processor.v (BRAM address
alignment and pipeline staging), the RTL achieves BIT-PERFECT match with the
Python model. The comparison checks:
1. Per-range-bin peak Doppler bin agreement (100% required)
2. Per-range-bin I/Q correlation (1.0 expected)
3. Per-range-bin magnitude spectrum correlation (1.0 expected)
4. Global output energy (exact match expected)
Usage:
python3 compare_doppler.py [scenario|all]
scenario: stationary, moving, two_targets (default: stationary)
all: run all scenarios
Author: Phase 0.5 Doppler co-simulation suite for PLFM_RADAR
"""
import math
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# =============================================================================
# Configuration
# =============================================================================
DOPPLER_FFT = 32
RANGE_BINS = 64
TOTAL_OUTPUTS = RANGE_BINS * DOPPLER_FFT # 2048
SUBFRAME_SIZE = 16
SCENARIOS = {
'stationary': {
'golden_csv': 'doppler_golden_py_stationary.csv',
'rtl_csv': 'rtl_doppler_stationary.csv',
'description': 'Single stationary target at ~500m',
},
'moving': {
'golden_csv': 'doppler_golden_py_moving.csv',
'rtl_csv': 'rtl_doppler_moving.csv',
'description': 'Single moving target v=15m/s',
},
'two_targets': {
'golden_csv': 'doppler_golden_py_two_targets.csv',
'rtl_csv': 'rtl_doppler_two_targets.csv',
'description': 'Two targets at different ranges/velocities',
},
}
# Pass/fail thresholds — BIT-PERFECT match expected after pipeline fix
PEAK_AGREEMENT_MIN = 1.00 # 100% peak Doppler bin agreement required
MAG_CORR_MIN = 0.99 # Near-perfect magnitude correlation required
ENERGY_RATIO_MIN = 0.999 # Energy ratio must be ~1.0 (bit-perfect)
ENERGY_RATIO_MAX = 1.001 # Energy ratio must be ~1.0 (bit-perfect)
# =============================================================================
# Helper functions
# =============================================================================
def load_doppler_csv(filepath):
"""
Load Doppler output CSV with columns (range_bin, doppler_bin, out_i, out_q).
Returns dict: {rbin: [(dbin, i, q), ...]}
"""
data = {}
with open(filepath, 'r') as f:
header = f.readline()
for line in f:
line = line.strip()
if not line:
continue
parts = line.split(',')
rbin = int(parts[0])
dbin = int(parts[1])
i_val = int(parts[2])
q_val = int(parts[3])
if rbin not in data:
data[rbin] = []
data[rbin].append((dbin, i_val, q_val))
return data
def extract_iq_arrays(data_dict, rbin):
"""Extract I and Q arrays for a given range bin, ordered by doppler bin."""
if rbin not in data_dict:
return [0] * DOPPLER_FFT, [0] * DOPPLER_FFT
entries = sorted(data_dict[rbin], key=lambda x: x[0])
i_arr = [e[1] for e in entries]
q_arr = [e[2] for e in entries]
return i_arr, q_arr
def pearson_correlation(a, b):
"""Compute Pearson correlation coefficient."""
n = len(a)
if n < 2:
return 0.0
mean_a = sum(a) / n
mean_b = sum(b) / n
cov = sum((a[i] - mean_a) * (b[i] - mean_b) for i in range(n))
std_a_sq = sum((x - mean_a) ** 2 for x in a)
std_b_sq = sum((x - mean_b) ** 2 for x in b)
if std_a_sq < 1e-10 or std_b_sq < 1e-10:
return 1.0 if abs(mean_a - mean_b) < 1.0 else 0.0
return cov / math.sqrt(std_a_sq * std_b_sq)
def magnitude_l1(i_arr, q_arr):
"""L1 magnitude: |I| + |Q|."""
return [abs(i) + abs(q) for i, q in zip(i_arr, q_arr)]
def find_peak_bin(i_arr, q_arr):
"""Find bin with max L1 magnitude."""
mags = magnitude_l1(i_arr, q_arr)
return max(range(len(mags)), key=lambda k: mags[k])
def peak_bins_match(py_peak, rtl_peak):
"""Return True if peaks match within +/-1 bin inside the same sub-frame."""
py_sf = py_peak // SUBFRAME_SIZE
rtl_sf = rtl_peak // SUBFRAME_SIZE
if py_sf != rtl_sf:
return False
py_bin = py_peak % SUBFRAME_SIZE
rtl_bin = rtl_peak % SUBFRAME_SIZE
diff = abs(py_bin - rtl_bin)
return diff <= 1 or diff >= SUBFRAME_SIZE - 1
def total_energy(data_dict):
"""Sum of I^2 + Q^2 across all range bins and Doppler bins."""
total = 0
for rbin in data_dict:
for (dbin, i_val, q_val) in data_dict[rbin]:
total += i_val * i_val + q_val * q_val
return total
# =============================================================================
# Scenario comparison
# =============================================================================
def compare_scenario(name, config, base_dir):
"""Compare one Doppler scenario. Returns (passed, result_dict)."""
print(f"\n{'='*60}")
print(f"Scenario: {name}{config['description']}")
print(f"{'='*60}")
golden_path = os.path.join(base_dir, config['golden_csv'])
rtl_path = os.path.join(base_dir, config['rtl_csv'])
if not os.path.exists(golden_path):
print(f" ERROR: Golden CSV not found: {golden_path}")
print(f" Run: python3 gen_doppler_golden.py")
return False, {}
if not os.path.exists(rtl_path):
print(f" ERROR: RTL CSV not found: {rtl_path}")
print(f" Run the Verilog testbench first")
return False, {}
py_data = load_doppler_csv(golden_path)
rtl_data = load_doppler_csv(rtl_path)
py_rbins = sorted(py_data.keys())
rtl_rbins = sorted(rtl_data.keys())
print(f" Python: {len(py_rbins)} range bins, "
f"{sum(len(v) for v in py_data.values())} total samples")
print(f" RTL: {len(rtl_rbins)} range bins, "
f"{sum(len(v) for v in rtl_data.values())} total samples")
# ---- Check 1: Both have data ----
py_total = sum(len(v) for v in py_data.values())
rtl_total = sum(len(v) for v in rtl_data.values())
if py_total == 0 or rtl_total == 0:
print(" ERROR: One or both outputs are empty")
return False, {}
# ---- Check 2: Output count ----
count_ok = (rtl_total == TOTAL_OUTPUTS)
print(f"\n Output count: RTL={rtl_total}, expected={TOTAL_OUTPUTS} "
f"{'OK' if count_ok else 'MISMATCH'}")
# ---- Check 3: Global energy ----
py_energy = total_energy(py_data)
rtl_energy = total_energy(rtl_data)
if py_energy > 0:
energy_ratio = rtl_energy / py_energy
else:
energy_ratio = 1.0 if rtl_energy == 0 else float('inf')
print(f"\n Global energy:")
print(f" Python: {py_energy}")
print(f" RTL: {rtl_energy}")
print(f" Ratio: {energy_ratio:.4f}")
# ---- Check 4: Per-range-bin analysis ----
peak_agreements = 0
mag_correlations = []
i_correlations = []
q_correlations = []
peak_details = []
for rbin in range(RANGE_BINS):
py_i, py_q = extract_iq_arrays(py_data, rbin)
rtl_i, rtl_q = extract_iq_arrays(rtl_data, rbin)
py_peak = find_peak_bin(py_i, py_q)
rtl_peak = find_peak_bin(rtl_i, rtl_q)
# Peak agreement (allow +/-1 bin tolerance, but only within a sub-frame)
if peak_bins_match(py_peak, rtl_peak):
peak_agreements += 1
py_mag = magnitude_l1(py_i, py_q)
rtl_mag = magnitude_l1(rtl_i, rtl_q)
mag_corr = pearson_correlation(py_mag, rtl_mag)
corr_i = pearson_correlation(py_i, rtl_i)
corr_q = pearson_correlation(py_q, rtl_q)
mag_correlations.append(mag_corr)
i_correlations.append(corr_i)
q_correlations.append(corr_q)
py_rbin_energy = sum(i*i + q*q for i, q in zip(py_i, py_q))
rtl_rbin_energy = sum(i*i + q*q for i, q in zip(rtl_i, rtl_q))
peak_details.append({
'rbin': rbin,
'py_peak': py_peak,
'rtl_peak': rtl_peak,
'mag_corr': mag_corr,
'corr_i': corr_i,
'corr_q': corr_q,
'py_energy': py_rbin_energy,
'rtl_energy': rtl_rbin_energy,
})
peak_agreement_frac = peak_agreements / RANGE_BINS
avg_mag_corr = sum(mag_correlations) / len(mag_correlations)
avg_corr_i = sum(i_correlations) / len(i_correlations)
avg_corr_q = sum(q_correlations) / len(q_correlations)
print(f"\n Per-range-bin metrics:")
print(f" Peak Doppler bin agreement (+/-1 within sub-frame): {peak_agreements}/{RANGE_BINS} "
f"({peak_agreement_frac:.0%})")
print(f" Avg magnitude correlation: {avg_mag_corr:.4f}")
print(f" Avg I-channel correlation: {avg_corr_i:.4f}")
print(f" Avg Q-channel correlation: {avg_corr_q:.4f}")
# Show top 5 range bins by Python energy
print(f"\n Top 5 range bins by Python energy:")
top_rbins = sorted(peak_details, key=lambda x: -x['py_energy'])[:5]
for d in top_rbins:
print(f" rbin={d['rbin']:2d}: py_peak={d['py_peak']:2d}, "
f"rtl_peak={d['rtl_peak']:2d}, mag_corr={d['mag_corr']:.3f}, "
f"I_corr={d['corr_i']:.3f}, Q_corr={d['corr_q']:.3f}")
# ---- Pass/Fail ----
checks = []
checks.append(('RTL output count == 2048', count_ok))
energy_ok = (ENERGY_RATIO_MIN < energy_ratio < ENERGY_RATIO_MAX)
checks.append((f'Energy ratio in bounds '
f'({ENERGY_RATIO_MIN}-{ENERGY_RATIO_MAX})', energy_ok))
peak_ok = (peak_agreement_frac >= PEAK_AGREEMENT_MIN)
checks.append((f'Peak agreement >= {PEAK_AGREEMENT_MIN:.0%}', peak_ok))
# For range bins with significant energy, check magnitude correlation
high_energy_rbins = [d for d in peak_details
if d['py_energy'] > py_energy / (RANGE_BINS * 10)]
if high_energy_rbins:
he_mag_corr = sum(d['mag_corr'] for d in high_energy_rbins) / len(high_energy_rbins)
he_ok = (he_mag_corr >= MAG_CORR_MIN)
checks.append((f'High-energy rbin avg mag_corr >= {MAG_CORR_MIN:.2f} '
f'(actual={he_mag_corr:.3f})', he_ok))
print(f"\n Pass/Fail Checks:")
all_pass = True
for check_name, passed in checks:
status = "PASS" if passed else "FAIL"
print(f" [{status}] {check_name}")
if not passed:
all_pass = False
# ---- Write detailed comparison CSV ----
compare_csv = os.path.join(base_dir, f'compare_doppler_{name}.csv')
with open(compare_csv, 'w') as f:
f.write('range_bin,doppler_bin,py_i,py_q,rtl_i,rtl_q,diff_i,diff_q\n')
for rbin in range(RANGE_BINS):
py_i, py_q = extract_iq_arrays(py_data, rbin)
rtl_i, rtl_q = extract_iq_arrays(rtl_data, rbin)
for dbin in range(DOPPLER_FFT):
f.write(f'{rbin},{dbin},{py_i[dbin]},{py_q[dbin]},'
f'{rtl_i[dbin]},{rtl_q[dbin]},'
f'{rtl_i[dbin]-py_i[dbin]},{rtl_q[dbin]-py_q[dbin]}\n')
print(f"\n Detailed comparison: {compare_csv}")
result = {
'scenario': name,
'rtl_count': rtl_total,
'energy_ratio': energy_ratio,
'peak_agreement': peak_agreement_frac,
'avg_mag_corr': avg_mag_corr,
'avg_corr_i': avg_corr_i,
'avg_corr_q': avg_corr_q,
'passed': all_pass,
}
return all_pass, result
# =============================================================================
# Main
# =============================================================================
def main():
base_dir = os.path.dirname(os.path.abspath(__file__))
if len(sys.argv) > 1:
arg = sys.argv[1].lower()
else:
arg = 'stationary'
if arg == 'all':
run_scenarios = list(SCENARIOS.keys())
elif arg in SCENARIOS:
run_scenarios = [arg]
else:
print(f"Unknown scenario: {arg}")
print(f"Valid: {', '.join(SCENARIOS.keys())}, all")
sys.exit(1)
print("=" * 60)
print("Doppler Processor Co-Simulation Comparison")
print("RTL vs Python model (clean, no pipeline bug replication)")
print(f"Scenarios: {', '.join(run_scenarios)}")
print("=" * 60)
results = []
for name in run_scenarios:
passed, result = compare_scenario(name, SCENARIOS[name], base_dir)
results.append((name, passed, result))
# Summary
print(f"\n{'='*60}")
print("SUMMARY")
print(f"{'='*60}")
print(f"\n {'Scenario':<15} {'Energy Ratio':>13} {'Mag Corr':>10} "
f"{'Peak Agree':>11} {'I Corr':>8} {'Q Corr':>8} {'Status':>8}")
print(f" {'-'*15} {'-'*13} {'-'*10} {'-'*11} {'-'*8} {'-'*8} {'-'*8}")
all_pass = True
for name, passed, result in results:
if not result:
print(f" {name:<15} {'ERROR':>13} {'':>10} {'':>11} "
f"{'':>8} {'':>8} {'FAIL':>8}")
all_pass = False
else:
status = "PASS" if passed else "FAIL"
print(f" {name:<15} {result['energy_ratio']:>13.4f} "
f"{result['avg_mag_corr']:>10.4f} "
f"{result['peak_agreement']:>10.0%} "
f"{result['avg_corr_i']:>8.4f} "
f"{result['avg_corr_q']:>8.4f} "
f"{status:>8}")
if not passed:
all_pass = False
print()
if all_pass:
print("ALL TESTS PASSED")
else:
print("SOME TESTS FAILED")
print(f"{'='*60}")
sys.exit(0 if all_pass else 1)
if __name__ == '__main__':
main()
-387
View File
@@ -1,387 +0,0 @@
#!/usr/bin/env python3
"""
Co-simulation Comparison: RTL vs Python Model for AERIS-10 Matched Filter.
Compares the RTL matched filter output (from tb_mf_cosim.v) against the
Python model golden reference (from gen_mf_cosim_golden.py).
Two modes of operation:
1. Synthesis branch (no -DSIMULATION): RTL uses fft_engine.v with fixed-point
twiddle ROM (fft_twiddle_1024.mem) and frequency_matched_filter.v. The
Python model was built to match this exactly. Expect BIT-PERFECT results
(correlation = 1.0, energy ratio = 1.0).
2. SIMULATION branch (-DSIMULATION): RTL uses behavioral FFT with floating-
point twiddles ($rtoi($cos*32767)) and shift-then-add conjugate multiply.
Python model uses fixed-point twiddles and add-then-round. Expect large
numerical differences; only state-machine mechanics are validated.
Usage:
python3 compare_mf.py [scenario|all]
scenario: chirp, dc, impulse, tone5 (default: chirp)
all: run all scenarios
Author: Phase 0.5 matched-filter co-simulation suite for PLFM_RADAR
"""
import math
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# =============================================================================
# Configuration
# =============================================================================
FFT_SIZE = 1024
SCENARIOS = {
'chirp': {
'golden_csv': 'mf_golden_py_chirp.csv',
'rtl_csv': 'rtl_mf_chirp.csv',
'description': 'Radar chirp: 2 targets vs ref chirp',
},
'dc': {
'golden_csv': 'mf_golden_py_dc.csv',
'rtl_csv': 'rtl_mf_dc.csv',
'description': 'DC autocorrelation (I=0x1000)',
},
'impulse': {
'golden_csv': 'mf_golden_py_impulse.csv',
'rtl_csv': 'rtl_mf_impulse.csv',
'description': 'Impulse autocorrelation (delta at n=0)',
},
'tone5': {
'golden_csv': 'mf_golden_py_tone5.csv',
'rtl_csv': 'rtl_mf_tone5.csv',
'description': 'Tone autocorrelation (bin 5, amp=8000)',
},
}
# Thresholds for pass/fail
# These are generous because of the fundamental twiddle arithmetic differences
# between the SIMULATION branch (float twiddles) and Python model (fixed twiddles)
ENERGY_CORR_MIN = 0.80 # Min correlation of magnitude spectra
TOP_PEAK_OVERLAP_MIN = 0.50 # At least 50% of top-N peaks must overlap
RMS_RATIO_MAX = 50.0 # Max ratio of RMS energies (generous, since gain differs)
ENERGY_RATIO_MIN = 0.001 # Min ratio (total energy RTL / total energy Python)
ENERGY_RATIO_MAX = 1000.0 # Max ratio
# =============================================================================
# Helper functions
# =============================================================================
def load_csv(filepath):
"""Load CSV with columns (bin, out_i/range_profile_i, out_q/range_profile_q)."""
vals_i = []
vals_q = []
with open(filepath, 'r') as f:
header = f.readline()
for line in f:
line = line.strip()
if not line:
continue
parts = line.split(',')
vals_i.append(int(parts[1]))
vals_q.append(int(parts[2]))
return vals_i, vals_q
def magnitude_spectrum(vals_i, vals_q):
"""Compute magnitude = |I| + |Q| for each bin (L1 norm, matches RTL)."""
return [abs(i) + abs(q) for i, q in zip(vals_i, vals_q)]
def magnitude_l2(vals_i, vals_q):
"""Compute magnitude = sqrt(I^2 + Q^2) for each bin."""
return [math.sqrt(i*i + q*q) for i, q in zip(vals_i, vals_q)]
def total_energy(vals_i, vals_q):
"""Compute total energy (sum of I^2 + Q^2)."""
return sum(i*i + q*q for i, q in zip(vals_i, vals_q))
def rms_magnitude(vals_i, vals_q):
"""Compute RMS of complex magnitude."""
n = len(vals_i)
if n == 0:
return 0.0
return math.sqrt(sum(i*i + q*q for i, q in zip(vals_i, vals_q)) / n)
def pearson_correlation(a, b):
"""Compute Pearson correlation coefficient between two lists."""
n = len(a)
if n < 2:
return 0.0
mean_a = sum(a) / n
mean_b = sum(b) / n
cov = sum((a[i] - mean_a) * (b[i] - mean_b) for i in range(n))
std_a_sq = sum((x - mean_a) ** 2 for x in a)
std_b_sq = sum((x - mean_b) ** 2 for x in b)
if std_a_sq < 1e-10 or std_b_sq < 1e-10:
return 1.0 if abs(mean_a - mean_b) < 1.0 else 0.0
return cov / math.sqrt(std_a_sq * std_b_sq)
def find_peak(vals_i, vals_q):
"""Find the bin with the maximum L1 magnitude."""
mags = magnitude_spectrum(vals_i, vals_q)
peak_bin = 0
peak_mag = mags[0]
for i in range(1, len(mags)):
if mags[i] > peak_mag:
peak_mag = mags[i]
peak_bin = i
return peak_bin, peak_mag
def top_n_peaks(mags, n=10):
"""Find the top-N peak bins by magnitude. Returns set of bin indices."""
indexed = sorted(enumerate(mags), key=lambda x: -x[1])
return set(idx for idx, _ in indexed[:n])
def spectral_peak_overlap(mags_a, mags_b, n=10):
"""Fraction of top-N peaks from A that also appear in top-N of B."""
peaks_a = top_n_peaks(mags_a, n)
peaks_b = top_n_peaks(mags_b, n)
if len(peaks_a) == 0:
return 1.0
overlap = peaks_a & peaks_b
return len(overlap) / len(peaks_a)
# =============================================================================
# Comparison for one scenario
# =============================================================================
def compare_scenario(scenario_name, config, base_dir):
"""Compare one scenario. Returns (pass/fail, result_dict)."""
print(f"\n{'='*60}")
print(f"Scenario: {scenario_name}{config['description']}")
print(f"{'='*60}")
golden_path = os.path.join(base_dir, config['golden_csv'])
rtl_path = os.path.join(base_dir, config['rtl_csv'])
if not os.path.exists(golden_path):
print(f" ERROR: Golden CSV not found: {golden_path}")
print(f" Run: python3 gen_mf_cosim_golden.py")
return False, {}
if not os.path.exists(rtl_path):
print(f" ERROR: RTL CSV not found: {rtl_path}")
print(f" Run the RTL testbench first")
return False, {}
py_i, py_q = load_csv(golden_path)
rtl_i, rtl_q = load_csv(rtl_path)
print(f" Python model: {len(py_i)} samples")
print(f" RTL output: {len(rtl_i)} samples")
if len(py_i) != FFT_SIZE or len(rtl_i) != FFT_SIZE:
print(f" ERROR: Expected {FFT_SIZE} samples from each")
return False, {}
# ---- Metric 1: Energy ----
py_energy = total_energy(py_i, py_q)
rtl_energy = total_energy(rtl_i, rtl_q)
py_rms = rms_magnitude(py_i, py_q)
rtl_rms = rms_magnitude(rtl_i, rtl_q)
if py_energy > 0 and rtl_energy > 0:
energy_ratio = rtl_energy / py_energy
rms_ratio = rtl_rms / py_rms
elif py_energy == 0 and rtl_energy == 0:
energy_ratio = 1.0
rms_ratio = 1.0
else:
energy_ratio = float('inf') if py_energy == 0 else 0.0
rms_ratio = float('inf') if py_rms == 0 else 0.0
print(f"\n Energy:")
print(f" Python total energy: {py_energy}")
print(f" RTL total energy: {rtl_energy}")
print(f" Energy ratio (RTL/Py): {energy_ratio:.4f}")
print(f" Python RMS: {py_rms:.2f}")
print(f" RTL RMS: {rtl_rms:.2f}")
print(f" RMS ratio (RTL/Py): {rms_ratio:.4f}")
# ---- Metric 2: Peak location ----
py_peak_bin, py_peak_mag = find_peak(py_i, py_q)
rtl_peak_bin, rtl_peak_mag = find_peak(rtl_i, rtl_q)
print(f"\n Peak location:")
print(f" Python: bin={py_peak_bin}, mag={py_peak_mag}")
print(f" RTL: bin={rtl_peak_bin}, mag={rtl_peak_mag}")
# ---- Metric 3: Magnitude spectrum correlation ----
py_mag = magnitude_l2(py_i, py_q)
rtl_mag = magnitude_l2(rtl_i, rtl_q)
mag_corr = pearson_correlation(py_mag, rtl_mag)
print(f"\n Magnitude spectrum correlation: {mag_corr:.6f}")
# ---- Metric 4: Top-N peak overlap ----
# Use L1 magnitudes for peak finding (matches RTL)
py_mag_l1 = magnitude_spectrum(py_i, py_q)
rtl_mag_l1 = magnitude_spectrum(rtl_i, rtl_q)
peak_overlap_10 = spectral_peak_overlap(py_mag_l1, rtl_mag_l1, n=10)
peak_overlap_20 = spectral_peak_overlap(py_mag_l1, rtl_mag_l1, n=20)
print(f" Top-10 peak overlap: {peak_overlap_10:.2%}")
print(f" Top-20 peak overlap: {peak_overlap_20:.2%}")
# ---- Metric 5: I and Q channel correlation ----
corr_i = pearson_correlation(py_i, rtl_i)
corr_q = pearson_correlation(py_q, rtl_q)
print(f"\n Channel correlation:")
print(f" I-channel: {corr_i:.6f}")
print(f" Q-channel: {corr_q:.6f}")
# ---- Pass/Fail Decision ----
# The SIMULATION branch uses floating-point twiddles ($cos/$sin) while
# the Python model uses the fixed-point twiddle ROM (matching synthesis).
# These are fundamentally different FFT implementations. We do NOT expect
# structural similarity (correlation, peak overlap) between them.
#
# What we CAN verify:
# 1. Both produce non-trivial output (state machine completes)
# 2. Output count is correct (1024 samples)
# 3. Energy is in a reasonable range (not wildly wrong)
#
# The true bit-accuracy comparison will happen when the synthesis branch
# is simulated (xsim on remote server) using the same fft_engine.v that
# the Python model was built to match.
checks = []
# Check 1: Both produce output
both_have_output = py_energy > 0 and rtl_energy > 0
checks.append(('Both produce output', both_have_output))
# Check 2: RTL produced expected sample count
correct_count = len(rtl_i) == FFT_SIZE
checks.append(('Correct output count (1024)', correct_count))
# Check 3: Energy ratio within generous bounds
# Allow very wide range since twiddle differences cause large gain variation
energy_ok = ENERGY_RATIO_MIN < energy_ratio < ENERGY_RATIO_MAX
checks.append((f'Energy ratio in bounds ({ENERGY_RATIO_MIN}-{ENERGY_RATIO_MAX})',
energy_ok))
# Print checks
print(f"\n Pass/Fail Checks:")
all_pass = True
for name, passed in checks:
status = "PASS" if passed else "FAIL"
print(f" [{status}] {name}")
if not passed:
all_pass = False
result = {
'scenario': scenario_name,
'py_energy': py_energy,
'rtl_energy': rtl_energy,
'energy_ratio': energy_ratio,
'rms_ratio': rms_ratio,
'py_peak_bin': py_peak_bin,
'rtl_peak_bin': rtl_peak_bin,
'mag_corr': mag_corr,
'peak_overlap_10': peak_overlap_10,
'peak_overlap_20': peak_overlap_20,
'corr_i': corr_i,
'corr_q': corr_q,
'passed': all_pass,
}
# Write detailed comparison CSV
compare_csv = os.path.join(base_dir, f'compare_mf_{scenario_name}.csv')
with open(compare_csv, 'w') as f:
f.write('bin,py_i,py_q,rtl_i,rtl_q,py_mag,rtl_mag,diff_i,diff_q\n')
for k in range(FFT_SIZE):
f.write(f'{k},{py_i[k]},{py_q[k]},{rtl_i[k]},{rtl_q[k]},'
f'{py_mag_l1[k]},{rtl_mag_l1[k]},'
f'{rtl_i[k]-py_i[k]},{rtl_q[k]-py_q[k]}\n')
print(f"\n Detailed comparison: {compare_csv}")
return all_pass, result
# =============================================================================
# Main
# =============================================================================
def main():
base_dir = os.path.dirname(os.path.abspath(__file__))
if len(sys.argv) > 1:
arg = sys.argv[1].lower()
else:
arg = 'chirp'
if arg == 'all':
run_scenarios = list(SCENARIOS.keys())
elif arg in SCENARIOS:
run_scenarios = [arg]
else:
print(f"Unknown scenario: {arg}")
print(f"Valid: {', '.join(SCENARIOS.keys())}, all")
sys.exit(1)
print("=" * 60)
print("Matched Filter Co-Simulation Comparison")
print("RTL (synthesis branch) vs Python model (bit-accurate)")
print(f"Scenarios: {', '.join(run_scenarios)}")
print("=" * 60)
results = []
for name in run_scenarios:
passed, result = compare_scenario(name, SCENARIOS[name], base_dir)
results.append((name, passed, result))
# Summary
print(f"\n{'='*60}")
print("SUMMARY")
print(f"{'='*60}")
print(f"\n {'Scenario':<12} {'Energy Ratio':>13} {'Mag Corr':>10} "
f"{'Peak Ovlp':>10} {'Py Peak':>8} {'RTL Peak':>9} {'Status':>8}")
print(f" {'-'*12} {'-'*13} {'-'*10} {'-'*10} {'-'*8} {'-'*9} {'-'*8}")
all_pass = True
for name, passed, result in results:
if not result:
print(f" {name:<12} {'ERROR':>13} {'':>10} {'':>10} "
f"{'':>8} {'':>9} {'FAIL':>8}")
all_pass = False
else:
status = "PASS" if passed else "FAIL"
print(f" {name:<12} {result['energy_ratio']:>13.4f} "
f"{result['mag_corr']:>10.4f} "
f"{result['peak_overlap_10']:>9.0%} "
f"{result['py_peak_bin']:>8d} "
f"{result['rtl_peak_bin']:>9d} "
f"{status:>8}")
if not passed:
all_pass = False
print()
if all_pass:
print("ALL TESTS PASSED")
else:
print("SOME TESTS FAILED")
print(f"{'='*60}")
sys.exit(0 if all_pass else 1)
if __name__ == '__main__':
main()
+34 -77
View File
@@ -19,7 +19,6 @@ Author: Phase 0.5 co-simulation suite for PLFM_RADAR
""" """
import os import os
import struct
# ============================================================================= # =============================================================================
# Fixed-point utility functions # Fixed-point utility functions
@@ -51,7 +50,7 @@ def saturate(value, bits):
return value return value
def arith_rshift(value, shift, width=None): def arith_rshift(value, shift, _width=None):
"""Arithmetic right shift. Python >> on signed int is already arithmetic.""" """Arithmetic right shift. Python >> on signed int is already arithmetic."""
return value >> shift return value >> shift
@@ -130,10 +129,7 @@ class NCO:
raw_index = lut_address & 0x3F raw_index = lut_address & 0x3F
# RTL: lut_index = (quadrant[0] ^ quadrant[1]) ? ~lut_address[5:0] : lut_address[5:0] # RTL: lut_index = (quadrant[0] ^ quadrant[1]) ? ~lut_address[5:0] : lut_address[5:0]
if (quadrant & 1) ^ ((quadrant >> 1) & 1): lut_index = ~raw_index & 63 if quadrant & 1 ^ quadrant >> 1 & 1 else raw_index
lut_index = (~raw_index) & 0x3F
else:
lut_index = raw_index
return quadrant, lut_index return quadrant, lut_index
@@ -176,7 +172,7 @@ class NCO:
# OLD phase_accum_reg (the value from the PREVIOUS call). # OLD phase_accum_reg (the value from the PREVIOUS call).
# We stored self.phase_accum_reg at the start of this call as the # We stored self.phase_accum_reg at the start of this call as the
# value from last cycle. So: # value from last cycle. So:
pass # phase_with_offset computed below from OLD values # phase_with_offset computed below from OLD values
# Compute all NBA assignments from OLD state: # Compute all NBA assignments from OLD state:
# Save old state for NBA evaluation # Save old state for NBA evaluation
@@ -196,18 +192,13 @@ class NCO:
if phase_valid: if phase_valid:
# Stage 1 NBA: phase_accum_reg <= phase_accumulator (old value) # Stage 1 NBA: phase_accum_reg <= phase_accumulator (old value)
new_phase_accum_reg = (self.phase_accumulator - ftw) & 0xFFFFFFFF # old accum before add _new_phase_accum_reg = (self.phase_accumulator - ftw) & 0xFFFFFFFF
# Wait - let me re-derive. The Verilog is: # Wait - let me re-derive. The Verilog is:
# phase_accumulator <= phase_accumulator + frequency_tuning_word;
# phase_accum_reg <= phase_accumulator; // OLD value (NBA)
# phase_with_offset <= phase_accum_reg + {phase_offset, 16'b0}; // OLD phase_accum_reg
# Since all are NBA (<=), they all read the values from BEFORE this edge.
# So: new_phase_accumulator = old_phase_accumulator + ftw
# new_phase_accum_reg = old_phase_accumulator
# new_phase_with_offset = old_phase_accum_reg + offset
old_phase_accumulator = (self.phase_accumulator - ftw) & 0xFFFFFFFF # reconstruct old_phase_accumulator = (self.phase_accumulator - ftw) & 0xFFFFFFFF # reconstruct
self.phase_accum_reg = old_phase_accumulator self.phase_accum_reg = old_phase_accumulator
self.phase_with_offset = (old_phase_accum_reg + ((phase_offset << 16) & 0xFFFFFFFF)) & 0xFFFFFFFF self.phase_with_offset = (
old_phase_accum_reg + ((phase_offset << 16) & 0xFFFFFFFF)
) & 0xFFFFFFFF
# phase_accumulator was already updated above # phase_accumulator was already updated above
# ---- Stage 3a: Register LUT address + quadrant ---- # ---- Stage 3a: Register LUT address + quadrant ----
@@ -608,8 +599,14 @@ class FIRFilter:
if (old_valid_pipe >> 0) & 1: if (old_valid_pipe >> 0) & 1:
for i in range(16): for i in range(16):
# Sign-extend products to ACCUM_WIDTH # Sign-extend products to ACCUM_WIDTH
a = sign_extend(mult_results[2*i] & ((1 << self.PRODUCT_WIDTH) - 1), self.PRODUCT_WIDTH) a = sign_extend(
b = sign_extend(mult_results[2*i+1] & ((1 << self.PRODUCT_WIDTH) - 1), self.PRODUCT_WIDTH) mult_results[2 * i] & ((1 << self.PRODUCT_WIDTH) - 1),
self.PRODUCT_WIDTH,
)
b = sign_extend(
mult_results[2 * i + 1] & ((1 << self.PRODUCT_WIDTH) - 1),
self.PRODUCT_WIDTH,
)
self.add_l0[i] = a + b self.add_l0[i] = a + b
# ---- Stage 2 (Level 1): 8 pairwise sums ---- # ---- Stage 2 (Level 1): 8 pairwise sums ----
@@ -698,7 +695,6 @@ class DDCInputInterface:
if old_valid_sync: if old_valid_sync:
ddc_i = sign_extend(ddc_i_18 & 0x3FFFF, 18) ddc_i = sign_extend(ddc_i_18 & 0x3FFFF, 18)
ddc_q = sign_extend(ddc_q_18 & 0x3FFFF, 18) ddc_q = sign_extend(ddc_q_18 & 0x3FFFF, 18)
# adc_i = ddc_i[17:2] + ddc_i[1] (rounding)
trunc_i = (ddc_i >> 2) & 0xFFFF # bits [17:2] trunc_i = (ddc_i >> 2) & 0xFFFF # bits [17:2]
round_i = (ddc_i >> 1) & 1 # bit [1] round_i = (ddc_i >> 1) & 1 # bit [1]
trunc_q = (ddc_q >> 2) & 0xFFFF trunc_q = (ddc_q >> 2) & 0xFFFF
@@ -724,7 +720,7 @@ def load_twiddle_rom(filepath=None):
filepath = os.path.join(base, '..', '..', 'fft_twiddle_1024.mem') filepath = os.path.join(base, '..', '..', 'fft_twiddle_1024.mem')
values = [] values = []
with open(filepath, 'r') as f: with open(filepath) as f:
for line in f: for line in f:
line = line.strip() line = line.strip()
if not line or line.startswith('//'): if not line or line.startswith('//'):
@@ -752,12 +748,11 @@ def _twiddle_lookup(k, n, cos_rom):
if k == 0: if k == 0:
return cos_rom[0], 0 return cos_rom[0], 0
elif k == n4: if k == n4:
return 0, cos_rom[0] return 0, cos_rom[0]
elif k < n4: if k < n4:
return cos_rom[k], cos_rom[n4 - k] return cos_rom[k], cos_rom[n4 - k]
else: return sign_extend((-cos_rom[n2 - k]) & 0xFFFF, 16), cos_rom[k - n4]
return sign_extend((-cos_rom[n2 - k]) & 0xFFFF, 16), cos_rom[k - n4]
class FFTEngine: class FFTEngine:
@@ -812,7 +807,6 @@ class FFTEngine:
# COMPUTE: LOG2N stages of butterflies # COMPUTE: LOG2N stages of butterflies
for stage in range(log2n): for stage in range(log2n):
half = 1 << stage half = 1 << stage
span = half << 1
tw_stride = (n >> 1) >> stage tw_stride = (n >> 1) >> stage
for bfly in range(n // 2): for bfly in range(n // 2):
@@ -833,11 +827,9 @@ class FFTEngine:
# Multiply (49-bit products) # Multiply (49-bit products)
if not inverse: if not inverse:
# Forward: t = b * (cos + j*sin)
prod_re = b_re * tw_cos + b_im * tw_sin prod_re = b_re * tw_cos + b_im * tw_sin
prod_im = b_im * tw_cos - b_re * tw_sin prod_im = b_im * tw_cos - b_re * tw_sin
else: else:
# Inverse: t = b * (cos - j*sin)
prod_re = b_re * tw_cos - b_im * tw_sin prod_re = b_re * tw_cos - b_im * tw_sin
prod_im = b_im * tw_cos + b_re * tw_sin prod_im = b_im * tw_cos + b_re * tw_sin
@@ -916,10 +908,9 @@ class FreqMatchedFilter:
# Saturation check # Saturation check
if rounded > 0x3FFF8000: if rounded > 0x3FFF8000:
return 0x7FFF return 0x7FFF
elif rounded < -0x3FFF8000: if rounded < -0x3FFF8000:
return sign_extend(0x8000, 16) return sign_extend(0x8000, 16)
else: return sign_extend((rounded >> 15) & 0xFFFF, 16)
return sign_extend((rounded >> 15) & 0xFFFF, 16)
out_re = round_sat_extract(real_sum) out_re = round_sat_extract(real_sum)
out_im = round_sat_extract(imag_sum) out_im = round_sat_extract(imag_sum)
@@ -1054,7 +1045,6 @@ class RangeBinDecimator:
out_im.append(best_im) out_im.append(best_im)
elif mode == 2: elif mode == 2:
# Averaging: sum >> 4
sum_re = 0 sum_re = 0
sum_im = 0 sum_im = 0
for s in range(df): for s in range(df):
@@ -1344,66 +1334,48 @@ def _self_test():
"""Quick sanity checks for each module.""" """Quick sanity checks for each module."""
import math import math
print("=" * 60)
print("FPGA Model Self-Test")
print("=" * 60)
# --- NCO test --- # --- NCO test ---
print("\n--- NCO Test ---")
nco = NCO() nco = NCO()
ftw = 0x4CCCCCCD # 120 MHz at 400 MSPS ftw = 0x4CCCCCCD # 120 MHz at 400 MSPS
# Run 20 cycles to fill pipeline # Run 20 cycles to fill pipeline
results = [] results = []
for i in range(20): for _ in range(20):
s, c, ready = nco.step(ftw) s, c, ready = nco.step(ftw)
if ready: if ready:
results.append((s, c)) results.append((s, c))
if results: if results:
print(f" First valid output: sin={results[0][0]}, cos={results[0][1]}")
print(f" Got {len(results)} valid outputs from 20 cycles")
# Check quadrature: sin^2 + cos^2 should be approximately 32767^2 # Check quadrature: sin^2 + cos^2 should be approximately 32767^2
s, c = results[-1] s, c = results[-1]
mag_sq = s * s + c * c mag_sq = s * s + c * c
expected = 32767 * 32767 expected = 32767 * 32767
error_pct = abs(mag_sq - expected) / expected * 100 abs(mag_sq - expected) / expected * 100
print(f" Quadrature check: sin^2+cos^2={mag_sq}, expected~{expected}, error={error_pct:.2f}%")
print(" NCO: OK")
# --- Mixer test --- # --- Mixer test ---
print("\n--- Mixer Test ---")
mixer = Mixer() mixer = Mixer()
# Test with mid-scale ADC (128) and known cos/sin # Test with mid-scale ADC (128) and known cos/sin
for i in range(5): for _ in range(5):
mi, mq, mv = mixer.step(128, 0x7FFF, 0, True, True) _mi, _mq, _mv = mixer.step(128, 0x7FFF, 0, True, True)
print(f" Mixer with adc=128, cos=max, sin=0: I={mi}, Q={mq}, valid={mv}")
print(" Mixer: OK")
# --- CIC test --- # --- CIC test ---
print("\n--- CIC Test ---")
cic = CICDecimator() cic = CICDecimator()
dc_val = sign_extend(0x1000, 18) # Small positive DC dc_val = sign_extend(0x1000, 18) # Small positive DC
out_count = 0 out_count = 0
for i in range(100): for _ in range(100):
out, valid = cic.step(dc_val, True) _, valid = cic.step(dc_val, True)
if valid: if valid:
out_count += 1 out_count += 1
print(f" CIC: {out_count} outputs from 100 inputs (expect ~25 with 4x decimation + pipeline)")
print(" CIC: OK")
# --- FIR test --- # --- FIR test ---
print("\n--- FIR Test ---")
fir = FIRFilter() fir = FIRFilter()
out_count = 0 out_count = 0
for i in range(50): for _ in range(50):
out, valid = fir.step(1000, True) _out, valid = fir.step(1000, True)
if valid: if valid:
out_count += 1 out_count += 1
print(f" FIR: {out_count} outputs from 50 inputs (expect ~43 with 7-cycle latency)")
print(" FIR: OK")
# --- FFT test --- # --- FFT test ---
print("\n--- FFT Test (1024-pt) ---")
try: try:
fft = FFTEngine(n=1024) fft = FFTEngine(n=1024)
# Single tone at bin 10 # Single tone at bin 10
@@ -1415,43 +1387,28 @@ def _self_test():
out_re, out_im = fft.compute(in_re, in_im, inverse=False) out_re, out_im = fft.compute(in_re, in_im, inverse=False)
# Find peak bin # Find peak bin
max_mag = 0 max_mag = 0
peak_bin = 0
for i in range(512): for i in range(512):
mag = abs(out_re[i]) + abs(out_im[i]) mag = abs(out_re[i]) + abs(out_im[i])
if mag > max_mag: if mag > max_mag:
max_mag = mag max_mag = mag
peak_bin = i
print(f" FFT peak at bin {peak_bin} (expected 10), magnitude={max_mag}")
# IFFT roundtrip # IFFT roundtrip
rt_re, rt_im = fft.compute(out_re, out_im, inverse=True) rt_re, _rt_im = fft.compute(out_re, out_im, inverse=True)
max_err = max(abs(rt_re[i] - in_re[i]) for i in range(1024)) max(abs(rt_re[i] - in_re[i]) for i in range(1024))
print(f" FFT->IFFT roundtrip max error: {max_err} LSBs")
print(" FFT: OK")
except FileNotFoundError: except FileNotFoundError:
print(" FFT: SKIPPED (twiddle file not found)") pass
# --- Conjugate multiply test --- # --- Conjugate multiply test ---
print("\n--- Conjugate Multiply Test ---")
# (1+j0) * conj(1+j0) = 1+j0 # (1+j0) * conj(1+j0) = 1+j0
# In Q15: 32767 * 32767 -> should get close to 32767 # In Q15: 32767 * 32767 -> should get close to 32767
r, m = FreqMatchedFilter.conjugate_multiply_sample(0x7FFF, 0, 0x7FFF, 0) _r, _m = FreqMatchedFilter.conjugate_multiply_sample(0x7FFF, 0, 0x7FFF, 0)
print(f" (32767+j0) * conj(32767+j0) = {r}+j{m} (expect ~32767+j0)")
# (0+j32767) * conj(0+j32767) = (0+j32767)(0-j32767) = 32767^2 -> ~32767 # (0+j32767) * conj(0+j32767) = (0+j32767)(0-j32767) = 32767^2 -> ~32767
r2, m2 = FreqMatchedFilter.conjugate_multiply_sample(0, 0x7FFF, 0, 0x7FFF) _r2, _m2 = FreqMatchedFilter.conjugate_multiply_sample(0, 0x7FFF, 0, 0x7FFF)
print(f" (0+j32767) * conj(0+j32767) = {r2}+j{m2} (expect ~32767+j0)")
print(" Conjugate Multiply: OK")
# --- Range decimator test --- # --- Range decimator test ---
print("\n--- Range Bin Decimator Test ---")
test_re = list(range(1024)) test_re = list(range(1024))
test_im = [0] * 1024 test_im = [0] * 1024
out_re, out_im = RangeBinDecimator.decimate(test_re, test_im, mode=0) out_re, out_im = RangeBinDecimator.decimate(test_re, test_im, mode=0)
print(f" Mode 0 (center): first 5 bins = {out_re[:5]} (expect [8, 24, 40, 56, 72])")
print(" Range Decimator: OK")
print("\n" + "=" * 60)
print("ALL SELF-TESTS PASSED")
print("=" * 60)
if __name__ == '__main__': if __name__ == '__main__':
+13 -13
View File
@@ -82,8 +82,8 @@ def generate_full_long_chirp():
for n in range(LONG_CHIRP_SAMPLES): for n in range(LONG_CHIRP_SAMPLES):
t = n / FS_SYS t = n / FS_SYS
phase = math.pi * chirp_rate * t * t phase = math.pi * chirp_rate * t * t
re_val = int(round(Q15_MAX * SCALE * math.cos(phase))) re_val = round(Q15_MAX * SCALE * math.cos(phase))
im_val = int(round(Q15_MAX * SCALE * math.sin(phase))) im_val = round(Q15_MAX * SCALE * math.sin(phase))
chirp_i.append(max(-32768, min(32767, re_val))) chirp_i.append(max(-32768, min(32767, re_val)))
chirp_q.append(max(-32768, min(32767, im_val))) chirp_q.append(max(-32768, min(32767, im_val)))
@@ -105,8 +105,8 @@ def generate_short_chirp():
for n in range(SHORT_CHIRP_SAMPLES): for n in range(SHORT_CHIRP_SAMPLES):
t = n / FS_SYS t = n / FS_SYS
phase = math.pi * chirp_rate * t * t phase = math.pi * chirp_rate * t * t
re_val = int(round(Q15_MAX * SCALE * math.cos(phase))) re_val = round(Q15_MAX * SCALE * math.cos(phase))
im_val = int(round(Q15_MAX * SCALE * math.sin(phase))) im_val = round(Q15_MAX * SCALE * math.sin(phase))
chirp_i.append(max(-32768, min(32767, re_val))) chirp_i.append(max(-32768, min(32767, re_val)))
chirp_q.append(max(-32768, min(32767, im_val))) chirp_q.append(max(-32768, min(32767, im_val)))
@@ -134,7 +134,7 @@ def main():
print("AERIS-10 Chirp .mem File Generator") print("AERIS-10 Chirp .mem File Generator")
print("=" * 60) print("=" * 60)
print() print()
print(f"Parameters:") print("Parameters:")
print(f" CHIRP_BW = {CHIRP_BW/1e6:.1f} MHz") print(f" CHIRP_BW = {CHIRP_BW/1e6:.1f} MHz")
print(f" FS_SYS = {FS_SYS/1e6:.1f} MHz") print(f" FS_SYS = {FS_SYS/1e6:.1f} MHz")
print(f" T_LONG_CHIRP = {T_LONG_CHIRP*1e6:.1f} us") print(f" T_LONG_CHIRP = {T_LONG_CHIRP*1e6:.1f} us")
@@ -206,32 +206,32 @@ def main():
for n in range(FFT_SIZE): for n in range(FFT_SIZE):
t = n / FS_SYS t = n / FS_SYS
phase = math.pi * chirp_rate * t * t phase = math.pi * chirp_rate * t * t
expected_i = max(-32768, min(32767, int(round(Q15_MAX * SCALE * math.cos(phase))))) expected_i = max(-32768, min(32767, round(Q15_MAX * SCALE * math.cos(phase))))
expected_q = max(-32768, min(32767, int(round(Q15_MAX * SCALE * math.sin(phase))))) expected_q = max(-32768, min(32767, round(Q15_MAX * SCALE * math.sin(phase))))
if long_i[n] != expected_i or long_q[n] != expected_q: if long_i[n] != expected_i or long_q[n] != expected_q:
mismatches += 1 mismatches += 1
if mismatches == 0: if mismatches == 0:
print(f" [PASS] Seg0 matches radar_scene.py generate_reference_chirp_q15()") print(" [PASS] Seg0 matches radar_scene.py generate_reference_chirp_q15()")
else: else:
print(f" [FAIL] Seg0 has {mismatches} mismatches vs generate_reference_chirp_q15()") print(f" [FAIL] Seg0 has {mismatches} mismatches vs generate_reference_chirp_q15()")
return 1 return 1
# Check magnitude envelope # Check magnitude envelope
max_mag = max(math.sqrt(i*i + q*q) for i, q in zip(long_i, long_q)) max_mag = max(math.sqrt(i*i + q*q) for i, q in zip(long_i, long_q, strict=False))
print(f" Max magnitude: {max_mag:.1f} (expected ~{Q15_MAX * SCALE:.1f})") print(f" Max magnitude: {max_mag:.1f} (expected ~{Q15_MAX * SCALE:.1f})")
print(f" Magnitude ratio: {max_mag / (Q15_MAX * SCALE):.6f}") print(f" Magnitude ratio: {max_mag / (Q15_MAX * SCALE):.6f}")
# Check seg3 zero padding # Check seg3 zero padding
seg3_i_path = os.path.join(MEM_DIR, 'long_chirp_seg3_i.mem') seg3_i_path = os.path.join(MEM_DIR, 'long_chirp_seg3_i.mem')
with open(seg3_i_path, 'r') as f: with open(seg3_i_path) as f:
seg3_lines = [l.strip() for l in f if l.strip()] seg3_lines = [line.strip() for line in f if line.strip()]
nonzero_seg3 = sum(1 for l in seg3_lines if l != '0000') nonzero_seg3 = sum(1 for line in seg3_lines if line != '0000')
print(f" Seg3 non-zero entries: {nonzero_seg3}/{len(seg3_lines)} " print(f" Seg3 non-zero entries: {nonzero_seg3}/{len(seg3_lines)} "
f"(expected 0 since chirp ends at sample 2999)") f"(expected 0 since chirp ends at sample 2999)")
if nonzero_seg3 == 0: if nonzero_seg3 == 0:
print(f" [PASS] Seg3 is all zeros (chirp 3000 samples < seg3 start 3072)") print(" [PASS] Seg3 is all zeros (chirp 3000 samples < seg3 start 3072)")
else: else:
print(f" [WARN] Seg3 has {nonzero_seg3} non-zero entries") print(f" [WARN] Seg3 has {nonzero_seg3} non-zero entries")
@@ -18,14 +18,13 @@ Usage:
Author: Phase 0.5 Doppler co-simulation suite for PLFM_RADAR Author: Phase 0.5 Doppler co-simulation suite for PLFM_RADAR
""" """
import math
import os import os
import sys import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from fpga_model import ( from fpga_model import (
DopplerProcessor, sign_extend, HAMMING_WINDOW DopplerProcessor
) )
from radar_scene import Target, generate_doppler_frame from radar_scene import Target, generate_doppler_frame
@@ -121,7 +120,7 @@ def generate_scenario(name, targets, description, base_dir):
"""Generate input hex + golden output for one scenario.""" """Generate input hex + golden output for one scenario."""
print(f"\n{'='*60}") print(f"\n{'='*60}")
print(f"Scenario: {name}{description}") print(f"Scenario: {name}{description}")
print(f"Model: CLEAN (dual 16-pt FFT)") print("Model: CLEAN (dual 16-pt FFT)")
print(f"{'='*60}") print(f"{'='*60}")
# Generate Doppler frame (32 chirps x 64 range bins) # Generate Doppler frame (32 chirps x 64 range bins)
@@ -133,8 +132,10 @@ def generate_scenario(name, targets, description, base_dir):
# RTL expects data streamed chirp-by-chirp: chirp0[rb0..rb63], chirp1[rb0..rb63], ... # RTL expects data streamed chirp-by-chirp: chirp0[rb0..rb63], chirp1[rb0..rb63], ...
packed_samples = [] packed_samples = []
for chirp in range(CHIRPS_PER_FRAME): for chirp in range(CHIRPS_PER_FRAME):
for rb in range(RANGE_BINS): packed_samples.extend(
packed_samples.append((frame_i[chirp][rb], frame_q[chirp][rb])) (frame_i[chirp][rb], frame_q[chirp][rb])
for rb in range(RANGE_BINS)
)
input_hex = os.path.join(base_dir, f"doppler_input_{name}.hex") input_hex = os.path.join(base_dir, f"doppler_input_{name}.hex")
write_hex_32bit(input_hex, packed_samples) write_hex_32bit(input_hex, packed_samples)
@@ -169,10 +170,10 @@ def generate_scenario(name, targets, description, base_dir):
# ---- Write golden hex (for optional RTL $readmemh comparison) ---- # ---- Write golden hex (for optional RTL $readmemh comparison) ----
golden_hex = os.path.join(base_dir, f"doppler_golden_py_{name}.hex") golden_hex = os.path.join(base_dir, f"doppler_golden_py_{name}.hex")
write_hex_32bit(golden_hex, list(zip(flat_i, flat_q))) write_hex_32bit(golden_hex, list(zip(flat_i, flat_q, strict=False)))
# ---- Find peak per range bin ---- # ---- Find peak per range bin ----
print(f"\n Peak Doppler bins per range bin (top 5 by magnitude):") print("\n Peak Doppler bins per range bin (top 5 by magnitude):")
peak_info = [] peak_info = []
for rbin in range(RANGE_BINS): for rbin in range(RANGE_BINS):
mags = [abs(doppler_i[rbin][d]) + abs(doppler_q[rbin][d]) mags = [abs(doppler_i[rbin][d]) + abs(doppler_q[rbin][d])
@@ -25,8 +25,8 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from fpga_model import ( from fpga_model import (
FFTEngine, FreqMatchedFilter, MatchedFilterChain, MatchedFilterChain,
RangeBinDecimator, sign_extend, saturate sign_extend, saturate
) )
@@ -36,7 +36,7 @@ FFT_SIZE = 1024
def load_hex_16bit(filepath): def load_hex_16bit(filepath):
"""Load 16-bit hex file (one value per line, with optional // comments).""" """Load 16-bit hex file (one value per line, with optional // comments)."""
values = [] values = []
with open(filepath, 'r') as f: with open(filepath) as f:
for line in f: for line in f:
line = line.strip() line = line.strip()
if not line or line.startswith('//'): if not line or line.startswith('//'):
@@ -191,8 +191,8 @@ def main():
sig_q = [] sig_q = []
for n in range(FFT_SIZE): for n in range(FFT_SIZE):
angle = 2.0 * math.pi * k * n / FFT_SIZE angle = 2.0 * math.pi * k * n / FFT_SIZE
sig_i.append(saturate(int(round(amp * math.cos(angle))), 16)) sig_i.append(saturate(round(amp * math.cos(angle)), 16))
sig_q.append(saturate(int(round(amp * math.sin(angle))), 16)) sig_q.append(saturate(round(amp * math.sin(angle)), 16))
ref_i = list(sig_i) ref_i = list(sig_i)
ref_q = list(sig_q) ref_q = list(sig_q)
r = generate_case("tone5", sig_i, sig_q, ref_i, ref_q, r = generate_case("tone5", sig_i, sig_q, ref_i, ref_q,
@@ -5,7 +5,7 @@ gen_multiseg_golden.py
Generate golden reference data for matched_filter_multi_segment co-simulation. Generate golden reference data for matched_filter_multi_segment co-simulation.
Tests the overlap-save segmented convolution wrapper: Tests the overlap-save segmented convolution wrapper:
- Long chirp: 3072 samples (4 segments × 1024, with 128-sample overlap) - Long chirp: 3072 samples (4 segments x 1024, with 128-sample overlap)
- Short chirp: 50 samples zero-padded to 1024 (1 segment) - Short chirp: 50 samples zero-padded to 1024 (1 segment)
The matched_filter_processing_chain is already verified bit-perfect. The matched_filter_processing_chain is already verified bit-perfect.
@@ -208,7 +208,6 @@ def generate_long_chirp_test():
input_buffer_i = [0] * BUFFER_SIZE input_buffer_i = [0] * BUFFER_SIZE
input_buffer_q = [0] * BUFFER_SIZE input_buffer_q = [0] * BUFFER_SIZE
buffer_write_ptr = 0 buffer_write_ptr = 0
current_segment = 0
input_idx = 0 input_idx = 0
chirp_samples_collected = 0 chirp_samples_collected = 0
@@ -219,7 +218,8 @@ def generate_long_chirp_test():
if seg == 0: if seg == 0:
buffer_write_ptr = 0 buffer_write_ptr = 0
else: else:
# Overlap-save: copy buffer[SEGMENT_ADVANCE:SEGMENT_ADVANCE+OVERLAP] -> buffer[0:OVERLAP] # Overlap-save: copy
# buffer[SEGMENT_ADVANCE:SEGMENT_ADVANCE+OVERLAP] -> buffer[0:OVERLAP]
for i in range(OVERLAP_SAMPLES): for i in range(OVERLAP_SAMPLES):
input_buffer_i[i] = input_buffer_i[i + SEGMENT_ADVANCE] input_buffer_i[i] = input_buffer_i[i + SEGMENT_ADVANCE]
input_buffer_q[i] = input_buffer_q[i + SEGMENT_ADVANCE] input_buffer_q[i] = input_buffer_q[i + SEGMENT_ADVANCE]
@@ -234,7 +234,6 @@ def generate_long_chirp_test():
# In radar_receiver_final.v, the DDC output is sign-extended: # In radar_receiver_final.v, the DDC output is sign-extended:
# .ddc_i({{2{adc_i_scaled[15]}}, adc_i_scaled}) # .ddc_i({{2{adc_i_scaled[15]}}, adc_i_scaled})
# So 16-bit -> 18-bit sign-extend -> then multi_segment does: # So 16-bit -> 18-bit sign-extend -> then multi_segment does:
# ddc_i[17:2] + ddc_i[1]
# For sign-extended 18-bit from 16-bit: # For sign-extended 18-bit from 16-bit:
# ddc_i[17:2] = original 16-bit value (since bits [17:16] = sign extension) # ddc_i[17:2] = original 16-bit value (since bits [17:16] = sign extension)
# ddc_i[1] = bit 1 of original value # ddc_i[1] = bit 1 of original value
@@ -277,9 +276,6 @@ def generate_long_chirp_test():
out_re, out_im = mf_chain.process(seg_data_i, seg_data_q, ref_i, ref_q) out_re, out_im = mf_chain.process(seg_data_i, seg_data_q, ref_i, ref_q)
segment_results.append((out_re, out_im)) segment_results.append((out_re, out_im))
print(f" Segment {seg}: collected {buffer_write_ptr} buffer samples, "
f"total chirp samples = {chirp_samples_collected}, "
f"input_idx = {input_idx}")
# Write hex files for the testbench # Write hex files for the testbench
out_dir = os.path.dirname(os.path.abspath(__file__)) out_dir = os.path.dirname(os.path.abspath(__file__))
@@ -317,7 +313,6 @@ def generate_long_chirp_test():
for b in range(1024): for b in range(1024):
f.write(f'{seg},{b},{out_re[b]},{out_im[b]}\n') f.write(f'{seg},{b},{out_re[b]},{out_im[b]}\n')
print(f"\n Written {LONG_SEGMENTS * 1024} golden samples to {csv_path}")
return TOTAL_SAMPLES, LONG_SEGMENTS, segment_results return TOTAL_SAMPLES, LONG_SEGMENTS, segment_results
@@ -342,8 +337,9 @@ def generate_short_chirp_test():
input_q.append(saturate(val_q, 16)) input_q.append(saturate(val_q, 16))
# Zero-pad to 1024 (as RTL does in ST_ZERO_PAD) # Zero-pad to 1024 (as RTL does in ST_ZERO_PAD)
padded_i = list(input_i) + [0] * (BUFFER_SIZE - SHORT_SAMPLES) # Note: padding computed here for documentation; actual buffer uses buf_i/buf_q below
padded_q = list(input_q) + [0] * (BUFFER_SIZE - SHORT_SAMPLES) _padded_i = list(input_i) + [0] * (BUFFER_SIZE - SHORT_SAMPLES)
_padded_q = list(input_q) + [0] * (BUFFER_SIZE - SHORT_SAMPLES)
# The buffer truncation: ddc_i[17:2] + ddc_i[1] # The buffer truncation: ddc_i[17:2] + ddc_i[1]
# For data already 16-bit sign-extended to 18: result is (val >> 2) + bit1 # For data already 16-bit sign-extended to 18: result is (val >> 2) + bit1
@@ -380,7 +376,6 @@ def generate_short_chirp_test():
# Write hex files # Write hex files
out_dir = os.path.dirname(os.path.abspath(__file__)) out_dir = os.path.dirname(os.path.abspath(__file__))
# Input (18-bit)
all_input_i_18 = [] all_input_i_18 = []
all_input_q_18 = [] all_input_q_18 = []
for n in range(SHORT_SAMPLES): for n in range(SHORT_SAMPLES):
@@ -402,19 +397,12 @@ def generate_short_chirp_test():
for b in range(1024): for b in range(1024):
f.write(f'{b},{out_re[b]},{out_im[b]}\n') f.write(f'{b},{out_re[b]},{out_im[b]}\n')
print(f" Written 1024 short chirp golden samples to {csv_path}")
return out_re, out_im return out_re, out_im
if __name__ == '__main__': if __name__ == '__main__':
print("=" * 60)
print("Multi-Segment Matched Filter Golden Reference Generator")
print("=" * 60)
print("\n--- Long Chirp (4 segments, overlap-save) ---")
total_samples, num_segs, seg_results = generate_long_chirp_test() total_samples, num_segs, seg_results = generate_long_chirp_test()
print(f" Total input samples: {total_samples}")
print(f" Segments: {num_segs}")
for seg in range(num_segs): for seg in range(num_segs):
out_re, out_im = seg_results[seg] out_re, out_im = seg_results[seg]
@@ -426,9 +414,7 @@ if __name__ == '__main__':
if mag > max_mag: if mag > max_mag:
max_mag = mag max_mag = mag
peak_bin = b peak_bin = b
print(f" Seg {seg}: peak at bin {peak_bin}, magnitude {max_mag}")
print("\n--- Short Chirp (1 segment, zero-padded) ---")
short_re, short_im = generate_short_chirp_test() short_re, short_im = generate_short_chirp_test()
max_mag = 0 max_mag = 0
peak_bin = 0 peak_bin = 0
@@ -437,8 +423,3 @@ if __name__ == '__main__':
if mag > max_mag: if mag > max_mag:
max_mag = mag max_mag = mag
peak_bin = b peak_bin = b
print(f" Short chirp: peak at bin {peak_bin}, magnitude {max_mag}")
print("\n" + "=" * 60)
print("ALL GOLDEN FILES GENERATED")
print("=" * 60)
+13 -17
View File
@@ -21,7 +21,6 @@ Author: Phase 0.5 co-simulation suite for PLFM_RADAR
import math import math
import os import os
import struct
# ============================================================================= # =============================================================================
@@ -156,7 +155,7 @@ def generate_if_chirp(n_samples, chirp_bw=CHIRP_BW, f_if=F_IF, fs=FS_ADC):
t = n / fs t = n / fs
# Instantaneous frequency: f_if - chirp_bw/2 + chirp_rate * t # Instantaneous frequency: f_if - chirp_bw/2 + chirp_rate * t
# Phase: integral of 2*pi*f(t)*dt # Phase: integral of 2*pi*f(t)*dt
f_inst = f_if - chirp_bw / 2 + chirp_rate * t _f_inst = f_if - chirp_bw / 2 + chirp_rate * t
phase = 2 * math.pi * (f_if - chirp_bw / 2) * t + math.pi * chirp_rate * t * t phase = 2 * math.pi * (f_if - chirp_bw / 2) * t + math.pi * chirp_rate * t * t
chirp_i.append(math.cos(phase)) chirp_i.append(math.cos(phase))
chirp_q.append(math.sin(phase)) chirp_q.append(math.sin(phase))
@@ -191,8 +190,8 @@ def generate_reference_chirp_q15(n_fft=FFT_SIZE, chirp_bw=CHIRP_BW, f_if=F_IF, f
# The beat frequency from a target at delay tau is: f_beat = chirp_rate * tau # The beat frequency from a target at delay tau is: f_beat = chirp_rate * tau
# Reference chirp is the TX chirp at baseband (zero delay) # Reference chirp is the TX chirp at baseband (zero delay)
phase = math.pi * chirp_rate * t * t phase = math.pi * chirp_rate * t * t
re_val = int(round(32767 * 0.9 * math.cos(phase))) re_val = round(32767 * 0.9 * math.cos(phase))
im_val = int(round(32767 * 0.9 * math.sin(phase))) im_val = round(32767 * 0.9 * math.sin(phase))
ref_re[n] = max(-32768, min(32767, re_val)) ref_re[n] = max(-32768, min(32767, re_val))
ref_im[n] = max(-32768, min(32767, im_val)) ref_im[n] = max(-32768, min(32767, im_val))
@@ -285,7 +284,7 @@ def generate_adc_samples(targets, n_samples, noise_stddev=3.0,
# Quantize to 8-bit unsigned (0-255), centered at 128 # Quantize to 8-bit unsigned (0-255), centered at 128
adc_samples = [] adc_samples = []
for val in adc_float: for val in adc_float:
quantized = int(round(val + 128)) quantized = round(val + 128)
quantized = max(0, min(255, quantized)) quantized = max(0, min(255, quantized))
adc_samples.append(quantized) adc_samples.append(quantized)
@@ -347,8 +346,8 @@ def generate_baseband_samples(targets, n_samples_baseband, noise_stddev=0.5,
bb_i = [] bb_i = []
bb_q = [] bb_q = []
for n in range(n_samples_baseband): for n in range(n_samples_baseband):
i_val = int(round(bb_i_float[n] + noise_stddev * rand_gaussian())) i_val = round(bb_i_float[n] + noise_stddev * rand_gaussian())
q_val = int(round(bb_q_float[n] + noise_stddev * rand_gaussian())) q_val = round(bb_q_float[n] + noise_stddev * rand_gaussian())
bb_i.append(max(-32768, min(32767, i_val))) bb_i.append(max(-32768, min(32767, i_val)))
bb_q.append(max(-32768, min(32767, q_val))) bb_q.append(max(-32768, min(32767, q_val)))
@@ -402,7 +401,7 @@ def generate_doppler_frame(targets, n_chirps=CHIRPS_PER_FRAME,
# range_bin = target_delay_in_baseband_samples / decimation_factor # range_bin = target_delay_in_baseband_samples / decimation_factor
delay_baseband_samples = target.delay_s * FS_SYS delay_baseband_samples = target.delay_s * FS_SYS
range_bin_float = delay_baseband_samples * n_range_bins / FFT_SIZE range_bin_float = delay_baseband_samples * n_range_bins / FFT_SIZE
range_bin = int(round(range_bin_float)) range_bin = round(range_bin_float)
if range_bin < 0 or range_bin >= n_range_bins: if range_bin < 0 or range_bin >= n_range_bins:
continue continue
@@ -427,10 +426,7 @@ def generate_doppler_frame(targets, n_chirps=CHIRPS_PER_FRAME,
rb = range_bin + delta rb = range_bin + delta
if 0 <= rb < n_range_bins: if 0 <= rb < n_range_bins:
# sinc-like weighting # sinc-like weighting
if delta == 0: weight = 1.0 if delta == 0 else 0.2 / abs(delta)
weight = 1.0
else:
weight = 0.2 / abs(delta)
chirp_i[rb] += amp * weight * math.cos(total_phase) chirp_i[rb] += amp * weight * math.cos(total_phase)
chirp_q[rb] += amp * weight * math.sin(total_phase) chirp_q[rb] += amp * weight * math.sin(total_phase)
@@ -438,8 +434,8 @@ def generate_doppler_frame(targets, n_chirps=CHIRPS_PER_FRAME,
row_i = [] row_i = []
row_q = [] row_q = []
for rb in range(n_range_bins): for rb in range(n_range_bins):
i_val = int(round(chirp_i[rb] + noise_stddev * rand_gaussian())) i_val = round(chirp_i[rb] + noise_stddev * rand_gaussian())
q_val = int(round(chirp_q[rb] + noise_stddev * rand_gaussian())) q_val = round(chirp_q[rb] + noise_stddev * rand_gaussian())
row_i.append(max(-32768, min(32767, i_val))) row_i.append(max(-32768, min(32767, i_val)))
row_q.append(max(-32768, min(32767, q_val))) row_q.append(max(-32768, min(32767, q_val)))
@@ -467,7 +463,7 @@ def write_hex_file(filepath, samples, bits=8):
with open(filepath, 'w') as f: with open(filepath, 'w') as f:
f.write(f"// {len(samples)} samples, {bits}-bit, hex format for $readmemh\n") f.write(f"// {len(samples)} samples, {bits}-bit, hex format for $readmemh\n")
for i, s in enumerate(samples): for _i, s in enumerate(samples):
if bits <= 8: if bits <= 8:
val = s & 0xFF val = s & 0xFF
elif bits <= 16: elif bits <= 16:
@@ -581,7 +577,7 @@ def scenario_sine_wave(n_adc_samples=16384, freq_hz=1e6, amplitude=50):
adc = [] adc = []
for n in range(n_adc_samples): for n in range(n_adc_samples):
t = n / FS_ADC t = n / FS_ADC
val = int(round(128 + amplitude * math.sin(2 * math.pi * freq_hz * t))) val = round(128 + amplitude * math.sin(2 * math.pi * freq_hz * t))
adc.append(max(0, min(255, val))) adc.append(max(0, min(255, val)))
return adc, [] return adc, []
@@ -668,7 +664,7 @@ def generate_all_test_vectors(output_dir=None):
f.write(f" ADC: {FS_ADC/1e6:.0f} MSPS, {ADC_BITS}-bit\n") f.write(f" ADC: {FS_ADC/1e6:.0f} MSPS, {ADC_BITS}-bit\n")
f.write(f" Range resolution: {RANGE_RESOLUTION:.1f} m\n") f.write(f" Range resolution: {RANGE_RESOLUTION:.1f} m\n")
f.write(f" Wavelength: {WAVELENGTH*1000:.2f} mm\n") f.write(f" Wavelength: {WAVELENGTH*1000:.2f} mm\n")
f.write(f"\n") f.write("\n")
f.write("Scenario 1: Single target\n") f.write("Scenario 1: Single target\n")
for t in targets1: for t in targets1:
@@ -20,7 +20,6 @@ Usage:
import numpy as np import numpy as np
import os import os
import sys
import argparse import argparse
# =========================================================================== # ===========================================================================
@@ -244,10 +243,7 @@ def nco_lookup(phase_accum, sin_lut):
quadrant = (lut_address >> 6) & 0x3 quadrant = (lut_address >> 6) & 0x3
# Mirror index for odd quadrants # Mirror index for odd quadrants
if (quadrant & 1) ^ ((quadrant >> 1) & 1): lut_idx = ~lut_address & 63 if quadrant & 1 ^ quadrant >> 1 & 1 else lut_address & 63
lut_idx = (~lut_address) & 0x3F
else:
lut_idx = lut_address & 0x3F
sin_abs = int(sin_lut[lut_idx]) sin_abs = int(sin_lut[lut_idx])
cos_abs = int(sin_lut[63 - lut_idx]) cos_abs = int(sin_lut[63 - lut_idx])
@@ -420,7 +416,7 @@ def run_ddc(adc_samples):
def load_twiddle_rom(twiddle_file): def load_twiddle_rom(twiddle_file):
"""Load the quarter-wave cosine ROM from .mem file.""" """Load the quarter-wave cosine ROM from .mem file."""
rom = [] rom = []
with open(twiddle_file, 'r') as f: with open(twiddle_file) as f:
for line in f: for line in f:
line = line.strip() line = line.strip()
if not line or line.startswith('//'): if not line or line.startswith('//'):
@@ -631,7 +627,7 @@ def run_range_bin_decimator(range_fft_i, range_fft_q,
# Averaging: sum group, then >> 4 (divide by 16) # Averaging: sum group, then >> 4 (divide by 16)
sum_i = np.int64(0) sum_i = np.int64(0)
sum_q = np.int64(0) sum_q = np.int64(0)
for s in range(decimation_factor): for _s in range(decimation_factor):
if in_idx >= input_bins: if in_idx >= input_bins:
break break
sum_i += int(range_fft_i[c, in_idx]) sum_i += int(range_fft_i[c, in_idx])
@@ -787,7 +783,7 @@ def run_mti_canceller(decim_i, decim_q, enable=True):
if not enable: if not enable:
mti_i[:] = decim_i mti_i[:] = decim_i
mti_q[:] = decim_q mti_q[:] = decim_q
print(f" Pass-through mode (MTI disabled)") print(" Pass-through mode (MTI disabled)")
return mti_i, mti_q return mti_i, mti_q
for c in range(n_chirps): for c in range(n_chirps):
@@ -803,7 +799,7 @@ def run_mti_canceller(decim_i, decim_q, enable=True):
mti_i[c, r] = saturate(diff_i, 16) mti_i[c, r] = saturate(diff_i, 16)
mti_q[c, r] = saturate(diff_q, 16) mti_q[c, r] = saturate(diff_q, 16)
print(f" Chirp 0: muted (zeros)") print(" Chirp 0: muted (zeros)")
print(f" Chirps 1-{n_chirps-1}: I range [{mti_i[1:].min()}, {mti_i[1:].max()}], " print(f" Chirps 1-{n_chirps-1}: I range [{mti_i[1:].min()}, {mti_i[1:].max()}], "
f"Q range [{mti_q[1:].min()}, {mti_q[1:].max()}]") f"Q range [{mti_q[1:].min()}, {mti_q[1:].max()}]")
return mti_i, mti_q return mti_i, mti_q
@@ -839,7 +835,7 @@ def run_dc_notch(doppler_i, doppler_q, width=2):
print(f"[DC NOTCH] width={width}, {n_range} range bins x {n_doppler} Doppler bins (dual sub-frame)") print(f"[DC NOTCH] width={width}, {n_range} range bins x {n_doppler} Doppler bins (dual sub-frame)")
if width == 0: if width == 0:
print(f" Pass-through (width=0)") print(" Pass-through (width=0)")
return notched_i, notched_q return notched_i, notched_q
zeroed_count = 0 zeroed_count = 0
@@ -976,10 +972,7 @@ def run_cfar_ca(doppler_i, doppler_q, guard=2, train=8,
# Saturate to MAG_WIDTH=17 bits # Saturate to MAG_WIDTH=17 bits
MAX_MAG = (1 << 17) - 1 # 131071 MAX_MAG = (1 << 17) - 1 # 131071
if threshold_raw > MAX_MAG: threshold_val = MAX_MAG if threshold_raw > MAX_MAG else int(threshold_raw)
threshold_val = MAX_MAG
else:
threshold_val = int(threshold_raw)
# Detection: magnitude > threshold # Detection: magnitude > threshold
if int(col[cut_idx]) > threshold_val: if int(col[cut_idx]) > threshold_val:
@@ -1029,7 +1022,7 @@ def run_float_reference(iq_i, iq_q):
Uses the exact same RTL Hamming window coefficients (Q15) to isolate Uses the exact same RTL Hamming window coefficients (Q15) to isolate
only the FFT fixed-point quantization error. only the FFT fixed-point quantization error.
""" """
print(f"\n[FLOAT REF] Running floating-point reference pipeline") print("\n[FLOAT REF] Running floating-point reference pipeline")
n_chirps, n_samples = iq_i.shape[0], iq_i.shape[1] if iq_i.ndim == 2 else len(iq_i) n_chirps, n_samples = iq_i.shape[0], iq_i.shape[1] if iq_i.ndim == 2 else len(iq_i)
@@ -1188,7 +1181,7 @@ def main():
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Load and quantize ADI data # Load and quantize ADI data
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
iq_i, iq_q, adc_8bit, config = load_and_quantize_adi_data( iq_i, iq_q, adc_8bit, _config = load_and_quantize_adi_data(
amp_data, amp_config, frame_idx=args.frame amp_data, amp_config, frame_idx=args.frame
) )
@@ -1384,10 +1377,10 @@ def main():
cfar_detections = np.argwhere(cfar_flags) cfar_detections = np.argwhere(cfar_flags)
cfar_det_list_file = os.path.join(output_dir, "fullchain_cfar_detections.txt") cfar_det_list_file = os.path.join(output_dir, "fullchain_cfar_detections.txt")
with open(cfar_det_list_file, 'w') as f: with open(cfar_det_list_file, 'w') as f:
f.write(f"# AERIS-10 Full-Chain CFAR Detection List\n") f.write("# AERIS-10 Full-Chain CFAR Detection List\n")
f.write(f"# Chain: decim -> MTI -> Doppler -> DC notch(w={DC_NOTCH_WIDTH}) -> CA-CFAR\n") f.write(f"# Chain: decim -> MTI -> Doppler -> DC notch(w={DC_NOTCH_WIDTH}) -> CA-CFAR\n")
f.write(f"# CFAR: guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X}, mode={CFAR_MODE}\n") f.write(f"# CFAR: guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X}, mode={CFAR_MODE}\n")
f.write(f"# Format: range_bin doppler_bin magnitude threshold\n") f.write("# Format: range_bin doppler_bin magnitude threshold\n")
for det in cfar_detections: for det in cfar_detections:
r, d = det r, d = det
f.write(f"{r} {d} {cfar_mag[r, d]} {cfar_thr[r, d]}\n") f.write(f"{r} {d} {cfar_mag[r, d]} {cfar_thr[r, d]}\n")
@@ -1406,9 +1399,9 @@ def main():
# Save full-chain detection reference # Save full-chain detection reference
fc_det_file = os.path.join(output_dir, "fullchain_detections.txt") fc_det_file = os.path.join(output_dir, "fullchain_detections.txt")
with open(fc_det_file, 'w') as f: with open(fc_det_file, 'w') as f:
f.write(f"# AERIS-10 Full-Chain Golden Reference Detections\n") f.write("# AERIS-10 Full-Chain Golden Reference Detections\n")
f.write(f"# Threshold: {args.threshold}\n") f.write(f"# Threshold: {args.threshold}\n")
f.write(f"# Format: range_bin doppler_bin magnitude\n") f.write("# Format: range_bin doppler_bin magnitude\n")
for d in fc_detections: for d in fc_detections:
rbin, dbin = d rbin, dbin = d
f.write(f"{rbin} {dbin} {fc_mag[rbin, dbin]}\n") f.write(f"{rbin} {dbin} {fc_mag[rbin, dbin]}\n")
@@ -1433,9 +1426,9 @@ def main():
# Save detection list # Save detection list
det_file = os.path.join(output_dir, "detections.txt") det_file = os.path.join(output_dir, "detections.txt")
with open(det_file, 'w') as f: with open(det_file, 'w') as f:
f.write(f"# AERIS-10 Golden Reference Detections\n") f.write("# AERIS-10 Golden Reference Detections\n")
f.write(f"# Threshold: {args.threshold}\n") f.write(f"# Threshold: {args.threshold}\n")
f.write(f"# Format: range_bin doppler_bin magnitude\n") f.write("# Format: range_bin doppler_bin magnitude\n")
for d in detections: for d in detections:
rbin, dbin = d rbin, dbin = d
f.write(f"{rbin} {dbin} {mag[rbin, dbin]}\n") f.write(f"{rbin} {dbin} {mag[rbin, dbin]}\n")
@@ -1484,12 +1477,12 @@ def main():
print(f" Range FFT: {FFT_SIZE}-point → {snr_range:.1f} dB vs float") print(f" Range FFT: {FFT_SIZE}-point → {snr_range:.1f} dB vs float")
print(f" Doppler FFT (direct): {DOPPLER_FFT_SIZE}-point Hamming → {snr_doppler:.1f} dB vs float") print(f" Doppler FFT (direct): {DOPPLER_FFT_SIZE}-point Hamming → {snr_doppler:.1f} dB vs float")
print(f" Detections (direct): {len(detections)} (threshold={args.threshold})") print(f" Detections (direct): {len(detections)} (threshold={args.threshold})")
print(f" Full-chain decimator: 1024→64 peak detection") print(" Full-chain decimator: 1024→64 peak detection")
print(f" Full-chain detections: {len(fc_detections)} (threshold={args.threshold})") print(f" Full-chain detections: {len(fc_detections)} (threshold={args.threshold})")
print(f" MTI+CFAR chain: decim → MTI → Doppler → DC notch(w={DC_NOTCH_WIDTH}) → CA-CFAR") print(f" MTI+CFAR chain: decim → MTI → Doppler → DC notch(w={DC_NOTCH_WIDTH}) → CA-CFAR")
print(f" CFAR detections: {len(cfar_detections)} (guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X})") print(f" CFAR detections: {len(cfar_detections)} (guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X})")
print(f" Hex stimulus files: {output_dir}/") print(f" Hex stimulus files: {output_dir}/")
print(f" Ready for RTL co-simulation with Icarus Verilog") print(" Ready for RTL co-simulation with Icarus Verilog")
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Optional plots # Optional plots
@@ -1498,7 +1491,7 @@ def main():
try: try:
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 2, figsize=(14, 10)) _fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Range FFT magnitude (chirp 0) # Range FFT magnitude (chirp 0)
range_mag = np.sqrt(range_fft_i.astype(float)**2 + range_fft_q.astype(float)**2) range_mag = np.sqrt(range_fft_i.astype(float)**2 + range_fft_q.astype(float)**2)
@@ -0,0 +1,42 @@
# Golden Reference Hex Files
These hex files are **committed golden references** for strict bit-exact
real-data regression tests (`tb_doppler_realdata.v`, `tb_fullchain_realdata.v`).
## When to regenerate
Regenerate whenever the Doppler processing pipeline changes:
- `doppler_processor.v` (FFT size, window, sub-frame structure)
- `xfft_16.v` / `fft_engine.v` (butterfly arithmetic, twiddle lookup)
- `range_bin_decimator.v` (decimation mode, peak detection logic)
- `fft_twiddle_16.mem` (twiddle factor ROM)
## How to regenerate
```bash
cd 9_Firmware/9_2_FPGA
python3 tb/cosim/real_data/golden_reference.py
# Then copy the Doppler-specific files:
python3 -c "
import numpy as np, os, shutil
h = 'tb/cosim/real_data/hex'
# Regenerate packed stimulus from range FFT npy
ri = np.load(f'{h}/range_fft_all_i.npy')
rq = np.load(f'{h}/range_fft_all_q.npy')
with open(f'{h}/doppler_input_realdata.hex','w') as f:
for c in range(32):
for r in range(64):
i=int(ri[c,r])&0xFFFF; q=int(rq[c,r])&0xFFFF
f.write(f'{(q<<16)|i:08X}\n')
shutil.copy2(f'{h}/doppler_map_i.hex', f'{h}/doppler_ref_i.hex')
shutil.copy2(f'{h}/doppler_map_q.hex', f'{h}/doppler_ref_q.hex')
"
```
## Architecture
Generated against the **dual 16-point FFT** Doppler architecture
(2 staggered-PRI sub-frames x 16-point Hamming-windowed FFT).
Source data: ADI CN0566 Phaser radar (10.525 GHz X-band FMCW, 4 MSPS).
@@ -1,291 +1,361 @@
# AERIS-10 Golden Reference Detections # AERIS-10 Golden Reference Detections
# Threshold: 10000 # Threshold: 10000
# Format: range_bin doppler_bin magnitude # Format: range_bin doppler_bin magnitude
0 0 44371 0 0 35364
0 1 24165 0 1 16147
0 31 17748 0 15 11821
1 0 34391 0 16 24536
1 1 17923 0 17 11208
1 31 18610 0 31 10122
2 0 28512 1 0 25697
2 1 13818 1 1 12174
2 31 15787 1 15 13421
3 0 47402 1 16 20002
3 1 25214 1 17 11568
3 31 23504 1 31 11299
4 0 51870 2 0 16788
4 1 32733 2 16 20207
4 31 31545 2 31 10711
5 0 31752 3 0 29174
5 1 13486 3 1 13965
5 31 19300 3 15 13305
6 0 63406 3 16 31517
6 1 33383 3 17 13478
6 31 36672 3 31 14101
7 0 37576 4 0 41986
7 1 21215 4 1 19241
7 31 27773 4 15 21030
8 0 14823 4 16 39714
10 0 30062 4 17 17538
10 1 13616 4 31 20394
10 31 17149 5 0 23766
5 1 11599
5 15 14843
5 16 18211
5 31 12009
6 0 42015
6 1 21423
6 15 21018
6 16 47402
6 17 22815
6 31 22736
7 0 32152
7 1 15393
7 15 14318
7 16 28911
7 17 13876
7 31 17156
8 0 11067
10 0 18848
10 15 10020
10 16 20027
10 31 10048
11 0 65534 11 0 65534
11 1 60963 11 1 37617
11 2 14848 11 2 14940
11 3 12082 11 15 43078
11 4 18060 11 16 65534
11 29 10045 11 17 39344
11 30 20661 11 31 45926
11 31 65536 12 0 58975
12 0 65536 12 1 22078
12 1 44569 12 15 34440
12 4 11189 12 16 59096
12 30 13936 12 17 22512
12 31 57036 12 31 28677
13 0 47038 13 0 38442
13 1 40212 13 1 29490
13 2 14655 13 15 37679
13 4 10242 13 16 44951
13 30 14945 13 17 27726
13 31 40237 13 31 39144
14 0 65534 14 0 52660
14 1 43568 14 1 27797
14 3 10974 14 2 13534
14 4 11491 14 15 39671
14 30 15272 14 16 57929
14 31 57983 14 17 24160
15 0 34501 14 31 31478
15 1 22496 15 0 30021
15 31 25197 15 1 12219
16 0 32784 15 15 17232
16 1 19309 15 16 29524
16 31 14005 15 17 13424
17 0 23063 15 31 17850
17 1 13730 16 0 17593
18 0 17087 16 16 24710
18 1 12092 16 31 13046
19 0 65535 17 0 17606
19 1 49084 17 1 11119
19 2 11399 17 16 13182
19 30 13119 18 16 15914
19 31 48411 19 0 55785
20 0 65509 19 1 29069
20 1 37881 19 15 29418
20 31 35014 19 16 55308
21 0 39614 19 17 27886
21 1 23389 19 31 30649
21 31 22417 20 0 49230
22 0 27174 20 1 24486
22 1 12577 20 15 21233
22 31 15278 20 16 45472
23 0 39885 20 17 21749
23 1 29247 20 31 21614
23 31 33561 21 0 26167
24 0 29644 21 1 13823
24 28 11071 21 15 10487
24 31 20937 21 16 29034
21 17 15861
21 31 10454
22 0 12791
22 16 22520
22 17 11384
22 31 11790
23 0 29337
23 1 17065
23 15 14174
23 16 39414
23 17 23310
23 31 17173
24 0 16889
24 15 15710
24 16 22395
24 31 15003
25 0 65535 25 0 65535
25 1 54580 25 1 40375
25 2 20278 25 15 54011
25 30 20041 25 16 61127
25 31 59445 25 17 38944
26 0 58162 25 31 48889
26 1 46544 26 0 46367
26 2 17230 26 1 39852
26 3 10127 26 15 29630
26 31 44711 26 16 53587
26 17 40655
26 31 34936
27 0 65535 27 0 65535
27 1 65535 27 1 65535
27 2 44599 27 15 64456
27 3 17124 27 16 65535
27 28 15139 27 17 65535
27 29 26067 27 31 58334
27 30 54631
27 31 65535
28 0 65535 28 0 65535
28 1 65535 28 1 57641
28 2 43056 28 15 65535
28 3 14647 28 16 65535
28 4 11808 28 17 54928
28 29 15256
28 30 50518
28 31 65535 28 31 65535
29 0 65535 29 0 65535
29 1 61621 29 1 44117
29 2 28859 29 2 13478
29 3 19523 29 14 11179
29 4 21765 29 15 65535
29 5 12687 29 16 65535
29 27 13175 29 17 45898
29 28 19619 29 18 10817
29 29 24365 29 31 60442
29 30 48682 30 0 44530
29 31 65535 30 1 36909
30 0 55399 30 2 14573
30 1 46683 30 15 43430
30 2 21192 30 16 51839
30 3 15905 30 17 37271
30 4 18003 30 31 47866
30 29 11105 31 0 40957
30 30 22360 31 1 52081
30 31 40830 31 2 12755
31 0 46504 31 15 42794
31 1 44346 31 16 41071
31 2 34200 31 17 50472
31 3 20677 31 18 11556
31 4 18570 31 31 43866
31 5 10430 32 0 35747
31 29 12684 32 1 19597
31 30 31778 32 15 25173
31 31 36195 32 16 39213
32 0 39540 32 17 21782
32 1 36657 32 31 29106
32 31 35394 33 0 34216
33 0 35482 33 1 41661
33 1 32886 33 15 42368
33 2 15041 33 16 38638
33 28 10103 33 17 40522
33 29 11617 33 31 45908
33 30 17465 34 0 36589
33 31 34603 34 1 17165
34 0 47950 34 15 16488
34 1 25855 34 16 26972
34 31 23198 34 17 12089
34 31 13576
35 0 65536 35 0 65536
35 1 63059 35 1 42536
35 2 24416 35 15 61612
35 30 27412 35 16 65536
35 31 65534 35 17 43084
36 0 65535 35 31 60807
36 1 41914 36 0 55831
36 2 11341 36 1 26499
36 30 11276 36 15 28393
36 31 41419 36 16 50059
38 0 63253 36 17 24420
38 1 46689 36 31 23905
38 2 13576 38 0 52721
38 30 14208 38 1 33692
38 31 49979 38 15 32463
39 0 33480 38 16 53145
39 1 25439 38 17 37178
39 31 23094 38 31 30632
40 0 52003 39 0 32288
40 1 47059 39 1 19461
40 2 13164 39 15 20183
40 31 37992 39 16 27198
39 17 16723
39 31 14041
40 0 47793
40 1 29861
40 15 23082
40 16 43109
40 17 30298
40 31 31219
41 0 65536 41 0 65536
41 1 65534 41 1 57642
41 2 25844 41 15 52984
41 3 14580 41 16 65536
41 4 12743 41 17 57420
41 30 22231 41 31 57035
41 31 65534 42 0 46393
42 0 52097 42 1 24862
42 1 45022 42 15 27123
42 2 10317 42 16 44734
42 28 11984 42 17 25836
42 29 10182 42 31 33316
42 30 13078 43 0 65535
42 31 40477 43 1 43056
43 0 61723 43 13 10481
43 1 48104 43 14 22074
43 2 17623 43 15 24792
43 3 10105 43 16 39506
43 28 28331 43 17 36481
43 29 24102 43 30 24870
43 31 45085 43 31 39062
44 0 65535 44 0 65535
44 1 65535 44 1 65535
44 2 60795 44 2 19166
44 3 25438 44 3 21321
44 27 39330 44 13 32864
44 28 60025 44 14 41461
44 29 52445 44 15 65535
44 30 35091 44 16 65535
44 17 58493
44 19 13967
44 29 29756
44 30 54069
44 31 65535 44 31 65535
45 0 65535 45 0 65535
45 1 65535 45 1 58886
45 2 27652 45 14 22013
45 3 14416 45 15 65116
45 4 10622 45 16 65535
45 27 16323 45 17 65535
45 28 40935 45 29 13068
45 29 30694 45 30 25759
45 30 29375 45 31 61393
45 31 65535 46 0 53411
46 0 65536 46 1 44267
46 1 57696 46 15 30631
46 2 14924 46 16 58196
46 30 14433 46 17 36338
46 31 45164 46 31 28840
47 0 59141 47 0 54574
47 1 44129 47 1 35574
47 2 15305 47 15 38960
47 28 13092 47 16 46623
47 30 13754 47 17 32070
47 31 47415 47 31 40091
48 0 27722 48 0 22302
48 1 13381 48 1 10865
48 31 16907 48 15 13917
49 0 51936 48 16 18042
49 1 43775 48 31 10930
49 2 13004 49 0 49013
49 31 40023 49 1 28270
50 0 45430 49 15 25242
50 1 39187 49 16 41969
50 2 15881 49 17 24924
50 30 12925 49 31 26704
50 31 38207 50 0 43858
51 0 34026 50 1 35227
51 1 33081 50 15 39737
51 31 34429 50 16 39085
52 0 34415 50 17 36353
52 1 15408 50 31 38658
52 31 19344 51 0 33868
53 0 52351 51 1 23364
53 1 42915 51 15 23348
53 2 14442 51 16 33246
53 30 13099 51 17 24398
53 31 42143 51 31 27415
54 0 62356 52 0 24500
54 1 49279 52 1 10467
54 2 15596 52 15 13661
54 30 15478 52 16 21073
54 31 46574 52 17 10681
55 0 33829 52 31 10164
55 1 15941 53 0 46806
55 31 18110 53 1 31121
56 0 65535 53 15 34423
56 1 46926 53 16 44568
56 2 11443 53 17 28497
56 28 12373 53 31 35229
56 29 12101 54 0 49713
56 30 14660 54 1 32292
56 31 53058 54 15 40878
54 16 54060
54 17 34252
54 31 43616
55 0 25597
55 1 13662
55 15 13184
55 16 20525
55 17 10363
55 31 12295
56 0 53620
56 1 23575
56 14 12578
56 15 34783
56 16 64499
56 17 32442
56 31 32139
58 0 65535 58 0 65535
58 1 56769 58 1 35008
58 2 14110 58 15 43324
58 28 12576 58 16 64959
58 29 16059 58 17 35348
58 30 18858 58 31 39154
58 31 63517 59 0 13238
59 0 30703 59 1 11374
59 1 24206 59 14 16623
59 28 17534 59 16 22346
59 29 12652 59 17 12583
60 0 35136 60 0 31259
60 1 21277 60 1 17900
60 31 25048 60 15 19638
61 0 28692 60 16 26331
61 1 11267 60 17 12543
61 28 11881 60 31 18056
61 31 17628 61 0 14596
62 0 35795 61 15 13600
62 1 18879 61 16 23597
62 31 18083 61 17 10681
63 0 65535 61 31 14915
63 1 40428 62 0 22096
63 28 11884 62 1 10515
63 29 13271 62 16 23642
63 30 14869 62 17 11146
63 31 52574 62 31 10180
63 0 58324
63 1 25269
63 15 32920
63 16 54165
63 17 27625
63 31 29816
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -76,22 +76,50 @@
0 0
0 0
0 0
0
0
0
0
0
0
0
0
0
0
0
0
0
1 1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1 1
1 1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1 1
0 0
0 0
@@ -2018,31 +2046,3 @@
0 0
0 0
0 0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
@@ -2,7 +2,7 @@
# Chain: decim -> MTI -> Doppler -> DC notch(w=2) -> CA-CFAR # Chain: decim -> MTI -> Doppler -> DC notch(w=2) -> CA-CFAR
# CFAR: guard=2, train=8, alpha=0x30, mode=CA # CFAR: guard=2, train=8, alpha=0x30, mode=CA
# Format: range_bin doppler_bin magnitude threshold # Format: range_bin doppler_bin magnitude threshold
2 27 40172 38280 2 14 57128 48153
2 28 65534 40749 2 29 20281 15318
2 29 58080 31302 2 30 44783 22389
2 30 16565 13386 3 26 19423 19422
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -2,188 +2,210 @@
# Threshold: 10000 # Threshold: 10000
# Format: range_bin doppler_bin magnitude # Format: range_bin doppler_bin magnitude
0 0 65534 0 0 65534
0 1 59350 0 1 28729
0 2 16748 0 2 18427
0 4 18802 0 3 14971
0 29 10539 0 14 11972
0 30 18526 0 15 53110
0 31 65536 0 16 65534
0 17 39344
0 31 45926
1 0 65535 1 0 65535
1 1 65535 1 1 65535
1 2 37002 1 2 16977
1 4 12412 1 4 13219
1 5 14956 1 10 10505
1 6 12586 1 12 12931
1 7 11607 1 14 23806
1 8 11379 1 15 65535
1 24 11725 1 16 65535
1 28 17218 1 17 65535
1 29 32939 1 31 55887
1 30 58888
1 31 65535
2 0 65535 2 0 65535
2 1 65535 2 1 65535
2 2 60795 2 2 19166
2 3 25438 2 3 21321
2 27 39330 2 13 32864
2 28 60025 2 14 41461
2 29 52445 2 15 65535
2 30 35091 2 16 65535
2 17 58493
2 19 13967
2 29 29756
2 30 54069
2 31 65535 2 31 65535
3 0 65535 3 0 53605
3 1 63297 3 1 48935
3 2 32758 3 2 24589
3 3 42197 3 4 12240
3 4 35819 3 5 14117
3 5 12663 3 8 12950
3 7 19561 3 11 13274
3 8 12012 3 12 12930
3 12 13537 3 14 23437
3 13 12879 3 15 29552
3 19 10255 3 16 65433
3 20 10129 3 17 52933
3 24 17256 3 18 24719
3 25 22733 3 19 17650
3 26 10202 3 20 15096
3 28 24061 3 21 13518
3 29 19639 3 27 13184
3 31 37328 3 28 15554
4 0 46755 3 29 23742
4 1 39569 3 30 17775
4 2 12396 3 31 19771
4 28 12471 4 0 46125
4 29 12156 4 1 38769
4 30 16659 4 14 10924
4 31 40340 4 15 33844
5 0 44089 4 16 38130
5 1 23634 4 17 36763
5 31 21331 4 31 34850
6 0 48634 5 0 29649
6 1 24635 5 1 15983
6 31 25423 5 16 27615
7 0 24477 5 17 13683
7 1 14206 5 31 11456
7 31 10955 6 0 29881
8 0 41014 6 1 14520
8 1 19527 6 15 14126
8 31 21133 6 16 33068
9 0 47277 6 17 14679
9 1 28366 6 31 16608
9 31 29936 7 0 15305
10 0 47095 7 16 16195
10 1 26150 8 0 22601
10 31 24009 8 16 32614
11 0 47384 8 17 16367
11 1 25409 8 31 16217
11 31 24250 9 0 33316
12 0 24648 9 1 14993
12 1 14298 9 15 19928
12 31 13970 9 16 38915
13 0 13062 9 17 20159
15 0 10284 9 31 17219
16 0 14267 10 0 31772
17 0 16165 10 1 16438
18 0 14235 10 15 13737
18 31 12120 10 16 29059
19 0 18006 10 17 15821
19 1 14936 10 31 13104
20 0 47569 11 0 30448
20 1 33826 11 1 15802
20 31 35752 11 15 12669
21 0 47804 11 16 30129
21 1 21420 11 17 14340
21 31 30292 11 31 12767
22 0 14968 12 0 12892
26 0 16086 12 16 15956
26 31 10462 13 16 10903
30 0 16628 17 0 11395
30 1 10044 17 16 10440
38 0 23453 19 0 11910
38 1 13989 19 16 12017
38 31 10672 20 0 42212
39 0 31656 20 1 17994
39 1 17367 20 15 23540
39 31 17314 20 16 42519
40 0 19156 20 17 15949
40 1 10817 20 31 23792
40 31 10083 21 0 19822
45 0 25385 21 2 13933
45 1 11685 21 3 12130
45 31 14673 21 13 11590
46 0 12576 21 14 14794
46 4 10141 21 15 14160
46 28 12358 21 16 36543
47 0 19657 21 17 19530
47 31 15741 21 31 13754
48 0 13189 22 0 12654
48 1 10038 26 16 10634
49 0 33747 30 0 11396
49 1 16561 38 0 12032
49 31 18910 38 1 11693
50 0 20552 38 16 18530
50 31 10843 39 0 13327
51 0 20068 39 1 12416
51 1 13887 39 2 10940
51 4 10305 39 15 10351
51 28 11339 39 16 25217
53 28 10166 39 17 11785
55 0 39891 39 31 12383
55 1 17615 40 16 14316
55 31 24898 45 0 16350
56 0 62796 45 16 16146
56 1 29788 46 0 11710
56 31 38261 46 16 12568
57 0 63585 46 31 12206
57 1 59760 47 15 12053
57 2 13027 47 16 17267
57 3 43395 49 0 22212
57 4 59148 49 15 11409
57 5 31472 49 16 20817
57 6 11913 50 0 14287
57 7 13807 50 16 13382
57 8 12132 51 0 15578
57 16 14068 51 1 11129
57 17 10379 51 16 12819
57 24 15712 53 16 10532
57 25 11076 55 0 24789
57 26 14856 55 15 10455
57 27 23468 55 16 25212
57 28 38479 55 17 10510
57 29 23078 55 31 12207
57 30 17921 56 0 45192
57 31 46558 56 1 16083
58 0 54425 56 15 22284
58 1 45222 56 16 40302
58 2 11380 56 17 17318
58 4 11700 56 31 19185
58 29 12022 57 0 41535
58 30 13911 57 1 27102
58 31 45374 57 2 50048
59 0 45581 57 3 30784
59 1 31538 57 6 10595
59 2 10481 57 8 12880
59 31 34132 57 12 16303
60 0 28622 57 13 21792
60 1 12594 57 14 36737
60 3 11799 57 15 51757
60 4 13327 57 16 53641
60 28 11737 57 17 33656
60 29 11439 57 18 11096
60 31 16902 57 31 50828
61 0 28716 58 0 49157
61 1 16605 58 1 40063
61 31 15745 58 15 33919
62 0 14151 58 16 44578
62 1 15747 58 17 46214
62 4 11738 58 31 35365
62 5 12479 59 0 40306
62 26 10789 59 1 20804
62 27 16875 59 15 16849
62 28 19372 59 16 25943
62 29 16120 59 18 11684
62 30 18215 59 30 15453
62 31 10810 59 31 26587
63 0 63651 60 0 19637
63 1 35648 60 15 12275
63 30 10692 60 16 18527
63 31 38169 60 30 10157
60 31 17278
61 0 15147
61 14 10732
61 15 12364
61 16 25489
61 17 13371
61 31 14278
62 1 14922
62 2 13924
62 16 29381
62 17 20524
62 31 12898
63 0 48946
63 1 23135
63 15 21609
63 16 44144
63 17 21410
63 31 19856
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,628 +0,0 @@
#!/usr/bin/env python3
"""
validate_mem_files.py Validate all .mem files against AERIS-10 radar parameters.
Checks:
1. Structural: line counts, hex format, value ranges for all 12 .mem files
2. FFT twiddle files: bit-exact match against cos(2*pi*k/N) in Q15
3. Long chirp .mem files: reverse-engineer parameters, check for chirp structure
4. Short chirp .mem files: check length, value range, spectral content
5. latency_buffer LATENCY=3187 parameter validation
Usage:
python3 validate_mem_files.py
"""
import math
import os
import sys
# ============================================================================
# AERIS-10 System Parameters (from radar_scene.py)
# ============================================================================
F_CARRIER = 10.5e9 # 10.5 GHz carrier
C_LIGHT = 3.0e8
F_IF = 120e6 # IF frequency
CHIRP_BW = 20e6 # 20 MHz sweep
FS_ADC = 400e6 # ADC sample rate
FS_SYS = 100e6 # System clock (100 MHz, after CIC 4x)
T_LONG_CHIRP = 30e-6 # 30 us long chirp
T_SHORT_CHIRP = 0.5e-6 # 0.5 us short chirp
CIC_DECIMATION = 4
FFT_SIZE = 1024
DOPPLER_FFT_SIZE = 16
LONG_CHIRP_SAMPLES = int(T_LONG_CHIRP * FS_SYS) # 3000 at 100 MHz
# Overlap-save parameters
OVERLAP_SAMPLES = 128
SEGMENT_ADVANCE = FFT_SIZE - OVERLAP_SAMPLES # 896
LONG_SEGMENTS = 4
MEM_DIR = os.path.join(os.path.dirname(__file__), '..', '..')
pass_count = 0
fail_count = 0
warn_count = 0
def check(condition, label):
global pass_count, fail_count
if condition:
print(f" [PASS] {label}")
pass_count += 1
else:
print(f" [FAIL] {label}")
fail_count += 1
def warn(label):
global warn_count
print(f" [WARN] {label}")
warn_count += 1
def read_mem_hex(filename):
"""Read a .mem file, return list of integer values (16-bit signed)."""
path = os.path.join(MEM_DIR, filename)
values = []
with open(path, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith('//'):
continue
val = int(line, 16)
# Interpret as 16-bit signed
if val >= 0x8000:
val -= 0x10000
values.append(val)
return values
# ============================================================================
# TEST 1: Structural validation of all .mem files
# ============================================================================
def test_structural():
print("\n=== TEST 1: Structural Validation ===")
expected = {
# FFT twiddle files (quarter-wave cosine ROMs)
'fft_twiddle_1024.mem': {'lines': 256, 'desc': '1024-pt FFT quarter-wave cos ROM'},
'fft_twiddle_16.mem': {'lines': 4, 'desc': '16-pt FFT quarter-wave cos ROM'},
# Long chirp segments (4 segments x 1024 samples each)
'long_chirp_seg0_i.mem': {'lines': 1024, 'desc': 'Long chirp seg 0 I'},
'long_chirp_seg0_q.mem': {'lines': 1024, 'desc': 'Long chirp seg 0 Q'},
'long_chirp_seg1_i.mem': {'lines': 1024, 'desc': 'Long chirp seg 1 I'},
'long_chirp_seg1_q.mem': {'lines': 1024, 'desc': 'Long chirp seg 1 Q'},
'long_chirp_seg2_i.mem': {'lines': 1024, 'desc': 'Long chirp seg 2 I'},
'long_chirp_seg2_q.mem': {'lines': 1024, 'desc': 'Long chirp seg 2 Q'},
'long_chirp_seg3_i.mem': {'lines': 1024, 'desc': 'Long chirp seg 3 I'},
'long_chirp_seg3_q.mem': {'lines': 1024, 'desc': 'Long chirp seg 3 Q'},
# Short chirp (50 samples)
'short_chirp_i.mem': {'lines': 50, 'desc': 'Short chirp I'},
'short_chirp_q.mem': {'lines': 50, 'desc': 'Short chirp Q'},
}
for fname, info in expected.items():
path = os.path.join(MEM_DIR, fname)
exists = os.path.isfile(path)
check(exists, f"{fname} exists")
if not exists:
continue
vals = read_mem_hex(fname)
check(len(vals) == info['lines'],
f"{fname}: {len(vals)} data lines (expected {info['lines']})")
# Check all values are in 16-bit signed range
in_range = all(-32768 <= v <= 32767 for v in vals)
check(in_range, f"{fname}: all values in [-32768, 32767]")
# ============================================================================
# TEST 2: FFT Twiddle Factor Validation
# ============================================================================
def test_twiddle_1024():
print("\n=== TEST 2a: FFT Twiddle 1024 Validation ===")
vals = read_mem_hex('fft_twiddle_1024.mem')
# Expected: cos(2*pi*k/1024) for k=0..255, in Q15 format
# Q15: value = round(cos(angle) * 32767)
max_err = 0
err_details = []
for k in range(min(256, len(vals))):
angle = 2.0 * math.pi * k / 1024.0
expected = int(round(math.cos(angle) * 32767.0))
expected = max(-32768, min(32767, expected))
actual = vals[k]
err = abs(actual - expected)
if err > max_err:
max_err = err
if err > 1:
err_details.append((k, actual, expected, err))
check(max_err <= 1,
f"fft_twiddle_1024.mem: max twiddle error = {max_err} LSB (tolerance: 1)")
if err_details:
for k, act, exp, e in err_details[:5]:
print(f" k={k}: got {act} (0x{act & 0xFFFF:04x}), expected {exp}, err={e}")
print(f" Max twiddle error: {max_err} LSB across {len(vals)} entries")
def test_twiddle_16():
print("\n=== TEST 2b: FFT Twiddle 16 Validation ===")
vals = read_mem_hex('fft_twiddle_16.mem')
max_err = 0
for k in range(min(4, len(vals))):
angle = 2.0 * math.pi * k / 16.0
expected = int(round(math.cos(angle) * 32767.0))
expected = max(-32768, min(32767, expected))
actual = vals[k]
err = abs(actual - expected)
if err > max_err:
max_err = err
check(max_err <= 1,
f"fft_twiddle_16.mem: max twiddle error = {max_err} LSB (tolerance: 1)")
print(f" Max twiddle error: {max_err} LSB across {len(vals)} entries")
# Print all 4 entries for reference
print(" Twiddle 16 entries:")
for k in range(min(4, len(vals))):
angle = 2.0 * math.pi * k / 16.0
expected = int(round(math.cos(angle) * 32767.0))
print(f" k={k}: file=0x{vals[k] & 0xFFFF:04x} ({vals[k]:6d}), "
f"expected=0x{expected & 0xFFFF:04x} ({expected:6d}), "
f"err={abs(vals[k] - expected)}")
# ============================================================================
# TEST 3: Long Chirp .mem File Analysis
# ============================================================================
def test_long_chirp():
print("\n=== TEST 3: Long Chirp .mem File Analysis ===")
# Load all 4 segments
all_i = []
all_q = []
for seg in range(4):
seg_i = read_mem_hex(f'long_chirp_seg{seg}_i.mem')
seg_q = read_mem_hex(f'long_chirp_seg{seg}_q.mem')
all_i.extend(seg_i)
all_q.extend(seg_q)
total_samples = len(all_i)
check(total_samples == 4096,
f"Total long chirp samples: {total_samples} (expected 4096 = 4 segs x 1024)")
# Compute magnitude envelope
magnitudes = [math.sqrt(i*i + q*q) for i, q in zip(all_i, all_q)]
max_mag = max(magnitudes)
min_mag = min(magnitudes)
avg_mag = sum(magnitudes) / len(magnitudes)
print(f" Magnitude: min={min_mag:.1f}, max={max_mag:.1f}, avg={avg_mag:.1f}")
print(f" Max magnitude as fraction of Q15 range: {max_mag/32767:.4f} ({max_mag/32767*100:.2f}%)")
# Check if this looks like it came from generate_reference_chirp_q15
# That function uses 32767 * 0.9 scaling => max magnitude ~29490
expected_max_from_model = 32767 * 0.9
uses_model_scaling = max_mag > expected_max_from_model * 0.8
if uses_model_scaling:
print(f" Scaling: CONSISTENT with radar_scene.py model (0.9 * Q15)")
else:
warn(f"Magnitude ({max_mag:.0f}) is much lower than expected from Python model "
f"({expected_max_from_model:.0f}). .mem files may have unknown provenance.")
# Check non-zero content: how many samples are non-zero?
nonzero_i = sum(1 for v in all_i if v != 0)
nonzero_q = sum(1 for v in all_q if v != 0)
print(f" Non-zero samples: I={nonzero_i}/{total_samples}, Q={nonzero_q}/{total_samples}")
# Analyze instantaneous frequency via phase differences
# Phase = atan2(Q, I)
phases = []
for i_val, q_val in zip(all_i, all_q):
if abs(i_val) > 5 or abs(q_val) > 5: # Skip near-zero samples
phases.append(math.atan2(q_val, i_val))
else:
phases.append(None)
# Compute phase differences (instantaneous frequency)
freq_estimates = []
for n in range(1, len(phases)):
if phases[n] is not None and phases[n-1] is not None:
dp = phases[n] - phases[n-1]
# Unwrap
while dp > math.pi:
dp -= 2 * math.pi
while dp < -math.pi:
dp += 2 * math.pi
# Frequency in Hz (at 100 MHz sample rate, since these are post-DDC)
f_inst = dp * FS_SYS / (2 * math.pi)
freq_estimates.append(f_inst)
if freq_estimates:
f_start = sum(freq_estimates[:50]) / 50 if len(freq_estimates) > 50 else freq_estimates[0]
f_end = sum(freq_estimates[-50:]) / 50 if len(freq_estimates) > 50 else freq_estimates[-1]
f_min = min(freq_estimates)
f_max = max(freq_estimates)
f_range = f_max - f_min
print(f"\n Instantaneous frequency analysis (post-DDC baseband):")
print(f" Start freq: {f_start/1e6:.3f} MHz")
print(f" End freq: {f_end/1e6:.3f} MHz")
print(f" Min freq: {f_min/1e6:.3f} MHz")
print(f" Max freq: {f_max/1e6:.3f} MHz")
print(f" Freq range: {f_range/1e6:.3f} MHz")
print(f" Expected BW: {CHIRP_BW/1e6:.3f} MHz")
# A chirp should show frequency sweep
is_chirp = f_range > 0.5e6 # At least 0.5 MHz sweep
check(is_chirp,
f"Long chirp shows frequency sweep ({f_range/1e6:.2f} MHz > 0.5 MHz)")
# Check if bandwidth roughly matches expected
bw_match = abs(f_range - CHIRP_BW) / CHIRP_BW < 0.5 # within 50%
if bw_match:
print(f" Bandwidth {f_range/1e6:.2f} MHz roughly matches expected {CHIRP_BW/1e6:.2f} MHz")
else:
warn(f"Bandwidth {f_range/1e6:.2f} MHz does NOT match expected {CHIRP_BW/1e6:.2f} MHz")
# Compare segment boundaries for overlap-save consistency
# In proper overlap-save, the chirp data should be segmented at 896-sample boundaries
# with segments being 1024-sample FFT blocks
print(f"\n Segment boundary analysis:")
for seg in range(4):
seg_i = read_mem_hex(f'long_chirp_seg{seg}_i.mem')
seg_q = read_mem_hex(f'long_chirp_seg{seg}_q.mem')
seg_mags = [math.sqrt(i*i + q*q) for i, q in zip(seg_i, seg_q)]
seg_avg = sum(seg_mags) / len(seg_mags)
seg_max = max(seg_mags)
# Check segment 3 zero-padding (chirp is 3000 samples, seg3 starts at 3072)
# Samples 3000-4095 should be zero (or near-zero) if chirp is exactly 3000 samples
if seg == 3:
# Seg3 covers chirp samples 3072..4095
# If chirp is only 3000 samples, then only samples 0..(3000-3072) = NONE are valid
# Actually chirp has 3000 samples total. Seg3 starts at index 3*1024=3072.
# So seg3 should only have 3000-3072 = -72 -> no valid chirp data!
# Wait, but the .mem files have 1024 lines with non-trivial data...
# Let's check if seg3 has significant data
zero_count = sum(1 for m in seg_mags if m < 2)
print(f" Seg {seg}: avg_mag={seg_avg:.1f}, max_mag={seg_max:.1f}, "
f"near-zero={zero_count}/{len(seg_mags)}")
if zero_count > 500:
print(f" -> Seg 3 mostly zeros (chirp shorter than 4096 samples)")
else:
print(f" -> Seg 3 has significant data throughout")
else:
print(f" Seg {seg}: avg_mag={seg_avg:.1f}, max_mag={seg_max:.1f}")
# ============================================================================
# TEST 4: Short Chirp .mem File Analysis
# ============================================================================
def test_short_chirp():
print("\n=== TEST 4: Short Chirp .mem File Analysis ===")
short_i = read_mem_hex('short_chirp_i.mem')
short_q = read_mem_hex('short_chirp_q.mem')
check(len(short_i) == 50, f"Short chirp I: {len(short_i)} samples (expected 50)")
check(len(short_q) == 50, f"Short chirp Q: {len(short_q)} samples (expected 50)")
# Expected: 0.5 us chirp at 100 MHz = 50 samples
expected_samples = int(T_SHORT_CHIRP * FS_SYS)
check(len(short_i) == expected_samples,
f"Short chirp length matches T_SHORT_CHIRP * FS_SYS = {expected_samples}")
magnitudes = [math.sqrt(i*i + q*q) for i, q in zip(short_i, short_q)]
max_mag = max(magnitudes)
avg_mag = sum(magnitudes) / len(magnitudes)
print(f" Magnitude: max={max_mag:.1f}, avg={avg_mag:.1f}")
print(f" Max as fraction of Q15: {max_mag/32767:.4f} ({max_mag/32767*100:.2f}%)")
# Check non-zero
nonzero = sum(1 for m in magnitudes if m > 1)
check(nonzero == len(short_i), f"All {nonzero}/{len(short_i)} samples non-zero")
# Check it looks like a chirp (phase should be quadratic)
phases = [math.atan2(q, i) for i, q in zip(short_i, short_q)]
freq_est = []
for n in range(1, len(phases)):
dp = phases[n] - phases[n-1]
while dp > math.pi: dp -= 2 * math.pi
while dp < -math.pi: dp += 2 * math.pi
freq_est.append(dp * FS_SYS / (2 * math.pi))
if freq_est:
f_start = freq_est[0]
f_end = freq_est[-1]
print(f" Freq start: {f_start/1e6:.3f} MHz, end: {f_end/1e6:.3f} MHz")
print(f" Freq range: {abs(f_end - f_start)/1e6:.3f} MHz")
# ============================================================================
# TEST 5: Generate Expected Chirp .mem and Compare
# ============================================================================
def test_chirp_vs_model():
print("\n=== TEST 5: Compare .mem Files vs Python Model ===")
# Generate reference using the same method as radar_scene.py
chirp_rate = CHIRP_BW / T_LONG_CHIRP # Hz/s
model_i = []
model_q = []
n_chirp = min(FFT_SIZE, LONG_CHIRP_SAMPLES) # 1024
for n in range(n_chirp):
t = n / FS_SYS
phase = math.pi * chirp_rate * t * t
re_val = int(round(32767 * 0.9 * math.cos(phase)))
im_val = int(round(32767 * 0.9 * math.sin(phase)))
model_i.append(max(-32768, min(32767, re_val)))
model_q.append(max(-32768, min(32767, im_val)))
# Read seg0 from .mem
mem_i = read_mem_hex('long_chirp_seg0_i.mem')
mem_q = read_mem_hex('long_chirp_seg0_q.mem')
# Compare magnitudes
model_mags = [math.sqrt(i*i + q*q) for i, q in zip(model_i, model_q)]
mem_mags = [math.sqrt(i*i + q*q) for i, q in zip(mem_i, mem_q)]
model_max = max(model_mags)
mem_max = max(mem_mags)
print(f" Python model seg0: max_mag={model_max:.1f} (Q15 * 0.9)")
print(f" .mem file seg0: max_mag={mem_max:.1f}")
print(f" Ratio (mem/model): {mem_max/model_max:.4f}")
# Check if they match (they almost certainly won't based on magnitude analysis)
matches = sum(1 for a, b in zip(model_i, mem_i) if a == b)
print(f" Exact I matches: {matches}/{len(model_i)}")
if matches > len(model_i) * 0.9:
print(f" -> .mem files MATCH Python model")
else:
warn(f".mem files do NOT match Python model. They likely have different provenance.")
# Try to detect scaling
if mem_max > 0:
ratio = model_max / mem_max
print(f" Scale factor (model/mem): {ratio:.2f}")
print(f" This suggests the .mem files used ~{1.0/ratio:.4f} scaling instead of 0.9")
# Check phase correlation (shape match regardless of scaling)
model_phases = [math.atan2(q, i) for i, q in zip(model_i, model_q)]
mem_phases = [math.atan2(q, i) for i, q in zip(mem_i, mem_q)]
# Compute phase differences
phase_diffs = []
for mp, fp in zip(model_phases, mem_phases):
d = mp - fp
while d > math.pi: d -= 2 * math.pi
while d < -math.pi: d += 2 * math.pi
phase_diffs.append(d)
avg_phase_diff = sum(phase_diffs) / len(phase_diffs)
max_phase_diff = max(abs(d) for d in phase_diffs)
print(f"\n Phase comparison (shape regardless of amplitude):")
print(f" Avg phase diff: {avg_phase_diff:.4f} rad ({math.degrees(avg_phase_diff):.2f} deg)")
print(f" Max phase diff: {max_phase_diff:.4f} rad ({math.degrees(max_phase_diff):.2f} deg)")
phase_match = max_phase_diff < 0.5 # within 0.5 rad
check(phase_match,
f"Phase shape match: max diff = {math.degrees(max_phase_diff):.1f} deg (tolerance: 28.6 deg)")
# ============================================================================
# TEST 6: Latency Buffer LATENCY=3187 Validation
# ============================================================================
def test_latency_buffer():
print("\n=== TEST 6: Latency Buffer LATENCY=3187 Validation ===")
# The latency buffer delays the reference chirp data to align with
# the matched filter processing chain output.
#
# The total latency through the processing chain depends on the branch:
#
# SYNTHESIS branch (fft_engine.v):
# - Load: 1024 cycles (input)
# - Forward FFT: LOG2N=10 stages x N/2=512 butterflies x 5-cycle pipeline = variable
# - Reference FFT: same
# - Conjugate multiply: 1024 cycles (4-stage pipeline in frequency_matched_filter)
# - Inverse FFT: same as forward
# - Output: 1024 cycles
# Total: roughly 3000-4000 cycles depending on pipeline fill
#
# The LATENCY=3187 value was likely determined empirically to align
# the reference chirp arriving at the processing chain with the
# correct time-domain position.
#
# Key constraint: LATENCY must be < 4096 (BRAM buffer size)
LATENCY = 3187
BRAM_SIZE = 4096
check(LATENCY < BRAM_SIZE,
f"LATENCY ({LATENCY}) < BRAM size ({BRAM_SIZE})")
# The fft_engine processes in stages:
# - LOAD: 1024 clocks (accepts input)
# - Per butterfly stage: 512 butterflies x 5 pipeline stages = ~2560 clocks + overhead
# Actually: 512 butterflies, each takes 5 cycles = 2560 per stage, 10 stages
# Total compute: 10 * 2560 = 25600 clocks
# But this is just for ONE FFT. The chain does 3 FFTs + multiply.
#
# For the SIMULATION branch, it's 1 clock per operation (behavioral).
# LATENCY=3187 doesn't apply to simulation branch behavior —
# it's the physical hardware pipeline latency.
#
# For synthesis: the latency_buffer feeds ref data to the chain via
# chirp_memory_loader_param → latency_buffer → chain.
# But wait — looking at radar_receiver_final.v:
# - mem_request drives valid_in on the latency buffer
# - The buffer delays {ref_i, ref_q} by LATENCY valid_in cycles
# - The delayed output feeds long_chirp_real/imag → chain
#
# The purpose: the chain in the SYNTHESIS branch reads reference data
# via the long_chirp_real/imag ports DURING ST_FWD_FFT (while collecting
# input samples). The reference data needs to arrive LATENCY cycles
# after the first mem_request, where LATENCY accounts for:
# - The fft_engine pipeline latency from input to output
# - Specifically, the chain processes: load 1024 → FFT → FFT → multiply → IFFT → output
# The reference is consumed during the second FFT (ST_REF_BITREV/BUTTERFLY)
# which starts after the first FFT completes.
# For now, validate that LATENCY is reasonable (between 1000 and 4095)
check(1000 < LATENCY < 4095,
f"LATENCY={LATENCY} in reasonable range [1000, 4095]")
# Check that the module name vs parameter is consistent
print(f" LATENCY parameter: {LATENCY}")
print(f" Module name: latency_buffer (parameterized, LATENCY={LATENCY})")
# Module name was renamed from latency_buffer_2159 to latency_buffer
# to match the actual parameterized LATENCY value. No warning needed.
# Validate address arithmetic won't overflow
# read_ptr = (write_ptr - LATENCY) mod 4096
# With 12-bit address, max write_ptr = 4095
# When write_ptr < LATENCY: read_ptr = 4096 + write_ptr - LATENCY
# Minimum: 4096 + 0 - 3187 = 909 (valid)
min_read_ptr = 4096 + 0 - LATENCY
check(min_read_ptr >= 0 and min_read_ptr < 4096,
f"Min read_ptr after wrap = {min_read_ptr} (valid: 0..4095)")
# The latency buffer uses valid_in gated reads, so it only counts
# valid samples. The number of valid_in pulses between first write
# and first read is LATENCY.
print(f" Buffer primes after {LATENCY} valid_in pulses, then outputs continuously")
# ============================================================================
# TEST 7: Cross-check chirp memory loader addressing
# ============================================================================
def test_memory_addressing():
print("\n=== TEST 7: Chirp Memory Loader Addressing ===")
# chirp_memory_loader_param uses: long_addr = {segment_select[1:0], sample_addr[9:0]}
# This creates a 12-bit address: seg[1:0] ++ addr[9:0]
# Segment 0: addresses 0x000..0x3FF (0..1023)
# Segment 1: addresses 0x400..0x7FF (1024..2047)
# Segment 2: addresses 0x800..0xBFF (2048..3071)
# Segment 3: addresses 0xC00..0xFFF (3072..4095)
for seg in range(4):
base = seg * 1024
end = base + 1023
addr_from_concat = (seg << 10) | 0 # {seg[1:0], 10'b0}
addr_end = (seg << 10) | 1023
check(addr_from_concat == base,
f"Seg {seg} base address: {{{seg}[1:0], 10'b0}} = {addr_from_concat} (expected {base})")
check(addr_end == end,
f"Seg {seg} end address: {{{seg}[1:0], 10'h3FF}} = {addr_end} (expected {end})")
# Memory is declared as: reg [15:0] long_chirp_i [0:4095]
# $readmemh loads seg0 to [0:1023], seg1 to [1024:2047], etc.
# Addressing via {segment_select, sample_addr} maps correctly.
print(" Addressing scheme: {segment_select[1:0], sample_addr[9:0]} -> 12-bit address")
print(" Memory size: [0:4095] (4096 entries) — matches 4 segments x 1024 samples")
# ============================================================================
# TEST 8: Seg3 zero-padding analysis
# ============================================================================
def test_seg3_padding():
print("\n=== TEST 8: Segment 3 Data Analysis ===")
# The long chirp has 3000 samples (30 us at 100 MHz).
# With 4 segments of 1024 samples = 4096 total memory slots.
# Segments are loaded contiguously into memory:
# Seg0: chirp samples 0..1023
# Seg1: chirp samples 1024..2047
# Seg2: chirp samples 2048..3071
# Seg3: chirp samples 3072..4095
#
# But the chirp only has 3000 samples! So seg3 should have:
# Valid chirp data at indices 0..(3000-3072-1) = NEGATIVE
# Wait — 3072 > 3000, so seg3 has NO valid chirp samples if chirp is exactly 3000.
#
# However, the overlap-save algorithm in matched_filter_multi_segment.v
# collects data differently:
# Seg0: collect 896 DDC samples, buffer[0:895], zero-pad [896:1023]
# Seg1: overlap from seg0[768:895] → buffer[0:127], collect 896 → buffer[128:1023]
# ...
# The chirp reference is indexed by segment_select + sample_addr,
# so it reads ALL 1024 values for each segment regardless.
#
# If the chirp is 3000 samples but only 4*1024=4096 slots exist,
# the question is: do the .mem files contain 3000 samples of real chirp
# data spread across 4096 slots, or something else?
seg3_i = read_mem_hex('long_chirp_seg3_i.mem')
seg3_q = read_mem_hex('long_chirp_seg3_q.mem')
mags = [math.sqrt(i*i + q*q) for i, q in zip(seg3_i, seg3_q)]
# Count trailing zeros (samples after chirp ends)
trailing_zeros = 0
for m in reversed(mags):
if m < 2:
trailing_zeros += 1
else:
break
nonzero = sum(1 for m in mags if m > 2)
print(f" Seg3 non-zero samples: {nonzero}/{len(seg3_i)}")
print(f" Seg3 trailing near-zeros: {trailing_zeros}")
print(f" Seg3 max magnitude: {max(mags):.1f}")
print(f" Seg3 first 5 magnitudes: {[f'{m:.1f}' for m in mags[:5]]}")
print(f" Seg3 last 5 magnitudes: {[f'{m:.1f}' for m in mags[-5:]]}")
if nonzero == 1024:
print(" -> Seg3 has data throughout (chirp extends beyond 3072 samples or is padded)")
# This means the .mem files encode 4096 chirp samples, not 3000
# The chirp duration used for .mem generation was different from T_LONG_CHIRP
actual_chirp_samples = 4 * 1024 # = 4096
actual_duration = actual_chirp_samples / FS_SYS
warn(f"Chirp in .mem files appears to be {actual_chirp_samples} samples "
f"({actual_duration*1e6:.1f} us), not {LONG_CHIRP_SAMPLES} samples "
f"({T_LONG_CHIRP*1e6:.1f} us)")
elif trailing_zeros > 100:
# Some padding at end
actual_valid = 3072 + (1024 - trailing_zeros)
print(f" -> Estimated valid chirp samples in .mem: ~{actual_valid}")
# ============================================================================
# MAIN
# ============================================================================
def main():
print("=" * 70)
print("AERIS-10 .mem File Validation")
print("=" * 70)
test_structural()
test_twiddle_1024()
test_twiddle_16()
test_long_chirp()
test_short_chirp()
test_chirp_vs_model()
test_latency_buffer()
test_memory_addressing()
test_seg3_padding()
print("\n" + "=" * 70)
print(f"SUMMARY: {pass_count} PASS, {fail_count} FAIL, {warn_count} WARN")
if fail_count == 0:
print("ALL CHECKS PASSED")
else:
print("SOME CHECKS FAILED")
print("=" * 70)
return 0 if fail_count == 0 else 1
if __name__ == '__main__':
sys.exit(main())
+4 -6
View File
@@ -28,8 +28,7 @@ N = 1024 # FFT length
def to_q15(value): def to_q15(value):
"""Clamp a floating-point value to 16-bit signed range [-32768, 32767].""" """Clamp a floating-point value to 16-bit signed range [-32768, 32767]."""
v = int(np.round(value)) v = int(np.round(value))
v = max(-32768, min(32767, v)) return max(-32768, min(32767, v))
return v
def to_hex16(value): def to_hex16(value):
@@ -91,7 +90,7 @@ def generate_case(case_num, sig_i, sig_q, ref_i, ref_q, description, outdir):
peak_q_q = out_q_q[peak_bin] peak_q_q = out_q_q[peak_bin]
# Write hex files # Write hex files
prefix = os.path.join(outdir, f"mf_golden") prefix = os.path.join(outdir, "mf_golden")
write_hex_file(f"{prefix}_sig_i_case{case_num}.hex", sig_i) write_hex_file(f"{prefix}_sig_i_case{case_num}.hex", sig_i)
write_hex_file(f"{prefix}_sig_q_case{case_num}.hex", sig_q) write_hex_file(f"{prefix}_sig_q_case{case_num}.hex", sig_q)
write_hex_file(f"{prefix}_ref_i_case{case_num}.hex", ref_i) write_hex_file(f"{prefix}_ref_i_case{case_num}.hex", ref_i)
@@ -108,7 +107,7 @@ def generate_case(case_num, sig_i, sig_q, ref_i, ref_q, description, outdir):
f"mf_golden_out_q_case{case_num}.hex", f"mf_golden_out_q_case{case_num}.hex",
] ]
summary = { return {
"case": case_num, "case": case_num,
"description": description, "description": description,
"peak_bin": peak_bin, "peak_bin": peak_bin,
@@ -119,7 +118,6 @@ def generate_case(case_num, sig_i, sig_q, ref_i, ref_q, description, outdir):
"peak_q_quant": peak_q_q, "peak_q_quant": peak_q_q,
"files": files, "files": files,
} }
return summary
def main(): def main():
@@ -233,7 +231,7 @@ def main():
f.write(f" Peak Q (float): {s['peak_q_float']:.6f}\n") f.write(f" Peak Q (float): {s['peak_q_float']:.6f}\n")
f.write(f" Peak I (quantized): {s['peak_i_quant']}\n") f.write(f" Peak I (quantized): {s['peak_i_quant']}\n")
f.write(f" Peak Q (quantized): {s['peak_q_quant']}\n") f.write(f" Peak Q (quantized): {s['peak_q_quant']}\n")
f.write(f" Files:\n") f.write(" Files:\n")
for fname in s["files"]: for fname in s["files"]:
f.write(f" {fname}\n") f.write(f" {fname}\n")
f.write("\n") f.write("\n")
+3 -4
View File
@@ -258,10 +258,9 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
// Gap 2: Capture status snapshot when request arrives in ft601 domain // Gap 2: Capture status snapshot when request arrives in ft601 domain
if (status_req_ft601) begin if (status_req_ft601) begin
// Pack register values into 5x 32-bit status words // Pack register values into 5x 32-bit status words
// Word 0: {0xFF, mode[1:0], stream_ctrl[2:0], cfar_threshold[15:0]} // Word 0: {0xFF[31:24], mode[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]}
status_words[0] <= {8'hFF, 3'b000, status_radar_mode, status_words[0] <= {8'hFF, status_radar_mode, status_stream_ctrl,
5'b00000, status_stream_ctrl, 3'b000, status_cfar_threshold};
status_cfar_threshold};
// Word 1: {long_chirp_cycles[15:0], long_listen_cycles[15:0]} // Word 1: {long_chirp_cycles[15:0], long_listen_cycles[15:0]}
status_words[1] <= {status_long_chirp, status_long_listen}; status_words[1] <= {status_long_chirp, status_long_listen};
// Word 2: {guard_cycles[15:0], short_chirp_cycles[15:0]} // Word 2: {guard_cycles[15:0], short_chirp_cycles[15:0]}
@@ -275,9 +275,9 @@ always @(posedge ft_clk or negedge ft_reset_n) begin
// Status snapshot on request // Status snapshot on request
if (status_req_ft) begin if (status_req_ft) begin
status_words[0] <= {8'hFF, 3'b000, status_radar_mode, // Word 0: {0xFF[31:24], mode[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]}
5'b00000, status_stream_ctrl, status_words[0] <= {8'hFF, status_radar_mode, status_stream_ctrl,
status_cfar_threshold}; 3'b000, status_cfar_threshold};
status_words[1] <= {status_long_chirp, status_long_listen}; status_words[1] <= {status_long_chirp, status_long_listen};
status_words[2] <= {status_guard, status_short_chirp}; status_words[2] <= {status_guard, status_short_chirp};
status_words[3] <= {status_short_listen, 10'd0, status_chirps_per_elev}; status_words[3] <= {status_short_listen, 10'd0, status_chirps_per_elev};
File diff suppressed because it is too large Load Diff
-41
View File
@@ -1,41 +0,0 @@
def update_gps_display(self):
"""Step 18: Update GPS display and center map"""
try:
while not self.gps_data_queue.empty():
gps_data = self.gps_data_queue.get_nowait()
self.current_gps = gps_data
# Update GPS label
self.gps_label.config(
text=f"GPS: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m")
# Update map
self.update_map_display(gps_data)
except queue.Empty:
pass
def update_map_display(self, gps_data):
"""Step 18: Update map display with current GPS position"""
try:
self.map_label.config(text=f"Radar Position: {gps_data.latitude:.6f}, {gps_data.longitude:.6f}\n"
f"Altitude: {gps_data.altitude:.1f}m\n"
f"Coverage: 50km radius\n"
f"Map centered on GPS coordinates")
except Exception as e:
logging.error(f"Error updating map display: {e}")
def main():
"""Main application entry point"""
try:
root = tk.Tk()
app = RadarGUI(root)
root.mainloop()
except Exception as e:
logging.error(f"Application error: {e}")
messagebox.showerror("Fatal Error", f"Application failed to start: {e}")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-678
View File
@@ -1,678 +0,0 @@
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import matplotlib.patches as patches
from scipy import signal
from scipy.fft import fft, fftshift
from scipy.signal import butter, filtfilt
import logging
from dataclasses import dataclass
from typing import List, Dict, Tuple
import threading
import queue
import time
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@dataclass
class RadarTarget:
range: float
velocity: float
azimuth: int
elevation: int
snr: float
chirp_type: str
timestamp: float
class SignalProcessor:
def __init__(self):
self.range_resolution = 1.0 # meters
self.velocity_resolution = 0.1 # m/s
self.cfar_threshold = 15.0 # dB
def doppler_fft(self, iq_data: np.ndarray, fs: float = 100e6) -> Tuple[np.ndarray, np.ndarray]:
"""
Perform Doppler FFT on IQ data
Returns Doppler frequencies and spectrum
"""
# Window function for FFT
window = np.hanning(len(iq_data))
windowed_data = (iq_data['I_value'].values + 1j * iq_data['Q_value'].values) * window
# Perform FFT
doppler_fft = fft(windowed_data)
doppler_fft = fftshift(doppler_fft)
# Frequency axis
N = len(iq_data)
freq_axis = np.linspace(-fs/2, fs/2, N)
# Convert to velocity (assuming radar frequency = 10 GHz)
radar_freq = 10e9
wavelength = 3e8 / radar_freq
velocity_axis = freq_axis * wavelength / 2
return velocity_axis, np.abs(doppler_fft)
def mti_filter(self, iq_data: np.ndarray, filter_type: str = 'single_canceler') -> np.ndarray:
"""
Moving Target Indicator filter
Removes stationary clutter with better shape handling
"""
if iq_data is None or len(iq_data) < 2:
return np.array([], dtype=complex)
try:
# Ensure we're working with complex data
complex_data = iq_data.astype(complex)
if filter_type == 'single_canceler':
# Single delay line canceler
if len(complex_data) < 2:
return np.array([], dtype=complex)
filtered = np.zeros(len(complex_data) - 1, dtype=complex)
for i in range(1, len(complex_data)):
filtered[i-1] = complex_data[i] - complex_data[i-1]
return filtered
elif filter_type == 'double_canceler':
# Double delay line canceler
if len(complex_data) < 3:
return np.array([], dtype=complex)
filtered = np.zeros(len(complex_data) - 2, dtype=complex)
for i in range(2, len(complex_data)):
filtered[i-2] = complex_data[i] - 2*complex_data[i-1] + complex_data[i-2]
return filtered
else:
return complex_data
except Exception as e:
logging.error(f"MTI filter error: {e}")
return np.array([], dtype=complex)
def cfar_detection(self, range_profile: np.ndarray, guard_cells: int = 2,
training_cells: int = 10, threshold_factor: float = 3.0) -> List[Tuple[int, float]]:
detections = []
N = len(range_profile)
# Ensure guard_cells and training_cells are integers
guard_cells = int(guard_cells)
training_cells = int(training_cells)
for i in range(N):
# Convert to integer indices
i_int = int(i)
if i_int < guard_cells + training_cells or i_int >= N - guard_cells - training_cells:
continue
# Leading window - ensure integer indices
lead_start = i_int - guard_cells - training_cells
lead_end = i_int - guard_cells
lead_cells = range_profile[lead_start:lead_end]
# Lagging window - ensure integer indices
lag_start = i_int + guard_cells + 1
lag_end = i_int + guard_cells + training_cells + 1
lag_cells = range_profile[lag_start:lag_end]
# Combine training cells
training_cells_combined = np.concatenate([lead_cells, lag_cells])
# Calculate noise floor (mean of training cells)
if len(training_cells_combined) > 0:
noise_floor = np.mean(training_cells_combined)
# Apply threshold
threshold = noise_floor * threshold_factor
if range_profile[i_int] > threshold:
detections.append((i_int, float(range_profile[i_int]))) # Ensure float magnitude
return detections
def range_fft(self, iq_data: np.ndarray, fs: float = 100e6, bw: float = 20e6) -> Tuple[np.ndarray, np.ndarray]:
"""
Perform range FFT on IQ data
Returns range profile
"""
# Window function
window = np.hanning(len(iq_data))
windowed_data = np.abs(iq_data) * window
# Perform FFT
range_fft = fft(windowed_data)
# Range calculation
N = len(iq_data)
range_max = (3e8 * N) / (2 * bw)
range_axis = np.linspace(0, range_max, N)
return range_axis, np.abs(range_fft)
def process_chirp_sequence(self, df: pd.DataFrame, chirp_type: str = 'LONG') -> Dict:
try:
# Filter data by chirp type
chirp_data = df[df['chirp_type'] == chirp_type]
if len(chirp_data) == 0:
return {}
# Group by chirp number
chirp_numbers = chirp_data['chirp_number'].unique()
num_chirps = len(chirp_numbers)
if num_chirps == 0:
return {}
# Get samples per chirp and ensure consistency
samples_per_chirp_list = [len(chirp_data[chirp_data['chirp_number'] == num])
for num in chirp_numbers]
# Use minimum samples to ensure consistent shape
samples_per_chirp = min(samples_per_chirp_list)
# Create range-Doppler matrix with consistent shape
range_doppler_matrix = np.zeros((samples_per_chirp, num_chirps), dtype=complex)
for i, chirp_num in enumerate(chirp_numbers):
chirp_samples = chirp_data[chirp_data['chirp_number'] == chirp_num]
# Take only the first samples_per_chirp samples to ensure consistent shape
chirp_samples = chirp_samples.head(samples_per_chirp)
# Create complex IQ data
iq_data = chirp_samples['I_value'].values + 1j * chirp_samples['Q_value'].values
# Ensure the shape matches
if len(iq_data) == samples_per_chirp:
range_doppler_matrix[:, i] = iq_data
# Apply MTI filter along slow-time (chirp-to-chirp)
mti_filtered = np.zeros_like(range_doppler_matrix)
for i in range(samples_per_chirp):
slow_time_data = range_doppler_matrix[i, :]
filtered = self.mti_filter(slow_time_data)
# Ensure filtered data matches expected shape
if len(filtered) == num_chirps:
mti_filtered[i, :] = filtered
else:
# Handle shape mismatch by padding or truncating
if len(filtered) < num_chirps:
padded = np.zeros(num_chirps, dtype=complex)
padded[:len(filtered)] = filtered
mti_filtered[i, :] = padded
else:
mti_filtered[i, :] = filtered[:num_chirps]
# Perform Doppler FFT along slow-time dimension
doppler_fft_result = np.zeros((samples_per_chirp, num_chirps), dtype=complex)
for i in range(samples_per_chirp):
doppler_fft_result[i, :] = fft(mti_filtered[i, :])
return {
'range_doppler_matrix': np.abs(doppler_fft_result),
'chirp_type': chirp_type,
'num_chirps': num_chirps,
'samples_per_chirp': samples_per_chirp
}
except Exception as e:
logging.error(f"Error in process_chirp_sequence: {e}")
return {}
class RadarGUI:
def __init__(self, root):
self.root = root
self.root.title("Radar Signal Processor - CSV Analysis")
self.root.geometry("1400x900")
# Initialize processor
self.processor = SignalProcessor()
# Data storage
self.df = None
self.processed_data = {}
self.detected_targets = []
# Create GUI
self.create_gui()
# Start background processing
self.processing_queue = queue.Queue()
self.processing_thread = threading.Thread(target=self.background_processing, daemon=True)
self.processing_thread.start()
# Update GUI periodically
self.root.after(100, self.update_gui)
def create_gui(self):
"""Create the main GUI layout"""
# Main frame
main_frame = ttk.Frame(self.root)
main_frame.pack(fill='both', expand=True, padx=10, pady=10)
# Control panel
control_frame = ttk.LabelFrame(main_frame, text="File Controls")
control_frame.pack(fill='x', pady=5)
# File selection
ttk.Button(control_frame, text="Load CSV File",
command=self.load_csv_file).pack(side='left', padx=5, pady=5)
self.file_label = ttk.Label(control_frame, text="No file loaded")
self.file_label.pack(side='left', padx=10, pady=5)
# Processing controls
ttk.Button(control_frame, text="Process Data",
command=self.process_data).pack(side='left', padx=5, pady=5)
ttk.Button(control_frame, text="Run CFAR Detection",
command=self.run_cfar_detection).pack(side='left', padx=5, pady=5)
# Status
self.status_label = ttk.Label(control_frame, text="Status: Ready")
self.status_label.pack(side='right', padx=10, pady=5)
# Display area
display_frame = ttk.Frame(main_frame)
display_frame.pack(fill='both', expand=True, pady=5)
# Create matplotlib figures
self.create_plots(display_frame)
# Targets list
targets_frame = ttk.LabelFrame(main_frame, text="Detected Targets")
targets_frame.pack(fill='x', pady=5)
self.targets_tree = ttk.Treeview(targets_frame,
columns=('Range', 'Velocity', 'Azimuth', 'Elevation', 'SNR', 'Chirp Type'),
show='headings', height=8)
self.targets_tree.heading('Range', text='Range (m)')
self.targets_tree.heading('Velocity', text='Velocity (m/s)')
self.targets_tree.heading('Azimuth', text='Azimuth (°)')
self.targets_tree.heading('Elevation', text='Elevation (°)')
self.targets_tree.heading('SNR', text='SNR (dB)')
self.targets_tree.heading('Chirp Type', text='Chirp Type')
self.targets_tree.column('Range', width=100)
self.targets_tree.column('Velocity', width=100)
self.targets_tree.column('Azimuth', width=80)
self.targets_tree.column('Elevation', width=80)
self.targets_tree.column('SNR', width=80)
self.targets_tree.column('Chirp Type', width=100)
self.targets_tree.pack(fill='x', padx=5, pady=5)
def create_plots(self, parent):
"""Create matplotlib plots"""
# Create figure with subplots
self.fig = Figure(figsize=(12, 8))
self.canvas = FigureCanvasTkAgg(self.fig, parent)
self.canvas.get_tk_widget().pack(fill='both', expand=True)
# Create subplots
self.ax1 = self.fig.add_subplot(221) # Range profile
self.ax2 = self.fig.add_subplot(222) # Doppler spectrum
self.ax3 = self.fig.add_subplot(223) # Range-Doppler map
self.ax4 = self.fig.add_subplot(224) # MTI filtered data
# Set titles
self.ax1.set_title('Range Profile')
self.ax1.set_xlabel('Range (m)')
self.ax1.set_ylabel('Magnitude')
self.ax1.grid(True)
self.ax2.set_title('Doppler Spectrum')
self.ax2.set_xlabel('Velocity (m/s)')
self.ax2.set_ylabel('Magnitude')
self.ax2.grid(True)
self.ax3.set_title('Range-Doppler Map')
self.ax3.set_xlabel('Doppler Bin')
self.ax3.set_ylabel('Range Bin')
self.ax4.set_title('MTI Filtered Data')
self.ax4.set_xlabel('Sample')
self.ax4.set_ylabel('Magnitude')
self.ax4.grid(True)
self.fig.tight_layout()
def load_csv_file(self):
"""Load CSV file generated by testbench"""
filename = filedialog.askopenfilename(
title="Select CSV file",
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")]
)
# Add magnitude and phase calculations after loading CSV
if self.df is not None:
# Calculate magnitude from I/Q values
self.df['magnitude'] = np.sqrt(self.df['I_value']**2 + self.df['Q_value']**2)
# Calculate phase from I/Q values
self.df['phase_rad'] = np.arctan2(self.df['Q_value'], self.df['I_value'])
# If you used magnitude_squared in CSV, calculate actual magnitude
if 'magnitude_squared' in self.df.columns:
self.df['magnitude'] = np.sqrt(self.df['magnitude_squared'])
if filename:
try:
self.status_label.config(text="Status: Loading CSV file...")
self.df = pd.read_csv(filename)
self.file_label.config(text=f"Loaded: {filename.split('/')[-1]}")
self.status_label.config(text=f"Status: Loaded {len(self.df)} samples")
# Show basic info
self.show_file_info()
except Exception as e:
messagebox.showerror("Error", f"Failed to load CSV file: {e}")
self.status_label.config(text="Status: Error loading file")
def show_file_info(self):
"""Display basic information about loaded data"""
if self.df is not None:
info_text = f"Samples: {len(self.df)} | "
info_text += f"Chirps: {self.df['chirp_number'].nunique()} | "
info_text += f"Long: {len(self.df[self.df['chirp_type'] == 'LONG'])} | "
info_text += f"Short: {len(self.df[self.df['chirp_type'] == 'SHORT'])}"
self.file_label.config(text=info_text)
def process_data(self):
"""Process loaded CSV data"""
if self.df is None:
messagebox.showwarning("Warning", "Please load a CSV file first")
return
self.status_label.config(text="Status: Processing data...")
# Add to processing queue
self.processing_queue.put(('process', self.df))
def run_cfar_detection(self):
"""Run CFAR detection on processed data"""
if self.df is None:
messagebox.showwarning("Warning", "Please load and process data first")
return
self.status_label.config(text="Status: Running CFAR detection...")
self.processing_queue.put(('cfar', self.df))
def background_processing(self):
while True:
try:
task_type, data = self.processing_queue.get(timeout=1.0)
if task_type == 'process':
self._process_data_background(data)
elif task_type == 'cfar':
self._run_cfar_background(data)
else:
logging.warning(f"Unknown task type: {task_type}")
self.processing_queue.task_done()
except queue.Empty:
continue
except Exception as e:
logging.error(f"Background processing error: {e}")
# Update GUI to show error state
self.root.after(0, lambda: self.status_label.config(
text=f"Status: Processing error - {e}"
))
def _process_data_background(self, df):
try:
# Process long chirps
long_chirp_data = self.processor.process_chirp_sequence(df, 'LONG')
# Process short chirps
short_chirp_data = self.processor.process_chirp_sequence(df, 'SHORT')
# Store results
self.processed_data = {
'long': long_chirp_data,
'short': short_chirp_data
}
# Update GUI in main thread
self.root.after(0, self._update_plots_after_processing)
except Exception as e:
logging.error(f"Processing error: {e}")
error_msg = str(e)
self.root.after(0, lambda msg=error_msg: self.status_label.config(
text=f"Status: Processing error - {msg}"
))
def _run_cfar_background(self, df):
try:
# Get first chirp for CFAR demonstration
first_chirp = df[df['chirp_number'] == df['chirp_number'].min()]
if len(first_chirp) == 0:
return
# Create IQ data - FIXED TYPO: first_chirp not first_chip
iq_data = first_chirp['I_value'].values + 1j * first_chirp['Q_value'].values
# Perform range FFT
range_axis, range_profile = self.processor.range_fft(iq_data)
# Run CFAR detection
detections = self.processor.cfar_detection(range_profile)
# Convert to target objects
self.detected_targets = []
for range_bin, magnitude in detections:
target = RadarTarget(
range=range_axis[range_bin],
velocity=0, # Would need Doppler processing for velocity
azimuth=0, # From actual data
elevation=0, # From actual data
snr=20 * np.log10(magnitude + 1e-9), # Convert to dB
chirp_type='LONG',
timestamp=time.time()
)
self.detected_targets.append(target)
# Update GUI in main thread
self.root.after(0, lambda: self._update_cfar_results(range_axis, range_profile, detections))
except Exception as e:
logging.error(f"CFAR detection error: {e}")
error_msg = str(e)
self.root.after(0, lambda msg=error_msg: self.status_label.config(
text=f"Status: CFAR error - {msg}"
))
def _update_plots_after_processing(self):
try:
# Clear all plots
for ax in [self.ax1, self.ax2, self.ax3, self.ax4]:
ax.clear()
# Plot 1: Range profile from first chirp
if self.df is not None and len(self.df) > 0:
try:
first_chirp_num = self.df['chirp_number'].min()
first_chirp = self.df[self.df['chirp_number'] == first_chirp_num]
if len(first_chirp) > 0:
iq_data = first_chirp['I_value'].values + 1j * first_chirp['Q_value'].values
range_axis, range_profile = self.processor.range_fft(iq_data)
if len(range_axis) > 0 and len(range_profile) > 0:
self.ax1.plot(range_axis, range_profile, 'b-')
self.ax1.set_title('Range Profile - First Chirp')
self.ax1.set_xlabel('Range (m)')
self.ax1.set_ylabel('Magnitude')
self.ax1.grid(True)
except Exception as e:
logging.warning(f"Range profile plot error: {e}")
self.ax1.set_title('Range Profile - Error')
# Plot 2: Doppler spectrum
if self.df is not None and len(self.df) > 0:
try:
sample_data = self.df.head(1024)
if len(sample_data) > 10:
iq_data = sample_data['I_value'].values + 1j * sample_data['Q_value'].values
velocity_axis, doppler_spectrum = self.processor.doppler_fft(iq_data)
if len(velocity_axis) > 0 and len(doppler_spectrum) > 0:
self.ax2.plot(velocity_axis, doppler_spectrum, 'g-')
self.ax2.set_title('Doppler Spectrum')
self.ax2.set_xlabel('Velocity (m/s)')
self.ax2.set_ylabel('Magnitude')
self.ax2.grid(True)
except Exception as e:
logging.warning(f"Doppler spectrum plot error: {e}")
self.ax2.set_title('Doppler Spectrum - Error')
# Plot 3: Range-Doppler map
if (self.processed_data.get('long') and
'range_doppler_matrix' in self.processed_data['long'] and
self.processed_data['long']['range_doppler_matrix'].size > 0):
try:
rd_matrix = self.processed_data['long']['range_doppler_matrix']
# Use integer indices for extent
extent = [0, int(rd_matrix.shape[1]), 0, int(rd_matrix.shape[0])]
im = self.ax3.imshow(10 * np.log10(rd_matrix + 1e-9),
aspect='auto', cmap='hot',
extent=extent)
self.ax3.set_title('Range-Doppler Map (Long Chirps)')
self.ax3.set_xlabel('Doppler Bin')
self.ax3.set_ylabel('Range Bin')
self.fig.colorbar(im, ax=self.ax3, label='dB')
except Exception as e:
logging.warning(f"Range-Doppler map plot error: {e}")
self.ax3.set_title('Range-Doppler Map - Error')
# Plot 4: MTI filtered data
if self.df is not None and len(self.df) > 0:
try:
sample_data = self.df.head(100)
if len(sample_data) > 10:
iq_data = sample_data['I_value'].values + 1j * sample_data['Q_value'].values
# Original data
original_mag = np.abs(iq_data)
# MTI filtered
mti_filtered = self.processor.mti_filter(iq_data)
if mti_filtered is not None and len(mti_filtered) > 0:
mti_mag = np.abs(mti_filtered)
# Use integer indices for plotting
x_original = np.arange(len(original_mag))
x_mti = np.arange(len(mti_mag))
self.ax4.plot(x_original, original_mag, 'b-', label='Original', alpha=0.7)
self.ax4.plot(x_mti, mti_mag, 'r-', label='MTI Filtered', alpha=0.7)
self.ax4.set_title('MTI Filter Comparison')
self.ax4.set_xlabel('Sample Index')
self.ax4.set_ylabel('Magnitude')
self.ax4.legend()
self.ax4.grid(True)
except Exception as e:
logging.warning(f"MTI filter plot error: {e}")
self.ax4.set_title('MTI Filter - Error')
# Adjust layout and draw
self.fig.tight_layout()
self.canvas.draw()
self.status_label.config(text="Status: Processing complete")
except Exception as e:
logging.error(f"Plot update error: {e}")
error_msg = str(e)
self.status_label.config(text=f"Status: Plot error - {error_msg}")
def _update_cfar_results(self, range_axis, range_profile, detections):
try:
# Clear the plot
self.ax1.clear()
# Plot range profile
self.ax1.plot(range_axis, range_profile, 'b-', label='Range Profile')
# Plot detections - ensure we use integer indices
if detections and len(range_axis) > 0:
detection_ranges = []
detection_mags = []
for bin_idx, mag in detections:
# Convert bin_idx to integer and ensure it's within bounds
bin_idx_int = int(bin_idx)
if 0 <= bin_idx_int < len(range_axis):
detection_ranges.append(range_axis[bin_idx_int])
detection_mags.append(mag)
if detection_ranges: # Only plot if we have valid detections
self.ax1.plot(detection_ranges, detection_mags, 'ro',
markersize=8, label='CFAR Detections')
self.ax1.set_title('Range Profile with CFAR Detections')
self.ax1.set_xlabel('Range (m)')
self.ax1.set_ylabel('Magnitude')
self.ax1.legend()
self.ax1.grid(True)
# Update targets list
self.update_targets_list()
self.canvas.draw()
self.status_label.config(text=f"Status: CFAR complete - {len(detections)} targets detected")
except Exception as e:
logging.error(f"CFAR results update error: {e}")
error_msg = str(e)
self.status_label.config(text=f"Status: CFAR results error - {error_msg}")
def update_targets_list(self):
"""Update the targets list display"""
# Clear current list
for item in self.targets_tree.get_children():
self.targets_tree.delete(item)
# Add detected targets
for i, target in enumerate(self.detected_targets):
self.targets_tree.insert('', 'end', values=(
f"{target.range:.1f}",
f"{target.velocity:.1f}",
f"{target.azimuth}",
f"{target.elevation}",
f"{target.snr:.1f}",
target.chirp_type
))
def update_gui(self):
"""Periodic GUI update"""
# You can add any periodic updates here
self.root.after(100, self.update_gui)
def main():
"""Main application entry point"""
try:
root = tk.Tk()
app = RadarGUI(root)
root.mainloop()
except Exception as e:
logging.error(f"Application error: {e}")
messagebox.showerror("Fatal Error", f"Application failed to start: {e}")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+180 -87
View File
@@ -8,15 +8,11 @@ import numpy as np
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure from matplotlib.figure import Figure
import matplotlib.patches as patches
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, List, Tuple, Optional
from scipy import signal
from sklearn.cluster import DBSCAN from sklearn.cluster import DBSCAN
from filterpy.kalman import KalmanFilter from filterpy.kalman import KalmanFilter
import crcmod import crcmod
import math
import webbrowser import webbrowser
import tempfile import tempfile
import os import os
@@ -30,9 +26,9 @@ except ImportError:
logging.warning("pyusb not available. USB functionality will be disabled.") logging.warning("pyusb not available. USB functionality will be disabled.")
try: try:
from pyftdi.ftdi import Ftdi from pyftdi.ftdi import Ftdi
from pyftdi.usbtools import UsbTools from pyftdi.usbtools import UsbTools # noqa: F401
from pyftdi.ftdi import FtdiError from pyftdi.ftdi import FtdiError # noqa: F401
FTDI_AVAILABLE = True FTDI_AVAILABLE = True
except ImportError: except ImportError:
FTDI_AVAILABLE = False FTDI_AVAILABLE = False
@@ -198,7 +194,9 @@ class MapGenerator:
var targetMarker = new google.maps.Marker({{ var targetMarker = new google.maps.Marker({{
position: {{lat: target.lat, lng: target.lng}}, position: {{lat: target.lat, lng: target.lng}},
map: map, map: map,
title: `Target: ${{target.range:.1f}}m, ${{target.velocity:.1f}}m/s`, title: (
`Target: ${{target.range:.1f}}m, ${{target.velocity:.1f}}m/s`
),
icon: {{ icon: {{
path: google.maps.SymbolPath.CIRCLE, path: google.maps.SymbolPath.CIRCLE,
scale: 6, scale: 6,
@@ -244,7 +242,6 @@ class MapGenerator:
</body> </body>
</html> </html>
""" """
pass
class FT601Interface: class FT601Interface:
""" """
@@ -280,8 +277,14 @@ class FT601Interface:
found_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid) found_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid)
for dev in found_devices: for dev in found_devices:
try: try:
product = usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "FT601 USB3.0" product = (
serial = usb.util.get_string(dev, dev.iSerialNumber) if dev.iSerialNumber else "Unknown" usb.util.get_string(dev, dev.iProduct)
if dev.iProduct else "FT601 USB3.0"
)
serial = (
usb.util.get_string(dev, dev.iSerialNumber)
if dev.iSerialNumber else "Unknown"
)
# Create FTDI URL for the device # Create FTDI URL for the device
url = f"ftdi://{vid:04x}:{pid:04x}:{serial}/1" url = f"ftdi://{vid:04x}:{pid:04x}:{serial}/1"
@@ -294,7 +297,7 @@ class FT601Interface:
'device': dev, 'device': dev,
'serial': serial 'serial': serial
}) })
except Exception as e: except (usb.core.USBError, ValueError):
devices.append({ devices.append({
'description': f"FT601 USB3.0 (VID:{vid:04X}, PID:{pid:04X})", 'description': f"FT601 USB3.0 (VID:{vid:04X}, PID:{pid:04X})",
'vendor_id': vid, 'vendor_id': vid,
@@ -304,7 +307,7 @@ class FT601Interface:
}) })
return devices return devices
except Exception as e: except (usb.core.USBError, ValueError) as e:
logging.error(f"Error listing FT601 devices: {e}") logging.error(f"Error listing FT601 devices: {e}")
# Return mock devices for testing # Return mock devices for testing
return [ return [
@@ -346,7 +349,7 @@ class FT601Interface:
logging.info(f"FT601 device opened: {device_url}") logging.info(f"FT601 device opened: {device_url}")
return True return True
except Exception as e: except OSError as e:
logging.error(f"Error opening FT601 device: {e}") logging.error(f"Error opening FT601 device: {e}")
return False return False
@@ -399,7 +402,7 @@ class FT601Interface:
logging.info(f"FT601 device opened: {device_info['description']}") logging.info(f"FT601 device opened: {device_info['description']}")
return True return True
except Exception as e: except usb.core.USBError as e:
logging.error(f"Error opening FT601 device: {e}") logging.error(f"Error opening FT601 device: {e}")
return False return False
@@ -423,7 +426,7 @@ class FT601Interface:
return bytes(data) return bytes(data)
return None return None
elif self.device and self.ep_in: if self.device and self.ep_in:
# Direct USB access # Direct USB access
if bytes_to_read is None: if bytes_to_read is None:
bytes_to_read = 512 bytes_to_read = 512
@@ -444,7 +447,7 @@ class FT601Interface:
return bytes(data) if data else None return bytes(data) if data else None
except Exception as e: except (usb.core.USBError, OSError) as e:
logging.error(f"Error reading from FT601: {e}") logging.error(f"Error reading from FT601: {e}")
return None return None
@@ -464,7 +467,7 @@ class FT601Interface:
self.ftdi.write_data(data) self.ftdi.write_data(data)
return True return True
elif self.device and self.ep_out: if self.device and self.ep_out:
# Direct USB access # Direct USB access
# FT601 supports large transfers # FT601 supports large transfers
max_packet = 512 max_packet = 512
@@ -475,7 +478,7 @@ class FT601Interface:
return True return True
except Exception as e: except usb.core.USBError as e:
logging.error(f"Error writing to FT601: {e}") logging.error(f"Error writing to FT601: {e}")
return False return False
@@ -494,7 +497,7 @@ class FT601Interface:
self.ftdi.set_bitmode(0xFF, Ftdi.BitMode.RESET) self.ftdi.set_bitmode(0xFF, Ftdi.BitMode.RESET)
logging.info("FT601 burst mode disabled") logging.info("FT601 burst mode disabled")
return True return True
except Exception as e: except OSError as e:
logging.error(f"Error configuring burst mode: {e}") logging.error(f"Error configuring burst mode: {e}")
return False return False
return False return False
@@ -506,14 +509,14 @@ class FT601Interface:
self.ftdi.close() self.ftdi.close()
self.is_open = False self.is_open = False
logging.info("FT601 device closed") logging.info("FT601 device closed")
except Exception as e: except OSError as e:
logging.error(f"Error closing FT601 device: {e}") logging.error(f"Error closing FT601 device: {e}")
if self.device and self.is_open: if self.device and self.is_open:
try: try:
usb.util.dispose_resources(self.device) usb.util.dispose_resources(self.device)
self.is_open = False self.is_open = False
except Exception as e: except usb.core.USBError as e:
logging.error(f"Error closing FT601 device: {e}") logging.error(f"Error closing FT601 device: {e}")
class STM32USBInterface: class STM32USBInterface:
@@ -545,15 +548,21 @@ class STM32USBInterface:
found_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid) found_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid)
for dev in found_devices: for dev in found_devices:
try: try:
product = usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "STM32 CDC" product = (
serial = usb.util.get_string(dev, dev.iSerialNumber) if dev.iSerialNumber else "Unknown" usb.util.get_string(dev, dev.iProduct)
if dev.iProduct else "STM32 CDC"
)
serial = (
usb.util.get_string(dev, dev.iSerialNumber)
if dev.iSerialNumber else "Unknown"
)
devices.append({ devices.append({
'description': f"{product} ({serial})", 'description': f"{product} ({serial})",
'vendor_id': vid, 'vendor_id': vid,
'product_id': pid, 'product_id': pid,
'device': dev 'device': dev
}) })
except: except (usb.core.USBError, ValueError):
devices.append({ devices.append({
'description': f"STM32 CDC (VID:{vid:04X}, PID:{pid:04X})", 'description': f"STM32 CDC (VID:{vid:04X}, PID:{pid:04X})",
'vendor_id': vid, 'vendor_id': vid,
@@ -562,10 +571,14 @@ class STM32USBInterface:
}) })
return devices return devices
except Exception as e: except (usb.core.USBError, ValueError) as e:
logging.error(f"Error listing USB devices: {e}") logging.error(f"Error listing USB devices: {e}")
# Return mock devices for testing # Return mock devices for testing
return [{'description': 'STM32 Virtual COM Port', 'vendor_id': 0x0483, 'product_id': 0x5740}] return [{
'description': 'STM32 Virtual COM Port',
'vendor_id': 0x0483,
'product_id': 0x5740,
}]
def open_device(self, device_info): def open_device(self, device_info):
"""Open STM32 USB CDC device""" """Open STM32 USB CDC device"""
@@ -590,12 +603,18 @@ class STM32USBInterface:
# Find bulk endpoints (CDC data interface) # Find bulk endpoints (CDC data interface)
self.ep_out = usb.util.find_descriptor( self.ep_out = usb.util.find_descriptor(
intf, intf,
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT custom_match=lambda e: (
usb.util.endpoint_direction(e.bEndpointAddress)
== usb.util.ENDPOINT_OUT
)
) )
self.ep_in = usb.util.find_descriptor( self.ep_in = usb.util.find_descriptor(
intf, intf,
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN custom_match=lambda e: (
usb.util.endpoint_direction(e.bEndpointAddress)
== usb.util.ENDPOINT_IN
)
) )
if self.ep_out is None or self.ep_in is None: if self.ep_out is None or self.ep_in is None:
@@ -606,7 +625,7 @@ class STM32USBInterface:
logging.info(f"STM32 USB device opened: {device_info['description']}") logging.info(f"STM32 USB device opened: {device_info['description']}")
return True return True
except Exception as e: except usb.core.USBError as e:
logging.error(f"Error opening USB device: {e}") logging.error(f"Error opening USB device: {e}")
return False return False
@@ -622,7 +641,7 @@ class STM32USBInterface:
packet = self._create_settings_packet(settings) packet = self._create_settings_packet(settings)
logging.info("Sending radar settings to STM32 via USB...") logging.info("Sending radar settings to STM32 via USB...")
return self._send_data(packet) return self._send_data(packet)
except Exception as e: except (ValueError, struct.error) as e:
logging.error(f"Error sending settings via USB: {e}") logging.error(f"Error sending settings via USB: {e}")
return False return False
@@ -639,7 +658,7 @@ class STM32USBInterface:
return None return None
logging.error(f"USB read error: {e}") logging.error(f"USB read error: {e}")
return None return None
except Exception as e: except ValueError as e:
logging.error(f"Error reading from USB: {e}") logging.error(f"Error reading from USB: {e}")
return None return None
@@ -659,7 +678,7 @@ class STM32USBInterface:
self.ep_out.write(chunk) self.ep_out.write(chunk)
return True return True
except Exception as e: except usb.core.USBError as e:
logging.error(f"Error sending data via USB: {e}") logging.error(f"Error sending data via USB: {e}")
return False return False
@@ -685,7 +704,7 @@ class STM32USBInterface:
try: try:
usb.util.dispose_resources(self.device) usb.util.dispose_resources(self.device)
self.is_open = False self.is_open = False
except Exception as e: except usb.core.USBError as e:
logging.error(f"Error closing USB device: {e}") logging.error(f"Error closing USB device: {e}")
@@ -700,8 +719,7 @@ class RadarProcessor:
def dual_cpi_fusion(self, range_profiles_1, range_profiles_2): def dual_cpi_fusion(self, range_profiles_1, range_profiles_2):
"""Dual-CPI fusion for better detection""" """Dual-CPI fusion for better detection"""
fused_profile = np.mean(range_profiles_1, axis=0) + np.mean(range_profiles_2, axis=0) return np.mean(range_profiles_1, axis=0) + np.mean(range_profiles_2, axis=0)
return fused_profile
def multi_prf_unwrap(self, doppler_measurements, prf1, prf2): def multi_prf_unwrap(self, doppler_measurements, prf1, prf2):
"""Multi-PRF velocity unwrapping""" """Multi-PRF velocity unwrapping"""
@@ -746,7 +764,7 @@ class RadarProcessor:
return clusters return clusters
def association(self, detections, clusters): def association(self, detections, _clusters):
"""Association of detections to tracks""" """Association of detections to tracks"""
associated_detections = [] associated_detections = []
@@ -830,13 +848,19 @@ class USBPacketParser:
lon = float(parts[1]) lon = float(parts[1])
alt = float(parts[2]) alt = float(parts[2])
pitch = float(parts[3]) # Pitch angle in degrees pitch = float(parts[3]) # Pitch angle in degrees
return GPSData(latitude=lat, longitude=lon, altitude=alt, pitch=pitch, timestamp=time.time()) return GPSData(
latitude=lat,
longitude=lon,
altitude=alt,
pitch=pitch,
timestamp=time.time(),
)
# Try binary format (30 bytes with pitch) # Try binary format (30 bytes with pitch)
if len(data) >= 30 and data[0:4] == b'GPSB': if len(data) >= 30 and data[0:4] == b'GPSB':
return self._parse_binary_gps_with_pitch(data) return self._parse_binary_gps_with_pitch(data)
except Exception as e: except ValueError as e:
logging.error(f"Error parsing GPS data: {e}") logging.error(f"Error parsing GPS data: {e}")
return None return None
@@ -888,7 +912,7 @@ class USBPacketParser:
timestamp=time.time() timestamp=time.time()
) )
except Exception as e: except (ValueError, struct.error) as e:
logging.error(f"Error parsing binary GPS with pitch: {e}") logging.error(f"Error parsing binary GPS with pitch: {e}")
return None return None
@@ -910,7 +934,7 @@ class RadarPacketParser:
if len(packet) < 6: if len(packet) < 6:
return None return None
sync = packet[0:2] _sync = packet[0:2]
packet_type = packet[2] packet_type = packet[2]
length = packet[3] length = packet[3]
@@ -922,18 +946,20 @@ class RadarPacketParser:
crc_calculated = self.calculate_crc(packet[0:4+length]) crc_calculated = self.calculate_crc(packet[0:4+length])
if crc_calculated != crc_received: if crc_calculated != crc_received:
logging.warning(f"CRC mismatch: got {crc_received:04X}, calculated {crc_calculated:04X}") logging.warning(
f"CRC mismatch: got {crc_received:04X}, "
f"calculated {crc_calculated:04X}"
)
return None return None
if packet_type == 0x01: if packet_type == 0x01:
return self.parse_range_packet(payload) return self.parse_range_packet(payload)
elif packet_type == 0x02: if packet_type == 0x02:
return self.parse_doppler_packet(payload) return self.parse_doppler_packet(payload)
elif packet_type == 0x03: if packet_type == 0x03:
return self.parse_detection_packet(payload) return self.parse_detection_packet(payload)
else: logging.warning(f"Unknown packet type: {packet_type:02X}")
logging.warning(f"Unknown packet type: {packet_type:02X}") return None
return None
def calculate_crc(self, data): def calculate_crc(self, data):
return self.crc16_func(data) return self.crc16_func(data)
@@ -956,7 +982,7 @@ class RadarPacketParser:
'chirp': chirp_counter, 'chirp': chirp_counter,
'timestamp': time.time() 'timestamp': time.time()
} }
except Exception as e: except (ValueError, struct.error) as e:
logging.error(f"Error parsing range packet: {e}") logging.error(f"Error parsing range packet: {e}")
return None return None
@@ -980,7 +1006,7 @@ class RadarPacketParser:
'chirp': chirp_counter, 'chirp': chirp_counter,
'timestamp': time.time() 'timestamp': time.time()
} }
except Exception as e: except (ValueError, struct.error) as e:
logging.error(f"Error parsing Doppler packet: {e}") logging.error(f"Error parsing Doppler packet: {e}")
return None return None
@@ -1002,7 +1028,7 @@ class RadarPacketParser:
'chirp': chirp_counter, 'chirp': chirp_counter,
'timestamp': time.time() 'timestamp': time.time()
} }
except Exception as e: except (usb.core.USBError, ValueError) as e:
logging.error(f"Error parsing detection packet: {e}") logging.error(f"Error parsing detection packet: {e}")
return None return None
@@ -1041,7 +1067,13 @@ class RadarGUI:
# Counters # Counters
self.received_packets = 0 self.received_packets = 0
self.current_gps = GPSData(latitude=41.9028, longitude=12.4964, altitude=0, pitch=0.0, timestamp=0) self.current_gps = GPSData(
latitude=41.9028,
longitude=12.4964,
altitude=0,
pitch=0.0,
timestamp=0,
)
self.corrected_elevations = [] self.corrected_elevations = []
self.map_file_path = None self.map_file_path = None
self.google_maps_api_key = "YOUR_GOOGLE_MAPS_API_KEY" self.google_maps_api_key = "YOUR_GOOGLE_MAPS_API_KEY"
@@ -1223,9 +1255,20 @@ class RadarGUI:
targets_frame = ttk.LabelFrame(display_frame, text="Detected Targets (Pitch Corrected)") targets_frame = ttk.LabelFrame(display_frame, text="Detected Targets (Pitch Corrected)")
targets_frame.pack(side='right', fill='y', padx=5) targets_frame.pack(side='right', fill='y', padx=5)
self.targets_tree = ttk.Treeview(targets_frame, self.targets_tree = ttk.Treeview(
columns=('ID', 'Range', 'Velocity', 'Azimuth', 'Elevation', 'Corrected Elev', 'SNR'), targets_frame,
show='headings', height=20) columns=(
'ID',
'Range',
'Velocity',
'Azimuth',
'Elevation',
'Corrected Elev',
'SNR',
),
show='headings',
height=20,
)
self.targets_tree.heading('ID', text='Track ID') self.targets_tree.heading('ID', text='Track ID')
self.targets_tree.heading('Range', text='Range (m)') self.targets_tree.heading('Range', text='Range (m)')
self.targets_tree.heading('Velocity', text='Velocity (m/s)') self.targets_tree.heading('Velocity', text='Velocity (m/s)')
@@ -1243,7 +1286,11 @@ class RadarGUI:
self.targets_tree.column('SNR', width=70) self.targets_tree.column('SNR', width=70)
# Add scrollbar to targets tree # Add scrollbar to targets tree
tree_scroll = ttk.Scrollbar(targets_frame, orient="vertical", command=self.targets_tree.yview) tree_scroll = ttk.Scrollbar(
targets_frame,
orient="vertical",
command=self.targets_tree.yview,
)
self.targets_tree.configure(yscrollcommand=tree_scroll.set) self.targets_tree.configure(yscrollcommand=tree_scroll.set)
self.targets_tree.pack(side='left', fill='both', expand=True, padx=5, pady=5) self.targets_tree.pack(side='left', fill='both', expand=True, padx=5, pady=5)
tree_scroll.pack(side='right', fill='y', padx=(0, 5), pady=5) tree_scroll.pack(side='right', fill='y', padx=(0, 5), pady=5)
@@ -1292,7 +1339,9 @@ class RadarGUI:
if not self.ft601_interface.open_device_direct(ft601_devices[ft601_index]): if not self.ft601_interface.open_device_direct(ft601_devices[ft601_index]):
device_url = ft601_devices[ft601_index]['url'] device_url = ft601_devices[ft601_index]['url']
if not self.ft601_interface.open_device(device_url): if not self.ft601_interface.open_device(device_url):
logging.warning("Failed to open FT601 device, continuing without radar data") logging.warning(
"Failed to open FT601 device, continuing without radar data"
)
messagebox.showwarning("Warning", "Failed to open FT601 device") messagebox.showwarning("Warning", "Failed to open FT601 device")
else: else:
# Configure burst mode if enabled # Configure burst mode if enabled
@@ -1319,9 +1368,9 @@ class RadarGUI:
logging.info("Radar system started successfully with FT601 USB 3.0") logging.info("Radar system started successfully with FT601 USB 3.0")
except Exception as e: except usb.core.USBError as e:
messagebox.showerror("Error", f"Failed to start radar: {e}") messagebox.showerror("Error", f"Failed to start radar: {e}")
logging.error(f"Start radar error: {e}") logging.error(f"Start radar error: {e}")
def stop_radar(self): def stop_radar(self):
"""Stop radar operation""" """Stop radar operation"""
@@ -1335,8 +1384,8 @@ class RadarGUI:
logging.info("Radar system stopped") logging.info("Radar system stopped")
def process_radar_data(self): def _process_radar_data_ft601(self):
"""Process incoming radar data from FT601""" """Process incoming radar data from FT601 (legacy, superseded by FTDI version)."""
buffer = bytearray() buffer = bytearray()
while True: while True:
if self.running and self.ft601_interface.is_open: if self.running and self.ft601_interface.is_open:
@@ -1364,18 +1413,18 @@ class RadarGUI:
else: else:
break break
except Exception as e: except usb.core.USBError as e:
logging.error(f"Error processing radar data: {e}") logging.error(f"Error processing radar data: {e}")
time.sleep(0.1) time.sleep(0.1)
else: else:
time.sleep(0.1) time.sleep(0.1)
def get_packet_length(self, packet): def get_packet_length(self, _packet):
"""Calculate packet length including header and footer""" """Calculate packet length including header and footer"""
# This should match your packet structure # This should match your packet structure
return 64 # Example: 64-byte packets return 64 # Example: 64-byte packets
def process_gps_data(self): def _process_gps_data_ft601(self):
"""Step 16/17: Process GPS data from STM32 via USB CDC""" """Step 16/17: Process GPS data from STM32 via USB CDC"""
while True: while True:
if self.running and self.stm32_usb_interface.is_open: if self.running and self.stm32_usb_interface.is_open:
@@ -1386,8 +1435,12 @@ class RadarGUI:
gps_data = self.usb_packet_parser.parse_gps_data(data) gps_data = self.usb_packet_parser.parse_gps_data(data)
if gps_data: if gps_data:
self.gps_data_queue.put(gps_data) self.gps_data_queue.put(gps_data)
logging.info(f"GPS Data received via USB: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m, Pitch {gps_data.pitch:.1f}°") logging.info(
except Exception as e: f"GPS Data received via USB: Lat {gps_data.latitude:.6f}, "
f"Lon {gps_data.longitude:.6f}, "
f"Alt {gps_data.altitude:.1f}m, Pitch {gps_data.pitch:.1f}°"
)
except usb.core.USBError as e:
logging.error(f"Error processing GPS data via USB: {e}") logging.error(f"Error processing GPS data via USB: {e}")
time.sleep(0.1) time.sleep(0.1)
@@ -1399,7 +1452,10 @@ class RadarGUI:
# Apply pitch correction to elevation # Apply pitch correction to elevation
raw_elevation = packet['elevation'] raw_elevation = packet['elevation']
corrected_elevation = self.apply_pitch_correction(raw_elevation, self.current_gps.pitch) corrected_elevation = self.apply_pitch_correction(
raw_elevation,
self.current_gps.pitch,
)
# Store correction for display # Store correction for display
self.corrected_elevations.append({ self.corrected_elevations.append({
@@ -1427,18 +1483,27 @@ class RadarGUI:
elif packet['type'] == 'doppler': elif packet['type'] == 'doppler':
lambda_wavelength = 3e8 / self.settings.system_frequency lambda_wavelength = 3e8 / self.settings.system_frequency
velocity = (packet['doppler_real'] / 32767.0) * (self.settings.prf1 * lambda_wavelength / 2) velocity = (packet['doppler_real'] / 32767.0) * (
self.settings.prf1 * lambda_wavelength / 2
)
self.update_target_velocity(packet, velocity) self.update_target_velocity(packet, velocity)
elif packet['type'] == 'detection': elif packet['type'] == 'detection':
if packet['detected']: if packet['detected']:
# Apply pitch correction to detection elevation # Apply pitch correction to detection elevation
raw_elevation = packet['elevation'] raw_elevation = packet['elevation']
corrected_elevation = self.apply_pitch_correction(raw_elevation, self.current_gps.pitch) corrected_elevation = self.apply_pitch_correction(
raw_elevation,
self.current_gps.pitch,
)
logging.info(f"CFAR Detection: Raw Elev {raw_elevation}°, Corrected Elev {corrected_elevation:.1f}°, Pitch {self.current_gps.pitch:.1f}°") logging.info(
f"CFAR Detection: Raw Elev {raw_elevation}°, "
f"Corrected Elev {corrected_elevation:.1f}°, "
f"Pitch {self.current_gps.pitch:.1f}°"
)
except Exception as e: except (ValueError, IndexError) as e:
logging.error(f"Error processing radar packet: {e}") logging.error(f"Error processing radar packet: {e}")
def update_range_doppler_map(self, target): def update_range_doppler_map(self, target):
@@ -1484,7 +1549,11 @@ class RadarGUI:
info_frame = ttk.Frame(map_frame) info_frame = ttk.Frame(map_frame)
info_frame.pack(fill='x', pady=5) info_frame.pack(fill='x', pady=5)
self.map_info_label = ttk.Label(info_frame, text="No GPS data received yet", font=('Arial', 10)) self.map_info_label = ttk.Label(
info_frame,
text="No GPS data received yet",
font=('Arial', 10),
)
self.map_info_label.pack() self.map_info_label.pack()
def open_map_in_browser(self): def open_map_in_browser(self):
@@ -1492,7 +1561,10 @@ class RadarGUI:
if self.map_file_path and os.path.exists(self.map_file_path): if self.map_file_path and os.path.exists(self.map_file_path):
webbrowser.open('file://' + os.path.abspath(self.map_file_path)) webbrowser.open('file://' + os.path.abspath(self.map_file_path))
else: else:
messagebox.showwarning("Warning", "No map file available. Generate map first by receiving GPS data.") messagebox.showwarning(
"Warning",
"No map file available. Generate map first by receiving GPS data.",
)
def refresh_map(self): def refresh_map(self):
"""Refresh the map with current data""" """Refresh the map with current data"""
@@ -1506,7 +1578,12 @@ class RadarGUI:
try: try:
# Create temporary HTML file # Create temporary HTML file
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f: with tempfile.NamedTemporaryFile(
mode='w',
suffix='.html',
delete=False,
encoding='utf-8',
) as f:
map_html = self.map_generator.generate_map( map_html = self.map_generator.generate_map(
self.current_gps, self.current_gps,
self.radar_processor.detected_targets, self.radar_processor.detected_targets,
@@ -1524,9 +1601,9 @@ class RadarGUI:
) )
logging.info(f"Map generated: {self.map_file_path}") logging.info(f"Map generated: {self.map_file_path}")
except Exception as e: except OSError as e:
logging.error(f"Error generating map: {e}") logging.error(f"Error generating map: {e}")
self.map_status_label.config(text=f"Map: Error - {str(e)}") self.map_status_label.config(text=f"Map: Error - {e!s}")
def update_gps_display(self): def update_gps_display(self):
"""Step 18: Update GPS and pitch display""" """Step 18: Update GPS and pitch display"""
@@ -1537,7 +1614,12 @@ class RadarGUI:
# Update GPS label # Update GPS label
self.gps_label.config( self.gps_label.config(
text=f"GPS: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m") text=(
f"GPS: Lat {gps_data.latitude:.6f}, "
f"Lon {gps_data.longitude:.6f}, "
f"Alt {gps_data.altitude:.1f}m"
)
)
# Update pitch label with color coding # Update pitch label with color coding
pitch_text = f"Pitch: {gps_data.pitch:+.1f}°" pitch_text = f"Pitch: {gps_data.pitch:+.1f}°"
@@ -1585,8 +1667,11 @@ class RadarGUI:
entry.grid(row=i, column=1, padx=5, pady=5) entry.grid(row=i, column=1, padx=5, pady=5)
self.settings_vars[attr] = var self.settings_vars[attr] = var
ttk.Button(settings_frame, text="Apply Settings", ttk.Button(
command=self.apply_settings).grid(row=len(entries), column=0, columnspan=2, pady=10) settings_frame,
text="Apply Settings",
command=self.apply_settings,
).grid(row=len(entries), column=0, columnspan=2, pady=10)
def apply_settings(self): def apply_settings(self):
"""Step 13: Apply and send radar settings via USB""" """Step 13: Apply and send radar settings via USB"""
@@ -1665,7 +1750,7 @@ class RadarGUI:
else: else:
break break
except Exception as e: except (usb.core.USBError, ValueError, struct.error) as e:
logging.error(f"Error processing radar data: {e}") logging.error(f"Error processing radar data: {e}")
time.sleep(0.1) time.sleep(0.1)
else: else:
@@ -1682,8 +1767,12 @@ class RadarGUI:
gps_data = self.usb_packet_parser.parse_gps_data(data) gps_data = self.usb_packet_parser.parse_gps_data(data)
if gps_data: if gps_data:
self.gps_data_queue.put(gps_data) self.gps_data_queue.put(gps_data)
logging.info(f"GPS Data received via USB: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m, Pitch {gps_data.pitch:.1f}°") logging.info(
except Exception as e: f"GPS Data received via USB: Lat {gps_data.latitude:.6f}, "
f"Lon {gps_data.longitude:.6f}, "
f"Alt {gps_data.altitude:.1f}m, Pitch {gps_data.pitch:.1f}°"
)
except usb.core.USBError as e:
logging.error(f"Error processing GPS data via USB: {e}") logging.error(f"Error processing GPS data via USB: {e}")
time.sleep(0.1) time.sleep(0.1)
@@ -1692,8 +1781,12 @@ class RadarGUI:
try: try:
# Update status with pitch information # Update status with pitch information
if self.running: if self.running:
self.status_label.config( self.status_label.config(
text=f"Status: Running - Packets: {self.received_packets} - Pitch: {self.current_gps.pitch:+.1f}°") text=(
f"Status: Running - Packets: {self.received_packets} - "
f"Pitch: {self.current_gps.pitch:+.1f}°"
)
)
# Update range-Doppler map # Update range-Doppler map
if hasattr(self, 'range_doppler_plot'): if hasattr(self, 'range_doppler_plot'):
@@ -1707,7 +1800,7 @@ class RadarGUI:
# Update GPS and pitch display # Update GPS and pitch display
self.update_gps_display() self.update_gps_display()
except Exception as e: except (ValueError, IndexError) as e:
logging.error(f"Error updating GUI: {e}") logging.error(f"Error updating GUI: {e}")
self.root.after(100, self.update_gui) self.root.after(100, self.update_gui)
@@ -1716,9 +1809,9 @@ def main():
"""Main application entry point""" """Main application entry point"""
try: try:
root = tk.Tk() root = tk.Tk()
app = RadarGUI(root) _app = RadarGUI(root) # must stay alive for mainloop
root.mainloop() root.mainloop()
except Exception as e: except Exception as e: # noqa: BLE001
logging.error(f"Application error: {e}") logging.error(f"Application error: {e}")
messagebox.showerror("Fatal Error", f"Application failed to start: {e}") messagebox.showerror("Fatal Error", f"Application failed to start: {e}")

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