ethernet: add HWITL ethernet test

This commit is contained in:
Fischer Moseley 2024-11-26 20:51:18 -08:00
parent 2761507803
commit 363bef8d87
8 changed files with 270 additions and 125 deletions

View File

@ -20,9 +20,9 @@ class EthernetIOCoreExample(Elaboratable):
vendor="xilinx",
toolchain="vivado",
refclk_freq=50e6,
clk_freq = 50e6,
fpga_ip_addr = "10.0.0.2",
host_ip_addr = "10.0.0.1",
clk_freq=50e6,
fpga_ip_addr="10.0.0.2",
host_ip_addr="10.0.0.1",
udp_port=2000,
)
@ -44,7 +44,7 @@ class EthernetIOCoreExample(Elaboratable):
("i", "clk", ClockSignal()),
("o", "ethclk", ethclk.clk),
)
platform.add_file("divider.sv", open("divider.sv"))
platform.add_file("divider.sv", open("../common/divider.sv"))
# Add Manta as a submodule
m.submodules.manta = DomainRenamer("ethclk")(self.manta)
@ -55,46 +55,46 @@ class EthernetIOCoreExample(Elaboratable):
m.d.comb += led.o.eq(self.leds[i])
m.submodules += led
# Wire Ethernet pins to the Manta instance
# This is only required for Amaranth < 0.5.2
eth_pin_names = ["mdio", "mdc", "reset", "rxd", "rxerr", "txd", "txen", "crs_dv", "int", "clk"]
eth_pin_names = [
"mdio",
"mdc",
"reset",
"rxd",
"rxerr",
"txd",
"txen",
"crs_dv",
"int",
"clk",
]
eth_pin_dirs = {name: "-" for name in eth_pin_names}
eth_pins = platform.request("eth", dir=eth_pin_dirs)
# For Amaranth > 0.5.2, this simpler syntax may be used:
# eth_pins = platform.request("eth")
# self.manta.interface.set_phy_io(
# rmii_clocks_ref_clk = eth_pins.clk,
# rmii_rst_n = eth_pins.reset,
# rmii_rx_data = eth_pins.rxd,
# rmii_crs_dv = eth_pins.crs_dv,
# rmii_tx_en = eth_pins.txen,
# rmii_tx_data = eth_pins.txd,
# rmii_mdc = eth_pins.mdc,
# rmii_mdio = eth_pins.mdio,
# )
# Run the PHY's ethclk from the 50MHz divider
m.submodules.eth_clk_io_buf = eth_clk_io_buf = io.Buffer("o", eth_pins.clk)
m.d.comb += eth_clk_io_buf.o.eq(ethclk.clk)
self.manta.interface._phy_io = [
("i", "rmii_clocks_ref_clk", ethclk.clk),
("o", "rmii_rst_n", eth_pins.reset.io),
("i", "rmii_rx_data", eth_pins.rxd.io),
("i", "rmii_crs_dv", eth_pins.crs_dv.io),
("o", "rmii_tx_en", eth_pins.txen.io),
("o", "rmii_tx_data", eth_pins.txd.io),
("o", "rmii_mdc", eth_pins.mdc.io),
("io", "rmii_mdio", eth_pins.mdio.io),
]
# Wire Ethernet pins to the Manta instance
self.manta.interface.set_phy_io(
rmii_clocks_ref_clk=ethclk.clk,
rmii_rst_n=eth_pins.reset.io,
rmii_rx_data=eth_pins.rxd.io,
rmii_crs_dv=eth_pins.crs_dv.io,
rmii_tx_en=eth_pins.txen.io,
rmii_tx_data=eth_pins.txd.io,
rmii_mdc=eth_pins.mdc.io,
rmii_mdio=eth_pins.mdio.io,
)
return m
def test(self):
# Build and program the FPGA
# self.platform.build(self, do_program=True)
self.platform.build(self, do_program=True)
# Iterate through all the LEDs, blinking them off and on
i = 0
@ -104,11 +104,15 @@ class EthernetIOCoreExample(Elaboratable):
sleep(0.1)
# Amaranth has a built-in build system, and well as a set of platform
# definitions for a huge number of FPGA boards. The class defined above is
# very generic, as it specifies a design independent of any particular FGPA
# board. This means that by changing which platform you pass UARTIOCoreExample
# below, you can port this example to any FPGA board!
# Although Amaranth provides an environment that is almost entirely independent
# of FPGA vendor or family, it does not provide any facilites for clock
# generation. As a result, this example design includes an external Verilog
# snippet containing a clock generator created by Vivado's Clock Wizard.
# This uses a MMCM clock generation primitive to make a 50MHz clock from the
# onboard 100MHz oscillator, in order to drive the Ethernet PHY. This primitive
# is only available on Xilinx Series-7 parts, so this example will only work on
# Series-7 parts clocked at 100MHz that have RMII PHYs connected...which is
# pretty much just the Nexys4DDR and the Arty A7 :)
if __name__ == "__main__":
from amaranth_boards.nexys4ddr import Nexys4DDRPlatform

1
examples/common/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
!divider.sv

View File

@ -51,7 +51,7 @@
//----------------------------------------------------------------------------
// User entered comments
//----------------------------------------------------------------------------
// popopopopopopopopopopop
//
//
//----------------------------------------------------------------------------
// Output Output Phase Duty Cycle Pk-to-Pk Phase

View File

@ -4,6 +4,7 @@ from random import getrandbits
from amaranth import *
from amaranth.hdl import IOPort
from manta.ethernet.phy_io_defs import phy_io_mapping
from manta.ethernet.sink_bridge import UDPSinkBridge
from manta.ethernet.source_bridge import UDPSourceBridge
from manta.utils import *
@ -76,7 +77,8 @@ class EthernetInterface(Elaboratable):
self.bus_i = Signal(InternalBus())
self.bus_o = Signal(InternalBus())
self._phy_io = self._define_phy_io()
# Define PHY IO, assuming that we're in a Verilog-based workflow.
self._define_phy_io(self._phy)
clk_freq_rounded = round(self._clk_freq)
self._dhcp_start = Signal()
@ -158,95 +160,24 @@ class EthernetInterface(Elaboratable):
octets = [bin(int(o))[2:].zfill(8) for o in ip_addr.split(".")]
return int("".join(octets), 2)
def _define_phy_io(self):
if self._phy in ["LiteEthPHYMII"]:
return [
("i", "mii_clocks_tx", mii_clocks_tx := IOPort(1)),
("i", "mii_clocks_rx", mii_clocks_rx := IOPort(1)),
("o", "mii_rst_n", mii_rst_n := IOPort(1)),
("io", "mii_mdio", mii_mdio := IOPort(1)),
("o", "mii_mdc", mii_mdc := IOPort(1)),
("i", "mii_rx_dv", mii_rx_dv := IOPort(1)),
("i", "mii_rx_er", mii_rx_er := Signal()),
("i", "mii_rx_data", mii_rx_data := IOPort(4)),
("o", "mii_tx_en", mii_tx_en := IOPort(1)),
("o", "mii_tx_data", mii_tx_data := IOPort(4)),
("i", "mii_col", mii_col := IOPort(1)),
("i", "mii_crs", mii_crs := IOPort(1)),
]
def _define_phy_io(self, phy):
phy_io = phy_io_mapping[phy]
elif self._phy in ["LiteEthPHYRMII"]:
return [
("i", "rmii_clocks_ref_clk", rmii_clocks_ref_clk := IOPort(1)),
("o", "rmii_rst_n", rmii_rst_n := IOPort(1)),
("i", "rmii_rx_data", rmii_rx_data := IOPort(2)),
("i", "rmii_crs_dv", rmii_crs_dv := IOPort(1)),
("o", "rmii_tx_en", rmii_tx_en := IOPort(1)),
("o", "rmii_tx_data", rmii_tx_data := IOPort(2)),
("o", "rmii_mdc", rmii_mdc := IOPort(1)),
("io", "rmii_mdio", rmii_mdio := IOPort(1)),
]
self._phy_io = [
(p.dir, p.name, IOPort(width=p.width, name=p.name)) for p in phy_io
]
elif self._phy in [
"LiteEthPHYGMII",
"LiteEthPHYGMIIMII",
]:
return [
("i", "gmii_clocks_tx", gmii_clocks_tx := IOPort(1)),
("o", "gmii_clocks_gtx", gmii_clocks_gtx := IOPort(1)),
("i", "gmii_clocks_rx", gmii_clocks_rx := IOPort(1)),
("o", "gmii_rst_n", gmii_rst_n := IOPort(1)),
("i", "gmii_int_n", gmii_int_n := IOPort(1)),
("io", "gmii_mdio", gmii_mdio := IOPort(1)),
("o", "gmii_mdc", gmii_mdc := IOPort(1)),
("i", "gmii_rx_dv", gmii_rx_dv := IOPort(1)),
("i", "gmii_rx_er", gmii_rx_er := IOPort(1)),
("i", "gmii_rx_data", gmii_rx_data := IOPort(8)),
("o", "gmii_tx_en", gmii_tx_en := IOPort(1)),
("o", "gmii_tx_er", gmii_tx_er := IOPort(1)),
("o", "gmii_tx_data", gmii_tx_data := IOPort(8)),
("i", "gmii_col", gmii_col := IOPort(1)),
("i", "gmii_crs", gmii_crs := IOPort(1)),
]
def set_phy_io(self, **kwargs):
# Given the user's IO, create a list of tuples that can be passed to Instance
# Only to be used in Amaranth-Native workflows!
elif self._phy in [
"LiteEthS7PHYRGMII",
"LiteEthECP5PHYRGMII",
]:
return [
("o", "rgmii_clocks_tx", rgmii_clocks_tx := IOPort(1)),
("i", "rgmii_clocks_rx", rgmii_clocks_rx := IOPort(1)),
("o", "rgmii_rst_n", rgmii_rst_n := IOPort(1)),
("i", "rgmii_int_n", rgmii_int_n := IOPort(1)),
("io", "rgmii_mdio", rgmii_mdio := IOPort(1)),
("o", "rgmii_mdc", rgmii_mdc := IOPort(1)),
("i", "rgmii_rx_ctl", rgmii_rx_ctl := IOPort(1)),
("i", "rgmii_rx_data", rgmii_rx_data := IOPort(4)),
("o", "rgmii_tx_ctl", rgmii_tx_ctl := IOPort(1)),
("o", "rgmii_tx_data", rgmii_tx_data := IOPort(4)),
]
all_phy_io = phy_io_mapping.values()
all_io_definitions = [io for phy_io in all_phy_io for io in phy_io]
find_io_def = lambda name: next(
(iod for iod in all_io_definitions if iod.name == name), None
)
elif self._phy in [
"A7_1000BASEX",
"A7_2500BASEX",
"K7_1000BASEX",
"K7_2500BASEX",
"KU_1000BASEX",
"KU_2500BASEX",
"USP_GTH_1000BASEX",
"USP_GTH_2500BASEX",
"USP_GTY_1000BASEX",
"USP_GTY_2500BASEX",
]:
return [
("i", "sgmii_refclk", sgmii_refclk := IOPort(1)),
("i", "sgmii_rst", sgmii_rst := IOPort(1)),
("o", "sgmii_txp", sgmii_txp := IOPort(1)),
("o", "sgmii_txn", sgmii_txn := IOPort(1)),
("i", "sgmii_rxp", sgmii_rxp := IOPort(1)),
("i", "sgmii_rxn", sgmii_rxn := IOPort(1)),
("o", "sgmii_link_up", sgmii_link_up := IOPort(1)),
]
self._phy_io = [(find_io_def(k).dir, k, v) for k, v in kwargs.items()]
def elaborate(self, platform):
m = Module()

View File

@ -0,0 +1,101 @@
from dataclasses import dataclass
@dataclass
class IODefinition:
dir: str
name: str
width: int
mii_phy_io = [
IODefinition("i", "mii_clocks_tx", 1),
IODefinition("i", "mii_clocks_rx", 1),
IODefinition("o", "mii_rst_n", 1),
IODefinition("io", "mii_mdio", 1),
IODefinition("o", "mii_mdc", 1),
IODefinition("i", "mii_rx_dv", 1),
IODefinition("i", "mii_rx_er", 1),
IODefinition("i", "mii_rx_data", 4),
IODefinition("o", "mii_tx_en", 1),
IODefinition("o", "mii_tx_data", 4),
IODefinition("i", "mii_col", 1),
IODefinition("i", "mii_crs", 1),
]
rmii_phy_io = [
IODefinition("i", "rmii_clocks_ref_clk", 1),
IODefinition("o", "rmii_rst_n", 1),
IODefinition("i", "rmii_rx_data", 2),
IODefinition("i", "rmii_crs_dv", 1),
IODefinition("o", "rmii_tx_en", 1),
IODefinition("o", "rmii_tx_data", 2),
IODefinition("o", "rmii_mdc", 1),
IODefinition("io", "rmii_mdio", 1),
]
gmii_phy_io = [
IODefinition("i", "gmii_clocks_tx", 1),
IODefinition("o", "gmii_clocks_gtx", 1),
IODefinition("i", "gmii_clocks_rx", 1),
IODefinition("o", "gmii_rst_n", 1),
IODefinition("i", "gmii_int_n", 1),
IODefinition("io", "gmii_mdio", 1),
IODefinition("o", "gmii_mdc", 1),
IODefinition("i", "gmii_rx_dv", 1),
IODefinition("i", "gmii_rx_er", 1),
IODefinition("i", "gmii_rx_data", 8),
IODefinition("o", "gmii_tx_en", 1),
IODefinition("o", "gmii_tx_er", 1),
IODefinition("o", "gmii_tx_data", 8),
IODefinition("i", "gmii_col", 1),
IODefinition("i", "gmii_crs", 1),
]
rgmii_phy_io = [
IODefinition("o", "rgmii_clocks_tx", 1),
IODefinition("i", "rgmii_clocks_rx", 1),
IODefinition("o", "rgmii_rst_n", 1),
IODefinition("i", "rgmii_int_n", 1),
IODefinition("io", "rgmii_mdio", 1),
IODefinition("o", "rgmii_mdc", 1),
IODefinition("i", "rgmii_rx_ctl", 1),
IODefinition("i", "rgmii_rx_data", 4),
IODefinition("o", "rgmii_tx_ctl", 1),
IODefinition("o", "rgmii_tx_data", 4),
]
sgmii_phy_io = [
IODefinition("i", "sgmii_refclk", 1),
IODefinition("i", "sgmii_rst", 1),
IODefinition("o", "sgmii_txp", 1),
IODefinition("o", "sgmii_txn", 1),
IODefinition("i", "sgmii_rxp", 1),
IODefinition("i", "sgmii_rxn", 1),
IODefinition("o", "sgmii_link_up", 1),
]
phy_io_mapping = {
# MII
"LiteEthPHYMII": mii_phy_io,
# RMII
"LiteEthPHYRMII": rmii_phy_io,
# GMII
"LiteEthPHYGMII": gmii_phy_io,
"LiteEthPHYGMIIMII": gmii_phy_io,
# RGMII
"LiteEthS7PHYRGMII": rgmii_phy_io,
"LiteEthECP5PHYRGMII": rgmii_phy_io,
# SGMII
"A7_1000BASEX": sgmii_phy_io,
"A7_2500BASEX": sgmii_phy_io,
"K7_1000BASEX": sgmii_phy_io,
"K7_2500BASEX": sgmii_phy_io,
"KU_1000BASEX": sgmii_phy_io,
"KU_2500BASEX": sgmii_phy_io,
"USP_GTH_1000BASEX": sgmii_phy_io,
"USP_GTH_2500BASEX": sgmii_phy_io,
"USP_GTY_1000BASEX": sgmii_phy_io,
"USP_GTY_2500BASEX": sgmii_phy_io,
}

View File

@ -0,0 +1,108 @@
import pytest
import time
from random import getrandbits
from amaranth import *
from amaranth.lib import io
from amaranth_boards.nexys4ddr import Nexys4DDRPlatform
from manta import *
from manta.utils import *
class EthernetMemoryCoreTest(Elaboratable):
def __init__(self, platform):
self.platform = platform
self.width = 28
self.depth = 612
# Create Manta instance
self.manta = Manta()
# Configure it to communicate over Ethernet
self.manta.interface = EthernetInterface(
phy="LiteEthPHYRMII",
device="xc7a",
vendor="xilinx",
toolchain="vivado",
refclk_freq=50e6,
clk_freq=50e6,
fpga_ip_addr="10.0.0.2",
host_ip_addr="10.0.0.1",
udp_port=2000,
)
self.manta.cores.mem = MemoryCore("bidirectional", self.width, self.depth)
def elaborate(self, platform):
m = Module()
# Create 50MHz clock domain
m.domains.ethclk = ethclk = ClockDomain()
m.submodules.divider = Instance(
"divider",
("i", "clk", ClockSignal()),
("o", "ethclk", ethclk.clk),
)
platform.add_file("../examples/common/divider.sv", open("divider.sv"))
# Add Manta as a submodule
m.submodules.manta = DomainRenamer("ethclk")(self.manta)
# This is only required for Amaranth < 0.5.2
eth_pin_names = [
"mdio",
"mdc",
"reset",
"rxd",
"rxerr",
"txd",
"txen",
"crs_dv",
"int",
"clk",
]
eth_pin_dirs = {name: "-" for name in eth_pin_names}
eth_pins = platform.request("eth", dir=eth_pin_dirs)
# For Amaranth > 0.5.2, this simpler syntax may be used:
# eth_pins = platform.request("eth")
# Run the PHY's ethclk from the 50MHz divider
m.submodules.eth_clk_io_buf = eth_clk_io_buf = io.Buffer("o", eth_pins.clk)
m.d.comb += eth_clk_io_buf.o.eq(ethclk.clk)
# Wire Ethernet pins to the Manta instance
self.manta.interface.set_phy_io(
rmii_clocks_ref_clk=ethclk.clk,
rmii_rst_n=eth_pins.reset.io,
rmii_rx_data=eth_pins.rxd.io,
rmii_crs_dv=eth_pins.crs_dv.io,
rmii_tx_en=eth_pins.txen.io,
rmii_tx_data=eth_pins.txd.io,
rmii_mdc=eth_pins.mdc.io,
rmii_mdio=eth_pins.mdio.io,
)
return m
def verify(self):
self.platform.build(self, do_program=True)
# Wait for the FPGA to acquire IP address
time.sleep(5)
for addr in jumble(range(self.depth)):
data = getrandbits(self.width)
self.manta.cores.mem.write(addr, data)
# Verify the same number is returned when reading
readback = self.manta.cores.mem.read(addr)
if readback != data:
raise ValueError(
f"Memory read from {hex(addr)} returned {hex(readback)} instead of {hex(data)}"
)
@pytest.mark.skipif(not xilinx_tools_installed(), reason="no toolchain installed")
def test_mem_core_xilinx():
EthernetMemoryCoreTest(Nexys4DDRPlatform()).verify()

View File

@ -33,10 +33,10 @@ from amaranth_boards.icestick import ICEStickPlatform
from amaranth_boards.nexys4ddr import Nexys4DDRPlatform
# Import Examples
from examples.amaranth.ethernet_io_core import EthernetIOCoreExample
from examples.amaranth.uart_io_core import UARTIOCoreExample
from examples.amaranth.uart_logic_analyzer import UARTLogicAnalyzerExample
from examples.amaranth.uart_memory_core import UARTMemoryCoreExample
from examples.amaranth.ethernet_io_core import EthernetIOCoreExample
# Manually specify a list of examples/platforms to test.

View File

@ -79,7 +79,7 @@ class MemoryCoreLoopbackTest(Elaboratable):
if self.mode in ["bidirectional", "host_to_fpga"]:
for addr in jumble(range(self.depth)):
# Write a random balue to a random bus address
# Write a random value to a random bus address
data = getrandbits(self.width)
self.manta.cores.mem.write(addr, data)
@ -87,7 +87,7 @@ class MemoryCoreLoopbackTest(Elaboratable):
readback = self.read_user_side(addr)
if readback != data:
raise ValueError(
f"Memory read from {hex(addr)} returned {hex(data)} instead of {hex(readback)}."
f"Memory read from {hex(addr)} returned {hex(readback)} instead of {hex(data)}."
)
if self.mode in ["bidirectional", "fpga_to_host"]:
@ -100,7 +100,7 @@ class MemoryCoreLoopbackTest(Elaboratable):
readback = self.manta.cores.mem.read(addr)
if readback != data:
raise ValueError(
f"Memory read from {hex(addr)} returned {hex(data)} instead of {hex(readback)}."
f"Memory read from {hex(addr)} returned {hex(readback)} instead of {hex(data)}."
)