Close

Real-time Demodulation

A project log for Homebrew SDR Receiver

My first attempt at building a phasing direct conversion receiver.

paul-horsfallPaul Horsfall 09/10/2024 at 10:150 Comments

When I initially tested the receiver I had to first capture a chunk of IQ samples, then later perform demodulation as a post-processing step. This was fine for an initial test, but doing this doesn't feel much like using a "radio", and more importantly it makes it awkward to experiment with the system.

What I needed was a real-time implementation of demodulation. I considered implementing something from "scratch", but in the end chose to use GNU Radio.

The main challenge in getting this working was figuring out how to get data from the receiver into GNU Radio. The solution I've settled on for now is to run a small auxiliary program that consumes data from the receiver (which looks like a serial device on the PC) and forwards it to GNU Radio over ZeroMQ.

The program itself is conceptually simple; it's just a endless loop that reads a bunch of IQ samples from stdin (which I redirect to the serial device presented by the receiver), converts each sample from a pair of bytes to a complex number, then hands the complex numbers off to ZeroMQ. Here's a sketch of this in Python:

import zmq
import numpy as np

ctx = zmq.Context()
socket = ctx.socket(zmq.PUB)
# local IP on which we're running
socket.bind('tcp://192.168.1.208:1234')

BUFSIZE = 1024 * 8
buf = np.zeros(BUFSIZE, dtype=np.complex64)

while True:
    for n in range(BUFSIZE):
        i, q = sys.stdin.buffer.read(2)
        buf[n] = (2*(i/255.)-1) + (2*(q/255.)-1)*1j
    socket.send(buf)

While this illustrates the basic idea, things are a little more complicated in practice. In particular, the receiver doesn't actually produce a simple stream of alternating I/Q bytes as the code above assumes. Instead, I'm doing some crude framing in order to distinguish I/Q sample pairs from one another. As a result, the program also implements a small state machine, and it's that which extracts raw I/Q bytes from the serial stream.

That detail aside, with this program in place, data can be brought into GNU Radio by adding a ZMQ SUB Source block to the flow graph, and configuring it with the IP address to which ZeroMQ is bound. (It might be advantageous for this to be 127.0.0.1 if the program and GNU Radio are on the same host.)

Below is a screenshot of the complete flow graph I created for receiving broadcast FM using this approach. It streams data from the receiver, performs demodulation, then sends the result to an audio device for playback.

The signal processing here is equivalent to the Python code I used previously, but with this I can now listen to the output of the receiver in real-time.

Discussions