EttusResearch / uhd

The USRP™ Hardware Driver Repository
http://uhd.ettus.com
Other
971 stars 653 forks source link

x310 multi-channel acquisition with Python API #771

Closed CarlosHdezUAH closed 2 months ago

CarlosHdezUAH commented 2 months ago

Hello! I am interested in acquiring multiple channels with my rig using the Python API. This is my setup: two x310 with two TwinRX each with one Octoclock. I have UHD v4.6.0.0.0 installed. I have Ubuntu 22.04 with Python 3.10.12.

And I am using the following code:


import argparse
import numpy as np
import uhd
import gc
import subprocess

def parse_args():
    """Parse the command line arguments"""
    parser = argparse.ArgumentParser()
    parser.add_argument("-a", "--args", default="", type=str,
                        help="Additional arguments for the USRP device")
    parser.add_argument("-o", "--output-file", type=str, required=True,
                        help="Output file prefix where samples will be saved")
    parser.add_argument("-f", "--freq", type=float, required=True,
                        help="Center frequency in Hz")
    parser.add_argument("-r", "--rate", default=1e6, type=float,
                        help="Sample rate in Hz (default: 1e6)")
    parser.add_argument("-d", "--duration", default=5.0, type=float,
                        help="Duration of reception in seconds (default: 5.0)")
    parser.add_argument("-c", "--channels", default=[0, 1], nargs="+", type=int,
                        help="List of channel IDs to receive from (default: 0 1)")
    parser.add_argument("-g", "--gain", type=int, default=10,
                        help="Gain in dB (default: 10)")
    parser.add_argument("-n", "--numpy", default=False, action="store_true",
                        help="Save output file in NumPy format (default: No)")
    parser.add_argument("-i", "--ip-address", type=str, required=True,
                        help="IP address of the USRP device")
    return parser.parse_args()

def get_board_number(ip_address):
    """Extract board number from IP address"""
    parts = ip_address.split('.')
    return parts[-2]  # Extract the second-to-last part of the IP address

def main():
    """RX samples and write to file"""
    args = parse_args()
    usrp = uhd.usrp.MultiUSRP(f"addr={args.ip_address} {args.args}")
    usrp.set_rx_bandwidth(args.rate, 0)
    num_samps = int(np.ceil(args.duration * args.rate))
    board_number = get_board_number(args.ip_address)

    # Receive samples for both channels simultaneously
    samps = usrp.recv_num_samps(num_samps, args.freq, args.rate, args.channels, args.gain)

    for channel_id, samples in zip(args.channels, samps):
        output_filename = f"{args.output_file}_{board_number}_{channel_id}.iq"
        print(f"Saving samples for TwinRX 192.168.{board_number}.2, channel {channel_id} to {output_filename}")
        samples.tofile(output_filename)

if __name__ == "__main__":
    try:
        main()
    finally:
        command = "sync; echo 3 > /proc/sys/vm/drop_caches"
        subprocess.run(['sudo', 'sh', '-c', command])
        print("Caché freed successfully.")
        gc.collect()
        print("RAM freed successfully")

I want to start by simply acquiring 2 channels of the same TwinRX (leaving synchronization aside although I should implement timestamps to handle overflows, although the goal is no overflows).

When I capture the two channels with a sampling rate of 50 MHz the ram starts to fill up until about a minute and a half later it fills up and starts giving overflows. To avoid the overflows I have the following idea: acquire samples continuously and every so often write a file with the samples, clear the ram and the cache to avoid it filling up. The problem is that deleting the ram would interrupt the acquisition process and could delete unwritten samples. The solution to this could be multithreading with the threading library? Honestly I still don't understand very well the difference between mutithreading and multiproccesing in python.

My question is: has anyone already faced this problem and can you tell me how I have to proceed to solve it?

I have read that with multiprocessing I could generate 1 process for each channel and within each process a subprocess to acquire and another to write the file. Something like that

Note: MTUs are at 9000 and I run the following lines of code before launching the acquisition:

sudo sysctl -w net.core.wmem_max=33554432
sudo sysctl -w net.core.wmem_max=33554432
sudo cpupower frequency-set --governor performance
sudo ethtool -G enp45s0f1 tx 8192 rx 8192  
sudo ethtool -G enp166s0f1 tx 8192 rx 8192 
sudo ethtool -G enp21s0f1 tx 8192 rx 8192  
sudo ethtool -G enp141s0f1 tx 8192 rx 8192 
sudo ethtool -G enp21s0f0 tx 8192 rx 8192
sudo ethtool -G enp141s0f0 tx 8192 rx 8192
sudo ethtool -G enp166s0f0 tx 8192 rx 8192
sudo ethtool -G enp45s0f0 tx 8192 rx 8192
mbr0wn commented 2 months ago

@CarlosHdezUAH there are some common design patterns:

In a nutshell, what you can do is have a receiver thread which fills one of your buffers and then writes a descriptor of that buffer into a FIFO.

When the buffer is full, you pull the next buffer out of another FIFO and start filling that. If there are no more buffers in your pool, then you'll still overflow but then there's the other thread: It will pull filled buffers out of the first FIFO and write them to disk. When they've been written, you put the empty buffer back into the second FIFO (which goes back to the receiver thread).

If your write-to-disk-thread can sustain the same average rate as your receiver, then you're golden, and you won't be interrupting your receiver thread.

For this case, you can probably use either multiprocessing or multithreading, because both threads are IO bound. But I would default to multiprocessing anyway to get better concurrency.

All of that said -- this is a more of a support request, and not a bug, so I'm going to close this. Best of luck!