From 725debb96408f0e1134fe1f365c4afaf07f1a949 Mon Sep 17 00:00:00 2001 From: Martin Braun Date: Wed, 28 Feb 2018 14:20:56 -0800 Subject: mpm: Rename n310 to n3xx This module (and class) are, in fact, used for all N3xx-derivates so renaming it is the more correct thing to do. --- mpm/CMakeLists.txt | 4 +- mpm/python/n3xx_bist | 28 +- mpm/python/usrp_hwd.py | 2 +- .../dboard_manager/magnesium_update_cpld.py | 2 +- mpm/python/usrp_mpm/periph_manager/CMakeLists.txt | 2 +- mpm/python/usrp_mpm/periph_manager/n310.py | 1304 -------------------- mpm/python/usrp_mpm/periph_manager/n3xx.py | 1304 ++++++++++++++++++++ mpm/python/usrp_mpm/rpc_server.py | 2 +- mpm/python/usrp_mpm/sys_utils/net.py | 2 +- 9 files changed, 1326 insertions(+), 1324 deletions(-) delete mode 100644 mpm/python/usrp_mpm/periph_manager/n310.py create mode 100644 mpm/python/usrp_mpm/periph_manager/n3xx.py (limited to 'mpm') diff --git a/mpm/CMakeLists.txt b/mpm/CMakeLists.txt index f805e4610..67130de4b 100644 --- a/mpm/CMakeLists.txt +++ b/mpm/CMakeLists.txt @@ -106,8 +106,8 @@ SET(PKG_MAN_DIR share/man/man1) # Setup library configuration ######################################################################## SET(CMAKE_CXX_STANDARD 11) -SET(MPM_DEVICE "n310" CACHE STRING "Choose an MPM device to build") -SET_PROPERTY(CACHE MPM_DEVICE PROPERTY STRINGS n310 tests) +SET(MPM_DEVICE "n3xx" CACHE STRING "Choose an MPM device to build") +SET_PROPERTY(CACHE MPM_DEVICE PROPERTY STRINGS n3xx tests) SET(UHD_HOST_ROOT ${CMAKE_SOURCE_DIR}/../host) diff --git a/mpm/python/n3xx_bist b/mpm/python/n3xx_bist index ae243d95e..00732a6c3 100755 --- a/mpm/python/n3xx_bist +++ b/mpm/python/n3xx_bist @@ -5,7 +5,9 @@ # SPDX-License-Identifier: GPL-3.0-or-later # """ -N310 Built-In Self Test (BIST) +N3XX Built-In Self Test (BIST) + +Will work on all derivatives of the N3xx series. """ from __future__ import print_function @@ -230,7 +232,7 @@ def expand_options(option_list): ############################################################################## # Bist class ############################################################################## -class N310BIST(object): +class N3XXBIST(object): """ BIST Tool for the USRP N3xx series """ @@ -280,11 +282,11 @@ class N310BIST(object): return parser def __init__(self): - self.args = N310BIST.make_arg_parser().parse_args() + self.args = N3XXBIST.make_arg_parser().parse_args() self.args.option = expand_options(self.args.option) try: - from usrp_mpm.periph_manager.n310 import n310 - default_rev = n310.mboard_max_rev + from usrp_mpm.periph_manager.n3xx import n3xx + default_rev = n3xx.mboard_max_rev except ImportError: # This means we're in dry run mode or something like that, so just # pick something @@ -456,8 +458,8 @@ class N310BIST(object): "eps": 34.11, "mode": 3 } - from usrp_mpm.periph_manager import n310 - gpio_tca6424 = n310.TCA6424(self.mb_rev) + from usrp_mpm.periph_manager import n3xx + gpio_tca6424 = n3xx.TCA6424(self.mb_rev) # Turn on GPS, give some time to acclimatize gpio_tca6424.set("PWREN-GPS") time.sleep(5) @@ -687,10 +689,10 @@ class N310BIST(object): 'write_patterns': list(patterns), 'read_patterns': list(patterns), } - from usrp_mpm.periph_manager import n310 - gpio_tca6424 = n310.TCA6424(self.mb_rev) + from usrp_mpm.periph_manager import n3xx + gpio_tca6424 = n3xx.TCA6424(self.mb_rev) gpio_tca6424.set("FPGA-GPIO-EN") - mb_regs = n310.MboardRegsControl(n310.n310.mboard_regs_label, self.log) + mb_regs = n3xx.MboardRegsControl(n3xx.n3xx.mboard_regs_label, self.log) mb_regs.set_fp_gpio_master(0xFFF) # Allow some time for the front-panel GPIOs to become usable time.sleep(.5) @@ -698,7 +700,7 @@ class N310BIST(object): ddr2 = 0xfc0 def _run_gpio(ddr, patterns): " Run a GPIO test for a given set of patterns " - gpio_ctrl = n310.FrontpanelGPIO(ddr) + gpio_ctrl = n3xx.FrontpanelGPIO(ddr) for pattern in patterns: gpio_set_all(gpio_ctrl, pattern, GPIO_WIDTH, ddr) time.sleep(0.1) @@ -771,7 +773,7 @@ def gpio_set_all(gpio_bank, value, gpio_size, ddr_mask): gpio_bank -- gpio bank type. value -- value to set onto gpio bank. gpio_size -- size of the gpio bank - ddr_mask -- data direction register bit mask. 0 is input; 1 is output. + ddr_mask -- data direction register bit mask. 0 is input; 1 is output. """ ddr_size = bin(ddr_mask).count("1") value_bitstring = ('{0:0' + str(ddr_size) + 'b}').format(value)[-(gpio_size):] @@ -785,7 +787,7 @@ def gpio_set_all(gpio_bank, value, gpio_size, ddr_mask): ############################################################################## def main(): " Go, go, go! " - return N310BIST().run() + return N3XXBIST().run() if __name__ == '__main__': exit(not main()) diff --git a/mpm/python/usrp_hwd.py b/mpm/python/usrp_hwd.py index 267415601..0b2cd1ecb 100755 --- a/mpm/python/usrp_hwd.py +++ b/mpm/python/usrp_hwd.py @@ -112,7 +112,7 @@ def init_only(log, default_args): """ # Create the periph_manager for this device # This call will be forwarded to the device specific implementation - # e.g. in periph_manager/n310.py + # e.g. in periph_manager/n3xx.py # Which implementation is called will be determined during # configuration with cmake (-DMPM_DEVICE). # mgr is thus derived from PeriphManagerBase diff --git a/mpm/python/usrp_mpm/dboard_manager/magnesium_update_cpld.py b/mpm/python/usrp_mpm/dboard_manager/magnesium_update_cpld.py index a5ccde40b..8459d90b9 100755 --- a/mpm/python/usrp_mpm/dboard_manager/magnesium_update_cpld.py +++ b/mpm/python/usrp_mpm/dboard_manager/magnesium_update_cpld.py @@ -12,7 +12,7 @@ import argparse import subprocess import pyudev from usrp_mpm.mpmlog import get_logger -from usrp_mpm.periph_manager.n310 import MboardRegsControl +from usrp_mpm.periph_manager.n3xx import MboardRegsControl OPENOCD_DIR = "/usr/share/openocd/scripts" CONFIGS = { diff --git a/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt b/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt index 7c988c96d..8000ebc6d 100644 --- a/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt +++ b/mpm/python/usrp_mpm/periph_manager/CMakeLists.txt @@ -11,7 +11,7 @@ SET(USRP_MPM_FILES ${USRP_MPM_FILES}) SET(USRP_MPM_PERIPHMGR_FILES ${CMAKE_CURRENT_SOURCE_DIR}/__init__.py.in ${CMAKE_CURRENT_SOURCE_DIR}/base.py - ${CMAKE_CURRENT_SOURCE_DIR}/n310.py + ${CMAKE_CURRENT_SOURCE_DIR}/n3xx.py ${CMAKE_CURRENT_SOURCE_DIR}/test.py ) LIST(APPEND USRP_MPM_FILES ${USRP_MPM_PERIPHMGR_FILES}) diff --git a/mpm/python/usrp_mpm/periph_manager/n310.py b/mpm/python/usrp_mpm/periph_manager/n310.py deleted file mode 100644 index cff7da997..000000000 --- a/mpm/python/usrp_mpm/periph_manager/n310.py +++ /dev/null @@ -1,1304 +0,0 @@ -# -# Copyright 2017 Ettus Research, a National Instruments Company -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -""" -N310 implementation module -""" - -from __future__ import print_function -import os -import copy -import shutil -import subprocess -import json -import datetime -import threading -from six import iteritems, itervalues -from builtins import object -from usrp_mpm.gpsd_iface import GPSDIface -from usrp_mpm.periph_manager import PeriphManagerBase -from usrp_mpm.mpmtypes import SID -from usrp_mpm.mpmutils import assert_compat_number, str2bool -from usrp_mpm.rpc_server import no_rpc -from usrp_mpm.sys_utils import dtoverlay -from usrp_mpm.sys_utils.sysfs_gpio import SysFSGPIO, GPIOBank -from usrp_mpm.sys_utils.uio import UIO -from usrp_mpm.sys_utils.sysfs_thermal import read_thermal_sensor_value -from usrp_mpm.xports import XportMgrUDP, XportMgrLiberio - -N3XX_DEFAULT_EXT_CLOCK_FREQ = 10e6 -N3XX_DEFAULT_CLOCK_SOURCE = 'internal' -N3XX_DEFAULT_TIME_SOURCE = 'internal' -N3XX_DEFAULT_ENABLE_GPS = True -N3XX_DEFAULT_ENABLE_FPGPIO = True -N3XX_FPGA_COMPAT = (5, 2) -N3XX_MONITOR_THREAD_INTERVAL = 1.0 # seconds -N3XX_SFP_TYPES = {0:"", 1:"1G", 2:"10G", 3:"A"} - -############################################################################### -# Additional peripheral controllers specific to Magnesium -############################################################################### -class TCA6424(object): - """ - Abstraction layer for the port/gpio expander - pins_list is an array of different version of TCA6424 pins map. - First element of this array corresponding to revC, second is revD etc... - """ - pins_list = [ - ( - 'PWREN-CLK-MGT156MHz', - 'NETCLK-CE', #revC name: 'PWREN-CLK-WB-CDCM', - 'NETCLK-RESETn', #revC name: 'WB-CDCM-RESETn', - 'NETCLK-PR0', #revC name: 'WB-CDCM-PR0', - 'NETCLK-PR1', #revC name: 'WB-CDCM-PR1', - 'NETCLK-OD0', #revC name: 'WB-CDCM-OD0', - 'NETCLK-OD1', #revC name: 'WB-CDCM-OD1', - 'NETCLK-OD2', #revC name: 'WB-CDCM-OD2', - 'PWREN-CLK-MAINREF', - 'CLK-MAINSEL-25MHz', #revC name: 'CLK-MAINREF-SEL1', - 'CLK-MAINSEL-EX_B', #revC name: 'CLK-MAINREF-SEL0', - '12', - 'CLK-MAINSEL-GPS', #revC name: '13', - 'FPGA-GPIO-EN', - 'PWREN-CLK-WB-20MHz', - 'PWREN-CLK-WB-25MHz', - 'GPS-PHASELOCK', - 'GPS-nINITSURV', - 'GPS-nRESET', - 'GPS-WARMUP', - 'GPS-SURVEY', - 'GPS-LOCKOK', - 'GPS-ALARM', - 'PWREN-GPS', - ), - ( - 'NETCLK-PR1', - 'NETCLK-PR0', - 'NETCLK-CE', - 'NETCLK-RESETn', - 'NETCLK-OD2', - 'NETCLK-OD1', - 'NETCLK-OD0', - 'PWREN-CLK-MGT156MHz', - 'PWREN-CLK-MAINREF', - 'CLK-MAINSEL-25MHz', - 'CLK-MAINSEL-EX_B', - '12', - 'CLK-MAINSEL-GPS', - 'FPGA-GPIO-EN', - 'PWREN-CLK-WB-20MHz', - 'PWREN-CLK-WB-25MHz', - 'GPS-PHASELOCK', - 'GPS-nINITSURV', - 'GPS-nRESET', - 'GPS-WARMUP', - 'GPS-SURVEY', - 'GPS-LOCKOK', - 'GPS-ALARM', - 'PWREN-GPS', - )] - - def __init__(self, rev): - # Default state: Turn on GPS power, take GPS out of reset or - # init-survey, turn on 156.25 MHz clock - # min Support from revC or rev = 2 - if rev == 2: - self.pins = self.pins_list[0] - else: - self.pins = self.pins_list[1] - - default_val = 0x860101 if rev == 2 else 0x860780 - self._gpios = SysFSGPIO('tca6424', 0xFFF7FF, 0x86F7FF, default_val) - - def set(self, name, value=None): - """ - Assert a pin by name - """ - assert name in self.pins - self._gpios.set(self.pins.index(name), value=value) - - def reset(self, name): - """ - Deassert a pin by name - """ - self.set(name, value=0) - - def get(self, name): - """ - Read back a pin by name - """ - assert name in self.pins - return self._gpios.get(self.pins.index(name)) - - -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, - 0xFFF, # use_mask - ddr - ) - -class BackpanelGPIO(GPIOBank): - """ - Abstraction layer for the back panel GPIO - """ - EMIO_BASE = 54 - BP_GPIO_OFFSET = 45 - LED_LINK = 0 - LED_REF = 1 - LED_GPS = 2 - - def __init__(self): - GPIOBank.__init__( - self, - 'zynq_gpio', - self.BP_GPIO_OFFSET + self.EMIO_BASE, - 0x7, # use_mask - 0x7, # ddr - ) - -class MboardRegsControl(object): - """ - Control the FPGA Motherboard registers - """ - # Motherboard registers - M_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_SFP0_INFO = 0x0028 - MB_SFP1_INFO = 0x002C - MB_GPIO_MASTER = 0x0030 - MB_GPIO_RADIO_SRC = 0x0034 - - # Bitfield locations for the MB_CLOCK_CTRL register. - MB_CLOCK_CTRL_PPS_SEL_INT_10 = 0 # pps_sel is one-hot encoded! - MB_CLOCK_CTRL_PPS_SEL_INT_25 = 1 - MB_CLOCK_CTRL_PPS_SEL_EXT = 2 - MB_CLOCK_CTRL_PPS_SEL_GPSDO = 3 - MB_CLOCK_CTRL_PPS_OUT_EN = 4 # output enabled = 1 - MB_CLOCK_CTRL_MEAS_CLK_RESET = 12 # set to 1 to reset mmcm, default is 0 - MB_CLOCK_CTRL_MEAS_CLK_LOCKED = 13 # locked indication for meas_clk mmcm - - 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.M_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 12 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 12 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 12 pins GPIO - 00: means the pin is driven by radio 0 - 01: means the pin is driven by radio 1 - 10: means the pin is driven by radio 2 - 11: means the pin is driven by radio 3 - """ - 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 12 pins GPIO. - 00: means the pin is driven by radio 0 - 01: means the pin is driven by radio 1 - 10: means the pin is driven by radio 2 - 11: means the pin is driven by radio 3 - """ - 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': - assert ref_clk_freq in (10e6, 25e6) - if ref_clk_freq == 10e6: - self.log.trace("Setting time source to internal " - "(10 MHz reference)...") - pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_INT_10 - elif ref_clk_freq == 25e6: - self.log.trace("Setting time source to internal " - "(25 MHz reference)...") - pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_INT_25 - elif time_source == 'external': - self.log.trace("Setting time source to external...") - pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_EXT - elif time_source == 'gpsdo': - self.log.trace("Setting time source to gpsdo...") - pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_GPSDO - else: - assert False - with self.regs.open(): - reg_val = self.peek32(self.MB_CLOCK_CTRL) & 0xFFFFFFF0 - reg_val = reg_val | (pps_sel_val & 0xF) - self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val)) - self.poke32(self.MB_CLOCK_CTRL, reg_val) - - def enable_pps_out(self, enable): - """ - Enables the PPS/Trig output on the back panel - """ - self.log.trace("%s PPS/Trig output!", - "Enabling" if enable else "Disabling") - mask = 0xFFFFFFFF ^ (0b1 << self.MB_CLOCK_CTRL_PPS_OUT_EN) - with self.regs.open(): - # mask the bit to clear it: - reg_val = self.peek32(self.MB_CLOCK_CTRL) & mask - if enable: - # set the bit if desired: - reg_val = reg_val | (0b1 << self.MB_CLOCK_CTRL_PPS_OUT_EN) - self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val)) - self.poke32(self.MB_CLOCK_CTRL, reg_val) - - def reset_meas_clk_mmcm(self, reset=True): - """ - Reset or unreset the MMCM for the measurement clock in the FPGA TDC. - """ - self.log.trace("%s measurement clock MMCM reset...", - "Asserting" if reset else "Clearing") - mask = 0xFFFFFFFF ^ (0b1 << self.MB_CLOCK_CTRL_MEAS_CLK_RESET) - with self.regs.open(): - # mask the bit to clear it - reg_val = self.peek32(self.MB_CLOCK_CTRL) & mask - if reset: - # set the bit if desired - reg_val = reg_val | (0b1 << self.MB_CLOCK_CTRL_MEAS_CLK_RESET) - self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val)) - self.poke32(self.MB_CLOCK_CTRL, reg_val) - - def get_meas_clock_mmcm_lock(self): - """ - Check the status of the MMCM for the measurement clock in the FPGA TDC. - """ - mask = 0b1 << self.MB_CLOCK_CTRL_MEAS_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("Measurement clock MMCM reporting unlocked. " - "MB_CLOCK_CTRL reg: 0x{:08X}".format(reg_val)) - else: - self.log.trace("Measurement clock MMCM locked!") - return locked - - def get_fpga_type(self): - """ - Reads the type of the FPGA image currently loaded - Returns a string with the type (ie HG, XG, AA, etc.) - """ - with self.regs.open(): - sfp0_info_rb = self.peek32(self.MB_SFP0_INFO) - sfp1_info_rb = self.peek32(self.MB_SFP1_INFO) - # Print the registers values as 32-bit hex values - self.log.trace("SFP0 Info: 0x{0:0{1}X}".format(sfp0_info_rb, 8)) - self.log.trace("SFP1 Info: 0x{0:0{1}X}".format(sfp1_info_rb, 8)) - - sfp0_type = N3XX_SFP_TYPES.get((sfp0_info_rb & 0x0000FF00) >> 8, "") - sfp1_type = N3XX_SFP_TYPES.get((sfp1_info_rb & 0x0000FF00) >> 8, "") - self.log.trace("SFP types: ({}, {})".format(sfp0_type, sfp1_type)) - if (sfp0_type == "") or (sfp1_type == ""): - return "" - elif (sfp0_type == "1G") and (sfp1_type == "10G"): - return "HG" - elif (sfp0_type == "10G") and (sfp1_type == "10G"): - return "XG" - elif (sfp0_type == "10G") and (sfp1_type == "A"): - return "XA" - elif (sfp0_type == "A") and (sfp1_type == "A"): - return "AA" - else: - self.log.warning("Unrecognized SFP type combination: ({}, {})".format( - sfp0_type, sfp1_type - )) - return "" - - -############################################################################### -# Transport managers -############################################################################### -class N310XportMgrUDP(XportMgrUDP): - " N310-specific UDP configuration " - xbar_dev = "/dev/crossbar0" - iface_config = { - 'sfp0': { - 'label': 'misc-enet-regs0', - 'xbar': 0, - 'xbar_port': 0, - 'ctrl_src_addr': 0, - }, - 'sfp1': { - 'label': 'misc-enet-regs1', - 'xbar': 0, - 'xbar_port': 1, - 'ctrl_src_addr': 1, - }, - 'eth1': { - 'label': 'misc-enet-regs0', - 'xbar': 0, - 'xbar_port': 0, - 'ctrl_src_addr': 0, - }, - 'eth2': { - 'label': 'misc-enet-regs1', - 'xbar': 0, - 'xbar_port': 1, - 'ctrl_src_addr': 1, - }, - } - -class N310XportMgrLiberio(XportMgrLiberio): - " N310-specific Liberio configuration " - max_chan = 10 - xbar_dev = "/dev/crossbar0" - xbar_port = 2 - -############################################################################### -# Main Class -############################################################################### -class n310(PeriphManagerBase): - """ - Holds N310 specific attributes and methods - """ - ######################################################################### - # Overridables - # - # See PeriphManagerBase for documentation on these fields - ######################################################################### - description = "N300-Series Device" - pids = {0x4242: 'n310', 0x4240: 'n300'} - mboard_eeprom_addr = "e0005000.i2c" - mboard_eeprom_max_len = 256 - mboard_info = {"type": "n3xx", - "product": "unknown", - } - mboard_max_rev = 4 # 4 == RevE - mboard_sensor_callback_map = { - 'ref_locked': 'get_ref_lock_sensor', - 'gps_locked': 'get_gps_lock_sensor', - 'gps_time': 'get_gps_time_sensor', - 'gps_tpv': 'get_gps_tpv_sensor', - 'gps_sky': 'get_gps_sky_sensor', - 'temp': 'get_temp_sensor', - 'fan': 'get_fan_sensor', - } - dboard_eeprom_addr = "e0004000.i2c" - dboard_eeprom_max_len = 64 - - # 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"] - # N310-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(eeprom_md, device_args): - """ - 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 - """ - # In the N3xx case, we name the dtbo file the same as the product. - # N310 -> n310.dtbo, N300 -> n300.dtbo and so on. - return [n310.pids[eeprom_md['pid']]] - - ########################################################################### - # Ctor and device initialization tasks - ########################################################################### - def __init__(self, args): - self._tear_down = False - self._status_monitor_thread = None - self._ext_clock_freq = None - self._clock_source = None - self._time_source = None - self._available_endpoints = list(range(256)) - self._bp_leds = None - super(n310, self).__init__(args) - if not self._device_initialized: - # Don't try and figure out what's going on. Just give up. - return - 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 _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( - N3XX_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', N3XX_DEFAULT_EXT_CLOCK_FREQ) - ) - if len(self.dboards) == 0: - self.log.warning( - "No dboards found, skipping setting clock and time source " \ - "configuration." - ) - self._clock_source = N3XX_DEFAULT_CLOCK_SOURCE - self._time_source = N3XX_DEFAULT_TIME_SOURCE - else: - self.set_clock_source( - default_args.get('clock_source', N3XX_DEFAULT_CLOCK_SOURCE) - ) - self.set_time_source( - default_args.get('time_source', N3XX_DEFAULT_TIME_SOURCE) - ) - self.enable_pps_out( - default_args.get('pps_export', True) - ) - - def _init_meas_clock(self): - """ - Initialize the TDC measurement clock. After this function returns, the - FPGA TDC meas_clock is valid. - """ - # No need to toggle reset here, simply confirm it is out of reset. - self.mboard_regs_control.reset_meas_clk_mmcm(False) - if not self.mboard_regs_control.get_meas_clock_mmcm_lock(): - raise RuntimeError("Measurement clock failed to init") - - 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 (update back-panel GPS LED) - - REF lock (update back-panel REF LED) - """ - self.log.trace("Launching monitor thread...") - cond = threading.Condition() - cond.acquire() - while not self._tear_down: - gps_locked = bool(self._gpios.get("GPS-LOCKOK")) - self.log.trace("Setting GPS LED to {}".format(gps_locked)) - self._bp_leds.set(self._bp_leds.LED_GPS, int(gps_locked)) - ref_locked = self.get_ref_lock_sensor()['value'] == 'true' - self.log.trace("Setting REF LED to {}".format(ref_locked)) - self._bp_leds.set(self._bp_leds.LED_REF, int(ref_locked)) - # Now wait - if cond.wait_for( - lambda: self._tear_down, - N3XX_MONITOR_THREAD_INTERVAL): - break - cond.release() - self.log.trace("Terminating monitor thread.") - - def _init_peripherals(self, args): - """ - Turn on all peripherals. This may throw an error on failure, so make - sure to catch it. - - Periphals 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 peripherals - self.log.trace("Initializing TCA6424 port expander controls...") - self._gpios = TCA6424(int(self.mboard_info['rev'])) - self.log.trace("Initializing back panel led controls...") - self._bp_leds = BackpanelGPIO() - self.log.trace("Enabling power of MGT156MHZ clk") - self._gpios.set("PWREN-CLK-MGT156MHz") - self.enable_1g_ref_clock() - self.enable_gps( - enable=str2bool( - args.get('enable_gps', N3XX_DEFAULT_ENABLE_GPS) - ) - ) - self.enable_fp_gpio( - enable=str2bool( - args.get( - 'enable_fp_gpio', - N3XX_DEFAULT_ENABLE_FPGPIO - ) - ) - ) - # 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 clocking - self.enable_ref_clock(enable=True) - self._ext_clock_freq = None - self._init_ref_clock_and_time(args) - self._init_meas_clock() - # Init CHDR transports - self._xport_mgrs = { - 'udp': N310XportMgrUDP(self.log.getChild('UDP')), - 'liberio': N310XportMgrLiberio(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="N3xxStatusMonitorThread", - 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 "clock_source" in args: - self.set_clock_source(args.get("clock_source")) - if "clock_source" in args or "time_source" in args: - self.set_time_source(args.get("time_source", self.get_time_source())) - result = super(n310, 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(n310, 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 N310, this means the overlay. - """ - self.log.trace("Tearing down N3xx device...") - self._tear_down = True - if self._device_initialized: - self._status_monitor_thread.join(3 * N3XX_MONITOR_THREAD_INTERVAL) - if self._status_monitor_thread.is_alive(): - self.log.error("Could not terminate monitor thread!") - active_overlays = self.list_active_overlays() - self.log.trace("N310 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 len(self._available_endpoints) == 0: - 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.debug( - "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. - """ - device_info = self._xport_mgrs['udp'].get_xport_info() \ - if self._device_initialized else {} - device_info.update({ - 'fpga_version': "{}.{}".format( - *self.mboard_regs_control.get_compat_number()) - }) - 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.trace("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 - if clock_source == 'internal': - self._gpios.set("CLK-MAINSEL-EX_B") - self._gpios.set("CLK-MAINSEL-25MHz") - self._gpios.reset("CLK-MAINSEL-GPS") - elif clock_source == 'gpsdo': - self._gpios.set("CLK-MAINSEL-EX_B") - self._gpios.reset("CLK-MAINSEL-25MHz") - self._gpios.set("CLK-MAINSEL-GPS") - else: # external - self._gpios.reset("CLK-MAINSEL-EX_B") - self._gpios.reset("CLK-MAINSEL-GPS") - # SKY13350 needs to be in known state - self._gpios.set("CLK-MAINSEL-25MHz") - self._clock_source = clock_source - ref_clk_freq = self.get_ref_clock_freq() - self.log.info("Reference clock frequency is: {} MHz".format( - ref_clk_freq/1e6 - )) - 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, ref_clk_freq/1e6 - ) - 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. - """ - assert freq in (10e6, 20e6, 25e6) - self.log.debug("We've been told the external reference clock " \ - "frequency is {} MHz.".format(freq/1e6)) - if self._ext_clk_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" - return { - 'internal': 25e6, - 'external': self._ext_clock_freq, - 'gpsdo': 20e6, - }[self._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() - self._time_source = time_source - self.mboard_regs_control.set_time_source( - time_source, self.get_ref_clock_freq()) - - 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 12 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 12 pins GPIO - 00: means the pin is driven by radio 0 - 01: means the pin is driven by radio 1 - 10: means the pin is driven by radio 2 - 11: means the pin is driven by radio 3 - """ - 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 12 pins GPIO. - 00: means the pin is driven by radio 0 - 01: means the pin is driven by radio 1 - 10: means the pin is driven by radio 2 - 11: means the pin is driven by radio 3 - """ - return self.mboard_regs_control.get_fp_gpio_radio_src() - ########################################################################### - # Hardware periphal controls - ########################################################################### - def enable_pps_out(self, enable): - " Export a PPS/Trigger to the back panel " - self.mboard_regs_control.enable_pps_out(enable) - - def enable_gps(self, enable): - """ - Turn power to the GPS off or on. - """ - self.log.trace("{} power to GPS".format( - "Enabling" if enable else "Disabling" - )) - self._gpios.set("PWREN-GPS", int(bool(enable))) - - def enable_fp_gpio(self, enable): - """ - Turn power to the front panel GPIO off or on. - """ - self.log.trace("{} power to front-panel GPIO".format( - "Enabling" if enable else "Disabling" - )) - self._gpios.set("FPGA-GPIO-EN", int(bool(enable))) - - def enable_ref_clock(self, enable): - """ - Enables the ref clock voltage (+3.3-MAINREF). Without setting this to - True, *no* ref clock works. - """ - self.log.trace("{} power to reference clocks".format( - "Enabling" if enable else "Disabling" - )) - self._gpios.set("PWREN-CLK-MAINREF", int(bool(enable))) - - def enable_1g_ref_clock(self): - """ - Enables 125 MHz refclock for 1G interface. - """ - self.log.trace("Enable 125 MHz Clock for 1G SFP interface.") - self._gpios.set("NETCLK-CE") - self._gpios.set("NETCLK-RESETn", 0) - self._gpios.set("NETCLK-PR0", 1) - self._gpios.set("NETCLK-PR1", 1) - self._gpios.set("NETCLK-OD0", 1) - self._gpios.set("NETCLK-OD1", 1) - self._gpios.set("NETCLK-OD2", 0) - self._gpios.set("PWREN-CLK-WB-25MHz", 1) - self.log.trace("Finished configuring NETCLK CDCM.") - self._gpios.set("NETCLK-RESETn", 1) - - ########################################################################### - # Sensors - ########################################################################### - def get_ref_lock_sensor(self): - """ - The N310 has no ref lock sensor, but because the ref lock is - historically considered a motherboard-level sensor, we will return the - combined lock status of all daughterboards. If no dboard is connected, - or none has a ref lock sensor, we simply return True. - """ - self.log.trace( - "Querying ref lock status from %d dboards.", - len(self.dboards) - ) - lock_status = all([ - not hasattr(db, 'get_ref_lock') or db.get_ref_lock() - for db in self.dboards - ]) - return { - 'name': 'ref_locked', - 'type': 'BOOLEAN', - 'unit': 'locked' if lock_status else 'unlocked', - 'value': str(lock_status).lower(), - } - - def get_temp_sensor(self): - """ - Get temperature sensor reading of the N3xx. - """ - self.log.trace("Reading fpga temperature.") - return_val = '-1' - try: - raw_val = read_thermal_sensor_value('fpga-thermal-zone', 'temp') - return_val = str(raw_val/1000) - except ValueError: - self.log.warning("Error when converting temperature value") - except KeyError: - self.log.warning("Can't read temp on fpga-thermal-zone") - return { - 'name': 'temperature', - 'type': 'REALNUM', - 'unit': 'C', - 'value': return_val - } - - def get_fan_sensor(self): - """ - Get cooling device reading of N3xx. In this case the speed of fan 0. - """ - self.log.trace("Reading fpga cooling device.") - return_val = '-1' - try: - raw_val = read_thermal_sensor_value('ec-fan0', 'cur_state') - return_val = str(raw_val) - except ValueError: - self.log.warning("Error when converting fan speed value") - except KeyError: - self.log.warning("Can't read cur_state on ec-fan0") - return { - 'name': 'cooling fan', - 'type': 'INTEGER', - 'unit': 'rpm', - 'value': return_val - } - - 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") - 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(), - } - - def get_gps_time_sensor(self): - """ - Calculates GPS time using a TPV response from GPSd, and returns as a sensor dict - - This time is not high accuracy. - """ - self.log.trace("Polling GPS time results from GPSD") - with GPSDIface() as gps_iface: - response_mode = 0 - # Read responses from GPSD until we get a non-trivial mode - while response_mode <= 0: - gps_info = gps_iface.get_gps_info(resp_class='tpv', timeout=15) - self.log.trace("GPS info: {}".format(gps_info)) - response_mode = gps_info.get("mode", 0) - time_str = gps_info.get("time", "") - self.log.trace("GPS time string: {}".format(time_str)) - time_dt = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S.%fZ") - self.log.trace("GPS datetime: {}".format(time_dt)) - epoch_dt = datetime.datetime(1970, 1, 1) - gps_time = int((time_dt - epoch_dt).total_seconds()) - return { - 'name': 'gps_time', - 'type': 'INTEGER', - 'unit': 'seconds', - 'value': str(gps_time), - } - - def get_gps_tpv_sensor(self): - """ - Get a TPV response from GPSd as a sensor dict - """ - self.log.trace("Polling GPS TPV results from GPSD") - with GPSDIface() as gps_iface: - response_mode = 0 - # Read responses from GPSD until we get a non-trivial mode - while response_mode <= 0: - gps_info = gps_iface.get_gps_info(resp_class='tpv', timeout=15) - self.log.trace("GPS info: {}".format(gps_info)) - response_mode = gps_info.get("mode", 0) - # Return the JSON'd results - gps_tpv = json.dumps(gps_info) - return { - 'name': 'gps_tpv', - 'type': 'STRING', - 'unit': '', - 'value': gps_tpv, - } - - def get_gps_sky_sensor(self): - """ - Get a SKY response from GPSd as a sensor dict - """ - self.log.trace("Polling GPS SKY results from GPSD") - with GPSDIface() as gps_iface: - # Just get the first SKY result - gps_info = gps_iface.get_gps_info(resp_class='sky', timeout=15) - # Return the JSON'd results - gps_sky = json.dumps(gps_info) - return { - 'name': 'gps_sky', - 'type': 'STRING', - 'unit': '', - 'value': gps_sky, - } - - ########################################################################### - # 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. - """ - try: - dboard = self.dboards[dboard_idx] - except KeyError: - error_msg = "Attempted to access invalid dboard index `{}' " \ - "in get_db_eeprom()!".format(dboard_idx) - self.log.error(error_msg) - raise RuntimeError(error_msg) - db_eeprom_data = copy.copy(dboard.device_info) - if hasattr(dboard, 'get_user_eeprom_data') and \ - callable(dboard.get_user_eeprom_data): - for blob_id, blob in iteritems(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 - eeprom_data -- Dictionary of EEPROM data to be written. It's up to the - specific device implementation on how to handle it. - """ - try: - dboard = self.dboards[dboard_idx] - except KeyError: - error_msg = "Attempted to access invalid dboard index `{}' " \ - "in set_db_eeprom()!".format(dboard_idx) - self.log.error(error_msg) - raise RuntimeError(error_msg) - if not hasattr(dboard, 'set_user_eeprom_data') or \ - not callable(dboard.set_user_eeprom_data): - error_msg = "Dboard has no set_user_eeprom_data() method!" - self.log.error(error_msg) - raise RuntimeError(error_msg) - safe_db_eeprom_user_data = {} - for blob_id, blob in iteritems(eeprom_data): - if blob_id in 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') - dboard.set_user_eeprom_data(safe_db_eeprom_user_data) - - ########################################################################### - # Component updating - ########################################################################### - @no_rpc - def update_fpga(self, filepath, metadata): - """ - Update the FPGA image in the filesystem and reload the overlay - :param filepath: path to new FPGA image - :param metadata: Dictionary of strings containing metadata - """ - self.log.trace("Updating FPGA with image at {} (metadata: `{}')" - .format(filepath, str(metadata))) - _, file_extension = os.path.splitext(filepath) - # Cut off the period from the file extension - file_extension = file_extension[1:].lower() - binfile_path = self.updateable_components['fpga']['path'].format( - self.mboard_info['product']) - if file_extension == "bit": - self.log.trace("Converting bit to bin file and writing to {}" - .format(binfile_path)) - from usrp_mpm.fpga_bit_to_bin import fpga_bit_to_bin - fpga_bit_to_bin(filepath, binfile_path, flip=True) - elif file_extension == "bin": - self.log.trace("Copying bin file to {}" - .format(binfile_path)) - shutil.copy(filepath, binfile_path) - else: - self.log.error("Invalid FPGA bitfile: {}" - .format(filepath)) - raise RuntimeError("Invalid N310 FPGA bitfile") - # RPC server will reload the periph manager after this. - return True - - @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 - - @no_rpc - def update_dts(self, filepath, metadata): - """ - Update the DTS image in the filesystem - :param filepath: path to new DTS image - :param metadata: Dictionary of strings containing metadata - """ - dtsfile_path = self.updateable_components['dts']['path'].format( - self.mboard_info['product']) - self.log.trace("Updating DTS with image at %s to %s (metadata: %s)", - filepath, dtsfile_path, str(metadata)) - shutil.copy(filepath, dtsfile_path) - dtbofile_path = self.updateable_components['dts']['output'].format( - self.mboard_info['product']) - self.log.trace("Compiling to %s...", dtbofile_path) - dtc_command = [ - 'dtc', - '--symbols', - '-O', 'dtb', - '-q', # Suppress warnings - '-o', - dtbofile_path, - dtsfile_path, - ] - self.log.trace("Executing command: `$ %s'", " ".join(dtc_command)) - try: - out = subprocess.check_output(dtc_command) - if out.strip() != "": - self.log.debug("`dtc' command output: \n%s", out) - except OSError as ex: - self.log.error("Could not execute `dtc' command. Binary probably "\ - "not installed. Please compile DTS by hand.") - # No fatal error here, in order not to break the current workflow - except subprocess.CalledProcessError as ex: - self.log.error("Error executing `dtc': %s", str(ex)) - return False - return True - - ####################################################################### - # Claimer API - ####################################################################### - def claim(self): - """ - This is called when the device is claimed, in case the device needs to - run any actions on claiming (e.g., light up an LED). - """ - if self._bp_leds is not None: - # Light up LINK - self._bp_leds.set(self._bp_leds.LED_LINK, 1) - - def unclaim(self): - """ - This is called when the device is unclaimed, in case the device needs - to run any actions on claiming (e.g., turn off an LED). - """ - if self._bp_leds is not None: - # Turn off LINK - self._bp_leds.set(self._bp_leds.LED_LINK, 0) - diff --git a/mpm/python/usrp_mpm/periph_manager/n3xx.py b/mpm/python/usrp_mpm/periph_manager/n3xx.py new file mode 100644 index 000000000..455206d84 --- /dev/null +++ b/mpm/python/usrp_mpm/periph_manager/n3xx.py @@ -0,0 +1,1304 @@ +# +# Copyright 2017 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +N3xx implementation module +""" + +from __future__ import print_function +import os +import copy +import shutil +import subprocess +import json +import datetime +import threading +from six import iteritems, itervalues +from builtins import object +from usrp_mpm.gpsd_iface import GPSDIface +from usrp_mpm.periph_manager import PeriphManagerBase +from usrp_mpm.mpmtypes import SID +from usrp_mpm.mpmutils import assert_compat_number, str2bool +from usrp_mpm.rpc_server import no_rpc +from usrp_mpm.sys_utils import dtoverlay +from usrp_mpm.sys_utils.sysfs_gpio import SysFSGPIO, GPIOBank +from usrp_mpm.sys_utils.uio import UIO +from usrp_mpm.sys_utils.sysfs_thermal import read_thermal_sensor_value +from usrp_mpm.xports import XportMgrUDP, XportMgrLiberio + +N3XX_DEFAULT_EXT_CLOCK_FREQ = 10e6 +N3XX_DEFAULT_CLOCK_SOURCE = 'internal' +N3XX_DEFAULT_TIME_SOURCE = 'internal' +N3XX_DEFAULT_ENABLE_GPS = True +N3XX_DEFAULT_ENABLE_FPGPIO = True +N3XX_FPGA_COMPAT = (5, 2) +N3XX_MONITOR_THREAD_INTERVAL = 1.0 # seconds +N3XX_SFP_TYPES = {0:"", 1:"1G", 2:"10G", 3:"A"} + +############################################################################### +# Additional peripheral controllers specific to Magnesium +############################################################################### +class TCA6424(object): + """ + Abstraction layer for the port/gpio expander + pins_list is an array of different version of TCA6424 pins map. + First element of this array corresponding to revC, second is revD etc... + """ + pins_list = [ + ( + 'PWREN-CLK-MGT156MHz', + 'NETCLK-CE', #revC name: 'PWREN-CLK-WB-CDCM', + 'NETCLK-RESETn', #revC name: 'WB-CDCM-RESETn', + 'NETCLK-PR0', #revC name: 'WB-CDCM-PR0', + 'NETCLK-PR1', #revC name: 'WB-CDCM-PR1', + 'NETCLK-OD0', #revC name: 'WB-CDCM-OD0', + 'NETCLK-OD1', #revC name: 'WB-CDCM-OD1', + 'NETCLK-OD2', #revC name: 'WB-CDCM-OD2', + 'PWREN-CLK-MAINREF', + 'CLK-MAINSEL-25MHz', #revC name: 'CLK-MAINREF-SEL1', + 'CLK-MAINSEL-EX_B', #revC name: 'CLK-MAINREF-SEL0', + '12', + 'CLK-MAINSEL-GPS', #revC name: '13', + 'FPGA-GPIO-EN', + 'PWREN-CLK-WB-20MHz', + 'PWREN-CLK-WB-25MHz', + 'GPS-PHASELOCK', + 'GPS-nINITSURV', + 'GPS-nRESET', + 'GPS-WARMUP', + 'GPS-SURVEY', + 'GPS-LOCKOK', + 'GPS-ALARM', + 'PWREN-GPS', + ), + ( + 'NETCLK-PR1', + 'NETCLK-PR0', + 'NETCLK-CE', + 'NETCLK-RESETn', + 'NETCLK-OD2', + 'NETCLK-OD1', + 'NETCLK-OD0', + 'PWREN-CLK-MGT156MHz', + 'PWREN-CLK-MAINREF', + 'CLK-MAINSEL-25MHz', + 'CLK-MAINSEL-EX_B', + '12', + 'CLK-MAINSEL-GPS', + 'FPGA-GPIO-EN', + 'PWREN-CLK-WB-20MHz', + 'PWREN-CLK-WB-25MHz', + 'GPS-PHASELOCK', + 'GPS-nINITSURV', + 'GPS-nRESET', + 'GPS-WARMUP', + 'GPS-SURVEY', + 'GPS-LOCKOK', + 'GPS-ALARM', + 'PWREN-GPS', + )] + + def __init__(self, rev): + # Default state: Turn on GPS power, take GPS out of reset or + # init-survey, turn on 156.25 MHz clock + # min Support from revC or rev = 2 + if rev == 2: + self.pins = self.pins_list[0] + else: + self.pins = self.pins_list[1] + + default_val = 0x860101 if rev == 2 else 0x860780 + self._gpios = SysFSGPIO('tca6424', 0xFFF7FF, 0x86F7FF, default_val) + + def set(self, name, value=None): + """ + Assert a pin by name + """ + assert name in self.pins + self._gpios.set(self.pins.index(name), value=value) + + def reset(self, name): + """ + Deassert a pin by name + """ + self.set(name, value=0) + + def get(self, name): + """ + Read back a pin by name + """ + assert name in self.pins + return self._gpios.get(self.pins.index(name)) + + +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, + 0xFFF, # use_mask + ddr + ) + +class BackpanelGPIO(GPIOBank): + """ + Abstraction layer for the back panel GPIO + """ + EMIO_BASE = 54 + BP_GPIO_OFFSET = 45 + LED_LINK = 0 + LED_REF = 1 + LED_GPS = 2 + + def __init__(self): + GPIOBank.__init__( + self, + 'zynq_gpio', + self.BP_GPIO_OFFSET + self.EMIO_BASE, + 0x7, # use_mask + 0x7, # ddr + ) + +class MboardRegsControl(object): + """ + Control the FPGA Motherboard registers + """ + # Motherboard registers + M_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_SFP0_INFO = 0x0028 + MB_SFP1_INFO = 0x002C + MB_GPIO_MASTER = 0x0030 + MB_GPIO_RADIO_SRC = 0x0034 + + # Bitfield locations for the MB_CLOCK_CTRL register. + MB_CLOCK_CTRL_PPS_SEL_INT_10 = 0 # pps_sel is one-hot encoded! + MB_CLOCK_CTRL_PPS_SEL_INT_25 = 1 + MB_CLOCK_CTRL_PPS_SEL_EXT = 2 + MB_CLOCK_CTRL_PPS_SEL_GPSDO = 3 + MB_CLOCK_CTRL_PPS_OUT_EN = 4 # output enabled = 1 + MB_CLOCK_CTRL_MEAS_CLK_RESET = 12 # set to 1 to reset mmcm, default is 0 + MB_CLOCK_CTRL_MEAS_CLK_LOCKED = 13 # locked indication for meas_clk mmcm + + 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.M_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 12 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 12 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 12 pins GPIO + 00: means the pin is driven by radio 0 + 01: means the pin is driven by radio 1 + 10: means the pin is driven by radio 2 + 11: means the pin is driven by radio 3 + """ + 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 12 pins GPIO. + 00: means the pin is driven by radio 0 + 01: means the pin is driven by radio 1 + 10: means the pin is driven by radio 2 + 11: means the pin is driven by radio 3 + """ + 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': + assert ref_clk_freq in (10e6, 25e6) + if ref_clk_freq == 10e6: + self.log.trace("Setting time source to internal " + "(10 MHz reference)...") + pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_INT_10 + elif ref_clk_freq == 25e6: + self.log.trace("Setting time source to internal " + "(25 MHz reference)...") + pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_INT_25 + elif time_source == 'external': + self.log.trace("Setting time source to external...") + pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_EXT + elif time_source == 'gpsdo': + self.log.trace("Setting time source to gpsdo...") + pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_GPSDO + else: + assert False + with self.regs.open(): + reg_val = self.peek32(self.MB_CLOCK_CTRL) & 0xFFFFFFF0 + reg_val = reg_val | (pps_sel_val & 0xF) + self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val)) + self.poke32(self.MB_CLOCK_CTRL, reg_val) + + def enable_pps_out(self, enable): + """ + Enables the PPS/Trig output on the back panel + """ + self.log.trace("%s PPS/Trig output!", + "Enabling" if enable else "Disabling") + mask = 0xFFFFFFFF ^ (0b1 << self.MB_CLOCK_CTRL_PPS_OUT_EN) + with self.regs.open(): + # mask the bit to clear it: + reg_val = self.peek32(self.MB_CLOCK_CTRL) & mask + if enable: + # set the bit if desired: + reg_val = reg_val | (0b1 << self.MB_CLOCK_CTRL_PPS_OUT_EN) + self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val)) + self.poke32(self.MB_CLOCK_CTRL, reg_val) + + def reset_meas_clk_mmcm(self, reset=True): + """ + Reset or unreset the MMCM for the measurement clock in the FPGA TDC. + """ + self.log.trace("%s measurement clock MMCM reset...", + "Asserting" if reset else "Clearing") + mask = 0xFFFFFFFF ^ (0b1 << self.MB_CLOCK_CTRL_MEAS_CLK_RESET) + with self.regs.open(): + # mask the bit to clear it + reg_val = self.peek32(self.MB_CLOCK_CTRL) & mask + if reset: + # set the bit if desired + reg_val = reg_val | (0b1 << self.MB_CLOCK_CTRL_MEAS_CLK_RESET) + self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val)) + self.poke32(self.MB_CLOCK_CTRL, reg_val) + + def get_meas_clock_mmcm_lock(self): + """ + Check the status of the MMCM for the measurement clock in the FPGA TDC. + """ + mask = 0b1 << self.MB_CLOCK_CTRL_MEAS_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("Measurement clock MMCM reporting unlocked. " + "MB_CLOCK_CTRL reg: 0x{:08X}".format(reg_val)) + else: + self.log.trace("Measurement clock MMCM locked!") + return locked + + def get_fpga_type(self): + """ + Reads the type of the FPGA image currently loaded + Returns a string with the type (ie HG, XG, AA, etc.) + """ + with self.regs.open(): + sfp0_info_rb = self.peek32(self.MB_SFP0_INFO) + sfp1_info_rb = self.peek32(self.MB_SFP1_INFO) + # Print the registers values as 32-bit hex values + self.log.trace("SFP0 Info: 0x{0:0{1}X}".format(sfp0_info_rb, 8)) + self.log.trace("SFP1 Info: 0x{0:0{1}X}".format(sfp1_info_rb, 8)) + + sfp0_type = N3XX_SFP_TYPES.get((sfp0_info_rb & 0x0000FF00) >> 8, "") + sfp1_type = N3XX_SFP_TYPES.get((sfp1_info_rb & 0x0000FF00) >> 8, "") + self.log.trace("SFP types: ({}, {})".format(sfp0_type, sfp1_type)) + if (sfp0_type == "") or (sfp1_type == ""): + return "" + elif (sfp0_type == "1G") and (sfp1_type == "10G"): + return "HG" + elif (sfp0_type == "10G") and (sfp1_type == "10G"): + return "XG" + elif (sfp0_type == "10G") and (sfp1_type == "A"): + return "XA" + elif (sfp0_type == "A") and (sfp1_type == "A"): + return "AA" + else: + self.log.warning("Unrecognized SFP type combination: ({}, {})".format( + sfp0_type, sfp1_type + )) + return "" + + +############################################################################### +# Transport managers +############################################################################### +class N3xxXportMgrUDP(XportMgrUDP): + " N3xx-specific UDP configuration " + xbar_dev = "/dev/crossbar0" + iface_config = { + 'sfp0': { + 'label': 'misc-enet-regs0', + 'xbar': 0, + 'xbar_port': 0, + 'ctrl_src_addr': 0, + }, + 'sfp1': { + 'label': 'misc-enet-regs1', + 'xbar': 0, + 'xbar_port': 1, + 'ctrl_src_addr': 1, + }, + 'eth1': { + 'label': 'misc-enet-regs0', + 'xbar': 0, + 'xbar_port': 0, + 'ctrl_src_addr': 0, + }, + 'eth2': { + 'label': 'misc-enet-regs1', + 'xbar': 0, + 'xbar_port': 1, + 'ctrl_src_addr': 1, + }, + } + +class N3xxXportMgrLiberio(XportMgrLiberio): + " N3xx-specific Liberio configuration " + max_chan = 10 + xbar_dev = "/dev/crossbar0" + xbar_port = 2 + +############################################################################### +# Main Class +############################################################################### +class n3xx(PeriphManagerBase): + """ + Holds N3xx specific attributes and methods + """ + ######################################################################### + # Overridables + # + # See PeriphManagerBase for documentation on these fields + ######################################################################### + description = "N300-Series Device" + pids = {0x4242: 'n310', 0x4240: 'n300'} + mboard_eeprom_addr = "e0005000.i2c" + mboard_eeprom_max_len = 256 + mboard_info = {"type": "n3xx", + "product": "unknown", + } + mboard_max_rev = 4 # 4 == RevE + mboard_sensor_callback_map = { + 'ref_locked': 'get_ref_lock_sensor', + 'gps_locked': 'get_gps_lock_sensor', + 'gps_time': 'get_gps_time_sensor', + 'gps_tpv': 'get_gps_tpv_sensor', + 'gps_sky': 'get_gps_sky_sensor', + 'temp': 'get_temp_sensor', + 'fan': 'get_fan_sensor', + } + dboard_eeprom_addr = "e0004000.i2c" + dboard_eeprom_max_len = 64 + + # 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"] + # N3xx-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(eeprom_md, device_args): + """ + 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 + """ + # In the N3xx case, we name the dtbo file the same as the product. + # N310 -> n310.dtbo, N300 -> n300.dtbo and so on. + return [n3xx.pids[eeprom_md['pid']]] + + ########################################################################### + # Ctor and device initialization tasks + ########################################################################### + def __init__(self, args): + self._tear_down = False + self._status_monitor_thread = None + self._ext_clock_freq = None + self._clock_source = None + self._time_source = None + self._available_endpoints = list(range(256)) + self._bp_leds = None + super(n3xx, self).__init__(args) + if not self._device_initialized: + # Don't try and figure out what's going on. Just give up. + return + 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 _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( + N3XX_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', N3XX_DEFAULT_EXT_CLOCK_FREQ) + ) + if len(self.dboards) == 0: + self.log.warning( + "No dboards found, skipping setting clock and time source " \ + "configuration." + ) + self._clock_source = N3XX_DEFAULT_CLOCK_SOURCE + self._time_source = N3XX_DEFAULT_TIME_SOURCE + else: + self.set_clock_source( + default_args.get('clock_source', N3XX_DEFAULT_CLOCK_SOURCE) + ) + self.set_time_source( + default_args.get('time_source', N3XX_DEFAULT_TIME_SOURCE) + ) + self.enable_pps_out( + default_args.get('pps_export', True) + ) + + def _init_meas_clock(self): + """ + Initialize the TDC measurement clock. After this function returns, the + FPGA TDC meas_clock is valid. + """ + # No need to toggle reset here, simply confirm it is out of reset. + self.mboard_regs_control.reset_meas_clk_mmcm(False) + if not self.mboard_regs_control.get_meas_clock_mmcm_lock(): + raise RuntimeError("Measurement clock failed to init") + + 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 (update back-panel GPS LED) + - REF lock (update back-panel REF LED) + """ + self.log.trace("Launching monitor thread...") + cond = threading.Condition() + cond.acquire() + while not self._tear_down: + gps_locked = bool(self._gpios.get("GPS-LOCKOK")) + self.log.trace("Setting GPS LED to {}".format(gps_locked)) + self._bp_leds.set(self._bp_leds.LED_GPS, int(gps_locked)) + ref_locked = self.get_ref_lock_sensor()['value'] == 'true' + self.log.trace("Setting REF LED to {}".format(ref_locked)) + self._bp_leds.set(self._bp_leds.LED_REF, int(ref_locked)) + # Now wait + if cond.wait_for( + lambda: self._tear_down, + N3XX_MONITOR_THREAD_INTERVAL): + break + cond.release() + self.log.trace("Terminating monitor thread.") + + def _init_peripherals(self, args): + """ + Turn on all peripherals. This may throw an error on failure, so make + sure to catch it. + + Periphals 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 peripherals + self.log.trace("Initializing TCA6424 port expander controls...") + self._gpios = TCA6424(int(self.mboard_info['rev'])) + self.log.trace("Initializing back panel led controls...") + self._bp_leds = BackpanelGPIO() + self.log.trace("Enabling power of MGT156MHZ clk") + self._gpios.set("PWREN-CLK-MGT156MHz") + self.enable_1g_ref_clock() + self.enable_gps( + enable=str2bool( + args.get('enable_gps', N3XX_DEFAULT_ENABLE_GPS) + ) + ) + self.enable_fp_gpio( + enable=str2bool( + args.get( + 'enable_fp_gpio', + N3XX_DEFAULT_ENABLE_FPGPIO + ) + ) + ) + # 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 clocking + self.enable_ref_clock(enable=True) + self._ext_clock_freq = None + self._init_ref_clock_and_time(args) + self._init_meas_clock() + # Init CHDR transports + self._xport_mgrs = { + 'udp': N3xxXportMgrUDP(self.log.getChild('UDP')), + 'liberio': N3xxXportMgrLiberio(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="N3xxStatusMonitorThread", + 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 "clock_source" in args: + self.set_clock_source(args.get("clock_source")) + if "clock_source" in args or "time_source" in args: + self.set_time_source(args.get("time_source", self.get_time_source())) + result = super(n3xx, 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(n3xx, 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 N3xx, this means the overlay. + """ + self.log.trace("Tearing down N3xx device...") + self._tear_down = True + if self._device_initialized: + self._status_monitor_thread.join(3 * N3XX_MONITOR_THREAD_INTERVAL) + if self._status_monitor_thread.is_alive(): + self.log.error("Could not terminate monitor thread!") + active_overlays = self.list_active_overlays() + self.log.trace("N3xx 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 len(self._available_endpoints) == 0: + 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.debug( + "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. + """ + device_info = self._xport_mgrs['udp'].get_xport_info() \ + if self._device_initialized else {} + device_info.update({ + 'fpga_version': "{}.{}".format( + *self.mboard_regs_control.get_compat_number()) + }) + 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.trace("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 + if clock_source == 'internal': + self._gpios.set("CLK-MAINSEL-EX_B") + self._gpios.set("CLK-MAINSEL-25MHz") + self._gpios.reset("CLK-MAINSEL-GPS") + elif clock_source == 'gpsdo': + self._gpios.set("CLK-MAINSEL-EX_B") + self._gpios.reset("CLK-MAINSEL-25MHz") + self._gpios.set("CLK-MAINSEL-GPS") + else: # external + self._gpios.reset("CLK-MAINSEL-EX_B") + self._gpios.reset("CLK-MAINSEL-GPS") + # SKY13350 needs to be in known state + self._gpios.set("CLK-MAINSEL-25MHz") + self._clock_source = clock_source + ref_clk_freq = self.get_ref_clock_freq() + self.log.info("Reference clock frequency is: {} MHz".format( + ref_clk_freq/1e6 + )) + 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, ref_clk_freq/1e6 + ) + 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. + """ + assert freq in (10e6, 20e6, 25e6) + self.log.debug("We've been told the external reference clock " \ + "frequency is {} MHz.".format(freq/1e6)) + if self._ext_clk_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" + return { + 'internal': 25e6, + 'external': self._ext_clock_freq, + 'gpsdo': 20e6, + }[self._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() + self._time_source = time_source + self.mboard_regs_control.set_time_source( + time_source, self.get_ref_clock_freq()) + + 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 12 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 12 pins GPIO + 00: means the pin is driven by radio 0 + 01: means the pin is driven by radio 1 + 10: means the pin is driven by radio 2 + 11: means the pin is driven by radio 3 + """ + 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 12 pins GPIO. + 00: means the pin is driven by radio 0 + 01: means the pin is driven by radio 1 + 10: means the pin is driven by radio 2 + 11: means the pin is driven by radio 3 + """ + return self.mboard_regs_control.get_fp_gpio_radio_src() + ########################################################################### + # Hardware periphal controls + ########################################################################### + def enable_pps_out(self, enable): + " Export a PPS/Trigger to the back panel " + self.mboard_regs_control.enable_pps_out(enable) + + def enable_gps(self, enable): + """ + Turn power to the GPS off or on. + """ + self.log.trace("{} power to GPS".format( + "Enabling" if enable else "Disabling" + )) + self._gpios.set("PWREN-GPS", int(bool(enable))) + + def enable_fp_gpio(self, enable): + """ + Turn power to the front panel GPIO off or on. + """ + self.log.trace("{} power to front-panel GPIO".format( + "Enabling" if enable else "Disabling" + )) + self._gpios.set("FPGA-GPIO-EN", int(bool(enable))) + + def enable_ref_clock(self, enable): + """ + Enables the ref clock voltage (+3.3-MAINREF). Without setting this to + True, *no* ref clock works. + """ + self.log.trace("{} power to reference clocks".format( + "Enabling" if enable else "Disabling" + )) + self._gpios.set("PWREN-CLK-MAINREF", int(bool(enable))) + + def enable_1g_ref_clock(self): + """ + Enables 125 MHz refclock for 1G interface. + """ + self.log.trace("Enable 125 MHz Clock for 1G SFP interface.") + self._gpios.set("NETCLK-CE") + self._gpios.set("NETCLK-RESETn", 0) + self._gpios.set("NETCLK-PR0", 1) + self._gpios.set("NETCLK-PR1", 1) + self._gpios.set("NETCLK-OD0", 1) + self._gpios.set("NETCLK-OD1", 1) + self._gpios.set("NETCLK-OD2", 0) + self._gpios.set("PWREN-CLK-WB-25MHz", 1) + self.log.trace("Finished configuring NETCLK CDCM.") + self._gpios.set("NETCLK-RESETn", 1) + + ########################################################################### + # Sensors + ########################################################################### + def get_ref_lock_sensor(self): + """ + The N3xx has no ref lock sensor, but because the ref lock is + historically considered a motherboard-level sensor, we will return the + combined lock status of all daughterboards. If no dboard is connected, + or none has a ref lock sensor, we simply return True. + """ + self.log.trace( + "Querying ref lock status from %d dboards.", + len(self.dboards) + ) + lock_status = all([ + not hasattr(db, 'get_ref_lock') or db.get_ref_lock() + for db in self.dboards + ]) + return { + 'name': 'ref_locked', + 'type': 'BOOLEAN', + 'unit': 'locked' if lock_status else 'unlocked', + 'value': str(lock_status).lower(), + } + + def get_temp_sensor(self): + """ + Get temperature sensor reading of the N3xx. + """ + self.log.trace("Reading fpga temperature.") + return_val = '-1' + try: + raw_val = read_thermal_sensor_value('fpga-thermal-zone', 'temp') + return_val = str(raw_val/1000) + except ValueError: + self.log.warning("Error when converting temperature value") + except KeyError: + self.log.warning("Can't read temp on fpga-thermal-zone") + return { + 'name': 'temperature', + 'type': 'REALNUM', + 'unit': 'C', + 'value': return_val + } + + def get_fan_sensor(self): + """ + Get cooling device reading of N3xx. In this case the speed of fan 0. + """ + self.log.trace("Reading fpga cooling device.") + return_val = '-1' + try: + raw_val = read_thermal_sensor_value('ec-fan0', 'cur_state') + return_val = str(raw_val) + except ValueError: + self.log.warning("Error when converting fan speed value") + except KeyError: + self.log.warning("Can't read cur_state on ec-fan0") + return { + 'name': 'cooling fan', + 'type': 'INTEGER', + 'unit': 'rpm', + 'value': return_val + } + + 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") + 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(), + } + + def get_gps_time_sensor(self): + """ + Calculates GPS time using a TPV response from GPSd, and returns as a sensor dict + + This time is not high accuracy. + """ + self.log.trace("Polling GPS time results from GPSD") + with GPSDIface() as gps_iface: + response_mode = 0 + # Read responses from GPSD until we get a non-trivial mode + while response_mode <= 0: + gps_info = gps_iface.get_gps_info(resp_class='tpv', timeout=15) + self.log.trace("GPS info: {}".format(gps_info)) + response_mode = gps_info.get("mode", 0) + time_str = gps_info.get("time", "") + self.log.trace("GPS time string: {}".format(time_str)) + time_dt = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S.%fZ") + self.log.trace("GPS datetime: {}".format(time_dt)) + epoch_dt = datetime.datetime(1970, 1, 1) + gps_time = int((time_dt - epoch_dt).total_seconds()) + return { + 'name': 'gps_time', + 'type': 'INTEGER', + 'unit': 'seconds', + 'value': str(gps_time), + } + + def get_gps_tpv_sensor(self): + """ + Get a TPV response from GPSd as a sensor dict + """ + self.log.trace("Polling GPS TPV results from GPSD") + with GPSDIface() as gps_iface: + response_mode = 0 + # Read responses from GPSD until we get a non-trivial mode + while response_mode <= 0: + gps_info = gps_iface.get_gps_info(resp_class='tpv', timeout=15) + self.log.trace("GPS info: {}".format(gps_info)) + response_mode = gps_info.get("mode", 0) + # Return the JSON'd results + gps_tpv = json.dumps(gps_info) + return { + 'name': 'gps_tpv', + 'type': 'STRING', + 'unit': '', + 'value': gps_tpv, + } + + def get_gps_sky_sensor(self): + """ + Get a SKY response from GPSd as a sensor dict + """ + self.log.trace("Polling GPS SKY results from GPSD") + with GPSDIface() as gps_iface: + # Just get the first SKY result + gps_info = gps_iface.get_gps_info(resp_class='sky', timeout=15) + # Return the JSON'd results + gps_sky = json.dumps(gps_info) + return { + 'name': 'gps_sky', + 'type': 'STRING', + 'unit': '', + 'value': gps_sky, + } + + ########################################################################### + # 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. + """ + try: + dboard = self.dboards[dboard_idx] + except KeyError: + error_msg = "Attempted to access invalid dboard index `{}' " \ + "in get_db_eeprom()!".format(dboard_idx) + self.log.error(error_msg) + raise RuntimeError(error_msg) + db_eeprom_data = copy.copy(dboard.device_info) + if hasattr(dboard, 'get_user_eeprom_data') and \ + callable(dboard.get_user_eeprom_data): + for blob_id, blob in iteritems(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 + eeprom_data -- Dictionary of EEPROM data to be written. It's up to the + specific device implementation on how to handle it. + """ + try: + dboard = self.dboards[dboard_idx] + except KeyError: + error_msg = "Attempted to access invalid dboard index `{}' " \ + "in set_db_eeprom()!".format(dboard_idx) + self.log.error(error_msg) + raise RuntimeError(error_msg) + if not hasattr(dboard, 'set_user_eeprom_data') or \ + not callable(dboard.set_user_eeprom_data): + error_msg = "Dboard has no set_user_eeprom_data() method!" + self.log.error(error_msg) + raise RuntimeError(error_msg) + safe_db_eeprom_user_data = {} + for blob_id, blob in iteritems(eeprom_data): + if blob_id in 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') + dboard.set_user_eeprom_data(safe_db_eeprom_user_data) + + ########################################################################### + # Component updating + ########################################################################### + @no_rpc + def update_fpga(self, filepath, metadata): + """ + Update the FPGA image in the filesystem and reload the overlay + :param filepath: path to new FPGA image + :param metadata: Dictionary of strings containing metadata + """ + self.log.trace("Updating FPGA with image at {} (metadata: `{}')" + .format(filepath, str(metadata))) + _, file_extension = os.path.splitext(filepath) + # Cut off the period from the file extension + file_extension = file_extension[1:].lower() + binfile_path = self.updateable_components['fpga']['path'].format( + self.mboard_info['product']) + if file_extension == "bit": + self.log.trace("Converting bit to bin file and writing to {}" + .format(binfile_path)) + from usrp_mpm.fpga_bit_to_bin import fpga_bit_to_bin + fpga_bit_to_bin(filepath, binfile_path, flip=True) + elif file_extension == "bin": + self.log.trace("Copying bin file to {}" + .format(binfile_path)) + shutil.copy(filepath, binfile_path) + else: + self.log.error("Invalid FPGA bitfile: {}" + .format(filepath)) + raise RuntimeError("Invalid N3xx FPGA bitfile") + # RPC server will reload the periph manager after this. + return True + + @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 + + @no_rpc + def update_dts(self, filepath, metadata): + """ + Update the DTS image in the filesystem + :param filepath: path to new DTS image + :param metadata: Dictionary of strings containing metadata + """ + dtsfile_path = self.updateable_components['dts']['path'].format( + self.mboard_info['product']) + self.log.trace("Updating DTS with image at %s to %s (metadata: %s)", + filepath, dtsfile_path, str(metadata)) + shutil.copy(filepath, dtsfile_path) + dtbofile_path = self.updateable_components['dts']['output'].format( + self.mboard_info['product']) + self.log.trace("Compiling to %s...", dtbofile_path) + dtc_command = [ + 'dtc', + '--symbols', + '-O', 'dtb', + '-q', # Suppress warnings + '-o', + dtbofile_path, + dtsfile_path, + ] + self.log.trace("Executing command: `$ %s'", " ".join(dtc_command)) + try: + out = subprocess.check_output(dtc_command) + if out.strip() != "": + self.log.debug("`dtc' command output: \n%s", out) + except OSError as ex: + self.log.error("Could not execute `dtc' command. Binary probably "\ + "not installed. Please compile DTS by hand.") + # No fatal error here, in order not to break the current workflow + except subprocess.CalledProcessError as ex: + self.log.error("Error executing `dtc': %s", str(ex)) + return False + return True + + ####################################################################### + # Claimer API + ####################################################################### + def claim(self): + """ + This is called when the device is claimed, in case the device needs to + run any actions on claiming (e.g., light up an LED). + """ + if self._bp_leds is not None: + # Light up LINK + self._bp_leds.set(self._bp_leds.LED_LINK, 1) + + def unclaim(self): + """ + This is called when the device is unclaimed, in case the device needs + to run any actions on claiming (e.g., turn off an LED). + """ + if self._bp_leds is not None: + # Turn off LINK + self._bp_leds.set(self._bp_leds.LED_LINK, 0) + diff --git a/mpm/python/usrp_mpm/rpc_server.py b/mpm/python/usrp_mpm/rpc_server.py index 3dbda4161..a983b6dcf 100644 --- a/mpm/python/usrp_mpm/rpc_server.py +++ b/mpm/python/usrp_mpm/rpc_server.py @@ -70,7 +70,7 @@ class MPMServer(RPCServer): self.session_id = None # Create the periph_manager for this device # This call will be forwarded to the device specific implementation - # e.g. in periph_manager/n310.py + # e.g. in periph_manager/n3xx.py # Which implementation is called will be determined during # configuration with cmake (-DMPM_DEVICE). # mgr is thus derived from PeriphManagerBase diff --git a/mpm/python/usrp_mpm/sys_utils/net.py b/mpm/python/usrp_mpm/sys_utils/net.py index 61b9f2c91..ccce0762e 100644 --- a/mpm/python/usrp_mpm/sys_utils/net.py +++ b/mpm/python/usrp_mpm/sys_utils/net.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # """ -N310 implementation module +Network utilities for MPM """ import itertools import socket -- cgit v1.2.3