Piszemy własną bibliotekę dla wyświetlacza LCD(HD44780) na AVR atmega88PA
Uruchomiliśmy już wyświetlacz LCD ze sterownikiem HD44780 wykorzystując do tego gotową bibliotekę w artykule Jak zaprogramować wyświetlacz LCD (HD44780) na AVR ATTINY84A . Polecam dzisiejsze ćwiczenie rozpocząć od tamtego ćwiczenia żeby nie mieć wątpliwości że podłączenie jest dobre i wszystko działa jak należy. Dzięki temu będziemy mogli wykluczyć potencjalne błędy związane z budową i skupić się na programowaniu.
Do tej pory wykorzystywałem mikrokontroler attiny84a, nadszedł czas by zacząć się oswajać z innymi typami. Dzisiaj wykorzystamy atmega88PA, mikrokontroler ten ma więcej pinów i ogólnie lepsze parametry więc można na nim zrobić trochę więcej. Podpięcie pinów będzie wyglądać trochę inaczej.
Podpięcie LCD 4×20 do mikrokontrolera atmega88PA
Przypomnę schematy wyświetlacza i mikrokontrolera, w przypadku wyświetlacza 4×20 podłączenie wygląda tak samo jak w 2×16, przynajmniej w moim przypadku (rzuć okiem na dokumentacje swojego wyświetlacza). Zasilanie i potencjometr podpinamy tak jak poprzednio.
Podpinam piny mikrokontrolera do następujących pinów wyświetlacza:
RS – PC4
RW – PC5
E – PB0
D4 – PC0
D5 – PC1
D6 – PC2
D7 – PC3
Podpinamy cztery piny danych D4 – D7, resztę zostawiamy wolną. Będziemy przesyłać dane po połowie bajta.
Zaczynamy programowanie wyświetlacza
Mamy tutaj 7 linii podpiętych plus 4 nie wykorzystane. Linie RS, RW i E służą do sterowania a D0 – D7 służą do przesyłania bajtów danych (8 linit na 8 bitów) danych, ale my wykorzystamy tylko D4-D7 czyli będzie przesyłać bajty po połowie. Już za chwilę omówimy sobie zagadnienie dokładniej.
RW służy do ustawiania trybu zapisu lub odczytu. Możemy podłączyć RW do GND dzięki czemu uzyskamy na tym pinie zawsze stan niski, ustawiając w ten sposób na stałe tryb zapisu, co może okazać się wystarczające do obsługi wyświetlacza, ale dzięki podpięciu RW do jednego z pinów mikrokontrolera możemy zaprogramować bibliotekę tak że obsługa wyświetlacza LCD z modułem HD44780 będzie znacznie szybsza. Wynika to z tego że możemy wysłać informację i odczekać określoną ilość czasu aż moduł HD44780 przetworzy dane lub możemy wcześniej wysłać zapytanie czy już przetworzył i zazwyczaj szybciej wysłać kolejną informację.
Linia RS będzie głównie służyła do określania czy przesyłamy kod znaku czy komendę. Linia E będzie służyła do sterowania przesyłanymi bitami.
Definicja portów i pinów
Prace zaczniemy standardowo od utworzenia pliku nagłówkowego gdzie będziemy trzymać wszelkie definicje zmienne w zależności od podłączenia i sprzętu. Dzięki temu, gdy weźmiemy inny AVR i podepniemy inne piny lub inny wyświetlacz z LCD zmieniamy konfigurację w jednym miejscu i od razu powinno działać. Można by tutaj użyć makra, czy wtedy będzie to łatwiejsze do zrozumienia? Na pewno mniej kodu, w przyszłych artykułach pewnie zaczniemy korzystać. Tworzymy plik lcd.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
//definicja pinów i portów <strong>#define LCD_DDR_RS DDRC #define LCD_DDR_RW DDRC #define LCD_DDR_E DDRB #define LCD_DDR4 DDRC #define LCD_DDR5 DDRC #define LCD_DDR6 DDRC #define LCD_DDR7 DDRC #define LCD_PORT_RS PORTC #define LCD_PORT_RW PORTC #define LCD_PORT_E PORTB #define LCD_PORT_4 PORTC #define LCD_PORT_5 PORTC #define LCD_PORT_6 PORTC #define LCD_PORT_7 PORTC #define LCD_PIN_RS (1<<PC4) #define LCD_PIN_RW (1<<PC5) #define LCD_PIN_E (1<<PB0) #define LCD_PIN_4 (1<<PC0) #define LCD_PIN_5 (1<<PC1) #define LCD_PIN_6 (1<<PC2) #define LCD_PIN_7 (1<<PC3)</strong> |
Plik obsługi wyświetlacza
Mamy już podpięte porty i piny to teraz można rozpocząć pisanie funkcji, a będzie ich tutaj kilka potrzebnych. Przede wszystkim potrzebujemy funkcji do przesyłania danych do wyświetlacza i jego inicjalizacji. Zacznijmy od utworzenia pliku lcd.c i umieszczenia w nim nagłówków. Będziemy korzystać z opóźnień.
1 2 3 |
<strong>#include "lcd.h" #define F_CPU 1000000 #include <util/delay.h></strong> |
Podstawowa funkcja jakiej będziemy potrzebować służy do przesłania połowy bajta danych. Zgodnie z założeniem jak wspomniałem wyżej będziemy przesyłać bajty po połowie. Stwórzmy zatem funkcję lcdSendHalfByte(uint8_t data), która będzie pobierała bajt danych i ustawiała odpowiednie stany na pinach mikrokontrolera.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
//funkcja, za pomocą której prześlemy połowę bajta danych //funkcja jest statyczna czyli o zasięgo lokalnym w pliku <strong>static void lcdSendHalfByte(uint8_t data){</strong> //sprawdzam czy pierwszy bit danych jest 1 czy 0 //Na pinie D4 ustawiam zgodnie z tym stan wysoki dla 1 i stan niski dla 0 <strong>if(data & (1<<0)){ LCD_PORT_4 |= LCD_PIN_4; } else { LCD_PORT_4 &= ~LCD_PIN_4; }</strong> //analogicznie sprawdzam kolejne bity i ustawiam piny <strong>if(data & (1<<1)){ LCD_PORT_5 |= LCD_PIN_5; } else { LCD_PORT_5 &= ~LCD_PIN_5; } if(data & (1<<2)){ LCD_PORT_6 |= LCD_PIN_6; } else { LCD_PORT_6 &= ~LCD_PIN_6; } if(data & (1<<3)){ LCD_PORT_7 |= LCD_PIN_7; } else { LCD_PORT_7 &= ~LCD_PIN_7; } }</strong> |
Nie ma tutaj chyba za wiele do tłumaczenia ponad to co w komentarzach. Tworzę cztery warunki i porównuję kolejno pierwsze cztery bity z bajtu. Dalej jeśli jest 1 to ustawiam stan wysoki na przypisanym pinie a jeśli 0 to stan niski.
Teraz jeszcze zanim przejdziemy do funkcji, dzięki której wyślemy cały bajt danych napiszmy małą funkcję ustawiającą linie D4-D7 na wyjście. W przyszłości będziemy używać tych linii także do odbioru danych więc dobrze mieć funkcję ustawiającą kierunek.
1 2 3 4 5 6 7 |
<strong>void lcdDirOut(void){ //ustawiam porty na wyjście LCD_DDR4 |= LCD_PIN_4; LCD_DDR5 |= LCD_PIN_5; LCD_DDR6 |= LCD_PIN_6; LCD_DDR7 |= LCD_PIN_7; }</strong> |
Mamy już funkcję kierunku i przesyłania połowy bajta, więc możemy przejść do poskładania tego w przesyłanie całego bajta.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//przesyłamy cały bajt czyli kod znaku <strong>void lcdSendByte(char data ){</strong> //ustawiam piny na wyjścia <strong>lcdDirOut();</strong> //ustawiam na RW stan niski oznacza to ZAPIS <strong>LCD_PORT_RW &= ~LCD_PIN_RW;</strong> //ustawiam stan wysoki na lini E przed wysłanie połowy bitu <strong>LCD_PORT_E |= LCD_PIN_E;</strong> //wysyłam starszą połowę bajtu, aby to zrobić robię przesunięcie o połowę czyli >>4 <strong>lcdSendHalfByte(data>>4);</strong> //ustawiam stan niski na E - zakończyłem przesyłanie pół bajtu <strong>LCD_PORT_E &= ~LCD_PIN_E;</strong> //przesyłam młodszą połowe bajtu <strong>LCD_PORT_E |= LCD_PIN_E; lcdSendHalfByte(data); LCD_PORT_E &= ~LCD_PIN_E;</strong> //czekam aż się znak obsłuży w wyświetlaczu <strong>_delay_us(120); }</strong> |
Kolejnym krokiem będzie powiedzenie wyświetlaczowi czy przesyłamy bajt danych związanych z kodem znaku czy komendą. Tą informacją sterujemy za pomocą lini RS. W zależności od tego czy jest to komenda ustawiamy stan niski lub stan wysoki dla kodu znaku.
1 2 3 4 5 6 7 8 9 10 11 |
//za pomocą lini rs informujemy czy przesyłamy komendę czy kod znaku <strong>void lcdWrite(uint8_t data, uint8_t isCommand){ if(isCommand){</strong> //dla komendy stan niski na lini RS <strong>LCD_PORT_RS &= ~LCD_PIN_RS; } else {</strong> //dla znaku stan wysoki na lini RS <strong>LCD_PORT_RS |= LCD_PIN_RS; } lcdSendByte(data); }</strong> |
Od razu utworzymy sobie funkcję wywołującą komendę czyszczenia wyświetlacza. Musimy przekazać komendę z kodem czyszczenia wyśweitlacza 0x01 będzie to po prostu jedynka na pierwszym bicie 0000001. Tutaj poczekamy nieco dłużej ponieważ wyświetlacz potrzebuje trochę czasu żeby się wyczyścić. Później trochę to usprawnimy.
1 2 3 4 5 |
<strong>void lcdClear(void) { lcdWrite(0x01, 1);</strong> //to oczekiwanie zmienimy na funkcję oczekiwania na zmianę flagi zajętości<strong> _delay_ms(4.9); }</strong> |
Inicjalizacja wyświetlacza
Nadszedł najtrudniejszy i zarazem najważniejszy moment czyli inicjalizacja wyświetlacza. Opis inicjalizacji możemy znaleźć w dokumentacji HD44780 i to co tutaj piszę nie jest jakąś tajemną wiedzą, wręcz przeciwnie bardzo łatwo dostępną. W dokumentacji możemy na przykład znaleźć takie strony
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<strong>void lcdInit(void) {</strong> //ustawiam piny na wyjścia <strong>LCD_DDR_RS |= LCD_PIN_RS; LCD_DDR_RW |= LCD_PIN_RW; LCD_DDR_E |= LCD_PIN_E; lcdDirOut();</strong> //ustawiam stan niski na piny sterujące czyli zeruję <strong>LCD_PORT_RS &= ~LCD_PIN_RS; LCD_PORT_RW &= ~LCD_PIN_RW; LCD_PORT_E &= ~LCD_PIN_E; LCD_PORT_4 &= ~LCD_PIN_4; LCD_PORT_5 &= ~LCD_PIN_5; LCD_PORT_6 &= ~LCD_PIN_6; LCD_PORT_7 &= ~LCD_PIN_7;</strong> //reset programu //czekam <strong>_delay_ms(2);</strong> //wysyłam komendę 8 bit mode <strong>LCD_PORT_E |= LCD_PIN_E; lcdSendHalfByte(0x03); LCD_PORT_E &= ~LCD_PIN_E; _delay_ms(1); LCD_PORT_E |= LCD_PIN_E; lcdSendHalfByte(0x03); //tryb 8 bit LCD_PORT_E &= ~LCD_PIN_E; _delay_ms(1); LCD_PORT_E |= LCD_PIN_E; lcdSendHalfByte(0x03); //tryb 8 bit LCD_PORT_E &= ~LCD_PIN_E; _delay_ms(1);</strong> //inicjalizacja //ustawiam tryb 4bit <strong>LCD_PORT_E |= LCD_PIN_E; lcdSendHalfByte(0x02); LCD_PORT_E &= ~LCD_PIN_E;</strong> <strong>lcdWrite(0x20|0x00|0x08|0x04 ,1); lcdWrite(0x08|0x04|0x00|0x00 ,1); lcdWrite(0x04|0x02|0x00 ,1); lcdClear();</strong> } |
Żeby zobaczyć jakiekolwiek działanie wyświetlacza pozostaje nam już tylko jedna funkcja do napisania, która pobierze tablicę znaków i wyśle do naszego wyświetlacza. Będzie to bardzo prosta funkcja ponieważ wszystko co potrzebujemy mamy już praktycznie napisane.
1 2 3 4 5 |
<strong>void lcdStr(char *str){ while(*str){ lcdWrite(*str++, 0); } }</strong> |
Funkcja po prostu pobiera tablicę kodów znaków i przesyła je w pętli za pomocą wcześniej napisanej funkcji lcdWrite. Po przesłaniu kodu znaku za pomocą inkrementacji przechodzimy do następnego znaku.
Pierwszy „Hello world”
Mamy już wszystko co potrzebne do wyświetlenia jakiegokolwiek napisu na wyświetlaczu. Przechodzimy teraz do pliku głównego naszego programu main.c i dodajemy kod, który napisaliśmy. Później w funkcji głównej inicjalizujemy wyświetlacz i przesyłamy jakąś tablicę znaków którą chcemy wyświetlić.
1 2 3 4 5 6 7 8 9 |
#include <avr/io.h> <strong>#include "lcd.c"</strong> int main(void) { <strong>lcdInit(); char tab[] = "Hello World!"; lcdStr(tab);</strong> } |
Mamy już podstawy, na jeśli wszystko zrobiłeś dobrze to teraz na wyświetlaczu masz napis Hello World!. To już coś ale przed nami wciąż dużo pracy do wykonania.
Po pierwsze trzeba usprawnić program żeby wykorzystywał linię RW i odczyt busyFlag czyli zamiast czekać dłuższy czas aż wyświetlacz obsłuży dane to pytać go czy już obsłużył dzięki czemu będzie działać znacznie szybciej.
Sterowanie kursorem, ponieważ na ten moment obsługa kolejnych linijek lub dłuższych tekstów może okazać się problematyczna. Warto napisać funkcję do przechodzenia do następnej linijki a także do wypełniania spacją odpowiednich przestrzeni żeby zrobić np wcięcie.
No i wisienką na torcie będzie dodawanie własnych znaków do pamięci wyświetlacza, aby był w stanie wyświetlić chociażby polskie znaki. Z drugiej strony jakby to dobrze rozegrać mając do dyspozycji praktycznie każdy piksel można stworzyć np prostą grę. Wystarczy trochę pomyśleć i można zrobić alternatywę dinozaura z chrome lub angry birds na wyświetlacz lcd, mikrokontroler i jeden microswitch.
Odczyt flagi zajętości
Jak już wyżej wspomniałem, wyświetlacz możemy podłączyć o obsłużyć na co najmniej dwa sposoby. W pierwszym nie wykorzystujemy linii RW tylko podpinamy ją pod GND i tylko wysyłamy dane. Przy permanentnym ustawieniu na zapis po wysłaniu komendy za każdym razem odczekujemy chwilę żeby mieć pewność że moduł wyświetlacza zakończył pracę i dopiero wtedy podejmujemy dalsze działania. Przy wykorzystaniu odczytu po prostu w pętli odpytujemy moduł czy jeszcze pracuje i od razu jak zwróci informację że już skończył przechodzimy do dalszych działań.
Zanim dojdziemy do napisania funkcji pobierającej flagę zajętości musimy przebrnąć przez kilka etapów, analogicznie jak przy zapisie. Najpierw ustawiamy linie D4-D7 na wejścia, dalej piszemy funkcję do pobrania połowy bajtu a dalej całego bajtu.
Zacznijmy od ustawienia pinów na wejścia, gdzieś na górze naszego pliku lcd.c
1 2 3 4 5 6 7 |
<strong>void lcdDirIn(void){</strong> //ustawiam porty na odczyt <strong> LCD_DDR4 &= ~LCD_PIN_4; LCD_DDR5 &= ~LCD_PIN_5; LCD_DDR6 &= ~LCD_PIN_6; LCD_DDR7 &= ~LCD_PIN_7; }</strong> |
Dalej analogicznie jak wcześniej tworzymy funkcję do odczytu połowy bajta.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
//funckcja do odczytu połowy bajta analogicznie jak w przypadku zapisu //ta funkcja będzie zwracać bajt z odczytanymi czterema bitami <strong>uint8_t lcdReadHalfByte(void){</strong> //ustawiam wszystkie bity na 0 <strong>uint8_t result = 0;</strong> //Sprawdzam stan na każdym z czterech bitów i //jeśli stan jest wysoki to ustawiam kolejny bit na 1 <strong>if((LCD_RPIN_4 & LCD_PIN_4)){ result |= (1<<0); } if((LCD_RPIN_5 & LCD_PIN_5)){ result |= (1<<1); } if((LCD_RPIN_6 & LCD_PIN_6)){ result |= (1<<2); } if((LCD_RPIN_7 & LCD_PIN_7)){ result |= (1<<3); } return result; }</strong> |
Jako że nie używamy jeszcze makra dodajemy do pliku nagłówkowego lcd.h cztery definicje użyte w powyższej funkcji. Koniecznie zgodnie z portami do których są podpięte wymienione piny.
1 2 3 4 |
<strong>#define LCD_RPIN_4 PINC #define LCD_RPIN_5 PINC #define LCD_RPIN_6 PINC #define LCD_RPIN_7 PINC</strong> |
Teraz analogicznie jak wcześniej tworzymy funkcję do odczytu całego bajta.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
//funckja do odczytu całego bajta zwracająca go <strong>uint8_t lcdReadByte(void) {</strong> // ustawiam wszystkie bity na 0 <strong>uint8_t result = 0;</strong> //ustawiam piny na wejście <strong>lcdDirIn();</strong> //ustawiam linię rw na odczyt czyli 1 <strong>LCD_PORT_RW |= LCD_PIN_RW;</strong> <strong>LCD_PORT_E |= LCD_PIN_E;</strong> //odczyt starszej części bajtu <strong>result |= (lcdReadHalfByte() << 4); LCD_PORT_E &= ~LCD_PIN_E;</strong> <strong>LCD_PORT_E |= LCD_PIN_E;</strong> //odczyt młodszej części bajtu <strong>result |= (lcdReadHalfByte()); LCD_PORT_E &= ~LCD_PIN_E;</strong> //zwracam odczytany bajt <strong> return result; }</strong> |
Na tym etapie odczyt bajta danych mamy już opanowany pozostaje tylko odczytać flagę zajętości. W zasadzie jest on potrzebna do odczekania aż wyświetlacz będzie gotowy na kolejne akcje. Napiszmy funkcję, która będzie w pętli odczytywać flagę zajętości aż się nie zwolni.
1 2 3 4 5 6 7 8 |
<strong>void waitBusy(void){ uint8_t busy = 1; LCD_PORT_RS &= ~LCD_PIN_RS; do { busy = (lcdReadByte() & (1<<7)); } while( busy); }</strong> |
I teraz skoro już mamy funkcję czekającą należy podmienić miejsca gdzie czekamy określoną ilość czasu na tę funkcję. (poza inicjalizacją gdzie wyświetlacz nie jest jeszcze gotowy do przesyłania tej informacji). Umieszczamy np przy wysyłaniu kolejnych bajtów.
1 2 3 4 5 6 7 8 9 10 11 |
void lcdSendByte(char data ){ lcdDirOut(); LCD_PORT_RW &= ~LCD_PIN_RW; LCD_PORT_E |= LCD_PIN_E; lcdSendHalfByte(data>>4); LCD_PORT_E &= ~LCD_PIN_E; LCD_PORT_E |= LCD_PIN_E; lcdSendHalfByte(data); LCD_PORT_E &= ~LCD_PIN_E; <strong> waitBusy();</strong> } |
I to tyle na ten moment w temacie odczytywania danych z wyświetlacza.
Ustawianie kursora na wyświetlaczu ze sterownikiem HD44780
Na ten moment nasz wyświetlacz powinien działać całkiem fajnie gdy chcemy wyświetlić bardzo krótki napis jednak przy próbie wyświetlenia dłuższego napisu, np takiego, który nie mieści się w pierwszej linijce może powstać problem. Spróbowałem na moim wyświetlaczu 4×20 wyświetlić dłuższy tekst. Taki oto kod wprowadziłem do mojej funkcji głównej programu w pliku main.c
1 2 3 4 5 6 |
int main(void) { <strong>lcdInit(); char tab[] = "Hello World! 1234546 789456153 987654321 555 444"; lcdStr(tab);</strong> } |
Efekt był dość nieoczekiwany. Co bardziej wnikliwi powinni zaobserwować sytuację, w której po zapełnieniu pierwszej linijki, kursor przeskakuje do trzeciej a później wraca do drugiej.
Napiszemy teraz kilka funkcji, które pomogą nam ustawić kursor dokładnie tam gdzie tego chcemy. Zacznijmy może od najprostszej, czyli ustawienia kursora na początku w dowolnym momencie.
Jak ustawić kursor na początku?
Jest to niezwykle proste na tym etapie, ponieważ wystarczy przesłać do naszego wyświetlacza tylko odpowiednią komendę. Funkcja do ustawiania kursora na początku będzie wyglądać w sposób następujący
1 2 3 4 5 |
//funkcja, która ustawia kursor na początku <strong>void lcdCursorStart(void) {</strong> //wysyłam komendę ustawiającą kursor na początku <strong> lcdWrite(0x02,1); }</strong> |
Komenda ustawiająca kursor na początku ma kod 0x02, generalnie w bajcie chodzi o ustawienie bitów na odpowiednie komendy i tym sposobem możemy przesłać ze dwie lub trzy komendy na raz. To by mogło całkiem sporo wyjaśniać co się działo przy inicjalizacji. Te kody niewiele nam mówią i prawdę mówiąc jak rozpracowywałem ten temat miałem z nimi pewien problem, w części źródeł nie było opisane co jest co, więc myślę że dobrym pomysłem będzie dopisanie definicji komend do pliku nagłówkowego.
Komendy wyświetlacza HD44780
Przejdźmy teraz do pliku lcd.h gdzie definiowaliśmy między innymi porty i piny a następnie dopiszmy tam nasze komendy wyświetlacza. Pierwsze z nich już doskonale znamy. Jedną z nich czyścimy wyświetlacz a drugą ustawiamy kursor na początku. Będzie jeszcze kilka komend między innymi do określenia czy kursor ma być widoczny co ma migać, jak przesunąć kursor i kilka innych określających jak ma się zachować nasz wyświetlacz. Część z nich używamy razem tak jak miało to miejsce przy inicjalizacji. Za chwilę zaktualizujemy sobie tamten kod. Poniżej kody poszczególnych komend. Mam nadzieję że większość z nich da się skojarzyć po nazwie definicji.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
//Komendy wyświetlacza <strong>#define LCD_CMD_CLEAR 0x01 #define LCD_CMD_RETURN_HOME = 0x02 #define LCD_CMD_ENTRY_MODE_SET 0x04 #define LCD_CMD_DISPLAY_CONTROL 0x08 #define LCD_CMD_CURSOR_SHIFT 0x10 #define LCD_CMD_FUNCTION_SET 0x20 #define LCD_CMD_SET_CGRAM_ADDRESS 0x40 #define LCD_CMD_SET_DDRAM_ADDRESS 0x80 #define LCD_CMD_ENTRY_INCREMENT 0x02 #define LCD_CMD_ENTRY_DECREMENT 0x00 #define LCD_CMD_ENTRY_SHIFT 0x01 #define LCD_CMD_ENTRY_NO_SHIFT 0x00 #define LCD_CMD_DISPLAY_ON 0x04 #define LCD_CMD_DISPLAY_OFF 0x00 #define LCD_CMD_CURSOR_ON 0x02 #define LCD_CMD_CURSOR_OFF 0x00 #define LCD_CMD_BLINK_ON 0x01 #define LCD_CMD_BLINK_OFF 0x00 #define LCD_CMD_DISPLAY_MOVE 0x08 #define LCD_CMD_CURSOR_MOVE 0x00 #define LCD_CMD_MOVE_RIGHT 0x04 #define LCD_CMD_MOVE_LEFT 0x00 #define LCD_CMD_8BIT_MODE 0x10 #define LCD_CMD_4BIT_MODE 0x00 #define LCD_CMD_2LINE 0x08 #define LCD_CMD_1LINE 0x00 #define LCD_CMD_5x10DOTS 0x04 #define LCD_CMD_5x8DOTS 0x00</strong> |
Jak widać możliwości tutaj mamy całkiem sporo. Spróbujmy sobie może teraz przypomnieć miejsce do tej pory niezbyt objaśnione przy inicjalizacji wyświetlacza gdzie wykorzystaliśmy część z powyższych kodów. Na ten moment kod powinien wyglądać tak w tamtym miejscu.
1 2 3 4 5 6 7 8 9 |
//inicjalizacja //ustawiam tryb 4bit LCD_PORT_E |= LCD_PIN_E; position LCD_PORT_E &= ~LCD_PIN_E; <strong>lcdWrite(0x20|0x00|0x08|0x04 ,1); lcdWrite(0x08|0x04|0x00|0x00 ,1); lcdWrite(0x04|0x02|0x00 ,1);</strong> lcdClear(); |
Spróbujmy teraz nieco zmodyfikować ten kod aby był bardziej czytelny i bardziej było wiadomo o co chodzi.
1 2 3 4 5 6 7 8 9 10 11 |
//inicjalizacja //ustawiam tryb 4bit LCD_PORT_E |= LCD_PIN_E; lcdSendHalfByte(LCD_CMD_4BIT_MODE); LCD_PORT_E &= ~LCD_PIN_E; //ustawienie przesyłania danych i wyświetlacza <strong>lcdWrite(LCD_CMD_FUNCTION_SET|LCD_CMD_4BIT_MODE|LCD_CMD_2LINE|LCD_CMD_5x10DOTS ,1);</strong> //ustawiam widoczność na wyświetlaczu, kursor na nie widoczny i miganie pozycji kursora <strong>lcdWrite(LCD_CMD_DISPLAY_CONTROL|LCD_CMD_DISPLAY_ON|LCD_CMD_CURSOR_OFF|LCD_CMD_BLINK_OFF ,1);</strong> //ustawiam kursor żeby się przesuwał o jeden w prawo bez przesuwania reszty <strong>lcdWrite(LCD_CMD_ENTRY_MODE_SET|LCD_CMD_ENTRY_INCREMENT|LCD_CMD_ENTRY_NO_SHIFT ,1);</strong> lcdClear(); |
Jak ustawić kursor na wyświetlaczu LCD 4×20?
W końcu nadszedł moment na napisanie funkcji dzięki, której będziemy w stanie umieścić kursor w dowolnym miejscu. Po pierwsze musimy znać adresy początku każdej z linii wyświetlacza. W tym miejscu wyjaśni się dlaczego na moim wyświetlaczu 4×20 po zapełnieniu 1linii przeskakuje do trzeciej a później wraca do drugiej. Teraz w pliku nagłówkowym zdefiniujmy te adresy.
1 2 3 4 5 |
//początek każdej z linii <strong>#define LCD_LINE1 0x00 #define LCD_LINE2 0x40 #define LCD_LINE3 0x14 #define LCD_LINE4 0x54</strong> |
Przy okazji powyższego warto pamiętać że 0x14 w to nie 14 a 4 + 16 czyli 20. Ten adres by wyjaśniał przejście do trzeciej a nie drugiej linii.
Skoro mamy już kody linii, które trzeba wysłać, to wystarczy dodać do tego pozycję w linii i otrzymamy pozycję. Napiszmy teraz bardzo podstawową funkcję kontroli pozycji kursora.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<strong>void lcdSetCursorPosition(uint8_t x, uint8_t y){ uint8_t lineStart = 0;</strong> //sprawdzam, którą linię wybrano i ustawiam adres startowy <strong>switch(y){ case 1 : lineStart = LCD_LINE1; break; case 2 : lineStart = LCD_LINE2; break; case 3 : lineStart = LCD_LINE3; break; case 4 : lineStart = LCD_LINE4; break; }</strong> //Wysyłam komendę ustawienia adresu //do adresu linii dodaję pozycję w linii <strong> lcdWrite(LCD_CMD_SET_DDRAM_ADDRESS|(lineStart|x), 1); }</strong> |
No i mamy to. Oczywiście tę bibliotekę dało by się znacznie usprawnić i rozbudować na przykład tak żeby obsługiwała przy okazji inne typy wyświetlaczy. Pewnie za jakiś czas to zrobię i wrzucę na gita do pobrania, a tymczasem zachęcam do usprawnień we własnym zakresie 🙂
Obsługa własnych znaków wyświetlacza z modułem HD44780 za pomocą mikrokontrolera AVR Atmega88PA
Miałem w tym miejscu napisać że została nam do obsłużenia ostatnia kwestia ale jak się okaże pozostaje ich jeszcze kilka. W każdym razie skupimy się teraz na dodaniu własnego znaku. Niestety jednocześnie możemy dodać ich tylko 8 do wyświetlacza, ale chyba nic nie stoi na przeszkodzie żeby te znaki podmienić według potrzeb np w innych komunikatach wyświetlacza.
Zasada tworzenia takiego znaku nie jest zbyt skomplikowana. Mamy zazwyczaj macierz pikseli 5×8 czyli osiem rzędów po 5px. I w tym zakresie możemy zaprojektować dowolny znak, który możemy zapisać do wyświetlacza a później go wykorzystywać wedle uznania. Aby zapisać taki znak będziemy potrzebowali przesłać 8 bajtów danych gdzie znaczenie będą miały ostatnie 5 bitów. W tym przypadku jedynka będzie oznaczała zapalony bit a 0 zgaszony.
Dobrze będzie jeszcze opracować pobieranie pozycji kursora żeby pobrać pozycję, wysłać znak i postawić kursor tam gdzie stał. W tym przykładzie spróbujemy sobie zrobić Polski znak „ą”, którego raczej nie uświadczymy wśród standardowych znaków wyświetlacza LCD. Poniżej rozpisałem poszczególne bajty znaku, które będziemy musieli przesłać.
000 00000 – 0
000 00000 – 0
000 01110 – 14
000 00001 – 1
000 01111 – 15
000 10001 – 17
000 01111 – 15
000 00010 – 2
Chcielibyśmy pobrać współrzędne kursora, aby to zrobić potrzebowalibyśmy funkcji, która zwracałaby dwie wartości, albo dwóch funkcji. Ewentualnie mogła by być to tablica dwuelementowa. Jednak jest to idealny moment na stworzenie struktury Pozycja zawierającej rekord i pozycję w rekordzie dla kursora. Zacznijmy więc od zdefiniowania struktury Position w pliku nagłówkowym lcd.h
1 2 3 |
<strong>typedef struct { uint8_t x,y; } Position;</strong> |
Teraz gdy już mamy strukturę napiszemy funkcję, która zwróci nam pozycję kursora. Do tej pory już ustaliliśmy całkiem sporo, a między innymi adresy początku każdej linii, bo adresy znaków w takim wyświetlaczu idą po kolei z różnicą pomiędzy rekordami. Jak wcześniej ustaliliśmy po pierwszy rekordzie jest trzeci a po trzecim jest drugi. Dlatego odczytujemy adres kursora i korygujemy jego współrzędne o adresy początku linii. W ten sposób uzyskamy nr linii i pozycję w tej linii. Patrz kod poniżej.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//odczytuję pozycję aktualną kursora <strong>Position lcdGetPosition(void){ Position position; position.x = lcdReadByte(); position.y = 1; if(position.x >= LCD_LINE4){ position.x -= LCD_LINE4; position.y = 4; } else if(position.x >= LCD_LINE2){ position.x -= LCD_LINE2; position.y = 2; } else if(position.x >= LCD_LINE3){ position.x -= LCD_LINE3; position.y = 3; } return position; }</strong> |
Teraz to mam już wszystko co potrzeba do stworzenia znaku, oprócz oczywiście najważniejszej funkcji, która to robi. Będzie to funkcja pobierająca tablicę bajtów znaku (tak jak rozpisałem to powyżej) oraz pozycję w pamięci wyświetlacza. Mamy do dyspozycji 64 bajty pamięci czyli 8 pozycji.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<strong>void lcdCreateChar(uint8_t *Data, uint8_t pos) { if(pos < 0) return; if(pos >= 8) return; Position position = lcdGetPosition(); uint8_t i; lcdWrite(LCD_CMD_SET_CGRAM_ADDRESS|(pos<<3), 1); for(i = 0; i < 8; i++) { lcdWrite(Data[i],0); } lcdSetCursorPosition(position.x, position.y); }</strong> |
Na początku sprawdzam czy przypadkiem nie wychodzimy poza zakres pozycji znaku w pamięci. Później pobieram pozycję kursora i zapisuję ją do zmiennej. Dalej wysyłam komendę z pozycją w pamięci po czym przesyłam po kolei 8 bajtów znaku. Na koniec ustawiam pozycję kursora tam gdzie był i mamy to.
Na koniec wypadałoby jeszcze pokazać przykład z użyciem tego, więc przechodzimy do funkcji głównej programu w main.c i dodajemy sobie dwie linijki kodu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
int main(void) { lcdInit(); char tab[] = "Hello World! "; lcdStr(tab); lcdSetCursorPosition(4,2); char tab2[] = "test 2"; lcdStr(tab2); //Dodaję do wyświetlacza kod znaku "ą" <strong>uint8_t charData[] = {0,0,14,1,15,17,15,2}; lcdCreateChar(charData, 0x01);</strong> //Wyświetlam na końcu znak "ą" <strong>char tab3[] = "\x01"; lcdStr(tab3);</strong> } |
No i mamy to na wyświetlaczu. W tym miejscu miałem zakończyć ale pomyślałem że warto jeszcze napisać funkcję wyświetlającą liczby, ponieważ używam tego wyświetlacza głównie do odczytu wartości z jakichś czujników itd, w sumie głównie do wyświetlania liczb mi służy.
Jak wyświetlić liczbę na wyświetlaczu LCD z modułem HD44780?
Spróbujemy jeszcze na koniec napisać prostą funkcję analogiczną do lcdStr(char *str) tylko że pobierającą i wyświetlającą liczby całkowite. Do napisania funkcji wyświetlającej liczby zmienno przecinkowe zapraszam już do pracy samodzielnej w ramach ćwiczenia.
1 2 3 4 |
<strong>void lcdInt(int number) { char bufor[17]; lcdStr(itoa(number, bufor, 10)); }</strong> |
Poszedłem trochę na łatwiznę i wykorzystałem funkcję wbudowaną itoa() pobierającą trzy argumenty. Pierwszy argument to liczba, drugi to adres tablicy bufora, do której chcemy załadować znaki i trzeci jakiej postaci chcemy liczbę. Możemy podać 10 czyli liczby dziesiętne a możemy też dać 16 dla szesnastkowych.
Wykorzystanie w funkcji głównej programu jest niezwykle proste i analogiczne do tego jak wyświetlamy po prostu łańcuch znaków.
1 2 3 4 5 6 7 |
int main(void) { ... lcdStr(tab3); <strong>lcdSetCursorPosition(1,3); lcdInt(1940);</strong> } |
Koniec
To tyle na dzisiaj. Chyba z tydzień rozpracowywałem ten temat 🙂 Oczywiście tę bibliotekę można by znacznie ulepszyć. Wypadałoby na przykład dodać obsługę innych typów wyświetlaczy jak te 2×16. Można by też znacznie zoptymalizować ten kod i sprawić by wyświetlał się tylko potrzebny kod, albo też obsłużyć podpięcie linii RW do GND bez tych wszystkich funkcji odczytu. Myślę jednak że rozpracowaliśmy tutaj wszystko co najważniejsze.
No i na koniec co udało się uzyskać pisząc ten artykuł.
Jeśli masz jakieś sugestie pisz śmiało.
Ćwiczenie
A jeśli chcesz sobie poćwiczyć to dobry moment żeby do tego projektu zapiąć jeszcze buzzer i jakiś microswitch, wykorzystać timery i napisać jakąś grę. Chociażby przesuwające się przeszkody i ich przeskakiwanie a po przegranej dowolny dźwięk z buzzera.