cores: add LiteDRAM core.
authorJean-François Nguyen <jf@lambdaconcept.com>
Mon, 28 Jun 2021 15:45:48 +0000 (17:45 +0200)
committerJean-François Nguyen <jf@lambdaconcept.com>
Mon, 28 Jun 2021 15:53:14 +0000 (17:53 +0200)
lambdasoc/cores/__init__.py [new file with mode: 0644]
lambdasoc/cores/litedram.py [new file with mode: 0644]
lambdasoc/test/test_cores_litedram.py [new file with mode: 0644]

diff --git a/lambdasoc/cores/__init__.py b/lambdasoc/cores/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lambdasoc/cores/litedram.py b/lambdasoc/cores/litedram.py
new file mode 100644 (file)
index 0000000..9936b85
--- /dev/null
@@ -0,0 +1,740 @@
+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
diff --git a/lambdasoc/test/test_cores_litedram.py b/lambdasoc/test/test_cores_litedram.py
new file mode 100644 (file)
index 0000000..58c1893
--- /dev/null
@@ -0,0 +1,461 @@
+# 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")