|
|
|
@ -2,10 +2,8 @@ |
|
|
|
|
# Copyright 2005,2006,2007 Free Software Foundation, Inc. |
|
|
|
|
# |
|
|
|
|
# OP25 4-Level Modulator Block |
|
|
|
|
# Copyright 2009, 2014 Max H. Parke KA1RBI |
|
|
|
|
# Copyright 2009, 2010, 2011, 2012, 2013, 2014, 2015 Max H. Parke KA1RBI |
|
|
|
|
# |
|
|
|
|
# coeffs for shaping and cosine filters from Eric Ramsey thesis |
|
|
|
|
# |
|
|
|
|
# This file is part of GNU Radio and part of OP25 |
|
|
|
|
# |
|
|
|
|
# This is free software; you can redistribute it and/or modify |
|
|
|
@ -34,13 +32,69 @@ from gnuradio import filter, digital, blocks |
|
|
|
|
from gnuradio.eng_option import eng_option |
|
|
|
|
from optparse import OptionParser |
|
|
|
|
import math |
|
|
|
|
from math import sin, cos, pi |
|
|
|
|
import numpy as np |
|
|
|
|
|
|
|
|
|
# default values (used in __init__ and add_options) |
|
|
|
|
_def_output_sample_rate = 48000 |
|
|
|
|
_def_excess_bw = 0.2 |
|
|
|
|
_def_symbol_rate = 4800 |
|
|
|
|
_def_reverse = False |
|
|
|
|
_def_verbose = False |
|
|
|
|
_def_log = False |
|
|
|
|
_def_span = 13 #desired number of impulse response coeffs, in units of symbols |
|
|
|
|
|
|
|
|
|
def transfer_function_rx(): |
|
|
|
|
# p25 c4fm de-emphasis filter |
|
|
|
|
# Specs undefined above 2,880 Hz. It would be nice to have a sharper |
|
|
|
|
# rolloff, but this filter is cheap enough.... |
|
|
|
|
xfer = [] # frequency domain transfer function |
|
|
|
|
for f in xrange(0,4800): |
|
|
|
|
# D(f) |
|
|
|
|
t = pi * f / 4800 |
|
|
|
|
if t < 1e-6: |
|
|
|
|
df = 1.0 |
|
|
|
|
else: |
|
|
|
|
df = sin (t) / t |
|
|
|
|
xfer.append(df) |
|
|
|
|
return xfer |
|
|
|
|
|
|
|
|
|
def transfer_function_tx(): |
|
|
|
|
xfer = [] # frequency domain transfer function |
|
|
|
|
for f in xrange(0, 2881): # specs cover 0 - 2,880 Hz |
|
|
|
|
# H(f) |
|
|
|
|
if f < 1920: |
|
|
|
|
hf = 1.0 |
|
|
|
|
else: |
|
|
|
|
hf = 0.5 + 0.5 * cos (2 * pi * float(f) / 1920.0) |
|
|
|
|
# P(f) |
|
|
|
|
t = pi * f / 4800.0 |
|
|
|
|
if t < 1e-6: |
|
|
|
|
pf = 1 |
|
|
|
|
else: |
|
|
|
|
pf = t / sin (t) |
|
|
|
|
# time domain convolution == frequency domain multiplication |
|
|
|
|
xfer.append(pf * hf) |
|
|
|
|
return xfer |
|
|
|
|
|
|
|
|
|
class c4fm_taps(object): |
|
|
|
|
"""Generate filter coefficients as per P25 C4FM spec""" |
|
|
|
|
def __init__(self, filter_gain = 1.0, sample_rate=_def_output_sample_rate, symbol_rate=_def_symbol_rate, span=_def_span, generator=transfer_function_tx): |
|
|
|
|
self.sample_rate = sample_rate |
|
|
|
|
self.symbol_rate = symbol_rate |
|
|
|
|
self.filter_gain = filter_gain |
|
|
|
|
self.sps = int(sample_rate / symbol_rate) |
|
|
|
|
self.ntaps = (self.sps * span) | 1 |
|
|
|
|
self.generator = generator |
|
|
|
|
|
|
|
|
|
def generate(self): |
|
|
|
|
impulse_response = np.fft.fftshift(np.fft.irfft(self.generator(), self.sample_rate)) |
|
|
|
|
start = np.argmax(impulse_response) - (self.ntaps-1) / 2 |
|
|
|
|
coeffs = impulse_response[start: start+self.ntaps] |
|
|
|
|
gain = self.filter_gain / sum(coeffs) |
|
|
|
|
return coeffs * gain |
|
|
|
|
|
|
|
|
|
def generate_code(self, varname='taps'): |
|
|
|
|
return '%s = [\n\t%s]' % (varname, ',\n\t'.join(['%10.6e' % f for f in self.generate()])) |
|
|
|
|
|
|
|
|
|
# ///////////////////////////////////////////////////////////////////////////// |
|
|
|
|
# modulator |
|
|
|
@ -50,7 +104,6 @@ class p25_mod_bf(gr.hier_block2): |
|
|
|
|
|
|
|
|
|
def __init__(self, |
|
|
|
|
output_sample_rate=_def_output_sample_rate, |
|
|
|
|
excess_bw=_def_excess_bw, |
|
|
|
|
reverse=_def_reverse, |
|
|
|
|
verbose=_def_verbose, |
|
|
|
|
log=_def_log): |
|
|
|
@ -66,8 +119,6 @@ class p25_mod_bf(gr.hier_block2): |
|
|
|
|
|
|
|
|
|
@param output_sample_rate: output sample rate |
|
|
|
|
@type output_sample_rate: integer |
|
|
|
|
@param excess_bw: Root-raised cosine filter excess bandwidth |
|
|
|
|
@type excess_bw: float |
|
|
|
|
@param reverse: reverse polarity flag |
|
|
|
|
@type reverse: bool |
|
|
|
|
@param verbose: Print information about modulator? |
|
|
|
@ -84,7 +135,6 @@ class p25_mod_bf(gr.hier_block2): |
|
|
|
|
lcm = gru.lcm(input_sample_rate, output_sample_rate) |
|
|
|
|
self._interp_factor = int(lcm // input_sample_rate) |
|
|
|
|
self._decimation = int(lcm // output_sample_rate) |
|
|
|
|
self._excess_bw = excess_bw |
|
|
|
|
|
|
|
|
|
mod_map = [1.0/3.0, 1.0, -(1.0/3.0), -1.0] |
|
|
|
|
self.C2S = digital.chunks_to_symbols_bf(mod_map) |
|
|
|
@ -93,27 +143,7 @@ class p25_mod_bf(gr.hier_block2): |
|
|
|
|
else: |
|
|
|
|
self.polarity = blocks.multiply_const_ff( 1) |
|
|
|
|
|
|
|
|
|
ntaps = 11 * self._interp_factor |
|
|
|
|
rrc_taps = filter.firdes.root_raised_cosine( |
|
|
|
|
self._interp_factor, # gain (since we're interpolating by sps) |
|
|
|
|
lcm, # sampling rate |
|
|
|
|
input_sample_rate, # symbol rate |
|
|
|
|
self._excess_bw, # excess bandwidth (roll-off factor) |
|
|
|
|
ntaps) |
|
|
|
|
|
|
|
|
|
# rrc_coeffs work slightly differently: each input sample |
|
|
|
|
# (from mod_map above) at 4800 rate, then 9 zeros are inserted |
|
|
|
|
# to bring to 48000 rate, then this filter is applied: |
|
|
|
|
# rrc_filter = gr.fir_filter_fff(1, rrc_coeffs) |
|
|
|
|
# FIXME: how to insert the 9 zero samples using gr ? |
|
|
|
|
# rrc_coeffs = [0, -0.003, -0.006, -0.009, -0.012, -0.014, -0.014, -0.013, -0.01, -0.006, 0, 0.007, 0.014, 0.02, 0.026, 0.029, 0.029, 0.027, 0.021, 0.012, 0, -0.013, -0.027, -0.039, -0.049, -0.054, -0.055, -0.049, -0.038, -0.021, 0, 0.024, 0.048, 0.071, 0.088, 0.098, 0.099, 0.09, 0.07, 0.039, 0, -0.045, -0.091, -0.134, -0.17, -0.193, -0.199, -0.184, -0.147, -0.085, 0, 0.105, 0.227, 0.36, 0.496, 0.629, 0.751, 0.854, 0.933, 0.983, 1, 0.983, 0.933, 0.854, 0.751, 0.629, 0.496, 0.36, 0.227, 0.105, 0, -0.085, -0.147, -0.184, -0.199, -0.193, -0.17, -0.134, -0.091, -0.045, 0, 0.039, 0.07, 0.09, 0.099, 0.098, 0.088, 0.071, 0.048, 0.024, 0, -0.021, -0.038, -0.049, -0.055, -0.054, -0.049, -0.039, -0.027, -0.013, 0, 0.012, 0.021, 0.027, 0.029, 0.029, 0.026, 0.02, 0.014, 0.007, 0, -0.006, -0.01, -0.013, -0.014, -0.014, -0.012, -0.009, -0.006, -0.003, 0] |
|
|
|
|
|
|
|
|
|
self.rrc_filter = filter.interp_fir_filter_fff(self._interp_factor, rrc_taps) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# FM pre-emphasis filter |
|
|
|
|
shaping_coeffs = [-0.018, 0.0347, 0.0164, -0.0064, -0.0344, -0.0522, -0.0398, 0.0099, 0.0798, 0.1311, 0.121, 0.0322, -0.113, -0.2499, -0.3007, -0.2137, -0.0043, 0.2825, 0.514, 0.604, 0.514, 0.2825, -0.0043, -0.2137, -0.3007, -0.2499, -0.113, 0.0322, 0.121, 0.1311, 0.0798, 0.0099, -0.0398, -0.0522, -0.0344, -0.0064, 0.0164, 0.0347, -0.018] |
|
|
|
|
self.shaping_filter = filter.fir_filter_fff(1, shaping_coeffs) |
|
|
|
|
self.filter = filter.interp_fir_filter_fff(self._interp_factor, c4fm_taps(sample_rate=output_sample_rate).generate()) |
|
|
|
|
|
|
|
|
|
if verbose: |
|
|
|
|
self._print_verbage() |
|
|
|
@ -121,16 +151,15 @@ class p25_mod_bf(gr.hier_block2): |
|
|
|
|
if log: |
|
|
|
|
self._setup_logging() |
|
|
|
|
|
|
|
|
|
self.connect(self, self.C2S, self.polarity, self.rrc_filter, self.shaping_filter) |
|
|
|
|
self.connect(self, self.C2S, self.polarity, self.filter) |
|
|
|
|
if (self._decimation > 1): |
|
|
|
|
self.decimator = filter.rational_resampler_fff(1, self._decimation) |
|
|
|
|
self.connect(self.shaping_filter, self.decimator, self) |
|
|
|
|
self.connect(self.filter, self.decimator, self) |
|
|
|
|
else: |
|
|
|
|
self.connect(self.shaping_filter, self) |
|
|
|
|
self.connect(self.filter, self) |
|
|
|
|
|
|
|
|
|
def _print_verbage(self): |
|
|
|
|
print "\nModulator:" |
|
|
|
|
print "RRS roll-off factor: %f" % self._excess_bw |
|
|
|
|
print "interpolation: %d decimation: %d" %(self._interp_factor, self._decimation) |
|
|
|
|
|
|
|
|
|
def _setup_logging(self): |
|
|
|
@ -139,8 +168,8 @@ class p25_mod_bf(gr.hier_block2): |
|
|
|
|
gr.file_sink(gr.sizeof_float, "tx_chunks2symbols.dat")) |
|
|
|
|
self.connect(self.polarity, |
|
|
|
|
gr.file_sink(gr.sizeof_float, "tx_polarity.dat")) |
|
|
|
|
self.connect(self.rrc_filter, |
|
|
|
|
gr.file_sink(gr.sizeof_float, "tx_rrc_filter.dat")) |
|
|
|
|
self.connect(self.filter, |
|
|
|
|
gr.file_sink(gr.sizeof_float, "tx_filter.dat")) |
|
|
|
|
self.connect(self.shaping_filter, |
|
|
|
|
gr.file_sink(gr.sizeof_float, "tx_shaping_filter.dat")) |
|
|
|
|
if (self._decimation > 1): |
|
|
|
@ -151,8 +180,6 @@ class p25_mod_bf(gr.hier_block2): |
|
|
|
|
""" |
|
|
|
|
Adds QPSK modulation-specific options to the standard parser |
|
|
|
|
""" |
|
|
|
|
parser.add_option("", "--excess-bw", type="float", default=_def_excess_bw, |
|
|
|
|
help="set RRC excess bandwith factor [default=%default] (PSK)") |
|
|
|
|
add_options=staticmethod(add_options) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|