diff options
Diffstat (limited to 'mpm')
24 files changed, 1668 insertions, 16 deletions
diff --git a/mpm/CMakeLists.txt b/mpm/CMakeLists.txt index 4f6c3b651..78759cc0b 100644 --- a/mpm/CMakeLists.txt +++ b/mpm/CMakeLists.txt @@ -123,7 +123,7 @@ if(_has_no_psabi) message(STATUS "Disabling psABI warnings.") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-psabi") endif(_has_no_psabi) -set(MPM_ALL_DEVICES n3xx e320 tests) +set(MPM_ALL_DEVICES n3xx e320 e31x tests) set(MPM_DEVICE "n3xx" CACHE STRING "Choose an MPM device to build") set_property(CACHE MPM_DEVICE PROPERTY STRINGS ${MPM_ALL_DEVICES}) # Validate MPM_DEVICE @@ -139,12 +139,15 @@ if(MPM_DEVICE STREQUAL "n3xx") set(ENABLE_MAGNESIUM ON) elseif(MPM_DEVICE STREQUAL "e320") set(ENABLE_E320 ON) +elseif(MPM_DEVICE STREQUAL "e31x") + set(ENABLE_E300 ON) endif() MPM_REGISTER_COMPONENT("LibMPM" ENABLE_LIBMPM ON "Boost_FOUND" OFF ON) MPM_REGISTER_COMPONENT("Mykonos" ENABLE_MYKONOS ON "ENABLE_LIBMPM" OFF OFF) MPM_REGISTER_COMPONENT("Magnesium" ENABLE_MAGNESIUM ON "ENABLE_MYKONOS" OFF OFF) MPM_REGISTER_COMPONENT("E320" ENABLE_E320 ON "ENABLE_LIBMPM" OFF OFF) +MPM_REGISTER_COMPONENT("E300" ENABLE_E300 ON "ENABLE_LIBMPM" OFF OFF) add_subdirectory(include) include_directories( diff --git a/mpm/include/mpm/CMakeLists.txt b/mpm/include/mpm/CMakeLists.txt index e5b7193c6..f3637c913 100644 --- a/mpm/include/mpm/CMakeLists.txt +++ b/mpm/include/mpm/CMakeLists.txt @@ -11,7 +11,7 @@ install(FILES if(ENABLE_MYKONOS) add_subdirectory(ad937x) -elseif(ENABLE_E320) +elseif(ENABLE_E320 OR ENABLE_E300) add_subdirectory(ad9361) endif(ENABLE_MYKONOS) diff --git a/mpm/include/mpm/ad9361/CMakeLists.txt b/mpm/include/mpm/ad9361/CMakeLists.txt index 94ea655a7..2b2ea03ec 100644 --- a/mpm/include/mpm/ad9361/CMakeLists.txt +++ b/mpm/include/mpm/ad9361/CMakeLists.txt @@ -8,5 +8,6 @@ install(FILES ad9361_ctrl.hpp e320_defaults.hpp + e31x_defaults.hpp DESTINATION ${INCLUDE_DIR}/mpm/catalina ) diff --git a/mpm/include/mpm/ad9361/ad9361_ctrl.hpp b/mpm/include/mpm/ad9361/ad9361_ctrl.hpp index 7673d7460..2405317fd 100644 --- a/mpm/include/mpm/ad9361/ad9361_ctrl.hpp +++ b/mpm/include/mpm/ad9361/ad9361_ctrl.hpp @@ -14,11 +14,16 @@ #include <functional> #include <string> #include <vector> +#include <future> namespace mpm { namespace chips { using uhd::usrp::ad9361_ctrl; }}; // namespace mpm::chips +//! Async calls +std::future<double> handle_tune; +std::future<double> handle_set_clock_rate; + // TODO: pull in filter_info_base #ifdef LIBMPM_PYTHON void export_catalina(py::module& top_module) @@ -58,6 +63,48 @@ void export_catalina(py::module& top_module) .def("_get_filter", &ad9361_ctrl::get_filter) .def("set_filter", &ad9361_ctrl::set_filter) .def("output_digital_test_tone", &ad9361_ctrl::output_digital_test_tone); + + m.def("async__tune", +[]( + ad9361_ctrl& catalina, + const std::string &which, + const double value + ){ + handle_tune = std::async(std::launch::async, + &ad9361_ctrl::tune, + &catalina, + which, + value + ); + }); + m.def("await__tune", +[]( + )->bool{ + if (handle_tune.wait_for(std::chrono::seconds(0)) + == std::future_status::ready){ + handle_tune.get(); + return true; + } + return false; + }); + m.def("async__set_clock_rate", +[]( + ad9361_ctrl& catalina, + const double value + ){ + handle_set_clock_rate = std::async(std::launch::async, + &ad9361_ctrl::set_clock_rate, + &catalina, + value + ); + }); + m.def("await__set_clock_rate", +[]( + )->bool{ + if (handle_set_clock_rate.wait_for(std::chrono::seconds(0)) + == std::future_status::ready){ + handle_set_clock_rate.get(); + return true; + } + return false; + }); + } #endif diff --git a/mpm/include/mpm/ad9361/e31x_defaults.hpp b/mpm/include/mpm/ad9361/e31x_defaults.hpp new file mode 100644 index 000000000..c2188af8b --- /dev/null +++ b/mpm/include/mpm/ad9361/e31x_defaults.hpp @@ -0,0 +1,45 @@ +// +// Copyright 2018 Ettus Research, a National Instruments Company +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#ifndef INCLUDED_E31X_DEFAULTS_HPP +#define INCLUDED_E31X_DEFAULTS_HPP + +#include "ad9361_ctrl.hpp" + +namespace mpm { namespace types { namespace e31x { + +using namespace uhd::usrp; + +class e31x_ad9361_client_t : public uhd::usrp::ad9361_params { +public: + ~e31x_ad9361_client_t() {} + double get_band_edge(frequency_band_t band) { + switch (band) { + case AD9361_RX_BAND0: return 1.2e9; + case AD9361_RX_BAND1: return 2.6e9; + case AD9361_TX_BAND0: return 2940.0e6; + default: return 0; + } + } + clocking_mode_t get_clocking_mode() { + return clocking_mode_t::AD9361_XTAL_N_CLK_PATH; + } + digital_interface_mode_t get_digital_interface_mode() { + return AD9361_DDR_FDD_LVCMOS; + } + digital_interface_delays_t get_digital_interface_timing() { + digital_interface_delays_t delays; + delays.rx_clk_delay = 0; + delays.rx_data_delay = 0xF; + delays.tx_clk_delay = 0; + delays.tx_data_delay = 0xF; + return delays; + } +}; + +}}} // namespace + +#endif // INCLUDED_E31X_DEFAULTS_HPP diff --git a/mpm/include/mpm/dboards/CMakeLists.txt b/mpm/include/mpm/dboards/CMakeLists.txt index 03a5404bc..d1dd569d7 100644 --- a/mpm/include/mpm/dboards/CMakeLists.txt +++ b/mpm/include/mpm/dboards/CMakeLists.txt @@ -13,4 +13,9 @@ elseif(ENABLE_E320) neon_manager.hpp DESTINATION ${INCLUDE_DIR}/mpm/dboards ) +elseif(ENABLE_E300) + install(FILES + e31x_db_manager.hpp + DESTINATION ${INCLUDE_DIR}/mpm/dboards + ) endif(ENABLE_MAGNESIUM) diff --git a/mpm/include/mpm/dboards/e31x_db_manager.hpp b/mpm/include/mpm/dboards/e31x_db_manager.hpp new file mode 100644 index 000000000..8777778e9 --- /dev/null +++ b/mpm/include/mpm/dboards/e31x_db_manager.hpp @@ -0,0 +1,43 @@ +// +// Copyright 2018 Ettus Research, a National Instruments Company +// Copyright 2019 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#pragma once + +#include <mpm/ad9361/ad9361_ctrl.hpp> +#include <mpm/types/lockable.hpp> +#include <mpm/types/regs_iface.hpp> +#include <boost/shared_ptr.hpp> +#include <memory> +#include <mutex> + +namespace mpm { namespace dboards { + class e31x_db_manager + { + public: + e31x_db_manager(const std::string &catalina_spidev); + + /*! Return a reference to the radio chip controls + */ + mpm::chips::ad9361_ctrl::sptr get_radio_ctrl(){ return _catalina_ctrl; } + + private: + mpm::chips::ad9361_ctrl::sptr _catalina_ctrl; + }; + +}}; /* namespace mpm::dboards */ + +#ifdef LIBMPM_PYTHON +void export_e31x_db(py::module& top_module) +{ + using namespace mpm::dboards; + auto m = top_module.def_submodule("dboards"); + + py::class_<mpm::dboards::e31x_db_manager>(m, "e31x_db_manager") + .def(py::init<std::string>()) + .def("get_radio_ctrl", &mpm::dboards::e31x_db_manager::get_radio_ctrl); +} +#endif diff --git a/mpm/lib/CMakeLists.txt b/mpm/lib/CMakeLists.txt index 10f8f0d9d..82a5d8e16 100644 --- a/mpm/lib/CMakeLists.txt +++ b/mpm/lib/CMakeLists.txt @@ -14,7 +14,7 @@ add_subdirectory(types) if(ENABLE_MYKONOS) add_subdirectory(mykonos) -elseif(ENABLE_E320) +elseif(ENABLE_E320 OR ENABLE_E300) add_subdirectory(catalina) endif(ENABLE_MYKONOS) diff --git a/mpm/lib/dboards/CMakeLists.txt b/mpm/lib/dboards/CMakeLists.txt index 0760d9fd1..4a13528eb 100644 --- a/mpm/lib/dboards/CMakeLists.txt +++ b/mpm/lib/dboards/CMakeLists.txt @@ -16,4 +16,8 @@ elseif(ENABLE_E320) USRP_PERIPHS_ADD_OBJECT(dboards neon_manager.cpp ) +elseif(ENABLE_E300) + USRP_PERIPHS_ADD_OBJECT(dboards + e31x_db_manager.cpp + ) endif(ENABLE_MAGNESIUM) diff --git a/mpm/lib/dboards/e31x_db_manager.cpp b/mpm/lib/dboards/e31x_db_manager.cpp new file mode 100644 index 000000000..b433dde30 --- /dev/null +++ b/mpm/lib/dboards/e31x_db_manager.cpp @@ -0,0 +1,79 @@ +// +// Copyright 2018 Ettus Research, a National Instruments Company +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#include <mpm/ad9361/e31x_defaults.hpp> +#include <mpm/dboards/e31x_db_manager.hpp> +#include <mpm/spi/spi_iface.hpp> +#include <mpm/spi/spi_regs_iface.hpp> +#include <mpm/types/regs_iface.hpp> +#include <boost/make_shared.hpp> +#include <memory> + +using namespace mpm::dboards; +using namespace mpm::chips; +using namespace mpm::types; +using namespace mpm::types::e31x; + +namespace { /*anon*/ + +constexpr uint32_t AD9361_SPI_WRITE_CMD = 0x00800000; +constexpr uint32_t AD9361_SPI_READ_CMD = 0x00000000; +constexpr uint32_t AD9361_SPI_ADDR_MASK = 0x003FFF00; +constexpr uint32_t AD9361_SPI_ADDR_SHIFT = 8; +constexpr uint32_t AD9361_SPI_DATA_MASK = 0x000000FF; +constexpr uint32_t AD9361_SPI_DATA_SHIFT = 0; +constexpr uint32_t AD9361_SPI_NUM_BITS = 24; +constexpr uint32_t AD9361_SPI_SPEED_HZ = 2000000; +constexpr int AD9361_SPI_MODE = 1; + +} // namespace /*anon*/ + +/*! MPM-style E310 SPI Iface for AD9361 CTRL + * + */ +class e310_ad9361_io_spi : public ad9361_io +{ +public: + e310_ad9361_io_spi(regs_iface::sptr regs_iface, uint32_t slave_num) : + _regs_iface(regs_iface), _slave_num(slave_num) { } + + ~e310_ad9361_io_spi() {/*nop*/} + + uint8_t peek8(uint32_t reg) + { + return _regs_iface->peek8(reg); + } + + void poke8(uint32_t reg, uint8_t val) + { + _regs_iface->poke8(reg, val); + } + +private: + regs_iface::sptr _regs_iface; + uint32_t _slave_num; +}; + +e31x_db_manager::e31x_db_manager(const std::string &catalina_spidev) +{ + // Make the MPM-style low level SPI Regs iface + auto spi_iface = mpm::spi::make_spi_regs_iface( + mpm::spi::spi_iface::make_spidev(catalina_spidev, AD9361_SPI_SPEED_HZ, AD9361_SPI_MODE), + AD9361_SPI_ADDR_SHIFT, + AD9361_SPI_DATA_SHIFT, + AD9361_SPI_READ_CMD, + AD9361_SPI_WRITE_CMD); + // Make the SPI interface + auto spi_io_iface = std::make_shared<e310_ad9361_io_spi>(spi_iface, 0); + // Translate from a std shared_ptr to Boost (for legacy compatability) + auto spi_io_iface_boost = boost::shared_ptr<e310_ad9361_io_spi>( + spi_io_iface.get(), + [spi_io_iface](...) mutable { spi_io_iface.reset(); }); + // Make the actual Catalina Ctrl object + _catalina_ctrl = ad9361_ctrl::make_spi( + boost::make_shared<e31x_ad9361_client_t>(), + spi_io_iface_boost); +} diff --git a/mpm/python/CMakeLists.txt b/mpm/python/CMakeLists.txt index c78dc60b1..3a5b30d4e 100644 --- a/mpm/python/CMakeLists.txt +++ b/mpm/python/CMakeLists.txt @@ -12,6 +12,8 @@ if(MPM_DEVICE STREQUAL "n3xx") add_library(pyusrp_periphs SHARED pyusrp_periphs/n3xx/pyusrp_periphs.cpp) elseif(MPM_DEVICE STREQUAL "e320") add_library(pyusrp_periphs SHARED pyusrp_periphs/e320/pyusrp_periphs.cpp) +elseif(MPM_DEVICE STREQUAL "e31x") + add_library(pyusrp_periphs SHARED pyusrp_periphs/e31x/pyusrp_periphs.cpp) endif(MPM_DEVICE STREQUAL "n3xx") target_include_directories(pyusrp_periphs PUBLIC diff --git a/mpm/python/pyusrp_periphs/e31x/pyusrp_periphs.cpp b/mpm/python/pyusrp_periphs/e31x/pyusrp_periphs.cpp new file mode 100644 index 000000000..b7971407c --- /dev/null +++ b/mpm/python/pyusrp_periphs/e31x/pyusrp_periphs.cpp @@ -0,0 +1,30 @@ +// +// Copyright 2018 Ettus Research, a National Instruments Company +// Copyright 2019 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#include <pybind11/pybind11.h> +namespace py = pybind11; +#define LIBMPM_PYTHON + +// Allow boost::shared_ptr<T> to be a holder class of an object (PyBind11 +// supports std::shared_ptr and std::unique_ptr out of the box) +#include <boost/shared_ptr.hpp> +PYBIND11_DECLARE_HOLDER_TYPE(T, boost::shared_ptr<T>); + +#include <mpm/ad9361/ad9361_ctrl.hpp> +#include <mpm/dboards/e31x_db_manager.hpp> +#include <mpm/spi/spi_python.hpp> +#include <mpm/types/types_python.hpp> +#include <mpm/xbar_iface.hpp> + +PYBIND11_MODULE(libpyusrp_periphs, m) +{ + export_types(m); + export_spi(m); + export_xbar(m); + export_catalina(m); + export_e31x_db(m); +} diff --git a/mpm/python/usrp_mpm/CMakeLists.txt b/mpm/python/usrp_mpm/CMakeLists.txt index 1b85c8561..6b6d99a30 100644 --- a/mpm/python/usrp_mpm/CMakeLists.txt +++ b/mpm/python/usrp_mpm/CMakeLists.txt @@ -15,6 +15,7 @@ set(USRP_MPM_TOP_FILES ${CMAKE_CURRENT_SOURCE_DIR}/components.py ${CMAKE_CURRENT_SOURCE_DIR}/discovery.py ${CMAKE_CURRENT_SOURCE_DIR}/eeprom.py + ${CMAKE_CURRENT_SOURCE_DIR}/e31x_legacy_eeprom.py ${CMAKE_CURRENT_SOURCE_DIR}/ethtable.py ${CMAKE_CURRENT_SOURCE_DIR}/gpsd_iface.py ${CMAKE_CURRENT_SOURCE_DIR}/liberiotable.py diff --git a/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt b/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt index 650a21aab..3e4f6ba76 100644 --- a/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt +++ b/mpm/python/usrp_mpm/dboard_manager/CMakeLists.txt @@ -19,6 +19,7 @@ set(USRP_MPM_DBMGR_FILES ${CMAKE_CURRENT_SOURCE_DIR}/dac_rh.py ${CMAKE_CURRENT_SOURCE_DIR}/eiscat.py ${CMAKE_CURRENT_SOURCE_DIR}/neon.py + ${CMAKE_CURRENT_SOURCE_DIR}/e31x_db.py ${CMAKE_CURRENT_SOURCE_DIR}/gain_rh.py ${CMAKE_CURRENT_SOURCE_DIR}/gaintables_rh.py ${CMAKE_CURRENT_SOURCE_DIR}/lmk_eiscat.py diff --git a/mpm/python/usrp_mpm/dboard_manager/__init__.py b/mpm/python/usrp_mpm/dboard_manager/__init__.py index 70e7881db..77fd84436 100644 --- a/mpm/python/usrp_mpm/dboard_manager/__init__.py +++ b/mpm/python/usrp_mpm/dboard_manager/__init__.py @@ -10,6 +10,7 @@ from .base import DboardManagerBase from .magnesium import Magnesium from .rhodium import Rhodium from .neon import Neon +from .e31x_db import E31x_db from .eiscat import EISCAT from .test import test from .unknown import unknown diff --git a/mpm/python/usrp_mpm/dboard_manager/e31x_db.py b/mpm/python/usrp_mpm/dboard_manager/e31x_db.py new file mode 100644 index 000000000..358b69f88 --- /dev/null +++ b/mpm/python/usrp_mpm/dboard_manager/e31x_db.py @@ -0,0 +1,212 @@ +# +# Copyright 2018 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +E310 dboard (RF and control) implementation module +""" + +import threading +import time +from six import iterkeys, iteritems +from usrp_mpm import lib # Pulls in everything from C++-land +from usrp_mpm.bfrfs import BufferFS +from usrp_mpm.dboard_manager import DboardManagerBase +from usrp_mpm.mpmlog import get_logger +from usrp_mpm.sys_utils.udev import get_eeprom_paths +from usrp_mpm.sys_utils.uio import UIO +from usrp_mpm.periph_manager.e31x_periphs import MboardRegsControl +from usrp_mpm.mpmutils import async_exec + +############################################################################### +# Main dboard control class +############################################################################### +class E31x_db(DboardManagerBase): + """ + Holds all dboard specific information and methods of the E31x_db dboard + """ + ######################################################################### + # Overridables + # + # See DboardManagerBase for documentation on these fields + ######################################################################### + pids = [0x0110] + rx_sensor_callback_map = { + 'ad9361_temperature': 'get_catalina_temp_sensor', + 'rssi' : 'get_rssi_sensor', + 'lo_lock' : 'get_lo_lock_sensor', + } + tx_sensor_callback_map = { + 'ad9361_temperature': 'get_catalina_temp_sensor', + } + # Maps the chipselects to the corresponding devices: + spi_chipselect = {"catalina": 0, + } + + default_master_clock_rate = 16e6 + MIN_MASTER_CLK_RATE = 220e3 + MAX_MASTER_CLK_RATE = 61.44e6 + + def __init__(self, slot_idx, **kwargs): + super(E31x_db, self).__init__(slot_idx, **kwargs) + self.log = get_logger("E31x_db-{}".format(slot_idx)) + self.log.trace("Initializing e31x 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))) + # These will get updated during init() + self.master_clock_rate = None + # Predeclare some attributes to make linter happy: + self.catalina = None + self.eeprom_fs = None + self.eeprom_path = 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.log.debug("Loading C++ drivers...") + # Setup Catalina / the E31x_db Manager + self._device = lib.dboards.e31x_db_manager( + self._spi_nodes['catalina'] + ) + self.catalina = self._device.get_radio_ctrl() + self.log.trace("Loaded C++ drivers.") + self._init_cat_api(self.catalina) + + def _init_cat_api(self, cat): + """ + Propagate the C++ Catalina 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 AD9361 methods to E31x_db class...") + for method in [ + x for x in dir(self.catalina) + if not x.startswith("_") and \ + callable(getattr(self.catalina, x))]: + self.log.trace("adding {}".format(method)) + setattr(self, method, export_method(cat, method)) + + def init(self, args): + if not self._periphs_initialized: + error_msg = "Cannot run init(), peripherals are not initialized!" + self.log.error(error_msg) + raise RuntimeError(error_msg) + master_clock_rate = \ + float(args.get('master_clock_rate', + self.default_master_clock_rate)) + assert self.MIN_MASTER_CLK_RATE <= master_clock_rate <= self.MAX_MASTER_CLK_RATE, \ + "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 + )) + # Some default chains on -- needed for setup purposes + self.catalina.set_active_chains(True, False, True, False) + self.set_catalina_clock_rate(self.master_clock_rate) + + return True + + def get_master_clock_rate(self): + " Return master clock rate (== sampling rate) " + return self.master_clock_rate + + ########################################################################## + # Sensors + ########################################################################## + def get_ad9361_lo_lock(self, which): + """ + Return LO lock status (Boolean!) of AD9361. 'which' must be + either 'tx' or 'rx' + """ + self.mboard_regs_label = "mboard-regs" + self.mboard_regs_control = MboardRegsControl( + self.mboard_regs_label, self.log) + if which == "tx": + locked = self. mboard_regs_control.get_ad9361_tx_lo_lock() + elif which == "rx": + locked = self. mboard_regs_control.get_ad9361_rx_lo_lock() + else: + locked = False + return locked + + def get_lo_lock_sensor(self, which): + """ + Get sensor dict with LO lock status + """ + self.log.trace("Reading LO Lock.") + lo_locked = self.get_ad9361_lo_lock(which) + return { + 'name': 'ad9361_lock', + 'type': 'BOOLEAN', + 'unit': 'locked' if lo_locked else 'unlocked', + 'value': str(lo_locked).lower(), + } + + def get_catalina_temp_sensor(self, _): + """ + Get temperature sensor reading of Catalina. + """ + # Note: the unused argument is channel + self.log.trace("Reading Catalina temperature.") + return { + 'name': 'ad9361_temperature', + 'type': 'REALNUM', + 'unit': 'C', + 'value': str(self.catalina.get_temperature()) + } + + def get_rssi_val(self, which): + """ + Return the current RSSI of `which` chain in Catalina + """ + return self.catalina.get_rssi(which) + + def get_rssi_sensor(self, chan): + """ + Return a sensor dictionary containing the current RSSI of `which` chain in Catalina + """ + which = 'RX' + str(chan+1) + return { + 'name': 'rssi', + 'type': 'REALNUM', + 'unit': 'dB', + 'value': str(self.get_rssi_val(which)), + } + + def set_catalina_clock_rate(self, rate): + """ + Async call to catalina set_clock_rate + """ + self.log.trace("Setting Clock rate to {}".format(rate)) + async_exec(lib.ad9361, "set_clock_rate", self.catalina, rate) + return rate + + def catalina_tune(self, which, freq): + """ + Async call to catalina tune + """ + self.log.trace("Tuning {} {}".format(which, freq)) + async_exec(lib.ad9361, "tune", self.catalina, which, freq) + return self.catalina.get_freq(which) diff --git a/mpm/python/usrp_mpm/dboard_manager/neon.py b/mpm/python/usrp_mpm/dboard_manager/neon.py index c0eb10eae..00438503d 100644 --- a/mpm/python/usrp_mpm/dboard_manager/neon.py +++ b/mpm/python/usrp_mpm/dboard_manager/neon.py @@ -18,7 +18,7 @@ from usrp_mpm.mpmlog import get_logger from usrp_mpm.sys_utils.udev import get_eeprom_paths from usrp_mpm.sys_utils.uio import UIO from usrp_mpm.periph_manager.e320_periphs import MboardRegsControl - +from usrp_mpm.mpmutils import async_exec ############################################################################### # Main dboard control class @@ -186,7 +186,7 @@ class Neon(DboardManagerBase): )) # Some default chains on -- needed for setup purposes self.catalina.set_active_chains(True, False, True, False) - self.catalina.set_clock_rate(self.master_clock_rate) + self.set_catalina_clock_rate(self.master_clock_rate) return True @@ -321,3 +321,18 @@ class Neon(DboardManagerBase): 'value': str(self.get_rssi_val(which)), } + def set_catalina_clock_rate(self, rate): + """ + Async call to catalina set_clock_rate + """ + self.log.trace("Setting Clock rate to {}".format(rate)) + async_exec(lib.ad9361, "set_clock_rate", self.catalina, rate) + return rate + + def catalina_tune(self, which, freq): + """ + Async call to catalina tune + """ + self.log.trace("Tuning {} {}".format(which, freq)) + async_exec(lib.ad9361, "tune", self.catalina, which, freq) + return self.catalina.get_freq(which) diff --git a/mpm/python/usrp_mpm/e31x_legacy_eeprom.py b/mpm/python/usrp_mpm/e31x_legacy_eeprom.py new file mode 100644 index 000000000..4c0fb19a5 --- /dev/null +++ b/mpm/python/usrp_mpm/e31x_legacy_eeprom.py @@ -0,0 +1,103 @@ +# +# Copyright 2017 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +EEPROM management code +""" + +import struct +import zlib +from builtins import zip +from builtins import object + + +class MboardEEPROM(object): + """ + Given a nvmem path, read out EEPROM values from the motherboard's EEPROM. + The format of data in the EEPROM must follow the following standard: + + E310 Legacy EEPROM Format + + - 2 bytes data_version_major + - 2 bytes data_version_minor + - 6 bytes MAC address + - 2 bytes hw_pid + - 2 bytes hw_rev + - 8 bytes serial number (zero-terminated string of 7 characters) + - 12 bytes padding + - 8 bytes user_name + + MAC addresses are ignored here; they are read elsewhere. If we really need + to know the MAC address of an interface, we can fish it out the raw data, + or ask the system. + """ + + # Refer e300_eeprom_manager.hpp. + eeprom_header_format = "<H H 6s H H 7s 12s 8s" + eeprom_header_keys = ('data_version_major', 'data_version_minor', 'mac_address', 'pid', 'rev', 'serial', 'pad', 'user_name') + +class DboardEEPROM(object): + """ + Given a nvmem path, read out EEPROM values from the daughterboard's EEPROM. + The format of data in the EEPROM must follow the following standard: + + E310 Legacy EEPROM Format + + - 2 bytes data_version_major + - 2 bytes data_version_minor + - 2 bytes hw_pid + - 2 bytes hw_rev + - 8 bytes serial number (zero-terminated string of 7 characters) + - 12 bytes padding + """ + + # Refer e300_eeprom_manager.hpp. + eeprom_header_format = "<H H H H 7s 12s" + eeprom_header_keys = ('data_version_major', 'data_version_minor', 'pid', 'rev', 'serial', 'pad') + +def read_eeprom( + isMotherboard, + nvmem_path, + offset, + eeprom_header_format, + eeprom_header_keys, + max_size=None, +): + """ + Read the EEPROM located at nvmem_path and return a tuple (header, data) + Header is already parsed in the common header fields + Data contains the full eeprom data structure + + nvmem_path -- Path to readable file (typically something in sysfs) + eeprom_header_format -- List of header formats, by version + eeprom_header_keys -- List of keys for the entries in the EEPROM + max_size -- Max number of bytes to be read. If omitted, will read the full file. + """ + + max_size = max_size or -1 + with open(nvmem_path, "rb") as nvmem_file: + data = nvmem_file.read(max_size)[offset:] + eeprom_parser = struct.Struct(eeprom_header_format) + eeprom_keys = eeprom_header_keys + parsed_data = eeprom_parser.unpack_from(data) + + if isMotherboard: # E310 MB. + # Rectify the PID and REV parsing. Reverse the bytes. + # PID and REV are the 4th and 5th elements in the tuple. + parsed_data_list = list(parsed_data) + parsed_data_list[3] = struct.unpack("<H", struct.pack(">H", parsed_data_list[3]))[0] + parsed_data_list[4] = struct.unpack("<H", struct.pack(">H", parsed_data_list[4]))[0] + parsed_data = tuple(parsed_data_list) + + else: # E310 DB. + # Rectify the PID and REV parsing. Reverse the bytes. + # PID and REV are the 3rd and 4th elements in the tuple. + parsed_data_list = list(parsed_data) + parsed_data_list[2] = struct.unpack("<H", struct.pack(">H", parsed_data_list[2]))[0] + parsed_data_list[3] = struct.unpack("<H", struct.pack(">H", parsed_data_list[3]))[0] + parsed_data = tuple(parsed_data_list) + + ret_val = (dict(list(zip(eeprom_keys, parsed_data))),data) + return ret_val
\ No newline at end of file diff --git a/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt b/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt index 994b1d64a..987bc184b 100644 --- a/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt +++ b/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt @@ -15,6 +15,8 @@ set(USRP_MPM_PERIPHMGR_FILES ${CMAKE_CURRENT_SOURCE_DIR}/n3xx_periphs.py ${CMAKE_CURRENT_SOURCE_DIR}/e320.py ${CMAKE_CURRENT_SOURCE_DIR}/e320_periphs.py + ${CMAKE_CURRENT_SOURCE_DIR}/e31x.py + ${CMAKE_CURRENT_SOURCE_DIR}/e31x_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/base.py b/mpm/python/usrp_mpm/periph_manager/base.py index 2dc69e908..e297effbd 100644 --- a/mpm/python/usrp_mpm/periph_manager/base.py +++ b/mpm/python/usrp_mpm/periph_manager/base.py @@ -131,6 +131,12 @@ class PeriphManagerBase(object): # specific implementation. Each PeriphManagerBase-derived class should list # information required to update the component, like a callback function updateable_components = {} + # The RPC server checks this value to determine if it needs to clear + # the RPC method registry. This is typically to remove stale references + # to RPC methods caused by removal of overlay on unclaim() by peripheral + # manager. Additionally the RPC server will re-register all methods on + # a claim(). Override and set to True in the derived class if desired. + clear_rpc_method_registry_on_unclaim = False @staticmethod def generate_device_info(eeprom_md, mboard_info, dboard_infos): diff --git a/mpm/python/usrp_mpm/periph_manager/e31x.py b/mpm/python/usrp_mpm/periph_manager/e31x.py new file mode 100644 index 000000000..722f8d7a9 --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/e31x.py @@ -0,0 +1,744 @@ +# +# Copyright 2018-2019 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +E310 implementation module +""" + +from __future__ import print_function +import bisect +import copy +import re +import threading +from six import iteritems, itervalues +from usrp_mpm.components import ZynqComponents +from usrp_mpm.dboard_manager import E31x_db +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.sysfs_thermal import read_sysfs_sensors_value +from usrp_mpm.sys_utils.udev import get_spidev_nodes +from usrp_mpm.xports import XportMgrLiberio +from usrp_mpm.periph_manager.e31x_periphs import MboardRegsControl +from usrp_mpm.sys_utils.udev import get_eeprom_paths +from usrp_mpm import e31x_legacy_eeprom + +E310_DEFAULT_CLOCK_SOURCE = 'internal' +E310_DEFAULT_TIME_SOURCE = 'internal' +E310_DEFAULT_ENABLE_FPGPIO = True +E310_FPGA_COMPAT = (1,0) +E310_DBOARD_SLOT_IDX = 0 + +############################################################################### +# Transport managers +############################################################################### + +class E310XportMgrLiberio(XportMgrLiberio): + " E310-specific Liberio configuration " + max_chan = 4 + xbar_dev = "/dev/crossbar0" + xbar_port = 0 + +############################################################################### +# Main Class +############################################################################### +class e31x(ZynqComponents, PeriphManagerBase): + """ + Holds E310 specific attributes and methods + """ + ######################################################################### + # Overridables + # + # See PeriphManagerBase for documentation on these fields + ######################################################################### + description = "E300-Series Device" + # 0x77d2 and 0x77d3 + pids = {0x77D2: 'e310_sg1', #sg1 + 0x77D3: 'e310_sg3'} #sg3 + mboard_eeprom_addr = "e0004000.i2c" + mboard_eeprom_offset = 0 + mboard_eeprom_max_len = 64 + # We have two nvem paths on the E310. + # This one ensures that we get the right path for the MB. + mboard_eeprom_path_index = 1 + mboard_info = {"type": "e3xx"} + mboard_sensor_callback_map = { + 'ref_locked': 'get_ref_lock_sensor', + 'temp_fpga' : 'get_fpga_temp_sensor', + 'temp_mb' : 'get_mb_temp_sensor', + } + dboard_eeprom_addr = "e0004000.i2c" + + # Actual DB EEPROM bytes are just 28. Reading just a couple more. + # Refer e300_eeprom_manager.hpp + dboard_eeprom_max_len = 32 + + max_num_dboards = 1 + + # 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"] + # E310-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, + }, + } + # This class removes the overlay in tear_down() resulting + # in stale references to methods in the RPC server. Setting + # this to True ensures that the RPC server clears all registered + # methods on unclaim() and registers them on the following claim(). + clear_rpc_method_registry_on_unclaim = True + + @classmethod + def generate_device_info(cls, eeprom_md, mboard_info, dboard_infos): + """ + Generate dictionary describing the device. + """ + # Add the default PeriphManagerBase information first + device_info = super().generate_device_info( + eeprom_md, mboard_info, dboard_infos) + # Then add E31x-specific information + mb_pid = eeprom_md.get('pid') + device_info['product'] = cls.pids.get(mb_pid, 'unknown') + return device_info + + @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']] + + + @staticmethod + def get_idle_dt_overlay(device_info): + """ + Overlay to be applied to enter low power idle state. + """ + # e.g. e310_sg3_idle + idle_overlay = device_info['product'] + '_idle' + return idle_overlay + + ########################################################################### + # Ctor and device initialization tasks + ########################################################################### + def __init__(self, args): + """ + Does partial initialization which loads low power idle image + """ + super(e31x, self).__init__() + # Start clean by removing MPM-owned overlays. + active_overlays = self.list_active_overlays() + mpm_overlays = self.list_owned_overlays() + for overlay in active_overlays: + if overlay in mpm_overlays: + dtoverlay.rm_overlay(overlay) + # Apply idle overlay on boot to save power until + # an application tries to use the device. + self.args_cached = args + self.apply_idle_overlay() + self._device_initialized = False + + def _init_normal(self): + """ + Does full initialization + """ + if self._device_initialized: + return + if self.is_idle(): + self.remove_idle_overlay() + self.overlay_apply() + self.init_dboards(self.args_cached) + if not self._device_initialized: + # Don't try and figure out what's going on. Just give up. + return + # Initialize _do_not_reload with value from _default_args (mpm.conf) + self._do_not_reload = str2bool(self._default_args.get("no_reload_fpga", "False")) + self._tear_down = False + self._clock_source = None + self._time_source = None + self._available_endpoints = list(range(256)) + self.dboard = self.dboards[E310_DBOARD_SLOT_IDX] + try: + self._init_peripherals(self.args_cached) + 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, dboard_infos, override_dboard_pids, default_args): + """ + Initialize all the daughterboards + + dboard_infos -- List of dictionaries as returned from + PeriphManagerBase._get_dboard_eeprom_info() + 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") + # We have only one dboard + dboard_info = dboard_infos[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.", E310_DBOARD_SLOT_IDX) + else: + dboard_info.update({ + 'spi_nodes': spi_nodes, + 'default_args': default_args, + }) + + self.dboards.append(E31x_db(E310_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( + E310_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. + """ + if not self.dboards: + self.log.warning( + "No dboards found, skipping setting clock and time source " + "configuration." + ) + self._clock_source = E310_DEFAULT_CLOCK_SOURCE + self._time_source = E310_DEFAULT_TIME_SOURCE + else: + self.set_clock_source( + default_args.get('clock_source', E310_DEFAULT_CLOCK_SOURCE) + ) + self.set_time_source( + default_args.get('time_source', E310_DEFAULT_TIME_SOURCE) + ) + + 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() + self.crossbar_base_port = self.mboard_regs_control.get_xbar_baseport() + self.log.debug("crossbar base port: {}".format(self.crossbar_base_port)) + + # Init clocking + self._init_ref_clock_and_time(args) + # Init CHDR transports + self._xport_mgrs = { + 'liberio': E310XportMgrLiberio(self.log.getChild('liberio')), + } + # Init complete. + self.log.debug("mboard info: {}".format(self.mboard_info)) + + def _read_mboard_eeprom(self): + """ + Read out mboard EEPROM. + Returns a tuple: (eeprom_dict, eeprom_rawdata), where the the former is + a de-serialized dictionary representation of the data, and the latter + is a binary string with the raw data. + + If no EEPROM is defined, returns empty values. + """ + if len(self.mboard_eeprom_addr): + (eeprom_head, eeprom_rawdata) = e31x_legacy_eeprom.read_eeprom( + True, # isMotherboard + get_eeprom_paths(self.mboard_eeprom_addr)[self.mboard_eeprom_path_index], + self.mboard_eeprom_offset, + e31x_legacy_eeprom.MboardEEPROM.eeprom_header_format, + e31x_legacy_eeprom.MboardEEPROM.eeprom_header_keys, + self.mboard_eeprom_max_len + ) + self.log.trace("Found EEPROM metadata: `{}'" + .format(str(eeprom_head))) + self.log.trace("Read {} bytes of EEPROM data." + .format(len(eeprom_rawdata))) + return eeprom_head, eeprom_rawdata + # Nothing defined? Return defaults. + self.log.trace("No mboard EEPROM path defined. " + "Skipping mboard EEPROM readout.") + return {}, b'' + + def _get_dboard_eeprom_info(self): + """ + Read back EEPROM info from the daughterboards + """ + if self.dboard_eeprom_addr is None: + self.log.debug("No dboard EEPROM addresses given.") + return [] + dboard_eeprom_addrs = self.dboard_eeprom_addr \ + if isinstance(self.dboard_eeprom_addr, list) \ + else [self.dboard_eeprom_addr] + dboard_eeprom_paths = [] + self.log.trace("Identifying dboard EEPROM paths from addrs `{}'..." + .format(",".join(dboard_eeprom_addrs))) + for dboard_eeprom_addr in dboard_eeprom_addrs: + self.log.trace("Resolving %s...", dboard_eeprom_addr) + dboard_eeprom_paths += get_eeprom_paths(dboard_eeprom_addr) + self.log.trace("Found dboard EEPROM paths: {}" + .format(",".join(dboard_eeprom_paths))) + if len(dboard_eeprom_paths) > self.max_num_dboards: + self.log.warning("Found more EEPROM paths than daughterboards. " + "Ignoring some of them.") + dboard_eeprom_paths = dboard_eeprom_paths[:self.max_num_dboards] + dboard_info = [] + for dboard_idx, dboard_eeprom_path in enumerate(dboard_eeprom_paths): + self.log.debug("Reading EEPROM info for dboard %d...", dboard_idx) + dboard_eeprom_md, dboard_eeprom_rawdata = e31x_legacy_eeprom.read_eeprom( + False, # is not motherboard. + dboard_eeprom_path, + self.dboard_eeprom_offset, + e31x_legacy_eeprom.DboardEEPROM.eeprom_header_format, + e31x_legacy_eeprom.DboardEEPROM.eeprom_header_keys, + self.dboard_eeprom_max_len + ) + self.log.trace("Found dboard EEPROM metadata: `{}'" + .format(str(dboard_eeprom_md))) + self.log.trace("Read %d bytes of dboard EEPROM data.", + len(dboard_eeprom_rawdata)) + db_pid = dboard_eeprom_md.get('pid') + if db_pid is None: + self.log.warning("No dboard PID found in dboard EEPROM!") + else: + self.log.debug("Found dboard PID in EEPROM: 0x{:04X}" + .format(db_pid)) + dboard_info.append({ + 'eeprom_md': dboard_eeprom_md, + 'eeprom_rawdata': dboard_eeprom_rawdata, + 'pid': db_pid, + }) + return dboard_info + + ########################################################################### + # Session init and deinit + ########################################################################### + def claim(self): + """ + Fully initializes a device when the rpc_server claim() + gets called to revive the device from idle state to be used + by an UHD application + """ + super(e31x, self).claim() + try: + self._init_normal() + except Exception as ex: + self.log.error("e31x claim() failed: %s", str(ex)) + + 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")) + if "no_reload_fpga" in args: + self._do_not_reload = str2bool(args.get("no_reload_fpga")) or args.get("no_reload_fpga") == "" + result = super(e31x, self).init(args) + for xport_mgr in itervalues(self._xport_mgrs): + xport_mgr.init(args) + return result + + + def apply_idle_overlay(self): + """ + Load all overlays required to go into idle power savings mode. + """ + idle_overlay = self.get_idle_dt_overlay(self.device_info) + self.log.debug("Motherboard requests device tree overlay for Idle power savings mode: {}".format( + idle_overlay + )) + dtoverlay.apply_overlay_safe(idle_overlay) + + def remove_idle_overlay(self): + """ + Remove idle mode overlay. + """ + idle_overlay = self.get_idle_dt_overlay(self.device_info) + self.log.trace("Removing Idle overlay: {}".format( + idle_overlay + )) + dtoverlay.rm_overlay(idle_overlay) + + def list_owned_overlays(self): + """ + Lists all overlays that can be possibly applied by MPM. + """ + all_overlays = self.list_required_dt_overlays(self.device_info) + all_overlays.append(self.get_idle_dt_overlay(self.device_info)) + return all_overlays + + 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(e31x, 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)) + if not self._do_not_reload: + self.tear_down() + # Reset back to value from _default_args (mpm.conf) + self._do_not_reload = str2bool(self._default_args.get("no_reload_fpga", "False")) + + def tear_down(self): + """ + Tear down all members that need to be specially handled before + deconstruction. + For E310, this means the overlay. + """ + self.log.trace("Tearing down E310 device...") + self._tear_down = True + self.dboards = [] + self.dboard = None + self.mboard_regs_control = None + self._device_initialized = False + active_overlays = self.list_active_overlays() + self.log.trace("E310 has active device tree overlays: {}".format( + active_overlays + )) + for overlay in active_overlays: + dtoverlay.rm_overlay(overlay) + self.apply_idle_overlay() + + def is_idle(self): + """ + Determine if the device is in the idle state. + """ + active_overlays = self.list_active_overlays() + idle_overlay = self.get_idle_dt_overlay(self.device_info) + is_idle = idle_overlay in active_overlays + if is_idle: + self.log.trace("Found idle overlay: %s", idle_overlay) + return is_idle + + ########################################################################### + # 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)) + assert self.mboard_info['rpc_connection'] in ('local') + if 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 ('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'] == '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 = {} + device_info.update({ + 'fpga_version': "{}.{}".format( + *self.mboard_regs_control.get_compat_number()), + 'fpga_version_hash': "{:x}.{}".format( + *self.mboard_regs_control.get_git_hash()), + '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 ('internal',) + + 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 + self.mboard_regs_control.set_clock_source(clock_source) + + 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) + + ########################################################################### + # 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 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_ref_lock_sensor(self): + """ + #TODO: Where is ref lock signal coming from? + """ + self.log.trace("Querying ref lock status.") + lock_status = bool(self.mboard_regs_control.get_refclk_lock()) + return { + 'name': 'ref_locked', + 'type': 'BOOLEAN', + 'unit': 'locked' if lock_status else 'unlocked', + 'value': str(lock_status).lower(), + } + + def get_mb_temp_sensor(self): + """ + Get temperature sensor reading of the E310. + """ + self.log.trace("Reading temperature.") + temp = '-1' + raw_val = {} + data_probes = ['temp1_input'] + try: + for data_probe in data_probes: + raw_val[data_probe] = read_sysfs_sensors_value('jc-42.4-temp', data_probe, 'hwmon', 'name')[0] + temp = str(raw_val['temp1_input'] / 1000) + except ValueError: + self.log.warning("Error when converting temperature value") + except KeyError: + self.log.warning("Can't read temp on thermal_zone".format(sensor)) + return { + 'name': 'temp_mb', + 'type': 'REALNUM', + 'unit': 'C', + 'value': temp + } + + def get_fpga_temp_sensor(self): + """ + Get temperature sensor reading of the E310. + """ + self.log.trace("Reading temperature.") + temp = '-1' + raw_val = {} + data_probes = ['in_temp0_raw', 'in_temp0_scale', 'in_temp0_offset'] + try: + for data_probe in data_probes: + raw_val[data_probe] = read_sysfs_sensors_value('xadc', data_probe, 'iio', 'name')[0] + temp = str((raw_val['in_temp0_raw'] + raw_val['in_temp0_offset']) * raw_val['in_temp0_scale'] / 1000) + except ValueError: + self.log.warning("Error when converting temperature value") + except KeyError: + self.log.warning("Can't read temp on thermal_zone".format(sensor)) + return { + 'name': 'temp_fpga', + 'type': 'REALNUM', + 'unit': 'C', + 'value': temp + } + + ########################################################################### + # 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 != E310_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) + return db_eeprom_data + + def set_db_eeprom(self, dboard_idx, eeprom_data): + self.log.warn("Called set_db_eeprom(), but not implemented!") + raise NotImplementedError + + ########################################################################### + # 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/e31x_periphs.py b/mpm/python/usrp_mpm/periph_manager/e31x_periphs.py new file mode 100644 index 000000000..0b166e5bc --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/e31x_periphs.py @@ -0,0 +1,297 @@ +# +# Copyright 2018 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +E310 peripherals +""" + +import datetime +import math +from usrp_mpm.sys_utils.sysfs_gpio import SysFSGPIO, GPIOBank +from usrp_mpm.sys_utils.uio import UIO + +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, + {'label': '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_GPIO_MASTER = 0x0030 + MB_GPIO_RADIO_SRC = 0x0034 + MB_GPS_CTRL = 0x0038 + MB_GPS_STATUS = 0x003C + MB_DBOARD_CTRL = 0x0040 + MB_DBOARD_STATUS = 0x0044 + MB_XBAR_BASEPORT = 0x0048 + + # 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_CLK_LOCKED = 2 + + # Bitfield locations for the MB_GPS_CTRL register. + #FIXME: Update for E310 + 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. + #FIXME: Update for E310 + 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 + + # Bitfield locations for the MB_DBOARD_STATUS register. + MB_DBOARD_STATUS_RX_LOCK = 6 + MB_DBOARD_STATUS_TX_LOCK = 7 + + 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: + compat_number = self.peek32(self.MB_COMPAT_NUM) + minor = compat_number & 0xff + major = (compat_number>>16) & 0xff + return (major, minor) + + 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: + 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: + 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: + 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: + 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: + 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: + 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): + """ + Set time source + """ + pps_sel_val = 0x0 + if time_source == 'internal': + self.log.trace("Setting time source to internal") + pps_sel_val = self.MB_CLOCK_CTRL_PPS_SEL_INT + elif time_source == 'gpsdo': + self.log.debug("Setting time source to gpsdo...") + pps_sel_val = self.MB_CLOCK_CTRL_PPS_SEL_GPS + elif time_source == 'external': + self.log.debug("Setting time source to external...") + pps_sel_val = self.MB_CLOCK_CTRL_PPS_SEL_EXT + else: + assert False, "Cannot set to invalid time source: {}".format(time_source) + with self.regs: + 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): + """ + Set clock source + """ + if clock_source == 'internal': + self.log.trace("Setting clock source to internal") + else: + assert False, "Cannot set to invalid clock source: {}".format(clock_source) + + def get_fpga_type(self): + """ + Reads the type of the FPGA image currently loaded + Returns a string with the type (SG1, SG3) + """ + #TODO: Add SG1 and SG3? + return "" + + def get_gps_status(self): + """ + Get GPS status + """ + mask = 0x1F + with self.regs: + gps_status = self.peek32(self.MB_GPS_STATUS) & mask + return gps_status + + def get_refclk_lock(self): + """ + Check the status of the reference clock in FPGA. + """ + mask = 0b1 << self.MB_CLOCK_CTRL_REF_CLK_LOCKED + with self.regs: + 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: + 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) + + def get_ad9361_tx_lo_lock(self): + """ + Check the status of TX LO lock from CTRL_OUT pins from Catalina + """ + mask = 0b1 << self.MB_DBOARD_STATUS_TX_LOCK + with self.regs: + reg_val = self.peek32(self.MB_DBOARD_STATUS) + locked = (reg_val & mask) > 0 + if not locked: + self.log.warning("TX RF PLL reporting unlocked. ") + else: + self.log.trace("TX RF PLL locked") + return locked + + def get_ad9361_rx_lo_lock(self): + """ + Check the status of RX LO lock from CTRL_OUT pins from Catalina + """ + mask = 0b1 << self.MB_DBOARD_STATUS_RX_LOCK + with self.regs: + reg_val = self.peek32(self.MB_DBOARD_STATUS) + locked = (reg_val & mask) > 0 + if not locked: + self.log.warning("RX RF PLL reporting unlocked. ") + else: + self.log.trace("RX RF PLL locked") + return locked + + def get_xbar_baseport(self): + "Get the RFNoC crossbar base port" + with self.regs: + return self.peek32(self.MB_XBAR_BASEPORT) diff --git a/mpm/python/usrp_mpm/rpc_server.py b/mpm/python/usrp_mpm/rpc_server.py index c67bf769b..80b4eb44e 100644 --- a/mpm/python/usrp_mpm/rpc_server.py +++ b/mpm/python/usrp_mpm/rpc_server.py @@ -286,6 +286,8 @@ class MPMServer(RPCServer): self._state.claim_status.value = True self.periph_manager.claimed = True self.periph_manager.claim() + if self.periph_manager.clear_rpc_method_registry_on_unclaim: + self._init_rpc_calls(self.periph_manager) self._state.lock.release() self.session_id = session_id + " ({})".format(self.client_host) self._reset_timer() @@ -337,6 +339,8 @@ class MPMServer(RPCServer): self._state.claim_status.value = False self._state.claim_token.value = b'' self.session_id = None + if self.periph_manager.clear_rpc_method_registry_on_unclaim: + self.clear_method_registry() try: self.periph_manager.claimed = False self.periph_manager.unclaim() @@ -451,19 +455,13 @@ class MPMServer(RPCServer): ########################################################################### # Update components ########################################################################### - def reset_mgr(self): + def clear_method_registry(self): """ - Reset the Peripheral Manager for this RPC server. + Clear all the methods in the RPC server method cache. """ - self.log.info("Resetting peripheral manager.") - self.periph_manager.tear_down() - self.periph_manager = None - self.periph_manager = self._mgr_generator() - self._init_rpc_calls(self.periph_manager) # RPCServer caches RPC methods, but that cache is not accessible here # (because Cython). Re-running `RPCServer.__init__` clears that cache, - # and allows us to register new RPC methods (which we need to do because - # we're resetting the PeriphManager). + # and allows us to register new RPC methods. # A note on maintenance: This has been deemed safe through inspection of # the RPCServer source code. However, this is not typical Python, and # changes in future versions of RPCServer may cause issues. @@ -471,6 +469,19 @@ class MPMServer(RPCServer): pack_params={'use_bin_type': True}, ) + def reset_mgr(self): + """ + Reset the Peripheral Manager for this RPC server. + """ + self.log.info("Resetting peripheral manager.") + self.periph_manager.tear_down() + self.periph_manager = None + self.periph_manager = self._mgr_generator() + self._init_rpc_calls(self.periph_manager) + # Clear the method cache in order to remove stale references to + # methods from the old peripheral manager (the one before reset) + self.clear_method_registry() + def update_component(self, token, file_metadata_l, data_l): """" Updates the device component files specified by the metadata and data diff --git a/mpm/tools/CMakeLists.txt b/mpm/tools/CMakeLists.txt index 05838b897..245720656 100644 --- a/mpm/tools/CMakeLists.txt +++ b/mpm/tools/CMakeLists.txt @@ -13,7 +13,7 @@ install(PROGRAMS set(eeprom_tool_sources) set(eeprom_tool_libs) -if(ENABLE_LIBMPM) +if(ENABLE_LIBMPM AND NOT ENABLE_E300) message(STATUS "Adding MPM EEPROM tools...") set(eeprom_tool_libs eeprom.c) list(APPEND eeprom_tool_sources @@ -23,7 +23,7 @@ if(ENABLE_LIBMPM) eeprom-init.c eeprom-set-flags.c ) -endif(ENABLE_LIBMPM) +endif(ENABLE_LIBMPM AND NOT ENABLE_E300) if(ENABLE_MYKONOS) message(STATUS "Adding N3XX-specific EEPROM tools...") set(eeprom_tool_libs eeprom.c) |