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/// The ADS1115 register addresses, selected by the address pointer.
11pub mod register {
12    /// Conversion register: the last 16-bit result, two's complement.
13    pub const CONVERSION: u8 = 0x00;
14    /// Config register: input, gain, mode, data rate, and comparator settings.
15    pub const CONFIG: u8 = 0x01;
16    /// Low threshold register for the comparator.
17    pub const LO_THRESH: u8 = 0x02;
18    /// High threshold register for the comparator.
19    pub const HI_THRESH: u8 = 0x03;
20}
21
22/// The power-on value of the Config register (0x8583).
23pub const CONFIG_RESET: u16 = 0x8583;
24
25/// The input multiplexer setting (Config bits 14:12).
26///
27/// The ADS1115 measures either of two differential pairs against `AIN3`, the pair
28/// `AIN0`/`AIN1`, or any one input against ground.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum Mux {
31    /// AINP = AIN0, AINN = AIN1 (the default).
32    Ain0Ain1,
33    /// AINP = AIN0, AINN = AIN3.
34    Ain0Ain3,
35    /// AINP = AIN1, AINN = AIN3.
36    Ain1Ain3,
37    /// AINP = AIN2, AINN = AIN3.
38    Ain2Ain3,
39    /// AINP = AIN0, AINN = GND (single-ended).
40    Ain0Gnd,
41    /// AINP = AIN1, AINN = GND (single-ended).
42    Ain1Gnd,
43    /// AINP = AIN2, AINN = GND (single-ended).
44    Ain2Gnd,
45    /// AINP = AIN3, AINN = GND (single-ended).
46    Ain3Gnd,
47}
48
49impl Mux {
50    /// Returns the 3-bit field code for this multiplexer setting.
51    pub fn code(self) -> u8 {
52        match self {
53            Mux::Ain0Ain1 => 0b000,
54            Mux::Ain0Ain3 => 0b001,
55            Mux::Ain1Ain3 => 0b010,
56            Mux::Ain2Ain3 => 0b011,
57            Mux::Ain0Gnd => 0b100,
58            Mux::Ain1Gnd => 0b101,
59            Mux::Ain2Gnd => 0b110,
60            Mux::Ain3Gnd => 0b111,
61        }
62    }
63
64    /// Builds a multiplexer setting from a 3-bit field code (the low three bits used).
65    pub fn from_code(code: u8) -> Mux {
66        match code & 0b111 {
67            0b000 => Mux::Ain0Ain1,
68            0b001 => Mux::Ain0Ain3,
69            0b010 => Mux::Ain1Ain3,
70            0b011 => Mux::Ain2Ain3,
71            0b100 => Mux::Ain0Gnd,
72            0b101 => Mux::Ain1Gnd,
73            0b110 => Mux::Ain2Gnd,
74            _ => Mux::Ain3Gnd,
75        }
76    }
77}
78
79/// The programmable gain amplifier's full-scale range (Config bits 11:9).
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub enum Pga {
82    /// Full-scale range ±6.144 V.
83    Fsr6_144,
84    /// Full-scale range ±4.096 V.
85    Fsr4_096,
86    /// Full-scale range ±2.048 V (the default).
87    Fsr2_048,
88    /// Full-scale range ±1.024 V.
89    Fsr1_024,
90    /// Full-scale range ±0.512 V.
91    Fsr0_512,
92    /// Full-scale range ±0.256 V.
93    Fsr0_256,
94}
95
96impl Pga {
97    /// Returns the 3-bit field code for this gain setting.
98    pub fn code(self) -> u8 {
99        match self {
100            Pga::Fsr6_144 => 0b000,
101            Pga::Fsr4_096 => 0b001,
102            Pga::Fsr2_048 => 0b010,
103            Pga::Fsr1_024 => 0b011,
104            Pga::Fsr0_512 => 0b100,
105            Pga::Fsr0_256 => 0b101,
106        }
107    }
108
109    /// Builds a gain setting from a 3-bit field code.
110    ///
111    /// Codes `110` and `111` are documented as also selecting ±0.256 V and map here
112    /// to [`Pga::Fsr0_256`].
113    pub fn from_code(code: u8) -> Pga {
114        match code & 0b111 {
115            0b000 => Pga::Fsr6_144,
116            0b001 => Pga::Fsr4_096,
117            0b010 => Pga::Fsr2_048,
118            0b011 => Pga::Fsr1_024,
119            0b100 => Pga::Fsr0_512,
120            _ => Pga::Fsr0_256,
121        }
122    }
123
124    /// Returns the positive full-scale input voltage for this gain, in microvolts.
125    pub fn full_scale_microvolts(self) -> u32 {
126        match self {
127            Pga::Fsr6_144 => 6_144_000,
128            Pga::Fsr4_096 => 4_096_000,
129            Pga::Fsr2_048 => 2_048_000,
130            Pga::Fsr1_024 => 1_024_000,
131            Pga::Fsr0_512 => 512_000,
132            Pga::Fsr0_256 => 256_000,
133        }
134    }
135}
136
137/// The conversion mode (Config bit 8).
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
139pub enum Mode {
140    /// Convert continuously.
141    Continuous,
142    /// Convert once per request, then power down (the default).
143    SingleShot,
144}
145
146/// The output data rate (Config bits 7:5), in samples per second.
147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub enum DataRate {
149    /// 8 samples per second.
150    Sps8,
151    /// 16 samples per second.
152    Sps16,
153    /// 32 samples per second.
154    Sps32,
155    /// 64 samples per second.
156    Sps64,
157    /// 128 samples per second (the default).
158    Sps128,
159    /// 250 samples per second.
160    Sps250,
161    /// 475 samples per second.
162    Sps475,
163    /// 860 samples per second.
164    Sps860,
165}
166
167impl DataRate {
168    /// Returns the 3-bit field code for this data rate.
169    pub fn code(self) -> u8 {
170        match self {
171            DataRate::Sps8 => 0b000,
172            DataRate::Sps16 => 0b001,
173            DataRate::Sps32 => 0b010,
174            DataRate::Sps64 => 0b011,
175            DataRate::Sps128 => 0b100,
176            DataRate::Sps250 => 0b101,
177            DataRate::Sps475 => 0b110,
178            DataRate::Sps860 => 0b111,
179        }
180    }
181
182    /// Builds a data rate from a 3-bit field code.
183    pub fn from_code(code: u8) -> DataRate {
184        match code & 0b111 {
185            0b000 => DataRate::Sps8,
186            0b001 => DataRate::Sps16,
187            0b010 => DataRate::Sps32,
188            0b011 => DataRate::Sps64,
189            0b100 => DataRate::Sps128,
190            0b101 => DataRate::Sps250,
191            0b110 => DataRate::Sps475,
192            _ => DataRate::Sps860,
193        }
194    }
195
196    /// Returns the rate in samples per second.
197    pub fn samples_per_second(self) -> u16 {
198        match self {
199            DataRate::Sps8 => 8,
200            DataRate::Sps16 => 16,
201            DataRate::Sps32 => 32,
202            DataRate::Sps64 => 64,
203            DataRate::Sps128 => 128,
204            DataRate::Sps250 => 250,
205            DataRate::Sps475 => 475,
206            DataRate::Sps860 => 860,
207        }
208    }
209}
210
211/// The comparator mode (Config bit 4).
212#[derive(Clone, Copy, Debug, PartialEq, Eq)]
213pub enum ComparatorMode {
214    /// Traditional comparator with hysteresis (the default).
215    Traditional,
216    /// Window comparator.
217    Window,
218}
219
220/// The ALERT/RDY pin polarity (Config bit 3).
221#[derive(Clone, Copy, Debug, PartialEq, Eq)]
222pub enum ComparatorPolarity {
223    /// Active low (the default).
224    ActiveLow,
225    /// Active high.
226    ActiveHigh,
227}
228
229/// Whether the comparator latches (Config bit 2).
230#[derive(Clone, Copy, Debug, PartialEq, Eq)]
231pub enum ComparatorLatch {
232    /// Non-latching: the pin clears once readings return within the thresholds (the
233    /// default).
234    NonLatching,
235    /// Latching: the pin stays asserted until the conversion data is read.
236    Latching,
237}
238
239/// The comparator queue, or disable (Config bits 1:0).
240#[derive(Clone, Copy, Debug, PartialEq, Eq)]
241pub enum ComparatorQueue {
242    /// Assert after one conversion beyond a threshold.
243    AfterOne,
244    /// Assert after two conversions beyond a threshold.
245    AfterTwo,
246    /// Assert after four conversions beyond a threshold.
247    AfterFour,
248    /// Disable the comparator and set ALERT/RDY high-impedance (the default).
249    Disabled,
250}
251
252impl ComparatorQueue {
253    /// Returns the 2-bit field code for this queue setting.
254    pub fn code(self) -> u8 {
255        match self {
256            ComparatorQueue::AfterOne => 0b00,
257            ComparatorQueue::AfterTwo => 0b01,
258            ComparatorQueue::AfterFour => 0b10,
259            ComparatorQueue::Disabled => 0b11,
260        }
261    }
262
263    /// Builds a queue setting from a 2-bit field code.
264    pub fn from_code(code: u8) -> ComparatorQueue {
265        match code & 0b11 {
266            0b00 => ComparatorQueue::AfterOne,
267            0b01 => ComparatorQueue::AfterTwo,
268            0b10 => ComparatorQueue::AfterFour,
269            _ => ComparatorQueue::Disabled,
270        }
271    }
272}
273
274/// A decoded ADS1115 Config register.
275///
276/// Build one, set the fields, and turn it into the 16-bit register value with
277/// [`bits`](Config::bits); or parse a register read with [`from_bits`](Config::from_bits).
278/// [`Config::default`] is the power-on state, `0x8583`.
279///
280/// # Examples
281///
282/// ```
283/// use pamoja_sensors::ads1115::{Config, Mux, Pga};
284///
285/// // Read AIN0 against ground at the ±4.096 V range, leaving everything else default.
286/// let config = Config {
287///     mux: Mux::Ain0Gnd,
288///     pga: Pga::Fsr4_096,
289///     ..Config::default()
290/// };
291/// // The high byte then low byte are written to the Config register.
292/// let [hi, lo] = config.bits().to_be_bytes();
293/// assert_eq!(Config::from_bits(u16::from_be_bytes([hi, lo])), config);
294/// ```
295#[derive(Clone, Copy, Debug, PartialEq, Eq)]
296pub struct Config {
297    /// Start a single conversion when written; reads as "not converting" when set.
298    pub start_conversion: bool,
299    /// The input multiplexer setting.
300    pub mux: Mux,
301    /// The full-scale range.
302    pub pga: Pga,
303    /// The conversion mode.
304    pub mode: Mode,
305    /// The output data rate.
306    pub data_rate: DataRate,
307    /// The comparator mode.
308    pub comparator_mode: ComparatorMode,
309    /// The ALERT/RDY pin polarity.
310    pub comparator_polarity: ComparatorPolarity,
311    /// Whether the comparator latches.
312    pub comparator_latch: ComparatorLatch,
313    /// The comparator queue, or disable.
314    pub comparator_queue: ComparatorQueue,
315}
316
317impl Default for Config {
318    fn default() -> Self {
319        Config {
320            start_conversion: true,
321            mux: Mux::Ain0Ain1,
322            pga: Pga::Fsr2_048,
323            mode: Mode::SingleShot,
324            data_rate: DataRate::Sps128,
325            comparator_mode: ComparatorMode::Traditional,
326            comparator_polarity: ComparatorPolarity::ActiveLow,
327            comparator_latch: ComparatorLatch::NonLatching,
328            comparator_queue: ComparatorQueue::Disabled,
329        }
330    }
331}
332
333impl Config {
334    /// Assembles the 16-bit Config register value.
335    ///
336    /// # Returns
337    ///
338    /// The register value to write, most significant bit first.
339    pub fn bits(self) -> u16 {
340        let mut bits = 0u16;
341        bits |= u16::from(self.start_conversion) << 15;
342        bits |= u16::from(self.mux.code()) << 12;
343        bits |= u16::from(self.pga.code()) << 9;
344        bits |= u16::from(matches!(self.mode, Mode::SingleShot)) << 8;
345        bits |= u16::from(self.data_rate.code()) << 5;
346        bits |= u16::from(matches!(self.comparator_mode, ComparatorMode::Window)) << 4;
347        bits |= u16::from(matches!(
348            self.comparator_polarity,
349            ComparatorPolarity::ActiveHigh
350        )) << 3;
351        bits |= u16::from(matches!(self.comparator_latch, ComparatorLatch::Latching)) << 2;
352        bits |= u16::from(self.comparator_queue.code());
353        bits
354    }
355
356    /// Parses a 16-bit Config register value.
357    ///
358    /// # Arguments
359    ///
360    /// * `bits` - the register value, as read from the device.
361    ///
362    /// # Returns
363    ///
364    /// The decoded configuration.
365    pub fn from_bits(bits: u16) -> Config {
366        Config {
367            start_conversion: bits & (1 << 15) != 0,
368            mux: Mux::from_code((bits >> 12) as u8),
369            pga: Pga::from_code((bits >> 9) as u8),
370            mode: if bits & (1 << 8) != 0 {
371                Mode::SingleShot
372            } else {
373                Mode::Continuous
374            },
375            data_rate: DataRate::from_code((bits >> 5) as u8),
376            comparator_mode: if bits & (1 << 4) != 0 {
377                ComparatorMode::Window
378            } else {
379                ComparatorMode::Traditional
380            },
381            comparator_polarity: if bits & (1 << 3) != 0 {
382                ComparatorPolarity::ActiveHigh
383            } else {
384                ComparatorPolarity::ActiveLow
385            },
386            comparator_latch: if bits & (1 << 2) != 0 {
387                ComparatorLatch::Latching
388            } else {
389                ComparatorLatch::NonLatching
390            },
391            comparator_queue: ComparatorQueue::from_code(bits as u8),
392        }
393    }
394}
395
396/// Converts a raw conversion result to nanovolts at the selected full-scale range.
397///
398/// The result is a 16-bit two's-complement code spanning plus or minus the full
399/// scale, so the voltage is `code * full_scale / 32768`. Working in nanovolts keeps
400/// the conversion exact in integer arithmetic across every gain setting.
401///
402/// # Arguments
403///
404/// * `pga` - the gain the conversion was taken at.
405/// * `raw` - the signed conversion register value.
406///
407/// # Returns
408///
409/// The measured voltage in nanovolts.
410pub fn to_nanovolts(pga: Pga, raw: i16) -> i64 {
411    raw as i64 * (pga.full_scale_microvolts() as i64 * 1000) / 32768
412}
413
414/// Converts a raw conversion result to volts at the selected full-scale range.
415///
416/// # Arguments
417///
418/// * `pga` - the gain the conversion was taken at.
419/// * `raw` - the signed conversion register value.
420///
421/// # Returns
422///
423/// The measured voltage in volts.
424pub fn to_volts(pga: Pga, raw: i16) -> f32 {
425    to_nanovolts(pga, raw) as f32 / 1_000_000_000.0
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431
432    #[test]
433    fn default_config_is_the_datasheet_reset_value() {
434        assert_eq!(Config::default().bits(), CONFIG_RESET);
435        assert_eq!(Config::from_bits(CONFIG_RESET), Config::default());
436    }
437
438    #[test]
439    fn config_round_trips_through_bits() {
440        let config = Config {
441            start_conversion: false,
442            mux: Mux::Ain2Gnd,
443            pga: Pga::Fsr0_256,
444            mode: Mode::Continuous,
445            data_rate: DataRate::Sps860,
446            comparator_mode: ComparatorMode::Window,
447            comparator_polarity: ComparatorPolarity::ActiveHigh,
448            comparator_latch: ComparatorLatch::Latching,
449            comparator_queue: ComparatorQueue::AfterFour,
450        };
451        assert_eq!(Config::from_bits(config.bits()), config);
452    }
453
454    #[test]
455    fn pga_codes_match_the_datasheet() {
456        assert_eq!(Pga::Fsr6_144.code(), 0b000);
457        assert_eq!(Pga::Fsr2_048.code(), 0b010);
458        assert_eq!(Pga::Fsr0_256.code(), 0b101);
459        // The reserved 110 and 111 codes also select ±0.256 V.
460        assert_eq!(Pga::from_code(0b110), Pga::Fsr0_256);
461        assert_eq!(Pga::from_code(0b111), Pga::Fsr0_256);
462    }
463
464    #[test]
465    fn full_scale_conversion_matches_the_per_gain_lsb() {
466        // One count at ±4.096 V is 125 µV; at ±6.144 V it is 187.5 µV.
467        assert_eq!(to_nanovolts(Pga::Fsr4_096, 1), 125_000);
468        assert_eq!(to_nanovolts(Pga::Fsr6_144, 1), 187_500);
469        // Two counts at ±0.256 V is 15.625 µV, exact in nanovolts.
470        assert_eq!(to_nanovolts(Pga::Fsr0_256, 2), 15_625);
471        // Positive full-scale code at ±4.096 V is just under the 4.096 V range.
472        assert_eq!(to_nanovolts(Pga::Fsr4_096, 0x7FFF), 4_095_875_000);
473        // Negative codes scale symmetrically.
474        assert_eq!(to_nanovolts(Pga::Fsr2_048, -16384), -1_024_000_000);
475    }
476}