Skip to main content

pamoja_sensors/ads1115/
driver.rs

1//! The ADS1115 driven over I2C: configure, start a single-shot conversion, wait, read.
2
3use embedded_hal::delay::DelayNs;
4use embedded_hal::i2c::I2c;
5use pamoja_core::Sensor;
6
7use super::{conversion_micros, register, Config, DataRate, Mode, Mux, Pga, Sample};
8use crate::driver::{read_word, write_word};
9use crate::error::{DriverError, SensorError};
10
11/// How many times the Config register is polled, 1 ms apart, after the conversion
12/// time has elapsed before the conversion is called overdue.
13pub const CONVERSION_POLLS: u8 = 10;
14
15/// An ADS1115 on an I2C bus, converting one input on demand.
16///
17/// The part is left in its power-down single-shot state between conversions, which
18/// is its reset state. [`init`](Ads1115::init) writes the input, gain, and data
19/// rate to the Config register and reads it back, which is the only identity check
20/// a part without an id register offers. [`sample`](Ads1115::sample) sets the
21/// operational-status bit to start a conversion, waits one period of the data rate
22/// plus the datasheet's ten percent rate variation, polls the status bit until the
23/// part reports idle, and reads the Conversion register. The reading carries the
24/// gain it was taken at, so [`Sample::volts`] needs nothing else. As a [`Sensor`]
25/// its reading is the [`Sample`]; [`Sensor::map`] with a
26/// [`Calibration`](https://docs.rs/pamoja-kit/latest/pamoja_kit/struct.Calibration.html)
27/// turns it into the physical quantity an analog probe measures.
28///
29/// # Examples
30///
31/// A soil moisture probe's analog output, read through an ADS1115 that is not plugged in
32/// yet: the simulated converter sees 1.2 V on AIN0 in its default ±2.048 V range, and the
33/// driver hands the sample back in volts.
34///
35/// ```
36/// use pamoja_core::Sensor;
37/// use pamoja_hal::script::{block_on, DelayLog};
38/// use pamoja_sensors::ads1115::{address, sim, Ads1115, Mux, Pga};
39///
40/// let part = sim::reporting(address::GND, Pga::Fsr2_048, 1.2);
41/// let adc = Ads1115::new(part, address::GND, DelayLog::new()).with_input(Mux::Ain0Gnd);
42/// let mut volts = adc.map(|sample| sample.volts());
43/// let reading = block_on(volts.read())?;
44/// assert!((reading - 1.2).abs() < 0.001);
45/// # Ok::<(), pamoja_core::Error>(())
46/// ```
47#[derive(Debug)]
48pub struct Ads1115<I2C, D> {
49    bus: I2C,
50    delay: D,
51    address: u8,
52    config: Config,
53    initialized: bool,
54}
55
56impl<I2C, D> Ads1115<I2C, D> {
57    /// Wraps an I2C bus and the part's address.
58    ///
59    /// # Arguments
60    ///
61    /// * `bus` - the I2C bus the part is on.
62    /// * `address` - the address the ADDR pin selects, from [`address`](super::address).
63    /// * `delay` - the timer that paces the conversions.
64    ///
65    /// # Returns
66    ///
67    /// The driver, converting AIN0 against AIN1 at the ±2.048 V range and 128
68    /// samples per second, the part's reset settings.
69    pub fn new(bus: I2C, address: u8, delay: D) -> Self {
70        Ads1115 {
71            bus,
72            delay,
73            address,
74            config: Config {
75                start_conversion: false,
76                mode: Mode::SingleShot,
77                ..Config::default()
78            },
79            initialized: false,
80        }
81    }
82
83    /// Selects the input the part converts.
84    ///
85    /// # Arguments
86    ///
87    /// * `mux` - the differential pair or the single-ended input.
88    ///
89    /// # Returns
90    ///
91    /// The driver.
92    pub fn with_input(mut self, mux: Mux) -> Self {
93        self.config.mux = mux;
94        self.initialized = false;
95        self
96    }
97
98    /// Selects the full-scale range, and with it the size of one count.
99    ///
100    /// # Arguments
101    ///
102    /// * `pga` - the range; the input must stay within the supply regardless.
103    ///
104    /// # Returns
105    ///
106    /// The driver.
107    pub fn with_gain(mut self, pga: Pga) -> Self {
108        self.config.pga = pga;
109        self.initialized = false;
110        self
111    }
112
113    /// Selects the data rate, and with it the conversion time and the noise.
114    ///
115    /// # Arguments
116    ///
117    /// * `rate` - samples per second.
118    ///
119    /// # Returns
120    ///
121    /// The driver.
122    pub fn with_data_rate(mut self, rate: DataRate) -> Self {
123        self.config.data_rate = rate;
124        self.initialized = false;
125        self
126    }
127
128    /// Returns the configuration the driver writes to the part.
129    pub fn config(&self) -> Config {
130        self.config
131    }
132
133    /// Gives back the bus and the delay.
134    ///
135    /// # Returns
136    ///
137    /// The bus and delay the driver was built from.
138    pub fn release(self) -> (I2C, D) {
139        (self.bus, self.delay)
140    }
141}
142
143impl<I2C: I2c, D: DelayNs> Ads1115<I2C, D> {
144    /// Writes the input, gain, and data rate and reads the Config register back.
145    ///
146    /// The operational-status bit is left clear so no conversion starts. Reading the
147    /// register back is the identity check: a part that does not hold what was
148    /// written is not an ADS1115 in its power-down state.
149    ///
150    /// # Errors
151    ///
152    /// Returns [`DriverError::Bus`] if the bus fails, and [`DriverError::Sensor`]
153    /// with [`SensorError::Identity`] if the register reads back differently.
154    pub fn init(&mut self) -> Result<(), DriverError<I2C::Error>> {
155        let written = self.config.bits();
156        write_word(&mut self.bus, self.address, register::CONFIG, written)
157            .map_err(DriverError::Bus)?;
158        let read =
159            read_word(&mut self.bus, self.address, register::CONFIG).map_err(DriverError::Bus)?;
160        if Config::from_bits(read).bits() & 0x7FFF != written & 0x7FFF {
161            return Err(SensorError::Identity.into());
162        }
163        self.initialized = true;
164        Ok(())
165    }
166
167    /// Runs one conversion of the configured input and returns the sample.
168    ///
169    /// Initializes the part first if [`init`](Ads1115::init) has not run. The
170    /// sequence is the datasheet's single-shot mode: the Config register written
171    /// with the operational-status bit set, the conversion time for the data rate,
172    /// the status bit polled until the part is idle, then the Conversion register.
173    ///
174    /// # Returns
175    ///
176    /// The sample, carrying the gain it was taken at.
177    ///
178    /// # Errors
179    ///
180    /// Returns [`DriverError::Bus`] if the bus fails, [`DriverError::Timeout`] if
181    /// the part never reports idle, or any error of [`init`](Ads1115::init).
182    pub fn sample(&mut self) -> Result<Sample, DriverError<I2C::Error>> {
183        if !self.initialized {
184            self.init()?;
185        }
186        let start = Config {
187            start_conversion: true,
188            ..self.config
189        };
190        write_word(&mut self.bus, self.address, register::CONFIG, start.bits())
191            .map_err(DriverError::Bus)?;
192        self.delay
193            .delay_us(conversion_micros(self.config.data_rate));
194
195        let mut idle = false;
196        for _ in 0..CONVERSION_POLLS {
197            let status = read_word(&mut self.bus, self.address, register::CONFIG)
198                .map_err(DriverError::Bus)?;
199            if Config::from_bits(status).start_conversion {
200                idle = true;
201                break;
202            }
203            self.delay.delay_ms(1);
204        }
205        if !idle {
206            return Err(DriverError::Timeout);
207        }
208
209        let raw = read_word(&mut self.bus, self.address, register::CONVERSION)
210            .map_err(DriverError::Bus)?;
211        Ok(Sample {
212            raw: raw as i16,
213            pga: self.config.pga,
214        })
215    }
216
217    /// Converts one input once without changing the configured input.
218    ///
219    /// # Arguments
220    ///
221    /// * `mux` - the input to convert this once.
222    ///
223    /// # Returns
224    ///
225    /// The sample.
226    ///
227    /// # Errors
228    ///
229    /// The errors of [`sample`](Ads1115::sample).
230    pub fn sample_input(&mut self, mux: Mux) -> Result<Sample, DriverError<I2C::Error>> {
231        let configured = self.config.mux;
232        self.config.mux = mux;
233        self.initialized = false;
234        let result = self.sample();
235        self.config.mux = configured;
236        self.initialized = false;
237        result
238    }
239}
240
241impl<I2C, D> Sensor for Ads1115<I2C, D>
242where
243    I2C: I2c + Send,
244    I2C::Error: core::fmt::Debug,
245    D: DelayNs + Send,
246{
247    type Reading = Sample;
248
249    async fn read(&mut self) -> pamoja_core::Result<Sample> {
250        self.sample().map_err(pamoja_core::Error::from)
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    use crate::ads1115::address;
258    use alloc::vec;
259    use alloc::vec::Vec;
260    use embedded_hal::i2c::{ErrorKind, NoAcknowledgeSource};
261    use pamoja_hal::script::{block_on, DelayLog, I2cScript, I2cStep};
262
263    fn config(mux: Mux, pga: Pga, rate: DataRate, start: bool) -> [u8; 2] {
264        Config {
265            start_conversion: start,
266            mux,
267            pga,
268            mode: Mode::SingleShot,
269            data_rate: rate,
270            ..Config::default()
271        }
272        .bits()
273        .to_be_bytes()
274    }
275
276    fn init_steps(address: u8, settings: [u8; 2]) -> Vec<I2cStep> {
277        vec![
278            I2cStep::write(address, [register::CONFIG, settings[0], settings[1]]),
279            I2cStep::write_read(address, [register::CONFIG], settings),
280        ]
281    }
282
283    fn sample_steps(address: u8, started: [u8; 2], raw: i16) -> Vec<I2cStep> {
284        vec![
285            I2cStep::write(address, [register::CONFIG, started[0], started[1]]),
286            I2cStep::write_read(address, [register::CONFIG], started),
287            I2cStep::write_read(address, [register::CONVERSION], raw.to_be_bytes()),
288        ]
289    }
290
291    #[test]
292    fn a_single_shot_conversion_follows_the_datasheet_sequence() {
293        let (mux, pga, rate) = (Mux::Ain0Gnd, Pga::Fsr4_096, DataRate::Sps860);
294        let mut steps = init_steps(address::SDA, config(mux, pga, rate, false));
295        steps.extend(sample_steps(
296            address::SDA,
297            config(mux, pga, rate, true),
298            -1024,
299        ));
300        let mut adc = Ads1115::new(I2cScript::new(steps), address::SDA, DelayLog::new())
301            .with_input(mux)
302            .with_gain(pga)
303            .with_data_rate(rate);
304        let sample = adc.sample().unwrap();
305        assert_eq!(sample.raw, -1024);
306        assert_eq!(sample.pga, pga);
307        assert!((sample.volts() + 0.128).abs() < 1e-6);
308        let (bus, delay) = adc.release();
309        assert!(bus.done());
310        assert_eq!(delay.waits_ns(), [1_280_000], "860 SPS plus ten percent");
311    }
312
313    #[test]
314    fn the_readback_identity_check_refuses_a_part_that_holds_something_else() {
315        let settings = config(Mux::Ain0Ain1, Pga::Fsr2_048, DataRate::Sps128, false);
316        let steps = [
317            I2cStep::write(address::GND, [register::CONFIG, settings[0], settings[1]]),
318            I2cStep::write_read(address::GND, [register::CONFIG], [0x00, 0x00]),
319        ];
320        let mut adc = Ads1115::new(I2cScript::new(steps), address::GND, DelayLog::new());
321        assert_eq!(adc.init(), Err(DriverError::Sensor(SensorError::Identity)));
322    }
323
324    #[test]
325    fn a_conversion_that_never_finishes_is_a_timeout() {
326        let settings = config(Mux::Ain0Ain1, Pga::Fsr2_048, DataRate::Sps128, false);
327        let started = config(Mux::Ain0Ain1, Pga::Fsr2_048, DataRate::Sps128, true);
328        let mut steps = init_steps(address::GND, settings);
329        steps.push(I2cStep::write(
330            address::GND,
331            [register::CONFIG, started[0], started[1]],
332        ));
333        for _ in 0..CONVERSION_POLLS {
334            steps.push(I2cStep::write_read(
335                address::GND,
336                [register::CONFIG],
337                settings,
338            ));
339        }
340        let mut adc = Ads1115::new(I2cScript::new(steps), address::GND, DelayLog::new());
341        assert_eq!(adc.sample(), Err(DriverError::Timeout));
342        let (bus, delay) = adc.release();
343        assert!(bus.done());
344        assert_eq!(delay.total_micros(), 8_595 + 10_000);
345    }
346
347    #[test]
348    fn a_bus_fault_surfaces_as_a_bus_error() {
349        let kind = ErrorKind::NoAcknowledge(NoAcknowledgeSource::Address);
350        let mut adc = Ads1115::new(
351            I2cScript::new([I2cStep::fault(address::GND, kind)]),
352            address::GND,
353            DelayLog::new(),
354        );
355        assert!(matches!(adc.init(), Err(DriverError::Bus(_))));
356    }
357
358    #[test]
359    fn sampling_another_input_once_leaves_the_configured_one_in_place() {
360        let (pga, rate) = (Pga::Fsr2_048, DataRate::Sps128);
361        let mut steps = init_steps(address::GND, config(Mux::Ain1Gnd, pga, rate, false));
362        steps.extend(sample_steps(
363            address::GND,
364            config(Mux::Ain1Gnd, pga, rate, true),
365            100,
366        ));
367        steps.extend(init_steps(
368            address::GND,
369            config(Mux::Ain0Gnd, pga, rate, false),
370        ));
371        steps.extend(sample_steps(
372            address::GND,
373            config(Mux::Ain0Gnd, pga, rate, true),
374            3200,
375        ));
376        let mut adc = Ads1115::new(I2cScript::new(steps), address::GND, DelayLog::new())
377            .with_input(Mux::Ain0Gnd);
378        assert_eq!(adc.sample_input(Mux::Ain1Gnd).unwrap().raw, 100);
379        assert_eq!(adc.config().mux, Mux::Ain0Gnd);
380        assert_eq!(adc.sample().unwrap().raw, 3200);
381        assert!(adc.release().0.done());
382    }
383
384    #[test]
385    fn as_a_sensor_it_reads_and_maps_to_volts() {
386        let (mux, pga, rate) = (Mux::Ain0Gnd, Pga::Fsr2_048, DataRate::Sps128);
387        let mut steps = init_steps(address::GND, config(mux, pga, rate, false));
388        steps.extend(sample_steps(
389            address::GND,
390            config(mux, pga, rate, true),
391            3200,
392        ));
393        let adc =
394            Ads1115::new(I2cScript::new(steps), address::GND, DelayLog::new()).with_input(mux);
395        let mut volts = adc.map(|sample| sample.volts());
396        let reading = block_on(volts.read()).unwrap();
397        assert!((reading - 0.2).abs() < 1e-6);
398    }
399
400    #[test]
401    fn a_single_shot_conversion_makes_the_transfers_the_datasheet_gives() {
402        const PART: u8 = crate::ads1115::address::GND;
403        let bus = I2cScript::new([
404            I2cStep::write(PART, [0x01, 0x45, 0x83]),
405            I2cStep::write_read(PART, [0x01], [0x45, 0x83]),
406            I2cStep::write(PART, [0x01, 0xC5, 0x83]),
407            I2cStep::write_read(PART, [0x01], [0xC5, 0x83]),
408            I2cStep::write_read(PART, [0x00], [0x0C, 0x80]),
409        ]);
410
411        let adc = Ads1115::new(bus, PART, DelayLog::new()).with_input(crate::ads1115::Mux::Ain0Gnd);
412        let mut volts = adc.map(|sample| sample.volts());
413        let reading = block_on(Sensor::read(&mut volts)).expect("the scripted part answers");
414        assert!((reading - 0.2).abs() < 1e-6);
415    }
416}