Jak zaprogramować czujnik temperatury DS18B20 na AVR ATTINY

Zaprogramowaliśmy już kilka ciekawych elementów, między innymi wysłaliśmy sygnał za pomocą gotowej biblioteki do wyświetlacza tak żeby coś wyświetlał. Napisaliśmy także własną funkcję do tworzenia odpowiedniego sygnału do sterowania serwomechanizmem. Dzisiaj pójdziemy krok dalej i zajmiemy się odbieraniem sygnału z czujnika.

Jeśli chodzi o odbieranie sygnału, wydaje mi się że dobrym przykładem będzie czujnik temperatury. Akurat mam pod ręką czujnik temperatury DALLAS 18B20 1647C4 +300AC jak na załączonym obrazku poniżej. Czujnik ten kosztuje kilka złotych więc podejrzewam że jeśli go kupisz twój budżet wytrzyma.

Jak podłączyć czujnik temperatury 18B20 do AVR ATTINY84A-PU?

Podejmijmy teraz próbę podłączenia naszego czujnika. Najlepiej na początku znaleźć notę katalogową i kilka informacji na temat jego działania. Najważniejsze informacje to:

  • Zakres temperatur -50…125°C 1-Wire
  • Napięcie zasilania: 3…5,5V

Z tego co widać powyżej nie powinno być problemu jeśli zasilimy sobie układ z programatora co jest najwygodniejsze do testów. Później można to na przykład podpiąć do dwóch albo trzech baterii paluszków 1,5V połączonych szeregowo, co powinno dać napięcie 3 lub 4,5V co mieści się w zakresie.

Jeśli udało się Tobie znaleźć kartę katalogową to już na pierwszej stronie możesz znaleźć opis poszczególnych nóżek. Dość jasno z niego wynika, że do pierwszej nóżki podciągamy GND do trzeciej VCC czyli plus a do środkowej podepniemy jeden z PINów naszego mikrokontrolera. Jeśli jeszcze masz problem z określeniem która nóżka jest która zauważ że obudowa jest zaokrąglona z jednej strony. Odwróć widok tak żeby płaska strona była na górze i już wszystko powinno być jasne.

Tylko pamiętaj że na poniższym schemacie jest pokazany widok od dołu czyli od tej strony nóżek. Jak go wetkniesz i spojrzysz z góry to będzie odwrotnie (oczywiście mi się udało podłączyć odwrotnie za pierwszym razem i dało się poczuć lekki zapach spalenizny po chwili – nie pomyl się!).

Obrazek posiada pusty atrybut alt; plik o nazwie czujnik_temperatury_schemat.jpg

Jak działa protokół 1-WIRE?

Do przesyłania danych z czujnika temperatury potrzebujemy tylko jednego PINu mikrokontrolera (co więcej możemy do tego jednego PINu podpiąć kilka czujników i będzie to działać). Co ważne tego jednego PINu używamy zarówno do wysyłania jak i odbierania danych.

Za chwilę przystąpimy do analizy, krok po kroku jak odbywa się komunikacja z naszym mikrokontrolerem ale wcześniej kilka słów jak to działa w teorii.

  1. Reset – pierwszą czynnością w komunikacji z czujnikiem temperatury DS18b20 jest wysłanie specjalnego sygnału reset do czujnika. Jest to inicjacja komunikacji. Sygnał powinien się rozpocząć od ustawienia stanu niskiego na ok 480us (mikrosekund). Po tym czasie linia powinna wrócić do stanu wysokiego, po ok 70us sprawdzamy stan linii. Jeśli wszystko jest połączone dobrze to powinien być stan niski. Następnie należy odczekać jeszcze 410 us przed dalszą komunikacją.
  2. Odczyt bitu – Aby odczytać bit należy to zainicjować poprzez ustawienie za pomocą mikrokontrolera stanu niskiego na 6us. Po ok 9us sprawdzamy stan linii i jeśli będzie niski to oznacza 0 a jeśli wysoki 1. Przed następną transmisją trzeba odczekać 55us.
  3. Wysyłanie bitu: Aby wysłać jedynkę należy ustawić stan niski na 6us i później stanu wysokiego na 64us. Aby wysłać zero należy ustawić stan niski na 60us a później stan wysoki na 10us.

Jak zapewne zauważyłeś sekwencje wysłania bitu podobnie jak w przypadku odczytu trwają po 70us.

Jak krok po kroku przetestować komunikację?

Spróbujmy teraz przeanalizować krok po kroku komunikację i odczyt temperatury z naszego czujnika DS18B20. W tym celu wykorzystamy wyświetlacz LCD z Jak zaprogramować wyświetlacz LCD (HD44780) na AVR ATTINY84A oraz czujnik podpięty pod PIN PB1. Zrobię program, który będzie działał w pętli wykonując pomiar co ok 1 sekundę. Mam wyświetlacz na 4 linie ale można zrobić to także na takim na 2.

Na początek w mojej funkcji głównej ustawię sobie licznik, który będzie liczył ilość pomiarów oraz wyświetlę tę informację na moim wyświetlaczu. Dalszy kod testów będę umieszczał zaraz przed odczekaniem sekundy na kolejne wykonanie kodu. Na ten moment moja główna funkcja programu będzie wyglądać jak poniżej (pamiętaj tylko o wykorzystaniu biblioteki do wyświetlacza LCD).

int main(void)
{
    LCD_Setup();
    int counter = 0;
    while (1) 
    {  
	counter++;
	LCD_Clear();
	LCD_GotoXY(0, 0);
	LCD_PrintString("Pomiar: ");
	LCD_PrintInteger(counter);
	//W tym miejscu będziemy testować czujnik
	_delay_ms(1000);
		
    }
    return 0;
}

Na początku sprawdźmy jak nasz mikrokontroler widzi stan na linii zanim podejmiemy jakiekolwiek działania.

if(PINB & (1 << PB1)) { //odczytuję odpowiedź
	LCD_GotoXY(0, 1);
	LCD_PrintString("Wysoki! ");
} else {
	LCD_GotoXY(0, 1);
	LCD_PrintString("Niski! ");
}
_delay_ms(1000);

Na ten moment powinieneś być już pewien że na twojej linii jest wysokie napięcie. Oczywiście o ile wszystko dobrze jest podłączone.

Reset DS18B20 przez 1-WIRE

Teraz sprawdzimy czy nasz czujnik w ogóle będzie odpowiadał na nasz sygnał. W tym celu trzeba będzie wykonać wcześniej opisaną sekwencję resetu. Czyli ustawić stan niski na linii, odczekać 480us, zmienić stan z powrotem na wysoki i poczekać 70 mikrosekund. Po tym czasie sprawdzam tak jak powyżej jaki jest stan. Mogę jeszcze odczekać 410 mikrosekund na kolejną akcję o czym za chwilę.

DDRB |= (1 << PB1); //ustawiam na wyjście 
PORTB &= ~(1 << PB1); //ustawiam stan niski
_delay_us(480); //czekam 480 mikrosekund
DDRB &= ~(1 << PB1); //ustawiam stan na wysoki
_delay_us(70); //czekam 70 mikrosekund
if(PINB & (1 << PB1)) { //odczytuję odpowiedź
	LCD_GotoXY(0, 1);
	LCD_PrintString("Wysoki! ");
} else {
	LCD_GotoXY(0, 1);
	LCD_PrintString("Niski! ");
}
_delay_us(410);

Dlaczego sygnał trwa 480us a później czekam właśnie 70us? Bo tak jest napisane w większości źródeł, które widziałem. Żeby dokładniej zrozumieć temat myślę że lepiej zajrzeć do dokumentacji DS18B20. To właśnie z niej możesz się dowiedzieć że potrzebny jest sygnał 480us od mikrokontrolera, później czujnik potrzebuje do 60us na odpowiedź i pomiędzy tymi 60us a 240us obniża stan na linii jeśli wszystko działa poprawnie. Czyli równie dobrze odpowiedź można odczytać po 120us lub 200us.

Funkcja reset w oddzielnym pliku

Skoro wiemy już jak działa reset, to nadszedł czas aby wyodrębnić funkcję reset, która będzie nam zwracała informacja czy reset jest ok. Czyli jeśli mamy stan niski to jest ok i będziemy mogli wykonywać kolejne działania. Dalej będzie trochę trudniej.

Zacznijmy od stworzenia nowego pliku do obsługi komunikacji 1-wire. Nazwijmy plik ONE_WIRE.c i umieścimy go w katalogu głównym projektu (w tym samym co main.c). W pliku tworzymy metodę ONE_WIRE_RESET(), która będzie zwracać 1 przy poprawnej odpowiedzi czujnika i 0 jeśli coś poszło nie tak.

uint8_t ONE_WIRE_RESET(){
	DDRB |= (1 << PB1); //ustawiam na wyjście
	PORTB &= ~(1 << PB1); //ustawiam stan niski
	_delay_us(480); //czekam 480 mikrosekund
	DDRB &= ~(1 << PB1); //ustawiam stan na wysoki
	_delay_us(70); //czekam 70 mikrosekund
	uint8_t result = 1;
	if(PINB & (1 << PB1)) { //odczytuję odpowiedź
		result = 0;
	}
	_delay_us(410);
	return result;
}

Teraz bardzo ważne aby dołączyć ten plik do naszego pliku głównego. Czyli na górze pliku main.c wstawiamy include „ONE_WIRE.c”. Teraz aby sprawdzić czy reset się powiódł wystarczy użyć tylko.

if(ONE_WIRE_RESET()){
	LCD_GotoXY(0, 1);
	LCD_PrintString("Reset OK! ");
}

Jak odczytać adres czujnika DS18B20?

Pierwszą informacją jaką operujemy jest adres czujnika. Jak wcześniej wspominałem do jednej linii możemy podłączyć kilka czujników tego typu i odnosić się do nich po ich adresach. Dzięki temu rozwiązaniu możemy w dowolnym momencie odczytać temperaturę z dowolnego czujnika.

Aby odczytać adres czujnika do linii musi być podpięty tylko jeden czujnik. Teraz będzie trochę trudniej bo musimy najpierw wysłać cały bajt danych będący odpowiednią komendą a później odebrać 8 bajtów adresu. Jak pamiętamy bajt to 8 bitów.

Komendy możemy znaleźć w dokumentacji czujnika tak jak na obrazku poniżej. Jak widać mamy tutaj opisane komendy w dość przystępny sposób jak na dokumentację techniczną oraz mamy ich kody.

Aby odczytać adres czujnika należy wykorzystać komendę Read Rom. Komenda ta ma kod 33h, jest to zapis szesnastkowy i w języku C przekształcamy to na kod 0x33. Bardzo prawdopodobne że poświęcę oddzielny artykuł na kody szesnastkowe, w każdym razie kod ten odpowiada liczbie 51 dziesiętnie.

W skrócie kody te to pary liczb w zapisie szesnastkowym (czyli od 0 do F), każda z tych liczb ma swoją reprezentację na 4 bitach. Na przykład liczba 3 ma kod 0011 binarnie. Aby zamienić to po prostu łączymy kod dwóch trójek czyli mamy 00110011 a zamiana kodu binarnego na dziesiętny to po prostu dodawanie potęg liczby dwa tam gdzie są jedynki jak idziemy od prawej do lewej. Tutaj będzie to 1 + 2 + 0 + 0 + 16 + 32 = 51. To tak bardziej jako ciekawostka bo program sam sobie zamieni kod szesnastkowy na binarny.

Jako że musimy jakoś wysłać komendę do naszego czujnika potrzebujemy odpowiednią metodę będącą w stanie wysłać bajt danych za pomocą komunikacji 1-WIRE. Analogicznie jak wcześniej napiszemy dwie metody ONE_WIRE_SEND_BIT(uint8_t bit) oraz ONE_WIRE_SEND_BYTE(uint8_t data). Do pierwszej funkcji będziemy przekazywać pojedynczy bit i wysyłać go wykorzystując algorytm opisany wcześniej. Do drugiej funkcji przekażemy bajt danych. Obie funkcje umieszczamy w pliku ONE_WIRE.c

void ONE_WIRE_SEND_BIT(uint8_t bit){
    DDRB |= (1 << PB1); //ustawiam na wyjście
    PORTB &= ~(1 << PB1); //ustawiam stan niski
    if(bit){ //sprawdzam czy wysyłam 0 czy 1
	_delay_us(6); //dla 1 sygnał trwa 6us
	DDRB &= ~(1 << PB1); //ustawiam stan na wysoki
	_delay_us(64); //czekam 64us do końca
    } if(!bit){
	_delay_us(60); //dla zera sygnal 60us
	DDRB &= ~(1 << PB1); //ustawiam stan na wysoki
	_delay_us(10); //czekam 10us do końca
    }
}

Teraz czas na metodę do wysłania całego bajtu. Oczywiście wykorzystamy metodę wyżej do przesyłania kolejnych bitów a więc będzie to prosta funkcja z pętlą.

void ONE_WIRE_SEND_BYTE(uint8_t data){
	for(int8_t i = 0; i <= 7;i++) {
		ONE_WIRE_SEND_BIT(data & (1 << i));
	}
}

Tutaj być może trochę trudniejsze do zrozumienia będzie przesunięcie bitu czyli składnia data & (1 << i) o co tutaj chodzi? Wyobraź sobie że data to zmienna przechowująca na ten moment liczbę 0x33 czyli 51, w systemie binarny będzie to po prostu ciąg 0011 0011 czyli nasze 8 bitów komendy. W pętli zmienną i przy każdym przebiegu zwiększamy o 1. Czyli w tym przypadku przy każdym przebiegu pętli będziemy się przesuwać od prawo do lewo odczytując kolejny bit przesuwając się o i miejsc od prawej. Będziemy po kolei wysyłać 1 1 0 0 1 1 0 0.

Po wysłaniu komendy powinniśmy już o otrzymać jakąś odpowiedź od naszego czujnika. Spróbujemy sobie wyświetlić 20 pierwszych bitów adresu na naszym wyświetlaczu. Akurat mój ma 20 znaków w rzędzie więc będzie to stosunkowo proste.

int result[20]; //tworzę tablicę na bity
if(ONE_WIRE_RESET()){ //wysyłam reset
    ONE_WIRE_SEND_BYTE(0x33); //Komenda odczytu adresu ROM
    for(int i = 0;i<20;i++){ //w pętli odczytuję bity
	DDRB |= (1 << PB1); //ustawiam na wyjście
	PORTB &= ~(1 << PB1); //ustawiam stan niski
	_delay_us(6); //czekam 480 mikrosekund
	DDRB &= ~(1 << PB1); //ustawiam stan na wysoki
	_delay_us(9);
	if(PINB & (1 << PB1)) { //odczytuję odpowiedź
	    result[i] = 1;
	} else {
	    result[i] = 0;
	}
	_delay_us(55);//czekam do końca bitu
    }
}
		
LCD_GotoXY(0, 1);
for(int i = 0;i<20;i++){
    LCD_PrintInteger(result[i]);
}

Jeśli otrzymałeś na wyświetlaczu ciąg zer i jedynek to najprawdopodobniej wszystko masz dobrze połączone i napisane i możemy przejść do napisania funkcji do odczytu poszczególnych bitów i bajtów. I jeszcze obrazek jak by to mogło wyglądać.

Zanim ruszymy dalej spróbujmy z powyższego ciągu wyłuskać 2 pierwsze bajty adresu czujnika. Mamy tutaj 20 bitów odczytanych po kolei, czyli 2,5 bajta. Trzeba pamiętać że bity są odczytywane od najmniej znaczącego czyli najpierw dzielimy ciąg na 8 bitowe co da 00010100 11001010 1000 a później odwracamy bity co powinno dać: 00101000 01010011 i mamy 2 liczby w postaci binarnej. Po zamianie na system dziesiętny otrzymamy 8 + 32 = 40 oraz 1 + 2 + 16 + 64 = 83. Dzięki tym obliczeniom będziemy w stanie sprawdzić czy nasza funkcja do odczytywania bajtów będzie działać poprawnie.

To teraz aby było łatwiej spróbujemy napisać metodę ONE_WIRE_READ_BIT(void), za pomocą której odczytamy bit z naszego czujnika. Już wiemy w jaki sposób to zrobić bo napisaliśmy już to wyżej w pętli.

uint8_t ONE_WIRE_READ_BIT(void){
    DDRB |= (1 << PB1); //ustawiam na wyjście
    PORTB &= ~(1 << PB1); //ustawiam stan niski
    _delay_us(6); //czekam 480 mikrosekund
    DDRB &= ~(1 << PB1); //ustawiam stan na wysoki
    _delay_us(9);
    uint8_t result = 0;
    if(PINB & (1 << PB1)) { //odczytuję odpowiedź
	result = 1;
    }
    _delay_us(55);//czekam do końca bitu
    return result;
}

Żeby sprawdzić czy dalej działa w naszej pętli main.c tam gdzie odczytujemy bity można na chwilę wypróbować naszą funkcję.

for(int i = 0;i<20;i++){
   result[i] = ONE_WIRE_READ_BIT();
}

Teraz jeszcze możemy potrzebować funkcji do odczytania całego bajtu danych. Adres to 8 bajtów czyli 64 bity, spróbujemy sobie to zaraz odczytać w jakiś sposób. Podejmijmy próbę napisania funkcji ONE_WIRE_READ_BYTE(void). Będzie to głównie pętla wykorzystująca funkcję ONE_WIRE_READ_BIT().

uint8_t ONE_WIRE_READ_BYTE(void)
{
    uint8_t byte = 0; //definiujemy zmienną na wynik i ustawiamy wartość na 0
    for(int8_t i = 0; i < 8; i++) {
	byte |= (ONE_WIRE_READ_BIT() << i); //zapisujemy kolejne bity
    }
    return byte;
}

Co dzieje się w pętli? Wcześniej zdefiniowaliśmy zmienną byte i ustawiliśmy na zero czyli 00000000 i teraz w pętli po kolei ustawiamy wartości zwrócone przez funkcję ONE_WIRE_READ_BIT(). Jeśli odczytaliśmy 1 to w miejsce i wstawiamy 1 a jeśli 0 to 0 i tak budujemy jakąś liczbę w systemie binarnym. Dzięki temu powinny nam wyjść takie same liczby jak liczyliśmy wyżej. Czyli pierwsze dwie jakie odczytamy to będą 40 oraz 83. W przypadku innego czujnika adres powinien być inny.

Spróbujmy wyświetlić cały adres na ekranie. Odczytam poszczególne bajty, zapiszę do tablicy a potem wyświetlę oddzielając kropką poszczególne bajty adresu. Nie mieściło mi się w jednej linii więc rozbiłem na dwie. Modyfikujemy teraz funkcję główną programu.

int result[8]; //tworzę tablicę na bity
if(ONE_WIRE_RESET()){
    ONE_WIRE_SEND_BYTE(0x33); //Komenda odczytu adresu ROM
    for(int i = 0;i<8;i++){
	result[i] = ONE_WIRE_READ_BYTE(); //odczyt kolejnych bajtów
    }		
}
		
LCD_GotoXY(0, 1);
for(int i = 0;i<8;i++){
    if(i == 4){
	LCD_GotoXY(0,2);//w połowie przechodzę do drugiej linii LCD
    }
    LCD_PrintInteger(result[i]); //wyświetlam bajt
    LCD_PrintString("."); //oddzielam kropką
}

I wynik jakby to mogło wyglądać

Wiemy już jak wymieniać informacje z czujnikiem DS18B20, wiemy gdzie w dokumentacji znaleźć komendy i jak się nimi posługiwać. Teraz nadszedł czas na podjęcie próby odczytania temperatury z naszego czunika.

Jak odczytać temperaturę z czujnika DS18B20?

Ogólny algorytm odczytu temperatury polega na zainicjowaniu odczytu, odczekaniu chwilę i odczytaniu pomiaru z pamięci czujnika. Odczytujemy 2 bajty danych, które należy później odpowiednio przeliczyć. Spróbujmy napisać odpowiedni kod.

int result[8]; //tworzę tablicę na bity
    if(ONE_WIRE_RESET()){
	ONE_WIRE_SEND_BYTE(0xcc); //Komenda skip ROM
	ONE_WIRE_SEND_BYTE(0x44); //rozpoczęcie pomiaru temperatury
	_delay_ms(750); //czekam aż pomiar się zrobi
	ONE_WIRE_RESET(); //zaczynam nowe zapytanie
	ONE_WIRE_SEND_BYTE(0xcc); //Komenda skip ROM
	ONE_WIRE_SEND_BYTE(0xbe); //Inicjuję komendę odczytu z pamięci
	uint16_t LSB = ONE_WIRE_READ_BYTE(); //odczyt LSB, pierwszy bajt pamięci
	uint16_t MSB = ONE_WIRE_READ_BYTE(); // odczym MSB, drugi bajt pamięci
	ONE_WIRE_RESET(); //koniec odczytu
	uint16_t resultTemp;
	resultTemp = ((MSB & 0x07) << 8) + LSB;
	//dla ujemnej temperatury
	if(MSB & 0xF0) {
		resultTemp = 2048 - resultTemp;
	}
	uint16_t temperature = resultTemp >> 4;
	//ułamek
	uint16_t frac = ((((uint32_t)resultTemp) & 0x0F) * 10);
		
	result[0] = temperature;
	result[1] = frac;	
    }
		
	LCD_GotoXY(0, 1);
	for(int i = 0;i<2;i++){
		LCD_PrintInteger(result[i]);
		if(i == 0){
			LCD_PrintString(".");
		}
	}

I na koniec wynik ostateczny z termometra.

To powyżej pokazuje tylko jak odczytać temperaturę z czujnika, kilka eksperymentów krok po kroku. Aby wykorzystać to w jakimś projekcie potrzeba jeszcze trochę pracy żeby ten kod poukładać, nieco zoptymalizować i przydało by się jeszcze trochę uzupełnić temat o odczyt sumy kontrolnej dla weryfikacji wyniku a także rozpracować podłączenie kilku czujników do jednej linii.