From b8625215e051f1df1ff02ee6ccf8e0a0bd400422 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Jean-Fran=C3=A7ois=20Nguyen?= Date: Fri, 29 Oct 2021 16:41:22 +0200 Subject: [PATCH] cores.pll: add PLL generators for Lattice ECP5 and Xilinx 7 Series. --- lambdasoc/cores/pll/__init__.py | 0 lambdasoc/cores/pll/lattice_ecp5.py | 360 ++++++++++++++++++ lambdasoc/cores/pll/xilinx_7series.py | 291 ++++++++++++++ lambdasoc/test/test_cores_pll_lattice_ecp5.py | 228 +++++++++++ .../test/test_cores_pll_xilinx_7series.py | 197 ++++++++++ 5 files changed, 1076 insertions(+) create mode 100644 lambdasoc/cores/pll/__init__.py create mode 100644 lambdasoc/cores/pll/lattice_ecp5.py create mode 100644 lambdasoc/cores/pll/xilinx_7series.py create mode 100644 lambdasoc/test/test_cores_pll_lattice_ecp5.py create mode 100644 lambdasoc/test/test_cores_pll_xilinx_7series.py diff --git a/lambdasoc/cores/pll/__init__.py b/lambdasoc/cores/pll/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lambdasoc/cores/pll/lattice_ecp5.py b/lambdasoc/cores/pll/lattice_ecp5.py new file mode 100644 index 0000000..7fe6df0 --- /dev/null +++ b/lambdasoc/cores/pll/lattice_ecp5.py @@ -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 index 0000000..8466b95 --- /dev/null +++ b/lambdasoc/cores/pll/xilinx_7series.py @@ -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 index 0000000..face4d6 --- /dev/null +++ b/lambdasoc/test/test_cores_pll_lattice_ecp5.py @@ -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 index 0000000..f4cdadc --- /dev/null +++ b/lambdasoc/test/test_cores_pll_xilinx_7series.py @@ -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) -- 2.30.2