Skip to content

Instantly share code, notes, and snippets.

@boxlor
Created January 14, 2017 07:17
Show Gist options
  • Save boxlor/02deb422096e98a63d284499c727f53c to your computer and use it in GitHub Desktop.
Save boxlor/02deb422096e98a63d284499c727f53c to your computer and use it in GitHub Desktop.
SDR-related utility to compute channels for PFB channelizers.
"""
Computes channelization scheme for a set of input frequencies.
Example output:
$ python channelize.py
Input frequency spacing:
freq[00] 854037500
freq[01] 854212500 dfreq=175000.000000
freq[02] 854312500 dfreq=100000.000000
freq[03] 854362500 dfreq=50000.000000
freq[04] 854512500 dfreq=150000.000000
freq[05] 854962500 dfreq=450000.000000
freq[06] 855487500 dfreq=525000.000000
freq[07] 855737500 dfreq=250000.000000
freq[08] 856212500 dfreq=475000.000000
freq[09] 856237500 dfreq=25000.000000
freq[10] 856587500 dfreq=350000.000000
freq[11] 856687500 dfreq=100000.000000
freq[12] 856962500 dfreq=275000.000000
freq[13] 857212500 dfreq=250000.000000
freq[14] 857237500 dfreq=25000.000000
freq[15] 858212500 dfreq=975000.000000
freq[16] 858237500 dfreq=25000.000000
freq[17] 858687500 dfreq=450000.000000
freq[18] 859212500 dfreq=525000.000000
freq[19] 859237500 dfreq=25000.000000
*** Forcing number of channels to be odd by adding a channel.
*** NOTE: Center channel (ch[19], pfb_ch[00]) may have issues if there is I/Q DC offset leakage.
group[0]: low: 854025000.000000, high: 855000000.000000, center: 854512500.000000, samp_rate: 975000.000000, num_chans: 39, chan_width: 25000
ch[00], pfb_ch[20]: 854037500.000000
ch[07], pfb_ch[27]: 854212500.000000
ch[11], pfb_ch[31]: 854312500.000000
ch[13], pfb_ch[33]: 854362500.000000
ch[19], pfb_ch[00]: 854512500.000000
ch[37], pfb_ch[18]: 854962500.000000
group[1]: low: 855475000.000000, high: 857250000.000000, center: 856362500.000000, samp_rate: 1775000.000000, num_chans: 71, chan_width: 25000
ch[00], pfb_ch[36]: 855487500.000000
ch[10], pfb_ch[46]: 855737500.000000
ch[29], pfb_ch[65]: 856212500.000000
ch[30], pfb_ch[66]: 856237500.000000
ch[44], pfb_ch[09]: 856587500.000000
ch[48], pfb_ch[13]: 856687500.000000
ch[59], pfb_ch[24]: 856962500.000000
ch[69], pfb_ch[34]: 857212500.000000
ch[70], pfb_ch[35]: 857237500.000000
*** Forcing number of channels to be odd by adding a channel.
group[2]: low: 858200000.000000, high: 859275000.000000, center: 858737500.000000, samp_rate: 1075000.000000, num_chans: 43, chan_width: 25000
ch[00], pfb_ch[22]: 858212500.000000
ch[01], pfb_ch[23]: 858237500.000000
ch[19], pfb_ch[41]: 858687500.000000
ch[40], pfb_ch[19]: 859212500.000000
ch[41], pfb_ch[20]: 859237500.000000
"""
M = 1.0e6
# Example P25 system:
# http://www.radioreference.com/apps/db/?sid=8285
# Not included: control channels.
INPUT_FREQS = [854.0375 * M,
854.2125 * M,
854.3125 * M,
854.3625 * M,
854.5125 * M,
854.9625 * M,
855.4875 * M,
855.7375 * M,
856.2125 * M,
856.2375 * M,
856.5875 * M,
856.6875 * M,
856.9625 * M,
857.2125 * M,
857.2375 * M,
858.2125 * M,
858.2375 * M,
858.6875 * M,
859.2125 * M,
859.2375 * M]
# See note below about defining groups by hand. I did this because I liked my grouping
# better than what define_groups produced.
GROUPS = [[], [], []]
# group0: 37 channels w/25kHz spacing
GROUPS[0] = [854.0375 * M,
854.2125 * M,
854.3125 * M,
854.3625 * M,
854.5125 * M,
854.9625 * M]
# group1: 71 channels w/25kHz spacing
GROUPS[1] = [855.4875 * M,
855.7375 * M,
856.2125 * M,
856.2375 * M,
856.5875 * M,
856.6875 * M,
856.9625 * M,
857.2125 * M,
857.2375 * M]
# group2: 42 channels w/25kHz spacing
GROUPS[2] = [858.2125 * M,
858.2375 * M,
858.6875 * M,
859.2125 * M,
859.2375 * M]
MAX_BW_HZ = 2.54 * M # RTL max reliable sample rate = 2.56 MS/s
def test_chan_width(freqs, chan_width):
"""
Given a sorted list of frequencies and possible channel width, returns
True if some multiple of chan_width lands exactly on each frequency in the
list.
"""
freq_idx = 0
current_freq = freqs[0]
while(True):
if(freqs[freq_idx] == current_freq):
freq_idx += 1
else:
# Try next channel
current_freq += chan_width
# Check for done
# Matched all freqs
if(freq_idx == len(freqs)):
return True
# Stop searching after maximum frequency
if(current_freq > freqs[freq_idx]):
return False
def compute_chan_width(freqs):
"""
Given a list of frequencies, returns the largest channel size that would
result in all frequencies being at the center of a channel.
"""
freqs.sort()
last_freq = None
dfreqs = []
for freq in freqs:
if(last_freq != None):
dfreq = (freq - last_freq)
dfreqs.append(dfreq)
# print freq, dfreq
# else:
# print freq, 0
last_freq = freq
# Largest possible channel width that could work is the minimum spacing
# between frequencies. This is the starting point.
chan_width = min(dfreqs)
# Iteratively search for a channel spacing that works. Might end up at 1Hz!
while((test_chan_width(freqs, chan_width) is False) and (chan_width > 0)):
chan_width -= 1
num_chans = int((freqs[-1] - freqs[0]) / chan_width) + 1
# print "chan_width: %f, num_chans: %d" % (chan_width, num_chans)
return chan_width, num_chans
def define_groups(freqs, max_bw_hz):
"""
Greedily divide list of input frequencies into groups of at most max_bw_hz.
"""
freqs.sort()
groups = []
low_freq = freqs[0]
group = []
for freq in freqs:
# for i, freq in enumerate(freqs):
if((freq - low_freq) <= max_bw_hz):
group.append(freq)
else:
# Append previous group
groups.append(group)
group = [freq]
low_freq = freq
if(len(group) != 0):
groups.append(group)
msg = "Partitioned %d input frequencies into %d groups "
msg += "based on your requirement for maximum group b/w of %f Hz:"
msg %= (len(freqs), len(groups), MAX_BW_HZ)
print msg
for i, group in enumerate(groups):
msg = "group[%d]: %d frequencies spanning %f Hz: %s"
msg %= (i, len(group), group[-1] - group[0], repr(group))
print msg
return groups
def show_spacing(freqs):
"""
Prints freqs list and delta frequency between each item in the list.
Assumes list is sorted.
"""
print "Input frequency spacing:"
last_freq = None
for i, freq in enumerate(freqs):
if(last_freq != None):
dfreq = freq - last_freq
msg = "\tfreq[%02d] %d\tdfreq=%f"
msg %= (i, freq, dfreq)
print msg
else:
msg = "\tfreq[%02d] %d"
msg %= (i, freq)
print msg
last_freq = freq
def main():
show_spacing(INPUT_FREQS)
# Originally the program divided the frequency list into groups based on
# the max b/w of each RTL dongle, but it's not as good as doing it
# manually. The define_groups routine should be re-written such that it
# divides the groups at the largest gaps, to reduce overall system b/w.
# But for now they are manually allocated at the top of the file.
# groups = define_groups(INPUT_FREQS, MAX_BW_HZ)
groups = GROUPS
for i, group in enumerate(groups):
chan_width, num_chans = compute_chan_width(group)
# Get channel numbers - depending on whether the center frequency is
# used these may change. Also these numbers are zero-based but the
# PFB channelizer in gnuradio starts with zero in the middle of the
# tuned band, so they always need to be remapped.
# Preliminary channel list
base = group[0]
chans = []
for freq in group:
chan = int((freq - base)/chan_width)
chans.append(chan)
if(num_chans & 1 == 0):
# Even number of channels: make odd by adding another channel so
# that center of channels lines up with desired signal in PFB
# output.
num_chans += 1
print "*** Forcing number of channels to be odd by adding a channel."
# Compute shift for channel remapping for PFB
shift = (num_chans+1)/2
pfb_chan_map = range(num_chans)[shift:] + range(num_chans)[:shift]
center_chan = (num_chans - 1) / 2
if(center_chan in chans):
# FIXME: Perhaps do something to avoid using the enter channel
# because of the I/Q DC leakage artifact?
msg = "*** NOTE: Center channel (ch[%02d], pfb_ch[%02d]) "
msg += " may have issues if there is I/Q DC offset leakage."
msg %= (center_chan, pfb_chan_map[center_chan])
print msg
tune_low = group[0] - (chan_width / 2.0)
tune_high = tune_low + (num_chans * chan_width)
tune_freq = (tune_high + tune_low) / 2.0
samp_rate = tune_high - tune_low
msg = 'group[%d]: low: %f, high: %f, center: %f, samp_rate: %f, num_chans: %d, chan_width: %d'
msg %= (i, tune_low, tune_high, tune_freq, samp_rate, num_chans, chan_width)
print msg
# remapped_chans = []
# for chan in chans:
for freq in group:
chan = int((freq - base)/chan_width)
# chans.append(chan)
# remapped_chans.append(pfb_chan_map[chan])
print '\tch[%02d], pfb_ch[%02d]: %f' % (chan, pfb_chan_map[chan], freq)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment