Skip to content
tmarhguyPublic

About

100 Mbps UDP/IP stack in SystemVerilog for Nexys A7; built for low-latency market data (ITCH).

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

28 Commits

Folders and files

Repository files navigation

100 Mbps UDP/IP Stack (Nexys A7)

Status Ethernet Nexys A7 SystemVerilog FPGA Vivado Timing cocotb UDP

SystemVerilog · cocotb · Python · Vivado · github.com/tmarhguy/udp-stack

Read the Docs

Docs: Technical manual — the whole system, built from docs/index.adoc with make docs

See also: Understanding the UDP Stack and Connecting to ITCH · ITCH Ethernet lab bring-up · ITCH synthesis / bitstream · NASDAQ ITCH Hardware Parser


Why this exists

In conversations with business friends — especially Wharton students — NASDAQ comes up a lot. The argument usually starts with the open book: visible bids and asks, buy low, sell high, move fast. itch is where I put the parser and order book in silicon. But ITCH rides on UDP, and UDP rides on Ethernet — and none of that exists by default on an FPGA.

TCP is not your friend if speed is your goal. Three-way handshake, retransmits, kernel buffers — all fine for a file download, all wrong when the only frame that matters is the latest one on the wire. UDP multicast is how exchanges push market data: fire the newest update, drop anything stale, keep moving.

This repo is the 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.

The design journal is where the TCP-vs-UDP argument lives; the Aug 08 essay is the public version. This README is the map.

Nexys A7 on the bench — Ethernet cable in, 7-segment and LEDs alive

Nexys A7-100T · Artix-7 · Ethernet in, heartbeat on the 7-segment, link LED lit — the stack is running on silicon


Contents


At a glance

Last Vivado build: 2026-08-10 · Vivado 2025.2

Timing Met @ 100 MHz — WNS +1.985 ns, WHS +0.037 ns, 0 failed endpoints
Fabric LUT 1.19% · FF 0.47% · IO 24.8% · BUFG 6.25% · power 0.115 W
Sim latency UDP payload echo 2 cy (20 ns) on loopback_echo @ 100 MHz
Bitstream core/core.runs/impl_1/top.bit · Vivado 2025.2

Details: docs/metrics.md · Vivado GUI walkthrough: core/README.md


What this repo does

NASDAQ's Mold-wrapped ITCH feed rides on UDP. Before any parser sees a byte, the FPGA needs to:

  • Bring up the LAN8720 PHY over RMII (50 MHz ref + 100 MHz system)
  • Strip preamble/FCS in the MAC, demux IPv4 from Ethernet
  • Parse IP and UDP headers, filter destination port 50000
  • Echo the payload cut-through and rebuild headers with swapped src/dst

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.

Simulation comes first: cocotb replays synthetic Ethernet frames against the stack core. 2-cycle loopback latency before the bitstream gets trusted.

Synthesized RTL schematic (left) — board I/O and PHY/MAC ingress Synthesized RTL schematic (right) — UDP stack core, 7-segment, and RMII egress

Left: board I/O + PHY/MAC · Right: u_stack, 7-segment, RMII egress


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 activity; LED[13] / LED[15] show link up. The 7-segment display and switch-mirrored LEDs tell you the bitstream is alive before you ever send a packet.


Architecture at a glance

Datapath

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

Board top: core/rtl/top.sv
Stack core: core/rtl/stack/udp_stack_core.sv

Layer modules

Layer Module Role
PHY rmii_phy_if, lan8720_mdio RMII byte stream, MDIO link status
L2 eth_mac_axis Preamble/FCS strip, CRC on TX
L2 demux eth_demux IPv4 forward; ARP detect (lab: pre-seed host MAC)
L3 ip_rx IPv4 header parse, dst-IP filter
L4 udp_rx UDP header parse, dst-port filter (50000)
App loopback_echo Cut-through payload echo + latency counter
TX stack_tx Rebuild Ethernet / IPv4 / UDP headers

Clocks

Clock Source Period
CLK100MHZ Board oscillator 10 ns (100 MHz)
eth_refclk PHY RMII ref 20 ns (50 MHz)

Async clock groups in core/constrs/nexys_a7_100t.xdc — required for the RMII CDC FIFO.

Lab defaults

Parameter Value
FPGA IP 192.168.1.10
Host IP 192.168.1.100
Host MAC 00:08:DC:12:34:56 (edit in core/rtl/top.sv)
UDP port 50000
Part xc7a100tcsg324-1

Broadcast (255.255.255.255) works for direct-cable tests without ARP.


Repository map

udp-stack/
├── core/
│   ├── rtl/                  # 15 SystemVerilog sources (design truth)
│   ├── constrs/              # Pin + clock constraints
│   ├── core.xpr              # Vivado project
│   └── README.md             # GUI setup walkthrough
├── sim/                      # cocotb + testbenches
├── docs/                     # [Documentation index](docs/README.md)
├── log/                      # Design journal — [index](log/README.md)
├── media/                    # Bench photos, Vivado screenshots
└── tools/                    # send_udp.py, bench_check.py

Platform

Board Digilent Nexys A7-100T
FPGA Xilinx Artix-7 xc7a100tcsg324-1 · 100 MHz system clock
Toolchain Xilinx Vivado 2025.2 (synthesis, place & route, bitstream)
Ethernet SMSC LAN8720A · RMII · lab UDP port 50000
Simulation cocotb + Icarus Verilog (CI on Ubuntu)

Build status

First clean Vivado run (2026-08-10): synthesis, implementation, and bitstream passed — timing closed at 100 MHz. Light on fabric, heavy on I/O — exactly what a wire-facing stack should look like.

Vivado Project Summary — synthesis and implementation complete, WNS +1.985 ns Vivado dashboard — utilization, timing, and power at a glance

Left: project summary · Right: utilization and timing dashboard (synth_1 / impl_1)

Resource Used Util%
LUT 756 1.19%
FF 599 0.47%
IO 52 24.76%
BUFG 2 6.25%
Power (est.) — 0.115 W

From core/core.runs/impl_1/ reports · screenshots in media/

Implemented package view — RMII and MDIO pins on xc7a100t Post-route device floorplan — stack logic placed on Artix-7 fabric

Left: package pinout (eth_rxd, eth_txd, eth_mdc, …) · Right: placed design on silicon

Program the board and the JTAG target shows up ready to go:

Vivado Hardware Manager — xc7a100t_0 programmed with top.bit

Hardware Manager — top.bit loaded, xc7a100t_0 on the bench


Run it

Simulate (stack loopback, no PHY):

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

Unit tests (UDP, IP, ARP cache, MAC):

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

Build in Vivado (GUI): see core/README.md — add core/rtl/ as Design Sources, set top to top, add core/constrs/nexys_a7_100t.xdc.

Program & test on the bench:

python tools/send_udp.py --host 255.255.255.255 --port 50000

Or targeted:

python tools/bench_check.py

Full bench notes: docs/board_setup.md

LEDs (after programming)

LED Meaning
[7:0] Mirror SW[7:0]
[8], [14] Heartbeat (~1 Hz)
[9] RX activity pulse
[10] TX activity pulse
[11] Stack error
[12] Latency valid (pulse)
[13], [15] PHY link up

7-segment: right 2 digits = SW[7:0] hex; next 2 = heartbeat counter.


Docs & notes

Doc What's in it
docs/index.adoc Technical manual source (Asciidoctor book)
docs/README.md Documentation index
docs/architecture.md Data path, module hierarchy, clocks
docs/board_setup.md Cable, LEDs, host IP, traffic
docs/metrics.md Timing, utilization, latency — sourced from reports
core/README.md Vivado GUI project setup
log/ Design journal

Design journal

Log Topic
2026-08-08 — Understanding UDP & ITCH Why UDP, link to itch

Writing (tmarhguy.com)

Essay Topic
Understanding the UDP Stack and Connecting to ITCH TCP vs UDP for market data
ITCH Ethernet lab bring-up Cable, link LED, end-to-end goal
ITCH synthesis / bitstream itch first clean Vivado run (Aug 02)

Project status

As of August 2026

Area Status Notes
RMII PHY + MDIO Working Link LED, 50/100 MHz CDC
MAC (RX/TX) Working Preamble strip, FCS on TX
IPv4 + UDP RX Working Port filter, header parse
Cut-through echo Working 2-cycle payload latency in sim
ARP Lab stub Pre-seed host MAC; arp_cache single-entry
Vivado bitstream Clean WNS +1.985 ns @ 100 MHz
itch integration Next Replace echo with Mold/ITCH ingress (itch)

Direction: Prove the wire path here, then hand the parsed byte stream to itch for order-book logic. Same board, same PHY — different payload handler above UDP.


Author

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. Questions or collabs — reach out.

Email Twitter Instagram Substack GitHub

University of Pennsylvania Class of 2028 Networking build in public

About

100 Mbps UDP/IP stack in SystemVerilog for Nexys A7; built for low-latency market data (ITCH).

Resources

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages