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