op25/op25/gr-op25_repeater/apps/tx/op25_c4fm_mod.py

303 lines
10 KiB
Python
Raw Normal View History

2014-06-13 00:46:23 +00:00
#
# Copyright 2005,2006,2007 Free Software Foundation, Inc.
#
# OP25 4-Level Modulator Block
2017-03-10 15:43:49 +00:00
# Copyright 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Max H. Parke KA1RBI
#
# GMSK code from gnuradio gr-digital/python/digital/gmsk.py
# gmsk.py is Copyright 2005-2007,2012 Free Software Foundation, Inc.
2014-06-13 00:46:23 +00:00
#
# This file is part of GNU Radio and part of OP25
#
# This is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
#
# It is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this; see the file COPYING. If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
#
"""
2017-03-10 15:43:49 +00:00
P25 C4FM pre-modulation block
with additions for RRC (for dmr/ysf) and dstar gmsk
2014-06-13 00:46:23 +00:00
"""
2022-05-20 00:21:21 +00:00
from gnuradio import gr, eng_notation
2014-06-13 00:46:23 +00:00
from gnuradio.digital import modulation_utils
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
2014-06-13 00:46:23 +00:00
# default values (used in __init__ and add_options)
_def_output_sample_rate = 48000
_def_symbol_rate = 4800
2014-06-13 00:46:23 +00:00
_def_reverse = False
_def_verbose = False
_def_log = False
_def_span = 13 #desired number of impulse response coeffs, in units of symbols
2017-03-10 15:43:49 +00:00
_def_gmsk_span = 4
_def_bt = 0.25
2019-05-11 20:41:49 +00:00
def transfer_function_rx(symbol_rate=_def_symbol_rate):
# 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 range(0,symbol_rate):
# D(f)
2019-05-11 20:41:49 +00:00
t = pi * f / symbol_rate
if t < 1e-6:
df = 1.0
else:
df = sin (t) / t
xfer.append(df)
return xfer
2019-05-11 20:41:49 +00:00
def transfer_function_tx(symbol_rate=_def_symbol_rate):
xfer = [] # frequency domain transfer function
for f in range(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
2019-05-11 20:41:49 +00:00
def transfer_function_dmr(symbol_rate=_def_symbol_rate):
2017-02-09 14:05:34 +00:00
xfer = [] # frequency domain transfer function
for f in range(0, 2881): # specs cover 0 - 2,880 Hz
2017-02-09 14:05:34 +00:00
if f < 1920:
hf = 1.0
else:
hf = 0.5 + 0.5 * cos (2 * pi * float(f) / 1920.0)
xfer.append(hf)
xfer = np.array(xfer, dtype=np.float64)
xfer = np.sqrt(xfer) # root cosine
return xfer
2020-02-24 01:25:49 +00:00
def _transfer_function_nxdn(symbol_rate, modulator=False):
2019-05-11 20:41:49 +00:00
assert symbol_rate == 2400 or symbol_rate == 4800
T = 1.0 / symbol_rate
a = 0.2 # rolloff
fl = int(0.5+(1-a)/(2*T))
fh = int(0.5+(1+a)/(2*T))
xfer = []
for f in range(0, symbol_rate):
2019-05-11 20:41:49 +00:00
if f < fl:
hf = 1.0
elif f >= fl and f <= fh:
hf = cos((T/(4*a)) * (2*pi*f - pi*(1-a)/T))
else:
hf = 0.0
x = pi * f * T
if f <= fh:
if x == 0 or sin(x) == 0:
df = 1.0
else:
2020-02-24 01:25:49 +00:00
if modulator:
df = sin(x) / x
else: # rx mode: demodulator
df = x / sin(x)
2019-05-11 20:41:49 +00:00
else:
2020-02-24 01:25:49 +00:00
df = 0.0
2019-05-11 20:41:49 +00:00
xfer.append(hf * df)
return xfer
2020-02-24 01:25:49 +00:00
# rx / demod case
def transfer_function_nxdn(symbol_rate=_def_symbol_rate):
return _transfer_function_nxdn(symbol_rate)
# tx / modulator case
def transfer_function_nxdn_tx(symbol_rate=_def_symbol_rate):
return _transfer_function_nxdn(symbol_rate, modulator=True)
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):
2019-05-11 20:41:49 +00:00
impulse_response = np.fft.fftshift(np.fft.irfft(self.generator(symbol_rate=self.symbol_rate), 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()]))
2014-06-13 00:46:23 +00:00
2017-03-10 15:43:49 +00:00
class gmsk_taps(object):
def __init__(self, filter_gain = 1.0, sample_rate=_def_output_sample_rate, symbol_rate=_def_symbol_rate, span=_def_gmsk_span, bt=_def_bt):
self.filter_gain = filter_gain
self.sample_rate = sample_rate
self.symbol_rate = symbol_rate
self.span = span
self.bt = bt
2020-06-19 01:30:19 +00:00
self.samples_per_symbol = self.sample_rate // self.symbol_rate
self.ntaps = self.span * self.samples_per_symbol
2017-03-10 15:43:49 +00:00
def generate(self):
# from gnuradio gr-digital/python/digital/gmsk.py
# Form Gaussian filter
# Generate Gaussian response (Needs to be convolved with window below).
gaussian_taps = filter.firdes.gaussian(
self.filter_gain, # gain
self.samples_per_symbol, # symbol_rate
self.bt, # bandwidth * symbol time
self.ntaps # number of taps
)
sqwave = (1,) * self.samples_per_symbol # rectangular window
taps = np.convolve(np.array(gaussian_taps),np.array(sqwave))
return taps
2014-06-13 00:46:23 +00:00
# /////////////////////////////////////////////////////////////////////////////
# modulator
# /////////////////////////////////////////////////////////////////////////////
class p25_mod_bf(gr.hier_block2):
def __init__(self,
output_sample_rate=_def_output_sample_rate,
reverse=_def_reverse,
verbose=_def_verbose,
2017-02-09 14:05:34 +00:00
generator=transfer_function_tx,
2017-03-10 15:43:49 +00:00
dstar=False,
bt=_def_bt,
2017-10-11 18:59:24 +00:00
rc=None,
2014-06-13 00:46:23 +00:00
log=_def_log):
"""
Hierarchical block for RRC-filtered P25 FM modulation.
The input is a dibit (P25 symbol) stream (char, not packed) and the
output is the float "C4FM" signal at baseband, suitable for application
to an FM modulator stage
Input is at the base symbol rate (4800), output sample rate is
typically either 32000 (USRP TX chain) or 48000 (sound card)
@param output_sample_rate: output sample rate
@type output_sample_rate: integer
@param reverse: reverse polarity flag
@type reverse: bool
@param verbose: Print information about modulator?
@type verbose: bool
@param debug: Print modulation data to files?
@type debug: bool
"""
gr.hier_block2.__init__(self, "p25_c4fm_mod_bf",
2014-06-13 00:46:23 +00:00
gr.io_signature(1, 1, gr.sizeof_char), # Input signature
gr.io_signature(1, 1, gr.sizeof_float)) # Output signature
2020-06-19 01:30:19 +00:00
input_sample_rate = 4800 # P25/ysf/dmr/dstar baseband symbol rate
if rc == 'nxdn48':
input_sample_rate = 2400 # only exception is nxdn48 = 2400 rate
2017-11-12 00:59:07 +00:00
intermediate_rate = 48000
self._interp_factor = intermediate_rate / input_sample_rate
2014-06-13 00:46:23 +00:00
2017-03-10 15:43:49 +00:00
self.dstar = dstar
self.bt = bt
if self.dstar:
self.C2S = digital.chunks_to_symbols_bf([-1, 1], 1)
else:
mod_map = [1.0/3.0, 1.0, -(1.0/3.0), -1.0]
self.C2S = digital.chunks_to_symbols_bf(mod_map)
2014-06-13 00:46:23 +00:00
if reverse:
self.polarity = blocks.multiply_const_ff(-1)
else:
self.polarity = blocks.multiply_const_ff( 1)
2017-02-09 14:05:34 +00:00
self.generator = generator
2020-06-19 01:30:19 +00:00
assert rc is None or rc == 'rc' or rc == 'rrc' or rc.startswith('nxdn')
2017-10-11 18:59:24 +00:00
if rc:
2017-11-12 00:59:07 +00:00
coeffs = filter.firdes.root_raised_cosine(1.0, intermediate_rate, input_sample_rate, 0.2, 91)
2017-10-11 18:59:24 +00:00
if rc == 'rc':
2017-11-12 00:59:07 +00:00
coeffs = c4fm_taps(sample_rate=intermediate_rate).generate()
2017-10-11 18:59:24 +00:00
elif self.dstar:
2017-11-12 00:59:07 +00:00
coeffs = gmsk_taps(sample_rate=intermediate_rate, bt=self.bt).generate()
2020-06-19 01:30:19 +00:00
elif rc.startswith('nxdn'):
coeffs = c4fm_taps(sample_rate=intermediate_rate, generator=transfer_function_nxdn_tx, symbol_rate=input_sample_rate).generate()
2017-10-11 18:59:24 +00:00
elif not rc:
2017-11-12 00:59:07 +00:00
coeffs = c4fm_taps(sample_rate=intermediate_rate, generator=self.generator).generate()
2017-03-10 15:43:49 +00:00
self.filter = filter.interp_fir_filter_fff(self._interp_factor, coeffs)
2014-06-13 00:46:23 +00:00
if verbose:
self._print_verbage()
if log:
self._setup_logging()
self.connect(self, self.C2S, self.polarity, self.filter)
2017-11-12 00:59:07 +00:00
if intermediate_rate != output_sample_rate:
self.arb_resamp = filter.pfb.arb_resampler_fff(float(output_sample_rate)/intermediate_rate)
self.connect(self.filter, self.arb_resamp, self)
2014-06-13 00:46:23 +00:00
else:
self.connect(self.filter, self)
2014-06-13 00:46:23 +00:00
def _print_verbage(self):
print ("\nModulator:")
print ("interpolation: %d decimation: %d" %(self._interp_factor, self._decimation))
2014-06-13 00:46:23 +00:00
def _setup_logging(self):
print ("Modulation logging turned on.")
2014-06-13 00:46:23 +00:00
self.connect(self.C2S,
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.filter,
gr.file_sink(gr.sizeof_float, "tx_filter.dat"))
2014-06-13 00:46:23 +00:00
if (self._decimation > 1):
self.connect(self.decimator,
gr.file_sink(gr.sizeof_float, "tx_decimator.dat"))
def add_options(parser):
"""
Adds QPSK modulation-specific options to the standard parser
"""
add_options=staticmethod(add_options)
def extract_kwargs_from_options(options):
"""
Given command line options, create dictionary suitable for passing to __init__
"""
return modulation_utils.extract_kwargs_from_options(dqpsk_mod.__init__,
('self',), options)
extract_kwargs_from_options=staticmethod(extract_kwargs_from_options)
#
# Add these to the mod/demod registry
#
modulation_utils.add_type_1_mod('op25_c4fm', p25_mod_bf)