diff options
Diffstat (limited to 'mpm/python/usrp_mpm/periph_manager/x4xx_clk_mgr.py')
-rw-r--r-- | mpm/python/usrp_mpm/periph_manager/x4xx_clk_mgr.py | 780 |
1 files changed, 780 insertions, 0 deletions
diff --git a/mpm/python/usrp_mpm/periph_manager/x4xx_clk_mgr.py b/mpm/python/usrp_mpm/periph_manager/x4xx_clk_mgr.py new file mode 100644 index 000000000..bf845dd65 --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/x4xx_clk_mgr.py @@ -0,0 +1,780 @@ +# +# Copyright 2021 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +X400 Clocking Management + +This module handles the analog clocks on the X4x0 motherboard. The clocking +architecture of the motherboard is spread out between a clocking auxiliary board, +which contains a GPS-displicined OCXO, but also connects an external reference +to the motherboard. It also houses a PLL for deriving a clock from the network +(eCPRI). The clocking aux board has its own control class (ClockingAuxBrdControl) +which also contains controls for the eCPRI PLL. + +The motherboard itself has two main PLLs for clocking purposes: The Sample PLL +(also SPLL) is used to create all clocks used for RF-related purposes. It creates +the sample clock (a very fast clock, ~3 GHz) and the PLL reference clock (PRC) +which is used as a timebase for the daughterboard CPLD and a reference for the +LO synthesizers (50-64 MHz). + +Its input is the base reference clock (BRC). This clock comes either from the +clocking aux board, which in turn can provide a reference from the OCXO (with or +without GPS-disciplining) or from the external reference input SMA port. +The BRC is typically 10-25 MHz. + +The BRC can also come from the reference PLL (RPLL), when the clock source is +set to 'mboard'. The RPLL produces clocks that are consumed by the GTY banks +(for Ethernet and Aurora), but it can also generate a reference clock for +the SPLL. By default, its reference is a fixed 100 MHz clock, but it can also be +driven by the eCPRI PLL, which itself can be driven by a clock from the GTY banks, +which is the case if the clock source is set to 'nsync'. + +The master clock rate (MCR) is not actually controlled in this module, but it +depends on the sample clock rate. It also depends on the RFDC settings, so it is +controlled in x4xx.py, which has access to both RFDC and X4xxClockMgr. + +Block diagram (for more details, see the schematic):: + + ┌────────────────────────────────────────────────────────┐ + │ Clocking Aux Board │ + │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ + │ │ GPSDO ├─> OCXO │ │External│ │eCPRI/ │ │ + │ │ │ │ │ │ │ │nsync │ │ + │ └────────┘ └────┬───┘ └───┬────┘ └────────┘ │ + │ │ │ │ + │ │ │ │ + │ ┌───────────v─────────v───┐ ┌───────────┐ │ + │ │ │ │eCPRI PLL │ │ + │ └┐ MUX ┌┘ │LMK05318 │ │ + │ └─┐ ┌─┘ │ │ │ + │ └─┬─────────────────┘ └──┬────────┘ │ + │ │ │ │ + └───────────┼───────────────────────────┼────────────────┘ + │ │ + │ ┌─────────────┐ │ + ┌──v──v┐ │ │ + │ MUX │ │ │ ┌───── 100 MHz + └──┬───┘ │ │ │ + │Base Ref. Clock │ │ │ + ┌───────v───────┐ │ ┌───────v──v──┐ + │ Sample PLL │ └──┤Reference PLL│ + │ LMK04832 │ │LMK03328 │ + │ │ │ │ + │ │ │ │ + └──┬─────────┬──┘ └────┬────────┘ + │ │ │ + │ │ │ + v v v + Sample PLL Reference GTY Banks + Clock Clock + + +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. +Most importantly, this class controls the sequencing of configuring clocks. This +means it won't only switch muxes and configure PLLs, but will also do things in +the right order, and put components in reset where necessary. +For this reason, it requires callbacks to reset RFDC and daughterboard clocks. +""" + +from enum import Enum +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_clk_aux import ClockingAuxBrdControl +from usrp_mpm.mpmutils import poll_with_timeout +from usrp_mpm.rpc_server import no_rpc + +# this is not the frequency out of the GPSDO(GPS Lite, 20MHz) itself but +# the GPSDO on the CLKAUX board is used to fine tune the OCXO via EFC +# which is running at 10MHz +X400_GPSDO_OCXO_CLOCK_FREQ = 10e6 +X400_RPLL_I2C_LABEL = 'rpll_i2c' +X400_DEFAULT_RPLL_REF_SOURCE = '100M_reliable_clk' +X400_DEFAULT_MGT_CLOCK_RATE = 156.25e6 +X400_DEFAULT_INT_CLOCK_FREQ = 25e6 + +class X4xxClockMgr: + """ + X4x0 Clocking Manager + + The clocking subsystem of X4x0 is very complex. This class is designed to + capture all clocking-related logic specific to the X4x0. + + This class controls clock and time sources. + """ + CLOCK_SOURCE_MBOARD = "mboard" + CLOCK_SOURCE_INTERNAL = ClockingAuxBrdControl.SOURCE_INTERNAL + CLOCK_SOURCE_EXTERNAL = ClockingAuxBrdControl.SOURCE_EXTERNAL + CLOCK_SOURCE_GPSDO = ClockingAuxBrdControl.SOURCE_GPSDO + CLOCK_SOURCE_NSYNC = ClockingAuxBrdControl.SOURCE_NSYNC + + TIME_SOURCE_INTERNAL = "internal" + TIME_SOURCE_EXTERNAL = "external" + TIME_SOURCE_GPSDO = "gpsdo" + TIME_SOURCE_QSFP0 = "qsfp0" + + # All valid sync_sources for X4xx in the form of (clock_source, time_source) + valid_sync_sources = { + (CLOCK_SOURCE_MBOARD, TIME_SOURCE_INTERNAL), + (CLOCK_SOURCE_INTERNAL, TIME_SOURCE_INTERNAL), + (CLOCK_SOURCE_EXTERNAL, TIME_SOURCE_EXTERNAL), + (CLOCK_SOURCE_EXTERNAL, TIME_SOURCE_INTERNAL), + (CLOCK_SOURCE_GPSDO, TIME_SOURCE_GPSDO), + (CLOCK_SOURCE_GPSDO, TIME_SOURCE_INTERNAL), + (CLOCK_SOURCE_NSYNC, TIME_SOURCE_INTERNAL), + } + + class SetSyncRetVal(Enum): + OK = 'OK' + NOP = 'nop' + FAIL = 'failure' + + def __init__(self, + clock_source, + time_source, + ref_clock_freq, + sample_clock_freq, + is_legacy_mode, + clk_aux_board, + cpld_control, + log): + # Store parent objects + self.log = log.getChild("ClkMgr") + self._cpld_control = cpld_control + self._clocking_auxbrd = clk_aux_board + self._time_source = time_source + self._clock_source = clock_source + self._int_clock_freq = X400_DEFAULT_INT_CLOCK_FREQ + self._ext_clock_freq = ref_clock_freq + # Preallocate other objects to satisfy linter + self.mboard_regs_control = None + self._sample_pll = None + self._reference_pll = None + self._rpll_i2c_bus = None + self._base_ref_clk_select = None + self._set_reset_rfdc = lambda **kwargs: None + self._set_reset_db_clocks = lambda *args: None + self._rpll_reference_sources = {} + # Init peripherals + self._init_available_srcs() + self._init_clk_peripherals() + # Now initialize the clocks themselves + self._init_ref_clock_and_time( + clock_source, + ref_clock_freq, + sample_clock_freq, + is_legacy_mode, + ) + self._init_meas_clock() + self._cpld_control.enable_pll_ref_clk() + + ########################################################################### + # Initialization code + ########################################################################### + def _init_available_srcs(self): + """ + Initialize the available clock and time sources. + """ + has_gps = self._clocking_auxbrd and self._clocking_auxbrd.is_gps_supported() + self._avail_clk_sources = [self.CLOCK_SOURCE_MBOARD] + if self._clocking_auxbrd: + self._avail_clk_sources.extend([ + self.CLOCK_SOURCE_INTERNAL, + self.CLOCK_SOURCE_EXTERNAL]) + if self._clocking_auxbrd.is_nsync_supported(): + self._avail_clk_sources.append(self.CLOCK_SOURCE_NSYNC) + if has_gps: + self._avail_clk_sources.append(self.CLOCK_SOURCE_GPSDO) + self.log.trace(f"Available clock sources are: {self._avail_clk_sources}") + self._avail_time_sources = [ + self.TIME_SOURCE_INTERNAL, self.TIME_SOURCE_EXTERNAL, self.TIME_SOURCE_QSFP0] + if has_gps: + self._avail_time_sources.append(self.TIME_SOURCE_GPSDO) + self.log.trace("Available time sources are: {}".format(self._avail_time_sources)) + + 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 + self._rpll_i2c_bus = i2c_dev.dt_symbol_get_i2c_bus(X400_RPLL_I2C_LABEL) + if self._rpll_i2c_bus is None: + raise RuntimeError("RPLL I2C bus could not be found") + reference_lmk_regs_iface = lib.i2c.make_i2cdev_regs_iface( + self._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) + + def _init_ref_clock_and_time(self, + clock_source, + ref_clock_freq, + sample_clock_freq, + is_legacy_mode, + ): + """ + Initialize clock and time sources. After this function returns, the + reference signals going to the FPGA are valid. + + This is only called once, during __init__(). Calling it again will set + clocks to defaults, but is also redundant since clocks do not need to be + initialized twice. + """ + # A dictionary of tuples (source #, rate) corresponding to each + # available RPLL reference source. + # source # 1 => PRIREF source + # source # 2 => SECREF source + self._rpll_reference_sources = {X400_DEFAULT_RPLL_REF_SOURCE: (2, 100e6)} + reference_rates = [None, None] + for source, rate in self._rpll_reference_sources.values(): + reference_rates[source-1] = rate + self._reference_pll.reference_rates = reference_rates + # Now initializes and reconfigure all clocks. + # If clock_source and ref_clock_freq are not provided, they will not be changed. + # If any other parameters are not provided, they will be configured with + # default values. + self._reset_clocks(value=True, reset_list=['cpld']) + if clock_source is not None: + self._set_brc_source(clock_source) + if ref_clock_freq is not None: + self._set_ref_clock_freq(ref_clock_freq) + self._config_rpll( + X400_DEFAULT_MGT_CLOCK_RATE, + X400_DEFAULT_INT_CLOCK_FREQ, + X400_DEFAULT_RPLL_REF_SOURCE) + self._config_spll(sample_clock_freq, is_legacy_mode) + self._reset_clocks(value=False, reset_list=['cpld']) + + def _init_meas_clock(self): + """ + Initialize the TDC measurement clock. After this function returns, the + FPGA TDC meas_clock is valid. + """ + # This may or may not be used for X400. Leave as a place holder + self.log.debug("TDC measurement clock not yet implemented.") + + ########################################################################### + # Public APIs (that are not exposed as MPM calls) + ########################################################################### + @no_rpc + def set_rfdc_reset_cb(self, rfdc_reset_cb): + """ + Set reference to RFDC control. Ideally, we'd get that in __init__(), but + due to order of operations, it's not ready yet when we call that. + """ + self._set_reset_rfdc = rfdc_reset_cb + + @no_rpc + def set_dboard_reset_cb(self, db_reset_cb): + """ + Set reference to RFDC control. Ideally, we'd get that in __init__(), but + due to order of operations, it's not ready yet when we call that. + """ + self._set_reset_db_clocks = db_reset_cb + + @no_rpc + def unset_cbs(self): + """ + Removes any stored references to our owning X4xx class instance + """ + self._set_reset_rfdc = None + self._set_reset_db_clocks = None + + @no_rpc + def config_pps_to_timekeeper(self, master_clock_rate): + """ Configures the path from the PPS to the timekeeper""" + pps_source = "internal_pps" \ + if self._time_source == self.TIME_SOURCE_INTERNAL \ + else "external_pps" + self._sync_spll_clocks(pps_source) + self._configure_pps_forwarding(True, master_clock_rate) + + @no_rpc + def get_clock_sources(self): + """ + Lists all available clock sources. + """ + return self._avail_clk_sources + + @no_rpc + def get_time_sources(self): + " Returns list of valid time sources " + return self._avail_time_sources + + @no_rpc + def get_ref_clock_freq(self): + " Returns the currently active reference clock frequency (BRC) " + clock_source = self.get_clock_source() + if clock_source == self.CLOCK_SOURCE_MBOARD: + return self._int_clock_freq + if clock_source == self.CLOCK_SOURCE_GPSDO: + return X400_GPSDO_OCXO_CLOCK_FREQ + # clock_source == "external": + return self._ext_clock_freq + + @no_rpc + 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() + return all([ + ref_pll_status['PLL1 lock'], + ref_pll_status['PLL2 lock'], + sample_pll_status['PLL1 lock'], + sample_pll_status['PLL2 lock'], + ]) + + @no_rpc + def set_spll_rate(self, sample_clock_freq, is_legacy_mode): + """ + Safely set the output rate of the sample PLL. + + This will do the required resets. + """ + self._reset_clocks(value=True, reset_list=('rfdc', 'cpld', 'db_clock')) + self._config_spll(sample_clock_freq, is_legacy_mode) + self._reset_clocks(value=False, reset_list=('rfdc', 'cpld', 'db_clock')) + + @no_rpc + def set_sync_source(self, clock_source, time_source): + """ + Selects reference clock and PPS sources. Unconditionally re-applies the + time source to ensure continuity between the reference clock and time + rates. + Note that if we change the source such that the time source is changed + to 'external', then we need to also disable exporting the reference + clock (RefOut and PPS-In are the same SMA connector). + """ + assert (clock_source, time_source) in self.valid_sync_sources, \ + f'Clock and time source pair ({clock_source}, {time_source}) is ' \ + 'not a valid selection' + # Now see if we can keep the current settings, or if we need to run an + # update of sync sources: + if (clock_source == self._clock_source) and (time_source == self._time_source): + spll_status = self._sample_pll.get_status() + rpll_status = self._reference_pll.get_status() + if (spll_status['PLL1 lock'] and spll_status['PLL2 lock'] and + rpll_status['PLL1 lock'] and rpll_status['PLL2 lock']): + # Nothing change no need to do anything + self.log.trace("New sync source assignment matches " + "previous assignment. Ignoring update command.") + return self.SetSyncRetVal.NOP + self.log.debug( + "Although the clock source has not changed, some PLLs " + "are not locked. Setting clock source again...") + self.log.trace("- SPLL status: {}".format(spll_status)) + self.log.trace("- RPLL status: {}".format(rpll_status)) + # Start setting sync source + self.log.debug( + f"Setting sync source to time_source={time_source}, " + f"clock_source={clock_source}") + self._time_source = time_source + # Reset downstream clocks (excluding RPLL) + self._reset_clocks(value=True, reset_list=('db_clock', 'cpld', 'rfdc', 'spll')) + self._set_brc_source(clock_source) + return self.SetSyncRetVal.OK + + @no_rpc + def set_clock_source_out(self, enable=True): + """ + Allows routing the clock configured as source on the clk aux board to + the RefOut terminal. This only applies to internal, gpsdo and nsync. + """ + clock_source = self.get_clock_source() + if self.get_time_source() == self.TIME_SOURCE_EXTERNAL: + raise RuntimeError( + 'Cannot export clock when using external time reference!') + if clock_source not in self._clocking_auxbrd.VALID_CLK_EXPORTS: + raise RuntimeError(f"Invalid source to export: `{clock_source}'") + if self._clocking_auxbrd is None: + raise RuntimeError("No clocking aux board available") + return self._clocking_auxbrd.export_clock(enable) + + ########################################################################### + # 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 nsync_change_input_source(self, source): + """ + Switches the input reference source of the clkaux lmk (the "eCPRI PLL"). + + Valid options are: fabric_clk, gty_rcv_clk, and sec_ref. + + fabric_clk and gty_rcv_clk are clock sources from the mboard. + They are both inputs to the primary reference source of the clkaux lmk. + sec_ref is the default reference select for the clkaux lmk, it has + two inputs: Ref in or internal and GPS mode + + Only a public API for the BIST. + """ + assert source in ( + self._clocking_auxbrd.SOURCE_NSYNC_LMK_PRI_FABRIC_CLK, + self._clocking_auxbrd.SOURCE_NSYNC_LMK_PRI_GTY_RCV_CLK, + self._clocking_auxbrd.NSYNC_SEC_REF, + ) + if source == self._clocking_auxbrd.SOURCE_NSYNC_LMK_PRI_FABRIC_CLK: + self.enable_ecpri_clocks(True, 'fabric') + self._clocking_auxbrd.set_nsync_ref_select( + self._clocking_auxbrd.NSYNC_PRI_REF) + self._clocking_auxbrd.set_nsync_pri_ref_source(source) + elif source == self._clocking_auxbrd.SOURCE_NSYNC_LMK_PRI_GTY_RCV_CLK: + self.enable_ecpri_clocks(True, 'gty_rcv') + self._clocking_auxbrd.set_nsync_ref_select( + self._clocking_auxbrd.NSYNC_PRI_REF) + self._clocking_auxbrd.set_nsync_pri_ref_source(source) + else: + self._clocking_auxbrd.set_nsync_ref_select( + self._clocking_auxbrd.NSYNC_SEC_REF) + + def config_rpll_to_nsync(self): + """ + Configures the rpll to use the LMK28PRIRefClk output + by the clkaux LMK + """ + # LMK28PRIRefClk only available when nsync is source, as lmk + # is powered off otherwise + self.set_sync_source(clock_source='nsync', time_source=self._time_source) + + # Add LMK28PRIRefClk as an available RPLL reference source + # 1 => PRIREF source; source is output at 25 MHz + # TODO: enable out4 on LMK + previous_ref_rate = self._reference_pll.reference_rates[0] + self._rpll_reference_sources['clkaux_nsync_clk'] = (1, 25e6) + self._reference_pll.reference_rates[0] = 25e6 + self._config_rpll(X400_DEFAULT_MGT_CLOCK_RATE, + X400_DEFAULT_INT_CLOCK_FREQ, + 'clkaux_nsync_clk') + + # remove clkaux_nsync_clk as a valid reference source for later calls + # to _config_rpll(), it is only valid in this configuration + self._reference_pll.reference_rates[0] = previous_ref_rate + del self._rpll_reference_sources['clkaux_nsync_clk'] + + 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() + + ########################################################################### + # Top-level APIs + # + # These calls will be available as MPM calls + ########################################################################### + def get_clock_source(self): + " Return the currently selected clock source " + return self._clock_source + + def get_time_source(self): + " Return the currently selected time source " + return self._time_source + + def get_spll_freq(self): + """ Returns the output frequency setting of the SPLL """ + 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. + Note: The ref clock will change if the sample clock frequency + is modified. + """ + prc_clock_map = { + 2.94912e9: 61.44e6, + 3e9: 62.5e6, + # 3e9: 50e6, RF Legacy mode will be checked separately + 3.072e9: 64e6, + } + + # RF Legacy Mode always has a PRC rate of 50 MHz + if self._sample_pll.is_legacy_mode: + return 50e6 + # else: + return prc_clock_map[self.get_spll_freq()] + + def set_ref_clk_tuning_word(self, tuning_word, out_select=0): + """ + Set the tuning word for the clocking aux board DAC. This wull update the + tuning word used by the DAC. + """ + if self._clocking_auxbrd is not None: + self._clocking_auxbrd.config_dac(tuning_word, out_select) + else: + raise RuntimeError("No clocking aux board available") + + def get_ref_clk_tuning_word(self, out_select=0): + """ + Get the tuning word configured for the clocking aux board DAC. + """ + if self._clocking_auxbrd is None: + raise RuntimeError("No clocking aux board available") + return self._clocking_auxbrd.read_dac(out_select) + + def store_ref_clk_tuning_word(self, tuning_word): + """ + Store the given tuning word in the clocking aux board ID EEPROM. + """ + if self._clocking_auxbrd is not None: + self._clocking_auxbrd.store_tuning_word(tuning_word) + else: + raise RuntimeError("No clocking aux board available") + + def get_sync_sources(self): + """ + Enumerates permissible sync sources. + """ + return [{ + "time_source": time_source, + "clock_source": clock_source + } for (clock_source, time_source) in self.valid_sync_sources] + + + ########################################################################### + # Low-level controls + ########################################################################### + def _reset_clocks(self, value, reset_list): + """ + Shuts down all clocks downstream to upstream or clears reset on all + clocks upstream to downstream. Specify the list of clocks to reset in + reset_list. The order of clocks specified in the reset_list does not + affect the order in which the clocks are reset. + """ + if value: + self.log.trace("Reset clocks: {}".format(reset_list)) + if 'db_clock' in reset_list: + self._set_reset_db_clocks(value) + if 'cpld' in reset_list: + self._cpld_control.enable_pll_ref_clk(enable=False) + if 'rfdc' in reset_list: + self._set_reset_rfdc(reset=True) + if 'spll' in reset_list: + self._sample_pll.reset(value, hard=True) + if 'rpll' in reset_list: + self._reference_pll.reset(value, hard=True) + else: + self.log.trace("Bring clocks out of reset: {}".format(reset_list)) + if 'rpll' in reset_list: + self._reference_pll.reset(value, hard=True) + if 'spll' in reset_list: + self._sample_pll.reset(value, hard=True) + if 'rfdc' in reset_list: + self._set_reset_rfdc(reset=False) + if 'cpld' in reset_list: + self._cpld_control.enable_pll_ref_clk(enable=True) + if 'db_clock' in reset_list: + self._set_reset_db_clocks(value) + + def _config_rpll(self, usr_clk_rate, internal_brc_rate, internal_brc_source): + """ + 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. + + usr_clk_rate - the custom clock rate to generate from PLL1 + internal_brc_rate - the rate to output as the BRC + internal_brc_source - the reference source which drives the RPLL + """ + if internal_brc_source not in self._rpll_reference_sources: + self.log.error('Invalid internal BRC source of {} was selected.' + .format(internal_brc_source)) + raise RuntimeError('Invalid internal BRC source of {} was selected.' + .format(internal_brc_source)) + ref_select = self._rpll_reference_sources[internal_brc_source][0] + + # If the desired rate matches the rate of the primary reference source, + # directly passthrough that reference source + if internal_brc_rate == self._reference_pll.reference_rates[0]: + brc_select = 'bypass' + else: + brc_select = 'PLL' + self._reference_pll.init() + self._reference_pll.config( + ref_select, internal_brc_rate, usr_clk_rate, brc_select) + # The internal BRC rate will only change when _config_rpll is called + # with a new internal BRC rate + self._int_clock_freq = internal_brc_rate + + def _config_spll(self, sample_clock_freq, is_legacy_mode): + """ + Configures the SPLL for the specified master clock rate. + """ + self._sample_pll.init() + self._sample_pll.config(sample_clock_freq, self.get_ref_clock_freq(), + is_legacy_mode) + + def _set_brc_source(self, clock_source): + """ + Switches the Base Reference Clock Source between internal, external, + mboard, and gpsdo using the GPIO pin and clocking aux board control. + internal is a clock source internal to the clocking aux board, but + external to the motherboard. + Should not be called outside of set_sync_source or _init_ref_clock_and_time + without proper reset and reconfig of downstream clocks. + """ + if clock_source == self.CLOCK_SOURCE_MBOARD: + self._base_ref_clk_select.set(1) + if self._clocking_auxbrd: + self._clocking_auxbrd.export_clock(False) + else: + if self._clocking_auxbrd is None: + self.log.error('Invalid BRC selection {}. No clocking aux ' + 'board was found.'.format(clock_source)) + raise RuntimeError('Invalid BRC selection {}'.format(clock_source)) + self._base_ref_clk_select.set(0) + if clock_source == self.CLOCK_SOURCE_EXTERNAL: + # This case is a bit special: We also need to tell the clocking + # aux board if we plan to consume the external time reference or + # not. + time_src_board = \ + ClockingAuxBrdControl.SOURCE_EXTERNAL \ + if self._time_source == self.TIME_SOURCE_EXTERNAL \ + else ClockingAuxBrdControl.SOURCE_INTERNAL + self._clocking_auxbrd.set_source( + ClockingAuxBrdControl.SOURCE_EXTERNAL, time_src_board) + elif clock_source == self.CLOCK_SOURCE_INTERNAL: + self._clocking_auxbrd.set_source(ClockingAuxBrdControl.SOURCE_INTERNAL) + elif clock_source == self.CLOCK_SOURCE_GPSDO: + self._clocking_auxbrd.set_source(ClockingAuxBrdControl.SOURCE_GPSDO) + elif clock_source == self.CLOCK_SOURCE_NSYNC: + self._clocking_auxbrd.set_source(ClockingAuxBrdControl.SOURCE_NSYNC) + else: + self.log.error('Invalid BRC selection {}'.format(clock_source)) + raise RuntimeError('Invalid BRC selection {}'.format(clock_source)) + self._clock_source = clock_source + self.log.debug(f"Base reference clock source is: {clock_source}") + + def _sync_spll_clocks(self, pps_source="internal_pps"): + """ + 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 = (self.get_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, enable, master_clock_rate, delay=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 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 + """ + return self.mboard_regs_control.configure_pps_forwarding( + enable, master_clock_rate, self.get_prc_rate(), delay) + + def _set_ref_clock_freq(self, freq): + """ + Tell our USRP what the frequency of the external reference clock is. + + Will throw if it's not a valid value. + """ + if (freq < 1e6) or (freq > 50e6): + raise RuntimeError('External reference clock frequency is out of the valid range.') + if (freq % 40e3) != 0: + # TODO: implement exception of a 50e3 step size for 200MSPS + raise RuntimeError('External reference clock frequency is of incorrect step size.') + self._ext_clock_freq = freq + # If the external source is currently selected we also need to re-apply the + # time_source. This call also updates the dboards' rates. + if self.get_clock_source() == self.CLOCK_SOURCE_EXTERNAL: + self.set_sync_source(self._clock_source, self._time_source) |