Building a Burst FSK Modem in GNU Radio with Message Lambda Blocks and Eventstream

Lots of cheap electronics tend to use burst FSK modems for wireless communications. GNU Radio has long been able to work with these sorts of communications, but typically it has done so by running an FSK demodulator continuously and then correlating the output for a known preamble sequence such as a with the correlate access code block, and then adding some kind of monolithic special purpose block. This article proposes a slightly different approach to building such a burst FSK waveform to inter-operate with many wireless devices of this style and to be a bit more modular, flexible, simple and intuitive to work with.

Building the Transmitter

In most communications systems, the transmitter is computationally less demanding than the receiver, additionally many FSK burst modems are low baud rate and not particularly demanding from a computational complexity standpoint. I use these two facts as motivation to justify an exceedingly lazy modem design which boils a burst FSK transmitter block down to three simple message lambda blocks as shown below.


Since they aren’t visible in the screenshot above, the block expression used for the three message lambda blocks are:

Map [0,1] bits to symbols [-1.0,+1.0]
 * lambda x: numpy.array(x, dtype=numpy.float32)*2-1
Interp to Samps per Sym
 * lambda x: numpy.tile(x,[sps,1]).T.reshape([1,len(x)*sps])
FSK Modulate
 * lambda x: numpy.array(numpy.exp(1j*2*numpy.pi*((dev*x*numpy.arange(len(x)))/samp_rate)),dtype="complex64")

We generate random packets of bits, prepend an arbitrary known preamble, map bits to symbols, interpolate to N samples per symbol, and finally modulate our symbols up by a complex sinusoid at plus or minus some FSK deviation frequency. At this point, the modulated bursts can simply be dropped into a transmit sample stream using an eventstream source block whose output is then throttled and run through a simple channel model.


Plotting our FSK burst transmit waveform’s output through the pulse shaping filter, we get a reasonable looking time and frequency profile of the transmitted output signal. This is kind of a fun example to show the power of lamda blocks, in this case we’ve been able to basically write an entire waveform from a couple python expressions all from within GRC with really minimal effort, for a nominal performance penalty, which in this case doesn’t really matter.   If performance was at some point deemed to be important here, blocks could easily be ported one by one to optimized C++ based equivalents.

Building the Receiver

We can take a similar approach to building a burst FSK receiver waveform with stream blocks, message blocks, and event detection. We simply run the received samples through a standard quadrature demodulator block matched roughly to the FSK deviation magnitude. Since this FLL is essentially a random walk when no signal is on the air, and nominally tracking carrier tightly when locked to a burst, we use the variance of the FLL’s output as a detection statistic for bursts, triggering an event when the variance goes below 15 in this case. (or the negative variance rises above -15 in this case).   This can be seen in the graph below.


In this case at every event we latch in a maximum burst length’s worth of FM demodulated float values containing one burst every time this threshold is reached using the trigger rising edge and eventstream sink blocks. These FM demodulated samples are run through a burst FSK time sync, which performs symbol timing recovery and outputs one soft float value per symbol. Please note this synchronization block is by no means an optimal implementation, and represents only a very quick stab at the problem – but seems to perform well enough in initial test. (I would love to clean this up with more optimal ML synchronization algorithms but haven’t had a chance yet).   The output bits can then be sliced to bits, packed into 8 bit chars, and passed in an output PDU downstream to feed a PDU Socket or TUNTAP interface. A screenshot of the receive waveform is shown below.


Our diagnostic output on the terminal shows that we are indeed recovering the inserted preamble bits correctly (0x036cf is consistently received in our output) Although sometimes with alignment slipping a bit or two in either direction and adjacent noise and data bits flipping randomly – A problem that could be easily taken care of with a MAC header search and framer.

creating event @ time 368183, length = 5000
sps = 8.000000
((tau . 5) (meta . 8) (es::event_length . 5000) (es::event_time . 368183) (es::event_type . edge_event))
pdu_length = 78
contents = 
0000: 40 36 cf 02 c5 d2 b7 48 0c bd 22 44 5c d1 84 c3 
0010: 0b 2e 49 2f 29 7b 96 dd 74 61 88 82 83 47 db 0b 
0020: 2a 40 20 55 81 7d b4 29 36 0e 1d 9e 65 86 4b af 
0030: 3e b9 b1 65 b0 f9 a1 4a be f2 54 1a b2 41 bf 02 
0040: 94 a9 c3 44 3d b2 bb b6 db 6d 2b e4 b4 67 
creating event @ time 421379, length = 5000
sps = 8.000000
((tau . 3) (meta . 8) (es::event_length . 5000) (es::event_time . 421379) (es::event_type . edge_event))
pdu_length = 78
contents = 
0000: 00 36 cf 0a 59 29 cb 7b 83 67 5f ac 29 c7 87 d7 
0010: 2e 63 d4 c4 3e ff 68 d2 01 56 cc ba 09 aa 33 16 
0020: 00 7a c7 54 57 12 e3 7b 5a 72 d0 4c 80 aa 44 f1 
0030: cf 77 16 28 65 31 a4 11 f3 12 07 ef e0 42 cc 47 
0040: 57 90 be aa 15 02 89 19 91 f6 c9 83 8a dc 

This waveform is available on github for your enjoyment.  Please send pull requests for any improvements you make!