tosca_drivers/
dht22.rs

1//! # DHT22 Driver
2//!
3//! This module provides an architecture-agnostic driver for the `DHT22`
4//! temperature and humidity sensor.
5//! The driver is synchronous to meet the strict timing requirements of the
6//! sensor's single-wire protocol.
7//! The initial start signal uses a brief asynchronous wait to initiate
8//! communication without blocking the executor, while all subsequent
9//! timing-critical operations use precise blocking delays to ensure accurate
10//! measurements.
11//!
12//! The `DHT22` sensor provides the following measurements:
13//! - **Humidity**: Relative humidity as a percentage (% RH)
14//! - **Temperature**: Temperature in degrees Celsius (°C)
15//!
16//! For detailed specifications, refer to the
17//! [datasheet](https://www.alldatasheet.com/datasheet-pdf/pdf/1132459/ETC2/DHT22.html)
18//! and the description of the proprietary
19//! [communication protocol](https://www.ocfreaks.com/basics-interfacing-dht11-dht22-humidity-temperature-sensor-mcu/).
20
21use core::result::Result::{self, Err, Ok};
22
23use embedded_hal::delay::DelayNs as SyncDelay;
24use embedded_hal::digital::{InputPin, OutputPin, PinState};
25
26use embedded_hal_async::delay::DelayNs as AsyncDelay;
27
28// Protocol-specific timing constants.
29const START_SIGNAL_LOW_MS: u32 = 18; // MCU pulls line low for at least 18 ms to initiate communication.
30const START_SIGNAL_HIGH_US: u32 = 40; // Then releases the line (high) for ~20–40 µs.
31const BIT_SAMPLE_DELAY_US: u32 = 35; // Time after which to sample the data bit.
32const POLL_DELAY_US: u32 = 1; // Delay between pin state polls when waiting for edges.
33const MAX_ATTEMPTS: usize = 100; // Maximum polling iterations before timeout.
34
35/// A single humidity and temperature measurement.
36#[derive(Debug, Clone, Copy)]
37pub struct Measurement {
38    /// Relative humidity as a percentage (% RH).
39    pub humidity: f32,
40    /// Temperature in degrees Celsius (°C).
41    pub temperature: f32,
42}
43
44/// Errors that may occur when interacting with the `DHT22` sensor.
45#[derive(Debug)]
46pub enum Dht22Error<E> {
47    /// GPIO pin errors.
48    Pin(E),
49    /// Data checksum mismatch.
50    ChecksumMismatch,
51    /// Timeout waiting for sensor response.
52    Timeout,
53}
54
55impl<E> From<E> for Dht22Error<E> {
56    fn from(e: E) -> Self {
57        Dht22Error::Pin(e)
58    }
59}
60
61/// The `DHT22` driver.
62pub struct Dht22<P, D>
63where
64    P: InputPin + OutputPin,
65    D: SyncDelay + AsyncDelay,
66{
67    pin: P,
68    delay: D,
69}
70
71// Raw sensor data: (humidity high, humidity low, temperature high, temperature low, checksum).
72type RawData = (u8, u8, u8, u8, u8);
73
74impl<P, D> Dht22<P, D>
75where
76    P: InputPin + OutputPin,
77    D: SyncDelay + AsyncDelay,
78{
79    /// Creates a [`Dht22`] driver for the given pin and delay provider.
80    #[must_use]
81    pub fn new(pin: P, delay: D) -> Self {
82        Self { pin, delay }
83    }
84
85    /// Reads a single humidity and temperature measurement.
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if:
90    /// - Reading from the pin fails
91    /// - The sensor does not respond within the expected timing window
92    /// - The received data fails checksum validation
93    pub fn read(&mut self) -> Result<Measurement, Dht22Error<P::Error>> {
94        // Initiate communication by sending the start signal to the sensor.
95        self.send_start_signal()?;
96
97        // Wait for the sensor’s response (low → high handshake).
98        self.wait_for_sensor_response()?;
99
100        // Read 5 bytes: humidity high + low, temperature high + low, and checksum.
101        let (hh, hl, th, tl, checksum) = self.read_raw_data()?;
102
103        // Validate that the transmitted checksum matches the calculated one.
104        Self::validate_checksum(hh, hl, th, tl, checksum)?;
105
106        Ok(Measurement {
107            humidity: Self::decode_humidity(hh, hl),
108            temperature: Self::decode_temperature(th, tl),
109        })
110    }
111
112    fn send_start_signal(&mut self) -> Result<(), Dht22Error<P::Error>> {
113        // Pull the line low for at least 18 ms to signal the sensor.
114        self.pin.set_low()?;
115        SyncDelay::delay_ms(&mut self.delay, START_SIGNAL_LOW_MS);
116
117        // Release the line high briefly before the sensor takes control of it.
118        self.pin.set_high()?;
119        SyncDelay::delay_us(&mut self.delay, START_SIGNAL_HIGH_US);
120
121        Ok(())
122    }
123
124    fn wait_for_sensor_response(&mut self) -> Result<(), Dht22Error<P::Error>> {
125        // The sensor pulls the line low and then high to acknowledge.
126        self.wait_until_state(PinState::Low)?;
127        self.wait_until_state(PinState::High)?;
128
129        Ok(())
130    }
131
132    fn read_raw_data(&mut self) -> Result<RawData, Dht22Error<P::Error>> {
133        // Sequentially read 5 bytes from the sensor.
134        Ok((
135            self.read_byte()?,
136            self.read_byte()?,
137            self.read_byte()?,
138            self.read_byte()?,
139            self.read_byte()?,
140        ))
141    }
142
143    #[inline]
144    fn validate_checksum(
145        hh: u8,
146        hl: u8,
147        th: u8,
148        tl: u8,
149        checksum: u8,
150    ) -> Result<(), Dht22Error<P::Error>> {
151        // The checksum is the low 8 bits of the sum of the first four bytes.
152        let sum = hh.wrapping_add(hl).wrapping_add(th).wrapping_add(tl);
153
154        if sum == checksum {
155            Ok(())
156        } else {
157            Err(Dht22Error::ChecksumMismatch)
158        }
159    }
160
161    #[inline]
162    fn decode_humidity(high: u8, low: u8) -> f32 {
163        // Combine two bytes into a 16-bit integer and divide by 10 (sensor sends humidity * 10).
164        f32::from((u16::from(high) << 8) | u16::from(low)) / 10.0
165    }
166
167    #[inline]
168    fn decode_temperature(high: u8, low: u8) -> f32 {
169        // The 16-bit temperature value has its sign bit at bit 15 (high byte’s MSB).
170        let raw = (u16::from(high & 0x7F) << 8) | u16::from(low);
171        let mut t = f32::from(raw) / 10.0;
172
173        // If the sign bit is set, temperature is negative.
174        if high & 0x80 != 0 {
175            t = -t;
176        }
177
178        t
179    }
180
181    fn wait_until_state(&mut self, state: PinState) -> Result<(), Dht22Error<P::Error>> {
182        // Poll the pin until it matches the desired state or timeout occurs.
183        for _ in 0..MAX_ATTEMPTS {
184            let reached = match state {
185                PinState::High => self.pin.is_high()?,
186                PinState::Low => self.pin.is_low()?,
187            };
188            if reached {
189                return Ok(());
190            }
191            SyncDelay::delay_us(&mut self.delay, POLL_DELAY_US);
192        }
193
194        Err(Dht22Error::Timeout)
195    }
196
197    fn read_byte(&mut self) -> Result<u8, Dht22Error<P::Error>> {
198        let mut byte = 0;
199
200        // Each bit transmission consists of a low pulse followed by a high pulse.
201        // The duration of the high pulse determines whether the bit is 0 or 1.
202        for i in 0..8 {
203            self.wait_until_state(PinState::Low)?; // Wait for the start of bit transmission.
204            self.wait_until_state(PinState::High)?; // Wait for the high phase.
205
206            // Sample after ~30 µs to determine bit value.
207            SyncDelay::delay_us(&mut self.delay, BIT_SAMPLE_DELAY_US);
208
209            // If the line is still high, the bit is 1; otherwise, it's 0.
210            if self.pin.is_high()? {
211                byte |= 1 << (7 - i); // Bits are transmitted MSB first.
212            }
213        }
214
215        Ok(byte)
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    extern crate std;
224    use std::vec;
225
226    use embedded_hal_mock::eh1::delay::NoopDelay;
227    use embedded_hal_mock::eh1::digital::{Mock as PinMock, State, Transaction as PinTransaction};
228
229    #[test]
230    fn test_send_start_signal() {
231        let expectations = [
232            PinTransaction::set(State::Low),
233            PinTransaction::set(State::High),
234        ];
235
236        let pin = PinMock::new(&expectations);
237        let delay = NoopDelay::new();
238        let mut dht22 = Dht22::new(pin, delay);
239
240        dht22.send_start_signal().unwrap();
241
242        dht22.pin.done();
243    }
244
245    #[test]
246    fn test_wait_for_sensor_response_success() {
247        let expectations = [
248            PinTransaction::get(State::Low),
249            PinTransaction::get(State::High),
250        ];
251
252        let pin = PinMock::new(&expectations);
253        let delay = NoopDelay::new();
254        let mut dht22 = Dht22::new(pin, delay);
255
256        dht22.wait_for_sensor_response().unwrap();
257
258        dht22.pin.done();
259    }
260
261    #[test]
262    fn test_wait_until_state_timeout() {
263        // Simulate all MAX_ATTEMPTS calls without ever reaching the desired state.
264        let expectations = vec![PinTransaction::get(State::High); MAX_ATTEMPTS];
265
266        let pin = PinMock::new(&expectations);
267        let delay = NoopDelay::new();
268        let mut dht22 = Dht22::new(pin, delay);
269
270        let result = dht22.wait_until_state(PinState::Low);
271        assert!(matches!(result, Err(Dht22Error::Timeout)));
272
273        dht22.pin.done();
274    }
275
276    #[test]
277    fn test_read_byte_all_zeros() {
278        let mut expectations = vec![];
279
280        // 8 bits: for each bit, wait for line low (start of bit), then high (bit signal), then sample line to determine 0.
281        for _ in 0..8 {
282            expectations.push(PinTransaction::get(State::Low)); // Falling edge.
283            expectations.push(PinTransaction::get(State::High)); // Rising edge.
284            expectations.push(PinTransaction::get(State::Low)); // Sampling: line low → bit 0.
285        }
286
287        let pin = PinMock::new(&expectations);
288        let delay = NoopDelay::new();
289        let mut dht22 = Dht22::new(pin, delay);
290
291        let byte = dht22.read_byte().unwrap();
292        assert_eq!(byte, 0x00);
293
294        dht22.pin.done();
295    }
296
297    #[test]
298    fn test_read_byte_all_ones() {
299        let mut expectations = vec![];
300
301        // 8 bits: for each bit, wait for line low (start of bit), then high (bit signal), then sample line to determine 1.
302        for _ in 0..8 {
303            expectations.push(PinTransaction::get(State::Low)); // Falling edge.
304            expectations.push(PinTransaction::get(State::High)); // Rising edge.
305            expectations.push(PinTransaction::get(State::High)); // Sampling: line high → bit 1.
306        }
307
308        let pin = PinMock::new(&expectations);
309        let delay = NoopDelay::new();
310        let mut dht22 = Dht22::new(pin, delay);
311
312        let byte = dht22.read_byte().unwrap();
313        assert_eq!(byte, 0xFF);
314
315        dht22.pin.done();
316    }
317
318    #[test]
319    fn test_decode_humidity_temperature() {
320        let humidity = Dht22::<PinMock, NoopDelay>::decode_humidity(0x02, 0x58); // 600 → 60.0%.
321        let temperature = Dht22::<PinMock, NoopDelay>::decode_temperature(0x00, 0xFA); // 250 → 25.0°C.
322        let temperature_neg = Dht22::<PinMock, NoopDelay>::decode_temperature(0x80, 0xFA); // Negative: -25.0°C.
323
324        assert!((humidity - 60.0).abs() < f32::EPSILON);
325        assert!((temperature - 25.0).abs() < f32::EPSILON);
326        assert!((temperature_neg + 25.0).abs() < f32::EPSILON);
327    }
328
329    #[test]
330    fn test_validate_checksum() {
331        let result_ok = Dht22::<PinMock, NoopDelay>::validate_checksum(1, 2, 3, 4, 10);
332        assert!(result_ok.is_ok());
333
334        let result_err = Dht22::<PinMock, NoopDelay>::validate_checksum(1, 2, 3, 4, 9);
335        assert!(matches!(result_err, Err(Dht22Error::ChecksumMismatch)));
336    }
337}