From 8b9f03efba5210780f1458a88bdc725624159139 Mon Sep 17 00:00:00 2001 From: George Hilliard Date: Tue, 6 Jul 2021 08:39:57 -0500 Subject: [PATCH 1/3] clock/lattice_ecp5/ECP5PLL: Expose standby signal --- litex/soc/cores/clock/lattice_ecp5.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/litex/soc/cores/clock/lattice_ecp5.py b/litex/soc/cores/clock/lattice_ecp5.py index ddf7d6591..d73f6536f 100644 --- a/litex/soc/cores/clock/lattice_ecp5.py +++ b/litex/soc/cores/clock/lattice_ecp5.py @@ -25,6 +25,7 @@ class ECP5PLL(Module): self.logger.info("Creating ECP5PLL.") self.reset = Signal() self.locked = Signal() + self.stdby = Signal() self.clkin_freq = None self.vcxo_freq = None self.nclkouts = 0 @@ -117,6 +118,7 @@ class ECP5PLL(Module): ("MFG_GMCREF_SEL", "2")], i_RST = self.reset, i_CLKI = self.clkin, + i_STDBY = self.stdby, o_LOCK = locked, p_FEEDBK_PATH = "INT_OS3", # CLKOS3 reserved for feedback with div=1. p_CLKOS3_ENABLE = "ENABLED", From 34ba649f380c37ad9d9a302b6d1a260fe5c32bf9 Mon Sep 17 00:00:00 2001 From: George Hilliard Date: Wed, 7 Jul 2021 00:55:52 -0500 Subject: [PATCH 2/3] clock/lattice_ecp5/ECP5PLL: implement 4-output solver Reimplement the configuration loop to allow all 4 outputs to be used by the user, if one of them is suitable for use as VCO feedback. The new strategy is to first iterate over requested outputs to see if any of them can be used as a feedback source. If one can, it is selected, and if no output is suitable, it attempts to instantiate one. Once the feedback path is selected, the VCO frequency is known and it attempts to calculate the remaining outputs' settings. In addition, this implementation now respects datasheet limits in two new ways: - It respects the post-input-divider minimum frequency of 10MHz - It respects the max output frequency of 400MHz for instantiated feedback outputs I am slightly unhappy with the seemingly-repetitive for loops. However each one has slightly different sematics and I don't see a way to combine them that doesn't hinder readability. --- litex/soc/cores/clock/lattice_ecp5.py | 84 +++++++++++++++++++-------- 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/litex/soc/cores/clock/lattice_ecp5.py b/litex/soc/cores/clock/lattice_ecp5.py index d73f6536f..12692d519 100644 --- a/litex/soc/cores/clock/lattice_ecp5.py +++ b/litex/soc/cores/clock/lattice_ecp5.py @@ -2,6 +2,7 @@ # This file is part of LiteX. # # Copyright (c) 2018-2020 Florent Kermarrec +# Copyright (c) 2021 George Hilliard # SPDX-License-Identifier: BSD-2-Clause from migen import * @@ -12,13 +13,14 @@ from litex.soc.cores.clock.common import * # Lattice / ECP5 ----------------------------------------------------------------------------------- class ECP5PLL(Module): - nclkouts_max = 3 + nclkouts_max = 4 clki_div_range = (1, 128+1) clkfb_div_range = (1, 128+1) clko_div_range = (1, 128+1) clki_freq_range = ( 8e6, 400e6) clko_freq_range = (3.125e6, 400e6) vco_freq_range = ( 400e6, 800e6) + pfd_freq_range = ( 10e6, 400e6) def __init__(self): self.logger = logging.getLogger("ECP5PLL") @@ -60,29 +62,66 @@ class ECP5PLL(Module): def compute_config(self): config = {} + + def in_range(n, r): + (r_min, r_max) = r + return n >= r_min and n <= r_max + + def found_clk(n, f, d, p): + config["clko{}_freq".format(n)] = f + config["clko{}_div".format(n)] = d + config["clko{}_phase".format(n)] = p + for clki_div in range(*self.clki_div_range): + if not in_range(self.clkin_freq / clki_div, self.pfd_freq_range): + continue config["clki_div"] = clki_div + for clkfb_div in range(*self.clkfb_div_range): - all_valid = True - vco_freq = self.clkin_freq/clki_div*clkfb_div*1 # clkos3_div=1 - (vco_freq_min, vco_freq_max) = self.vco_freq_range - if vco_freq >= vco_freq_min and vco_freq <= vco_freq_max: - for n, (clk, f, p, m) in sorted(self.clkouts.items()): - valid = False - for d in range(*self.clko_div_range): - clk_freq = vco_freq/d - if abs(clk_freq - f) <= f*m: - config["clko{}_freq".format(n)] = clk_freq - config["clko{}_div".format(n)] = d - config["clko{}_phase".format(n)] = p - valid = True - break - if not valid: - all_valid = False + # pick a suitable feedback clock + found_fb = None + for n, (clk, f, p, m) in sorted(self.clkouts.items()): + for d in range(*self.clko_div_range): + vco_freq = self.clkin_freq/clki_div*clkfb_div*d + clk_freq = vco_freq/d + if in_range(vco_freq, self.vco_freq_range) \ + and abs(clk_freq - f) <= f*m: + found_fb = n + found_clk(n, f, d, p) + break + if found_fb is not None: + break else: - all_valid = False + # none found, try to use a new output + for d in range(*self.clko_div_range): + vco_freq = self.clkin_freq/clki_div*clkfb_div*d + clk_freq = vco_freq/d + if self.nclkouts < self.nclkouts_max \ + and in_range(vco_freq, self.vco_freq_range) \ + and in_range(clk_freq, self.clko_freq_range): + found_fb = self.nclkouts + found_clk(found_fb, clk_freq, d, 0) + break + else: + continue + + # vco_freq is known, compute remaining clocks' output settings + all_valid = True + for n, (clk, f, p, m) in sorted(self.clkouts.items()): + if n == found_fb: + continue # already picked this one + for d in range(*self.clko_div_range): + clk_freq = vco_freq/d + if abs(clk_freq - f) <= f*m: + found_clk(n, f, d, p) + break + else: + all_valid = False if all_valid: + if found_fb > self.nclkouts: + self.create_clkout(ClockDomain('feedback'), vco_freq / clkfb_div) config["vco"] = vco_freq + config["clkfb"] = found_fb config["clkfb_div"] = clkfb_div compute_config_log(self.logger, config) return config @@ -107,8 +146,8 @@ class ECP5PLL(Module): def do_finalize(self): config = self.compute_config() - clkfb = Signal() locked = Signal() + n_to_l = {0: "P", 1: "S", 2: "S2", 3: "S3"} self.params.update( attr=[ ("FREQUENCY_PIN_CLKI", str(self.clkin_freq/1e6)), @@ -120,17 +159,12 @@ class ECP5PLL(Module): i_CLKI = self.clkin, i_STDBY = self.stdby, o_LOCK = locked, - p_FEEDBK_PATH = "INT_OS3", # CLKOS3 reserved for feedback with div=1. - p_CLKOS3_ENABLE = "ENABLED", - p_CLKOS3_DIV = 1, - p_CLKOS3_FPHASE = 0, - p_CLKOS3_CPHASE = 23, + p_FEEDBK_PATH = "INT_O{}".format(n_to_l[config['clkfb']]), p_CLKFB_DIV = config["clkfb_div"], p_CLKI_DIV = config["clki_div"] ) self.comb += self.locked.eq(locked & ~self.reset) for n, (clk, f, p, m) in sorted(self.clkouts.items()): - n_to_l = {0: "P", 1: "S", 2: "S2"} div = config["clko{}_div".format(n)] cphase = int(p*(div + 1)/360 + div - 1) self.params["p_CLKO{}_ENABLE".format(n_to_l[n])] = "ENABLED" From 8954041a9314c53c5f2b395aef00bd367384a2af Mon Sep 17 00:00:00 2001 From: George Hilliard Date: Thu, 8 Jul 2021 08:11:47 -0500 Subject: [PATCH 3/3] clock/lattice_ecp5/ECP5PLL: Only consider non-dpa clocks as feedback Dynamically adjusting the phase of a feedback will cause it to unlock. The phase adjust ports are shared by all the outputs, so there is no technical way to prevent this. Allow the user to indicate that they will not adjust a clock when requesting an output by setting uses_dpa=False, and only consider those that the user has promised not to use. --- litex/soc/cores/clock/lattice_ecp5.py | 15 ++++++++++----- test/test_clock.py | 3 ++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/litex/soc/cores/clock/lattice_ecp5.py b/litex/soc/cores/clock/lattice_ecp5.py index 12692d519..05fe6f694 100644 --- a/litex/soc/cores/clock/lattice_ecp5.py +++ b/litex/soc/cores/clock/lattice_ecp5.py @@ -30,6 +30,7 @@ class ECP5PLL(Module): self.stdby = Signal() self.clkin_freq = None self.vcxo_freq = None + self.dpa_en = False self.nclkouts = 0 self.clkouts = {} self.config = {} @@ -47,13 +48,13 @@ class ECP5PLL(Module): self.clkin_freq = freq register_clkin_log(self.logger, clkin, freq) - def create_clkout(self, cd, freq, phase=0, margin=1e-2, with_reset=True): + def create_clkout(self, cd, freq, phase=0, margin=1e-2, with_reset=True, uses_dpa=True): (clko_freq_min, clko_freq_max) = self.clko_freq_range assert freq >= clko_freq_min assert freq <= clko_freq_max assert self.nclkouts < self.nclkouts_max clkout = Signal() - self.clkouts[self.nclkouts] = (clkout, freq, phase, margin) + self.clkouts[self.nclkouts] = (clkout, freq, phase, margin, uses_dpa) if with_reset: self.specials += AsyncResetSynchronizer(cd, ~self.locked) self.comb += cd.clk.eq(clkout) @@ -80,7 +81,10 @@ class ECP5PLL(Module): for clkfb_div in range(*self.clkfb_div_range): # pick a suitable feedback clock found_fb = None - for n, (clk, f, p, m) in sorted(self.clkouts.items()): + for n, (clk, f, p, m, dpa) in sorted(self.clkouts.items()): + if dpa and self.dpa_en: + # cannot use clocks whose phase the user will change + continue for d in range(*self.clko_div_range): vco_freq = self.clkin_freq/clki_div*clkfb_div*d clk_freq = vco_freq/d @@ -107,7 +111,7 @@ class ECP5PLL(Module): # vco_freq is known, compute remaining clocks' output settings all_valid = True - for n, (clk, f, p, m) in sorted(self.clkouts.items()): + for n, (clk, f, p, m, dpa) in sorted(self.clkouts.items()): if n == found_fb: continue # already picked this one for d in range(*self.clko_div_range): @@ -128,6 +132,7 @@ class ECP5PLL(Module): raise ValueError("No PLL config found") def expose_dpa(self): + self.dpa_en = True self.phase_sel = Signal(2) self.phase_dir = Signal() self.phase_step = Signal() @@ -164,7 +169,7 @@ class ECP5PLL(Module): p_CLKI_DIV = config["clki_div"] ) self.comb += self.locked.eq(locked & ~self.reset) - for n, (clk, f, p, m) in sorted(self.clkouts.items()): + for n, (clk, f, p, m, dpa) in sorted(self.clkouts.items()): div = config["clko{}_div".format(n)] cphase = int(p*(div + 1)/360 + div - 1) self.params["p_CLKO{}_ENABLE".format(n_to_l[n])] = "ENABLED" diff --git a/test/test_clock.py b/test/test_clock.py index f0a3ed1e4..b23726f0c 100644 --- a/test/test_clock.py +++ b/test/test_clock.py @@ -117,7 +117,8 @@ class TestClock(unittest.TestCase): pll = ECP5PLL() pll.register_clkin(Signal(), 100e6) for i in range(pll.nclkouts_max): - pll.create_clkout(ClockDomain("clkout{}".format(i)), 200e6) + pll.create_clkout(ClockDomain("clkout{}".format(i)), 200e6, uses_dpa=(i != 0)) + pll.expose_dpa() pll.compute_config() # Lattice / NX