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
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()
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.
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
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(
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
self._sdram = None
self._sram = None
+ self._ethmac = None
@property
def memory_map(self):
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,
),
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
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),
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
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):
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):
--- /dev/null
+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
--- /dev/null
+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