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}