From cab1efc412c2eb6fe6800dada1a037fffdf6a344 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Jean-Fran=C3=A7ois=20Nguyen?= Date: Mon, 28 Jun 2021 17:45:48 +0200 Subject: [PATCH] cores: add LiteDRAM core. --- lambdasoc/cores/__init__.py | 0 lambdasoc/cores/litedram.py | 740 ++++++++++++++++++++++++++ lambdasoc/test/test_cores_litedram.py | 461 ++++++++++++++++ 3 files changed, 1201 insertions(+) create mode 100644 lambdasoc/cores/__init__.py create mode 100644 lambdasoc/cores/litedram.py create mode 100644 lambdasoc/test/test_cores_litedram.py diff --git a/lambdasoc/cores/__init__.py b/lambdasoc/cores/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lambdasoc/cores/litedram.py b/lambdasoc/cores/litedram.py new file mode 100644 index 0000000..9936b85 --- /dev/null +++ b/lambdasoc/cores/litedram.py @@ -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="".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 index 0000000..58c1893 --- /dev/null +++ b/lambdasoc/test/test_cores_litedram.py @@ -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") -- 2.30.2