Your first receiver : a simple example
In this example we will code a simple narrow band FM receiver working in the UHF PMR band. We will have :
- A RTLSDR receiver, tuned at 446 MHz, with a sampling rate of 1 MHz,
- A Digital Down Converter to extract a 12.5 kHz band somewhere in the 1MHz,
- A FM demodulator with AGC.
First architecture
We will begin with two tasks :
- A first task controlling the radio and setting it to push IQ samples in a Queue,
- A second task, reading the IQ Blocks from the queue and doing the DDC and the FM demodulation
The receiver task (second task) receives the following parameters :
- The Queue name,
- The frequency offset (where the DDC should be tuned).
The main task code (download here):
// 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( 1e6 )) {
print('Error, cannot change sample rate ?');
exit();
}
// Set center frequency
var start_frequency = 446 ;
rx.setRxCenterFreq( start_frequency ); // This is in MHz !
// Create a queue to send data
var queue_name = 'IQ_Data_Queue' ;
var fifo_from_rx = Queues.create(queue_name);
// declare the PMR Channels offsets
var pmr_channels = new Array();
pmr_channels.push( 446.00625 ) ; // channel 1
pmr_channels.push( 446.01875 ) ; // channel 2
pmr_channels.push( 446.03125 ) ; // channel 3
pmr_channels.push( 446.04375 ) ; // channel 3
// Create the consumer task
// We will receive PMR channel 0
var pmr_channel_offset = pmr_channels[0] - start_frequency ;
var id = createTask('fm_receiver.js',queue_name, pmr_channel_offset );
// engage streaming
if( !fifo_from_rx.ReadFromRx( rx ) ) {
print('Cannot stream from rx');
exit();
}
waitTask( id );
The fm_receiver.js task code (download here):
// read the queue name from the parameters
var queue_name = argv(0);
// read the channel offset
pmr_channel_offset = parseFloat( argv(1)) * 1e6 ; // convert to Hz for DDC
// access the queue
var fifo_from_rx = Queues.create(queue_name);
// configure the DDC
var ddc = new DDC();
ddc.setCenter( pmr_channel_offset );
ddc.setOutBandwidth( 12.5e3 );
// configure a FM demodulator and plug it in the DDC
var FMdemod = new NBFM('fm');
FMdemod.configure( {'modulation_index': 0.2} );
ddc.setDemodulator( FMdemod ); // tells the DDC to call the FMDemod object before output
ddc.setAGC(true);
var IQBlock = new IQData('iq');
// now loop : read IQ block from radio, do something
while( fifo_from_rx.isFromRx() ) {
if( IQBlock.readFromQueue( fifo_from_rx ) ) { // load samples from input queue into IQBlock object
ddc.write( IQBlock);
var fm_audio = ddc.read(); // output is IQData !!
// do something with the audio
// here we just dump data to see if it works
fm_audio.dump();
}
}
Download the two files and store them in the same folder. Then, from a Linux command line, do :
/opt/vmbase/sdrvm -f ./fm_rx_master.js
The FM demodulated blocks are received and the .dump() command displays the key informations. You should read something like :
...
(task:1)> IQData object dump:
(task:1)> name : no_name
(task:1)> sample rate: 12500 Hz
(task:1)> length : 1639 samples
(task:1)> Center Frq : 446.006 MHz
(task:1)> Channels : 1
(task:1)> duration : 0.131 secs. [131.1 msecs]
(task:1)> GPS status : NO fix
(task:1)> Attribute : not set
(task:1)> IQData object dump:
(task:1)> name : no_name
(task:1)> sample rate: 12500 Hz
(task:1)> length : 1638 samples
(task:1)> Center Frq : 446.006 MHz
(task:1)> Channels : 1
(task:1)> duration : 0.131 secs. [131.0 msecs]
(task:1)> GPS status : NO fix
(task:1)> Attribute : not set
(task:1)> IQData object dump:
...
Add a squelch
We want now to estimate the received signal strength and only process samples if the signal is strong enough. We will add in the receiver a state machine to handle the different states, as follows:
- Initial state (state=0 in the code) : we estimate the current RMS level (averaged) during 5 seconds , store the value (in fact rms - 5 dB) and then move to state 1,
- State 1: we wait for the signal to increase at least of threshold value. If this is the case, we move to state 2, otherwise stay in state 1,
- State 2: we are processing the signal. The transmission may have disappeared, so we check the level. If the level is too low, we move to state 3, otherwise stay in state 2.
- state 3 : we may have lost the signal, we stay in this state for a maximum of 1 second. After 1 second, if the signal has not increased, then we go back to state 1, otherwise go to state 2
(Download fm_rx_master.js task code)
(Download fm_receiver_squelch.js task code)
The core of the state machine is proposed below :
- We first add the following variables:
var ts = timestamp();
var rms = -140 ;
var last_rms = -140 ;
var engaged_rms = 0 ;
var threshold = 5 ;
var state = 0 ; // FSM
and the main loop of the code is now :
var fm_audio = ddc.read(); // output is IQData !!
// Average received power (moving average)
rms = 0.6*rms + 0.4*ddc.getRMS();
print( 'rms=' + rms , ' state=' + state ) ;
switch( state ) {
case 0:
// init - we wait for the RMS level to be stable
// We leave 5 second for this
var elapsed = timestamp() - ts ;
if( elapsed > 5000 ) { // 5000 milliseconds
state = 1 ;
print('Estimation of background level done, level is ' + rms);
}
break ;
case 1 :
// we wait for the signal to increase at least of "threshold"
if( rms >= (last_rms+threshold)) {
// we have a signal increase, engage
state = 2 ;
engaged_rms = rms - 5;
fm_signal_ok = true ;
print('We have something');
}
break ;
case 2 :
// we are receiving, check that the power s
if( rms < engaged_rms ) {
ts = timestamp();
state = 3 ; // we may have lost the signal, check later
print('Looks like signal is lost ?')
}
fm_signal_ok = true ;
break ;
case 3:
// we may have lost the signal
if( rms < engaged_rms ) {
// how long ?
var elapsed = timestamp() - ts ;
if( elapsed > 1000 ) {
// lost !
print('lost !');
state = 1 ; // go back to wait state
}
} else {
print('signal is back');
fm_signal_ok = true ;
state = 2 ;
}
break ;
}
last_rms = rms ;
// do something with the audio
// here we just dump data to see if it works
if( fm_signal_ok == true ) {
fm_audio.dump();
}
Monitoring all channels simultaneously
We will now implement a "all in one" system where all the channels are monitored simultaneously. For this purpose, we will use a DDCBank system that will allow us to have multiple narrow band receivers from the same radio stream.
- Our main function will create the DDCBank and allocate sub channels,
- We will run one task per channel.
The starting task is changed as follows:
// declare the PMR Channels offsets
var pmr_channels = new Array();
pmr_channels.push( 446.00625 ) ; // channel 1
pmr_channels.push( 446.01875 ) ; // channel 2
pmr_channels.push( 446.03125 ) ; // channel 3
pmr_channels.push( 446.04375 ) ; // channel 4
//
var tasks = new Array();
// Create a DDCBank with one channel for each PMR frequency
var multi_ddc = new DDCBank( rx, pmr_channels.length );
for( var i=0 ; i < pmr_channels.length ; i++ ) {
// compute offset
var pmr_channel_offset = pmr_channels[i] - start_frequency ;
// allocate one DDC here, with a 12500 Hz output
var ddc_channel = multi_ddc.createChannel( 12500 ) ;
// configure central frequency for this channel
ddc_channel.setOffset( pmr_channel_offset * 1e6);
// retrieve the ID so the task will be able to control the DDC
var channel_id = ddc_channel.getUUID();
// start the task
var tid = createTask( 'fm_receiver.js', channel_id );
// keep the task ID
tasks.push( tid );
}
// wait for all tasks to end
while( tasks.length > 0 ) {
var tid = tasks[0] ;
waitTask(tid);
tasks.shift();
}
Each receiver has to pull IQ data from its own DDC. We only provide here a basic code :
var channel_id = argv(0);
var ddc = new DDCBankChannel(channel_id);
// start the DDC receiver
if( ddc.start() == false ) {
print(' error starting channel id : ' + channel_id );
exit();
}
for( ; ; ) {
var iq = ddc.getIQ(true);
// do something with the IQ data
iq.dump();
}
(Download fm_multirx_master.js task code)
(Download fm_receiver.js task code)
Last update: February 14, 2022