Implement generic MAX2771 raw Ethernet recorder

This commit is contained in:
onur
2026-08-01 01:15:47 +03:00
commit 79d07fdda7
57 changed files with 4810 additions and 0 deletions
+25
View File
@@ -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
+42
View File
@@ -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
+506
View File
@@ -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]]
+26
View File
@@ -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.
+65
View File
@@ -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.
+24
View File
@@ -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 |
+15
View File
@@ -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)
+571
View File
@@ -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;
}
+122
View File
@@ -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
+23
View File
@@ -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
+233
View File
@@ -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
+199
View File
@@ -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
+42
View File
@@ -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
+69
View File
@@ -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
+97
View File
@@ -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
+33
View File
@@ -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
+38
View File
@@ -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
+27
View File
@@ -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
+4
View File
@@ -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]]
+4
View File
@@ -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]]
+28
View File
@@ -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
+19
View File
@@ -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."
+232
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+36
View File
@@ -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
+10
View File
@@ -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
+104
View File
@@ -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
+85
View File
@@ -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
+39
View File
@@ -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
+93
View File
@@ -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]"
+24
View File
@@ -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
+103
View File
@@ -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
+49
View File
@@ -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
+32
View File
@@ -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
+100
View File
@@ -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
+125
View File
@@ -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