aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--mpm/python/CMakeLists.txt6
-rw-r--r--mpm/python/aurora_bist_test.py105
-rw-r--r--mpm/python/usrp_mpm/CMakeLists.txt1
-rw-r--r--mpm/python/usrp_mpm/aurora_control.py418
4 files changed, 528 insertions, 2 deletions
diff --git a/mpm/python/CMakeLists.txt b/mpm/python/CMakeLists.txt
index 2f13d9475..6cad6e516 100644
--- a/mpm/python/CMakeLists.txt
+++ b/mpm/python/CMakeLists.txt
@@ -53,5 +53,7 @@ EXECUTE_PROCESS(COMMAND ${PYTHON_EXECUTABLE} -c
)
INSTALL(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/build/lib/usrp_mpm DESTINATION ${CMAKE_INSTALL_PREFIX}/${USRP_MPM_PYTHON_DIR})
INSTALL(PROGRAMS
- usrp_hwd.py
- DESTINATION ${RUNTIME_DIR})
+ usrp_hwd.py
+ aurora_bist_test.py
+ DESTINATION ${RUNTIME_DIR}
+)
diff --git a/mpm/python/aurora_bist_test.py b/mpm/python/aurora_bist_test.py
new file mode 100644
index 000000000..4d6227b60
--- /dev/null
+++ b/mpm/python/aurora_bist_test.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+#
+# Copyright 2017 Ettus Research (National Instruments)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+"""
+Aurora BIST command line utility
+"""
+
+from __future__ import print_function
+import argparse
+import usrp_mpm as mpm
+from usrp_mpm.uio import UIO
+from usrp_mpm.aurora_control import AuroraControl
+
+########################################################################
+# command line options
+########################################################################
+def parse_args():
+ " Create argparser, return args "
+ parser = argparse.ArgumentParser(
+ description='Controller for Ettus Aurora BIST Engine'
+ )
+ parser.add_argument(
+ '--uio-dev', default='misc-enet-regs0',
+ help='UIO device for master device peeks and pokes'
+ )
+ parser.add_argument(
+ '--base-addr', type=int, default=0,
+ help='Base address for register read/writes'
+ )
+ parser.add_argument(
+ '--slave-uio-dev', default=None,
+ help='UIO device for slave device peeks and pokes'
+ )
+ parser.add_argument(
+ '--slave-base-addr', type=int, default=0,
+ help='Base address for register read/writes on slave'
+ )
+ parser.add_argument(
+ '--test', default='ber', choices=['ber', 'latency'],
+ help='Type of test to run'
+ )
+ parser.add_argument(
+ '--duration', type=int, default=10, help='Duration of test in seconds'
+ )
+ parser.add_argument(
+ '--rate', type=int, default=1245, help='BIST throughput in MB/s'
+ )
+ parser.add_argument(
+ '--loopback', action="store_true",
+ help="Don't run a test, but set this Aurora into loopback mode"
+ )
+ return parser.parse_args()
+
+########################################################################
+# main
+########################################################################
+def main():
+ " Dawaj, dawaj! "
+ args = parse_args()
+ # Initialize logger for downstream components
+ mpm.get_main_logger().getChild('main')
+ master_core = AuroraControl(
+ UIO(label=args.uio_dev, read_only=False),
+ args.base_addr,
+ )
+ slave_core = None if args.slave_uio_dev is None else AuroraControl(
+ UIO(label=args.slave_uio_dev, read_only=False),
+ args.slave_base_addr,
+ )
+ if args.loopback:
+ master_core.reset_core()
+ master_core.set_loopback(enable=True)
+ return True
+ # Run BIST
+ if args.test == 'ber':
+ print("Performing BER BIST test.")
+ master_core.run_ber_loopback_bist(
+ args.duration,
+ args.rate * 8e6,
+ slave_core,
+ )
+ else:
+ print("Performing Latency BIST test.")
+ master_core.run_latency_loopback_bist(
+ args.duration,
+ args.rate * 8e6,
+ slave_core,
+ )
+
+if __name__ == '__main__':
+ exit(not main())
diff --git a/mpm/python/usrp_mpm/CMakeLists.txt b/mpm/python/usrp_mpm/CMakeLists.txt
index bef7d9824..1badab41b 100644
--- a/mpm/python/usrp_mpm/CMakeLists.txt
+++ b/mpm/python/usrp_mpm/CMakeLists.txt
@@ -21,6 +21,7 @@
SET(USRP_MPM_FILES ${USRP_MPM_FILES})
SET(USRP_MPM_TOP_FILES
${CMAKE_CURRENT_SOURCE_DIR}/__init__.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/aurora_control.py
${CMAKE_CURRENT_SOURCE_DIR}/discovery.py
${CMAKE_CURRENT_SOURCE_DIR}/dtoverlay.py
${CMAKE_CURRENT_SOURCE_DIR}/eeprom.py
diff --git a/mpm/python/usrp_mpm/aurora_control.py b/mpm/python/usrp_mpm/aurora_control.py
new file mode 100644
index 000000000..1174a2d0d
--- /dev/null
+++ b/mpm/python/usrp_mpm/aurora_control.py
@@ -0,0 +1,418 @@
+#
+# Copyright 2017 Ettus Research (National Instruments)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+"""
+Aurora SFP control
+"""
+
+import math
+import time
+from builtins import str
+from builtins import object
+import tqdm
+from .mpmlog import get_logger
+
+def mean(vals):
+ " Calculate arithmetic mean of vals "
+ return float(sum(vals)) / max(len(vals), 1)
+
+def stddev(vals, mu=None):
+ " Calculate std deviation of vals "
+ mu = mu or mean(vals)
+ return float(sum(((x - mu)**2 for x in vals))) / max(len(vals), 1)
+
+class AuroraControl(object):
+ """
+ Controls an Aurora core.
+ """
+ # These are relative addresses
+ REG_AURORA_MAC_CTRL = 0x30
+ REG_AURORA_MAC_CTRL_STATUS = 0x30
+ REG_AURORA_PHY_CTRL_STATUS = 0x34
+ REG_AURORA_OVERRUNS = 0x38
+ REG_CHECKSUM_ERRORS = 0x3C
+ REG_BIST_CHECKER_SAMPS = 0x40
+ REG_BIST_CHECKER_ERRORS = 0x44
+ REG_AURORA_ENABLED_CHECK = 0x48
+
+ MAC_STATUS_LINK_UP_MSK = 0x00000001
+ MAC_STATUS_HARD_ERR_MSK = 0x00000002
+ MAC_STATUS_SOFT_ERR_MSK = 0x00000004
+ MAC_STATUS_BIST_LOCKED_MSK = 0x00000008
+ MAC_STATUS_BIST_LATENCY_MSK = 0x000FFFF0
+ MAC_STATUS_BIST_LATENCY_OFFSET = 4
+
+ RATE_RES_BITS = 6
+
+ DEFAULT_BUS_CLK_RATE = 208e6
+
+ def __init__(self, peeker_poker32, base_addr=None, bus_clk_rate=None):
+ assert hasattr(peeker_poker32, 'peek32') \
+ and callable(peeker_poker32.peek32)
+ assert hasattr(peeker_poker32, 'poke32') \
+ and callable(peeker_poker32.poke32)
+ self.log = get_logger("AuroraCore")
+ self._regs = peeker_poker32
+ base_addr = base_addr or 0
+ self.log.debug("Base address in register space is: 0x{:04X}".format(
+ base_addr
+ ))
+ self.poke32 = lambda addr, data: self._regs.poke32(
+ addr + base_addr, data
+ )
+ self.peek32 = lambda addr: self._regs.peek32(addr + base_addr)
+ self.mac_ctrl = 0x000
+ self.set_mac_ctrl(self.mac_ctrl)
+ time.sleep(.5)
+ self.bus_clk_rate = bus_clk_rate
+ if self.bus_clk_rate is None:
+ self.bus_clk_rate = self.DEFAULT_BUS_CLK_RATE
+ self.log.warning("Unspecified bus clock rate. Assuming default "
+ "rate of {} MHz.".format(self.bus_clk_rate/1e6))
+ else:
+ self.log.debug("Bus clock rate: {} MHz.".format(
+ self.bus_clk_rate/1e6
+ ))
+ self.bist_max_time_limit = math.floor(2**48/self.bus_clk_rate)-1
+ self.log.debug("BIST max time limit: {} s".format(
+ self.bist_max_time_limit
+ ))
+ self.log.debug("Status of PHY link: 0x{:08X}".format(
+ self.read_phy_ctrl_status()
+ ))
+ if not self.is_phy_link_up():
+ raise RuntimeError("PHY link not up. Check connectors.")
+
+ def read_mac_ctrl_status(self):
+ " Return MAC ctrl status word from core "
+ return self.peek32(self.REG_AURORA_MAC_CTRL_STATUS)
+
+ def read_phy_ctrl_status(self):
+ " Return PHY ctrl status word from core "
+ return self.peek32(self.REG_AURORA_PHY_CTRL_STATUS)
+
+ def read_overruns(self):
+ " Return overrun count from core "
+ return self.peek32(self.REG_AURORA_OVERRUNS)
+
+ def read_checksum_errors(self):
+ " Return checksum error count from core "
+ return self.peek32(self.REG_CHECKSUM_ERRORS)
+
+ def read_bist_checker_samps(self):
+ " Return number of samps processed from core "
+ return self.peek32(self.REG_BIST_CHECKER_SAMPS)
+
+ def read_bist_checker_errors(self):
+ " Return number of errors from core "
+ return self.peek32(self.REG_BIST_CHECKER_ERRORS)
+
+ def set_mac_ctrl(self, mac_ctrl_word):
+ " Write to the MAC ctrl register "
+ self.log.debug("Setting MAC ctrl word to: 0x{:08X}".format(mac_ctrl_word))
+ self.poke32(self.REG_AURORA_MAC_CTRL, mac_ctrl_word)
+
+ def set_bist_checker_and_gen(self, enable):
+ " Enable or disable Aurora BIST: Checker + Generator "
+ if enable:
+ self.log.info("Enable Aurora BIST Checker and Gen")
+ self.mac_ctrl = self.mac_ctrl | 0b11
+ else:
+ self.log.info("Disable Aurora BIST Checker and Gen")
+ self.mac_ctrl = self.mac_ctrl & 0xFFFFFFFC
+ self.set_mac_ctrl(self.mac_ctrl)
+
+ def set_bist_checker(self, enable):
+ " Enable or disable Aurora BIST: Checker only "
+ if enable:
+ self.log.info("Enable Aurora BIST Checker")
+ self.mac_ctrl = self.mac_ctrl | 0b01
+ else:
+ self.log.info("Disable Aurora BIST Checker")
+ self.mac_ctrl = self.mac_ctrl & 0xFFFFFFFE
+ self.set_mac_ctrl(self.mac_ctrl)
+
+ def set_bist_gen(self, enable):
+ " Enable or disable Aurora BIST: Generator only "
+ if enable:
+ self.log.info("Enable Aurora BIST Gen")
+ self.mac_ctrl = self.mac_ctrl | 0b10
+ else:
+ self.log.info("Disable Aurora BIST Gen")
+ self.mac_ctrl = self.mac_ctrl & 0xFFFFFFFD
+ self.set_mac_ctrl(self.mac_ctrl)
+
+ def set_loopback(self, enable):
+ " Enable or disable Aurora loopback mode "
+ if enable:
+ self.log.info("Enable Aurora loopback")
+ self.mac_ctrl = self.mac_ctrl | 0b100
+ else:
+ self.log.info("Disable Aurora loopback")
+ self.mac_ctrl = self.mac_ctrl & 0xFFFFFFFB
+ self.set_mac_ctrl(self.mac_ctrl)
+
+ def set_bist_rate(self, rate_word):
+ " Set BIST rate. It's a 6-bit value in the MAC ctrl register. "
+ self.log.debug("Setting Aurora BIST rate word to 0x{:02X}".format(
+ rate_word
+ ))
+ self.mac_ctrl = self.mac_ctrl | ((rate_word & 0x3F) << 3)
+ self.set_mac_ctrl(self.mac_ctrl)
+
+ def reset_phy(self):
+ " Reset Aurora PHY "
+ self.log.debug("Reset Aurora PHY")
+ self.mac_ctrl = self.mac_ctrl | (1<<9)
+ self.set_mac_ctrl(self.mac_ctrl)
+ self.clear_control_reg()
+
+ def clear_mac(self):
+ " Clear Aurora MAC "
+ self.log.debug("Clear Aurora MAC")
+ self.mac_ctrl = self.mac_ctrl | (1<<10)
+ self.set_mac_ctrl(self.mac_ctrl)
+ self.clear_control_reg()
+
+ def clear_control_reg(self):
+ " Zero out Aurora control register "
+ self.mac_ctrl = 0
+ self.set_mac_ctrl(self.mac_ctrl)
+
+ def get_rate_setting(self, requested_rate, bus_clk_rate):
+ """
+ From a requested bit rate, return the value for the rate register, and
+ the coerced value.
+ """
+ max_rate_word = 2**self.RATE_RES_BITS - 1
+ lines_per_clock = float(requested_rate) / 64 / bus_clk_rate
+ rate_word = int(lines_per_clock * 2**self.RATE_RES_BITS) - 1
+ rate_word = min(max(rate_word, 0), max_rate_word)
+ coerced_rate = ((1+rate_word) / 2**self.RATE_RES_BITS) \
+ * 64 * bus_clk_rate
+ return rate_word, coerced_rate
+
+ def is_phy_link_up(self):
+ """
+ Return True if the PHY link was successfully negotiated.
+ """
+ return bool(self.read_phy_ctrl_status() & 0x1)
+
+ def reset_core(self):
+ " Reset PHY and MAC "
+ self.clear_control_reg()
+ self.clear_mac()
+ self.reset_phy()
+
+ def run_latency_loopback_bist(
+ self,
+ duration,
+ requested_rate,
+ slave=None,
+ ):
+ """
+ Run latency loopback BIST
+
+ slave -- the other sfp core gets set to loopback mode
+ ctrl -- sorta the master sfp core
+ duration -- time we want to run the bist
+ requested_rate -- Requested BIST rate in bits/s
+ """
+ rate_word, coerced_rate = \
+ self.get_rate_setting(requested_rate, self.bus_clk_rate)
+ self.log.info(
+ 'Running Latency Loopback BIST at %.0fMB/s for %.0fs...',
+ coerced_rate/8e6, duration
+ )
+ self._pre_test_init()
+ start_time = time.time()
+ results = {
+ 'latencies': [],
+ 'mst_lock_errors': 0,
+ 'mst_hard_errors': 0,
+ 'mst_overruns': 0,
+ }
+ try:
+ for _ in range(duration*10):
+ self.set_bist_rate(rate_word)
+ self.set_bist_checker_and_gen(enable=True)
+ # Wait and check if BIST locked
+ time.sleep(0.05)
+ mst_status = self.read_mac_ctrl_status()
+ if not mst_status & self.MAC_STATUS_BIST_LOCKED_MSK:
+ results['mst_lock_errors'] += 1
+ self.log.info('lock errors: %d', results['mst_lock_errors'])
+ # Turn off the BIST generator
+ self.set_bist_gen(0)
+ # Validate status and no overruns
+ mst_status = self.read_mac_ctrl_status()
+ results['mst_overruns'] = self.read_overruns()
+ if mst_status & self.MAC_STATUS_HARD_ERR_MSK:
+ results['mst_hard_errors'] += 1
+ time.sleep(0.05)
+ self.clear_control_reg()
+ # Compute latency
+ results['latencies'].append(
+ self._mst_status_to_latency_us(mst_status)
+ )
+ except KeyboardInterrupt:
+ self.log.warning('Operation cancelled by user.')
+ stop_time = time.time()
+ # Report
+ if results['mst_lock_errors'] > 0:
+ self.log.error(
+ 'BIST engine did not lock onto a PRBS word %d times!',
+ results['mst_lock_errors']
+ )
+ if results['mst_hard_errors'] > 0:
+ self.log.error(
+ 'There were %d hard errors in master PHY',
+ results['mst_hard_errors']
+ )
+ if results['mst_overruns'] > 0:
+ self.log.error(
+ 'There were %d buffer overruns in master PHY',
+ results['mst_overruns']
+ )
+ mu_lat = mean(results['latencies'])
+ results['elapsed_time'] = stop_time - start_time
+ self.log.info('BIST Complete!')
+ self.log.info('- Elapsed Time = ' + str(results['elapsed_time']))
+ self.log.info('- Roundtrip Latency Mean = %.2fus', mu_lat)
+ self.log.info('- Roundtrip Latency Stdev = %.6fus',
+ stddev(results['latencies'], mu=mu_lat))
+ # Turn off BIST loopback
+ time.sleep(0.5)
+ if slave is not None:
+ results['sla_overruns'], results['sla_hard_errors'] = \
+ self._get_slave_status(slave)
+ self._post_test_cleanup()
+ return results
+
+ def run_ber_loopback_bist(self, duration, requested_rate, slave=None):
+ """
+ Run BER Bist. Pump lots of bits through, and see how many come back
+ correctly.
+
+ duration -- Time to run the test in seconds
+ """
+ rate_word, coerced_rate = \
+ self.get_rate_setting(requested_rate, self.bus_clk_rate)
+ self.log.info('Running BER Loopback BIST at {}MB/s for {}s...'.format(
+ coerced_rate/8e6, duration
+ ))
+ self._pre_test_init()
+ mst_overruns = 0
+ self.log.info("Starting BER test...")
+ start_time = time.time()
+ self.set_bist_rate(rate_word)
+ self.set_bist_checker_and_gen(enable=True)
+ # Wait and check if BIST locked
+ time.sleep(0.5)
+ mst_status = self.read_mac_ctrl_status()
+ if not mst_status & self.MAC_STATUS_BIST_LOCKED_MSK:
+ error_msg = 'BIST engine did not lock onto a PRBS word! ' \
+ 'MAC status word: 0x{:08X}'.format(mst_status)
+ self.log.error(error_msg)
+ raise RuntimeError(error_msg)
+ # Wait for requested time
+ try:
+ time.sleep(duration)
+ except KeyboardInterrupt:
+ self.log.warning('Operation cancelled by user.')
+ # Turn off the BIST generator and loopback
+ self.set_bist_gen(enable=False)
+ results = {}
+ results['time_elapsed'] = time.time() - start_time
+ time.sleep(0.5)
+ # Validate status and no overruns
+ mst_status = self.read_mac_ctrl_status()
+ results['mst_overruns'] = self.read_overruns()
+ results['mst_samps'] = 65536 * self.read_bist_checker_samps()
+ results['mst_errors'] = self.read_bist_checker_errors()
+ if mst_status & self.MAC_STATUS_HARD_ERR_MSK:
+ self.log.error('Hard errors in master PHY')
+ results['mst_hard_errors'] = True
+ if mst_overruns > 0:
+ self.log.error('Buffer overruns in master PHY')
+ if slave is not None:
+ results['sla_overruns'], results['sla_hard_errors'] = \
+ self._get_slave_status(slave)
+ if results['mst_samps'] != 0:
+ mst_latency_us = self._mst_status_to_latency_us(mst_status)
+ self.log.info('BIST Complete!')
+ self.log.info('- Elapsed Time = {:.2} s'.format(
+ results['time_elapsed']
+ ))
+ self.log.info('- Max BER (Bit Error Ratio) = %.4g ' \
+ '(%d errors out of %d)',
+ float(results['mst_errors']+1) / results['mst_samps'],
+ results['mst_errors'], results['mst_samps'])
+ self.log.info('- Max Roundtrip Latency = %.1fus'%mst_latency_us)
+ self.log.info('- Approx Throughput = %.0fMB/s',
+ (8e-6*results['mst_samps'])/results['time_elapsed'])
+ else:
+ self.log.error('No samples received -- BIST Failed!')
+ self._post_test_cleanup(slave)
+ return results
+
+ def _get_slave_status(self, slave):
+ """
+ Read back status from the slave
+ """
+ slave.clear_control_reg()
+ sla_status = slave.read_mac_ctrl_status()
+ sla_overruns = slave.read_overruns()
+ if sla_status & self.MAC_STATUS_HARD_ERR_MSK:
+ self.log.error('Hard errors in slave PHY')
+ sla_hard_errors = slave.read_overruns()
+ if sla_overruns > 0:
+ self.log.error('Buffer overruns in slave PHY')
+ return sla_overruns, sla_hard_errors
+
+ def _mst_status_to_latency_us(self, mst_status):
+ " Convert a MAC status word into latency in microseconds "
+ latency_cyc = 16.0 * \
+ ((mst_status & self.MAC_STATUS_BIST_LATENCY_MSK) \
+ >> self.MAC_STATUS_BIST_LATENCY_OFFSET)
+ return 1e6 * latency_cyc / self.bus_clk_rate
+
+ def _pre_test_init(self, slave=None):
+ " Set up core(s) for BISTing "
+ self.reset_core()
+ if slave is not None:
+ slave.reset_core()
+ time.sleep(1.5)
+ if slave is not None:
+ self.set_loopback(enable=False)
+ slave.set_loopback(enable=True)
+ self.log.debug("Status of PHY link: 0x{:08X}".format(
+ self.read_phy_ctrl_status()
+ ))
+ if not self.is_phy_link_up():
+ raise RuntimeError("PHY link not up. Check connectors.")
+
+ def _post_test_cleanup(self, slave=None):
+ " Drain and Cleanup "
+ self.log.info('Cleaning up...')
+ self.set_bist_checker(enable=True)
+ if slave is not None:
+ slave.set_bist_checker(enable=True)
+ time.sleep(0.5)
+ self.clear_control_reg()
+ if slave is not None:
+ slave.clear_control_reg()
+