Post Go back to editing

Trying to port Phaser FMCW radar code to run on ADLAM-Pluto

Category: Software
Product Number: ADLAM-Pluto
Software Version: Pluto firmware 0.38

Hello,

I am trying to run the Phaser CN0566 code for FMCW radar by  on a ADLAM-Pluto that I've hacked by using the steps given by Jon on his youtube channel, But as I don't need two TX/RX channels so I've just increased the pluto's bandwidth.
I've written a spectrum analyzer like code for the pluto bas per my requirements and removed the parts of code that are required by phaser. 
When I run the code the GUI starts but as I press Start the pluto throws a connection time out error at the sdr.rx() line.

I have checked and I my software versions all satisfy the the requirements put forward by Jon Kraft in his youtube video.

I use a bin file to load the data so that there should not be any load on  the PC the parameters I use in the bin file are

fs = 5e6 #sampling frequency
bandwidth = 750e3
on_time = 0.9e-3 # sec
off_time = 0.1e-3
onsamp = 4096
off_samp = 1024


Below is the code for the GUI

from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, QLabel
from PyQt5.QtCore import Qt, QTimer
import numpy as np
import pyqtgraph as pg
import adi
import sys

class RealTimeFFTWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        # file_path = 'calc.iq'
        file_path = 'pylfm.iq'
        self.tx_data = np.fromfile(file_path, dtype=np.complex64)
        self.tx_data *= 2**14

        # Initialize the PlutoSDR
        self.center_frequency = int(435e6)
        self.buffer_size = int(len(self.tx_data))
        self.bandwidth = int(750e3)
        self.sample_rate = int(5e6)
        self.period = 200e-6
        self.buffers = np.array([], dtype = np.complex64)
        try:
            # self.sdr = adi.Pluto(uri="usb:1.8.5")
            self.sdr = adi.Pluto(uri="ip:192.168.2.1")  # Adjust IP address as necessary
            self.sdr.rx_lo = self.center_frequency
            # self.sdr.filter = 'AD936x_LP_666kHz_2MSPS.ftr'
            self.sdr.sample_rate = self.sample_rate
            self.sdr.rx_rf_bandwidth = self.bandwidth
            self.sdr.rx_hardwaregain_chan0 = 0.0
            self.sdr.rx_buffer_size = self.buffer_size
        except Exception as e:
            print(f"Error initializing PlutoSDR: {e}")
            sys.exit(1)

        #   Array to store received samples
        self.received_samples = np.array([], dtype=np.complex64)

        self.sdr.tx_rf_bandwidth = self.bandwidth
        self.sdr.tx_lo = self.center_frequency
        self.sdr._tx_buffer_size = self.buffer_size
        self.sdr.gain_control_mode_chan0 = "manual"
        self.sdr.tx_hardwaregain_chan0 = 0.0  # the range is -90 to 0 dB
        self.sdr.tx_cyclic_buffer = True  # Enable cyclic buffers

        # Configure TDD controller
        sdr_pins = adi.one_bit_adc_dac(uri="ip:192.168.2.1")
        sdr_pins.gpio_tdd_ext_sync = False # If set to True, this enables external capture triggering using the L24N GPIO on the Pluto.  When set to false, an internal trigger pulse will be generated every second
        tdd = adi.tddn(uri="ip:192.168.2.1")
        tdd.enable = False         # disable TDD to configure the registers
        tdd.sync_external = True
        tdd.startup_delay_ms = 0
        tdd.frame_length_ms = self.period/1e3 + 0.2    # each chirp is spaced this far apart
        num_chirps = 1
        tdd.burst_count = num_chirps       # number of chirps in one continuous receive buffer

        tdd.channel[0].enable = True
        tdd.channel[0].polarity = False
        tdd.channel[0].on_ms = 0.01
        tdd.channel[0].off_ms = 0.1
        tdd.channel[1].enable = True
        tdd.channel[1].polarity = False
        tdd.channel[1].on_ms = 0.01
        tdd.channel[1].off_ms = 0.1
        tdd.channel[2].enable = False
        tdd.enable = True


        # # Pluto receive buffer size needs to be greater than total time for all chirps
        # total_time = tdd.frame_length_ms * num_chirps   # time in ms
        # print("Total Time for all Chirps:  ", total_time, "ms")
        # buffer_time = 0
        # power=12
        # while total_time > buffer_time:     
        #     power=power+1
        #     buffer_size = int(2**power) 
        #     buffer_time = buffer_size/self.sdr.sample_rate*1000   # buffer time in ms
        #     if power==23:
        #         break     # max pluto buffer size is 2**23, but for tdd burst mode, set to 2**22
        # print("buffer_size:", buffer_size)
        # self.sdr.rx_buffer_size = buffer_size
        # print("buffer_time:", buffer_time, " ms")
    
        # Set up the GUI
        self.setWindowTitle("SAR GUI")
        self.setGeometry(400, 100, 1200, 800)

        central_widget = pg.GraphicsLayoutWidget()
        self.setCentralWidget(central_widget)
        layout = QHBoxLayout(central_widget)

        plot_panel = QWidget()
        plot_layout = QVBoxLayout(plot_panel)

        # Add side control panel
        side_panel = QWidget()
        side_layout = QVBoxLayout(side_panel)
        side_panel.setFixedWidth(220)

        layout.addWidget(plot_panel)
        layout.addWidget(side_panel)

        # Add label widget for displaying coordinates and data
        self.label = QLabel()
        self.label.setStyleSheet("color: yellow;")
        self.label.setAlignment(Qt.AlignRight) 

        # Add PlotWidgets for FFT and Time Domain
        self.fft_plot = pg.PlotWidget(title="Beat Signal FFT")
        self.timedomain_plot = pg.PlotWidget(title="Time Domain")
        plot_layout.addWidget(self.timedomain_plot)
        plot_layout.addWidget(self.label)
        plot_layout.addWidget(self.fft_plot)   

        # FFT Curve
        self.curve_fft_below = self.fft_plot.plot(pen=pg.mkPen(color="limegreen", width=1.5), fillLevel=-110, fillBrush=(50, 50, 200, 100))
        self.curve_fft_above = self.fft_plot.plot(pen=pg.mkPen(color="limegreen", width=1.5), fillLevel=30, fillBrush=(1, 1, 1, 100))
        self.curve_fft_max_hold = self.fft_plot.plot(pen=pg.mkPen(color="red", width=1.5), fillLevel=30)

        # Time Domain Plot Controls
        self.vLine_time = pg.InfiniteLine(angle=90, movable=False)
        self.hLine_time = pg.InfiniteLine(angle=0, movable=False)
        self.timedomain_plot.addItem(self.vLine_time, ignoreBounds=True)
        self.timedomain_plot.addItem(self.hLine_time, ignoreBounds=True)

        # Set background color to dark gray
        self.fft_plot.setBackground('#222222')
        self.timedomain_plot.setBackground('#222222')

        # Add controls to the side panel
        self.start_button = QPushButton("Start")
        self.start_button.clicked.connect(self.start_update)
        self.start_button.clicked.connect(self.transmit)
        self.start_button.setStyleSheet("background-color: green;")
        side_layout.addWidget(self.start_button)

        self.max_hold_button = QPushButton("Max Hold")
        self.max_hold_button.clicked.connect(self.toggle_max_hold)
        self.max_hold_button.setStyleSheet("background-color: white;")
        side_layout.addWidget(self.max_hold_button)
        side_layout.addStretch()

        # Crosshair lines
        self.vLine = pg.InfiniteLine(angle=90, movable=False)
        self.hLine = pg.InfiniteLine(angle=0, movable=False)
        self.fft_plot.addItem(self.vLine, ignoreBounds=True)
        self.fft_plot.addItem(self.hLine, ignoreBounds=True)

        # Mouse movement event handling
        self.proxy_fft = pg.SignalProxy(self.fft_plot.scene().sigMouseMoved, rateLimit=60, slot=self.mouseMovedFFT)
        self.proxy_time = pg.SignalProxy(self.timedomain_plot.scene().sigMouseMoved, rateLimit=60, slot=self.mouseMovedTime)

        # Variables for real-time update
        self.update_interval = 10  # milliseconds
        self.timer = QTimer()
        self.timer.timeout.connect(self.update_plot)
        self.is_running = False

        self.max_hold_enabled = False
        self.max_hold_data = None

        # Add x and y labels to FFT plot
        self.fft_plot.setLabel('left', text='Power (dB)')
        self.timedomain_plot.setLabel('bottom', text='Time')
        self.timedomain_plot.setLabel('left', text='Amplitude')

    def mouseMovedFFT(self, evt):
        pos = evt[0]  # Using signal proxy turns original arguments into a tuple
        if self.fft_plot.sceneBoundingRect().contains(pos):
            mousePoint = self.fft_plot.plotItem.vb.mapSceneToView(pos)
            x = mousePoint.x()
            y = mousePoint.y()
            # Convert x from plot coordinate to frequency in MHz
            freq_mhz = x
            self.vLine.setPos(x)
            self.hLine.setPos(y)
            self.label.setText(f'Frequency: {freq_mhz:.2f} MHz, Power: {y:.2f} dB')

    def mouseMovedTime(self, evt):
        pos = evt[0]
        if self.timedomain_plot.sceneBoundingRect().contains(pos):
            mousePoint = self.timedomain_plot.plotItem.vb.mapSceneToView(pos)
            x = mousePoint.x()
            y = mousePoint.y()
            # Convert x from plot coordinate to time in seconds
            time_sec = x
            self.vLine_time.setPos(x)
            self.hLine_time.setPos(y)
            self.label.setText(f'Time: {time_sec:.2f} s, Amplitude: {y:.2f}')

    def start_update(self):
        if not self.is_running:
            self.timer.start(self.update_interval)
            self.is_running = True
            self.start_button.setText("Stop")
            self.start_button.setStyleSheet("background-color: red;")
        else:
            self.timer.stop()
            self.is_running = False
            self.start_button.setText("Start")
            self.start_button.setStyleSheet("background-color: green;")

            # Stop the SDR and save data
            self.stop_sdr()
            self.save_received_samples()
            

    def toggle_max_hold(self):
        self.max_hold_enabled = not self.max_hold_enabled
        if self.max_hold_enabled:
            self.max_hold_button.setText("Max Hold (Enabled)")
            self.max_hold_button.setStyleSheet("background-color: gray;")
            self.max_hold_data = None  # Reset max hold data
        else:
            self.max_hold_button.setText("Max Hold")
            self.max_hold_button.setStyleSheet("background-color: white;")

    def transmit(self):
        self.sdr.tx(self.tx_data)

    def update_plot(self):
        rx_samples = self.sdr.rx()
        
        # Handle mismatch in lengths
        if len(rx_samples) != len(self.tx_data):
            print("Mismatch in transmitted and received data lengths.")
            if len(rx_samples) > len(self.tx_data):
                rx_samples = rx_samples[:len(self.tx_data)]
                print("Mismatch Fixed.")
            else:
                rx_samples = np.pad(rx_samples, (0, len(self.tx_data) - len(rx_samples)), 'constant')
                print("Mismatch Fixed.")

        # Store the received samples in the array
        self.received_samples = np.concatenate((self.received_samples, rx_samples))

        # Multiply transmitted and received pulses
        beat_signal = self.tx_data * np.conj(rx_samples)

        window = np.hanning(len(beat_signal))
        beat_signal_windowed = window * beat_signal

        # Time Domain
        iq_time = np.linspace(0, len(rx_samples) / self.sdr.sample_rate, len(rx_samples))
        self.timedomain_plot.plot(iq_time, rx_samples.real, pen='limegreen', clear=True)

        # FFT update
        beat_signal_fft = np.fft.fftshift(np.fft.fft(beat_signal_windowed))

        # Calculate the frequency axis
        freq_axis = np.fft.fftshift(np.fft.fftfreq(len(beat_signal_fft), d=1/self.sdr.sample_rate))
        freq_axis_mhz = freq_axis / 1e6  # Convert to MHz

        # Update plot with axis limits and tick formatting
        self.fft_plot.setXRange(freq_axis_mhz[0], freq_axis_mhz[-1], padding=0)
        tick_step = 5  # Define the step for ticks in MHz
        tick_positions = np.arange(freq_axis_mhz[0], freq_axis_mhz[-1] + tick_step, tick_step)
        tick_labels = [f"{v:.2f} MHz" for v in tick_positions]
        self.fft_plot.getAxis('bottom').setTicks([list(zip(tick_positions, tick_labels))])

        beat_signal_fft_db = 20 * np.log10(np.abs(beat_signal_fft) + 1e-6)  # Added small offset to avoid log(0)
        baseline = np.mean(beat_signal_fft_db[:100])
        baseline_adjustment = -90 - baseline
        beat_signal_fft_db_corrected = beat_signal_fft_db + baseline_adjustment

        threshold = -90  # Adjust threshold level as needed
        beat_signal_fft_db_corrected[beat_signal_fft_db_corrected < threshold] = threshold

        if self.max_hold_enabled:
            if self.max_hold_data is None:
                self.max_hold_data = beat_signal_fft_db_corrected
            else:
                self.max_hold_data = np.maximum(self.max_hold_data, beat_signal_fft_db_corrected)
                self.curve_fft_max_hold.setData(freq_axis_mhz, self.max_hold_data, connect='all', antialias=True)
        else:
            self.curve_fft_max_hold.setData([], [])

        self.curve_fft_above.setData(freq_axis_mhz, beat_signal_fft_db_corrected, connect='all', antialias=True)

    def stop_sdr(self):
        # Stop the transmission and reception
        self.sdr.tx_destroy_buffer()
        self.sdr.rx_destroy_buffer()
        print("SDR transmission and reception stopped.")

    def save_received_samples(self):
        # Save the received samples to a binary file
        output_file = "received_samples.bin"
        self.received_samples = self.received_samples.astype(np.complex64)
        self.received_samples.tofile(output_file)
        print(f"Received samples saved to {output_file}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = RealTimeFFTWindow()
    window.show()
    sys.exit(app.exec_())


Below is the error thrown
Traceback (most recent call last):
  File "/home/oem/ADVR9361/SAR.py", line 443, in update_plot
    rx_samples = self.sdr.rx()
  File "/home/oem/.local/lib/python3.8/site-packages/adi/rx_tx.py", line 248, in rx
    data = self.__rx_complex()
  File "/home/oem/.local/lib/python3.8/site-packages/adi/rx_tx.py", line 213, in __rx_complex
    x = self._rx_buffered_data()
  File "/home/oem/.local/lib/python3.8/site-packages/adi/compat.py", line 149, in _rx_buffered_data
    self._rxbuf.refill()
  File "/home/oem/.local/lib/python3.8/site-packages/iio.py", line 1003, in refill
    _buffer_refill(self._buffer)
  File "/home/oem/.local/lib/python3.8/site-packages/iio.py", line 62, in _check_negative
    raise OSError(-result, _strerror(-result))
TimeoutError: [Errno 110] Connection timed out




small changes in description
[edited by: Deadshot at 10:00 AM (GMT -4) on 9 Aug 2024]
Parents
  • Timeout means that it takes too long for the device to respond. What are the buffersizes/samplerates you expect to receive ? If these are fine, ithen it's probably some of the synchronization pieces that do not trigger properly.

  • Hello ,
    Thank you for your quick reply.

    What are the buffersizes/samplerates you expect to receive ?

    Without the TDD controller part the spectrum analyser runs perfectly, but since I added the TDD controller part the code gives this error.

    If these are fine, ithen it's probably some of the synchronization pieces that do not trigger properly.

    Yeah that's what I too was thinking so I tried to search if there are any sample codes that I can try to run first then I'll try to make changes in my code but I found none,
    And in the video Jon had said that for most cases the values of the attributes in the TDD part will be the same.

    Will you be able to help me debug the code?

    Regards,

  • Yes, if you're not using the GPIO on the Phaser to trigger the start of TDD, then you'll need to send it a software trigger command (tdd.sync_soft).   Try something like this:

    # %% Setup TDD Timing
    # Enable phaser logic in pluto
    gpio = adi.one_bit_adc_dac(sdr_ip)
    time.sleep(0.1)
    gpio.gpio_phaser_enable = True # when true, each channel[1] start outputs a pulse to Pluto L10P pin (TXDATA_1V8 on Phaser schematic)
    time.sleep(0.1)

    # Configure TDD properties
    tdd = adi.tddn(sdr_ip)
    tdd.enable = False # make sure the TDD is disabled before changing properties
    tdd.frame_length_ms = 520 # each burst (L10P toggle) is spaced this apart
    tdd.startup_delay_ms = 0 # startup delay
    tdd.burst_count = 1 # number of bursts in a row
    tdd.channel[0].enable = True
    tdd.channel[0].polarity = False
    tdd.channel[0].on_ms = 0.0
    tdd.channel[0].off_ms = 0.2
    tdd.channel[1].enable = True
    tdd.channel[1].polarity = False
    tdd.channel[1].on_ms = 0.0
    tdd.channel[1].off_ms = 0.2
    tdd.channel[2].enable = False
    tdd.channel[2].polarity = False
    tdd.channel[2].on_ms = 0
    tdd.channel[2].off_raw = 10
    tdd.sync_external = True # enable external sync trigger
    tdd.sync_internal = False # enable the internal sync trigger
    tdd.enable = True # enable TDD engine

    # transmit data from Pluto
    my_sdr._ctx.set_timeout(30000)
    time.sleep(0.5)
    my_sdr._rx_init_channels()
    time.sleep(0.5)
    tdd.enable = False
    tdd.channel[2].enable = True
    my_sdr.tx([iq_chirp, iq_chirp])
    tdd.enable = True

    # Receive data

    tdd.sync_soft = 1
    data = my_sdr.rx()

Reply
  • Yes, if you're not using the GPIO on the Phaser to trigger the start of TDD, then you'll need to send it a software trigger command (tdd.sync_soft).   Try something like this:

    # %% Setup TDD Timing
    # Enable phaser logic in pluto
    gpio = adi.one_bit_adc_dac(sdr_ip)
    time.sleep(0.1)
    gpio.gpio_phaser_enable = True # when true, each channel[1] start outputs a pulse to Pluto L10P pin (TXDATA_1V8 on Phaser schematic)
    time.sleep(0.1)

    # Configure TDD properties
    tdd = adi.tddn(sdr_ip)
    tdd.enable = False # make sure the TDD is disabled before changing properties
    tdd.frame_length_ms = 520 # each burst (L10P toggle) is spaced this apart
    tdd.startup_delay_ms = 0 # startup delay
    tdd.burst_count = 1 # number of bursts in a row
    tdd.channel[0].enable = True
    tdd.channel[0].polarity = False
    tdd.channel[0].on_ms = 0.0
    tdd.channel[0].off_ms = 0.2
    tdd.channel[1].enable = True
    tdd.channel[1].polarity = False
    tdd.channel[1].on_ms = 0.0
    tdd.channel[1].off_ms = 0.2
    tdd.channel[2].enable = False
    tdd.channel[2].polarity = False
    tdd.channel[2].on_ms = 0
    tdd.channel[2].off_raw = 10
    tdd.sync_external = True # enable external sync trigger
    tdd.sync_internal = False # enable the internal sync trigger
    tdd.enable = True # enable TDD engine

    # transmit data from Pluto
    my_sdr._ctx.set_timeout(30000)
    time.sleep(0.5)
    my_sdr._rx_init_channels()
    time.sleep(0.5)
    tdd.enable = False
    tdd.channel[2].enable = True
    my_sdr.tx([iq_chirp, iq_chirp])
    tdd.enable = True

    # Receive data

    tdd.sync_soft = 1
    data = my_sdr.rx()

Children
  • Thank you, Jon the code runs fine now but I still am facing problem I think that I has some thing to do with the synchronization of the TX and the RX channels or the fact that as my distance between the Transmitter-Target-Reciver is very less so the wave is instantaneously received at the receiver, even though I am using a RF cable of 5m length I can see the beat frequency moving all over the frequency spectrum. My gui code also stores each received buffer in a single array which is then stored as a bin file when we stop the transmission, and when I plot that bin file I see that the buffers are either overlapped or some samples are being dropped and not received but that does not happen in your case on the phaser, could you please help me to understand what parameters are causing this error.

    Below is the figure showing the overlap of the buffers

    Regards,
    Deadshot

  • I'm working with the software developers now on a better way to synchronize this, and with greater accuracy.  I think we're close to a solution (new firmware for Pluto).  But in the meantime, you can try this more cumbersome approach:

    for i in range(1):
        tdd.enable = False
        my_sdr.tx_destroy_buffer()
        my_sdr.tx([iq, iq])
        tdd.channel[2].enable = True
        tdd.enable = True
        tdd.sync_soft =True
        data = my_sdr.rx()