Files

18 KiB
Raw Permalink Blame History

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_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:

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.