diff options
-rw-r--r-- | mpm/python/CMakeLists.txt | 6 | ||||
-rw-r--r-- | mpm/python/aurora_bist_test.py | 105 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/CMakeLists.txt | 1 | ||||
-rw-r--r-- | mpm/python/usrp_mpm/aurora_control.py | 418 |
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() + |