dht_mmap_rust/
lib.rs

1pub mod mmap_gpio;
2mod scoped_thread_priority;
3
4use crate::mmap_gpio::{GpioMmapAccess, GpioOpenError};
5use crate::scoped_thread_priority::HighThreadPriorityGuard;
6use std::thread::sleep;
7use std::time::{Duration, SystemTime};
8
9/*
10
11This is a small library to read DHT11 and DHT22 sensors using memory mapped IO
12
13It's mostly a port of the C library by Adafruit:
14  https://github.com/adafruit/Adafruit_Python_DHT/blob/master/source/Raspberry_Pi/pi_dht_read.c
15
16For exact specifications on how the DHT11 communicates, see this DHT11 datasheet by mouser:
17  https://www.mouser.com/datasheet/2/758/DHT11-Technical-Data-Sheet-Translated-Version-1143054.pdf
18
19For specifications on the GPIO memory addresses, see the BCM2711 ARM Peripherals datasheet (PI 4B)
20  https://datasheets.raspberrypi.com/bcm2711/bcm2711-peripherals.pdf
21The registers used are already specified in the PI 1 and PI zero, see the BCM2835 datasheet:
22  https://www.raspberrypi.org/app/uploads/2012/02/BCM2835-ARM-Peripherals.pdf
23
24GPIO pin numbers are in the range 0..53 for the PI 1, but were increased up to 0..57 for the PI 4.
25Since the registers used were already reserved on the PI 1, this library allows writing to those higher
26pin numbers, even though they don't do anything.
27At this time, the PIs all only have 40 GPIO pins (0..39).
28
29*/
30
31/// Maximum "ticks" / volatile reads attempted before a read is considered a timeout. Might need
32/// to be increased for faster devices.
33const DHT_MAXCOUNT: u32 = 32000;
34
35/// Number of "bits" transmitted by the DHT for a single reading
36const DHT_PULSES: usize = 41;
37
38/// The type of Dht used. Specifying the wrong type results in absurd values, but does not pose
39/// any other risk, as only data interpretation is affected.
40pub enum DhtType {
41    Dht11,
42    Dht22,
43}
44
45/// Handle on a Dht device, can be used to read sensor data.
46/// The internal memmap reference is automatically cleared on drop.
47pub struct Dht {
48    gpio: GpioMmapAccess,
49    pin: usize,
50    typ: DhtType,
51}
52
53/// Reason that reading from the Dht failed. See individual errors for explanations.
54/// All of these can generally be retried.
55#[derive(Copy, Clone, PartialEq, Debug)]
56pub enum DhtError {
57    /// A timeout happened before data transmission started
58    ReadTimeoutEarly,
59    /// A timeout happened during data transmission
60    ReadTimeoutData,
61    /// The data received does not match the checksum - there likely was some transmission error
62    ChecksumMismatch,
63}
64
65impl Dht {
66    /// Create a new `Dht` struct, which can be used to read sensor data.
67    /// Note: Specifying the wrong DhtType only results in absurd readings, but does not have any other risks.
68    /// The `gpio_pin` is the GPIO number, not the pin number. You can find annotated visual charts for your PI's layout online.
69    /// Make sure you have specified the correct pin number.
70    /// Pin numbers from 0..57 are allowed here, but only pins 0..39 are actually available on current PI hardware.
71    pub fn new(typ: DhtType, gpio_pin: usize) -> Result<Dht, GpioOpenError> {
72        if gpio_pin > 57 {
73            panic!(
74                "Gpio pin number was too large: {}. Aborting to prevent memory overwrites.",
75                gpio_pin
76            );
77        }
78
79        return Ok(Dht {
80            gpio: GpioMmapAccess::new()?,
81            pin: gpio_pin,
82            typ,
83        });
84    }
85
86    /// Try to get a reading from the DHT sensor.  
87    /// **Note** that reading from DHT sensors is inherently unreliable: expect one in three reads to fail, round about.
88    /// Additionally, in rare cases individual reads may result in incorrect values.
89    /// This is a hardware issue. If you need more reliable reads, read twice
90    /// and make sure the results don't deviate much.  
91    /// Lastly, if you need *accurate* readings, cross-check your readings to some sensor you know to be correct,
92    /// it has been reported online that some DHT modules report values that are offset from the actual value by
93    /// some amount.
94    pub fn read(&mut self) -> Result<Reading, DhtError> {
95        let pin = self.pin;
96        let gpio = &mut self.gpio;
97
98        // Number of "reads" is recorded into this array, meaning how many times the gpio
99        // state could be polled before the pin read state switched.
100        // Every even entry is the length of a "low", every odd entry is the length of a "high"
101        let mut pulse_counts = [0u32; DHT_PULSES * 2];
102
103        // Final data will be recorded into this array
104        let mut data = [0u8; 5];
105
106        unsafe {
107            // Set high scheduling priority
108            let _priority_guard = HighThreadPriorityGuard::new();
109
110            // Pull high for 500ms
111            gpio.pi_mmio_set_output(pin);
112            gpio.pi_mmio_set_high(pin);
113            sleep(Duration::from_millis(500));
114
115            // Pull low for 20ms
116            gpio.pi_mmio_set_low(pin);
117            busy_wait_milliseconds(20);
118
119            // Start reading response
120            gpio.pi_mmio_set_input(pin);
121
122            let mut temp: u32 = 0;
123
124            // Spin for a bit. Write volatile so that the compiler does not optimize out.
125            for i in 0..500 {
126                ((&mut temp) as *mut u32).write_volatile(i);
127            }
128
129            // Spin until input is "low"
130            let mut count = 0u32;
131            while gpio.pi_mmio_input(pin) {
132                count += 1;
133                if count >= DHT_MAXCOUNT {
134                    return Err(DhtError::ReadTimeoutEarly);
135                }
136            }
137
138            // Record all pulses and their lengths
139            for i in (0..(DHT_PULSES * 2)).step_by(2) {
140                // Count how long pin is low and store in pulse_counts[i]
141                while !gpio.pi_mmio_input(pin) {
142                    pulse_counts[i] += 1;
143                    if pulse_counts[i] >= DHT_MAXCOUNT {
144                        // Timeout waiting for response.
145                        return Err(DhtError::ReadTimeoutData);
146                    }
147                }
148                // Count how long pin is high and store in pulse_counts[i+1]
149                while gpio.pi_mmio_input(pin) {
150                    pulse_counts[i + 1] += 1;
151                    if pulse_counts[i + 1] >= DHT_MAXCOUNT {
152                        // Timeout waiting for response.
153                        return Err(DhtError::ReadTimeoutData);
154                    }
155                }
156            }
157        }
158
159        // Compute the average low pulse width to use as a 50 microsecond reference threshold.
160        // The "low" parts are expected to be 50 microseconds every time, while the "high" is
161        // either 26-26 micros (bit is 0) or 70 micros (bit is 1). The low part timing can thus
162        // be used as a threshold for timing.
163        // Ignore the first two readings because they are a constant 80 microsecond pulse.
164        let mut threshold = 0u32;
165        for i_raw in 1..DHT_PULSES {
166            let i = i_raw * 2;
167            threshold += pulse_counts[i];
168        }
169        threshold = threshold / (DHT_PULSES as u32 - 1);
170
171        // Interpret each high pulse as a 0 or 1 by comparing it to the 50us reference.
172        // If the count is less than 50us it must be a ~28us 0 pulse. If it's higher,
173        // then it must be a ~70us 1 pulse.
174        for i in (3..(DHT_PULSES * 2)).step_by(2) {
175            let index = (i - 3) / 16;
176            data[index] <<= 1;
177            if pulse_counts[i] >= threshold {
178                // One bit for long pulse.
179                data[index] |= 1;
180            }
181            // Else zero bit for short pulse.
182        }
183
184        // Verify checksum of received data.
185        if data[4] != ((data[0] + data[1] + data[2] + data[3]) & 0xFF) {
186            return Err(DhtError::ChecksumMismatch);
187        }
188
189        return match self.typ {
190            DhtType::Dht11 => {
191                // This deviates from the Adafruit C code: According to the spec (link at the top of this file),
192                // the second and fourth byte are decimal parts for humidity and temperature respectively.
193                Ok(Reading {
194                    humidity: data[0] as f32 + data[1] as f32 * 0.1,
195                    temperature: data[2] as f32 + data[3] as f32 * 0.1,
196                })
197            }
198            DhtType::Dht22 => {
199                let humidity = (data[0] as u32 * 256 + data[1] as u32) as f32 / 10.0f32;
200                let mut temperature =
201                    ((data[2] & 0x7F) as u32 * 256 + data[3] as u32) as f32 / 10.0f32;
202                if data[2] & 0x80 != 0 {
203                    temperature *= -1.0f32;
204                }
205
206                Ok(Reading {
207                    humidity,
208                    temperature,
209                })
210            }
211        };
212    }
213}
214
215/// Reading from the DHT sensor.
216#[derive(Copy, Clone, PartialEq, Debug)]
217pub struct Reading {
218    temperature: f32,
219    humidity: f32,
220}
221
222impl Reading {
223    pub fn temperature(&self) -> f32 {
224        return self.temperature;
225    }
226
227    pub fn humidity(&self) -> f32 {
228        return self.humidity;
229    }
230}
231
232/// Busy-wait `ms` milliseconds, don't yield to the system
233fn busy_wait_milliseconds(ms: u64) {
234    let before = SystemTime::now();
235
236    loop {
237        if before.elapsed().unwrap().as_millis() as u64 >= ms {
238            return;
239        }
240    }
241}
242
243#[test]
244pub fn test_basic_reading_dht11_gpio_2() {
245    #[cfg(all(not(target_arch = "arm"), not(target_arch = "aarch64")))]
246    panic!("This test needs to be run on a PI - the architecture was not ARM, so this test will exit now.");
247
248    println!("\nTesting if reading succeeds. This tests assumes a DHT11 is connected on GPIO 2\n");
249
250    test_dht(DhtType::Dht11, 2);
251}
252
253#[test]
254pub fn test_basic_reading_dht22_gpio_3() {
255    #[cfg(all(not(target_arch = "arm"), not(target_arch = "aarch64")))]
256    panic!("This test needs to be run on a PI - the architecture was not ARM, so this test will exit now.");
257
258    println!("\nTesting if reading succeeds. This tests assumes a DHT11 is connected on GPIO 3\n");
259
260    test_dht(DhtType::Dht22, 3);
261}
262
263#[allow(dead_code)]
264fn test_dht(typ: DhtType, pin: usize) {
265    let mut dht = Dht::new(typ, pin).unwrap();
266
267    for i in 1..11 {
268        let reading = dht.read();
269        match reading {
270            Ok(reading) => {
271                if reading.temperature() < 5f32 || reading.temperature() > 35f32 {
272                    panic!(
273                        "Reading outside of defined sane range: Temperature is {}",
274                        reading.temperature()
275                    );
276                }
277                println!(
278                    "({}) Reading success: Temp {} Hum {}",
279                    i,
280                    reading.temperature(),
281                    reading.humidity()
282                );
283                return;
284            }
285            Err(err) => {
286                println!("({}) Error while reading: {:?}", i, err);
287            }
288        }
289    }
290
291    panic!("Reading failed all 10 times, something is not right.");
292}