Mini stacja pogodowa - czyli DHT11 oraz DH22 w duecie z AVR

Jako iż nie miałem do tej pory żadnego termometru na zewnątrz postanowiłem wykonać sobie mini stację pogodową wskazującą temperaturę i wilgotność wewnątrz i na zewnątrz. Do tego celu wykorzystałem popularne i łatwo dostępne cyfrowe czujniki DHT11 i DHT22.

 

DHT11 jest trochę tańszy, ale nie może pracować w temperaturach ujemnych, dlatego został wykorzystany do pomiarów wewnątrz. DHT22 zaś można mierzyć temperatury nawet do -40 stopni Celsjusza, więc nada się do pomiarów na zewnątrz. Oba czujniki mogą być zasilane napięciem w zakresie od 3,3 do 5,5V. Do wyświetlania wyników pomiarów zastosowałem zwykły wyświetlacz alfanumeryczny 1602, a do sterowania całością mikrokontroler Attiny24A-SSU, który akurat miałem pod ręką. Ma on jedynie 2kB pamięci flash, także program powinien zostać napisany dość oszczędnie. Do tak prostego zadania będzie wystarczający. Do zasilania układu wykorzystałem 3 baterie AA. Nie miałem pod ręką niestety koszyków na baterie AAA, wtedy całe urządzenie mogłoby być trochę mniejsze i lżejsze. Założyłem, że czujnik wewnętrzny będzie umieszczony w obudowie urządzenia (lekko wystający), a czujnik zewnętrzny będzie podłączony kablem. Mając te wszystkie założenia można przystąpić do zaprojektowania płytki drukowanej.

Schemat jest dość prosty, zawiera jedynie niezbędne elementy do prawidłowej pracy mikrokontrolera i wyświetlacza, gniazdo programowania ISP oraz złącza do czujników. Na tej podstawie przygotowałem wzór ścieżek płytki drukowanej.

Tak jak schemat tak i wyjściowa płytka jest bardzo prosta. Wystarczyła płytka jednostronna.

Mając gotowy projekt wykonałem PCB metodą termotransferową i przylutowałem małe elementy.

Na podstawie modelu 3d płytki PCB przygotowałem obudowę do wydrukowania na drukarce 3d. Konieczne było uwzględnienie miejsca na koszyk baterii, włącznik zasilania, otwory na przewód do czujnika zewnętrznego i samego czujnika wewnętrznego. Mając już wydrukowaną obudowę można przylutować resztę elementów i umieścić urządzenie w obudowie.

Czarny napis uzyskałem dzięki wcięciu tekstu w modelu obudowy i pokolorowaniu wnętrza markerem do płyt CD. Obudowę na czujnik zewnętrzny znalazłem na Thingiverse.

Mając gotowy sprzęt czas na przygotowanie oprogramowania. Obsługa wyświetlacza LCD jest dość dobrze opisana w internecie i książkach, stąd pomijam tutaj całkowicie to zagadnienie. Warto skupić się na obsłudze czujników. Większość artykułów na ten temat w internecie radzi podłączyć czujnik do Arduino, wykorzystać gotową bibliotekę i już. Tutaj jednak do dyspozycji jest małe Attiny24, więc komunikację należało opracować samodzielnie. Oddzielnie dla DHT11 i DHT22, gdyż komunikacja z jednym i drugim nieznacznie się od siebie różni. Na początek warto przygotować sobie struktury, które będą zawierały wyniki pomiarów z czujników. Adresy (wskaźniki) tych struktur będziemy przekazywali jako argumenty do funkcji odczytujących dane z czujników. Oprócz tego napiszemy od razu makrodefinicje pinów mikrokontrolera, do których są podłączone czujniki. Osobiście w swoich programach używam wygodnych makrodefinicji preprocesora opisanych w książkach Tomasza Francuza o mikrokontrolerach AVR, dzięki czemu przy przeniesieniu programu na inny mikrokontroler wystarczy w jednym miejscu zmienić sposób podłączenia czujnika. Do pliku nagłówkowego dołączymy również symbole statusów komunikacji z czujnikami oraz deklaracje funkcji - odczytującej z czujnika DHT11 oraz odczytującej z czujnika DHT22.

/*
 * dht11.h
 *
 * Created: 09.01.2021 16:10:19
 *  Author: Mateusz
 */ 


#ifndef DHT11_H_
#define DHT11_H_

#define DHT11_PIN B, PB1
#define DHT22_PIN B, PB0

#define DHT11_OK		0
#define DHT11_BLAD_CRC	1
#define DHT11_TIMEOUT	2

typedef struct 
{
	int8_t temp;
	uint8_t temp_ulamek;
	uint8_t wilgotnosc;
	uint8_t wilgotnosc_ulamek;
}dht22_odczyt;

typedef struct
{
	uint8_t temp;
	uint8_t temp_ulamek;
	uint8_t wilgotnosc;
	uint8_t wilgotnosc_ulamek;
}dht11_odczyt;

uint8_t dht22_odczytaj(dht22_odczyt *odczyt);
uint8_t dht11_odczytaj(dht11_odczyt *odczyt);




#endif /* DHT11_H_ */

W przypadku czujnika DHT11 mierzymy jedynie wartości dodatnie, stąd wszystkie zmienne są typu całkowitego bez znaku. DHT22 mierzy również temperatury ujemne, stąd część całkowita temperatury jest typu całkowitego ze znakiem. Dla wygody podzieliłem także część całkowitą i ułamkową. Wykorzystanie liczb zmiennoprzecinkowych (float) na AVR jest bardzo nieoptymalne i zużywa dużo pamięci. Operując na zmiennych typu float program nie zmieściłby się na takim małym mikrokontrolerze.

Oba czujniki do komunikacji wykorzystują jednoprzewodową, dwukierunkową magistralę szeregową. Nie jest ona obsługiwana sprzętowo przez AVR, dlatego będziemy ręcznie "machali" stanem pinu IO.

Pierwszą funkcją będzie odczytująca wyniki z czujnika DHT11. Jak pisałem wcześniej jako argument funkcja będzie przyjmowała wskaźnik na strukturę, w której ma umieścić wyniki pomiaru. Jako, że proces komunikacji jest krytyczny czasowo, należy pamiętać aby na czas wysyłania i odbierania danych zablokować obsługę przerwań. Co prawda w moim programie przerwania nie będą wykorzystywane, ale należy pamiętać o tym w przypadku chęci wykorzystania kodu w innych projektach. Przerwania blokujemy umieszczając ciąg instrukcji w bloku atomowym (nie zapominając o dołączeniu nagłówka util/atomic.h). Ramka danych odbieranych z czujnika ma długość 40 bitów, dlatego dane te umieścimy w pięcioelementowej tablicy liczb ośmiobitowych. Ramka danych składa się z części całkowitej wilgotności, części ułamkowej wilgotności, części całkowitej temperatury, części ułamkowej temperatury i sumy kontrolnej będącej ośmioma najmłodszymi bitami sumy pozostałych przesłanych bajtów. Ramka danych jest taka sama dla czujników DHT11 oraz DHT22.

Proces komunikacji z czujnikiem rozpoczyna się od nadania sygnału START przez mikrokontroler. Polega to na wymuszeniu poziomu niskiego na linii danych przez 18ms, następnie stanu wysokiego przez 20-40us i oczekiwanie na wymuszenie przez czujnik stanu niskiego, które powinno trwać 80us oraz następnie wymuszenie stanu wysokiego również przez 80us.

Od tego momentu rozpoczyna się odbiór danych. Najpierw czujnik wymusza stan niski przez 50us, co zapowiada transmisję kolejnego bitu. Następnie czujnik wymusza stan wysoki. Jeśli stan ten utrzymuje się przez 26-28us to nadany bit ma wartość 0, jeśli zaś utrzymuje się 70us to bit ma wartość 1. Cykl ten powtarza się 40 razy, aż przesłana zostanie cała ramka danych.

Na koniec następuje przyporządkowanie poszczególnych bajtów do wyjściowej struktury i obliczenie sumy kontrolnej. Jeśli wszystko się zgadza i nigdzie po drodze komunikacja nie została zakłócona to zwracany jest przez funkcję status OK.

uint8_t dht11_odczytaj(dht11_odczyt *odczyt)
{
	uint8_t dane[5] = {0,0,0,0,0};
	uint8_t bit = 7; //numer transmitowanego bitu w danym bajcie
	uint8_t bajt = 0; //numer transmitowanego bajtu
	ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
	{
		//Sygnał start dla czujnika
		WYJSCIE(DHT11_PIN);
		NISKI(DHT11_PIN);
		_delay_ms(18);
		WYSOKI(DHT11_PIN);
		_delay_us(40);
		WEJSCIE(DHT11_PIN);
		//Oczekiwanie na potwierdzenie otrzymania sygnału start przez czujnik
		uint8_t i = 90;
		while (!(STAN(DHT11_PIN))) //Dopóki jest stan niski na pinie
		{
			//Oczekiwanie 80us na odpowiedź
			if (i-- == 0)
				return DHT11_TIMEOUT;
			else
				_delay_us(1);
		}
		i = 90;
		while (STAN(DHT11_PIN)) //Dopóki jest stan wysoki na pinie
		{
			//Oczekiwanie 80us na odpowiedź
			if (i-- == 0)
				return DHT11_TIMEOUT;
			else
				_delay_us(1);
		}
		//Obiór 40 bitów
		for (uint8_t j = 0; j < 40; j++)
		{
			i = 60;
			while (!(STAN(DHT11_PIN))) //Dopóki jest stan niski na pinie
			{
				//Rozpoczęcie nadawania kolejnego bitu
				if (i-- == 0)
					return DHT11_TIMEOUT;
				else
					_delay_us(1);
				
			}
			i = 0;
			while (STAN(DHT11_PIN)) //Dopóki jest stan wysoki na pinie
			{
				if (i++ == 80)
					return DHT11_TIMEOUT;
				else
					_delay_us(1);
			}
			//Jeśli krótki impuls to bit ma wartość 0
			//Jeśli impuls trwa ~70us to jest to wartość 1
			if (i > 30)
			{
				dane[bajt] |= (1 << bit);
			}
			if (bit == 0) //Jeśli odczytano wszystkie bity danego bajtu
			{
				bit = 7;
				bajt++;
			}
			else
				bit--;
		}
	}
	//Przyporządkowanie odczytanych danych do właściwych komórek
	odczyt->wilgotnosc = dane[0];
	odczyt->wilgotnosc_ulamek = dane[1];
	odczyt->temp = dane[2];
	odczyt->temp_ulamek = dane[3];
	
	//Sprawdzenie sumy kontrolnej
	if (dane[4] != dane[0] + dane[1] + dane[2] + dane[3]) // Jeśli CRC się nie zgadza
	return DHT11_BLAD_CRC;
	
	return DHT11_OK;
	
}

W przypadku DHT22 funkcja jest bardzo podobna, ale nie taka sama. Pierwsza różnica odnosi się do sygnału start, w którym wymuszenie stanu niskiego powinno trwać 1ms zamiast 18ms. Różni się także obróbka odebranych danych.

uint8_t dht22_odczytaj(dht22_odczyt *odczyt)
{
	uint8_t dane[5] = {0,0,0,0,0};
	uint8_t bit = 7;
	uint8_t bajt = 0;	
	ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
	{
		WYJSCIE(DHT22_PIN);
		NISKI(DHT22_PIN);
		_delay_ms(1);
		WYSOKI(DHT22_PIN);
		_delay_us(40);
		WEJSCIE(DHT22_PIN);
		//Oczekiwanie na potwierdzenie
		uint8_t i = 90;
		while (!(STAN(DHT22_PIN))) //Dopóki jest stan niski na pinie
		{
			if (i-- == 0)
			return DHT11_TIMEOUT;
			else
			_delay_us(1);
		}
		i = 90;
		while (STAN(DHT22_PIN)) //Dopóki jest stan wysoki na pinie
		{
			if (i-- == 0)
			return DHT11_TIMEOUT;
			else
			_delay_us(1);
		}
		
		for (uint8_t j = 0; j < 40; j++)
		{
			i = 60;
			while (!(STAN(DHT22_PIN))) //Dopóki jest stan niski na pinie
			{
				if (i-- == 0)
				return DHT11_TIMEOUT;
				else
				_delay_us(1);
				
			}
			i = 0;
			while (STAN(DHT22_PIN)) //Dopóki jest stan wysoki na pinie
			{
				if (i++ == 80)
				return DHT11_TIMEOUT;
				else
				_delay_us(1);
			}
			//Jeśli krótki impuls to bit ma wartość 0
			//Jeśli impuls trwa ~70us to jest to wartość 1
			if (i > 30)
			{
				dane[bajt] |= (1 << bit);
			}
			if (bit == 0) //Jeśli odczytano wszystkie bity danego bajtu
			{
				bit = 7;
				bajt++;
			}
			else
			bit--;
		}
	}
	//Przyporządkowanie odczytanych danych do właściwych komórek
	int16_t temp = (uint16_t)(dane[2] & 0x7F) << 8 | dane[3];
	uint16_t wilg = (uint16_t)(dane[0]) << 8 | dane[1];
	odczyt->wilgotnosc = wilg /10;
	odczyt->wilgotnosc_ulamek = wilg % 10;
	odczyt->temp = temp / 10;
	if (dane[2] & 0x80)
		odczyt->temp = -odczyt->temp;
	odczyt->temp_ulamek = temp % 10;
	
	//Sprawdzenie sumy kontrolnej
	if (dane[4] != ((uint16_t)(dane[0] + dane[1] + dane[2] + dane[3]) & 0xFF)) // Jeśli CRC się nie zgadza
		return DHT11_BLAD_CRC;
	
	return DHT11_OK;
	
}

Mając te funkcje można przygotować pętlę główną programu. Myślę, że nie potrzeba tutaj komentować.

/*
 * StacjaPogodowa.c
 *
 * Created: 23.01.2021 16:15:09
 * Author : Mateusz
 */ 

#include <avr/io.h>
#include <avr/pgmspace.h>
#include <stdlib.h>
#include <util/delay.h>
#include "makra.h"
#include "dht11.h"
#include "lcd.h"


int main(void)
{
    lcdInicjalizacja();
	lcdTekst(PSTR("WITAM!"));
	_delay_ms(1000);
	
	dht22_odczyt dht22;
	dht11_odczyt dht11;
    while (1) 
    {
		
		lcdCzysc();
		
		if (dht22_odczytaj(&dht22) == DHT11_TIMEOUT)
		{
			lcdTekst(PSTR("TIMEOUT"));
		}
		else
		{
			if (dht22.temp < 0)
			{
				lcdTekst(PSTR("-"));
				lcd_uint8_t(-dht22.temp);
				lcdTekst(PSTR(","));
				lcd_uint8_t(dht22.temp_ulamek);
				lcdTekst(PSTR("C ZEW "));
			}
			else
			{
				lcd_uint8_t(dht22.temp);
				lcdTekst(PSTR(","));
				lcd_uint8_t(dht22.temp_ulamek);
				lcdTekst(PSTR("C  ZEW "));
			}
			
			lcd_uint8_t(dht22.wilgotnosc);
			lcdTekst(PSTR(","));
			lcd_uint8_t(dht22.wilgotnosc_ulamek);
			lcdTekst(PSTR("%"));
		}
		lcdXY(0,1);
		if (dht11_odczytaj(&dht11) == DHT11_TIMEOUT)
		{
			lcdTekst(PSTR("TIMEOUT"));
		}
		else
		{		
			lcd_uint8_t(dht11.temp);
			lcdTekst(PSTR(","));
			lcd_uint8_t(dht11.temp_ulamek);
			lcdTekst(PSTR("C  WEW "));
			lcd_uint8_t(dht11.wilgotnosc);
			lcdTekst(PSTR(","));
			lcd_uint8_t(dht11.wilgotnosc_ulamek);
			lcdTekst(PSTR("%"));
		}
		
		
		
		_delay_ms(3000);
    }
}

Załączniki:

Pełny kod źródłowy programu

Obudowa w formacie STL do wydrukowania

Schemat i wzór ścieżek PCB (KiCAD)