From 79d07fdda72744909d4a71c7b2cac349d04dfd87 Mon Sep 17 00:00:00 2001 From: onur Date: Sat, 1 Aug 2026 01:15:47 +0300 Subject: [PATCH] Implement generic MAX2771 raw Ethernet recorder --- .gitignore | 25 + Makefile | 42 ++ README.md | 506 ++++++++++++++++ constraints/genesys2_max2771_recorder.xdc | 4 + docs/hardware_assumptions.md | 26 + docs/raw_ethernet_protocol.md | 65 ++ docs/timing_constraints.md | 24 + host/Makefile | 15 + host/max2771_raw_rx.c | 571 ++++++++++++++++++ rtl/common/async_fifo_gray.sv | 122 ++++ rtl/common/ethernet_crc32.sv | 23 + rtl/common/ethernet_packetizer.sv | 233 +++++++ rtl/common/ethernet_tx_mac.sv | 199 ++++++ rtl/common/gray_counter_sync.sv | 42 ++ rtl/common/max2771_capture.sv | 69 +++ rtl/common/mdio_master.sv | 97 +++ rtl/common/phy_reset_ctrl.sv | 33 + rtl/common/pulse_sync.sv | 38 ++ rtl/common/reset_sync.sv | 27 + scripts/build_vivado.tcl | 4 + scripts/program_fpga.tcl | 4 + scripts/run_common_sim.sh | 28 + targets/efinix_ti375_c529/Makefile | 19 + targets/efinix_ti375_c529/README.md | 232 +++++++ .../constraints/ti375_max2771_recorder.sdc | 26 + .../efinix_ti375_c529/docs/efinity_flow.md | 26 + .../docs/hardware_assumptions.md | 67 ++ .../interface/create_interface.py | 154 +++++ .../rtl/phy_init_rtl8211f.sv | 147 +++++ .../efinix_ti375_c529/rtl/rgmii_tx_efinix.sv | 31 + .../rtl/ti375_ethernet_selftest_top.sv | 142 +++++ .../rtl/ti375_max2771_recorder_top.sv | 202 +++++++ .../scripts/build_efinity.sh | 75 +++ .../scripts/build_ethernet_selftest.sh | 8 + .../efinix_ti375_c529/scripts/check_timing.py | 35 ++ .../scripts/generate_project.py | 101 ++++ .../scripts/program_efinity.sh | 21 + .../scripts/program_ethernet_selftest.sh | 7 + targets/efinix_ti375_c529/scripts/run_sim.sh | 36 ++ .../tb/tb_phy_init_rtl8211f.sv | 93 +++ .../tb/tb_rgmii_tx_efinix.sv | 36 ++ targets/genesys2/Makefile | 10 + targets/genesys2/README.md | 104 ++++ .../constraints/genesys2_max2771_recorder.xdc | 75 +++ targets/genesys2/docs/hardware_assumptions.md | 48 ++ targets/genesys2/docs/timing_constraints.md | 42 ++ .../genesys2/rtl/ethernet_clock_gen_xilinx.sv | 54 ++ targets/genesys2/rtl/max2771_recorder_top.sv | 172 ++++++ targets/genesys2/rtl/phy_init_rtl8211e.sv | 85 +++ targets/genesys2/rtl/rgmii_tx_xilinx.sv | 39 ++ targets/genesys2/scripts/build_vivado.tcl | 93 +++ targets/genesys2/scripts/program_fpga.tcl | 24 + tb/common/tb_async_fifo_gray.sv | 103 ++++ tb/common/tb_capture_overflow.sv | 49 ++ tb/common/tb_ethernet_crc32.sv | 32 + tb/common/tb_ethernet_tx_mac.sv | 100 +++ tb/common/tb_recorder_stream.sv | 125 ++++ 57 files changed, 4810 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 constraints/genesys2_max2771_recorder.xdc create mode 100644 docs/hardware_assumptions.md create mode 100644 docs/raw_ethernet_protocol.md create mode 100644 docs/timing_constraints.md create mode 100644 host/Makefile create mode 100644 host/max2771_raw_rx.c create mode 100644 rtl/common/async_fifo_gray.sv create mode 100644 rtl/common/ethernet_crc32.sv create mode 100644 rtl/common/ethernet_packetizer.sv create mode 100644 rtl/common/ethernet_tx_mac.sv create mode 100644 rtl/common/gray_counter_sync.sv create mode 100644 rtl/common/max2771_capture.sv create mode 100644 rtl/common/mdio_master.sv create mode 100644 rtl/common/phy_reset_ctrl.sv create mode 100644 rtl/common/pulse_sync.sv create mode 100644 rtl/common/reset_sync.sv create mode 100644 scripts/build_vivado.tcl create mode 100644 scripts/program_fpga.tcl create mode 100755 scripts/run_common_sim.sh create mode 100644 targets/efinix_ti375_c529/Makefile create mode 100644 targets/efinix_ti375_c529/README.md create mode 100644 targets/efinix_ti375_c529/constraints/ti375_max2771_recorder.sdc create mode 100644 targets/efinix_ti375_c529/docs/efinity_flow.md create mode 100644 targets/efinix_ti375_c529/docs/hardware_assumptions.md create mode 100755 targets/efinix_ti375_c529/interface/create_interface.py create mode 100644 targets/efinix_ti375_c529/rtl/phy_init_rtl8211f.sv create mode 100644 targets/efinix_ti375_c529/rtl/rgmii_tx_efinix.sv create mode 100644 targets/efinix_ti375_c529/rtl/ti375_ethernet_selftest_top.sv create mode 100644 targets/efinix_ti375_c529/rtl/ti375_max2771_recorder_top.sv create mode 100755 targets/efinix_ti375_c529/scripts/build_efinity.sh create mode 100755 targets/efinix_ti375_c529/scripts/build_ethernet_selftest.sh create mode 100755 targets/efinix_ti375_c529/scripts/check_timing.py create mode 100755 targets/efinix_ti375_c529/scripts/generate_project.py create mode 100755 targets/efinix_ti375_c529/scripts/program_efinity.sh create mode 100755 targets/efinix_ti375_c529/scripts/program_ethernet_selftest.sh create mode 100755 targets/efinix_ti375_c529/scripts/run_sim.sh create mode 100644 targets/efinix_ti375_c529/tb/tb_phy_init_rtl8211f.sv create mode 100644 targets/efinix_ti375_c529/tb/tb_rgmii_tx_efinix.sv create mode 100644 targets/genesys2/Makefile create mode 100644 targets/genesys2/README.md create mode 100644 targets/genesys2/constraints/genesys2_max2771_recorder.xdc create mode 100644 targets/genesys2/docs/hardware_assumptions.md create mode 100644 targets/genesys2/docs/timing_constraints.md create mode 100644 targets/genesys2/rtl/ethernet_clock_gen_xilinx.sv create mode 100644 targets/genesys2/rtl/max2771_recorder_top.sv create mode 100644 targets/genesys2/rtl/phy_init_rtl8211e.sv create mode 100644 targets/genesys2/rtl/rgmii_tx_xilinx.sv create mode 100644 targets/genesys2/scripts/build_vivado.tcl create mode 100644 targets/genesys2/scripts/program_fpga.tcl create mode 100644 tb/common/tb_async_fifo_gray.sv create mode 100644 tb/common/tb_capture_overflow.sv create mode 100644 tb/common/tb_ethernet_crc32.sv create mode 100644 tb/common/tb_ethernet_tx_mac.sv create mode 100644 tb/common/tb_recorder_stream.sv diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..866cfe4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# FPGA/simulator generated directories +.Xil/ +build/ +build_selftest/ +targets/*/build/ +targets/*/build_selftest/ + +# Generated binaries and FPGA images +host/max2771_raw_rx +*.bit +*.hex +*.dcp + +# Tool logs/reports outside build directories +reports/ +transcript +clockInfo.txt +vivado*.jou +vivado*.log +vivado*.str +vsim.wlf + +# Local capture artifacts +*.bin +*.bin.log diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f1d020a --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +.PHONY: all host host-test sim genesys2-sim genesys2-build genesys2-program \ + efinix-sim efinix-build efinix-program efinix-selftest-build \ + efinix-selftest-program clean + +all: host sim + +host: + $(MAKE) -C host all + +host-test: + $(MAKE) -C host test + +sim: + ./scripts/run_common_sim.sh + +genesys2-sim: + $(MAKE) -C targets/genesys2 sim + +genesys2-build: + $(MAKE) -C targets/genesys2 build + +genesys2-program: + $(MAKE) -C targets/genesys2 program + +efinix-sim: + $(MAKE) -C targets/efinix_ti375_c529 sim + +efinix-build: + $(MAKE) -C targets/efinix_ti375_c529 build + +efinix-program: + $(MAKE) -C targets/efinix_ti375_c529 program + +efinix-selftest-build: + $(MAKE) -C targets/efinix_ti375_c529 selftest-build + +efinix-selftest-program: + $(MAKE) -C targets/efinix_ti375_c529 selftest-program + +clean: + $(MAKE) -C host clean + $(RM) -r build/common/questa build/genesys2 diff --git a/README.md b/README.md new file mode 100644 index 0000000..697a7d6 --- /dev/null +++ b/README.md @@ -0,0 +1,506 @@ +# MAX2771 processor-free raw Ethernet recorder + +This repository contains one protocol-compatible recorder implementation with +two FPGA board targets: + +| Target | FPGA/tool | PHY | Hardware status | +|---|---|---|---| +| Digilent Genesys 2 | Kintex-7 `xc7k325tffg900-2`, Vivado | RTL8211E-VL | synthesis, implementation and simulation verified | +| Efinix Ti375 C529 kit | Titanium `Ti375C529`, Efinity | RTL8211FDI | built, programmed and tested over a real 1 Gb/s cable | + +Both targets use exactly the same handwritten capture, FIFO, packetizer, +Ethernet MAC, CRC and MDIO controller RTL. They differ only where the board, +FPGA family or Ethernet PHY genuinely differs. There is no CPU, PS, RISC-V, +MicroBlaze, AXI, block design, generated Ethernet/FIFO IP, IP stack, ARP, IP, +UDP or TCP. + +## What is generic and what is board-specific? + +The portable modules live only in `rtl/common/`; they are not duplicated under +either target: + +| Common module | Purpose | +|---|---| +| `max2771_capture.sv` | captures each 4-bit word, assigns its 64-bit index and counts loss | +| `async_fifo_gray.sv` | handwritten dual-clock FIFO with binary/Gray pointers and two-stage CDC | +| `gray_counter_sync.sv` | coherently transfers monotonically increasing counters | +| `pulse_sync.sv` | safe event transfer between unrelated clock domains | +| `reset_sync.sv` | asynchronous assertion, synchronous deassertion | +| `ethernet_packetizer.sv` | builds the 32-byte MX27 header and packs two samples per byte | +| `ethernet_tx_mac.sv` | emits preamble, SFD, Ethernet header, padding, FCS and IFG | +| `ethernet_crc32.sv` | reflected IEEE Ethernet CRC-32 calculation | +| `mdio_master.sv` | handwritten IEEE Clause-22 MDIO transaction engine | +| `phy_reset_ctrl.sv` | deterministic millisecond PHY reset timing | + +Board-specific sources are deliberately separate: + +- `targets/genesys2/rtl/` contains the Kintex-7 top-level, primitive MMCM, + Xilinx ODDR RGMII output and RTL8211E PHY-status decoder. +- `targets/efinix_ti375_c529/rtl/` contains the Ti375 top-level, Efinity + GPIO-DDIO mapping, RTL8211FDI PHY-status decoder and a separate hardware + Ethernet self-test top-level. +- Each target owns its constraints, pin/periphery generation, build scripts, + programming script, target tests and hardware notes. + +Changing a common protocol/data-path module affects both targets. Changing a +file below one target affects only that board. The build scripts use explicit +source lists and never glob another target's RTL. + +## Repository hierarchy + +```text +data_recorder/ +├── rtl/common/ portable synthesizable RTL used by both boards +├── tb/common/ portable self-checking protocol/FIFO/MAC tests +├── scripts/run_common_sim.sh common Questa regression +├── targets/ +│ ├── genesys2/ +│ │ ├── rtl/ Kintex-7 top, MMCM, ODDR, RTL8211E logic +│ │ ├── constraints/ verified Genesys 2 XDC +│ │ ├── scripts/ non-project Vivado build/program scripts +│ │ ├── docs/ Genesys pin and timing assumptions +│ │ ├── Makefile +│ │ └── README.md +│ └── efinix_ti375_c529/ +│ ├── rtl/ Ti375 production and self-test tops +│ ├── interface/ Efinity Interface Designer Python API script +│ ├── constraints/ handwritten CDC/clock SDC additions +│ ├── scripts/ build, timing check, program and simulation +│ ├── tb/ PHY and RGMII target tests +│ ├── docs/ Efinity flow and hardware assumptions +│ ├── Makefile +│ └── README.md +├── host/ Linux AF_PACKET/TPACKET_V3 receiver +├── docs/raw_ethernet_protocol.md byte-exact board-independent wire protocol +├── docs/hardware_assumptions.md cross-target assumptions/sign-off index +├── docs/timing_constraints.md cross-target timing/CDC index +├── constraints/ Genesys XDC compatibility entry point +├── Makefile convenient entry points for both targets +└── build/ generated locally; ignored by Git +``` + +The root `scripts/build_vivado.tcl`, `scripts/program_fpga.tcl` and +`constraints/genesys2_max2771_recorder.xdc` paths are compatibility entry +points required by the original Genesys flow. They source the authoritative +files under `targets/genesys2/`; there is still only one implementation to +maintain. + +## End-to-end architecture + +```text +MAX2771 4-bit bus + │ one word per selected RF clock edge + ▼ +input DDR/IO registers → sample index/loss tag → 16,384 × 69 async FIFO + │ │ + │ RF clock domain │ 125 MHz domain + └──────────────── Gray-pointer CDC ───────────┘ + ▼ + 2936-sample packetizer + ▼ + raw Ethernet MAC + CRC/FCS + IFG + ▼ + GMII byte → RGMII DDR adapter + ▼ + onboard 1000BASE-T PHY → Linux NIC + ▼ + AF_PACKET/TPACKET_V3 → packed `.bin` +``` + +The MAX2771 cannot be backpressured. If the FIFO fills, the current sample is +not written over unread data; the drop counter increments and the first later +successful sample carries a discontinuity marker. + +## Raw Ethernet protocol + +- destination: `ff:ff:ff:ff:ff:ff` +- source: `02:27:71:00:00:01` +- EtherType: `0x88b5` +- standard payload: 1500 bytes +- custom header: 32 bytes, magic `MX27`, network byte order +- sample data: 1468 bytes = 2936 four-bit samples +- packing: earlier sample in `[7:4]`, later sample in `[3:0]` +- no VLAN and no jumbo frames + +The FPGA emits preamble, SFD, FCS and 12-byte IFG. A normal Linux NIC removes +preamble/SFD/FCS before exposing the frame to `AF_PACKET`. The complete header, +flags and byte offsets are in [the raw protocol specification](docs/raw_ethernet_protocol.md). + +## Prerequisites + +Common host/simulation tools: + +- Linux with GCC and GNU Make; +- root or `CAP_NET_RAW` for live capture; +- QuestaSim for the checked HDL regression (Icarus-compatible common RTL can + also be integrated independently); +- a dedicated Gigabit Ethernet interface and storage faster than 16.368 MB/s + for the maximum specified sample rate. + +Genesys 2 additionally requires Vivado supporting Kintex-7. Efinix requires an +Efinity installation supporting Ti375; this workstation uses +`/tools/efinix/efinity/2026.1`. + +## First checkout and common tests + +From the repository root: + +```bash +make host +make host-test +make sim +``` + +`make sim` tests CRC, complete frame bytes/FCS/IFG, randomized asynchronous +FIFO wrap/full/empty, sample reconstruction, packet sequence/index propagation, +overflow handling and reset behavior. Target-specific test commands are shown +below. + +## Genesys 2: wiring, bitstream and programming + +Detailed target documentation is in +[targets/genesys2/README.md](targets/genesys2/README.md). + +MAX2771-to-JA wiring, all at 3.3 V LVCMOS: + +| Signal | FPGA ball | Connector | +|---|---:|---:| +| RF clock | U27 | JA[0] | +| data 0 | U28 | JA[1] | +| data 1 | T26 | JA[2] | +| data 2 | T27 | JA[3] | +| data 3 | T22 | JA[4] | + +Build from the repository root: + +```bash +make genesys2-build +``` + +This runs Vivado in memory/non-project mode, synthesizes, places, routes, +checks timing/CDC/DRC and writes: + +```text +build/genesys2/vivado/max2771_recorder.bit +build/genesys2/reports/*.rpt +``` + +Program volatile FPGA configuration over Digilent JTAG: + +```bash +make genesys2-program +``` + +Or program a selected bitstream explicitly: + +```bash +vivado -mode batch \ + -source targets/genesys2/scripts/program_fpga.tcl \ + -tclargs build/genesys2/vivado/max2771_recorder.bit +``` + +## Efinix Ti375 C529: wiring, bitstream and programming + +Detailed target documentation is in +[targets/efinix_ti375_c529/README.md](targets/efinix_ti375_c529/README.md). + +MAX2771-to-PMOD0/J13 wiring, all at `3.3_V_LVCMOS`: + +| Signal | FPGA ball/resource | Connector | +|---|---|---:| +| RF clock | A12 / `GPIOR_95_CLK12` | J13.10 | +| data 0 | G16 / `GPIOR_89` | J13.2 | +| data 1 | F16 / `GPIOR_90` | J13.3 | +| data 2 | F17 / `GPIOR_91` | J13.4 | +| data 3 | G17 / `GPIOR_92` | J13.7 | + +Set the tool location and build: + +```bash +export EFINITY_HOME=/tools/efinix/efinity/2026.1 +make efinix-build +``` + +The GUI-independent Efinity CLI flow generates temporary project/periphery +files, runs Interface Designer DRC, synthesis, P&R, timing/CDC checks and writes: + +```text +targets/efinix_ti375_c529/build/outflow/ti375_max2771_recorder.bit +targets/efinix_ti375_c529/build/outflow/ti375_max2771_recorder.hex +``` + +The direct RF GCLK is constrained to 30.547 ns by default. A slower source, +including the hardware-tested ~16.368 MHz mode, works without rebuilding. For +a genuinely different maximum input frequency, rebuild with its period: + +```bash +RF_CLOCK_PERIOD_NS=61.094 make efinix-build # example 16.368 MHz constraint +``` + +Keeping the default 30.547 ns constraint while operating at 16.368 MHz is +conservative and recommended if later operation may return to 32.736 MHz. + +Program the volatile JTAG configuration: + +```bash +export EFINITY_HOME=/tools/efinix/efinity/2026.1 +make efinix-program +``` + +The connected board was verified as JTAG ID `0x006a0a79`. Programming does not +write SPI boot flash. + +### Efinix Ethernet hardware self-test + +To test PHY/MDIO/RGMII/MAC/protocol without a MAX2771 clock: + +```bash +export EFINITY_HOME=/tools/efinix/efinity/2026.1 +make efinix-selftest-build +make efinix-selftest-program +``` + +This sends the repeating nibble sequence `0,1,...,15`. Capture it with the host +program and check that the file repeats `01 23 45 67 89 ab cd ef`. Restore the +production recorder afterwards: + +```bash +make efinix-program +``` + +The real-board self-test received 35,446 consecutive frames and 104,069,456 +samples in 3.034 seconds with zero frame gap, sample gap, FPGA drop, socket +drop or pattern mismatch. + +## Linux receiver build + +```bash +make -C host clean all +make -C host test +``` + +The compilation command is equivalent to: + +```bash +gcc -O3 -std=c11 -Wall -Wextra -Wpedantic \ + -o host/max2771_raw_rx host/max2771_raw_rx.c +``` + +The receiver first requests `TPACKET_V3`/`PACKET_MMAP` and falls back to a +polling `recvfrom()` path when unavailable. Both paths validate source/filter, +EtherType, magic, version, header size, payload length, packet sequence, sample +index and FPGA-reported drop count. + +Grant capability once to run without full root: + +```bash +sudo setcap cap_net_raw,cap_net_admin=eip host/max2771_raw_rx +getcap host/max2771_raw_rx +``` + +Rebuilding the executable removes file capabilities; repeat `setcap` after a +clean rebuild. Alternatively run the receiver with `sudo`. + +## Permanent Linux interface setup for raw Layer-2 + +No IP address is needed. NetworkManager's default DHCP profile may display an +“Activation failed” popup because an FPGA does not run DHCP. Replace the profile +for the dedicated NIC, shown here as `enp3s0`, with a persistent Layer-2-only +profile: + +```bash +nmcli connection show +nmcli connection modify 'Wired connection 1' \ + connection.id 'FPGA Raw Ethernet' \ + connection.interface-name enp3s0 \ + connection.autoconnect yes \ + connection.autoconnect-retries 0 \ + ipv4.method disabled ipv4.never-default yes ipv4.ignore-auto-dns yes \ + ipv6.method disabled ipv6.never-default yes ipv6.ignore-auto-dns yes \ + connection.mdns no connection.llmnr no +nmcli connection up 'FPGA Raw Ethernet' ifname enp3s0 +``` + +Verify the direct cable: + +```bash +ip -br link show enp3s0 +ethtool enp3s0 +``` + +It must report `1000Mb/s`, full duplex and link detected. The FPGA deliberately +does not transmit at 100 Mb/s because 32.736 Msps × 4 bits already exceeds a +100 Mb/s link before Ethernet overhead. + +Optional performance setup: + +```bash +sudo ethtool -K enp3s0 gro off lro off +sudo ethtool --set-eee enp3s0 eee off +sudo sysctl -w net.core.rmem_max=268435456 +``` + +Not every NIC supports every offload/EEE switch; `Operation not supported` is +not fatal. Firewall changes are normally irrelevant because these frames are +below IP. Prefer a direct cable or isolated switch because all FPGA frames are +broadcast. + +## Starting a capture + +Normal unlimited capture with zero-fill gap preservation: + +```bash +sudo host/max2771_raw_rx \ + --interface enp3s0 \ + --output max2771_capture.bin \ + --source-mac 02:27:71:00:00:01 \ + --gap-mode zero-fill +``` + +Ten-second diagnostic capture: + +```bash +sudo host/max2771_raw_rx \ + --interface enp3s0 \ + --output capture_10s.bin \ + --log capture_10s.log \ + --source-mac 02:27:71:00:00:01 \ + --gap-mode stop \ + --duration 10 \ + --verbose +``` + +Stop automatically at one GiB: + +```bash +sudo host/max2771_raw_rx \ + --interface enp3s0 \ + --output capture_1GiB.bin \ + --max-bytes 1073741824 \ + --gap-mode zero-fill \ + --quiet +``` + +`SIGINT`/Ctrl-C and `SIGTERM` stop cleanly, flush the buffered output and write +the sidecar log. + +### Receiver options + +| Option | Meaning | +|---|---| +| `--interface IFACE` | required Linux interface | +| `--output FILE` | packed sample bytes; default `max2771_capture.bin` | +| `--log FILE` | text statistics log; default `OUTPUT.log` | +| `--source-mac MAC` | accept only the specified FPGA source | +| `--gap-mode stop` | terminate on the first discontinuity | +| `--gap-mode skip` | report gaps and concatenate received data | +| `--gap-mode zero-fill` | insert zero nibbles to preserve sample time; default | +| `--duration SECONDS` | wall-clock capture limit | +| `--max-bytes BYTES` | output-size limit | +| `--quiet` | final log only | +| `--verbose` | print every accepted frame | + +Avoid `--verbose` during long high-rate captures because terminal output itself +can become a performance bottleneck. + +## Output files + +The `.bin` file contains only packed samples. It contains no Ethernet header, +MX27 header, preamble, SFD or FCS: + +```text +byte 0 [7:4] = sample 0 +byte 0 [3:0] = sample 1 +byte 1 [7:4] = sample 2 +byte 1 [3:0] = sample 3 +... +``` + +To unpack in Python: + +```python +data = open("max2771_capture.bin", "rb").read() +samples = [n for b in data for n in (b >> 4, b & 0x0f)] +``` + +The text log records start time, interface, observed source MAC, protocol +version, first/last sample indices, frame gaps, FPGA drops, host-observed sample +gaps, bytes/samples written, elapsed time, average rate and packet-socket drops. + +## Throughput and FIFO behavior + +At the maximum requested rate: + +```text +32.736 Msps × 4 bits = 130.944 Mb/s raw +32.736 Msps / 2936 = 11,149.864 frames/s +Ethernet wire consumption ≈ 137.188 Mb/s = 13.719% of 1 Gb/s +sample-file write rate = 16.368 MB/s ≈ 58.9 GB/hour +``` + +At the tested ~16.368 MHz RF clock, raw input is approximately 65.472 Mb/s and +the sample file grows at about 8.184 MB/s. The packet format and RTL do not +change with RF clock frequency; the host derives continuity from sample index, +not from a hard-coded rate. + +The packetizer starts only with at least 2936 FIFO entries. Its conservative +three-cycle read handshake plus Ethernet transmission sustains about 35.47 +Msps, providing margin at 32.736 Msps. The 16,384-entry FIFO cannot cover +multi-second PHY auto-negotiation. Capture therefore may report initial FPGA +drops if the MAX2771 clock starts before link-ready. Start RF after Ethernet +link if a loss-free sample-zero boundary is required. + +## LED meanings + +Genesys 2 uses eight LEDs; see its target README for the complete table. Efinix +has five verified LEDs: + +| Efinix LED | Meaning | +|---|---| +| LED9 | Ethernet PLL/system ready | +| LED10 | qualified 1000/full link ready | +| LED11 | RF sample activity | +| LED12 | FIFO/frame activity | +| LED13 | overflow/sample loss latched | + +## Timing and hardware sign-off + +Both checked builds close internal setup and hold timing and report no +unexpected CDC warnings. A clean Vivado 2026.1 Genesys 2 build on 1 August 2026 +reports +0.212 ns worst setup slack and +0.090 ns worst hold slack. The clean +Efinity 2026.1 A12 production build reports +0.636 ns Ethernet setup, +0.094 ns +Ethernet hold, +25.614 ns RF setup and +0.031 ns RF hold at the nominal +32.736 MHz constraint; its CDC report says `No Synchronizer warnings to +report`. + +These numbers do not fabricate MAX2771 external timing. Final source-synchronous +input sign-off still requires the MAX2771 clock-to-data min/max values plus +cable/adapter skew. Each target's hardware/timing document identifies exactly +where measured constraints belong. + +## Troubleshooting + +- **Activation failed popup:** use the NetworkManager Layer-2 profile above; + do not wait for DHCP. +- **No link:** check cable, PHY reset, board power and `ethtool`; only + 1000/full is accepted. +- **Link but no frames:** confirm RF clock/data are present and the link-ready + LED is on. Use the Efinix internal self-test to isolate RF input from Ethernet. +- **Overflow LED:** capture started before link or transmission stopped. The + drop counter and sample indices quantify the discontinuity. +- **Socket drops:** enlarge receive buffers, disable unsupported offloads where + possible, use a dedicated NIC and verify storage speed. +- **Only a subset of nibble values:** RTL records pins literally. Check MAX2771 + output-mode configuration, signal wiring and logic levels before changing bit + interpretation. +- **Timing failure after a change:** inspect the target timing/CDC reports; do + not add broad false paths. +- **Efinity programmer finds no target:** verify `lsusb -d 0403:6011`, board + power and the programming USB cable. + +## Generated files and version control + +Build products, tool work directories, reports, FPGA images, the compiled host +binary and capture `.bin` files are ignored by Git. A fresh checkout recreates +them with the commands in this README. Source-controlled files contain RTL, +constraints, testbenches, scripts, C source and documentation only. diff --git a/constraints/genesys2_max2771_recorder.xdc b/constraints/genesys2_max2771_recorder.xdc new file mode 100644 index 0000000..d059176 --- /dev/null +++ b/constraints/genesys2_max2771_recorder.xdc @@ -0,0 +1,4 @@ +# Compatibility entry point. The authoritative, single-copy constraint file is +# targets/genesys2/constraints/genesys2_max2771_recorder.xdc. +set compatibility_dir [file normalize [file dirname [info script]]] +source [file normalize [file join $compatibility_dir .. targets genesys2 constraints genesys2_max2771_recorder.xdc]] diff --git a/docs/hardware_assumptions.md b/docs/hardware_assumptions.md new file mode 100644 index 0000000..0c63f74 --- /dev/null +++ b/docs/hardware_assumptions.md @@ -0,0 +1,26 @@ +# Hardware assumptions index + +Board-specific facts and assumptions are kept beside the target that uses them: + +- [Genesys 2 hardware assumptions](../targets/genesys2/docs/hardware_assumptions.md) +- [Efinix Ti375 C529 hardware assumptions](../targets/efinix_ti375_c529/docs/hardware_assumptions.md) + +The following items are shared assumptions from the recorder specification: + +- the MAX2771 produces one literal 4-bit word on every selected RF clock edge; +- nominal maximum RF clock frequency is 32.736 MHz; +- the default capture edge is rising and the default packing puts the earlier + sample in the high nibble; +- the source cannot be backpressured. + +The following are deliberately **not** claimed as verified: + +- MAX2771 clock-to-data minimum and maximum delays at the connector; +- RF cable/adapter clock-to-data skew and signal integrity; +- a loss-free sample-zero boundary when RF capture starts before Gigabit link; +- timing behavior outside the FPGA speed/temperature model selected by each + target build. + +Obtain the MAX2771 timing values from the exact device/mode datasheet and +measure the assembled clock/data path before final board-level timing sign-off. +No arbitrary `set_input_delay` is presented as a measured value. diff --git a/docs/raw_ethernet_protocol.md b/docs/raw_ethernet_protocol.md new file mode 100644 index 0000000..d0b685c --- /dev/null +++ b/docs/raw_ethernet_protocol.md @@ -0,0 +1,65 @@ +# MAX2771 raw Ethernet protocol + +This protocol is Ethernet Layer 2 only. It has no IP, ARP, DHCP, ICMP, UDP, or TCP component. + +## Ethernet envelope + +| Item | Value | +|---|---| +| Preamble | seven bytes `55` | +| SFD | `D5` | +| Destination | `FF:FF:FF:FF:FF:FF` | +| Source | `02:27:71:00:00:01` | +| EtherType | `0x88B5`, most-significant byte first | +| Maximum Ethernet payload | 1500 bytes | +| VLAN / jumbo frames | not used | +| FCS | reflected Ethernet CRC-32, inverted, least-significant byte first | +| IFG | at least 12 byte times | + +The CRC is initialized to `0xFFFFFFFF`, updated LSB-first over destination MAC through the final payload or pad byte with reflected polynomial `0xEDB88320`, then inverted. Preamble and SFD are not part of the CRC. Linux NICs normally remove preamble, SFD, and FCS before an `AF_PACKET` application receives the frame. + +## Custom payload header + +All multibyte header fields use network byte order. The fixed header is exactly 32 bytes. + +| Offset | Size | Field | Encoding | +|---:|---:|---|---| +| 0 | 4 | magic | ASCII `MX27` (`4D 58 32 37`) | +| 4 | 1 | version | `1` | +| 5 | 1 | flags | described below | +| 6 | 2 | header length | `0x0020` | +| 8 | 4 | data-frame sequence | unsigned, wraps modulo 2^32 | +| 12 | 8 | first sample index | unsigned absolute RF sample index | +| 20 | 2 | sample count | number of real 4-bit samples in this frame | +| 22 | 1 | bits per sample | `4` | +| 23 | 1 | packing format | `1` for earlier sample in high nibble | +| 24 | 4 | dropped sample count | cumulative FPGA count, low 32 bits | +| 28 | 4 | reserved | zero | + +Flags are: + +| Bit | Meaning | +|---:|---| +| 0 | FIFO overflow/input loss occurred since the previous reported packet | +| 1 | first data packet after FPGA reset | +| 2 | first data packet after the PHY reports a resolved 1000BASE-T full-duplex link | +| 7:3 | zero | + +Normal frames contain a 32-byte custom header and 1468 data bytes: exactly 2936 samples. Sequence increments once per transmitted data frame. A sample with index `N` followed by `N+1` is contiguous regardless of nibble values. + +## Packing + +For samples `S0, S1, S2, S3`, each limited to four bits, data bytes are: + +```text +byte 0 = {S0[3:0], S1[3:0]} +byte 1 = {S2[3:0], S3[3:0]} +``` + +There is no sign extension, I/Q interpretation, bit reversal, or numeric conversion. An exceptional overflow boundary can close a short frame. If its sample count is odd, the unused low nibble of the final frame byte is zero and is not a sample. The host parser uses `sample_count`, so it never writes that pad nibble. + +## Loss semantics + +Each FIFO entry carries its absolute 64-bit input sample index. The packetizer compares adjacent entries. If a full-FIFO interval creates an index discontinuity, it closes the pre-gap packet as an exceptional partial frame and begins the next packet at the first post-gap index. This makes the missing interval visible to the host. It never silently crosses a gap inside a packet. + +Packet-sequence gaps indicate frames lost after the FPGA MAC. Sample-index gaps indicate missing sample time, whether due to an FPGA overflow or a lost Ethernet frame. The cumulative drop field distinguishes FPGA-side input loss. The receiver extends the low-32-bit drop count across wraps while it is running. diff --git a/docs/timing_constraints.md b/docs/timing_constraints.md new file mode 100644 index 0000000..b46d4b9 --- /dev/null +++ b/docs/timing_constraints.md @@ -0,0 +1,24 @@ +# Timing constraints index + +The authoritative target timing notes are: + +- [Genesys 2 timing constraints](../targets/genesys2/docs/timing_constraints.md) +- [Efinix timing/build notes](../targets/efinix_ti375_c529/docs/efinity_flow.md) + +Both implementations constrain the RF clock to 30.547 ns by default and the +Ethernet GMII domain to 8.000 ns. Their generated 125 MHz RGMII transmit clock +phase and board-specific I/O timing are created in the corresponding target +clock/periphery implementation. + +Only known asynchronous relationships and synchronizer/Gray-pointer CDC paths +are treated as asynchronous. The design does not apply a broad false path from +RF capture to Ethernet logic; the handwritten asynchronous FIFO is the explicit +boundary. Unknown MAX2771 external input delays remain documented measurement +requirements rather than invented constraints. + +Clean builds checked on 1 August 2026 reported: + +| Target | Setup slack | Hold slack | CDC | +|---|---:|---:|---| +| Genesys 2, Vivado 2026.1 | +0.212 ns worst | +0.090 ns worst | no unexpected major warning accepted by build script | +| Ti375 C529, Efinity 2026.1 | +0.636 ns Ethernet, +25.614 ns RF | +0.094 ns Ethernet, +0.031 ns RF | no synchronizer warnings | diff --git a/host/Makefile b/host/Makefile new file mode 100644 index 0000000..db19a0b --- /dev/null +++ b/host/Makefile @@ -0,0 +1,15 @@ +CC ?= gcc +CFLAGS ?= -O3 -std=c11 -Wall -Wextra -Wpedantic +TARGET := max2771_raw_rx + +.PHONY: all clean test +all: $(TARGET) + +$(TARGET): max2771_raw_rx.c + $(CC) $(CFLAGS) -o $@ $< + +test: $(TARGET) + ./$(TARGET) --self-test + +clean: + $(RM) $(TARGET) diff --git a/host/max2771_raw_rx.c b/host/max2771_raw_rx.c new file mode 100644 index 0000000..2ea1cbc --- /dev/null +++ b/host/max2771_raw_rx.c @@ -0,0 +1,571 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MX27_ETHERTYPE 0x88b5u +#define MX27_HEADER_LEN 32u +#define OUTPUT_BUFFER_SIZE (8u * 1024u * 1024u) +#define RING_BLOCK_SIZE (2u * 1024u * 1024u) +#define RING_BLOCK_COUNT 64u +#define RING_FRAME_SIZE 2048u + +enum gap_mode { GAP_STOP, GAP_SKIP, GAP_ZERO_FILL }; + +struct options { + const char *interface_name; + const char *output_path; + const char *log_path; + bool source_filter; + uint8_t source_mac[6]; + enum gap_mode gap_mode; + double duration; + uint64_t max_bytes; + bool quiet; + bool verbose; + bool self_test; +}; + +struct sample_writer { + FILE *file; + bool high_pending; + uint8_t pending_byte; + uint64_t samples_written; + uint64_t bytes_written; + uint64_t sample_limit; +}; + +struct stats { + uint64_t received_frames; + uint64_t invalid_frames; + uint64_t missing_frames; + uint64_t out_of_order_frames; + uint64_t host_missing_samples; + uint64_t fpga_dropped_samples; + uint64_t first_sample_index; + uint64_t last_sample_index; + bool have_sample_index; + uint32_t expected_sequence; + bool have_sequence; + uint64_t expected_sample_index; + bool have_expected_sample; + uint32_t last_drop_low; + bool have_drop_count; + uint8_t protocol_version; + uint8_t observed_source[6]; + bool have_source; + uint64_t socket_packets; + uint64_t socket_drops; + uint64_t freeze_count; +}; + +struct receiver_context { + struct options *opt; + struct sample_writer writer; + struct stats stats; +}; + +static volatile sig_atomic_t stop_requested; + +static void signal_handler(int signal_number) { + (void)signal_number; + stop_requested = 1; +} + +static uint16_t read_be16(const uint8_t *p) { + return (uint16_t)(((uint16_t)p[0] << 8) | (uint16_t)p[1]); +} + +static uint32_t read_be32(const uint8_t *p) { + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | (uint32_t)p[3]; +} + +static uint64_t read_be64(const uint8_t *p) { + return ((uint64_t)read_be32(p) << 32) | (uint64_t)read_be32(p + 4); +} + +static double monotonic_seconds(void) { + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { + perror("clock_gettime"); + exit(EXIT_FAILURE); + } + return (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0; +} + +static int parse_mac(const char *text, uint8_t mac[6]) { + unsigned int v[6]; + char extra; + if (sscanf(text, "%x:%x:%x:%x:%x:%x%c", &v[0], &v[1], &v[2], + &v[3], &v[4], &v[5], &extra) != 6) return -1; + for (size_t i = 0; i < 6; ++i) { + if (v[i] > 255u) return -1; + mac[i] = (uint8_t)v[i]; + } + return 0; +} + +static void format_mac(char out[18], const uint8_t mac[6]) { + (void)snprintf(out, 18, "%02x:%02x:%02x:%02x:%02x:%02x", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); +} + +static int write_bytes(struct sample_writer *w, const uint8_t *data, size_t length) { + if (length == 0u) return 0; + if (fwrite(data, 1u, length, w->file) != length) { + fprintf(stderr, "output write failed: %s\n", strerror(errno)); + return -1; + } + w->bytes_written += (uint64_t)length; + return 0; +} + +static bool writer_at_limit(const struct sample_writer *w) { + return w->sample_limit != 0u && w->samples_written >= w->sample_limit; +} + +static int write_nibble(struct sample_writer *w, uint8_t value) { + if (writer_at_limit(w)) return 1; + value &= 0x0fu; + if (!w->high_pending) { + w->pending_byte = (uint8_t)(value << 4); + w->high_pending = true; + } else { + uint8_t byte = (uint8_t)(w->pending_byte | value); + if (write_bytes(w, &byte, 1u) != 0) return -1; + w->high_pending = false; + } + ++w->samples_written; + return writer_at_limit(w) ? 1 : 0; +} + +static int write_zero_samples(struct sample_writer *w, uint64_t count) { + static const uint8_t zeros[65536] = {0}; + if (w->sample_limit != 0u && count > w->sample_limit - w->samples_written) + count = w->sample_limit - w->samples_written; + if (w->high_pending && count != 0u) { + int rc = write_nibble(w, 0u); + --count; + if (rc != 0) return rc; + } + while (count >= 2u) { + uint64_t byte_count = count / 2u; + size_t chunk = byte_count > sizeof(zeros) ? sizeof(zeros) : (size_t)byte_count; + if (write_bytes(w, zeros, chunk) != 0) return -1; + w->samples_written += (uint64_t)chunk * 2u; + count -= (uint64_t)chunk * 2u; + } + if (count != 0u) return write_nibble(w, 0u); + return writer_at_limit(w) ? 1 : 0; +} + +static int write_packed_samples(struct sample_writer *w, const uint8_t *packed, + uint32_t first_sample, uint32_t count) { + if (w->sample_limit != 0u) { + uint64_t remaining = w->sample_limit - w->samples_written; + if ((uint64_t)count > remaining) count = (uint32_t)remaining; + } + if (!w->high_pending && (first_sample & 1u) == 0u && (count & 1u) == 0u) { + if (write_bytes(w, packed + first_sample / 2u, count / 2u) != 0) return -1; + w->samples_written += count; + return writer_at_limit(w) ? 1 : 0; + } + for (uint32_t i = 0; i < count; ++i) { + uint32_t sample = first_sample + i; + uint8_t byte = packed[sample / 2u]; + uint8_t nibble = (sample & 1u) == 0u ? (uint8_t)(byte >> 4) : (uint8_t)(byte & 0x0fu); + int rc = write_nibble(w, nibble); + if (rc != 0) return rc; + } + return 0; +} + +static int finish_writer(struct sample_writer *w) { + if (w->high_pending) { + if (write_bytes(w, &w->pending_byte, 1u) != 0) return -1; + w->high_pending = false; + } + return fflush(w->file) == 0 ? 0 : -1; +} + +/* Return 0 to continue, 1 for requested clean stop, and -1 on fatal I/O error. */ +static int process_frame(struct receiver_context *ctx, const uint8_t *frame, size_t length) { + struct options *opt = ctx->opt; + struct stats *s = &ctx->stats; + if (length < ETH_HLEN + MX27_HEADER_LEN) return 0; + if (read_be16(frame + 12) != MX27_ETHERTYPE) return 0; + const uint8_t *source = frame + 6; + if (opt->source_filter && memcmp(source, opt->source_mac, 6) != 0) return 0; + + const uint8_t *h = frame + ETH_HLEN; + if (memcmp(h, "MX27", 4) != 0 || h[4] != 1u || read_be16(h + 6) != MX27_HEADER_LEN) { + ++s->invalid_frames; + return 0; + } + uint16_t sample_count = read_be16(h + 20); + size_t sample_bytes = ((size_t)sample_count + 1u) / 2u; + if (h[22] != 4u || h[23] != 1u || sample_count == 0u || + MX27_HEADER_LEN + sample_bytes > 1500u || + length < ETH_HLEN + MX27_HEADER_LEN + sample_bytes) { + ++s->invalid_frames; + return 0; + } + + uint8_t flags = h[5]; + uint32_t sequence = read_be32(h + 8); + uint64_t first_index = read_be64(h + 12); + uint32_t drop_low = read_be32(h + 24); + bool discontinuity = (flags & 1u) != 0u; + uint64_t positive_gap = 0u; + uint32_t skip_leading = 0u; + + if (!s->have_source) { + memcpy(s->observed_source, source, 6); + s->have_source = true; + s->protocol_version = h[4]; + } + + if (s->have_sequence && sequence != s->expected_sequence) { + uint32_t delta = sequence - s->expected_sequence; + discontinuity = true; + if (delta < 0x80000000u) s->missing_frames += delta; + else ++s->out_of_order_frames; + if (!opt->quiet) + fprintf(stderr, "packet sequence discontinuity: expected %u got %u\n", + s->expected_sequence, sequence); + } + s->expected_sequence = sequence + 1u; + s->have_sequence = true; + + if (s->have_expected_sample && first_index != s->expected_sample_index) { + discontinuity = true; + if (first_index > s->expected_sample_index) { + positive_gap = first_index - s->expected_sample_index; + s->host_missing_samples += positive_gap; + } else { + uint64_t overlap = s->expected_sample_index - first_index; + skip_leading = overlap >= sample_count ? sample_count : (uint32_t)overlap; + } + if (!opt->quiet) + fprintf(stderr, "sample-index discontinuity: expected %llu got %llu\n", + (unsigned long long)s->expected_sample_index, + (unsigned long long)first_index); + } + + if (!s->have_drop_count) { + s->fpga_dropped_samples = drop_low; + s->have_drop_count = true; + } else { + uint32_t delta = drop_low - s->last_drop_low; + if (delta != 0u) discontinuity = true; + s->fpga_dropped_samples += delta; + } + s->last_drop_low = drop_low; + + if (discontinuity && opt->gap_mode == GAP_STOP) { + if (!opt->quiet) fprintf(stderr, "stopping on discontinuity (--gap-mode stop)\n"); + return 1; + } + if (positive_gap != 0u && opt->gap_mode == GAP_ZERO_FILL) { + int rc = write_zero_samples(&ctx->writer, positive_gap); + if (rc != 0) return rc; + } + + uint32_t samples_to_write = (uint32_t)sample_count - skip_leading; + int rc = write_packed_samples(&ctx->writer, h + MX27_HEADER_LEN, + skip_leading, samples_to_write); + if (rc < 0) return rc; + + ++s->received_frames; + if (!s->have_sample_index) { + s->first_sample_index = first_index; + s->have_sample_index = true; + } + s->last_sample_index = first_index + (uint64_t)sample_count - 1u; + s->expected_sample_index = first_index + (uint64_t)sample_count; + s->have_expected_sample = true; + if (opt->verbose) + fprintf(stderr, "frame=%u first=%llu samples=%u flags=0x%02x dropped=%u\n", + sequence, (unsigned long long)first_index, sample_count, flags, drop_low); + return rc; +} + +static void usage(FILE *stream, const char *program) { + fprintf(stream, + "Usage: %s --interface IFACE [options]\n" + " --output FILE packed sample output (default max2771_capture.bin)\n" + " --log FILE text log (default OUTPUT.log)\n" + " --source-mac MAC accept only this source MAC\n" + " --gap-mode MODE stop, skip, or zero-fill (default zero-fill)\n" + " --duration SECONDS stop after elapsed wall time\n" + " --max-bytes BYTES stop at this many output bytes\n" + " --quiet | --verbose select diagnostic detail\n" + " --help show this help\n", program); +} + +static int parse_options(int argc, char **argv, struct options *opt) { + static const struct option long_options[] = { + {"interface", required_argument, NULL, 'i'}, {"output", required_argument, NULL, 'o'}, + {"log", required_argument, NULL, 'l'}, {"source-mac", required_argument, NULL, 's'}, + {"gap-mode", required_argument, NULL, 'g'}, {"duration", required_argument, NULL, 'd'}, + {"max-bytes", required_argument, NULL, 'm'}, {"quiet", no_argument, NULL, 'q'}, + {"verbose", no_argument, NULL, 'v'}, {"self-test", no_argument, NULL, 't'}, + {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, 0} + }; + memset(opt, 0, sizeof(*opt)); + opt->output_path = "max2771_capture.bin"; + opt->gap_mode = GAP_ZERO_FILL; + int c; + while ((c = getopt_long(argc, argv, "i:o:l:s:g:d:m:qvth", long_options, NULL)) != -1) { + char *end = NULL; + switch (c) { + case 'i': opt->interface_name = optarg; break; + case 'o': opt->output_path = optarg; break; + case 'l': opt->log_path = optarg; break; + case 's': + if (parse_mac(optarg, opt->source_mac) != 0) { fprintf(stderr,"invalid MAC: %s\n",optarg); return -1; } + opt->source_filter = true; break; + case 'g': + if (strcmp(optarg,"stop")==0) opt->gap_mode=GAP_STOP; + else if (strcmp(optarg,"skip")==0) opt->gap_mode=GAP_SKIP; + else if (strcmp(optarg,"zero-fill")==0) opt->gap_mode=GAP_ZERO_FILL; + else { fprintf(stderr,"invalid gap mode: %s\n",optarg); return -1; } + break; + case 'd': + errno=0; opt->duration=strtod(optarg,&end); + if(errno!=0 || end==optarg || *end!='\0' || opt->duration<=0.0) return -1; + break; + case 'm': + errno=0; opt->max_bytes=strtoull(optarg,&end,0); + if(errno!=0 || end==optarg || *end!='\0' || opt->max_bytes==0u) return -1; + break; + case 'q': opt->quiet=true; opt->verbose=false; break; + case 'v': opt->verbose=true; opt->quiet=false; break; + case 't': opt->self_test=true; break; + case 'h': usage(stdout,argv[0]); exit(EXIT_SUCCESS); + default: return -1; + } + } + if (!opt->self_test && opt->interface_name == NULL) return -1; + return 0; +} + +static int open_bound_socket(const char *interface_name) { + unsigned int ifindex = if_nametoindex(interface_name); + if (ifindex == 0u) { fprintf(stderr,"unknown interface %s: %s\n",interface_name,strerror(errno)); return -1; } + int fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); + if (fd < 0) { perror("socket(AF_PACKET)"); return -1; } + int rcvbuf = 256 * 1024 * 1024; + if (setsockopt(fd,SOL_SOCKET,SO_RCVBUF,&rcvbuf,sizeof(rcvbuf)) != 0) + fprintf(stderr,"warning: SO_RCVBUF request failed: %s\n",strerror(errno)); + struct sockaddr_ll address; + memset(&address,0,sizeof(address)); + address.sll_family=AF_PACKET; + address.sll_protocol=htons(ETH_P_ALL); + address.sll_ifindex=(int)ifindex; + if (bind(fd,(struct sockaddr *)&address,sizeof(address)) != 0) { + fprintf(stderr,"bind(%s): %s\n",interface_name,strerror(errno)); close(fd); return -1; + } + return fd; +} + +static int receive_mmap(int fd, struct receiver_context *ctx, double start_time) { + int version=TPACKET_V3; + if(setsockopt(fd,SOL_PACKET,PACKET_VERSION,&version,sizeof(version))!=0) return -2; + struct tpacket_req3 req; + memset(&req,0,sizeof(req)); + req.tp_block_size=RING_BLOCK_SIZE; + req.tp_frame_size=RING_FRAME_SIZE; + req.tp_block_nr=RING_BLOCK_COUNT; + req.tp_frame_nr=(RING_BLOCK_SIZE/RING_FRAME_SIZE)*RING_BLOCK_COUNT; + req.tp_retire_blk_tov=64u; + if(setsockopt(fd,SOL_PACKET,PACKET_RX_RING,&req,sizeof(req))!=0) return -2; + size_t ring_size=(size_t)req.tp_block_size*req.tp_block_nr; + uint8_t *ring=mmap(NULL,ring_size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); + if(ring==MAP_FAILED) { perror("mmap PACKET_RX_RING"); return -1; } + struct pollfd pfd={.fd=fd,.events=POLLIN,.revents=0}; + unsigned int block=0u; + int result=0; + while(!stop_requested) { + if(ctx->opt->duration>0.0 && monotonic_seconds()-start_time>=ctx->opt->duration) break; + struct tpacket_block_desc *bd=(struct tpacket_block_desc *)(ring+(size_t)block*req.tp_block_size); + if((bd->hdr.bh1.block_status&TP_STATUS_USER)==0u) { + int pr=poll(&pfd,1,250); + if(pr<0 && errno!=EINTR) { perror("poll"); result=-1; break; } + continue; + } + struct tpacket3_hdr *packet=(struct tpacket3_hdr *)((uint8_t *)bd+bd->hdr.bh1.offset_to_first_pkt); + for(uint32_t n=0;nhdr.bh1.num_pkts;n++) { + const uint8_t *data=(const uint8_t *)packet+packet->tp_mac; + int rc=process_frame(ctx,data,packet->tp_snaplen); + if(rc!=0) { result=rc<0?-1:0; stop_requested=1; break; } + packet=(struct tpacket3_hdr *)((uint8_t *)packet+packet->tp_next_offset); + } + __sync_synchronize(); + bd->hdr.bh1.block_status=TP_STATUS_KERNEL; + block=(block+1u)%req.tp_block_nr; + } + if(munmap(ring,ring_size)!=0) perror("munmap"); + struct tpacket_req3 disable; + memset(&disable,0,sizeof(disable)); + if(setsockopt(fd,SOL_PACKET,PACKET_RX_RING,&disable,sizeof(disable))!=0) + fprintf(stderr,"warning: disabling RX ring failed: %s\n",strerror(errno)); + return result; +} + +static int receive_recvfrom(int fd, struct receiver_context *ctx, double start_time) { + uint8_t buffer[65536]; + struct pollfd pfd={.fd=fd,.events=POLLIN,.revents=0}; + while(!stop_requested) { + if(ctx->opt->duration>0.0 && monotonic_seconds()-start_time>=ctx->opt->duration) break; + int pr=poll(&pfd,1,250); + if(pr<0) { if(errno==EINTR) continue; perror("poll"); return -1; } + if(pr==0) continue; + ssize_t length=recvfrom(fd,buffer,sizeof(buffer),0,NULL,NULL); + if(length<0) { if(errno==EINTR) continue; perror("recvfrom"); return -1; } + int rc=process_frame(ctx,buffer,(size_t)length); + if(rc!=0) return rc<0?-1:0; + } + return 0; +} + +static void collect_socket_stats(int fd, struct stats *s) { + struct tpacket_stats_v3 ps; + socklen_t len=sizeof(ps); + memset(&ps,0,sizeof(ps)); + if(getsockopt(fd,SOL_PACKET,PACKET_STATISTICS,&ps,&len)==0) { + s->socket_packets=ps.tp_packets; + s->socket_drops=ps.tp_drops; + s->freeze_count=ps.tp_freeze_q_cnt; + } +} + +static int write_log(const char *path, const struct receiver_context *ctx, + const char *start_text, double elapsed) { + FILE *log=fopen(path,"w"); + if(log==NULL) { fprintf(stderr,"open log %s: %s\n",path,strerror(errno)); return -1; } + char mac[18]="unknown"; + if(ctx->stats.have_source) format_mac(mac,ctx->stats.observed_source); + const char *mode=ctx->opt->gap_mode==GAP_STOP?"stop":ctx->opt->gap_mode==GAP_SKIP?"skip":"zero-fill"; + fprintf(log,"start_time: %s\ninterface: %s\nsource_mac: %s\nprotocol_version: %u\n", + start_text,ctx->opt->interface_name?ctx->opt->interface_name:"self-test",mac,ctx->stats.protocol_version); + fprintf(log,"gap_mode: %s\nfirst_sample_index: %llu\nlast_sample_index: %llu\n", + mode,(unsigned long long)ctx->stats.first_sample_index,(unsigned long long)ctx->stats.last_sample_index); + fprintf(log,"received_frames: %llu\nmissing_frames: %llu\nout_of_order_frames: %llu\n", + (unsigned long long)ctx->stats.received_frames,(unsigned long long)ctx->stats.missing_frames, + (unsigned long long)ctx->stats.out_of_order_frames); + fprintf(log,"fpga_dropped_samples: %llu\nhost_observed_missing_samples: %llu\ninvalid_frames: %llu\n", + (unsigned long long)ctx->stats.fpga_dropped_samples, + (unsigned long long)ctx->stats.host_missing_samples, + (unsigned long long)ctx->stats.invalid_frames); + fprintf(log,"bytes_written: %llu\nsamples_written: %llu\nelapsed_seconds: %.6f\naverage_sample_rate: %.3f\n", + (unsigned long long)ctx->writer.bytes_written,(unsigned long long)ctx->writer.samples_written, + elapsed,elapsed>0.0?(double)ctx->writer.samples_written/elapsed:0.0); + fprintf(log,"socket_packets: %llu\nsocket_drops: %llu\nsocket_freeze_count: %llu\n", + (unsigned long long)ctx->stats.socket_packets,(unsigned long long)ctx->stats.socket_drops, + (unsigned long long)ctx->stats.freeze_count); + if(fclose(log)!=0) { perror("close log"); return -1; } + return 0; +} + +static void put_be32(uint8_t *p,uint32_t v) { p[0]=(uint8_t)(v>>24);p[1]=(uint8_t)(v>>16);p[2]=(uint8_t)(v>>8);p[3]=(uint8_t)v; } +static void put_be64(uint8_t *p,uint64_t v) { put_be32(p,(uint32_t)(v>>32));put_be32(p+4,(uint32_t)v); } + +static int parser_self_test(struct options *opt) { + uint8_t frame[ETH_HLEN+MX27_HEADER_LEN+2u]; + memset(frame,0,sizeof(frame)); + memset(frame,0xff,6); frame[6]=0x02;frame[7]=0x27;frame[8]=0x71;frame[11]=0x01; + frame[12]=0x88;frame[13]=0xb5; + uint8_t *h=frame+ETH_HLEN; + memcpy(h,"MX27",4);h[4]=1;h[6]=0;h[7]=32;h[20]=0;h[21]=4;h[22]=4;h[23]=1; + h[32]=0x01;h[33]=0x23; + struct receiver_context ctx; + memset(&ctx,0,sizeof(ctx));ctx.opt=opt;ctx.writer.file=tmpfile(); + if(ctx.writer.file==NULL) { perror("tmpfile"); return -1; } + opt->gap_mode=GAP_SKIP;opt->quiet=true; + put_be32(h+8,10);put_be64(h+12,100); + if(process_frame(&ctx,frame,sizeof(frame))!=0) return -1; + put_be32(h+8,12);put_be64(h+12,108);h[5]=1u;put_be32(h+24,4u); + if(process_frame(&ctx,frame,sizeof(frame))!=0) return -1; + int ok=(ctx.stats.received_frames==2u && ctx.stats.missing_frames==1u && + ctx.stats.host_missing_samples==4u && ctx.stats.fpga_dropped_samples==4u && + ctx.writer.samples_written==8u); + (void)fclose(ctx.writer.file); + if(!ok) { fprintf(stderr,"parser self-test failed\n"); return -1; } + puts("PASS host parser self-test: packet and sample gaps detected"); + return 0; +} + +int main(int argc, char **argv) { + struct options opt; + if(parse_options(argc,argv,&opt)!=0) { usage(stderr,argv[0]); return EXIT_FAILURE; } + if(opt.self_test) return parser_self_test(&opt)==0?EXIT_SUCCESS:EXIT_FAILURE; + + char *default_log=NULL; + if(opt.log_path==NULL) { + size_t n=strlen(opt.output_path)+5u; + default_log=malloc(n); + if(default_log==NULL) { perror("malloc"); return EXIT_FAILURE; } + (void)snprintf(default_log,n,"%s.log",opt.output_path); + opt.log_path=default_log; + } + struct receiver_context ctx; + memset(&ctx,0,sizeof(ctx));ctx.opt=&opt; + ctx.writer.file=fopen(opt.output_path,"wb"); + if(ctx.writer.file==NULL) { fprintf(stderr,"open output %s: %s\n",opt.output_path,strerror(errno));free(default_log);return EXIT_FAILURE; } + char *output_buffer=malloc(OUTPUT_BUFFER_SIZE); + if(output_buffer==NULL || setvbuf(ctx.writer.file,output_buffer,_IOFBF,OUTPUT_BUFFER_SIZE)!=0) { + fprintf(stderr,"failed to allocate/configure output buffer\n"); + free(output_buffer);fclose(ctx.writer.file);free(default_log);return EXIT_FAILURE; + } + if(opt.max_bytes!=0u) ctx.writer.sample_limit=opt.max_bytes>UINT64_MAX/2u?UINT64_MAX:opt.max_bytes*2u; + int fd=open_bound_socket(opt.interface_name); + if(fd<0) { fclose(ctx.writer.file);free(output_buffer);free(default_log);return EXIT_FAILURE; } + + struct sigaction action; + memset(&action,0,sizeof(action));action.sa_handler=signal_handler;sigemptyset(&action.sa_mask); + if(sigaction(SIGINT,&action,NULL)!=0 || sigaction(SIGTERM,&action,NULL)!=0) { + perror("sigaction");close(fd);fclose(ctx.writer.file);free(output_buffer);free(default_log);return EXIT_FAILURE; + } + time_t wall_start=time(NULL); + struct tm tm_start; + char start_text[64]; + if(localtime_r(&wall_start,&tm_start)==NULL) strcpy(start_text,"unknown"); + else (void)strftime(start_text,sizeof(start_text),"%Y-%m-%dT%H:%M:%S%z",&tm_start); + double start=monotonic_seconds(); + if(!opt.quiet) fprintf(stderr,"capturing EtherType 0x88B5 on %s -> %s\n",opt.interface_name,opt.output_path); + int rc=receive_mmap(fd,&ctx,start); + if(rc==-2) { + if(!opt.quiet) fprintf(stderr,"TPACKET_V3 unavailable; falling back to recvfrom()\n"); + rc=receive_recvfrom(fd,&ctx,start); + } + double elapsed=monotonic_seconds()-start; + collect_socket_stats(fd,&ctx.stats); + if(finish_writer(&ctx.writer)!=0) { perror("flush output");rc=-1; } + if(fclose(ctx.writer.file)!=0) { perror("close output");rc=-1; } + close(fd); + if(write_log(opt.log_path,&ctx,start_text,elapsed)!=0) rc=-1; + if(!opt.quiet) { + fprintf(stderr,"frames=%llu missing_frames=%llu missing_samples=%llu fpga_drops=%llu bytes=%llu elapsed=%.3fs socket_drops=%llu\n", + (unsigned long long)ctx.stats.received_frames,(unsigned long long)ctx.stats.missing_frames, + (unsigned long long)ctx.stats.host_missing_samples,(unsigned long long)ctx.stats.fpga_dropped_samples, + (unsigned long long)ctx.writer.bytes_written,elapsed,(unsigned long long)ctx.stats.socket_drops); + } + free(output_buffer);free(default_log); + return rc==0?EXIT_SUCCESS:EXIT_FAILURE; +} diff --git a/rtl/common/async_fifo_gray.sv b/rtl/common/async_fifo_gray.sv new file mode 100644 index 0000000..b51c86f --- /dev/null +++ b/rtl/common/async_fifo_gray.sv @@ -0,0 +1,122 @@ +`default_nettype none + +// Handwritten dual-clock FIFO. Only Gray pointers cross clock domains. Memory +// writes occur in w_clk and the read data is registered in r_clk, allowing BRAM +// inference without a combinational CDC path. +module async_fifo_gray #( + parameter int unsigned DATA_WIDTH = 69, + parameter int unsigned DEPTH = 16384 +) ( + input wire logic w_clk, + input wire logic w_rst, + input wire logic w_en, + input wire logic [DATA_WIDTH-1:0] w_data, + output logic w_full, + output logic [$clog2(DEPTH):0] w_level, + output logic [63:0] overflow_attempt_count, + + input wire logic r_clk, + input wire logic r_rst, + input wire logic r_en, + output logic [DATA_WIDTH-1:0] r_data, + output logic r_valid, + output logic r_empty, + output logic [$clog2(DEPTH):0] r_level, + output logic [63:0] underflow_attempt_count +); + localparam int unsigned ADDR_WIDTH = $clog2(DEPTH); + localparam int unsigned PTR_WIDTH = ADDR_WIDTH + 1; + + (* ram_style = "block" *) logic [DATA_WIDTH-1:0] mem [0:DEPTH-1]; + logic [PTR_WIDTH-1:0] wbin, wgray, rbin, rgray; + logic [PTR_WIDTH-1:0] wbin_next, wgray_next; + logic [PTR_WIDTH-1:0] rbin_next, rgray_next; + (* ASYNC_REG = "TRUE" *) logic [PTR_WIDTH-1:0] rgray_wsync1, rgray_wsync2; + (* ASYNC_REG = "TRUE" *) logic [PTR_WIDTH-1:0] wgray_rsync1, wgray_rsync2; + logic [PTR_WIDTH-1:0] rbin_wsync, wbin_rsync; + logic [DATA_WIDTH-1:0] r_mem_data; + logic w_push, r_pop; + integer i; + + initial begin + if (DEPTH < 4 || (DEPTH & (DEPTH-1)) != 0) + $error("async_fifo_gray DEPTH must be a power of two >= 4"); + if (DATA_WIDTH == 0) $error("async_fifo_gray DATA_WIDTH must be nonzero"); + end + + assign w_push = w_en && !w_full; + assign r_pop = r_en && !r_empty; + assign r_data = r_valid ? r_mem_data : {DATA_WIDTH{1'b0}}; + assign wbin_next = wbin + {{(PTR_WIDTH-1){1'b0}}, w_push}; + assign rbin_next = rbin + {{(PTR_WIDTH-1){1'b0}}, r_pop}; + assign wgray_next = (wbin_next >> 1) ^ wbin_next; + assign rgray_next = (rbin_next >> 1) ^ rbin_next; + + always_comb begin + rbin_wsync[PTR_WIDTH-1] = rgray_wsync2[PTR_WIDTH-1]; + wbin_rsync[PTR_WIDTH-1] = wgray_rsync2[PTR_WIDTH-1]; + for (i = PTR_WIDTH-2; i >= 0; i = i - 1) begin + rbin_wsync[i] = rbin_wsync[i+1] ^ rgray_wsync2[i]; + wbin_rsync[i] = wbin_rsync[i+1] ^ wgray_rsync2[i]; + end + end + + always_comb begin + w_level = wbin - rbin_wsync; + r_level = wbin_rsync - rbin; + end + + always_ff @(posedge w_clk or posedge w_rst) begin + if (w_rst) begin + wbin <= '0; + wgray <= '0; + w_full <= 1'b0; + rgray_wsync1 <= '0; + rgray_wsync2 <= '0; + overflow_attempt_count <= 64'd0; + end else begin + rgray_wsync1 <= rgray; + rgray_wsync2 <= rgray_wsync1; + if (w_en && w_full) + overflow_attempt_count <= overflow_attempt_count + 64'd1; + wbin <= wbin_next; + wgray <= wgray_next; + // Invert the two most-significant synchronized read-pointer bits. + w_full <= (wgray_next == {~rgray_wsync2[PTR_WIDTH-1:PTR_WIDTH-2], + rgray_wsync2[PTR_WIDTH-3:0]}); + end + end + + always_ff @(posedge r_clk or posedge r_rst) begin + if (r_rst) begin + rbin <= '0; + rgray <= '0; + r_valid <= 1'b0; + r_empty <= 1'b1; + wgray_rsync1 <= '0; + wgray_rsync2 <= '0; + underflow_attempt_count <= 64'd0; + end else begin + wgray_rsync1 <= wgray; + wgray_rsync2 <= wgray_rsync1; + r_valid <= r_pop; + if (r_en && r_empty) + underflow_attempt_count <= underflow_attempt_count + 64'd1; + rbin <= rbin_next; + rgray <= rgray_next; + r_empty <= (rgray_next == wgray_rsync2); + end + end + + // Keep memory access out of the asynchronously-reset pointer processes. + // This is the supported true-dual-clock block-RAM inference template. + always_ff @(posedge w_clk) begin + if (w_push) mem[wbin[ADDR_WIDTH-1:0]] <= w_data; + end + + always_ff @(posedge r_clk) begin + if (r_pop) r_mem_data <= mem[rbin[ADDR_WIDTH-1:0]]; + end +endmodule + +`default_nettype wire diff --git a/rtl/common/ethernet_crc32.sv b/rtl/common/ethernet_crc32.sv new file mode 100644 index 0000000..80c84d9 --- /dev/null +++ b/rtl/common/ethernet_crc32.sv @@ -0,0 +1,23 @@ +`default_nettype none + +// One reflected Ethernet CRC-32 byte update. Initialize crc_in to 0xffffffff, +// update over destination MAC through padding, then transmit ~crc in low-byte +// first order. Polynomial x^32+x^26+x^23+...+1 is 0xedb88320 reflected. +module ethernet_crc32 ( + input wire logic [31:0] crc_in, + input wire logic [7:0] data_in, + output logic [31:0] crc_out +); + integer i; + logic [31:0] c; + always_comb begin + c = crc_in; + for (i = 0; i < 8; i = i + 1) begin + if (c[0] ^ data_in[i]) c = (c >> 1) ^ 32'hedb88320; + else c = (c >> 1); + end + crc_out = c; + end +endmodule + +`default_nettype wire diff --git a/rtl/common/ethernet_packetizer.sv b/rtl/common/ethernet_packetizer.sv new file mode 100644 index 0000000..2a17454 --- /dev/null +++ b/rtl/common/ethernet_packetizer.sv @@ -0,0 +1,233 @@ +`default_nettype none + +module ethernet_packetizer #( + parameter int unsigned FIFO_DEPTH = 16384, + parameter int unsigned FULL_SAMPLES = 2936, + parameter bit EARLIER_SAMPLE_HIGH_NIBBLE = 1'b1 +) ( + input wire logic clk, + input wire logic rst, + input wire logic link_ready, + input wire logic [31:0] dropped_samples_low, + input wire logic fifo_empty, + input wire logic [$clog2(FIFO_DEPTH):0] fifo_level, + output logic fifo_rd_en, + input wire logic [68:0] fifo_rd_data, + input wire logic fifo_rd_valid, + + output logic mac_frame_start, + output logic [10:0] mac_payload_length, + output logic [7:0] mac_payload_data, + output logic mac_payload_valid, + input wire logic mac_payload_ready, + input wire logic mac_busy, + input wire logic mac_frame_done, + + output logic [31:0] packet_sequence, + output logic frame_activity_pulse +); + localparam int unsigned MAX_DATA_BYTES = (FULL_SAMPLES + 1) / 2; + localparam int unsigned SAMPLE_W = $clog2(FULL_SAMPLES + 1); + localparam logic [SAMPLE_W-1:0] FULL_SAMPLES_W = FULL_SAMPLES; + + typedef enum logic [2:0] {P_IDLE, P_LOAD, P_READY, P_STREAM, P_WAIT_DONE} pstate_t; + pstate_t state; + (* ram_style = "distributed" *) logic [7:0] sample_bytes [0:MAX_DATA_BYTES-1]; + logic [SAMPLE_W-1:0] load_sample_count; + logic [15:0] frame_sample_count; + logic [63:0] frame_first_index; + logic [63:0] expected_index; + logic [31:0] frame_drop_snapshot; + logic [31:0] last_reported_drop; + logic [7:0] frame_flags; + logic [10:0] stream_position; + logic waiting_read; + logic hold_valid; + logic [68:0] hold_entry; + logic recovery_in_frame; + logic link_d; + logic link_packet_pending; + + wire logic current_valid = hold_valid || fifo_rd_valid; + wire logic [68:0] current_entry = hold_valid ? hold_entry : fifo_rd_data; + wire logic current_discontinuity = current_entry[68]; + wire logic [63:0] current_index = current_entry[67:4]; + wire logic [3:0] current_sample = current_entry[3:0]; + wire logic [SAMPLE_W-1:0] samples_with_current = load_sample_count + {{(SAMPLE_W-1){1'b0}},1'b1}; + + initial begin + if (FULL_SAMPLES < 2 || FULL_SAMPLES > 2936) + $error("FULL_SAMPLES must be in the range 2..2936"); + if ((FULL_SAMPLES & 1) != 0) + $error("Normal FULL_SAMPLES must be even to preserve byte alignment"); + end + + function automatic logic [7:0] header_byte(input logic [5:0] offset); + begin + case (offset) + 6'd0: header_byte = 8'h4d; // M + 6'd1: header_byte = 8'h58; // X + 6'd2: header_byte = 8'h32; // 2 + 6'd3: header_byte = 8'h37; // 7 + 6'd4: header_byte = 8'd1; + 6'd5: header_byte = frame_flags; + 6'd6: header_byte = 8'h00; + 6'd7: header_byte = 8'h20; + 6'd8: header_byte = packet_sequence[31:24]; + 6'd9: header_byte = packet_sequence[23:16]; + 6'd10: header_byte = packet_sequence[15:8]; + 6'd11: header_byte = packet_sequence[7:0]; + 6'd12: header_byte = frame_first_index[63:56]; + 6'd13: header_byte = frame_first_index[55:48]; + 6'd14: header_byte = frame_first_index[47:40]; + 6'd15: header_byte = frame_first_index[39:32]; + 6'd16: header_byte = frame_first_index[31:24]; + 6'd17: header_byte = frame_first_index[23:16]; + 6'd18: header_byte = frame_first_index[15:8]; + 6'd19: header_byte = frame_first_index[7:0]; + 6'd20: header_byte = frame_sample_count[15:8]; + 6'd21: header_byte = frame_sample_count[7:0]; + 6'd22: header_byte = 8'd4; + 6'd23: header_byte = EARLIER_SAMPLE_HIGH_NIBBLE ? 8'd1 : 8'd2; + 6'd24: header_byte = frame_drop_snapshot[31:24]; + 6'd25: header_byte = frame_drop_snapshot[23:16]; + 6'd26: header_byte = frame_drop_snapshot[15:8]; + 6'd27: header_byte = frame_drop_snapshot[7:0]; + default: header_byte = 8'h00; + endcase + end + endfunction + + always_comb begin + mac_payload_valid = (state == P_STREAM); + if (stream_position < 11'd32) + mac_payload_data = header_byte(stream_position[5:0]); + else + mac_payload_data = sample_bytes[stream_position - 11'd32]; + end + + always_ff @(posedge clk) begin + if (rst) begin + state <= P_IDLE; + fifo_rd_en <= 1'b0; + mac_frame_start <= 1'b0; + mac_payload_length <= 11'd0; + packet_sequence <= 32'd0; + frame_activity_pulse <= 1'b0; + load_sample_count <= '0; + frame_sample_count <= '0; + frame_first_index <= 64'd0; + expected_index <= 64'd0; + frame_drop_snapshot <= 32'd0; + last_reported_drop <= 32'd0; + frame_flags <= 8'd0; + stream_position <= 11'd0; + waiting_read <= 1'b0; + hold_valid <= 1'b0; + hold_entry <= 69'd0; + recovery_in_frame <= 1'b0; + link_d <= 1'b0; + link_packet_pending <= 1'b1; + end else begin + fifo_rd_en <= 1'b0; + mac_frame_start <= 1'b0; + frame_activity_pulse <= 1'b0; + link_d <= link_ready; + if (link_ready && !link_d) link_packet_pending <= 1'b1; + + case (state) + P_IDLE: begin + waiting_read <= 1'b0; + load_sample_count <= '0; + recovery_in_frame <= 1'b0; + // A held boundary sample counts toward the next full frame. + if (link_ready && ((hold_valid && fifo_level >= FULL_SAMPLES-1) || + (!hold_valid && fifo_level >= FULL_SAMPLES))) begin + state <= P_LOAD; + end + end + + P_LOAD: begin + if (current_valid) begin + waiting_read <= 1'b0; + if (hold_valid) hold_valid <= 1'b0; + + if ((load_sample_count != 0) && + (current_discontinuity || current_index != expected_index)) begin + // Preserve the first post-gap sample for the next frame. + hold_entry <= {1'b0, current_entry[67:0]}; + hold_valid <= 1'b1; + frame_sample_count <= {{(16-SAMPLE_W){1'b0}}, load_sample_count}; + mac_payload_length <= 11'd32 + + ((load_sample_count + {{(SAMPLE_W-1){1'b0}},1'b1}) >> 1); + recovery_in_frame <= 1'b1; + state <= P_READY; + end else begin + if (load_sample_count == 0) begin + frame_first_index <= current_index; + recovery_in_frame <= current_discontinuity; + end + expected_index <= current_index + 64'd1; + if (!load_sample_count[0]) begin + if (EARLIER_SAMPLE_HIGH_NIBBLE) + sample_bytes[load_sample_count >> 1] <= {current_sample, 4'h0}; + else + sample_bytes[load_sample_count >> 1] <= {4'h0, current_sample}; + end else begin + if (EARLIER_SAMPLE_HIGH_NIBBLE) + sample_bytes[load_sample_count >> 1][3:0] <= current_sample; + else + sample_bytes[load_sample_count >> 1][7:4] <= current_sample; + end + load_sample_count <= samples_with_current; + if (samples_with_current == FULL_SAMPLES_W) begin + frame_sample_count <= {{(16-SAMPLE_W){1'b0}}, samples_with_current}; + mac_payload_length <= 11'd32 + (FULL_SAMPLES / 2); + state <= P_READY; + end + end + end else if (!waiting_read && !fifo_empty) begin + fifo_rd_en <= 1'b1; + waiting_read <= 1'b1; + end + end + + P_READY: begin + if (link_ready && !mac_busy) begin + frame_drop_snapshot <= dropped_samples_low; + frame_flags <= {5'd0, + link_packet_pending, + (packet_sequence == 32'd0), + (recovery_in_frame || dropped_samples_low != last_reported_drop)}; + last_reported_drop <= dropped_samples_low; + link_packet_pending <= 1'b0; + stream_position <= 11'd0; + mac_frame_start <= 1'b1; + frame_activity_pulse <= 1'b1; + state <= P_STREAM; + end + end + + P_STREAM: begin + if (mac_payload_ready && mac_payload_valid) begin + if (stream_position + 11'd1 == mac_payload_length) + state <= P_WAIT_DONE; + else + stream_position <= stream_position + 11'd1; + end + end + + P_WAIT_DONE: begin + if (mac_frame_done) begin + packet_sequence <= packet_sequence + 32'd1; + state <= P_IDLE; + end + end + + default: state <= P_IDLE; + endcase + end + end +endmodule + +`default_nettype wire diff --git a/rtl/common/ethernet_tx_mac.sv b/rtl/common/ethernet_tx_mac.sv new file mode 100644 index 0000000..88e7587 --- /dev/null +++ b/rtl/common/ethernet_tx_mac.sv @@ -0,0 +1,199 @@ +`default_nettype none + +module ethernet_tx_mac #( + parameter logic [47:0] DEST_MAC = 48'hffffffffffff, + parameter logic [47:0] SOURCE_MAC = 48'h022771000001, + parameter logic [15:0] ETHERTYPE = 16'h88b5 +) ( + input wire logic clk, + input wire logic rst, + input wire logic frame_start, + input wire logic [10:0] payload_length, + input wire logic [7:0] payload_data, + input wire logic payload_valid, + output logic payload_ready, + output logic [7:0] gmii_txd, + output logic gmii_tx_en, + output logic gmii_tx_er, + output logic busy, + output logic frame_done, + output logic [63:0] frames_transmitted, + output logic [63:0] payload_bytes_transmitted +); + typedef enum logic [3:0] { + S_IDLE, S_PREAMBLE, S_SFD, S_DEST, S_SOURCE, S_TYPE, + S_PAYLOAD, S_PAD, S_FCS, S_IFG + } state_t; + state_t state; + logic [3:0] byte_count; + logic [10:0] payload_len_q; + logic [10:0] payload_count; + logic [5:0] pad_remaining; + logic [3:0] ifg_count; + logic [31:0] crc_q; + logic [31:0] crc_next; + logic [31:0] fcs_q; + logic [15:0] requested_body_length; + + ethernet_crc32 u_crc_next ( + .crc_in(crc_q), .data_in(gmii_txd), .crc_out(crc_next) + ); + + always_comb begin + gmii_txd = 8'h00; + gmii_tx_en = 1'b0; + gmii_tx_er = 1'b0; + payload_ready = 1'b0; + busy = (state != S_IDLE); + + case (state) + S_PREAMBLE: begin gmii_tx_en = 1'b1; gmii_txd = 8'h55; end + S_SFD: begin gmii_tx_en = 1'b1; gmii_txd = 8'hd5; end + S_DEST: begin + gmii_tx_en = 1'b1; + case (byte_count) + 4'd0: gmii_txd = DEST_MAC[47:40]; + 4'd1: gmii_txd = DEST_MAC[39:32]; + 4'd2: gmii_txd = DEST_MAC[31:24]; + 4'd3: gmii_txd = DEST_MAC[23:16]; + 4'd4: gmii_txd = DEST_MAC[15:8]; + default: gmii_txd = DEST_MAC[7:0]; + endcase + end + S_SOURCE: begin + gmii_tx_en = 1'b1; + case (byte_count) + 4'd0: gmii_txd = SOURCE_MAC[47:40]; + 4'd1: gmii_txd = SOURCE_MAC[39:32]; + 4'd2: gmii_txd = SOURCE_MAC[31:24]; + 4'd3: gmii_txd = SOURCE_MAC[23:16]; + 4'd4: gmii_txd = SOURCE_MAC[15:8]; + default: gmii_txd = SOURCE_MAC[7:0]; + endcase + end + S_TYPE: begin + gmii_tx_en = 1'b1; + gmii_txd = (byte_count == 4'd0) ? ETHERTYPE[15:8] : ETHERTYPE[7:0]; + end + S_PAYLOAD: begin + gmii_tx_en = 1'b1; + payload_ready = 1'b1; + gmii_txd = payload_data; + end + S_PAD: begin gmii_tx_en = 1'b1; gmii_txd = 8'h00; end + S_FCS: begin + gmii_tx_en = 1'b1; + case (byte_count) + 4'd0: gmii_txd = fcs_q[7:0]; + 4'd1: gmii_txd = fcs_q[15:8]; + 4'd2: gmii_txd = fcs_q[23:16]; + default: gmii_txd = fcs_q[31:24]; + endcase + end + default: begin end + endcase + end + + always_comb requested_body_length = 16'd14 + {5'd0, payload_length}; + + always_ff @(posedge clk) begin + if (rst) begin + state <= S_IDLE; + byte_count <= 4'd0; + payload_len_q <= 11'd0; + payload_count <= 11'd0; + pad_remaining <= 6'd0; + ifg_count <= 4'd0; + crc_q <= 32'hffffffff; + fcs_q <= 32'd0; + frame_done <= 1'b0; + frames_transmitted <= 64'd0; + payload_bytes_transmitted <= 64'd0; + end else begin + frame_done <= 1'b0; + case (state) + S_IDLE: begin + if (frame_start && payload_length <= 11'd1500) begin + payload_len_q <= payload_length; + payload_count <= 11'd0; + byte_count <= 4'd0; + crc_q <= 32'hffffffff; + if (requested_body_length < 16'd60) + pad_remaining <= 16'd60 - requested_body_length; + else + pad_remaining <= 6'd0; + state <= S_PREAMBLE; + end + end + S_PREAMBLE: begin + if (byte_count == 4'd6) begin byte_count <= 4'd0; state <= S_SFD; end + else byte_count <= byte_count + 4'd1; + end + S_SFD: begin byte_count <= 4'd0; state <= S_DEST; end + S_DEST: begin + crc_q <= crc_next; + if (byte_count == 4'd5) begin byte_count <= 4'd0; state <= S_SOURCE; end + else byte_count <= byte_count + 4'd1; + end + S_SOURCE: begin + crc_q <= crc_next; + if (byte_count == 4'd5) begin byte_count <= 4'd0; state <= S_TYPE; end + else byte_count <= byte_count + 4'd1; + end + S_TYPE: begin + crc_q <= crc_next; + if (byte_count == 4'd1) begin + byte_count <= 4'd0; + if (payload_len_q != 11'd0) state <= S_PAYLOAD; + else if (pad_remaining != 6'd0) state <= S_PAD; + else begin fcs_q <= ~crc_next; state <= S_FCS; end + end else byte_count <= 4'd1; + end + S_PAYLOAD: begin + if (payload_valid) begin + crc_q <= crc_next; + payload_count <= payload_count + 11'd1; + if (payload_count + 11'd1 == payload_len_q) begin + if (pad_remaining != 6'd0) state <= S_PAD; + else begin fcs_q <= ~crc_next; byte_count <= 4'd0; state <= S_FCS; end + end + end + end + S_PAD: begin + crc_q <= crc_next; + if (pad_remaining == 6'd1) begin + pad_remaining <= 6'd0; + fcs_q <= ~crc_next; + byte_count <= 4'd0; + state <= S_FCS; + end else pad_remaining <= pad_remaining - 6'd1; + end + S_FCS: begin + if (byte_count == 4'd3) begin byte_count <= 4'd0; ifg_count <= 4'd0; state <= S_IFG; end + else byte_count <= byte_count + 4'd1; + end + S_IFG: begin + if (ifg_count == 4'd11) begin + ifg_count <= 4'd0; + state <= S_IDLE; + frame_done <= 1'b1; + frames_transmitted <= frames_transmitted + 64'd1; + payload_bytes_transmitted <= payload_bytes_transmitted + {53'd0, payload_len_q}; + end else ifg_count <= ifg_count + 4'd1; + end + default: state <= S_IDLE; + endcase + end + end + +`ifndef SYNTHESIS + // The packetizer preassembles every frame, so this contract must hold and + // hardware never encodes TX_ER on RGMII. + always_ff @(posedge clk) begin + if (!rst && state == S_PAYLOAD && !payload_valid) + $error("ethernet_tx_mac payload stream underflow"); + end +`endif +endmodule + +`default_nettype wire diff --git a/rtl/common/gray_counter_sync.sv b/rtl/common/gray_counter_sync.sv new file mode 100644 index 0000000..7912a31 --- /dev/null +++ b/rtl/common/gray_counter_sync.sv @@ -0,0 +1,42 @@ +`default_nettype none + +// Safe observation of a monotonic source-domain counter. The Gray encoding +// changes one bit per increment; two destination flops isolate metastability. +module gray_counter_sync #( + parameter int unsigned WIDTH = 64 +) ( + input wire logic src_clk, + input wire logic src_rst, + input wire logic [WIDTH-1:0] src_binary, + input wire logic dst_clk, + input wire logic dst_rst, + output logic [WIDTH-1:0] dst_binary +); + logic [WIDTH-1:0] src_gray; + (* ASYNC_REG = "TRUE" *) logic [WIDTH-1:0] gray_sync1; + (* ASYNC_REG = "TRUE" *) logic [WIDTH-1:0] gray_sync2; + integer i; + + always_ff @(posedge src_clk) begin + if (src_rst) src_gray <= '0; + else src_gray <= (src_binary >> 1) ^ src_binary; + end + + always_ff @(posedge dst_clk) begin + if (dst_rst) begin + gray_sync1 <= '0; + gray_sync2 <= '0; + end else begin + gray_sync1 <= src_gray; + gray_sync2 <= gray_sync1; + end + end + + always_comb begin + dst_binary[WIDTH-1] = gray_sync2[WIDTH-1]; + for (i = WIDTH-2; i >= 0; i = i - 1) + dst_binary[i] = dst_binary[i+1] ^ gray_sync2[i]; + end +endmodule + +`default_nettype wire diff --git a/rtl/common/max2771_capture.sv b/rtl/common/max2771_capture.sv new file mode 100644 index 0000000..bd5343b --- /dev/null +++ b/rtl/common/max2771_capture.sv @@ -0,0 +1,69 @@ +`default_nettype none + +module max2771_capture #( + parameter bit CAPTURE_RISING = 1'b1 +) ( + input wire logic rf_clk_pmod, + input wire logic rst, + input wire logic [3:0] rf_data_pmod, + input wire logic fifo_full, + output wire logic fifo_w_clk, + output wire logic fifo_w_en, + output wire logic [68:0] fifo_w_data, + output logic [63:0] samples_seen, + output logic [63:0] samples_captured, + output logic [63:0] samples_dropped, + output logic loss_latched, + output logic sample_activity_pulse +); + // The inversion selects the physical falling edge while retaining a single + // positive-edge sequential implementation. Vivado maps clock inversion onto + // the clocking resource; fifo_w_clk clocks both capture bookkeeping and FIFO. + assign fifo_w_clk = CAPTURE_RISING ? rf_clk_pmod : ~rf_clk_pmod; + + (* IOB = "TRUE" *) logic [3:0] sample_iob; + logic [63:0] sample_index_iob; + logic sample_valid; + logic discontinuity_pending; + + assign fifo_w_en = sample_valid; + // Entry format: loss immediately before this sample, absolute index, nibble. + assign fifo_w_data = {discontinuity_pending, sample_index_iob, sample_iob}; + + always_ff @(posedge fifo_w_clk or posedge rst) begin + if (rst) begin + sample_iob <= 4'd0; + sample_index_iob <= 64'd0; + sample_valid <= 1'b0; + discontinuity_pending <= 1'b0; + samples_seen <= 64'd0; + samples_captured <= 64'd0; + samples_dropped <= 64'd0; + loss_latched <= 1'b0; + sample_activity_pulse <= 1'b0; + end else begin + // IOB input register: no combinational data path reaches the FIFO. + sample_iob <= rf_data_pmod; + sample_index_iob <= samples_seen; + sample_valid <= 1'b1; + samples_seen <= samples_seen + 64'd1; + + sample_activity_pulse <= 1'b0; + if (samples_seen[15:0] == 16'hffff) + sample_activity_pulse <= 1'b1; + + if (sample_valid) begin + if (fifo_full) begin + samples_dropped <= samples_dropped + 64'd1; + discontinuity_pending <= 1'b1; + loss_latched <= 1'b1; + end else begin + samples_captured <= samples_captured + 64'd1; + discontinuity_pending <= 1'b0; + end + end + end + end +endmodule + +`default_nettype wire diff --git a/rtl/common/mdio_master.sv b/rtl/common/mdio_master.sv new file mode 100644 index 0000000..d5aac11 --- /dev/null +++ b/rtl/common/mdio_master.sv @@ -0,0 +1,97 @@ +`default_nettype none + +// IEEE 802.3 Clause-22 management interface. MDC is stopped low between +// commands. A read releases MDIO for both turnaround bits and all data bits. +module mdio_master #( + parameter int unsigned MDC_DIV = 25 +) ( + input wire logic clk, + input wire logic rst, + input wire logic start, + input wire logic command_write, + input wire logic [4:0] phy_address, + input wire logic [4:0] register_address, + input wire logic [15:0] write_data, + output logic busy, + output logic done, + output logic [15:0] read_data, + output logic read_valid, + output logic mdc, + input wire logic mdio_i, + output logic mdio_o, + output logic mdio_oe +); + localparam int unsigned DIV_W = (MDC_DIV < 2) ? 1 : $clog2(MDC_DIV); + logic [DIV_W-1:0] div_count; + logic [5:0] bit_index; + logic [63:0] frame_bits; + logic [15:0] read_shift; + logic read_command_q; + + initial begin + if (MDC_DIV < 2) $error("MDC_DIV must be at least two"); + end + + always_comb begin + mdio_o = frame_bits[bit_index]; + mdio_oe = busy; + if (busy && read_command_q && bit_index <= 6'd17) + mdio_oe = 1'b0; + end + + always_ff @(posedge clk) begin + if (rst) begin + busy <= 1'b0; + done <= 1'b0; + read_valid <= 1'b0; + read_data <= 16'd0; + read_shift <= 16'd0; + mdc <= 1'b0; + div_count <= '0; + bit_index <= 6'd63; + frame_bits <= 64'hffffffff_6000_0000; + read_command_q <= 1'b0; + end else begin + done <= 1'b0; + read_valid <= 1'b0; + if (!busy) begin + mdc <= 1'b0; + div_count <= '0; + if (start) begin + busy <= 1'b1; + bit_index <= 6'd63; + read_shift <= 16'd0; + read_command_q <= !command_write; + frame_bits[63:32] <= 32'hffffffff; + frame_bits[31:30] <= 2'b01; + frame_bits[29:28] <= command_write ? 2'b01 : 2'b10; + frame_bits[27:23] <= phy_address; + frame_bits[22:18] <= register_address; + frame_bits[17:16] <= 2'b10; + frame_bits[15:0] <= write_data; + end + end else if (div_count == MDC_DIV-1) begin + div_count <= '0; + if (!mdc) begin + // Rising MDC: PHY sources read data; master samples it. + mdc <= 1'b1; + if (read_command_q && bit_index <= 6'd15) + read_shift <= {read_shift[14:0], mdio_i}; + end else begin + // Falling MDC: move to the next bit or finish the command. + mdc <= 1'b0; + if (bit_index == 6'd0) begin + busy <= 1'b0; + done <= 1'b1; + if (read_command_q) begin + read_data <= read_shift; + read_valid <= 1'b1; + end + end else bit_index <= bit_index - 6'd1; + end + end else div_count <= div_count + {{(DIV_W-1){1'b0}},1'b1}; + end + end +endmodule + +`default_nettype wire diff --git a/rtl/common/phy_reset_ctrl.sv b/rtl/common/phy_reset_ctrl.sv new file mode 100644 index 0000000..a70ef89 --- /dev/null +++ b/rtl/common/phy_reset_ctrl.sv @@ -0,0 +1,33 @@ +`default_nettype none + +module phy_reset_ctrl #( + parameter int unsigned CLK_HZ = 125_000_000, + parameter int unsigned RESET_LOW_MS = 10 +) ( + input wire logic clk, + input wire logic rst, + output logic phy_reset_n, + output logic reset_complete +); + localparam longint unsigned RESET_CYCLES = (CLK_HZ / 1000) * RESET_LOW_MS; + localparam int unsigned COUNT_W = (RESET_CYCLES < 2) ? 1 : $clog2(RESET_CYCLES + 1); + logic [COUNT_W-1:0] count; + + always_ff @(posedge clk) begin + if (rst) begin + phy_reset_n <= 1'b0; + reset_complete <= 1'b0; + count <= '0; + end else if (!reset_complete) begin + phy_reset_n <= 1'b0; + if (count == RESET_CYCLES-1) begin + phy_reset_n <= 1'b1; + reset_complete <= 1'b1; + end else count <= count + {{(COUNT_W-1){1'b0}},1'b1}; + end else begin + phy_reset_n <= 1'b1; + end + end +endmodule + +`default_nettype wire diff --git a/rtl/common/pulse_sync.sv b/rtl/common/pulse_sync.sv new file mode 100644 index 0000000..45992ed --- /dev/null +++ b/rtl/common/pulse_sync.sv @@ -0,0 +1,38 @@ +`default_nettype none + +// Toggle-based pulse crossing. Pulses must be separated by at least two +// destination clock cycles. Assertion is asynchronous only at the synchronizer. +module pulse_sync ( + input wire logic src_clk, + input wire logic src_rst, + input wire logic src_pulse, + input wire logic dst_clk, + input wire logic dst_rst, + output logic dst_pulse +); + logic src_toggle; + (* ASYNC_REG = "TRUE" *) logic dst_sync1; + (* ASYNC_REG = "TRUE" *) logic dst_sync2; + logic dst_sync2_d; + + always_ff @(posedge src_clk) begin + if (src_rst) src_toggle <= 1'b0; + else if (src_pulse) src_toggle <= ~src_toggle; + end + + always_ff @(posedge dst_clk) begin + if (dst_rst) begin + dst_sync1 <= 1'b0; + dst_sync2 <= 1'b0; + dst_sync2_d <= 1'b0; + dst_pulse <= 1'b0; + end else begin + dst_sync1 <= src_toggle; + dst_sync2 <= dst_sync1; + dst_sync2_d <= dst_sync2; + dst_pulse <= dst_sync2 ^ dst_sync2_d; + end + end +endmodule + +`default_nettype wire diff --git a/rtl/common/reset_sync.sv b/rtl/common/reset_sync.sv new file mode 100644 index 0000000..2f45b3e --- /dev/null +++ b/rtl/common/reset_sync.sv @@ -0,0 +1,27 @@ +`default_nettype none + +module reset_sync #( + parameter int unsigned STAGES = 2 +) ( + input wire logic clk, + input wire logic arst, + output logic srst +); + (* ASYNC_REG = "TRUE" *) logic [STAGES-1:0] sync_ff; + + initial begin + if (STAGES < 2) $error("reset_sync STAGES must be at least two"); + end + + always_ff @(posedge clk or posedge arst) begin + if (arst) begin + sync_ff <= '1; + end else begin + sync_ff <= {sync_ff[STAGES-2:0], 1'b0}; + end + end + + assign srst = sync_ff[STAGES-1]; +endmodule + +`default_nettype wire diff --git a/scripts/build_vivado.tcl b/scripts/build_vivado.tcl new file mode 100644 index 0000000..c722ccb --- /dev/null +++ b/scripts/build_vivado.tcl @@ -0,0 +1,4 @@ +# Compatibility entry point retained for the documented repository-root path. +# The only Genesys 2 implementation lives with its target-specific sources. +set compatibility_dir [file normalize [file dirname [info script]]] +source [file normalize [file join $compatibility_dir .. targets genesys2 scripts build_vivado.tcl]] diff --git a/scripts/program_fpga.tcl b/scripts/program_fpga.tcl new file mode 100644 index 0000000..a20d5bb --- /dev/null +++ b/scripts/program_fpga.tcl @@ -0,0 +1,4 @@ +# Compatibility entry point retained for the documented repository-root path. +# argv/tclargs are intentionally left unchanged for the target script. +set compatibility_dir [file normalize [file dirname [info script]]] +source [file normalize [file join $compatibility_dir .. targets genesys2 scripts program_fpga.tcl]] diff --git a/scripts/run_common_sim.sh b/scripts/run_common_sim.sh new file mode 100755 index 0000000..54a2877 --- /dev/null +++ b/scripts/run_common_sim.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +sim_dir="$repo_dir/build/common/questa" +mkdir -p "$sim_dir" +if [[ -d "$sim_dir/work" ]]; then + vdel -lib "$sim_dir/work" -all >/dev/null +fi +vlib "$sim_dir/work" + +vlog -work "$sim_dir/work" -sv \ + "$repo_dir/rtl/common/async_fifo_gray.sv" \ + "$repo_dir/rtl/common/max2771_capture.sv" \ + "$repo_dir/rtl/common/ethernet_crc32.sv" \ + "$repo_dir/rtl/common/ethernet_packetizer.sv" \ + "$repo_dir/rtl/common/ethernet_tx_mac.sv" \ + "$repo_dir/tb/common/tb_ethernet_crc32.sv" \ + "$repo_dir/tb/common/tb_ethernet_tx_mac.sv" \ + "$repo_dir/tb/common/tb_async_fifo_gray.sv" \ + "$repo_dir/tb/common/tb_recorder_stream.sv" \ + "$repo_dir/tb/common/tb_capture_overflow.sv" + +for test in tb_ethernet_crc32 tb_ethernet_tx_mac tb_async_fifo_gray \ + tb_recorder_stream tb_capture_overflow; do + echo "== $test ==" + vsim -c -lib "$sim_dir/work" "$test" -do 'run -all; quit -f' +done diff --git a/targets/efinix_ti375_c529/Makefile b/targets/efinix_ti375_c529/Makefile new file mode 100644 index 0000000..6b33a67 --- /dev/null +++ b/targets/efinix_ti375_c529/Makefile @@ -0,0 +1,19 @@ +.PHONY: sim build program selftest-build selftest-program clean-help + +sim: + ./scripts/run_sim.sh + +build: + ./scripts/build_efinity.sh + +program: + ./scripts/program_efinity.sh + +selftest-build: + ./scripts/build_ethernet_selftest.sh + +selftest-program: + ./scripts/program_ethernet_selftest.sh + +clean-help: + @echo "Generated files are under build/. Remove that directory manually if a clean build is required." diff --git a/targets/efinix_ti375_c529/README.md b/targets/efinix_ti375_c529/README.md new file mode 100644 index 0000000..c7b64f3 --- /dev/null +++ b/targets/efinix_ti375_c529/README.md @@ -0,0 +1,232 @@ +# MAX2771 raw recorder — Efinix Titanium Ti375 C529 kit + +This is a separate board target for the Efinix Titanium Ti375 C529 development +kit. It does not modify the Genesys 2 top-level, constraints, or Vivado flow. +The protocol-compatible capture FIFO, packetizer, CRC and raw Ethernet MAC are +the read-only common RTL in `../../rtl/common`; every Efinix-specific source is +contained in this directory and the source list is explicit in +`scripts/generate_project.py`. + +No RISC-V/SoC block, CPU, AXI fabric, generated Ethernet MAC, IP stack, or +Efinix Ethernet IP is used. Only the Efinity periphery resources needed for +GPIO DDIO and PLL clocking are generated. + +## Data path + +```text +MAX2771 J13/PMOD0 + clock -> A12/GPIOR_95_CLK12 global clock -> GPIO DDR input registers + data ---------------------> rising/falling sample selection + -> sample index + loss marker + -> handwritten 16,384 x 69 async FIFO + -> 2936-sample packetizer + -> handwritten Ethernet MAC/FCS +100 MHz oscillator -> Ethernet PLL -> 125 MHz / 125 MHz +90 degrees + -> GPIO DDIO RGMII -> RTL8211FDI -> RJ45 +``` + +`CAPTURE_RISING=1` selects the GPIO `_HI` samples; `0` selects `_LO` samples. +`EARLIER_SAMPLE_HIGH_NIBBLE=1` produces the required byte order: earlier sample +in bits 7:4, later sample in bits 3:0. + +## MAX2771 wiring + +Use J13, identified as PMOD0 in the official kit guide. The PMOD header is +3.3 V. Do not reuse the Genesys 2 JA ball names on this board. + +| RTL signal | J13 pin | Ti375 logical pin | C529 ball | Level | +|---|---:|---|---|---| +| RF clock input | 10 | GPIOR_95_CLK12 | A12 | 3.3 V | +| data bit 0 | 2 | GPIOR_89 | G16 | 3.3 V | +| data bit 1 | 3 | GPIOR_90 | F16 | 3.3 V | +| data bit 2 | 4 | GPIOR_91 | F17 | 3.3 V | +| data bit 3 | 7 | GPIOR_92 | G17 | 3.3 V | +| ground | 5 or 11 | — | — | 0 V | +| PMOD supply | 6 or 12 | — | — | 3.3 V | + +The MAX2771 clock-to-data minimum/maximum values and cable/adapter skew are not +known. Interface Designer generates constraints for its internal GPIO register +paths; those numbers are not a substitute for MAX2771 external timing. Measure +or obtain the external values before claiming complete board-level RF input +timing closure. See `docs/hardware_assumptions.md`. + +## Ethernet hardware + +The board PHY is RTL8211FDI and the RGMII bus is 1.8 V. The board straps PHY +address 2, receive delay enabled and transmit delay disabled. Receive is not +implemented. The FPGA creates the required transmit-clock phase shift with a +125 MHz, +90-degree PLL output and sends TXD/TXCTL with the unshifted 125 MHz +clock. PHY reset remains low for 10 ms, followed by a 30 ms settle period. + +The MDIO controller selects Realtek page `0xa43`, reads PHYSR register `0x1a`, +restores page 0, and only asserts `link_ready` for real-time link=1, +speed=1000 Mb/s and full duplex. A 100 Mb/s link deliberately transmits no +frames: the 130.944 Mb/s raw sample stream already exceeds a 100 Mb/s line +before Ethernet overhead. + +## Clocks + +| Function | Source | PLL | Outputs | +|---|---|---|---| +| Ethernet | board 100 MHz oscillator, U4 | PLL_BL0, EXT_CLK1 | CLK0 100 MHz dummy, CLK1 125 MHz, CLK2 125 MHz +90° | +| RF capture | MAX2771 clock, J13.10/A12 | direct GPIOR_95_CLK12 GCLK | nominal 32.736 MHz | + +The 100 MHz dummy output satisfies the Titanium CLKOUT0 restriction. The +Ethernet PLL outputs and their physical reference resources are checked by the Efinity +Interface Designer API during every build. The direct RF GCLK period defaults +to 30.547 ns and can be overridden with `RF_CLOCK_PERIOD_NS` when the actual +MAX2771 clock differs. + +## Raw Ethernet protocol + +Frames use destination `ff:ff:ff:ff:ff:ff`, source `02:27:71:00:00:01`, and +EtherType `0x88b5`. A normal Ethernet payload is exactly 1500 bytes: the +32-byte `MX27` header followed by 1468 packed bytes/2936 samples. All metadata +is network byte order. The byte protocol is unchanged from +[`docs/raw_ethernet_protocol.md`](../../docs/raw_ethernet_protocol.md). + +The FPGA emits preamble, SFD, Ethernet header, payload, FCS and 12-byte IFG. +Normal host NICs remove preamble/SFD/FCS before delivery to `AF_PACKET`. + +## Efinity headless CLI build + +“Non-project mode” is Vivado terminology and is not used here. This target has +the Efinity equivalent headless flow: scripts generate the periphery and a +temporary project XML, then invoke the supported command-line compile flow. + +The installed path on this machine is: + +```bash +export EFINITY_HOME=/tools/efinix/efinity/2026.1 +cd targets/efinix_ti375_c529 +make build +``` + +Outputs are under `build/outflow/`, including the pinout, interface timing, +synthesis, P&R, timing reports and `ti375_max2771_recorder.bit`. The script +fails on API/DRC/compile errors, absent required clocks, missing timing report, +negative timing slack, or missing bitstream. + +The final A12/GPIOR_95 Efinity 2026.1 C4 build completed successfully on +1 August 2026. Post-route slack was +0.636 ns setup/+0.094 ns hold at 125 MHz +and +25.614 ns setup/+0.031 ns hold at 32.736 MHz. The generated CDC report says +`No Synchronizer warnings to report`. Placement used 36,253/362,880 XLRs +(9.99%), 138/2,688 memory blocks (5.13%), and no DSP blocks. + +The kit guide revision describes a C4 timing grade and the local Efinity kit +example also uses C4. Confirm the marking on the actual FPGA. Override only the +timing model, not the base package device, when necessary: + +```bash +EFINIX_TIMING_MODEL=I3 make build +``` + +## Programming + +Connect the board USB/JTAG interface, power the fan as required by the kit, and +run: + +```bash +export EFINITY_HOME=/tools/efinix/efinity/2026.1 +make program +``` + +This calls `efx_run.py ... --flow program --pgm_opts mode=jtag` with the +generated `.bit` file. It does not alter the boot flash. + +For an RF-independent physical Ethernet test, a separate diagnostic top-level +generates the repeating nibble sequence 0 through 15 and uses the production +packetizer, MAC, FCS, MDIO and RGMII blocks: + +```bash +make selftest-build +make selftest-program +``` + +Restore the recorder afterwards with `make program`; JTAG configuration is +volatile. On the tested board/direct cable, a 3.034-second self-test capture +received 35,446 consecutive frames/104,069,456 samples with no protocol gap, +FPGA drop, socket drop, or pattern mismatch. + +## Host capture + +The raw protocol and host application are board-independent: + +```bash +make -C ../../host +sudo ip link set dev enp3s0 up +sudo ip addr flush dev enp3s0 +sudo ethtool enp3s0 +sudo ethtool -K enp3s0 gro off lro off # unsupported options may be ignored +sudo sysctl -w net.core.rmem_max=268435456 +sudo ../../host/max2771_raw_rx \ + --interface enp3s0 \ + --output max2771_capture.bin \ + --gap-mode zero-fill +``` + +No IP address, ARP or firewall change is required. Prefer a direct cable or an +isolated switch because every frame is broadcast. For non-root operation: + +```bash +sudo setcap cap_net_raw,cap_net_admin=eip ../../host/max2771_raw_rx +``` + +Use a dedicated Gigabit NIC and storage capable of sustained writes above +16.368 MB/s. + +## Throughput + +At 32.736 Msps, the source is 130.944 Mb/s (16.368 MB/s). Full frames occur at +approximately 11,149.864 frames/s. Including preamble/SFD, MAC header, 1500-byte +payload, FCS and IFG, each frame occupies 1538 byte-times, or 12,304 bits. Line +use is approximately 137.188 Mb/s, 13.719% of 1 Gb/s; raw line-rate margin is +about 7.29x. + +The current packetizer first loads a frame buffer and then transmits it. At 125 +MHz, its conservative three-clock read handshake loads 2936 samples in about +70.464 us; the frame plus IFG takes 12.304 us. This gives about 8.3% scheduler +margin over the required frame rate. During loading the FIFO drains faster than +the RF source; during frame transmission about 403 new samples arrive. In +steady state it reaches the 2936-sample launch threshold, finishes near 2709 +samples, then refills. Link-down occupancy can instead reach the full 16,384 +entries; all further input samples are counted as drops, never overwritten. + +## LEDs + +| LED | Meaning | +|---|---| +| LED9 | Ethernet PLL/reset system ready; may toggle only while Ethernet PLL is acquiring lock | +| LED10 | qualified 1000/full link ready | +| LED11 | RF sample activity | +| LED12 | FIFO has data; frame-count divider gives visible TX activity | +| LED13 | FIFO overflow/sample loss latched | + +There are five verified user LEDs, so FIFO and transmit activity share LED12. + +## Simulation + +```bash +cd targets/efinix_ti375_c529 +make sim +``` + +The target tests verify RGMII half-cycle/control mapping and RTL8211FDI PHYSR +link qualification/link-down counting. The script then runs the full shared +recorder regression: standard CRC vector, complete Ethernet bytes/FCS/IFG, +random async FIFO CDC, two-frame sample reconstruction, header/index/sequence, +reset behavior and overflow handling. + +## Troubleshooting + +- No Ethernet frames: verify `ethtool` reports 1000 Mb/s/full duplex and LED10 + is on. A 100 Mb/s negotiation is intentionally rejected. +- LED13 on before link: capture cannot be backpressured; the FIFO fills while + link is unavailable, and the loss is correctly counted. +- Interface DRC failure: check that the base device is `Ti375C529` and the + board wiring/pins have not been changed. +- RF timing failure or corrupted nibbles: confirm 3.3 V levels, ground, capture + edge and the actual MAX2771 clock-to-data window with measurement. +- Efinity warns about `libstdc++.so`: this is emitted by the installed 2026.1 + setup script on this host. Do not delete tool files automatically; follow the + vendor’s installation guidance if the tools fail to run. diff --git a/targets/efinix_ti375_c529/constraints/ti375_max2771_recorder.sdc b/targets/efinix_ti375_c529/constraints/ti375_max2771_recorder.sdc new file mode 100644 index 0000000..c742620 --- /dev/null +++ b/targets/efinix_ti375_c529/constraints/ti375_max2771_recorder.sdc @@ -0,0 +1,26 @@ +# Core timing constraints for the Ti375 C529 target. The Efinity-generated +# outflow/ti375_max2771_recorder.pt.sdc is prepended by build_efinity.sh and +# contains the periphery clock constraints. + +# The external 100 MHz Ethernet reference, its PLL outputs, and the direct +# 32.736 MHz GPIOR_95_CLK12 input are constrained in the generated periphery +# SDC. Their core-side net names include eth_clk and rf_clk_pmod; no duplicate +# create_clock is added here. + +# MAX2771 sampling is source-synchronous but exact clock-to-data plus board +# skew is not supplied by the MAX2771 connection specification. Do not invent +# set_input_delay numbers. Add measured/datasheet-derived -min and -max values +# here before claiming external I/O timing closure, for example: +# set_input_delay -clock rf_clk_pmod -max [get_ports {rf_data_pmod[*]}] +# set_input_delay -clock rf_clk_pmod -min [get_ports {rf_data_pmod[*]}] + +# Only the RF and Ethernet clocks are asynchronous. FIFO Gray-pointer +# synchronizers, the Gray counter synchronizer and pulse synchronizer are the +# deliberate CDC paths. No functional RF-to-Ethernet combinational path exists. +set_clock_groups -asynchronous \ + -group [get_clocks {rf_clk_pmod}] \ + -group [get_clocks {eth_clk eth_clk90 pll_dummy_clk}] + +# Efinity's async_reg attribute keeps the two synchronizer stages adjacent. +# The RTL also limits CDC to Gray-coded pointers/counters or single-bit toggles; +# no blanket false path is applied to unrelated logic. diff --git a/targets/efinix_ti375_c529/docs/efinity_flow.md b/targets/efinix_ti375_c529/docs/efinity_flow.md new file mode 100644 index 0000000..8a255a9 --- /dev/null +++ b/targets/efinix_ti375_c529/docs/efinity_flow.md @@ -0,0 +1,26 @@ +# Headless Efinity build flow + +This target does not call itself “non-project mode”; that is a Vivado-specific +term. Efinity requires a project XML for `efx_run.py`. The XML is generated on +every command-line build and is not maintained through the GUI. + +1. `generate_project.py` creates the device/source/constraint XML. +2. `create_interface.py` uses the supported Efinity Python API to create PLL, + GPIO, DDIO, pin and I/O-standard resources and runs Interface Designer DRC. +3. The generated `.pt.sdc` is merged with the handwritten CDC constraints. +4. `efx_run.py --flow compile` performs synthesis, place-and-route and + bitstream generation. +5. `check_timing.py` rejects missing clocks and negative setup/hold slack. +6. `program_efinity.sh` uses the supported JTAG program flow. + +No `.xci`, block design, processor, AXI system or generated Ethernet core is +created. The generated periphery is limited to low-level Titanium PLL and GPIO +register resources, equivalent in role to primitive instantiation on the +Genesys target. + +The exact command on this workstation is: + +```bash +export EFINITY_HOME=/tools/efinix/efinity/2026.1 +make -C targets/efinix_ti375_c529 build +``` diff --git a/targets/efinix_ti375_c529/docs/hardware_assumptions.md b/targets/efinix_ti375_c529/docs/hardware_assumptions.md new file mode 100644 index 0000000..114f611 --- /dev/null +++ b/targets/efinix_ti375_c529/docs/hardware_assumptions.md @@ -0,0 +1,67 @@ +# Ti375 C529 hardware facts and assumptions + +## Verified from official Efinix material + +- Board: Titanium Ti375 C529 development kit, C529 package. +- On-board Ethernet is an RTL8211FDI 10/100/1000 transceiver with RGMII. +- PHY reset is active-low and must be asserted for at least 10 ms. +- PHY address straps are `010`, address 2. +- RGMII bank 2E and PHY `DVDD_RG` operate at 1.8 V. +- RX delay strap is high (about 2 ns); TX delay strap is low. This transmitter + therefore creates its own +90-degree RGMII transmit clock. +- J13/PMOD0 supports 3.3 V. +- Board oscillator U4 is 100 MHz on GPIOL_25_PLLIN1. +- Ethernet TX balls are TXCTL B20, TXC C17 and TXD0..3 B17,A16,A17,C19. +- MDC is B19, MDIO B14 and PHY reset D10. +- LED9..13 logical pins/balls are GPIOL_23/V4, GPIOL_24/U6, + GPIOT_N_50/L18, GPIOT_N_19/R20 and GPIOT_N_64_CLK15_N/E19. + +The authoritative sources are the official kit page, development-kit user +guide, board schematic/BOM and Ti375 C529 pinout spreadsheet linked there. + +## Verified by the installed Efinity 2026.1 device database + +- U4/GPIOL_25_PLLIN1 connects to `PLL_BL0` external clock 1. +- A12 maps to the global-clock-capable `GPIOR_95_CLK12` resource; the RF clock + is used directly, without a sampling PLL. +- The 100 -> 100/125/125(+90°) MHz Ethernet PLL solution is legal for + Ti375C529 C4. +- The pin I/O-bank voltages used by `create_interface.py` pass Interface + Designer DRC. + +## Assumptions supplied by the recorder specification + +- MAX2771 clock nominally 32.736 MHz. +- One 4-bit sample is valid per selected clock edge. +- Data bits are recorded literally without signed conversion or reinterpretation. +- Earlier sample is normally packed in the high nibble. +- Ethernet destination/source/EtherType are broadcast, + `02:27:71:00:00:01`, and `0x88b5`. + +## Wiring supplied for this Ti375 target + +- RF clock: J13.10, A12, `GPIOR_95_CLK12`. +- RF data 0..3: J13.2/J13.3/J13.4/J13.7, G16/F16/F17/G17, + `GPIOR_89` through `GPIOR_92`. +- All five signals use `3.3_V_LVCMOS`. + +## Must be confirmed from the actual hardware + +- FPGA speed/temperature grade. The kit guide and installed Efinity example + use C4; current sales material may show a different ordering code. Read the + device marking and set `EFINIX_TIMING_MODEL` accordingly. +- The MAX2771 output electrical mode is compatible with 3.3 V LVCMOS at J13. + Do not connect a higher-voltage or differential output directly. +- Cable/adapter signal integrity is adequate at 32.736 MHz. + +## Requires datasheet information or measurement + +- MAX2771 clock-to-data minimum/maximum delay at the selected interface mode. +- Board/adapter trace skew and setup/hold margin at the Ti375 input register. +- Whether rising or falling-edge capture gives the intended word boundary. +- RGMII clock/data phase at the PHY pins. Confirm the nominal +2 ns relation + with an oscilloscope during initial bring-up if hardware acceptance requires + measured compliance. + +Efinity’s generated `pt.sdc` input-delay values describe its internal interface +register path. They are tool-generated, not claimed MAX2771 timing numbers. diff --git a/targets/efinix_ti375_c529/interface/create_interface.py b/targets/efinix_ti375_c529/interface/create_interface.py new file mode 100755 index 0000000..4c3fdc3 --- /dev/null +++ b/targets/efinix_ti375_c529/interface/create_interface.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +"""Create the Ti375 C529 periphery with the supported Efinity Python API.""" + +import math +import os +import pprint +import sys + +efinity_home = os.environ.get("EFINITY_HOME") +if not efinity_home: + raise SystemExit("EFINITY_HOME is not set; source $EFINITY_HOME/bin/setup.sh first") + +sys.path.append(os.path.join(efinity_home, "pt", "bin")) +from api_service.design import DesignAPI # type: ignore # supplied by Efinity + +PROJECT = os.environ.get("EFINITY_PROJECT", "ti375_max2771_recorder") +DEVICE = os.environ.get("EFINIX_DEVICE", "Ti375C529") + +design = DesignAPI(is_verbose=True) +design.create(PROJECT, DEVICE, "./", overwrite=True) + + +def set_io(name, standard, drive=None, slew=None): + design.set_property(name, "IO_STANDARD", standard) + if drive is not None: + design.set_property(name, "DRIVE_STRENGTH", str(drive)) + if slew is not None: + design.set_property(name, "SLEW_RATE", str(slew)) + + +def output_gpio(name, pin, standard, drive=None, slew=None): + design.create_output_gpio(name) + design.assign_pkg_pin(name, pin) + set_io(name, standard, drive, slew) + + +# 100 MHz board oscillator on GPIOL_25_PLLIN1 (package ball U4). Using the +# left-side PLL keeps PLL_TR0 available for the MAX2771 clock on J13 pin 1. +design.create_block("eth_pll", block_type="PLL") +design.set_property("eth_pll", {"REFCLK_FREQ": "100.0"}, block_type="PLL") +design.gen_pll_ref_clock( + "eth_pll", + pll_res="PLL_BL0", + refclk_src="EXTERNAL", + refclk_name="clk100", + ext_refclk_no="1", +) +design.assign_pkg_pin("clk100", "U4") +set_io("clk100", "3.3_V_LVCMOS") + +design.set_property("eth_pll", "LOCKED_PIN", "pll_locked", block_type="PLL") +design.set_property("eth_pll", "RSTN_PIN", "pll_resetn", block_type="PLL") +design.set_property("eth_pll", "CLKOUT0_PIN", "pll_dummy_clk", block_type="PLL") +design.set_property( + "eth_pll", {"CLKOUT1_EN": "1", "CLKOUT1_PIN": "eth_clk"}, block_type="PLL" +) +design.set_property( + "eth_pll", {"CLKOUT2_EN": "1", "CLKOUT2_PIN": "eth_clk90"}, block_type="PLL" +) +design.set_property("eth_pll", "FEEDBACK_MODE", "LOCAL", block_type="PLL") +design.set_property("eth_pll", "FEEDBACK_CLK", "CLK0", block_type="PLL") + +target_freq = { + "CLKOUT0_FREQ": "100.0", + "CLKOUT0_PHASE": "0.0", + "CLKOUT1_FREQ": "125.0", + "CLKOUT1_PHASE": "0.0", + "CLKOUT2_FREQ": "125.0", + "CLKOUT2_PHASE": "90.0", +} +pll_results = design.auto_calc_pll_clock("eth_pll", target_freq) +if not pll_results: + raise SystemExit("Efinity could not find a legal 100 MHz -> 100/125/125 MHz PLL solution") +pprint.pprint(pll_results) +pll_props = design.get_property( + "eth_pll", + ["CLKOUT0_FREQ", "CLKOUT1_FREQ", "CLKOUT2_FREQ", "CLKOUT2_PHASE"], + block_type="PLL", +) +for key, expected in (("CLKOUT0_FREQ", 100.0), ("CLKOUT1_FREQ", 125.0), + ("CLKOUT2_FREQ", 125.0), ("CLKOUT2_PHASE", 90.0)): + if not math.isclose(float(pll_props[key]), expected, rel_tol=1.0e-6, abs_tol=1.0e-6): + raise SystemExit(f"PLL property {key} is {pll_props[key]}, expected {expected}") + +# MAX2771 clock wiring supplied for this target: J13.10, package A12, +# GPIOR_95_CLK12. This is a dedicated global-clock-capable GPIO, so preserve +# the source-synchronous clock directly instead of passing it through a PLL. +design.create_input_clock_gpio("rf_clk_pmod") +design.assign_pkg_pin("rf_clk_pmod", "A12") +set_io("rf_clk_pmod", "3.3_V_LVCMOS") + +design.create_input_gpio("rf_data_pmod", 3, 0) +for bit, pin in enumerate(("G16", "F16", "F17", "G17")): + design.assign_pkg_pin(f"rf_data_pmod[{bit}]", pin) +design.set_property("rf_data_pmod", "IN_REG", "DDIO_RESYNC_PIPE") +design.set_property("rf_data_pmod", "IN_CLK_PIN", "rf_clk_pmod") +design.set_property("rf_data_pmod", "IN_HI_PIN", "rf_data_pmod_HI") +design.set_property("rf_data_pmod", "IN_LO_PIN", "rf_data_pmod_LO") +set_io("rf_data_pmod", "3.3_V_LVCMOS") + +# RTL8211FDI RGMII transmit side, all in bank 2E at 1.8 V. +design.create_output_gpio("eth_txd", 3, 0) +for bit, pin in enumerate(("B17", "A16", "A17", "C19")): + design.assign_pkg_pin(f"eth_txd[{bit}]", pin) +design.set_property("eth_txd", "OUT_REG", "DDIO_RESYNC") +design.set_property("eth_txd", "OUT_CLK_PIN", "eth_clk") +design.set_property("eth_txd", "OUT_HI_PIN", "eth_txd_HI") +design.set_property("eth_txd", "OUT_LO_PIN", "eth_txd_LO") +set_io("eth_txd", "1.8_V_LVCMOS", drive=16, slew=1) + +design.create_output_gpio("eth_txctl") +design.assign_pkg_pin("eth_txctl", "B20") +design.set_property("eth_txctl", "OUT_REG", "DDIO_RESYNC") +design.set_property("eth_txctl", "OUT_CLK_PIN", "eth_clk") +design.set_property("eth_txctl", "OUT_HI_PIN", "eth_txctl_HI") +design.set_property("eth_txctl", "OUT_LO_PIN", "eth_txctl_LO") +set_io("eth_txctl", "1.8_V_LVCMOS", drive=16, slew=1) + +design.create_output_gpio("eth_txc") +design.assign_pkg_pin("eth_txc", "C17") +design.set_property("eth_txc", "OUT_REG", "DDIO_RESYNC") +design.set_property("eth_txc", "OUT_CLK_PIN", "eth_clk90") +design.set_property("eth_txc", "OUT_HI_PIN", "eth_txc_HI") +design.set_property("eth_txc", "OUT_LO_PIN", "eth_txc_LO") +set_io("eth_txc", "1.8_V_LVCMOS", drive=16, slew=1) + +output_gpio("eth_mdc", "B19", "1.8_V_LVCMOS", drive=8, slew=1) +output_gpio("eth_phyrst_n", "D10", "3.3_V_LVCMOS", drive=8, slew=0) + +design.create_inout_gpio("eth_mdio") +design.assign_pkg_pin("eth_mdio", "B14") +design.set_property("eth_mdio", "IN_PIN", "eth_mdio_IN") +design.set_property("eth_mdio", "OUT_PIN", "eth_mdio_OUT") +design.set_property("eth_mdio", "OE_PIN", "eth_mdio_OE") +set_io("eth_mdio", "1.8_V_LVCMOS", drive=8, slew=0) + +# LED9..LED13 span both 3.3 V and 1.8 V banks, so they are separate periphery +# blocks connected to members of the core-side user_led bus. +for name, bit, pin, standard in ( + ("led9", 0, "V4", "3.3_V_LVCMOS"), + ("led10", 1, "U6", "3.3_V_LVCMOS"), + ("led11", 2, "L18", "1.8_V_LVCMOS"), + ("led12", 3, "R20", "1.8_V_LVCMOS"), + ("led13", 4, "E19", "1.8_V_LVCMOS"), +): + design.create_output_gpio(name) + design.assign_pkg_pin(name, pin) + design.set_property(name, "OUT_PIN", f"user_led[{bit}]") + set_io(name, standard, drive=8, slew=0) + +if not design.check_design(): + raise SystemExit("Efinity Interface Designer DRC failed") +design.generate(enable_bitstream=True) +design.save() diff --git a/targets/efinix_ti375_c529/rtl/phy_init_rtl8211f.sv b/targets/efinix_ti375_c529/rtl/phy_init_rtl8211f.sv new file mode 100644 index 0000000..247d74d --- /dev/null +++ b/targets/efinix_ti375_c529/rtl/phy_init_rtl8211f.sv @@ -0,0 +1,147 @@ +`default_nettype none + +// RTL8211FDI link qualification over IEEE 802.3 Clause-22 MDIO. The board +// straps the PHY for auto-negotiation and address 2. Realtek PHYSR is on page +// 0xa43, register 0x1a; it reports real-time link, resolved speed and duplex. +// Page 0 is restored after every poll so no hidden PHY state leaks outward. +module phy_init_rtl8211f #( + parameter int unsigned CLK_HZ = 125_000_000, + parameter int unsigned SETTLE_MS = 30, + parameter int unsigned POLL_MS = 100, + parameter logic [4:0] PHY_ADDRESS = 5'd2 +) ( + input wire logic clk, + input wire logic rst, + input wire logic phy_reset_complete, + output logic mdio_start, + output logic mdio_write, + output logic [4:0] mdio_phy_address, + output logic [4:0] mdio_register_address, + output logic [15:0] mdio_write_data, + input wire logic mdio_busy, + input wire logic mdio_done, + input wire logic [15:0] mdio_read_data, + output logic initialization_complete, + output logic link_ready, + output logic [63:0] link_down_events +); + localparam longint unsigned SETTLE_CYCLES = (CLK_HZ / 1000) * SETTLE_MS; + localparam longint unsigned POLL_CYCLES = (CLK_HZ / 1000) * POLL_MS; + localparam longint unsigned MAX_CYCLES = + (POLL_CYCLES > SETTLE_CYCLES) ? POLL_CYCLES : SETTLE_CYCLES; + localparam int unsigned TIMER_W = (MAX_CYCLES < 2) ? 1 : $clog2(MAX_CYCLES + 1); + + typedef enum logic [3:0] { + S_WAIT_RESET, S_SETTLE, + S_START_PAGE, S_WAIT_PAGE, + S_START_PHYSR, S_WAIT_PHYSR, + S_START_RESTORE, S_WAIT_RESTORE, + S_POLL_WAIT + } state_t; + state_t state; + logic [TIMER_W-1:0] timer; + logic [15:0] physr; + logic qualified_link; + + always_comb begin + mdio_write = (state == S_START_PAGE || state == S_WAIT_PAGE || + state == S_START_RESTORE || state == S_WAIT_RESTORE); + mdio_phy_address = PHY_ADDRESS; + mdio_register_address = (state == S_START_PHYSR || state == S_WAIT_PHYSR) + ? 5'h1a : 5'h1f; + mdio_write_data = (state == S_START_PAGE || state == S_WAIT_PAGE) + ? 16'h0a43 : 16'h0000; + + // PHYSR[5:4]=2'b10 is 1000 Mb/s, bit 3 full duplex, bit 2 link. + qualified_link = (physr[5:4] == 2'b10) && physr[3] && physr[2]; + end + + always_ff @(posedge clk) begin + if (rst) begin + state <= S_WAIT_RESET; + timer <= '0; + physr <= 16'd0; + mdio_start <= 1'b0; + initialization_complete <= 1'b0; + link_ready <= 1'b0; + link_down_events <= 64'd0; + end else begin + mdio_start <= 1'b0; + case (state) + S_WAIT_RESET: begin + link_ready <= 1'b0; + if (phy_reset_complete) begin + timer <= '0; + state <= S_SETTLE; + end + end + + S_SETTLE: begin + if (timer == SETTLE_CYCLES-1) begin + timer <= '0; + state <= S_START_PAGE; + end else begin + timer <= timer + {{(TIMER_W-1){1'b0}}, 1'b1}; + end + end + + S_START_PAGE: begin + if (!mdio_busy) begin + mdio_start <= 1'b1; + state <= S_WAIT_PAGE; + end + end + S_WAIT_PAGE: if (mdio_done) state <= S_START_PHYSR; + + S_START_PHYSR: begin + if (!mdio_busy) begin + mdio_start <= 1'b1; + state <= S_WAIT_PHYSR; + end + end + S_WAIT_PHYSR: begin + if (mdio_done) begin + physr <= mdio_read_data; + state <= S_START_RESTORE; + end + end + + S_START_RESTORE: begin + if (!mdio_busy) begin + mdio_start <= 1'b1; + state <= S_WAIT_RESTORE; + end + end + S_WAIT_RESTORE: begin + if (mdio_done) begin + initialization_complete <= 1'b1; + if (link_ready && !qualified_link) + link_down_events <= link_down_events + 64'd1; + link_ready <= qualified_link; + timer <= '0; + state <= S_POLL_WAIT; + end + end + + S_POLL_WAIT: begin + if (timer == POLL_CYCLES-1) begin + timer <= '0; + state <= S_START_PAGE; + end else begin + timer <= timer + {{(TIMER_W-1){1'b0}}, 1'b1}; + end + end + + default: state <= S_WAIT_RESET; + endcase + end + end + + initial begin + if (CLK_HZ < 1000) $error("CLK_HZ must be at least 1000"); + if (SETTLE_MS == 0 || POLL_MS == 0) + $error("SETTLE_MS and POLL_MS must be non-zero"); + end +endmodule + +`default_nettype wire diff --git a/targets/efinix_ti375_c529/rtl/rgmii_tx_efinix.sv b/targets/efinix_ti375_c529/rtl/rgmii_tx_efinix.sv new file mode 100644 index 0000000..42a5eda --- /dev/null +++ b/targets/efinix_ti375_c529/rtl/rgmii_tx_efinix.sv @@ -0,0 +1,31 @@ +`default_nettype none + +// Core-side half-cycle values for Efinity Interface Designer DDIO outputs. +// The physical GPIOs are configured as DDIO_RESYNC in interface/create_interface.py. +// *_HI is launched on the rising edge and *_LO on the falling edge. +module rgmii_tx_efinix ( + input wire logic [7:0] gmii_txd, + input wire logic gmii_tx_en, + input wire logic gmii_tx_er, + output logic [3:0] eth_txd_HI, + output logic [3:0] eth_txd_LO, + output logic eth_txctl_HI, + output logic eth_txctl_LO, + output logic eth_txc_HI, + output logic eth_txc_LO +); + always_comb begin + // RGMII: GMII bits 3:0 on the rising edge, bits 7:4 on falling. + eth_txd_HI = gmii_txd[3:0]; + eth_txd_LO = gmii_txd[7:4]; + eth_txctl_HI = gmii_tx_en; + eth_txctl_LO = gmii_tx_en ^ gmii_tx_er; + + // These constants are clocked by the 90-degree PLL output in the + // periphery, producing the forwarded 125 MHz RGMII TX clock. + eth_txc_HI = 1'b1; + eth_txc_LO = 1'b0; + end +endmodule + +`default_nettype wire diff --git a/targets/efinix_ti375_c529/rtl/ti375_ethernet_selftest_top.sv b/targets/efinix_ti375_c529/rtl/ti375_ethernet_selftest_top.sv new file mode 100644 index 0000000..c90068f --- /dev/null +++ b/targets/efinix_ti375_c529/rtl/ti375_ethernet_selftest_top.sv @@ -0,0 +1,142 @@ +`default_nettype none + +// Hardware-only Ethernet diagnostic. This top intentionally bypasses the RF +// capture/FIFO and supplies an endless, ordered 0..15 sample sequence to the +// production packetizer, MAC, FCS, RGMII and PHY-management blocks. +module ti375_ethernet_selftest_top ( + input wire logic pll_dummy_clk, + input wire logic eth_clk, + input wire logic eth_clk90, + input wire logic pll_locked, + output logic pll_resetn, + input wire logic rf_clk_pmod, + input wire logic [3:0] rf_data_pmod_HI, + input wire logic [3:0] rf_data_pmod_LO, + output logic eth_mdc, + input wire logic eth_mdio_IN, + output logic eth_mdio_OUT, + output logic eth_mdio_OE, + output logic eth_phyrst_n, + output logic [3:0] eth_txd_HI, + output logic [3:0] eth_txd_LO, + output logic eth_txctl_HI, + output logic eth_txctl_LO, + output logic eth_txc_HI, + output logic eth_txc_LO, + output logic [4:0] user_led +); + localparam int unsigned FIFO_DEPTH = 16384; + + logic eth_rst; + logic phy_reset_complete; + logic mdio_start, mdio_write, mdio_busy, mdio_done; + logic [4:0] mdio_phy_address, mdio_register_address; + logic [15:0] mdio_write_data, mdio_read_data; + logic phy_init_complete, link_ready; + logic [63:0] link_down_events; + + logic fifo_rd_en, fifo_rd_valid; + logic [68:0] fifo_rd_data; + logic [63:0] generated_sample_index; + logic mac_frame_start, mac_payload_valid, mac_payload_ready; + logic mac_busy, mac_frame_done; + logic [10:0] mac_payload_length; + logic [7:0] mac_payload_data; + logic [31:0] packet_sequence; + logic frame_activity_pulse; + logic [7:0] gmii_txd; + logic gmii_tx_en, gmii_tx_er; + logic [63:0] frames_transmitted, payload_bytes_transmitted; + logic [22:0] heartbeat; + logic activity_toggle; + + assign pll_resetn = 1'b1; + + reset_sync u_eth_reset (.clk(eth_clk), .arst(!pll_locked), .srst(eth_rst)); + + phy_reset_ctrl #(.CLK_HZ(125_000_000), .RESET_LOW_MS(10)) u_phy_reset ( + .clk(eth_clk), .rst(eth_rst), .phy_reset_n(eth_phyrst_n), + .reset_complete(phy_reset_complete) + ); + phy_init_rtl8211f u_phy_init ( + .clk(eth_clk), .rst(eth_rst), .phy_reset_complete(phy_reset_complete), + .mdio_start(mdio_start), .mdio_write(mdio_write), + .mdio_phy_address(mdio_phy_address), + .mdio_register_address(mdio_register_address), .mdio_write_data(mdio_write_data), + .mdio_busy(mdio_busy), .mdio_done(mdio_done), .mdio_read_data(mdio_read_data), + .initialization_complete(phy_init_complete), .link_ready(link_ready), + .link_down_events(link_down_events) + ); + mdio_master #(.MDC_DIV(25)) u_mdio ( + .clk(eth_clk), .rst(eth_rst), .start(mdio_start), + .command_write(mdio_write), .phy_address(mdio_phy_address), + .register_address(mdio_register_address), .write_data(mdio_write_data), + .busy(mdio_busy), .done(mdio_done), .read_data(mdio_read_data), + .read_valid(), .mdc(eth_mdc), .mdio_i(eth_mdio_IN), + .mdio_o(eth_mdio_OUT), .mdio_oe(eth_mdio_OE) + ); + + // One-cycle read response that has the same entry format as max2771_capture: + // {discontinuity, absolute sample index, literal four-bit sample}. + always_ff @(posedge eth_clk) begin + if (eth_rst) begin + fifo_rd_valid <= 1'b0; + fifo_rd_data <= 69'd0; + generated_sample_index <= 64'd0; + heartbeat <= 23'd0; + activity_toggle <= 1'b0; + end else begin + fifo_rd_valid <= fifo_rd_en; + if (fifo_rd_en) begin + fifo_rd_data <= {1'b0, generated_sample_index, + generated_sample_index[3:0]}; + generated_sample_index <= generated_sample_index + 64'd1; + end + heartbeat <= heartbeat + 23'd1; + if (frame_activity_pulse) + activity_toggle <= !activity_toggle; + end + end + + ethernet_packetizer #( + .FIFO_DEPTH(FIFO_DEPTH), .FULL_SAMPLES(2936), + .EARLIER_SAMPLE_HIGH_NIBBLE(1'b1) + ) u_packetizer ( + .clk(eth_clk), .rst(eth_rst), .link_ready(link_ready), + .dropped_samples_low(32'd0), .fifo_empty(1'b0), + .fifo_level(15'd16384), .fifo_rd_en(fifo_rd_en), + .fifo_rd_data(fifo_rd_data), .fifo_rd_valid(fifo_rd_valid), + .mac_frame_start(mac_frame_start), .mac_payload_length(mac_payload_length), + .mac_payload_data(mac_payload_data), .mac_payload_valid(mac_payload_valid), + .mac_payload_ready(mac_payload_ready), .mac_busy(mac_busy), + .mac_frame_done(mac_frame_done), .packet_sequence(packet_sequence), + .frame_activity_pulse(frame_activity_pulse) + ); + ethernet_tx_mac u_mac ( + .clk(eth_clk), .rst(eth_rst), .frame_start(mac_frame_start), + .payload_length(mac_payload_length), .payload_data(mac_payload_data), + .payload_valid(mac_payload_valid), .payload_ready(mac_payload_ready), + .gmii_txd(gmii_txd), .gmii_tx_en(gmii_tx_en), .gmii_tx_er(gmii_tx_er), + .busy(mac_busy), .frame_done(mac_frame_done), + .frames_transmitted(frames_transmitted), + .payload_bytes_transmitted(payload_bytes_transmitted) + ); + rgmii_tx_efinix u_rgmii_tx ( + .gmii_txd(gmii_txd), .gmii_tx_en(gmii_tx_en), .gmii_tx_er(gmii_tx_er), + .eth_txd_HI(eth_txd_HI), .eth_txd_LO(eth_txd_LO), + .eth_txctl_HI(eth_txctl_HI), .eth_txctl_LO(eth_txctl_LO), + .eth_txc_HI(eth_txc_HI), .eth_txc_LO(eth_txc_LO) + ); + + always_comb begin + user_led[0] = pll_locked || (!pll_locked && (pll_dummy_clk ^ eth_clk90)); + user_led[1] = link_ready; + user_led[2] = heartbeat[22] || (!pll_locked && + (rf_clk_pmod ^ (^rf_data_pmod_HI) ^ + (^rf_data_pmod_LO))); + user_led[3] = activity_toggle; + user_led[4] = 1'b0; + end +endmodule + +`default_nettype wire diff --git a/targets/efinix_ti375_c529/rtl/ti375_max2771_recorder_top.sv b/targets/efinix_ti375_c529/rtl/ti375_max2771_recorder_top.sv new file mode 100644 index 0000000..9499169 --- /dev/null +++ b/targets/efinix_ti375_c529/rtl/ti375_max2771_recorder_top.sv @@ -0,0 +1,202 @@ +`default_nettype none + +module ti375_max2771_recorder_top #( + parameter bit CAPTURE_RISING = 1'b1, + parameter bit EARLIER_SAMPLE_HIGH_NIBBLE = 1'b1, + parameter int unsigned FIFO_DEPTH = 16384 +) ( + // Core clocks and status generated by the Efinity periphery PLL. + input wire logic pll_dummy_clk, + input wire logic eth_clk, + input wire logic eth_clk90, + input wire logic pll_locked, + output logic pll_resetn, + input wire logic rf_clk_pmod, + input wire logic [3:0] rf_data_pmod_HI, + input wire logic [3:0] rf_data_pmod_LO, + + output logic eth_mdc, + input wire logic eth_mdio_IN, + output logic eth_mdio_OUT, + output logic eth_mdio_OE, + output logic eth_phyrst_n, + output logic [3:0] eth_txd_HI, + output logic [3:0] eth_txd_LO, + output logic eth_txctl_HI, + output logic eth_txctl_LO, + output logic eth_txc_HI, + output logic eth_txc_LO, + output logic [4:0] user_led +); + localparam int unsigned FIFO_LEVEL_W = $clog2(FIFO_DEPTH) + 1; + logic eth_rst, rf_rst; + logic [3:0] selected_rf_data; + logic rf_fifo_clk; + logic fifo_w_en, fifo_full; + logic [68:0] fifo_w_data; + (* keep = "true" *) logic [FIFO_LEVEL_W-1:0] fifo_w_level, fifo_r_level; + (* keep = "true" *) logic [63:0] samples_seen, samples_captured, samples_dropped; + logic [63:0] dropped_samples_eth; + logic loss_latched_rf, loss_sync1, loss_sync2; + logic sample_activity_rf, sample_activity_eth; + logic fifo_rd_en, fifo_rd_valid, fifo_empty; + logic [68:0] fifo_rd_data; + (* keep = "true" *) logic [63:0] fifo_overflow_attempts, fifo_underflow_attempts; + + logic phy_reset_complete; + logic mdio_start, mdio_write, mdio_busy, mdio_done, mdio_read_valid; + logic [4:0] mdio_phy_address, mdio_register_address; + logic [15:0] mdio_write_data, mdio_read_data; + logic phy_init_complete, link_ready; + (* keep = "true" *) logic [63:0] link_down_events; + + logic mac_frame_start, mac_payload_valid, mac_payload_ready, mac_busy, mac_frame_done; + logic [10:0] mac_payload_length; + logic [7:0] mac_payload_data; + logic [7:0] gmii_txd; + logic gmii_tx_en, gmii_tx_er; + (* keep = "true" *) logic [63:0] frames_transmitted, payload_bytes_transmitted; + logic [31:0] packet_sequence; + logic frame_activity_pulse; + logic [23:0] sample_led_stretch; + logic [12:0] frame_activity_divider; + logic frame_activity_toggle; + + // Efinity requires CLKOUT0 to stay within 4x of the reference clock. It is + // a 100 MHz housekeeping output. The pre-lock LED probe below also keeps + // both PLL clock interface ports present for generated SDC constraints. + assign pll_resetn = 1'b1; + assign selected_rf_data = CAPTURE_RISING ? rf_data_pmod_HI : rf_data_pmod_LO; + + reset_sync u_eth_reset (.clk(eth_clk), .arst(!pll_locked), .srst(eth_rst)); + + // Both physical edges are captured in Titanium GPIO DDIO registers. The + // selected half-cycle is then passed through the common capture/index block. + max2771_capture #(.CAPTURE_RISING(1'b1)) u_capture ( + .rf_clk_pmod(rf_clk_pmod), .rst(rf_rst), .rf_data_pmod(selected_rf_data), + .fifo_full(fifo_full), .fifo_w_clk(rf_fifo_clk), .fifo_w_en(fifo_w_en), + .fifo_w_data(fifo_w_data), .samples_seen(samples_seen), + .samples_captured(samples_captured), .samples_dropped(samples_dropped), + .loss_latched(loss_latched_rf), .sample_activity_pulse(sample_activity_rf) + ); + reset_sync u_rf_reset ( + .clk(rf_fifo_clk), .arst(!pll_locked), .srst(rf_rst) + ); + + async_fifo_gray #(.DATA_WIDTH(69), .DEPTH(FIFO_DEPTH)) u_sample_fifo ( + .w_clk(rf_fifo_clk), .w_rst(rf_rst), .w_en(fifo_w_en), .w_data(fifo_w_data), + .w_full(fifo_full), .w_level(fifo_w_level), + .overflow_attempt_count(fifo_overflow_attempts), + .r_clk(eth_clk), .r_rst(eth_rst), .r_en(fifo_rd_en), .r_data(fifo_rd_data), + .r_valid(fifo_rd_valid), .r_empty(fifo_empty), .r_level(fifo_r_level), + .underflow_attempt_count(fifo_underflow_attempts) + ); + + gray_counter_sync #(.WIDTH(64)) u_drop_sync ( + .src_clk(rf_fifo_clk), .src_rst(rf_rst), .src_binary(samples_dropped), + .dst_clk(eth_clk), .dst_rst(eth_rst), .dst_binary(dropped_samples_eth) + ); + pulse_sync u_sample_pulse_sync ( + .src_clk(rf_fifo_clk), .src_rst(rf_rst), .src_pulse(sample_activity_rf), + .dst_clk(eth_clk), .dst_rst(eth_rst), .dst_pulse(sample_activity_eth) + ); + + always_ff @(posedge eth_clk) begin + if (eth_rst) begin + loss_sync1 <= 1'b0; + loss_sync2 <= 1'b0; + end else begin + loss_sync1 <= loss_latched_rf; + loss_sync2 <= loss_sync1; + end + end + + phy_reset_ctrl #(.CLK_HZ(125_000_000), .RESET_LOW_MS(10)) u_phy_reset ( + .clk(eth_clk), .rst(eth_rst), .phy_reset_n(eth_phyrst_n), + .reset_complete(phy_reset_complete) + ); + phy_init_rtl8211f u_phy_init ( + .clk(eth_clk), .rst(eth_rst), .phy_reset_complete(phy_reset_complete), + .mdio_start(mdio_start), .mdio_write(mdio_write), + .mdio_phy_address(mdio_phy_address), + .mdio_register_address(mdio_register_address), .mdio_write_data(mdio_write_data), + .mdio_busy(mdio_busy), .mdio_done(mdio_done), .mdio_read_data(mdio_read_data), + .initialization_complete(phy_init_complete), .link_ready(link_ready), + .link_down_events(link_down_events) + ); + mdio_master #(.MDC_DIV(25)) u_mdio ( + .clk(eth_clk), .rst(eth_rst), .start(mdio_start), + .command_write(mdio_write), .phy_address(mdio_phy_address), + .register_address(mdio_register_address), .write_data(mdio_write_data), + .busy(mdio_busy), .done(mdio_done), .read_data(mdio_read_data), + .read_valid(mdio_read_valid), .mdc(eth_mdc), + .mdio_i(eth_mdio_IN), .mdio_o(eth_mdio_OUT), .mdio_oe(eth_mdio_OE) + ); + + ethernet_packetizer #( + .FIFO_DEPTH(FIFO_DEPTH), .FULL_SAMPLES(2936), + .EARLIER_SAMPLE_HIGH_NIBBLE(EARLIER_SAMPLE_HIGH_NIBBLE) + ) u_packetizer ( + .clk(eth_clk), .rst(eth_rst), .link_ready(link_ready), + .dropped_samples_low(dropped_samples_eth[31:0]), + .fifo_empty(fifo_empty), .fifo_level(fifo_r_level), + .fifo_rd_en(fifo_rd_en), .fifo_rd_data(fifo_rd_data), + .fifo_rd_valid(fifo_rd_valid), .mac_frame_start(mac_frame_start), + .mac_payload_length(mac_payload_length), .mac_payload_data(mac_payload_data), + .mac_payload_valid(mac_payload_valid), .mac_payload_ready(mac_payload_ready), + .mac_busy(mac_busy), .mac_frame_done(mac_frame_done), + .packet_sequence(packet_sequence), .frame_activity_pulse(frame_activity_pulse) + ); + ethernet_tx_mac u_mac ( + .clk(eth_clk), .rst(eth_rst), .frame_start(mac_frame_start), + .payload_length(mac_payload_length), .payload_data(mac_payload_data), + .payload_valid(mac_payload_valid), .payload_ready(mac_payload_ready), + .gmii_txd(gmii_txd), .gmii_tx_en(gmii_tx_en), .gmii_tx_er(gmii_tx_er), + .busy(mac_busy), .frame_done(mac_frame_done), + .frames_transmitted(frames_transmitted), + .payload_bytes_transmitted(payload_bytes_transmitted) + ); + rgmii_tx_efinix u_rgmii_tx ( + .gmii_txd(gmii_txd), .gmii_tx_en(gmii_tx_en), .gmii_tx_er(gmii_tx_er), + .eth_txd_HI(eth_txd_HI), .eth_txd_LO(eth_txd_LO), + .eth_txctl_HI(eth_txctl_HI), .eth_txctl_LO(eth_txctl_LO), + .eth_txc_HI(eth_txc_HI), .eth_txc_LO(eth_txc_LO) + ); + + always_ff @(posedge eth_clk) begin + if (eth_rst) begin + sample_led_stretch <= 24'd0; + frame_activity_divider <= 13'd0; + frame_activity_toggle <= 1'b0; + end else begin + if (sample_activity_eth) sample_led_stretch <= 24'hffffff; + else if (sample_led_stretch != 24'd0) + sample_led_stretch <= sample_led_stretch - 24'd1; + + if (frame_activity_pulse) begin + if (frame_activity_divider == 13'd4095) begin + frame_activity_divider <= 13'd0; + frame_activity_toggle <= !frame_activity_toggle; + end else begin + frame_activity_divider <= frame_activity_divider + 13'd1; + end + end + end + end + + always_comb begin + // Ti375 C529 has five verified user LEDs (LED9 through LED13). + user_led[0] = (pll_locked && !eth_rst) || + (!pll_locked && (pll_dummy_clk ^ eth_clk90)); + user_led[1] = link_ready; + // The pre-lock term keeps both physical DDIO capture edges in the + // implemented design even when CAPTURE_RISING is a constant parameter. + user_led[2] = (sample_led_stretch != 24'd0) || + (!pll_locked && (^rf_data_pmod_LO)); + user_led[3] = !fifo_empty && (frame_activity_toggle || !link_ready); + user_led[4] = loss_sync2; + end + +endmodule + +`default_nettype wire diff --git a/targets/efinix_ti375_c529/scripts/build_efinity.sh b/targets/efinix_ti375_c529/scripts/build_efinity.sh new file mode 100755 index 0000000..edc5cd0 --- /dev/null +++ b/targets/efinix_ti375_c529/scripts/build_efinity.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +target_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +build_dir=${EFINIX_BUILD_DIR:-"$target_dir/build"} +project=${EFINITY_PROJECT:-ti375_max2771_recorder} +constraint_file=${EFINITY_CONSTRAINT_FILE:-"$target_dir/constraints/ti375_max2771_recorder.sdc"} + +if [[ -z ${EFINITY_HOME:-} ]]; then + echo "ERROR: set EFINITY_HOME to the Efinity installation directory" >&2 + echo "Example: export EFINITY_HOME=/opt/efinity/2025.2" >&2 + exit 2 +fi +if [[ ! -f "$EFINITY_HOME/bin/setup.sh" ]]; then + echo "ERROR: $EFINITY_HOME/bin/setup.sh not found" >&2 + exit 2 +fi + +# shellcheck disable=SC1090 +export PYTHONPATH=${PYTHONPATH:-} +source "$EFINITY_HOME/bin/setup.sh" +mkdir -p "$build_dir" + +export EFINITY_BUILD_DIR="$build_dir" +export EFINITY_PROJECT="$project" +export EFINIX_DEVICE=${EFINIX_DEVICE:-Ti375C529} +export EFINIX_TIMING_MODEL=${EFINIX_TIMING_MODEL:-C4} +rf_clock_period_ns=${RF_CLOCK_PERIOD_NS:-30.547} +if [[ ! $rf_clock_period_ns =~ ^[0-9]+([.][0-9]+)?$ ]]; then + echo "ERROR: RF_CLOCK_PERIOD_NS must be a positive numeric period in ns" >&2 + exit 2 +fi + +cd "$build_dir" +"$EFINITY_HOME/bin/python3" "$target_dir/scripts/generate_project.py" +"$EFINITY_HOME/bin/python3" "$target_dir/interface/create_interface.py" + +pt_sdc="$build_dir/outflow/$project.pt.sdc" +if [[ ! -f "$pt_sdc" ]]; then + echo "ERROR: Interface Designer did not generate $pt_sdc" >&2 + exit 1 +fi +{ + # A direct GCLK input has no frequency property in Interface Designer, so + # Efinity emits this documented placeholder for the user clock period. + sed "s//$rf_clock_period_ns/g" "$pt_sdc" + sed -n '1,$p' "$constraint_file" +} > "$build_dir/${project}_merged.sdc" +if grep -q '' "$build_dir/${project}_merged.sdc"; then + echo "ERROR: unresolved RF input-clock period in merged SDC" >&2 + exit 1 +fi + +"$EFINITY_HOME/bin/python3" "$EFINITY_HOME/scripts/efx_run.py" \ + "$build_dir/$project.xml" --flow compile + +"$EFINITY_HOME/bin/python3" "$target_dir/scripts/check_timing.py" \ + "$build_dir/outflow/$project.timing.rpt" + +cdc_report="$build_dir/outflow/$project.cdc.rpt" +if [[ ! -f "$cdc_report" ]] || ! grep -q "No Synchronizer warnings to report" "$cdc_report"; then + echo "ERROR: Efinity CDC/synchronizer report is absent or contains warnings" >&2 + exit 1 +fi +if grep -Eq "No ports matched|No clocks matched|No valid pin\(s\) found for clock" \ + "$build_dir/outflow/$project.log"; then + echo "ERROR: major SDC coverage warning found in Efinity log" >&2 + exit 1 +fi + +if [[ ! -f "$build_dir/outflow/$project.bit" ]]; then + echo "ERROR: expected JTAG bitstream was not generated" >&2 + exit 1 +fi +echo "Bitstream: $build_dir/outflow/$project.bit" diff --git a/targets/efinix_ti375_c529/scripts/build_ethernet_selftest.sh b/targets/efinix_ti375_c529/scripts/build_ethernet_selftest.sh new file mode 100755 index 0000000..6293640 --- /dev/null +++ b/targets/efinix_ti375_c529/scripts/build_ethernet_selftest.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +target_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +export EFINITY_PROJECT=ti375_ethernet_selftest +export EFINITY_TOP=ti375_ethernet_selftest_top +export EFINITY_BUILD_DIR=${EFINITY_SELFTEST_BUILD_DIR:-"$target_dir/build_selftest"} +exec "$target_dir/scripts/build_efinity.sh" diff --git a/targets/efinix_ti375_c529/scripts/check_timing.py b/targets/efinix_ti375_c529/scripts/check_timing.py new file mode 100755 index 0000000..1793475 --- /dev/null +++ b/targets/efinix_ti375_c529/scripts/check_timing.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Fail conservatively on Efinity timing violations or absent clock coverage.""" + +import re +import sys +from pathlib import Path + +if len(sys.argv) != 2: + raise SystemExit("usage: check_timing.py OUTflow/project.timing.rpt") +path = Path(sys.argv[1]) +if not path.is_file(): + raise SystemExit(f"timing report not found: {path}") +text = path.read_text(encoding="utf-8", errors="replace") + +if re.search(r"\b(?:VIOLATED|timing\s+not\s+met|negative\s+slack)\b", text, re.I): + raise SystemExit(f"timing violation reported in {path}") + +lower = text.lower() +start = lower.find("clock relationship summary") +if start < 0: + raise SystemExit("Clock Relationship Summary is absent; cannot prove timing closure") +end_candidates = [x for x in ( + lower.find("path details", start + 1), + lower.find("max critical", start + 1), +) if x >= 0] +summary = text[start:min(end_candidates) if end_candidates else len(text)] + +# Relationship rows contain the setup/hold slack values. Reject any negative +# decimal in this summary; clock names on this design contain no negative nums. +if re.search(r"(?&2 + exit 2 +fi +# shellcheck disable=SC1090 +export PYTHONPATH=${PYTHONPATH:-} +source "$EFINITY_HOME/bin/setup.sh" +if [[ ! -f "$build_dir/outflow/$project.bit" ]]; then + echo "ERROR: build first; bitstream is absent" >&2 + exit 1 +fi +cd "$build_dir" +"$EFINITY_HOME/bin/python3" "$EFINITY_HOME/scripts/efx_run.py" \ + "$build_dir/$project.xml" --flow program --pgm_opts mode=jtag diff --git a/targets/efinix_ti375_c529/scripts/program_ethernet_selftest.sh b/targets/efinix_ti375_c529/scripts/program_ethernet_selftest.sh new file mode 100755 index 0000000..21cb149 --- /dev/null +++ b/targets/efinix_ti375_c529/scripts/program_ethernet_selftest.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +target_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +export EFINITY_PROJECT=ti375_ethernet_selftest +export EFINITY_BUILD_DIR=${EFINITY_SELFTEST_BUILD_DIR:-"$target_dir/build_selftest"} +exec "$target_dir/scripts/program_efinity.sh" diff --git a/targets/efinix_ti375_c529/scripts/run_sim.sh b/targets/efinix_ti375_c529/scripts/run_sim.sh new file mode 100755 index 0000000..13bc868 --- /dev/null +++ b/targets/efinix_ti375_c529/scripts/run_sim.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +target_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +repo_dir=$(cd "$target_dir/../.." && pwd) +sim_dir="$target_dir/build/questa" +mkdir -p "$sim_dir" +if [[ -d "$sim_dir/work" ]]; then + vdel -lib "$sim_dir/work" -all >/dev/null +fi +vlib "$sim_dir/work" + +vlog -work "$sim_dir/work" -sv \ + "$repo_dir/rtl/common/reset_sync.sv" \ + "$repo_dir/rtl/common/pulse_sync.sv" \ + "$repo_dir/rtl/common/gray_counter_sync.sv" \ + "$repo_dir/rtl/common/async_fifo_gray.sv" \ + "$repo_dir/rtl/common/max2771_capture.sv" \ + "$repo_dir/rtl/common/ethernet_crc32.sv" \ + "$repo_dir/rtl/common/ethernet_packetizer.sv" \ + "$repo_dir/rtl/common/ethernet_tx_mac.sv" \ + "$repo_dir/rtl/common/phy_reset_ctrl.sv" \ + "$repo_dir/rtl/common/mdio_master.sv" \ + "$target_dir/rtl/phy_init_rtl8211f.sv" \ + "$target_dir/rtl/rgmii_tx_efinix.sv" \ + "$target_dir/rtl/ti375_max2771_recorder_top.sv" \ + "$target_dir/tb/tb_rgmii_tx_efinix.sv" \ + "$target_dir/tb/tb_phy_init_rtl8211f.sv" + +for test in tb_rgmii_tx_efinix tb_phy_init_rtl8211f; do + echo "== $test ==" + vsim -c -lib "$sim_dir/work" "$test" -do 'run -all; quit -f' +done + +echo "Efinix target unit tests passed. Running the shared recorder regression..." +"$repo_dir/scripts/run_common_sim.sh" diff --git a/targets/efinix_ti375_c529/tb/tb_phy_init_rtl8211f.sv b/targets/efinix_ti375_c529/tb/tb_phy_init_rtl8211f.sv new file mode 100644 index 0000000..34f509e --- /dev/null +++ b/targets/efinix_ti375_c529/tb/tb_phy_init_rtl8211f.sv @@ -0,0 +1,93 @@ +`timescale 1ns/1ps +`default_nettype none + +module tb_phy_init_rtl8211f; + logic clk = 1'b0; + logic rst = 1'b1; + logic reset_complete = 1'b0; + logic start, write_cmd, busy = 1'b0, done = 1'b0; + logic [4:0] phy_addr, reg_addr; + logic [15:0] write_data, read_data = 16'd0; + logic init_complete, link_ready; + logic [63:0] link_down_events; + integer transaction = 0; + integer response_delay = 0; + + always #5 clk = !clk; + + phy_init_rtl8211f #( + .CLK_HZ(1000), .SETTLE_MS(1), .POLL_MS(1), .PHY_ADDRESS(5'd2) + ) dut ( + .clk(clk), .rst(rst), .phy_reset_complete(reset_complete), + .mdio_start(start), .mdio_write(write_cmd), .mdio_phy_address(phy_addr), + .mdio_register_address(reg_addr), .mdio_write_data(write_data), + .mdio_busy(busy), .mdio_done(done), .mdio_read_data(read_data), + .initialization_complete(init_complete), .link_ready(link_ready), + .link_down_events(link_down_events) + ); + + // Minimal transaction-level MDIO responder. First PHYSR poll resolves + // 1000/full; the second reports link down to exercise the event counter. + always_ff @(posedge clk) begin + done <= 1'b0; + if (start && !busy) begin + if (phy_addr !== 5'd2) + $fatal(1, "wrong PHY address"); + busy <= 1'b1; + response_delay <= 2; + end else if (busy && response_delay != 0) begin + response_delay <= response_delay - 1; + end else if (busy) begin + busy <= 1'b0; + done <= 1'b1; + case (transaction) + 0: begin + if (reg_addr !== 5'h1f || !write_cmd || write_data !== 16'h0a43) + $fatal(1, "expected PHYSR page-select write"); + end + 1: begin + if (reg_addr !== 5'h1a || write_cmd) + $fatal(1, "expected PHYSR read"); + read_data <= 16'h002c; // 1000 Mb/s + full duplex + link + end + 2: begin + if (reg_addr !== 5'h1f || !write_cmd || write_data !== 16'h0000) + $fatal(1, "expected page-zero restore"); + end + 3: begin + if (reg_addr !== 5'h1f || !write_cmd || write_data !== 16'h0a43) + $fatal(1, "expected second page-select write"); + end + 4: begin + if (reg_addr !== 5'h1a || write_cmd) + $fatal(1, "expected link-down PHYSR read"); + read_data <= 16'h0028; // 1000 Mb/s + full duplex, link clear + end + default: begin + if (reg_addr !== 5'h1f || !write_cmd || write_data !== 16'h0000) + $fatal(1, "expected second page-zero restore"); + end + endcase + transaction <= transaction + 1; + end + end + + initial begin + repeat (3) @(posedge clk); + rst <= 1'b0; + reset_complete <= 1'b1; + wait (init_complete && link_ready); + if (link_down_events !== 64'd0) $fatal(1, "spurious link-down event"); + wait (!link_ready); + if (link_down_events !== 64'd1) $fatal(1, "link-down event not counted"); + $display("PASS tb_phy_init_rtl8211f"); + $finish; + end + + initial begin + #5000; + $fatal(1, "timeout"); + end +endmodule + +`default_nettype wire diff --git a/targets/efinix_ti375_c529/tb/tb_rgmii_tx_efinix.sv b/targets/efinix_ti375_c529/tb/tb_rgmii_tx_efinix.sv new file mode 100644 index 0000000..72d957a --- /dev/null +++ b/targets/efinix_ti375_c529/tb/tb_rgmii_tx_efinix.sv @@ -0,0 +1,36 @@ +`timescale 1ns/1ps +`default_nettype none + +module tb_rgmii_tx_efinix; + logic [7:0] gmii_txd; + logic gmii_tx_en, gmii_tx_er; + logic [3:0] hi, lo; + logic ctl_hi, ctl_lo, txc_hi, txc_lo; + + rgmii_tx_efinix dut ( + .gmii_txd(gmii_txd), .gmii_tx_en(gmii_tx_en), .gmii_tx_er(gmii_tx_er), + .eth_txd_HI(hi), .eth_txd_LO(lo), + .eth_txctl_HI(ctl_hi), .eth_txctl_LO(ctl_lo), + .eth_txc_HI(txc_hi), .eth_txc_LO(txc_lo) + ); + + initial begin + gmii_txd = 8'ha5; gmii_tx_en = 1'b1; gmii_tx_er = 1'b0; #1; + if (hi !== 4'h5 || lo !== 4'ha || ctl_hi !== 1'b1 || ctl_lo !== 1'b1) + $fatal(1, "RGMII normal-data mapping failed"); + if (txc_hi !== 1'b1 || txc_lo !== 1'b0) + $fatal(1, "RGMII forwarded-clock values failed"); + + gmii_txd = 8'h3c; gmii_tx_en = 1'b1; gmii_tx_er = 1'b1; #1; + if (hi !== 4'hc || lo !== 4'h3 || ctl_hi !== 1'b1 || ctl_lo !== 1'b0) + $fatal(1, "RGMII TX error-control encoding failed"); + + gmii_tx_en = 1'b0; gmii_tx_er = 1'b0; #1; + if (ctl_hi !== 1'b0 || ctl_lo !== 1'b0) + $fatal(1, "RGMII idle-control encoding failed"); + $display("PASS tb_rgmii_tx_efinix"); + $finish; + end +endmodule + +`default_nettype wire diff --git a/targets/genesys2/Makefile b/targets/genesys2/Makefile new file mode 100644 index 0000000..def30cb --- /dev/null +++ b/targets/genesys2/Makefile @@ -0,0 +1,10 @@ +.PHONY: sim build program + +sim: + ../../scripts/run_common_sim.sh + +build: + vivado -mode batch -source scripts/build_vivado.tcl + +program: + vivado -mode batch -source scripts/program_fpga.tcl diff --git a/targets/genesys2/README.md b/targets/genesys2/README.md new file mode 100644 index 0000000..3fcabdb --- /dev/null +++ b/targets/genesys2/README.md @@ -0,0 +1,104 @@ +# Digilent Genesys 2 target + +This directory contains only the Genesys 2 adaptation of the MAX2771 recorder. +The protocol/capture/FIFO/MAC RTL comes from `../../rtl/common`; no portable +module is copied here. + +## Target-specific files + +| File | Function | +|---|---| +| `rtl/max2771_recorder_top.sv` | connects common blocks, status counters and LEDs | +| `rtl/ethernet_clock_gen_xilinx.sv` | 200 MHz differential input and primitive MMCM for 125 MHz/125 MHz +90° | +| `rtl/rgmii_tx_xilinx.sv` | Kintex-7 ODDR implementation for TXD/TXCTL/TXC | +| `rtl/phy_init_rtl8211e.sv` | polls Genesys RTL8211E PHY address 1 and gates 1000/full link | +| `constraints/genesys2_max2771_recorder.xdc` | verified clock, JA, Ethernet and LED pins | +| `scripts/build_vivado.tcl` | in-memory/non-project synthesis, P&R, reports and bitstream | +| `scripts/program_fpga.tcl` | volatile JTAG configuration | + +## Wiring + +| Signal | FPGA ball | JA pin | Standard | +|---|---:|---:|---| +| `rf_clk_pmod` | U27 | JA[0] | LVCMOS33 | +| `rf_data_pmod[0]` | U28 | JA[1] | LVCMOS33 | +| `rf_data_pmod[1]` | T26 | JA[2] | LVCMOS33 | +| `rf_data_pmod[2]` | T27 | JA[3] | LVCMOS33 | +| `rf_data_pmod[3]` | T22 | JA[4] | LVCMOS33 | + +Connect a common ground and use only a 3.3-V-compatible digital source. + +## Simulation + +From the repository root: + +```bash +make genesys2-sim +``` + +This runs the common FIFO/protocol/MAC regression. Xilinx primitives are +guarded by `SYNTHESIS`, so vendor simulation libraries are not required for the +common regression. + +## Build + +```bash +make genesys2-build +``` + +Equivalent direct command: + +```bash +vivado -mode batch -source targets/genesys2/scripts/build_vivado.tcl +``` + +Output: + +```text +build/genesys2/vivado/max2771_recorder.bit +build/genesys2/reports/ +``` + +The build uses part `xc7k325tffg900-2`, top `max2771_recorder_top`, creates an +in-memory project, fails on negative setup/hold slack or unexpected major CDC +warnings and uses no block design or generated IP. A clean Vivado 2026.1 build +on 1 August 2026 completed synthesis, placement, routing, DRC and bitstream +generation with +0.212 ns worst setup slack and +0.090 ns worst hold slack. + +## Programming + +Connect the Digilent JTAG USB port and run: + +```bash +make genesys2-program +``` + +This configures the FPGA volatile SRAM. It does not write configuration flash. +Power cycling removes the design. + +## Clocking and RGMII + +The 200 MHz differential oscillator drives an MMCM with a 1000 MHz VCO. It +generates 125 MHz at 0° for the MAC/data ODDRs and 125 MHz at +90° for the TXC +ODDR. The resulting nominal +2 ns clock shift is required because the board +straps RTL8211E transmit delay off. + +PHY reset remains asserted for 10 ms, register access waits another 30 ms, and +transmit starts only after PHY-specific status reports resolved 1000 Mb/s/full +duplex link. Ethernet receive is not implemented. + +## LEDs + +| LED | Meaning | +|---:|---| +| 0 | MMCM locked/system ready | +| 1 | qualified 1000/full link | +| 2 | RF sample activity | +| 3 | FIFO non-empty | +| 4 | sample loss/overflow latched | +| 5 | frame activity | +| 6 | first PHY status read completed | +| 7 | Ethernet-domain heartbeat | + +See [hardware assumptions](docs/hardware_assumptions.md) and +[timing constraints](docs/timing_constraints.md) before hardware sign-off. diff --git a/targets/genesys2/constraints/genesys2_max2771_recorder.xdc b/targets/genesys2/constraints/genesys2_max2771_recorder.xdc new file mode 100644 index 0000000..ed0a853 --- /dev/null +++ b/targets/genesys2/constraints/genesys2_max2771_recorder.xdc @@ -0,0 +1,75 @@ +############################################################################### +# Digilent Genesys 2 / MAX2771 raw recorder +# Board pins copied from Digilent Genesys-2-Master.xdc, repository commit +# 00a3404901f35aa9567b01ecb3f2c233b6efe9f4. FPGA: xc7k325tffg900-2. +############################################################################### + +# 200 MHz differential system oscillator. +set_property -dict {PACKAGE_PIN AD12 IOSTANDARD LVDS} [get_ports sysclk_p] +set_property -dict {PACKAGE_PIN AD11 IOSTANDARD LVDS} [get_ports sysclk_n] +create_clock -name sys_clk_200 -period 5.000 [get_ports sysclk_p] + +# Active-low CPU reset button. +set_property -dict {PACKAGE_PIN R19 IOSTANDARD LVCMOS33 PULLUP TRUE} [get_ports cpu_resetn] + +# MAX2771 on JA. This mapping is supplied by the design specification and also +# matches the official Digilent JA pin locations. +set_property -dict {PACKAGE_PIN U27 IOSTANDARD LVCMOS33} [get_ports rf_clk_pmod] +set_property -dict {PACKAGE_PIN U28 IOSTANDARD LVCMOS33} [get_ports {rf_data_pmod[0]}] +set_property -dict {PACKAGE_PIN T26 IOSTANDARD LVCMOS33} [get_ports {rf_data_pmod[1]}] +set_property -dict {PACKAGE_PIN T27 IOSTANDARD LVCMOS33} [get_ports {rf_data_pmod[2]}] +set_property -dict {PACKAGE_PIN T22 IOSTANDARD LVCMOS33} [get_ports {rf_data_pmod[3]}] +create_clock -name rf_sample_clk -period 30.547 -waveform {0.000 15.2735} [get_ports rf_clk_pmod] + +# MAX2771 clock-to-data timing is not supplied by the board manual or by the +# project specification. Do not invent it. Before hardware sign-off, replace +# RF_INPUT_MAX_NS and RF_INPUT_MIN_NS below with limits derived from the selected +# MAX2771 output mode plus measured cable/PMOD skew, then uncomment the commands. +# set_input_delay -clock rf_sample_clk -max RF_INPUT_MAX_NS [get_ports {rf_data_pmod[*]}] +# set_input_delay -clock rf_sample_clk -min RF_INPUT_MIN_NS [get_ports {rf_data_pmod[*]}] + +# Realtek RTL8211E-VL management and transmit-side RGMII, Bank 33 at 1.5 V. +set_property -dict {PACKAGE_PIN AF12 IOSTANDARD LVCMOS15 SLEW SLOW} [get_ports eth_mdc] +set_property -dict {PACKAGE_PIN AG12 IOSTANDARD LVCMOS15 PULLUP TRUE} [get_ports eth_mdio] +set_property -dict {PACKAGE_PIN AH24 IOSTANDARD LVCMOS33 SLEW SLOW} [get_ports eth_phyrst_n] +set_property -dict {PACKAGE_PIN AE10 IOSTANDARD LVCMOS15 SLEW FAST} [get_ports eth_txck] +set_property -dict {PACKAGE_PIN AJ12 IOSTANDARD LVCMOS15 SLEW FAST} [get_ports {eth_txd[0]}] +set_property -dict {PACKAGE_PIN AK11 IOSTANDARD LVCMOS15 SLEW FAST} [get_ports {eth_txd[1]}] +set_property -dict {PACKAGE_PIN AJ11 IOSTANDARD LVCMOS15 SLEW FAST} [get_ports {eth_txd[2]}] +set_property -dict {PACKAGE_PIN AK10 IOSTANDARD LVCMOS15 SLEW FAST} [get_ports {eth_txd[3]}] +set_property -dict {PACKAGE_PIN AK14 IOSTANDARD LVCMOS15 SLEW FAST} [get_ports eth_txctl] + +# The MMCM VCO is 1000 MHz (200 MHz * 5). CLKOUT0 and CLKOUT1 are 125 MHz; +# CLKOUT1 has +90 degrees = +2.000 ns. Vivado derives internal MMCM clocks. +# This explicit forwarded-clock definition constrains the PHY interface. +create_generated_clock -name rgmii_txc \ + -source [get_pins u_clock_gen/u_mmcm/CLKOUT1] -divide_by 1 [get_ports eth_txck] +create_generated_clock -name mdio_mdc \ + -source [get_pins u_clock_gen/u_mmcm/CLKOUT0] -divide_by 50 [get_ports eth_mdc] + +# RGMII v2.0 source-synchronous budget: the receiver requires 1.0 ns setup and +# 1.0 ns hold around the delayed TXC edge. The +2 ns FPGA clock phase centers the +# data eye because the board straps RTL8211E TXDLY off. +set_output_delay -clock rgmii_txc -max 1.000 [get_ports {eth_txd[*] eth_txctl}] +set_output_delay -clock rgmii_txc -min -1.000 [get_ports {eth_txd[*] eth_txctl}] + +# All RF<->Ethernet crossings are structurally confined to u_sample_fifo Gray +# pointers, u_drop_sync Gray counter, u_sample_pulse_sync, and reset synchronizers. +# There is intentionally no ordinary logic path between these asynchronous clocks. +set_clock_groups -asynchronous \ + -group [get_clocks rf_sample_clk] \ + -group [get_clocks -include_generated_clocks sys_clk_200] + +# Force the four MAX2771 sampling registers and all RGMII DDR registers into IOBs. +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *u_capture/sample_iob_reg*}] +set_property IOB TRUE [get_cells -hier -filter {NAME =~ *u_rgmii_tx*g_txd_oddr*u_oddr* || NAME =~ *u_rgmii_tx*u_ctl_oddr* || NAME =~ *u_rgmii_tx*u_txc_oddr*}] + +# Eight verified user LEDs. +set_property -dict {PACKAGE_PIN T28 IOSTANDARD LVCMOS33} [get_ports {led[0]}] +set_property -dict {PACKAGE_PIN V19 IOSTANDARD LVCMOS33} [get_ports {led[1]}] +set_property -dict {PACKAGE_PIN U30 IOSTANDARD LVCMOS33} [get_ports {led[2]}] +set_property -dict {PACKAGE_PIN U29 IOSTANDARD LVCMOS33} [get_ports {led[3]}] +set_property -dict {PACKAGE_PIN V20 IOSTANDARD LVCMOS33} [get_ports {led[4]}] +set_property -dict {PACKAGE_PIN V26 IOSTANDARD LVCMOS33} [get_ports {led[5]}] +set_property -dict {PACKAGE_PIN W24 IOSTANDARD LVCMOS33} [get_ports {led[6]}] +set_property -dict {PACKAGE_PIN W23 IOSTANDARD LVCMOS33} [get_ports {led[7]}] diff --git a/targets/genesys2/docs/hardware_assumptions.md b/targets/genesys2/docs/hardware_assumptions.md new file mode 100644 index 0000000..320747a --- /dev/null +++ b/targets/genesys2/docs/hardware_assumptions.md @@ -0,0 +1,48 @@ +# Hardware information and assumptions + +## Verified board information + +The pin source is Digilent's official [`Genesys-2-Master.xdc`](https://github.com/Digilent/digilent-xdc/blob/00a3404901f35aa9567b01ecb3f2c233b6efe9f4/Genesys-2-Master.xdc), commit `00a3404901f35aa9567b01ecb3f2c233b6efe9f4`. The official board definition names part `xc7k325tffg900-2` and the system oscillator as 200 MHz. The [Genesys 2 reference manual](https://digilent.com/reference/_media/reference/programmable-logic/genesys-2/genesys2_rm.pdf) identifies the PHY as RTL8211E-VL. + +| Function | FPGA pin | Standard | Notes | +|---|---|---|---| +| `sysclk_p/n` | AD12 / AD11 | LVDS | 200 MHz differential oscillator | +| `cpu_resetn` | R19 | LVCMOS33 | active-low board reset | +| MDC / MDIO | AF12 / AG12 | LVCMOS15 | PHY address strapped to 1 | +| RGMII TXC | AE10 | LVCMOS15 | Bank 33, 1.5 V | +| RGMII TXD[0:3] | AJ12, AK11, AJ11, AK10 | LVCMOS15 | in vector order | +| RGMII TXCTL | AK14 | LVCMOS15 | no TX error is generated | +| PHY reset | AH24 | LVCMOS33 | active low | +| PHY INTB / PMEB | AK16 / AK15 | LVCMOS18 | open-drain; not used by this design | +| RGMII RXC | AG10 | LVCMOS15 | receive path not implemented | +| RGMII RXCTL | AH11 | LVCMOS15 | receive path not implemented | +| RGMII RXD[0:3] | AJ14, AH14, AK13, AJ13 | LVCMOS15 | receive path not implemented | +| LEDs[0:7] | T28, V19, U30, U29, V20, V26, W24, W23 | LVCMOS33 | user status LEDs | + +The reference manual states that the board straps auto-negotiation on, advertises 10/100/1000 modes, selects PHY address `00001`, and selects no PHY-internal RGMII RX or TX delay. It also shows a 25 MHz PHY crystal; the FPGA does not supply the PHY reference clock. + +The RTL8211E datasheet requires PHYRSTB low for at least 10 ms and a further 30 ms before register access. The RTL implements both intervals. Link gating polls PHY-specific status register `0x11`; transmit is enabled only when bits `[15:14]=10` (1000 Mb/s), bit 13 (full duplex), bit 11 (resolved), and bit 10 (real-time link) are all set. + +## Assumptions from the specification + +- `rf_clk_pmod` is nominally 32.736 MHz. +- A four-bit word is valid on every configured sampling edge. +- Default capture is rising-edge and default packing is earlier-sample-high-nibble. +- JA is a 3.3 V interface and the MAX2771-side signals presented to JA are electrically compatible with LVCMOS33. +- Ethernet destination is broadcast and source is the fixed locally administered address `02:27:71:00:00:01`. + +## MAX2771 datasheet confirmation required + +- Confirm the selected MAX2771 digital-output mode really produces one complete four-bit word per RF clock edge. +- Confirm output voltage, drive strength, clock polarity, setup, hold, and clock-to-output limits for the exact supply and load. +- Confirm whether the default rising capture edge is the center of the valid data eye. Change `CAPTURE_RISING` only after that check. +- Confirm whether the four bits represent the desired channel/order. RTL records `[3:0]` literally and deliberately does not reinterpret it. + +## Measurement required + +- Measure MAX2771 clock-to-data timing at the Genesys 2 JA pins, including cable/adapter skew. Put the resulting min/max values into the commented `set_input_delay` commands in the XDC. +- Measure the four data-line skews if wiring is not length matched. +- Verify the forwarded RGMII TX clock is centered approximately 2.0 ns after TXD/TXCTL transitions at the PHY pins. The MMCM provides the nominal shift; board trace and I/O delay still deserve hardware measurement. +- Confirm clean 3.3 V levels, ground return, edge quality, and absence of overshoot at JA before long captures. + +No unverified board pin has been inserted into the constraints. diff --git a/targets/genesys2/docs/timing_constraints.md b/targets/genesys2/docs/timing_constraints.md new file mode 100644 index 0000000..a2885db --- /dev/null +++ b/targets/genesys2/docs/timing_constraints.md @@ -0,0 +1,42 @@ +# Timing constraints and closure + +## Clock plan + +| Clock | Source | Frequency / period | Use | +|---|---|---|---| +| `sys_clk_200` | AD12/AD11 LVDS oscillator | 200 MHz / 5.000 ns | MMCM input | +| `clk125_i` | MMCM VCO 1000 MHz, divide 8 | 125 MHz / 8.000 ns, 0° | packetizer, MAC, MDIO, RGMII data ODDR | +| `clk125_90_i` | same MMCM output divide | 125 MHz / 8.000 ns, +90° | forwarded RGMII TXC ODDR | +| `rgmii_txc` | ODDR from `clk125_90_i` | 125 MHz, +2.000 ns | PHY source-synchronous clock | +| `mdio_mdc` | counter from 125 MHz | 2.5 MHz | Clause-22 management | +| `rf_sample_clk` | U27 | 32.736 MHz / 30.547 ns | MAX2771 capture and FIFO write | + +The RTL8211E is strapped with TX delay disabled. Therefore the FPGA creates RGMII-ID source-clock skew: GMII/RGMII data changes from ODDRs clocked at 0°, and TXC changes from an ODDR clocked at +90°. At 125 MHz, 90° is 2.000 ns. RGMII TX output delays use the standard 1.0 ns setup/1.0 ns hold receive budget around that centered clock edge. + +## CDC exceptions + +The RF and Ethernet clocks are asynchronous. The only crossings are: + +- FIFO write/read Gray pointers, each through two `ASYNC_REG` stages; +- the Gray-coded cumulative drop counter, through two `ASYNC_REG` stages; +- a toggle-based sample-activity indicator, through two `ASYNC_REG` stages; +- reset synchronizers with asynchronous assertion and synchronous deassertion. + +The XDC asynchronous clock grouping is valid because no ordinary data/control logic crosses those groups. The multi-bit FIFO data is stored in true dual-clock block RAM and is read only under synchronized pointer control; it is not sampled as a combinational bus crossing. + +Vivado reports CDC-6 for the three intentionally Gray-coded buses even when it recognizes their two ASYNC_REG stages. The build script accepts only named FIFO/drop-counter instances and fails for any other Warning or Critical CDC object. + +## MAX2771 input delay boundary + +The clock is constrained to 30.547 ns and the input registers are forced into IOBs. The project does **not** claim RF input setup/hold sign-off yet: neither the task input nor the Genesys 2 documentation provides MAX2771 `tCO`, cable delay, or inter-bit skew. The XDC contains conspicuous placeholder commands rather than fabricated numbers. + +Before hardware sign-off: + +1. derive earliest/latest data arrival at each FPGA pin from the MAX2771 datasheet; +2. add measured cable/adapter and bit-to-bit skew; +3. replace the placeholder max/min values in the XDC; +4. rerun `make genesys2-build` and inspect + `build/genesys2/reports/timing_post_route.rpt`; +5. verify the eye with an oscilloscope or logic analyzer at JA. + +The current 2026.1 build routes successfully with setup WNS `+0.212 ns` and hold WNS `+0.090 ns` for constrained paths. These numbers do not replace the missing external RF input-delay model. diff --git a/targets/genesys2/rtl/ethernet_clock_gen_xilinx.sv b/targets/genesys2/rtl/ethernet_clock_gen_xilinx.sv new file mode 100644 index 0000000..9e0f3f3 --- /dev/null +++ b/targets/genesys2/rtl/ethernet_clock_gen_xilinx.sv @@ -0,0 +1,54 @@ +`default_nettype none + +// Genesys 2: 200 MHz differential oscillator -> 1000 MHz MMCM VCO -> +// 125 MHz GMII clock and a +90 degree (2.000 ns) RGMII TX clock. +module ethernet_clock_gen ( + input wire logic sysclk_p, + input wire logic sysclk_n, + input wire logic reset, + output logic eth_clk, + output logic eth_clk90, + output logic locked +); +`ifdef SYNTHESIS + wire logic clk200; + wire logic clkfb; + wire logic clkfb_buf; + wire logic clk125_i; + wire logic clk125_90_i; + wire logic mmcm_locked; + + IBUFDS #(.DIFF_TERM("FALSE"), .IBUF_LOW_PWR("TRUE")) u_sysclk_ibuf ( + .I(sysclk_p), .IB(sysclk_n), .O(clk200) + ); + MMCME2_BASE #( + .BANDWIDTH("OPTIMIZED"), + .CLKIN1_PERIOD(5.000), + .DIVCLK_DIVIDE(1), + .CLKFBOUT_MULT_F(5.000), + .CLKOUT0_DIVIDE_F(8.000), + .CLKOUT0_PHASE(0.000), + .CLKOUT1_DIVIDE(8), + .CLKOUT1_PHASE(90.000), + .STARTUP_WAIT("FALSE") + ) u_mmcm ( + .CLKIN1(clk200), .CLKFBIN(clkfb_buf), .RST(reset), .PWRDWN(1'b0), + .CLKFBOUT(clkfb), .CLKOUT0(clk125_i), .CLKOUT1(clk125_90_i), + .LOCKED(mmcm_locked), + .CLKOUT0B(), .CLKOUT1B(), .CLKOUT2(), .CLKOUT2B(), .CLKOUT3(), + .CLKOUT3B(), .CLKOUT4(), .CLKOUT5(), .CLKOUT6(), .CLKFBOUTB() + ); + BUFG u_fb_buf (.I(clkfb), .O(clkfb_buf)); + BUFG u_clk_buf (.I(clk125_i), .O(eth_clk)); + BUFG u_clk90_buf (.I(clk125_90_i), .O(eth_clk90)); + assign locked = mmcm_locked; +`else + // Top-level behavioral simulation supplies a 125 MHz clock on sysclk_p. + // Unit tests use GMII directly; this branch avoids a UNISIM dependency. + assign eth_clk = sysclk_p; + assign eth_clk90 = sysclk_p; + assign locked = !reset; +`endif +endmodule + +`default_nettype wire diff --git a/targets/genesys2/rtl/max2771_recorder_top.sv b/targets/genesys2/rtl/max2771_recorder_top.sv new file mode 100644 index 0000000..b063123 --- /dev/null +++ b/targets/genesys2/rtl/max2771_recorder_top.sv @@ -0,0 +1,172 @@ +`default_nettype none + +module max2771_recorder_top #( + parameter bit CAPTURE_RISING = 1'b1, + parameter bit EARLIER_SAMPLE_HIGH_NIBBLE = 1'b1, + parameter int unsigned FIFO_DEPTH = 16384 +) ( + input wire logic sysclk_p, + input wire logic sysclk_n, + input wire logic cpu_resetn, + input wire logic rf_clk_pmod, + input wire logic [3:0] rf_data_pmod, + + output logic eth_mdc, + inout wire logic eth_mdio, + output logic eth_phyrst_n, + output logic eth_txck, + output logic [3:0] eth_txd, + output logic eth_txctl, + output logic [7:0] led +); + localparam int unsigned FIFO_LEVEL_W = $clog2(FIFO_DEPTH) + 1; + logic eth_clk, eth_clk90, mmcm_locked; + logic eth_rst, rf_rst; + logic rf_fifo_clk; + logic fifo_w_en, fifo_full; + logic [68:0] fifo_w_data; + (* KEEP = "TRUE" *) logic [FIFO_LEVEL_W-1:0] fifo_w_level, fifo_r_level; + (* KEEP = "TRUE" *) logic [63:0] samples_seen, samples_captured, samples_dropped; + logic [63:0] dropped_samples_eth; + logic loss_latched_rf; + logic sample_activity_rf, sample_activity_eth; + logic fifo_rd_en, fifo_rd_valid, fifo_empty; + logic [68:0] fifo_rd_data; + (* KEEP = "TRUE" *) logic [63:0] fifo_overflow_attempts, fifo_underflow_attempts; + + logic phy_reset_complete; + logic mdio_start, mdio_write, mdio_busy, mdio_done, mdio_read_valid; + logic [4:0] mdio_phy_address, mdio_register_address; + logic [15:0] mdio_write_data, mdio_read_data; + logic mdio_i, mdio_o, mdio_oe; + logic phy_init_complete, link_ready; + (* KEEP = "TRUE" *) logic [63:0] link_down_events; + + logic mac_frame_start, mac_payload_valid, mac_payload_ready, mac_busy, mac_frame_done; + logic [10:0] mac_payload_length; + logic [7:0] mac_payload_data; + logic [7:0] gmii_txd; + logic gmii_tx_en, gmii_tx_er; + (* KEEP = "TRUE" *) logic [63:0] frames_transmitted, payload_bytes_transmitted; + logic [31:0] packet_sequence; + logic frame_activity_pulse; + logic [23:0] sample_led_stretch, frame_led_stretch; + logic [26:0] heartbeat; + + ethernet_clock_gen u_clock_gen ( + .sysclk_p(sysclk_p), .sysclk_n(sysclk_n), .reset(!cpu_resetn), + .eth_clk(eth_clk), .eth_clk90(eth_clk90), .locked(mmcm_locked) + ); + reset_sync u_eth_reset (.clk(eth_clk), .arst(!cpu_resetn || !mmcm_locked), .srst(eth_rst)); + + max2771_capture #(.CAPTURE_RISING(CAPTURE_RISING)) u_capture ( + .rf_clk_pmod(rf_clk_pmod), .rst(rf_rst), .rf_data_pmod(rf_data_pmod), + .fifo_full(fifo_full), .fifo_w_clk(rf_fifo_clk), .fifo_w_en(fifo_w_en), + .fifo_w_data(fifo_w_data), .samples_seen(samples_seen), + .samples_captured(samples_captured), .samples_dropped(samples_dropped), + .loss_latched(loss_latched_rf), .sample_activity_pulse(sample_activity_rf) + ); + reset_sync u_rf_reset (.clk(rf_fifo_clk), .arst(!cpu_resetn || !mmcm_locked), .srst(rf_rst)); + + async_fifo_gray #(.DATA_WIDTH(69), .DEPTH(FIFO_DEPTH)) u_sample_fifo ( + .w_clk(rf_fifo_clk), .w_rst(rf_rst), .w_en(fifo_w_en), .w_data(fifo_w_data), + .w_full(fifo_full), .w_level(fifo_w_level), + .overflow_attempt_count(fifo_overflow_attempts), + .r_clk(eth_clk), .r_rst(eth_rst), .r_en(fifo_rd_en), .r_data(fifo_rd_data), + .r_valid(fifo_rd_valid), .r_empty(fifo_empty), .r_level(fifo_r_level), + .underflow_attempt_count(fifo_underflow_attempts) + ); + + gray_counter_sync #(.WIDTH(64)) u_drop_sync ( + .src_clk(rf_fifo_clk), .src_rst(rf_rst), .src_binary(samples_dropped), + .dst_clk(eth_clk), .dst_rst(eth_rst), .dst_binary(dropped_samples_eth) + ); + pulse_sync u_sample_pulse_sync ( + .src_clk(rf_fifo_clk), .src_rst(rf_rst), .src_pulse(sample_activity_rf), + .dst_clk(eth_clk), .dst_rst(eth_rst), .dst_pulse(sample_activity_eth) + ); + + phy_reset_ctrl u_phy_reset ( + .clk(eth_clk), .rst(eth_rst), .phy_reset_n(eth_phyrst_n), + .reset_complete(phy_reset_complete) + ); + phy_init u_phy_init ( + .clk(eth_clk), .rst(eth_rst), .phy_reset_complete(phy_reset_complete), + .mdio_start(mdio_start), .mdio_write(mdio_write), + .mdio_phy_address(mdio_phy_address), + .mdio_register_address(mdio_register_address), .mdio_write_data(mdio_write_data), + .mdio_busy(mdio_busy), .mdio_done(mdio_done), .mdio_read_data(mdio_read_data), + .initialization_complete(phy_init_complete), .link_ready(link_ready), + .link_down_events(link_down_events) + ); + mdio_master u_mdio ( + .clk(eth_clk), .rst(eth_rst), .start(mdio_start), + .command_write(mdio_write), .phy_address(mdio_phy_address), + .register_address(mdio_register_address), .write_data(mdio_write_data), + .busy(mdio_busy), .done(mdio_done), .read_data(mdio_read_data), + .read_valid(mdio_read_valid), .mdc(eth_mdc), + .mdio_i(mdio_i), .mdio_o(mdio_o), .mdio_oe(mdio_oe) + ); +`ifdef SYNTHESIS + IOBUF u_mdio_iobuf (.I(mdio_o), .O(mdio_i), .T(!mdio_oe), .IO(eth_mdio)); +`else + assign eth_mdio = mdio_oe ? mdio_o : 1'bz; + assign mdio_i = eth_mdio; +`endif + + ethernet_packetizer #( + .FIFO_DEPTH(FIFO_DEPTH), .FULL_SAMPLES(2936), + .EARLIER_SAMPLE_HIGH_NIBBLE(EARLIER_SAMPLE_HIGH_NIBBLE) + ) u_packetizer ( + .clk(eth_clk), .rst(eth_rst), .link_ready(link_ready), + .dropped_samples_low(dropped_samples_eth[31:0]), + .fifo_empty(fifo_empty), .fifo_level(fifo_r_level), + .fifo_rd_en(fifo_rd_en), .fifo_rd_data(fifo_rd_data), + .fifo_rd_valid(fifo_rd_valid), .mac_frame_start(mac_frame_start), + .mac_payload_length(mac_payload_length), .mac_payload_data(mac_payload_data), + .mac_payload_valid(mac_payload_valid), .mac_payload_ready(mac_payload_ready), + .mac_busy(mac_busy), .mac_frame_done(mac_frame_done), + .packet_sequence(packet_sequence), .frame_activity_pulse(frame_activity_pulse) + ); + ethernet_tx_mac u_mac ( + .clk(eth_clk), .rst(eth_rst), .frame_start(mac_frame_start), + .payload_length(mac_payload_length), .payload_data(mac_payload_data), + .payload_valid(mac_payload_valid), .payload_ready(mac_payload_ready), + .gmii_txd(gmii_txd), .gmii_tx_en(gmii_tx_en), .gmii_tx_er(gmii_tx_er), + .busy(mac_busy), .frame_done(mac_frame_done), + .frames_transmitted(frames_transmitted), + .payload_bytes_transmitted(payload_bytes_transmitted) + ); + rgmii_tx u_rgmii_tx ( + .gmii_clk(eth_clk), .tx_clk90(eth_clk90), .gmii_txd(gmii_txd), + .gmii_tx_en(gmii_tx_en), .gmii_tx_er(gmii_tx_er), + .rgmii_txc(eth_txck), .rgmii_txd(eth_txd), .rgmii_tx_ctl(eth_txctl) + ); + + always_ff @(posedge eth_clk) begin + if (eth_rst) begin + sample_led_stretch <= 24'd0; + frame_led_stretch <= 24'd0; + heartbeat <= 27'd0; + end else begin + heartbeat <= heartbeat + 27'd1; + if (sample_activity_eth) sample_led_stretch <= 24'hffffff; + else if (sample_led_stretch != 24'd0) sample_led_stretch <= sample_led_stretch - 24'd1; + if (frame_activity_pulse) frame_led_stretch <= 24'hffffff; + else if (frame_led_stretch != 24'd0) frame_led_stretch <= frame_led_stretch - 24'd1; + end + end + + always_comb begin + led[0] = mmcm_locked && !eth_rst; + led[1] = link_ready; + led[2] = (sample_led_stretch != 24'd0); + led[3] = !fifo_empty; + led[4] = loss_latched_rf; + led[5] = (frame_led_stretch != 24'd0); + led[6] = phy_init_complete; + led[7] = heartbeat[26]; + end +endmodule + +`default_nettype wire diff --git a/targets/genesys2/rtl/phy_init_rtl8211e.sv b/targets/genesys2/rtl/phy_init_rtl8211e.sv new file mode 100644 index 0000000..7d61663 --- /dev/null +++ b/targets/genesys2/rtl/phy_init_rtl8211e.sv @@ -0,0 +1,85 @@ +`default_nettype none + +// The Genesys 2 straps the RTL8211E-VL for auto-negotiation and PHY address 1. +// After the mandatory post-reset settling time, this controller polls PHYSR +// (register 0x11) and permits transmit only for resolved 1000 Mb/s full duplex. +module phy_init #( + parameter int unsigned CLK_HZ = 125_000_000, + parameter int unsigned SETTLE_MS = 30, + parameter int unsigned POLL_MS = 100 +) ( + input wire logic clk, + input wire logic rst, + input wire logic phy_reset_complete, + output logic mdio_start, + output logic mdio_write, + output logic [4:0] mdio_phy_address, + output logic [4:0] mdio_register_address, + output logic [15:0] mdio_write_data, + input wire logic mdio_busy, + input wire logic mdio_done, + input wire logic [15:0] mdio_read_data, + output logic initialization_complete, + output logic link_ready, + output logic [63:0] link_down_events +); + localparam longint unsigned SETTLE_CYCLES = (CLK_HZ / 1000) * SETTLE_MS; + localparam longint unsigned POLL_CYCLES = (CLK_HZ / 1000) * POLL_MS; + localparam int unsigned TIMER_W = $clog2(((POLL_CYCLES > SETTLE_CYCLES) ? POLL_CYCLES : SETTLE_CYCLES) + 1); + typedef enum logic [2:0] {I_WAIT_RESET, I_SETTLE, I_START, I_WAIT_MDIO, I_POLL_WAIT} istate_t; + istate_t state; + logic [TIMER_W-1:0] timer; + logic new_link_ready; + + always_comb begin + mdio_write = 1'b0; + mdio_phy_address = 5'd1; + mdio_register_address = 5'h11; + mdio_write_data = 16'd0; + new_link_ready = (mdio_read_data[15:14] == 2'b10) && + mdio_read_data[13] && mdio_read_data[11] && mdio_read_data[10]; + end + + always_ff @(posedge clk) begin + if (rst) begin + state <= I_WAIT_RESET; + timer <= '0; + mdio_start <= 1'b0; + initialization_complete <= 1'b0; + link_ready <= 1'b0; + link_down_events <= 64'd0; + end else begin + mdio_start <= 1'b0; + case (state) + I_WAIT_RESET: begin + link_ready <= 1'b0; + if (phy_reset_complete) begin timer <= '0; state <= I_SETTLE; end + end + I_SETTLE: begin + if (timer == SETTLE_CYCLES-1) begin timer <= '0; state <= I_START; end + else timer <= timer + {{(TIMER_W-1){1'b0}},1'b1}; + end + I_START: begin + if (!mdio_busy) begin mdio_start <= 1'b1; state <= I_WAIT_MDIO; end + end + I_WAIT_MDIO: begin + if (mdio_done) begin + initialization_complete <= 1'b1; + if (link_ready && !new_link_ready) + link_down_events <= link_down_events + 64'd1; + link_ready <= new_link_ready; + timer <= '0; + state <= I_POLL_WAIT; + end + end + I_POLL_WAIT: begin + if (timer == POLL_CYCLES-1) begin timer <= '0; state <= I_START; end + else timer <= timer + {{(TIMER_W-1){1'b0}},1'b1}; + end + default: state <= I_WAIT_RESET; + endcase + end + end +endmodule + +`default_nettype wire diff --git a/targets/genesys2/rtl/rgmii_tx_xilinx.sv b/targets/genesys2/rtl/rgmii_tx_xilinx.sv new file mode 100644 index 0000000..3db79f3 --- /dev/null +++ b/targets/genesys2/rtl/rgmii_tx_xilinx.sv @@ -0,0 +1,39 @@ +`default_nettype none + +module rgmii_tx ( + input wire logic gmii_clk, + input wire logic tx_clk90, + input wire logic [7:0] gmii_txd, + input wire logic gmii_tx_en, + input wire logic gmii_tx_er, + output logic rgmii_txc, + output logic [3:0] rgmii_txd, + output logic rgmii_tx_ctl +); +`ifdef SYNTHESIS + genvar i; + generate for (i = 0; i < 4; i = i + 1) begin : g_txd_oddr + ODDR #(.DDR_CLK_EDGE("SAME_EDGE"), .INIT(1'b0), .SRTYPE("SYNC")) u_oddr ( + .C(gmii_clk), .CE(1'b1), .D1(gmii_txd[i]), .D2(gmii_txd[i+4]), + .Q(rgmii_txd[i]), .R(1'b0), .S(1'b0) + ); + end endgenerate + ODDR #(.DDR_CLK_EDGE("SAME_EDGE"), .INIT(1'b0), .SRTYPE("SYNC")) u_ctl_oddr ( + .C(gmii_clk), .CE(1'b1), .D1(gmii_tx_en), .D2(gmii_tx_en ^ gmii_tx_er), + .Q(rgmii_tx_ctl), .R(1'b0), .S(1'b0) + ); + ODDR #(.DDR_CLK_EDGE("SAME_EDGE"), .INIT(1'b0), .SRTYPE("SYNC")) u_txc_oddr ( + .C(tx_clk90), .CE(1'b1), .D1(1'b1), .D2(1'b0), + .Q(rgmii_txc), .R(1'b0), .S(1'b0) + ); +`else + // Edge-select behavioral model; hardware uses the ODDR primitives above. + always_comb begin + rgmii_txd = gmii_clk ? gmii_txd[3:0] : gmii_txd[7:4]; + rgmii_tx_ctl = gmii_clk ? gmii_tx_en : (gmii_tx_en ^ gmii_tx_er); + rgmii_txc = tx_clk90; + end +`endif +endmodule + +`default_nettype wire diff --git a/targets/genesys2/scripts/build_vivado.tcl b/targets/genesys2/scripts/build_vivado.tcl new file mode 100644 index 0000000..0879615 --- /dev/null +++ b/targets/genesys2/scripts/build_vivado.tcl @@ -0,0 +1,93 @@ +# Non-project Vivado build for the Genesys 2 MAX2771 recorder. +# Run from repository root: +# vivado -mode batch -source targets/genesys2/scripts/build_vivado.tcl + +set script_dir [file normalize [file dirname [info script]]] +set target_dir [file normalize [file join $script_dir ..]] +set root_dir [file normalize [file join $target_dir .. ..]] +set build_dir [file join $root_dir build genesys2 vivado] +set report_dir [file join $root_dir build genesys2 reports] +file mkdir $build_dir +file mkdir $report_dir + +set part xc7k325tffg900-2 +set top max2771_recorder_top +create_project -in_memory -part $part +set_property target_language Verilog [current_project] +set_property default_lib work [current_project] +set_property verilog_define {SYNTHESIS=1} [current_fileset] + +set common_rtl [lsort [glob -nocomplain [file join $root_dir rtl common *.sv]]] +set target_rtl [lsort [glob -nocomplain [file join $target_dir rtl *.sv]]] +set rtl_files [concat $common_rtl $target_rtl] +if {[llength $rtl_files] == 0} { error "No RTL files found" } +read_verilog -sv $rtl_files +read_xdc [file join $target_dir constraints genesys2_max2771_recorder.xdc] + +synth_design -top $top -part $part -flatten_hierarchy rebuilt +write_checkpoint -force [file join $build_dir post_synth.dcp] +report_utilization -file [file join $report_dir post_synth_utilization.rpt] +report_clocks -file [file join $report_dir clocks.rpt] +report_clock_utilization -file [file join $report_dir clock_utilization.rpt] +check_timing -verbose -file [file join $report_dir check_timing_post_synth.rpt] +report_cdc -details -file [file join $report_dir cdc_post_synth.rpt] + +# Fail for registers lacking a clock. I/O delay omissions are separately and +# deliberately documented because the MAX2771 tCO/board skew is not yet known. +set no_clock_text [check_timing -override_defaults no_clock -return_string] +if {[regexp -nocase {([1-9][0-9]*) register[^\n]*no clock} $no_clock_text]} { + error "Unconstrained sequential clocks detected; see check_timing_post_synth.rpt" +} + +# Vivado exposes CDC violations as objects after report_cdc. Any Critical or +# Warning CDC not waived by structural ASYNC_REG recognition stops the build. +if {[llength [info commands get_cdc_violations]] != 0} { + set major_cdc [get_cdc_violations -quiet -filter {SEVERITY == "Critical" || SEVERITY == "Warning"}] + set unexpected_cdc 0 + foreach violation $major_cdc { + # CDC-6 is Vivado's expected classification for a multi-bit Gray bus + # whose first two destination stages carry ASYNC_REG. Only the three + # named Gray synchronizers are accepted; every other warning is fatal. + set description [report_property -return_string $violation] + if {![regexp {u_sample_fifo/(wgray_rsync1|rgray_wsync1)|u_drop_sync/gray_sync1} $description]} { + puts stderr "Unexpected CDC violation:\n$description" + set unexpected_cdc 1 + } + } + if {$unexpected_cdc} { error "Major unexpected CDC warnings detected; see cdc_post_synth.rpt" } +} + +opt_design +place_design +phys_opt_design +write_checkpoint -force [file join $build_dir post_place.dcp] +report_timing_summary -delay_type min_max -report_unconstrained -check_timing_verbose \ + -file [file join $report_dir timing_post_place.rpt] + +route_design +phys_opt_design +write_checkpoint -force [file join $build_dir post_route.dcp] +report_timing_summary -delay_type min_max -report_unconstrained -check_timing_verbose \ + -file [file join $report_dir timing_post_route.rpt] +report_timing -delay_type max -max_paths 50 -sort_by group \ + -file [file join $report_dir setup_paths.rpt] +report_timing -delay_type min -max_paths 50 -sort_by group \ + -file [file join $report_dir hold_paths.rpt] +report_utilization -hierarchical -file [file join $report_dir utilization_post_route.rpt] +report_methodology -file [file join $report_dir methodology_post_route.rpt] +report_drc -file [file join $report_dir drc_post_route.rpt] + +set setup_paths [get_timing_paths -quiet -delay_type max -max_paths 1] +set hold_paths [get_timing_paths -quiet -delay_type min -max_paths 1] +if {[llength $setup_paths] == 0 || [llength $hold_paths] == 0} { + error "Timing paths unavailable; clocks may be unconstrained" +} +set setup_slack [get_property SLACK [lindex $setup_paths 0]] +set hold_slack [get_property SLACK [lindex $hold_paths 0]] +if {$setup_slack < 0.0 || $hold_slack < 0.0} { + error "Timing failed: setup WNS=$setup_slack ns, hold WNS=$hold_slack ns" +} + +write_bitstream -force [file join $build_dir max2771_recorder.bit] +puts "BUILD PASSED: setup WNS=$setup_slack ns, hold WNS=$hold_slack ns" +puts "Bitstream: [file join $build_dir max2771_recorder.bit]" diff --git a/targets/genesys2/scripts/program_fpga.tcl b/targets/genesys2/scripts/program_fpga.tcl new file mode 100644 index 0000000..ed384e9 --- /dev/null +++ b/targets/genesys2/scripts/program_fpga.tcl @@ -0,0 +1,24 @@ +# Run: vivado -mode batch -source targets/genesys2/scripts/program_fpga.tcl -tclargs [bitstream] +set script_dir [file normalize [file dirname [info script]]] +set target_dir [file normalize [file join $script_dir ..]] +set root_dir [file normalize [file join $target_dir .. ..]] +if {$argc > 0} { + set bitstream [file normalize [lindex $argv 0]] +} else { + set bitstream [file join $root_dir build genesys2 vivado max2771_recorder.bit] +} +if {![file exists $bitstream]} { error "Bitstream not found: $bitstream" } + +open_hw_manager +connect_hw_server +open_hw_target +set devices [get_hw_devices -quiet -filter {PART =~ "xc7k325t*"}] +if {[llength $devices] == 0} { error "No xc7k325t JTAG device found" } +set device [lindex $devices 0] +current_hw_device $device +refresh_hw_device -update_hw_probes false $device +set_property PROGRAM.FILE $bitstream $device +program_hw_devices $device +refresh_hw_device $device +puts "Programmed $device with $bitstream" +close_hw_manager diff --git a/tb/common/tb_async_fifo_gray.sv b/tb/common/tb_async_fifo_gray.sv new file mode 100644 index 0000000..d31a27f --- /dev/null +++ b/tb/common/tb_async_fifo_gray.sv @@ -0,0 +1,103 @@ +`timescale 1ns/1ps +`default_nettype none + +module tb_async_fifo_gray; + localparam int DEPTH = 16; + logic w_clk=0, r_clk=0, w_rst=1, r_rst=1; + logic w_en=0, r_en=0; + logic [15:0] w_data=0, r_data; + logic w_full, r_empty, r_valid; + logic [$clog2(DEPTH):0] w_level, r_level; + logic [63:0] overflow_count, underflow_count; + logic [15:0] expected [0:20000]; + integer writes=0, reads=0, errors=0; + integer cycles; + + always #3.5 w_clk = ~w_clk; + always #5.5 r_clk = ~r_clk; + + async_fifo_gray #(.DATA_WIDTH(16), .DEPTH(DEPTH)) dut ( + .w_clk(w_clk), .w_rst(w_rst), .w_en(w_en), .w_data(w_data), + .w_full(w_full), .w_level(w_level), .overflow_attempt_count(overflow_count), + .r_clk(r_clk), .r_rst(r_rst), .r_en(r_en), .r_data(r_data), + .r_valid(r_valid), .r_empty(r_empty), .r_level(r_level), + .underflow_attempt_count(underflow_count) + ); + + always @(posedge w_clk) begin + if (!w_rst && w_en && !w_full) begin + expected[writes] = w_data; + writes = writes + 1; + end + end + always @(posedge r_clk) begin + if (!r_rst && r_valid) begin + if (r_data !== expected[reads]) begin + $error("FIFO mismatch at read %0d: got %04x expected %04x", reads, r_data, expected[reads]); + errors = errors + 1; + end + reads = reads + 1; + end + end + + initial begin + repeat (4) @(posedge w_clk); + w_rst = 0; + repeat (3) @(posedge r_clk); + r_rst = 0; + + // Fill through the full boundary and intentionally attempt overflow. + @(negedge w_clk); w_en=1; + repeat (DEPTH+8) begin + @(negedge w_clk); w_data = w_data + 16'd1; + end + @(negedge w_clk); w_en=0; + if (!w_full) $fatal(1, "FIFO did not assert full"); + + // Drain through empty and intentionally attempt underflow. + @(negedge r_clk); r_en=1; + repeat (DEPTH+12) @(negedge r_clk); + @(negedge r_clk); r_en=0; + repeat (4) @(posedge r_clk); + if (!r_empty) $fatal(1, "FIFO did not assert empty"); + + // Random unrelated-clock traffic for wrap-around and simultaneous use. + fork + begin + for (cycles=0; cycles<5000; cycles=cycles+1) begin + @(negedge w_clk); + w_en = ($urandom_range(0,99) < 68); + w_data = w_data + 16'd1; + end + @(negedge w_clk); w_en=0; + end + begin + repeat (5000) begin + @(negedge r_clk); + r_en = ($urandom_range(0,99) < 54); + end + @(negedge r_clk); r_en=0; + end + join + + // Final drain. + r_en=1; + while (reads < writes) @(negedge r_clk); + r_en=0; + repeat (4) @(posedge r_clk); + if (errors != 0) $fatal(1, "FIFO scoreboard saw %0d errors", errors); + if (overflow_count == 0) $fatal(1, "overflow attempt counter stayed zero"); + if (underflow_count == 0) $fatal(1, "underflow attempt counter stayed zero"); + if (reads != writes) $fatal(1, "writes=%0d reads=%0d", writes, reads); + $display("PASS tb_async_fifo_gray: %0d ordered words, overflow=%0d underflow=%0d", + reads, overflow_count, underflow_count); + $finish; + end + + initial begin + #500000; + $fatal(1, "FIFO test timeout"); + end +endmodule + +`default_nettype wire diff --git a/tb/common/tb_capture_overflow.sv b/tb/common/tb_capture_overflow.sv new file mode 100644 index 0000000..751d5de --- /dev/null +++ b/tb/common/tb_capture_overflow.sv @@ -0,0 +1,49 @@ +`timescale 1ns/1ps +`default_nettype none + +module tb_capture_overflow; + localparam int DEPTH=16; + logic rf_clk=0, r_clk=0, rst=1; + logic [3:0] rf_data=0; + logic fifo_clk,w_en,w_full,r_en=0,r_valid,r_empty; + logic [68:0] w_data,r_data; + logic [$clog2(DEPTH):0] wl,rl; + logic [63:0] seen,captured,dropped,ovf,udf; + logic loss,activity; + integer i,read_count=0; + always #15.2735 rf_clk=~rf_clk; + always #4 r_clk=~r_clk; + + max2771_capture cap(.rf_clk_pmod(rf_clk),.rst(rst),.rf_data_pmod(rf_data), + .fifo_full(w_full),.fifo_w_clk(fifo_clk),.fifo_w_en(w_en),.fifo_w_data(w_data), + .samples_seen(seen),.samples_captured(captured),.samples_dropped(dropped), + .loss_latched(loss),.sample_activity_pulse(activity)); + async_fifo_gray #(.DATA_WIDTH(69),.DEPTH(DEPTH)) fifo( + .w_clk(fifo_clk),.w_rst(rst),.w_en(w_en),.w_data(w_data),.w_full(w_full), + .w_level(wl),.overflow_attempt_count(ovf),.r_clk(r_clk),.r_rst(rst), + .r_en(r_en),.r_data(r_data),.r_valid(r_valid),.r_empty(r_empty), + .r_level(rl),.underflow_attempt_count(udf)); + + always @(posedge r_clk) if(r_valid) begin + if(r_data[67:4]!==read_count || r_data[3:0]!==((read_count+1)&15)) + $fatal(1,"overwrite/order failure read=%0d entry index=%0d data=%x",read_count,r_data[67:4],r_data[3:0]); + read_count=read_count+1; + end + + initial begin + repeat(3) @(posedge rf_clk); rst=0; + for(i=0;i<48;i=i+1) begin @(negedge rf_clk); rf_data=(i+1)&15; end + @(posedge rf_clk); #1; + if(!loss || dropped==0 || ovf==0 || dropped!==ovf) + $fatal(1,"overflow status failure loss=%b dropped=%0d attempts=%0d",loss,dropped,ovf); + r_en=1; + wait(read_count==DEPTH); + r_en=0; + if(captured>1)^32'hedb88320) : (c>>1); + crc_byte=c; + end + endfunction + + initial begin + payload[0]=8'hde; payload[1]=8'had; payload[2]=8'hbe; payload[3]=8'hef; + repeat(4) @(posedge clk); rst=0; + @(posedge clk); frame_start=1; + @(posedge clk); frame_start=0; + wait(frame_done); + @(posedge clk); + + if (cap_count != 72) $fatal(1, "frame byte count %0d expected 72", cap_count); + for (i=0;i<7;i=i+1) if(captured[i]!==8'h55) $fatal(1,"bad preamble byte %0d",i); + if(captured[7]!==8'hd5) $fatal(1,"bad SFD"); + for (i=0;i<6;i=i+1) body[i]=8'hff; + body[6]=8'h02; body[7]=8'h27; body[8]=8'h71; body[9]=8'h00; body[10]=8'h00; body[11]=8'h01; + body[12]=8'h88; body[13]=8'hb5; + body[14]=8'hde; body[15]=8'had; body[16]=8'hbe; body[17]=8'hef; + for(i=18;i<60;i=i+1) body[i]=8'h00; + for(i=0;i<60;i=i+1) if(captured[8+i]!==body[i]) + $fatal(1,"body mismatch byte %0d got %02x expected %02x",i,captured[8+i],body[i]); + reference_crc=32'hffffffff; + for(i=0;i<60;i=i+1) reference_crc=crc_byte(reference_crc,body[i]); + reference_crc=~reference_crc; + if(captured[68]!==reference_crc[7:0] || captured[69]!==reference_crc[15:8] || + captured[70]!==reference_crc[23:16] || captured[71]!==reference_crc[31:24]) + $fatal(1,"FCS mismatch got %02x%02x%02x%02x expected %08x", + captured[71],captured[70],captured[69],captured[68],reference_crc); + if(frames!==64'd1 || payload_bytes!==64'd4) $fatal(1,"MAC counters incorrect"); + + // Reset while idle, then send another complete frame. No TX_EN fragment + // may appear during reset or before the new preamble. + rst=1; payload_pos=0; cap_count=0; + repeat(3) begin @(posedge clk); if(gmii_tx_en) $fatal(1,"TX_EN asserted during idle reset"); end + rst=0; + repeat(2) @(posedge clk); + frame_start=1; @(posedge clk); frame_start=0; + wait(frame_done); @(posedge clk); + if(cap_count!=72) $fatal(1,"post-reset frame malformed: %0d bytes",cap_count); + for(i=0;i<7;i=i+1) if(captured[i]!==8'h55) $fatal(1,"post-reset preamble byte %0d",i); + reference_crc=32'hffffffff; + for(i=0;i<60;i=i+1) reference_crc=crc_byte(reference_crc,body[i]); + reference_crc=~reference_crc; + if({captured[71],captured[70],captured[69],captured[68]}!==reference_crc) + $fatal(1,"post-reset FCS mismatch"); + $display("PASS tb_ethernet_tx_mac: frame/padding/FCS/IFG and reset-between-frames verified"); + $finish; + end + initial begin #10000; $fatal(1,"MAC test timeout"); end +endmodule + +`default_nettype wire diff --git a/tb/common/tb_recorder_stream.sv b/tb/common/tb_recorder_stream.sv new file mode 100644 index 0000000..3ff46dc --- /dev/null +++ b/tb/common/tb_recorder_stream.sv @@ -0,0 +1,125 @@ +`timescale 1ns/1ps +`default_nettype none + +module tb_recorder_stream; + localparam int FIFO_DEPTH=8192; + localparam int FRAME_BYTES=1526; + logic w_clk=0, clk=0, w_rst=1, rst=1; + logic w_en=0, w_full, r_en, r_valid, r_empty; + logic [68:0] w_data=0, r_data; + logic [$clog2(FIFO_DEPTH):0] w_level, r_level; + logic [63:0] ovf, udf; + logic frame_start, payload_valid, payload_ready, mac_busy, frame_done; + logic [10:0] payload_length; + logic [7:0] payload_data, gmii_txd; + logic gmii_tx_en, gmii_tx_er; + logic [31:0] packet_sequence; + logic activity; + logic [63:0] frames, payload_bytes; + byte captured [0:4095]; + integer cap_count=0; + integer produced=0; + integer i, f, base, data_base; + logic [31:0] crc_ref, fcs_seen; + logic [31:0] seq_seen; + logic [63:0] index_seen; + logic [15:0] count_seen; + + always #5 w_clk=~w_clk; // 100 MHz producer, unrelated to reader + always #4 clk=~clk; // 125 MHz Ethernet + + async_fifo_gray #(.DATA_WIDTH(69),.DEPTH(FIFO_DEPTH)) fifo ( + .w_clk(w_clk),.w_rst(w_rst),.w_en(w_en),.w_data(w_data),.w_full(w_full), + .w_level(w_level),.overflow_attempt_count(ovf),.r_clk(clk),.r_rst(rst), + .r_en(r_en),.r_data(r_data),.r_valid(r_valid),.r_empty(r_empty), + .r_level(r_level),.underflow_attempt_count(udf) + ); + ethernet_packetizer #(.FIFO_DEPTH(FIFO_DEPTH),.FULL_SAMPLES(2936)) packetizer ( + .clk(clk),.rst(rst),.link_ready(1'b1),.dropped_samples_low(32'd5), + .fifo_empty(r_empty),.fifo_level(r_level),.fifo_rd_en(r_en), + .fifo_rd_data(r_data),.fifo_rd_valid(r_valid),.mac_frame_start(frame_start), + .mac_payload_length(payload_length),.mac_payload_data(payload_data), + .mac_payload_valid(payload_valid),.mac_payload_ready(payload_ready), + .mac_busy(mac_busy),.mac_frame_done(frame_done), + .packet_sequence(packet_sequence),.frame_activity_pulse(activity) + ); + ethernet_tx_mac mac ( + .clk(clk),.rst(rst),.frame_start(frame_start),.payload_length(payload_length), + .payload_data(payload_data),.payload_valid(payload_valid),.payload_ready(payload_ready), + .gmii_txd(gmii_txd),.gmii_tx_en(gmii_tx_en),.gmii_tx_er(gmii_tx_er), + .busy(mac_busy),.frame_done(frame_done),.frames_transmitted(frames), + .payload_bytes_transmitted(payload_bytes) + ); + + always @(posedge clk) if (!rst && gmii_tx_en) begin + captured[cap_count]=gmii_txd; + cap_count=cap_count+1; + if(gmii_tx_er) $fatal(1,"GMII underflow/error in stream test"); + end + + function automatic [31:0] crc_byte(input [31:0] cin,input [7:0] din); + reg[31:0] c; integer b; + begin c=cin; for(b=0;b<8;b=b+1) + c=(c[0]^din[b])?((c>>1)^32'hedb88320):(c>>1); crc_byte=c; end + endfunction + + initial begin + repeat(5) @(posedge w_clk); w_rst=0; + repeat(5) @(posedge clk); rst=0; + // Supply exactly two contiguous normal frames plus CDC slack. + for(produced=0;produced<5872;produced=produced+1) begin + @(negedge w_clk); + while(w_full) @(negedge w_clk); + w_en=1; + w_data={1'b0,64'(produced),4'(produced & 15)}; + end + @(negedge w_clk); w_en=0; + wait(packet_sequence==32'd2); + repeat(4) @(posedge clk); + if(cap_count!=2*FRAME_BYTES) $fatal(1,"captured %0d bytes expected %0d",cap_count,2*FRAME_BYTES); + + for(f=0;f<2;f=f+1) begin + base=f*FRAME_BYTES; + for(i=0;i<7;i=i+1) if(captured[base+i]!==8'h55) $fatal(1,"frame %0d preamble",f); + if(captured[base+7]!==8'hd5 || captured[base+20]!==8'h88 || captured[base+21]!==8'hb5) + $fatal(1,"frame %0d L2 header",f); + if(captured[base+22]!=="M" || captured[base+23]!=="X" || + captured[base+24]!=="2" || captured[base+25]!=="7") $fatal(1,"frame %0d magic",f); + if(captured[base+26]!==8'd1 || captured[base+28]!==8'd0 || captured[base+29]!==8'd32) + $fatal(1,"frame %0d protocol constants",f); + seq_seen={captured[base+30],captured[base+31],captured[base+32],captured[base+33]}; + index_seen={captured[base+34],captured[base+35],captured[base+36],captured[base+37], + captured[base+38],captured[base+39],captured[base+40],captured[base+41]}; + count_seen={captured[base+42],captured[base+43]}; + if(seq_seen!==f || index_seen!==64'(f*2936) || count_seen!==16'd2936) + $fatal(1,"frame %0d metadata seq=%0d index=%0d count=%0d",f,seq_seen,index_seen,count_seen); + if(captured[base+44]!==8'd4 || captured[base+45]!==8'd1) + $fatal(1,"frame %0d packing metadata",f); + // First frame carries reset+link flags; second carries none. + if((f==0 && captured[base+27]!==8'h07) || (f==1 && captured[base+27]!==8'h00)) + $fatal(1,"frame %0d flags %02x",f,captured[base+27]); + if ({captured[base+46],captured[base+47],captured[base+48],captured[base+49]} !== 32'd5) + $fatal(1,"frame %0d dropped-count header",f); + data_base=base+54; + for(i=0;i<2936;i=i+1) begin + if((i&1)==0) begin + if(captured[data_base+(i>>1)][7:4]!==((f*2936+i)&15)) + $fatal(1,"frame %0d sample %0d high nibble",f,i); + end else if(captured[data_base+(i>>1)][3:0]!==((f*2936+i)&15)) + $fatal(1,"frame %0d sample %0d low nibble",f,i); + end + crc_ref=32'hffffffff; + for(i=8;i<1522;i=i+1) crc_ref=crc_byte(crc_ref,captured[base+i]); + crc_ref=~crc_ref; + fcs_seen={captured[base+1525],captured[base+1524],captured[base+1523],captured[base+1522]}; + if(fcs_seen!==crc_ref) $fatal(1,"frame %0d FCS got %08x expected %08x",f,fcs_seen,crc_ref); + end + if(frames!==64'd2 || payload_bytes!==64'd3000 || udf!==64'd0) + $fatal(1,"counter failure frames=%0d payload=%0d underflow=%0d",frames,payload_bytes,udf); + $display("PASS tb_recorder_stream: two full frames, 5872 samples, headers/index/sequence/FCS verified"); + $finish; + end + initial begin #1000000; $fatal(1,"stream test timeout"); end +endmodule + +`default_nettype wire