Skip to main content

lh_dht22_rp/
lib.rs

1//! DHT22 humidity and temperature sensor driver for the RP2040 microcontroller.
2//! Based on rp2040_hal and embedded_hal
3//!
4//! Communicates though a single data wire which requires special pin handling
5//! for bi-directional communication.
6//!
7//! Reference:
8//! <https://cdn-shop.adafruit.com/datasheets/Digital+humidity+and+temperature+sensor+AM2302.pdf>
9//!
10//!
11//! Example:
12//!
13//! ```rust
14//! 
15//! use lh_dht22_rp as dht22;
16//! ...
17//!
18//! // Hal Boilerplate ...
19//! ...
20//!
21//! let dht_pin = pins.gpio1;
22//! let mut dht = dht22::DHT22::new(dht_pin, timer);
23//!
24//! match dht.read() {
25//!     Ok((humidity, temperature)) => {
26//!            println!("Humidity   : {:.1} %RH", humidity);
27//!            println!("Temperature: {:.1} C", temperature); },
28//!     Err(e) => println!("Err: {e}"),
29//!     }
30//! ```
31
32#![no_std]
33
34use rp2040_hal::gpio;
35use rp2040_hal::timer::{Instant, Timer};
36
37use embedded_hal::digital::{InputPin, OutputPin};
38use embedded_hal_0_2::blocking::delay::DelayUs;
39
40// —————————————————————————————————————————————————————————————————————————————————————————————————
41//                                             Globals
42// —————————————————————————————————————————————————————————————————————————————————————————————————
43
44const TIMEOUT_MS: u64 = 2 * 1000;
45const INIT_STATE_CHECK_DELAY_US: i32 = 100;
46const START_SIGNAL_US: u32 = 5_000;
47const SETUP_TIME_US: u32 = 20;
48const HIGH: u8 = 1;
49const LOW: u8 = 0;
50
51type Output = gpio::Pin<gpio::DynPinId, gpio::FunctionSio<gpio::SioOutput>, gpio::PullUp>;
52type Input = gpio::Pin<gpio::DynPinId, gpio::FunctionSio<gpio::SioInput>, gpio::PullNone>;
53
54pub type Result<T> = core::result::Result<T, DhtError>;
55
56// —————————————————————————————————————————————————————————————————————————————————————————————————
57//                                              DHT22
58// —————————————————————————————————————————————————————————————————————————————————————————————————
59
60pub struct DHT22 {
61    pin:        Output,
62    timer:      Timer,
63    start_time: Instant,
64}
65
66impl DHT22 {
67    /// Creates a new DHT22 sensor instance.
68    /// Requires the Pin connected to the DHT22 Data line, and a copy of the mcu timer.
69    pub fn new(pin: impl gpio::AnyPin, timer: Timer) -> Self {
70        let mut pin = pin.into_output();
71        pin.set_high();
72
73        let start_time = timer.get_counter();
74
75        Self { pin, timer, start_time }
76    }
77
78    #[inline]
79    /// Internal check for time out
80    fn not_timed_out(&self) -> Result<()> {
81        let elapsed = self
82            .timer
83            .get_counter()
84            .checked_duration_since(self.start_time)
85            .unwrap()
86            .to_millis();
87
88        if elapsed > TIMEOUT_MS {
89            return Err(DhtError::Timeout);
90        }
91        Ok(())
92    }
93
94    #[inline]
95    /// Waits until the desired state is read. Errors on timeout
96    fn wait_for_state(&mut self, state: u8, pin: &mut Input) -> Result<()> {
97        loop {
98            if get_input_state(pin) == state {
99                return Ok(());
100            }
101
102            if self.not_timed_out().is_err() {
103                return Err(DhtError::Timeout);
104            }
105        }
106    }
107
108    /// Reads the data from the sensor.
109    /// Returns Ok((humidity, temperature)) or Err(DhtError)
110    pub fn read(&mut self) -> Result<(f32, f32)> {
111        const PACKET_SIZE: usize = 40;
112        //
113        self.start_time = self.timer.get_counter();
114
115        // DTH22 sends a 16b + 16b + 8b package
116        let mut buffer = [0u8; PACKET_SIZE / 8];
117
118        // Requesting Data
119        let mut pin = self.pin.into_output();
120        pin.set_low();
121        self.timer.delay_us(START_SIGNAL_US);
122        pin.set_high();
123        self.timer.delay_us(SETUP_TIME_US);
124
125        // Switching pin into Input type
126        let mut pin = pin.into_input();
127
128        // Critical Section Interrupt Free - for time sensitive ops
129        let transaction_result = critical_section::with(|_cs| {
130            // Receiving Prelude - Expecting the pin to be HIGH at this time
131            self.timer.delay_us(INIT_STATE_CHECK_DELAY_US); // This may vary so it could be improved
132            if get_input_state(&mut pin) == LOW {
133                return Err(DhtError::Communication);
134            }
135
136            // Waiting for data transmission to start
137            self.wait_for_state(LOW, &mut pin)?;
138
139            // Reading Data
140            for i in 0..PACKET_SIZE {
141                // Waiting for Bit tx signaled by HIGH state
142                self.wait_for_state(HIGH, &mut pin)?;
143
144                // Reading bit value
145                self.timer.delay_us(35);
146                let state = get_input_state(&mut pin);
147
148                // Adding bit to buffer
149                let byte_index = i / 8;
150                let bit_index = 7 - (i % 8);
151                if state == 1 {
152                    buffer[byte_index] |= 1 << bit_index;
153                }
154
155                // Wait until bit finished sending
156                if state == HIGH {
157                    self.wait_for_state(LOW, &mut pin)?;
158                }
159            }
160
161            Ok(())
162        });
163
164        // Resetting pin state
165        let mut pin = self.pin.into_output();
166        pin.set_high();
167
168        // Evaluating transaction result
169        transaction_result?;
170
171        // Compute Checksum
172        let checksum = buffer[4];
173        let checksum_truth = buffer[0]
174            .wrapping_add(buffer[1])
175            .wrapping_add(buffer[2])
176            .wrapping_add(buffer[3]);
177
178        // If all received bits are 1
179        if checksum_truth == 252 {
180            return Err(DhtError::Connection);
181        }
182
183        if checksum != checksum_truth {
184            return Err(DhtError::Checksum);
185        }
186
187        // Compute Humidity
188        let humidity = u16::from_be_bytes([buffer[0], buffer[1]]);
189        let humidity = humidity as f32 * 0.1;
190
191        // Compute Temperature
192        let temperature = u16::from_be_bytes([buffer[2], buffer[3]]);
193
194        // Negative if highest bit is 1
195        let temperature = if temperature >> 15 == 1 {
196            (temperature & !(1 << 15)) as f32 * -0.1
197        }
198        else {
199            temperature as f32 * 0.1
200        };
201
202        Ok((humidity, temperature))
203    }
204}
205
206// —————————————————————————————————————————————————————————————————————————————————————————————————
207//                                              Error
208// —————————————————————————————————————————————————————————————————————————————————————————————————
209
210#[derive(Debug, Clone, Eq, PartialEq)]
211pub enum DhtError {
212    Timeout,
213    Checksum,
214    Communication,
215    Connection,
216}
217
218impl core::fmt::Display for DhtError {
219    fn fmt(
220        &self,
221        fmt: &mut core::fmt::Formatter<'_>,
222    ) -> core::result::Result<(), core::fmt::Error> {
223        match self {
224            DhtError::Timeout => write!(fmt, "timeout"),
225            DhtError::Checksum => write!(fmt, "invalid data"),
226            DhtError::Communication => write!(fmt, "communication error"),
227            DhtError::Connection => write!(fmt, "connection error"),
228        }
229    }
230}
231
232// —————————————————————————————————————————————————————————————————————————————————————————————————
233//                                             Traits
234// —————————————————————————————————————————————————————————————————————————————————————————————————
235
236/// Trait for constructing a dynamic input or output pin from scratch
237#[allow(clippy::wrong_self_convention)]
238pub trait ReconstructPin {
239    fn into_output(&self) -> Output;
240    fn into_input(&self) -> Input;
241}
242
243impl<T: gpio::AnyPin> ReconstructPin for T {
244    #[inline]
245    /// Returns a dynamic output pin
246    fn into_output(&self) -> Output {
247        let id = self.borrow().id().num;
248        // HAL is pedantic with self-creating pins, but we have ownership over the original pin
249        unsafe {
250            let pin = gpio::new_pin(gpio::DynPinId {
251                bank: gpio::DynBankId::Bank0,
252                num:  id,
253            });
254
255            pin.try_into_function::<gpio::FunctionSio<gpio::SioOutput>>()
256                .expect("Pin into Output")
257                .into_pull_type::<gpio::PullUp>()
258        }
259    }
260
261    #[inline]
262    /// Returns a dynamic input pin
263    fn into_input(&self) -> Input {
264        let id = self.borrow().id().num;
265        // HAL is pedantic with self-creating pins, but we have ownership over the original pin
266        unsafe {
267            let pin = gpio::new_pin(gpio::DynPinId {
268                bank: gpio::DynBankId::Bank0,
269                num:  id,
270            });
271
272            pin.try_into_function::<gpio::FunctionSio<gpio::SioInput>>()
273                .expect("Pin into Input")
274                .into_pull_type::<gpio::PullNone>()
275        }
276    }
277}
278
279// —————————————————————————————————————————————————————————————————————————————————————————————————
280//                                         Functions
281// —————————————————————————————————————————————————————————————————————————————————————————————————
282
283#[inline]
284fn get_input_state(pin: &mut Input) -> u8 {
285    if pin.is_high().unwrap() {
286        return HIGH;
287    }
288    LOW
289}