mirror of
https://github.com/saymrwulf/uhd.git
synced 2026-05-15 21:01:26 +00:00
304 lines
12 KiB
Python
304 lines
12 KiB
Python
#
|
|
# Copyright 2021-2022 Ettus Research, a National Instruments Company
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
#
|
|
"""
|
|
X4x0 Motherboard Clocking Control
|
|
|
|
This module contains the X4xxClockCtrl class, which is the worker bee for the
|
|
X4xxClockManager class.
|
|
|
|
The code in this module controls the RPLL and SPLL as well as some of the muxing.
|
|
The eCPRI PLL is controlled from the ClockingAuxBrdControl class.
|
|
"""
|
|
|
|
from usrp_mpm import lib # Pulls in everything from C++-land
|
|
from usrp_mpm.sys_utils import i2c_dev
|
|
from usrp_mpm.sys_utils.gpio import Gpio
|
|
from usrp_mpm.sys_utils.udev import dt_symbol_get_spidev
|
|
from usrp_mpm.periph_manager.x4xx_periphs import MboardRegsControl
|
|
from usrp_mpm.periph_manager.x4xx_sample_pll import LMK04832X4xx
|
|
from usrp_mpm.periph_manager.x4xx_reference_pll import LMK03328X4xx
|
|
from usrp_mpm.periph_manager.x4xx_clock_types import \
|
|
RpllRefSel, RpllBrcSrcSel, BrcSource
|
|
|
|
X400_RPLL_I2C_LABEL = 'rpll_i2c'
|
|
|
|
class X4xxClockCtrl:
|
|
"""
|
|
X4x0 Clocking Control
|
|
|
|
This class controls various parts of the X4xx clocking:
|
|
- Sample PLL (LMK04832)
|
|
- Reference PLL (LMK03328)
|
|
- Base Reference Clock source selection (via GPIO)
|
|
- Clocking-related motherboard registers
|
|
- Clocking-related settings on the motherboard CPLD
|
|
|
|
This class exposes APIs to control all of these components. The sequencing
|
|
logic etc. is encoded in the X4xxClockManager class.
|
|
"""
|
|
def __init__(self, cpld_control, log):
|
|
# Store parent objects
|
|
self.log = log.getChild("ClkMgr")
|
|
self._cpld_control = cpld_control
|
|
# Preallocate other objects to satisfy linter
|
|
self.mboard_regs_control = None # Needs to be set later from outside!
|
|
self._sample_pll = None
|
|
self._reference_pll = None
|
|
self._base_ref_clk_select = None
|
|
# Init peripherals
|
|
self._init_clk_peripherals()
|
|
|
|
###########################################################################
|
|
# Initialization code
|
|
###########################################################################
|
|
def _init_clk_peripherals(self):
|
|
"""
|
|
Initialize objects for peripherals owned by this class. Most importantly,
|
|
this includes the RPLL and SPLL control classes.
|
|
"""
|
|
# Create SPI and I2C interfaces to the LMK registers
|
|
spll_spi_node = dt_symbol_get_spidev('spll')
|
|
sample_lmk_regs_iface = lib.spi.make_spidev_regs_iface(
|
|
spll_spi_node,
|
|
1000000, # Speed (Hz)
|
|
0x3, # SPI mode
|
|
8, # Addr shift
|
|
0, # Data shift
|
|
1<<23, # Read flag
|
|
0, # Write flag
|
|
)
|
|
# Initialize I2C connection to RPLL
|
|
rpll_i2c_bus = i2c_dev.dt_symbol_get_i2c_bus(X400_RPLL_I2C_LABEL)
|
|
if rpll_i2c_bus is None:
|
|
raise RuntimeError("RPLL I2C bus could not be found")
|
|
self.log.trace("Using RPLL I2C bus: %s", str(rpll_i2c_bus))
|
|
reference_lmk_regs_iface = lib.i2c.make_i2cdev_regs_iface(
|
|
rpll_i2c_bus,
|
|
0x54, # addr
|
|
False, # ten_bit_addr
|
|
100, # timeout_ms
|
|
1 # reg_addr_size
|
|
)
|
|
self._sample_pll = LMK04832X4xx(sample_lmk_regs_iface, self.log)
|
|
self._reference_pll = LMK03328X4xx(reference_lmk_regs_iface, self.log)
|
|
# Init BRC select GPIO control
|
|
self._base_ref_clk_select = Gpio('BASE-REFERENCE-CLOCK-SELECT', Gpio.OUTPUT, 1)
|
|
|
|
###########################################################################
|
|
# Low-level controls
|
|
###########################################################################
|
|
def reset_clock(self, value, clock_to_reset):
|
|
"""
|
|
Helper API to reset or re-enable a clock.
|
|
"""
|
|
if clock_to_reset == 'cpld':
|
|
self._cpld_control.enable_pll_ref_clk(enable=not value)
|
|
if clock_to_reset == 'spll':
|
|
self._sample_pll.reset(value, hard=True)
|
|
if clock_to_reset == 'rpll':
|
|
self._reference_pll.reset(value, hard=True)
|
|
|
|
def config_rpll(self,
|
|
internal_brc_source: RpllRefSel,
|
|
ref_rate: float,
|
|
internal_brc_rate: float,
|
|
usr_clk_rate: float):
|
|
"""
|
|
Configures the LMK03328 to generate the desired MGT reference clocks
|
|
and internal BRC rate.
|
|
|
|
Currently, the MGT protocol selection is not supported, but a custom
|
|
usr_clk_rate can be generated from PLL1.
|
|
|
|
internal_brc_source - the reference source which drives the RPLL
|
|
(primary or secondary). Primary is the clocking
|
|
aux card, secondary is the 100 MHz oscillator.
|
|
ref_rate - The frequency which is being provided as reference at the
|
|
input corresponding to internal_brc_source.
|
|
internal_brc_rate - the rate to output as the BRC
|
|
usr_clk_rate - the custom clock rate to generate from PLL1
|
|
"""
|
|
assert internal_brc_source in RpllRefSel
|
|
assert internal_brc_source == RpllRefSel.SECONDARY or \
|
|
ref_rate is not None
|
|
|
|
# If the desired rate matches the rate of the primary reference source,
|
|
# directly passthrough that reference source
|
|
if internal_brc_source == RpllRefSel.PRIMARY and \
|
|
internal_brc_rate == ref_rate:
|
|
brc_select = RpllBrcSrcSel.BYPASS
|
|
else:
|
|
brc_select = RpllBrcSrcSel.PLL
|
|
|
|
# TODO: Questionable if we always need to call init() here
|
|
self._reference_pll.init()
|
|
self._reference_pll.config(
|
|
internal_brc_source,
|
|
ref_rate,
|
|
internal_brc_rate,
|
|
usr_clk_rate,
|
|
brc_select)
|
|
|
|
def config_spll(self, pll_settings: dict):
|
|
"""
|
|
Configures the SPLL for the specified master clock rate.
|
|
"""
|
|
# TODO: Questionable if we always need to call init() here
|
|
self._sample_pll.init()
|
|
self._sample_pll.config(pll_settings)
|
|
|
|
def get_spll_freq(self):
|
|
"""
|
|
Returns the output frequency setting of the SPLL that is routed to the
|
|
ADCs/DACs.
|
|
"""
|
|
return self._sample_pll.output_freq
|
|
|
|
def get_prc_rate(self):
|
|
"""
|
|
Returns the rate of the PLL Reference Clock (PRC) which is
|
|
routed to the daughterboards (as an LO reference and CPLD clock) and to
|
|
the FPGA (used to generate data_clk and rfdc_clk from the MMCM).
|
|
Note: The PRC will change if the sample clock frequency is modified.
|
|
"""
|
|
return self._sample_pll.prc_freq
|
|
|
|
def select_brc_source(self, brc_src: BrcSource):
|
|
"""
|
|
Configures the GaAs switch (U10/U11) that controls which BRC is used.
|
|
|
|
"""
|
|
if brc_src == BrcSource.RPLL:
|
|
self._base_ref_clk_select.set(1)
|
|
elif brc_src == BrcSource.CLK_AUX:
|
|
self._base_ref_clk_select.set(0)
|
|
else:
|
|
raise RuntimeError("Invalid BRC source.")
|
|
|
|
def sync_spll_clocks(self, pps_source: str, ref_clock_freq: float):
|
|
"""
|
|
Synchronize base reference clock (BRC) and PLL reference clock (PRC)
|
|
at start of PPS trigger.
|
|
|
|
Uses the LMK 04832 pll1_r_divider_sync to synchronize BRC with PRC.
|
|
This sync method uses a callback to actual trigger the sync. Before
|
|
the trigger is pulled (CLOCK_CTRL_PLL_SYNC_TRIGGER) PPS source is
|
|
configured base on current reference clock and pps_source. After sync
|
|
trigger the method waits for 1sec for the CLOCK_CTRL_PLL_SYNC_DONE
|
|
to go high.
|
|
|
|
:param pps_source: select whether internal ("internal_pps") or external
|
|
("external_pps") PPS should be used. This parameter
|
|
is taken into account when the current clock source
|
|
is external. If the current clock source is set to
|
|
internal then this parameter is not taken into
|
|
account.
|
|
:return: success state of sync call
|
|
:raises RuntimeError: for invalid combinations of reference clock and
|
|
pps_source
|
|
"""
|
|
def select_pps():
|
|
"""
|
|
Select PPS source based on current clock source and pps_source.
|
|
|
|
This returns the bits for the motherboard CLOCK_CTRL register that
|
|
control the PPS source.
|
|
"""
|
|
EXT_PPS = "external_pps"
|
|
INT_PPS = "internal_pps"
|
|
PPS_SOURCES = (EXT_PPS, INT_PPS)
|
|
assert pps_source in PPS_SOURCES, \
|
|
"%s not in %s" % (pps_source, PPS_SOURCES)
|
|
|
|
supported_configs = {
|
|
(10E6, EXT_PPS): MboardRegsControl.CLOCK_CTRL_PPS_EXT,
|
|
(10E6, INT_PPS): MboardRegsControl.CLOCK_CTRL_PPS_INT_10MHz,
|
|
(25E6, INT_PPS): MboardRegsControl.CLOCK_CTRL_PPS_INT_25MHz
|
|
}
|
|
|
|
config = (ref_clock_freq, pps_source)
|
|
if config not in supported_configs:
|
|
raise RuntimeError("Unsupported combination of reference clock "
|
|
"(%.2E) and PPS source (%s) for PPS sync." %
|
|
config)
|
|
return supported_configs[config]
|
|
|
|
return self._sample_pll.pll1_r_divider_sync(
|
|
lambda: self.mboard_regs_control.pll_sync_trigger(select_pps()))
|
|
|
|
def configure_pps_forwarding(
|
|
self,
|
|
tk_idx: int,
|
|
enable: bool,
|
|
radio_clock_rate: float,
|
|
delay: float = 1.0):
|
|
"""
|
|
Configures the PPS forwarding to the sample clock domain (master
|
|
clock rate). This function assumes sync_spll_clocks function has
|
|
already been executed.
|
|
|
|
:param tk_idx: Which timekeeper to configure.
|
|
:param enable: Boolean to choose whether PPS is forwarded to the
|
|
sample clock domain.
|
|
|
|
:param delay: Delay in seconds from the PPS rising edge to the edge
|
|
occurence in the application. This value has to be in
|
|
range 0 < x <= 1. In order to forward the PPS signal
|
|
from base reference clock to sample clock an aligned
|
|
rising edge of the clock is required. This can be
|
|
created by the sync_spll_clocks function. Based on the
|
|
greatest common divisor of the two clock rates there
|
|
are multiple occurences of an aligned edge each second.
|
|
One of these aligned edges has to be chosen for the
|
|
PPS forwarding by setting this parameter.
|
|
|
|
:return: None, Exception on error
|
|
"""
|
|
# FIXME MULTI_RATE enable multiple MCRs / timekeepers, use tk_idx
|
|
return self.mboard_regs_control.configure_pps_forwarding(
|
|
enable, tk_idx, radio_clock_rate, self.get_prc_rate(), delay)
|
|
|
|
|
|
def get_ref_locked(self):
|
|
"""
|
|
Return lock status both RPLL and SPLL.
|
|
"""
|
|
ref_pll_status = self._reference_pll.get_status()
|
|
sample_pll_status = self._sample_pll.get_status()
|
|
self.log.trace(
|
|
f"RPLL1 lock: {ref_pll_status['PLL1 lock']} "
|
|
f"RPLL2 lock: {ref_pll_status['PLL2 lock']} "
|
|
f"SPLL1 lock: {sample_pll_status['PLL1 lock']} "
|
|
f"SPLL2 lock: {sample_pll_status['PLL2 lock']}")
|
|
return all([
|
|
ref_pll_status['PLL1 lock'],
|
|
ref_pll_status['PLL2 lock'],
|
|
sample_pll_status['PLL1 lock'],
|
|
sample_pll_status['PLL2 lock'],
|
|
])
|
|
|
|
###########################################################################
|
|
# Top-level BIST APIs
|
|
#
|
|
# These calls will be available as MPM calls. They are only needed by BIST.
|
|
###########################################################################
|
|
def enable_ecpri_clocks(self, enable=True, clock='both'):
|
|
"""
|
|
Enable or disable the export of FABRIC and GTY_RCV eCPRI
|
|
clocks. Main use case until we support eCPRI is manufacturing
|
|
testing.
|
|
"""
|
|
self.mboard_regs_control.enable_ecpri_clocks(enable, clock)
|
|
|
|
def get_fpga_aux_ref_freq(self):
|
|
"""
|
|
Return the tick count of an FPGA counter which measures the width of
|
|
the PPS signal on the FPGA_AUX_REF FPGA input using a 40 MHz clock.
|
|
Main use case until we support eCPRI is manufacturing testing.
|
|
A return value of 0 indicates absence of a valid PPS signal on the
|
|
FPGA_AUX_REF line.
|
|
"""
|
|
return self.mboard_regs_control.get_fpga_aux_ref_freq()
|