plutotx

plutotx is a very simple console application that drives Adalm Pluto to generate a CW tone on the frequency and power level selected by the user.

I hope that the information and the C source that you will read below can be a small help for all developers who want to create a new SDR project.

From the archive available for download you will also find the binaries compiled for Windows and Linux x86, so it could also be useful to those who are not developers but simply have an interest in experimenting with Pluto.

Latest Release:

plutotx v.1.1 (04 august 2022)
File size: 705,962  Bytes
MD5 bb0d3b1e42ff892f377fcf5e2cdbb7f3
SHA1 30ade07ad365b23c211f85632c6e9edcb1efdaa0
SHA256 607ae42da31bddaf1528a4dadac7b8ce711a4e2e9ee9210726c3fb2e1d6f673c

- F: 2,147GHz limit
- I: Output bursts issue
- I: Switching off any DDS second tone active
- A: Include, library and instruction to compile under Windows and Linux

Older versions:

plutotx (4 august 2022)
File size: 685,626 Bytes
MD5 e241666bd41a84e5eff1ec44dab2283f
SHA1 e775f8fce1af59c833a850635b6df135cda9ef1b
SHA256 bf1e782b704b5671c901bfb53280a7f754475eba3d0204dd5bf25f99326e393b

To compiling and execute plutotx needs libiio library from ADI. Download and install the library for your specific SO from here: libiio

To launch, plutotx requires three parameters: frequency in kHz, a power level output expressed in dBm and a URI of device to connect (optional)

eg.: plutotx 432410 -10

plutotx will connect to default URI of device ip:192.168.2.1 if the third optional parameter is not specified.

How it works

I will describe the source of plutotx in a simplified form to facilitate understanding of the steps required to generate the CW tone:

  • Connect to Pluto device and acquire the context structure
  • From the acquired context test the model of the transceiver if an AD9364
  • Find devices physical transceiver and the DAC/TX output driver (FPGA)
  • Find channels of I, Q, TX chain and TX Local Oscillator
  • Apply the default configuration
  • Set the TX attenuator value
  • Set the TX bandwidth
  • Set the frequency and phase of the I and Q channels
  • Set the frequency of the TX Local Oscillator
  • Turn on the TX output activating channels I and Q raw

First of all, we need to connect to Pluto device and acquire the context structure:

struct iio_context *ctx;
ctx = iio_create_context_from_uri("ip:192.168.2.1");

From the acquired context test the model of the transceiver if an AD9364:

if((value=iio_context_get_attr_value(ctx, "ad9361-phy,model"))!=NULL)
  {
  if(strcmp(value,"ad9364"))
    stderrandexit("Pluto not expanded",0,__LINE__);
  }else
   stderrandexit("Error retrieving phy model",0,__LINE__);

Find devices physical transceiver and the DAC/TX output driver (FPGA):

phy = iio_context_find_device(ctx, "ad9361-phy");
dds_core_lpc = iio_context_find_device(ctx, "cf-ad9361-dds-core-lpc");

Find channels of I, Q, TX chain and TX Local Oscillator:

tx0_i = iio_device_find_channel(dds_core_lpc, "altvoltage0", true);
tx0_q = iio_device_find_channel(dds_core_lpc, "altvoltage2", true);
tx_chain=iio_device_find_channel(phy, "voltage0", true);
tx_lo=iio_device_find_channel(phy, "altvoltage1", true);

Apply the default configuration. This step is not necessary probably but recommended if using another SDR application before plutotx:

//enable internal TX local oscillator
if((rc=iio_channel_attr_write_bool(tx_lo,"external",false))<0)
  stderrandexit(NULL,rc,__LINE__);

//disable fastlock feature of TX local oscillator
if((rc=iio_channel_attr_write_bool(tx_lo,"fastlock_store",false))<0)
  stderrandexit(NULL,rc,__LINE__);

//power on TX local oscillator
if((rc=iio_channel_attr_write_bool(tx_lo,"powerdown",false))<0)
  stderrandexit(NULL,rc,__LINE__);

//full duplex mode
if((rc=iio_device_attr_write(phy,"ensm_mode","fdd"))<0)
  stderrandexit(NULL,rc,__LINE__);

//calibration mode to manual
if((rc=iio_device_attr_write(phy,"calib_mode","manual"))<0)
  stderrandexit(NULL,rc,__LINE__);

Line 18 sets the TX calibration mode to manual to avoid spikes on output over the TX power level sets by the user.

Set the TX attenuator value. The attenuator value is calculated from the value requested by the user minus the output power of Pluto that is about 10 dBm defined by REFTXPWR:

if((rc=iio_channel_attr_write_double(tx_chain,"hardwaregain",dBm-REFTXPWR))<0)
  stderrandexit(NULL,rc,__LINE__);

Set the TX bandwidth:

if((rc=iio_channel_attr_write_longlong(tx_chain,"rf_bandwidth",FBANDWIDTH))<0)
  stderrandexit(NULL,rc,__LINE__);

Set the scale, frequency and phase of the I and Q channels:

if((rc=iio_channel_attr_write_double(tx0_i,"scale",1))<0)
  stderrandexit(NULL,rc,__LINE__);

if((rc=iio_channel_attr_write_double(tx0_q,"scale",1))<0)
  stderrandexit(NULL,rc,__LINE__);

if((rc=iio_channel_attr_write_longlong(tx0_i,"frequency",FCW))<0)
  stderrandexit(NULL,rc,__LINE__);

if((rc=iio_channel_attr_write_longlong(tx0_q,"frequency",FCW))<0)
  stderrandexit(NULL,rc,__LINE__);

if((rc=iio_channel_attr_write_longlong(tx0_i,"phase",90000))<0)
  stderrandexit(NULL,rc,__LINE__);

if((rc=iio_channel_attr_write_longlong(tx0_q,"phase",0))<0)
  stderrandexit(NULL,rc,__LINE__);

Set the frequency of the TX Local Oscillator. The TX local oscillator frequency will be the value requested by the user minus the frequency of the CW tone.

if((rc=iio_channel_attr_write_longlong(tx_lo,"frequency",freq-FCW))<0)
  stderrandexit(NULL,rc,__LINE__);

Turn on the TX output activating channels I and Q raw:

int rc;

if((rc=iio_channel_attr_write_bool(
		tx0_i,
		"raw",
		1))<0)
 stderrandexit(NULL,rc,__LINE__);

if((rc=iio_channel_attr_write_bool(
		tx0_q,
		"raw",
		1))<0)
 stderrandexit(NULL,rc,__LINE__);

Entire source code of plutotx:

/*
 Author: Alberto Ferraris IU1KVL - https://www.albfer.com

 This program is free software: you can redistribute it and/or modify
 it under the terms of the version 3 GNU General Public License as
 published by the Free Software Foundation.
 
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 */
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "iio.h"

#define URIPLUTO "ip:192.168.2.1"
#define MINFREQ 50000000
#define MAXFREQ 6000000000
#define MINDBM -89
#define MAXDBM 10
#define REFTXPWR 10
#define FBANDWIDTH 4000000
#define FSAMPLING 4000000
#define FCW 1000000

struct iio_channel *tx0_i, *tx0_q;

void stderrandexit(const char *msg, int errcode, int line)
{
if(errcode<0)
  fprintf(stderr, "Error:%d, program terminated (line:%d)\n", errcode, line);
  else
  fprintf(stderr, "%s, program terminated (line:%d)\n",msg, line);
exit(-1);
}

void CWOnOff(int onoff)
{
int rc;

if((rc=iio_channel_attr_write_bool(
		tx0_i,
		"raw",
		onoff))<0)
 stderrandexit(NULL,rc,__LINE__);

if((rc=iio_channel_attr_write_bool(
		tx0_q,
		"raw",
		onoff))<0)
 stderrandexit(NULL,rc,__LINE__);
}

int main(int argc, char* argv[])
{
struct iio_context *ctx;
struct iio_device *phy;
struct iio_device *dds_core_lpc;
struct iio_channel *tx_chain;
struct iio_channel *tx_lo;
const char *value;
long long freq;
double dBm;
int rc;
int ch;

if(argc<3)
  {
  printf("Usage: plutotx kHz dBm [uri]\n");
  return  0;
  }

freq=atol(argv[1])*1000;

if(freq<MINFREQ || freq>MAXFREQ)
  stderrandexit("Frequency is not in range",0,__LINE__);

dBm=atof(argv[2]);

if(dBm<MINDBM || dBm>MAXDBM)
  stderrandexit("dBm is not in range",0,__LINE__);

if(argc>3)
  ctx = iio_create_context_from_uri(argv[3]);
  else
  ctx = iio_create_context_from_uri(URIPLUTO);

if(ctx==NULL)
  stderrandexit("Connection failed",0,__LINE__);

if((value=iio_context_get_attr_value(ctx, "ad9361-phy,model"))!=NULL)
  {
  if(strcmp(value,"ad9364"))
    stderrandexit("Pluto is not expanded",0,__LINE__);
  }else
   stderrandexit("Error retrieving phy model",0,__LINE__);

phy = iio_context_find_device(ctx, "ad9361-phy");
dds_core_lpc = iio_context_find_device(ctx, "cf-ad9361-dds-core-lpc");  
tx0_i = iio_device_find_channel(dds_core_lpc, "altvoltage0", true);
tx0_q = iio_device_find_channel(dds_core_lpc, "altvoltage2", true);
tx_chain=iio_device_find_channel(phy, "voltage0", true);
tx_lo=iio_device_find_channel(phy, "altvoltage1", true);

if(!phy || !dds_core_lpc || !tx0_i || !tx0_q || !tx_chain || !tx_lo)
  stderrandexit("Error finding device or channel",0,__LINE__);

//enable internal TX local oscillator
if((rc=iio_channel_attr_write_bool(tx_lo,"external",false))<0)
  stderrandexit(NULL,rc,__LINE__);

//disable fastlock feature of TX local oscillator
if((rc=iio_channel_attr_write_bool(tx_lo,"fastlock_store",false))<0)
  stderrandexit(NULL,rc,__LINE__);

//power on TX local oscillator
if((rc=iio_channel_attr_write_bool(tx_lo,"powerdown",false))<0)
  stderrandexit(NULL,rc,__LINE__);

//full duplex mode
if((rc=iio_device_attr_write(phy,"ensm_mode","fdd"))<0)
  stderrandexit(NULL,rc,__LINE__);

//calibration mode to manual
if((rc=iio_device_attr_write(phy,"calib_mode","manual"))<0)
  stderrandexit(NULL,rc,__LINE__);

CWOnOff(0);  

if((rc=iio_channel_attr_write_double(tx_chain,"hardwaregain",dBm-REFTXPWR))<0)
  stderrandexit(NULL,rc,__LINE__);

if((rc=iio_channel_attr_write_longlong(tx_chain,"rf_bandwidth",FBANDWIDTH))<0)
  stderrandexit(NULL,rc,__LINE__);

if((rc=iio_channel_attr_write_longlong(tx_chain,"sampling_frequency",FSAMPLING))<0)
  stderrandexit(NULL,rc,__LINE__);

if((rc=iio_channel_attr_write_double(tx0_i,"scale",1))<0)
  stderrandexit(NULL,rc,__LINE__);

if((rc=iio_channel_attr_write_double(tx0_q,"scale",1))<0)
  stderrandexit(NULL,rc,__LINE__);

if((rc=iio_channel_attr_write_longlong(tx0_i,"frequency",FCW))<0)
  stderrandexit(NULL,rc,__LINE__);

if((rc=iio_channel_attr_write_longlong(tx0_q,"frequency",FCW))<0)
  stderrandexit(NULL,rc,__LINE__);

if((rc=iio_channel_attr_write_longlong(tx0_i,"phase",90000))<0)
  stderrandexit(NULL,rc,__LINE__);

if((rc=iio_channel_attr_write_longlong(tx0_q,"phase",0))<0)
  stderrandexit(NULL,rc,__LINE__);

if((rc=iio_channel_attr_write_longlong(tx_lo,"frequency",freq-FCW))<0)
  stderrandexit(NULL,rc,__LINE__);

CWOnOff(1);

printf("TX ON! Q to exit or E to keep TX ON and exit\n");

while(1)
     {
     ch=getchar();
     if(ch=='q' || ch=='Q')
       {
       CWOnOff(0);
       break;
       }
     if(ch=='e' || ch=='E')
       break;
     };

iio_context_destroy(ctx);

return 0;
}

8 thoughts on “plutotx”

  1. Disclaimer: I work at Analog Devices – sometimes on the maintainable of libiio, and helped design the PlutoSDR.

    First – this looks great. Thanks for putting together.

    A few comments:
    – Line 87 : check `hardwaregain_available` on the device – it changes over frequency. It’s [min step max]:
    iio_attr -a usb -o -c ad9361-phy . hardwaregain_available
    Using auto-detected IIO context at URI “usb:3.6.5”
    dev ‘ad9361-phy’, channel ‘voltage0’ (output), attr ‘hardwaregain_available’, value ‘[-89.750000 0.250000 0.000000]’
    Also – don’t think this is dBm (dB with a milliwatt reference). It’s just dB (dB with respect to something unknown).

    – Line 100: check `frequency_available` attribute to make sure that the LO is within limits.
    iio_attr -a usb -c ad9361-phy *_LO *_available
    Using auto-detected IIO context at URI “usb:3.6.5”
    dev ‘ad9361-phy’, channel ‘altvoltage1’ (output), id ‘TX_LO’, attr ‘frequency_available’, value ‘[325000000 1 3800000000]’
    dev ‘ad9361-phy’, channel ‘altvoltage0’ (output), id ‘RX_LO’, attr ‘frequency_available’, value ‘[325000000 1 3800000000]’
    that way – you can remove MAXFREQ and MINFREQ

    – Line 131: never recommend to anyone to turn Tx cal off. This violates datasheet specs, and Tx performance is not guaranteed. Don’t do this. It is true – calibration will squirt out a few short tones during this process – entire your PA is off during this time.

    – line 144: If you want to use the DDS – that’s cool, but you need to set all 4 (not just 2). You don’t know what the other one is set to.
    iio_attr -a usb -o -c cf-ad9361-dds-core-lpc . frequency
    Using auto-detected IIO context at URI “usb:3.6.5”
    dev ‘cf-ad9361-dds-core-lpc’, channel ‘altvoltage3’ (output), id ‘TX1_Q_F2’, attr ‘frequency’, value ‘9279985’
    dev ‘cf-ad9361-dds-core-lpc’, channel ‘altvoltage1’ (output), id ‘TX1_I_F2’, attr ‘frequency’, value ‘9279985’
    dev ‘cf-ad9361-dds-core-lpc’, channel ‘altvoltage0’ (output), id ‘TX1_I_F1’, attr ‘frequency’, value ‘5999622’
    dev ‘cf-ad9361-dds-core-lpc’, channel ‘altvoltage2’ (output), id ‘TX1_Q_F1’, attr ‘frequency’, value ‘5999622’

    It might be easier to use the built in BIST Tone.
    dev ‘ad9361-phy’, debug attr ‘bist_tone’, value :’0′
    have a look at : https://wiki.analog.com/resources/tools-software/linux-drivers/iio-transceiver/ad9361#bist_tone for more info. (then you just pick a nominal sample rate – and then calculate LO to position it in the right place) – then you don’t need to worry about the DDS as all.

    Thanks again for the example.

    1. Dear Robin,

      I am truly honored for your comment!

      Thank you so much for your suggestions!

      And thank you, thank you, and thank you again for your work! With Pluto you have opened new worlds to us hams radio!

      Let me share some comments.

      I wrote this simple example following some requests by enthusiastic users of Pluto and in the base of experience derived from writing the SATSAGEN application in the last months.
      I think that the hardware gain of the output device is the TX attenuator value expressed by dB. The available values of TX hardware gain are the same on the whole range of frequencies, as I verified on the transceiver of Pluto, while the values possible of the hardware gain of the RX change in the scope of frequencies. I implemented the check of the available RX gain values on the code of SATSAGEN for rectifying the user inputs related to the frequency and gain requested.
      In my example, the output in dBm is reached by setting the TX attenuator with the value computed from the dBm requested by the user minus the constant defined by REFTXPWR that is ten dBm, about the maximum output power of Pluto.
      I wanted to write a plutotx example as simple as possible, so I didn’t check the available values (that are the same on the TX hardware gain as write before). I didn’t also implement a curve of linearization of TX, as we implemented in SATSAGEN. Anyway, finally, the output power is approximatively what the user request.
      The thing that worries me is that the bursts fired on the auto-calibration may be harmful to the input stage because they may reach the maximum output level (I have noticed the attenuator is bypassed), in the loopback scenarios, for example; moreover, in the spectrum analyzer with a tracking option scenario, the LNA stability can be affected during the scan.
      Unfortunately, I have noticed that some bursts are outputted anyway with this example of code, despite the manual-calibration setting. This can be mitigated adding this line before the sampling rate setting, as I have implemented in SATSAGEN:
      if((rc=iio_channel_attr_write_longlong(tx_lo,”frequency”,MINFREQ))<0) stderrandexit(NULL,rc,__LINE__);
      The power output linearity of Pluto has verified with SATSAGEN tool, the results can read in the document Evaluation of SDR Boards and Toolchains from the European Space Agency and Libre Space Foundation, chapter 2.2.7.5, page 115 at https://gitlab.com/librespacefoundation/sdrmakerspace/sdreval/-/raw/master/Report/pdf/Evaluation_of_SDR_Boards-1.1.pdf
      I can say that the use of DDS is relatively simple. It is true, I should have the foresight at least to check the switching off of the second tone as I did in SATSAGEN by inserting the following lines:
      struct iio_channel *tx1_i, *tx1_q;
      tx1_i = iio_device_find_channel(dds_core_lpc, “altvoltage1”, true);
      tx1_q = iio_device_find_channel(dds_core_lpc, “altvoltage3″, true);
      if((rc=iio_channel_attr_write_bool(tx1_i,”raw”,0))<0) stderrandexit(NULL,rc,__LINE__);
      if((rc=iio_channel_attr_write_bool(tx1_q,"raw",0))<0) stderrandexit(NULL,rc,__LINE__);
      if((rc=iio_channel_attr_write_double(tx1_i,"scale",0))<0) stderrandexit(NULL,rc,__LINE__);
      if((rc=iio_channel_attr_write_double(tx1_q,"scale",0))<0) stderrandexit(NULL,rc,__LINE__);

      I have not considered using the BIST tone, I thought this feature was developed for testing purposes only, moreover the frequency of tone is strictly related to sampling rate that may be a limitation or complication when used with active RX chain.

      Thank you again!

      Best Regards

      Alberto

      1. Thanks Alberto for this really useful application. I have used a lot of your code in the Signal Generator that I have just added to the BATC Portsdown 4 Digital ATV System https://github.com/BritishAmateurTelevisionClub/portsdown4 . This is based on a Raspberry Pi 4 with a touchscreen, and can use a Pluto to transmit Digital ATV. The Portsdown 4 hardware and software is also compatible with Colin G4EML’s Langstone SSB/CW/FM transceiver which also uses the Pluto.

        In response to Robin’s comments, I have added the following lines:

        struct iio_channel *tx1_i, *tx1_q;

        tx1_i = iio_device_find_channel(dds_core_lpc, “altvoltage1”, true);
        tx1_q = iio_device_find_channel(dds_core_lpc, “altvoltage3”, true);

        // Disable the DDSs on Channel 1
        if((rc = iio_channel_attr_write_double(tx1_i, “scale”, 0.0)) < 0)
        {
        stderrandexit(NULL, rc, __LINE__);
        }
        if((rc = iio_channel_attr_write_double(tx1_q, "scale", 0.0)) < 0)
        {
        stderrandexit(NULL, rc, __LINE__);
        }

        I have also added the option to turn on the calibration, and only set the sampling frequency (which seems to be persistent between calls) when the calibration is on. I found that setting the sampling frequency caused a short high power output pulse whatever the state of the Pluto was.

        Lastly, I have added a look-up table for the frequency response of the Pluto, which is applied to the displayed output figure. The displayed output is now within about 1 dB of the measured output (using an HP432A) over the whole frequency range.

        Dave, G8GKQ

  2. Hi Alberto
    This is a really useful application and a good explanation. One point is that the executable won’t transmit above 2147483 kHz which I think is a 32 bit integer overflow at line 80 in your code. What is the easiest toolchain for me to rebuild from source on Windows?

    Best Regards

    Pete Morys G4HQX

    1. Hi Pete!
      I’m sorry, you are right, the correct line is:
      freq=(long long)atol(argv[1])*1000;
      I published version 1.1 with that correction and others.
      You can compile plutotx with a free C++ compiler like this.
      Thank you
      Regards
      Alberto

  3. Good day Alberto
    Thank you for you tool.

    I cant see sinus, like modulation exist:
    plutotx 80000 0

    why ?

    Thank you.

    1. Hi!
      Thank you for your comment!
      Yes, the carrier is modulated by 1 MHz of IF.
      The perfect sinus can’t be view due to the output transformer distortion (anyway at this low frequency of 80 MHz, the output of transceiver could be a square wave!)
      I suggest you to try again the test with last version of SATSAGEN with the DC Generator feature, where the carrier is not modulated.
      Cheers,
      Alberto

Leave a Reply

Your email address will not be published. Required fields are marked *