diff --git a/host/examples/python/benchmark_rate.py b/host/examples/python/benchmark_rate.py index 88741b16f..555e7648c 100755 --- a/host/examples/python/benchmark_rate.py +++ b/host/examples/python/benchmark_rate.py @@ -1,11 +1,52 @@ #!/usr/bin/env python3 # -# Copyright 2018 Ettus Research, a National Instruments Company -# Copyright 2019 Ettus Research, a National Instruments Brand +# Copyright 2025 Ettus Research, a National Instruments Brand # # SPDX-License-Identifier: GPL-3.0-or-later # -"""Benchmark rate using Python API.""" +"""Benchmark rate using Python API. + +This script benchmarks the receive and transmit rates of a USRP device using +the UHD Python API. It allows users to specify the device address, sample rate, +subdevice specifications, reference clock source, and other parameters for +the benchmarking process. The script can perform receive-only, transmit-only, +or full-duplex tests, and it provides detailed statistics on the number of +samples received, transmitted, dropped, and any errors encountered during +the process. The results are printed in a formatted summary at the end of +the test. + +This script is useful for evaluating I/O performance between host PC and USRP +device. It is helpful for troubleshooting and optimizing network performance. +It can be used in research, development, and testing scenarios where accurate +measurement of data rates and error rates is critical. + +Example Usage: + # Receive-only test at 10 Msps for 5 seconds + python3 benchmark_rate.py --rx_rate 10e6 --duration 5 + + # Transmit-only test at 5 Msps for 10 seconds + python3 benchmark_rate.py --tx_rate 5e6 --duration 10 + + # Full-duplex test with RX at 10 Msps and TX at 5 Msps + python3 benchmark_rate.py --rx_rate 10e6 --tx_rate 5e6 --duration 15 + + # Specify device arguments and subdevice + python3 benchmark_rate.py -a addr=192.168.10.2 --rx_subdev "A:0" --tx_subdev "A:0" + + # Use external clock reference and PPS + python3 benchmark_rate.py --ref external --pps external --rx_rate 10e6 --tx_rate 5e6 + +Note 1: Refer https://files.ettus.com/manual/structuhd_1_1stream__args__t.html +to see the list of available over-the-wire (--rx_otw, --tx_otw) and CPU +(--rx_cpu, --tx_cpu) sample modes. + +Note 2: --tx_channels and --rx_channels are used to specify the TX and RX channels +respectively. Or one can also use argument --channels to specify both TX and RX +channels together. Note that if both --rx_channels/--tx_channels and --channels +are specified than preference is always given to --rx_channels/--tx_channels. +The argument--channels is only used as a fallback if --rx_channels/--tx_channels +are not mentioned. +""" import argparse import logging @@ -22,47 +63,99 @@ INIT_DELAY = 0.05 # 50mS initial delay before transmit def parse_args(): - """Parse the command line arguments.""" - description = """UHD Benchmark Rate (Python API) + """Parse the command line arguments. - Utility to stress test a USRP device. + UHD Benchmark Rate (Python API). + Utility to benchmark the rates at which the USRP and the host (external + CPU, embedded ARM) can receive and transmit without loosing any data. - Specify --rx_rate for a receive-only test. - Specify --tx_rate for a transmit-only test. - Specify both options for a full-duplex test. - """ + Specify --rx_rate for a receive-only test. + Specify --tx_rate for a transmit-only test. + Specify both options for a full-duplex test. + """ parser = argparse.ArgumentParser( - formatter_class=argparse.RawTextHelpFormatter, description=description - ) - parser.add_argument("-a", "--args", default="", type=str, help="single uhd device address args") - parser.add_argument( - "-d", "--duration", default=10.0, type=float, help="duration for the test in seconds" - ) - parser.add_argument("--rx_subdev", type=str, help="specify the device subdev for RX") - parser.add_argument("--tx_subdev", type=str, help="specify the device subdev for TX") - parser.add_argument("--rx_rate", type=float, help="specify to perform a RX rate test (sps)") - parser.add_argument("--tx_rate", type=float, help="specify to perform a TX rate test (sps)") - parser.add_argument( - "--rx_otw", type=str, default="sc16", help="specify the over-the-wire sample mode for RX" + formatter_class=argparse.RawDescriptionHelpFormatter, description=__doc__ ) parser.add_argument( - "--tx_otw", type=str, default="sc16", help="specify the over-the-wire sample mode for TX" + "-a", + "--args", + default="", + type=str, + help="""specifies the USRP device arguments, which holds + multiple key value pairs separated by commas + (e.g., addr=192.168.40.2,type=x300) [default = ""].""", ) parser.add_argument( - "--rx_cpu", type=str, default="fc32", help="specify the host/cpu sample mode for RX" + "-d", + "--duration", + default=10.0, + type=float, + help="specify the duration for the test in seconds [default = 10.0].", ) parser.add_argument( - "--tx_cpu", type=str, default="fc32", help="specify the host/cpu sample mode for TX" + "--rx_subdev", type=str, help="specify the subdevice for RX [default = None]." + ) + parser.add_argument( + "--tx_subdev", type=str, help="specify the subdevice for TX [default = None]." + ) + parser.add_argument( + "--rx_rate", type=float, help="specify to perform a RX rate test (samples/sec)." + ) + parser.add_argument( + "--tx_rate", type=float, help="specify to perform a TX rate test (samples/sec)." + ) + parser.add_argument( + "--rx_otw", + type=str, + default="sc16", + help="specify the over-the-wire sample mode for RX [default= sc16].", + ) + parser.add_argument( + "--tx_otw", + type=str, + default="sc16", + help="specify the over-the-wire sample mode for TX [default = sc16].", + ) + parser.add_argument( + "--rx_cpu", + type=str, + default="fc32", + help="specify the host/cpu sample mode for RX [default = fc32].", + ) + parser.add_argument( + "--tx_cpu", + type=str, + default="fc32", + help="specify the host/cpu sample mode for TX [default =fc32].", + ) + parser.add_argument( + "--rx_stream_args", + default="", + help="specify the additional stream arguments for the RX streamer " + "(e.g., samples per packet).", + ) + parser.add_argument( + "--tx_stream_args", + default="", + help="specify the additional stream arguments for the TX streamer " + "(e.g., samples per packet).", + ) + parser.add_argument( + "--ref", + type=str, + help="specify the reference clock source (internal, external, mimo, gpsdo).", + ) + parser.add_argument( + "--pps", type=str, help="specify the PPS source (internal, external, mimo, gpsdo)." ) - parser.add_argument("--rx_stream_args", help="stream args for RX streamer", default="") - parser.add_argument("--tx_stream_args", help="stream args for TX streamer", default="") - parser.add_argument("--ref", type=str, help="clock reference (internal, external, mimo, gpsdo)") - parser.add_argument("--pps", type=str, help="PPS source (internal, external, mimo, gpsdo)") parser.add_argument( "--random", action="store_true", default=False, - help="Run with random values of samples in send() and recv() to stress-test" " the I/O.", + help="specify whether to run the test with random packet size. If set to true, " + "a random number of samples ranging from 1 to maximum samples per packet is " + "used per send() and recv(). If set to false, the number of samples will be " + "set to maximum samples per packet.", ) parser.add_argument( "-c", @@ -70,19 +163,20 @@ def parse_args(): default=[0], nargs="+", type=int, - help='which channel(s) to use (specify "0", "1", "0 1", etc)', + help='specify the channel(s) to use as both RX and TX (e.g., "0", "1", "0 1", etc) ' + "Refer Note 2 above on how --channels related to --rx_channel/--tx_channels [default = 0].", ) parser.add_argument( "--rx_channels", nargs="+", type=int, - help='which RX channel(s) to use (specify "0", "1", "0 1", etc)', + help='specify the RX channel(s) to use (e.g., "0", "1", "0 1", etc) [default = None].', ) parser.add_argument( "--tx_channels", nargs="+", type=int, - help='which TX channel(s) to use (specify "0", "1", "0 1", etc)', + help='specify the TX channel(s) to use (e.g., "0", "1", "0 1", etc) [default = None].', ) return parser.parse_args() diff --git a/host/examples/python/curses_fft.py b/host/examples/python/curses_fft.py index f435def79..6d6c22f03 100755 --- a/host/examples/python/curses_fft.py +++ b/host/examples/python/curses_fft.py @@ -1,35 +1,102 @@ #!/usr/bin/env python3 # -# Copyright 2017-2018 Ettus Research, a National Instruments Company +# Copyright 2025 Ettus Research, a National Instruments Brand # # SPDX-License-Identifier: GPL-3.0-or-later # -""" -Curses FFT example using Python API +"""Curses FFT example using Python API. + +This example demonstrates how to use the UHD Python API to create a simple +FFT (Fast Fourier Transform) display using the curses library. The script +receives samples from a USRP device and displays the instantaneous estimate of +the power spectral density of the received signal on the terminal window. The +display is updated in real-time as new samples are received. The user can +adjust parameters such as frequency, gain, and sample rate through command-line +arguments. This example is useful for visualizing the frequency spectrum of +signals received by the USRP device, allowing users to monitor and analyze the +signal characteristics in a user-friendly manner. + +Example Usage: +curses_fft.py --args addr=192.168.10.2 --freq 2.4e9 --rate 1e6 --gain 10 + --nsamps 100000 --dyn 60 """ import argparse import curses as cs + import numpy as np import uhd def parse_args(): - """Parse the command line arguments""" - parser = argparse.ArgumentParser() - parser.add_argument("-a", "--args", default="", type=str) - parser.add_argument("-f", "--freq", type=float, required=True) - parser.add_argument("-r", "--rate", default=1e6, type=float) - parser.add_argument("-g", "--gain", type=int, default=10) - parser.add_argument("-c", "--channel", type=int, default=0) - parser.add_argument("-n", "--nsamps", type=int, default=100000) - parser.add_argument("--dyn", type=int, default=60) - parser.add_argument("--ref", type=int, default=0) + """Parse the command line arguments.""" + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__, + ) + parser.add_argument( + "-a", + "--args", + default="", + type=str, + help="""specifies the USRP device arguments, which holds + multiple key value pairs separated by commas + (e.g., addr=192.168.40.2,type=x300) [default = ""].""", + ) + parser.add_argument( + "-f", + "--freq", + type=float, + required=True, + help="specifies the center frequency in Hz [default = 1e4].", + ) + parser.add_argument( + "-r", + "--rate", + default=1e6, + type=float, + help="specifies the sample rate in samples/sec [default = 1e6].", + ) + parser.add_argument( + "-g", "--gain", type=int, default=10, help="specifies the gain in dB [default = 10]." + ) + parser.add_argument( + "-c", + "--channel", + type=int, + default=0, + help='specifies the channels to use (e.g., "0", "1", "0 1", etc) [Default = 0]', + ) + parser.add_argument( + "-n", + "--nsamps", + type=int, + default=100000, + help="specifies the total number of samples to be received for FFT calculation " + "and display on the screen before refereshing [default = 100000].", + ) + parser.add_argument( + "--dyn", + type=int, + default=60, + help="specifies the dynamic range in dB. This " + "defines the range of power levels relative to the reference level. Adjusting " + "dynamic range influences the resolution displayed on y-axis of the plot [default = 60].", + ) + parser.add_argument( + "--ref", + type=int, + default=0, + help="specifies the reference level in dB. " + "This represent the maximum power level displayed on the plot. All power levels " + " are relative to reference level Adjusting the reference level allows to " + "focus on specific power ranges in the signal [default = 0].", + ) return parser.parse_args() def psd(nfft, samples): - """Return the power spectral density of `samples`""" + """Return the power spectral density of `samples`.""" window = np.hamming(nfft) result = np.multiply(window, samples) result = np.fft.fftshift(np.fft.fft(result, nfft)) @@ -40,12 +107,12 @@ def psd(nfft, samples): def clip(minval, maxval, value): - """Clip the value between a and b""" + """Clip the value between a and b.""" return min(minval, max(maxval, value)) def main(): - """Create Curses display of FFT""" + """Create Curses display of FFT.""" args = parse_args() usrp = uhd.usrp.MultiUSRP(args.args) @@ -120,17 +187,17 @@ def main(): print(metadata.strerror()) if samps: real_samps = min(num_samps - recv_samps, samps) - samples[:, recv_samps:recv_samps + real_samps] = recv_buffer[:, 0:real_samps] + samples[:, recv_samps : recv_samps + real_samps] = recv_buffer[:, 0:real_samps] recv_samps += real_samps # Get the power in each bin bins = psd(width, samples[args.channel][0:width]) for i in range(y_axis_width, width): - vertical_slot = clip(height, 0, np.int(bins[i] / db_step)) + vertical_slot = clip(height, 0, np.int32(bins[i] / db_step)) try: for j in range(vertical_slot, height): - screen.addch(j, i, '*') + screen.addch(j, i, "*") except cs.error: pass screen.refresh() diff --git a/host/examples/python/fft_loopback.py b/host/examples/python/fft_loopback.py index 9d75e3f39..5063204af 100755 --- a/host/examples/python/fft_loopback.py +++ b/host/examples/python/fft_loopback.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2024 Ettus Research, a National Instruments Brand +# Copyright 2025 Ettus Research, a National Instruments Brand # # SPDX-License-Identifier: GPL-3.0-or-later # @@ -20,9 +20,10 @@ The data flow is as follows: │ └─> Radio Rx ─> DDC ─> OFDM Rx ─> Host Rx Streamer -It assumes the RFNoC topology like the one shown below. The configuration of the -SFPs may be 10 GbE (1 to 4 ports) or 100 GbE. The number of channels per radio -and DDC/DUC pair can be 1 or 2, with the DDC/DUC being optional. The following +It assumes the RFNoC topology shown below. Make sure that the bitfile used has +an FFT block instantiated as shown below. The configuration of the SFPs may +be 10 GbE (1 to 4 ports) or 100 GbE. The number of channels per radio and +DDC/DUC pair can be 1 or 2, with the DDC/DUC being optional. The following diagram shows one channel per radio for simplicity. ┌────────────┐ Ch0 ┌─────────┐ ┌─────────┐ ┌─────────┐ @@ -51,14 +52,18 @@ diagram shows one channel per radio for simplicity. │ │<─────────────┤ │<────┤ │<────┤ │ └────────────┘ └─────────┘ └─────────┘ └─────────┘ +Example usage: +fft_loopback.py --args addr=192.168.10.2 --fft-length 1024 --num-symbols 10 + --freq 915e6 --amplitude 0.6 --cp-list 352 288 --channels 0,1 + --tx-gain 25 --rx-gain 45 --rate 10e6 --delay 5 """ import argparse +import math import matplotlib.pyplot as plt import numpy as np import uhd -import math from matplotlib.widgets import Slider graph_connections = { @@ -105,28 +110,30 @@ def parse_args(): "--args", type=str, default="", - help="Device args (e.g., addr=192.168.10.2)", + help="""specifies the USRP device arguments, which holds + multiple key value pairs separated by commas + (e.g., addr=192.168.40.2,type=x300) [default = ""].""", ) parser.add_argument( "-s", "--fft-length", type=int, default=4096, - help="FFT length (default = 4096)", + help="specifies the FFT length [default = 4096].", ) parser.add_argument( "-n", "--num-symbols", type=int, default=14, - help="Number of symbols to transmit/receive (default=14)", + help="specifies the number of symbols to transmit/receive [default = 14].", ) parser.add_argument( "-y", "--amplitude", type=float, default=0.5, - help="Amplitude of the tone", + help="specifies the amplitude of the tone that is being transmitted [default = 0.5].", ) parser.add_argument( "-p", @@ -134,7 +141,7 @@ def parse_args(): type=int, nargs="+", default=[], - help="List of cyclic prefix lengths to use (default=[])", + help="specifies the list of cyclic prefix lengths to use [default = []].", ) parser.add_argument( "-c", @@ -142,45 +149,45 @@ def parse_args(): type=str, choices=["0", "1", "2", "3", "0,1", "2,3", "0,1,2,3"], default="0,1", - help="The channels to use (default=0,1)", + help="specifies the channels to use [default = 0,1].", ) parser.add_argument( "-l", "--loopback", action="store_true", - help="Use internal loopback", + help="specifies to loop the transmit signal back to FPGA internally within the radio block.", ) parser.add_argument( "-d", "--delay", type=int, default=None, - help="The number of cycles to delay RX vs. TX when streaming", + help="specifies the number of FPGA clock cycles to delay RX vs. TX when streaming [default = None].", ) parser.add_argument( "--tx-gain", type=int, default=15, - help="The TX gain (default=15)", + help="specifies the TX gain in dB [default = 15].", ) parser.add_argument( "--rx-gain", type=int, default=50, - help="The RX gain (default=50)", + help="specifies the RX gain in dB [default = 50].", ) parser.add_argument( "-r", "--rate", type=float, - help="The TX/RX rate of host data (default is master clock rate)", + help="specifies the TX/RX sample rate in samples/sec of host data [default = master clock rate].", ) parser.add_argument( "-f", "--freq", type=float, default=1.5e9, - help="The TX/RX radio frequency (default is 1.5 GHz)", + help="specifies the TX/RX radio center frequency in Hz [default = 1.5 GHz]", ) return parser.parse_args() @@ -297,7 +304,7 @@ def print_fft_block_info(ofdm): def default_scaling(fft_length): - """Return the scaling value for the default 1/N scaling""" + """Return the scaling value for the default 1/N scaling.""" # 1/N scaling should be 1010...10 if fft_length is a power of 4 and 0110...10 # if fft_length is not a power of 4. # diff --git a/host/examples/python/remote_rx.py b/host/examples/python/remote_rx.py index fb709f11e..070133a43 100755 --- a/host/examples/python/remote_rx.py +++ b/host/examples/python/remote_rx.py @@ -1,53 +1,128 @@ #!/usr/bin/env python3 # -# Copyright 2022 Ettus Research, a National Instruments Brand +# Copyright 2025 Ettus Research, a National Instruments Brand # # SPDX-License-Identifier: GPL-3.0-or-later # -""" -Stream IQ data from a USRP to a remote destination. +"""Stream IQ data from a USRP to a remote destination. + +The script is designed to stream IQ (In-phase and Quadrature) data from a +USRP device (connected to a host computer) to a remote destination over a +network. The remote destination can be different from the host computer. +The example allows users to configure various parameters such as frequency, +sampling rate, gain, and destination details for the streaming process. +Data is streamed until the configured duration expires or the user interrupts +the process (Ctrl-C). + +This script is useful for applications where IQ data needs to be streamed +from a USRP device to a remote server or application for further processing, +such as signal analysis, demodulation, or storage. It is particularly suited +for networked SDR setups. + +The hardware setup includes +- a USRP device +- a host computer connected to the USRP device +- a remote destination (IP address and port) where the IQ data will be sent +- a network connection between the host computer, USRP device and the remote +destination + +Example Usage: +remote_rx.py --args addr=192.168.10.2 --rate 1e6 --freq 2.4e9 --gain 10 + --duration 10 --channels 0 1 --dest-addr 192.168.10.100 + --dest-port 12345 12346 """ -import sys import argparse +import sys import time + import uhd INIT_DELAY = 0.05 # 50mS initial delay before receive + def parse_args(): - """Parse the command line arguments""" - parser = argparse.ArgumentParser() - parser.add_argument("-a", "--args", default="", - help="Device args (e.g., addr=192.168.40.2)") - parser.add_argument("-r", "--rate", type=float, - help="Output rate (samples/sec)") - parser.add_argument("-f", "--freq", type=float, required=True, - help="Center frequency") - parser.add_argument("-g", "--gain", type=float, default=0.0, - help="Gain (dB)") - parser.add_argument("-d", "--duration", default=None, type=float, - help="Stream duration (seconds), leave out to stream until stopped") - parser.add_argument("-c", "--channels", default=[0], nargs="+", type=int, - help="Which channel(s) to use (specify \"0\", \"1\", \"0 1\", etc)") - parser.add_argument("-i", "--dest-addr", type=str, required=True, - help="Remote destination IP address") - parser.add_argument("-p", "--dest-port", nargs="+", type=int, required=True, - help="Remote destination UDP port") - parser.add_argument("--adapter", type=str, - help="Adapter to use for remote streaming (e.g. 'sfp0')") - parser.add_argument("--dest-mac-addr", - help="Manually provide destination MAC address in the " - "format 01:a2:4f:6d:7e:5f. By default, the device will " - "use ARP to identify the MAC address.") - parser.add_argument("--keep-hdr", action="store_true", - help="Specify this argument to keep CHDR headers on outgoing packets") + """Parse the command line arguments.""" + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__, + ) + parser.add_argument( + "-a", + "--args", + default="", + help="""specifies the USRP device arguments, which holds + multiple key value pairs separated by commas + (e.g., addr=192.168.40.2,type=x300) [default = ""].""", + ) + parser.add_argument( + "-r", + "--rate", + type=float, + default=1e6, + help="Specifies the sample rate in samples/sec [default = 1e6].", + ) + parser.add_argument( + "-f", + "--freq", + type=float, + required=True, + help="Specifies the center frequency in Hz [input is required].", + ) + parser.add_argument( + "-g", "--gain", type=float, default=0.0, help="Specifies the RX gain in dB [default = 0.0]." + ) + parser.add_argument( + "-d", + "--duration", + default=None, + type=float, + help="Specifies the stream duration in seconds, leave out to stream until stopped [default = None].", + ) + parser.add_argument( + "-c", + "--channels", + default=[0], + nargs="+", + type=int, + help='Specifies the RX channel(s) to use (specify "0", "1", "0 1", etc) [default = 0].', + ) + parser.add_argument( + "-i", + "--dest-addr", + type=str, + required=True, + help="Specifies the remote destination IP address [input is required].", + ) + parser.add_argument( + "-p", + "--dest-port", + nargs="+", + type=int, + required=True, + help="Specifies the remote destination UDP port [input is required].", + ) + parser.add_argument( + "--adapter", + type=str, + help="Specifies the adapter to use for remote streaming (e.g. 'sfp0').", + ) + parser.add_argument( + "--dest-mac-addr", + help="Specifies the destination MAC address in the format 01:a2:4f:6d:7e:5f. " + "If this argument is not used, the USRP device will use ARP to identify the MAC address.", + ) + parser.add_argument( + "--keep-hdr", + action="store_true", + help="Specify this argument to keep CHDR headers on outgoing packets. If not specified, " + "the headers will be stripped from the packets.", + ) return parser.parse_args() -def get_stream_cmd(usrp, rate, duration, start_time = None): - """ - Generate a stream command based on rate and duration. - """ + +def get_stream_cmd(usrp, rate, duration, start_time=None): + """Generate a stream command based on rate and duration.""" if duration: stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.num_done) stream_cmd.num_samps = int(rate * duration) @@ -60,8 +135,9 @@ def get_stream_cmd(usrp, rate, duration, start_time = None): stream_cmd.time_spec = start_time return stream_cmd + def check_channels(usrp, args): - """Check that the device has sufficient RX channels available""" + """Check that the device has sufficient RX channels available.""" # Check that each channel specified is less than the number of total number of rx channels # the device can support channels = args.channels @@ -71,10 +147,9 @@ def check_channels(usrp, args): return [] return channels + def main(): - """ - Go, go, go! - """ + """Run remote Rx.""" args = parse_args() usrp = uhd.usrp.MultiUSRP(args.args) channels = check_channels(usrp, args) @@ -83,7 +158,7 @@ def main(): if len(channels) != len(args.dest_port): print("Number of channels must match number of ports.") return False - print("Selected RX channels: {}.".format(', '.join(str(ch) for ch in channels))) + print("Selected RX channels: {}.".format(", ".join(str(ch) for ch in channels))) if args.rate: print(f"Requesting sampling rate {args.rate/1e6} Msps...") for chan in channels: @@ -103,14 +178,19 @@ def main(): for chx, chan in enumerate(channels): stream_args = uhd.usrp.StreamArgs("sc16", "sc16") stream_args.channels = [chan] - stream_args.args = \ - f"dest_addr={args.dest_addr},dest_port={args.dest_port[chx]}," \ - f"stream_mode={'full_packet' if args.keep_hdr else 'raw_payload'}" + \ - (f",adapter={args.adapter}" if args.adapter else "") + \ - (f",dest_mac_addr={args.dest_mac_addr}" if args.dest_mac_addr else "") + stream_args.args = ( + f"dest_addr={args.dest_addr},dest_port={args.dest_port[chx]}," + f"stream_mode={'full_packet' if args.keep_hdr else 'raw_payload'}" + + (f",adapter={args.adapter}" if args.adapter else "") + + (f",dest_mac_addr={args.dest_mac_addr}" if args.dest_mac_addr else "") + ) rx_streamers.append(usrp.get_rx_stream(stream_args)) print("Starting stream(s)...") - start_time = uhd.types.TimeSpec(usrp.get_time_now().get_real_secs() + INIT_DELAY) if len(channels) > 1 else None + start_time = ( + uhd.types.TimeSpec(usrp.get_time_now().get_real_secs() + INIT_DELAY) + if len(channels) > 1 + else None + ) for rx_streamer in rx_streamers: stream_cmd = get_stream_cmd(usrp, actual_rate, args.duration, start_time) rx_streamer.issue_stream_cmd(stream_cmd) @@ -129,5 +209,6 @@ def main(): print("Streaming complete. Exiting.") return 0 + if __name__ == "__main__": sys.exit(main()) diff --git a/host/examples/python/replay_capture.py b/host/examples/python/replay_capture.py index 336477772..61d648569 100755 --- a/host/examples/python/replay_capture.py +++ b/host/examples/python/replay_capture.py @@ -1,6 +1,13 @@ #!/usr/bin/env python3 -""" -Captures samples into DRAM, then streams those to the host. +# +# Copyright 2025 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +"""Captures samples into DRAM, then streams those to the host. + +The example uses a single replay block to capture samples from multiple radios +and stores data in a file on the host. Note: The --freq, --gain, and --antenna options can take a single value, which is applied to all channels, or a list of values which is applied to individual @@ -20,18 +27,31 @@ If this happens, the following options may help: by adapting network driver settings (see also https://kb.ettus.com/USRP_Host_Performance_Tuning_Tips_and_Tricks#Increasing_Ring_Buffers) - Fall back to a slower data rate, e.g., use 1 GbE instead of 10 or 100 GbE + +Example usage: +replay_capture.py --args addr=192.168.10.2,type=x300 + --output-file captured_samples.dat --rate 10e6 --freq 1e9 1.1e9 + --gain 20 30 --antenna TX/RX RX2 --duration 5 + --radio-channels "0/Radio#0:0" "0/Radio#0:1" --block "0/Replay#0" + --pkt-size 1472 --numpy + +Note: The argument --output-file specifies the file path to store received data +and needs to point to a location where the user has write access. """ -import os -import sys -import time -import shutil -import tempfile import argparse +import os +import shutil +import sys +import tempfile +import time + import numpy as np import uhd + try: import tqdm + HAVE_TQDM = True except ImportError: HAVE_TQDM = False @@ -42,53 +62,126 @@ BYTES_PER_SAMP = 4 # pylint: disable=too-many-arguments def parse_args(): - """ - Return parsed command line args - """ + """Return parsed command line args.""" parser = argparse.ArgumentParser( - description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument("--args", "-a", type=str, default="", - help="Device args to use when connecting to the USRP.") - parser.add_argument("-o", "--output-file", required=True, - help="Where to store the samples.") - parser.add_argument("-r", "--rate", type=float, - help="Sampling Rate (defaults to maximum rate)") - parser.add_argument("-f", "--freq", type=float, required=True, nargs="+", - help="Center frequency") - parser.add_argument("-g", "--gain", type=int, default=[10], nargs="+", help="Gain (dB)") - parser.add_argument("--antenna", help="Antenna", nargs="+") - parser.add_argument("-d", "--duration", type=float, - help="Duration in seconds to capture. " - "Default behavior is to fill the DRAM.") - parser.add_argument("--delay", "-l", type=float, default=0.5, - help="Capture delay in seconds") - parser.add_argument("-c", "--radio-channels", default=["0/Radio#0:0"], nargs="+", - help="List radios plus their channels " - "(defaults to \"0/Radio#0:0\")") - parser.add_argument("--block", "-b", type=str, default="0/Replay#0", - help="Replay block to test. Defaults to \"0/Replay#0\".") - parser.add_argument("--pkt-size", "-k", type=int, default=None, - help="Playback packet size in bytes. " - "Defaults to maximum packet size for this transport") - parser.add_argument("-n", "--numpy", default=False, action="store_true", - help="Save output file in NumPy format (default: No)") - parser.add_argument("--cpu-format", default="sc16", choices=["sc16", "fc32"], - help="Data format for storing data") - parser.add_argument("--throttle", type=float, default=0.25, - help="Throttle for streaming to host, range (0, 1] (default: 0.25)") - parser.add_argument("--no-progress", "-N", action="store_true", - help="Do not display progress bar during data transfer") + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "--args", + "-a", + type=str, + default="", + help="""specifies the USRP device arguments, which holds + multiple key value pairs separated by commas + (e.g., addr=192.168.40.2,type=x300) [default = ""].""", + ) + parser.add_argument( + "-o", + "--output-file", + required=True, + help="specifies the file path to store the captured samples [input is required].", + ) + parser.add_argument( + "-r", + "--rate", + type=float, + help="specifies the sample rate in samples/sec [default = MCR].", + ) + parser.add_argument( + "-f", + "--freq", + type=float, + required=True, + nargs="+", + help="specifies the center frequency in Hz for each channel [input is required].", + ) + parser.add_argument( + "-g", + "--gain", + type=int, + default=[10], + nargs="+", + help="specifies the receive gain in dB for each channel [default = 10].", + ) + parser.add_argument( + "--antenna", + type=str, + nargs="+", + help="specifies the antenna to use for each channel " "(e.g., TX/RX TX2) [default = None].", + ) + parser.add_argument( + "-d", + "--duration", + type=float, + help="specifies the duration of the capture in seconds " + "[default = sets the duration such that DRAM completely fills].", + ) + parser.add_argument( + "--delay", + "-l", + type=float, + default=0.5, + help="specifies the delay in seconds between issuing a capture command " + "on the host and starting the capture [default = 0.5].", + ) + parser.add_argument( + "-c", + "--radio-channels", + default=["0/Radio#0:0"], + nargs="+", + help="specifies the list of radios plus their channels " '[default = "0/Radio#0:0"].', + ) + parser.add_argument( + "--block", + "-b", + type=str, + default="0/Replay#0", + help='specifies the Replay block to test [default = "0/Replay#0"].', + ) + parser.add_argument( + "--pkt-size", + "-k", + type=int, + default=None, + help="specifies the CHDR packet size in bytes to stream data to the host " + "[default = maximum CHDR packet size for this transport layer].", + ) + parser.add_argument( + "-n", + "--numpy", + default=False, + action="store_true", + help="specifies whether to save the output file in NumPy format. If --numpy " + "is not specified, the output file will be saved in binary format.", + ) + parser.add_argument( + "--cpu-format", + default="sc16", + choices=["sc16", "fc32"], + help="specifies the cpu format for storing data [default = sc16].", + ) + parser.add_argument( + "--throttle", + type=float, + default=0.25, + help="specifies the throttle for streaming to host in the range of (0, 1]. " + "E.g., use 1 for maximum rate, use 0.5 for half the maximum rate, etc. " + "Note that other factors may affect the actual rate, such as the rate of " + "the source or the speed supported by the transport [default = 0.25].", + ) + parser.add_argument( + "--no-progress", + "-N", + action="store_true", + help="specifies whether to display progress bar or not during data transfer.", + ) return parser.parse_args() + def enumerate_radios(graph, radio_chans): - """ - Return a list of radio/chan pairs to use for this test. - """ + """Return a list of radio/chain pairs to use for this test.""" radio_id_chan_pairs = [ - (r.split(':', 2)[0], int(r.split(':', 2)[1])) - if ':' in r else (r, 0) - for r in radio_chans + (r.split(":", 2)[0], int(r.split(":", 2)[1])) if ":" in r else (r, 0) for r in radio_chans ] # Sanity checks available_radios = graph.find_blocks("Radio") @@ -96,14 +189,12 @@ def enumerate_radios(graph, radio_chans): for rcp in radio_id_chan_pairs: if rcp[0] not in available_radios: raise RuntimeError(f"'{rcp[0]}' is not a valid radio block ID!") - radio_chan_pairs.append( - (uhd.rfnoc.RadioControl(graph.get_block(rcp[0])), rcp[1])) + radio_chan_pairs.append((uhd.rfnoc.RadioControl(graph.get_block(rcp[0])), rcp[1])) return radio_chan_pairs + def connect_radios(graph, replay, radio_chan_pairs, freqs, gains, antennas, rate): - """ - Set up the replay/radio part of the graph, and configure radios - """ + """Set up the replay/radio part of the graph, and configure radios.""" if rate is None: rate = radio_chan_pairs[0][0].get_rate() print(f"Requested rate: {rate/1e6:.2f} Msps") @@ -112,27 +203,33 @@ def connect_radios(graph, replay, radio_chan_pairs, freqs, gains, antennas, rate radio, chan = rcp print( f"Connecting {rcp[0].get_unique_id()}:{rcp[1]} to " - f"{replay.get_unique_id()}:{replay_port_idx}") + f"{replay.get_unique_id()}:{replay_port_idx}" + ) radio.set_rx_frequency(freqs[replay_port_idx % len(freqs)], rcp[1]) radio.set_rx_gain(gains[replay_port_idx % len(gains)], rcp[1]) if antennas is not None: radio.set_rx_antenna(antennas[replay_port_idx % len(antennas)], rcp[1]) - print(f"--> Radio settings: fc={radio.get_rx_frequency(chan)/1e6:.2f} MHz, " - f" gain={radio.get_rx_gain(chan)} dB, " - f"antenna={radio.get_rx_antenna(chan)}") + print( + f"--> Radio settings: fc={radio.get_rx_frequency(chan)/1e6:.2f} MHz, " + f" gain={radio.get_rx_gain(chan)} dB, " + f"antenna={radio.get_rx_antenna(chan)}" + ) radio_to_replay_graph = uhd.rfnoc.connect_through_blocks( - graph, - rcp[0].get_unique_id(), rcp[1], - replay.get_unique_id(), replay_port_idx) - ddc_block = next(( - (x.dst_blockid, x.dst_port) - for x in radio_to_replay_graph - if uhd.rfnoc.BlockID(x.dst_blockid).get_block_name() == 'DDC' - ), None) + graph, rcp[0].get_unique_id(), rcp[1], replay.get_unique_id(), replay_port_idx + ) + ddc_block = next( + ( + (x.dst_blockid, x.dst_port) + for x in radio_to_replay_graph + if uhd.rfnoc.BlockID(x.dst_blockid).get_block_name() == "DDC" + ), + None, + ) if ddc_block is not None: print(f"Found DDC block on channel {chan}.") - this_rate = uhd.rfnoc.DdcBlockControl( - graph.get_block(ddc_block[0])).set_output_rate(rate, rcp[1]) + this_rate = uhd.rfnoc.DdcBlockControl(graph.get_block(ddc_block[0])).set_output_rate( + rate, rcp[1] + ) else: this_rate = rcp[0].set_rate(rate) if actual_rate is None: @@ -144,9 +241,7 @@ def connect_radios(graph, replay, radio_chan_pairs, freqs, gains, antennas, rate def _sanitize_args(replay, num_ports, num_bytes, pkt_size_bytes=None): - """ - Sanitize requested args based on the capabilities of the replay block - """ + """Sanitize requested args based on the capabilities of the replay block.""" assert num_ports <= replay.get_num_input_ports() ## Figure out how many bytes to send mem_size = replay.get_mem_size() @@ -157,11 +252,15 @@ def _sanitize_args(replay, num_ports, num_bytes, pkt_size_bytes=None): print(f"Requested Record size per port: {num_bytes // 1024 // 1024} MiB") if num_bytes > mem_size // num_ports: num_bytes = mem_size // num_ports - print(f"WARNING: Exceeds allocated space per port! " - f"Reducing to {num_bytes // 1024 // 1024} MiB") + print( + f"WARNING: Exceeds allocated space per port! " + f"Reducing to {num_bytes // 1024 // 1024} MiB" + ) for port in range(num_ports): - print(f"Port {port} address space: " - f"0x{mem_stride*port:08X} - 0x{mem_stride*port+num_bytes:08X}") + print( + f"Port {port} address space: " + f"0x{mem_stride*port:08X} - 0x{mem_stride*port+num_bytes:08X}" + ) replay.set_play_type("sc16", 0) replay.set_record_type("sc16", 0) if pkt_size_bytes is not None: @@ -169,11 +268,8 @@ def _sanitize_args(replay, num_ports, num_bytes, pkt_size_bytes=None): return mem_stride, num_bytes -def run_capture(graph, replay, radio_chan_pairs, mem_stride, num_samps, rate, - cap_delay): - """ - Record from radio into DRAM - """ +def run_capture(graph, replay, radio_chan_pairs, mem_stride, num_samps, rate, cap_delay): + """Record from radio into DRAM.""" num_bytes = num_samps * BYTES_PER_SAMP num_ports = len(radio_chan_pairs) ## Arm replay block for recording @@ -186,9 +282,9 @@ def run_capture(graph, replay, radio_chan_pairs, mem_stride, num_samps, rate, stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.num_done) stream_cmd.num_samps = num_samps * rate_ratio stream_cmd.stream_now = False - stream_cmd.time_spec = \ - graph.get_mb_controller().get_timekeeper(0).get_time_now() + \ - uhd.types.TimeSpec(cap_delay) + stream_cmd.time_spec = graph.get_mb_controller().get_timekeeper( + 0 + ).get_time_now() + uhd.types.TimeSpec(cap_delay) print(f"Requesting {num_samps} samples from {num_ports} radio(s)...") print(f"Capture will take approx. {num_samps/rate:.1f} seconds...") for rcp in radio_chan_pairs: @@ -196,32 +292,27 @@ def run_capture(graph, replay, radio_chan_pairs, mem_stride, num_samps, rate, ## Wait for record buffers to fill up timeout = time.monotonic() + num_samps / rate + cap_delay + 2.0 if HAVE_TQDM: - with tqdm.tqdm(total=num_bytes*num_ports, - unit_scale=True, unit="byte") as pbar: + with tqdm.tqdm(total=num_bytes * num_ports, unit_scale=True, unit="byte") as pbar: total_bytes_recorded = 0 while total_bytes_recorded < num_ports * num_bytes: - bytes_recorded = sum([ - replay.get_record_fullness(port) - for port in range(num_ports)]) + bytes_recorded = sum( + [replay.get_record_fullness(port) for port in range(num_ports)] + ) pbar.update(bytes_recorded - total_bytes_recorded) total_bytes_recorded = bytes_recorded time.sleep(0.100) if time.monotonic() > timeout: raise RuntimeError("Timeout while loading replay buffer!") else: - while any((replay.get_record_fullness(port) < num_bytes - for port in range(num_ports))): + while any((replay.get_record_fullness(port) < num_bytes for port in range(num_ports))): time.sleep(0.200) if time.monotonic() > timeout: raise RuntimeError("Timeout while loading replay buffer!") return num_bytes // BYTES_PER_SAMP -def rx_data_to_host(replay, rx_streamer, output_data, mem_stride, num_samps, - pkt_size_bytes): - """ - Download data from a previously configured replay block via a streamer - object. - """ + +def rx_data_to_host(replay, rx_streamer, output_data, mem_stride, num_samps, pkt_size_bytes): + """Download data from previously configured replay block via streamer object.""" print("Downloading data to host...") rx_md = uhd.types.RXMetadata() num_ports = rx_streamer.get_num_channels() @@ -240,23 +331,24 @@ def rx_data_to_host(replay, rx_streamer, output_data, mem_stride, num_samps, rx_streamer.issue_stream_cmd(stream_cmd) if HAVE_TQDM: num_rx = 0 - output_buf = np.zeros((num_ports, 1000*max_samps_per_pkt), dtype=output_data.dtype) - with tqdm.tqdm(total=num_bytes*num_ports, - unit_scale=True, unit="byte") as pbar: + output_buf = np.zeros((num_ports, 1000 * max_samps_per_pkt), dtype=output_data.dtype) + with tqdm.tqdm(total=num_bytes * num_ports, unit_scale=True, unit="byte") as pbar: while num_rx < num_samps: num_rx_i = rx_streamer.recv(output_buf, rx_md, 1.0) if rx_md.error_code == uhd.types.RXMetadataErrorCode.timeout: print("recv() timed out. Exiting...") break - if rx_md.error_code == uhd.types.RXMetadataErrorCode.overflow and \ - rx_md.out_of_sequence: + if ( + rx_md.error_code == uhd.types.RXMetadataErrorCode.overflow + and rx_md.out_of_sequence + ): print("Detected sequence error!") elif rx_md.error_code == uhd.types.RXMetadataErrorCode.overflow: print("ERROR: Overflow detected!") elif rx_md.error_code != uhd.types.RXMetadataErrorCode.none: print("ERROR: recv() gave unexpected error code: " + rx_md.strerror()) pbar.update(num_rx_i * BYTES_PER_SAMP * num_ports) - output_data[:, num_rx:num_rx+num_rx_i] = output_buf[:, 0:num_rx_i] + output_data[:, num_rx : num_rx + num_rx_i] = output_buf[:, 0:num_rx_i] num_rx += num_rx_i else: num_rx = rx_streamer.recv(output_data, rx_md, 5.0) @@ -269,9 +361,7 @@ def rx_data_to_host(replay, rx_streamer, output_data, mem_stride, num_samps, def main(): - """ - Run capture - """ + """Run capture.""" global HAVE_TQDM args = parse_args() if args.no_progress: @@ -279,12 +369,13 @@ def main(): graph = uhd.rfnoc.RfnocGraph(args.args) replay = uhd.rfnoc.ReplayBlockControl(graph.get_block(args.block)) radio_chan_pairs = enumerate_radios(graph, args.radio_channels) - rate = connect_radios(graph, replay, radio_chan_pairs, - args.freq, args.gain, args.antenna, args.rate) + rate = connect_radios( + graph, replay, radio_chan_pairs, args.freq, args.gain, args.antenna, args.rate + ) print(f"Using rate: {rate/1e6:.3f} Msps") # Set up streamer stream_args = uhd.usrp.StreamArgs(args.cpu_format, "sc16") - stream_args.args['throttle'] = str(args.throttle) + stream_args.args["throttle"] = str(args.throttle) rx_streamer = graph.create_rx_streamer(len(radio_chan_pairs), stream_args) num_ports = rx_streamer.get_num_channels() for chan in range(len(radio_chan_pairs)): @@ -293,9 +384,7 @@ def main(): graph.connect(replay.get_unique_id(), chan, rx_streamer, chan) graph.commit() - num_bytes = ( - args.duration * rate * BYTES_PER_SAMP if args.duration is not None else None - ) + num_bytes = args.duration * rate * BYTES_PER_SAMP if args.duration is not None else None mem_stride, num_bytes = _sanitize_args( replay, len(radio_chan_pairs), @@ -303,30 +392,30 @@ def main(): pkt_size_bytes=args.pkt_size, ) num_samps = run_capture( - graph, replay, radio_chan_pairs, - mem_stride, num_bytes//BYTES_PER_SAMP, rate, - args.delay) + graph, replay, radio_chan_pairs, mem_stride, num_bytes // BYTES_PER_SAMP, rate, args.delay + ) if args.numpy: tmp_dir = tempfile.mkdtemp() - mmap_filename = os.path.join(tmp_dir, 'replay_capture.dat') + mmap_filename = os.path.join(tmp_dir, "replay_capture.dat") else: mmap_filename = args.output_file - cap_dtype = np.complex64 if args.cpu_format == 'fc32' else np.uint32 + cap_dtype = np.complex64 if args.cpu_format == "fc32" else np.uint32 - output_data = np.memmap( - mmap_filename, shape=(num_ports, num_samps), mode='w+', dtype=cap_dtype) + output_data = np.memmap(mmap_filename, shape=(num_ports, num_samps), mode="w+", dtype=cap_dtype) output_data.flush() - rx_data_to_host(replay, rx_streamer, output_data, - mem_stride, num_samps, replay.get_max_packet_size(0)) + rx_data_to_host( + replay, rx_streamer, output_data, mem_stride, num_samps, replay.get_max_packet_size(0) + ) if args.numpy: print(f"Saving data as Numpy array to {args.output_file}...") - with open(args.output_file, 'wb') as out_file: + with open(args.output_file, "wb") as out_file: np.save(out_file, output_data) output_data = None shutil.rmtree(tmp_dir) return True + if __name__ == "__main__": sys.exit(not main()) diff --git a/host/examples/python/rx_to_file.py b/host/examples/python/rx_to_file.py index 73afb1888..30ca4772c 100755 --- a/host/examples/python/rx_to_file.py +++ b/host/examples/python/rx_to_file.py @@ -1,62 +1,141 @@ #!/usr/bin/env python3 # -# Copyright 2017-2018 Ettus Research, a National Instruments Company +# Copyright 2025 Ettus Research, a National Instruments Brand # # SPDX-License-Identifier: GPL-3.0-or-later # -""" -RX samples to file using Python API +"""Stream received samples to the host and store in a file using Python API. + +This example demonstrates how to use the UHD Python API to receive samples +from a USRP device and store them in a file on the host computer. It supports +two modes of operation: + +1. Host based: This mode uses the MultiUSRP API + to stream +received samples directly from the USRP to a host. This mode subjects the +host's ability (which depends on CPU speed, network interface speed, CPU thread +interruptions) to stream samples as fast as the USRP is receiving them. + +2. RFNoC Replay: This mode uses advanced API functions to stream samples via +DRAM using capabilities of RFNoC (RF Network-on-Chip) +. The samples are first received and +buffered in DRAM and them streamed to host. This mode requires FPGA bitfiles +that contain the RFNoC Replay Block and is not supported by all USRP devices. + +This example is useful for applications where users want to capture +samples from a USRP device for offline processing, analysis, or storage. + +Example Usage: +rx_to_file.py --args addr=192.168.10.2 --output-file samples.dat --freq 2.45e9 + --rate 20e6 --duration 15 --channels 0 1 --gain 30 + +Note: argument --output-file needs to point to a location where thr user has +write access. """ import argparse + import numpy as np import uhd -from uhd.usrp import dram_utils from uhd.types import StreamCMD, StreamMode +from uhd.usrp import dram_utils def parse_args(): - """Parse the command line arguments""" + """Parse the command line arguments.""" parser = argparse.ArgumentParser() - parser.add_argument("-a", "--args", default="", type=str) - parser.add_argument("-o", "--output-file", type=str, required=True) - parser.add_argument("-f", "--freq", type=float, required=True) - parser.add_argument("-r", "--rate", default=1e6, type=float) - parser.add_argument("-d", "--duration", default=5.0, type=float) - parser.add_argument("-c", "--channels", default=0, nargs="+", type=int) - parser.add_argument("-g", "--gain", type=int, default=10) - parser.add_argument("-n", "--numpy", default=False, action="store_true", - help="Save output file in NumPy format (default: No)") - parser.add_argument("--dram", action='store_true', - help="If given, will attempt to stream via DRAM") + parser.add_argument( + "-a", + "--args", + default="", + type=str, + help="""specifies the USRP device arguments, which holds + multiple key value pairs separated by commas + (e.g., addr=192.168.40.2,type=x300) [default = ""].""", + ) + parser.add_argument( + "-o", + "--output-file", + type=str, + required=True, + help="specifies the name of the file(s) to write binary samples to, " + "inserts channel number into the file name if more than one is given [default = None].", + ) + parser.add_argument( + "-f", + "--freq", + type=float, + required=True, + help="specifies the center frequency in Hz [input is required].", + ) + parser.add_argument( + "-r", + "--rate", + default=1e6, + type=float, + help="specifies the sample rate in samples/sec [default = 1e6].", + ) + parser.add_argument( + "-d", + "--duration", + default=5.0, + type=float, + help="specifies the RX stream duration in seconds [default = 5.0].", + ) + parser.add_argument( + "-c", + "--channels", + default=0, + nargs="+", + type=int, + help='specifies the channel(s) to use (specify "0", "1", "0 1", etc) [default = 0].', + ) + parser.add_argument( + "-g", + "--gain", + type=int, + default=10, + help="specifies the receive gain in dB [default = 10].", + ) + parser.add_argument( + "-n", + "--numpy", + default=False, + action="store_true", + help="specifies whether to save the output file in NumPy format. If --numpy " + "is not specified, the output file will be in binary format.", + ) + parser.add_argument( + "--dram", + action="store_true", + help="specifies the mode of operation (Host based or RFNoC Replay). " + "If argument --dram is specified, the program will attempt to use the RFNoC " + "Replay mode, else the Host Based is used.", + ) return parser.parse_args() + def multi_usrp_rx(args): - """ - multi_usrp based RX example - """ + """multi_usrp based RX example.""" usrp = uhd.usrp.MultiUSRP(args.args) - num_samps = int(np.ceil(args.duration*args.rate)) + num_samps = int(np.ceil(args.duration * args.rate)) if not isinstance(args.channels, list): args.channels = [args.channels] samps = usrp.recv_num_samps(num_samps, args.freq, args.rate, args.channels, args.gain) - with open(args.output_file, 'wb') as out_file: + with open(args.output_file, "wb") as out_file: if args.numpy: np.save(out_file, samps, allow_pickle=False, fix_imports=False) else: samps.tofile(out_file) + def rfnoc_dram_rx(args): - """ - rfnoc_graph + replay-block based RX example - """ + """rfnoc_graph + replay-block based RX example.""" # Init graph graph = uhd.rfnoc.RfnocGraph(args.args) - num_samps = int(np.ceil(args.duration*args.rate)) + num_samps = int(np.ceil(args.duration * args.rate)) if graph.get_num_mboards() > 1: - print( - "ERROR: This example only supports DRAM streaming on a single " - "motherboard.") + print("ERROR: This example only supports DRAM streaming on a single " "motherboard.") return # Init radios and replay block available_radio_chans = [ @@ -67,7 +146,7 @@ def rfnoc_dram_rx(args): radio_chans = [available_radio_chans[x] for x in args.channels] print("Receiving from radio channels:", end="") print("\n* ".join((f"{r}:{c}" for r, c in radio_chans))) - dram = dram_utils.DramReceiver(graph, radio_chans, cpu_format='fc32') + dram = dram_utils.DramReceiver(graph, radio_chans, cpu_format="fc32") replay = dram.replay_blocks[0] print(f"Using replay block {replay.get_block_id()}") for (radio, radio_chan), ddc_info in zip(dram.radio_chan_pairs, dram.ddc_chan_pairs): @@ -90,14 +169,15 @@ def rfnoc_dram_rx(args): dram.issue_stream_cmd(stream_cmd) rx_md = uhd.types.RXMetadata() dram.recv(data, rx_md) - with open(args.output_file, 'wb') as out_file: + with open(args.output_file, "wb") as out_file: if args.numpy: np.save(out_file, data, allow_pickle=False, fix_imports=False) else: data.tofile(out_file) + def main(): - """RX samples and write to file""" + """RX samples and write to file.""" args = parse_args() if args.dram: @@ -105,5 +185,6 @@ def main(): else: multi_usrp_rx(args) + if __name__ == "__main__": main() diff --git a/host/examples/python/tx_waveforms.py b/host/examples/python/tx_waveforms.py index e39ed60e8..e401d9ca8 100755 --- a/host/examples/python/tx_waveforms.py +++ b/host/examples/python/tx_waveforms.py @@ -1,6 +1,6 @@ # !/usr/bin/env python3 # -# Copyright 2025 Ettus Research, a National Instruments Company +# Copyright 2025 Ettus Research, a National Instruments Brand # # SPDX-License-Identifier: GPL-3.0-or-later """Example to transmit waveforms using UHD python API. @@ -27,8 +27,8 @@ pattern from DRAM. This mode requires FPGA bitfiles that contain the RFNoC Replay Block and is not supported on all USRPs. Example usage: -tx_waveforms.py [--args addr=192.168.10.2] --freq 2.4e9 [--rate 1e6 --duration 10 - --channels 0 --wave-freq 1e4 --wave-ampl 0.3] [--dram] +tx_waveforms.py --args addr=192.168.10.2 --freq 2.4e9 --rate 1e6 --duration 10 + --channels 0 --wave-freq 1e4 --wave-ampl 0.3 --dram """ import argparse @@ -54,7 +54,9 @@ def parse_args(): "--args", type=str, default="", - help='specifies the uhd device arguments, which holds multiple key value pairs separated by commas (e.g., addr=192.168.40.2,type=x300) [Default = ""]', + help="""specifies the USRP device arguments, which holds + multiple key value pairs separated by commas + (e.g., addr=192.168.40.2,type=x300) [default = ""].""", ) parser.add_argument( "-w", diff --git a/host/examples/python/usrp_power_meter.py b/host/examples/python/usrp_power_meter.py index fc59ae689..253380576 100644 --- a/host/examples/python/usrp_power_meter.py +++ b/host/examples/python/usrp_power_meter.py @@ -1,65 +1,130 @@ #!/usr/bin/env python3 # -# Copyright 2020 Ettus Research, a National Instruments Brand +# Copyright 2025 Ettus Research, a National Instruments Brand # # SPDX-License-Identifier: GPL-3.0-or-later # -""" -Use a calibrated USRP as a power meter +"""Use a calibrated USRP as a power meter. + +This example demonstrates how to use a calibrated USRP device as a power +meter. It measures the received signal power in dBm at a specified center +frequency (plus a local oscillator offset) and within the configured analog +filter bandwidth. Results are provided either as a one-time measurement or +continuously until interrupted. This script is useful for applications +requiring precise power measurements, such as signal strength analysis or +calibration. + +Refer to the UHD Manual on how to calibrate a USRP device: +https://files.ettus.com/manual/page_calibration.html + +Example Usage: +usrp_power_meter.py --args addr=192.168.10.2 --freq 2.1e9 --rate 10e6 + --bandwidth 5e6 --antenna RX1 --ref-level -10 + --samps-per-est 1e7 --mode continuous """ -import sys -import signal import argparse +import signal +import sys + import uhd + def parse_args(): - """Parse the command line arguments""" + """Parse the command line arguments.""" parser = argparse.ArgumentParser() - parser.add_argument("-a", "--args", default="", - help="USRP Device Args") - parser.add_argument("-f", "--freq", type=float, required=True, - help="Center Frequency") - parser.add_argument("-o", "--lo-offset", type=float, default=0.0, - help="Optional LO offset") - parser.add_argument("-c", "--channel", type=int, default=0, - help="USRP RX Channel Index") - parser.add_argument("-t", "--antenna", - help="USRP RX Antenna") - parser.add_argument("-r", "--rate", default=1e6, type=float, - help="Sampling Rate") - parser.add_argument("-b", "--bandwidth", type=float, - help="Analog filter bandwidth (if supported)") - parser.add_argument("-l", "--ref-level", type=float, default=-15, - help="RX reference power level. " - "This should be higher than the expected power.") - parser.add_argument("-n", "--samps-per-est", type=float, default=1e6, - help="Samples per estimate.") - parser.add_argument("--mode", choices=['one-shot', 'continuous'], default='one-shot', - help="Measure once, or keep measuring until Ctrl-C is pressed.") + parser.add_argument( + "-a", + "--args", + default="", + help="""specifies the USRP device arguments, which holds + multiple key value pairs separated by commas + (e.g., addr=192.168.40.2,type=x300) [default = ""].""", + ) + parser.add_argument( + "-f", + "--freq", + type=float, + required=True, + help="specifies the center frequency in Hz [input is required].", + ) + parser.add_argument( + "-o", + "--lo-offset", + type=float, + default=0.0, + help="specifies the local oscillator (LO) offset with respect to the " + "configured center frequency [default = 0.0].", + ) + parser.add_argument( + "-c", + "--channel", + type=int, + default=0, + help="specifies the RX Channel to use [default = 0].", + ) + parser.add_argument("-t", "--antenna", help="specifies the RX Antenna to use [default = None].") + parser.add_argument( + "-r", + "--rate", + default=1e6, + type=float, + help="specifies the sample rate in samples/sec [default = 1e6].", + ) + parser.add_argument( + "-b", + "--bandwidth", + type=float, + help="specifies the analog filter bandwidth in Hz (if supported) [default = None].", + ) + parser.add_argument( + "-l", + "--ref-level", + type=float, + default=-15, + help="specifies the RX reference power level in dBm. The receiver will " + "automatically pick a gain such that an input CW tone at reference power " + "level will fully excite the ADC. The Reference power level should be higher " + "than the expected power level [default = -15].", + ) + parser.add_argument( + "-n", + "--samps-per-est", + type=float, + default=1e6, + help="specifies the number of samples per estimate [default = 1e6].", + ) + parser.add_argument( + "--mode", + choices=["one-shot", "continuous"], + default="one-shot", + help="specifies the operation mode, that is either measure once, " + "or keep measuring until Ctrl-C is pressed. [default = one-shot]", + ) return parser.parse_args() + def get_streamer(usrp, chan): - """ - Return an RX streamer with fc32 output - """ - stream_args = uhd.usrp.StreamArgs('fc32', 'sc16') + """Return an RX streamer with fc32 output.""" + stream_args = uhd.usrp.StreamArgs("fc32", "sc16") stream_args.channels = [chan] return usrp.get_rx_stream(stream_args) + def setup_device(usrp, args): - """ - Apply the settings from args to the device - """ + """Apply the settings from args to the device.""" chan = args.channel if chan > usrp.get_rx_num_channels(): - print("ERROR: Invalid channel selected: {} (only {} channels available!)" - .format(chan, usrp.get_rx_num_channels())) + print( + "ERROR: Invalid channel selected: {} (only {} channels available!)".format( + chan, usrp.get_rx_num_channels() + ) + ) raise RuntimeError("Invalid channel selected") print("Using channel: {}".format(chan)) if args.antenna: - print("Setting RX antenna to `{}'...".format(args.antenna), end='') + print("Setting RX antenna to `{}'...".format(args.antenna), end="") usrp.set_rx_antenna(args.antenna, chan) print("OK") @@ -68,15 +133,14 @@ def setup_device(usrp, args): print("ERROR: This device is not calibrated for RX at RF%d-%s!" % (chan, antenna)) raise RuntimeError("Device not calibrated for selected antenna") - print("Requesting RX rate of {} Msps...".format(args.rate / 1e6), end='') + print("Requesting RX rate of {} Msps...".format(args.rate / 1e6), end="") usrp.set_rx_rate(args.rate, chan) if abs(usrp.get_rx_rate(chan) - args.rate) > 1.0: - print("ALMOST. Actual rate: {} Msps" - .format(usrp.get_rx_rate(chan) / 1e6)) + print("ALMOST. Actual rate: {} Msps".format(usrp.get_rx_rate(chan) / 1e6)) else: print("OK") - print("Requesting RX frequency of {} MHz...".format(args.freq / 1e6), end='') + print("Requesting RX frequency of {} MHz...".format(args.freq / 1e6), end="") tr = uhd.types.TuneRequest(args.freq, args.lo_offset) usrp.set_rx_freq(tr, chan) if abs(usrp.get_rx_freq(chan) - args.freq) > 1.0: @@ -84,8 +148,7 @@ def setup_device(usrp, args): else: print("OK") - print("Requesting RX power reference level of {:.2f} dBm..." - .format(args.ref_level), end='') + print("Requesting RX power reference level of {:.2f} dBm...".format(args.ref_level), end="") usrp.set_rx_power_reference(args.ref_level, chan) ref_level = usrp.get_rx_power_reference(chan) if abs(ref_level - args.ref_level) > 1.0: @@ -94,45 +157,45 @@ def setup_device(usrp, args): print("OK") if args.bandwidth: - print("Requesting analog RX bandwidth of {} Msps..." - .format(args.bandwidth), end='') + print("Requesting analog RX bandwidth of {} Msps...".format(args.bandwidth), end="") usrp.set_rx_bandwidth(args.bandwidth, chan) if abs(usrp.get_rx_bandwidth(chan) - args.bandwidth) > 1.0: - print("ALMOST. Actual bandwidth: {} MHz" - .format(usrp.get_rx_bandwidth(chan) / 1e6)) + print("ALMOST. Actual bandwidth: {} MHz".format(usrp.get_rx_bandwidth(chan) / 1e6)) else: print("OK") return (chan, ref_level) + RUN = True + def main(): - """ - gogogo - """ + """Run USRP power meter.""" args = parse_args() usrp = uhd.usrp.MultiUSRP(args.args) (chan, ref_level) = setup_device(usrp, args) streamer = get_streamer(usrp, chan) - if args.mode == 'continuous': + if args.mode == "continuous": + def handle_sigint(_sig, _frame): print("Caught Ctrl-C, exiting...") global RUN RUN = False + signal.signal(signal.SIGINT, handle_sigint) while RUN: try: - power_dbfs = uhd.dsp.signals.get_usrp_power( - streamer, num_samps=int(args.samps_per_est)) + power_dbfs = uhd.dsp.signals.get_usrp_power(streamer, num_samps=int(args.samps_per_est)) except RuntimeError: # This is a hack b/c the signal handler is not gracefully handling # SIGINT break power_dbm = power_dbfs + ref_level print("Received power: {:+6.2f} dBm".format(power_dbm)) - if args.mode == 'one-shot': + if args.mode == "one-shot": break return True + if __name__ == "__main__": sys.exit(not main()) diff --git a/host/examples/python/x440_L_band_capture.py b/host/examples/python/x440_L_band_capture.py index a2a1f6f70..573057cb5 100644 --- a/host/examples/python/x440_L_band_capture.py +++ b/host/examples/python/x440_L_band_capture.py @@ -1,87 +1,141 @@ #!/usr/bin/env python3 -""" +# +# Copyright 2025 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +"""x440 L-band capture example using Python API. + This example requires -a USRP X440 -UHD 4.6 or higher -matplotlib in Python installation - -the X4_1600 bitfile installed + -X4_1600 bitfile installed -Demonstrates dual rates on X440 at the example of the L-band by recording data into the replay -blocks at two given master clock rates and two different center frequencies and displays the -captured data in a pyplot. Since the X440 is a direct sampling device, Nyquist zones and their -boundaries are important to keep in mind. The converter rate determines the Nyquist zone -boundaries: +This example demonstrates simultaneous reception at two different rates in the +L band. Samples received at two different channels at two different master +clock rates and two different center frequencies are captured in two different +replay blocks. An FFT is computed for each of the two sets of captured data. +The FFT outputs are concatenated and displayed in a single plot. Since the X440 +is a direct sampling device, Nyquist zones and their boundaries are important +to keep in mind. The converter rate determines the Nyquist zone boundaries: Nyquist boundaries = n * (converter rate / 2) -The master clock rate can be derived from the converter rate by dividing it by the decimation -factor 2, 4 or 8. It is equivalent to the bandwidth that can be captured on one channel. -The maximum bandwidth that can be achieved by the X440 is 2 GHz when using the maximum converter -rate of 4 GSps. Although from a bandwidth perspective this would be sufficient to cover the L-band, -this does not work because these 2 GHz are roughly between 0 and 2 GHz while the L-band is located -between 1 GHz and 2.4 GHz. To work around this, two different rates can be used in the X440. The -captured spectra of both can be stitched together. This example does this in a visual way only to -demonstrate the principle. In that way we are crossing Nyquist gaps in the two spectra, but -you should apply external filtering to avoid the aliasing images that will be visible otherwise. +The master clock rate can be derived from the converter rate by dividing it by +decimation factor of 2, 4 or 8. It is equivalent to the bandwidth that can be +captured on one channel. The maximum master clock rate (and hence bandwidth) +that is supported by USRP X440 is 2 GHz, when using the maximum converter rate +of 4 GSps. Although from a bandwidth perspective this would be sufficient to +cover the L-band, this does not work because these 2 GHz are roughly between 0 +and 2 GHz while the L-band is located between 1 GHz and 2.4 GHz. To work around +this, two different rates can be used in the X440. The captured spectra of both +can be stitched together. This example does this in a visual way only to +demonstrate this principle. In that way we are crossing Nyquist gaps in the two +spectra. Users should apply external filtering to avoid the aliasing images +that will be visible otherwise. -Because the X4_1600 bitfile is used, only channel 0 of each radio is available and will be used. +Because the X4_1600 bitfile is used, only channel 0 of each radio is available +and will be used. -By default this example will use master clock rates, converter rates and center frequencies to -cover the L-band. The parameters are: +By default, this example will use master clock rates, converter rates and +center frequencies to cover the L-band. The parameters are: -Master clock rate 0: 1024 MHz (converter rate: 4.096 GHz) -Master clock rate 1: 1280 MHz (converter rate: 2.560 GHz) -Center frequency 0: 0.9 GHz -Center frequency 1: 2 GHz -With these settings, radio 0 will be able to capture the L-band between 0.39 GHz and 1.4 GHz in its -first Nyquist zone, and radio 1 will cover the range between 1.36 GHz and 2.64 GHz with its second -Nyquist zone. +With these settings, radio 0 will be able to capture the L-band between 0.39 GHz +and 1.4 GHz in its first Nyquist zone, and radio 1 will cover the range between +1.36 GHz and 2.64 GHz with its second Nyquist zone. + +Example usage: +x440_L_band_capture.py --args addr=192.168.10.2 --mcrs 1024e6 1280e6 + --freq 1.1e9 2.3e9 --antenna RX1 RX2 --duration 0.005 + --delay 0.015 --throttle 0.3 """ -import time import argparse import sys -import uhd -from uhd import rfnoc -import numpy as np +import time + import matplotlib.pyplot as plt +import numpy as np +import uhd # sc16 (32-bit) samples on the USRP BYTES_PER_SAMP = 4 + # pylint: disable=too-many-arguments def parse_args(): - """ - Return parsed command line args - """ + """Return parsed command line args.""" parser = argparse.ArgumentParser( - description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument("--args", "-a", type=str, default="", - help="Device args to use when connecting to the USRP.") - parser.add_argument("--mcrs", "-m", type=float, required=False, nargs=2, - help="Master clock rates for both channels, default " - "1024e6 1280e6", - default=[1024e6,1280e6]) - parser.add_argument("-f", "--freq", type=float, required=False, nargs=2, - help="Center frequencies for 2 channels, default 900e6 2e9", - default=[900e6, 2e9]) - parser.add_argument("--antenna", help="Antennas for both channels, default " - "RX1 RX1", nargs=2, - default=['RX1', 'RX1']) - parser.add_argument("-d", "--duration", type=float, - help="Duration in seconds to capture. " - "Default is 0.0001 seconds.", default=0.0001) - parser.add_argument("--delay", "-l", type=float, default=0.01, - help="Capture delay in seconds, default 0.01") - parser.add_argument("--throttle", type=float, default=0.25, - help="Throttle for streaming to host, range (0, 1] (default: 0.25)") + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "--args", + "-a", + type=str, + default="", + help="""specifies the USRP device arguments, which holds + multiple key value pairs separated by commas + (e.g., addr=192.168.40.2,type=x300) [default = ""].""", + ) + parser.add_argument( + "--mcrs", + "-m", + type=float, + required=False, + nargs=2, + help="specifies the master clock rates in samples/sec for both receive channels " + "[default = 1024e6 1280e6].", + default=[1024e6, 1280e6], + ) + parser.add_argument( + "-f", + "--freq", + type=float, + required=False, + nargs=2, + help="specifies the center frequencies in Hz for both receive channels " + "[default = 900e6 2e9].", + default=[900e6, 2e9], + ) + parser.add_argument( + "--antenna", + nargs=2, + help="specifies the antennas for both receive channels [default = RX1 RX1].", + default=["RX1", "RX1"], + ) + parser.add_argument( + "-d", + "--duration", + type=float, + help="specifies the duration in seconds to capture [default = 0.0001].", + default=0.0001, + ) + parser.add_argument( + "--delay", + "-l", + type=float, + default=0.01, + help="specifies the delay in seconds between issuing a capture command " + "on the host and starting the capture [default = 0.01].", + ) + parser.add_argument( + "--throttle", + type=float, + default=0.25, + help="specifies the throttle for streaming to host in the range of (0, 1]. " + "E.g., use 1 for maximum rate, use 0.5 for half the maximum rate, etc. " + "Note that other factors may affect the actual rate, such as the rate of " + "the source or the speed supported by the transport [default = 0.25].", + ) return parser.parse_args() def main(): - """ - Main function of the dual rate example. - """ + """Main function of the dual rate example.""" args = parse_args() dev_args = f"{args.args},product=x440,master_clock_rate=({args.mcrs[0]};{args.mcrs[1]})" @@ -112,10 +166,10 @@ def main(): num_samps1 = 0 num_ports = 1 - cap_dtype=np.complex64 + cap_dtype = np.complex64 stream_args = uhd.usrp.StreamArgs("fc32", "sc16") - stream_args.args['throttle'] = str(throttle) + stream_args.args["throttle"] = str(throttle) # Set up streamer0 rx_streamer0 = graph.create_rx_streamer(num_ports, stream_args) @@ -129,29 +183,40 @@ def main(): capture_duration = args.duration cap_delay = args.delay - update_interval = 0.2 # Update every update_interval seconds. - + update_interval = 0.2 # Update every update_interval seconds. # Plotting initialization. - plt.ion() # Stop matplotlib windows from blocking - plt.rcParams['toolbar'] = 'None' + plt.ion() # Stop matplotlib windows from blocking + plt.rcParams["toolbar"] = "None" - fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(15, 6), gridspec_kw = {'wspace':0, 'hspace':0}) + fig, axes = plt.subplots( + nrows=1, ncols=2, figsize=(15, 6), gridspec_kw={"wspace": 0, "hspace": 0} + ) ax_replay0 = axes[0] ax_replay1 = axes[1] # Setup figure, axis and initiate plot xdata_replay0, ydata_replay0 = [], [] - ln_replay0, = ax_replay0.plot([], [],) - ax_replay0.title.set_text(f"Radio0 operating at {round(radio0.get_rate()/1e6, 2)} MSps tuned to {round(radio0.get_rx_frequency(0)/1e9, 2)} GHz.") - ax_replay0.set_ylim(-160,20) + (ln_replay0,) = ax_replay0.plot( + [], + [], + ) + ax_replay0.title.set_text( + f"Radio0 operating at {round(radio0.get_rate()/1e6, 2)} MSps tuned to {round(radio0.get_rx_frequency(0)/1e9, 2)} GHz." + ) + ax_replay0.set_ylim(-160, 20) ax_replay0.grid() ax_replay0.margins(x=0) xdata_replay1, ydata_replay1 = [], [] - ln_replay1, = ax_replay1.plot([], [],) - ax_replay1.title.set_text(f"Radio1 operating at {round(radio1.get_rate()/1e6, 2)} MSps tuned to {round(radio1.get_rx_frequency(0)/1e9, 2)} GHz.") - ax_replay1.set_ylim(-160,20) + (ln_replay1,) = ax_replay1.plot( + [], + [], + ) + ax_replay1.title.set_text( + f"Radio1 operating at {round(radio1.get_rate()/1e6, 2)} MSps tuned to {round(radio1.get_rx_frequency(0)/1e9, 2)} GHz." + ) + ax_replay1.set_ylim(-160, 20) ax_replay1.grid() ax_replay1.margins(x=0) @@ -186,12 +251,9 @@ def main(): stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.num_done) stream_cmd.num_samps = num_samps0 stream_cmd.stream_now = False - stream_cmd.time_spec = \ - time_now + \ - uhd.types.TimeSpec(cap_delay) + stream_cmd.time_spec = time_now + uhd.types.TimeSpec(cap_delay) radio0.issue_stream_cmd(stream_cmd, 0) - # Record from radio1 into DRAM using replay1 num_samps1 = int(args.mcrs[1] * capture_duration) num_bytes1 = num_samps1 * BYTES_PER_SAMP @@ -205,23 +267,23 @@ def main(): stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.num_done) stream_cmd.num_samps = num_samps1 stream_cmd.stream_now = False - stream_cmd.time_spec = \ - time_now + \ - uhd.types.TimeSpec(cap_delay) + stream_cmd.time_spec = time_now + uhd.types.TimeSpec(cap_delay) radio1.issue_stream_cmd(stream_cmd, 0) # Wait for record buffers to fill up timeout = time.monotonic() + num_samps0 / args.mcrs[0] + cap_delay + 10 - while any((replay0.get_record_fullness(port) < num_bytes0 - for port in range(num_ports))): + while any( + (replay0.get_record_fullness(port) < num_bytes0 for port in range(num_ports)) + ): time.sleep(0.100) if time.monotonic() > timeout: raise RuntimeError("Timeout while loading replay0 buffer!") ## Wait for record buffers to fill up timeout = time.monotonic() + num_samps1 / args.mcrs[1] + cap_delay + 10 - while any((replay1.get_record_fullness(port) < num_bytes1 - for port in range(num_ports))): + while any( + (replay1.get_record_fullness(port) < num_bytes1 for port in range(num_ports)) + ): time.sleep(0.100) if time.monotonic() > timeout: raise RuntimeError("Timeout while loading replay1 buffer!") @@ -230,8 +292,8 @@ def main(): rx_md = uhd.types.RXMetadata() num_bytes = num_samps0 * BYTES_PER_SAMP - pkt_size_bytes = replay0.get_max_packet_size(0) - max_samps_per_pkt = pkt_size_bytes // BYTES_PER_SAMP + # pkt_size_bytes = replay0.get_max_packet_size(0) + # max_samps_per_pkt = pkt_size_bytes // BYTES_PER_SAMP mem_stride = replay0.get_mem_size() // num_ports # Configure playback regions for idx in range(num_ports): @@ -244,7 +306,6 @@ def main(): stream_cmd.time_spec = uhd.types.TimeSpec(0.0) rx_streamer0.issue_stream_cmd(stream_cmd) - num_rx = rx_streamer0.recv(output_buf_replay0, rx_md, 5.0) if rx_md.error_code != uhd.types.RXMetadataErrorCode.none: print("Error during download: " + rx_md.strerror()) @@ -252,22 +313,28 @@ def main(): print("ERROR: Fewer samples received than expected!") # Get the new data - N = len(output_buf_replay0) - window = np.hamming(N) + len_buf_replay0 = len(output_buf_replay0) + window = np.hamming(len_buf_replay0) fft = np.fft.fft(output_buf_replay0 * window) - window_power = sum(window * window) / N - logfft = 20*np.log10(np.abs(np.fft.fftshift(fft))) - 10*np.log10(window_power) - 20*np.log10(N) + 3 - xdata_replay0 = np.arange(-N/2,N/2,1) / N * args.mcrs[0] + radio0.get_rx_frequency(0) + window_power = sum(window * window) / len_buf_replay0 + logfft = ( + 20 * np.log10(np.abs(np.fft.fftshift(fft))) + - 10 * np.log10(window_power) + - 20 * np.log10(len_buf_replay0) + + 3 + ) + xdata_replay0 = np.arange( + -len_buf_replay0 / 2, len_buf_replay0 / 2, 1 + ) / len_buf_replay0 * args.mcrs[0] + radio0.get_rx_frequency(0) ydata_replay0 = logfft - output_buf_replay1 = np.zeros(num_samps1, dtype=cap_dtype) - #print("Downloading data to host from Replay1...") + # print("Downloading data to host from Replay1...") rx_md = uhd.types.RXMetadata() num_bytes = num_samps1 * BYTES_PER_SAMP - pkt_size_bytes = replay1.get_max_packet_size(0) - max_samps_per_pkt = pkt_size_bytes // BYTES_PER_SAMP + # pkt_size_bytes = replay1.get_max_packet_size(0) + # max_samps_per_pkt = pkt_size_bytes // BYTES_PER_SAMP mem_stride = replay1.get_mem_size() // num_ports # Configure playback regions for idx in range(num_ports): @@ -280,7 +347,6 @@ def main(): stream_cmd.time_spec = uhd.types.TimeSpec(0.0) rx_streamer1.issue_stream_cmd(stream_cmd) - num_rx = rx_streamer1.recv(output_buf_replay1, rx_md, 5.0) if rx_md.error_code != uhd.types.RXMetadataErrorCode.none: print("Error during download: " + rx_md.strerror()) @@ -288,12 +354,19 @@ def main(): print("ERROR: Fewer samples received than expected!") # Get the new data - N = len(output_buf_replay1) - window = np.hamming(N) + len_buf_replay1 = len(output_buf_replay1) + window = np.hamming(len_buf_replay1) fft = np.fft.fft(output_buf_replay1 * window) - window_power = sum(window * window) / N - logfft = 20*np.log10(np.abs(np.fft.fftshift(fft))) - 10*np.log10(window_power) - 20*np.log10(N) + 3 - xdata_replay1 = np.arange(-N/2,N/2,1) / N * args.mcrs[1] + radio1.get_rx_frequency(0) + window_power = sum(window * window) / len_buf_replay1 + logfft = ( + 20 * np.log10(np.abs(np.fft.fftshift(fft))) + - 10 * np.log10(window_power) + - 20 * np.log10(len_buf_replay1) + + 3 + ) + xdata_replay1 = np.arange( + -len_buf_replay1 / 2, len_buf_replay1 / 2, 1 + ) / len_buf_replay1 * args.mcrs[1] + radio1.get_rx_frequency(0) ydata_replay1 = logfft # Reset the data in the plot @@ -312,7 +385,6 @@ def main(): fig.canvas.draw() fig.canvas.flush_events() - time.sleep(update_interval) except KeyboardInterrupt: