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}