18 KiB
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
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
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.
Prerequisites
Common host/simulation tools:
- Linux with GCC and GNU Make;
- root or
CAP_NET_RAWfor 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:
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.
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:
make genesys2-build
This runs Vivado in memory/non-project mode, synthesizes, places, routes, checks timing/CDC/DRC and writes:
build/genesys2/vivado/max2771_recorder.bit
build/genesys2/reports/*.rpt
Program volatile FPGA configuration over Digilent JTAG:
make genesys2-program
Or program a selected bitstream explicitly:
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.
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:
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:
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:
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:
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:
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:
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
make -C host clean all
make -C host test
The compilation command is equivalent to:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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.