Witam Wszystkich.
Jest to mój pierwszy artykuł na majsterkowie. Raczej byłem użytkownikiem typu read-only i postanowiłem to zmienić.
Bardzo dużo potrzebowałem czasu żeby ruszyć z elektroniką, podchodziłem już chyba 2 razy, dopiero za 3 razem udało mi się zrobić już coś praktycznego i z głową.
Chciałbym wam zaprezentować połączenie mikrokontrolera z rodziny AVR (Atmega8) z czujnikiem temperatury i wilgotności (DHT11).
W artkule bardziej bym chciał się skupić na programowaniu i napisaniu własnej biblioteki także wydaje mi się że artykuł będzie bardziej skierowany dla początkujących programistów układów.
Na co dzień pracuję jako programista c# (głównie aplikacje biznesowe) ale od zawsze mnie ciągnęło w stronę elektroniki i w końcu cieszę się że udało mi się ruszyć.
Wykaz części:
-Mikrokontroler Atmega8 (L)
-Czujnik temperatury i wilgotności DHT11
-Wyświetlacz HD44780
-Gold piny
-Rezystor 10k
Przygotowanie kodu:
Ważna informacja, pierwotny kod ściągnąłem od tego Pana – http://davidegironi.blogspot.com/2013/02/reading-temperature-and-humidity-on-avr.html
Stwierdziłem że chciałbym się zagłębić dokładnie w ten kod i zacząłem wszystko analizować co tam się dzieję, troszkę zrefaktoryzowałem i pomyślałem że się podzielę przemyśleniami.
Żeby stworzyć bibliotekę musimy utworzyć dwa pliki, plik nagłówkowy .h i plik z kodem .c.
W pliku nagłówkowym podajemy dwie funkcje z których skorzystamy:
1 2 |
extern int8_t DHT11_Read(uint8_t dataType); // główna funkcja która odczytuje dane static void ResetPort(void); // funkcja statyczna która nie będzie dostępna poza plikiem, resetująca stan pinu czujnika. |
Zadeklarujemy sobie również kilka pomocniczych dyrektyw preprocesora, dzięki temu możemy napisać uniwersalny kod a jedyne zmiany będą w tych właśnie dyrektywach.
1 2 3 4 |
#define DHT11_DDR DDRC #define DHT11_PORT PORTC #define DHT11_PIN PINC #define DHT11_INPUTPIN PC5 |
W trakcie kompilacji wszystkie wystąpienie DHT11_DDR będą podmienione na DDRC itd.
Operacje logiczne:
Aby wymusić stan niski lub wysoki dla pinu mamy kilka możliwości. Aby rozwiązanie było w miarę uniwersalne można wykorzystać operacje bitowe OR i AND. Zajmijmy się najpierw ustawieniem stanu wysokiego, do rozważenia mamy dwa przypadki, albo na pinie jest stan niski albo wysoki. Nasz port PC5 w systemie dwójkowym wygląda tak – 00100000.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
Przypadek dla OR - jeśli jest ustawione 0 na porcie 11011111 // PIN 5 ma stan niski - 0 00100000 // PC5 11111111 Przypadek dla OR - jeśli jest ustawione 1 na porcie 00100000 // PIN 5 ma stan wysoki - 1 00100000 // PC5 00100000 Przypadek dla AND - jeśli jest ustawione 0 na porcie 11011111 // PIN 5 ma stan niski - 0 00100000 // PC5 00000000 Przypadek dla AND - jeśli jest ustawione 1 na porcie 00100000 // PIN 5 ma stan wysoki - 1 00100000 // PC5 00100000 |
Jak widać, w tym wypadku OR rozwiązuje problem.
Jeśli byśmy chcieli ustawić stan niski to już się sprawa trochę komplikuje, żadne z powyższych operacji nie rozwiązuje naszego problemu. Można jednak wykorzystać kolejną operację jaką jest odwrócenie.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
Przypadek dla OR - jeśli jest ustawione 0 na porcie 11011111 // PIN 5 ma stan niski - 0 11011111 // Odwrócony PC5 11011111 Przypadek dla OR - jeśli jest ustawione 1 na porcie 11111111 // PIN 5 ma stan wysoki - 1 11011111 // Odwrócony PC5 11111111 Przypadek dla AND - jeśli jest ustawione 0 na porcie 11011111 // PIN 5 ma stan niski - 0 11011111 // Odwrócony PC5 11011111 Przypadek dla AND - jeśli jest ustawione 1 na porcie 00100000 // PIN 5 ma stan wysoki - 1 11011111 // Odwrócony PC5 00000000 |
Teraz widać że wykonując zaprzeczenie i AND jesteśmy w stanie ustawić stan niski na żądanym pinie.
Implementacja:
Żeby poprawnie podłączyć czujnik DHT11 oraz napisać kod biblioteki, potrzebna nam jest dokumentacja: http://www.micropik.com/PDF/dht11.pdf
Z dokumentacji możemy wyczytać że czujnik zwraca dane w 40 bitach (czyli 5 bajtach).
8 bitów danych wilgotności
8 bitów danych wilgotności
8 bitów danych temperatury
8 bitów danych temperatury
8 bitów suma kontrolna
Ostatni bajt to suma kontrolna, tak żebyśmy byli w stanie potwierdzić czy dobrze odczytaliśmy dane.
Suma 4 pierwszych bajtów powinna być równa sumie kontrolnej.
Pierwsze linijki naszej funkcji to po prostu deklaracja, inicjalizacja zmiennych i przygotowanie portu:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
uint8_t data[5]; //tablica o rozmiarze 5 - czujnik zwraca 5 bajtów uint8_t bit,byte = 0; //zmienne pomocnicze dla pętli memset(data, 0, sizeof(data)); // wypełnia tablicę podanymi wartościami, w tym wypadku jest to 0. ResetPort(); // funkcja "resetująca" port static void ResetPort(void) { DHT11_DDR |= (1<<DHT11_INPUTPIN); // stan wysoki dla pinu w rejestrze DDR DHT11_PORT |= (1<<DHT11_INPUTPIN); // stan wysoki dla pinu w rejestrze PORT - oznacza to że działa on jako wyjście _delay_ms(100); // czekamy 100ms } |
Zgodnie z dokumentacją, jeśli chcemy rozpocząć komunikację z DHT11 musimy podać stan niski na nóżkę danych czujnika. Między innymi dlatego w funkcji ResetPort ustawiamy stan wysoki, tak żeby czujnik nie zaczął nadawać.
When the communication between MCU and DHT11 begins, the programme of MCU will set Data Single-bus voltage level from high to low
and this process must take at least 18ms to ensure DHT’s detection of MCU’s signal, then MCU will pull up voltage and wait 20-40us for DHT’s response.
Aby rozpocząć komunikację z DHT11 musimy podać stan niski na nóżkę danych czujnika i odczekać co najmniej 18ms.
1 2 |
DHT11_PORT &= ~(1<<DHT11_INPUTPIN); // ustawienie stanu niskiego _delay_ms(18); |
Następnie ustawiamy z powrotem stan wysoki oraz ustawiamy na naszym rejestrze DDR PIN aby działał jako wejście tak abyśmy mogli odczytać dane i czekamy 20-40us.
1 2 3 |
DHT11_PORT |= (1<<DHT11_INPUTPIN); // ustawienie stanu wysokiego DHT11_DDR &= ~(1<<DHT11_INPUTPIN); // ustawienie stanu niskiego - pin zacznie działać jako wejście _delay_us(40); |
Once DHT detects the start signal, it will send out a low-voltage-level response signal, which lasts 80us.
Then the programme of DHT sets Data Single-bus voltage level from low to high and keeps it for 80us for DHT’s preparation for sending data.
Jeśli czujnik zrozumiał nasz sygnał powinien ustawić stan niski na nóżce który powinien trwać 80ms. Następnie ustawiany jest stan wysoki które również trwa 80us.
Sprawdźmy więc czy rzeczywiście proces odbywa się w ten sposób. W przypadku błędu możemy zwrócić jakąś liczbę (która nie będzie temperaturą zwróconą przez czujnik). Czujnik zwraca dane z zakresu 0-50 ℃, więc jeśli zwrócimy np. -1 będziemy wiedzieć że nie jest to wartość z czujnika. Gdy dodamy kolejną dyrektywę i w przyszłości zmienimy zdanie że chcemy zwracać inną wartość, wystarczy to zmienić tylko w jednym miejscu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#define DHT11_Error -1 if(DHT11_PIN & (1<< DHT11_INPUTPIN)) { return DHT11_Error; } _delay_us(80); if(!(DHT11_PIN & (1<< DHT11_INPUTPIN))) { return DHT11_Error; } _delay_us(80); |
When DHT is sending data to MCU, every bit of data begins with the 50us low-voltage-level and the length of the following high-voltage-level signal determines whether data bit is “0” or “1”.
Gdy czujnik zaczyna wysyłać dane, ustawia stan niski na nóżce przez 50us, następnie jest ustawiany stan wysoki i w zależności od długości trwania otrzymujemy wartość 0 lub 1 (26-28ms oznacza 0).
Aby uprościć kod wystarczy sprawdzić czy stan wysoki się utrzymuje po 26-28us, jeśli tak to wtedy mamy wartość 1.
Przy odczycie chcemy wypełnić każdy bajt danych dlatego warto skorzystać z pętli for. Pierwsza pętla jest wykonywana 5 razy ponieważ DHT11 zwraca 5 bajtów.
Następnie próbujemy odczytać każdy bit po kolei dlatego musimy zrobić zagnieżdżoną pętlę która wykona się 8 razy (1 bajt – 8 bitów).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
for (byte=0; byte<5; byte++) // DHT11 zwraca 5 bajtów dlatego pętla wykona się 5 razy. { uint8_t result=0; // zmienna która będzie przechowywać bajt wynikowy. for(bit=0; bit<8; bit++) // 1 bajt ma 8 bitów dlatego musimy zrobić pętlę która się wykona 8 razy { while(!(DHT11_PIN & (1<< DHT11_INPUTPIN))); // Czekamy aż czujnik ustawi stan wysoki na nóżce. _delay_us(30); // Jeśli stan wysoki trwa od 26-28us to wtedy czujnik zwraca 0. if(DHT11_PIN & (1<< DHT11_INPUTPIN)) // Jeśli stan jest dalej wysoki to znaczy że musimy zapisać 1. result |= (1<<(7-bit)); // Zwiększamy więc zmienną wynikową o 1 zwrócone przez czujnik. while(DHT11_PIN & (1<< DHT11_INPUTPIN)); // Czekamy aż czujnik przestanie skończy nadawać bit. } data[byte] = result; // Na końcu zmieniamy wartość w tablicy wynikowej na taką którą otrzymaliśmy. } |
Następnie sprawdzamy sumę kontrolną, jeśli jest poprawna to wtedy zwracamy wartość, w innym przypadku zwracamy błąd.
1 2 3 4 5 6 7 8 9 10 11 12 |
if (data[0] + data[1] + data[2] + data[3] == data[4]) { if(dataType ==0) { return data[2]; } else if(dataType == 1) { return data[0]; } } |
Refaktoring:
To już jest takie małe zboczenie zawodowe ale czasami warto przejrzeć nasz kod i sprawdzić czy można coś w nim jeszcze poprawić.
Przykładowo linijka:
1 |
while(DHT11_PIN & (1<< DHT11_INPUTPIN)); // Czekamy aż czunik ustawi stan wysoki na nóżce. |
Jak tak patrzę na ten kod to jest dla mnie trochę mało czytelny. Można przenieść tą część kodu do dyrektywy preprocesora, np.
1 |
#define DHT11_High (DHT11_PIN & (1<< DHT11_INPUTPIN)) |
Dzięki temu nasz kod będzie wyglądał tak:
1 |
while(DHT11_High); |
Według mnie teraz jest czytelniej i wiemy że pętla będzie się wykonywać dopóki jest stan wysoki. Dodatkowo pozbywamy się duplikacji kodu gdyż to samo wyrażenie jest wykorzystane w ifie:
1 |
if(DHT11_PIN & (1<< DHT11_INPUTPIN)) |
które możemy zastąpić
1 |
if(DHT11_High) // jeśli jest stan wysoki to wtedy wykonaj kod |
Załączam pliki programu, dht11.h, dht11.c oraz main.c w ostatecznej wersji u mnie, po refaktoryzacji. Mam nadzieję że komuś się przyda ten artykuł, bardzo dobrze jest rozumieć kod z którego się korzysta.
Pozdrawiam wszystkich majsterkowiczów!
Oj, widać że kolega rzadko coś w C robi…
#define DHT11_High DHT11_PIN & (1<< DHT11_INPUTPIN)
i to napisał zawodowy programista???
Popraw szybko bo oczy bolą, a jeszcze ktoś podobnej konstrukcji będzie chciał w innym kontekście użyć i będzie szukać błędu.
Nie czepiałbym się, ale chciałeś pokazać jak się pisze biblioteki ;)
Witam,
Przyznam szczerzę że z programowaniem niskoobietkowym nie miałem za dużo styczności. Jedyne co mi przychodzi do głowy w tym przypadku to brakuje nawiasów wokół całego wyrażenia, może to powodować potencjalne problemy. Proszę o informację zwrotną, co tutaj jest źle, w imieniu moim i przyszłych użytkowników. Bardzo dziękuję za odpowiedź i zainteresowanie moim artykułem, pozdrawiam.
Przede wszystkim, przez użycie definicji do odkreślania używanych pinów, ta biblioteka nie jest abstrakcyjna, co eliminuje wiele przypadków użycia, na przykład uniemożliwia używanie wielu czujników czy rekonfigurację w trakcie działania programu.
Dalej, tak jak już zauważyłeś nawiasy w definicjach są wskazane. Tworzenia definicji a’la DHT11_Low niepotrzebnie zaciemnia kod. Większość standardów kodowania w C nakazuje minimalizację używania definicji (a szczególnie złożonych wyrażeń). Nazwa DHT11_Low jest myląca – sugeruje, że to jest coś w stylu 4 mniej znaczących bitów. Jeżeli chciałbyś to wyrażenie uprościć, to lepiej jest zrobić coś w stylu “while(! (DHT11_PIN & B_DHT11_INPUTPIN))” gdzie B_DHT11_INPUTPIN to (1 << DHT11_INPUTPIN)
Odnośnie sposobu definiowania rejestrów, to polecam schemat przyjęty przez Intela. Przykładowy plik do znalezienia: PchRegsSpi.h. W tym projekcie to trochę na wyrost ale dobrze się używa.
Dokładnie o to – poprawiłeś i masz zaległą piątkę.
Na wypadek gdyby ktoś miał jakieś wątpliwości czemu się czepiam:
Otóż kod makra jest (po rozwinięciu) wstawiany bezpośrednio do kodu źródłowego programu w miejscu wywołania. Czyli np. konstrukcja typu:
#define sum(a,b) a+b
c=sum(1,2);
będzie po przejściu przez preprocesor wyglądać po prostu:
c=1+2;
Czyli pozornie wszystko jest w porządku…
Ale co będzie w przypadku:
c=sum(1,2) * sum(3,4);
No będzie fatalnie – zostanie to przerobione na:
c= 1 + 2 * 3 + 4;
czyli zamiast oczekiwanego wyniku 21 dostaniemy 11…
Również należy wewnątrz makra stosować nawiasy wokół parametrów dokładnie z tych samych powodów – czyli taka prosta definicja powinna wyglądać tak:
#define sum(a, b) ((a) + (b))
Ogólnie – w wielu przypadkach zamiast makr warto stosować funkcje a atrybutem inline, np.:
int inline sum(int a, int b) {return a + b;}
Niestety – w przypadku C ten sposób nie zdaje egzaminu w przypadku wywołań z różnymi typami argumentów (w C++ jest to możliwe).
DHT11 jest b. kiepskim czujnikiem, stosowałem go przez jakiś czas w łazience i wyglądało to jakby wyniki były przypadkowe i nie zmieniały się w czasie, lepiej zastosować DHT22. Niewiele droższy a o niebo lepszy. Na zdjęciu porównanie.