Add LiteEth support.
authorJean-François Nguyen <jf@lambdaconcept.com>
Fri, 29 Oct 2021 18:21:24 +0000 (20:21 +0200)
committerJean-François Nguyen <jf@lambdaconcept.com>
Fri, 29 Oct 2021 18:21:24 +0000 (20:21 +0200)
examples/minerva_soc.py
lambdasoc/cores/liteeth.py [new file with mode: 0644]
lambdasoc/periph/eth.py [new file with mode: 0644]
lambdasoc/soc/cpu.py
lambdasoc/software/bios

index 02675f2a72b36a9489210535dca479e1bfcf297c..99b05646c2e82417edc2fce7277192d47bf9f15c 100644 (file)
@@ -19,12 +19,13 @@ from lambdasoc.periph.serial import AsyncSerialPeripheral
 from lambdasoc.periph.sram import SRAMPeripheral
 from lambdasoc.periph.timer import TimerPeripheral
 from lambdasoc.periph.sdram import SDRAMPeripheral
+from lambdasoc.periph.eth import EthernetMACPeripheral
 
 from lambdasoc.soc.cpu import CPUSoC, BIOSBuilder
 
 from lambdasoc.cores.pll.lattice_ecp5 import PLL_LatticeECP5
 from lambdasoc.cores.pll.xilinx_7series import PLL_Xilinx7Series
-from lambdasoc.cores import litedram
+from lambdasoc.cores import litedram, liteeth
 from lambdasoc.cores.utils import request_bare
 
 from lambdasoc.sim.blackboxes.serial import AsyncSerial_Blackbox
@@ -35,13 +36,14 @@ __all__ = ["MinervaSoC"]
 
 
 class _ClockResetGenerator(Elaboratable):
-    def __init__(self, *, sync_clk_freq, with_sdram):
+    def __init__(self, *, sync_clk_freq, with_sdram, with_ethernet):
         if not isinstance(sync_clk_freq, (int, float)) or sync_clk_freq <= 0:
             raise ValueError("Sync domain clock frequency must be a positive integer or float, "
                              "not {!r}"
                              .format(sync_clk_freq))
         self.sync_clk_freq = sync_clk_freq
         self.with_sdram    = bool(with_sdram)
+        self.with_ethernet = bool(with_ethernet)
 
     def elaborate(self, platform):
         m = Module()
@@ -55,6 +57,15 @@ class _ClockResetGenerator(Elaboratable):
         if platform.default_rst is not None:
             m.d.comb += ResetSignal("_ref").eq(platform.request(platform.default_rst, 0).i)
 
+        # On the Arty A7, the DP83848 Ethernet PHY uses a 25 MHz reference clock.
+        if isinstance(platform, ArtyA7_35Platform) and self.with_ethernet:
+            m.domains += ClockDomain("_eth_ref", local=True)
+            m.submodules += Instance("BUFGCE",
+                i_I  = ClockSignal("_eth_ref"),
+                i_CE = ~ResetSignal("_eth_ref"),
+                o_O  = platform.request("eth_clk25", 0).o,
+            )
+
         # The LiteDRAM core provides its own PLL, which drives the litedram_user clock domain.
         # We reuse this clock domain as the sync domain, in order to avoid CDC between LiteDRAM
         # and the SoC interconnect.
@@ -70,6 +81,24 @@ class _ClockResetGenerator(Elaboratable):
                 ResetSignal("sync").eq(ResetSignal("litedram_user")),
             ]
 
+            # On the Arty A7, we still use our own PLL to drive the Ethernet PHY reference clock.
+            if isinstance(platform, ArtyA7_35Platform) and self.with_ethernet:
+                eth_ref_pll_params = PLL_Xilinx7Series.Parameters(
+                    i_domain     = "_ref",
+                    i_freq       = platform.default_clk_frequency,
+                    i_reset_less = platform.default_rst is None,
+                    o_domain     = "_eth_ref",
+                    o_freq       = 25e6,
+                )
+                m.submodules.eth_ref_pll = eth_ref_pll = PLL_Xilinx7Series(eth_ref_pll_params)
+
+                if platform.default_rst is not None:
+                    eth_ref_pll_arst = ~eth_ref_pll.locked | ResetSignal("_ref")
+                else:
+                    eth_ref_pll_arst = ~eth_ref_pll.locked
+
+                m.submodules += ResetSynchronizer(eth_ref_pll_arst, domain="_eth_ref")
+
         # In simulation mode, the sync clock domain is directly driven by the platform clock.
         elif isinstance(platform, CXXRTLPlatform):
             assert self.sync_clk_freq == platform.default_clk_frequency
@@ -87,6 +116,8 @@ class _ClockResetGenerator(Elaboratable):
                     o_domain     = "sync",
                     o_freq       = self.sync_clk_freq,
                 )
+                if self.with_ethernet:
+                    sync_pll_params.add_secondary_output(domain="_eth_ref", freq=25e6)
                 m.submodules.sync_pll = sync_pll = PLL_Xilinx7Series(sync_pll_params)
             elif isinstance(platform, (ECPIX545Platform, ECPIX585Platform)):
                 sync_pll_params = PLL_LatticeECP5.Parameters(
@@ -106,6 +137,8 @@ class _ClockResetGenerator(Elaboratable):
                 sync_pll_arst = ~sync_pll.locked
 
             m.submodules += ResetSynchronizer(sync_pll_arst, domain="sync")
+            if isinstance(platform, ArtyA7_35Platform) and self.with_ethernet:
+                m.submodules += ResetSynchronizer(sync_pll_arst, domain="_eth_ref")
 
         return m
 
@@ -161,6 +194,7 @@ class MinervaSoC(CPUSoC, Elaboratable):
 
         self._sdram  = None
         self._sram   = None
+        self._ethmac = None
 
     @property
     def memory_map(self):
@@ -170,8 +204,10 @@ class MinervaSoC(CPUSoC, Elaboratable):
     def constants(self):
         return super().constants.union(
             SDRAM  = self.sdram .constant_map if self.sdram  is not None else None,
+            ETHMAC = self.ethmac.constant_map if self.ethmac is not None else None,
             SOC    = ConstantMap(
                 WITH_SDRAM        = self.sdram  is not None,
+                WITH_ETHMAC       = self.ethmac is not None,
                 MEMTEST_ADDR_SIZE = 8192,
                 MEMTEST_DATA_SIZE = 8192,
             ),
@@ -206,12 +242,25 @@ class MinervaSoC(CPUSoC, Elaboratable):
         self._sram = SRAMPeripheral(size=size)
         self._decoder.add(self._sram.bus, addr=addr)
 
+    @property
+    def ethmac(self):
+        return self._ethmac
+
+    def add_ethmac(self, core, *, addr, irqno, local_ip, remote_ip):
+        if self._ethmac is not None:
+            raise AttributeError("Ethernet MAC has already been set to {!r}"
+                                 .format(self._ethmac))
+        self._ethmac = EthernetMACPeripheral(core=core, local_ip=local_ip, remote_ip=remote_ip)
+        self._decoder.add(self._ethmac.bus, addr=addr)
+        self.intc.add_irq(self._ethmac.irq, index=irqno)
+
     def elaborate(self, platform):
         m = Module()
 
         m.submodules.crg = _ClockResetGenerator(
             sync_clk_freq = self.sync_clk_freq,
             with_sdram    = self.sdram is not None,
+            with_ethernet = self.ethmac is not None,
         )
 
         m.submodules.cpu        = self.cpu
@@ -227,6 +276,8 @@ class MinervaSoC(CPUSoC, Elaboratable):
             m.submodules.sdram = self.sdram
         if self.sram is not None:
             m.submodules.sram = self.sram
+        if self.ethmac is not None:
+            m.submodules.ethmac = self.ethmac
 
         m.d.comb += [
             self._arbiter.bus.connect(self._decoder.bus),
@@ -257,6 +308,14 @@ if __name__ == "__main__":
     parser.add_argument("--baudrate", type=int,
             default=9600,
             help="UART baudrate (default: 9600)")
+    parser.add_argument("--with-ethernet", action="store_true",
+            help="enable Ethernet")
+    parser.add_argument("--local-ip", type=str,
+            default="192.168.1.50",
+            help="Local IPv4 address (default: 192.168.1.50)")
+    parser.add_argument("--remote-ip", type=str,
+            default="192.168.1.100",
+            help="Remote IPv4 address (default: 192.168.1.100)")
     args = parser.parse_args()
 
     # Platform selection
@@ -326,6 +385,30 @@ if __name__ == "__main__":
         litedram_core = None
         mainram_size  = args.internal_sram_size
 
+    # LiteEth
+
+    if args.with_ethernet:
+        if isinstance(platform, CXXRTLPlatform):
+            raise NotImplementedError("Ethernet is currently unsupported in simulation.")
+        elif isinstance(platform, ArtyA7_35Platform):
+            liteeth_config = liteeth.Artix7Config(
+                phy_iface = "mii",
+                clk_freq  = int(25e6),
+            )
+        elif isinstance(platform, (ECPIX545Platform, ECPIX585Platform)):
+            liteeth_config = liteeth.ECP5Config(
+                phy_iface = "rgmii",
+                clk_freq  = int(125e6),
+            )
+        else:
+            assert False
+
+        liteeth_pins = request_bare(platform, f"eth_{liteeth_config.phy_iface}", 0)
+        liteeth_core = liteeth.Core(liteeth_config, pins=liteeth_pins)
+        liteeth_core.build(liteeth.Builder(), platform, args.build_dir)
+    else:
+        liteeth_core = None
+
     # UART
 
     if isinstance(platform, CXXRTLPlatform):
@@ -385,6 +468,10 @@ if __name__ == "__main__":
     else:
         soc.add_internal_sram(addr=0x40000000, size=args.internal_sram_size)
 
+    if args.with_ethernet:
+        soc.add_ethmac(liteeth_core, addr=0x90000000, irqno=2,
+                       local_ip=args.local_ip, remote_ip=args.remote_ip)
+
     soc.build(build_dir=args.build_dir, do_init=True)
 
     if isinstance(platform, CXXRTLPlatform):
diff --git a/lambdasoc/cores/liteeth.py b/lambdasoc/cores/liteeth.py
new file mode 100644 (file)
index 0000000..1583750
--- /dev/null
@@ -0,0 +1,352 @@
+from abc import ABCMeta, abstractmethod
+import csv
+import jinja2
+import textwrap
+import re
+
+from nmigen import *
+from nmigen import tracer
+from nmigen.utils import log2_int
+from nmigen.hdl.rec import Layout
+from nmigen.build.plat import Platform
+from nmigen.build.run import BuildPlan, BuildProducts
+
+from nmigen_soc import wishbone
+from nmigen_soc.memory import MemoryMap
+
+from .. import __version__
+from ..periph import IRQLine
+
+
+__all__ = [
+    "Config", "ECP5Config", "Artix7Config",
+    "Core",
+    "Builder",
+]
+
+class Config(metaclass=ABCMeta):
+    def __init__(self, *,
+            phy_iface,
+            clk_freq,
+            rx_slots = 2,
+            tx_slots = 2,
+            endianess="little"):
+
+        if phy_iface not in {"mii", "rmii", "rgmii"}:
+            raise ValueError("LiteEth PHY interface must be one of \"mii\", \"rmii\" or "
+                             "\"rgmii\", not {!r}".format(phy_iface))
+        if not isinstance(clk_freq, int) or clk_freq <= 0:
+            raise ValueError("LiteEth clock frequency must be a positive integer, not {!r}"
+                             .format(clk_freq))
+        if not isinstance(rx_slots, int) or rx_slots < 0:
+            raise ValueError("LiteEth Rx FIFO slots must be a non-negative integer, not {!r}"
+                             .format(rx_slots))
+        if not isinstance(tx_slots, int) or tx_slots < 0:
+            raise ValueError("LiteEth Tx FIFO slots must be a non-negative integer, not {!r}"
+                             .format(tx_slots))
+        if endianess not in {"big", "little"}:
+            raise ValueError("LitEth endianess must be one of \"big\" or \"little\", not {!r}"
+                             .format(endianess))
+
+        self.phy_iface = phy_iface
+        self.clk_freq  = clk_freq
+        self.rx_slots  = rx_slots
+        self.tx_slots  = tx_slots
+        self.endianess = endianess
+        # FIXME: hardcoded
+        self.ctrl_addr = 0x0
+        self.data_addr = 0x10000
+
+    @property
+    @abstractmethod
+    def phy_name(self):
+        raise NotImplementedError
+
+    @property
+    @abstractmethod
+    def vendor(self):
+        raise NotImplementedError
+
+
+class ECP5Config(Config):
+    vendor = "lattice"
+
+    @property
+    def phy_name(self):
+        if self.phy_iface in {"mii", "rmii", "gmii"}:
+            return "LiteEthPHY{}".format(self.phy_iface.upper())
+        elif self.phy_iface == "rgmii":
+            return "LiteEthECP5PHYRGMII"
+        else:
+            assert False
+
+
+class Artix7Config(Config):
+    vendor = "xilinx"
+
+    @property
+    def phy_name(self):
+        if self.phy_iface in {"mii", "rmii", "gmii"}:
+            return "LiteEthPHY{}".format(self.phy_iface.upper())
+        elif self.phy_iface == "rgmii":
+            return "LiteEthS7PHYRGMII"
+        else:
+            assert False
+
+
+class Core(Elaboratable):
+    def __init__(self, config, *, pins=None, name=None, src_loc_at=0):
+        if not isinstance(config, Config):
+            raise TypeError("Config must be an instance liteeth.Config, "
+                            "not {!r}"
+                            .format(config))
+        self.config = config
+
+        if name is not None and not isinstance(name, str):
+            raise TypeError("Name must be a string, not {!r}".format(name))
+        self.name = name or tracer.get_var_name(depth=2 + src_loc_at)
+
+        self.irq = IRQLine(name=f"{self.name}_irq")
+
+        self._bus  = None
+        self._pins = pins
+
+    @property
+    def bus(self):
+        if self._bus is None:
+            raise AttributeError("Bus memory map has not been populated. "
+                                 "Core.build(do_build=True) must be called before accessing "
+                                 "Core.bus")
+        return self._bus
+
+    def _populate_map(self, build_products):
+        if not isinstance(build_products, BuildProducts):
+            raise TypeError("Build products must be an instance of BuildProducts, not {!r}"
+                            .format(build_products))
+
+        # LiteEth's Wishbone bus has a granularity of 8 bits.
+        ctrl_map = MemoryMap(addr_width=1, data_width=8)
+        data_map = MemoryMap(addr_width=1, data_width=8)
+
+        csr_csv = build_products.get(f"{self.name}_csr.csv", mode="t")
+        for row in csv.reader(csr_csv.split("\n"), delimiter=","):
+            if not row or row[0][0] == "#": continue
+            res_type, res_name, addr, size, attrs = row
+            if res_type == "csr_register":
+                ctrl_map.add_resource(
+                    object(),
+                    name   = res_name,
+                    addr   = int(addr, 16),
+                    size   = int(size, 10) * 32 // ctrl_map.data_width,
+                    extend = True,
+                )
+
+        # TODO: rephrase
+        # The LiteEth MAC core uses a memory region capable of holding an IEEE 802.3 Ethernet frame
+        # (rounded to the nearest power-of-two) for each Rx/Tx slot.
+        data_map.add_resource(
+            object(),
+            name   = "ethmac_slots",
+            size   = (self.config.rx_slots + self.config.tx_slots) * 2048,
+            extend = True,
+        )
+
+        bus_map = MemoryMap(addr_width=1, data_width=8)
+        bus_map.add_window(ctrl_map, addr=self.config.ctrl_addr, extend=True)
+        bus_map.add_window(data_map, addr=self.config.data_addr, extend=True)
+
+        self._bus = wishbone.Interface(
+            addr_width  = bus_map.addr_width
+                        - log2_int(32 // bus_map.data_width),
+            data_width  = 32,
+            granularity = bus_map.data_width,
+            features    = {"cti", "bte", "err"},
+        )
+        self._bus.memory_map = bus_map
+
+    def build(self, builder, platform, build_dir, *, do_build=True, name_force=False):
+        if not isinstance(builder, Builder):
+            raise TypeError("Builder must be an instance of liteeth.Builder, not {!r}"
+                            .format(builder))
+
+        plan = builder.prepare(self, name_force=name_force)
+        if not do_build:
+            return plan
+
+        products = plan.execute_local(f"{build_dir}/lambdasoc.cores.liteeth") # TODO __package__
+        self._populate_map(products)
+
+        core_src = f"liteeth_core/liteeth_core.v"
+        platform.add_file(core_src, products.get(core_src, mode="t"))
+
+        return products
+
+    def elaborate(self, platform):
+        core_kwargs = {
+                "i_sys_clock" : ClockSignal("sync"),
+                "i_sys_reset" : ResetSignal("sync"),
+
+                "i_wishbone_adr"   : self.bus.adr,
+                "i_wishbone_dat_w" : self.bus.dat_w,
+                "o_wishbone_dat_r" : self.bus.dat_r,
+                "i_wishbone_sel"   : self.bus.sel,
+                "i_wishbone_cyc"   : self.bus.cyc,
+                "i_wishbone_stb"   : self.bus.stb,
+                "o_wishbone_ack"   : self.bus.ack,
+                "i_wishbone_we"    : self.bus.we,
+                "i_wishbone_cti"   : self.bus.cti,
+                "i_wishbone_bte"   : self.bus.bte,
+                "o_wishbone_err"   : self.bus.err,
+
+                "o_interrupt" : self.irq,
+        }
+
+        if self._pins is not None:
+            if self.config.phy_iface == "mii":
+                core_kwargs.update({
+                    "i_mii_eth_clocks_tx" : self._pins.tx_clk,
+                    "i_mii_eth_clocks_rx" : self._pins.rx_clk,
+                    "o_mii_eth_rst_n"     : self._pins.rst,
+                    "io_mii_eth_mdio"     : self._pins.mdio,
+                    "o_mii_eth_mdc"       : self._pins.mdc,
+                    "i_mii_eth_rx_dv"     : self._pins.rx_dv,
+                    "i_mii_eth_rx_er"     : self._pins.rx_er,
+                    "i_mii_eth_rx_data"   : self._pins.rx_data,
+                    "o_mii_eth_tx_en"     : self._pins.tx_en,
+                    "o_mii_eth_tx_data"   : self._pins.tx_data,
+                    "i_mii_eth_col"       : self._pins.col,
+                    "i_mii_eth_crs"       : self._pins.crs,
+                })
+            elif self.config.phy_iface == "rmii":
+                core_kwargs.update({
+                    "o_rmii_eth_clocks_ref_clk" : self._pins.clk,
+                    "o_rmii_eth_rst_n"          : self._pins.rst,
+                    "io_rmii_eth_mdio"          : self._pins.mdio,
+                    "o_rmii_eth_mdc"            : self._pins.mdc,
+                    "i_rmii_eth_crs_dv"         : self._pins.crs_dv,
+                    "i_rmii_eth_rx_data"        : self._pins.rx_data,
+                    "o_rmii_eth_tx_en"          : self._pins.tx_en,
+                    "o_rmii_eth_tx_data"        : self._pins.tx_data,
+                })
+            elif self.config.phy_iface == "gmii":
+                core_kwargs.update({
+                    "o_gmii_eth_clocks_tx" : self._pins.tx_clk,
+                    "i_gmii_eth_clocks_rx" : self._pins.rx_clk,
+                    "o_gmii_eth_rst_n"     : self._pins.rst,
+                    "i_gmii_eth_int_n"     : Const(1),
+                    "io_gmii_eth_mdio"     : self._pins.mdio,
+                    "o_gmii_eth_mdc"       : self._pins.mdc,
+                    "i_gmii_eth_rx_dv"     : self._pins.rx_dv,
+                    "i_gmii_eth_rx_er"     : self._pins.rx_er,
+                    "i_gmii_eth_rx_data"   : self._pins.rx_data,
+                    "o_gmii_eth_tx_en"     : self._pins.tx_en,
+                    "o_gmii_eth_tx_er"     : self._pins.tx_er,
+                    "o_gmii_eth_tx_data"   : self._pins.tx_data,
+                    "i_gmii_eth_col"       : self._pins.col,
+                    "i_gmii_eth_crs"       : self._pins.crs,
+                })
+            elif self.config.phy_iface == "rgmii":
+                core_kwargs.update({
+                    "o_rgmii_eth_clocks_tx" : self._pins.tx_clk,
+                    "i_rgmii_eth_clocks_rx" : self._pins.rx_clk,
+                    "o_rgmii_eth_rst_n"     : self._pins.rst,
+                    "i_rgmii_eth_int_n"     : Const(1),
+                    "io_rgmii_eth_mdio"     : self._pins.mdio,
+                    "o_rgmii_eth_mdc"       : self._pins.mdc,
+                    "i_rgmii_eth_rx_ctl"    : self._pins.rx_ctrl,
+                    "i_rgmii_eth_rx_data"   : self._pins.rx_data,
+                    "o_rgmii_eth_tx_ctl"    : self._pins.tx_ctrl,
+                    "o_rgmii_eth_tx_data"   : self._pins.tx_data,
+                })
+            else:
+                assert False
+
+        return Instance(f"{self.name}", **core_kwargs)
+
+
+class Builder:
+    file_templates = {
+        "build_{{top.name}}.sh": r"""
+            # {{autogenerated}}
+            set -e
+            {{emit_commands()}}
+        """,
+        "{{top.name}}_config.yml": r"""
+            # {{autogenerated}}
+            {
+                # PHY ----------------------------------------------------------------------
+                "phy":         {{top.config.phy_name}},
+                "vendor":      {{top.config.vendor}},
+
+                # Core ---------------------------------------------------------------------
+                "clk_freq":    {{top.config.clk_freq}},
+                "core":        wishbone,
+                "nrxslots":    {{top.config.rx_slots}},
+                "ntxslots":    {{top.config.tx_slots}},
+                "endianness":   {{top.config.endianess}},
+
+                "soc": {
+                    "mem_map": {
+                        "csr":    {{hex(top.config.ctrl_addr)}},
+                        "ethmac": {{hex(top.config.data_addr)}},
+                    },
+                },
+            }
+        """,
+    }
+    command_templates = [
+        # FIXME: add --name upstream
+        r"""
+            python -m liteeth.gen
+                --output-dir {{top.name}}
+                --gateware-dir {{top.name}}
+                --csr-csv {{top.name}}_csr.csv
+                {{top.name}}_config.yml
+        """,
+    ]
+
+    def __init__(self):
+        self.namespace = set()
+
+    def prepare(self, core, *, name_force=False):
+        if not isinstance(core, Core):
+            raise TypeError("LiteEth core must be an instance of liteeth.Core, not {!r}"
+                            .format(core))
+
+        if core.name in self.namespace and not name_force:
+            raise ValueError(
+                "LiteEth core name '{}' has already been used for a previous build. Building "
+                "this instance may overwrite previous build products. Passing `name_force=True` "
+                "will disable this check".format(core.name)
+            )
+        self.namespace.add(core.name)
+
+        autogenerated = f"Automatically generated by LambdaSoC {__version__}. Do not edit."
+
+        def emit_commands():
+            commands = []
+            for index, command_tpl in enumerate(self.command_templates):
+                command = render(command_tpl, origin="<command#{}>".format(index + 1))
+                command = re.sub(r"\s+", " ", command)
+                commands.append(command)
+            return "\n".join(commands)
+
+        def render(source, origin):
+            try:
+                source = textwrap.dedent(source).strip()
+                compiled = jinja2.Template(source, trim_blocks=True, lstrip_blocks=True)
+            except jinja2.TemplateSyntaxError as e:
+                e.args = ("{} (at {}:{})".format(e.message, origin, e.lineno),)
+                raise
+            return compiled.render({
+                "autogenerated": autogenerated,
+                "emit_commands": emit_commands,
+                "hex": hex,
+                "top": core,
+            })
+
+        plan = BuildPlan(script=f"build_{core.name}")
+        for filename_tpl, content_tpl in self.file_templates.items():
+            plan.add_file(render(filename_tpl, origin=filename_tpl),
+                          render(content_tpl,  origin=content_tpl))
+        return plan
diff --git a/lambdasoc/periph/eth.py b/lambdasoc/periph/eth.py
new file mode 100644 (file)
index 0000000..13bef12
--- /dev/null
@@ -0,0 +1,79 @@
+from ipaddress import IPv4Address
+
+from nmigen import *
+
+from nmigen_soc import wishbone
+from nmigen_soc.memory import MemoryMap
+from nmigen_soc.periph import ConstantMap
+
+from . import Peripheral
+from .event import IRQLine
+
+from ..cores import liteeth
+from ..soc.base import ConstantAddr
+
+
+__all__ = ["EthernetMACPeripheral"]
+
+
+class EthernetMACPeripheral(Peripheral, Elaboratable):
+    """Ethernet MAC peripheral.
+
+    Parameters
+    ----------
+    core : :class:`liteeth.Core`
+        LiteEth core.
+    local_ip : :class:`ipaddress.IPv4Address`
+        Local IP address. Defaults to 192.168.1.50.
+    remote_ip : :class:`ipaddress.IPv4Address`
+        Remote IP address. Defaults to 192.168.1.100.
+    """
+    def __init__(self, *, core, local_ip="192.168.1.50", remote_ip="192.168.1.100"):
+        super().__init__()
+
+        if not isinstance(core, liteeth.Core):
+            raise TypeError("LiteEth core must be an instance of lambdasoc.cores.liteeth.Core, "
+                            "not {!r}"
+                            .format(core))
+
+        self.core      = core
+        self.local_ip  = IPv4Address(local_ip)
+        self.remote_ip = IPv4Address(remote_ip)
+
+        bus_map = MemoryMap(
+            name       = self.name,
+            addr_width = core.bus.memory_map.addr_width,
+            data_width = core.bus.memory_map.data_width,
+        )
+        bus_map.add_window(core.bus.memory_map)
+
+        self.bus = wishbone.Interface(
+            addr_width  = core.bus.addr_width,
+            data_width  = core.bus.data_width,
+            granularity = core.bus.granularity,
+            features    = {"cti", "bte", "err"},
+        )
+        self.bus.memory_map = bus_map
+
+        self.irq = IRQLine(name=f"{self.name}_irq")
+
+    @property
+    def constant_map(self):
+        return ConstantMap(
+            CTRL_OFFSET = ConstantAddr(self.core.config.ctrl_addr),
+            DATA_OFFSET = ConstantAddr(self.core.config.data_addr),
+            RX_SLOTS    = self.core.config.rx_slots,
+            TX_SLOTS    = self.core.config.tx_slots,
+            SLOT_SIZE   = 2048,
+            LOCAL_IP    = int(self.local_ip),
+            REMOTE_IP   = int(self.remote_ip),
+        )
+
+    def elaborate(self, platform):
+        m = Module()
+        m.submodules.core = self.core
+        m.d.comb += [
+            self.bus.connect(self.core.bus),
+            self.irq.eq(self.core.irq),
+        ]
+        return m
index 24149f4a98987f63ed016ff15d9cf951b6124e50..9d338d49d5587f063b20be6a325ad3f5a95765b3 100644 (file)
@@ -4,7 +4,7 @@ from nmigen_soc.periph import ConstantMap, ConstantBool, ConstantInt
 
 from .base import *
 from ..cpu import CPU
-from ..cores import litedram
+from ..cores import litedram, liteeth
 from ..periph.intc import InterruptController
 from ..periph.sram import SRAMPeripheral
 from ..periph.serial import AsyncSerialPeripheral
@@ -142,6 +142,9 @@ class BIOSBuilder(ConfigBuilder):
             {% if soc.sdram is not none %}
             litedram_dir={{build_dir}}/{{litedram_pkg}}/{{soc.sdram.core.name}}
             {% endif %}
+            {% if soc.ethmac is not none %}
+            liteeth_dir={{build_dir}}/{{liteeth_pkg}}/{{soc.ethmac.core.name}}
+            {% endif %}
             build={{bios_dir}}
             KCONFIG_CONFIG={{bios_dir}}/{{name}}.config
             make -C {{software_dir}}/bios 1>&2
@@ -158,6 +161,7 @@ class BIOSBuilder(ConfigBuilder):
             "cpp_format": cpp_format,
             "bios_dir": os.path.abspath(f"{build_dir}/{__name__}"),
             "litedram_pkg": litedram.__name__,
+            "liteeth_pkg": liteeth.__name__,
         })
 
         return super().prepare(soc, build_dir, name, **render_params)
index ba0aa9698d60f2e6b6c398a01614352f3bd827fa..32d60226f6728a183a6677cbd2451c9f1a52ec74 160000 (submodule)
@@ -1 +1 @@
-Subproject commit ba0aa9698d60f2e6b6c398a01614352f3bd827fa
+Subproject commit 32d60226f6728a183a6677cbd2451c9f1a52ec74