Lattice NX PLL Support
This commit is contained in:
parent
9359aa0688
commit
a6fd7b5d37
|
@ -0,0 +1,424 @@
|
|||
#
|
||||
# This file is part of LiteX.
|
||||
#
|
||||
# Copyright (c) 2020 David Corrigan <davidcorrigan714@gmail.com>
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
import math
|
||||
import pprint
|
||||
from math import log, log10, exp, pi
|
||||
from cmath import phase
|
||||
|
||||
from migen import *
|
||||
|
||||
from litex.soc.cores.clock import register_clkin_log, create_clkout_log, compute_config_log
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
io_i2 = namedtuple('io_i2',['io', 'i2', 'IPP_CTRL', 'BW_CTL_BIAS', 'IPP_SEL'])
|
||||
nx_pll_param_permutation = namedtuple("nx_pll_param_permutation",[
|
||||
"C1","C2","C3","C4","C5","C6",
|
||||
"IPP_CTRL","BW_CTL_BIAS","IPP_SEL","CSET","CRIPPLE","V2I_PP_RES","IPI_CMP"])
|
||||
|
||||
class NXPLL(Module):
|
||||
nclkouts_max = 5
|
||||
clki_div_range = ( 1, 128+1)
|
||||
clkfb_div_range = ( 1, 128+1)
|
||||
clko_div_range = ( 1, 128+1)
|
||||
clki_freq_range = ( 10e6, 500e6)
|
||||
clko_freq_range = ( 6.25e6, 800e6)
|
||||
vco_in_freq_range = ( 10e6, 500e6)
|
||||
vco_out_freq_range = ( 800e6, 1600e6)
|
||||
instance_num = 0
|
||||
|
||||
def __init__(self, platform = None, create_output_port_clocks=False):
|
||||
self.logger = logging.getLogger("NXPLL")
|
||||
self.logger.info("Creating NXPLL.")
|
||||
self.params = {}
|
||||
self.reset = Signal()
|
||||
self.locked = Signal()
|
||||
self.params["o_LOCK"] = self.locked
|
||||
self.clkin_freq = None
|
||||
self.vcxo_freq = None
|
||||
self.nclkouts = 0
|
||||
self.clkouts = {}
|
||||
self.config = {}
|
||||
self.name = 'PLL_' + str(NXPLL.instance_num)
|
||||
NXPLL.instance_num += 1
|
||||
self.platform = platform
|
||||
self.create_output_port_clocks = create_output_port_clocks
|
||||
|
||||
self.calc_valid_io_i2()
|
||||
self.calc_tf_coefficients()
|
||||
|
||||
def register_clkin(self, clkin, freq):
|
||||
(clki_freq_min, clki_freq_max) = self.clki_freq_range
|
||||
assert freq >= clki_freq_min
|
||||
assert freq <= clki_freq_max
|
||||
self.clkin = Signal()
|
||||
if isinstance(clkin, (Signal, ClockSignal)):
|
||||
self.comb += self.clkin.eq(clkin)
|
||||
else:
|
||||
raise ValueError
|
||||
self.clkin_freq = freq
|
||||
register_clkin_log(self.logger, clkin, freq)
|
||||
|
||||
def create_clkout(self, cd, freq, phase=0, margin=1e-2):
|
||||
(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
|
||||
self.clkouts[self.nclkouts] = (cd.clk, freq, phase, margin)
|
||||
create_clkout_log(self.logger, cd.name, freq, margin, self.nclkouts)
|
||||
self.nclkouts += 1
|
||||
|
||||
def compute_config(self):
|
||||
config = {}
|
||||
for clki_div in range(*self.clki_div_range):
|
||||
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
|
||||
(vco_freq_min, vco_freq_max) = self.vco_out_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
|
||||
else:
|
||||
all_valid = False
|
||||
if all_valid:
|
||||
config["vco"] = vco_freq
|
||||
config["clkfb_div"] = clkfb_div
|
||||
compute_config_log(self.logger, config)
|
||||
return config
|
||||
raise ValueError("No PLL config found")
|
||||
|
||||
def calculate_analog_parameters(self, clki_freq, fb_div, bw_factor = 5):
|
||||
config = {}
|
||||
|
||||
params = self.calc_optimal_params(clki_freq, fb_div, 1, bw_factor)
|
||||
config["p_CSET"] = params["CSET"]
|
||||
config["p_CRIPPLE"] = params["CRIPPLE"]
|
||||
config["p_V2I_PP_RES"] = params["V2I_PP_RES"]
|
||||
config["p_IPP_SEL"] = params["IPP_SEL"]
|
||||
config["p_IPP_CTRL"] = params["IPP_CTRL"]
|
||||
config["p_BW_CTL_BIAS"] = params["BW_CTL_BIAS"]
|
||||
config["p_IPI_CMP"] = params["IPI_CMP"]
|
||||
|
||||
return config
|
||||
|
||||
def do_finalize(self):
|
||||
config = self.compute_config()
|
||||
clkfb = Signal()
|
||||
|
||||
self.params.update(
|
||||
p_V2I_PP_ICTRL = "0b11111", # Hard coded in all reference files
|
||||
p_IPI_CMPN = "0b0011", # Hard coded in all reference files
|
||||
|
||||
p_V2I_1V_EN = "ENABLED", # Enabled = 1V (Default in references, but not the primitive), Disabled = 0.9V
|
||||
p_V2I_KVCO_SEL = "60", # if (VOLTAGE == 0.9V) 85 else 60
|
||||
p_KP_VCO = "0b00011", # if (VOLTAGE == 0.9V) 0b11001 else 0b00011
|
||||
|
||||
p_PLLPD_N = "USED",
|
||||
p_REF_INTEGER_MODE = "ENABLED", # Ref manual has a discrepency so lets always set this value just in case
|
||||
p_REF_MMD_DIG = "1", # Divider for the input clock, ie 'M'
|
||||
|
||||
i_PLLRESET = self.reset,
|
||||
i_REFCK = self.clkin,
|
||||
o_LOCK = self.locked,
|
||||
|
||||
# Use CLKOS5 & divider for feedback
|
||||
p_SEL_FBK = "FBKCLK5",
|
||||
p_ENCLK_CLKOS5 = "ENABLED",
|
||||
p_DIVF = str(config["clkfb_div"]-1), # str(Actual value - 1)
|
||||
p_DELF = str(config["clkfb_div"]-1),
|
||||
p_CLKMUX_FB = "CMUX_CLKOS5",
|
||||
i_FBKCK = clkfb,
|
||||
o_CLKOS5 = clkfb,
|
||||
|
||||
# Set feedback divider to 1
|
||||
p_FBK_INTEGER_MODE = "ENABLED",
|
||||
p_FBK_MASK = "0b00000000",
|
||||
p_FBK_MMD_DIG = "1",
|
||||
)
|
||||
|
||||
analog_params = self.calculate_analog_parameters(self.clkin_freq, config["clkfb_div"])
|
||||
self.params.update(analog_params)
|
||||
n_to_l = {0: "P", 1: "S", 2: "S2", 3:"S3", 4:"S4"}
|
||||
|
||||
for n, (clk, f, p, m) in sorted(self.clkouts.items()):
|
||||
div = config["clko{}_div".format(n)]
|
||||
phase = int((1+p/360) * div)
|
||||
letter = chr(n+65)
|
||||
self.params["p_ENCLK_CLKO{}".format(n_to_l[n])] = "ENABLED"
|
||||
self.params["p_DIV{}".format(letter)] = str(div-1)
|
||||
self.params["p_PHI{}".format(letter)] = "0"
|
||||
self.params["p_DEL{}".format(letter)] = str(phase - 1)
|
||||
self.params["o_CLKO{}".format(n_to_l[n])] = clk
|
||||
|
||||
# In theory this really shouldn't be necessary, in practice
|
||||
# the tooling seems to have suspicous clock latency values
|
||||
# on generated clocks that are causing timing problems and Lattice
|
||||
# hasn't responded to my support requests on the matter.
|
||||
if self.platform and self.create_output_port_clocks:
|
||||
self.platform.add_platform_command("create_clock -period {} -name {} [get_pins {}.PLL_inst/CLKO{}]".format(str(1/f*1e9), self.name + "_" + n_to_l[n],self.name, n_to_l[n]))
|
||||
|
||||
if self.platform and self.create_output_port_clocks:
|
||||
i = 0
|
||||
self.specials += Instance("PLL", name = self.name, **self.params)
|
||||
|
||||
# The gist of calculating the analog parameters is to run through all the
|
||||
# permutations of the parameters and find the optimum set of values based
|
||||
# on the transfer function of the PLL loop filter. There are constraints on
|
||||
# on a few specific parameters, the open loop transfer function, and the closed loop
|
||||
# transfer function. An optimal solution is chosen based on the bandwidth
|
||||
# of the response relative to the input reference frequency of the PLL.
|
||||
|
||||
# Later revs of the Lattice calculator BW_FACTOR is set to 10, may need to change it
|
||||
def calc_optimal_params(self, fref, fbkdiv, M = 1, BW_FACTOR = 5):
|
||||
print("Calculating Analog Paramters for a reference freqeuncy of " + str(fref*1e-6) +
|
||||
" Mhz, feedback div " + str(fbkdiv) + ", and input div " + str(M) + "."
|
||||
)
|
||||
|
||||
best_params = None
|
||||
best_3db = 0
|
||||
|
||||
for params in self.transfer_func_coefficients:
|
||||
closed_loop_peak = self.closed_loop_peak(fbkdiv, params)
|
||||
if (closed_loop_peak["peak"] < 0.8 or
|
||||
closed_loop_peak["peak"] > 1.35):
|
||||
continue
|
||||
|
||||
open_loop_crossing = self.open_loop_crossing(fbkdiv, params)
|
||||
if open_loop_crossing["phase"] <= 45:
|
||||
continue
|
||||
|
||||
closed_loop_3db = self.closed_loop_3db(fbkdiv, params)
|
||||
bw_factor = fref*1e6 / M / closed_loop_3db["f"]
|
||||
if bw_factor < BW_FACTOR:
|
||||
continue
|
||||
|
||||
if best_3db < closed_loop_3db["f"]:
|
||||
best_3db = closed_loop_3db["f"]
|
||||
best_params = params
|
||||
|
||||
print("Done calculating analog parameters:")
|
||||
HDL_params = self.numerical_params_to_HDL_params(best_params)
|
||||
pprint.pprint(HDL_params)
|
||||
|
||||
return HDL_params
|
||||
|
||||
|
||||
def numerical_params_to_HDL_params(self, params):
|
||||
IPP_SEL_LUT = {1: 1, 2: 3, 3: 7, 4: 15}
|
||||
ret = {
|
||||
"CRIPPLE": str(int(params.CRIPPLE / 1e-12)) + "P",
|
||||
"CSET": str(int((params.CSET / 4e-12)*4)) + "P",
|
||||
"V2I_PP_RES": "{0:g}".format(params.V2I_PP_RES/1e3).replace(".","P") + "K",
|
||||
"IPP_CTRL": "0b{0:04b}".format(int(params.IPP_CTRL / 1e-6 + 3)),
|
||||
"IPI_CMP": "0b{0:04b}".format(int(params.IPI_CMP / .5e-6)),
|
||||
"BW_CTL_BIAS": "0b{0:04b}".format(params.BW_CTL_BIAS),
|
||||
"IPP_SEL": "0b{0:04b}".format(IPP_SEL_LUT[params.IPP_SEL]),
|
||||
}
|
||||
|
||||
return ret
|
||||
|
||||
def calc_valid_io_i2(self):
|
||||
# Valid permutations of IPP_CTRL, BW_CTL_BIAS, IPP_SEL, and IPI_CMP paramters are constrained
|
||||
# by the following equation so we can narrow the problem space by calculating the
|
||||
# them early in the process.
|
||||
# ip = 5.0/3 * ipp_ctrl*bw_ctl_bias*ipp_sel
|
||||
# ip/ipi_cmp == 50 +- 1e-4
|
||||
|
||||
self.valid_io_i2_permutations = []
|
||||
|
||||
# List out the valid values of each parameter
|
||||
IPP_CTRL_VALUES = range(1,4+1)
|
||||
IPP_CTRL_UNITS = 1e-6
|
||||
IPP_CTRL_VALUES = [element * IPP_CTRL_UNITS for element in IPP_CTRL_VALUES]
|
||||
BW_CTL_BIAS_VALUES = range(1,15+1)
|
||||
IPP_SEL_VALUES = range(1,4+1)
|
||||
IPI_CMP_VALUES = range(1,15+1)
|
||||
IPI_CMP_UNITS = 0.5e-6
|
||||
IPI_CMP_VALUES = [element * IPI_CMP_UNITS for element in IPI_CMP_VALUES]
|
||||
|
||||
for IPP_CTRL in IPP_CTRL_VALUES:
|
||||
for BW_CTL_BIAS in BW_CTL_BIAS_VALUES:
|
||||
for IPP_SEL in IPP_SEL_VALUES:
|
||||
for IPI_CMP in IPI_CMP_VALUES:
|
||||
is_valid_io_i2 = self.is_valid_io_i2(IPP_CTRL, BW_CTL_BIAS, IPP_SEL, IPI_CMP)
|
||||
if is_valid_io_i2 and self.is_unique_io(is_valid_io_i2['io']):
|
||||
self.valid_io_i2_permutations.append( io_i2(
|
||||
is_valid_io_i2['io'], is_valid_io_i2['i2'],
|
||||
IPP_CTRL, BW_CTL_BIAS, IPP_SEL
|
||||
) )
|
||||
|
||||
def is_unique_io(self, io):
|
||||
return not any(x.io == io for x in self.valid_io_i2_permutations)
|
||||
|
||||
def is_valid_io_i2(self, IPP_CTRL, BW_CTL_BIAS, IPP_SEL, IPI_CMP):
|
||||
tolerance = 1e-4
|
||||
ip = 5.0/3.0 * IPP_CTRL * BW_CTL_BIAS * IPP_SEL
|
||||
i2 = IPI_CMP
|
||||
if abs(ip/i2-50) < tolerance:
|
||||
return {'io':ip,'i2':i2}
|
||||
else:
|
||||
return False
|
||||
|
||||
def calc_tf_coefficients(self):
|
||||
# Take the permutations of the various analog parameters
|
||||
# then precalculate the coefficients of the transfer function.
|
||||
# During the final calculations sub in the feedback divisor
|
||||
# to get the final transfer functions.
|
||||
|
||||
# (ABF+EC)s^2 + (A(F(G+1)+B) + ED)s + A(G+1) C1s^s + C2s + C3
|
||||
# tf = -------------------------------------------- = --------------------------
|
||||
# ns^2(CFs^2 + (DF+C)s + D) ns^2(C4s^2 + C5s + C6)
|
||||
|
||||
# A = i2*g3*ki
|
||||
# B = r1*c3
|
||||
# C = B*c2
|
||||
# D = c2+c3
|
||||
# E = io*ki*k1
|
||||
# F = r*cs
|
||||
# G = k3
|
||||
# n = total divisor of the feedback signal (output + N)
|
||||
|
||||
# Constants
|
||||
c3 = 20e-12
|
||||
g3 = 0.2952e-3
|
||||
k1 = 6
|
||||
k3 = 100
|
||||
ki = 508e9
|
||||
r1 = 9.8e6
|
||||
B = r1*c3
|
||||
|
||||
# PLL Parameters
|
||||
CSET_VALUES = range(2,17+1)
|
||||
CSET_UNITS = 4e-12
|
||||
CSET_VALUES = [element * CSET_UNITS for element in CSET_VALUES]
|
||||
CRIPPLE_VALUES = [1, 3, 5, 7, 9, 11, 13, 15]
|
||||
CRIPPLE_UNITS = 1e-12
|
||||
CRIPPLE_VALUES = [element * CRIPPLE_UNITS for element in CRIPPLE_VALUES]
|
||||
V2I_PP_RES_VALUES = [9000, 9300, 9700, 10000, 10300, 10700, 11000, 11300]
|
||||
|
||||
self.transfer_func_coefficients = []
|
||||
|
||||
# Run through all the permutations and cache it all
|
||||
for io_i2 in self.valid_io_i2_permutations:
|
||||
for CSET in CSET_VALUES:
|
||||
for CRIPPLE in CRIPPLE_VALUES:
|
||||
for V2I_PP_RES in V2I_PP_RES_VALUES:
|
||||
A = io_i2.i2*g3*ki
|
||||
B = r1*c3
|
||||
C = B*CSET
|
||||
D = CSET+c3
|
||||
E = io_i2.io*ki*k1
|
||||
F = V2I_PP_RES*CRIPPLE
|
||||
G = k3
|
||||
|
||||
self.transfer_func_coefficients.append( nx_pll_param_permutation(
|
||||
A*B*F+E*C, # C1
|
||||
A*(F*(G+1)+B)+E*D, # C2
|
||||
A*(G+1), # C3
|
||||
C*F, # C4
|
||||
D*F+C, # C5
|
||||
D, # C6
|
||||
io_i2.IPP_CTRL, io_i2.BW_CTL_BIAS, io_i2.IPP_SEL,
|
||||
CSET, CRIPPLE, V2I_PP_RES, io_i2.i2
|
||||
))
|
||||
|
||||
def calc_tf(self, n, s, params):
|
||||
return ( (params.C1 * s ** 2 + params.C2 * s + params.C3) /
|
||||
( n * s ** 2 * (params.C4 * s ** 2 + params.C5 * s + params.C6) ) )
|
||||
|
||||
def closed_loop_peak(self, fbkdiv, params):
|
||||
f = 1e6
|
||||
step = 1.1
|
||||
step_divs = 0
|
||||
|
||||
peak_value = -99
|
||||
peak_f = 0
|
||||
|
||||
last_value = -99
|
||||
|
||||
while f < 1e9:
|
||||
s = 1j * 2 * pi * f
|
||||
tf_value = self.calc_tf(fbkdiv, s, params)
|
||||
this_result = 20*log10(abs(tf_value/(1+tf_value)))
|
||||
if this_result > peak_value:
|
||||
peak_value = this_result
|
||||
peak_f = f
|
||||
|
||||
if this_result < last_value and step_divs < 5:
|
||||
f = f/(step**2)
|
||||
step = (step - 1) * .5 + 1
|
||||
step_divs = step_divs + 1
|
||||
elif this_result < last_value and step_divs == 5:
|
||||
break
|
||||
else:
|
||||
last_value = this_result
|
||||
f = f * step
|
||||
|
||||
return {"peak":peak_value, "peak_freq":peak_f}
|
||||
|
||||
def closed_loop_3db(self, fbkdiv, params):
|
||||
f = 1e6
|
||||
step = 1.1
|
||||
step_divs = 0
|
||||
|
||||
last_f = 1
|
||||
|
||||
while f < 1e9:
|
||||
s = 1j * 2 * pi * f
|
||||
tf_value = self.calc_tf(fbkdiv, s, params)
|
||||
this_result = 20*log10(abs(tf_value/(1+tf_value)))
|
||||
|
||||
if (this_result+3) < 0 and step_divs < 5:
|
||||
f = last_f
|
||||
step = (step - 1) * .5 + 1
|
||||
step_divs = step_divs + 1
|
||||
elif (this_result+3) < 0 and step_divs == 5:
|
||||
break
|
||||
else:
|
||||
last_f = f
|
||||
f = f * step
|
||||
|
||||
return {"f":last_f}
|
||||
|
||||
def open_loop_crossing(self, fbkdiv, params):
|
||||
f = 1e6
|
||||
step = 1.1
|
||||
step_divs = 0
|
||||
|
||||
last_f = 1
|
||||
last_tf = 0
|
||||
|
||||
while f < 1e9:
|
||||
s = 1j * 2 * pi * f
|
||||
tf_value = self.calc_tf(fbkdiv, s, params)
|
||||
this_result = 20*log10(abs(tf_value))
|
||||
|
||||
if this_result < 0 and step_divs < 5:
|
||||
f = last_f
|
||||
step = (step - 1) * .5 + 1
|
||||
step_divs = step_divs + 1
|
||||
elif this_result < 0 and step_divs == 5:
|
||||
break
|
||||
else:
|
||||
last_f = f
|
||||
last_tf = tf_value
|
||||
f = f * step
|
||||
|
||||
return {"f":last_f, "phase":phase(-last_tf)*180/pi}
|
Loading…
Reference in New Issue