Post Go back to editing

ZC706+FSCOMMS5 trying to transmit and receive QPSK symbols using pyadi-iio

Category: Software

Hello I'm trying to transmit and receive (via loopback with a BNC cable) QPSK symbols on channel 0 of the FSCOMMS5 . Unfortunately the received QPSK symbols seem to have 4 levels of amplitude as opposed to 2 (-1,1) for in-phase and quadrature. In the attached image, figures 0 & 1 plot the samples of transmitted in-phase and quadrature QPSK samples respectively. Whereas figures 2 & 3 plot the received samples of in-phase and quadrature QPSK samples respectively. I understand that a phase mismatch is expected, what I don't understand is why the received stream has 4 different amplitude levels when only two were expected.

I'm including the code below. It's basically a slightly modified version of the PlutoSDR transmit and receive example on PySDR.org

import numpy as np
import adi
import matplotlib.pyplot as plt


sample_rate = 1e6 # Hz
center_freq = 915e6 # Hz
num_samps = 500000 # number of samples per call to rx()
sdr = adi.FMComms5()
sdr.tx_enabled_channels = [0]
sdr.rx_enabled_channels = [0]
sdr.sample_rate = int(sample_rate)

# Config Tx
sdr.tx_rf_bandwidth = int(sample_rate) # filter cutoff, just set it to the same as sample rate
sdr.tx_lo = int(center_freq)
sdr.tx_hardwaregain_chan0 = 0 # Increase to increase tx power, valid range is -90 to 0 dB

# Config Rx
sdr.rx_lo = int(center_freq)
sdr.rx_rf_bandwidth = int(sample_rate)
sdr.rx_buffer_size = num_samps
sdr.gain_control_mode_chan0 = 'manual'
sdr.rx_hardwaregain_chan0 = 10.0 # dB, increase to increase the receive gain, but be careful not to saturate the ADC

# Create transmit waveform (QPSK, 16 samples per symbol)
num_symbols = 1000
x_int = np.random.randint(0, 4, num_symbols) # 0 to 3
x_degrees = x_int*360/4.0 + 45 # 45, 135, 225, 315 degrees
x_radians = x_degrees*np.pi/180.0 # sin() and cos() takes in radians
x_symbols = np.cos(x_radians) + 1j*np.sin(x_radians) # this produces our QPSK complex symbols
samples = np.repeat(x_symbols, 16) # 16 samples per symbol (rectangular pulses)
samples *= 2**14 # The PlutoSDR expects samples to be between -2^14 and +2^14, not -1 and +1 like some SDRs
plt.figure(0)
plt.plot(np.real(samples[0:1000]))
plt.figure(1)
plt.plot(np.imag(samples[0:1000]))

# Start the transmitter
sdr.tx_cyclic_buffer = True # Enable cyclic buffers
sdr.tx(samples) # start transmitting

# Clear buffer just to be safe
for i in range (0, 10):
    raw_data = sdr.rx()

# Receive samples
rx_samples = sdr.rx()
print(rx_samples)

# Stop transmitting
sdr.tx_destroy_buffer()

# Calculate power spectral density (frequency domain version of signal)
psd = np.abs(np.fft.fftshift(np.fft.fft(rx_samples)))**2
psd_dB = 10*np.log10(psd)
f = np.linspace(sample_rate/-2, sample_rate/2, len(psd))

# Plot time domain
plt.figure(2)
plt.plot(np.real(rx_samples[:1000]))
plt.figure(3)
plt.plot(np.imag(rx_samples[:1000]))
plt.xlabel("Time")

# Plot freq domain
plt.figure(4)
plt.plot(f/1e6, psd_dB)
plt.xlabel("Frequency [MHz]")
plt.ylabel("PSD")
plt.show()

The figures are also attached.

 

I'd appreciate any pointers as to why the received and transmitted data has a different number of amplitude levels.

Parents
  • Thanks for the link Travis. I knew I was missing the matched filtering step, but wanted to be sure. I added a raised cosine filter at the transmitter and the receiver (I hope I implemented it correctly!). I still seem to be getting 3 amplitude levels (2 + zero). I realize that my question is not strictly about the AD9361 but would appreciate any help. Should there be zero crossings? I also realize that I haven't done any phase synchronization yet. That will come next.

    import numpy as np
    import adi
    import matplotlib.pyplot as plt
    from scipy import signal
    
    sample_rate = 1e6 # Hz
    center_freq = 915e6 # Hz
    num_samps = 500000 # number of samples per call to rx()
    sdr = adi.FMComms5()
    sdr.tx_enabled_channels = [0]
    sdr.rx_enabled_channels = [0]
    sdr.sample_rate = int(sample_rate)
    
    # Config Tx
    sdr.tx_rf_bandwidth = int(sample_rate) # filter cutoff, just set it to the same as sample rate
    sdr.tx_lo = int(center_freq)
    sdr.tx_hardwaregain_chan0 = 0 # Increase to increase tx power, valid range is -90 to 0 dB
    
    # Config Rx
    sdr.rx_lo = int(center_freq)
    sdr.rx_rf_bandwidth = int(sample_rate)
    sdr.rx_buffer_size = num_samps
    sdr.gain_control_mode_chan0 = 'manual'
    sdr.rx_hardwaregain_chan0 = 10.0 # dB, increase to increase the receive gain, but be careful not to saturate the ADC
    
    # Create a raised-cosine filter
    num_taps = 101
    beta = 0.35
    Ts = 16/1e6 # Assume sample rate is 1e6, so sample period is 1e-6, so *symbol* period is 16e-6
    t = np.arange(-1*(num_taps-1)/2, num_taps/2 +1) # if num_taps = 101 =>(-50,51)
    h = 1/Ts*np.sinc(t/Ts) * np.cos(np.pi*beta*t/Ts) / (1 - (2*beta*t/Ts)**2)
    
    # Create transmit waveform (QPSK, 16 samples per symbol)
    num_symbols = 1000
    x_int = np.random.randint(0, 4, num_symbols) # 0 to 3
    x_degrees = x_int*360/4.0 + 45 # 45, 135, 225, 315 degrees
    x_radians = x_degrees*np.pi/180.0 # sin() and cos() takes in radians
    
    x_symbols_i = np.cos(x_radians)
    x_symbols_q = np.sin(x_radians) # this produces our QPSK complex symbols I & Q portions
    
    #upsample (16 sps)
    x_symbols_up_i =  np.repeat(x_symbols_i, 16)
    x_symbols_up_q =  np.repeat(x_symbols_q, 16)
    
    #apply matched filter at the transmitter end
    x_symbols_up_filt_i =np.convolve(h, x_symbols_up_i)
    x_symbols_up_filt_q =np.convolve(h, x_symbols_up_q)
    
    # IQ signal to be transmitted
    x_symbols_up_filt = x_symbols_up_filt_i + 1j*x_symbols_up_filt_q
    
    # The PlutoSDR & FMCOMMS5 expects samples to be between -2^14 and +2^14, not -1 and +1 like some SDRs
    x_symbols_up_filt *= 2**14 
    
    plt.figure(0)
    plt.plot(np.real(x_symbols_up_filt[0:1000]))
    plt.figure(1)
    plt.plot(np.imag(x_symbols_up_filt[0:1000]))
    
    # Start the transmitter
    sdr.tx_cyclic_buffer = True # Enable cyclic buffers
    sdr.tx(x_symbols_up_filt) # start transmitting
    
    # Clear buffer just to be safe
    for i in range (0, 10):
        raw_data = sdr.rx()
    
    # Receive samples
    rx_samples = sdr.rx()
    #print(rx_samples)
    
    # Stop transmitting
    sdr.tx_destroy_buffer()
    
    #apply matched filter on the receiver side should possibly downsample as well ? 
    rx_samples_filt_i =np.convolve(h,np.real(rx_samples))
    rx_samples_filt_q =np.convolve(h,np.imag(rx_samples))
    
    
    #skip the downsampling for now
    #rx_samples_filt_down_i = signal.upfirdn(h=[1],x=rx_samples_filt_i,up=1,down=16)
    #rx_samples_filt_down_q = signal.upfirdn(h=[1],x=rx_samples_filt_q,up=1,down=16)
    
    rx_samples_filt = rx_samples_filt_i  + 1j*rx_samples_filt_q
    
    
    # Calculate power spectral density (frequency domain version of signal)
    psd = np.abs(np.fft.fftshift(np.fft.fft(rx_samples_filt)))**2
    psd_dB = 10*np.log10(psd)
    f = np.linspace(sample_rate/-2, sample_rate/2, len(psd))
    
    #print(rx_samples)
    #Plot time domain
    plt.figure(2)
    plt.plot(np.real(rx_samples_filt[:1000]))
    plt.figure(3)
    plt.plot(np.imag(rx_samples_filt[:1000]))
    plt.xlabel("Time")
    
    #Plot freq domain
    plt.figure(4)
    plt.plot(f/1e6, psd_dB)
    plt.xlabel("Frequency [MHz]")
    plt.ylabel("PSD")
    plt.show()

    BTW I was wondering if there's any resource out there that specifically targets implementing basic digital modulation using pyadi-iio and AD936x based hardware. 'SDR for Engineers' is a great resource for learning the theory; which I admit I'm a bit rusty on, but targets Matlab, and pysdr.org is a great introduction to SDRs as well but it's only introductory.   

Reply
  • Thanks for the link Travis. I knew I was missing the matched filtering step, but wanted to be sure. I added a raised cosine filter at the transmitter and the receiver (I hope I implemented it correctly!). I still seem to be getting 3 amplitude levels (2 + zero). I realize that my question is not strictly about the AD9361 but would appreciate any help. Should there be zero crossings? I also realize that I haven't done any phase synchronization yet. That will come next.

    import numpy as np
    import adi
    import matplotlib.pyplot as plt
    from scipy import signal
    
    sample_rate = 1e6 # Hz
    center_freq = 915e6 # Hz
    num_samps = 500000 # number of samples per call to rx()
    sdr = adi.FMComms5()
    sdr.tx_enabled_channels = [0]
    sdr.rx_enabled_channels = [0]
    sdr.sample_rate = int(sample_rate)
    
    # Config Tx
    sdr.tx_rf_bandwidth = int(sample_rate) # filter cutoff, just set it to the same as sample rate
    sdr.tx_lo = int(center_freq)
    sdr.tx_hardwaregain_chan0 = 0 # Increase to increase tx power, valid range is -90 to 0 dB
    
    # Config Rx
    sdr.rx_lo = int(center_freq)
    sdr.rx_rf_bandwidth = int(sample_rate)
    sdr.rx_buffer_size = num_samps
    sdr.gain_control_mode_chan0 = 'manual'
    sdr.rx_hardwaregain_chan0 = 10.0 # dB, increase to increase the receive gain, but be careful not to saturate the ADC
    
    # Create a raised-cosine filter
    num_taps = 101
    beta = 0.35
    Ts = 16/1e6 # Assume sample rate is 1e6, so sample period is 1e-6, so *symbol* period is 16e-6
    t = np.arange(-1*(num_taps-1)/2, num_taps/2 +1) # if num_taps = 101 =>(-50,51)
    h = 1/Ts*np.sinc(t/Ts) * np.cos(np.pi*beta*t/Ts) / (1 - (2*beta*t/Ts)**2)
    
    # Create transmit waveform (QPSK, 16 samples per symbol)
    num_symbols = 1000
    x_int = np.random.randint(0, 4, num_symbols) # 0 to 3
    x_degrees = x_int*360/4.0 + 45 # 45, 135, 225, 315 degrees
    x_radians = x_degrees*np.pi/180.0 # sin() and cos() takes in radians
    
    x_symbols_i = np.cos(x_radians)
    x_symbols_q = np.sin(x_radians) # this produces our QPSK complex symbols I & Q portions
    
    #upsample (16 sps)
    x_symbols_up_i =  np.repeat(x_symbols_i, 16)
    x_symbols_up_q =  np.repeat(x_symbols_q, 16)
    
    #apply matched filter at the transmitter end
    x_symbols_up_filt_i =np.convolve(h, x_symbols_up_i)
    x_symbols_up_filt_q =np.convolve(h, x_symbols_up_q)
    
    # IQ signal to be transmitted
    x_symbols_up_filt = x_symbols_up_filt_i + 1j*x_symbols_up_filt_q
    
    # The PlutoSDR & FMCOMMS5 expects samples to be between -2^14 and +2^14, not -1 and +1 like some SDRs
    x_symbols_up_filt *= 2**14 
    
    plt.figure(0)
    plt.plot(np.real(x_symbols_up_filt[0:1000]))
    plt.figure(1)
    plt.plot(np.imag(x_symbols_up_filt[0:1000]))
    
    # Start the transmitter
    sdr.tx_cyclic_buffer = True # Enable cyclic buffers
    sdr.tx(x_symbols_up_filt) # start transmitting
    
    # Clear buffer just to be safe
    for i in range (0, 10):
        raw_data = sdr.rx()
    
    # Receive samples
    rx_samples = sdr.rx()
    #print(rx_samples)
    
    # Stop transmitting
    sdr.tx_destroy_buffer()
    
    #apply matched filter on the receiver side should possibly downsample as well ? 
    rx_samples_filt_i =np.convolve(h,np.real(rx_samples))
    rx_samples_filt_q =np.convolve(h,np.imag(rx_samples))
    
    
    #skip the downsampling for now
    #rx_samples_filt_down_i = signal.upfirdn(h=[1],x=rx_samples_filt_i,up=1,down=16)
    #rx_samples_filt_down_q = signal.upfirdn(h=[1],x=rx_samples_filt_q,up=1,down=16)
    
    rx_samples_filt = rx_samples_filt_i  + 1j*rx_samples_filt_q
    
    
    # Calculate power spectral density (frequency domain version of signal)
    psd = np.abs(np.fft.fftshift(np.fft.fft(rx_samples_filt)))**2
    psd_dB = 10*np.log10(psd)
    f = np.linspace(sample_rate/-2, sample_rate/2, len(psd))
    
    #print(rx_samples)
    #Plot time domain
    plt.figure(2)
    plt.plot(np.real(rx_samples_filt[:1000]))
    plt.figure(3)
    plt.plot(np.imag(rx_samples_filt[:1000]))
    plt.xlabel("Time")
    
    #Plot freq domain
    plt.figure(4)
    plt.plot(f/1e6, psd_dB)
    plt.xlabel("Frequency [MHz]")
    plt.ylabel("PSD")
    plt.show()

    BTW I was wondering if there's any resource out there that specifically targets implementing basic digital modulation using pyadi-iio and AD936x based hardware. 'SDR for Engineers' is a great resource for learning the theory; which I admit I'm a bit rusty on, but targets Matlab, and pysdr.org is a great introduction to SDRs as well but it's only introductory.   

Children
  • Should there be zero crossings?

    If you have energy around zero for long periods of time it really means your timing is off and need to fractionally delay the signal as you are "sampling" at the wrong point.

    but targets Matlab, and pysdr.org is a great introduction to SDRs as well but it's only introductory.   

    Not really, at least to my knowledge. The synchronization algorithms should be relatively easy to translate from the SDR for Engineers book, just more of the issues are the filters. Python is really limited on the comms collateral IMHO and most of the open-source stuff exists in GNU Radio.

    This is a good source but does not really discuss comms in detail: https://greenteapress.com/wp/think-dsp/

    If you are looking for something more analytical I would go with Michael Rice's book, it is MATLAB but does not use really anything besides base MATLAB.

    -Travis

  • Thanks for the book suggestions I  have acquired Michael Rice's book. I will read all three. I will also have a look at the GNU radio stuff.