pluto-sdr 0.1.1

HAL for ADALM-Pluto SDR
Documentation
// Author: Roman Hayn
// MIT License 2023

use iio::{Buffer, Channel, Context, Device};
use industrial_io as iio;

// Small abstraction for simpler and faster coding down the line
// -> The PlutoSDR uses an ad936x internally
// set ad9361-phy -> altvoltage0 to RX_LO freq.
// set ad9361-phy -> voltage0 to RX_bb / sampling
// cf-ad9361-lpc -> voltage0 is RX0_I
// cf-ad9361-lpc -> voltage1 is RX0_Q
// the device cf-ad9361-lpc supports buffers

// device name definitions
// PHY controls all settings of the pluto
const DEV_PHY: &str = "ad9361-phy";
const DEV_LPC_RX: &str = "cf-ad9361-lpc";

// TX uses a DDS Signal Generator
const DEV_LPC_TX: &str = "cf-ad9361-dds-core-lpc";

// measure internal voltages / temperature etc
const _DEV_XADC: &str = "xadc";

// channel name definitions
// for rx and tx, I and Q samples
const CH_XX_I: &str = "voltage0";
const CH_XX_Q: &str = "voltage1";

/// Shorthand for functions
/// Input  -> RX
/// Output -> TX
///
/// `my_pluto.set_rf_bandwidth(123456, RX);`
pub const RX: bool = false;
pub const TX: bool = true;

/// This represents you Pluto device. This library provides a lot of different functions to send
/// and receive data, to change the Pluto's settings and more!
///
/// A small example that receives samples with minimal effort
/// ```
/// use pluto_sdr::pluto::{Pluto, RX};
/// let my_pluto = Pluto::connect("ip:192.168.2.2").unwrap();
/// //setup rf settings of receiver
/// let _ = my_pluto.set_sampling_freq( 5_000_000 ); // 5 Mhz
/// let _ = my_pluto.set_lo_rx( 2_400_000_000 ); // 2.4 GHz
/// let _ = my_pluto.set_rf_bandwidth( 2_500_000 , RX); // 2.5 MHz
/// let _ = my_pluto.set_hwgain(10.0, RX);
/// // get the receive channels
/// let (rx_i, rx_q) = my_pluto.rx_ch0();
/// // before creating a buffer and receiving samples,
/// // enable the channels you need
/// rx_i.enable();
/// let mut buf = my_pluto.create_buffer_rx(32).unwrap();
/// for _ in 0..100 {
///     buf.refill().unwrap();
///     let rx_i_samples = rx_i.read::<i16>(&buf).unwrap();
///     // print or analyze chuncks of samples here:
///     println!("{:?}", rx_i_samples);
/// }
/// ```
#[derive(Debug)]
pub struct Pluto {
    /// Reference to the Pluto IIO Context
    pub context: Context,
    /// Pluto's PHY: It controls most settings
    pub phy: Device,
    /// Pluto's 12-bit ADC
    pub rxadc: Device,
    /// Pluto's DAC: A 12-bit [DDS](https://en.wikipedia.org/wiki/Direct_digital_synthesis) Signal Generator
    pub txdac: Device,
}

impl Pluto {
    /// Connect to your ADALM-Pluto SDR using an IP address or USB / serial port.
    /// Resolving the hostname `pluto.local` can be quite slow (multiple seconds).
    /// Timeouts are even slower!
    /// ```
    /// use pluto_sdr::pluto::Pluto;
    ///
    /// let my_pluto = Pluto::connect("ip:pluto3.local");
    /// let my_pluto = Pluto::connect("ip:192.168.2.2");
    /// let my_pluto = Pluto::connect("usb:1.7.5");
    /// println!("{:?}", my_pluto);
    /// ```
    pub fn connect(uri: &str) -> Option<Self> {
        let context = match iio::Context::from_uri(uri) {
            Ok(ctx) => ctx,
            Err(_) => {
                return None;
            }
        };

        // as long as we have a context (and the context is from an ADALM-Pluto)
        // getting the PHY, dac and adc should not fail.
        let phy = context.find_device(DEV_PHY).unwrap();
        let rxadc = context.find_device(DEV_LPC_RX).unwrap();
        let txdac = context.find_device(DEV_LPC_TX).unwrap();

        println!("[PLUTO-SDR] Initialized:  {}", uri);

        Some(Pluto {
            context,
            phy,
            rxadc,
            txdac,
        })
    }

    /// Set the RF Local Oscillator frequency for the receiver.
    ///
    /// Mapping: `Pluto -> Device: phy/ad9361-phy -> Channel: altvoltage0 (RX_LO) -> Attribute: frequency (i64)`
    pub fn set_lo_rx(&self, f_lo: i64) -> Result<(), ()> {
        let res = self.phy.find_channel("altvoltage0", true);
        match res {
            Some(e) => {
                e.attr_write_int("frequency", f_lo).unwrap();
                Ok(())
            }
            None => Err(()),
        }
    }

    /// Set the RF Local Oscillator frequency for Transmission.
    ///
    /// Mapping: `Pluto -> Device: phy/ad9361-phy -> Channel: altvoltage1 (TX_LO) -> Attribute: frequency (i64)`
    pub fn set_lo_tx(&self, f_lo: i64) -> Result<(), ()> {
        let res = self.phy.find_channel("altvoltage1", true);
        match res {
            Some(e) => {
                e.attr_write_int("frequency", f_lo).unwrap();
                Ok(())
            }
            None => Err(()),
        }
    }

    /// Set the RF bandwidth for RX or TX (specified by argument `rxtx`).
    ///
    /// Mapping: `Pluto -> Device: phy/ad9361-phy -> Channel: voltage0 -> Attribute: rf_bandwidth (i64)`
    pub fn set_rf_bandwidth(&self, f_b: i64, rxtx: bool) -> Result<(), ()> {
        let res = self.phy.find_channel("voltage0", rxtx);
        match res {
            Some(chan) => {
                chan.attr_write_int("rf_bandwidth", f_b).unwrap();
                Ok(())
            }
            None => Err(()),
        }
    }

    /// Set the RX/TX hardware gain (RX or TX specified by argument `rxtx`).
    /// When setting gain for RX, it defaults to "manual" gain mode (manual/autogain).
    ///
    /// Mapping: `Pluto -> Device: phy/ad9361-phy -> Channel: voltage0 -> Attribute: hardwaregain (i64)`
    pub fn set_hwgain(&self, g: f64, rxtx: bool) -> Result<(), ()> {
        let res = self.phy.find_channel("voltage0", rxtx);
        match res {
            Some(chan) => {
                // only RX supports auto gain modes
                // set manual mode just in case
                if !rxtx {
                    chan.attr_write_str("gain_control_mode", "manual").unwrap();
                }
                chan.attr_write_float("hardwaregain", g).unwrap();
                Ok(())
            }
            None => Err(()),
        }
    }

    /// Sets the sample rate of both RX and TX.
    ///
    /// Mapping: `Pluto -> Device: phy/ad9361-phy -> Channel: voltage0 -> Attribute: sampling_frequency (i64)`
    pub fn set_sampling_freq(&self, fs: i64) -> Result<(), ()> {
        let res = self.phy.find_channel("voltage0", false);
        match res {
            Some(chan) => {
                chan.attr_write_int("sampling_frequency", fs).unwrap();
                Ok(())
            }
            None => Err(()),
        }
    }

    /// Get the RX channel 0 data stream.
    /// ```
    /// use pluto_sdr::pluto::Pluto;
    /// let my_pluto = Pluto::connect("ip:192.168.2.2").unwrap();
    /// let (rx_i, rx_q) = my_pluto.rx_ch0();
    /// // before creating a buffer and receiving samples,
    /// // enable the channels you need
    /// rx_i.enable();
    /// let mut buf = my_pluto.create_buffer_rx(32).unwrap();
    /// for _ in 0..100 {
    ///     buf.refill().unwrap();
    ///     let rx_i_samples = rx_i.read::<i16>(&buf).unwrap();
    ///     println!("{:?}", rx_i_samples);
    /// }
    /// ```
    /// Mapping: `Pluto -> Device: rxadc/cf-ad9361-lpc -> Channel: voltage0/1`
    pub fn rx_ch0(&self) -> (Channel, Channel) {
        (self.rx_i_ch0(), self.rx_q_ch0())
    }

    /// Get the TX channel 0 data sink.
    /// Similar to [rx_ch0()][crate::pluto::Pluto::rx_ch0()],
    /// but we can write samples to the Channel.
    /// ```
    /// use pluto_sdr::pluto::Pluto;
    /// let my_pluto = Pluto::connect("ip:192.168.2.2").unwrap();
    /// let (tx_i, tx_q) = my_pluto.tx_ch0();
    ///
    /// // the signal we want to send
    /// let signal = vec![1,0,-1,0];
    /// // before creating a buffer and transmitting samples,
    /// // enable the channels you need
    /// tx_i.enable();
    /// let mut buf = my_pluto.create_buffer_tx(signal.len(), true).unwrap();
    /// let _ = tx_i.write::<i16>(&buf, signal.as_slice()).unwrap();
    /// buf.push().unwrap();
    /// ```
    /// Mapping: `Pluto -> Device: txdac/cf-ad9361-dds-core-lpc -> Channel: voltage0/1`
    pub fn tx_ch0(&self) -> (Channel, Channel) {
        (self.tx_i_ch0(), self.tx_q_ch0())
    }

    pub fn rx_i_ch0(&self) -> Channel {
        self.rxadc.find_channel(CH_XX_I, false).unwrap()
    }

    pub fn rx_q_ch0(&self) -> Channel {
        self.rxadc.find_channel(CH_XX_Q, false).unwrap()
    }

    pub fn tx_i_ch0(&self) -> Channel {
        self.txdac.find_channel(CH_XX_I, true).unwrap()
    }

    pub fn tx_q_ch0(&self) -> Channel {
        self.txdac.find_channel(CH_XX_Q, true).unwrap()
    }

    pub fn create_buffer_rx(&self, size: usize) -> iio::Result<Buffer> {
        // can not receive with a cyclic buffer
        self.rxadc.create_buffer(size, false)
    }

    pub fn create_buffer_tx(&self, size: usize, cyclic: bool) -> iio::Result<Buffer> {
        self.txdac.create_buffer(size, cyclic)
    }

    // this does not just simply toggle the output
    pub fn toggle_dds(&self, enable: bool) -> () {
        // sets all output channels on/off
        for chan in self.txdac.channels() {
            match chan.id() {
                Some(name) => {
                    if name.contains("altvoltage") {
                        if enable {
                            chan.attr_write_int("raw", 1).unwrap();
                        } else {
                            chan.attr_write_int("raw", 0).unwrap();
                        }
                    }
                }
                None => {}
            }
        }
    }

    pub fn list_dds(&self) -> () {
        for chan in self.txdac.channels() {
            match chan.id() {
                Some(name) => {
                    if name.contains("altvoltage") {
                        println!("-> {}:\n    {:?}", name, chan.attr_read_all().unwrap());
                    }
                }
                None => {}
            }
        }
    }
}