Implementation of a multi-carrier DMR/YSF/M17 base station transceiver in software defined radio (MMDVM, GNU Radio, LimeSDR)

By Adrian, Sat 25 March 2023, in category Amateur radio

LimeSDR, amateur radio, DMR, Digital voice

Multi-carrier base station transceiver for DMR, YSF, M17 etc. with MMDVM and LimeSDR hardware

The idea of using a SDR for FM multi-carrier transmissions is quite old. Apart from all the industry implementations, some other open source projects which used this concept are Osmocom analog and OsmoTRX GSM BTS transceiver.
The multi-carrier base station transceiver for some amateur radio digital voice modes, described here, is an evolution from the previous attempt to use MMDVM with LimeSDR transceivers which can be read here: Implementation of an ETSI compliant DMR base station with the LimeNet Micro SDR working in TDMA mode (two inbound timeslots). It uses a fork of MMDVM (written by Jonathan Naylor G4KLX) created by Rakesh Peter which enables running MMDVM on a Linux host and communication with MMDVMHost via a pseudo TTY instead of actual serial port.

The code used for this is free software and can be found in these repositories:

There is also a mirror of all the code on Github. The code has seen limited testing due to a large number of test scenarios and limited resources. There are still a number of known issues and bugs, version 0.8.9 of QRadioLink fixes some of the serious ones and requires tag 1.0 in MMDVM-SDR.


Because I did not have dedicated MMDVM hardware for digital voice, but I had SDR hardware, I've created GNU Radio flowgraphs (integrated in QRadioLink), and attempted to transmit and receive multiple DMR, Yaesu System Fusion and M17 channels at the same time using LimeSDR hardware (LimeNet-Micro, LimeSDR-mini). Theoretically other modes supported by MMDVM should work as well, but I did not have the necessary radios to test them. At the moment the code is very much LimeSDR specific, but in principle it should be possible with some effort to adapt it to work with any other hardware which supports timestamps: Ettus USRP, BladeRF etc.

I had several reasons to pursue this avenue into SDR enabled multicarrier functionality. First, while MMDVM can be configured to run all these digital voice modes at the same time and connect to multiple amateur networks, only one type of transmission from a single network and a single digital mode can occur at a time on the 12.5 kHz radio channel. Our local amateur community is spread across at least two DMR and two System Fusion networks, so I consider it desirable to connect MMDVM to all these networks. However, since MMDVM uses a single radio channel and due to implementation details, simultaneous radio traffic from multiple networks cannot occur over the radio interface.
Traffic on a DMR network will interfere with traffic on the System Fusion network or traffic from another DMR network, with MMDVMHost deciding which one takes priority and preventing the other traffic from being transmitted. Typically people resolve this issue by using multiple MMDVM-capable hardware (hotspots, repeaters etc.) each of them dedicated to a single network. Another limitation, mostly arising from one of the DMR networks' architecture, is that both the national talkgroup and the emergency talkgroup (112) can only be mapped on timeslot 1. So a hotspot or a repeater connected to this DMR network will encounter issues if both the national talkgroup and the emergency talkgroup carry active voice traffic. Again, using multiple dedicated MMDVM hardware and assigning the talkgroups on different devices could resolve this conflict. Increasing system traffic capacity comes with its own set of problems (described below).

MMDVM capable hardware generally seem to consist of a Linux computing platform on a low power ARM platform (Raspberry Pi, Pi Zero etc.) which runs MMDVMHost and other user interface and configuration software (dashboard etc.), coupled with a dedicated board carrying the RF chip (ADF7021 or similar) and the microcontroller running MMDVM. Having multiple MMDVM radio channels requires using multiple such hardware combinations which does not scale very well, both in cost and in maintenance. However the upgrade to a SDR based implementation might not be justified in most cases when MMDVM specific hardware is already available.

On the other hand, having a SDR device already available, this function can be accomplished with a single radio, a single power amplifier, a single antenna and a single computing platform. The tradeoff is the increased software complexity, less reliability (explained below in the Issues section) and the greater necessary computing power (less energy efficiency) required by the increased sample rate. By using general purpose SDR platforms, the number of channels which can be transmitted at the same time is only limited by software design, CPU usage and configured sample rate of the device. The solution is probably less energy efficient than having multiple MMDVM boards, but it was fun and challenging to work on it and I also learned some things about the inherent strenghts and limitations of SDR and specifically LimeSDR hardware. As it was pointed out to me this sort of setup might not be practical at all in areas with a high spectrum usage.

It is also possible this could open some investigation avenues towards amateur radio trunking / DMR tier III but that would require a lot more serious effort that would probably span both MMDVMHost and MMDVM. At the moment the modifications to MMDVM are very small and involve mostly the I/O code and the code used for serial communication to MMDVMHost. The DMR TX code was also slightly changed to clear transmit buffers when switching to an idle state. This avoids sending samples from a previous partial transmission to QRadioLink and confusing the transmit timing code.

Implementation details

Right click on the images to see in original size

The high level architecture of all the necessary components can be seen below

Essentially multiple MMDVM and MMDVMHost processes are started, one pair for each used channel. The QRadioLink process starts and runs the GNU Radio C++ flowgraphs, interfacing with MMDVM-SDR via ZeroMQ IPC sockets, and sending samples from GNU Radio to the SDR via the dedicated LimeSDR support libraries. QRadioLink also has a CLI control interface which can handle tuning, gain control and other less important functions like configuring the offset between RX and TX etc.
The SDR can only be operated for this purpose in duplex mode with a significant offset between the transmit and receive frequencies (3 MHz offset was tested). Also, because of relatively high power of the terminal radios, the receive gain needed to be turned way down (setting 10 or less) if used close to the SDR. The ZeroMQ sockets could be replaced with TCP or UDP sockets, but I found ZeroMQ to be faster to set up and use, and there is a lot of documentation and relevant code in GNU Radio resources about using it. Even though ZeroMQ is configured here to use inter-process communication (IPC), it does support TCP sockets, which could be used if a distributed computing architecture is desirable. At the moment the IPC communication method is still hardcoded in both MMDVM-SDR and QRadioLink.

Within QRadioLink, the high level schematic of the GNU Radio blocks and connections for the receiver can be seen below.

The receiver operates at 240 ksps, each 12.5 kHz radio channel apart from channel zero is shifted to baseband in a separate flowgraph branch, filtered, FM demodulated and downsampled to 24 ksps, which is the sample rate used by MMDVM. Rakesh Peter pointed out to me that using a polyphase channelizer would be a more efficient way in GNU Radio to extract the channels, but I did not have the time to implement and test this idea yet.
The GNU Radio sink (which is the last GNU radio block before samples exit QRadioLink) has a number of inputs equal to the number of configured channels and buffers internally a number of samples equal to one DMR timeslot (this was determined to be necessary for proper MMDVM decoding by experimentation) and then sends them in a ZeroMQ packet to each MMDVM-SDR instance for the actual processing. Time tags from the gr-limesdr source block are used to keep an internal time reference for DMR burst synchronization purposes. Metadata marking the start of DMR timeslots 1 and 2 is also added to the sample packet, if the burst synchronizer decides the current sample is at the start of a timeslot, based on a simple sample counter. The metadata is used by the MMDVM DMR receiver to decode the proper DMR timeslot. It is not used at all in case of Yaesu System Fusion and M17 receivers since they do not use TDMA multiplexing.

The high level description of the GNU Radio blocks and connections for the transmitter can be seen below.

A single GNU Radio source block (entrypoint in the GNU Radio flowgraph) is reponsible for retriving samples from multiple MMDVM instances, which are then FM modulated, resampled to the final sample rate and shifted away from baseband according to the channel position in the raster. Although this source block runs in a single thread and collects all MMDVM channel samples, it has a number of outputs equal to the number of channels configured. Each channel is then processed in a separate branch of the flowgraph before finally being super imposed by addition into a single 240 ksps transmission. Again, as pointed out by Rakesh, the flowgraph might benefit from using a polyphase synthesizer. This is something that still needs to be investigated in the future.
Before final addition, the sample levels are divided by the number of carriers used, to keep the overall level between -1.0 and 1.0 as desirable for the DAC.
Although the FM modulation used by the digital voice modes is constant envelope, the image rejection filters used when resampling to the final sample rate do introduce some amplitude variation. An attempt to mitigate this was to slightly reduce the digital gain by adjusting the levels to values lower than 1.0 to avoid clipping in the DAC.
Because the output power of the SDR is divided among all transmitted carriers, a doubling of the number of used channels leads to a 3 dB reduction (or halving) of effective power per carrier. In the 7 channel configuration used for testing, the total SDR output power is -3 dBm and the useful power per channel is -11 dBm. So the hypothetical power amplifier used needs to be scaled accordingly.

Transmit timing is controlled by the GNU Radio scheduler which asks for more samples as the SDR FIFO fills and the hardware transmits the samples at the specified time. The timing is only relevant for DMR transmission which uses TDMA. It is irrelevant for Yaesu System Fusion and M17.
The GNU Radio source block, in order to not block the flowgraphs if one of the MMDVM channels has no samples to output, will continuously generate samples with a value of zero for all channels which have no traffic. These zero samples, once FM modulated downstream, will generate a continuous carrier for unused channels, which is not desirable. So an additional block is added after the FM modulator, which zeroes out this carrier, based on knowledge from the source block, which tags idle channel samples accordingly. Once such a tag containing the number of samples to be zeroed out is encountered, the block sets the samples to zero on its output. Without the presence of this tag (on active channels), the input of the block is simply copied to the output.

The transmitter code was a little more complex. Apart from time synchronization, there were some other issues in working together with MMDVM, which manifested especially for DMR. My understanding of MMDVM was not quite strong, but I think essentially the DMR TX code will generate idle data for idle timeslots as fast as it can as long as the TX buffer has space available. In my first attempts, MMDVM was pushing samples directly to QRadioLink without any knowledge of the GNU Radio flowgraph state, emptying its buffer each time, thus was running either too fast or too slow and was not synchronized at all with the GNU Radio flowgraph.

This manifested into issues with repeated trasnmissions: either frequent interruptions, missed audio frames if MMDVM was running faster than the GNU radio source block, or conversely, slot timing overruns, slot rollovers and other such problems if the MMDVM thread was running slower than the GNU radio source. Mostly network to RF and RF to network was fine with a certain hardcoded sleep time in MMDVM, which made it hard for me to find and understand the issue during testing until I was able to test the repeated transmission with two DMR terminals. Then I realized there was missing synchronization and the solution was not usable at all. I found it better to let the GNU Radio scheduler drive the timing of MMDVM and only empty the MMDVM buffer upon request by GNU Radio. The major change was the type of ZeroMQ socket used. Instead of using a PUSH socket in MMDVM and PULL socket in GNU Radio (which is a type of fire-and-forget data transfer), I changed it to a request-reply socket, where the GNU Radio scheduler decides when to ask for samples and sends a request to MMDVM, and MMDVM listens for such a request and replies with the needed samples (or the number zero if none are available), thus keeping its internal buffer filled until it is time to transmit a new batch consisting of an entire DMR timeslot. This mostly eliminated the overrun / underrun issues (although not completely).

DMR burst timing method is shown in image below

As it can be seen, early samples are time aligned to the last timeslot, while late sample packets will be transmitted with a delay based on the actual time they were received from MMDVM. While this could in theory generate a lot of time gaps manifested in underruns, in practice this method works 98 percent of the time. Some small periodic interruptions in locally repeated transmissions could be blamed on this timing and slot assigning method. So there is still a lot of room for improvement.

Transmit timing used for DMR is driven only by channel 1 (baseband channel). This results in an implementation-specific limitation. If any of the used channels need to be mapped to a DMR channel, channel 1 should be a DMR channel or must remain unassigned. Adding time tags for multiple channels resulted in time conflicts leading to dropped sample packets by the SDR device.

The source block requires SDR timing knowledge to correctly generate timestamps. This means that we need the receive flowgraph to work first and create the internal time reference based on received time tags, before outputting samples from the source block. The MMDVM source block outputs exactly 720 samples each time, corresponding to a full DMR timeslot.

This is where the LimeSDR specific code occurs. If the GNU Radio source runs unconstrained (by design in GNU Radio the scheduler will run the source thread as fast as it can as long as the buffers are not all filled up and there is no backpressure in the flowgraph), the first few runs will occur very fast, generate idle samples (zero) which will fill the LimeSDR FIFO. This leads to a latency of around 10 seconds or more on transmit which I was not able to eliminate despite configuring a lower size of the FIFO. The solution I was able to come up with was to constrain the speed of execution of the GNU Radio source thread by adding a sleep value centered around one DMR timeslot (30 milliseconds) but also controlled by the LimeSDR FIFO fill state. An empty FIFO will lead to faster (more frequent) thread execution while a FIFO which starts filling up above 50 percent will slow down the source thread execution. This requires knowledge about the internal LimeSDR FIFO fill state and thus makes the code specific to this device family. I suspect that there is a better way to avoid this issue and eliminate the hardware specificity. The sleep added in the source thread is the reason for some additional issues, including mandatory latency increase (the samples need to be time tagged a lot further in the future than needed (30 msec - 100 msec depending on CPU power).

Operational setup

The SDR hardware used were the LimeNET-Micro operated in USB device mode and the LimeSDR-mini. They mostly work the same and are usable at the sample rate of 240 ksps. The LimeNET-Micro has a more accurate internal clock and can be disciplined with a GPS signal. It also runs less hot than the mini variant.

The computing platform chosen was a 4-core / 8 thread x86_64 mini computer used headless, with Ethernet, Wifi and USB connectivity. While the whole system can run on an ARM64 platform, it does not perform very well. With a reduced number of channels (4 or less) and a degraded performance in DMR mode, I was also able to use a Rockchip board with 6 cores big.LITTLE architecture. I hope some future optimization of the code will enable better performance on ARM. But at the moment there are still issues with DMR timing on the ARM platform.

The operating system used was Debian Bullseye with GNU Radio 3.8. Debian Bookworm with GNU Radio 3.10 still remains to be tested, although the code compiles from a dedicated branch for 3.10 in QRadioLink.

Due to the new way of communication with MMDVM-SDR, the startup order of components has become mandatory. As many MMDVM instances need to be started as the number of channels configured in QRadioLink, otherwise the GNU Radio flowgraph will not work at all. MMDVM instances need to be started before QRadioLink, and I've used a short bash script to automate this. QRadioLink is started without the GUI in headless mode using the --mmdvm argument.

After the initial startup sequence which can be followed in the console, QRadioLink will only output messages on stdout and stderr if sample packets are dropped by the SDR or the FIFO size drops too much (below 30 percent) or fills too much (above 70 percent). Any such output can suggest issues. It is normal for a few sample packets to be dropped right after startup.

The console control interface of QRadioLink can be accessed by telneting into local port 4939 (or port number configured in the settings). From here some adjustments can be made at runtime (gain, receive frequency etc.)

Tmux is used to start and monitor as many MMDVMHost instances as many MMDVM channels are used. By using separate configs, one can assign channels and modes as desired. Care must be taken that the correct pseudo-TTY is pointed to in the config. RX and TX levels for MMDVM can be adjusted also depeding on mode, but the most important settings involve inverting TX and RX samples.

Outcome and measurements

Here are the results as measured on the spectrum analyzer for a 7 carrier configuration (7 channels in QRadioLink, 7 MMDVM instances). Occupied spectrum is 162.5 kHz. Edge channels at -200 and +200 kHz are left unoccupied. Several filter settings were tried, and although they are hardcoded in the GNU Radio flowgraphs, they could be made configurable.

Transmitted spectrum for 7 DMR channels

DMR MER on single channel

Degradation and improvement of MER can be observed by tightening or relaxing the image rejection filters. This also affects adjacent channel rejection figures. Only tested channel raster of 25 kHz. 12.5 kHz separation is available in the config but I judged it too problematic to use so far.

ACPR on center channel

ACPR on edge channel


This is work in progress. Not all issues may be known yet, but here are some of those found so far during testing.


For operation instructions, refer to the MMDVM operation documentation. The documentation may be incomplete in places and contain outdated information while this topic is still being worked on.


This article was made possible by Jonathan Naylor G4KLX (the author of MMDVM and MMDVMHost), Rakesh Peter (the author of the Linux port of MMDVM, MMDVM-SDR) and the GNU Radio, M17 and MMDVM communities with useful advice.