aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xmpm/python/e320_bist561
-rwxr-xr-xmpm/python/n3xx_bist697
-rw-r--r--mpm/python/usrp_mpm/bist.py598
3 files changed, 786 insertions, 1070 deletions
diff --git a/mpm/python/e320_bist b/mpm/python/e320_bist
index ef7c85980..ad5b2163a 100755
--- a/mpm/python/e320_bist
+++ b/mpm/python/e320_bist
@@ -6,21 +6,12 @@
#
"""
E320 Built-In Self Test (BIST)
-
"""
from __future__ import print_function
-import os
import sys
-import subprocess
-import re
-import socket
-import select
import time
-import json
-from datetime import datetime
-import argparse
-from six import iteritems
+from usrp_mpm import bist
# Timeout values are in seconds:
GPS_WARMUP_TIMEOUT = 70 # Data sheet says "about a minute"
@@ -29,113 +20,30 @@ GPS_LOCKOK_TIMEOUT = 2 # Data sheet says about 15 minutes. Because our test
# reduce this value in order for the BIST to pass faster
# by default.
# Temperature Sensor Mapping based on location
-temp_sensor_map = {
+TEMP_SENSOR_MAP = {
"thermal_zone0" : "internal",
"thermal_zone1" : "rf_channelA",
"thermal_zone2" : "fpga",
"thermal_zone3" : "rf_channelB",
"thermal_zone4" : "main_power"
}
-##############################################################################
-# Aurora/SFP BIST code
-##############################################################################
-def get_sfp_bist_defaults():
- " Default dictionary for SFP/Aurora BIST dry-runs "
- return {
- 'elapsed_time': 1.0,
- 'max_roundtrip_latency': 0.8e-6,
- 'throughput': 1000e6,
- 'max_ber': 8.5e-11,
- 'errors': 0,
- 'bits': 12012486656,
- }
-
-def assert_aurora_image(master, slave):
- """
- Make sure we have an FPGA image with which we can run the requested tests.
-
- Will load an AA image if not, which always satisfies all conditions for
- running Aurora tests.
- """
- from usrp_mpm.sys_utils import uio
- if not uio.find_uio_device(master)[0] or \
- (slave is not None and not uio.find_uio_device(slave)[0]):
- load_fpga_image('AA')
-
-def run_aurora_bist(master, slave=None):
- """
- Spawn a BER test
- """
- from usrp_mpm import aurora_control
- from usrp_mpm.sys_utils.uio import open_uio
-
- class DummyContext(object):
- """Dummy class for context managers"""
- def __enter__(self):
- return
-
- def __exit__(self, exc_type, exc_value, traceback):
- return exc_type is None
-
- # Go, go, go!
- try:
- assert_aurora_image(master, slave)
- with open_uio(label=master, read_only=False) as master_au_uio:
- master_au_ctrl = aurora_control.AuroraControl(master_au_uio)
- with open_uio(label=slave, read_only=False)\
- if slave is not None else DummyContext() as slave_au_uio:
- slave_au_ctrl = aurora_control.AuroraControl(slave_au_uio)\
- if slave is not None else None
- return master_au_ctrl.run_ber_loopback_bist(
- duration=10,
- requested_rate=1300 * 8e6,
- slave=slave_au_ctrl,
- )
- except Exception as ex:
- print("Unexpected exception: {}".format(str(ex)))
- exit(1)
-
-
-def aurora_results_to_status(bist_results):
- """
- Convert a dictionary coming from AuroraControl BIST to one that we can use
- for this BIST
- """
- return bist_results['mst_errors'] == 0, {
- 'elapsed_time': bist_results['time_elapsed'],
- 'max_roundtrip_latency': bist_results['mst_latency_us'],
- 'throughput': bist_results['approx_throughput'],
- 'max_ber': bist_results['max_ber'],
- 'errors': bist_results['mst_errors'],
- 'bits': bist_results['mst_samps'],
- }
##############################################################################
-# Helpers
+# Bist class
##############################################################################
-def post_results(results):
- """
- Given a dictionary, post the results.
-
- This will print the results as JSON to stdout.
- """
- print(json.dumps(
- results,
- sort_keys=True,
- indent=4,
- separators=(',', ': ')
- ))
-
-def filter_results_for_lv(results):
+class E320BIST(bist.UsrpBIST):
"""
- The LabView JSON parser does not support a variety of things, such as
- nested dicts, and some downstream LV applications freak out if certain keys
- are not what they expect.
- This is a long hard-coded list of how results should look like for those
- cases. Note: This list needs manual supervision and attention for the case
- where either subsystems get renamed, or other architectural changes should
- occur.
+ BIST Tool for the USRP E320
"""
+ usrp_type = "E320"
+ # This defines special tests that are really collections of other tests.
+ collections = {
+ 'standard': ["ddr3", "gpsdo", "rtc", "temp", "fan", "tpm", "gyro",
+ "ref_clock_int"],
+ 'extended': "*",
+ }
+ # Default FPGA image type
+ DEFAULT_FPGA_TYPE = '1G'
lv_compat_format = {
'ddr3': {
'throughput': -1,
@@ -172,250 +80,35 @@ def filter_results_for_lv(results):
'read_patterns': [],
},
'temp': {
- temp_sensor_map['thermal_zone0']: -1,
- temp_sensor_map['thermal_zone1']: -1,
- temp_sensor_map['thermal_zone2']: -1,
- temp_sensor_map['thermal_zone3']: -1,
- temp_sensor_map['thermal_zone4']: -1,
+ TEMP_SENSOR_MAP['thermal_zone0']: -1,
+ TEMP_SENSOR_MAP['thermal_zone1']: -1,
+ TEMP_SENSOR_MAP['thermal_zone2']: -1,
+ TEMP_SENSOR_MAP['thermal_zone3']: -1,
+ TEMP_SENSOR_MAP['thermal_zone4']: -1,
},
'fan': {
'cooling_device0': -1,
},
}
- # OK now go and brush up the results:
- def fixup_dict(result_dict, ref_dict):
- """
- Touches up result_dict according to ref_dict by the following rules:
- - If a key is in result_dict that is not in ref_dict, delete that
- - If a key is in ref_dict that is not in result_dict, use the value
- from ref_dict
- """
- ref_dict['error_msg'] = ""
- ref_dict['status'] = False
- result_dict = {
- k: v for k, v in iteritems(result_dict)
- if k in ref_dict or k in ('error_msg', 'status')
- }
- result_dict = {
- k: result_dict.get(k, ref_dict[k]) for k in ref_dict
- }
- return result_dict
- results = {
- testname: fixup_dict(testresults, lv_compat_format[testname]) \
- if testname in lv_compat_format else testresults
- for testname, testresults in iteritems(results)
- }
- return results
-
-def sock_read_line(my_sock, timeout=60, interval=0.1):
- """
- Read from a socket until newline. If there was no newline until the timeout
- occurs, raise an error. Otherwise, return the line.
- """
- line = b''
- end_time = time.time() + timeout
- while time.time() < end_time:
- socket_ready = select.select([my_sock], [], [], 0)[0]
- if socket_ready:
- next_char = my_sock.recv(1)
- if next_char == b'\n':
- return line.decode('ascii')
- line += next_char
- else:
- time.sleep(interval)
- raise RuntimeError("sock_read_line() exceeded read timeout!")
-
-def poll_with_timeout(state_check, timeout_ms, interval_ms):
- """
- Calls state_check() every interval_ms until it returns a positive value, or
- until a timeout is exceeded.
-
- Returns True if state_check() returned True within the timeout.
- """
- max_time = time.time() + (float(timeout_ms) / 1000)
- interval_s = float(interval_ms) / 1000
- while time.time() < max_time:
- if state_check():
- return True
- time.sleep(interval_s)
- return False
-
-def expand_options(option_list):
- """
- Turn a list ['foo=bar', 'spam=eggs'] into a dictionary {'foo': 'bar',
- 'spam': 'eggs'}.
- """
- return dict(x.split('=') for x in option_list)
+ device_args = "type=e3xx,addr=127.0.0.1"
-##############################################################################
-# Bist class
-##############################################################################
-class E320BIST(object):
- """
- BIST Tool for the USRP E320
- """
- # This defines special tests that are really collections of other tests.
- collections = {
- 'standard': ["ddr3", "gpsdo", "rtc", "temp", "fan", "tpm", "gyro", "ref_clock_int"],
- 'extended': "*",
- }
- # Default FPGA image type
- DEFAULT_FPGA_TYPE = '1G'
+ def __init__(self):
+ super(E320BIST, self).__init__()
- @staticmethod
- def make_arg_parser():
- """
- Return arg parser
- """
- parser = argparse.ArgumentParser(
- description="E320 BIST Tool",
- )
- parser.add_argument(
- '-n', '--dry-run', action='store_true',
- help="Fake out the tests. All tests will return a valid" \
- " response, but will not actually interact with hardware.",
- )
- parser.add_argument(
- '-v', '--verbose', action='store_true',
- help="Crank up verbosity level",
- )
- parser.add_argument(
- '--debug', action='store_true',
- help="For debugging this tool.",
- )
- parser.add_argument(
- '--option', '-o', action='append', default=[],
- help="Option for individual test.",
- )
- parser.add_argument(
- '--lv-compat', action='store_true',
- help="Provides compatibility with the LV JSON parser. Don't run "
- "this mode unless you know what you're doing. The JSON "
- "output does not necessarily reflect the actual system "
- "status when using this mode.",
- )
- parser.add_argument(
- '--skip-fpga-reload', action='store_true',
- help="Skip reloading the default FPGA image post-test. Note: by"
- "specifying this argument, the FPGA image loaded could be "
- "anything post-test.",
- )
- parser.add_argument(
- 'tests',
- help="List the tests that should be run",
- nargs='+', # There has to be at least one
- )
- return parser
+ def get_mb_periph_mgr(self):
+ """Return reference to an e320 periph manager"""
+ from usrp_mpm.periph_manager.e320 import e320
+ return e320
- def __init__(self):
- self.args = E320BIST.make_arg_parser().parse_args()
- self.args.option = expand_options(self.args.option)
- # If this is true, trigger a reload of the default FPGA image
- self.reload_fpga_image = False
- try:
- from usrp_mpm.periph_manager.e320 import e320
- default_rev = e320.mboard_max_rev
- except ImportError:
- # This means we're in dry run mode or something like that, so just
- # pick something
- default_rev = 3
- self.mb_rev = int(self.args.option.get('mb_rev', default_rev))
- self.tests_to_run = set()
- for test in self.args.tests:
- if test in self.collections:
- for test in self.expand_collection(test):
- self.tests_to_run.add(test)
- else:
- self.tests_to_run.add(test)
- try:
- # Keep this import here so we can do dry-runs without any MPM code
- from usrp_mpm import get_main_logger
- if not self.args.verbose:
- from usrp_mpm.mpmlog import WARNING
- get_main_logger().setLevel(WARNING)
- self.log = get_main_logger().getChild('main')
- except ImportError:
- print("No logging capability available.")
-
- def expand_collection(self, coll):
- """
- Return names of tests in a collection
- """
- tests = self.collections[coll]
- if tests == "*":
- tests = {x.replace('bist_', '')
- for x in dir(self)
- if x.find('bist_') == 0
- }
- else:
- tests = set(tests)
- return tests
-
- def run(self):
- """
- Execute tests.
+ def get_product_id(self):
+ """Return the mboard product ID (e320):"""
+ return bist.get_product_id_from_eeprom(valid_ids=['e320'])
- Returns True on Success.
- """
- def execute_test(testname):
- """
- Actually run a test.
- """
- testmethod_name = "bist_{0}".format(testname)
- sys.stderr.write(
- "Executing test method: {0}\n\n".format(testmethod_name)
- )
- try:
- status, data = getattr(self, testmethod_name)()
- data['status'] = status
- data['error_msg'] = data.get('error_msg', '')
- return status, data
- except AttributeError:
- sys.stderr.write("Test not defined: {}\n".format(testname))
- return False, {}
- except Exception as ex:
- sys.stderr.write(
- "Test {} failed to execute: {}\n".format(testname, str(ex))
- )
- if self.args.debug:
- raise
- return False, {'error_msg': str(ex)}
- tests_successful = True
- result = {}
- for test in self.tests_to_run:
- status, result_data = execute_test(test)
- tests_successful = tests_successful and status
- result[test] = result_data
- if self.args.lv_compat:
- result = filter_results_for_lv(result)
- post_results(result)
- if self.reload_fpga_image and not self.args.skip_fpga_reload:
- load_fpga_image(self.DEFAULT_FPGA_TYPE)
- return tests_successful
#############################################################################
# BISTS
# All bist_* methods must return True/False success values!
#############################################################################
- def bist_rtc(self):
- """
- BIST for RTC (real time clock)
-
- Return dictionary:
- - date: Returns the current UTC time, with seconds-accuracy, in ISO 8601
- format, as a string. As if running 'date -Iseconds -u'.
- - time: Same time, but in seconds since epoch.
-
- Return status:
- Unless datetime throws an exception, returns True.
- """
- assert 'rtc' in self.tests_to_run
- utc_now = datetime.utcnow()
- return True, {
- 'time': time.mktime(utc_now.timetuple()),
- 'date': utc_now.replace(microsecond=0).isoformat() + "+00:00",
- }
-
def bist_gyro(self):
"""
BIST for GYRO (MPU9250)
@@ -458,25 +151,7 @@ class E320BIST(object):
assert 'ddr3' in self.tests_to_run
if self.args.dry_run:
return True, {'throughput': 1250e6}
- result = {}
- ddr3_bist_executor = 'uhd_usrp_probe --args addr=127.0.0.1'
- try:
- output = subprocess.check_output(
- ddr3_bist_executor,
- stderr=subprocess.STDOUT,
- shell=True,
- )
- except subprocess.CalledProcessError as ex:
- # Don't throw errors from uhd_usrp_probe
- output = ex.output
- output = output.decode("utf-8")
- mobj = re.search(r"Throughput: (?P<thrup>[0-9.]+)\s?MB", output)
- if mobj is not None:
- result['throughput'] = float(mobj.group('thrup')) * 1000
- else:
- result['throughput'] = 0
- result['error_msg'] = result.get('error_msg', '') + \
- "\n\nFailed match throughput regex!"
+ result = bist.test_ddr3_with_usrp_probe()
return result.get('throughput', 0) > 1000e3, result
def bist_gpsdo(self):
@@ -522,7 +197,7 @@ class E320BIST(object):
sys.stderr.write(
"Waiting for WARMUP to go low for up to {} seconds...\n".format(
gps_warmup_timeout))
- if not poll_with_timeout(
+ if not bist.poll_with_timeout(
lambda: not bool((mb_regs.get_gps_status() >> 4) & 0x1),
gps_warmup_timeout*1000, 1000
):
@@ -534,7 +209,7 @@ class E320BIST(object):
sys.stderr.write(
"Waiting for LOCKOK to go high for up to {} seconds...\n".format(
gps_lockok_timeout))
- if not poll_with_timeout(
+ if not bist.poll_with_timeout(
mb_regs.get_gps_locked_val,
gps_lockok_timeout*1000,
1000
@@ -550,24 +225,8 @@ class E320BIST(object):
sys.stderr.write("GPS-ALARM status: {}\n".format(
(gps_status >> 1) & 0x1
))
- # Now read back response from chip
- my_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- my_sock.connect(('localhost', 2947))
- sys.stderr.write("Connected to GPSDO socket.\n")
- query_cmd = b'?WATCH={"enable":true,"json":true}'
- my_sock.sendall(query_cmd)
- sys.stderr.write("Sent query: {}\n".format(query_cmd))
- sock_read_line(my_sock, timeout=10)
- sys.stderr.write("Received initial newline.\n")
- result = {}
- while result.get('class', None) != 'TPV':
- json_result = sock_read_line(my_sock, timeout=60)
- sys.stderr.write(
- "Received JSON response: {}\n\n".format(json_result)
- )
- result = json.loads(json_result)
- my_sock.sendall(b'?WATCH={"enable":false}')
- my_sock.close()
+ # Now the chip is on, read back the TPV result
+ result = bist.get_gpsd_tpv_result()
# If we reach this line, we have a valid result and the chip responded.
# However, it doesn't necessarily mean we had a GPS lock.
return True, result
@@ -589,60 +248,9 @@ class E320BIST(object):
return True, {
'tpm0_caps': "Fake caps value\n\nVersion 0.0.0",
}
- result = {}
- props_to_read = ('caps',)
- base_path = '/sys/class/tpm'
- for tpm_device in os.listdir(base_path):
- if tpm_device.startswith('tpm'):
- for key in props_to_read:
- result['{}_{}'.format(tpm_device, key)] = open(
- os.path.join(base_path, tpm_device, key), 'r'
- ).read().strip()
+ result = bist.get_tpm_caps_info()
return len(result) == 1, result
- def ref_clock_helper(self,clock_source):
- """
- Helper function to determine reference clock lock
- Description: Checks to see if we can lock to a clock source.
-
- External Equipment: None
- Return dictionary:
- - <sensor-name>:
- - locked: Boolean lock status
- """
- assert clock_source in ("internal", "external"),\
- "Invalid clock source selected ({}). Valid choices: {}".format(
- clock_source, ("internal", "external"))
- if self.args.dry_run:
- return True, {'ref_locked': True}
- result = {}
- env = os.environ.copy()
- env['UHD_LOG_CONSOLE_LEVEL'] = 'error'
- cmd = ['uhd_usrp_probe', '--args', 'addr=127.0.0.1,clock_source=' + clock_source,
- '--sensor']
- sensor_path = '/mboards/0/sensors/ref_locked'
- cmd.append(sensor_path)
- ref_lock_executor = ' '.join(cmd)
- try:
- output = subprocess.check_output(
- ref_lock_executor,
- stderr=subprocess.STDOUT,
- env=env,
- shell=True,
- )
- except subprocess.CalledProcessError as ex:
- # Don't throw errors from uhd_usrp_probe
- output = ex.output
- output = output.decode("utf-8")
- mobj = re.search(r"true$", output.strip())
- if mobj is not None:
- result['ref_locked'] = True
- else:
- result['ref_locked'] = False
- result['error_msg'] = ("Reference Clock not locked."
- " Extra output:" + output)
- return result
-
def bist_ref_clock_int(self):
"""
BIST for clock lock from internal (20MHz).
@@ -658,7 +266,9 @@ class E320BIST(object):
need to be asserted.
"""
assert 'ref_clock_int' in self.tests_to_run
- result = self.ref_clock_helper('internal')
+ if self.args.dry_run:
+ return True, {'ref_locked': True}
+ result = bist.get_ref_clock_prop('internal', 'internal')
return 'error_msg' not in result, result
def bist_ref_clock_ext(self):
@@ -676,7 +286,9 @@ class E320BIST(object):
need to be asserted.
"""
assert 'ref_clock_ext' in self.tests_to_run
- result = self.ref_clock_helper('external')
+ if self.args.dry_run:
+ return True, {'ref_locked': True}
+ result = bist.get_ref_clock_prop('external', 'external')
return 'error_msg' not in result, result
def bist_sfp_loopback(self):
@@ -696,10 +308,14 @@ class E320BIST(object):
- bits: Number of bits that were transferred
"""
if self.args.dry_run:
- return True, get_sfp_bist_defaults()
- sfp_bist_results = run_aurora_bist(master='misc-auro-regs')
+ return True, bist.get_sfp_bist_defaults()
+ sfp_bist_results = bist.run_aurora_bist(
+ device_args=self.device_args,
+ product_id=self.get_product_id(),
+ master='misc-auro-regs',
+ )
self.reload_fpga_image = True
- return aurora_results_to_status(sfp_bist_results)
+ return bist.aurora_results_to_status(sfp_bist_results)
def bist_gpio(self):
"""
@@ -737,7 +353,7 @@ class E320BIST(object):
" Run a GPIO test for a given set of patterns "
gpio_ctrl = e320_periphs.FrontpanelGPIO(ddr)
for pattern in patterns:
- gpio_set_all(gpio_ctrl, pattern, GPIO_WIDTH, ddr)
+ bist.gpio_set_all(gpio_ctrl, pattern, GPIO_WIDTH, ddr)
time.sleep(0.1)
gpio_rb = gpio_ctrl.get_all()
if pattern != gpio_rb:
@@ -763,16 +379,8 @@ class E320BIST(object):
assert 'temp' in self.tests_to_run
if self.args.dry_run:
return True, {'internal': 30000}
- import pyudev
- context = pyudev.Context()
- result = {
- temp_sensor_map[device.sys_name]: \
- int(device.attributes.get('temp').decode('ascii'))
- for device in context.list_devices(subsystem='thermal')
-
- if 'temp' in device.attributes.available_attributes \
- and device.attributes.get('temp') is not None
- }
+ result = bist.get_temp_sensor_value(
+ lambda device: TEMP_SENSOR_MAP[device.sys_name])
if len(result) < 1:
result['error_msg'] = "No temperature sensors found!"
return 'error_msg' not in result, result
@@ -790,14 +398,7 @@ class E320BIST(object):
assert 'fan' in self.tests_to_run
if self.args.dry_run:
return True, {'cooling_device0': 10000}
- import pyudev
- context = pyudev.Context()
- result = {
- device.sys_name: int(device.attributes.get('cur_state'))
- for device in context.list_devices(subsystem='thermal')
- if 'cur_state' in device.attributes.available_attributes \
- and device.attributes.get('cur_state') is not None
- }
+ result = bist.get_fan_values()
return len(result) == 1, result
def bist_link_up(self):
@@ -812,65 +413,9 @@ class E320BIST(object):
assert 'link_up' in self.tests_to_run
if self.args.dry_run:
return True, {'sfp0': 'UP'}
- from pyroute2 import IPRoute
- result = {}
- with IPRoute() as ipr:
- links = ipr.link_lookup(ifname='sfp0')
- if not links:
- return False, {'error_msg': "No interface found"}
- link_info = next(iter(ipr.get_links(links)), None)
- if link_info == None:
- return False, {'error_msg': "Error on get_links for sfp0"}
- result['sfp0'] = link_info.get_attr('IFLA_OPERSTATE')
- if result['sfp0'] != 'UP':
- result['error_msg'] = "Link not up for interface"
+ result = bist.get_link_up('sfp0')
return 'error_msg' not in result, result
-
-def gpio_set_all(gpio_bank, value, gpio_size, ddr_mask):
- """Helper function for set gpio.
- What this function do is take decimal value and convert to a binary string
- then try to set those individual bits to the gpio_bank.
- Arguments:
- 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_size = bin(ddr_mask).count("1")
- value_bitstring = ('{0:0' + str(ddr_size) + 'b}').format(value)[-(gpio_size):]
- ddr_bitstring = ('{0:0' + str(gpio_size) + 'b}').format(ddr_mask)[-(gpio_size):]
- for i in range(gpio_size):
- if ddr_bitstring[gpio_size - 1 - i] == "1":
- gpio_bank.set(i, value_bitstring[i % ddr_size])
-
-def get_product_id():
- """Return the mboard product ID (e320):"""
- cmd = ['eeprom-id']
- output = subprocess.check_output(
- cmd,
- stderr=subprocess.STDOUT,
- shell=True,
- ).decode('utf-8')
- if 'e320' in output:
- return 'e320'
- raise AssertionError("Cannot determine product ID.")
-
-def load_fpga_image(fpga_type):
- """Load an FPGA image (1G, XG, AA, ...)"""
- cmd = ['uhd_image_loader', '--args', 'type=e3xx,addr=127.0.0.1', '--fpga-path']
- images_folder = '/usr/share/uhd/images/'
- fpga_file_name = \
- 'usrp_' + get_product_id() + '_fpga_' + fpga_type.upper() + '.bit'
- fpga_image = images_folder + fpga_file_name
- cmd.append(fpga_image)
- cmd_str = ' '.join(cmd)
- subprocess.check_output(
- cmd_str,
- stderr=subprocess.STDOUT,
- shell=True
- )
-
##############################################################################
# main
##############################################################################
diff --git a/mpm/python/n3xx_bist b/mpm/python/n3xx_bist
index 18f1c3b4b..cc7dd40c6 100755
--- a/mpm/python/n3xx_bist
+++ b/mpm/python/n3xx_bist
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2017 Ettus Research, National Instruments Company
+# Copyright 2017-2018 Ettus Research, National Instruments Company
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
@@ -11,17 +11,9 @@ Will work on all derivatives of the N3xx series.
"""
from __future__ import print_function
-import os
import sys
-import subprocess
-import re
-import socket
-import select
import time
-import json
-from datetime import datetime
-import argparse
-from six import iteritems
+from usrp_mpm import bist
# Timeout values are in seconds:
GPS_WARMUP_TIMEOUT = 70 # Data sheet says "about a minute"
@@ -31,105 +23,20 @@ GPS_LOCKOK_TIMEOUT = 2 # Data sheet says about 15 minutes. Because our test
# by default.
##############################################################################
-# Aurora/SFP BIST code
+# Bist class
##############################################################################
-def get_sfp_bist_defaults():
- " Default dictionary for SFP/Aurora BIST dry-runs "
- return {
- 'elapsed_time': 1.0,
- 'max_roundtrip_latency': 0.8e-6,
- 'throughput': 1000e6,
- 'max_ber': 8.5e-11,
- 'errors': 0,
- 'bits': 12012486656,
- }
-
-def assert_aurora_image(master, slave):
- """
- Make sure we have an FPGA image with which we can run the requested tests.
-
- Will load an AA image if not, which always satisfies all conditions for
- running Aurora tests.
- """
- from usrp_mpm.sys_utils import uio
- if not uio.find_uio_device(master)[0] or \
- (slave is not None and not uio.find_uio_device(slave)[0]):
- load_fpga_image('AA')
-
-def run_aurora_bist(master, slave=None):
+class N3XXBIST(bist.UsrpBIST):
"""
- Spawn a BER test
- """
- from usrp_mpm import aurora_control
- from usrp_mpm.sys_utils.uio import open_uio
-
- class DummyContext(object):
- """Dummy class for context managers"""
- def __enter__(self):
- return
-
- def __exit__(self, exc_type, exc_value, traceback):
- return exc_type is None
-
- # Go, go, go!
- try:
- assert_aurora_image(master, slave)
- with open_uio(label=master, read_only=False) as master_au_uio:
- master_au_ctrl = aurora_control.AuroraControl(master_au_uio)
- with open_uio(label=slave, read_only=False)\
- if slave is not None else DummyContext() as slave_au_uio:
- slave_au_ctrl = aurora_control.AuroraControl(slave_au_uio)\
- if slave is not None else None
- return master_au_ctrl.run_ber_loopback_bist(
- duration=10,
- requested_rate=1300 * 8e6,
- slave=slave_au_ctrl,
- )
- except Exception as ex:
- print("Unexpected exception: {}".format(str(ex)))
- exit(1)
-
-
-def aurora_results_to_status(bist_results):
- """
- Convert a dictionary coming from AuroraControl BIST to one that we can use
- for this BIST
+ BIST Tool for the USRP N3xx series
"""
- return bist_results['mst_errors'] == 0, {
- 'elapsed_time': bist_results['time_elapsed'],
- 'max_roundtrip_latency': bist_results['mst_latency_us'],
- 'throughput': bist_results['approx_throughput'],
- 'max_ber': bist_results['max_ber'],
- 'errors': bist_results['mst_errors'],
- 'bits': bist_results['mst_samps'],
+ usrp_type = "N3XX"
+ # This defines special tests that are really collections of other tests.
+ collections = {
+ 'standard': ["ddr3", "gpsdo", "rtc", "temp", "fan", "tpm"],
+ 'extended': "*",
}
-
-##############################################################################
-# Helpers
-##############################################################################
-def post_results(results):
- """
- Given a dictionary, post the results.
-
- This will print the results as JSON to stdout.
- """
- print(json.dumps(
- results,
- sort_keys=True,
- indent=4,
- separators=(',', ': ')
- ))
-
-def filter_results_for_lv(results):
- """
- The LabView JSON parser does not support a variety of things, such as
- nested dicts, and some downstream LV applications freak out if certain keys
- are not what they expect.
- This is a long hard-coded list of how results should look like for those
- cases. Note: This list needs manual supervision and attention for the case
- where either subsystems get renamed, or other architectural changes should
- occur.
- """
+ # Default FPGA image type
+ DEFAULT_FPGA_TYPE = 'HG'
lv_compat_format = {
'ddr3': {
'throughput': -1,
@@ -184,240 +91,24 @@ def filter_results_for_lv(results):
'lock_status': 0,
},
}
- # OK now go and brush up the results:
- def fixup_dict(result_dict, ref_dict):
- """
- Touches up result_dict according to ref_dict by the following rules:
- - If a key is in result_dict that is not in ref_dict, delete that
- - If a key is in ref_dict that is not in result_dict, use the value
- from ref_dict
- """
- ref_dict['error_msg'] = ""
- ref_dict['status'] = False
- result_dict = {
- k: v for k, v in iteritems(result_dict)
- if k in ref_dict or k in ('error_msg', 'status')
- }
- result_dict = {
- k: result_dict.get(k, ref_dict[k]) for k in ref_dict
- }
- return result_dict
- results = {
- testname: fixup_dict(testresults, lv_compat_format[testname]) \
- if testname in lv_compat_format else testresults
- for testname, testresults in iteritems(results)
- }
- return results
-
-def sock_read_line(my_sock, timeout=60, interval=0.1):
- """
- Read from a socket until newline. If there was no newline until the timeout
- occurs, raise an error. Otherwise, return the line.
- """
- line = b''
- end_time = time.time() + timeout
- while time.time() < end_time:
- socket_ready = select.select([my_sock], [], [], 0)[0]
- if socket_ready:
- next_char = my_sock.recv(1)
- if next_char == b'\n':
- return line.decode('ascii')
- line += next_char
- else:
- time.sleep(interval)
- raise RuntimeError("sock_read_line() exceeded read timeout!")
-
-def poll_with_timeout(state_check, timeout_ms, interval_ms):
- """
- Calls state_check() every interval_ms until it returns a positive value, or
- until a timeout is exceeded.
-
- Returns True if state_check() returned True within the timeout.
- """
- max_time = time.time() + (float(timeout_ms) / 1000)
- interval_s = float(interval_ms) / 1000
- while time.time() < max_time:
- if state_check():
- return True
- time.sleep(interval_s)
- return False
-
-def expand_options(option_list):
- """
- Turn a list ['foo=bar', 'spam=eggs'] into a dictionary {'foo': 'bar',
- 'spam': 'eggs'}.
- """
- return dict(x.split('=') for x in option_list)
-
-##############################################################################
-# Bist class
-##############################################################################
-class N3XXBIST(object):
- """
- BIST Tool for the USRP N3xx series
- """
- # This defines special tests that are really collections of other tests.
- collections = {
- 'standard': ["ddr3", "gpsdo", "rtc", "temp", "fan", "tpm"],
- 'extended': "*",
- }
- # Default FPGA image type
- DEFAULT_FPGA_TYPE = 'HG'
-
- @staticmethod
- def make_arg_parser():
- """
- Return arg parser
- """
- parser = argparse.ArgumentParser(
- description="N3xx BIST Tool",
- )
- parser.add_argument(
- '-n', '--dry-run', action='store_true',
- help="Fake out the tests. All tests will return a valid" \
- " response, but will not actually interact with hardware.",
- )
- parser.add_argument(
- '-v', '--verbose', action='store_true',
- help="Crank up verbosity level",
- )
- parser.add_argument(
- '--debug', action='store_true',
- help="For debugging this tool.",
- )
- parser.add_argument(
- '--option', '-o', action='append', default=[],
- help="Option for individual test.",
- )
- parser.add_argument(
- '--lv-compat', action='store_true',
- help="Provides compatibility with the LV JSON parser. Don't run "
- "this mode unless you know what you're doing. The JSON "
- "output does not necessarily reflect the actual system "
- "status when using this mode.",
- )
- parser.add_argument(
- '--skip-fpga-reload', action='store_true',
- help="Skip reloading the default FPGA image post-test. Note: by"
- "specifying this argument, the FPGA image loaded could be "
- "anything post-test.",
- )
- parser.add_argument(
- 'tests',
- help="List the tests that should be run",
- nargs='+', # There has to be at least one
- )
- return parser
+ device_args = "type=n3xx,addr=127.0.0.1"
def __init__(self):
- self.args = N3XXBIST.make_arg_parser().parse_args()
- self.args.option = expand_options(self.args.option)
- # If this is true, trigger a reload of the default FPGA image
- self.reload_fpga_image = False
- try:
- 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
- default_rev = 3
- self.mb_rev = int(self.args.option.get('mb_rev', default_rev))
- self.tests_to_run = set()
- for test in self.args.tests:
- if test in self.collections:
- for test in self.expand_collection(test):
- self.tests_to_run.add(test)
- else:
- self.tests_to_run.add(test)
- try:
- # Keep this import here so we can do dry-runs without any MPM code
- from usrp_mpm import get_main_logger
- if not self.args.verbose:
- from usrp_mpm.mpmlog import WARNING
- get_main_logger().setLevel(WARNING)
- self.log = get_main_logger().getChild('main')
- except ImportError:
- print("No logging capability available.")
-
- def expand_collection(self, coll):
- """
- Return names of tests in a collection
- """
- tests = self.collections[coll]
- if tests == "*":
- tests = {x.replace('bist_', '')
- for x in dir(self)
- if x.find('bist_') == 0
- }
- else:
- tests = set(tests)
- return tests
-
- def run(self):
- """
- Execute tests.
+ bist.UsrpBIST.__init__(self)
- Returns True on Success.
- """
- def execute_test(testname):
- """
- Actually run a test.
- """
- testmethod_name = "bist_{0}".format(testname)
- sys.stderr.write(
- "Executing test method: {0}\n\n".format(testmethod_name)
- )
- try:
- status, data = getattr(self, testmethod_name)()
- data['status'] = status
- data['error_msg'] = data.get('error_msg', '')
- return status, data
- except AttributeError:
- sys.stderr.write("Test not defined: {}\n".format(testname))
- return False, {}
- except Exception as ex:
- sys.stderr.write(
- "Test {} failed to execute: {}\n".format(testname, str(ex))
- )
- if self.args.debug:
- raise
- return False, {'error_msg': str(ex)}
- tests_successful = True
- result = {}
- for test in self.tests_to_run:
- status, result_data = execute_test(test)
- tests_successful = tests_successful and status
- result[test] = result_data
- if self.args.lv_compat:
- result = filter_results_for_lv(result)
- post_results(result)
- if self.reload_fpga_image and not self.args.skip_fpga_reload:
- load_fpga_image(self.DEFAULT_FPGA_TYPE)
- return tests_successful
+ def get_mb_periph_mgr(self):
+ """Return reference to an n3xx periph manager"""
+ from usrp_mpm.periph_manager.n3xx import n3xx
+ return n3xx
+
+ def get_product_id(self):
+ """Return the mboard product ID (n310 or n300):"""
+ return bist.get_product_id_from_eeprom(valid_ids=['n300', 'n310'])
#############################################################################
# BISTS
# All bist_* methods must return True/False success values!
#############################################################################
- def bist_rtc(self):
- """
- BIST for RTC (real time clock)
-
- Return dictionary:
- - date: Returns the current UTC time, with seconds-accuracy, in ISO 8601
- format, as a string. As if running 'date -Iseconds -u'.
- - time: Same time, but in seconds since epoch.
-
- Return status:
- Unless datetime throws an exception, returns True.
- """
- assert 'rtc' in self.tests_to_run
- utc_now = datetime.utcnow()
- return True, {
- 'time': time.mktime(utc_now.timetuple()),
- 'date': utc_now.replace(microsecond=0).isoformat() + "+00:00",
- }
-
def bist_ddr3(self):
"""
BIST for PL DDR3 DRAM
@@ -437,25 +128,7 @@ class N3XXBIST(object):
assert 'ddr3' in self.tests_to_run
if self.args.dry_run:
return True, {'throughput': 1250e6}
- result = {}
- ddr3_bist_executor = 'uhd_usrp_probe --args addr=127.0.0.1'
- try:
- output = subprocess.check_output(
- ddr3_bist_executor,
- stderr=subprocess.STDOUT,
- shell=True,
- )
- except subprocess.CalledProcessError as ex:
- # Don't throw errors from uhd_usrp_probe
- output = ex.output
- output = output.decode("utf-8")
- mobj = re.search(r"Throughput: (?P<thrup>[0-9.]+)\s?MB", output)
- if mobj is not None:
- result['throughput'] = float(mobj.group('thrup')) * 1000
- else:
- result['throughput'] = 0
- result['error_msg'] = result.get('error_msg', '') + \
- "\n\nFailed match throughput regex!"
+ result = bist.test_ddr3_with_usrp_probe()
return result.get('throughput', 0) > 1000e3, result
def bist_gpsdo(self):
@@ -501,7 +174,7 @@ class N3XXBIST(object):
sys.stderr.write(
"Waiting for WARMUP to go low for up to {} seconds...\n".format(
gps_warmup_timeout))
- if not poll_with_timeout(
+ if not bist.poll_with_timeout(
lambda: not gpio_tca6424.get('GPS-WARMUP'),
gps_warmup_timeout*1000, 1000
):
@@ -513,7 +186,7 @@ class N3XXBIST(object):
sys.stderr.write(
"Waiting for LOCKOK to go high for up to {} seconds...\n".format(
gps_lockok_timeout))
- if not poll_with_timeout(
+ if not bist.poll_with_timeout(
lambda: gpio_tca6424.get('GPS-LOCKOK'),
gps_lockok_timeout*1000,
1000
@@ -528,24 +201,8 @@ class N3XXBIST(object):
sys.stderr.write("GPS-ALARM status: {}\n".format(
gpio_tca6424.get('GPS-ALARM')
))
- # Now read back response from chip
- my_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- my_sock.connect(('localhost', 2947))
- sys.stderr.write("Connected to GPSDO socket.\n")
- query_cmd = b'?WATCH={"enable":true,"json":true}'
- my_sock.sendall(query_cmd)
- sys.stderr.write("Sent query: {}\n".format(query_cmd))
- sock_read_line(my_sock, timeout=10)
- sys.stderr.write("Received initial newline.\n")
- result = {}
- while result.get('class', None) != 'TPV':
- json_result = sock_read_line(my_sock, timeout=60)
- sys.stderr.write(
- "Received JSON response: {}\n\n".format(json_result)
- )
- result = json.loads(json_result)
- my_sock.sendall(b'?WATCH={"enable":false}')
- my_sock.close()
+ # Now the chip is on, read back the TPV result
+ result = bist.get_gpsd_tpv_result()
# If we reach this line, we have a valid result and the chip responded.
# However, it doesn't necessarily mean we had a GPS lock.
return True, result
@@ -567,17 +224,87 @@ class N3XXBIST(object):
return True, {
'tpm0_caps': "Fake caps value\n\nVersion 0.0.0",
}
- result = {}
- props_to_read = ('caps',)
- base_path = '/sys/class/tpm'
- for tpm_device in os.listdir(base_path):
- if tpm_device.startswith('tpm'):
- for key in props_to_read:
- result['{}_{}'.format(tpm_device, key)] = open(
- os.path.join(base_path, tpm_device, key), 'r'
- ).read().strip()
+ result = bist.get_tpm_caps_info()
return len(result) == 1, result
+ def bist_ref_clock_int(self):
+ """
+ BIST for clock lock from internal (25 MHz) source.
+ Description: Checks to see if the daughtercard can lock to an internal
+ clock source.
+
+ External Equipment: None
+ Return dictionary:
+ - <sensor-name>:
+ - locked: Boolean lock status
+
+ There can be multiple ref lock sensors; for a pass condition they all
+ need to be asserted.
+ """
+ assert 'ref_clock_int' in self.tests_to_run
+ if self.args.dry_run:
+ return True, {'ref_locked': True}
+ result = bist.get_ref_clock_prop(
+ 'internal',
+ 'internal',
+ extra_args={'skip_rfic': 1, 'rfnoc_num_blocks': 0}
+ )
+ return 'error_msg' not in result, result
+
+ def bist_ref_clock_ext(self):
+ """
+ BIST for clock lock from external source. Note: This test requires a
+ connected daughterboard with a 'ref lock' sensor available.
+
+ Description: Checks to see if the daughtercard can lock to the external
+ reference clock.
+
+ External Equipment: 10 MHz reference Source connected to "ref in".
+
+ Return dictionary:
+ - <sensor-name>:
+ - locked: Boolean lock status
+
+ There can be multiple ref lock sensors; for a pass condition they all
+ need to be asserted.
+ """
+ assert 'ref_clock_ext' in self.tests_to_run
+ if self.args.dry_run:
+ return True, {'ref_locked': True}
+ result = bist.get_ref_clock_prop(
+ 'external',
+ 'external',
+ extra_args={'skip_rfic': 1, 'rfnoc_num_blocks': 0}
+ )
+ return 'error_msg' not in result, result
+
+ def bist_ref_clock_gpsdo(self):
+ """
+ BIST for clock lock from external source. Note: This test requires a
+ connected daughterboard with a 'ref lock' sensor available.
+
+ Description: Checks to see if the daughtercard can lock to the external
+ reference clock.
+
+ External Equipment: 10 MHz reference Source connected to "ref in".
+
+ Return dictionary:
+ - <sensor-name>:
+ - locked: Boolean lock status
+
+ There can be multiple ref lock sensors; for a pass condition they all
+ need to be asserted.
+ """
+ assert 'ref_clock_gpsdo' in self.tests_to_run
+ if self.args.dry_run:
+ return True, {'ref_locked': True}
+ result = bist.get_ref_clock_prop(
+ 'gpsdo',
+ 'gpsdo',
+ extra_args={'skip_rfic': 1, 'rfnoc_num_blocks': 0}
+ )
+ return 'error_msg' not in result, result
+
def bist_sfp0_loopback(self):
"""
BIST for SFP+ ports:
@@ -596,10 +323,14 @@ class N3XXBIST(object):
- bits: Number of bits that were transferred
"""
if self.args.dry_run:
- return True, get_sfp_bist_defaults()
- sfp_bist_results = run_aurora_bist(master='misc-auro-regs0')
+ return True, bist.get_sfp_bist_defaults()
+ sfp_bist_results = bist.run_aurora_bist(
+ device_args=self.device_args,
+ product_id=self.get_product_id(),
+ master='misc-auro-regs0',
+ )
self.reload_fpga_image = True
- return aurora_results_to_status(sfp_bist_results)
+ return bist.aurora_results_to_status(sfp_bist_results)
def bist_sfp1_loopback(self):
"""
@@ -619,10 +350,14 @@ class N3XXBIST(object):
- bits: Number of bits that were transferred
"""
if self.args.dry_run:
- return True, get_sfp_bist_defaults()
- sfp_bist_results = run_aurora_bist(master='misc-auro-regs1')
+ return True, bist.get_sfp_bist_defaults()
+ sfp_bist_results = bist.run_aurora_bist(
+ device_args=self.device_args,
+ product_id=self.get_product_id(),
+ master='misc-auro-regs1',
+ )
self.reload_fpga_image = True
- return aurora_results_to_status(sfp_bist_results)
+ return bist.aurora_results_to_status(sfp_bist_results)
def bist_sfp_loopback(self):
"""
@@ -642,13 +377,15 @@ class N3XXBIST(object):
- bits: Number of bits that were transferred
"""
if self.args.dry_run:
- return True, get_sfp_bist_defaults()
- sfp_bist_results = run_aurora_bist(
+ return True, bist.get_sfp_bist_defaults()
+ sfp_bist_results = bist.run_aurora_bist(
+ device_args=self.device_args,
+ product_id=self.get_product_id(),
master='misc-auro-regs0',
slave='misc-auro-regs1',
)
self.reload_fpga_image = True
- return aurora_results_to_status(sfp_bist_results)
+ return bist.aurora_results_to_status(sfp_bist_results)
def bist_gpio(self):
"""
@@ -690,7 +427,7 @@ class N3XXBIST(object):
" Run a GPIO test for a given set of patterns "
gpio_ctrl = n3xx_periphs.FrontpanelGPIO(ddr)
for pattern in patterns:
- gpio_set_all(gpio_ctrl, pattern, GPIO_WIDTH, ddr)
+ bist.gpio_set_all(gpio_ctrl, pattern, GPIO_WIDTH, ddr)
time.sleep(0.1)
gpio_rb = gpio_ctrl.get_all()
if pattern != gpio_rb:
@@ -716,15 +453,8 @@ class N3XXBIST(object):
assert 'temp' in self.tests_to_run
if self.args.dry_run:
return True, {'fpga-thermal-zone': 30000}
- import pyudev
- context = pyudev.Context()
- result = {
- device.attributes.get('type').decode('ascii'): \
- int(device.attributes.get('temp').decode('ascii'))
- for device in context.list_devices(subsystem='thermal')
- if 'temp' in device.attributes.available_attributes \
- and device.attributes.get('temp') is not None
- }
+ result = bist.get_temp_sensor_value(
+ lambda device: device.attributes.get('type').decode('ascii'))
if len(result) < 1:
result['error_msg'] = "No temperature sensors found!"
return 'error_msg' not in result, result
@@ -742,14 +472,7 @@ class N3XXBIST(object):
assert 'fan' in self.tests_to_run
if self.args.dry_run:
return True, {'cooling_device0': 10000, 'cooling_device1': 10000}
- import pyudev
- context = pyudev.Context()
- result = {
- device.sys_name: int(device.attributes.get('cur_state'))
- for device in context.list_devices(subsystem='thermal')
- if 'cur_state' in device.attributes.available_attributes \
- and device.attributes.get('cur_state') is not None
- }
+ result = bist.get_fan_values()
return len(result) == 2, result
def bist_whiterabbit(self):
@@ -768,7 +491,11 @@ class N3XXBIST(object):
from usrp_mpm.sys_utils import uio
if not uio.find_uio_device(n3xx.wr_regs_label, logger=self.log)[0]:
self.log.info("Need to load WX image before proceeding...")
- load_fpga_image('WX')
+ bist.load_fpga_image(
+ 'WX',
+ self.device_args,
+ self.get_product_id(),
+ )
self.log.info("Image loading complete.")
self.reload_fpga_image = True
mb_regs = n3xx_periphs.MboardRegsControl(
@@ -776,7 +503,7 @@ class N3XXBIST(object):
mb_regs.set_time_source('sfp0', 25e6)
wr_regs_control = WhiteRabbitRegsControl(
n3xx.wr_regs_label, self.log)
- lock_status = poll_with_timeout(
+ lock_status = bist.poll_with_timeout(
lambda: wr_regs_control.get_time_lock_status(),
40000, # Try for x ms... this number is set from a few benchtop tests
1000, # Poll every... second! why not?
@@ -786,160 +513,6 @@ class N3XXBIST(object):
}
return lock_status, result
- def bist_ref_clock_int(self):
- """
- BIST for clock and pps lock from internal (25MHz).
- Description: Checks to see if we can lock to an internal
- clock source.
-
- External Equipment: None
- Return dictionary:
- - <sensor-name>:
- - locked: Boolean lock status
-
- There can be multiple ref lock sensors; for a pass condition they all
- need to be asserted.
- """
- assert 'ref_clock_int' in self.tests_to_run
- result = self.ref_clock_helper('internal', 'internal')
- return 'error_msg' not in result, result
-
- def bist_ref_clock_ext(self):
- """
- BIST for clock pps lock from external (10MHz) source.
- Description: Checks to see if we can lock to an external
- clock source.
-
- External Equipment: External 10 MHz reference clock needed (from Octoclock)
- Return dictionary:
- - <sensor-name>:
- - locked: Boolean lock status
-
- There can be multiple ref lock sensors; for a pass condition they all
- need to be asserted.
- """
- assert 'ref_clock_ext' in self.tests_to_run
- result = self.ref_clock_helper('external', 'external')
- return 'error_msg' not in result, result
-
- def bist_ref_clock_gpsdo(self):
- """
- BIST for clock and pps lock from gpsdo (20MHz) source.
- Description: Checks to see if we can lock to an external
- clock source.
-
- External Equipment: None
- Return dictionary:
- - <sensor-name>:
- - locked: Boolean lock status
-
- There can be multiple ref lock sensors; for a pass condition they all
- need to be asserted.
- """
- assert 'ref_clock_gpsdo' in self.tests_to_run
- result = self.ref_clock_helper('gpsdo', 'gpsdo')
- return 'error_msg' not in result, result
-
- def ref_clock_helper(self, clock_source, time_source):
- """
- Helper function to determine reference clock lock
- Description: Checks to see if we can lock to a clock source.
- External Equipment: None
- Return dictionary:
- - <sensor-name>:
- - locked: Boolean lock status
- """
- assert clock_source in ("gpsdo", "internal", "external"),\
- "Invalid clock source selected ({}). Valid choices: {}".format(
- clock_source, ("gpsdo", "internal", "external"))
-
- assert time_source in ("gpsdo", "internal", "external", "sfp0"),\
- "Invalid time source selected ({}). Valid choices: {}".format(
- time_source, ("gpsdo", "internal", "external", "sfp0"))
-
- if self.args.dry_run:
- return True, {'ref_locked': True}
- result = {}
- env = os.environ.copy()
- env['UHD_LOG_CONSOLE_LEVEL'] = 'error'
- cmd = ['uhd_usrp_probe', '--args',
- 'addr=127.0.0.1,'
- 'rfnoc_num_blocks=0,'
- 'skip_rfic=1,'
- 'clock_source={c},time_source={t}'.format(c=clock_source,
- t=time_source),
- '--sensor']
- sensor_path = '/mboards/0/sensors/ref_locked'
- cmd.append(sensor_path)
- ref_lock_executor = ' '.join(cmd)
- try:
- output = subprocess.check_output(
- ref_lock_executor,
- stderr=subprocess.STDOUT,
- env=env,
- shell=True,
- )
- except subprocess.CalledProcessError as ex:
- # Don't throw errors from uhd_usrp_probe
- output = ex.output
- output = output.decode("utf-8")
- mobj = re.search(r"true$", output.strip())
- if mobj is not None:
- result['ref_locked'] = True
- else:
- result['ref_locked'] = False
- result['error_msg'] = ("Reference Clock not locked."
- " Extra output:" + output)
- return result
-
-
-
-def gpio_set_all(gpio_bank, value, gpio_size, ddr_mask):
- """Helper function for set gpio.
- What this function do is take decimal value and convert to a binary string
- then try to set those individual bits to the gpio_bank.
- Arguments:
- 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_size = bin(ddr_mask).count("1")
- value_bitstring = ('{0:0' + str(ddr_size) + 'b}').format(value)[-(gpio_size):]
- ddr_bitstring = ('{0:0' + str(gpio_size) + 'b}').format(ddr_mask)[-(gpio_size):]
- for i in range(gpio_size):
- if ddr_bitstring[gpio_size - 1 - i] == "1":
- gpio_bank.set(i, value_bitstring[i % ddr_size])
-
-def get_product_id():
- """Return the mboard product ID (n310 or n300):"""
- cmd = ['eeprom-id']
- output = subprocess.check_output(
- cmd,
- stderr=subprocess.STDOUT,
- shell=True,
- ).decode('utf-8')
- if 'n310' in output:
- return 'n310'
- elif 'n300' in output:
- return 'n300'
- raise AssertionError("Cannot determine product ID.")
-
-def load_fpga_image(fpga_type):
- """Load an FPGA image (HG, XG, AA, ...)"""
- cmd = ['uhd_image_loader', '--args', 'type=n3xx', '--fpga-path']
- images_folder = '/usr/share/uhd/images/'
- fpga_file_name = \
- 'usrp_' + get_product_id() + '_fpga_' + fpga_type.upper() + '.bit'
- fpga_image = images_folder + fpga_file_name
- cmd.append(fpga_image)
- cmd_str = ' '.join(cmd)
- subprocess.check_output(
- cmd_str,
- stderr=subprocess.STDOUT,
- shell=True
- )
-
##############################################################################
# main
##############################################################################
diff --git a/mpm/python/usrp_mpm/bist.py b/mpm/python/usrp_mpm/bist.py
new file mode 100644
index 000000000..b16fefa35
--- /dev/null
+++ b/mpm/python/usrp_mpm/bist.py
@@ -0,0 +1,598 @@
+#
+# Copyright 2018 Ettus Research, a National Instruments Company
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+"""
+Utilities to write BIST executables for USRPs
+"""
+
+import os
+import re
+import sys
+import time
+import json
+import select
+import socket
+from datetime import datetime
+import argparse
+import subprocess
+from six import iteritems
+
+##############################################################################
+# Aurora/SFP BIST code
+##############################################################################
+def get_sfp_bist_defaults():
+ " Default dictionary for SFP/Aurora BIST dry-runs "
+ return {
+ 'elapsed_time': 1.0,
+ 'max_roundtrip_latency': 0.8e-6,
+ 'throughput': 1000e6,
+ 'max_ber': 8.5e-11,
+ 'errors': 0,
+ 'bits': 12012486656,
+ }
+
+def assert_aurora_image(master, slave, device_args, product_id, aurora_image_type='AA'):
+ """
+ Make sure we have an FPGA image with which we can run the requested tests.
+
+ Will load an AA image if not, which always satisfies all conditions for
+ running Aurora tests.
+ """
+ from usrp_mpm.sys_utils import uio
+ if not uio.find_uio_device(master)[0] or \
+ (slave is not None and not uio.find_uio_device(slave)[0]):
+ load_fpga_image(
+ fpga_type=aurora_image_type,
+ device_args=device_args,
+ product_id=product_id,
+ )
+
+def aurora_results_to_status(bist_results):
+ """
+ Convert a dictionary coming from AuroraControl BIST to one that we can use
+ for this BIST
+ """
+ return bist_results['mst_errors'] == 0, {
+ 'elapsed_time': bist_results['time_elapsed'],
+ 'max_roundtrip_latency': bist_results['mst_latency_us'],
+ 'throughput': bist_results['approx_throughput'],
+ 'max_ber': bist_results['max_ber'],
+ 'errors': bist_results['mst_errors'],
+ 'bits': bist_results['mst_samps'],
+ }
+
+def run_aurora_bist(
+ device_args,
+ product_id,
+ master,
+ slave=None,
+ requested_rate=1300*8e6,
+ aurora_image_type='AA'):
+ """
+ Spawn a BER test
+ """
+ from usrp_mpm import aurora_control
+ from usrp_mpm.sys_utils.uio import open_uio
+
+ class DummyContext(object):
+ """Dummy class for context managers"""
+ def __enter__(self):
+ return
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ return exc_type is None
+
+ # Go, go, go!
+ try:
+ assert_aurora_image(master, slave, device_args, product_id, aurora_image_type)
+ with open_uio(label=master, read_only=False) as master_au_uio:
+ master_au_ctrl = aurora_control.AuroraControl(master_au_uio)
+ with open_uio(label=slave, read_only=False)\
+ if slave is not None else DummyContext() as slave_au_uio:
+ slave_au_ctrl = aurora_control.AuroraControl(slave_au_uio)\
+ if slave is not None else None
+ return master_au_ctrl.run_ber_loopback_bist(
+ duration=10,
+ requested_rate=requested_rate,
+ slave=slave_au_ctrl,
+ )
+ except Exception as ex:
+ print("Unexpected exception: {}".format(str(ex)))
+ exit(1)
+
+
+##############################################################################
+# Helpers
+##############################################################################
+def post_results(results):
+ """
+ Given a dictionary, post the results.
+
+ This will print the results as JSON to stdout.
+ """
+ print(json.dumps(
+ results,
+ sort_keys=True,
+ indent=4,
+ separators=(',', ': ')
+ ))
+
+def sock_read_line(my_sock, timeout=60, interval=0.1):
+ """
+ Read from a socket until newline. If there was no newline until the timeout
+ occurs, raise an error. Otherwise, return the line.
+ """
+ line = b''
+ end_time = time.time() + timeout
+ while time.time() < end_time:
+ socket_ready = select.select([my_sock], [], [], 0)[0]
+ if socket_ready:
+ next_char = my_sock.recv(1)
+ if next_char == b'\n':
+ return line.decode('ascii')
+ line += next_char
+ else:
+ time.sleep(interval)
+ raise RuntimeError("sock_read_line() exceeded read timeout!")
+
+def poll_with_timeout(state_check, timeout_ms, interval_ms):
+ """
+ Calls state_check() every interval_ms until it returns a positive value, or
+ until a timeout is exceeded.
+
+ Returns True if state_check() returned True within the timeout.
+ """
+ max_time = time.time() + (float(timeout_ms) / 1000)
+ interval_s = float(interval_ms) / 1000
+ while time.time() < max_time:
+ if state_check():
+ return True
+ time.sleep(interval_s)
+ return False
+
+def expand_options(option_list):
+ """
+ Turn a list ['foo=bar', 'spam=eggs'] into a dictionary {'foo': 'bar',
+ 'spam': 'eggs'}.
+ """
+ return dict(x.split('=') for x in option_list)
+
+def load_fpga_image(
+ fpga_type,
+ device_args,
+ product_id,
+ images_folder='/usr/share/uhd/images/'):
+ """Load an FPGA image (1G, XG, AA, ...)"""
+ # cmd = ['uhd_image_loader', '--args', 'type=e3xx,addr=127.0.0.1', '--fpga-path']
+ fpga_file_name = \
+ 'usrp_' + product_id + '_fpga_' + fpga_type.upper() + '.bit'
+ fpga_image = images_folder + fpga_file_name
+ cmd = [
+ 'uhd_image_loader',
+ '--args', device_args,
+ '--fpga-path', fpga_image
+ ]
+ cmd_str = ' '.join(cmd)
+ subprocess.check_output(
+ cmd_str,
+ stderr=subprocess.STDOUT,
+ shell=True
+ )
+
+def filter_results_for_lv(results, lv_compat_format):
+ """
+ The LabView JSON parser does not support a variety of things, such as
+ nested dicts, and some downstream LV applications freak out if certain keys
+ are not what they expect.
+ This is a long hard-coded list of how results should look like for those
+ cases. Note: This list needs manual supervision and attention for the case
+ where either subsystems get renamed, or other architectural changes should
+ occur.
+ """
+ def fixup_dict(result_dict, ref_dict):
+ """
+ Touches up result_dict according to ref_dict by the following rules:
+ - If a key is in result_dict that is not in ref_dict, delete that
+ - If a key is in ref_dict that is not in result_dict, use the value
+ from ref_dict
+ """
+ ref_dict['error_msg'] = ""
+ ref_dict['status'] = False
+ result_dict = {
+ k: v for k, v in iteritems(result_dict)
+ if k in ref_dict or k in ('error_msg', 'status')
+ }
+ result_dict = {
+ k: result_dict.get(k, ref_dict[k]) for k in ref_dict
+ }
+ return result_dict
+ # GoGoGo
+ results = {
+ testname: fixup_dict(testresults, lv_compat_format[testname]) \
+ if testname in lv_compat_format else testresults
+ for testname, testresults in iteritems(results)
+ }
+ return results
+
+def get_product_id_from_eeprom(valid_ids):
+ """Return the mboard product ID
+
+ Returns something like n300, n310, e320...
+ """
+ cmd = ['eeprom-id']
+ output = subprocess.check_output(
+ cmd,
+ stderr=subprocess.STDOUT,
+ shell=True,
+ ).decode('utf-8')
+ for valid_id in valid_ids:
+ if valid_id in output:
+ return valid_id
+ raise AssertionError("Cannot determine product ID.: `{}'".format(output))
+
+def get_tpm_caps_info():
+ """Read 'caps' info from TPM subsystem"""
+ result = {}
+ props_to_read = ('caps',)
+ base_path = '/sys/class/tpm'
+ for tpm_device in os.listdir(base_path):
+ if tpm_device.startswith('tpm'):
+ for key in props_to_read:
+ result['{}_{}'.format(tpm_device, key)] = open(
+ os.path.join(base_path, tpm_device, key), 'r'
+ ).read().strip()
+ return result
+
+def gpio_set_all(gpio_bank, value, gpio_size, ddr_mask):
+ """Helper function for set gpio.
+ What this function do is take decimal value and convert to a binary string
+ then try to set those individual bits to the gpio_bank.
+ Arguments:
+ 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_size = bin(ddr_mask).count("1")
+ value_bitstring = \
+ ('{0:0' + str(ddr_size) + 'b}').format(value)[-(gpio_size):]
+ ddr_bitstring = \
+ ('{0:0' + str(gpio_size) + 'b}').format(ddr_mask)[-(gpio_size):]
+ for i in range(gpio_size):
+ if ddr_bitstring[gpio_size - 1 - i] == "1":
+ gpio_bank.set(i, value_bitstring[i % ddr_size])
+
+##############################################################################
+# Common tests
+##############################################################################
+def test_ddr3_with_usrp_probe():
+ """
+ Run uhd_usrp_probe and scrape the output to see if the DRAM FIFO block is
+ reporting a good throughput. This is a bit of a roundabout way of testing
+ the DDR3, but it uses existing software and also tests the RFNoC pathways.
+ """
+ result = {}
+ ddr3_bist_executor = 'uhd_usrp_probe --args addr=127.0.0.1'
+ try:
+ output = subprocess.check_output(
+ ddr3_bist_executor,
+ stderr=subprocess.STDOUT,
+ shell=True,
+ )
+ except subprocess.CalledProcessError as ex:
+ # Don't throw errors from uhd_usrp_probe
+ output = ex.output
+ output = output.decode("utf-8")
+ mobj = re.search(r"Throughput: (?P<thrup>[0-9.]+)\s?MB", output)
+ if mobj is not None:
+ result['throughput'] = float(mobj.group('thrup')) * 1000
+ else:
+ result['throughput'] = 0
+ result['error_msg'] = result.get('error_msg', '') + \
+ "\n\nFailed match throughput regex!"
+ return result
+
+
+def get_gpsd_tpv_result():
+ """
+ Query gpsd via a socket and return the corresponding JSON result as a
+ dictionary.
+ """
+ my_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ my_sock.connect(('localhost', 2947))
+ sys.stderr.write("Connected to GPSDO socket.\n")
+ query_cmd = b'?WATCH={"enable":true,"json":true}'
+ my_sock.sendall(query_cmd)
+ sys.stderr.write("Sent query: {}\n".format(query_cmd))
+ sock_read_line(my_sock, timeout=10)
+ sys.stderr.write("Received initial newline.\n")
+ result = {}
+ while result.get('class', None) != 'TPV':
+ json_result = sock_read_line(my_sock, timeout=60)
+ sys.stderr.write(
+ "Received JSON response: {}\n\n".format(json_result)
+ )
+ result = json.loads(json_result)
+ my_sock.sendall(b'?WATCH={"enable":false}')
+ my_sock.close()
+ return result
+
+def get_ref_clock_prop(clock_source, time_source, extra_args=None):
+ """
+ Helper function to determine reference clock lock
+ Description: Checks to see if we can lock to a clock source.
+ The actual value is yanked from the property tree.
+
+ External Equipment: None
+ Return dictionary:
+ - <sensor-name>:
+ - locked: Boolean lock status
+ """
+ extra_args = extra_args or {}
+ result = {}
+ extra_args_str = ",".join(
+ ['{k}={v}'.format(k=k, v=v) for k, v in iteritems(extra_args)])
+ cmd = [
+ 'uhd_usrp_probe',
+ '--args',
+ 'addr=127.0.0.1,clock_source={c},time_source={t},{e}'.format(
+ c=clock_source, t=time_source, e=extra_args_str),
+ '--sensor'
+ ]
+ sensor_path = '/mboards/0/sensors/ref_locked'
+ cmd.append(sensor_path)
+ ref_lock_executor = ' '.join(cmd)
+ try:
+ output = subprocess.check_output(
+ ref_lock_executor,
+ stderr=subprocess.PIPE,
+ shell=True,
+ )
+ except subprocess.CalledProcessError as ex:
+ # Don't throw errors from uhd_usrp_probe
+ output = ex.output
+ output = output.decode("utf-8").strip()
+ mobj = re.search(r"true$", output)
+ if mobj is not None:
+ result['ref_locked'] = True
+ else:
+ result['ref_locked'] = False
+ result['error_msg'] = "Reference Clock not locked: " + output
+ return result
+
+def get_temp_sensor_value(temp_sensor_map):
+ """
+ Read a temp sensor value from the system and return a dictionary of the
+ form {temp_sensor_lookup(device): $temp}
+ """
+ import pyudev
+ context = pyudev.Context()
+ return {
+ temp_sensor_map(device): \
+ int(device.attributes.get('temp').decode('ascii'))
+ for device in context.list_devices(subsystem='thermal')
+ if 'temp' in device.attributes.available_attributes \
+ and device.attributes.get('temp') is not None
+ }
+
+def get_fan_values():
+ """
+ Return a dict of fan name -> fan speed key/values.
+ """
+ import pyudev
+ context = pyudev.Context()
+ return {
+ device.sys_name: int(device.attributes.get('cur_state'))
+ for device in context.list_devices(subsystem='thermal')
+ if 'cur_state' in device.attributes.available_attributes \
+ and device.attributes.get('cur_state') is not None
+ }
+
+def get_link_up(if_name):
+ """
+ Return a dictionary {if_name: IFLA_OPERSTATE}
+ """
+ from pyroute2 import IPRoute
+ result = {}
+ with IPRoute() as ipr:
+ links = ipr.link_lookup(ifname=if_name)
+ if not links:
+ return {'error_msg': "No interface found"}
+ link_info = next(iter(ipr.get_links(links)), None)
+ if link_info is None:
+ return {'error_msg': "Error on get_links for sfp0"}
+ result['sfp0'] = link_info.get_attr('IFLA_OPERSTATE')
+ if result['sfp0'] != 'UP':
+ result['error_msg'] = "Link not up for interface"
+ return result
+
+
+
+##############################################################################
+# BIST class
+##############################################################################
+class UsrpBIST(object):
+ """
+ BIST parent class
+ """
+ usrp_type = None
+ default_rev = 3 # Because why not
+ # This defines special tests that are really collections of other tests.
+ collections = {
+ 'standard': ["rtc",],
+ 'extended': "*",
+ }
+ # Default FPGA image type
+ DEFAULT_FPGA_TYPE = None
+ lv_compat_format = None
+ device_args = 'addr=127.0.0.1'
+
+ def make_arg_parser(self):
+ """
+ Return arg parser
+ """
+ parser = argparse.ArgumentParser(
+ description="{} BIST Tool".format(self.usrp_type),
+ )
+ parser.add_argument(
+ '-n', '--dry-run', action='store_true',
+ help="Fake out the tests. All tests will return a valid" \
+ " response, but will not actually interact with hardware.",
+ )
+ parser.add_argument(
+ '-v', '--verbose', action='store_true',
+ help="Crank up verbosity level",
+ )
+ parser.add_argument(
+ '--debug', action='store_true',
+ help="For debugging this tool.",
+ )
+ parser.add_argument(
+ '--option', '-o', action='append', default=[],
+ help="Option for individual test.",
+ )
+ parser.add_argument(
+ '--lv-compat', action='store_true',
+ help="Provides compatibility with the LV JSON parser. Don't run "
+ "this mode unless you know what you're doing. The JSON "
+ "output does not necessarily reflect the actual system "
+ "status when using this mode.",
+ )
+ parser.add_argument(
+ '--skip-fpga-reload', action='store_true',
+ help="Skip reloading the default FPGA image post-test. Note: by"
+ "specifying this argument, the FPGA image loaded could be "
+ "anything post-test.",
+ )
+ parser.add_argument(
+ 'tests',
+ help="List the tests that should be run",
+ nargs='+', # There has to be at least one
+ )
+ return parser
+
+ def get_mb_periph_mgr(self):
+ """Needs to be implemented by child class"""
+ raise NotImplementedError
+
+ def get_product_id(self):
+ """Needs to be implemented by child class"""
+ raise NotImplementedError
+
+ def __init__(self):
+ assert self.DEFAULT_FPGA_TYPE is not None
+ assert self.device_args is not None
+ assert self.usrp_type is not None
+ assert self.lv_compat_format is not None
+ self.args = self.make_arg_parser().parse_args()
+ self.args.option = expand_options(self.args.option)
+ # If this is true, trigger a reload of the default FPGA image
+ self.reload_fpga_image = False
+ try:
+ default_rev = self.get_mb_periph_mgr().mboard_max_rev
+ except ImportError:
+ # This means we're in dry run mode or something like that, so just
+ # pick something
+ default_rev = self.default_rev
+ self.mb_rev = int(self.args.option.get('mb_rev', default_rev))
+ self.tests_to_run = set()
+ for test in self.args.tests:
+ if test in self.collections:
+ for this_test in self.expand_collection(test):
+ self.tests_to_run.add(this_test)
+ else:
+ self.tests_to_run.add(test)
+ try:
+ # Keep this import here so we can do dry-runs without any MPM code
+ from usrp_mpm import get_main_logger
+ if not self.args.verbose:
+ from usrp_mpm.mpmlog import WARNING
+ get_main_logger().setLevel(WARNING)
+ self.log = get_main_logger().getChild('main')
+ except ImportError:
+ print("No logging capability available.")
+
+ def expand_collection(self, coll):
+ """
+ Return names of tests in a collection
+ """
+ tests = self.collections[coll]
+ if tests == "*":
+ tests = {x.replace('bist_', '')
+ for x in dir(self)
+ if x.find('bist_') == 0
+ }
+ else:
+ tests = set(tests)
+ return tests
+
+ def run(self):
+ """
+ Execute tests.
+
+ Returns True on Success.
+ """
+ def execute_test(testname):
+ """
+ Actually run a test.
+ """
+ testmethod_name = "bist_{0}".format(testname)
+ sys.stderr.write(
+ "Executing test method: {0}\n\n".format(testmethod_name)
+ )
+ if not hasattr(self, testmethod_name):
+ sys.stderr.write("Test not defined: `{}`\n".format(testname))
+ return False, {}
+ try:
+ status, data = getattr(self, testmethod_name)()
+ data['status'] = status
+ data['error_msg'] = data.get('error_msg', '')
+ return status, data
+ except Exception as ex:
+ sys.stderr.write(
+ "Test {} failed to execute: {}\n".format(testname, str(ex))
+ )
+ if self.args.debug:
+ raise
+ return False, {'error_msg': str(ex)}
+ tests_successful = True
+ result = {}
+ for test in self.tests_to_run:
+ status, result_data = execute_test(test)
+ tests_successful = tests_successful and status
+ result[test] = result_data
+ if self.args.lv_compat:
+ result = filter_results_for_lv(result, self.lv_compat_format)
+ post_results(result)
+ if self.reload_fpga_image and not self.args.skip_fpga_reload:
+ load_fpga_image(
+ self.DEFAULT_FPGA_TYPE,
+ self.device_args,
+ self.get_product_id(),
+ )
+ return tests_successful
+
+#############################################################################
+# BISTS
+# All bist_* methods must return True/False success values!
+#############################################################################
+ def bist_rtc(self):
+ """
+ BIST for RTC (real time clock)
+
+ Return dictionary:
+ - date: Returns the current UTC time, with seconds-accuracy, in ISO 8601
+ format, as a string. As if running 'date -Iseconds -u'.
+ - time: Same time, but in seconds since epoch.
+
+ Return status:
+ Unless datetime throws an exception, returns True.
+ """
+ assert 'rtc' in self.tests_to_run
+ utc_now = datetime.utcnow()
+ return True, {
+ 'time': time.mktime(utc_now.timetuple()),
+ 'date': utc_now.replace(microsecond=0).isoformat() + "+00:00",
+ }