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}