M17 Quickstart TX HackRF

This is meant to be a series of short and sweet guide to a few ways to run M17 over RF today. This one focuses on transmitting and receiving M17 with m17-cxx-demod and GnuRadio. This’ll require a little more Linux familiarity than the OpenWebRX guide, but it’s not too bad.

I’d say the hardest part is making sure you have the right dependencies installed - on some linux systems it can be a real bear. If you run into any troubles, make sure to ask for help and we’ll try to sort it out and get it documented.


There are a few options for basic testing, that need more work for integration:

  • SDR receiver - rtl_fm
  • SDR transmitter - GNU Radio Companion

And some that are closer to being useful, if not outright usable already:

  • SDR receiver - OpenWebRX
  • WX9O’s TNC3 and a capable mobile radio + m17_kiss_ht
  • G4KLX’s M17Client and an MMDVM interface or hotspot

We’ll need a known-good transmitter for testing all of these, so I’m starting with WX9O’s m17-cxx-demod repository and a HackRF.

In doing that, we’ll also try the M17 receiver to make sure both tx and rx work.

Videos are best viewed on a laptop or desktop computer. You’ll have a damn hard time reading the text, much less following along on a mobile phone or tablet.


First - per the instructions in that repository, you’ll need:

This code requires the codec2-devel, boost-devel and gtest-devel packages be installed.
It also requires a modern C++17 compiler (GCC 8 minimum).

Those package names look like Debian-style packages with the -devel suffix for development headers.

Arch Linux (almost) always includes development headers with any package, so the package names on my system are just codec2, boost, and gtest.

These’re pretty common and it turns out I already had them installed, so no effort on my part as Arch keeps a fairly modern compiler suite too.

You’ll also want sox to handle converting audio files, GNU Radio including GNURadio Companion, and the drivers and libraries for your various SDRs. That’s a little outside the scope of this post, but you can come ask in chat and we’ll unstick you if we can.

If you’re uncomfortable with the command line, this will require a bit more sticktuitiveness than you may like.


I’m going to run these binaries from the build directory instead of installing them to my system proper, so I won’t run “make install”.

Following the instructions, we get something like this to download and compile the software:

git clone https://github.com/mobilinkd/m17-cxx-demod.git
cd m17-cxx-demod
mkdir build
cd build
cmake ..

If all goes well, your build/ directory should now have an apps/ subdirectory that contains m17-mod and m17-demod, (and some other files we’ll ignore).

Hopefully all has gone well. If not, don’t give up, read the errors carefully, and if all else fails ask for help.

While that compiles - actually it’s probably already done, it doesn’t take too long - let’s grab the .grc file (GNU Radio Companion graph) from https://github.com/mobilinkd/m17-gnuradio and open it with GNU Radio Companion.

If you have a HackRF instead of a Pluto, you’ll need to replace the sink with an Osmocom Sink. I’ve a screenshot for your perusal and I’ll try to explain the magic numbers.

oscomocom sink block screenshot

In “Device Arguments”, you can specify the hackrf to transmit from with hackrf={serial number}. You can find this serial number with the hackrf_info command and copy and paste it into this field in GRC. I’ve blurred my serial number out because … I don’t know, but it seems merely prudent on the internet these days.

Sync should be set to “Don’t Sync” because otherwise the osmocom driver will look fruitlessly for a PPS (Pulse Per Second) input that doesn’t exist and try to synchronize to it.

“sample_rate” in the Sample Rate field is a GRC variable, which is generated from multiplying the symbol rate (which must be 4800 symbols per second for M17 simply because that’s the defined value for M17) by the interpolation rate to get a valid sample rate for the SDR. More on that in a moment.

The “Ch0 Frequency, Hz” is going to be the transmit frequency.

Finally, the gain fields are guesses of my own that don’t seem to overload my receiver and informed by a weak memory of the HackRF documentation.

(I never remember which one of the IF or BB gain fields is relevant for TX, but since it does no harm to just have them both in there I just set them both to the same value since only one has any effect.)

I think everything else is untouched and the default value.

Sample Rate

There are three GRC variables in this graph. These allow for referencing common values across multiple processing blocks, which makes modification like we’re about to do much easier. Here’s my working set of values for TX with the HackRF:

working GRC variable values

I have only changed the interpolation rate, which makes the sample_rate recalculate automatically.

The HackRF does not perform well with low ( < 2Msps ) rates, and using a minimum of 4 or even 8 Msps is recommended. You can see WX9O has documented the minimum sample rate for the PlutoSDR, and the HackRF is just a little pickier. An interpolation value of 1024 brought the sample rate up to just under 5 Msps and that works just fine for me.

There’s one final thing we need to do to transmit, which is to create the input file and tell GRC to use that as the source to transmit.

This right here:

File Input Block

File input field

That path controls where to find an M17 bitstream - which we still need to generate. Let’s do that now.

Transmit and Receive: Prep an audio file

I like using historical or meme audio files for testing. This one is from Apollo 14!

You can download it and use it yourself for this, or you can take just about any audio file and make it compatible for this test with this sox command line:

sox input.wav -b 16 -e signed-integer -r 8k -c 1 output.wav

You can read up on the details with man sox, but this is converting whatever the input format is to a single channel signed 16bit audio file sampled at 8000 Hz and storing it in output.wav

You can also add filters and effects to the end of the command. I might recommend norm which will effectively autoscale the volume of the file so you get consistent results:

sox input.wav -b 16 -e signed-integer -r 8k -c 1 output.wav norm

You might also want to normalize and then attenuate the volume just a tad:

sox input.wav -b 16 -e signed-integer -r 8k -c 1 output.wav norm gain -3

Those sox commands should work with just about any standard audio file as input, but I’ve only tried wavs because those are what I happen to have on hand.

You can also opt to record your own voice, if you really believe you’re more interesting than Apollo 14:

arecord -r 8000 -f S16_LE -c 1 out.wav

The -c is the number of channels, -r is the sample rate, and S16_LE means use 16 bit little endian integers. We have to have these specific values just because that’s what m17-mod can take as input, which is itself constrained by the Codec2 library.

Loopback test

  • We’re going to take the wav file, strip it of the wav metadata so it’s just the raw audio, and pass it to m17-mod.
  • m17-mod will encode that into an M17 baseband stream and tag it as coming from YOURCALL (Replace with your own callsign please).
  • That baseband will then be passed straight into m17-demod, which will decode it to audio again as well as display the LSF data and show debugging information.
  • And finally that audio will be decoded and played by sox (play binary is part of sox package).
sox apollo11_1.wav -t raw - | ./apps/m17-mod -S YOURCALL | ./apps/m17-demod -l -d | play -q -b 16 -r 8000 -c1 -t s16 -

(If you want the apollo11 wav)

In this video I play part of the original wav first, and then play the M17 loopback audio.

Hopefully that’s just worked for you too - if not, please keep trying and ask for help when you’re well and truly stuck and making no progress and we’ll unstick you and work to prevent others getting stuck in the same way.

Let’s run the same thing, but with real radios in between the m17-mod and m17-demod.

We won’t be able to transmit with m17-mod directly on the command line, since the baseband signal needs a little more processing before it can become the IQ samples expected by an SDR to transmit.

Hardware Clocks

Note: the stock HackRF oscillator is expected to have ~20ppm error, which is not great, not terrible. Similarly, many cheap RTL-SDRs can have errors well north of 100ppm error. Neither are sufficient for our purposes.

If memory serves I’ve added a reasonable reference to my HackRF, but I’m feeding a fairly good 10MHz reference signal in from a cheap GPSDO anyway because I don’t remember for sure. My RTL-SDR (nooelec NESDR) has a .5 PPM TCXO which is plenty good.

If you run into issues, check to make sure your transmitter and receiver are on the same frequency in practical terms. Frequency errors here can mean an absolute error in frequency (so 146.52M becomes 146.45M for instance), or even a drift in frequency.

Drift is going to be impacted heavily by temperature changes, so run both rx and tx for a while until they’re warmed up and you can use gqrx or other graphical SDR programs to see what frequency the transmitter shows up on in the receiver. Use that observed frequency for the receiver command line instead of the nominal one to improve the likelihood of it working for you.

RF Transmission

This is for those with legal privileges to transmit, and/or the know-how to avoid the signal getting out and being a liability.

First let’s split up that loopback command line and make some changes:

sox apollo11_1.wav -t raw - | ./apps/m17-mod -S YOURCALL -b > m17.bin

You’ll notice the sox part is identical, and the m17-mod part has had a -b added. This generates just the bitstream instead of baseband audio, and we use typiccal shell processing to dump that output into a file called m17.bin. This will be used in GNU Radio Companion as our file source.

 rtl_fm -E offset -f 146.52M -s 48k | ./apps/m17-demod -l -d | play -q -b 16 -r 8000 -c1 -t s16 -

And here’s the receive side. It’s 100% identical as the loopback test, except it’s now being fed data from rtl_fm.

Go back to this block:

File Input Block

and modify it to point to your newly generated m17.bin, and hit the “play” button in GRC to try to transmit.

It’s reasonably likely that you’ll run into some form of issue with your radio, SDR libraries, or GNU Radio itself if you’re not spending a fair amount of time in GNU Radio already - I don’t spend enough time in it and usually run into something. This time it was the Sync field expecting a PPS input on the HackRF, which I’ve tried to save you from.

Once you’ve got it working, you should have results something like this:


Fantastic work WX9O, and many thanks!