From bc067c8bbd3330f9648128b3223427145ed9e921 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Thu, 19 Jun 2014 17:59:51 +0300 Subject: [PATCH] Add QPSK16 modem --- .gitignore | 2 + common.py | 69 +++++++++++++++++++ pll.py | 33 +++++++++ recv.py | 196 +++++++++++++++++++++++++++++++++++++++++++++++++++++ send.py | 72 ++++++++++++++++++++ sigproc.py | 53 +++++++++++++++ 6 files changed, 425 insertions(+) create mode 100644 .gitignore create mode 100644 common.py create mode 100644 pll.py create mode 100644 recv.py create mode 100644 send.py create mode 100644 sigproc.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d0c7da --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.int16 +*.pyc diff --git a/common.py b/common.py new file mode 100644 index 0000000..d57e16e --- /dev/null +++ b/common.py @@ -0,0 +1,69 @@ +import hashlib +import struct +import logging +log = logging.getLogger(__name__) + +Fs = 32e3 +Ts = 1.0 / Fs + +Fc = 9e3 +Tc = 1.0 / Fc + +Tsym = 1e-3 +Nsym = int(Tsym / Fs) + +F0 = Fc - 1e3 +F1 = Fc + 1e3 +Nsym = int(Tsym / Ts) +baud = int(1/Tsym) + +scaling = 10000.0 + +LENGTH_FORMAT = ' COHERENCE_THRESHOLD: + counter += 1 + else: + counter = 0 + + if counter == CARRIER_THRESHOLD: + length = CARRIER_THRESHOLD * Nsym + return offset - length + Nsym, offset + +def find_start(x, start): + WINDOW = Nsym * 10 + length = CARRIER_DURATION * Nsym + begin, end = start - WINDOW, start + length + WINDOW + x_ = x[begin:end] + + Hc = exp_iwt(Fc, len(x_)) + P = np.abs(Hc.conj() * x_) ** 2 + cumsumP = P.cumsum() + start = np.argmax(cumsumP[length:] - cumsumP[:-length]) + begin + log.info('Carrier starts at {:.3f} ms'.format(start * Tsym * 1e3 / Nsym)) + return start + +def equalize(symbols): + bits = np.round(np.abs(symbols)) + bits = np.array(bits, dtype=int) + prefix = [1]*300 + [0]*100 + ([1]*10 + [0]*10)*20 + [0]*100 + n = len(prefix) + if all(bits[:n] == prefix): + + S = symbols[:n] + + A = np.array([ S[1:], S[:-1], prefix[:-1] ]).T + b = prefix[1:] + + b0, b1, a1 = linalg.lstsq(A, b)[0] + y = np.array(list(sigproc.lfilter([b0, b1], [1, -a1], symbols))) + y = y.conj() # TODO + constellation(y) + + prefix_bits, noise = slicer(y[:n], 0.5) + assert(all(prefix_bits == np.array(prefix))) + log.info( 'Prefix OK, at SNR: {:.1f} dB'.format( 20 * np.log10(np.sqrt(len(prefix_bits)) / norm(noise)) ) ) + + data_bits = sigproc.qpsk.decode(y[n:]) + data_bits = list(data_bits) + log.info( 'Demodulated {} payload bits'.format(len(data_bits)) ) + return data_bits + +def slicer(symbols, threshold): + bits = symbols.real > threshold + noise = symbols - bits + return bits, noise + +def constellation(y): + theta = np.linspace(0, 2*np.pi, 1000) + + pylab.figure() + pylab.subplot(121) + pylab.plot(y.real, y.imag, '.') + pylab.plot(np.cos(theta), np.sin(theta), ':') + pylab.grid('on') + pylab.axis('equal') + + pylab.subplot(122) + pylab.plot(np.arange(len(y)) * Tsym, y.real, '.') + pylab.grid('on') + +def main(t, x): + + x = (x - np.mean(x)) + result = detect(Fc) + if result is None: + log.info('No carrier detected') + return + + begin, end = result + x_ = x[begin:end] + t_ = t[begin:end] + Hc = exp_iwt(Fc, len(x_)) + Zc = np.dot(Hc.conj(), x_) / (0.5*len(x_)) + amp = abs(Zc) + phase = np.angle(Zc, deg=True) + log.info('Carrier detected at ~{:.1f} ms @ {:.1f} kHz: coherence={:.3f}%, amplitude={:.3f}'.format( + begin * Tsym * 1e3 / Nsym, Fc / 1e3, abs(coherence(x_, Fc)) * 100, amp + )) + + start = find_start(x, begin) + x = x[start:] / amp + t = t[start:] + + Hc = exp_iwt(Fc, Nsym) / (0.5*Nsym) + func = lambda y: np.dot(Hc, y) + symbols = [] + for _, coeff in iterate(x, Nsym, advance=Nsym, func=func): + symbols.append(coeff) + symbols = np.array(symbols) + + data_bits = equalize(symbols) + if data_bits is None: + log.info('Cannot demodulate symbols!') + else: + data = iterate(data_bits, bufsize=8, advance=8, func=to_bytes) + data = ''.join(c for _, c in data) + data = unpack(data) + log.info(repr(data)) + + pylab.show() + + +if __name__ == '__main__': + fname, = sys.argv[1:] + t, x = load(fname) + main(t, x) + pylab.show() diff --git a/send.py b/send.py new file mode 100644 index 0000000..e8c3f5d --- /dev/null +++ b/send.py @@ -0,0 +1,72 @@ +from matplotlib import pyplot as plt +import subprocess as sp +import numpy as np + +import os +import sys +import time +import signal +import logging +logging.basicConfig(level=0, format='%(message)s') +log = logging.getLogger(__name__) + +import sigproc +from common import * + +def play(fd): + args = ['aplay', fd.name, '-f', 'S16_LE', '-c', '1', '-r', str(int(Fs))] + ret = sp.call(args=args) + assert ret == 0 + +def record(fname): + args = ['arecord', fname, '-f', 'S16_LE', '-c', '1', '-r', str(int(Fs))] + p = sp.Popen(args=args) + p.stop = lambda: os.kill(r.pid, signal.SIGINT) + return p + + +log.info('MODEM Fc={}KHz, {} BAUD'.format(Fc/1e3, baud)) + + +class Symbol(object): + t = np.arange(0, Nsym) * Ts +# c0 = np.sin(2 * np.pi * F0 * t) +# c1 = np.sin(2 * np.pi * F1 * t) + carrier = np.exp(2j * np.pi * Fc * t) + +class Signal(object): + def __init__(self, fd): + self.fd = fd + def send(self, sym, n=1): + sym = sym.imag * scaling + sym = sym.astype('int16') + for i in range(n): + sym.tofile(fd) + +sym = Symbol() + +data = os.urandom(1024) +data = pack(data) + +bits = list(to_bits(data)) + +if __name__ == '__main__': + + with open('tx.int16', 'wb') as fd: + sig = Signal(fd) + sig.send(sym.carrier*0, n=100) + sig.send(sym.carrier*1, n=300) + sig.send(sym.carrier*0, n=100) + for i in range(20): + sig.send(sym.carrier*1, n=10) + sig.send(sym.carrier*0, n=10) + + sig.send(sym.carrier*0, n=100) + for s in sigproc.qpsk.encode(bits): + sig.send(sym.carrier * s) + + + r = record('rx.int16') + p = play(fd) + time.sleep(0.2) + r.stop() diff --git a/sigproc.py b/sigproc.py new file mode 100644 index 0000000..000de11 --- /dev/null +++ b/sigproc.py @@ -0,0 +1,53 @@ +import numpy as np + +def lfilter(b, a, x): + b = np.array(b) / a[0] + a = np.array(a[1:]) / a[0] + + x_ = [0] * len(b) + y_ = [0] * len(a) + for v in x: + x_ = [v] + x_[:-1] + u = np.dot(x_, b) + u = u - np.dot(y_, a) + + y_ = [u] + y_[1:] + yield u + +class QPSK(object): + def __init__(self, bits_per_symbol): + self._enc = {tuple(): 0.0} # End-Of-Stream symbol + index = 0 + amps = [0.6, 1.0] + N = (2 ** bits_per_symbol) / len(amps) + for a in amps: + for i in range(N): + k = tuple(int(index & (1 << j) != 0) for j in range(bits_per_symbol)) + v = np.exp(2j * i * np.pi / N) + self._enc[k] = v * a + index += 1 + self._dec = {v: k for k, v in self._enc.items()} + self.bits_per_symbol = bits_per_symbol + + def encode(self, bits): + assert len(bits) % self.bits_per_symbol == 0 + for i in range(0, len(bits), self.bits_per_symbol): + s = self._enc[ tuple(bits[i:i+self.bits_per_symbol]) ] + yield s + + def decode(self, symbols): + keys = np.array(self._dec.keys()) + for s in symbols: + index = np.argmin(np.abs(s - keys)) + bits = self._dec[ keys[index] ] + for b in bits: + yield b + + +qpsk = QPSK(bits_per_symbol=4) + +def test_qpsk(): + q = QPSK(bits_per_symbol=2) + bits = [1,1, 0,1, 0,0, 1,0] + S = qpsk.encode(bits) + assert list(qpsk.decode(list(S))) == bits