eth: Add 1000BASE-X PCS core to AC701 example design

Signed-off-by: Alex Forencich <alex@alexforencich.com>
This commit is contained in:
Alex Forencich
2026-04-13 21:24:42 -07:00
parent 09ec52f8eb
commit cb54131ec0
8 changed files with 344 additions and 10 deletions

View File

@@ -4,18 +4,21 @@
This example design targets the Xilinx AC701 FPGA board.
The design places a looped-back MACs the BASE-T port, as well as XFCP on the USB UART for monitoring and control.
The design places looped-back MACs on both the BASE-T port and the SFP+ cage, as well as XFCP on the USB UART for monitoring and control.
* USB UART
* XFCP (921600 baud)
* RJ-45 Ethernet port with Marvell 88E1116 PHY
* Looped-back MAC via RGMII
* SFP+ cage
* Looped-back 1000BASE-X via Xilinx PCS/PMA core and GTX transceiver
## Board details
* FPGA: XC7A200T-2FBG676C
* USB UART: Silicon Labs CP2103
* 1000BASE-T PHY: Marvell 88E1116 via RGMII
* 1000BASE-X PHY: Xilinx PCS/PMA core via GTX transceiver
## Licensing

View File

@@ -18,6 +18,7 @@ TAXI_SRC_DIR = $(LIB_DIR)/taxi/src
# Files for synthesis
SYN_FILES = $(RTL_DIR)/fpga.sv
SYN_FILES += $(RTL_DIR)/fpga_core.sv
SYN_FILES += $(TAXI_SRC_DIR)/eth/rtl/taxi_eth_mac_1g_fifo.f
SYN_FILES += $(TAXI_SRC_DIR)/eth/rtl/taxi_eth_mac_1g_rgmii_fifo.f
SYN_FILES += $(TAXI_SRC_DIR)/xfcp/rtl/taxi_xfcp_if_uart.f
SYN_FILES += $(TAXI_SRC_DIR)/xfcp/rtl/taxi_xfcp_switch.sv
@@ -32,6 +33,7 @@ XDC_FILES += ../syn/fpga.xdc
XDC_FILES += ../syn/gpio.xdc
XDC_FILES += ../syn/i2c.xdc
XDC_FILES += ../syn/phy.xdc
XDC_FILES += ../syn/sfp.xdc
XDC_FILES += ../syn/eth_rgmii.xdc
XDC_FILES += $(TAXI_SRC_DIR)/eth/syn/vivado/taxi_rgmii_phy_if.tcl
XDC_FILES += $(TAXI_SRC_DIR)/eth/syn/vivado/taxi_eth_mac_fifo.tcl
@@ -40,7 +42,7 @@ XDC_FILES += $(TAXI_SRC_DIR)/sync/syn/vivado/taxi_sync_reset.tcl
XDC_FILES += $(TAXI_SRC_DIR)/sync/syn/vivado/taxi_sync_signal.tcl
# IP
#IP_TCL_FILES = ../ip/sgmii_pcs_pma_0.tcl
IP_TCL_FILES += ../ip/basex_pcs_pma_0.tcl
# Configuration
#CONFIG_TCL_FILES = ./config.tcl

View File

@@ -0,0 +1,17 @@
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2026 FPGA Ninja, LLC
#
# Authors:
# - Alex Forencich
#
create_ip -name gig_ethernet_pcs_pma -vendor xilinx.com -library ip -module_name basex_pcs_pma_0
set_property -dict [list \
CONFIG.Standard {1000BASEX} \
CONFIG.Physical_Interface {Transceiver} \
CONFIG.Management_Interface {false} \
CONFIG.Auto_Negotiation {false} \
CONFIG.SupportLevel {Include_Shared_Logic_in_Core} \
] [get_ips basex_pcs_pma_0]

View File

@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
/*
Copyright (c) 2014-2026 FPGA Ninja, LLC
Copyright (c) 2014-2025 FPGA Ninja, LLC
Authors:
- Alex Forencich
@@ -61,6 +61,20 @@ module fpga #
inout wire logic i2c_sda,
output wire logic i2c_mux_reset,
/*
* Ethernet: SFP+
*/
input wire logic sfp_rx_p,
input wire logic sfp_rx_n,
output wire logic sfp_tx_p,
output wire logic sfp_tx_n,
input wire logic sfp_mgt_refclk_0_p,
input wire logic sfp_mgt_refclk_0_n,
output wire logic [1:0] sfp_mgt_clk_sel,
output wire logic sfp_tx_disable,
input wire logic sfp_rx_los,
/*
* Ethernet: 1000BASE-T RGMII
*/
@@ -259,6 +273,113 @@ assign i2c_scl = i2c_scl_o ? 1'bz : 1'b0;
assign i2c_sda_i = i2c_sda;
assign i2c_sda = i2c_sda_o ? 1'bz : 1'b0;
// 1000BASE-X SFP
assign sfp_mgt_clk_sel = 2'b00;
wire sfp_gmii_clk_int;
wire sfp_gmii_rst_int;
wire sfp_gmii_clk_en_int;
wire [7:0] sfp_gmii_txd_int;
wire sfp_gmii_tx_en_int;
wire sfp_gmii_tx_er_int;
wire [7:0] sfp_gmii_rxd_int;
wire sfp_gmii_rx_dv_int;
wire sfp_gmii_rx_er_int;
wire sfp_gtrefclk;
wire sfp_gtrefclk_bufg;
wire sfp_txuserclk;
wire sfp_txuserclk2;
wire sfp_rxuserclk;
wire sfp_rxuserclk2;
wire sfp_pma_reset;
wire sfp_mmcm_locked;
wire sfp_resetdone;
assign sfp_gmii_clk_int = sfp_txuserclk2;
taxi_sync_reset #(
.N(4)
)
sync_reset_sfp_inst (
.clk(sfp_gmii_clk_int),
.rst(rst_int || !sfp_resetdone),
.out(sfp_gmii_rst_int)
);
wire [15:0] sfp_status_vect;
wire sfp_status_link_status = sfp_status_vect[0];
wire sfp_status_link_synchronization = sfp_status_vect[1];
wire sfp_status_rudi_c = sfp_status_vect[2];
wire sfp_status_rudi_i = sfp_status_vect[3];
wire sfp_status_rudi_invalid = sfp_status_vect[4];
wire sfp_status_rxdisperr = sfp_status_vect[5];
wire sfp_status_rxnotintable = sfp_status_vect[6];
wire sfp_status_phy_link_status = sfp_status_vect[7];
wire [1:0] sfp_status_remote_fault_encdg = sfp_status_vect[9:8];
wire [1:0] sfp_status_speed = sfp_status_vect[11:10];
wire sfp_status_duplex = sfp_status_vect[12];
wire sfp_status_remote_fault = sfp_status_vect[13];
wire [1:0] sfp_status_pause = sfp_status_vect[15:14];
wire [4:0] sfp_config_vect;
assign sfp_config_vect[4] = 1'b0; // autonegotiation enable
assign sfp_config_vect[3] = 1'b0; // isolate
assign sfp_config_vect[2] = 1'b0; // power down
assign sfp_config_vect[1] = 1'b0; // loopback enable
assign sfp_config_vect[0] = 1'b0; // unidirectional enable
basex_pcs_pma_0
sfp_pcspma (
// Transceiver Interface
.gtrefclk_p(sfp_mgt_refclk_0_p),
.gtrefclk_n(sfp_mgt_refclk_0_n),
.gtrefclk_out(sfp_gtrefclk),
.gtrefclk_bufg_out(sfp_gtrefclk_bufg),
.txp(sfp_tx_p),
.txn(sfp_tx_n),
.rxp(sfp_rx_p),
.rxn(sfp_rx_n),
.resetdone(sfp_resetdone),
.userclk_out(sfp_txuserclk),
.userclk2_out(sfp_txuserclk2),
.rxuserclk_out(sfp_rxuserclk),
.rxuserclk2_out(sfp_rxuserclk2),
.independent_clock_bufg(clk_int),
.pma_reset_out(sfp_pma_reset),
.mmcm_locked_out(sfp_mmcm_locked),
.gt0_pll0outclk_out(),
.gt0_pll0outrefclk_out(),
.gt0_pll1outclk_out(),
.gt0_pll1outrefclk_out(),
.gt0_pll0lock_out(),
.gt0_pll0refclklost_out(),
// GMII Interface
.gmii_txd(sfp_gmii_txd_int),
.gmii_tx_en(sfp_gmii_tx_en_int),
.gmii_tx_er(sfp_gmii_tx_er_int),
.gmii_rxd(sfp_gmii_rxd_int),
.gmii_rx_dv(sfp_gmii_rx_dv_int),
.gmii_rx_er(sfp_gmii_rx_er_int),
.gmii_isolate(),
// Management: Alternative to MDIO Interface
.configuration_vector(sfp_config_vect),
.status_vector(sfp_status_vect),
// General IO's
.reset(rst_int),
.signal_detect(1'b1)
);
assign sfp_gmii_clk_en_int = 1'b1;
// SGMII interface debug:
// SW4:1 (sw[3]) off for payload byte, on for status vector
// SW4:4 (sw[0]) off for LSB of status vector, on for MSB
// assign led = sw[3] ? (sw[0] ? sfp_status_vect[15:8] : sfp_status_vect[7:0]) : led_int;
wire [3:0] phy_rxd_int;
wire phy_rx_ctl_int;
@@ -334,7 +455,7 @@ core_inst (
.btnr(btnr_int),
.btnc(btnc_int),
.sw(sw_int),
.led(led_int),
.led(led),
/*
* UART: 115200 bps, 8N1
@@ -352,6 +473,21 @@ core_inst (
.i2c_sda_i(i2c_sda_i),
.i2c_sda_o(i2c_sda_o),
/*
* Ethernet: 1000BASE-X SFP
*/
.sfp_gmii_clk(sfp_gmii_clk_int),
.sfp_gmii_rst(sfp_gmii_rst_int),
.sfp_gmii_clk_en(sfp_gmii_clk_en_int),
.sfp_gmii_rxd(sfp_gmii_rxd_int),
.sfp_gmii_rx_dv(sfp_gmii_rx_dv_int),
.sfp_gmii_rx_er(sfp_gmii_rx_er_int),
.sfp_gmii_txd(sfp_gmii_txd_int),
.sfp_gmii_tx_en(sfp_gmii_tx_en_int),
.sfp_gmii_tx_er(sfp_gmii_tx_er_int),
.sfp_tx_disable(sfp_tx_disable),
.sfp_rx_los(sfp_rx_los),
/*
* Ethernet: 1000BASE-T RGMII
*/

View File

@@ -22,7 +22,7 @@ module fpga_core #
// vendor ("GENERIC", "XILINX", "ALTERA")
parameter string VENDOR = "XILINX",
// device family
parameter string FAMILY = "kintex7",
parameter string FAMILY = "artix7",
// Use 90 degree clock for RGMII transmit
parameter logic USE_CLK90 = 1'b1
)
@@ -62,6 +62,21 @@ module fpga_core #
input wire logic i2c_sda_i,
output wire logic i2c_sda_o,
/*
* Ethernet: 1000BASE-X SFP
*/
input wire logic sfp_gmii_clk,
input wire logic sfp_gmii_rst,
input wire logic sfp_gmii_clk_en,
input wire logic [7:0] sfp_gmii_rxd,
input wire logic sfp_gmii_rx_dv,
input wire logic sfp_gmii_rx_er,
output wire logic [7:0] sfp_gmii_txd,
output wire logic sfp_gmii_tx_en,
output wire logic sfp_gmii_tx_er,
output wire logic sfp_tx_disable,
input wire logic sfp_rx_los,
/*
* Ethernet: 1000BASE-T
*/
@@ -157,7 +172,7 @@ xfcp_stats_inst (
.s_axis_stat(axis_stat)
);
taxi_axis_if #(.DATA_W(16), .KEEP_W(1), .KEEP_EN(0), .LAST_EN(0), .USER_EN(1), .USER_W(1), .ID_EN(1), .ID_W(10)) axis_eth_stat[1]();
taxi_axis_if #(.DATA_W(16), .KEEP_W(1), .KEEP_EN(0), .LAST_EN(0), .USER_EN(1), .USER_W(1), .ID_EN(1), .ID_W(10)) axis_eth_stat[2]();
taxi_axis_arb_mux #(
.S_COUNT($size(axis_eth_stat)),
@@ -229,7 +244,7 @@ taxi_eth_mac_1g_rgmii_fifo #(
.RX_FIFO_DEPTH(16384),
.RX_FRAME_FIFO(1)
)
eth_mac_inst (
baset_mac_inst (
.gtx_clk(clk),
.gtx_clk90(clk90),
.gtx_rst(rst),
@@ -288,6 +303,94 @@ eth_mac_inst (
.cfg_rx_enable(1'b1)
);
// SFP+
assign sfp_tx_disable = 1'b0;
taxi_axis_if #(.DATA_W(8), .ID_W(8), .USER_EN(1), .USER_W(1)) axis_sfp_eth();
taxi_axis_if #(.DATA_W(96), .KEEP_W(1), .ID_W(8)) axis_sfp_tx_cpl();
taxi_eth_mac_1g_fifo #(
.PADDING_EN(1),
.MIN_FRAME_LEN(64),
.STAT_EN(1),
.STAT_TX_LEVEL(1),
.STAT_RX_LEVEL(1),
.STAT_ID_BASE(16+16),
.STAT_UPDATE_PERIOD(1024),
.STAT_STR_EN(1),
.STAT_PREFIX_STR("SFP"),
.TX_FIFO_DEPTH(16384),
.TX_FRAME_FIFO(1),
.RX_FIFO_DEPTH(16384),
.RX_FRAME_FIFO(1)
)
sfp_mac_inst (
.rx_clk(sfp_gmii_clk),
.rx_rst(sfp_gmii_rst),
.tx_clk(sfp_gmii_clk),
.tx_rst(sfp_gmii_rst),
.logic_clk(clk),
.logic_rst(rst),
/*
* Transmit interface (AXI stream)
*/
.s_axis_tx(axis_sfp_eth),
.m_axis_tx_cpl(axis_sfp_tx_cpl),
/*
* Receive interface (AXI stream)
*/
.m_axis_rx(axis_sfp_eth),
/*
* GMII interface
*/
.gmii_rxd(sfp_gmii_rxd),
.gmii_rx_dv(sfp_gmii_rx_dv),
.gmii_rx_er(sfp_gmii_rx_er),
.gmii_txd(sfp_gmii_txd),
.gmii_tx_en(sfp_gmii_tx_en),
.gmii_tx_er(sfp_gmii_tx_er),
/*
* Control
*/
.rx_clk_enable(sfp_gmii_clk_en),
.tx_clk_enable(sfp_gmii_clk_en),
.rx_mii_select(1'b0),
.tx_mii_select(1'b0),
/*
* Statistics
*/
.stat_clk(clk),
.stat_rst(rst),
.m_axis_stat(axis_eth_stat[1]),
/*
* Status
*/
.tx_error_underflow(),
.tx_fifo_overflow(),
.tx_fifo_bad_frame(),
.tx_fifo_good_frame(),
.rx_error_bad_frame(),
.rx_error_bad_fcs(),
.rx_fifo_overflow(),
.rx_fifo_bad_frame(),
.rx_fifo_good_frame(),
/*
* Configuration
*/
.cfg_tx_max_pkt_len(16'd9218),
.cfg_tx_ifg(8'd12),
.cfg_tx_enable(1'b1),
.cfg_rx_max_pkt_len(16'd9218),
.cfg_rx_enable(1'b1)
);
endmodule
`resetall

View File

@@ -0,0 +1,58 @@
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2014-2026 FPGA Ninja, LLC
#
# Authors:
# - Alex Forencich
#
# XDC constraints for the Xilinx AC701 board
# part: xc7a200tfbg676-2
# GTX for Ethernet
set_property -dict {LOC AC12} [get_ports sfp_rx_p] ;# MGTXRXP0_213 GTPE2_CHANNEL_X0Y0 / GTPE2_COMMON_X0Y0 from P3.13
set_property -dict {LOC AD12} [get_ports sfp_rx_n] ;# MGTXRXN0_213 GTPE2_CHANNEL_X0Y0 / GTPE2_COMMON_X0Y0 from P3.12
set_property -dict {LOC AC10} [get_ports sfp_tx_p] ;# MGTXTXP0_213 GTPE2_CHANNEL_X0Y0 / GTPE2_COMMON_X0Y0 from P3.18
set_property -dict {LOC AD10} [get_ports sfp_tx_n] ;# MGTXTXN0_213 GTPE2_CHANNEL_X0Y0 / GTPE2_COMMON_X0Y0 from P3.19
set_property -dict {LOC AA13} [get_ports sfp_mgt_refclk_0_p] ;# MGTREFCLK0P_213 from U3.10
set_property -dict {LOC AB13} [get_ports sfp_mgt_refclk_0_n] ;# MGTREFCLK0N_213 from U3.11
#set_property -dict {LOC AA11} [get_ports sfp_mgt_refclk_1_p] ;# MGTREFCLK1P_213 from U4.10
#set_property -dict {LOC AB11} [get_ports sfp_mgt_refclk_1_n] ;# MGTREFCLK1N_213 from U4.11
#set_property -dict {LOC D23 IOSTANDARD LVDS_25} [get_ports sfp_recclk_p] ;# to Si5324 U24.16 CKIN1_P
#set_property -dict {LOC D24 IOSTANDARD LVDS_25} [get_ports sfp_recclk_n] ;# to Si5324 U24.17 CKIN1_N
#set_property -dict {LOC B24 IOSTANDARD LVCMOS25 SLEW SLOW DRIVE 12} [get_ports si5324_rst]
#set_property -dict {LOC M19 IOSTANDARD LVCMOS25 PULLUP true} [get_ports si5324_int]
set_property -dict {LOC R18 IOSTANDARD LVCMOS33 SLEW SLOW DRIVE 12} [get_ports {sfp_tx_disable}]
set_property -dict {LOC R23 IOSTANDARD LVCMOS33} [get_ports {sfp_rx_los}]
# U3 clock mux for SFP_MGT_CLK_0
# 2'b00 = EPHYCLK_Q0 (125 MHz)
# 2'b01 = SI5324_OUT0
# 2'b10 = FMC1_HPC_GBTCLK0
# 2'b00 = NC
set_property -dict {LOC B26 IOSTANDARD LVCMOS25 SLEW SLOW DRIVE 12} [get_ports "sfp_mgt_clk_sel[0]"]
set_property -dict {LOC C24 IOSTANDARD LVCMOS25 SLEW SLOW DRIVE 12} [get_ports "sfp_mgt_clk_sel[1]"]
# 125 MHz MGT reference clock (SGMII, 1000BASE-X)
create_clock -period 8.000 -name sfp_mgt_refclk_0 [get_ports sfp_mgt_refclk_0_p]
# U4 clock mux for SFP_MGT_CLK_1
# 2'b00 = SMA_MGT_REFCLK
# 2'b01 = SI5324_OUT1
# 2'b10 = FMC1_HPC_GBTCLK1
# 2'b00 = NC
#set_property -dict {LOC A24 IOSTANDARD LVCMOS25 SLEW SLOW DRIVE 12} [get_ports "pcie_mgt_clk_sel[0]"]
#set_property -dict {LOC C26 IOSTANDARD LVCMOS25 SLEW SLOW DRIVE 12} [get_ports "pcie_mgt_clk_sel[1]"]
# 125 MHz MGT reference clock (SGMII, 1000BASE-X)
#create_clock -period 6.400 -name sgmii_mgt_refclk_1 [get_ports sfp_mgt_refclk_1_p]
#set_false_path -to [get_ports {si5324_rst}]
#set_output_delay 0 [get_ports {si5324_rst}]
#set_false_path -from [get_ports {si5324_int}]
#set_input_delay 0 [get_ports {si5324_int}]
set_false_path -to [get_ports {sfp_tx_disable}]
set_output_delay 0 [get_ports {sfp_tx_disable}]

View File

@@ -23,6 +23,7 @@ COCOTB_TOPLEVEL = $(DUT)
MODULE = $(COCOTB_TEST_MODULES)
TOPLEVEL = $(COCOTB_TOPLEVEL)
VERILOG_SOURCES += $(RTL_DIR)/$(DUT).sv
VERILOG_SOURCES += $(TAXI_SRC_DIR)/eth/rtl/taxi_eth_mac_1g_fifo.f
VERILOG_SOURCES += $(TAXI_SRC_DIR)/eth/rtl/taxi_eth_mac_1g_rgmii_fifo.f
VERILOG_SOURCES += $(TAXI_SRC_DIR)/xfcp/rtl/taxi_xfcp_if_uart.f
VERILOG_SOURCES += $(TAXI_SRC_DIR)/xfcp/rtl/taxi_xfcp_switch.sv

View File

@@ -12,7 +12,6 @@ Authors:
import logging
import os
import pytest
import cocotb_test.simulator
import cocotb
@@ -30,14 +29,21 @@ class TB:
self.log = logging.getLogger("cocotb.tb")
self.log.setLevel(logging.DEBUG)
# cocotb.start_soon(Clock(dut.phy_sgmii_clk, 8, units="ns").start())
self.baset_phy = RgmiiPhy(dut.phy_txd, dut.phy_tx_ctl, dut.phy_tx_clk,
dut.phy_rxd, dut.phy_rx_ctl, dut.phy_rx_clk, speed=speed)
cocotb.start_soon(Clock(dut.sfp_gmii_clk, 8, units="ns").start())
self.sfp_source = GmiiSource(dut.sfp_gmii_rxd, dut.sfp_gmii_rx_er, dut.sfp_gmii_rx_dv,
dut.sfp_gmii_clk, dut.sfp_gmii_rst, dut.sfp_gmii_clk_en)
self.sfp_sink = GmiiSink(dut.sfp_gmii_txd, dut.sfp_gmii_tx_er, dut.sfp_gmii_tx_en,
dut.sfp_gmii_clk, dut.sfp_gmii_rst, dut.sfp_gmii_clk_en)
self.uart_source = UartSource(dut.uart_rxd, baud=921600, bits=8, stop_bits=1)
self.uart_sink = UartSink(dut.uart_txd, baud=921600, bits=8, stop_bits=1)
dut.sfp_gmii_clk_en.setimmediatevalue(1)
dut.btnu.setimmediatevalue(0)
dut.btnl.setimmediatevalue(0)
dut.btnd.setimmediatevalue(0)
@@ -51,16 +57,19 @@ class TB:
async def init(self):
self.dut.rst.setimmediatevalue(0)
self.dut.sfp_gmii_rst.setimmediatevalue(0)
for k in range(10):
await RisingEdge(self.dut.clk)
self.dut.rst.value = 1
self.dut.sfp_gmii_rst.value = 1
for k in range(10):
await RisingEdge(self.dut.clk)
self.dut.rst.value = 0
self.dut.sfp_gmii_rst.value = 0
async def _run_clk(self):
t = Timer(2, 'ns')
@@ -130,6 +139,10 @@ async def run_test(dut):
tests.append(cocotb.start_soon(mac_test(tb, tb.baset_phy.rx, tb.baset_phy.tx)))
tb.log.info("Start SFP MAC loopback test")
tests.append(cocotb.start_soon(mac_test(tb, tb.sfp_source, tb.sfp_sink)))
await Combine(*tests)
await RisingEdge(dut.clk)
@@ -164,6 +177,7 @@ def test_fpga_core(request):
verilog_sources = [
os.path.join(rtl_dir, f"{dut}.sv"),
os.path.join(taxi_src_dir, "eth", "rtl", "taxi_eth_mac_1g_fifo.f"),
os.path.join(taxi_src_dir, "eth", "rtl", "taxi_eth_mac_1g_rgmii_fifo.f"),
os.path.join(taxi_src_dir, "xfcp", "rtl", "taxi_xfcp_if_uart.f"),
os.path.join(taxi_src_dir, "xfcp", "rtl", "taxi_xfcp_switch.sv"),