Skip to main content

pamoja_sensors/ds18b20/
driver.rs

1//! The DS18B20 driven over a 1-Wire bus: address, convert, wait, read the scratchpad.
2
3use embedded_hal::delay::DelayNs;
4use pamoja_core::Sensor;
5use pamoja_hal::onewire::{OneWireBus, OneWireError, RomCode};
6
7use super::{command, Resolution, Scratchpad, EEPROM_WRITE_MICROS};
8use crate::error::DriverError;
9
10/// A DS18B20 on a 1-Wire bus, measuring on demand.
11///
12/// [`Ds18b20::new`] addresses the only thermometer on the bus with `SKIP ROM`;
13/// [`Ds18b20::addressed`] addresses one of several by its ROM code with `MATCH ROM`.
14/// [`init`](Ds18b20::init) writes the resolution and alarm thresholds to the
15/// scratchpad and reads it back to confirm the part answers.
16/// [`measure`](Ds18b20::measure) issues `CONVERT T`, waits the datasheet's maximum
17/// conversion time for the resolution in use, then reads the nine-byte scratchpad,
18/// whose CRC is checked before a temperature is taken from it. As a [`Sensor`] its
19/// reading is the [`Scratchpad`]; one channel is selected with [`Sensor::map`].
20///
21/// The driver assumes the part is powered from its VDD pin. A part on parasite
22/// power needs a strong pull-up during every conversion, which the bus underneath
23/// has to supply; [`powered_externally`](Ds18b20::powered_externally) reports which
24/// wiring the part has.
25///
26/// # Examples
27///
28/// A bit-banged bus over a scripted pin: the pin answers the presence sample low,
29/// then shifts out a scratchpad of a part at 25.0625 C.
30///
31/// ```
32/// use pamoja_core::Sensor;
33/// use pamoja_hal::digital::PinState::{High, Low};
34/// use pamoja_hal::onewire::BitBang;
35/// use pamoja_hal::script::{block_on, DelayLog, PinScript};
36/// use pamoja_sensors::ds18b20::{temperature_from_celsius, Ds18b20, Resolution, Scratchpad};
37///
38/// let raw = temperature_from_celsius(25.0625, Resolution::Bits12);
39/// let scratchpad = Scratchpad::new(raw, Resolution::Bits12, 75, -10).to_bytes();
40/// let mut levels = vec![Low, Low];
41/// for byte in scratchpad {
42///     levels.extend((0..8).map(|bit| if (byte >> bit) & 1 == 1 { High } else { Low }));
43/// }
44///
45/// let bus = BitBang::new(PinScript::new(levels), DelayLog::new());
46/// let mut probe = Ds18b20::new(bus, DelayLog::new());
47/// let mut celsius = probe.map(|scratchpad| scratchpad.temperature_celsius());
48/// assert_eq!(block_on(celsius.read())?, 25.0625);
49/// # Ok::<(), pamoja_core::Error>(())
50/// ```
51#[derive(Debug)]
52pub struct Ds18b20<B, D> {
53    bus: B,
54    delay: D,
55    rom: Option<RomCode>,
56    resolution: Resolution,
57    alarm_high: i8,
58    alarm_low: i8,
59}
60
61impl<B, D> Ds18b20<B, D> {
62    /// Wraps a bus carrying a single thermometer.
63    ///
64    /// # Arguments
65    ///
66    /// * `bus` - the 1-Wire bus.
67    /// * `delay` - the timer that paces the conversions.
68    ///
69    /// # Returns
70    ///
71    /// The driver, at 12-bit resolution with the alarm thresholds at the part's
72    /// limits.
73    pub fn new(bus: B, delay: D) -> Self {
74        Ds18b20 {
75            bus,
76            delay,
77            rom: None,
78            resolution: Resolution::Bits12,
79            alarm_high: 125,
80            alarm_low: -55,
81        }
82    }
83
84    /// Wraps a bus carrying several devices and addresses one by its ROM code.
85    ///
86    /// # Arguments
87    ///
88    /// * `bus` - the 1-Wire bus.
89    /// * `delay` - the timer that paces the conversions.
90    /// * `rom` - the thermometer's ROM code, as a
91    ///   [`Search`](pamoja_hal::onewire::Search) returns it.
92    ///
93    /// # Returns
94    ///
95    /// The driver.
96    pub fn addressed(bus: B, delay: D, rom: RomCode) -> Self {
97        let mut probe = Ds18b20::new(bus, delay);
98        probe.rom = Some(rom);
99        probe
100    }
101
102    /// Sets the conversion resolution, which also sets the conversion time.
103    ///
104    /// The setting is written to the part by [`init`](Ds18b20::init).
105    ///
106    /// # Arguments
107    ///
108    /// * `resolution` - 9 to 12 bits.
109    ///
110    /// # Returns
111    ///
112    /// The driver.
113    pub fn with_resolution(mut self, resolution: Resolution) -> Self {
114        self.resolution = resolution;
115        self
116    }
117
118    /// Sets the alarm thresholds the part compares each conversion against.
119    ///
120    /// The thresholds are written to the part by [`init`](Ds18b20::init) and matter
121    /// to an [alarm search](pamoja_hal::onewire::Search::alarms).
122    ///
123    /// # Arguments
124    ///
125    /// * `high` - the upper threshold in whole degrees Celsius.
126    /// * `low` - the lower threshold in whole degrees Celsius.
127    ///
128    /// # Returns
129    ///
130    /// The driver.
131    pub fn with_alarms(mut self, high: i8, low: i8) -> Self {
132        self.alarm_high = high;
133        self.alarm_low = low;
134        self
135    }
136
137    /// Returns the ROM code the driver addresses, or `None` for the only device.
138    pub fn rom(&self) -> Option<RomCode> {
139        self.rom
140    }
141
142    /// Returns the resolution the driver converts at.
143    pub fn resolution(&self) -> Resolution {
144        self.resolution
145    }
146
147    /// Gives back the bus and the delay.
148    ///
149    /// # Returns
150    ///
151    /// The bus and delay the driver was built from.
152    pub fn release(self) -> (B, D) {
153        (self.bus, self.delay)
154    }
155}
156
157impl<B: OneWireBus, D: DelayNs> Ds18b20<B, D> {
158    /// Writes the resolution and alarm thresholds and reads the scratchpad back.
159    ///
160    /// The sequence is the datasheet's `WRITE SCRATCHPAD` of the three configurable
161    /// bytes, then a `READ SCRATCHPAD` whose CRC and configuration byte confirm the
162    /// part took them. The settings live in the scratchpad until power is lost;
163    /// [`save_settings`](Ds18b20::save_settings) copies them to the EEPROM.
164    ///
165    /// # Errors
166    ///
167    /// Returns [`DriverError::Bus`] if no device answers or the line fails,
168    /// [`DriverError::Sensor`] if the scratchpad read back fails its CRC or does not
169    /// hold the resolution written.
170    pub fn init(&mut self) -> Result<(), DriverError<OneWireError<B::Error>>> {
171        self.select()?;
172        let scratchpad = [
173            command::WRITE_SCRATCHPAD,
174            self.alarm_high as u8,
175            self.alarm_low as u8,
176            self.resolution.config_byte(),
177        ];
178        self.bus
179            .write_bytes(&scratchpad)
180            .map_err(|error| DriverError::Bus(OneWireError::Pin(error)))?;
181        let readback = self.read_scratchpad()?;
182        if readback.resolution() != self.resolution {
183            return Err(DriverError::Sensor(crate::SensorError::Invalid));
184        }
185        Ok(())
186    }
187
188    /// Runs one conversion and returns the scratchpad.
189    ///
190    /// The sequence is the datasheet's: address the part, `CONVERT T`, wait the
191    /// maximum conversion time for the resolution, address the part again, and
192    /// `READ SCRATCHPAD`. The resolution the driver waits for is the one it holds,
193    /// so run [`init`](Ds18b20::init) first if the part's EEPROM setting differs.
194    ///
195    /// # Returns
196    ///
197    /// The CRC-checked scratchpad, with the temperature in it.
198    ///
199    /// # Errors
200    ///
201    /// Returns [`DriverError::Bus`] if no device answers or the line fails, and
202    /// [`DriverError::Sensor`] if the scratchpad fails its CRC.
203    pub fn measure(&mut self) -> Result<Scratchpad, DriverError<OneWireError<B::Error>>> {
204        self.select()?;
205        self.bus
206            .write_byte(command::CONVERT_T)
207            .map_err(|error| DriverError::Bus(OneWireError::Pin(error)))?;
208        self.delay.delay_us(self.resolution.max_conversion_micros());
209        self.read_scratchpad()
210    }
211
212    /// Reads the scratchpad without converting, for the last result or the settings.
213    ///
214    /// # Returns
215    ///
216    /// The CRC-checked scratchpad.
217    ///
218    /// # Errors
219    ///
220    /// Returns [`DriverError::Bus`] if no device answers or the line fails, and
221    /// [`DriverError::Sensor`] if the scratchpad fails its CRC.
222    pub fn read_scratchpad(&mut self) -> Result<Scratchpad, DriverError<OneWireError<B::Error>>> {
223        self.select()?;
224        self.bus
225            .write_byte(command::READ_SCRATCHPAD)
226            .map_err(|error| DriverError::Bus(OneWireError::Pin(error)))?;
227        let mut bytes = [0u8; 9];
228        self.bus
229            .read_bytes(&mut bytes)
230            .map_err(|error| DriverError::Bus(OneWireError::Pin(error)))?;
231        Ok(Scratchpad::parse(&bytes)?)
232    }
233
234    /// Copies the scratchpad settings to the EEPROM, so they survive power loss.
235    ///
236    /// # Errors
237    ///
238    /// Returns [`DriverError::Bus`] if no device answers or the line fails.
239    pub fn save_settings(&mut self) -> Result<(), DriverError<OneWireError<B::Error>>> {
240        self.select()?;
241        self.bus
242            .write_byte(command::COPY_SCRATCHPAD)
243            .map_err(|error| DriverError::Bus(OneWireError::Pin(error)))?;
244        self.delay.delay_us(EEPROM_WRITE_MICROS);
245        Ok(())
246    }
247
248    /// Asks the part whether it is powered from VDD or parasitically from the line.
249    ///
250    /// # Returns
251    ///
252    /// `true` for external power, `false` for parasite power, which needs a strong
253    /// pull-up during conversions.
254    ///
255    /// # Errors
256    ///
257    /// Returns [`DriverError::Bus`] if no device answers or the line fails.
258    pub fn powered_externally(&mut self) -> Result<bool, DriverError<OneWireError<B::Error>>> {
259        self.select()?;
260        self.bus
261            .write_byte(command::READ_POWER_SUPPLY)
262            .map_err(|error| DriverError::Bus(OneWireError::Pin(error)))?;
263        self.bus
264            .read_bit()
265            .map_err(|error| DriverError::Bus(OneWireError::Pin(error)))
266    }
267
268    fn select(&mut self) -> Result<(), DriverError<OneWireError<B::Error>>> {
269        match self.rom {
270            Some(rom) => self.bus.match_rom(&rom),
271            None => self.bus.skip_rom(),
272        }
273        .map_err(DriverError::Bus)
274    }
275}
276
277impl<B, D> Sensor for Ds18b20<B, D>
278where
279    B: OneWireBus + Send,
280    B::Error: core::fmt::Debug,
281    D: DelayNs + Send,
282{
283    type Reading = Scratchpad;
284
285    async fn read(&mut self) -> pamoja_core::Result<Scratchpad> {
286        self.measure().map_err(pamoja_core::Error::from)
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use crate::ds18b20::temperature_from_celsius;
294    use crate::SensorError;
295    use alloc::vec;
296    use alloc::vec::Vec;
297    use core::convert::Infallible;
298    use pamoja_hal::digital::PinState::{High, Low};
299    use pamoja_hal::onewire::{command as rom_command, BitBang};
300    use pamoja_hal::script::{block_on, DelayLog, PinScript};
301
302    /// A thermometer on a simulated bus: it records every byte written after a reset
303    /// and answers the function commands the way the datasheet describes.
304    struct Simulated {
305        rom: RomCode,
306        present: bool,
307        scratchpad: [u8; 9],
308        written: Vec<u8>,
309        reading: Vec<bool>,
310        external_power: bool,
311    }
312
313    impl Simulated {
314        fn new(celsius: f32) -> Simulated {
315            let raw = temperature_from_celsius(celsius, Resolution::Bits12);
316            Simulated {
317                rom: RomCode::new(0x28, 0x0000_05E2_FDC3).unwrap(),
318                present: true,
319                scratchpad: Scratchpad::new(raw, Resolution::Bits12, 125, -55).to_bytes(),
320                written: Vec::new(),
321                reading: Vec::new(),
322                external_power: true,
323            }
324        }
325
326        fn queue_bytes(&mut self, bytes: &[u8]) {
327            for &byte in bytes {
328                self.reading
329                    .extend((0..8).map(|bit| (byte >> bit) & 1 == 1));
330            }
331        }
332
333        fn act(&mut self) {
334            let addressed = match self.written.first() {
335                Some(&rom_command::SKIP_ROM) => 1,
336                Some(&rom_command::MATCH_ROM)
337                    if self.written.len() >= 9 && self.written[1..9] == self.rom.bytes() =>
338                {
339                    9
340                }
341                _ => return,
342            };
343            match self.written.get(addressed) {
344                Some(&command::READ_SCRATCHPAD) => {
345                    let scratchpad = self.scratchpad;
346                    self.queue_bytes(&scratchpad);
347                }
348                Some(&command::WRITE_SCRATCHPAD) if self.written.len() == addressed + 4 => {
349                    self.scratchpad[2] = self.written[addressed + 1];
350                    self.scratchpad[3] = self.written[addressed + 2];
351                    self.scratchpad[4] = self.written[addressed + 3];
352                    self.scratchpad[8] = crate::ds18b20::crc8(&self.scratchpad[..8]);
353                }
354                Some(&command::READ_POWER_SUPPLY) => {
355                    let external = self.external_power;
356                    self.reading.push(external);
357                }
358                _ => {}
359            }
360        }
361    }
362
363    impl OneWireBus for Simulated {
364        type Error = Infallible;
365
366        fn reset(&mut self) -> Result<bool, Infallible> {
367            self.written.clear();
368            self.reading.clear();
369            Ok(self.present)
370        }
371
372        fn write_bit(&mut self, _bit: bool) -> Result<(), Infallible> {
373            unreachable!("bytes are written whole")
374        }
375
376        fn read_bit(&mut self) -> Result<bool, Infallible> {
377            Ok(if self.reading.is_empty() {
378                true
379            } else {
380                self.reading.remove(0)
381            })
382        }
383
384        fn write_byte(&mut self, byte: u8) -> Result<(), Infallible> {
385            self.written.push(byte);
386            self.act();
387            Ok(())
388        }
389    }
390
391    #[test]
392    fn measure_converts_then_reads_a_crc_checked_scratchpad() {
393        let mut probe = Ds18b20::new(Simulated::new(25.0625), DelayLog::new());
394        let scratchpad = probe.measure().unwrap();
395        assert_eq!(scratchpad.temperature_celsius(), 25.0625);
396        assert_eq!(scratchpad.resolution(), Resolution::Bits12);
397        let (bus, delay) = probe.release();
398        assert_eq!(
399            bus.written,
400            [rom_command::SKIP_ROM, command::READ_SCRATCHPAD]
401        );
402        assert_eq!(delay.waits_ns(), [750_000_000]);
403    }
404
405    #[test]
406    fn init_writes_the_settings_and_confirms_them_from_the_readback() {
407        let mut probe = Ds18b20::new(Simulated::new(20.0), DelayLog::new())
408            .with_resolution(Resolution::Bits9)
409            .with_alarms(30, -5);
410        probe.init().unwrap();
411        let scratchpad = probe.read_scratchpad().unwrap();
412        assert_eq!(scratchpad.resolution(), Resolution::Bits9);
413        assert_eq!(scratchpad.alarm_high(), 30);
414        assert_eq!(scratchpad.alarm_low(), -5);
415        probe.measure().unwrap();
416        let (_, delay) = probe.release();
417        assert_eq!(
418            delay.waits_ns(),
419            [93_750_000],
420            "a 9-bit conversion waits 93.75 ms"
421        );
422    }
423
424    #[test]
425    fn an_addressed_probe_matches_its_rom_and_a_stranger_is_ignored() {
426        let bus = Simulated::new(18.5);
427        let rom = bus.rom;
428        let mut probe = Ds18b20::addressed(bus, DelayLog::new(), rom);
429        assert_eq!(probe.rom(), Some(rom));
430        assert_eq!(probe.measure().unwrap().temperature_celsius(), 18.5);
431
432        let stranger = RomCode::new(0x28, 0x0000_0000_0001).unwrap();
433        let mut wrong = Ds18b20::addressed(Simulated::new(18.5), DelayLog::new(), stranger);
434        assert_eq!(wrong.measure(), Err(DriverError::Sensor(SensorError::Crc)));
435    }
436
437    #[test]
438    fn an_empty_bus_and_a_corrupted_scratchpad_are_reported_apart() {
439        let mut absent = Simulated::new(0.0);
440        absent.present = false;
441        let mut probe = Ds18b20::new(absent, DelayLog::new());
442        assert_eq!(
443            probe.measure(),
444            Err(DriverError::Bus(OneWireError::NoDevice))
445        );
446
447        let mut noisy = Simulated::new(0.0);
448        noisy.scratchpad[1] ^= 0x10;
449        let mut probe = Ds18b20::new(noisy, DelayLog::new());
450        assert_eq!(probe.measure(), Err(DriverError::Sensor(SensorError::Crc)));
451    }
452
453    #[test]
454    fn power_supply_and_eeprom_copy_follow_their_commands() {
455        let mut parasite = Simulated::new(0.0);
456        parasite.external_power = false;
457        let mut probe = Ds18b20::new(parasite, DelayLog::new());
458        assert!(!probe.powered_externally().unwrap());
459        probe.save_settings().unwrap();
460        let (bus, delay) = probe.release();
461        assert_eq!(
462            bus.written,
463            [rom_command::SKIP_ROM, command::COPY_SCRATCHPAD]
464        );
465        assert_eq!(delay.waits_ns(), [EEPROM_WRITE_MICROS * 1_000]);
466    }
467
468    #[test]
469    fn over_a_bit_banged_pin_the_wire_carries_the_datasheet_sequence() {
470        let raw = temperature_from_celsius(-10.125, Resolution::Bits12);
471        let bytes = Scratchpad::new(raw, Resolution::Bits12, 125, -55).to_bytes();
472        let mut levels = vec![Low, Low];
473        for byte in bytes {
474            levels.extend((0..8).map(|bit| if (byte >> bit) & 1 == 1 { High } else { Low }));
475        }
476        let bus = BitBang::new(PinScript::new(levels), DelayLog::new());
477        let mut probe = Ds18b20::new(bus, DelayLog::new());
478        let scratchpad = probe.measure().unwrap();
479        assert_eq!(scratchpad.temperature_celsius(), -10.125);
480        let (bus, _) = probe.release();
481        let (pin, _) = bus.into_parts();
482        assert_eq!(
483            pin.driven().len(),
484            2 + 16 + 16 + 2 + 16 + 16 + 144,
485            "two resets, four command bytes, and 72 read slots, two edges each"
486        );
487        assert_eq!(pin.remaining(), 0);
488    }
489
490    #[test]
491    fn as_a_sensor_it_reads_and_maps_to_celsius() {
492        let mut celsius = Ds18b20::new(Simulated::new(85.0), DelayLog::new())
493            .map(|scratchpad| scratchpad.temperature_celsius());
494        assert_eq!(block_on(celsius.read()).unwrap(), 85.0);
495    }
496}