Integration of standalone IP header parser into full Ethernet receiver system. Completes Phase 1 MII receiver implementation with MAC and IP layer parsing, clock domain crossing, and hardware validation on Arty A7-100T.
Phase 1D integrates the IP parser module (developed and validated in Phase 1C/v3a) into the complete MII Ethernet receiver pipeline. The system now parses Ethernet frames through MAC and IP layers, extracting protocol fields and validating checksums in real-time. All processing occurs at line rate (100 Mbps) with proper clock domain crossing between the 25 MHz MII interface and 100 MHz system clock.
Trading Relevance:
- Multi-layer protocol parsing (MAC → IP → UDP path toward market data)
- Clock domain crossing patterns for multi-rate systems
- Header validation and filtering at wire speed
- Statistics aggregation for monitoring and diagnostics
mii_eth_top (100 MHz system clock, 25 MHz Ethernet RX clock)
├── clk_wiz_0 (PLL: 100 MHz → 25 MHz for PHY TX, 100 MHz buffered)
├── phy_reset_ctrl (20ms reset generation for DP83848J PHY)
├── mii_receiver (25 MHz domain)
│ ├── Nibble-to-byte assembly
│ ├── Preamble/SFD detection and stripping
│ └── Frame valid generation
├── mac_parser (25 MHz domain)
│ ├── Destination MAC filtering
│ ├── Frame parsing and statistics
│ └── Byte stream output for IP parser
├── ip_parser (25 MHz domain) -> NEW
│ ├── EtherType detection (0x0800 for IPv4)
│ ├── IP header extraction (src/dst IP, protocol, length)
│ ├── Checksum validation (16-bit one's complement)
│ └── Version/IHL validation
├── Clock Domain Crossing (25 MHz → 100 MHz)
│ ├── 2FF synchronizers for single-bit signals
│ └── Multi-bit sampling on stable pulse edges
├── stats_counter (100 MHz domain)
│ ├── Frame and IP packet counters
│ ├── Error aggregation
│ └── LED display with mode selection
├── mdio_master (100 MHz domain)
│ └── PHY register access for status/debug
└── uart_tx (100 MHz domain)
└── Debug output for packet information
MII PHY (DP83848J)
↓ (25 MHz, 4-bit nibbles)
mii_receiver
↓ (25 MHz, bytes with frame_valid)
mac_parser
↓ (25 MHz, filtered byte stream + byte_counter)
ip_parser
↓ (25 MHz, IP fields + validation flags)
2FF Synchronizers
↓ (100 MHz, synchronized signals)
stats_counter / uart_tx
↓
LED Display / UART Output
-
eth_rx_clk (25 MHz) - Recovered from PHY, drives MII receiver
- mii_receiver module
- mac_parser module
- ip_parser module (NEW - runs in same domain as MAC parser)
-
clk (100 MHz) - System clock from PLL
- stats_counter module
- mdio_master module
- uart_tx module
- Button debouncing and LED control
Clock Domain Crossing:
- Single-bit signals (ip_valid, error flags) use 2FF synchronizers
- Multi-bit signal (ip_protocol) sampled on ip_valid pulse edge, then synchronized
- Board: Xilinx Arty A7-100T Development Board
- FPGA: Artix-7 XC7A100T-1CSG324C
- PHY: TI DP83848J (10/100 Mbps Ethernet, MII interface)
- Tools: AMD Vivado Design Suite 2020.2 or later
- Test Equipment:
- USB-to-Ethernet adapter (or direct PC Ethernet connection)
- Python 3 with Scapy library for packet generation
Pin Connections:
- RJ-45 Ethernet jack (J11) to network
- USB-UART (J10) for debug output at 115200 baud
- LEDs (LD0-LD3) for statistics display
- RGB LED (LD4) for link status and errors
- Buttons (BTN0-BTN3) for reset and mode control
cd 06-udp-parser-mii-v4
build 06-udp-parser-mii-v4
prog 06-udp-parser-mii-v4Or using Vivado directly:
vivado -mode batch -source build.tcl
vivado -mode batch -source program.tcl- Connect Ethernet cable between PC and Arty board (J11)
- Connect USB cable for power and UART (J10)
- Observe LD4 RGB LED:
- Green: Link established (100 Mbps)
- Red: Errors detected
cd test
pip install scapy # If not already installed
sudo python3 test_ip_ethernet.pyExpected Output:
============================================================
Test: Valid UDP Packet
============================================================
Packet summary: Ether / IP / UDP 192.168.1.10:12345 > 192.168.1.100:80 / Raw
IP: 192.168.1.10 -> 192.168.1.100
Protocol: 17 (udp)
Total Length: 38
Checksum: 0xB5E0
✓ Sent successfully!
Expected behavior:
- LED increments
- Protocol = 0x11 (UDP)
- In Mode 2: LED shows 0x1 (0x11 & 0xF)
Press BTN3 to cycle through display modes:
- Mode 0: Frame count (lower 4 bits) on LD0-LD3
- Mode 1: MDIO register data (lower 4 bits) on LD0-LD3
- Mode 2: IP protocol field (lower 4 bits) on LD0-LD3
- UDP: 0x1 (protocol 0x11)
- TCP: 0x6 (protocol 0x06)
- ICMP: 0x1 (protocol 0x01)
Location: mii_eth_top.vhd:657-676
The IP parser instantiation receives MAC parser outputs directly:
ip_parser_inst: entity work.ip_parser
port map (
clk => eth_rx_clk, -- Run in same domain as data (25 MHz)
reset => mdio_rst,
-- From MAC parser
frame_valid => mac_frame_valid,
data_in => mac_data_out,
byte_index => to_integer(mac_byte_counter),
-- Outputs (25 MHz domain)
ip_valid => ip_valid,
ip_src => ip_src,
ip_dst => ip_dst,
ip_protocol => ip_protocol,
ip_total_length => ip_total_length,
ip_checksum_ok => ip_checksum_ok,
ip_version_err => ip_version_err,
ip_ihl_err => ip_ihl_err,
ip_checksum_err => ip_checksum_err
);Design Rationale: Running IP parser in 25 MHz domain avoids clock domain crossing for high-bandwidth data stream. Only low-bandwidth outputs (flags and protocol field) are synchronized to 100 MHz system clock.
Location: mac_parser.vhd
Added outputs to feed IP parser:
data_out : out STD_LOGIC_VECTOR(7 downto 0); -- Current byte for IP parser
bye_counter : out unsigned(10 downto 0) -- Byte index in frameThese outputs provide the byte stream and position information required by the IP parser state machine.
Location: mii_eth_top.vhd:692-729
Single-bit signals (ip_valid, checksum_ok, error flags) use 2-stage flip-flop synchronizers:
process(clk)
begin
if rising_edge(clk) then
-- 2FF synchronizer for single-bit signals
ip_valid_sync1 <= ip_valid;
ip_valid_sync2 <= ip_valid_sync1;
ip_checksum_ok_sync1 <= ip_checksum_ok;
ip_checksum_ok_sync2 <= ip_checksum_ok_sync1;
end if;
end process;Multi-bit signal (ip_protocol) sampled when ip_valid asserts, then synchronized:
-- Sample protocol when ip_valid pulses (stable multi-bit value)
if ip_valid = '1' then
ip_protocol_sync1 <= ip_protocol;
end if;
ip_protocol_sync2 <= ip_protocol_sync1;This avoids metastability on multi-bit busses by sampling only when data is stable.
Location: stats_counter.vhd
Extended with IP packet statistics:
ip_valid_in : in std_logic; -- Pulse on valid IP packet
ip_checksum_ok_in : in std_logic; -- IP checksum validation
ip_protocol_in : in std_logic_vector(7 downto 0); -- Protocol fieldTracks both MAC-level frame counts and IP-level packet counts separately.
Multiple error sources:
rx_error- From mii_receiver (preamble, SFD, or length errors)stats_error- From stats_counter internal errorsip_checksum_err- IP checksum validation failuresip_version_err- IPv4 version field validation failuresip_ihl_err- IP header length validation failures
Aggregation:
led_rgb(2) <= rx_error or stats_error; -- Red LED on any errorSeparate error signals prevent multiple driver synthesis errors while allowing OR-based aggregation for LED output.
Location: mii_eth_top.vhd:746
led <= ip_protocol_sync2(3 downto 0) when debug_mode = "10" else
current_reg(3 downto 0) when debug_mode = "01" else
frame_count_leds;Mode selection uses 2-bit counter (debug_mode) toggled by BTN3 with debouncing.
The test script (test_ip_ethernet.py) validates IP parser functionality with six test scenarios:
| Test | Description | Protocol | Expected LED (Mode 2) | Expected Behavior |
|---|---|---|---|---|
| 1 | Valid UDP | 0x11 | 0x1 | LED increments, checksum_ok='1' |
| 2 | Valid TCP | 0x06 | 0x6 | LED increments, checksum_ok='1' |
| 3 | Valid ICMP | 0x01 | 0x1 | LED increments, checksum_ok='1' |
| 4 | Invalid checksum | 0x11 | — | No increment, red LED asserts |
| 5 | Wrong MAC (broadcast) | 0x11 | — | Filtered by MAC parser |
| 6 | Burst (10 packets) | 0x11 | 0x1 | LED shows count = 0xA |
Test Execution:
# Run all tests
sudo python3 test_ip_ethernet.py
# Run specific test
sudo python3 test_ip_ethernet.py --test 1
# Run burst with custom count
sudo python3 test_ip_ethernet.py --test 6 --burst 20Date: November 5, 2025 Location: mii_eth_top.vhd:654, mii_eth_top.vhd:759
Symptom:
Synthesis error:
Multiple Driver Nets: Net stats_counter_inst/led_rgb_OBUF[1] has multiple drivers:
stats_counter_inst/led_rgb_OBUF[2]_inst_i_1/O, and
mii_receiver/rx_error_reg/Q
Root Cause:
Signal rx_error had two drivers in conflicting hierarchy paths:
- Output port from
mii_receivermodule - Connected to
stats_counterled_error input port, which also drives led_rgb(2)
Port mapping led_error => rx_error created second driver through stats_counter logic.
Fix:
Created separate signal stats_error for stats_counter output:
-- Added new signal (line 293)
signal stats_error : std_logic;
-- Changed stats_counter port map (line 654)
led_error => stats_error -- Use separate signal, not rx_error
-- Combined both error sources for LED (line 759)
led_rgb(2) <= rx_error or stats_error;Verification:
Synthesis completes successfully. Both error types illuminate red LED correctly.
Lesson:
Avoid connecting output ports of different modules to the same signal when those modules also use the signal as an output. Use intermediate signals with explicit OR/AND logic for combining multiple error sources.
Date: November 5, 2025 Location: test_ip_ethernet.py:118
Symptom:
Python test script crashes with format string error:
Total Length: None
Error: unsupported format string passed to NoneType.__format__
Root Cause:
Scapy does not calculate IP length and checksum fields until packet is explicitly built. Attempting to format None values (uncomputed fields) with format specifier caused TypeError.
Original code:
print(f"Total Length: {packet[IP].len}") # len is None before building
print(f"Checksum: 0x{packet[IP].chksum:04X}") # chksum is NoneFix:
Complete rewrite of packet building and display logic:
def send_test_packet(iface, description, packet):
# Force packet construction to calculate auto-fields
built_packet = packet.__class__(bytes(packet))
if IP in built_packet:
ip_layer = built_packet[IP]
# Conditional formatting for potentially None fields
if ip_layer.len is not None:
print(f"Total Length: {ip_layer.len}")
else:
print(f"Total Length: auto")
if ip_layer.chksum is not None:
print(f"Checksum: 0x{ip_layer.chksum:04X}")
else:
print(f"Checksum: auto")
# Send the built packet
sendp(built_packet, iface=iface, verbose=False)Also fixed string encoding: Raw(b"Hello FPGA") instead of Raw("Hello FPGA").
Verification:
All test cases run successfully with correct checksum values displayed.
Lesson:
Scapy auto-calculates fields lazily. Force packet building with packet.__class__(bytes(packet)) before accessing computed fields. Always check for None before formatting values.
Development Status: Complete - Hardware verified Phase: 1D - IP Parser Integration Lines of Code:
- mii_eth_top.vhd: ~800 lines
- ip_parser.vhd: ~250 lines
- mac_parser.vhd: ~200 lines
- mii_receiver.vhd: ~150 lines
- test_ip_ethernet.py: ~305 lines
FPGA Resource Utilization:
| Resource | Used | Available | Utilization |
|---|---|---|---|
| LUTs | ~1,200 | 63,400 | 1.9% |
| FFs | ~900 | 126,800 | 0.7% |
| Block RAM | 0 | 135 | 0% |
| DSPs | 0 | 240 | 0% |
Timing:
- WNS (Worst Negative Slack): 4.2ns (meets timing)
- System Clock (100 MHz): 10ns period
- Ethernet RX Clock (25 MHz): 40ns period
Development Time:
- IP parser standalone development (Completed on Phase 1C(v3a)): ~10 hours
- Integration into full system (Phase 1D): ~4 hours
- Test script development and debugging: ~3 hours
- Total: ~7 hours
Test Environment:
- Arty A7-100T connected via USB Ethernet adapter
- PC interface: enp0s20f0u1 (80:3f:5d:fb:17:63)
- FPGA MAC: 00:0a:35:02:af:9a
- Link speed: 100 Mbps full duplex
Test Results:
| Test | Packets Sent | LED Count | Mode 2 Display | Status |
|---|---|---|---|---|
| Valid UDP | 1 | Increments | 0x1 | PASS |
| Valid TCP | 1 | Increments | 0x6 | PASS |
| Valid ICMP | 1 | Increments | 0x1 | PASS |
| Invalid checksum | 1 | No change | — | PASS |
| Wrong MAC | 1 | No change | — | PASS |
| Burst (10) | 10 | Shows 0xA | 0x1 | PASS |
Performance:
- Zero packet loss at 100 Mbps line rate
- Checksum validation correct for all test cases
- MAC filtering prevents invalid frames from reaching IP parser
- Clock domain crossing stable with no metastability observed
Running IP parser in 25 MHz domain (same as MAC parser) avoids clock domain crossing for high-bandwidth byte stream. Only low-rate control signals and extracted fields cross to 100 MHz domain.
Trade-off: Processing throughput vs. CDC complexity. For line-rate parsing, minimize CDC on data paths.
Multi-bit busses (ip_protocol) cannot use 2FF synchronizers directly due to bit skew. Solution: sample when stable (on ip_valid pulse edge), then synchronize the latched value.
Multiple error sources require careful signal routing to avoid multiple drivers. Create separate signals for each module's error output, then explicitly combine with OR logic at top level.
Scapy's lazy field calculation requires explicit packet building before accessing computed values. Always call packet.__class__(bytes(packet)) to force field computation.
Standalone module development (Phase 1C) with comprehensive testbenches significantly simplified integration (Phase 1D). All IP parser bugs caught in simulation before hardware integration.
- Extract UDP source/destination ports
- Access UDP payload data
- Port-based filtering and statistics
- Sub-microsecond arrival time capture
- Compare PHY timestamp vs. FPGA timestamp
- Jitter measurement and analysis
- Per-protocol packet counts (UDP, TCP, ICMP)
- Checksum error rates
- Frame size histograms
- Write capability for PHY configuration
- Link speed/duplex negotiation control
- Error counter readback from PHY
- AMD Vivado Design Suite 2020.2 or later
- Python 3.7+
- Scapy library (
pip install scapy) - Root privileges for raw packet transmission (Linux/Mac)
- Administrator privileges for packet transmission (Windows)
- IEEE 802.3 Ethernet Standard
- RFC 791: Internet Protocol
- TI DP83848J Datasheet
- Arty A7 Reference Manual
- Scapy Documentation
Status: Complete - Hardware verified Created: November 5, 2025 Last Updated: November 6, 2025 Completed: November 6, 2025
Phase Progression:
- Phase 1A: MII receiver with MAC parser
- Phase 1B: MDIO interface
- Phase 1C: IP parser standalone development
- Phase 1D: IP parser integration (current)
- Phase 1E: UDP parser and timestamping (planned)