Skip to main content

pamoja_sensors/
ds18b20.rs

1//! Maxim DS18B20 1-Wire digital thermometer.
2//!
3//! The DS18B20 reports temperature as a 16-bit two's-complement number in a nine-byte
4//! scratchpad, with a CRC byte that covers the rest. This module decodes that
5//! temperature exactly as the datasheet's temperature/data table specifies, reads the
6//! resolution out of the configuration byte, and verifies the scratchpad's CRC with
7//! the Maxim 1-Wire polynomial so a read corrupted on the bus is caught rather than
8//! trusted.
9//!
10//! It is pure logic: a caller drives the 1-Wire transactions (convert, then read
11//! scratchpad) and hands the nine bytes to [`Scratchpad::parse`].
12
13use crate::SensorError;
14
15/// The 1-Wire family code that identifies a DS18B20 in the first ROM byte.
16pub const FAMILY_CODE: u8 = 0x28;
17
18/// The conversion resolution, selected by the R1/R0 bits of the configuration byte.
19///
20/// Higher resolution resolves smaller steps but takes longer to convert, a tradeoff
21/// the datasheet spells out. The temperature register always carries 1/16 °C per
22/// count; at lower resolutions the unused low bits simply read as zero.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum Resolution {
25    /// 9-bit, 0.5 °C steps, up to 93.75 ms per conversion.
26    Bits9,
27    /// 10-bit, 0.25 °C steps, up to 187.5 ms per conversion.
28    Bits10,
29    /// 11-bit, 0.125 °C steps, up to 375 ms per conversion.
30    Bits11,
31    /// 12-bit, 0.0625 °C steps, up to 750 ms per conversion. The power-on default.
32    Bits12,
33}
34
35impl Resolution {
36    /// Returns the number of significant bits this resolution produces.
37    ///
38    /// # Returns
39    ///
40    /// `9`, `10`, `11`, or `12`.
41    pub fn bits(self) -> u8 {
42        match self {
43            Resolution::Bits9 => 9,
44            Resolution::Bits10 => 10,
45            Resolution::Bits11 => 11,
46            Resolution::Bits12 => 12,
47        }
48    }
49
50    /// Returns the configuration-register byte that selects this resolution.
51    ///
52    /// The byte places R1/R0 in bits 6:5 over the datasheet's fixed surrounding
53    /// pattern (bit 7 clear, bit 4 set, bits 3:0 set), so 12-bit is `0x7F`, 11-bit
54    /// `0x5F`, 10-bit `0x3F`, and 9-bit `0x1F`.
55    ///
56    /// # Returns
57    ///
58    /// The configuration byte written to scratchpad byte 4.
59    pub fn config_byte(self) -> u8 {
60        let r1r0 = match self {
61            Resolution::Bits9 => 0b00,
62            Resolution::Bits10 => 0b01,
63            Resolution::Bits11 => 0b10,
64            Resolution::Bits12 => 0b11,
65        };
66        0b0001_1111 | (r1r0 << 5)
67    }
68
69    /// Reads the resolution out of a configuration byte's R1/R0 bits.
70    ///
71    /// # Arguments
72    ///
73    /// * `byte` - the configuration register (scratchpad byte 4).
74    ///
75    /// # Returns
76    ///
77    /// The resolution selected by bits 6:5.
78    pub fn from_config_byte(byte: u8) -> Resolution {
79        match (byte >> 5) & 0b11 {
80            0b00 => Resolution::Bits9,
81            0b01 => Resolution::Bits10,
82            0b10 => Resolution::Bits11,
83            _ => Resolution::Bits12,
84        }
85    }
86
87    /// Returns the temperature step this resolution resolves, in micro-degrees Celsius.
88    ///
89    /// # Returns
90    ///
91    /// `500000` (0.5 °C) for 9-bit down to `62500` (0.0625 °C) for 12-bit.
92    pub fn step_micro_celsius(self) -> u32 {
93        match self {
94            Resolution::Bits9 => 500_000,
95            Resolution::Bits10 => 250_000,
96            Resolution::Bits11 => 125_000,
97            Resolution::Bits12 => 62_500,
98        }
99    }
100
101    /// Returns the datasheet's maximum conversion time, in microseconds.
102    ///
103    /// # Returns
104    ///
105    /// `93750` for 9-bit, doubling up to `750000` for 12-bit.
106    pub fn max_conversion_micros(self) -> u32 {
107        match self {
108            Resolution::Bits9 => 93_750,
109            Resolution::Bits10 => 187_500,
110            Resolution::Bits11 => 375_000,
111            Resolution::Bits12 => 750_000,
112        }
113    }
114}
115
116/// Converts a raw temperature register value to micro-degrees Celsius, exactly.
117///
118/// Each count is 1/16 °C, which is 62500 micro-degrees, so the conversion is exact in
119/// integer arithmetic and needs no floating point.
120///
121/// # Arguments
122///
123/// * `raw` - the 16-bit two's-complement temperature register, as a signed value.
124///
125/// # Returns
126///
127/// The temperature in micro-degrees Celsius (millionths of a degree).
128pub fn temperature_to_micro_celsius(raw: i16) -> i32 {
129    raw as i32 * 62_500
130}
131
132/// Converts a raw temperature register value to degrees Celsius.
133///
134/// # Arguments
135///
136/// * `raw` - the 16-bit two's-complement temperature register, as a signed value.
137///
138/// # Returns
139///
140/// The temperature in degrees Celsius.
141pub fn temperature_to_celsius(raw: i16) -> f32 {
142    raw as f32 / 16.0
143}
144
145/// Converts micro-degrees Celsius to a raw temperature register value.
146///
147/// The result is truncated to the step `resolution` resolves, since the datasheet
148/// specifies that the bits below a selected resolution read as zero.
149///
150/// # Arguments
151///
152/// * `micro_celsius` - the temperature in millionths of a degree Celsius.
153/// * `resolution` - the resolution the part is configured for.
154///
155/// # Returns
156///
157/// The 16-bit two's-complement temperature register, as a signed value.
158pub fn temperature_from_micro_celsius(micro_celsius: i32, resolution: Resolution) -> i16 {
159    let counts = micro_celsius.div_euclid(62_500) as i16;
160    let unused = 12 - resolution.bits();
161    (counts >> unused) << unused
162}
163
164/// Converts degrees Celsius to a raw temperature register value.
165///
166/// # Arguments
167///
168/// * `celsius` - the temperature in degrees Celsius.
169/// * `resolution` - the resolution the part is configured for.
170///
171/// # Returns
172///
173/// The 16-bit two's-complement temperature register, as a signed value.
174pub fn temperature_from_celsius(celsius: f32, resolution: Resolution) -> i16 {
175    temperature_from_micro_celsius((celsius * 1_000_000.0) as i32, resolution)
176}
177
178/// Computes the Maxim 1-Wire CRC-8 over `data`.
179///
180/// This is the CRC the DS18B20 (and every Maxim 1-Wire device) appends to its ROM
181/// code and scratchpad. The polynomial is X^8 + X^5 + X^4 + 1, processed
182/// least-significant-bit first from a zero shift register, which is the reflected
183/// form `0x8C`.
184///
185/// # Arguments
186///
187/// * `data` - the bytes the CRC covers, in transmission order.
188///
189/// # Returns
190///
191/// The 8-bit CRC; for a correctly received message followed by its CRC byte, running
192/// this over all of them yields zero.
193pub fn crc8(data: &[u8]) -> u8 {
194    let mut crc = 0u8;
195    for &byte in data {
196        let mut bits = byte;
197        for _ in 0..8 {
198            let mix = (crc ^ bits) & 0x01;
199            crc >>= 1;
200            if mix != 0 {
201                crc ^= 0x8C;
202            }
203            bits >>= 1;
204        }
205    }
206    crc
207}
208
209/// A decoded, CRC-verified DS18B20 scratchpad.
210///
211/// The scratchpad is nine bytes: temperature LSB and MSB, the high and low alarm
212/// thresholds, the configuration byte, three reserved bytes, and a CRC. [`parse`]
213/// checks the CRC before exposing any of it.
214///
215/// [`parse`]: Scratchpad::parse
216///
217/// # Examples
218///
219/// ```
220/// use pamoja_sensors::ds18b20::{temperature_from_celsius, Resolution, Scratchpad};
221///
222/// // What a part sitting at +25.0625 °C, set to 12-bit steps with its thresholds at
223/// // +75/-10 °C, puts on the bus. A driver reads these nine bytes off the wire.
224/// let bytes = Scratchpad::new(
225///     temperature_from_celsius(25.0625, Resolution::Bits12),
226///     Resolution::Bits12,
227///     75,
228///     -10,
229/// )
230/// .to_bytes();
231///
232/// let scratchpad = Scratchpad::parse(&bytes)?;
233/// assert_eq!(scratchpad.temperature_micro_celsius(), 25_062_500);
234/// assert_eq!(scratchpad.resolution(), Resolution::Bits12);
235/// # Ok::<(), pamoja_sensors::SensorError>(())
236/// ```
237#[derive(Clone, Copy, Debug, PartialEq, Eq)]
238pub struct Scratchpad {
239    raw_temperature: i16,
240    alarm_high: i8,
241    alarm_low: i8,
242    resolution: Resolution,
243}
244
245impl Scratchpad {
246    /// Parses and CRC-checks a nine-byte scratchpad.
247    ///
248    /// # Arguments
249    ///
250    /// * `bytes` - the nine scratchpad bytes in the order the device sends them, the
251    ///   ninth being the CRC.
252    ///
253    /// # Returns
254    ///
255    /// The decoded scratchpad.
256    ///
257    /// # Errors
258    ///
259    /// Returns [`SensorError::Crc`] if the CRC byte does not match the first eight,
260    /// which means the read was corrupted and should be repeated.
261    pub fn parse(bytes: &[u8; 9]) -> Result<Scratchpad, SensorError> {
262        if crc8(&bytes[..8]) != bytes[8] {
263            return Err(SensorError::Crc);
264        }
265        let raw_temperature = i16::from_le_bytes([bytes[0], bytes[1]]);
266        Ok(Scratchpad {
267            raw_temperature,
268            alarm_high: bytes[2] as i8,
269            alarm_low: bytes[3] as i8,
270            resolution: Resolution::from_config_byte(bytes[4]),
271        })
272    }
273
274    /// Builds the scratchpad a part in the given state reports.
275    ///
276    /// This is the inverse of [`parse`](Self::parse), so a node can be developed and
277    /// tested against the bytes a thermometer would send without one attached.
278    ///
279    /// # Arguments
280    ///
281    /// * `raw_temperature` - the 16-bit two's-complement temperature register, as a
282    ///   signed value; [`temperature_from_celsius`] builds one from a temperature.
283    /// * `resolution` - the resolution the part is configured for.
284    /// * `alarm_high` - the high alarm threshold in whole degrees Celsius (TH).
285    /// * `alarm_low` - the low alarm threshold in whole degrees Celsius (TL).
286    ///
287    /// # Returns
288    ///
289    /// The scratchpad holding those values.
290    pub fn new(
291        raw_temperature: i16,
292        resolution: Resolution,
293        alarm_high: i8,
294        alarm_low: i8,
295    ) -> Scratchpad {
296        Scratchpad {
297            raw_temperature,
298            alarm_high,
299            alarm_low,
300            resolution,
301        }
302    }
303
304    /// Returns the nine bytes a part holding this scratchpad puts on the bus.
305    ///
306    /// Bytes 5 to 7 are the reserved bytes, which a part reports but which carry no
307    /// reading; they are filled with the values its scratchpad figure shows so the
308    /// frame is the one a device would actually send. The ninth byte is the CRC over
309    /// the other eight, so the result parses.
310    ///
311    /// # Returns
312    ///
313    /// The nine scratchpad bytes in transmission order, CRC last.
314    pub fn to_bytes(&self) -> [u8; 9] {
315        let temperature = self.raw_temperature.to_le_bytes();
316        let mut bytes = [
317            temperature[0],
318            temperature[1],
319            self.alarm_high as u8,
320            self.alarm_low as u8,
321            self.resolution.config_byte(),
322            0xFF,
323            0x0C,
324            0x10,
325            0x00,
326        ];
327        bytes[8] = crc8(&bytes[..8]);
328        bytes
329    }
330
331    /// Returns the raw temperature register value.
332    ///
333    /// # Returns
334    ///
335    /// The 16-bit two's-complement register, as a signed value.
336    pub fn raw_temperature(&self) -> i16 {
337        self.raw_temperature
338    }
339
340    /// Returns the temperature in micro-degrees Celsius.
341    ///
342    /// # Returns
343    ///
344    /// The temperature, exact, in millionths of a degree Celsius.
345    pub fn temperature_micro_celsius(&self) -> i32 {
346        temperature_to_micro_celsius(self.raw_temperature)
347    }
348
349    /// Returns the temperature in degrees Celsius.
350    ///
351    /// # Returns
352    ///
353    /// The temperature in degrees Celsius.
354    pub fn temperature_celsius(&self) -> f32 {
355        temperature_to_celsius(self.raw_temperature)
356    }
357
358    /// Returns the configured conversion resolution.
359    pub fn resolution(&self) -> Resolution {
360        self.resolution
361    }
362
363    /// Returns the high temperature alarm threshold (TH), in whole degrees Celsius.
364    pub fn alarm_high(&self) -> i8 {
365        self.alarm_high
366    }
367
368    /// Returns the low temperature alarm threshold (TL), in whole degrees Celsius.
369    pub fn alarm_low(&self) -> i8 {
370        self.alarm_low
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn temperature_table_matches_the_datasheet() {
380        // The DS18B20 datasheet's temperature/data relationship table, in 1/16 °C.
381        let table: &[(i16, i32)] = &[
382            (0x07D0, 125_000_000),
383            (0x0550, 85_000_000),
384            (0x0191, 25_062_500),
385            (0x00A2, 10_125_000),
386            (0x0008, 500_000),
387            (0x0000, 0),
388            (i16::from_le_bytes([0xF8, 0xFF]), -500_000),
389            (i16::from_le_bytes([0x5E, 0xFF]), -10_125_000),
390            (i16::from_le_bytes([0x6F, 0xFE]), -25_062_500),
391            (i16::from_le_bytes([0x90, 0xFC]), -55_000_000),
392        ];
393        for &(raw, micro) in table {
394            assert_eq!(temperature_to_micro_celsius(raw), micro, "raw {raw:#06x}");
395        }
396    }
397
398    #[test]
399    fn crc8_matches_the_published_check_value() {
400        // CRC-8/MAXIM-DOW check value for the ASCII string "123456789" is 0xA1.
401        assert_eq!(crc8(b"123456789"), 0xA1);
402        // An empty message leaves the zero-initialised register untouched.
403        assert_eq!(crc8(&[]), 0x00);
404    }
405
406    #[test]
407    fn crc_over_a_message_and_its_crc_is_zero() {
408        let data = [0x28, 0xFF, 0x64, 0x1E, 0x0C, 0x00, 0x00, 0x00];
409        let crc = crc8(&data);
410        let mut with_crc = [0u8; 9];
411        with_crc[..8].copy_from_slice(&data);
412        with_crc[8] = crc;
413        assert_eq!(crc8(&with_crc), 0x00);
414    }
415
416    #[test]
417    fn a_scratchpad_round_trips_through_parse() {
418        let mut bytes = [0x91, 0x01, 75, 0xF6, 0x7F, 0xFF, 0x00, 0x10, 0x00];
419        bytes[8] = crc8(&bytes[..8]);
420        let scratchpad = Scratchpad::parse(&bytes).expect("valid crc");
421        assert_eq!(scratchpad.raw_temperature(), 0x0191);
422        assert_eq!(scratchpad.temperature_micro_celsius(), 25_062_500);
423        assert_eq!(scratchpad.resolution(), Resolution::Bits12);
424        assert_eq!(scratchpad.alarm_high(), 75);
425        assert_eq!(scratchpad.alarm_low(), -10);
426    }
427
428    #[test]
429    fn a_corrupted_scratchpad_fails_the_crc() {
430        let mut bytes = [0x91, 0x01, 75, 0xF6, 0x7F, 0xFF, 0x00, 0x10, 0x00];
431        bytes[8] = crc8(&bytes[..8]);
432        bytes[0] ^= 0x01; // flip a temperature bit after the CRC was computed
433        assert_eq!(Scratchpad::parse(&bytes), Err(SensorError::Crc));
434    }
435
436    #[test]
437    fn resolution_config_bytes_match_the_datasheet() {
438        assert_eq!(Resolution::Bits9.config_byte(), 0x1F);
439        assert_eq!(Resolution::Bits10.config_byte(), 0x3F);
440        assert_eq!(Resolution::Bits11.config_byte(), 0x5F);
441        assert_eq!(Resolution::Bits12.config_byte(), 0x7F);
442        for resolution in [
443            Resolution::Bits9,
444            Resolution::Bits10,
445            Resolution::Bits11,
446            Resolution::Bits12,
447        ] {
448            assert_eq!(
449                Resolution::from_config_byte(resolution.config_byte()),
450                resolution
451            );
452        }
453    }
454    #[test]
455    fn a_built_scratchpad_parses_back_to_what_it_was_built_from() {
456        let built = Scratchpad::new(
457            temperature_from_celsius(25.0625, Resolution::Bits12),
458            Resolution::Bits12,
459            75,
460            -10,
461        );
462        let parsed = Scratchpad::parse(&built.to_bytes()).expect("a built scratchpad is valid");
463        assert_eq!(parsed, built);
464        assert_eq!(parsed.raw_temperature(), 0x0191);
465        assert_eq!(parsed.temperature_micro_celsius(), 25_062_500);
466        assert_eq!(parsed.alarm_high(), 75);
467        assert_eq!(parsed.alarm_low(), -10);
468    }
469
470    #[test]
471    fn a_built_scratchpad_carries_the_datasheet_temperature_bytes() {
472        // The +25.0625 °C row of the datasheet's temperature/data table is 0x0191, sent
473        // least-significant byte first, and byte 4 selects 12-bit resolution.
474        let bytes = Scratchpad::new(0x0191, Resolution::Bits12, 75, -10).to_bytes();
475        assert_eq!(bytes[0], 0x91);
476        assert_eq!(bytes[1], 0x01);
477        assert_eq!(bytes[4], 0x7F);
478        assert_eq!(bytes[8], crc8(&bytes[..8]));
479    }
480
481    #[test]
482    fn a_temperature_truncates_to_the_step_the_resolution_resolves() {
483        // 12-bit resolves a sixteenth of a degree; the coarser settings read the bits
484        // below their step as zero, so the same temperature lands on a lower count.
485        assert_eq!(temperature_from_celsius(25.0625, Resolution::Bits12), 401);
486        assert_eq!(temperature_from_celsius(25.0625, Resolution::Bits11), 400);
487        assert_eq!(temperature_from_celsius(25.0625, Resolution::Bits10), 400);
488        assert_eq!(temperature_from_celsius(25.0625, Resolution::Bits9), 400);
489        assert_eq!(
490            temperature_from_micro_celsius(-10_062_500, Resolution::Bits12),
491            -161
492        );
493    }
494
495    #[test]
496    fn every_temperature_register_survives_a_round_trip() {
497        for raw in [-880i16, -161, -1, 0, 1, 401, 1250] {
498            let micro = temperature_to_micro_celsius(raw);
499            assert_eq!(
500                temperature_from_micro_celsius(micro, Resolution::Bits12),
501                raw
502            );
503        }
504    }
505}