diff options
| author | Brent Stapleton <brent.stapleton@ettus.com> | 2018-04-17 19:33:23 -0700 | 
|---|---|---|
| committer | Brent Stapleton <bstapleton@g.hmc.edu> | 2018-07-18 15:37:27 -0700 | 
| commit | 300a5e3f6e5e845b4b8d093222e1c51ca4640f79 (patch) | |
| tree | 62f8bb7bc7d847b8f32f1fe5b4c9c06ef608080e /mpm/python/usrp_mpm/periph_manager | |
| parent | a1d6530ce50ca9590739ffa40464747d3db968eb (diff) | |
| download | uhd-300a5e3f6e5e845b4b8d093222e1c51ca4640f79.tar.gz uhd-300a5e3f6e5e845b4b8d093222e1c51ca4640f79.tar.bz2 uhd-300a5e3f6e5e845b4b8d093222e1c51ca4640f79.zip | |
mpm: initial commit of E320 code
Co-authored-by: Sugandha Gupta <sugandha.gupta@ettus.com>
Diffstat (limited to 'mpm/python/usrp_mpm/periph_manager')
| -rw-r--r-- | mpm/python/usrp_mpm/periph_manager/CMakeLists.txt | 8 | ||||
| -rw-r--r-- | mpm/python/usrp_mpm/periph_manager/e320.py | 671 | ||||
| -rw-r--r-- | mpm/python/usrp_mpm/periph_manager/e320_periphs.py | 383 | 
3 files changed, 1059 insertions, 3 deletions
| diff --git a/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt b/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt index 095916ff7..0689cdda9 100644 --- a/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt +++ b/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt @@ -1,7 +1,7 @@  # -# Copyright 2017 Ettus Research, National Instruments Company +# Copyright 2017-2018 Ettus Research, a National Instruments Company  # -# SPDX-License-Identifier: GPL-3.0 +# SPDX-License-Identifier: GPL-3.0-or-later  #  ######################################################################## @@ -13,6 +13,8 @@ SET(USRP_MPM_PERIPHMGR_FILES      ${CMAKE_CURRENT_SOURCE_DIR}/base.py      ${CMAKE_CURRENT_SOURCE_DIR}/n3xx.py      ${CMAKE_CURRENT_SOURCE_DIR}/n3xx_periphs.py -) +    ${CMAKE_CURRENT_SOURCE_DIR}/e320.py +    ${CMAKE_CURRENT_SOURCE_DIR}/e320_periphs.py +    )  LIST(APPEND USRP_MPM_FILES ${USRP_MPM_PERIPHMGR_FILES})  SET(USRP_MPM_FILES ${USRP_MPM_FILES} PARENT_SCOPE) diff --git a/mpm/python/usrp_mpm/periph_manager/e320.py b/mpm/python/usrp_mpm/periph_manager/e320.py new file mode 100644 index 000000000..ac50909ff --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/e320.py @@ -0,0 +1,671 @@ +# +# Copyright 2018 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +E320 implementation module +""" + +from __future__ import print_function +import bisect +import copy +import threading +from six import iteritems, itervalues +from usrp_mpm.components import ZynqComponents +from usrp_mpm.dboard_manager import Neon +from usrp_mpm.mpmtypes import SID +from usrp_mpm.mpmutils import assert_compat_number, str2bool +from usrp_mpm.periph_manager import PeriphManagerBase +from usrp_mpm.rpc_server import no_rpc +from usrp_mpm.sys_utils import dtoverlay +from usrp_mpm.sys_utils.udev import get_spidev_nodes +from usrp_mpm.xports import XportMgrUDP, XportMgrLiberio +from usrp_mpm.periph_manager.e320_periphs import MboardRegsControl + +E320_DEFAULT_INT_CLOCK_FREQ = 20e6 +E320_DEFAULT_EXT_CLOCK_FREQ = 10e6 +E320_DEFAULT_CLOCK_SOURCE = 'internal' +E320_DEFAULT_TIME_SOURCE = 'internal' +E320_DEFAULT_ENABLE_GPS = True +E320_DEFAULT_FPGPIO_VOLTAGE = 0 +E320_FPGA_COMPAT = (3, 0) +E320_MONITOR_THREAD_INTERVAL = 1.0 # seconds  # TODO Verify this +E320_DBOARD_SLOT_IDX = 0 + + +############################################################################### +# Transport managers +############################################################################### +class E320XportMgrUDP(XportMgrUDP): +    "E320-specific UDP configuration" +    xbar_dev = "/dev/crossbar0" +    iface_config = { +        'sfp0': { +            'label': 'misc-enet-regs', +            'xbar': 0, +            'xbar_port': 0, +            'ctrl_src_addr': 0, +        } +    } + +class E320XportMgrLiberio(XportMgrLiberio): +    " E320-specific Liberio configuration " +    max_chan = 6 +    xbar_dev = "/dev/crossbar0" +    xbar_port = 1 + +############################################################################### +# Main Class +############################################################################### +class e320(ZynqComponents, PeriphManagerBase): +    """ +    Holds E320 specific attributes and methods +    """ +    ######################################################################### +    # Overridables +    # +    # See PeriphManagerBase for documentation on these fields +    ######################################################################### +    description = "E300-Series Device" +    pids = {0xe320: 'e320'} +    mboard_eeprom_addr = "e0004000.i2c" +    mboard_eeprom_offset = 0 +    mboard_eeprom_max_len = 256 +    mboard_info = {"type": "e3xx", +                   "product": "e320" +                  } +    mboard_max_rev = 2  # RevB +    mboard_sensor_callback_map = { +        # FIXME add sensors +    } +    max_num_dboards = 1 +    crossbar_base_port = 2  # It's 2 because 0,1 are SFP,DMA + +    # We're on a Zynq target, so the following two come from the Zynq standard +    # device tree overlay (tree/arch/arm/boot/dts/zynq-7000.dtsi) +    dboard_spimaster_addrs = ["e0006000.spi", "e0007000.spi"] +    # E320-specific settings +    # Label for the mboard UIO +    mboard_regs_label = "mboard-regs" +    # Override the list of updateable components +    updateable_components = { +        'fpga': { +            'callback': "update_fpga", +            'path': '/lib/firmware/{}.bin', +            'reset': True, +        }, +        'dts': { +            'callback': "update_dts", +            'path': '/lib/firmware/{}.dts', +            'output': '/lib/firmware/{}.dtbo', +            'reset': False, +        }, +    } + +    @staticmethod +    def list_required_dt_overlays(device_info): +        """ +        Lists device tree overlays that need to be applied before this class can +        be used. List of strings. +        Are applied in order. + +        eeprom_md -- Dictionary of info read out from the mboard EEPROM +        device_args -- Arbitrary dictionary of info, typically user-defined +        """ +        return [device_info['product']] + +    ########################################################################### +    # Ctor and device initialization tasks +    ########################################################################### +    def __init__(self, args): +        super(e320, self).__init__(args) +        if not self._device_initialized: +            # Don't try and figure out what's going on. Just give up. +            return +        self._tear_down = False +        self._status_monitor_thread = None +        self._ext_clock_freq = E320_DEFAULT_EXT_CLOCK_FREQ +        self._clock_source = None +        self._time_source = None +        self._available_endpoints = list(range(256)) +        self.dboard = self.dboards[E320_DBOARD_SLOT_IDX] +        try: +            self._init_peripherals(args) +        except Exception as ex: +            self.log.error("Failed to initialize motherboard: %s", str(ex)) +            self._initialization_status = str(ex) +            self._device_initialized = False + +    def _init_dboards(self, _, override_dboard_pids, default_args): +        """ +        Initialize all the daughterboards + +        (dboard_infos) -- N/A +        override_dboard_pids -- List of dboard PIDs to force +        default_args -- Default args +        """ +        # Override the base class's implementation in order to avoid initializing our one "dboard" +        # in the same way that, for example, N310's dboards are initialized. Specifically, +        # - skip dboard EEPROM setup (we don't have one) +        # - change the way we handle SPI devices +        if override_dboard_pids: +            self.log.warning("Overriding daughterboard PIDs with: {}" +                             .format(override_dboard_pids)) +            raise NotImplementedError("Can't override dboard pids") +        # The DBoard PID is the same as the MBoard PID +        db_pid = list(self.pids.keys())[0] +        # Set up the SPI nodes +        spi_nodes = [] +        for spi_addr in self.dboard_spimaster_addrs: +            for spi_node in get_spidev_nodes(spi_addr): +                bisect.insort(spi_nodes, spi_node) +        self.log.trace("Found spidev nodes: {0}".format(spi_nodes)) + +        if not spi_nodes: +            self.log.warning("No SPI nodes for dboard %d.", E320_DBOARD_SLOT_IDX) +        dboard_info = { +            'eeprom_md': self.mboard_info, +            'eeprom_rawdata': self._eeprom_rawdata, +            'pid': db_pid, +            'spi_nodes': spi_nodes, +            'default_args': default_args, +        } +        # This will actually instantiate the dboard class: +        self.dboards.append(Neon(E320_DBOARD_SLOT_IDX, **dboard_info)) +        self.log.info("Found %d daughterboard(s).", len(self.dboards)) + +    def _check_fpga_compat(self): +        " Throw an exception if the compat numbers don't match up " +        actual_compat = self.mboard_regs_control.get_compat_number() +        self.log.debug("Actual FPGA compat number: {:d}.{:d}".format( +            actual_compat[0], actual_compat[1] +        )) +        assert_compat_number( +            E320_FPGA_COMPAT, +            self.mboard_regs_control.get_compat_number(), +            component="FPGA", +            fail_on_old_minor=True, +            log=self.log +        ) + +    def _init_ref_clock_and_time(self, default_args): +        """ +        Initialize clock and time sources. After this function returns, the +        reference signals going to the FPGA are valid. +        """ +        self._ext_clock_freq = float( +            default_args.get('ext_clock_freq', E320_DEFAULT_EXT_CLOCK_FREQ) +        ) +        if not self.dboards: +            self.log.warning( +                "No dboards found, skipping setting clock and time source " +                "configuration." +            ) +            self._clock_source = E320_DEFAULT_CLOCK_SOURCE +            self._time_source = E320_DEFAULT_TIME_SOURCE +        else: +            self.set_clock_source( +                default_args.get('clock_source', E320_DEFAULT_CLOCK_SOURCE) +            ) +            self.set_time_source( +                default_args.get('time_source', E320_DEFAULT_TIME_SOURCE) +            ) + +    def _monitor_status(self): +        """ +        Status monitoring thread: This should be executed in a thread. It will +        continuously monitor status of the following peripherals: + +        - GPS lock +        """ +        self.log.trace("Launching monitor loop...") +        cond = threading.Condition() +        cond.acquire() +        while not self._tear_down: +            gps_locked = self.get_gps_lock_sensor()['value'] == 'true' +            # Now wait +            if cond.wait_for( +                    lambda: self._tear_down, +                    E320_MONITOR_THREAD_INTERVAL): +                break +        cond.release() +        self.log.trace("Terminating monitor loop.") + +    def _init_peripherals(self, args): +        """ +        Turn on all peripherals. This may throw an error on failure, so make +        sure to catch it. + +        Peripherals are initialized in the order of least likely to fail, to most +        likely. +        """ +        # Sanity checks +        assert self.mboard_info.get('product') in self.pids.values(), \ +            "Device product could not be determined!" +        # Init Mboard Regs +        self.mboard_regs_control = MboardRegsControl( +            self.mboard_regs_label, self.log) +        self.mboard_regs_control.get_git_hash() +        self.mboard_regs_control.get_build_timestamp() +        self._check_fpga_compat() +        self._update_fpga_type() +        # Init peripherals +        self.enable_gps( +            enable=str2bool( +                args.get('enable_gps', E320_DEFAULT_ENABLE_GPS) +            ) +        ) +        self.enable_fp_gpio( +            voltage=args.get( +                        'fp_gpio_voltage', +                        E320_DEFAULT_FPGPIO_VOLTAGE +                    ) +        ) +        # Init clocking +        self._init_ref_clock_and_time(args) +        # Init CHDR transports +        self._xport_mgrs = { +            'udp': E320XportMgrUDP(self.log.getChild('UDP')), +            'liberio': E320XportMgrLiberio(self.log.getChild('liberio')), +        } +        # Spawn status monitoring thread +        self.log.trace("Spawning status monitor thread...") +        self._status_monitor_thread = threading.Thread( +            target=self._monitor_status, +            name="E320StatusMonitorThread", +            daemon=True, +        ) +        self._status_monitor_thread.start() +        # Init complete. +        self.log.debug("mboard info: {}".format(self.mboard_info)) + +    ########################################################################### +    # Session init and deinit +    ########################################################################### +    def init(self, args): +        """ +        Calls init() on the parent class, and then programs the Ethernet +        dispatchers accordingly. +        """ +        if not self._device_initialized: +            self.log.warning( +                "Cannot run init(), device was never fully initialized!") +            return False +        if args.get("clock_source", "") != "": +            self.set_clock_source(args.get("clock_source")) +        if args.get("time_source", "") != "": +            self.set_time_source(args.get("time_source")) +        result = super(e320, self).init(args) +        for xport_mgr in itervalues(self._xport_mgrs): +            xport_mgr.init(args) +        return result + +    def deinit(self): +        """ +        Clean up after a UHD session terminates. +        """ +        if not self._device_initialized: +            self.log.warning( +                "Cannot run deinit(), device was never fully initialized!") +            return +        super(e320, self).deinit() +        for xport_mgr in itervalues(self._xport_mgrs): +            xport_mgr.deinit() +        self.log.trace("Resetting SID pool...") +        self._available_endpoints = list(range(256)) + +    def tear_down(self): +        """ +        Tear down all members that need to be specially handled before +        deconstruction. +        For E320, this means the overlay. +        """ +        self.log.trace("Tearing down E320 device...") +        self._tear_down = True +        if self._device_initialized: +            self._status_monitor_thread.join(3 * E320_MONITOR_THREAD_INTERVAL) +            if self._status_monitor_thread.is_alive(): +                self.log.error("Could not terminate monitor thread! This could result in resource leaks.") +        active_overlays = self.list_active_overlays() +        self.log.trace("E320 has active device tree overlays: {}".format( +            active_overlays +        )) +        for overlay in active_overlays: +            dtoverlay.rm_overlay(overlay) + +    ########################################################################### +    # Transport API +    ########################################################################### +    def request_xport( +            self, +            dst_address, +            suggested_src_address, +            xport_type +        ): +        """ +        See PeriphManagerBase.request_xport() for docs. +        """ +        # Try suggested address first, then just pick the first available one: +        src_address = suggested_src_address +        if src_address not in self._available_endpoints: +            if not self._available_endpoints: +                raise RuntimeError( +                    "Depleted pool of SID endpoints for this device!") +            else: +                src_address = self._available_endpoints[0] +        sid = SID(src_address << 16 | dst_address) +        # Note: This SID may change its source address! +        self.log.trace( +            "request_xport(dst=0x%04X, suggested_src_address=0x%04X, xport_type=%s): " \ +            "operating on temporary SID: %s", +            dst_address, suggested_src_address, str(xport_type), str(sid)) +        # FIXME token! +        assert self.mboard_info['rpc_connection'] in ('remote', 'local') +        if self.mboard_info['rpc_connection'] == 'remote': +            return self._xport_mgrs['udp'].request_xport( +                sid, +                xport_type, +            ) +        elif self.mboard_info['rpc_connection'] == 'local': +            return self._xport_mgrs['liberio'].request_xport( +                sid, +                xport_type, +            ) + +    def commit_xport(self, xport_info): +        """ +        See PeriphManagerBase.commit_xport() for docs. + +        Reminder: All connections are incoming, i.e. "send" or "TX" means +        remote device to local device, and "receive" or "RX" means this local +        device to remote device. "Remote device" can be, for example, a UHD +        session. +        """ +        ## Go, go, go +        assert self.mboard_info['rpc_connection'] in ('remote', 'local') +        sid = SID(xport_info['send_sid']) +        self._available_endpoints.remove(sid.src_ep) +        self.log.debug("Committing transport for SID %s, xport info: %s", +                       str(sid), str(xport_info)) +        if self.mboard_info['rpc_connection'] == 'remote': +            return self._xport_mgrs['udp'].commit_xport(sid, xport_info) +        elif self.mboard_info['rpc_connection'] == 'local': +            return self._xport_mgrs['liberio'].commit_xport(sid, xport_info) + +    ########################################################################### +    # Device info +    ########################################################################### +    def get_device_info_dyn(self): +        """ +        Append the device info with current IP addresses. +        """ +        if not self._device_initialized: +            return {} +        device_info = self._xport_mgrs['udp'].get_xport_info() +        device_info.update({ +            'fpga_version': "{}.{}".format( +                *self.mboard_regs_control.get_compat_number()), +            'fpga': self.updateable_components.get('fpga', {}).get('type',""), +        }) +        return device_info + +    ########################################################################### +    # Clock/Time API +    ########################################################################### +    def get_clock_sources(self): +        " Lists all available clock sources. " +        self.log.trace("Listing available clock sources...") +        return ('external', 'internal', 'gpsdo') + +    def get_clock_source(self): +        " Returns the currently selected clock source " +        return self._clock_source + +    def set_clock_source(self, *args): +        """ +        Switch reference clock. + +        Throws if clock_source is not a valid value. +        """ +        clock_source = args[0] +        assert clock_source in self.get_clock_sources() +        self.log.debug("Setting clock source to `{}'".format(clock_source)) +        if clock_source == self.get_clock_source(): +            self.log.trace("Nothing to do -- clock source already set.") +            return +        self._clock_source = clock_source +        ref_clk_freq = self.get_ref_clock_freq() +        self.mboard_regs_control.set_clock_source(clock_source, ref_clk_freq) +        self.log.debug("Reference clock frequency is: {} MHz".format( +            ref_clk_freq/1e6 +        )) +        self.dboard.update_ref_clock_freq(ref_clk_freq) + +    def set_ref_clock_freq(self, freq): +        """ +        Tell our USRP what the frequency of the external reference clock is. + +        Will throw if it's not a valid value. +        """ +        # Other frequencies have not been tested +        assert freq in (10e6, 20e6) +        self.log.debug("We've been told the external reference clock " \ +                       "frequency is {} MHz.".format(freq / 1e6)) +        if self._ext_clock_freq == freq: +            self.log.trace("New external reference clock frequency " \ +                           "assignment matches previous assignment. Ignoring " \ +                           "update command.") +            return +        self._ext_clock_freq = freq +        if self.get_clock_source() == 'external': +            for slot, dboard in enumerate(self.dboards): +                if hasattr(dboard, 'update_ref_clock_freq'): +                    self.log.trace( +                        "Updating reference clock on dboard %d to %f MHz...", +                        slot, freq/1e6 +                    ) +                    dboard.update_ref_clock_freq(freq) + + +    def get_ref_clock_freq(self): +        " Returns the currently active reference clock frequency" +        clock_source = self.get_clock_source() +        if clock_source == "internal" or clock_source == "gpsdo": +            return E320_DEFAULT_INT_CLOCK_FREQ +        elif clock_source == "external": +            return self._ext_clock_freq + +    def get_time_sources(self): +        " Returns list of valid time sources " +        return ['internal', 'external', 'gpsdo'] + +    def get_time_source(self): +        " Return the currently selected time source " +        return self._time_source + +    def set_time_source(self, time_source): +        " Set a time source " +        assert time_source in self.get_time_sources() +        if time_source == self.get_time_source(): +            self.log.trace("Nothing to do -- time source already set.") +            return +        self._time_source = time_source +        self.mboard_regs_control.set_time_source(time_source, self.get_ref_clock_freq()) + +    ########################################################################### +    # Hardware peripheral controls +    ########################################################################### + +    def set_fp_gpio_master(self, value): +        """set driver for front panel GPIO +        Arguments: +            value {unsigned} -- value is a single bit bit mask of 12 pins GPIO +        """ +        self.mboard_regs_control.set_fp_gpio_master(value) + +    def get_fp_gpio_master(self): +        """get "who" is driving front panel gpio +           The return value is a bit mask of 8 pins GPIO. +           0: means the pin is driven by PL +           1: means the pin is driven by PS +        """ +        return self.mboard_regs_control.get_fp_gpio_master() + +    def set_fp_gpio_radio_src(self, value): +        """set driver for front panel GPIO +        Arguments: +            value {unsigned} -- value is 2-bit bit mask of 8 pins GPIO +           00: means the pin is driven by radio 0 +           01: means the pin is driven by radio 1 +        """ +        self.mboard_regs_control.set_fp_gpio_radio_src(value) + +    def get_fp_gpio_radio_src(self): +        """get which radio is driving front panel gpio +           The return value is 2-bit bit mask of 8 pins GPIO. +           00: means the pin is driven by radio 0 +           01: means the pin is driven by radio 1 +        """ +        return self.mboard_regs_control.get_fp_gpio_radio_src() + +    def enable_gps(self, enable): +        """ +        Turn power to the GPS (CLK_GPS_PWR_EN) off or on. +        """ +        self.mboard_regs_control.enable_gps(enable) + +    def enable_fp_gpio(self, voltage): +        """ +        Turn power to the front panel GPIO off or on and set voltage +        to (1.8, 2.5, 3.3V) and setting to 0 turns off GPIO. +        """ +        self.log.trace("{} power to front-panel GPIO".format( +            "Enabling" if voltage == 0 else "Disabling" +        )) +        self.mboard_regs_control.enable_fp_gpio(voltage) + +    def set_fp_gpio_voltage(self, value): +        """ +        Set Front Panel GPIO voltage (1.8, 2.5 or 3.3 Volts) +        """ +        self.log.trace("Setting front-panel GPIO voltage to {:3.1f} V".format(value)) +        self.mboard_regs_control.set_fp_gpio_voltage(value) + +    def get_fp_gpio_voltage(self): +        """ +        Get Front Panel GPIO voltage (1.8, 2.5 or 3.3 Volts) +        """ +        value = self.mboard_regs_control.get_fp_gpio_voltage() +        self.log.trace("Current front-panel GPIO voltage {:3.1f} V".format(value)) +        return value + +    def set_channel_mode(self, channel_mode): +        "Set channel mode in FPGA and select which tx channel to use" +        self.mboard_regs_control.set_channel_mode(channel_mode) + +    ########################################################################### +    # Sensors +    ########################################################################### +    def get_temp_sensor(self): +        """ +        Get temperature sensor reading of the E320. +        """ +        # TODO: This is Catalina's temperature. Do we want to return a different temp? +        return self.catalina.get_temperature() + +    def get_gps_lock_sensor(self): +        """ +        Get lock status of GPS as a sensor dict +        """ +        self.log.trace("Reading status GPS lock pin from port expander") +        raise NotImplementedError("GPS lock not implemented") +        # FIXME put it in a register +        # TODO: implement get_gps_lock, splits up functionality +        #gps_locked = bool(self._gpios.get("GPS-LOCKOK")) +        #return { +        #    'name': 'gps_lock', +        #    'type': 'BOOLEAN', +        #    'unit': 'locked' if gps_locked else 'unlocked', +        #    'value': str(gps_locked).lower(), +        #} + +    # TODO: Add other GPS sensors (time, TPV, SKY, etc.) +    # TODO: Add all physical sensors we can + +    ########################################################################### +    # EEPROMs +    ########################################################################### +    def get_mb_eeprom(self): +        """ +        Return a dictionary with EEPROM contents. + +        All key/value pairs are string -> string. + +        We don't actually return the EEPROM contents, instead, we return the +        mboard info again. This filters the EEPROM contents to what we think +        the user wants to know/see. +        """ +        return self.mboard_info + +    def set_mb_eeprom(self, eeprom_vals): +        """ +        See PeriphManagerBase.set_mb_eeprom() for docs. +        """ +        self.log.warn("Called set_mb_eeprom(), but not implemented!") +        raise NotImplementedError + +    def get_db_eeprom(self, dboard_idx): +        """ +        See PeriphManagerBase.get_db_eeprom() for docs. +        """ +        if dboard_idx != E320_DBOARD_SLOT_IDX: +            self.log.warn("Trying to access invalid dboard index {}. " +                          "Using the only dboard.".format(dboard_idx)) +        db_eeprom_data = copy.copy(self.dboard.device_info) +        for blob_id, blob in iteritems(self.dboard.get_user_eeprom_data()): +            if blob_id in db_eeprom_data: +                self.log.warn("EEPROM user data contains invalid blob ID " +                              "%s", blob_id) +            else: +                db_eeprom_data[blob_id] = blob +        return db_eeprom_data + +    def set_db_eeprom(self, dboard_idx, eeprom_data): +        """ +        Write new EEPROM contents with eeprom_map. + +        Arguments: +        dboard_idx -- Slot index of dboard (can only be E320_DBOARD_SLOT_IDX) +        eeprom_data -- Dictionary of EEPROM data to be written. It's up to the +                       specific device implementation on how to handle it. +        """ +        if dboard_idx != E320_DBOARD_SLOT_IDX: +            self.log.warn("Trying to access invalid dboard index {}. " +                          "Using the only dboard.".format(dboard_idx)) +        safe_db_eeprom_user_data = {} +        for blob_id, blob in iteritems(eeprom_data): +            if blob_id in self.dboard.device_info: +                error_msg = "Trying to overwrite read-only EEPROM " \ +                            "entry `{}'!".format(blob_id) +                self.log.error(error_msg) +                raise RuntimeError(error_msg) +            if not isinstance(blob, str) and not isinstance(blob, bytes): +                error_msg = "Blob data for ID `{}' is not a " \ +                            "string!".format(blob_id) +                self.log.error(error_msg) +                raise RuntimeError(error_msg) +            assert isinstance(blob, str) +            safe_db_eeprom_user_data[blob_id] = blob.encode('ascii') +        self.dboard.set_user_eeprom_data(safe_db_eeprom_user_data) + +    ########################################################################### +    # Component updating +    ########################################################################### +    # Note: Component updating functions defined by ZynqComponents +    @no_rpc +    def _update_fpga_type(self): +        """Update the fpga type stored in the updateable components""" +        fpga_type = self.mboard_regs_control.get_fpga_type() +        self.log.debug("Updating mboard FPGA type info to {}".format(fpga_type)) +        self.updateable_components['fpga']['type'] = fpga_type diff --git a/mpm/python/usrp_mpm/periph_manager/e320_periphs.py b/mpm/python/usrp_mpm/periph_manager/e320_periphs.py new file mode 100644 index 000000000..d98c5a0e5 --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/e320_periphs.py @@ -0,0 +1,383 @@ +# +# Copyright 2018 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +E320 peripherals +""" + +import datetime +from usrp_mpm.sys_utils.sysfs_gpio import SysFSGPIO, GPIOBank +from usrp_mpm.sys_utils.uio import UIO + +# Map register values to SFP transport types +E320_SFP_TYPES = { +    0: "",    # Port not connected +    1: "1G", +    2: "10G", +    3: "A",   # Aurora +} + +E320_FPGA_TYPES_BY_SFP = { +    (""):    "", +    ("1G"):  "1G", +    ("10G"): "XG", +    ("A"):   "AA", +} + +class FrontpanelGPIO(GPIOBank): +    """ +    Abstraction layer for the front panel GPIO +    """ +    EMIO_BASE = 54 +    FP_GPIO_OFFSET = 32 # Bit offset within the ps_gpio_* pins + +    def __init__(self, ddr): +        GPIOBank.__init__( +            self, +            'zynq_gpio', +            self.FP_GPIO_OFFSET + self.EMIO_BASE, +            0xFF, # use_mask +            ddr +        ) + +class MboardRegsControl(object): +    """ +    Control the FPGA Motherboard registers +    """ +    # Motherboard registers +    MB_COMPAT_NUM     = 0x0000 +    MB_DATESTAMP      = 0x0004 +    MB_GIT_HASH       = 0x0008 +    MB_SCRATCH        = 0x000C +    MB_NUM_CE         = 0x0010 +    MB_NUM_IO_CE      = 0x0014 +    MB_CLOCK_CTRL     = 0x0018 +    MB_XADC_RB        = 0x001C +    MB_BUS_CLK_RATE   = 0x0020 +    MB_BUS_COUNTER    = 0x0024 +    MB_SFP_PORT_INFO  = 0x0028 +    MB_GPIO_CTRL      = 0x002C +    MB_GPIO_MASTER    = 0x0030 +    MB_GPIO_RADIO_SRC = 0x0034 +    MB_GPS_CTRL       = 0x0038 +    MB_GPS_STATUS     = 0x003C +    MB_DBOARD_CTRL    = 0x0040 +    MB_DBOARD_STATUS  = 0x0044 + +    # Bitfield locations for the MB_CLOCK_CTRL register. +    MB_CLOCK_CTRL_PPS_SEL_INT = 0 +    MB_CLOCK_CTRL_PPS_SEL_EXT = 1 +    MB_CLOCK_CTRL_REF_SEL = 2 +    MB_CLOCK_CTRL_REF_CLK_LOCKED = 3 + +    # Bitfield locations for the MB_GPIO_CTRL register. +    MB_GPIO_CTRL_BUFFER_OE_N = 0 +    MB_GPIO_CTRL_EN_VAR_SUPPLY = 1 +    MB_GPIO_CTRL_EN_2V5 = 2 +    MB_GPIO_CTRL_EN_3V3 = 3 + +    # Bitfield locations for the MB_GPS_CTRL register. +    MB_GPS_CTRL_PWR_EN = 0 +    MB_GPS_CTRL_RST_N = 1 +    MB_GPS_CTRL_INITSURV_N = 2 + +    # Bitfield locations for the MB_GPS_STATUS register. +    MB_GPS_STATUS_LOCK = 0 +    MB_GPS_STATUS_ALARM = 1 +    MB_GPS_STATUS_PHASELOCK = 2 +    MB_GPS_STATUS_SURVEY = 3 +    MB_GPS_STATUS_WARMUP = 4 + +    # Bitfield locations for the MB_DBOARD_CTRL register. +    MB_DBOARD_CTRL_MIMO = 0 +    MB_DBOARD_CTRL_TX_CHAN_SEL = 1 + +    def __init__(self, label, log): +        self.log = log +        self.regs = UIO( +            label=label, +            read_only=False +        ) +        self.poke32 = self.regs.poke32 +        self.peek32 = self.regs.peek32 + +    def get_compat_number(self): +        """get FPGA compat number + +        This function reads back FPGA compat number. +        The return is a tuple of +        2 numbers: (major compat number, minor compat number ) +        """ +        with self.regs.open(): +            compat_number = self.peek32(self.MB_COMPAT_NUM) +        minor = compat_number & 0xff +        major = (compat_number>>16) & 0xff +        return (major, minor) + +    def enable_fp_gpio(self, value): +        """ Enable front panel GPIO buffers and power supply +        and set voltage 1.8, 2.5 or 3.3 V +        Setting value to 0 would disable gpio +        """ +        if value == 0: +            enable = False +        else: +            enable = True +            self.set_fp_gpio_voltage(value) +        mask = 0xFFFFFFFF ^ ((0b1 << self.MB_GPIO_CTRL_BUFFER_OE_N) | \ +                             (0b1 << self.MB_GPIO_CTRL_EN_VAR_SUPPLY)) +        with self.regs.open(): +            reg_val = self.peek32(self.MB_GPIO_CTRL) & mask +            reg_val = reg_val | (not enable << self.MB_GPIO_CTRL_BUFFER_OE_N) | \ +                                (enable << self.MB_GPIO_CTRL_EN_VAR_SUPPLY) +            self.log.trace("Writing MB_GPIO_CTRL to 0x{:08X}".format(reg_val)) +            return self.poke32(self.MB_GPIO_CTRL, reg_val) + +    def set_fp_gpio_voltage(self, value): +        """ Set Front Panel GPIO voltage (in volts) +        3V3 2V5 | Voltage +        ----------------- +         0   0  | 1.8 V +         0   1  | 2.5 V +         1   0  | 3.3 V +        Arguments: +            value : 1.8, 2.5 or 3.3 +        """ +        assert value in (1.8, 2.5, 3.3) +        if value == 1.8: +            voltage_reg = 0 +        elif value == 2.5: +            voltage_reg = 1 +        elif value == 3.3: +            voltage_reg = 2 +        mask = 0xFFFFFFFF ^ ((0b1 << self.MB_GPIO_CTRL_EN_3V3) | \ +                             (0b1 << self.MB_GPIO_CTRL_EN_2V5)) +        with self.regs.open(): +            reg_val = self.peek32(self.MB_GPIO_CTRL) & mask +            reg_val = reg_val | (voltage_reg << self.MB_GPIO_CTRL_EN_2V5) +            self.log.trace("Writing MB_GPIO_CTRL to 0x{:08X}".format(reg_val)) +            return self.poke32(self.MB_GPIO_CTRL, reg_val) + +    def get_fp_gpio_voltage(self): +        """ +        Get Front Panel GPIO voltage (in volts) +        """ +        mask = 0x3 << self.MB_GPIO_CTRL_EN_2V5 +        voltage = [1.8, 2.5, 3.3] +        with self.regs.open(): +            reg_val = (self.peek32(self.MB_GPIO_CTRL) & mask) >> self.MB_GPIO_CTRL_EN_2V5 +        return voltage[reg_val] + +    def set_fp_gpio_master(self, value): +        """set driver for front panel GPIO +        Arguments: +            value {unsigned} -- value is a single bit bit mask of 8 pins GPIO +        """ +        with self.regs.open(): +            return self.poke32(self.MB_GPIO_MASTER, value) + +    def get_fp_gpio_master(self): +        """get "who" is driving front panel gpio +           The return value is a bit mask of 8 pins GPIO. +           0: means the pin is driven by PL +           1: means the pin is driven by PS +        """ +        with self.regs.open(): +            return self.peek32(self.MB_GPIO_MASTER) & 0xfff + +    def set_fp_gpio_radio_src(self, value): +        """set driver for front panel GPIO +        Arguments: +            value {unsigned} -- value is 2-bit bit mask of 8 pins GPIO +           00: means the pin is driven by radio 0 +           01: means the pin is driven by radio 1 +        """ +        with self.regs.open(): +            return self.poke32(self.MB_GPIO_RADIO_SRC, value) + +    def get_fp_gpio_radio_src(self): +        """get which radio is driving front panel gpio +           The return value is 2-bit bit mask of 8 pins GPIO. +           00: means the pin is driven by radio 0 +           01: means the pin is driven by radio 1 +        """ +        with self.regs.open(): +            return self.peek32(self.MB_GPIO_RADIO_SRC) & 0xffffff + +    def get_build_timestamp(self): +        """ +        Returns the build date/time for the FPGA image. +        The return is datetime string with the  ISO 8601 format +        (YYYY-MM-DD HH:MM:SS.mmmmmm) +        """ +        with self.regs.open(): +            datestamp_rb = self.peek32(self.MB_DATESTAMP) +        if datestamp_rb > 0: +            dt_str = datetime.datetime( +                year=((datestamp_rb>>17)&0x3F)+2000, +                month=(datestamp_rb>>23)&0x0F, +                day=(datestamp_rb>>27)&0x1F, +                hour=(datestamp_rb>>12)&0x1F, +                minute=(datestamp_rb>>6)&0x3F, +                second=((datestamp_rb>>0)&0x3F)) +            self.log.trace("FPGA build timestamp: {}".format(str(dt_str))) +            return str(dt_str) +        else: +            # Compatibility with FPGAs without datestamp capability +            return '' + +    def get_git_hash(self): +        """ +        Returns the GIT hash for the FPGA build. +        The return is a tuple of +        2 numbers: (short git hash, bool: is the tree dirty?) +        """ +        with self.regs.open(): +            git_hash_rb = self.peek32(self.MB_GIT_HASH) +        git_hash = git_hash_rb & 0x0FFFFFFF +        tree_dirty = ((git_hash_rb & 0xF0000000) > 0) +        dirtiness_qualifier = 'dirty' if tree_dirty else 'clean' +        self.log.trace("FPGA build GIT Hash: {:07x} ({})".format( +            git_hash, dirtiness_qualifier)) +        return (git_hash, dirtiness_qualifier) + +    def set_time_source(self, time_source, ref_clk_freq): +        """ +        Set time source +        """ +        pps_sel_val = 0x0 +        if time_source == 'internal' or time_source == 'gpsdo': +            self.log.trace("Setting time source to internal (GPSDO)" +                           "({:.1f} MHz reference)...".format(ref_clk_freq)) +            pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_INT +        elif time_source == 'external': +            self.log.debug("Setting time source to external...") +            pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_EXT +        else: +            assert False, "Cannot set to invalid time source: {}".format(time_source) +        with self.regs.open(): +            reg_val = self.peek32(self.MB_CLOCK_CTRL) & 0xFFFFFF90 +            # prevent glitches by writing a cleared value first, then the final value. +            self.poke32(self.MB_CLOCK_CTRL, reg_val) +            reg_val = reg_val | (pps_sel_val & 0x6F) +            self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val)) +            self.poke32(self.MB_CLOCK_CTRL, reg_val) + +    def set_clock_source(self, clock_source, ref_clk_freq): +        """ +        Set clock source +        """ +        if clock_source == 'internal' or clock_source == 'gpsdo': +            self.log.trace("Setting clock source to internal (GPSDO)" +                           "({:.1f} MHz reference)...".format(ref_clk_freq)) +            ref_sel_val = 0b0 +        elif clock_source == 'external': +            self.log.debug("Setting clock source to external..." +                           "({:.1f} MHz reference)...".format(ref_clk_freq)) +            ref_sel_val = 0b1 +        else: +            assert False, "Cannot set to invalid clock source: {}".format(clock_source) +        mask = 0xFFFFFFFF ^ (0b1 << self.MB_CLOCK_CTRL_REF_SEL) +        with self.regs.open(): +            reg_val = self.peek32(self.MB_CLOCK_CTRL) & mask +            reg_val = reg_val | (ref_sel_val << self.MB_CLOCK_CTRL_REF_SEL) +            self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val)) +            self.poke32(self.MB_CLOCK_CTRL, reg_val) + +    def get_fpga_type(self): +        """ +        Reads the type of the FPGA image currently loaded +        Returns a string with the type (ie 1G, XG, AU, etc.) +        """ +        with self.regs.open(): +            sfp_info_rb = self.peek32(self.MB_SFP_PORT_INFO) +        # Print the registers values as 32-bit hex values +        self.log.trace("SFP Info: 0x{0:0{1}X}".format(sfp_info_rb, 8)) +        try: +            sfp_type = E320_SFP_TYPES.get((sfp_info_rb & 0x0000FF00) >> 8, "") +            self.log.trace("SFP type: {}".format(sfp_type)) +            return sfp_type +        except KeyError: +            self.log.warning("Unrecognized SFP type: {}" +                             .format(sfp_type)) +        return "" + +    def get_gps_locked_val(self): +        """ +        Get GPS LOCK status +        """ +        mask = 0b1 << self.MB_GPS_STATUS_LOCK +        with self.regs.open(): +            reg_val = self.peek32(self.MB_GPS_STATUS) & mask +            gps_locked = reg_val & 0x1 #FIXME +        if gps_locked: +            self.log.trace("GPS locked!") +        # Can return this value because the gps_locked value is on the LSB +        return gps_locked + +    def get_gps_status(self): +        """ +        Get GPS status +        """ +        mask = 0x1F +        with self.regs.open(): +            gps_status = self.peek32(self.MB_GPS_STATUS) & mask +        return gps_status + +    def enable_gps(self, enable): +        """ +        Turn power to the GPS (CLK_GPS_PWR_EN) off or on. +        Power signal is GPS_3V3. +        """ +        self.log.trace("{} power to GPS".format( +            "Enabling" if enable else "Disabling" +        )) +        mask = 0xFFFFFFFF ^ (0b1 << self.MB_GPS_CTRL_PWR_EN) +        with self.regs.open(): +            reg_val = self.peek32(self.MB_GPS_CTRL) & mask +            reg_val = reg_val | (enable << self.MB_GPS_CTRL_PWR_EN) +            self.log.trace("Writing MB_GPS_CTRL to 0x{:08X}".format(reg_val)) +            return self.poke32(self.MB_GPS_CTRL, reg_val) + +    def get_refclk_lock(self): +        """ +        Check the status of the reference clock (adf4002) in FPGA. +        """ +        mask = 0b1 << self.MB_CLOCK_CTRL_REF_CLK_LOCKED +        with self.regs.open(): +            reg_val = self.peek32(self.MB_CLOCK_CTRL) +        locked = (reg_val & mask) > 0 +        if not locked: +            self.log.warning("Reference Clock reporting unlocked. " +                             "MB_CLOCK_CTRL reg: 0x{:08X}".format(reg_val)) +        else: +            self.log.trace("Reference Clock locked!") +        return locked + +    def set_channel_mode(self, channel_mode): +        """ +        Set channel mode in FPGA and select which tx channel to use +        channel mode = "MIMO" for mimo +        channel mode = "SISO_TX1", "SISO_TX0" for siso tx1, tx0 respectively. +        """ +        with self.regs.open(): +            reg_val = self.peek32(self.MB_DBOARD_CTRL) +            if channel_mode == "MIMO": +                reg_val = (0b1 << self.MB_DBOARD_CTRL_MIMO) +                self.log.trace("Setting channel mode in AD9361 interface: {}".format("2R2T" if channel_mode == 2 else "1R1T")) +            else: +                # Warn if user tries to set either tx0/tx1 in mimo mode +                # as both will be set automatically +                if channel_mode == "SISO_TX1": +                    # in SISO mode, Channel 1 +                    reg_val = (0b1 << self.MB_DBOARD_CTRL_TX_CHAN_SEL) | (0b0 << self.MB_DBOARD_CTRL_MIMO) +                    self.log.trace("Setting TX channel in AD9361 interface to: TX1") +                elif channel_mode == "SISO_TX0": +                    # in SISO mode, Channel 0 +                    reg_val = (0b0 << self.MB_DBOARD_CTRL_TX_CHAN_SEL) | (0b0 << self.MB_DBOARD_CTRL_MIMO) +                    self.log.trace("Setting TX channel in AD9361 interface to: TX0") +            self.log.trace("Writing MB_DBOARD_CTRL to 0x{:08X}".format(reg_val)) +            self.poke32(self.MB_DBOARD_CTRL, reg_val) + | 
