Skip to content

AirStack Application Notes

Continuous Streaming

Data streams are guaranteed to be continuous in with the AirStack driver, SoapyAIRT. That is, there is no gap in time between the last sample in a buffer and the first sample in the following buffer. The following sections outline best practices to maintain a continuous data stream as well as highlighting conditions to avoid where streams may become discontinuous.

Errors - Anytime the SoapyAIRT API returns an error code, any currently active data stream can no longer be considered continuous. Different data streams share a common hardware resource, so errors with one stream can affect others. As a result, users may want to restart all streams if an error is encountered. For application compatibility, we do attempt to restart any stream automatically, however it is STRONGLY recommended that users manually restart affected streams to attempt to fix the root cause of the error. Error codes and their possible root causes are shown below:

Error Code Error Name Description
-1 SOAPY_SDR_TIMEOUT Inactive hardware that has not been properly configured. Has the stream not been activated?
-2 SOAPY_SDR_STREAM_ERROR Generic streaming error. See logs for more info.
-3 SOAPY_SDR_CORRUPTION Multiple channels in a single stream are reporting different issues (likely cause is a single channel had an overflow). See kernel logs for specific details.
-4 SOAPY_SDR_OVERFLOW All channels in the stream have overflowed. The application is not reading samples from the radio fast enough.
-5 SOAPY_SDR_NOT_SUPPORTED You are asking the AIR-T to do something that it cannot do.
-6 SOAPY_SDR_TIME_ERROR As of AirStack 0.5 you should not see this error reported by the AIR-T.
-7 SOAPY_SDR_UNDERFLOW See "TX Streams" below. You are not providing samples to the radio fast enough.

Note that SOAPY_SDR_TIMEOUT can also be returned in the event a HW triggered or timed command has been configured, but the triggering event (timestamp or trigger signal rising edge) has not occurred within a specified timeout. If this occurs, please either retry the same API call that returned the error, or increase your timeout accordingly.

​Finally, it should be noted that in order to make logging more verbose (especially useful when providing log information / output to customer support) that an environment variable may be set appropriately. This is done by export SOAPY_SDR_LOG_LEVEL=DEBUG in the terminal.

Multiple Channel Synchronization - For streams with multiple channels, there are normally no guarantees that the channels are synchronized. In the general RX case, reception starts at an arbitrary time, with each channel starting immediately after the previous channel. For TX, each channel's buffer is generally transmitted as quickly as possible, with no attempt to transmit each buffer at the same instant in time. With that said, AirStack 1.0 introduced the Time API and the capability to synchronize channels using timestamps. See the Time API Tutorial for details. Users of earlier versions of AirStack can also synchronize RX channels via use of the HW trigger functionality detailed in the Triggering Tutorial.

For RX, it is important to note that the Time API and triggering functionality operate on activateStream(). That is, once a stream is activated at a particular time or due to a triggering signal, all channels in the stream have started signal reception at the exact same instant in time. If there are any stream errors during reception, not only are all channels in the stream no longer synchronized, but the stream of data is no longer continuous. As a result, for RX, any stream error should generally be handled by calling deactivateStream() and then activateStream() to re-synchronize the stream. For TX related considerations, please see the section below entitled "TX Streams".

Changing SDR Settings - There are specific cases where changing a SDR setting will result in discontinuous data.

First, if the sample rate is changed, any active stream that uses the channel affected by the change will be flushed to remove any samples with the old setting applied. As a result, the data stream will no longer be continuous.

Second, all active data streams are no longer continuous if frequency is changed. This occurs because the RF front end must recalibrate for LO suppression and quadrature error correction. Note that this means that changing the TX frequency may affect RX streams and vice-versa.
Overall, the best practice is to change SDR settings/parameters without an active stream (i.e., deactivate all streams, apply the new setting, and then re-active the streams). We perform these steps inside of the AirStack drivers if an active stream is detected while SDR settings are being changed because we recognize that not all applications can control when settings are being manipulated. With that said, we recommend restarting streams manually in the application code itself in order to explicitly document the fact that a discontinuity will occur when the setting is being changed.

TX Streams - For the general case of synchronous I/O1, the SoapyAIRT API expects all TX buffers passed to it to be continuous, unless the SOAPY_SDR_END_BURST flag is set when calling writeStream(). That is, without the flag set, the API expects each buffer to be part of a continuous transmission. The flag being set indicates that a transmission has ended and the API expects there to be a gap in time between the current call to writeStream() and any subsequent data transfer.
Note that, by default, underflow conditions (gaps in time between two buffers we are expecting to be continuous) are not reported by the SoapyAIRT API. This is due to compatibility issues with returning the error code and how other applications handle the error. Users can enable underflow checking/reporting by adding report_tx_underflow=true to the device arguments passed in to the SoapySDR constructor (it is not currently recommended to enable this feature in any GNU Radio applications).
As of AirStack 0.5.3, the transmit buffer can be managed through the SoapySDR API via the tx_buffer_size device/stream argument. We recommend that you set the TX buffer size to the largest transfer size (in number of samples) you expect to make in your application, as transfers larger than this will result in a truncated transfer (i.e., only the number of samples will be transmitted that can fit in the buffer). To set the tx_buffer_size, either set it in the device constructor (e.g., sdr = SoapySDR.Device(dict(driver="SoapyAIRT", tx_buffer_size="4096")) or when setting up a TX stream as shown below.

stream_args = dict(tx_buffer_size="4096")
tx_stream = sdr.setupStream(SOAPY_SDR_TX, SOAPY_SDR_CS16, channel_indexes, stream_args)

For advanced users interested in eking out every ounce of performance, the tx_buffer_size can be set to zero. This creates a scenario where the buffer passed to writeStream() will be used directly for DMA transfers (aka. "zero copy"). This may or may not result in a performance increase for your application, as a memory copy operation is replaced with a memory mapping operation. Benchmarking calls to writeStream() is recommended to test if and how much performance is improved. This feature can only be enabled for CS16 streams and requires that the buffer passed to writeStream() is aligned to a 128 byte boundary and is a multiple in size of 128 bytes. The best way to ensure such alignment is to use the C++ SoapySDR API and to allocate your TX buffer with std::aligned_alloc().

RX Streams - For RX, samples are continuously read from the device into a kernel buffer. This buffer is sized by default to prevent overflows as much as possible without totally monopolizing memory usage. Advanced users may want to change the size of this kernel buffer for various memory usage and performance considerations. For example, if your application calls for capturing a small number of samples before tuning to a new frequency, you may want to reduce the buffer size to prevent having to allocate a large (mostly unused) buffer. To do so, run the following commands on the AIR-T, with no active SDR applications running. Note that the units for buffer size are number of memory pages, where each page on the AIR-T is 4096 bytes. This setting does not persist between restarts of the AIR-T, but it can be scripted/automated to do so if desired.

sudo rmmod xdma
sudo modprobe xdma rx_buffer_size_pages=512

External Time and Clock Reference

Clock Reference

Every AIR-T model supports receiving an external 10 MHz reference clock to act as the device clock for applications that require synchronization between multiple AIR-Ts. This can be accomplished via piping in a signal to the REF input on the AIR-T. For details on the specifications of the external signal, please see the Product Guide) for your hardware.

To enable this feature in software, you will need to either set the clk_src device argument or call setClockSource() via the SoapySDR API (e.g., sdr.setClockSource(“external”)). The clk_src parameter can be set via the device constructor (e.g., sdr = SoapySDR.Device(dict(driver="SoapyAIRT", clk_src="external"))) or if you are using GNU Radio, simply add clk_src=”external" to the Device arguments field in the sink or source block.
The AIR-T model 8201 comes with a GPS module that can be used as a reference clock if desired. To enable this functionality, follow the same software steps as for the external reference, except replace external with GPS. No hardware modifications are required on the 8201 to use the GPS reference, simply enable this feature via software. Note that the GPS clock may not function if there is not a clear GPS signal to the device. As a result, it is recommended to revert back to the internal clock in the event of a transient GPS signal, as shown in the Python code example below.

sdr = SoapySDR.Device(dict(driver="SoapyAIRT"))
except RuntimeError as e:
    print("GPS Clock Reference Failed!")

Time Reference

Similar to the clock reference, an external PPS input port is present on all AIR-T models. In AirStack 1.0 and later, the time reference (i.e., PPS) can be enabled similarly to the clock (i.e., 10 MHz) reference. This can be accomplished via the SoapySDR Time API (e.g., sdr.setTimeSource("external")) or via the time_src device argument (e.g., sdr = SoapySDR.Device(dict(driver="SoapyAIRT", time_src="external"))). Note that users of GNU Radio can also set the time_src device argument in their source or sink block. Also, as with the clock reference, the AIR-T model 8201 has a GPS option to use the PPS from the GPS module as the time reference.
One important thing to test when setting the time reference is the presence or absence of the PPS when using an external or GPS time source. The following class provides a simplified interface to ensuring the PPS is present.

class TimeSourceValidator:
    def __init__(self, sdr):
        if not sdr.hasHardwareTime("pps"):
            raise ValueError("SDR hardware does not support PPS!")
        self._sdr = sdr
        self._init_pps_count = self._read_pps_count()

    def pps_detected(self):
        return self._init_pps_count != self._read_pps_count()

    def _read_pps_count(self):
        return int(self._sdr.getHardwareTime("pps") / 1e9)

The following example shows how this class can be leveraged to detect the presence or absence of a time reference.

sdr = SoapySDR.Device(dict(driver="SoapyAIRT"))
time_src_check = TimeSourceValidator(sdr)
# At this point, perform some operation that takes at least a whole second. That
# is, call the pps_detected() method once you are sure a whole second has passed
# and that the PPS time should have incremented. setupStream() is a good choice
# here as it generally takes a good amount of time to initialize the hardware to
# begin streaming.
rx_stream = sdr.setupStream(SOAPY_SDR_RX, SOAPY_SDR_CS16, [0])
# setupStream() has returned, so a good amount of time has passed, PPS should
# have incremented.
if not time_src_check.pps_detected():
    raise RuntimeError("PPS not detected!")

Independent APIs, Dependent Settings

The above two subsections showed how both the clock and time reference can be set on the AIR-T. It should be noted that these settings are controlled via two separate APIs, but in most cases an application will set both of these references to the same value. For example, it would be strange for an application to use an internal time source with a GPS clock source.

Heterogeneous CPUs: A Cautionary Tale

The AIR-T features a six core heterogeneous CPU where two cores are designed by Nvidia and are referred to by their codename: Denver. These cores are generally optimized for single thread performance, but create issues for multithreaded applications. Namely, applications can schedule some threads onto the Denver cores and some threads onto the A57 cores, which leads to issues with data sharing across the CPUs. As a result, all versions of AirStack effectively disable the Denver cores on the AIR-T to prevent performance degradation and to optimize power usage.

AirStack 1.0 introduced a new version of real-time Linux that appeared to have issues with disabling CPU cores during boot time. As a result, for this specific release of AirStack, the Denver cores remained enabled, but clocked to their minimum clock rate (to reduce power usage as much as possible). The operating system is also instructed to ignore these CPU cores and not schedule any work on them, but these kernel-level settings can be overwritten by applications via use of CPU affinity. As a result, users of AirStack 1.0 may want to manually disable the Denver cores to ensure that no work ever gets scheduled on the Denver cores and to minimize power usage as much as possible. This can be achieved by running the following commands with root privileges on the AIR-T as shown below.

dwd@airt-dev:~$ sudo su
[sudo] password for dwd:
root@airt-dev:/home/dwd# echo 0 > /sys/devices/system/cpu/cpu1/online
root@airt-dev:/home/dwd# echo 0 > /sys/devices/system/cpu/cpu2/online
root@airt-dev:/home/dwd# exit

USRP Hardware Driver (UHD) Support

AirStack allows for the AIR-T to run UHD applications via a translation layer known as SoapyUHD. As a result, applications previously developed for the USRP family of devices can be run on the AIR-T, but may require some minor modifications.

It should be noted that compatibility between UHD and the AIR-T is a "moving target" as various software components are always being updated. As a result, if you are experiencing issues with UHD, please reach out to us via our support channels and we will be happy to assist you.


It is recommended to install UHD and SoapyUHD through conda if possible. This will "sandbox" the UHD installation to a specific conda environment and not affect the installation of SoapySDR and other critical components of AirStack.

A good starting point is the GNU Radio Tutorial as this sets up a conda environment with just about everything needed to run UHD applications (including GNU Radio). However, there is one missing component (SoapyUHD) that does not get installed via this tutorial. SoapyUHD is absolutely critical for our purposes here, since it contains the software that translates between the UHD and SoapySDR APIs and allows for UHD to treat the AIR-T as a UHD device. As a result, we must install it prior to running any UHD applications on the AIR-T.

If you are following the GNU Radio Tutorial and/or already have a conda environment created, you can run conda install soapysdr-module-uhd to install SoapyUHD. Alternatively, if you are starting from scratch and creating a new conda environment, simply include soapysdr-module-uhd as a dependency when creating/editing the YAML file.

To verify that you have all the correct components installed, simply run uhd_find_devices and you should see the AIR-T report itself as a UHD device. From there, you can also run uhd_fft to run an example UHD application.

GNU Radio Blocks

Many users who are interfacing to SDR hardware via UHD are doing so through the UHD source and sink blocks in GNU Radio. For these users (i.e., those who have existing GNU Radio flowgraphs/applications using the UHD blocks), we strongly recommend simply replacing the source/sink blocks in your flowgraph with the appropriate SoapyAIRT blocks. This offers a number of advantages; namely it avoids layers of software/complexity and allows for the application to communicate with the hardware as directly as possible. That said, we also recognize that the UHD blocks are more mature and can offer more advanced features than the comparable gr-soapy blocks. That said, there are a few known issues with using the UHD blocks. We discuss these (and their associated workarounds) in the sections to follow.

Known Issue: UHD Source Multi Channel Sync

The UHD source block in GNU Radio will attempt to always synchronize channels in a multiple channel stream via a timed command. That is, signal reception is told to start some time in the future in order to ensure all channels start reception at the same time and are therefore synchronized to one another.

When working with the UHD source block, you may encounter a warning or error message from the AIR-T describing that all channels did not start signal reception at the same time. If you do not require RX channels to be synchronized, you can ignore these messages. If you do require synchronized channels, there is a workaround you can employ.

The root cause of the issue is that the start time inside the command to start signal reception is 100 milliseconds in the future from whatever the hardware is reporting "now" to be. This is a hardcoded value in the block itself. Based on the complexity of your flowgraph, this may or may not be sufficient time for the command to be issued to the hardware. However, via a call to the block's Python API we can edit this time parameter accordingly. The example code below shows how to set the start time to be 1.5 seconds in the future, which is plenty of time for the AIR-T.

time_now = self.uhd_usrp_source_0.get_time_now()
start_time = time_now + 1.5

Unfortunately, this parameter cannot be readily set through GNU Radio Companion (GRC). As a result, the above code must be manually added to the Python script generated by GRC each time the script is generated. We recommend placing these lines of code in the "top block's" constructor, above where all the connections between blocks are made.

  1. AirStack 1.0 introduced the concept of Asynchronous TX, where a TX buffer is first scheduled via writeStream() and then the status of whether or not the buffer was actually transmitted is reported via readStreamStatus(). See the Transmit Tutorial for details. In terms of continuity, due to how the buffers are scheduled, it is generally very difficult to ensure that two timed or triggered TX buffers occur without any gaps in transmission. As a result, underflows are only reported for synchronous (i.e., not timed or triggered) TX. 

Last update: January 17, 2024