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}