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}