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}