Software for my QO-100 groundstation

You may have heard about my groundstation for QO-100 (Es’hail-2), which is based on a BeagleBone black and a LimeSDR Mini. A description of this station appeared in a LimeSDR field report. However, I haven’t spoken in detail about the software yet, since I was testing various things and using a makeshift setup until I had some time to put together a solution that I really liked.

Now I am quite happy with the result and indeed I have decided to start up a Github repository with the software I am using in case anyone finds it useful. This post is a description of the software I am using for the narrowband transponder.

The main idea for my narrowband transponder solution is that the BeagleBone Black will just run some software which controls the LimeSDR Mini and streams IQ samples using Ethernet to and from another computer where all the heavy duty work is done. In my case, this computer is my laptop, so I can operate from anywhere in the house, connected through ethernet or even WiFi.

For receiving the downlink, I want to use Linrad because I am a huge fan of that SDR software. I have been using it for all sorts of things, some of which you can see in other blogposts. Thus, it seems natural to use the Linrad network protocol to stream data from the BeagleBone Black. It is also quite nice that this protocol uses multicast UDP, so I can have several instances of Linrad (perhaps on different computers) listening to the same downlink stream.

Since Linrad’s transmit capabilities are not so developed, I am using GNU Radio for the uplink. Most of the time, I will use a GNU Radio flowgraph that gets audio either from my microphone or another application (such as fldigi or WSJT-X) using PulseAudio, filters it to an SSB bandwidth and modulates it on the correct frequency of the transponder. Thus, the GNU Radio flowgraph is essentially an SSB transmiter covering the complete transponder. Using GNU Radio opens up the possibility for other experiments, such as running a digital transmitter implemented completely within GNU Radio.

The main piece of software that runs in the BeagleBone Black is called limesdr_linrad, and is loosely based on the limesdr_toolbox software. It uses the LimeSuite API to drive the LimeSDR Mini in full duplex, streams RX samples using the Linrad network protocol and gets TX samples from a FIFO. The FIFO is then connected to GNU Radio using socat and a TCP stream. I have also tried UDP, but it seems to give some problems with lost packets.

The limesdr_linrad utility implements an additional feature not found in limesdr_toolbox. It can use the LMS7200M NCOs to implement a low-IF instead of the usual zero-IF. This avoids the usual DC spike.

Even though limesdr_linrad accepts parameters to tune to any frequency or use any bandwidth, it is designed with the idea to be used for the QO-100 narrowband transponder in the following manner.

The sample rate is 300ksps, to cover all the 250kHz transponder without wasting network bandwidth. Both the RX and the TX are tuned to the middle of the transponder downlink (in my case a 739.675MHz IF, since I am using a standard Astra LNB with a 9750MHz LO, and 2400.175MHz, since I am modulating directly on 2.4GHz with the LimeSDR Mini).

The RX uses a 0.5MHz low-IF, which is more than enough to avoid the DC spike. The TX uses a 2MHz low-IF. This places the local oscillator at 2398.175MHz, well away from the transponder, in case any LO leakage would get through the transponder. It also helps by filtering out harmonics, since the TX lowpass filter is placed at 3MHz. Of course, the ADCs and DACs of the LMS7200M run at a much higher rate than 300ksps, in order to improve performance, so the LMS7200M FIRs are used for decimation/interpolation, which is what allows us to run the NCOs at a frequency higher than the sample rate. This is all handled behind the scenes by LimeSuite API.

The receive gain is set to 0.2 (normalized gain between 0 and 1 using LMS_SetNormalizedGain()), since there is a short run of coax cable between the LNB and the LimeSDR Mini, so the signal level is already very high. For the transmit gain, I have experimented a little to find the configuration I find most satisfying. This brings me into a discussion of how I like to modulate my SSB voice signal.

The main idea is that I speak normally into the microphone, and the audio gets modulated to RF, without any AGC or compression whatsoever. Compression is used to decrease the PAPR of an SSB voice signal in order to get more power of a PA when the SNR of the signal is not very high. This distorts the voice, but increases the average power, so it is easier to copy the signal in low SNR conditions.

In HF, propagation is unpredictable and we are used to dealing with low SNR signals. Thus, using some form of speech processing to decrease the PAPR is commonplace and helpful. Extreme speech processing is often used in contesting or DX. By boosting some audio frequencies, speech becomes easier to understand in harsh conditions. Unfortunately this has made us get used to all sorts of distorted voice signals.

In contrast, in the QO-100 narrowband transponder the propagation is completely predictable and there is a lot of dynamic range available. The rule is that you should not be stronger than the transponder beacons, but the digital beacon is usually some 18dB (in 2.7kHz of bandwidth) above the transponder noise floor. I have already spoken about the possibilities this gives here, but applied to SSB voice it just means that there is no reason to use speech compression.

Therefore, I just adjust the transmit level so that on voice peaks the average power is approximately at beacon level (where the average power is measured with a reasonable short averaging interval in order to see the power going up and down constantly with the voice articulation). Sometimes the peak envelope power might be well above the beacon level, but if averaged over a whole word, the power I use from the transponder is smaller than that of other stations that use compression.

Since I am using no AGC, there are some precautions and considerations that need to be taken. First of all, the IQ output from the GNU Radio flowgraph should not clip, since clipping would splatter over the whole transponder. This has made me set a rather low gain in the GNU Radio transmitter flowgraph. Most of the time, only a small portion of the LimeSDR Mini DAC full range is used.

On the other hand, since the DAC is run at a reduced range on purpose, the gain of the LimeSDR Mini should be set much higher than in the usual case when DAC is run near full scale. In my case I have no problems with uplink power, because my station includes a 100W PA (intended for the wideband transponder), so I have no concerns about my peak power being limited.

While adjusting the TX gain of the LimeSDR Mini, I have observed that below a normalized gain of 0.62 the TX calibration fails because the TX signal is not strong enough to perform the calibration. The main difference between a successful and an unsuccessful calibration is a huge DC spike (LO leakage) in the case of an unsuccessful calibration. Therefore, it pays off to use a higher gain than desired if this means getting a successful calibration. In this case, the same transmit power can be achieved by using a smaller signal amplitude, and the spurious signals will be much lower. The calibration depends on the gain, so the trick of calibrating at higher gain and then reducing the gain doesn’t really work.

In my case this is not a problem, because for SSB voice I have determined that a normalized gain of around 0.8 is appropriate. If I was going to use only digital signals, which have a much lower PAPR, it would make sense to use the DAC at near full range, and set a much lower range. That would give a gain lower than 0.62 to be at beacon level while running with the DAC near full range. Therefore, it would be better to set the gain at 0.62 and reduce the signal amplitude. In the extreme case where there is a lot of gain in the PA after the LimeSDR and using a gain of 0.62 is absurdly high, an attenuator after the LimeSDR may be used.

Another interesting phenomenon I have encountered in the receive path is that of DC bias. The LMS7200M ADCs have 12 bits. I am using the LMS_FMT_I16 stream format with the LimeSuite API, which puts the 12 bits as the most significant bits of an int16_t. The issue is that the bins of the ADC are not symmetric about zero: the bin corresponding to -1 (-16 when read as an int16_t) has the same probability as the bin corresponding to 0, etc. Therefore, there is a DC bias of -1/2 LSB. It is trivial to correct this by adding 8 to the int16_t samples. If the bias is not corrected, then a DC spike will appear in the middle of the passband, even though a low-IF is being used. Therefore, limesdr_linrad corrects for this bias before streaming the samples to Linrad.

Regarding the Linrad protocol, I have taken most of the implementation from gr-linrad, my GNU Radio out-of-tree module that implements a streamer using the RAW16 and RAW24 Linrad protocols. You can see more information about gr-linrad in this post, including a description of the Linrad network protocol and instructions about how to set up Linrad to receive from the network.

The limesdr_linrad streamer uses the RAW16 protocol. For simplicity, the Linrad TCP server is not implemented within limesdr_linrad, but rather in a Python script called This script needs to be run simultaneously to limesdr_linrad.

In the figure below you can see my Linrad setup as I am having a QSO with Wolfgang Knauert DM3AWK.

Receiving the QO-100 narrowband transponder with Linrad

To receive TX samples from GNU Radio, socat is used. The limesdr_linrad reads samples from a FIFO in /tmp/txfifo to which socat is expected to write. This gives us flexibility in how to interface with GNU Radio and also simplifies the implementation of limesdr_linrad. I am using TCP, so I am running a socat server on the BeagleBone Black by doing

socat -u TCP-LISTEN:6969,fork PIPE:/tmp/txfifo

The GNU Radio flowgraph that I am using to transmit has a simple GUI shown below. The flowgraph gets audio from an Audio source (pulseaudio), modulates it on SSB and sends the samples by TCP.

GNU Radio SSB transmitter for QO-100

The spectrum and waveform are shown to help in adjusting the gain. There are fields to enter the gain (or signal amplitude), a frequency correction to compensate the LimeSDR Mini clock drift, and the corresponding downlink frequency of the transmit signal (taking out the 10489MHz part that is common to all the transponder signals). This is done to simplify tuning to the same frequency in Linrad (which also shows downlink frequencies) and the GNU Radio flowgraph. The uplink frequency is shown (without the leading 2400MHz part) for reference when logging the contact.

In my hardware setup, the PA PTT is controlled from a GPIO on the BeagleBone Black. A simple shell script shown below is used to control the PTT easily.

echo $1 > /sys/class/gpio/gpio116/value

Currently I am logging in to the BeagleBone Black with SSH to toggle the PTT. In the future, I will allow the PTT to be controlled directly from the network. Another thing that I want to add to my software setup is Hamlib control, at least to toggle the PTT automatically from digital modes software. I will probably base my Hamlib software on the Python example by Jim Ahlstrom N2ADR.

Another improvement that I want to make to the station is to use an external reference with the LimeSDR Mini. This is probably the first that I will tackle, since it will make operating the station much more comfortable, removing the need to keep adjusting the frequency offset.


  1. Hi, I just wanted to know if you have had any fifo underruns on the transmitting side? I tried your code and even when having everything running on the same machine to eliminate network issues I get sudden underruns which leads to stuttering of the transmitted audio.

    Regards, and thank you for this excellent writeup and code!


    1. Hi Joshua, at 300ksps (used when the transponder was 250kHz) I didn’t have much problems. At 600ksps (which I use now for the kHz transponder) I had some problems with TCP, but switched to UDP and everything is working fine.

Leave a comment

