Exemple #1
0
    def _init_periphs(self):
        """
        Initialize power and peripherals that don't need user-settings
        """
        self._port_expander = TCA6408(self._get_i2c_dev(self.slot_idx))
        self._power_on()
        self.log.debug("Loading C++ drivers...")

        # The Mykonos TX DeFramer lane crossbar requires configuration on a per-slot
        # basis due to motherboard MGT lane swapping.
        # The RX framer lane crossbar configuration
        # is identical for both slots and is hard-coded within the Mykonos API.
        deserializer_lane_xbar = 0xD2 if self.slot_idx == 0 else 0x72

        self._device = lib.dboards.magnesium_manager(
            self._spi_nodes['mykonos'], deserializer_lane_xbar)
        self.mykonos = self._device.get_radio_ctrl()
        self.spi_lock = self._device.get_spi_lock()
        self.log.trace("Loaded C++ drivers.")
        self._init_myk_api(self.mykonos)
        self.eeprom_fs, self.eeprom_path = self._init_user_eeprom(
            self._get_user_eeprom_info(self.rev))
        self.log.trace("Loading SPI devices...")
        self._spi_ifaces = {
            key: self.spi_factories[key](self._spi_nodes[key])
            for key in self.spi_factories
        }
        self.cpld = MgCPLD(self._spi_ifaces['cpld'], self.log)
        self.device_info['cpld_rev'] = \
                str(self.cpld.major_rev) + '.' + str(self.cpld.minor_rev)
Exemple #2
0
    def _init_periphs(self):
        """
        Initialize power and peripherals that don't need user-settings
        """
        self._port_expander = TCA6408(self._get_i2c_dev(self.slot_idx))
        self._power_on()
        self.log.debug("Loading C++ drivers...")

        # The Mykonos TX DeFramer lane crossbar requires configuration on a per-slot
        # basis due to motherboard MGT lane swapping.
        # The RX framer lane crossbar configuration
        # is identical for both slots and is hard-coded within the Mykonos API.
        deserializer_lane_xbar = 0xD2 if self.slot_idx == 0 else 0x72

        self._device = lib.dboards.magnesium_manager(
            self._spi_nodes['mykonos'],
            deserializer_lane_xbar
        )
        self.mykonos = self._device.get_radio_ctrl()
        self.spi_lock = self._device.get_spi_lock()
        self.log.trace("Loaded C++ drivers.")
        self._init_myk_api(self.mykonos)
        self.log.debug(
            "AD9371: ARM version: {arm_ver} API version: {api_ver} "
            "Device revision: {dev_rev}".format(
                arm_ver=self.get_arm_version(),
                api_ver=self.get_api_version(),
                dev_rev=self.get_device_rev(),
            )
        )
        self.eeprom_fs, self.eeprom_path = self._init_user_eeprom(
            self._get_user_eeprom_info(self.rev)
        )
        self.log.trace("Loading SPI devices...")
        self._spi_ifaces = {
            key: self.spi_factories[key](self._spi_nodes[key])
            for key in self.spi_factories
        }
        self.cpld = MgCPLD(self._spi_ifaces['cpld'], self.log)
        self.device_info['cpld_rev'] = \
                str(self.cpld.major_rev) + '.' + str(self.cpld.minor_rev)
Exemple #3
0
class Magnesium(DboardManagerBase):
    """
    Holds all dboard specific information and methods of the magnesium dboard
    """
    #########################################################################
    # Overridables
    #
    # See DboardManagerBase for documentation on these fields
    #########################################################################
    pids = [0x150]
    rx_sensor_callback_map = {
        'lowband_lo_locked': 'get_lowband_tx_lo_locked_sensor',
        'ad9371_lo_locked': 'get_ad9371_tx_lo_locked_sensor',
    }
    tx_sensor_callback_map = {
        'lowband_lo_locked': 'get_lowband_rx_lo_locked_sensor',
        'ad9371_lo_locked': 'get_ad9371_rx_lo_locked_sensor',
    }
    # Maps the chipselects to the corresponding devices:
    spi_chipselect = {"cpld": 0, "lmk": 1, "mykonos": 2, "phase_dac": 3}
    ### End of overridables #################################################
    # Class-specific, but constant settings:
    spi_factories = {
        "cpld": create_spidev_iface_cpld,
        "lmk": create_spidev_iface_lmk,
        "phase_dac": create_spidev_iface_phasedac,
    }
    #file system path to i2c-adapter/mux
    base_i2c_adapter = '/sys/class/i2c-adapter'
    # Map I2C channel to slot index
    i2c_chan_map = {0: 'i2c-9', 1: 'i2c-10'}
    # This map describes how the user data is stored in EEPROM. If a dboard rev
    # changes the way the EEPROM is used, we add a new entry. If a dboard rev
    # is not found in the map, then we go backward until we find a suitable rev
    user_eeprom = {
        2: { # RevC
            'label': "e0004000.i2c",
            'offset': 1024,
            'max_size': 32786 - 1024,
            'alignment': 1024,
        },
    }
    default_master_clock_rate = 125e6
    default_time_source = 'internal'
    default_current_jesd_rate = 2500e6

    def __init__(self, slot_idx, **kwargs):
        super(Magnesium, self).__init__(slot_idx, **kwargs)
        self.log = get_logger("Magnesium-{}".format(slot_idx))
        self.log.trace("Initializing Magnesium 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.current_jesd_rate = None
        # Predeclare some attributes to make linter happy:
        self.lmk = None
        self._port_expander = None
        self.mykonos = None
        self.eeprom_fs = None
        self.eeprom_path = None
        self.cpld = None
        self._init_args = {}
        # 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
        """
        self._port_expander = TCA6408(self._get_i2c_dev(self.slot_idx))
        self._power_on()
        self.log.debug("Loading C++ drivers...")

        # The Mykonos TX DeFramer lane crossbar requires configuration on a per-slot
        # basis due to motherboard MGT lane swapping.
        # The RX framer lane crossbar configuration
        # is identical for both slots and is hard-coded within the Mykonos API.
        deserializer_lane_xbar = 0xD2 if self.slot_idx == 0 else 0x72

        self._device = lib.dboards.magnesium_manager(
            self._spi_nodes['mykonos'], deserializer_lane_xbar)
        self.mykonos = self._device.get_radio_ctrl()
        self.spi_lock = self._device.get_spi_lock()
        self.log.trace("Loaded C++ drivers.")
        self._init_myk_api(self.mykonos)
        self.log.debug("AD9371: ARM version: {arm_ver} API version: {api_ver} "
                       "Device revision: {dev_rev}".format(
                           arm_ver=self.get_arm_version(),
                           api_ver=self.get_api_version(),
                           dev_rev=self.get_device_rev(),
                       ))
        self.eeprom_fs, self.eeprom_path = self._init_user_eeprom(
            self._get_user_eeprom_info(self.rev))
        self.log.trace("Loading SPI devices...")
        self._spi_ifaces = {
            key: self.spi_factories[key](self._spi_nodes[key])
            for key in self.spi_factories
        }
        self.cpld = MgCPLD(self._spi_ifaces['cpld'], self.log)
        self.device_info['cpld_rev'] = \
                str(self.cpld.major_rev) + '.' + str(self.cpld.minor_rev)

    def _power_on(self):
        " Turn on power to daughterboard "
        self.log.trace("Powering on slot_idx={}...".format(self.slot_idx))
        self._port_expander.set("PWR-EN-3.6V")
        self._port_expander.set("PWR-EN-1.5V")
        self._port_expander.set("PWR-EN-5.5V")
        self._port_expander.set("LED")

    def _power_off(self):
        " Turn off power to daughterboard "
        self.log.trace("Powering off slot_idx={}...".format(self.slot_idx))
        self._port_expander.reset("PWR-EN-3.6V")
        self._port_expander.reset("PWR-EN-1.5V")
        self._port_expander.reset("PWR-EN-5.5V")
        self._port_expander.reset("LED")

    def _get_i2c_dev(self, slot_idx):
        " 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[slot_idx])
        return pyudev.Devices.from_sys_path(context, i2c_dev_path)

    def _init_myk_api(self, myk):
        """
        Propagate the C++ Mykonos API into Python land.
        """
        def export_method(obj, method):
            " Export a method object, including docstring "
            meth_obj = getattr(obj, method)

            def func(*args):
                " Functor for storing docstring too "
                return meth_obj(*args)

            func.__doc__ = meth_obj.__doc__
            return func

        self.log.trace("Forwarding AD9371 methods to Magnesium class...")
        for method in [
                x for x in dir(self.mykonos)
                if not x.startswith("_") and \
                        callable(getattr(self.mykonos, x))]:
            self.log.trace("adding {}".format(method))
            setattr(self, method, export_method(myk, method))

    def _get_user_eeprom_info(self, rev):
        """
        Return an EEPROM access map (from self.user_eeprom) based on the rev.
        """
        rev_for_lookup = rev
        while rev_for_lookup not in self.user_eeprom:
            if rev_for_lookup < 0:
                raise RuntimeError(
                    "Could not find a user EEPROM map for "
                    "revision %d!", rev)
            rev_for_lookup -= 1
        assert rev_for_lookup in self.user_eeprom, \
                "Invalid EEPROM lookup rev!"
        return self.user_eeprom[rev_for_lookup]

    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.debug("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 = float(args['ref_clk_freq'])
                ref_clk_freq_changed = True
        assert self.ref_clock_freq is not None
        # Check if master clock freq changed (would require a full init)
        master_clock_rate = \
            float(args.get('master_clock_rate',
                           self.default_master_clock_rate))
        assert master_clock_rate in (122.88e6, 125e6, 153.6e6), \
                "Invalid master clock rate: {:.02f} MHz".format(
                    master_clock_rate / 1e6)
        master_clock_rate_changed = \
            master_clock_rate != self.master_clock_rate
        if master_clock_rate_changed:
            self.master_clock_rate = master_clock_rate
            self.log.debug("Updating master clock rate to {:.02f} MHz!".format(
                self.master_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={}".format(
                    self.master_clock_rate / 1e6,
                    self.ref_clock_freq,
                ))
        # Note: MagnesiumInitManager.init() can still override fast_reinit.
        # Consider it a hint.
        result = MagnesiumInitManager(self, self._spi_ifaces).init(
            args, self._init_args, fast_reinit)
        if result:
            self._init_args = args
        return 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 won't terminate this process until the thread is complete.
        # This does not stop anyone from killing this process (and the thread)
        # while the EEPROM write is happening, though.

    def get_master_clock_rate(self):
        " Return master clock rate (== sampling rate) "
        return self.master_clock_rate

    def update_ref_clock_freq(self, freq):
        """
        Call this function if the frequency of the reference clock changes (the
        10, 20, 25 MHz one). Note: Won't actually re-run any settings.
        """
        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

    ##########################################################################
    # Sensors
    ##########################################################################
    def get_ref_lock(self):
        """
        Returns True if the LMK reference is locked.

        Note: This does not return a sensor dict. The sensor API call is
        in the motherboard class.
        """
        if self.lmk is None:
            self.log.trace("LMK object not yet initialized, defaulting to " \
                           "no ref locked!")
            return False
        lmk_lock_status = self.lmk.check_plls_locked()
        self.log.trace("LMK lock status is: {}".format(lmk_lock_status))
        return lmk_lock_status

    def get_lowband_lo_lock(self, which):
        """
        Return LO lock status (Boolean!) of the lowband LOs. 'which' must be
        either 'tx' or 'rx'
        """
        assert which.lower() in ('tx', 'rx')
        return self.cpld.get_lo_lock_status(which.upper())

    def get_ad9371_lo_lock(self, which):
        """
        Return LO lock status (Boolean!) of the lowband LOs. 'which' must be
        either 'tx' or 'rx'
        """
        return self.mykonos.get_lo_locked(which.upper())

    def get_lowband_tx_lo_locked_sensor(self, chan):
        " TX lowband LO lock sensor "
        self.log.trace("Querying TX lowband LO lock status for chan %d...",
                       chan)
        lock_status = self.get_lowband_lo_lock('tx')
        return {
            'name': 'lowband_lo_locked',
            'type': 'BOOLEAN',
            'unit': 'locked' if lock_status else 'unlocked',
            'value': str(lock_status).lower(),
        }

    def get_lowband_rx_lo_locked_sensor(self, chan):
        " RX lowband LO lock sensor "
        self.log.trace("Querying RX lowband LO lock status for chan %d...",
                       chan)
        lock_status = self.get_lowband_lo_lock('rx')
        return {
            'name': 'lowband_lo_locked',
            'type': 'BOOLEAN',
            'unit': 'locked' if lock_status else 'unlocked',
            'value': str(lock_status).lower(),
        }

    def get_ad9371_tx_lo_locked_sensor(self, chan):
        " TX ad9371 LO lock sensor "
        self.log.trace("Querying TX AD9371 LO lock status for chan %d...",
                       chan)
        lock_status = self.get_ad9371_lo_lock('tx')
        return {
            'name': 'ad9371_lo_locked',
            'type': 'BOOLEAN',
            'unit': 'locked' if lock_status else 'unlocked',
            'value': str(lock_status).lower(),
        }

    def get_ad9371_rx_lo_locked_sensor(self, chan):
        " RX ad9371 LO lock sensor "
        self.log.trace("Querying RX AD9371 LO lock status for chan %d...",
                       chan)
        lock_status = self.get_ad9371_lo_lock('tx')
        return {
            'name': 'ad9371_lo_locked',
            'type': 'BOOLEAN',
            'unit': 'locked' if lock_status else 'unlocked',
            'value': str(lock_status).lower(),
        }

    ##########################################################################
    # Debug
    ##########################################################################
    def cpld_peek(self, addr):
        """
        Debug for accessing the CPLD via the RPC shell.
        """
        return self.cpld.peek16(addr)

    def cpld_poke(self, addr, data):
        """
        Debug for accessing the CPLD via the RPC shell.
        """
        self.cpld.poke16(addr, data)
        return self.cpld.peek16(addr)

    def dump_jesd_core(self):
        " Debug method to dump all JESD core regs "
        with open_uio(label="dboard-regs-{}".format(self.slot_idx),
                      read_only=False) as dboard_ctrl_regs:
            for i in range(0x2000, 0x2110, 0x10):
                print(("0x%04X " % i), end=' ')
                for j in range(0, 0x10, 0x4):
                    print(("%08X" % dboard_ctrl_regs.peek32(i + j)), end=' ')
                print("")

    def dbcore_peek(self, addr):
        """
        Debug for accessing the DB Core registers via the RPC shell.
        """
        with open_uio(label="dboard-regs-{}".format(self.slot_idx),
                      read_only=False) as dboard_ctrl_regs:
            rd_data = dboard_ctrl_regs.peek32(addr)
            self.log.trace(
                "DB Core Register 0x{:04X} response: 0x{:08X}".format(
                    addr, rd_data))
            return rd_data

    def dbcore_poke(self, addr, data):
        """
        Debug for accessing the DB Core registers via the RPC shell.
        """
        with open_uio(label="dboard-regs-{}".format(self.slot_idx),
                      read_only=False) as dboard_ctrl_regs:
            self.log.trace(
                "Writing DB Core Register 0x{:04X} with 0x{:08X}...".format(
                    addr, data))
            dboard_ctrl_regs.poke32(addr, data)
Exemple #4
0
class Magnesium(DboardManagerBase):
    """
    Holds all dboard specific information and methods of the magnesium dboard
    """
    #########################################################################
    # Overridables
    #
    # See DboardManagerBase for documentation on these fields
    #########################################################################
    pids = [0x150]
    rx_sensor_callback_map = {
        'lowband_lo_locked': 'get_lowband_tx_lo_locked_sensor',
        'ad9371_lo_locked': 'get_ad9371_tx_lo_locked_sensor',
    }
    tx_sensor_callback_map = {
        'lowband_lo_locked': 'get_lowband_rx_lo_locked_sensor',
        'ad9371_lo_locked': 'get_ad9371_rx_lo_locked_sensor',
    }
    # Maps the chipselects to the corresponding devices:
    spi_chipselect = {"cpld": 0, "lmk": 1, "mykonos": 2, "phase_dac": 3}
    ### End of overridables #################################################
    # Class-specific, but constant settings:
    spi_factories = {
        "cpld": create_spidev_iface_cpld,
        "lmk": create_spidev_iface_lmk,
        "phase_dac": create_spidev_iface_phasedac,
    }
    #file system path to i2c-adapter/mux
    base_i2c_adapter = '/sys/class/i2c-adapter'
    # Map I2C channel to slot index
    i2c_chan_map = {0: 'i2c-9', 1: 'i2c-10'}
    # This map describes how the user data is stored in EEPROM. If a dboard rev
    # changes the way the EEPROM is used, we add a new entry. If a dboard rev
    # is not found in the map, then we go backward until we find a suitable rev
    user_eeprom = {
        2: { # RevC
            'label': "e0004000.i2c",
            'offset': 1024,
            'max_size': 32786 - 1024,
            'alignment': 1024,
        },
    }
    # DAC is initialized to midscale automatically on power-on: 16-bit DAC, so midpoint
    # is at 2^15 = 32768. However, the linearity of the DAC is best just below that
    # point, so we set it to the (carefully calculated) alternate value instead.
    INIT_PHASE_DAC_WORD = 31000  # Intentionally decimal
    PHASE_DAC_SPI_ADDR = 0x0
    # External PPS pipeline delay from the PPS captured at the FPGA to TDC input,
    # in reference clock ticks
    EXT_PPS_DELAY = 5
    # Variable PPS delay before the RP/SP pulsers begin. Fixed value for the N3xx devices.
    N3XX_INT_PPS_DELAY = 4
    default_master_clock_rate = 125e6
    default_time_source = 'internal'
    default_current_jesd_rate = 2500e6

    def __init__(self, slot_idx, **kwargs):
        super(Magnesium, self).__init__(slot_idx, **kwargs)
        self.log = get_logger("Magnesium-{}".format(slot_idx))
        self.log.trace("Initializing Magnesium 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.current_jesd_rate = None
        # Predeclare some attributes to make linter happy:
        self.lmk = None
        self._port_expander = None
        self.mykonos = None
        self.eeprom_fs = None
        self.eeprom_path = None
        self.cpld = None
        self._init_cals_mask = 0
        self._tracking_cals_mask = 0
        self._init_cals_timeout = 0
        # 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
        """
        self._port_expander = TCA6408(self._get_i2c_dev(self.slot_idx))
        self._power_on()
        self.log.debug("Loading C++ drivers...")

        # The Mykonos TX DeFramer lane crossbar requires configuration on a per-slot
        # basis due to motherboard MGT lane swapping.
        # The RX framer lane crossbar configuration
        # is identical for both slots and is hard-coded within the Mykonos API.
        deserializer_lane_xbar = 0xD2 if self.slot_idx == 0 else 0x72

        self._device = lib.dboards.magnesium_manager(
            self._spi_nodes['mykonos'], deserializer_lane_xbar)
        self.mykonos = self._device.get_radio_ctrl()
        self.spi_lock = self._device.get_spi_lock()
        self.log.trace("Loaded C++ drivers.")
        self._init_myk_api(self.mykonos)
        self.eeprom_fs, self.eeprom_path = self._init_user_eeprom(
            self._get_user_eeprom_info(self.rev))
        self.log.trace("Loading SPI devices...")
        self._spi_ifaces = {
            key: self.spi_factories[key](self._spi_nodes[key])
            for key in self.spi_factories
        }
        self.cpld = MgCPLD(self._spi_ifaces['cpld'], self.log)
        self.device_info['cpld_rev'] = \
                str(self.cpld.major_rev) + '.' + str(self.cpld.minor_rev)

    def _power_on(self):
        " Turn on power to daughterboard "
        self.log.trace("Powering on slot_idx={}...".format(self.slot_idx))
        self._port_expander.set("PWR-EN-3.6V")
        self._port_expander.set("PWR-EN-1.5V")
        self._port_expander.set("PWR-EN-5.5V")
        self._port_expander.set("LED")

    def _power_off(self):
        " Turn off power to daughterboard "
        self.log.trace("Powering off slot_idx={}...".format(self.slot_idx))
        self._port_expander.reset("PWR-EN-3.6V")
        self._port_expander.reset("PWR-EN-1.5V")
        self._port_expander.reset("PWR-EN-5.5V")
        self._port_expander.reset("LED")

    def _get_i2c_dev(self, slot_idx):
        " 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[slot_idx])
        return pyudev.Devices.from_sys_path(context, i2c_dev_path)

    def _init_myk_api(self, myk):
        """
        Propagate the C++ Mykonos API into Python land.
        """
        def export_method(obj, method):
            " Export a method object, including docstring "
            meth_obj = getattr(obj, method)

            def func(*args):
                " Functor for storing docstring too "
                return meth_obj(*args)

            func.__doc__ = meth_obj.__doc__
            return func

        self.log.trace("Forwarding AD9371 methods to Magnesium class...")
        for method in [
                x for x in dir(self.mykonos)
                if not x.startswith("_") and \
                        callable(getattr(self.mykonos, x))]:
            self.log.trace("adding {}".format(method))
            setattr(self, method, export_method(myk, method))

    def _get_user_eeprom_info(self, rev):
        """
        Return an EEPROM access map (from self.user_eeprom) based on the rev.
        """
        rev_for_lookup = rev
        while rev_for_lookup not in self.user_eeprom:
            if rev_for_lookup < 0:
                raise RuntimeError(
                    "Could not find a user EEPROM map for "
                    "revision %d!", rev)
            rev_for_lookup -= 1
        assert rev_for_lookup in self.user_eeprom, \
                "Invalid EEPROM lookup rev!"
        return self.user_eeprom[rev_for_lookup]

    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
        """
        def _init_lmk(lmk_spi, ref_clk_freq, master_clk_rate, pdac_spi,
                      init_phase_dac_word, phase_dac_spi_addr):
            """
            Sets the phase DAC to initial value, and then brings up the LMK
            according to the selected ref clock frequency.
            Will throw if something fails.
            """
            self.log.trace(
                "Initializing Phase DAC to d{}.".format(init_phase_dac_word))
            pdac_spi.poke16(phase_dac_spi_addr, init_phase_dac_word)
            return LMK04828Mg(lmk_spi, self.spi_lock, ref_clk_freq,
                              master_clk_rate, self.log)

        def _sync_db_clock():
            " Synchronizes the DB clock to the common reference "
            reg_offset = 0x200
            ref_clk_freq = self.ref_clock_freq
            ext_pps_delay = self.EXT_PPS_DELAY
            if args.get('time_source', self.default_time_source) == 'sfp0':
                reg_offset = 0x400
                ref_clk_freq = 62.5e6
                ext_pps_delay = 1  # only 1 flop between the WR core output and the TDC input
            synchronizer = ClockSynchronizer(
                dboard_ctrl_regs,
                self.lmk,
                self._spi_ifaces['phase_dac'],
                reg_offset,
                self.master_clock_rate,
                ref_clk_freq,
                860E-15,  # fine phase shift. TODO don't hardcode. This should live in the EEPROM
                self.INIT_PHASE_DAC_WORD,
                self.PHASE_DAC_SPI_ADDR,
                ext_pps_delay,
                self.N3XX_INT_PPS_DELAY,
                self.slot_idx)
            # The radio clock traces on the motherboard are 69 ps longer for Daughterboard B
            # than Daughterboard A. We want both of these clocks to align at the converters
            # on each board, so adjust the target value for DB B. This is an N3xx series
            # peculiarity and will not apply to other motherboards.
            trace_delay_offset = {0: 0.0e-0, 1: 69.0e-12}[self.slot_idx]
            offset = synchronizer.run(num_meas=[512, 128],
                                      target_offset=trace_delay_offset)
            offset_error = abs(offset)
            if offset_error > 100e-12:
                self.log.error(
                    "Clock synchronizer measured an offset of {:.1f} ps!".
                    format(offset_error * 1e12))
                raise RuntimeError(
                    "Clock synchronizer measured an offset of {:.1f} ps!".
                    format(offset_error * 1e12))
            else:
                self.log.debug(
                    "Residual synchronization error: {:.1f} ps.".format(
                        offset_error * 1e12))
            synchronizer = None
            self.log.debug("Sample Clock Synchronization Complete!")

        ## Go, go, go!
        # Sanity checks and input validation:
        self.log.debug("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)
        if 'ref_clk_freq' in args:
            self.ref_clock_freq = float(args['ref_clk_freq'])
            assert self.ref_clock_freq in (10e6, 20e6, 25e6)
        assert self.ref_clock_freq is not None
        master_clock_rate = \
            float(args.get('master_clock_rate',
                           self.default_master_clock_rate))
        assert master_clock_rate in (122.88e6, 125e6, 153.6e6), \
                "Invalid master clock rate: {:.02f} MHz".format(
                    master_clock_rate / 1e6)
        master_clock_rate_changed = master_clock_rate != self.master_clock_rate
        if master_clock_rate_changed:
            self.master_clock_rate = master_clock_rate
            self.log.debug("Updating master clock rate to {:.02f} MHz!".format(
                self.master_clock_rate / 1e6))
        # Init some more periphs:
        # The following peripherals are only used during init, so we don't want
        # to hang on to them for the full lifetime of the Magnesium class. This
        # helps us close file descriptors associated with the UIO objects.
        with open_uio(label="dboard-regs-{}".format(self.slot_idx),
                      read_only=False) as dboard_ctrl_regs:
            self.log.trace("Creating jesdcore object...")
            jesdcore = nijesdcore.NIMgJESDCore(dboard_ctrl_regs, self.slot_idx)
            # Now get cracking with the actual init sequence:
            self.log.trace("Creating dboard clock control object...")
            db_clk_control = DboardClockControl(dboard_ctrl_regs, self.log)
            self.log.debug("Reset Dboard Clocking and JESD204B interfaces...")
            db_clk_control.reset_mmcm()
            jesdcore.reset()
            self.log.trace("Initializing LMK...")
            self.lmk = _init_lmk(
                self._spi_ifaces['lmk'],
                self.ref_clock_freq,
                self.master_clock_rate,
                self._spi_ifaces['phase_dac'],
                self.INIT_PHASE_DAC_WORD,
                self.PHASE_DAC_SPI_ADDR,
            )
            db_clk_control.enable_mmcm()
            # Synchronize DB Clocks
            _sync_db_clock()
            self.log.debug(
                "Sample Clocks and Phase DAC Configured Successfully!")
            # Clocks and PPS are now fully active!
            self.mykonos.set_master_clock_rate(self.master_clock_rate)
            self.init_jesd(jesdcore, args)
            jesdcore = None  # Help with garbage collection
            # That's all that requires access to the dboard regs!
        if bool(args.get('rfic_digital_loopback')):
            self.log.warning("RF Functionality Disabled: JESD204b digital loopback " \
                             "enabled inside Mykonos!")
            self.mykonos.enable_jesd_loopback(1)
        else:
            self.mykonos.start_radio()
        return True

    def _parse_and_convert_cal_args(self, table, cal_args):
        """Parse calibration string and convert it to a number

        Arguments:
            table {dictionary} -- a look up table that map a type of calibration
                                  to its bit mask.(defined in AD9375-UG992)
            cal_args {string} --  string arguments from user in form of "CAL1|CAL2|CAL3"
                                  or "CAL1 CAL2 CAL3"  or "CAL1;CAL2;CAL3"

        Returns:
            int -- calibration value bit mask.
        """
        value = 0
        try:
            return int(cal_args, 0)
        except ValueError:
            pass
        for key in re.split(r'[;|\s]\s*', cal_args):
            value_tmp = table.get(key.upper())
            if (value_tmp) != None:
                value |= value_tmp
            else:
                self.log.warning(
                    "Calibration key `%s' is not in calibration table. "
                    "Ignoring this key.", key.upper())
        return value

    def init_rf_cal(self, args):
        " Setup RF CAL "
        self.log.debug("Setting up RF CAL...")
        try:
            self._init_cals_mask = \
                    self._parse_and_convert_cal_args(
                        INIT_CALIBRATION_TABLE,
                        args.get('init_cals', 'DEFAULT')
                    )
            self._tracking_cals_mask = \
                    self._parse_and_convert_cal_args(
                        TRACKING_CALIBRATION_TABLE,
                        args.get('tracking_cals', 'DEFAULT')
                    )
            self._init_cals_timeout = int(
                args.get('init_cals_timeout',
                         str(self.mykonos.DEFAULT_INIT_CALS_TIMEOUT)), 0)
        except ValueError as ex:
            self.log.warning("init() args missing or error using default \
                             value seeing following exception print out.")
            self.log.warning("{}".format(ex))
            self._init_cals_mask = self._parse_and_convert_cal_args(
                INIT_CALIBRATION_TABLE, 'DEFAULT')
            self._tracking_cals_mask = self._parse_and_convert_cal_args(
                TRACKING_CALIBRATION_TABLE, 'DEFAULT')
            self._init_cals_timeout = self.mykonos.DEFAULT_INIT_CALS_TIMEOUT
        self.log.debug("args[init_cals]=0x{:02X}".format(self._init_cals_mask))
        self.log.debug("args[tracking_cals]=0x{:02X}".format(
            self._tracking_cals_mask))
        async_exec(self.mykonos, "setup_cal", self._init_cals_mask,
                   self._tracking_cals_mask, self._init_cals_timeout)

    def init_lo_source(self, args):
        """Set all LO

        This function will initialize all LO to user specified sources.
        If there's no source is specified, the default one will be used.

        Arguments:
            args {string:string} -- device arguments.
        """
        self.log.debug("Setting up LO source..")
        rx_lo_source = args.get("rx_lo_source", "internal")
        tx_lo_source = args.get("tx_lo_source", "internal")
        self.mykonos.set_lo_source("RX", rx_lo_source)
        self.mykonos.set_lo_source("TX", tx_lo_source)
        self.log.debug("RX LO source is set at {}".format(
            self.mykonos.get_lo_source("RX")))
        self.log.debug("TX LO source is set at {}".format(
            self.mykonos.get_lo_source("TX")))

    def init_jesd(self, jesdcore, args):
        """
        Bring up the JESD link between Mykonos and the N310.
        All clocks must be set up and stable before starting this routine.
        """
        jesdcore.check_core()

        # JESD Lane Rate only depends on the master_clock_rate selection, since all
        # other link parameters (LMFS,N) remain constant.
        L = 4
        M = 4
        F = 2
        S = 1
        N = 16
        new_rate = self.master_clock_rate * M * N * (10.0 / 8) / L / S
        self.log.trace("Calculated JESD204b lane rate is {} Gbps".format(
            new_rate / 1e9))
        self.set_jesd_rate(jesdcore, new_rate)

        self.log.trace("Pulsing Mykonos Hard Reset...")
        self.cpld.reset_mykonos()
        self.log.trace("Initializing Mykonos...")
        self.init_lo_source(args)
        self.mykonos.begin_initialization()
        # Multi-chip Sync requires two SYSREF pulses at least 17us apart.
        jesdcore.send_sysref_pulse()
        time.sleep(0.001)  # 17us... ish.
        jesdcore.send_sysref_pulse()
        async_exec(self.mykonos, "finish_initialization")
        # TODO:can we call this after JESD?
        self.init_rf_cal(args)
        self.log.trace("Starting JESD204b Link Initialization...")
        # Generally, enable the source before the sink. Start with the DAC side.
        self.log.trace("Starting FPGA framer...")
        jesdcore.init_framer()
        self.log.trace("Starting Mykonos deframer...")
        self.mykonos.start_jesd_rx()
        # Now for the ADC link. Note that the Mykonos framer will not start issuing CGS
        # characters until SYSREF is received by the framer. Therefore we enable the
        # framer in Mykonos and the FPGA, send a SYSREF pulse to everyone, and then
        # start the deframer in the FPGA.
        self.log.trace("Starting Mykonos framer...")
        self.mykonos.start_jesd_tx()
        jesdcore.enable_lmfc(True)
        jesdcore.send_sysref_pulse()
        # Allow a bit of time for SYSREF to reach Mykonos and then CGS to appear. In
        # several experiments this time requirement was only in the 100s of nanoseconds.
        time.sleep(0.001)
        self.log.trace("Starting FPGA deframer...")
        jesdcore.init_deframer()

        # Allow a bit of time for CGS/ILA to complete.
        time.sleep(0.100)
        error_flag = False
        if not jesdcore.get_framer_status():
            self.log.error("JESD204b FPGA Core Framer is not synced!")
            error_flag = True
        if not self.check_mykonos_deframer_status():
            self.log.error("Mykonos JESD204b Deframer is not synced!")
            error_flag = True
        if not jesdcore.get_deframer_status():
            self.log.error("JESD204b FPGA Core Deframer is not synced!")
            error_flag = True
        if not self.check_mykonos_framer_status():
            self.log.error("Mykonos JESD204b Framer is not synced!")
            error_flag = True
        if (self.mykonos.get_multichip_sync_status() & 0xB) != 0xB:
            self.log.error("Mykonos Multi-chip Sync failed!")
            error_flag = True
        if error_flag:
            raise RuntimeError(
                'JESD204B Link Initialization Failed. See MPM logs for details.'
            )
        self.log.debug("JESD204B Link Initialization & Training Complete")

    def check_mykonos_framer_status(self):
        " Return True if Mykonos Framer is in good state "
        rb = self.mykonos.get_framer_status()
        self.log.trace(
            "Mykonos Framer Status Register: 0x{:04X}".format(rb & 0xFF))
        tx_state = {0: 'CGS', 1: 'ILAS', 2: 'ADC Data'}[rb & 0b11]
        ilas_state = {
            0: 'CGS',
            1: '1st Multframe',
            2: '2nd Multframe',
            3: '3rd Multframe',
            4: '4th Multframe',
            5: 'Last Multframe',
            6: 'invalid state',
            7: 'ILAS Complete'
        }[(rb & 0b11100) >> 2]
        sysref_rx = (rb & (0b1 << 5)) > 0
        fifo_ptr_delta_changed = (rb & (0b1 << 6)) > 0
        sysref_phase_error = (rb & (0b1 << 7)) > 0
        # According to emails with ADI, fifo_ptr_delta_changed may be buggy.
        # Deterministic latency is still achieved even when this bit is toggled, so
        # ADI's recommendation is to ignore it. The expected state of this bit 0, but
        # occasionally it toggles to 1. It is unclear why exactly this happens.
        success = ((tx_state == 'ADC Data') & (ilas_state == 'ILAS Complete')
                   & sysref_rx & (not sysref_phase_error))
        logger = self.log.trace if success else self.log.warning
        logger("Mykonos Framer, TX State: %s", tx_state)
        logger("Mykonos Framer, ILAS State: %s", ilas_state)
        logger("Mykonos Framer, SYSREF Received: {}".format(sysref_rx))
        logger(
            "Mykonos Framer, FIFO Ptr Delta Change: {} (ignored, possibly buggy)"
            .format(fifo_ptr_delta_changed))
        logger("Mykonos Framer, SYSREF Phase Error: {}".format(
            sysref_phase_error))
        return success

    def check_mykonos_deframer_status(self):
        " Return True if Mykonos Deframer is in good state "
        rb = self.mykonos.get_deframer_status()
        self.log.trace(
            "Mykonos Deframer Status Register: 0x{:04X}".format(rb & 0xFF))

        frame_symbol_error = (rb & (0b1 << 0)) > 0
        ilas_multifrm_error = (rb & (0b1 << 1)) > 0
        ilas_framing_error = (rb & (0b1 << 2)) > 0
        ilas_checksum_valid = (rb & (0b1 << 3)) > 0
        prbs_error = (rb & (0b1 << 4)) > 0
        sysref_received = (rb & (0b1 << 5)) > 0
        deframer_irq = (rb & (0b1 << 6)) > 0
        success = ((not frame_symbol_error) & (not ilas_multifrm_error) &
                   (not ilas_framing_error) & ilas_checksum_valid &
                   (not prbs_error) & sysref_received & (not deframer_irq))
        logger = self.log.trace if success else self.log.warning
        logger("Mykonos Deframer, Frame Symbol Error: {}".format(
            frame_symbol_error))
        logger("Mykonos Deframer, ILAS Multiframe Error: {}".format(
            ilas_multifrm_error))
        logger("Mykonos Deframer, ILAS Frame Error: {}".format(
            ilas_framing_error))
        logger("Mykonos Deframer, ILAS Checksum Valid: {}".format(
            ilas_checksum_valid))
        logger("Mykonos Deframer, PRBS Error: {}".format(prbs_error))
        logger("Mykonos Deframer, SYSREF Received: {}".format(sysref_received))
        logger(
            "Mykonos Deframer, Deframer IRQ Received: {}".format(deframer_irq))
        return success

    def set_jesd_rate(self, jesdcore, new_rate, force=False):
        """
        Make the QPLL and GTX changes required to change the JESD204B core rate.
        """
        # The core is directly compiled for 125 MHz sample rate, which
        # corresponds to a lane rate of 2.5 Gbps. The same QPLL and GTX settings apply
        # for the 122.88 MHz sample rate.
        #
        # The higher LTE rate, 153.6 MHz, requires changes to the default configuration
        # of the MGT components. This function performs the required changes in the
        # following order (as recommended by UG476).
        #
        # 1) Modify any QPLL settings.
        # 2) Perform the QPLL reset routine by pulsing reset then waiting for lock.
        # 3) Modify any GTX settings.
        # 4) Perform the GTX reset routine by pulsing reset and waiting for reset done.

        assert new_rate in (2457.6e6, 2500e6, 3072e6)

        # On first run, we have no idea how the FPGA is configured... so let's force an
        # update to our rate.
        force = force or (self.current_jesd_rate is None)

        skip_drp = False
        if not force:
            #           Current     New       Skip?
            skip_drp = {
                2457.6e6: {
                    2457.6e6: True,
                    2500.0e6: True,
                    3072.0e6: False
                },
                2500.0e6: {
                    2457.6e6: True,
                    2500.0e6: True,
                    3072.0e6: False
                },
                3072.0e6: {
                    2457.6e6: False,
                    2500.0e6: False,
                    3072.0e6: True
                }
            }[self.current_jesd_rate][new_rate]

        if skip_drp:
            self.log.trace(
                "Current lane rate is compatible with the new rate. Skipping "
                "reconfiguration.")

        # These are the only registers in the QPLL and GTX that change based on the
        # selected line rate. The MGT wizard IP was generated for each of the rates and
        # reference clock frequencies and then diffed to create this table.
        QPLL_CFG = {
            2457.6e6: 0x680181,
            2500e6: 0x680181,
            3072e6: 0x06801C1
        }[new_rate]
        QPLL_FBDIV = {2457.6e6: 0x120, 2500e6: 0x120, 3072e6: 0x80}[new_rate]
        MGT_PMA_RSV = {
            2457.6e6: 0x1E7080,
            2500e6: 0x1E7080,
            3072e6: 0x18480
        }[new_rate]
        MGT_RX_CLK25_DIV = {2457.6e6: 5, 2500e6: 5, 3072e6: 7}[new_rate]
        MGT_TX_CLK25_DIV = {2457.6e6: 5, 2500e6: 5, 3072e6: 7}[new_rate]
        MGT_RXOUT_DIV = {2457.6e6: 4, 2500e6: 4, 3072e6: 2}[new_rate]
        MGT_TXOUT_DIV = {2457.6e6: 4, 2500e6: 4, 3072e6: 2}[new_rate]
        MGT_RXCDR_CFG = {
            2457.6e6: 0x03000023ff10100020,
            2500e6: 0x03000023ff10100020,
            3072e6: 0x03000023ff10200020
        }[new_rate]

        # 1-2) Do the QPLL first
        if not skip_drp:
            self.log.trace("Changing QPLL settings to support {} Gbps".format(
                new_rate / 1e9))
            jesdcore.set_drp_target('qpll', 0)
            # QPLL_CONFIG is spread across two regs: 0x32 (dedicated) and 0x33 (shared)
            reg_x32 = QPLL_CFG & 0xFFFF  # [16:0] -> [16:0]
            reg_x33 = jesdcore.drp_access(rd=True, addr=0x33)
            reg_x33 = (reg_x33 & 0xF800) | (
                (QPLL_CFG >> 16) & 0x7FF)  # [26:16] -> [11:0]
            jesdcore.drp_access(rd=False, addr=0x32, wr_data=reg_x32)
            jesdcore.drp_access(rd=False, addr=0x33, wr_data=reg_x33)
            # QPLL_FBDIV is shared with other settings in reg 0x36
            reg_x36 = jesdcore.drp_access(rd=True, addr=0x36)
            reg_x36 = (reg_x36 & 0xFC00) | (QPLL_FBDIV & 0x3FF
                                            )  # in bits [9:0]
            jesdcore.drp_access(rd=False, addr=0x36, wr_data=reg_x36)

        # Run the QPLL reset sequence and prep the MGTs for modification.
        jesdcore.init()

        # 3-4) And the 4 MGTs second
        if not skip_drp:
            self.log.trace("Changing MGT settings to support {} Gbps".format(
                new_rate / 1e9))
            for lane in range(4):
                jesdcore.set_drp_target('mgt', lane)
                # MGT_PMA_RSV is split over 0x99 (LSBs) and 0x9A
                reg_x99 = MGT_PMA_RSV & 0xFFFF
                reg_x9a = (MGT_PMA_RSV >> 16) & 0xFFFF
                jesdcore.drp_access(rd=False, addr=0x99, wr_data=reg_x99)
                jesdcore.drp_access(rd=False, addr=0x9A, wr_data=reg_x9a)
                # MGT_RX_CLK25_DIV is embedded with others in 0x11. The
                # encoding for the DRP register value is one less than the
                # desired value.
                reg_x11 = jesdcore.drp_access(rd=True, addr=0x11)
                reg_x11 = (reg_x11 & 0xF83F) | \
                          ((MGT_RX_CLK25_DIV-1 & 0x1F) << 6) # [10:6]
                jesdcore.drp_access(rd=False, addr=0x11, wr_data=reg_x11)
                # MGT_TX_CLK25_DIV is embedded with others in 0x6A. The
                # encoding for the DRP register value is one less than the
                # desired value.
                reg_x6a = jesdcore.drp_access(rd=True, addr=0x6A)
                reg_x6a = (reg_x6a & 0xFFE0) | (MGT_TX_CLK25_DIV - 1 & 0x1F
                                                )  # [4:0]
                jesdcore.drp_access(rd=False, addr=0x6A, wr_data=reg_x6a)
                # MGT_RXCDR_CFG is split over 0xA8 (LSBs) through 0xAD
                for reg_num, reg_addr in enumerate(range(0xA8, 0xAE)):
                    reg_data = (MGT_RXCDR_CFG >> 16 * reg_num) & 0xFFFF
                    jesdcore.drp_access(rd=False,
                                        addr=reg_addr,
                                        wr_data=reg_data)
                # MGT_RXOUT_DIV and MGT_TXOUT_DIV are embedded together in
                # 0x88. The encoding for the DRP register value is
                # drp_val=log2(attribute)
                reg_x88 = (int(math.log(MGT_RXOUT_DIV, 2)) & 0x7) | \
                         ((int(math.log(MGT_TXOUT_DIV, 2)) & 0x7) << 4) # RX=[2:0] TX=[6:4]
                jesdcore.drp_access(rd=False, addr=0x88, wr_data=reg_x88)
            self.log.trace("GTX settings changed to support {} Gbps".format(
                new_rate / 1e9))
            jesdcore.disable_drp_target()

        self.log.trace("JESD204b Lane Rate set to {} Gbps!".format(new_rate /
                                                                   1e9))
        self.current_jesd_rate = new_rate
        return

    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 won't terminate this process until the thread is complete.
        # This does not stop anyone from killing this process (and the thread)
        # while the EEPROM write is happening, though.

    def get_master_clock_rate(self):
        " Return master clock rate (== sampling rate) "
        return self.master_clock_rate

    def update_ref_clock_freq(self, freq):
        """
        Call this function if the frequency of the reference clock changes (the
        10, 20, 25 MHz one). Note: Won't actually re-run any settings.
        """
        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

    ##########################################################################
    # Sensors
    ##########################################################################
    def get_ref_lock(self):
        """
        Returns True if the LMK reference is locked.

        Note: This does not return a sensor dict. The sensor API call is
        in the motherboard class.
        """
        if self.lmk is None:
            self.log.trace("LMK object not yet initialized, defaulting to " \
                           "no ref locked!")
            return False
        lmk_lock_status = self.lmk.check_plls_locked()
        self.log.trace("LMK lock status is: {}".format(lmk_lock_status))
        return lmk_lock_status

    def get_lowband_lo_lock(self, which):
        """
        Return LO lock status (Boolean!) of the lowband LOs. 'which' must be
        either 'tx' or 'rx'
        """
        assert which.lower() in ('tx', 'rx')
        return self.cpld.get_lo_lock_status(which.upper())

    def get_ad9371_lo_lock(self, which):
        """
        Return LO lock status (Boolean!) of the lowband LOs. 'which' must be
        either 'tx' or 'rx'
        """
        return self.mykonos.get_lo_locked(which.upper())

    def get_lowband_tx_lo_locked_sensor(self, chan):
        " TX lowband LO lock sensor "
        self.log.trace("Querying TX lowband LO lock status for chan %d...",
                       chan)
        lock_status = self.get_lowband_lo_lock('tx')
        return {
            'name': 'lowband_lo_locked',
            'type': 'BOOLEAN',
            'unit': 'locked' if lock_status else 'unlocked',
            'value': str(lock_status).lower(),
        }

    def get_lowband_rx_lo_locked_sensor(self, chan):
        " RX lowband LO lock sensor "
        self.log.trace("Querying RX lowband LO lock status for chan %d...",
                       chan)
        lock_status = self.get_lowband_lo_lock('rx')
        return {
            'name': 'lowband_lo_locked',
            'type': 'BOOLEAN',
            'unit': 'locked' if lock_status else 'unlocked',
            'value': str(lock_status).lower(),
        }

    def get_ad9371_tx_lo_locked_sensor(self, chan):
        " TX ad9371 LO lock sensor "
        self.log.trace("Querying TX AD9371 LO lock status for chan %d...",
                       chan)
        lock_status = self.get_ad9371_lo_lock('tx')
        return {
            'name': 'ad9371_lo_locked',
            'type': 'BOOLEAN',
            'unit': 'locked' if lock_status else 'unlocked',
            'value': str(lock_status).lower(),
        }

    def get_ad9371_rx_lo_locked_sensor(self, chan):
        " RX ad9371 LO lock sensor "
        self.log.trace("Querying RX AD9371 LO lock status for chan %d...",
                       chan)
        lock_status = self.get_ad9371_lo_lock('tx')
        return {
            'name': 'ad9371_lo_locked',
            'type': 'BOOLEAN',
            'unit': 'locked' if lock_status else 'unlocked',
            'value': str(lock_status).lower(),
        }

    ##########################################################################
    # Debug
    ##########################################################################
    def cpld_peek(self, addr):
        """
        Debug for accessing the CPLD via the RPC shell.
        """
        return self.cpld.peek16(addr)

    def cpld_poke(self, addr, data):
        """
        Debug for accessing the CPLD via the RPC shell.
        """
        self.cpld.poke16(addr, data)
        return self.cpld.peek16(addr)

    def dump_jesd_core(self):
        " Debug method to dump all JESD core regs "
        with open_uio(label="dboard-regs-{}".format(self.slot_idx),
                      read_only=False) as dboard_ctrl_regs:
            for i in range(0x2000, 0x2110, 0x10):
                print(("0x%04X " % i), end=' ')
                for j in range(0, 0x10, 0x4):
                    print(("%08X" % dboard_ctrl_regs.peek32(i + j)), end=' ')
                print("")

    def dbcore_peek(self, addr):
        """
        Debug for accessing the DB Core registers via the RPC shell.
        """
        with open_uio(label="dboard-regs-{}".format(self.slot_idx),
                      read_only=False) as dboard_ctrl_regs:
            rd_data = dboard_ctrl_regs.peek32(addr)
            self.log.trace(
                "DB Core Register 0x{:04X} response: 0x{:08X}".format(
                    addr, rd_data))
            return rd_data

    def dbcore_poke(self, addr, data):
        """
        Debug for accessing the DB Core registers via the RPC shell.
        """
        with open_uio(label="dboard-regs-{}".format(self.slot_idx),
                      read_only=False) as dboard_ctrl_regs:
            self.log.trace(
                "Writing DB Core Register 0x{:04X} with 0x{:08X}...".format(
                    addr, data))
            dboard_ctrl_regs.poke32(addr, data)
Exemple #5
0
class Magnesium(BfrfsEEPROM, DboardManagerBase):
    """
    Holds all dboard specific information and methods of the magnesium dboard
    """
    #########################################################################
    # Overridables
    #
    # See DboardManagerBase for documentation on these fields
    #########################################################################
    pids = [0x150]
    rx_sensor_callback_map = {
        'lowband_lo_locked': 'get_lowband_tx_lo_locked_sensor',
        'ad9371_lo_locked': 'get_ad9371_tx_lo_locked_sensor',
    }
    tx_sensor_callback_map = {
        'lowband_lo_locked': 'get_lowband_rx_lo_locked_sensor',
        'ad9371_lo_locked': 'get_ad9371_rx_lo_locked_sensor',
    }
    # Maps the chipselects to the corresponding devices:
    spi_chipselect = {"cpld": 0, "lmk": 1, "mykonos": 2, "phase_dac": 3}
    ### End of overridables #################################################
    # Class-specific, but constant settings:
    spi_factories = {
        "cpld": create_spidev_iface_cpld,
        "lmk": create_spidev_iface_lmk,
        "phase_dac": create_spidev_iface_phasedac,
    }
    #file system path to i2c-adapter/mux
    base_i2c_adapter = '/sys/class/i2c-adapter'
    # Map I2C channel to slot index
    i2c_chan_map = {0: 'i2c-9', 1: 'i2c-10'}
    # This map describes how the user data is stored in EEPROM. If a dboard rev
    # changes the way the EEPROM is used, we add a new entry. If a dboard rev
    # is not found in the map, then we go backward until we find a suitable rev
    user_eeprom = {
        2: {  # RevC
            'label': "e0004000.i2c",
            'offset': 1024,
            'max_size': 32786 - 1024,
            'alignment': 1024,  # FIXME check alignment is correct (block size)
        },
    }
    default_master_clock_rate = 125e6
    default_time_source = 'internal'
    default_current_jesd_rate = 2500e6

    def __init__(self, slot_idx, **kwargs):
        DboardManagerBase.__init__(self, slot_idx, **kwargs)
        self.log = get_logger("Magnesium-{}".format(slot_idx))
        self.log.trace("Initializing Magnesium 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.current_jesd_rate = None
        # Predeclare some attributes to make linter happy:
        self.lmk = None
        self._port_expander = None
        self.mykonos = None
        self.eeprom_fs = None
        self.eeprom_path = 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
        """
        self._port_expander = TCA6408(self._get_i2c_dev(self.slot_idx))
        self._power_on()
        self.log.debug("Loading C++ drivers...")

        # The Mykonos TX DeFramer lane crossbar requires configuration on a per-slot
        # basis due to motherboard MGT lane swapping.
        # The RX framer lane crossbar configuration
        # is identical for both slots and is hard-coded within the Mykonos API.
        deserializer_lane_xbar = 0xD2 if self.slot_idx == 0 else 0x72

        self._device = lib.dboards.magnesium_manager(
            self._spi_nodes['mykonos'], deserializer_lane_xbar)
        self.mykonos = self._device.get_radio_ctrl()
        self.spi_lock = self._device.get_spi_lock()
        self.log.trace("Loaded C++ drivers.")
        self._init_myk_api(self.mykonos)
        self.log.debug("AD9371: ARM version: {arm_ver} API version: {api_ver} "
                       "Device revision: {dev_rev}".format(
                           arm_ver=self.get_arm_version(),
                           api_ver=self.get_api_version(),
                           dev_rev=self.get_device_rev(),
                       ))
        BfrfsEEPROM.__init__(self)
        self.log.trace("Loading SPI devices...")
        self._spi_ifaces = {
            key: self.spi_factories[key](self._spi_nodes[key])
            for key in self.spi_factories
        }
        self.cpld = MgCPLD(self._spi_ifaces['cpld'], self.log)
        self.device_info['cpld_rev'] = \
                str(self.cpld.major_rev) + '.' + str(self.cpld.minor_rev)

    def _power_on(self):
        " Turn on power to daughterboard "
        self.log.trace("Powering on slot_idx={}...".format(self.slot_idx))
        self._port_expander.set("PWR-EN-3.6V")
        self._port_expander.set("PWR-EN-1.5V")
        self._port_expander.set("PWR-EN-5.5V")
        self._port_expander.set("LED")

    def _power_off(self):
        " Turn off power to daughterboard "
        self.log.trace("Powering off slot_idx={}...".format(self.slot_idx))
        self._port_expander.reset("PWR-EN-3.6V")
        self._port_expander.reset("PWR-EN-1.5V")
        self._port_expander.reset("PWR-EN-5.5V")
        self._port_expander.reset("LED")

    def _get_i2c_dev(self, slot_idx):
        " 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[slot_idx])
        return pyudev.Devices.from_sys_path(context, i2c_dev_path)

    def _init_myk_api(self, myk):
        """
        Propagate the C++ Mykonos API into Python land.
        """
        def export_method(obj, method):
            " Export a method object, including docstring "
            meth_obj = getattr(obj, method)

            def func(*args):
                " Functor for storing docstring too "
                return meth_obj(*args)

            func.__doc__ = meth_obj.__doc__
            return func

        self.log.trace("Forwarding AD9371 methods to Magnesium class...")
        for method in [
                x for x in dir(self.mykonos)
                if not x.startswith("_") and \
                        callable(getattr(self.mykonos, x))]:
            self.log.trace("adding {}".format(method))
            setattr(self, method, export_method(myk, method))

    def init(self, args):
        """
        Execute necessary init dance to bring up dboard
        """
        # Sanity checks and input validation:
        self.log.debug("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 = float(args['ref_clk_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)
        master_clock_rate = \
            float(args.get('master_clock_rate',
                           self.default_master_clock_rate))
        assert master_clock_rate in (122.88e6, 125e6, 153.6e6), \
                "Invalid master clock rate: {:.02f} MHz".format(
                    master_clock_rate / 1e6)
        master_clock_rate_changed = \
            master_clock_rate != self.master_clock_rate
        if master_clock_rate_changed:
            self.master_clock_rate = master_clock_rate
            self.log.debug("Updating master clock rate to {:.02f} MHz!".format(
                self.master_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={}".format(
                    self.master_clock_rate / 1e6,
                    self.ref_clock_freq,
                ))
        # Note: MagnesiumInitManager.init() can still override fast_reinit.
        # Consider it a hint.
        result = MagnesiumInitManager(self, self._spi_ifaces).init(
            args, self._init_args, fast_reinit)
        if result:
            self._init_args = args
        return result

    ##########################################################################
    # 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.

        The only downstream receiver of the clock that is not reset here are the
        lowband LOs, which are controlled through the host UHD interface.
        """
        if self._init_args is None:
            # Then we're already in a safe state
            return
        # Reset Mykonos, since it receives a copy of the clock from the LMK.
        self.cpld.reset_mykonos(keep_in_reset=True)
        with open_uio(label="dboard-regs-{}".format(self.slot_idx),
                      read_only=False) as dboard_ctrl_regs:
            # Clear the Sample Clock enables and place the MMCM in reset.
            db_clk_control = DboardClockControl(dboard_ctrl_regs, self.log)
            db_clk_control.reset_mmcm()
            # Place the JESD204b core in reset, mainly to reset QPLL/CPLLs.
            jesdcore = nijesdcore.NIJESDCore(
                dboard_ctrl_regs, self.slot_idx,
                **MagnesiumInitManager.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) "
        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._init_args = {**self._init_args, **kwargs}
            self.log.info(
                "Re-initializing daughter board. This may take some time.")
            self._reinit(self.master_clock_rate)
            self.log.debug("Daughter board re-initialization done.")

    ##########################################################################
    # Sensors
    ##########################################################################
    def get_ref_lock(self):
        """
        Returns True if the LMK reference is locked.

        Note: This does not return a sensor dict. The sensor API call is
        in the motherboard class.
        """
        if self.lmk is None:
            self.log.trace("LMK object not yet initialized, defaulting to " \
                           "no ref locked!")
            return False
        lmk_lock_status = self.lmk.check_plls_locked()
        self.log.trace("LMK lock status is: {}".format(lmk_lock_status))
        return lmk_lock_status

    def get_lowband_lo_lock(self, which):
        """
        Return LO lock status (Boolean!) of the lowband LOs. 'which' must be
        either 'tx' or 'rx'
        """
        assert which.lower() in ('tx', 'rx')
        return self.cpld.get_lo_lock_status(which.upper())

    def get_ad9371_lo_lock(self, which):
        """
        Return LO lock status (Boolean!) of the lowband LOs. 'which' must be
        either 'tx' or 'rx'
        """
        return self.mykonos.get_lo_locked(which.upper())

    def get_lowband_tx_lo_locked_sensor(self, chan):
        " TX lowband LO lock sensor "
        self.log.trace("Querying TX lowband LO lock status for chan %d...",
                       chan)
        lock_status = self.get_lowband_lo_lock('tx')
        return {
            'name': 'lowband_lo_locked',
            'type': 'BOOLEAN',
            'unit': 'locked' if lock_status else 'unlocked',
            'value': str(lock_status).lower(),
        }

    def get_lowband_rx_lo_locked_sensor(self, chan):
        " RX lowband LO lock sensor "
        self.log.trace("Querying RX lowband LO lock status for chan %d...",
                       chan)
        lock_status = self.get_lowband_lo_lock('rx')
        return {
            'name': 'lowband_lo_locked',
            'type': 'BOOLEAN',
            'unit': 'locked' if lock_status else 'unlocked',
            'value': str(lock_status).lower(),
        }

    def get_ad9371_tx_lo_locked_sensor(self, chan):
        " TX ad9371 LO lock sensor "
        self.log.trace("Querying TX AD9371 LO lock status for chan %d...",
                       chan)
        lock_status = self.get_ad9371_lo_lock('tx')
        return {
            'name': 'ad9371_lo_locked',
            'type': 'BOOLEAN',
            'unit': 'locked' if lock_status else 'unlocked',
            'value': str(lock_status).lower(),
        }

    def get_ad9371_rx_lo_locked_sensor(self, chan):
        " RX ad9371 LO lock sensor "
        self.log.trace("Querying RX AD9371 LO lock status for chan %d...",
                       chan)
        lock_status = self.get_ad9371_lo_lock('tx')
        return {
            'name': 'ad9371_lo_locked',
            'type': 'BOOLEAN',
            'unit': 'locked' if lock_status else 'unlocked',
            'value': str(lock_status).lower(),
        }

    ##########################################################################
    # Debug
    ##########################################################################
    def cpld_peek(self, addr):
        """
        Debug for accessing the CPLD via the RPC shell.
        """
        return self.cpld.peek16(addr)

    def cpld_poke(self, addr, data):
        """
        Debug for accessing the CPLD via the RPC shell.
        """
        self.cpld.poke16(addr, data)
        return self.cpld.peek16(addr)

    def dump_jesd_core(self):
        " Debug method to dump all JESD core regs "
        with open_uio(label="dboard-regs-{}".format(self.slot_idx),
                      read_only=False) as dboard_ctrl_regs:
            for i in range(0x2000, 0x2110, 0x10):
                print(("0x%04X " % i), end=' ')
                for j in range(0, 0x10, 0x4):
                    print(("%08X" % dboard_ctrl_regs.peek32(i + j)), end=' ')
                print("")

    def dbcore_peek(self, addr):
        """
        Debug for accessing the DB Core registers via the RPC shell.
        """
        with open_uio(label="dboard-regs-{}".format(self.slot_idx),
                      read_only=False) as dboard_ctrl_regs:
            rd_data = dboard_ctrl_regs.peek32(addr)
            self.log.trace(
                "DB Core Register 0x{:04X} response: 0x{:08X}".format(
                    addr, rd_data))
            return rd_data

    def dbcore_poke(self, addr, data):
        """
        Debug for accessing the DB Core registers via the RPC shell.
        """
        with open_uio(label="dboard-regs-{}".format(self.slot_idx),
                      read_only=False) as dboard_ctrl_regs:
            self.log.trace(
                "Writing DB Core Register 0x{:04X} with 0x{:08X}...".format(
                    addr, data))
            dboard_ctrl_regs.poke32(addr, data)
Exemple #6
0
class Magnesium(DboardManagerBase):
    """
    Holds all dboard specific information and methods of the magnesium dboard
    """
    #########################################################################
    # Overridables
    #
    # See DboardManagerBase for documentation on these fields
    #########################################################################
    pids = [0x150]
    rx_sensor_callback_map = {
        'lowband_lo_locked': 'get_lowband_tx_lo_locked_sensor',
        'ad9371_lo_locked': 'get_ad9371_tx_lo_locked_sensor',
    }
    tx_sensor_callback_map = {
        'lowband_lo_locked': 'get_lowband_rx_lo_locked_sensor',
        'ad9371_lo_locked': 'get_ad9371_rx_lo_locked_sensor',
    }
    # Maps the chipselects to the corresponding devices:
    spi_chipselect = {"cpld": 0, "lmk": 1, "mykonos": 2, "phase_dac": 3}
    ### End of overridables #################################################
    # Class-specific, but constant settings:
    spi_factories = {
        "cpld": create_spidev_iface_cpld,
        "lmk": create_spidev_iface_lmk,
        "phase_dac": create_spidev_iface_phasedac,
    }
    #file system path to i2c-adapter/mux
    base_i2c_adapter = '/sys/class/i2c-adapter'
    # Map I2C channel to slot index
    i2c_chan_map = {0: 'i2c-9', 1: 'i2c-10'}
    # This map describes how the user data is stored in EEPROM. If a dboard rev
    # changes the way the EEPROM is used, we add a new entry. If a dboard rev
    # is not found in the map, then we go backward until we find a suitable rev
    user_eeprom = {
        2: { # RevC
            'label': "e0004000.i2c",
            'offset': 1024,
            'max_size': 32786 - 1024,
            'alignment': 1024,
        },
    }
    default_master_clock_rate = 125e6
    default_time_source = 'internal'
    default_current_jesd_rate = 2500e6

    def __init__(self, slot_idx, **kwargs):
        super(Magnesium, self).__init__(slot_idx, **kwargs)
        self.log = get_logger("Magnesium-{}".format(slot_idx))
        self.log.trace("Initializing Magnesium 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.current_jesd_rate = None
        # Predeclare some attributes to make linter happy:
        self.lmk = None
        self._port_expander = None
        self.mykonos = None
        self.eeprom_fs = None
        self.eeprom_path = 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
        """
        self._port_expander = TCA6408(self._get_i2c_dev(self.slot_idx))
        self._power_on()
        self.log.debug("Loading C++ drivers...")

        # The Mykonos TX DeFramer lane crossbar requires configuration on a per-slot
        # basis due to motherboard MGT lane swapping.
        # The RX framer lane crossbar configuration
        # is identical for both slots and is hard-coded within the Mykonos API.
        deserializer_lane_xbar = 0xD2 if self.slot_idx == 0 else 0x72

        self._device = lib.dboards.magnesium_manager(
            self._spi_nodes['mykonos'],
            deserializer_lane_xbar
        )
        self.mykonos = self._device.get_radio_ctrl()
        self.spi_lock = self._device.get_spi_lock()
        self.log.trace("Loaded C++ drivers.")
        self._init_myk_api(self.mykonos)
        self.log.debug(
            "AD9371: ARM version: {arm_ver} API version: {api_ver} "
            "Device revision: {dev_rev}".format(
                arm_ver=self.get_arm_version(),
                api_ver=self.get_api_version(),
                dev_rev=self.get_device_rev(),
            )
        )
        self.eeprom_fs, self.eeprom_path = self._init_user_eeprom(
            self._get_user_eeprom_info(self.rev)
        )
        self.log.trace("Loading SPI devices...")
        self._spi_ifaces = {
            key: self.spi_factories[key](self._spi_nodes[key])
            for key in self.spi_factories
        }
        self.cpld = MgCPLD(self._spi_ifaces['cpld'], self.log)
        self.device_info['cpld_rev'] = \
                str(self.cpld.major_rev) + '.' + str(self.cpld.minor_rev)

    def _power_on(self):
        " Turn on power to daughterboard "
        self.log.trace("Powering on slot_idx={}...".format(self.slot_idx))
        self._port_expander.set("PWR-EN-3.6V")
        self._port_expander.set("PWR-EN-1.5V")
        self._port_expander.set("PWR-EN-5.5V")
        self._port_expander.set("LED")

    def _power_off(self):
        " Turn off power to daughterboard "
        self.log.trace("Powering off slot_idx={}...".format(self.slot_idx))
        self._port_expander.reset("PWR-EN-3.6V")
        self._port_expander.reset("PWR-EN-1.5V")
        self._port_expander.reset("PWR-EN-5.5V")
        self._port_expander.reset("LED")

    def _get_i2c_dev(self, slot_idx):
        " 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[slot_idx]
        )
        return pyudev.Devices.from_sys_path(context, i2c_dev_path)

    def _init_myk_api(self, myk):
        """
        Propagate the C++ Mykonos API into Python land.
        """
        def export_method(obj, method):
            " Export a method object, including docstring "
            meth_obj = getattr(obj, method)
            def func(*args):
                " Functor for storing docstring too "
                return meth_obj(*args)
            func.__doc__ = meth_obj.__doc__
            return func
        self.log.trace("Forwarding AD9371 methods to Magnesium class...")
        for method in [
                x for x in dir(self.mykonos)
                if not x.startswith("_") and \
                        callable(getattr(self.mykonos, x))]:
            self.log.trace("adding {}".format(method))
            setattr(self, method, export_method(myk, method))

    def _get_user_eeprom_info(self, rev):
        """
        Return an EEPROM access map (from self.user_eeprom) based on the rev.
        """
        rev_for_lookup = rev
        while rev_for_lookup not in self.user_eeprom:
            if rev_for_lookup < 0:
                raise RuntimeError("Could not find a user EEPROM map for "
                                   "revision %d!", rev)
            rev_for_lookup -= 1
        assert rev_for_lookup in self.user_eeprom, \
                "Invalid EEPROM lookup rev!"
        return self.user_eeprom[rev_for_lookup]

    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.debug("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 = float(args['ref_clk_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)
        master_clock_rate = \
            float(args.get('master_clock_rate',
                           self.default_master_clock_rate))
        assert master_clock_rate in (122.88e6, 125e6, 153.6e6), \
                "Invalid master clock rate: {:.02f} MHz".format(
                    master_clock_rate / 1e6)
        master_clock_rate_changed = \
            master_clock_rate != self.master_clock_rate
        if master_clock_rate_changed:
            self.master_clock_rate = master_clock_rate
            self.log.debug(
                "Updating master clock rate to {:.02f} MHz!"
                .format(self.master_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={}"
                .format(
                    self.master_clock_rate / 1e6,
                    self.ref_clock_freq,
                )
            )
        # Note: MagnesiumInitManager.init() can still override fast_reinit.
        # Consider it a hint.
        result = MagnesiumInitManager(self, self._spi_ifaces).init(
            args, self._init_args, fast_reinit)
        if result:
            self._init_args = args
        return 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 won't terminate this process 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.

        The only downstream receiver of the clock that is not reset here are the
        lowband LOs, which are controlled through the host UHD interface.
        """
        if self._init_args is None:
            # Then we're already in a safe state
            return
        # Reset Mykonos, since it receives a copy of the clock from the LMK.
        self.cpld.reset_mykonos(keep_in_reset=True)
        with open_uio(
            label="dboard-regs-{}".format(self.slot_idx),
            read_only=False
        ) as dboard_ctrl_regs:
            # Clear the Sample Clock enables and place the MMCM in reset.
            db_clk_control = DboardClockControl(dboard_ctrl_regs, self.log)
            db_clk_control.reset_mmcm()
            # Place the JESD204b core in reset, mainly to reset QPLL/CPLLs.
            jesdcore = nijesdcore.NIJESDCore(dboard_ctrl_regs, self.slot_idx,
                                             **MagnesiumInitManager.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) "
        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._init_args = {**self._init_args, **kwargs}
            self.log.info("Re-initializing daughter board. This may take some time.")
            self._reinit(self.master_clock_rate)
            self.log.debug("Daughter board re-initialization done.")


    ##########################################################################
    # Sensors
    ##########################################################################
    def get_ref_lock(self):
        """
        Returns True if the LMK reference is locked.

        Note: This does not return a sensor dict. The sensor API call is
        in the motherboard class.
        """
        if self.lmk is None:
            self.log.trace("LMK object not yet initialized, defaulting to " \
                           "no ref locked!")
            return False
        lmk_lock_status = self.lmk.check_plls_locked()
        self.log.trace("LMK lock status is: {}".format(lmk_lock_status))
        return lmk_lock_status

    def get_lowband_lo_lock(self, which):
        """
        Return LO lock status (Boolean!) of the lowband LOs. 'which' must be
        either 'tx' or 'rx'
        """
        assert which.lower() in ('tx', 'rx')
        return self.cpld.get_lo_lock_status(which.upper())

    def get_ad9371_lo_lock(self, which):
        """
        Return LO lock status (Boolean!) of the lowband LOs. 'which' must be
        either 'tx' or 'rx'
        """
        return self.mykonos.get_lo_locked(which.upper())

    def get_lowband_tx_lo_locked_sensor(self, chan):
        " TX lowband LO lock sensor "
        self.log.trace("Querying TX lowband LO lock status for chan %d...",
                       chan)
        lock_status = self.get_lowband_lo_lock('tx')
        return {
            'name': 'lowband_lo_locked',
            'type': 'BOOLEAN',
            'unit': 'locked' if lock_status else 'unlocked',
            'value': str(lock_status).lower(),
        }

    def get_lowband_rx_lo_locked_sensor(self, chan):
        " RX lowband LO lock sensor "
        self.log.trace("Querying RX lowband LO lock status for chan %d...",
                       chan)
        lock_status = self.get_lowband_lo_lock('rx')
        return {
            'name': 'lowband_lo_locked',
            'type': 'BOOLEAN',
            'unit': 'locked' if lock_status else 'unlocked',
            'value': str(lock_status).lower(),
        }

    def get_ad9371_tx_lo_locked_sensor(self, chan):
        " TX ad9371 LO lock sensor "
        self.log.trace("Querying TX AD9371 LO lock status for chan %d...", chan)
        lock_status = self.get_ad9371_lo_lock('tx')
        return {
            'name': 'ad9371_lo_locked',
            'type': 'BOOLEAN',
            'unit': 'locked' if lock_status else 'unlocked',
            'value': str(lock_status).lower(),
        }

    def get_ad9371_rx_lo_locked_sensor(self, chan):
        " RX ad9371 LO lock sensor "
        self.log.trace("Querying RX AD9371 LO lock status for chan %d...", chan)
        lock_status = self.get_ad9371_lo_lock('tx')
        return {
            'name': 'ad9371_lo_locked',
            'type': 'BOOLEAN',
            'unit': 'locked' if lock_status else 'unlocked',
            'value': str(lock_status).lower(),
        }


    ##########################################################################
    # Debug
    ##########################################################################
    def cpld_peek(self, addr):
        """
        Debug for accessing the CPLD via the RPC shell.
        """
        return self.cpld.peek16(addr)

    def cpld_poke(self, addr, data):
        """
        Debug for accessing the CPLD via the RPC shell.
        """
        self.cpld.poke16(addr, data)
        return self.cpld.peek16(addr)

    def dump_jesd_core(self):
        " Debug method to dump all JESD core regs "
        with open_uio(
            label="dboard-regs-{}".format(self.slot_idx),
            read_only=False
        ) as dboard_ctrl_regs:
            for i in range(0x2000, 0x2110, 0x10):
                print(("0x%04X " % i), end=' ')
                for j in range(0, 0x10, 0x4):
                    print(("%08X" % dboard_ctrl_regs.peek32(i + j)), end=' ')
                print("")

    def dbcore_peek(self, addr):
        """
        Debug for accessing the DB Core registers via the RPC shell.
        """
        with open_uio(
            label="dboard-regs-{}".format(self.slot_idx),
            read_only=False
        ) as dboard_ctrl_regs:
            rd_data = dboard_ctrl_regs.peek32(addr)
            self.log.trace("DB Core Register 0x{:04X} response: 0x{:08X}".format(addr, rd_data))
            return rd_data

    def dbcore_poke(self, addr, data):
        """
        Debug for accessing the DB Core registers via the RPC shell.
        """
        with open_uio(
            label="dboard-regs-{}".format(self.slot_idx),
            read_only=False
        ) as dboard_ctrl_regs:
            self.log.trace("Writing DB Core Register 0x{:04X} with 0x{:08X}...".format(addr, data))
            dboard_ctrl_regs.poke32(addr, data)