UDP Stack at a glance

100 Mbps UDP/IP stack in SystemVerilog on the Digilent Nexys A7-100T — RMII PHY, MAC, IPv4, UDP, cut-through echo — the networking column so itch can parse NASDAQ ITCH instead of wondering how bytes got off the cable.

Nexys A7 on the bench with Ethernet connected
Figure 1. A7 on the bench — Ethernet in, heartbeat on 7-segment, link LED lit
GitHub Repository Why UDP for ITCH

15 SystemVerilog files implement the full RX-to-TX path in logic. Send a datagram to port 50000 and the FPGA returns it with 2-cycle latency.

Measured fact (2026-08-10 build) Value

UDP payload echo (loopback_echo @ 100 MHz)

2 cycles (20 ns)

Timing @ 100 MHz

WNS +1.985 ns, WHS +0.037 ns, 0 failing

Fabric xc7a100tcsg324-1

LUT 756 (1.19%), FF 599 (0.47%), IO 52 (24.8%), 0.115 W

About this manual

This is the detailed engineering reference for the project. It is the companion to the concise, GitHub-facing README.md:

  • README.md — project identity, status, quick start, bench photos.

  • docs/ (this manual) — architecture, interfaces, bring-up, verification, and implementation detail in engineering-manual style.

  • log/ — design journal with the TCP-vs-UDP argument.

  • tmarhguy.com/writing — public essays behind the build.

Conventions used throughout:

  • monospace marks commands, file paths, identifiers, and literals.

  • File paths are repository-relative, e.g. core/rtl/top.sv.

  • Measured numbers cite their report or log source. Unmeasured values are TBD (unmeasured) — never silently estimated.

  • Normative Markdown (docs/*.md, core/README.md, log/) is linked, not duplicated. This manual summarizes; the source file is truth.

Start with Introduction for scope and reading order, then Architecture for the system overview. The sidebar on the left is collapsible and searchable (press /).

1. Introduction

This chapter defines what the system is, what this manual covers, and how to navigate the rest of the document.

1.1. Purpose

NASDAQ’s Mold-wrapped ITCH feed rides on UDP multicast. Before any parser sees a byte, the FPGA needs a wire-to-payload path: bring up the LAN8720 PHY over RMII, strip Ethernet framing, demux IPv4, parse IP and UDP, and hand payload bytes to application logic.

This repo is that networking column — RMII PHY, MAC, IPv4, UDP, cut-through echo — so itch can worry about messages instead of wondering how bytes got off the cable. The design is 15 SystemVerilog files of dedicated logic covering the full RX-to-TX path.

Today the proof point is a UDP echo: send a datagram from the host, get it back on silicon with deterministic latency. Tomorrow the same MAC → IP → UDP spine plugs into `itch’s Mold unwrap — same RJ45, different payload handler.

The fastest way to see what the system is: the bench GIF at the top of this manual, and the loop in Architecture.

1.2. Scope

The manual covers:

It does not replace:

  • README.md — project identity, quick start, bench photos

  • docs/architecture.md, docs/metrics.md, docs/board_setup.md, docs/bitstream.md — normative short references (linked, not duplicated)

  • core/README.md — Vivado GUI walkthrough

  • log/ — full design journal text

  • Vivado reports under core/core.runs/impl_1/ — measurement sources

1.3. How to read this manual

Read chapters in order the first time. Chapters 1–2 give context; chapters 3–5 give the working model and bench use; chapters 6–7 give the evidence the stack behaves as claimed.

Each chapter is self-contained enough to be entered directly from the sidebar once the overview in Architecture is familiar.

1.4. Conventions

Normal cross-reference and link usage
  • Internal references look like this: Verification.

  • External links look like this: Understanding UDP & ITCH, the public version of the TCP-vs-UDP argument.

  • Inline code uses monospace, e.g. top.sv, udp_stack_core, 50000, 192.168.1.10.

  • File paths are repository-relative, e.g. core/rtl/stack/stack_tx.sv.

List conventions
  • Unordered lists state properties or inventories.

  • Ordered lists state procedures where sequence matters:

    1. Program the bitstream.

    2. Plug Ethernet and check link LEDs.

    3. Send UDP and watch RX/TX pulse.

Honest status is a project rule. Roadmap items stay labelled roadmap; unverified claims stay out of this manual. Where a number has not been measured it is written TBD (unmeasured).
The sidebar on the left is collapsible. Select a chapter title to navigate; select its arrow control to expand or collapse subsections. Press / to search this manual.

2. Architecture

System overview: what the pieces are, how packets flow in and back out, and where each piece lives in the tree.

2.1. The loop

Every lab session runs the same story. The full RX-to-TX arc runs on one FPGA.

  HOST IN              STACK                 HOST OUT
  ───────              ─────                 ────────
  UDP datagram    →    RMII RX → MAC    →    UDP reply
  port 50000           IP → UDP → echo       (swapped hdrs)
  1. Wire in. Live traffic hits the on-board LAN8720 PHY. RMII RX, IPv4 filter, UDP port match — payload bytes reach loopback_echo without a CPU memcpy.

  2. Echo. Cut-through forwarding rebuilds Ethernet + IPv4 + UDP headers with swapped addresses. Latency instrumentation reports cycle count on LED[12].

  3. Wire out. Reply leaves through the same MAC and PHY. LED[9] / LED[10] pulse on RX/TX; LED[13] / LED[15] show link up. The 7-segment and switch-mirrored LEDs tell you the bitstream is alive before you send a packet.

2.2. Datapath

RMII PHY ──► eth_mac_axis ──► eth_demux ──► ip_rx ──► udp_rx ──► loopback_echo
                                                                    │
                                                              stack_tx ◄──┘
                                                                    │
                                                         eth_mac_axis ──► RMII PHY

Inside udp_stack_core:

eth_demux → ip_rx → udp_rx → loopback_echo → axis_fifo → stack_tx
Table 1. Board hierarchy
Role Path

Board top

core/rtl/top.sv

Stack core

core/rtl/stack/udp_stack_core.sv

Sim top

sim/hdl/udp_loopback_top.sv

Synthesized hierarchy: top → u_mdio, u_phy, u_mac, u_stack, u_seg7.

Synthesized RTL schematic left — board I/O and PHY/MAC ingress
Figure 2. RTL schematic — board I/O and PHY/MAC (left) · stack core and RMII egress (right)
Synthesized RTL schematic right — UDP stack core and RMII egress

2.3. Further reading

  • docs/architecture.md — datapath, modules, clocks (normative short form)

  • core/rtl/top.sv — board wiring, LED and 7-segment assignments

  • core/rtl/stack/udp_stack_core.sv — stack integration

  • Design — per-layer behavior behind each box above

3. Design

Per-layer behavior, clocks, FIFOs, and lab defaults. This is the conceptual companion to the pin-level detail in Interfaces and the build evidence in Implementation.

3.1. Layer modules

Table 2. Layer responsibilities
Layer Module Role

PHY

rmii_phy_if, lan8720_mdio

RMII 2-bit nibble stream at 50 MHz ref; MDIO link-status poll, link_up to LEDs

L2

eth_mac_axis

Preamble (55…D5) strip on RX, FCS append + CRC32 on TX, IFG spacing; AXI-Stream byte interfaces (no preamble/FCS on AXIS)

L2 demux

eth_demux

IPv4 forward by EtherType 0x0800; ARP EtherType detect (arp_seen); non-IPv4 dropped

L3

ip_rx

20-byte IPv4 header parse, protocol == 17 (UDP) check, dst-IP == 192.168.1.10 filter; ip_error on mismatch

L4

udp_rx

8-byte UDP header parse, dst-port == 50000 filter; forwards m_src_port for reply; udp_error on mismatch

App

loopback_echo

Cut-through payload pass (tdata/tvalid/tlast straight through) + free-running cycle_cnt latency from first RX beat to last TX beat

TX

stack_tx

42-byte Ethernet (14) + IPv4 (20) + UDP (8) header rebuild with swapped src/dst, then payload stream

ARP

arp_cache

Single-entry stub; lab uses pre-seeded HOST_MAC in top.sv

Common

axis_fifo, byte_cdc_fifo, eth_crc32, seg7_display

Payload elasticity, RMII 50 MHz ↔ 100 MHz crossing, FCS, heartbeat display

Source tree: core/rtl/{phy,mac,arp,ip,udp,stack,common}/ — 15 .sv files total.

3.2. RX behavior

  • MAC RxIdle → RxPre → RxData: wait for 0x55, expect 0xD5 SFD, then stream bytes to m_axis. phy_rx_frame_end terminates the frame.

  • eth_demux forwards only IPv4; ARP is flagged, not answered in lab.

  • ip_rx states StHdr → StFwd / StDrop: captures protocol, src/dst IP across bytes 9–19; asserts m_meta_valid only on proto == 17 and dst-IP match, else ip_error and drop.

  • udp_rx states StHdr → StPay / StDrop: captures src/dst ports across bytes 0–3; asserts m_meta_valid only on dst-port 50000, else udp_error and drop.

  • loopback_echo is combinational pass-through for data with sidecar counters: armed on first valid && ready, latency_cycles = cycle_cnt - start_cycle pulsed on last beat. LED[12] mirrors latency_valid.

3.3. TX behavior

stack_tx states StIdle → StHdr → StPay:

  • pkt_start (raised by udp_stack_core after payload length is known) latches cfg_udp_len, cfg_dst_mac/ip/port.

  • StHdr emits 42 header bytes via hdr_byte(): dst MAC, LOCAL_MAC 00:0A:35:00:00:01, EtherType 0x0800, IPv4 0x45…, proto 0x11, src 192.168.1.10, dst = RX src IP, src port 50000, dst = RX src port, UDP length, zero checksum.

  • StPay drains axis_fifo payload with tlast preserved.

  • MAC TxIdle → TxIfg → TxPre → TxData → TxFcs: 12-byte IFG wait, 8-byte preamble, payload with running eth_crc32, 4-byte FCS, phy_tx_frame_end on last CRC byte.

stack_error = ip_err | udp_err drives LED[11].

3.4. Clocks and crossing

Clock Source Period

CLK100MHZ

Board oscillator (pin E3)

10 ns (100 MHz)

eth_refclk

PHY RMII ref (pin D5)

20 ns (50 MHz)

core/constrs/nexys_a7_100t.xdc declares both clocks and set_clock_groups -asynchronous between them — required for the byte_cdc_fifo RMII crossing. top.sv adds a 4-stage reset synchronizer off CPU_RESETN.

Implemented package view with RMII and MDIO pins
Figure 3. Implemented package view — eth_rxd/txd/mdc/mdio pinned on xc7a100t

3.5. Lab defaults

Parameter Value Defined in

FPGA IP

192.168.1.10

udp_stack_core.LOCAL_IP / stack_tx.LOCAL_IP

FPGA MAC

00:0A:35:00:00:01

stack_tx.LOCAL_MAC

Host IP

192.168.1.100

top.sv HOST_IP

Host MAC

00:08:DC:12:34:56

top.sv HOST_MAC — set to your PC

UDP port

50000

top.UDP_PORT → DST_PORT

Part

xc7a100tcsg324-1

Vivado project

Broadcast (255.255.255.255) works for direct-cable tests without ARP. See Interfaces for the tools that use these values.

4. Interfaces

How operators and scripts touch the system: lab addresses, host tools, LEDs, 7-segment, and pin constraints.

4.1. Network addresses

From core/rtl/top.sv and core/rtl/stack/udp_stack_core.sv:

Field Value

FPGA IP

192.168.1.10

Host IP

192.168.1.100

UDP port

50000

Host MAC

set HOST_MAC in top.sv to your PC’s MAC

Edit before building for a new host:

localparam logic [47:0] HOST_MAC = 48'h00_08_DC_12_34_56;
localparam logic [31:0] HOST_IP  = {8'd192, 8'd168, 8'd1, 8'd100};

4.2. Host tools

tools/send_udp.py — send a datagram and wait for the echo:

# Direct cable without ARP — broadcast works
python tools/send_udp.py --host 255.255.255.255 --port 50000

# Targeted echo with custom payload
python tools/send_udp.py --host 192.168.1.10 --port 50000 --payload "echo-test"

# Repeat N times
python tools/send_udp.py --host 192.168.1.10 --port 50000 --payload "bench" --repeat 10

tools/bench_check.py — wire-level sanity wrapper around the above (--host 255.255.255.255 --payload bench):

python tools/bench_check.py

Both use plain UDP sockets with SO_BROADCAST and a 2 s reply timeout. No reply (timeout) means link, address, or bitstream needs checking — see Bring-Up.

4.3. LEDs

From core/rtl/top.sv:

Table 3. LED map after programming
LED Meaning

[7:0]

Mirror SW[7:0]

[8], [14]

Heartbeat (~1 Hz, hb_led)

[9]

RX activity pulse (5 M-cycle stretch)

[10]

TX activity pulse (5 M-cycle stretch)

[11]

Stack error (ip_err | udp_err, 10 M-cycle stretch)

[12]

Latency-valid pulse (latency_valid)

[13], [15]

PHY link up (link_up from MDIO)

4.4. 7-segment display

Right to left on the board (from seg_value in top.sv):

  • Right 2 digits: SW[7:0] in hex.

  • Next 2 digits: heartbeat counter (slowly increments each blink).

  • Left 4 digits: 0000.

4.5. Pin constraints

core/constrs/nexys_a7_100t.xdc maps top ports to Nexys A7 pins:

Constraint Purpose

CLK100MHZ @ E3

100 MHz board clock, 10 ns

eth_refclk @ D5

50 MHz RMII ref, 20 ns

set_clock_groups -asynchronous

unrelated clocks for CDC FIFO

CPU_RESETN @ C12

center-button reset path (eth_rstn = CPU_RESETN & ~rst_sync)

SW[15:0] / LED[15:0]

switches and status above

CA–CG, DP, AN[7:0]

7-segment cathodes and anodes

eth_rxd[1:0]/txd[1:0]/txen/crs_dv/mdc/mdio/rstn

RMII + MDIO to LAN8720

The XDC must stay paired with top. Port names match exactly (CLK100MHZ, eth_refclk, LED[15:0]). constraints/udp_consts.xdc is a legacy copy — core/constrs/nexys_a7_100t.xdc is the build truth.

5. Bring-Up

Program, cable, and traffic for the echo on the bench. docs/board_setup.md is the normative short form; this chapter adds the GUI flow and checklist in manual order.

5.1. Vivado project

Item Value

Toolchain

Vivado 2025.2

Project file

core/core.xpr

Top module

top

RTL

core/rtl/ (15 .sv files)

Constraints

core/constrs/nexys_a7_100t.xdc

Part

xc7a100tcsg324-1

Output

core/core.runs/impl_1/top.bit

GUI flow (full steps in core/README.md):

  1. File → Project → New in core/, RTL project, part xc7a100tcsg324-1.

  2. Add Sources → Add Directories → core/rtl/ (all 15 files stay checked).

  3. Set top: right-click top.sv → Set as Top.

  4. Add Constraints → core/constrs/nexys_a7_100t.xdc.

  5. Run Synthesis → Implementation → Generate Bitstream.

5.2. Program and cable

  1. Program bitstream: core/core.runs/impl_1/top.bit.

  2. Connect RJ-45 to the FPGA Ethernet port, not a monitor passthrough.

  3. Host on 192.168.1.x (192.168.1.100 default) or use broadcast.

Nexys A7 on bench with Ethernet and heartbeat display
Figure 4. Bench — RJ45 connected, heartbeat on 7-segment, link LED near the port
Vivado Hardware Manager with top.bit programmed
Figure 5. Hardware Manager — top.bit on xc7a100t_0

5.3. Traffic

python tools/send_udp.py --host 255.255.255.255 --port 50000
python tools/send_udp.py --host 192.168.1.10 --port 50000 --payload "echo-test"
python tools/bench_check.py

5.4. Bring-up checklist

  1. Program bitstream → LED[8] blinks ~once per second.

  2. Flip switches → LED[7:0] follow SW[7:0]; right 7-seg digits show hex.

  3. Plug Ethernet → LED[13]/[15] on when link is up.

  4. Send UDP → LED[9]/[10] pulse; send_udp.py prints Reply … bytes.

If segments stay pale, re-synthesize after confirming rtl/common/seg7_display.sv is in Design Sources. If no reply, check HOST_MAC, host subnet, and that the cable is in the FPGA jack. Post-route evidence lives in Implementation.

6. Verification

How the project proves the stack is correct — and how to re-run that proof locally. Simulation comes first: the bitstream is trusted only after the loopback passes.

6.1. Testbench map

Gate Toplevel Python driver

loopback (default)

udp_loopback_tb

sim/cocotb/test_loopback_perf.py

udp

udp_rx_tb

sim/cocotb/test_udp.py

ip

ip_rx_tb

sim/cocotb/test_ip.py

arp

arp_cache_tb

sim/cocotb/test_arp.py

mac

eth_mac_axis_tb

sim/cocotb/test_mac_axis.py

HDL TBs live in sim/hdl/; packet helpers in sim/python/eth_packets.py; runner in sim/cocotb/run_tests.py (Icarus by default, SIM overridable, Windows-friendly with no GNU make).

6.2. Running the gates

cd sim/cocotb
pip install -r ../requirements.txt
python run_tests.py

Unit gates (same as CI):

cd sim/cocotb
TEST=udp python run_tests.py
TEST=ip  python run_tests.py
TEST=arp python run_tests.py
TEST=mac python run_tests.py

This is the same gate CI runs on every push (.github/workflows/ci.yml): install Icarus + requirements, run loopback, run the four unit gates, upload sim/results/latency_summary.json.

6.3. Correctness properties

Property Where it is checked

UDP payload echo preserves bytes and tlast

loopback perf test

IP drops non-UDP and wrong dst-IP with ip_error

test_ip

UDP drops wrong dst-port with udp_error

test_udp

ARP single-entry behavior

test_arp

MAC strips preamble, forwards frame, appends FCS

test_mac_axis

stack_error = ip_err | udp_err is the single board-visible error aggregate (see Interfaces).

6.4. Latency budget

Source: sim/results/latency_summary.json — written by test_loopback_perf.

Metric Budget Measured

Payload loopback (loopback_echo) @ 100 MHz

≤ 20 cycles (200 ns)

2 cycles (20 ns)

{
  "loopback_max_cycles": 2,
  "loopback_max_ns": 20
}

Regenerate with cd sim/cocotb && python run_tests.py. Wire-level confirmation is LED[12] pulsing per packet on the bench.

When adding a header feature, add the drop-path test first. A failing unit test is the specification; a passing one is the proof.

7. Implementation

Vivado build flow and measured post-route behavior. Numbers below are copied from files in this repo — not estimated. Last Vivado build: 2026-08-10, Vivado 2025.2.

7.1. Reports

Reports: core/core.runs/impl_1/

  • Timing: top_timing_summary_routed.rpt — Design Timing Summary.

  • Utilization: top_utilization_placed.rpt (post-place).

  • Power: top_power_routed.rpt.

Screenshots: media/proj_summary.png, media/dashboard.png, media/impl_device_design.png, media/synth_design.png.

7.2. Timing @ 100 MHz

Metric Value

WNS

+1.985 ns

TNS

0.000 ns

WHS

+0.037 ns

THS

0.000 ns

Failing endpoints

0

Timing closed at 100 MHz with ~2 ns of setup slack.

7.3. Utilization — xc7a100tcsg324-1

Resource Used Util%

Slice LUTs

756

1.19%

Slice FFs

599

0.47%

Bonded IOB

52

24.76%

BUFG

2

6.25%

Vivado Project Summary rounds LUT/FF to 1% and IO to 25% — same build, coarser display. Light on fabric, heavy on I/O — exactly what a wire-facing stack should look like.

Vivado Project Summary with WNS +1.985 ns
Figure 6. Left: project summary · Right: utilization and timing dashboard (synth_1 / impl_1)
Vivado dashboard with utilization timing and power

7.4. Power

Source: top_power_routed.rpt

Rail Value

Total on-chip (est.)

0.115 W

Dynamic

0.018 W

Device static

0.097 W

Junction (est.)

25.5 °C

7.5. Floorplan

Post-route floorplan — logic placement on Artix-7 fabric (same 2026-08-10 build):

Post-route device floorplan on Artix-7

The stack concentrates in left clock regions; I/O banks sit at the package edges. Package pinout (eth_rxd/txd/mdc/mdio) matches top.sv and nexys_a7_100t.xdc — see Design.

kgCO₂ or latency numbers copied from another project do not belong here. If it was not measured in this tree, it is TBD (unmeasured).

8. Known Limitations

Honest boundaries: what the system is, and what it is not. This chapter overrides any optimistic reading of earlier chapters.

8.1. What is real today

  • RMII PHY + MDIO link detect (link_up on LED[13]/[15], 50/100 MHz CDC).

  • MAC RX preamble strip and TX preamble + FCS + IFG (eth_mac_axis).

  • IPv4 header parse with dst-IP filter (ip_rx).

  • UDP header parse with dst-port 50000 filter (udp_rx).

  • Cut-through payload echo with 2-cycle sim latency (loopback_echo).

  • Header rebuild with swapped src/dst (stack_tx + axis_fifo).

  • Vivado 2025.2 bitstream with closed timing (WNS +1.985 ns @ 100 MHz).

  • cocotb loopback + udp/ip/arp/mac gates in CI.

  • Bench tools send_udp.py / bench_check.py with broadcast support.

8.2. What is explicitly not here

  • ARP is a lab stub. eth_demux detects ARP EtherType; arp_cache is single-entry. Dynamic lookup is not wired — pre-seed HOST_MAC in top.sv or use 255.255.255.255 broadcast for direct-cable tests.

  • ITCH parsing lives in itch. Payload handling stops at echo here. Mold unwrap and order-book logic live in itch.

  • UDP only. TCP, DHCP, and ICMP packets are dropped. Non-UDP IPv4 raises ip_error and is dropped by design.

  • Checksums are minimal. TX IP/UDP checksums are zero-filled lab values; RX does not validate header checksums beyond structure and filter.

8.3. Limits to design around

  • A packet is echoed if and only if it is IPv4 + UDP + dst-IP match
    dst-port 50000; everything else is dropped with stack_error pulsed.

  • Published numbers are measured in this tree (2-cycle loopback, WNS +1.985 ns, 756 LUT / 599 FF / 0.115 W). Unmeasured values are TBD (unmeasured).

  • Same board, same PHY for itch — different payload handler above UDP. Replace loopback_echo with Mold/ITCH ingress; keep MAC → IP → UDP.

9. Journal and References

Design narrative, public essays, companion repos, and the doc map. The journal is the argument; this manual is the map.

9.1. Design journal

log/ holds the running lab record:

Log Topic

log/2026-08-08 - Understanding the UDP stack and connecting to ITCH.md

Why UDP, TCP three-way-handshake cost, hardware advantage (FPGA ns vs PCIe/OS), multicast goal — the Wharton open-book origin

Core argument in one table (from the log):

Protocol Priority Analogy Integrity

TCP

Reliability

Downloading a file

ordered, retransmitted, never drops — pauses on loss

UDP

Speed & recency

Live video call

drops stale frames, always pushes latest

Exchanges push market data over UDP multicast: fire the newest update, drop anything stale, keep moving. Parsing ITCH in hardware puts the FPGA in the loop instead of kernel buffers and interrupts.

9.2. Writing (tmarhguy.com)

Essay Topic

Understanding the UDP Stack and Connecting to ITCH

TCP vs UDP for market data, public version of the log

ITCH Ethernet lab bring-up

Cable, link LED, end-to-end bench goal

ITCH synthesis / bitstream

First clean Vivado run (Aug 02, itch context)

9.3. Companion repos

9.4. Doc map

Doc What’s in it

docs/README.md

Documentation index (short form)

docs/architecture.md

Datapath, modules, clocks

docs/board_setup.md

Cable, LEDs, host IP, traffic

docs/metrics.md

Timing, utilization, latency from reports

docs/bitstream.md

Vivado build flow and output path

core/README.md

Vivado GUI project setup

log/

Design journal

Cite reports by filename (top_timing_summary_routed.rpt) and screenshots by media/ path so any reader can re-derive the numbers in Implementation.

10. Authors

The manual and the stack are produced by the author behind the design and the public build log.

10.1. Tyrone Marhguy

Computer Engineering '28, University of Pennsylvania. Personal FPGA project: custom UDP/IP on Nexys A7, companion stack for hardware ITCH parsing, and a public build log at tmarhguy.com.

Questions or collabs — reach out. See also projects and engineering log for Tomato, SeaLion, FramePort, and the ITCH series this stack supports.