Skip to main content

pamoja_sensors/
ads1115.rs

1//! Texas Instruments ADS1115 16-bit I2C analog-to-digital converter.
2//!
3//! The ADS1115 turns an analog signal (a soil-moisture probe, a pH electrode, a
4//! divider) into a 16-bit reading, and a single Config register selects the input,
5//! the gain, the rate, and the comparator. This module builds and parses that Config
6//! register field by field and converts a raw conversion result into a voltage at the
7//! selected full-scale range, following the datasheet's register tables and per-gain
8//! LSB sizes.
9
10#[cfg(feature = "embedded-hal")]
11mod driver;
12#[cfg(feature = "embedded-hal")]
13pub mod sim;
14
15#[cfg(feature = "embedded-hal")]
16pub use driver::{Ads1115, CONVERSION_POLLS};
17
18/// The I2C addresses the ADDR pin selects, by what it is tied to.
19pub mod address {
20    /// ADDR tied to GND: `1001000b`.
21    pub const GND: u8 = 0x48;
22    /// ADDR tied to VDD: `1001001b`.
23    pub const VDD: u8 = 0x49;
24    /// ADDR tied to SDA: `1001010b`.
25    pub const SDA: u8 = 0x4A;
26    /// ADDR tied to SCL: `1001011b`.
27    pub const SCL: u8 = 0x4B;
28}
29
30/// The time from a stable supply until the part accepts a transfer, in microseconds.
31pub const POWER_UP_MICROS: u32 = 50;
32
33/// The ADS1115 register addresses, selected by the address pointer.
34pub mod register {
35    /// Conversion register: the last 16-bit result, two's complement.
36    pub const CONVERSION: u8 = 0x00;
37    /// Config register: input, gain, mode, data rate, and comparator settings.
38    pub const CONFIG: u8 = 0x01;
39    /// Low threshold register for the comparator.
40    pub const LO_THRESH: u8 = 0x02;
41    /// High threshold register for the comparator.
42    pub const HI_THRESH: u8 = 0x03;
43}
44
45/// The power-on value of the Config register (0x8583).
46pub const CONFIG_RESET: u16 = 0x8583;
47
48/// The input multiplexer setting (Config bits 14:12).
49///
50/// The ADS1115 measures either of two differential pairs against `AIN3`, the pair
51/// `AIN0`/`AIN1`, or any one input against ground.
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum Mux {
54    /// AINP = AIN0, AINN = AIN1 (the default).
55    Ain0Ain1,
56    /// AINP = AIN0, AINN = AIN3.
57    Ain0Ain3,
58    /// AINP = AIN1, AINN = AIN3.
59    Ain1Ain3,
60    /// AINP = AIN2, AINN = AIN3.
61    Ain2Ain3,
62    /// AINP = AIN0, AINN = GND (single-ended).
63    Ain0Gnd,
64    /// AINP = AIN1, AINN = GND (single-ended).
65    Ain1Gnd,
66    /// AINP = AIN2, AINN = GND (single-ended).
67    Ain2Gnd,
68    /// AINP = AIN3, AINN = GND (single-ended).
69    Ain3Gnd,
70}
71
72impl Mux {
73    /// Returns the 3-bit field code for this multiplexer setting.
74    pub fn code(self) -> u8 {
75        match self {
76            Mux::Ain0Ain1 => 0b000,
77            Mux::Ain0Ain3 => 0b001,
78            Mux::Ain1Ain3 => 0b010,
79            Mux::Ain2Ain3 => 0b011,
80            Mux::Ain0Gnd => 0b100,
81            Mux::Ain1Gnd => 0b101,
82            Mux::Ain2Gnd => 0b110,
83            Mux::Ain3Gnd => 0b111,
84        }
85    }
86
87    /// Builds a multiplexer setting from a 3-bit field code (the low three bits used).
88    pub fn from_code(code: u8) -> Mux {
89        match code & 0b111 {
90            0b000 => Mux::Ain0Ain1,
91            0b001 => Mux::Ain0Ain3,
92            0b010 => Mux::Ain1Ain3,
93            0b011 => Mux::Ain2Ain3,
94            0b100 => Mux::Ain0Gnd,
95            0b101 => Mux::Ain1Gnd,
96            0b110 => Mux::Ain2Gnd,
97            _ => Mux::Ain3Gnd,
98        }
99    }
100}
101
102/// The programmable gain amplifier's full-scale range (Config bits 11:9).
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub enum Pga {
105    /// Full-scale range ±6.144 V.
106    Fsr6_144,
107    /// Full-scale range ±4.096 V.
108    Fsr4_096,
109    /// Full-scale range ±2.048 V (the default).
110    Fsr2_048,
111    /// Full-scale range ±1.024 V.
112    Fsr1_024,
113    /// Full-scale range ±0.512 V.
114    Fsr0_512,
115    /// Full-scale range ±0.256 V.
116    Fsr0_256,
117}
118
119impl Pga {
120    /// Returns the 3-bit field code for this gain setting.
121    pub fn code(self) -> u8 {
122        match self {
123            Pga::Fsr6_144 => 0b000,
124            Pga::Fsr4_096 => 0b001,
125            Pga::Fsr2_048 => 0b010,
126            Pga::Fsr1_024 => 0b011,
127            Pga::Fsr0_512 => 0b100,
128            Pga::Fsr0_256 => 0b101,
129        }
130    }
131
132    /// Builds a gain setting from a 3-bit field code.
133    ///
134    /// Codes `110` and `111` are documented as also selecting ±0.256 V and map here
135    /// to [`Pga::Fsr0_256`].
136    pub fn from_code(code: u8) -> Pga {
137        match code & 0b111 {
138            0b000 => Pga::Fsr6_144,
139            0b001 => Pga::Fsr4_096,
140            0b010 => Pga::Fsr2_048,
141            0b011 => Pga::Fsr1_024,
142            0b100 => Pga::Fsr0_512,
143            _ => Pga::Fsr0_256,
144        }
145    }
146
147    /// Returns the positive full-scale input voltage for this gain, in microvolts.
148    pub fn full_scale_microvolts(self) -> u32 {
149        match self {
150            Pga::Fsr6_144 => 6_144_000,
151            Pga::Fsr4_096 => 4_096_000,
152            Pga::Fsr2_048 => 2_048_000,
153            Pga::Fsr1_024 => 1_024_000,
154            Pga::Fsr0_512 => 512_000,
155            Pga::Fsr0_256 => 256_000,
156        }
157    }
158}
159
160/// The conversion mode (Config bit 8).
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
162pub enum Mode {
163    /// Convert continuously.
164    Continuous,
165    /// Convert once per request, then power down (the default).
166    SingleShot,
167}
168
169/// The output data rate (Config bits 7:5), in samples per second.
170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
171pub enum DataRate {
172    /// 8 samples per second.
173    Sps8,
174    /// 16 samples per second.
175    Sps16,
176    /// 32 samples per second.
177    Sps32,
178    /// 64 samples per second.
179    Sps64,
180    /// 128 samples per second (the default).
181    Sps128,
182    /// 250 samples per second.
183    Sps250,
184    /// 475 samples per second.
185    Sps475,
186    /// 860 samples per second.
187    Sps860,
188}
189
190impl DataRate {
191    /// Returns the 3-bit field code for this data rate.
192    pub fn code(self) -> u8 {
193        match self {
194            DataRate::Sps8 => 0b000,
195            DataRate::Sps16 => 0b001,
196            DataRate::Sps32 => 0b010,
197            DataRate::Sps64 => 0b011,
198            DataRate::Sps128 => 0b100,
199            DataRate::Sps250 => 0b101,
200            DataRate::Sps475 => 0b110,
201            DataRate::Sps860 => 0b111,
202        }
203    }
204
205    /// Builds a data rate from a 3-bit field code.
206    pub fn from_code(code: u8) -> DataRate {
207        match code & 0b111 {
208            0b000 => DataRate::Sps8,
209            0b001 => DataRate::Sps16,
210            0b010 => DataRate::Sps32,
211            0b011 => DataRate::Sps64,
212            0b100 => DataRate::Sps128,
213            0b101 => DataRate::Sps250,
214            0b110 => DataRate::Sps475,
215            _ => DataRate::Sps860,
216        }
217    }
218
219    /// Returns the rate in samples per second.
220    pub fn samples_per_second(self) -> u16 {
221        match self {
222            DataRate::Sps8 => 8,
223            DataRate::Sps16 => 16,
224            DataRate::Sps32 => 32,
225            DataRate::Sps64 => 64,
226            DataRate::Sps128 => 128,
227            DataRate::Sps250 => 250,
228            DataRate::Sps475 => 475,
229            DataRate::Sps860 => 860,
230        }
231    }
232}
233
234/// The comparator mode (Config bit 4).
235#[derive(Clone, Copy, Debug, PartialEq, Eq)]
236pub enum ComparatorMode {
237    /// Traditional comparator with hysteresis (the default).
238    Traditional,
239    /// Window comparator.
240    Window,
241}
242
243/// The ALERT/RDY pin polarity (Config bit 3).
244#[derive(Clone, Copy, Debug, PartialEq, Eq)]
245pub enum ComparatorPolarity {
246    /// Active low (the default).
247    ActiveLow,
248    /// Active high.
249    ActiveHigh,
250}
251
252/// Whether the comparator latches (Config bit 2).
253#[derive(Clone, Copy, Debug, PartialEq, Eq)]
254pub enum ComparatorLatch {
255    /// Non-latching: the pin clears once readings return within the thresholds (the
256    /// default).
257    NonLatching,
258    /// Latching: the pin stays asserted until the conversion data is read.
259    Latching,
260}
261
262/// The comparator queue, or disable (Config bits 1:0).
263#[derive(Clone, Copy, Debug, PartialEq, Eq)]
264pub enum ComparatorQueue {
265    /// Assert after one conversion beyond a threshold.
266    AfterOne,
267    /// Assert after two conversions beyond a threshold.
268    AfterTwo,
269    /// Assert after four conversions beyond a threshold.
270    AfterFour,
271    /// Disable the comparator and set ALERT/RDY high-impedance (the default).
272    Disabled,
273}
274
275impl ComparatorQueue {
276    /// Returns the 2-bit field code for this queue setting.
277    pub fn code(self) -> u8 {
278        match self {
279            ComparatorQueue::AfterOne => 0b00,
280            ComparatorQueue::AfterTwo => 0b01,
281            ComparatorQueue::AfterFour => 0b10,
282            ComparatorQueue::Disabled => 0b11,
283        }
284    }
285
286    /// Builds a queue setting from a 2-bit field code.
287    pub fn from_code(code: u8) -> ComparatorQueue {
288        match code & 0b11 {
289            0b00 => ComparatorQueue::AfterOne,
290            0b01 => ComparatorQueue::AfterTwo,
291            0b10 => ComparatorQueue::AfterFour,
292            _ => ComparatorQueue::Disabled,
293        }
294    }
295}
296
297/// A decoded ADS1115 Config register.
298///
299/// Build one, set the fields, and turn it into the 16-bit register value with
300/// [`bits`](Config::bits); or parse a register read with [`from_bits`](Config::from_bits).
301/// [`Config::default`] is the power-on state, `0x8583`.
302///
303/// # Examples
304///
305/// ```
306/// use pamoja_sensors::ads1115::{Config, Mux, Pga};
307///
308/// // Read AIN0 against ground at the ±4.096 V range, leaving everything else default.
309/// let config = Config {
310///     mux: Mux::Ain0Gnd,
311///     pga: Pga::Fsr4_096,
312///     ..Config::default()
313/// };
314/// // The high byte then low byte are written to the Config register.
315/// let [hi, lo] = config.bits().to_be_bytes();
316/// assert_eq!(Config::from_bits(u16::from_be_bytes([hi, lo])), config);
317/// ```
318#[derive(Clone, Copy, Debug, PartialEq, Eq)]
319pub struct Config {
320    /// Start a single conversion when written; reads as "not converting" when set.
321    pub start_conversion: bool,
322    /// The input multiplexer setting.
323    pub mux: Mux,
324    /// The full-scale range.
325    pub pga: Pga,
326    /// The conversion mode.
327    pub mode: Mode,
328    /// The output data rate.
329    pub data_rate: DataRate,
330    /// The comparator mode.
331    pub comparator_mode: ComparatorMode,
332    /// The ALERT/RDY pin polarity.
333    pub comparator_polarity: ComparatorPolarity,
334    /// Whether the comparator latches.
335    pub comparator_latch: ComparatorLatch,
336    /// The comparator queue, or disable.
337    pub comparator_queue: ComparatorQueue,
338}
339
340impl Default for Config {
341    fn default() -> Self {
342        Config {
343            start_conversion: true,
344            mux: Mux::Ain0Ain1,
345            pga: Pga::Fsr2_048,
346            mode: Mode::SingleShot,
347            data_rate: DataRate::Sps128,
348            comparator_mode: ComparatorMode::Traditional,
349            comparator_polarity: ComparatorPolarity::ActiveLow,
350            comparator_latch: ComparatorLatch::NonLatching,
351            comparator_queue: ComparatorQueue::Disabled,
352        }
353    }
354}
355
356impl Config {
357    /// Assembles the 16-bit Config register value.
358    ///
359    /// # Returns
360    ///
361    /// The register value to write, most significant bit first.
362    pub fn bits(self) -> u16 {
363        let mut bits = 0u16;
364        bits |= u16::from(self.start_conversion) << 15;
365        bits |= u16::from(self.mux.code()) << 12;
366        bits |= u16::from(self.pga.code()) << 9;
367        bits |= u16::from(matches!(self.mode, Mode::SingleShot)) << 8;
368        bits |= u16::from(self.data_rate.code()) << 5;
369        bits |= u16::from(matches!(self.comparator_mode, ComparatorMode::Window)) << 4;
370        bits |= u16::from(matches!(
371            self.comparator_polarity,
372            ComparatorPolarity::ActiveHigh
373        )) << 3;
374        bits |= u16::from(matches!(self.comparator_latch, ComparatorLatch::Latching)) << 2;
375        bits |= u16::from(self.comparator_queue.code());
376        bits
377    }
378
379    /// Parses a 16-bit Config register value.
380    ///
381    /// # Arguments
382    ///
383    /// * `bits` - the register value, as read from the device.
384    ///
385    /// # Returns
386    ///
387    /// The decoded configuration.
388    pub fn from_bits(bits: u16) -> Config {
389        Config {
390            start_conversion: bits & (1 << 15) != 0,
391            mux: Mux::from_code((bits >> 12) as u8),
392            pga: Pga::from_code((bits >> 9) as u8),
393            mode: if bits & (1 << 8) != 0 {
394                Mode::SingleShot
395            } else {
396                Mode::Continuous
397            },
398            data_rate: DataRate::from_code((bits >> 5) as u8),
399            comparator_mode: if bits & (1 << 4) != 0 {
400                ComparatorMode::Window
401            } else {
402                ComparatorMode::Traditional
403            },
404            comparator_polarity: if bits & (1 << 3) != 0 {
405                ComparatorPolarity::ActiveHigh
406            } else {
407                ComparatorPolarity::ActiveLow
408            },
409            comparator_latch: if bits & (1 << 2) != 0 {
410                ComparatorLatch::Latching
411            } else {
412                ComparatorLatch::NonLatching
413            },
414            comparator_queue: ComparatorQueue::from_code(bits as u8),
415        }
416    }
417}
418
419/// Converts a raw conversion result to nanovolts at the selected full-scale range.
420///
421/// The result is a 16-bit two's-complement code spanning plus or minus the full
422/// scale, so the voltage is `code * full_scale / 32768`. Working in nanovolts keeps
423/// the conversion exact in integer arithmetic across every gain setting.
424///
425/// # Arguments
426///
427/// * `pga` - the gain the conversion was taken at.
428/// * `raw` - the signed conversion register value.
429///
430/// # Returns
431///
432/// The measured voltage in nanovolts.
433pub fn to_nanovolts(pga: Pga, raw: i16) -> i64 {
434    raw as i64 * (pga.full_scale_microvolts() as i64 * 1000) / 32768
435}
436
437/// Converts a raw conversion result to volts at the selected full-scale range.
438///
439/// # Arguments
440///
441/// * `pga` - the gain the conversion was taken at.
442/// * `raw` - the signed conversion register value.
443///
444/// # Returns
445///
446/// The measured voltage in volts.
447pub fn to_volts(pga: Pga, raw: i16) -> f32 {
448    to_nanovolts(pga, raw) as f32 / 1_000_000_000.0
449}
450
451/// The longest one conversion takes at a data rate, in microseconds.
452///
453/// A conversion takes one period of the data rate, and the datasheet allows the
454/// rate to vary by ten percent, so the period is stretched by that much. A driver
455/// waits this long after starting a single-shot conversion before it polls the
456/// operational-status bit.
457///
458/// # Arguments
459///
460/// * `rate` - the data rate the conversion runs at.
461///
462/// # Returns
463///
464/// The period in microseconds, with the ten percent margin.
465///
466/// # Examples
467///
468/// ```
469/// use pamoja_sensors::ads1115::{conversion_micros, DataRate};
470///
471/// assert_eq!(conversion_micros(DataRate::Sps128), 8_595);
472/// assert_eq!(conversion_micros(DataRate::Sps860), 1_280);
473/// ```
474pub fn conversion_micros(rate: DataRate) -> u32 {
475    let period = 1_000_000u32.div_ceil(u32::from(rate.samples_per_second()));
476    period + period.div_ceil(10)
477}
478
479/// One conversion result with the gain it was taken at, so it converts to volts.
480#[derive(Clone, Copy, Debug, PartialEq, Eq)]
481pub struct Sample {
482    /// The signed 16-bit conversion register value.
483    pub raw: i16,
484    /// The full-scale range the conversion ran at.
485    pub pga: Pga,
486}
487
488impl Sample {
489    /// Returns the measured voltage in volts.
490    pub fn volts(&self) -> f32 {
491        to_volts(self.pga, self.raw)
492    }
493
494    /// Returns the measured voltage in nanovolts, exact in integer arithmetic.
495    pub fn nanovolts(&self) -> i64 {
496        to_nanovolts(self.pga, self.raw)
497    }
498
499    /// Reports whether the conversion sits at either end code, 7FFFh or 8000h, where the
500    /// output clips for a signal beyond the full-scale range (datasheet section 7.5.4 and
501    /// Table 7-3).
502    ///
503    /// A clipped sample means the input is at or past the range the gain selects, so its
504    /// voltage is a bound rather than the reading; a wider range reads it.
505    ///
506    /// # Returns
507    ///
508    /// `true` at either end code.
509    ///
510    /// # Examples
511    ///
512    /// ```
513    /// use pamoja_sensors::ads1115::{Pga, Sample};
514    ///
515    /// let pinned = Sample { raw: i16::MAX, pga: Pga::Fsr2_048 };
516    /// assert!(pinned.clipped());
517    /// assert!(!Sample { raw: 18_800, pga: Pga::Fsr4_096 }.clipped());
518    /// ```
519    pub fn clipped(&self) -> bool {
520        self.raw == i16::MAX || self.raw == i16::MIN
521    }
522
523    /// Returns the conversion register bytes for this sample, most significant first.
524    ///
525    /// This is what the part sends when the register is read, so a test can script it.
526    pub fn to_bytes(&self) -> [u8; 2] {
527        self.raw.to_be_bytes()
528    }
529}
530
531#[cfg(test)]
532mod driver_support_tests {
533    use super::*;
534
535    #[test]
536    fn only_the_end_codes_of_table_7_3_are_clipped() {
537        // Table 7-3: at or above +FS reads 7FFFh, at or below -FS reads 8000h, and one step
538        // either side of zero reads 0001h and FFFFh.
539        let code = |raw: u16| Sample {
540            raw: raw as i16,
541            pga: Pga::Fsr2_048,
542        };
543        assert!(code(0x7FFF).clipped());
544        assert!(code(0x8000).clipped());
545        assert!(!code(0x0001).clipped());
546        assert!(!code(0x0000).clipped());
547        assert!(!code(0xFFFF).clipped());
548        assert!(!code(0x7FFE).clipped());
549        assert!(!code(0x8001).clipped());
550    }
551
552    #[test]
553    fn conversion_time_is_one_period_plus_the_ten_percent_variation() {
554        assert_eq!(conversion_micros(DataRate::Sps8), 137_500);
555        assert_eq!(conversion_micros(DataRate::Sps128), 8_595);
556        assert_eq!(conversion_micros(DataRate::Sps860), 1_280);
557    }
558
559    #[test]
560    fn a_sample_converts_at_its_gain_and_scripts_its_bytes() {
561        let sample = Sample {
562            raw: 3200,
563            pga: Pga::Fsr2_048,
564        };
565        assert!((sample.volts() - 0.2).abs() < 1e-6);
566        assert_eq!(sample.nanovolts(), 200_000_000);
567        assert_eq!(sample.to_bytes(), [0x0C, 0x80]);
568    }
569
570    #[test]
571    fn the_addresses_follow_the_addr_pin_table() {
572        assert_eq!(address::GND, 0b100_1000);
573        assert_eq!(address::VDD, 0b100_1001);
574        assert_eq!(address::SDA, 0b100_1010);
575        assert_eq!(address::SCL, 0b100_1011);
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582
583    #[test]
584    fn default_config_is_the_datasheet_reset_value() {
585        assert_eq!(Config::default().bits(), CONFIG_RESET);
586        assert_eq!(Config::from_bits(CONFIG_RESET), Config::default());
587    }
588
589    #[test]
590    fn config_round_trips_through_bits() {
591        let config = Config {
592            start_conversion: false,
593            mux: Mux::Ain2Gnd,
594            pga: Pga::Fsr0_256,
595            mode: Mode::Continuous,
596            data_rate: DataRate::Sps860,
597            comparator_mode: ComparatorMode::Window,
598            comparator_polarity: ComparatorPolarity::ActiveHigh,
599            comparator_latch: ComparatorLatch::Latching,
600            comparator_queue: ComparatorQueue::AfterFour,
601        };
602        assert_eq!(Config::from_bits(config.bits()), config);
603    }
604
605    #[test]
606    fn pga_codes_match_the_datasheet() {
607        assert_eq!(Pga::Fsr6_144.code(), 0b000);
608        assert_eq!(Pga::Fsr2_048.code(), 0b010);
609        assert_eq!(Pga::Fsr0_256.code(), 0b101);
610        // The reserved 110 and 111 codes also select ±0.256 V.
611        assert_eq!(Pga::from_code(0b110), Pga::Fsr0_256);
612        assert_eq!(Pga::from_code(0b111), Pga::Fsr0_256);
613    }
614
615    #[test]
616    fn full_scale_conversion_matches_the_per_gain_lsb() {
617        // One count at ±4.096 V is 125 µV; at ±6.144 V it is 187.5 µV.
618        assert_eq!(to_nanovolts(Pga::Fsr4_096, 1), 125_000);
619        assert_eq!(to_nanovolts(Pga::Fsr6_144, 1), 187_500);
620        // Two counts at ±0.256 V is 15.625 µV, exact in nanovolts.
621        assert_eq!(to_nanovolts(Pga::Fsr0_256, 2), 15_625);
622        // Positive full-scale code at ±4.096 V is just under the 4.096 V range.
623        assert_eq!(to_nanovolts(Pga::Fsr4_096, 0x7FFF), 4_095_875_000);
624        // Negative codes scale symmetrically.
625        assert_eq!(to_nanovolts(Pga::Fsr2_048, -16384), -1_024_000_000);
626    }
627}