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.

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
* MESSAGE DEBUG PRINT PDU VERBOSE *
((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
* MESSAGE DEBUG PRINT PDU VERBOSE *
((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!

Since adding message based lambda blocks the other day, it seemed an obvious pairing to include a stream equivalent block as well for really rapid prototyping needs. The interface is a bit less simple because we can have N input ports and M output ports, I settled on defining a lambda function which takes a list of input vectors (the same input_items list of numpy arrays normal python blocks get, and an integer output port index). To implement a simplish stream block now that consumes N input streams and produces M output streams, this function prototype simply needs to be implemented. This won’t let you do everything, for instance output streams still need to produce the same number of items, and they are assumed to consume at this same rate. Extending this to a sync decimate or interpolater should be straight forward, but the function prototype would need to become a bit more complex for a “general work” kind of interface which allows producing/consuming each port at different rates, so I chose not to address that for now.

Example Stream Lambda Block  Flowgraph

A simple example graph which demonstrates the use of these blocks is shown below, this graph takes a simple Gaussian noise source in, throttles it to a fixed throughput rate, and then feeds it through a 2-in 2-out stream lambda block. In this case, we provide the following lambda function for the block,

lambda input_items, output_index: (input_items[output_index][::2]**2)*[0.1,0.2][output_index] + [-1,1][output_index]

That is to say:

output_items[0] = input_items[0][::2]**2*0.1 – 1

output_items[1] = input_items[1][::2]**2*0.2 + 1

Here, each stream is independent of the other output, and they could have been implemented as two seperate blocks, but this is just provided as an illustration.

The second block then is a 2-in 1-out stream lambda block which merges the two streams into one output stream, this mapping is given by:

lambda input_items, output_index: (input_items[0][:] * numpy.conj(input_items[1][:]))

This one is a bit simpler, as we will simply have a single evaluation:

output_items[0] = input_items[0]*conj(input_items[1])

The resulting output signal is then shown in the output plot.

What’s really fun is these lambda functions can be updated at runtime to change the block’s functionality. I’ve added a variable_text_input block to gr-pyqt which allows for runtime input of new lambda functions to each block which are passed to the GRC setter callback for each stream lambda block. By running this graph, you can now fiddle with the block’s algorithms at run time and immediately see the output effect on the stream signal. Just don’t type any invalid python at run-time or you’ll crash the block’s thread context.

The dynamic update version of the stream lambda block demo is shown below.

These dynamic function updates should work with the message lambda block as well, but that is not shown here.

The GRC graph for this example can be found @ https://github.com/osh/gr-pyqt/blob/master/apps/test_stream_lambda.grc