Skip to content

adilsondias-engineer/06-fpga-udp-parser-mii

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Project 6: MII Ethernet Frame Receiver (CORRECTED) - Phase 1A

Part of FPGA Trading Systems Portfolio

This project is part of a complete end-to-end trading system:

  • Main Repository: fpga-trading-systems
  • Project Number: 6 of 30
  • Category: FPGA Core
  • Dependencies: Project 3 (FIFO concepts)

Hardware Ethernet Frame Receiver using MII Interface


##Important: RGMII vs MII

Previous Mistake: The initial implementation used RGMII (Reduced Gigabit MII), which is WRONG for the Arty A7.

Correct Interface: The Arty A7 uses MII (Media Independent Interface) with the TI DP83848J PHY chip.

Key Differences:

Feature RGMII (Wrong) MII (Correct)
Speed 1000 Mbps 10/100 Mbps
Data Width 4-bit DDR 4-bit SDR
Clock Freq 125 MHz 25 MHz (100 Mbps)
Clock Source FPGA drives PHY provides
Reference Clock None FPGA -> 25 MHz -> PHY

Process Improvement: Always check official board documentation FIRST!


Overview

This project implements a hardware Ethernet frame receiver on the Arty A7-100 FPGA using the correct MII interface. It demonstrates:

  • 25 MHz reference clock generation for PHY
  • MII receiver with nibble-to-byte assembly
  • MAC frame parsing with address filtering
  • Statistics display on LEDs
  • Clock domain crossing (25 MHz ↔ 100 MHz)

This is the foundation for building low-latency market data receivers used in high-frequency trading systems.


Hardware Requirements

  • Board: Arty A7-100T (XC7A100T-1CSG324C)
  • PHY: TI DP83848J (10/100 Mbps, MII interface)
  • Ethernet Cable: Standard Cat5e/Cat6
  • PC: With Ethernet port or USB-Ethernet adapter
  • Software: Vivado 2020.2 or newer

MII Interface Details

Signal Overview

From FPGA to PHY:

  • eth_ref_clk - 25 MHz reference clock (REQUIRED!)
  • eth_rstn - PHY reset (active LOW)
  • eth_txd[3:0] - Transmit data (not used in Phase 1)
  • eth_tx_en - Transmit enable (not used in Phase 1)

From PHY to FPGA:

  • eth_rx_clk - 25 MHz receive clock (PHY provides this!)
  • eth_tx_clk - 25 MHz transmit clock (PHY provides this!)
  • eth_rxd[3:0] - Receive data
  • eth_rx_dv - Receive data valid
  • eth_rx_er - Receive error
  • eth_col - Collision detect
  • eth_crs - Carrier sense

Critical Understanding

Clock Flow:

FPGA generates 25 MHz -> eth_ref_clk -> PHY X1 pin
PHY generates 25 MHz -> eth_rx_clk -> FPGA (for RX sampling)
PHY generates 25 MHz -> eth_tx_clk -> FPGA (for TX clocking)

This is OPPOSITE of RGMII where FPGA drives both clocks!


Architecture

Module Hierarchy

mii_eth_top
├── PLLE2_BASE (100 MHz -> 25 MHz reference clock)
├── mii_rx (MII receiver - nibble assembly)
├── mac_parser (MAC frame parser with filtering)
└── stats_counter (LED display + activity indicator)

Clock Domains

Domain Frequency Usage
sys_clk 100 MHz System control, LED display
eth_ref_clk 25 MHz PHY reference (generated by FPGA)
eth_rx_clk 25 MHz RX data sampling (from PHY)
eth_tx_clk 25 MHz TX data clocking (from PHY)

Data Flow

Ethernet Cable
    |
PHY (DP83848J)
    | (MII - 4-bit nibbles @ 25 MHz)
mii_rx (assemble nibbles -> bytes)
    | (8-bit bytes)
mac_parser (parse frame, filter by MAC)
    | (frame_valid pulse)
2FF Synchronizer (25 MHz -> 100 MHz)
    |
stats_counter (count frames, drive LEDs)

Building the Project

1. Create Vivado Project

create_project mii_ethernet ./mii_ethernet -part xc7a100tcsg324-1
set_property board_part digilentinc.com:arty-a7-100:part0:1.1 [current_project]

2. Add Source Files

Add in this order:

  1. src/mii_rx.vhd
  2. src/mac_parser.vhd
  3. src/stats_counter.vhd
  4. src/mii_eth_top.vhd (set as top)
  5. constraints/arty_a7_100t_mii.xdc

3. Run Synthesis

launch_runs synth_1 -jobs 4
wait_on_run synth_1

Expected Results:

  • No critical warnings
  • ~800 LUTs, ~500 FFs
  • 1 PLL/MMCM used

4. Run Implementation

launch_runs impl_1 -to_step write_bitstream -jobs 4
wait_on_run impl_1

Check Timing:

  • Open Reports -> Timing Summary
  • WNS (Worst Negative Slack) must be POSITIVE
  • All setup/hold times must pass

5. Program FPGA

open_hw_manager
connect_hw_server
open_hw_target
set_property PROGRAM.FILE {./mii_ethernet.runs/impl_1/mii_eth_top.bit} [current_hw_device]
program_hw_devices

Network Configuration

PC Ethernet Setup

Windows:

Control Panel -> Network Connections -> Ethernet Adapter
Properties -> TCP/IPv4 -> Use the following IP address:

IP Address: 192.168.1.1
Subnet Mask: 255.255.255.0
Gateway: (leave blank)

Linux:

sudo ifconfig eth0 192.168.1.1 netmask 255.255.255.0 up

FPGA Configuration

Hardcoded in design:

  • MAC Address: 00:0A:35:02:AF:9A
  • Also accepts: Broadcast (FF:FF:FF:FF:FF:FF)

Testing

Visual Inspection

After programming:

  1. Wait 5 seconds for PHY initialization
  2. LED1 (blue) should turn ON - PHY ready
  3. Check Ethernet link LEDs in RJ45 jack - Should show link

Send Test Frames

Install Scapy:

pip install scapy

Run test script:

sudo python3 test_mii_ethernet.py

Expected Behavior:

  • LEDs 0-3 increment in binary: 0001, 0010, 0011, 0100...
  • LED0 (green) blinks briefly on each frame received
  • LED1 (blue) stays ON (PHY ready)
  • LED2 (red) turns ON only if error detected

Manual Testing with Ping

ping 192.168.1.100

Each ping generates ~2 frames (ARP + ICMP), so LEDs should count:

  • 1st ping: LEDs = 0010 (2 frames)
  • 2nd ping: LEDs = 0010 or 0011 (ARP may be cached)
  • 3rd ping: LEDs increment steadily

LED Indicators

LED Color Function
LED0 White Bit 0 of frame counter
LED1 White Bit 1 of frame counter
LED2 White Bit 2 of frame counter
LED3 White Bit 3 of frame counter
RGB0 Green Activity indicator (blinks on frame)
RGB1 Blue PHY ready (ON after reset complete)
RGB2 Red Error indicator (ON if rx_error)

Troubleshooting

No Link / PC LED Off

Check:

  1. Wait at least 5 seconds after programming
  2. Verify RGB LED1 (blue) is ON - PHY reset complete
  3. Check Ethernet cable is securely connected
  4. Verify PC network adapter is enabled
  5. Check timing report (WNS must be positive)

Vivado Checks:

# After implementation
open_run impl_1
report_timing_summary
# WNS should be > 0

LEDs Don't Count

Check:

  1. Link is established (PC LED ON, RJ45 LEDs ON)
  2. Send frames with correct destination MAC: 00:0A:35:02:AF:9A
  3. Verify in Wireshark that frames are leaving PC
  4. Check RGB LED2 (red) - if ON, errors detected

Wireshark Filter:

eth.dst == 00:0a:35:02:af:9a

Timing Violations

If WNS is negative:

  1. Check clock constraints in XDC
  2. Verify all clock domain crossings use 2FF synchronizers
  3. Review critical path in timing report
  4. May need to pipeline critical paths

Key Implementation Details

1. Reference Clock Generation

-- 100 MHz -> 25 MHz using PLL
PLLE2_BASE (
    CLKFBOUT_MULT => 8,      -- 100 × 8 = 800 MHz VCO
    CLKOUT0_DIVIDE => 32     -- 800 ÷ 32 = 25 MHz
)

2. PHY Reset Timing

-- Minimum 10ms reset pulse (per DP83848J datasheet)
-- We use 20ms to be safe
if reset_counter < 2_000_000 then  -- 20ms @ 100 MHz
    phy_reset_n <= '0';
else
    phy_reset_n <= '1';
end if;

3. MII Nibble Assembly

-- MII sends data as 4-bit nibbles
-- Low nibble first, then high nibble
if nibble_cnt = '0' then
    nibble_low <= mii_rxd;  -- Bits 3:0
else
    rx_data <= mii_rxd & nibble_low;  -- Bits 7:4 & 3:0
    rx_valid <= '1';
end if;

4. Clock Domain Crossing

-- 2FF synchronizer: 25 MHz -> 100 MHz
process(sys_clk)
begin
    if rising_edge(sys_clk) then
        sync1 <= signal_from_25mhz;  -- First FF (may go metastable)
        sync2 <= sync1;               -- Second FF (stable output)
    end if;
end process;

Differences from RGMII Implementation

Aspect RGMII (Wrong) MII (Correct)
Data Sampling Both edges Rising edge only
Clock Source FPGA drives PHY provides
Reference None 25 MHz from FPGA
Nibble Rate 250 MHz effective 25 MHz
Complexity High (DDR) Low (SDR)
Lines of Code ~300 ~200

MII is actually simpler! The DDR complexity in RGMII made it harder to implement.


Next Steps (Phase 1B - Future)

  1. IP Header Parsing

    • Extract IP addresses
    • Parse IP protocol field
  2. UDP Parsing

    • Extract UDP ports
    • Access UDP payload
  3. Timestamping

    • Capture frame arrival time
    • Sub-microsecond precision
  4. MDIO Interface

    • Read PHY status registers
    • Configure PHY settings

Resources


Lessons Learned

1. Always Check Documentation First

  • Wasted 4+ hours implementing wrong interface (RGMII vs MII)
  • Could have been avoided by reading manual first
  • New rule: Documentation -> Planning -> Coding

2. MII is Simpler Than RGMII

  • Single Data Rate (SDR) vs Double Data Rate (DDR)
  • PHY provides clocks (easier than driving them)
  • Less complex timing constraints

3. Clock Domain Crossing is Critical

  • Must use 2FF synchronizers for single-bit signals
  • Asynchronous FIFO for multi-bit data
  • Proper timing constraints are essential

4. PHY Reset Timing Matters

  • Minimum 10ms reset pulse required
  • Use 20ms to be safe
  • Improper reset -> No link establishment

Metrics

  • Development Time: ~6 hours (initial RGMII mistake + MII rewrite + debugging)
  • Lines of Code: 773 total (654 VHDL + 119 XDC)
    • mii_eth_top.vhd: 256 lines
    • mac_parser.vhd: 177 lines
    • mii_rx.vhd: 131 lines
    • stats_counter.vhd: 90 lines
    • arty_a7_100t_mii.xdc: 119 lines
  • Test Coverage: Automated Python tests (Scapy) + manual ping validation
  • Bug Fixes: 2 critical issues resolved (documented below)
  • Hardware Verification: Complete - frame reception tested on Arty A7-100T

🔧 Bugs Fixed

Bug #1: PLLE2_BASE Generic Parameter Type Error

Date Fixed: November 4, 2025

Error Message:

type error near false ; current type boolean; expected type string

Location: mii_eth_top.vhd:130

Root Cause: Xilinx VHDL primitives like PLLE2_BASE expect string literals for their generic parameters, not boolean values. The code incorrectly used FALSE (boolean) instead of "FALSE" (string).

Incorrect Code:

PLLE2_BASE
    generic map (
        BANDWIDTH        => "OPTIMIZED",
        CLKFBOUT_MULT    => 8,
        CLKOUT0_DIVIDE   => 32,
        CLKIN1_PERIOD    => 10.0,
        DIVCLK_DIVIDE    => 1,
        STARTUP_WAIT     => FALSE    -- Boolean (wrong type)
    )

Fix:

PLLE2_BASE
    generic map (
        BANDWIDTH        => "OPTIMIZED",
        CLKFBOUT_MULT    => 8,
        CLKOUT0_DIVIDE   => 32,
        CLKIN1_PERIOD    => 10.0,
        DIVCLK_DIVIDE    => 1,
        STARTUP_WAIT     => "FALSE"  -- String literal (correct)
    )

Impact: Synthesis would fail. No workaround possible - must use string literals.

Lesson Learned: Xilinx primitives are very particular about parameter types. Always check UG953 (Vivado Design Suite 7 Series FPGA and Zynq-7000 SoC Libraries Guide) for exact parameter types.


Bug #2: MII Receiver Not Stripping Ethernet Preamble/SFD

Date Fixed: November 4, 2025

Symptom:

  • Ethernet link established (orange LED blinking)
  • Wireshark shows frames being sent to FPGA's MAC address
  • LEDs don't count frames - stuck at 0000
  • No errors indicated (red LED off)

Location: mii_rx.vhd

Root Cause: Every Ethernet frame begins with an 8-byte preamble:

  • 7 bytes of 0x55 (preamble for clock synchronization)
  • 1 byte of 0xD5 (Start Frame Delimiter - SFD)

The MII receiver was passing all bytes to the MAC parser, including the preamble. The MAC parser expected the first byte to be the destination MAC address, but instead received 0x55.

Result: MAC address matching always failed (0x55 ≠ 0x00:0A:35:02:AF:9A), so no frames were counted.

Frame Structure:

[PREAMBLE: 7×0x55][SFD: 0xD5][DEST MAC][SRC MAC][ETHERTYPE][PAYLOAD][FCS]
                              ↑ MAC parser expects to start here

Fix: Added preamble/SFD detection and stripping in mii_rx.vhd:

-- New signals added
signal sfd_detected : STD_LOGIC := '0';
signal preamble_done : STD_LOGIC := '0';
constant SFD_BYTE : STD_LOGIC_VECTOR(7 downto 0) := x"D5";

-- Modified nibble assembly logic
-- Assemble complete byte
byte_data <= mii_rxd & nibble_low;

-- Check for SFD byte (0xD5) - marks end of preamble
if (mii_rxd & nibble_low) = SFD_BYTE and sfd_detected = '0' then
    sfd_detected  <= '1';
    preamble_done <= '1';
    frame_start   <= '1';  -- Signal start of actual frame data
    -- Don't output this byte
else
    -- Only output bytes AFTER SFD
    if preamble_done = '1' then
        rx_data  <= mii_rxd & nibble_low;
        rx_valid <= '1';
    end if;
end if;

How It Works:

  1. Wait for mii_rx_dv to go high (PHY starts sending)
  2. Assemble nibbles into bytes
  3. Detect SFD byte (0xD5) - signals end of preamble
  4. Set frame_start pulse on SFD detection
  5. Only output bytes AFTER the SFD - skip preamble entirely

Impact: Without this fix, the FPGA would never recognize any Ethernet frames, making the project non-functional.

Test Results After Fix:

Sending 10 frames to 00:0a:35:02:af:9a...
LEDs counting: 0001 -> 0010 -> 0011 -> 0100...
Green LED blinking on each frame
Blue LED steady (PHY ready)
Red LED off (no errors)

Lesson Learned:

  • PHY chips strip physical layer overhead (preamble/SFD) in some modes, but not MII
  • Always verify protocol layering - what does the PHY provide vs what must the FPGA handle?
  • MII receivers must strip preamble/SFD themselves
  • RMII and other interfaces may handle this differently
  • Check IEEE 802.3 specification for exact byte-level framing

Debugging Approach

The preamble stripping issue was diagnosed through systematic analysis despite Wireshark's inability to display physical layer data:

1. Symptom Analysis

Observed:
+ Ethernet link established (RJ45 LEDs ON, PC shows connection)
+ Wireshark shows frames being sent to correct MAC (00:0a:35:02:af:9a)
+ Frame rate visible on RJ45 orange LED (blinking when sending)
- FPGA LEDs stuck at 0000 (not counting frames)
- Blue LED ON (PHY ready - no initialization problem)
- Red LED OFF (no errors signaled by PHY)

Conclusion: Hardware is working, frames are arriving, but software logic is rejecting them.

2. Wireshark Analysis (Layer Visibility)

Wireshark filter: eth.dst == 00:0a:35:02:af:9a

Frame 1: 60 bytes on wire
Ethernet II, Src: PC_MAC, Dst: 00:0a:35:02:af:9a
    Destination: 00:0a:35:02:af:9a  <- Correct!
    Source: xx:xx:xx:xx:xx:xx
    Type: IPv4 (0x0800)
Internet Protocol...

What Wireshark CANNOT show:

  • Preamble (7 bytes of 0x55)
  • SFD (1 byte of 0xD5)
  • FCS/CRC (4 bytes at end)

Why? The host PC's NIC strips these before passing frames to the OS.

Conclusion: Frames are definitely being sent with correct destination MAC.

3. Code Review: Data Flow Analysis

Traced the data path:

PHY -> mii_rx_dv/mii_rxd (nibbles)
    -> mii_rx module (assemble to bytes)
    -> rx_data/rx_valid
    -> mac_parser (filter by MAC)
    -> frame_valid
    -> stats_counter (increment LEDs)

Question: What byte does mac_parser expect first?

Answer: Destination MAC address (first byte should be 0x00)

Question: What byte is MII actually sending first?

Answer: Unknown - need to investigate!

4. MII Specification Research

Checked IEEE 802.3 Clause 22 (MII specification):

Key Finding:

"The MII transfers data between the PHY and MAC. The PHY is responsible for encoding/decoding at the physical layer, but the preamble and SFD are part of the MAC frame format and are transmitted through the MII interface."

Translation: MII passes preamble/SFD to FPGA - it does NOT strip them!

5. Logical Deduction

If MII passes preamble, then:

Byte 0:  0x55  (preamble)
Byte 1:  0x55  (preamble)
...
Byte 6:  0x55  (preamble)
Byte 7:  0xD5  (SFD)
Byte 8:  0x00  (Dest MAC byte 0)  <- mac_parser expects THIS as first byte
Byte 9:  0x0A  (Dest MAC byte 1)
...

But the initial code was doing:

mac_parser receives byte 0 = 0x55  -- Treats this as destination MAC

if dest_mac_buf = MAC_ADDR then  -- Compares 0x55... ≠ 0x00:0A:35...
    frame_valid <= '1';           -- Never matches!
end if;

This explains everything!

6. Alternative Verification Methods

Method A: ILA (Integrated Logic Analyzer)

Add Vivado ILA core to capture signals:

# In constraints
create_debug_core u_ila_0 ila
set_property port_width 8 [get_debug_ports u_ila_0/probe0]
connect_debug_port u_ila_0/probe0 [get_nets rx_data[*]]
connect_debug_port u_ila_0/probe1 [get_nets rx_valid]

Would show:

Trigger on rx_valid = 1
rx_data[7:0]:  0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0xD5, 0x00, 0x0A...
                                                             ↑ MAC starts here

Method B: Debug FIFO with UART Output

Add small debug module:

-- Capture first 16 bytes of each frame
if frame_start = '1' then
    byte_counter <= 0;
end if;

if rx_valid = '1' and byte_counter < 16 then
    debug_buffer(byte_counter) <= rx_data;
    byte_counter <= byte_counter + 1;
end if;

-- Send to UART when frame ends
if frame_end = '1' then
    uart_send_buffer(debug_buffer, 16);  -- Send first 16 bytes
end if;

Would output:

Frame bytes: 55 55 55 55 55 55 55 D5 00 0A 35 02 AF 9A ...
             Preamble----------- ^SFD ^MAC address starts

Method C: Simulation with Realistic Testbench

Create testbench that sends actual Ethernet frame with preamble:

-- Send preamble
for i in 0 to 6 loop
    send_nibbles(x"55");  -- 7 bytes of 0x55
end loop;

-- Send SFD
send_nibbles(x"D5");

-- Send MAC frame
send_nibbles(x"00");  -- Dest MAC byte 0
send_nibbles(x"0A");  -- Dest MAC byte 1
...

Run simulation and observe waveforms - would clearly show preamble being output.

Method D: LED/Counter Debugging (Quick & Dirty)

Add to mac_parser.vhd:

signal first_byte : std_logic_vector(7 downto 0);

if frame_start = '1' and rx_valid = '1' then
    first_byte <= rx_data;  -- Capture first byte
end if;

-- Display on LEDs
led_debug <= first_byte(3 downto 0);  -- Would show 0x5 (from 0x55)

Would immediately show first byte is 0x55 not 0x00.

7. Root Cause Analysis

Logical deduction was sufficient without requiring ILA or simulation:

  1. Unambiguous symptoms - Frames not counted despite link establishment
  2. Clear failure point - MAC address filtering (only rejection mechanism in design)
  3. Specification documentation - IEEE 802.3 defines MII preamble behavior
  4. Interface-specific behavior - MII passes preamble (unlike higher-level interfaces)

The specification research revealed that MII provides raw frame data including preamble, requiring explicit stripping logic in the receiver.


Status: Tested and working on hardware Completed: November 4, 2025 Last Updated: November 4, 2025 Hardware: Xilinx Arty A7-100T (XC7A100T-1CSG324C)

Recent Fixes:

  • PLLE2_BASE generic parameter type corrected (boolean -> string) (04/11/2025)
  • MII preamble/SFD stripping implemented - frames now counted correctly (04/11/2025)
  • Hardware verification complete - all tests passing (04/11/2025)

Part of FPGA Learning Journey - Building trading-relevant hardware skills
Portfolio Project: Demonstrates protocol design, state machines, error handling, and professional debugging

About

UDP/IP network stack from scratch (v1) - MII PHY, MAC/IP/UDP parsing in VHDL. First working version with <2μs latency.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors