# Spectrum of a baseband digital signal

I'm building a Continuous-Phase BFSK Transmitter using Python and PlutoSDR. Something that I don't understand is the spectrum of the up-sampled baseband signal - the digital 0's and 1's over-sampled by the Sample per Symbol (fs/sr = 1000KHz/5KHz = 200) ratio: The graphs describe the signal in time and frequency, baseband, modulated and demodulated, now the thing I don't understand is the upper right plot, the "PSD of Baseband Signal" graph, why there are what seems to be duplications of the main lobe? all of them span over a 5KHz BW (Symbol rate = 5KHz, fs = 100KHz). all of them together are the baseband signal? I'd think the signal is just the main lobe in the middle. The other graphs show the signal in the time domain (upper left plot), spectrogram, signal after CP-BFSK modulation, Demodulated data (using Quadrature demodulation) and filtered Demodulated Data in time, and their spectrums.

adding the python code for reference:

from binascii import crc32
import sys
from matplotlib import gridspec
sys.path.append(r"C:\Users\User\Desktop\Python_Scripts\PlutoSDR")
import mySDRlib as sdr
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec

def cp_bfsk_mod(bin_data,fs,sr,f0,f1,cp_flag=1):
samples_per_symbol = int(fs/sr)
num_of_samples = len(bin_data)*samples_per_symbol
samples_array = np.arange(num_of_samples)
bits_as_freqs = [f0 if b==0 else f1 for b in bin_data]
upsampled_bits_as_freqs = np.repeat(bits_as_freqs,samples_per_symbol)

if cp_flag:
arg = np.cumsum(2*np.pi*upsampled_bits_as_freqs/fs)
else:
arg = 2*np.pi*upsampled_bits_as_freqs*samples_array/fs

modulated_bfsk_data = np.exp(1j*arg)
return modulated_bfsk_data

def apply_protocol(bin_data):
preamble = [1,0]*8 # 16
start_flag = [1,0,1,1,1,1,0,0,1,0,1,1,1,1,0,0] # 0xBCBC # 16
byte_data = bytes(bin_data)
crc = crc32(byte_data)
crc_bin = sdr.int2bin(crc,32)
crc_bin_list = [int(bit) for bit in list(crc_bin)] # 32
whole_message = preamble+start_flag+bin_data+crc_bin_list
return whole_message

# Globals
PLOTS = 1
TX = 0
PULSE_SHAPING = 1
REPEAT_TX = 10
NFFT = 1024
WINDOWS_FUNC = sdr.firdes.WIN_BLACKMAN_HARRIS

# Constants
fs = 1000e3
sr = 5e3
sps = int(fs/sr)
f0 = -100e3
f1 = -f0

# Data
ascii_data = "Hello World!"*25
bin_data = sdr.ascii2bin(ascii_data)
protocol_data = apply_protocol(bin_data)
bin_data = protocol_data

# Modulated Data
sampled_bin_data = np.repeat([2*b-1 for b in bin_data],sps)
modulated_binary_data = cp_bfsk_mod(bin_data,fs,sr,f0,f1,1)
symbol_time_vec = np.arange(0,len(modulated_binary_data),sps)

# Transmitter Filter
low_cutoff = 97.5e3
high_cutoff = 102.5e3
transition_bw = 5e3
filter_gain = 1

# Transmitter
fcRx=1433e6
rx_gain_dB=30
tx_gain_dB=-50
# fcTx=434e6
fcTx=915e6

if TX:
pluto_sdr = sdr.pluto_init(fs,buff_size=1,fcRx=fcRx,rx_gain_dB=rx_gain_dB,fcTx=fcTx)
# sdr.pluto_tx_x_sec(pluto_sdr,modulated_binary_data,fs,fcTx,-80,3)
pluto_sdr.sample_rate = int(fs)
pluto_sdr.tx_rf_bandwidth = int(fs)
pluto_sdr.tx_lo = int(fcTx)
pluto_sdr.tx_hardwaregain_chan0 = tx_gain_dB
if PULSE_SHAPING:
samples_to_tx = sdr.apply_bandpass_filter(modulated_binary_data,fs,low_cutoff,\
high_cutoff,transition_bw,filter_gain,"real",WINDOWS_FUNC)
else:
samples_to_tx = modulated_binary_data
samples_to_tx *= 2**14

print(f'Transmitting...')
for i in range(REPEAT_TX):
pluto_sdr.tx(samples_to_tx)
print(f'Finished Transmitting.')

if PLOTS:
# Testing Demodulation
fvec = np.linspace(-fs//2,fs//2,len(modulated_binary_data))
modulated_binary_data = sdr.apply_bandpass_filter(modulated_binary_data,fs,low_cutoff,\
high_cutoff,transition_bw,filter_gain,"real",WINDOWS_FUNC)

fig1 = plt.figure('BFSK FIR BPF Modulated Data')
r,c = 4,2
grid = GridSpec(nrows=r,ncols=c)

ax0.plot(modulated_binary_data.real,'b')
ax0.plot(sampled_bin_data,'--r')
ax0.plot(symbol_time_vec,np.zeros(len(symbol_time_vec)),'gx')
ax0.set_xlabel('Samples')
ax0.set_ylabel('Amplitude')
ax0.set_title('Binary & Modulated Data')
ax0.set_xlim([-1e2,1.2e3])
ax0.legend(['Modulated Signal',"Binary Data",'Symbol Time Slots'],loc="upper right")

# ax1.plot(fvec,sdr.PSD(sampled_bin_data,len(sampled_bin_data),True))
ax1.plot(np.linspace(-fs//2,fs//2,len(sampled_bin_data)),sdr.PSD(sampled_bin_data,len(sampled_bin_data),True))
ax1.set_xlabel('Freq. [Hz]')
ax1.set_ylabel('Magnitude [W/Hz]')
ax1.set_title('PSD of Baseband Signal')
ax1.set_xlim([-50e3,50e3])
ax1.set_ylim([-120,10])

ax2.specgram(modulated_binary_data,NFFT=NFFT,Fs=fs)
ax2.set_title('Spectrogram')

ax3.plot(fvec,sdr.PSD(modulated_binary_data,len(modulated_binary_data),True))
ax3.set_xlabel('Freq. [Hz]')
ax3.set_ylabel('Magnitude [W/Hz]')
ax3.set_title('PSD of Passband Signal')

filtered_demodulated_data = sdr.apply_lowpass_filter(demodulated_data,fs,5e3,1e3)

ax4.plot(demodulated_data)
ax4.plot(filtered_demodulated_data,'orange')
ax4.set_xlabel('Samples')
ax4.set_ylabel('Amplitude')
ax4.set_title('Demodulated Data')
ax4.legend(['Demodulated Data','Filtered Demodulated Data'],loc="upper right")
ax4.set_xlim([-1e3,10e3])

fvec_demod = np.linspace(-fs//2,fs//2,len(demodulated_data))
ax5.plot(fvec_demod,sdr.PSD(demodulated_data,len(demodulated_data),True))
ax5.plot(fvec_demod,sdr.PSD(filtered_demodulated_data,len(filtered_demodulated_data),True),'orange')
ax5.set_xlabel('Freq. [Hz]')
ax5.set_ylabel('Magnitude [W/Hz]')
ax5.set_title('PSD of Demodulated Signal')
ax5.legend(['Demodulated Data','Filtered Demodulated Data'],loc="upper right")
ax5.set_xlim([-50e3,50e3])

fig1.set_tight_layout(True)

plt.show()


• that's not proper (or at least, reasonably good) oversampling/upsampling! where did you get this method from? May 16, 2022 at 22:00
• Actually I've seen it here on dsp stackexchange: dsp.stackexchange.com/questions/80768/… May 17, 2022 at 6:49

The spectral shape mainly hails from overlaying an f0- and f1-shifte sinc as formed by the upsampling filter – which is a boxcar filter in your case.

So, basically, you're doing a bad version of upsampling: instead of repeating samples samples_per_symbol times, you should insert samples_per_symbol - 1 zeros after every sample:

upsampled_bits_as_freqs = np.zeros(len(bits_as_freqs) * samples_per_symbol)
upsampled_bits_as_freqs[::samples_per_symbol] = bits_as_fres


and then proceed to apply a 1/samples_per_symbol-bandwidth lowpass filter to remove the images that creates.

Your repeat approach does the same – just that instead of an appropriate low-pass filter, you apply a filter that's samples_per_symbol ones. The Fourier transform of that rectangle is the sinc shape you see!

• Thanks for your answer, I've tried your suggestion, but I don't understand the results :( i.stack.imgur.com/7oyqF.png I changed: upsampled_bits_as_freqs = np.zeros(len(bits_as_freqs) * samples_per_symbol) upsampled_bits_as_freqs[::samples_per_symbol] = bits_as_freqs ... and sampled_bin_data = np.zeros(len(bin_data)*sps) sampled_bin_data[::sps] = bin_data the upper right spectrum shows the spectrum of sampled_bin_data, while the upper left shows it in time. May 17, 2022 at 6:55
• what did you use as low-pass filter? May 17, 2022 at 15:58
• Actually my bad, I didn't use one, but looking at the spectrum, it seems there is nothing to filter? The role of the LPF you mentioned will be like a pulse-shaping filter? to make those impulses have some kind of a pulse-shape? May 17, 2022 at 20:51
• no, it's an anti-imaging filter May 17, 2022 at 21:24
• Oh! I understood my mistake, thanks for explaining! :) May 21, 2022 at 13:34