diff options
Diffstat (limited to 'mpm/python/usrp_mpm/dboard_manager/rhodium.py')
-rw-r--r-- | mpm/python/usrp_mpm/dboard_manager/rhodium.py | 573 |
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("") |