Skip to main content

m5stack_core/driver/
ds18b20.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! DS18B20 1-Wire digital temperature sensor driver via RMT peripheral.
3//!
4//! Broadcasts a single Convert T to all sensors, waits for the conversion to
5//! complete, then uses the ROM Search algorithm to enumerate every sensor and
6//! reads each device's full 9-byte scratchpad (validating its CRC-8). The
7//! temperature (bytes 0..1) is returned as signed 12.4 fixed-point (0.0625 °C
8//! resolution).
9//!
10//! 1-Wire commands used:
11//!   0xCC  Skip ROM — address all devices
12//!   0x44  Convert T — start temperature conversion (750 ms max at 12-bit)
13//!   0x55  Match ROM — address specific device
14//!   0xBE  Read Scratchpad — returns 9 bytes (TEMP_LSB, TEMP_MSB, …, CRC)
15//!
16//! RMT channel assignment (chip-specific):
17//!   ESP32 (fire27):   TX=channel0, RX=channel2
18//!   ESP32-S3 (cores3): TX=channel0, RX=channel4
19//!
20//! Wiring (M5Stack Grove): the 1-Wire data line goes on **Port B (black)** — the
21//! signal pin that follows VCC, i.e. **G26** on Fire27 and **G9** on CoreS3. An
22//! external 4.7 kΩ pull-up from data to 3V3 is required. (Port A / red is the
23//! I2C port and is not used here.)
24//!
25//! Datasheet: <https://www.analog.com/media/en/technical-documentation/data-sheets/DS18B20.pdf>
26use crate::driver::onewire::{Address, OneWire, Search, SearchError, crc8};
27use embassy_time::{Duration, Instant, Timer};
28use esp_hal::{gpio::AnyPin, peripherals::RMT, rmt::Rmt, time::Rate};
29use heapless::Vec;
30use thiserror_no_std::Error;
31
32#[derive(Debug, Error)]
33pub enum Error {
34    #[error("failed to configure the RMT peripheral: {0:?}")]
35    RmtConfigError(#[from] esp_hal::rmt::ConfigError),
36    #[error("temperature conversion did not complete within 750 ms")]
37    ConversionTimeout,
38    #[error("DS18B20 scratchpad failed its CRC-8 check")]
39    ScratchpadCrcMismatch,
40    #[error("1-Wire bus error: {0}")]
41    OneWireError(#[from] crate::driver::onewire::Error),
42    #[error("more sensors connected than the driver supports")]
43    TooManySensorsConnected,
44}
45
46pub struct Ds18b20Driver {
47    ow: OneWire<'static>,
48    /// ROM addresses discovered by the last scan, reused across reads.
49    addresses: Vec<Address, { Self::MAX_SENSORS }>,
50}
51
52impl Ds18b20Driver {
53    const MAX_SENSORS: usize = 16;
54    pub fn new(rmt: RMT<'static>, pin: AnyPin<'static>) -> Result<Self, Error> {
55        let rmt = Rmt::new(rmt, Rate::from_mhz(80_u32))?.into_async();
56        // ESP32: TX channel 0, RX channel 2
57        // ESP32-S3: TX channels 0-3, RX channels 4-7
58        #[cfg(feature = "fire27")]
59        let ow = OneWire::new(rmt.channel0, rmt.channel2, pin)?;
60        #[cfg(feature = "cores3")]
61        let ow = OneWire::new(rmt.channel0, rmt.channel4, pin)?;
62        Ok(Ds18b20Driver {
63            ow,
64            addresses: Vec::new(),
65        })
66    }
67
68    /// Enumerate the bus and cache the discovered ROM addresses.
69    ///
70    /// Runs the full ROM Search once. [`read_all_temperatures`] calls this
71    /// lazily on first use; call it explicitly to refresh the set. As no sensor
72    /// hot-plug is assumed at runtime, the cached addresses are reused across
73    /// reads — avoiding a 64-step ROM search on every cycle (which otherwise
74    /// dominates the RMT round-trips and task wakeups, the main cost here since
75    /// the RMT does the bit timing in hardware).
76    pub async fn rescan(&mut self) -> Result<(), Error> {
77        self.addresses.clear();
78        let mut search = Search::new();
79        loop {
80            match search.next(&mut self.ow).await {
81                Ok(address) => {
82                    debug!("found device {}", address);
83                    if self.addresses.push(address).is_err() {
84                        return Err(Error::TooManySensorsConnected);
85                    }
86                }
87                // A corrupt ROM CRC affects only this one enumeration step; the
88                // search state has advanced, so keep scanning the rest.
89                Err(SearchError::CrcMismatch) => warn!("ROM CRC mismatch, skipping device"),
90                Err(SearchError::SearchComplete) | Err(SearchError::NoDevicesPresent) => break,
91                Err(SearchError::BusError(e)) => return Err(e.into()),
92            }
93        }
94        Ok(())
95    }
96
97    pub async fn read_all_temperatures(
98        &mut self,
99    ) -> Result<impl Iterator<Item = (Address, f32)>, Error> {
100        if self.addresses.is_empty() {
101            self.rescan().await?;
102        }
103
104        trace!("Broadcasting a measure temperature command to all attached sensors");
105        self.ow.reset().await?;
106        for cmd in [0xCC, 0x44] {
107            self.ow.send_byte(cmd).await?;
108        }
109
110        // A read immediately after Convert T returns the *previous* conversion
111        // (the very first read after power-up is the 85 °C reset value). Wait
112        // for completion before reading so callers get the fresh measurement.
113        self.wait_conversion_complete().await?;
114
115        // Read each cached sensor by ROM address. A sensor that drops out is
116        // warned and omitted from this cycle's results; since no hot-plug is
117        // assumed, its address stays cached so it is retried on the next read.
118        let mut temperatures = Vec::<(Address, f32), { Self::MAX_SENSORS }>::new();
119        for i in 0..self.addresses.len() {
120            let address = self.addresses[i];
121            match self.read_scratchpad(address).await {
122                Ok(temperature_celsius) => {
123                    // Capacity is guaranteed: addresses.len() <= MAX_SENSORS.
124                    let _ = temperatures.push((address, temperature_celsius));
125                    debug!("sensor {}: {}°C", address, temperature_celsius);
126                }
127                Err(e) => warn!("sensor {} lost: {:?}", address, e),
128            }
129        }
130        Ok(temperatures.into_iter())
131    }
132
133    /// Poll the bus until the in-progress temperature conversion completes.
134    ///
135    /// An externally-powered DS18B20 holds the bus low while converting and
136    /// releases it (a read slot returns 1) when finished. Returns
137    /// [`Error::ConversionTimeout`] if the 12-bit worst case (750 ms, plus
138    /// margin) elapses without completion.
139    async fn wait_conversion_complete(&mut self) -> Result<(), Error> {
140        let start = Instant::now();
141        loop {
142            let mut bit = [false; 1];
143            self.ow.exchange_bits(&[true], &mut bit).await?;
144            if bit[0] {
145                return Ok(());
146            }
147            if start.elapsed() > Duration::from_millis(800) {
148                return Err(Error::ConversionTimeout);
149            }
150            // Conversion is signalled by the read-slot result above, not by this
151            // delay; it only paces the poll so we don't hammer the bus.
152            Timer::after(Duration::from_millis(10)).await;
153        }
154    }
155
156    /// Address a single device, read its full 9-byte scratchpad, validate the
157    /// CRC, and return the temperature in °C.
158    async fn read_scratchpad(&mut self, address: Address) -> Result<f32, Error> {
159        self.ow.reset().await?;
160        self.ow.send_byte(0x55).await?; // Match ROM
161        self.ow.send_address(address).await?;
162        self.ow.send_byte(0xBE).await?; // Read Scratchpad
163        let mut scratch = [0u8; 9];
164        for byte in scratch.iter_mut() {
165            // Propagates as `OneWireError`, preserving the underlying bus error.
166            *byte = self.ow.exchange_byte(0xFF).await?;
167        }
168        // Byte 8 is a CRC-8 over bytes 0..8; reject a corrupt read.
169        if crc8(&scratch[..8]) != scratch[8] {
170            return Err(Error::ScratchpadCrcMismatch);
171        }
172        Ok(fixed::types::I12F4::from_le_bytes([scratch[0], scratch[1]]).into())
173    }
174}