--- /dev/null
+from abc import ABCMeta, abstractmethod
+import csv
+import jinja2
+import os
+import re
+import textwrap
+
+from nmigen import *
+from nmigen import tracer
+from nmigen.build.run import BuildPlan
+from nmigen.utils import log2_int
+
+from nmigen_soc import wishbone
+from nmigen_soc.memory import MemoryMap
+
+from .. import __version__
+
+
+__all__ = [
+ "Config", "ECP5Config", "Artix7Config",
+ "NativePort", "Core",
+ "Builder",
+]
+
+
+class Config(metaclass=ABCMeta):
+ _doc_template = """
+ {description}
+
+ Parameters
+ ----------
+ memtype : str
+ DRAM type (e.g. `"DDR3"`).
+ module_name : str
+ DRAM module name.
+ module_bytes : int
+ Number of byte groups of the DRAM interface.
+ module_ranks : int
+ Number of ranks. A rank is a set of DRAM chips that are connected to the same CS pin.
+ input_clk_freq : int
+ Frequency of the input clock, which drives the internal PLL.
+ user_clk_freq : int
+ Frequency of the user clock, which is generated by the internal PLL.
+ input_domain : str
+ Input clock domain. Defaults to `"litedram_input"`.
+ user_domain : str
+ User clock domain. Defaults to `"litedram_user"`.
+ user_data_width : int
+ User port data width. Defaults to 128.
+ cmd_buffer_depth : int
+ Command buffer depth. Defaults to 16.
+ csr_data_width : int
+ CSR bus data width. Defaults to 32.
+ {parameters}
+ """
+
+ __doc__ = _doc_template.format(
+ description="""
+ LiteDRAM base configuration.
+ """.strip(),
+ parameters="",
+ )
+ def __init__(self, *,
+ memtype,
+ module_name,
+ module_bytes,
+ module_ranks,
+ input_clk_freq,
+ user_clk_freq,
+ input_domain = "litedram_input",
+ user_domain = "litedram_user",
+ user_data_width = 128,
+ cmd_buffer_depth = 16,
+ csr_data_width = 32):
+
+ if memtype == "DDR2":
+ rate = "1:2"
+ elif memtype in {"DDR3", "DDR4"}:
+ rate = "1:4"
+ else:
+ raise ValueError("Unsupported DRAM type, must be one of \"DDR2\", \"DDR3\" or "
+ "\"DDR4\", not {!r}"
+ .format(memtype))
+
+ if not isinstance(module_name, str):
+ raise ValueError("Module name must be a string, not {!r}"
+ .format(module_name))
+ if not isinstance(module_bytes, int) or module_bytes <= 0:
+ raise ValueError("Number of byte groups must be a positive integer, not {!r}"
+ .format(module_bytes))
+ if not isinstance(module_ranks, int) or module_ranks <= 0:
+ raise ValueError("Number of ranks must be a positive integer, not {!r}"
+ .format(module_ranks))
+ if not isinstance(input_clk_freq, int) or input_clk_freq <= 0:
+ raise ValueError("Input clock frequency must be a positive integer, not {!r}"
+ .format(input_clk_freq))
+ if not isinstance(user_clk_freq, int) or user_clk_freq <= 0:
+ raise ValueError("User clock frequency must be a positive integer, not {!r}"
+ .format(user_clk_freq))
+ if not isinstance(input_domain, str):
+ raise ValueError("Input domain name must be a string, not {!r}"
+ .format(input_domain))
+ if not isinstance(user_domain, str):
+ raise ValueError("User domain name must be a string, not {!r}"
+ .format(user_domain))
+ if user_data_width not in {8, 16, 32, 64, 128}:
+ raise ValueError("User port data width must be one of 8, 16, 32, 64 or 128, "
+ "not {!r}"
+ .format(user_data_width))
+ if not isinstance(cmd_buffer_depth, int) or cmd_buffer_depth <= 0:
+ raise ValueError("Command buffer depth must be a positive integer, not {!r}"
+ .format(cmd_buffer_depth))
+ if csr_data_width not in {8, 16, 32, 64}:
+ raise ValueError("CSR data width must be one of 8, 16, 32, or 64, not {!r}"
+ .format(csr_data_width))
+
+ self.memtype = memtype
+ self._rate = rate
+ self.module_name = module_name
+ self.module_bytes = module_bytes
+ self.module_ranks = module_ranks
+ self.input_clk_freq = input_clk_freq
+ self.user_clk_freq = user_clk_freq
+ self.input_domain = input_domain
+ self.user_domain = user_domain
+ self.user_data_width = user_data_width
+ self.cmd_buffer_depth = cmd_buffer_depth
+ self.csr_data_width = csr_data_width
+
+ @property
+ @abstractmethod
+ def phy_name(self):
+ """LiteDRAM PHY name.
+ """
+ raise NotImplementedError
+
+ def get_module(self):
+ """Get DRAM module description.
+
+ Return value
+ ------------
+ An instance of :class:`litedram.modules.SDRAMModule`, describing its geometry and timings.
+ """
+ import litedram.modules
+ module_class = getattr(litedram.modules, self.module_name)
+ module = module_class(
+ clk_freq = self.user_clk_freq,
+ rate = self._rate,
+ )
+ assert module.memtype == self.memtype
+ return module
+
+ def request_pins(self, platform, name, number):
+ """Request DRAM pins.
+
+ This helper requests the DRAM pins with `dir="-"` and `xdr=0`, because LiteDRAM already
+ provides its own I/O buffers.
+
+ Arguments
+ ---------
+ platform : :class:`nmigen.build.Platform`
+ Target platform.
+ name : str
+ DRAM resource name.
+ number : int
+ DRAM resource number.
+
+ Return value
+ ------------
+ A :class:`Record` providing raw access to DRAM pins.
+ """
+ res = platform.lookup(name, number)
+ return platform.request(
+ name, number,
+ dir={io.name: "-" for io in res.ios},
+ xdr={io.name: 0 for io in res.ios},
+ )
+
+
+class ECP5Config(Config):
+ phy_name = "ECP5DDRPHY"
+
+ __doc__ = Config._doc_template.format(
+ description = """
+ LiteDRAM configuration for ECP5 FPGAs.
+ """.strip(),
+ parameters = r"""
+ init_clk_freq : int
+ Frequency of the PHY initialization clock, which is generated by the internal PLL.
+ """.strip(),
+ )
+ def __init__(self, *, init_clk_freq, **kwargs):
+ super().__init__(**kwargs)
+
+ if not isinstance(init_clk_freq, int) or init_clk_freq <= 0:
+ raise ValueError("Init clock frequency must be a positive integer, not {!r}"
+ .format(init_clk_freq))
+ self.init_clk_freq = init_clk_freq
+
+
+class Artix7Config(Config):
+ phy_name = "A7DDRPHY"
+
+ __doc__ = Config._doc_template.format(
+ description = """
+ LiteDRAM configuration for Artix 7 FPGAs.
+ """.strip(),
+ parameters = r"""
+ speedgrade : str
+ FPGA speed grade (e.g. "-1").
+ cmd_latency : int
+ Command additional latency.
+ rtt_nom : int
+ Nominal termination impedance.
+ rtt_wr : int
+ Write termination impedance.
+ ron : int
+ Output driver impedance.
+ iodelay_clk_freq : int
+ IODELAY reference clock frequency.
+ """.strip(),
+ )
+ def __init__(self, *,
+ speedgrade,
+ cmd_latency,
+ rtt_nom,
+ rtt_wr,
+ ron,
+ iodelay_clk_freq,
+ **kwargs):
+ super().__init__(**kwargs)
+
+ speedgrades = ("-1", "-2", "-2L", "-2G", "-3")
+ if speedgrade not in speedgrades:
+ raise ValueError("Speed grade must be one of \'{}\', not {!r}"
+ .format("\', \'".join(speedgrades), speedgrade))
+ if not isinstance(cmd_latency, int) or cmd_latency < 0:
+ raise ValueError("Command latency must be a non-negative integer, not {!r}"
+ .format(cmd_latency))
+ if not isinstance(rtt_nom, int) or rtt_nom < 0:
+ raise ValueError("Nominal termination impedance must be a non-negative integer, "
+ "not {!r}"
+ .format(rtt_nom))
+ if not isinstance(rtt_wr, int) or rtt_wr < 0:
+ raise ValueError("Write termination impedance must be a non-negative integer, "
+ "not {!r}"
+ .format(rtt_wr))
+ if not isinstance(ron, int) or ron < 0:
+ raise ValueError("Output driver impedance must be a non-negative integer, "
+ "not {!r}"
+ .format(ron))
+ if not isinstance(iodelay_clk_freq, int) or iodelay_clk_freq <= 0:
+ raise ValueError("IODELAY clock frequency must be a positive integer, not {!r}"
+ .format(iodelay_clk_freq))
+
+ self.speedgrade = speedgrade
+ self.cmd_latency = cmd_latency
+ self.rtt_nom = rtt_nom
+ self.rtt_wr = rtt_wr
+ self.ron = ron
+ self.iodelay_clk_freq = iodelay_clk_freq
+
+
+class NativePort(Record):
+ """LiteDRAM native port interface.
+
+ In the "Attributes" section, port directions are given from the point of view of user logic.
+
+ Parameters
+ ----------
+ addr_width : int
+ Port address width.
+ data_width : int
+ Port data width.
+
+ Attributes
+ ----------
+ granularity : int
+ Port granularity, i.e. its smallest transferable unit of data. LiteDRAM native ports have a
+ granularity of 8 bits.
+ cmd.valid : Signal(), in
+ Command valid.
+ cmd.ready : Signal(), out
+ Command ready. Commands are accepted when `cmd.valid` and `cmd.ready` are both asserted.
+ cmd.last : Signal(), in
+ Command last. Indicates the last command of a burst.
+ cmd.we : Signal(), in
+ Command write enable. Indicates that this command is a write.
+ cmd.addr : Signal(addr_width), in
+ Command address.
+ w.valid : Signal(), in
+ Write valid.
+ w.ready : Signal(), out
+ Write ready. Write data is accepted when `w.valid` and `w.ready` are both asserted.
+ w.data : Signal(data_width), in
+ Write data.
+ w.we : Signal(data_width // granularity), bitmask, in
+ Write mask. Indicates which bytes in `w.data` are valid.
+ r.valid : Signal(), out
+ Read valid.
+ r.ready : Signal(), in
+ Read ready. Read data is consumed when `r.valid` and `r.ready` are both asserted.
+ r.data : Signal(data_width), out
+ Read data.
+ """
+ def __init__(self, *, addr_width, data_width, name=None, src_loc_at=0):
+ if not isinstance(addr_width, int) or addr_width <= 0:
+ raise ValueError("Address width must be a positive integer, not {!r}"
+ .format(addr_width))
+ if not isinstance(data_width, int) or data_width <= 0 or data_width & data_width - 1:
+ raise ValueError("Data width must be a positive power of two integer, not {!r}"
+ .format(data_width))
+
+ self.addr_width = addr_width
+ self.data_width = data_width
+ self.granularity = 8
+ self._map = None
+
+ super().__init__([
+ ("cmd", [
+ ("valid", 1),
+ ("ready", 1),
+ ("last", 1),
+ ("we", 1),
+ ("addr", addr_width),
+ ]),
+ ("w", [
+ ("valid", 1),
+ ("ready", 1),
+ ("data", data_width),
+ ("we", data_width // self.granularity),
+ ]),
+ ("r", [
+ ("valid", 1),
+ ("ready", 1),
+ ("data", data_width),
+ ]),
+ ], name=name, src_loc_at=1 + src_loc_at)
+
+ @property
+ def memory_map(self):
+ """Map of the native port.
+
+ Return value
+ ------------
+ An instance of :class:`nmigen_soc.memory.MemoryMap`.
+
+ Exceptions
+ ----------
+ Raises an :exn:`AttributeError` if the port does not have a memory map.
+ """
+ if self._map is None:
+ raise AttributeError("Native port {!r} does not have a memory map"
+ .format(self))
+ return self._map
+
+ @memory_map.setter
+ def memory_map(self, memory_map):
+ if not isinstance(memory_map, MemoryMap):
+ raise TypeError("Memory map must be an instance of MemoryMap, not {!r}"
+ .format(memory_map))
+ if memory_map.data_width != 8:
+ raise ValueError("Memory map has data width {}, which is not the same as native port "
+ "granularity {}"
+ .format(memory_map.data_width, 8))
+ granularity_bits = log2_int(self.data_width // 8)
+ if memory_map.addr_width != max(1, self.addr_width + granularity_bits):
+ raise ValueError("Memory map has address width {}, which is not the same as native "
+ "port address width {} ({} address bits + {} granularity bits)"
+ .format(memory_map.addr_width, self.addr_width + granularity_bits,
+ self.addr_width, granularity_bits))
+ memory_map.freeze()
+ self._map = memory_map
+
+
+class Core(Elaboratable):
+ _namespace = set()
+
+ @classmethod
+ def clear_namespace(cls):
+ """Clear private namespace.
+
+ Every time an instance of :class:`litedram.Core` is created, its name is stored in a
+ private namespace. This allows us to detect name collisions, which are problematic for at
+ least two reasons:
+ * by default, a sub-directory named after the instance is created at build-time in
+ order to isolate it from other LiteDRAM builds. A collision would overwrite previous
+ build products.
+ * the instance name becomes the name of its top-level Verilog module. Importing two
+ modules with the same name will cause a toolchain error.
+
+ :meth:`litedram.Core.clear_namespace` resets this namespace. It is intended for cases where
+ stateless class instantiations are desirable, such as unit testing.
+ """
+ cls._namespace.clear()
+
+ """An nMigen wrapper for a standalone LiteDRAM core.
+
+ Parameters
+ ----------
+ config : :class:`Config`
+ LiteDRAM configuration.
+ pins : :class:`nmigen.lib.io.Pin`
+ Optional. DRAM pins. See :class:`nmigen_boards.resources.DDR3Resource` for layout.
+ name : str
+ Optional. Name of the LiteDRAM core. If ``None`` (default) the name is inferred from the
+ name of the variable this instance is assigned to.
+ name_force: bool
+ Force name. If ``True``, no exception will be raised in case of a name collision with a
+ previous LiteDRAM instance. Defaults to ``False``.
+
+ Attributes
+ ----------
+ name : str
+ Name of the LiteDRAM core.
+ size : int
+ DRAM size, in bytes.
+ user_port : :class:`NativePort`
+ User port. Provides access to the DRAM storage.
+
+ Exceptions
+ ----------
+ Raises a :exn:`ValueError` if ``name`` collides with the name given to a previous LiteDRAM
+ instance and ``name_force`` is ``False``.
+ """
+ def __init__(self, config, *, pins=None, name=None, name_force=False, src_loc_at=0):
+ if not isinstance(config, Config):
+ raise TypeError("Config must be an instance of litedram.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))
+ name = name or tracer.get_var_name(depth=2 + src_loc_at)
+
+ if not name_force and name in Core._namespace:
+ raise ValueError(
+ "Name '{}' has already been used for a previous litedram.Core instance. Building "
+ "this instance may overwrite previous build products. Passing `name_force=True` "
+ "will disable this check.".format(name)
+ )
+ Core._namespace.add(name)
+ self.name = name
+
+ module = config.get_module()
+ size = config.module_bytes \
+ * 2**( module.geom_settings.bankbits
+ + module.geom_settings.rowbits
+ + module.geom_settings.colbits)
+
+ self.size = size
+
+ user_addr_width = module.geom_settings.rowbits \
+ + module.geom_settings.colbits \
+ + log2_int(module.nbanks) \
+ + max(log2_int(config.module_ranks), 1)
+
+ self.user_port = NativePort(
+ addr_width = user_addr_width - log2_int(config.user_data_width // 8),
+ data_width = config.user_data_width,
+ )
+ user_map = MemoryMap(addr_width=user_addr_width, data_width=8)
+ user_map.add_resource("user_port_0", size=size)
+ self.user_port.memory_map = user_map
+
+ self._ctrl_bus = None
+ self._pins = pins
+
+ @property
+ def ctrl_bus(self):
+ """Control bus interface.
+
+ *Please note that accesses to the CSRs exposed by this interface are not atomic.*
+
+ The memory map of this interface is populated by reading the ``{{self.name}}_csr.csv``
+ file from the build products.
+
+ Return value
+ ------------
+ An instance of :class:`nmigen_soc.wishbone.Interface`.
+
+ Exceptions
+ ----------
+ Raises an :exn:`AttributeError` if this getter is called before LiteDRAM is built (i.e.
+ before :meth:`Core.build` is called with `do_build=True`).
+ """
+ if self._ctrl_bus is None:
+ raise AttributeError("Core.build(do_build=True) must be called before accessing "
+ "Core.ctrl_bus")
+ return self._ctrl_bus
+
+ def build(self, do_build=True, build_dir="build/litedram", sim=False):
+ """Build the LiteDRAM core.
+
+ Arguments
+ ---------
+ do_build : bool
+ Build the LiteDRAM core. Defaults to `True`.
+ build_dir : str
+ Build directory.
+ sim : bool
+ Do the build in simulation mode (i.e. with a PHY model). Defaults to `False`.
+
+ Return value
+ ------------
+ An instance of :class:`nmigen.build.run.LocalBuildProducts` if ``do_build`` is ``True``.
+ Otherwise, an instance of :class:``nmigen.build.run.BuildPlan``.
+ """
+ plan = Builder().prepare(self, build_dir, sim)
+ if not do_build:
+ return plan
+
+ products = plan.execute_local(build_dir)
+
+ # LiteDRAM's Wishbone to CSR bridge uses an 8-bit granularity.
+ ctrl_map = MemoryMap(addr_width=1, data_width=8)
+
+ with products.extract(f"{self.name}_csr.csv") as csr_csv_filename:
+ with open(csr_csv_filename, "r") as csr_csv:
+ for row in csv.reader(csr_csv, delimiter=","):
+ if row[0][0] == "#": continue
+ res_type, res_name, addr, size, attrs = row
+ if res_type == "csr_register":
+ ctrl_map.add_resource(
+ res_name,
+ addr = int(addr, 16),
+ size = int(size, 10) * self.config.csr_data_width
+ // ctrl_map.data_width,
+ extend = True,
+ )
+
+ self._ctrl_bus = wishbone.Interface(
+ addr_width = ctrl_map.addr_width
+ - log2_int(self.config.csr_data_width // ctrl_map.data_width),
+ data_width = self.config.csr_data_width,
+ granularity = ctrl_map.data_width,
+ )
+ self._ctrl_bus.memory_map = ctrl_map
+
+ return products
+
+ def elaborate(self, platform):
+ core_kwargs = {
+ "i_clk" : ClockSignal(self.config.input_domain),
+ "i_rst" : ResetSignal(self.config.input_domain),
+ "o_user_clk" : ClockSignal(self.config.user_domain),
+ "o_user_rst" : ResetSignal(self.config.user_domain),
+
+ "i_wb_ctrl_adr" : self.ctrl_bus.adr,
+ "i_wb_ctrl_dat_w" : self.ctrl_bus.dat_w,
+ "o_wb_ctrl_dat_r" : self.ctrl_bus.dat_r,
+ "i_wb_ctrl_sel" : self.ctrl_bus.sel,
+ "i_wb_ctrl_cyc" : self.ctrl_bus.cyc,
+ "i_wb_ctrl_stb" : self.ctrl_bus.stb,
+ "o_wb_ctrl_ack" : self.ctrl_bus.ack,
+ "i_wb_ctrl_we" : self.ctrl_bus.we,
+
+ "i_user_port_0_cmd_valid" : self.user_port.cmd.valid,
+ "o_user_port_0_cmd_ready" : self.user_port.cmd.ready,
+ "i_user_port_0_cmd_we" : self.user_port.cmd.we,
+ "i_user_port_0_cmd_addr" : self.user_port.cmd.addr,
+ "i_user_port_0_wdata_valid" : self.user_port.w.valid,
+ "o_user_port_0_wdata_ready" : self.user_port.w.ready,
+ "i_user_port_0_wdata_we" : self.user_port.w.we,
+ "i_user_port_0_wdata_data" : self.user_port.w.data,
+ "o_user_port_0_rdata_valid" : self.user_port.r.valid,
+ "i_user_port_0_rdata_ready" : self.user_port.r.ready,
+ "o_user_port_0_rdata_data" : self.user_port.r.data,
+ }
+
+ if self._pins is not None:
+ core_kwargs.update({
+ "o_ddram_a" : self._pins.a,
+ "o_ddram_ba" : self._pins.ba,
+ "o_ddram_ras_n" : self._pins.ras,
+ "o_ddram_cas_n" : self._pins.cas,
+ "o_ddram_we_n" : self._pins.we,
+ "o_ddram_dm" : self._pins.dm,
+ "o_ddram_clk_p" : self._pins.clk.p,
+ "o_ddram_cke" : self._pins.clk_en,
+ "o_ddram_odt" : self._pins.odt,
+ })
+
+ if hasattr(self._pins, "cs"):
+ core_kwargs.update({
+ "o_ddram_cs_n" : self._pins.cs,
+ })
+
+ if hasattr(self._pins, "rst"):
+ core_kwargs.update({
+ "o_ddram_reset_n" : self._pins.rst,
+ })
+
+ if isinstance(self.config, ECP5Config):
+ core_kwargs.update({
+ "i_ddram_dq" : self._pins.dq,
+ "i_ddram_dqs_p" : self._pins.dqs.p,
+ })
+ elif isinstance(self.config, Artix7Config):
+ core_kwargs.update({
+ "io_ddram_dq" : self._pins.dq,
+ "io_ddram_dqs_p" : self._pins.dqs.p,
+ "io_ddram_dqs_n" : self._pins.dqs.n,
+ "o_ddram_clk_n" : self._pins.clk.n,
+ })
+ else:
+ assert False
+
+ return Instance(f"{self.name}", **core_kwargs)
+
+
+class Builder:
+ """
+ LiteDRAM builder
+ ----------------
+
+ Build products (any):
+ * ``{{top.name}}_csr.csv`` : CSR listing.
+ * ``{{top.name}}/build_{{top.name}}.sh``: LiteDRAM build script.
+ * ``{{top.name}}/{{top.name}}.v`` : LiteDRAM core.
+ * ``{{top.name}}/software/include/generated/csr.h`` : CSR accessors.
+ * ``{{top.name}}/software/include/generated/git.h`` : Git version.
+ * ``{{top.name}}/software/include/generated/mem.h`` : Memory regions.
+ * ``{{top.name}}/software/include/generated/sdram_phy.h`` : SDRAM initialization sequence.
+ * ``{{top.name}}/software/include/generated/soc.h`` : SoC constants.
+
+ Build products (ECP5):
+ * ``{{top.name}}/{{top.name}}.lpf`` : Constraints file.
+ * ``{{top.name}}/{{top.name}}.ys`` : Yosys script.
+
+ Build products (Artix 7):
+ * ``{{top.name}}/{{top.name}}.xdc`` : Constraints file
+ * ``{{top.name}}/{{top.name}}.tcl`` : Vivado script.
+ """
+
+ file_templates = {
+ "build_{{top.name}}.sh": r"""
+ # {{autogenerated}}
+ set -e
+ {{emit_commands()}}
+ """,
+ "{{top.name}}_config.yml": r"""
+ # {{autogenerated}}
+ {
+ # General ------------------------------------------------------------------
+ "cpu": "None",
+ {% if top.config.phy_name == "A7DDRPHY" %}
+ "speedgrade": {{top.config.speedgrade}},
+ {% endif %}
+ "memtype": "{{top.config.memtype}}",
+
+ # PHY ----------------------------------------------------------------------
+ {% if top.config.phy_name == "A7DDRPHY" %}
+ "cmd_latency": {{top.config.cmd_latency}},
+ {% endif %}
+ "sdram_module": "{{top.config.module_name}}",
+ "sdram_module_nb": {{top.config.module_bytes}},
+ "sdram_rank_nb": {{top.config.module_ranks}},
+ "sdram_phy": "{{top.config.phy_name}}",
+
+ # Electrical ---------------------------------------------------------------
+ {% if top.config.phy_name == "A7DDRPHY" %}
+ "rtt_nom": "{{top.config.rtt_nom}}ohm",
+ "rtt_wr": "{{top.config.rtt_wr}}ohm",
+ "ron": "{{top.config.ron}}ohm",
+ {% endif %}
+
+ # Frequency ----------------------------------------------------------------
+ "input_clk_freq": {{top.config.input_clk_freq}},
+ "sys_clk_freq": {{top.config.user_clk_freq}},
+ {% if top.config.phy_name == "ECP5DDRPHY" %}
+ "init_clk_freq": {{top.config.init_clk_freq}},
+ {% elif top.config.phy_name == "A7DDRPHY" %}
+ "iodelay_clk_freq": {{top.config.iodelay_clk_freq}},
+ {% endif %}
+
+ # Core ---------------------------------------------------------------------
+ "cmd_buffer_depth": {{top.config.cmd_buffer_depth}},
+ "csr_data_width": {{top.config.csr_data_width}},
+
+ # User Ports ---------------------------------------------------------------
+ "user_ports": {
+ "0": {
+ "type": "native",
+ "data_width": {{top.config.user_data_width}},
+ },
+ },
+ }
+ """,
+ }
+ command_templates = [
+ r"""
+ python -m litedram.gen
+ --name {{top.name}}
+ --output-dir {{top.name}}
+ --gateware-dir {{top.name}}
+ --csr-csv {{top.name}}_csr.csv
+ {% if sim %}
+ --sim
+ {% endif %}
+ {{top.name}}_config.yml
+ """,
+ ]
+
+ def prepare(self, top, build_dir, sim):
+ if not isinstance(top, Core):
+ raise TypeError("Top module must be an instance of litedram.Core, not {!r}"
+ .format(top))
+
+ 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,
+ "build_dir": os.path.abspath(build_dir),
+ "emit_commands": emit_commands,
+ "sim": sim,
+ "top": top,
+ })
+
+ plan = BuildPlan(script=f"build_{top.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
+# nmigen: UnusedElaboratable=no
+
+import unittest
+
+from nmigen_soc.memory import MemoryMap
+
+from litedram.modules import SDRAMModule
+
+from ..cores import litedram
+
+
+class DummyConfig(litedram.Config):
+ phy_name = "dummy"
+
+
+class ConfigTestCase(unittest.TestCase):
+ def test_simple(self):
+ cfg = DummyConfig(
+ memtype = "DDR3",
+ module_name = "MT41K256M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(70e6),
+ input_domain = "input",
+ user_domain = "user",
+ user_data_width = 32,
+ cmd_buffer_depth = 8,
+ csr_data_width = 32,
+ )
+ self.assertEqual(cfg.memtype, "DDR3")
+ self.assertEqual(cfg.module_name, "MT41K256M16")
+ self.assertEqual(cfg.module_bytes, 2)
+ self.assertEqual(cfg.module_ranks, 1)
+ self.assertEqual(cfg.phy_name, "dummy")
+ self.assertEqual(cfg.input_clk_freq, int(100e6))
+ self.assertEqual(cfg.user_clk_freq, int(70e6))
+ self.assertEqual(cfg.input_domain, "input")
+ self.assertEqual(cfg.user_domain, "user")
+ self.assertEqual(cfg.user_data_width, 32)
+ self.assertEqual(cfg.cmd_buffer_depth, 8)
+ self.assertEqual(cfg.csr_data_width, 32)
+
+ def test_get_module(self):
+ cfg = DummyConfig(
+ memtype = "DDR3",
+ module_name = "MT41K256M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(70e6),
+ )
+ module = cfg.get_module()
+ self.assertIsInstance(module, SDRAMModule)
+
+ def test_wrong_memtype(self):
+ with self.assertRaisesRegex(ValueError,
+ r"Unsupported DRAM type, must be one of \"DDR2\", \"DDR3\" or \"DDR4\", "
+ r"not 'foo'"):
+ cfg = DummyConfig(
+ memtype = "foo",
+ module_name = "MT41K256M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(70e6),
+ )
+
+ def test_wrong_module_name(self):
+ with self.assertRaisesRegex(ValueError,
+ r"Module name must be a string, not 42"):
+ cfg = DummyConfig(
+ memtype = "DDR3",
+ module_name = 42,
+ module_bytes = 2,
+ module_ranks = 1,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(70e6),
+ )
+
+ def test_wrong_module_bytes(self):
+ with self.assertRaisesRegex(ValueError,
+ r"Number of byte groups must be a positive integer, not 'foo'"):
+ cfg = DummyConfig(
+ memtype = "DDR3",
+ module_name = "MT41K256M16",
+ module_bytes = "foo",
+ module_ranks = 1,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(70e6),
+ )
+
+ def test_wrong_module_ranks(self):
+ with self.assertRaisesRegex(ValueError,
+ r"Number of ranks must be a positive integer, not 'foo'"):
+ cfg = DummyConfig(
+ memtype = "DDR3",
+ module_name = "MT41K256M16",
+ module_bytes = 2,
+ module_ranks = "foo",
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(70e6),
+ )
+
+ def test_wrong_input_clk_freq(self):
+ with self.assertRaisesRegex(ValueError,
+ r"Input clock frequency must be a positive integer, not -1"):
+ cfg = DummyConfig(
+ memtype = "DDR3",
+ module_name = "MT41K256M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ input_clk_freq = -1,
+ user_clk_freq = int(70e6),
+ )
+
+ def test_wrong_user_clk_freq(self):
+ with self.assertRaisesRegex(ValueError,
+ r"User clock frequency must be a positive integer, not -1"):
+ cfg = DummyConfig(
+ memtype = "DDR3",
+ module_name = "MT41K256M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ input_clk_freq = int(100e6),
+ user_clk_freq = -1,
+ )
+
+ def test_wrong_input_domain(self):
+ with self.assertRaisesRegex(ValueError,
+ r"Input domain name must be a string, not 42"):
+ cfg = DummyConfig(
+ memtype = "DDR3",
+ module_name = "MT41K256M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(70e6),
+ input_domain = 42,
+ )
+
+ def test_wrong_user_domain(self):
+ with self.assertRaisesRegex(ValueError,
+ r"User domain name must be a string, not 42"):
+ cfg = DummyConfig(
+ memtype = "DDR3",
+ module_name = "MT41K256M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(70e6),
+ user_domain = 42,
+ )
+
+ def test_wrong_user_data_width(self):
+ with self.assertRaisesRegex(ValueError,
+ r"User port data width must be one of 8, 16, 32, 64 or 128, not 42"):
+ cfg = DummyConfig(
+ memtype = "DDR3",
+ module_name = "MT41K256M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(70e6),
+ user_data_width = 42,
+ )
+
+ def test_wrong_cmd_buffer_depth(self):
+ with self.assertRaisesRegex(ValueError,
+ r"Command buffer depth must be a positive integer, not 'foo'"):
+ cfg = DummyConfig(
+ memtype = "DDR3",
+ module_name = "MT41K256M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(70e6),
+ cmd_buffer_depth = "foo",
+ )
+
+ def test_wrong_csr_data_width(self):
+ with self.assertRaisesRegex(ValueError,
+ r"CSR data width must be one of 8, 16, 32, or 64, not 42"):
+ cfg = DummyConfig(
+ memtype = "DDR3",
+ module_name = "MT41K256M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(70e6),
+ csr_data_width = 42,
+ )
+
+
+class ECP5ConfigTestCase(unittest.TestCase):
+ def test_simple(self):
+ cfg = litedram.ECP5Config(
+ memtype = "DDR3",
+ module_name = "MT41K256M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(70e6),
+ init_clk_freq = int(25e6),
+ )
+ self.assertEqual(cfg.init_clk_freq, int(25e6))
+ self.assertEqual(cfg.phy_name, "ECP5DDRPHY")
+
+ def test_wrong_init_clk_freq(self):
+ with self.assertRaisesRegex(ValueError,
+ r"Init clock frequency must be a positive integer, not -1"):
+ cfg = litedram.ECP5Config(
+ memtype = "DDR3",
+ module_name = "MT41K256M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(70e6),
+ init_clk_freq = -1,
+ )
+
+
+class Artix7ConfigTestCase(unittest.TestCase):
+ def test_simple(self):
+ cfg = litedram.Artix7Config(
+ memtype = "DDR3",
+ speedgrade = "-1",
+ cmd_latency = 0,
+ module_name = "MT41K128M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ rtt_nom = 60,
+ rtt_wr = 60,
+ ron = 34,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(100e6),
+ iodelay_clk_freq = int(200e6),
+ )
+ self.assertEqual(cfg.speedgrade, "-1")
+ self.assertEqual(cfg.cmd_latency, 0)
+ self.assertEqual(cfg.rtt_nom, 60)
+ self.assertEqual(cfg.rtt_wr, 60)
+ self.assertEqual(cfg.ron, 34)
+ self.assertEqual(cfg.iodelay_clk_freq, int(200e6))
+ self.assertEqual(cfg.phy_name, "A7DDRPHY")
+
+ def test_wrong_speedgrade(self):
+ with self.assertRaisesRegex(ValueError,
+ r"Speed grade must be one of '-1', '-2', '-2L', '-2G', '-3', "
+ r"not '-42'"):
+ cfg = litedram.Artix7Config(
+ memtype = "DDR3",
+ speedgrade = "-42",
+ cmd_latency = 0,
+ module_name = "MT41K128M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ rtt_nom = 60,
+ rtt_wr = 60,
+ ron = 34,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(100e6),
+ iodelay_clk_freq = int(200e6),
+ )
+
+ def test_wrong_cmd_latency(self):
+ with self.assertRaisesRegex(ValueError,
+ r"Command latency must be a non-negative integer, not -42"):
+ cfg = litedram.Artix7Config(
+ memtype = "DDR3",
+ speedgrade = "-1",
+ cmd_latency = -42,
+ module_name = "MT41K128M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ rtt_nom = 60,
+ rtt_wr = 60,
+ ron = 34,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(100e6),
+ iodelay_clk_freq = int(200e6),
+ )
+
+ def test_wrong_rtt_nom(self):
+ with self.assertRaisesRegex(ValueError,
+ r"Nominal termination impedance must be a non-negative integer, not -42"):
+ cfg = litedram.Artix7Config(
+ memtype = "DDR3",
+ speedgrade = "-1",
+ cmd_latency = 0,
+ module_name = "MT41K128M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ rtt_nom = -42,
+ rtt_wr = 60,
+ ron = 34,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(100e6),
+ iodelay_clk_freq = int(200e6),
+ )
+
+ def test_wrong_rtt_wr(self):
+ with self.assertRaisesRegex(ValueError,
+ r"Write termination impedance must be a non-negative integer, not -42"):
+ cfg = litedram.Artix7Config(
+ memtype = "DDR3",
+ speedgrade = "-1",
+ cmd_latency = 0,
+ module_name = "MT41K128M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ rtt_nom = 60,
+ rtt_wr = -42,
+ ron = 34,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(100e6),
+ iodelay_clk_freq = int(200e6),
+ )
+
+ def test_wrong_ron(self):
+ with self.assertRaisesRegex(ValueError,
+ r"Output driver impedance must be a non-negative integer, not -42"):
+ cfg = litedram.Artix7Config(
+ memtype = "DDR3",
+ speedgrade = "-1",
+ cmd_latency = 0,
+ module_name = "MT41K128M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ rtt_nom = 60,
+ rtt_wr = 60,
+ ron = -42,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(100e6),
+ iodelay_clk_freq = int(200e6),
+ )
+
+ def test_wrong_iodelay_clk_freq(self):
+ with self.assertRaisesRegex(ValueError,
+ r"IODELAY clock frequency must be a positive integer, not -1"):
+ cfg = litedram.Artix7Config(
+ memtype = "DDR3",
+ speedgrade = "-1",
+ cmd_latency = 0,
+ module_name = "MT41K128M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ rtt_nom = 60,
+ rtt_wr = 60,
+ ron = 34,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(100e6),
+ iodelay_clk_freq = -1,
+ )
+
+
+class NativePortTestCase(unittest.TestCase):
+ def test_simple(self):
+ port = litedram.NativePort(addr_width=10, data_width=32)
+ self.assertEqual(port.addr_width, 10)
+ self.assertEqual(port.data_width, 32)
+ self.assertEqual(port.granularity, 8)
+ self.assertEqual(len(port.cmd.addr), 10)
+ self.assertEqual(len(port.w.data), 32)
+ self.assertEqual(len(port.w.we), 4)
+ self.assertEqual(len(port.r.data), 32)
+ self.assertEqual(
+ repr(port),
+ "(rec port "
+ "(rec port__cmd valid ready last we addr) "
+ "(rec port__w valid ready data we) "
+ "(rec port__r valid ready data))"
+ )
+
+ def test_memory_map(self):
+ port = litedram.NativePort(addr_width=10, data_width=32)
+ port_map = MemoryMap(addr_width=12, data_width=8)
+ port.memory_map = port_map
+ self.assertIs(port.memory_map, port_map)
+
+ def test_wrong_memory_map(self):
+ port = litedram.NativePort(addr_width=10, data_width=32)
+ with self.assertRaisesRegex(TypeError,
+ r"Memory map must be an instance of MemoryMap, not 'foo'"):
+ port.memory_map = "foo"
+
+ def test_wrong_memory_map_data_width(self):
+ port = litedram.NativePort(addr_width=10, data_width=32)
+ port_map = MemoryMap(addr_width=11, data_width=16)
+ with self.assertRaisesRegex(ValueError,
+ r"Memory map has data width 16, which is not the same as native port granularity "
+ r"8"):
+ port.memory_map = port_map
+
+ def test_wrong_memory_map_addr_width(self):
+ port = litedram.NativePort(addr_width=10, data_width=32)
+ port_map = MemoryMap(addr_width=11, data_width=8)
+ with self.assertRaisesRegex(ValueError,
+ r"Memory map has address width 11, which is not the same as native port address "
+ r"width 12 \(10 address bits \+ 2 granularity bits\)"):
+ port.memory_map = port_map
+
+
+class CoreTestCase(unittest.TestCase):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._cfg = litedram.ECP5Config(
+ memtype = "DDR3",
+ module_name = "MT41K256M16",
+ module_bytes = 2,
+ module_ranks = 1,
+ input_clk_freq = int(100e6),
+ user_clk_freq = int(70e6),
+ init_clk_freq = int(25e6),
+ )
+
+ def setUp(self):
+ litedram.Core.clear_namespace()
+
+ def tearDown(self):
+ litedram.Core.clear_namespace()
+
+ def test_simple(self):
+ core = litedram.Core(self._cfg)
+ self.assertIs(core.config, self._cfg)
+ self.assertEqual(core.name, "core")
+ self.assertEqual(core.size, 512 * 1024 * 1024)
+ self.assertEqual(core.user_port.addr_width, 25)
+ self.assertEqual(core.user_port.data_width, 128)
+ self.assertEqual(core.user_port.memory_map.addr_width, 29)
+ self.assertEqual(core.user_port.memory_map.data_width, 8)
+
+ def test_name_force(self):
+ core_1 = litedram.Core(self._cfg, name="core")
+ core_2 = litedram.Core(self._cfg, name="core", name_force=True)
+ self.assertEqual(core_1.name, "core")
+ self.assertEqual(core_2.name, "core")
+
+ def test_ctrl_bus_not_ready(self):
+ core = litedram.Core(self._cfg)
+ with self.assertRaisesRegex(AttributeError,
+ r"Core.build\(do_build=True\) must be called before accessing Core\.ctrl_bus"):
+ core.ctrl_bus
+
+ def test_wrong_config(self):
+ with self.assertRaisesRegex(TypeError,
+ r"Config must be an instance of litedram\.Config, not 'foo'"):
+ core = litedram.Core("foo")
+
+ def test_wrong_name(self):
+ with self.assertRaisesRegex(TypeError,
+ r"Name must be a string, not 42"):
+ core = litedram.Core(self._cfg, name=42)
+
+ def test_wrong_name_collision(self):
+ core_1 = litedram.Core(self._cfg, name="core")
+ with self.assertRaisesRegex(ValueError,
+ r"Name 'core' has already been used for a previous litedram\.Core instance\. "
+ r"Building this instance may overwrite previous build products. Passing "
+ r"`name_force=True` will disable this check."):
+ core_2 = litedram.Core(self._cfg, name="core")