dht_hal_drv/lib.rs
1//! # HAL based driver for Digital Humidity and Temperature sensors (DHT)
2//!
3//! Because of some limitations in HAL API and limitations in some HW implementations
4//! using this sensor **is some kind of tricky**.
5//!
6//! DHT use one pin for communication that should work in open drain (open connect) mode.
7//! For hardware that does not have such pin implementations you should emulate its behaviour.
8//!
9//! You can get readings by:
10//! * using single function to read all data
11//! * using splitted functions for initialization and reading while converting pin to different modes between calls
12//!
13//! Should notice that DHT initialization process has some caveats.
14//! There have to be near 1 sec delay before next reading.
15//! At his time pull-up resistor in DHT circuit would pull up data pin and this would prepare DHT for next reading cycle.
16//!
17//! Delay implementation issues should be taken into account.
18//! On some platforms sleep at some amount of microseconds means "sleep at least N us".
19//! For example on RPi with `std::thread::sleep` nothing would work.
20//! For such case should use `dht_split_read` without delay or another sleep implementations like `spin_sleep`.
21//!
22//! ## Examples
23//!
24//! ### Using open drain pin
25//!
26//! ```
27//! let delay; // Something that implements DelayUs trait
28//! let open_drain_pin; // Open drain pin, should be in open mode by default
29//! // Need to create closure with HW specific delay logic that DHT driver is not aware of
30//! let mut delay_us = |d| delay.delay_us(d);
31//!
32//! // ... Some code of your APP ... //
33//! let result = dht_read(DhtType::DHT11, &mut open_drain_pin, &mut delay_us);
34//! // ... Other code of your APP ... //
35//! ```
36//!
37//! ### Using dht_split_* functions
38//!
39//! Such approach is useful if you your device does not have open drain pin and you need to emulate it
40//! or you have slow CPU and do not want to use delays while reading.
41//!
42//! ```
43//! use dht_hal_drv::{dht_split_init, dht_split_read, DhtError, DhtType, DhtValue};
44//!
45//! // ... Some code of your APP ... //
46//!
47//! let delay; // Something that implements DelayUs trait
48//! let pin_in; // Pin configured as input floating
49//!
50//! // Should create closure with
51//! // custom HW specific delay logic that DHT driver is not aware of
52//! let mut delay_us = |d| delay.delay_us(d);
53//!
54//! // pin to output mode
55//! let mut pin_out = pin_in.into_push_pull_output();
56//!
57//! // Initialize DHT data transfer
58//! // Before reading begins MCU must send signal to DHT which would initiate data transfer from DHT.
59//! dht_split_init(&mut pin_out, &mut delay_us);
60//!
61//! // You can check dht_split_init response for errors if you want
62//!
63//! // WARNING there should be no additional logic between dht_split_init and dht_split_read
64//!
65//! // Should convert pin back to input floating
66//! let mut pin_in = pin_out.into_floating_input(cr);
67//!
68//! // Now let's read some data
69//! // Here you can pass empty delay_us closure to skip using delays on slow CPU
70//! let readings = dht_split_read(DhtType::DHT11, &mut pin_in, &mut delay_us);
71//!
72//! // ... Other code of your APP ... //
73//!
74//! ```
75//!
76//! Working examples for particular HW platforms could be found in source repository.
77//!
78//! ## Inspiration sources
79//!
80//! - [Adafruit DHT.cpp](https://github.com/adafruit/DHT-sensor-library/blob/master/DHT.cpp)
81//! - [Adafruit python lib pi_dht_read.c](https://github.com/adafruit/Adafruit_Python_DHT/blob/master/source/Raspberry_Pi/pi_dht_read.c)
82//! - [Full signals diagrams for DHT](http://www.adafruit.com/datasheets/Digital%20humidity%20and%20temperature%20sensor%20AM2302.pdf).
83//!
84#![no_std]
85use core::prelude::v1::Result;
86use embedded_hal::digital::v2::{InputPin, OutputPin};
87
88///
89/// You should receive this in a case of some unpleasant situation.
90///
91#[derive(Debug)]
92pub enum DhtError {
93 /// Unable to read data from sensor
94 Readings,
95 /// Data was read but checksum validation failed
96 Checksum,
97 /// Error reading pin values or acquire pin etc.
98 IO,
99}
100
101///
102/// Describe available DHT sensor types.
103///
104#[derive(Debug, Clone)]
105pub enum DhtType {
106 DHT11,
107 DHT21,
108 DHT22,
109}
110
111///
112/// Contains readings from DHT sensor.
113///
114#[derive(Debug)]
115pub struct DhtValue {
116 dht_type: DhtType,
117 value: [u8; 5],
118}
119
120impl DhtValue {
121 /// Return temperature readings in Celsius.
122 /// All raw data from DHT sensor are related to this scale.
123 pub fn temperature(&self) -> f32 {
124 match &self.dht_type {
125 DhtType::DHT11 => self.value[2] as f32,
126 _ => {
127 let mut v: f32 = (self.value[2] & 0x7F) as f32;
128 v = (v * 256.0 + self.value[3] as f32) * 0.1;
129 if self.value[2] & 0x80 > 0 {
130 v *= -1.0;
131 }
132 v
133 }
134 }
135 }
136
137 /// Convert temperature readings from Celsius to Fahrenheit
138 /// for those who love to use piffle scales and measurements.
139 pub fn temperature_f(&self) -> f32 {
140 self.temperature() * 1.8 + 32.0
141 }
142
143 /// Return humidity readins in percents.
144 pub fn humidity(&self) -> f32 {
145 match &self.dht_type {
146 DhtType::DHT11 => self.value[0] as f32,
147 _ => {
148 let mut v: f32 = self.value[0] as f32;
149 v = (v * 256.0 + self.value[1] as f32) * 0.1;
150 v
151 }
152 }
153 }
154}
155
156///
157/// Read DHT sensor via open drain (open collector) pin or its emulation.
158///
159///
160/// DHT sensor communication designed to use open drain like pin with pull up resistor.
161/// Some hardware does not have open drain pins but it is possible to emulate it
162/// in this case set_high() should convert pin into input floating mode.
163///
164/// If you have slow CPU clocks speed you may need to read data without delays at all.
165/// Thus you should use `dht_split_*` functions instead and pass an empty closure to `dht_split_read`.
166///
167/// # Arguments
168///
169/// * `dht` - DHT sensor type we are reading
170/// * `open_pin` - Open drain like pin
171/// * `delay_us` - Closure where you should call appropriate delay/sleep/whatever API with microseconds as input.
172///
173pub fn dht_read<IO_PIN>(
174 dht: DhtType,
175 open_pin: &mut IO_PIN,
176 delay_us: &mut dyn FnMut(u16) -> (),
177) -> Result<DhtValue, DhtError>
178where
179 IO_PIN: InputPin + OutputPin,
180{
181 dht_split_init(open_pin, delay_us)?;
182 // Toggle open drain pin to open (high) state.
183 open_pin.set_high().map_err(|_| DhtError::IO)?;
184 dht_split_read(dht, open_pin, delay_us)
185}
186
187///
188/// Initialize DHT sensor (sending start signal) to start readings.
189///
190/// Notice that there have to be about 1 sec delay before each reading (between calling `dht_split_init`).
191/// At this period data pin should be pulled up by resistor connected to DHT
192/// which is default connection scheme for DHT.
193/// It implies that pin should be set in input floating mode after previous reading.
194///
195/// In Adafruit drivers you can see that there is initial delay with high impedance for about 500-700ms.
196/// You do not need this delay if you read sensor not to often and do other logic between readings.
197///
198/// # Arguments
199///
200/// * `output_pin` - Output pin trait for DHT data pin.
201/// * `delay_us` - Closure where you should call appropriate delay/sleep/whatever API with microseconds as input.
202///
203pub fn dht_split_init<Error>(
204 output_pin: &mut dyn OutputPin<Error = Error>,
205 delay_us: &mut dyn FnMut(u16) -> (),
206) -> Result<(), DhtError> {
207 // Voltage level from high to low.
208 // This process must take at least 18ms to ensure DHT’s detection of MCU's signal.
209 output_pin.set_low().map_err(|_| DhtError::IO)?;
210 delay_us(20_000);
211 Ok(())
212}
213
214///
215/// Call this function immediately after [initialization](fn.dht_split_init.html) to acquire proper sensor readings.
216///
217/// # Arguments
218///
219/// * `dht` - DHT sensor type
220/// * `input_pin` - Input pin trait for DHT data pin
221/// * `delay_us` - Closure with delay/sleep/whatever API with microseconds as input,
222/// NOTE that for low frequency CPUs (about 2Mhz or less) you should pass empty closure.
223///
224pub fn dht_split_read<Error>(
225 dht: DhtType,
226 input_pin: &mut dyn InputPin<Error = Error>,
227 delay_us: &mut dyn FnMut(u16) -> (),
228) -> Result<DhtValue, DhtError> {
229 let threshold = 20;
230 let rate = 100;
231 // On RPi 3 without delays max "while" cycles per pin state is about 500, initial cycle is about 100
232 // Thus if "while" would stuck at 20*100 cycles on initial cycle it would case an error
233 // or if it would stuck at 500*100 somewhere in the middle.
234 // With delays we should have about 20 cycles per pin state.
235 dht_split_read_customizable(dht, input_pin, delay_us, threshold, rate)
236}
237
238///
239/// Advanced customizable read function, you probably would not need to use it directly.
240/// Call it immediately after [initialization](fn.dht_split_init.html) to acquire proper sensor readings.
241///
242///
243/// # Arguments
244///
245/// * `dht` - DHT sensor type
246/// * `input_pin` - Input pin trait for DHT data pin
247/// * `delay_us' - Closure with delay/sleep/whatever API with microseconds as input,
248/// NOTE that for low frequency CPUs (about 2Mhz or less) you should pass empty closure.
249/// * `reads_threshold` - Initial threshold (cycles count) before reading cycle starts.
250/// * `reads_error_rate` - If actual cycles count in one pin state would exceed last threshold at provided rate it would cause an error.
251///
252pub fn dht_split_read_customizable<Error>(
253 dht: DhtType,
254 input_pin: &mut dyn InputPin<Error = Error>,
255 delay_us: &mut dyn FnMut(u16) -> (),
256 reads_threshold: u32,
257 reads_error_rate: u32,
258) -> Result<DhtValue, DhtError> {
259 // Initialize variables
260 let mut err: Option<DhtError> = None;
261 let mut data: [u8; 5] = [0; 5]; // Set 40 bits of received data to zero.
262 let mut cycles: [u32; 83] = [0; 83];
263
264 // MCU will pull up voltage and wait 20-40us for DHT’s response
265 // Delay a bit to let sensor pull data line low.
266
267 // READ to cycles[0] - or skip to next
268
269 // Now start reading the data line to get the value from the DHT sensor.
270 // First expect a low signal for ~80 microseconds followed by a high signal
271 // for ~80 microseconds again.
272
273 // READ to cycles[1] and cycles[2]
274
275 // Now read the 40 bits sent by the sensor. Each bit is sent as a 50
276 // microsecond low pulse followed by a variable length high pulse. If the
277 // high pulse is ~28 microseconds then it's a 0 and if it's ~70 microseconds
278 // then it's a 1. We measure the cycle count of the initial 50us low pulse
279 // and use that to compare to the cycle count of the high pulse to determine
280 // if the bit is a 0 (high state cycle count < low state cycle count), or a
281 // 1 (high state cycle count > low state cycle count). Note that for speed all
282 // the pulses are read into a array and then examined in a later step.
283
284 // READ to cycles[3+] as low level and cycles[4+] as high level
285
286 // Initial threshold
287 // If reads cycles count would be more at `reads_error_rate` reading would stop.
288 let mut threshold = reads_threshold;
289
290 // As I can see at least 10 instructions should be executed at each cycle.
291 // If CPU frequency is 8MHz and IPC value is 1 than 28us
292 // would approximately match up to 22 loop cycles without using any delay.
293 // With delay of 2us it have to be about 8 cycles.
294 // For 1MHz CPU it is about 2 cycles.
295 // NOTICE for slow CPU you should not implement `delay_us` this closure should be empty.
296 let delay_us_value = 2;
297 let mut i = 0;
298 while i < 83 {
299 let high = input_pin.is_high().map_err(|_| DhtError::IO)?;
300 if (i % 2 == 0) == high {
301 // Instead of reading time we just count number of cycles until next level value
302 cycles[i] += 1;
303 if high && cycles[i] / threshold > reads_error_rate {
304 // Check errors only on high cycles
305 // When DHT stop transfer data resistor would pull pin up
306 err = Some(DhtError::Readings);
307 break;
308 }
309 } else {
310 if high && cycles[i] > threshold {
311 // Raise error threshold dynamically
312 // to adjust this value to current CPU speed
313 threshold = cycles[i];
314 }
315 i += 1;
316 }
317
318 // // Reasonable delay for fast CPU
319 delay_us(delay_us_value);
320 }
321
322 // Inspect pulses and determine which ones are 0 (high state cycle count < low
323 // state cycle count), or 1 (high state cycle count > low state cycle count).
324 // We skip first 3 values because there is no\t data there
325 for i in 0..40 {
326 let low_cycle = cycles[2 * i + 3];
327 let high_cycle = cycles[2 * i + 4];
328
329 data[i / 8] <<= 1;
330 if high_cycle > low_cycle {
331 // High cycles are greater than 50us low cycle count, must be a 1.
332 data[i / 8] |= 1;
333 }
334 // Else high cycles are less than (or equal to, a weird case) the 50us low
335 // cycle count so this must be a zero. Nothing needs to be changed in the
336 // stored data.
337 }
338
339 // DEBUG CODE, works only with std enabled
340 // {
341 // println!("Cycles {:?}",cycles.iter().map(ToString::to_string).collect::<Vec<String>>().join(", "));
342 // print!("DHT readings: ");
343 // print!("{:X} {:X} {:X} {:X}", data[0], data[1], data[2], data[3]);
344 // println!(
345 // " {:X} == {:X} (checksum)",
346 // data[4],
347 // (data[0] as u16 + data[1] as u16 + data[2] as u16 + data[3] as u16) & 0xFF
348 // );
349 // }
350
351 // Check we read 40 bits and that the checksum matches.
352 let checksum = (data[0] as u16 + data[1] as u16 + data[2] as u16 + data[3] as u16) & 0xFF;
353 if data[4] as u16 == checksum {
354 Ok(DhtValue {
355 value: data,
356 dht_type: dht,
357 })
358 } else {
359 Err(err.unwrap_or(DhtError::Checksum))
360 }
361}