aboutsummaryrefslogtreecommitdiffstats
path: root/mpm/python/usrp_mpm/dboard_manager/rhodium.py
diff options
context:
space:
mode:
Diffstat (limited to 'mpm/python/usrp_mpm/dboard_manager/rhodium.py')
-rw-r--r--mpm/python/usrp_mpm/dboard_manager/rhodium.py573
1 files changed, 573 insertions, 0 deletions
diff --git a/mpm/python/usrp_mpm/dboard_manager/rhodium.py b/mpm/python/usrp_mpm/dboard_manager/rhodium.py
new file mode 100644
index 000000000..81ca221a7
--- /dev/null
+++ b/mpm/python/usrp_mpm/dboard_manager/rhodium.py
@@ -0,0 +1,573 @@
+#
+# Copyright 2018 Ettus Research, a National Instruments Company
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+Rhodium dboard implementation module
+"""
+
+from __future__ import print_function
+import os
+import threading
+from six import iterkeys, iteritems
+from usrp_mpm import lib # Pulls in everything from C++-land
+from usrp_mpm.dboard_manager import DboardManagerBase
+from usrp_mpm.dboard_manager.rh_periphs import TCA6408, FPGAtoDbGPIO
+from usrp_mpm.dboard_manager.rh_init import RhodiumInitManager
+from usrp_mpm.dboard_manager.rh_periphs import RhCPLD
+from usrp_mpm.dboard_manager.rh_periphs import DboardClockControl
+from usrp_mpm.cores import nijesdcore
+from usrp_mpm.dboard_manager.adc_rh import AD9695Rh
+from usrp_mpm.dboard_manager.dac_rh import DAC37J82Rh
+from usrp_mpm.mpmlog import get_logger
+from usrp_mpm.sys_utils.uio import UIO
+from usrp_mpm.sys_utils.udev import get_eeprom_paths
+from usrp_mpm.bfrfs import BufferFS
+
+
+###############################################################################
+# SPI Helpers
+###############################################################################
+
+def create_spidev_iface_lmk(dev_node):
+ """
+ Create a regs iface from a spidev node
+ """
+ return lib.spi.make_spidev_regs_iface(
+ dev_node,
+ 1000000, # Speed (Hz)
+ 0, # SPI mode
+ 8, # Addr shift
+ 0, # Data shift
+ 1<<23, # Read flag
+ 0 # Write flag
+ )
+
+def create_spidev_iface_cpld(dev_node):
+ """
+ Create a regs iface from a spidev node (CPLD register protocol)
+ """
+ return lib.spi.make_spidev_regs_iface(
+ dev_node,
+ 1000000, # Speed (Hz)
+ 0, # SPI mode
+ 17, # Addr shift
+ 0, # Data shift
+ 1<<16, # Read flag
+ 0 # Write flag
+ )
+
+def create_spidev_iface_cpld_gain_loader(dev_node):
+ """
+ Create a regs iface from a spidev node (CPLD gain table protocol)
+ """
+ return lib.spi.make_spidev_regs_iface(
+ dev_node,
+ 1000000, # Speed (Hz)
+ 0, # SPI mode
+ 16, # Addr shift
+ 4, # Data shift
+ 0, # Read flag
+ 1<<3 # Write flag
+ )
+
+def create_spidev_iface_phasedac(dev_node):
+ """
+ Create a regs iface from a spidev node (AD5683)
+
+ The data shift for the SPI interface is defined based on the command
+ operation defined in the AD5683 datasheet.
+ Each SPI transaction is 24-bit: [23:20] -> command; [19:0] -> data
+ The 4 LSBs are all don't cares (Xs), regardless of the DAC's resolution.
+ Therefore, to simplify DAC writes, we compensate for all the don't care
+ bits with the data shift parameter here (4), thus 16-bit data field.
+ Special care must be taken when writing to the control register,
+ since the 6-bit payload is placed in [19:14] of the SPI transaction,
+ which is equivalent to bits [15:10] of our 16-bit data field.
+ For futher details, please refer to the AD5683's datasheet.
+ """
+ return lib.spi.make_spidev_regs_iface(
+ str(dev_node),
+ 1000000, # Speed (Hz)
+ 1, # SPI mode
+ 20, # Addr shift
+ 4, # Data shift
+ 0, # Read flag (phase DAC is write-only)
+ 0, # Write flag
+ )
+
+def create_spidev_iface_adc(dev_node):
+ """
+ Create a regs iface from a spidev node (AD9695)
+ """
+ return lib.spi.make_spidev_regs_iface(
+ str(dev_node),
+ 1000000, # Speed (Hz)
+ 0, # SPI mode
+ 8, # Addr shift
+ 0, # Data shift
+ 1<<23, # Read flag
+ 0, # Write flag
+ )
+
+def create_spidev_iface_dac(dev_node):
+ """
+ Create a regs iface from a spidev node (DAC37J82)
+ """
+ return lib.spi.make_spidev_regs_iface(
+ str(dev_node),
+ 1000000, # Speed (Hz)
+ 0, # SPI mode
+ 16, # Addr shift
+ 0, # Data shift
+ 1<<23, # Read flag
+ 0, # Write flag
+ )
+
+
+###############################################################################
+# Main dboard control class
+###############################################################################
+
+class Rhodium(DboardManagerBase):
+ """
+ Holds all dboard specific information and methods of the Rhodium dboard
+ """
+ #########################################################################
+ # Overridables
+ #
+ # See DboardManagerBase for documentation on these fields
+ #########################################################################
+ pids = [0x152]
+ #file system path to i2c-adapter/mux
+ base_i2c_adapter = '/sys/class/i2c-adapter'
+ # Maps the chipselects to the corresponding devices:
+ spi_chipselect = {
+ "cpld" : 0,
+ "cpld_gain_loader" : 0,
+ "lmk" : 1,
+ "phase_dac" : 2,
+ "adc" : 3,
+ "dac" : 4}
+ ### End of overridables #################################################
+ # Class-specific, but constant settings:
+ spi_factories = {
+ "cpld": create_spidev_iface_cpld,
+ "cpld_gain_loader": create_spidev_iface_cpld_gain_loader,
+ "lmk": create_spidev_iface_lmk,
+ "phase_dac": create_spidev_iface_phasedac,
+ "adc": create_spidev_iface_adc,
+ "dac": create_spidev_iface_dac
+ }
+ # Map I2C channel to slot index
+ i2c_chan_map = {0: 'i2c-9', 1: 'i2c-10'}
+ user_eeprom = {
+ 2: { # RevC
+ 'label': "e0004000.i2c",
+ 'offset': 1024,
+ 'max_size': 32786 - 1024,
+ 'alignment': 1024,
+ },
+ }
+ default_master_clock_rate = 245.76e6
+ default_time_source = 'internal'
+ default_current_jesd_rate = 4915.2e6
+
+ def __init__(self, slot_idx, **kwargs):
+ super(Rhodium, self).__init__(slot_idx, **kwargs)
+ self.log = get_logger("Rhodium-{}".format(slot_idx))
+ self.log.trace("Initializing Rhodium daughterboard, slot index %d",
+ self.slot_idx)
+ self.rev = int(self.device_info['rev'])
+ self.log.trace("This is a rev: {}".format(chr(65 + self.rev)))
+ # This is a default ref clock freq, it must be updated before init() is
+ # called!
+ self.ref_clock_freq = None
+ # These will get updated during init()
+ self.master_clock_rate = None
+ self.sampling_clock_rate = None
+ self.current_jesd_rate = None
+ # Predeclare some attributes to make linter happy:
+ self.lmk = None
+ self._port_expander = None
+ self.cpld = None
+ # If _init_args is None, it means that init() hasn't yet been called.
+ self._init_args = None
+ # Now initialize all peripherals. If that doesn't work, put this class
+ # into a non-functional state (but don't crash, or we can't talk to it
+ # any more):
+ try:
+ self._init_periphs()
+ self._periphs_initialized = True
+ except Exception as ex:
+ self.log.error("Failed to initialize peripherals: %s",
+ str(ex))
+ self._periphs_initialized = False
+
+
+ def _init_periphs(self):
+ """
+ Initialize power and peripherals that don't need user-settings
+ """
+ def _get_i2c_dev():
+ " Return the I2C path for this daughterboard "
+ import pyudev
+ context = pyudev.Context()
+ i2c_dev_path = os.path.join(
+ self.base_i2c_adapter,
+ self.i2c_chan_map[self.slot_idx]
+ )
+ return pyudev.Devices.from_sys_path(context, i2c_dev_path)
+ def _init_spi_devices():
+ " Returns abstraction layers to all the SPI devices "
+ self.log.trace("Loading SPI interfaces...")
+ return {
+ key: self.spi_factories[key](self._spi_nodes[key])
+ for key in self._spi_nodes
+ }
+ def _init_dboard_regs():
+ " Create a UIO object to talk to dboard regs "
+ self.log.trace("Getting UIO to talk to dboard regs...")
+ return UIO(
+ label="dboard-regs-{}".format(self.slot_idx),
+ read_only=False
+ )
+ self._port_expander = TCA6408(_get_i2c_dev())
+ self._daughterboard_gpio = FPGAtoDbGPIO(self.slot_idx)
+ self.log.debug("Turning on Module and RF power supplies")
+ self._power_on()
+ self._spi_ifaces = _init_spi_devices()
+ self.log.debug("Loaded SPI interfaces!")
+ self.cpld = RhCPLD(self._spi_ifaces['cpld'], self.log)
+ self.log.debug("Loaded CPLD interfaces!")
+ self.radio_regs = _init_dboard_regs()
+ self.radio_regs._open()
+ # Create DAC interface (analog output is disabled).
+ self.log.trace("Creating DAC control object...")
+ self.dac = DAC37J82Rh(self.slot_idx, self._spi_ifaces['dac'], self.log)
+ # Create ADC interface (JESD204B link is powered down).
+ self.log.trace("Creating ADC control object...")
+ self.adc = AD9695Rh(self.slot_idx, self._spi_ifaces['adc'], self.log)
+ self.log.info("Succesfully loaded all peripherals!")
+
+ def _power_on(self):
+ " Turn on power to daughterboard "
+ self.log.trace("Powering on slot_idx={}...".format(self.slot_idx))
+ self._daughterboard_gpio.set(FPGAtoDbGPIO.DB_POWER_ENABLE, 1)
+ self._daughterboard_gpio.set(FPGAtoDbGPIO.RF_POWER_ENABLE, 1)
+ # Check each power good signal
+
+ def _power_off(self):
+ " Turn off power to daughterboard "
+ self.log.trace("Powering off slot_idx={}...".format(self.slot_idx))
+ self._daughterboard_gpio.set(FPGAtoDbGPIO.DB_POWER_ENABLE, 0)
+ self._daughterboard_gpio.set(FPGAtoDbGPIO.RF_POWER_ENABLE, 0)
+
+ def _init_user_eeprom(self, eeprom_info):
+ """
+ Reads out user-data EEPROM, and intializes a BufferFS object from that.
+ """
+ self.log.trace("Initializing EEPROM user data...")
+ eeprom_paths = get_eeprom_paths(eeprom_info.get('label'))
+ self.log.trace("Found the following EEPROM paths: `{}'".format(
+ eeprom_paths))
+ eeprom_path = eeprom_paths[self.slot_idx]
+ self.log.trace("Selected EEPROM path: `{}'".format(eeprom_path))
+ user_eeprom_offset = eeprom_info.get('offset', 0)
+ self.log.trace("Selected EEPROM offset: %d", user_eeprom_offset)
+ user_eeprom_data = open(eeprom_path, 'rb').read()[user_eeprom_offset:]
+ self.log.trace("Total EEPROM size is: %d bytes", len(user_eeprom_data))
+ # FIXME verify EEPROM sectors
+ return BufferFS(
+ user_eeprom_data,
+ max_size=eeprom_info.get('max_size'),
+ alignment=eeprom_info.get('alignment', 1024),
+ log=self.log
+ ), eeprom_path
+
+ def init(self, args):
+ """
+ Execute necessary init dance to bring up dboard
+ """
+ # Sanity checks and input validation:
+ self.log.info("init() called with args `{}'".format(
+ ",".join(['{}={}'.format(x, args[x]) for x in args])
+ ))
+ if not self._periphs_initialized:
+ error_msg = "Cannot run init(), peripherals are not initialized!"
+ self.log.error(error_msg)
+ raise RuntimeError(error_msg)
+ # Check if ref clock freq changed (would require a full init)
+ ref_clk_freq_changed = False
+ if 'ref_clk_freq' in args:
+ new_ref_clock_freq = float(args['ref_clk_freq'])
+ assert new_ref_clock_freq in (10e6, 20e6, 25e6)
+ if new_ref_clock_freq != self.ref_clock_freq:
+ self.ref_clock_freq = new_ref_clock_freq
+ ref_clk_freq_changed = True
+ self.log.debug(
+ "Updating reference clock frequency to {:.02f} MHz!"
+ .format(self.ref_clock_freq / 1e6)
+ )
+ assert self.ref_clock_freq is not None
+ # Check if master clock freq changed (would require a full init)
+ new_master_clock_rate = \
+ float(args.get('master_clock_rate', self.default_master_clock_rate))
+ assert new_master_clock_rate in (200e6, 245.76e6, 250e6), \
+ "Invalid master clock rate: {:.02f} MHz".format(new_master_clock_rate / 1e6)
+ master_clock_rate_changed = new_master_clock_rate != self.master_clock_rate
+ if master_clock_rate_changed:
+ self.master_clock_rate = new_master_clock_rate
+ self.log.debug("Updating master clock rate to {:.02f} MHz!".format(
+ self.master_clock_rate / 1e6
+ ))
+ # From the host's perspective (i.e. UHD), master_clock_rate is thought as
+ # the data rate that the radio NoC block works on (200/245.76/250 MSPS).
+ # For Rhodium, that rate is different from the RF sampling rate = JESD rate
+ # (400/491.52/500 MHz). The FPGA has fixed half-band filters that decimate
+ # and interpolate between the radio block and the JESD core.
+ # Therefore, the board configuration through MPM relies on the sampling freq.,
+ # so a sampling_clock_rate value is internally set based on the master_clock_rate
+ # parameter given by the host.
+ self.sampling_clock_rate = 2 * self.master_clock_rate
+ self.log.trace("Updating sampling clock rate to {:.02f} MHz!".format(
+ self.sampling_clock_rate / 1e6
+ ))
+ # Track if we're able to do a "fast reinit", which means there were no
+ # major changes and can skip all slow initialization steps.
+ fast_reinit = \
+ not bool(args.get("force_reinit", False)) \
+ and not master_clock_rate_changed \
+ and not ref_clk_freq_changed
+ if fast_reinit:
+ self.log.debug("Attempting fast re-init with the following settings: "
+ "master_clock_rate={} MHz ref_clk_freq={} MHz"
+ .format(self.master_clock_rate / 1e6, self.ref_clock_freq / 1e6))
+ init_result = True
+ else:
+ init_result = RhodiumInitManager(self, self._spi_ifaces).init(args)
+ if init_result:
+ self._init_args = args
+ return init_result
+
+ def get_user_eeprom_data(self):
+ """
+ Return a dict of blobs stored in the user data section of the EEPROM.
+ """
+ return {
+ blob_id: self.eeprom_fs.get_blob(blob_id)
+ for blob_id in iterkeys(self.eeprom_fs.entries)
+ }
+
+ def set_user_eeprom_data(self, eeprom_data):
+ """
+ Update the local EEPROM with the data from eeprom_data.
+
+ The actual writing to EEPROM can take some time, and is thus kicked
+ into a background task. Don't call set_user_eeprom_data() quickly in
+ succession. Also, while the background task is running, reading the
+ EEPROM is unavailable and MPM won't be able to reboot until it's
+ completed.
+ However, get_user_eeprom_data() will immediately return the correct
+ data after this method returns.
+ """
+ for blob_id, blob in iteritems(eeprom_data):
+ self.eeprom_fs.set_blob(blob_id, blob)
+ self.log.trace("Writing EEPROM info to `{}'".format(self.eeprom_path))
+ eeprom_offset = self.user_eeprom[self.rev]['offset']
+ def _write_to_eeprom_task(path, offset, data, log):
+ " Writer task: Actually write to file "
+ # Note: This can be sped up by only writing sectors that actually
+ # changed. To do so, this function would need to read out the
+ # current state of the file, do some kind of diff, and then seek()
+ # to the different sectors. When very large blobs are being
+ # written, it doesn't actually help all that much, of course,
+ # because in that case, we'd anyway be changing most of the EEPROM.
+ with open(path, 'r+b') as eeprom_file:
+ log.trace("Seeking forward to `{}'".format(offset))
+ eeprom_file.seek(eeprom_offset)
+ log.trace("Writing a total of {} bytes.".format(
+ len(self.eeprom_fs.buffer)))
+ eeprom_file.write(data)
+ log.trace("EEPROM write complete.")
+ thread_id = "eeprom_writer_task_{}".format(self.slot_idx)
+ if any([x.name == thread_id for x in threading.enumerate()]):
+ # Should this be fatal?
+ self.log.warn("Another EEPROM writer thread is already active!")
+ writer_task = threading.Thread(
+ target=_write_to_eeprom_task,
+ args=(
+ self.eeprom_path,
+ eeprom_offset,
+ self.eeprom_fs.buffer,
+ self.log
+ ),
+ name=thread_id,
+ )
+ writer_task.start()
+ # Now return and let the copy finish on its own. The thread will detach
+ # and MPM this process won't terminate until the thread is complete.
+ # This does not stop anyone from killing this process (and the thread)
+ # while the EEPROM write is happening, though.
+
+
+ ##########################################################################
+ # Clocking control APIs
+ ##########################################################################
+
+ def set_clk_safe_state(self):
+ """
+ Disable all components that could react badly to a sudden change in
+ clocking. After calling this method, all clocks will be off. Calling
+ _reinit() will turn them on again.
+ """
+ if self._init_args is None:
+ # Then we're already in a safe state
+ return
+ # Put the ADC and the DAC in a safe state because they receive a LMK's clock.
+ # The DAC37J82 datasheet only recommends disabling its analog output before
+ # a clock is provided to the chip.
+ self.dac.tx_enable(False)
+ self.adc.power_down_channel(True)
+ # Clear the Sample Clock enables and place the MMCM in reset.
+ db_clk_control = DboardClockControl(self.radio_regs, self.log)
+ db_clk_control.reset_mmcm()
+ # Place the JESD204b core in reset, mainly to reset QPLL/CPLLs.
+ jesdcore = nijesdcore.NIJESDCore(self.radio_regs, self.slot_idx,
+ **RhodiumInitManager.JESD_DEFAULT_ARGS)
+ jesdcore.reset()
+ # The reference clock is handled elsewhere since it is a motherboard-
+ # level clock.
+
+ def _reinit(self, master_clock_rate):
+ """
+ This will re-run init(). We store all the settings in _init_args, so we
+ will bring the device into the same state that it was before, with the
+ exception of frequency and gain. Those need to be re-set by UHD in order
+ not to invalidate the UHD caches.
+ """
+ args = self._init_args
+ args["master_clock_rate"] = master_clock_rate
+ args["ref_clk_freq"] = self.ref_clock_freq
+ # If we add API calls to reset the cals, they need to update
+ # self._init_args
+ self.master_clock_rate = None # <= This will force a re-init
+ self.init(args)
+ # self.master_clock_rate is now OK again
+
+ def set_master_clock_rate(self, rate):
+ """
+ Set the master clock rate to rate. Note this will trigger a
+ re-initialization of the entire clocking, unless rate matches the
+ current master clock rate.
+ """
+ if rate == self.master_clock_rate:
+ self.log.debug(
+ "New master clock rate assignment matches previous assignment. "
+ "Ignoring set_master_clock_rate() command.")
+ return self.master_clock_rate
+ self._reinit(rate)
+ return rate
+
+ def get_master_clock_rate(self):
+ " Return master clock rate (== sampling rate / 2) "
+ return self.master_clock_rate
+
+ def update_ref_clock_freq(self, freq, **kwargs):
+ """
+ Call this function if the frequency of the reference clock changes
+ (the 10, 20, 25 MHz one).
+
+ If this function is called while the device is in an initialized state,
+ it will also re-trigger the initialization sequence.
+
+ No need to set the device in a safe state because (presumably) the user
+ has already switched the clock rate externally. All we need to do now
+ is re-initialize with the new rate.
+ """
+ assert freq in (10e6, 20e6, 25e6), \
+ "Invalid ref clock frequency: {}".format(freq)
+ self.log.trace("Changing ref clock frequency to %f MHz", freq/1e6)
+ self.ref_clock_freq = freq
+ if self._init_args is not None:
+ self._reinit(self.master_clock_rate)
+
+
+ ##########################################################################
+ # Debug
+ ##########################################################################
+
+ def cpld_peek(self, addr):
+ """
+ Debug for accessing the CPLD via the RPC shell.
+ """
+ self.log.trace("CPLD Signature: 0x{:X}".format(self.cpld.peek(0x00)))
+ revision_msb = self.cpld.peek16(0x04)
+ self.log.trace("CPLD Revision: 0x{:X}"
+ .format(self.cpld.peek16(0x03) | (revision_msb << 16)))
+ return self.cpld.peek16(addr)
+
+ def cpld_poke(self, addr, data):
+ """
+ Debug for accessing the CPLD via the RPC shell.
+ """
+ self.log.trace("CPLD Signature: 0x{:X}".format(self.cpld.peek16(0x00)))
+ revision_msb = self.cpld.peek16(0x04)
+ self.log.trace("CPLD Revision: 0x{:X}"
+ .format(self.cpld.peek16(0x03) | (revision_msb << 16)))
+ self.cpld.poke16(addr, data)
+ return self.cpld.peek16(addr)
+
+ def lmk_peek(self, addr):
+ """
+ Debug for accessing the LMK via the RPC shell.
+ """
+ lmk_regs = self._spi_ifaces['lmk']
+ self.log.trace("LMK Chip ID: 0x{:X}".format(lmk_regs.peek8(0x03)))
+ return lmk_regs.peek8(addr)
+
+ def lmk_poke(self, addr, data):
+ """
+ Debug for accessing the LMK via the RPC shell.
+ """
+ lmk_regs = self._spi_ifaces['lmk']
+ self.log.trace("LMK Chip ID: 0x{:X}".format(lmk_regs.peek8(0x03)))
+ lmk_regs.poke8(addr, data)
+ return lmk_regs.peek8(addr)
+
+ def pdac_poke(self, addr, data):
+ """
+ Debug for accessing the Phase DAC via the RPC shell.
+ """
+ pdac_regs = self._spi_ifaces['phase_dac']
+ pdac_regs.poke16(addr, data)
+ return
+
+ def adc_peek(self, addr):
+ """
+ Debug for accessing the ADC via the RPC shell.
+ """
+ adc_regs = self._spi_ifaces['adc']
+ self.log.trace("ADC Chip ID: 0x{:X}".format(adc_regs.peek8(0x04)))
+ return adc_regs.peek8(addr)
+
+ def adc_poke(self, addr, data):
+ """
+ Debug for accessing the ADC via the RPC shell
+ """
+ adc_regs = self._spi_ifaces['adc']
+ self.log.trace("ADC Chip ID: 0x{:X}".format(adc_regs.peek8(0x04)))
+ adc_regs.poke8(addr, data)
+ return adc_regs.peek8(addr)
+
+ def dump_jesd_core(self):
+ """
+ Debug for reading out all JESD core registers via RPC shell
+ """
+ radio_regs = UIO(label="dboard-regs-{}".format(self.slot_idx))
+ for i in range(0x2000, 0x2110, 0x10):
+ print(("0x%04X " % i), end=' ')
+ for j in range(0, 0x10, 0x4):
+ print(("%08X" % radio_regs.peek32(i + j)), end=' ')
+ print("")