Uwaga! pełen kod tego wpisu dostępny jest tutaj
W poprzednim wpisie o wyświetlaczu LCD uczyłem się jak przy wykorzystaniu maliny, rusta i prostego wyświetlacza zaprezentować dane użytkownikowi. Kolejnym elementem w moim zestawie startowym jest czujnik wilgotności DHT11.
Czujnik DHT11 zgodnie z dokumentacją jest w stanie mierzyć relatywną wilgotność z dokładnością +/-5% a temperatury +/-2°C. Zupełnie wystarczająco na potrzeby domowego projektu. Do komunikacji z maliną wykorzystuje pojedynczy pin GPIO. Sterując czujnikiem należy wysyłać i odbierać sygnały w określonej sekwencji zgodnej z protokołem - inaczej czujnik nie będzie wysyłał poprawnych pomiarów.
Budowa projektu Link to heading
W pierwszej kolejności w lib.rs
tworzę obiekty domenowe:
pub struct DHT11 {
pin: IoPin,
}
#[derive(Debug)]
pub struct Readings {
pub temperature: f64,
pub humidity: f64,
}
Sygnał odebrany od czujnika składa się z 40 bitów.
- Pierwszy bajt zawiera część odczytu wilgotności przed przecinkiem
- Drugi bajt zawiera część odczytu wilgotności po przecinku
- Trzeci bajt zawiera cześć odczytu temperatury przed przecinkiem
- Czwarty bajt zawiera część odczytu temperatury po przecinku
- Piąty bajt zawiera sumę kontrolną - sumę poprzednich bajtów
Dlatego struct Readings
zawiera dwa pola - jedno zawierające temperraturę, drugie wilgotność.
Jako, że czujnik DHT11 do komunikacji używa pojedynczego pina który jest zarówno wejściem i wyjściem to struct DHT11
zawiera jedno pole. Oprócz tego aby umożliwić czytelny wydruk (czasem wartości mogą mieć podobne wartości) należy zaimplementować traita fmt::Display
:
impl fmt::Display for Readings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Readings({:.2}%, {:.2}C)",
self.humidity, self.temperature
)
}
}
Pora rozpocząć implementację odczytu wartości naszego czujnika!
impl DHT11 {
pub fn new(gpio_num: u8) -> Result<DHT11> {
Ok(DHT11 {
pin: Gpio::new()?.get(gpio_num)?.into_io(Mode::Output),
})
}
/// initialize
fn init_sequence(&mut self) -> () {
// step 2 of documentation
self.pin.set_mode(Mode::Output);
self.pin.set_low();
delay_ms(18);
self.pin.set_high();
delay_us(50)
}
pub fn read(&mut self) -> Result<Readings> {
self.init_sequence();
let mut bytes = [0u8; 5];
{
// step 3 of documentation
self.pin.set_mode(Mode::Input);
wait_level(&mut self.pin, Level::Low)?;
wait_level(&mut self.pin, Level::High)?;
wait_level(&mut self.pin, Level::Low)?;
// step 4 of documentation
for b in bytes.iter_mut() {
for _ in 0..8 {
*b <<= 1;
wait_level(&mut self.pin, Level::High)?;
let dur = wait_level(&mut self.pin, Level::Low)?;
if dur > 26 {
*b |= 1;
}
}
}
}
let sum: u16 = bytes.iter().take(4).map(|b| *b as u16).sum();
if bytes[4] as u16 == sum & 0x00FF {
Ok(Readings {
temperature: bytes[2] as f64 + (bytes[3] as f64 / 10.0),
humidity: bytes[0] as f64 + (bytes[1] as f64 / 10.0),
})
} else {
Err(anyhow!("Check sum!"))
}
}
}
fn wait_level(pin: &mut IoPin, level: Level) -> Result<u8> {
for i in 0u8..255 {
if pin.read() == level {
return Ok(i);
}
delay_us(1);
}
Err(anyhow!("Timeout!"))
}
Dodajemy funkcję new
która umożliwia na wskazanie numeru pin, do którego będzie podłączona szyna danych w naszym czujniku.
pub fn new(gpio_num: u8) -> Result<DHT11> {
Ok(DHT11 {
pin: Gpio::new()?.get(gpio_num)?.into_io(Mode::Output),
})
}
Zgodnie z dokumentacją czujnika, przed każdym odczytem należy wykonać inicjalizację, czyli ustawić pin w stan niski na 18 ms i zmienić na stan wysoki:
fn init_sequence(&mut self) -> () {
// step 2 of documentation
self.pin.set_mode(Mode::Output);
self.pin.set_low();
delay_ms(18);
self.pin.set_high();
delay_us(50)
}
W dalszej kolejności musimy przejść w tryb wejściowy i odebrać 40 bitów sygnału.
Zadanie to wykonuje metoda read
przyjmująca jako argument mutowalną referencję do samego siebie.
W tym momencie musimy oczekiwać w trybie wejściowym na sekwencję sygnałów:
// step 3 of documentation
self.pin.set_mode(Mode::Input);
wait_level(&mut self.pin, Level::Low)?;
wait_level(&mut self.pin, Level::High)?;
wait_level(&mut self.pin, Level::Low)?;
Teraz możemy zainicjować tablicę spodziewanych 5 bajtów i przypisać im odpowiednie wartości:
let mut bytes = [0u8; 5];
// step 4 of documentation
for b in bytes.iter_mut() {
for _ in 0..8 {
*b <<= 1;
wait_level(&mut self.pin, Level::High)?;
let dur = wait_level(&mut self.pin, Level::Low)?;
if dur > 26 {
*b |= 1;
}
}
}
Pozostaje sprawdzić czy suma kontrolna znajdująca się w ostatnim bajcie jest zgodna z odczytami.
let sum: u16 = bytes.iter().take(4).map(|b| *b as u16).sum();
if bytes[4] as u16 == sum & 0x00FF {
Ok(Readings {
temperature: bytes[2] as f64 + (bytes[3] as f64 / 10.0),
humidity: bytes[0] as f64 + (bytes[1] as f64 / 10.0),
})
} else {
Err(anyhow!("Check sum!"))
}
Pierwszy bajt zawiera pełnoliczbową część odczytu wilgotności, drugi część dziesiętną. Analogicznie w przypadku bajtów 3 i 4 jedynie dla temperatury. Suma kontrolna liczona jest jako suma kolejnych bajtów:
bytes.iter().take(4).map(|b| *b as u16).sum()
Złożenie kodu w całość Link to heading
W celu prezentacji otrzymanych odczytów możemy użyć układu z poprzedniego wpisu i wyświetlić je na wyświetlaczu LCD. W tym celu najpierw musimy dodać zależność do modułu z biblioteką zawierającą kod z poprzedniego rozdziału. Zakładając taką samą strukturę kodu jak w repozytorium rustberry starczy dodać jedną linijkę do pliku cargo.toml w bloku zależności:
lcd = { path = "../lcd" }
A następnie w pliku main.rs
możemy napisać:
use anyhow::Result;
use dht11::DHT11;
use lcd::Lcd;
use std::{thread, time::Duration};
fn main()-> Result<()> {
let mut sensor = DHT11::new(21)?;
let mut lcd = Lcd::new()?;
lcd.init()?;
loop {
let result = sensor.read();
match result {
Ok(readings) => {
println!("{}", readings);
let msg = format!("Temp: {:.1}C\nHumid: {:.1}%", readings.temperature, readings.humidity);
lcd.message(msg)?
},
Err(err) => eprintln!("{}",err)
}
thread::sleep(Duration::from_secs(3));
}
}
Jeżeli wykonaliśmy poprawnie podłączenie czujnika i ekranu, to dostaniemy w efekcie odczyty wyświetlane na naszym ekranie!
Podsumowanie Link to heading
Nauczyłem się obsługiwać czujnik wilgotności i temperatury DHT11 przy wykorzystaniu pinów GPIO w trybie wejścia/wyjścia.
Przy okazji nauczyłem się używać własnych bibliotek jako podmoduły projektu.
Okazuje się, że łączenie wielu projektów przy wykorzystaniu cargo
jest bardzo przyjemne i nie różni się niczym względem dodania zależności do crate z crate.rs.