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.
This commit is contained in:
George Hilliard 2021-07-07 00:55:52 -05:00
parent 8b9f03efba
commit 34ba649f38
1 changed files with 59 additions and 25 deletions

View File

@ -2,6 +2,7 @@
# This file is part of LiteX.
#
# Copyright (c) 2018-2020 Florent Kermarrec <florent@enjoy-digital.fr>
# Copyright (c) 2021 George Hilliard <thirtythreeforty@gmail.com>
# 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"