cores.pll: add PLL generators for Lattice ECP5 and Xilinx 7 Series.
authorJean-François Nguyen <jf@lambdaconcept.com>
Fri, 29 Oct 2021 14:41:22 +0000 (16:41 +0200)
committerJean-François Nguyen <jf@lambdaconcept.com>
Fri, 29 Oct 2021 14:43:01 +0000 (16:43 +0200)
lambdasoc/cores/pll/__init__.py [new file with mode: 0644]
lambdasoc/cores/pll/lattice_ecp5.py [new file with mode: 0644]
lambdasoc/cores/pll/xilinx_7series.py [new file with mode: 0644]
lambdasoc/test/test_cores_pll_lattice_ecp5.py [new file with mode: 0644]
lambdasoc/test/test_cores_pll_xilinx_7series.py [new file with mode: 0644]

diff --git a/lambdasoc/cores/pll/__init__.py b/lambdasoc/cores/pll/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lambdasoc/cores/pll/lattice_ecp5.py b/lambdasoc/cores/pll/lattice_ecp5.py
new file mode 100644 (file)
index 0000000..7fe6df0
--- /dev/null
@@ -0,0 +1,360 @@
+from collections import namedtuple, OrderedDict
+
+from nmigen import *
+
+
+__all__ = ["PLL_LatticeECP5"]
+
+
+class PLL_LatticeECP5(Elaboratable):
+    class Parameters:
+        class Output(namedtuple("Output", ["domain", "freq", "div", "cphase", "fphase"])):
+            """PLL output parameters."""
+            __slots__ = ()
+
+        """PLL parameters for Lattice ECP5 FPGAs.
+
+        Parameters
+        ----------
+        i_domain : str
+            Input clock domain.
+        i_freq : int or float
+            Input clock frequency, in Hz.
+        i_reset_less : bool
+            If `True`, the input clock domain does not use a reset signal. Defaults to `True`.
+        o_domain : str
+            Primary output clock domain.
+        o_freq : int or float
+            Primary output clock frequency, in Hz.
+        internal_fb : bool
+            Internal feedback mode. Optional. Defaults to `False`.
+
+        Attributes
+        ----------
+        i_domain : str
+            Input clock domain.
+        i_freq : int
+            Input clock frequency, in Hz.
+        i_reset_less : bool
+            If `True`, the input clock domain does not use a reset signal.
+        i_div : int
+            Input clock divisor.
+        o_domain : str
+            Primary output clock domain.
+        o_freq : int
+            Primary output clock frequency, in Hz.
+        fb_internal : bool
+            Internal feedback mode.
+        fb_div : int
+            Feedback clock divisor.
+        op : :class:`PLL_LatticeECP5.Parameters.Output`
+            Primary output parameters.
+        os, os2, os3 : :class:`PLL_LatticeECP5.Parameters.Output` or None
+            Secondary output parameters, or `None` if absent.
+        """
+        def __init__(self, *, i_domain, i_freq, o_domain, o_freq, i_reset_less=True, fb_internal=False):
+            if not isinstance(i_domain, str):
+                raise TypeError("Input domain must be a string, not {!r}"
+                                .format(i_domain))
+            if not isinstance(i_freq, (int, float)):
+                raise TypeError("Input frequency must be an integer or a float, not {!r}"
+                                .format(i_freq))
+            if not 8e6 <= i_freq <= 400e6:
+                raise ValueError("Input frequency must be between 8 and 400 MHz, not {} MHz"
+                                 .format(i_freq / 1e6))
+            if not isinstance(o_domain, str):
+                raise TypeError("Output domain must be a string, not {!r}"
+                                .format(o_domain))
+            if not isinstance(o_freq, (int, float)):
+                raise TypeError("Output frequency must be an integer or a float, not {!r}"
+                                .format(o_freq))
+            if not 10e6 <= o_freq <= 400e6:
+                raise ValueError("Output frequency must be between 10 and 400 MHz, not {} MHz"
+                                 .format(o_freq / 1e6))
+
+            self.i_domain     = i_domain
+            self.i_freq       = int(i_freq)
+            self.i_reset_less = bool(i_reset_less)
+            self.o_domain     = o_domain
+            self.o_freq       = int(o_freq)
+            self.fb_internal  = bool(fb_internal)
+
+            self._i_div       = None
+            self._fb_div      = None
+            self._op          = None
+            self._os          = None
+            self._os2         = None
+            self._os3         = None
+            self._2nd_outputs = OrderedDict()
+            self._frozen      = False
+
+        @property
+        def i_div(self):
+            self.compute()
+            return self._i_div
+
+        @property
+        def fb_div(self):
+            self.compute()
+            return self._fb_div
+
+        @property
+        def op(self):
+            self.compute()
+            return self._op
+
+        @property
+        def os(self):
+            self.compute()
+            return self._os
+
+        @property
+        def os2(self):
+            self.compute()
+            return self._os2
+
+        @property
+        def os3(self):
+            self.compute()
+            return self._os3
+
+        def add_secondary_output(self, *, domain, freq, phase=0.0):
+            """Add secondary PLL output.
+
+            Arguments
+            ---------
+            domain : str
+                Output clock domain.
+            freq : int
+                Output clock frequency.
+            phase : int or float
+                Output clock phase, in degrees. Optional. Defaults to 0.
+            """
+            if self._frozen:
+                raise ValueError("PLL parameters have already been computed. Other outputs cannot "
+                                 "be added")
+            if not isinstance(domain, str):
+                raise TypeError("Output domain must be a string, not {!r}"
+                                .format(domain))
+            if not isinstance(freq, (int, float)):
+                raise TypeError("Output frequency must be an integer or a float, not {!r}"
+                                .format(freq))
+            if not 10e6 <= freq <= 400e6:
+                raise ValueError("Output frequency must be between 10 and 400 MHz, not {} MHz"
+                                 .format(freq / 1e6))
+            if not isinstance(phase, (int, float)):
+                raise TypeError("Output phase must be an integer or a float, not {!r}"
+                                .format(phase))
+            if not 0 <= phase <= 360:
+                raise ValueError("Output phase must be between 0 and 360 degrees, not {}"
+                                 .format(phase))
+            if len(self._2nd_outputs) == 3:
+                raise ValueError("This PLL can drive at most 3 secondary outputs")
+            if domain in self._2nd_outputs:
+                raise ValueError("Output domain '{}' has already been added".format(domain))
+
+            self._2nd_outputs[domain] = freq, phase
+
+        def _iter_variants(self):
+            for i_div in range(1, 128 + 1):
+                pfd_freq = self.i_freq / i_div
+                if not 3.125e6 <= pfd_freq <= 400e6:
+                    continue
+                for fb_div in range(1, 80 + 1):
+                    for op_div in range(1, 128 + 1):
+                        vco_freq = pfd_freq * fb_div * op_div
+                        if not 400e6 <= vco_freq <= 800e6:
+                            continue
+                        op_freq = vco_freq / op_div
+                        if not 10e6 <= op_freq <= 400e6:
+                            continue
+                        yield (i_div, fb_div, op_div, pfd_freq, op_freq)
+
+        def compute(self):
+            """Compute PLL parameters.
+
+            This method is idempotent. As a side-effect of its first call, the visible state of the
+            :class:`PLL_LatticeECP5.Parameters` instance becomes immutable (e.g. adding more PLL outputs
+            will fail).
+            """
+            if self._frozen:
+                return
+
+            variants = list(self._iter_variants())
+            if not variants:
+                raise ValueError("Input ({} MHz) to primary output ({} MHz) constraint was not "
+                                 "satisfied"
+                                 .format(self.i_freq / 1e6, self.o_freq / 1e6))
+
+            def error(variant):
+                i_div, fb_div, op_div, pfd_freq, op_freq = variant
+                vco_freq = pfd_freq * fb_div * op_div
+                return abs(op_freq - self.o_freq), abs(vco_freq - 600e6), abs(pfd_freq - 200e6)
+
+            i_div, fb_div, op_div, pfd_freq, op_freq = min(variants, key=error)
+
+            vco_freq = pfd_freq * fb_div * op_div
+            op_shift = (1 / op_freq) * 0.5
+
+            self._i_div  = i_div
+            self._fb_div = fb_div
+
+            self._op = PLL_LatticeECP5.Parameters.Output(
+                domain = self.o_domain,
+                freq   = op_freq,
+                div    = op_div,
+                cphase = op_shift * vco_freq,
+                fphase = 0,
+            )
+
+            for i, (os_domain, (os_freq, os_phase)) in enumerate(self._2nd_outputs.items()):
+                os_name   = "_os{}".format(i + 1 if i > 0 else "")
+                os_shift  = (1 / os_freq) * os_phase / 360.0
+                os_params = PLL_LatticeECP5.Parameters.Output(
+                    domain = os_domain,
+                    freq   = os_freq,
+                    div    = vco_freq // os_freq,
+                    cphase = self._op.cphase + (os_shift * vco_freq),
+                    fphase = 0,
+                )
+                setattr(self, os_name, os_params)
+
+            self._frozen = True
+
+    """PLL for Lattice ECP5 FPGAs.
+
+    Parameters
+    ----------
+    params : :class:`PLL_LatticeECP5.Parameters`
+        PLL parameters.
+
+    Attributes
+    ----------
+    params : :class:`PLL_LatticeECP5.Parameters`
+        PLL parameters.
+    locked : Signal(), out
+        PLL lock status.
+    """
+    def __init__(self, params):
+        if not isinstance(params, PLL_LatticeECP5.Parameters):
+            raise TypeError("PLL parameters must be an instance of PLL_LatticeECP5.Parameters, not {!r}"
+                            .format(params))
+
+        params.compute()
+        self.params = params
+        self.locked = Signal()
+
+    def elaborate(self, platform):
+        pll_kwargs = {
+            "a_ICP_CURRENT"            : 12,
+            "a_LPF_RESISTOR"           : 8,
+            "a_MFG_ENABLE_FILTEROPAMP" : 1,
+            "a_MFG_GMCREF_SEL"         : 2,
+            "p_INTFB_WAKE"             : "DISABLED",
+            "p_STDBY_ENABLE"           : "DISABLED",
+            "p_DPHASE_SOURCE"          : "DISABLED",
+            "p_OUTDIVIDER_MUXA"        : "DIVA",
+            "p_OUTDIVIDER_MUXB"        : "DIVB",
+            "p_OUTDIVIDER_MUXC"        : "DIVC",
+            "p_OUTDIVIDER_MUXD"        : "DIVD",
+
+            "i_PHASESEL0"              : Const(0),
+            "i_PHASESEL1"              : Const(0),
+            "i_PHASEDIR"               : Const(1),
+            "i_PHASESTEP"              : Const(1),
+            "i_PHASELOADREG"           : Const(1),
+            "i_PLLWAKESYNC"            : Const(0),
+            "i_ENCLKOP"                : Const(0),
+
+            "o_LOCK"                   : self.locked,
+
+            "a_FREQUENCY_PIN_CLKI"     : int(self.params.i_freq / 1e6),
+            "p_CLKI_DIV"               : self.params.i_div,
+            "i_CLKI"                   : ClockSignal(self.params.i_domain),
+
+            "a_FREQUENCY_PIN_CLKOP"    : int(self.params.op.freq / 1e6),
+            "p_CLKOP_ENABLE"           : "ENABLED",
+            "p_CLKOP_DIV"              : self.params.op.div,
+            "p_CLKOP_CPHASE"           : self.params.op.cphase,
+            "p_CLKOP_FPHASE"           : self.params.op.fphase,
+            "o_CLKOP"                  : ClockSignal(self.params.op.domain),
+        }
+
+        # Secondary outputs
+
+        if self.params.os is not None:
+            pll_kwargs.update({
+                "a_FREQUENCY_PIN_CLKOS" : int(self.params.os.freq / 1e6),
+                "p_CLKOS_ENABLE"        : "ENABLED",
+                "p_CLKOS_DIV"           : self.params.os.div,
+                "p_CLKOS_CPHASE"        : self.params.os.cphase,
+                "p_CLKOS_FPHASE"        : self.params.os.fphase,
+                "o_CLKOS"               : ClockSignal(self.params.os.domain),
+            })
+        else:
+            pll_kwargs.update({
+                "p_CLKOS_ENABLE"        : "DISABLED",
+            })
+
+        if self.params.os2 is not None:
+            pll_kwargs.update({
+                "a_FREQUENCY_PIN_CLKOS2" : int(self.params.os2.freq / 1e6),
+                "p_CLKOS2_ENABLE"        : "ENABLED",
+                "p_CLKOS2_DIV"           : self.params.os2.div,
+                "p_CLKOS2_CPHASE"        : self.params.os2.cphase,
+                "p_CLKOS2_FPHASE"        : self.params.os2.fphase,
+                "o_CLKOS2"               : ClockSignal(self.params.os2.domain),
+            })
+        else:
+            pll_kwargs.update({
+                "p_CLKOS2_ENABLE"        : "DISABLED",
+            })
+
+        if self.params.os3 is not None:
+            pll_kwargs.update({
+                "a_FREQUENCY_PIN_CLKOS3" : int(self.params.os3.freq / 1e6),
+                "p_CLKOS3_ENABLE"        : "ENABLED",
+                "p_CLKOS3_DIV"           : self.params.os3.div,
+                "p_CLKOS3_CPHASE"        : self.params.os3.cphase,
+                "p_CLKOS3_FPHASE"        : self.params.os3.fphase,
+                "o_CLKOS3"               : ClockSignal(self.params.os3.domain),
+            })
+        else:
+            pll_kwargs.update({
+                "p_CLKOS3_ENABLE"        : "DISABLED",
+            })
+
+        # Reset
+
+        if not self.params.i_reset_less:
+            pll_kwargs.update({
+                "p_PLLRST_ENA" : "ENABLED",
+                "i_RST"        : ResetSignal(self.params.i_domain),
+            })
+        else:
+            pll_kwargs.update({
+                "p_PLLRST_ENA" : "DISABLED",
+                "i_RST"        : Const(0),
+            })
+
+        # Feedback
+
+        pll_kwargs.update({
+            "p_CLKFB_DIV" : int(self.params.fb_div),
+        })
+
+        if self.params.fb_internal:
+            clkintfb = Signal()
+            pll_kwargs.update({
+                "p_FEEDBK_PATH" : "INT_OP",
+                "i_CLKFB"       : clkintfb,
+                "o_CLKINTFB"    : clkintfb,
+            })
+        else:
+            pll_kwargs.update({
+                "p_FEEDBK_PATH" : "CLKOP",
+                "i_CLKFB"       : ClockSignal(self.params.op.domain),
+                "o_CLKINTFB"    : Signal(),
+            })
+
+        return Instance("EHXPLLL", **pll_kwargs)
diff --git a/lambdasoc/cores/pll/xilinx_7series.py b/lambdasoc/cores/pll/xilinx_7series.py
new file mode 100644 (file)
index 0000000..8466b95
--- /dev/null
@@ -0,0 +1,291 @@
+from collections import namedtuple, OrderedDict
+
+from nmigen import *
+
+
+__all__ = ["PLL_Xilinx7Series"]
+
+
+class PLL_Xilinx7Series(Elaboratable):
+    class Parameters:
+        class Output(namedtuple("Output", ["domain", "freq", "div", "phase"])):
+            """PLL output parameters."""
+            __slots__ = ()
+
+        """PLL parameters for Xilinx 7 Series FPGAs.
+
+        Parameters
+        ----------
+        i_domain : str
+            Input clock domain.
+        i_freq : int or float
+            Input clock frequency, in Hz.
+        i_reset_less : bool
+            If `True`, the input clock domain does not use a reset signal. Defaults to `True`.
+        o_domain : str
+            Primary output clock domain.
+        o_freq : int or float
+            Primary output clock frequency, in Hz.
+
+        Attributes
+        ----------
+        i_domain : str
+            Input clock domain.
+        i_freq : int
+            Input clock frequency, in Hz.
+        i_reset_less : bool
+            If `True`, the input clock domain does not use a reset signal.
+        o_domain : str
+            Primary output clock domain.
+        o_freq : int
+            Primary output clock frequency, in Hz.
+        divclk_div : int
+            Input clock divisor.
+        clkfbout_mult : int
+            Feedback clock multiplier.
+        clkout0 : :class:`PLL_Xilinx7Series.Parameters.Output`
+            Primary output parameters.
+        clkout{1..5} : :class:`PLL_Xilinx7Series.Parameters.Output` or None
+            Secondary output parameters, or `None` if absent.
+        """
+        def __init__(self, *, i_domain, i_freq, o_domain, o_freq, i_reset_less=True):
+            if not isinstance(i_domain, str):
+                raise TypeError("Input domain must be a string, not {!r}"
+                                .format(i_domain))
+            if not isinstance(i_freq, (int, float)):
+                raise TypeError("Input frequency must be an integer or a float, not {!r}"
+                                .format(i_freq))
+            if not 19e6 <= i_freq <= 800e6:
+                raise ValueError("Input frequency must be between 19 and 800 MHz, not {} MHz"
+                                 .format(i_freq / 1e6))
+            if not isinstance(o_domain, str):
+                raise TypeError("Output domain must be a string, not {!r}"
+                                .format(o_domain))
+            if not isinstance(o_freq, (int, float)):
+                raise TypeError("Output frequency must be an integer or a float, not {!r}"
+                                 .format(o_freq))
+            if not 6.25e6 <= o_freq <= 800e6:
+                raise ValueError("Output frequency must be between 6.25 and 800 MHz, not {} MHz"
+                                 .format(o_freq / 1e6))
+
+            self.i_domain       = i_domain
+            self.i_freq         = int(i_freq)
+            self.i_reset_less   = bool(i_reset_less)
+            self.o_domain       = o_domain
+            self.o_freq         = int(o_freq)
+
+            self._divclk_div    = None
+            self._clkfbout_mult = None
+            self._clkout0       = None
+            self._clkout1       = None
+            self._clkout2       = None
+            self._clkout3       = None
+            self._clkout4       = None
+            self._clkout5       = None
+
+            self._2nd_outputs   = OrderedDict()
+            self._frozen        = False
+
+        @property
+        def divclk_div(self):
+            self.compute()
+            return self._divclk_div
+
+        @property
+        def clkfbout_mult(self):
+            self.compute()
+            return self._clkfbout_mult
+
+        @property
+        def clkout0(self):
+            self.compute()
+            return self._clkout0
+
+        @property
+        def clkout1(self):
+            self.compute()
+            return self._clkout1
+
+        @property
+        def clkout2(self):
+            self.compute()
+            return self._clkout2
+
+        @property
+        def clkout3(self):
+            self.compute()
+            return self._clkout3
+
+        @property
+        def clkout4(self):
+            self.compute()
+            return self._clkout4
+
+        @property
+        def clkout5(self):
+            self.compute()
+            return self._clkout5
+
+        def add_secondary_output(self, *, domain, freq, phase=0.0):
+            """Add secondary PLL output.
+
+            Arguments
+            ---------
+            domain : str
+                Output clock domain.
+            freq : int
+                Output clock frequency.
+            phase : int or float
+                Output clock phase, in degrees. Optional. Defaults to 0.
+            """
+            if self._frozen:
+                raise ValueError("PLL parameters have already been computed. Other outputs cannot "
+                                 "be added")
+            if not isinstance(domain, str):
+                raise TypeError("Output domain must be a string, not {!r}"
+                                .format(domain))
+            if not isinstance(freq, (int, float)):
+                raise TypeError("Output frequency must be an integer or a float, not {!r}"
+                                .format(freq))
+            if not 6.25e6 <= freq <= 800e6:
+                raise ValueError("Output frequency must be between 6.25 and 800 MHz, not {} MHz"
+                                 .format(freq / 1e6))
+            if not isinstance(phase, (int, float)):
+                raise TypeError("Output phase must be an integer or a float, not {!r}"
+                                .format(phase))
+            if not 0 <= phase <= 360.0:
+                raise ValueError("Output phase must be between 0 and 360 degrees, not {}"
+                                 .format(phase))
+            if len(self._2nd_outputs) == 5:
+                raise ValueError("This PLL can drive at most 5 secondary outputs")
+            if domain in self._2nd_outputs:
+                raise ValueError("Output domain '{}' has already been added".format(domain))
+
+            self._2nd_outputs[domain] = freq, phase
+
+        def _iter_variants(self):
+            # FIXME: PFD freq ?
+            for divclk_div in range(1, 56 + 1):
+                for clkfbout_mult in reversed(range(2, 64 + 1)):
+                    vco_freq = self.i_freq * clkfbout_mult / divclk_div
+                    # This VCO range assumes a -1 speedgrade.
+                    if not 800e6 <= vco_freq <= 1600e6:
+                        continue
+                    for clkout0_div in range(1, 128 + 1):
+                        clkout0_freq = vco_freq / clkout0_div
+                        if not 6.25e6 <= clkout0_freq <= 800e6:
+                            continue
+                        yield (divclk_div, clkfbout_mult, clkout0_freq, clkout0_div)
+
+        def compute(self):
+            """Compute PLL parameters.
+
+            This method is idempotent. As a side-effect of its first call, the visible state of the
+            :class:`PLL_Xilinx7Series.Parameters` instance becomes immutable (e.g. adding more PLL outputs
+            will fail).
+            """
+            if self._frozen:
+                return
+
+            variants = list(self._iter_variants())
+            if not variants:
+                raise ValueError("Input ({} MHz) to primary output ({} MHz) constraint was not "
+                                 "satisfied"
+                                 .format(self.i_freq / 1e6, self.o_freq / 1e6))
+
+            def error(variant):
+                divclk_div, clkfbout_mult, clkout0_freq, clkout0_div = variant
+                vco_freq = self.i_freq * clkfbout_mult / divclk_div
+                # Idem, assuming a -1 speedgrade.
+                return abs(clkout0_freq - self.o_freq), abs(vco_freq - (800e6 + 1600e6) / 2)
+
+            divclk_div, clkfbout_mult, clkout0_freq, clkout0_div = min(variants, key=error)
+
+            self._divclk_div    = divclk_div
+            self._clkfbout_mult = clkfbout_mult
+
+            vco_freq = self.i_freq * clkfbout_mult / divclk_div
+            self._clkout0 = PLL_Xilinx7Series.Parameters.Output(
+                domain = self.o_domain,
+                freq   = clkout0_freq,
+                div    = clkout0_div,
+                phase  = 0.0,
+            )
+
+            for i, (out_domain, (out_freq, out_phase)) in enumerate(self._2nd_outputs.items()):
+                out_name = "_clkout{}".format(i + 1)
+                out_params = PLL_Xilinx7Series.Parameters.Output(
+                    domain = out_domain,
+                    freq   = out_freq,
+                    div    = vco_freq / out_freq,
+                    phase  = (self._clkout0.phase + out_phase) % 360.0,
+                )
+                setattr(self, out_name, out_params)
+
+            self._frozen = True
+
+    """PLL for Xilinx 7 Series FPGAs.
+
+    Parameters
+    ----------
+    params : :class:`PLL_Xilinx7Series.Parameters`
+        PLL parameters.
+
+    Attributes
+    ----------
+    params : :class:`PLL_Xilinx7Series.Parameters`
+        PLL parameters.
+    locked : Signal(), out
+        PLL lock status.
+    """
+    def __init__(self, params):
+        if not isinstance(params, PLL_Xilinx7Series.Parameters):
+            raise TypeError("PLL parameters must be an instance of PLL_Xilinx7Series.Parameters, not {!r}"
+                            .format(params))
+
+        params.compute()
+        self.params = params
+        self.locked = Signal()
+
+    def elaborate(self, platform):
+        pll_fb = Signal()
+
+        pll_kwargs = {
+            "p_STARTUP_WAIT"   : "FALSE",
+            "i_PWRDWN"         : Const(0),
+            "o_LOCKED"         : self.locked,
+
+            "p_REF_JITTER1"    : 0.01,
+            "p_CLKIN1_PERIOD"  : 1e9 / self.params.i_freq,
+            "i_CLKIN1"         : ClockSignal(self.params.i_domain),
+
+            "p_DIVCLK_DIVIDE"  : self.params.divclk_div,
+            "p_CLKFBOUT_MULT"  : self.params.clkfbout_mult,
+            "i_CLKFBIN"        : pll_fb,
+            "o_CLKFBOUT"       : pll_fb,
+
+            "p_CLKOUT0_DIVIDE" : self.params.clkout0.div,
+            "p_CLKOUT0_PHASE"  : self.params.clkout0.phase,
+            "o_CLKOUT0"        : ClockSignal(self.params.clkout0.domain),
+        }
+
+        if self.params.i_reset_less:
+            pll_kwargs.update({
+                "i_RST" : Const(0),
+            })
+        else:
+            pll_kwargs.update({
+                "i_RST" : ResetSignal(self.params.i_domain),
+            })
+
+        for i in range(5):
+            clkout_name   = "clkout{}".format(i + 1)
+            clkout_params = getattr(self.params, clkout_name)
+            if clkout_params is not None:
+                pll_kwargs.update({
+                    f"p_{clkout_name.upper()}_DIVIDE" : clkout_params.div,
+                    f"p_{clkout_name.upper()}_PHASE"  : clkout_params.phase,
+                    f"o_{clkout_name.upper()}"        : ClockSignal(clkout_params.domain),
+                })
+
+        return Instance("PLLE2_BASE", **pll_kwargs)
diff --git a/lambdasoc/test/test_cores_pll_lattice_ecp5.py b/lambdasoc/test/test_cores_pll_lattice_ecp5.py
new file mode 100644 (file)
index 0000000..face4d6
--- /dev/null
@@ -0,0 +1,228 @@
+# nmigen: UnusedElaboratable=no
+
+import unittest
+from nmigen import *
+
+from ..cores.pll.lattice_ecp5 import PLL_LatticeECP5
+
+
+class PLL_LatticeECP5__ParametersTestCase(unittest.TestCase):
+    def test_simple(self):
+        params1 = PLL_LatticeECP5.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        self.assertEqual(params1.i_domain, "foo")
+        self.assertEqual(params1.i_freq, 100e6)
+        self.assertEqual(params1.i_reset_less, True)
+        self.assertEqual(params1.o_domain, "bar")
+        self.assertEqual(params1.o_freq, 50e6)
+        self.assertEqual(params1.fb_internal, False)
+
+        params2 = PLL_LatticeECP5.Parameters(
+            i_domain     = "baz",
+            i_freq       = int(12e6),
+            i_reset_less = False,
+            o_domain     = "qux",
+            o_freq       = int(48e6),
+            fb_internal  = True,
+        )
+        self.assertEqual(params2.i_domain, "baz")
+        self.assertEqual(params2.i_freq, 12e6)
+        self.assertEqual(params2.i_reset_less, False)
+        self.assertEqual(params2.o_domain, "qux")
+        self.assertEqual(params2.o_freq, 48e6)
+        self.assertEqual(params2.fb_internal, True)
+
+    def test_wrong_i_domain(self):
+        with self.assertRaisesRegex(TypeError,
+                r"Input domain must be a string, not 1"):
+            params = PLL_LatticeECP5.Parameters(
+                i_domain = 1,
+                i_freq   = 100e6,
+                o_domain = "bar",
+                o_freq   = 50e6,
+            )
+
+    def test_wrong_i_freq_type(self):
+        with self.assertRaisesRegex(TypeError,
+                r"Input frequency must be an integer or a float, not 'baz'"):
+            params = PLL_LatticeECP5.Parameters(
+                i_domain = "foo",
+                i_freq   = "baz",
+                o_domain = "bar",
+                o_freq   = 50e6,
+            )
+
+    def test_wrong_i_freq_range(self):
+        with self.assertRaisesRegex(ValueError,
+                r"Input frequency must be between 8 and 400 MHz, not 420.0 MHz"):
+            params = PLL_LatticeECP5.Parameters(
+                i_domain = "foo",
+                i_freq   = 420e6,
+                o_domain = "bar",
+                o_freq   = 50e6,
+            )
+
+    def test_wrong_o_domain(self):
+        with self.assertRaisesRegex(TypeError,
+                r"Output domain must be a string, not 1"):
+            params = PLL_LatticeECP5.Parameters(
+                i_domain = "foo",
+                i_freq   = 100e6,
+                o_domain = 1,
+                o_freq   = 50e6,
+            )
+
+    def test_wrong_o_freq_type(self):
+        with self.assertRaisesRegex(TypeError,
+                r"Output frequency must be an integer or a float, not 'baz'"):
+            params = PLL_LatticeECP5.Parameters(
+                i_domain = "foo",
+                i_freq   = 50e6,
+                o_domain = "bar",
+                o_freq   = "baz",
+            )
+
+    def test_wrong_o_freq_range(self):
+        with self.assertRaisesRegex(ValueError,
+                r"Output frequency must be between 10 and 400 MHz, not 420.0 MHz"):
+            params = PLL_LatticeECP5.Parameters(
+                i_domain = "foo",
+                i_freq   = 100e6,
+                o_domain = "bar",
+                o_freq   = 420e6,
+            )
+
+    def test_add_secondary_output_wrong_domain(self):
+        params = PLL_LatticeECP5.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        with self.assertRaisesRegex(TypeError,
+                r"Output domain must be a string, not 1"):
+            params.add_secondary_output(domain=1, freq=10e6)
+
+    def test_add_secondary_output_wrong_freq_type(self):
+        params = PLL_LatticeECP5.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        with self.assertRaisesRegex(TypeError,
+                r"Output frequency must be an integer or a float, not 'a'"):
+            params.add_secondary_output(domain="baz", freq="a")
+
+    def test_add_secondary_output_wrong_freq_range(self):
+        params = PLL_LatticeECP5.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        with self.assertRaisesRegex(ValueError,
+                r"Output frequency must be between 10 and 400 MHz, not 8.0 MHz"):
+            params.add_secondary_output(domain="baz", freq=8e6)
+
+    def test_add_secondary_output_wrong_phase_type(self):
+        params = PLL_LatticeECP5.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        with self.assertRaisesRegex(TypeError,
+                r"Output phase must be an integer or a float, not 'a'"):
+            params.add_secondary_output(domain="baz", freq=10e6, phase="a")
+
+    def test_add_secondary_output_wrong_phase_range(self):
+        params = PLL_LatticeECP5.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        with self.assertRaisesRegex(ValueError,
+                r"Output phase must be between 0 and 360 degrees, not -1"):
+            params.add_secondary_output(domain="baz", freq=10e6, phase=-1)
+
+    def test_add_secondary_output_exceeded(self):
+        params = PLL_LatticeECP5.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        params.add_secondary_output(domain="a", freq=10e6)
+        params.add_secondary_output(domain="b", freq=10e6)
+        params.add_secondary_output(domain="c", freq=10e6)
+        with self.assertRaisesRegex(ValueError,
+                r"This PLL can drive at most 3 secondary outputs"):
+            params.add_secondary_output(domain="d", freq=10e6)
+
+    def test_add_secondary_output_same_domain(self):
+        params = PLL_LatticeECP5.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        params.add_secondary_output(domain="a", freq=10e6)
+        with self.assertRaisesRegex(ValueError,
+                r"Output domain 'a' has already been added"):
+            params.add_secondary_output(domain="a", freq=10e6)
+
+    def test_compute_primary(self):
+        def result(i_freq, o_freq):
+            params = PLL_LatticeECP5.Parameters(
+                i_domain = "i",
+                i_freq   = i_freq,
+                o_domain = "o",
+                o_freq   = o_freq,
+            )
+            params.compute()
+            return (params.i_div, params.fb_div, params.op.div)
+
+        vectors = [
+            # Values are taken from ecppll in prjtrellis.
+            # i_freq, o_freq, i_div, fb_div, op_div
+            (   12e6,   48e6,     1,      4,     12),
+            (   12e6,   60e6,     1,      5,     10),
+            (   20e6,   30e6,     2,      3,     20),
+            (   45e6,   30e6,     3,      2,     20),
+            (  100e6,  400e6,     1,      4,      1),
+            (  200e6,  400e6,     1,      2,      1),
+            (   50e6,  400e6,     1,      8,      1),
+            (   70e6,   40e6,     7,      4,     15),
+            (   12e6,   36e6,     1,      3,     17),
+            (   12e6,   96e6,     1,      8,      6),
+            (   90e6,   40e6,     9,      4,     15),
+            (   90e6,   50e6,     9,      5,     12),
+            (   43e6,   86e6,     1,      2,      7),
+        ]
+
+        self.assertEqual(
+            [(i_freq, o_freq, *result(i_freq, o_freq)) for i_freq, o_freq, *_ in vectors],
+            vectors
+        )
+
+    # TODO
+    # def test_compute_secondary(self):
+        # pass
+
+    def test_add_secondary_output_frozen(self):
+        params = PLL_LatticeECP5.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        params.compute()
+        with self.assertRaisesRegex(ValueError,
+                r"PLL parameters have already been computed. Other outputs cannot be added"):
+            params.add_secondary_output(domain="a", freq=10e6)
diff --git a/lambdasoc/test/test_cores_pll_xilinx_7series.py b/lambdasoc/test/test_cores_pll_xilinx_7series.py
new file mode 100644 (file)
index 0000000..f4cdadc
--- /dev/null
@@ -0,0 +1,197 @@
+# nmigen: UnusedElaboratable=no
+
+import unittest
+from nmigen import *
+
+from ..cores.pll.xilinx_7series import PLL_Xilinx7Series
+
+
+class PLL_Xilinx7Series__ParametersTestCase(unittest.TestCase):
+    def test_simple(self):
+        params1 = PLL_Xilinx7Series.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        self.assertEqual(params1.i_domain, "foo")
+        self.assertEqual(params1.i_freq, 100e6)
+        self.assertEqual(params1.i_reset_less, True)
+        self.assertEqual(params1.o_domain, "bar")
+        self.assertEqual(params1.o_freq, 50e6)
+
+        params2 = PLL_Xilinx7Series.Parameters(
+            i_domain     = "baz",
+            i_freq       = int(20e6),
+            i_reset_less = False,
+            o_domain     = "qux",
+            o_freq       = int(40e6),
+        )
+        self.assertEqual(params2.i_domain, "baz")
+        self.assertEqual(params2.i_freq, 20e6)
+        self.assertEqual(params2.i_reset_less, False)
+        self.assertEqual(params2.o_domain, "qux")
+        self.assertEqual(params2.o_freq, 40e6)
+
+    def test_wrong_i_domain(self):
+        with self.assertRaisesRegex(TypeError,
+                r"Input domain must be a string, not 1"):
+            params = PLL_Xilinx7Series.Parameters(
+                i_domain = 1,
+                i_freq   = 100e6,
+                o_domain = "bar",
+                o_freq   = 50e6,
+            )
+
+    def test_wrong_i_freq_type(self):
+        with self.assertRaisesRegex(TypeError,
+                r"Input frequency must be an integer or a float, not 'baz'"):
+            params = PLL_Xilinx7Series.Parameters(
+                i_domain = "foo",
+                i_freq   = "baz",
+                o_domain = "bar",
+                o_freq   = 50e6,
+            )
+
+    def test_wrong_i_freq_range(self):
+        with self.assertRaisesRegex(ValueError,
+                r"Input frequency must be between 19 and 800 MHz, not 820.0 MHz"):
+            params = PLL_Xilinx7Series.Parameters(
+                i_domain = "foo",
+                i_freq   = 820e6,
+                o_domain = "bar",
+                o_freq   = 50e6,
+            )
+
+    def test_wrong_o_domain(self):
+        with self.assertRaisesRegex(TypeError,
+                r"Output domain must be a string, not 1"):
+            params = PLL_Xilinx7Series.Parameters(
+                i_domain = "foo",
+                i_freq   = 100e6,
+                o_domain = 1,
+                o_freq   = 50e6,
+            )
+
+    def test_wrong_o_freq_type(self):
+        with self.assertRaisesRegex(TypeError,
+                r"Output frequency must be an integer or a float, not 'baz'"):
+            params = PLL_Xilinx7Series.Parameters(
+                i_domain = "foo",
+                i_freq   = 50e6,
+                o_domain = "bar",
+                o_freq   = "baz",
+            )
+
+    def test_wrong_o_freq_range(self):
+        with self.assertRaisesRegex(ValueError,
+                r"Output frequency must be between 6.25 and 800 MHz, not 820.0 MHz"):
+            params = PLL_Xilinx7Series.Parameters(
+                i_domain = "foo",
+                i_freq   = 100e6,
+                o_domain = "bar",
+                o_freq   = 820e6,
+            )
+
+    def test_add_secondary_output_wrong_domain(self):
+        params = PLL_Xilinx7Series.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        with self.assertRaisesRegex(TypeError,
+                r"Output domain must be a string, not 1"):
+            params.add_secondary_output(domain=1, freq=10e6)
+
+    def test_add_secondary_output_wrong_freq_type(self):
+        params = PLL_Xilinx7Series.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        with self.assertRaisesRegex(TypeError,
+                r"Output frequency must be an integer or a float, not 'a'"):
+            params.add_secondary_output(domain="baz", freq="a")
+
+    def test_add_secondary_output_wrong_freq_range(self):
+        params = PLL_Xilinx7Series.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        with self.assertRaisesRegex(ValueError,
+                r"Output frequency must be between 6.25 and 800 MHz, not 5.0 MHz"):
+            params.add_secondary_output(domain="baz", freq=5e6)
+
+    def test_add_secondary_output_wrong_phase_type(self):
+        params = PLL_Xilinx7Series.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        with self.assertRaisesRegex(TypeError,
+                r"Output phase must be an integer or a float, not 'a'"):
+            params.add_secondary_output(domain="baz", freq=10e6, phase="a")
+
+    def test_add_secondary_output_wrong_phase_range(self):
+        params = PLL_Xilinx7Series.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        with self.assertRaisesRegex(ValueError,
+                r"Output phase must be between 0 and 360 degrees, not -1"):
+            params.add_secondary_output(domain="baz", freq=10e6, phase=-1)
+
+    def test_add_secondary_output_exceeded(self):
+        params = PLL_Xilinx7Series.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        params.add_secondary_output(domain="a", freq=10e6)
+        params.add_secondary_output(domain="b", freq=10e6)
+        params.add_secondary_output(domain="c", freq=10e6)
+        params.add_secondary_output(domain="d", freq=10e6)
+        params.add_secondary_output(domain="e", freq=10e6)
+        with self.assertRaisesRegex(ValueError,
+                r"This PLL can drive at most 5 secondary outputs"):
+            params.add_secondary_output(domain="f", freq=10e6)
+
+    def test_add_secondary_output_same_domain(self):
+        params = PLL_Xilinx7Series.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        params.add_secondary_output(domain="a", freq=10e6)
+        with self.assertRaisesRegex(ValueError,
+                r"Output domain 'a' has already been added"):
+            params.add_secondary_output(domain="a", freq=10e6)
+
+    # TODO
+    # def test_compute_primary(self):
+    #     pass
+
+    # TODO
+    # def test_compute_secondary(self):
+        # pass
+
+    def test_add_secondary_output_frozen(self):
+        params = PLL_Xilinx7Series.Parameters(
+            i_domain = "foo",
+            i_freq   = 100e6,
+            o_domain = "bar",
+            o_freq   = 50e6,
+        )
+        params.compute()
+        with self.assertRaisesRegex(ValueError,
+                r"PLL parameters have already been computed. Other outputs cannot be added"):
+            params.add_secondary_output(domain="a", freq=10e6)