Redakcja: Dondu
Na forum często poruszany jest temat odczytu i wyświetlenia temperatury z czujnika np. DS18B20. Wielki problem stwarza początkującym konwersja do postaci string bez użycia operacji zmiennoprzecinkowych, która zabierają niepotrzebnie tak cenną pamięć programu (czasami nawet kilka kB):
oskar777 Czujnik temperatury, prośba z znalezieniu błędu
Float mi potrzebny by odczytać (i wyświetlić) temperaturę z przecinkiem, nie wiem jak zrobić to inaczej.
Float mi potrzebny by odczytać (i wyświetlić) temperaturę z przecinkiem, nie wiem jak zrobić to inaczej.
Jak więc rozwiązać to bez użycia FLOAT i zyskać kB pamięci?
Przypatrzmy się formatowi temperatury udostępnianym przez DS18B20:
LSB - młodszy bajt
MSB - starszy bajt
Z - znak temperatury (1-ujemna, 0-dodatnia) dlaczego aż na tylu bitach? Pytaj projektantów DS18B20 :-)
Bity 0...3 - część ułamkowa
Bity 4...10 - część całkowita
16 bitowa prezentacja formatu temperatury DS18B20 w kodzie uzupełnień do 2 – U2
Odpowiednie bity mają następujące wartości (wagi bitów):
Dodatnia temperatura
Obliczenie dodatniej temperatury polega na dodaniu powyższych wartości dla bitów, które maja wartość równą 1.
Przykład:
Odczytano LSB=0x91 oraz MSB=0x01. Po złożeniu MSB i LSB otrzymujemy:
Odczytany pomiar = 0x0191 = 0b0000 0001 1001 0001
Dodajemy wartości (wagi) bitów zawierających jedynkę:
T = bit8 + bit7+ bit4 + bit0 = 16 + 8 + 1 + 0,0625 = 25,0625°C
Prawda, że proste? :-)
Ujemna temperatura
Do zrozumienia formatu temperatur ujemnych przyda nam się znajomość kodu U2.
Co to jest ten kod U2?
Jest to format zapisu liczb całkowitych w systemie dwójkowym. Format jest bardzo prosty, liczby dodatnie od 0 do połowy zakresu mają swoje normalne wartości, np. dla liczb 8 bitowych będą to liczby od 0 do 127. Liczby od połowy zakresu do jego końca reprezentują liczby ujemne, czyli dla liczb 8 bitowych będą to liczby od 128 do 255. Im mniejsza wartość tym większa wartość bezwzględna liczby.
Przykłady:
0b01111111 = 127
0b00000010 = 2
0b00000001 = 1
0b00000000 = 0
0b11111111 = -1
0b11111110 = -2
0b10000000 = -128
Z przykładów tych można zaobserwować, że najbardziej znaczący bit wskazuje znak liczby, jeśli ma wartość 1 to liczba jest ujemna i właśnie tak samo jest w formacie temperatury czujnika DS z tym, że znak liczby Z został powielony na kilku bitach, co nie jest do końca prawdą. Jest to kwestia umowna, bo gdyby przyjąć, że znak jest tylko na najstarszym bicie to i tak by to niczego nie zmieniło z punktu widzenia liczby w kodzie U2.
Jak wyciągnąć wartość bezwzględną z liczby ujemnej w kodzie U2?
Wystarczy liczbę zanegować i dodać 1. Skąd wzięło się te 1? Jest potrzebne, bez tego 1 mamy wtedy dwa zera -0 i +0, a to jest już kod U1.
0b00000000 = +0
0b11111111 = -0
Dodając 1 pozbywamy się drugiego zera i mamy kod uzupełnień do dwóch (U2).
Co oznacza „uzupełnień do dwóch”?
Analogii można szukać w warzywniaku obok. Mamy zapłacić 64zł, dajemy stówę i co robi sprzedawca przy wydawaniu reszty? Uzupełnia do 100 czyli:
64zł + 1zł + 5zł + 10zł + 20zł = 100zł
A jak ma to się do „uzupełnień do 2”?
Liczba w kodzie U2 może mieć dowolną, większą od 0 ilość bitów. Im więcej bitów tym większe wartości bezwzględne można na takiej liczbie zapisać. Definicja kodu U2 jest oparta na najmniejszej możliwej ilości bitów, czyli dla liczby 1 bitowej. Taka liczba może przyjąć tylko dwie wartości, a z wcześniejszych przykładów wiemy, że na najstarszym bicie zapisany jest znak liczby.
W tym przypadku ten jeden bit jest również najstarszym bitem i reprezentuje znak liczby, więc w kodzie U2 taka liczba przyjmie wartości 0 i -1. Przeliczenie 1 bitowej liczby ujemnej na kod U2 polega na uzupełnieniu bezwzględnej wartości tej liczby do 2 i stąd kod uzupełnień do 2. W praktyce łatwiej jest zastosować odejmowanie.
0U2 = 0
-1U2 = 2 – 1 = 0b1
Dla n bitowej liczby zamiast 2 podstawiamy 2n czyli dla 2 bitowej będzie to tak wyglądać:
1U2 = 0b01
0U2 = 0b00
-1U2 = 4 – 1 = 0b11
-2U2 = 4 – 2 = 0b10
W formacie temperatury z DS18B20 część całkowita została przesunięta o 4 bity w kierunku starszych bitów, a w miejsce tych 4 bitów została wsunięta część ułamkowa, również zapisana w kodzie U2.
Część całkowita w połączeniu z częścią ułamkową tworzą jedność podlegającą zasadom kodu U2, a z zasad tych wynika, że aby uzyskać wartość bezwzględną liczby ujemnej wystarczy całość zanegować oraz dodać 1.
Jak obliczyć część ułamkową?
Część ułamkowa zajmuje 4 bity więc może przyjąć 16 wartości. Każda z tych wartości to 1/16 czyli 0,0625.
Aby uzyskać część ułamkową w postaci liczby całkowitej łatwej do konwersji na tekst, wystarczy pomnożyć wartość tych 4 bitów przez 625. Uzyskana wartość po przetworzeniu na tekst będzie reprezentować część ułamkową temperatury z dokładnością do 1/10000, czyli dla przykładu 25,3125.
Taka dokładność nie jest nam potrzebna, w przypadku temperatury wystarczy jedna lub dwie cyfry po przecinku. Jeśli decydujemy się na jedną cyfrę po przecinku to uzyskaną wartość dzielimy przez 1000 i temperaturę wyświetlamy w postaci X.X°C, jeśli zdecydujemy się na dwie cyfry po przecinku to uzyskaną wartość dzielimy przez 100 i temperaturę wyświetlamy w formacie X.XX°C.
Część całkowita
Część całkowita temperatury z formatu DSa nie wymaga przeliczeń, wystarczą tylko odpowiednie przesunięcia bitowe.
Kod C
Przekonwertujmy teorię na kod.
typedef struct { unsigned char Calkowita; unsigned char Ulamek; unsigned char Znak; }TTemperatura; char * DSTempToStr(char *buf, unsigned char DStempLSB, unsigned char DStempMSB) { char * ret = buf; TTemperatura temp; unsigned short DStemp = (DStempMSB << 8 ) | DStempLSB; temp.Znak = DStempMSB >> 7; if( temp.Znak ) { DStemp = ~DStemp + 1; } temp.Calkowita = ( unsigned char )(( DStemp >> 4 ) & 0x7F ); temp.Ulamek = ( unsigned char )((( DStemp & 0xF ) * 625) / 100 ); if( temp.Znak ) { *buf++ = '-'; } buf = ltoaz( temp.Calkowita, buf, 10, 0 ); *buf++ = '.'; buf = ltoaz( temp.Ulamek, buf, 10, 2); *buf++ = 'C'; *buf = '\0'; return ret; }
Funkcja ltoaz()
W powyższym kodzie używana jest pomocnicza funkcja ltoaz() do konwersji 32bitowej wartości na tekst:
char * ltoaz( unsigned long val, char *buf, int radix, int dig ) { char tmp[ 32 ]; int i = 0; if(( radix >= 2 ) && ( radix <= 16 )) { do { tmp[ i ] = ( val % radix ) + '0'; if(tmp[ i ] > '9' ) tmp[ i ] += 7; val /= radix; i++; }while( val ); while(( i < dig ) && ( i < 32 )) { tmp[ i++ ] = '0'; } while( i ) { *buf++ = tmp[ i-1 ]; i--; } } *buf = 0; return buf; }gdzie:
val – wartość do konwersji
buf – wskaźnik na bufor znaków
radix - liczba unikalnych cyfr (system)
dig – minimalna ilość cyfr (zera wiodące)
return – wskaźnik na koniec stringu
Użycie funkcji DSTempToStr
Jeżeli Twoja funkcja wyświetlająca na LCD wygląda tak:
PrintToLcd( char * buf, int x, int y);to temperaturę wyświetlisz tak:
char text[10]; PrintToLcd( DSTempToStr( text, DS_LSB, DS_MSB ),x , y );
lukisio
Zastosowałem i sprawdza się super! Oszczędność około 1,5kb
Zastosowałem i sprawdza się super! Oszczędność około 1,5kb
Autor: Deucalion
Redakcja: Dondu
Witam serdecznie!
OdpowiedzUsuńZ zaciekawieniem przeczytałem cenne informacje odnośnie obsługi DS18B20 - rzeczywiście takie podejście do tematu znacząco zmniejszyło wielkość kody wynikowego - ponad 1,5kB.
Pozwoliłem sobie conieco zmodyfikować kody i dodać coś od siebie. Tutaj http://stsystem.elektroda.eu/index.php/programowanie/avr/60-ds18b20 zapraszam celem obejrzenia rezultatów moich działań.
Pozdrawiam :)
Witam,
OdpowiedzUsuńCzy kolega mógłby zamieścić przykład uzyskiwania wyniku w takiej formie żeby dało się go wyświetlić na segmentowym wyświetlaczu led?
Pozdrawiam