Cyfrowy termometr z wykorzystaniem płytki atmel_display
O wykorzystaniu płytki atmel_display do pomiaru i wyświetlania temperatury pisałem już bardzo dawno temu. Jednak dopiero ostatnio zmobilizowałem się i w jedno popołudnie powstał prosty termometr o dość szerokim zakresie mierzonych temperatur. Program napisałem w C, korzystając z gotowych bilbliotek, bo ciężko na nowo odkrywać obsługę magistrali 1-wire czy multipleksowanie siedmiosegmentowego wyświetlacza.
Termometr – baza sprzętowa
Osoby, które nie wiedzą czym jest płytka atmel_display, zapraszam do kilku krótkich wpisów z 2012 i 2013 roku:
- Mini płytka testowa AVR z odzysku
- Usprawnienie mini płytki testowej AVR z dekodera Cyfrowego Polsatu
- Test mini płytki AVR z ATtiny2313 – pierwszy program
Do budowy termometry wykorzystałem przerobioną płytkę (z wyprowadzonymi na gniazdo dodatkowymi portami), ale równie dobrze będzie działać wersja oryginalna, zdemontowana z dekodera. Jako czujnik temperatury użyłem tani i popularny układ DS18B20 wraz z rezystorem 4k7Ω, którym linia DATA została podciągnięta do plusa zasilania. Schemat całości wygląda tak:
Posiadacze płytki bez przeróbek linię DQ czujnika powinny podpiąć do portu PB5, PB6 lub PB7, gdyż w oryginale PB4 nie jest wyprowadzony na goldpiny. Taka zmiana będzie wymagała tylko niewielkiej korekty w oprogramowaniu o czym niżej.
Termometr na attiny2313 – kod programu w C
Pisząc program skorzystałem z opisu obsługi czujnika, którego autorem jest Gerard Marull Paretas. Dokument pochodzi z 2007 roku, ale kod praktycznie bez problemu skompilował się na obecnej wersji gcc. Wart przejrzenia dokument można pobrać stąd:
Biblioteka ds18b20.h – definicja portu i pinu pod który jest podpięty port DQ została opatrzona stosownym komentarzem:
/* * ds18b20.h * * Created on: 20 sie 2015 * Author: wojtek */ #ifndef DS18B20_H_ #define DS18B20_H_ // Definiujemy pod jaki pin jest podpieta magistrala 1-wire #define DS18B20_PORT PORTB #define DS18B20_DDR DDRB #define DS18B20_PIN PINB #define DS18B20_DQ PB4 //Pin jako wejscie i wyjscie #define DS18B20_INPUT_MODE DS18B20_DDR &= ~(1<<DS18B20_DQ) #define DS18B20_OUTPUT_MODE DS18B20_DDR |= (1<<DS18B20_DQ) //Ustawianie stanu wysokiego i nieskiego #define DS18B20_LOW DS18B20_PORT &= ~(1<<DS18B20_DQ) #define DS18B20_HIGH DS18B20_PORT |= (1<<DS18B20_DQ) //Komendy dla DS18B20 #define DS18B20_CMD_SKIPROM 0xCC #define DS18B20_CMD_CONVERTTEMP 0x44 #define DS18B20_CMD_RSCRATCHPAD 0xBE #define DS18B20_CMD_WSCRATCHPAD 0x4E //Deklaracje funkcji uint8_t ds18b20_reset(void); void ds18b20_write_bit(uint8_t bit); uint8_t ds18b20_read_bit(void); void ds18b20_write_byte (uint8_t byte); uint8_t ds18b20_read_byte(void); void ds18b20_config (void); uint8_t ds18b20_read_temperature(void); #endif /* DS18B20_H_ */
Plik ds18b20.c – kod jest prosty, nie sprawdzamy tu sumy kontrolnej wyniku, ani adresu czujnika ds18b20. Stąd też wynika ograniczenie obsługi tylko jednego układu 1-wire:
/* * ds18b20.c * * Created on: 20 sie 2015 * Author: wojtek */ #include <avr/io.h> #include <util/delay.h> #include "ds18b20.h" //Reset magistrali i odpowiedz ukladu 1-wire uint8_t ds18b20_reset(void) { uint8_t i; //ustawianie 0 linii 1-wire DS18B20_LOW; DS18B20_OUTPUT_MODE; _delay_us(480); //oczekiwanie na odpowiedz z ds18b20 DS18B20_INPUT_MODE; _delay_us(60); //sprawdzanie czy ds18b20 odpowiedzial (ustawil 0) i = (DS18B20_PIN & (1<<DS18B20_DQ)); _delay_us(420); // zwracamy wynik resetu 1 - blad, ds18b20 nie odpowiedzial, 0 - OK return i; } //zapis bitu na magistrale void ds18b20_write_bit(uint8_t bit) { DS18B20_LOW; DS18B20_OUTPUT_MODE; _delay_us(1); if (bit) DS18B20_INPUT_MODE; _delay_us(60); DS18B20_INPUT_MODE; } //odczyt bitu z magistrali uint8_t ds18b20_read_bit(void) { uint8_t bit = 0; DS18B20_LOW; DS18B20_OUTPUT_MODE; _delay_us(1); DS18B20_INPUT_MODE; _delay_us(14); if (DS18B20_PIN & (1<<DS18B20_DQ)) bit=1; _delay_us(45); return bit; } //zapis bajtu na magistralę void ds18b20_write_byte (uint8_t byte) { uint8_t i=8; while(i--) { ds18b20_write_bit(byte&1); byte>>=1; } } //odczyt bajtu z magistrali uint8_t ds18b20_read_byte(void) { uint8_t i=8, n=0; while(i--) { //przesunięcie w prawo o jedną pozycję i zapisanie odczytanej wartości n>>=1; n |= (ds18b20_read_bit()<<7); } return n; } // ustawienie rozdzielczosci 9-bitowej void ds18b20_config (void) { ds18b20_reset(); ds18b20_write_byte(DS18B20_CMD_SKIPROM); ds18b20_write_byte(DS18B20_CMD_WSCRATCHPAD); ds18b20_write_byte(0); //zapis do TH Register ds18b20_write_byte(0); //zapis do TL Register ds18b20_write_byte(0x1F); //zapis do conf. register rozdzielczosci 9 bit } //odczyt temperatury uint8_t ds18b20_read_temperature(void) { uint8_t temperature[2]; uint8_t digit; ds18b20_reset(); ds18b20_write_byte(DS18B20_CMD_SKIPROM); ds18b20_write_byte(DS18B20_CMD_CONVERTTEMP); while(!ds18b20_read_bit()); //Reset, pominięcie odczytu adresu, komenda odczytu Sratchpada ds18b20_reset(); ds18b20_write_byte(DS18B20_CMD_SKIPROM); ds18b20_write_byte(DS18B20_CMD_RSCRATCHPAD); //Odczyt dwóch bajtów scratchpada i zapis do tablicy temperature temperature[0] = ds18b20_read_byte(); // mlodsze bity LSB temperature[1] = ds18b20_read_byte(); // starsze bity MSB ds18b20_reset(); digit = temperature[0]>>4; //pozbywam sie czesci ulamkowych digit |= (temperature[1]&0xF)<<4; //przesuwam wartosc w lewo o 4 pozycje, sumuje z mlodszym bajtem, mam znak na najstarszym bicie //digit = (temperature[0]&0xF); return digit; //zwracam temperaturę w liczbie całkowitej, znak na najstarszym bicie }
Do obsługi wyświetlacza wykorzystałem kod z książki Mirosława Kardasia – Mikrokontrolery AVR Język C – podstawy programowania. Ze względu na całkowicie odmienny sprzęt, w kodzie musiałem wprowadzić małe zmiany. W obsłudze przerwania inkrementuję dodatkową zmienną check_temp, która jest sprawdzana w pętli głównej – jeśli jest większa niż 200, następuje odczyt z DS18B20. Dlaczego akurat 200? 200*5ms (co tyle jest generowane przerwanie) = 1 sekunda – jest to czas odświeżania wyniku. Wyświetlacz jest multipleksowany z częstotliwością 200Hz. Biblioteka led_7seg.h:
/* * led_7seg.h * * Created on: 20 sie 2015 * Author: wojtek */ #ifndef LED_7SEG_H_ #define LED_7SEG_H_ //definicje portow i pinow #define LED_DATA PORTD //segmenty dolaczone do portu D #define LED_DATA_DIR DDRD //kierunek portu #define ANODY_PORT PORTB //anody podlaczone do portu B #define ANODY_DIR DDRB // kierunek portu B #define CA0 (1<<PB0) //anoda pierwszej cyfry sterowana z PB0 #define CA1 (1<<PB1) #define CA2 (1<<PB2) #define CA3 (1<<PB3) #define SEG_A (1<<0) //przyporzadkowanie numeru portu do segmentu #define SEG_B (1<<1) #define SEG_C (1<<2) #define SEG_D (1<<3) #define SEG_E (1<<4) #define SEG_F (1<<5) #define SEG_G (1<<6) //extern - zmienne beda dostepne w kazdym pliku, ktory dolaczy ten plik naglowkowy, volatile - nie beda odkladane do rejestru extern volatile uint8_t cy0; extern volatile uint8_t cy1; extern volatile uint8_t cy2; extern volatile uint8_t cy3; extern volatile uint8_t check_temp; void d_led_init(void); #endif /* LED_7SEG_H_ */
Zawartość pliku led_7seg.c:
/* * led_7seg.c * * Created on: 20 sie 2015 * Author: wojtek */ #include <avr/io.h> #include <avr/interrupt.h> #include <avr/pgmspace.h> #include "led_7seg.h" volatile uint8_t cy0; //bufory cyfr volatile uint8_t cy1; volatile uint8_t cy2; volatile uint8_t cy3; volatile uint8_t check_temp; //do opoznienia odczytu temperatury const uint8_t cyfry[15] PROGMEM = { ~(SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F), //0 ~(SEG_B | SEG_C), //1 ~(SEG_A | SEG_B | SEG_D | SEG_E | SEG_G), //2 ~(SEG_A | SEG_B | SEG_C | SEG_D | SEG_G), //3 ~(SEG_B | SEG_C | SEG_F | SEG_G), //4 ~(SEG_A | SEG_C | SEG_D | SEG_F | SEG_G), //5 ~(SEG_A | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G), //6 ~(SEG_A | SEG_B | SEG_C | SEG_F), //7 ~(SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G), //8 ~(SEG_A | SEG_B | SEG_C | SEG_D | SEG_F | SEG_G), //9 ~(SEG_A | SEG_B | SEG_F | SEG_G), //stopien ~(SEG_G), //- ~(SEG_A | SEG_D | SEG_E | SEG_F | SEG_G), //E ~(SEG_E | SEG_G), //r 0xFF //pusty }; void d_led_init(void) { LED_DATA_DIR = 0xFF; //wszystkie porty jako wyjscia - sterowanie katodami LED_DATA = 0xFF; //wygaszenie wszystkich katod /* 4 piny portu B jako wyjscia (sterowanie anodami wyswietlaczy) */ ANODY_DIR |= CA0 | CA1 | CA2 | CA3; /* wygaszenie wszystkich wyswietlaczy */ ANODY_PORT &= 0xF0; // ustawienie TIMER0 TCCR0A |= (1<<WGM01); //tryb CTC TCCR0B |= (1<<CS01) | (1<<CS00); //preskaler 64 OCR0A = 77; //dodatkowy podział‚ przez 77, dający przerwanie co ~200Hz TIMSK |= (1<<OCIE0A); //zezwolenie na przerwanie Compare Match } // =================== PROCEDURA OBSŁUGI PRZERWANIA ============================== ISR (TIMER0_COMPA_vect) { static uint8_t licznik = 1; //zmienna do przelaczania kolejno anod wyswietlaczy ANODY_PORT &= 0xF0; //wygasza cyfry, zeby nie bylo podswietlania segmentow if(licznik == 1) LED_DATA = pgm_read_byte( &cyfry[cy0] ); //na port wystaw dane zapalajace segmenty else if (licznik == 2) LED_DATA = pgm_read_byte( &cyfry[cy1] ); else if (licznik == 4) LED_DATA = pgm_read_byte( &cyfry[cy2] ); else if (licznik == 8) LED_DATA = pgm_read_byte( &cyfry[cy3] ); //cykliczne przelaczanie w kazdym przerwaniu, podanie zasilania na anodę 0001-0010-0100-1000 ANODY_PORT = (ANODY_PORT & 0xF0) | (licznik & 0x0F); //licznik na port anod licznik <<= 1; //przesun zawartosc licznika o 1 w lewo i wpisz w licznik if(licznik>8) licznik = 1; check_temp++; //zwiększ o 1 wartość check_temp }
W pętli głównej programu jest odpytywany czujnik temperatury wraz ze sprawdzeniem jego podłączenia oraz przetworzenie odczytanej wartości – przekształcenie, jeśli temperatura jest ujemna i rozbicie wyniku na cyfry.
/* * main.c * * Created on: 20 sie 2015 * Author: wojtek * : v.1.3 */ #include <avr/io.h> #include <avr/interrupt.h> #include <util/delay.h> #include "led_7seg.h" #include "ds18b20.h" int main(void) { d_led_init(); //inicjalizacja wyswietlacza 7-seg //wyswietla wersje softu cy0 = 3; cy1 = 1; sei(); //wlaczenie globalnego zezwolenia na przerwania ds18b20_config(); //ustawia rodzielczosc 9-bitow _delay_ms(500); //petla glowna while(1) { if (check_temp > 200) //pomiar co ~sekunde, check_temp zwiekszane w przerwaniu { if(ds18b20_reset()) // sprawdzam obecnosc ds18b20, jesli nie podlaczony, Err na LED { cy0 = 14; cy1 = 13; //r cy2 = 13; //r cy3 = 12; //E } else { //odczyt temperatury z ds18b20 uint8_t temp = ds18b20_read_temperature(); uint8_t sign = (temp & 0x80) >> 7; //sprawdzanie, czy temp ujemna if (sign) //jesli ujemna { temp = ~temp; //konwersja liczby ujemnej temp ++; //na dodatnia } cy0 = 10; //symbol stopien cy1 = temp%10; //jednosci /* wariant 1, temp + i -, znak - na cy3, brak nieznaczacych '0' cy2 = (temp < 10 ? 14 : temp/10); //dziesiatki lub pusto gdy temp<10 cy3 = (sign == 0 ? 14 : 11); //pusto lub '-' gdy temp < 0 */ // wariant 2, - na cy2 gdy temp <10 if (temp < 10) //jesli mniejsza od 10 { cy2 = (sign == 1 ? 11 : 14); //i ujemna, to minus, jak dodatnia, to pusto } else { cy2 = temp/10; //jesli > 10, to cyfra dzisiatek } cy3 = ((sign == 1 && temp>9) ? 11 : 14);//jestli ujemna i > 10 to '-', inaczej pusto } check_temp = 0; //wyzeruj kontrolke odczytu z ds18b20 } } }
Napis Err na wyświetlaczu oznacza, że nie został podłączony czujnik DS18B20, jego linia DQ lub masa. Jeśli wyświetlany wynik to 85ºC i nie zmienia się, to czujnik 1-wire nie ma podłączonego zasilania +5V. Termometr sprawdziłem w temperaturach od -20ºC do ponad +100ºC, kod programu nie uwzględnia temperatur wyższych niż 99ºC.
Program napisałem w linuksie, korzystając z IDE Eclipse w wersji 4.5.0 (Mars). Jeśli ktoś jest zainteresowany instalacją i konfiguracją środowiska programistycznego dla mikrokontrolerów AVR, to polecam ten wpis: Instalacja narzędzi dla AVR i IDE Eclipse w Linux Mint.
Firmware zajmuje tylko 33,4% (684 bajty) pamięci flash attiny2313 i 7 bajtów w RAM, można więc dopisać dodatkowe funkcje, które zamienią termometr np. w termostat.
Mikrokontroler AVR attiny2313 musi mieć ustawione fabryczne wartości fusebitów, czyli Fuse High Byte: DFh (1101 1111) i Fuse Low Byte: 64h (0110 0100). Płytka zdemontowana z dekodera Polsatu nie posiada zaprogramowanego bitu CKDIV8 (podział częstotliwości zegara przez 8, czyli faktyczne taktowanie 1MHz), stąd konieczność zmiany w Fuse Low Byte z E4h na 64h.
Udostępniam cały projekt, który można wygodnie zaimportować do Eclipsa: atmel_display_termometr.
Działanie termometru na attiny2313
Termometr po włączeniu przez pół sekundy wyświetla wersję firmware’u w postaci 0013, czyli 1.3. Następnie przez krótką chwilę wyświetli się temperatura 85ºC i dalej zmierzona już wartość. Z uwagi na brak kropek (przecinków) na wyświetlaczu, zrezygnowałem z wyświetlania ułamkowych części temperatury – chyba bez szkody dla całości, biorąc pod uwagę dokładność samego czujnika. Poniżej kilka zdjęć z działania termometru:
Implementację wszystkich funkcjonalności kodu przedstawia poniższy film:
Termometr pobiera około 60mA przy zasilaniu napięciem +5V. Układ pozytywnie przeszedł testy kilkudniowej ciągłej pracy, nie było też problemów z obsługą czujnika DS18B20 – podłączałem w sumie 5 sztuk pochodzących z różnych źródeł.
Po publikacji wpisu o atmel_display, Grzegorz podesłał link do swojego programu termometru, który można znaleźć tutaj: https://bitbucket.org/grzechu81/dsb616_thermometer/src/. Nie testowałem tego kodu w mikrokontrolerze, ale widać tu m.in. funkcję sprawdzania crc. Inne przykłady realizacji termometru na tej płytkce (głównie Bascom) można znaleźć na elektrodzie. Napiszcie, jeśli wykorzystujecie atmel_display w inny sposób, lub macie pomysł na jakiś układ.
Mam pomysł prosty licznik do nawijarki +1 DO 9999
Do tego ta płytka nada się idealnie.