Pipe SDR IQ data through FM demodulator for FSK9600 AX25 reception

Problem #

I thought that RTL-SDR and its command line tools are so common in these days that software for decoding everything and especially simple FSK9600 definitely exists. I was kind of right…except there are some corner cases. I looked for command line tools because decoding should work on BeagleBoneBlack which is not the most powerful computer.

There is rtl_fm that uses RTL-SDR dongle to receive actual signal from air and it also demodulates signal on the fly. Demodulated audio signal can then be piped to multimon-ng that decodes FSK9600 AX25 packets.

rtl_fm -f 437.505M -M fm -s 1024000 -r 22050 | multimon-ng -t raw -a FSK9600 /dev/stdin

Actually I have not tested this command in real situation, but it should work with strong signals. Narrow band FM uses +- 2.5 kHz deviation, but some nano satellites are using deviation around +- 3.5 kHz, therefore rtl_fm’s demodulation process might not give you the best output.

Here is my corner case problem - what if I want to receive data from satellites? In this case I have to deal with doppler shift. Above mentioned command does not work anymore because center frequency will be too much off from actual frequency. There is no doppler correction support in rtl_fm and this is okay. Unix command line programs should do only one thing and they better do it well.

Gqrx + rtl_tcp #

I considered it impossible to receive satellites using only rtl_fm + multimon-ng so I looked for alternatives. What if I just use BeagleBoneBlack as SDR server and heavy lifting is done using Mac or Linux? In that way I should have more chance with doppler correction because decent SDR software typically has plugins for that job. SDR server can be started as:

rtl_tcp -a

Gqrx which is a nice graphical SDR waterfall program for Mac and Linux has support for such server. Or at least it seems it has. Maybe there are newer versions that work. However I followed installation instructions from here and these Gqrx and rtl_tcp versions did not work together.
Sometimes rtl_tcp gave socket error, sometimes Gqrx showed random signals, sometimes it showed even correct signal, but there was huge delay displaying it. I used HackRF to generate continuous signal. Doing this I knew what kind of signal to expect on Gqrx waterfall. Anyway I was too frustrated to continue with this path and I did not even think about possible version mismatch at this time.

Pipeable FM demodulator #

Back to basics. I could not get Gqrx running using rtl_tcp as a data source and actually I did not try too much either. My ultimate goal was to use only command line tools to do the job. I figured out that rtl_fm can also output raw signed 16 bit IQ data. I like rtl_fm more compared to rtl_sdr because rtl_fm by default tunes little bit off the center frequency to reject DC signal. So the goal was such chain:

rtl_fm -f 437.505M -M raw -s 1024000 -r 22050 | doppler -tle "ESTCUBE 1" cubesat.txt | demod -M fm -deviation 3.5k | multimon-ng -t raw -a FSK9600 /dev/stdin

I have some doppler corrected raw files already available therefore I left doppler (which is not yet ready) aside. I was interested in if there is pipeable FM demodulator available that can be used as shown an hypothetical example command. After some googling I came to conclusion that radio amateurs are mostly using GUI based SDR programs that are made for MS Windows. It seems that very few people are experimenting with satellite reception using Linux command line tools. Anyway I did not find drop in solution to my problem, however I found couple of open source FM demodulators that can be modified to match my needs. There is pyFmRadio, SoftFM and Google radioreceiver. I gave Google radioreceiver a go because it has nice testing application already bundled that almost did what I wanted to achieve - take raw IQ data as input and output demodulated audio.

First thing that I noticed was input format, by default it takes 8 bit IQ data as input, however rtl_sdr and rtl_fm are producing signed 16 bit IQ data.

So samplesFromInt16 was added to dsp.cc (here is also original samplesFromUint8 for comparison):

Samples samplesFromUint8(uint8_t* buffer, int length) {
  Samples out(length);
  for (int i = 0; i < length; ++i) {
    out[i] = buffer[i] / 128.0 - 1;
  return out;

Samples samplesFromInt16(int16_t* buffer, int length) {
  Samples out(length);
  for (int i = 0; i < length; ++i) {
    out[i] = buffer[i] / 32768.0;
  return out;

demod-stdin.cc was modified to:

//decoder->decode(samplesFromUint8(reinterpret_cast<uint8_t*>(buffer), read), cfg.stereo);
decoder->decode(samplesFromInt16(reinterpret_cast<int16_t*>(buffer), read / 2), cfg.stereo);

After these modifications I was able to play raw IQ recording from a FM radio station:

cat fm_radio_r2.iq | ./demod-stdin -mod WBFM -bandwidth 170000 -maxf 75000 -inrate 230400 -outrate 48000 | play -t raw -r 48k -e signed-integer -b 16 -c 2 -V1 -

Of course it is possible to listen FM radio using just rtl_fm and play together. My experiment proved that my modifications were correct and FM demodulator really works.

For clarification here is command that was used to capture above mentioned fm_radio_r2.iq file:

rtl_fm -f 103.4M -M raw -s 230400 fm_radio_r2.iq

FSK9600 data decoding #

I have already done some GNU Radio block diagrams to do quadrature demodulation for FSK9600 signals. Here is the diagram that outputs 2 files, one of them is demodulated, other one has just gone through low pass filter:

Notice that original signal is mixed with -49.26 kHz cosine signal source, because original recording is not centered around 0 frequency, which is needed for other GNU Radio blocks.

Following command was used to test if demodulated audio produced by GNU Radio is fine enough for multimon-ng. Indeed it worked nicely.

sox -t wav 9600AFdump.wav -r 22050 -esigned-integer -b16 -t raw - | multimon-ng -t raw -a FSK9600 /dev/stdin

Original demod-stdin produces 2 channel demodulated audio, however multimon-ng uses 1 channel audio as input, therefore following modification was added to demod-stdin.cc:

//cout.write(outBlock, 4);
cout.write(outBlock, 2); 

It just writes only left channel audio to output. Right channel was the same anyway if narrowband FM demodulator was used.

After that 9600RFdump.wav was passed through modified demod-stdin using narrowband FM demodulation with 3.5 kHz deviation:

sox -t wav 9600RFdump.wav -esigned-integer -b16 -r 1024000 -t raw - | ./demod-stdin -mod NBFM -maxf 3500 -inrate 1024000 -outrate 22050 | multimon-ng -t raw -a FSK9600 /dev/stdin

It did not work this time. Comparing GNU radio quadrature demodulated audio with demod-stdin output revealed following:

quadrature vs fm demod.png

Bottom signal is from demod-stdin and its output is quite “wobbly” compared to GNU Radio. Little side note - this picture is made using Low Pass Filter 5 kHz cutoff frequency and 10 KHz channel_width instead of 8 KHz and 12.5 KHz that can be seen on block diagram above. Later settings improved SNR a little (preamble peaks were higher, this nice sinusoid is 0xAA preamble pattern), but it was still “wobbling”. Looking at the picture gave me an idea to make this output rectangular as can be seen on quadrature case. It was very easy to implement, only following 2 lines were added to demod-stdin.cc:

  if (left > 0) left = 32767;
  if (left < 0) left = -32767;

For some reason multimon-ng was still unable to decode data after these modifications. To see what is difference between GNU Radio and demod-stdin demodulated audio I did side by side comparison using Audacity.


From that comparison I noticed that demod-stdin introduces lag that seems to increase in time if 22050 samples per second output is used. It is marked with red arrows in the picture. My hypothesis is that multimon-ng is very strict about symbol timing. It should not be. Looking more at the demod-stdin source revealed that it is recommended to use 48000 sps output. Now there was another problem - multimon-ng only uses 22050 sps as input. So I digged into multimon-ng source and found out that it should be quite easy fix. Only one line might be changed in demod_fsk96.c:

//#define FREQ_SAMP  22050
#define FREQ_SAMP  48000

Indeed after this it started to decode AX25 packets using modified multimon-ng with the following command:

sox -t wav 9600RFdump.wav -esigned-integer -b16 -r 1024000 -t raw - | ./demod-stdin -mod NBFM -maxf 3500 -inrate 1024000 -outrate 48000 | multimon-ng -t raw -a FSK9600 /dev/stdin

Update 13.01.15 #

Source is now available: demod, multimon-ng fork.

Currently working command without doppler is:

rtl_fm -f 437.505M -M raw -s 1024000 | demod -mod NBFM -maxf 3500 -inputtype i16 -inrate 1024000 -outrate 48000 -channels 1 -squaredoutput | multimon-ng -t raw -a FSK9600 /dev/stdin

Update 23.02.15 #

Here you can read about better liquid-dsp based demodulator that I wrote from scratch.

Update 25.02.15 #

Doppler is now ready, read about it from here.


Now read this

Parsing line-based protocol using Rust and nom part 1

Let’s suppose we have some kind of line protocol and we would like to parse it in Rust. Here is an example how this protocol might look like: "#MEAS_NUM;voltage;20.1;V\n" "#MEAS_TEXT;serial;CAFEBABE\n" "#INPUT;Is it broken?;YES,NO,MAYBE\... Continue →