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!
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.
- 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
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 dataeth_rx_dv- Receive data valideth_rx_er- Receive erroreth_col- Collision detecteth_crs- Carrier sense
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!
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)
| 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) |
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)
create_project mii_ethernet ./mii_ethernet -part xc7a100tcsg324-1
set_property board_part digilentinc.com:arty-a7-100:part0:1.1 [current_project]Add in this order:
src/mii_rx.vhdsrc/mac_parser.vhdsrc/stats_counter.vhdsrc/mii_eth_top.vhd(set as top)constraints/arty_a7_100t_mii.xdc
launch_runs synth_1 -jobs 4
wait_on_run synth_1Expected Results:
- No critical warnings
- ~800 LUTs, ~500 FFs
- 1 PLL/MMCM used
launch_runs impl_1 -to_step write_bitstream -jobs 4
wait_on_run impl_1Check Timing:
- Open
Reports -> Timing Summary - WNS (Worst Negative Slack) must be POSITIVE
- All setup/hold times must pass
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_devicesWindows:
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 upHardcoded in design:
- MAC Address:
00:0A:35:02:AF:9A - Also accepts: Broadcast (
FF:FF:FF:FF:FF:FF)
After programming:
- Wait 5 seconds for PHY initialization
- LED1 (blue) should turn ON - PHY ready
- Check Ethernet link LEDs in RJ45 jack - Should show link
Install Scapy:
pip install scapyRun test script:
sudo python3 test_mii_ethernet.pyExpected 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
ping 192.168.1.100Each 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 | 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) |
Check:
- Wait at least 5 seconds after programming
- Verify RGB LED1 (blue) is ON - PHY reset complete
- Check Ethernet cable is securely connected
- Verify PC network adapter is enabled
- Check timing report (WNS must be positive)
Vivado Checks:
# After implementation
open_run impl_1
report_timing_summary
# WNS should be > 0Check:
- Link is established (PC LED ON, RJ45 LEDs ON)
- Send frames with correct destination MAC:
00:0A:35:02:AF:9A - Verify in Wireshark that frames are leaving PC
- Check RGB LED2 (red) - if ON, errors detected
Wireshark Filter:
eth.dst == 00:0a:35:02:af:9a
If WNS is negative:
- Check clock constraints in XDC
- Verify all clock domain crossings use 2FF synchronizers
- Review critical path in timing report
- May need to pipeline critical paths
-- 100 MHz -> 25 MHz using PLL
PLLE2_BASE (
CLKFBOUT_MULT => 8, -- 100 × 8 = 800 MHz VCO
CLKOUT0_DIVIDE => 32 -- 800 ÷ 32 = 25 MHz
)-- 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;-- 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;-- 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;| 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.
-
IP Header Parsing
- Extract IP addresses
- Parse IP protocol field
-
UDP Parsing
- Extract UDP ports
- Access UDP payload
-
Timestamping
- Capture frame arrival time
- Sub-microsecond precision
-
MDIO Interface
- Read PHY status registers
- Configure PHY settings
- Arty A7 Manual: ARTY_A7_COMPLETE_REFERENCE.md
- Ethernet Specs: ARTY_A7_ETHERNET_SPECS.md
- DP83848J Datasheet: Texas Instruments website
- MII Specification: IEEE 802.3 Clause 22
- Wasted 4+ hours implementing wrong interface (RGMII vs MII)
- Could have been avoided by reading manual first
- New rule: Documentation -> Planning -> Coding
- Single Data Rate (SDR) vs Double Data Rate (DDR)
- PHY provides clocks (easier than driving them)
- Less complex timing constraints
- Must use 2FF synchronizers for single-bit signals
- Asynchronous FIFO for multi-bit data
- Proper timing constraints are essential
- Minimum 10ms reset pulse required
- Use 20ms to be safe
- Improper reset -> No link establishment
- Development Time: ~6 hours (initial RGMII mistake + MII rewrite + debugging)
- Lines of Code: 773 total (654 VHDL + 119 XDC)
mii_eth_top.vhd: 256 linesmac_parser.vhd: 177 linesmii_rx.vhd: 131 linesstats_counter.vhd: 90 linesarty_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
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.
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:
- Wait for
mii_rx_dvto go high (PHY starts sending) - Assemble nibbles into bytes
- Detect SFD byte (0xD5) - signals end of preamble
- Set
frame_startpulse on SFD detection - 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
The preamble stripping issue was diagnosed through systematic analysis despite Wireshark's inability to display physical layer data:
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.
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.
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!
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!
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!
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.
Logical deduction was sufficient without requiring ILA or simulation:
- Unambiguous symptoms - Frames not counted despite link establishment
- Clear failure point - MAC address filtering (only rejection mechanism in design)
- Specification documentation - IEEE 802.3 defines MII preamble behavior
- 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