Implement generic MAX2771 raw Ethernet recorder
This commit is contained in:
+25
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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]]
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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 |
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,571 @@
|
|||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <getopt.h>
|
||||||
|
#include <linux/if_packet.h>
|
||||||
|
#include <linux/if_ether.h>
|
||||||
|
#include <net/if.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#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;n<bd->hdr.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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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]]
|
||||||
@@ -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]]
|
||||||
Executable
+28
@@ -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
|
||||||
@@ -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."
|
||||||
@@ -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.
|
||||||
@@ -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 <MEASURED_MAX_NS> [get_ports {rf_data_pmod[*]}]
|
||||||
|
# set_input_delay -clock rf_clk_pmod -min <MEASURED_MIN_NS> [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.
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
+154
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
+75
@@ -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/<USER_PERIOD>/$rf_clock_period_ns/g" "$pt_sdc"
|
||||||
|
sed -n '1,$p' "$constraint_file"
|
||||||
|
} > "$build_dir/${project}_merged.sdc"
|
||||||
|
if grep -q '<USER_PERIOD>' "$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"
|
||||||
@@ -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"
|
||||||
+35
@@ -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"(?<![A-Za-z0-9_])-\d+(?:\.\d+)?", summary):
|
||||||
|
raise SystemExit("negative setup/hold slack in Clock Relationship Summary")
|
||||||
|
for required in ("eth_clk", "rf_clk_pmod"):
|
||||||
|
if required not in text:
|
||||||
|
raise SystemExit(f"required clock {required!r} is absent from timing report")
|
||||||
|
print(f"Timing report accepted: {path}")
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate an Efinity project XML without a GUI/project database dependency."""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import xml.dom.minidom
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
TARGET = Path(__file__).resolve().parents[1]
|
||||||
|
ROOT = TARGET.parents[1]
|
||||||
|
BUILD = Path(os.environ.get("EFINIX_BUILD_DIR", TARGET / "build")).resolve()
|
||||||
|
PROJECT = os.environ.get("EFINITY_PROJECT", "ti375_max2771_recorder")
|
||||||
|
TOP = os.environ.get("EFINITY_TOP", "ti375_max2771_recorder_top")
|
||||||
|
DEVICE = os.environ.get("EFINIX_DEVICE", "Ti375C529")
|
||||||
|
TIMING_MODEL = os.environ.get("EFINIX_TIMING_MODEL", "C4")
|
||||||
|
EFINITY_HOME = Path(os.environ["EFINITY_HOME"]).resolve()
|
||||||
|
|
||||||
|
version_file = EFINITY_HOME / "scripts" / "sw_version.txt"
|
||||||
|
SW_VERSION = version_file.read_text(encoding="utf-8").splitlines()[0].strip()
|
||||||
|
|
||||||
|
sources = [
|
||||||
|
ROOT / "rtl" / "common" / "reset_sync.sv",
|
||||||
|
ROOT / "rtl" / "common" / "pulse_sync.sv",
|
||||||
|
ROOT / "rtl" / "common" / "gray_counter_sync.sv",
|
||||||
|
ROOT / "rtl" / "common" / "async_fifo_gray.sv",
|
||||||
|
ROOT / "rtl" / "common" / "max2771_capture.sv",
|
||||||
|
ROOT / "rtl" / "common" / "ethernet_crc32.sv",
|
||||||
|
ROOT / "rtl" / "common" / "ethernet_packetizer.sv",
|
||||||
|
ROOT / "rtl" / "common" / "ethernet_tx_mac.sv",
|
||||||
|
ROOT / "rtl" / "common" / "phy_reset_ctrl.sv",
|
||||||
|
ROOT / "rtl" / "common" / "mdio_master.sv",
|
||||||
|
TARGET / "rtl" / "phy_init_rtl8211f.sv",
|
||||||
|
TARGET / "rtl" / "rgmii_tx_efinix.sv",
|
||||||
|
TARGET / "rtl" / "ti375_max2771_recorder_top.sv",
|
||||||
|
TARGET / "rtl" / "ti375_ethernet_selftest_top.sv",
|
||||||
|
]
|
||||||
|
for source in sources:
|
||||||
|
if not source.is_file():
|
||||||
|
raise SystemExit(f"missing RTL source: {source}")
|
||||||
|
|
||||||
|
ns = "http://www.efinixinc.com/enf_proj"
|
||||||
|
root = ET.Element("efx:project", {
|
||||||
|
"xmlns:efx": ns,
|
||||||
|
"name": PROJECT,
|
||||||
|
"location": str(BUILD),
|
||||||
|
"sw_version": SW_VERSION,
|
||||||
|
"last_change_date": "Date : " + datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||||
|
})
|
||||||
|
device_info = ET.SubElement(root, "efx:device_info")
|
||||||
|
ET.SubElement(device_info, "efx:family", name="Titanium")
|
||||||
|
ET.SubElement(device_info, "efx:device", name=DEVICE)
|
||||||
|
ET.SubElement(device_info, "efx:timing_model", name=TIMING_MODEL)
|
||||||
|
|
||||||
|
design_info = ET.SubElement(root, "efx:design_info", {
|
||||||
|
"def_veri_version": "verilog_2k", "def_vhdl_version": "vhdl_2008"
|
||||||
|
})
|
||||||
|
ET.SubElement(design_info, "efx:top_module", name=TOP)
|
||||||
|
for source in sources:
|
||||||
|
ET.SubElement(design_info, "efx:design_file", {
|
||||||
|
"name": str(source), "version": "default", "library": "default"
|
||||||
|
})
|
||||||
|
|
||||||
|
constraint_info = ET.SubElement(root, "efx:constraint_info")
|
||||||
|
ET.SubElement(constraint_info, "efx:sdc_file", name=str(BUILD / f"{PROJECT}_merged.sdc"))
|
||||||
|
ET.SubElement(root, "efx:misc_info")
|
||||||
|
ET.SubElement(root, "efx:ip_info")
|
||||||
|
|
||||||
|
def add_param(parent, name, value, value_type="e_string"):
|
||||||
|
ET.SubElement(parent, "efx:param", name=name, value=str(value), value_type=value_type)
|
||||||
|
|
||||||
|
synth = ET.SubElement(root, "efx:synthesis", tool_name="efx_map")
|
||||||
|
add_param(synth, "work_dir", "work_syn")
|
||||||
|
add_param(synth, "mode", "speed", "e_option")
|
||||||
|
add_param(synth, "infer-clk-enable", "3", "e_option")
|
||||||
|
add_param(synth, "infer-sync-set-reset", "1", "e_option")
|
||||||
|
add_param(synth, "bram_output_regs_packing", "1", "e_option")
|
||||||
|
add_param(synth, "retiming", "1", "e_option")
|
||||||
|
add_param(synth, "seq_opt", "1", "e_option")
|
||||||
|
|
||||||
|
pnr = ET.SubElement(root, "efx:place_and_route", tool_name="efx_pnr")
|
||||||
|
add_param(pnr, "work_dir", "work_pnr")
|
||||||
|
|
||||||
|
pgm = ET.SubElement(root, "efx:bitstream_generation", tool_name="efx_pgm")
|
||||||
|
add_param(pgm, "mode", "active", "e_string")
|
||||||
|
add_param(pgm, "width", "1", "e_string")
|
||||||
|
add_param(pgm, "oscillator_clock_divider", "DIV8", "e_option")
|
||||||
|
add_param(pgm, "spi_low_power_mode", "on", "e_bool")
|
||||||
|
add_param(pgm, "io_weak_pullup", "on", "e_bool")
|
||||||
|
add_param(pgm, "enable_roms", "smart", "e_option")
|
||||||
|
add_param(pgm, "bitstream_compression", "on", "e_bool")
|
||||||
|
add_param(pgm, "generate_bit", "on", "e_bool")
|
||||||
|
add_param(pgm, "generate_hex", "on", "e_bool")
|
||||||
|
ET.SubElement(root, "efx:debugger")
|
||||||
|
ET.SubElement(root, "efx:security")
|
||||||
|
|
||||||
|
BUILD.mkdir(parents=True, exist_ok=True)
|
||||||
|
raw = ET.tostring(root, encoding="utf-8")
|
||||||
|
pretty = xml.dom.minidom.parseString(raw).toprettyxml(indent=" ")
|
||||||
|
(BUILD / f"{PROJECT}.xml").write_text(pretty, encoding="utf-8")
|
||||||
|
print(BUILD / f"{PROJECT}.xml")
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
target_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
build_dir=${EFINITY_BUILD_DIR:-"$target_dir/build"}
|
||||||
|
project=${EFINITY_PROJECT:-ti375_max2771_recorder}
|
||||||
|
|
||||||
|
if [[ -z ${EFINITY_HOME:-} || ! -f "$EFINITY_HOME/bin/setup.sh" ]]; then
|
||||||
|
echo "ERROR: set EFINITY_HOME to a valid Efinity installation" >&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
|
||||||
@@ -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"
|
||||||
Executable
+36
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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]}]
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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]"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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<DEPTH) $fatal(1,"captured count %0d smaller than FIFO depth",captured);
|
||||||
|
$display("PASS tb_capture_overflow: no overwrite, dropped=%0d and loss latched",dropped);
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
initial begin #100000; $fatal(1,"capture overflow timeout"); end
|
||||||
|
endmodule
|
||||||
|
|
||||||
|
`default_nettype wire
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
`timescale 1ns/1ps
|
||||||
|
`default_nettype none
|
||||||
|
|
||||||
|
module tb_ethernet_crc32;
|
||||||
|
logic [31:0] crc_in;
|
||||||
|
logic [7:0] data_in;
|
||||||
|
logic [31:0] crc_out;
|
||||||
|
byte vector [0:8];
|
||||||
|
integer i;
|
||||||
|
|
||||||
|
ethernet_crc32 dut (.crc_in(crc_in), .data_in(data_in), .crc_out(crc_out));
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
vector[0]="1"; vector[1]="2"; vector[2]="3"; vector[3]="4"; vector[4]="5";
|
||||||
|
vector[5]="6"; vector[6]="7"; vector[7]="8"; vector[8]="9";
|
||||||
|
crc_in = 32'hffffffff;
|
||||||
|
data_in = 8'd0;
|
||||||
|
#1;
|
||||||
|
for (i = 0; i < 9; i = i + 1) begin
|
||||||
|
data_in = vector[i];
|
||||||
|
#1;
|
||||||
|
crc_in = crc_out;
|
||||||
|
#1;
|
||||||
|
end
|
||||||
|
if ((~crc_in) !== 32'hcbf43926)
|
||||||
|
$fatal(1, "CRC check vector failed: got %08x expected cbf43926", ~crc_in);
|
||||||
|
$display("PASS tb_ethernet_crc32: CRC-32('123456789') = cbf43926");
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
endmodule
|
||||||
|
|
||||||
|
`default_nettype wire
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
`timescale 1ns/1ps
|
||||||
|
`default_nettype none
|
||||||
|
|
||||||
|
module tb_ethernet_tx_mac;
|
||||||
|
logic clk=0, rst=1;
|
||||||
|
logic frame_start=0;
|
||||||
|
logic [10:0] payload_length=11'd4;
|
||||||
|
logic [7:0] payload_data;
|
||||||
|
logic payload_valid, payload_ready;
|
||||||
|
logic [7:0] gmii_txd;
|
||||||
|
logic gmii_tx_en, gmii_tx_er, busy, frame_done;
|
||||||
|
logic [63:0] frames, payload_bytes;
|
||||||
|
byte payload [0:3];
|
||||||
|
byte captured [0:255];
|
||||||
|
byte body [0:59];
|
||||||
|
integer payload_pos=0, cap_count=0, i;
|
||||||
|
logic [31:0] reference_crc;
|
||||||
|
|
||||||
|
always #4 clk=~clk;
|
||||||
|
assign payload_data = payload[payload_pos];
|
||||||
|
assign payload_valid = (payload_pos < 4);
|
||||||
|
|
||||||
|
ethernet_tx_mac dut (
|
||||||
|
.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(busy), .frame_done(frame_done), .frames_transmitted(frames),
|
||||||
|
.payload_bytes_transmitted(payload_bytes)
|
||||||
|
);
|
||||||
|
|
||||||
|
always @(posedge clk) begin
|
||||||
|
if (!rst && gmii_tx_en) begin
|
||||||
|
captured[cap_count] = gmii_txd;
|
||||||
|
cap_count = cap_count + 1;
|
||||||
|
if (gmii_tx_er) $fatal(1, "unexpected GMII TX error");
|
||||||
|
end
|
||||||
|
if (!rst && payload_ready && payload_valid)
|
||||||
|
payload_pos <= payload_pos + 1;
|
||||||
|
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
|
||||||
|
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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user