pluto_sdr/
pluto.rs

1// Author: Roman Hayn
2// MIT License 2023
3
4use iio::{Buffer, Channel, Context, Device};
5use industrial_io as iio;
6
7// Small abstraction for simpler and faster coding down the line
8// -> The PlutoSDR uses an ad936x internally
9// set ad9361-phy -> altvoltage0 to RX_LO freq.
10// set ad9361-phy -> voltage0 to RX_bb / sampling
11// cf-ad9361-lpc -> voltage0 is RX0_I
12// cf-ad9361-lpc -> voltage1 is RX0_Q
13// the device cf-ad9361-lpc supports buffers
14
15// device name definitions
16// PHY controls all settings of the pluto
17const DEV_PHY: &str = "ad9361-phy";
18const DEV_LPC_RX: &str = "cf-ad9361-lpc";
19
20// TX uses a DDS Signal Generator
21const DEV_LPC_TX: &str = "cf-ad9361-dds-core-lpc";
22
23// measure internal voltages / temperature etc
24const _DEV_XADC: &str = "xadc";
25
26// channel name definitions
27// for rx and tx, I and Q samples
28const CH_XX_I: &str = "voltage0";
29const CH_XX_Q: &str = "voltage1";
30
31/// Shorthand for functions
32/// Input  -> RX
33/// Output -> TX
34///
35/// `my_pluto.set_rf_bandwidth(123456, RX);`
36pub const RX: bool = false;
37pub const TX: bool = true;
38
39/// This represents you Pluto device. This library provides a lot of different functions to send
40/// and receive data, to change the Pluto's settings and more!
41///
42/// A small example that receives samples with minimal effort
43/// ```
44/// use pluto_sdr::pluto::{Pluto, RX};
45/// let my_pluto = Pluto::connect("ip:192.168.2.2").unwrap();
46/// //setup rf settings of receiver
47/// let _ = my_pluto.set_sampling_freq( 5_000_000 ); // 5 Mhz
48/// let _ = my_pluto.set_lo_rx( 2_400_000_000 ); // 2.4 GHz
49/// let _ = my_pluto.set_rf_bandwidth( 2_500_000 , RX); // 2.5 MHz
50/// let _ = my_pluto.set_hwgain(10.0, RX);
51/// // get the receive channels
52/// let (rx_i, rx_q) = my_pluto.rx_ch0();
53/// // before creating a buffer and receiving samples,
54/// // enable the channels you need
55/// rx_i.enable();
56/// let mut buf = my_pluto.create_buffer_rx(32).unwrap();
57/// for _ in 0..100 {
58///     buf.refill().unwrap();
59///     let rx_i_samples = rx_i.read::<i16>(&buf).unwrap();
60///     // print or analyze chuncks of samples here:
61///     println!("{:?}", rx_i_samples);
62/// }
63/// ```
64#[derive(Debug)]
65pub struct Pluto {
66    /// Reference to the Pluto IIO Context
67    pub context: Context,
68    /// Pluto's PHY: It controls most settings
69    pub phy: Device,
70    /// Pluto's 12-bit ADC
71    pub rxadc: Device,
72    /// Pluto's DAC: A 12-bit [DDS](https://en.wikipedia.org/wiki/Direct_digital_synthesis) Signal Generator
73    pub txdac: Device,
74}
75
76impl Pluto {
77    /// Connect to your ADALM-Pluto SDR using an IP address or USB / serial port.
78    /// Resolving the hostname `pluto.local` can be quite slow (multiple seconds).
79    /// Timeouts are even slower!
80    /// ```
81    /// use pluto_sdr::pluto::Pluto;
82    ///
83    /// let my_pluto = Pluto::connect("ip:pluto3.local");
84    /// let my_pluto = Pluto::connect("ip:192.168.2.2");
85    /// let my_pluto = Pluto::connect("usb:1.7.5");
86    /// println!("{:?}", my_pluto);
87    /// ```
88    pub fn connect(uri: &str) -> Option<Self> {
89        let context = match iio::Context::from_uri(uri) {
90            Ok(ctx) => ctx,
91            Err(_) => {
92                return None;
93            }
94        };
95
96        // as long as we have a context (and the context is from an ADALM-Pluto)
97        // getting the PHY, dac and adc should not fail.
98        let phy = context.find_device(DEV_PHY).unwrap();
99        let rxadc = context.find_device(DEV_LPC_RX).unwrap();
100        let txdac = context.find_device(DEV_LPC_TX).unwrap();
101
102        println!("[PLUTO-SDR] Initialized:  {}", uri);
103
104        Some(Pluto {
105            context,
106            phy,
107            rxadc,
108            txdac,
109        })
110    }
111
112    /// Set the RF Local Oscillator frequency for the receiver.
113    ///
114    /// Mapping: `Pluto -> Device: phy/ad9361-phy -> Channel: altvoltage0 (RX_LO) -> Attribute: frequency (i64)`
115    pub fn set_lo_rx(&self, f_lo: i64) -> Result<(), ()> {
116        let res = self.phy.find_channel("altvoltage0", true);
117        match res {
118            Some(e) => {
119                e.attr_write_int("frequency", f_lo).unwrap();
120                Ok(())
121            }
122            None => Err(()),
123        }
124    }
125
126    /// Set the RF Local Oscillator frequency for Transmission.
127    ///
128    /// Mapping: `Pluto -> Device: phy/ad9361-phy -> Channel: altvoltage1 (TX_LO) -> Attribute: frequency (i64)`
129    pub fn set_lo_tx(&self, f_lo: i64) -> Result<(), ()> {
130        let res = self.phy.find_channel("altvoltage1", true);
131        match res {
132            Some(e) => {
133                e.attr_write_int("frequency", f_lo).unwrap();
134                Ok(())
135            }
136            None => Err(()),
137        }
138    }
139
140    /// Set the RF bandwidth for RX or TX (specified by argument `rxtx`).
141    ///
142    /// Mapping: `Pluto -> Device: phy/ad9361-phy -> Channel: voltage0 -> Attribute: rf_bandwidth (i64)`
143    pub fn set_rf_bandwidth(&self, f_b: i64, rxtx: bool) -> Result<(), ()> {
144        let res = self.phy.find_channel("voltage0", rxtx);
145        match res {
146            Some(chan) => {
147                chan.attr_write_int("rf_bandwidth", f_b).unwrap();
148                Ok(())
149            }
150            None => Err(()),
151        }
152    }
153
154    /// Set the RX/TX hardware gain (RX or TX specified by argument `rxtx`).
155    /// When setting gain for RX, it defaults to "manual" gain mode (manual/autogain).
156    ///
157    /// Mapping: `Pluto -> Device: phy/ad9361-phy -> Channel: voltage0 -> Attribute: hardwaregain (i64)`
158    pub fn set_hwgain(&self, g: f64, rxtx: bool) -> Result<(), ()> {
159        let res = self.phy.find_channel("voltage0", rxtx);
160        match res {
161            Some(chan) => {
162                // only RX supports auto gain modes
163                // set manual mode just in case
164                if !rxtx {
165                    chan.attr_write_str("gain_control_mode", "manual").unwrap();
166                }
167                chan.attr_write_float("hardwaregain", g).unwrap();
168                Ok(())
169            }
170            None => Err(()),
171        }
172    }
173
174    /// Sets the sample rate of both RX and TX.
175    ///
176    /// Mapping: `Pluto -> Device: phy/ad9361-phy -> Channel: voltage0 -> Attribute: sampling_frequency (i64)`
177    pub fn set_sampling_freq(&self, fs: i64) -> Result<(), ()> {
178        let res = self.phy.find_channel("voltage0", false);
179        match res {
180            Some(chan) => {
181                chan.attr_write_int("sampling_frequency", fs).unwrap();
182                Ok(())
183            }
184            None => Err(()),
185        }
186    }
187
188    /// Get the RX channel 0 data stream.
189    /// ```
190    /// use pluto_sdr::pluto::Pluto;
191    /// let my_pluto = Pluto::connect("ip:192.168.2.2").unwrap();
192    /// let (rx_i, rx_q) = my_pluto.rx_ch0();
193    /// // before creating a buffer and receiving samples,
194    /// // enable the channels you need
195    /// rx_i.enable();
196    /// let mut buf = my_pluto.create_buffer_rx(32).unwrap();
197    /// for _ in 0..100 {
198    ///     buf.refill().unwrap();
199    ///     let rx_i_samples = rx_i.read::<i16>(&buf).unwrap();
200    ///     println!("{:?}", rx_i_samples);
201    /// }
202    /// ```
203    /// Mapping: `Pluto -> Device: rxadc/cf-ad9361-lpc -> Channel: voltage0/1`
204    pub fn rx_ch0(&self) -> (Channel, Channel) {
205        (self.rx_i_ch0(), self.rx_q_ch0())
206    }
207
208    /// Get the TX channel 0 data sink.
209    /// Similar to [rx_ch0()][crate::pluto::Pluto::rx_ch0()],
210    /// but we can write samples to the Channel.
211    /// ```
212    /// use pluto_sdr::pluto::Pluto;
213    /// let my_pluto = Pluto::connect("ip:192.168.2.2").unwrap();
214    /// let (tx_i, tx_q) = my_pluto.tx_ch0();
215    ///
216    /// // the signal we want to send
217    /// let signal = vec![1,0,-1,0];
218    /// // before creating a buffer and transmitting samples,
219    /// // enable the channels you need
220    /// tx_i.enable();
221    /// let mut buf = my_pluto.create_buffer_tx(signal.len(), true).unwrap();
222    /// let _ = tx_i.write::<i16>(&buf, signal.as_slice()).unwrap();
223    /// buf.push().unwrap();
224    /// ```
225    /// Mapping: `Pluto -> Device: txdac/cf-ad9361-dds-core-lpc -> Channel: voltage0/1`
226    pub fn tx_ch0(&self) -> (Channel, Channel) {
227        (self.tx_i_ch0(), self.tx_q_ch0())
228    }
229
230    pub fn rx_i_ch0(&self) -> Channel {
231        self.rxadc.find_channel(CH_XX_I, false).unwrap()
232    }
233
234    pub fn rx_q_ch0(&self) -> Channel {
235        self.rxadc.find_channel(CH_XX_Q, false).unwrap()
236    }
237
238    pub fn tx_i_ch0(&self) -> Channel {
239        self.txdac.find_channel(CH_XX_I, true).unwrap()
240    }
241
242    pub fn tx_q_ch0(&self) -> Channel {
243        self.txdac.find_channel(CH_XX_Q, true).unwrap()
244    }
245
246    pub fn create_buffer_rx(&self, size: usize) -> iio::Result<Buffer> {
247        // can not receive with a cyclic buffer
248        self.rxadc.create_buffer(size, false)
249    }
250
251    pub fn create_buffer_tx(&self, size: usize, cyclic: bool) -> iio::Result<Buffer> {
252        self.txdac.create_buffer(size, cyclic)
253    }
254
255    // this does not just simply toggle the output
256    pub fn toggle_dds(&self, enable: bool) -> () {
257        // sets all output channels on/off
258        for chan in self.txdac.channels() {
259            match chan.id() {
260                Some(name) => {
261                    if name.contains("altvoltage") {
262                        if enable {
263                            chan.attr_write_int("raw", 1).unwrap();
264                        } else {
265                            chan.attr_write_int("raw", 0).unwrap();
266                        }
267                    }
268                }
269                None => {}
270            }
271        }
272    }
273
274    pub fn list_dds(&self) -> () {
275        for chan in self.txdac.channels() {
276            match chan.id() {
277                Some(name) => {
278                    if name.contains("altvoltage") {
279                        println!("-> {}:\n    {:?}", name, chan.attr_read_all().unwrap());
280                    }
281                }
282                None => {}
283            }
284        }
285    }
286}