Using radio devices

The SDRVM has two types of radio interfaces :

  • Native, where specific parts of the SDRVM internal code have been written and implements direct calls to the vendor API,
  • Generic, using the SOAPY SDR interface: any radio having a SOAPY driver can be used with the SDR Virtual Machine.

The following native drivers are available:

  • AirSpy SDR from Airspy
  • BladeRF from Nuand, supporting BladeRF 1 and BladeRF 2,
  • LimeSDR from LimeMicro,
  • Cloud-S3 from SDR-Technologies.

All these interfaces are managed via the JSRadio object. Using a radio device is done in two steps :

  • First, the device needs to be detected and connected to the SDRVM. It is no longer available for other applications running in the host computer,
  • Then, the connected device can be used through the JSRadio interface.

Note

Connecting and using a radio device (native or Soapy) takes a fiew seconds because of internal initialisation. It is between 2 and 4 seconds, depending on host computer and device type.

Soapy RTLSDR example

We will first check that the RTLSDR interface is correctly detected by Soapy. From your command-line interface, type the following command :

 SoapySDRUtil --probe

This will search for connected devices that can be handled via the Soapy interface. For example:

######################################################
##     Soapy SDR -- the SDR abstraction library     ##
######################################################

Probe device 
Found Rafael Micro R820T tuner
[INFO] Opening Generic RTL2832U OEM :: 00000001...
Found Rafael Micro R820T tuner

----------------------------------------------------
-- Device identification
----------------------------------------------------
  driver=RTLSDR
  hardware=R820T
  index=0
  origin=https://github.com/pothosware/SoapyRTLSDR

----------------------------------------------------
-- Peripheral summary
----------------------------------------------------
  Channels: 1 Rx, 0 Tx
  Timestamps: YES
  Time sources: sw_ticks
  Other Settings:
     * Direct Sampling - RTL-SDR Direct Sampling Mode
       [key=direct_samp, default=0, type=string, options=(0, 1, 2)]
     * Offset Tune - RTL-SDR Offset Tuning Mode
       [key=offset_tune, default=false, type=bool]
     * I/Q Swap - RTL-SDR I/Q Swap Mode
       [key=iq_swap, default=false, type=bool]
     * Digital AGC - RTL-SDR digital AGC Mode
       [key=digital_agc, default=false, type=bool]
     * Bias Tee - RTL-SDR Blog V.3 Bias-Tee Mode
       [key=biastee, default=false, type=bool]

----------------------------------------------------
-- RX Channel 0
----------------------------------------------------
  Full-duplex: NO
  Supports AGC: YES
  Stream formats: CS8, CS16, CF32
  Native format: CS8 [full-scale=128]
  Stream args:
     * Buffer Size - Number of bytes per buffer, multiples of 512 only.
       [key=bufflen, units=bytes, default=262144, type=int]
     * Ring buffers - Number of buffers in the ring.
       [key=buffers, units=buffers, default=15, type=int]
     * Async buffers - Number of async usb buffers (advanced).
       [key=asyncBuffs, units=buffers, default=0, type=int]
  Antennas: RX
  Full gain range: [0, 49.6] dB
    TUNER gain range: [0, 49.6] dB
  Full freq range: [23.999, 1764] MHz
    RF freq range: [24, 1764] MHz
    CORR freq range: [-0.001, 0.001] MHz
  Sample rates: [0.225001, 0.3], [0.900001, 3.2] MSps
  Filter bandwidths: [0, 8] MHz

We do have a RTLSDR interface detected by SOAPY. We can now use it in the SDRVM.

// Ask the Soapy interface to search for a RTLSDR compatible device
var rx = Soapy.makeDevice( {'query' : 'driver=rtlsdr' , 'device_name': 'radio'});
// Test that we do have a valid object in return
if( typeof rx != 'object' ) {
 print('no radio ?');
 exit();
}

if( !rx.isValid()) {
 print('no radio ?');
 exit();
}

Note that we have added a specific parameter called 'device_name', we will soon see where this is useful.

BladeRF example

var rx = BladeRF.makeDevice( { 'device_name' : 'radio'} )
if( typeof rx != 'object' ) {
 print('no radio ?');
 exit();
}

if( !rx.isValid()) {
 print('no radio ?');
 exit();
}

Basic configuration

Now that we have a way to configure and use our radio, we can set the sampling rate, the center frequency etc. :

// set sample rate
if( rx.setRxSampleRate( 2e6 )) {
  print('Sample rate changed');
}
// Set center frequency
rx.setRxCenterFreq( 868.57 ); // This is in MHz !

Using the device in another task

Assume you need to use the same device in different tasks. You then need a way to access the same object. One easy way is to use the 'device_name' value we added in the configuration step.

We can do this by creating directly a JSRadio object with the correct name :

var device_name = 'radio' ; // the one we used in the configuration task
var rx = new JSRadio(device_name);
// set sample rate
if( rx.setRxSampleRate( 2e6 )) {
  print('Sample rate changed');
}
// Set center frequency
rx.setRxCenterFreq( 868.57 ); // This is in MHz !

Capturing RF blocks

The device is now configured on the correct frequency, with the correct sampling rate. We can now request a capture of 1M samples:

 var samples = rx.Capture( 1e6 ) ;

 print('----------------------------------');
 print('Collected data details:');
 samples.dump();

Running the full example (download here for code with RTLSDR) will produce the following output :

Found Rafael Micro R820T tuner
[INFO] Opening Generic RTL2832U OEM :: 00000001...
...
...
(boot:0)> Sample rate changed
[INFO] Using format CF32.
(boot:0)> ----------------------------------
(boot:0)> Collected data details:
(boot:0)> IQData object dump:
(boot:0)>  name       : no_name
(boot:0)>  sample rate: 2.000 MHz
(boot:0)>  length     : 1000000 samples
(boot:0)>  Center Frq : 868.570 MHz
(boot:0)>  Channels   : 1
(boot:0)>  duration   : 0.500 secs. [500.0 msecs]
(boot:0)>  GPS status : NO fix
(boot:0)>  Attribute  : not set

Passing IQ data from one task to another

A good way to speed-up the processing is to spread it over multiple tasks. One task generates the IQ Data - from a capture for example - while the second task does the processing.

We will in the following example do :

  • A main task will turn on the radio, perform IQ captures and push them to a second one, this will be our 'producer' task.
  • The second task will just estimate the RMS value of the collected data and print it, this will be our 'consumer' task.

The two tasks will use a Queue object : the producer task will 'push' IQ blocks, the consumer task will dequeue the data. An empty block (no samples) means that the consumer task must stop.

Producing data

The producer task code (download here for code with RTLSDR):

// Ask the Soapy interface to search for a RTLSDR compatible device
var rx = Soapy.makeDevice( {'query' : 'driver=rtlsdr' , 'device_name': 'radio'});
// Test that we do have a valid object in return
if( typeof rx != 'object' ) {
 print('no radio ?');
 exit();
}

if( !rx.isValid()) {
 print('no radio ?');
 exit();
}

// set sample rate
if( rx.setRxSampleRate( 2e6 )) {
  print('Sample rate changed');
}
// Set center frequency
var start_frequency = 800 ;
rx.setRxCenterFreq( start_frequency ); // This is in MHz !

// Create a queue to send data 
var queue_name = 'IQ_Data_Queue' ;
var q = Queues.create(queue_name);

// Create the consumer task
var id = createTask('sdr_consumer.js',queue_name);

// perform 10 captures and send them to the consumer task
for( var i=0 ; i < 10 ; i++ ) {
     var samples = rx.Capture( 1e6 ) ;
     // send samples to processing task
     q.enqueue( samples );
     // step 1 MHz up
     start_frequency = start_frequency + 1 ;
     rx.setRxCenterFreq( start_frequency );
}

// send one empty block to unlock processing task
q.enqueue( new IQData('empty') );
waitTask( id );

Consuming data and using it

The consumer task code (download here for code with RTLSDR):

var queue_name = argv(0);
var q = Queues.create(queue_name);

// consume IQ data from queue
for( ;; ) {
     var IQ_received = q.dequeue(true) ; // we block and wait to receive something so CPU is very low...
     if( IQ_received.getLength() == 0 ) {
         print('Empty block, end');
         exit();
     }
     var rms = IQ_received.rms();
     var fcenter= IQ_received.getCenterFrequency()  ;
     print('Received RMS level is :' + rms + ' at ' + fcenter + ' MHz');
}

Adding metadata

The previous example introduced how to send IQ samples from one task to another using the Queues. It can be also required to add specific data for the processing task. An easy way to proceed is to attach 'attributes' to the IQ data before pushing it to the Queue.

The subset of code below shows how to implement this in the previous examples.

  • In the 'producer' task :
    ...
    var samples = rx.Capture( 1e6 ) ;
    // Add specific attributes to the IQ block
    var metadata = {
        'compute_rms' : true,
        'another_field' : 3.14,
        'yet_another' : 'please proceed'
    } ;
    samples.setAttribute( metadata );
    // send samples to processing task
    q.enqueue( samples );
    ...
  • The added metadata can be used in the 'consumer' task as follows:
    ...
    var IQ_received = q.dequeue(true) ; // we block and wait to receive something so CPU is very low...
     if( IQ_received.getLength() == 0 ) {
         print('Empty block, end');
         exit();
     }
     // Read metata data
     var metadata = IQ_received.getAttribute() ;
     if( metatada.compute_rms == true ) {
         var rms = IQ_received.rms();
         var fcenter= IQ_received.getCenterFrequency()  ;
         print('Received RMS level is :' + rms + ' at ' + fcenter + ' MHz');
     }
    ...
Last update: March 26, 2023