diff --git a/examples/amaranth/ethernet_io_core.py b/examples/amaranth/ethernet_io_core.py index b0be3ef..e2919ac 100644 --- a/examples/amaranth/ethernet_io_core.py +++ b/examples/amaranth/ethernet_io_core.py @@ -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 diff --git a/examples/common/.gitignore b/examples/common/.gitignore new file mode 100644 index 0000000..c54b99a --- /dev/null +++ b/examples/common/.gitignore @@ -0,0 +1 @@ +!divider.sv \ No newline at end of file diff --git a/examples/verilog/nexys4_ddr/ether_logic_analyzer_io_core/divider.sv b/examples/common/divider.sv similarity index 99% rename from examples/verilog/nexys4_ddr/ether_logic_analyzer_io_core/divider.sv rename to examples/common/divider.sv index 623cce7..f6ade06 100644 --- a/examples/verilog/nexys4_ddr/ether_logic_analyzer_io_core/divider.sv +++ b/examples/common/divider.sv @@ -51,7 +51,7 @@ //---------------------------------------------------------------------------- // User entered comments //---------------------------------------------------------------------------- -// popopopopopopopopopopop +// // //---------------------------------------------------------------------------- // Output Output Phase Duty Cycle Pk-to-Pk Phase diff --git a/src/manta/ethernet/__init__.py b/src/manta/ethernet/__init__.py index ecef087..36a1c8a 100644 --- a/src/manta/ethernet/__init__.py +++ b/src/manta/ethernet/__init__.py @@ -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() diff --git a/src/manta/ethernet/phy_io_defs.py b/src/manta/ethernet/phy_io_defs.py new file mode 100644 index 0000000..6c52c60 --- /dev/null +++ b/src/manta/ethernet/phy_io_defs.py @@ -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, +} diff --git a/test/test_ethernet_interface.py b/test/test_ethernet_interface.py new file mode 100644 index 0000000..5fc3778 --- /dev/null +++ b/test/test_ethernet_interface.py @@ -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() \ No newline at end of file diff --git a/test/test_examples_build.py b/test/test_examples_build.py index 9dd7bff..db04a28 100644 --- a/test/test_examples_build.py +++ b/test/test_examples_build.py @@ -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. diff --git a/test/test_mem_core_hw.py b/test/test_mem_core_hw.py index 3827557..a8e2c28 100644 --- a/test/test_mem_core_hw.py +++ b/test/test_mem_core_hw.py @@ -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)}." )